@wave3d/core 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/model.d.ts +209 -6
- package/dist/config/model.js +85 -3
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/presets.js +295 -0
- package/dist/presets.js.map +1 -1
- package/dist/renderer/WaveGeometry.js +21 -0
- package/dist/renderer/WaveGeometry.js.map +1 -1
- package/dist/renderer/WaveRenderer.d.ts +62 -0
- package/dist/renderer/WaveRenderer.js +349 -10
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/WaveRendererGPU.js +32 -2
- package/dist/renderer/WaveRendererGPU.js.map +1 -1
- package/dist/renderer/interaction.js +12 -0
- package/dist/renderer/interaction.js.map +1 -1
- package/dist/renderer/particleField.js +26 -2
- package/dist/renderer/particleField.js.map +1 -1
- package/dist/renderer/particleFieldGPU.js +6 -0
- package/dist/renderer/particleFieldGPU.js.map +1 -1
- package/dist/renderer/shaders.js +748 -13
- package/dist/renderer/shaders.js.map +1 -1
- package/dist/renderer/tsl/dissolve.js +56 -0
- package/dist/renderer/tsl/dissolve.js.map +1 -0
- package/dist/renderer/tsl/particleMaterial.js +46 -8
- package/dist/renderer/tsl/particleMaterial.js.map +1 -1
- package/dist/renderer/tsl/uniforms.js +40 -1
- package/dist/renderer/tsl/uniforms.js.map +1 -1
- package/dist/renderer/tsl/waveMaterial.js +275 -26
- package/dist/renderer/tsl/waveMaterial.js.map +1 -1
- package/dist/renderer/tsl/waveShape.js +31 -10
- package/dist/renderer/tsl/waveShape.js.map +1 -1
- package/dist/renderer/wavePath.js +189 -0
- package/dist/renderer/wavePath.js.map +1 -0
- package/dist/shell/createWave.d.ts +23 -3
- package/dist/shell/createWave.js +5 -4
- package/dist/shell/createWave.js.map +1 -1
- package/dist/shell/probe.d.ts +28 -0
- package/dist/shell/probe.js +58 -11
- package/dist/shell/probe.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +3401 -2108
- package/dist/standalone/wave3d.standalone.webgpu.js +7372 -5848
- package/dist/standalone.d.ts +2 -2
- package/dist/standalone.js +2 -2
- package/dist/studio/StudioWaveRenderer.d.ts +115 -8
- package/dist/studio/StudioWaveRenderer.js +588 -14
- package/dist/studio/StudioWaveRenderer.js.map +1 -1
- package/dist/studio/index.d.ts +2 -2
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/randomize.js +0 -1
- package/dist/studio/randomize.js.map +1 -1
- package/package.json +1 -1
- package/skills/wave3d/SKILL.md +142 -5
package/dist/presets.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createDefaultConfig, makeStops, makeWaveSpread } from "./config/model.js";
|
|
2
|
+
import { arcPath } from "./renderer/wavePath.js";
|
|
2
3
|
//#region src/presets.ts
|
|
3
4
|
/**
|
|
4
5
|
* Built-in presets: each a complete studio config (scene + one or more waves) in the wave model.
|
|
@@ -96,6 +97,29 @@ const PARTICLE_PRESETS = {
|
|
|
96
97
|
life: 7
|
|
97
98
|
}
|
|
98
99
|
};
|
|
100
|
+
/** That preset's debris, alternating a coarse INK field with a fine violet one. Each is nearly
|
|
101
|
+
* single-tone on purpose: a continuum between ink and lilac passes through mid-purple, which makes
|
|
102
|
+
* a cloud read as haze instead of black-and-violet debris. */
|
|
103
|
+
const snapDust = (i) => {
|
|
104
|
+
const ink = i % 2 === 0;
|
|
105
|
+
return {
|
|
106
|
+
count: ink ? 5e3 : 3500,
|
|
107
|
+
size: ink ? 15.4 : 8.3,
|
|
108
|
+
seed: 11 + i * 7,
|
|
109
|
+
sizeJitter: 1,
|
|
110
|
+
color: ink ? "#0b0812" : "#9b79ec",
|
|
111
|
+
color2: ink ? "#1d1230" : "#c9b4f6",
|
|
112
|
+
shape: "square",
|
|
113
|
+
blend: "normal",
|
|
114
|
+
edgeBias: 0,
|
|
115
|
+
drift: ink ? 320 : 520,
|
|
116
|
+
rise: 30,
|
|
117
|
+
wander: 60,
|
|
118
|
+
twinkle: 0,
|
|
119
|
+
life: 6,
|
|
120
|
+
speed: 1
|
|
121
|
+
};
|
|
122
|
+
};
|
|
99
123
|
/** Degrees → radians (for the Particle Zoo's per-wave rotation, authored in degrees). */
|
|
100
124
|
const rad = (deg) => deg * Math.PI / 180;
|
|
101
125
|
/** Identity, but it pins a Zoo entry's inline interaction literal to {@link WaveInteractionConfig}:
|
|
@@ -1117,6 +1141,277 @@ const PRESETS = {
|
|
|
1117
1141
|
c.transparentBackground = false;
|
|
1118
1142
|
return c;
|
|
1119
1143
|
},
|
|
1144
|
+
/**
|
|
1145
|
+
* DISINTEGRATION — the "snap". A combed ribbon looping over itself around an open eye, crumbling
|
|
1146
|
+
* into blocky debris across a straight edge down the canvas.
|
|
1147
|
+
*
|
|
1148
|
+
* The form is `wrapAmount` and `helixRoll` TOGETHER, which is the combination worth knowing: wrap
|
|
1149
|
+
* bends the ribbon's length into a loop, roll turns its cross-section as it travels, and a wide
|
|
1150
|
+
* sheet doing both at once folds into broad lobes that cross in front of each other around a hole.
|
|
1151
|
+
* Neither alone gets there — wrap alone is a clean band (a napkin ring), roll alone is a coil that
|
|
1152
|
+
* stays on its axis.
|
|
1153
|
+
*
|
|
1154
|
+
* Two material choices carry the rest, and both are the opposite of what looks obvious:
|
|
1155
|
+
*
|
|
1156
|
+
* - `lineDerivativePower: 0`. The wireframe normally scales strand thickness by the screen-space
|
|
1157
|
+
* uv derivative, which is right for a ribbon seen whole. Framed CLOSE it inverts: the bigger
|
|
1158
|
+
* the sheet is on screen the smaller its derivative, so the strands thin to pale grey exactly
|
|
1159
|
+
* where they should carry ink. At 0 the duty cycle is `lineThickness` alone, crisp at any zoom.
|
|
1160
|
+
* - `lineGapOpacity: 1` — OPAQUE gaps, the theme's default. Clear gaps let the folds layer
|
|
1161
|
+
* through each other, which sounds richer and reads as a transparent weave; opaque ones let
|
|
1162
|
+
* the near lobe HIDE the far one, which is what makes this read as a solid object. The black
|
|
1163
|
+
* masses then come for free: where the surface turns away the strands compress until the page
|
|
1164
|
+
* between them disappears.
|
|
1165
|
+
*
|
|
1166
|
+
* The second wave is the same ribbon at a third of the wrap and 5x the scale — the broad sweep the
|
|
1167
|
+
* loop sits in. Both share one screen-space dissolve front with `dust: 1` pinning the debris to it,
|
|
1168
|
+
* and scroll runs it to 0.95.
|
|
1169
|
+
*/
|
|
1170
|
+
Disintegration: () => {
|
|
1171
|
+
const c = PRESETS["Hero"]();
|
|
1172
|
+
const base = c.waves[0];
|
|
1173
|
+
base.theme = "wireframe";
|
|
1174
|
+
base.usePaletteTexture = false;
|
|
1175
|
+
base.palette = [
|
|
1176
|
+
{
|
|
1177
|
+
color: "#cbb8f5",
|
|
1178
|
+
pos: 0
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
color: "#7c55d8",
|
|
1182
|
+
pos: .03
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
color: "#2a1750",
|
|
1186
|
+
pos: .075
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
color: "#07050c",
|
|
1190
|
+
pos: .15
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
color: "#07050c",
|
|
1194
|
+
pos: .85
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
color: "#2a1750",
|
|
1198
|
+
pos: .925
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
color: "#7c55d8",
|
|
1202
|
+
pos: .97
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
color: "#cbb8f5",
|
|
1206
|
+
pos: 1
|
|
1207
|
+
}
|
|
1208
|
+
];
|
|
1209
|
+
base.gradientType = "linear";
|
|
1210
|
+
base.gradientAngle = 90;
|
|
1211
|
+
base.gradientShift = 0;
|
|
1212
|
+
base.hueShift = 0;
|
|
1213
|
+
base.colorContrast = 1;
|
|
1214
|
+
base.colorSaturation = 1;
|
|
1215
|
+
base.fiberStrength = 0;
|
|
1216
|
+
base.lineAmount = 700;
|
|
1217
|
+
base.lineThickness = 1;
|
|
1218
|
+
base.lineDerivativePower = 0;
|
|
1219
|
+
base.lineSharpness = .96;
|
|
1220
|
+
base.lineDepthFade = 0;
|
|
1221
|
+
base.lineGapColor = "#0a0714";
|
|
1222
|
+
base.lineLight = .8;
|
|
1223
|
+
base.rungAmount = 0;
|
|
1224
|
+
base.edgeFeather = .1;
|
|
1225
|
+
base.blendMode = "normal";
|
|
1226
|
+
base.opacity = 1;
|
|
1227
|
+
base.radialAmount = 0;
|
|
1228
|
+
base.displaceAmount = 60;
|
|
1229
|
+
base.displaceFrequency = {
|
|
1230
|
+
x: .003,
|
|
1231
|
+
y: .008
|
|
1232
|
+
};
|
|
1233
|
+
base.twistFrequency = {
|
|
1234
|
+
x: 0,
|
|
1235
|
+
y: 0,
|
|
1236
|
+
z: 0
|
|
1237
|
+
};
|
|
1238
|
+
base.twistPower = {
|
|
1239
|
+
x: 4,
|
|
1240
|
+
y: 4,
|
|
1241
|
+
z: 4
|
|
1242
|
+
};
|
|
1243
|
+
base.helixRadius = 0;
|
|
1244
|
+
base.helixRoll = 1;
|
|
1245
|
+
base.helixPhase = 0;
|
|
1246
|
+
base.speed = .08;
|
|
1247
|
+
base.position = {
|
|
1248
|
+
x: 0,
|
|
1249
|
+
y: 0,
|
|
1250
|
+
z: 0
|
|
1251
|
+
};
|
|
1252
|
+
base.dissolve = {
|
|
1253
|
+
amount: .5,
|
|
1254
|
+
axis: "screenX",
|
|
1255
|
+
reverse: true,
|
|
1256
|
+
band: .22,
|
|
1257
|
+
scale: 38,
|
|
1258
|
+
blocky: .8,
|
|
1259
|
+
dust: 1
|
|
1260
|
+
};
|
|
1261
|
+
base.interaction = { bindings: [{
|
|
1262
|
+
source: "scroll",
|
|
1263
|
+
target: "dissolveAmount",
|
|
1264
|
+
to: .95,
|
|
1265
|
+
smoothing: .2
|
|
1266
|
+
}] };
|
|
1267
|
+
const loop = structuredClone(base);
|
|
1268
|
+
loop.seed = 0;
|
|
1269
|
+
loop.path = arcPath(1);
|
|
1270
|
+
loop.helixTurns = 1.5;
|
|
1271
|
+
loop.scale = {
|
|
1272
|
+
x: 3.4,
|
|
1273
|
+
y: 3.4,
|
|
1274
|
+
z: 3.4
|
|
1275
|
+
};
|
|
1276
|
+
loop.rotation = {
|
|
1277
|
+
x: 45,
|
|
1278
|
+
y: 25,
|
|
1279
|
+
z: 15
|
|
1280
|
+
};
|
|
1281
|
+
loop.particles = snapDust(0);
|
|
1282
|
+
loop.particles.count = 9e3;
|
|
1283
|
+
loop.particles.size = 18;
|
|
1284
|
+
c.waves = [loop];
|
|
1285
|
+
c.waveCount = 1;
|
|
1286
|
+
c.background = "#f7f6f1";
|
|
1287
|
+
c.backgroundMode = "color";
|
|
1288
|
+
c.transparentBackground = false;
|
|
1289
|
+
c.grain = 0;
|
|
1290
|
+
c.blur = 0;
|
|
1291
|
+
c.ambient = .25;
|
|
1292
|
+
c.lights = [{
|
|
1293
|
+
position: {
|
|
1294
|
+
x: 600,
|
|
1295
|
+
y: 900,
|
|
1296
|
+
z: 800
|
|
1297
|
+
},
|
|
1298
|
+
color: "#ffffff",
|
|
1299
|
+
intensity: 2
|
|
1300
|
+
}];
|
|
1301
|
+
c.cameraDistance = 5001;
|
|
1302
|
+
c.cameraPosition = {
|
|
1303
|
+
x: 0,
|
|
1304
|
+
y: 0,
|
|
1305
|
+
z: 5e3
|
|
1306
|
+
};
|
|
1307
|
+
c.cameraTarget = {
|
|
1308
|
+
x: -40,
|
|
1309
|
+
y: 120,
|
|
1310
|
+
z: 0
|
|
1311
|
+
};
|
|
1312
|
+
c.cameraZoom = .75;
|
|
1313
|
+
c.cameraMinVisibleWidth = .7;
|
|
1314
|
+
return c;
|
|
1315
|
+
},
|
|
1316
|
+
/**
|
|
1317
|
+
* ONE sheet of glass, lit entirely by what is behind it.
|
|
1318
|
+
*
|
|
1319
|
+
* Glass has no colour of its own here — tint is almost off. The colour is a dark field with a few
|
|
1320
|
+
* saturated lamps in it, and the sheet bends and films that: the same trick materials3d's rod
|
|
1321
|
+
* preset uses, where a warm-to-magenta plate behind the glass is what you are actually looking at.
|
|
1322
|
+
* A mesh gradient is that plate, so the pools stay separated instead of averaging to one wash.
|
|
1323
|
+
*
|
|
1324
|
+
* Thin-film iridescence is pushed hard, because it tints only what BOUNCES — the reflection, the
|
|
1325
|
+
* rim and the specular — so the sheet shifts hue along its edges while what you see THROUGH it
|
|
1326
|
+
* stays the colour of the lamp behind. That split is what reads as oil-on-glass rather than as a
|
|
1327
|
+
* coloured object.
|
|
1328
|
+
*/
|
|
1329
|
+
"Liquid Glass": () => {
|
|
1330
|
+
const c = PRESETS["Hero"]();
|
|
1331
|
+
const glass = c.waves[0];
|
|
1332
|
+
glass.theme = "glass";
|
|
1333
|
+
glass.glassRipple = .9;
|
|
1334
|
+
glass.glassRippleScale = .02;
|
|
1335
|
+
glass.glassFlow = .7;
|
|
1336
|
+
glass.glassStrength = 120;
|
|
1337
|
+
glass.glassChroma = 1.1;
|
|
1338
|
+
glass.glassTint = .08;
|
|
1339
|
+
glass.glassIrid = .85;
|
|
1340
|
+
glass.glassFilmNm = 420;
|
|
1341
|
+
glass.glassSpec = 1.5;
|
|
1342
|
+
glass.edgeFeather = .16;
|
|
1343
|
+
glass.scale = {
|
|
1344
|
+
x: 8,
|
|
1345
|
+
y: 7,
|
|
1346
|
+
z: 4
|
|
1347
|
+
};
|
|
1348
|
+
glass.position = {
|
|
1349
|
+
x: 430,
|
|
1350
|
+
y: -300,
|
|
1351
|
+
z: 60
|
|
1352
|
+
};
|
|
1353
|
+
c.waves = [glass];
|
|
1354
|
+
c.waveCount = 1;
|
|
1355
|
+
c.backgroundMode = "gradient";
|
|
1356
|
+
c.backgroundGradientType = "mesh";
|
|
1357
|
+
c.backgroundGradientSource = "stops";
|
|
1358
|
+
c.transparentBackground = false;
|
|
1359
|
+
c.background = "#04060f";
|
|
1360
|
+
c.backgroundMeshSoftness = .7;
|
|
1361
|
+
c.backgroundMeshPoints = [
|
|
1362
|
+
{
|
|
1363
|
+
color: "#ff2d95",
|
|
1364
|
+
x: .74,
|
|
1365
|
+
y: .2,
|
|
1366
|
+
influence: .45
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
color: "#7b5bff",
|
|
1370
|
+
x: .4,
|
|
1371
|
+
y: .46,
|
|
1372
|
+
influence: .45
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
color: "#22d3ee",
|
|
1376
|
+
x: .62,
|
|
1377
|
+
y: .8,
|
|
1378
|
+
influence: .4
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
color: "#ff8a3d",
|
|
1382
|
+
x: .92,
|
|
1383
|
+
y: .58,
|
|
1384
|
+
influence: .32
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
color: "#04060f",
|
|
1388
|
+
x: .06,
|
|
1389
|
+
y: .12,
|
|
1390
|
+
influence: 1
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
color: "#04060f",
|
|
1394
|
+
x: .1,
|
|
1395
|
+
y: .92,
|
|
1396
|
+
influence: 1
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
color: "#04060f",
|
|
1400
|
+
x: .95,
|
|
1401
|
+
y: .95,
|
|
1402
|
+
influence: .9
|
|
1403
|
+
}
|
|
1404
|
+
];
|
|
1405
|
+
c.grain = 0;
|
|
1406
|
+
c.bloomStrength = 0;
|
|
1407
|
+
c.cameraZoom = .45;
|
|
1408
|
+
c.cameraTarget = {
|
|
1409
|
+
x: -40,
|
|
1410
|
+
y: -220,
|
|
1411
|
+
z: 0
|
|
1412
|
+
};
|
|
1413
|
+
return c;
|
|
1414
|
+
},
|
|
1120
1415
|
Kaleidoscope: () => {
|
|
1121
1416
|
const c = PRESETS["Wave 3"]();
|
|
1122
1417
|
const w = c.waves[0];
|
package/dist/presets.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presets.js","names":[],"sources":["../src/presets.ts"],"sourcesContent":["/**\n * Built-in presets: each a complete studio config (scene + one or more waves) in the wave model.\n * IP-clean — no copyrighted assets. The studio layers its own extra presets (and its historical\n * \"Stripe *\" display names) on top; see apps/studio/src/presets.ts.\n */\nimport { createDefaultConfig, makeStops, makeWaveSpread } from \"./config/model\";\nimport type {\n NoiseBand,\n ParticlesConfig,\n StudioConfig,\n WaveInteractionConfig,\n} from \"./config/model\";\n\nconst RAD = 180 / Math.PI;\n\n/**\n * Reusable particle STYLES applied to a wave's `particles` block. Surfaced in the studio's per-wave\n * Particles folder (the \"style\" picker) and used by the \"Particle Zoo\" showcase preset. Each is a dust\n * look independent of the wave it rides — clone before mutating (`structuredClone`).\n */\nexport const PARTICLE_PRESETS: Record<string, ParticlesConfig> = {\n Glitter: {\n count: 16000,\n size: 2.6,\n seed: 7,\n color: \"#ffdca8\",\n shape: \"glitter\",\n edgeBias: 1,\n drift: 300,\n bias: -0.6,\n twinkle: 0.8,\n },\n Embers: {\n count: 16000,\n size: 3.6,\n seed: 7,\n color: \"#ff8a2c\",\n color2: \"#ffe19a\",\n shape: \"soft\",\n edgeBias: 0.15,\n drift: 40,\n rise: 320,\n wander: 120,\n twinkle: 0.85,\n life: 6,\n },\n Snow: {\n count: 12000,\n size: 3,\n seed: 4,\n color: \"#eef4ff\",\n color2: \"#bcd0ee\",\n shape: \"soft\",\n edgeBias: 0.1,\n drift: 20,\n rise: -260,\n wander: 80,\n twinkle: 0.5,\n life: 8,\n },\n Sparks: {\n count: 9000,\n size: 5,\n seed: 9,\n color: \"#ffd27a\",\n color2: \"#fff4d0\",\n shape: \"streak\",\n edgeBias: 1,\n drift: 620,\n rise: 90,\n wander: 20,\n bias: -0.4,\n twinkle: 0.9,\n life: 2.5,\n },\n Fireflies: {\n count: 2200,\n size: 5,\n seed: 11,\n color: \"#dfff9a\",\n color2: \"#8fd94a\",\n shape: \"star\",\n edgeBias: 0.1,\n drift: 25,\n swirl: 0.3,\n wander: 170,\n twinkle: 1,\n life: 5,\n },\n Bubbles: {\n count: 3000,\n size: 7,\n sizeJitter: 0.9,\n seed: 6,\n color: \"#cdeefb\",\n color2: \"#8fd0e6\",\n shape: \"ring\",\n edgeBias: 0.2,\n drift: 30,\n rise: 240,\n wander: 60,\n twinkle: 0.3,\n life: 7,\n },\n};\n\n/** Degrees → radians (for the Particle Zoo's per-wave rotation, authored in degrees). */\nconst rad = (deg: number): number => (deg * Math.PI) / 180;\n\n/** Identity, but it pins a Zoo entry's inline interaction literal to {@link WaveInteractionConfig}:\n * without it the entries are merely INFERRED, so `source` / `target` widen to `string` (needing an\n * `as const` per field) and a misspelt knob would slip through unchecked. */\nconst interaction = (it: WaveInteractionConfig): WaveInteractionConfig => it;\n\n/** Clamp a signed magnitude to ±m (used to cap the Zoo's long-range particle motion terms). */\nconst capMag = (v: number | undefined, m: number): number | undefined =>\n v == null ? v : Math.sign(v) * Math.min(Math.abs(v), m);\n\n/** Build a preset from a set of wave parameters. rotation/hue are given in RADIANS and\n * converted to degrees. All presets are solid-theme, so they reuse the hero palette +\n * surfaceColor fibers (600/0.2) and sheen 0, like the hero. camTarget/zoom frame the\n * wave (we pan the look-at to centre each one). */\nfunction buildPreset(p: {\n speed: number;\n contrast: number;\n sat: number;\n hueRad: number;\n dispX: number;\n dispZ: number;\n dispAmt: number;\n pos: [number, number, number];\n rotRad: [number, number, number];\n scale: [number, number, number];\n twF: [number, number, number];\n twP: [number, number, number];\n glow: [number, number, number];\n grain: number;\n blur: number;\n zoom: number;\n camTarget: [number, number];\n noiseBands?: NoiseBand[];\n twistMotion?: boolean;\n}): StudioConfig {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.speed = p.speed;\n w.colorContrast = p.contrast;\n w.colorSaturation = p.sat;\n w.hueShift = p.hueRad * RAD;\n w.displaceFrequency = { x: p.dispX, y: p.dispZ };\n w.displaceAmount = p.dispAmt;\n w.position = { x: p.pos[0], y: p.pos[1], z: p.pos[2] };\n w.rotation = { x: p.rotRad[0] * RAD, y: p.rotRad[1] * RAD, z: p.rotRad[2] * RAD };\n w.scale = { x: p.scale[0], y: p.scale[1], z: p.scale[2] };\n w.twistFrequency = { x: p.twF[0], y: p.twF[1], z: p.twF[2] };\n w.twistPower = { x: p.twP[0], y: p.twP[1], z: p.twP[2] };\n w.creaseLight = p.glow[0];\n w.creaseSharpness = p.glow[1];\n w.creaseSoftness = p.glow[2];\n if (p.noiseBands) w.noiseBands = p.noiseBands;\n if (p.twistMotion) w.twistMotion = true;\n c.grain = p.grain;\n c.blur = p.blur;\n c.cameraPosition = { x: 100, y: 0, z: 5000 };\n c.cameraTarget = { x: p.camTarget[0], y: p.camTarget[1], z: 0 };\n c.cameraZoom = p.zoom;\n return c;\n}\n\n/** Presets: each a complete studio config (scene + one or more waves) in the wave model. */\nexport const PRESETS: Record<string, () => StudioConfig> = {\n // The app's default wave: a centred, full-frame ribbon (window-independent framing).\n // Shown first and named \"Hero\"; several presets below derive from it.\n Hero: () =>\n buildPreset({\n speed: 0.04,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [380, -301.7, -11.1],\n rotRad: [-0.44959, -0.11759, 1.874407],\n scale: [9, 8, 5],\n twF: [-0.65, 0.41, -0.58],\n twP: [3.63, 0.7, 3.95],\n glow: [1.98, 0.806, 0.834],\n grain: 1.1,\n blur: 0.02,\n zoom: 0.55,\n camTarget: [-420, -200], // user-tuned default framing\n }),\n // Stripe's real hero, recreated faithfully: an orthographic ×10 scene that overflows the\n // frame, so only the twisted crop shows. This is the model's plain default config.\n \"Wave 2\": () => createDefaultConfig(),\n // camTarget on the waves below is a first-pass centring; tune per-wave. NOTE: Wave 4 also\n // uses a variant vertex shader (animated twist-X wobble) we don't fully replicate — its\n // STATIC frame is close, the motion differs.\n \"Wave 3\": () =>\n buildPreset({\n speed: 0.08,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [-200.7, -65.4, -11.1],\n rotRad: [-2.875593, 3.095927, -2.925927],\n scale: [3, 3, 3],\n twF: [0.059, 0.32, -0.397],\n twP: [3.63, 0.44, 5.99],\n glow: [3.86, 0.923, 1],\n grain: 1.2,\n blur: 0.02,\n zoom: 1.3,\n camTarget: [-104, 13], // centred; zoomed in (wide/flat wave)\n }),\n \"Wave 4\": () =>\n buildPreset({\n speed: 0.0525,\n contrast: 0.969,\n sat: 1.383,\n hueRad: 0.0376991,\n dispX: 0.005,\n dispZ: 0.0212,\n dispAmt: 6.68,\n pos: [206.1, -438, -11.1],\n rotRad: [-0.666018, -0.031416, 0.779115],\n scale: [6.0501, 8.3983, 6.9854],\n twF: [-0.424, 0.024, -1.312],\n twP: [1.81, 0.94, 4.76],\n glow: [1.55, 1.174, 0.972],\n grain: 0.576,\n blur: 0,\n zoom: 0.9316,\n camTarget: [194, -402], // centred on the wave\n twistMotion: true, // variant vertex shader — animated twist-X wobble\n noiseBands: [\n {\n startX: 0.856,\n endX: 1,\n startY: 0,\n endY: 0.913,\n feather: 0.5,\n strength: 0.346,\n frequency: 1018,\n colorAttenuation: 1,\n parabolaPower: 0,\n },\n {\n startX: 0.038,\n endX: 0.538,\n startY: 0.105,\n endY: 1,\n feather: 0.3315,\n strength: 1,\n frequency: 190,\n colorAttenuation: 0,\n parabolaPower: 2.11,\n },\n ],\n }),\n // The dark-background hero: identical geometry/camera to the default hero, but theme\n // \"wireframe\" → the line shader on a dark page background, with grain 1.2. Same palette.\n Wireframe: () => {\n const c = createDefaultConfig();\n c.waves[0].theme = \"wireframe\";\n c.grain = 1.2;\n c.background = \"#0a2540\"; // dark navy page background\n c.transparentBackground = false;\n return c;\n },\n \"Neon Dark Multistrand\": () => {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.theme = \"wireframe\"; // line shader on the near-black background — neon wireframe look\n w.blendMode = \"additive\";\n w.palette = makeStops([\"#00f5d4\", \"#00bbf9\", \"#9b5de5\", \"#f15bb5\", \"#fee440\"]);\n w.creaseLight = 1.0;\n c.background = \"#05060c\";\n c.transparentBackground = false; // fill the dark bg so the neon lines read on black (not the page)\n c.waves = makeWaveSpread(w, 3); // three overlapping neon waves\n c.waveCount = 3;\n return c;\n },\n \"Mesh Gradient\": () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n c.grain = 0.3;\n c.blur = 0.008;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n \"Solar Bloom\": () => {\n // Radial gradient: a warm core blooming out to a deep-indigo edge. usePaletteTexture off so\n // our own stops map along the radial gradCoord instead of sampling the baked hero LUT.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"radial\";\n w.gradientShift = 0.14;\n w.palette = [\n { color: \"#fff3c4\", pos: 0 }, // warm-white core\n { color: \"#ffd166\", pos: 0.22 }, // gold\n { color: \"#ff8c42\", pos: 0.42 }, // orange\n { color: \"#ff5d8f\", pos: 0.62 }, // coral-pink\n { color: \"#a64dff\", pos: 0.82 }, // violet\n { color: \"#241246\", pos: 1 }, // deep indigo edge\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.01;\n // Deep warm radial vignette behind the bloom.\n c.background = \"#0a0714\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"radial\";\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#2a1330\", \"#08040f\"]);\n c.transparentBackground = false;\n return c;\n },\n \"Latte Ring\": () => {\n // A warm cream-and-gold ring of combed silk curling around a dark void — the crema swirl on a\n // latte. The camera ORBITS a wide radial fan (arc 286°) so its combed length sweeps across the\n // frame as a flowing arc instead of a head-on fan; a mesh gradient warms it gold→cream with an\n // orange edge, and a noise band frays the fibers finer toward the tips. Exercises the radial wave\n // mode + the particle layer (ambient field + shed-from-edge dust into the void).\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n // A wide radial fan (fans the ribbon's length); the orbiting camera below crops it to a ring.\n w.radialAmount = 0.72;\n w.radialArc = 286;\n w.radialCenter = -160;\n w.radialRadius = 300;\n w.radialSpread = 1.47;\n w.rotation = { x: 0, y: 0, z: 0 };\n w.position = { x: -280, y: -320, z: 0 };\n w.scale = { x: 1.12, y: 1.12, z: 1.12 };\n w.opacity = 0.5;\n // A big broad swell (amount 101.73) with a fine second octave riding on it, so the ring's silk\n // ripples and folds as it turns; `speed` sets the swell's drift rate.\n w.displaceFrequency = { x: 0.0026, y: 0.0048 };\n w.displaceAmount = 101.73;\n w.detailAmount = 6;\n w.detailFrequency = 0.1;\n w.speed = 0.21;\n // Fine combed fibers; the noise band overrides them finer + wispier over the OUTER half (uv.y>0.5)\n // for organic variation instead of a uniform comb. (Bands override fiber params per uv region.)\n w.fiberCount = 110;\n w.fiberStrength = 0.95;\n w.creaseLight = 0.55;\n w.edgeFeather = 0.34;\n w.noiseBands = [\n {\n startX: 0,\n endX: 1,\n startY: 0.5,\n endY: 1,\n feather: 0.4,\n strength: 1,\n frequency: 300,\n colorAttenuation: 0.85,\n parabolaPower: 2.5,\n },\n ];\n // Mesh gradient: gold + cream dominant with an orange accent — the latte's crema tones. A 2D\n // colour field (not a length-wise ramp) so the warmth pools within the fibers.\n w.usePaletteTexture = false;\n w.gradientType = \"mesh\";\n w.meshGradientSoftness = 0.7;\n w.meshGradientPoints = [\n { color: \"#f3c06a\", x: 0.5, y: 0.1, influence: 0.7 }, // warm gold\n { color: \"#f9edd4\", x: 0.34, y: 0.36, influence: 0.62 }, // hot cream\n { color: \"#e6923a\", x: 0.15, y: 0.55, influence: 0.5 }, // orange accent\n { color: \"#f1d49a\", x: 0.82, y: 0.42, influence: 0.55 }, // gold\n { color: \"#ece7d8\", x: 0.58, y: 0.88, influence: 0.72 }, // cool cream\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.15;\n // Orbit the camera around the fan so its length reads as a sweeping arc wrapping the void.\n c.cameraDistance = 600;\n c.cameraPosition = { x: -854.327, y: 135.331, z: 478.048 };\n c.cameraTarget = { x: -720.043, y: 10.575, z: -93.27 };\n c.cameraZoom = 1.222;\n // Scene: a near-black void with gentle bloom on the silk + dust.\n c.background = \"#050404\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n c.grain = 0.25;\n c.blur = 0;\n c.bloomStrength = 0.18;\n c.bloomRadius = 0.55;\n c.bloomThreshold = 0.72;\n // Golden dust shed off the ring's own edge into the void (biased to one flank). Per-wave: it rides\n // this wave's deform and drifts outward from it. edgeBias 1 = spawn on the rim (the shed look).\n w.particles = {\n count: 20000,\n size: 2.9,\n color: \"#ffdca8\",\n seed: 7,\n twinkle: 0.8,\n edgeBias: 1,\n drift: 490,\n bias: -0.6,\n };\n return c;\n },\n \"Particle Zoo\": () => {\n // One scene demoing every particle STYLE — each wave sheds a different kind of dust off its own\n // surface (embers / snow / sparks / fireflies / bubbles). Every wave is a DIFFERENT shape family\n // too (dome / flat sheet / radial fan / helix coil / twist curl), spread apart so none overlap and\n // the frame stays full. The showcase for the per-wave particle system; styles come from\n // PARTICLE_PRESETS (the studio \"preset style\" picker).\n //\n // Each specimen also answers the cursor a DIFFERENT way — agitate / push / click-ripple /\n // param-binding / drag-wake — so the scene doubles as the interactivity showcase, and each one\n // shows its dust reacting alongside its ribbon.\n const c = createDefaultConfig();\n c.background = \"#05070d\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n c.grain = 0.26;\n c.blur = 0;\n c.bloomStrength = 0.3;\n c.bloomThreshold = 0.66; // high, so the central sparks fan glows without blowing the middle out\n c.cameraPosition = { x: 100, y: 0, z: 5000 };\n c.cameraTarget = { x: 0, y: 0, z: 0 };\n c.cameraZoom = 0.6;\n // Shared cursor. A TIGHT radius so hovering one specimen doesn't stir its neighbours: the waves\n // sit ~700-1400 world units apart in a 1250-tall frame, and radius is a fraction of viewport\n // height doubled into NDC-y, so 0.22 → 0.44 NDC-y ≈ one specimen. Touch is opt-in and this is a\n // showcase, so turn it on (listeners are passive — it does not block page scrolling).\n c.interaction = { enabled: true, radius: 0.22, ribbonFlow: 0.5, touch: true };\n // Five well-separated waves, each a DIFFERENT shape family so the silhouettes read as distinct,\n // not five copies of one ribbon. Frame at cameraZoom 0.6 spans ~2222×1250 world units centred on\n // the origin (x ∈ [-1111, 1111], y ∈ [-625, 625], orthographic → screen-xy = world-xy), so the\n // positions/scales below keep the meshes from overlapping. `shape` carries the per-family knobs\n // (displace / twist / helix / radial); `rot` is in degrees. style, palette, opacity as before.\n const zoo = [\n {\n // Embers rising off a broad DOME — low-frequency, high-amplitude displacement mound.\n style: \"Embers\",\n // STIR THE FIRE — cursor agitation. The churn octave stirs the mound and the embers riding\n // it scatter; shove high so the ones already airborne get swept along too.\n interact: interaction({ hover: { agitate: 34, lighten: 0.25 } }),\n shove: 2.5,\n palette: [\"#ff7a1e\", \"#ffd27a\", \"#c23a12\"],\n pos: [-720, 285],\n scale: [1.0, 1.0, 1.0],\n rot: [-20, 0, -14],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.0024, y: 0.0065 },\n displaceAmount: 118,\n twistPower: { x: 2.4, y: 2.4, z: 3.2 },\n },\n },\n {\n // Snow settling on a near-flat drifting SHEET — tilted toward the camera, barely rippled.\n style: \"Snow\",\n // PRESS INTO THE DRIFT — a NEGATIVE push, so the cursor dents the sheet away from you like a\n // palm in deep snow, and the settled flakes sink into the hollow with it. Slow smoothing so\n // the hollow follows heavily; low shove because falling snow shouldn't whip around.\n interact: interaction({ hover: { push: -22, smoothing: 0.45 } }),\n shove: 0.6,\n palette: [\"#8fb8e8\", \"#e8f1fb\", \"#aeb9d6\"],\n pos: [700, 320],\n scale: [1.1, 1.0, 0.9],\n rot: [-62, 0, 12],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.006, y: 0.013 },\n displaceAmount: 22,\n twistPower: { x: 0.8, y: 0.8, z: 1.0 },\n },\n },\n {\n // Sparks bursting off a RADIAL FAN — the one spiky, fanned silhouette; dimmed so it glows\n // without blowing the centre out.\n style: \"Sparks\",\n // STRIKE IT — click/tap only, no hover field. A ring radiates from the hit and, at full\n // shove, visibly blows OUT through the spark burst instead of stopping at the ribbon: the\n // clearest demo of dust reacting to the pointer.\n interact: interaction({ press: { ripple: 46 } }),\n shove: 4,\n palette: [\"#ffd27a\", \"#fff4d0\", \"#c8853a\"],\n pos: [0, -30],\n scale: [0.85, 0.85, 0.85],\n rot: [0, 0, 0],\n opacity: 0.32,\n shape: {\n radialAmount: 0.82,\n radialArc: 118,\n radialCenter: 90, // fan points up\n radialRadius: 40,\n radialSpread: 1.0,\n displaceAmount: 30,\n },\n },\n {\n // Fireflies wandering around a HELIX coil — a screw-thread column of light.\n style: \"Fireflies\",\n // WIND THE COIL — a BINDING, not a pointer field. Hovering turns the helix a full rotation\n // and the fireflies ride round with it, because each field mirrors its wave's live shape\n // uniforms every frame. Deliberately the one specimen with no `hover` block: its particle\n // program never compiles the pointer path, yet its dust still follows the wave.\n interact: interaction({\n bindings: [{ source: \"hover\", target: \"helixPhase\", to: 360, smoothing: 0.5 }],\n }),\n palette: [\"#3d5a24\", \"#7bd23a\", \"#40521f\"],\n pos: [-560, -300],\n scale: [1.25, 1.25, 1.25],\n rot: [0, 0, 8],\n opacity: 0.72,\n shape: {\n helixTurns: 3,\n helixRadius: 44,\n helixRoll: 1,\n displaceAmount: 8,\n twistPower: { x: 1.0, y: 1.0, z: 1.2 },\n },\n },\n {\n // Bubbles rising off a strongly TWISTED curl — an S-ribbon coiling on its length.\n style: \"Bubbles\",\n // DRAG THROUGH THE WATER — the drag-wake. Sweep the cursor and the ribbon behind it is\n // pulled into a trailing trough that heals when you stop, with the bubbles streaming into\n // the wake; `thin` adds a watery translucency right under the cursor.\n interact: interaction({ hover: { wake: 30, thin: 0.45 } }),\n shove: 1.6,\n palette: [\"#2e8fb0\", \"#7fd4e8\", \"#1a5a6e\"],\n pos: [660, -270],\n scale: [1.0, 1.15, 1.0],\n rot: [-16, 0, -20],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.005, y: 0.011 },\n displaceAmount: 46,\n twistFrequency: { x: -0.06, y: 0.09, z: -0.5 },\n twistPower: { x: 4.0, y: 6.0, z: 9.0 },\n },\n },\n ];\n const base = c.waves[0];\n c.waves = zoo.map((z, i) => {\n const w = structuredClone(base);\n w.usePaletteTexture = false;\n w.gradientType = \"linear\";\n w.gradientAngle = 0;\n w.palette = makeStops(z.palette);\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1;\n w.colorSaturation = 1.1;\n w.opacity = z.opacity;\n w.scale = { x: z.scale[0], y: z.scale[1], z: z.scale[2] };\n w.rotation = { x: rad(z.rot[0]), y: rad(z.rot[1]), z: rad(z.rot[2]) };\n w.position = { x: z.pos[0], y: z.pos[1], z: 0 };\n w.seed = i * 7;\n w.speed = 0.05;\n Object.assign(w, z.shape); // the wave's distinct shape family (displace / twist / helix / radial)\n // Each PARTICLE_PRESET is tuned as a standalone hero cloud whose dust flings far; here every\n // specimen should sit ON the wave that emits it, reading as dust clinging to the surface rather\n // than a cloud floating nearby. Pull the motion terms right down (a small fraction of the hero\n // reach + tight clamps) so the dust overlaps its own wave; the styles still read apart by sprite\n // shape / colour / wave shape.\n const p = structuredClone(PARTICLE_PRESETS[z.style]);\n const PULL = 0.13; // fraction of each preset's hero-scale reach kept here\n if (p.drift != null) p.drift = capMag(p.drift * PULL, 24);\n if (p.rise != null) p.rise = capMag(p.rise * PULL, 28);\n if (p.wander != null) p.wander = capMag(p.wander * PULL, 15);\n if (p.swirl != null) p.swirl = capMag(p.swirl * PULL, 0.05);\n if (z.shove != null) p.pointerShove = z.shove;\n w.particles = p;\n // One distinct interaction per specimen (agitate / push / ripple / binding / wake) — see each\n // entry above. Absent would mean inert, so only assign what the entry authored.\n if (z.interact) w.interaction = structuredClone(z.interact);\n return w;\n });\n c.waveCount = c.waves.length;\n return c;\n },\n Holographic: () => {\n // Conic gradient: an iridescent oil-slick sweep. The palette wraps (first ≈ last stop) so\n // the conic seam is invisible.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"conic\";\n w.gradientShift = 0.08;\n w.palette = [\n { color: \"#8ef6e4\", pos: 0 }, // mint (seam)\n { color: \"#6ec3ff\", pos: 0.18 }, // sky\n { color: \"#9b8cff\", pos: 0.36 }, // periwinkle\n { color: \"#ff8ad8\", pos: 0.54 }, // pink\n { color: \"#ffd98e\", pos: 0.72 }, // peach\n { color: \"#a0f0c8\", pos: 0.88 }, // seafoam\n { color: \"#8ef6e4\", pos: 1 }, // mint again (seamless wrap)\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.04;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.12;\n c.grain = 0.28;\n c.blur = 0.01;\n // Subtle deep teal → violet wash behind the iridescence.\n c.background = \"#05060c\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"linear\";\n c.backgroundGradientAngle = 135;\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#04121a\", \"#0a0518\"]);\n c.transparentBackground = false;\n return c;\n },\n Aurora: () => {\n // Mesh gradient: a moody aurora — teals/greens drifting into violet over a night-sky base\n // (distinct from the brighter iOS-style \"Mesh Gradient\").\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a1f3c\", x: 0.08, y: 0.12, influence: 0.62 },\n { color: \"#1fddb0\", x: 0.3, y: 0.7, influence: 0.78 },\n { color: \"#57f5a3\", x: 0.58, y: 0.86, influence: 0.7 },\n { color: \"#3a86ff\", x: 0.82, y: 0.55, influence: 0.62 },\n { color: \"#a15cff\", x: 0.5, y: 0.32, influence: 0.7 },\n { color: \"#071433\", x: 0.92, y: 0.08, influence: 0.6 },\n ];\n w.meshGradientSoftness = 0.72;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.008;\n // Dark night-sky MESH backdrop (also shows off the mesh background type).\n c.background = \"#03060f\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"mesh\";\n c.backgroundMeshPoints = [\n { color: \"#02040c\", x: 0.15, y: 0.85, influence: 0.7 },\n { color: \"#08243a\", x: 0.5, y: 0.5, influence: 0.75 },\n { color: \"#0a0f2e\", x: 0.85, y: 0.7, influence: 0.7 },\n { color: \"#04121a\", x: 0.7, y: 0.2, influence: 0.6 },\n { color: \"#000208\", x: 0.12, y: 0.12, influence: 0.6 },\n ];\n c.backgroundMeshSoftness = 0.75;\n c.transparentBackground = false;\n return c;\n },\n Palestine: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"palestine\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1;\n w.colorSaturation = 1;\n c.grain = 0.35;\n c.background = \"#f2efe8\";\n c.transparentBackground = true;\n return c;\n },\n Spain: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"spain\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.18;\n w.colorSaturation = 1.25;\n w.creaseLight = 1.6; // moderate crease-light: rich crimson without washing to salmon (Hero's is 1.98)\n c.grain = 0.3;\n c.background = \"#1a0608\"; // deep oxblood stage\n c.backgroundMode = \"color\";\n c.transparentBackground = false; // opaque, so the dark stage makes the flag pop\n return c;\n },\n \"Vaporwave Sunset\": () => {\n // The Hero wave re-posed/re-framed, plus the vaporwave palette.\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.position.x = 525;\n w.rotation.x = -0.64 * RAD;\n w.rotation.z = 1.68 * RAD;\n w.paletteSource = \"vaporwave\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.08;\n w.colorSaturation = 1.15;\n w.creaseLight = 1.25;\n c.cameraZoom = 1.1;\n c.cameraTarget = { x: 150, y: 360, z: 0 };\n c.background = \"#09051f\";\n c.transparentBackground = false;\n return c;\n },\n Corkscrew: () => {\n // The helix mode, shown off on its own: `helixRoll` at 1 rolls the ribbon's cross-section in\n // step with the sweep, so the flat strip becomes an auger blade winding around its own length\n // axis, and `helixRadius` lifts that blade off the axis so the turns read as a screw thread\n // rather than a flat twist. No twist at all — this shape is unreachable with twistFrequency,\n // whose expStep angle is monotone and can only ramp once (see the helix docs in config/model).\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.helixTurns = 5;\n w.helixRadius = 45;\n w.helixRoll = 1;\n w.helixPhase = 0;\n w.twistFrequency = { x: 0, y: 0, z: 0 };\n w.twistPower = { x: 4, y: 4, z: 2 };\n // A slow swell along the blade so it breathes; the corkscrew itself is static geometry.\n w.displaceAmount = 16;\n w.displaceFrequency = { x: 0.006, y: 0.0008 };\n w.speed = 0.1;\n w.position = { x: 0, y: 0, z: 0 };\n w.rotation = { x: 0, y: 0, z: 12 }; // tilt so it climbs across the frame\n w.scale = { x: 3, y: 3, z: 1.5 };\n // Mesh gradient: the colour field runs along the blade, so each turn picks up a different part\n // of the spectrum instead of the one hue a linear stop ramp would give.\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n // Framed down the axis rather than side-on: the coil reads as a screw receding into the frame,\n // and each turn shows its blade face instead of an edge. cameraDistance is the orbit radius\n // (= |position − target|); the camera is orthographic, so it's the rig's dolly, not the scale —\n // cameraZoom sets that.\n c.cameraPosition = { x: -526.009, y: -285.284, z: -425.489 };\n c.cameraTarget = { x: -95.046, y: -17.053, z: -105.608 };\n c.cameraDistance = 600;\n c.cameraZoom = 1.176;\n c.grain = 0.3;\n c.blur = 0.008;\n c.bloomStrength = 0.35;\n c.bloomRadius = 0.6;\n c.bloomThreshold = 0.55;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n Kaleidoscope: () => {\n const c = PRESETS[\"Wave 3\"]();\n const w = c.waves[0];\n w.paletteSource = \"kaleidoscope\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.12;\n c.grain = 0.5;\n return c;\n },\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"presets.js","names":[],"sources":["../src/presets.ts"],"sourcesContent":["/**\n * Built-in presets: each a complete studio config (scene + one or more waves) in the wave model.\n * IP-clean — no copyrighted assets. The studio layers its own extra presets (and its historical\n * \"Stripe *\" display names) on top; see apps/studio/src/presets.ts.\n */\nimport { createDefaultConfig, makeStops, makeWaveSpread } from \"./config/model\";\nimport { arcPath } from \"./renderer/wavePath\";\nimport type {\n NoiseBand,\n ParticlesConfig,\n StudioConfig,\n WaveInteractionConfig,\n} from \"./config/model\";\n\nconst RAD = 180 / Math.PI;\n\n/**\n * Reusable particle STYLES applied to a wave's `particles` block. Surfaced in the studio's per-wave\n * Particles folder (the \"style\" picker) and used by the \"Particle Zoo\" showcase preset. Each is a dust\n * look independent of the wave it rides — clone before mutating (`structuredClone`).\n */\nexport const PARTICLE_PRESETS: Record<string, ParticlesConfig> = {\n Glitter: {\n count: 16000,\n size: 2.6,\n seed: 7,\n color: \"#ffdca8\",\n shape: \"glitter\",\n edgeBias: 1,\n drift: 300,\n bias: -0.6,\n twinkle: 0.8,\n },\n Embers: {\n count: 16000,\n size: 3.6,\n seed: 7,\n color: \"#ff8a2c\",\n color2: \"#ffe19a\",\n shape: \"soft\",\n edgeBias: 0.15,\n drift: 40,\n rise: 320,\n wander: 120,\n twinkle: 0.85,\n life: 6,\n },\n Snow: {\n count: 12000,\n size: 3,\n seed: 4,\n color: \"#eef4ff\",\n color2: \"#bcd0ee\",\n shape: \"soft\",\n edgeBias: 0.1,\n drift: 20,\n rise: -260,\n wander: 80,\n twinkle: 0.5,\n life: 8,\n },\n Sparks: {\n count: 9000,\n size: 5,\n seed: 9,\n color: \"#ffd27a\",\n color2: \"#fff4d0\",\n shape: \"streak\",\n edgeBias: 1,\n drift: 620,\n rise: 90,\n wander: 20,\n bias: -0.4,\n twinkle: 0.9,\n life: 2.5,\n },\n Fireflies: {\n count: 2200,\n size: 5,\n seed: 11,\n color: \"#dfff9a\",\n color2: \"#8fd94a\",\n shape: \"star\",\n edgeBias: 0.1,\n drift: 25,\n swirl: 0.3,\n wander: 170,\n twinkle: 1,\n life: 5,\n },\n Bubbles: {\n count: 3000,\n size: 7,\n sizeJitter: 0.9,\n seed: 6,\n color: \"#cdeefb\",\n color2: \"#8fd0e6\",\n shape: \"ring\",\n edgeBias: 0.2,\n drift: 30,\n rise: 240,\n wander: 60,\n twinkle: 0.3,\n life: 7,\n },\n};\n\n/** That preset's debris, alternating a coarse INK field with a fine violet one. Each is nearly\n * single-tone on purpose: a continuum between ink and lilac passes through mid-purple, which makes\n * a cloud read as haze instead of black-and-violet debris. */\nconst snapDust = (i: number): ParticlesConfig => {\n const ink = i % 2 === 0;\n return {\n count: ink ? 5000 : 3500,\n size: ink ? 15.4 : 8.3,\n seed: 11 + i * 7,\n sizeJitter: 1,\n color: ink ? \"#0b0812\" : \"#9b79ec\",\n color2: ink ? \"#1d1230\" : \"#c9b4f6\",\n shape: \"square\",\n blend: \"normal\", // additive can only brighten; this debris has to read DARK on warm paper\n edgeBias: 0, // the front decides which motes leave, not the rim\n drift: ink ? 320 : 520,\n rise: 30,\n wander: 60,\n twinkle: 0,\n life: 6,\n speed: 1,\n };\n};\n\n/** Degrees → radians (for the Particle Zoo's per-wave rotation, authored in degrees). */\nconst rad = (deg: number): number => (deg * Math.PI) / 180;\n\n/** Identity, but it pins a Zoo entry's inline interaction literal to {@link WaveInteractionConfig}:\n * without it the entries are merely INFERRED, so `source` / `target` widen to `string` (needing an\n * `as const` per field) and a misspelt knob would slip through unchecked. */\nconst interaction = (it: WaveInteractionConfig): WaveInteractionConfig => it;\n\n/** Clamp a signed magnitude to ±m (used to cap the Zoo's long-range particle motion terms). */\nconst capMag = (v: number | undefined, m: number): number | undefined =>\n v == null ? v : Math.sign(v) * Math.min(Math.abs(v), m);\n\n/** Build a preset from a set of wave parameters. rotation/hue are given in RADIANS and\n * converted to degrees. All presets are solid-theme, so they reuse the hero palette +\n * surfaceColor fibers (600/0.2) and sheen 0, like the hero. camTarget/zoom frame the\n * wave (we pan the look-at to centre each one). */\nfunction buildPreset(p: {\n speed: number;\n contrast: number;\n sat: number;\n hueRad: number;\n dispX: number;\n dispZ: number;\n dispAmt: number;\n pos: [number, number, number];\n rotRad: [number, number, number];\n scale: [number, number, number];\n twF: [number, number, number];\n twP: [number, number, number];\n glow: [number, number, number];\n grain: number;\n blur: number;\n zoom: number;\n camTarget: [number, number];\n noiseBands?: NoiseBand[];\n twistMotion?: boolean;\n}): StudioConfig {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.speed = p.speed;\n w.colorContrast = p.contrast;\n w.colorSaturation = p.sat;\n w.hueShift = p.hueRad * RAD;\n w.displaceFrequency = { x: p.dispX, y: p.dispZ };\n w.displaceAmount = p.dispAmt;\n w.position = { x: p.pos[0], y: p.pos[1], z: p.pos[2] };\n w.rotation = { x: p.rotRad[0] * RAD, y: p.rotRad[1] * RAD, z: p.rotRad[2] * RAD };\n w.scale = { x: p.scale[0], y: p.scale[1], z: p.scale[2] };\n w.twistFrequency = { x: p.twF[0], y: p.twF[1], z: p.twF[2] };\n w.twistPower = { x: p.twP[0], y: p.twP[1], z: p.twP[2] };\n w.creaseLight = p.glow[0];\n w.creaseSharpness = p.glow[1];\n w.creaseSoftness = p.glow[2];\n if (p.noiseBands) w.noiseBands = p.noiseBands;\n if (p.twistMotion) w.twistMotion = true;\n c.grain = p.grain;\n c.blur = p.blur;\n c.cameraPosition = { x: 100, y: 0, z: 5000 };\n c.cameraTarget = { x: p.camTarget[0], y: p.camTarget[1], z: 0 };\n c.cameraZoom = p.zoom;\n return c;\n}\n\n/** Presets: each a complete studio config (scene + one or more waves) in the wave model. */\nexport const PRESETS: Record<string, () => StudioConfig> = {\n // The app's default wave: a centred, full-frame ribbon (window-independent framing).\n // Shown first and named \"Hero\"; several presets below derive from it.\n Hero: () =>\n buildPreset({\n speed: 0.04,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [380, -301.7, -11.1],\n rotRad: [-0.44959, -0.11759, 1.874407],\n scale: [9, 8, 5],\n twF: [-0.65, 0.41, -0.58],\n twP: [3.63, 0.7, 3.95],\n glow: [1.98, 0.806, 0.834],\n grain: 1.1,\n blur: 0.02,\n zoom: 0.55,\n camTarget: [-420, -200], // user-tuned default framing\n }),\n // Stripe's real hero, recreated faithfully: an orthographic ×10 scene that overflows the\n // frame, so only the twisted crop shows. This is the model's plain default config.\n \"Wave 2\": () => createDefaultConfig(),\n // camTarget on the waves below is a first-pass centring; tune per-wave. NOTE: Wave 4 also\n // uses a variant vertex shader (animated twist-X wobble) we don't fully replicate — its\n // STATIC frame is close, the motion differs.\n \"Wave 3\": () =>\n buildPreset({\n speed: 0.08,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [-200.7, -65.4, -11.1],\n rotRad: [-2.875593, 3.095927, -2.925927],\n scale: [3, 3, 3],\n twF: [0.059, 0.32, -0.397],\n twP: [3.63, 0.44, 5.99],\n glow: [3.86, 0.923, 1],\n grain: 1.2,\n blur: 0.02,\n zoom: 1.3,\n camTarget: [-104, 13], // centred; zoomed in (wide/flat wave)\n }),\n \"Wave 4\": () =>\n buildPreset({\n speed: 0.0525,\n contrast: 0.969,\n sat: 1.383,\n hueRad: 0.0376991,\n dispX: 0.005,\n dispZ: 0.0212,\n dispAmt: 6.68,\n pos: [206.1, -438, -11.1],\n rotRad: [-0.666018, -0.031416, 0.779115],\n scale: [6.0501, 8.3983, 6.9854],\n twF: [-0.424, 0.024, -1.312],\n twP: [1.81, 0.94, 4.76],\n glow: [1.55, 1.174, 0.972],\n grain: 0.576,\n blur: 0,\n zoom: 0.9316,\n camTarget: [194, -402], // centred on the wave\n twistMotion: true, // variant vertex shader — animated twist-X wobble\n noiseBands: [\n {\n startX: 0.856,\n endX: 1,\n startY: 0,\n endY: 0.913,\n feather: 0.5,\n strength: 0.346,\n frequency: 1018,\n colorAttenuation: 1,\n parabolaPower: 0,\n },\n {\n startX: 0.038,\n endX: 0.538,\n startY: 0.105,\n endY: 1,\n feather: 0.3315,\n strength: 1,\n frequency: 190,\n colorAttenuation: 0,\n parabolaPower: 2.11,\n },\n ],\n }),\n // The dark-background hero: identical geometry/camera to the default hero, but theme\n // \"wireframe\" → the line shader on a dark page background, with grain 1.2. Same palette.\n Wireframe: () => {\n const c = createDefaultConfig();\n c.waves[0].theme = \"wireframe\";\n c.grain = 1.2;\n c.background = \"#0a2540\"; // dark navy page background\n c.transparentBackground = false;\n return c;\n },\n \"Neon Dark Multistrand\": () => {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.theme = \"wireframe\"; // line shader on the near-black background — neon wireframe look\n w.blendMode = \"additive\";\n w.palette = makeStops([\"#00f5d4\", \"#00bbf9\", \"#9b5de5\", \"#f15bb5\", \"#fee440\"]);\n w.creaseLight = 1.0;\n c.background = \"#05060c\";\n c.transparentBackground = false; // fill the dark bg so the neon lines read on black (not the page)\n c.waves = makeWaveSpread(w, 3); // three overlapping neon waves\n c.waveCount = 3;\n return c;\n },\n \"Mesh Gradient\": () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n c.grain = 0.3;\n c.blur = 0.008;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n \"Solar Bloom\": () => {\n // Radial gradient: a warm core blooming out to a deep-indigo edge. usePaletteTexture off so\n // our own stops map along the radial gradCoord instead of sampling the baked hero LUT.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"radial\";\n w.gradientShift = 0.14;\n w.palette = [\n { color: \"#fff3c4\", pos: 0 }, // warm-white core\n { color: \"#ffd166\", pos: 0.22 }, // gold\n { color: \"#ff8c42\", pos: 0.42 }, // orange\n { color: \"#ff5d8f\", pos: 0.62 }, // coral-pink\n { color: \"#a64dff\", pos: 0.82 }, // violet\n { color: \"#241246\", pos: 1 }, // deep indigo edge\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.01;\n // Deep warm radial vignette behind the bloom.\n c.background = \"#0a0714\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"radial\";\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#2a1330\", \"#08040f\"]);\n c.transparentBackground = false;\n return c;\n },\n \"Latte Ring\": () => {\n // A warm cream-and-gold ring of combed silk curling around a dark void — the crema swirl on a\n // latte. The camera ORBITS a wide radial fan (arc 286°) so its combed length sweeps across the\n // frame as a flowing arc instead of a head-on fan; a mesh gradient warms it gold→cream with an\n // orange edge, and a noise band frays the fibers finer toward the tips. Exercises the radial wave\n // mode + the particle layer (ambient field + shed-from-edge dust into the void).\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n // A wide radial fan (fans the ribbon's length); the orbiting camera below crops it to a ring.\n w.radialAmount = 0.72;\n w.radialArc = 286;\n w.radialCenter = -160;\n w.radialRadius = 300;\n w.radialSpread = 1.47;\n w.rotation = { x: 0, y: 0, z: 0 };\n w.position = { x: -280, y: -320, z: 0 };\n w.scale = { x: 1.12, y: 1.12, z: 1.12 };\n w.opacity = 0.5;\n // A big broad swell (amount 101.73) with a fine second octave riding on it, so the ring's silk\n // ripples and folds as it turns; `speed` sets the swell's drift rate.\n w.displaceFrequency = { x: 0.0026, y: 0.0048 };\n w.displaceAmount = 101.73;\n w.detailAmount = 6;\n w.detailFrequency = 0.1;\n w.speed = 0.21;\n // Fine combed fibers; the noise band overrides them finer + wispier over the OUTER half (uv.y>0.5)\n // for organic variation instead of a uniform comb. (Bands override fiber params per uv region.)\n w.fiberCount = 110;\n w.fiberStrength = 0.95;\n w.creaseLight = 0.55;\n w.edgeFeather = 0.34;\n w.noiseBands = [\n {\n startX: 0,\n endX: 1,\n startY: 0.5,\n endY: 1,\n feather: 0.4,\n strength: 1,\n frequency: 300,\n colorAttenuation: 0.85,\n parabolaPower: 2.5,\n },\n ];\n // Mesh gradient: gold + cream dominant with an orange accent — the latte's crema tones. A 2D\n // colour field (not a length-wise ramp) so the warmth pools within the fibers.\n w.usePaletteTexture = false;\n w.gradientType = \"mesh\";\n w.meshGradientSoftness = 0.7;\n w.meshGradientPoints = [\n { color: \"#f3c06a\", x: 0.5, y: 0.1, influence: 0.7 }, // warm gold\n { color: \"#f9edd4\", x: 0.34, y: 0.36, influence: 0.62 }, // hot cream\n { color: \"#e6923a\", x: 0.15, y: 0.55, influence: 0.5 }, // orange accent\n { color: \"#f1d49a\", x: 0.82, y: 0.42, influence: 0.55 }, // gold\n { color: \"#ece7d8\", x: 0.58, y: 0.88, influence: 0.72 }, // cool cream\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.15;\n // Orbit the camera around the fan so its length reads as a sweeping arc wrapping the void.\n c.cameraDistance = 600;\n c.cameraPosition = { x: -854.327, y: 135.331, z: 478.048 };\n c.cameraTarget = { x: -720.043, y: 10.575, z: -93.27 };\n c.cameraZoom = 1.222;\n // Scene: a near-black void with gentle bloom on the silk + dust.\n c.background = \"#050404\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n c.grain = 0.25;\n c.blur = 0;\n c.bloomStrength = 0.18;\n c.bloomRadius = 0.55;\n c.bloomThreshold = 0.72;\n // Golden dust shed off the ring's own edge into the void (biased to one flank). Per-wave: it rides\n // this wave's deform and drifts outward from it. edgeBias 1 = spawn on the rim (the shed look).\n w.particles = {\n count: 20000,\n size: 2.9,\n color: \"#ffdca8\",\n seed: 7,\n twinkle: 0.8,\n edgeBias: 1,\n drift: 490,\n bias: -0.6,\n };\n return c;\n },\n \"Particle Zoo\": () => {\n // One scene demoing every particle STYLE — each wave sheds a different kind of dust off its own\n // surface (embers / snow / sparks / fireflies / bubbles). Every wave is a DIFFERENT shape family\n // too (dome / flat sheet / radial fan / helix coil / twist curl), spread apart so none overlap and\n // the frame stays full. The showcase for the per-wave particle system; styles come from\n // PARTICLE_PRESETS (the studio \"preset style\" picker).\n //\n // Each specimen also answers the cursor a DIFFERENT way — agitate / push / click-ripple /\n // param-binding / drag-wake — so the scene doubles as the interactivity showcase, and each one\n // shows its dust reacting alongside its ribbon.\n const c = createDefaultConfig();\n c.background = \"#05070d\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n c.grain = 0.26;\n c.blur = 0;\n c.bloomStrength = 0.3;\n c.bloomThreshold = 0.66; // high, so the central sparks fan glows without blowing the middle out\n c.cameraPosition = { x: 100, y: 0, z: 5000 };\n c.cameraTarget = { x: 0, y: 0, z: 0 };\n c.cameraZoom = 0.6;\n // Shared cursor. A TIGHT radius so hovering one specimen doesn't stir its neighbours: the waves\n // sit ~700-1400 world units apart in a 1250-tall frame, and radius is a fraction of viewport\n // height doubled into NDC-y, so 0.22 → 0.44 NDC-y ≈ one specimen. Touch is opt-in and this is a\n // showcase, so turn it on (listeners are passive — it does not block page scrolling).\n c.interaction = { enabled: true, radius: 0.22, ribbonFlow: 0.5, touch: true };\n // Five well-separated waves, each a DIFFERENT shape family so the silhouettes read as distinct,\n // not five copies of one ribbon. Frame at cameraZoom 0.6 spans ~2222×1250 world units centred on\n // the origin (x ∈ [-1111, 1111], y ∈ [-625, 625], orthographic → screen-xy = world-xy), so the\n // positions/scales below keep the meshes from overlapping. `shape` carries the per-family knobs\n // (displace / twist / helix / radial); `rot` is in degrees. style, palette, opacity as before.\n const zoo = [\n {\n // Embers rising off a broad DOME — low-frequency, high-amplitude displacement mound.\n style: \"Embers\",\n // STIR THE FIRE — cursor agitation. The churn octave stirs the mound and the embers riding\n // it scatter; shove high so the ones already airborne get swept along too.\n interact: interaction({ hover: { agitate: 34, lighten: 0.25 } }),\n shove: 2.5,\n palette: [\"#ff7a1e\", \"#ffd27a\", \"#c23a12\"],\n pos: [-720, 285],\n scale: [1.0, 1.0, 1.0],\n rot: [-20, 0, -14],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.0024, y: 0.0065 },\n displaceAmount: 118,\n twistPower: { x: 2.4, y: 2.4, z: 3.2 },\n },\n },\n {\n // Snow settling on a near-flat drifting SHEET — tilted toward the camera, barely rippled.\n style: \"Snow\",\n // PRESS INTO THE DRIFT — a NEGATIVE push, so the cursor dents the sheet away from you like a\n // palm in deep snow, and the settled flakes sink into the hollow with it. Slow smoothing so\n // the hollow follows heavily; low shove because falling snow shouldn't whip around.\n interact: interaction({ hover: { push: -22, smoothing: 0.45 } }),\n shove: 0.6,\n palette: [\"#8fb8e8\", \"#e8f1fb\", \"#aeb9d6\"],\n pos: [700, 320],\n scale: [1.1, 1.0, 0.9],\n rot: [-62, 0, 12],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.006, y: 0.013 },\n displaceAmount: 22,\n twistPower: { x: 0.8, y: 0.8, z: 1.0 },\n },\n },\n {\n // Sparks bursting off a RADIAL FAN — the one spiky, fanned silhouette; dimmed so it glows\n // without blowing the centre out.\n style: \"Sparks\",\n // STRIKE IT — click/tap only, no hover field. A ring radiates from the hit and, at full\n // shove, visibly blows OUT through the spark burst instead of stopping at the ribbon: the\n // clearest demo of dust reacting to the pointer.\n interact: interaction({ press: { ripple: 46 } }),\n shove: 4,\n palette: [\"#ffd27a\", \"#fff4d0\", \"#c8853a\"],\n pos: [0, -30],\n scale: [0.85, 0.85, 0.85],\n rot: [0, 0, 0],\n opacity: 0.32,\n shape: {\n radialAmount: 0.82,\n radialArc: 118,\n radialCenter: 90, // fan points up\n radialRadius: 40,\n radialSpread: 1.0,\n displaceAmount: 30,\n },\n },\n {\n // Fireflies wandering around a HELIX coil — a screw-thread column of light.\n style: \"Fireflies\",\n // WIND THE COIL — a BINDING, not a pointer field. Hovering turns the helix a full rotation\n // and the fireflies ride round with it, because each field mirrors its wave's live shape\n // uniforms every frame. Deliberately the one specimen with no `hover` block: its particle\n // program never compiles the pointer path, yet its dust still follows the wave.\n interact: interaction({\n bindings: [{ source: \"hover\", target: \"helixPhase\", to: 360, smoothing: 0.5 }],\n }),\n palette: [\"#3d5a24\", \"#7bd23a\", \"#40521f\"],\n pos: [-560, -300],\n scale: [1.25, 1.25, 1.25],\n rot: [0, 0, 8],\n opacity: 0.72,\n shape: {\n helixTurns: 3,\n helixRadius: 44,\n helixRoll: 1,\n displaceAmount: 8,\n twistPower: { x: 1.0, y: 1.0, z: 1.2 },\n },\n },\n {\n // Bubbles rising off a strongly TWISTED curl — an S-ribbon coiling on its length.\n style: \"Bubbles\",\n // DRAG THROUGH THE WATER — the drag-wake. Sweep the cursor and the ribbon behind it is\n // pulled into a trailing trough that heals when you stop, with the bubbles streaming into\n // the wake; `thin` adds a watery translucency right under the cursor.\n interact: interaction({ hover: { wake: 30, thin: 0.45 } }),\n shove: 1.6,\n palette: [\"#2e8fb0\", \"#7fd4e8\", \"#1a5a6e\"],\n pos: [660, -270],\n scale: [1.0, 1.15, 1.0],\n rot: [-16, 0, -20],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.005, y: 0.011 },\n displaceAmount: 46,\n twistFrequency: { x: -0.06, y: 0.09, z: -0.5 },\n twistPower: { x: 4.0, y: 6.0, z: 9.0 },\n },\n },\n ];\n const base = c.waves[0];\n c.waves = zoo.map((z, i) => {\n const w = structuredClone(base);\n w.usePaletteTexture = false;\n w.gradientType = \"linear\";\n w.gradientAngle = 0;\n w.palette = makeStops(z.palette);\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1;\n w.colorSaturation = 1.1;\n w.opacity = z.opacity;\n w.scale = { x: z.scale[0], y: z.scale[1], z: z.scale[2] };\n w.rotation = { x: rad(z.rot[0]), y: rad(z.rot[1]), z: rad(z.rot[2]) };\n w.position = { x: z.pos[0], y: z.pos[1], z: 0 };\n w.seed = i * 7;\n w.speed = 0.05;\n Object.assign(w, z.shape); // the wave's distinct shape family (displace / twist / helix / radial)\n // Each PARTICLE_PRESET is tuned as a standalone hero cloud whose dust flings far; here every\n // specimen should sit ON the wave that emits it, reading as dust clinging to the surface rather\n // than a cloud floating nearby. Pull the motion terms right down (a small fraction of the hero\n // reach + tight clamps) so the dust overlaps its own wave; the styles still read apart by sprite\n // shape / colour / wave shape.\n const p = structuredClone(PARTICLE_PRESETS[z.style]);\n const PULL = 0.13; // fraction of each preset's hero-scale reach kept here\n if (p.drift != null) p.drift = capMag(p.drift * PULL, 24);\n if (p.rise != null) p.rise = capMag(p.rise * PULL, 28);\n if (p.wander != null) p.wander = capMag(p.wander * PULL, 15);\n if (p.swirl != null) p.swirl = capMag(p.swirl * PULL, 0.05);\n if (z.shove != null) p.pointerShove = z.shove;\n w.particles = p;\n // One distinct interaction per specimen (agitate / push / ripple / binding / wake) — see each\n // entry above. Absent would mean inert, so only assign what the entry authored.\n if (z.interact) w.interaction = structuredClone(z.interact);\n return w;\n });\n c.waveCount = c.waves.length;\n return c;\n },\n Holographic: () => {\n // Conic gradient: an iridescent oil-slick sweep. The palette wraps (first ≈ last stop) so\n // the conic seam is invisible.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"conic\";\n w.gradientShift = 0.08;\n w.palette = [\n { color: \"#8ef6e4\", pos: 0 }, // mint (seam)\n { color: \"#6ec3ff\", pos: 0.18 }, // sky\n { color: \"#9b8cff\", pos: 0.36 }, // periwinkle\n { color: \"#ff8ad8\", pos: 0.54 }, // pink\n { color: \"#ffd98e\", pos: 0.72 }, // peach\n { color: \"#a0f0c8\", pos: 0.88 }, // seafoam\n { color: \"#8ef6e4\", pos: 1 }, // mint again (seamless wrap)\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.04;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.12;\n c.grain = 0.28;\n c.blur = 0.01;\n // Subtle deep teal → violet wash behind the iridescence.\n c.background = \"#05060c\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"linear\";\n c.backgroundGradientAngle = 135;\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#04121a\", \"#0a0518\"]);\n c.transparentBackground = false;\n return c;\n },\n Aurora: () => {\n // Mesh gradient: a moody aurora — teals/greens drifting into violet over a night-sky base\n // (distinct from the brighter iOS-style \"Mesh Gradient\").\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a1f3c\", x: 0.08, y: 0.12, influence: 0.62 },\n { color: \"#1fddb0\", x: 0.3, y: 0.7, influence: 0.78 },\n { color: \"#57f5a3\", x: 0.58, y: 0.86, influence: 0.7 },\n { color: \"#3a86ff\", x: 0.82, y: 0.55, influence: 0.62 },\n { color: \"#a15cff\", x: 0.5, y: 0.32, influence: 0.7 },\n { color: \"#071433\", x: 0.92, y: 0.08, influence: 0.6 },\n ];\n w.meshGradientSoftness = 0.72;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.008;\n // Dark night-sky MESH backdrop (also shows off the mesh background type).\n c.background = \"#03060f\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"mesh\";\n c.backgroundMeshPoints = [\n { color: \"#02040c\", x: 0.15, y: 0.85, influence: 0.7 },\n { color: \"#08243a\", x: 0.5, y: 0.5, influence: 0.75 },\n { color: \"#0a0f2e\", x: 0.85, y: 0.7, influence: 0.7 },\n { color: \"#04121a\", x: 0.7, y: 0.2, influence: 0.6 },\n { color: \"#000208\", x: 0.12, y: 0.12, influence: 0.6 },\n ];\n c.backgroundMeshSoftness = 0.75;\n c.transparentBackground = false;\n return c;\n },\n Palestine: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"palestine\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1;\n w.colorSaturation = 1;\n c.grain = 0.35;\n c.background = \"#f2efe8\";\n c.transparentBackground = true;\n return c;\n },\n Spain: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"spain\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.18;\n w.colorSaturation = 1.25;\n w.creaseLight = 1.6; // moderate crease-light: rich crimson without washing to salmon (Hero's is 1.98)\n c.grain = 0.3;\n c.background = \"#1a0608\"; // deep oxblood stage\n c.backgroundMode = \"color\";\n c.transparentBackground = false; // opaque, so the dark stage makes the flag pop\n return c;\n },\n \"Vaporwave Sunset\": () => {\n // The Hero wave re-posed/re-framed, plus the vaporwave palette.\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.position.x = 525;\n w.rotation.x = -0.64 * RAD;\n w.rotation.z = 1.68 * RAD;\n w.paletteSource = \"vaporwave\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.08;\n w.colorSaturation = 1.15;\n w.creaseLight = 1.25;\n c.cameraZoom = 1.1;\n c.cameraTarget = { x: 150, y: 360, z: 0 };\n c.background = \"#09051f\";\n c.transparentBackground = false;\n return c;\n },\n Corkscrew: () => {\n // The helix mode, shown off on its own: `helixRoll` at 1 rolls the ribbon's cross-section in\n // step with the sweep, so the flat strip becomes an auger blade winding around its own length\n // axis, and `helixRadius` lifts that blade off the axis so the turns read as a screw thread\n // rather than a flat twist. No twist at all — this shape is unreachable with twistFrequency,\n // whose expStep angle is monotone and can only ramp once (see the helix docs in config/model).\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.helixTurns = 5;\n w.helixRadius = 45;\n w.helixRoll = 1;\n w.helixPhase = 0;\n w.twistFrequency = { x: 0, y: 0, z: 0 };\n w.twistPower = { x: 4, y: 4, z: 2 };\n // A slow swell along the blade so it breathes; the corkscrew itself is static geometry.\n w.displaceAmount = 16;\n w.displaceFrequency = { x: 0.006, y: 0.0008 };\n w.speed = 0.1;\n w.position = { x: 0, y: 0, z: 0 };\n w.rotation = { x: 0, y: 0, z: 12 }; // tilt so it climbs across the frame\n w.scale = { x: 3, y: 3, z: 1.5 };\n // Mesh gradient: the colour field runs along the blade, so each turn picks up a different part\n // of the spectrum instead of the one hue a linear stop ramp would give.\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n // Framed down the axis rather than side-on: the coil reads as a screw receding into the frame,\n // and each turn shows its blade face instead of an edge. cameraDistance is the orbit radius\n // (= |position − target|); the camera is orthographic, so it's the rig's dolly, not the scale —\n // cameraZoom sets that.\n c.cameraPosition = { x: -526.009, y: -285.284, z: -425.489 };\n c.cameraTarget = { x: -95.046, y: -17.053, z: -105.608 };\n c.cameraDistance = 600;\n c.cameraZoom = 1.176;\n c.grain = 0.3;\n c.blur = 0.008;\n c.bloomStrength = 0.35;\n c.bloomRadius = 0.6;\n c.bloomThreshold = 0.55;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n /**\n * DISINTEGRATION — the \"snap\". A combed ribbon looping over itself around an open eye, crumbling\n * into blocky debris across a straight edge down the canvas.\n *\n * The form is `wrapAmount` and `helixRoll` TOGETHER, which is the combination worth knowing: wrap\n * bends the ribbon's length into a loop, roll turns its cross-section as it travels, and a wide\n * sheet doing both at once folds into broad lobes that cross in front of each other around a hole.\n * Neither alone gets there — wrap alone is a clean band (a napkin ring), roll alone is a coil that\n * stays on its axis.\n *\n * Two material choices carry the rest, and both are the opposite of what looks obvious:\n *\n * - `lineDerivativePower: 0`. The wireframe normally scales strand thickness by the screen-space\n * uv derivative, which is right for a ribbon seen whole. Framed CLOSE it inverts: the bigger\n * the sheet is on screen the smaller its derivative, so the strands thin to pale grey exactly\n * where they should carry ink. At 0 the duty cycle is `lineThickness` alone, crisp at any zoom.\n * - `lineGapOpacity: 1` — OPAQUE gaps, the theme's default. Clear gaps let the folds layer\n * through each other, which sounds richer and reads as a transparent weave; opaque ones let\n * the near lobe HIDE the far one, which is what makes this read as a solid object. The black\n * masses then come for free: where the surface turns away the strands compress until the page\n * between them disappears.\n *\n * The second wave is the same ribbon at a third of the wrap and 5x the scale — the broad sweep the\n * loop sits in. Both share one screen-space dissolve front with `dust: 1` pinning the debris to it,\n * and scroll runs it to 0.95.\n */\n Disintegration: () => {\n const c = PRESETS[\"Hero\"]();\n const base = c.waves[0];\n base.theme = \"wireframe\";\n // INK across the width with violet only on the narrow rims: a near-black striped surface that\n // lights to violet at its near edges, not a violet sheet with dark edges.\n base.usePaletteTexture = false;\n base.palette = [\n { color: \"#cbb8f5\", pos: 0 },\n { color: \"#7c55d8\", pos: 0.03 },\n { color: \"#2a1750\", pos: 0.075 },\n { color: \"#07050c\", pos: 0.15 },\n { color: \"#07050c\", pos: 0.85 },\n { color: \"#2a1750\", pos: 0.925 },\n { color: \"#7c55d8\", pos: 0.97 },\n { color: \"#cbb8f5\", pos: 1 },\n ];\n base.gradientType = \"linear\";\n base.gradientAngle = 90; // across uv.x, the folded width — so the ramp runs across the strands\n base.gradientShift = 0;\n base.hueShift = 0;\n base.colorContrast = 1;\n base.colorSaturation = 1;\n base.fiberStrength = 0; // the strands ARE the texture here; streaks would only muddy them\n base.lineAmount = 700;\n base.lineThickness = 1; // with the derivative term off, this IS the duty cycle\n base.lineDerivativePower = 0; // see above — the knob that decides whether there is ink at all\n base.lineSharpness = 0.96;\n base.lineDepthFade = 0; // full contrast at every depth; the default is tuned for a single ribbon\n // The between-strand gaps are the sheet's BODY, not the page. Left at the default they take the\n // page background, and warm paper showing between the strands keeps the whole thing reading\n // pale however black the ink is — an explicit near-black gap is what makes it deep. Still\n // opaque, so the near lobe hides the far one and a stack of folds reads as one solid object\n // rather than a transparent weave.\n base.lineGapColor = \"#0a0714\";\n // Lit, round strands. A stripe is otherwise a MASK — no cross-section, so no highlight can run\n // along one and the bundle reads as hatching however dense it gets. These two give each strand a\n // crest and flanks and shade it as the surface turns, which is the difference between a drawing\n // of a combed surface and a combed surface.\n base.lineLight = 0.8;\n base.rungAmount = 0;\n base.edgeFeather = 0.1;\n base.blendMode = \"normal\";\n base.opacity = 1;\n base.radialAmount = 0;\n // A broad swell so the lobes undulate instead of reading as machined tube.\n base.displaceAmount = 60;\n base.displaceFrequency = { x: 0.003, y: 0.008 };\n base.twistFrequency = { x: 0, y: 0, z: 0 };\n base.twistPower = { x: 4, y: 4, z: 4 };\n base.helixRadius = 0;\n base.helixRoll = 1; // roll without radius: the sheet turns through itself rather than coiling away\n base.helixPhase = 0;\n base.speed = 0.08;\n base.position = { x: 0, y: 0, z: 0 };\n base.dissolve = {\n // HALF gone in the still frame: the preset is named for the front, so it should arrive with\n // the sheet already half debris rather than barely nicked.\n amount: 0.5,\n axis: \"screenX\", // on the CANVAS, so the front is an edge of the frame however the sheet is posed\n reverse: true, // eat in from the RIGHT, leaving the standing fold on the left\n band: 0.22,\n scale: 38,\n blocky: 0.8,\n dust: 1, // each mote is the chunk of surface that just left\n };\n // Scroll takes the front the rest of the way. `from` is omitted, so at scroll 0 the sheet sits at\n // its authored 0.26 and the still frame is what a reader sees before they move.\n base.interaction = {\n bindings: [{ source: \"scroll\", target: \"dissolveAmount\", to: 0.95, smoothing: 0.2 }],\n };\n\n // ONE sheet: a closed circular PATH with one and a half turns of roll, so it folds into lobes\n // that cross in front of each other around an open eye. The path is what makes the ribbon come\n // back on itself at all — and every point of it is draggable in the studio.\n const loop = structuredClone(base);\n loop.seed = 0;\n loop.path = arcPath(1);\n loop.helixTurns = 1.5;\n loop.scale = { x: 3.4, y: 3.4, z: 3.4 };\n loop.rotation = { x: 45, y: 25, z: 15 };\n loop.particles = snapDust(0); // the INK debris: dark motes, not the violet ones\n // Half a sheet's worth of surface has to go somewhere, and this is now the only emitter in the\n // preset — the second wave that carried its own field is gone.\n loop.particles.count = 9000;\n loop.particles.size = 18;\n\n c.waves = [loop];\n c.waveCount = 1;\n\n c.background = \"#f7f6f1\"; // warm paper\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n c.grain = 0;\n c.blur = 0;\n // One key light, up and to the right of the camera: the strand shading needs somewhere for the\n // glint to come from, and a single source keeps it readable as one direction of light.\n c.ambient = 0.25;\n c.lights = [{ position: { x: 600, y: 900, z: 800 }, color: \"#ffffff\", intensity: 2 }];\n c.cameraDistance = 5001;\n c.cameraPosition = { x: 0, y: 0, z: 5000 };\n // Centred on what is LEFT of the fold plus the debris it sheds. The old -170 balanced a second,\n // much wider sheet that sat behind this one; with that gone the same target pushed the\n // composition off to the right, and y rises to sit the standing half against the frame.\n c.cameraTarget = { x: -40, y: 120, z: 0 };\n c.cameraZoom = 0.75;\n // The front is a SCREEN edge, so a narrow phone cropping to a quarter of the authored width\n // would put it somewhere else entirely on the composition. Holding 70% of that width on screen\n // keeps the fold and its debris in the same relationship.\n c.cameraMinVisibleWidth = 0.7;\n return c;\n },\n /**\n * ONE sheet of glass, lit entirely by what is behind it.\n *\n * Glass has no colour of its own here — tint is almost off. The colour is a dark field with a few\n * saturated lamps in it, and the sheet bends and films that: the same trick materials3d's rod\n * preset uses, where a warm-to-magenta plate behind the glass is what you are actually looking at.\n * A mesh gradient is that plate, so the pools stay separated instead of averaging to one wash.\n *\n * Thin-film iridescence is pushed hard, because it tints only what BOUNCES — the reflection, the\n * rim and the specular — so the sheet shifts hue along its edges while what you see THROUGH it\n * stays the colour of the lamp behind. That split is what reads as oil-on-glass rather than as a\n * coloured object.\n */\n \"Liquid Glass\": () => {\n const c = PRESETS[\"Hero\"]();\n const glass = c.waves[0];\n\n glass.theme = \"glass\";\n glass.glassRipple = 0.9;\n glass.glassRippleScale = 0.02;\n glass.glassFlow = 0.7;\n glass.glassStrength = 120; // well past the default: there is a lot behind it worth displacing\n glass.glassChroma = 1.1;\n glass.glassTint = 0.08; // almost none — the colour belongs to the backdrop, not the sheet\n glass.glassIrid = 0.85;\n glass.glassFilmNm = 420; // the soap-bubble band\n glass.glassSpec = 1.5;\n glass.edgeFeather = 0.16;\n glass.scale = { x: 8, y: 7, z: 4 };\n glass.position = { x: 430, y: -300, z: 60 };\n\n c.waves = [glass];\n c.waveCount = 1;\n\n // The lamps. Dark everywhere else on purpose: pools of colour in a dark field read as GLOW,\n // where the same colours spread evenly just look like a bright poster and the glass stops\n // standing out from them at all.\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"mesh\";\n c.backgroundGradientSource = \"stops\";\n c.transparentBackground = false;\n c.background = \"#04060f\";\n c.backgroundMeshSoftness = 0.7;\n c.backgroundMeshPoints = [\n { color: \"#ff2d95\", x: 0.74, y: 0.2, influence: 0.45 },\n { color: \"#7b5bff\", x: 0.4, y: 0.46, influence: 0.45 },\n { color: \"#22d3ee\", x: 0.62, y: 0.8, influence: 0.4 },\n { color: \"#ff8a3d\", x: 0.92, y: 0.58, influence: 0.32 },\n { color: \"#04060f\", x: 0.06, y: 0.12, influence: 1 },\n { color: \"#04060f\", x: 0.1, y: 0.92, influence: 1 },\n { color: \"#04060f\", x: 0.95, y: 0.95, influence: 0.9 },\n ];\n c.grain = 0;\n // No bloom. At 0.25 it was invisible next to the same frame without it and it more than doubled\n // the cross-backend difference on its own, because the two bloom passes already disagree.\n c.bloomStrength = 0;\n c.cameraZoom = 0.45;\n c.cameraTarget = { x: -40, y: -220, z: 0 };\n return c;\n },\n Kaleidoscope: () => {\n const c = PRESETS[\"Wave 3\"]();\n const w = c.waves[0];\n w.paletteSource = \"kaleidoscope\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.12;\n c.grain = 0.5;\n return c;\n },\n};\n"],"mappings":"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"}
|
|
@@ -94,10 +94,31 @@ var WaveGeometry = class {
|
|
|
94
94
|
}
|
|
95
95
|
plane.setIndex(merged);
|
|
96
96
|
plane.computeVertexNormals();
|
|
97
|
+
const nU = new Float32Array(pos.count * 4);
|
|
98
|
+
const nV = new Float32Array(pos.count * 4);
|
|
99
|
+
for (let iy = 0; iy <= subY; iy++) for (let ix = 0; ix <= subX; ix++) {
|
|
100
|
+
const i = iy * cols + ix;
|
|
101
|
+
const iU = iy * cols + (ix < subX ? ix + 1 : ix - 1);
|
|
102
|
+
const iV = (iy < subY ? iy + 1 : iy - 1) * cols + ix;
|
|
103
|
+
nU.set([
|
|
104
|
+
pos.getX(iU),
|
|
105
|
+
pos.getY(iU),
|
|
106
|
+
pos.getZ(iU),
|
|
107
|
+
uv.getX(iU) - uv.getX(i)
|
|
108
|
+
], i * 4);
|
|
109
|
+
nV.set([
|
|
110
|
+
pos.getX(iV),
|
|
111
|
+
pos.getY(iV),
|
|
112
|
+
pos.getZ(iV),
|
|
113
|
+
uv.getY(iV) - uv.getY(i)
|
|
114
|
+
], i * 4);
|
|
115
|
+
}
|
|
97
116
|
this.geometry.setIndex(plane.getIndex());
|
|
98
117
|
this.geometry.setAttribute("position", plane.getAttribute("position"));
|
|
99
118
|
this.geometry.setAttribute("uv", plane.getAttribute("uv"));
|
|
100
119
|
this.geometry.setAttribute("normal", plane.getAttribute("normal"));
|
|
120
|
+
this.geometry.setAttribute("positionU", new THREE.BufferAttribute(nU, 4));
|
|
121
|
+
this.geometry.setAttribute("positionV", new THREE.BufferAttribute(nV, 4));
|
|
101
122
|
this.geometry.computeBoundingSphere();
|
|
102
123
|
plane.dispose();
|
|
103
124
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WaveGeometry.js","names":[],"sources":["../../src/renderer/WaveGeometry.ts"],"sourcesContent":["import * as THREE from \"three\";\n\n/** Native plane size for folded() — keep this exact (400) so the vertex\n * shader's displace/twist frequencies (calibrated to this scale) stay faithful. */\nconst NATIVE = 400;\nconst FOLD_X = 16; // |x| < 16 is the semicircular hinge; outside it the two flat arms\nconst SHIFT = NATIVE / 4; // recentre the folded cross-section along x\n\nconst X_AXIS = new THREE.Vector3(1, 0, 0);\nconst Y_AXIS = new THREE.Vector3(0, 1, 0);\n\n/**\n * Local-Z centre of the folded ribbon's width. The fold collapses x ∈ [-NATIVE/2, NATIVE/2] onto a\n * single arm and SHIFT recentres it, which lands the width at [-100, 84] rather than symmetric\n * about 0 — so a rotation about local X through the ORIGIN would swing the ribbon's two long edges\n * to radii 100 and 84 (a visibly lopsided helix). The vertex shader's helix roll rotates about this\n * line instead, so both edges come out at equal radius.\n */\nexport const RIBBON_Z_CENTER = (SHIFT - NATIVE / 2 + (SHIFT - FOLD_X)) / 2;\n\n/**\n * Base wave geometry — `folded()`: a flat PlaneGeometry folded into a hairpin\n * (sideways-U) cross-section, then stood up so the fold runs along the wave's length.\n *\n * - Each vertex gets a half-thickness `r` (per-vertex math below): tight at the middle\n * of the wave's length, flaring toward both ends.\n * - The strip |x| < FOLD_X becomes a semicircular hinge; the plane's two halves bend\n * around it into parallel arms offset to +r and -r.\n * - Two −90° rotations (about X then Y) orient the U upright and down its length.\n *\n * folded() leaves the U open along one side and hollow at both ends, so at oblique\n * camera angles you could see straight through it. We weld the open side and cap both\n * ends with extra triangles so the mesh is a watertight solid — welding/capping adds\n * faces only, no vertex positions move.\n *\n * All further deformation (displacement, twist, transform) happens in the vertex shader\n * on top of this base.\n *\n * UV AXES — the canonical statement, because this is easy to get backwards and the rest of\n * the codebase reasons in uv. The plane is folded along its local x, which is the COLUMN\n * direction (uv.x), and the two −90° rotations land world = (plane.y, plane.z, plane.x):\n *\n * uv.y → the ribbon's 400-unit LENGTH (world X — the axis `displaceFrequency.x` drives)\n * uv.x → the folded ~188-unit WIDTH, wrapping the hairpin cross-section (world Z)\n *\n * So u runs ACROSS the fold and v runs ALONG it. The welding below corroborates it twice:\n * the end-caps fan across columns at rows v=0 / v=subX, and the seam joins col 0 to col\n * subX down every row — a join that necessarily runs the full length. Measured on a built\n * mesh, the correlation of uv.y with world X is exactly 1.0, and of uv.x with world X, 0.\n * (uv.x vs world Z also reads 0 — the fold's own signature: a monotone mapping would give\n * ±1, but the hairpin runs out along one arm and back along the other.)\n *\n * Consequences that read backwards if you assume otherwise: a `NoiseBand`'s startX/endX\n * are the SHORT axis; `edgeFeather` softens the two ends, not the long edges; the palette\n * texture's \"edge tint\" lands on the ends; `parabolaPower` bunches streaks toward the long\n * edges; and the twist X/Z falloffs run lengthwise while Y runs across the width.\n */\nexport class WaveGeometry {\n readonly geometry: THREE.BufferGeometry;\n private segments = -1;\n\n constructor(segments: number) {\n this.geometry = new THREE.BufferGeometry();\n this.resize(segments);\n }\n\n resize(segments: number): void {\n if (segments === this.segments) return;\n this.segments = segments;\n\n // subX across the fold (the cross-section), subY along the length (twice as dense).\n const subX = THREE.MathUtils.clamp(Math.round(segments), 48, 200);\n const subY = subX * 2;\n\n const plane = new THREE.PlaneGeometry(NATIVE, NATIVE, subX, subY);\n const pos = plane.attributes.position as THREE.BufferAttribute;\n const uv = plane.attributes.uv as THREE.BufferAttribute;\n const v = new THREE.Vector3();\n\n for (let i = 0; i < pos.count; i++) {\n v.fromBufferAttribute(pos, i);\n const uy = uv.getY(i);\n // r: cross-section half-thickness — tight (2) at the middle of the length, flaring (4)\n // toward both ends. The pow() term is a sharp parabolic bump peaking at uv.y = 0.5.\n const r = 4 - 2 * Math.pow(4 * uy * (1 - uy), 9.5);\n\n if (v.x < -FOLD_X) {\n v.z += r; // long arm, at +r\n } else if (v.x < FOLD_X) {\n // semicircular hinge: z sweeps +r → -r, x collapses to the bend\n v.z = Math.cos(THREE.MathUtils.mapLinear(v.x, -FOLD_X, FOLD_X, 0, Math.PI)) * r;\n v.x =\n Math.cos(THREE.MathUtils.mapLinear(v.x, -FOLD_X, FOLD_X, -Math.PI / 2, Math.PI / 2)) * r -\n FOLD_X;\n } else {\n v.z -= r; // folded-over arm, mirrored back at -r\n v.x = -v.x;\n }\n\n v.x += SHIFT;\n v.applyAxisAngle(X_AXIS, -Math.PI / 2);\n v.applyAxisAngle(Y_AXIS, -Math.PI / 2);\n pos.setXYZ(i, v.x, v.y, v.z);\n }\n pos.needsUpdate = true;\n\n // Seal the hairpin's OPEN side. folded() leaves the two arm tips unconnected — the\n // plane's u=0 and u=subX edges, which fold to adjacent tips at +r and -r — so at oblique\n // camera angles you can see through the U to the background. Weld those two edges with a\n // strip of triangles, closing the tube. No vertex positions move; this only adds faces\n // over the previously-open seam.\n const cols = subX + 1;\n const srcIdx = plane.getIndex();\n const merged = srcIdx ? Array.from(srcIdx.array as ArrayLike<number>) : [];\n // (a) Weld the U's side opening: the u=0 and u=subX edges fold to adjacent tips at ±r.\n for (let iy = 0; iy < subY; iy++) {\n const a = iy * cols; // (row iy, col 0) — arm-A tip\n const b = (iy + 1) * cols; // (row iy+1, col 0)\n const c = a + subX; // (row iy, col subX) — arm-B tip\n const d = b + subX; // (row iy+1, col subX)\n merged.push(a, c, b, b, c, d);\n }\n // (b) Cap the two length-ends (v=0 and v=subX rows): the folded sheet is a hollow channel\n // open at both ends, so an edge-on camera sees straight through it. Fan-triangulate each\n // end's U cross-section (apex = the col-0 tip) to close it — making the wave a closed solid.\n for (const row of [0, subY]) {\n const apex = row * cols;\n for (let ix = 1; ix < subX; ix++) merged.push(apex, row * cols + ix, row * cols + ix + 1);\n }\n plane.setIndex(merged);\n\n plane.computeVertexNormals();\n\n // Move the baked attributes onto our reusable geometry, then drop the temp.\n this.geometry.setIndex(plane.getIndex());\n this.geometry.setAttribute(\"position\", plane.getAttribute(\"position\"));\n this.geometry.setAttribute(\"uv\", plane.getAttribute(\"uv\"));\n this.geometry.setAttribute(\"normal\", plane.getAttribute(\"normal\"));\n this.geometry.computeBoundingSphere();\n plane.dispose();\n }\n\n dispose(): void {\n this.geometry.dispose();\n }\n}\n"],"mappings":";;;;AAIA,MAAM,SAAS;AACf,MAAM,SAAS;AACf,MAAM,QAAQ,SAAS;AAEvB,MAAM,SAAS,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;AACxC,MAAM,SAAS,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDxC,IAAa,eAAb,MAA0B;CACxB;CACA,WAAmB;CAEnB,YAAY,UAAkB;EAC5B,KAAK,WAAW,IAAI,MAAM,eAAe;EACzC,KAAK,OAAO,QAAQ;CACtB;CAEA,OAAO,UAAwB;EAC7B,IAAI,aAAa,KAAK,UAAU;EAChC,KAAK,WAAW;EAGhB,MAAM,OAAO,MAAM,UAAU,MAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,GAAG;EAChE,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQ,IAAI,MAAM,cAAc,QAAQ,QAAQ,MAAM,IAAI;EAChE,MAAM,MAAM,MAAM,WAAW;EAC7B,MAAM,KAAK,MAAM,WAAW;EAC5B,MAAM,IAAI,IAAI,MAAM,QAAQ;EAE5B,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,OAAO,KAAK;GAClC,EAAE,oBAAoB,KAAK,CAAC;GAC5B,MAAM,KAAK,GAAG,KAAK,CAAC;GAGpB,MAAM,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,GAAG;GAEjD,IAAI,EAAE,IAAI,KACR,EAAE,KAAK;QACF,IAAI,EAAE,IAAI,QAAQ;IAEvB,EAAE,IAAI,KAAK,IAAI,MAAM,UAAU,UAAU,EAAE,GAAG,KAAS,QAAQ,GAAG,KAAK,EAAE,CAAC,IAAI;IAC9E,EAAE,IACA,KAAK,IAAI,MAAM,UAAU,UAAU,EAAE,GAAG,KAAS,QAAQ,CAAC,KAAK,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,IAAI,IACvF;GACJ,OAAO;IACL,EAAE,KAAK;IACP,EAAE,IAAI,CAAC,EAAE;GACX;GAEA,EAAE,KAAK;GACP,EAAE,eAAe,QAAQ,CAAC,KAAK,KAAK,CAAC;GACrC,EAAE,eAAe,QAAQ,CAAC,KAAK,KAAK,CAAC;GACrC,IAAI,OAAO,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;EAC7B;EACA,IAAI,cAAc;EAOlB,MAAM,OAAO,OAAO;EACpB,MAAM,SAAS,MAAM,SAAS;EAC9B,MAAM,SAAS,SAAS,MAAM,KAAK,OAAO,KAA0B,IAAI,CAAC;EAEzE,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,MAAM;GAChC,MAAM,IAAI,KAAK;GACf,MAAM,KAAK,KAAK,KAAK;GACrB,MAAM,IAAI,IAAI;GACd,MAAM,IAAI,IAAI;GACd,OAAO,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;EAC9B;EAIA,KAAK,MAAM,OAAO,CAAC,GAAG,IAAI,GAAG;GAC3B,MAAM,OAAO,MAAM;GACnB,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,MAAM,OAAO,KAAK,MAAM,MAAM,OAAO,IAAI,MAAM,OAAO,KAAK,CAAC;EAC1F;EACA,MAAM,SAAS,MAAM;EAErB,MAAM,qBAAqB;EAG3B,KAAK,SAAS,SAAS,MAAM,SAAS,CAAC;EACvC,KAAK,SAAS,aAAa,YAAY,MAAM,aAAa,UAAU,CAAC;EACrE,KAAK,SAAS,aAAa,MAAM,MAAM,aAAa,IAAI,CAAC;EACzD,KAAK,SAAS,aAAa,UAAU,MAAM,aAAa,QAAQ,CAAC;EACjE,KAAK,SAAS,sBAAsB;EACpC,MAAM,QAAQ;CAChB;CAEA,UAAgB;EACd,KAAK,SAAS,QAAQ;CACxB;AACF"}
|
|
1
|
+
{"version":3,"file":"WaveGeometry.js","names":[],"sources":["../../src/renderer/WaveGeometry.ts"],"sourcesContent":["import * as THREE from \"three\";\n\n/** Native plane size for folded() — keep this exact (400) so the vertex\n * shader's displace/twist frequencies (calibrated to this scale) stay faithful. */\nconst NATIVE = 400;\nconst FOLD_X = 16; // |x| < 16 is the semicircular hinge; outside it the two flat arms\nconst SHIFT = NATIVE / 4; // recentre the folded cross-section along x\n\nconst X_AXIS = new THREE.Vector3(1, 0, 0);\nconst Y_AXIS = new THREE.Vector3(0, 1, 0);\n\n/**\n * Local-Z centre of the folded ribbon's width. The fold collapses x ∈ [-NATIVE/2, NATIVE/2] onto a\n * single arm and SHIFT recentres it, which lands the width at [-100, 84] rather than symmetric\n * about 0 — so a rotation about local X through the ORIGIN would swing the ribbon's two long edges\n * to radii 100 and 84 (a visibly lopsided helix). The vertex shader's helix roll rotates about this\n * line instead, so both edges come out at equal radius.\n */\nexport const RIBBON_Z_CENTER = (SHIFT - NATIVE / 2 + (SHIFT - FOLD_X)) / 2;\n\n/** Half the folded ribbon's width about {@link RIBBON_Z_CENTER} — the [-100, 84] extent above. */\nexport const RIBBON_HALF_WIDTH = (SHIFT - FOLD_X - (SHIFT - NATIVE / 2)) / 2;\n\n/**\n * Base wave geometry — `folded()`: a flat PlaneGeometry folded into a hairpin\n * (sideways-U) cross-section, then stood up so the fold runs along the wave's length.\n *\n * - Each vertex gets a half-thickness `r` (per-vertex math below): tight at the middle\n * of the wave's length, flaring toward both ends.\n * - The strip |x| < FOLD_X becomes a semicircular hinge; the plane's two halves bend\n * around it into parallel arms offset to +r and -r.\n * - Two −90° rotations (about X then Y) orient the U upright and down its length.\n *\n * folded() leaves the U open along one side and hollow at both ends, so at oblique\n * camera angles you could see straight through it. We weld the open side and cap both\n * ends with extra triangles so the mesh is a watertight solid — welding/capping adds\n * faces only, no vertex positions move.\n *\n * All further deformation (displacement, twist, transform) happens in the vertex shader\n * on top of this base.\n *\n * UV AXES — the canonical statement, because this is easy to get backwards and the rest of\n * the codebase reasons in uv. The plane is folded along its local x, which is the COLUMN\n * direction (uv.x), and the two −90° rotations land world = (plane.y, plane.z, plane.x):\n *\n * uv.y → the ribbon's 400-unit LENGTH (world X — the axis `displaceFrequency.x` drives)\n * uv.x → the folded ~188-unit WIDTH, wrapping the hairpin cross-section (world Z)\n *\n * So u runs ACROSS the fold and v runs ALONG it. The welding below corroborates it twice:\n * the end-caps fan across columns at rows v=0 / v=subX, and the seam joins col 0 to col\n * subX down every row — a join that necessarily runs the full length. Measured on a built\n * mesh, the correlation of uv.y with world X is exactly 1.0, and of uv.x with world X, 0.\n * (uv.x vs world Z also reads 0 — the fold's own signature: a monotone mapping would give\n * ±1, but the hairpin runs out along one arm and back along the other.)\n *\n * Consequences that read backwards if you assume otherwise: a `NoiseBand`'s startX/endX\n * are the SHORT axis; `edgeFeather` softens the two ends, not the long edges; the palette\n * texture's \"edge tint\" lands on the ends; `parabolaPower` bunches streaks toward the long\n * edges; and the twist X/Z falloffs run lengthwise while Y runs across the width.\n */\nexport class WaveGeometry {\n readonly geometry: THREE.BufferGeometry;\n private segments = -1;\n\n constructor(segments: number) {\n this.geometry = new THREE.BufferGeometry();\n this.resize(segments);\n }\n\n resize(segments: number): void {\n if (segments === this.segments) return;\n this.segments = segments;\n\n // subX across the fold (the cross-section), subY along the length (twice as dense).\n const subX = THREE.MathUtils.clamp(Math.round(segments), 48, 200);\n const subY = subX * 2;\n\n const plane = new THREE.PlaneGeometry(NATIVE, NATIVE, subX, subY);\n const pos = plane.attributes.position as THREE.BufferAttribute;\n const uv = plane.attributes.uv as THREE.BufferAttribute;\n const v = new THREE.Vector3();\n\n for (let i = 0; i < pos.count; i++) {\n v.fromBufferAttribute(pos, i);\n const uy = uv.getY(i);\n // r: cross-section half-thickness — tight (2) at the middle of the length, flaring (4)\n // toward both ends. The pow() term is a sharp parabolic bump peaking at uv.y = 0.5.\n const r = 4 - 2 * Math.pow(4 * uy * (1 - uy), 9.5);\n\n if (v.x < -FOLD_X) {\n v.z += r; // long arm, at +r\n } else if (v.x < FOLD_X) {\n // semicircular hinge: z sweeps +r → -r, x collapses to the bend\n v.z = Math.cos(THREE.MathUtils.mapLinear(v.x, -FOLD_X, FOLD_X, 0, Math.PI)) * r;\n v.x =\n Math.cos(THREE.MathUtils.mapLinear(v.x, -FOLD_X, FOLD_X, -Math.PI / 2, Math.PI / 2)) * r -\n FOLD_X;\n } else {\n v.z -= r; // folded-over arm, mirrored back at -r\n v.x = -v.x;\n }\n\n v.x += SHIFT;\n v.applyAxisAngle(X_AXIS, -Math.PI / 2);\n v.applyAxisAngle(Y_AXIS, -Math.PI / 2);\n pos.setXYZ(i, v.x, v.y, v.z);\n }\n pos.needsUpdate = true;\n\n // Seal the hairpin's OPEN side. folded() leaves the two arm tips unconnected — the\n // plane's u=0 and u=subX edges, which fold to adjacent tips at +r and -r — so at oblique\n // camera angles you can see through the U to the background. Weld those two edges with a\n // strip of triangles, closing the tube. No vertex positions move; this only adds faces\n // over the previously-open seam.\n const cols = subX + 1;\n const srcIdx = plane.getIndex();\n const merged = srcIdx ? Array.from(srcIdx.array as ArrayLike<number>) : [];\n // (a) Weld the U's side opening: the u=0 and u=subX edges fold to adjacent tips at ±r.\n for (let iy = 0; iy < subY; iy++) {\n const a = iy * cols; // (row iy, col 0) — arm-A tip\n const b = (iy + 1) * cols; // (row iy+1, col 0)\n const c = a + subX; // (row iy, col subX) — arm-B tip\n const d = b + subX; // (row iy+1, col subX)\n merged.push(a, c, b, b, c, d);\n }\n // (b) Cap the two length-ends (v=0 and v=subX rows): the folded sheet is a hollow channel\n // open at both ends, so an edge-on camera sees straight through it. Fan-triangulate each\n // end's U cross-section (apex = the col-0 tip) to close it — making the wave a closed solid.\n for (const row of [0, subY]) {\n const apex = row * cols;\n for (let ix = 1; ix < subX; ix++) merged.push(apex, row * cols + ix, row * cols + ix + 1);\n }\n plane.setIndex(merged);\n\n plane.computeVertexNormals();\n\n // Each vertex's grid neighbours, baked as attributes: the next column (across the width) and\n // the next row (along the length), each as its base position plus the SIGNED uv step to it in\n // `w`. The vertex shader runs the same deformation on both and crosses the two tangents, which\n // gives glass a normal that is exact for whatever the shape does and smooth across the mesh —\n // the fragment's dFdx normal is constant per triangle, and a 120 px refraction turns every\n // triangle edge into a seam. The last column and row step BACKWARD; the sign in `w` lets the\n // shader flip that tangent so the normal keeps its orientation. Read only under VERTEX_NORMAL.\n const nU = new Float32Array(pos.count * 4);\n const nV = new Float32Array(pos.count * 4);\n for (let iy = 0; iy <= subY; iy++) {\n for (let ix = 0; ix <= subX; ix++) {\n const i = iy * cols + ix;\n const iU = iy * cols + (ix < subX ? ix + 1 : ix - 1);\n const iV = (iy < subY ? iy + 1 : iy - 1) * cols + ix;\n nU.set([pos.getX(iU), pos.getY(iU), pos.getZ(iU), uv.getX(iU) - uv.getX(i)], i * 4);\n nV.set([pos.getX(iV), pos.getY(iV), pos.getZ(iV), uv.getY(iV) - uv.getY(i)], i * 4);\n }\n }\n\n // Move the baked attributes onto our reusable geometry, then drop the temp.\n this.geometry.setIndex(plane.getIndex());\n this.geometry.setAttribute(\"position\", plane.getAttribute(\"position\"));\n this.geometry.setAttribute(\"uv\", plane.getAttribute(\"uv\"));\n this.geometry.setAttribute(\"normal\", plane.getAttribute(\"normal\"));\n this.geometry.setAttribute(\"positionU\", new THREE.BufferAttribute(nU, 4));\n this.geometry.setAttribute(\"positionV\", new THREE.BufferAttribute(nV, 4));\n this.geometry.computeBoundingSphere();\n plane.dispose();\n }\n\n dispose(): void {\n this.geometry.dispose();\n }\n}\n"],"mappings":"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"}
|