Skip to content

Instantly share code, notes, and snippets.

@kurtdekker
Created May 19, 2026 22:51
Show Gist options
  • Select an option

  • Save kurtdekker/d2cc7a18b9ba8a07626d0bab8d37395a to your computer and use it in GitHub Desktop.

Select an option

Save kurtdekker/d2cc7a18b9ba8a07626d0bab8d37395a to your computer and use it in GitHub Desktop.
Making precision UI art in Python Pygame
# 5:33 PM 3/21/2014
# making instrument graphics for Jetpack Kurt
# 2024 - porting to Python3... does not produce
# the same output yet. CRAP!!
#print ("this does not produce correct output.")
#exit(1)
# Turns out Python3 port is fine but the
# TGA writer has bugs, writing extra run length alpha going down!
# Therefore, see note in resources file: Note_On_Duplicate_Files.txt
# and this has been used to make what is running in game now, mostly.
import pygame
import os
from math import pi
# some common angles and radiuses and sizes, etc.
angle_lowerbase = 135
inner_radius_percent = 0.75
outer_radius_percent = 0.95
ALPHA_LIGHT = 100
ALPHA_HEAVY = 160
ALPHA_FULL = 200
ALPHA_OUTLINE = 100
# when outlining, what is considered "mimumum"
MINALPHA = 90
COLOR_BORDER = (100,100,100,ALPHA_LIGHT)
COLOR_TIC = (255,255,255,ALPHA_HEAVY)
COLOR_NEEDLE = (200,255,255,ALPHA_FULL)
COLOR_GREENLINE_ARC = (0, 255, 0, ALPHA_LIGHT)
COLOR_YELLOWLINE_ARC = (255, 255, 0, ALPHA_LIGHT)
COLOR_REDLINE_ARC = (255, 0, 0, ALPHA_HEAVY)
COLOR_INDICATOR_LIGHT = (255,255,255)
COLOR_SPINNER = COLOR_NEEDLE
PRIMARY_OUTLINE_FRACTION = 0.012
def fullpath(filename):
outpath = "../../jetpack/Assets/Resources/Textures/instruments/"
try:
os.mkdir( outpath)
except OSError:
pass
return "%s%s.png" % (outpath, filename)
class Instrument:
def __init__(self,sz=None):
if sz is None:
self.sz = 512
else:
self.sz = sz
self.hz = self.sz // 2
self.sfc = pygame.Surface((self.sz,self.sz), pygame.SRCALPHA)
self.sfc.fill( (0,0,0,0))
self.radius = self.sz // 2
return
def add_outline( self, percent = None):
if percent is None:
percent = PRIMARY_OUTLINE_FRACTION
bordersz = int(self.sz * percent)
# I call it "black2" in honor of color224
# in the DPE original palette!
black2 = pygame.Surface( (self.sz, self.sz), pygame.SRCALPHA)
black2.fill(0)
# make a solid black version of the graphic
for j in range( self.sz):
for i in range( self.sz):
c = self.sfc.get_at((i,j))
if c.a > MINALPHA:
black2.set_at( (i, j), (0,0,0,255))
composite = pygame.Surface( (self.sz, self.sz), pygame.SRCALPHA)
composite.fill(0)
# stamp the black version in a grid around
# and offset on work buffer
for y in range( -bordersz, bordersz+1):
width = bordersz # just this alone makes a square outline
# whereas this makes a diamond-shaped one, which
# looks way better, not so thick on diagonals
width = bordersz - abs(y)
for x in range( -width, width+1):
composite.blit( black2, (x,y))
# Two steps in one pass:
# 1) remove pixels that have color from source, OR
# 2) or adjust alpha of composite black
for j in range( self.sz):
for i in range( self.sz):
c = composite.get_at((i,j))
if c.a > MINALPHA:
original_pixel = self.sfc.get_at((i,j))
if original_pixel.a > MINALPHA:
composite.set_at((i,j),(0,255,255,0))
else:
c.a = ALPHA_OUTLINE
composite.set_at( (i,j), c)
# blit original graphic onto this outline "crust"
self.sfc.blit( composite, (0,0))
return
def stamp_border(self, percent = None):
if percent is None:
percent = 0.020
bordersz = int(self.sz * percent)
pygame.draw.rect( self.sfc, COLOR_BORDER, (0, 0, self.sz, bordersz))
pygame.draw.rect( self.sfc, COLOR_BORDER, (0, 0, bordersz, self.sz))
pygame.draw.rect( self.sfc, COLOR_BORDER, (0, self.sz - bordersz, self.sz, bordersz))
pygame.draw.rect( self.sfc, COLOR_BORDER, (self.sz - bordersz, 0, bordersz, self.sz))
return
# caution: this is only a one-deep stack right now!!
def push(self):
self.savesfc = self.sfc.copy()
return
def pop(self):
self.sfc = self.savesfc
return
def save(self,filename):
pygame.image.save( self.sfc, fullpath( filename))
return
def main():
# this tick is struck at rotation = 0 at the TOP CENTER!!
TIC_SMALL = Instrument()
tic_length = int( TIC_SMALL.hz * (outer_radius_percent - inner_radius_percent))
tic_width = int(TIC_SMALL.sz * 0.033)
x = (TIC_SMALL.sz - tic_width) // 2
y = int(TIC_SMALL.hz * (1 - outer_radius_percent))
pygame.draw.rect( TIC_SMALL.sfc, COLOR_TIC, (x,y,tic_width,tic_length))
POWER_PLATE = Instrument()
numticks = 8
for i in range( numticks + 1):
for j in range(2):
if i == 0 and j == 0:
continue
sign = j * 2 - 1
angle = float( angle_lowerbase * i * sign) / numticks
sfc = pygame.transform.rotozoom( TIC_SMALL.sfc, angle, 1)
# this tints the actual ticmarks colors...
# if sign < 0:
# tmp = sfc.copy()
# tmp.fill( (255,200,150))
# sfc.blit( tmp, (0,0), special_flags = pygame.BLEND_MULT)
x = (POWER_PLATE.sfc.get_width() - sfc.get_width()) // 2
y = (POWER_PLATE.sfc.get_height() - sfc.get_height()) // 2
POWER_PLATE.sfc.blit( sfc, (x,y))
# <WIP> bend the upper tic marks yellow/red ?
FUEL_PLATE = Instrument()
tmpfuel = pygame.transform.rotozoom( POWER_PLATE.sfc, 180 - angle_lowerbase, 1)
# this draws the colored red/yellow arc
inset = int( FUEL_PLATE.hz * (1 - inner_radius_percent))
arc_thickness = FUEL_PLATE.sz // 8
for i in range(2):
dial_rect = (inset + i, inset, FUEL_PLATE.sz - inset * 2, FUEL_PLATE.sz - inset * 2),
a1 = pi * 1.00
a2 = pi * 1.25
pygame.draw.arc( FUEL_PLATE.sfc, COLOR_YELLOWLINE_ARC, dial_rect,
a1, a2, arc_thickness)
a1 = pi * 1.25
a2 = pi * 1.50
pygame.draw.arc( FUEL_PLATE.sfc, COLOR_REDLINE_ARC, dial_rect,
a1, a2, arc_thickness)
FUEL_PLATE.sfc.blit( tmpfuel,
( (FUEL_PLATE.sfc.get_width() - tmpfuel.get_width()) // 2,
(FUEL_PLATE.sfc.get_height() - tmpfuel.get_height()) // 2))
TEMP_PLATE = Instrument()
tmptemp = pygame.transform.rotozoom( POWER_PLATE.sfc, 180 - angle_lowerbase, 1)
# this draws the colored green/yellow/red arc
inset = int( TEMP_PLATE.hz * (1 - inner_radius_percent))
arc_thickness = TEMP_PLATE.sz // 8
for i in range(2):
dial_rect = (inset + i, inset, TEMP_PLATE.sz - inset * 2, TEMP_PLATE.sz - inset * 2),
a1 = pi * 0.24
a2 = pi * 1.00
pygame.draw.arc( TEMP_PLATE.sfc, COLOR_GREENLINE_ARC, dial_rect,
a1, a2, arc_thickness)
a1 = pi * 0.12
a2 = pi * 0.24
pygame.draw.arc( TEMP_PLATE.sfc, COLOR_YELLOWLINE_ARC, dial_rect,
a1, a2, arc_thickness)
a1 = pi * 0.00
a2 = pi * 0.12
pygame.draw.arc( TEMP_PLATE.sfc, COLOR_REDLINE_ARC, dial_rect,
a1, a2, arc_thickness)
TEMP_PLATE.sfc.blit( tmptemp,
( (TEMP_PLATE.sfc.get_width() - tmptemp.get_width()) // 2,
(TEMP_PLATE.sfc.get_height() - tmptemp.get_height()) // 2))
TEMP_PLATE.add_outline()
TEMP_PLATE.stamp_border()
TEMP_PLATE.save( "temp_plate");
FUEL_PLATE.add_outline()
FUEL_PLATE.stamp_border()
FUEL_PLATE.save( "fuel_plate");
# ------------ finish VSI
# quick cheese make the VSI by turning the RPM plate sideways!!
VSI_PLATE = Instrument()
VSI_PLATE.sfc = pygame.transform.rotate( POWER_PLATE.sfc, 90)
# this draws the colored yellow arc
inset = int( VSI_PLATE.hz * (1 - inner_radius_percent))
arc_thickness = VSI_PLATE.sz // 8
for i in range(2):
dial_rect = (inset + i, inset, VSI_PLATE.sz - inset * 2, VSI_PLATE.sz - inset * 2),
a1 = pi * 1.50
a2 = pi * 1.77
pygame.draw.arc( VSI_PLATE.sfc, COLOR_YELLOWLINE_ARC, dial_rect,
a1, a2, arc_thickness)
VSI_PLATE.add_outline()
VSI_PLATE.stamp_border()
VSI_PLATE.save( "vsi_plate")
# ------------ finish POWER
# do this late because POWER_PLATE is used to make other gauges
# this draws the colored yellow arc
inset = int( POWER_PLATE.hz * (1 - inner_radius_percent))
arc_thickness = POWER_PLATE.sz // 8
for i in range(2):
dial_rect = (inset + i, inset, POWER_PLATE.sz - inset * 2, POWER_PLATE.sz - inset * 2),
a1 = pi * 1.73
a2 = pi * 1.90
pygame.draw.arc( POWER_PLATE.sfc, COLOR_YELLOWLINE_ARC, dial_rect,
a1, a2, arc_thickness)
POWER_PLATE.add_outline()
POWER_PLATE.stamp_border()
POWER_PLATE.save( "power_plate")
def add_needle_center_circle(I):
pygame.draw.circle(
I.sfc,
COLOR_NEEDLE,
(I.hz, I.hz),
int(I.sz * 0.11))
return
NEEDLE1 = Instrument()
needle_width = int(NEEDLE1.sz * 0.12)
needle_y = int(NEEDLE1.hz * (1 - inner_radius_percent))
points1 = (
(NEEDLE1.hz, needle_y),
(NEEDLE1.hz + needle_width // 2, NEEDLE1.hz),
(NEEDLE1.hz - needle_width // 2, NEEDLE1.hz),
)
pygame.draw.polygon( NEEDLE1.sfc, COLOR_NEEDLE, points1)
NEEDLE1.push()
NEEDLE1.add_outline()
NEEDLE1.save( "needle1")
NEEDLE1.pop()
add_needle_center_circle( NEEDLE1)
NEEDLE1.add_outline()
NEEDLE1.save( "needle1c")
NEEDLE2 = Instrument()
points2 = (
points1[2],
(points1[2][0], points1[0][1] + (points1[0][0] - points1[2][0])),
points1[0],
(points1[1][0], points1[0][1] + (points1[0][0] - points1[2][0])),
points1[1],
)
pygame.draw.polygon( NEEDLE2.sfc, COLOR_NEEDLE, points2)
NEEDLE2.push()
NEEDLE2.add_outline()
NEEDLE2.save( "needle2")
NEEDLE2.pop()
add_needle_center_circle( NEEDLE2)
NEEDLE2.add_outline()
NEEDLE2.save( "needle2c")
LOWFUEL = Instrument()
pygame.draw.circle( LOWFUEL.sfc, COLOR_INDICATOR_LIGHT,
(int(LOWFUEL.sz * 0.80), int( LOWFUEL.sz * 0.80)),
int( LOWFUEL.sz * 0.15))
LOWFUEL.add_outline()
LOWFUEL.save( "low_fuel_light")
LOWFUEL.save( "high_temp_light")
ENGINEON = Instrument()
pygame.draw.circle( ENGINEON.sfc, COLOR_INDICATOR_LIGHT,
(int(ENGINEON.sz * 0.50), int(ENGINEON.sz * 0.86)),
int( ENGINEON.sz * 0.12))
ENGINEON.add_outline()
ENGINEON.save( "engine_on_light")
SPINNER = Instrument(128)
pygame.draw.rect( SPINNER.sfc, COLOR_SPINNER,
(
int( SPINNER.sz * 0.45),
int( SPINNER.sz * 0.10),
int( SPINNER.sz * 0.10),
int( SPINNER.sz * 0.80)
)
)
SPINNER.add_outline(PRIMARY_OUTLINE_FRACTION * 4)
SPINNER.save( "spinner")
return
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment