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
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