@godot-scene-web/effects 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particles-CQC9BFSI.js","names":["lerp","clamp01"],"sources":["../src/particles/godot-renderer.ts","../src/particles/instance-buffer.ts","../src/particles/pack-instances.ts","../src/particles/sampling.ts","../src/particles/simulate.ts","../src/particles/state.ts"],"sourcesContent":["// Which Godot rendering backend the browser is imitating, and the ONE colour\n// difference between them that godot-scene-web models.\n//\n// ---------------------------------------------------------------------------\n// This is not a colour-management layer, and must not grow into one.\n// ---------------------------------------------------------------------------\n//\n// godot-scene-web is deliberately sRGB end to end: `render-structure.ts` pins\n// `color-interpolation-filters=\"sRGB\"` on every emitted `<filter>`, `tint-bake.ts`\n// applies its colour matrices in the sRGB BYTE domain, and both particle backends\n// hand straight sRGB values to the canvas. That stance is correct and is not being\n// revisited here. `srgbToLinear` below exists to reproduce ONE injected constant in\n// Godot's own pipeline, on ONE property, and nothing else may use it.\n//\n// **What Godot actually does.** Its RendererRD backends (`forward_plus` and `mobile`)\n// run `ParticleProcessMaterial.color` through `Color::srgb_to_linear()` exactly once,\n// on the CPU, at UBO-upload time — and then write the result into a NON-LINEAR canvas\n// with no inverse conversion anywhere downstream. RD 2D is NOT linear end to end:\n// `rendering/viewport/hdr_2d` defaults to false, so the render target is\n// `R8G8B8A8_UNORM` (`servers/rendering/renderer_rd/storage_rd/texture_storage.cpp`\n// :4308-4314), the present blit's `convert_to_srgb` is false\n// (`renderer_compositor_rd.cpp:98`), vertex colours are not linearized, and there is no\n// sRGB present step. So the right mental model is \"Godot linearizes this one property\n// and then forgets to undo it\", NOT \"Godot's 2D renderer works in linear light\".\n// Reproducing it therefore means applying the same curve to the same property — not\n// converting a colour space.\n//\n// **The chain, in Godot 4.5.1** (`../godot-4.5.1-stable`):\n// `scene/resources/particle_process_material.cpp:309`\n// `code += \"uniform vec4 color_value : source_color;\\n\";`\n// `servers/rendering/renderer_rd/storage_rd/particles_storage.cpp:1768`\n// `update_parameters_uniform_set(…, 3, true, false)` — the `true` is\n// `p_use_linear_color`, passed UNCONDITIONALLY (unlike the canvas path at\n// `renderer_canvas_render_rd.cpp:670`, which keys it off `hdr_2d`).\n// `servers/rendering/renderer_rd/storage_rd/material_storage.cpp:456-459`\n// (shader-default path) and `servers/rendering/storage/variant_converters.h`\n// :208-213 (the user-set-value path a `.tscn` `color = Color(…)` takes) — both\n// converge on `Color::srgb_to_linear()`.\n//\n// **The scope is exactly one property, and Godot's own code proves it.** Every OTHER\n// colour in the RD canvas path is converted only under `use_linear_colors`, which is\n// `render_target_is_using_hdr(...)` (`renderer_canvas_render_rd.cpp:669`) — so with\n// `hdr_2d` off, the canvas `modulate` (`:692`), per-rect modulation (`:2408`) and polygon\n// vertex colours (`:2622`) are all left alone. `particles_storage.cpp:1768` is the one\n// call site that passes `true` unconditionally. `CPUParticles2D` is not affected either:\n// it computes `p.color = color_ramp * color … * base_color` on the CPU\n// (`scene/2d/cpu_particles_2d.cpp:1086-1096`) and emits vertex colours, and the string\n// `srgb_to_linear` does not occur in that file at all. Hence `fromProcessMaterial` below.\n//\n// **Compatibility/GLES3 does none of it.** Its material UBO fill\n// (`drivers/gles3/storage/material_storage.cpp:765-793`) has no `p_use_linear_color`\n// parameter at all, and the single `srgb_to_linear` in that whole file is the commented\n// out line 1724 in the global-shader-uniform store. So a GLES3 capture and a browser\n// render agree, and it is a Forward+/Mobile capture that is the outlier.\n//\n// **Stated limitation: `hdr_2d = true`.** With an HDR 2D viewport Godot's chain is a\n// different one — the render target becomes a float format, the canvas material path\n// switches to its linear uniform set (`renderer_canvas_render_rd.cpp:2272`), and the\n// present blit re-encodes to sRGB (`renderer_compositor_rd.cpp:98`) — and this\n// correction would then be WRONG, because the conversion it reproduces is no longer\n// left un-undone. godot-scene-web models no HDR-2D concept at all: it does not parse\n// `rendering/viewport/hdr_2d`, and `godotRenderer` cannot express it. Consumers running\n// an HDR 2D viewport should set `godotRenderer: \"gl_compatibility\"` to opt out of the\n// correction; that is the closest available answer, not an exact one.\n\n/**\n * Which Godot rendering backend a browser render should imitate.\n *\n * Named after the CAUSE rather than after the correction: the two RendererRD backends\n * behave one way and Compatibility the other, and the option says which engine produced\n * the pixels the browser is being asked to match.\n *\n * Defaults to `\"forward_plus\"` — Godot's own default `rendering/renderer/rendering_method`\n * for a new project, and what a capture is overwhelmingly likely to have come from.\n */\nexport type GodotRendererBackend =\n | \"forward_plus\"\n | \"mobile\"\n | \"gl_compatibility\";\n\n/** The default backend: Godot's own default for a new project. */\nexport const DEFAULT_GODOT_RENDERER: GodotRendererBackend = \"forward_plus\";\n\n/** Coerce an untrusted value (a hand-authored spec blob, a consumer option) to a backend. */\nexport function normalizeGodotRenderer(value: unknown): GodotRendererBackend {\n return value === \"mobile\" ||\n value === \"gl_compatibility\" ||\n value === \"forward_plus\"\n ? value\n : DEFAULT_GODOT_RENDERER;\n}\n\n/**\n * True when the backend runs `ParticleProcessMaterial.color` through\n * `Color::srgb_to_linear()` at UBO-upload time. Both RendererRD backends do;\n * Compatibility/GLES3 does not.\n */\nexport function linearizesParticleColor(\n renderer: GodotRendererBackend,\n): boolean {\n return renderer !== \"gl_compatibility\";\n}\n\n/**\n * Godot's `Color::srgb_to_linear()`, one channel — `core/math/color.h:191-197`:\n *\n * ```cpp\n * r < 0.04045f ? r * (1.0f / 12.92f)\n * : Math::pow(float((r + 0.055) * (1.0 / (1.0 + 0.055))), 2.4f)\n * ```\n *\n * The piecewise IEC 61966-2-1 curve with exponent **2.4**, not a `pow(x, 2.2)`\n * approximation, and the threshold comparison is strict `<`. Written here the way Godot\n * writes it — reciprocal multiplies rather than divides — so the two read the same;\n * Godot evaluates the inner expression in double and then narrows to float before\n * `pow`, while JS stays in double throughout, a difference orders of magnitude below\n * the 1/255 the comparison is made at.\n *\n * Note the fixed point at both ends: `srgbToLinear(0) === 0` and `srgbToLinear(1) === 1`.\n * A fully saturated channel is where this curve is the identity, which is why it is the\n * per-channel regression check on the parity fixture (see `docs/parity.md`).\n */\nexport function srgbToLinear(value: number): number {\n return value < 0.04045\n ? value * (1 / 12.92)\n : ((value + 0.055) * (1 / 1.055)) ** 2.4;\n}\n\n/**\n * A particle system's base colour as the given backend uploads it: RGB through\n * {@link srgbToLinear} on the RendererRD backends, returned unchanged on\n * Compatibility/GLES3.\n *\n * `fromProcessMaterial` is the scope gate. Only `ParticleProcessMaterial.color` — a\n * `GPUParticles2D` with a process material — rides the UBO path that linearizes. When the\n * base colour instead came from `CPUParticles2D.color` or the node's `modulate`, it reaches\n * the canvas by a route Godot leaves in sRGB whatever the backend, and this returns it\n * untouched.\n *\n * **ALPHA IS NEVER TOUCHED.** `Color::srgb_to_linear()` passes `a` straight through\n * (`color.h:196`), so the coverage the blend algebra runs on is the authored value on\n * every backend and every path.\n *\n * Always returns a fresh tuple, so callers never alias the raw colour they passed in.\n */\nexport function linearizeParticleBaseColor(\n color: readonly [number, number, number, number],\n renderer: GodotRendererBackend,\n fromProcessMaterial: boolean,\n): [number, number, number, number] {\n return fromProcessMaterial && linearizesParticleColor(renderer)\n ? [\n srgbToLinear(color[0]),\n srgbToLinear(color[1]),\n srgbToLinear(color[2]),\n color[3],\n ]\n : [color[0], color[1], color[2], color[3]];\n}\n","/** Floats per particle render instance: center.xy, scale.xy, rotation, color.rgba, frame. */\nexport const INSTANCE_STRIDE = 10;\n\n/**\n * CPU-owned packed particle instances. This class deliberately knows nothing about\n * WebGL, WebGPU, a canvas, or a device lifecycle; render adapters own residency.\n */\nexport class InstanceBuffer {\n data: Float32Array;\n /** Number of instances written since the last reset. */\n count = 0;\n private capacity: number;\n\n constructor(initialCapacity = 256) {\n this.capacity = Math.max(1, initialCapacity);\n this.data = new Float32Array(this.capacity * INSTANCE_STRIDE);\n }\n\n reset(): void {\n this.count = 0;\n }\n\n push(\n x: number,\n y: number,\n scaleX: number,\n scaleY: number,\n rotation: number,\n r: number,\n g: number,\n b: number,\n a: number,\n frame: number,\n ): void {\n this.ensureCapacity(this.count + 1);\n const offset = this.count * INSTANCE_STRIDE;\n const data = this.data;\n data[offset] = x;\n data[offset + 1] = y;\n data[offset + 2] = scaleX;\n data[offset + 3] = scaleY;\n data[offset + 4] = rotation;\n data[offset + 5] = r;\n data[offset + 6] = g;\n data[offset + 7] = b;\n data[offset + 8] = a;\n data[offset + 9] = frame;\n this.count += 1;\n }\n\n ensureCapacity(instances: number): void {\n if (instances <= this.capacity) return;\n let next = this.capacity;\n while (next < instances) next *= 2;\n const grown = new Float32Array(next * INSTANCE_STRIDE);\n grown.set(this.data);\n this.data = grown;\n this.capacity = next;\n }\n}\n","import type { ParticleRenderConfig } from \"./config\";\nimport type { InstanceBuffer } from \"./instance-buffer\";\nimport type { ParticleSystemState } from \"./state\";\n\nexport interface ParticleInstanceTransform {\n readonly xx: number;\n readonly xy: number;\n readonly yx: number;\n readonly yy: number;\n readonly originX: number;\n readonly originY: number;\n readonly scale?: number;\n readonly rotation?: number;\n}\n\nexport interface ParticleInstancePackInput {\n readonly state: ParticleSystemState;\n readonly config: Pick<\n ParticleRenderConfig,\n \"hframes\" | \"vframes\" | \"flipbookCropOnly\"\n >;\n readonly instances: InstanceBuffer;\n readonly textureWidth: number;\n readonly textureHeight: number;\n readonly origin?: readonly [number, number];\n readonly transform?: ParticleInstanceTransform;\n readonly modulate?: readonly [number, number, number, number];\n}\n\n/** The sprite-sheet grid used for a texture; untextured particles always have one frame. */\nexport function frameGridFor(\n textured: boolean,\n hframes: number,\n vframes: number,\n): [number, number] {\n return textured ? [Math.max(1, hframes), Math.max(1, vframes)] : [1, 1];\n}\n\n/** Pack live particle state into a caller-owned, reusable GPU instance buffer. */\nexport function packParticleInstances(\n input: ParticleInstancePackInput,\n): number {\n const { state, config, instances } = input;\n const originX = input.origin?.[0] ?? 0;\n const originY = input.origin?.[1] ?? 0;\n const frameW = config.flipbookCropOnly\n ? input.textureWidth\n : input.textureWidth / Math.max(1, config.hframes);\n const frameH = config.flipbookCropOnly\n ? input.textureHeight\n : input.textureHeight / Math.max(1, config.vframes);\n const transform = input.transform;\n const modulate = input.modulate;\n const xx = transform?.xx ?? 1,\n xy = transform?.xy ?? 0,\n yx = transform?.yx ?? 0,\n yy = transform?.yy ?? 1;\n const tx = transform?.originX ?? 0,\n ty = transform?.originY ?? 0;\n const scale = transform?.scale ?? 1,\n rotation = transform?.rotation ?? 0;\n const mr = modulate?.[0] ?? 1,\n mg = modulate?.[1] ?? 1,\n mb = modulate?.[2] ?? 1,\n ma = modulate?.[3] ?? 1;\n instances.reset();\n for (let i = 0; i < state.particles.length; i += 1) {\n const p = state.particles[i];\n if (!(p.active && p.a > 0)) continue;\n const localX = originX + p.x,\n localY = originY + p.y;\n instances.push(\n xx * localX + yx * localY + tx,\n xy * localX + yy * localY + ty,\n Math.max(0, frameW * p.scaleX * scale),\n Math.max(0, frameH * p.scaleY * scale),\n p.rotation + rotation,\n p.r * mr,\n p.g * mg,\n p.b * mb,\n p.a * ma,\n p.frame,\n );\n }\n return instances.count;\n}\n","/** Renderer-neutral ramp and curve values used by particle state and texture bakers. */\nexport interface ParticleGradientStop {\n offset: number;\n color: [number, number, number, number];\n}\n\nexport interface ParticleCurvePoint {\n x: number;\n y: number;\n}\n\nfunction lerp(a: number, b: number, t: number): number {\n return a + (b - a) * t;\n}\n\n/** Godot-style endpoint-clamped gradient sampling. */\nexport function sampleParticleGradient(\n stops: readonly ParticleGradientStop[],\n t: number,\n interpolationMode = 0,\n): [number, number, number, number] {\n if (stops.length === 0) return [0, 0, 0, 1];\n if (t <= stops[0].offset) return stops[0].color;\n const last = stops[stops.length - 1];\n if (t >= last.offset) return last.color;\n for (let index = 0; index < stops.length - 1; index += 1) {\n const a = stops[index];\n const b = stops[index + 1];\n if (t >= a.offset && t <= b.offset) {\n if (interpolationMode === 1) return a.color;\n const fraction = (t - a.offset) / (b.offset - a.offset || 1);\n return [\n lerp(a.color[0], b.color[0], fraction),\n lerp(a.color[1], b.color[1], fraction),\n lerp(a.color[2], b.color[2], fraction),\n lerp(a.color[3], b.color[3], fraction),\n ];\n }\n }\n return last.color;\n}\n\n/** Allocation-free gradient sampling for simulation hot paths. */\nexport function sampleParticleGradientInto(\n stops: readonly ParticleGradientStop[],\n t: number,\n out: [number, number, number, number],\n interpolationMode = 0,\n): void {\n if (stops.length === 0) {\n out[0] = 0;\n out[1] = 0;\n out[2] = 0;\n out[3] = 1;\n return;\n }\n let a = stops[0];\n if (t <= a.offset) {\n out[0] = a.color[0];\n out[1] = a.color[1];\n out[2] = a.color[2];\n out[3] = a.color[3];\n return;\n }\n const last = stops[stops.length - 1];\n if (t >= last.offset) {\n out[0] = last.color[0];\n out[1] = last.color[1];\n out[2] = last.color[2];\n out[3] = last.color[3];\n return;\n }\n for (let index = 0; index < stops.length - 1; index += 1) {\n a = stops[index];\n const b = stops[index + 1];\n if (t >= a.offset && t <= b.offset) {\n const f =\n interpolationMode === 1\n ? 0\n : (t - a.offset) / (b.offset - a.offset || 1);\n out[0] = lerp(a.color[0], b.color[0], f);\n out[1] = lerp(a.color[1], b.color[1], f);\n out[2] = lerp(a.color[2], b.color[2], f);\n out[3] = lerp(a.color[3], b.color[3], f);\n return;\n }\n }\n out[0] = last.color[0];\n out[1] = last.color[1];\n out[2] = last.color[2];\n out[3] = last.color[3];\n}\n\n/** Godot-style endpoint-clamped linear curve sampling. */\nexport function sampleParticleCurve(\n points: readonly ParticleCurvePoint[],\n t: number,\n): number {\n if (points.length === 0) return 0;\n if (t <= points[0].x) return points[0].y;\n const last = points[points.length - 1];\n if (t >= last.x) return last.y;\n for (let index = 0; index < points.length - 1; index += 1) {\n const a = points[index];\n const b = points[index + 1];\n if (t >= a.x && t <= b.x)\n return lerp(a.y, b.y, (t - a.x) / (b.x - a.x || 1));\n }\n return last.y;\n}\n\nexport function normalizeParticleCurve(\n points: readonly ParticleCurvePoint[] | undefined,\n): ParticleCurvePoint[] | undefined {\n return points\n ?.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y))\n .slice()\n .sort((a, b) => a.x - b.x);\n}\n","// Deterministic CPU particle simulation — a \"visually plausible\" port of Godot 4.5\n// `CPUParticles2D::_particles_process` (scene/2d/cpu_particles_2d.cpp), with the\n// GPU-only shapes/params from particle_process_material.cpp folded into the same\n// config (the build-time reader unifies CPU/GPU into one `ParticleSpecConfig`).\n//\n// Pure + DOM-free + GL-free, so it is unit-testable in plain node. Not frame-exact to\n// Godot: one LCG RNG and one seeding scheme; fractional-delta ignored; forward hue\n// matrix; ring treated as a flat annulus. The things that DO matter for the look are\n// kept faithful: spawn random draw ORDER, explosiveness/randomness birth timing,\n// per-particle fixed force seed, the color/scale curve chain, and Godot units\n// (spread = half-angle degrees, orbit = rev/s, gravity in px, align_y binds +Y).\n\nimport type { ParticleConfig } from \"./config\";\nimport { sampleParticleCurve, sampleParticleGradientInto } from \"./sampling\";\nimport type { Particle, ParticleSystemState } from \"./state\";\n\nconst TAU = Math.PI * 2;\nconst DEG2RAD = Math.PI / 180;\n\nfunction lerp(a: number, b: number, t: number): number {\n return a + (b - a) * t;\n}\n\nfunction clamp01(value: number): number {\n return value < 0 ? 0 : value > 1 ? 1 : value;\n}\n\n// Godot's MINSTD (Park–Miller) LCG, returning [0,1] and advancing the seed in place.\n// One generator + one seeding scheme for the whole sim (distribution, not exact\n// values, is what matters at the \"visually plausible\" bar).\nfunction randFromSeed(holder: { seed: number }): number {\n let s = holder.seed | 0;\n if (s === 0) s = 305420679;\n const k = Math.trunc(s / 127773);\n s = 16807 * (s - k * 127773) - 2836 * k;\n if (s < 0) s += 2147483647;\n holder.seed = s >>> 0;\n return (holder.seed % 65536) / 65535;\n}\n\nconst spawnRng = { seed: 1 };\nconst forceRng = { seed: 1 };\nconst displayColor: [number, number, number, number] = [1, 1, 1, 1];\n\nfunction mixSeed(s: number): number {\n return Math.imul((s >>> 0) ^ 0x9e3779b9, 2654435761) >>> 0;\n}\n\n// Stable [0,1) hash used for per-slot birth-time jitter (randomness).\nfunction hash01(i: number, seed: number): number {\n let h = Math.imul((i + 1) ^ (seed | 0), 2654435761) >>> 0;\n h ^= h >>> 15;\n h = Math.imul(h, 2246822519) >>> 0;\n h ^= h >>> 13;\n return (h >>> 0) / 4294967296;\n}\n\n// Forward YIQ-style hue rotation matrix (particle_process_material.cpp form).\nfunction hueRotateInto(\n p: Particle,\n r: number,\n g: number,\n b: number,\n angle: number,\n): void {\n const c = Math.cos(angle);\n const s = Math.sin(angle);\n const rr =\n r * (0.299 + 0.701 * c + 0.168 * s) +\n g * (0.587 - 0.587 * c + 0.33 * s) +\n b * (0.114 - 0.114 * c - 0.497 * s);\n const gg =\n r * (0.299 - 0.299 * c - 0.328 * s) +\n g * (0.587 + 0.413 * c + 0.035 * s) +\n b * (0.114 - 0.114 * c + 0.292 * s);\n const bb =\n r * (0.299 - 0.3 * c + 1.25 * s) +\n g * (0.587 - 0.588 * c - 1.05 * s) +\n b * (0.114 + 0.886 * c - 0.203 * s);\n p.r = rr;\n p.g = gg;\n p.b = bb;\n}\n\n// Emission position in node-local pixels, using `rng()` draws. Sphere/sphere-surface\n// are both approximated as a uniform-area disk (the radial distribution difference is\n// invisible for a 2D glow); ring is an area-uniform annulus.\nfunction sampleEmissionInto(cfg: ParticleConfig, p: Particle): void {\n let x = 0;\n let y = 0;\n const shape = cfg.emissionShape;\n if (shape === 1 || shape === 2) {\n const a = randFromSeed(spawnRng) * TAU;\n const r = cfg.emissionSphereRadius * Math.sqrt(randFromSeed(spawnRng));\n x = Math.cos(a) * r;\n y = Math.sin(a) * r;\n } else if (shape === 3) {\n x = (randFromSeed(spawnRng) * 2 - 1) * cfg.emissionBoxExtents[0];\n y = (randFromSeed(spawnRng) * 2 - 1) * cfg.emissionBoxExtents[1];\n } else if (shape === 6) {\n const a = randFromSeed(spawnRng) * TAU;\n const outer = Math.max(\n cfg.emissionRingRadius,\n cfg.emissionRingInnerRadius,\n 0.0001,\n );\n const inner = Math.max(0, Math.min(cfg.emissionRingInnerRadius, outer));\n const r = Math.sqrt(\n randFromSeed(spawnRng) * (outer * outer - inner * inner) + inner * inner,\n );\n x = Math.cos(a) * r;\n y = Math.sin(a) * r;\n }\n // shape 0 (point) / unsupported -> origin.\n p.x = x * cfg.emissionScale[0] + cfg.emissionOffset[0];\n p.y = y * cfg.emissionScale[1] + cfg.emissionOffset[1];\n}\n\n// The base spawn time of slot `i` within a cycle, in [0,1) of lifetime. explosiveness\n// compresses all births toward 0 (burst); randomness jitters each slot.\nfunction restartPhase(cfg: ParticleConfig, i: number): number {\n let rp = cfg.amount > 0 ? i / cfg.amount : 0;\n if (cfg.randomness > 0) {\n rp += (cfg.randomness * hash01(i, cfg.seed)) / cfg.amount;\n }\n return rp * (1 - cfg.explosiveness);\n}\n\n// Spawn / restart a particle (cpu_particles_2d.cpp:837-937). Spawn randoms are drawn\n// in Godot's order — angle, scale, hue, anim-offset, [start color], spread, speed,\n// lifetime, then emission position — because reordering visibly changes which\n// particles are big/bright/fast together.\nfunction restartParticle(\n state: ParticleSystemState,\n i: number,\n cycle: number,\n): void {\n const cfg = state.config;\n const p = state.particles[i];\n const baseSeed = (cfg.seed + i * 2 + cycle) >>> 0;\n p.seed = baseSeed || 1;\n spawnRng.seed = mixSeed(baseSeed) || 1;\n\n p.angleRand = randFromSeed(spawnRng);\n p.scaleRand = randFromSeed(spawnRng);\n p.hueRand = randFromSeed(spawnRng);\n p.animOffsetRand = randFromSeed(spawnRng);\n if (cfg.colorInitialRamp?.length)\n sampleParticleGradientInto(\n cfg.colorInitialRamp,\n randFromSeed(spawnRng),\n p.startColor,\n );\n else {\n p.startColor[0] = 1;\n p.startColor[1] = 1;\n p.startColor[2] = 1;\n p.startColor[3] = 1;\n }\n\n const dirAngle = Math.atan2(cfg.direction[1], cfg.direction[0]);\n const angle =\n dirAngle + (randFromSeed(spawnRng) * 2 - 1) * cfg.spread * DEG2RAD;\n const speed = lerp(\n cfg.initialVelocityMin,\n cfg.initialVelocityMax,\n randFromSeed(spawnRng),\n );\n p.vx = Math.cos(angle) * speed;\n p.vy = Math.sin(angle) * speed;\n\n p.rotation = lerp(cfg.angleMin, cfg.angleMax, p.angleRand) * DEG2RAD;\n if (cfg.alignY) {\n const sp = Math.hypot(p.vx, p.vy);\n // Godot binds the sprite's +Y to the velocity; +PI/2 turns \"up\" toward travel.\n if (sp > 0) p.rotation = Math.atan2(p.vy, p.vx) + Math.PI / 2;\n }\n p.lifetime = Math.max(\n 0.01,\n cfg.lifetime * (1 - randFromSeed(spawnRng) * cfg.lifetimeRandomness),\n );\n\n sampleEmissionInto(cfg, p);\n p.time = 0;\n p.active = true;\n updateDisplay(cfg, p, 0);\n}\n\n// Per-frame display outputs: size from scale curve(s) × random base scale; color from\n// color_ramp(tv) × base × hue × start-color; alpha from color × alpha_curve(tv);\n// flipbook frame from anim offset + tv × anim speed.\nfunction updateDisplay(cfg: ParticleConfig, p: Particle, tv: number): void {\n let sx = 1;\n let sy = 1;\n const hasSplit =\n (cfg.scaleCurveX && cfg.scaleCurveX.length > 0) ||\n (cfg.scaleCurveY && cfg.scaleCurveY.length > 0);\n if (hasSplit) {\n sx = cfg.scaleCurveX?.length ? sampleParticleCurve(cfg.scaleCurveX, tv) : 1;\n sy = cfg.scaleCurveY?.length ? sampleParticleCurve(cfg.scaleCurveY, tv) : 1;\n } else if (cfg.scaleCurve && cfg.scaleCurve.length > 0) {\n sx = sampleParticleCurve(cfg.scaleCurve, tv);\n sy = sx;\n }\n const base = lerp(cfg.scaleMin, cfg.scaleMax, p.scaleRand);\n p.scaleX = Math.max(1e-5, sx * base);\n p.scaleY = Math.max(1e-5, sy * base);\n\n if (cfg.colorRamp?.length)\n sampleParticleGradientInto(cfg.colorRamp, tv, displayColor);\n else {\n displayColor[0] = 1;\n displayColor[1] = 1;\n displayColor[2] = 1;\n displayColor[3] = 1;\n }\n let r = displayColor[0];\n let g = displayColor[1];\n let b = displayColor[2];\n let a = displayColor[3];\n // `baseColorRender`, NOT `baseColor` — the base colour as the target Godot backend\n // uploads it (see `godot-renderer.ts`). The multiply order is Godot's, and Godot's is\n // internally inconsistent: `color_value` is linearized on the CPU before the UBO write,\n // while `color_ramp`/`color_initial_ramp` carry no `source_color` hint\n // (`particle_process_material.cpp:313-319`) so they are sampled raw and multiplied in\n // afterwards (`:626-627`, `:595-597`). The two factors are in different colour spaces in\n // the engine, so they must be here too: linearize the base, then multiply the ramp — not\n // the other way round, and never the ramp itself.\n r *= cfg.baseColorRender[0];\n g *= cfg.baseColorRender[1];\n b *= cfg.baseColorRender[2];\n a *= cfg.baseColorRender[3];\n if (cfg.alphaCurve && cfg.alphaCurve.length > 0) {\n a *= sampleParticleCurve(cfg.alphaCurve, tv);\n }\n const hueMag =\n lerp(cfg.hueVariationMin, cfg.hueVariationMax, p.hueRand) *\n (cfg.hueCurve && cfg.hueCurve.length > 0\n ? sampleParticleCurve(cfg.hueCurve, tv)\n : 1);\n if (hueMag !== 0) {\n hueRotateInto(p, r, g, b, hueMag * TAU);\n r = p.r;\n g = p.g;\n b = p.b;\n }\n p.r = r * p.startColor[0];\n p.g = g * p.startColor[1];\n p.b = b * p.startColor[2];\n p.a = a * p.startColor[3];\n\n // The frame INDEX is chosen over `frameCount` (the authored frame total, which can be fewer\n // than the grid holds) and then WRAPPED into the grid's cells — Godot's\n // `mod(progress, hframes * vframes)`. A fully-packed sheet makes the two equal, i.e. a plain\n // grid index.\n const cells = cfg.hframes * cfg.vframes;\n const total = cfg.frameCount && cfg.frameCount > 0 ? cfg.frameCount : cells;\n if (cells > 1) {\n const animSpeed = lerp(\n cfg.animSpeedMin,\n cfg.animSpeedMax,\n p.animOffsetRand,\n );\n const animOffset = lerp(\n cfg.animOffsetMin,\n cfg.animOffsetMax,\n p.animOffsetRand,\n );\n const phase = animOffset + tv * animSpeed;\n const f = cfg.animLoop ? phase - Math.floor(phase) : clamp01(phase);\n p.frame = Math.min(total - 1, Math.max(0, Math.floor(f * total))) % cells;\n } else {\n p.frame = 0;\n }\n}\n\n// Integrate one alive particle by `dt` (cpu_particles_2d.cpp:944-1122). Forces use a\n// per-particle seed RE-READ each frame, so each particle's accel/damp/orbit/angular\n// magnitudes are fixed-but-distinct across its life (fresh randoms => unnatural jitter).\nfunction integrate(cfg: ParticleConfig, p: Particle, dt: number): void {\n p.time += dt;\n if (p.time >= p.lifetime) {\n p.active = false;\n return;\n }\n const tv = p.time / p.lifetime;\n forceRng.seed = p.seed;\n\n let fx = cfg.gravity[0];\n let fy = cfg.gravity[1];\n const speed = Math.hypot(p.vx, p.vy);\n const la = lerp(\n cfg.linearAccelMin,\n cfg.linearAccelMax,\n randFromSeed(forceRng),\n );\n if (speed > 0 && la !== 0) {\n fx += (p.vx / speed) * la;\n fy += (p.vy / speed) * la;\n }\n const dlen = Math.hypot(p.x, p.y);\n const ra = lerp(\n cfg.radialAccelMin,\n cfg.radialAccelMax,\n randFromSeed(forceRng),\n );\n if (dlen > 0 && ra !== 0) {\n fx += (p.x / dlen) * ra;\n fy += (p.y / dlen) * ra;\n }\n const ta = lerp(\n cfg.tangentialAccelMin,\n cfg.tangentialAccelMax,\n randFromSeed(forceRng),\n );\n if (dlen > 0 && ta !== 0) {\n fx += (-p.y / dlen) * ta;\n fy += (p.x / dlen) * ta;\n }\n p.vx += fx * dt;\n p.vy += fy * dt;\n\n const orbit = lerp(\n cfg.orbitVelocityMin,\n cfg.orbitVelocityMax,\n randFromSeed(forceRng),\n );\n if (orbit !== 0) {\n const a = -orbit * dt * TAU;\n const cos = Math.cos(a);\n const sin = Math.sin(a);\n const nx = p.x * cos - p.y * sin;\n const ny = p.x * sin + p.y * cos;\n p.x = nx;\n p.y = ny;\n }\n\n const damp = lerp(cfg.dampingMin, cfg.dampingMax, randFromSeed(forceRng));\n if (damp > 0) {\n const cur = Math.hypot(p.vx, p.vy);\n if (cur > 0) {\n const dec = cfg.dampingAsFriction ? cur * damp * 0.05 * dt : damp * dt;\n const v = Math.max(0, cur - dec);\n p.vx = (p.vx / cur) * v;\n p.vy = (p.vy / cur) * v;\n }\n }\n\n const av = lerp(\n cfg.angularVelocityMin,\n cfg.angularVelocityMax,\n randFromSeed(forceRng),\n );\n p.rotation =\n (lerp(cfg.angleMin, cfg.angleMax, p.angleRand) + p.time * av) * DEG2RAD;\n\n p.x += p.vx * dt;\n p.y += p.vy * dt;\n\n if (cfg.alignY) {\n const sp = Math.hypot(p.vx, p.vy);\n if (sp > 0) p.rotation = Math.atan2(p.vy, p.vx) + Math.PI / 2;\n }\n\n updateDisplay(cfg, p, tv);\n}\n\n// One fixed sub-step: advance the per-cycle clock (wrapping + incrementing `cycle`,\n// and clearing `emitting` when a one-shot completes its cycle), (re)start any slot\n// whose birth phase the clock crossed this step, then integrate all alive particles.\n// Crossing detection mirrors cpu_particles_2d.cpp:789-833 (handles the wrap interval).\nfunction step(state: ParticleSystemState, dt: number): void {\n const cfg = state.config;\n const lifetime = cfg.lifetime;\n const prevTime = state.time;\n let time = prevTime + dt;\n if (time >= lifetime) {\n const cycles = Math.floor(time / lifetime);\n state.cycle += cycles;\n time -= cycles * lifetime;\n if (cfg.oneShot) {\n state.emitting = false;\n }\n }\n state.time = time;\n const count = state.count;\n for (let i = 0; i < count; i += 1) {\n const p = state.particles[i];\n if (state.emitting) {\n const restartTime = restartPhase(cfg, i) * lifetime;\n const restart =\n time > prevTime\n ? restartTime >= prevTime && restartTime < time\n : restartTime >= prevTime || restartTime < time;\n if (restart) {\n restartParticle(state, i, state.cycle);\n }\n }\n if (p.active) {\n integrate(cfg, p, dt);\n }\n }\n}\n\n/**\n * Advance the system by `dt` seconds (real time), stepping the simulation in fixed\n * `1/fixed_fps` (or 1/30) chunks so the look is frame-rate independent. Mutates\n * `state` in place. `maxSteps` bounds the loop (warm-up / tab-switch spikes).\n *\n * Returns the number of fixed sub-steps it actually executed — the unit of work this function\n * does, and the only honest denominator for its cost: one display frame can run zero steps (a\n * fast display under a 30Hz `fixed_fps`, or `speed_scale: 0`) or many (a long dt, a warm-up), so\n * a profiler that divided wall-clock by FRAMES would be measuring the display, not the sim (see\n * `ParticleProfile.simSteps` in `./runtime`). Purely additive: every caller may ignore it, and\n * this function stays pure of any clock.\n */\nexport function simulateParticles(\n state: ParticleSystemState,\n dt: number,\n maxSteps = 1000,\n): number {\n if (!(dt > 0)) return 0;\n const cfg = state.config;\n const frameTime = cfg.fixedFps > 0 ? 1 / cfg.fixedFps : 1 / 30;\n state.remainder += dt * cfg.speedScale;\n let steps = 0;\n while (state.remainder >= frameTime && steps < maxSteps) {\n step(state, frameTime);\n state.remainder -= frameTime;\n steps += 1;\n }\n return steps;\n}\n\n/**\n * Warm-start a freshly created system by its `preprocess` time (Godot pre-simulates\n * that much before first draw, so a long-lived ambient — fog with preprocess=100 —\n * appears mid-drift instead of empty/bursty). A REPEATING system reaches its steady\n * state within two lifetime cycles (a particle's look depends on its age, not absolute\n * time), so simulating `min(preprocess, 2 x lifetime)` is visually identical to the\n * full preprocess at bounded cost; a one-shot's whole life fits in that window too.\n * Steps are sized to cover the window (the default `maxSteps` caps at ~33s), and the\n * sub-step remainder is dropped so the leftover doesn't fast-forward the first live\n * frames at ~1000 steps per tick.\n */\nexport function preprocessParticles(state: ParticleSystemState): void {\n const cfg = state.config;\n if (cfg.preprocess <= 0) return;\n const warm = Math.min(cfg.preprocess, cfg.lifetime * 2);\n const stepHz = cfg.fixedFps > 0 ? cfg.fixedFps : 30;\n simulateParticles(state, warm, Math.ceil(warm * stepHz) + 2);\n state.remainder = 0;\n}\n\n// Fraction of a lifetime a NON-preprocessed system is advanced to for a static/frozen frame, so the frozen\n// spray shows particles in flight rather than an empty t≈0 (all slots born but not yet moved). A one-shot burst\n// looks best caught mid-flight (before its particles die); a continuous emitter reaches a full spread within one\n// lifetime (every slot has emitted once). Cheap to retune.\nconst STATIC_WARM_ONESHOT_FRACTION = 0.4;\nconst STATIC_WARM_REPEAT_FRACTION = 1;\n\n/**\n * Warm a system to a representative FROZEN state for the runtime's static/particles mode. Reuses the authored\n * `preprocess` (an ambient emitter with preprocess>0 reaches its steady drift — identical to `preprocessParticles`),\n * and for a system that would otherwise sit at spawn (preprocess<=0, e.g. a one-shot burst or an un-preprocessed\n * emitter) advances a representative slice of a lifetime so the frozen frame is populated. Mutates `state`; the\n * caller draws once and then stops simulating (see the particle runtime's static mode).\n *\n * This warm has no notion of the burst ENDING — it is a single representative frame, so a one-shot warmed here\n * would otherwise be drawn as mid-flight forever. `staticOneShotExpired` is what retires it.\n */\nexport function warmStaticParticles(state: ParticleSystemState): void {\n const cfg = state.config;\n if (cfg.preprocess > 0) {\n preprocessParticles(state);\n return;\n }\n const warm =\n cfg.lifetime *\n (cfg.oneShot ? STATIC_WARM_ONESHOT_FRACTION : STATIC_WARM_REPEAT_FRACTION);\n const stepHz = cfg.fixedFps > 0 ? cfg.fixedFps : 30;\n simulateParticles(state, warm, Math.ceil(warm * stepHz) + 2);\n // Drop the sub-step remainder so the frozen state is clean (nothing left to fast-forward if sim resumes).\n state.remainder = 0;\n}\n\n/**\n * Godot's own ACTIVE WINDOW for one one-shot cycle, in seconds: `lifetime * (2 - explosiveness)`\n * (particles.cpp `active_time`). At explosiveness 1 every particle is born at t=0, so the cycle is one\n * lifetime; at 0 the births are spread over a full lifetime, so the last particle dies at 2x lifetime.\n *\n * This is the SAME law the game-side mod uses to schedule a frozen one-shot's synthesized end-of-burst\n * (`CouchCoopHeadlessVisualSuspender.FinishNudgeDelaySeconds`), deliberately: the two sides have to agree on\n * when a burst is over, or one of them keeps drawing/reporting it after the other has stopped. No clamp and no\n * margin here — the mod's margin exists so its removal delta lands AFTER the client's tail, and this side IS\n * that tail. `normalizeParticleConfig` already guarantees a finite `lifetime >= 0.01` and `explosiveness` in\n * [0,1], so the result is finite and positive.\n *\n * `speedScale` is deliberately NOT folded in, for the same reason: the mod's law does not either, and a\n * disagreement would be worse than the (rare, small) inaccuracy of a re-timed burst.\n */\nexport function oneShotBurstSeconds(cfg: ParticleConfig): number {\n return cfg.lifetime * (2 - cfg.explosiveness);\n}\n\n/**\n * FROZEN-MODE expiry decision: has a one-shot burst the client has been drawing statically outlived its own\n * active window, so the runtime should stop drawing it? Pure (no clock, no DOM) — the caller supplies the\n * seconds elapsed since IT first saw this system emitting.\n *\n * WHY THIS EXISTS. In frozen/static mode a system is warmed to a representative mid-flight frame and that frame\n * is parked forever — which is right for an ambient emitter (it really does emit forever) and wrong for a\n * one-shot (it is a BURST; it ends). Nothing else can retire it: the frozen runtime never simulates, so the\n * sim's own end-of-cycle never runs, and the only other input is the host's `emitting` flag — which a host can\n * get stuck on (the live case: a game-side freeze left `Emitting` latched true on every energy-counter VFX, so\n * the mirror drew a permanent \"energy ring\" over a counter the game was showing bare).\n *\n * WHY \"since FIRST SIGHT\". The client cannot know when the game started the burst — it sees only \"this spec\n * says emitting\". One full active window from first sight is exactly what the burst itself would do, so a\n * legitimate transient (a hit spark, a card-play flourish) still shows for its natural life; only a burst that\n * outlives its own window — i.e. one nothing ever turned off — is dropped.\n */\nexport function staticOneShotExpired(\n cfg: ParticleConfig,\n secondsSinceFirstEmitting: number,\n): boolean {\n if (!cfg.oneShot || !cfg.emitting) return false;\n return secondsSinceFirstEmitting >= oneShotBurstSeconds(cfg);\n}\n\n/** Live particle count (for tests / draw). */\nexport function activeParticleCount(state: ParticleSystemState): number {\n let n = 0;\n for (const p of state.particles) {\n if (p.active) n += 1;\n }\n return n;\n}\n","// Pure, DOM-free particle config + state. This is the seam the deterministic\n// `simulateParticles` (in `./simulate`) mutates, and the boundary the live runtime\n// (`./runtime`) and the unit tests share. No WebGL, no DOM — so the whole simulation\n// is testable in plain node / jsdom (which has no GL).\n\nimport type { ParticleConfig, ParticleRenderConfig } from \"./config\";\nimport {\n linearizeParticleBaseColor,\n normalizeGodotRenderer,\n} from \"./godot-renderer\";\nimport { normalizeParticleCurve } from \"./sampling\";\n\nexport type { ParticleConfig } from \"./config\";\n\n// One simulated particle. Spawn-time randoms are stored so per-frame display\n// (rotation/scale/color/flipbook) stays consistent; `seed` re-seeds the per-frame\n// force RNG each step so each particle's accel/damp/orbit magnitude is fixed-but-\n// distinct (mirroring Godot's per-particle force seed).\nexport interface Particle {\n active: boolean;\n /** Age in seconds. */\n time: number;\n /** This particle's randomized lifetime (seconds). */\n lifetime: number;\n /** Position in node-local pixels. */\n x: number;\n y: number;\n /** Velocity in px/s. */\n vx: number;\n vy: number;\n /** Rotation in radians. */\n rotation: number;\n /** Per-frame force RNG seed (stable per particle). */\n seed: number;\n angleRand: number;\n scaleRand: number;\n hueRand: number;\n animOffsetRand: number;\n /** color_initial_ramp sampled once at spawn (or white). */\n startColor: [number, number, number, number];\n // ---- per-frame display outputs (read by the renderer) ----\n scaleX: number;\n scaleY: number;\n r: number;\n g: number;\n b: number;\n a: number;\n /** Flipbook frame index. */\n frame: number;\n}\n\nexport interface ParticleSystemState {\n config: ParticleConfig;\n particles: Particle[];\n /** System time within the current cycle, wrapped to [0, lifetime). */\n time: number;\n /** Number of completed lifetime cycles (drives spawn timing + one-shot end). */\n cycle: number;\n /** Live emit flag — starts at `config.emitting`, cleared after a one-shot cycle. */\n emitting: boolean;\n /** Fixed-step remainder accumulator. */\n remainder: number;\n /** Effective particle count after the maxInstances clamp. */\n count: number;\n}\n\nfunction makeParticle(): Particle {\n return {\n active: false,\n time: 0,\n lifetime: 1,\n x: 0,\n y: 0,\n vx: 0,\n vy: 0,\n rotation: 0,\n seed: 0,\n angleRand: 0,\n scaleRand: 0,\n hueRand: 0,\n animOffsetRand: 0,\n startColor: [1, 1, 1, 1],\n scaleX: 1,\n scaleY: 1,\n r: 1,\n g: 1,\n b: 1,\n a: 1,\n frame: 0,\n };\n}\n\nconst DEFAULT_MAX_INSTANCES = 2048;\n\nexport function createParticleState(\n config: ParticleConfig,\n maxInstances = DEFAULT_MAX_INSTANCES,\n): ParticleSystemState {\n const count = Math.max(1, Math.min(maxInstances, Math.round(config.amount)));\n const particles: Particle[] = [];\n for (let i = 0; i < count; i += 1) {\n particles.push(makeParticle());\n }\n return {\n config,\n particles,\n time: 0,\n cycle: 0,\n emitting: config.emitting,\n remainder: 0,\n count,\n };\n}\n\n/** Whether the system still needs simulating (live particles or still emitting). */\nexport function particlesAreLive(state: ParticleSystemState): boolean {\n if (state.emitting) return true;\n for (const p of state.particles) {\n if (p.active) return true;\n }\n return false;\n}\n\nfunction num(value: unknown, fallback: number): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nfunction bool(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback;\n}\n\nfunction vec2(value: unknown, fallback: [number, number]): [number, number] {\n return Array.isArray(value) &&\n value.length >= 2 &&\n typeof value[0] === \"number\" &&\n typeof value[1] === \"number\"\n ? [value[0], value[1]]\n : fallback;\n}\n\nfunction rgba(\n value: unknown,\n fallback: [number, number, number, number],\n): [number, number, number, number] {\n return Array.isArray(value) && value.length >= 4\n ? [\n num(value[0], fallback[0]),\n num(value[1], fallback[1]),\n num(value[2], fallback[2]),\n num(value[3], fallback[3]),\n ]\n : fallback;\n}\n\n// Normalize a (possibly partial / hand-authored) parsed spec into a full config with\n// Godot defaults. The build-time serializer writes every field, so this mostly fills\n// defensive defaults — but it keeps the runtime robust to missing keys.\nexport function normalizeParticleConfig(\n raw: Partial<ParticleConfig> | null | undefined,\n): ParticleConfig {\n const c = raw ?? {};\n const kind =\n c.kind === \"CPUParticles2D\" ? \"CPUParticles2D\" : \"GPUParticles2D\";\n const godotRenderer = normalizeGodotRenderer(c.godotRenderer);\n const baseColor = rgba(c.baseColor, [1, 1, 1, 1]);\n // A hand-authored blob that omits the flag is read as \"this is the process-material\n // colour\" for GPU particles (the only thing a `ParticleProcessMaterial` can be) and as\n // \"node colour\" for CPU ones. `visual-2d.ts` always states it explicitly.\n const baseColorFromProcessMaterial =\n typeof c.baseColorFromProcessMaterial === \"boolean\"\n ? c.baseColorFromProcessMaterial\n : kind === \"GPUParticles2D\";\n return {\n kind,\n godotRenderer,\n amount: Math.max(1, Math.round(num(c.amount, 8))),\n amountRatio: clamp01(num(c.amountRatio, 1)),\n lifetime: Math.max(0.01, num(c.lifetime, 1)),\n lifetimeRandomness: clamp01(num(c.lifetimeRandomness, 0)),\n oneShot: bool(c.oneShot, false),\n emitting: bool(c.emitting, true),\n explosiveness: clamp01(num(c.explosiveness, 0)),\n randomness: clamp01(num(c.randomness, 0)),\n preprocess: Math.max(0, num(c.preprocess, 0)),\n speedScale: num(c.speedScale, 1),\n fixedFps: Math.max(0, Math.round(num(c.fixedFps, 0))),\n localCoords: bool(c.localCoords, false),\n drawOrder: Math.round(num(c.drawOrder, 0)),\n seed: Math.round(num(c.seed, 0)),\n emissionShape: Math.round(num(c.emissionShape, 0)),\n emissionOffset: vec2(c.emissionOffset, [0, 0]),\n emissionScale: vec2(c.emissionScale, [1, 1]),\n emissionSphereRadius: num(c.emissionSphereRadius, 0),\n emissionRingRadius: num(c.emissionRingRadius, 0),\n emissionRingInnerRadius: num(c.emissionRingInnerRadius, 0),\n emissionRingHeight: num(c.emissionRingHeight, 0),\n emissionBoxExtents: vec2(c.emissionBoxExtents, [0, 0]),\n direction: vec2(c.direction, [1, 0]),\n spread: num(c.spread, 45),\n initialVelocityMin: num(c.initialVelocityMin, 0),\n initialVelocityMax: num(c.initialVelocityMax, 0),\n angleMin: num(c.angleMin, 0),\n angleMax: num(c.angleMax, 0),\n angularVelocityMin: num(c.angularVelocityMin, 0),\n angularVelocityMax: num(c.angularVelocityMax, 0),\n gravity: vec2(c.gravity, [0, 980]),\n linearAccelMin: num(c.linearAccelMin, 0),\n linearAccelMax: num(c.linearAccelMax, 0),\n radialAccelMin: num(c.radialAccelMin, 0),\n radialAccelMax: num(c.radialAccelMax, 0),\n tangentialAccelMin: num(c.tangentialAccelMin, 0),\n tangentialAccelMax: num(c.tangentialAccelMax, 0),\n dampingMin: num(c.dampingMin, 0),\n dampingMax: num(c.dampingMax, 0),\n dampingAsFriction: bool(c.dampingAsFriction, false),\n orbitVelocityMin: num(c.orbitVelocityMin, 0),\n orbitVelocityMax: num(c.orbitVelocityMax, 0),\n scaleMin: num(c.scaleMin, 1),\n scaleMax: num(c.scaleMax, 1),\n hueVariationMin: num(c.hueVariationMin, 0),\n hueVariationMax: num(c.hueVariationMax, 0),\n alignY: bool(c.alignY, false),\n baseColor,\n baseColorFromProcessMaterial,\n // THE colour-space seam. `baseColor` above stays the raw Godot inspector value; this is\n // it as `godotRenderer` uploads it, RGB through `Color::srgb_to_linear()` on the\n // RendererRD backends and untouched on Compatibility. Derived HERE, once per config,\n // rather than in `updateDisplay` — so it lands BEFORE the ramp multiply (matching\n // Godot's own composition order, see `simulate.ts`), costs nothing per frame, and is\n // inherited identically by both backends, which read the colour from the shared\n // per-instance attribute (`instance-buffer.ts`) rather than a per-backend uniform.\n baseColorRender: linearizeParticleBaseColor(\n baseColor,\n godotRenderer,\n baseColorFromProcessMaterial,\n ),\n hframes: Math.max(1, Math.round(num(c.hframes, 1))),\n vframes: Math.max(1, Math.round(num(c.vframes, 1))),\n frameCount: Math.max(0, Math.round(num(c.frameCount, 0))),\n animLoop: bool(c.animLoop, false),\n animSpeedMin: num(c.animSpeedMin, 0),\n animSpeedMax: num(c.animSpeedMax, 0),\n animOffsetMin: num(c.animOffsetMin, 0),\n animOffsetMax: num(c.animOffsetMax, 0),\n colorRamp: c.colorRamp,\n colorInitialRamp: c.colorInitialRamp,\n scaleCurve: normalizeParticleCurve(c.scaleCurve),\n scaleCurveX: normalizeParticleCurve(c.scaleCurveX),\n scaleCurveY: normalizeParticleCurve(c.scaleCurveY),\n alphaCurve: normalizeParticleCurve(c.alphaCurve),\n hueCurve: normalizeParticleCurve(c.hueCurve),\n };\n}\n\n/** Normalize portable renderer inputs without admitting HTML URL or placement fields. */\nexport function normalizeParticleRenderConfig(\n raw: Partial<ParticleRenderConfig> | null | undefined,\n): ParticleRenderConfig {\n const value = raw ?? {};\n const erode = value.alphaErode;\n const alphaErode =\n erode && Number.isFinite(erode.threshold) && Number.isFinite(erode.softness)\n ? { threshold: erode.threshold, softness: Math.max(0, erode.softness) }\n : null;\n return {\n ...normalizeParticleConfig(value),\n textureWidth: Math.max(0, num(value.textureWidth, 0)),\n textureHeight: Math.max(0, num(value.textureHeight, 0)),\n flipbookCropOnly: value.flipbookCropOnly === true,\n blendMode: Math.round(num(value.blendMode, 0)),\n colorLut: value.colorLut,\n colorLutInterpolation: value.colorLutInterpolation,\n alphaFromRed: value.alphaFromRed === true,\n alphaErode,\n uvPolar: value.uvPolar === true,\n };\n}\n\nfunction clamp01(value: number): number {\n return value < 0 ? 0 : value > 1 ? 1 : value;\n}\n\n// `alphaErode` → a finite {threshold, softness} pair, or null (no erosion). A softness of 0 is a HARD step,\n// which is what Godot's smoothstep(x, x, v) degenerates to, so it is kept as authored.\n"],"mappings":"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"}
@@ -0,0 +1,255 @@
1
+ //#region src/shaders/godot-shader.d.ts
2
+ declare class UnsupportedShaderError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ type GodotBlendMode = "mix" | "add" | "sub" | "mul" | "premul_alpha";
6
+ interface ShaderUniform {
7
+ /** Uniform name exactly as declared (matches `shader_parameter/<name>`). */
8
+ name: string;
9
+ /** GLSL type: float | int | bool | vec2 | vec3 | vec4 | mat2..4. */
10
+ type: string;
11
+ /** Array length for uniforms declared as `foo[N]`, if any. */
12
+ arrayLength?: number;
13
+ /** Parsed default from `= <literal>` (number, or component array), if any. */
14
+ default?: number | number[];
15
+ }
16
+ interface ShaderSampler {
17
+ /** Sampler uniform name (matches `shader_parameter/<name>`). */
18
+ name: string;
19
+ /** `: repeat_enable` hint -> the runtime sets wrap REPEAT (else CLAMP). */
20
+ repeat: boolean;
21
+ }
22
+ interface ShaderVarying {
23
+ /** GLSL type as declared (`vec4`, `float`, …) — Godot's spelling, not an emitter's. */
24
+ type: string;
25
+ name: string;
26
+ }
27
+ interface TranspiledShader {
28
+ vertexGlsl: string;
29
+ fragmentGlsl: string;
30
+ /** User scalar/vector uniforms (built-ins + samplers excluded), in order. */
31
+ uniforms: ShaderUniform[];
32
+ /** User `sampler2D` uniforms; the runtime binds a texture per entry. */
33
+ samplers: ShaderSampler[];
34
+ blend: GodotBlendMode;
35
+ /** The shader reads `TIME` -> the runtime must drive it from a clock (rAF). */
36
+ usesTime: boolean;
37
+ /** The shader reads `TEXTURE_PIXEL_SIZE` -> runtime supplies 1/textureSize. */
38
+ usesTexturePixelSize: boolean;
39
+ /** The shader reads `SCREEN_UV` -> runtime supplies the node's viewport rect. */
40
+ usesScreenUv: boolean;
41
+ /** The shader samples `SCREEN_TEXTURE` (the built-in token, or a declared
42
+ * `hint_screen_texture` sampler) -> the runtime must supply a screen capture.
43
+ * Only renderable when the runtime opts in (`enableScreenTextureCapture`). */
44
+ usesScreenTexture: boolean;
45
+ /** The shader reads `SCREEN_PIXEL_SIZE` -> runtime supplies 1/captureSize. */
46
+ usesScreenPixelSize: boolean;
47
+ }
48
+ declare function expandGodotShaderIncludes(source: string, resolveInclude: (path: string) => Promise<string | undefined> | string | undefined, seen?: Set<string>, depth?: number): Promise<string>;
49
+ /** The parsing front-end's output: Godot text in, emitter-neutral pieces out. Shared
50
+ * by the GLSL emitter below and by any sibling emitter (the WGSL one) — parsing a
51
+ * `.gdshader` twice, once per target language, is how the two would drift apart. */
52
+ interface ParsedShader {
53
+ blend: GodotBlendMode;
54
+ uniforms: ShaderUniform[];
55
+ samplers: ShaderSampler[];
56
+ /** Names of `hint_screen_texture` sampler uniforms (conventionally
57
+ * `SCREEN_TEXTURE`). Excluded from `samplers` — they bind the runtime's
58
+ * screen capture, not a user texture. */
59
+ screenTextureNames: string[];
60
+ varyings: ShaderVarying[];
61
+ vertexBody: string | null;
62
+ fragmentBody: string;
63
+ /** Top-level helper functions and consts (everything left after the directives,
64
+ * uniforms, varyings, `vertex()` and `fragment()` are removed), verbatim. */
65
+ helpers: string;
66
+ /** Identifiers statically declared `int` (uniforms + locals). Bare integer
67
+ * literals compared against these must NOT be promoted to float. */
68
+ intIdentifiers: Set<string>;
69
+ }
70
+ /** One vertex-computed varying, hoisted into the fragment as a local because our
71
+ * fullscreen quad makes it constant. Emitter-neutral: `type`/`name` are Godot's
72
+ * spelling and `expr` is the vertex RHS with the vertex `COLOR` (the node's combined
73
+ * modulate·self_modulate) already substituted by `MODULATE`. Each emitter formats
74
+ * its own declaration syntax around these three fields. */
75
+ interface ShaderVaryingHoist {
76
+ type: string;
77
+ name: string;
78
+ expr: string;
79
+ }
80
+ /** Everything an emitter must know about a `ParsedShader` beyond its text: which
81
+ * runtime-supplied built-ins to declare, how the node MODULATE has to be applied,
82
+ * whether COLOR seeds opaque, and the varyings to hoist. Derived once by
83
+ * `analyzeShader` so the GLSL and WGSL emitters cannot drift on the MODULATE /
84
+ * opaque-fill rules, which encode measured Godot behavior rather than a preference. */
85
+ interface ShaderAnalysis {
86
+ /** The concatenated scan region the flags were derived from (helpers + vertex +
87
+ * fragment). Hand this to `rejectUnsupported` — it must see the same text. */
88
+ logic: string;
89
+ /** Reads `TIME` -> the runtime must drive it from a clock (rAF). */
90
+ usesTime: boolean;
91
+ /** Reads `TEXTURE_PIXEL_SIZE` -> runtime supplies 1/textureSize. */
92
+ usesTexturePixelSize: boolean;
93
+ /** Reads `SCREEN_UV` -> runtime supplies the node's viewport rect. */
94
+ usesScreenUv: boolean;
95
+ /** Samples `SCREEN_TEXTURE` (built-in token or a `hint_screen_texture` sampler). */
96
+ usesScreenTexture: boolean;
97
+ /** Reads `SCREEN_PIXEL_SIZE` -> runtime supplies 1/captureSize. */
98
+ usesScreenPixelSize: boolean;
99
+ /** Reads `PI` -> the emitter must define the constant (Godot has it built in). */
100
+ usesPi: boolean;
101
+ /** A MODULATE uniform must be declared: the body names it, the engine multiply is
102
+ * synthesized, or a hoisted varying was seeded from it. */
103
+ needsModulate: boolean;
104
+ /** Append the engine's `COLOR *= MODULATE` (see `analyzeShader` for when not to). */
105
+ autoModulate: boolean;
106
+ /** Seed `COLOR.a = 1` instead of the sampled texture alpha (pure-fill shaders). */
107
+ opaqueColor: boolean;
108
+ varyingHoists: ShaderVaryingHoist[];
109
+ }
110
+ /** Transpile a `.gdshader` source string. Throws `UnsupportedShaderError`. */
111
+ declare function transpileGodotShader(source: string): TranspiledShader;
112
+ /** The text every built-in flag is scanned over. The vertex body and the top-level
113
+ * helpers count, not just `fragment()`: a helper may read TIME/PI, and a varying
114
+ * computed in `vertex()` is hoisted into the fragment. `rejectUnsupported` must be
115
+ * given this same region, or a guard would scan less text than the flags did. */
116
+ declare function shaderLogic(parsed: ParsedShader): string;
117
+ /** Derive the emitter-neutral facts about a parsed shader (see `ShaderAnalysis`).
118
+ * Pure, and deliberately NOT a validator: run `rejectUnsupported(analysis.logic,
119
+ * parsed)` for that, before analyzing, so an unsupported built-in outranks a
120
+ * varying-hoist complaint. Throws `UnsupportedShaderError` only for a `vertex()`
121
+ * body that cannot be reduced to constant varying assignments. */
122
+ declare function analyzeShader(parsed: ParsedShader): ShaderAnalysis;
123
+ /** Parse cleaned Godot source (run `stripComments` + `sanitizeReservedIdentifiers`
124
+ * first) into the emitter-neutral `ParsedShader`. Throws `UnsupportedShaderError`. */
125
+ declare function parseShader(src: string): ParsedShader;
126
+ /** Throw `UnsupportedShaderError` for constructs no emitter supports. Give it
127
+ * `shaderLogic(parsed)` — the same region the analysis flags are scanned over. */
128
+ declare function rejectUnsupported(logic: string, parsed: ParsedShader): void;
129
+ /**
130
+ * The shader source inside a Godot text-resource container, or `source` unchanged.
131
+ *
132
+ * Throws {@link UnsupportedShaderError} for a container with no usable `code` property —
133
+ * NEVER returns the container. That refusal is the whole point: before it existed, every
134
+ * parse step happened to succeed against a `.tres` (the `shader_type`, the `uniform`
135
+ * declarations and the `fragment()` body all match INSIDE the escaped `code` string), and
136
+ * then the subtractive `helpers` residual in `parseShader` carried the entire container
137
+ * into the emitted GLSL — declarations, then `[gd_resource type="VisualShader" …` as the
138
+ * first line of what should have been shader code. The driver reported a syntax error at
139
+ * a `[`, which is a long way from "this file is not a shader".
140
+ */
141
+ declare function unwrapShaderResource(source: string): string;
142
+ declare function stripComments(src: string): string;
143
+ declare function sanitizeReservedIdentifiers(src: string): string;
144
+ declare function extractFunction(src: string, name: string): string | null;
145
+ /** Index of the `}` closing the `{` at `openIndex`. Brace depth only — enough for the
146
+ * supported subset, which has no braces inside strings (Godot shaders have none). */
147
+ declare function matchBrace(src: string, openIndex: number): number;
148
+ declare function promoteIntLiterals(src: string, intIdentifiers?: Iterable<string>): string;
149
+ /** Whole-identifier match: `TIME` must not fire on `LIFETIME`, `COLOR` not on
150
+ * `COLOR_KEY`. Every built-in probe in this file goes through it. */
151
+ declare function hasToken(src: string, token: string): boolean;
152
+ /** Whole-identifier replace, same boundary rule as `hasToken`. */
153
+ declare function replaceToken(src: string, token: string, replacement: string): string;
154
+ //#endregion
155
+ //#region src/shaders/transpile-wgsl.d.ts
156
+ /** "WGSL can't, WebGL can." Thrown for shapes the GLSL emitter renders happily; the
157
+ * runtime answers it with a per-binding WebGL fallback, not with the CSS fallback. */
158
+ declare class UnsupportedWgslShaderError extends UnsupportedShaderError {
159
+ constructor(message: string);
160
+ }
161
+ /** One member of the single uniform struct, with the byte offset a writer needs. */
162
+ interface WgslUniformField {
163
+ /** Uniform name exactly as Godot declared it (matches `shader_parameter/<name>`).
164
+ * NOT necessarily the emitted WGSL member name — a Godot name that collides with a
165
+ * WGSL reserved word or with a built-in member is renamed inside the module only. */
166
+ name: string;
167
+ /** WGSL type of the member (`f32`, `i32`, `vec3f`, `vec2i`, `mat3x3f`, …). */
168
+ type: string;
169
+ /** Godot's own spelling (`float`, `bool`, `vec3`, …). A `bool` uniform is stored as
170
+ * `f32` (WGSL `bool` is not host-shareable), so a writer needs this to know that an
171
+ * `f32` member is really a 1/0 flag. */
172
+ godotType: string;
173
+ /** Element count for `foo[N]` uniforms. Elements are 16 bytes apart (WGSL uniform
174
+ * address space rounds array stride up to 16). */
175
+ arrayLength?: number;
176
+ offsetBytes: number;
177
+ sizeBytes: number;
178
+ default?: number | number[];
179
+ }
180
+ /** Byte offsets of the runtime-supplied built-ins inside the SAME uniform struct.
181
+ * `uvFit`/`uvWindow` are always present; the rest exist only when the shader reads
182
+ * the corresponding built-in (`usesTime`, `usesTexturePixelSize`, MODULATE, SCREEN_UV). */
183
+ interface WgslBuiltinOffsets {
184
+ uvFit: number;
185
+ uvWindow: number;
186
+ time?: number;
187
+ texturePixelSize?: number;
188
+ modulate?: number;
189
+ screenOrigin?: number;
190
+ screenSize?: number;
191
+ }
192
+ /** The transpiler-owned `@group(0)` binding table. User sampler `i` (index into
193
+ * `samplers`) occupies TWO bindings: `userSamplersBase + 2*i` for the texture and
194
+ * `userSamplersBase + 2*i + 1` for its sampler. */
195
+ interface WgslBindings {
196
+ uniform: 0;
197
+ texture: 1;
198
+ textureSampler: 2;
199
+ userSamplersBase: 3;
200
+ }
201
+ interface TranspiledWgslShader {
202
+ /** ONE module: `vs_main` + `fs_main` + the polyfills and helpers actually used. */
203
+ wgsl: string;
204
+ vertexEntry: "vs_main";
205
+ fragmentEntry: "fs_main";
206
+ /** Size of the single uniform struct, rounded up to 16. */
207
+ uniformStructSizeBytes: number;
208
+ builtinOffsets: WgslBuiltinOffsets;
209
+ /** User scalar/vector uniforms, offsets into the same struct, declaration order. */
210
+ uniforms: WgslUniformField[];
211
+ /** User `sampler2D` uniforms under their GODOT names, in binding order. */
212
+ samplers: ShaderSampler[];
213
+ bindings: WgslBindings;
214
+ blend: GodotBlendMode;
215
+ usesTime: boolean;
216
+ usesTexturePixelSize: boolean;
217
+ usesScreenUv: boolean;
218
+ }
219
+ /** Transpile a `.gdshader` source string to one WGSL module plus its uniform layout.
220
+ * Throws `UnsupportedShaderError` (no backend can) or `UnsupportedWgslShaderError`
221
+ * (this backend can't — fall the binding back to WebGL). */
222
+ declare function transpileGodotShaderWgsl(source: string): TranspiledWgslShader;
223
+ interface WgslStructField {
224
+ name: string;
225
+ /** WGSL type name (`f32`, `i32`, `vec2f`, `vec3f`, `vec4f`, `vec2i`, `mat3x3f`, …). */
226
+ type: string;
227
+ /** Element count when the member is an array. */
228
+ arrayLength?: number;
229
+ }
230
+ interface WgslStructMember extends WgslStructField {
231
+ offsetBytes: number;
232
+ sizeBytes: number;
233
+ alignBytes: number;
234
+ }
235
+ interface WgslStructLayout {
236
+ members: WgslStructMember[];
237
+ /** Struct size, rounded up to the struct alignment. */
238
+ sizeBytes: number;
239
+ alignBytes: number;
240
+ }
241
+ /** Lay out a WGSL `var<uniform>` struct by the uniform address space rules: members keep
242
+ * DECLARATION ORDER (a reorder would silently move every offset a writer already holds),
243
+ * each is placed at the next multiple of its alignment, and the struct size is rounded up
244
+ * to the struct alignment. Arrays get an element stride of `roundUp(align(E), size(E))`,
245
+ * which the uniform address space additionally requires to be a multiple of 16 — so
246
+ * `array<vec3f, N>` and `array<vec4f, N>` are natively fine and `array<f32, N>` /
247
+ * `array<vec2f, N>` are refused (they would need per-element padding on the host side).
248
+ *
249
+ * These are exactly WGSL's own natural layout rules, applied to the members in the order
250
+ * the module declares them, which is why the emitted struct carries no explicit padding
251
+ * members: the compiler computes the same offsets this function records. */
252
+ declare function wgslStructLayout(fields: readonly WgslStructField[]): WgslStructLayout;
253
+ //#endregion
254
+ export { GodotBlendMode, ParsedShader, ShaderAnalysis, ShaderSampler, ShaderUniform, ShaderVarying, ShaderVaryingHoist, TranspiledShader, TranspiledWgslShader, UnsupportedShaderError, UnsupportedWgslShaderError, WgslBindings, WgslBuiltinOffsets, WgslStructField, WgslStructLayout, WgslStructMember, WgslUniformField, analyzeShader, expandGodotShaderIncludes, extractFunction, hasToken, matchBrace, parseShader, promoteIntLiterals, rejectUnsupported, replaceToken, sanitizeReservedIdentifiers, shaderLogic, stripComments, transpileGodotShader, transpileGodotShaderWgsl, unwrapShaderResource, wgslStructLayout };
255
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/shaders/godot-shader.ts","../../src/shaders/transpile-wgsl.ts"],"mappings":"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"}