generate_splash.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. #!/usr/bin/env python3
  2. """Generate a polished SpoolBuddy boot splash image (1024x600).
  3. Uses the SpoolBuddy logo with baked-in glow, radial gradient background,
  4. subtle light rays, and vignette effects for a premium kiosk boot screen.
  5. Usage:
  6. python3 generate_splash.py [output_path]
  7. Requires: Pillow (pip install Pillow)
  8. """
  9. import math
  10. import os
  11. import sys
  12. from PIL import Image, ImageDraw, ImageFilter
  13. # --- Configuration ---
  14. WIDTH, HEIGHT = 1024, 600
  15. BG_CENTER = (55, 55, 55) # Notably lighter center
  16. BG_EDGE = (2, 2, 2) # Nearly black edges
  17. ACCENT = (0, 200, 75) # Brighter SpoolBuddy green
  18. ACCENT_GLOW = (0, 255, 100) # Vivid glow core
  19. LOGO_SCALE = 0.50 # Scale logo to 50% of canvas width
  20. GLOW_RADIUS = 160 # Very wide glow spread
  21. VIGNETTE_STRENGTH = 0.85 # Heavy edge darkening
  22. RAY_COUNT = 32 # More radial light rays
  23. RAY_OPACITY = 50 # Clearly visible rays (0-255)
  24. def radial_gradient(size, center_color, edge_color):
  25. """Create a radial gradient from center to edges."""
  26. w, h = size
  27. img = Image.new("RGB", size)
  28. pixels = img.load()
  29. cx, cy = w // 2, h // 2
  30. max_dist = math.sqrt(cx**2 + cy**2)
  31. for y in range(h):
  32. for x in range(w):
  33. dist = math.sqrt((x - cx) ** 2 + (y - cy) ** 2)
  34. t = min(dist / max_dist, 1.0)
  35. # Ease-out curve for smoother falloff
  36. t = t * t
  37. r = int(center_color[0] + (edge_color[0] - center_color[0]) * t)
  38. g = int(center_color[1] + (edge_color[1] - center_color[1]) * t)
  39. b = int(center_color[2] + (edge_color[2] - center_color[2]) * t)
  40. pixels[x, y] = (r, g, b)
  41. return img
  42. def create_light_rays(size, num_rays, opacity):
  43. """Create subtle radial light rays emanating from center."""
  44. w, h = size
  45. rays = Image.new("RGBA", size, (0, 0, 0, 0))
  46. draw = ImageDraw.Draw(rays)
  47. cx, cy = w // 2, h // 2
  48. max_radius = int(math.sqrt(cx**2 + cy**2)) + 50
  49. for i in range(num_rays):
  50. angle = (2 * math.pi * i) / num_rays
  51. # Vary ray width slightly for organic feel
  52. half_width = math.radians(1.5 + (i % 3) * 0.5)
  53. a1 = angle - half_width
  54. a2 = angle + half_width
  55. points = [
  56. (cx, cy),
  57. (cx + int(max_radius * math.cos(a1)), cy + int(max_radius * math.sin(a1))),
  58. (cx + int(max_radius * math.cos(a2)), cy + int(max_radius * math.sin(a2))),
  59. ]
  60. # Green-tinted rays
  61. draw.polygon(points, fill=(ACCENT[0], ACCENT[1], ACCENT[2], opacity))
  62. # Heavy blur to make rays soft and diffuse
  63. rays = rays.filter(ImageFilter.GaussianBlur(radius=30))
  64. return rays
  65. def create_vignette(size, strength):
  66. """Create a vignette (edge darkening) mask."""
  67. w, h = size
  68. vignette = Image.new("L", size, 255)
  69. pixels = vignette.load()
  70. cx, cy = w / 2, h / 2
  71. max_dist = math.sqrt(cx**2 + cy**2)
  72. for y in range(h):
  73. for x in range(w):
  74. dist = math.sqrt((x - cx) ** 2 + (y - cy) ** 2)
  75. t = dist / max_dist
  76. # Ramp darkening from ~40% radius outward
  77. fade = max(0, (t - 0.4) / 0.6)
  78. fade = fade * fade # Quadratic ease
  79. val = int(255 * (1 - fade * strength))
  80. pixels[x, y] = max(0, val)
  81. return vignette
  82. def create_glow(logo_img, color, radius, intensity=1.5):
  83. """Create a colored glow effect from a logo's alpha channel."""
  84. # Extract alpha as the glow shape
  85. if logo_img.mode != "RGBA":
  86. return Image.new("RGBA", logo_img.size, (0, 0, 0, 0))
  87. alpha = logo_img.split()[3]
  88. # Create colored version of the alpha shape
  89. glow = Image.new("RGBA", logo_img.size, (0, 0, 0, 0))
  90. glow_pixels = glow.load()
  91. alpha_pixels = alpha.load()
  92. for y in range(logo_img.height):
  93. for x in range(logo_img.width):
  94. a = alpha_pixels[x, y]
  95. if a > 0:
  96. boosted = min(255, int(a * intensity))
  97. glow_pixels[x, y] = (color[0], color[1], color[2], boosted)
  98. # Blur to create the glow spread
  99. glow = glow.filter(ImageFilter.GaussianBlur(radius=radius))
  100. return glow
  101. def generate_splash(output_path):
  102. """Generate the final splash image."""
  103. print(f"Generating {WIDTH}x{HEIGHT} splash image...")
  104. # 1. Radial gradient background
  105. print(" Creating radial gradient background...")
  106. canvas = radial_gradient((WIDTH, HEIGHT), BG_CENTER, BG_EDGE)
  107. # 2. Light rays
  108. print(" Adding light rays...")
  109. rays = create_light_rays((WIDTH, HEIGHT), RAY_COUNT, RAY_OPACITY)
  110. canvas.paste(
  111. Image.alpha_composite(
  112. Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0)),
  113. rays,
  114. ),
  115. (0, 0),
  116. rays,
  117. )
  118. # 3. Load and scale logo
  119. print(" Loading SpoolBuddy logo...")
  120. script_dir = os.path.dirname(os.path.abspath(__file__))
  121. logo_paths = [
  122. os.path.join(script_dir, "..", "..", "frontend", "public", "spoolbuddy_logo_dark.png"),
  123. os.path.join(script_dir, "..", "..", "frontend", "public", "img", "spoolbuddy_logo_dark.png"),
  124. ]
  125. logo = None
  126. for p in logo_paths:
  127. resolved = os.path.normpath(p)
  128. if os.path.exists(resolved):
  129. logo = Image.open(resolved).convert("RGBA")
  130. print(f" Loaded logo from {resolved}")
  131. break
  132. if logo is None:
  133. print(" ERROR: Could not find spoolbuddy_logo_dark.png")
  134. sys.exit(1)
  135. # Scale logo to target width
  136. target_w = int(WIDTH * LOGO_SCALE)
  137. scale = target_w / logo.width
  138. target_h = int(logo.height * scale)
  139. logo = logo.resize((target_w, target_h), Image.LANCZOS)
  140. # Center position (shift up slightly for visual balance)
  141. logo_x = (WIDTH - target_w) // 2
  142. logo_y = (HEIGHT - target_h) // 2 - 10
  143. # 4. Glow behind logo (two layers: wide diffuse + tight bright)
  144. print(" Rendering glow effects...")
  145. # Wide diffuse glow — very prominent
  146. glow_wide = create_glow(logo, ACCENT, radius=GLOW_RADIUS, intensity=3.0)
  147. glow_canvas = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
  148. glow_canvas.paste(glow_wide, (logo_x, logo_y), glow_wide)
  149. canvas = Image.alpha_composite(canvas.convert("RGBA"), glow_canvas)
  150. # Medium glow layer for extra punch
  151. glow_mid = create_glow(logo, ACCENT_GLOW, radius=GLOW_RADIUS // 2, intensity=2.5)
  152. glow_canvas_mid = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
  153. glow_canvas_mid.paste(glow_mid, (logo_x, logo_y), glow_mid)
  154. canvas = Image.alpha_composite(canvas, glow_canvas_mid)
  155. # Tighter bright glow core
  156. glow_tight = create_glow(logo, ACCENT_GLOW, radius=GLOW_RADIUS // 4, intensity=2.0)
  157. glow_canvas2 = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
  158. glow_canvas2.paste(glow_tight, (logo_x, logo_y), glow_tight)
  159. canvas = Image.alpha_composite(canvas, glow_canvas2)
  160. # 5. Composite logo on top
  161. print(" Compositing logo...")
  162. canvas.paste(logo, (logo_x, logo_y), logo)
  163. # 6. Apply vignette
  164. print(" Applying vignette...")
  165. vignette = create_vignette((WIDTH, HEIGHT), VIGNETTE_STRENGTH)
  166. canvas_rgb = canvas.convert("RGB")
  167. # Multiply canvas by vignette mask
  168. r, g, b = canvas_rgb.split()
  169. r = Image.composite(r, Image.new("L", (WIDTH, HEIGHT), 0), vignette)
  170. g = Image.composite(g, Image.new("L", (WIDTH, HEIGHT), 0), vignette)
  171. b = Image.composite(b, Image.new("L", (WIDTH, HEIGHT), 0), vignette)
  172. canvas = Image.merge("RGB", (r, g, b))
  173. # 7. Save
  174. canvas.save(output_path, "PNG", optimize=True)
  175. file_size = os.path.getsize(output_path) / 1024
  176. print(f" Saved to {output_path} ({file_size:.0f} KB)")
  177. if __name__ == "__main__":
  178. out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(os.path.abspath(__file__)), "splash.png")
  179. generate_splash(out)