Modifying the VGA palette

Overview

This article focuses on VGA palette manipulation (256 colors) and some special effects that can be accomplished in indexed-color graphic modes. It doesn't explain how the default EGA/VGA palette is generated or how CGA colors work (the intensity/yellow/burst bits - you may check out the PCX specifications for a quick explanation of that.)

Basic DAC Operations

Working with indexed colors

VGA modes are "indexed color modes," as opposed to modern "direct color modes." It means that instead of directly picking a Red-Green-Blue color triplet and applying it to a single pixel on the screen, we refer to an index in a predefined palette instead. You may think of the palette as a swatch or a collection of numbered empty slots where you can put whatever color you'd like.

The length of the palette depends on the graphic mode: in SCREEN 7, 8 or 9 the palette is only 16 slots long (0 to 15,) while it is 256 slots long in SCREEN 13 (0 to 255.) It is a good idea to set index 0 to black as the video buffer is cleared with zeros (thus resulting in a black screen.)

Just like direct color modes, each attribute in the palette is composed of three primary colors: Red, Green and Blue (in that order.) In standard VGA, each RGB component is defined as a 6-bit intensity ranging from 0 (mute) to 63 (very intense) for a grand total of 262,144 different RGB combinations.

The resolution of each primary can be cranked up in VESA and SVGA mode. Modern hardware usually defines the intensity of each component with a full byte (ranging from 0 to 255) with some engines and graphic libraries going one step further by using normalized components (floating-point values between 0.0 and 1.0) to unify the color definition process.

The big takeaway here is that the VGA offers a paint-by-the-number system, where colors are displayed on the screen using an index number referring to the palette, rather than direct-color selection; some advanced palette techniques will either change colors within the palette without changing the numbers, while others will keep the palette intact and modify numbers as needed.

That's all you've got?

There are only two built-in color manipulation instructions in QuickBASIC: PALETTE (which tweaks one attribute with RGB triplets), and PALETTE USING (which tweaks the entirety of the 256-color palette via a 1024 bytes long array.) Virtually no one uses those functions because honestly, they are a bit shit (slow, cumbersome, not flexible enough;) thus, finding example code for these two abominations is borderline impossible, so allow me to take one for the team before we move on:

DECLARE SUB setColor (attr AS INTEGER, r AS INTEGER, g AS INTEGER, b AS INTEGER)
DECLARE SUB setPalette ()

' Enter Mode 13 (320x200, 256 colors)
SCREEN 13

' Display the whole color palette
FOR y% = 0 TO 15
  FOR x% = 0 TO 15
    LINE (x% * 10, y% * 10)-STEP(9, 9), x% + (y% * 16), BF
  NEXT x%
NEXT y%

' Wait for user input
COLOR 15: LOCATE 3, 25: PRINT "Press a key - 1": SLEEP

setColor 254, 0, 29, 63 ' Set attribute 254 to teal
setColor 255, 63, 34, 0 ' Set attribute 255 to orange

' Wait for user input
COLOR 15: LOCATE 3, 25: PRINT "Press a key - 2": SLEEP

' Modify the whole palette
setPalette

' {attr} is the color attribute index, must be 0 - 255
' {r}, {g} and {b} are Red, Green and Blue intensity, must be 0 - 63
SUB setColor (attr AS INTEGER, r AS INTEGER, g AS INTEGER, b AS INTEGER)
  PALETTE attr, r + g * 256& + b * 65536
END SUB

' Fill the whole palette with 64 shades of red, green, blue and gray
SUB setPalette
  DIM full(0 TO 255) AS LONG

  FOR i% = 0 TO 63
    full(i%) = i%               ' Red   (attributes   0 to 63)
    full(64 + i%) = i% * 256&   ' Green (attributes  64 to 127)
    full(128 + i%) = i% * 65536 ' Blue  (attributes 128 to 191)
    full(192 + i%) = full(i%) + full(64 + i%) + full(128 + i%) ' Gray
  NEXT i%

  PALETTE USING full(0)
END SUB

Setting up a custom palette

Every tutorial out there will tell you the exact same thing: "Why don't you feed your colors to the DAC (Digital/Analog Converter)? It's super fast!" And I'm going to tell you the exact same thing too. But how do we do that exactly? First things first, let's have a look at the different ports we'll be meddling with:

The Palette Mask register is used to mask the attribute indices we can access via the DAC Control Read register and DAC Control Write register. To make sure we can access all attributes, we send it the value &HFF (you only need to do this once as soon as you change graphic mode.) DAC Control Read register and DAC Control Write register take the attribute index we want to access for reading and writing respectively. To get or set the color primaries (RGB triplet,) we use the DAC Control Data register three times in a row (once for red, once for green, and once more for blue.) This port is read and write. Sending information directly to the DAC is so easy and common I still have to see one QuickBASIC program using the built-in PALETTE function instead:

DECLARE SUB setColor (attr AS INTEGER, r AS INTEGER, g AS INTEGER, b AS INTEGER)
DECLARE SUB setPalette ()

CONST DACPELMask = &H3C6  ' DAC (Digital/Analog Converter) PEL Mask register
CONST DACWrite = &H3C8    ' DAC Write Port
CONST DACData = &H3C9     ' DAC Data Port

' Enter Mode 13 (320x200, 256 colors)
SCREEN 13
OUT DACPELMask, &HFF      ' Mask all attributes

' Display the whole color palette
FOR y% = 0 TO 15
  FOR x% = 0 TO 15
    LINE (x% * 10, y% * 10)-STEP(9, 9), x% + (y% * 16), BF
  NEXT x%
NEXT y%

' Wait for user input
COLOR 15: LOCATE 3, 25: PRINT "Press a key - 1": SLEEP

' Tweak attributes 254 and 255 to teal and orange
setColor 254, 0, 29, 63
setColor 255, 63, 34, 0

' Wait for user input
COLOR 15: LOCATE 3, 25: PRINT "Press a key - 2": SLEEP

' Modify the whole palette
setPalette

' {attr} is the color attribute index, must be 0 - 255
' {r}, {g} and {b} are Red, Green and Blue intensity, must be 0 - 63
SUB setColor (attr AS INTEGER, r AS INTEGER, g AS INTEGER, b AS INTEGER)
  OUT DACWrite, attr    ' Select attribute
  OUT DACData, r        ' Feed red intensity
  OUT DACData, g        ' Feed green intensity
  OUT DACData, b        ' Feed blue intensity
END SUB

' Fill the whole palette with 64 shades of red, green, blue and gray
SUB setPalette
  ' Attributes 0 to 63: red shades
  OUT DACWrite, 0   ' Start writing at 0
  FOR i% = 0 TO 63
    OUT DACData, i% ' Red
    OUT DACData, 0  ' Green
    OUT DACData, 0  ' Blue
  NEXT i%

  ' Note: there's no need to explicitly send the next
  ' attribute number to modify to DACWrite because the
  ' card kept track of it for us. We've made 3x64 writes
  ' to DACData so far, next attribute will be number 64.

  ' Attributes 64 to 127: green shades
  FOR i% = 0 TO 63
    OUT DACData, 0  ' Red
    OUT DACData, i% ' Green
    OUT DACData, 0  ' Blue
  NEXT i%

  ' Attributes 128 to 191: blue shades
  FOR i% = 0 TO 63
    OUT DACData, 0  ' Red
    OUT DACData, 0  ' Green
    OUT DACData, i% ' Blue
  NEXT i%

  ' Attributes 192 to 255: gray shades
  FOR i% = 0 TO 191
    OUT DACData, i% ' Red = Green = Blue
  NEXT i%
END SUB

Like explained previously, we specify what color we want to access by feeding its index to the DAC Control Write register, and then we call the DAC Control Data register three times in a row to pass each primary and set the palette attribute. Four noteworthy things: one - we never explicitly state which primary color we're feeding the Data register because the DAC is smart enough to keep track of what data still needs to be provided. Two - the DAC will reset its queue upon invoking the DAC Control Write register (every time we send data to &H3C8, the DAC will assume the next call to &H3C9 will be red, and then green, and blue.) Three - the attribute index automatically increments after the DAC Control Data register has been called three times, so we don't even have to specify the color index within the loop! Four - changes made to the palette are instantaneous, there's no need to change the data inside the video buffer for the color change to be visible.

