@field/cascade 0.4.0 → 0.5.1
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/AGENTS.md +18 -0
- package/ARCHITECTURE.md +53 -7
- package/CHANGELOG.md +47 -0
- package/CONTRIBUTING.md +33 -0
- package/DESIGN.md +9 -6
- package/README.md +80 -4
- package/dist/assets/{CodeEditor-BNOJIDzL.js → CodeEditor-dOrU3aQg.js} +7 -7
- package/dist/assets/{CodePanel-2wp2G0Fj.js → CodePanel-Bqy85qen.js} +2 -2
- package/dist/assets/{CustomNodeDialog-DNqaVBqA.js → CustomNodeDialog-DkZlOgjs.js} +1 -1
- package/dist/assets/{ProjectSettings-DcdYF2E1.js → ProjectSettings-D192mr6g.js} +2 -2
- package/dist/assets/{SettingsDialog-D2xVvyiR.js → SettingsDialog-BTeeIE3T.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-DW288ykF.js → VersionHistoryDialog-TZQKkn3Y.js} +1 -1
- package/dist/assets/{core-B1y38i95.js → core-DwbcGL0o.js} +2 -2
- package/dist/assets/{cssMode-BLucnaln.js → cssMode-Dcjn356x.js} +1 -1
- package/dist/assets/definitions-C_SY6Bhg.js +1 -0
- package/dist/assets/{editor.api-B3Et3Pph.js → editor.api-D7Q83606.js} +1 -1
- package/dist/assets/{freemarker2-DVlPoano.js → freemarker2-DCCiHlV6.js} +1 -1
- package/dist/assets/geo-CT-wtvaE.js +1 -0
- package/dist/assets/{handlebars-B5FZBAMR.js → handlebars-BCxC8xu1.js} +1 -1
- package/dist/assets/{html-FodD8KbR.js → html-COwepzSd.js} +1 -1
- package/dist/assets/{htmlMode-CsG57VCD.js → htmlMode-Dpg7H0rx.js} +1 -1
- package/dist/assets/{image-B3F-nq25.js → image-CMfMMxlC.js} +3 -3
- package/dist/assets/index-De1cIZ9N.js +212 -0
- package/dist/assets/{index-Y2ock1cw.css → index-ilgbOu6G.css} +1 -1
- package/dist/assets/{javascript-B3gonF_H.js → javascript-CSwWxfLr.js} +1 -1
- package/dist/assets/{jsonMode-Y3w1ahZg.js → jsonMode-BXRriTpj.js} +1 -1
- package/dist/assets/{liquid-CTpWXIYO.js → liquid-Cmkfyogc.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-CODBdKEZ.js → lspLanguageFeatures-xH9JbZGO.js} +1 -1
- package/dist/assets/{mdx-CDAQJZIz.js → mdx-BzrNjNvK.js} +1 -1
- package/dist/assets/nodeTypeUtils-BDJsc2Ks.js +1 -0
- package/dist/assets/pop-BdKEHslJ.js +1 -0
- package/dist/assets/{python-LOIlxJU-.js → python-D4E9jbx7.js} +1 -1
- package/dist/assets/{razor-CllPQSHo.js → razor-B3x9cIEM.js} +1 -1
- package/dist/assets/{tsMode-CO28ut8t.js → tsMode-dhQYPNMZ.js} +1 -1
- package/dist/assets/{typescript-ruU9wKQ_.js → typescript-CITkDxAE.js} +1 -1
- package/dist/assets/{workers-Bc1Ywp4-.js → workers-BQvF9bbO.js} +1 -1
- package/dist/assets/{xml-DoXP2Dh1.js → xml-DvHlU3A8.js} +1 -1
- package/dist/assets/{yaml-D9YPyGiJ.js → yaml-uznsLgs8.js} +1 -1
- package/dist/cli/index.js +16227 -12672
- package/dist/cli/runtime/io.ts +31 -25
- package/dist/contracts/definition.d.ts +21 -2
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/particles.d.ts +54 -0
- package/dist/contracts/particles.js +167 -0
- package/dist/contracts/scene.d.ts +129 -0
- package/dist/contracts/scene.js +73 -0
- package/dist/contracts/schema.js +1 -1
- package/dist/contracts/validate.js +4 -2
- package/dist/contracts/values.d.ts +3 -2
- package/dist/contracts/values.js +9 -0
- package/dist/index.html +3 -3
- package/dist/io.d.ts +4 -16
- package/dist/io.js +26 -8
- package/dist/player/app.js +810 -0
- package/dist/player/embed.d.ts +4 -0
- package/dist/player/embed.js +77 -0
- package/dist/player/types.d.ts +35 -0
- package/dist/runtime/builtins/core/index.d.ts +3 -2
- package/dist/runtime/builtins/core/index.js +6 -2
- package/dist/runtime/builtins/core/structural.d.ts +168 -0
- package/dist/runtime/builtins/core/structural.js +175 -0
- package/dist/runtime/builtins/core/time.d.ts +165 -0
- package/dist/runtime/builtins/core/time.js +110 -0
- package/dist/runtime/builtins/geo/definitions.d.ts +247 -0
- package/dist/runtime/builtins/geo/definitions.js +70 -0
- package/dist/runtime/builtins/geo/index.d.ts +2 -1
- package/dist/runtime/builtins/geo/index.js +3 -1
- package/dist/runtime/builtins/geo/render.d.ts +87 -0
- package/dist/runtime/builtins/geo/render.js +87 -0
- package/dist/runtime/builtins/geo/svgExport.d.ts +11 -0
- package/dist/runtime/builtins/geo/svgExport.js +10 -0
- package/dist/runtime/builtins/index.d.ts +5 -2
- package/dist/runtime/builtins/index.js +13 -3
- package/dist/runtime/builtins/pop/definitions.d.ts +2120 -0
- package/dist/runtime/builtins/pop/definitions.js +815 -0
- package/dist/runtime/builtins/pop/index.d.ts +2 -0
- package/dist/runtime/builtins/pop/index.js +2 -0
- package/dist/runtime/builtins/scene/definitions.d.ts +356 -0
- package/dist/runtime/builtins/scene/definitions.js +224 -0
- package/dist/runtime/builtins/scene/index.d.ts +2 -0
- package/dist/runtime/builtins/scene/index.js +2 -0
- package/dist/runtime/geometry/render.d.ts +109 -0
- package/dist/runtime/geometry/render.js +145 -0
- package/dist/runtime/geometry/svg.d.ts +29 -0
- package/dist/runtime/geometry/svg.js +11 -5
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/pop/cache.d.ts +27 -0
- package/dist/runtime/pop/cache.js +123 -0
- package/dist/runtime/pop/forces.d.ts +202 -0
- package/dist/runtime/pop/forces.js +519 -0
- package/dist/runtime/pop/index.d.ts +5 -0
- package/dist/runtime/pop/index.js +5 -0
- package/dist/runtime/pop/state.d.ts +136 -0
- package/dist/runtime/pop/state.js +283 -0
- package/dist/runtime/pop/trail.d.ts +62 -0
- package/dist/runtime/pop/trail.js +142 -0
- package/dist/runtime/runtime.js +221 -3
- package/dist/runtime/scene/index.d.ts +50 -0
- package/dist/runtime/scene/index.js +94 -0
- package/dist/studio/panel.d.ts +68 -1
- package/doc/HEADLESS_GPU.md +4 -4
- package/doc/NODE_REFERENCE.md +372 -1
- package/doc/PROJECT_AUTHORING.md +16 -4
- package/doc/WEB_PLAYER.md +131 -0
- package/package.json +16 -2
- package/dist/assets/geo-DhyFdBXX.js +0 -1
- package/dist/assets/index-CC6YQw2n.js +0 -212
- package/dist/assets/nodeTypeUtils-BBA1JqHx.js +0 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Geometry } from "@field/cascade/contracts";
|
|
2
|
+
import type { ParticleForce } from "./state.js";
|
|
3
|
+
/**
|
|
4
|
+
* The forces, as plain objects rather than nodes.
|
|
5
|
+
*
|
|
6
|
+
* A force reads the state and returns an acceleration. It never moves
|
|
7
|
+
* anything — integration happens once, in `step`, because two forces that each
|
|
8
|
+
* integrated would disagree about the order of operations and the result would
|
|
9
|
+
* depend on wiring order rather than on physics.
|
|
10
|
+
*
|
|
11
|
+
* Houdini's names for the parameters, per the standing rule.
|
|
12
|
+
*/
|
|
13
|
+
/** Constant acceleration. Houdini's POP Force, whose parameter is `force`. */
|
|
14
|
+
export declare function gravity(force: readonly number[]): ParticleForce;
|
|
15
|
+
/**
|
|
16
|
+
* Velocity-opposing drag. Houdini's POP Drag, parameter `airresist`.
|
|
17
|
+
*
|
|
18
|
+
* Returned as an acceleration rather than applied as a multiplier on velocity,
|
|
19
|
+
* which is the same choice as everywhere else here: a drag that scaled velocity
|
|
20
|
+
* directly would be integrating, and its result would then depend on whether it
|
|
21
|
+
* ran before or after a force that also scaled velocity.
|
|
22
|
+
*/
|
|
23
|
+
export declare function drag(airresist: number): ParticleForce;
|
|
24
|
+
/**
|
|
25
|
+
* A heading read from a noise field — the first of the four forces the
|
|
26
|
+
* living-identity piece applies.
|
|
27
|
+
*
|
|
28
|
+
* Value noise on a hashed lattice rather than a gradient noise: it is a few
|
|
29
|
+
* lines, it is deterministic from `(seed, cell)` with no state, and at the
|
|
30
|
+
* scales this is used at the difference is not visible. A curl noise belongs in
|
|
31
|
+
* the same file later and wants the same hash.
|
|
32
|
+
*
|
|
33
|
+
* `frequency` is in turns per unit, `amplitude` in units per second squared.
|
|
34
|
+
*/
|
|
35
|
+
export declare function noiseField(options: {
|
|
36
|
+
readonly seed: number;
|
|
37
|
+
readonly frequency: number;
|
|
38
|
+
readonly amplitude: number;
|
|
39
|
+
/** Advances the field itself, so the flow evolves. Stated by the caller from
|
|
40
|
+
* the frame, never read from a clock here. */
|
|
41
|
+
readonly evolve?: number;
|
|
42
|
+
}): ParticleForce;
|
|
43
|
+
/**
|
|
44
|
+
* Neighbour separation — the third force, and the expensive one.
|
|
45
|
+
*
|
|
46
|
+
* A uniform grid rebuilt every step, which is what the original does and what
|
|
47
|
+
* `PLAN geometry` says about spatial indexes: they live inside the node that
|
|
48
|
+
* needs them rather than becoming a type. O(n) for the scales here; a system
|
|
49
|
+
* large enough to need something better should say so with a measurement.
|
|
50
|
+
*/
|
|
51
|
+
export declare function separation(options: {
|
|
52
|
+
readonly radius: number;
|
|
53
|
+
readonly strength: number;
|
|
54
|
+
}): ParticleForce;
|
|
55
|
+
/**
|
|
56
|
+
* Deterministic value noise. A hash, not a PRNG: the same seed and cell always
|
|
57
|
+
* give the same number, so nothing carries between particles or frames — the
|
|
58
|
+
* property that makes re-simulation from frame zero reproduce exactly.
|
|
59
|
+
*/
|
|
60
|
+
export declare function valueNoise(seed: number, x: number, y: number, z?: number): number;
|
|
61
|
+
/**
|
|
62
|
+
* A force read from a scalar field — the second of the four the
|
|
63
|
+
* living-identity piece applies, and the one that makes it say something.
|
|
64
|
+
*
|
|
65
|
+
* The original renders its word to an offscreen canvas, blurs it, reads the
|
|
66
|
+
* pixels back and takes central differences to get a gradient. So the target is
|
|
67
|
+
* a **raster field** rather than a path or an SDF, and that matters: particles
|
|
68
|
+
* are pulled toward wherever the field is bright, whatever drew it. Text is one
|
|
69
|
+
* source; a photograph or a noise image would work identically.
|
|
70
|
+
*
|
|
71
|
+
* The field arrives as luminance already sampled into a grid, because reading
|
|
72
|
+
* an `image` port means decoding a file and that belongs in a node rather than
|
|
73
|
+
* in the arithmetic.
|
|
74
|
+
*/
|
|
75
|
+
export declare function fieldForce(options: {
|
|
76
|
+
/** Row-major luminance, 0..1, `width * height` values. */
|
|
77
|
+
readonly field: Float32Array;
|
|
78
|
+
readonly width: number;
|
|
79
|
+
readonly height: number;
|
|
80
|
+
/** The world rectangle the field covers, `[minX, minY, maxX, maxY]`. */
|
|
81
|
+
readonly bounds: readonly [number, number, number, number];
|
|
82
|
+
readonly amplitude: number;
|
|
83
|
+
}): ParticleForce;
|
|
84
|
+
/**
|
|
85
|
+
* Pull particles toward a set of target points. Houdini's POP Attract.
|
|
86
|
+
*
|
|
87
|
+
* The composable half of the field idea. `fieldForce` above takes a sampled
|
|
88
|
+
* raster, which is what the original piece uses; this takes geometry, which is
|
|
89
|
+
* what a graph can wire without any node doing IO — and turning a word, a
|
|
90
|
+
* photograph or a shape into target points is upstream work belonging to
|
|
91
|
+
* whatever node knows about words, photographs or shapes.
|
|
92
|
+
*
|
|
93
|
+
* `radius` is what keeps this affordable: beyond it a target is ignored, so
|
|
94
|
+
* the cost is proportional to nearby targets rather than to all of them. A
|
|
95
|
+
* radius of 0 means every target pulls every particle, which is occasionally
|
|
96
|
+
* what you want and is quadratic.
|
|
97
|
+
*/
|
|
98
|
+
export declare function attract(options: {
|
|
99
|
+
readonly targets: ArrayLike<number>;
|
|
100
|
+
readonly targetSize: number;
|
|
101
|
+
readonly targetCount: number;
|
|
102
|
+
readonly amplitude: number;
|
|
103
|
+
readonly radius: number;
|
|
104
|
+
}): ParticleForce;
|
|
105
|
+
/**
|
|
106
|
+
* A flow field carried as geometry: points with a direction on them.
|
|
107
|
+
*
|
|
108
|
+
* Marcus's description of the original, 2026-09-09: *"a delicate balance
|
|
109
|
+
* between a force pulling the particles towards the centre spine of the type
|
|
110
|
+
* and tangentially around their outlines."* Two components of one field, and
|
|
111
|
+
* that sentence is the whole specification.
|
|
112
|
+
*
|
|
113
|
+
* **Why a blurred field gives you a spine.** A hard letterform's gradient
|
|
114
|
+
* points at the nearest edge, so attracting up it pins particles to outlines.
|
|
115
|
+
* Blur the letterform first and each stroke becomes a ridge whose peak runs
|
|
116
|
+
* down its middle — so the gradient points at the *spine*, and the level sets
|
|
117
|
+
* run *along* the outline. Which is why the original blurs its text before
|
|
118
|
+
* reading pixels back: the blur is not softening, it is what turns an outline
|
|
119
|
+
* into a skeleton.
|
|
120
|
+
*
|
|
121
|
+
* So `normal` pulls up the gradient toward the spine, and `tangential` pushes
|
|
122
|
+
* along the perpendicular — around the outline rather than across it. Balance
|
|
123
|
+
* those two and you get the original's behaviour; take either away and you get
|
|
124
|
+
* a clump or a drift.
|
|
125
|
+
*
|
|
126
|
+
* The field arrives as points carrying `N`, because that is a vector field in
|
|
127
|
+
* Cascade's existing vocabulary and it keeps the rasterising upstream in
|
|
128
|
+
* whatever node knows about type. A core force doing image IO is the thing
|
|
129
|
+
* that stopped being pure earlier.
|
|
130
|
+
*/
|
|
131
|
+
export declare function flowField(options: {
|
|
132
|
+
/** Field sample positions, flat, `size` components each. */
|
|
133
|
+
readonly positions: ArrayLike<number>;
|
|
134
|
+
readonly positionSize: number;
|
|
135
|
+
/** The gradient at each sample, same count, 2 components — pointing uphill,
|
|
136
|
+
* toward the spine. */
|
|
137
|
+
readonly directions: ArrayLike<number>;
|
|
138
|
+
/** The field's value at each sample, 0..1. Optional; without it `hold` can
|
|
139
|
+
* do nothing, because there is no level to hold. */
|
|
140
|
+
readonly levels?: ArrayLike<number>;
|
|
141
|
+
readonly count: number;
|
|
142
|
+
/** Along `N`, toward the spine. */
|
|
143
|
+
readonly normal: number;
|
|
144
|
+
/** Perpendicular to `N`, around the outline. Sign picks the direction. */
|
|
145
|
+
readonly tangential: number;
|
|
146
|
+
/**
|
|
147
|
+
* Hold this level, rather than climb to the peak.
|
|
148
|
+
*
|
|
149
|
+
* Marcus's sharper description, 2026-09-09: *"the lines flow tangentially
|
|
150
|
+
* around the letters in a bundle … loosely describing the type."* A bundle
|
|
151
|
+
* is a **contour**, and a contour is a level set — so the way to get one is
|
|
152
|
+
* not to pull particles toward the spine but to hold them at a chosen
|
|
153
|
+
* distance from it and let the tangent carry them round.
|
|
154
|
+
*
|
|
155
|
+
* Without this, pure tangential flow does produce a bundle and then every
|
|
156
|
+
* particle converges onto the same level set: measured, and it came out as
|
|
157
|
+
* one line along the baseline of the whole word rather than a contour per
|
|
158
|
+
* letter. `hold` is what keeps a particle on the contour it started near.
|
|
159
|
+
*/
|
|
160
|
+
readonly level?: number;
|
|
161
|
+
readonly hold?: number;
|
|
162
|
+
/** Samples beyond this are ignored, and it is also the grid's cell size. */
|
|
163
|
+
readonly radius: number;
|
|
164
|
+
}): ParticleForce;
|
|
165
|
+
/**
|
|
166
|
+
* Houdini's POP convention: a force node **accumulates into a `force`
|
|
167
|
+
* attribute**, and the solver integrates once.
|
|
168
|
+
*
|
|
169
|
+
* Added 2026-09-09, when `pop.Simulate` was rebuilt as a network Marcus can
|
|
170
|
+
* dive into. It is not a refinement — it is the difference between a chain of
|
|
171
|
+
* force nodes that works and one that is silently wrong.
|
|
172
|
+
*
|
|
173
|
+
* The nodes as first written each called `step()` themselves, which integrates.
|
|
174
|
+
* Chain two of them inside a container and time advances **twice per step**:
|
|
175
|
+
* the particles move further than the timestep says, the result changes when
|
|
176
|
+
* you reorder nodes that should commute, and nothing reports an error. Reading
|
|
177
|
+
* that from a picture is close to impossible, which is why the model has to be
|
|
178
|
+
* right rather than tested.
|
|
179
|
+
*
|
|
180
|
+
* So: `accumulateForce` adds an acceleration to the geometry's `force`
|
|
181
|
+
* attribute and moves nothing. `forceAttribute` is the force the solver reads
|
|
182
|
+
* back. The solver integrates and clears it, exactly as a POP network does.
|
|
183
|
+
*/
|
|
184
|
+
export declare function accumulateForce(geometry: Geometry, contribution: Float32Array, size: number): Geometry;
|
|
185
|
+
/**
|
|
186
|
+
* The accumulated `force` on a geometry, as a force the solver can sum with the
|
|
187
|
+
* rest.
|
|
188
|
+
*
|
|
189
|
+
* Takes the geometry rather than reading through `ParticleState`, because the
|
|
190
|
+
* state deliberately carries only the attributes the contract makes mandatory —
|
|
191
|
+
* adding a slot for every attribute a force node might invent is how a state
|
|
192
|
+
* object becomes a bag. The solver reads this off the geometry it was handed.
|
|
193
|
+
*
|
|
194
|
+
* Zero when the attribute is absent, which is the correct reading of "no force
|
|
195
|
+
* node ran": a solver with nothing wired into it should hold the particles
|
|
196
|
+
* still rather than refuse to cook.
|
|
197
|
+
*/
|
|
198
|
+
export declare function forceAttribute(geometry: Geometry): ParticleForce;
|
|
199
|
+
/** Drop the accumulated force. The solver calls this after integrating, so a
|
|
200
|
+
* step never inherits the previous step's forces. */
|
|
201
|
+
export declare function clearForce(geometry: Geometry): Geometry;
|
|
202
|
+
//# sourceMappingURL=forces.d.ts.map
|