paperlab 0.0.1 → 0.2.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/index.cjs CHANGED
@@ -17,15 +17,15 @@ var __copyProps = (to, from, except, desc) => {
17
17
  }
18
18
  return to;
19
19
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ var __toESM = (mod2, isNodeMode, target) => (target = mod2 != null ? __create(__getProtoOf(mod2)) : {}, __copyProps(
21
21
  // If the importer is in node compatibility mode or this is not an ESM
22
22
  // file that has been converted to a CommonJS file using a Babel-
23
23
  // compatible transform (i.e. "__esModule" has not been set), then set
24
24
  // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
25
+ isNodeMode || !mod2 || !mod2.__esModule ? __defProp(target, "default", { value: mod2, enumerable: true }) : target,
26
+ mod2
27
27
  ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: true }), mod2);
29
29
 
30
30
  // src/index.ts
31
31
  var index_exports = {};
@@ -44,8 +44,12 @@ __export(index_exports, {
44
44
  PaperMaterial: () => PaperMaterial,
45
45
  PaperMesh: () => PaperMesh,
46
46
  PaperMirror: () => PaperMirror,
47
+ PaperStage: () => PaperStage,
48
+ PaperStageScene: () => PaperStageScene,
47
49
  PaperStateMachine: () => PaperStateMachine,
48
50
  SHEET_LIFT: () => SHEET_LIFT,
51
+ TRANSMISSION_GAIN: () => TRANSMISSION_GAIN,
52
+ accordion: () => accordion,
49
53
  applyDeformerStack: () => applyDeformerStack,
50
54
  atlasGrid: () => atlasGrid,
51
55
  backContentSchema: () => backContentSchema,
@@ -53,6 +57,7 @@ __export(index_exports, {
53
57
  behaviorConfigSchema: () => behaviorConfigSchema,
54
58
  bend: () => bend,
55
59
  bendOptionsSchema: () => bendOptionsSchema,
60
+ book: () => book,
56
61
  buildAgentPayload: () => buildAgentPayload,
57
62
  buildDisplacementGLSL: () => buildDisplacementGLSL,
58
63
  buildFieldAgentPayload: () => buildFieldAgentPayload,
@@ -60,32 +65,45 @@ __export(index_exports, {
60
65
  buildFieldFragmentShader: () => buildFieldFragmentShader,
61
66
  buildFieldVertexShader: () => buildFieldVertexShader,
62
67
  buildJsxSnippet: () => buildJsxSnippet,
68
+ buildStageAgentPayload: () => buildStageAgentPayload,
69
+ buildStageComponentSource: () => buildStageComponentSource,
63
70
  carry: () => carry,
64
71
  carryDrive: () => carryDrive,
65
72
  carryOptionsSchema: () => carryOptionsSchema,
66
- cascade: () => cascade,
67
73
  clothConfigSchema: () => clothConfigSchema,
74
+ colonnade: () => colonnade,
68
75
  composeSurface: () => composeSurface,
69
76
  contentSchema: () => contentSchema,
70
77
  contentText: () => contentText,
71
78
  coreStateNames: () => coreStateNames,
72
79
  cornerNames: () => cornerNames,
73
80
  createSheetGeometry: () => createSheetGeometry,
81
+ createWalkPath: () => createWalkPath,
82
+ crumple: () => crumple,
83
+ crumpleBehavior: () => crumpleBehavior,
84
+ crumpleBehaviorOptionsSchema: () => crumpleBehaviorOptionsSchema,
85
+ crumpleOptionsSchema: () => crumpleOptionsSchema,
74
86
  curl: () => curl,
75
87
  curlOptionsSchema: () => curlOptionsSchema,
76
88
  dampTo: () => dampTo,
77
- deck: () => deck,
78
89
  deformerInstanceSchema: () => deformerInstanceSchema,
79
90
  describeConfig: () => describeConfig,
80
91
  describeFieldConfig: () => describeFieldConfig,
92
+ describeStage: () => describeStage,
81
93
  diffConfig: () => diffConfig,
82
94
  diffFieldProps: () => diffFieldProps,
95
+ diffStage: () => diffStage,
83
96
  displacePoint: () => displacePoint,
84
97
  distinctFieldPresets: () => distinctFieldPresets,
98
+ drape: () => drape,
99
+ drapeOptionsSchema: () => drapeOptionsSchema,
85
100
  drawBacking: () => drawBacking,
86
101
  fall: () => fall,
87
102
  fallOptionsSchema: () => fallOptionsSchema,
103
+ fan: () => fan,
104
+ fieldBounds: () => fieldBounds,
88
105
  fieldKeyboardStep: () => fieldKeyboardStep,
106
+ fitCamera: () => fitCamera,
89
107
  flattenNumeric: () => flattenNumeric,
90
108
  flight: () => flight,
91
109
  flightOptionsSchema: () => flightOptionsSchema,
@@ -102,12 +120,13 @@ __export(index_exports, {
102
120
  getLayout: () => getLayout,
103
121
  getLightingPreset: () => getLightingPreset,
104
122
  getPreset: () => getPreset,
123
+ getStagePreset: () => getStagePreset,
105
124
  getStock: () => getStock,
125
+ getWalk: () => getWalk,
106
126
  groupFieldPapers: () => groupFieldPapers,
107
127
  gust: () => gust,
108
128
  hang: () => hang,
109
129
  hangOptionsSchema: () => hangOptionsSchema,
110
- helix: () => helix,
111
130
  idleNames: () => idleNames,
112
131
  idlePresets: () => idlePresets,
113
132
  isBuiltinPreset: () => isBuiltinPreset,
@@ -120,6 +139,7 @@ __export(index_exports, {
120
139
  listDeformers: () => listDeformers,
121
140
  listLayouts: () => listLayouts,
122
141
  listPresets: () => listPresets,
142
+ listStagePresets: () => listStagePresets,
123
143
  makeGoboTexture: () => makeGoboTexture,
124
144
  mergeConfig: () => mergeConfig,
125
145
  mergeWithDeletes: () => mergeWithDeletes,
@@ -133,8 +153,11 @@ __export(index_exports, {
133
153
  peelOptionsSchema: () => peelOptionsSchema,
134
154
  physicsNames: () => physicsNames,
135
155
  physicsSchema: () => physicsSchema,
156
+ pile: () => pile,
157
+ qualityNames: () => qualityNames,
136
158
  quantizeProgress: () => quantizeProgress,
137
159
  quantizeTime: () => quantizeTime,
160
+ rack: () => rack,
138
161
  receiptContentSchema: () => receiptContentSchema,
139
162
  receiptTotals: () => receiptTotals,
140
163
  recordStateOverride: () => recordStateOverride,
@@ -143,6 +166,7 @@ __export(index_exports, {
143
166
  registerLayout: () => registerLayout,
144
167
  registerPreset: () => registerPreset,
145
168
  resolveConfig: () => resolveConfig,
169
+ resolveDeformerStack: () => resolveDeformerStack,
146
170
  resolveFieldSlotConfig: () => resolveFieldSlotConfig,
147
171
  resolveMode: () => resolveMode,
148
172
  resolveSegments: () => resolveSegments,
@@ -151,7 +175,6 @@ __export(index_exports, {
151
175
  roll: () => roll,
152
176
  rollOptionsSchema: () => rollOptionsSchema,
153
177
  runParityHarness: () => runParityHarness,
154
- scatter: () => scatter,
155
178
  sceneSchema: () => sceneSchema,
156
179
  serializePreset: () => serializePreset,
157
180
  sheet: () => sheet,
@@ -159,20 +182,28 @@ __export(index_exports, {
159
182
  sheetLayoutSchema: () => sheetLayoutSchema,
160
183
  sheetSchema: () => sheetSchema,
161
184
  sheetSlotXY: () => sheetSlotXY,
185
+ shotNames: () => shotNames,
162
186
  silhouetteRects: () => silhouetteRects,
187
+ spill: () => spill,
188
+ spread: () => spread,
163
189
  stackMinSegments: () => stackMinSegments,
164
190
  stackUniformValues: () => stackUniformValues,
191
+ stagePresets: () => stagePresets,
192
+ stageSchema: () => stageSchema,
165
193
  stateDefSchema: () => stateDefSchema,
166
194
  stateEventTransitions: () => stateEventTransitions,
167
195
  stateTransitionSchema: () => stateTransitionSchema,
168
196
  stockNames: () => stockNames,
169
197
  stockSchema: () => stockSchema,
170
198
  stocks: () => stocks,
199
+ stringifyStage: () => stringifyStage,
171
200
  stripStates: () => stripStates,
172
201
  supportsWebGL: () => supportsWebGL,
173
202
  surfaceSchema: () => surfaceSchema,
203
+ sweep: () => sweep,
174
204
  tornEdgesOnDetach: () => tornEdgesOnDetach,
175
- tunnel: () => tunnel,
205
+ translucencyUniforms: () => translucencyUniforms,
206
+ translucencyValues: () => translucencyValues,
176
207
  uniquePresetName: () => uniquePresetName,
177
208
  unregisterPreset: () => unregisterPreset,
178
209
  unroll: () => unroll,
@@ -180,6 +211,9 @@ __export(index_exports, {
180
211
  useContentAtlas: () => useContentAtlas,
181
212
  usePaperStates: () => usePaperStates,
182
213
  usePrefersReducedMotion: () => usePrefersReducedMotion,
214
+ walkNameFor: () => walkNameFor,
215
+ walkNames: () => walkNames,
216
+ walks: () => walks,
183
217
  wall: () => wall,
184
218
  wave: () => wave,
185
219
  waveOptionsSchema: () => waveOptionsSchema,
@@ -193,13 +227,13 @@ var import_fiber3 = require("@react-three/fiber");
193
227
  var import_react7 = require("react");
194
228
 
195
229
  // src/PaperMesh.tsx
196
- var THREE6 = __toESM(require("three"), 1);
230
+ var THREE7 = __toESM(require("three"), 1);
197
231
  var import_gsap2 = require("gsap");
198
232
  var import_fiber = require("@react-three/fiber");
199
233
  var import_react5 = require("react");
200
234
 
201
235
  // src/config/schema.ts
202
- var import_zod11 = require("zod");
236
+ var import_zod12 = require("zod");
203
237
 
204
238
  // src/config/merge.ts
205
239
  function mergeConfig(base, override) {
@@ -311,6 +345,7 @@ void FN(inout vec3 p, vec2 uv, float t) {
311
345
  }
312
346
  `
313
347
  ),
348
+ strength: "amount",
314
349
  uniforms: (o) => {
315
350
  const [sx, sy] = CORNER_SIGNS[o.corner];
316
351
  return {
@@ -772,16 +807,62 @@ var flight = {
772
807
  }
773
808
  };
774
809
 
810
+ // src/behaviors/crumple.ts
811
+ var import_zod11 = require("zod");
812
+ var crumpleBehaviorOptionsSchema = import_zod11.z.object({
813
+ /** 0 = flat sheet, 1 = crushed. */
814
+ progress: import_zod11.z.number().min(0).max(1).default(0.55),
815
+ /** Few big facets at 0, many small ones at 1. */
816
+ coarseness: import_zod11.z.number().min(0).max(1).default(0.35),
817
+ /** How far the sheet curls in on itself as it crushes. */
818
+ ball: import_zod11.z.number().min(0).max(1).default(0.5),
819
+ /** A different crush of the same paper. */
820
+ seed: import_zod11.z.number().int().min(0).max(7).default(0)
821
+ });
822
+ var crumpleBehavior = {
823
+ id: "crumple",
824
+ label: "Crumple",
825
+ defaults: crumpleBehaviorOptionsSchema.parse({}),
826
+ optionsSchema: crumpleBehaviorOptionsSchema,
827
+ progressParam: "progress",
828
+ duration: 2.6,
829
+ loopMode: "yoyo",
830
+ stack(o) {
831
+ return [
832
+ {
833
+ type: "crumple",
834
+ options: {
835
+ amount: o.progress,
836
+ scale: 1.5 + o.coarseness * 4.5,
837
+ pull: 0.5,
838
+ seed: o.seed
839
+ }
840
+ },
841
+ // The sheet closing in on itself. Paper does not crush flat, and
842
+ // without this the result reads as texture rather than as a ball.
843
+ {
844
+ type: "bend",
845
+ options: { curvature: o.progress * o.ball * 0.9, angle: 35 }
846
+ }
847
+ ];
848
+ }
849
+ };
850
+
775
851
  // src/config/schema.ts
776
- var sheetSchema = import_zod11.z.object({
852
+ var sheetSchema = import_zod12.z.object({
777
853
  /** World units. A letter sheet is ~1 × 1.4, a receipt ~1 × 2.6. */
778
- width: import_zod11.z.number().positive().max(20).default(1),
779
- height: import_zod11.z.number().positive().max(20).default(1.4),
854
+ width: import_zod12.z.number().positive().max(20).default(1),
855
+ height: import_zod12.z.number().positive().max(20).default(1.4),
780
856
  /** Visual thickness in mm-ish units; drives edge/shadow treatment, not geometry (yet). */
781
- thickness: import_zod11.z.number().min(0).max(2).default(0.2),
782
- /** 'auto' sizes the grid from the active deformers' needs. */
783
- segments: import_zod11.z.union([import_zod11.z.literal("auto"), import_zod11.z.number().int().min(2).max(256)]).default("auto"),
784
- cornerRadius: import_zod11.z.number().min(0).max(0.5).default(0)
857
+ thickness: import_zod12.z.number().min(0).max(2).default(0.2),
858
+ /**
859
+ * `'auto'` gives the LONG side 72 segments, whatever is on the sheet — a
860
+ * deformer's `minSegments` can only raise that floor, never lower it, so in
861
+ * practice a blank sheet is tessellated exactly as finely as a crumpled
862
+ * one. Set a number to take the decision yourself.
863
+ */
864
+ segments: import_zod12.z.union([import_zod12.z.literal("auto"), import_zod12.z.number().int().min(2).max(256)]).default("auto"),
865
+ cornerRadius: import_zod12.z.number().min(0).max(0.5).default(0)
785
866
  });
786
867
  var stockNames = [
787
868
  "printer",
@@ -792,46 +873,46 @@ var stockNames = [
792
873
  "photo-gloss",
793
874
  "sticker"
794
875
  ];
795
- var stockSchema = import_zod11.z.enum(stockNames);
796
- var blankContentBase = import_zod11.z.object({
797
- type: import_zod11.z.literal("blank")
876
+ var stockSchema = import_zod12.z.enum(stockNames);
877
+ var blankContentBase = import_zod12.z.object({
878
+ type: import_zod12.z.literal("blank")
798
879
  });
799
- var imageContentBase = import_zod11.z.object({
800
- type: import_zod11.z.literal("image"),
801
- src: import_zod11.z.string(),
802
- fit: import_zod11.z.enum(["cover", "contain"]).default("cover"),
880
+ var imageContentBase = import_zod12.z.object({
881
+ type: import_zod12.z.literal("image"),
882
+ src: import_zod12.z.string(),
883
+ fit: import_zod12.z.enum(["cover", "contain"]).default("cover"),
803
884
  /** Read by the hidden DOM mirror and the no-WebGL fallback. */
804
- alt: import_zod11.z.string().optional()
885
+ alt: import_zod12.z.string().optional()
805
886
  });
806
- var textContentBase = import_zod11.z.object({
807
- type: import_zod11.z.literal("text"),
808
- text: import_zod11.z.string().default("Dear reader,"),
809
- font: import_zod11.z.string().default('Georgia, "Times New Roman", serif'),
887
+ var textContentBase = import_zod12.z.object({
888
+ type: import_zod12.z.literal("text"),
889
+ text: import_zod12.z.string().default("Dear reader,"),
890
+ font: import_zod12.z.string().default('Georgia, "Times New Roman", serif'),
810
891
  /** px at texture resolution (long edge = 1024 logical px before DPR). */
811
- size: import_zod11.z.number().min(8).max(256).default(44),
812
- weight: import_zod11.z.number().min(100).max(900).default(400),
813
- color: import_zod11.z.string().default("#2b2620"),
814
- align: import_zod11.z.enum(["left", "center", "right"]).default("left"),
892
+ size: import_zod12.z.number().min(8).max(256).default(44),
893
+ weight: import_zod12.z.number().min(100).max(900).default(400),
894
+ color: import_zod12.z.string().default("#2b2620"),
895
+ align: import_zod12.z.enum(["left", "center", "right"]).default("left"),
815
896
  /** Fraction of the short edge. */
816
- padding: import_zod11.z.number().min(0).max(0.4).default(0.09),
817
- lineHeight: import_zod11.z.number().min(0.8).max(3).default(1.45)
897
+ padding: import_zod12.z.number().min(0).max(0.4).default(0.09),
898
+ lineHeight: import_zod12.z.number().min(0.8).max(3).default(1.45)
818
899
  });
819
- var receiptContentBase = import_zod11.z.object({
820
- type: import_zod11.z.literal("receipt"),
821
- store: import_zod11.z.string().default("PAPERLAB"),
822
- address: import_zod11.z.string().default("124 PAPER ST"),
823
- items: import_zod11.z.array(import_zod11.z.object({ name: import_zod11.z.string(), price: import_zod11.z.number() })).default([
900
+ var receiptContentBase = import_zod12.z.object({
901
+ type: import_zod12.z.literal("receipt"),
902
+ store: import_zod12.z.string().default("PAPERLAB"),
903
+ address: import_zod12.z.string().default("124 PAPER ST"),
904
+ items: import_zod12.z.array(import_zod12.z.object({ name: import_zod12.z.string(), price: import_zod12.z.number() })).default([
824
905
  { name: "CURL, TRUE", price: 12 },
825
906
  { name: "ROLL, TIGHT", price: 8.5 },
826
907
  { name: "SHEET, ONE", price: 0.99 }
827
908
  ]),
828
- taxRate: import_zod11.z.number().min(0).max(1).default(0.08),
829
- barcode: import_zod11.z.boolean().default(true),
909
+ taxRate: import_zod12.z.number().min(0).max(1).default(0.08),
910
+ barcode: import_zod12.z.boolean().default(true),
830
911
  /** Fixed so presets render deterministically; omit for "now". */
831
- timestamp: import_zod11.z.string().optional(),
832
- footer: import_zod11.z.string().default("KEEP FOR YOUR RECORDS")
912
+ timestamp: import_zod12.z.string().optional(),
913
+ footer: import_zod12.z.string().default("KEEP FOR YOUR RECORDS")
833
914
  });
834
- var backContentSchema = import_zod11.z.discriminatedUnion("type", [
915
+ var backContentSchema = import_zod12.z.discriminatedUnion("type", [
835
916
  blankContentBase,
836
917
  imageContentBase,
837
918
  textContentBase,
@@ -842,137 +923,140 @@ var blankContentSchema = blankContentBase.extend(withBack);
842
923
  var imageContentSchema = imageContentBase.extend(withBack);
843
924
  var textContentSchema = textContentBase.extend(withBack);
844
925
  var receiptContentSchema = receiptContentBase.extend(withBack);
845
- var contentSchema = import_zod11.z.discriminatedUnion("type", [
926
+ var contentSchema = import_zod12.z.discriminatedUnion("type", [
846
927
  blankContentSchema,
847
928
  imageContentSchema,
848
929
  textContentSchema,
849
930
  receiptContentSchema
850
931
  ]);
851
932
  var paperEdges = ["top", "right", "bottom", "left"];
852
- var surfaceSchema = import_zod11.z.object({
933
+ var surfaceSchema = import_zod12.z.object({
853
934
  /** Paper fiber noise, 0..1. */
854
- grain: import_zod11.z.number().min(0).max(1).optional(),
935
+ grain: import_zod12.z.number().min(0).max(1).optional(),
936
+ /** Light passing through the sheet from behind, 0..1. Stock defaults apply. */
937
+ translucency: import_zod12.z.number().min(0).max(1).optional(),
855
938
  /** Torn-edge alpha with a lightened fiber band. */
856
- deckle: import_zod11.z.object({
857
- edges: import_zod11.z.array(import_zod11.z.enum(paperEdges)).default(["bottom"]),
858
- roughness: import_zod11.z.number().min(0).max(1).default(0.5)
939
+ deckle: import_zod12.z.object({
940
+ edges: import_zod12.z.array(import_zod12.z.enum(paperEdges)).default(["bottom"]),
941
+ roughness: import_zod12.z.number().min(0).max(1).default(0.5)
859
942
  }).optional(),
860
943
  /** Visual AO/highlight companion to the fold deformer. */
861
- creaseLines: import_zod11.z.object({
944
+ creaseLines: import_zod12.z.object({
862
945
  /** Crease line direction, degrees (0 = horizontal lines). */
863
- angle: import_zod11.z.number().min(-360).max(360).default(0),
946
+ angle: import_zod12.z.number().min(-360).max(360).default(0),
864
947
  /** Positions across the sheet, 0..1 fractions. */
865
- positions: import_zod11.z.array(import_zod11.z.number().min(0).max(1)).default([1 / 3, 2 / 3]),
866
- strength: import_zod11.z.number().min(0).max(1).default(0.5)
948
+ positions: import_zod12.z.array(import_zod12.z.number().min(0).max(1)).default([1 / 3, 2 / 3]),
949
+ strength: import_zod12.z.number().min(0).max(1).default(0.5)
867
950
  }).optional(),
868
951
  /** Yellowing + foxing spots, 0..1. */
869
- aging: import_zod11.z.number().min(0).max(1).optional(),
952
+ aging: import_zod12.z.number().min(0).max(1).optional(),
870
953
  /** Reversed front-content ghost on the backside, 0..1. Stock defaults apply. */
871
- showThrough: import_zod11.z.number().min(0).max(1).optional(),
954
+ showThrough: import_zod12.z.number().min(0).max(1).optional(),
872
955
  /**
873
956
  * Postage-stamp perforation: alpha-punched semicircular holes along chosen
874
957
  * edges. `state` flips an edge to a ripped-through profile (torn) — set
875
958
  * automatically when a paper detaches from a `sheet` field, manual wins.
876
959
  */
877
- perforation: import_zod11.z.object({
878
- edges: import_zod11.z.union([import_zod11.z.array(import_zod11.z.enum(paperEdges)), import_zod11.z.literal("all")]).default("all"),
960
+ perforation: import_zod12.z.object({
961
+ edges: import_zod12.z.union([import_zod12.z.array(import_zod12.z.enum(paperEdges)), import_zod12.z.literal("all")]).default("all"),
879
962
  /** World units — default tuned to stamp scale. */
880
- holeRadius: import_zod11.z.number().min(2e-3).max(0.1).default(0.016),
881
- spacing: import_zod11.z.number().min(0.01).max(0.5).default(0.055),
882
- state: import_zod11.z.object({
883
- top: import_zod11.z.enum(["intact", "torn"]).optional(),
884
- right: import_zod11.z.enum(["intact", "torn"]).optional(),
885
- bottom: import_zod11.z.enum(["intact", "torn"]).optional(),
886
- left: import_zod11.z.enum(["intact", "torn"]).optional()
963
+ holeRadius: import_zod12.z.number().min(2e-3).max(0.1).default(0.016),
964
+ spacing: import_zod12.z.number().min(0.01).max(0.5).default(0.055),
965
+ state: import_zod12.z.object({
966
+ top: import_zod12.z.enum(["intact", "torn"]).optional(),
967
+ right: import_zod12.z.enum(["intact", "torn"]).optional(),
968
+ bottom: import_zod12.z.enum(["intact", "torn"]).optional(),
969
+ left: import_zod12.z.enum(["intact", "torn"]).optional()
887
970
  }).default({})
888
971
  }).optional()
889
972
  });
890
- var behaviorConfigSchema = import_zod11.z.discriminatedUnion("type", [
891
- peelOptionsSchema.extend({ type: import_zod11.z.literal("peel") }),
892
- unrollOptionsSchema.extend({ type: import_zod11.z.literal("unroll") }),
893
- flipOptionsSchema.extend({ type: import_zod11.z.literal("flip") }),
894
- letterFoldOptionsSchema.extend({ type: import_zod11.z.literal("letter-fold") }),
895
- hangOptionsSchema.extend({ type: import_zod11.z.literal("hang") }),
896
- flyOptionsSchema.extend({ type: import_zod11.z.literal("fly") }),
897
- fallOptionsSchema.extend({ type: import_zod11.z.literal("fall") }),
898
- carryOptionsSchema.extend({ type: import_zod11.z.literal("carry") }),
899
- flightOptionsSchema.extend({ type: import_zod11.z.literal("flight") })
973
+ var behaviorConfigSchema = import_zod12.z.discriminatedUnion("type", [
974
+ peelOptionsSchema.extend({ type: import_zod12.z.literal("peel") }),
975
+ unrollOptionsSchema.extend({ type: import_zod12.z.literal("unroll") }),
976
+ flipOptionsSchema.extend({ type: import_zod12.z.literal("flip") }),
977
+ letterFoldOptionsSchema.extend({ type: import_zod12.z.literal("letter-fold") }),
978
+ hangOptionsSchema.extend({ type: import_zod12.z.literal("hang") }),
979
+ flyOptionsSchema.extend({ type: import_zod12.z.literal("fly") }),
980
+ fallOptionsSchema.extend({ type: import_zod12.z.literal("fall") }),
981
+ carryOptionsSchema.extend({ type: import_zod12.z.literal("carry") }),
982
+ flightOptionsSchema.extend({ type: import_zod12.z.literal("flight") }),
983
+ crumpleBehaviorOptionsSchema.extend({ type: import_zod12.z.literal("crumple") })
900
984
  ]);
901
- var deformerInstanceSchema = import_zod11.z.object({
902
- type: import_zod11.z.string(),
903
- options: import_zod11.z.record(import_zod11.z.unknown()).default({}),
904
- enabled: import_zod11.z.boolean().default(true)
985
+ var deformerInstanceSchema = import_zod12.z.object({
986
+ type: import_zod12.z.string(),
987
+ options: import_zod12.z.record(import_zod12.z.unknown()).default({}),
988
+ enabled: import_zod12.z.boolean().default(true)
905
989
  });
906
990
  var physicsNames = ["none", "float", "tumble", "dangle", "taped", "breeze"];
907
- var clothConfigSchema = import_zod11.z.object({
908
- type: import_zod11.z.literal("cloth"),
909
- pins: import_zod11.z.enum(["top-edge", "top-corners", "corner", "none"]).default("top-edge"),
910
- wind: import_zod11.z.number().min(0).max(1).default(0.3),
991
+ var clothConfigSchema = import_zod12.z.object({
992
+ type: import_zod12.z.literal("cloth"),
993
+ pins: import_zod12.z.enum(["top-edge", "top-corners", "corner", "none"]).default("top-edge"),
994
+ wind: import_zod12.z.number().min(0).max(1).default(0.3),
911
995
  /** Bend stiffness: 1 = crisp paper, 0 = silk. */
912
- stiffness: import_zod11.z.number().min(0).max(1).default(0.8),
913
- gravity: import_zod11.z.number().min(0).max(2).default(1),
996
+ stiffness: import_zod12.z.number().min(0).max(1).default(0.8),
997
+ gravity: import_zod12.z.number().min(0).max(2).default(1),
914
998
  /** Local-space ground plane the sheet settles onto. */
915
- floor: import_zod11.z.number().min(-5).max(0).default(-1.4)
999
+ floor: import_zod12.z.number().min(-5).max(0).default(-1.4)
916
1000
  });
917
- var physicsSchema = import_zod11.z.union([
918
- import_zod11.z.enum(physicsNames),
919
- import_zod11.z.literal("cloth").transform(() => clothConfigSchema.parse({ type: "cloth" })),
1001
+ var physicsSchema = import_zod12.z.union([
1002
+ import_zod12.z.enum(physicsNames),
1003
+ import_zod12.z.literal("cloth").transform(() => clothConfigSchema.parse({ type: "cloth" })),
920
1004
  clothConfigSchema
921
1005
  ]);
922
- var lightingNames = ["studio", "window", "leaves", "goldenhour", "noir"];
923
- var sceneSchema = import_zod11.z.object({
924
- lighting: import_zod11.z.enum(lightingNames).default("studio")
1006
+ var lightingNames = ["studio", "window", "leaves", "goldenhour", "noir", "nave"];
1007
+ var sceneSchema = import_zod12.z.object({
1008
+ lighting: import_zod12.z.enum(lightingNames).default("studio")
925
1009
  });
926
1010
  var coreStateNames = ["rest", "hover", "pressed", "picked", "placed"];
927
1011
  var isStateName = (s) => coreStateNames.includes(s) || s.startsWith("custom:");
928
- var stateNameSchema = import_zod11.z.string().refine(isStateName, {
1012
+ var stateNameSchema = import_zod12.z.string().refine(isStateName, {
929
1013
  message: `state names are ${coreStateNames.join(", ")} or "custom:<name>"`
930
1014
  });
931
- var stateTransitionSchema = import_zod11.z.object({
932
- duration: import_zod11.z.number().min(0).max(5).default(0.35),
1015
+ var stateTransitionSchema = import_zod12.z.object({
1016
+ duration: import_zod12.z.number().min(0).max(5).default(0.35),
933
1017
  /** GSAP ease name. */
934
- ease: import_zod11.z.string().default("power2.out")
1018
+ ease: import_zod12.z.string().default("power2.out")
935
1019
  });
936
- var stateDefSchema = import_zod11.z.object({
1020
+ var stateDefSchema = import_zod12.z.object({
937
1021
  /** Deep-partial override of the paper schema (behavior params, surface, …). */
938
- overrides: import_zod11.z.record(import_zod11.z.unknown()).default({}),
1022
+ overrides: import_zod12.z.record(import_zod12.z.unknown()).default({}),
939
1023
  /** Transition INTO this state. */
940
1024
  transition: stateTransitionSchema.default({}),
941
1025
  /** Chained actions after arriving. v1: 'emit:<event>' only. */
942
- onEnter: import_zod11.z.array(import_zod11.z.string().regex(/^emit:[\w-]+$/, 'v1 actions are "emit:<event>"')).default([])
1026
+ onEnter: import_zod12.z.array(import_zod12.z.string().regex(/^emit:[\w-]+$/, 'v1 actions are "emit:<event>"')).default([])
943
1027
  });
944
- var paperStatesSchema = import_zod11.z.object({
1028
+ var paperStatesSchema = import_zod12.z.object({
945
1029
  initial: stateNameSchema.default("rest"),
946
- states: import_zod11.z.record(import_zod11.z.string(), stateDefSchema).default({}).refine((rec) => Object.keys(rec).every(isStateName), {
1030
+ states: import_zod12.z.record(import_zod12.z.string(), stateDefSchema).default({}).refine((rec) => Object.keys(rec).every(isStateName), {
947
1031
  message: `state names are ${coreStateNames.join(", ")} or "custom:<name>"`
948
1032
  }),
949
1033
  /** World-units drag distance that flips pressed → picked (pick-enabled behaviors only). */
950
- pickThreshold: import_zod11.z.number().min(5e-3).max(1).default(0.1)
1034
+ pickThreshold: import_zod12.z.number().min(5e-3).max(1).default(0.1)
951
1035
  });
952
- var metaSchema = import_zod11.z.object({
953
- name: import_zod11.z.string().default("untitled"),
954
- author: import_zod11.z.string().optional(),
955
- version: import_zod11.z.string().default("0"),
956
- tags: import_zod11.z.array(import_zod11.z.string()).default([])
1036
+ var metaSchema = import_zod12.z.object({
1037
+ name: import_zod12.z.string().default("untitled"),
1038
+ author: import_zod12.z.string().optional(),
1039
+ version: import_zod12.z.string().default("0"),
1040
+ tags: import_zod12.z.array(import_zod12.z.string()).default([])
957
1041
  });
958
- var paperConfigSchema = import_zod11.z.object({
1042
+ var paperConfigSchema = import_zod12.z.object({
959
1043
  meta: metaSchema.default({}),
960
1044
  sheet: sheetSchema.default({}),
961
1045
  stock: stockSchema.default("printer"),
962
1046
  content: contentSchema.default({ type: "blank" }),
963
1047
  /** A behavior OR a raw deformer stack — if both are present, `deformers` wins (it's the fork). */
964
1048
  behavior: behaviorConfigSchema.optional(),
965
- deformers: import_zod11.z.array(deformerInstanceSchema).optional(),
1049
+ deformers: import_zod12.z.array(deformerInstanceSchema).optional(),
966
1050
  surface: surfaceSchema.default({}),
967
1051
  physics: physicsSchema.default("none"),
968
1052
  scene: sceneSchema.default({}),
969
- onTwos: import_zod11.z.boolean().default(false),
1053
+ onTwos: import_zod12.z.boolean().default(false),
970
1054
  /** Interaction state machine — overrides-on-base diffs (spec M6 §1). */
971
1055
  states: paperStatesSchema.optional()
972
1056
  }).superRefine((config, ctx) => {
973
1057
  if (typeof config.physics === "object" && (config.behavior || config.deformers)) {
974
1058
  ctx.addIssue({
975
- code: import_zod11.z.ZodIssueCode.custom,
1059
+ code: import_zod12.z.ZodIssueCode.custom,
976
1060
  path: ["physics"],
977
1061
  message: "cloth physics and behavior/deformers are exclusive \u2014 cloth owns the vertices (pick Shape OR Simulation)"
978
1062
  });
@@ -983,7 +1067,7 @@ var paperConfigSchema = import_zod11.z.object({
983
1067
  if (!def) continue;
984
1068
  if (def.overrides.states !== void 0) {
985
1069
  ctx.addIssue({
986
- code: import_zod11.z.ZodIssueCode.custom,
1070
+ code: import_zod12.z.ZodIssueCode.custom,
987
1071
  path: ["states", "states", name, "overrides"],
988
1072
  message: "state overrides cannot override `states` (no nested state machines)"
989
1073
  });
@@ -994,7 +1078,7 @@ var paperConfigSchema = import_zod11.z.object({
994
1078
  if (!result.success) {
995
1079
  const first = result.error.issues[0];
996
1080
  ctx.addIssue({
997
- code: import_zod11.z.ZodIssueCode.custom,
1081
+ code: import_zod12.z.ZodIssueCode.custom,
998
1082
  path: ["states", "states", name, "overrides"],
999
1083
  message: `state "${name}" overrides don't validate against the paper schema: ${first ? `${first.path.join(".")} \u2014 ${first.message}` : "invalid"}`
1000
1084
  });
@@ -1038,6 +1122,7 @@ var stocks = {
1038
1122
  color: "#fbfaf7",
1039
1123
  roughness: 0.88,
1040
1124
  opacity: 1,
1125
+ translucency: 0.2,
1041
1126
  inkColor: "#222222",
1042
1127
  banding: 0,
1043
1128
  defaultSurface: { grain: 0.12 },
@@ -1050,6 +1135,7 @@ var stocks = {
1050
1135
  color: "#f6f3e9",
1051
1136
  roughness: 0.62,
1052
1137
  opacity: 1,
1138
+ translucency: 0.34,
1053
1139
  inkColor: "#3a3a3a",
1054
1140
  banding: 0.35,
1055
1141
  defaultSurface: { aging: 0.1 },
@@ -1062,6 +1148,7 @@ var stocks = {
1062
1148
  color: "#c9a06c",
1063
1149
  roughness: 0.96,
1064
1150
  opacity: 1,
1151
+ translucency: 0.08,
1065
1152
  inkColor: "#33261a",
1066
1153
  banding: 0,
1067
1154
  defaultSurface: { grain: 0.5 },
@@ -1074,6 +1161,7 @@ var stocks = {
1074
1161
  color: "#e9e4d6",
1075
1162
  roughness: 0.95,
1076
1163
  opacity: 1,
1164
+ translucency: 0.38,
1077
1165
  inkColor: "#3d3a34",
1078
1166
  banding: 0,
1079
1167
  defaultSurface: { grain: 0.7, aging: 0.15 },
@@ -1086,6 +1174,7 @@ var stocks = {
1086
1174
  color: "#f4f2ec",
1087
1175
  roughness: 0.42,
1088
1176
  opacity: 0.62,
1177
+ translucency: 0.86,
1089
1178
  inkColor: "#4a453d",
1090
1179
  banding: 0,
1091
1180
  defaultSurface: {},
@@ -1098,6 +1187,7 @@ var stocks = {
1098
1187
  color: "#ffffff",
1099
1188
  roughness: 0.22,
1100
1189
  opacity: 1,
1190
+ translucency: 0.03,
1101
1191
  inkColor: "#111111",
1102
1192
  banding: 0,
1103
1193
  defaultSurface: {},
@@ -1112,6 +1202,7 @@ var stocks = {
1112
1202
  color: "#ffffff",
1113
1203
  roughness: 0.3,
1114
1204
  opacity: 1,
1205
+ translucency: 0.06,
1115
1206
  inkColor: "#1a1a1a",
1116
1207
  banding: 0,
1117
1208
  defaultSurface: {},
@@ -1269,7 +1360,25 @@ var builtins = {
1269
1360
  type: "image",
1270
1361
  src: "https://images.unsplash.com/photo-1501854140801-50d01698950b?w=1200&q=80",
1271
1362
  fit: "cover"
1272
- }
1363
+ },
1364
+ // No print lies perfectly flat. A shade of bow is the whole difference
1365
+ // between a sheet of paper and a rectangle — and since this is the field
1366
+ // starter, it is what a layout's per-sheet bias has to scale.
1367
+ deformers: [{ type: "bend", options: { curvature: 0.35, angle: 0 } }]
1368
+ },
1369
+ "crumpled-note": {
1370
+ meta: { name: "Crumpled note", tags: ["crumple", "text", "handled"] },
1371
+ sheet: { width: 1.1, height: 1.4 },
1372
+ stock: "printer",
1373
+ content: {
1374
+ type: "text",
1375
+ text: "I wrote it out three times\nand threw all three away.",
1376
+ size: 42
1377
+ },
1378
+ behavior: { type: "crumple", progress: 0.62, coarseness: 0.4, ball: 0.55 },
1379
+ // Handled paper is dirty paper: the grain is what stops the facets
1380
+ // reading as folded plastic.
1381
+ surface: { grain: 0.5, aging: 0.18 }
1273
1382
  },
1274
1383
  "typed-note": {
1275
1384
  meta: { name: "Typed note", tags: ["text", "starter"] },
@@ -1505,17 +1614,20 @@ function useContentTexture(content, sheet2, stock) {
1505
1614
  // src/deformers/compose.ts
1506
1615
  var THREE3 = __toESM(require("three"), 1);
1507
1616
 
1617
+ // src/deformers/registry.ts
1618
+ var import_zod19 = require("zod");
1619
+
1508
1620
  // src/deformers/roll.ts
1509
- var import_zod12 = require("zod");
1510
- var rollOptionsSchema = import_zod12.z.object({
1621
+ var import_zod13 = require("zod");
1622
+ var rollOptionsSchema = import_zod13.z.object({
1511
1623
  /** Direction of rolling in the sheet plane, degrees. 0 = +x, 90 = +y. */
1512
- angle: import_zod12.z.number().min(-360).max(360).default(90),
1624
+ angle: import_zod13.z.number().min(-360).max(360).default(90),
1513
1625
  /** Signed distance (along the roll direction, from sheet center) where the roll begins. */
1514
- boundary: import_zod12.z.number().min(-20).max(20).default(0),
1626
+ boundary: import_zod13.z.number().min(-20).max(20).default(0),
1515
1627
  /** Cylinder radius — sharpness of the roll. */
1516
- radius: import_zod12.z.number().min(0.01).max(2).default(0.12),
1628
+ radius: import_zod13.z.number().min(0.01).max(2).default(0.12),
1517
1629
  /** Radius growth per radian so multi-turn rolls spiral instead of z-fighting. */
1518
- spiral: import_zod12.z.number().min(0).max(0.2).default(0.015)
1630
+ spiral: import_zod13.z.number().min(0).max(0.2).default(0.015)
1519
1631
  });
1520
1632
  var DEG2 = Math.PI / 180;
1521
1633
  var roll = {
@@ -1570,15 +1682,20 @@ void FN(inout vec3 p, vec2 uv, float t) {
1570
1682
  };
1571
1683
 
1572
1684
  // src/deformers/bend.ts
1573
- var import_zod13 = require("zod");
1574
- var bendOptionsSchema = import_zod13.z.object({
1685
+ var import_zod14 = require("zod");
1686
+ var bendOptionsSchema = import_zod14.z.object({
1575
1687
  /** 1/radius in world units; sign flips the arc direction. 0 = flat. */
1576
- curvature: import_zod13.z.number().min(-4).max(4).default(0.6),
1688
+ curvature: import_zod14.z.number().min(-4).max(4).default(0.6),
1577
1689
  /** Bend axis direction in the sheet plane, degrees. 0 bends across x. */
1578
- angle: import_zod13.z.number().min(-360).max(360).default(0)
1690
+ angle: import_zod14.z.number().min(-360).max(360).default(0)
1579
1691
  });
1580
1692
  var DEG3 = Math.PI / 180;
1581
1693
  var EPS = 1e-5;
1694
+ function sinMinusX(x) {
1695
+ if (Math.abs(x) > 1) return Math.sin(x) - x;
1696
+ const x2 = x * x;
1697
+ return -x * x2 / 6 * (1 - x2 / 20 * (1 - x2 / 42 * (1 - x2 / 72)));
1698
+ }
1582
1699
  var bend = {
1583
1700
  id: "bend",
1584
1701
  label: "Bend",
@@ -1591,49 +1708,56 @@ var bend = {
1591
1708
  const dirY = Math.sin(o.angle * DEG3);
1592
1709
  const d = out.x * dirX + out.y * dirY;
1593
1710
  const r = 1 / o.curvature;
1594
- const theta = d / r;
1711
+ const theta = d * o.curvature;
1595
1712
  const sin = Math.sin(theta);
1596
- const cos = Math.cos(theta);
1597
- const newD = (r - out.z) * sin;
1598
- const newZ = r * (1 - cos) + out.z * cos;
1599
- out.x += dirX * (newD - d);
1600
- out.y += dirY * (newD - d);
1601
- out.z = newZ;
1713
+ const halfSin = Math.sin(theta * 0.5);
1714
+ const z0 = out.z;
1715
+ const shift = r * sinMinusX(theta) - z0 * sin;
1716
+ out.x += dirX * shift;
1717
+ out.y += dirY * shift;
1718
+ out.z = 2 * r * halfSin * halfSin + z0 * Math.cos(theta);
1602
1719
  },
1603
1720
  glsl: {
1604
1721
  chunk: (
1605
1722
  /* glsl */
1606
1723
  `
1724
+ float FN_sinm(float x) {
1725
+ if (abs(x) > 1.0) return sin(x) - x;
1726
+ float x2 = x * x;
1727
+ return (-x * x2 / 6.0) * (1.0 - (x2 / 20.0) * (1.0 - (x2 / 42.0) * (1.0 - x2 / 72.0)));
1728
+ }
1729
+
1607
1730
  void FN(inout vec3 p, vec2 uv, float t) {
1608
1731
  if (abs(U_curvature) < 1e-5) return;
1609
1732
  vec2 dir = vec2(cos(U_angle), sin(U_angle));
1610
1733
  float d = dot(p.xy, dir);
1611
1734
  float r = 1.0 / U_curvature;
1612
- float theta = d / r;
1735
+ float theta = d * U_curvature;
1613
1736
  float sn = sin(theta);
1614
- float cs = cos(theta);
1615
- float newD = (r - p.z) * sn;
1616
- float newZ = r * (1.0 - cs) + p.z * cs;
1617
- p.xy += dir * (newD - d);
1618
- p.z = newZ;
1737
+ float hs = sin(theta * 0.5);
1738
+ float z0 = p.z;
1739
+ float shift = r * FN_sinm(theta) - z0 * sn;
1740
+ p.xy += dir * shift;
1741
+ p.z = 2.0 * r * hs * hs + z0 * cos(theta);
1619
1742
  }
1620
1743
  `
1621
1744
  ),
1745
+ strength: "curvature",
1622
1746
  uniforms: (o) => ({ curvature: o.curvature, angle: o.angle * DEG3 })
1623
1747
  }
1624
1748
  };
1625
1749
 
1626
1750
  // src/deformers/fold.ts
1627
- var import_zod14 = require("zod");
1628
- var foldOptionsSchema = import_zod14.z.object({
1751
+ var import_zod15 = require("zod");
1752
+ var foldOptionsSchema = import_zod15.z.object({
1629
1753
  /** Direction of the fold travel in the sheet plane, degrees (the crease line runs perpendicular). */
1630
- angle: import_zod14.z.number().min(-360).max(360).default(90),
1754
+ angle: import_zod15.z.number().min(-360).max(360).default(90),
1631
1755
  /** Signed distance of the crease line from the sheet center, along the travel direction. */
1632
- offset: import_zod14.z.number().min(-20).max(20).default(0),
1756
+ offset: import_zod15.z.number().min(-20).max(20).default(0),
1633
1757
  /** How far the flap folds over, degrees. 180 = flat against the sheet. */
1634
- foldAngle: import_zod14.z.number().min(-180).max(180).default(90),
1758
+ foldAngle: import_zod15.z.number().min(-180).max(180).default(90),
1635
1759
  /** Width of the soft hinge — paper never creases to a mathematical edge. */
1636
- radius: import_zod14.z.number().min(5e-3).max(0.5).default(0.04)
1760
+ radius: import_zod15.z.number().min(5e-3).max(0.5).default(0.04)
1637
1761
  });
1638
1762
  var DEG4 = Math.PI / 180;
1639
1763
  var fold = {
@@ -1701,6 +1825,7 @@ void FN(inout vec3 p, vec2 uv, float t) {
1701
1825
  }
1702
1826
  `
1703
1827
  ),
1828
+ strength: "foldAngle",
1704
1829
  uniforms: (o) => ({
1705
1830
  angle: o.angle * DEG4,
1706
1831
  offset: o.offset,
@@ -1711,16 +1836,16 @@ void FN(inout vec3 p, vec2 uv, float t) {
1711
1836
  };
1712
1837
 
1713
1838
  // src/deformers/wave.ts
1714
- var import_zod15 = require("zod");
1715
- var waveOptionsSchema = import_zod15.z.object({
1716
- amplitude: import_zod15.z.number().min(0).max(0.3).default(0.04),
1717
- wavelength: import_zod15.z.number().min(0.05).max(2).default(0.5),
1839
+ var import_zod16 = require("zod");
1840
+ var waveOptionsSchema = import_zod16.z.object({
1841
+ amplitude: import_zod16.z.number().min(0).max(0.3).default(0.04),
1842
+ wavelength: import_zod16.z.number().min(0.05).max(2).default(0.5),
1718
1843
  /** Travel speed; 0 freezes the ripple. */
1719
- speed: import_zod15.z.number().min(0).max(3).default(0.8),
1844
+ speed: import_zod16.z.number().min(0).max(3).default(0.8),
1720
1845
  /** Travel direction in the sheet plane, degrees. */
1721
- angle: import_zod15.z.number().min(-360).max(360).default(90),
1846
+ angle: import_zod16.z.number().min(-360).max(360).default(90),
1722
1847
  /** Zero the displacement at one edge (a taped/pinned edge doesn't ripple). */
1723
- pinnedEdge: import_zod15.z.enum(["none", "top", "bottom", "left", "right"]).default("none")
1848
+ pinnedEdge: import_zod16.z.enum(["none", "top", "bottom", "left", "right"]).default("none")
1724
1849
  });
1725
1850
  var DEG5 = Math.PI / 180;
1726
1851
  var TAU = Math.PI * 2;
@@ -1762,6 +1887,7 @@ void FN(inout vec3 p, vec2 uv, float t) {
1762
1887
  }
1763
1888
  `
1764
1889
  ),
1890
+ strength: "amplitude",
1765
1891
  uniforms: (o) => ({
1766
1892
  amplitude: o.amplitude,
1767
1893
  wavelength: o.wavelength,
@@ -1772,6 +1898,188 @@ void FN(inout vec3 p, vec2 uv, float t) {
1772
1898
  }
1773
1899
  };
1774
1900
 
1901
+ // src/deformers/drape.ts
1902
+ var import_zod17 = require("zod");
1903
+ var drapeOptionsSchema = import_zod17.z.object({
1904
+ /** Fold depth at the free edge, world units. */
1905
+ amplitude: import_zod17.z.number().min(0).max(0.6).default(0.12),
1906
+ /** How many folds run down the drop. */
1907
+ folds: import_zod17.z.number().min(0.5).max(16).default(4),
1908
+ /**
1909
+ * How fast folds deepen away from the pinned edge. 1 is linear; higher
1910
+ * holds the top flat and gathers all the movement at the free end, which
1911
+ * is what a sheet hung from a rod actually does.
1912
+ */
1913
+ falloff: import_zod17.z.number().min(0.3).max(4).default(1.6),
1914
+ /** How much a second, non-harmonic fold breaks the regularity. */
1915
+ irregular: import_zod17.z.number().min(0).max(1).default(0.45),
1916
+ /** How much the sheet narrows as its folds deepen. */
1917
+ gather: import_zod17.z.number().min(0).max(1).default(0.5),
1918
+ pinnedEdge: import_zod17.z.enum(["top", "bottom"]).default("top")
1919
+ });
1920
+ var TAU2 = Math.PI * 2;
1921
+ var drape = {
1922
+ id: "drape",
1923
+ label: "Drape",
1924
+ defaults: drapeOptionsSchema.parse({}),
1925
+ optionsSchema: drapeOptionsSchema,
1926
+ geometry: { minSegments: 48 },
1927
+ displace(out, uv, o) {
1928
+ if (o.amplitude === 0) return;
1929
+ const drop = o.pinnedEdge === "top" ? 1 - uv.y : uv.y;
1930
+ const depth = drop ** o.falloff;
1931
+ const u = uv.x * TAU2 * o.folds;
1932
+ const fold2 = Math.sin(u) + o.irregular * 0.6 * Math.sin(u * 1.7 + 2.1);
1933
+ out.z += o.amplitude * depth * fold2;
1934
+ const pinch = o.gather * depth * Math.min(o.amplitude * o.folds * 0.8, 0.6);
1935
+ out.x *= 1 - pinch;
1936
+ },
1937
+ glsl: {
1938
+ chunk: (
1939
+ /* glsl */
1940
+ `
1941
+ void FN(inout vec3 p, vec2 uv, float t) {
1942
+ if (U_amplitude == 0.0) return;
1943
+ float drop = U_pin == 1.0 ? 1.0 - uv.y : uv.y;
1944
+ float depth = pow(drop, U_falloff);
1945
+ float u = uv.x * 6.283185307179586 * U_folds;
1946
+ float fold = sin(u) + U_irregular * 0.6 * sin(u * 1.7 + 2.1);
1947
+ p.z += U_amplitude * depth * fold;
1948
+ float pinch = U_gather * depth * min(U_amplitude * U_folds * 0.8, 0.6);
1949
+ p.x *= 1.0 - pinch;
1950
+ }
1951
+ `
1952
+ ),
1953
+ strength: "amplitude",
1954
+ uniforms: (o) => ({
1955
+ amplitude: o.amplitude,
1956
+ folds: o.folds,
1957
+ falloff: o.falloff,
1958
+ irregular: o.irregular,
1959
+ gather: o.gather,
1960
+ pin: o.pinnedEdge === "top" ? 1 : 2
1961
+ })
1962
+ }
1963
+ };
1964
+
1965
+ // src/deformers/crumple.ts
1966
+ var import_zod18 = require("zod");
1967
+ var crumpleOptionsSchema = import_zod18.z.object({
1968
+ /** How crushed, 0..1. Peak-to-peak height, and it drives the pull too. */
1969
+ amount: import_zod18.z.number().min(0).max(1).default(0.35),
1970
+ /** Facets per world unit. Higher is finer, and needs more segments to resolve. */
1971
+ scale: import_zod18.z.number().min(0.5).max(8).default(3),
1972
+ /**
1973
+ * How much the sheet draws in on itself. Crumpled paper occupies a smaller
1974
+ * footprint than flat paper; without this it reads as an embossed sheet
1975
+ * rather than a crushed one.
1976
+ */
1977
+ pull: import_zod18.z.number().min(0).max(1).default(0.4),
1978
+ /** A different crush of the same paper. */
1979
+ seed: import_zod18.z.number().int().min(0).max(7).default(0)
1980
+ });
1981
+ function mod(x, y) {
1982
+ return x - y * Math.floor(x / y);
1983
+ }
1984
+ function jitter(cx, cy, seed) {
1985
+ const hx = mod(cx * 37 + cy * 17 + seed * 5, 64);
1986
+ const hy = mod(cx * 23 + cy * 41 + seed * 11, 64);
1987
+ return [0.2 + 0.6 * mod(hx * 13, 7) / 6, 0.2 + 0.6 * mod(hy * 29, 11) / 10];
1988
+ }
1989
+ var NORM = 0.5366;
1990
+ function cellSign(cx, cy, seed) {
1991
+ return 1 - 2 * mod(cx * 11 + cy * 7 + seed * 3, 2);
1992
+ }
1993
+ var crumple = {
1994
+ id: "crumple",
1995
+ label: "Crumple",
1996
+ defaults: crumpleOptionsSchema.parse({}),
1997
+ optionsSchema: crumpleOptionsSchema,
1998
+ // A crease the grid cannot resolve is a smooth bump, and a sheet of smooth
1999
+ // bumps is not a crumple. Note this is a FLOOR, and `segments: 'auto'`
2000
+ // already hands the long side 72 — so this only bites when a preset asks
2001
+ // for a coarser grid by hand. Measured (`pnpm perf:field`), the real cost
2002
+ // of this deformer is not geometry at all: it is nine cell evaluations per
2003
+ // probe and three probes per vertex for the normal.
2004
+ geometry: { minSegments: 72 },
2005
+ displace(out, _uv, o) {
2006
+ if (o.amount === 0) return;
2007
+ const qx = out.x * o.scale;
2008
+ const qy = out.y * o.scale;
2009
+ const gx = Math.floor(qx);
2010
+ const gy = Math.floor(qy);
2011
+ let f1 = 1e9;
2012
+ let f2 = 1e9;
2013
+ let winX = gx;
2014
+ let winY = gy;
2015
+ for (let dy = -1; dy <= 1; dy++) {
2016
+ for (let dx = -1; dx <= 1; dx++) {
2017
+ const cx = gx + dx;
2018
+ const cy = gy + dy;
2019
+ const [jx, jy] = jitter(cx, cy, o.seed);
2020
+ const ex = cx + jx - qx;
2021
+ const ey = cy + jy - qy;
2022
+ const dist = Math.sqrt(ex * ex + ey * ey);
2023
+ if (dist < f1) {
2024
+ f2 = f1;
2025
+ f1 = dist;
2026
+ winX = cx;
2027
+ winY = cy;
2028
+ } else if (dist < f2) {
2029
+ f2 = dist;
2030
+ }
2031
+ }
2032
+ }
2033
+ out.z += cellSign(winX, winY, o.seed) * (f2 - f1) * o.amount * NORM;
2034
+ const pull = 1 - o.amount * o.pull * 0.35;
2035
+ out.x *= pull;
2036
+ out.y *= pull;
2037
+ },
2038
+ glsl: {
2039
+ chunk: (
2040
+ /* glsl */
2041
+ `
2042
+ vec2 FN_jitter(float cx, float cy, float seed) {
2043
+ float hx = mod(cx * 37.0 + cy * 17.0 + seed * 5.0, 64.0);
2044
+ float hy = mod(cx * 23.0 + cy * 41.0 + seed * 11.0, 64.0);
2045
+ return vec2(0.2 + 0.6 * mod(hx * 13.0, 7.0) / 6.0, 0.2 + 0.6 * mod(hy * 29.0, 11.0) / 10.0);
2046
+ }
2047
+
2048
+ float FN_sign(float cx, float cy, float seed) {
2049
+ return 1.0 - 2.0 * mod(cx * 11.0 + cy * 7.0 + seed * 3.0, 2.0);
2050
+ }
2051
+
2052
+ void FN(inout vec3 p, vec2 uv, float t) {
2053
+ if (U_amount == 0.0) return;
2054
+ vec2 flat2 = p.xy;
2055
+ vec2 q = flat2 * U_scale;
2056
+ vec2 g = floor(q);
2057
+
2058
+ float f1 = 1e9;
2059
+ float f2 = 1e9;
2060
+ vec2 win = g;
2061
+ for (int dy = -1; dy <= 1; dy++) {
2062
+ for (int dx = -1; dx <= 1; dx++) {
2063
+ vec2 c = g + vec2(float(dx), float(dy));
2064
+ float dist = length(c + FN_jitter(c.x, c.y, U_seed) - q);
2065
+ if (dist < f1) { f2 = f1; f1 = dist; win = c; }
2066
+ else if (dist < f2) { f2 = dist; }
2067
+ }
2068
+ }
2069
+
2070
+ p.z += FN_sign(win.x, win.y, U_seed) * (f2 - f1) * U_amount * ${NORM};
2071
+ float pull = 1.0 - U_amount * U_pull * 0.35;
2072
+ p.xy = flat2 * pull;
2073
+ }
2074
+ `
2075
+ ),
2076
+ // `amount` drives both the height and the pull, so a field instance's
2077
+ // bias scales the whole crush rather than half of it.
2078
+ strength: "amount",
2079
+ uniforms: (o) => ({ amount: o.amount, scale: o.scale, pull: o.pull, seed: o.seed })
2080
+ }
2081
+ };
2082
+
1775
2083
  // src/deformers/registry.ts
1776
2084
  var registry = /* @__PURE__ */ new Map();
1777
2085
  function registerDeformer(deformer) {
@@ -1792,6 +2100,26 @@ registerDeformer(curl);
1792
2100
  registerDeformer(bend);
1793
2101
  registerDeformer(fold);
1794
2102
  registerDeformer(wave);
2103
+ registerDeformer(drape);
2104
+ registerDeformer(crumple);
2105
+ function resolveDeformerStack(raw) {
2106
+ return raw.map((instance, i) => {
2107
+ const deformer = getDeformer(instance.type);
2108
+ const schema = deformer.optionsSchema instanceof import_zod19.z.ZodObject ? deformer.optionsSchema.strict() : deformer.optionsSchema;
2109
+ const parsed = schema.safeParse(instance.options ?? {});
2110
+ if (!parsed.success) {
2111
+ const issue = parsed.error.issues[0];
2112
+ throw new Error(
2113
+ `[paperlab] deformers[${i}] ("${instance.type}"): ${issue ? `${issue.path.join(".") || "options"} \u2014 ${issue.message}` : "invalid options"}`
2114
+ );
2115
+ }
2116
+ return {
2117
+ type: instance.type,
2118
+ options: parsed.data,
2119
+ enabled: instance.enabled
2120
+ };
2121
+ });
2122
+ }
1795
2123
  function stackIsAnimated(stack) {
1796
2124
  return stack.some((i) => i.enabled !== false && registry.get(i.type)?.animated);
1797
2125
  }
@@ -1859,6 +2187,7 @@ registerBehavior(fly);
1859
2187
  registerBehavior(fall);
1860
2188
  registerBehavior(carry);
1861
2189
  registerBehavior(flight);
2190
+ registerBehavior(crumpleBehavior);
1862
2191
 
1863
2192
  // src/physics/idle.ts
1864
2193
  var idleNames = ["float", "tumble", "dangle", "taped", "breeze"];
@@ -2007,13 +2336,13 @@ var ClothSim = class {
2007
2336
  this.stillFrames = 0;
2008
2337
  }
2009
2338
  /** Nearest particle to a local-space point — the grab interface. */
2010
- grabNearest(x, y, z18) {
2339
+ grabNearest(x, y, z27) {
2011
2340
  let best = -1;
2012
2341
  let bestDist = Infinity;
2013
2342
  for (let i = 0; i < this.count; i++) {
2014
2343
  const dx = this.positions[i * 3] - x;
2015
2344
  const dy = this.positions[i * 3 + 1] - y;
2016
- const dz = this.positions[i * 3 + 2] - z18;
2345
+ const dz = this.positions[i * 3 + 2] - z27;
2017
2346
  const d = dx * dx + dy * dy + dz * dz;
2018
2347
  if (d < bestDist) {
2019
2348
  bestDist = d;
@@ -2024,15 +2353,15 @@ var ClothSim = class {
2024
2353
  this.wake();
2025
2354
  return best;
2026
2355
  }
2027
- moveGrab(x, y, z18) {
2356
+ moveGrab(x, y, z27) {
2028
2357
  if (this.grabbedIndex < 0) return;
2029
2358
  const i3 = this.grabbedIndex * 3;
2030
2359
  this.positions[i3] = x;
2031
2360
  this.positions[i3 + 1] = y;
2032
- this.positions[i3 + 2] = z18;
2361
+ this.positions[i3 + 2] = z27;
2033
2362
  this.prev[i3] = x;
2034
2363
  this.prev[i3 + 1] = y;
2035
- this.prev[i3 + 2] = z18;
2364
+ this.prev[i3 + 2] = z27;
2036
2365
  this.wake();
2037
2366
  }
2038
2367
  release() {
@@ -2068,19 +2397,19 @@ var ClothSim = class {
2068
2397
  }
2069
2398
  const x = p[i3];
2070
2399
  const y = p[i3 + 1];
2071
- const z18 = p[i3 + 2];
2400
+ const z27 = p[i3 + 2];
2072
2401
  const gust2 = wind * (0.55 + 0.45 * Math.sin(this.time * 1.7 + x * 2.1 + y * 1.3)) * 0.9;
2073
2402
  const ax = gust2 * 0.25;
2074
2403
  const az = gust2;
2075
2404
  const vx = (x - this.prev[i3]) * damping;
2076
2405
  const vy = (y - this.prev[i3 + 1]) * damping;
2077
- const vz = (z18 - this.prev[i3 + 2]) * damping;
2406
+ const vz = (z27 - this.prev[i3 + 2]) * damping;
2078
2407
  this.prev[i3] = x;
2079
2408
  this.prev[i3 + 1] = y;
2080
- this.prev[i3 + 2] = z18;
2409
+ this.prev[i3 + 2] = z27;
2081
2410
  p[i3] = x + vx + ax * dt2;
2082
2411
  p[i3 + 1] = y + vy - gravity * 3.2 * dt2;
2083
- p[i3 + 2] = z18 + vz + az * dt2;
2412
+ p[i3 + 2] = z27 + vz + az * dt2;
2084
2413
  maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
2085
2414
  }
2086
2415
  for (let iter = 0; iter < SOLVER_ITERATIONS; iter++) {
@@ -2126,18 +2455,171 @@ var ClothSim = class {
2126
2455
  };
2127
2456
 
2128
2457
  // src/surface/PaperMaterial.tsx
2129
- var THREE5 = __toESM(require("three"), 1);
2458
+ var THREE6 = __toESM(require("three"), 1);
2130
2459
  var import_react2 = require("react");
2131
2460
  var import_three_custom_shader_material = __toESM(require("three-custom-shader-material"), 1);
2132
2461
 
2133
2462
  // src/surface/compose.ts
2463
+ var THREE5 = __toESM(require("three"), 1);
2464
+
2465
+ // src/surface/translucency.ts
2134
2466
  var THREE4 = __toESM(require("three"), 1);
2467
+
2468
+ // src/scene/lighting.ts
2469
+ var lightingPresets = {
2470
+ studio: {
2471
+ id: "studio",
2472
+ label: "Studio",
2473
+ ambient: 0.65,
2474
+ key: { color: "#ffffff", intensity: 1.6, position: [2.5, 4, 3] },
2475
+ contactShadowOpacity: 0.3,
2476
+ contactShadowBlur: 2.4,
2477
+ exposure: 1,
2478
+ shadow: { mapSize: 1024, radius: 4 }
2479
+ },
2480
+ window: {
2481
+ id: "window",
2482
+ label: "Window",
2483
+ ambient: 0.5,
2484
+ key: { color: "#ffe3c0", intensity: 1.9, position: [3, 2.6, 2.6] },
2485
+ contactShadowOpacity: 0.35,
2486
+ contactShadowBlur: 2.6,
2487
+ exposure: 1,
2488
+ shadow: { mapSize: 1024, radius: 5 },
2489
+ gobo: { kind: "blinds", drift: 4e-3, angle: 0.62 }
2490
+ },
2491
+ leaves: {
2492
+ id: "leaves",
2493
+ label: "Leaves",
2494
+ ambient: 0.45,
2495
+ key: { color: "#fff2d8", intensity: 2, position: [2.2, 3.6, 2.4] },
2496
+ contactShadowOpacity: 0.4,
2497
+ contactShadowBlur: 2.8,
2498
+ exposure: 1,
2499
+ shadow: { mapSize: 1024, radius: 6 },
2500
+ gobo: { kind: "leaves", drift: 0.012, angle: 0.7 }
2501
+ },
2502
+ goldenhour: {
2503
+ id: "goldenhour",
2504
+ label: "Golden hour",
2505
+ ambient: 0.32,
2506
+ key: { color: "#ffb066", intensity: 2.4, position: [4, 0.9, 2.2] },
2507
+ contactShadowOpacity: 0.45,
2508
+ contactShadowBlur: 3.2,
2509
+ exposure: 1.15,
2510
+ shadow: { mapSize: 1024, radius: 7 }
2511
+ },
2512
+ noir: {
2513
+ id: "noir",
2514
+ label: "Noir",
2515
+ ambient: 0.07,
2516
+ key: { color: "#ffffff", intensity: 2.6, position: [2, 3, 1.6] },
2517
+ contactShadowOpacity: 0.7,
2518
+ contactShadowBlur: 1.1,
2519
+ exposure: 1.05,
2520
+ shadow: { mapSize: 2048, radius: 1 }
2521
+ },
2522
+ nave: {
2523
+ id: "nave",
2524
+ label: "Nave",
2525
+ // Dim, and the key sits BEHIND the walk rather than beside it: this mode
2526
+ // is carried by light coming through the paper, not off it. Ambient is
2527
+ // nearly nothing so the only bright thing in frame is the source itself.
2528
+ ambient: 0.09,
2529
+ key: { color: "#fff1dc", intensity: 3.4, position: [0, 7, -16] },
2530
+ contactShadowOpacity: 0.55,
2531
+ contactShadowBlur: 3.6,
2532
+ exposure: 1.2,
2533
+ shadow: { mapSize: 2048, radius: 6 },
2534
+ // Warm and light, not black: distance in a backlit hall washes TOWARD
2535
+ // the source, which is what separates haze from murk.
2536
+ fog: { color: "#c9baa3", near: 5, far: 38 }
2537
+ }
2538
+ };
2539
+ function getLightingPreset(name) {
2540
+ return lightingPresets[name];
2541
+ }
2542
+
2543
+ // src/surface/translucency.ts
2544
+ var TRANSLUCENCY_VARYINGS = (
2545
+ /* glsl */
2546
+ `
2547
+ varying vec3 vPlWorldNormal;
2548
+ varying vec3 vPlViewDir;
2549
+ `
2550
+ );
2551
+ function translucencyVertexChunk(slots) {
2552
+ return (
2553
+ /* glsl */
2554
+ `
2555
+ {
2556
+ mat4 plModel = ${slots.model};
2557
+ vec4 plWorld = plModel * vec4(${slots.position}, 1.0);
2558
+ // Uniform scale only \u2014 layouts scale sheets evenly, so the plain 3\xD73 is
2559
+ // the correct normal matrix here and skips an inverse-transpose.
2560
+ vPlWorldNormal = normalize(mat3(plModel) * ${slots.normal});
2561
+ vPlViewDir = cameraPosition - plWorld.xyz;
2562
+ }
2563
+ `
2564
+ );
2565
+ }
2566
+ var TRANSMISSION_GAIN = 0.5;
2567
+ var TRANSLUCENCY_FRAGMENT = (
2568
+ /* glsl */
2569
+ `
2570
+ uniform float uTranslucency;
2571
+ uniform vec3 uBackLightDir;
2572
+ uniform vec3 uBackLightColor;
2573
+ uniform float uAmbientTransmission;
2574
+ ${TRANSLUCENCY_VARYINGS}
2575
+
2576
+ vec3 plTransmission(vec3 inkFilter) {
2577
+ if (uTranslucency <= 0.0) return vec3(0.0);
2578
+ vec3 n = normalize(vPlWorldNormal);
2579
+ // Sheets render double-sided; the back face needs the normal it actually shows.
2580
+ if (!gl_FrontFacing) n = -n;
2581
+ // The lamp is BEHIND this sheet when the face we are looking at points away
2582
+ // from it \u2014 that is the whole test.
2583
+ float behind = clamp(-dot(n, uBackLightDir), 0.0, 1.0);
2584
+ // A grazing view looks through more paper, and more paper passes less light.
2585
+ float thickness = abs(dot(n, normalize(vPlViewDir)));
2586
+ // Paper in a lit room glows whatever way it is turned \u2014 a sheet standing
2587
+ // edge-on to the only lamp is not black. Without this floor, a banner
2588
+ // whose face runs parallel to the key light gets neither diffuse nor
2589
+ // transmission and drops out of the picture entirely.
2590
+ vec3 arriving = uBackLightColor * behind + uAmbientTransmission;
2591
+ return arriving * uTranslucency * mix(0.25, 1.0, thickness) * inkFilter;
2592
+ }
2593
+ `
2594
+ );
2595
+ function translucencyValues(translucency, lighting) {
2596
+ const preset = getLightingPreset(lighting);
2597
+ const [x, y, z27] = preset.key.position;
2598
+ const direction = new THREE4.Vector3(x, y, z27);
2599
+ if (direction.lengthSq() < 1e-12) direction.set(0, 1, 0);
2600
+ direction.normalize();
2601
+ const color = new THREE4.Color(preset.key.color).multiplyScalar(preset.key.intensity * TRANSMISSION_GAIN);
2602
+ return { translucency, direction, color, ambient: preset.ambient * TRANSMISSION_GAIN };
2603
+ }
2604
+ function translucencyUniforms(translucency, lighting) {
2605
+ const values = translucencyValues(translucency, lighting);
2606
+ return {
2607
+ uTranslucency: { value: values.translucency },
2608
+ uBackLightDir: { value: values.direction },
2609
+ uBackLightColor: { value: values.color },
2610
+ uAmbientTransmission: { value: values.ambient }
2611
+ };
2612
+ }
2613
+
2614
+ // src/surface/compose.ts
2135
2615
  var VERTEX = (
2136
2616
  /* glsl */
2137
2617
  `
2138
2618
  varying vec2 vPaperUv;
2619
+ ${TRANSLUCENCY_VARYINGS}
2139
2620
  void main() {
2140
2621
  vPaperUv = uv;
2622
+ ${translucencyVertexChunk({ model: "modelMatrix", position: "position", normal: "normal" })}
2141
2623
  }
2142
2624
  `
2143
2625
  );
@@ -2174,7 +2656,7 @@ float plFbm(vec2 p) {
2174
2656
  }
2175
2657
  `
2176
2658
  );
2177
- var edgeFlags = (edges) => new THREE4.Vector4(
2659
+ var edgeFlags = (edges) => new THREE5.Vector4(
2178
2660
  edges.includes("top") ? 1 : 0,
2179
2661
  edges.includes("right") ? 1 : 0,
2180
2662
  edges.includes("bottom") ? 1 : 0,
@@ -2309,7 +2791,7 @@ void plAging(inout vec4 color) {
2309
2791
  }
2310
2792
  `
2311
2793
  );
2312
- function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false, hasBackMap: false }, sheet2 = { width: 1, height: 1.4 }) {
2794
+ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false, hasBackMap: false }, sheet2 = { width: 1, height: 1.4 }, lighting = "studio") {
2313
2795
  const grain = surface.grain ?? stock.defaultSurface.grain;
2314
2796
  const aging = surface.aging ?? stock.defaultSurface.aging;
2315
2797
  const deckle = surface.deckle;
@@ -2325,9 +2807,12 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
2325
2807
  uBackDarken: {
2326
2808
  value: stock.adhesive ? 1 : 1 - Math.min(0.45, 0.12 + thickness * 0.9) * stock.opacity
2327
2809
  },
2328
- uStockColor: { value: new THREE4.Color(stock.color) },
2810
+ uStockColor: { value: new THREE5.Color(stock.color) },
2329
2811
  uOpacity: { value: stock.opacity },
2330
- uShowThrough: { value: showThrough }
2812
+ uShowThrough: { value: showThrough },
2813
+ // Always compiled in: the shader early-outs at zero translucency, which
2814
+ // is cheaper than carrying a second program structure for it.
2815
+ ...translucencyUniforms(surface.translucency ?? stock.translucency, lighting)
2331
2816
  };
2332
2817
  if (maps.hasFrontMap) uniforms.uFrontMap = { value: null };
2333
2818
  if (maps.hasBackMap) uniforms.uBackMap = { value: null };
@@ -2349,13 +2834,13 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
2349
2834
  calls.push("plPerforation(csm_DiffuseColor);");
2350
2835
  uniforms.uPerfEdges = { value: edgeFlags(edges) };
2351
2836
  uniforms.uPerfTorn = {
2352
- value: new THREE4.Vector4(
2837
+ value: new THREE5.Vector4(
2353
2838
  ...paperEdges.map((e) => edges.includes(e) && perforation.state[e] === "torn" ? 1 : 0)
2354
2839
  )
2355
2840
  };
2356
2841
  uniforms.uPerfRadius = { value: perforation.holeRadius };
2357
2842
  uniforms.uPerfSpacing = { value: perforation.spacing };
2358
- uniforms.uSheetSize = { value: new THREE4.Vector2(sheet2.width, sheet2.height) };
2843
+ uniforms.uSheetSize = { value: new THREE5.Vector2(sheet2.width, sheet2.height) };
2359
2844
  }
2360
2845
  if (creases) {
2361
2846
  chunks.push(CREASE_CHUNK);
@@ -2381,6 +2866,7 @@ uniform float uOpacity;
2381
2866
  uniform float uShowThrough;
2382
2867
  ${maps.hasFrontMap ? "uniform sampler2D uFrontMap;" : ""}
2383
2868
  ${maps.hasBackMap && !stock.adhesive ? "uniform sampler2D uBackMap;" : ""}
2869
+ ${TRANSLUCENCY_FRAGMENT}
2384
2870
  ${chunks.join("\n")}
2385
2871
  void main() {
2386
2872
  vec3 front = ${frontExpr};
@@ -2393,6 +2879,8 @@ void main() {
2393
2879
  ${calls.join("\n ")}
2394
2880
  if (!gl_FrontFacing) csm_DiffuseColor.rgb *= uBackDarken;
2395
2881
  ${stock.adhesive ? "// Adhesive underside: higher specular than the printed face.\n if (!gl_FrontFacing) csm_Roughness = 0.18;" : ""}
2882
+ // What the key light pushes through the sheet, filtered by the ink on it.
2883
+ csm_Emissive = plTransmission(front);
2396
2884
  }
2397
2885
  `
2398
2886
  );
@@ -2425,7 +2913,8 @@ function PaperMaterial({
2425
2913
  backTexture,
2426
2914
  surface,
2427
2915
  thickness,
2428
- sheet: sheet2
2916
+ sheet: sheet2,
2917
+ lighting = "studio"
2429
2918
  }) {
2430
2919
  const composed = composeSurface(
2431
2920
  surface,
@@ -2435,13 +2924,14 @@ function PaperMaterial({
2435
2924
  hasFrontMap: Boolean(texture),
2436
2925
  hasBackMap: Boolean(backTexture)
2437
2926
  },
2438
- sheet2
2927
+ sheet2,
2928
+ lighting
2439
2929
  );
2440
2930
  const bound = (0, import_react2.useMemo)(() => composed.uniforms, [composed.structureKey]);
2441
2931
  (0, import_react2.useEffect)(() => {
2442
2932
  for (const [key, uniform] of Object.entries(composed.uniforms)) {
2443
2933
  if (!bound[key] || key === "uFrontMap" || key === "uBackMap") continue;
2444
- if (bound[key].value instanceof THREE5.Color && uniform.value instanceof THREE5.Color) {
2934
+ if (bound[key].value instanceof THREE6.Color && uniform.value instanceof THREE6.Color) {
2445
2935
  ;
2446
2936
  bound[key].value.copy(uniform.value);
2447
2937
  } else {
@@ -2456,7 +2946,7 @@ function PaperMaterial({
2456
2946
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2457
2947
  import_three_custom_shader_material.default,
2458
2948
  {
2459
- baseMaterial: THREE5.MeshStandardMaterial,
2949
+ baseMaterial: THREE6.MeshStandardMaterial,
2460
2950
  vertexShader: composed.vertexShader,
2461
2951
  fragmentShader: composed.fragmentShader,
2462
2952
  uniforms: bound,
@@ -2466,7 +2956,7 @@ function PaperMaterial({
2466
2956
  transparent: stock.opacity < 1,
2467
2957
  opacity: stock.opacity,
2468
2958
  alphaTest: composed.alphaTest,
2469
- side: THREE5.DoubleSide
2959
+ side: THREE6.DoubleSide
2470
2960
  },
2471
2961
  composed.structureKey
2472
2962
  );
@@ -2890,6 +3380,8 @@ function resolveConfigKey(props) {
2890
3380
  props.content ?? null,
2891
3381
  props.behavior ?? null,
2892
3382
  props.deformers ?? null,
3383
+ props.surface ?? null,
3384
+ props.scene ?? null,
2893
3385
  props.physics ?? null,
2894
3386
  props.onTwos ?? null
2895
3387
  ]);
@@ -2902,17 +3394,19 @@ function resolveConfig(props) {
2902
3394
  if (props.content) overrides.content = props.content;
2903
3395
  if (props.behavior) overrides.behavior = props.behavior;
2904
3396
  if (props.deformers) overrides.deformers = props.deformers;
3397
+ if (props.surface) overrides.surface = { ...base.surface, ...props.surface };
3398
+ if (props.scene) overrides.scene = { ...base.scene, ...props.scene };
2905
3399
  if (props.physics) overrides.physics = props.physics;
2906
3400
  if (props.onTwos !== void 0) overrides.onTwos = props.onTwos;
2907
3401
  return paperConfigSchema.parse(mergeConfig(base, overrides));
2908
3402
  }
2909
3403
  var CLOTH_MAX_SEGMENTS = 28;
2910
- var dragPlane = new THREE6.Plane();
2911
- var dragPoint = new THREE6.Vector3();
2912
- var planeNormal = new THREE6.Vector3();
2913
- var anchorScratch = new THREE6.Vector3();
2914
- var worldScratch = new THREE6.Vector3();
2915
- var quatScratch = new THREE6.Quaternion();
3404
+ var dragPlane = new THREE7.Plane();
3405
+ var dragPoint = new THREE7.Vector3();
3406
+ var planeNormal = new THREE7.Vector3();
3407
+ var anchorScratch = new THREE7.Vector3();
3408
+ var worldScratch = new THREE7.Vector3();
3409
+ var quatScratch = new THREE7.Quaternion();
2916
3410
  var PaperMesh = (0, import_react5.forwardRef)(function PaperMesh2(props, ref) {
2917
3411
  const resolved = (0, import_react5.useMemo)(() => resolveConfig(props), [resolveConfigKey(props)]);
2918
3412
  const reduced = usePrefersReducedMotion(props.reducedMotion);
@@ -2957,7 +3451,7 @@ var PaperMesh = (0, import_react5.forwardRef)(function PaperMesh2(props, ref) {
2957
3451
  if (!isCloth) return createSheetGeometry(config.sheet, minSegments);
2958
3452
  const [sx, sy] = resolveSegments(config.sheet, 2);
2959
3453
  const capped = Math.min(Math.max(sx, sy), CLOTH_MAX_SEGMENTS);
2960
- return new THREE6.PlaneGeometry(config.sheet.width, config.sheet.height, capped, capped);
3454
+ return new THREE7.PlaneGeometry(config.sheet.width, config.sheet.height, capped, capped);
2961
3455
  }, [sheetKey, minSegments, isCloth]);
2962
3456
  (0, import_react5.useEffect)(() => () => geometry.dispose(), [geometry]);
2963
3457
  const basePositions = (0, import_react5.useMemo)(
@@ -3148,7 +3642,7 @@ var PaperMesh = (0, import_react5.forwardRef)(function PaperMesh2(props, ref) {
3148
3642
  const p = overridesRef.current[behavior.progressParam];
3149
3643
  if (typeof p === "number") props.onProgress?.(p);
3150
3644
  };
3151
- const grabAnchor = (0, import_react5.useRef)(new THREE6.Vector3());
3645
+ const grabAnchor = (0, import_react5.useRef)(new THREE7.Vector3());
3152
3646
  const clothDown = (e) => {
3153
3647
  if (!isCloth || !sim || !props.interactive || !groupRef.current) return;
3154
3648
  e.stopPropagation();
@@ -3204,7 +3698,8 @@ var PaperMesh = (0, import_react5.forwardRef)(function PaperMesh2(props, ref) {
3204
3698
  backTexture,
3205
3699
  surface: config.surface,
3206
3700
  thickness: config.sheet.thickness,
3207
- sheet: config.sheet
3701
+ sheet: config.sheet,
3702
+ lighting: config.scene.lighting
3208
3703
  }
3209
3704
  )
3210
3705
  }
@@ -3245,7 +3740,7 @@ function buildStack(config, overrides, behavior, t = 0) {
3245
3740
  const idleStack = idle?.stack?.() ?? [];
3246
3741
  let shapeStack = [];
3247
3742
  if (config.deformers) {
3248
- shapeStack = config.deformers;
3743
+ shapeStack = resolveDeformerStack(config.deformers);
3249
3744
  } else if (config.behavior) {
3250
3745
  const b = behavior ?? getBehavior(config.behavior.type);
3251
3746
  let options = { ...config.behavior, ...overrides };
@@ -3257,71 +3752,10 @@ function buildStack(config, overrides, behavior, t = 0) {
3257
3752
  }
3258
3753
 
3259
3754
  // src/scene/PaperLighting.tsx
3260
- var THREE7 = __toESM(require("three"), 1);
3755
+ var THREE8 = __toESM(require("three"), 1);
3261
3756
  var import_react6 = require("react");
3262
3757
  var import_fiber2 = require("@react-three/fiber");
3263
3758
  var import_drei = require("@react-three/drei");
3264
-
3265
- // src/scene/lighting.ts
3266
- var lightingPresets = {
3267
- studio: {
3268
- id: "studio",
3269
- label: "Studio",
3270
- ambient: 0.65,
3271
- key: { color: "#ffffff", intensity: 1.6, position: [2.5, 4, 3] },
3272
- contactShadowOpacity: 0.3,
3273
- contactShadowBlur: 2.4,
3274
- exposure: 1,
3275
- shadow: { mapSize: 1024, radius: 4 }
3276
- },
3277
- window: {
3278
- id: "window",
3279
- label: "Window",
3280
- ambient: 0.5,
3281
- key: { color: "#ffe3c0", intensity: 1.9, position: [3, 2.6, 2.6] },
3282
- contactShadowOpacity: 0.35,
3283
- contactShadowBlur: 2.6,
3284
- exposure: 1,
3285
- shadow: { mapSize: 1024, radius: 5 },
3286
- gobo: { kind: "blinds", drift: 4e-3, angle: 0.62 }
3287
- },
3288
- leaves: {
3289
- id: "leaves",
3290
- label: "Leaves",
3291
- ambient: 0.45,
3292
- key: { color: "#fff2d8", intensity: 2, position: [2.2, 3.6, 2.4] },
3293
- contactShadowOpacity: 0.4,
3294
- contactShadowBlur: 2.8,
3295
- exposure: 1,
3296
- shadow: { mapSize: 1024, radius: 6 },
3297
- gobo: { kind: "leaves", drift: 0.012, angle: 0.7 }
3298
- },
3299
- goldenhour: {
3300
- id: "goldenhour",
3301
- label: "Golden hour",
3302
- ambient: 0.32,
3303
- key: { color: "#ffb066", intensity: 2.4, position: [4, 0.9, 2.2] },
3304
- contactShadowOpacity: 0.45,
3305
- contactShadowBlur: 3.2,
3306
- exposure: 1.15,
3307
- shadow: { mapSize: 1024, radius: 7 }
3308
- },
3309
- noir: {
3310
- id: "noir",
3311
- label: "Noir",
3312
- ambient: 0.07,
3313
- key: { color: "#ffffff", intensity: 2.6, position: [2, 3, 1.6] },
3314
- contactShadowOpacity: 0.7,
3315
- contactShadowBlur: 1.1,
3316
- exposure: 1.05,
3317
- shadow: { mapSize: 2048, radius: 1 }
3318
- }
3319
- };
3320
- function getLightingPreset(name) {
3321
- return lightingPresets[name];
3322
- }
3323
-
3324
- // src/scene/PaperLighting.tsx
3325
3759
  var import_jsx_runtime4 = require("react/jsx-runtime");
3326
3760
  function mulberry32(seed) {
3327
3761
  let a = seed;
@@ -3378,19 +3812,24 @@ function makeGoboTexture(kind) {
3378
3812
  }
3379
3813
  }
3380
3814
  }
3381
- const texture = new THREE7.CanvasTexture(canvas);
3382
- texture.wrapS = texture.wrapT = THREE7.RepeatWrapping;
3815
+ const texture = new THREE8.CanvasTexture(canvas);
3816
+ texture.wrapS = texture.wrapT = THREE8.RepeatWrapping;
3383
3817
  return texture;
3384
3818
  }
3385
3819
  function PaperLighting({
3386
3820
  preset = "studio",
3387
3821
  floor = -1.2,
3388
3822
  scale = 10,
3389
- reducedMotion
3823
+ reducedMotion,
3824
+ shadowMapSize,
3825
+ contactShadow = true
3390
3826
  }) {
3391
3827
  const p = getLightingPreset(preset);
3828
+ const mapSize = shadowMapSize ?? p.shadow.mapSize;
3829
+ const castShadow = mapSize > 0;
3392
3830
  const reduced = usePrefersReducedMotion(reducedMotion);
3393
3831
  const gl = (0, import_fiber2.useThree)((s) => s.gl);
3832
+ const scene = (0, import_fiber2.useThree)((s) => s.scene);
3394
3833
  (0, import_react6.useEffect)(() => {
3395
3834
  const previous = gl.toneMappingExposure;
3396
3835
  gl.toneMappingExposure = p.exposure;
@@ -3398,11 +3837,15 @@ function PaperLighting({
3398
3837
  gl.toneMappingExposure = previous;
3399
3838
  };
3400
3839
  }, [gl, p.exposure]);
3401
- const goboMap = (0, import_react6.useMemo)(
3402
- () => p.gobo ? makeGoboTexture(p.gobo.kind) : null,
3403
- // eslint-disable-next-line react-hooks/exhaustive-deps
3404
- [p.gobo?.kind]
3405
- );
3840
+ (0, import_react6.useEffect)(() => {
3841
+ if (!p.fog) return;
3842
+ const previous = scene.fog;
3843
+ scene.fog = new THREE8.Fog(p.fog.color, p.fog.near, p.fog.far);
3844
+ return () => {
3845
+ scene.fog = previous;
3846
+ };
3847
+ }, [scene, p.fog]);
3848
+ const goboMap = (0, import_react6.useMemo)(() => p.gobo ? makeGoboTexture(p.gobo.kind) : null, [p.gobo?.kind]);
3406
3849
  (0, import_react6.useEffect)(() => () => goboMap?.dispose(), [goboMap]);
3407
3850
  const driftRef = (0, import_react6.useRef)(0);
3408
3851
  (0, import_fiber2.useFrame)((_, delta) => {
@@ -3421,9 +3864,9 @@ function PaperLighting({
3421
3864
  angle: p.gobo.angle,
3422
3865
  penumbra: 0.5,
3423
3866
  decay: 0,
3424
- castShadow: true,
3867
+ castShadow,
3425
3868
  map: goboMap,
3426
- "shadow-mapSize": [p.shadow.mapSize, p.shadow.mapSize],
3869
+ "shadow-mapSize": [mapSize || 1, mapSize || 1],
3427
3870
  "shadow-radius": p.shadow.radius,
3428
3871
  "shadow-normalBias": 0.05
3429
3872
  }
@@ -3433,13 +3876,13 @@ function PaperLighting({
3433
3876
  position: p.key.position,
3434
3877
  color: p.key.color,
3435
3878
  intensity: p.key.intensity,
3436
- castShadow: true,
3437
- "shadow-mapSize": [p.shadow.mapSize, p.shadow.mapSize],
3879
+ castShadow,
3880
+ "shadow-mapSize": [mapSize || 1, mapSize || 1],
3438
3881
  "shadow-radius": p.shadow.radius,
3439
3882
  "shadow-normalBias": 0.05
3440
3883
  }
3441
3884
  ),
3442
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3885
+ contactShadow && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3443
3886
  import_drei.ContactShadows,
3444
3887
  {
3445
3888
  position: [0, floor, 0],
@@ -3476,12 +3919,13 @@ var Paper = (0, import_react7.forwardRef)(function Paper2({ children, className,
3476
3919
  });
3477
3920
 
3478
3921
  // src/PaperField.tsx
3922
+ var THREE14 = __toESM(require("three"), 1);
3479
3923
  var import_gsap5 = require("gsap");
3480
3924
  var import_fiber6 = require("@react-three/fiber");
3481
3925
  var import_react14 = require("react");
3482
3926
 
3483
3927
  // src/field/dropZones.tsx
3484
- var THREE8 = __toESM(require("three"), 1);
3928
+ var THREE9 = __toESM(require("three"), 1);
3485
3929
  var import_react8 = require("react");
3486
3930
  var import_jsx_runtime6 = require("react/jsx-runtime");
3487
3931
  var DropZoneRegistry = class {
@@ -3552,8 +3996,8 @@ function DropZoneVisual({ registry: registry4, config }) {
3552
3996
  const style = config.highlight ?? "glow";
3553
3997
  const [w, h] = config.bounds.size;
3554
3998
  const edges = (0, import_react8.useMemo)(() => {
3555
- const plane = new THREE8.PlaneGeometry(w, h);
3556
- const geo = new THREE8.EdgesGeometry(plane);
3999
+ const plane = new THREE9.PlaneGeometry(w, h);
4000
+ const geo = new THREE9.EdgesGeometry(plane);
3557
4001
  plane.dispose();
3558
4002
  return geo;
3559
4003
  }, [w, h]);
@@ -3576,18 +4020,18 @@ function DropZoneVisual({ registry: registry4, config }) {
3576
4020
  }
3577
4021
 
3578
4022
  // src/field/sheetGrid.ts
3579
- var import_zod16 = require("zod");
3580
- var sheetLayoutSchema = import_zod16.z.object({
3581
- rows: import_zod16.z.number().int().min(1).max(12).default(2),
3582
- columns: import_zod16.z.number().int().min(1).max(12).default(5),
4023
+ var import_zod20 = require("zod");
4024
+ var sheetLayoutSchema = import_zod20.z.object({
4025
+ rows: import_zod20.z.number().int().min(1).max(12).default(2),
4026
+ columns: import_zod20.z.number().int().min(1).max(12).default(5),
3583
4027
  /** World-units gap between slots. Stamps are printed in register — no jitter. */
3584
- gutter: import_zod16.z.number().min(0).max(1).default(0.08),
4028
+ gutter: import_zod20.z.number().min(0).max(1).default(0.08),
3585
4029
  /** Slot footprint in world units (the paper preset should match). */
3586
- cellWidth: import_zod16.z.number().min(0.1).max(4).default(0.72),
3587
- cellHeight: import_zod16.z.number().min(0.1).max(4).default(0.86),
4030
+ cellWidth: import_zod20.z.number().min(0.1).max(4).default(0.72),
4031
+ cellHeight: import_zod20.z.number().min(0.1).max(4).default(0.86),
3588
4032
  /** Render the shared backing sheet behind the grid. */
3589
- backing: import_zod16.z.boolean().default(true),
3590
- backingMargin: import_zod16.z.number().min(0).max(1).default(0.12)
4033
+ backing: import_zod20.z.boolean().default(true),
4034
+ backingMargin: import_zod20.z.number().min(0).max(1).default(0.12)
3591
4035
  });
3592
4036
  var SHEET_LIFT = 0.012;
3593
4037
  function withSheetCellFromPaper(parsed, rawOptions, paperDims) {
@@ -3679,18 +4123,18 @@ function resolveFieldSlotConfig(slot, fallback, index, layoutId, layoutOptions)
3679
4123
  }
3680
4124
 
3681
4125
  // src/field/fieldGroup.tsx
3682
- var THREE10 = __toESM(require("three"), 1);
4126
+ var THREE11 = __toESM(require("three"), 1);
3683
4127
  var import_gsap3 = require("gsap");
3684
4128
  var import_fiber4 = require("@react-three/fiber");
3685
4129
  var import_react10 = require("react");
3686
4130
  var import_three_custom_shader_material2 = __toESM(require("three-custom-shader-material"), 1);
3687
4131
 
3688
4132
  // src/content/atlas.ts
3689
- var THREE9 = __toESM(require("three"), 1);
4133
+ var THREE10 = __toESM(require("three"), 1);
3690
4134
  var import_react9 = require("react");
3691
4135
  var MAX_ATLAS = 4096;
3692
- function atlasGrid(count) {
3693
- const cols = Math.max(1, Math.ceil(Math.sqrt(count)));
4136
+ function atlasGrid(count, aspect = 1) {
4137
+ const cols = Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * Math.max(aspect, 0.01)))));
3694
4138
  return { cols, rows: Math.max(1, Math.ceil(count / cols)) };
3695
4139
  }
3696
4140
  function useContentAtlas(contents, sheet2, stock) {
@@ -3698,18 +4142,22 @@ function useContentAtlas(contents, sheet2, stock) {
3698
4142
  const key = JSON.stringify({ contents, w: sheet2.width, h: sheet2.height, stock: stock.id });
3699
4143
  (0, import_react9.useEffect)(() => {
3700
4144
  let disposed = false;
3701
- const { cols, rows } = atlasGrid(contents.length);
3702
4145
  const aspect = sheet2.height / sheet2.width;
3703
- const tileW = Math.min(1024, Math.floor(MAX_ATLAS / cols));
3704
- const tileH = Math.min(Math.round(tileW * aspect), Math.floor(MAX_ATLAS / rows));
4146
+ const { cols, rows } = atlasGrid(contents.length, aspect);
4147
+ let tileW = Math.min(1024, Math.floor(MAX_ATLAS / cols));
4148
+ let tileH = Math.round(tileW * aspect);
4149
+ if (tileH * rows > MAX_ATLAS) {
4150
+ tileH = Math.floor(MAX_ATLAS / rows);
4151
+ tileW = Math.max(1, Math.round(tileH / aspect));
4152
+ }
3705
4153
  const canvas = document.createElement("canvas");
3706
4154
  canvas.width = tileW * cols;
3707
4155
  canvas.height = tileH * rows;
3708
4156
  const ctx = canvas.getContext("2d");
3709
4157
  ctx.fillStyle = stock.color;
3710
4158
  ctx.fillRect(0, 0, canvas.width, canvas.height);
3711
- const texture = new THREE9.CanvasTexture(canvas);
3712
- texture.colorSpace = THREE9.SRGBColorSpace;
4159
+ const texture = new THREE10.CanvasTexture(canvas);
4160
+ texture.colorSpace = THREE10.SRGBColorSpace;
3713
4161
  texture.anisotropy = 4;
3714
4162
  setAtlas({ texture, cols, rows });
3715
4163
  const drawTile = (index, tile) => {
@@ -3757,7 +4205,7 @@ function stackUniformValues(stack, sheet2) {
3757
4205
  return uniforms;
3758
4206
  }
3759
4207
  function buildDisplacementGLSL(stack, sheet2) {
3760
- const decls = ["uniform vec2 uSheet;"];
4208
+ const decls = ["uniform vec2 uSheet;", "float plBias = 1.0;"];
3761
4209
  const functions = [];
3762
4210
  const calls = [];
3763
4211
  const uniforms = { uSheet: [sheet2.width, sheet2.height] };
@@ -3776,15 +4224,24 @@ function buildDisplacementGLSL(stack, sheet2) {
3776
4224
  decls.push(`uniform ${glslType(value)} ${ns}${key};`);
3777
4225
  uniforms[ns + key] = value;
3778
4226
  }
4227
+ const strength = deformer.glsl.strength;
3779
4228
  functions.push(
3780
- deformer.glsl.chunk.replaceAll("FN", fn).replace(/U_(\w+)/g, (_, name) => ns + name)
4229
+ deformer.glsl.chunk.replaceAll("FN", fn).replace(
4230
+ /U_(\w+)/g,
4231
+ (_, name) => (
4232
+ // The strength uniform reads through the per-instance bias, so one
4233
+ // instanced draw call can bend every sheet by a different amount.
4234
+ name === strength ? `(${ns}${name} * plBias)` : ns + name
4235
+ )
4236
+ )
3781
4237
  );
3782
4238
  calls.push(`${fn}(q, uv, t);`);
3783
4239
  });
3784
4240
  const displaceSrc = (
3785
4241
  /* glsl */
3786
4242
  `
3787
- vec3 plDisplace(vec3 p, vec2 uv, float t) {
4243
+ vec3 plDisplace(vec3 p, vec2 uv, float t, float bias) {
4244
+ plBias = bias;
3788
4245
  vec3 q = p;
3789
4246
  ${calls.join("\n ")}
3790
4247
  return q;
@@ -3801,19 +4258,22 @@ function buildFieldVertexShader(composed) {
3801
4258
  uniform float uPlTime;
3802
4259
  attribute float aPhase;
3803
4260
  attribute float aAtlas;
4261
+ attribute float aBias;
3804
4262
  varying vec2 vPaperUv;
3805
4263
  varying float vAtlas;
4264
+ ${TRANSLUCENCY_VARYINGS}
3806
4265
  ${composed.functionsSrc}
3807
4266
  ${composed.displaceSrc}
3808
4267
  void main() {
3809
4268
  float t = uPlTime + aPhase;
3810
- vec3 p = plDisplace(position, uv, t);
4269
+ vec3 p = plDisplace(position, uv, t, aBias);
3811
4270
  vec2 step = uSheet * 0.01;
3812
- vec3 px = plDisplace(position + vec3(step.x, 0.0, 0.0), uv + vec2(0.01, 0.0), t);
3813
- vec3 py = plDisplace(position + vec3(0.0, step.y, 0.0), uv + vec2(0.0, 0.01), t);
4271
+ vec3 px = plDisplace(position + vec3(step.x, 0.0, 0.0), uv + vec2(0.01, 0.0), t, aBias);
4272
+ vec3 py = plDisplace(position + vec3(0.0, step.y, 0.0), uv + vec2(0.0, 0.01), t, aBias);
3814
4273
  vec3 n = cross(px - p, py - p);
3815
4274
  csm_Normal = length(n) > 1e-12 ? normalize(n) : vec3(0.0, 0.0, 1.0);
3816
4275
  csm_Position = p;
4276
+ ${translucencyVertexChunk({ model: "modelMatrix * instanceMatrix", position: "p", normal: "csm_Normal" })}
3817
4277
  vPaperUv = uv;
3818
4278
  vAtlas = aAtlas;
3819
4279
  }
@@ -3831,6 +4291,7 @@ uniform vec3 uStockColor;
3831
4291
  uniform float uShowThrough;
3832
4292
  varying vec2 vPaperUv;
3833
4293
  varying float vAtlas;
4294
+ ${TRANSLUCENCY_FRAGMENT}
3834
4295
  void main() {
3835
4296
  float col = mod(vAtlas, uAtlasGrid.x);
3836
4297
  float row = floor(vAtlas / uAtlasGrid.x);
@@ -3842,6 +4303,10 @@ void main() {
3842
4303
  csm_DiffuseColor = vec4(uStockColor * mix(vec3(1.0), front.rgb, uShowThrough), 1.0);
3843
4304
  csm_DiffuseColor.rgb *= uBackDarken;
3844
4305
  }
4306
+ // Light coming through the sheet, filtered by what is printed on it. Same
4307
+ // ink either side \u2014 the light passes through the same fibres regardless of
4308
+ // which face happens to be turned toward the camera.
4309
+ csm_Emissive = plTransmission(front.rgb);
3845
4310
  }
3846
4311
  `
3847
4312
  );
@@ -3851,16 +4316,149 @@ function cap(s) {
3851
4316
  }
3852
4317
 
3853
4318
  // src/field/layouts/index.ts
3854
- var import_zod17 = require("zod");
3855
- var TAU2 = Math.PI * 2;
3856
- function jitter(seed, i) {
4319
+ var import_zod22 = require("zod");
4320
+
4321
+ // src/stage/path.ts
4322
+ var import_zod21 = require("zod");
4323
+ var walkPathSchema = import_zod21.z.object({
4324
+ /**
4325
+ * Control points on the ground plane, [x, z]. The default walks away from
4326
+ * the camera down -Z — the shot every reference image is composed on.
4327
+ */
4328
+ points: import_zod21.z.array(import_zod21.z.tuple([import_zod21.z.number(), import_zod21.z.number()])).min(2).default([
4329
+ [0, 9],
4330
+ [0, -9]
4331
+ ]),
4332
+ /** Join the last point back to the first: an endless walk, and the only form `phase` can slide. */
4333
+ closed: import_zod21.z.boolean().default(false)
4334
+ });
4335
+ var SAMPLES_PER_SEGMENT = 24;
4336
+ var EPSILON = 1e-6;
4337
+ var ALPHA = 0.5;
4338
+ function knot(a, b, t) {
4339
+ return t + Math.max(Math.hypot(b[0] - a[0], b[1] - a[1]), EPSILON) ** ALPHA;
4340
+ }
4341
+ function lerpKnot(a, b, ta, tb, t) {
4342
+ const span = tb - ta;
4343
+ const k = span === 0 ? 0 : (t - ta) / span;
4344
+ return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k];
4345
+ }
4346
+ function segmentPoint(p0, p1, p2, p3, u) {
4347
+ const t0 = 0;
4348
+ const t1 = knot(p0, p1, t0);
4349
+ const t2 = knot(p1, p2, t1);
4350
+ const t3 = knot(p2, p3, t2);
4351
+ const t = t1 + (t2 - t1) * u;
4352
+ const a1 = lerpKnot(p0, p1, t0, t1, t);
4353
+ const a2 = lerpKnot(p1, p2, t1, t2, t);
4354
+ const a3 = lerpKnot(p2, p3, t2, t3, t);
4355
+ const b1 = lerpKnot(a1, a2, t0, t2, t);
4356
+ const b2 = lerpKnot(a2, a3, t1, t3, t);
4357
+ return lerpKnot(b1, b2, t1, t2, t);
4358
+ }
4359
+ function reflect(end, inner) {
4360
+ return [2 * end[0] - inner[0], 2 * end[1] - inner[1]];
4361
+ }
4362
+ function createWalkPath(options) {
4363
+ const pts = options.points;
4364
+ const n = pts.length;
4365
+ const closed = options.closed && n > 2;
4366
+ const segments = closed ? n : n - 1;
4367
+ const samples = [];
4368
+ const cumulative = [];
4369
+ let total = 0;
4370
+ for (let seg = 0; seg < segments; seg++) {
4371
+ const p1 = pts[seg];
4372
+ const p2 = pts[(seg + 1) % n];
4373
+ const p0 = closed ? pts[(seg - 1 + n) % n] : seg > 0 ? pts[seg - 1] : reflect(pts[0], pts[1]);
4374
+ const p3 = closed ? pts[(seg + 2) % n] : seg + 2 < n ? pts[seg + 2] : reflect(pts[n - 1], pts[n - 2]);
4375
+ const last = seg === segments - 1 && !closed ? SAMPLES_PER_SEGMENT : SAMPLES_PER_SEGMENT - 1;
4376
+ for (let k = 0; k <= last; k++) {
4377
+ const point = segmentPoint(p0, p1, p2, p3, k / SAMPLES_PER_SEGMENT);
4378
+ const previous = samples[samples.length - 1];
4379
+ if (previous) total += Math.hypot(point[0] - previous[0], point[1] - previous[1]);
4380
+ samples.push(point);
4381
+ cumulative.push(total);
4382
+ }
4383
+ }
4384
+ if (closed) {
4385
+ const first = samples[0];
4386
+ const previous = samples[samples.length - 1];
4387
+ total += Math.hypot(first[0] - previous[0], first[1] - previous[1]);
4388
+ samples.push([first[0], first[1]]);
4389
+ cumulative.push(total);
4390
+ }
4391
+ const length = total;
4392
+ function normalize(s) {
4393
+ if (!Number.isFinite(s)) return 0;
4394
+ if (!closed) return Math.min(Math.max(s, 0), 1);
4395
+ const wrapped = s - Math.floor(s);
4396
+ return wrapped;
4397
+ }
4398
+ function pointAt(s) {
4399
+ const target = normalize(s) * length;
4400
+ if (length === 0) return [samples[0][0], samples[0][1]];
4401
+ let low = 0;
4402
+ let high = cumulative.length - 1;
4403
+ while (low < high) {
4404
+ const mid = low + high >> 1;
4405
+ if (cumulative[mid] < target) low = mid + 1;
4406
+ else high = mid;
4407
+ }
4408
+ const i = Math.max(low, 1);
4409
+ const before = cumulative[i - 1];
4410
+ const span = cumulative[i] - before;
4411
+ const k = span <= 0 ? 0 : (target - before) / span;
4412
+ const a = samples[i - 1];
4413
+ const b = samples[i];
4414
+ return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k];
4415
+ }
4416
+ function tangentAt(s) {
4417
+ const ds = length > 0 ? Math.min(0.01, 0.5 / length) : 0.01;
4418
+ const here = normalize(s);
4419
+ const a = pointAt(closed ? here - ds : Math.max(here - ds, 0));
4420
+ const b = pointAt(closed ? here + ds : Math.min(here + ds, 1));
4421
+ const dx = b[0] - a[0];
4422
+ const dz = b[1] - a[1];
4423
+ const len = Math.hypot(dx, dz);
4424
+ return len < EPSILON ? [0, -1] : [dx / len, dz / len];
4425
+ }
4426
+ function normalAt(s) {
4427
+ const [tx, tz] = tangentAt(s);
4428
+ return [tz, -tx];
4429
+ }
4430
+ return { length, closed, pointAt, tangentAt, normalAt };
4431
+ }
4432
+ var cache = /* @__PURE__ */ new Map();
4433
+ var CACHE_LIMIT = 32;
4434
+ function getWalkPath(options) {
4435
+ const key = `${options.closed ? "c" : "o"}|${options.points.map((p) => `${p[0]},${p[1]}`).join(";")}`;
4436
+ const hit = cache.get(key);
4437
+ if (hit) return hit;
4438
+ const path = createWalkPath(options);
4439
+ if (cache.size >= CACHE_LIMIT) {
4440
+ const oldest = cache.keys().next().value;
4441
+ if (oldest !== void 0) cache.delete(oldest);
4442
+ }
4443
+ cache.set(key, path);
4444
+ return path;
4445
+ }
4446
+
4447
+ // src/field/layouts/index.ts
4448
+ var DEFAULT_SHEET = { width: 1, height: 1.4 };
4449
+ var TAU3 = Math.PI * 2;
4450
+ var DEG6 = Math.PI / 180;
4451
+ function jitter2(seed, i) {
3857
4452
  let h = Math.imul(seed * 1e3 + i + 1 ^ 2654435769, 2654435761);
3858
4453
  h = Math.imul(h ^ h >>> 13, 3266489917);
3859
4454
  return ((h ^ h >>> 16) >>> 0) / 4294967295 * 2 - 1;
3860
4455
  }
3861
- var ringSchema = import_zod17.z.object({
3862
- radius: import_zod17.z.number().min(0.5).max(12).default(2.6),
3863
- tiltDeg: import_zod17.z.number().min(-45).max(45).default(8)
4456
+ function ramp(i, n) {
4457
+ return n > 1 ? i / (n - 1) : 1;
4458
+ }
4459
+ var ringSchema = import_zod22.z.object({
4460
+ radius: import_zod22.z.number().min(0.5).max(12).default(2.6),
4461
+ tiltDeg: import_zod22.z.number().min(-45).max(45).default(8)
3864
4462
  });
3865
4463
  var ring = {
3866
4464
  id: "ring",
@@ -3868,7 +4466,7 @@ var ring = {
3868
4466
  defaults: ringSchema.parse({}),
3869
4467
  optionsSchema: ringSchema,
3870
4468
  pose(i, n, o, phase) {
3871
- const theta = (i / n + phase) * TAU2;
4469
+ const theta = (i / n + phase) * TAU3;
3872
4470
  return {
3873
4471
  position: [Math.sin(theta) * o.radius, 0, Math.cos(theta) * o.radius],
3874
4472
  // Face radially OUTWARD so the papers nearest the camera show their
@@ -3878,128 +4476,314 @@ var ring = {
3878
4476
  };
3879
4477
  }
3880
4478
  };
3881
- var deckSchema = import_zod17.z.object({
3882
- spread: import_zod17.z.number().min(0).max(1).default(0.3),
3883
- lift: import_zod17.z.number().min(5e-3).max(0.08).default(0.014)
4479
+ var fanSchema = import_zod22.z.object({
4480
+ /** Total angular sweep from the first sheet to the last, degrees. */
4481
+ sweep: import_zod22.z.number().min(0).max(180).default(72),
4482
+ /** Where the shared pin sits, in half-sheet-heights below center. 1 = the bottom edge. */
4483
+ hinge: import_zod22.z.number().min(0).max(4).default(1.15),
4484
+ /** Thickness step so the sheets stack in order instead of z-fighting. */
4485
+ lift: import_zod22.z.number().min(2e-3).max(0.08).default(0.012),
4486
+ /** How much flatter the middle of the fan sits than its outer sheets. */
4487
+ bow: import_zod22.z.number().min(0).max(1).default(0.7)
3884
4488
  });
3885
- var deck = {
3886
- id: "deck",
3887
- label: "Deck",
3888
- defaults: deckSchema.parse({}),
3889
- optionsSchema: deckSchema,
3890
- pose(i, _n, o) {
4489
+ var fan = {
4490
+ id: "fan",
4491
+ label: "Fan",
4492
+ defaults: fanSchema.parse({}),
4493
+ optionsSchema: fanSchema,
4494
+ pose(i, n, o, _phase, sheet2) {
4495
+ const f = n > 1 ? i / (n - 1) : 0.5;
4496
+ const theta = (f - 0.5) * o.sweep * DEG6;
4497
+ const hinge = o.hinge * sheet2.height / 2;
4498
+ const open = Math.abs(f - 0.5) * 2;
3891
4499
  return {
3892
- position: [jitter(1, i) * 0.09 * o.spread * 3, jitter(2, i) * 0.07 * o.spread * 3, i * o.lift],
3893
- rotation: [0, 0, jitter(3, i) * 0.35 * o.spread],
3894
- scale: 1
4500
+ position: [-Math.sin(theta) * hinge, Math.cos(theta) * hinge - hinge, i * o.lift],
4501
+ rotation: [0, 0, theta],
4502
+ scale: 1,
4503
+ bias: 1 - (1 - open) * o.bow
3895
4504
  };
3896
4505
  }
3897
4506
  };
3898
- var cascadeSchema = import_zod17.z.object({
3899
- gap: import_zod17.z.number().min(0.2).max(2).default(0.55),
3900
- drop: import_zod17.z.number().min(0).max(1).default(0.2)
4507
+ var spreadSchema = import_zod22.z.object({
4508
+ /** How far each sheet slides past the one below it. */
4509
+ slip: import_zod22.z.number().min(0.02).max(2).default(0.3),
4510
+ /** Direction of the slide, degrees. 0 slides right, 90 slides up. */
4511
+ angle: import_zod22.z.number().min(-180).max(180).default(28),
4512
+ lift: import_zod22.z.number().min(2e-3).max(0.08).default(0.012),
4513
+ /** How much more the sheets at the far end of the slide bow. */
4514
+ bow: import_zod22.z.number().min(0).max(1).default(0.6),
4515
+ /** Nothing hand-slid is perfectly square — a touch of per-sheet rotation. */
4516
+ drift: import_zod22.z.number().min(0).max(1).default(0.15)
3901
4517
  });
3902
- var cascade = {
3903
- id: "cascade",
3904
- label: "Cascade",
3905
- defaults: cascadeSchema.parse({}),
3906
- optionsSchema: cascadeSchema,
4518
+ var spread = {
4519
+ id: "spread",
4520
+ label: "Spread",
4521
+ defaults: spreadSchema.parse({}),
4522
+ optionsSchema: spreadSchema,
3907
4523
  pose(i, n, o) {
3908
4524
  const centered = i - (n - 1) / 2;
4525
+ const a = o.angle * DEG6;
3909
4526
  return {
3910
- position: [centered * o.gap, -centered * o.drop, -i * 0.06],
3911
- rotation: [0, 0, jitter(4, i) * 0.06],
3912
- scale: 1
4527
+ position: [Math.cos(a) * o.slip * centered, Math.sin(a) * o.slip * centered, i * o.lift],
4528
+ rotation: [0, 0, jitter2(11, i) * 0.2 * o.drift],
4529
+ scale: 1,
4530
+ bias: 1 - (1 - ramp(i, n)) * o.bow
3913
4531
  };
3914
4532
  }
3915
4533
  };
3916
- var helixSchema = import_zod17.z.object({
3917
- radius: import_zod17.z.number().min(0.5).max(12).default(2.2),
3918
- height: import_zod17.z.number().min(0.5).max(10).default(2.6),
3919
- turns: import_zod17.z.number().min(0.5).max(5).default(1.5)
4534
+ var pileSchema = import_zod22.z.object({
4535
+ /** How far sheets wander from the center of the heap. */
4536
+ scatter: import_zod22.z.number().min(0).max(2).default(0.22),
4537
+ /** Widest angle a sheet sits off square, degrees. */
4538
+ turn: import_zod22.z.number().min(0).max(180).default(24),
4539
+ lift: import_zod22.z.number().min(2e-3).max(0.08).default(0.011),
4540
+ /** How flat the sheets underneath are pressed by the ones on top. */
4541
+ press: import_zod22.z.number().min(0).max(1).default(0.85),
4542
+ seed: import_zod22.z.number().int().min(0).max(9999).default(3)
3920
4543
  });
3921
- var helix = {
3922
- id: "helix",
3923
- label: "Helix",
3924
- defaults: helixSchema.parse({}),
3925
- optionsSchema: helixSchema,
3926
- pose(i, n, o, phase) {
3927
- const f = n > 1 ? i / (n - 1) : 0;
3928
- const theta = (f * o.turns + phase) * TAU2;
4544
+ var pile = {
4545
+ id: "pile",
4546
+ label: "Pile",
4547
+ defaults: pileSchema.parse({}),
4548
+ optionsSchema: pileSchema,
4549
+ pose(i, n, o) {
3929
4550
  return {
3930
- position: [Math.sin(theta) * o.radius, (f - 0.5) * o.height, Math.cos(theta) * o.radius],
3931
- // Outward-facing, same as the ring fronts toward the viewer outside.
3932
- rotation: [0, theta, 0],
3933
- scale: 1
4551
+ position: [jitter2(o.seed, i) * o.scatter, jitter2(o.seed + 1, i) * o.scatter * 0.8, i * o.lift],
4552
+ rotation: [0, 0, jitter2(o.seed + 2, i) * o.turn * DEG6],
4553
+ scale: 1,
4554
+ bias: 1 - (1 - ramp(i, n)) * o.press
3934
4555
  };
3935
4556
  }
3936
4557
  };
3937
- var wallSchema = import_zod17.z.object({
3938
- gapX: import_zod17.z.number().min(0.05).max(1).default(0.22),
3939
- gapY: import_zod17.z.number().min(0.05).max(1).default(0.3),
3940
- jitterAmt: import_zod17.z.number().min(0).max(1).default(0.25)
4558
+ var wallSchema = import_zod22.z.object({
4559
+ gapX: import_zod22.z.number().min(0.05).max(1).default(0.22),
4560
+ gapY: import_zod22.z.number().min(0.05).max(1).default(0.3),
4561
+ jitterAmt: import_zod22.z.number().min(0).max(1).default(0.25),
4562
+ /** Spread of sag across the wall — no two pinned sheets hang alike. */
4563
+ sag: import_zod22.z.number().min(0).max(1).default(0.45)
3941
4564
  });
3942
4565
  var wall = {
3943
4566
  id: "wall",
3944
4567
  label: "Wall",
3945
4568
  defaults: wallSchema.parse({}),
3946
4569
  optionsSchema: wallSchema,
3947
- pose(i, n, o) {
3948
- const cols = Math.ceil(Math.sqrt(n * 1.4));
4570
+ pose(i, n, o, _phase, sheet2) {
4571
+ const cols = Math.ceil(Math.sqrt(n * sheet2.height / sheet2.width));
3949
4572
  const rows = Math.ceil(n / cols);
3950
4573
  const col = i % cols;
3951
4574
  const row = Math.floor(i / cols);
3952
- const cellW = 1 + o.gapX;
3953
- const cellH = 1.4 + o.gapY;
4575
+ const cellW = sheet2.width + o.gapX;
4576
+ const cellH = sheet2.height + o.gapY;
3954
4577
  return {
3955
4578
  position: [
3956
4579
  (col - (cols - 1) / 2) * cellW,
3957
4580
  ((rows - 1) / 2 - row) * cellH,
3958
- jitter(5, i) * 0.04 * o.jitterAmt * 4
4581
+ jitter2(5, i) * 0.04 * o.jitterAmt * 4
3959
4582
  ],
3960
- rotation: [0, 0, jitter(6, i) * 0.05 * o.jitterAmt * 4],
3961
- scale: 1
4583
+ rotation: [0, 0, jitter2(6, i) * 0.05 * o.jitterAmt * 4],
4584
+ scale: 1,
4585
+ bias: 1 - Math.abs(jitter2(7, i)) * o.sag
3962
4586
  };
3963
4587
  }
3964
4588
  };
3965
- var tunnelSchema = import_zod17.z.object({
3966
- radius: import_zod17.z.number().min(0.5).max(6).default(1.7),
3967
- spacing: import_zod17.z.number().min(0.1).max(3).default(0.55)
4589
+ var spillSchema = import_zod22.z.object({
4590
+ spreadX: import_zod22.z.number().min(0.5).max(8).default(2.4),
4591
+ spreadY: import_zod22.z.number().min(0.5).max(8).default(1.5),
4592
+ depth: import_zod22.z.number().min(0).max(6).default(1.6),
4593
+ /** How far sheets pitch and roll out of the picture plane. */
4594
+ tumble: import_zod22.z.number().min(0).max(1).default(0.5),
4595
+ /** Spread of bend across the sheets — a spill does not fold them alike. */
4596
+ vary: import_zod22.z.number().min(0).max(1).default(0.6),
4597
+ seed: import_zod22.z.number().int().min(0).max(9999).default(7)
3968
4598
  });
3969
- var tunnel = {
3970
- id: "tunnel",
3971
- label: "Tunnel",
3972
- defaults: tunnelSchema.parse({}),
3973
- optionsSchema: tunnelSchema,
3974
- pose(i, _n, o, phase) {
3975
- const theta = i * 2.39996 + phase * TAU2;
4599
+ var spill = {
4600
+ id: "spill",
4601
+ label: "Spill",
4602
+ defaults: spillSchema.parse({}),
4603
+ optionsSchema: spillSchema,
4604
+ pose(i, _n, o) {
4605
+ const tumble = o.tumble * 2;
3976
4606
  return {
3977
- position: [Math.cos(theta) * o.radius, Math.sin(theta) * o.radius, -i * o.spacing],
3978
- rotation: [0, 0, theta - Math.PI / 2],
3979
- scale: 1
4607
+ position: [
4608
+ jitter2(o.seed, i) * o.spreadX,
4609
+ jitter2(o.seed + 1, i) * o.spreadY,
4610
+ jitter2(o.seed + 2, i) * o.depth
4611
+ ],
4612
+ rotation: [
4613
+ jitter2(o.seed + 3, i) * 0.4 * tumble,
4614
+ jitter2(o.seed + 4, i) * 0.5 * tumble,
4615
+ jitter2(o.seed + 5, i) * 0.4 * tumble
4616
+ ],
4617
+ scale: 0.85 + Math.abs(jitter2(o.seed + 6, i)) * 0.3,
4618
+ bias: 1 - Math.abs(jitter2(o.seed + 7, i)) * o.vary
3980
4619
  };
3981
4620
  }
3982
4621
  };
3983
- var scatterSchema = import_zod17.z.object({
3984
- spreadX: import_zod17.z.number().min(0.5).max(8).default(2.4),
3985
- spreadY: import_zod17.z.number().min(0.5).max(8).default(1.5),
3986
- depth: import_zod17.z.number().min(0).max(6).default(1.6),
3987
- seed: import_zod17.z.number().int().min(0).max(9999).default(7)
4622
+ var sweepSchema = import_zod22.z.object({
4623
+ columns: import_zod22.z.number().int().min(1).max(24).default(5),
4624
+ /** Breathing room around each specimen. */
4625
+ gap: import_zod22.z.number().min(0).max(2).default(0.22),
4626
+ /** Deformation at the first specimen and at the last. */
4627
+ from: import_zod22.z.number().min(0).max(1).default(0),
4628
+ to: import_zod22.z.number().min(0).max(1).default(1)
3988
4629
  });
3989
- var scatter = {
3990
- id: "scatter",
3991
- label: "Scatter",
3992
- defaults: scatterSchema.parse({}),
3993
- optionsSchema: scatterSchema,
3994
- pose(i, _n, o) {
4630
+ var sweep = {
4631
+ id: "sweep",
4632
+ label: "Sweep",
4633
+ defaults: sweepSchema.parse({}),
4634
+ optionsSchema: sweepSchema,
4635
+ pose(i, n, o, _phase, sheet2) {
4636
+ const cols = Math.min(o.columns, Math.max(n, 1));
4637
+ const rows = Math.ceil(n / cols);
4638
+ const col = i % cols;
4639
+ const row = Math.floor(i / cols);
3995
4640
  return {
3996
4641
  position: [
3997
- jitter(o.seed, i) * o.spreadX,
3998
- jitter(o.seed + 1, i) * o.spreadY,
3999
- jitter(o.seed + 2, i) * o.depth
4642
+ (col - (cols - 1) / 2) * (sheet2.width + o.gap),
4643
+ ((rows - 1) / 2 - row) * (sheet2.height + o.gap),
4644
+ 0
4000
4645
  ],
4001
- rotation: [jitter(o.seed + 3, i) * 0.4, jitter(o.seed + 4, i) * 0.5, jitter(o.seed + 5, i) * 0.4],
4002
- scale: 0.85 + Math.abs(jitter(o.seed + 6, i)) * 0.3
4646
+ rotation: [0, 0, 0],
4647
+ scale: 1,
4648
+ bias: o.from + (o.to - o.from) * ramp(i, n)
4649
+ };
4650
+ }
4651
+ };
4652
+ var bookSchema = import_zod22.z.object({
4653
+ /** How far the outermost page lifts off the block, degrees. */
4654
+ spread: import_zod22.z.number().min(0).max(150).default(55),
4655
+ /** Fraction of the pages bound to the left. 0 = a one-sided sample book. */
4656
+ split: import_zod22.z.number().min(0).max(1).default(0.5),
4657
+ /** Page thickness — the gap between pages of one block. */
4658
+ lift: import_zod22.z.number().min(1e-3).max(0.05).default(8e-3),
4659
+ /** How much more a lifted page arcs than one lying flat in the block. */
4660
+ gutter: import_zod22.z.number().min(0).max(1).default(0.6)
4661
+ });
4662
+ var book = {
4663
+ id: "book",
4664
+ label: "Book",
4665
+ defaults: bookSchema.parse({}),
4666
+ optionsSchema: bookSchema,
4667
+ pose(i, n, o, _phase, sheet2) {
4668
+ const half = sheet2.width / 2;
4669
+ const left = Math.round(n * o.split);
4670
+ const onLeft = i < left;
4671
+ const count = onLeft ? left : n - left;
4672
+ const k = onLeft ? i : i - left;
4673
+ const f = count > 1 ? k / (count - 1) : 1;
4674
+ const theta = f * o.spread * DEG6;
4675
+ const side = onLeft ? -1 : 1;
4676
+ const cos = Math.cos(theta);
4677
+ const sin = Math.sin(theta);
4678
+ const offset = k * o.lift;
4679
+ return {
4680
+ position: [side * (half * cos - offset * sin), 0, half * sin + offset * cos],
4681
+ rotation: [0, -side * theta, 0],
4682
+ scale: 1,
4683
+ bias: 1 - (1 - f) * o.gutter
4684
+ };
4685
+ }
4686
+ };
4687
+ var accordionSchema = import_zod22.z.object({
4688
+ /** How far each panel tilts off the strip's line, degrees. 0 = flat, 90 = shut. */
4689
+ angle: import_zod22.z.number().min(0).max(89).default(55),
4690
+ /** A concertina holds its creases — how much bow the panels keep. */
4691
+ slack: import_zod22.z.number().min(0).max(1).default(0.15)
4692
+ });
4693
+ var accordion = {
4694
+ id: "accordion",
4695
+ label: "Accordion",
4696
+ defaults: accordionSchema.parse({}),
4697
+ optionsSchema: accordionSchema,
4698
+ pose(i, n, o, _phase, sheet2) {
4699
+ const theta = o.angle * DEG6;
4700
+ const side = i % 2 === 0 ? 1 : -1;
4701
+ const step = sheet2.width * Math.cos(theta);
4702
+ return {
4703
+ position: [(i - (n - 1) / 2) * step, 0, 0],
4704
+ rotation: [0, side * theta, 0],
4705
+ scale: 1,
4706
+ bias: o.slack
4707
+ };
4708
+ }
4709
+ };
4710
+ var rackSchema = import_zod22.z.object({
4711
+ /** Gap along the row, as a fraction of the paper's width. Under 1 they overlap. */
4712
+ spacing: import_zod22.z.number().min(0.05).max(2).default(0.82),
4713
+ /** How far a sheet leans back off vertical, degrees. */
4714
+ lean: import_zod22.z.number().min(0).max(70).default(16),
4715
+ /** How much that lean differs sheet to sheet — nothing propped is uniform. */
4716
+ vary: import_zod22.z.number().min(0).max(1).default(0.55),
4717
+ /** Small rotations off square. */
4718
+ sway: import_zod22.z.number().min(0).max(1).default(0.35),
4719
+ seed: import_zod22.z.number().int().min(0).max(9999).default(5)
4720
+ });
4721
+ var rack = {
4722
+ id: "rack",
4723
+ label: "Rack",
4724
+ defaults: rackSchema.parse({}),
4725
+ optionsSchema: rackSchema,
4726
+ pose(i, n, o, _phase, sheet2) {
4727
+ const lean = o.lean * DEG6 * (1 + jitter2(o.seed, i) * o.vary);
4728
+ const half = sheet2.height / 2;
4729
+ return {
4730
+ position: [
4731
+ (i - (n - 1) / 2) * sheet2.width * o.spacing,
4732
+ // Standing on the floor: the bottom edge stays at y = 0 as it leans.
4733
+ half * Math.cos(lean),
4734
+ -half * Math.sin(lean) + i * 4e-3
4735
+ ],
4736
+ rotation: [-lean, jitter2(o.seed + 1, i) * 0.12 * o.sway, jitter2(o.seed + 2, i) * 0.06 * o.sway],
4737
+ scale: 1,
4738
+ // A sheet leaning further has more of its own weight to carry.
4739
+ bias: o.lean === 0 ? 0 : Math.min(1, lean / (o.lean * DEG6 * (1 + o.vary)))
4740
+ };
4741
+ }
4742
+ };
4743
+ var colonnadeSchema = import_zod22.z.object({
4744
+ /** The walk the colonnade is built along — see `stage/path`. */
4745
+ path: walkPathSchema.default({}),
4746
+ /** Half-width of the clear aisle: how far each banner stands off the walk line. */
4747
+ aisle: import_zod22.z.number().min(0.2).max(20).default(2.4),
4748
+ /** How much that gap opens and closes along the walk. Nothing hung by hand is a corridor. */
4749
+ breathe: import_zod22.z.number().min(0).max(1).default(0.3),
4750
+ /** Widest angle a banner turns off square to the aisle, degrees. */
4751
+ twist: import_zod22.z.number().min(0).max(90).default(22),
4752
+ /** Fraction of the walk left clear at each end, so the figure has somewhere to enter from. */
4753
+ margin: import_zod22.z.number().min(0).max(0.45).default(0.05),
4754
+ /** Spread of banner heights, 0..1. */
4755
+ rise: import_zod22.z.number().min(0).max(1).default(0.28),
4756
+ /** How far the banners lift off the floor, as a fraction of their height. 0 = they pool on it. */
4757
+ hover: import_zod22.z.number().min(0).max(1).default(0),
4758
+ /** Spread of deformation — no two lengths of hung paper drape alike. */
4759
+ drape: import_zod22.z.number().min(0).max(1).default(0.5),
4760
+ seed: import_zod22.z.number().int().min(0).max(9999).default(2)
4761
+ });
4762
+ var colonnade = {
4763
+ id: "colonnade",
4764
+ label: "Colonnade",
4765
+ defaults: colonnadeSchema.parse({}),
4766
+ optionsSchema: colonnadeSchema,
4767
+ pose(i, n, o, phase, sheet2) {
4768
+ const path = getWalkPath(o.path);
4769
+ const side = i % 2 === 0 ? 1 : -1;
4770
+ const pairs = Math.max(Math.ceil(n / 2), 1);
4771
+ const k = Math.floor(i / 2);
4772
+ const span = 1 - o.margin * 2;
4773
+ const step = pairs > 1 ? span / (pairs - 1) : 0;
4774
+ const base = o.margin + (pairs > 1 ? k * step : span / 2) + side * step * 0.25;
4775
+ const s = path.closed ? base + phase : base;
4776
+ const [px, pz] = path.pointAt(s);
4777
+ const [nx, nz] = path.normalAt(s);
4778
+ const scale = 1 + jitter2(o.seed, i) * o.rise * 0.5;
4779
+ const offset = o.aisle * (1 + jitter2(o.seed + 1, i) * o.breathe);
4780
+ const height = sheet2.height * scale;
4781
+ const yaw = Math.atan2(-side * nx, -side * nz) + jitter2(o.seed + 2, i) * o.twist * DEG6;
4782
+ return {
4783
+ position: [px + nx * side * offset, height / 2 + height * o.hover, pz + nz * side * offset],
4784
+ rotation: [0, yaw, 0],
4785
+ scale,
4786
+ bias: 1 - Math.abs(jitter2(o.seed + 3, i)) * o.drape
4003
4787
  };
4004
4788
  }
4005
4789
  };
@@ -4028,36 +4812,48 @@ function listLayouts() {
4028
4812
  return [...registry3.keys()];
4029
4813
  }
4030
4814
  registerLayout(ring);
4031
- registerLayout(deck);
4032
- registerLayout(cascade);
4033
- registerLayout(helix);
4815
+ registerLayout(fan);
4816
+ registerLayout(spread);
4817
+ registerLayout(pile);
4034
4818
  registerLayout(wall);
4035
- registerLayout(tunnel);
4036
- registerLayout(scatter);
4819
+ registerLayout(spill);
4820
+ registerLayout(sweep);
4821
+ registerLayout(book);
4822
+ registerLayout(accordion);
4823
+ registerLayout(rack);
4824
+ registerLayout(colonnade);
4037
4825
  registerLayout(sheet);
4038
4826
 
4827
+ // src/field/stack.ts
4828
+ function fieldShapeStack(config, progress) {
4829
+ if (config.deformers) return resolveDeformerStack(config.deformers);
4830
+ if (!config.behavior) return [];
4831
+ const behavior = getBehavior(config.behavior.type);
4832
+ const options = { ...config.behavior, [behavior.progressParam]: progress };
4833
+ return behavior.stack(options, config.sheet);
4834
+ }
4835
+
4039
4836
  // src/field/fieldGroup.tsx
4040
4837
  var import_jsx_runtime7 = require("react/jsx-runtime");
4041
- var scratchObj = new THREE10.Object3D();
4838
+ var scratchObj = new THREE11.Object3D();
4042
4839
  var scratchAero = { position: [0, 0, 0], rotation: [0, 0, 0] };
4043
4840
  var FIELD_SEGMENT_CAP = 48;
4044
4841
  function FieldGroup({ group, shared }) {
4045
4842
  const { config, indices, contents } = group;
4046
4843
  const count = indices.length;
4047
4844
  const stock = getStock(config.stock);
4048
- const behavior = config.behavior ? getBehavior(config.behavior.type) : null;
4845
+ const behavior = config.behavior && !config.deformers ? getBehavior(config.behavior.type) : null;
4049
4846
  const progressRef = (0, import_react10.useRef)(
4050
4847
  behavior ? config.behavior[behavior.progressParam] : 0
4051
4848
  );
4052
- const buildStackAt = (progress) => {
4053
- if (!config.behavior || !behavior) return [];
4054
- const options = { ...config.behavior, [behavior.progressParam]: progress };
4055
- return behavior.stack(options, config.sheet);
4056
- };
4849
+ const buildStackAt = (progress) => fieldShapeStack(config, progress);
4057
4850
  const initialStack = (0, import_react10.useMemo)(
4058
4851
  () => buildStackAt(progressRef.current),
4059
- // eslint-disable-next-line react-hooks/exhaustive-deps
4060
- [JSON.stringify(config.behavior ?? null), JSON.stringify(config.sheet)]
4852
+ [
4853
+ JSON.stringify(config.behavior ?? null),
4854
+ JSON.stringify(config.deformers ?? null),
4855
+ JSON.stringify(config.sheet)
4856
+ ]
4061
4857
  );
4062
4858
  const structureKey = initialStack.map((i) => i.type).join("|");
4063
4859
  const geometry = (0, import_react10.useMemo)(() => {
@@ -4070,12 +4866,14 @@ function FieldGroup({ group, shared }) {
4070
4866
  );
4071
4867
  const atlasIdx = new Float32Array(count);
4072
4868
  const phase = new Float32Array(count);
4869
+ const bias = new Float32Array(count).fill(1);
4073
4870
  for (let i = 0; i < count; i++) {
4074
4871
  atlasIdx[i] = i;
4075
4872
  phase[i] = indices[i] * 0.618034 % 1 * 4;
4076
4873
  }
4077
- geo.setAttribute("aAtlas", new THREE10.InstancedBufferAttribute(atlasIdx, 1));
4078
- geo.setAttribute("aPhase", new THREE10.InstancedBufferAttribute(phase, 1));
4874
+ geo.setAttribute("aAtlas", new THREE11.InstancedBufferAttribute(atlasIdx, 1));
4875
+ geo.setAttribute("aPhase", new THREE11.InstancedBufferAttribute(phase, 1));
4876
+ geo.setAttribute("aBias", new THREE11.InstancedBufferAttribute(bias, 1));
4079
4877
  return geo;
4080
4878
  }, [JSON.stringify(config.sheet), structureKey, count]);
4081
4879
  (0, import_react10.useEffect)(() => () => geometry.dispose(), [geometry]);
@@ -4085,23 +4883,34 @@ function FieldGroup({ group, shared }) {
4085
4883
  const uniforms = {};
4086
4884
  for (const [name, value] of Object.entries(composed.uniforms)) {
4087
4885
  uniforms[name] = {
4088
- value: Array.isArray(value) && value.length === 2 ? new THREE10.Vector2(...value) : value
4886
+ value: Array.isArray(value) && value.length === 2 ? new THREE11.Vector2(...value) : value
4089
4887
  };
4090
4888
  }
4091
4889
  uniforms.uPlTime = { value: 0 };
4092
4890
  uniforms.uAtlas = { value: null };
4093
- uniforms.uAtlasGrid = { value: new THREE10.Vector2(1, 1) };
4891
+ uniforms.uAtlasGrid = { value: new THREE11.Vector2(1, 1) };
4094
4892
  uniforms.uBackDarken = {
4095
4893
  value: 1 - Math.min(0.45, 0.12 + config.sheet.thickness * 0.9) * stock.opacity
4096
4894
  };
4097
- uniforms.uStockColor = { value: new THREE10.Color(stock.color) };
4895
+ uniforms.uStockColor = { value: new THREE11.Color(stock.color) };
4098
4896
  uniforms.uShowThrough = { value: config.surface.showThrough ?? stock.showThrough };
4897
+ Object.assign(
4898
+ uniforms,
4899
+ translucencyUniforms(config.surface.translucency ?? stock.translucency, config.scene.lighting)
4900
+ );
4099
4901
  return {
4100
4902
  vertexShader: buildFieldVertexShader(composed),
4101
4903
  fragmentShader: buildFieldFragmentShader(),
4102
4904
  uniforms
4103
4905
  };
4104
- }, [structureKey, JSON.stringify(config.sheet), stock.id, config.surface.showThrough]);
4906
+ }, [
4907
+ structureKey,
4908
+ JSON.stringify(config.sheet),
4909
+ stock.id,
4910
+ config.surface.showThrough,
4911
+ config.surface.translucency,
4912
+ config.scene.lighting
4913
+ ]);
4105
4914
  (0, import_react10.useEffect)(() => {
4106
4915
  if (!atlas) return;
4107
4916
  shader.uniforms.uAtlas.value = atlas.texture;
@@ -4135,7 +4944,7 @@ function FieldGroup({ group, shared }) {
4135
4944
  for (const [name, value] of Object.entries(values)) {
4136
4945
  const uniform = shader.uniforms[name];
4137
4946
  if (!uniform) continue;
4138
- if (uniform.value instanceof THREE10.Vector2 && Array.isArray(value)) {
4947
+ if (uniform.value instanceof THREE11.Vector2 && Array.isArray(value)) {
4139
4948
  uniform.value.set(value[0], value[1]);
4140
4949
  } else {
4141
4950
  uniform.value = value;
@@ -4145,15 +4954,19 @@ function FieldGroup({ group, shared }) {
4145
4954
  const layout = getLayout(shared.layoutId);
4146
4955
  const morph = shared.morphRef.current;
4147
4956
  const behaviorTransform = behavior?.transform && config.behavior ? behavior : null;
4957
+ const biasAttr = geometry.getAttribute("aBias");
4958
+ const biases = biasAttr.array;
4959
+ let biasChanged = false;
4148
4960
  for (let j = 0; j < count; j++) {
4149
4961
  const i = indices[j];
4150
- let pose = layout.pose(i, shared.total, shared.layoutOptions, shared.phaseRef.current);
4962
+ let pose = layout.pose(i, shared.total, shared.layoutOptions, shared.phaseRef.current, shared.sheet);
4151
4963
  if (morph.from && morph.t < 1) {
4152
4964
  const prev = getLayout(morph.from.id).pose(
4153
4965
  i,
4154
4966
  shared.total,
4155
4967
  morph.from.options,
4156
- shared.phaseRef.current
4968
+ shared.phaseRef.current,
4969
+ shared.sheet
4157
4970
  );
4158
4971
  pose = lerpPose(prev, pose, easeInOut(morph.t));
4159
4972
  }
@@ -4163,6 +4976,11 @@ function FieldGroup({ group, shared }) {
4163
4976
  pose = lerpPose(entrancePose(shared.entranceType, i, pose), pose, easeOut(tIn));
4164
4977
  }
4165
4978
  }
4979
+ const bias = Math.min(1, Math.max(0, pose.bias ?? 1));
4980
+ if (biases[j] !== bias) {
4981
+ biases[j] = bias;
4982
+ biasChanged = true;
4983
+ }
4166
4984
  scratchObj.position.set(...pose.position);
4167
4985
  scratchObj.rotation.set(...pose.rotation);
4168
4986
  scratchObj.scale.setScalar(pose.scale);
@@ -4187,6 +5005,7 @@ function FieldGroup({ group, shared }) {
4187
5005
  mesh.setMatrixAt(j, scratchObj.matrix);
4188
5006
  }
4189
5007
  mesh.instanceMatrix.needsUpdate = true;
5008
+ if (biasChanged) biasAttr.needsUpdate = true;
4190
5009
  });
4191
5010
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4192
5011
  "instancedMesh",
@@ -4199,13 +5018,13 @@ function FieldGroup({ group, shared }) {
4199
5018
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4200
5019
  import_three_custom_shader_material2.default,
4201
5020
  {
4202
- baseMaterial: THREE10.MeshStandardMaterial,
5021
+ baseMaterial: THREE11.MeshStandardMaterial,
4203
5022
  vertexShader: shader.vertexShader,
4204
5023
  fragmentShader: shader.fragmentShader,
4205
5024
  uniforms: shader.uniforms,
4206
5025
  roughness: stock.roughness,
4207
5026
  metalness: 0,
4208
- side: THREE10.DoubleSide
5027
+ side: THREE11.DoubleSide
4209
5028
  },
4210
5029
  `${structureKey}:${count}`
4211
5030
  )
@@ -4217,14 +5036,16 @@ function entrancePose(type, i, target) {
4217
5036
  return {
4218
5037
  position: [target.position[0], target.position[1] - 3.2, target.position[2] - 0.5],
4219
5038
  rotation: [target.rotation[0] - 0.7, target.rotation[1], target.rotation[2] + 0.25],
4220
- scale: target.scale * 0.85
5039
+ scale: target.scale * 0.85,
5040
+ bias: target.bias
4221
5041
  };
4222
5042
  }
4223
5043
  const a = i * 2.399;
4224
5044
  return {
4225
5045
  position: [Math.cos(a) * 7, Math.sin(a * 1.3) * 4, Math.sin(a) * 6],
4226
5046
  rotation: [Math.sin(a) * 2, a, Math.cos(a) * 2],
4227
- scale: target.scale * 0.6
5047
+ scale: target.scale * 0.6,
5048
+ bias: target.bias
4228
5049
  };
4229
5050
  }
4230
5051
  function lerpPose(a, b, t) {
@@ -4240,14 +5061,15 @@ function lerpPose(a, b, t) {
4240
5061
  lerp(a.rotation[1], b.rotation[1]),
4241
5062
  lerp(a.rotation[2], b.rotation[2])
4242
5063
  ],
4243
- scale: lerp(a.scale, b.scale)
5064
+ scale: lerp(a.scale, b.scale),
5065
+ bias: lerp(a.bias ?? 1, b.bias ?? 1)
4244
5066
  };
4245
5067
  }
4246
5068
  var easeOut = (t) => 1 - (1 - t) ** 3;
4247
5069
  var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
4248
5070
 
4249
5071
  // src/field/backingSheet.tsx
4250
- var THREE11 = __toESM(require("three"), 1);
5072
+ var THREE12 = __toESM(require("three"), 1);
4251
5073
  var import_react11 = require("react");
4252
5074
 
4253
5075
  // src/content/backing.ts
@@ -4329,7 +5151,7 @@ function BackingSheet({
4329
5151
  c.height = Math.max(2, Math.round(height * scale));
4330
5152
  return c;
4331
5153
  }, [width, height]);
4332
- const texture = (0, import_react11.useMemo)(() => canvas ? new THREE11.CanvasTexture(canvas) : null, [canvas]);
5154
+ const texture = (0, import_react11.useMemo)(() => canvas ? new THREE12.CanvasTexture(canvas) : null, [canvas]);
4333
5155
  const removedKey = [...removed].sort((a, b) => a - b).join(",");
4334
5156
  (0, import_react11.useEffect)(() => {
4335
5157
  if (!canvas || !texture) return;
@@ -4344,7 +5166,7 @@ function BackingSheet({
4344
5166
  }
4345
5167
 
4346
5168
  // src/field/interactiveField.tsx
4347
- var THREE12 = __toESM(require("three"), 1);
5169
+ var THREE13 = __toESM(require("three"), 1);
4348
5170
  var import_gsap4 = require("gsap");
4349
5171
  var import_fiber5 = require("@react-three/fiber");
4350
5172
  var import_react12 = require("react");
@@ -4369,7 +5191,6 @@ function InteractiveField(props) {
4369
5191
  const patch = slotPatches[i];
4370
5192
  return patch ? paperConfigSchema.parse(mergeConfig(config, patch)) : config;
4371
5193
  }),
4372
- // eslint-disable-next-line react-hooks/exhaustive-deps
4373
5194
  [
4374
5195
  JSON.stringify(papers),
4375
5196
  JSON.stringify(fallback ?? null),
@@ -4379,9 +5200,8 @@ function InteractiveField(props) {
4379
5200
  ]
4380
5201
  );
4381
5202
  const poses = (0, import_react12.useMemo)(
4382
- () => slotConfigs.map((_, i) => layout.pose(i, total, layoutOptions, 0)),
4383
- // eslint-disable-next-line react-hooks/exhaustive-deps
4384
- [layoutId, JSON.stringify(layoutOptions), total]
5203
+ () => slotConfigs.map((_, i) => layout.pose(i, total, layoutOptions, 0, props.sheet)),
5204
+ [layoutId, JSON.stringify(layoutOptions), total, props.sheet.width, props.sheet.height]
4385
5205
  );
4386
5206
  const groupRefs = (0, import_react12.useRef)([]);
4387
5207
  const handleRefs = (0, import_react12.useRef)([]);
@@ -4399,10 +5219,10 @@ function InteractiveField(props) {
4399
5219
  }
4400
5220
  props.onSlotStateChange?.(i, state);
4401
5221
  };
4402
- const raycaster = (0, import_react12.useMemo)(() => new THREE12.Raycaster(), []);
4403
- const planeScratch = (0, import_react12.useMemo)(() => new THREE12.Plane(new THREE12.Vector3(0, 0, 1), 0), []);
4404
- const pointScratch = (0, import_react12.useMemo)(() => new THREE12.Vector3(), []);
4405
- const ndcScratch = (0, import_react12.useMemo)(() => new THREE12.Vector2(), []);
5222
+ const raycaster = (0, import_react12.useMemo)(() => new THREE13.Raycaster(), []);
5223
+ const planeScratch = (0, import_react12.useMemo)(() => new THREE13.Plane(new THREE13.Vector3(0, 0, 1), 0), []);
5224
+ const pointScratch = (0, import_react12.useMemo)(() => new THREE13.Vector3(), []);
5225
+ const ndcScratch = (0, import_react12.useMemo)(() => new THREE13.Vector2(), []);
4406
5226
  const planePoint = (clientX, clientY, planeZ) => {
4407
5227
  const rect = gl.domElement.getBoundingClientRect();
4408
5228
  ndcScratch.set(
@@ -4565,7 +5385,7 @@ function InteractiveField(props) {
4565
5385
  if (carried.pointerId !== null && e.pointerId !== carried.pointerId) return;
4566
5386
  const name = slotName(carried.slot);
4567
5387
  const zone = zonesLive().find(
4568
- (z18) => zoneContains(z18, carried.x.value, carried.y.value) && zoneAccepts(z18, name)
5388
+ (z27) => zoneContains(z27, carried.x.value, carried.y.value) && zoneAccepts(z27, name)
4569
5389
  );
4570
5390
  if (zone) settleInto(carried, zone);
4571
5391
  else returnHome(carried);
@@ -4606,12 +5426,12 @@ function InteractiveField(props) {
4606
5426
  group.position.z = carried.homePose.position[2] + 0.15;
4607
5427
  handle.set("drive", carryDrive(speed));
4608
5428
  const lag = 0.25;
4609
- group.rotation.y += (THREE12.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
4610
- group.rotation.x += (THREE12.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
5429
+ group.rotation.y += (THREE13.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
5430
+ group.rotation.x += (THREE13.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
4611
5431
  }
4612
5432
  const name = slotName(carried.slot);
4613
5433
  const zone = zonesLive().find(
4614
- (z18) => zoneContains(z18, group.position.x, group.position.y) && zoneAccepts(z18, name)
5434
+ (z27) => zoneContains(z27, group.position.x, group.position.y) && zoneAccepts(z27, name)
4615
5435
  );
4616
5436
  registry4.setHovered(zone?.id ?? null);
4617
5437
  const targetScale = (zone ? 1.03 : 1) * carried.homePose.scale;
@@ -4627,7 +5447,7 @@ function InteractiveField(props) {
4627
5447
  },
4628
5448
  placeAtZone: (slot, zoneId) => {
4629
5449
  const carried = carriedRef.current;
4630
- const zone = zonesLive().find((z18) => z18.id === zoneId);
5450
+ const zone = zonesLive().find((z27) => z27.id === zoneId);
4631
5451
  const group = groupRefs.current[slot];
4632
5452
  if (!carried || carried.slot !== slot || !zone || !group) return;
4633
5453
  group.position.x = zone.bounds.position[0];
@@ -4640,7 +5460,7 @@ function InteractiveField(props) {
4640
5460
  const carried = carriedRef.current;
4641
5461
  if (carried && carried.slot === slot && !carried.settling) returnHome(carried);
4642
5462
  },
4643
- zoneIds: () => zonesLive().map((z18) => z18.id),
5463
+ zoneIds: () => zonesLive().map((z27) => z27.id),
4644
5464
  slotState: (slot) => slotStates[slot] ?? "rest"
4645
5465
  };
4646
5466
  (0, import_react12.useEffect)(() => {
@@ -4758,25 +5578,88 @@ function FieldKeyboardMirror({
4758
5578
  if (handled) e.preventDefault();
4759
5579
  if (carry2 !== carrying) setCarrying(carry2);
4760
5580
  };
4761
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: mirrorHidden, role: "group", "aria-label": "Interactive papers", children: papers.map((slot, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
4762
- "button",
4763
- {
4764
- type: "button",
4765
- onKeyDown: onKeyDown(i),
4766
- "aria-label": paperLabel(slot, i),
4767
- "aria-pressed": carrying?.slot === i,
4768
- children: [
4769
- paperLabel(slot, i),
4770
- carrying?.slot === i && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { "aria-live": "polite", children: [
4771
- " ",
4772
- "\u2014 carrying; zone ",
4773
- controller.current?.zoneIds()[carrying.zoneIndex] ?? "none",
4774
- "; Enter places, Escape returns"
4775
- ] })
4776
- ]
4777
- },
4778
- i
4779
- )) });
5581
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("fieldset", { style: mirrorHidden, children: [
5582
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("legend", { children: "Interactive papers" }),
5583
+ papers.map((slot, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
5584
+ "button",
5585
+ {
5586
+ type: "button",
5587
+ onKeyDown: onKeyDown(i),
5588
+ "aria-label": paperLabel(slot, i),
5589
+ "aria-pressed": carrying?.slot === i,
5590
+ children: [
5591
+ paperLabel(slot, i),
5592
+ carrying?.slot === i && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { "aria-live": "polite", children: [
5593
+ " ",
5594
+ "\u2014 carrying; zone ",
5595
+ controller.current?.zoneIds()[carrying.zoneIndex] ?? "none",
5596
+ "; Enter places, Escape returns"
5597
+ ] })
5598
+ ]
5599
+ },
5600
+ i
5601
+ ))
5602
+ ] });
5603
+ }
5604
+
5605
+ // src/field/framing.ts
5606
+ var PHASE_SAMPLES = 8;
5607
+ function resolveLayoutOptions(layoutId, layout, propOptions, firstSheet) {
5608
+ const parsed = layout.optionsSchema.parse({
5609
+ ...layout.defaults,
5610
+ ...propOptions
5611
+ });
5612
+ if (layoutId !== "sheet") return parsed;
5613
+ return withSheetCellFromPaper(
5614
+ parsed,
5615
+ propOptions,
5616
+ firstSheet
5617
+ );
5618
+ }
5619
+ function fieldBounds(layout, n, options, sheet2) {
5620
+ const reach = Math.hypot(sheet2.width, sheet2.height) / 2;
5621
+ if (n <= 0) return { center: [0, 0, 0], half: [reach, reach, 0] };
5622
+ const min = [Infinity, Infinity, Infinity];
5623
+ const max = [-Infinity, -Infinity, -Infinity];
5624
+ for (let s = 0; s < PHASE_SAMPLES; s++) {
5625
+ for (let i = 0; i < n; i++) {
5626
+ const pose = layout.pose(i, n, options, s / PHASE_SAMPLES, sheet2);
5627
+ const r = reach * Math.max(pose.scale, 0);
5628
+ for (let axis = 0; axis < 3; axis++) {
5629
+ min[axis] = Math.min(min[axis], pose.position[axis] - r);
5630
+ max[axis] = Math.max(max[axis], pose.position[axis] + r);
5631
+ }
5632
+ }
5633
+ }
5634
+ return {
5635
+ center: [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2],
5636
+ half: [(max[0] - min[0]) / 2, (max[1] - min[1]) / 2, (max[2] - min[2]) / 2]
5637
+ };
5638
+ }
5639
+ var LIFT = 0.11;
5640
+ var DEG7 = Math.PI / 180;
5641
+ function fitCamera(layout, n, options, sheet2, fovDeg, aspect, margin = 1.06) {
5642
+ const { center } = fieldBounds(layout, n, options, sheet2);
5643
+ const reach = Math.hypot(sheet2.width, sheet2.height) / 2 * margin;
5644
+ const vTan = Math.tan(fovDeg * DEG7 / 2);
5645
+ const hTan = vTan * Math.max(aspect, 0.01);
5646
+ let distance = 0.1;
5647
+ for (let s = 0; s < PHASE_SAMPLES; s++) {
5648
+ for (let i = 0; i < Math.max(n, 0); i++) {
5649
+ const pose = layout.pose(i, n, options, s / PHASE_SAMPLES, sheet2);
5650
+ const r = reach * Math.max(pose.scale, 0);
5651
+ const depth = pose.position[2] - center[2];
5652
+ distance = Math.max(
5653
+ distance,
5654
+ (Math.abs(pose.position[0] - center[0]) + r) / hTan + depth,
5655
+ (Math.abs(pose.position[1] - center[1]) + r) / vTan + depth
5656
+ );
5657
+ }
5658
+ }
5659
+ return {
5660
+ position: [center[0], center[1] + distance * LIFT, center[2] + distance],
5661
+ target: center
5662
+ };
4780
5663
  }
4781
5664
 
4782
5665
  // src/PaperField.tsx
@@ -4790,25 +5673,18 @@ var PaperFieldMesh = (0, import_react14.forwardRef)(
4790
5673
  );
4791
5674
  const groups = (0, import_react14.useMemo)(
4792
5675
  () => groupFieldPapers(papers, props.preset),
4793
- // eslint-disable-next-line react-hooks/exhaustive-deps
4794
5676
  [JSON.stringify(papers), JSON.stringify(props.preset ?? null)]
4795
5677
  );
4796
5678
  const total = papers.length;
4797
5679
  const layoutId = props.layout ?? "ring";
4798
5680
  const layout = getLayout(layoutId);
4799
5681
  const firstSheet = groups[0]?.config.sheet;
4800
- const layoutOptions = (0, import_react14.useMemo)(() => {
4801
- const parsed = layout.optionsSchema.parse({
4802
- ...layout.defaults,
4803
- ...props.layoutOptions
4804
- });
4805
- if (layoutId !== "sheet") return parsed;
4806
- return withSheetCellFromPaper(
4807
- parsed,
4808
- props.layoutOptions,
4809
- firstSheet
4810
- );
4811
- }, [layoutId, JSON.stringify(props.layoutOptions ?? {}), firstSheet?.width, firstSheet?.height]);
5682
+ const layoutOptions = (0, import_react14.useMemo)(
5683
+ // Sheet grids size their cells from the papers themselves — gutter is
5684
+ // then literally the spacing between stamps (explicit cell dims win).
5685
+ () => resolveLayoutOptions(layoutId, layout, props.layoutOptions, firstSheet),
5686
+ [layoutId, JSON.stringify(props.layoutOptions ?? {}), firstSheet?.width, firstSheet?.height]
5687
+ );
4812
5688
  const phaseRef = (0, import_react14.useRef)(0);
4813
5689
  const dragVelRef = (0, import_react14.useRef)(0);
4814
5690
  const mountTimeRef = (0, import_react14.useRef)(-1);
@@ -4866,6 +5742,7 @@ var PaperFieldMesh = (0, import_react14.forwardRef)(
4866
5742
  morphRef,
4867
5743
  layoutId,
4868
5744
  layoutOptions,
5745
+ sheet: firstSheet ?? DEFAULT_SHEET,
4869
5746
  entranceType,
4870
5747
  stagger: props.entrance?.stagger ?? 0.06,
4871
5748
  entranceDuration: props.entrance?.duration ?? 0.9,
@@ -4884,6 +5761,7 @@ var PaperFieldMesh = (0, import_react14.forwardRef)(
4884
5761
  layoutId,
4885
5762
  layoutOptions,
4886
5763
  sheetOptions,
5764
+ sheet: firstSheet ?? DEFAULT_SHEET,
4887
5765
  reducedMotion: props.reducedMotion,
4888
5766
  zones: props.zones,
4889
5767
  onSlotStateChange: props.onSlotStateChange,
@@ -4894,10 +5772,47 @@ var PaperFieldMesh = (0, import_react14.forwardRef)(
4894
5772
  }
4895
5773
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("group", { ref, children: [
4896
5774
  sheetOptions?.backing && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BackingSheet, { options: sheetOptions, count: total, removed: EMPTY_SET }),
4897
- groups.map((group, gi) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(FieldGroup, { group, shared }, `${gi}:${group.indices.length}`))
5775
+ groups.map((group, gi) => (
5776
+ // biome-ignore lint/suspicious/noArrayIndexKey: groups are derived from the slot list in order, so position is their only identity.
5777
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(FieldGroup, { group, shared }, `${gi}:${group.indices.length}`)
5778
+ ))
4898
5779
  ] });
4899
5780
  }
4900
5781
  );
5782
+ function FitCamera(meshProps) {
5783
+ const camera = (0, import_fiber6.useThree)((s) => s.camera);
5784
+ const width = (0, import_fiber6.useThree)((s) => s.size.width);
5785
+ const height = (0, import_fiber6.useThree)((s) => s.size.height);
5786
+ const field = (0, import_react14.useMemo)(() => {
5787
+ const papers = effectiveFieldPapers(meshProps.papers, meshProps.images);
5788
+ const layoutId = meshProps.layout ?? "ring";
5789
+ const layout = getLayout(layoutId);
5790
+ const sheet2 = groupFieldPapers(papers, meshProps.preset)[0]?.config.sheet;
5791
+ const options = resolveLayoutOptions(layoutId, layout, meshProps.layoutOptions, sheet2);
5792
+ return { layout, n: papers.length, options, sheet: sheet2 ?? DEFAULT_SHEET };
5793
+ }, [
5794
+ JSON.stringify(meshProps.papers ?? null),
5795
+ JSON.stringify(meshProps.images ?? null),
5796
+ JSON.stringify(meshProps.preset ?? null),
5797
+ meshProps.layout,
5798
+ JSON.stringify(meshProps.layoutOptions ?? {})
5799
+ ]);
5800
+ (0, import_react14.useEffect)(() => {
5801
+ if (!(camera instanceof THREE14.PerspectiveCamera)) return;
5802
+ const { position, target } = fitCamera(
5803
+ field.layout,
5804
+ field.n,
5805
+ field.options,
5806
+ field.sheet,
5807
+ camera.fov,
5808
+ width / Math.max(height, 1)
5809
+ );
5810
+ camera.position.set(...position);
5811
+ camera.lookAt(...target);
5812
+ camera.updateProjectionMatrix();
5813
+ }, [camera, width, height, field]);
5814
+ return null;
5815
+ }
4901
5816
  var PaperField = (0, import_react14.forwardRef)(function PaperField2({ children, className, style, ...meshProps }, ref) {
4902
5817
  const registry4 = (0, import_react14.useMemo)(() => new DropZoneRegistry(), []);
4903
5818
  const a11yRef = (0, import_react14.useRef)(null);
@@ -4907,11 +5822,11 @@ var PaperField = (0, import_react14.forwardRef)(function PaperField2({ children,
4907
5822
  );
4908
5823
  const interactive = (0, import_react14.useMemo)(
4909
5824
  () => fieldIsInteractive(papers, meshProps.preset, meshProps.interactive),
4910
- // eslint-disable-next-line react-hooks/exhaustive-deps
4911
5825
  [JSON.stringify(papers), JSON.stringify(meshProps.preset ?? null), meshProps.interactive]
4912
5826
  );
4913
5827
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className, style: { width: "100%", height: "100%", ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(DropZoneContext.Provider, { value: registry4, children: [
4914
5828
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_fiber6.Canvas, { shadows: true, camera: { position: [0, 0.6, 5.2], fov: 45 }, dpr: [1, 2], children: [
5829
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(FitCamera, { ...meshProps }),
4915
5830
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ambientLight", { intensity: 0.7 }),
4916
5831
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
4917
5832
  "directionalLight",
@@ -4930,202 +5845,762 @@ var PaperField = (0, import_react14.forwardRef)(function PaperField2({ children,
4930
5845
  ] }) });
4931
5846
  });
4932
5847
 
4933
- // src/field/parity.ts
4934
- var THREE13 = __toESM(require("three"), 1);
4935
- var GRID = 48;
4936
- var PARITY_EPSILON = 5e-4;
4937
- var parityCases = [
4938
- {
4939
- name: "roll: defaults",
4940
- stack: [{ type: "roll", options: { angle: 90, boundary: 0, radius: 0.12, spiral: 0.015 } }],
4941
- sheet: { width: 1, height: 1.4 },
4942
- t: 0
4943
- },
4944
- {
4945
- name: "roll: tight receipt roll, rolling down",
4946
- stack: [{ type: "roll", options: { angle: 270, boundary: -0.4, radius: 0.07, spiral: 0.02 } }],
4947
- sheet: { width: 1, height: 2.6 },
4948
- t: 0
4949
- },
4950
- {
4951
- name: "curl: bottom-right peel",
4952
- stack: [{ type: "curl", options: { corner: "bottom-right", amount: 0.45, radius: 0.2, skew: 0 } }],
4953
- sheet: { width: 1.5, height: 1 },
4954
- t: 0
4955
- },
4956
- {
4957
- name: "curl: skewed top-left",
4958
- stack: [{ type: "curl", options: { corner: "top-left", amount: 0.7, radius: 0.3, skew: 15 } }],
4959
- sheet: { width: 1, height: 1 },
4960
- t: 0
4961
- },
4962
- {
4963
- name: "bend: positive arc at an angle",
4964
- stack: [{ type: "bend", options: { curvature: 1.2, angle: 33 } }],
4965
- sheet: { width: 1, height: 1.4 },
4966
- t: 0
4967
- },
4968
- {
4969
- name: "bend: negative arc",
4970
- stack: [{ type: "bend", options: { curvature: -0.8, angle: 0 } }],
4971
- sheet: { width: 1, height: 1.4 },
4972
- t: 0
4973
- },
4974
- {
4975
- name: "fold: 90\xB0 hinge",
4976
- stack: [{ type: "fold", options: { angle: 90, offset: 0, foldAngle: 90, radius: 0.06 } }],
4977
- sheet: { width: 1, height: 1.4 },
4978
- t: 0
4979
- },
4980
- {
4981
- name: "fold: deep fold, travelling down",
4982
- stack: [{ type: "fold", options: { angle: 270, offset: 0.2, foldAngle: 165, radius: 0.04 } }],
4983
- sheet: { width: 1, height: 1.4 },
4984
- t: 0
4985
- },
4986
- {
4987
- name: "wave: free ripple at t=1.234",
4988
- stack: [
5848
+ // src/stage/PaperStage.tsx
5849
+ var THREE17 = __toESM(require("three"), 1);
5850
+ var import_fiber8 = require("@react-three/fiber");
5851
+ var import_react17 = require("react");
5852
+ var import_zod26 = require("zod");
5853
+
5854
+ // src/stage/camera.ts
5855
+ var import_zod23 = require("zod");
5856
+ var shotNames = ["follow", "lead", "low", "wide"];
5857
+ var shotSchema = import_zod23.z.object({
5858
+ shot: import_zod23.z.enum(shotNames).default("follow"),
5859
+ /**
5860
+ * How far the camera stands off the figure ALONG the walk, world units.
5861
+ * `wide` reads it as how far back it stands; how far it steps aside is
5862
+ * derived from the paper, since that is what it has to clear.
5863
+ */
5864
+ distance: import_zod23.z.number().min(0.2).max(40).default(4.5),
5865
+ /** Multiplier on the shot's natural camera height. 1 is as designed. */
5866
+ height: import_zod23.z.number().min(0).max(6).default(1),
5867
+ /** How far up the walk the camera looks past the figure, world units. */
5868
+ lookAhead: import_zod23.z.number().min(0).max(40).default(7),
5869
+ /** Sideways step off the walk line, world units. Positive is the walker's left. */
5870
+ offset: import_zod23.z.number().min(-20).max(20).default(0)
5871
+ });
5872
+ var EYE = {
5873
+ follow: 0.95,
5874
+ lead: 0.95,
5875
+ // Down near the floor, where the banners tower — the worm's-eye of the
5876
+ // reference frames, and the cheapest way to make paper read as architecture.
5877
+ low: 0.12,
5878
+ wide: 1.1
5879
+ };
5880
+ var AIM = {
5881
+ // Chest height on the figure, a third of the way up the paper — enough
5882
+ // tilt that a printed banner reads, not so much that the floor is lost.
5883
+ follow: { figure: 0.62, paper: 0.3 },
5884
+ // Framing the figure itself, so the paper only lifts the aim a little.
5885
+ lead: { figure: 0.62, paper: 0.1 },
5886
+ // Up the banners. The figure is incidental to this shot.
5887
+ low: { figure: 0, paper: 0.62 },
5888
+ wide: { figure: 0.62, paper: 0.2 }
5889
+ };
5890
+ var WIDE_STANDOFF = 1.5;
5891
+ var DEFAULT_PAPER_RATIO = 4.9;
5892
+ function resolveScale(scale) {
5893
+ if (typeof scale === "number") return { figure: scale, paper: scale * DEFAULT_PAPER_RATIO };
5894
+ return scale;
5895
+ }
5896
+ function walkPoint(path, distance) {
5897
+ if (path.length === 0) return path.pointAt(0);
5898
+ if (path.closed) return path.pointAt(distance / path.length);
5899
+ if (distance < 0) {
5900
+ const [x, z27] = path.pointAt(0);
5901
+ const [tx, tz] = path.tangentAt(0);
5902
+ return [x + tx * distance, z27 + tz * distance];
5903
+ }
5904
+ if (distance > path.length) {
5905
+ const over = distance - path.length;
5906
+ const [x, z27] = path.pointAt(1);
5907
+ const [tx, tz] = path.tangentAt(1);
5908
+ return [x + tx * over, z27 + tz * over];
5909
+ }
5910
+ return path.pointAt(distance / path.length);
5911
+ }
5912
+ function walkNormal(path, distance) {
5913
+ if (path.length === 0) return path.normalAt(0);
5914
+ if (path.closed) return path.normalAt(distance / path.length);
5915
+ return path.normalAt(Math.min(Math.max(distance, 0), path.length) / path.length);
5916
+ }
5917
+ function stageCamera(path, walked, scale, options) {
5918
+ const { figure, paper } = resolveScale(scale);
5919
+ const eye = figure * EYE[options.shot] * options.height;
5920
+ const aim = figure * AIM[options.shot].figure + paper * AIM[options.shot].paper;
5921
+ let station;
5922
+ let mark;
5923
+ if (options.shot === "lead") {
5924
+ station = walked + options.distance;
5925
+ mark = walked;
5926
+ } else if (options.shot === "wide") {
5927
+ station = walked - options.distance;
5928
+ mark = walked;
5929
+ } else {
5930
+ station = walked - options.distance;
5931
+ mark = walked + options.lookAhead;
5932
+ }
5933
+ const [sx, sz] = walkPoint(path, station);
5934
+ const [mx, mz] = walkPoint(path, mark);
5935
+ const [nx, nz] = walkNormal(path, station);
5936
+ const step = options.offset + (options.shot === "wide" ? paper * WIDE_STANDOFF : 0);
5937
+ return {
5938
+ position: [sx + nx * step, eye, sz + nz * step],
5939
+ target: [mx, aim, mz]
5940
+ };
5941
+ }
5942
+
5943
+ // src/stage/Figure.tsx
5944
+ var THREE15 = __toESM(require("three"), 1);
5945
+ var import_react15 = require("react");
5946
+ var import_fiber7 = require("@react-three/fiber");
5947
+
5948
+ // src/stage/gait.ts
5949
+ var import_zod24 = require("zod");
5950
+ var figureSchema = import_zod24.z.object({
5951
+ /** Standing height in world units — the scale reference the whole stage is read against. */
5952
+ height: import_zod24.z.number().min(0.5).max(4).default(1.75),
5953
+ /** World units per second along the walk. A relaxed indoor pace is ~1.2. */
5954
+ speed: import_zod24.z.number().min(0).max(4).default(1.2),
5955
+ /** Stride length as a fraction of height — how far one step carries. */
5956
+ stride: import_zod24.z.number().min(0.1).max(1).default(0.42),
5957
+ /** Arm swing, 0..1. Drop it toward 0 for hands-in-pockets stillness. */
5958
+ swing: import_zod24.z.number().min(0).max(1).default(1),
5959
+ /** Silhouette color. Near-black by default: it should read as an absence, not an object. */
5960
+ color: import_zod24.z.string().default("#0a0a0c")
5961
+ });
5962
+ var PROPORTIONS = {
5963
+ hip: 0.53,
5964
+ shoulder: 0.82,
5965
+ headRadius: 0.045,
5966
+ headCenter: 0.935,
5967
+ thigh: 0.245,
5968
+ shin: 0.235,
5969
+ upperArm: 0.185,
5970
+ foreArm: 0.165,
5971
+ torsoWidth: 0.19,
5972
+ hipWidth: 0.095,
5973
+ limbRadius: 0.028
5974
+ };
5975
+ var THIGH_SWING = 0.42;
5976
+ var ARM_SWING = 0.5;
5977
+ var KNEE_FLEX = 1.1;
5978
+ var BOB = 0.016;
5979
+ var LEAN = 0.045;
5980
+ var TAU4 = Math.PI * 2;
5981
+ function cycleLength(o) {
5982
+ return o.stride * o.height * 2;
5983
+ }
5984
+ function figureGait(distance, o) {
5985
+ const cycle = cycleLength(o);
5986
+ const phase = cycle > 0 ? (distance / cycle % 1 + 1) % 1 : 0;
5987
+ const w = phase * TAU4;
5988
+ const leftThigh = THIGH_SWING * Math.sin(w);
5989
+ const rightThigh = THIGH_SWING * Math.sin(w + Math.PI);
5990
+ const flex = (at) => -KNEE_FLEX * Math.max(0, Math.cos(w - at)) ** 1.5;
5991
+ const leftKnee = flex(7 * Math.PI / 4);
5992
+ const rightKnee = flex(3 * Math.PI / 4);
5993
+ const leftArm = -ARM_SWING * o.swing * Math.sin(w);
5994
+ const rightArm = -ARM_SWING * o.swing * Math.sin(w + Math.PI);
5995
+ const bob = -BOB * o.height * (1 - Math.abs(Math.cos(w)));
5996
+ return {
5997
+ phase,
5998
+ bob,
5999
+ lean: LEAN * Math.min(o.speed / 1.2, 1),
6000
+ leftThigh,
6001
+ rightThigh,
6002
+ leftKnee,
6003
+ rightKnee,
6004
+ leftArm,
6005
+ rightArm
6006
+ };
6007
+ }
6008
+ function placeFigure(path, distance, o) {
6009
+ const raw = path.length > 0 ? distance / path.length : 0;
6010
+ const s = path.closed ? (raw % 1 + 1) % 1 : Math.min(Math.max(raw, 0), 1);
6011
+ const [x, z27] = path.pointAt(s);
6012
+ const [tx, tz] = path.tangentAt(s);
6013
+ const travelled = path.closed ? distance : Math.min(distance, path.length);
6014
+ return {
6015
+ position: [x, 0, z27],
6016
+ yaw: Math.atan2(tx, tz),
6017
+ pose: figureGait(travelled, o),
6018
+ s
6019
+ };
6020
+ }
6021
+
6022
+ // src/stage/Figure.tsx
6023
+ var import_jsx_runtime12 = require("react/jsx-runtime");
6024
+ function Segment({ length, radius, material }) {
6025
+ const shaft = Math.max(length - radius * 2, 1e-3);
6026
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("mesh", { position: [0, -length / 2, 0], material, castShadow: true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("capsuleGeometry", { args: [radius, shaft, 4, 10] }) });
6027
+ }
6028
+ function Figure({ path, figure, distance, frozen }) {
6029
+ const reducedMotion = usePrefersReducedMotion();
6030
+ const still = frozen ?? reducedMotion;
6031
+ const options = (0, import_react15.useMemo)(() => figureSchema.parse(figure ?? {}), [figure]);
6032
+ const walk = (0, import_react15.useMemo)(() => getWalkPath(walkPathSchema.parse(path ?? {})), [path]);
6033
+ const root = (0, import_react15.useRef)(null);
6034
+ const hips = (0, import_react15.useRef)(null);
6035
+ const legL = (0, import_react15.useRef)(null);
6036
+ const legR = (0, import_react15.useRef)(null);
6037
+ const kneeL = (0, import_react15.useRef)(null);
6038
+ const kneeR = (0, import_react15.useRef)(null);
6039
+ const armL = (0, import_react15.useRef)(null);
6040
+ const armR = (0, import_react15.useRef)(null);
6041
+ const material = (0, import_react15.useMemo)(
6042
+ () => new THREE15.MeshBasicMaterial({ color: options.color, toneMapped: false }),
6043
+ [options.color]
6044
+ );
6045
+ (0, import_react15.useEffect)(() => () => material.dispose(), [material]);
6046
+ const h = options.height;
6047
+ const p = PROPORTIONS;
6048
+ const torso = (p.shoulder - p.hip) * h;
6049
+ (0, import_fiber7.useFrame)((state) => {
6050
+ const walked = distance ?? (still ? 0 : state.clock.elapsedTime * options.speed);
6051
+ const { position, yaw, pose } = placeFigure(walk, walked, options);
6052
+ root.current?.position.set(position[0], position[1], position[2]);
6053
+ if (root.current) root.current.rotation.y = yaw;
6054
+ if (hips.current) {
6055
+ hips.current.position.y = p.hip * h + (still ? 0 : pose.bob);
6056
+ hips.current.rotation.x = pose.lean;
6057
+ }
6058
+ if (legL.current) legL.current.rotation.x = still ? 0 : -pose.leftThigh;
6059
+ if (legR.current) legR.current.rotation.x = still ? 0 : -pose.rightThigh;
6060
+ if (kneeL.current) kneeL.current.rotation.x = still ? 0 : -pose.leftKnee;
6061
+ if (kneeR.current) kneeR.current.rotation.x = still ? 0 : -pose.rightKnee;
6062
+ if (armL.current) armL.current.rotation.x = still ? 0 : -pose.leftArm;
6063
+ if (armR.current) armR.current.rotation.x = still ? 0 : -pose.rightArm;
6064
+ });
6065
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("group", { ref: root, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("group", { ref: hips, children: [
6066
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("mesh", { position: [0, torso / 2, 0], material, castShadow: true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("capsuleGeometry", { args: [p.torsoWidth * h / 2, torso * 0.72, 4, 12] }) }),
6067
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("mesh", { position: [0, (p.headCenter - p.hip) * h, 0], material, castShadow: true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("sphereGeometry", { args: [p.headRadius * h, 14, 12] }) }),
6068
+ [-1, 1].map((side) => {
6069
+ const leg = side < 0 ? legL : legR;
6070
+ const knee = side < 0 ? kneeL : kneeR;
6071
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("group", { ref: leg, position: [side * p.hipWidth * h / 2, 0, 0], children: [
6072
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Segment, { length: p.thigh * h, radius: p.limbRadius * h, material }),
6073
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("group", { ref: knee, position: [0, -p.thigh * h, 0], children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Segment, { length: p.shin * h, radius: p.limbRadius * h * 0.9, material }) })
6074
+ ] }, `leg${side}`);
6075
+ }),
6076
+ [-1, 1].map((side) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6077
+ "group",
4989
6078
  {
4990
- type: "wave",
4991
- options: { amplitude: 0.05, wavelength: 0.5, speed: 1, angle: 20, pinnedEdge: "none" }
4992
- }
4993
- ],
4994
- sheet: { width: 1, height: 1 },
4995
- t: 1.234
4996
- },
4997
- {
4998
- name: "wave: pinned top at t=2.5",
4999
- stack: [
6079
+ ref: side < 0 ? armL : armR,
6080
+ position: [side * p.torsoWidth * h / 2, torso, 0],
6081
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6082
+ Segment,
6083
+ {
6084
+ length: (p.upperArm + p.foreArm) * h,
6085
+ radius: p.limbRadius * h * 0.8,
6086
+ material
6087
+ }
6088
+ )
6089
+ },
6090
+ `arm${side}`
6091
+ ))
6092
+ ] }) });
6093
+ }
6094
+
6095
+ // src/stage/Surround.tsx
6096
+ var THREE16 = __toESM(require("three"), 1);
6097
+ var import_react16 = require("react");
6098
+ var import_jsx_runtime13 = require("react/jsx-runtime");
6099
+ function makeSkyTexture(horizon, zenith) {
6100
+ const canvas = document.createElement("canvas");
6101
+ canvas.width = 4;
6102
+ canvas.height = 256;
6103
+ const ctx = canvas.getContext("2d");
6104
+ const grade = ctx.createLinearGradient(0, 0, 0, canvas.height);
6105
+ grade.addColorStop(0, zenith);
6106
+ grade.addColorStop(0.35, zenith);
6107
+ grade.addColorStop(0.92, horizon);
6108
+ grade.addColorStop(1, horizon);
6109
+ ctx.fillStyle = grade;
6110
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
6111
+ const texture = new THREE16.CanvasTexture(canvas);
6112
+ texture.colorSpace = THREE16.SRGBColorSpace;
6113
+ return texture;
6114
+ }
6115
+ function makeGlowTexture(color) {
6116
+ const size = 256;
6117
+ const canvas = document.createElement("canvas");
6118
+ canvas.width = canvas.height = size;
6119
+ const ctx = canvas.getContext("2d");
6120
+ const glow = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
6121
+ glow.addColorStop(0, color);
6122
+ glow.addColorStop(0.55, color);
6123
+ glow.addColorStop(1, "rgba(0, 0, 0, 0)");
6124
+ ctx.fillStyle = glow;
6125
+ ctx.fillRect(0, 0, size, size);
6126
+ const texture = new THREE16.CanvasTexture(canvas);
6127
+ texture.colorSpace = THREE16.SRGBColorSpace;
6128
+ return texture;
6129
+ }
6130
+ function Source({
6131
+ size,
6132
+ position,
6133
+ yaw,
6134
+ color
6135
+ }) {
6136
+ const texture = (0, import_react16.useMemo)(() => makeGlowTexture(color), [color]);
6137
+ (0, import_react16.useEffect)(() => () => texture.dispose(), [texture]);
6138
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("mesh", { position, rotation: [0, yaw, 0], children: [
6139
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("planeGeometry", { args: [size * 2.4, size * 1.8] }),
6140
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
6141
+ "meshBasicMaterial",
5000
6142
  {
5001
- type: "wave",
5002
- options: { amplitude: 0.04, wavelength: 0.4, speed: 1.5, angle: 80, pinnedEdge: "top" }
6143
+ map: texture,
6144
+ transparent: true,
6145
+ depthWrite: false,
6146
+ toneMapped: false,
6147
+ fog: false
5003
6148
  }
5004
- ],
5005
- sheet: { width: 1.2, height: 1.5 },
5006
- t: 2.5
5007
- },
5008
- {
5009
- name: "stacked: letter-fold pair (fold \u2218 fold)",
5010
- stack: [
5011
- { type: "fold", options: { angle: 270, offset: 0.2333, foldAngle: 120, radius: 0.05 } },
5012
- { type: "fold", options: { angle: 90, offset: 0.2333, foldAngle: 100, radius: 0.08 } }
5013
- ],
5014
- sheet: { width: 1, height: 1.4 },
5015
- t: 0
5016
- },
5017
- {
5018
- name: "stacked: bend \u2218 roll \u2218 wave",
5019
- stack: [
5020
- { type: "bend", options: { curvature: 0.6, angle: 0 } },
5021
- { type: "roll", options: { angle: 90, boundary: 0.1, radius: 0.15, spiral: 0 } },
6149
+ )
6150
+ ] });
6151
+ }
6152
+ function Surround({ radius, horizon, zenith }) {
6153
+ const texture = (0, import_react16.useMemo)(() => makeSkyTexture(horizon, zenith), [horizon, zenith]);
6154
+ (0, import_react16.useEffect)(() => () => texture.dispose(), [texture]);
6155
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("mesh", { children: [
6156
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("sphereGeometry", { args: [radius, 32, 24] }),
6157
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("meshBasicMaterial", { map: texture, side: THREE16.BackSide, fog: false })
6158
+ ] });
6159
+ }
6160
+
6161
+ // src/stage/schema.ts
6162
+ var import_zod25 = require("zod");
6163
+ var stageSourceSchema = import_zod25.z.object({
6164
+ /** The bright void the walk resolves toward. Without it the vanishing point is a hole. */
6165
+ enabled: import_zod25.z.boolean().default(true),
6166
+ color: import_zod25.z.string().default("#fff4e2"),
6167
+ /** How far past the end of the walk it stands, world units. */
6168
+ beyond: import_zod25.z.number().min(0).max(80).default(10),
6169
+ /**
6170
+ * A cyclorama around the whole stage, graded from the source colour at the
6171
+ * horizon to near-dark overhead. The source plane only faces down the walk,
6172
+ * so without this every shot that isn't axial — `wide` especially — looks
6173
+ * out at a black void where the room should be.
6174
+ */
6175
+ surround: import_zod25.z.boolean().default(true),
6176
+ /** Colour overhead. The horizon takes the source's own colour. */
6177
+ zenith: import_zod25.z.string().default("#241c17"),
6178
+ /** Size, as a multiple of the PAPER height — it only has to out-fill the frame. */
6179
+ spread: import_zod25.z.number().min(1).max(60).default(5)
6180
+ });
6181
+ var stageGroundSchema = import_zod25.z.object({
6182
+ /** The floor. Without something to catch the shadows there is no ground and no scale. */
6183
+ enabled: import_zod25.z.boolean().default(true),
6184
+ color: import_zod25.z.string().default("#0e0b09")
6185
+ });
6186
+ var stageSchema = import_zod25.z.object({
6187
+ path: walkPathSchema.default({}),
6188
+ shot: shotSchema.default({}),
6189
+ figure: figureSchema.default({}),
6190
+ /** Stage mode is built for `nave`; the others are all front-lit. */
6191
+ lighting: import_zod25.z.enum(lightingNames).default("nave"),
6192
+ showFigure: import_zod25.z.boolean().default(true),
6193
+ source: stageSourceSchema.default({}),
6194
+ ground: stageGroundSchema.default({})
6195
+ });
6196
+
6197
+ // src/stage/quality.ts
6198
+ var qualityNames = ["auto", "low", "medium", "high"];
6199
+ var qualityTiers = {
6200
+ /** Anything with a GPU. */
6201
+ high: { dpr: 2, shadowMapSize: 2048, segments: 72, surround: true, contactShadow: true },
6202
+ /** The default worth aiming at: an integrated laptop GPU from the last few years. */
6203
+ medium: { dpr: 1.5, shadowMapSize: 1024, segments: 48, surround: true, contactShadow: false },
6204
+ /**
6205
+ * Old integrated graphics, a throttled phone, a software rasterizer. The
6206
+ * scene still READS — banners, figure, backlight, walk — it just stops
6207
+ * paying for the parts nobody would miss at this framerate.
6208
+ */
6209
+ low: { dpr: 1, shadowMapSize: 0, segments: 28, surround: true, contactShadow: false }
6210
+ };
6211
+ var INITIAL_TIER = "medium";
6212
+ var FIRST_WINDOW = 20;
6213
+ var STEADY_WINDOW = 60;
6214
+ var SETTLE_FRAMES = 45;
6215
+ var TIER_ORDER = ["low", "medium", "high"];
6216
+ function qualityFor(name) {
6217
+ return qualityTiers[name === "auto" ? INITIAL_TIER : name];
6218
+ }
6219
+ function tierUp(tier) {
6220
+ return TIER_ORDER[Math.min(TIER_ORDER.indexOf(tier) + 1, TIER_ORDER.length - 1)];
6221
+ }
6222
+ function tierDown(tier) {
6223
+ return TIER_ORDER[Math.max(TIER_ORDER.indexOf(tier) - 1, 0)];
6224
+ }
6225
+
6226
+ // src/stage/PaperStage.tsx
6227
+ var import_jsx_runtime14 = require("react/jsx-runtime");
6228
+ var BANNER = {
6229
+ sheet: { width: 1.5, height: 8.5, segments: "auto" },
6230
+ stock: "vellum",
6231
+ surface: { grain: 0.22 },
6232
+ deformers: [{ type: "drape", options: { amplitude: 0.16, folds: 3, falloff: 1.7, gather: 0.28 } }]
6233
+ };
6234
+ function splitAcrossBanners(text, banners) {
6235
+ const words = text.split(/\s+/).filter(Boolean);
6236
+ if (words.length === 0 || banners <= 0) return [];
6237
+ const per = Math.ceil(words.length / banners);
6238
+ const out = [];
6239
+ for (let i = 0; i < words.length; i += per) out.push(words.slice(i, i + per).join("\n"));
6240
+ return out;
6241
+ }
6242
+ function bannerTextSize(lines) {
6243
+ return Math.round(Math.min(150, Math.max(26, 720 / Math.max(lines, 1))));
6244
+ }
6245
+ function ShotRig({
6246
+ stage,
6247
+ paperHeight,
6248
+ progress,
6249
+ still
6250
+ }) {
6251
+ const camera = (0, import_fiber8.useThree)((s) => s.camera);
6252
+ const path = (0, import_react17.useMemo)(() => getWalkPath(stage.path), [stage.path]);
6253
+ const scale = (0, import_react17.useMemo)(
6254
+ () => ({ figure: stage.figure.height, paper: paperHeight }),
6255
+ [stage.figure.height, paperHeight]
6256
+ );
6257
+ (0, import_fiber8.useFrame)((state) => {
6258
+ const walked = stageWalked(path.length, stage, progress, still ? 0 : state.clock.elapsedTime);
6259
+ const { position, target } = stageCamera(path, walked, scale, stage.shot);
6260
+ camera.position.set(position[0], position[1], position[2]);
6261
+ camera.lookAt(target[0], target[1], target[2]);
6262
+ });
6263
+ return null;
6264
+ }
6265
+ function stageWalked(length, stage, progress, elapsed) {
6266
+ if (progress !== void 0) return progress * length;
6267
+ return elapsed * stage.figure.speed;
6268
+ }
6269
+ var FLOOR_FPS = 26;
6270
+ var CEILING_FPS = 55;
6271
+ function QualityWatch({ tier, onChange }) {
6272
+ const samples = (0, import_react17.useRef)([]);
6273
+ const settle = (0, import_react17.useRef)(SETTLE_FRAMES);
6274
+ const window2 = (0, import_react17.useRef)(FIRST_WINDOW);
6275
+ const settled = (0, import_react17.useCallback)((next) => {
6276
+ samples.current = [];
6277
+ settle.current = SETTLE_FRAMES;
6278
+ window2.current = STEADY_WINDOW;
6279
+ return next;
6280
+ }, []);
6281
+ (0, import_fiber8.useFrame)((_, delta) => {
6282
+ if (settle.current > 0) {
6283
+ settle.current -= 1;
6284
+ return;
6285
+ }
6286
+ if (delta > 0.5) return;
6287
+ samples.current.push(delta);
6288
+ if (samples.current.length < window2.current) return;
6289
+ const sorted = [...samples.current].sort((a, b) => a - b);
6290
+ const median = sorted[Math.floor(sorted.length / 2)];
6291
+ const fps = 1 / median;
6292
+ samples.current = [];
6293
+ window2.current = STEADY_WINDOW;
6294
+ if (fps < FLOOR_FPS) {
6295
+ const next = tierDown(tier);
6296
+ if (next !== tier) onChange(settled(next));
6297
+ } else if (fps > CEILING_FPS) {
6298
+ const next = tierUp(tier);
6299
+ if (next !== tier) onChange(settled(next));
6300
+ }
6301
+ });
6302
+ return null;
6303
+ }
6304
+ function PaperStageScene({
6305
+ stage: stageInput,
6306
+ quality = "auto",
6307
+ onQualityChange,
6308
+ layout = "colonnade",
6309
+ layoutOptions,
6310
+ papers,
6311
+ images,
6312
+ text,
6313
+ preset,
6314
+ count = 22,
6315
+ progress,
6316
+ reducedMotion
6317
+ }) {
6318
+ const still = usePrefersReducedMotion(reducedMotion);
6319
+ const [tier, setTier] = (0, import_react17.useState)(quality === "auto" ? INITIAL_TIER : quality);
6320
+ (0, import_react17.useEffect)(() => {
6321
+ if (quality !== "auto") setTier(quality);
6322
+ }, [quality]);
6323
+ (0, import_react17.useEffect)(() => {
6324
+ onQualityChange?.(tier);
6325
+ }, [tier, onQualityChange]);
6326
+ const settings = quality === "auto" ? qualityTiers[tier] : qualityFor(quality);
6327
+ const stage = (0, import_react17.useMemo)(() => stageSchema.parse(stageInput ?? {}), [stageInput]);
6328
+ const path = (0, import_react17.useMemo)(() => getWalkPath(stage.path), [stage.path]);
6329
+ const paperHeight = (0, import_react17.useMemo)(() => resolveConfig({ preset: preset ?? BANNER }).sheet.height, [preset]);
6330
+ const paper = (0, import_react17.useMemo)(() => {
6331
+ const base = preset ?? BANNER;
6332
+ if (typeof base !== "object") return preset ?? BANNER;
6333
+ const sheet2 = base.sheet ?? {};
6334
+ return { ...base, sheet: { ...sheet2, segments: settings.segments } };
6335
+ }, [preset, settings.segments]);
6336
+ const resolvedLayoutOptions = (0, import_react17.useMemo)(() => {
6337
+ const schema = getLayout(layout).optionsSchema;
6338
+ const takesPath = schema instanceof import_zod26.z.ZodObject && "path" in schema.shape;
6339
+ return takesPath ? { ...layoutOptions, path: stage.path } : layoutOptions;
6340
+ }, [layout, layoutOptions, stage.path]);
6341
+ const slots = (0, import_react17.useMemo)(() => {
6342
+ if (papers) return papers;
6343
+ if (images) return void 0;
6344
+ if (text !== void 0) {
6345
+ const columns = Array.isArray(text) ? text : splitAcrossBanners(text, count);
6346
+ const longest = columns.reduce((n, c) => Math.max(n, c.split("\n").length), 1);
6347
+ const size = bannerTextSize(longest);
6348
+ return columns.map((column) => ({
6349
+ content: {
6350
+ type: "text",
6351
+ text: column,
6352
+ size,
6353
+ align: "center",
6354
+ color: "#241f1a",
6355
+ lineHeight: 1.25,
6356
+ font: 'Georgia, "Times New Roman", serif',
6357
+ weight: 400,
6358
+ padding: 0.06
6359
+ }
6360
+ }));
6361
+ }
6362
+ return Array.from({ length: count }, () => ({}));
6363
+ }, [papers, images, text, count]);
6364
+ const figureDistance = progress !== void 0 ? progress * path.length : void 0;
6365
+ const surroundRadius = (0, import_react17.useMemo)(
6366
+ () => Math.max(path.length * 1.6, paperHeight * 9),
6367
+ [path.length, paperHeight]
6368
+ );
6369
+ const source = (0, import_react17.useMemo)(() => {
6370
+ const [x, z27] = walkPoint(path, path.length + stage.source.beyond);
6371
+ const [tx, tz] = path.tangentAt(1);
6372
+ const size = paperHeight * stage.source.spread;
6373
+ return { position: [x, size * 0.35, z27], yaw: Math.atan2(-tx, -tz), size };
6374
+ }, [path, stage.source.beyond, stage.source.spread, paperHeight]);
6375
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
6376
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ShotRig, { stage, paperHeight, progress, still }),
6377
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
6378
+ PaperLighting,
5022
6379
  {
5023
- type: "wave",
5024
- options: { amplitude: 0.02, wavelength: 0.6, speed: 0.7, angle: 45, pinnedEdge: "none" }
6380
+ preset: stage.lighting,
6381
+ floor: 0,
6382
+ scale: 60,
6383
+ reducedMotion,
6384
+ shadowMapSize: settings.shadowMapSize,
6385
+ contactShadow: settings.contactShadow
5025
6386
  }
5026
- ],
5027
- sheet: { width: 1, height: 1.4 },
5028
- t: 0.8
5029
- }
5030
- ];
5031
- function buildParityFragment(stack, sheet2) {
5032
- const composed = buildDisplacementGLSL(stack, sheet2);
5033
- return (
5034
- /* glsl */
5035
- `#version 300 es
5036
- precision highp float;
5037
- uniform float uPlTime;
5038
- ${composed.functionsSrc}
5039
- ${composed.displaceSrc}
5040
- out vec4 outColor;
5041
- void main() {
5042
- vec2 uv = (gl_FragCoord.xy - 0.5) / float(${GRID - 1});
5043
- vec3 p = vec3((uv - 0.5) * uSheet, 0.0);
5044
- outColor = vec4(plDisplace(p, uv, uPlTime), 1.0);
6387
+ ),
6388
+ quality === "auto" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(QualityWatch, { tier, onChange: setTier }),
6389
+ stage.source.surround && settings.surround && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Surround, { radius: surroundRadius, horizon: stage.source.color, zenith: stage.source.zenith }),
6390
+ stage.source.enabled && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Source, { size: source.size, position: source.position, yaw: source.yaw, color: stage.source.color }),
6391
+ stage.ground.enabled && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("mesh", { rotation: [-Math.PI / 2, 0, 0], receiveShadow: true, children: [
6392
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("planeGeometry", { args: [surroundRadius * 1.3, surroundRadius * 1.3] }),
6393
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("meshStandardMaterial", { color: stage.ground.color, roughness: 1 })
6394
+ ] }),
6395
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
6396
+ PaperFieldMesh,
6397
+ {
6398
+ preset: paper,
6399
+ papers: slots,
6400
+ images,
6401
+ layout,
6402
+ layoutOptions: resolvedLayoutOptions,
6403
+ motion: { driver: "none" },
6404
+ entrance: { type: "none" },
6405
+ reducedMotion
6406
+ }
6407
+ ),
6408
+ stage.showFigure && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Figure, { path: stage.path, figure: stage.figure, distance: figureDistance, frozen: reducedMotion })
6409
+ ] });
5045
6410
  }
5046
- `
5047
- );
6411
+ function PaperStage({ children, className, style, ...sceneProps }) {
6412
+ const dpr = qualityFor(sceneProps.quality ?? "auto").dpr;
6413
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className, style: { width: "100%", height: "100%", ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
6414
+ import_fiber8.Canvas,
6415
+ {
6416
+ shadows: true,
6417
+ dpr: [1, dpr],
6418
+ camera: { fov: 38, near: 0.05, far: 400 },
6419
+ onCreated: ({ gl, scene }) => {
6420
+ gl.toneMapping = THREE17.ACESFilmicToneMapping;
6421
+ scene.background = new THREE17.Color("#0c0a0b");
6422
+ },
6423
+ children: [
6424
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PaperStageScene, { ...sceneProps }),
6425
+ children
6426
+ ]
6427
+ }
6428
+ ) });
5048
6429
  }
5049
- var VERT = `#version 300 es
5050
- in vec2 aPos;
5051
- void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
5052
- `;
5053
- function compile(gl, type, src) {
5054
- const shader = gl.createShader(type);
5055
- gl.shaderSource(shader, src);
5056
- gl.compileShader(shader);
5057
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
5058
- throw new Error(`[paperlab parity] shader compile failed: ${gl.getShaderInfoLog(shader)}`);
6430
+
6431
+ // src/stage/walks.ts
6432
+ var walkNames = ["straight", "bend", "ess", "ring", "spiral"];
6433
+ var walks = {
6434
+ /** Straight down the nave, away from the camera. The reference shot. */
6435
+ straight: {
6436
+ points: [
6437
+ [0, 16],
6438
+ [0, -20]
6439
+ ],
6440
+ closed: false
6441
+ },
6442
+ /** One long curve, so the far end of the colonnade stays hidden until you reach it. */
6443
+ bend: {
6444
+ points: [
6445
+ [-2, 16],
6446
+ [0, 6],
6447
+ [5, -3],
6448
+ [12, -10]
6449
+ ],
6450
+ closed: false
6451
+ },
6452
+ /** Two opposed curves — the walk turns twice and the banners turn with it. */
6453
+ ess: {
6454
+ points: [
6455
+ [6, 17],
6456
+ [-3, 7],
6457
+ [3, -5],
6458
+ [-6, -17]
6459
+ ],
6460
+ closed: false
6461
+ },
6462
+ /** A closed loop: the only walk `phase` can slide, and the only endless one. */
6463
+ ring: {
6464
+ points: [
6465
+ [11, 0],
6466
+ [0, 11],
6467
+ [-11, 0],
6468
+ [0, -11]
6469
+ ],
6470
+ closed: true
6471
+ },
6472
+ /** Inward and tightening — the space closes as the figure goes deeper. */
6473
+ spiral: {
6474
+ points: [
6475
+ [14, 2],
6476
+ [2, 13],
6477
+ [-11, 1],
6478
+ [-1, -9],
6479
+ [7, -2],
6480
+ [1, 4]
6481
+ ],
6482
+ closed: false
5059
6483
  }
5060
- return shader;
6484
+ };
6485
+ function getWalk(name) {
6486
+ return walks[name];
5061
6487
  }
5062
- function runCaseOnGPU(gl, c) {
5063
- const composed = buildDisplacementGLSL(c.stack, c.sheet);
5064
- const program = gl.createProgram();
5065
- gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERT));
5066
- gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, buildParityFragment(c.stack, c.sheet)));
5067
- gl.linkProgram(program);
5068
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
5069
- throw new Error(`[paperlab parity] link failed: ${gl.getProgramInfoLog(program)}`);
6488
+
6489
+ // src/stage/presets.ts
6490
+ var banner = (width, height, drape2 = {}) => ({
6491
+ sheet: { width, height, segments: "auto" },
6492
+ stock: "vellum",
6493
+ surface: { grain: 0.22 },
6494
+ deformers: [
6495
+ { type: "drape", options: { amplitude: 0.16, folds: 3, falloff: 1.7, gather: 0.28, ...drape2 } }
6496
+ ]
6497
+ });
6498
+ var stagePresets = {
6499
+ nave: {
6500
+ id: "nave",
6501
+ label: "Nave",
6502
+ description: "A straight aisle of hanging banners, lit from the far end.",
6503
+ stage: {
6504
+ path: walks.straight,
6505
+ shot: { shot: "follow", distance: 5, lookAhead: 12, offset: 1.5 },
6506
+ lighting: "nave"
6507
+ },
6508
+ layout: "colonnade",
6509
+ layoutOptions: { aisle: 2.6, twist: 22, drape: 0.6, rise: 0.3 },
6510
+ paper: banner(1.5, 8.5),
6511
+ count: 18,
6512
+ text: "the paper remembers every hand that folded it and every room it was carried through"
6513
+ },
6514
+ procession: {
6515
+ id: "procession",
6516
+ label: "Procession",
6517
+ description: "The walk turns twice, so the far end stays hidden until you reach it.",
6518
+ stage: {
6519
+ path: walks.ess,
6520
+ shot: { shot: "low", distance: 4, lookAhead: 9, offset: 1.1 },
6521
+ lighting: "nave",
6522
+ figure: { speed: 1.05 }
6523
+ },
6524
+ layout: "colonnade",
6525
+ layoutOptions: { aisle: 2.2, twist: 34, breathe: 0.45, drape: 0.7 },
6526
+ paper: banner(1.3, 9.5, { folds: 4, amplitude: 0.2 }),
6527
+ count: 28,
6528
+ text: "every letter you did not send is still folded somewhere in the dark waiting to be read aloud"
6529
+ },
6530
+ cloister: {
6531
+ id: "cloister",
6532
+ label: "Cloister",
6533
+ description: "A closed loop. The figure walks it forever and the banners drift past.",
6534
+ stage: {
6535
+ path: walks.ring,
6536
+ shot: { shot: "follow", distance: 4.5, lookAhead: 8, offset: 1.2 },
6537
+ lighting: "nave"
6538
+ },
6539
+ layout: "colonnade",
6540
+ layoutOptions: { aisle: 2.4, twist: 18, rise: 0.22 },
6541
+ paper: banner(1.6, 7.5),
6542
+ count: 24,
6543
+ text: "around and around and the same words come back changed"
6544
+ },
6545
+ threshold: {
6546
+ id: "threshold",
6547
+ label: "Threshold",
6548
+ description: "A few enormous sheets, wide enough apart to walk between and read.",
6549
+ stage: {
6550
+ // Its own short walk. A colonnade spreads over the WHOLE path whatever
6551
+ // it is populating, so ten banners on the default 36-unit walk stand
6552
+ // seven apart and the shot looks down an empty corridor.
6553
+ path: {
6554
+ points: [
6555
+ [0, 9],
6556
+ [0, -11]
6557
+ ],
6558
+ closed: false
6559
+ },
6560
+ // The aisle has to stay inside the frustum at the distance the shot
6561
+ // stands: paper half a frame-width off the walk line is paper you
6562
+ // never see. `lead` fails here for the same reason and worse.
6563
+ shot: { shot: "follow", distance: 6.5, lookAhead: 9, offset: 0.9 },
6564
+ lighting: "nave",
6565
+ figure: { speed: 0.85 },
6566
+ source: { spread: 4 }
6567
+ },
6568
+ layout: "colonnade",
6569
+ layoutOptions: { aisle: 2.4, twist: 14, breathe: 0.18, margin: 0.12, rise: 0.2 },
6570
+ paper: banner(2.6, 10, { folds: 2, amplitude: 0.24, falloff: 2 }),
6571
+ count: 10,
6572
+ text: "stand closer and read what it cost to write this down"
6573
+ },
6574
+ archive: {
6575
+ id: "archive",
6576
+ label: "Archive",
6577
+ description: "Narrow strips packed tight \u2014 a corridor of records you edge through.",
6578
+ stage: {
6579
+ path: walks.bend,
6580
+ // Far enough back that the figure reads as small; a `low` camera
6581
+ // three units behind a body is all body.
6582
+ shot: { shot: "low", distance: 8, lookAhead: 13, offset: 0.5 },
6583
+ lighting: "nave",
6584
+ ground: { color: "#0b0908" }
6585
+ },
6586
+ layout: "colonnade",
6587
+ layoutOptions: { aisle: 1.7, twist: 44, breathe: 0.5, drape: 0.75, rise: 0.4 },
6588
+ paper: banner(0.85, 11, { folds: 2, amplitude: 0.12 }),
6589
+ count: 44,
6590
+ text: "catalogued indexed cross referenced filed and never once opened by anyone at all"
5070
6591
  }
5071
- gl.useProgram(program);
5072
- for (const [name, value] of Object.entries(composed.uniforms)) {
5073
- const loc = gl.getUniformLocation(program, name);
5074
- if (!loc) continue;
5075
- if (typeof value === "number") gl.uniform1f(loc, value);
5076
- else if (value.length === 2) gl.uniform2f(loc, value[0], value[1]);
5077
- else if (value.length === 3) gl.uniform3f(loc, value[0], value[1], value[2]);
5078
- else gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
6592
+ };
6593
+ function getStagePreset(id) {
6594
+ const preset = stagePresets[id];
6595
+ if (!preset) {
6596
+ throw new Error(
6597
+ `[paperlab] Unknown stage preset "${id}". Available: ${Object.keys(stagePresets).join(", ")}`
6598
+ );
5079
6599
  }
5080
- const timeLoc = gl.getUniformLocation(program, "uPlTime");
5081
- if (timeLoc) gl.uniform1f(timeLoc, c.t);
5082
- const quad = gl.createBuffer();
5083
- gl.bindBuffer(gl.ARRAY_BUFFER, quad);
5084
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
5085
- const posLoc = gl.getAttribLocation(program, "aPos");
5086
- gl.enableVertexAttribArray(posLoc);
5087
- gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
5088
- const tex = gl.createTexture();
5089
- gl.bindTexture(gl.TEXTURE_2D, tex);
5090
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, GRID, GRID, 0, gl.RGBA, gl.FLOAT, null);
5091
- const fbo = gl.createFramebuffer();
5092
- gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
5093
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
5094
- gl.viewport(0, 0, GRID, GRID);
5095
- gl.drawArrays(gl.TRIANGLES, 0, 3);
5096
- const out = new Float32Array(GRID * GRID * 4);
5097
- gl.readPixels(0, 0, GRID, GRID, gl.RGBA, gl.FLOAT, out);
5098
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
5099
- return out;
6600
+ return preset;
5100
6601
  }
5101
- function runParityHarness(canvas) {
5102
- const cnv = canvas ?? document.createElement("canvas");
5103
- const gl = cnv.getContext("webgl2");
5104
- if (!gl) throw new Error("[paperlab parity] WebGL2 unavailable");
5105
- if (!gl.getExtension("EXT_color_buffer_float")) {
5106
- throw new Error("[paperlab parity] EXT_color_buffer_float unavailable");
5107
- }
5108
- const point = new THREE13.Vector3();
5109
- return parityCases.map((c) => {
5110
- const gpu = runCaseOnGPU(gl, c);
5111
- let maxError = 0;
5112
- for (let row = 0; row < GRID; row++) {
5113
- for (let col = 0; col < GRID; col++) {
5114
- const u = col / (GRID - 1);
5115
- const v = row / (GRID - 1);
5116
- point.set((u - 0.5) * c.sheet.width, (v - 0.5) * c.sheet.height, 0);
5117
- displacePoint(point, u, v, c.stack, { t: c.t, sheet: c.sheet });
5118
- const i4 = (row * GRID + col) * 4;
5119
- maxError = Math.max(
5120
- maxError,
5121
- Math.abs(gpu[i4] - point.x),
5122
- Math.abs(gpu[i4 + 1] - point.y),
5123
- Math.abs(gpu[i4 + 2] - point.z)
5124
- );
5125
- }
5126
- }
5127
- return { name: c.name, maxError, pass: maxError < PARITY_EPSILON };
5128
- });
6602
+ function listStagePresets() {
6603
+ return Object.keys(stagePresets);
5129
6604
  }
5130
6605
 
5131
6606
  // src/config/diff.ts
@@ -5204,7 +6679,8 @@ var BEHAVIOR_PHRASES = {
5204
6679
  fly: () => "arched and fluttering like it is airborne",
5205
6680
  fall: () => "rippling with one corner lifted, like a dropped sheet",
5206
6681
  carry: () => "drooping from a pinched corner, fluttering as if being carried",
5207
- flight: (o) => o.path === "loop" ? "tumbling through a seamless airborne loop" : "tumbling across the scene on the wind"
6682
+ flight: (o) => o.path === "loop" ? "tumbling through a seamless airborne loop" : "tumbling across the scene on the wind",
6683
+ crumple: (o) => o.progress < 0.3 ? "lightly handled \u2014 a few soft creases across it" : o.progress < 0.7 ? "crushed into irregular creased facets, as if screwed up and flattened out again" : "balled up in a fist"
5208
6684
  };
5209
6685
  function describeConfig(config) {
5210
6686
  const stock = getStock(config.stock);
@@ -5269,6 +6745,498 @@ Constraints: don't modify the preset values; three >= 0.160 and React 19 are
5269
6745
  required; the component needs no props.`;
5270
6746
  }
5271
6747
 
6748
+ // src/stage/export.ts
6749
+ var SCROLL_HEIGHTS = 4;
6750
+ function walkNameFor(path) {
6751
+ const key = JSON.stringify({ points: path.points, closed: path.closed });
6752
+ return walkNames.find(
6753
+ (name) => JSON.stringify({ points: walks[name].points, closed: walks[name].closed }) === key
6754
+ );
6755
+ }
6756
+ function stripDefaults(value, defaults) {
6757
+ if (Array.isArray(value) || Array.isArray(defaults)) {
6758
+ return JSON.stringify(value) === JSON.stringify(defaults) ? void 0 : value;
6759
+ }
6760
+ if (value && defaults && typeof value === "object" && typeof defaults === "object") {
6761
+ const out = {};
6762
+ for (const [key, child] of Object.entries(value)) {
6763
+ const kept = stripDefaults(child, defaults[key]);
6764
+ if (kept !== void 0) out[key] = kept;
6765
+ }
6766
+ return Object.keys(out).length > 0 ? out : void 0;
6767
+ }
6768
+ return value === defaults ? void 0 : value;
6769
+ }
6770
+ function diffStage(stage) {
6771
+ const resolved = stageSchema.parse(stage);
6772
+ const defaults = stageSchema.parse({});
6773
+ const diff = stripDefaults(resolved, defaults) ?? {};
6774
+ if (diff.path !== void 0) diff.path = resolved.path;
6775
+ return diff;
6776
+ }
6777
+ function stringifyStage(value, indent = 0) {
6778
+ const pad = " ".repeat(indent);
6779
+ const inner = " ".repeat(indent + 1);
6780
+ if (Array.isArray(value)) {
6781
+ if (value.length === 0) return "[]";
6782
+ if (value.every((v) => typeof v === "number")) return `[${value.join(", ")}]`;
6783
+ const items = value.map((v) => `${inner}${stringifyStage(v, indent + 1)}`);
6784
+ return `[
6785
+ ${items.join(",\n")}
6786
+ ${pad}]`;
6787
+ }
6788
+ if (value && typeof value === "object") {
6789
+ const entries = Object.entries(value);
6790
+ if (entries.length === 0) return "{}";
6791
+ const items = entries.map(([k, v]) => `${inner}${JSON.stringify(k)}: ${stringifyStage(v, indent + 1)}`);
6792
+ return `{
6793
+ ${items.join(",\n")}
6794
+ ${pad}}`;
6795
+ }
6796
+ return JSON.stringify(value);
6797
+ }
6798
+ var SHOT_PHRASES = {
6799
+ follow: "from behind and a little above them, looking up the walk",
6800
+ lead: "from in front, walking backward as they come on",
6801
+ low: "from down at floor level, looking up the banners",
6802
+ wide: "from off to one side, level with them"
6803
+ };
6804
+ function describeStage(input) {
6805
+ const stage = stageSchema.parse(input.stage);
6806
+ const count = input.count ?? 22;
6807
+ const walk = walkNameFor(stage.path);
6808
+ const parts = [];
6809
+ const shape = walk === "straight" || walk === void 0 ? "a straight walk" : walk === "ring" ? "a closed loop of a walk" : `an "${walk}" walk that curves as it goes`;
6810
+ parts.push(`${count} tall paper banners standing along ${shape}`);
6811
+ if (input.text?.trim()) {
6812
+ parts.push("each printed with a column of your text running down it");
6813
+ }
6814
+ if (stage.showFigure) {
6815
+ parts.push(`a small dark figure walking between them, seen ${SHOT_PHRASES[stage.shot.shot]}`);
6816
+ }
6817
+ parts.push(
6818
+ stage.lighting === "nave" ? "the whole space dim and lit from behind, so the paper glows and the far end of the walk is a bright void" : `lit with the "${stage.lighting}" preset`
6819
+ );
6820
+ if (input.scroll) parts.push("and scrolling the page walks the figure deeper into it");
6821
+ return parts.join(", ");
6822
+ }
6823
+ function propLines(input, indent) {
6824
+ const lines = [];
6825
+ if (input.paper) lines.push(`${indent}preset={banner}`);
6826
+ if (input.text?.trim()) lines.push(`${indent}text={text}`);
6827
+ if (input.count !== void 0) lines.push(`${indent}count={${input.count}}`);
6828
+ if (input.layout !== "colonnade") lines.push(`${indent}layout="${input.layout}"`);
6829
+ const layoutOptions = input.layoutOptions ?? {};
6830
+ const layoutDefaults = getLayout(input.layout).defaults;
6831
+ const changed = {};
6832
+ for (const [key, value] of Object.entries(layoutOptions)) {
6833
+ if (JSON.stringify(value) !== JSON.stringify(layoutDefaults[key])) changed[key] = value;
6834
+ }
6835
+ if (Object.keys(changed).length > 0) {
6836
+ lines.push(`${indent}layoutOptions={${stringifyStage(changed).replace(/\n\s*/g, " ")}}`);
6837
+ }
6838
+ lines.push(`${indent}stage={stage}`);
6839
+ return lines.join("\n");
6840
+ }
6841
+ function buildStageComponentSource(input) {
6842
+ const name = input.componentName ?? "PaperNave";
6843
+ const stage = diffStage(input.stage);
6844
+ const stageConst = `const stage = ${stringifyStage(stage)} satisfies StageConfigInput`;
6845
+ const bannerConst = input.paper ? `
6846
+
6847
+ const banner = ${stringifyStage(diffConfig(paperConfigSchema.parse(input.paper)))} satisfies PaperConfigInput` : "";
6848
+ const textConst = input.text?.trim() ? `
6849
+
6850
+ const text = ${JSON.stringify(input.text)}` : "";
6851
+ if (!input.scroll) {
6852
+ return `import { PaperStage, type StageConfigInput${input.paper ? ", type PaperConfigInput" : ""} } from 'paperlab'
6853
+
6854
+ ${stageConst}${bannerConst}${textConst}
6855
+
6856
+ export function ${name}() {
6857
+ return (
6858
+ <PaperStage
6859
+ ${propLines(input, " ")}
6860
+ />
6861
+ )
6862
+ }`;
6863
+ }
6864
+ return `import { useEffect, useRef, useState } from 'react'
6865
+ import { PaperStage, type StageConfigInput${input.paper ? ", type PaperConfigInput" : ""} } from 'paperlab'
6866
+
6867
+ ${stageConst}${bannerConst}${textConst}
6868
+
6869
+ export function ${name}() {
6870
+ const ref = useRef<HTMLDivElement>(null)
6871
+ const [progress, setProgress] = useState(0)
6872
+
6873
+ // Scroll the section, walk the figure. The stage is pinned for the height
6874
+ // of the section, so the page scrolling past it IS the walk.
6875
+ useEffect(() => {
6876
+ const el = ref.current
6877
+ if (!el) return
6878
+ const onScroll = () => {
6879
+ const { top, height } = el.getBoundingClientRect()
6880
+ const travel = Math.max(height - window.innerHeight, 1)
6881
+ setProgress(Math.min(Math.max(-top / travel, 0), 1))
6882
+ }
6883
+ onScroll()
6884
+ window.addEventListener('scroll', onScroll, { passive: true })
6885
+ window.addEventListener('resize', onScroll)
6886
+ return () => {
6887
+ window.removeEventListener('scroll', onScroll)
6888
+ window.removeEventListener('resize', onScroll)
6889
+ }
6890
+ }, [])
6891
+
6892
+ return (
6893
+ <div ref={ref} style={{ height: '${SCROLL_HEIGHTS * 100}vh' }}>
6894
+ <div style={{ position: 'sticky', top: 0, height: '100vh' }}>
6895
+ <PaperStage
6896
+ ${propLines(input, " ")}
6897
+ progress={progress}
6898
+ />
6899
+ </div>
6900
+ </div>
6901
+ )
6902
+ }`;
6903
+ }
6904
+ function buildStageAgentPayload(input) {
6905
+ const name = input.componentName ?? "PaperNave";
6906
+ const sizing = input.scroll ? `4. Sizing: the component brings its own height \u2014 it reserves ${SCROLL_HEIGHTS} viewport
6907
+ heights of scroll and pins the canvas inside that. Drop it into the page
6908
+ flow as a section; do NOT wrap it in a fixed-height container.` : `4. Sizing: the component fills its parent container. Place it where I ask;
6909
+ give the parent an explicit height.`;
6910
+ return `Integrate a Paperlab stage \u2014 paper as architecture, with a figure walking through it \u2014 into this project. (paperlab agent-payload v${AGENT_PAYLOAD_VERSION})
6911
+
6912
+ 1. Install the dependencies:
6913
+
6914
+ npm i paperlab three @react-three/fiber gsap
6915
+
6916
+ 2. Create the component below as \`components/${name}.tsx\` (or the project's
6917
+ component convention). It is self-contained \u2014 it owns its own <Canvas>,
6918
+ its own camera and its own lighting:
6919
+
6920
+ \`\`\`tsx
6921
+ ${buildStageComponentSource(input)}
6922
+ \`\`\`
6923
+
6924
+ 3. Placement: this is a full-bleed scene, not an inline element. Give it the
6925
+ full width of the viewport.
6926
+
6927
+ ${sizing}
6928
+
6929
+ 5. Verify: run the dev server. You should see ${describeStage(input)}.
6930
+ If the canvas is blank, the container almost certainly has no height \u2014 give
6931
+ it one (this is the classic React Three Fiber integration bug, not a
6932
+ paperlab bug).
6933
+
6934
+ Constraints: don't modify the stage values; the camera is driven by the
6935
+ stage's own shot, so don't add OrbitControls; three >= 0.160 and React 19 are
6936
+ required; the component needs no props.`;
6937
+ }
6938
+
6939
+ // src/field/parity.ts
6940
+ var THREE18 = __toESM(require("three"), 1);
6941
+ var GRID = 48;
6942
+ var PARITY_EPSILON = 5e-4;
6943
+ var parityCases = [
6944
+ {
6945
+ name: "roll: defaults",
6946
+ stack: [{ type: "roll", options: { angle: 90, boundary: 0, radius: 0.12, spiral: 0.015 } }],
6947
+ sheet: { width: 1, height: 1.4 },
6948
+ t: 0
6949
+ },
6950
+ {
6951
+ name: "roll: tight receipt roll, rolling down",
6952
+ stack: [{ type: "roll", options: { angle: 270, boundary: -0.4, radius: 0.07, spiral: 0.02 } }],
6953
+ sheet: { width: 1, height: 2.6 },
6954
+ t: 0
6955
+ },
6956
+ {
6957
+ name: "curl: bottom-right peel",
6958
+ stack: [{ type: "curl", options: { corner: "bottom-right", amount: 0.45, radius: 0.2, skew: 0 } }],
6959
+ sheet: { width: 1.5, height: 1 },
6960
+ t: 0
6961
+ },
6962
+ {
6963
+ name: "curl: skewed top-left",
6964
+ stack: [{ type: "curl", options: { corner: "top-left", amount: 0.7, radius: 0.3, skew: 15 } }],
6965
+ sheet: { width: 1, height: 1 },
6966
+ t: 0
6967
+ },
6968
+ {
6969
+ name: "bend: positive arc at an angle",
6970
+ stack: [{ type: "bend", options: { curvature: 1.2, angle: 33 } }],
6971
+ sheet: { width: 1, height: 1.4 },
6972
+ t: 0
6973
+ },
6974
+ {
6975
+ /**
6976
+ * The gentle end, which nothing used to cover: the gate only ever tried
6977
+ * |curvature| ≥ 0.6, and `photo-print` — the field starter preset — bends
6978
+ * at 0.35. That band is where the arc's float32 evaluation loses its
6979
+ * cancellation, and where the two paths were 6e-4 apart until `bend` was
6980
+ * rewritten in cancellation-free form. Keep a case down here.
6981
+ */
6982
+ name: "bend: barely-there arc (the band photo-print lives in)",
6983
+ stack: [{ type: "bend", options: { curvature: 0.35, angle: 0 } }],
6984
+ sheet: { width: 1.2, height: 0.9 },
6985
+ t: 0
6986
+ },
6987
+ {
6988
+ name: "bend: the gentlest arc the schema allows",
6989
+ stack: [{ type: "bend", options: { curvature: 0.02, angle: 61 } }],
6990
+ sheet: { width: 2, height: 2.6 },
6991
+ t: 0
6992
+ },
6993
+ {
6994
+ name: "bend: negative arc",
6995
+ stack: [{ type: "bend", options: { curvature: -0.8, angle: 0 } }],
6996
+ sheet: { width: 1, height: 1.4 },
6997
+ t: 0
6998
+ },
6999
+ {
7000
+ name: "fold: 90\xB0 hinge",
7001
+ stack: [{ type: "fold", options: { angle: 90, offset: 0, foldAngle: 90, radius: 0.06 } }],
7002
+ sheet: { width: 1, height: 1.4 },
7003
+ t: 0
7004
+ },
7005
+ {
7006
+ name: "fold: deep fold, travelling down",
7007
+ stack: [{ type: "fold", options: { angle: 270, offset: 0.2, foldAngle: 165, radius: 0.04 } }],
7008
+ sheet: { width: 1, height: 1.4 },
7009
+ t: 0
7010
+ },
7011
+ {
7012
+ name: "wave: free ripple at t=1.234",
7013
+ stack: [
7014
+ {
7015
+ type: "wave",
7016
+ options: { amplitude: 0.05, wavelength: 0.5, speed: 1, angle: 20, pinnedEdge: "none" }
7017
+ }
7018
+ ],
7019
+ sheet: { width: 1, height: 1 },
7020
+ t: 1.234
7021
+ },
7022
+ {
7023
+ name: "wave: pinned top at t=2.5",
7024
+ stack: [
7025
+ {
7026
+ type: "wave",
7027
+ options: { amplitude: 0.04, wavelength: 0.4, speed: 1.5, angle: 80, pinnedEdge: "top" }
7028
+ }
7029
+ ],
7030
+ sheet: { width: 1.2, height: 1.5 },
7031
+ t: 2.5
7032
+ },
7033
+ {
7034
+ name: "drape: banner hung from the top",
7035
+ stack: [
7036
+ {
7037
+ type: "drape",
7038
+ options: { amplitude: 0.17, folds: 5, falloff: 1.6, irregular: 0.45, gather: 0.5, pinnedEdge: "top" }
7039
+ }
7040
+ ],
7041
+ sheet: { width: 1.5, height: 8.5 },
7042
+ t: 0
7043
+ },
7044
+ {
7045
+ name: "drape: deep irregular folds pinned at the bottom",
7046
+ stack: [
7047
+ {
7048
+ type: "drape",
7049
+ options: { amplitude: 0.55, folds: 11, falloff: 0.4, irregular: 1, gather: 1, pinnedEdge: "bottom" }
7050
+ }
7051
+ ],
7052
+ sheet: { width: 2.2, height: 3 },
7053
+ t: 0
7054
+ },
7055
+ {
7056
+ name: "crumple: defaults",
7057
+ stack: [{ type: "crumple", options: { amount: 0.35, scale: 3, pull: 0.4, seed: 0 } }],
7058
+ sheet: { width: 1, height: 1.4 },
7059
+ t: 0
7060
+ },
7061
+ {
7062
+ // The adversarial one. `fract` is a sawtooth, so the two halves disagree
7063
+ // hardest where a crease lands exactly on a sample — a fine scale and a
7064
+ // seed whose fold directions are near-axis puts the most creases in
7065
+ // reach of the grid.
7066
+ name: "crumple: fully crushed, fine creases, off-axis seed",
7067
+ stack: [{ type: "crumple", options: { amount: 1, scale: 7.5, pull: 1, seed: 5 } }],
7068
+ sheet: { width: 1.3, height: 0.9 },
7069
+ t: 0
7070
+ },
7071
+ {
7072
+ // Crush then curl, which is the order the crumple BEHAVIOR stacks them:
7073
+ // the creases have to be placed on the flat sheet, not on a bent one.
7074
+ name: "stacked: crumple \u2218 bend (the crumple behavior)",
7075
+ stack: [
7076
+ { type: "crumple", options: { amount: 0.62, scale: 3.3, pull: 0.5, seed: 2 } },
7077
+ { type: "bend", options: { curvature: 0.31, angle: 35 } }
7078
+ ],
7079
+ sheet: { width: 1.1, height: 1.4 },
7080
+ t: 0
7081
+ },
7082
+ {
7083
+ name: "stacked: letter-fold pair (fold \u2218 fold)",
7084
+ stack: [
7085
+ { type: "fold", options: { angle: 270, offset: 0.2333, foldAngle: 120, radius: 0.05 } },
7086
+ { type: "fold", options: { angle: 90, offset: 0.2333, foldAngle: 100, radius: 0.08 } }
7087
+ ],
7088
+ sheet: { width: 1, height: 1.4 },
7089
+ t: 0
7090
+ },
7091
+ {
7092
+ name: "stacked: bend \u2218 roll \u2218 wave",
7093
+ stack: [
7094
+ { type: "bend", options: { curvature: 0.6, angle: 0 } },
7095
+ { type: "roll", options: { angle: 90, boundary: 0.1, radius: 0.15, spiral: 0 } },
7096
+ {
7097
+ type: "wave",
7098
+ options: { amplitude: 0.02, wavelength: 0.6, speed: 0.7, angle: 45, pinnedEdge: "none" }
7099
+ }
7100
+ ],
7101
+ sheet: { width: 1, height: 1.4 },
7102
+ t: 0.8
7103
+ }
7104
+ ];
7105
+ function buildParityFragment(stack, sheet2) {
7106
+ const composed = buildDisplacementGLSL(stack, sheet2);
7107
+ return (
7108
+ /* glsl */
7109
+ `#version 300 es
7110
+ precision highp float;
7111
+ uniform float uPlTime;
7112
+ uniform float uPlBias;
7113
+ ${composed.functionsSrc}
7114
+ ${composed.displaceSrc}
7115
+ out vec4 outColor;
7116
+ void main() {
7117
+ vec2 uv = (gl_FragCoord.xy - 0.5) / float(${GRID - 1});
7118
+ vec3 p = vec3((uv - 0.5) * uSheet, 0.0);
7119
+ outColor = vec4(plDisplace(p, uv, uPlTime, uPlBias), 1.0);
7120
+ }
7121
+ `
7122
+ );
7123
+ }
7124
+ var VERT = `#version 300 es
7125
+ in vec2 aPos;
7126
+ void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
7127
+ `;
7128
+ function compile(gl, type, src) {
7129
+ const shader = gl.createShader(type);
7130
+ gl.shaderSource(shader, src);
7131
+ gl.compileShader(shader);
7132
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
7133
+ throw new Error(`[paperlab parity] shader compile failed: ${gl.getShaderInfoLog(shader)}`);
7134
+ }
7135
+ return shader;
7136
+ }
7137
+ function runCaseOnGPU(gl, c, bias = 1) {
7138
+ const composed = buildDisplacementGLSL(c.stack, c.sheet);
7139
+ const program = gl.createProgram();
7140
+ gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERT));
7141
+ gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, buildParityFragment(c.stack, c.sheet)));
7142
+ gl.linkProgram(program);
7143
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
7144
+ throw new Error(`[paperlab parity] link failed: ${gl.getProgramInfoLog(program)}`);
7145
+ }
7146
+ gl.useProgram(program);
7147
+ for (const [name, value] of Object.entries(composed.uniforms)) {
7148
+ const loc = gl.getUniformLocation(program, name);
7149
+ if (!loc) continue;
7150
+ if (typeof value === "number") gl.uniform1f(loc, value);
7151
+ else if (value.length === 2) gl.uniform2f(loc, value[0], value[1]);
7152
+ else if (value.length === 3) gl.uniform3f(loc, value[0], value[1], value[2]);
7153
+ else gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
7154
+ }
7155
+ const timeLoc = gl.getUniformLocation(program, "uPlTime");
7156
+ if (timeLoc) gl.uniform1f(timeLoc, c.t);
7157
+ const biasLoc = gl.getUniformLocation(program, "uPlBias");
7158
+ if (biasLoc) gl.uniform1f(biasLoc, bias);
7159
+ const quad = gl.createBuffer();
7160
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
7161
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
7162
+ const posLoc = gl.getAttribLocation(program, "aPos");
7163
+ gl.enableVertexAttribArray(posLoc);
7164
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
7165
+ const tex = gl.createTexture();
7166
+ gl.bindTexture(gl.TEXTURE_2D, tex);
7167
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, GRID, GRID, 0, gl.RGBA, gl.FLOAT, null);
7168
+ const fbo = gl.createFramebuffer();
7169
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
7170
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
7171
+ gl.viewport(0, 0, GRID, GRID);
7172
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
7173
+ const out = new Float32Array(GRID * GRID * 4);
7174
+ gl.readPixels(0, 0, GRID, GRID, gl.RGBA, gl.FLOAT, out);
7175
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
7176
+ return out;
7177
+ }
7178
+ function runBiasCases(gl) {
7179
+ const results = [];
7180
+ for (const c of parityCases) {
7181
+ const strengths = c.stack.map((i) => getDeformer(i.type).glsl?.strength !== void 0);
7182
+ if (!strengths.every((h) => h === strengths[0])) continue;
7183
+ const scales = strengths[0];
7184
+ const at0 = runCaseOnGPU(gl, c, 0);
7185
+ const reference = scales ? null : runCaseOnGPU(gl, c, 1);
7186
+ let maxError = 0;
7187
+ for (let row = 0; row < GRID; row++) {
7188
+ for (let col = 0; col < GRID; col++) {
7189
+ const i4 = (row * GRID + col) * 4;
7190
+ const ex = reference ? reference[i4] : (col / (GRID - 1) - 0.5) * c.sheet.width;
7191
+ const ey = reference ? reference[i4 + 1] : (row / (GRID - 1) - 0.5) * c.sheet.height;
7192
+ const ez = reference ? reference[i4 + 2] : 0;
7193
+ maxError = Math.max(
7194
+ maxError,
7195
+ Math.abs(at0[i4] - ex),
7196
+ Math.abs(at0[i4 + 1] - ey),
7197
+ Math.abs(at0[i4 + 2] - ez)
7198
+ );
7199
+ }
7200
+ }
7201
+ results.push({
7202
+ name: `bias: ${c.name} \u2192 ${scales ? "flat at 0" : "ignores bias"}`,
7203
+ maxError,
7204
+ pass: maxError < PARITY_EPSILON
7205
+ });
7206
+ }
7207
+ return results;
7208
+ }
7209
+ function runParityHarness(canvas) {
7210
+ const cnv = canvas ?? document.createElement("canvas");
7211
+ const gl = cnv.getContext("webgl2");
7212
+ if (!gl) throw new Error("[paperlab parity] WebGL2 unavailable");
7213
+ if (!gl.getExtension("EXT_color_buffer_float")) {
7214
+ throw new Error("[paperlab parity] EXT_color_buffer_float unavailable");
7215
+ }
7216
+ const point = new THREE18.Vector3();
7217
+ const parity = parityCases.map((c) => {
7218
+ const gpu = runCaseOnGPU(gl, c);
7219
+ let maxError = 0;
7220
+ for (let row = 0; row < GRID; row++) {
7221
+ for (let col = 0; col < GRID; col++) {
7222
+ const u = col / (GRID - 1);
7223
+ const v = row / (GRID - 1);
7224
+ point.set((u - 0.5) * c.sheet.width, (v - 0.5) * c.sheet.height, 0);
7225
+ displacePoint(point, u, v, c.stack, { t: c.t, sheet: c.sheet });
7226
+ const i4 = (row * GRID + col) * 4;
7227
+ maxError = Math.max(
7228
+ maxError,
7229
+ Math.abs(gpu[i4] - point.x),
7230
+ Math.abs(gpu[i4 + 1] - point.y),
7231
+ Math.abs(gpu[i4 + 2] - point.z)
7232
+ );
7233
+ }
7234
+ }
7235
+ return { name: c.name, maxError, pass: maxError < PARITY_EPSILON };
7236
+ });
7237
+ return [...parity, ...runBiasCases(gl)];
7238
+ }
7239
+
5272
7240
  // src/config/field-export.ts
5273
7241
  var MOTION_DEFAULTS = { driver: "autoplay", speed: 0.5 };
5274
7242
  var ENTRANCE_DEFAULTS = { type: "rise", stagger: 0.06, duration: 0.9 };
@@ -5310,12 +7278,15 @@ function diffFieldProps(input) {
5310
7278
  }
5311
7279
  var LAYOUT_PHRASES = {
5312
7280
  ring: "a ring you can see around",
5313
- deck: "a stacked deck",
5314
- cascade: "a diagonal cascade",
5315
- helix: "a climbing helix",
5316
- wall: "a jittered wall grid",
5317
- tunnel: "a receding tunnel",
5318
- scatter: "a loose scatter",
7281
+ fan: "a fanned swatch deck hinged at one corner",
7282
+ spread: "a stack slid sideways, each sheet bowing further than the last",
7283
+ pile: "a heap on a desk, the sheets underneath pressed flat",
7284
+ wall: "a pinned studio wall of sheets, none hanging quite square",
7285
+ spill: "a dropped stack caught mid-air, each sheet bent its own way",
7286
+ sweep: "a specimen chart of one sheet at every stage of its deformation",
7287
+ book: "an open book, its pages splayed from a shared spine",
7288
+ accordion: "one continuous strip folded into a concertina",
7289
+ rack: "prints stood in a row and leaning back against a wall",
5319
7290
  sheet: "a stamp-block grid on a shared backing sheet"
5320
7291
  };
5321
7292
  function describeFieldConfig(input) {
@@ -5336,7 +7307,7 @@ function describeFieldConfig(input) {
5336
7307
  parts.push("hovering peels/reacts per paper (interaction states)");
5337
7308
  if (zones.length > 0) {
5338
7309
  parts.push(
5339
- `dragging one past its tear threshold detaches it to carry to the ${zones.map((z18) => z18.id).join(" or ")} zone (release elsewhere flutters it back)`
7310
+ `dragging one past its tear threshold detaches it to carry to the ${zones.map((z27) => z27.id).join(" or ")} zone (release elsewhere flutters it back)`
5340
7311
  );
5341
7312
  }
5342
7313
  } else {
@@ -5466,8 +7437,12 @@ function resolveMode(requested, ctx) {
5466
7437
  PaperMaterial,
5467
7438
  PaperMesh,
5468
7439
  PaperMirror,
7440
+ PaperStage,
7441
+ PaperStageScene,
5469
7442
  PaperStateMachine,
5470
7443
  SHEET_LIFT,
7444
+ TRANSMISSION_GAIN,
7445
+ accordion,
5471
7446
  applyDeformerStack,
5472
7447
  atlasGrid,
5473
7448
  backContentSchema,
@@ -5475,6 +7450,7 @@ function resolveMode(requested, ctx) {
5475
7450
  behaviorConfigSchema,
5476
7451
  bend,
5477
7452
  bendOptionsSchema,
7453
+ book,
5478
7454
  buildAgentPayload,
5479
7455
  buildDisplacementGLSL,
5480
7456
  buildFieldAgentPayload,
@@ -5482,32 +7458,45 @@ function resolveMode(requested, ctx) {
5482
7458
  buildFieldFragmentShader,
5483
7459
  buildFieldVertexShader,
5484
7460
  buildJsxSnippet,
7461
+ buildStageAgentPayload,
7462
+ buildStageComponentSource,
5485
7463
  carry,
5486
7464
  carryDrive,
5487
7465
  carryOptionsSchema,
5488
- cascade,
5489
7466
  clothConfigSchema,
7467
+ colonnade,
5490
7468
  composeSurface,
5491
7469
  contentSchema,
5492
7470
  contentText,
5493
7471
  coreStateNames,
5494
7472
  cornerNames,
5495
7473
  createSheetGeometry,
7474
+ createWalkPath,
7475
+ crumple,
7476
+ crumpleBehavior,
7477
+ crumpleBehaviorOptionsSchema,
7478
+ crumpleOptionsSchema,
5496
7479
  curl,
5497
7480
  curlOptionsSchema,
5498
7481
  dampTo,
5499
- deck,
5500
7482
  deformerInstanceSchema,
5501
7483
  describeConfig,
5502
7484
  describeFieldConfig,
7485
+ describeStage,
5503
7486
  diffConfig,
5504
7487
  diffFieldProps,
7488
+ diffStage,
5505
7489
  displacePoint,
5506
7490
  distinctFieldPresets,
7491
+ drape,
7492
+ drapeOptionsSchema,
5507
7493
  drawBacking,
5508
7494
  fall,
5509
7495
  fallOptionsSchema,
7496
+ fan,
7497
+ fieldBounds,
5510
7498
  fieldKeyboardStep,
7499
+ fitCamera,
5511
7500
  flattenNumeric,
5512
7501
  flight,
5513
7502
  flightOptionsSchema,
@@ -5524,12 +7513,13 @@ function resolveMode(requested, ctx) {
5524
7513
  getLayout,
5525
7514
  getLightingPreset,
5526
7515
  getPreset,
7516
+ getStagePreset,
5527
7517
  getStock,
7518
+ getWalk,
5528
7519
  groupFieldPapers,
5529
7520
  gust,
5530
7521
  hang,
5531
7522
  hangOptionsSchema,
5532
- helix,
5533
7523
  idleNames,
5534
7524
  idlePresets,
5535
7525
  isBuiltinPreset,
@@ -5542,6 +7532,7 @@ function resolveMode(requested, ctx) {
5542
7532
  listDeformers,
5543
7533
  listLayouts,
5544
7534
  listPresets,
7535
+ listStagePresets,
5545
7536
  makeGoboTexture,
5546
7537
  mergeConfig,
5547
7538
  mergeWithDeletes,
@@ -5555,8 +7546,11 @@ function resolveMode(requested, ctx) {
5555
7546
  peelOptionsSchema,
5556
7547
  physicsNames,
5557
7548
  physicsSchema,
7549
+ pile,
7550
+ qualityNames,
5558
7551
  quantizeProgress,
5559
7552
  quantizeTime,
7553
+ rack,
5560
7554
  receiptContentSchema,
5561
7555
  receiptTotals,
5562
7556
  recordStateOverride,
@@ -5565,6 +7559,7 @@ function resolveMode(requested, ctx) {
5565
7559
  registerLayout,
5566
7560
  registerPreset,
5567
7561
  resolveConfig,
7562
+ resolveDeformerStack,
5568
7563
  resolveFieldSlotConfig,
5569
7564
  resolveMode,
5570
7565
  resolveSegments,
@@ -5573,7 +7568,6 @@ function resolveMode(requested, ctx) {
5573
7568
  roll,
5574
7569
  rollOptionsSchema,
5575
7570
  runParityHarness,
5576
- scatter,
5577
7571
  sceneSchema,
5578
7572
  serializePreset,
5579
7573
  sheet,
@@ -5581,20 +7575,28 @@ function resolveMode(requested, ctx) {
5581
7575
  sheetLayoutSchema,
5582
7576
  sheetSchema,
5583
7577
  sheetSlotXY,
7578
+ shotNames,
5584
7579
  silhouetteRects,
7580
+ spill,
7581
+ spread,
5585
7582
  stackMinSegments,
5586
7583
  stackUniformValues,
7584
+ stagePresets,
7585
+ stageSchema,
5587
7586
  stateDefSchema,
5588
7587
  stateEventTransitions,
5589
7588
  stateTransitionSchema,
5590
7589
  stockNames,
5591
7590
  stockSchema,
5592
7591
  stocks,
7592
+ stringifyStage,
5593
7593
  stripStates,
5594
7594
  supportsWebGL,
5595
7595
  surfaceSchema,
7596
+ sweep,
5596
7597
  tornEdgesOnDetach,
5597
- tunnel,
7598
+ translucencyUniforms,
7599
+ translucencyValues,
5598
7600
  uniquePresetName,
5599
7601
  unregisterPreset,
5600
7602
  unroll,
@@ -5602,6 +7604,9 @@ function resolveMode(requested, ctx) {
5602
7604
  useContentAtlas,
5603
7605
  usePaperStates,
5604
7606
  usePrefersReducedMotion,
7607
+ walkNameFor,
7608
+ walkNames,
7609
+ walks,
5605
7610
  wall,
5606
7611
  wave,
5607
7612
  waveOptionsSchema,