Reading palette information

QuickBASIC has no built-in function to retrieve palette information. Hopefully, we can easily code one using the DAC Control Read register! This register is the counter-part of the DAC Control Write register and works the exact same way too: invoking &H3C7 forces the DAC to expect red in next &H3C9 call, and the color index increments with every three consecutive calls to &H3C9. And with that, we can retrieve RGB intensities of any given attribute:

DECLARE SUB getColor (attr AS INTEGER, r AS INTEGER, g AS INTEGER, b AS INTEGER)
DECLARE SUB setColor (attr AS INTEGER, r AS INTEGER, g AS INTEGER, b AS INTEGER)

CONST DACPELMask = &H3C6 ' DAC (Digital/Analog Converter) PEL Mask register
CONST DACRead = &H3C7    ' DAC Read Port/DAC State register
CONST DACData = &H3C9    ' DAC Data Port

DIM r AS INTEGER, g AS INTEGER, b AS INTEGER

' Enter mode 13 (320x200, 256 colors)
SCREEN 13
OUT DACPELMask, &HFF     ' Mask all attributes

' Display the whole color palette
FOR y% = 0 TO 15
  FOR x% = 0 TO 15
    LINE (x% * 8, y% * 8)-STEP(7, 7), x% + (y% * 16), BF
  NEXT x%
NEXT y%

' Wait for user input
COLOR 15: LOCATE 3, 25: PRINT "Press a key - 1": SLEEP

' Get color 254 and 255
getColor 254, r, g, b
LOCATE 4, 25: PRINT "254:"; r; g; b
getColor 255, r, g, b
LOCATE 6, 25: PRINT "255:"; r; g; b

' Tweak attributes 254 and 255 to teal and orange
setColor 254, 0, 29, 63
setColor 255, 63, 34, 0

' Get color 254 and 255 again
getColor 254, r, g, b
LOCATE 5, 25: PRINT "254:"; r; g; b
getColor 255, r, g, b
LOCATE 7, 25: PRINT "254:"; r; g; b

' {attr} is the color attribute index, must be 0 - 255
' {r}, {g} and {b} are Red, Green and Blue intensity
SUB getColor (attr AS INTEGER, r AS INTEGER, g AS INTEGER, b AS INTEGER)
  OUT DACRead, attr ' Select attribute
  r = INP(DACData)  ' Read red intensity
  g = INP(DACData)  ' Read green intensity
  b = INP(DACData)  ' Read blue intensity
END SUB

' {attr} is the color attribute index, must be 0 - 255
' {r}, {g} and {b} are Red, Green and Blue intensity, must be 0 - 63
SUB setColor (attr AS INTEGER, r AS INTEGER, g AS INTEGER, b AS INTEGER)
  OUT DACWrite, attr  ' Select attribute
  OUT DACData, r      ' Feed red intensity
  OUT DACData, g      ' Feed green intensity
  OUT DACData, b      ' Feed blue intensity
END SUB

Cool effects

Color filtering

Making a distinction between palette transformation (the actual color data) and DAC operations (programming the VGA card to apply a specific palette) will help us create flexible palette fading routines, so we can fade to black (or any other color) and create "flashing" effects, similar to the screen turning red in DOOM when the player gets shot. So first things first, let's rewrite our routines so DAC operations are isolated:

CONST DACPELMask = &H3C6         ' DAC (Digital/Analog Converter) PEL Mask register
CONST DACRead = &H3C7            ' DAC Read Port/DAC State register
CONST DACWrite = &H3C8           ' DAC Write Port/DAC State register
CONST DACData = &H3C9            ' DAC Data Port

DIM clr(767) AS INTEGER          ' Palette buffer

SCREEN 13                        ' Enter mode 13
OUT DACPELMask, &HFF             ' Mask all attributes

' Read original palette, {array} must be (0 to 767)
SUB paletteGet (array() AS INTEGER)
  OUT DACRead, 0                 ' Select first attribute
  FOR i% = 0 TO 767              ' Read 256 colors (768 components)
    array(i%) = INP(DACData)     ' Red, green and blue, queued
  NEXT i%
