Projects bibleweb Docs atmosphere-synthesis.md

Atmosphere System: MonoGame → Web Migration Synthesis

Last modified March 29, 2026

Atmosphere System: MonoGame → Web Migration Synthesis

Date: 2026-03-29 Status: Reference document — synthesises all 10 atmosphere research papers into a single actionable plan Audience: Implementation agents working on the BibleWeb atmosphere system


Overview

This document maps every visual feature of the MonoGame C# atmospheric scenes (Sea of Galilee, Jerusalem) to a concrete web implementation. It draws on 10 research papers and recommends a layered Canvas 2D + CSS hybrid architecture, with optional WebGL upgrade paths for advanced effects. The result is a phased implementation plan from MVP atmospheric background to full MonoGame parity.


1. Feature Migration Matrix

Each MonoGame visual feature is mapped to its recommended web technology, implementation complexity, and which phase it belongs to.

MonoGame Feature Web Technology Complexity Phase
Sky: deep purple → rust gradient (Galilee) CSS linear-gradient + @property custom property transition Low 1
Sky: 3-point sunset gradient (Jerusalem) CSS linear-gradient with 3+ color stops, @property animated Low 1
Vignette: 256×256 radial, 0.3 strength, dark purple CSS radial-gradient overlay div, near-zero cost Low 1
Scene transitions: 1.5s crossfade Dual-canvas opacity tween via requestAnimationFrame, eased Low 1
Moon: 12px radius, craters, subtle glow Canvas 2D: filled arc + seeded random crater circles + screen radial gradient Low 1
Sun: 16px radius, core white → warm edges Canvas 2D: filled arc + radial gradient halo, screen blend Low 1
Mountains: multi-octave fBm noise, 3–5 layers simplex-noise fBm, ctx.beginPath/fill per layer, baked to OffscreenCanvas Medium 1
Atmospheric depth haze on mountains Color lerp toward sky color per layer (near layers lighter), baked once Low 1
Shore: foreground clearing for campfire Noise-based path with manually flattened section at campfire x-range, baked Low 1
Water: animated waves Sum of sine curves drawn as ctx.beginPath/fill with createLinearGradient, per-frame Low 1
Water: foam Semi-transparent sine strip path 2–4px above wave line, per-frame Low 1
Moonlight pillar / shimmer (0.15 freq) createLinearGradient strip at moon x-position, screen blend, slow Math.sin oscillation Low 1
Particles: 512 pool, warm beige alpha~40, 4–8s Custom TypeScript object pool (512 pre-allocated), quadratic fade envelope, Canvas 2D Low 1
Particles: Jerusalem amber alpha~25, 4–9s Same pool system, EmitterConfig swapped Low 1
Campfire: 3 dynamic flame tongues (~8px) Particle-based flame tongues with screen/lighter blend, or DOOM-fire pixel buffer Medium 1
Campfire glow: (255,140,60) @ 0.08 Pre-rendered soft-particle sprite (OffscreenCanvas radial gradient), drawImage per frame Low 1
Moonlight wash: (100,120,170) @ 0.06 Full-canvas semi-transparent fillRect in scene's tint color, screen blend Low 1
Warm wash: (255,170,80) @ 0.14 Same approach, warm amber tint Low 1
Glow circles: 128×128, quadratic falloff Pre-rendered OffscreenCanvas radial gradient, drawImage + screen Low 1
City silhouette: low-profile, lit windows Seeded random rect buildings + occasional fillRect windows, baked to OffscreenCanvas Medium 1
Light shafts: 4 dynamic, 0.015 speed, -20° Elongated triangle paths with gradient fill, screen blend, slow Math.sin drift Medium 2
Sun atmospheric halo Large outer radial gradient, screen, rendered once during bake Low 1
Sky anti-banding (GPU interpolation in MonoGame) SVG feTurbulence displacement on CSS gradient, or tri-noise in baked Canvas sky Low 1
Bloom / post-processing WebGL 3-pass (brightness extract → separable Gaussian blur → additive composite) High 2
Ordered dithering (Bayer matrix) WebGL fragment shader (4×4 Bayer matrix), full-scene post-process pass High 3
Palette LUT / colour grading WebGL 3D LUT shader (16³ texture, single pass) High 3
Skin switching (pixel ↔ clean) CSS data-skin attribute on <html>, structural token overrides, lazy CSS load Medium 4
Font: m5x7 pixel font @font-face woff2, --font-ui CSS token, loaded lazily (browser skips unless used) Low 4
Font: NotoSans / DroidSansHebrew fallback Already available via Google Fonts / system stack Low 1

