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
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.
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 |
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.
┌──────────────────────────────────────────────────────────────┐
│ 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
└──────────────────────────────────────────────────────────────┘
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:
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.
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).
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.
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.
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.
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.
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 canvasesprefers-reduced-motion detection → stop loop, render static frameGoal: 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.
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 |
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 |
The following decisions are supported by convergent evidence across multiple papers. Each is noted with the primary paper(s) that establish it.
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.
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.
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).
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.
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);
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
The following questions require either a prototyping decision or a product decision before implementation proceeds.
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:
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.
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).
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.
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:
RenderTexture compositing, mature spritesheet support — but adds 280 KB to the lazy-loaded bundleRecommendation: 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.
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.
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 |
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)
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.