END SUB

' Set palette, {array} must be (0 to 767)
SUB paletteSet (array() AS INTEGER)
  OUT DACWrite, 0                ' Select first attribute
  FOR i% = 0 TO 767              ' Set all 768 components
    OUT DACData, array(i%)       ' Red, green and blue, queued
  NEXT i%
END SUB

Now, let's come up with two routines to convert palette colors to grayscale and sepia:

' Convert palette to Grayscale
SUB toneGrayscale (array() AS INTEGER)
  DIM gray AS INTEGER

  FOR i% = 0 TO 767 STEP 3
    gray = array(i%) * .299 + array(i% + 1) * .587 + array(i% + 2) * .114
    array(i%) = gray        ' Red component
    array(i% + 1) = gray    ' Green component
    array(i% + 2) = gray    ' Blue component
  NEXT i%
END SUB

' Convert palette to sepia
SUB toneSepia (array() AS INTEGER)
  DIM r AS INTEGER, g AS INTEGER, b AS INTEGER

  FOR i% = 0 TO 767 STEP 3
    r = array(i%) * .393 + array(i% + 1) * .769 + array(i% + 2) * .189
    g = array(i%) * .349 + array(i% + 1) * .686 + array(i% + 2) * .168
    b = array(i%) * .272 + array(i% + 1) * .534 + array(i% + 2) * .131
    IF (r > 63) THEN array(i%) = 63 ELSE array(i%) = r
    IF (g > 63) THEN array(i% + 1) = 63 ELSE array(i% + 1) = g
    IF (b > 63) THEN array(i% + 2) = 63 ELSE array(i% + 2) = b
  NEXT i%
END SUB

Palette fading

If we think about it for a second, both fade in and out routines do the exact same thing: they gradually modify each color of the current palette so they become a color of the target palette. So why not write two different routines when an all-purpose routine could do the job?

'
' Transition from current palette to target palette
'
SUB paletteTrans (target() AS INTEGER, duration AS SINGLE)
  DIM source(767) AS INTEGER
  DIM starts AS SINGLE, grad AS SINGLE

  paletteGet source()                          ' Capture current palette
  FOR i% = 0 TO 767                            ' and compute difference
    target(i%) = target(i%) - source(i%)       ' (delta) between source
  NEXT i%                                      ' and target.

  starts = TIMER                               ' Get current time

  OUT DACWrite, 0                              ' Select first attribute
  DO
    grad = (TIMER - starts) / duration         ' Change for this frame
    IF (grad > 1) THEN grad = 1                ' Cap change
    FOR i% = 0 TO 767
      OUT DACData, CINT(source(i%) + (target(i%) * grad))
    NEXT i%
  LOOP WHILE (grad < 1)
END SUB

Adding previous pieces of code to the mix, here's what it would look like:

CONST DACPELMask = &H3C6         ' DAC (Digital/Analog Converter) PEL Mask register
CONST DACRead = &H3C7            ' DAC Read Port/DAC State register
CONST DACWrite = &H3C8           ' DAC Write Port/DAC State register
CONST DACData = &H3C9            ' DAC Data Port

DIM clr(767) AS INTEGER          ' Palette buffer

SCREEN 13                        ' Enter mode 13
OUT DACPELMask, &HFF             ' Mask all attributes

' Display the whole color palette
FOR y% = 0 TO 15
  FOR x% = 0 TO 15
    LINE (x% * 10, y% * 10)-STEP(9, 9), x% + (y% * 16), BF
  NEXT x%
NEXT y%

paletteGet clr()      ' Capture current palette
toneGrayScale clr()   ' Convert to grayscale
paletteTrans clr(), 3 ' Fade to grayscale

SUB paletteGet (array() AS INTEGER)
  OUT DACRead, 0                 ' Select first attribute
  FOR i% = 0 TO 767              ' Read 256 colors (768 components)
    array(i%) = INP(DACData)     ' Red, green and blue, queued
  NEXT i%
END SUB