2. Recommended Architecture

The 10 research papers converge on a single architecture: a layered hybrid using CSS for static/slow effects, Canvas 2D for procedural generation and particles, and optional WebGL for effects that genuinely need per-pixel GPU access. This mirrors the MonoGame rendering stack without the complexity of a full WebGL setup until it is needed.

Layer Stack (back to front)

┌──────────────────────────────────────────────────────────────┐
│  HTML/Svelte UI — verse text, notes, navigation              │  DOM / CSS
├──────────────────────────────────────────────────────────────┤
│  Vignette overlay                                             │  CSS radial-gradient div
├──────────────────────────────────────────────────────────────┤
│  Foreground particle layer (campfire sparks, dust in front)  │  Canvas 2D (transparent bg)
├──────────────────────────────────────────────────────────────┤
│  Animated layer: water, campfire, light shafts, particles    │  Canvas 2D — 30fps rAF loop
├──────────────────────────────────────────────────────────────┤
│  Baked background: mountains, moon/sun, city, shore          │  OffscreenCanvas → drawImage
├──────────────────────────────────────────────────────────────┤
│  Sky gradient layer                                           │  CSS linear-gradient + @property
└──────────────────────────────────────────────────────────────┘

Why This Architecture

Rendering-approaches recommends Canvas 2D + CSS hybrid as primary, with WebGL as the optional upgrade. Case-studies confirms: CrossCode runs a full commercial pixel art RPG on Canvas 2D only. Performance-mobile shows Canvas 2D is entirely sufficient for the 50–150 particle count target on mid-range mobile.

Key structural decisions:

  1. Sky as CSS — zero JavaScript cost. @property custom properties enable the 1.5s crossfade between scene sky colors at compositor-thread speed. Rendering-approaches proves CSS handles multi-stop gradients and animated transitions at no GPU cost.

  2. Static elements baked to OffscreenCanvas — mountains, moon/sun, city, craters, shore, horizon glow. These are generated once at scene load using simplex-noise + alea seeded PRNG, posted from a Web Worker if generation is slow. Procedural-generation lays out the exact baking strategy with three tiers: static (bake once), semi-static (slow oscillation), animated (every frame).

  3. Two Canvas 2D layers for particles — one canvas behind the terrain (background dust) and one in front (foreground sparks). Particle-systems establishes this directly from the MonoGame source pattern where background particles render before terrain and foreground particles render after.

  4. OffscreenCanvas + Web Worker for the animation loop — the main thread is the Svelte UI and is never touched by the rAF loop. Performance-mobile establishes this as the highest-impact optimisation for preventing jank during text selection, note editing, and navigation.

  5. CSS vignette — the MonoGame 256×256 vignette texture is replaced by a CSS radial-gradient overlay div. Post-processing benchmarks this as ~0ms per-frame vs 0.3ms for Canvas 2D gradient, at equivalent visual quality.

SvelteKit Component Structure

SceneBackground.svelte
  ├── <div class="sky-layer" />           ← CSS gradient, @property animated
  ├── <canvas bind:this={bgCanvas} />     ← baked mountains/moon/city (rarely redrawn)
  ├── <canvas bind:this={bgParticles} />  ← ambient dust behind terrain
  ├── <canvas bind:this={animCanvas} />   ← water, fire, light shafts (30fps)
  ├── <canvas bind:this={fgParticles} />  ← campfire sparks, foreground dust
  └── <div class="vignette-overlay" />    ← CSS radial-gradient

All canvases are pointer-events: none so they never intercept Bible text interaction. All init code lives in onMount (SSR-safe). The OffscreenCanvas worker receives the animCanvas handle.


3. Implementation Phases

Phase 1 — Minimal Viable Atmosphere

Goal: Recognisable atmospheric background. Sky, mountains, moon/sun, water animation, campfire, ambient dust, vignette, scene crossfade. Zero WebGL dependency.

