From dc212eed8a2d580ee26290dea3393940a8003a60 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Thu, 12 Mar 2026 03:04:03 +0100 Subject: [PATCH] brainstorm --- .../brainstorming-session-2026-03-12-001.md | 9 +- ...G-layered-rendering-research-2026-03-12.md | 927 ++++++++++++++++++ 2 files changed, 932 insertions(+), 4 deletions(-) create mode 100644 _bmad-output/planning-artifacts/research/technical-WebGL-SVG-layered-rendering-research-2026-03-12.md diff --git a/_bmad-output/brainstorming/brainstorming-session-2026-03-12-001.md b/_bmad-output/brainstorming/brainstorming-session-2026-03-12-001.md index 50620654..0f0e1045 100644 --- a/_bmad-output/brainstorming/brainstorming-session-2026-03-12-001.md +++ b/_bmad-output/brainstorming/brainstorming-session-2026-03-12-001.md @@ -1,12 +1,13 @@ --- -stepsCompleted: [1] -inputDocuments: [] +stepsCompleted: [1, 2] +inputDocuments: + - "_bmad-output/planning-artifacts/research/technical-WebGL-SVG-layered-rendering-research-2026-03-12.md" session_topic: "WebGL + SVG Layered Rendering Architecture for Relief Icons" session_goals: "Explore all viable approaches for achieving correct layer ordering when mixing WebGL (Three.js) and SVG rendering for the relief icons layer; specifically evaluate and expand on the multi-SVG/multi-DOM-element architecture; surface edge cases, risks, and non-obvious possibilities" -selected_approach: "AI-Recommended" +selected_approach: "Progressive Technique Flow" techniques_used: [] ideas_generated: [] -context_file: "" +context_file: "_bmad-output/planning-artifacts/research/technical-WebGL-SVG-layered-rendering-research-2026-03-12.md" --- # Brainstorming Session — WebGL Relief Icons Rendering Architecture diff --git a/_bmad-output/planning-artifacts/research/technical-WebGL-SVG-layered-rendering-research-2026-03-12.md b/_bmad-output/planning-artifacts/research/technical-WebGL-SVG-layered-rendering-research-2026-03-12.md new file mode 100644 index 00000000..a736c5b7 --- /dev/null +++ b/_bmad-output/planning-artifacts/research/technical-WebGL-SVG-layered-rendering-research-2026-03-12.md @@ -0,0 +1,927 @@ +# Technical Research Report: WebGL + SVG Layered Rendering Architecture + +**Date:** 2026-03-12 +**Project:** Fantasy-Map-Generator +**Topic:** Browser compositing strategies for mixing Three.js WebGL and D3 SVG while preserving layer ordering +**Status:** Complete + +--- + +## 1. Research Scope + +### Problem Statement + +The Fantasy-Map-Generator relief icons layer is currently rendered via SVG (slow at scale). The goal is to replace or augment it with Three.js WebGL for GPU-accelerated rendering. Three candidate architectures were identified: + +| Option | Description | Known Issue | +| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| **A — Canvas beside SVG** | `` element placed before/after the main SVG element | No layer interleaving possible — canvas cannot be interleaved within SVG layer groups | +| **B — WebGL inside ``** | Three.js canvas nested inside an SVG `` | Forces FBO compositing on every frame; slower than pure SVG in practice | +| **C — DOM-Split architecture** | The single SVG is decomposed into multiple sibling DOM elements (canvas + SVG fragments), positioned absolutely and ordered via z-index | A major architectural change that may restore layer interleaving | + +This report evaluates the browser-level feasibility of Option C and any additional approaches. + +--- + +## 2. Browser Compositing: CSS Stacking Contexts + +**Source:** MDN Web Docs — "Stacking context", CSS Compositing specification + +### 2.1 What Creates a Stacking Context + +Each of the following CSS conditions on an element creates a **stacking context** (a compositing boundary within which children are atomically composed together before being composited with siblings): + +| CSS Property / Condition | Value | +| ---------------------------------------------- | -------------------------------------------------------------------- | +| `position` (non-static) + `z-index` (non-auto) | Any `position: relative/absolute/fixed/sticky` with explicit z-index | +| `opacity` | `< 1` | +| `transform` | Any value except `none` | +| `filter` | Any value except `none` | +| `backdrop-filter` | Any value except `none` | +| `clip-path` | Any value except `none` | +| `mask` / `mask-image` / `mask-border` | Any non-none value | +| `mix-blend-mode` | Any value except `normal` | +| `isolation` | `isolate` | +| `will-change` | Any value that would create a stacking context if set | +| `contain` | `layout`, `paint`, `strict`, or `content` | +| `perspective` | Any value except `none` | + +### 2.2 Key Rules for DOM-Split Architecture + +1. **Stacking contexts are atomic**: All children of a stacking context are painted together before compositing with sibling stacking contexts. Z-index values are only meaningful within the _same parent_ stacking context. +2. **Multiple sibling elements** at the same level of the DOM, each with `position: absolute` and different `z-index` values, form a Z-ordered stack — regardless of whether they are SVG, ``, or `
`. +3. **`isolation: isolate`** can be used on a wrapper container to explicitly create a stacking context boundary, preventing `mix-blend-mode` from propagating upward and creating predictable compositing behavior. +4. **`will-change: transform`** hints to the browser compositor that the element should get its own layer. This is useful for canvas elements that update frequently — it avoids invalidating surrounding layers. + +### 2.3 Implication for Option C + +Option C is **architecturally valid** from the browser stacking context model: + +- Each visual "layer" becomes a sibling DOM node at the same ancestry level +- `position: absolute; inset: 0;` makes all siblings coextensive (covering the same rectangle) +- `z-index` controls paint order +- The browser compositor stacks them correctly, regardless of element type + +--- + +## 3. OffscreenCanvas + ImageBitmapRenderingContext + +**Source:** MDN Web Docs — OffscreenCanvas API + +### 3.1 Status + +`OffscreenCanvas` is **Baseline Widely Available** as of 2024: + +| Browser | Minimum Version | +| ------- | --------------- | +| Chrome | 69 | +| Firefox | 105 | +| Safari | 16.4 | +| Edge | 79 | + +### 3.2 Key Patterns + +**Pattern A — Worker WebGL → ImageBitmap → Visible Canvas (zero-copy transfer)** + +```javascript +// main thread +const offscreen = canvas.transferControlToOffscreen(); +const worker = new Worker("relief-worker.js"); +worker.postMessage({canvas: offscreen}, [offscreen]); + +// worker thread (relief-worker.js) +self.onmessage = ({data}) => { + const gl = data.canvas.getContext("webgl2"); + // render loop... + // Frames automatically appear on the visible canvas +}; +``` + +This pushes the entire WebGL render loop off the main thread. The visible `` element stays in the DOM at the correct z-index. The GPU work happens in a Worker. + +**Pattern B — OffscreenCanvas + transferToImageBitmap (pull-style)** + +```javascript +// main thread +const offscreen = new OffscreenCanvas(width, height); +const gl = offscreen.getContext("webgl2"); +// render into offscreen... +const bitmap = offscreen.transferToImageBitmap(); +visibleCanvas.getContext("bitmaprenderer").transferFromImageBitmap(bitmap); +``` + +This is synchronous on the main thread. The `ImageBitmapRenderingContext` (`bitmaprenderer`) is a lightweight context that just paints a pre-composited bitmap — no per-pixel work on the main thread. + +### 3.3 WebGL Best Practices (from research) + +- Prefer `webgl2` context +- Set `alpha: false` **only** if no transparency is needed; `alpha: false` can have a significant performance cost on some GPUs +- Use `gl.flush()` if rendering without `requestAnimationFrame` (e.g., render-on-demand MapGenerator pattern) +- Batch draw calls: minimize state changes between draw calls +- Use `texStorage2D` + `texSubImage2D` for texture atlas uploads +- Avoid FBO invalidation (the main cost of Option B / ``) + +--- + +## 4. Three.js Primitives for Relief Icon Rendering + +**Source:** Three.js documentation — InstancedMesh, Sprite, CSS2DRenderer, CSS3DRenderer + +### 4.1 InstancedMesh + +`InstancedMesh(geometry, material, count)` enables drawing N copies of the same geometry in a **single draw call**: + +```javascript +const mesh = new THREE.InstancedMesh(iconGeometry, iconMaterial, maxIcons); +// Per-instance position/scale/rotation: +mesh.setMatrixAt(i, matrix); // then mesh.instanceMatrix.needsUpdate = true +// Per-instance color tint: +mesh.setColorAt(i, color); // then mesh.instanceColor.needsUpdate = true +``` + +Key: The relief icon atlas (a single texture with all icon types as tiles) can be used with `InstancedMesh` where a per-instance attribute selects the UV offset into the atlas. This requires `ShaderMaterial` or `RawShaderMaterial` with custom UV attribute. + +### 4.2 Sprite + +`Sprite` is always camera-facing (billboard). Accepts `SpriteMaterial` with texture: + +```javascript +const sprite = new THREE.Sprite(new THREE.SpriteMaterial({map: texture})); +sprite.center.set(0.5, 0.5); // anchor point +``` + +Sprites are **per-instance objects** — expensive for thousands of icons. `InstancedMesh` is preferred. + +### 4.3 CSS2DRenderer / CSS3DRenderer (Official Three.js Addon) + +Three.js has official addon renderers that solve the **inverse problem** (overlaying DOM elements on WebGL): + +- `CSS2DRenderer` creates a `
` overlay, absolutely positioned, matching the WebGL canvas size +- `CSS3DObject` / `CSS3DSprite` HTML elements are synced to 3D world-space positions +- They use the same camera/projection — no manual coordinate mapping needed + +**Relevance for Option C**: This pattern reveals that Three.js _already uses_ a multi-layer approach for mixing rendering technologies. The same coordinate synchronization technique can be applied in reverse — syncing multiple sibling DOM elements (canvas layers + SVG layers) to the same logical coordinate space. + +--- + +## 5. Mapbox GL v3 — "Slots" Layer Ordering Pattern + +**Source:** Mapbox GL JS v3 migration guide + +### 5.1 The Slots Concept + +Mapbox GL v3 introduced "slots" as a way to interleave custom layers within a single WebGL render context without splitting the DOM: + +```javascript +map.on("style.load", () => { + map.addLayer({id: "my-layer", slot: "middle"}); + // Slots: 'bottom' | 'middle' | 'top' +}); +``` + +Layers are rendered in slot order: all `bottom` layers first, then `middle`, then `top` — within a single WebGL context. + +### 5.2 Implications + +- This pattern demonstrates that even experts at Mapbox chose a **slot abstraction inside one WebGL context** rather than DOM splitting for their primary layer ordering solution +- DOM splitting is not used in production WebGL mapping libraries — they manage draw order explicitly within a single WebGL context +- The slot system is limited to predefined positions (bottom/middle/top) in Mapbox, not arbitrary interleavings + +--- + +## 6. deck.gl — Reactive Layer Architecture + +**Source:** deck.gl developer guide — Composite Layers, Layer Extensions + +### 6.1 Layer Descriptor Model + +deck.gl layers are **cheap, immutable descriptors** — creating a new `Layer` instance does not create GPU resources. The GPU state (buffers, textures, shaders) is managed separately and survives layer re-creation via shallow-diffing of props. + +### 6.2 CompositeLayer Pattern + +```javascript +class LabeledIconLayer extends CompositeLayer { + renderLayers() { + return [ + new IconLayer({ ...icon props... }), + new TextLayer({ ...text props... }), + ]; + } +} +``` + +Composite layers solve the "label + icon" problem (multiple primitives per data point) without DOM involvement. + +### 6.3 Layer Extensions + +`LayerExtension` injects custom shader code (via GLSL injection hooks) into existing layers without subclassing. This is the mechanism for adding per-biome/per-icon-type shader behavior to an `InstancedMesh`-equivalent layer. + +### 6.4 Note on Interleaving + +The deck.gl interleaving documentation page (`/docs/developer-guide/interleaving`) returned 404. From context, deck.gl integrates with Mapbox/MapLibre by registering a "custom layer" inside the base map's render loop — meaning deck.gl layers participate in the slot system when `interleaved: true` is passed to the `DeckGL` constructor. + +--- + +## 7. CSS will-change and GPU Compositor Layer Promotion + +**Source:** MDN Web Docs — `will-change` + +### 7.1 Usage + +```css +.webgl-relief-layer { + will-change: transform; +} +``` + +- Hints browser to promote element to its own GPU compositor layer +- Creates a stacking context (important for z-ordering) +- **Warning**: Use sparingly — each compositor layer consumes GPU memory +- Best practice: set via JS immediately before animation/update, remove (set to `auto`) after + +### 7.2 Dynamic Application + +```javascript +canvas.style.willChange = "transform"; +// ... perform update ... +canvas.style.willChange = "auto"; +``` + +For the FMG use-case (render-on-demand, not continuous animation), manually toggling `will-change` around the render call can reduce compositor overhead. + +--- + +## 8. CSS isolation: isolate + +**Source:** MDN Web Docs — `isolation` + +```css +.map-container { + isolation: isolate; +} +``` + +- `isolation: isolate` **forces** a new stacking context on the element +- Baseline Widely Available (Chrome 41+, Safari 8+) +- Most useful when `mix-blend-mode` is applied to children — `isolation: isolate` on the container prevents blend modes from compositing against elements outside the container +- For FMG map layers: `isolation: isolate` on the map container prevents any child `mix-blend-mode` (e.g., on the heightmap layer) from bleeding into UI elements outside the map + +--- + +## 9. Alternative Approaches Discovered + +Beyond the original three options: + +### Option D — Render-to-Texture (Snapshot Pattern) + +1. Three.js renders relief icons into a `WebGLRenderTarget` (off-screen FBO) +2. The FBO texture is exported as `ImageBitmap` using `gl.readPixels` or `THREE.WebGLRenderer.readRenderTargetPixels` +3. The bitmap is placed into an SVG `` element at the correct layer position +4. This makes the WebGL output a **static image** from SVG's perspective — no special compositing needed + +**Trade-off**: Every time icons change (zoom/pan), a re-render + readback is needed. `gl.readPixels` is synchronous and can stall the GPU pipeline. Acceptable for render-on-demand (FMG rarely re-renders all layers simultaneously). + +### Option E — Pure InstancedMesh in Single Canvas, Sync Coordinates + +The entire map is moved from SVG to a single `` with Three.js rendering all layers. SVG-specific features (text labels, vector coastlines) can use Three.js `CSS3DRenderer` overlay. + +**Trade-off**: Complete rewrite. Loss of SVG export capability and SVG-level accessibility. + +### Option F — WebGL Points/Particles for Relief Icons + +Replace Three.js Scene/Mesh with a `THREE.Points` + `PointsMaterial(sizeAttenuation: true)` + custom sprite sheet UVs. Each relief icon is a point particle with: + +- Position: `BufferGeometry.setAttribute('position', ...)` +- UV offset into atlas: `BufferGeometry.setAttribute('uv', ...)` +- Custom vertex shader for per-point rotation/scale + +Single draw call, extreme simplicity. No instanced matrix overhead. + +### Option G — CSS3DRenderer Overlay (Hybrid DOM + WebGL) + +Three.js `CSS3DRenderer` creates a `
` layer synchronized with the WebGL camera. By embedding SVG content inside `CSS3DObject` instances, SVG elements could theoretically track with map pan/zoom via CSS3D transforms without any canvas at all. + +**Trade-off**: CSS 3D transforms are limited (no SVG-level precision), and browser compositing adds overhead. Not suitable for thousands of icons. + +--- + +## 10. Synthesis: Evaluation Matrix + +| Option | Layer Ordering | GPU Performance | Main-Thread Cost | Implementation Complexity | Risk | +| -------------------- | ------------------- | ------------------------------------ | ---------------- | ------------------------- | ----------------------------------- | +| A: Canvas beside SVG | ❌ Fixed at outside | ✅ Single canvas | Low | Low | None — already tried | +| B: foreignObject | ✅ Correct | ❌ FBO every frame | High | Low | Confirmed problematic | +| C: DOM Split | ✅ Correct | ✅ Good (one canvas per WebGL layer) | Medium | **High** | Browser behavior with many canvases | +| D: Render-to-Texture | ✅ SVG-native | Medium (readPixels stall) | Low (snapshot) | Medium | Stall on large maps | +| E: Pure Canvas | ✅ (trivial in 3D) | ✅ Best | Low | Very High | Full rewrite + SVG export loss | +| F: Points/Particles | ✅ Same as canvas | ✅ Best single-draw | Low | Low | Orthographic camera needed | +| G: CSS3D Overlay | ✅ Correct | Medium | Medium | Medium | CSS3D precision limits | + +--- + +## 11. Key Technical Constraints for Fantasy-Map-Generator + +From codebase analysis: + +1. **Render-on-demand pattern**: FMG re-renders layers when map data changes (not on every animation frame). This means `requestAnimationFrame` loops are not used. +2. **SVG + D3 coordinate system**: All current layer positions use SVG viewport coordinates. Any WebGL canvas must map to the same coordinate space. +3. **Layer order is significant**: At least these layers must interleave correctly: `terrs` (terrain), `rivers`, `routes`, `relief`, `borders`, `burgIcons`, `markers`. +4. **TypedArray cell data**: The packed graph data is already GPU-friendly (TypedArrays). Uploading to WebGL buffers is straightforward. +5. **Two canvas limit concern**: Browsers typically allow 8-16 WebGL contexts before older ones are lost. Each `` using WebGL counts. Option C with multiple WebGL canvases may hit this limit. + +--- + +## 12. Recommended Focus Areas for Brainstorming + +Based on research, the three most promising directions for deep exploration: + +1. **Option C (DOM Split) — low canvas count variant**: Use a single WebGL canvas for all GPU-accelerated layers (not one per layer). Multiple SVG fragments and one WebGL canvas are interleaved by z-index. The single canvas renders only the union of GPU layers. + +2. **Option F (Points/Particles) — single canvas, relief only**: Keep the existing SVG stack intact, add one `` element with WebGL `gl.POINTS` rendering relief icons. Position the canvas using `position: absolute` and `z-index` in the correct slot. This is the minimal change. + +3. **Option D (Snapshot) — render-to-texture + SVG ``**: Three.js renders into FBO, FMG reads it as ImageBitmap (or uses CanvasTexture trick), injects into SVG `` tag at correct layer. Leverages SVG's native layer ordering. + +--- + +## 13. Wave 2 Research: Single-Canvas WebGL Layer Management + +_Added in response to follow-up question: "Can we render all layers into a single WebGL Canvas with sublayers (z-index) inside, toggle them, reorder them, then have 1 SVG on top for interactive elements?"_ + +### 13.1 Mapbox GL JS — Proof of Concept (Production Scale) + +Mapbox GL renders its entire map (ocean, terrain, labels, symbols, raster tiles, lines) in **one single WebGL context**. Layer management APIs: + +| API | Description | +| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| `map.moveLayer(id, beforeId)` | Reorders a layer in the draw stack within the same slot; cross-slot moves are silently ignored | +| `map.setLayoutProperty(id, 'visibility', 'none'\|'visible')` | O(1) visibility toggle — GPU buffers (VBOs, textures) stay allocated | +| `map.addLayer(layerObj, beforeId)` | Inserts a new layer at a specific position in the draw stack | + +**Key constraint:** Mapbox's slot system (`bottom`, `middle`, `top`) buckets layers — `moveLayer` only works within a slot. This implies even at production scale, a hierarchical ordering model is needed for complex layer stacks. + +**Conclusion:** Single WebGL canvas with full layer ordering is used in production at massive scale. It absolutely works technically. + +### 13.2 deck.gl — Layer Management Patterns (Optimal GPU Preservation) + +deck.gl uses a declarative layer array (`deck.setProps({ layers: [...] })`). Key learnings: + +| Pattern | Mechanism | GPU Impact | +| --------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **Toggle visibility** | `new Layer({ visible: false })` prop | **GPU state preserved** — VBOs, shader programs stay allocated; recommended over conditional rendering | +| **Conditional rendering** (remove from array) | Layer removed from layers array | GPU state **destroyed** — expensive re-upload on next show; NOT recommended | +| **Reorder layers** | Re-order entries in layers array + call `setProps` | deck.gl diffs by layer `id`, matches existing GPU state; zero re-upload | +| **Z-fighting prevention** | `getPolygonOffset: ({ layerIndex }) => [0, -layerIndex * 100]` | Automatic polygon offset per layer index; handles coplanar layers | + +**Critical insight:** The `visible: false` prop pattern preserves GPU state (avoids costly re-upload on show/hide toggle). This pattern is the correct one to use when designing FMG layer toggles in any WebGL migration. + +### 13.3 Three.js — Layer Management APIs + +| API | Description | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `Object3D.visible = false` | Hides the object; GPU buffers stay allocated; equivalent to deck.gl `visible: false` | +| `Object3D.renderOrder = n` | Integer draw order override; lower numbers rendered first (painter's algorithm); works on `Group` objects (all children sorted together) | +| `Object3D.layers` (Layers bitmask) | 32 bitmask slots for **camera culling**, not draw order; `camera.layers.enable(n)` + `object.layers.set(n)` — camera renders only objects sharing at least one layer bit. Useful for selective rendering passes, not for z-ordering. | + +**For FMG use case:** `renderOrder` is the correct API for z-ordering multiple logical layers within one Three.js scene. `visible` is the correct API for layer toggles. + +```typescript +// In Three.js, assign each FMG layer a Group with renderOrder: +const oceanlayers = new THREE.Group(); +oceanlayers.renderOrder = 1; // bottom + +const rivers = new THREE.Group(); +rivers.renderOrder = 11; + +const relief = new THREE.Group(); +relief.renderOrder = 12; + +const burgIcons = new THREE.Group(); +burgIcons.renderOrder = 26; + +// Toggle: +burgIcons.visible = false; // GPU buffers stay allocated, instant toggle + +// Reorder: +rivers.renderOrder = 15; // Instantly reorders in next frame +``` + +--- + +## 14. FMG SVG Architecture Deep Dive (Complete Layer Inventory) + +_Confirmed via full codebase analysis of public/main.js and src/index.html._ + +### 14.1 Container Structure + +- `` — root SVG element; D3 zoom and resize targets it +- `` — single transform container; **ALL map layers live here**; D3 zoom applies `translate(x y) scale(z)` to this element +- `#scaleBar`, `#legend`, `#vignette` — **outside** `#viewbox`; fixed on screen (screen coordinates, not map coordinates) + +### 14.2 Complete Layer Stack (32 layers, bottom to top) + +| # | `` | Renders | SVG Feature Used | +| --- | ------------------------------------------------------------ | ---------------------------------- | --------------------------------------------------------------------------------------- | +| 1 | `ocean` + `oceanLayers`, `oceanPattern` | Depth gradient, ocean texture | ``, ``, `url(#oceanic)` pattern | +| 2 | `lakes` + 6 sublayers | Lake fills by type | `` | +| 3 | `landmass` | Base land color | `` + fill color | +| 4 | `texture` | Land texture overlay | `` | +| 5 | `terrs` + `oceanHeights`, `landHeights` | Elevation contour bands | `` | +| 6 | `biomes` | Biome color fills | `` | +| 7 | `cells` | Voronoi cell grid | Single `` | +| 8 | `gridOverlay` | Hex/square grid | `` + `url(#pattern_*)` pattern | +| 9 | `coordinates` + `coordinateGrid`, `coordinateLabels` | Lat/lon graticule + labels | ``, **``** | +| 10 | `compass` | Compass rose | **``** | +| 11 | `rivers` | River bezier curves | `` | +| 12 | `terrain` | Relief icons | **``** per icon | +| 13 | `relig` | Religion fills | `` | +| 14 | `cults` | Culture fills | `` | +| 15 | `regions` + `statesBody`, `statesHalo` | State fills + halo | ``, **``** | +| 16 | `provs` + `provincesBody`, `provinceLabels` | Province fills + labels | ``, **``** | +| 17 | `zones` | Zone fills | `` | +| 18 | `borders` + `stateBorders`, `provinceBorders` | Political borders | `` | +| 19 | `routes` + `roads`, `trails`, `searoutes` | Transport routes | `` | +| 20 | `temperature` | Temperature cells + values | colored fills, **``** | +| 21 | `coastline` + `sea_island`, `lake_island` | Coastline shapes | `` | +| 22 | `ice` | Glaciers, icebergs | `` | +| 23 | `prec` + `wind` | Precipitation circles, wind arrows | ``, **`` (unicode glyphs)** | +| 24 | `population` + `rural`, `urban` | Population bars | `` | +| 25 | `emblems` + `burgEmblems`, `provinceEmblems`, `stateEmblems` | Heraldic CoAs | **`` COA symbols from `#defs-emblems`** | +| 26 | `icons` + `burgIcons`, `anchors` | Burg icons, port anchors | **`` from `#defElements`** | +| 27 | `labels` + `states`, `addedLabels`, `burgLabels` | All map text labels | **``** (curved state labels), **``** (burg labels) | +| 28 | `armies` | Regiment markers | **`` (emoji/icon)**, `` (banner) | +| 29 | `markers` | Point of interest icons | `` | +| 30 | `fogging-cont` | Fog of war | `` + mask | +| 31 | `ruler` | Measurement tools | ``, ``, **``** | +| 32 | `debug` | Editor overlays | temporary | + +### 14.3 SVG Features that Cannot Trivially Move to WebGL + +| Feature | Layers Using It | WebGL Migration Cost | +| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `` + `` (curved text) | `#labels>#states`, `#addedLabels` | **VERY HIGH** — SDF font atlas or DOM overlay required; `` has no WebGL equivalent | +| `` (normal) | `#burgLabels`, `#coordinateLabels`, `#provinceLabels`, `#temperature`, `#prec>#wind`, `#armies`, `#scaleBar`, `#legend` | HIGH — CSS2DRenderer overlay or SDF atlas | +| `` + defs symbols | `#terrain`, `#compass`, `#emblems`, `#icons`, `#burgIcons`, `#anchors` | HIGH — must pre-rasterize all symbols to texture atlases | +| `` | `#regions>#statesHalo` | MEDIUM — WebGL stencil buffer; doable but different model | +| `fill="url(#pattern*)"` | `#ocean>#oceanPattern`, `#gridOverlay` | MEDIUM — WebGL texture tiling shader | +| `fill="url(#hatch*)"` | Various political layers | MEDIUM — WebGL texture tiling shader | +| `` | `#texture`, `#markers`, `#armies` | LOW — Three.js `PlaneGeometry` + `TextureLoader` | + +### 14.4 SVG Export System (Critical Path) + +File: `public/modules/io/export.js` — `exportToSvg()` / `getMapURL()` + +The export function: + +1. **Clones `#map` SVG element** via `cloneNode(true)` +2. Serializes the entire SVG DOM tree to XML string +3. Copies emblem COA symbols, compass, burg icons, grid patterns, hatch patterns into export defs +4. Converts raster image `href`s to base64 for self-contained export +5. Inlines all computed CSS styles +6. Copies all font face data-URIs as inline `