feat: revert to light background, fix DMG layout arrow
Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
47e3714e31
commit
312b7a7580
@@ -2597,31 +2597,33 @@
|
||||
class="relative h-screen w-full flex flex-col bg-exo-dark-gray overflow-hidden"
|
||||
>
|
||||
<!-- Scanline overlay -->
|
||||
<div
|
||||
class="fixed inset-0 pointer-events-none z-50 scanlines opacity-20"
|
||||
></div>
|
||||
{#if !showOnboarding}
|
||||
<div
|
||||
class="fixed inset-0 pointer-events-none z-50 scanlines opacity-20"
|
||||
></div>
|
||||
|
||||
<!-- Shooting Stars Background - one every ~15s -->
|
||||
<div class="shooting-stars">
|
||||
<div
|
||||
class="shooting-star"
|
||||
style="top: 10%; left: 20%; --duration: 45s; --delay: 0s;"
|
||||
></div>
|
||||
<div
|
||||
class="shooting-star"
|
||||
style="top: 30%; left: 65%; --duration: 45s; --delay: 15s;"
|
||||
></div>
|
||||
<div
|
||||
class="shooting-star"
|
||||
style="top: 50%; left: 40%; --duration: 45s; --delay: 30s;"
|
||||
></div>
|
||||
</div>
|
||||
<!-- Shooting Stars Background - one every ~15s -->
|
||||
<div class="shooting-stars">
|
||||
<div
|
||||
class="shooting-star"
|
||||
style="top: 10%; left: 20%; --duration: 45s; --delay: 0s;"
|
||||
></div>
|
||||
<div
|
||||
class="shooting-star"
|
||||
style="top: 30%; left: 65%; --duration: 45s; --delay: 15s;"
|
||||
></div>
|
||||
<div
|
||||
class="shooting-star"
|
||||
style="top: 50%; left: 40%; --duration: 45s; --delay: 30s;"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showOnboarding}
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<!-- FULL-SCREEN ONBOARDING WIZARD -->
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="flex-1 flex items-center justify-center relative z-10">
|
||||
<div class="flex-1 flex items-center justify-center relative z-10 bg-white">
|
||||
{#if onboardingStep === 1}
|
||||
<!-- Step 1: Welcome -->
|
||||
<div
|
||||
@@ -2636,11 +2638,11 @@
|
||||
exo
|
||||
</div>
|
||||
<h1
|
||||
class="text-2xl font-sans font-light text-white/90 mb-3 tracking-wide"
|
||||
class="text-2xl font-sans font-light text-gray-900 mb-3 tracking-wide"
|
||||
>
|
||||
Welcome to exo
|
||||
</h1>
|
||||
<p class="text-base font-sans text-white/50 leading-relaxed">
|
||||
<p class="text-base font-sans text-gray-500 leading-relaxed">
|
||||
Run AI models locally, across all your devices.
|
||||
</p>
|
||||
</div>
|
||||
@@ -2674,7 +2676,7 @@
|
||||
>
|
||||
<div class="text-center mb-4">
|
||||
<h1
|
||||
class="text-2xl font-sans font-light text-white/90 mb-2 tracking-wide"
|
||||
class="text-2xl font-sans font-light text-gray-900 mb-2 tracking-wide"
|
||||
>
|
||||
Add more devices, run bigger models
|
||||
</h1>
|
||||
@@ -2791,7 +2793,7 @@
|
||||
x="42"
|
||||
y="92"
|
||||
text-anchor="middle"
|
||||
fill="rgba(255,255,255,0.7)"
|
||||
fill="rgba(0,0,0,0.7)"
|
||||
style="font-size: 13px; font-family: system-ui, sans-serif;"
|
||||
>
|
||||
MacBook Pro
|
||||
@@ -2800,7 +2802,7 @@
|
||||
x="42"
|
||||
y="108"
|
||||
text-anchor="middle"
|
||||
fill="rgba(255,255,255,0.35)"
|
||||
fill="rgba(0,0,0,0.35)"
|
||||
style="font-size: 11px; font-family: 'SF Mono', monospace;"
|
||||
>
|
||||
36 GB
|
||||
@@ -2862,7 +2864,7 @@
|
||||
x="39"
|
||||
y="86"
|
||||
text-anchor="middle"
|
||||
fill="rgba(255,255,255,0.7)"
|
||||
fill="rgba(0,0,0,0.7)"
|
||||
style="font-size: 13px; font-family: system-ui, sans-serif;"
|
||||
>
|
||||
Mac Studio
|
||||
@@ -2871,7 +2873,7 @@
|
||||
x="39"
|
||||
y="102"
|
||||
text-anchor="middle"
|
||||
fill="rgba(255,255,255,0.35)"
|
||||
fill="rgba(0,0,0,0.35)"
|
||||
style="font-size: 11px; font-family: 'SF Mono', monospace;"
|
||||
>
|
||||
192 GB
|
||||
@@ -2939,15 +2941,15 @@
|
||||
width="110"
|
||||
height="48"
|
||||
rx="8"
|
||||
fill="rgba(255,255,255,0.03)"
|
||||
stroke="rgba(255,255,255,0.12)"
|
||||
fill="rgba(0,0,0,0.04)"
|
||||
stroke="rgba(0,0,0,0.1)"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<text
|
||||
x="55"
|
||||
y="20"
|
||||
text-anchor="middle"
|
||||
fill="rgba(255,255,255,0.7)"
|
||||
fill="rgba(0,0,0,0.7)"
|
||||
style="font-size: 13px; font-family: system-ui, sans-serif;"
|
||||
>
|
||||
Qwen3 8B
|
||||
@@ -2956,7 +2958,7 @@
|
||||
x="55"
|
||||
y="37"
|
||||
text-anchor="middle"
|
||||
fill="rgba(255,255,255,0.35)"
|
||||
fill="rgba(0,0,0,0.35)"
|
||||
style="font-size: 11px; font-family: 'SF Mono', monospace;"
|
||||
>
|
||||
4 GB
|
||||
@@ -2978,10 +2980,10 @@
|
||||
rx="8"
|
||||
fill={deviceAnimPhase >= 3
|
||||
? "rgba(255,215,0,0.06)"
|
||||
: "rgba(255,255,255,0.015)"}
|
||||
: "rgba(0,0,0,0.03)"}
|
||||
stroke={deviceAnimPhase >= 3
|
||||
? "rgba(255,215,0,0.35)"
|
||||
: "rgba(255,255,255,0.06)"}
|
||||
: "rgba(0,0,0,0.08)"}
|
||||
stroke-width="1"
|
||||
filter={deviceAnimPhase >= 3
|
||||
? "url(#onb-gold-glow)"
|
||||
@@ -2997,13 +2999,13 @@
|
||||
height="11"
|
||||
rx="2"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
stroke="rgba(0,0,0,0.2)"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<path
|
||||
d="M 2 9 V 6 C 2 3 4.5 1 7 1 C 9.5 1 12 3 12 6 V 9"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
stroke="rgba(0,0,0,0.2)"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</g>
|
||||
@@ -3044,10 +3046,10 @@
|
||||
rx="8"
|
||||
fill={deviceAnimPhase >= 4
|
||||
? "rgba(255,215,0,0.06)"
|
||||
: "rgba(255,255,255,0.015)"}
|
||||
: "rgba(0,0,0,0.03)"}
|
||||
stroke={deviceAnimPhase >= 4
|
||||
? "rgba(255,215,0,0.35)"
|
||||
: "rgba(255,255,255,0.06)"}
|
||||
: "rgba(0,0,0,0.08)"}
|
||||
stroke-width="1"
|
||||
filter={deviceAnimPhase >= 4
|
||||
? "url(#onb-gold-glow)"
|
||||
@@ -3063,13 +3065,13 @@
|
||||
height="11"
|
||||
rx="2"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
stroke="rgba(0,0,0,0.2)"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<path
|
||||
d="M 2 9 V 6 C 2 3 4.5 1 7 1 C 9.5 1 12 3 12 6 V 9"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
stroke="rgba(0,0,0,0.2)"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</g>
|
||||
@@ -3110,10 +3112,10 @@
|
||||
rx="8"
|
||||
fill={deviceAnimPhase >= 4
|
||||
? "rgba(255,215,0,0.08)"
|
||||
: "rgba(255,255,255,0.015)"}
|
||||
: "rgba(0,0,0,0.03)"}
|
||||
stroke={deviceAnimPhase >= 4
|
||||
? "rgba(255,215,0,0.45)"
|
||||
: "rgba(255,255,255,0.06)"}
|
||||
: "rgba(0,0,0,0.08)"}
|
||||
stroke-width={deviceAnimPhase >= 4 ? "1.5" : "1"}
|
||||
filter={deviceAnimPhase >= 4
|
||||
? "url(#onb-gold-glow)"
|
||||
@@ -3129,13 +3131,13 @@
|
||||
height="11"
|
||||
rx="2"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
stroke="rgba(0,0,0,0.2)"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<path
|
||||
d="M 2 9 V 6 C 2 3 4.5 1 7 1 C 9.5 1 12 3 12 6 V 9"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
stroke="rgba(0,0,0,0.2)"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</g>
|
||||
@@ -3168,7 +3170,7 @@
|
||||
x="300"
|
||||
y="72"
|
||||
text-anchor="middle"
|
||||
fill="rgba(255,255,255,0.25)"
|
||||
fill="rgba(0,0,0,0.3)"
|
||||
style="font-size: 11px; font-family: system-ui, sans-serif; text-transform: uppercase; letter-spacing: 0.12em;"
|
||||
in:fade={{ duration: 400 }}
|
||||
>
|
||||
@@ -3213,11 +3215,11 @@
|
||||
>
|
||||
<div class="text-center mb-6">
|
||||
<h1
|
||||
class="text-2xl font-sans font-light text-white/90 mb-2 tracking-wide"
|
||||
class="text-2xl font-sans font-light text-gray-900 mb-2 tracking-wide"
|
||||
>
|
||||
Your devices
|
||||
</h1>
|
||||
<p class="text-sm font-sans text-white/50">
|
||||
<p class="text-sm font-sans text-gray-500">
|
||||
{nodeCount} device{nodeCount !== 1 ? "s" : ""} connected
|
||||
{#if clusterTotalMemoryGB() > 0}
|
||||
· {clusterTotalMemoryGB().toFixed(0)} GB total memory
|
||||
@@ -3225,7 +3227,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-full h-80 bg-exo-dark-gray/40 rounded-lg overflow-hidden border border-white/5 mb-8"
|
||||
class="w-full h-80 bg-exo-dark-gray rounded-lg overflow-hidden border border-gray-200 mb-8"
|
||||
>
|
||||
<TopologyGraph
|
||||
class="w-full h-full"
|
||||
@@ -3235,7 +3237,7 @@
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm font-sans text-white/40 mt-2 max-w-md text-center leading-relaxed"
|
||||
class="text-sm font-sans text-gray-400 mt-2 max-w-md text-center leading-relaxed"
|
||||
>
|
||||
Install exo on more devices on your network to combine their power —
|
||||
they connect automatically.
|
||||
@@ -3270,18 +3272,18 @@
|
||||
>
|
||||
<div class="text-center mb-8">
|
||||
<h1
|
||||
class="text-2xl font-sans font-light text-white/90 mb-2 tracking-wide"
|
||||
class="text-2xl font-sans font-light text-gray-900 mb-2 tracking-wide"
|
||||
>
|
||||
Choose a model
|
||||
</h1>
|
||||
<p class="text-sm font-sans text-white/50">
|
||||
<p class="text-sm font-sans text-gray-500">
|
||||
Pick a model to download and run locally.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if onboardingError}
|
||||
<div
|
||||
class="w-full mb-6 px-4 py-3 rounded-lg border border-red-500/30 bg-red-500/10 text-sm font-mono text-red-300"
|
||||
class="w-full mb-6 px-4 py-3 rounded-lg border border-red-200 bg-red-50 text-sm font-mono text-red-600"
|
||||
in:fade={{ duration: 200 }}
|
||||
>
|
||||
{onboardingError}
|
||||
@@ -3290,7 +3292,7 @@
|
||||
|
||||
{#if onboardingModels.length === 0}
|
||||
<div class="text-center py-8">
|
||||
<div class="text-sm text-white/40 font-sans animate-pulse">
|
||||
<div class="text-sm text-gray-400 font-sans animate-pulse">
|
||||
Loading models...
|
||||
</div>
|
||||
</div>
|
||||
@@ -3304,13 +3306,13 @@
|
||||
type="button"
|
||||
onclick={() => onboardingLaunchModel(model.id)}
|
||||
class="w-full flex items-center justify-between gap-4 px-5 py-4 rounded-xl border transition-all duration-200 cursor-pointer {fitsNow
|
||||
? 'border-white/8 bg-white/[0.03] hover:border-exo-yellow/30 hover:bg-white/[0.05]'
|
||||
: 'border-white/5 bg-white/[0.01] hover:border-white/10 opacity-60'}"
|
||||
? 'border-gray-200 bg-gray-50 hover:border-exo-yellow/50 hover:bg-yellow-50/50'
|
||||
: 'border-gray-200 bg-gray-50/50 hover:border-gray-300 opacity-60'}"
|
||||
>
|
||||
<div class="flex flex-col items-start gap-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-sm font-sans font-medium text-white/90 truncate"
|
||||
class="text-sm font-sans font-medium text-gray-900 truncate"
|
||||
>{model.name || model.id}</span
|
||||
>
|
||||
{#each tags as tag}
|
||||
@@ -3320,16 +3322,16 @@
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
<span class="text-xs font-mono text-white/30 truncate"
|
||||
<span class="text-xs font-mono text-gray-400 truncate"
|
||||
>{model.id}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<span class="text-xs font-mono text-white/40"
|
||||
<span class="text-xs font-mono text-gray-500"
|
||||
>{sizeGB >= 1 ? sizeGB.toFixed(0) : sizeGB.toFixed(1)} GB</span
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-white/30"
|
||||
class="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -3352,7 +3354,7 @@
|
||||
onclick={() => {
|
||||
isModelPickerOpen = true;
|
||||
}}
|
||||
class="text-sm font-sans text-white/40 hover:text-exo-yellow/80 transition-colors cursor-pointer underline underline-offset-4 decoration-white/15 hover:decoration-exo-yellow/30"
|
||||
class="text-sm font-sans text-gray-400 hover:text-exo-yellow transition-colors cursor-pointer underline underline-offset-4 decoration-gray-300 hover:decoration-exo-yellow/30"
|
||||
>
|
||||
Browse all models
|
||||
</button>
|
||||
@@ -3366,11 +3368,11 @@
|
||||
>
|
||||
<div class="mb-8">
|
||||
<h1
|
||||
class="text-2xl font-sans font-light text-white/90 mb-2 tracking-wide"
|
||||
class="text-2xl font-sans font-light text-gray-900 mb-2 tracking-wide"
|
||||
>
|
||||
Downloading
|
||||
</h1>
|
||||
<p class="text-sm text-white/60">
|
||||
<p class="text-sm text-gray-500">
|
||||
{#if onboardingModelId}
|
||||
<span class="text-exo-yellow">{onboardingModelId}</span>
|
||||
{/if}
|
||||
@@ -3379,38 +3381,38 @@
|
||||
|
||||
{#if onboardingDownloadProgress}
|
||||
<div class="w-full max-w-md mx-auto space-y-4">
|
||||
<div class="relative h-2 bg-white/5 rounded-full overflow-hidden">
|
||||
<div class="relative h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 bg-gradient-to-r from-exo-yellow to-amber-400 rounded-full transition-all duration-500"
|
||||
style="width: {onboardingDownloadProgress.percentage}%"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs font-mono text-white/50">
|
||||
<div class="flex justify-between text-xs font-mono text-gray-500">
|
||||
<span>{onboardingDownloadProgress.percentage.toFixed(1)}%</span>
|
||||
<span
|
||||
>{formatBytes(onboardingDownloadProgress.downloadedBytes)} /
|
||||
{formatBytes(onboardingDownloadProgress.totalBytes)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs font-mono text-white/40">
|
||||
<div class="flex justify-between text-xs font-mono text-gray-400">
|
||||
<span>{formatSpeed(onboardingDownloadProgress.speed)}</span>
|
||||
<span>ETA: {formatEta(onboardingDownloadProgress.etaMs)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full max-w-md mx-auto">
|
||||
<div class="relative h-2 bg-white/5 rounded-full overflow-hidden">
|
||||
<div class="relative h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-exo-yellow to-amber-400 rounded-full animate-pulse"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-xs font-mono text-white/40 mt-4">
|
||||
<p class="text-xs font-mono text-gray-400 mt-4">
|
||||
Preparing download...
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<p class="text-xs font-sans text-white/30 mt-8">
|
||||
<p class="text-xs font-sans text-gray-300 mt-8">
|
||||
This may take a few minutes depending on your connection.
|
||||
</p>
|
||||
</div>
|
||||
@@ -3423,11 +3425,11 @@
|
||||
>
|
||||
<div class="mb-8">
|
||||
<h1
|
||||
class="text-2xl font-sans font-light text-white/90 mb-2 tracking-wide"
|
||||
class="text-2xl font-sans font-light text-gray-900 mb-2 tracking-wide"
|
||||
>
|
||||
Loading into memory
|
||||
</h1>
|
||||
<p class="text-sm text-white/60">
|
||||
<p class="text-sm text-gray-500">
|
||||
{#if onboardingModelId}
|
||||
<span class="text-exo-yellow">{onboardingModelId}</span>
|
||||
{/if}
|
||||
@@ -3440,7 +3442,7 @@
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-white/40 font-sans">Almost ready...</p>
|
||||
<p class="text-sm text-gray-400 font-sans">Almost ready...</p>
|
||||
</div>
|
||||
{:else if onboardingStep === 7}
|
||||
<!-- Step 7: Chat — centered input auto-appears, first message transitions to dashboard -->
|
||||
@@ -3451,20 +3453,20 @@
|
||||
>
|
||||
<!-- Subtle branding -->
|
||||
<div
|
||||
class="text-2xl font-mono text-white/15 font-bold tracking-wider mb-8"
|
||||
class="text-2xl font-mono text-gray-200 font-bold tracking-wider mb-8"
|
||||
>
|
||||
exo
|
||||
</div>
|
||||
|
||||
<!-- Model name -->
|
||||
{#if onboardingModelId}
|
||||
<p class="text-sm text-white/35 font-sans mb-4">
|
||||
<p class="text-sm text-gray-400 font-sans mb-4">
|
||||
{onboardingModelId.split("/").pop() ?? onboardingModelId}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<!-- Centered ChatForm — first message completes onboarding -->
|
||||
<div class="w-full">
|
||||
<div class="w-full bg-exo-dark-gray rounded-xl p-4">
|
||||
<ChatForm
|
||||
placeholder="Ask anything"
|
||||
autofocus={true}
|
||||
@@ -3485,7 +3487,7 @@
|
||||
sendMessage(chip);
|
||||
completeOnboarding();
|
||||
}}
|
||||
class="px-4 py-2 rounded-full border border-white/8 bg-white/[0.03] text-[13px] font-sans text-white/50 hover:bg-white/[0.07] hover:text-white/70 hover:border-white/15 transition-all duration-200 cursor-pointer"
|
||||
class="px-4 py-2 rounded-full border border-gray-200 bg-gray-50 text-[13px] font-sans text-gray-500 hover:bg-gray-100 hover:text-gray-700 hover:border-gray-300 transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
{chip}
|
||||
</button>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 6.8 KiB |
@@ -1,34 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Copy the static DMG background image to the specified output path.
|
||||
"""Generate the DMG background image with a centered drag-to-Applications arrow.
|
||||
|
||||
The background is a 1600×740 retina PNG (2× for 800×400 logical window) with a
|
||||
hand-drawn arrow and yellow bookmark accents on a white background. Based on
|
||||
Ollama's DMG background (MIT-licensed).
|
||||
The output is a 1600×740 retina PNG (2× for 800×400 logical window).
|
||||
Icons are positioned at (200, 190) and (600, 190) in logical coordinates;
|
||||
the arrow is drawn centered between them.
|
||||
|
||||
Usage:
|
||||
python3 generate-background.py output.png
|
||||
python3 generate-background.py [output.png]
|
||||
|
||||
If no output path is given, overwrites the bundled background.png in-place.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BACKGROUND_PNG = Path(__file__).parent / "background.png"
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
# Retina dimensions (2× logical 800×400)
|
||||
WIDTH = 1600
|
||||
HEIGHT = 740
|
||||
|
||||
# Icon positions in logical coords → retina coords
|
||||
# App icon at (200, 190), Applications at (600, 190)
|
||||
APP_X = 200 * 2 # 400
|
||||
APPS_X = 600 * 2 # 1200
|
||||
ICON_Y = 190 * 2 # 380
|
||||
|
||||
# Arrow drawn between icons, slightly above icon center
|
||||
ARROW_START_X = APP_X + 160 # past the icon
|
||||
ARROW_END_X = APPS_X - 160 # before the Applications icon
|
||||
ARROW_Y = ICON_Y # same height as icons
|
||||
ARROW_RISE = 120 # upward arc height
|
||||
|
||||
|
||||
def draw_arrow(draw: ImageDraw.ImageDraw) -> None:
|
||||
"""Draw a hand-drawn-style curved arrow from app icon toward Applications."""
|
||||
color = (30, 30, 30)
|
||||
line_width = 8
|
||||
|
||||
# Compute bezier curve points for a gentle upward arc
|
||||
points: list[tuple[float, float]] = []
|
||||
steps = 80
|
||||
for i in range(steps + 1):
|
||||
t = i / steps
|
||||
# Quadratic bezier: start → control → end
|
||||
cx = (ARROW_START_X + ARROW_END_X) / 2
|
||||
cy = ARROW_Y - ARROW_RISE
|
||||
x = (1 - t) ** 2 * ARROW_START_X + 2 * (1 - t) * t * cx + t**2 * ARROW_END_X
|
||||
y = (1 - t) ** 2 * ARROW_Y + 2 * (1 - t) * t * cy + t**2 * ARROW_Y
|
||||
points.append((x, y))
|
||||
|
||||
# Draw the curve as connected line segments
|
||||
for i in range(len(points) - 1):
|
||||
draw.line([points[i], points[i + 1]], fill=color, width=line_width)
|
||||
|
||||
# Arrowhead at the end
|
||||
end_x, end_y = points[-1]
|
||||
# Direction from second-to-last to last point
|
||||
prev_x, prev_y = points[-3]
|
||||
angle = math.atan2(end_y - prev_y, end_x - prev_x)
|
||||
head_len = 36
|
||||
head_angle = math.radians(25)
|
||||
|
||||
left_x = end_x - head_len * math.cos(angle - head_angle)
|
||||
left_y = end_y - head_len * math.sin(angle - head_angle)
|
||||
right_x = end_x - head_len * math.cos(angle + head_angle)
|
||||
right_y = end_y - head_len * math.sin(angle + head_angle)
|
||||
|
||||
draw.polygon(
|
||||
[(end_x, end_y), (left_x, left_y), (right_x, right_y)],
|
||||
fill=color,
|
||||
)
|
||||
|
||||
|
||||
def generate_background(output_path: str) -> None:
|
||||
"""Copy the static background image to the output path."""
|
||||
if not BACKGROUND_PNG.exists():
|
||||
print(f"Error: {BACKGROUND_PNG} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
shutil.copy2(BACKGROUND_PNG, output_path)
|
||||
"""Generate a white DMG background with a centered arrow."""
|
||||
img = Image.new("RGBA", (WIDTH, HEIGHT), (255, 255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw_arrow(draw)
|
||||
img.save(output_path, "PNG")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <output.png>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
generate_background(sys.argv[1])
|
||||
print(f"Background image written to {sys.argv[1]}")
|
||||
default_output = str(Path(__file__).parent / "background.png")
|
||||
out = sys.argv[1] if len(sys.argv) >= 2 else default_output
|
||||
generate_background(out)
|
||||
print(f"Background image written to {out}")
|
||||
|
||||
Reference in New Issue
Block a user