What to build:

Component Technique
Sky gradient (both scenes) CSS linear-gradient + @property + transition: 1.5s
Vignette CSS radial-gradient overlay div
Mountains (3 layers, fBm) simplex-noise baked to OffscreenCanvas at scene load
Moon (disc + craters + glow) Canvas 2D: arc + seeded circles + screen radial gradient, baked
Sun (disc + halo) Canvas 2D: arc + outer radial gradient, baked
City silhouette Seeded random rects + lit-window dots, baked
Shore clearing Noise path with flat campfire section, baked
Water waves Sine sum path, createLinearGradient, per-frame
Water foam Semi-transparent sine strip, per-frame
Moonlight pillar createLinearGradient strip + screen, per-frame
Campfire: 3 flame tongues Particle system, globalCompositeOperation: 'lighter'
Campfire glow Pre-rendered OffscreenCanvas radial gradient, drawImage per frame
Ambient dust particles Custom TypeScript emitter + 512-item object pool
Moonlight/campfire wash tints Full-canvas fillRect + screen blend
Scene crossfade (1.5s) Dual-canvas opacity tween, quad ease-in-out
Sky anti-banding SVG feTurbulence displacement on sky layer

What Phase 1 does NOT include: WebGL, bloom, dithering, palette LUT, pixel skin switching.

Expected bundle impact: simplex-noise (~6 KB gzip) + alea (~1 KB). Everything else is zero-dependency Canvas 2D + CSS.

Performance target: 30fps on mid-range mobile (2019–2021). Pause via visibilitychange + IntersectionObserver.

Accessibility requirements (all non-negotiable for Phase 1):

  • aria-hidden="true" on all scene canvases
  • prefers-reduced-motion detection → stop loop, render static frame
  • Visible pause/stop control (WCAG 2.2.2 Level A — mandatory)
  • Campfire flame animation capped at ≤3 Hz flicker frequency (WCAG 2.3.1 Level A)

Phase 2 — Full Scene Fidelity

Goal: Match the MonoGame visual output including light shafts, full glow effects, and smooth scene transitions with interpolated light positions.

What Phase 2 adds:

Component Technique Paper
Light shafts (4 dynamic, Jerusalem) Elongated triangles + gradient fill + screen, slow drift via Math.sin Procedural-generation
Full particle interpolation during crossfade Lerp emitter configs (spawnRate, alpha, color) with scene t Post-processing
WebGL pipeline (opt-in) WebGL2 context, framebuffer, full-screen quad infrastructure Rendering-approaches
True bloom glow (campfire, sun) WebGL 3-pass: brightness extract → Gaussian blur → additive composite Post-processing
PixiJS upgrade path If profiling shows Canvas 2D bottleneck, migrate to PixiJS v8 ParticleContainer Libraries-frameworks
Quality tier system High (100+ particles) / Medium (30–50) / Low (10) / Static detection at load Performance-mobile

When to start Phase 2: After Phase 1 ships and is profiled in production. The WebGL framebuffer pipeline is a setup cost worth paying only when bloom/dithering are needed, per the architectural recommendation in post-processing.


Phase 3 — Advanced Effects

Goal: Full post-processing pipeline matching or exceeding the MonoGame visual stack. Retro dithering, palette limiting, chromatic aberration.

What Phase 3 adds:

Component Technique Paper
Ordered dithering (Bayer) WebGL fragment shader, 4×4 matrix, full-scene post-process Post-processing, Pixel-art-web
Palette LUT / colour grading WebGL 3D LUT (16³ PNG texture), single pass Post-processing
Blue noise dithering variant 64×64 tiled blue noise texture in fragment shader Post-processing
GPU sky anti-banding Triangle noise in WebGL sky fragment shader Post-processing
Campfire bloom isolation Render campfire sprites to RenderTexture, apply bloom only there Case-studies (PixiJS tip)
CRT scanline overlay (optional) CSS repeating-linear-gradient, 2px strips, zero JS Pixel-art-web

Phase 4 — Skin System and Polish

Goal: The pixel art mode is a first-class visual skin, selectable independently from the colour theme. Fonts, UI borders, shadows, and background texture all switch when data-skin="pixel" is set.

What Phase 4 adds:

