atmo.rsvp
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #25 from flo-bit/feat/spaces

add butterfly background theme

authored by

Florian and committed by
GitHub
c3c9a8e2 f3ec3ab1

+189
+3
src/lib/components/ThemeBackground.svelte
··· 4 4 import Stars from './themes/Stars.svelte'; 5 5 import Matrix from './themes/Matrix.svelte'; 6 6 import Fireflies from './themes/Fireflies.svelte'; 7 + import Butterflies from './themes/Butterflies.svelte'; 7 8 import Kaleidoscope from './themes/Kaleidoscope.svelte'; 8 9 9 10 let { ··· 24 25 <Matrix /> 25 26 {:else if theme.name === 'fireflies'} 26 27 <Fireflies /> 28 + {:else if theme.name === 'butterflies'} 29 + <Butterflies /> 27 30 {:else if theme.name === 'kaleidoscope'} 28 31 <Kaleidoscope /> 29 32 {/if}
+185
src/lib/components/themes/Butterflies.svelte
··· 1 + <script lang="ts"> 2 + import { browser } from '$app/environment'; 3 + 4 + let canvas: HTMLCanvasElement | undefined = $state(undefined); 5 + 6 + $effect(() => { 7 + if (!canvas || !browser) return; 8 + 9 + const ctx = canvas.getContext('2d')!; 10 + let animId: number; 11 + 12 + const COUNT = 22; 13 + const DRIFT_SPEED = 45; // px/sec forward flight 14 + const DART_RATE = 0.15; // darts per second per butterfly 15 + const ROTATION_EASE = 4; // rad/sec — how fast body turns toward heading 16 + 17 + interface Butterfly { 18 + x: number; 19 + y: number; 20 + heading: number; // radians — current direction of travel 21 + rotation: number; // body orientation, lags heading slightly 22 + turnRate: number; // radians/sec of random-walk steering 23 + flapPhase: number; 24 + flapRate: number; // Hz-ish 25 + depthPhase: number; // slow "closer/further" scaling 26 + depthRate: number; // Hz of depth oscillation 27 + size: number; 28 + hueShift: number; 29 + alpha: number; 30 + } 31 + 32 + let lastWidth = 0; 33 + function resize() { 34 + const w = window.innerWidth; 35 + if (w === lastWidth) return; 36 + lastWidth = w; 37 + canvas!.width = w; 38 + canvas!.height = window.screen.height; 39 + } 40 + resize(); 41 + window.addEventListener('resize', resize); 42 + 43 + function spawn(): Butterfly { 44 + const angle = Math.random() * Math.PI * 2; 45 + return { 46 + x: Math.random() * canvas!.width, 47 + y: Math.random() * canvas!.height, 48 + heading: angle, 49 + rotation: angle, 50 + turnRate: 0.8 + Math.random() * 1.2, // 0.8–2 rad/√s of wander 51 + flapPhase: Math.random() * Math.PI * 2, 52 + flapRate: 1.5 + Math.random() * 1.5, // 1.5–3 flaps/sec 53 + depthPhase: Math.random() * Math.PI * 2, 54 + depthRate: 0.08 + Math.random() * 0.08, // 1/12s to 1/6s — 6–12s period 55 + size: 10 + Math.random() * 18, 56 + hueShift: (Math.random() - 0.5) * 40, 57 + alpha: 0.35 + Math.random() * 0.4 58 + }; 59 + } 60 + 61 + const butterflies: Butterfly[] = Array.from({ length: COUNT }, spawn); 62 + 63 + const accentColor = getComputedStyle(document.documentElement) 64 + .getPropertyValue('--color-accent-500') 65 + .trim(); 66 + 67 + // Right wing: top + bottom lobe drawn as a single path so their overlap 68 + // fills once (no alpha accumulation). Positioned so the inner edge sits 69 + // at +x ≈ 0.05, keeping the two wings from bleeding into each other at 70 + // the body axis. 71 + function drawWing(s: number) { 72 + ctx.beginPath(); 73 + ctx.ellipse(s * 0.65, -s * 0.3, s * 0.6, s * 0.5, -0.25, 0, Math.PI * 2); 74 + ctx.ellipse(s * 0.55, s * 0.35, s * 0.5, s * 0.4, 0.2, 0, Math.PI * 2); 75 + ctx.fill(); 76 + } 77 + 78 + function drawButterfly(b: Butterfly) { 79 + // Smooth 0→1 flap at the natural flapRate (Hz) 80 + const flap = (Math.sin(b.flapPhase) + 1) / 2; 81 + const wingScaleX = 0.45 + 0.55 * flap; // 0.45 (folded) → 1.0 (open) 82 + 83 + // Combined scale: slow "depth" breathing (±25% over ~10s) plus a tiny 84 + // bump synced to each wing flap so peak-open wings read as closer. 85 + const depth = 1 + 0.25 * Math.sin(b.depthPhase); 86 + const flapBump = 1 + 0.08 * flap; 87 + const scale = depth * flapBump; 88 + 89 + ctx.save(); 90 + ctx.translate(b.x, b.y); 91 + ctx.scale(scale, scale); 92 + ctx.rotate(b.rotation + Math.PI / 2); // body points along velocity 93 + 94 + const fill = accentColor 95 + ? `oklch(from ${accentColor} l c calc(h + ${b.hueShift}) / ${b.alpha})` 96 + : `rgba(80, 140, 240, ${b.alpha})`; 97 + ctx.fillStyle = fill; 98 + 99 + // Right wing 100 + ctx.save(); 101 + ctx.scale(wingScaleX, 1); 102 + drawWing(b.size); 103 + ctx.restore(); 104 + 105 + // Left wing (mirrored) 106 + ctx.save(); 107 + ctx.scale(-wingScaleX, 1); 108 + drawWing(b.size); 109 + ctx.restore(); 110 + 111 + // Tiny body — slightly darker tint of the accent 112 + ctx.fillStyle = accentColor 113 + ? `oklch(from ${accentColor} calc(l * 0.55) c calc(h + ${b.hueShift}) / ${Math.min(1, b.alpha + 0.2)})` 114 + : `rgba(30, 40, 80, ${Math.min(1, b.alpha + 0.2)})`; 115 + ctx.beginPath(); 116 + ctx.ellipse(0, 0, b.size * 0.08, b.size * 0.55, 0, 0, Math.PI * 2); 117 + ctx.fill(); 118 + 119 + ctx.restore(); 120 + } 121 + 122 + let lastTime = performance.now(); 123 + 124 + function draw(now: number) { 125 + const dt = Math.min((now - lastTime) / 1000, 0.1); 126 + lastTime = now; 127 + 128 + const w = canvas!.width; 129 + const h = canvas!.height; 130 + 131 + ctx.clearRect(0, 0, w, h); 132 + 133 + // Framerate-independent turn-toward-heading easing factor. 134 + const rotationBlend = 1 - Math.exp(-ROTATION_EASE * dt); 135 + const sqrtDt = Math.sqrt(dt); 136 + 137 + for (const b of butterflies) { 138 + b.flapPhase += b.flapRate * 2 * Math.PI * dt; 139 + b.depthPhase += b.depthRate * 2 * Math.PI * dt; 140 + 141 + // Heading random walk — sqrt(dt) keeps the per-second angular 142 + // variance constant regardless of framerate (Brownian scaling). 143 + b.heading += (Math.random() - 0.5) * 2 * b.turnRate * sqrtDt; 144 + 145 + // Occasional sharp dart, as a Poisson process at DART_RATE/sec. 146 + if (Math.random() < DART_RATE * dt) { 147 + b.heading += (Math.random() - 0.5) * Math.PI; 148 + } 149 + 150 + b.x += Math.cos(b.heading) * DRIFT_SPEED * dt; 151 + b.y += Math.sin(b.heading) * DRIFT_SPEED * dt; 152 + 153 + // Body orientation eases toward heading with constant time constant. 154 + let delta = b.heading - b.rotation; 155 + while (delta > Math.PI) delta -= Math.PI * 2; 156 + while (delta < -Math.PI) delta += Math.PI * 2; 157 + b.rotation += delta * rotationBlend; 158 + 159 + const margin = b.size * 2; 160 + if (b.x < -margin) b.x = w + margin; 161 + if (b.x > w + margin) b.x = -margin; 162 + if (b.y < -margin) b.y = h + margin; 163 + if (b.y > h + margin) b.y = -margin; 164 + 165 + drawButterfly(b); 166 + } 167 + 168 + animId = requestAnimationFrame(draw); 169 + } 170 + 171 + animId = requestAnimationFrame(draw); 172 + 173 + return () => { 174 + cancelAnimationFrame(animId); 175 + window.removeEventListener('resize', resize); 176 + }; 177 + }); 178 + </script> 179 + 180 + <div class="bg-base-50 dark:bg-base-900 pointer-events-none fixed inset-0 -z-10"> 181 + <canvas 182 + bind:this={canvas} 183 + class="absolute inset-0 h-full w-full opacity-60 blur-[1.5px]" 184 + ></canvas> 185 + </div>
+1
src/lib/theme.ts
··· 26 26 warp: 'Stars', 27 27 matrix: 'Matrix', 28 28 fireflies: 'Fireflies', 29 + butterflies: 'Butterflies', 29 30 kaleidoscope: 'Kaleidoscope' 30 31 };