@wave3d/core 0.8.0 → 0.9.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.
- package/README.md +3 -1
- package/dist/config/model.d.ts +48 -2
- package/dist/config/model.js +8 -1
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/renderer/WaveRenderer.d.ts +142 -10
- package/dist/renderer/WaveRenderer.js +209 -53
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/WaveRendererGPU.js +291 -0
- package/dist/renderer/WaveRendererGPU.js.map +1 -0
- package/dist/renderer/gpu-loader.js +2 -0
- package/dist/renderer/index.d.ts +3 -2
- package/dist/renderer/interaction.d.ts +29 -0
- package/dist/renderer/interaction.js +77 -31
- package/dist/renderer/interaction.js.map +1 -1
- package/dist/renderer/interactionGates.js +59 -0
- package/dist/renderer/interactionGates.js.map +1 -0
- package/dist/renderer/particleField.d.ts +1 -59
- package/dist/renderer/particleField.js +49 -29
- package/dist/renderer/particleField.js.map +1 -1
- package/dist/renderer/particleFieldGPU.js +141 -0
- package/dist/renderer/particleFieldGPU.js.map +1 -0
- package/dist/renderer/tilt.d.ts +17 -0
- package/dist/renderer/tilt.js +124 -0
- package/dist/renderer/tilt.js.map +1 -0
- package/dist/renderer/tsl/color.js +129 -0
- package/dist/renderer/tsl/color.js.map +1 -0
- package/dist/renderer/tsl/noise.js +83 -0
- package/dist/renderer/tsl/noise.js.map +1 -0
- package/dist/renderer/tsl/packedArray.js +76 -0
- package/dist/renderer/tsl/packedArray.js.map +1 -0
- package/dist/renderer/tsl/particleMaterial.js +133 -0
- package/dist/renderer/tsl/particleMaterial.js.map +1 -0
- package/dist/renderer/tsl/particleUniforms.js +44 -0
- package/dist/renderer/tsl/particleUniforms.js.map +1 -0
- package/dist/renderer/tsl/pointerField.js +73 -0
- package/dist/renderer/tsl/pointerField.js.map +1 -0
- package/dist/renderer/tsl/post.js +63 -0
- package/dist/renderer/tsl/post.js.map +1 -0
- package/dist/renderer/tsl/postChain.js +56 -0
- package/dist/renderer/tsl/postChain.js.map +1 -0
- package/dist/renderer/tsl/postEffects.js +221 -0
- package/dist/renderer/tsl/postEffects.js.map +1 -0
- package/dist/renderer/tsl/types.js +28 -0
- package/dist/renderer/tsl/types.js.map +1 -0
- package/dist/renderer/tsl/uniforms.js +152 -0
- package/dist/renderer/tsl/uniforms.js.map +1 -0
- package/dist/renderer/tsl/waveMaterial.js +185 -0
- package/dist/renderer/tsl/waveMaterial.js.map +1 -0
- package/dist/renderer/tsl/waveShape.js +108 -0
- package/dist/renderer/tsl/waveShape.js.map +1 -0
- package/dist/shell/createWave.d.ts +29 -0
- package/dist/shell/createWave.js +39 -11
- package/dist/shell/createWave.js.map +1 -1
- package/dist/shell/probe.js +18 -1
- package/dist/shell/probe.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +1811 -1594
- package/dist/standalone/wave3d.standalone.webgpu.js +36647 -0
- package/dist/standalone.d.ts +10 -3
- package/dist/standalone.js +10 -3
- package/dist/standalone.js.map +1 -1
- package/dist/studio/StudioWaveRenderer.d.ts +4 -0
- package/dist/studio/StudioWaveRenderer.js +9 -0
- package/dist/studio/StudioWaveRenderer.js.map +1 -1
- package/dist/studio/StudioWaveRendererGPU.js +18 -0
- package/dist/studio/StudioWaveRendererGPU.js.map +1 -0
- package/dist/studio/index.d.ts +12 -2
- package/dist/studio/index.js +14 -1
- package/dist/studio/index.js.map +1 -0
- package/package.json +12 -3
- package/skills/wave3d/SKILL.md +29 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waveMaterial.js","names":["tabs"],"sources":["../../../src/renderer/tsl/waveMaterial.ts"],"sourcesContent":["/**\n * The wave's `NodeMaterial` — the TSL port of `vertexShader` + `fragmentShader` / `lineFragmentShader`.\n *\n * The GLSL `#ifdef` variants become JS flags, so each material builds exactly the graph it needs\n * instead of relying on a define set and a program cache key.\n *\n * Two deliberate differences from the GLSL, both because the node pipeline already does the work:\n * - No `PREMULTIPLIED_ALPHA` block. `NodeMaterial.setupOutput()` calls `setupPremultipliedAlpha()`\n * when `material.premultipliedAlpha` is set, so premultiplying here would double-apply it.\n * - No output colour-space conversion. The material writes LINEAR, exactly as the GLSL does; the\n * conversion happens once at the end of the post chain, matching `OutputPass` on the WebGL path.\n */\nimport { NodeMaterial } from \"three/webgpu\";\nimport {\n Fn,\n varying,\n vec2,\n vec3,\n vec4,\n float,\n uv,\n positionLocal,\n positionWorld,\n cameraPosition,\n cameraProjectionMatrix,\n cameraViewMatrix,\n modelWorldMatrix,\n screenUV,\n dFdx,\n dFdy,\n fwidth,\n abs as tabs,\n sin,\n cos,\n pow,\n max,\n clamp,\n mix,\n smoothstep,\n cross,\n dot,\n normalize,\n radians,\n Loop,\n If,\n Break,\n select,\n} from \"three/tsl\";\nimport { MAX_LIGHTS, MAX_NOISE_BANDS } from \"../../config/model\";\nimport { simplexNoise, grainHash } from \"./noise\";\nimport { waveShape, applyTwist, type WaveShapeFlags } from \"./waveShape\";\nimport { applyColorGrade, waveBaseColor, hueShift, parabola, mapLinear } from \"./color\";\nimport { pointerField } from \"./pointerField\";\nimport type { FloatNode, Vec2Node, Vec3Node } from \"./types\";\nimport type { WaveTslUniforms } from \"./uniforms\";\n\nexport interface WaveMaterialFlags extends WaveShapeFlags {\n theme: \"solid\" | \"wireframe\";\n /** Pointer field: per-wave, config-only, so input never triggers a rebuild. */\n pointerFx: boolean;\n /** Click ripples, which nest inside the pointer field. */\n pointerRipples: boolean;\n depthTint: boolean;\n edgeFeather: boolean;\n rungs: boolean;\n /**\n * True when the active backend uses [0,1] clip Z (WebGPU) rather than [-1,1] (WebGL).\n *\n * `camera.coordinateSystem` changes what `projectionMatrix` produces — for an orthographic camera\n * the two differ by exactly `z_webgl = 2 * z_webgpu - 1`. The depth fade and depth tint consume\n * clip Z directly (`clamp(z * 6, 0, 1)`), so without this remap the wireframe theme and the tint\n * would silently render differently on each backend. WebGPURenderer can run either backend, so\n * this is read from the live renderer rather than assumed.\n */\n webgpuClipZ: boolean;\n}\n\n/** Linear + orbit time. Only the one selected by `loopMotion` is read, as in the GLSL. */\nfunction timeNodes(u: WaveTslUniforms, flags: WaveShapeFlags): { t: FloatNode; loopOff: Vec2Node } {\n if (!flags.loopMotion) {\n return { t: u.uTime.mul(u.uSpeed).add(u.uSeed), loopOff: vec2(0, 0) };\n }\n // Seamless loop: sample the noise on a circle of radius loopR at angle loopTheta — exactly\n // periodic with period uLoopSeconds, and the tangential speed matches the linear drift.\n const loopTheta = u.uTime.mul(float(6.28318530718).div(u.uLoopSeconds)).add(u.uSeed).toVar();\n const loopR = u.uSpeed.mul(u.uLoopSeconds).mul(0.159154943092); // = uSpeed·uLoopSeconds / (2π)\n return { t: float(0), loopOff: vec2(cos(loopTheta), sin(loopTheta)).mul(loopR) };\n}\n\n/**\n * Striations: subtle high-frequency simplex grain ADDED to the colour — colour-matched (weaker\n * where blue is high), only near folds, and concentrated toward the ends. Blends rather than\n * reading as hard lines. Noise bands override the params inside rectangular uv regions.\n */\nfunction surfaceStreaks(\n u: WaveTslUniforms,\n uvIn: Vec2Node,\n color: Vec3Node,\n crease: FloatNode,\n): Vec3Node {\n const strength = float(u.uFiberStrength).toVar(\"streakStrength\");\n const freq = float(u.uFiberCount).toVar(\"streakFreq\");\n const colorAtten = float(0.9).toVar(\"streakAtten\");\n const paraPow = float(3.0).toVar(\"streakPara\");\n\n Loop({ start: 0, end: MAX_NOISE_BANDS, type: \"int\" }, ({ i }) => {\n If(float(i).greaterThanEqual(float(u.uNumNoiseBands)), () => {\n Break();\n });\n const b = u.uNoiseBandBounds.el(i).toVar();\n const prm = u.uNoiseBandParams.el(i).toVar();\n const feather = max(prm.x, 1.0e-4).toVar();\n const blend = smoothstep(b.x.sub(feather), b.x, uvIn.x)\n .mul(float(1).sub(smoothstep(b.y, b.y.add(feather), uvIn.x)))\n .mul(smoothstep(b.z.sub(feather), b.z, uvIn.y))\n .mul(float(1).sub(smoothstep(b.w, b.w.add(feather), uvIn.y)));\n strength.assign(mix(strength, prm.y, blend));\n freq.assign(mix(freq, prm.z, blend));\n colorAtten.assign(mix(colorAtten, prm.w, blend));\n paraPow.assign(mix(paraPow, u.uNoiseBandParaPow.el(i), blend));\n });\n\n // The high frequency runs along uv.x (the folded WIDTH), packing many thin stripes across the\n // cross-section while uv.y is barely scaled, so each stretches into a fine LENGTHWISE fiber.\n const p = float(1).sub(parabola(uvIn.x, paraPow));\n const n0 = simplexNoise(vec2(uvIn.x.mul(0.1), uvIn.y.mul(0.5))).toVar();\n const n1raw = simplexNoise(\n vec2(uvIn.x.mul(freq.add(freq.mul(0.5).mul(n0))), uvIn.y.mul(4.0).mul(n0)),\n );\n const n1 = mapLinear(n1raw, -1, 1, 0, 1);\n return color.add(\n n1\n .mul(strength)\n .mul(float(1).sub(color.b.mul(colorAtten)))\n .mul(crease)\n .mul(p),\n );\n}\n\n/** Build one wave's material for the given flags. */\nexport function buildWaveMaterial(u: WaveTslUniforms, flags: WaveMaterialFlags): NodeMaterial {\n const material = new NodeMaterial();\n material.transparent = true;\n material.depthTest = true;\n material.depthWrite = true;\n material.side = 2; // THREE.DoubleSide\n\n // ---- Vertex ----\n // The pointer falloff is computed in the vertex stage alongside the displacement and carried to\n // the fragment, matching the GLSL's `varying float vPointerFall`.\n let pointerFall: FloatNode | null = null;\n\n material.positionNode = Fn(() => {\n const { t, loopOff } = timeNodes(u, flags);\n const ws = waveShape(u, flags, positionLocal, uv(), t, loopOff);\n if (!flags.pointerFx) return ws.pos;\n\n // Displace along the wave's own (post-twist) up-axis, weighted by a screen-space falloff around\n // the smoothed cursor. Everything here is ADDITIVE, so the shared path above is untouched.\n // Shared clip transform, computed once and reused for the cursor metric and the ribbon tangent\n // (the compiler is not guaranteed to CSE the triple product).\n const mvp = cameraProjectionMatrix.mul(cameraViewMatrix).mul(modelWorldMatrix).toVar(\"mvp\");\n const preClip = mvp.mul(vec4(ws.pos, 1.0)).toVar(\"preClip\");\n const hit = pointerField(\n u,\n { loopMotion: flags.loopMotion, ripples: flags.pointerRipples },\n preClip.xy.div(max(preClip.w, 1.0e-6)),\n mvp,\n ws.twists,\n ws.pos,\n t,\n loopOff,\n );\n pointerFall = varying(hit.fall, \"vPointerFall\");\n // Rotations are linear, so displacing the post-twist axis equals displacing pre-twist Y.\n const dispAxis = applyTwist(\n applyTwist(applyTwist(vec3(0, 1, 0), ws.twists[0]), ws.twists[1]),\n ws.twists[2],\n );\n return ws.pos.add(dispAxis.mul(hit.disp));\n })();\n\n // Clip-space depth, normalised to the WebGL [-1,1] convention the GLSL was written against.\n const rawClip = varying(\n cameraProjectionMatrix\n .mul(cameraViewMatrix)\n .mul(modelWorldMatrix)\n .mul(vec4(material.positionNode as unknown as Vec3Node, 1.0)),\n \"vClipPosition\",\n );\n const clipZ: FloatNode = flags.webgpuClipZ ? rawClip.z.mul(2).sub(1) : rawClip.z;\n\n // ---- Fragment ----\n material.outputNode =\n flags.theme === \"wireframe\"\n ? buildWireframeFragment(u, flags, clipZ, pointerFall)\n : buildSolidFragment(u, flags, clipZ, pointerFall);\n\n return material;\n}\n\n/** The wireframe thin-line theme: colour carved into fine lengthwise strands, faded by depth. */\nfunction buildWireframeFragment(\n u: WaveTslUniforms,\n flags: WaveMaterialFlags,\n clipZ: FloatNode,\n pointerFall: FloatNode | null,\n) {\n return Fn(() => {\n const vUv = uv();\n const color = applyColorGrade(u, waveBaseColor(u, vUv)).toVar(\"lineColor\");\n if (pointerFall) {\n color.assign(hueShift(color, radians(u.uPointerHue).mul(pointerFall)));\n color.mulAssign(float(1).add(u.uPointerLighten.mul(pointerFall)));\n }\n\n // Carve into fine lengthwise strands; thickness from the screen-space uv derivative.\n const dy = dFdy(vUv).toVar();\n const lineThickness = u.uLineThickness\n .mul(pow(tabs(dy.x.mul(u.uMaxWidth)), u.uLineDerivativePower))\n .toVar(\"lineThickness\");\n if (pointerFall) {\n // Strands taper to hairlines near the cursor.\n lineThickness.mulAssign(clamp(float(1).sub(u.uPointerThin.mul(pointerFall)), 0, 1));\n }\n const a = smoothstep(lineThickness, 0.0, tabs(sin(vUv.x.mul(u.uLineAmount)))).toVar(\"lineA\");\n\n if (flags.rungs) {\n // The same carve at constant uv.y, so this family runs ACROSS the ribbon where the one above\n // runs along it — together they read as a ladder. Width comes from fwidth(), not the\n // lengthwise term's dFdy(vUv).x, which is the derivative of the wrong axis for this direction.\n const rung = tabs(sin(vUv.y.mul(u.uRungAmount)));\n a.assign(\n max(a, smoothstep(u.uRungThickness.mul(u.uRungAmount).mul(fwidth(vUv.y)), 0.0, rung)),\n );\n }\n\n // Depth fade: the wave recedes into the background colour with depth.\n const depthFade = clamp(clipZ.mul(6.0), 0, 1);\n const faded = mix(u.uClearColor, color, a.mul(float(1).sub(depthFade))).toVar(\"lineFaded\");\n // Deep \"squared\" look — composited, not replace-blended (see applyBlendMode).\n const out = select(u.uSquared.greaterThan(0.5), faded.mul(faded), faded);\n return vec4(out, u.uOpacity);\n })();\n}\n\n/** The solid surface theme. */\nfunction buildSolidFragment(\n u: WaveTslUniforms,\n flags: WaveMaterialFlags,\n clipZ: FloatNode,\n pointerFall: FloatNode | null,\n) {\n return Fn(() => {\n const vUv = uv();\n const vWorldPos = positionWorld;\n const vViewDir = cameraPosition.sub(vWorldPos).toVar(\"vViewDir\");\n\n // crease: a foreshortening / fold detector from the screen-space uv derivative. It drives BOTH\n // the roundness shading and where the streaks appear — this is what gives the wave its\n // thickness without any normal-based lighting.\n const crease = float(0).toVar(\"crease\");\n crease.assign(dFdy(vUv).y.mul(u.uResolution.y).mul(u.uCreaseLight));\n crease.assign(clamp(mapLinear(crease, -1, 1, 0, 1), 0, 1));\n crease.assign(pow(crease, u.uCreaseSharpness));\n crease.assign(clamp(smoothstep(0.0, u.uCreaseSoftness, crease), 0, 1));\n\n const col = waveBaseColor(u, vUv).toVar(\"col\");\n col.assign(surfaceStreaks(u, vUv, col, crease));\n col.assign(applyColorGrade(u, col));\n\n if (pointerFall) {\n // Local hue rotation + brightness lift near the cursor (both fade out with the falloff).\n col.assign(hueShift(col, radians(u.uPointerHue).mul(pointerFall)));\n col.mulAssign(float(1).add(u.uPointerLighten.mul(pointerFall)));\n }\n\n // Iridescence: a thin-film hue that shifts with view angle — grazing parts of the ribbon shift\n // most, so the colour flows as the ribbon curves.\n If(u.uIridescence.greaterThan(0.001), () => {\n const iridN = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));\n const iridFacing = tabs(dot(iridN, normalize(vViewDir)));\n col.assign(hueShift(col, float(1).sub(iridFacing).mul(u.uIridescence).mul(Math.PI)));\n });\n\n // Sheen: lift the flat (low-crease) areas toward white. Pose-dependent, so kept gentle.\n col.addAssign(float(1).sub(crease).mul(0.25).mul(u.uSheen));\n\n // Pose-robust roundness: shade by the camera-facing ratio of the derivative surface normal so\n // the ribbon reads as a rounded solid from any angle.\n If(u.uRoundness.greaterThan(0.001), () => {\n const volN = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));\n const facing = tabs(dot(volN, normalize(vViewDir))).toVar(\"facing\");\n col.mulAssign(mix(float(1).sub(u.uRoundness.mul(0.6)), 1.0, facing));\n col.addAssign(smoothstep(0.65, 1.0, facing).mul(u.uRoundness).mul(0.18));\n });\n\n // Optional positionable lights — additive and gentle, on top of the base shading.\n If(u.uNumLights.greaterThan(0), () => {\n const n = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos))).toVar(\"lightN\");\n const vd = normalize(vViewDir).toVar(\"lightV\");\n If(dot(n, vd).lessThan(0.0), () => {\n n.assign(n.negate());\n });\n Loop({ start: 0, end: MAX_LIGHTS, type: \"int\" }, ({ i }) => {\n If(float(i).greaterThanEqual(float(u.uNumLights)), () => {\n Break();\n });\n const l = normalize(u.uLightPos.el(i).sub(vWorldPos)).toVar();\n const lc = u.uLightColor.el(i).mul(u.uLightIntensity.el(i)).toVar();\n const diff = max(dot(n, l), 0.0);\n const spec = pow(max(dot(n, normalize(l.add(vd))), 0.0), 28.0);\n col.addAssign(col.mul(diff).mul(lc).mul(0.16).add(lc.mul(spec).mul(0.1)));\n });\n });\n\n col.mulAssign(clamp(u.uAmbient, 0, 1).add(0.55)); // overall level; default 0.45 => x1.0\n\n if (flags.depthTint) {\n // Fade far fragments toward a colour so a multi-wave stack gains atmospheric separation.\n col.assign(mix(col, u.uDepthTintColor, clamp(clipZ.mul(6.0), 0, 1).mul(u.uDepthTint)));\n }\n\n If(u.uTexture.greaterThan(0.001), () => {\n col.mulAssign(float(1).add(grainHash(vUv.mul(850.0)).sub(0.5).mul(u.uTexture).mul(0.25)));\n });\n\n // Soft ribbon ENDS (fades on vUv.y, the length) + optional viewport-edge fade.\n const ribEdge = flags.edgeFeather\n ? smoothstep(0.0, u.uEdgeFeather, vUv.y).mul(\n float(1).sub(smoothstep(float(1).sub(u.uEdgeFeather), 1.0, vUv.y)),\n )\n : smoothstep(0.0, 0.1, vUv.y).mul(float(1).sub(smoothstep(0.9, 1.0, vUv.y)));\n const alpha = u.uOpacity.mul(ribEdge).toVar(\"alpha\");\n if (pointerFall) {\n alpha.mulAssign(clamp(float(1).sub(u.uPointerThin.mul(pointerFall)), 0, 1)); // local translucency\n }\n\n If(u.uEdgeFade.greaterThan(0.001), () => {\n const sc = screenUV;\n const vig = smoothstep(0.0, u.uEdgeFade, sc.x)\n .mul(float(1).sub(smoothstep(float(1).sub(u.uEdgeFade), 1.0, sc.x)))\n .mul(smoothstep(0.0, u.uEdgeFade, sc.y))\n .mul(float(1).sub(smoothstep(float(1).sub(u.uEdgeFade), 1.0, sc.y)));\n alpha.mulAssign(vig);\n });\n\n // Deep \"squared\" hero colour: square colour AND alpha so the soft ribbon edges keep the crisp\n // feather of the original squared-blend look, but composited (premultiplied) rather than\n // replace-blended, so they no longer punch holes.\n col.assign(clamp(col, vec3(0), vec3(1)));\n const squared = u.uSquared.greaterThan(0.5);\n return vec4(select(squared, col.mul(col), col), select(squared, alpha.mul(alpha), alpha));\n })();\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { simplexNoise } from "./noise.js";
|
|
2
|
+
import { Fn, clamp, cos, cross, dot, exp2, float, max, mix, normalize, pow, radians, sin, vec2, vec3 } from "three/tsl";
|
|
3
|
+
//#region src/renderer/tsl/waveShape.ts
|
|
4
|
+
/**
|
|
5
|
+
* The wave SHAPE deform in TSL — the port of the `waveShapeChunk` GLSL in `../shaders.ts`.
|
|
6
|
+
*
|
|
7
|
+
* A baked hairpin position + its uv become the displaced / helixed / twisted / fanned local
|
|
8
|
+
* position, plus the three twists the pointer field needs. The particle emitter calls this too, so
|
|
9
|
+
* a wave and the dust it sheds ride one deform.
|
|
10
|
+
*
|
|
11
|
+
* The GLSL `#ifdef` gates (LOOP_MOTION, DETAIL_OCTAVE, HELIX, TWIST_MOTION, RADIAL) become plain JS
|
|
12
|
+
* flags: the graph is built per material, so an unused branch is simply never constructed. That is
|
|
13
|
+
* strictly better than the define-based variants — there is no dead uniform to declare and no
|
|
14
|
+
* program-key bookkeeping — but it does mean the "byte-identical when off" contract the GLSL
|
|
15
|
+
* comments assert is no longer a thing to verify. `parity/` covers the behaviour instead.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A falloff from 1 (at x=0) toward 0, sharpness set by n. `max()` guards `pow(0, n)` (= Infinity →
|
|
19
|
+
* NaN), which also makes negative n safe — it just concentrates the twist toward the other end.
|
|
20
|
+
*/
|
|
21
|
+
const expStep = /*@__PURE__*/ Fn(([x, n]) => exp2(exp2(n).mul(pow(max(x, .001), n)).negate())).setLayout({
|
|
22
|
+
name: "wave_expStep",
|
|
23
|
+
type: "float",
|
|
24
|
+
inputs: [{
|
|
25
|
+
name: "x",
|
|
26
|
+
type: "float"
|
|
27
|
+
}, {
|
|
28
|
+
name: "n",
|
|
29
|
+
type: "float"
|
|
30
|
+
}]
|
|
31
|
+
});
|
|
32
|
+
/** Rotate `v` about `t.axis` by `t.angle` — the row-vector product the GLSL performs. */
|
|
33
|
+
const applyTwist = (v, t) => {
|
|
34
|
+
const k = normalize(t.axis);
|
|
35
|
+
const c = cos(t.angle);
|
|
36
|
+
const s = sin(t.angle);
|
|
37
|
+
return v.mul(c).add(cross(k, v).mul(s)).add(k.mul(dot(k, v)).mul(float(1).sub(c)));
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Radial fan: remap the ribbon to polar around the LOCAL origin so its LENGTH fans into a plume.
|
|
41
|
+
*
|
|
42
|
+
* uv.x (the folded WIDTH) becomes the fan ANGLE across `arc`; uv.y (the LENGTH) becomes the RADIUS,
|
|
43
|
+
* so a constant-uv.x combed fiber turns into a constant-angle radial spoke. `amount` 0 is the
|
|
44
|
+
* identity mix. Exported so `parity:math` can probe it against the GLSL.
|
|
45
|
+
*/
|
|
46
|
+
function applyRadial(pos, uv, amount, arc, spread, radius, center) {
|
|
47
|
+
const rAng = radians(center).add(clamp(uv.x, 0, 1).sub(.5).mul(radians(arc))).toVar("rAng");
|
|
48
|
+
const rRho = radius.add(uv.y.mul(400).mul(spread));
|
|
49
|
+
const rEr = vec3(cos(rAng), sin(rAng), 0);
|
|
50
|
+
const rEt = vec3(sin(rAng).negate(), cos(rAng), 0);
|
|
51
|
+
return mix(pos, rEr.mul(rRho).add(rEt.mul(pos.z.sub(-8)).mul(.5)).add(vec3(0, 0, pos.y)), clamp(amount, 0, 1));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Deform one vertex. `t` is linear time and `loopOff` the orbit offset; only the one selected by
|
|
55
|
+
* `flags.loopMotion` is read, exactly as in the GLSL where the other is a dead argument.
|
|
56
|
+
*/
|
|
57
|
+
function waveShape(u, flags, position, uv, t, loopOff) {
|
|
58
|
+
const pos = position.toVar();
|
|
59
|
+
const dispArg = flags.loopMotion ? vec2(pos.x.mul(u.uDispFreqX), pos.z.mul(u.uDispFreqZ)).add(loopOff) : vec2(pos.x.mul(u.uDispFreqX).add(t), pos.z.mul(u.uDispFreqZ).add(t));
|
|
60
|
+
pos.y.addAssign(simplexNoise(dispArg).mul(u.uDispAmount));
|
|
61
|
+
if (flags.detailOctave) {
|
|
62
|
+
const detailArg = flags.loopMotion ? vec2(pos.x.mul(u.uDetailFreq), pos.z.mul(u.uDetailFreq)).add(loopOff) : vec2(pos.x.mul(u.uDetailFreq).add(t), pos.z.mul(u.uDetailFreq).add(t));
|
|
63
|
+
pos.y.addAssign(simplexNoise(detailArg).mul(u.uDetailAmount));
|
|
64
|
+
}
|
|
65
|
+
if (flags.helix) {
|
|
66
|
+
const hAng = float(6.28318530718).mul(u.uHelixTurns).mul(uv.y).add(radians(u.uHelixPhase)).toVar();
|
|
67
|
+
const rollA = hAng.mul(u.uHelixRoll);
|
|
68
|
+
const rollC = cos(rollA);
|
|
69
|
+
const rollS = sin(rollA);
|
|
70
|
+
const rel = vec2(pos.y, pos.z.sub(-8)).toVar();
|
|
71
|
+
pos.y.assign(rel.x.mul(rollC).sub(rel.y.mul(rollS)));
|
|
72
|
+
pos.z.assign(float(-8).add(rel.x.mul(rollS)).add(rel.y.mul(rollC)));
|
|
73
|
+
pos.y.addAssign(u.uHelixRadius.mul(cos(hAng)));
|
|
74
|
+
pos.z.addAssign(u.uHelixRadius.mul(sin(hAng)));
|
|
75
|
+
}
|
|
76
|
+
let twistXFreq = u.uTwFreqX;
|
|
77
|
+
if (flags.twistMotion) {
|
|
78
|
+
const noiseArg = flags.loopMotion ? vec2(uv.y.mul(2), 0).add(loopOff) : vec2(uv.y.mul(2), t);
|
|
79
|
+
twistXFreq = u.uTwFreqX.sub(simplexNoise(noiseArg).mul(.1));
|
|
80
|
+
}
|
|
81
|
+
const twists = [
|
|
82
|
+
{
|
|
83
|
+
axis: vec3(.5, 0, .5),
|
|
84
|
+
angle: u.uTwFreqY.mul(expStep(uv.x, u.uTwPowY))
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
axis: vec3(0, .5, .5),
|
|
88
|
+
angle: twistXFreq.mul(expStep(uv.y, u.uTwPowX))
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
axis: vec3(.5, 0, .5),
|
|
92
|
+
angle: u.uTwFreqZ.mul(expStep(uv.y, u.uTwPowZ))
|
|
93
|
+
}
|
|
94
|
+
];
|
|
95
|
+
const twisted = applyTwist(applyTwist(applyTwist(pos, twists[0]), twists[1]), twists[2]).toVar();
|
|
96
|
+
if (!flags.radial) return {
|
|
97
|
+
pos: twisted,
|
|
98
|
+
twists
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
pos: applyRadial(twisted, uv, u.uRadialAmount, u.uRadialArc, u.uRadialSpread, u.uRadialRadius, u.uRadialCenter),
|
|
102
|
+
twists
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { applyRadial, applyTwist, expStep, waveShape };
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=waveShape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waveShape.js","names":[],"sources":["../../../src/renderer/tsl/waveShape.ts"],"sourcesContent":["/**\n * The wave SHAPE deform in TSL — the port of the `waveShapeChunk` GLSL in `../shaders.ts`.\n *\n * A baked hairpin position + its uv become the displaced / helixed / twisted / fanned local\n * position, plus the three twists the pointer field needs. The particle emitter calls this too, so\n * a wave and the dust it sheds ride one deform.\n *\n * The GLSL `#ifdef` gates (LOOP_MOTION, DETAIL_OCTAVE, HELIX, TWIST_MOTION, RADIAL) become plain JS\n * flags: the graph is built per material, so an unused branch is simply never constructed. That is\n * strictly better than the define-based variants — there is no dead uniform to declare and no\n * program-key bookkeeping — but it does mean the \"byte-identical when off\" contract the GLSL\n * comments assert is no longer a thing to verify. `parity/` covers the behaviour instead.\n */\nimport {\n Fn,\n float,\n vec2,\n vec3,\n cos,\n sin,\n radians,\n exp2,\n pow,\n max,\n clamp,\n mix,\n cross,\n dot,\n normalize,\n} from \"three/tsl\";\nimport { RIBBON_Z_CENTER } from \"../WaveGeometry\";\nimport { simplexNoise } from \"./noise\";\nimport type { FloatNode, Vec2Node, Vec3Node } from \"./types\";\nimport type { WaveTslUniforms } from \"./uniforms\";\n\n/**\n * A falloff from 1 (at x=0) toward 0, sharpness set by n. `max()` guards `pow(0, n)` (= Infinity →\n * NaN), which also makes negative n safe — it just concentrates the twist toward the other end.\n */\nexport const expStep = /*@__PURE__*/ Fn(([x, n]: [FloatNode, FloatNode]) =>\n exp2(\n exp2(n)\n .mul(pow(max(x, 1.0e-3), n))\n .negate(),\n ),\n).setLayout({\n name: \"wave_expStep\",\n type: \"float\",\n inputs: [\n { name: \"x\", type: \"float\" },\n { name: \"n\", type: \"float\" },\n ],\n});\n\n/**\n * One twist: an axis and the angle the GLSL passes to `rotationMatrix`.\n *\n * The GLSL builds a matrix and applies it ROW-vector style — `(vec4(pos,1) * R).xyz`. Note the\n * matrix it builds is the TRANSPOSE of the standard Rodrigues matrix (GLSL's `mat4(...)` fills\n * column-major, and the literal is laid out by rows), so `v * R` is `transpose(R) * v`, which is a\n * plain rotation by +angle. Check with axis = +z and v = (1,0,0): `v * R` = (cos, sin, 0).\n *\n * `applyTwist` performs that rotation directly, so there is no matrix-storage-order assumption to\n * get wrong between GLSL's `mat4(...)` and WGSL's `mat4x4<f32>(...)`. Rotations are linear, so the\n * same helper serves direction vectors (the pointer field's displacement axis and ribbon tangent)\n * with no w-component special case. `parity:math` pins this against the original.\n */\nexport interface Twist {\n axis: Vec3Node;\n angle: FloatNode;\n}\n\n/** Rotate `v` about `t.axis` by `t.angle` — the row-vector product the GLSL performs. */\nexport const applyTwist = (v: Vec3Node, t: Twist): Vec3Node => {\n const k = normalize(t.axis);\n const c = cos(t.angle);\n const s = sin(t.angle);\n return v\n .mul(c)\n .add(cross(k, v).mul(s))\n .add(k.mul(dot(k, v)).mul(float(1).sub(c)));\n};\n\n/**\n * The helix: the periodic sweep the three twists (monotone falloffs) can't reach.\n *\n * Runs AFTER the displacement (so the noise still samples the undeformed position) and BEFORE the\n * twist (so they compose). The roll is about the ribbon's width centre, not the origin — see\n * RIBBON_Z_CENTER in WaveGeometry. Exported so `parity:math` can probe it against the GLSL.\n */\nexport function applyHelix(\n pos: Vec3Node,\n uvY: FloatNode,\n turns: FloatNode,\n phaseDeg: FloatNode,\n roll: FloatNode,\n radius: FloatNode,\n): Vec3Node {\n const hAng = float(6.28318530718).mul(turns).mul(uvY).add(radians(phaseDeg)).toVar();\n const rollA = hAng.mul(roll);\n const rollC = cos(rollA);\n const rollS = sin(rollA);\n const rel = vec2(pos.y, pos.z.sub(RIBBON_Z_CENTER)).toVar();\n return vec3(\n pos.x,\n rel.x\n .mul(rollC)\n .sub(rel.y.mul(rollS))\n .add(radius.mul(cos(hAng))),\n float(RIBBON_Z_CENTER)\n .add(rel.x.mul(rollS))\n .add(rel.y.mul(rollC))\n .add(radius.mul(sin(hAng))),\n );\n}\n\n/**\n * Radial fan: remap the ribbon to polar around the LOCAL origin so its LENGTH fans into a plume.\n *\n * uv.x (the folded WIDTH) becomes the fan ANGLE across `arc`; uv.y (the LENGTH) becomes the RADIUS,\n * so a constant-uv.x combed fiber turns into a constant-angle radial spoke. `amount` 0 is the\n * identity mix. Exported so `parity:math` can probe it against the GLSL.\n */\nexport function applyRadial(\n pos: Vec3Node,\n uv: Vec2Node,\n amount: FloatNode,\n arc: FloatNode,\n spread: FloatNode,\n radius: FloatNode,\n center: FloatNode,\n): Vec3Node {\n const rAng = radians(center)\n .add(clamp(uv.x, 0, 1).sub(0.5).mul(radians(arc)))\n .toVar(\"rAng\");\n const rRho = radius.add(uv.y.mul(400.0).mul(spread)); // 400 = native ribbon length\n const rEr = vec3(cos(rAng), sin(rAng), 0.0); // radial dir, in local X-Y (the screen plane)\n const rEt = vec3(sin(rAng).negate(), cos(rAng), 0.0); // tangential\n const fanned = rEr\n .mul(rRho)\n .add(rEt.mul(pos.z.sub(RIBBON_Z_CENTER)).mul(0.5))\n .add(vec3(0.0, 0.0, pos.y));\n return mix(pos, fanned, clamp(amount, 0, 1));\n}\n\n/** Which optional blocks this material's graph includes — the JS twin of the GLSL `#ifdef` set. */\nexport interface WaveShapeFlags {\n loopMotion: boolean;\n detailOctave: boolean;\n helix: boolean;\n twistMotion: boolean;\n radial: boolean;\n}\n\nexport interface WaveShapeResult {\n pos: Vec3Node;\n /** The three twists, in application order — the pointer field carries its axes through these. */\n twists: [Twist, Twist, Twist];\n}\n\n/**\n * Deform one vertex. `t` is linear time and `loopOff` the orbit offset; only the one selected by\n * `flags.loopMotion` is read, exactly as in the GLSL where the other is a dead argument.\n */\nexport function waveShape(\n u: WaveTslUniforms,\n flags: WaveShapeFlags,\n position: Vec3Node,\n uv: Vec2Node,\n t: FloatNode,\n loopOff: Vec2Node,\n): WaveShapeResult {\n const pos = position.toVar();\n\n // Displacement lifts Y by simplex noise of the (x,z) position.\n const dispArg = flags.loopMotion\n ? vec2(pos.x.mul(u.uDispFreqX), pos.z.mul(u.uDispFreqZ)).add(loopOff)\n : vec2(pos.x.mul(u.uDispFreqX).add(t), pos.z.mul(u.uDispFreqZ).add(t));\n pos.y.addAssign(simplexNoise(dispArg).mul(u.uDispAmount));\n\n if (flags.detailOctave) {\n // A second, finer octave riding on the broad swell (loop-orbit shared so it stays periodic).\n const detailArg = flags.loopMotion\n ? vec2(pos.x.mul(u.uDetailFreq), pos.z.mul(u.uDetailFreq)).add(loopOff)\n : vec2(pos.x.mul(u.uDetailFreq).add(t), pos.z.mul(u.uDetailFreq).add(t));\n pos.y.addAssign(simplexNoise(detailArg).mul(u.uDetailAmount));\n }\n\n if (flags.helix) {\n // The periodic sweep the three twists (monotone falloffs) can't reach. Runs AFTER displacement\n // (so the noise still samples undeformed pos) and BEFORE the twist (so they compose).\n const hAng = float(6.28318530718)\n .mul(u.uHelixTurns)\n .mul(uv.y)\n .add(radians(u.uHelixPhase))\n .toVar();\n // Roll about the ribbon's width centre, not the origin — see RIBBON_Z_CENTER in WaveGeometry.\n const rollA = hAng.mul(u.uHelixRoll);\n const rollC = cos(rollA);\n const rollS = sin(rollA);\n const rel = vec2(pos.y, pos.z.sub(RIBBON_Z_CENTER)).toVar();\n pos.y.assign(rel.x.mul(rollC).sub(rel.y.mul(rollS)));\n pos.z.assign(float(RIBBON_Z_CENTER).add(rel.x.mul(rollS)).add(rel.y.mul(rollC)));\n pos.y.addAssign(u.uHelixRadius.mul(cos(hAng)));\n pos.z.addAssign(u.uHelixRadius.mul(sin(hAng)));\n }\n\n // The X-twist frequency feeding the second rotation. Under TWIST_MOTION it is modulated by\n // simplex noise indexed along the ribbon (uv.y), so the twist breathes over time.\n let twistXFreq: FloatNode = u.uTwFreqX;\n if (flags.twistMotion) {\n const noiseArg = flags.loopMotion\n ? vec2(uv.y.mul(2.0), 0.0).add(loopOff)\n : vec2(uv.y.mul(2.0), t);\n twistXFreq = u.uTwFreqX.sub(simplexNoise(noiseArg).mul(0.1));\n }\n\n // Three-axis twist. rotA keys off uv.x (the folded WIDTH), rotB/rotC off uv.y (the LENGTH).\n const twists: [Twist, Twist, Twist] = [\n { axis: vec3(0.5, 0.0, 0.5), angle: u.uTwFreqY.mul(expStep(uv.x, u.uTwPowY)) },\n { axis: vec3(0.0, 0.5, 0.5), angle: twistXFreq.mul(expStep(uv.y, u.uTwPowX)) },\n { axis: vec3(0.5, 0.0, 0.5), angle: u.uTwFreqZ.mul(expStep(uv.y, u.uTwPowZ)) },\n ];\n const twisted = applyTwist(applyTwist(applyTwist(pos, twists[0]), twists[1]), twists[2]).toVar();\n\n if (!flags.radial) return { pos: twisted, twists };\n return {\n pos: applyRadial(\n twisted,\n uv,\n u.uRadialAmount,\n u.uRadialArc,\n u.uRadialSpread,\n u.uRadialRadius,\n u.uRadialCenter,\n ),\n twists,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StudioConfig } from "../config/model.js";
|
|
2
|
+
import { TiltStatus } from "../renderer/tilt.js";
|
|
2
3
|
import { WaveRenderer } from "../renderer/WaveRenderer.js";
|
|
3
4
|
import { core_loader_d_exports } from "../core-loader.js";
|
|
4
5
|
import { PosterFit } from "./poster.js";
|
|
@@ -22,6 +23,18 @@ interface WaveOptions {
|
|
|
22
23
|
rootMargin?: string;
|
|
23
24
|
/** "auto" probes WebGL (with failIfMajorPerformanceCaveat); "force" skips the probe; "off" stays a poster. */
|
|
24
25
|
webgl?: "auto" | "force" | "off";
|
|
26
|
+
/**
|
|
27
|
+
* Which renderer backend to use.
|
|
28
|
+
*
|
|
29
|
+
* - `"webgl"` (default) — the GLSL renderer. Nothing extra is downloaded.
|
|
30
|
+
* - `"webgpu"` — the TSL renderer, which itself falls back to a WebGL2 backend where WebGPU is
|
|
31
|
+
* unavailable, so it always renders.
|
|
32
|
+
* - `"auto"` — WebGPU where an adapter can be acquired, else the GLSL renderer.
|
|
33
|
+
*
|
|
34
|
+
* Anything other than `"webgl"` fetches a separate ~197 KB (gzipped) chunk holding three's node
|
|
35
|
+
* system, so it is opt-in rather than the default. See `renderer/gpu-loader.ts`.
|
|
36
|
+
*/
|
|
37
|
+
backend?: "webgl" | "webgpu" | "auto";
|
|
25
38
|
/** Forward prefers-reduced-motion to the renderer (freezes to a full static frame). Default true. */
|
|
26
39
|
respectReducedMotion?: boolean;
|
|
27
40
|
/** With reduced motion: "static" upgrades to a frozen frame; "poster" stays a poster. Default "static". */
|
|
@@ -61,6 +74,22 @@ interface WaveHandle {
|
|
|
61
74
|
/** Feed a `custom:<name>` interaction input for `custom:*` bindings. Staged (last value per name)
|
|
62
75
|
* before upgrade and replayed once the renderer is live; a no-op if no binding consumes it. */
|
|
63
76
|
setInteractionInput(name: string, value: number): void;
|
|
77
|
+
/**
|
|
78
|
+
* Explicitly ask for the device-orientation sensor. OPTIONAL, and on iOS it opens a modal
|
|
79
|
+
* permission dialog — nothing calls it for you, and a decorative scene should simply go without
|
|
80
|
+
* tilt on that platform rather than interrupt the reader. CALL IT FROM A USER GESTURE: iOS 13+
|
|
81
|
+
* grants the sensor only from inside a tap handler.
|
|
82
|
+
*
|
|
83
|
+
* Before the wave has upgraded there is no renderer to ask, so the request is REMEMBERED and
|
|
84
|
+
* replayed on upgrade; that replay is no longer inside the gesture, so on iOS it resolves false
|
|
85
|
+
* and the reader taps again. Gate the button on {@link WaveOptions.onReady} (or the element's
|
|
86
|
+
* `wave3d-ready` event) and the first tap is the only tap.
|
|
87
|
+
*/
|
|
88
|
+
enableTilt(): Promise<boolean>;
|
|
89
|
+
/** Where the tilt sensor stands. `"prompt"` is exactly when a tap-to-enable affordance helps. */
|
|
90
|
+
tiltStatus(): TiltStatus;
|
|
91
|
+
/** Take the next orientation reading as the neutral pose (the reader has changed grip). */
|
|
92
|
+
recenterTilt(): void;
|
|
64
93
|
play(): void;
|
|
65
94
|
pause(): void;
|
|
66
95
|
/** Safe to call in any state (aborts a pending upgrade, disposes a live renderer, removes the poster). */
|
package/dist/shell/createWave.js
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
import { hasWebGL, prefersReducedData, prefersReducedMotion } from "./probe.js";
|
|
1
|
+
import { hasWebGL, hasWebGPU, prefersReducedData, prefersReducedMotion } from "./probe.js";
|
|
2
2
|
import { ensurePositioned, setupPoster } from "./poster.js";
|
|
3
3
|
//#region src/shell/createWave.ts
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
/** The stub for WebGL-only builds: refuses, and the caller falls back to the GLSL renderer. */
|
|
5
|
+
const noGpuBackend = () => Promise.reject(/* @__PURE__ */ new Error("This build ships the WebGL renderer only."));
|
|
6
|
+
function createWaveImpl(loadCore, loadGpu, container, config, options) {
|
|
7
|
+
/** Pick the renderer class, fetching the TSL backend only when it is actually wanted. */
|
|
8
|
+
const resolveBackend = async () => {
|
|
9
|
+
const glsl = async () => (await loadCore()).WaveRenderer;
|
|
10
|
+
if (backend === "webgl") return glsl();
|
|
11
|
+
if (backend === "auto" && !await hasWebGPU()) return glsl();
|
|
12
|
+
try {
|
|
13
|
+
return (await loadGpu()).WaveRendererGPU;
|
|
14
|
+
} catch {
|
|
15
|
+
return glsl();
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const { lazy = true, rootMargin = "200px", webgl = "auto", backend = "webgl", respectReducedMotion = true, reducedMotionBehavior = "static", respectSaveData = true, fadeMs = 300 } = options;
|
|
11
19
|
let state = "poster";
|
|
12
20
|
let renderer = null;
|
|
13
21
|
let staged = { ...config };
|
|
14
22
|
if (options.paused !== void 0) staged.paused = options.paused;
|
|
15
23
|
const stagedInputs = /* @__PURE__ */ new Map();
|
|
24
|
+
/** enableTilt() called before the upgrade — replayed once the renderer exists (see WaveHandle). */
|
|
25
|
+
let tiltRequested = false;
|
|
16
26
|
let aborted = false;
|
|
17
27
|
let io = null;
|
|
18
28
|
let lostTimer;
|
|
@@ -68,12 +78,19 @@ function createWaveImpl(loadCore, container, config, options) {
|
|
|
68
78
|
...staged
|
|
69
79
|
};
|
|
70
80
|
const rendererOptions = { respectReducedMotion };
|
|
71
|
-
renderer = new
|
|
81
|
+
renderer = new (await (resolveBackend()))(container, full, rendererOptions);
|
|
82
|
+
await renderer.init();
|
|
83
|
+
if (aborted) {
|
|
84
|
+
renderer.dispose();
|
|
85
|
+
renderer = null;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
72
88
|
const canvas = renderer.renderer.domElement;
|
|
73
89
|
canvas.addEventListener("webglcontextlost", onContextLost, false);
|
|
74
90
|
canvas.addEventListener("webglcontextrestored", onContextRestored, false);
|
|
75
91
|
renderer.start();
|
|
76
92
|
for (const [name, value] of stagedInputs) renderer.setInteractionInput(name, value);
|
|
93
|
+
if (tiltRequested) renderer.enableTilt();
|
|
77
94
|
setState("running");
|
|
78
95
|
options.onReady?.(renderer);
|
|
79
96
|
if (poster) requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
@@ -131,6 +148,17 @@ function createWaveImpl(loadCore, container, config, options) {
|
|
|
131
148
|
if (renderer) renderer.setInteractionInput(name, value);
|
|
132
149
|
else stagedInputs.set(name, value);
|
|
133
150
|
},
|
|
151
|
+
enableTilt() {
|
|
152
|
+
if (renderer) return renderer.enableTilt();
|
|
153
|
+
tiltRequested = true;
|
|
154
|
+
return Promise.resolve(false);
|
|
155
|
+
},
|
|
156
|
+
tiltStatus() {
|
|
157
|
+
return renderer?.tiltStatus() ?? "prompt";
|
|
158
|
+
},
|
|
159
|
+
recenterTilt() {
|
|
160
|
+
renderer?.recenterTilt();
|
|
161
|
+
},
|
|
134
162
|
play() {
|
|
135
163
|
if (renderer) {
|
|
136
164
|
renderer.getConfig().paused = false;
|
|
@@ -162,11 +190,11 @@ function createWaveImpl(loadCore, container, config, options) {
|
|
|
162
190
|
* No static three import: the engine arrives via a dynamic import, so the shell stays tiny.
|
|
163
191
|
*/
|
|
164
192
|
function createWave(container, config = {}, options = {}) {
|
|
165
|
-
return createWaveImpl(options.loadCore ?? (() => import("../core-loader.js")), container, config, options);
|
|
193
|
+
return createWaveImpl(options.loadCore ?? (() => import("../core-loader.js")), () => import("../renderer/gpu-loader.js"), container, config, options);
|
|
166
194
|
}
|
|
167
195
|
/** The drop-in embed contract: an alias of {@link createWave}. */
|
|
168
196
|
const mountWave = createWave;
|
|
169
197
|
//#endregion
|
|
170
|
-
export { createWave, createWaveImpl, mountWave };
|
|
198
|
+
export { createWave, createWaveImpl, mountWave, noGpuBackend };
|
|
171
199
|
|
|
172
200
|
//# sourceMappingURL=createWave.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createWave.js","names":[],"sources":["../../src/shell/createWave.ts"],"sourcesContent":["import type { StudioConfig } from \"../config/model\";\nimport type { WaveRenderer, WaveRendererOptions } from \"../renderer/WaveRenderer\";\nimport { hasWebGL, prefersReducedMotion, prefersReducedData } from \"./probe\";\nimport { setupPoster, ensurePositioned, type Poster, type PosterFit } from \"./poster\";\n\nexport type { PosterFit } from \"./poster\";\n\n/** Why the shell showed the poster instead of a live wave. */\nexport type FallbackReason =\n | \"no-webgl\"\n | \"reduced-motion\"\n | \"save-data\"\n | \"context-lost\"\n | \"load-error\";\n\n/** poster → loading → running, or → fallback (permanent poster). */\nexport type WaveState = \"poster\" | \"loading\" | \"running\" | \"fallback\";\n\n/** The heavy module fetched on upgrade. */\ntype CoreModule = typeof import(\"../core-loader\");\n\nexport interface WaveOptions {\n /** Poster URL / data-URI. Defaults to adopting the container's `<img data-wave3d-poster>` (SSR). */\n poster?: string;\n /** Poster `object-fit`. Default `\"fill\"` — matches the canvas (which renders edge-to-edge at the\n * container's aspect), so a poster captured at that aspect hands off with no visible jump. Use\n * `\"cover\"` to crop a different-aspect placeholder instead of stretching it. See {@link PosterFit}. */\n posterFit?: PosterFit;\n /** Wait until the container nears the viewport before fetching the engine. Default true. */\n lazy?: boolean;\n /** IntersectionObserver margin for the lazy trigger. Default \"200px\". */\n rootMargin?: string;\n /** \"auto\" probes WebGL (with failIfMajorPerformanceCaveat); \"force\" skips the probe; \"off\" stays a poster. */\n webgl?: \"auto\" | \"force\" | \"off\";\n /** Forward prefers-reduced-motion to the renderer (freezes to a full static frame). Default true. */\n respectReducedMotion?: boolean;\n /** With reduced motion: \"static\" upgrades to a frozen frame; \"poster\" stays a poster. Default \"static\". */\n reducedMotionBehavior?: \"static\" | \"poster\";\n /** Keep a permanent poster when the user has Save-Data on. Default true. */\n respectSaveData?: boolean;\n /** Poster→canvas crossfade duration (ms). Default 300. */\n fadeMs?: number;\n /** Start paused. */\n paused?: boolean;\n onReady?(renderer: WaveRenderer): void;\n onFallback?(reason: FallbackReason): void;\n onStateChange?(state: WaveState): void;\n /** Seam for the standalone/CDN build to supply the core synchronously (three already bundled). */\n loadCore?(): Promise<CoreModule>;\n}\n\nexport interface SnapshotOptions {\n /** Image MIME type. Default `\"image/webp\"`. */\n type?: string;\n /** Encoder quality 0–1 for lossy types. */\n quality?: number;\n /** Render with a transparent background. Default true. */\n transparent?: boolean;\n /** Render a fixed animation-time for a reproducible frame (default: the live frame). Poster\n * captures should pass `0` — the frame the wave opens on, so the file doesn't churn per capture. */\n time?: number;\n}\n\nexport interface WaveHandle {\n readonly state: WaveState;\n readonly renderer: WaveRenderer | null;\n /** Capture the current live frame as an image Blob (a poster you can host/cache). Resolves `null`\n * until the wave is running (poster / fallback / pre-upgrade) — wait for {@link WaveOptions.onReady}\n * (or the element's `wave3d-ready` event) first. */\n snapshot(options?: SnapshotOptions): Promise<Blob | null>;\n /** Merge a partial config. Staged before upgrade; after, setConfig() then refreshPlayback(). */\n set(config: Partial<StudioConfig>): void;\n /** Feed a `custom:<name>` interaction input for `custom:*` bindings. Staged (last value per name)\n * before upgrade and replayed once the renderer is live; a no-op if no binding consumes it. */\n setInteractionInput(name: string, value: number): void;\n play(): void;\n pause(): void;\n /** Safe to call in any state (aborts a pending upgrade, disposes a live renderer, removes the poster). */\n destroy(): void;\n}\n\n/**\n * The shell implementation. `loadCore` is an explicit parameter (not read from options) so the\n * standalone/CDN build can pass a synchronous core and NOT bundle the dynamic-import path — its\n * output stays a single file. The public {@link createWave} supplies the dynamic-import default.\n */\nexport function createWaveImpl(\n loadCore: () => Promise<CoreModule>,\n container: HTMLElement,\n config: Partial<StudioConfig>,\n options: WaveOptions,\n): WaveHandle {\n const {\n lazy = true,\n rootMargin = \"200px\",\n webgl = \"auto\",\n respectReducedMotion = true,\n reducedMotionBehavior = \"static\",\n respectSaveData = true,\n fadeMs = 300,\n } = options;\n\n let state: WaveState = \"poster\";\n let renderer: WaveRenderer | null = null;\n let staged: Partial<StudioConfig> = { ...config };\n if (options.paused !== undefined) staged.paused = options.paused;\n // Interaction inputs fed before the renderer exists — last value per name, replayed on upgrade.\n const stagedInputs = new Map<string, number>();\n\n let aborted = false;\n let io: IntersectionObserver | null = null;\n let lostTimer: ReturnType<typeof setTimeout> | undefined;\n let lossCount = 0;\n\n ensurePositioned(container);\n const poster: Poster | null = setupPoster(container, options.poster, options.posterFit);\n\n function setState(next: WaveState): void {\n if (state === next) return;\n state = next;\n options.onStateChange?.(next);\n }\n\n function fallback(reason: FallbackReason): void {\n setState(\"fallback\");\n poster?.show();\n options.onFallback?.(reason);\n }\n\n function onContextRestored(): void {\n clearTimeout(lostTimer); // three rebuilt the context in time; stay live\n }\n\n function onContextLost(): void {\n lossCount += 1;\n clearTimeout(lostTimer);\n if (lossCount >= 2) {\n teardownRenderer();\n fallback(\"context-lost\");\n return;\n }\n // three (WaveRenderer) tries to restore; if it hasn't within ~4s, give up to the poster.\n lostTimer = setTimeout(() => {\n teardownRenderer();\n fallback(\"context-lost\");\n }, 4000);\n }\n\n function teardownRenderer(): void {\n if (!renderer) return;\n const canvas = renderer.renderer.domElement;\n canvas.removeEventListener(\"webglcontextlost\", onContextLost);\n canvas.removeEventListener(\"webglcontextrestored\", onContextRestored);\n renderer.dispose();\n renderer = null;\n }\n\n async function upgrade(): Promise<void> {\n setState(\"loading\");\n let core: CoreModule;\n try {\n core = await loadCore();\n } catch {\n if (!aborted) fallback(\"load-error\");\n return;\n }\n if (aborted) return;\n\n const full: StudioConfig = { ...core.createDefaultConfig(), ...staged };\n const rendererOptions: WaveRendererOptions = { respectReducedMotion };\n renderer = new core.WaveRenderer(container, full, rendererOptions);\n const canvas = renderer.renderer.domElement;\n canvas.addEventListener(\"webglcontextlost\", onContextLost, false);\n canvas.addEventListener(\"webglcontextrestored\", onContextRestored, false);\n renderer.start();\n for (const [name, value] of stagedInputs) renderer.setInteractionInput(name, value);\n setState(\"running\");\n options.onReady?.(renderer);\n\n if (poster) {\n // Crossfade only after two frames, so the wave has definitely painted first.\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n if (!aborted && renderer) poster.fadeOut(fadeMs);\n }),\n );\n }\n }\n\n function probeAndUpgrade(): void {\n if (aborted) return;\n if (webgl === \"auto\" && !hasWebGL()) {\n fallback(\"no-webgl\");\n return;\n }\n void upgrade();\n }\n\n function begin(): void {\n // Permanent-poster gates (checked before any lazy wait or engine fetch).\n if (webgl === \"off\") return; // deliberate poster-only mode — stay \"poster\", no fallback callback\n if (respectSaveData && prefersReducedData()) return fallback(\"save-data\");\n if (respectReducedMotion && reducedMotionBehavior === \"poster\" && prefersReducedMotion()) {\n return fallback(\"reduced-motion\");\n }\n if (lazy && typeof IntersectionObserver !== \"undefined\") {\n io = new IntersectionObserver(\n (entries) => {\n if (entries.some((e) => e.isIntersecting)) {\n io?.disconnect();\n io = null;\n probeAndUpgrade();\n }\n },\n { rootMargin },\n );\n io.observe(container);\n } else {\n probeAndUpgrade();\n }\n }\n\n const handle: WaveHandle = {\n get state() {\n return state;\n },\n get renderer() {\n return renderer;\n },\n snapshot(opts = {}) {\n if (!renderer) return Promise.resolve(null);\n const { type = \"image/webp\", quality, transparent = true, time } = opts;\n return renderer.captureImage(type, transparent, quality, time);\n },\n set(next) {\n if (renderer) {\n renderer.setConfig({ ...renderer.getConfig(), ...next });\n renderer.refreshPlayback(); // setConfig doesn't re-evaluate `paused` on its own\n } else {\n staged = { ...staged, ...next };\n }\n },\n setInteractionInput(name, value) {\n if (renderer) renderer.setInteractionInput(name, value);\n else stagedInputs.set(name, value);\n },\n play() {\n if (renderer) {\n renderer.getConfig().paused = false;\n renderer.refreshPlayback();\n } else {\n staged.paused = false;\n }\n },\n pause() {\n if (renderer) {\n renderer.getConfig().paused = true;\n renderer.refreshPlayback();\n } else {\n staged.paused = true;\n }\n },\n destroy() {\n aborted = true;\n io?.disconnect();\n io = null;\n clearTimeout(lostTimer);\n teardownRenderer();\n poster?.remove();\n },\n };\n\n begin();\n return handle;\n}\n\n/**\n * Mount a self-optimizing wave into a container: shows a poster immediately, then — lazily, and only\n * when the browser can actually run it — fetches the engine, builds the renderer, and crossfades in.\n * Falls back to the poster on no-WebGL / save-data / reduced-motion / context-loss / load errors.\n * No static three import: the engine arrives via a dynamic import, so the shell stays tiny.\n */\nexport function createWave(\n container: HTMLElement,\n config: Partial<StudioConfig> = {},\n options: WaveOptions = {},\n): WaveHandle {\n return createWaveImpl(\n options.loadCore ?? (() => import(\"../core-loader\")),\n container,\n config,\n options,\n );\n}\n\n/** The drop-in embed contract: an alias of {@link createWave}. */\nexport const mountWave = createWave;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createWave.js","names":[],"sources":["../../src/shell/createWave.ts"],"sourcesContent":["import type { StudioConfig } from \"../config/model\";\nimport type { WaveRenderer, WaveRendererOptions } from \"../renderer/WaveRenderer\";\nimport type { TiltStatus } from \"../renderer/tilt\";\nimport { hasWebGL, hasWebGPU, prefersReducedMotion, prefersReducedData } from \"./probe\";\nimport { setupPoster, ensurePositioned, type Poster, type PosterFit } from \"./poster\";\n\nexport type { PosterFit } from \"./poster\";\n\n/** Why the shell showed the poster instead of a live wave. */\nexport type FallbackReason =\n | \"no-webgl\"\n | \"reduced-motion\"\n | \"save-data\"\n | \"context-lost\"\n | \"load-error\";\n\n/** poster → loading → running, or → fallback (permanent poster). */\nexport type WaveState = \"poster\" | \"loading\" | \"running\" | \"fallback\";\n\n/** The heavy module fetched on upgrade. */\ntype CoreModule = typeof import(\"../core-loader\");\n\nexport interface WaveOptions {\n /** Poster URL / data-URI. Defaults to adopting the container's `<img data-wave3d-poster>` (SSR). */\n poster?: string;\n /** Poster `object-fit`. Default `\"fill\"` — matches the canvas (which renders edge-to-edge at the\n * container's aspect), so a poster captured at that aspect hands off with no visible jump. Use\n * `\"cover\"` to crop a different-aspect placeholder instead of stretching it. See {@link PosterFit}. */\n posterFit?: PosterFit;\n /** Wait until the container nears the viewport before fetching the engine. Default true. */\n lazy?: boolean;\n /** IntersectionObserver margin for the lazy trigger. Default \"200px\". */\n rootMargin?: string;\n /** \"auto\" probes WebGL (with failIfMajorPerformanceCaveat); \"force\" skips the probe; \"off\" stays a poster. */\n webgl?: \"auto\" | \"force\" | \"off\";\n /**\n * Which renderer backend to use.\n *\n * - `\"webgl\"` (default) — the GLSL renderer. Nothing extra is downloaded.\n * - `\"webgpu\"` — the TSL renderer, which itself falls back to a WebGL2 backend where WebGPU is\n * unavailable, so it always renders.\n * - `\"auto\"` — WebGPU where an adapter can be acquired, else the GLSL renderer.\n *\n * Anything other than `\"webgl\"` fetches a separate ~197 KB (gzipped) chunk holding three's node\n * system, so it is opt-in rather than the default. See `renderer/gpu-loader.ts`.\n */\n backend?: \"webgl\" | \"webgpu\" | \"auto\";\n /** Forward prefers-reduced-motion to the renderer (freezes to a full static frame). Default true. */\n respectReducedMotion?: boolean;\n /** With reduced motion: \"static\" upgrades to a frozen frame; \"poster\" stays a poster. Default \"static\". */\n reducedMotionBehavior?: \"static\" | \"poster\";\n /** Keep a permanent poster when the user has Save-Data on. Default true. */\n respectSaveData?: boolean;\n /** Poster→canvas crossfade duration (ms). Default 300. */\n fadeMs?: number;\n /** Start paused. */\n paused?: boolean;\n onReady?(renderer: WaveRenderer): void;\n onFallback?(reason: FallbackReason): void;\n onStateChange?(state: WaveState): void;\n /** Seam for the standalone/CDN build to supply the core synchronously (three already bundled). */\n loadCore?(): Promise<CoreModule>;\n}\n\nexport interface SnapshotOptions {\n /** Image MIME type. Default `\"image/webp\"`. */\n type?: string;\n /** Encoder quality 0–1 for lossy types. */\n quality?: number;\n /** Render with a transparent background. Default true. */\n transparent?: boolean;\n /** Render a fixed animation-time for a reproducible frame (default: the live frame). Poster\n * captures should pass `0` — the frame the wave opens on, so the file doesn't churn per capture. */\n time?: number;\n}\n\nexport interface WaveHandle {\n readonly state: WaveState;\n readonly renderer: WaveRenderer | null;\n /** Capture the current live frame as an image Blob (a poster you can host/cache). Resolves `null`\n * until the wave is running (poster / fallback / pre-upgrade) — wait for {@link WaveOptions.onReady}\n * (or the element's `wave3d-ready` event) first. */\n snapshot(options?: SnapshotOptions): Promise<Blob | null>;\n /** Merge a partial config. Staged before upgrade; after, setConfig() then refreshPlayback(). */\n set(config: Partial<StudioConfig>): void;\n /** Feed a `custom:<name>` interaction input for `custom:*` bindings. Staged (last value per name)\n * before upgrade and replayed once the renderer is live; a no-op if no binding consumes it. */\n setInteractionInput(name: string, value: number): void;\n /**\n * Explicitly ask for the device-orientation sensor. OPTIONAL, and on iOS it opens a modal\n * permission dialog — nothing calls it for you, and a decorative scene should simply go without\n * tilt on that platform rather than interrupt the reader. CALL IT FROM A USER GESTURE: iOS 13+\n * grants the sensor only from inside a tap handler.\n *\n * Before the wave has upgraded there is no renderer to ask, so the request is REMEMBERED and\n * replayed on upgrade; that replay is no longer inside the gesture, so on iOS it resolves false\n * and the reader taps again. Gate the button on {@link WaveOptions.onReady} (or the element's\n * `wave3d-ready` event) and the first tap is the only tap.\n */\n enableTilt(): Promise<boolean>;\n /** Where the tilt sensor stands. `\"prompt\"` is exactly when a tap-to-enable affordance helps. */\n tiltStatus(): TiltStatus;\n /** Take the next orientation reading as the neutral pose (the reader has changed grip). */\n recenterTilt(): void;\n play(): void;\n pause(): void;\n /** Safe to call in any state (aborts a pending upgrade, disposes a live renderer, removes the poster). */\n destroy(): void;\n}\n\n/**\n * The shell implementation. `loadCore` is an explicit parameter (not read from options) so the\n * standalone/CDN build can pass a synchronous core and NOT bundle the dynamic-import path — its\n * output stays a single file. The public {@link createWave} supplies the dynamic-import default.\n */\ntype RendererCtor = new (\n container: HTMLElement,\n config: StudioConfig,\n options: WaveRendererOptions,\n) => WaveRenderer;\n\n/**\n * Fetches the TSL/WebGPU backend.\n *\n * INJECTED rather than referenced directly, so a build that cannot code-split — the single-file\n * standalone, which the studio inlines as one Blob into exported embed HTML — can supply a loader\n * that never mentions the module. Left as a default `import()` inside this file, the bundler would\n * inline the whole node system into that artifact: measured at 419 KB gzipped against 197 KB.\n */\nexport type GpuLoader = () => Promise<{ WaveRendererGPU: RendererCtor }>;\n\n/** The stub for WebGL-only builds: refuses, and the caller falls back to the GLSL renderer. */\nexport const noGpuBackend: GpuLoader = () =>\n Promise.reject(new Error(\"This build ships the WebGL renderer only.\"));\n\nexport function createWaveImpl(\n loadCore: () => Promise<CoreModule>,\n loadGpu: GpuLoader,\n container: HTMLElement,\n config: Partial<StudioConfig>,\n options: WaveOptions,\n): WaveHandle {\n /** Pick the renderer class, fetching the TSL backend only when it is actually wanted. */\n const resolveBackend = async (): Promise<RendererCtor> => {\n const glsl = async (): Promise<RendererCtor> =>\n (await loadCore()).WaveRenderer as unknown as RendererCtor;\n if (backend === \"webgl\") return glsl();\n if (backend === \"auto\" && !(await hasWebGPU())) return glsl();\n try {\n return (await loadGpu()).WaveRendererGPU;\n } catch {\n // A missing chunk or an unusable adapter is not fatal: the GLSL renderer draws the same scene.\n return glsl();\n }\n };\n const {\n lazy = true,\n rootMargin = \"200px\",\n webgl = \"auto\",\n backend = \"webgl\",\n respectReducedMotion = true,\n reducedMotionBehavior = \"static\",\n respectSaveData = true,\n fadeMs = 300,\n } = options;\n\n let state: WaveState = \"poster\";\n let renderer: WaveRenderer | null = null;\n let staged: Partial<StudioConfig> = { ...config };\n if (options.paused !== undefined) staged.paused = options.paused;\n // Interaction inputs fed before the renderer exists — last value per name, replayed on upgrade.\n const stagedInputs = new Map<string, number>();\n /** enableTilt() called before the upgrade — replayed once the renderer exists (see WaveHandle). */\n let tiltRequested = false;\n\n let aborted = false;\n let io: IntersectionObserver | null = null;\n let lostTimer: ReturnType<typeof setTimeout> | undefined;\n let lossCount = 0;\n\n ensurePositioned(container);\n const poster: Poster | null = setupPoster(container, options.poster, options.posterFit);\n\n function setState(next: WaveState): void {\n if (state === next) return;\n state = next;\n options.onStateChange?.(next);\n }\n\n function fallback(reason: FallbackReason): void {\n setState(\"fallback\");\n poster?.show();\n options.onFallback?.(reason);\n }\n\n function onContextRestored(): void {\n clearTimeout(lostTimer); // three rebuilt the context in time; stay live\n }\n\n function onContextLost(): void {\n lossCount += 1;\n clearTimeout(lostTimer);\n if (lossCount >= 2) {\n teardownRenderer();\n fallback(\"context-lost\");\n return;\n }\n // three (WaveRenderer) tries to restore; if it hasn't within ~4s, give up to the poster.\n lostTimer = setTimeout(() => {\n teardownRenderer();\n fallback(\"context-lost\");\n }, 4000);\n }\n\n function teardownRenderer(): void {\n if (!renderer) return;\n const canvas = renderer.renderer.domElement;\n canvas.removeEventListener(\"webglcontextlost\", onContextLost);\n canvas.removeEventListener(\"webglcontextrestored\", onContextRestored);\n renderer.dispose();\n renderer = null;\n }\n\n async function upgrade(): Promise<void> {\n setState(\"loading\");\n let core: CoreModule;\n try {\n core = await loadCore();\n } catch {\n if (!aborted) fallback(\"load-error\");\n return;\n }\n if (aborted) return;\n\n const full: StudioConfig = { ...core.createDefaultConfig(), ...staged };\n const rendererOptions: WaveRendererOptions = { respectReducedMotion };\n // The TSL backend lives behind its own dynamic import so `three/webgpu` never enters the eager\n // graph. If that chunk fails to load, or WebGPU turns out to be unusable under \"auto\", fall\n // back to the GLSL renderer rather than the poster — it renders the same scene either way.\n const Backend = await resolveBackend();\n renderer = new Backend(container, full, rendererOptions);\n await renderer.init(); // no-op on WebGL; starts the backend on WebGPU\n if (aborted) {\n renderer.dispose();\n renderer = null;\n return;\n }\n const canvas = renderer.renderer.domElement;\n canvas.addEventListener(\"webglcontextlost\", onContextLost, false);\n canvas.addEventListener(\"webglcontextrestored\", onContextRestored, false);\n renderer.start();\n for (const [name, value] of stagedInputs) renderer.setInteractionInput(name, value);\n if (tiltRequested) void renderer.enableTilt();\n setState(\"running\");\n options.onReady?.(renderer);\n\n if (poster) {\n // Crossfade only after two frames, so the wave has definitely painted first.\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n if (!aborted && renderer) poster.fadeOut(fadeMs);\n }),\n );\n }\n }\n\n function probeAndUpgrade(): void {\n if (aborted) return;\n if (webgl === \"auto\" && !hasWebGL()) {\n fallback(\"no-webgl\");\n return;\n }\n void upgrade();\n }\n\n function begin(): void {\n // Permanent-poster gates (checked before any lazy wait or engine fetch).\n if (webgl === \"off\") return; // deliberate poster-only mode — stay \"poster\", no fallback callback\n if (respectSaveData && prefersReducedData()) return fallback(\"save-data\");\n if (respectReducedMotion && reducedMotionBehavior === \"poster\" && prefersReducedMotion()) {\n return fallback(\"reduced-motion\");\n }\n if (lazy && typeof IntersectionObserver !== \"undefined\") {\n io = new IntersectionObserver(\n (entries) => {\n if (entries.some((e) => e.isIntersecting)) {\n io?.disconnect();\n io = null;\n probeAndUpgrade();\n }\n },\n { rootMargin },\n );\n io.observe(container);\n } else {\n probeAndUpgrade();\n }\n }\n\n const handle: WaveHandle = {\n get state() {\n return state;\n },\n get renderer() {\n return renderer;\n },\n snapshot(opts = {}) {\n if (!renderer) return Promise.resolve(null);\n const { type = \"image/webp\", quality, transparent = true, time } = opts;\n return renderer.captureImage(type, transparent, quality, time);\n },\n set(next) {\n if (renderer) {\n renderer.setConfig({ ...renderer.getConfig(), ...next });\n renderer.refreshPlayback(); // setConfig doesn't re-evaluate `paused` on its own\n } else {\n staged = { ...staged, ...next };\n }\n },\n setInteractionInput(name, value) {\n if (renderer) renderer.setInteractionInput(name, value);\n else stagedInputs.set(name, value);\n },\n enableTilt() {\n if (renderer) return renderer.enableTilt();\n tiltRequested = true;\n return Promise.resolve(false);\n },\n tiltStatus() {\n return renderer?.tiltStatus() ?? \"prompt\";\n },\n recenterTilt() {\n renderer?.recenterTilt();\n },\n play() {\n if (renderer) {\n renderer.getConfig().paused = false;\n renderer.refreshPlayback();\n } else {\n staged.paused = false;\n }\n },\n pause() {\n if (renderer) {\n renderer.getConfig().paused = true;\n renderer.refreshPlayback();\n } else {\n staged.paused = true;\n }\n },\n destroy() {\n aborted = true;\n io?.disconnect();\n io = null;\n clearTimeout(lostTimer);\n teardownRenderer();\n poster?.remove();\n },\n };\n\n begin();\n return handle;\n}\n\n/**\n * Mount a self-optimizing wave into a container: shows a poster immediately, then — lazily, and only\n * when the browser can actually run it — fetches the engine, builds the renderer, and crossfades in.\n * Falls back to the poster on no-WebGL / save-data / reduced-motion / context-loss / load errors.\n * No static three import: the engine arrives via a dynamic import, so the shell stays tiny.\n */\nexport function createWave(\n container: HTMLElement,\n config: Partial<StudioConfig> = {},\n options: WaveOptions = {},\n): WaveHandle {\n return createWaveImpl(\n options.loadCore ?? (() => import(\"../core-loader\")),\n () => import(\"../renderer/gpu-loader\"),\n container,\n config,\n options,\n );\n}\n\n/** The drop-in embed contract: an alias of {@link createWave}. */\nexport const mountWave = createWave;\n"],"mappings":"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"}
|
package/dist/shell/probe.js
CHANGED
|
@@ -19,6 +19,23 @@ function hasWebGL() {
|
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether the browser exposes a usable WebGPU adapter.
|
|
24
|
+
*
|
|
25
|
+
* Async, unlike {@link hasWebGL}: requesting an adapter is inherently asynchronous, and
|
|
26
|
+
* `navigator.gpu` being present does not mean one can be acquired. Also note WebGPU is only exposed
|
|
27
|
+
* to a SECURE CONTEXT — on `about:blank` or plain http, `navigator.gpu` is `undefined` even in a
|
|
28
|
+
* browser that fully supports it.
|
|
29
|
+
*/
|
|
30
|
+
async function hasWebGPU() {
|
|
31
|
+
const gpu = navigator.gpu;
|
|
32
|
+
if (typeof navigator === "undefined" || !gpu) return false;
|
|
33
|
+
try {
|
|
34
|
+
return await gpu.requestAdapter() !== null;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
22
39
|
/** True when the OS/browser is set to reduce motion. */
|
|
23
40
|
function prefersReducedMotion() {
|
|
24
41
|
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
@@ -29,6 +46,6 @@ function prefersReducedData() {
|
|
|
29
46
|
return navigator.connection?.saveData === true;
|
|
30
47
|
}
|
|
31
48
|
//#endregion
|
|
32
|
-
export { hasWebGL, prefersReducedData, prefersReducedMotion };
|
|
49
|
+
export { hasWebGL, hasWebGPU, prefersReducedData, prefersReducedMotion };
|
|
33
50
|
|
|
34
51
|
//# sourceMappingURL=probe.js.map
|
package/dist/shell/probe.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"probe.js","names":[],"sources":["../../src/shell/probe.ts"],"sourcesContent":["// WebGL capability probe for the shell. Kept dependency-free (no three) so it can run before the\n// heavy renderer chunk is fetched — the shell decides poster-vs-upgrade from this.\n\n/**\n * Synchronously test whether the browser can give us a usable WebGL context. Uses\n * `failIfMajorPerformanceCaveat` so a software/blocklisted renderer (which would run the wave at a\n * slideshow framerate) reports as unavailable and we keep the poster. Releases the throwaway\n * context immediately via WEBGL_lose_context so probing doesn't consume one of the browser's ~16\n * live contexts.\n */\nexport function hasWebGL(): boolean {\n if (typeof document === \"undefined\") return false;\n try {\n const canvas = document.createElement(\"canvas\");\n const attrs: WebGLContextAttributes = { failIfMajorPerformanceCaveat: true };\n const gl =\n canvas.getContext(\"webgl2\", attrs) ??\n (canvas.getContext(\"webgl\", attrs) as WebGLRenderingContext | null);\n if (!gl) return false;\n gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n return true;\n } catch {\n return false;\n }\n}\n\n/** True when the OS/browser is set to reduce motion. */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n\n/** True when the user has asked for reduced data usage (Save-Data / Data Saver). */\nexport function prefersReducedData(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const conn = (navigator as Navigator & { connection?: { saveData?: boolean } }).connection;\n return conn?.saveData === true;\n}\n"],"mappings":";;;;;;;;AAUA,SAAgB,WAAoB;CAClC,IAAI,OAAO,aAAa,aAAa,OAAO;CAC5C,IAAI;EACF,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,MAAM,QAAgC,EAAE,8BAA8B,KAAK;EAC3E,MAAM,KACJ,OAAO,WAAW,UAAU,KAAK,KAChC,OAAO,WAAW,SAAS,KAAK;EACnC,IAAI,CAAC,IAAI,OAAO;EAChB,GAAG,aAAa,oBAAoB,CAAC,EAAE,YAAY;EACnD,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,uBAAgC;CAC9C,OACE,OAAO,WAAW,eAClB,OAAO,OAAO,eAAe,cAC7B,OAAO,WAAW,kCAAkC,CAAC,CAAC;AAE1D;;AAGA,SAAgB,qBAA8B;CAC5C,IAAI,OAAO,cAAc,aAAa,OAAO;CAE7C,OADc,UAAkE,YACnE,aAAa;AAC5B"}
|
|
1
|
+
{"version":3,"file":"probe.js","names":[],"sources":["../../src/shell/probe.ts"],"sourcesContent":["// WebGL capability probe for the shell. Kept dependency-free (no three) so it can run before the\n// heavy renderer chunk is fetched — the shell decides poster-vs-upgrade from this.\n\n/**\n * Synchronously test whether the browser can give us a usable WebGL context. Uses\n * `failIfMajorPerformanceCaveat` so a software/blocklisted renderer (which would run the wave at a\n * slideshow framerate) reports as unavailable and we keep the poster. Releases the throwaway\n * context immediately via WEBGL_lose_context so probing doesn't consume one of the browser's ~16\n * live contexts.\n */\nexport function hasWebGL(): boolean {\n if (typeof document === \"undefined\") return false;\n try {\n const canvas = document.createElement(\"canvas\");\n const attrs: WebGLContextAttributes = { failIfMajorPerformanceCaveat: true };\n const gl =\n canvas.getContext(\"webgl2\", attrs) ??\n (canvas.getContext(\"webgl\", attrs) as WebGLRenderingContext | null);\n if (!gl) return false;\n gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Whether the browser exposes a usable WebGPU adapter.\n *\n * Async, unlike {@link hasWebGL}: requesting an adapter is inherently asynchronous, and\n * `navigator.gpu` being present does not mean one can be acquired. Also note WebGPU is only exposed\n * to a SECURE CONTEXT — on `about:blank` or plain http, `navigator.gpu` is `undefined` even in a\n * browser that fully supports it.\n */\nexport async function hasWebGPU(): Promise<boolean> {\n const gpu = (navigator as Navigator & { gpu?: { requestAdapter(): Promise<unknown> } }).gpu;\n if (typeof navigator === \"undefined\" || !gpu) return false;\n try {\n return (await gpu.requestAdapter()) !== null;\n } catch {\n return false;\n }\n}\n\n/** True when the OS/browser is set to reduce motion. */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n\n/** True when the user has asked for reduced data usage (Save-Data / Data Saver). */\nexport function prefersReducedData(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const conn = (navigator as Navigator & { connection?: { saveData?: boolean } }).connection;\n return conn?.saveData === true;\n}\n"],"mappings":";;;;;;;;AAUA,SAAgB,WAAoB;CAClC,IAAI,OAAO,aAAa,aAAa,OAAO;CAC5C,IAAI;EACF,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,MAAM,QAAgC,EAAE,8BAA8B,KAAK;EAC3E,MAAM,KACJ,OAAO,WAAW,UAAU,KAAK,KAChC,OAAO,WAAW,SAAS,KAAK;EACnC,IAAI,CAAC,IAAI,OAAO;EAChB,GAAG,aAAa,oBAAoB,CAAC,EAAE,YAAY;EACnD,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;AAUA,eAAsB,YAA8B;CAClD,MAAM,MAAO,UAA2E;CACxF,IAAI,OAAO,cAAc,eAAe,CAAC,KAAK,OAAO;CACrD,IAAI;EACF,OAAQ,MAAM,IAAI,eAAe,MAAO;CAC1C,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,uBAAgC;CAC9C,OACE,OAAO,WAAW,eAClB,OAAO,OAAO,eAAe,cAC7B,OAAO,WAAW,kCAAkC,CAAC,CAAC;AAE1D;;AAGA,SAAgB,qBAA8B;CAC5C,IAAI,OAAO,cAAc,aAAa,OAAO;CAE7C,OADc,UAAkE,YACnE,aAAa;AAC5B"}
|