Component Technique Paper
Skin/theme two-layer model data-skin attribute + CSS structural tokens (--radius-*, --shadow-*, --font-ui) Theme-switching
Lazy CSS loading for pixel skin Vite ?inline dynamic import, inject <style> tag on activation Theme-switching
Flash-of-wrong-skin prevention Inline script in app.html reads localStorage before first paint Theme-switching
m5x7 font on demand @font-face woff2, browser skips download until --font-ui references it Theme-switching
Font prefetch on hover <link rel="prefetch"> injected when user hovers the skin toggle Theme-switching
Skin transition (fade + snap) 200ms opacity fade-out, apply skin, fade-in Theme-switching
Nine-slice pixel panel borders CSS border-image with Kenney CC0 assets Theme-switching
In-app animation toggle Visible 44×44px button, aria-label, aria-pressed Accessibility
Focus Mode (solid background) User-toggled setting that replaces scene with solid --color-background Accessibility

4. Key Technical Decisions

The following decisions are supported by convergent evidence across multiple papers. Each is noted with the primary paper(s) that establish it.

Decision 1: Canvas 2D as primary renderer (no WebGL until Phase 2)

Support: Rendering-approaches (§9), Libraries-frameworks (§7, Phase 1 recommendation), Case-studies (§1 — CrossCode, Canvas 2D only), Performance-mobile (§3).

Canvas 2D handles 512 particles, animated sine-wave water, and all lighting overlays comfortably at 30fps on mid-range mobile. The crossover to WebGL is worth the setup cost only when bloom, dithering, or >2,000 particles are required. Starting with Canvas 2D avoids the boilerplate cost (200–300 lines for a basic WebGL pipeline) and maintains zero library dependencies in Phase 1.

Decision 2: OffscreenCanvas + Web Worker for the render loop

Support: Rendering-approaches (§7), Pixel-art-web (§10), Performance-mobile (§8), Case-studies (§7).

The most important single architectural decision. The Bible study UI (text selection, note editing, search) runs on the main thread. The atmospheric scene must not compete with it. Transferring the canvas to a worker means the rAF loop, particle physics, and background draws are entirely isolated. Every paper that discusses OffscreenCanvas rates it as the highest-value optimisation for main-thread-sensitive apps.

Decision 3: Bake static elements to OffscreenCanvas at scene load

Support: Procedural-generation (§8), Pixel-art-web (§6), Performance-mobile (§8).

Mountains, moon/sun, city silhouette, craters, shore, and horizon glow never change within a scene. Computing them once and blitting via drawImage() each frame costs near-zero per-frame CPU. The baking can happen in the Web Worker itself — generate, transfer as ImageBitmap, and the main animation loop only calls drawImage(bakedBg, 0, 0).

Decision 4: Seed generation from book/chapter ID

Support: Procedural-generation (§7).

Math.random() is not seedable. The alea package (string-seedable PRNG, integrates directly with simplex-noise) lets each Bible book have a unique but reproducible mountain topology, moon crater layout, and city skyline:

const prng = alea(`${bookId}-${chapterId}`);
const noise2D = createNoise2D(prng);

This is a zero-cost feature that makes the atmospheric system feel personalised to each book.

Decision 5: 30fps target for atmospheric loop

Support: Performance-mobile (§1, §3, §10), Case-studies (§7), Rendering-approaches (§1).

iOS Safari throttles rAF to 30fps automatically in Low Power Mode. Award-winning WebGL sites throttle background loops to 30fps and users do not notice. A 2023 energy study shows 30fps halves GPU active time vs 60fps. The delta-time gating pattern from performance-mobile (§1) prevents drift:

const FRAME_INTERVAL = 1000 / 30; // 33.33ms
if (elapsed < FRAME_INTERVAL) return;
lastFrameTime = timestamp - (elapsed % FRAME_INTERVAL);

Decision 6: Campfire flicker capped at ≤3 Hz

Support: Accessibility (§1, §2, §8), Performance-mobile (§9).

WCAG 2.3.1 (Level A, legally mandatory) prohibits flashing at more than 3 Hz over a significant screen area. Real campfires flicker at 8–12 Hz. A flame animation cycling at 10 fps = ~3.3 Hz is a direct WCAG violation. Fixing this is simple: use luminance-preserving palette (warm amber, not white flashes) and cycle flame frames at 2 fps or less. The aesthetic is "cosy ember glow," which is correct for a Bible study context.

