three-nebula 11.0.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/three-nebula.cjs +109 -0
  3. package/dist/three-nebula.cjs.map +1 -0
  4. package/dist/three-nebula.mjs +2968 -0
  5. package/dist/three-nebula.mjs.map +1 -0
  6. package/dist/three-nebula.umd.js +109 -0
  7. package/dist/three-nebula.umd.js.map +1 -0
  8. package/package.json +23 -47
  9. package/specs/01-emitter-hierarchy.md +200 -0
  10. package/specs/02-determinism-scrubbing.md +188 -0
  11. package/specs/03-sound-renderer.md +183 -0
  12. package/specs/04-schema-versioning.md +140 -0
  13. package/specs/05-content-addressed-assets.md +288 -0
  14. package/specs/README.md +80 -0
  15. package/build/cjs/behaviour/Alpha.js +0 -189
  16. package/build/cjs/behaviour/Attraction.js +0 -189
  17. package/build/cjs/behaviour/Behaviour.js +0 -253
  18. package/build/cjs/behaviour/Collision.js +0 -146
  19. package/build/cjs/behaviour/Color.js +0 -148
  20. package/build/cjs/behaviour/CrossZone.js +0 -141
  21. package/build/cjs/behaviour/Force.js +0 -126
  22. package/build/cjs/behaviour/Gravity.js +0 -78
  23. package/build/cjs/behaviour/RandomDrift.js +0 -153
  24. package/build/cjs/behaviour/Repulsion.js +0 -123
  25. package/build/cjs/behaviour/Rotate.js +0 -265
  26. package/build/cjs/behaviour/Scale.js +0 -177
  27. package/build/cjs/behaviour/Spring.js +0 -133
  28. package/build/cjs/behaviour/constants.js +0 -28
  29. package/build/cjs/behaviour/index.js +0 -112
  30. package/build/cjs/behaviour/types.js +0 -33
  31. package/build/cjs/constants/index.js +0 -61
  32. package/build/cjs/core/Particle.js +0 -390
  33. package/build/cjs/core/Pool.js +0 -203
  34. package/build/cjs/core/System.js +0 -366
  35. package/build/cjs/core/constants.js +0 -163
  36. package/build/cjs/core/fromJSON.js +0 -128
  37. package/build/cjs/core/fromJSONAsync.js +0 -239
  38. package/build/cjs/core/index.js +0 -32
  39. package/build/cjs/core/three/Euler.js +0 -233
  40. package/build/cjs/core/three/Math.js +0 -91
  41. package/build/cjs/core/three/Matrix4.js +0 -745
  42. package/build/cjs/core/three/Quaternion.js +0 -451
  43. package/build/cjs/core/three/Vector3.js +0 -464
  44. package/build/cjs/core/three/constants.js +0 -298
  45. package/build/cjs/core/three/index.js +0 -48
  46. package/build/cjs/core/types.js +0 -13
  47. package/build/cjs/debug/Debug.js +0 -209
  48. package/build/cjs/debug/constants.js +0 -19
  49. package/build/cjs/debug/index.js +0 -24
  50. package/build/cjs/debug/log.js +0 -38
  51. package/build/cjs/ease/index.js +0 -149
  52. package/build/cjs/emitter/Emitter.js +0 -798
  53. package/build/cjs/emitter/FollowEmitter.js +0 -146
  54. package/build/cjs/emitter/constants.js +0 -20
  55. package/build/cjs/emitter/index.js +0 -24
  56. package/build/cjs/emitter/types.js +0 -11
  57. package/build/cjs/events/EventDispatcher.js +0 -105
  58. package/build/cjs/events/constants.js +0 -23
  59. package/build/cjs/events/index.js +0 -27
  60. package/build/cjs/index.js +0 -151
  61. package/build/cjs/initializer/Body.js +0 -122
  62. package/build/cjs/initializer/BodySprite.js +0 -146
  63. package/build/cjs/initializer/Initializer.js +0 -109
  64. package/build/cjs/initializer/InitializerUtil.js +0 -54
  65. package/build/cjs/initializer/Life.js +0 -106
  66. package/build/cjs/initializer/Mass.js +0 -101
  67. package/build/cjs/initializer/Position.js +0 -152
  68. package/build/cjs/initializer/Radius.js +0 -118
  69. package/build/cjs/initializer/Rate.js +0 -150
  70. package/build/cjs/initializer/Rotation.js +0 -95
  71. package/build/cjs/initializer/Texture.js +0 -139
  72. package/build/cjs/initializer/Velocity/PolarVelocity.js +0 -102
  73. package/build/cjs/initializer/Velocity/RadialVelocity.js +0 -111
  74. package/build/cjs/initializer/Velocity/VectorVelocity.js +0 -108
  75. package/build/cjs/initializer/Velocity/Velocity.js +0 -94
  76. package/build/cjs/initializer/Velocity/index.js +0 -32
  77. package/build/cjs/initializer/constants.js +0 -43
  78. package/build/cjs/initializer/index.js +0 -108
  79. package/build/cjs/initializer/types.js +0 -37
  80. package/build/cjs/math/ArraySpan.js +0 -103
  81. package/build/cjs/math/Box.js +0 -46
  82. package/build/cjs/math/ColorSpan.js +0 -113
  83. package/build/cjs/math/MathUtils.js +0 -57
  84. package/build/cjs/math/Polar3D.js +0 -115
  85. package/build/cjs/math/Span.js +0 -88
  86. package/build/cjs/math/Vector3D.js +0 -76
  87. package/build/cjs/math/constants.js +0 -35
  88. package/build/cjs/math/index.js +0 -122
  89. package/build/cjs/math/integration.js +0 -59
  90. package/build/cjs/math/types.js +0 -19
  91. package/build/cjs/renderer/BaseRenderer.js +0 -108
  92. package/build/cjs/renderer/CustomRenderer.js +0 -65
  93. package/build/cjs/renderer/GPURenderer/Desktop/index.js +0 -371
  94. package/build/cjs/renderer/GPURenderer/Desktop/shaders/fragmentShader.js +0 -13
  95. package/build/cjs/renderer/GPURenderer/Desktop/shaders/index.js +0 -22
  96. package/build/cjs/renderer/GPURenderer/Desktop/shaders/vertexShader.js +0 -17
  97. package/build/cjs/renderer/GPURenderer/Mobile/index.js +0 -385
  98. package/build/cjs/renderer/GPURenderer/Mobile/shaders/fragmentShader.js +0 -13
  99. package/build/cjs/renderer/GPURenderer/Mobile/shaders/index.js +0 -22
  100. package/build/cjs/renderer/GPURenderer/Mobile/shaders/vertexShader.js +0 -15
  101. package/build/cjs/renderer/GPURenderer/common/ParticleBuffer/constants.js +0 -58
  102. package/build/cjs/renderer/GPURenderer/common/ParticleBuffer/index.js +0 -93
  103. package/build/cjs/renderer/GPURenderer/common/TextureAtlas/constants.js +0 -9
  104. package/build/cjs/renderer/GPURenderer/common/TextureAtlas/index.js +0 -221
  105. package/build/cjs/renderer/GPURenderer/common/constants.js +0 -22
  106. package/build/cjs/renderer/GPURenderer/common/index.js +0 -38
  107. package/build/cjs/renderer/GPURenderer/common/shaders/constants.js +0 -9
  108. package/build/cjs/renderer/GPURenderer/common/stores/Target.js +0 -45
  109. package/build/cjs/renderer/GPURenderer/common/stores/UniqueList.js +0 -51
  110. package/build/cjs/renderer/GPURenderer/common/stores/index.js +0 -22
  111. package/build/cjs/renderer/GPURenderer/index.js +0 -97
  112. package/build/cjs/renderer/MeshRenderer.js +0 -136
  113. package/build/cjs/renderer/SpriteRenderer.js +0 -68
  114. package/build/cjs/renderer/index.js +0 -40
  115. package/build/cjs/renderer/types.js +0 -21
  116. package/build/cjs/utils/ColorUtil.js +0 -39
  117. package/build/cjs/utils/PUID.js +0 -26
  118. package/build/cjs/utils/THREEUtil.js +0 -40
  119. package/build/cjs/utils/Util.js +0 -69
  120. package/build/cjs/utils/index.js +0 -61
  121. package/build/cjs/utils/uid.js +0 -12
  122. package/build/cjs/zone/BoxZone.js +0 -178
  123. package/build/cjs/zone/LineZone.js +0 -101
  124. package/build/cjs/zone/MeshZone.js +0 -101
  125. package/build/cjs/zone/PointZone.js +0 -99
  126. package/build/cjs/zone/ScreenZone.js +0 -164
  127. package/build/cjs/zone/SphereZone.js +0 -155
  128. package/build/cjs/zone/Zone.js +0 -172
  129. package/build/cjs/zone/index.js +0 -56
  130. package/build/cjs/zone/types.js +0 -21
  131. package/build/esm/behaviour/Alpha.js +0 -134
  132. package/build/esm/behaviour/Attraction.js +0 -135
  133. package/build/esm/behaviour/Behaviour.js +0 -215
  134. package/build/esm/behaviour/Collision.js +0 -100
  135. package/build/esm/behaviour/Color.js +0 -101
  136. package/build/esm/behaviour/CrossZone.js +0 -82
  137. package/build/esm/behaviour/Force.js +0 -83
  138. package/build/esm/behaviour/Gravity.js +0 -40
  139. package/build/esm/behaviour/RandomDrift.js +0 -97
  140. package/build/esm/behaviour/Repulsion.js +0 -81
  141. package/build/esm/behaviour/Rotate.js +0 -214
  142. package/build/esm/behaviour/Scale.js +0 -131
  143. package/build/esm/behaviour/Spring.js +0 -90
  144. package/build/esm/behaviour/constants.js +0 -11
  145. package/build/esm/behaviour/index.js +0 -14
  146. package/build/esm/behaviour/types.js +0 -14
  147. package/build/esm/constants/index.js +0 -42
  148. package/build/esm/core/Particle.js +0 -357
  149. package/build/esm/core/Pool.js +0 -161
  150. package/build/esm/core/System.js +0 -320
  151. package/build/esm/core/constants.js +0 -127
  152. package/build/esm/core/fromJSON.js +0 -107
  153. package/build/esm/core/fromJSONAsync.js +0 -210
  154. package/build/esm/core/index.js +0 -4
  155. package/build/esm/core/three/Euler.js +0 -224
  156. package/build/esm/core/three/Math.js +0 -84
  157. package/build/esm/core/three/Matrix4.js +0 -739
  158. package/build/esm/core/three/Quaternion.js +0 -445
  159. package/build/esm/core/three/Vector3.js +0 -457
  160. package/build/esm/core/three/constants.js +0 -148
  161. package/build/esm/core/three/index.js +0 -5
  162. package/build/esm/core/types.js +0 -4
  163. package/build/esm/debug/Debug.js +0 -195
  164. package/build/esm/debug/constants.js +0 -7
  165. package/build/esm/debug/index.js +0 -3
  166. package/build/esm/debug/log.js +0 -31
  167. package/build/esm/ease/index.js +0 -113
  168. package/build/esm/emitter/Emitter.js +0 -708
  169. package/build/esm/emitter/FollowEmitter.js +0 -97
  170. package/build/esm/emitter/constants.js +0 -7
  171. package/build/esm/emitter/index.js +0 -3
  172. package/build/esm/emitter/types.js +0 -3
  173. package/build/esm/events/EventDispatcher.js +0 -82
  174. package/build/esm/events/constants.js +0 -9
  175. package/build/esm/events/index.js +0 -4
  176. package/build/esm/index.js +0 -13
  177. package/build/esm/initializer/Body.js +0 -83
  178. package/build/esm/initializer/BodySprite.js +0 -108
  179. package/build/esm/initializer/Initializer.js +0 -84
  180. package/build/esm/initializer/InitializerUtil.js +0 -48
  181. package/build/esm/initializer/Life.js +0 -66
  182. package/build/esm/initializer/Mass.js +0 -60
  183. package/build/esm/initializer/Position.js +0 -98
  184. package/build/esm/initializer/Radius.js +0 -75
  185. package/build/esm/initializer/Rate.js +0 -107
  186. package/build/esm/initializer/Rotation.js +0 -54
  187. package/build/esm/initializer/Texture.js +0 -101
  188. package/build/esm/initializer/Velocity/PolarVelocity.js +0 -63
  189. package/build/esm/initializer/Velocity/RadialVelocity.js +0 -72
  190. package/build/esm/initializer/Velocity/VectorVelocity.js +0 -69
  191. package/build/esm/initializer/Velocity/Velocity.js +0 -52
  192. package/build/esm/initializer/Velocity/index.js +0 -4
  193. package/build/esm/initializer/constants.js +0 -26
  194. package/build/esm/initializer/index.js +0 -12
  195. package/build/esm/initializer/types.js +0 -16
  196. package/build/esm/math/ArraySpan.js +0 -62
  197. package/build/esm/math/Box.js +0 -25
  198. package/build/esm/math/ColorSpan.js +0 -71
  199. package/build/esm/math/MathUtils.js +0 -48
  200. package/build/esm/math/Polar3D.js +0 -81
  201. package/build/esm/math/Span.js +0 -63
  202. package/build/esm/math/Vector3D.js +0 -34
  203. package/build/esm/math/constants.js +0 -24
  204. package/build/esm/math/index.js +0 -10
  205. package/build/esm/math/integration.js +0 -47
  206. package/build/esm/math/types.js +0 -7
  207. package/build/esm/renderer/BaseRenderer.js +0 -78
  208. package/build/esm/renderer/CustomRenderer.js +0 -23
  209. package/build/esm/renderer/GPURenderer/Desktop/index.js +0 -357
  210. package/build/esm/renderer/GPURenderer/Desktop/shaders/fragmentShader.js +0 -30
  211. package/build/esm/renderer/GPURenderer/Desktop/shaders/index.js +0 -3
  212. package/build/esm/renderer/GPURenderer/Desktop/shaders/vertexShader.js +0 -36
  213. package/build/esm/renderer/GPURenderer/Mobile/index.js +0 -375
  214. package/build/esm/renderer/GPURenderer/Mobile/shaders/fragmentShader.js +0 -27
  215. package/build/esm/renderer/GPURenderer/Mobile/shaders/index.js +0 -3
  216. package/build/esm/renderer/GPURenderer/Mobile/shaders/vertexShader.js +0 -33
  217. package/build/esm/renderer/GPURenderer/common/ParticleBuffer/constants.js +0 -30
  218. package/build/esm/renderer/GPURenderer/common/ParticleBuffer/index.js +0 -70
  219. package/build/esm/renderer/GPURenderer/common/TextureAtlas/constants.js +0 -2
  220. package/build/esm/renderer/GPURenderer/common/TextureAtlas/index.js +0 -206
  221. package/build/esm/renderer/GPURenderer/common/constants.js +0 -13
  222. package/build/esm/renderer/GPURenderer/common/index.js +0 -4
  223. package/build/esm/renderer/GPURenderer/common/shaders/constants.js +0 -2
  224. package/build/esm/renderer/GPURenderer/common/stores/Target.js +0 -26
  225. package/build/esm/renderer/GPURenderer/common/stores/UniqueList.js +0 -29
  226. package/build/esm/renderer/GPURenderer/common/stores/index.js +0 -3
  227. package/build/esm/renderer/GPURenderer/index.js +0 -57
  228. package/build/esm/renderer/MeshRenderer.js +0 -93
  229. package/build/esm/renderer/SpriteRenderer.js +0 -30
  230. package/build/esm/renderer/index.js +0 -5
  231. package/build/esm/renderer/types.js +0 -8
  232. package/build/esm/utils/ColorUtil.js +0 -32
  233. package/build/esm/utils/PUID.js +0 -19
  234. package/build/esm/utils/THREEUtil.js +0 -31
  235. package/build/esm/utils/Util.js +0 -60
  236. package/build/esm/utils/index.js +0 -13
  237. package/build/esm/utils/uid.js +0 -3
  238. package/build/esm/zone/BoxZone.js +0 -133
  239. package/build/esm/zone/LineZone.js +0 -61
  240. package/build/esm/zone/MeshZone.js +0 -60
  241. package/build/esm/zone/PointZone.js +0 -59
  242. package/build/esm/zone/ScreenZone.js +0 -116
  243. package/build/esm/zone/SphereZone.js +0 -113
  244. package/build/esm/zone/Zone.js +0 -141
  245. package/build/esm/zone/index.js +0 -7
  246. package/build/esm/zone/types.js +0 -8
  247. package/build/three-nebula.js +0 -2
  248. package/build/three-nebula.js.map +0 -1
@@ -0,0 +1,140 @@
1
+ # Spec 04 — Schema Versioning
2
+
3
+ A `version` field and a migration chain, so the schema can change without
4
+ stranding six years of saved systems.
5
+
6
+ **Blocks:** 01 (hierarchy), 05 (assets) — both break the schema.
7
+ **Size:** small. Boring. Lands first.
8
+
9
+ ---
10
+
11
+ ## Why first
12
+
13
+ 01 turns `emitters[]` into a tree. 05 replaces inlined base64 with hash refs.
14
+ Both are breaking changes to the JSON.
15
+
16
+ There are systems in the wild right now — six years of them, in Discords, in
17
+ gists, in people's projects. If a new three-nebula rejects them, the goodwill
18
+ that survived six years of dormancy does not survive that.
19
+
20
+ This spec is the escape hatch that makes every later schema change cheap. It must
21
+ exist before the first one lands, not after.
22
+
23
+ ---
24
+
25
+ ## Stage 0 — Audit
26
+
27
+ 1. Does the JSON have any version marker today? Any at all?
28
+ 2. What does `System.fromJSON` / `fromJSONAsync` do on unrecognised fields —
29
+ throw, ignore, or crash?
30
+ 3. Are there existing JSON fixtures in the test suite? How many? Are they
31
+ representative?
32
+ 4. Is the JSON schema documented anywhere authoritative, or is the parser the spec?
33
+
34
+ ---
35
+
36
+ ## Stage 1 — The field
37
+
38
+ ```jsonc
39
+ {
40
+ "version": 2,
41
+ "emitters": [ /* ... */ ]
42
+ }
43
+ ```
44
+
45
+ **Integer, monotonic. Not semver.** Semver invites debate about what counts as a
46
+ minor change. An integer that increments whenever the shape changes has no
47
+ ambiguity and the migration chain is trivially ordered.
48
+
49
+ Rules:
50
+
51
+ - **Missing `version` → treat as `0`** (legacy, pre-versioning). This is the
52
+ entire back-compat story for existing content and it must never be removed.
53
+ - **`version` > current → clear, actionable error.** Not a crash, not silent
54
+ partial parse. `"This system was created with a newer version of three-nebula
55
+ (v5). This build supports up to v3. Update three-nebula."`
56
+ - **`version` < current → run the migration chain.**
57
+
58
+ ---
59
+
60
+ ## Stage 2 — Migration registry
61
+
62
+ ```js
63
+ const migrations = {
64
+ 0: migrate0to1, // legacy → versioned
65
+ 1: migrate1to2, // flat emitters → tree (spec 01)
66
+ 2: migrate2to3, // base64 textures → hash refs (spec 05)
67
+ };
68
+
69
+ function migrate(json) {
70
+ let v = json.version ?? 0;
71
+ while (v < CURRENT_VERSION) {
72
+ json = migrations[v](json);
73
+ v += 1;
74
+ json.version = v;
75
+ }
76
+ return json;
77
+ }
78
+ ```
79
+
80
+ Requirements:
81
+
82
+ - Each migration is a **pure function**: JSON in, JSON out. No side effects, no
83
+ network, no I/O. (05 needs async blob writes — see that spec for how it splits
84
+ the pure transform from the storage step.)
85
+ - Migrations are **append-only**. Once shipped, a migration is frozen forever.
86
+ Never edit a released migration; add a new one.
87
+ - `System.fromJSON` runs `migrate()` before anything else touches the object.
88
+ - Migrations must be individually unit-testable without instantiating a system.
89
+
90
+ ---
91
+
92
+ ## Stage 3 — The fixture corpus
93
+
94
+ This is what stops the guarantee from rotting.
95
+
96
+ - Collect **real** legacy JSON. Not synthesised — actual systems from the repo's
97
+ examples, the docs site, and (if you can get them) from users. Ask in the
98
+ Discords; people will send you their old files and it's a nice re-engagement
99
+ excuse.
100
+ - Store under `test/fixtures/schema/v0/`, `v1/`, etc.
101
+ - Golden test: every fixture at every version loads without error and produces
102
+ a system whose structure matches an expected snapshot.
103
+ - **Add a fixture for every version, at the time that version ships.** A v2
104
+ fixture written after v4 exists is not evidence of anything.
105
+
106
+ **Acceptance:** CI fails if any historical fixture stops loading.
107
+
108
+ ---
109
+
110
+ ## Stage 4 — Write path
111
+
112
+ - `System.toJSON()` always emits the current version. Never write old versions.
113
+ - No "save as v1" affordance. Downgrade is not a supported operation and
114
+ pretending otherwise creates an infinite matrix.
115
+ - The editor's export always produces current-version JSON.
116
+
117
+ ---
118
+
119
+ ## Interaction with 01 and 05
120
+
121
+ The migration for **01** (flat → tree) is mechanical: wrap each existing emitter
122
+ with `children: []`. Every legacy system is a tree of depth 1. Lossless.
123
+
124
+ The migration for **05** (base64 → refs) is *not* purely mechanical, because it
125
+ must write blobs somewhere. The pure transform can only rewrite the reference;
126
+ the blob extraction needs a storage target. See 05, Stage 3 — the resolution is
127
+ that the migration emits an intermediate form carrying the decoded bytes, and the
128
+ asset store consumes it.
129
+
130
+ Do not let 05's awkwardness leak into this spec's design. Migrations stay pure;
131
+ 05 adapts.
132
+
133
+ ---
134
+
135
+ ## Explicitly out of scope
136
+
137
+ - Downgrade / backward migration
138
+ - Schema validation (JSON Schema, zod, etc.) — worth doing, separate concern,
139
+ do not couple it to versioning
140
+ - Versioning the *bundle* format (05's container) — related but distinct
@@ -0,0 +1,288 @@
1
+ # Spec 05 — Content-Addressed Assets
2
+
3
+ Replace inlined base64 textures with content-addressed references, plus a
4
+ pluggable resolver and a bundle container.
5
+
6
+ **Depends on:** 04 (schema versioning) — this breaks the schema.
7
+ **Blocks:** 03 (audio blob storage)
8
+
9
+ ---
10
+
11
+ ## Context: the current design was correct
12
+
13
+ Textures are currently base64-encoded directly into the system JSON. **This was
14
+ the right call.** Nebula was an NW.js desktop app with no server: one portable
15
+ file, no asset resolution, no CORS, atomically versioned, works offline.
16
+
17
+ The constraint has changed, not the reasoning. Any consumer holding more than a
18
+ handful of systems needs dedup, CDN caching, and stable asset identity — none of
19
+ which are possible when assets are anonymous strings inside documents.
20
+
21
+ **Do not frame this as fixing a mistake. It's an inherited constraint being lifted.**
22
+
23
+ ---
24
+
25
+ ## What breaks at scale
26
+
27
+ | Problem | Consequence |
28
+ |---|---|
29
+ | No dedup | The same dozen smoke puffs stored once per system. 500 systems = 500 copies. |
30
+ | No CDN caching | Texture bytes ride inside the JSON; every fetch re-downloads them. Nothing is reusable across systems. |
31
+ | Flipbooks | A 4K 8×8 sheet is several MB raw, ~33% more base64'd. `JSON.parse` on a 10MB blob stalls the main thread. |
32
+ | No asset identity | Two systems using the same texture are unrelatable. "What uses this asset?" is unanswerable. |
33
+ | Useless diffs | Any texture tweak rewrites the whole document. |
34
+
35
+ Asset identity is the one with no workaround. With a content hash, "what uses
36
+ this?" is a single lookup; with inlined base64 the question cannot be asked.
37
+
38
+ ## Precedent
39
+
40
+ glTF ships exactly these three forms of the same problem:
41
+
42
+ - `.gltf` + external files → references
43
+ - `.gltf` with `data:` URIs → **the current design**
44
+ - `.glb` → binary container, JSON chunk + binary chunk, no base64
45
+
46
+ The industry converged on GLB, for precisely the 33% tax plus parse cost. Steal
47
+ the resolution, not just the problem statement — and lean on it in docs, because
48
+ three.js users already know this shape.
49
+
50
+ ---
51
+
52
+ ## Stage 0 — Audit
53
+
54
+ 1. How are textures currently represented in the JSON? Exact field paths.
55
+ 2. Is base64 the only form, or are URLs also accepted?
56
+ 3. Where does texture loading happen — is there a single choke point or is it
57
+ scattered across renderers/initializers?
58
+ 4. Is `Body` / sprite texture handling shared with anything else?
59
+ 5. Is there any caching of decoded textures today?
60
+
61
+ ---
62
+
63
+ ## Stage 1 — Reference format
64
+
65
+ ```jsonc
66
+ {
67
+ "renderer": {
68
+ "type": "sprite",
69
+ "texture": { "$ref": "sha256:a3f2c1..." }
70
+ }
71
+ }
72
+ ```
73
+
74
+ - **sha256 over the raw bytes**, hex-encoded, prefixed with the algorithm.
75
+ The prefix is cheap and buys algorithm agility.
76
+ - The hash **is** the identity. Immutable by construction.
77
+ - Same format for audio blobs (03) and any future asset type. One store.
78
+
79
+ ---
80
+
81
+ ## Stage 2 — Resolver interface
82
+
83
+ The runtime must not know or care where bytes come from.
84
+
85
+ ```ts
86
+ interface AssetResolver {
87
+ resolve(ref: string): Promise<ArrayBuffer>;
88
+ has(ref: string): boolean;
89
+ }
90
+ ```
91
+
92
+ Implementations:
93
+
94
+ - `MemoryResolver` — a `Map`, for tests and for the embed case
95
+ - `BundleResolver` — reads from an unpacked container (Stage 4)
96
+ - `HttpResolver` — `GET {baseUrl}/{hash}`, immutable cache semantics
97
+ - `IndexedDBResolver` — browser-local persistence
98
+ - `ChainResolver` — tries several in order
99
+
100
+ **Decoded-texture cache keyed by hash.** This is where dedup actually pays: a
101
+ thousand systems referencing the same gradient decode it once.
102
+
103
+ ---
104
+
105
+ ## Stage 3 — Migration from base64
106
+
107
+ 04 requires migrations to be pure functions. Blob extraction needs a storage
108
+ target. The resolution:
109
+
110
+ - The **pure** migration rewrites `"data:image/png;base64,..."` →
111
+ `{ "$ref": "sha256:..." }` and emits the decoded bytes into a side-channel on
112
+ the returned object (`__pendingAssets: Map<ref, ArrayBuffer>`).
113
+ - `System.fromJSON` (which is already async) drains `__pendingAssets` into
114
+ whatever resolver it was given, then discards the side-channel.
115
+ - Migrations stay pure. The async lives at the call site where it belongs.
116
+
117
+ **Back-compat is permanent.** Keep accepting `data:` URIs and plain URLs forever;
118
+ normalise to refs on read. Legacy systems must keep loading with no ceremony.
119
+
120
+ ---
121
+
122
+ ## Stage 4 — Bundle container
123
+
124
+ A system plus every asset it references, as one portable file. This is what makes
125
+ a system genuinely self-contained: a JSON whose textures live on somebody else's
126
+ server is not portable, it's a dangling dependency.
127
+
128
+ *Prior art:* Effekseer's `.efkpkg` does exactly this — an effect bundled with every
129
+ resource it references, travelling as one artifact. glTF's `.glb` is the same idea
130
+ in a container three.js users already know.
131
+
132
+ Two viable shapes:
133
+
134
+ - **Zip** — trivially inspectable, universal tooling, streams poorly.
135
+ - **GLB-style binary container** — JSON chunk + blob chunk, fast to parse,
136
+ requires custom tooling.
137
+
138
+ **Recommend zip for v1.** Deflate on already-compressed PNGs is near-free; store
139
+ them uncompressed. Universal tooling matters more than parse speed at this stage,
140
+ and the format is versioned separately from the schema (04) so it can change.
141
+
142
+ ```
143
+ system.nebula (zip)
144
+ ├── manifest.json { bundleVersion, systemRef, assets: [{ ref, mime, bytes }] }
145
+ ├── system.json (version-stamped, refs only)
146
+ └── assets/
147
+ ├── a3f2c1... (raw bytes, filename = hash)
148
+ └── 9b7e04...
149
+ ```
150
+
151
+ **Verify hashes on read.** A ref that doesn't match its bytes is a corrupt or
152
+ tampered bundle and should fail loudly.
153
+
154
+ ---
155
+
156
+ ## Stage 5 — Export modes
157
+
158
+ There are **three** export modes and they are not interchangeable. The bundle
159
+ (Stage 4) is the default. Base64 is a narrow third path, not the export format.
160
+
161
+ | Mode | Output | Use | Assets |
162
+ |---|---|---|---|
163
+ | `bundle` **(default)** | `.nebula` zip | Distributing a system to anyone else | Packed, hash-verified |
164
+ | `refs` | `system.json` + loose asset files | A build pipeline where the bundler/CDN handles assets | External |
165
+ | `embed` | one `system.json` | Copy-paste into a CodePen/gist, single-file demo, no build step | `data:` URIs |
166
+
167
+ **Why `bundle` beats `embed` almost everywhere:** a zip is *also* one
168
+ self-contained portable file. It just doesn't pay the ~33% base64 tax or the
169
+ `JSON.parse` cost on a multi-MB string. Anywhere the goal is "one file I can hand
170
+ someone," `bundle` is strictly better.
171
+
172
+ **`embed`'s only real advantage is pasteability.** A zip cannot be pasted into a
173
+ text editor. That is a genuine use case — plausibly how a lot of three.js
174
+ developers first try the library — so the mode stays and should be a first-class,
175
+ documented affordance. It is not the default and it is not the storage format.
176
+
177
+ **Guard rail:** `embed` should warn (not fail) above a size threshold — suggest
178
+ 2MB of decoded assets. The moment flipbooks are involved, `embed` stops being
179
+ viable and users should be told why rather than discovering it as a mystery stall.
180
+
181
+ Keep base64 as an **explicit opt-in export flag**, not the canonical form.
182
+
183
+ ```js
184
+ system.toBundle() // .nebula zip (default for sharing)
185
+ system.toJSON() // refs (default for build pipelines)
186
+ system.toJSON({ embedAssets: true }) // data: URIs, one self-contained file
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Stage 6 — Runtime & distribution
192
+
193
+ **The bundle is an interchange format, not a runtime format.** Nothing should
194
+ ship a zip to production. This is the single most important thing in this spec to
195
+ get right, and it is currently only implied by the resolver interface.
196
+
197
+ | Phase | Format | Who handles it |
198
+ |---|---|---|
199
+ | **Interchange** | `.nebula` zip | Publishing, downloading, handing a file to a colleague |
200
+ | **Build** | unpacked → `system.json` + loose assets | The bundler |
201
+ | **Runtime** | refs resolved to URLs | The app |
202
+
203
+ The zip exists to move a system from A to B intact. It is unpacked **once, at
204
+ build time**, and never appears in production — the same way nobody ships a zip
205
+ of `node_modules`.
206
+
207
+ ### Why runtime-unzip is actively wrong on the web
208
+
209
+ - **HTTP caching dies.** A zip is one opaque blob. Change one texture and the
210
+ whole thing redownloads. Loose hashed assets cache individually, forever.
211
+ - **No parallel fetch.** The browser will pull six textures concurrently. A zip
212
+ is one serial request, then a decompress.
213
+ - **Main-thread cost** for the unzip, on every load.
214
+ - **No transcoding.** Consumers may want webp/avif, or KTX2 for GPU textures.
215
+ You cannot run an image pipeline over bytes sealed in a zip.
216
+ - **No code splitting.** The system can't be lazily loaded per-route if it's
217
+ welded into a blob.
218
+
219
+ ### Consumer paths
220
+
221
+ **With a build pipeline (Vite/Next/webpack — the common case).**
222
+
223
+ Their bundler *already does content-addressing*: it emits `fire-tex.a3f2c1.png`
224
+ with immutable cache headers. Our hash refs map onto that almost exactly.
225
+
226
+ Ship a **bundler plugin** (`@nebula/vite-plugin`, others later):
227
+
228
+ ```js
229
+ import fireSystem from './effects/fire.nebula'
230
+ // → unpacked at build, assets emitted through Vite's pipeline,
231
+ // refs resolved to hashed URLs, tree-shaken, lazy-loadable
232
+ ```
233
+
234
+ Architecturally this is nearly free: the plugin constructs an `AssetResolver`
235
+ (Stage 2) backed by the bundler's emitted URLs. Small package, high DX leverage.
236
+
237
+ **Without a build step** — Webflow, a CMS, a marketing site, a `<script>` tag.
238
+
239
+ This is where `embed` (Stage 5) earns its keep. One JSON, base64 assets, no asset
240
+ pipeline to configure, no CORS. For an agency dropping one hero effect onto a
241
+ landing page with no tooling, this is the correct answer.
242
+
243
+ **CDN-hosted refs** — viable for a live web page (`HttpResolver`, immutable cache
244
+ keys). **Never for a shipped game**: offline breaks, latency, CORS, a GDPR
245
+ surface from player IPs, and a hard dependency on our uptime. Document it as
246
+ demo/web-embed convenience only, with no SLA.
247
+
248
+ ### Hard architectural constraint
249
+
250
+ **three-nebula core must never contain a zip decoder.**
251
+
252
+ Bundle handling lives in a separate package — `@nebula/bundle` — consumed by the
253
+ editing tools, packaging tools, and the bundler plugin. The runtime only ever
254
+ accepts an `AssetResolver`.
255
+
256
+ This keeps the core small, keeps fflate (or equivalent) out of every consumer's
257
+ bundle, and means the zip-vs-GLB decision in Stage 4 can be revisited later
258
+ without touching the runtime at all.
259
+
260
+ It also validates the Stage 2 resolver design: `HttpResolver` covers
261
+ agency-with-CDN, `MemoryResolver` covers embed, the bundler plugin covers the
262
+ build case — and none of them require the runtime to know that a bundle format
263
+ exists.
264
+
265
+ ---
266
+
267
+ ## Downstream (not this spec — just don't foreclose it)
268
+
269
+ Content-addressing makes several things tractable that are impossible while assets
270
+ are anonymous strings inside documents:
271
+
272
+ - **Shared asset libraries** — curated packs; "what uses this texture?" as a
273
+ query; one asset becoming a dependency of many systems.
274
+ - **Programmatic system generation** — with a tagged, hashed asset store, asset
275
+ selection becomes *retrieval* rather than *generation*.
276
+ - **Asset-level metadata** — licensing, attribution and provenance attach to the
277
+ hash once, rather than to every copy.
278
+
279
+ Nothing in this spec builds any of that. It only needs to avoid ruling it out.
280
+
281
+ ---
282
+
283
+ ## Explicitly out of scope
284
+
285
+ - Any specific storage backend — that's infrastructure, not runtime
286
+ - Texture tagging / search
287
+ - Licensing metadata schema — flag that it will attach to the hash, then leave it
288
+ - Compression / transcoding (KTX2, basis) — real future win, separate spec
@@ -0,0 +1,80 @@
1
+ # three-nebula Modernisation — Spec Index
2
+
3
+ ## Purpose
4
+
5
+ Five specs covering runtime modernisation work for the three-nebula library.
6
+
7
+ **The numbers are identifiers, not execution order.** See the dependency graph below.
8
+
9
+ ## Specs
10
+
11
+ | # | Spec | Shape |
12
+ |---|------|-------|
13
+ | 01 | Emitter Hierarchy (nested emitters, ribbon/trail renderer, pooling) | New capability + schema break |
14
+ | 02 | Determinism & Scrubbing | Architecture, invisible, blocking |
15
+ | 03 | Sound Renderer | New capability, additive |
16
+ | 04 | Schema Versioning | Architecture, enabling |
17
+ | 05 | Content-Addressed Assets | Architecture + schema break |
18
+
19
+ ## Dependency graph
20
+
21
+ ```
22
+ 04 (versioning) ──┬──> 01 (hierarchy)
23
+ │ ▲
24
+ └──> 05 (assets)
25
+
26
+ 02 (determinism) ───────────┴──> 03 (sound)
27
+ ```
28
+
29
+ **Hard constraints:**
30
+
31
+ - **04 lands first.** Both 01 and 05 break the JSON schema. Without a migration
32
+ path in place, every existing saved system is stranded. 04 is small and boring
33
+ and must precede both.
34
+ - **02 entangles with 01.** Child emitter instances need seeds derived from the
35
+ parent particle's ID. Landing 01 without 02 means touching every initializer
36
+ and behaviour twice.
37
+ - **03 depends on 02** for jitter (pitch/offset randomisation must be seeded) and
38
+ on **05** for audio blob storage.
39
+
40
+ Suggested landing order: **04 → 02 → 01 → 05 → 03**
41
+
42
+ 03 (sound) is deliberately last: it is the most additive and least entangled,
43
+ and it is the easiest to defer if time runs short.
44
+
45
+ ## On prior art
46
+
47
+ Several specs cite existing FX tools — PopcornFX, Niagara, Effekseer, glTF — where
48
+ they solved a problem first and solved it well. These are **references, not
49
+ comparisons**: the point is to learn from work that's already been done rather than
50
+ rediscover it, and to give an implementer somewhere concrete to read.
51
+
52
+ Where a design here differs from prior art, that's a deliberate choice and the
53
+ reasoning is stated. Where it copies, that's also deliberate.
54
+
55
+ ## Assumptions flagged for verification
56
+
57
+ These specs were written without repo access. Every spec contains an
58
+ **Audit** stage as Stage 0. Claude Code should run those audits first and
59
+ correct the specs before implementation. In particular, the following are
60
+ **guesses** and may already be solved:
61
+
62
+ - Whether particle pooling exists and how it works
63
+ - Whether a seeded PRNG exists, or whether `Math.random()` is called directly
64
+ - Whether particles carry a stable ID
65
+ - Whether flipbook / sprite-sheet playback already exists in some form
66
+ - The current renderer interface's exact lifecycle hooks
67
+ - Whether the sim uses a fixed timestep or raw delta
68
+
69
+ If the audits contradict a spec, **trust the repo**.
70
+
71
+ ## Out of scope
72
+
73
+ - Anything outside the three-nebula library itself
74
+ - Applications built on top of the library
75
+ - Backend, hosting, or distribution infrastructure
76
+ - **Offline rendering / baking.** Stepping a system headlessly and encoding the
77
+ frames to sheets or video is a *consumer* of this library, not a feature of it:
78
+ it needs `reset()`, `step()` and determinism (02) and nothing else from the
79
+ runtime. The rest is three.js, canvas and WebCodecs. The library's only
80
+ obligation is the headless contract in **02, Stage 6**.