Files
cinny/src/app/components/seasonal/themes/Halloween.css.ts
T
Lotus CIandClaude Opus 5.5 fda6e0f74e
CI / Build & Quality Checks (pull_request) Successful in 1m47s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 7s
CI / Playwright smoke (e2e) (pull_request) Successful in 13m39s
feat(seasonal): Halloween — more going on: wisps, a witch, a swarm (#239)
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-25 15:17:33 -04:00

147 lines
5.3 KiB
TypeScript

import { keyframes, style } from '@vanilla-extract/css';
/**
* Halloween overlay styles. Motion is transform/opacity only.
*
* [#239] Colours come from two custom properties set on the overlay root so
* the bats/spider stay dark silhouettes on light themes and turn to a pale
* dusk-violet on dark themes (a black bat on a black theme is invisible).
*/
export const halloweenRoot = style({
position: 'absolute',
inset: 0,
vars: {
'--hw-ink': 'oklch(0.24 0.04 300)',
'--hw-web': 'oklch(0.42 0.02 290 / 0.42)',
'--hw-eye': 'oklch(0.72 0.18 55)',
},
selectors: {
'body.dark-theme &, body.butter-theme &': {
vars: {
'--hw-ink': 'oklch(0.72 0.06 300)',
'--hw-web': 'oklch(0.85 0.02 290 / 0.36)',
'--hw-eye': 'oklch(0.8 0.17 60)',
},
},
},
});
/** A bat group crosses the top of the screen in ~9 s, then a long calm. */
export const animBatFlight = keyframes({
'0%': { transform: 'translate3d(-10vw, 0, 0)', opacity: '0' },
'1%': { opacity: '1' },
'29%': { opacity: '1' },
'30%': { transform: 'translate3d(110vw, -6vh, 0)', opacity: '0' },
'100%': { transform: 'translate3d(110vw, -6vh, 0)', opacity: '0' },
});
/** Up-and-down bobbing on each bat, so the group doesn't move as one block. */
export const animBatBob = keyframes({
'0%': { transform: 'translate3d(0, 0, 0)' },
'50%': { transform: 'translate3d(0, 14px, 0)' },
'100%': { transform: 'translate3d(0, 0, 0)' },
});
/** Wing beat: the silhouette squashes vertically around the body. */
export const animBatFlap = keyframes({
'0%': { transform: 'scaleY(1)' },
'50%': { transform: 'scaleY(0.62)' },
'100%': { transform: 'scaleY(1)' },
});
/** The spider bobs on its thread (thread and spider move together). */
export const animSpiderBob = keyframes({
'0%': { transform: 'translate3d(0, 0, 0)' },
'50%': { transform: 'translate3d(0, 18px, 0)' },
'100%': { transform: 'translate3d(0, 0, 0)' },
});
/** A slow sway of the spider itself on the end of the thread. */
export const animSpiderSway = keyframes({
'0%': { transform: 'rotate(-6deg)' },
'50%': { transform: 'rotate(6deg)' },
'100%': { transform: 'rotate(-6deg)' },
});
/**
* A small ghost rises from the lower part of the screen, drifting sideways,
* and fades out — ~8 s in a 50 s cycle.
*/
export const animGhost = keyframes({
'0%': { transform: 'translate3d(0, 0, 0) scale(0.9)', opacity: '0' },
'3%': { opacity: '0.8' },
'8%': { transform: 'translate3d(6vw, -20vh, 0) scale(1)' },
'13%': { opacity: '0.75' },
'16%': { transform: 'translate3d(-2vw, -42vh, 0) scale(1.05)', opacity: '0' },
'100%': { transform: 'translate3d(-2vw, -42vh, 0) scale(1.05)', opacity: '0' },
});
/** The ghost's tail wisps, a gentle wobble. */
export const animGhostWobble = keyframes({
'0%': { transform: 'rotate(-4deg)' },
'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' },
});