@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.2

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.
Files changed (218) hide show
  1. package/CLAUDE.md +87 -0
  2. package/LICENSE.md +664 -0
  3. package/README.md +84 -2
  4. package/dist/dist-manifest.json +218 -0
  5. package/dist/eyes/index.js +109 -0
  6. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  7. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  8. package/dist/fat/pkg-node/package.json +12 -0
  9. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  10. package/dist/frqtl/compat/boot-node.js +34 -0
  11. package/dist/frqtl/compat/engine.js +127 -0
  12. package/dist/frqtl/compat/package-root.js +24 -0
  13. package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
  14. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  15. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  16. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  17. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  18. package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
  19. package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
  20. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  21. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  22. package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
  23. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  24. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  25. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  26. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  27. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  28. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  30. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  31. package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
  32. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  33. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  34. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  35. package/dist/frqtl/compat/vendor.js +196 -0
  36. package/dist/frqtl/node-entry.js +17 -0
  37. package/dist/frqtl/objects/FrqtlApp.js +2920 -0
  38. package/dist/frqtl/objects/ObjectFactory.js +296 -0
  39. package/dist/frqtl/objects/RecipeContext.js +322 -0
  40. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  41. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  42. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  43. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  44. package/dist/frqtl/objects/instruments/capture.js +710 -0
  45. package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
  46. package/dist/frqtl/objects/kinds-entry.js +14 -0
  47. package/dist/frqtl/objects/kinds.js +190 -0
  48. package/dist/frqtl/objects/objects/Box.js +273 -0
  49. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  50. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  51. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  52. package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
  53. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  54. package/dist/frqtl/objects/objects/LightSource.js +351 -0
  55. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  56. package/dist/frqtl/objects/objects/Tube.js +471 -0
  57. package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
  58. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  59. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  60. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  61. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  62. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  66. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  67. package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
  68. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  69. package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
  70. package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
  71. package/dist/frqtl/objects/objects/tagger.js +228 -0
  72. package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
  73. package/dist/frqtl/objects/schema/index.js +256 -0
  74. package/dist/frqtl/objects/schema/scopes.js +457 -0
  75. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  76. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  77. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  78. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  79. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  80. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  81. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  82. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  83. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
  85. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  86. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  87. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  88. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  89. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  90. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  91. package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
  92. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  94. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  95. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
  96. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  97. package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
  98. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  99. package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
  100. package/dist/frqtl/objects/v1-replacements.js +29 -0
  101. package/dist/frqtl/package-core.js +23 -0
  102. package/dist/frqtl/parts/CameraControls.js +643 -0
  103. package/dist/frqtl/parts/CameraPath.js +575 -0
  104. package/dist/frqtl/parts/CollisionActions.js +34 -0
  105. package/dist/frqtl/parts/CollisionTypes.js +30 -0
  106. package/dist/frqtl/parts/DetectorWall.js +496 -0
  107. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  108. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  109. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  110. package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
  111. package/dist/frqtl/parts/LargeObject.js +405 -0
  112. package/dist/frqtl/parts/LightColor.js +255 -0
  113. package/dist/frqtl/parts/LiveChart.js +285 -0
  114. package/dist/frqtl/parts/LogicGate.js +717 -0
  115. package/dist/frqtl/parts/OvenBox.js +611 -0
  116. package/dist/frqtl/parts/PointSource.js +762 -0
  117. package/dist/frqtl/parts/Rectangle.js +200 -0
  118. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  119. package/dist/frqtl/parts/SlitScreen.js +628 -0
  120. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  121. package/dist/frqtl/parts/Trace.js +508 -0
  122. package/dist/frqtl/parts/Tunnel.js +1057 -0
  123. package/dist/frqtl/parts/Wall.js +1211 -0
  124. package/dist/frqtl/parts/index.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  126. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  127. package/dist/frqtl/scenes/atomic-model.js +88 -0
  128. package/dist/frqtl/scenes/big-bang.js +46 -0
  129. package/dist/frqtl/scenes/blackbody.js +81 -0
  130. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  131. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  132. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  133. package/dist/frqtl/scenes/counting-wall.js +38 -0
  134. package/dist/frqtl/scenes/double-slit.js +31 -0
  135. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  136. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  137. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  138. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
  139. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  140. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  141. package/dist/frqtl/scenes/empty.js +14 -0
  142. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  143. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  144. package/dist/frqtl/scenes/mirror-route.js +140 -0
  145. package/dist/frqtl/scenes/mirror.js +38 -0
  146. package/dist/frqtl/scenes/omni-3d.js +27 -0
  147. package/dist/frqtl/scenes/omni-source.js +22 -0
  148. package/dist/frqtl/scenes/self-interference.js +39 -0
  149. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  150. package/dist/frqtl/scenes/straight-walk.js +22 -0
  151. package/dist/frqtl/scenes/sun.js +39 -0
  152. package/dist/frqtl/scenes/two-source.js +37 -0
  153. package/dist/frqtl/scenes/wave.js +26 -0
  154. package/dist/shim/bundle/frqnb.js +112 -0
  155. package/dist/shim/bundle/index.js +121 -0
  156. package/dist/shim/bundle/indexHtml.js +107 -0
  157. package/dist/shim/bundle/manifest.js +95 -0
  158. package/dist/shim/bundle/zip.js +243 -0
  159. package/dist/shim/bus.js +67 -0
  160. package/dist/shim/controls/index.js +627 -0
  161. package/dist/shim/controls/registry.js +274 -0
  162. package/dist/shim/engine/artifact-shell.js +211 -0
  163. package/dist/shim/engine/boot-phases.js +44 -0
  164. package/dist/shim/engine/engine-stack-boot.js +59 -0
  165. package/dist/shim/engine/router.js +690 -0
  166. package/dist/shim/experiment-frame/index.js +296 -0
  167. package/dist/shim/expr/ast-schema.js +369 -0
  168. package/dist/shim/field-descriptor/index.js +325 -0
  169. package/dist/shim/frame-data/index.js +224 -0
  170. package/dist/shim/index.js +67 -0
  171. package/dist/shim/notebook/canvas-guard.js +82 -0
  172. package/dist/shim/notebook/catalog.js +35 -0
  173. package/dist/shim/notebook/format.js +736 -0
  174. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  175. package/dist/shim/notebook/kernel.js +671 -0
  176. package/dist/shim/notebook/knob.js +150 -0
  177. package/dist/shim/notebook/module-resolver.js +260 -0
  178. package/dist/shim/notebook/notebook-knobs.js +65 -0
  179. package/dist/shim/notebook/produced-bindings.js +67 -0
  180. package/dist/shim/notebook/realm-registry.js +121 -0
  181. package/dist/shim/notebook/realm-transport.js +73 -0
  182. package/dist/shim/notebook/setup-builder.js +507 -0
  183. package/dist/shim/notebook/store.js +159 -0
  184. package/dist/shim/notebook/vendor-importmap.js +134 -0
  185. package/dist/shim/notebook/view-builder.js +189 -0
  186. package/dist/shim/notebook/view-ctx.js +100 -0
  187. package/dist/shim/notebook/view-handle.js +202 -0
  188. package/dist/shim/notebook/warmup.js +102 -0
  189. package/dist/shim/package.json +4 -0
  190. package/dist/shim/page-params/index.js +213 -0
  191. package/dist/shim/project/identity.js +112 -0
  192. package/dist/shim/project/index.js +19 -0
  193. package/dist/shim/project/link.js +402 -0
  194. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  195. package/dist/shim/project/register-node.mjs +17 -0
  196. package/dist/shim/project/specifiers.js +106 -0
  197. package/dist/shim/registry/copy-gate.js +110 -0
  198. package/dist/shim/registry/exhibits.js +455 -0
  199. package/dist/shim/registry/experiments.js +215 -0
  200. package/dist/shim/registry/index.js +19 -0
  201. package/dist/shim/registry/scene-source.js +157 -0
  202. package/dist/shim/registry/walkthrough.js +804 -0
  203. package/dist/shim/render/capture-envelope.js +279 -0
  204. package/dist/shim/render/heat-colormap.js +92 -0
  205. package/dist/shim/render/render-stages.js +855 -0
  206. package/dist/shim/render/sun-look.js +33 -0
  207. package/dist/shim/render-state/apply.js +262 -0
  208. package/dist/shim/render-state/index.js +74 -0
  209. package/dist/shim/rpc/index.js +207 -0
  210. package/dist/shim/runtime/index.js +1023 -0
  211. package/dist/shim/storage/boot.js +69 -0
  212. package/dist/shim/storage/index.js +169 -0
  213. package/dist/shim/storage/local-storage-provider.js +230 -0
  214. package/dist/shim/storage/make-namespace-store.js +132 -0
  215. package/dist/shim/storage/modules.js +249 -0
  216. package/dist/shim/storage/projects.js +150 -0
  217. package/llms.txt +12 -0
  218. package/package.json +63 -4