SUB toneGrayscale (array() AS INTEGER)
  DIM gray AS INTEGER

  FOR i% = 0 TO 767 STEP 3
    gray = array(i%) * .299 + array(i% + 1) * .587 + array(i% + 2) * .114
    array(i%) = gray        ' Red component
    array(i% + 1) = gray    ' Green component
    array(i% + 2) = gray    ' Blue component
  NEXT i%
END SUB

SUB paletteTrans (target() AS INTEGER, duration AS SINGLE)
  DIM source(767) AS INTEGER
  DIM starts AS SINGLE, grad AS SINGLE

  paletteGet source()                          ' Capture current palette
  FOR i% = 0 TO 767                            ' and compute difference
    target(i%) = target(i%) - source(i%)       ' (delta) between source
  NEXT i%                                      ' and target.

  starts = TIMER                               ' Get current time

  OUT DACWrite, 0                              ' Select first attribute
  DO
    grad = (TIMER - starts) / duration         ' Change for this frame
    IF (grad > 1) THEN grad = 1                ' Cap change
    FOR i% = 0 TO 767
      OUT DACData, CINT(source(i%) + (target(i%) * grad))
    NEXT i%
  LOOP WHILE (grad < 1)
END SUB

Palette shifting

We know that once a palette attribute is modified, the result appears instantly on the screen. The same way, palette shifting (also called "color cycling") relies entirely on the DAC and is (almost) instantaneous since it doesn't require writing to (or reading from) video memory. This can be used to "animate" fire, smoke, water ripples, moving mist, twinkling stars, etc. In short, a few consecutive attributes are filled with a mirrored color gradient and are "shifted" at a regular time interval. Here's a dirty way to do it:

CONST DACPelMask = &H3C6
CONST DACRead = &H3C7
CONST DACWrite = &H3C8
CONST DACData = &H3C9

' init mode 13
SCREEN 13
OUT DACPelMask, &HFF

' draw palette
FOR y2% = 0 TO 15
  FOR x2% = 0 TO 15
    LINE (95 + x2% * 8, 35 + y2% * 8)-STEP(7, 7), x2% + (y2% * 16), BF
  NEXT x2%
NEXT y2%

' go
DO
  palShift 32, 24
LOOP UNTIL LEN(INKEY$)

''
'' Shifting {shftCount} palette attributes, starting at index {shftStart}
''
SUB palShift (shftStart AS INTEGER, shftCount AS INTEGER)
  DIM shftBuffer AS STRING
  DIM offset AS INTEGER

  ' create temporary buffer
  shftBuffer = SPACE$(shftCount * 3)

  ' copy section to shift
  OUT DACRead, shftStart
  FOR i% = 0 TO shftCount - 1
    offset = (i% * 3) + 1
    MID$(shftBuffer, offset, 1) = CHR$(INP(DACData))
    MID$(shftBuffer, offset + 1, 1) = CHR$(INP(DACData))
    MID$(shftBuffer, offset + 2, 1) = CHR$(INP(DACData))
  NEXT i%

  ' shift temporary buffer to the right
  shftBuffer = RIGHT$(shftBuffer, 3) + LEFT$(shftBuffer, LEN(shftBuffer) - 3)

  ' replace colors
  OUT DACWrite, shftStart
  FOR i% = 0 TO shftCount - 1
    offset = (i% * 3) + 1
    OUT DACData, ASC(MID$(shftBuffer, offset, 1))
    OUT DACData, ASC(MID$(shftBuffer, offset + 1, 1))
    OUT DACData, ASC(MID$(shftBuffer, offset + 2, 1))
  NEXT i%
END SUB

The two following screenshots of "Sam & Max Hit The Road" show how multiple asynchronous palette shifts can be used to achieve amazing water effects. In the first screenshot, only six consecutive frames are captured, which makes the animation jerky except for some ripples to the left of Sam's face. The second screenshot attempts to get a better feel for the animation, but some shifts are still misaligned (see the wave under the black bush to the left of the alligator's hat, or the water in the shadow of his tail.)

And here's another one from "Black Thorne," showing how waterfalls were made (other palette shifting effects are not properly synchronized and do not look the way they are meant to, see the palette on the right.)

Palette swap

Like said previously, VGA uses a paint-by-the-number system, so we can filter color indices in such a way that a value used on a sprite sheet is redirected to a different value in the palette. This effect is called "palette swap." It is fairly easy to put in place and was heavily used in the 90s to save memory by repurposing existing graphics. It was used in early Mortal Kombat for the various ninja sprites (Scorpion, Reptile, Sub-zero, and others are the exact same sprite, only with different colors,) in Duke Nukem 3D to create the various flavors of Liztroops, to create Luigi from Mario in Super Mario Bros, and reuse foreground tiles as background decoration in the Mega Man series on the NES.

This effect isn't obtained via palette manipulation (there's no programming of the DAC involved,) but via filters instead. Here's one way you can do it:

DIM index AS STRING
DIM palSwap(2) AS STRING * 256

' load swap indices (unmapped, gray, EGA)
RESTORE sampleSwap
FOR j% = 0 TO 2
  FOR i% = 0 TO 255
    READ index
    MID$(palSwap(j%), i% + 1, 1) = CHR$(VAL("&H" + index))
  NEXT i%
NEXT j%

SCREEN 13

' Draw the same image using different filters
FOR j% = 0 TO 2
  drawPic j% * 32, 0, palSwap(j%)
  drawPal j% * 32, 20, palSwap(j%)
NEXT j%

samplePic:
  ' Sample picture (unmapped colors - decimal)
  DATA 53,52,99,99,52,53,53,53,53,53,53,52,99,99,52,53
  DATA 52,99,99,99,42,42,42,42,42,42,42,99,99,99,99,52
  DATA 53,53,53,42,43,43,43,43,67,67,67,42,42,99,99,99
  DATA 53,53,42,43,43,43,06,42,67,67,67,67,67,42,52,52
  DATA 53,53,42,43,43,06,42,66,42,67,67,67,67,42,53,53
  DATA 53,53,42,43,06,42,06,66,66,42,42,67,67,42,53,53
  DATA 53,53,42,43,06,66,15,66,66,15,66,42,67,67,42,53
  DATA 53,53,42,43,06,66,15,66,66,15,66,42,67,67,42,53
  DATA 51,51,42,43,06,65,65,66,66,65,65,42,67,67,42,51
  DATA 45,45,45,42,06,66,66,66,66,66,66,42,67,67,42,45
  DATA 45,45,45,45,06,66,15,15,15,66,66,42,67,67,42,45
  DATA 46,45,46,45,06,66,66,15,15,66,66,42,42,42,46,46
  DATA 46,46,46,46,46,06,66,66,66,42,06,46,46,46,46,46
  DATA 47,46,47,46,05,05,06,42,42,66,06,05,05,46,47,46
  DATA 47,47,47,05,37,37,05,66,66,66,05,37,37,05,47,47
  DATA 47,47,47,05,37,37,37,37,37,37,37,37,37,05,47,47

