diff options
| -rw-r--r-- | lib/components/LeafScene.svelte | 197 | ||||
| -rw-r--r-- | lib/display/state.ts | 9 | ||||
| -rw-r--r-- | lib/leaves/physics.ts | 89 | ||||
| -rw-r--r-- | lib/leaves/spawn.ts | 57 | ||||
| -rw-r--r-- | lib/leaves/types.ts | 96 | ||||
| -rw-r--r-- | lib/leaves/variants.ts | 56 | ||||
| -rw-r--r-- | lib/leaves/wind.ts | 259 | ||||
| -rw-r--r-- | lib/screens/BeginScreen.svelte | 6 |
8 files changed, 0 insertions, 769 deletions
diff --git a/lib/components/LeafScene.svelte b/lib/components/LeafScene.svelte deleted file mode 100644 index 53dce6b..0000000 --- a/lib/components/LeafScene.svelte +++ /dev/null @@ -1,197 +0,0 @@ -<script lang="ts"> - import { onDestroy, onMount } from 'svelte' - import { createRNG } from '@hollowdark/rng/seeded' - import { reduceMotion } from '@hollowdark/lib/display/state' - import { LEAF_VARIANTS } from '@hollowdark/lib/leaves/variants' - import { spawnLeaf } from '@hollowdark/lib/leaves/spawn' - import { isLeafOffscreen, stepLeaf } from '@hollowdark/lib/leaves/physics' - import { - initialWindSystem, - particleOpacity, - stepWind, - type IdSource - } from '@hollowdark/lib/leaves/wind' - import type { Leaf, SceneDimensions, WindSystem } from '@hollowdark/lib/leaves/types' - - const TARGET_LEAF_COUNT = 14 - const GROUND_Y_RATIO = 0.86 - - const rng = createRNG(Math.floor(performance.now() * 1000) | 0) - - let container: HTMLDivElement | null = $state(null) - let scene: SceneDimensions = $state({ width: 0, height: 0, groundY: 0 }) - let leaves: Leaf[] = $state([]) - let wind: WindSystem = $state(initialWindSystem(rng)) - - let rafId: number | null = null - let lastTimeMs = 0 - let nextLeafId = 0 - let nextGustId = 0 - let nextParticleId = 0 - let resizeObserver: ResizeObserver | null = null - - const ids: IdSource = { - nextGustId: () => nextGustId++, - nextParticleId: () => nextParticleId++ - } - - function measure(): void { - if (!container) return - const rect = container.getBoundingClientRect() - scene = { - width: rect.width, - height: rect.height, - groundY: rect.height * GROUND_Y_RATIO - } - } - - onMount(() => { - measure() - - for (let i = 0; i < TARGET_LEAF_COUNT; i++) { - leaves = [...leaves, spawnLeaf(nextLeafId++, scene, rng)] - } - - if (container && typeof ResizeObserver !== 'undefined') { - resizeObserver = new ResizeObserver(measure) - resizeObserver.observe(container) - } - - const tick = (nowMs: number): void => { - const dtMs = Math.min(50, nowMs - lastTimeMs) - lastTimeMs = nowMs - const dt = dtMs / 1000 - const timeS = nowMs / 1000 - - wind = stepWind(wind, dtMs, scene, rng, ids) - - const next: Leaf[] = [] - for (const leaf of leaves) { - const advanced = stepLeaf(leaf, dt, timeS, wind, scene) - if (!isLeafOffscreen(advanced, scene)) { - next.push(advanced) - } - } - - while (next.length < TARGET_LEAF_COUNT) { - next.push(spawnLeaf(nextLeafId++, scene, rng)) - } - - leaves = next - rafId = requestAnimationFrame(tick) - } - - rafId = requestAnimationFrame((nowMs) => { - lastTimeMs = nowMs - rafId = requestAnimationFrame(tick) - }) - }) - - onDestroy(() => { - if (rafId !== null) cancelAnimationFrame(rafId) - resizeObserver?.disconnect() - }) -</script> - -{#if !$reduceMotion} -<div class="leaf-scene" bind:this={container} aria-hidden="true"> - <svg class="defs" width="0" height="0" focusable="false"> - <defs> - {#each LEAF_VARIANTS as variant (variant.id)} - <symbol id="leaf-{variant.id}" viewBox={variant.viewBox}> - <path - d={variant.pathData} - fill="currentColor" - stroke="currentColor" - stroke-width="0.6" - stroke-linejoin="round" - /> - </symbol> - {/each} - </defs> - </svg> - - <div class="ground" style:top="{scene.groundY}px"></div> - - {#if wind.activeGust} - {#each wind.activeGust.particles as particle (particle.id)} - <div - class="wind-particle" - style:left="{particle.x}px" - style:top="{particle.y}px" - style:width="{particle.size}px" - style:height="{particle.size}px" - style:opacity={particleOpacity(particle)} - ></div> - {/each} - {/if} - - {#each leaves as leaf (leaf.id)} - <svg - class="leaf" - viewBox="0 0 40 48" - style:left="{leaf.x}px" - style:top="{leaf.y}px" - style:width="{leaf.size}px" - style:color={leaf.color} - style:transform="translate(-50%, -50%) rotate({leaf.rotation}deg)" - focusable="false" - > - <use href="#leaf-{leaf.variantId}" /> - </svg> - {/each} -</div> -{/if} - -<style> - .leaf-scene { - position: absolute; - inset: 0; - overflow: hidden; - pointer-events: none; - z-index: 0; - } - - .defs { - position: absolute; - width: 0; - height: 0; - } - - .ground { - position: absolute; - left: 0; - right: 0; - height: 1px; - background: linear-gradient( - to right, - rgba(232, 226, 213, 0) 0%, - rgba(232, 226, 213, 0.04) 30%, - rgba(232, 226, 213, 0.04) 70%, - rgba(232, 226, 213, 0) 100% - ); - opacity: 0.6; - } - - .leaf { - position: absolute; - display: block; - will-change: transform, top, left; - filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.25)); - } - - .wind-particle { - position: absolute; - border-radius: 50%; - background: rgba(232, 226, 213, 0.45); - transform: translate(-50%, -50%); - pointer-events: none; - box-shadow: 0 0 4px rgba(232, 226, 213, 0.2); - } - - @media (prefers-reduced-motion: reduce) { - .leaf-scene { - display: none; - } - } -</style> diff --git a/lib/display/state.ts b/lib/display/state.ts deleted file mode 100644 index d768e6c..0000000 --- a/lib/display/state.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { writable, type Writable } from 'svelte/store' - -/** - * User-facing override for motion-heavy visuals. When true, the leaf - * scene is suppressed and long transitions are skipped even on systems - * that do not signal `prefers-reduced-motion`. The CSS media query still - * applies independently — this store only strengthens it. - */ -export const reduceMotion: Writable<boolean> = writable(false) diff --git a/lib/leaves/physics.ts b/lib/leaves/physics.ts deleted file mode 100644 index 9510311..0000000 --- a/lib/leaves/physics.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { Leaf, SceneDimensions, WindSystem } from '@hollowdark/lib/leaves/types' -import { windForceAt } from '@hollowdark/lib/leaves/wind' - -const GRAVITY_PX_PER_S2 = 14 -const TERMINAL_VY_PX_PER_S = 35 -const WIND_FORCE_SCALE = 0.55 -const SWAY_AMPLITUDE_PX_PER_S = 16 -const HORIZONTAL_DRAG_PER_FRAME = 0.98 -const VERTICAL_DRAG_PER_FRAME = 0.995 -const SETTLED_HORIZONTAL_DRAG_PER_FRAME = 0.88 -const SETTLED_ROT_DAMPING_PER_FRAME = 0.92 -const SETTLE_LIFT_THRESHOLD = 30 -const OFFSCREEN_MARGIN_PX = 140 - -/** - * Step one leaf forward by `dt` seconds. Applies wind-derived acceleration, - * gravity, drag, and rotation. Ground collision settles the leaf; strong - * upward force from a nearby gust can lift it again. - */ -export function stepLeaf( - leaf: Leaf, - dt: number, - timeS: number, - wind: WindSystem, - scene: SceneDimensions -): Leaf { - const { fx, fy } = windForceAt(wind, leaf.x, leaf.y) - - const sway = leaf.settled - ? 0 - : SWAY_AMPLITUDE_PX_PER_S * - leaf.swayFrequency * - Math.cos(leaf.swayPhase + timeS * leaf.swayFrequency * Math.PI * 2) - - let vx = leaf.vx + (fx * WIND_FORCE_SCALE + sway) * dt - let vy = leaf.vy + fy * WIND_FORCE_SCALE * dt - - if (!leaf.settled) { - vy = Math.min(TERMINAL_VY_PX_PER_S, vy + GRAVITY_PX_PER_S2 * dt) - } - - const frames = dt * 60 - vx *= Math.pow(HORIZONTAL_DRAG_PER_FRAME, frames) - vy *= Math.pow(VERTICAL_DRAG_PER_FRAME, frames) - - if (leaf.settled) { - vx *= Math.pow(SETTLED_HORIZONTAL_DRAG_PER_FRAME, frames) - } - - const x = leaf.x + vx * dt - let y = leaf.y + (leaf.settled ? 0 : vy * dt) - - let settled = leaf.settled - if (!settled && y >= scene.groundY) { - settled = true - y = scene.groundY - vy = Math.min(0, vy) - } - - if (settled && vy < -SETTLE_LIFT_THRESHOLD) { - settled = false - } - - let rotationSpeed = leaf.rotationSpeed - if (settled) { - rotationSpeed *= Math.pow(SETTLED_ROT_DAMPING_PER_FRAME, frames) - } - const rotation = leaf.rotation + rotationSpeed * dt - - return { - ...leaf, - x, - y, - vx, - vy, - rotation, - rotationSpeed, - settled - } -} - -/** A leaf has drifted far enough off-screen to drop from the array. */ -export function isLeafOffscreen(leaf: Leaf, scene: SceneDimensions): boolean { - return ( - leaf.x < -OFFSCREEN_MARGIN_PX || - leaf.x > scene.width + OFFSCREEN_MARGIN_PX || - leaf.y > scene.height + OFFSCREEN_MARGIN_PX - ) -} diff --git a/lib/leaves/spawn.ts b/lib/leaves/spawn.ts deleted file mode 100644 index 631239d..0000000 --- a/lib/leaves/spawn.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { SeededRNG } from '@hollowdark/rng/seeded' -import { LEAF_COLORS, LEAF_VARIANTS } from '@hollowdark/lib/leaves/variants' -import type { Leaf, SceneDimensions } from '@hollowdark/lib/leaves/types' - -const MIN_SIZE = 18 -const MAX_SIZE = 36 -const MIN_ROT_SPEED = -35 -const MAX_ROT_SPEED = 35 -const MIN_SWAY_FREQ_HZ = 0.18 -const MAX_SWAY_FREQ_HZ = 0.5 -const FROM_RIGHT_PROBABILITY = 0.72 - -/** - * Create a fresh leaf entering the scene from the upwind edge. With - * `FROM_RIGHT_PROBABILITY`, leaves spawn off the right side at a random y - * above the ground; the rest drop in from above at a random x. This - * matches a horizontal-wind feel rather than vertical snowfall. - */ -export function spawnLeaf(id: number, scene: SceneDimensions, rng: SeededRNG): Leaf { - const variant = rng.pick(LEAF_VARIANTS) - const color = rng.pick(LEAF_COLORS) - const size = MIN_SIZE + rng.next() * (MAX_SIZE - MIN_SIZE) - const fromRight = rng.nextBool(FROM_RIGHT_PROBABILITY) - - let x: number - let y: number - let vx: number - let vy: number - - if (fromRight) { - x = scene.width + size - y = rng.next() * scene.groundY * 0.88 - vx = -18 - rng.next() * 12 - vy = 2 + rng.next() * 8 - } else { - x = rng.next() * scene.width - y = -size - rng.next() * 40 - vx = -4 + rng.next() * 4 - vy = 12 + rng.next() * 10 - } - - return { - id, - variantId: variant.id, - color, - size, - x, - y, - vx, - vy, - rotation: rng.next() * 360, - rotationSpeed: MIN_ROT_SPEED + rng.next() * (MAX_ROT_SPEED - MIN_ROT_SPEED), - swayPhase: rng.next() * Math.PI * 2, - swayFrequency: MIN_SWAY_FREQ_HZ + rng.next() * (MAX_SWAY_FREQ_HZ - MIN_SWAY_FREQ_HZ), - settled: false - } -} diff --git a/lib/leaves/types.ts b/lib/leaves/types.ts deleted file mode 100644 index 869a296..0000000 --- a/lib/leaves/types.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * An individual leaf silhouette. `pathData` is the `d` attribute of an - * `<path>` inside a `<symbol>` with the given viewBox. Rendered at various - * pixel sizes; the aspect ratio derives from viewBox. - */ -export interface LeafVariant { - readonly id: string - readonly viewBox: string - readonly pathData: string -} - -/** - * One leaf in flight. `x` / `y` are in CSS pixels from the top-left of the - * scene container. `rotation` is degrees. `swayPhase` offsets the horizontal - * sway oscillation per leaf so the cloud doesn't drift in sync. - */ -export interface Leaf { - readonly id: number - readonly variantId: string - readonly color: string - readonly size: number - readonly x: number - readonly y: number - readonly vx: number - readonly vy: number - readonly rotation: number - readonly rotationSpeed: number - readonly swayPhase: number - readonly swayFrequency: number - readonly settled: boolean -} - -/** Viewport dimensions and the y-coordinate where leaves settle. */ -export interface SceneDimensions { - readonly width: number - readonly height: number - readonly groundY: number -} - -/** - * The three gust flavours the wind system can emit. Each has its own - * duration, strength, and particle-spawning profile. - * - * breeze small, slow, gentle, frequent — common idle motion - * gust medium, strong, mostly straight — occasional sweep - * whirl small, circular — rare twirl that spirals around its centre - */ -export type GustKind = 'breeze' | 'gust' | 'whirl' - -/** - * A wisp of visible air inside a gust — a short-lived dot that traces the - * gust's flow. Rendered as a small semi-transparent circle with opacity - * computed from `ageMs / lifetimeMs`. - */ -export interface WindParticle { - readonly id: number - readonly x: number - readonly y: number - readonly vx: number - readonly vy: number - readonly ageMs: number - readonly lifetimeMs: number - readonly size: number -} - -/** - * A localised wind event. Lives for `totalDurationMs`, during which it - * applies force to leaves within `radius` of (`centerX`, `centerY`) and - * spawns `particles` that trace the wind's motion. - */ -export interface Gust { - readonly id: number - readonly kind: GustKind - readonly centerX: number - readonly centerY: number - readonly radius: number - readonly strength: number - readonly direction: number - readonly elapsedMs: number - readonly totalDurationMs: number - readonly particles: readonly WindParticle[] - readonly nextParticleSpawnInMs: number -} - -/** - * The overall wind state. A constant horizontal prevailing wind drifts - * every leaf leftward at a low speed. Gusts are seldom, one at a time, - * and superimposed on the prevailing flow — they only apply within their - * own radius. - */ -export interface WindSystem { - readonly prevailingVx: number - readonly prevailingVy: number - readonly activeGust: Gust | null - readonly nextGustInMs: number -} diff --git a/lib/leaves/variants.ts b/lib/leaves/variants.ts deleted file mode 100644 index d948184..0000000 --- a/lib/leaves/variants.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { LeafVariant } from '@hollowdark/lib/leaves/types' - -/** - * Six recognisable leaf silhouettes sharing a 40×48 viewBox. Each path - * includes both the leaf body and a stem line so it reads as a leaf at - * small render sizes. Rendered with `currentColor` so the leaf's tint is - * set by the host element's inline color style. - */ -export const LEAF_VARIANTS: readonly LeafVariant[] = [ - { - id: 'maple', - viewBox: '0 0 40 48', - pathData: - 'M 20 3 L 22 13 L 30 11 L 26 19 L 34 22 L 26 24 L 30 33 L 22 29 L 20 40 L 18 29 L 10 33 L 14 24 L 6 22 L 14 19 L 10 11 L 18 13 Z M 20 40 L 20 47' - }, - { - id: 'oak', - viewBox: '0 0 40 48', - pathData: - 'M 20 3 C 17 4 14 6 12 9 C 10 8 7 8 7 11 C 7 14 10 15 12 14 C 10 16 7 18 7 22 C 8 24 11 23 13 21 C 12 24 11 28 14 30 C 16 29 18 26 18 23 C 19 27 20 34 20 40 C 20 34 21 27 22 23 C 22 26 24 29 26 30 C 29 28 28 24 27 21 C 29 23 32 24 33 22 C 33 18 30 16 28 14 C 30 15 33 14 33 11 C 33 8 30 8 28 9 C 26 6 23 4 20 3 Z M 20 35 L 20 47' - }, - { - id: 'birch', - viewBox: '0 0 40 48', - pathData: - 'M 20 3 C 12 6 8 14 8 22 C 8 32 14 40 20 43 C 26 40 32 32 32 22 C 32 14 28 6 20 3 Z M 20 10 L 20 47' - }, - { - id: 'aspen', - viewBox: '0 0 40 48', - pathData: - 'M 20 5 C 27 6 34 12 32 22 C 30 30 24 36 20 40 C 16 36 10 30 8 22 C 6 12 13 6 20 5 Z M 20 12 L 20 46' - }, - { - id: 'willow', - viewBox: '0 0 40 48', - pathData: - 'M 20 2 C 17 10 16 22 17 34 C 18 42 22 42 23 34 C 24 22 23 10 20 2 Z M 20 10 L 20 47' - }, - { - id: 'linden', - viewBox: '0 0 40 48', - pathData: - 'M 20 3 C 13 5 7 12 8 22 C 9 30 14 38 20 46 C 26 38 31 30 32 22 C 33 12 27 5 20 3 Z M 20 14 L 20 45' - } -] - -/** Autumn palette — muted warm tones that sit against the dark background. */ -export const LEAF_COLORS: readonly string[] = [ - '#b8884a', - '#a67035', - '#6e4923', - '#8b6a3a', - '#c4975a', - '#7a4e22' -] diff --git a/lib/leaves/wind.ts b/lib/leaves/wind.ts deleted file mode 100644 index 5bcac7f..0000000 --- a/lib/leaves/wind.ts +++ /dev/null @@ -1,259 +0,0 @@ -import type { SeededRNG } from '@hollowdark/rng/seeded' -import type { - Gust, - GustKind, - SceneDimensions, - WindParticle, - WindSystem -} from '@hollowdark/lib/leaves/types' - -const PREVAILING_VX_PX_PER_S = -28 -const PREVAILING_VY_PX_PER_S = 8 - -const NEXT_GUST_MIN_MS = 9_000 -const NEXT_GUST_MAX_MS = 22_000 - -const GUST_KIND_WEIGHTS: readonly (readonly [GustKind, number])[] = [ - ['breeze', 6], - ['gust', 3], - ['whirl', 1] -] - -const BREEZE_DURATION_MS = [2_400, 4_200] as const -const GUST_DURATION_MS = [3_200, 5_500] as const -const WHIRL_DURATION_MS = [4_500, 7_500] as const - -const BREEZE_RADIUS = [70, 140] as const -const GUST_RADIUS = [150, 260] as const -const WHIRL_RADIUS = [90, 170] as const - -const BREEZE_STRENGTH = [25, 55] as const -const GUST_STRENGTH = [70, 130] as const -const WHIRL_STRENGTH = [55, 95] as const - -/** Construct the initial wind system. Schedules the first gust a few - * seconds out so the scene opens on the gentle prevailing wind. */ -export function initialWindSystem(rng: SeededRNG): WindSystem { - return { - prevailingVx: PREVAILING_VX_PX_PER_S, - prevailingVy: PREVAILING_VY_PX_PER_S, - activeGust: null, - nextGustInMs: 4_000 + rng.next() * 6_000 - } -} - -/** - * Force per unit mass at a given point. Returns the prevailing wind plus, - * if inside a gust, the gust's contribution with linear radial falloff. - */ -export function windForceAt( - wind: WindSystem, - x: number, - y: number -): { readonly fx: number; readonly fy: number } { - let fx = wind.prevailingVx - let fy = wind.prevailingVy - - const g = wind.activeGust - if (g) { - const dx = x - g.centerX - const dy = y - g.centerY - const dist = Math.sqrt(dx * dx + dy * dy) - if (dist < g.radius) { - const falloff = 1 - dist / g.radius - if (g.kind === 'whirl') { - const safe = Math.max(0.001, dist) - const tx = -dy / safe - const ty = dx / safe - fx += tx * g.strength * falloff - fy += ty * g.strength * falloff - } else { - fx += Math.cos(g.direction) * g.strength * falloff - fy += Math.sin(g.direction) * g.strength * falloff - } - } - } - return { fx, fy } -} - -/** Counter-holder so the caller can issue unique monotonic ids. */ -export interface IdSource { - nextGustId(): number - nextParticleId(): number -} - -/** - * Step the wind system by `dtMs`. Advances particles inside an active - * gust, spawns new particles, retires the gust when its duration ends, - * and schedules the next gust after an idle pause. - */ -export function stepWind( - wind: WindSystem, - dtMs: number, - scene: SceneDimensions, - rng: SeededRNG, - ids: IdSource -): WindSystem { - if (wind.activeGust) { - const g = wind.activeGust - const elapsed = g.elapsedMs + dtMs - - if (elapsed >= g.totalDurationMs) { - return { - ...wind, - activeGust: null, - nextGustInMs: NEXT_GUST_MIN_MS + rng.next() * (NEXT_GUST_MAX_MS - NEXT_GUST_MIN_MS) - } - } - - const advanced = advanceParticles(g, dtMs) - let particles = advanced - let nextSpawnInMs = g.nextParticleSpawnInMs - dtMs - - if (nextSpawnInMs <= 0) { - particles = [...advanced, spawnParticle(g, ids.nextParticleId(), rng)] - nextSpawnInMs = particleSpawnIntervalMs(g.kind) - } - - return { - ...wind, - activeGust: { - ...g, - elapsedMs: elapsed, - particles, - nextParticleSpawnInMs: nextSpawnInMs - } - } - } - - const remaining = wind.nextGustInMs - dtMs - if (remaining <= 0) { - return { - ...wind, - activeGust: createGust(ids.nextGustId(), scene, rng), - nextGustInMs: 0 - } - } - return { ...wind, nextGustInMs: remaining } -} - -function createGust(id: number, scene: SceneDimensions, rng: SeededRNG): Gust { - const kind = rng.weightedPick(GUST_KIND_WEIGHTS) - const centerX = rng.next() * scene.width - const centerY = scene.height * (0.25 + rng.next() * 0.55) - - let radius: number - let strength: number - let duration: number - let direction: number - - if (kind === 'breeze') { - radius = BREEZE_RADIUS[0] + rng.next() * (BREEZE_RADIUS[1] - BREEZE_RADIUS[0]) - strength = BREEZE_STRENGTH[0] + rng.next() * (BREEZE_STRENGTH[1] - BREEZE_STRENGTH[0]) - duration = BREEZE_DURATION_MS[0] + rng.next() * (BREEZE_DURATION_MS[1] - BREEZE_DURATION_MS[0]) - direction = Math.PI + (rng.next() - 0.5) * 0.5 - } else if (kind === 'gust') { - radius = GUST_RADIUS[0] + rng.next() * (GUST_RADIUS[1] - GUST_RADIUS[0]) - strength = GUST_STRENGTH[0] + rng.next() * (GUST_STRENGTH[1] - GUST_STRENGTH[0]) - duration = GUST_DURATION_MS[0] + rng.next() * (GUST_DURATION_MS[1] - GUST_DURATION_MS[0]) - direction = Math.PI + (rng.next() - 0.5) * 0.35 - } else { - radius = WHIRL_RADIUS[0] + rng.next() * (WHIRL_RADIUS[1] - WHIRL_RADIUS[0]) - strength = WHIRL_STRENGTH[0] + rng.next() * (WHIRL_STRENGTH[1] - WHIRL_STRENGTH[0]) - duration = WHIRL_DURATION_MS[0] + rng.next() * (WHIRL_DURATION_MS[1] - WHIRL_DURATION_MS[0]) - direction = rng.nextBool(0.5) ? 1 : -1 - } - - return { - id, - kind, - centerX, - centerY, - radius, - strength, - direction, - elapsedMs: 0, - totalDurationMs: duration, - particles: [], - nextParticleSpawnInMs: 0 - } -} - -function particleSpawnIntervalMs(kind: GustKind): number { - return kind === 'breeze' ? 160 : kind === 'gust' ? 110 : 140 -} - -function spawnParticle(gust: Gust, id: number, rng: SeededRNG): WindParticle { - const angle = rng.next() * Math.PI * 2 - const r = rng.next() * gust.radius * 0.9 - const x = gust.centerX + Math.cos(angle) * r - const y = gust.centerY + Math.sin(angle) * r - - let vx: number - let vy: number - if (gust.kind === 'whirl') { - const safe = Math.max(0.001, r) - vx = (-Math.sin(angle) * gust.direction * gust.strength) / Math.max(1, safe / 40) - vy = (Math.cos(angle) * gust.direction * gust.strength) / Math.max(1, safe / 40) - } else { - vx = Math.cos(gust.direction) * gust.strength * 0.55 - vy = Math.sin(gust.direction) * gust.strength * 0.55 - } - - return { - id, - x, - y, - vx, - vy, - ageMs: 0, - lifetimeMs: 800 + rng.next() * 1_200, - size: 1.2 + rng.next() * 1.6 - } -} - -function advanceParticles(gust: Gust, dtMs: number): readonly WindParticle[] { - const dt = dtMs / 1000 - const out: WindParticle[] = [] - for (const p of gust.particles) { - const age = p.ageMs + dtMs - if (age >= p.lifetimeMs) continue - - let vx = p.vx - let vy = p.vy - - if (gust.kind === 'whirl') { - const dx = p.x - gust.centerX - const dy = p.y - gust.centerY - const dist = Math.sqrt(dx * dx + dy * dy) - if (dist > 0) { - const safe = Math.max(0.001, dist) - vx += (-dy / safe) * gust.direction * 45 * dt - vy += (dx / safe) * gust.direction * 45 * dt - } - } - - const drag = Math.pow(0.94, dt * 60) - vx *= drag - vy *= drag - - out.push({ - ...p, - x: p.x + vx * dt, - y: p.y + vy * dt, - vx, - vy, - ageMs: age - }) - } - return out -} - -/** Opacity envelope for a particle: fade in the first 20%, hold, fade out - * the last 30%. */ -export function particleOpacity(p: WindParticle): number { - const phase = p.ageMs / p.lifetimeMs - if (phase < 0.2) return phase / 0.2 - if (phase > 0.7) return Math.max(0, (1 - phase) / 0.3) - return 1 -} diff --git a/lib/screens/BeginScreen.svelte b/lib/screens/BeginScreen.svelte index 88f708e..e15b516 100644 --- a/lib/screens/BeginScreen.svelte +++ b/lib/screens/BeginScreen.svelte @@ -2,7 +2,6 @@ import AppTitle from '@hollowdark/lib/components/AppTitle.svelte' import AppVersion from '@hollowdark/lib/components/AppVersion.svelte' import BeginActions from '@hollowdark/lib/components/BeginActions.svelte' - import LeafScene from '@hollowdark/lib/components/LeafScene.svelte' import type { BeginState } from '@hollowdark/loading/session' interface Props { @@ -23,8 +22,6 @@ </script> <section class="begin"> - <LeafScene /> - <div class="content"> <div class="top"> <AppTitle size={38} subtitle="A Literary Life Simulation" /> @@ -45,12 +42,9 @@ justify-content: center; padding: var(--space-12); position: relative; - overflow: hidden; } .content { - position: relative; - z-index: 1; display: flex; flex-direction: column; align-items: center; |