@@ -0,0 +1,861 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * p5Provider — RendererProvider backed by p5.js 2.x (global mode).
5
+ *
6
+ * Implements the RendererProvider interface (renderer-provider-contract.js):
7
+ * init({ parentEl, width, height }) → HTMLCanvasElement (parentEl REQUIRED: requireMountHost)
8
+ * renderTick(tickData, largeObjects)
9
+ * filter(filterSpec) / removeFilter(name)
10
+ * setSize(width, height)
11
+ * start() / stop()
12
+ * destroy()
13
+ * raw()
14
+ * captureStill() — OPTIONAL (the `composite` stage tap; 2D-canvas readback)
15
+ *
16
+ * Design:
17
+ * - Operates in p5 global mode: p5 installs background/fill/etc. as globals (compat/vendor.js loads it).
18
+ * - FrqtlApp owns the RAF loop and calls renderTick() each draw frame.
19
+ * - The provider DOES NOT own the loop (start/stop are no-ops).
20
+ * - canvas creation happens in init() called from p5's setup() context.
21
+ * - filter() queues a post-process pass applied after each renderTick().
22
+ * - Post-process passes use WebGL2 on a secondary offscreen canvas and blit back.
23
+ *
24
+ * p5 2.x vs 0.6.0 compatibility:
25
+ * API surface: background, fill, noStroke, stroke, strokeWeight, noFill, ellipse,
26
+ * rect, createCanvas, loop, noLoop, frameRate — unchanged between 0.6.0 and 2.x.
27
+ * createCanvas() returns a p5.Element; .parent(element) still works.
28
+ * p5.dom is built into the main bundle in 2.x (no separate script tag needed).
29
+ */
30
+
31
+ import * as Camera2dGeometry from '../camera2d-geometry.js';
32
+ import * as CaptureStillCommon from '../captureStill-common.js';
33
+ import { FieldBuilder } from '../FieldBuilder.js';
34
+ import * as FieldDescriptorSchema from '../../../../shim/field-descriptor/index.js';
35
+ import { Filters } from '../Filters.js';
36
+ import * as FrameDataSchema from '../../../../shim/frame-data/index.js';
37
+ import { LightColor } from '../../../parts/LightColor.js';
38
+ import * as OrbitBag from '../orbit-bag.js';
39
+ import * as RenderCapabilitiesCommon from '../renderCapabilities-common.js';
40
+ import { RenderShape } from '../RenderShape.js';
41
+ import * as RendererProviderContract from '../renderer-provider-contract.js';
42
+
43
+
44
+ var VERT_SRC = [
45
+ '#version 300 es',
46
+ 'in vec2 aPosition;',
47
+ 'out vec2 vTexCoord;',
48
+ 'void main() {',
49
+ ' vTexCoord = aPosition * 0.5 + 0.5;',
50
+ ' gl_Position = vec4(aPosition, 0.0, 1.0);',
51
+ '}'
52
+ ].join('\n');
53
+
54
+ // ── Shader helpers ──────────────────────────────────────────────────────
55
+
56
+ function compileShader(gl, type, src) {
57
+ var sh = gl.createShader(type);
58
+ gl.shaderSource(sh, src);
59
+ gl.compileShader(sh);
60
+ if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
61
+ console.error('[p5-provider] shader compile error:', gl.getShaderInfoLog(sh));
62
+ gl.deleteShader(sh);
63
+ return null;
64
+ }
65
+ return sh;
66
+ }
67
+
68
+ function compileProgram(gl, vertSrc, fragSrc) {
69
+ var vs = compileShader(gl, gl.VERTEX_SHADER, vertSrc);
70
+ var fs = compileShader(gl, gl.FRAGMENT_SHADER, fragSrc);
71
+ if (!vs || !fs) return null;
72
+ var prog = gl.createProgram();
73
+ gl.attachShader(prog, vs);
74
+ gl.attachShader(prog, fs);
75
+ gl.linkProgram(prog);
76
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
77
+ console.error('[p5-provider] shader link error:', gl.getProgramInfoLog(prog));
78
+ return null;
79
+ }
80
+ return prog;
81
+ }
82
+
83
+ // Convert a GLSL 1.0-style fragment source to GLSL 300 es.
84
+ // Only used when the caller provides a glsl field without a version directive.
85
+ function upgradeGlsl(src) {
86
+ // Strip declarations we re-emit at the top (uniforms, varyings, precision)
87
+ var body = src
88
+ .replace(/precision\s+\w+\s+float\s*;/g, '')
89
+ .replace(/uniform\s+sampler2D\s+\w+\s*;/g, '')
90
+ .replace(/uniform\s+vec2\s+\w+\s*;/g, '')
91
+ .replace(/uniform\s+float\s+\w+\s*;/g, '')
92
+ .replace(/varying\s+vec2\s+\w+\s*;/g, '')
93
+ .replace(/texture2D\s*\(/g, 'texture(')
94
+ .replace(/gl_FragColor/g, 'fragColor');
95
+ return [
96
+ '#version 300 es',
97
+ 'precision highp float;',
98
+ 'out vec4 fragColor;',
99
+ 'uniform sampler2D uField;',
100
+ 'uniform sampler2D uScene;',
101
+ 'uniform vec2 uResolution;',
102
+ 'uniform float uTime;',
103
+ 'in vec2 vTexCoord;',
104
+ body
105
+ ].join('\n');
106
+ }
107
+
108
+ // ── renderCapabilities() (OPTIONAL contract method — S5a) ────────────────
109
+ //
110
+ // Studio Controls Realignment §7 r2 item 2/4: renderCapabilities() is an
111
+ // OPTIONAL RendererProvider method (the getCameraControl() precedent — absent
112
+ // ⇒ makeControlsForRenderSurface emits no render controls, never an error).
113
+ // Declares ONLY what THIS provider actually supports (condition
114
+ // "per-provider truth" — no schema-mirror registry claiming a capability a
115
+ // given backend can't honor). p5 supports the THREE universal knobs
116
+ // (shape/colorScheme/renderBaseCanvas — README §3: "both p5 and three
117
+ // providers honor this identically") plus every Filters.js entry (filter()/
118
+ // removeFilter() are REQUIRED contract methods on every provider) — it does
119
+ // NOT declare the three-only effect-layer knobs (godrays/glow/heat/smoke —
120
+ // p5 has no such render path; grep confirms).
121
+ //
122
+ // Defined as a MODULE-LEVEL free function (not inside makeP5Provider's
123
+ // per-instance closure) so it needs no live p5/WebGL context — capability
124
+ // DESCRIPTORS are static metadata (name/type/constraint/unit/default);
125
+ // live VALUES are read separately via FrqtlApp.getRenderOption /
126
+ // isFilterActive. This is what makes it unit-testable in plain Node.
127
+ // S5b DRY tidy (the 3
128
+ // universal descriptors + the filter loop were hand-copied between this
129
+ // file and three-provider.js): the ONE shared owner is now
130
+ // `renderCapabilities-common.js` — see that file's own docstring.
131
+
132
+
133
+ // THE 2D camera map (pan/zoom). Pure + backend-free, so the provider stays thin and the
134
+ // SAME map serves the offline unprojectStill — one authority, live and offline.
135
+
136
+
137
+ // THE one field source (FieldBuilder.buildHeightField). An absent FieldBuilder is a load-order
138
+ // fault, never an all-zero field — the same rule capture.js's `field` tap throws on.
139
+
140
+ // THE point-shape authority for the pose bag (`{x,y,z}` AND `[x,y,z]`, one identical
141
+ // throw under every provider). This provider re-validates NO point field — a second
142
+ // opinion about a shape the contract owns is exactly the drift orbit-bag.js closes.
143
+
144
+ function _renderCapabilitiesP5() {
145
+ var normalizeFieldDescriptor = FieldDescriptorSchema.normalizeFieldDescriptor;
146
+ var common = RenderCapabilitiesCommon;
147
+ var FiltersRef = Filters;
148
+ return common.universalRenderCapabilities(
149
+ normalizeFieldDescriptor,
150
+ RenderShape,
151
+ FrameDataSchema
152
+ ).concat(common.filterRenderCapabilities(normalizeFieldDescriptor, FiltersRef));
153
+ }
154
+
155
+ // ── Factory ─────────────────────────────────────────────────────────────
156
+
157
+ function makeP5Provider() {
158
+ // The runner's physics rect, stated by init({width, height}) / setSize(w, h). null until
159
+ // then: every read goes through _physW()/_physH(), which THROW before it is stated — a
160
+ // provider has no size of its own (RENDERER_CONTRACT §1 init row).
161
+ var _width = null;
162
+ var _height = null;
163
+ function _unsized() {
164
+ return new Error('p5Provider: the physics rect was read before init({width, height}) or ' +
165
+ 'setSize(w, h) stated it. FrqtlApp.createCanvas hands the runner\'s canvas_rect to ' +
166
+ 'provider.init — the provider has no default size.');
167
+ }
168
+ function _physW() { if (_width === null) { throw _unsized(); } return _width; }
169
+ function _physH() { if (_height === null) { throw _unsized(); } return _height; }
170
+ var _filters = []; // ordered list of active filterSpec objects
171
+ var _initialized = false;
172
+ var _p5Canvas = null; // p5.Element returned by createCanvas()
173
+ var _filterState = {}; // per-filter compiled WebGL2 state cache
174
+ var _lastTickData = null; // latest tickData for field-texture derivation
175
+ // The renderBaseCanvas value that the LAST composite actually ran with. Recorded
176
+ // at the point of compositing (not read from the runner later) because this
177
+ // provider is the authority for what it composited — a capture's provenance must
178
+ // state what produced the pixels, not what the option happens to say afterwards
179
+ // (RENDERER_CONTRACT §1a.4: no caller-supplied provenance). null until the first
180
+ // renderTick, which is exactly the "nothing composited yet" case.
181
+ var _lastRenderBaseCanvas = null;
182
+ // The `uTime` every filter pass of the LAST composite drew with (the contract's
183
+ // filterTime of the rendered tick), recorded where it is fed for the same reason;
184
+ // null when no filter ran.
185
+ var _lastFilterTime = null;
186
+ // The 2D camera pose — the DEGENERATE orbit bag {distance, target}. Initialised lazily
187
+ // (init() supplies the real dims). Null target = canvas centre; distance = visible
188
+ // physics-px height. This is CAMERA state, not a render option: every still stamps it
189
+ // as `pose`, so a zoomed 2D capture is self-describing instead of silently mislabeled.
190
+ var _pose = null;
191
+ function _poseOrDefault() {
192
+ if (!_pose) { _pose = Camera2dGeometry.defaultPose2d(_physW(), _physH()); }
193
+ return _pose;
194
+ }
195
+ // The whole 2D pose bag, COPIED — never the live object (a captured pose must not keep
196
+ // tracking the camera; the still-provenance aliasing discipline): what getOrbit()
197
+ // returns and what every pose-change listener receives.
198
+ function _poseBag() {
199
+ var p = _poseOrDefault();
200
+ return { distance: p.distance, target: p.target ? { x: p.target.x, y: p.target.y } : null };
201
+ }
202
+ // The pose-change listener list (OrbitBag.makePoseNotice), provider-scoped so every
203
+ // handle getCameraControl() returns shares it. Created on the first subscription; a
204
+ // pose write (setOrbit, setPreset, a resize) notifies it.
205
+ var _poseNotice = null;
206
+ function _notifyPose() { if (_poseNotice) { _poseNotice.notify(_poseBag()); } }
207
+
208
+ // ── Layer helpers ───────────────────────────────────────────────────
209
+ //
210
+ // Layers are a RENDERER-SIDE organizing pattern over the per-tick handle
211
+ // (tickData.frameData + largeObjects). There is NO engine concept of a
212
+ // layer and NO spec-side layer descriptor — the structure is intrinsic to
213
+ // the handle: each quantum already self-identifies its dimension via
214
+ // FrameData.layer, and largeObjects carry their own render attributes
215
+ // (visible/color/alpha/drawAsCircle/backdrop/image). Each helper below
216
+ // walks the handle and draws one named layer. The IDE Layers panel reflects
217
+ // these by reading dimensionData ∪ largeObjects and toggles them via the
218
+ // per-object .visible flag / renderBaseCanvas — no new contract.
219
+ //
220
+ // p5 draws the layers here; three/custom implement the SAME named layers in
221
+ // their own renderTick. Swapping renderers never touches this file — only
222
+ // the RendererProvider interface + the handle are shared.
223
+
224
+ /**
225
+ * Backdrop layer: non-colliding LargeObjects carrying an image, drawn first
226
+ * (under everything). The image is a p5.Image / HTMLImageElement loaded by
227
+ * the experiment page; the renderer only composites it into the LO's rect.
228
+ */
229
+ function _layerBackdrop(largeObjects) {
230
+ if (!largeObjects || typeof image !== 'function') return;
231
+ for (var i = 0; i < largeObjects.length; i++) {
232
+ var lo = largeObjects[i];
233
+ if (lo.visible === false || !lo.backdrop || !lo.image) continue;
234
+ image(lo.image, lo.rect.left, lo.rect.top, lo.rect.w, lo.rect.h);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Bodies layer: structural LargeObjects (walls, detectors, blocks) — the
240
+ * colliding sprites. Backdrops are skipped here (drawn in _layerBackdrop).
241
+ */
242
+ function _layerBodies(largeObjects) {
243
+ if (!largeObjects) return;
244
+ for (var i = 0; i < largeObjects.length; i++) {
245
+ var lo = largeObjects[i];
246
+ if (lo.visible === false || lo.backdrop) continue;
247
+ var loa = (lo.alpha !== undefined) ? lo.alpha : 255;
248
+ // LiveChart: composite the ferried OFFSCREEN Chart.js canvas as an in-scene
249
+ // image panel (the R5 ferry contract). The renderer only wraps the canvas
250
+ // element — Chart.js never enters the renderer. lo.liveCanvas is an accessor
251
+ // returning the CURRENT element (it churns each redraw). drawingContext.drawImage
252
+ // is the safe cross-version path (p5's image() may reject a raw canvas); a colored
253
+ // panel shell draws until the first chart render lands.
254
+ if (lo.liveCanvas) {
255
+ var _lcv = (typeof lo.liveCanvas === 'function') ? lo.liveCanvas() : lo.liveCanvas;
256
+ var _drewLive = false;
257
+ push();
258
+ if (_lcv && typeof drawingContext !== 'undefined' && drawingContext &&
259
+ typeof drawingContext.drawImage === 'function') {
260
+ var _pa = drawingContext.globalAlpha;
261
+ drawingContext.globalAlpha = Math.max(0, Math.min(1, loa / 255));
262
+ try {
263
+ drawingContext.drawImage(_lcv, lo.rect.left, lo.rect.top, lo.rect.w, lo.rect.h);
264
+ _drewLive = true;
265
+ } catch (e) { /* pre-layout frame — fall through to the panel shell */ }
266
+ drawingContext.globalAlpha = _pa;
267
+ } else if (_lcv && typeof image === 'function') {
268
+ try { image(_lcv, lo.rect.left, lo.rect.top, lo.rect.w, lo.rect.h); _drewLive = true; }
269
+ catch (e) { /* fall through */ }
270
+ }
271
+ if (!_drewLive) {
272
+ noStroke();
273
+ fill(lo.color[0], lo.color[1], lo.color[2], loa);
274
+ rect(lo.rect.left, lo.rect.top, lo.rect.w, lo.rect.h);
275
+ }
276
+ pop();
277
+ continue;
278
+ }
279
+ // Shell SHAPE (R2-3, capability-based, zero engine change): a per-LO
280
+ // shellShape (ferried render.shape vocab) picks the drawn primitive — box/cube →
281
+ // rect, ball/circle/sphere → ellipse (cube/sphere degrade to box/ball in 2D).
282
+ // Absent → the legacy drawAsCircle boolean (byte-identical for every prior LO).
283
+ var _asCircle;
284
+ if (typeof lo.shellShape === 'string' && lo.shellShape) {
285
+ _asCircle = (lo.shellShape !== 'box' && lo.shellShape !== 'cube');
286
+ } else {
287
+ _asCircle = !!lo.drawAsCircle;
288
+ }
289
+ var _cx = lo.rect.left + lo.rect.w / 2, _cy = lo.rect.top + lo.rect.h / 2;
290
+ // R2 — VISIBLE ROTATION: a ferried orientationDeg (Wall sugar) rotates the drawn
291
+ // shell about its center. The drawn face is the wall: a rotated wall draws NO
292
+ // outline of its un-rotated engine rect. orientation 0 ⇒ the legacy axis-aligned
293
+ // draw, byte-identical (SU1). NOTE: lo.rect is the wrapper's TRUE-slab rect (the
294
+ // broad-phase-superset expansion applies only to the ENGINE's registered rect).
295
+ var _deg = (typeof lo.orientationDeg === 'number') ? (((lo.orientationDeg % 360) + 360) % 360) : 0;
296
+ if (_deg !== 0) {
297
+ // the rotated shell face (drawn centered under the rotation transform).
298
+ push();
299
+ noStroke();
300
+ fill(lo.color[0], lo.color[1], lo.color[2], loa);
301
+ translate(_cx, _cy);
302
+ rotate(radians(_deg));
303
+ if (_asCircle) { ellipse(0, 0, lo.rect.w, lo.rect.h); }
304
+ else { rect(-lo.rect.w / 2, -lo.rect.h / 2, lo.rect.w, lo.rect.h); }
305
+ pop();
306
+ continue;
307
+ }
308
+ noStroke();
309
+ fill(lo.color[0], lo.color[1], lo.color[2], loa);
310
+ if (_asCircle) {
311
+ ellipse(_cx, _cy, lo.rect.w, lo.rect.h);
312
+ } else {
313
+ rect(lo.rect.left, lo.rect.top, lo.rect.w, lo.rect.h);
314
+ }
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Particle layers: leaf-frame quanta grouped by their self-organizing
320
+ * .layer (the dimension number the engine assigns — FrameData.layer),
321
+ * sorted ascending so higher dimensions composite on top. Order WITHIN a
322
+ * layer is preserved, so single-dimension experiments (wave-interference)
323
+ * render byte-identically to the pre-refactor frame loop.
324
+ *
325
+ * @param {object} tickData
326
+ * @param {object} renderOpts — the runner's _renderOptions bag (setRenderOption).
327
+ */
328
+ function _layerParticles(tickData, renderOpts) {
329
+ if (!tickData || !tickData.frameData) return;
330
+ // LightColor + RenderShape are MANDATORY canonical contracts (the single color/shape
331
+ // sources). A missing one is a hard load-order/packaging error — FAIL LOUD, never a
332
+ // silent fallback to white/'ball' (Invariant 2).
333
+ var lc = LightColor;
334
+ var rs = RenderShape;
335
+
336
+ var byLayer = {};
337
+ var order = [];
338
+ for (var j = 0; j < tickData.frameData.length; j++) {
339
+ var L = tickData.frameData[j].layer;
340
+ var key = (L === undefined || L === null) ? 0 : L;
341
+ if (!byLayer[key]) { byLayer[key] = []; order.push(key); }
342
+ byLayer[key].push(tickData.frameData[j]);
343
+ }
344
+ order.sort(function (a, b) { return a - b; });
345
+
346
+ for (var oi = 0; oi < order.length; oi++) {
347
+ var group = byLayer[order[oi]];
348
+ for (var k = 0; k < group.length; k++) { _drawQuantum(group[k], lc, rs, renderOpts); }
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Draw a single quantum.
354
+ *
355
+ * Color priority:
356
+ * render.color (exact CSV r,g,b,a)
357
+ * > render.colorScheme per-frame key in string_map (LightColor ramp)
358
+ * > renderOpts.colorScheme default (LightColor ramp)
359
+ * > 'warmcool' default
360
+ *
361
+ * Shape priority: RenderShape.resolve(frame, renderOpts) — single authority.
362
+ * 'ball' → round glow ellipse + halo (existing default behavior)
363
+ * 'box' → solid rect at voxel size
364
+ * 'circle' → outline ellipse (noFill + stroke)
365
+ */
366
+ function _drawQuantum(fd, lc, rs, renderOpts) {
367
+ var rgb;
368
+ // Render color: user-space data on the generic string_map (CSV 'r,g,b,a'); the engine
369
+ // carries it opaque (I1). Same channel as render.outline.
370
+ var _sm = fd.data && fd.data.string_map;
371
+ var renderColor = null;
372
+ if (_sm && _sm['render.color']) {
373
+ var _cp = String(_sm['render.color']).split(',').map(Number);
374
+ if (_cp.length >= 3 && _cp.every(function (n) { return !isNaN(n); })) renderColor = _cp;
375
+ }
376
+ // Color scheme: per-frame override > experiment default > 'warmcool'.
377
+ var frameScheme = (_sm && _sm['render.colorScheme']) ? _sm['render.colorScheme']
378
+ : ((renderOpts && renderOpts.colorScheme) ? renderOpts.colorScheme : 'warmcool');
379
+
380
+ if (renderColor) {
381
+ rgb = renderColor;
382
+ } else {
383
+ // LightColor is the single color authority — getRgbBySchemeName dispatches by scheme
384
+ // and always returns [r,g,b]. Alpha: single authority — getAlphaFromIntensity (wider
385
+ // dynamic range; eliminates the old 180/255 floor so sparse quanta read visually dim).
386
+ var color = lc.getRgbBySchemeName(frameScheme, fd.fullness);
387
+ var alpha01 = lc.getAlphaFromIntensity(fd.fullness);
388
+ rgb = [color[0], color[1], color[2], alpha01];
389
+ }
390
+
391
+ // Shape: RenderShape.resolve is the single authority (it returns 'ball' as its own default).
392
+ var shape = rs.resolve(fd, renderOpts);
393
+ var r = rgb[0], g = rgb[1], b = rgb[2], a = (rgb[3] !== undefined ? rgb[3] : 1);
394
+
395
+ noStroke();
396
+ if (shape === 'box') {
397
+ // Solid filled rectangle at the voxel size. Gives boxes-of-balls a crisp voxel look.
398
+ fill(r, g, b, a * 255);
399
+ rect(fd.rect.left, fd.rect.top, fd.rect.w, fd.rect.h);
400
+ } else if (shape === 'circle') {
401
+ // Outline circle — noFill + stroke, good for wave-front / boundary visualization.
402
+ strokeWeight(1);
403
+ stroke(r, g, b, a * 255);
404
+ noFill();
405
+ ellipse(fd.rect.x, fd.rect.y, fd.rect.w, fd.rect.h);
406
+ } else {
407
+ // 'ball' (default) — round glow ellipse + halo (existing behavior, unchanged).
408
+ // render.outline is a legacy override for this branch: 'true' → rect outline.
409
+ var _outline = !!(_sm && _sm['render.outline'] === 'true');
410
+ if (!_outline) {
411
+ var r1 = Math.max(1, fd.rect.w / 2);
412
+ var r2 = Math.max(2, fd.rect.w);
413
+ fill(r, g, b, a * 255);
414
+ ellipse(fd.rect.x, fd.rect.y, r1, r1);
415
+ fill(r, g, b, 25);
416
+ ellipse(fd.rect.x, fd.rect.y, r2, r2);
417
+ } else {
418
+ strokeWeight(1);
419
+ stroke(r, g, b, a * 255);
420
+ noFill();
421
+ rect(fd.rect.left, fd.rect.top, fd.rect.w, fd.rect.h);
422
+ }
423
+ }
424
+ }
425
+
426
+ // ── Compose the named layers in z-order ─────────────────────────────
427
+ // Per-layer visibility is INDEPENDENT:
428
+ // backdrop & bodies — toggle per-object via LargeObject.visible
429
+ // particles — toggle via drawParticles (renderBaseCanvas gate)
430
+ // Hiding particles still composes backdrop + bodies, so e.g. a pond
431
+ // backdrop refracts under the water shader without the photons. This is
432
+ // the per-layer independence the IDE Layers panel rides on.
433
+ function _composeLayers(tickData, largeObjects, drawParticles, renderOpts) {
434
+ if (typeof background === 'undefined') return; // p5 not in global scope yet
435
+ background(0);
436
+ // ── 2D CAMERA: pan/zoom applied ONCE, around every layer ──
437
+ // Derived from the pose through the one authority, so the picture and
438
+ // screenToWorld/unprojectStill can never disagree. At the default pose k === 1
439
+ // and this is the identity transform — every pre-existing scene is byte-unchanged.
440
+ var v = Camera2dGeometry.resolveView2d(_poseOrDefault(), _physW(), _physH());
441
+ var _framed = (typeof push === 'function' && typeof scale === 'function' &&
442
+ typeof translate === 'function');
443
+ if (_framed) {
444
+ push();
445
+ translate(_physW() / 2, _physH() / 2);
446
+ scale(v.k);
447
+ translate(-v.cx, -v.cy);
448
+ }
449
+ _layerBackdrop(largeObjects); // non-colliding image LOs (under all)
450
+ _layerBodies(largeObjects); // structural sprites
451
+ if (drawParticles) { _layerParticles(tickData, renderOpts); } // quanta, grouped by .layer
452
+ if (_framed) { pop(); }
453
+ }
454
+
455
+ // ── Filter post-processing (WebGL2) ─────────────────────────────────
456
+
457
+ // The provider's OWN canvas — the element p5's createCanvas() returned in init(). Never
458
+ // looked up in the document: the first <canvas> on a page is not necessarily this one.
459
+ function _getP5Canvas() {
460
+ if (_p5Canvas && _p5Canvas.elt) return _p5Canvas.elt;
461
+ throw new Error('p5Provider: no canvas to read — the provider\'s p5 canvas element is absent. ' +
462
+ 'init({width, height}) creates it (p5\'s createCanvas() → .elt); call init first ' +
463
+ '(FrqtlApp.createCanvas does).');
464
+ }
465
+
466
+ // Smooth heightfield via shared FieldBuilder (single source, identical to three provider).
467
+ function _buildFieldTexture() {
468
+ if (!_lastTickData || !_lastTickData.frameData) {
469
+ return new Uint8Array(FieldBuilder.FIELD_OPTS.w * FieldBuilder.FIELD_OPTS.h);
470
+ }
471
+ // FieldBuilder uses its own fixed resolution (FIELD_OPTS.w x h), not canvas size;
472
+ // canvasW/canvasH map physics px onto it.
473
+ return FieldBuilder.buildHeightField(_lastTickData.frameData,
474
+ Object.assign({}, FieldBuilder.FIELD_OPTS, { canvasW: _physW(), canvasH: _physH() }));
475
+ }
476
+
477
+ function _initFilterState(spec, canvas) {
478
+ var offscreen = document.createElement('canvas');
479
+ offscreen.width = canvas.width;
480
+ offscreen.height = canvas.height;
481
+ var gl = offscreen.getContext('webgl2');
482
+ if (!gl) {
483
+ console.warn('[p5-provider] WebGL2 unavailable for filter:', spec.name);
484
+ return { skip: true };
485
+ }
486
+
487
+ var fragSrc = (spec.glsl.indexOf('#version') === -1)
488
+ ? upgradeGlsl(spec.glsl)
489
+ : spec.glsl;
490
+
491
+ var prog = compileProgram(gl, VERT_SRC, fragSrc);
492
+ if (!prog) return { skip: true };
493
+
494
+ var quadBuf = gl.createBuffer();
495
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
496
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
497
+
498
+ function makeTex() {
499
+ var t = gl.createTexture();
500
+ gl.bindTexture(gl.TEXTURE_2D, t);
501
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
502
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
503
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
504
+ return t;
505
+ }
506
+
507
+ return {
508
+ gl: gl, prog: prog, quadBuf: quadBuf,
509
+ sceneTex: makeTex(), fieldTex: makeTex(),
510
+ offscreen: offscreen
511
+ };
512
+ }
513
+
514
+ function _runFilter(spec, time) {
515
+ var canvas = _getP5Canvas();
516
+
517
+ var key = spec.name || 'filter';
518
+ var state = _filterState[key];
519
+ if (!state) {
520
+ state = _initFilterState(spec, canvas);
521
+ _filterState[key] = state;
522
+ }
523
+ if (state.skip) return;
524
+
525
+ var gl = state.gl;
526
+ // R-7: pixelDensity fix — the p5 canvas's LOGICAL size vs. its
527
+ // backing-store size may differ by the device pixel ratio (pixelDensity).
528
+ // Two sizes matter here:
529
+ // logW / logH = LOGICAL pixels = the physics canvas size, the SOT
530
+ // (init's opts.width/height ← runner.canvas_rect; ==
531
+ // createCanvas(_width,_height)). The p5 2D context is
532
+ // scaled by pixelDensity, so it draws in THESE units and
533
+ // a full-canvas rect spans (0,0)→(logW,logH).
534
+ // bsW / bsH = backing-store pixels (canvas.width / canvas.height =
535
+ // logical × pixelDensity). The offscreen WebGL2 buffer,
536
+ // viewport, and uResolution use the backing store.
537
+ //
538
+ // BUGFIX (letterbox interaction): the blit dst rect below MUST be the
539
+ // LOGICAL/SOT size (logW/logH), NOT canvas.clientWidth/clientHeight.
540
+ // clientWidth/Height is the DISPLAYED element box, which the object-fit
541
+ // letterbox (+ max-width/max-height:100% clamps in experiment-layout.css)
542
+ // distorts to a wide/short rectangle on a wide viewport. Blitting into
543
+ // that distorted box left the bottom of the square uncovered (raw
544
+ // particles showing through under the water shader). logW/logH is the
545
+ // same SOT the base particle draw + the object-fit letterbox consume, so
546
+ // the shader now covers the FULL physics square at any viewport width.
547
+ var bsW = canvas.width;
548
+ var bsH = canvas.height;
549
+ var logW = _physW();
550
+ var logH = _physH();
551
+
552
+ // Resize the offscreen buffer only when the backing-store size changes.
553
+ if (state.offscreen.width !== bsW || state.offscreen.height !== bsH) {
554
+ state.offscreen.width = bsW;
555
+ state.offscreen.height = bsH;
556
+ }
557
+
558
+ gl.viewport(0, 0, bsW, bsH);
559
+
560
+ // Upload scene (from the backing store — full bsW × bsH)
561
+ gl.bindTexture(gl.TEXTURE_2D, state.sceneTex);
562
+ try { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); }
563
+ catch (e) { return; }
564
+
565
+ // Upload field (smooth heightfield from shared FieldBuilder; fixed resolution)
566
+ var fOpts = FieldBuilder.FIELD_OPTS;
567
+ var fw = fOpts.w;
568
+ var fh = fOpts.h;
569
+ var fieldData = _buildFieldTexture();
570
+ gl.bindTexture(gl.TEXTURE_2D, state.fieldTex);
571
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, fw, fh, 0, gl.RED, gl.UNSIGNED_BYTE, fieldData);
572
+
573
+ // Draw
574
+ gl.useProgram(state.prog);
575
+ var posLoc = gl.getAttribLocation(state.prog, 'aPosition');
576
+ gl.bindBuffer(gl.ARRAY_BUFFER, state.quadBuf);
577
+ gl.enableVertexAttribArray(posLoc);
578
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
579
+
580
+ // uResolution = backing-store size so texel = 1/uResolution matches the texture.
581
+ // The finite-difference normal uses this to step one texel in UV space.
582
+ gl.uniform2f(gl.getUniformLocation(state.prog, 'uResolution'), bsW, bsH);
583
+ // uTime = simulation seconds at the rendered tick (the contract's filterTime).
584
+ gl.uniform1f(gl.getUniformLocation(state.prog, 'uTime'), time);
585
+
586
+ gl.activeTexture(gl.TEXTURE0);
587
+ gl.bindTexture(gl.TEXTURE_2D, state.sceneTex);
588
+ gl.uniform1i(gl.getUniformLocation(state.prog, 'uScene'), 0);
589
+
590
+ gl.activeTexture(gl.TEXTURE1);
591
+ gl.bindTexture(gl.TEXTURE_2D, state.fieldTex);
592
+ gl.uniform1i(gl.getUniformLocation(state.prog, 'uField'), 1);
593
+
594
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
595
+
596
+ // R-7 FIX: Blit result back to p5 canvas via 2D context with explicit
597
+ // src (backing-store) → dst (LOGICAL/SOT) rect mapping.
598
+ //
599
+ // The p5 2D context is scaled by pixelDensity, so it draws in LOGICAL
600
+ // units: a full-canvas rect is (0,0)→(logW,logH). The offscreen buffer
601
+ // is bsW × bsH (= logical × pixelDensity). Specifying the full
602
+ // src (0,0,bsW,bsH) → full dst (0,0,logW,logH) stretches the entire
603
+ // shader output over the entire physics canvas, regardless of
604
+ // pixelDensity AND regardless of how CSS (object-fit letterbox) then
605
+ // scales the element for display. Using the DISPLAYED client size here
606
+ // instead would couple the framebuffer to the letterboxed box and leave
607
+ // part of the square uncovered on a wide viewport (the bug this fixes).
608
+ var ctx2d = canvas.getContext('2d');
609
+ if (ctx2d) {
610
+ ctx2d.drawImage(state.offscreen,
611
+ 0, 0, bsW, bsH, // source: full offscreen (backing-store pixels)
612
+ 0, 0, logW, logH // destination: full physics canvas (logical/SOT units)
613
+ );
614
+ }
615
+ }
616
+
617
+ // ── Public interface ─────────────────────────────────────────────────
618
+
619
+ var provider = {
620
+
621
+ /**
622
+ * init — create the p5 canvas and RETURN it as an HTMLCanvasElement (`.elt`).
623
+ * MUST be called from inside p5's window.setup() (createCanvas is a p5 global).
624
+ */
625
+ init: function (opts) {
626
+ RendererProviderContract.requireProviderSize('p5Provider.init',
627
+ opts && opts.width, opts && opts.height);
628
+ _width = opts.width;
629
+ _height = opts.height;
630
+ // The mount host is resolved BEFORE createCanvas: a missing or unknown canvasParentId
631
+ // throws the contract's sentence instead of leaving the canvas at p5's own body default
632
+ // (null) or reaching p5's parent(id), whose missing-id path is a bare TypeError.
633
+ var host = RendererProviderContract.requireMountHost('p5Provider.init', opts.parentEl);
634
+ _p5Canvas = createCanvas(_physW(), _physH());
635
+ _p5Canvas.parent(host);
636
+ _initialized = true;
637
+ return _getP5Canvas(); // the HTMLCanvasElement (RENDERER_CONTRACT §1 init row)
638
+ },
639
+
640
+ renderTick: function (tickData, largeObjects, options) {
641
+ _lastTickData = tickData;
642
+ // renderBaseCanvas gates ONLY the particle layer (back-compat: the
643
+ // "shade particles" toggle). The backdrop + bodies layers always
644
+ // compose, so hiding particles leaves other layers independently
645
+ // visible (e.g. a pond backdrop refracting under water without the
646
+ // photons). Default true = particles drawn. Never touches uField/physics.
647
+ var drawParticles = !options || options.renderBaseCanvas !== false;
648
+ _lastRenderBaseCanvas = drawParticles; // capture provenance (§1a.4)
649
+ _composeLayers(tickData, largeObjects, drawParticles, options);
650
+ var time = _filters.length > 0
651
+ ? RendererProviderContract.filterTime(tickData) : null;
652
+ _lastFilterTime = time; // capture provenance (§1a.4)
653
+ for (var i = 0; i < _filters.length; i++) { _runFilter(_filters[i], time); }
654
+ },
655
+
656
+ filter: function (filterSpec, perFilterOptions) {
657
+ // R-6: perFilterOptions (optional) merges over the runner options for this
658
+ // specific filter call. The merged options are stored on the spec copy so
659
+ // renderTick picks them up on the next draw.
660
+ if (!filterSpec) return;
661
+ if (filterSpec.name) {
662
+ for (var i = 0; i < _filters.length; i++) {
663
+ if (_filters[i].name === filterSpec.name) {
664
+ // Update perFilterOptions on the existing entry
665
+ if (perFilterOptions) {
666
+ _filters[i]._perFilterOptions = perFilterOptions;
667
+ }
668
+ return;
669
+ }
670
+ }
671
+ }
672
+ // Store perFilterOptions alongside the spec (shallow copy to avoid mutating caller)
673
+ var entry = filterSpec;
674
+ if (perFilterOptions) {
675
+ entry = Object.assign({}, filterSpec);
676
+ entry._perFilterOptions = perFilterOptions;
677
+ }
678
+ _filters.push(entry);
679
+ },
680
+
681
+ removeFilter: function (nameOrSpec) {
682
+ var name = (typeof nameOrSpec === 'string') ? nameOrSpec
683
+ : (nameOrSpec && nameOrSpec.name);
684
+ _filters = _filters.filter(function (f) { return f.name !== name; });
685
+ if (name && _filterState[name]) { delete _filterState[name]; }
686
+ },
687
+
688
+ setSize: function (w, h) {
689
+ RendererProviderContract.requireProviderSize('p5Provider.setSize', w, h);
690
+ _width = w; _height = h;
691
+ if (_initialized && typeof resizeCanvas === 'function') { resizeCanvas(w, h); }
692
+ _notifyPose(); // the 2D map changes with the canvas (target null = its centre)
693
+ },
694
+
695
+ // Loop is owned by FrqtlApp — these are intentional no-ops.
696
+ start: function () {},
697
+ stop: function () {},
698
+
699
+ destroy: function () {
700
+ if (_p5Canvas && _p5Canvas.remove) { _p5Canvas.remove(); }
701
+ _initialized = false;
702
+ _filters = [];
703
+ _filterState = {};
704
+ _lastTickData = null;
705
+ _lastFilterTime = null;
706
+ },
707
+
708
+ raw: function () { return _p5Canvas; },
709
+
710
+ // renderCapabilities — OPTIONAL (see the module-level docstring above
711
+ // _renderCapabilitiesP5). Static per-provider metadata; no live-instance
712
+ // state needed, so the free function above is called unmodified.
713
+ renderCapabilities: _renderCapabilitiesP5,
714
+
715
+ // ── screenToWorld (OPTIONAL convenience capability, S5b) ────────────
716
+ // REPOINTED (2026-07-30) to consume the SAME 2D pose state the draw transform
717
+ // uses. Previously a fixed CSS→physics ratio; under pan/zoom that map must
718
+ // change, and if the pose lived anywhere but the camera authority two authorities
719
+ // would feed one mapping — the split-basis failure the 3D consolidation removed.
720
+ // At the default pose this is byte-identical to the old ratio scale (k === 1), so
721
+ // every existing drag/pick is unchanged until someone actually pans or zooms.
722
+ // `worldZ` still has no effect (2D has no depth transform; accepted for signature
723
+ // parity with three-provider.js).
724
+ screenToWorld: function (canvasCssX, canvasCssY, canvasCssWidth, canvasCssHeight /*, worldZ */) {
725
+ return Camera2dGeometry.screenToWorld2d(
726
+ _poseOrDefault(), _physW(), _physH(),
727
+ canvasCssX, canvasCssY, canvasCssWidth || _physW(), canvasCssHeight || _physH());
728
+ },
729
+
730
+ // ── getCameraControl (OPTIONAL capability) — the DEGENERATE orbit bag ──
731
+ // RENDERER_CONTRACT §1a.2. This provider DOES have camera pose control (pan +
732
+ // zoom), so it returns a handle rather than null: `getCameraControl()` returns
733
+ // null ONLY when a provider has no pose control AT ALL. The handle's `poseFields`
734
+ // declares exactly the axes this camera has — `distance` and `target`, and NO
735
+ // azimuth/elevation/roll — so a host hides the angular affordances by FIELD
736
+ // PRESENCE (the `roll`/`move()` capability idiom) and never asks "is this 2D?".
737
+ //
738
+ // All handle discipline carries over unchanged from the 3D camera: `setOrbit`
739
+ // applies a PATCH (only the keys mentioned), `getOrbit` returns the whole bag
740
+ // value-exact so a captured pose round-trips, a present non-finite value THROWS
741
+ // (throw-or-apply — a silent clamp would leave the camera in a plausible wrong
742
+ // place with no diagnostic), and `setPreset` is a FULL framing reset.
743
+ getCameraControl: function () {
744
+ var geom = Camera2dGeometry;
745
+ function _finite(v, name, where) {
746
+ if (typeof v === 'number' && isFinite(v)) { return v; }
747
+ throw new Error('p5-provider camera ' + where + ': ' + name +
748
+ ' must be a finite number, got ' + String(v) +
749
+ ' — throw-or-apply, never a silent clamp.');
750
+ }
751
+ function _presets() { return geom.PRESETS_2D(_physW(), _physH()); }
752
+ var ctl = {
753
+ // FERRIED from the geometry authority, exactly as the 3D handle ferries
754
+ // its own preset table — the provider holds no declarative data.
755
+ presets: Object.keys(_presets()),
756
+ poseFields: geom.POSE_FIELDS_2D(_physW(), _physH()),
757
+ setOrbit: function (spec) {
758
+ if (!spec) { return; }
759
+ // POINT SHAPE FIRST, at the ONE owner: `{x,y,z}` and `[x,y,z]` both
760
+ // arrive here as the canonical object form, and a malformed point has
761
+ // already thrown the same message the 3D handle throws.
762
+ spec = OrbitBag.normalizeOrbitPatch(spec);
763
+ var pose = _poseOrDefault();
764
+ if (spec.distance !== undefined) {
765
+ var d = _finite(spec.distance, 'distance', 'setOrbit');
766
+ if (d <= 0) {
767
+ throw new Error('p5-provider camera setOrbit: distance is the VISIBLE ' +
768
+ 'HEIGHT in physics px and must be > 0, got ' + d + '. (FAIL-LOUD.)');
769
+ }
770
+ pose.distance = d;
771
+ }
772
+ // `target` is only touched when the spec MENTIONS it, so a zoom write
773
+ // never silently re-centres. An explicit `target:null` re-centres.
774
+ // The point is already normalized (object form or null); this camera
775
+ // PROJECTS it — it stores {x,y} and ignores z, which is what "the 2D
776
+ // pose is the degenerate orbit bag" means at the storage level.
777
+ if ('target' in spec) {
778
+ var t = spec.target;
779
+ pose.target = t ? { x: t.x, y: t.y } : null;
780
+ }
781
+ // An angular key is a REAL mistake here, not a no-op: silently
782
+ // ignoring it would let a 3D camera path path "run" in 2D and produce
783
+ // fabricated evidence. The handle declares what it has; asking for
784
+ // more fails loud.
785
+ // The angular axes are named once, at the pose-vocabulary owner.
786
+ var angular = OrbitBag.ANGULAR_FIELDS;
787
+ for (var i = 0; i < angular.length; i++) {
788
+ if (spec[angular[i]] !== undefined) {
789
+ throw new Error('p5-provider camera setOrbit: this camera declares no "' +
790
+ angular[i] + '" axis (2D pose is {distance, target}). Check ' +
791
+ 'cameraControl.poseFields rather than assuming a mode. (FAIL-LOUD.)');
792
+ }
793
+ }
794
+ _notifyPose();
795
+ },
796
+ getOrbit: function () { return _poseBag(); },
797
+ // addPoseListener(fn) → unsubscribe — the POSE-CHANGE NOTICE: `fn(bag)` runs
798
+ // after EVERY pose write, with the getOrbit() bag (RENDERER_CONTRACT §1a.2).
799
+ addPoseListener: function (fn) {
800
+ if (!_poseNotice) { _poseNotice = OrbitBag.makePoseNotice(); }
801
+ return _poseNotice.add(fn);
802
+ },
803
+ setPreset: function (name) {
804
+ // A preset is a FULL framing reset — the reliable way back. A name this
805
+ // camera does not offer THROWS (a silent no-op would leave a saved or
806
+ // codegen'd preset doing nothing with no signal).
807
+ var all = _presets(), p = all[name];
808
+ if (!p) {
809
+ throw new Error('[p5-provider camera] setPreset("' + name + '"): this camera offers ' +
810
+ 'only [' + Object.keys(all).join(', ') + '] (ctl.presets). (FAIL-LOUD.)');
811
+ }
812
+ ctl.setOrbit(p);
813
+ }
814
+ // No look()/move(): this camera has no angular axes and no dolly beyond
815
+ // `distance`. Their ABSENCE is the capability check (the move() precedent).
816
+ };
817
+ return ctl;
818
+ },
819
+
820
+ // ── projection (OPTIONAL capability — the scene's camera intrinsics) ──
821
+ // RENDERER_CONTRACT §1a.5 + §9: the DISPLAY provider is boot-fixed (swapping it
822
+ // is a reload) while OBSERVERS are live and own no projection — so this is
823
+ // scene-level and rides the SceneRecord, not each still. p5's 2D mode is the
824
+ // `affine2d` map — the per-still pan/zoom terms live on the still's own `pose`,
825
+ // which is where mutable state belongs.
826
+ projection: function () {
827
+ return { kind: 'affine2d' };
828
+ },
829
+
830
+ // ── captureStill (OPTIONAL capability — the `composite` tap executor) ──
831
+ // RENDERER_CONTRACT §1a.4. The 2D-canvas readback: `getImageData` is already
832
+ // RGBA, row-major, TOP-LEFT, and a 2D canvas PERSISTS between draws, so no flip
833
+ // and no re-render is needed (contrast the GL path). All bag assembly, the length
834
+ // check and the blank-buffer guard live in the shared owner. Returns a Promise per
835
+ // the frozen signature (a WebGPU backend's only readback is async).
836
+ //
837
+ // `filtersApplied` is read from THIS provider's own ordered `_filters` list — the
838
+ // one ordered authority, and the only one that sees a direct BYO `prov.filter()`.
839
+ captureStill: function () {
840
+ var common = CaptureStillCommon;
841
+ var bag = common.captureFrom2dCanvas(_getP5Canvas(), {
842
+ cssWidth: _physW(),
843
+ cssHeight: _physH(),
844
+ physicsW: _physW(),
845
+ physicsH: _physH(),
846
+ filtersApplied: _filters.map(function (f) { return f.name; }),
847
+ renderBaseCanvas: _lastRenderBaseCanvas,
848
+ filterTime: _lastFilterTime,
849
+ // The TickData these canvas pixels were drawn from → `renderedTick`.
850
+ composited: _lastTickData
851
+ });
852
+ return Promise.resolve(bag);
853
+ },
854
+
855
+ get initialized() { return _initialized; }
856
+ };
857
+
858
+ return provider;
859
+ }
860
+
861
+ export { makeP5Provider, _renderCapabilitiesP5 };