sampleSwap:
  ' Swap - unmapped (hex)
  DATA 00,01,02,03,04,05,06,07,08,09,0A,0B,0C,0D,0E,0F
  DATA 10,11,12,13,14,15,16,17,18,19,1A,1B,1C,1D,1E,1F
  DATA 20,21,22,23,24,25,26,27,28,29,2A,2B,2C,2D,2E,2F
  DATA 30,31,32,33,34,35,36,37,38,39,3A,3B,3C,3D,3E,3F
  DATA 40,41,42,43,44,45,46,47,48,49,4A,4B,4C,4D,4E,4F
  DATA 50,51,52,53,54,55,56,57,58,59,5A,5B,5C,5D,5E,5F
  DATA 60,61,62,63,64,65,66,67,68,69,6A,6B,6C,6D,6E,6F
  DATA 70,71,72,73,74,75,76,77,78,79,7A,7B,7C,7D,7E,7F
  DATA 80,81,82,83,84,85,86,87,88,89,8A,8B,8C,8D,8E,8F
  DATA 90,91,92,93,94,95,96,97,98,99,9A,9B,9C,9D,9E,9F
  DATA A0,A1,A2,A3,A4,A5,A6,A7,A8,A9,AA,AB,AC,AD,AE,AF
  DATA B0,B1,B2,B3,B4,B5,B6,B7,B8,B9,BA,BB,BC,BD,BE,BF
  DATA C0,C1,C2,C3,C4,C5,C6,C7,C8,C9,CA,CB,CC,CD,CE,CF
  DATA D0,D1,D2,D3,D4,D5,D6,D7,D8,D9,DA,DB,DC,DD,DE,DF
  DATA E0,E1,E2,E3,E4,E5,E6,E7,E8,E9,EA,EB,EC,ED,EE,EF
  DATA F0,F1,F2,F3,F4,F5,F6,F7,F8,F9,FA,FB,FC,FD,FE,FF

  ' Swap - grayscale (hex)
  DATA 00,11,17,18,14,15,17,07,08,17,1C,1D,19,1A,1E,0F
  DATA 00,11,12,13,14,15,16,17,18,19,1A,1B,1C,1D,1E,0F
  DATA 12,13,15,08,17,17,08,08,16,18,1A,1C,1E,1D,1C,07
  DATA 1A,1B,07,07,1C,1A,17,15,1A,1A,1B,07,1C,07,07,07
  DATA 07,1C,1D,1E,1E,1E,1E,1D,1D,1D,1D,1D,1D,1D,1C,1B
  DATA 1C,1D,1D,1D,1D,1D,1D,1D,1D,1D,1E,1E,0F,1E,1E,1E
  DATA 1E,1E,1E,1E,1E,1E,1D,1D,11,11,12,12,13,13,13,12
  DATA 12,13,15,08,17,17,08,16,15,15,15,16,16,15,13,12
  DATA 15,15,15,16,16,16,16,16,15,16,08,17,17,17,17,17
  DATA 08,17,17,17,17,08,16,15,08,08,08,17,17,17,17,17
  DATA 08,17,17,17,18,17,17,17,17,17,17,17,17,17,17,08
  DATA 00,11,11,11,12,11,11,11,11,12,12,13,14,14,13,13
  DATA 12,13,13,13,13,12,12,11,12,13,13,13,13,13,13,13
  DATA 13,13,14,14,14,14,14,14,14,14,14,14,14,13,13,13
  DATA 13,13,13,14,14,14,14,14,13,14,14,14,14,14,14,14
  DATA 14,14,14,14,14,14,14,13,00,00,00,00,00,00,00,00

  ' Swap - EGA (hex)
  DATA 00,01,02,03,04,05,06,07,08,09,0A,0B,0C,0D,0E,0F
  DATA 00,00,00,08,08,08,08,08,08,07,07,07,07,07,0F,0F
  DATA 01,09,09,05,0D,05,0C,0C,04,06,06,0E,0E,0E,0A,0A
  DATA 02,0A,0A,03,0B,03,09,09,09,09,0D,0D,0D,0D,0D,0C
  DATA 0C,0C,0E,0E,0E,0E,0E,0A,0A,0A,0B,0B,0B,0B,0B,09
  DATA 07,07,0F,0F,0F,0F,0F,07,07,07,0F,0F,0F,0F,0F,07
  DATA 07,07,0F,0F,0F,0F,0F,07,01,01,01,08,05,08,04,04
  DATA 04,04,06,06,06,06,02,02,02,02,02,08,03,08,01,01
  DATA 08,08,08,08,08,08,08,08,08,08,08,08,08,08,08,08
  DATA 08,08,08,08,08,08,08,08,08,08,08,08,08,08,08,08
  DATA 08,08,08,08,08,08,08,08,08,08,08,08,08,08,08,08
  DATA 00,00,00,00,08,00,00,00,00,00,00,00,06,00,00,00
  DATA 00,00,00,00,08,00,00,00,08,08,08,08,08,08,08,08
  DATA 08,08,08,08,08,08,08,08,08,08,08,08,08,08,08,08
  DATA 08,08,08,08,08,08,08,08,08,08,08,08,08,08,08,08
  DATA 08,08,08,08,08,08,08,08,00,00,00,00,00,00,00,00

SUB drawPal (x AS INTEGER, y AS INTEGER, map AS STRING)
  DIM index AS INTEGER

  FOR y2% = 0 TO 15
    FOR x2% = 0 TO 15
      PSET (x + x2%, y + y2%), ASC(MID$(map, 1 + (x2% + (y2% * 16)), 1))
    NEXT x2%
  NEXT y2%
