aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-04-22 09:18:18 +0530
committerBobby <[email protected]>2026-04-22 09:18:18 +0530
commit7f80e8a1a0c4e71fde9a1530cc49fd03da01c099 (patch)
tree895cdebcc531f22e569acbad2f2944047593e99d
parentbbb8a51490f98dfc02ab9a0fd79248806c80e705 (diff)
downloadhollowdark-7f80e8a1a0c4e71fde9a1530cc49fd03da01c099.tar.xz
hollowdark-7f80e8a1a0c4e71fde9a1530cc49fd03da01c099.zip
Remove the leaf scene and its supporting systems from the Begin screen
-rw-r--r--lib/components/LeafScene.svelte197
-rw-r--r--lib/display/state.ts9
-rw-r--r--lib/leaves/physics.ts89
-rw-r--r--lib/leaves/spawn.ts57
-rw-r--r--lib/leaves/types.ts96
-rw-r--r--lib/leaves/variants.ts56
-rw-r--r--lib/leaves/wind.ts259
-rw-r--r--lib/screens/BeginScreen.svelte6
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;