Created
May 19, 2026 22:51
-
-
Save kurtdekker/d2cc7a18b9ba8a07626d0bab8d37395a to your computer and use it in GitHub Desktop.
Making precision UI art in Python Pygame
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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