From fda6e0f74e9bc1b2961b2339f13d81a67b093004 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Fri, 25 Sep 2026 15:17:33 -0400 Subject: [PATCH] =?UTF-8?q?feat(seasonal):=20Halloween=20=E2=80=94=20more?= =?UTF-8?q?=20going=20on:=20wisps,=20a=20witch,=20a=20swarm=20(#239)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jared felt the first redesign was too sparse for a full theme: most of the time only two webs and a spider were on screen. - Will-o'-the-wisps: six soft green/orange glows wandering and pulsing, always present while animating (not in the still scene, where static glows would sit on text). - A witch on a broom crosses just below the header about once a minute (redrawn so she reads as a seated figure, not a blob). - Bats every 20 s (was 30), plus a faster swarm of eight every 60 s. - The big spider now climbs up its thread and drops back with a bounce. - A second, small spider on the left web; a tiny jack-o'-lantern with a flickering candle hangs by the right web (desktop). Rendered and inspected at the witch/swarm moments in light, dark and phone, plus reduced; still 60 fps on vs off. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .../seasonal/themes/Halloween.css.ts | 62 ++++++ .../components/seasonal/themes/Halloween.tsx | 194 ++++++++++++++++-- 2 files changed, 244 insertions(+), 12 deletions(-) diff --git a/src/app/components/seasonal/themes/Halloween.css.ts b/src/app/components/seasonal/themes/Halloween.css.ts index 70ce577ce..110e80265 100644 --- a/src/app/components/seasonal/themes/Halloween.css.ts +++ b/src/app/components/seasonal/themes/Halloween.css.ts @@ -82,3 +82,65 @@ export const animGhostWobble = keyframes({ '50%': { transform: 'rotate(4deg)' }, '100%': { transform: 'rotate(-4deg)' }, }); + +/** [#239] Will-o'-the-wisp: a slow wandering loop (paired with a pulse). */ +export const animWispWander = keyframes({ + '0%': { transform: 'translate3d(0, 0, 0)' }, + '25%': { transform: 'translate3d(40px, -30px, 0)' }, + '50%': { transform: 'translate3d(-10px, -60px, 0)' }, + '75%': { transform: 'translate3d(-45px, -20px, 0)' }, + '100%': { transform: 'translate3d(0, 0, 0)' }, +}); + +/** Soft breathing glow of a wisp. */ +export const animWispPulse = keyframes({ + '0%': { opacity: '0.35', transform: 'scale(0.85)' }, + '50%': { opacity: '0.9', transform: 'scale(1.1)' }, + '100%': { opacity: '0.35', transform: 'scale(0.85)' }, +}); + +/** A witch on a broom crosses high up, right to left, in ~11 s of 60 s. */ +export const animWitch = keyframes({ + '0%': { transform: 'translate3d(0, 0, 0) rotate(-4deg)', opacity: '0' }, + '1%': { opacity: '1' }, + '9%': { transform: 'translate3d(-60vw, -3vh, 0) rotate(3deg)' }, + '17.5%': { opacity: '1' }, + '18%': { transform: 'translate3d(-118vw, 2vh, 0) rotate(-3deg)', opacity: '0' }, + '100%': { transform: 'translate3d(-118vw, 2vh, 0) rotate(-3deg)', opacity: '0' }, +}); + +/** + * The big spider: bobs for most of the cycle, then climbs up its thread, + * pauses, and drops back with a little bounce (24 s cycle). + */ +export const animSpiderClimb = keyframes({ + '0%': { transform: 'translate3d(0, 0, 0)' }, + '20%': { transform: 'translate3d(0, 14px, 0)' }, + '40%': { transform: 'translate3d(0, 0, 0)' }, + '55%': { transform: 'translate3d(0, -22vh, 0)' }, + '68%': { transform: 'translate3d(0, -22vh, 0)' }, + '71%': { transform: 'translate3d(0, 24px, 0)' }, + '74%': { transform: 'translate3d(0, -8px, 0)' }, + '77%': { transform: 'translate3d(0, 6px, 0)' }, + '80%': { transform: 'translate3d(0, 0, 0)' }, + '100%': { transform: 'translate3d(0, 0, 0)' }, +}); + +/** Jack-o'-lantern candle flicker (opacity of the glow behind the face). */ +export const animCandle = keyframes({ + '0%': { opacity: '0.75' }, + '20%': { opacity: '1' }, + '35%': { opacity: '0.6' }, + '55%': { opacity: '0.95' }, + '70%': { opacity: '0.7' }, + '100%': { opacity: '0.75' }, +}); + +/** The swarm crosses faster than the regular bats: ~7 s of a 60 s cycle. */ +export const animSwarmFlight = keyframes({ + '0%': { transform: 'translate3d(-10vw, 0, 0)', opacity: '0' }, + '0.5%': { opacity: '1' }, + '11.5%': { opacity: '1' }, + '12%': { transform: 'translate3d(110vw, -8vh, 0)', opacity: '0' }, + '100%': { transform: 'translate3d(110vw, -8vh, 0)', opacity: '0' }, +}); diff --git a/src/app/components/seasonal/themes/Halloween.tsx b/src/app/components/seasonal/themes/Halloween.tsx index dc3298e81..3b29db4b6 100644 --- a/src/app/components/seasonal/themes/Halloween.tsx +++ b/src/app/components/seasonal/themes/Halloween.tsx @@ -4,10 +4,16 @@ import { animBatBob, animBatFlap, animBatFlight, + animCandle, animGhost, animGhostWobble, animSpiderBob, + animSpiderClimb, animSpiderSway, + animSwarmFlight, + animWispPulse, + animWispWander, + animWitch, halloweenRoot, } from './Halloween.css'; @@ -134,30 +140,34 @@ function Spider({ size }: { size: number }) { ); } -/** Spider hanging from the top edge; thread and body bob together. */ +/** Spider hanging from the top edge; thread and body move together. */ function HangingSpider({ left, length, animate, + size = 30, + climb = false, }: { left: string; length: string; animate: boolean; + size?: number; + climb?: boolean; }) { + let motion: string | undefined; + if (animate) { + motion = climb + ? `${animSpiderClimb} 24s ease-in-out 6s infinite` + : `${animSpiderBob} 7s ease-in-out infinite`; + } return (