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/README.md +52 -8
- package/dist/index.cjs +2652 -647
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1129 -81
- package/dist/index.d.ts +1129 -81
- package/dist/index.js +2609 -638
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
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 = (
|
|
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 || !
|
|
26
|
-
|
|
25
|
+
isNodeMode || !mod2 || !mod2.__esModule ? __defProp(target, "default", { value: mod2, enumerable: true }) : target,
|
|
26
|
+
mod2
|
|
27
27
|
));
|
|
28
|
-
var __toCommonJS = (
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
779
|
-
height:
|
|
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:
|
|
782
|
-
/**
|
|
783
|
-
|
|
784
|
-
|
|
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 =
|
|
796
|
-
var blankContentBase =
|
|
797
|
-
type:
|
|
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 =
|
|
800
|
-
type:
|
|
801
|
-
src:
|
|
802
|
-
fit:
|
|
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:
|
|
885
|
+
alt: import_zod12.z.string().optional()
|
|
805
886
|
});
|
|
806
|
-
var textContentBase =
|
|
807
|
-
type:
|
|
808
|
-
text:
|
|
809
|
-
font:
|
|
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:
|
|
812
|
-
weight:
|
|
813
|
-
color:
|
|
814
|
-
align:
|
|
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:
|
|
817
|
-
lineHeight:
|
|
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 =
|
|
820
|
-
type:
|
|
821
|
-
store:
|
|
822
|
-
address:
|
|
823
|
-
items:
|
|
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:
|
|
829
|
-
barcode:
|
|
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:
|
|
832
|
-
footer:
|
|
912
|
+
timestamp: import_zod12.z.string().optional(),
|
|
913
|
+
footer: import_zod12.z.string().default("KEEP FOR YOUR RECORDS")
|
|
833
914
|
});
|
|
834
|
-
var backContentSchema =
|
|
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 =
|
|
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 =
|
|
933
|
+
var surfaceSchema = import_zod12.z.object({
|
|
853
934
|
/** Paper fiber noise, 0..1. */
|
|
854
|
-
grain:
|
|
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:
|
|
857
|
-
edges:
|
|
858
|
-
roughness:
|
|
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:
|
|
944
|
+
creaseLines: import_zod12.z.object({
|
|
862
945
|
/** Crease line direction, degrees (0 = horizontal lines). */
|
|
863
|
-
angle:
|
|
946
|
+
angle: import_zod12.z.number().min(-360).max(360).default(0),
|
|
864
947
|
/** Positions across the sheet, 0..1 fractions. */
|
|
865
|
-
positions:
|
|
866
|
-
strength:
|
|
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:
|
|
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:
|
|
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:
|
|
878
|
-
edges:
|
|
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:
|
|
881
|
-
spacing:
|
|
882
|
-
state:
|
|
883
|
-
top:
|
|
884
|
-
right:
|
|
885
|
-
bottom:
|
|
886
|
-
left:
|
|
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 =
|
|
891
|
-
peelOptionsSchema.extend({ type:
|
|
892
|
-
unrollOptionsSchema.extend({ type:
|
|
893
|
-
flipOptionsSchema.extend({ type:
|
|
894
|
-
letterFoldOptionsSchema.extend({ type:
|
|
895
|
-
hangOptionsSchema.extend({ type:
|
|
896
|
-
flyOptionsSchema.extend({ type:
|
|
897
|
-
fallOptionsSchema.extend({ type:
|
|
898
|
-
carryOptionsSchema.extend({ type:
|
|
899
|
-
flightOptionsSchema.extend({ type:
|
|
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 =
|
|
902
|
-
type:
|
|
903
|
-
options:
|
|
904
|
-
enabled:
|
|
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 =
|
|
908
|
-
type:
|
|
909
|
-
pins:
|
|
910
|
-
wind:
|
|
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:
|
|
913
|
-
gravity:
|
|
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:
|
|
999
|
+
floor: import_zod12.z.number().min(-5).max(0).default(-1.4)
|
|
916
1000
|
});
|
|
917
|
-
var physicsSchema =
|
|
918
|
-
|
|
919
|
-
|
|
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 =
|
|
924
|
-
lighting:
|
|
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 =
|
|
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 =
|
|
932
|
-
duration:
|
|
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:
|
|
1018
|
+
ease: import_zod12.z.string().default("power2.out")
|
|
935
1019
|
});
|
|
936
|
-
var stateDefSchema =
|
|
1020
|
+
var stateDefSchema = import_zod12.z.object({
|
|
937
1021
|
/** Deep-partial override of the paper schema (behavior params, surface, …). */
|
|
938
|
-
overrides:
|
|
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:
|
|
1026
|
+
onEnter: import_zod12.z.array(import_zod12.z.string().regex(/^emit:[\w-]+$/, 'v1 actions are "emit:<event>"')).default([])
|
|
943
1027
|
});
|
|
944
|
-
var paperStatesSchema =
|
|
1028
|
+
var paperStatesSchema = import_zod12.z.object({
|
|
945
1029
|
initial: stateNameSchema.default("rest"),
|
|
946
|
-
states:
|
|
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:
|
|
1034
|
+
pickThreshold: import_zod12.z.number().min(5e-3).max(1).default(0.1)
|
|
951
1035
|
});
|
|
952
|
-
var metaSchema =
|
|
953
|
-
name:
|
|
954
|
-
author:
|
|
955
|
-
version:
|
|
956
|
-
tags:
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
1510
|
-
var rollOptionsSchema =
|
|
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:
|
|
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:
|
|
1626
|
+
boundary: import_zod13.z.number().min(-20).max(20).default(0),
|
|
1515
1627
|
/** Cylinder radius — sharpness of the roll. */
|
|
1516
|
-
radius:
|
|
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:
|
|
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
|
|
1574
|
-
var bendOptionsSchema =
|
|
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:
|
|
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:
|
|
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
|
|
1711
|
+
const theta = d * o.curvature;
|
|
1595
1712
|
const sin = Math.sin(theta);
|
|
1596
|
-
const
|
|
1597
|
-
const
|
|
1598
|
-
const
|
|
1599
|
-
out.x += dirX *
|
|
1600
|
-
out.y += dirY *
|
|
1601
|
-
out.z =
|
|
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
|
|
1735
|
+
float theta = d * U_curvature;
|
|
1613
1736
|
float sn = sin(theta);
|
|
1614
|
-
float
|
|
1615
|
-
float
|
|
1616
|
-
float
|
|
1617
|
-
p.xy += dir *
|
|
1618
|
-
p.z =
|
|
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
|
|
1628
|
-
var foldOptionsSchema =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
1715
|
-
var waveOptionsSchema =
|
|
1716
|
-
amplitude:
|
|
1717
|
-
wavelength:
|
|
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:
|
|
1844
|
+
speed: import_zod16.z.number().min(0).max(3).default(0.8),
|
|
1720
1845
|
/** Travel direction in the sheet plane, degrees. */
|
|
1721
|
-
angle:
|
|
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:
|
|
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,
|
|
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] -
|
|
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,
|
|
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] =
|
|
2361
|
+
this.positions[i3 + 2] = z27;
|
|
2033
2362
|
this.prev[i3] = x;
|
|
2034
2363
|
this.prev[i3 + 1] = y;
|
|
2035
|
-
this.prev[i3 + 2] =
|
|
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
|
|
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 = (
|
|
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] =
|
|
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] =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
2911
|
-
var dragPoint = new
|
|
2912
|
-
var planeNormal = new
|
|
2913
|
-
var anchorScratch = new
|
|
2914
|
-
var worldScratch = new
|
|
2915
|
-
var quatScratch = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3382
|
-
texture.wrapS = texture.wrapT =
|
|
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
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
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
|
|
3867
|
+
castShadow,
|
|
3425
3868
|
map: goboMap,
|
|
3426
|
-
"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
|
|
3437
|
-
"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
|
|
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
|
|
3556
|
-
const geo = new
|
|
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
|
|
3580
|
-
var sheetLayoutSchema =
|
|
3581
|
-
rows:
|
|
3582
|
-
columns:
|
|
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:
|
|
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:
|
|
3587
|
-
cellHeight:
|
|
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:
|
|
3590
|
-
backingMargin:
|
|
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
|
|
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
|
|
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
|
|
3704
|
-
|
|
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
|
|
3712
|
-
texture.colorSpace =
|
|
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(
|
|
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
|
|
3855
|
-
|
|
3856
|
-
|
|
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
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
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) *
|
|
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
|
|
3882
|
-
|
|
3883
|
-
|
|
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
|
|
3886
|
-
id: "
|
|
3887
|
-
label: "
|
|
3888
|
-
defaults:
|
|
3889
|
-
optionsSchema:
|
|
3890
|
-
pose(i,
|
|
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: [
|
|
3893
|
-
rotation: [0, 0,
|
|
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
|
|
3899
|
-
|
|
3900
|
-
|
|
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
|
|
3903
|
-
id: "
|
|
3904
|
-
label: "
|
|
3905
|
-
defaults:
|
|
3906
|
-
optionsSchema:
|
|
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: [
|
|
3911
|
-
rotation: [0, 0,
|
|
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
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
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
|
|
3922
|
-
id: "
|
|
3923
|
-
label: "
|
|
3924
|
-
defaults:
|
|
3925
|
-
optionsSchema:
|
|
3926
|
-
pose(i, n, o
|
|
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: [
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
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 =
|
|
3938
|
-
gapX:
|
|
3939
|
-
gapY:
|
|
3940
|
-
jitterAmt:
|
|
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 *
|
|
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 =
|
|
3953
|
-
const cellH =
|
|
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
|
-
|
|
4581
|
+
jitter2(5, i) * 0.04 * o.jitterAmt * 4
|
|
3959
4582
|
],
|
|
3960
|
-
rotation: [0, 0,
|
|
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
|
|
3966
|
-
|
|
3967
|
-
|
|
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
|
|
3970
|
-
id: "
|
|
3971
|
-
label: "
|
|
3972
|
-
defaults:
|
|
3973
|
-
optionsSchema:
|
|
3974
|
-
pose(i, _n, o
|
|
3975
|
-
const
|
|
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: [
|
|
3978
|
-
|
|
3979
|
-
|
|
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
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
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
|
|
3990
|
-
id: "
|
|
3991
|
-
label: "
|
|
3992
|
-
defaults:
|
|
3993
|
-
optionsSchema:
|
|
3994
|
-
pose(i,
|
|
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
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4642
|
+
(col - (cols - 1) / 2) * (sheet2.width + o.gap),
|
|
4643
|
+
((rows - 1) / 2 - row) * (sheet2.height + o.gap),
|
|
4644
|
+
0
|
|
4000
4645
|
],
|
|
4001
|
-
rotation: [
|
|
4002
|
-
scale:
|
|
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(
|
|
4032
|
-
registerLayout(
|
|
4033
|
-
registerLayout(
|
|
4815
|
+
registerLayout(fan);
|
|
4816
|
+
registerLayout(spread);
|
|
4817
|
+
registerLayout(pile);
|
|
4034
4818
|
registerLayout(wall);
|
|
4035
|
-
registerLayout(
|
|
4036
|
-
registerLayout(
|
|
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
|
|
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
|
-
|
|
4060
|
-
|
|
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
|
|
4078
|
-
geo.setAttribute("aPhase", new
|
|
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
|
|
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
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4403
|
-
const planeScratch = (0, import_react12.useMemo)(() => new
|
|
4404
|
-
const pointScratch = (0, import_react12.useMemo)(() => new
|
|
4405
|
-
const ndcScratch = (0, import_react12.useMemo)(() => new
|
|
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
|
-
(
|
|
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 += (
|
|
4610
|
-
group.rotation.x += (
|
|
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
|
-
(
|
|
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((
|
|
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((
|
|
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.
|
|
4762
|
-
"
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
"
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
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
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
})
|
|
4805
|
-
|
|
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) =>
|
|
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/
|
|
4934
|
-
var
|
|
4935
|
-
var
|
|
4936
|
-
var
|
|
4937
|
-
var
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
{
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
}
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
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
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
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
|
-
|
|
5002
|
-
|
|
6143
|
+
map: texture,
|
|
6144
|
+
transparent: true,
|
|
6145
|
+
depthWrite: false,
|
|
6146
|
+
toneMapped: false,
|
|
6147
|
+
fog: false
|
|
5003
6148
|
}
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
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
|
-
|
|
5024
|
-
|
|
6380
|
+
preset: stage.lighting,
|
|
6381
|
+
floor: 0,
|
|
6382
|
+
scale: 60,
|
|
6383
|
+
reducedMotion,
|
|
6384
|
+
shadowMapSize: settings.shadowMapSize,
|
|
6385
|
+
contactShadow: settings.contactShadow
|
|
5025
6386
|
}
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
]
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
/*
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
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
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
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
|
-
|
|
6484
|
+
};
|
|
6485
|
+
function getWalk(name) {
|
|
6486
|
+
return walks[name];
|
|
5061
6487
|
}
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
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
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
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
|
-
|
|
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
|
|
5102
|
-
|
|
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
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
wall: "a
|
|
5317
|
-
|
|
5318
|
-
|
|
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((
|
|
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
|
-
|
|
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,
|