Decision 7: CSS vignette, not a drawn overlay

Support: Post-processing (§1), Performance-mobile (§2).

The MonoGame 256×256 vignette texture costs ~0.3ms per frame as a Canvas 2D draw call, or near-zero as a GPU compositor-thread CSS overlay. The CSS version — a radial-gradient overlay div with pointer-events: none — is parameterisable via CSS custom properties per scene and has no per-frame JavaScript cost.

Decision 8: Custom particle implementation over libraries

Support: Particle-systems (§5, §9), Libraries-frameworks (§7).

At 512 particles maximum, the overhead of tsParticles (~50–200 KB gzip), Sparticles (~10 KB), or Proton (~35 KB) is unjustified. A custom emitter + free-list pool is ~150 lines of TypeScript, zero bundle cost, and provides precise control over render layering that libraries make awkward. The pre-allocated pool (512 Particle objects, never GC'd during animation) is a direct port of the MonoGame pool pattern.


5. Risk Assessment

Risk 1: Mobile thermal throttling (High probability on sustained use)

What happens: Mid-range Android devices (2019–2021 hardware, common in the 55+ demographic) can trigger CPU/GPU thermal throttling within 5–10 minutes of sustained Canvas animation. Frame times that start at 30fps can degrade to 15fps over a session.

Mitigation: Quality tier system keyed on deviceMemory + hardwareConcurrency at load time (Chromium-only, with conservative fallback for Firefox/Safari). Indirect thermal detection via consecutiveSlowFrames counter — if 10+ frames exceed 50ms, step down to the next quality tier. Performance-mobile (§4) provides the exact detection pattern.

Fallback design: The "Low" tier (10 particles, 15fps) must look intentional, not broken. The "Static" tier (single baked frame, zero animation) should be warm and beautiful as a still image.

Risk 2: WebGL context exhaustion on mobile

What happens: Mobile browsers allow 8–16 simultaneous WebGL contexts per page. Creating multiple contexts (one per scene canvas) can hit this limit, causing silent context loss and a blank canvas.

Mitigation: Phase 2+ uses a single WebGL context shared across all render targets. Canvas 2D in Phase 1 is immune to this issue. The webglcontextlost / webglcontextrestored event pair provides graceful recovery regardless.

Risk 3: OffscreenCanvas Web Worker debugging complexity

What happens: Canvas state inside a Web Worker is not inspectable in standard browser DevTools. Debugging rendering bugs requires adding postMessage-based logging rather than breakpoints.

Mitigation: Build and test the rendering code on the main thread first. Only transfer to OffscreenCanvas once the visuals are correct. Keep the worker communication interface minimal (scene config object + scene change messages).

Risk 4: Banding on CSS sky gradients

What happens: CSS gradients operate in 8-bit sRGB. A dark sky gradient (deep purple → midnight → black) over a tall viewport can show 5–8 visible quantisation bands — the exact issue the MonoGame GPU vertex interpolation avoids.

Mitigation: Apply SVG feTurbulence displacement to the sky layer (universal browser support, near-zero cost) to break up banding into imperceptible grain. Post-processing (§8) provides the exact markup. In Phase 2 with WebGL, the GPU sky shader adds triangle noise in the fragment shader natively.

Risk 5: WCAG 2.3.1 campfire flash failure

What happens: If the campfire flame animation cycles at >3 Hz with significant luminance delta between frames, it is a legal compliance failure (WCAG Level A, ADA/EAA exposure).

Mitigation: Enforce a hard ≤2fps sprite cycle rate for campfire animations. Use low-luminance-contrast palette: amber → orange, never yellow-white → black. Run PEAT (Photosensitive Epilepsy Analysis Tool) on a screen recording before each release. Accessibility (§8) provides the exact thresholds.

Risk 6: Perlin noise generation jank on first scene load

What happens: Generating 3–5 fBm mountain layers at 6 octaves across 1280px costs non-trivial CPU time, potentially causing a jank spike during initial page render.

Mitigation: Move baking to a Web Worker (all noise generation happens off-thread). Show a simple CSS gradient background immediately; replace with the baked scene once the Worker posts back the ImageBitmap. The user never sees a blank canvas. Procedural-generation (§8) documents the exact worker baking pattern.

Risk 7: Safari OffscreenCanvas partial support

What happens: While OffscreenCanvas is broadly supported (Chrome 69+, Firefox 105+, Safari 16.4+), Safari's support has historically been incomplete in certain edge cases, particularly with WebGL inside workers.

Mitigation: Use Canvas 2D (not WebGL) inside the worker — Canvas 2D OffscreenCanvas support is more reliable across all targets. Phase 2 WebGL effects use a separate main-thread context for the post-processing pipeline.


6. Open Questions

The following questions require either a prototyping decision or a product decision before implementation proceeds.

OQ-1: Internal canvas resolution — 320×180 or native?

The question: The MonoGame app uses a low-res pixel art aesthetic. Should the web canvas render at 320×180 and scale up via image-rendering: pixelated, or at full viewport resolution?

Trade-offs:

  • 320×180 with upscale: authentic pixel art aesthetic, every mountain pixel is large and clearly "chunky," much lower fill cost, all pixel art techniques (integer coordinates, sprite sheets) are natural
  • Native resolution: smoother at high DPI, text overlay is sharper, less "retro game" feel — more "atmospheric art"

Recommendation: 320×180 for the "pixel skin," native (or 0.5× native) for the "clean skin." The skin system from Phase 4 makes this a resolved question per-skin. For Phase 1, decide which skin is the primary target.

DECIDED: Default to 320×180 (low-res pixel art upscaled with nearest-neighbor) for broad device compatibility. Offer native resolution as an opt-in setting for users with capable hardware. This gives the retro aesthetic by default while allowing smoothness for those who want it.

Paper: Pixel-art-web (§3, §8) documents both approaches fully.

OQ-2: Should the scene react to scroll position?

The question: Award-winning sites use scroll-driven scene transitions (sky darkens as user reads deeper). BibleWeb could shift the sky from dawn to dusk as the user reads through a chapter.

Trade-offs: Scroll-driven parallax is a known vestibular trigger (accessibility §7). The ADHD user segment (documented in user_adhd_documentation.md) would likely find scroll-reactive backgrounds highly distracting. The safer, simpler option is a fixed atmospheric state per book/chapter.

DECIDED: Subtle reading-progression atmosphere (not parallax). As the user reads deeper into a chapter, the scene subtly shifts — vignette gently deepens, particles slow slightly, sky darkens a touch. This creates a 'settling in' feeling. Parallax is explicitly avoided due to vestibular disorder risks (see accessibility research). Default: off (accessibility-safe). Available as an opt-in setting ('Reading atmosphere' toggle).

OQ-3: How many distinct scenes are needed at launch?

The question: Two scenes are documented (Sea of Galilee, Jerusalem). Other scenes are implied (desert, temple, garden, sea voyage). How many scenes need Phase 1 assets?

Impact: Each scene needs a baked background config (mountain colors, sky gradient stops, sun/moon position, city or no city). This is a data/design question, not a technical one. The code architecture handles any number of scene configs.

DECIDED: Ship with 2 scenes at launch — Sea of Galilee (night) and Jerusalem (sunset). Additional scenes can be added later as the config-driven scene system makes this straightforward.

OQ-4: PixiJS or raw WebGL for Phase 2?

The question: When WebGL is needed for bloom and advanced particles, should Phase 2 use PixiJS v8 (280 KB gzip, lazy-loaded) or raw WebGL2 (0 KB, ~200 lines of boilerplate)?

Trade-offs:

  • PixiJS: visual particle editor (pixiparticles.com), built-in displacement filter for water, RenderTexture compositing, mature spritesheet support — but adds 280 KB to the lazy-loaded bundle
  • Raw WebGL2: zero bundle cost, full control, correct for a narrow use case — but no tooling, no displacement filter, 100% custom

Recommendation: Start with raw WebGL2 for the post-processing pipeline (bloom, dithering) since those are single full-screen quad passes with known implementations. Only adopt PixiJS if the scene needs displacement-mapped water or particle counts requiring ParticleContainer. Libraries-frameworks (§1) documents PixiJS precisely for this upgrade path.

DECIDED: Raw WebGL2 for Phase 2. No PixiJS dependency — keeps bundle lean and gives full control over shaders for bloom, water displacement, and dithering effects.

OQ-5: At what particle count is WebGL worth the switch?

The question: The research establishes ~2,000–3,000 as the Canvas 2D / WebGL crossover. The current target is 50–150 particles for mobile. What triggers a WebGL upgrade?

Answer from research: If profiling shows Canvas 2D particle draws exceeding 15ms of the 33ms frame budget at the high-quality tier, migrate to PixiJS ParticleContainer. The upgrade is a render-layer swap, not an architectural change, if the particle system is correctly abstracted. Particle-systems (§4) benchmarks the exact thresholds.

DECIDED: Per research findings, the Canvas 2D → WebGL crossover point is ~2,000–3,000 particles. Since our atmospheric scenes use 50–512 particles, the WebGL upgrade is triggered only when adding dense volumetric effects (Phase 3+), not by particle count alone.


Appendix A: Browser Compatibility Summary

All Phase 1 technologies are universally supported. Phase 2+ technologies have explicit coverage notes.

Technology Chrome Firefox Safari Notes
CSS @property ✓ (89+) ✓ (15.4+) Universal modern
Canvas 2D Universal
OffscreenCanvas ✓ (69+) ✓ (105+) ✓ (16.4+) Universal modern
simplex-noise npm ES module, zero platform dep
image-rendering: pixelated ✓ (10+) ~95% global
WebGL2 ✓ (15+) Universal since 2022
SVG feTurbulence Universal
prefers-reduced-motion Universal
deviceMemory API Chromium only; use as enhancement
Battery Status API Chromium only; use as enhancement
WebGPU ✓ (113+) ✓ (147+, partial) ✓ (26+) ~70% — not safe as sole renderer

Appendix B: File and Module Layout (Proposed)

src/lib/atmosphere/
  ├── scene-background.svelte    ← top-level component, manages canvas lifecycle
  ├── renderer.worker.ts         ← OffscreenCanvas rAF loop (Canvas 2D)
  ├── bake.worker.ts             ← OffscreenCanvas scene baking (noise generation)
  ├── emitter.ts                 ← ParticleEmitter + ParticlePool (custom, 512 cap)
  ├── scenes/
  │   ├── galilee.ts             ← EmitterConfig, sky stops, moon pos, seed
  │   └── jerusalem.ts           ← EmitterConfig, sky stops, sun pos, light shafts
  ├── procedural/
  │   ├── mountains.ts           ← drawMountainLayer, fBm, atmospheric color lerp
  │   ├── water.ts               ← drawWater, drawFoam, drawMoonReflection
  │   ├── celestial.ts           ← drawMoon, drawSun, drawGlow
  │   ├── city.ts                ← drawCitysilhouette (seeded)
  │   └── fire.ts                ← campfire flame tongue particles
  └── post/                      ← Phase 2+
      ├── bloom.ts               ← WebGL 3-pass bloom pipeline
      └── dither.ts              ← WebGL Bayer dither pass

src/lib/theme/
  ├── themes.css                 ← color tokens (existing)
  ├── pixel-skin.css             ← structural tokens for data-skin="pixel" (Phase 4)
  └── fonts.css                  ← @font-face: m5x7, NotoSans, DroidSansHebrew

static/skins/
  └── pixel-assets.css           ← nine-slice borders, background references (Phase 4)

Appendix C: Performance Budget Reference

At 30fps on mid-range mobile, the 33.33ms frame budget should be allocated roughly:

Work Target Time
drawImage(bakedBg, 0, 0) ~0.5ms
Water wave path + fill ~1.5ms
Campfire particle update + draw (50 particles) ~3ms
Ambient dust update + draw (50 particles) ~3ms
Moonlight strip + wash tint ~0.5ms
Canvas clear ~0.5ms
Worker overhead / postMessage ~0.5ms
Total ~9.5ms
Headroom ~23ms

The generous headroom is intentional. It accommodates mid-range mobile variability, Svelte reactivity costs during verse loading, and occasional GC pauses. The target is sustainable 30fps, not maximised effects.