Files

503 lines
16 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
Generate period-accurate decorative GIFs for 90s retro websites.
Creates pixel art icons, bullet points, animated badges, etc.
"""
import struct
import os
import sys
def create_gif(width, height, frames=None, loop=0):
"""Create a GIF89a image with optional animation frames.
frames: list of [[(r,g,b), ...], ...] where each frame is pixel colour array
If frames is None, creates a simple solid colour image at 1x1"""
if frames is None:
# Default to simple solid colour
frames = [[(255, 255, 255)]]
if len(frames[0]) == 0:
frames = [[(0, 0, 0)]]
# Build a master frame for header dimensions
fh, fw = len(frames[0]), len(frames[0][0]) if frames[0] else 1
# GIF Header
header = b'GIF89a'
# Logical Screen Descriptor
lsd = struct.pack('<HH', fw, fh)
lsd += b'\x00' # No GCT, 8-bit colour resolution
lsd += b'\x00' # Background colour index
lsd += b'\x00' # Pixel aspect ratio
# Image Descriptor for each frame
all_img_data = b''
for frame_data in frames:
fheight = len(frame_data)
fwidth = len(frame_data[0]) if frame_data else 1
img = b'\x2C' # Image Separator
img += struct.pack('<HHHH', 0, 0, fwidth, fheight)
img += b'\x00' # no local colour table
# LZW minimum code size
img += bytes([2])
col_index = 1 # use colour index 1 (2 colours = 2-bit codes)
# Build data for each pixel
for row in frame_data:
for pixel in row:
total_pixels = fwidth * fheight
raw_bits = 1
bits = []
for _ in range(total_pixels):
bits.append(1)
bitstream = ''.join(str(b) for b in bits)
data_bytes = bytearray()
for i in range(0, len(bitstream), 8):
byte = bitstream[i:i+8]
if len(byte) < 8:
byte = byte.ljust(8, '0')
data_bytes.append(int(byte, 2))
clear_code = 4
eoi_code = 5
sub_data = bytes([clear_code]) + bytes(data_bytes) + bytes([eoi_code])
i = 0
while i < len(sub_data):
block_size = min(255, len(sub_data) - i)
img += bytes([block_size])
img += sub_data[i:i+block_size]
i += block_size
img += b'\x00' # sub-block terminator
img += b'\x3B' # trailer
return header + lsd + all_img_data
def write_single_pixel_gif(filepath, r, g, b, ext=''):
"""Create a minimal single-pixel GIF with a given colour, using raw bytes.
More reliable than the complex LZW encoder above."""
# Minimal valid GIF89a: 1x1 pixel, single colour
gif = bytearray()
gif.extend(b'GIF89a')
# Logical Screen Descriptor
gif += struct.pack('<HH', 1, 1)
gif += b'\x80' # GCT present, 1-bit colour resolution, 2^1=2 colours
gif += b'\x00' # background
gif += b'\x00' # aspect ratio
# Global Color Table: 2 entries (index 0 = black, index 1 = target colour)
gif += b'\x00\x00\x00' # index 0: black
gif += struct.pack('BBB', r, g, b) # index 1: target colour
# Image Descriptor
gif += b'\x2C' # image separator
gif += struct.pack('<HHHH', 0, 0, 1, 1)
gif += b'\x00' # no local colour table
# LZW minimum code size
gif.append(2)
# Image data sub-block
gif += bytes([4, 2, 0x02, 0x00, 5, 0x00]) # clear, data, eoi in compact form
gif += b'\x3B' # trailer
with open(filepath, 'wb') as f:
f.write(bytes(gif))
def write_pixel_image_gif(filepath, pixels, fg=(255,255,255), bg=(0,0,0)):
"""Write a small pixel-art GIF. pixels = 2D array of 0 (bg) or 1 (fg)"""
w = len(pixels[0])
h = len(pixels)
gif = bytearray()
gif.extend(b'GIF89a')
gif += struct.pack('<HH', w, h)
gif += b'\x80' # GCT present, 1-bit
gif += b'\x00\x00'
# GCT: index 0 = bg, index 1 = fg
gif += struct.pack('BBB', *bg)
gif += struct.pack('BBB', *fg)
# Image Descriptor
gif += b'\x2C'
gif += struct.pack('<HHHH', 0, 0, w, h)
gif += b'\x00'
# LZW code size
gif.append(2)
# Create pixel data as bit stream
bits = bytearray()
for y in range(h):
for x in range(w):
bits.append(pixels[y][x])
# Pack into bytes (MSB first)
data = bytearray()
for i in range(0, len(bits), 8):
byte = 0
for j in range(8):
if i + j < len(bits):
byte |= (bits[i+j] << (7-j))
data.append(byte)
# Encode: clear + data + eoi
sub_data = bytearray()
sub_data.append(4) # clear code (100 in binary for 2-bit)
# Add pixel runs
for i, b in enumerate(bits):
sub_data.append(b)
sub_data.append(5) # EOI
sub_data.append(0) # terminator for last byte
# Write sub-blocks
offset = 0
while offset < len(sub_data):
block_size = min(255, len(sub_data) - offset)
gif.append(block_size)
gif.extend(sub_data[offset:offset+block_size])
offset += block_size
gif.append(0) # sub-block terminator
gif.append(0x3B) # trailer
with open(filepath, 'wb') as f:
f.write(bytes(gif))
def main():
outdir = sys.argv[1] if len(sys.argv) > 1 else './retro-images'
os.makedirs(outdir, exist_ok=True)
created = 0
# ========== SPACER GIFS (simple single-pixel) ==========
spacers = [
('blue.gif', 0, 0, 200),
('navy.gif', 0, 0, 128),
('white.gif', 255, 255, 255),
('grey.gif', 192, 192, 192),
('light_grey.gif', 220, 220, 220),
('dark_grey.gif', 100, 100, 100),
('red.gif', 200, 0, 0),
('yellow.gif', 255, 255, 0),
('green.gif', 0, 255, 0),
('orange.gif', 255, 165, 0),
('cyan.gif', 0, 255, 255),
('magenta.gif', 255, 0, 255),
('gold.gif', 255, 215, 0),
('transparent.gif', 0, 0, 0), # special handling
]
for fname, r, g, b in spacers:
if fname == 'transparent.gif':
# Transparent 1x1 GIF
gif = bytearray(b'GIF89a')
gif += struct.pack('<HH', 1, 1)
gif += b'\x80\x00\x00\x00' # GCT, 1-bit
gif += b'\x00\x00\x00' # transparent (index 0)
gif += b'\x00\xFF\x00' # solid (index 1, arbitrary colour)
gif += b'\x21\xF9\x04\x01\x00\x00\x00\x00' # graphics control extension for transparency
gif += b'\x2C' # image separator
gif += struct.pack('<HHHH', 0, 0, 1, 1)
gif += b'\x00'
gif.append(2) # min code size
gif += bytes([4, 1, 1, 2, 0x02, 0x00, 5, 0x00])
gif += b'\x3B'
with open(os.path.join(outdir, fname), 'wb') as f:
f.write(bytes(gif))
print(f" OK {fname:20s} (1x1 transparent)")
else:
write_single_pixel_gif(os.path.join(outdir, fname), r, g, b)
print(f" OK {fname:20s} ({r},{g},{b})")
created += len(spacers)
# ========== BULLET DOT ==========
# Small 8x8 diamond bullet
bullet = [
[0,0,0,1,1,0,0,0],
[0,0,1,1,1,1,0,0],
[0,1,1,1,1,1,1,0],
[1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1],
[0,1,1,1,1,1,1,0],
[0,0,1,1,1,1,0,0],
[0,0,0,1,1,0,0,0],
]
write_pixel_image_gif(os.path.join(outdir, 'bullet.gif'), bullet, fg=(0,0,200), bg=(0,0,0))
print(" OK bullet.gif (8x8 blue diamond bullet)")
created += 1
# ========== NEW! BADGE ==========
new_badge = [
[1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,0,0,1,1,1,0,1],
[1,0,1,0,1,0,0,0,1,0,0,1],
[1,0,1,1,1,0,0,1,1,0,0,1],
[1,0,1,0,1,0,0,0,0,0,0,1],
[1,0,1,1,1,0,0,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1],
]
write_pixel_image_gif(os.path.join(outdir, 'new.gif'), new_badge, fg=(255, 0, 0), bg=(255, 255, 0))
print(" OK new.gif (9x12 red 'NEW!' badge on yellow)")
created += 1
# ========== STAR ==========
star = [
[0,0,0,1,0,0,0],
[0,0,1,1,1,0,0],
[0,1,1,1,1,1,0],
[1,1,0,1,0,1,1],
[1,0,0,0,0,0,1],
[0,1,1,1,1,1,0],
[0,0,1,1,1,0,0],
[0,1,1,0,1,1,0],
]
write_pixel_image_gif(os.path.join(outdir, 'star.gif'), star, fg=(255, 215, 0), bg=(0, 0, 0))
print(" OK star.gif (7x8 gold star)")
created += 1
# ========== SPARKLE ==========
sparkle = [
[0,0,1,0,0],
[0,0,1,0,0],
[1,1,1,1,1],
[0,0,1,0,0],
[0,0,1,0,0],
]
write_pixel_image_gif(os.path.join(outdir, 'sparkle.gif'), sparkle, fg=(0, 255, 255), bg=(0, 0, 0))
print(" OK sparkle.gif (5x5 cyan sparkle)")
created += 1
# ========== MAILTO ENVELOPE ==========
mail = [
[1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,1],
[1,0,0,1,0,0,0,1],
[1,0,1,0,0,1,0,1],
[1,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1],
]
write_pixel_image_gif(os.path.join(outdir, 'mailto.gif'), mail, fg=(255, 255, 0), bg=(0, 50, 0))
print(" OK mailto.gif (7x8 yellow envelope on dark green)")
created += 1
# ========== CONSTRUCTION SIGN ==========
construction = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1],
[1,1,0,0,1,0,1,0,1,0,1,0,0,0,1,1],
[1,1,0,0,0,1,0,1,0,1,0,0,0,1,1,1],
[1,1,0,0,1,0,1,0,1,0,1,0,0,0,1,1],
[1,1,0,0,0,1,0,1,0,1,0,0,0,1,1,1],
[1,1,0,0,1,0,1,0,1,0,1,0,0,0,1,1],
[1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1],
[1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
]
write_pixel_image_gif(os.path.join(outdir, 'construction.gif'), construction, fg=(255, 165, 0), bg=(0, 0, 0))
print(" OK construction.gif (9x16 orange construction icon)")
created += 1
# ========== EMAIL ICON ==========
email_icon = [
[0,1,1,1,1,1,1,0],
[1,1,0,0,0,0,1,1],
[1,0,1,0,0,1,0,1],
[1,0,0,1,1,0,0,1],
[1,0,0,0,0,0,0,1],
[1,1,0,0,0,0,1,1],
[0,1,1,1,1,1,1,0],
]
write_pixel_image_gif(os.path.join(outdir, 'email_icon.gif'), email_icon, fg=(0, 0, 200), bg=(0, 0, 0))
print(" OK email_icon.gif (7x8 blue mail icon)")
created += 1
# ========== WEB RING SYMBOL ==========
webring = [
[0,0,1,1,1,1,1,1,0,0],
[0,1,0,0,0,0,0,0,1,0],
[1,0,1,0,0,0,0,1,0,1],
[1,0,0,1,0,0,1,0,0,1],
[1,0,0,0,1,1,0,0,0,1],
[1,0,0,0,1,1,0,0,0,1],
[1,0,0,1,0,0,1,0,0,1],
[1,0,1,0,0,0,0,1,0,1],
[0,1,0,0,0,0,0,0,1,0],
[0,0,1,1,1,1,1,1,0,0],
]
write_pixel_image_gif(os.path.join(outdir, 'webring.gif'), webring, fg=(0, 200, 0), bg=(0, 0, 0))
print(" OK webring.gif (10x10 green ring icon)")
created += 1
# ========== FIREFLAME BOTTOM BAR ==========
flame = [
[0,0,0,0,1,1,0,0,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,1,0,0,1,1,0,0],
[0,1,1,0,1,1,0,1,1,0],
[1,1,0,1,1,1,1,0,1,1],
[1,0,1,0,0,0,0,1,0,1],
[1,1,1,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,0,0,0],
]
write_pixel_image_gif(os.path.join(outdir, 'flame.gif'), flame, fg=(255, 100, 0), bg=(0, 0, 0))
print(" OK flame.gif (8x10 orange flame icon)")
created += 1
# ========== NAV ARROW ==========
arrow_right = [
[0,0,0,0,1,0,0,0],
[0,0,0,1,1,0,0,0],
[0,0,0,1,1,0,0,0],
[1,0,1,1,0,1,0,0],
[0,0,0,1,1,0,0,0],
[0,0,0,1,1,0,0,0],
[0,0,0,0,1,0,0,0],
]
write_pixel_image_gif(os.path.join(outdir, 'arrow_right.gif'), arrow_right, fg=(0, 255, 0), bg=(0, 0, 0))
print(" OK arrow_right.gif (7x8 green right arrow)")
created += 1
arrow_left = [
[0,0,0,1,0,0,0,0],
[0,0,0,1,0,0,0,0],
[0,0,0,1,0,0,0,0],
[0,0,1,0,0,1,0,0],
[0,0,0,1,0,0,0,0],
[0,0,0,1,0,0,0,0],
[0,0,0,1,0,0,0,0],
]
write_pixel_image_gif(os.path.join(outdir, 'arrow_left.gif'), arrow_left, fg=(0, 255, 0), bg=(0, 0, 0))
print(" OK arrow_left.gif (7x8 green left arrow)")
created += 1
# ========== TILDED STAR (for decorative headings) ==========
tstar = []
for row_data in [
[0,0,0,0,1,0,0,0,0],
[0,0,0,1,1,1,0,0,0],
[0,0,1,1,1,1,1,0,0],
[0,1,1,1,1,1,1,1,0],
[1,1,0,1,1,1,0,1,1],
[1,0,0,0,1,0,0,0,1],
[0,1,1,1,1,1,1,1,0],
[0,0,1,0,0,0,1,0,0],
[0,0,0,1,0,1,0,0,0],
]:
tstar.append(row_data)
write_pixel_image_gif(os.path.join(outdir, 'tstar.gif'), tstar, fg=(255, 255, 0), bg=(0, 0, 0))
print(" OK tstar.gif (9x9 yellow star")
created += 1
# ========== COUNTER DIGITS ==========
# Simple 5x3 pixel digit glyphs for visitor counter
# Digits 0-9 as 5-col, 3-row arrays
digits = {
0: [
[1,1,1,0,0],
[1,0,0,1,0],
[1,1,1,0,0],
],
1: [
[0,1,0,0,0],
[1,1,0,0,0],
[1,1,1,0,0],
],
2: [
[1,1,1,0,0],
[0,0,1,1,0],
[1,1,0,1,0],
],
3: [
[1,1,1,0,0],
[0,0,1,1,0],
[1,1,1,0,0],
],
4: [
[1,0,0,1,0],
[1,1,1,1,0],
[0,0,0,1,0],
],
5: [
[1,1,1,0,0],
[1,0,0,1,0],
[1,1,1,1,0],
],
6: [
[1,1,1,0,0],
[1,0,0,1,0],
[1,1,1,1,0],
],
7: [
[0,1,1,1,0],
[0,0,0,1,0],
[0,0,0,1,0],
],
8: [
[1,1,1,0,0],
[1,0,0,1,0],
[1,1,1,0,0],
],
9: [
[1,1,1,0,0],
[1,0,0,1,0],
[1,1,0,1,0],
],
}
for digit_char in '0123450042':
d = int(digit_char)
digits[d].append([0]*5)
write_pixel_image_gif(os.path.join(outdir, f'counter_{digit_char}.gif'), digits[d], fg=(255, 255, 255), bg=(0, 0, 0))
print(f" OK counter_{digit_char}.gif")
created += 1
# ========== VISITOR COUNTER STRING ==========
# Make counter.gif as sequence: " 12342"
# Build a multi-pixel counter image
count_digits = '000000'
count_w = 21
count_h = 12
count_img = [[0]*count_w for _ in range(count_h)]
for i, c in enumerate(count_digits):
d = digits[int(c)]
for row in range(len(d)):
for col in range(len(d[row])):
count_img[row][i*3+col+1] = d[row][col]
write_pixel_image_gif(os.path.join(outdir, 'counter.gif'), count_img, fg=(255, 255, 150), bg=(0, 0, 0))
print(f" OK counter.gif (21x12 '000000')")
created += 1
# ========== COUNTER BOTTOM ==========
count_bottom_digits = '000000'
write_pixel_image_gif(os.path.join(outdir, 'counter_bottom.gif'), count_img, fg=(200, 200, 100), bg=(0, 0, 0))
print(f" OK counter_bottom.gif (21x12 '000000')")
created += 1
print(f"\nGenerated {created} GIFs in {outdir}/")
if __name__ == '__main__':
main()