END SUB

SUB drawPic (x AS INTEGER, y AS INTEGER, map AS STRING)
  DIM index AS INTEGER

  RESTORE samplePic
  FOR y2% = 0 TO 15
    FOR x2% = 0 TO 15
      READ index
      PSET (x + x2%, y + y2%), ASC(MID$(map, 1 + index, 1))
    NEXT x2%
  NEXT y2%
END SUB

Transparency and color blending

Pixel color manipulation is much easier in direct color modes (at least 16 bits per pixel) because the color of each pixel can be set individually, and so, new colors can be created as needed. In indexed color modes however, the palette must be built to make blending possible (it should have smooth color gradients, visually similar tones, etc.) The second part of the job is the creation of a lookup table, which will both insure that all colors can be blended together, but also improve performances since there's no computation required at run-time.

The lookup table is similar to the filter we used for palette swap, except it's two-dimensional instead of linear (two indices are used to retrieve one result, unlike palette swap that only needs one index.) Since they are two-dimensional, lookup tables require more memory: for a 256 color palette, you'd need to store 256x256 bytes of information! To keep things in check, the following example only generates a 16x16 transparency lookup table using all 256 colors available in the palette (any of the first 16 colors of the palette can be blended together:)

TYPE clrUDT
  R AS INTEGER
  G AS INTEGER
  B AS INTEGER
END TYPE

CONST DACPelMask = &H3C6
CONST DACRead = &H3C7
CONST DACData = &H3C9

CONST numColors = 16
CONST fullRange = 256

REDIM lookup(numColors * numColors - 1) AS INTEGER
DIM SHARED clrPal(fullRange - 1) AS clrUDT

SCREEN 13

' load current palette to structure
OUT DACPelMask, &HFF
OUT DACRead, 0
FOR i% = LBOUND(clrPal) TO UBOUND(clrPal)
  clrPal(i%).R = INP(DACData)
  clrPal(i%).G = INP(DACData)
  clrPal(i%).B = INP(DACData)
NEXT i%

' build lookup table
FOR b% = 0 TO numColors - 1
  FOR a% = 0 TO numColors - 1
    lookup(a% + b% * numColors) = transColor%(.5, a%, b%)
  NEXT a%
NEXT b%

' display lookup table (each cell will contain the blending result of color in column a% and color in row b%)
FOR b% = 0 TO numColors - 1
  PSET (b% + 2, 0), b%
  PSET (0, b% + 2), b%
  FOR a% = 0 TO numColors - 1
    PSET (a% + 2, b% + 2), lookup(a% + b% * numColors)
  NEXT a%
NEXT b%

' computes alpha-blending and finds closest existing attribute
' uses taxicab distance which is faster but less accurate than
' dist = SQR(delta.R ^ 2 + delta.G ^ 2 + delta.B ^ 2)
FUNCTION transColor% (alpha AS SINGLE, c1 AS INTEGER, c2 AS INTEGER)
  DIM bld AS clrUDT, dist AS INTEGER, temp AS INTEGER

  ' Get RGB value of alpha blending
  bld.R = alpha * clrPal(c1).R + (1 - alpha) * clrPal(c2).R
  bld.G = alpha * clrPal(c1).G + (1 - alpha) * clrPal(c2).G
  bld.B = alpha * clrPal(c1).B + (1 - alpha) * clrPal(c2).B

  ' Find index to closest RGB attribute
  dist = 255
  FOR i% = LBOUND(clrPal) TO UBOUND(clrPal)
    temp = ABS(bld.R - clrPal(i%).R) + ABS(bld.G - clrPal(i%).G) + ABS(bld.B - clrPal(i%).B)
    IF (temp < dist) THEN dist = temp: transColor% = i%
  NEXT i%
END FUNCTION

That's all folks

There are others things that could be touched on, such as ordering the palette to create faster effects (like blur, dynamic fire and grayscale transparency) or technical specifications of palette files... but I'll leave it at that for now. For good measure, I'm dropping this file, which contains what I would consider the most helpful code snippets on this topic (there's no error check, use your brain,) have fun.

Other pages you might find useful: