gloop-sim 0.0.0-stage → 1.0.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 (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/dist/assert.d.ts +1 -0
  4. package/dist/assert.js +6 -0
  5. package/dist/assert.js.map +1 -0
  6. package/dist/backend/cpu.d.ts +2 -0
  7. package/dist/backend/cpu.js +23 -0
  8. package/dist/backend/cpu.js.map +1 -0
  9. package/dist/backend/gpu.d.ts +2 -0
  10. package/dist/backend/gpu.js +145 -0
  11. package/dist/backend/gpu.js.map +1 -0
  12. package/dist/backend/index.d.ts +14 -0
  13. package/dist/backend/index.js +23 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/device.d.ts +11 -0
  16. package/dist/device.js +66 -0
  17. package/dist/device.js.map +1 -0
  18. package/dist/index.d.ts +18 -0
  19. package/dist/index.js +18 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/initializer/fixed.d.ts +2 -0
  22. package/dist/initializer/fixed.js +12 -0
  23. package/dist/initializer/fixed.js.map +1 -0
  24. package/dist/initializer/index.d.ts +14 -0
  25. package/dist/initializer/index.js +23 -0
  26. package/dist/initializer/index.js.map +1 -0
  27. package/dist/initializer/random.d.ts +2 -0
  28. package/dist/initializer/random.js +16 -0
  29. package/dist/initializer/random.js.map +1 -0
  30. package/dist/integrator/euler.d.ts +2 -0
  31. package/dist/integrator/euler.js +34 -0
  32. package/dist/integrator/euler.js.map +1 -0
  33. package/dist/integrator/index.d.ts +14 -0
  34. package/dist/integrator/index.js +23 -0
  35. package/dist/integrator/index.js.map +1 -0
  36. package/dist/integrator/semi-implicit-euler.d.ts +2 -0
  37. package/dist/integrator/semi-implicit-euler.js +36 -0
  38. package/dist/integrator/semi-implicit-euler.js.map +1 -0
  39. package/dist/model/elastic-network/cpu.d.ts +2 -0
  40. package/dist/model/elastic-network/cpu.js +90 -0
  41. package/dist/model/elastic-network/cpu.js.map +1 -0
  42. package/dist/model/elastic-network/gpu.d.ts +2 -0
  43. package/dist/model/elastic-network/gpu.js +114 -0
  44. package/dist/model/elastic-network/gpu.js.map +1 -0
  45. package/dist/model/elastic-network/index.d.ts +2 -0
  46. package/dist/model/elastic-network/index.js +69 -0
  47. package/dist/model/elastic-network/index.js.map +1 -0
  48. package/dist/model/index.d.ts +15 -0
  49. package/dist/model/index.js +24 -0
  50. package/dist/model/index.js.map +1 -0
  51. package/dist/model/inertial.d.ts +2 -0
  52. package/dist/model/inertial.js +17 -0
  53. package/dist/model/inertial.js.map +1 -0
  54. package/dist/render/backend/channels.d.ts +17 -0
  55. package/dist/render/backend/channels.js +35 -0
  56. package/dist/render/backend/channels.js.map +1 -0
  57. package/dist/render/backend/cloud.d.ts +77 -0
  58. package/dist/render/backend/cloud.js +29 -0
  59. package/dist/render/backend/cloud.js.map +1 -0
  60. package/dist/render/backend/compile.d.ts +31 -0
  61. package/dist/render/backend/compile.js +73 -0
  62. package/dist/render/backend/compile.js.map +1 -0
  63. package/dist/render/backend/glyph.d.ts +8 -0
  64. package/dist/render/backend/glyph.js +38 -0
  65. package/dist/render/backend/glyph.js.map +1 -0
  66. package/dist/render/backend/glyphs.d.ts +6 -0
  67. package/dist/render/backend/glyphs.js +146 -0
  68. package/dist/render/backend/glyphs.js.map +1 -0
  69. package/dist/render/backend/gpu.d.ts +12 -0
  70. package/dist/render/backend/gpu.js +278 -0
  71. package/dist/render/backend/gpu.js.map +1 -0
  72. package/dist/render/backend/material.d.ts +30 -0
  73. package/dist/render/backend/material.js +57 -0
  74. package/dist/render/backend/material.js.map +1 -0
  75. package/dist/render/backend/particles.d.ts +5 -0
  76. package/dist/render/backend/particles.js +22 -0
  77. package/dist/render/backend/particles.js.map +1 -0
  78. package/dist/render/backend/post.d.ts +29 -0
  79. package/dist/render/backend/post.js +77 -0
  80. package/dist/render/backend/post.js.map +1 -0
  81. package/dist/render/backend/solids.d.ts +10 -0
  82. package/dist/render/backend/solids.js +134 -0
  83. package/dist/render/backend/solids.js.map +1 -0
  84. package/dist/render/camera.d.ts +73 -0
  85. package/dist/render/camera.js +32 -0
  86. package/dist/render/camera.js.map +1 -0
  87. package/dist/render/channel.d.ts +25 -0
  88. package/dist/render/channel.js +9 -0
  89. package/dist/render/channel.js.map +1 -0
  90. package/dist/render/color.d.ts +34 -0
  91. package/dist/render/color.js +56 -0
  92. package/dist/render/color.js.map +1 -0
  93. package/dist/render/effect.d.ts +81 -0
  94. package/dist/render/effect.js +30 -0
  95. package/dist/render/effect.js.map +1 -0
  96. package/dist/render/frame.d.ts +60 -0
  97. package/dist/render/frame.js +52 -0
  98. package/dist/render/frame.js.map +1 -0
  99. package/dist/render/index.d.ts +12 -0
  100. package/dist/render/index.js +2 -0
  101. package/dist/render/index.js.map +1 -0
  102. package/dist/render/layer.d.ts +90 -0
  103. package/dist/render/layer.js +54 -0
  104. package/dist/render/layer.js.map +1 -0
  105. package/dist/render/ramp.d.ts +93 -0
  106. package/dist/render/ramp.js +120 -0
  107. package/dist/render/ramp.js.map +1 -0
  108. package/dist/render/sampler.d.ts +34 -0
  109. package/dist/render/sampler.js +53 -0
  110. package/dist/render/sampler.js.map +1 -0
  111. package/dist/render/signal.d.ts +66 -0
  112. package/dist/render/signal.js +51 -0
  113. package/dist/render/signal.js.map +1 -0
  114. package/dist/render/sources.d.ts +33 -0
  115. package/dist/render/sources.js +46 -0
  116. package/dist/render/sources.js.map +1 -0
  117. package/dist/render/springs/anchors.d.ts +37 -0
  118. package/dist/render/springs/anchors.js +34 -0
  119. package/dist/render/springs/anchors.js.map +1 -0
  120. package/dist/render/springs/construction.d.ts +30 -0
  121. package/dist/render/springs/construction.js +66 -0
  122. package/dist/render/springs/construction.js.map +1 -0
  123. package/dist/render/springs/impulses.d.ts +45 -0
  124. package/dist/render/springs/impulses.js +144 -0
  125. package/dist/render/springs/impulses.js.map +1 -0
  126. package/dist/render/springs/index.d.ts +9 -0
  127. package/dist/render/springs/index.js +2 -0
  128. package/dist/render/springs/index.js.map +1 -0
  129. package/dist/render/springs/link.d.ts +51 -0
  130. package/dist/render/springs/link.js +42 -0
  131. package/dist/render/springs/link.js.map +1 -0
  132. package/dist/render/springs/links.d.ts +70 -0
  133. package/dist/render/springs/links.js +210 -0
  134. package/dist/render/springs/links.js.map +1 -0
  135. package/dist/render/springs/palette.d.ts +36 -0
  136. package/dist/render/springs/palette.js +22 -0
  137. package/dist/render/springs/palette.js.map +1 -0
  138. package/dist/render/springs/preset.d.ts +65 -0
  139. package/dist/render/springs/preset.js +114 -0
  140. package/dist/render/springs/preset.js.map +1 -0
  141. package/dist/render/springs/segment.d.ts +40 -0
  142. package/dist/render/springs/segment.js +81 -0
  143. package/dist/render/springs/segment.js.map +1 -0
  144. package/dist/render/springs/source.d.ts +35 -0
  145. package/dist/render/springs/source.js +70 -0
  146. package/dist/render/springs/source.js.map +1 -0
  147. package/dist/render/springs/springs.d.ts +40 -0
  148. package/dist/render/springs/springs.js +34 -0
  149. package/dist/render/springs/springs.js.map +1 -0
  150. package/dist/render/springs/strain.d.ts +21 -0
  151. package/dist/render/springs/strain.js +39 -0
  152. package/dist/render/springs/strain.js.map +1 -0
  153. package/dist/render/stage.d.ts +96 -0
  154. package/dist/render/stage.js +2 -0
  155. package/dist/render/stage.js.map +1 -0
  156. package/dist/render/view.d.ts +63 -0
  157. package/dist/render/view.js +63 -0
  158. package/dist/render/view.js.map +1 -0
  159. package/dist/swarm.d.ts +5 -0
  160. package/dist/swarm.js +24 -0
  161. package/dist/swarm.js.map +1 -0
  162. package/dist/types.d.ts +290 -0
  163. package/dist/types.js +2 -0
  164. package/dist/types.js.map +1 -0
  165. package/docs/assets/logo.svg +112 -0
  166. package/package.json +102 -3
@@ -0,0 +1,70 @@
1
+ import { type Layer } from "../../render/layer.js";
2
+ import { type ColorInput, type ColorValue, type ScalarInput, type ScalarValue } from "../../render/ramp.js";
3
+ import type { LinkGroup } from "../../render/springs/link.js";
4
+ import type { Springs } from "../../render/springs/source.js";
5
+ /**
6
+ * Render-only fracture: strained links visually split and later re-form,
7
+ * without touching the simulation.
8
+ */
9
+ export interface FractureOptions {
10
+ /**
11
+ * The strain, as a fraction of rest length, at which a link visually breaks.
12
+ */
13
+ readonly threshold: number;
14
+ /**
15
+ * How long, in seconds, a fractured link stays broken before re-forming.
16
+ */
17
+ readonly recovery: number;
18
+ }
19
+ /**
20
+ * A layer of links drawn between particle pairs, styled by constant or
21
+ * signal-driven properties.
22
+ */
23
+ export interface LinksLayer extends Layer<Springs> {
24
+ /**
25
+ * Restricts the layer to links tagged with this group. When omitted, every
26
+ * link is drawn.
27
+ */
28
+ readonly group?: LinkGroup;
29
+ /**
30
+ * The base color of each link band.
31
+ */
32
+ readonly color: ColorValue<Springs>;
33
+ /**
34
+ * The link width in the layer's own width-units; always positive. World
35
+ * thickness scales from it.
36
+ */
37
+ readonly width: ScalarValue<Springs>;
38
+ /**
39
+ * The link opacity, within 0..1.
40
+ */
41
+ readonly opacity: ScalarValue<Springs>;
42
+ /**
43
+ * A narrow emissive highlight, `0..1`, added on top of the link color.
44
+ * Omitted means no emission.
45
+ */
46
+ readonly emissive?: ScalarValue<Springs>;
47
+ /**
48
+ * Chromatic separation: how far, in link-width units, faint red and blue
49
+ * copies of the link are offset to either side of it. Omitted means the link
50
+ * is drawn once, with no separation.
51
+ */
52
+ readonly chromatic?: ScalarValue<Springs>;
53
+ /**
54
+ * Enables render-only fracture on the layer. Disabled when omitted.
55
+ */
56
+ readonly fracture?: FractureOptions;
57
+ }
58
+ /**
59
+ * A links layer over the springs.
60
+ */
61
+ export declare function links({ from, group, color, width, opacity, emissive, chromatic, fracture, }: {
62
+ from: Springs;
63
+ group?: LinkGroup;
64
+ color: ColorInput<Springs>;
65
+ width?: ScalarInput<Springs>;
66
+ opacity?: ScalarInput<Springs>;
67
+ emissive?: ScalarInput<Springs>;
68
+ chromatic?: ScalarInput<Springs>;
69
+ fracture?: FractureOptions;
70
+ }): LinksLayer;
@@ -0,0 +1,210 @@
1
+ import { vec3 } from "three/tsl";
2
+ import { Group } from "three/webgpu";
3
+ import { assert } from "../../assert.js";
4
+ import { attributeNameOf, channelAttribute, channelsOf } from "../../render/backend/channels.js";
5
+ import { compileColor, compileScalar } from "../../render/backend/compile.js";
6
+ import { material as materialFactory } from "../../render/backend/material.js";
7
+ import { PackedPositions } from "../../render/frame.js";
8
+ import { constant, verifyIsNonNegative, verifyIsPositive, verifyIsUnit, } from "../../render/layer.js";
9
+ import { colorValue, scalarValue, } from "../../render/ramp.js";
10
+ import { segmentAttribute, segmentGeometry, segmentMesh, VERTICES_PER_SEGMENT, writeSegment, } from "../../render/springs/segment.js";
11
+ // World-space thickness, in scene units, of a link one width-unit wide. Link
12
+ // widths are otherwise unitless, so this sets their overall scale.
13
+ const LINK_WIDTH_WORLD = 0.011;
14
+ // How strongly the emissive term brightens a link's color under strain.
15
+ const EMISSIVE_GAIN = 3;
16
+ // How much fainter a chromatic fringe is than the link it separates from.
17
+ const FRINGE_OPACITY = 0.4;
18
+ /**
19
+ * A links layer over the springs.
20
+ */
21
+ export function links({ from, group, color, width = 1, opacity = 1, emissive, chromatic, fracture, }) {
22
+ verifyIsPositive(constant(width), "links width");
23
+ verifyIsUnit(constant(opacity), "links opacity");
24
+ verifyIsUnit(constant(emissive), "links emissive");
25
+ verifyIsNonNegative(constant(chromatic), "links chromatic");
26
+ if (fracture !== undefined) {
27
+ assert(fracture.threshold >= 0 && fracture.threshold <= 1, `fracture threshold must be within 0..1`);
28
+ assert(fracture.recovery > 0, `fracture recovery must be positive, but is ${fracture.recovery}`);
29
+ }
30
+ const spec = {
31
+ source: from,
32
+ group,
33
+ color: colorValue(color),
34
+ width: scalarValue(width),
35
+ opacity: scalarValue(opacity),
36
+ emissive: emissive === undefined ? undefined : scalarValue(emissive),
37
+ chromatic: chromatic === undefined ? undefined : scalarValue(chromatic),
38
+ fracture,
39
+ build: () => buildLinks(spec),
40
+ };
41
+ return spec;
42
+ }
43
+ function selectLinks(layer) {
44
+ const selected = [];
45
+ for (const [index, link] of layer.source.links.entries()) {
46
+ if (layer.group === undefined || link.group === layer.group) {
47
+ selected.push(index);
48
+ }
49
+ }
50
+ return selected;
51
+ }
52
+ function createBand({ count, side, color, opacity, additive, channels, }) {
53
+ const { geometry, positions } = segmentGeometry(count);
54
+ const channelAttributes = addChannelAttributes({ geometry, channels, count });
55
+ const material = materialFactory.flat({ color, opacity, additive });
56
+ const mesh = segmentMesh(geometry, material);
57
+ return {
58
+ side,
59
+ mesh,
60
+ positionArray: positions.array,
61
+ channelArrays: channelAttributes.map((values) => values.array),
62
+ flush() {
63
+ positions.needsUpdate = true;
64
+ for (const values of channelAttributes) {
65
+ values.needsUpdate = true;
66
+ }
67
+ },
68
+ dispose() {
69
+ geometry.dispose();
70
+ material.dispose();
71
+ },
72
+ };
73
+ }
74
+ function addChannelAttributes({ geometry, channels, count, }) {
75
+ return channels.map((channel) => {
76
+ const values = segmentAttribute(count, 1);
77
+ geometry.setAttribute(attributeNameOf(channel), values);
78
+ return values;
79
+ });
80
+ }
81
+ /**
82
+ * Builds a links layer as a screen-facing thick-line mesh, its endpoints and
83
+ * channel attributes rewritten each frame. Width is baked into the geometry on
84
+ * the CPU, since WebGPU cannot draw wide lines directly. Chromatic separation
85
+ * adds two additive copies, one red and one blue, offset to either side.
86
+ */
87
+ function buildLinks(layer) {
88
+ const indices = selectLinks(layer);
89
+ const count = indices.length;
90
+ const channels = channelsOf([layer.color, layer.opacity, layer.emissive]);
91
+ const bands = createBands({
92
+ count,
93
+ channels,
94
+ color: linkColor(layer),
95
+ opacity: compileScalar(layer.opacity, channelAttribute),
96
+ chromatic: layer.chromatic !== undefined,
97
+ });
98
+ const group = new Group().add(...bands.map((band) => band.mesh));
99
+ const fractureUntil = new Float32Array(count);
100
+ const channelValues = new Float32Array(channels.length);
101
+ return {
102
+ object: group,
103
+ update(frame) {
104
+ const samples = sampleFrame({ layer, channels, frame });
105
+ for (const [slot, linkIndex] of indices.entries()) {
106
+ writeLink({ layer, bands, samples, slot, linkIndex, fractureUntil, channelValues });
107
+ }
108
+ for (const band of bands) {
109
+ band.flush();
110
+ }
111
+ },
112
+ dispose() {
113
+ for (const band of bands) {
114
+ group.remove(band.mesh);
115
+ band.dispose();
116
+ }
117
+ },
118
+ };
119
+ }
120
+ function sampleFrame({ layer, channels, frame, }) {
121
+ return {
122
+ channelArrays: channels.map((channel) => channel.values(layer.source)),
123
+ widthAt: layer.width.prepare(layer.source),
124
+ chromaticAt: layer.chromatic?.prepare(layer.source),
125
+ strains: layer.fracture === undefined ? undefined : layer.source.strains(),
126
+ points: PackedPositions.of(frame.positions),
127
+ time: frame.time,
128
+ };
129
+ }
130
+ function writeLink({ layer, bands, samples, slot, linkIndex, fractureUntil, channelValues, }) {
131
+ const link = layer.source.links[linkIndex];
132
+ assert(link !== undefined, `link ${linkIndex} is missing`);
133
+ gatherChannelValues({ arrays: samples.channelArrays, linkIndex, target: channelValues });
134
+ const halfWidth = linkHalfWidth({
135
+ width: samples.widthAt(linkIndex),
136
+ fracture: layer.fracture,
137
+ strains: samples.strains,
138
+ linkIndex,
139
+ slot,
140
+ time: samples.time,
141
+ fractureUntil,
142
+ });
143
+ const separation = samples.chromaticAt === undefined ? 0 : samples.chromaticAt(linkIndex) * LINK_WIDTH_WORLD;
144
+ for (const band of bands) {
145
+ writeBandSegment({ band, slot, link, points: samples.points, halfWidth, separation, channelValues });
146
+ }
147
+ }
148
+ function linkColor(layer) {
149
+ const base = compileColor(layer.color, channelAttribute);
150
+ if (layer.emissive === undefined) {
151
+ return base;
152
+ }
153
+ const emissive = compileScalar(layer.emissive, channelAttribute);
154
+ return base.add(base.mul(emissive.mul(EMISSIVE_GAIN)));
155
+ }
156
+ function createBands({ count, channels, color, opacity, chromatic, }) {
157
+ const base = createBand({ count, side: 0, color, opacity, additive: false, channels });
158
+ if (!chromatic) {
159
+ return [base];
160
+ }
161
+ const fringeOpacity = opacity.mul(FRINGE_OPACITY);
162
+ const red = color.mul(vec3(1, 0, 0));
163
+ const blue = color.mul(vec3(0, 0, 1));
164
+ return [
165
+ createBand({ count, side: -1, color: red, opacity: fringeOpacity, additive: true, channels }),
166
+ createBand({ count, side: 1, color: blue, opacity: fringeOpacity, additive: true, channels }),
167
+ base,
168
+ ];
169
+ }
170
+ function gatherChannelValues({ arrays, linkIndex, target, }) {
171
+ for (const [position, array] of arrays.entries()) {
172
+ const value = array[linkIndex];
173
+ assert(value !== undefined, `channel ${position} has no value for link ${linkIndex}`);
174
+ target[position] = value;
175
+ }
176
+ }
177
+ function writeBandSegment({ band, slot, link, points, halfWidth, separation, channelValues, }) {
178
+ writeSegment({
179
+ target: band.positionArray,
180
+ segment: slot,
181
+ a: points.at(link.between[0]),
182
+ b: points.at(link.between[1]),
183
+ halfWidth,
184
+ shift: band.side * separation,
185
+ });
186
+ for (const [position, array] of band.channelArrays.entries()) {
187
+ const value = channelValues[position];
188
+ assert(value !== undefined, `channel ${position} has no value`);
189
+ array.fill(value, slot * VERTICES_PER_SEGMENT, (slot + 1) * VERTICES_PER_SEGMENT);
190
+ }
191
+ }
192
+ function linkHalfWidth({ width, fracture, strains, linkIndex, slot, time, fractureUntil, }) {
193
+ const halfWidth = (width * LINK_WIDTH_WORLD) / 2;
194
+ if (fracture === undefined || strains === undefined) {
195
+ return halfWidth;
196
+ }
197
+ const strain = strains[linkIndex];
198
+ assert(strain !== undefined, `link ${linkIndex} has no strain sample`);
199
+ return fracturedHalfWidth({ fracture, strain, slot, time, fractureUntil, halfWidth });
200
+ }
201
+ function fracturedHalfWidth({ fracture, strain, slot, time, fractureUntil, halfWidth, }) {
202
+ const brokenUntil = fractureUntil[slot];
203
+ assert(brokenUntil !== undefined, `link slot ${slot} has no fracture timer`);
204
+ if (time >= brokenUntil && strain > fracture.threshold) {
205
+ fractureUntil[slot] = time + fracture.recovery;
206
+ return 0;
207
+ }
208
+ return time < brokenUntil ? 0 : halfWidth;
209
+ }
210
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../../src/render/springs/links.ts"],"names":[],"mappings":"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","sourcesContent":["import { vec3 } from \"three/tsl\";\nimport { type BufferAttribute, type BufferGeometry, Group, type Mesh } from \"three/webgpu\";\nimport { assert } from \"@/assert.js\";\nimport { attributeNameOf, channelAttribute, channelsOf } from \"@/render/backend/channels.js\";\nimport { type ColorNode, compileColor, compileScalar, type ScalarNode } from \"@/render/backend/compile.js\";\nimport { material as materialFactory } from \"@/render/backend/material.js\";\nimport type { Channel } from \"@/render/channel.js\";\nimport { type Frame, PackedPositions } from \"@/render/frame.js\";\nimport {\n constant,\n type Layer,\n type LayerObject,\n verifyIsNonNegative,\n verifyIsPositive,\n verifyIsUnit,\n} from \"@/render/layer.js\";\nimport {\n type ColorInput,\n type ColorValue,\n colorValue,\n type ScalarInput,\n type ScalarValue,\n scalarValue,\n} from \"@/render/ramp.js\";\nimport type { Link, LinkGroup } from \"@/render/springs/link.js\";\nimport {\n segmentAttribute,\n segmentGeometry,\n segmentMesh,\n VERTICES_PER_SEGMENT,\n writeSegment,\n} from \"@/render/springs/segment.js\";\nimport type { Springs } from \"@/render/springs/source.js\";\n\n// World-space thickness, in scene units, of a link one width-unit wide. Link\n// widths are otherwise unitless, so this sets their overall scale.\nconst LINK_WIDTH_WORLD = 0.011;\n\n// How strongly the emissive term brightens a link's color under strain.\nconst EMISSIVE_GAIN = 3;\n\n// How much fainter a chromatic fringe is than the link it separates from.\nconst FRINGE_OPACITY = 0.4;\n\n/**\n * Render-only fracture: strained links visually split and later re-form,\n * without touching the simulation.\n */\nexport interface FractureOptions {\n /**\n * The strain, as a fraction of rest length, at which a link visually breaks.\n */\n readonly threshold: number;\n\n /**\n * How long, in seconds, a fractured link stays broken before re-forming.\n */\n readonly recovery: number;\n}\n\n/**\n * A layer of links drawn between particle pairs, styled by constant or\n * signal-driven properties.\n */\nexport interface LinksLayer extends Layer<Springs> {\n /**\n * Restricts the layer to links tagged with this group. When omitted, every\n * link is drawn.\n */\n readonly group?: LinkGroup;\n\n /**\n * The base color of each link band.\n */\n readonly color: ColorValue<Springs>;\n\n /**\n * The link width in the layer's own width-units; always positive. World\n * thickness scales from it.\n */\n readonly width: ScalarValue<Springs>;\n\n /**\n * The link opacity, within 0..1.\n */\n readonly opacity: ScalarValue<Springs>;\n\n /**\n * A narrow emissive highlight, `0..1`, added on top of the link color.\n * Omitted means no emission.\n */\n readonly emissive?: ScalarValue<Springs>;\n\n /**\n * Chromatic separation: how far, in link-width units, faint red and blue\n * copies of the link are offset to either side of it. Omitted means the link\n * is drawn once, with no separation.\n */\n readonly chromatic?: ScalarValue<Springs>;\n\n /**\n * Enables render-only fracture on the layer. Disabled when omitted.\n */\n readonly fracture?: FractureOptions;\n}\n\n/**\n * A links layer over the springs.\n */\nexport function links({\n from,\n group,\n color,\n width = 1,\n opacity = 1,\n emissive,\n chromatic,\n fracture,\n}: {\n from: Springs;\n group?: LinkGroup;\n color: ColorInput<Springs>;\n width?: ScalarInput<Springs>;\n opacity?: ScalarInput<Springs>;\n emissive?: ScalarInput<Springs>;\n chromatic?: ScalarInput<Springs>;\n fracture?: FractureOptions;\n}): LinksLayer {\n verifyIsPositive(constant(width), \"links width\");\n verifyIsUnit(constant(opacity), \"links opacity\");\n verifyIsUnit(constant(emissive), \"links emissive\");\n verifyIsNonNegative(constant(chromatic), \"links chromatic\");\n if (fracture !== undefined) {\n assert(fracture.threshold >= 0 && fracture.threshold <= 1, `fracture threshold must be within 0..1`);\n assert(fracture.recovery > 0, `fracture recovery must be positive, but is ${fracture.recovery}`);\n }\n const spec: LinksLayer = {\n source: from,\n group,\n color: colorValue(color),\n width: scalarValue(width),\n opacity: scalarValue(opacity),\n emissive: emissive === undefined ? undefined : scalarValue(emissive),\n chromatic: chromatic === undefined ? undefined : scalarValue(chromatic),\n fracture,\n build: () => buildLinks(spec),\n };\n return spec;\n}\n\nfunction selectLinks(layer: LinksLayer): number[] {\n const selected: number[] = [];\n for (const [index, link] of layer.source.links.entries()) {\n if (layer.group === undefined || link.group === layer.group) {\n selected.push(index);\n }\n }\n return selected;\n}\n\n/**\n * One drawn copy of the link set: the base band sits on the link, and the two\n * chromatic fringes sit one separation to either side of it.\n */\ninterface Band {\n readonly side: -1 | 0 | 1;\n readonly mesh: Mesh;\n readonly positionArray: Float32Array;\n readonly channelArrays: readonly Float32Array[];\n flush(): void;\n dispose(): void;\n}\n\nfunction createBand({\n count,\n side,\n color,\n opacity,\n additive,\n channels,\n}: {\n count: number;\n side: -1 | 0 | 1;\n color: ColorNode;\n opacity: ScalarNode;\n additive: boolean;\n channels: readonly Channel<Springs>[];\n}): Band {\n const { geometry, positions } = segmentGeometry(count);\n const channelAttributes = addChannelAttributes({ geometry, channels, count });\n const material = materialFactory.flat({ color, opacity, additive });\n const mesh = segmentMesh(geometry, material);\n\n return {\n side,\n mesh,\n positionArray: positions.array as Float32Array,\n channelArrays: channelAttributes.map((values) => values.array as Float32Array),\n flush(): void {\n positions.needsUpdate = true;\n for (const values of channelAttributes) {\n values.needsUpdate = true;\n }\n },\n dispose(): void {\n geometry.dispose();\n material.dispose();\n },\n };\n}\n\nfunction addChannelAttributes({\n geometry,\n channels,\n count,\n}: {\n geometry: BufferGeometry;\n channels: readonly Channel<Springs>[];\n count: number;\n}): BufferAttribute[] {\n return channels.map((channel) => {\n const values = segmentAttribute(count, 1);\n geometry.setAttribute(attributeNameOf(channel), values);\n return values;\n });\n}\n\n/**\n * Builds a links layer as a screen-facing thick-line mesh, its endpoints and\n * channel attributes rewritten each frame. Width is baked into the geometry on\n * the CPU, since WebGPU cannot draw wide lines directly. Chromatic separation\n * adds two additive copies, one red and one blue, offset to either side.\n */\nfunction buildLinks(layer: LinksLayer): LayerObject {\n const indices = selectLinks(layer);\n const count = indices.length;\n const channels = channelsOf<Springs>([layer.color, layer.opacity, layer.emissive]);\n const bands = createBands({\n count,\n channels,\n color: linkColor(layer),\n opacity: compileScalar(layer.opacity, channelAttribute),\n chromatic: layer.chromatic !== undefined,\n });\n\n const group = new Group().add(...bands.map((band) => band.mesh));\n\n const fractureUntil = new Float32Array(count);\n const channelValues = new Float32Array(channels.length);\n\n return {\n object: group,\n update(frame: Frame): void {\n const samples = sampleFrame({ layer, channels, frame });\n for (const [slot, linkIndex] of indices.entries()) {\n writeLink({ layer, bands, samples, slot, linkIndex, fractureUntil, channelValues });\n }\n for (const band of bands) {\n band.flush();\n }\n },\n dispose(): void {\n for (const band of bands) {\n group.remove(band.mesh);\n band.dispose();\n }\n },\n };\n}\n\ninterface FrameSamples {\n readonly channelArrays: readonly Float32Array[];\n readonly widthAt: (link: number) => number;\n readonly chromaticAt: ((link: number) => number) | undefined;\n readonly strains: Float32Array | undefined;\n readonly points: PackedPositions;\n readonly time: number;\n}\n\nfunction sampleFrame({\n layer,\n channels,\n frame,\n}: {\n layer: LinksLayer;\n channels: readonly Channel<Springs>[];\n frame: Frame;\n}): FrameSamples {\n return {\n channelArrays: channels.map((channel) => channel.values(layer.source)),\n widthAt: layer.width.prepare(layer.source),\n chromaticAt: layer.chromatic?.prepare(layer.source),\n strains: layer.fracture === undefined ? undefined : layer.source.strains(),\n points: PackedPositions.of(frame.positions),\n time: frame.time,\n };\n}\n\nfunction writeLink({\n layer,\n bands,\n samples,\n slot,\n linkIndex,\n fractureUntil,\n channelValues,\n}: {\n layer: LinksLayer;\n bands: readonly Band[];\n samples: FrameSamples;\n slot: number;\n linkIndex: number;\n fractureUntil: Float32Array;\n channelValues: Float32Array;\n}): void {\n const link = layer.source.links[linkIndex];\n assert(link !== undefined, `link ${linkIndex} is missing`);\n\n gatherChannelValues({ arrays: samples.channelArrays, linkIndex, target: channelValues });\n\n const halfWidth = linkHalfWidth({\n width: samples.widthAt(linkIndex),\n fracture: layer.fracture,\n strains: samples.strains,\n linkIndex,\n slot,\n time: samples.time,\n fractureUntil,\n });\n const separation = samples.chromaticAt === undefined ? 0 : samples.chromaticAt(linkIndex) * LINK_WIDTH_WORLD;\n\n for (const band of bands) {\n writeBandSegment({ band, slot, link, points: samples.points, halfWidth, separation, channelValues });\n }\n}\n\nfunction linkColor(layer: LinksLayer): ColorNode {\n const base = compileColor(layer.color, channelAttribute);\n if (layer.emissive === undefined) {\n return base;\n }\n const emissive = compileScalar(layer.emissive, channelAttribute);\n return base.add(base.mul(emissive.mul(EMISSIVE_GAIN)));\n}\n\nfunction createBands({\n count,\n channels,\n color,\n opacity,\n chromatic,\n}: {\n count: number;\n channels: readonly Channel<Springs>[];\n color: ColorNode;\n opacity: ScalarNode;\n chromatic: boolean;\n}): Band[] {\n const base = createBand({ count, side: 0, color, opacity, additive: false, channels });\n if (!chromatic) {\n return [base];\n }\n const fringeOpacity = opacity.mul(FRINGE_OPACITY);\n const red = color.mul(vec3(1, 0, 0));\n const blue = color.mul(vec3(0, 0, 1));\n return [\n createBand({ count, side: -1, color: red, opacity: fringeOpacity, additive: true, channels }),\n createBand({ count, side: 1, color: blue, opacity: fringeOpacity, additive: true, channels }),\n base,\n ];\n}\n\nfunction gatherChannelValues({\n arrays,\n linkIndex,\n target,\n}: {\n arrays: readonly Float32Array[];\n linkIndex: number;\n target: Float32Array;\n}): void {\n for (const [position, array] of arrays.entries()) {\n const value = array[linkIndex];\n assert(value !== undefined, `channel ${position} has no value for link ${linkIndex}`);\n target[position] = value;\n }\n}\n\nfunction writeBandSegment({\n band,\n slot,\n link,\n points,\n halfWidth,\n separation,\n channelValues,\n}: {\n band: Band;\n slot: number;\n link: Link;\n points: PackedPositions;\n halfWidth: number;\n separation: number;\n channelValues: Float32Array;\n}): void {\n writeSegment({\n target: band.positionArray,\n segment: slot,\n a: points.at(link.between[0]),\n b: points.at(link.between[1]),\n halfWidth,\n shift: band.side * separation,\n });\n for (const [position, array] of band.channelArrays.entries()) {\n const value = channelValues[position];\n assert(value !== undefined, `channel ${position} has no value`);\n array.fill(value, slot * VERTICES_PER_SEGMENT, (slot + 1) * VERTICES_PER_SEGMENT);\n }\n}\n\nfunction linkHalfWidth({\n width,\n fracture,\n strains,\n linkIndex,\n slot,\n time,\n fractureUntil,\n}: {\n width: number;\n fracture: FractureOptions | undefined;\n strains: Float32Array | undefined;\n linkIndex: number;\n slot: number;\n time: number;\n fractureUntil: Float32Array;\n}): number {\n const halfWidth = (width * LINK_WIDTH_WORLD) / 2;\n if (fracture === undefined || strains === undefined) {\n return halfWidth;\n }\n const strain = strains[linkIndex];\n assert(strain !== undefined, `link ${linkIndex} has no strain sample`);\n return fracturedHalfWidth({ fracture, strain, slot, time, fractureUntil, halfWidth });\n}\n\nfunction fracturedHalfWidth({\n fracture,\n strain,\n slot,\n time,\n fractureUntil,\n halfWidth,\n}: {\n fracture: FractureOptions;\n strain: number;\n slot: number;\n time: number;\n fractureUntil: Float32Array;\n halfWidth: number;\n}): number {\n const brokenUntil = fractureUntil[slot];\n assert(brokenUntil !== undefined, `link slot ${slot} has no fracture timer`);\n if (time >= brokenUntil && strain > fracture.threshold) {\n fractureUntil[slot] = time + fracture.recovery;\n return 0;\n }\n return time < brokenUntil ? 0 : halfWidth;\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import type { Color } from "../../render/color.js";
2
+ export interface Palette {
3
+ /**
4
+ * The background the scene is drawn against.
5
+ */
6
+ readonly void: Color;
7
+ /**
8
+ * The primary foreground color, for the brightest marks and text.
9
+ */
10
+ readonly text: Color;
11
+ /**
12
+ * The color of static structures and secondary text.
13
+ */
14
+ readonly structure: Color;
15
+ /**
16
+ * The color strained links and impulses shift toward.
17
+ */
18
+ readonly stress: Color;
19
+ /**
20
+ * The color a link passes through between rest and {@link Palette.stress}.
21
+ */
22
+ readonly charge: Color;
23
+ /**
24
+ * The color for resting links, borders, and construction geometry.
25
+ */
26
+ readonly dim: Color;
27
+ }
28
+ /**
29
+ * The palette factories exposed on `springs.palette`.
30
+ */
31
+ export declare namespace palette {
32
+ /**
33
+ * The standard spring-network palette, with individual entries overridable.
34
+ */
35
+ function standard(overrides?: Partial<Palette>): Palette;
36
+ }
@@ -0,0 +1,22 @@
1
+ const STANDARD = {
2
+ void: "#07090B",
3
+ text: "#F0F4F8",
4
+ structure: "#546E7A",
5
+ stress: "#B8D8D8",
6
+ charge: "#9D4EDD",
7
+ dim: "#263238",
8
+ };
9
+ /**
10
+ * The palette factories exposed on `springs.palette`.
11
+ */
12
+ export var palette;
13
+ (function (palette) {
14
+ /**
15
+ * The standard spring-network palette, with individual entries overridable.
16
+ */
17
+ function standard(overrides = {}) {
18
+ return { ...STANDARD, ...overrides };
19
+ }
20
+ palette.standard = standard;
21
+ })(palette || (palette = {}));
22
+ //# sourceMappingURL=palette.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"palette.js","sourceRoot":"","sources":["../../../src/render/springs/palette.ts"],"names":[],"mappings":"AAkCA,MAAM,QAAQ,GAAY;IACxB,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,SAAS;IACf,SAAS,EAAE,SAAS;IACpB,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;IACjB,GAAG,EAAE,SAAS;CACf,CAAC;AAEF;;GAEG;AACH,MAAM,KAAW,OAAO,CAOvB;AAPD,WAAiB,OAAO;IACtB;;OAEG;IACH,SAAgB,QAAQ,CAAC,YAA8B,EAAE;QACvD,OAAO,EAAE,GAAG,QAAQ,EAAE,GAAG,SAAS,EAAE,CAAC;IACvC,CAAC;IAFe,gBAAQ,WAEvB,CAAA;AACH,CAAC,EAPgB,OAAO,KAAP,OAAO,QAOvB","sourcesContent":["import type { Color } from \"@/render/color.js\";\n\nexport interface Palette {\n /**\n * The background the scene is drawn against.\n */\n readonly void: Color;\n\n /**\n * The primary foreground color, for the brightest marks and text.\n */\n readonly text: Color;\n\n /**\n * The color of static structures and secondary text.\n */\n readonly structure: Color;\n\n /**\n * The color strained links and impulses shift toward.\n */\n readonly stress: Color;\n\n /**\n * The color a link passes through between rest and {@link Palette.stress}.\n */\n readonly charge: Color;\n\n /**\n * The color for resting links, borders, and construction geometry.\n */\n readonly dim: Color;\n}\n\nconst STANDARD: Palette = {\n void: \"#07090B\",\n text: \"#F0F4F8\",\n structure: \"#546E7A\",\n stress: \"#B8D8D8\",\n charge: \"#9D4EDD\",\n dim: \"#263238\",\n};\n\n/**\n * The palette factories exposed on `springs.palette`.\n */\nexport namespace palette {\n /**\n * The standard spring-network palette, with individual entries overridable.\n */\n export function standard(overrides: Partial<Palette> = {}): Palette {\n return { ...STANDARD, ...overrides };\n }\n}\n"]}
@@ -0,0 +1,65 @@
1
+ import { type Camera } from "../../render/camera.js";
2
+ import { type Effect } from "../../render/effect.js";
3
+ import { type Layer } from "../../render/layer.js";
4
+ import { type Source } from "../../render/sources.js";
5
+ import { type Palette } from "../../render/springs/palette.js";
6
+ import { type Springs } from "../../render/springs/source.js";
7
+ /**
8
+ * Whether the free links use render-only fracture (`"visual"`) or none
9
+ * (`false`).
10
+ */
11
+ export type FractureMode = "visual" | false;
12
+ /**
13
+ * Options for {@link preset.standard}.
14
+ */
15
+ export interface PresetOptions {
16
+ /**
17
+ * The springs the links and impulses layers draw.
18
+ */
19
+ readonly from: Springs;
20
+ /**
21
+ * Overrides for individual palette entries.
22
+ */
23
+ readonly palette?: Partial<Palette>;
24
+ /**
25
+ * Overall strength of the preset, `0..1`. Scales the emissive response,
26
+ * bloom, and impulse frequency together.
27
+ */
28
+ readonly intensity?: number;
29
+ /**
30
+ * Whether to include the construction-geometry layer.
31
+ */
32
+ readonly construction?: boolean;
33
+ /**
34
+ * Whether to include the impulses layer.
35
+ */
36
+ readonly impulses?: boolean;
37
+ /**
38
+ * Render-only fracture behavior for the free links.
39
+ */
40
+ readonly fracture?: FractureMode;
41
+ }
42
+ export interface Preset {
43
+ /**
44
+ * The resolved palette, with any overrides applied.
45
+ */
46
+ readonly palette: Palette;
47
+ /**
48
+ * The layers to draw, in back-to-front order.
49
+ */
50
+ readonly layers: readonly Layer<Source>[];
51
+ /**
52
+ * The post-processing effects applied to the composed frame.
53
+ */
54
+ readonly effects: readonly Effect[];
55
+ /**
56
+ * The camera the scene is viewed through.
57
+ */
58
+ readonly camera: Camera;
59
+ }
60
+ export declare namespace preset {
61
+ /**
62
+ * The standard preset for creating a swarm with springs.
63
+ */
64
+ function standard({ from, palette: overrides, intensity, construction, impulses, fracture, }: PresetOptions): Preset;
65
+ }
@@ -0,0 +1,114 @@
1
+ import { assert } from "../../assert.js";
2
+ import { camera as cameraFactory } from "../../render/camera.js";
3
+ import { effect as effectFactory } from "../../render/effect.js";
4
+ import { layer as layerFactory } from "../../render/layer.js";
5
+ import { ramp as rampFactory } from "../../render/ramp.js";
6
+ import { sources } from "../../render/sources.js";
7
+ import { anchors as anchorsFactory } from "../../render/springs/anchors.js";
8
+ import { construction as constructionFactory } from "../../render/springs/construction.js";
9
+ import { impulses as impulsesFactory } from "../../render/springs/impulses.js";
10
+ import { links as linksFactory } from "../../render/springs/links.js";
11
+ import { palette as paletteFactory } from "../../render/springs/palette.js";
12
+ import { strain as strainSignal } from "../../render/springs/source.js";
13
+ export var preset;
14
+ (function (preset) {
15
+ /**
16
+ * The standard preset for creating a swarm with springs.
17
+ */
18
+ function standard({ from, palette: overrides, intensity = 0.8, construction = true, impulses = true, fracture = "visual", }) {
19
+ assert(intensity >= 0 && intensity <= 1, `preset intensity must be within 0..1, but is ${intensity}`);
20
+ const palette = paletteFactory.standard(overrides);
21
+ const particles = sources.particles();
22
+ const layers = [];
23
+ if (construction) {
24
+ layers.push(constructionLayer(particles, palette));
25
+ }
26
+ layers.push(pinnedLinksLayer(from, palette), freeLinksLayer({ from, palette, intensity, fracture }));
27
+ if (impulses) {
28
+ layers.push(impulsesLayer({ from, palette, intensity }));
29
+ }
30
+ layers.push(particlesLayer(particles, palette), anchorsLayer(from, palette));
31
+ return { palette, layers, effects: standardEffects(intensity), camera: cameraFactory.perspective() };
32
+ }
33
+ preset.standard = standard;
34
+ })(preset || (preset = {}));
35
+ function constructionLayer(particles, palette) {
36
+ return constructionFactory({ from: particles, color: palette.dim, opacity: 0.12, rings: 5 });
37
+ }
38
+ function pinnedLinksLayer(from, palette) {
39
+ return linksFactory({
40
+ from,
41
+ group: "pinned",
42
+ color: palette.structure,
43
+ width: 1.25,
44
+ opacity: 0.35,
45
+ });
46
+ }
47
+ function freeLinksLayer({ from, palette, intensity, fracture, }) {
48
+ const strain = strainSignal({ domain: [0, 0.4] });
49
+ return linksFactory({
50
+ from,
51
+ group: "free",
52
+ color: rampFactory.color({
53
+ by: strain,
54
+ stops: [
55
+ { at: 0, value: palette.dim },
56
+ { at: 0.7, value: palette.structure },
57
+ { at: 1, value: palette.stress },
58
+ ],
59
+ }),
60
+ width: rampFactory.scalar({
61
+ by: strain,
62
+ stops: [
63
+ { at: 0, value: 0.45 },
64
+ { at: 1, value: 2.1 },
65
+ ],
66
+ }),
67
+ opacity: rampFactory.scalar({
68
+ by: strain,
69
+ stops: [
70
+ { at: 0, value: 0.18 },
71
+ { at: 1, value: 0.9 },
72
+ ],
73
+ }),
74
+ emissive: rampFactory.scalar({
75
+ by: strain,
76
+ stops: [
77
+ { at: 0, value: 0 },
78
+ { at: 0.75, value: 0.05 * intensity },
79
+ { at: 1, value: intensity },
80
+ ],
81
+ }),
82
+ fracture: fracture === "visual" ? { threshold: 0.33, recovery: 0.6 } : undefined,
83
+ });
84
+ }
85
+ function impulsesLayer({ from, palette, intensity, }) {
86
+ return impulsesFactory({
87
+ from,
88
+ stress: 0.22,
89
+ color: palette.text,
90
+ tailColor: palette.stress,
91
+ frequency: 0.12 * intensity,
92
+ speed: 1.5,
93
+ length: 0.08,
94
+ });
95
+ }
96
+ function particlesLayer(particles, palette) {
97
+ return layerFactory.particles({
98
+ from: particles,
99
+ shape: "cross",
100
+ color: palette.structure,
101
+ size: 2,
102
+ opacity: 0.8,
103
+ });
104
+ }
105
+ function anchorsLayer(from, palette) {
106
+ return anchorsFactory({ from, shape: "gyroscope", color: palette.structure, size: 4.4, opacity: 0.8 });
107
+ }
108
+ function standardEffects(intensity) {
109
+ return [
110
+ effectFactory.bloom({ threshold: 0.85, intensity: 0.65 * intensity, radius: 0.12 }),
111
+ effectFactory.vignette({ intensity: 0.45, smoothness: 0.85 }),
112
+ ];
113
+ }
114
+ //# sourceMappingURL=preset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preset.js","sourceRoot":"","sources":["../../../src/render/springs/preset.ts"],"names":[],"mappings":"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","sourcesContent":["import { assert } from \"@/assert.js\";\nimport { type Camera, camera as cameraFactory } from \"@/render/camera.js\";\nimport { type Effect, effect as effectFactory } from \"@/render/effect.js\";\nimport { type Layer, layer as layerFactory } from \"@/render/layer.js\";\nimport { ramp as rampFactory } from \"@/render/ramp.js\";\nimport { type ParticleSource, type Source, sources } from \"@/render/sources.js\";\nimport { anchors as anchorsFactory } from \"@/render/springs/anchors.js\";\nimport { construction as constructionFactory } from \"@/render/springs/construction.js\";\nimport { impulses as impulsesFactory } from \"@/render/springs/impulses.js\";\nimport { links as linksFactory } from \"@/render/springs/links.js\";\nimport { type Palette, palette as paletteFactory } from \"@/render/springs/palette.js\";\nimport { type Springs, strain as strainSignal } from \"@/render/springs/source.js\";\n\n/**\n * Whether the free links use render-only fracture (`\"visual\"`) or none\n * (`false`).\n */\nexport type FractureMode = \"visual\" | false;\n\n/**\n * Options for {@link preset.standard}.\n */\nexport interface PresetOptions {\n /**\n * The springs the links and impulses layers draw.\n */\n readonly from: Springs;\n\n /**\n * Overrides for individual palette entries.\n */\n readonly palette?: Partial<Palette>;\n\n /**\n * Overall strength of the preset, `0..1`. Scales the emissive response,\n * bloom, and impulse frequency together.\n */\n readonly intensity?: number;\n\n /**\n * Whether to include the construction-geometry layer.\n */\n readonly construction?: boolean;\n\n /**\n * Whether to include the impulses layer.\n */\n readonly impulses?: boolean;\n\n /**\n * Render-only fracture behavior for the free links.\n */\n readonly fracture?: FractureMode;\n}\n\nexport interface Preset {\n /**\n * The resolved palette, with any overrides applied.\n */\n readonly palette: Palette;\n /**\n * The layers to draw, in back-to-front order.\n */\n readonly layers: readonly Layer<Source>[];\n /**\n * The post-processing effects applied to the composed frame.\n */\n readonly effects: readonly Effect[];\n /**\n * The camera the scene is viewed through.\n */\n readonly camera: Camera;\n}\n\nexport namespace preset {\n /**\n * The standard preset for creating a swarm with springs.\n */\n export function standard({\n from,\n palette: overrides,\n intensity = 0.8,\n construction = true,\n impulses = true,\n fracture = \"visual\",\n }: PresetOptions): Preset {\n assert(intensity >= 0 && intensity <= 1, `preset intensity must be within 0..1, but is ${intensity}`);\n\n const palette: Palette = paletteFactory.standard(overrides);\n const particles = sources.particles();\n\n const layers: Layer<Source>[] = [];\n\n if (construction) {\n layers.push(constructionLayer(particles, palette));\n }\n\n layers.push(pinnedLinksLayer(from, palette), freeLinksLayer({ from, palette, intensity, fracture }));\n\n if (impulses) {\n layers.push(impulsesLayer({ from, palette, intensity }));\n }\n\n layers.push(particlesLayer(particles, palette), anchorsLayer(from, palette));\n\n return { palette, layers, effects: standardEffects(intensity), camera: cameraFactory.perspective() };\n }\n}\n\nfunction constructionLayer(particles: ParticleSource, palette: Palette): Layer<Source> {\n return constructionFactory({ from: particles, color: palette.dim, opacity: 0.12, rings: 5 });\n}\n\nfunction pinnedLinksLayer(from: Springs, palette: Palette): Layer<Source> {\n return linksFactory({\n from,\n group: \"pinned\",\n color: palette.structure,\n width: 1.25,\n opacity: 0.35,\n });\n}\n\nfunction freeLinksLayer({\n from,\n palette,\n intensity,\n fracture,\n}: {\n from: Springs;\n palette: Palette;\n intensity: number;\n fracture: FractureMode;\n}): Layer<Source> {\n const strain = strainSignal({ domain: [0, 0.4] });\n\n return linksFactory({\n from,\n group: \"free\",\n color: rampFactory.color({\n by: strain,\n stops: [\n { at: 0, value: palette.dim },\n { at: 0.7, value: palette.structure },\n { at: 1, value: palette.stress },\n ],\n }),\n width: rampFactory.scalar({\n by: strain,\n stops: [\n { at: 0, value: 0.45 },\n { at: 1, value: 2.1 },\n ],\n }),\n opacity: rampFactory.scalar({\n by: strain,\n stops: [\n { at: 0, value: 0.18 },\n { at: 1, value: 0.9 },\n ],\n }),\n emissive: rampFactory.scalar({\n by: strain,\n stops: [\n { at: 0, value: 0 },\n { at: 0.75, value: 0.05 * intensity },\n { at: 1, value: intensity },\n ],\n }),\n fracture: fracture === \"visual\" ? { threshold: 0.33, recovery: 0.6 } : undefined,\n });\n}\n\nfunction impulsesLayer({\n from,\n palette,\n intensity,\n}: {\n from: Springs;\n palette: Palette;\n intensity: number;\n}): Layer<Source> {\n return impulsesFactory({\n from,\n stress: 0.22,\n color: palette.text,\n tailColor: palette.stress,\n frequency: 0.12 * intensity,\n speed: 1.5,\n length: 0.08,\n });\n}\n\nfunction particlesLayer(particles: ParticleSource, palette: Palette): Layer<Source> {\n return layerFactory.particles({\n from: particles,\n shape: \"cross\",\n color: palette.structure,\n size: 2,\n opacity: 0.8,\n });\n}\n\nfunction anchorsLayer(from: Springs, palette: Palette): Layer<Source> {\n return anchorsFactory({ from, shape: \"gyroscope\", color: palette.structure, size: 4.4, opacity: 0.8 });\n}\n\nfunction standardEffects(intensity: number): Effect[] {\n return [\n effectFactory.bloom({ threshold: 0.85, intensity: 0.65 * intensity, radius: 0.12 }),\n effectFactory.vignette({ intensity: 0.45, smoothness: 0.85 }),\n ];\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import { BufferAttribute, BufferGeometry, type Material, Mesh } from "three/webgpu";
2
+ /**
3
+ * The number of vertices a thick segment expands to.
4
+ */
5
+ export declare const VERTICES_PER_SEGMENT = 6;
6
+ type Point = readonly [number, number, number];
7
+ /**
8
+ * A dynamic buffer attribute holding `itemSize` floats per vertex for `count`
9
+ * segments.
10
+ */
11
+ export declare function segmentAttribute(count: number, itemSize: number): BufferAttribute;
12
+ /**
13
+ * A geometry for `count` segments, with a dynamic position attribute that
14
+ * {@link writeSegment} fills.
15
+ */
16
+ export declare function segmentGeometry(count: number): {
17
+ geometry: BufferGeometry;
18
+ positions: BufferAttribute;
19
+ };
20
+ /**
21
+ * A mesh over segment geometry. Frustum culling is off, since the positions are
22
+ * rewritten every frame and the geometry's bounds are never recomputed.
23
+ */
24
+ export declare function segmentMesh(geometry: BufferGeometry, material: Material): Mesh;
25
+ interface WriteSegmentOptions {
26
+ target: Float32Array;
27
+ segment: number;
28
+ a: Point;
29
+ b: Point;
30
+ halfWidth: number;
31
+ shift?: number;
32
+ }
33
+ /**
34
+ * Writes a thick segment between endpoints `a` and `b` into `target`, drawn as
35
+ * two triangles that lie flat in the screen plane. The width is added at a right
36
+ * angle to the segment, and `shift` slides the whole quad that same way, for
37
+ * drawing offset copies. `segment` picks which slice of `target` to fill.
38
+ */
39
+ export declare function writeSegment({ target, segment, a, b, halfWidth, shift }: WriteSegmentOptions): void;
40
+ export {};