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.js
CHANGED
|
@@ -3,13 +3,13 @@ import { Canvas } from "@react-three/fiber";
|
|
|
3
3
|
import { forwardRef as forwardRef2, useMemo as useMemo5 } from "react";
|
|
4
4
|
|
|
5
5
|
// src/PaperMesh.tsx
|
|
6
|
-
import * as
|
|
6
|
+
import * as THREE7 from "three";
|
|
7
7
|
import { gsap as gsap2 } from "gsap";
|
|
8
8
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
9
9
|
import { forwardRef, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as useRef2 } from "react";
|
|
10
10
|
|
|
11
11
|
// src/config/schema.ts
|
|
12
|
-
import { z as
|
|
12
|
+
import { z as z12 } from "zod";
|
|
13
13
|
|
|
14
14
|
// src/config/merge.ts
|
|
15
15
|
function mergeConfig(base, override) {
|
|
@@ -121,6 +121,7 @@ void FN(inout vec3 p, vec2 uv, float t) {
|
|
|
121
121
|
}
|
|
122
122
|
`
|
|
123
123
|
),
|
|
124
|
+
strength: "amount",
|
|
124
125
|
uniforms: (o) => {
|
|
125
126
|
const [sx, sy] = CORNER_SIGNS[o.corner];
|
|
126
127
|
return {
|
|
@@ -582,16 +583,62 @@ var flight = {
|
|
|
582
583
|
}
|
|
583
584
|
};
|
|
584
585
|
|
|
586
|
+
// src/behaviors/crumple.ts
|
|
587
|
+
import { z as z11 } from "zod";
|
|
588
|
+
var crumpleBehaviorOptionsSchema = z11.object({
|
|
589
|
+
/** 0 = flat sheet, 1 = crushed. */
|
|
590
|
+
progress: z11.number().min(0).max(1).default(0.55),
|
|
591
|
+
/** Few big facets at 0, many small ones at 1. */
|
|
592
|
+
coarseness: z11.number().min(0).max(1).default(0.35),
|
|
593
|
+
/** How far the sheet curls in on itself as it crushes. */
|
|
594
|
+
ball: z11.number().min(0).max(1).default(0.5),
|
|
595
|
+
/** A different crush of the same paper. */
|
|
596
|
+
seed: z11.number().int().min(0).max(7).default(0)
|
|
597
|
+
});
|
|
598
|
+
var crumpleBehavior = {
|
|
599
|
+
id: "crumple",
|
|
600
|
+
label: "Crumple",
|
|
601
|
+
defaults: crumpleBehaviorOptionsSchema.parse({}),
|
|
602
|
+
optionsSchema: crumpleBehaviorOptionsSchema,
|
|
603
|
+
progressParam: "progress",
|
|
604
|
+
duration: 2.6,
|
|
605
|
+
loopMode: "yoyo",
|
|
606
|
+
stack(o) {
|
|
607
|
+
return [
|
|
608
|
+
{
|
|
609
|
+
type: "crumple",
|
|
610
|
+
options: {
|
|
611
|
+
amount: o.progress,
|
|
612
|
+
scale: 1.5 + o.coarseness * 4.5,
|
|
613
|
+
pull: 0.5,
|
|
614
|
+
seed: o.seed
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
// The sheet closing in on itself. Paper does not crush flat, and
|
|
618
|
+
// without this the result reads as texture rather than as a ball.
|
|
619
|
+
{
|
|
620
|
+
type: "bend",
|
|
621
|
+
options: { curvature: o.progress * o.ball * 0.9, angle: 35 }
|
|
622
|
+
}
|
|
623
|
+
];
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
585
627
|
// src/config/schema.ts
|
|
586
|
-
var sheetSchema =
|
|
628
|
+
var sheetSchema = z12.object({
|
|
587
629
|
/** World units. A letter sheet is ~1 × 1.4, a receipt ~1 × 2.6. */
|
|
588
|
-
width:
|
|
589
|
-
height:
|
|
630
|
+
width: z12.number().positive().max(20).default(1),
|
|
631
|
+
height: z12.number().positive().max(20).default(1.4),
|
|
590
632
|
/** Visual thickness in mm-ish units; drives edge/shadow treatment, not geometry (yet). */
|
|
591
|
-
thickness:
|
|
592
|
-
/**
|
|
593
|
-
|
|
594
|
-
|
|
633
|
+
thickness: z12.number().min(0).max(2).default(0.2),
|
|
634
|
+
/**
|
|
635
|
+
* `'auto'` gives the LONG side 72 segments, whatever is on the sheet — a
|
|
636
|
+
* deformer's `minSegments` can only raise that floor, never lower it, so in
|
|
637
|
+
* practice a blank sheet is tessellated exactly as finely as a crumpled
|
|
638
|
+
* one. Set a number to take the decision yourself.
|
|
639
|
+
*/
|
|
640
|
+
segments: z12.union([z12.literal("auto"), z12.number().int().min(2).max(256)]).default("auto"),
|
|
641
|
+
cornerRadius: z12.number().min(0).max(0.5).default(0)
|
|
595
642
|
});
|
|
596
643
|
var stockNames = [
|
|
597
644
|
"printer",
|
|
@@ -602,46 +649,46 @@ var stockNames = [
|
|
|
602
649
|
"photo-gloss",
|
|
603
650
|
"sticker"
|
|
604
651
|
];
|
|
605
|
-
var stockSchema =
|
|
606
|
-
var blankContentBase =
|
|
607
|
-
type:
|
|
652
|
+
var stockSchema = z12.enum(stockNames);
|
|
653
|
+
var blankContentBase = z12.object({
|
|
654
|
+
type: z12.literal("blank")
|
|
608
655
|
});
|
|
609
|
-
var imageContentBase =
|
|
610
|
-
type:
|
|
611
|
-
src:
|
|
612
|
-
fit:
|
|
656
|
+
var imageContentBase = z12.object({
|
|
657
|
+
type: z12.literal("image"),
|
|
658
|
+
src: z12.string(),
|
|
659
|
+
fit: z12.enum(["cover", "contain"]).default("cover"),
|
|
613
660
|
/** Read by the hidden DOM mirror and the no-WebGL fallback. */
|
|
614
|
-
alt:
|
|
661
|
+
alt: z12.string().optional()
|
|
615
662
|
});
|
|
616
|
-
var textContentBase =
|
|
617
|
-
type:
|
|
618
|
-
text:
|
|
619
|
-
font:
|
|
663
|
+
var textContentBase = z12.object({
|
|
664
|
+
type: z12.literal("text"),
|
|
665
|
+
text: z12.string().default("Dear reader,"),
|
|
666
|
+
font: z12.string().default('Georgia, "Times New Roman", serif'),
|
|
620
667
|
/** px at texture resolution (long edge = 1024 logical px before DPR). */
|
|
621
|
-
size:
|
|
622
|
-
weight:
|
|
623
|
-
color:
|
|
624
|
-
align:
|
|
668
|
+
size: z12.number().min(8).max(256).default(44),
|
|
669
|
+
weight: z12.number().min(100).max(900).default(400),
|
|
670
|
+
color: z12.string().default("#2b2620"),
|
|
671
|
+
align: z12.enum(["left", "center", "right"]).default("left"),
|
|
625
672
|
/** Fraction of the short edge. */
|
|
626
|
-
padding:
|
|
627
|
-
lineHeight:
|
|
673
|
+
padding: z12.number().min(0).max(0.4).default(0.09),
|
|
674
|
+
lineHeight: z12.number().min(0.8).max(3).default(1.45)
|
|
628
675
|
});
|
|
629
|
-
var receiptContentBase =
|
|
630
|
-
type:
|
|
631
|
-
store:
|
|
632
|
-
address:
|
|
633
|
-
items:
|
|
676
|
+
var receiptContentBase = z12.object({
|
|
677
|
+
type: z12.literal("receipt"),
|
|
678
|
+
store: z12.string().default("PAPERLAB"),
|
|
679
|
+
address: z12.string().default("124 PAPER ST"),
|
|
680
|
+
items: z12.array(z12.object({ name: z12.string(), price: z12.number() })).default([
|
|
634
681
|
{ name: "CURL, TRUE", price: 12 },
|
|
635
682
|
{ name: "ROLL, TIGHT", price: 8.5 },
|
|
636
683
|
{ name: "SHEET, ONE", price: 0.99 }
|
|
637
684
|
]),
|
|
638
|
-
taxRate:
|
|
639
|
-
barcode:
|
|
685
|
+
taxRate: z12.number().min(0).max(1).default(0.08),
|
|
686
|
+
barcode: z12.boolean().default(true),
|
|
640
687
|
/** Fixed so presets render deterministically; omit for "now". */
|
|
641
|
-
timestamp:
|
|
642
|
-
footer:
|
|
688
|
+
timestamp: z12.string().optional(),
|
|
689
|
+
footer: z12.string().default("KEEP FOR YOUR RECORDS")
|
|
643
690
|
});
|
|
644
|
-
var backContentSchema =
|
|
691
|
+
var backContentSchema = z12.discriminatedUnion("type", [
|
|
645
692
|
blankContentBase,
|
|
646
693
|
imageContentBase,
|
|
647
694
|
textContentBase,
|
|
@@ -652,137 +699,140 @@ var blankContentSchema = blankContentBase.extend(withBack);
|
|
|
652
699
|
var imageContentSchema = imageContentBase.extend(withBack);
|
|
653
700
|
var textContentSchema = textContentBase.extend(withBack);
|
|
654
701
|
var receiptContentSchema = receiptContentBase.extend(withBack);
|
|
655
|
-
var contentSchema =
|
|
702
|
+
var contentSchema = z12.discriminatedUnion("type", [
|
|
656
703
|
blankContentSchema,
|
|
657
704
|
imageContentSchema,
|
|
658
705
|
textContentSchema,
|
|
659
706
|
receiptContentSchema
|
|
660
707
|
]);
|
|
661
708
|
var paperEdges = ["top", "right", "bottom", "left"];
|
|
662
|
-
var surfaceSchema =
|
|
709
|
+
var surfaceSchema = z12.object({
|
|
663
710
|
/** Paper fiber noise, 0..1. */
|
|
664
|
-
grain:
|
|
711
|
+
grain: z12.number().min(0).max(1).optional(),
|
|
712
|
+
/** Light passing through the sheet from behind, 0..1. Stock defaults apply. */
|
|
713
|
+
translucency: z12.number().min(0).max(1).optional(),
|
|
665
714
|
/** Torn-edge alpha with a lightened fiber band. */
|
|
666
|
-
deckle:
|
|
667
|
-
edges:
|
|
668
|
-
roughness:
|
|
715
|
+
deckle: z12.object({
|
|
716
|
+
edges: z12.array(z12.enum(paperEdges)).default(["bottom"]),
|
|
717
|
+
roughness: z12.number().min(0).max(1).default(0.5)
|
|
669
718
|
}).optional(),
|
|
670
719
|
/** Visual AO/highlight companion to the fold deformer. */
|
|
671
|
-
creaseLines:
|
|
720
|
+
creaseLines: z12.object({
|
|
672
721
|
/** Crease line direction, degrees (0 = horizontal lines). */
|
|
673
|
-
angle:
|
|
722
|
+
angle: z12.number().min(-360).max(360).default(0),
|
|
674
723
|
/** Positions across the sheet, 0..1 fractions. */
|
|
675
|
-
positions:
|
|
676
|
-
strength:
|
|
724
|
+
positions: z12.array(z12.number().min(0).max(1)).default([1 / 3, 2 / 3]),
|
|
725
|
+
strength: z12.number().min(0).max(1).default(0.5)
|
|
677
726
|
}).optional(),
|
|
678
727
|
/** Yellowing + foxing spots, 0..1. */
|
|
679
|
-
aging:
|
|
728
|
+
aging: z12.number().min(0).max(1).optional(),
|
|
680
729
|
/** Reversed front-content ghost on the backside, 0..1. Stock defaults apply. */
|
|
681
|
-
showThrough:
|
|
730
|
+
showThrough: z12.number().min(0).max(1).optional(),
|
|
682
731
|
/**
|
|
683
732
|
* Postage-stamp perforation: alpha-punched semicircular holes along chosen
|
|
684
733
|
* edges. `state` flips an edge to a ripped-through profile (torn) — set
|
|
685
734
|
* automatically when a paper detaches from a `sheet` field, manual wins.
|
|
686
735
|
*/
|
|
687
|
-
perforation:
|
|
688
|
-
edges:
|
|
736
|
+
perforation: z12.object({
|
|
737
|
+
edges: z12.union([z12.array(z12.enum(paperEdges)), z12.literal("all")]).default("all"),
|
|
689
738
|
/** World units — default tuned to stamp scale. */
|
|
690
|
-
holeRadius:
|
|
691
|
-
spacing:
|
|
692
|
-
state:
|
|
693
|
-
top:
|
|
694
|
-
right:
|
|
695
|
-
bottom:
|
|
696
|
-
left:
|
|
739
|
+
holeRadius: z12.number().min(2e-3).max(0.1).default(0.016),
|
|
740
|
+
spacing: z12.number().min(0.01).max(0.5).default(0.055),
|
|
741
|
+
state: z12.object({
|
|
742
|
+
top: z12.enum(["intact", "torn"]).optional(),
|
|
743
|
+
right: z12.enum(["intact", "torn"]).optional(),
|
|
744
|
+
bottom: z12.enum(["intact", "torn"]).optional(),
|
|
745
|
+
left: z12.enum(["intact", "torn"]).optional()
|
|
697
746
|
}).default({})
|
|
698
747
|
}).optional()
|
|
699
748
|
});
|
|
700
|
-
var behaviorConfigSchema =
|
|
701
|
-
peelOptionsSchema.extend({ type:
|
|
702
|
-
unrollOptionsSchema.extend({ type:
|
|
703
|
-
flipOptionsSchema.extend({ type:
|
|
704
|
-
letterFoldOptionsSchema.extend({ type:
|
|
705
|
-
hangOptionsSchema.extend({ type:
|
|
706
|
-
flyOptionsSchema.extend({ type:
|
|
707
|
-
fallOptionsSchema.extend({ type:
|
|
708
|
-
carryOptionsSchema.extend({ type:
|
|
709
|
-
flightOptionsSchema.extend({ type:
|
|
749
|
+
var behaviorConfigSchema = z12.discriminatedUnion("type", [
|
|
750
|
+
peelOptionsSchema.extend({ type: z12.literal("peel") }),
|
|
751
|
+
unrollOptionsSchema.extend({ type: z12.literal("unroll") }),
|
|
752
|
+
flipOptionsSchema.extend({ type: z12.literal("flip") }),
|
|
753
|
+
letterFoldOptionsSchema.extend({ type: z12.literal("letter-fold") }),
|
|
754
|
+
hangOptionsSchema.extend({ type: z12.literal("hang") }),
|
|
755
|
+
flyOptionsSchema.extend({ type: z12.literal("fly") }),
|
|
756
|
+
fallOptionsSchema.extend({ type: z12.literal("fall") }),
|
|
757
|
+
carryOptionsSchema.extend({ type: z12.literal("carry") }),
|
|
758
|
+
flightOptionsSchema.extend({ type: z12.literal("flight") }),
|
|
759
|
+
crumpleBehaviorOptionsSchema.extend({ type: z12.literal("crumple") })
|
|
710
760
|
]);
|
|
711
|
-
var deformerInstanceSchema =
|
|
712
|
-
type:
|
|
713
|
-
options:
|
|
714
|
-
enabled:
|
|
761
|
+
var deformerInstanceSchema = z12.object({
|
|
762
|
+
type: z12.string(),
|
|
763
|
+
options: z12.record(z12.unknown()).default({}),
|
|
764
|
+
enabled: z12.boolean().default(true)
|
|
715
765
|
});
|
|
716
766
|
var physicsNames = ["none", "float", "tumble", "dangle", "taped", "breeze"];
|
|
717
|
-
var clothConfigSchema =
|
|
718
|
-
type:
|
|
719
|
-
pins:
|
|
720
|
-
wind:
|
|
767
|
+
var clothConfigSchema = z12.object({
|
|
768
|
+
type: z12.literal("cloth"),
|
|
769
|
+
pins: z12.enum(["top-edge", "top-corners", "corner", "none"]).default("top-edge"),
|
|
770
|
+
wind: z12.number().min(0).max(1).default(0.3),
|
|
721
771
|
/** Bend stiffness: 1 = crisp paper, 0 = silk. */
|
|
722
|
-
stiffness:
|
|
723
|
-
gravity:
|
|
772
|
+
stiffness: z12.number().min(0).max(1).default(0.8),
|
|
773
|
+
gravity: z12.number().min(0).max(2).default(1),
|
|
724
774
|
/** Local-space ground plane the sheet settles onto. */
|
|
725
|
-
floor:
|
|
775
|
+
floor: z12.number().min(-5).max(0).default(-1.4)
|
|
726
776
|
});
|
|
727
|
-
var physicsSchema =
|
|
728
|
-
|
|
729
|
-
|
|
777
|
+
var physicsSchema = z12.union([
|
|
778
|
+
z12.enum(physicsNames),
|
|
779
|
+
z12.literal("cloth").transform(() => clothConfigSchema.parse({ type: "cloth" })),
|
|
730
780
|
clothConfigSchema
|
|
731
781
|
]);
|
|
732
|
-
var lightingNames = ["studio", "window", "leaves", "goldenhour", "noir"];
|
|
733
|
-
var sceneSchema =
|
|
734
|
-
lighting:
|
|
782
|
+
var lightingNames = ["studio", "window", "leaves", "goldenhour", "noir", "nave"];
|
|
783
|
+
var sceneSchema = z12.object({
|
|
784
|
+
lighting: z12.enum(lightingNames).default("studio")
|
|
735
785
|
});
|
|
736
786
|
var coreStateNames = ["rest", "hover", "pressed", "picked", "placed"];
|
|
737
787
|
var isStateName = (s) => coreStateNames.includes(s) || s.startsWith("custom:");
|
|
738
|
-
var stateNameSchema =
|
|
788
|
+
var stateNameSchema = z12.string().refine(isStateName, {
|
|
739
789
|
message: `state names are ${coreStateNames.join(", ")} or "custom:<name>"`
|
|
740
790
|
});
|
|
741
|
-
var stateTransitionSchema =
|
|
742
|
-
duration:
|
|
791
|
+
var stateTransitionSchema = z12.object({
|
|
792
|
+
duration: z12.number().min(0).max(5).default(0.35),
|
|
743
793
|
/** GSAP ease name. */
|
|
744
|
-
ease:
|
|
794
|
+
ease: z12.string().default("power2.out")
|
|
745
795
|
});
|
|
746
|
-
var stateDefSchema =
|
|
796
|
+
var stateDefSchema = z12.object({
|
|
747
797
|
/** Deep-partial override of the paper schema (behavior params, surface, …). */
|
|
748
|
-
overrides:
|
|
798
|
+
overrides: z12.record(z12.unknown()).default({}),
|
|
749
799
|
/** Transition INTO this state. */
|
|
750
800
|
transition: stateTransitionSchema.default({}),
|
|
751
801
|
/** Chained actions after arriving. v1: 'emit:<event>' only. */
|
|
752
|
-
onEnter:
|
|
802
|
+
onEnter: z12.array(z12.string().regex(/^emit:[\w-]+$/, 'v1 actions are "emit:<event>"')).default([])
|
|
753
803
|
});
|
|
754
|
-
var paperStatesSchema =
|
|
804
|
+
var paperStatesSchema = z12.object({
|
|
755
805
|
initial: stateNameSchema.default("rest"),
|
|
756
|
-
states:
|
|
806
|
+
states: z12.record(z12.string(), stateDefSchema).default({}).refine((rec) => Object.keys(rec).every(isStateName), {
|
|
757
807
|
message: `state names are ${coreStateNames.join(", ")} or "custom:<name>"`
|
|
758
808
|
}),
|
|
759
809
|
/** World-units drag distance that flips pressed → picked (pick-enabled behaviors only). */
|
|
760
|
-
pickThreshold:
|
|
810
|
+
pickThreshold: z12.number().min(5e-3).max(1).default(0.1)
|
|
761
811
|
});
|
|
762
|
-
var metaSchema =
|
|
763
|
-
name:
|
|
764
|
-
author:
|
|
765
|
-
version:
|
|
766
|
-
tags:
|
|
812
|
+
var metaSchema = z12.object({
|
|
813
|
+
name: z12.string().default("untitled"),
|
|
814
|
+
author: z12.string().optional(),
|
|
815
|
+
version: z12.string().default("0"),
|
|
816
|
+
tags: z12.array(z12.string()).default([])
|
|
767
817
|
});
|
|
768
|
-
var paperConfigSchema =
|
|
818
|
+
var paperConfigSchema = z12.object({
|
|
769
819
|
meta: metaSchema.default({}),
|
|
770
820
|
sheet: sheetSchema.default({}),
|
|
771
821
|
stock: stockSchema.default("printer"),
|
|
772
822
|
content: contentSchema.default({ type: "blank" }),
|
|
773
823
|
/** A behavior OR a raw deformer stack — if both are present, `deformers` wins (it's the fork). */
|
|
774
824
|
behavior: behaviorConfigSchema.optional(),
|
|
775
|
-
deformers:
|
|
825
|
+
deformers: z12.array(deformerInstanceSchema).optional(),
|
|
776
826
|
surface: surfaceSchema.default({}),
|
|
777
827
|
physics: physicsSchema.default("none"),
|
|
778
828
|
scene: sceneSchema.default({}),
|
|
779
|
-
onTwos:
|
|
829
|
+
onTwos: z12.boolean().default(false),
|
|
780
830
|
/** Interaction state machine — overrides-on-base diffs (spec M6 §1). */
|
|
781
831
|
states: paperStatesSchema.optional()
|
|
782
832
|
}).superRefine((config, ctx) => {
|
|
783
833
|
if (typeof config.physics === "object" && (config.behavior || config.deformers)) {
|
|
784
834
|
ctx.addIssue({
|
|
785
|
-
code:
|
|
835
|
+
code: z12.ZodIssueCode.custom,
|
|
786
836
|
path: ["physics"],
|
|
787
837
|
message: "cloth physics and behavior/deformers are exclusive \u2014 cloth owns the vertices (pick Shape OR Simulation)"
|
|
788
838
|
});
|
|
@@ -793,7 +843,7 @@ var paperConfigSchema = z11.object({
|
|
|
793
843
|
if (!def) continue;
|
|
794
844
|
if (def.overrides.states !== void 0) {
|
|
795
845
|
ctx.addIssue({
|
|
796
|
-
code:
|
|
846
|
+
code: z12.ZodIssueCode.custom,
|
|
797
847
|
path: ["states", "states", name, "overrides"],
|
|
798
848
|
message: "state overrides cannot override `states` (no nested state machines)"
|
|
799
849
|
});
|
|
@@ -804,7 +854,7 @@ var paperConfigSchema = z11.object({
|
|
|
804
854
|
if (!result.success) {
|
|
805
855
|
const first = result.error.issues[0];
|
|
806
856
|
ctx.addIssue({
|
|
807
|
-
code:
|
|
857
|
+
code: z12.ZodIssueCode.custom,
|
|
808
858
|
path: ["states", "states", name, "overrides"],
|
|
809
859
|
message: `state "${name}" overrides don't validate against the paper schema: ${first ? `${first.path.join(".")} \u2014 ${first.message}` : "invalid"}`
|
|
810
860
|
});
|
|
@@ -848,6 +898,7 @@ var stocks = {
|
|
|
848
898
|
color: "#fbfaf7",
|
|
849
899
|
roughness: 0.88,
|
|
850
900
|
opacity: 1,
|
|
901
|
+
translucency: 0.2,
|
|
851
902
|
inkColor: "#222222",
|
|
852
903
|
banding: 0,
|
|
853
904
|
defaultSurface: { grain: 0.12 },
|
|
@@ -860,6 +911,7 @@ var stocks = {
|
|
|
860
911
|
color: "#f6f3e9",
|
|
861
912
|
roughness: 0.62,
|
|
862
913
|
opacity: 1,
|
|
914
|
+
translucency: 0.34,
|
|
863
915
|
inkColor: "#3a3a3a",
|
|
864
916
|
banding: 0.35,
|
|
865
917
|
defaultSurface: { aging: 0.1 },
|
|
@@ -872,6 +924,7 @@ var stocks = {
|
|
|
872
924
|
color: "#c9a06c",
|
|
873
925
|
roughness: 0.96,
|
|
874
926
|
opacity: 1,
|
|
927
|
+
translucency: 0.08,
|
|
875
928
|
inkColor: "#33261a",
|
|
876
929
|
banding: 0,
|
|
877
930
|
defaultSurface: { grain: 0.5 },
|
|
@@ -884,6 +937,7 @@ var stocks = {
|
|
|
884
937
|
color: "#e9e4d6",
|
|
885
938
|
roughness: 0.95,
|
|
886
939
|
opacity: 1,
|
|
940
|
+
translucency: 0.38,
|
|
887
941
|
inkColor: "#3d3a34",
|
|
888
942
|
banding: 0,
|
|
889
943
|
defaultSurface: { grain: 0.7, aging: 0.15 },
|
|
@@ -896,6 +950,7 @@ var stocks = {
|
|
|
896
950
|
color: "#f4f2ec",
|
|
897
951
|
roughness: 0.42,
|
|
898
952
|
opacity: 0.62,
|
|
953
|
+
translucency: 0.86,
|
|
899
954
|
inkColor: "#4a453d",
|
|
900
955
|
banding: 0,
|
|
901
956
|
defaultSurface: {},
|
|
@@ -908,6 +963,7 @@ var stocks = {
|
|
|
908
963
|
color: "#ffffff",
|
|
909
964
|
roughness: 0.22,
|
|
910
965
|
opacity: 1,
|
|
966
|
+
translucency: 0.03,
|
|
911
967
|
inkColor: "#111111",
|
|
912
968
|
banding: 0,
|
|
913
969
|
defaultSurface: {},
|
|
@@ -922,6 +978,7 @@ var stocks = {
|
|
|
922
978
|
color: "#ffffff",
|
|
923
979
|
roughness: 0.3,
|
|
924
980
|
opacity: 1,
|
|
981
|
+
translucency: 0.06,
|
|
925
982
|
inkColor: "#1a1a1a",
|
|
926
983
|
banding: 0,
|
|
927
984
|
defaultSurface: {},
|
|
@@ -1079,7 +1136,25 @@ var builtins = {
|
|
|
1079
1136
|
type: "image",
|
|
1080
1137
|
src: "https://images.unsplash.com/photo-1501854140801-50d01698950b?w=1200&q=80",
|
|
1081
1138
|
fit: "cover"
|
|
1082
|
-
}
|
|
1139
|
+
},
|
|
1140
|
+
// No print lies perfectly flat. A shade of bow is the whole difference
|
|
1141
|
+
// between a sheet of paper and a rectangle — and since this is the field
|
|
1142
|
+
// starter, it is what a layout's per-sheet bias has to scale.
|
|
1143
|
+
deformers: [{ type: "bend", options: { curvature: 0.35, angle: 0 } }]
|
|
1144
|
+
},
|
|
1145
|
+
"crumpled-note": {
|
|
1146
|
+
meta: { name: "Crumpled note", tags: ["crumple", "text", "handled"] },
|
|
1147
|
+
sheet: { width: 1.1, height: 1.4 },
|
|
1148
|
+
stock: "printer",
|
|
1149
|
+
content: {
|
|
1150
|
+
type: "text",
|
|
1151
|
+
text: "I wrote it out three times\nand threw all three away.",
|
|
1152
|
+
size: 42
|
|
1153
|
+
},
|
|
1154
|
+
behavior: { type: "crumple", progress: 0.62, coarseness: 0.4, ball: 0.55 },
|
|
1155
|
+
// Handled paper is dirty paper: the grain is what stops the facets
|
|
1156
|
+
// reading as folded plastic.
|
|
1157
|
+
surface: { grain: 0.5, aging: 0.18 }
|
|
1083
1158
|
},
|
|
1084
1159
|
"typed-note": {
|
|
1085
1160
|
meta: { name: "Typed note", tags: ["text", "starter"] },
|
|
@@ -1315,17 +1390,20 @@ function useContentTexture(content, sheet2, stock) {
|
|
|
1315
1390
|
// src/deformers/compose.ts
|
|
1316
1391
|
import * as THREE3 from "three";
|
|
1317
1392
|
|
|
1393
|
+
// src/deformers/registry.ts
|
|
1394
|
+
import { z as z19 } from "zod";
|
|
1395
|
+
|
|
1318
1396
|
// src/deformers/roll.ts
|
|
1319
|
-
import { z as
|
|
1320
|
-
var rollOptionsSchema =
|
|
1397
|
+
import { z as z13 } from "zod";
|
|
1398
|
+
var rollOptionsSchema = z13.object({
|
|
1321
1399
|
/** Direction of rolling in the sheet plane, degrees. 0 = +x, 90 = +y. */
|
|
1322
|
-
angle:
|
|
1400
|
+
angle: z13.number().min(-360).max(360).default(90),
|
|
1323
1401
|
/** Signed distance (along the roll direction, from sheet center) where the roll begins. */
|
|
1324
|
-
boundary:
|
|
1402
|
+
boundary: z13.number().min(-20).max(20).default(0),
|
|
1325
1403
|
/** Cylinder radius — sharpness of the roll. */
|
|
1326
|
-
radius:
|
|
1404
|
+
radius: z13.number().min(0.01).max(2).default(0.12),
|
|
1327
1405
|
/** Radius growth per radian so multi-turn rolls spiral instead of z-fighting. */
|
|
1328
|
-
spiral:
|
|
1406
|
+
spiral: z13.number().min(0).max(0.2).default(0.015)
|
|
1329
1407
|
});
|
|
1330
1408
|
var DEG2 = Math.PI / 180;
|
|
1331
1409
|
var roll = {
|
|
@@ -1380,15 +1458,20 @@ void FN(inout vec3 p, vec2 uv, float t) {
|
|
|
1380
1458
|
};
|
|
1381
1459
|
|
|
1382
1460
|
// src/deformers/bend.ts
|
|
1383
|
-
import { z as
|
|
1384
|
-
var bendOptionsSchema =
|
|
1461
|
+
import { z as z14 } from "zod";
|
|
1462
|
+
var bendOptionsSchema = z14.object({
|
|
1385
1463
|
/** 1/radius in world units; sign flips the arc direction. 0 = flat. */
|
|
1386
|
-
curvature:
|
|
1464
|
+
curvature: z14.number().min(-4).max(4).default(0.6),
|
|
1387
1465
|
/** Bend axis direction in the sheet plane, degrees. 0 bends across x. */
|
|
1388
|
-
angle:
|
|
1466
|
+
angle: z14.number().min(-360).max(360).default(0)
|
|
1389
1467
|
});
|
|
1390
1468
|
var DEG3 = Math.PI / 180;
|
|
1391
1469
|
var EPS = 1e-5;
|
|
1470
|
+
function sinMinusX(x) {
|
|
1471
|
+
if (Math.abs(x) > 1) return Math.sin(x) - x;
|
|
1472
|
+
const x2 = x * x;
|
|
1473
|
+
return -x * x2 / 6 * (1 - x2 / 20 * (1 - x2 / 42 * (1 - x2 / 72)));
|
|
1474
|
+
}
|
|
1392
1475
|
var bend = {
|
|
1393
1476
|
id: "bend",
|
|
1394
1477
|
label: "Bend",
|
|
@@ -1401,49 +1484,56 @@ var bend = {
|
|
|
1401
1484
|
const dirY = Math.sin(o.angle * DEG3);
|
|
1402
1485
|
const d = out.x * dirX + out.y * dirY;
|
|
1403
1486
|
const r = 1 / o.curvature;
|
|
1404
|
-
const theta = d
|
|
1487
|
+
const theta = d * o.curvature;
|
|
1405
1488
|
const sin = Math.sin(theta);
|
|
1406
|
-
const
|
|
1407
|
-
const
|
|
1408
|
-
const
|
|
1409
|
-
out.x += dirX *
|
|
1410
|
-
out.y += dirY *
|
|
1411
|
-
out.z =
|
|
1489
|
+
const halfSin = Math.sin(theta * 0.5);
|
|
1490
|
+
const z0 = out.z;
|
|
1491
|
+
const shift = r * sinMinusX(theta) - z0 * sin;
|
|
1492
|
+
out.x += dirX * shift;
|
|
1493
|
+
out.y += dirY * shift;
|
|
1494
|
+
out.z = 2 * r * halfSin * halfSin + z0 * Math.cos(theta);
|
|
1412
1495
|
},
|
|
1413
1496
|
glsl: {
|
|
1414
1497
|
chunk: (
|
|
1415
1498
|
/* glsl */
|
|
1416
1499
|
`
|
|
1500
|
+
float FN_sinm(float x) {
|
|
1501
|
+
if (abs(x) > 1.0) return sin(x) - x;
|
|
1502
|
+
float x2 = x * x;
|
|
1503
|
+
return (-x * x2 / 6.0) * (1.0 - (x2 / 20.0) * (1.0 - (x2 / 42.0) * (1.0 - x2 / 72.0)));
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1417
1506
|
void FN(inout vec3 p, vec2 uv, float t) {
|
|
1418
1507
|
if (abs(U_curvature) < 1e-5) return;
|
|
1419
1508
|
vec2 dir = vec2(cos(U_angle), sin(U_angle));
|
|
1420
1509
|
float d = dot(p.xy, dir);
|
|
1421
1510
|
float r = 1.0 / U_curvature;
|
|
1422
|
-
float theta = d
|
|
1511
|
+
float theta = d * U_curvature;
|
|
1423
1512
|
float sn = sin(theta);
|
|
1424
|
-
float
|
|
1425
|
-
float
|
|
1426
|
-
float
|
|
1427
|
-
p.xy += dir *
|
|
1428
|
-
p.z =
|
|
1513
|
+
float hs = sin(theta * 0.5);
|
|
1514
|
+
float z0 = p.z;
|
|
1515
|
+
float shift = r * FN_sinm(theta) - z0 * sn;
|
|
1516
|
+
p.xy += dir * shift;
|
|
1517
|
+
p.z = 2.0 * r * hs * hs + z0 * cos(theta);
|
|
1429
1518
|
}
|
|
1430
1519
|
`
|
|
1431
1520
|
),
|
|
1521
|
+
strength: "curvature",
|
|
1432
1522
|
uniforms: (o) => ({ curvature: o.curvature, angle: o.angle * DEG3 })
|
|
1433
1523
|
}
|
|
1434
1524
|
};
|
|
1435
1525
|
|
|
1436
1526
|
// src/deformers/fold.ts
|
|
1437
|
-
import { z as
|
|
1438
|
-
var foldOptionsSchema =
|
|
1527
|
+
import { z as z15 } from "zod";
|
|
1528
|
+
var foldOptionsSchema = z15.object({
|
|
1439
1529
|
/** Direction of the fold travel in the sheet plane, degrees (the crease line runs perpendicular). */
|
|
1440
|
-
angle:
|
|
1530
|
+
angle: z15.number().min(-360).max(360).default(90),
|
|
1441
1531
|
/** Signed distance of the crease line from the sheet center, along the travel direction. */
|
|
1442
|
-
offset:
|
|
1532
|
+
offset: z15.number().min(-20).max(20).default(0),
|
|
1443
1533
|
/** How far the flap folds over, degrees. 180 = flat against the sheet. */
|
|
1444
|
-
foldAngle:
|
|
1534
|
+
foldAngle: z15.number().min(-180).max(180).default(90),
|
|
1445
1535
|
/** Width of the soft hinge — paper never creases to a mathematical edge. */
|
|
1446
|
-
radius:
|
|
1536
|
+
radius: z15.number().min(5e-3).max(0.5).default(0.04)
|
|
1447
1537
|
});
|
|
1448
1538
|
var DEG4 = Math.PI / 180;
|
|
1449
1539
|
var fold = {
|
|
@@ -1511,6 +1601,7 @@ void FN(inout vec3 p, vec2 uv, float t) {
|
|
|
1511
1601
|
}
|
|
1512
1602
|
`
|
|
1513
1603
|
),
|
|
1604
|
+
strength: "foldAngle",
|
|
1514
1605
|
uniforms: (o) => ({
|
|
1515
1606
|
angle: o.angle * DEG4,
|
|
1516
1607
|
offset: o.offset,
|
|
@@ -1521,16 +1612,16 @@ void FN(inout vec3 p, vec2 uv, float t) {
|
|
|
1521
1612
|
};
|
|
1522
1613
|
|
|
1523
1614
|
// src/deformers/wave.ts
|
|
1524
|
-
import { z as
|
|
1525
|
-
var waveOptionsSchema =
|
|
1526
|
-
amplitude:
|
|
1527
|
-
wavelength:
|
|
1615
|
+
import { z as z16 } from "zod";
|
|
1616
|
+
var waveOptionsSchema = z16.object({
|
|
1617
|
+
amplitude: z16.number().min(0).max(0.3).default(0.04),
|
|
1618
|
+
wavelength: z16.number().min(0.05).max(2).default(0.5),
|
|
1528
1619
|
/** Travel speed; 0 freezes the ripple. */
|
|
1529
|
-
speed:
|
|
1620
|
+
speed: z16.number().min(0).max(3).default(0.8),
|
|
1530
1621
|
/** Travel direction in the sheet plane, degrees. */
|
|
1531
|
-
angle:
|
|
1622
|
+
angle: z16.number().min(-360).max(360).default(90),
|
|
1532
1623
|
/** Zero the displacement at one edge (a taped/pinned edge doesn't ripple). */
|
|
1533
|
-
pinnedEdge:
|
|
1624
|
+
pinnedEdge: z16.enum(["none", "top", "bottom", "left", "right"]).default("none")
|
|
1534
1625
|
});
|
|
1535
1626
|
var DEG5 = Math.PI / 180;
|
|
1536
1627
|
var TAU = Math.PI * 2;
|
|
@@ -1572,6 +1663,7 @@ void FN(inout vec3 p, vec2 uv, float t) {
|
|
|
1572
1663
|
}
|
|
1573
1664
|
`
|
|
1574
1665
|
),
|
|
1666
|
+
strength: "amplitude",
|
|
1575
1667
|
uniforms: (o) => ({
|
|
1576
1668
|
amplitude: o.amplitude,
|
|
1577
1669
|
wavelength: o.wavelength,
|
|
@@ -1582,6 +1674,188 @@ void FN(inout vec3 p, vec2 uv, float t) {
|
|
|
1582
1674
|
}
|
|
1583
1675
|
};
|
|
1584
1676
|
|
|
1677
|
+
// src/deformers/drape.ts
|
|
1678
|
+
import { z as z17 } from "zod";
|
|
1679
|
+
var drapeOptionsSchema = z17.object({
|
|
1680
|
+
/** Fold depth at the free edge, world units. */
|
|
1681
|
+
amplitude: z17.number().min(0).max(0.6).default(0.12),
|
|
1682
|
+
/** How many folds run down the drop. */
|
|
1683
|
+
folds: z17.number().min(0.5).max(16).default(4),
|
|
1684
|
+
/**
|
|
1685
|
+
* How fast folds deepen away from the pinned edge. 1 is linear; higher
|
|
1686
|
+
* holds the top flat and gathers all the movement at the free end, which
|
|
1687
|
+
* is what a sheet hung from a rod actually does.
|
|
1688
|
+
*/
|
|
1689
|
+
falloff: z17.number().min(0.3).max(4).default(1.6),
|
|
1690
|
+
/** How much a second, non-harmonic fold breaks the regularity. */
|
|
1691
|
+
irregular: z17.number().min(0).max(1).default(0.45),
|
|
1692
|
+
/** How much the sheet narrows as its folds deepen. */
|
|
1693
|
+
gather: z17.number().min(0).max(1).default(0.5),
|
|
1694
|
+
pinnedEdge: z17.enum(["top", "bottom"]).default("top")
|
|
1695
|
+
});
|
|
1696
|
+
var TAU2 = Math.PI * 2;
|
|
1697
|
+
var drape = {
|
|
1698
|
+
id: "drape",
|
|
1699
|
+
label: "Drape",
|
|
1700
|
+
defaults: drapeOptionsSchema.parse({}),
|
|
1701
|
+
optionsSchema: drapeOptionsSchema,
|
|
1702
|
+
geometry: { minSegments: 48 },
|
|
1703
|
+
displace(out, uv, o) {
|
|
1704
|
+
if (o.amplitude === 0) return;
|
|
1705
|
+
const drop = o.pinnedEdge === "top" ? 1 - uv.y : uv.y;
|
|
1706
|
+
const depth = drop ** o.falloff;
|
|
1707
|
+
const u = uv.x * TAU2 * o.folds;
|
|
1708
|
+
const fold2 = Math.sin(u) + o.irregular * 0.6 * Math.sin(u * 1.7 + 2.1);
|
|
1709
|
+
out.z += o.amplitude * depth * fold2;
|
|
1710
|
+
const pinch = o.gather * depth * Math.min(o.amplitude * o.folds * 0.8, 0.6);
|
|
1711
|
+
out.x *= 1 - pinch;
|
|
1712
|
+
},
|
|
1713
|
+
glsl: {
|
|
1714
|
+
chunk: (
|
|
1715
|
+
/* glsl */
|
|
1716
|
+
`
|
|
1717
|
+
void FN(inout vec3 p, vec2 uv, float t) {
|
|
1718
|
+
if (U_amplitude == 0.0) return;
|
|
1719
|
+
float drop = U_pin == 1.0 ? 1.0 - uv.y : uv.y;
|
|
1720
|
+
float depth = pow(drop, U_falloff);
|
|
1721
|
+
float u = uv.x * 6.283185307179586 * U_folds;
|
|
1722
|
+
float fold = sin(u) + U_irregular * 0.6 * sin(u * 1.7 + 2.1);
|
|
1723
|
+
p.z += U_amplitude * depth * fold;
|
|
1724
|
+
float pinch = U_gather * depth * min(U_amplitude * U_folds * 0.8, 0.6);
|
|
1725
|
+
p.x *= 1.0 - pinch;
|
|
1726
|
+
}
|
|
1727
|
+
`
|
|
1728
|
+
),
|
|
1729
|
+
strength: "amplitude",
|
|
1730
|
+
uniforms: (o) => ({
|
|
1731
|
+
amplitude: o.amplitude,
|
|
1732
|
+
folds: o.folds,
|
|
1733
|
+
falloff: o.falloff,
|
|
1734
|
+
irregular: o.irregular,
|
|
1735
|
+
gather: o.gather,
|
|
1736
|
+
pin: o.pinnedEdge === "top" ? 1 : 2
|
|
1737
|
+
})
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1741
|
+
// src/deformers/crumple.ts
|
|
1742
|
+
import { z as z18 } from "zod";
|
|
1743
|
+
var crumpleOptionsSchema = z18.object({
|
|
1744
|
+
/** How crushed, 0..1. Peak-to-peak height, and it drives the pull too. */
|
|
1745
|
+
amount: z18.number().min(0).max(1).default(0.35),
|
|
1746
|
+
/** Facets per world unit. Higher is finer, and needs more segments to resolve. */
|
|
1747
|
+
scale: z18.number().min(0.5).max(8).default(3),
|
|
1748
|
+
/**
|
|
1749
|
+
* How much the sheet draws in on itself. Crumpled paper occupies a smaller
|
|
1750
|
+
* footprint than flat paper; without this it reads as an embossed sheet
|
|
1751
|
+
* rather than a crushed one.
|
|
1752
|
+
*/
|
|
1753
|
+
pull: z18.number().min(0).max(1).default(0.4),
|
|
1754
|
+
/** A different crush of the same paper. */
|
|
1755
|
+
seed: z18.number().int().min(0).max(7).default(0)
|
|
1756
|
+
});
|
|
1757
|
+
function mod(x, y) {
|
|
1758
|
+
return x - y * Math.floor(x / y);
|
|
1759
|
+
}
|
|
1760
|
+
function jitter(cx, cy, seed) {
|
|
1761
|
+
const hx = mod(cx * 37 + cy * 17 + seed * 5, 64);
|
|
1762
|
+
const hy = mod(cx * 23 + cy * 41 + seed * 11, 64);
|
|
1763
|
+
return [0.2 + 0.6 * mod(hx * 13, 7) / 6, 0.2 + 0.6 * mod(hy * 29, 11) / 10];
|
|
1764
|
+
}
|
|
1765
|
+
var NORM = 0.5366;
|
|
1766
|
+
function cellSign(cx, cy, seed) {
|
|
1767
|
+
return 1 - 2 * mod(cx * 11 + cy * 7 + seed * 3, 2);
|
|
1768
|
+
}
|
|
1769
|
+
var crumple = {
|
|
1770
|
+
id: "crumple",
|
|
1771
|
+
label: "Crumple",
|
|
1772
|
+
defaults: crumpleOptionsSchema.parse({}),
|
|
1773
|
+
optionsSchema: crumpleOptionsSchema,
|
|
1774
|
+
// A crease the grid cannot resolve is a smooth bump, and a sheet of smooth
|
|
1775
|
+
// bumps is not a crumple. Note this is a FLOOR, and `segments: 'auto'`
|
|
1776
|
+
// already hands the long side 72 — so this only bites when a preset asks
|
|
1777
|
+
// for a coarser grid by hand. Measured (`pnpm perf:field`), the real cost
|
|
1778
|
+
// of this deformer is not geometry at all: it is nine cell evaluations per
|
|
1779
|
+
// probe and three probes per vertex for the normal.
|
|
1780
|
+
geometry: { minSegments: 72 },
|
|
1781
|
+
displace(out, _uv, o) {
|
|
1782
|
+
if (o.amount === 0) return;
|
|
1783
|
+
const qx = out.x * o.scale;
|
|
1784
|
+
const qy = out.y * o.scale;
|
|
1785
|
+
const gx = Math.floor(qx);
|
|
1786
|
+
const gy = Math.floor(qy);
|
|
1787
|
+
let f1 = 1e9;
|
|
1788
|
+
let f2 = 1e9;
|
|
1789
|
+
let winX = gx;
|
|
1790
|
+
let winY = gy;
|
|
1791
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1792
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1793
|
+
const cx = gx + dx;
|
|
1794
|
+
const cy = gy + dy;
|
|
1795
|
+
const [jx, jy] = jitter(cx, cy, o.seed);
|
|
1796
|
+
const ex = cx + jx - qx;
|
|
1797
|
+
const ey = cy + jy - qy;
|
|
1798
|
+
const dist = Math.sqrt(ex * ex + ey * ey);
|
|
1799
|
+
if (dist < f1) {
|
|
1800
|
+
f2 = f1;
|
|
1801
|
+
f1 = dist;
|
|
1802
|
+
winX = cx;
|
|
1803
|
+
winY = cy;
|
|
1804
|
+
} else if (dist < f2) {
|
|
1805
|
+
f2 = dist;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
out.z += cellSign(winX, winY, o.seed) * (f2 - f1) * o.amount * NORM;
|
|
1810
|
+
const pull = 1 - o.amount * o.pull * 0.35;
|
|
1811
|
+
out.x *= pull;
|
|
1812
|
+
out.y *= pull;
|
|
1813
|
+
},
|
|
1814
|
+
glsl: {
|
|
1815
|
+
chunk: (
|
|
1816
|
+
/* glsl */
|
|
1817
|
+
`
|
|
1818
|
+
vec2 FN_jitter(float cx, float cy, float seed) {
|
|
1819
|
+
float hx = mod(cx * 37.0 + cy * 17.0 + seed * 5.0, 64.0);
|
|
1820
|
+
float hy = mod(cx * 23.0 + cy * 41.0 + seed * 11.0, 64.0);
|
|
1821
|
+
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);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
float FN_sign(float cx, float cy, float seed) {
|
|
1825
|
+
return 1.0 - 2.0 * mod(cx * 11.0 + cy * 7.0 + seed * 3.0, 2.0);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
void FN(inout vec3 p, vec2 uv, float t) {
|
|
1829
|
+
if (U_amount == 0.0) return;
|
|
1830
|
+
vec2 flat2 = p.xy;
|
|
1831
|
+
vec2 q = flat2 * U_scale;
|
|
1832
|
+
vec2 g = floor(q);
|
|
1833
|
+
|
|
1834
|
+
float f1 = 1e9;
|
|
1835
|
+
float f2 = 1e9;
|
|
1836
|
+
vec2 win = g;
|
|
1837
|
+
for (int dy = -1; dy <= 1; dy++) {
|
|
1838
|
+
for (int dx = -1; dx <= 1; dx++) {
|
|
1839
|
+
vec2 c = g + vec2(float(dx), float(dy));
|
|
1840
|
+
float dist = length(c + FN_jitter(c.x, c.y, U_seed) - q);
|
|
1841
|
+
if (dist < f1) { f2 = f1; f1 = dist; win = c; }
|
|
1842
|
+
else if (dist < f2) { f2 = dist; }
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
p.z += FN_sign(win.x, win.y, U_seed) * (f2 - f1) * U_amount * ${NORM};
|
|
1847
|
+
float pull = 1.0 - U_amount * U_pull * 0.35;
|
|
1848
|
+
p.xy = flat2 * pull;
|
|
1849
|
+
}
|
|
1850
|
+
`
|
|
1851
|
+
),
|
|
1852
|
+
// `amount` drives both the height and the pull, so a field instance's
|
|
1853
|
+
// bias scales the whole crush rather than half of it.
|
|
1854
|
+
strength: "amount",
|
|
1855
|
+
uniforms: (o) => ({ amount: o.amount, scale: o.scale, pull: o.pull, seed: o.seed })
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1585
1859
|
// src/deformers/registry.ts
|
|
1586
1860
|
var registry = /* @__PURE__ */ new Map();
|
|
1587
1861
|
function registerDeformer(deformer) {
|
|
@@ -1602,6 +1876,26 @@ registerDeformer(curl);
|
|
|
1602
1876
|
registerDeformer(bend);
|
|
1603
1877
|
registerDeformer(fold);
|
|
1604
1878
|
registerDeformer(wave);
|
|
1879
|
+
registerDeformer(drape);
|
|
1880
|
+
registerDeformer(crumple);
|
|
1881
|
+
function resolveDeformerStack(raw) {
|
|
1882
|
+
return raw.map((instance, i) => {
|
|
1883
|
+
const deformer = getDeformer(instance.type);
|
|
1884
|
+
const schema = deformer.optionsSchema instanceof z19.ZodObject ? deformer.optionsSchema.strict() : deformer.optionsSchema;
|
|
1885
|
+
const parsed = schema.safeParse(instance.options ?? {});
|
|
1886
|
+
if (!parsed.success) {
|
|
1887
|
+
const issue = parsed.error.issues[0];
|
|
1888
|
+
throw new Error(
|
|
1889
|
+
`[paperlab] deformers[${i}] ("${instance.type}"): ${issue ? `${issue.path.join(".") || "options"} \u2014 ${issue.message}` : "invalid options"}`
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
type: instance.type,
|
|
1894
|
+
options: parsed.data,
|
|
1895
|
+
enabled: instance.enabled
|
|
1896
|
+
};
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1605
1899
|
function stackIsAnimated(stack) {
|
|
1606
1900
|
return stack.some((i) => i.enabled !== false && registry.get(i.type)?.animated);
|
|
1607
1901
|
}
|
|
@@ -1669,6 +1963,7 @@ registerBehavior(fly);
|
|
|
1669
1963
|
registerBehavior(fall);
|
|
1670
1964
|
registerBehavior(carry);
|
|
1671
1965
|
registerBehavior(flight);
|
|
1966
|
+
registerBehavior(crumpleBehavior);
|
|
1672
1967
|
|
|
1673
1968
|
// src/physics/idle.ts
|
|
1674
1969
|
var idleNames = ["float", "tumble", "dangle", "taped", "breeze"];
|
|
@@ -1817,13 +2112,13 @@ var ClothSim = class {
|
|
|
1817
2112
|
this.stillFrames = 0;
|
|
1818
2113
|
}
|
|
1819
2114
|
/** Nearest particle to a local-space point — the grab interface. */
|
|
1820
|
-
grabNearest(x, y,
|
|
2115
|
+
grabNearest(x, y, z27) {
|
|
1821
2116
|
let best = -1;
|
|
1822
2117
|
let bestDist = Infinity;
|
|
1823
2118
|
for (let i = 0; i < this.count; i++) {
|
|
1824
2119
|
const dx = this.positions[i * 3] - x;
|
|
1825
2120
|
const dy = this.positions[i * 3 + 1] - y;
|
|
1826
|
-
const dz = this.positions[i * 3 + 2] -
|
|
2121
|
+
const dz = this.positions[i * 3 + 2] - z27;
|
|
1827
2122
|
const d = dx * dx + dy * dy + dz * dz;
|
|
1828
2123
|
if (d < bestDist) {
|
|
1829
2124
|
bestDist = d;
|
|
@@ -1834,15 +2129,15 @@ var ClothSim = class {
|
|
|
1834
2129
|
this.wake();
|
|
1835
2130
|
return best;
|
|
1836
2131
|
}
|
|
1837
|
-
moveGrab(x, y,
|
|
2132
|
+
moveGrab(x, y, z27) {
|
|
1838
2133
|
if (this.grabbedIndex < 0) return;
|
|
1839
2134
|
const i3 = this.grabbedIndex * 3;
|
|
1840
2135
|
this.positions[i3] = x;
|
|
1841
2136
|
this.positions[i3 + 1] = y;
|
|
1842
|
-
this.positions[i3 + 2] =
|
|
2137
|
+
this.positions[i3 + 2] = z27;
|
|
1843
2138
|
this.prev[i3] = x;
|
|
1844
2139
|
this.prev[i3 + 1] = y;
|
|
1845
|
-
this.prev[i3 + 2] =
|
|
2140
|
+
this.prev[i3 + 2] = z27;
|
|
1846
2141
|
this.wake();
|
|
1847
2142
|
}
|
|
1848
2143
|
release() {
|
|
@@ -1878,19 +2173,19 @@ var ClothSim = class {
|
|
|
1878
2173
|
}
|
|
1879
2174
|
const x = p[i3];
|
|
1880
2175
|
const y = p[i3 + 1];
|
|
1881
|
-
const
|
|
2176
|
+
const z27 = p[i3 + 2];
|
|
1882
2177
|
const gust2 = wind * (0.55 + 0.45 * Math.sin(this.time * 1.7 + x * 2.1 + y * 1.3)) * 0.9;
|
|
1883
2178
|
const ax = gust2 * 0.25;
|
|
1884
2179
|
const az = gust2;
|
|
1885
2180
|
const vx = (x - this.prev[i3]) * damping;
|
|
1886
2181
|
const vy = (y - this.prev[i3 + 1]) * damping;
|
|
1887
|
-
const vz = (
|
|
2182
|
+
const vz = (z27 - this.prev[i3 + 2]) * damping;
|
|
1888
2183
|
this.prev[i3] = x;
|
|
1889
2184
|
this.prev[i3 + 1] = y;
|
|
1890
|
-
this.prev[i3 + 2] =
|
|
2185
|
+
this.prev[i3 + 2] = z27;
|
|
1891
2186
|
p[i3] = x + vx + ax * dt2;
|
|
1892
2187
|
p[i3 + 1] = y + vy - gravity * 3.2 * dt2;
|
|
1893
|
-
p[i3 + 2] =
|
|
2188
|
+
p[i3 + 2] = z27 + vz + az * dt2;
|
|
1894
2189
|
maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
|
|
1895
2190
|
}
|
|
1896
2191
|
for (let iter = 0; iter < SOLVER_ITERATIONS; iter++) {
|
|
@@ -1936,18 +2231,171 @@ var ClothSim = class {
|
|
|
1936
2231
|
};
|
|
1937
2232
|
|
|
1938
2233
|
// src/surface/PaperMaterial.tsx
|
|
1939
|
-
import * as
|
|
2234
|
+
import * as THREE6 from "three";
|
|
1940
2235
|
import { useEffect as useEffect2, useMemo } from "react";
|
|
1941
2236
|
import CustomShaderMaterial from "three-custom-shader-material";
|
|
1942
2237
|
|
|
1943
2238
|
// src/surface/compose.ts
|
|
2239
|
+
import * as THREE5 from "three";
|
|
2240
|
+
|
|
2241
|
+
// src/surface/translucency.ts
|
|
1944
2242
|
import * as THREE4 from "three";
|
|
2243
|
+
|
|
2244
|
+
// src/scene/lighting.ts
|
|
2245
|
+
var lightingPresets = {
|
|
2246
|
+
studio: {
|
|
2247
|
+
id: "studio",
|
|
2248
|
+
label: "Studio",
|
|
2249
|
+
ambient: 0.65,
|
|
2250
|
+
key: { color: "#ffffff", intensity: 1.6, position: [2.5, 4, 3] },
|
|
2251
|
+
contactShadowOpacity: 0.3,
|
|
2252
|
+
contactShadowBlur: 2.4,
|
|
2253
|
+
exposure: 1,
|
|
2254
|
+
shadow: { mapSize: 1024, radius: 4 }
|
|
2255
|
+
},
|
|
2256
|
+
window: {
|
|
2257
|
+
id: "window",
|
|
2258
|
+
label: "Window",
|
|
2259
|
+
ambient: 0.5,
|
|
2260
|
+
key: { color: "#ffe3c0", intensity: 1.9, position: [3, 2.6, 2.6] },
|
|
2261
|
+
contactShadowOpacity: 0.35,
|
|
2262
|
+
contactShadowBlur: 2.6,
|
|
2263
|
+
exposure: 1,
|
|
2264
|
+
shadow: { mapSize: 1024, radius: 5 },
|
|
2265
|
+
gobo: { kind: "blinds", drift: 4e-3, angle: 0.62 }
|
|
2266
|
+
},
|
|
2267
|
+
leaves: {
|
|
2268
|
+
id: "leaves",
|
|
2269
|
+
label: "Leaves",
|
|
2270
|
+
ambient: 0.45,
|
|
2271
|
+
key: { color: "#fff2d8", intensity: 2, position: [2.2, 3.6, 2.4] },
|
|
2272
|
+
contactShadowOpacity: 0.4,
|
|
2273
|
+
contactShadowBlur: 2.8,
|
|
2274
|
+
exposure: 1,
|
|
2275
|
+
shadow: { mapSize: 1024, radius: 6 },
|
|
2276
|
+
gobo: { kind: "leaves", drift: 0.012, angle: 0.7 }
|
|
2277
|
+
},
|
|
2278
|
+
goldenhour: {
|
|
2279
|
+
id: "goldenhour",
|
|
2280
|
+
label: "Golden hour",
|
|
2281
|
+
ambient: 0.32,
|
|
2282
|
+
key: { color: "#ffb066", intensity: 2.4, position: [4, 0.9, 2.2] },
|
|
2283
|
+
contactShadowOpacity: 0.45,
|
|
2284
|
+
contactShadowBlur: 3.2,
|
|
2285
|
+
exposure: 1.15,
|
|
2286
|
+
shadow: { mapSize: 1024, radius: 7 }
|
|
2287
|
+
},
|
|
2288
|
+
noir: {
|
|
2289
|
+
id: "noir",
|
|
2290
|
+
label: "Noir",
|
|
2291
|
+
ambient: 0.07,
|
|
2292
|
+
key: { color: "#ffffff", intensity: 2.6, position: [2, 3, 1.6] },
|
|
2293
|
+
contactShadowOpacity: 0.7,
|
|
2294
|
+
contactShadowBlur: 1.1,
|
|
2295
|
+
exposure: 1.05,
|
|
2296
|
+
shadow: { mapSize: 2048, radius: 1 }
|
|
2297
|
+
},
|
|
2298
|
+
nave: {
|
|
2299
|
+
id: "nave",
|
|
2300
|
+
label: "Nave",
|
|
2301
|
+
// Dim, and the key sits BEHIND the walk rather than beside it: this mode
|
|
2302
|
+
// is carried by light coming through the paper, not off it. Ambient is
|
|
2303
|
+
// nearly nothing so the only bright thing in frame is the source itself.
|
|
2304
|
+
ambient: 0.09,
|
|
2305
|
+
key: { color: "#fff1dc", intensity: 3.4, position: [0, 7, -16] },
|
|
2306
|
+
contactShadowOpacity: 0.55,
|
|
2307
|
+
contactShadowBlur: 3.6,
|
|
2308
|
+
exposure: 1.2,
|
|
2309
|
+
shadow: { mapSize: 2048, radius: 6 },
|
|
2310
|
+
// Warm and light, not black: distance in a backlit hall washes TOWARD
|
|
2311
|
+
// the source, which is what separates haze from murk.
|
|
2312
|
+
fog: { color: "#c9baa3", near: 5, far: 38 }
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
function getLightingPreset(name) {
|
|
2316
|
+
return lightingPresets[name];
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// src/surface/translucency.ts
|
|
2320
|
+
var TRANSLUCENCY_VARYINGS = (
|
|
2321
|
+
/* glsl */
|
|
2322
|
+
`
|
|
2323
|
+
varying vec3 vPlWorldNormal;
|
|
2324
|
+
varying vec3 vPlViewDir;
|
|
2325
|
+
`
|
|
2326
|
+
);
|
|
2327
|
+
function translucencyVertexChunk(slots) {
|
|
2328
|
+
return (
|
|
2329
|
+
/* glsl */
|
|
2330
|
+
`
|
|
2331
|
+
{
|
|
2332
|
+
mat4 plModel = ${slots.model};
|
|
2333
|
+
vec4 plWorld = plModel * vec4(${slots.position}, 1.0);
|
|
2334
|
+
// Uniform scale only \u2014 layouts scale sheets evenly, so the plain 3\xD73 is
|
|
2335
|
+
// the correct normal matrix here and skips an inverse-transpose.
|
|
2336
|
+
vPlWorldNormal = normalize(mat3(plModel) * ${slots.normal});
|
|
2337
|
+
vPlViewDir = cameraPosition - plWorld.xyz;
|
|
2338
|
+
}
|
|
2339
|
+
`
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
var TRANSMISSION_GAIN = 0.5;
|
|
2343
|
+
var TRANSLUCENCY_FRAGMENT = (
|
|
2344
|
+
/* glsl */
|
|
2345
|
+
`
|
|
2346
|
+
uniform float uTranslucency;
|
|
2347
|
+
uniform vec3 uBackLightDir;
|
|
2348
|
+
uniform vec3 uBackLightColor;
|
|
2349
|
+
uniform float uAmbientTransmission;
|
|
2350
|
+
${TRANSLUCENCY_VARYINGS}
|
|
2351
|
+
|
|
2352
|
+
vec3 plTransmission(vec3 inkFilter) {
|
|
2353
|
+
if (uTranslucency <= 0.0) return vec3(0.0);
|
|
2354
|
+
vec3 n = normalize(vPlWorldNormal);
|
|
2355
|
+
// Sheets render double-sided; the back face needs the normal it actually shows.
|
|
2356
|
+
if (!gl_FrontFacing) n = -n;
|
|
2357
|
+
// The lamp is BEHIND this sheet when the face we are looking at points away
|
|
2358
|
+
// from it \u2014 that is the whole test.
|
|
2359
|
+
float behind = clamp(-dot(n, uBackLightDir), 0.0, 1.0);
|
|
2360
|
+
// A grazing view looks through more paper, and more paper passes less light.
|
|
2361
|
+
float thickness = abs(dot(n, normalize(vPlViewDir)));
|
|
2362
|
+
// Paper in a lit room glows whatever way it is turned \u2014 a sheet standing
|
|
2363
|
+
// edge-on to the only lamp is not black. Without this floor, a banner
|
|
2364
|
+
// whose face runs parallel to the key light gets neither diffuse nor
|
|
2365
|
+
// transmission and drops out of the picture entirely.
|
|
2366
|
+
vec3 arriving = uBackLightColor * behind + uAmbientTransmission;
|
|
2367
|
+
return arriving * uTranslucency * mix(0.25, 1.0, thickness) * inkFilter;
|
|
2368
|
+
}
|
|
2369
|
+
`
|
|
2370
|
+
);
|
|
2371
|
+
function translucencyValues(translucency, lighting) {
|
|
2372
|
+
const preset = getLightingPreset(lighting);
|
|
2373
|
+
const [x, y, z27] = preset.key.position;
|
|
2374
|
+
const direction = new THREE4.Vector3(x, y, z27);
|
|
2375
|
+
if (direction.lengthSq() < 1e-12) direction.set(0, 1, 0);
|
|
2376
|
+
direction.normalize();
|
|
2377
|
+
const color = new THREE4.Color(preset.key.color).multiplyScalar(preset.key.intensity * TRANSMISSION_GAIN);
|
|
2378
|
+
return { translucency, direction, color, ambient: preset.ambient * TRANSMISSION_GAIN };
|
|
2379
|
+
}
|
|
2380
|
+
function translucencyUniforms(translucency, lighting) {
|
|
2381
|
+
const values = translucencyValues(translucency, lighting);
|
|
2382
|
+
return {
|
|
2383
|
+
uTranslucency: { value: values.translucency },
|
|
2384
|
+
uBackLightDir: { value: values.direction },
|
|
2385
|
+
uBackLightColor: { value: values.color },
|
|
2386
|
+
uAmbientTransmission: { value: values.ambient }
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// src/surface/compose.ts
|
|
1945
2391
|
var VERTEX = (
|
|
1946
2392
|
/* glsl */
|
|
1947
2393
|
`
|
|
1948
2394
|
varying vec2 vPaperUv;
|
|
2395
|
+
${TRANSLUCENCY_VARYINGS}
|
|
1949
2396
|
void main() {
|
|
1950
2397
|
vPaperUv = uv;
|
|
2398
|
+
${translucencyVertexChunk({ model: "modelMatrix", position: "position", normal: "normal" })}
|
|
1951
2399
|
}
|
|
1952
2400
|
`
|
|
1953
2401
|
);
|
|
@@ -1984,7 +2432,7 @@ float plFbm(vec2 p) {
|
|
|
1984
2432
|
}
|
|
1985
2433
|
`
|
|
1986
2434
|
);
|
|
1987
|
-
var edgeFlags = (edges) => new
|
|
2435
|
+
var edgeFlags = (edges) => new THREE5.Vector4(
|
|
1988
2436
|
edges.includes("top") ? 1 : 0,
|
|
1989
2437
|
edges.includes("right") ? 1 : 0,
|
|
1990
2438
|
edges.includes("bottom") ? 1 : 0,
|
|
@@ -2119,7 +2567,7 @@ void plAging(inout vec4 color) {
|
|
|
2119
2567
|
}
|
|
2120
2568
|
`
|
|
2121
2569
|
);
|
|
2122
|
-
function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false, hasBackMap: false }, sheet2 = { width: 1, height: 1.4 }) {
|
|
2570
|
+
function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false, hasBackMap: false }, sheet2 = { width: 1, height: 1.4 }, lighting = "studio") {
|
|
2123
2571
|
const grain = surface.grain ?? stock.defaultSurface.grain;
|
|
2124
2572
|
const aging = surface.aging ?? stock.defaultSurface.aging;
|
|
2125
2573
|
const deckle = surface.deckle;
|
|
@@ -2135,9 +2583,12 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
2135
2583
|
uBackDarken: {
|
|
2136
2584
|
value: stock.adhesive ? 1 : 1 - Math.min(0.45, 0.12 + thickness * 0.9) * stock.opacity
|
|
2137
2585
|
},
|
|
2138
|
-
uStockColor: { value: new
|
|
2586
|
+
uStockColor: { value: new THREE5.Color(stock.color) },
|
|
2139
2587
|
uOpacity: { value: stock.opacity },
|
|
2140
|
-
uShowThrough: { value: showThrough }
|
|
2588
|
+
uShowThrough: { value: showThrough },
|
|
2589
|
+
// Always compiled in: the shader early-outs at zero translucency, which
|
|
2590
|
+
// is cheaper than carrying a second program structure for it.
|
|
2591
|
+
...translucencyUniforms(surface.translucency ?? stock.translucency, lighting)
|
|
2141
2592
|
};
|
|
2142
2593
|
if (maps.hasFrontMap) uniforms.uFrontMap = { value: null };
|
|
2143
2594
|
if (maps.hasBackMap) uniforms.uBackMap = { value: null };
|
|
@@ -2159,13 +2610,13 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
2159
2610
|
calls.push("plPerforation(csm_DiffuseColor);");
|
|
2160
2611
|
uniforms.uPerfEdges = { value: edgeFlags(edges) };
|
|
2161
2612
|
uniforms.uPerfTorn = {
|
|
2162
|
-
value: new
|
|
2613
|
+
value: new THREE5.Vector4(
|
|
2163
2614
|
...paperEdges.map((e) => edges.includes(e) && perforation.state[e] === "torn" ? 1 : 0)
|
|
2164
2615
|
)
|
|
2165
2616
|
};
|
|
2166
2617
|
uniforms.uPerfRadius = { value: perforation.holeRadius };
|
|
2167
2618
|
uniforms.uPerfSpacing = { value: perforation.spacing };
|
|
2168
|
-
uniforms.uSheetSize = { value: new
|
|
2619
|
+
uniforms.uSheetSize = { value: new THREE5.Vector2(sheet2.width, sheet2.height) };
|
|
2169
2620
|
}
|
|
2170
2621
|
if (creases) {
|
|
2171
2622
|
chunks.push(CREASE_CHUNK);
|
|
@@ -2191,6 +2642,7 @@ uniform float uOpacity;
|
|
|
2191
2642
|
uniform float uShowThrough;
|
|
2192
2643
|
${maps.hasFrontMap ? "uniform sampler2D uFrontMap;" : ""}
|
|
2193
2644
|
${maps.hasBackMap && !stock.adhesive ? "uniform sampler2D uBackMap;" : ""}
|
|
2645
|
+
${TRANSLUCENCY_FRAGMENT}
|
|
2194
2646
|
${chunks.join("\n")}
|
|
2195
2647
|
void main() {
|
|
2196
2648
|
vec3 front = ${frontExpr};
|
|
@@ -2203,6 +2655,8 @@ void main() {
|
|
|
2203
2655
|
${calls.join("\n ")}
|
|
2204
2656
|
if (!gl_FrontFacing) csm_DiffuseColor.rgb *= uBackDarken;
|
|
2205
2657
|
${stock.adhesive ? "// Adhesive underside: higher specular than the printed face.\n if (!gl_FrontFacing) csm_Roughness = 0.18;" : ""}
|
|
2658
|
+
// What the key light pushes through the sheet, filtered by the ink on it.
|
|
2659
|
+
csm_Emissive = plTransmission(front);
|
|
2206
2660
|
}
|
|
2207
2661
|
`
|
|
2208
2662
|
);
|
|
@@ -2235,7 +2689,8 @@ function PaperMaterial({
|
|
|
2235
2689
|
backTexture,
|
|
2236
2690
|
surface,
|
|
2237
2691
|
thickness,
|
|
2238
|
-
sheet: sheet2
|
|
2692
|
+
sheet: sheet2,
|
|
2693
|
+
lighting = "studio"
|
|
2239
2694
|
}) {
|
|
2240
2695
|
const composed = composeSurface(
|
|
2241
2696
|
surface,
|
|
@@ -2245,13 +2700,14 @@ function PaperMaterial({
|
|
|
2245
2700
|
hasFrontMap: Boolean(texture),
|
|
2246
2701
|
hasBackMap: Boolean(backTexture)
|
|
2247
2702
|
},
|
|
2248
|
-
sheet2
|
|
2703
|
+
sheet2,
|
|
2704
|
+
lighting
|
|
2249
2705
|
);
|
|
2250
2706
|
const bound = useMemo(() => composed.uniforms, [composed.structureKey]);
|
|
2251
2707
|
useEffect2(() => {
|
|
2252
2708
|
for (const [key, uniform] of Object.entries(composed.uniforms)) {
|
|
2253
2709
|
if (!bound[key] || key === "uFrontMap" || key === "uBackMap") continue;
|
|
2254
|
-
if (bound[key].value instanceof
|
|
2710
|
+
if (bound[key].value instanceof THREE6.Color && uniform.value instanceof THREE6.Color) {
|
|
2255
2711
|
;
|
|
2256
2712
|
bound[key].value.copy(uniform.value);
|
|
2257
2713
|
} else {
|
|
@@ -2266,7 +2722,7 @@ function PaperMaterial({
|
|
|
2266
2722
|
return /* @__PURE__ */ jsx(
|
|
2267
2723
|
CustomShaderMaterial,
|
|
2268
2724
|
{
|
|
2269
|
-
baseMaterial:
|
|
2725
|
+
baseMaterial: THREE6.MeshStandardMaterial,
|
|
2270
2726
|
vertexShader: composed.vertexShader,
|
|
2271
2727
|
fragmentShader: composed.fragmentShader,
|
|
2272
2728
|
uniforms: bound,
|
|
@@ -2276,7 +2732,7 @@ function PaperMaterial({
|
|
|
2276
2732
|
transparent: stock.opacity < 1,
|
|
2277
2733
|
opacity: stock.opacity,
|
|
2278
2734
|
alphaTest: composed.alphaTest,
|
|
2279
|
-
side:
|
|
2735
|
+
side: THREE6.DoubleSide
|
|
2280
2736
|
},
|
|
2281
2737
|
composed.structureKey
|
|
2282
2738
|
);
|
|
@@ -2700,6 +3156,8 @@ function resolveConfigKey(props) {
|
|
|
2700
3156
|
props.content ?? null,
|
|
2701
3157
|
props.behavior ?? null,
|
|
2702
3158
|
props.deformers ?? null,
|
|
3159
|
+
props.surface ?? null,
|
|
3160
|
+
props.scene ?? null,
|
|
2703
3161
|
props.physics ?? null,
|
|
2704
3162
|
props.onTwos ?? null
|
|
2705
3163
|
]);
|
|
@@ -2712,17 +3170,19 @@ function resolveConfig(props) {
|
|
|
2712
3170
|
if (props.content) overrides.content = props.content;
|
|
2713
3171
|
if (props.behavior) overrides.behavior = props.behavior;
|
|
2714
3172
|
if (props.deformers) overrides.deformers = props.deformers;
|
|
3173
|
+
if (props.surface) overrides.surface = { ...base.surface, ...props.surface };
|
|
3174
|
+
if (props.scene) overrides.scene = { ...base.scene, ...props.scene };
|
|
2715
3175
|
if (props.physics) overrides.physics = props.physics;
|
|
2716
3176
|
if (props.onTwos !== void 0) overrides.onTwos = props.onTwos;
|
|
2717
3177
|
return paperConfigSchema.parse(mergeConfig(base, overrides));
|
|
2718
3178
|
}
|
|
2719
3179
|
var CLOTH_MAX_SEGMENTS = 28;
|
|
2720
|
-
var dragPlane = new
|
|
2721
|
-
var dragPoint = new
|
|
2722
|
-
var planeNormal = new
|
|
2723
|
-
var anchorScratch = new
|
|
2724
|
-
var worldScratch = new
|
|
2725
|
-
var quatScratch = new
|
|
3180
|
+
var dragPlane = new THREE7.Plane();
|
|
3181
|
+
var dragPoint = new THREE7.Vector3();
|
|
3182
|
+
var planeNormal = new THREE7.Vector3();
|
|
3183
|
+
var anchorScratch = new THREE7.Vector3();
|
|
3184
|
+
var worldScratch = new THREE7.Vector3();
|
|
3185
|
+
var quatScratch = new THREE7.Quaternion();
|
|
2726
3186
|
var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
|
|
2727
3187
|
const resolved = useMemo3(() => resolveConfig(props), [resolveConfigKey(props)]);
|
|
2728
3188
|
const reduced = usePrefersReducedMotion(props.reducedMotion);
|
|
@@ -2767,7 +3227,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
|
|
|
2767
3227
|
if (!isCloth) return createSheetGeometry(config.sheet, minSegments);
|
|
2768
3228
|
const [sx, sy] = resolveSegments(config.sheet, 2);
|
|
2769
3229
|
const capped = Math.min(Math.max(sx, sy), CLOTH_MAX_SEGMENTS);
|
|
2770
|
-
return new
|
|
3230
|
+
return new THREE7.PlaneGeometry(config.sheet.width, config.sheet.height, capped, capped);
|
|
2771
3231
|
}, [sheetKey, minSegments, isCloth]);
|
|
2772
3232
|
useEffect5(() => () => geometry.dispose(), [geometry]);
|
|
2773
3233
|
const basePositions = useMemo3(
|
|
@@ -2958,7 +3418,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
|
|
|
2958
3418
|
const p = overridesRef.current[behavior.progressParam];
|
|
2959
3419
|
if (typeof p === "number") props.onProgress?.(p);
|
|
2960
3420
|
};
|
|
2961
|
-
const grabAnchor = useRef2(new
|
|
3421
|
+
const grabAnchor = useRef2(new THREE7.Vector3());
|
|
2962
3422
|
const clothDown = (e) => {
|
|
2963
3423
|
if (!isCloth || !sim || !props.interactive || !groupRef.current) return;
|
|
2964
3424
|
e.stopPropagation();
|
|
@@ -3014,7 +3474,8 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
|
|
|
3014
3474
|
backTexture,
|
|
3015
3475
|
surface: config.surface,
|
|
3016
3476
|
thickness: config.sheet.thickness,
|
|
3017
|
-
sheet: config.sheet
|
|
3477
|
+
sheet: config.sheet,
|
|
3478
|
+
lighting: config.scene.lighting
|
|
3018
3479
|
}
|
|
3019
3480
|
)
|
|
3020
3481
|
}
|
|
@@ -3055,7 +3516,7 @@ function buildStack(config, overrides, behavior, t = 0) {
|
|
|
3055
3516
|
const idleStack = idle?.stack?.() ?? [];
|
|
3056
3517
|
let shapeStack = [];
|
|
3057
3518
|
if (config.deformers) {
|
|
3058
|
-
shapeStack = config.deformers;
|
|
3519
|
+
shapeStack = resolveDeformerStack(config.deformers);
|
|
3059
3520
|
} else if (config.behavior) {
|
|
3060
3521
|
const b = behavior ?? getBehavior(config.behavior.type);
|
|
3061
3522
|
let options = { ...config.behavior, ...overrides };
|
|
@@ -3067,71 +3528,10 @@ function buildStack(config, overrides, behavior, t = 0) {
|
|
|
3067
3528
|
}
|
|
3068
3529
|
|
|
3069
3530
|
// src/scene/PaperLighting.tsx
|
|
3070
|
-
import * as
|
|
3531
|
+
import * as THREE8 from "three";
|
|
3071
3532
|
import { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef3 } from "react";
|
|
3072
3533
|
import { useFrame as useFrame2, useThree as useThree2 } from "@react-three/fiber";
|
|
3073
3534
|
import { ContactShadows } from "@react-three/drei";
|
|
3074
|
-
|
|
3075
|
-
// src/scene/lighting.ts
|
|
3076
|
-
var lightingPresets = {
|
|
3077
|
-
studio: {
|
|
3078
|
-
id: "studio",
|
|
3079
|
-
label: "Studio",
|
|
3080
|
-
ambient: 0.65,
|
|
3081
|
-
key: { color: "#ffffff", intensity: 1.6, position: [2.5, 4, 3] },
|
|
3082
|
-
contactShadowOpacity: 0.3,
|
|
3083
|
-
contactShadowBlur: 2.4,
|
|
3084
|
-
exposure: 1,
|
|
3085
|
-
shadow: { mapSize: 1024, radius: 4 }
|
|
3086
|
-
},
|
|
3087
|
-
window: {
|
|
3088
|
-
id: "window",
|
|
3089
|
-
label: "Window",
|
|
3090
|
-
ambient: 0.5,
|
|
3091
|
-
key: { color: "#ffe3c0", intensity: 1.9, position: [3, 2.6, 2.6] },
|
|
3092
|
-
contactShadowOpacity: 0.35,
|
|
3093
|
-
contactShadowBlur: 2.6,
|
|
3094
|
-
exposure: 1,
|
|
3095
|
-
shadow: { mapSize: 1024, radius: 5 },
|
|
3096
|
-
gobo: { kind: "blinds", drift: 4e-3, angle: 0.62 }
|
|
3097
|
-
},
|
|
3098
|
-
leaves: {
|
|
3099
|
-
id: "leaves",
|
|
3100
|
-
label: "Leaves",
|
|
3101
|
-
ambient: 0.45,
|
|
3102
|
-
key: { color: "#fff2d8", intensity: 2, position: [2.2, 3.6, 2.4] },
|
|
3103
|
-
contactShadowOpacity: 0.4,
|
|
3104
|
-
contactShadowBlur: 2.8,
|
|
3105
|
-
exposure: 1,
|
|
3106
|
-
shadow: { mapSize: 1024, radius: 6 },
|
|
3107
|
-
gobo: { kind: "leaves", drift: 0.012, angle: 0.7 }
|
|
3108
|
-
},
|
|
3109
|
-
goldenhour: {
|
|
3110
|
-
id: "goldenhour",
|
|
3111
|
-
label: "Golden hour",
|
|
3112
|
-
ambient: 0.32,
|
|
3113
|
-
key: { color: "#ffb066", intensity: 2.4, position: [4, 0.9, 2.2] },
|
|
3114
|
-
contactShadowOpacity: 0.45,
|
|
3115
|
-
contactShadowBlur: 3.2,
|
|
3116
|
-
exposure: 1.15,
|
|
3117
|
-
shadow: { mapSize: 1024, radius: 7 }
|
|
3118
|
-
},
|
|
3119
|
-
noir: {
|
|
3120
|
-
id: "noir",
|
|
3121
|
-
label: "Noir",
|
|
3122
|
-
ambient: 0.07,
|
|
3123
|
-
key: { color: "#ffffff", intensity: 2.6, position: [2, 3, 1.6] },
|
|
3124
|
-
contactShadowOpacity: 0.7,
|
|
3125
|
-
contactShadowBlur: 1.1,
|
|
3126
|
-
exposure: 1.05,
|
|
3127
|
-
shadow: { mapSize: 2048, radius: 1 }
|
|
3128
|
-
}
|
|
3129
|
-
};
|
|
3130
|
-
function getLightingPreset(name) {
|
|
3131
|
-
return lightingPresets[name];
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
// src/scene/PaperLighting.tsx
|
|
3135
3535
|
import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
3136
3536
|
function mulberry32(seed) {
|
|
3137
3537
|
let a = seed;
|
|
@@ -3188,19 +3588,24 @@ function makeGoboTexture(kind) {
|
|
|
3188
3588
|
}
|
|
3189
3589
|
}
|
|
3190
3590
|
}
|
|
3191
|
-
const texture = new
|
|
3192
|
-
texture.wrapS = texture.wrapT =
|
|
3591
|
+
const texture = new THREE8.CanvasTexture(canvas);
|
|
3592
|
+
texture.wrapS = texture.wrapT = THREE8.RepeatWrapping;
|
|
3193
3593
|
return texture;
|
|
3194
3594
|
}
|
|
3195
3595
|
function PaperLighting({
|
|
3196
3596
|
preset = "studio",
|
|
3197
3597
|
floor = -1.2,
|
|
3198
3598
|
scale = 10,
|
|
3199
|
-
reducedMotion
|
|
3599
|
+
reducedMotion,
|
|
3600
|
+
shadowMapSize,
|
|
3601
|
+
contactShadow = true
|
|
3200
3602
|
}) {
|
|
3201
3603
|
const p = getLightingPreset(preset);
|
|
3604
|
+
const mapSize = shadowMapSize ?? p.shadow.mapSize;
|
|
3605
|
+
const castShadow = mapSize > 0;
|
|
3202
3606
|
const reduced = usePrefersReducedMotion(reducedMotion);
|
|
3203
3607
|
const gl = useThree2((s) => s.gl);
|
|
3608
|
+
const scene = useThree2((s) => s.scene);
|
|
3204
3609
|
useEffect6(() => {
|
|
3205
3610
|
const previous = gl.toneMappingExposure;
|
|
3206
3611
|
gl.toneMappingExposure = p.exposure;
|
|
@@ -3208,11 +3613,15 @@ function PaperLighting({
|
|
|
3208
3613
|
gl.toneMappingExposure = previous;
|
|
3209
3614
|
};
|
|
3210
3615
|
}, [gl, p.exposure]);
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3616
|
+
useEffect6(() => {
|
|
3617
|
+
if (!p.fog) return;
|
|
3618
|
+
const previous = scene.fog;
|
|
3619
|
+
scene.fog = new THREE8.Fog(p.fog.color, p.fog.near, p.fog.far);
|
|
3620
|
+
return () => {
|
|
3621
|
+
scene.fog = previous;
|
|
3622
|
+
};
|
|
3623
|
+
}, [scene, p.fog]);
|
|
3624
|
+
const goboMap = useMemo4(() => p.gobo ? makeGoboTexture(p.gobo.kind) : null, [p.gobo?.kind]);
|
|
3216
3625
|
useEffect6(() => () => goboMap?.dispose(), [goboMap]);
|
|
3217
3626
|
const driftRef = useRef3(0);
|
|
3218
3627
|
useFrame2((_, delta) => {
|
|
@@ -3231,9 +3640,9 @@ function PaperLighting({
|
|
|
3231
3640
|
angle: p.gobo.angle,
|
|
3232
3641
|
penumbra: 0.5,
|
|
3233
3642
|
decay: 0,
|
|
3234
|
-
castShadow
|
|
3643
|
+
castShadow,
|
|
3235
3644
|
map: goboMap,
|
|
3236
|
-
"shadow-mapSize": [
|
|
3645
|
+
"shadow-mapSize": [mapSize || 1, mapSize || 1],
|
|
3237
3646
|
"shadow-radius": p.shadow.radius,
|
|
3238
3647
|
"shadow-normalBias": 0.05
|
|
3239
3648
|
}
|
|
@@ -3243,13 +3652,13 @@ function PaperLighting({
|
|
|
3243
3652
|
position: p.key.position,
|
|
3244
3653
|
color: p.key.color,
|
|
3245
3654
|
intensity: p.key.intensity,
|
|
3246
|
-
castShadow
|
|
3247
|
-
"shadow-mapSize": [
|
|
3655
|
+
castShadow,
|
|
3656
|
+
"shadow-mapSize": [mapSize || 1, mapSize || 1],
|
|
3248
3657
|
"shadow-radius": p.shadow.radius,
|
|
3249
3658
|
"shadow-normalBias": 0.05
|
|
3250
3659
|
}
|
|
3251
3660
|
),
|
|
3252
|
-
/* @__PURE__ */ jsx4(
|
|
3661
|
+
contactShadow && /* @__PURE__ */ jsx4(
|
|
3253
3662
|
ContactShadows,
|
|
3254
3663
|
{
|
|
3255
3664
|
position: [0, floor, 0],
|
|
@@ -3286,12 +3695,13 @@ var Paper = forwardRef2(function Paper2({ children, className, style, ...meshPro
|
|
|
3286
3695
|
});
|
|
3287
3696
|
|
|
3288
3697
|
// src/PaperField.tsx
|
|
3698
|
+
import * as THREE14 from "three";
|
|
3289
3699
|
import { gsap as gsap5 } from "gsap";
|
|
3290
3700
|
import { Canvas as Canvas2, useFrame as useFrame5, useThree as useThree4 } from "@react-three/fiber";
|
|
3291
3701
|
import { forwardRef as forwardRef3, useEffect as useEffect12, useMemo as useMemo10, useRef as useRef6 } from "react";
|
|
3292
3702
|
|
|
3293
3703
|
// src/field/dropZones.tsx
|
|
3294
|
-
import * as
|
|
3704
|
+
import * as THREE9 from "three";
|
|
3295
3705
|
import { createContext, useContext, useEffect as useEffect7, useMemo as useMemo6, useSyncExternalStore } from "react";
|
|
3296
3706
|
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3297
3707
|
var DropZoneRegistry = class {
|
|
@@ -3362,8 +3772,8 @@ function DropZoneVisual({ registry: registry4, config }) {
|
|
|
3362
3772
|
const style = config.highlight ?? "glow";
|
|
3363
3773
|
const [w, h] = config.bounds.size;
|
|
3364
3774
|
const edges = useMemo6(() => {
|
|
3365
|
-
const plane = new
|
|
3366
|
-
const geo = new
|
|
3775
|
+
const plane = new THREE9.PlaneGeometry(w, h);
|
|
3776
|
+
const geo = new THREE9.EdgesGeometry(plane);
|
|
3367
3777
|
plane.dispose();
|
|
3368
3778
|
return geo;
|
|
3369
3779
|
}, [w, h]);
|
|
@@ -3386,18 +3796,18 @@ function DropZoneVisual({ registry: registry4, config }) {
|
|
|
3386
3796
|
}
|
|
3387
3797
|
|
|
3388
3798
|
// src/field/sheetGrid.ts
|
|
3389
|
-
import { z as
|
|
3390
|
-
var sheetLayoutSchema =
|
|
3391
|
-
rows:
|
|
3392
|
-
columns:
|
|
3799
|
+
import { z as z20 } from "zod";
|
|
3800
|
+
var sheetLayoutSchema = z20.object({
|
|
3801
|
+
rows: z20.number().int().min(1).max(12).default(2),
|
|
3802
|
+
columns: z20.number().int().min(1).max(12).default(5),
|
|
3393
3803
|
/** World-units gap between slots. Stamps are printed in register — no jitter. */
|
|
3394
|
-
gutter:
|
|
3804
|
+
gutter: z20.number().min(0).max(1).default(0.08),
|
|
3395
3805
|
/** Slot footprint in world units (the paper preset should match). */
|
|
3396
|
-
cellWidth:
|
|
3397
|
-
cellHeight:
|
|
3806
|
+
cellWidth: z20.number().min(0.1).max(4).default(0.72),
|
|
3807
|
+
cellHeight: z20.number().min(0.1).max(4).default(0.86),
|
|
3398
3808
|
/** Render the shared backing sheet behind the grid. */
|
|
3399
|
-
backing:
|
|
3400
|
-
backingMargin:
|
|
3809
|
+
backing: z20.boolean().default(true),
|
|
3810
|
+
backingMargin: z20.number().min(0).max(1).default(0.12)
|
|
3401
3811
|
});
|
|
3402
3812
|
var SHEET_LIFT = 0.012;
|
|
3403
3813
|
function withSheetCellFromPaper(parsed, rawOptions, paperDims) {
|
|
@@ -3489,18 +3899,18 @@ function resolveFieldSlotConfig(slot, fallback, index, layoutId, layoutOptions)
|
|
|
3489
3899
|
}
|
|
3490
3900
|
|
|
3491
3901
|
// src/field/fieldGroup.tsx
|
|
3492
|
-
import * as
|
|
3902
|
+
import * as THREE11 from "three";
|
|
3493
3903
|
import { gsap as gsap3 } from "gsap";
|
|
3494
3904
|
import { useFrame as useFrame3 } from "@react-three/fiber";
|
|
3495
3905
|
import { useEffect as useEffect9, useMemo as useMemo7, useRef as useRef4 } from "react";
|
|
3496
3906
|
import CustomShaderMaterial2 from "three-custom-shader-material";
|
|
3497
3907
|
|
|
3498
3908
|
// src/content/atlas.ts
|
|
3499
|
-
import * as
|
|
3909
|
+
import * as THREE10 from "three";
|
|
3500
3910
|
import { useEffect as useEffect8, useState as useState4 } from "react";
|
|
3501
3911
|
var MAX_ATLAS = 4096;
|
|
3502
|
-
function atlasGrid(count) {
|
|
3503
|
-
const cols = Math.max(1, Math.ceil(Math.sqrt(count)));
|
|
3912
|
+
function atlasGrid(count, aspect = 1) {
|
|
3913
|
+
const cols = Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * Math.max(aspect, 0.01)))));
|
|
3504
3914
|
return { cols, rows: Math.max(1, Math.ceil(count / cols)) };
|
|
3505
3915
|
}
|
|
3506
3916
|
function useContentAtlas(contents, sheet2, stock) {
|
|
@@ -3508,18 +3918,22 @@ function useContentAtlas(contents, sheet2, stock) {
|
|
|
3508
3918
|
const key = JSON.stringify({ contents, w: sheet2.width, h: sheet2.height, stock: stock.id });
|
|
3509
3919
|
useEffect8(() => {
|
|
3510
3920
|
let disposed = false;
|
|
3511
|
-
const { cols, rows } = atlasGrid(contents.length);
|
|
3512
3921
|
const aspect = sheet2.height / sheet2.width;
|
|
3513
|
-
const
|
|
3514
|
-
|
|
3922
|
+
const { cols, rows } = atlasGrid(contents.length, aspect);
|
|
3923
|
+
let tileW = Math.min(1024, Math.floor(MAX_ATLAS / cols));
|
|
3924
|
+
let tileH = Math.round(tileW * aspect);
|
|
3925
|
+
if (tileH * rows > MAX_ATLAS) {
|
|
3926
|
+
tileH = Math.floor(MAX_ATLAS / rows);
|
|
3927
|
+
tileW = Math.max(1, Math.round(tileH / aspect));
|
|
3928
|
+
}
|
|
3515
3929
|
const canvas = document.createElement("canvas");
|
|
3516
3930
|
canvas.width = tileW * cols;
|
|
3517
3931
|
canvas.height = tileH * rows;
|
|
3518
3932
|
const ctx = canvas.getContext("2d");
|
|
3519
3933
|
ctx.fillStyle = stock.color;
|
|
3520
3934
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
3521
|
-
const texture = new
|
|
3522
|
-
texture.colorSpace =
|
|
3935
|
+
const texture = new THREE10.CanvasTexture(canvas);
|
|
3936
|
+
texture.colorSpace = THREE10.SRGBColorSpace;
|
|
3523
3937
|
texture.anisotropy = 4;
|
|
3524
3938
|
setAtlas({ texture, cols, rows });
|
|
3525
3939
|
const drawTile = (index, tile) => {
|
|
@@ -3567,7 +3981,7 @@ function stackUniformValues(stack, sheet2) {
|
|
|
3567
3981
|
return uniforms;
|
|
3568
3982
|
}
|
|
3569
3983
|
function buildDisplacementGLSL(stack, sheet2) {
|
|
3570
|
-
const decls = ["uniform vec2 uSheet;"];
|
|
3984
|
+
const decls = ["uniform vec2 uSheet;", "float plBias = 1.0;"];
|
|
3571
3985
|
const functions = [];
|
|
3572
3986
|
const calls = [];
|
|
3573
3987
|
const uniforms = { uSheet: [sheet2.width, sheet2.height] };
|
|
@@ -3586,15 +4000,24 @@ function buildDisplacementGLSL(stack, sheet2) {
|
|
|
3586
4000
|
decls.push(`uniform ${glslType(value)} ${ns}${key};`);
|
|
3587
4001
|
uniforms[ns + key] = value;
|
|
3588
4002
|
}
|
|
4003
|
+
const strength = deformer.glsl.strength;
|
|
3589
4004
|
functions.push(
|
|
3590
|
-
deformer.glsl.chunk.replaceAll("FN", fn).replace(
|
|
4005
|
+
deformer.glsl.chunk.replaceAll("FN", fn).replace(
|
|
4006
|
+
/U_(\w+)/g,
|
|
4007
|
+
(_, name) => (
|
|
4008
|
+
// The strength uniform reads through the per-instance bias, so one
|
|
4009
|
+
// instanced draw call can bend every sheet by a different amount.
|
|
4010
|
+
name === strength ? `(${ns}${name} * plBias)` : ns + name
|
|
4011
|
+
)
|
|
4012
|
+
)
|
|
3591
4013
|
);
|
|
3592
4014
|
calls.push(`${fn}(q, uv, t);`);
|
|
3593
4015
|
});
|
|
3594
4016
|
const displaceSrc = (
|
|
3595
4017
|
/* glsl */
|
|
3596
4018
|
`
|
|
3597
|
-
vec3 plDisplace(vec3 p, vec2 uv, float t) {
|
|
4019
|
+
vec3 plDisplace(vec3 p, vec2 uv, float t, float bias) {
|
|
4020
|
+
plBias = bias;
|
|
3598
4021
|
vec3 q = p;
|
|
3599
4022
|
${calls.join("\n ")}
|
|
3600
4023
|
return q;
|
|
@@ -3611,19 +4034,22 @@ function buildFieldVertexShader(composed) {
|
|
|
3611
4034
|
uniform float uPlTime;
|
|
3612
4035
|
attribute float aPhase;
|
|
3613
4036
|
attribute float aAtlas;
|
|
4037
|
+
attribute float aBias;
|
|
3614
4038
|
varying vec2 vPaperUv;
|
|
3615
4039
|
varying float vAtlas;
|
|
4040
|
+
${TRANSLUCENCY_VARYINGS}
|
|
3616
4041
|
${composed.functionsSrc}
|
|
3617
4042
|
${composed.displaceSrc}
|
|
3618
4043
|
void main() {
|
|
3619
4044
|
float t = uPlTime + aPhase;
|
|
3620
|
-
vec3 p = plDisplace(position, uv, t);
|
|
4045
|
+
vec3 p = plDisplace(position, uv, t, aBias);
|
|
3621
4046
|
vec2 step = uSheet * 0.01;
|
|
3622
|
-
vec3 px = plDisplace(position + vec3(step.x, 0.0, 0.0), uv + vec2(0.01, 0.0), t);
|
|
3623
|
-
vec3 py = plDisplace(position + vec3(0.0, step.y, 0.0), uv + vec2(0.0, 0.01), t);
|
|
4047
|
+
vec3 px = plDisplace(position + vec3(step.x, 0.0, 0.0), uv + vec2(0.01, 0.0), t, aBias);
|
|
4048
|
+
vec3 py = plDisplace(position + vec3(0.0, step.y, 0.0), uv + vec2(0.0, 0.01), t, aBias);
|
|
3624
4049
|
vec3 n = cross(px - p, py - p);
|
|
3625
4050
|
csm_Normal = length(n) > 1e-12 ? normalize(n) : vec3(0.0, 0.0, 1.0);
|
|
3626
4051
|
csm_Position = p;
|
|
4052
|
+
${translucencyVertexChunk({ model: "modelMatrix * instanceMatrix", position: "p", normal: "csm_Normal" })}
|
|
3627
4053
|
vPaperUv = uv;
|
|
3628
4054
|
vAtlas = aAtlas;
|
|
3629
4055
|
}
|
|
@@ -3641,6 +4067,7 @@ uniform vec3 uStockColor;
|
|
|
3641
4067
|
uniform float uShowThrough;
|
|
3642
4068
|
varying vec2 vPaperUv;
|
|
3643
4069
|
varying float vAtlas;
|
|
4070
|
+
${TRANSLUCENCY_FRAGMENT}
|
|
3644
4071
|
void main() {
|
|
3645
4072
|
float col = mod(vAtlas, uAtlasGrid.x);
|
|
3646
4073
|
float row = floor(vAtlas / uAtlasGrid.x);
|
|
@@ -3652,6 +4079,10 @@ void main() {
|
|
|
3652
4079
|
csm_DiffuseColor = vec4(uStockColor * mix(vec3(1.0), front.rgb, uShowThrough), 1.0);
|
|
3653
4080
|
csm_DiffuseColor.rgb *= uBackDarken;
|
|
3654
4081
|
}
|
|
4082
|
+
// Light coming through the sheet, filtered by what is printed on it. Same
|
|
4083
|
+
// ink either side \u2014 the light passes through the same fibres regardless of
|
|
4084
|
+
// which face happens to be turned toward the camera.
|
|
4085
|
+
csm_Emissive = plTransmission(front.rgb);
|
|
3655
4086
|
}
|
|
3656
4087
|
`
|
|
3657
4088
|
);
|
|
@@ -3661,16 +4092,149 @@ function cap(s) {
|
|
|
3661
4092
|
}
|
|
3662
4093
|
|
|
3663
4094
|
// src/field/layouts/index.ts
|
|
3664
|
-
import { z as
|
|
3665
|
-
|
|
3666
|
-
|
|
4095
|
+
import { z as z22 } from "zod";
|
|
4096
|
+
|
|
4097
|
+
// src/stage/path.ts
|
|
4098
|
+
import { z as z21 } from "zod";
|
|
4099
|
+
var walkPathSchema = z21.object({
|
|
4100
|
+
/**
|
|
4101
|
+
* Control points on the ground plane, [x, z]. The default walks away from
|
|
4102
|
+
* the camera down -Z — the shot every reference image is composed on.
|
|
4103
|
+
*/
|
|
4104
|
+
points: z21.array(z21.tuple([z21.number(), z21.number()])).min(2).default([
|
|
4105
|
+
[0, 9],
|
|
4106
|
+
[0, -9]
|
|
4107
|
+
]),
|
|
4108
|
+
/** Join the last point back to the first: an endless walk, and the only form `phase` can slide. */
|
|
4109
|
+
closed: z21.boolean().default(false)
|
|
4110
|
+
});
|
|
4111
|
+
var SAMPLES_PER_SEGMENT = 24;
|
|
4112
|
+
var EPSILON = 1e-6;
|
|
4113
|
+
var ALPHA = 0.5;
|
|
4114
|
+
function knot(a, b, t) {
|
|
4115
|
+
return t + Math.max(Math.hypot(b[0] - a[0], b[1] - a[1]), EPSILON) ** ALPHA;
|
|
4116
|
+
}
|
|
4117
|
+
function lerpKnot(a, b, ta, tb, t) {
|
|
4118
|
+
const span = tb - ta;
|
|
4119
|
+
const k = span === 0 ? 0 : (t - ta) / span;
|
|
4120
|
+
return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k];
|
|
4121
|
+
}
|
|
4122
|
+
function segmentPoint(p0, p1, p2, p3, u) {
|
|
4123
|
+
const t0 = 0;
|
|
4124
|
+
const t1 = knot(p0, p1, t0);
|
|
4125
|
+
const t2 = knot(p1, p2, t1);
|
|
4126
|
+
const t3 = knot(p2, p3, t2);
|
|
4127
|
+
const t = t1 + (t2 - t1) * u;
|
|
4128
|
+
const a1 = lerpKnot(p0, p1, t0, t1, t);
|
|
4129
|
+
const a2 = lerpKnot(p1, p2, t1, t2, t);
|
|
4130
|
+
const a3 = lerpKnot(p2, p3, t2, t3, t);
|
|
4131
|
+
const b1 = lerpKnot(a1, a2, t0, t2, t);
|
|
4132
|
+
const b2 = lerpKnot(a2, a3, t1, t3, t);
|
|
4133
|
+
return lerpKnot(b1, b2, t1, t2, t);
|
|
4134
|
+
}
|
|
4135
|
+
function reflect(end, inner) {
|
|
4136
|
+
return [2 * end[0] - inner[0], 2 * end[1] - inner[1]];
|
|
4137
|
+
}
|
|
4138
|
+
function createWalkPath(options) {
|
|
4139
|
+
const pts = options.points;
|
|
4140
|
+
const n = pts.length;
|
|
4141
|
+
const closed = options.closed && n > 2;
|
|
4142
|
+
const segments = closed ? n : n - 1;
|
|
4143
|
+
const samples = [];
|
|
4144
|
+
const cumulative = [];
|
|
4145
|
+
let total = 0;
|
|
4146
|
+
for (let seg = 0; seg < segments; seg++) {
|
|
4147
|
+
const p1 = pts[seg];
|
|
4148
|
+
const p2 = pts[(seg + 1) % n];
|
|
4149
|
+
const p0 = closed ? pts[(seg - 1 + n) % n] : seg > 0 ? pts[seg - 1] : reflect(pts[0], pts[1]);
|
|
4150
|
+
const p3 = closed ? pts[(seg + 2) % n] : seg + 2 < n ? pts[seg + 2] : reflect(pts[n - 1], pts[n - 2]);
|
|
4151
|
+
const last = seg === segments - 1 && !closed ? SAMPLES_PER_SEGMENT : SAMPLES_PER_SEGMENT - 1;
|
|
4152
|
+
for (let k = 0; k <= last; k++) {
|
|
4153
|
+
const point = segmentPoint(p0, p1, p2, p3, k / SAMPLES_PER_SEGMENT);
|
|
4154
|
+
const previous = samples[samples.length - 1];
|
|
4155
|
+
if (previous) total += Math.hypot(point[0] - previous[0], point[1] - previous[1]);
|
|
4156
|
+
samples.push(point);
|
|
4157
|
+
cumulative.push(total);
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
if (closed) {
|
|
4161
|
+
const first = samples[0];
|
|
4162
|
+
const previous = samples[samples.length - 1];
|
|
4163
|
+
total += Math.hypot(first[0] - previous[0], first[1] - previous[1]);
|
|
4164
|
+
samples.push([first[0], first[1]]);
|
|
4165
|
+
cumulative.push(total);
|
|
4166
|
+
}
|
|
4167
|
+
const length = total;
|
|
4168
|
+
function normalize(s) {
|
|
4169
|
+
if (!Number.isFinite(s)) return 0;
|
|
4170
|
+
if (!closed) return Math.min(Math.max(s, 0), 1);
|
|
4171
|
+
const wrapped = s - Math.floor(s);
|
|
4172
|
+
return wrapped;
|
|
4173
|
+
}
|
|
4174
|
+
function pointAt(s) {
|
|
4175
|
+
const target = normalize(s) * length;
|
|
4176
|
+
if (length === 0) return [samples[0][0], samples[0][1]];
|
|
4177
|
+
let low = 0;
|
|
4178
|
+
let high = cumulative.length - 1;
|
|
4179
|
+
while (low < high) {
|
|
4180
|
+
const mid = low + high >> 1;
|
|
4181
|
+
if (cumulative[mid] < target) low = mid + 1;
|
|
4182
|
+
else high = mid;
|
|
4183
|
+
}
|
|
4184
|
+
const i = Math.max(low, 1);
|
|
4185
|
+
const before = cumulative[i - 1];
|
|
4186
|
+
const span = cumulative[i] - before;
|
|
4187
|
+
const k = span <= 0 ? 0 : (target - before) / span;
|
|
4188
|
+
const a = samples[i - 1];
|
|
4189
|
+
const b = samples[i];
|
|
4190
|
+
return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k];
|
|
4191
|
+
}
|
|
4192
|
+
function tangentAt(s) {
|
|
4193
|
+
const ds = length > 0 ? Math.min(0.01, 0.5 / length) : 0.01;
|
|
4194
|
+
const here = normalize(s);
|
|
4195
|
+
const a = pointAt(closed ? here - ds : Math.max(here - ds, 0));
|
|
4196
|
+
const b = pointAt(closed ? here + ds : Math.min(here + ds, 1));
|
|
4197
|
+
const dx = b[0] - a[0];
|
|
4198
|
+
const dz = b[1] - a[1];
|
|
4199
|
+
const len = Math.hypot(dx, dz);
|
|
4200
|
+
return len < EPSILON ? [0, -1] : [dx / len, dz / len];
|
|
4201
|
+
}
|
|
4202
|
+
function normalAt(s) {
|
|
4203
|
+
const [tx, tz] = tangentAt(s);
|
|
4204
|
+
return [tz, -tx];
|
|
4205
|
+
}
|
|
4206
|
+
return { length, closed, pointAt, tangentAt, normalAt };
|
|
4207
|
+
}
|
|
4208
|
+
var cache = /* @__PURE__ */ new Map();
|
|
4209
|
+
var CACHE_LIMIT = 32;
|
|
4210
|
+
function getWalkPath(options) {
|
|
4211
|
+
const key = `${options.closed ? "c" : "o"}|${options.points.map((p) => `${p[0]},${p[1]}`).join(";")}`;
|
|
4212
|
+
const hit = cache.get(key);
|
|
4213
|
+
if (hit) return hit;
|
|
4214
|
+
const path = createWalkPath(options);
|
|
4215
|
+
if (cache.size >= CACHE_LIMIT) {
|
|
4216
|
+
const oldest = cache.keys().next().value;
|
|
4217
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
4218
|
+
}
|
|
4219
|
+
cache.set(key, path);
|
|
4220
|
+
return path;
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
// src/field/layouts/index.ts
|
|
4224
|
+
var DEFAULT_SHEET = { width: 1, height: 1.4 };
|
|
4225
|
+
var TAU3 = Math.PI * 2;
|
|
4226
|
+
var DEG6 = Math.PI / 180;
|
|
4227
|
+
function jitter2(seed, i) {
|
|
3667
4228
|
let h = Math.imul(seed * 1e3 + i + 1 ^ 2654435769, 2654435761);
|
|
3668
4229
|
h = Math.imul(h ^ h >>> 13, 3266489917);
|
|
3669
4230
|
return ((h ^ h >>> 16) >>> 0) / 4294967295 * 2 - 1;
|
|
3670
4231
|
}
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
4232
|
+
function ramp(i, n) {
|
|
4233
|
+
return n > 1 ? i / (n - 1) : 1;
|
|
4234
|
+
}
|
|
4235
|
+
var ringSchema = z22.object({
|
|
4236
|
+
radius: z22.number().min(0.5).max(12).default(2.6),
|
|
4237
|
+
tiltDeg: z22.number().min(-45).max(45).default(8)
|
|
3674
4238
|
});
|
|
3675
4239
|
var ring = {
|
|
3676
4240
|
id: "ring",
|
|
@@ -3678,7 +4242,7 @@ var ring = {
|
|
|
3678
4242
|
defaults: ringSchema.parse({}),
|
|
3679
4243
|
optionsSchema: ringSchema,
|
|
3680
4244
|
pose(i, n, o, phase) {
|
|
3681
|
-
const theta = (i / n + phase) *
|
|
4245
|
+
const theta = (i / n + phase) * TAU3;
|
|
3682
4246
|
return {
|
|
3683
4247
|
position: [Math.sin(theta) * o.radius, 0, Math.cos(theta) * o.radius],
|
|
3684
4248
|
// Face radially OUTWARD so the papers nearest the camera show their
|
|
@@ -3688,128 +4252,314 @@ var ring = {
|
|
|
3688
4252
|
};
|
|
3689
4253
|
}
|
|
3690
4254
|
};
|
|
3691
|
-
var
|
|
3692
|
-
|
|
3693
|
-
|
|
4255
|
+
var fanSchema = z22.object({
|
|
4256
|
+
/** Total angular sweep from the first sheet to the last, degrees. */
|
|
4257
|
+
sweep: z22.number().min(0).max(180).default(72),
|
|
4258
|
+
/** Where the shared pin sits, in half-sheet-heights below center. 1 = the bottom edge. */
|
|
4259
|
+
hinge: z22.number().min(0).max(4).default(1.15),
|
|
4260
|
+
/** Thickness step so the sheets stack in order instead of z-fighting. */
|
|
4261
|
+
lift: z22.number().min(2e-3).max(0.08).default(0.012),
|
|
4262
|
+
/** How much flatter the middle of the fan sits than its outer sheets. */
|
|
4263
|
+
bow: z22.number().min(0).max(1).default(0.7)
|
|
3694
4264
|
});
|
|
3695
|
-
var
|
|
3696
|
-
id: "
|
|
3697
|
-
label: "
|
|
3698
|
-
defaults:
|
|
3699
|
-
optionsSchema:
|
|
3700
|
-
pose(i,
|
|
4265
|
+
var fan = {
|
|
4266
|
+
id: "fan",
|
|
4267
|
+
label: "Fan",
|
|
4268
|
+
defaults: fanSchema.parse({}),
|
|
4269
|
+
optionsSchema: fanSchema,
|
|
4270
|
+
pose(i, n, o, _phase, sheet2) {
|
|
4271
|
+
const f = n > 1 ? i / (n - 1) : 0.5;
|
|
4272
|
+
const theta = (f - 0.5) * o.sweep * DEG6;
|
|
4273
|
+
const hinge = o.hinge * sheet2.height / 2;
|
|
4274
|
+
const open = Math.abs(f - 0.5) * 2;
|
|
3701
4275
|
return {
|
|
3702
|
-
position: [
|
|
3703
|
-
rotation: [0, 0,
|
|
3704
|
-
scale: 1
|
|
4276
|
+
position: [-Math.sin(theta) * hinge, Math.cos(theta) * hinge - hinge, i * o.lift],
|
|
4277
|
+
rotation: [0, 0, theta],
|
|
4278
|
+
scale: 1,
|
|
4279
|
+
bias: 1 - (1 - open) * o.bow
|
|
3705
4280
|
};
|
|
3706
4281
|
}
|
|
3707
4282
|
};
|
|
3708
|
-
var
|
|
3709
|
-
|
|
3710
|
-
|
|
4283
|
+
var spreadSchema = z22.object({
|
|
4284
|
+
/** How far each sheet slides past the one below it. */
|
|
4285
|
+
slip: z22.number().min(0.02).max(2).default(0.3),
|
|
4286
|
+
/** Direction of the slide, degrees. 0 slides right, 90 slides up. */
|
|
4287
|
+
angle: z22.number().min(-180).max(180).default(28),
|
|
4288
|
+
lift: z22.number().min(2e-3).max(0.08).default(0.012),
|
|
4289
|
+
/** How much more the sheets at the far end of the slide bow. */
|
|
4290
|
+
bow: z22.number().min(0).max(1).default(0.6),
|
|
4291
|
+
/** Nothing hand-slid is perfectly square — a touch of per-sheet rotation. */
|
|
4292
|
+
drift: z22.number().min(0).max(1).default(0.15)
|
|
3711
4293
|
});
|
|
3712
|
-
var
|
|
3713
|
-
id: "
|
|
3714
|
-
label: "
|
|
3715
|
-
defaults:
|
|
3716
|
-
optionsSchema:
|
|
4294
|
+
var spread = {
|
|
4295
|
+
id: "spread",
|
|
4296
|
+
label: "Spread",
|
|
4297
|
+
defaults: spreadSchema.parse({}),
|
|
4298
|
+
optionsSchema: spreadSchema,
|
|
3717
4299
|
pose(i, n, o) {
|
|
3718
4300
|
const centered = i - (n - 1) / 2;
|
|
4301
|
+
const a = o.angle * DEG6;
|
|
3719
4302
|
return {
|
|
3720
|
-
position: [
|
|
3721
|
-
rotation: [0, 0,
|
|
3722
|
-
scale: 1
|
|
4303
|
+
position: [Math.cos(a) * o.slip * centered, Math.sin(a) * o.slip * centered, i * o.lift],
|
|
4304
|
+
rotation: [0, 0, jitter2(11, i) * 0.2 * o.drift],
|
|
4305
|
+
scale: 1,
|
|
4306
|
+
bias: 1 - (1 - ramp(i, n)) * o.bow
|
|
3723
4307
|
};
|
|
3724
4308
|
}
|
|
3725
4309
|
};
|
|
3726
|
-
var
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
4310
|
+
var pileSchema = z22.object({
|
|
4311
|
+
/** How far sheets wander from the center of the heap. */
|
|
4312
|
+
scatter: z22.number().min(0).max(2).default(0.22),
|
|
4313
|
+
/** Widest angle a sheet sits off square, degrees. */
|
|
4314
|
+
turn: z22.number().min(0).max(180).default(24),
|
|
4315
|
+
lift: z22.number().min(2e-3).max(0.08).default(0.011),
|
|
4316
|
+
/** How flat the sheets underneath are pressed by the ones on top. */
|
|
4317
|
+
press: z22.number().min(0).max(1).default(0.85),
|
|
4318
|
+
seed: z22.number().int().min(0).max(9999).default(3)
|
|
3730
4319
|
});
|
|
3731
|
-
var
|
|
3732
|
-
id: "
|
|
3733
|
-
label: "
|
|
3734
|
-
defaults:
|
|
3735
|
-
optionsSchema:
|
|
3736
|
-
pose(i, n, o
|
|
3737
|
-
const f = n > 1 ? i / (n - 1) : 0;
|
|
3738
|
-
const theta = (f * o.turns + phase) * TAU2;
|
|
4320
|
+
var pile = {
|
|
4321
|
+
id: "pile",
|
|
4322
|
+
label: "Pile",
|
|
4323
|
+
defaults: pileSchema.parse({}),
|
|
4324
|
+
optionsSchema: pileSchema,
|
|
4325
|
+
pose(i, n, o) {
|
|
3739
4326
|
return {
|
|
3740
|
-
position: [
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
4327
|
+
position: [jitter2(o.seed, i) * o.scatter, jitter2(o.seed + 1, i) * o.scatter * 0.8, i * o.lift],
|
|
4328
|
+
rotation: [0, 0, jitter2(o.seed + 2, i) * o.turn * DEG6],
|
|
4329
|
+
scale: 1,
|
|
4330
|
+
bias: 1 - (1 - ramp(i, n)) * o.press
|
|
3744
4331
|
};
|
|
3745
4332
|
}
|
|
3746
4333
|
};
|
|
3747
|
-
var wallSchema =
|
|
3748
|
-
gapX:
|
|
3749
|
-
gapY:
|
|
3750
|
-
jitterAmt:
|
|
4334
|
+
var wallSchema = z22.object({
|
|
4335
|
+
gapX: z22.number().min(0.05).max(1).default(0.22),
|
|
4336
|
+
gapY: z22.number().min(0.05).max(1).default(0.3),
|
|
4337
|
+
jitterAmt: z22.number().min(0).max(1).default(0.25),
|
|
4338
|
+
/** Spread of sag across the wall — no two pinned sheets hang alike. */
|
|
4339
|
+
sag: z22.number().min(0).max(1).default(0.45)
|
|
3751
4340
|
});
|
|
3752
4341
|
var wall = {
|
|
3753
4342
|
id: "wall",
|
|
3754
4343
|
label: "Wall",
|
|
3755
4344
|
defaults: wallSchema.parse({}),
|
|
3756
4345
|
optionsSchema: wallSchema,
|
|
3757
|
-
pose(i, n, o) {
|
|
3758
|
-
const cols = Math.ceil(Math.sqrt(n *
|
|
4346
|
+
pose(i, n, o, _phase, sheet2) {
|
|
4347
|
+
const cols = Math.ceil(Math.sqrt(n * sheet2.height / sheet2.width));
|
|
3759
4348
|
const rows = Math.ceil(n / cols);
|
|
3760
4349
|
const col = i % cols;
|
|
3761
4350
|
const row = Math.floor(i / cols);
|
|
3762
|
-
const cellW =
|
|
3763
|
-
const cellH =
|
|
4351
|
+
const cellW = sheet2.width + o.gapX;
|
|
4352
|
+
const cellH = sheet2.height + o.gapY;
|
|
3764
4353
|
return {
|
|
3765
4354
|
position: [
|
|
3766
4355
|
(col - (cols - 1) / 2) * cellW,
|
|
3767
4356
|
((rows - 1) / 2 - row) * cellH,
|
|
3768
|
-
|
|
4357
|
+
jitter2(5, i) * 0.04 * o.jitterAmt * 4
|
|
3769
4358
|
],
|
|
3770
|
-
rotation: [0, 0,
|
|
3771
|
-
scale: 1
|
|
4359
|
+
rotation: [0, 0, jitter2(6, i) * 0.05 * o.jitterAmt * 4],
|
|
4360
|
+
scale: 1,
|
|
4361
|
+
bias: 1 - Math.abs(jitter2(7, i)) * o.sag
|
|
3772
4362
|
};
|
|
3773
4363
|
}
|
|
3774
4364
|
};
|
|
3775
|
-
var
|
|
3776
|
-
|
|
3777
|
-
|
|
4365
|
+
var spillSchema = z22.object({
|
|
4366
|
+
spreadX: z22.number().min(0.5).max(8).default(2.4),
|
|
4367
|
+
spreadY: z22.number().min(0.5).max(8).default(1.5),
|
|
4368
|
+
depth: z22.number().min(0).max(6).default(1.6),
|
|
4369
|
+
/** How far sheets pitch and roll out of the picture plane. */
|
|
4370
|
+
tumble: z22.number().min(0).max(1).default(0.5),
|
|
4371
|
+
/** Spread of bend across the sheets — a spill does not fold them alike. */
|
|
4372
|
+
vary: z22.number().min(0).max(1).default(0.6),
|
|
4373
|
+
seed: z22.number().int().min(0).max(9999).default(7)
|
|
3778
4374
|
});
|
|
3779
|
-
var
|
|
3780
|
-
id: "
|
|
3781
|
-
label: "
|
|
3782
|
-
defaults:
|
|
3783
|
-
optionsSchema:
|
|
3784
|
-
pose(i, _n, o
|
|
3785
|
-
const
|
|
4375
|
+
var spill = {
|
|
4376
|
+
id: "spill",
|
|
4377
|
+
label: "Spill",
|
|
4378
|
+
defaults: spillSchema.parse({}),
|
|
4379
|
+
optionsSchema: spillSchema,
|
|
4380
|
+
pose(i, _n, o) {
|
|
4381
|
+
const tumble = o.tumble * 2;
|
|
3786
4382
|
return {
|
|
3787
|
-
position: [
|
|
3788
|
-
|
|
3789
|
-
|
|
4383
|
+
position: [
|
|
4384
|
+
jitter2(o.seed, i) * o.spreadX,
|
|
4385
|
+
jitter2(o.seed + 1, i) * o.spreadY,
|
|
4386
|
+
jitter2(o.seed + 2, i) * o.depth
|
|
4387
|
+
],
|
|
4388
|
+
rotation: [
|
|
4389
|
+
jitter2(o.seed + 3, i) * 0.4 * tumble,
|
|
4390
|
+
jitter2(o.seed + 4, i) * 0.5 * tumble,
|
|
4391
|
+
jitter2(o.seed + 5, i) * 0.4 * tumble
|
|
4392
|
+
],
|
|
4393
|
+
scale: 0.85 + Math.abs(jitter2(o.seed + 6, i)) * 0.3,
|
|
4394
|
+
bias: 1 - Math.abs(jitter2(o.seed + 7, i)) * o.vary
|
|
3790
4395
|
};
|
|
3791
4396
|
}
|
|
3792
4397
|
};
|
|
3793
|
-
var
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
4398
|
+
var sweepSchema = z22.object({
|
|
4399
|
+
columns: z22.number().int().min(1).max(24).default(5),
|
|
4400
|
+
/** Breathing room around each specimen. */
|
|
4401
|
+
gap: z22.number().min(0).max(2).default(0.22),
|
|
4402
|
+
/** Deformation at the first specimen and at the last. */
|
|
4403
|
+
from: z22.number().min(0).max(1).default(0),
|
|
4404
|
+
to: z22.number().min(0).max(1).default(1)
|
|
3798
4405
|
});
|
|
3799
|
-
var
|
|
3800
|
-
id: "
|
|
3801
|
-
label: "
|
|
3802
|
-
defaults:
|
|
3803
|
-
optionsSchema:
|
|
3804
|
-
pose(i,
|
|
4406
|
+
var sweep = {
|
|
4407
|
+
id: "sweep",
|
|
4408
|
+
label: "Sweep",
|
|
4409
|
+
defaults: sweepSchema.parse({}),
|
|
4410
|
+
optionsSchema: sweepSchema,
|
|
4411
|
+
pose(i, n, o, _phase, sheet2) {
|
|
4412
|
+
const cols = Math.min(o.columns, Math.max(n, 1));
|
|
4413
|
+
const rows = Math.ceil(n / cols);
|
|
4414
|
+
const col = i % cols;
|
|
4415
|
+
const row = Math.floor(i / cols);
|
|
3805
4416
|
return {
|
|
3806
4417
|
position: [
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
4418
|
+
(col - (cols - 1) / 2) * (sheet2.width + o.gap),
|
|
4419
|
+
((rows - 1) / 2 - row) * (sheet2.height + o.gap),
|
|
4420
|
+
0
|
|
3810
4421
|
],
|
|
3811
|
-
rotation: [
|
|
3812
|
-
scale:
|
|
4422
|
+
rotation: [0, 0, 0],
|
|
4423
|
+
scale: 1,
|
|
4424
|
+
bias: o.from + (o.to - o.from) * ramp(i, n)
|
|
4425
|
+
};
|
|
4426
|
+
}
|
|
4427
|
+
};
|
|
4428
|
+
var bookSchema = z22.object({
|
|
4429
|
+
/** How far the outermost page lifts off the block, degrees. */
|
|
4430
|
+
spread: z22.number().min(0).max(150).default(55),
|
|
4431
|
+
/** Fraction of the pages bound to the left. 0 = a one-sided sample book. */
|
|
4432
|
+
split: z22.number().min(0).max(1).default(0.5),
|
|
4433
|
+
/** Page thickness — the gap between pages of one block. */
|
|
4434
|
+
lift: z22.number().min(1e-3).max(0.05).default(8e-3),
|
|
4435
|
+
/** How much more a lifted page arcs than one lying flat in the block. */
|
|
4436
|
+
gutter: z22.number().min(0).max(1).default(0.6)
|
|
4437
|
+
});
|
|
4438
|
+
var book = {
|
|
4439
|
+
id: "book",
|
|
4440
|
+
label: "Book",
|
|
4441
|
+
defaults: bookSchema.parse({}),
|
|
4442
|
+
optionsSchema: bookSchema,
|
|
4443
|
+
pose(i, n, o, _phase, sheet2) {
|
|
4444
|
+
const half = sheet2.width / 2;
|
|
4445
|
+
const left = Math.round(n * o.split);
|
|
4446
|
+
const onLeft = i < left;
|
|
4447
|
+
const count = onLeft ? left : n - left;
|
|
4448
|
+
const k = onLeft ? i : i - left;
|
|
4449
|
+
const f = count > 1 ? k / (count - 1) : 1;
|
|
4450
|
+
const theta = f * o.spread * DEG6;
|
|
4451
|
+
const side = onLeft ? -1 : 1;
|
|
4452
|
+
const cos = Math.cos(theta);
|
|
4453
|
+
const sin = Math.sin(theta);
|
|
4454
|
+
const offset = k * o.lift;
|
|
4455
|
+
return {
|
|
4456
|
+
position: [side * (half * cos - offset * sin), 0, half * sin + offset * cos],
|
|
4457
|
+
rotation: [0, -side * theta, 0],
|
|
4458
|
+
scale: 1,
|
|
4459
|
+
bias: 1 - (1 - f) * o.gutter
|
|
4460
|
+
};
|
|
4461
|
+
}
|
|
4462
|
+
};
|
|
4463
|
+
var accordionSchema = z22.object({
|
|
4464
|
+
/** How far each panel tilts off the strip's line, degrees. 0 = flat, 90 = shut. */
|
|
4465
|
+
angle: z22.number().min(0).max(89).default(55),
|
|
4466
|
+
/** A concertina holds its creases — how much bow the panels keep. */
|
|
4467
|
+
slack: z22.number().min(0).max(1).default(0.15)
|
|
4468
|
+
});
|
|
4469
|
+
var accordion = {
|
|
4470
|
+
id: "accordion",
|
|
4471
|
+
label: "Accordion",
|
|
4472
|
+
defaults: accordionSchema.parse({}),
|
|
4473
|
+
optionsSchema: accordionSchema,
|
|
4474
|
+
pose(i, n, o, _phase, sheet2) {
|
|
4475
|
+
const theta = o.angle * DEG6;
|
|
4476
|
+
const side = i % 2 === 0 ? 1 : -1;
|
|
4477
|
+
const step = sheet2.width * Math.cos(theta);
|
|
4478
|
+
return {
|
|
4479
|
+
position: [(i - (n - 1) / 2) * step, 0, 0],
|
|
4480
|
+
rotation: [0, side * theta, 0],
|
|
4481
|
+
scale: 1,
|
|
4482
|
+
bias: o.slack
|
|
4483
|
+
};
|
|
4484
|
+
}
|
|
4485
|
+
};
|
|
4486
|
+
var rackSchema = z22.object({
|
|
4487
|
+
/** Gap along the row, as a fraction of the paper's width. Under 1 they overlap. */
|
|
4488
|
+
spacing: z22.number().min(0.05).max(2).default(0.82),
|
|
4489
|
+
/** How far a sheet leans back off vertical, degrees. */
|
|
4490
|
+
lean: z22.number().min(0).max(70).default(16),
|
|
4491
|
+
/** How much that lean differs sheet to sheet — nothing propped is uniform. */
|
|
4492
|
+
vary: z22.number().min(0).max(1).default(0.55),
|
|
4493
|
+
/** Small rotations off square. */
|
|
4494
|
+
sway: z22.number().min(0).max(1).default(0.35),
|
|
4495
|
+
seed: z22.number().int().min(0).max(9999).default(5)
|
|
4496
|
+
});
|
|
4497
|
+
var rack = {
|
|
4498
|
+
id: "rack",
|
|
4499
|
+
label: "Rack",
|
|
4500
|
+
defaults: rackSchema.parse({}),
|
|
4501
|
+
optionsSchema: rackSchema,
|
|
4502
|
+
pose(i, n, o, _phase, sheet2) {
|
|
4503
|
+
const lean = o.lean * DEG6 * (1 + jitter2(o.seed, i) * o.vary);
|
|
4504
|
+
const half = sheet2.height / 2;
|
|
4505
|
+
return {
|
|
4506
|
+
position: [
|
|
4507
|
+
(i - (n - 1) / 2) * sheet2.width * o.spacing,
|
|
4508
|
+
// Standing on the floor: the bottom edge stays at y = 0 as it leans.
|
|
4509
|
+
half * Math.cos(lean),
|
|
4510
|
+
-half * Math.sin(lean) + i * 4e-3
|
|
4511
|
+
],
|
|
4512
|
+
rotation: [-lean, jitter2(o.seed + 1, i) * 0.12 * o.sway, jitter2(o.seed + 2, i) * 0.06 * o.sway],
|
|
4513
|
+
scale: 1,
|
|
4514
|
+
// A sheet leaning further has more of its own weight to carry.
|
|
4515
|
+
bias: o.lean === 0 ? 0 : Math.min(1, lean / (o.lean * DEG6 * (1 + o.vary)))
|
|
4516
|
+
};
|
|
4517
|
+
}
|
|
4518
|
+
};
|
|
4519
|
+
var colonnadeSchema = z22.object({
|
|
4520
|
+
/** The walk the colonnade is built along — see `stage/path`. */
|
|
4521
|
+
path: walkPathSchema.default({}),
|
|
4522
|
+
/** Half-width of the clear aisle: how far each banner stands off the walk line. */
|
|
4523
|
+
aisle: z22.number().min(0.2).max(20).default(2.4),
|
|
4524
|
+
/** How much that gap opens and closes along the walk. Nothing hung by hand is a corridor. */
|
|
4525
|
+
breathe: z22.number().min(0).max(1).default(0.3),
|
|
4526
|
+
/** Widest angle a banner turns off square to the aisle, degrees. */
|
|
4527
|
+
twist: z22.number().min(0).max(90).default(22),
|
|
4528
|
+
/** Fraction of the walk left clear at each end, so the figure has somewhere to enter from. */
|
|
4529
|
+
margin: z22.number().min(0).max(0.45).default(0.05),
|
|
4530
|
+
/** Spread of banner heights, 0..1. */
|
|
4531
|
+
rise: z22.number().min(0).max(1).default(0.28),
|
|
4532
|
+
/** How far the banners lift off the floor, as a fraction of their height. 0 = they pool on it. */
|
|
4533
|
+
hover: z22.number().min(0).max(1).default(0),
|
|
4534
|
+
/** Spread of deformation — no two lengths of hung paper drape alike. */
|
|
4535
|
+
drape: z22.number().min(0).max(1).default(0.5),
|
|
4536
|
+
seed: z22.number().int().min(0).max(9999).default(2)
|
|
4537
|
+
});
|
|
4538
|
+
var colonnade = {
|
|
4539
|
+
id: "colonnade",
|
|
4540
|
+
label: "Colonnade",
|
|
4541
|
+
defaults: colonnadeSchema.parse({}),
|
|
4542
|
+
optionsSchema: colonnadeSchema,
|
|
4543
|
+
pose(i, n, o, phase, sheet2) {
|
|
4544
|
+
const path = getWalkPath(o.path);
|
|
4545
|
+
const side = i % 2 === 0 ? 1 : -1;
|
|
4546
|
+
const pairs = Math.max(Math.ceil(n / 2), 1);
|
|
4547
|
+
const k = Math.floor(i / 2);
|
|
4548
|
+
const span = 1 - o.margin * 2;
|
|
4549
|
+
const step = pairs > 1 ? span / (pairs - 1) : 0;
|
|
4550
|
+
const base = o.margin + (pairs > 1 ? k * step : span / 2) + side * step * 0.25;
|
|
4551
|
+
const s = path.closed ? base + phase : base;
|
|
4552
|
+
const [px, pz] = path.pointAt(s);
|
|
4553
|
+
const [nx, nz] = path.normalAt(s);
|
|
4554
|
+
const scale = 1 + jitter2(o.seed, i) * o.rise * 0.5;
|
|
4555
|
+
const offset = o.aisle * (1 + jitter2(o.seed + 1, i) * o.breathe);
|
|
4556
|
+
const height = sheet2.height * scale;
|
|
4557
|
+
const yaw = Math.atan2(-side * nx, -side * nz) + jitter2(o.seed + 2, i) * o.twist * DEG6;
|
|
4558
|
+
return {
|
|
4559
|
+
position: [px + nx * side * offset, height / 2 + height * o.hover, pz + nz * side * offset],
|
|
4560
|
+
rotation: [0, yaw, 0],
|
|
4561
|
+
scale,
|
|
4562
|
+
bias: 1 - Math.abs(jitter2(o.seed + 3, i)) * o.drape
|
|
3813
4563
|
};
|
|
3814
4564
|
}
|
|
3815
4565
|
};
|
|
@@ -3838,36 +4588,48 @@ function listLayouts() {
|
|
|
3838
4588
|
return [...registry3.keys()];
|
|
3839
4589
|
}
|
|
3840
4590
|
registerLayout(ring);
|
|
3841
|
-
registerLayout(
|
|
3842
|
-
registerLayout(
|
|
3843
|
-
registerLayout(
|
|
4591
|
+
registerLayout(fan);
|
|
4592
|
+
registerLayout(spread);
|
|
4593
|
+
registerLayout(pile);
|
|
3844
4594
|
registerLayout(wall);
|
|
3845
|
-
registerLayout(
|
|
3846
|
-
registerLayout(
|
|
4595
|
+
registerLayout(spill);
|
|
4596
|
+
registerLayout(sweep);
|
|
4597
|
+
registerLayout(book);
|
|
4598
|
+
registerLayout(accordion);
|
|
4599
|
+
registerLayout(rack);
|
|
4600
|
+
registerLayout(colonnade);
|
|
3847
4601
|
registerLayout(sheet);
|
|
3848
4602
|
|
|
4603
|
+
// src/field/stack.ts
|
|
4604
|
+
function fieldShapeStack(config, progress) {
|
|
4605
|
+
if (config.deformers) return resolveDeformerStack(config.deformers);
|
|
4606
|
+
if (!config.behavior) return [];
|
|
4607
|
+
const behavior = getBehavior(config.behavior.type);
|
|
4608
|
+
const options = { ...config.behavior, [behavior.progressParam]: progress };
|
|
4609
|
+
return behavior.stack(options, config.sheet);
|
|
4610
|
+
}
|
|
4611
|
+
|
|
3849
4612
|
// src/field/fieldGroup.tsx
|
|
3850
4613
|
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
3851
|
-
var scratchObj = new
|
|
4614
|
+
var scratchObj = new THREE11.Object3D();
|
|
3852
4615
|
var scratchAero = { position: [0, 0, 0], rotation: [0, 0, 0] };
|
|
3853
4616
|
var FIELD_SEGMENT_CAP = 48;
|
|
3854
4617
|
function FieldGroup({ group, shared }) {
|
|
3855
4618
|
const { config, indices, contents } = group;
|
|
3856
4619
|
const count = indices.length;
|
|
3857
4620
|
const stock = getStock(config.stock);
|
|
3858
|
-
const behavior = config.behavior ? getBehavior(config.behavior.type) : null;
|
|
4621
|
+
const behavior = config.behavior && !config.deformers ? getBehavior(config.behavior.type) : null;
|
|
3859
4622
|
const progressRef = useRef4(
|
|
3860
4623
|
behavior ? config.behavior[behavior.progressParam] : 0
|
|
3861
4624
|
);
|
|
3862
|
-
const buildStackAt = (progress) =>
|
|
3863
|
-
if (!config.behavior || !behavior) return [];
|
|
3864
|
-
const options = { ...config.behavior, [behavior.progressParam]: progress };
|
|
3865
|
-
return behavior.stack(options, config.sheet);
|
|
3866
|
-
};
|
|
4625
|
+
const buildStackAt = (progress) => fieldShapeStack(config, progress);
|
|
3867
4626
|
const initialStack = useMemo7(
|
|
3868
4627
|
() => buildStackAt(progressRef.current),
|
|
3869
|
-
|
|
3870
|
-
|
|
4628
|
+
[
|
|
4629
|
+
JSON.stringify(config.behavior ?? null),
|
|
4630
|
+
JSON.stringify(config.deformers ?? null),
|
|
4631
|
+
JSON.stringify(config.sheet)
|
|
4632
|
+
]
|
|
3871
4633
|
);
|
|
3872
4634
|
const structureKey = initialStack.map((i) => i.type).join("|");
|
|
3873
4635
|
const geometry = useMemo7(() => {
|
|
@@ -3880,12 +4642,14 @@ function FieldGroup({ group, shared }) {
|
|
|
3880
4642
|
);
|
|
3881
4643
|
const atlasIdx = new Float32Array(count);
|
|
3882
4644
|
const phase = new Float32Array(count);
|
|
4645
|
+
const bias = new Float32Array(count).fill(1);
|
|
3883
4646
|
for (let i = 0; i < count; i++) {
|
|
3884
4647
|
atlasIdx[i] = i;
|
|
3885
4648
|
phase[i] = indices[i] * 0.618034 % 1 * 4;
|
|
3886
4649
|
}
|
|
3887
|
-
geo.setAttribute("aAtlas", new
|
|
3888
|
-
geo.setAttribute("aPhase", new
|
|
4650
|
+
geo.setAttribute("aAtlas", new THREE11.InstancedBufferAttribute(atlasIdx, 1));
|
|
4651
|
+
geo.setAttribute("aPhase", new THREE11.InstancedBufferAttribute(phase, 1));
|
|
4652
|
+
geo.setAttribute("aBias", new THREE11.InstancedBufferAttribute(bias, 1));
|
|
3889
4653
|
return geo;
|
|
3890
4654
|
}, [JSON.stringify(config.sheet), structureKey, count]);
|
|
3891
4655
|
useEffect9(() => () => geometry.dispose(), [geometry]);
|
|
@@ -3895,23 +4659,34 @@ function FieldGroup({ group, shared }) {
|
|
|
3895
4659
|
const uniforms = {};
|
|
3896
4660
|
for (const [name, value] of Object.entries(composed.uniforms)) {
|
|
3897
4661
|
uniforms[name] = {
|
|
3898
|
-
value: Array.isArray(value) && value.length === 2 ? new
|
|
4662
|
+
value: Array.isArray(value) && value.length === 2 ? new THREE11.Vector2(...value) : value
|
|
3899
4663
|
};
|
|
3900
4664
|
}
|
|
3901
4665
|
uniforms.uPlTime = { value: 0 };
|
|
3902
4666
|
uniforms.uAtlas = { value: null };
|
|
3903
|
-
uniforms.uAtlasGrid = { value: new
|
|
4667
|
+
uniforms.uAtlasGrid = { value: new THREE11.Vector2(1, 1) };
|
|
3904
4668
|
uniforms.uBackDarken = {
|
|
3905
4669
|
value: 1 - Math.min(0.45, 0.12 + config.sheet.thickness * 0.9) * stock.opacity
|
|
3906
4670
|
};
|
|
3907
|
-
uniforms.uStockColor = { value: new
|
|
4671
|
+
uniforms.uStockColor = { value: new THREE11.Color(stock.color) };
|
|
3908
4672
|
uniforms.uShowThrough = { value: config.surface.showThrough ?? stock.showThrough };
|
|
4673
|
+
Object.assign(
|
|
4674
|
+
uniforms,
|
|
4675
|
+
translucencyUniforms(config.surface.translucency ?? stock.translucency, config.scene.lighting)
|
|
4676
|
+
);
|
|
3909
4677
|
return {
|
|
3910
4678
|
vertexShader: buildFieldVertexShader(composed),
|
|
3911
4679
|
fragmentShader: buildFieldFragmentShader(),
|
|
3912
4680
|
uniforms
|
|
3913
4681
|
};
|
|
3914
|
-
}, [
|
|
4682
|
+
}, [
|
|
4683
|
+
structureKey,
|
|
4684
|
+
JSON.stringify(config.sheet),
|
|
4685
|
+
stock.id,
|
|
4686
|
+
config.surface.showThrough,
|
|
4687
|
+
config.surface.translucency,
|
|
4688
|
+
config.scene.lighting
|
|
4689
|
+
]);
|
|
3915
4690
|
useEffect9(() => {
|
|
3916
4691
|
if (!atlas) return;
|
|
3917
4692
|
shader.uniforms.uAtlas.value = atlas.texture;
|
|
@@ -3945,7 +4720,7 @@ function FieldGroup({ group, shared }) {
|
|
|
3945
4720
|
for (const [name, value] of Object.entries(values)) {
|
|
3946
4721
|
const uniform = shader.uniforms[name];
|
|
3947
4722
|
if (!uniform) continue;
|
|
3948
|
-
if (uniform.value instanceof
|
|
4723
|
+
if (uniform.value instanceof THREE11.Vector2 && Array.isArray(value)) {
|
|
3949
4724
|
uniform.value.set(value[0], value[1]);
|
|
3950
4725
|
} else {
|
|
3951
4726
|
uniform.value = value;
|
|
@@ -3955,15 +4730,19 @@ function FieldGroup({ group, shared }) {
|
|
|
3955
4730
|
const layout = getLayout(shared.layoutId);
|
|
3956
4731
|
const morph = shared.morphRef.current;
|
|
3957
4732
|
const behaviorTransform = behavior?.transform && config.behavior ? behavior : null;
|
|
4733
|
+
const biasAttr = geometry.getAttribute("aBias");
|
|
4734
|
+
const biases = biasAttr.array;
|
|
4735
|
+
let biasChanged = false;
|
|
3958
4736
|
for (let j = 0; j < count; j++) {
|
|
3959
4737
|
const i = indices[j];
|
|
3960
|
-
let pose = layout.pose(i, shared.total, shared.layoutOptions, shared.phaseRef.current);
|
|
4738
|
+
let pose = layout.pose(i, shared.total, shared.layoutOptions, shared.phaseRef.current, shared.sheet);
|
|
3961
4739
|
if (morph.from && morph.t < 1) {
|
|
3962
4740
|
const prev = getLayout(morph.from.id).pose(
|
|
3963
4741
|
i,
|
|
3964
4742
|
shared.total,
|
|
3965
4743
|
morph.from.options,
|
|
3966
|
-
shared.phaseRef.current
|
|
4744
|
+
shared.phaseRef.current,
|
|
4745
|
+
shared.sheet
|
|
3967
4746
|
);
|
|
3968
4747
|
pose = lerpPose(prev, pose, easeInOut(morph.t));
|
|
3969
4748
|
}
|
|
@@ -3973,6 +4752,11 @@ function FieldGroup({ group, shared }) {
|
|
|
3973
4752
|
pose = lerpPose(entrancePose(shared.entranceType, i, pose), pose, easeOut(tIn));
|
|
3974
4753
|
}
|
|
3975
4754
|
}
|
|
4755
|
+
const bias = Math.min(1, Math.max(0, pose.bias ?? 1));
|
|
4756
|
+
if (biases[j] !== bias) {
|
|
4757
|
+
biases[j] = bias;
|
|
4758
|
+
biasChanged = true;
|
|
4759
|
+
}
|
|
3976
4760
|
scratchObj.position.set(...pose.position);
|
|
3977
4761
|
scratchObj.rotation.set(...pose.rotation);
|
|
3978
4762
|
scratchObj.scale.setScalar(pose.scale);
|
|
@@ -3997,6 +4781,7 @@ function FieldGroup({ group, shared }) {
|
|
|
3997
4781
|
mesh.setMatrixAt(j, scratchObj.matrix);
|
|
3998
4782
|
}
|
|
3999
4783
|
mesh.instanceMatrix.needsUpdate = true;
|
|
4784
|
+
if (biasChanged) biasAttr.needsUpdate = true;
|
|
4000
4785
|
});
|
|
4001
4786
|
return /* @__PURE__ */ jsx7(
|
|
4002
4787
|
"instancedMesh",
|
|
@@ -4009,13 +4794,13 @@ function FieldGroup({ group, shared }) {
|
|
|
4009
4794
|
children: /* @__PURE__ */ jsx7(
|
|
4010
4795
|
CustomShaderMaterial2,
|
|
4011
4796
|
{
|
|
4012
|
-
baseMaterial:
|
|
4797
|
+
baseMaterial: THREE11.MeshStandardMaterial,
|
|
4013
4798
|
vertexShader: shader.vertexShader,
|
|
4014
4799
|
fragmentShader: shader.fragmentShader,
|
|
4015
4800
|
uniforms: shader.uniforms,
|
|
4016
4801
|
roughness: stock.roughness,
|
|
4017
4802
|
metalness: 0,
|
|
4018
|
-
side:
|
|
4803
|
+
side: THREE11.DoubleSide
|
|
4019
4804
|
},
|
|
4020
4805
|
`${structureKey}:${count}`
|
|
4021
4806
|
)
|
|
@@ -4027,14 +4812,16 @@ function entrancePose(type, i, target) {
|
|
|
4027
4812
|
return {
|
|
4028
4813
|
position: [target.position[0], target.position[1] - 3.2, target.position[2] - 0.5],
|
|
4029
4814
|
rotation: [target.rotation[0] - 0.7, target.rotation[1], target.rotation[2] + 0.25],
|
|
4030
|
-
scale: target.scale * 0.85
|
|
4815
|
+
scale: target.scale * 0.85,
|
|
4816
|
+
bias: target.bias
|
|
4031
4817
|
};
|
|
4032
4818
|
}
|
|
4033
4819
|
const a = i * 2.399;
|
|
4034
4820
|
return {
|
|
4035
4821
|
position: [Math.cos(a) * 7, Math.sin(a * 1.3) * 4, Math.sin(a) * 6],
|
|
4036
4822
|
rotation: [Math.sin(a) * 2, a, Math.cos(a) * 2],
|
|
4037
|
-
scale: target.scale * 0.6
|
|
4823
|
+
scale: target.scale * 0.6,
|
|
4824
|
+
bias: target.bias
|
|
4038
4825
|
};
|
|
4039
4826
|
}
|
|
4040
4827
|
function lerpPose(a, b, t) {
|
|
@@ -4050,14 +4837,15 @@ function lerpPose(a, b, t) {
|
|
|
4050
4837
|
lerp(a.rotation[1], b.rotation[1]),
|
|
4051
4838
|
lerp(a.rotation[2], b.rotation[2])
|
|
4052
4839
|
],
|
|
4053
|
-
scale: lerp(a.scale, b.scale)
|
|
4840
|
+
scale: lerp(a.scale, b.scale),
|
|
4841
|
+
bias: lerp(a.bias ?? 1, b.bias ?? 1)
|
|
4054
4842
|
};
|
|
4055
4843
|
}
|
|
4056
4844
|
var easeOut = (t) => 1 - (1 - t) ** 3;
|
|
4057
4845
|
var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
|
4058
4846
|
|
|
4059
4847
|
// src/field/backingSheet.tsx
|
|
4060
|
-
import * as
|
|
4848
|
+
import * as THREE12 from "three";
|
|
4061
4849
|
import { useEffect as useEffect10, useMemo as useMemo8 } from "react";
|
|
4062
4850
|
|
|
4063
4851
|
// src/content/backing.ts
|
|
@@ -4139,7 +4927,7 @@ function BackingSheet({
|
|
|
4139
4927
|
c.height = Math.max(2, Math.round(height * scale));
|
|
4140
4928
|
return c;
|
|
4141
4929
|
}, [width, height]);
|
|
4142
|
-
const texture = useMemo8(() => canvas ? new
|
|
4930
|
+
const texture = useMemo8(() => canvas ? new THREE12.CanvasTexture(canvas) : null, [canvas]);
|
|
4143
4931
|
const removedKey = [...removed].sort((a, b) => a - b).join(",");
|
|
4144
4932
|
useEffect10(() => {
|
|
4145
4933
|
if (!canvas || !texture) return;
|
|
@@ -4154,7 +4942,7 @@ function BackingSheet({
|
|
|
4154
4942
|
}
|
|
4155
4943
|
|
|
4156
4944
|
// src/field/interactiveField.tsx
|
|
4157
|
-
import * as
|
|
4945
|
+
import * as THREE13 from "three";
|
|
4158
4946
|
import { gsap as gsap4 } from "gsap";
|
|
4159
4947
|
import { useFrame as useFrame4, useThree as useThree3 } from "@react-three/fiber";
|
|
4160
4948
|
import { useContext as useContext2, useEffect as useEffect11, useMemo as useMemo9, useRef as useRef5, useState as useState5 } from "react";
|
|
@@ -4179,7 +4967,6 @@ function InteractiveField(props) {
|
|
|
4179
4967
|
const patch = slotPatches[i];
|
|
4180
4968
|
return patch ? paperConfigSchema.parse(mergeConfig(config, patch)) : config;
|
|
4181
4969
|
}),
|
|
4182
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4183
4970
|
[
|
|
4184
4971
|
JSON.stringify(papers),
|
|
4185
4972
|
JSON.stringify(fallback ?? null),
|
|
@@ -4189,9 +4976,8 @@ function InteractiveField(props) {
|
|
|
4189
4976
|
]
|
|
4190
4977
|
);
|
|
4191
4978
|
const poses = useMemo9(
|
|
4192
|
-
() => slotConfigs.map((_, i) => layout.pose(i, total, layoutOptions, 0)),
|
|
4193
|
-
|
|
4194
|
-
[layoutId, JSON.stringify(layoutOptions), total]
|
|
4979
|
+
() => slotConfigs.map((_, i) => layout.pose(i, total, layoutOptions, 0, props.sheet)),
|
|
4980
|
+
[layoutId, JSON.stringify(layoutOptions), total, props.sheet.width, props.sheet.height]
|
|
4195
4981
|
);
|
|
4196
4982
|
const groupRefs = useRef5([]);
|
|
4197
4983
|
const handleRefs = useRef5([]);
|
|
@@ -4209,10 +4995,10 @@ function InteractiveField(props) {
|
|
|
4209
4995
|
}
|
|
4210
4996
|
props.onSlotStateChange?.(i, state);
|
|
4211
4997
|
};
|
|
4212
|
-
const raycaster = useMemo9(() => new
|
|
4213
|
-
const planeScratch = useMemo9(() => new
|
|
4214
|
-
const pointScratch = useMemo9(() => new
|
|
4215
|
-
const ndcScratch = useMemo9(() => new
|
|
4998
|
+
const raycaster = useMemo9(() => new THREE13.Raycaster(), []);
|
|
4999
|
+
const planeScratch = useMemo9(() => new THREE13.Plane(new THREE13.Vector3(0, 0, 1), 0), []);
|
|
5000
|
+
const pointScratch = useMemo9(() => new THREE13.Vector3(), []);
|
|
5001
|
+
const ndcScratch = useMemo9(() => new THREE13.Vector2(), []);
|
|
4216
5002
|
const planePoint = (clientX, clientY, planeZ) => {
|
|
4217
5003
|
const rect = gl.domElement.getBoundingClientRect();
|
|
4218
5004
|
ndcScratch.set(
|
|
@@ -4375,7 +5161,7 @@ function InteractiveField(props) {
|
|
|
4375
5161
|
if (carried.pointerId !== null && e.pointerId !== carried.pointerId) return;
|
|
4376
5162
|
const name = slotName(carried.slot);
|
|
4377
5163
|
const zone = zonesLive().find(
|
|
4378
|
-
(
|
|
5164
|
+
(z27) => zoneContains(z27, carried.x.value, carried.y.value) && zoneAccepts(z27, name)
|
|
4379
5165
|
);
|
|
4380
5166
|
if (zone) settleInto(carried, zone);
|
|
4381
5167
|
else returnHome(carried);
|
|
@@ -4416,12 +5202,12 @@ function InteractiveField(props) {
|
|
|
4416
5202
|
group.position.z = carried.homePose.position[2] + 0.15;
|
|
4417
5203
|
handle.set("drive", carryDrive(speed));
|
|
4418
5204
|
const lag = 0.25;
|
|
4419
|
-
group.rotation.y += (
|
|
4420
|
-
group.rotation.x += (
|
|
5205
|
+
group.rotation.y += (THREE13.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
|
|
5206
|
+
group.rotation.x += (THREE13.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
|
|
4421
5207
|
}
|
|
4422
5208
|
const name = slotName(carried.slot);
|
|
4423
5209
|
const zone = zonesLive().find(
|
|
4424
|
-
(
|
|
5210
|
+
(z27) => zoneContains(z27, group.position.x, group.position.y) && zoneAccepts(z27, name)
|
|
4425
5211
|
);
|
|
4426
5212
|
registry4.setHovered(zone?.id ?? null);
|
|
4427
5213
|
const targetScale = (zone ? 1.03 : 1) * carried.homePose.scale;
|
|
@@ -4437,7 +5223,7 @@ function InteractiveField(props) {
|
|
|
4437
5223
|
},
|
|
4438
5224
|
placeAtZone: (slot, zoneId) => {
|
|
4439
5225
|
const carried = carriedRef.current;
|
|
4440
|
-
const zone = zonesLive().find((
|
|
5226
|
+
const zone = zonesLive().find((z27) => z27.id === zoneId);
|
|
4441
5227
|
const group = groupRefs.current[slot];
|
|
4442
5228
|
if (!carried || carried.slot !== slot || !zone || !group) return;
|
|
4443
5229
|
group.position.x = zone.bounds.position[0];
|
|
@@ -4450,7 +5236,7 @@ function InteractiveField(props) {
|
|
|
4450
5236
|
const carried = carriedRef.current;
|
|
4451
5237
|
if (carried && carried.slot === slot && !carried.settling) returnHome(carried);
|
|
4452
5238
|
},
|
|
4453
|
-
zoneIds: () => zonesLive().map((
|
|
5239
|
+
zoneIds: () => zonesLive().map((z27) => z27.id),
|
|
4454
5240
|
slotState: (slot) => slotStates[slot] ?? "rest"
|
|
4455
5241
|
};
|
|
4456
5242
|
useEffect11(() => {
|
|
@@ -4568,25 +5354,88 @@ function FieldKeyboardMirror({
|
|
|
4568
5354
|
if (handled) e.preventDefault();
|
|
4569
5355
|
if (carry2 !== carrying) setCarrying(carry2);
|
|
4570
5356
|
};
|
|
4571
|
-
return /* @__PURE__ */
|
|
4572
|
-
"
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
"
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
5357
|
+
return /* @__PURE__ */ jsxs7("fieldset", { style: mirrorHidden, children: [
|
|
5358
|
+
/* @__PURE__ */ jsx10("legend", { children: "Interactive papers" }),
|
|
5359
|
+
papers.map((slot, i) => /* @__PURE__ */ jsxs7(
|
|
5360
|
+
"button",
|
|
5361
|
+
{
|
|
5362
|
+
type: "button",
|
|
5363
|
+
onKeyDown: onKeyDown(i),
|
|
5364
|
+
"aria-label": paperLabel(slot, i),
|
|
5365
|
+
"aria-pressed": carrying?.slot === i,
|
|
5366
|
+
children: [
|
|
5367
|
+
paperLabel(slot, i),
|
|
5368
|
+
carrying?.slot === i && /* @__PURE__ */ jsxs7("span", { "aria-live": "polite", children: [
|
|
5369
|
+
" ",
|
|
5370
|
+
"\u2014 carrying; zone ",
|
|
5371
|
+
controller.current?.zoneIds()[carrying.zoneIndex] ?? "none",
|
|
5372
|
+
"; Enter places, Escape returns"
|
|
5373
|
+
] })
|
|
5374
|
+
]
|
|
5375
|
+
},
|
|
5376
|
+
i
|
|
5377
|
+
))
|
|
5378
|
+
] });
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
// src/field/framing.ts
|
|
5382
|
+
var PHASE_SAMPLES = 8;
|
|
5383
|
+
function resolveLayoutOptions(layoutId, layout, propOptions, firstSheet) {
|
|
5384
|
+
const parsed = layout.optionsSchema.parse({
|
|
5385
|
+
...layout.defaults,
|
|
5386
|
+
...propOptions
|
|
5387
|
+
});
|
|
5388
|
+
if (layoutId !== "sheet") return parsed;
|
|
5389
|
+
return withSheetCellFromPaper(
|
|
5390
|
+
parsed,
|
|
5391
|
+
propOptions,
|
|
5392
|
+
firstSheet
|
|
5393
|
+
);
|
|
5394
|
+
}
|
|
5395
|
+
function fieldBounds(layout, n, options, sheet2) {
|
|
5396
|
+
const reach = Math.hypot(sheet2.width, sheet2.height) / 2;
|
|
5397
|
+
if (n <= 0) return { center: [0, 0, 0], half: [reach, reach, 0] };
|
|
5398
|
+
const min = [Infinity, Infinity, Infinity];
|
|
5399
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
5400
|
+
for (let s = 0; s < PHASE_SAMPLES; s++) {
|
|
5401
|
+
for (let i = 0; i < n; i++) {
|
|
5402
|
+
const pose = layout.pose(i, n, options, s / PHASE_SAMPLES, sheet2);
|
|
5403
|
+
const r = reach * Math.max(pose.scale, 0);
|
|
5404
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
5405
|
+
min[axis] = Math.min(min[axis], pose.position[axis] - r);
|
|
5406
|
+
max[axis] = Math.max(max[axis], pose.position[axis] + r);
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
return {
|
|
5411
|
+
center: [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2],
|
|
5412
|
+
half: [(max[0] - min[0]) / 2, (max[1] - min[1]) / 2, (max[2] - min[2]) / 2]
|
|
5413
|
+
};
|
|
5414
|
+
}
|
|
5415
|
+
var LIFT = 0.11;
|
|
5416
|
+
var DEG7 = Math.PI / 180;
|
|
5417
|
+
function fitCamera(layout, n, options, sheet2, fovDeg, aspect, margin = 1.06) {
|
|
5418
|
+
const { center } = fieldBounds(layout, n, options, sheet2);
|
|
5419
|
+
const reach = Math.hypot(sheet2.width, sheet2.height) / 2 * margin;
|
|
5420
|
+
const vTan = Math.tan(fovDeg * DEG7 / 2);
|
|
5421
|
+
const hTan = vTan * Math.max(aspect, 0.01);
|
|
5422
|
+
let distance = 0.1;
|
|
5423
|
+
for (let s = 0; s < PHASE_SAMPLES; s++) {
|
|
5424
|
+
for (let i = 0; i < Math.max(n, 0); i++) {
|
|
5425
|
+
const pose = layout.pose(i, n, options, s / PHASE_SAMPLES, sheet2);
|
|
5426
|
+
const r = reach * Math.max(pose.scale, 0);
|
|
5427
|
+
const depth = pose.position[2] - center[2];
|
|
5428
|
+
distance = Math.max(
|
|
5429
|
+
distance,
|
|
5430
|
+
(Math.abs(pose.position[0] - center[0]) + r) / hTan + depth,
|
|
5431
|
+
(Math.abs(pose.position[1] - center[1]) + r) / vTan + depth
|
|
5432
|
+
);
|
|
5433
|
+
}
|
|
5434
|
+
}
|
|
5435
|
+
return {
|
|
5436
|
+
position: [center[0], center[1] + distance * LIFT, center[2] + distance],
|
|
5437
|
+
target: center
|
|
5438
|
+
};
|
|
4590
5439
|
}
|
|
4591
5440
|
|
|
4592
5441
|
// src/PaperField.tsx
|
|
@@ -4600,25 +5449,18 @@ var PaperFieldMesh = forwardRef3(
|
|
|
4600
5449
|
);
|
|
4601
5450
|
const groups = useMemo10(
|
|
4602
5451
|
() => groupFieldPapers(papers, props.preset),
|
|
4603
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4604
5452
|
[JSON.stringify(papers), JSON.stringify(props.preset ?? null)]
|
|
4605
5453
|
);
|
|
4606
5454
|
const total = papers.length;
|
|
4607
5455
|
const layoutId = props.layout ?? "ring";
|
|
4608
5456
|
const layout = getLayout(layoutId);
|
|
4609
5457
|
const firstSheet = groups[0]?.config.sheet;
|
|
4610
|
-
const layoutOptions = useMemo10(
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
})
|
|
4615
|
-
|
|
4616
|
-
return withSheetCellFromPaper(
|
|
4617
|
-
parsed,
|
|
4618
|
-
props.layoutOptions,
|
|
4619
|
-
firstSheet
|
|
4620
|
-
);
|
|
4621
|
-
}, [layoutId, JSON.stringify(props.layoutOptions ?? {}), firstSheet?.width, firstSheet?.height]);
|
|
5458
|
+
const layoutOptions = useMemo10(
|
|
5459
|
+
// Sheet grids size their cells from the papers themselves — gutter is
|
|
5460
|
+
// then literally the spacing between stamps (explicit cell dims win).
|
|
5461
|
+
() => resolveLayoutOptions(layoutId, layout, props.layoutOptions, firstSheet),
|
|
5462
|
+
[layoutId, JSON.stringify(props.layoutOptions ?? {}), firstSheet?.width, firstSheet?.height]
|
|
5463
|
+
);
|
|
4622
5464
|
const phaseRef = useRef6(0);
|
|
4623
5465
|
const dragVelRef = useRef6(0);
|
|
4624
5466
|
const mountTimeRef = useRef6(-1);
|
|
@@ -4676,6 +5518,7 @@ var PaperFieldMesh = forwardRef3(
|
|
|
4676
5518
|
morphRef,
|
|
4677
5519
|
layoutId,
|
|
4678
5520
|
layoutOptions,
|
|
5521
|
+
sheet: firstSheet ?? DEFAULT_SHEET,
|
|
4679
5522
|
entranceType,
|
|
4680
5523
|
stagger: props.entrance?.stagger ?? 0.06,
|
|
4681
5524
|
entranceDuration: props.entrance?.duration ?? 0.9,
|
|
@@ -4694,6 +5537,7 @@ var PaperFieldMesh = forwardRef3(
|
|
|
4694
5537
|
layoutId,
|
|
4695
5538
|
layoutOptions,
|
|
4696
5539
|
sheetOptions,
|
|
5540
|
+
sheet: firstSheet ?? DEFAULT_SHEET,
|
|
4697
5541
|
reducedMotion: props.reducedMotion,
|
|
4698
5542
|
zones: props.zones,
|
|
4699
5543
|
onSlotStateChange: props.onSlotStateChange,
|
|
@@ -4704,10 +5548,47 @@ var PaperFieldMesh = forwardRef3(
|
|
|
4704
5548
|
}
|
|
4705
5549
|
return /* @__PURE__ */ jsxs8("group", { ref, children: [
|
|
4706
5550
|
sheetOptions?.backing && /* @__PURE__ */ jsx11(BackingSheet, { options: sheetOptions, count: total, removed: EMPTY_SET }),
|
|
4707
|
-
groups.map((group, gi) =>
|
|
5551
|
+
groups.map((group, gi) => (
|
|
5552
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: groups are derived from the slot list in order, so position is their only identity.
|
|
5553
|
+
/* @__PURE__ */ jsx11(FieldGroup, { group, shared }, `${gi}:${group.indices.length}`)
|
|
5554
|
+
))
|
|
4708
5555
|
] });
|
|
4709
5556
|
}
|
|
4710
5557
|
);
|
|
5558
|
+
function FitCamera(meshProps) {
|
|
5559
|
+
const camera = useThree4((s) => s.camera);
|
|
5560
|
+
const width = useThree4((s) => s.size.width);
|
|
5561
|
+
const height = useThree4((s) => s.size.height);
|
|
5562
|
+
const field = useMemo10(() => {
|
|
5563
|
+
const papers = effectiveFieldPapers(meshProps.papers, meshProps.images);
|
|
5564
|
+
const layoutId = meshProps.layout ?? "ring";
|
|
5565
|
+
const layout = getLayout(layoutId);
|
|
5566
|
+
const sheet2 = groupFieldPapers(papers, meshProps.preset)[0]?.config.sheet;
|
|
5567
|
+
const options = resolveLayoutOptions(layoutId, layout, meshProps.layoutOptions, sheet2);
|
|
5568
|
+
return { layout, n: papers.length, options, sheet: sheet2 ?? DEFAULT_SHEET };
|
|
5569
|
+
}, [
|
|
5570
|
+
JSON.stringify(meshProps.papers ?? null),
|
|
5571
|
+
JSON.stringify(meshProps.images ?? null),
|
|
5572
|
+
JSON.stringify(meshProps.preset ?? null),
|
|
5573
|
+
meshProps.layout,
|
|
5574
|
+
JSON.stringify(meshProps.layoutOptions ?? {})
|
|
5575
|
+
]);
|
|
5576
|
+
useEffect12(() => {
|
|
5577
|
+
if (!(camera instanceof THREE14.PerspectiveCamera)) return;
|
|
5578
|
+
const { position, target } = fitCamera(
|
|
5579
|
+
field.layout,
|
|
5580
|
+
field.n,
|
|
5581
|
+
field.options,
|
|
5582
|
+
field.sheet,
|
|
5583
|
+
camera.fov,
|
|
5584
|
+
width / Math.max(height, 1)
|
|
5585
|
+
);
|
|
5586
|
+
camera.position.set(...position);
|
|
5587
|
+
camera.lookAt(...target);
|
|
5588
|
+
camera.updateProjectionMatrix();
|
|
5589
|
+
}, [camera, width, height, field]);
|
|
5590
|
+
return null;
|
|
5591
|
+
}
|
|
4711
5592
|
var PaperField = forwardRef3(function PaperField2({ children, className, style, ...meshProps }, ref) {
|
|
4712
5593
|
const registry4 = useMemo10(() => new DropZoneRegistry(), []);
|
|
4713
5594
|
const a11yRef = useRef6(null);
|
|
@@ -4717,11 +5598,11 @@ var PaperField = forwardRef3(function PaperField2({ children, className, style,
|
|
|
4717
5598
|
);
|
|
4718
5599
|
const interactive = useMemo10(
|
|
4719
5600
|
() => fieldIsInteractive(papers, meshProps.preset, meshProps.interactive),
|
|
4720
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4721
5601
|
[JSON.stringify(papers), JSON.stringify(meshProps.preset ?? null), meshProps.interactive]
|
|
4722
5602
|
);
|
|
4723
5603
|
return /* @__PURE__ */ jsx11("div", { className, style: { width: "100%", height: "100%", ...style }, children: /* @__PURE__ */ jsxs8(DropZoneContext.Provider, { value: registry4, children: [
|
|
4724
5604
|
/* @__PURE__ */ jsxs8(Canvas2, { shadows: true, camera: { position: [0, 0.6, 5.2], fov: 45 }, dpr: [1, 2], children: [
|
|
5605
|
+
/* @__PURE__ */ jsx11(FitCamera, { ...meshProps }),
|
|
4725
5606
|
/* @__PURE__ */ jsx11("ambientLight", { intensity: 0.7 }),
|
|
4726
5607
|
/* @__PURE__ */ jsx11(
|
|
4727
5608
|
"directionalLight",
|
|
@@ -4740,202 +5621,762 @@ var PaperField = forwardRef3(function PaperField2({ children, className, style,
|
|
|
4740
5621
|
] }) });
|
|
4741
5622
|
});
|
|
4742
5623
|
|
|
4743
|
-
// src/
|
|
4744
|
-
import * as
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
{
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
}
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
5624
|
+
// src/stage/PaperStage.tsx
|
|
5625
|
+
import * as THREE17 from "three";
|
|
5626
|
+
import { Canvas as Canvas3, useFrame as useFrame7, useThree as useThree5 } from "@react-three/fiber";
|
|
5627
|
+
import { useCallback, useEffect as useEffect15, useMemo as useMemo13, useRef as useRef8, useState as useState7 } from "react";
|
|
5628
|
+
import { z as z26 } from "zod";
|
|
5629
|
+
|
|
5630
|
+
// src/stage/camera.ts
|
|
5631
|
+
import { z as z23 } from "zod";
|
|
5632
|
+
var shotNames = ["follow", "lead", "low", "wide"];
|
|
5633
|
+
var shotSchema = z23.object({
|
|
5634
|
+
shot: z23.enum(shotNames).default("follow"),
|
|
5635
|
+
/**
|
|
5636
|
+
* How far the camera stands off the figure ALONG the walk, world units.
|
|
5637
|
+
* `wide` reads it as how far back it stands; how far it steps aside is
|
|
5638
|
+
* derived from the paper, since that is what it has to clear.
|
|
5639
|
+
*/
|
|
5640
|
+
distance: z23.number().min(0.2).max(40).default(4.5),
|
|
5641
|
+
/** Multiplier on the shot's natural camera height. 1 is as designed. */
|
|
5642
|
+
height: z23.number().min(0).max(6).default(1),
|
|
5643
|
+
/** How far up the walk the camera looks past the figure, world units. */
|
|
5644
|
+
lookAhead: z23.number().min(0).max(40).default(7),
|
|
5645
|
+
/** Sideways step off the walk line, world units. Positive is the walker's left. */
|
|
5646
|
+
offset: z23.number().min(-20).max(20).default(0)
|
|
5647
|
+
});
|
|
5648
|
+
var EYE = {
|
|
5649
|
+
follow: 0.95,
|
|
5650
|
+
lead: 0.95,
|
|
5651
|
+
// Down near the floor, where the banners tower — the worm's-eye of the
|
|
5652
|
+
// reference frames, and the cheapest way to make paper read as architecture.
|
|
5653
|
+
low: 0.12,
|
|
5654
|
+
wide: 1.1
|
|
5655
|
+
};
|
|
5656
|
+
var AIM = {
|
|
5657
|
+
// Chest height on the figure, a third of the way up the paper — enough
|
|
5658
|
+
// tilt that a printed banner reads, not so much that the floor is lost.
|
|
5659
|
+
follow: { figure: 0.62, paper: 0.3 },
|
|
5660
|
+
// Framing the figure itself, so the paper only lifts the aim a little.
|
|
5661
|
+
lead: { figure: 0.62, paper: 0.1 },
|
|
5662
|
+
// Up the banners. The figure is incidental to this shot.
|
|
5663
|
+
low: { figure: 0, paper: 0.62 },
|
|
5664
|
+
wide: { figure: 0.62, paper: 0.2 }
|
|
5665
|
+
};
|
|
5666
|
+
var WIDE_STANDOFF = 1.5;
|
|
5667
|
+
var DEFAULT_PAPER_RATIO = 4.9;
|
|
5668
|
+
function resolveScale(scale) {
|
|
5669
|
+
if (typeof scale === "number") return { figure: scale, paper: scale * DEFAULT_PAPER_RATIO };
|
|
5670
|
+
return scale;
|
|
5671
|
+
}
|
|
5672
|
+
function walkPoint(path, distance) {
|
|
5673
|
+
if (path.length === 0) return path.pointAt(0);
|
|
5674
|
+
if (path.closed) return path.pointAt(distance / path.length);
|
|
5675
|
+
if (distance < 0) {
|
|
5676
|
+
const [x, z27] = path.pointAt(0);
|
|
5677
|
+
const [tx, tz] = path.tangentAt(0);
|
|
5678
|
+
return [x + tx * distance, z27 + tz * distance];
|
|
5679
|
+
}
|
|
5680
|
+
if (distance > path.length) {
|
|
5681
|
+
const over = distance - path.length;
|
|
5682
|
+
const [x, z27] = path.pointAt(1);
|
|
5683
|
+
const [tx, tz] = path.tangentAt(1);
|
|
5684
|
+
return [x + tx * over, z27 + tz * over];
|
|
5685
|
+
}
|
|
5686
|
+
return path.pointAt(distance / path.length);
|
|
5687
|
+
}
|
|
5688
|
+
function walkNormal(path, distance) {
|
|
5689
|
+
if (path.length === 0) return path.normalAt(0);
|
|
5690
|
+
if (path.closed) return path.normalAt(distance / path.length);
|
|
5691
|
+
return path.normalAt(Math.min(Math.max(distance, 0), path.length) / path.length);
|
|
5692
|
+
}
|
|
5693
|
+
function stageCamera(path, walked, scale, options) {
|
|
5694
|
+
const { figure, paper } = resolveScale(scale);
|
|
5695
|
+
const eye = figure * EYE[options.shot] * options.height;
|
|
5696
|
+
const aim = figure * AIM[options.shot].figure + paper * AIM[options.shot].paper;
|
|
5697
|
+
let station;
|
|
5698
|
+
let mark;
|
|
5699
|
+
if (options.shot === "lead") {
|
|
5700
|
+
station = walked + options.distance;
|
|
5701
|
+
mark = walked;
|
|
5702
|
+
} else if (options.shot === "wide") {
|
|
5703
|
+
station = walked - options.distance;
|
|
5704
|
+
mark = walked;
|
|
5705
|
+
} else {
|
|
5706
|
+
station = walked - options.distance;
|
|
5707
|
+
mark = walked + options.lookAhead;
|
|
5708
|
+
}
|
|
5709
|
+
const [sx, sz] = walkPoint(path, station);
|
|
5710
|
+
const [mx, mz] = walkPoint(path, mark);
|
|
5711
|
+
const [nx, nz] = walkNormal(path, station);
|
|
5712
|
+
const step = options.offset + (options.shot === "wide" ? paper * WIDE_STANDOFF : 0);
|
|
5713
|
+
return {
|
|
5714
|
+
position: [sx + nx * step, eye, sz + nz * step],
|
|
5715
|
+
target: [mx, aim, mz]
|
|
5716
|
+
};
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5719
|
+
// src/stage/Figure.tsx
|
|
5720
|
+
import * as THREE15 from "three";
|
|
5721
|
+
import { useEffect as useEffect13, useMemo as useMemo11, useRef as useRef7 } from "react";
|
|
5722
|
+
import { useFrame as useFrame6 } from "@react-three/fiber";
|
|
5723
|
+
|
|
5724
|
+
// src/stage/gait.ts
|
|
5725
|
+
import { z as z24 } from "zod";
|
|
5726
|
+
var figureSchema = z24.object({
|
|
5727
|
+
/** Standing height in world units — the scale reference the whole stage is read against. */
|
|
5728
|
+
height: z24.number().min(0.5).max(4).default(1.75),
|
|
5729
|
+
/** World units per second along the walk. A relaxed indoor pace is ~1.2. */
|
|
5730
|
+
speed: z24.number().min(0).max(4).default(1.2),
|
|
5731
|
+
/** Stride length as a fraction of height — how far one step carries. */
|
|
5732
|
+
stride: z24.number().min(0.1).max(1).default(0.42),
|
|
5733
|
+
/** Arm swing, 0..1. Drop it toward 0 for hands-in-pockets stillness. */
|
|
5734
|
+
swing: z24.number().min(0).max(1).default(1),
|
|
5735
|
+
/** Silhouette color. Near-black by default: it should read as an absence, not an object. */
|
|
5736
|
+
color: z24.string().default("#0a0a0c")
|
|
5737
|
+
});
|
|
5738
|
+
var PROPORTIONS = {
|
|
5739
|
+
hip: 0.53,
|
|
5740
|
+
shoulder: 0.82,
|
|
5741
|
+
headRadius: 0.045,
|
|
5742
|
+
headCenter: 0.935,
|
|
5743
|
+
thigh: 0.245,
|
|
5744
|
+
shin: 0.235,
|
|
5745
|
+
upperArm: 0.185,
|
|
5746
|
+
foreArm: 0.165,
|
|
5747
|
+
torsoWidth: 0.19,
|
|
5748
|
+
hipWidth: 0.095,
|
|
5749
|
+
limbRadius: 0.028
|
|
5750
|
+
};
|
|
5751
|
+
var THIGH_SWING = 0.42;
|
|
5752
|
+
var ARM_SWING = 0.5;
|
|
5753
|
+
var KNEE_FLEX = 1.1;
|
|
5754
|
+
var BOB = 0.016;
|
|
5755
|
+
var LEAN = 0.045;
|
|
5756
|
+
var TAU4 = Math.PI * 2;
|
|
5757
|
+
function cycleLength(o) {
|
|
5758
|
+
return o.stride * o.height * 2;
|
|
5759
|
+
}
|
|
5760
|
+
function figureGait(distance, o) {
|
|
5761
|
+
const cycle = cycleLength(o);
|
|
5762
|
+
const phase = cycle > 0 ? (distance / cycle % 1 + 1) % 1 : 0;
|
|
5763
|
+
const w = phase * TAU4;
|
|
5764
|
+
const leftThigh = THIGH_SWING * Math.sin(w);
|
|
5765
|
+
const rightThigh = THIGH_SWING * Math.sin(w + Math.PI);
|
|
5766
|
+
const flex = (at) => -KNEE_FLEX * Math.max(0, Math.cos(w - at)) ** 1.5;
|
|
5767
|
+
const leftKnee = flex(7 * Math.PI / 4);
|
|
5768
|
+
const rightKnee = flex(3 * Math.PI / 4);
|
|
5769
|
+
const leftArm = -ARM_SWING * o.swing * Math.sin(w);
|
|
5770
|
+
const rightArm = -ARM_SWING * o.swing * Math.sin(w + Math.PI);
|
|
5771
|
+
const bob = -BOB * o.height * (1 - Math.abs(Math.cos(w)));
|
|
5772
|
+
return {
|
|
5773
|
+
phase,
|
|
5774
|
+
bob,
|
|
5775
|
+
lean: LEAN * Math.min(o.speed / 1.2, 1),
|
|
5776
|
+
leftThigh,
|
|
5777
|
+
rightThigh,
|
|
5778
|
+
leftKnee,
|
|
5779
|
+
rightKnee,
|
|
5780
|
+
leftArm,
|
|
5781
|
+
rightArm
|
|
5782
|
+
};
|
|
5783
|
+
}
|
|
5784
|
+
function placeFigure(path, distance, o) {
|
|
5785
|
+
const raw = path.length > 0 ? distance / path.length : 0;
|
|
5786
|
+
const s = path.closed ? (raw % 1 + 1) % 1 : Math.min(Math.max(raw, 0), 1);
|
|
5787
|
+
const [x, z27] = path.pointAt(s);
|
|
5788
|
+
const [tx, tz] = path.tangentAt(s);
|
|
5789
|
+
const travelled = path.closed ? distance : Math.min(distance, path.length);
|
|
5790
|
+
return {
|
|
5791
|
+
position: [x, 0, z27],
|
|
5792
|
+
yaw: Math.atan2(tx, tz),
|
|
5793
|
+
pose: figureGait(travelled, o),
|
|
5794
|
+
s
|
|
5795
|
+
};
|
|
5796
|
+
}
|
|
5797
|
+
|
|
5798
|
+
// src/stage/Figure.tsx
|
|
5799
|
+
import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
5800
|
+
function Segment({ length, radius, material }) {
|
|
5801
|
+
const shaft = Math.max(length - radius * 2, 1e-3);
|
|
5802
|
+
return /* @__PURE__ */ jsx12("mesh", { position: [0, -length / 2, 0], material, castShadow: true, children: /* @__PURE__ */ jsx12("capsuleGeometry", { args: [radius, shaft, 4, 10] }) });
|
|
5803
|
+
}
|
|
5804
|
+
function Figure({ path, figure, distance, frozen }) {
|
|
5805
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
5806
|
+
const still = frozen ?? reducedMotion;
|
|
5807
|
+
const options = useMemo11(() => figureSchema.parse(figure ?? {}), [figure]);
|
|
5808
|
+
const walk = useMemo11(() => getWalkPath(walkPathSchema.parse(path ?? {})), [path]);
|
|
5809
|
+
const root = useRef7(null);
|
|
5810
|
+
const hips = useRef7(null);
|
|
5811
|
+
const legL = useRef7(null);
|
|
5812
|
+
const legR = useRef7(null);
|
|
5813
|
+
const kneeL = useRef7(null);
|
|
5814
|
+
const kneeR = useRef7(null);
|
|
5815
|
+
const armL = useRef7(null);
|
|
5816
|
+
const armR = useRef7(null);
|
|
5817
|
+
const material = useMemo11(
|
|
5818
|
+
() => new THREE15.MeshBasicMaterial({ color: options.color, toneMapped: false }),
|
|
5819
|
+
[options.color]
|
|
5820
|
+
);
|
|
5821
|
+
useEffect13(() => () => material.dispose(), [material]);
|
|
5822
|
+
const h = options.height;
|
|
5823
|
+
const p = PROPORTIONS;
|
|
5824
|
+
const torso = (p.shoulder - p.hip) * h;
|
|
5825
|
+
useFrame6((state) => {
|
|
5826
|
+
const walked = distance ?? (still ? 0 : state.clock.elapsedTime * options.speed);
|
|
5827
|
+
const { position, yaw, pose } = placeFigure(walk, walked, options);
|
|
5828
|
+
root.current?.position.set(position[0], position[1], position[2]);
|
|
5829
|
+
if (root.current) root.current.rotation.y = yaw;
|
|
5830
|
+
if (hips.current) {
|
|
5831
|
+
hips.current.position.y = p.hip * h + (still ? 0 : pose.bob);
|
|
5832
|
+
hips.current.rotation.x = pose.lean;
|
|
5833
|
+
}
|
|
5834
|
+
if (legL.current) legL.current.rotation.x = still ? 0 : -pose.leftThigh;
|
|
5835
|
+
if (legR.current) legR.current.rotation.x = still ? 0 : -pose.rightThigh;
|
|
5836
|
+
if (kneeL.current) kneeL.current.rotation.x = still ? 0 : -pose.leftKnee;
|
|
5837
|
+
if (kneeR.current) kneeR.current.rotation.x = still ? 0 : -pose.rightKnee;
|
|
5838
|
+
if (armL.current) armL.current.rotation.x = still ? 0 : -pose.leftArm;
|
|
5839
|
+
if (armR.current) armR.current.rotation.x = still ? 0 : -pose.rightArm;
|
|
5840
|
+
});
|
|
5841
|
+
return /* @__PURE__ */ jsx12("group", { ref: root, children: /* @__PURE__ */ jsxs9("group", { ref: hips, children: [
|
|
5842
|
+
/* @__PURE__ */ jsx12("mesh", { position: [0, torso / 2, 0], material, castShadow: true, children: /* @__PURE__ */ jsx12("capsuleGeometry", { args: [p.torsoWidth * h / 2, torso * 0.72, 4, 12] }) }),
|
|
5843
|
+
/* @__PURE__ */ jsx12("mesh", { position: [0, (p.headCenter - p.hip) * h, 0], material, castShadow: true, children: /* @__PURE__ */ jsx12("sphereGeometry", { args: [p.headRadius * h, 14, 12] }) }),
|
|
5844
|
+
[-1, 1].map((side) => {
|
|
5845
|
+
const leg = side < 0 ? legL : legR;
|
|
5846
|
+
const knee = side < 0 ? kneeL : kneeR;
|
|
5847
|
+
return /* @__PURE__ */ jsxs9("group", { ref: leg, position: [side * p.hipWidth * h / 2, 0, 0], children: [
|
|
5848
|
+
/* @__PURE__ */ jsx12(Segment, { length: p.thigh * h, radius: p.limbRadius * h, material }),
|
|
5849
|
+
/* @__PURE__ */ jsx12("group", { ref: knee, position: [0, -p.thigh * h, 0], children: /* @__PURE__ */ jsx12(Segment, { length: p.shin * h, radius: p.limbRadius * h * 0.9, material }) })
|
|
5850
|
+
] }, `leg${side}`);
|
|
5851
|
+
}),
|
|
5852
|
+
[-1, 1].map((side) => /* @__PURE__ */ jsx12(
|
|
5853
|
+
"group",
|
|
4799
5854
|
{
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
5855
|
+
ref: side < 0 ? armL : armR,
|
|
5856
|
+
position: [side * p.torsoWidth * h / 2, torso, 0],
|
|
5857
|
+
children: /* @__PURE__ */ jsx12(
|
|
5858
|
+
Segment,
|
|
5859
|
+
{
|
|
5860
|
+
length: (p.upperArm + p.foreArm) * h,
|
|
5861
|
+
radius: p.limbRadius * h * 0.8,
|
|
5862
|
+
material
|
|
5863
|
+
}
|
|
5864
|
+
)
|
|
5865
|
+
},
|
|
5866
|
+
`arm${side}`
|
|
5867
|
+
))
|
|
5868
|
+
] }) });
|
|
5869
|
+
}
|
|
5870
|
+
|
|
5871
|
+
// src/stage/Surround.tsx
|
|
5872
|
+
import * as THREE16 from "three";
|
|
5873
|
+
import { useEffect as useEffect14, useMemo as useMemo12 } from "react";
|
|
5874
|
+
import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
5875
|
+
function makeSkyTexture(horizon, zenith) {
|
|
5876
|
+
const canvas = document.createElement("canvas");
|
|
5877
|
+
canvas.width = 4;
|
|
5878
|
+
canvas.height = 256;
|
|
5879
|
+
const ctx = canvas.getContext("2d");
|
|
5880
|
+
const grade = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
|
5881
|
+
grade.addColorStop(0, zenith);
|
|
5882
|
+
grade.addColorStop(0.35, zenith);
|
|
5883
|
+
grade.addColorStop(0.92, horizon);
|
|
5884
|
+
grade.addColorStop(1, horizon);
|
|
5885
|
+
ctx.fillStyle = grade;
|
|
5886
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
5887
|
+
const texture = new THREE16.CanvasTexture(canvas);
|
|
5888
|
+
texture.colorSpace = THREE16.SRGBColorSpace;
|
|
5889
|
+
return texture;
|
|
5890
|
+
}
|
|
5891
|
+
function makeGlowTexture(color) {
|
|
5892
|
+
const size = 256;
|
|
5893
|
+
const canvas = document.createElement("canvas");
|
|
5894
|
+
canvas.width = canvas.height = size;
|
|
5895
|
+
const ctx = canvas.getContext("2d");
|
|
5896
|
+
const glow = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
5897
|
+
glow.addColorStop(0, color);
|
|
5898
|
+
glow.addColorStop(0.55, color);
|
|
5899
|
+
glow.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
5900
|
+
ctx.fillStyle = glow;
|
|
5901
|
+
ctx.fillRect(0, 0, size, size);
|
|
5902
|
+
const texture = new THREE16.CanvasTexture(canvas);
|
|
5903
|
+
texture.colorSpace = THREE16.SRGBColorSpace;
|
|
5904
|
+
return texture;
|
|
5905
|
+
}
|
|
5906
|
+
function Source({
|
|
5907
|
+
size,
|
|
5908
|
+
position,
|
|
5909
|
+
yaw,
|
|
5910
|
+
color
|
|
5911
|
+
}) {
|
|
5912
|
+
const texture = useMemo12(() => makeGlowTexture(color), [color]);
|
|
5913
|
+
useEffect14(() => () => texture.dispose(), [texture]);
|
|
5914
|
+
return /* @__PURE__ */ jsxs10("mesh", { position, rotation: [0, yaw, 0], children: [
|
|
5915
|
+
/* @__PURE__ */ jsx13("planeGeometry", { args: [size * 2.4, size * 1.8] }),
|
|
5916
|
+
/* @__PURE__ */ jsx13(
|
|
5917
|
+
"meshBasicMaterial",
|
|
4810
5918
|
{
|
|
4811
|
-
|
|
4812
|
-
|
|
5919
|
+
map: texture,
|
|
5920
|
+
transparent: true,
|
|
5921
|
+
depthWrite: false,
|
|
5922
|
+
toneMapped: false,
|
|
5923
|
+
fog: false
|
|
4813
5924
|
}
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
5925
|
+
)
|
|
5926
|
+
] });
|
|
5927
|
+
}
|
|
5928
|
+
function Surround({ radius, horizon, zenith }) {
|
|
5929
|
+
const texture = useMemo12(() => makeSkyTexture(horizon, zenith), [horizon, zenith]);
|
|
5930
|
+
useEffect14(() => () => texture.dispose(), [texture]);
|
|
5931
|
+
return /* @__PURE__ */ jsxs10("mesh", { children: [
|
|
5932
|
+
/* @__PURE__ */ jsx13("sphereGeometry", { args: [radius, 32, 24] }),
|
|
5933
|
+
/* @__PURE__ */ jsx13("meshBasicMaterial", { map: texture, side: THREE16.BackSide, fog: false })
|
|
5934
|
+
] });
|
|
5935
|
+
}
|
|
5936
|
+
|
|
5937
|
+
// src/stage/schema.ts
|
|
5938
|
+
import { z as z25 } from "zod";
|
|
5939
|
+
var stageSourceSchema = z25.object({
|
|
5940
|
+
/** The bright void the walk resolves toward. Without it the vanishing point is a hole. */
|
|
5941
|
+
enabled: z25.boolean().default(true),
|
|
5942
|
+
color: z25.string().default("#fff4e2"),
|
|
5943
|
+
/** How far past the end of the walk it stands, world units. */
|
|
5944
|
+
beyond: z25.number().min(0).max(80).default(10),
|
|
5945
|
+
/**
|
|
5946
|
+
* A cyclorama around the whole stage, graded from the source colour at the
|
|
5947
|
+
* horizon to near-dark overhead. The source plane only faces down the walk,
|
|
5948
|
+
* so without this every shot that isn't axial — `wide` especially — looks
|
|
5949
|
+
* out at a black void where the room should be.
|
|
5950
|
+
*/
|
|
5951
|
+
surround: z25.boolean().default(true),
|
|
5952
|
+
/** Colour overhead. The horizon takes the source's own colour. */
|
|
5953
|
+
zenith: z25.string().default("#241c17"),
|
|
5954
|
+
/** Size, as a multiple of the PAPER height — it only has to out-fill the frame. */
|
|
5955
|
+
spread: z25.number().min(1).max(60).default(5)
|
|
5956
|
+
});
|
|
5957
|
+
var stageGroundSchema = z25.object({
|
|
5958
|
+
/** The floor. Without something to catch the shadows there is no ground and no scale. */
|
|
5959
|
+
enabled: z25.boolean().default(true),
|
|
5960
|
+
color: z25.string().default("#0e0b09")
|
|
5961
|
+
});
|
|
5962
|
+
var stageSchema = z25.object({
|
|
5963
|
+
path: walkPathSchema.default({}),
|
|
5964
|
+
shot: shotSchema.default({}),
|
|
5965
|
+
figure: figureSchema.default({}),
|
|
5966
|
+
/** Stage mode is built for `nave`; the others are all front-lit. */
|
|
5967
|
+
lighting: z25.enum(lightingNames).default("nave"),
|
|
5968
|
+
showFigure: z25.boolean().default(true),
|
|
5969
|
+
source: stageSourceSchema.default({}),
|
|
5970
|
+
ground: stageGroundSchema.default({})
|
|
5971
|
+
});
|
|
5972
|
+
|
|
5973
|
+
// src/stage/quality.ts
|
|
5974
|
+
var qualityNames = ["auto", "low", "medium", "high"];
|
|
5975
|
+
var qualityTiers = {
|
|
5976
|
+
/** Anything with a GPU. */
|
|
5977
|
+
high: { dpr: 2, shadowMapSize: 2048, segments: 72, surround: true, contactShadow: true },
|
|
5978
|
+
/** The default worth aiming at: an integrated laptop GPU from the last few years. */
|
|
5979
|
+
medium: { dpr: 1.5, shadowMapSize: 1024, segments: 48, surround: true, contactShadow: false },
|
|
5980
|
+
/**
|
|
5981
|
+
* Old integrated graphics, a throttled phone, a software rasterizer. The
|
|
5982
|
+
* scene still READS — banners, figure, backlight, walk — it just stops
|
|
5983
|
+
* paying for the parts nobody would miss at this framerate.
|
|
5984
|
+
*/
|
|
5985
|
+
low: { dpr: 1, shadowMapSize: 0, segments: 28, surround: true, contactShadow: false }
|
|
5986
|
+
};
|
|
5987
|
+
var INITIAL_TIER = "medium";
|
|
5988
|
+
var FIRST_WINDOW = 20;
|
|
5989
|
+
var STEADY_WINDOW = 60;
|
|
5990
|
+
var SETTLE_FRAMES = 45;
|
|
5991
|
+
var TIER_ORDER = ["low", "medium", "high"];
|
|
5992
|
+
function qualityFor(name) {
|
|
5993
|
+
return qualityTiers[name === "auto" ? INITIAL_TIER : name];
|
|
5994
|
+
}
|
|
5995
|
+
function tierUp(tier) {
|
|
5996
|
+
return TIER_ORDER[Math.min(TIER_ORDER.indexOf(tier) + 1, TIER_ORDER.length - 1)];
|
|
5997
|
+
}
|
|
5998
|
+
function tierDown(tier) {
|
|
5999
|
+
return TIER_ORDER[Math.max(TIER_ORDER.indexOf(tier) - 1, 0)];
|
|
6000
|
+
}
|
|
6001
|
+
|
|
6002
|
+
// src/stage/PaperStage.tsx
|
|
6003
|
+
import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
6004
|
+
var BANNER = {
|
|
6005
|
+
sheet: { width: 1.5, height: 8.5, segments: "auto" },
|
|
6006
|
+
stock: "vellum",
|
|
6007
|
+
surface: { grain: 0.22 },
|
|
6008
|
+
deformers: [{ type: "drape", options: { amplitude: 0.16, folds: 3, falloff: 1.7, gather: 0.28 } }]
|
|
6009
|
+
};
|
|
6010
|
+
function splitAcrossBanners(text, banners) {
|
|
6011
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
6012
|
+
if (words.length === 0 || banners <= 0) return [];
|
|
6013
|
+
const per = Math.ceil(words.length / banners);
|
|
6014
|
+
const out = [];
|
|
6015
|
+
for (let i = 0; i < words.length; i += per) out.push(words.slice(i, i + per).join("\n"));
|
|
6016
|
+
return out;
|
|
6017
|
+
}
|
|
6018
|
+
function bannerTextSize(lines) {
|
|
6019
|
+
return Math.round(Math.min(150, Math.max(26, 720 / Math.max(lines, 1))));
|
|
6020
|
+
}
|
|
6021
|
+
function ShotRig({
|
|
6022
|
+
stage,
|
|
6023
|
+
paperHeight,
|
|
6024
|
+
progress,
|
|
6025
|
+
still
|
|
6026
|
+
}) {
|
|
6027
|
+
const camera = useThree5((s) => s.camera);
|
|
6028
|
+
const path = useMemo13(() => getWalkPath(stage.path), [stage.path]);
|
|
6029
|
+
const scale = useMemo13(
|
|
6030
|
+
() => ({ figure: stage.figure.height, paper: paperHeight }),
|
|
6031
|
+
[stage.figure.height, paperHeight]
|
|
6032
|
+
);
|
|
6033
|
+
useFrame7((state) => {
|
|
6034
|
+
const walked = stageWalked(path.length, stage, progress, still ? 0 : state.clock.elapsedTime);
|
|
6035
|
+
const { position, target } = stageCamera(path, walked, scale, stage.shot);
|
|
6036
|
+
camera.position.set(position[0], position[1], position[2]);
|
|
6037
|
+
camera.lookAt(target[0], target[1], target[2]);
|
|
6038
|
+
});
|
|
6039
|
+
return null;
|
|
6040
|
+
}
|
|
6041
|
+
function stageWalked(length, stage, progress, elapsed) {
|
|
6042
|
+
if (progress !== void 0) return progress * length;
|
|
6043
|
+
return elapsed * stage.figure.speed;
|
|
6044
|
+
}
|
|
6045
|
+
var FLOOR_FPS = 26;
|
|
6046
|
+
var CEILING_FPS = 55;
|
|
6047
|
+
function QualityWatch({ tier, onChange }) {
|
|
6048
|
+
const samples = useRef8([]);
|
|
6049
|
+
const settle = useRef8(SETTLE_FRAMES);
|
|
6050
|
+
const window2 = useRef8(FIRST_WINDOW);
|
|
6051
|
+
const settled = useCallback((next) => {
|
|
6052
|
+
samples.current = [];
|
|
6053
|
+
settle.current = SETTLE_FRAMES;
|
|
6054
|
+
window2.current = STEADY_WINDOW;
|
|
6055
|
+
return next;
|
|
6056
|
+
}, []);
|
|
6057
|
+
useFrame7((_, delta) => {
|
|
6058
|
+
if (settle.current > 0) {
|
|
6059
|
+
settle.current -= 1;
|
|
6060
|
+
return;
|
|
6061
|
+
}
|
|
6062
|
+
if (delta > 0.5) return;
|
|
6063
|
+
samples.current.push(delta);
|
|
6064
|
+
if (samples.current.length < window2.current) return;
|
|
6065
|
+
const sorted = [...samples.current].sort((a, b) => a - b);
|
|
6066
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
6067
|
+
const fps = 1 / median;
|
|
6068
|
+
samples.current = [];
|
|
6069
|
+
window2.current = STEADY_WINDOW;
|
|
6070
|
+
if (fps < FLOOR_FPS) {
|
|
6071
|
+
const next = tierDown(tier);
|
|
6072
|
+
if (next !== tier) onChange(settled(next));
|
|
6073
|
+
} else if (fps > CEILING_FPS) {
|
|
6074
|
+
const next = tierUp(tier);
|
|
6075
|
+
if (next !== tier) onChange(settled(next));
|
|
6076
|
+
}
|
|
6077
|
+
});
|
|
6078
|
+
return null;
|
|
6079
|
+
}
|
|
6080
|
+
function PaperStageScene({
|
|
6081
|
+
stage: stageInput,
|
|
6082
|
+
quality = "auto",
|
|
6083
|
+
onQualityChange,
|
|
6084
|
+
layout = "colonnade",
|
|
6085
|
+
layoutOptions,
|
|
6086
|
+
papers,
|
|
6087
|
+
images,
|
|
6088
|
+
text,
|
|
6089
|
+
preset,
|
|
6090
|
+
count = 22,
|
|
6091
|
+
progress,
|
|
6092
|
+
reducedMotion
|
|
6093
|
+
}) {
|
|
6094
|
+
const still = usePrefersReducedMotion(reducedMotion);
|
|
6095
|
+
const [tier, setTier] = useState7(quality === "auto" ? INITIAL_TIER : quality);
|
|
6096
|
+
useEffect15(() => {
|
|
6097
|
+
if (quality !== "auto") setTier(quality);
|
|
6098
|
+
}, [quality]);
|
|
6099
|
+
useEffect15(() => {
|
|
6100
|
+
onQualityChange?.(tier);
|
|
6101
|
+
}, [tier, onQualityChange]);
|
|
6102
|
+
const settings = quality === "auto" ? qualityTiers[tier] : qualityFor(quality);
|
|
6103
|
+
const stage = useMemo13(() => stageSchema.parse(stageInput ?? {}), [stageInput]);
|
|
6104
|
+
const path = useMemo13(() => getWalkPath(stage.path), [stage.path]);
|
|
6105
|
+
const paperHeight = useMemo13(() => resolveConfig({ preset: preset ?? BANNER }).sheet.height, [preset]);
|
|
6106
|
+
const paper = useMemo13(() => {
|
|
6107
|
+
const base = preset ?? BANNER;
|
|
6108
|
+
if (typeof base !== "object") return preset ?? BANNER;
|
|
6109
|
+
const sheet2 = base.sheet ?? {};
|
|
6110
|
+
return { ...base, sheet: { ...sheet2, segments: settings.segments } };
|
|
6111
|
+
}, [preset, settings.segments]);
|
|
6112
|
+
const resolvedLayoutOptions = useMemo13(() => {
|
|
6113
|
+
const schema = getLayout(layout).optionsSchema;
|
|
6114
|
+
const takesPath = schema instanceof z26.ZodObject && "path" in schema.shape;
|
|
6115
|
+
return takesPath ? { ...layoutOptions, path: stage.path } : layoutOptions;
|
|
6116
|
+
}, [layout, layoutOptions, stage.path]);
|
|
6117
|
+
const slots = useMemo13(() => {
|
|
6118
|
+
if (papers) return papers;
|
|
6119
|
+
if (images) return void 0;
|
|
6120
|
+
if (text !== void 0) {
|
|
6121
|
+
const columns = Array.isArray(text) ? text : splitAcrossBanners(text, count);
|
|
6122
|
+
const longest = columns.reduce((n, c) => Math.max(n, c.split("\n").length), 1);
|
|
6123
|
+
const size = bannerTextSize(longest);
|
|
6124
|
+
return columns.map((column) => ({
|
|
6125
|
+
content: {
|
|
6126
|
+
type: "text",
|
|
6127
|
+
text: column,
|
|
6128
|
+
size,
|
|
6129
|
+
align: "center",
|
|
6130
|
+
color: "#241f1a",
|
|
6131
|
+
lineHeight: 1.25,
|
|
6132
|
+
font: 'Georgia, "Times New Roman", serif',
|
|
6133
|
+
weight: 400,
|
|
6134
|
+
padding: 0.06
|
|
6135
|
+
}
|
|
6136
|
+
}));
|
|
6137
|
+
}
|
|
6138
|
+
return Array.from({ length: count }, () => ({}));
|
|
6139
|
+
}, [papers, images, text, count]);
|
|
6140
|
+
const figureDistance = progress !== void 0 ? progress * path.length : void 0;
|
|
6141
|
+
const surroundRadius = useMemo13(
|
|
6142
|
+
() => Math.max(path.length * 1.6, paperHeight * 9),
|
|
6143
|
+
[path.length, paperHeight]
|
|
6144
|
+
);
|
|
6145
|
+
const source = useMemo13(() => {
|
|
6146
|
+
const [x, z27] = walkPoint(path, path.length + stage.source.beyond);
|
|
6147
|
+
const [tx, tz] = path.tangentAt(1);
|
|
6148
|
+
const size = paperHeight * stage.source.spread;
|
|
6149
|
+
return { position: [x, size * 0.35, z27], yaw: Math.atan2(-tx, -tz), size };
|
|
6150
|
+
}, [path, stage.source.beyond, stage.source.spread, paperHeight]);
|
|
6151
|
+
return /* @__PURE__ */ jsxs11(Fragment2, { children: [
|
|
6152
|
+
/* @__PURE__ */ jsx14(ShotRig, { stage, paperHeight, progress, still }),
|
|
6153
|
+
/* @__PURE__ */ jsx14(
|
|
6154
|
+
PaperLighting,
|
|
4832
6155
|
{
|
|
4833
|
-
|
|
4834
|
-
|
|
6156
|
+
preset: stage.lighting,
|
|
6157
|
+
floor: 0,
|
|
6158
|
+
scale: 60,
|
|
6159
|
+
reducedMotion,
|
|
6160
|
+
shadowMapSize: settings.shadowMapSize,
|
|
6161
|
+
contactShadow: settings.contactShadow
|
|
4835
6162
|
}
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
]
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
/*
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
6163
|
+
),
|
|
6164
|
+
quality === "auto" && /* @__PURE__ */ jsx14(QualityWatch, { tier, onChange: setTier }),
|
|
6165
|
+
stage.source.surround && settings.surround && /* @__PURE__ */ jsx14(Surround, { radius: surroundRadius, horizon: stage.source.color, zenith: stage.source.zenith }),
|
|
6166
|
+
stage.source.enabled && /* @__PURE__ */ jsx14(Source, { size: source.size, position: source.position, yaw: source.yaw, color: stage.source.color }),
|
|
6167
|
+
stage.ground.enabled && /* @__PURE__ */ jsxs11("mesh", { rotation: [-Math.PI / 2, 0, 0], receiveShadow: true, children: [
|
|
6168
|
+
/* @__PURE__ */ jsx14("planeGeometry", { args: [surroundRadius * 1.3, surroundRadius * 1.3] }),
|
|
6169
|
+
/* @__PURE__ */ jsx14("meshStandardMaterial", { color: stage.ground.color, roughness: 1 })
|
|
6170
|
+
] }),
|
|
6171
|
+
/* @__PURE__ */ jsx14(
|
|
6172
|
+
PaperFieldMesh,
|
|
6173
|
+
{
|
|
6174
|
+
preset: paper,
|
|
6175
|
+
papers: slots,
|
|
6176
|
+
images,
|
|
6177
|
+
layout,
|
|
6178
|
+
layoutOptions: resolvedLayoutOptions,
|
|
6179
|
+
motion: { driver: "none" },
|
|
6180
|
+
entrance: { type: "none" },
|
|
6181
|
+
reducedMotion
|
|
6182
|
+
}
|
|
6183
|
+
),
|
|
6184
|
+
stage.showFigure && /* @__PURE__ */ jsx14(Figure, { path: stage.path, figure: stage.figure, distance: figureDistance, frozen: reducedMotion })
|
|
6185
|
+
] });
|
|
4855
6186
|
}
|
|
4856
|
-
|
|
4857
|
-
);
|
|
6187
|
+
function PaperStage({ children, className, style, ...sceneProps }) {
|
|
6188
|
+
const dpr = qualityFor(sceneProps.quality ?? "auto").dpr;
|
|
6189
|
+
return /* @__PURE__ */ jsx14("div", { className, style: { width: "100%", height: "100%", ...style }, children: /* @__PURE__ */ jsxs11(
|
|
6190
|
+
Canvas3,
|
|
6191
|
+
{
|
|
6192
|
+
shadows: true,
|
|
6193
|
+
dpr: [1, dpr],
|
|
6194
|
+
camera: { fov: 38, near: 0.05, far: 400 },
|
|
6195
|
+
onCreated: ({ gl, scene }) => {
|
|
6196
|
+
gl.toneMapping = THREE17.ACESFilmicToneMapping;
|
|
6197
|
+
scene.background = new THREE17.Color("#0c0a0b");
|
|
6198
|
+
},
|
|
6199
|
+
children: [
|
|
6200
|
+
/* @__PURE__ */ jsx14(PaperStageScene, { ...sceneProps }),
|
|
6201
|
+
children
|
|
6202
|
+
]
|
|
6203
|
+
}
|
|
6204
|
+
) });
|
|
4858
6205
|
}
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
6206
|
+
|
|
6207
|
+
// src/stage/walks.ts
|
|
6208
|
+
var walkNames = ["straight", "bend", "ess", "ring", "spiral"];
|
|
6209
|
+
var walks = {
|
|
6210
|
+
/** Straight down the nave, away from the camera. The reference shot. */
|
|
6211
|
+
straight: {
|
|
6212
|
+
points: [
|
|
6213
|
+
[0, 16],
|
|
6214
|
+
[0, -20]
|
|
6215
|
+
],
|
|
6216
|
+
closed: false
|
|
6217
|
+
},
|
|
6218
|
+
/** One long curve, so the far end of the colonnade stays hidden until you reach it. */
|
|
6219
|
+
bend: {
|
|
6220
|
+
points: [
|
|
6221
|
+
[-2, 16],
|
|
6222
|
+
[0, 6],
|
|
6223
|
+
[5, -3],
|
|
6224
|
+
[12, -10]
|
|
6225
|
+
],
|
|
6226
|
+
closed: false
|
|
6227
|
+
},
|
|
6228
|
+
/** Two opposed curves — the walk turns twice and the banners turn with it. */
|
|
6229
|
+
ess: {
|
|
6230
|
+
points: [
|
|
6231
|
+
[6, 17],
|
|
6232
|
+
[-3, 7],
|
|
6233
|
+
[3, -5],
|
|
6234
|
+
[-6, -17]
|
|
6235
|
+
],
|
|
6236
|
+
closed: false
|
|
6237
|
+
},
|
|
6238
|
+
/** A closed loop: the only walk `phase` can slide, and the only endless one. */
|
|
6239
|
+
ring: {
|
|
6240
|
+
points: [
|
|
6241
|
+
[11, 0],
|
|
6242
|
+
[0, 11],
|
|
6243
|
+
[-11, 0],
|
|
6244
|
+
[0, -11]
|
|
6245
|
+
],
|
|
6246
|
+
closed: true
|
|
6247
|
+
},
|
|
6248
|
+
/** Inward and tightening — the space closes as the figure goes deeper. */
|
|
6249
|
+
spiral: {
|
|
6250
|
+
points: [
|
|
6251
|
+
[14, 2],
|
|
6252
|
+
[2, 13],
|
|
6253
|
+
[-11, 1],
|
|
6254
|
+
[-1, -9],
|
|
6255
|
+
[7, -2],
|
|
6256
|
+
[1, 4]
|
|
6257
|
+
],
|
|
6258
|
+
closed: false
|
|
4869
6259
|
}
|
|
4870
|
-
|
|
6260
|
+
};
|
|
6261
|
+
function getWalk(name) {
|
|
6262
|
+
return walks[name];
|
|
4871
6263
|
}
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
6264
|
+
|
|
6265
|
+
// src/stage/presets.ts
|
|
6266
|
+
var banner = (width, height, drape2 = {}) => ({
|
|
6267
|
+
sheet: { width, height, segments: "auto" },
|
|
6268
|
+
stock: "vellum",
|
|
6269
|
+
surface: { grain: 0.22 },
|
|
6270
|
+
deformers: [
|
|
6271
|
+
{ type: "drape", options: { amplitude: 0.16, folds: 3, falloff: 1.7, gather: 0.28, ...drape2 } }
|
|
6272
|
+
]
|
|
6273
|
+
});
|
|
6274
|
+
var stagePresets = {
|
|
6275
|
+
nave: {
|
|
6276
|
+
id: "nave",
|
|
6277
|
+
label: "Nave",
|
|
6278
|
+
description: "A straight aisle of hanging banners, lit from the far end.",
|
|
6279
|
+
stage: {
|
|
6280
|
+
path: walks.straight,
|
|
6281
|
+
shot: { shot: "follow", distance: 5, lookAhead: 12, offset: 1.5 },
|
|
6282
|
+
lighting: "nave"
|
|
6283
|
+
},
|
|
6284
|
+
layout: "colonnade",
|
|
6285
|
+
layoutOptions: { aisle: 2.6, twist: 22, drape: 0.6, rise: 0.3 },
|
|
6286
|
+
paper: banner(1.5, 8.5),
|
|
6287
|
+
count: 18,
|
|
6288
|
+
text: "the paper remembers every hand that folded it and every room it was carried through"
|
|
6289
|
+
},
|
|
6290
|
+
procession: {
|
|
6291
|
+
id: "procession",
|
|
6292
|
+
label: "Procession",
|
|
6293
|
+
description: "The walk turns twice, so the far end stays hidden until you reach it.",
|
|
6294
|
+
stage: {
|
|
6295
|
+
path: walks.ess,
|
|
6296
|
+
shot: { shot: "low", distance: 4, lookAhead: 9, offset: 1.1 },
|
|
6297
|
+
lighting: "nave",
|
|
6298
|
+
figure: { speed: 1.05 }
|
|
6299
|
+
},
|
|
6300
|
+
layout: "colonnade",
|
|
6301
|
+
layoutOptions: { aisle: 2.2, twist: 34, breathe: 0.45, drape: 0.7 },
|
|
6302
|
+
paper: banner(1.3, 9.5, { folds: 4, amplitude: 0.2 }),
|
|
6303
|
+
count: 28,
|
|
6304
|
+
text: "every letter you did not send is still folded somewhere in the dark waiting to be read aloud"
|
|
6305
|
+
},
|
|
6306
|
+
cloister: {
|
|
6307
|
+
id: "cloister",
|
|
6308
|
+
label: "Cloister",
|
|
6309
|
+
description: "A closed loop. The figure walks it forever and the banners drift past.",
|
|
6310
|
+
stage: {
|
|
6311
|
+
path: walks.ring,
|
|
6312
|
+
shot: { shot: "follow", distance: 4.5, lookAhead: 8, offset: 1.2 },
|
|
6313
|
+
lighting: "nave"
|
|
6314
|
+
},
|
|
6315
|
+
layout: "colonnade",
|
|
6316
|
+
layoutOptions: { aisle: 2.4, twist: 18, rise: 0.22 },
|
|
6317
|
+
paper: banner(1.6, 7.5),
|
|
6318
|
+
count: 24,
|
|
6319
|
+
text: "around and around and the same words come back changed"
|
|
6320
|
+
},
|
|
6321
|
+
threshold: {
|
|
6322
|
+
id: "threshold",
|
|
6323
|
+
label: "Threshold",
|
|
6324
|
+
description: "A few enormous sheets, wide enough apart to walk between and read.",
|
|
6325
|
+
stage: {
|
|
6326
|
+
// Its own short walk. A colonnade spreads over the WHOLE path whatever
|
|
6327
|
+
// it is populating, so ten banners on the default 36-unit walk stand
|
|
6328
|
+
// seven apart and the shot looks down an empty corridor.
|
|
6329
|
+
path: {
|
|
6330
|
+
points: [
|
|
6331
|
+
[0, 9],
|
|
6332
|
+
[0, -11]
|
|
6333
|
+
],
|
|
6334
|
+
closed: false
|
|
6335
|
+
},
|
|
6336
|
+
// The aisle has to stay inside the frustum at the distance the shot
|
|
6337
|
+
// stands: paper half a frame-width off the walk line is paper you
|
|
6338
|
+
// never see. `lead` fails here for the same reason and worse.
|
|
6339
|
+
shot: { shot: "follow", distance: 6.5, lookAhead: 9, offset: 0.9 },
|
|
6340
|
+
lighting: "nave",
|
|
6341
|
+
figure: { speed: 0.85 },
|
|
6342
|
+
source: { spread: 4 }
|
|
6343
|
+
},
|
|
6344
|
+
layout: "colonnade",
|
|
6345
|
+
layoutOptions: { aisle: 2.4, twist: 14, breathe: 0.18, margin: 0.12, rise: 0.2 },
|
|
6346
|
+
paper: banner(2.6, 10, { folds: 2, amplitude: 0.24, falloff: 2 }),
|
|
6347
|
+
count: 10,
|
|
6348
|
+
text: "stand closer and read what it cost to write this down"
|
|
6349
|
+
},
|
|
6350
|
+
archive: {
|
|
6351
|
+
id: "archive",
|
|
6352
|
+
label: "Archive",
|
|
6353
|
+
description: "Narrow strips packed tight \u2014 a corridor of records you edge through.",
|
|
6354
|
+
stage: {
|
|
6355
|
+
path: walks.bend,
|
|
6356
|
+
// Far enough back that the figure reads as small; a `low` camera
|
|
6357
|
+
// three units behind a body is all body.
|
|
6358
|
+
shot: { shot: "low", distance: 8, lookAhead: 13, offset: 0.5 },
|
|
6359
|
+
lighting: "nave",
|
|
6360
|
+
ground: { color: "#0b0908" }
|
|
6361
|
+
},
|
|
6362
|
+
layout: "colonnade",
|
|
6363
|
+
layoutOptions: { aisle: 1.7, twist: 44, breathe: 0.5, drape: 0.75, rise: 0.4 },
|
|
6364
|
+
paper: banner(0.85, 11, { folds: 2, amplitude: 0.12 }),
|
|
6365
|
+
count: 44,
|
|
6366
|
+
text: "catalogued indexed cross referenced filed and never once opened by anyone at all"
|
|
4880
6367
|
}
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
else gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
|
|
6368
|
+
};
|
|
6369
|
+
function getStagePreset(id) {
|
|
6370
|
+
const preset = stagePresets[id];
|
|
6371
|
+
if (!preset) {
|
|
6372
|
+
throw new Error(
|
|
6373
|
+
`[paperlab] Unknown stage preset "${id}". Available: ${Object.keys(stagePresets).join(", ")}`
|
|
6374
|
+
);
|
|
4889
6375
|
}
|
|
4890
|
-
|
|
4891
|
-
if (timeLoc) gl.uniform1f(timeLoc, c.t);
|
|
4892
|
-
const quad = gl.createBuffer();
|
|
4893
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
|
|
4894
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
4895
|
-
const posLoc = gl.getAttribLocation(program, "aPos");
|
|
4896
|
-
gl.enableVertexAttribArray(posLoc);
|
|
4897
|
-
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
|
|
4898
|
-
const tex = gl.createTexture();
|
|
4899
|
-
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
4900
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, GRID, GRID, 0, gl.RGBA, gl.FLOAT, null);
|
|
4901
|
-
const fbo = gl.createFramebuffer();
|
|
4902
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
4903
|
-
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
|
4904
|
-
gl.viewport(0, 0, GRID, GRID);
|
|
4905
|
-
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
4906
|
-
const out = new Float32Array(GRID * GRID * 4);
|
|
4907
|
-
gl.readPixels(0, 0, GRID, GRID, gl.RGBA, gl.FLOAT, out);
|
|
4908
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
4909
|
-
return out;
|
|
6376
|
+
return preset;
|
|
4910
6377
|
}
|
|
4911
|
-
function
|
|
4912
|
-
|
|
4913
|
-
const gl = cnv.getContext("webgl2");
|
|
4914
|
-
if (!gl) throw new Error("[paperlab parity] WebGL2 unavailable");
|
|
4915
|
-
if (!gl.getExtension("EXT_color_buffer_float")) {
|
|
4916
|
-
throw new Error("[paperlab parity] EXT_color_buffer_float unavailable");
|
|
4917
|
-
}
|
|
4918
|
-
const point = new THREE13.Vector3();
|
|
4919
|
-
return parityCases.map((c) => {
|
|
4920
|
-
const gpu = runCaseOnGPU(gl, c);
|
|
4921
|
-
let maxError = 0;
|
|
4922
|
-
for (let row = 0; row < GRID; row++) {
|
|
4923
|
-
for (let col = 0; col < GRID; col++) {
|
|
4924
|
-
const u = col / (GRID - 1);
|
|
4925
|
-
const v = row / (GRID - 1);
|
|
4926
|
-
point.set((u - 0.5) * c.sheet.width, (v - 0.5) * c.sheet.height, 0);
|
|
4927
|
-
displacePoint(point, u, v, c.stack, { t: c.t, sheet: c.sheet });
|
|
4928
|
-
const i4 = (row * GRID + col) * 4;
|
|
4929
|
-
maxError = Math.max(
|
|
4930
|
-
maxError,
|
|
4931
|
-
Math.abs(gpu[i4] - point.x),
|
|
4932
|
-
Math.abs(gpu[i4 + 1] - point.y),
|
|
4933
|
-
Math.abs(gpu[i4 + 2] - point.z)
|
|
4934
|
-
);
|
|
4935
|
-
}
|
|
4936
|
-
}
|
|
4937
|
-
return { name: c.name, maxError, pass: maxError < PARITY_EPSILON };
|
|
4938
|
-
});
|
|
6378
|
+
function listStagePresets() {
|
|
6379
|
+
return Object.keys(stagePresets);
|
|
4939
6380
|
}
|
|
4940
6381
|
|
|
4941
6382
|
// src/config/diff.ts
|
|
@@ -5014,7 +6455,8 @@ var BEHAVIOR_PHRASES = {
|
|
|
5014
6455
|
fly: () => "arched and fluttering like it is airborne",
|
|
5015
6456
|
fall: () => "rippling with one corner lifted, like a dropped sheet",
|
|
5016
6457
|
carry: () => "drooping from a pinched corner, fluttering as if being carried",
|
|
5017
|
-
flight: (o) => o.path === "loop" ? "tumbling through a seamless airborne loop" : "tumbling across the scene on the wind"
|
|
6458
|
+
flight: (o) => o.path === "loop" ? "tumbling through a seamless airborne loop" : "tumbling across the scene on the wind",
|
|
6459
|
+
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"
|
|
5018
6460
|
};
|
|
5019
6461
|
function describeConfig(config) {
|
|
5020
6462
|
const stock = getStock(config.stock);
|
|
@@ -5079,6 +6521,498 @@ Constraints: don't modify the preset values; three >= 0.160 and React 19 are
|
|
|
5079
6521
|
required; the component needs no props.`;
|
|
5080
6522
|
}
|
|
5081
6523
|
|
|
6524
|
+
// src/stage/export.ts
|
|
6525
|
+
var SCROLL_HEIGHTS = 4;
|
|
6526
|
+
function walkNameFor(path) {
|
|
6527
|
+
const key = JSON.stringify({ points: path.points, closed: path.closed });
|
|
6528
|
+
return walkNames.find(
|
|
6529
|
+
(name) => JSON.stringify({ points: walks[name].points, closed: walks[name].closed }) === key
|
|
6530
|
+
);
|
|
6531
|
+
}
|
|
6532
|
+
function stripDefaults(value, defaults) {
|
|
6533
|
+
if (Array.isArray(value) || Array.isArray(defaults)) {
|
|
6534
|
+
return JSON.stringify(value) === JSON.stringify(defaults) ? void 0 : value;
|
|
6535
|
+
}
|
|
6536
|
+
if (value && defaults && typeof value === "object" && typeof defaults === "object") {
|
|
6537
|
+
const out = {};
|
|
6538
|
+
for (const [key, child] of Object.entries(value)) {
|
|
6539
|
+
const kept = stripDefaults(child, defaults[key]);
|
|
6540
|
+
if (kept !== void 0) out[key] = kept;
|
|
6541
|
+
}
|
|
6542
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
6543
|
+
}
|
|
6544
|
+
return value === defaults ? void 0 : value;
|
|
6545
|
+
}
|
|
6546
|
+
function diffStage(stage) {
|
|
6547
|
+
const resolved = stageSchema.parse(stage);
|
|
6548
|
+
const defaults = stageSchema.parse({});
|
|
6549
|
+
const diff = stripDefaults(resolved, defaults) ?? {};
|
|
6550
|
+
if (diff.path !== void 0) diff.path = resolved.path;
|
|
6551
|
+
return diff;
|
|
6552
|
+
}
|
|
6553
|
+
function stringifyStage(value, indent = 0) {
|
|
6554
|
+
const pad = " ".repeat(indent);
|
|
6555
|
+
const inner = " ".repeat(indent + 1);
|
|
6556
|
+
if (Array.isArray(value)) {
|
|
6557
|
+
if (value.length === 0) return "[]";
|
|
6558
|
+
if (value.every((v) => typeof v === "number")) return `[${value.join(", ")}]`;
|
|
6559
|
+
const items = value.map((v) => `${inner}${stringifyStage(v, indent + 1)}`);
|
|
6560
|
+
return `[
|
|
6561
|
+
${items.join(",\n")}
|
|
6562
|
+
${pad}]`;
|
|
6563
|
+
}
|
|
6564
|
+
if (value && typeof value === "object") {
|
|
6565
|
+
const entries = Object.entries(value);
|
|
6566
|
+
if (entries.length === 0) return "{}";
|
|
6567
|
+
const items = entries.map(([k, v]) => `${inner}${JSON.stringify(k)}: ${stringifyStage(v, indent + 1)}`);
|
|
6568
|
+
return `{
|
|
6569
|
+
${items.join(",\n")}
|
|
6570
|
+
${pad}}`;
|
|
6571
|
+
}
|
|
6572
|
+
return JSON.stringify(value);
|
|
6573
|
+
}
|
|
6574
|
+
var SHOT_PHRASES = {
|
|
6575
|
+
follow: "from behind and a little above them, looking up the walk",
|
|
6576
|
+
lead: "from in front, walking backward as they come on",
|
|
6577
|
+
low: "from down at floor level, looking up the banners",
|
|
6578
|
+
wide: "from off to one side, level with them"
|
|
6579
|
+
};
|
|
6580
|
+
function describeStage(input) {
|
|
6581
|
+
const stage = stageSchema.parse(input.stage);
|
|
6582
|
+
const count = input.count ?? 22;
|
|
6583
|
+
const walk = walkNameFor(stage.path);
|
|
6584
|
+
const parts = [];
|
|
6585
|
+
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`;
|
|
6586
|
+
parts.push(`${count} tall paper banners standing along ${shape}`);
|
|
6587
|
+
if (input.text?.trim()) {
|
|
6588
|
+
parts.push("each printed with a column of your text running down it");
|
|
6589
|
+
}
|
|
6590
|
+
if (stage.showFigure) {
|
|
6591
|
+
parts.push(`a small dark figure walking between them, seen ${SHOT_PHRASES[stage.shot.shot]}`);
|
|
6592
|
+
}
|
|
6593
|
+
parts.push(
|
|
6594
|
+
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`
|
|
6595
|
+
);
|
|
6596
|
+
if (input.scroll) parts.push("and scrolling the page walks the figure deeper into it");
|
|
6597
|
+
return parts.join(", ");
|
|
6598
|
+
}
|
|
6599
|
+
function propLines(input, indent) {
|
|
6600
|
+
const lines = [];
|
|
6601
|
+
if (input.paper) lines.push(`${indent}preset={banner}`);
|
|
6602
|
+
if (input.text?.trim()) lines.push(`${indent}text={text}`);
|
|
6603
|
+
if (input.count !== void 0) lines.push(`${indent}count={${input.count}}`);
|
|
6604
|
+
if (input.layout !== "colonnade") lines.push(`${indent}layout="${input.layout}"`);
|
|
6605
|
+
const layoutOptions = input.layoutOptions ?? {};
|
|
6606
|
+
const layoutDefaults = getLayout(input.layout).defaults;
|
|
6607
|
+
const changed = {};
|
|
6608
|
+
for (const [key, value] of Object.entries(layoutOptions)) {
|
|
6609
|
+
if (JSON.stringify(value) !== JSON.stringify(layoutDefaults[key])) changed[key] = value;
|
|
6610
|
+
}
|
|
6611
|
+
if (Object.keys(changed).length > 0) {
|
|
6612
|
+
lines.push(`${indent}layoutOptions={${stringifyStage(changed).replace(/\n\s*/g, " ")}}`);
|
|
6613
|
+
}
|
|
6614
|
+
lines.push(`${indent}stage={stage}`);
|
|
6615
|
+
return lines.join("\n");
|
|
6616
|
+
}
|
|
6617
|
+
function buildStageComponentSource(input) {
|
|
6618
|
+
const name = input.componentName ?? "PaperNave";
|
|
6619
|
+
const stage = diffStage(input.stage);
|
|
6620
|
+
const stageConst = `const stage = ${stringifyStage(stage)} satisfies StageConfigInput`;
|
|
6621
|
+
const bannerConst = input.paper ? `
|
|
6622
|
+
|
|
6623
|
+
const banner = ${stringifyStage(diffConfig(paperConfigSchema.parse(input.paper)))} satisfies PaperConfigInput` : "";
|
|
6624
|
+
const textConst = input.text?.trim() ? `
|
|
6625
|
+
|
|
6626
|
+
const text = ${JSON.stringify(input.text)}` : "";
|
|
6627
|
+
if (!input.scroll) {
|
|
6628
|
+
return `import { PaperStage, type StageConfigInput${input.paper ? ", type PaperConfigInput" : ""} } from 'paperlab'
|
|
6629
|
+
|
|
6630
|
+
${stageConst}${bannerConst}${textConst}
|
|
6631
|
+
|
|
6632
|
+
export function ${name}() {
|
|
6633
|
+
return (
|
|
6634
|
+
<PaperStage
|
|
6635
|
+
${propLines(input, " ")}
|
|
6636
|
+
/>
|
|
6637
|
+
)
|
|
6638
|
+
}`;
|
|
6639
|
+
}
|
|
6640
|
+
return `import { useEffect, useRef, useState } from 'react'
|
|
6641
|
+
import { PaperStage, type StageConfigInput${input.paper ? ", type PaperConfigInput" : ""} } from 'paperlab'
|
|
6642
|
+
|
|
6643
|
+
${stageConst}${bannerConst}${textConst}
|
|
6644
|
+
|
|
6645
|
+
export function ${name}() {
|
|
6646
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
6647
|
+
const [progress, setProgress] = useState(0)
|
|
6648
|
+
|
|
6649
|
+
// Scroll the section, walk the figure. The stage is pinned for the height
|
|
6650
|
+
// of the section, so the page scrolling past it IS the walk.
|
|
6651
|
+
useEffect(() => {
|
|
6652
|
+
const el = ref.current
|
|
6653
|
+
if (!el) return
|
|
6654
|
+
const onScroll = () => {
|
|
6655
|
+
const { top, height } = el.getBoundingClientRect()
|
|
6656
|
+
const travel = Math.max(height - window.innerHeight, 1)
|
|
6657
|
+
setProgress(Math.min(Math.max(-top / travel, 0), 1))
|
|
6658
|
+
}
|
|
6659
|
+
onScroll()
|
|
6660
|
+
window.addEventListener('scroll', onScroll, { passive: true })
|
|
6661
|
+
window.addEventListener('resize', onScroll)
|
|
6662
|
+
return () => {
|
|
6663
|
+
window.removeEventListener('scroll', onScroll)
|
|
6664
|
+
window.removeEventListener('resize', onScroll)
|
|
6665
|
+
}
|
|
6666
|
+
}, [])
|
|
6667
|
+
|
|
6668
|
+
return (
|
|
6669
|
+
<div ref={ref} style={{ height: '${SCROLL_HEIGHTS * 100}vh' }}>
|
|
6670
|
+
<div style={{ position: 'sticky', top: 0, height: '100vh' }}>
|
|
6671
|
+
<PaperStage
|
|
6672
|
+
${propLines(input, " ")}
|
|
6673
|
+
progress={progress}
|
|
6674
|
+
/>
|
|
6675
|
+
</div>
|
|
6676
|
+
</div>
|
|
6677
|
+
)
|
|
6678
|
+
}`;
|
|
6679
|
+
}
|
|
6680
|
+
function buildStageAgentPayload(input) {
|
|
6681
|
+
const name = input.componentName ?? "PaperNave";
|
|
6682
|
+
const sizing = input.scroll ? `4. Sizing: the component brings its own height \u2014 it reserves ${SCROLL_HEIGHTS} viewport
|
|
6683
|
+
heights of scroll and pins the canvas inside that. Drop it into the page
|
|
6684
|
+
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;
|
|
6685
|
+
give the parent an explicit height.`;
|
|
6686
|
+
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})
|
|
6687
|
+
|
|
6688
|
+
1. Install the dependencies:
|
|
6689
|
+
|
|
6690
|
+
npm i paperlab three @react-three/fiber gsap
|
|
6691
|
+
|
|
6692
|
+
2. Create the component below as \`components/${name}.tsx\` (or the project's
|
|
6693
|
+
component convention). It is self-contained \u2014 it owns its own <Canvas>,
|
|
6694
|
+
its own camera and its own lighting:
|
|
6695
|
+
|
|
6696
|
+
\`\`\`tsx
|
|
6697
|
+
${buildStageComponentSource(input)}
|
|
6698
|
+
\`\`\`
|
|
6699
|
+
|
|
6700
|
+
3. Placement: this is a full-bleed scene, not an inline element. Give it the
|
|
6701
|
+
full width of the viewport.
|
|
6702
|
+
|
|
6703
|
+
${sizing}
|
|
6704
|
+
|
|
6705
|
+
5. Verify: run the dev server. You should see ${describeStage(input)}.
|
|
6706
|
+
If the canvas is blank, the container almost certainly has no height \u2014 give
|
|
6707
|
+
it one (this is the classic React Three Fiber integration bug, not a
|
|
6708
|
+
paperlab bug).
|
|
6709
|
+
|
|
6710
|
+
Constraints: don't modify the stage values; the camera is driven by the
|
|
6711
|
+
stage's own shot, so don't add OrbitControls; three >= 0.160 and React 19 are
|
|
6712
|
+
required; the component needs no props.`;
|
|
6713
|
+
}
|
|
6714
|
+
|
|
6715
|
+
// src/field/parity.ts
|
|
6716
|
+
import * as THREE18 from "three";
|
|
6717
|
+
var GRID = 48;
|
|
6718
|
+
var PARITY_EPSILON = 5e-4;
|
|
6719
|
+
var parityCases = [
|
|
6720
|
+
{
|
|
6721
|
+
name: "roll: defaults",
|
|
6722
|
+
stack: [{ type: "roll", options: { angle: 90, boundary: 0, radius: 0.12, spiral: 0.015 } }],
|
|
6723
|
+
sheet: { width: 1, height: 1.4 },
|
|
6724
|
+
t: 0
|
|
6725
|
+
},
|
|
6726
|
+
{
|
|
6727
|
+
name: "roll: tight receipt roll, rolling down",
|
|
6728
|
+
stack: [{ type: "roll", options: { angle: 270, boundary: -0.4, radius: 0.07, spiral: 0.02 } }],
|
|
6729
|
+
sheet: { width: 1, height: 2.6 },
|
|
6730
|
+
t: 0
|
|
6731
|
+
},
|
|
6732
|
+
{
|
|
6733
|
+
name: "curl: bottom-right peel",
|
|
6734
|
+
stack: [{ type: "curl", options: { corner: "bottom-right", amount: 0.45, radius: 0.2, skew: 0 } }],
|
|
6735
|
+
sheet: { width: 1.5, height: 1 },
|
|
6736
|
+
t: 0
|
|
6737
|
+
},
|
|
6738
|
+
{
|
|
6739
|
+
name: "curl: skewed top-left",
|
|
6740
|
+
stack: [{ type: "curl", options: { corner: "top-left", amount: 0.7, radius: 0.3, skew: 15 } }],
|
|
6741
|
+
sheet: { width: 1, height: 1 },
|
|
6742
|
+
t: 0
|
|
6743
|
+
},
|
|
6744
|
+
{
|
|
6745
|
+
name: "bend: positive arc at an angle",
|
|
6746
|
+
stack: [{ type: "bend", options: { curvature: 1.2, angle: 33 } }],
|
|
6747
|
+
sheet: { width: 1, height: 1.4 },
|
|
6748
|
+
t: 0
|
|
6749
|
+
},
|
|
6750
|
+
{
|
|
6751
|
+
/**
|
|
6752
|
+
* The gentle end, which nothing used to cover: the gate only ever tried
|
|
6753
|
+
* |curvature| ≥ 0.6, and `photo-print` — the field starter preset — bends
|
|
6754
|
+
* at 0.35. That band is where the arc's float32 evaluation loses its
|
|
6755
|
+
* cancellation, and where the two paths were 6e-4 apart until `bend` was
|
|
6756
|
+
* rewritten in cancellation-free form. Keep a case down here.
|
|
6757
|
+
*/
|
|
6758
|
+
name: "bend: barely-there arc (the band photo-print lives in)",
|
|
6759
|
+
stack: [{ type: "bend", options: { curvature: 0.35, angle: 0 } }],
|
|
6760
|
+
sheet: { width: 1.2, height: 0.9 },
|
|
6761
|
+
t: 0
|
|
6762
|
+
},
|
|
6763
|
+
{
|
|
6764
|
+
name: "bend: the gentlest arc the schema allows",
|
|
6765
|
+
stack: [{ type: "bend", options: { curvature: 0.02, angle: 61 } }],
|
|
6766
|
+
sheet: { width: 2, height: 2.6 },
|
|
6767
|
+
t: 0
|
|
6768
|
+
},
|
|
6769
|
+
{
|
|
6770
|
+
name: "bend: negative arc",
|
|
6771
|
+
stack: [{ type: "bend", options: { curvature: -0.8, angle: 0 } }],
|
|
6772
|
+
sheet: { width: 1, height: 1.4 },
|
|
6773
|
+
t: 0
|
|
6774
|
+
},
|
|
6775
|
+
{
|
|
6776
|
+
name: "fold: 90\xB0 hinge",
|
|
6777
|
+
stack: [{ type: "fold", options: { angle: 90, offset: 0, foldAngle: 90, radius: 0.06 } }],
|
|
6778
|
+
sheet: { width: 1, height: 1.4 },
|
|
6779
|
+
t: 0
|
|
6780
|
+
},
|
|
6781
|
+
{
|
|
6782
|
+
name: "fold: deep fold, travelling down",
|
|
6783
|
+
stack: [{ type: "fold", options: { angle: 270, offset: 0.2, foldAngle: 165, radius: 0.04 } }],
|
|
6784
|
+
sheet: { width: 1, height: 1.4 },
|
|
6785
|
+
t: 0
|
|
6786
|
+
},
|
|
6787
|
+
{
|
|
6788
|
+
name: "wave: free ripple at t=1.234",
|
|
6789
|
+
stack: [
|
|
6790
|
+
{
|
|
6791
|
+
type: "wave",
|
|
6792
|
+
options: { amplitude: 0.05, wavelength: 0.5, speed: 1, angle: 20, pinnedEdge: "none" }
|
|
6793
|
+
}
|
|
6794
|
+
],
|
|
6795
|
+
sheet: { width: 1, height: 1 },
|
|
6796
|
+
t: 1.234
|
|
6797
|
+
},
|
|
6798
|
+
{
|
|
6799
|
+
name: "wave: pinned top at t=2.5",
|
|
6800
|
+
stack: [
|
|
6801
|
+
{
|
|
6802
|
+
type: "wave",
|
|
6803
|
+
options: { amplitude: 0.04, wavelength: 0.4, speed: 1.5, angle: 80, pinnedEdge: "top" }
|
|
6804
|
+
}
|
|
6805
|
+
],
|
|
6806
|
+
sheet: { width: 1.2, height: 1.5 },
|
|
6807
|
+
t: 2.5
|
|
6808
|
+
},
|
|
6809
|
+
{
|
|
6810
|
+
name: "drape: banner hung from the top",
|
|
6811
|
+
stack: [
|
|
6812
|
+
{
|
|
6813
|
+
type: "drape",
|
|
6814
|
+
options: { amplitude: 0.17, folds: 5, falloff: 1.6, irregular: 0.45, gather: 0.5, pinnedEdge: "top" }
|
|
6815
|
+
}
|
|
6816
|
+
],
|
|
6817
|
+
sheet: { width: 1.5, height: 8.5 },
|
|
6818
|
+
t: 0
|
|
6819
|
+
},
|
|
6820
|
+
{
|
|
6821
|
+
name: "drape: deep irregular folds pinned at the bottom",
|
|
6822
|
+
stack: [
|
|
6823
|
+
{
|
|
6824
|
+
type: "drape",
|
|
6825
|
+
options: { amplitude: 0.55, folds: 11, falloff: 0.4, irregular: 1, gather: 1, pinnedEdge: "bottom" }
|
|
6826
|
+
}
|
|
6827
|
+
],
|
|
6828
|
+
sheet: { width: 2.2, height: 3 },
|
|
6829
|
+
t: 0
|
|
6830
|
+
},
|
|
6831
|
+
{
|
|
6832
|
+
name: "crumple: defaults",
|
|
6833
|
+
stack: [{ type: "crumple", options: { amount: 0.35, scale: 3, pull: 0.4, seed: 0 } }],
|
|
6834
|
+
sheet: { width: 1, height: 1.4 },
|
|
6835
|
+
t: 0
|
|
6836
|
+
},
|
|
6837
|
+
{
|
|
6838
|
+
// The adversarial one. `fract` is a sawtooth, so the two halves disagree
|
|
6839
|
+
// hardest where a crease lands exactly on a sample — a fine scale and a
|
|
6840
|
+
// seed whose fold directions are near-axis puts the most creases in
|
|
6841
|
+
// reach of the grid.
|
|
6842
|
+
name: "crumple: fully crushed, fine creases, off-axis seed",
|
|
6843
|
+
stack: [{ type: "crumple", options: { amount: 1, scale: 7.5, pull: 1, seed: 5 } }],
|
|
6844
|
+
sheet: { width: 1.3, height: 0.9 },
|
|
6845
|
+
t: 0
|
|
6846
|
+
},
|
|
6847
|
+
{
|
|
6848
|
+
// Crush then curl, which is the order the crumple BEHAVIOR stacks them:
|
|
6849
|
+
// the creases have to be placed on the flat sheet, not on a bent one.
|
|
6850
|
+
name: "stacked: crumple \u2218 bend (the crumple behavior)",
|
|
6851
|
+
stack: [
|
|
6852
|
+
{ type: "crumple", options: { amount: 0.62, scale: 3.3, pull: 0.5, seed: 2 } },
|
|
6853
|
+
{ type: "bend", options: { curvature: 0.31, angle: 35 } }
|
|
6854
|
+
],
|
|
6855
|
+
sheet: { width: 1.1, height: 1.4 },
|
|
6856
|
+
t: 0
|
|
6857
|
+
},
|
|
6858
|
+
{
|
|
6859
|
+
name: "stacked: letter-fold pair (fold \u2218 fold)",
|
|
6860
|
+
stack: [
|
|
6861
|
+
{ type: "fold", options: { angle: 270, offset: 0.2333, foldAngle: 120, radius: 0.05 } },
|
|
6862
|
+
{ type: "fold", options: { angle: 90, offset: 0.2333, foldAngle: 100, radius: 0.08 } }
|
|
6863
|
+
],
|
|
6864
|
+
sheet: { width: 1, height: 1.4 },
|
|
6865
|
+
t: 0
|
|
6866
|
+
},
|
|
6867
|
+
{
|
|
6868
|
+
name: "stacked: bend \u2218 roll \u2218 wave",
|
|
6869
|
+
stack: [
|
|
6870
|
+
{ type: "bend", options: { curvature: 0.6, angle: 0 } },
|
|
6871
|
+
{ type: "roll", options: { angle: 90, boundary: 0.1, radius: 0.15, spiral: 0 } },
|
|
6872
|
+
{
|
|
6873
|
+
type: "wave",
|
|
6874
|
+
options: { amplitude: 0.02, wavelength: 0.6, speed: 0.7, angle: 45, pinnedEdge: "none" }
|
|
6875
|
+
}
|
|
6876
|
+
],
|
|
6877
|
+
sheet: { width: 1, height: 1.4 },
|
|
6878
|
+
t: 0.8
|
|
6879
|
+
}
|
|
6880
|
+
];
|
|
6881
|
+
function buildParityFragment(stack, sheet2) {
|
|
6882
|
+
const composed = buildDisplacementGLSL(stack, sheet2);
|
|
6883
|
+
return (
|
|
6884
|
+
/* glsl */
|
|
6885
|
+
`#version 300 es
|
|
6886
|
+
precision highp float;
|
|
6887
|
+
uniform float uPlTime;
|
|
6888
|
+
uniform float uPlBias;
|
|
6889
|
+
${composed.functionsSrc}
|
|
6890
|
+
${composed.displaceSrc}
|
|
6891
|
+
out vec4 outColor;
|
|
6892
|
+
void main() {
|
|
6893
|
+
vec2 uv = (gl_FragCoord.xy - 0.5) / float(${GRID - 1});
|
|
6894
|
+
vec3 p = vec3((uv - 0.5) * uSheet, 0.0);
|
|
6895
|
+
outColor = vec4(plDisplace(p, uv, uPlTime, uPlBias), 1.0);
|
|
6896
|
+
}
|
|
6897
|
+
`
|
|
6898
|
+
);
|
|
6899
|
+
}
|
|
6900
|
+
var VERT = `#version 300 es
|
|
6901
|
+
in vec2 aPos;
|
|
6902
|
+
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
|
6903
|
+
`;
|
|
6904
|
+
function compile(gl, type, src) {
|
|
6905
|
+
const shader = gl.createShader(type);
|
|
6906
|
+
gl.shaderSource(shader, src);
|
|
6907
|
+
gl.compileShader(shader);
|
|
6908
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
6909
|
+
throw new Error(`[paperlab parity] shader compile failed: ${gl.getShaderInfoLog(shader)}`);
|
|
6910
|
+
}
|
|
6911
|
+
return shader;
|
|
6912
|
+
}
|
|
6913
|
+
function runCaseOnGPU(gl, c, bias = 1) {
|
|
6914
|
+
const composed = buildDisplacementGLSL(c.stack, c.sheet);
|
|
6915
|
+
const program = gl.createProgram();
|
|
6916
|
+
gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERT));
|
|
6917
|
+
gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, buildParityFragment(c.stack, c.sheet)));
|
|
6918
|
+
gl.linkProgram(program);
|
|
6919
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
6920
|
+
throw new Error(`[paperlab parity] link failed: ${gl.getProgramInfoLog(program)}`);
|
|
6921
|
+
}
|
|
6922
|
+
gl.useProgram(program);
|
|
6923
|
+
for (const [name, value] of Object.entries(composed.uniforms)) {
|
|
6924
|
+
const loc = gl.getUniformLocation(program, name);
|
|
6925
|
+
if (!loc) continue;
|
|
6926
|
+
if (typeof value === "number") gl.uniform1f(loc, value);
|
|
6927
|
+
else if (value.length === 2) gl.uniform2f(loc, value[0], value[1]);
|
|
6928
|
+
else if (value.length === 3) gl.uniform3f(loc, value[0], value[1], value[2]);
|
|
6929
|
+
else gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
|
|
6930
|
+
}
|
|
6931
|
+
const timeLoc = gl.getUniformLocation(program, "uPlTime");
|
|
6932
|
+
if (timeLoc) gl.uniform1f(timeLoc, c.t);
|
|
6933
|
+
const biasLoc = gl.getUniformLocation(program, "uPlBias");
|
|
6934
|
+
if (biasLoc) gl.uniform1f(biasLoc, bias);
|
|
6935
|
+
const quad = gl.createBuffer();
|
|
6936
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
|
|
6937
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
6938
|
+
const posLoc = gl.getAttribLocation(program, "aPos");
|
|
6939
|
+
gl.enableVertexAttribArray(posLoc);
|
|
6940
|
+
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
|
|
6941
|
+
const tex = gl.createTexture();
|
|
6942
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
6943
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, GRID, GRID, 0, gl.RGBA, gl.FLOAT, null);
|
|
6944
|
+
const fbo = gl.createFramebuffer();
|
|
6945
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
6946
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
|
6947
|
+
gl.viewport(0, 0, GRID, GRID);
|
|
6948
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
6949
|
+
const out = new Float32Array(GRID * GRID * 4);
|
|
6950
|
+
gl.readPixels(0, 0, GRID, GRID, gl.RGBA, gl.FLOAT, out);
|
|
6951
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
6952
|
+
return out;
|
|
6953
|
+
}
|
|
6954
|
+
function runBiasCases(gl) {
|
|
6955
|
+
const results = [];
|
|
6956
|
+
for (const c of parityCases) {
|
|
6957
|
+
const strengths = c.stack.map((i) => getDeformer(i.type).glsl?.strength !== void 0);
|
|
6958
|
+
if (!strengths.every((h) => h === strengths[0])) continue;
|
|
6959
|
+
const scales = strengths[0];
|
|
6960
|
+
const at0 = runCaseOnGPU(gl, c, 0);
|
|
6961
|
+
const reference = scales ? null : runCaseOnGPU(gl, c, 1);
|
|
6962
|
+
let maxError = 0;
|
|
6963
|
+
for (let row = 0; row < GRID; row++) {
|
|
6964
|
+
for (let col = 0; col < GRID; col++) {
|
|
6965
|
+
const i4 = (row * GRID + col) * 4;
|
|
6966
|
+
const ex = reference ? reference[i4] : (col / (GRID - 1) - 0.5) * c.sheet.width;
|
|
6967
|
+
const ey = reference ? reference[i4 + 1] : (row / (GRID - 1) - 0.5) * c.sheet.height;
|
|
6968
|
+
const ez = reference ? reference[i4 + 2] : 0;
|
|
6969
|
+
maxError = Math.max(
|
|
6970
|
+
maxError,
|
|
6971
|
+
Math.abs(at0[i4] - ex),
|
|
6972
|
+
Math.abs(at0[i4 + 1] - ey),
|
|
6973
|
+
Math.abs(at0[i4 + 2] - ez)
|
|
6974
|
+
);
|
|
6975
|
+
}
|
|
6976
|
+
}
|
|
6977
|
+
results.push({
|
|
6978
|
+
name: `bias: ${c.name} \u2192 ${scales ? "flat at 0" : "ignores bias"}`,
|
|
6979
|
+
maxError,
|
|
6980
|
+
pass: maxError < PARITY_EPSILON
|
|
6981
|
+
});
|
|
6982
|
+
}
|
|
6983
|
+
return results;
|
|
6984
|
+
}
|
|
6985
|
+
function runParityHarness(canvas) {
|
|
6986
|
+
const cnv = canvas ?? document.createElement("canvas");
|
|
6987
|
+
const gl = cnv.getContext("webgl2");
|
|
6988
|
+
if (!gl) throw new Error("[paperlab parity] WebGL2 unavailable");
|
|
6989
|
+
if (!gl.getExtension("EXT_color_buffer_float")) {
|
|
6990
|
+
throw new Error("[paperlab parity] EXT_color_buffer_float unavailable");
|
|
6991
|
+
}
|
|
6992
|
+
const point = new THREE18.Vector3();
|
|
6993
|
+
const parity = parityCases.map((c) => {
|
|
6994
|
+
const gpu = runCaseOnGPU(gl, c);
|
|
6995
|
+
let maxError = 0;
|
|
6996
|
+
for (let row = 0; row < GRID; row++) {
|
|
6997
|
+
for (let col = 0; col < GRID; col++) {
|
|
6998
|
+
const u = col / (GRID - 1);
|
|
6999
|
+
const v = row / (GRID - 1);
|
|
7000
|
+
point.set((u - 0.5) * c.sheet.width, (v - 0.5) * c.sheet.height, 0);
|
|
7001
|
+
displacePoint(point, u, v, c.stack, { t: c.t, sheet: c.sheet });
|
|
7002
|
+
const i4 = (row * GRID + col) * 4;
|
|
7003
|
+
maxError = Math.max(
|
|
7004
|
+
maxError,
|
|
7005
|
+
Math.abs(gpu[i4] - point.x),
|
|
7006
|
+
Math.abs(gpu[i4 + 1] - point.y),
|
|
7007
|
+
Math.abs(gpu[i4 + 2] - point.z)
|
|
7008
|
+
);
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
return { name: c.name, maxError, pass: maxError < PARITY_EPSILON };
|
|
7012
|
+
});
|
|
7013
|
+
return [...parity, ...runBiasCases(gl)];
|
|
7014
|
+
}
|
|
7015
|
+
|
|
5082
7016
|
// src/config/field-export.ts
|
|
5083
7017
|
var MOTION_DEFAULTS = { driver: "autoplay", speed: 0.5 };
|
|
5084
7018
|
var ENTRANCE_DEFAULTS = { type: "rise", stagger: 0.06, duration: 0.9 };
|
|
@@ -5120,12 +7054,15 @@ function diffFieldProps(input) {
|
|
|
5120
7054
|
}
|
|
5121
7055
|
var LAYOUT_PHRASES = {
|
|
5122
7056
|
ring: "a ring you can see around",
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
wall: "a
|
|
5127
|
-
|
|
5128
|
-
|
|
7057
|
+
fan: "a fanned swatch deck hinged at one corner",
|
|
7058
|
+
spread: "a stack slid sideways, each sheet bowing further than the last",
|
|
7059
|
+
pile: "a heap on a desk, the sheets underneath pressed flat",
|
|
7060
|
+
wall: "a pinned studio wall of sheets, none hanging quite square",
|
|
7061
|
+
spill: "a dropped stack caught mid-air, each sheet bent its own way",
|
|
7062
|
+
sweep: "a specimen chart of one sheet at every stage of its deformation",
|
|
7063
|
+
book: "an open book, its pages splayed from a shared spine",
|
|
7064
|
+
accordion: "one continuous strip folded into a concertina",
|
|
7065
|
+
rack: "prints stood in a row and leaning back against a wall",
|
|
5129
7066
|
sheet: "a stamp-block grid on a shared backing sheet"
|
|
5130
7067
|
};
|
|
5131
7068
|
function describeFieldConfig(input) {
|
|
@@ -5146,7 +7083,7 @@ function describeFieldConfig(input) {
|
|
|
5146
7083
|
parts.push("hovering peels/reacts per paper (interaction states)");
|
|
5147
7084
|
if (zones.length > 0) {
|
|
5148
7085
|
parts.push(
|
|
5149
|
-
`dragging one past its tear threshold detaches it to carry to the ${zones.map((
|
|
7086
|
+
`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)`
|
|
5150
7087
|
);
|
|
5151
7088
|
}
|
|
5152
7089
|
} else {
|
|
@@ -5275,8 +7212,12 @@ export {
|
|
|
5275
7212
|
PaperMaterial,
|
|
5276
7213
|
PaperMesh,
|
|
5277
7214
|
PaperMirror,
|
|
7215
|
+
PaperStage,
|
|
7216
|
+
PaperStageScene,
|
|
5278
7217
|
PaperStateMachine,
|
|
5279
7218
|
SHEET_LIFT,
|
|
7219
|
+
TRANSMISSION_GAIN,
|
|
7220
|
+
accordion,
|
|
5280
7221
|
applyDeformerStack,
|
|
5281
7222
|
atlasGrid,
|
|
5282
7223
|
backContentSchema,
|
|
@@ -5284,6 +7225,7 @@ export {
|
|
|
5284
7225
|
behaviorConfigSchema,
|
|
5285
7226
|
bend,
|
|
5286
7227
|
bendOptionsSchema,
|
|
7228
|
+
book,
|
|
5287
7229
|
buildAgentPayload,
|
|
5288
7230
|
buildDisplacementGLSL,
|
|
5289
7231
|
buildFieldAgentPayload,
|
|
@@ -5291,32 +7233,45 @@ export {
|
|
|
5291
7233
|
buildFieldFragmentShader,
|
|
5292
7234
|
buildFieldVertexShader,
|
|
5293
7235
|
buildJsxSnippet,
|
|
7236
|
+
buildStageAgentPayload,
|
|
7237
|
+
buildStageComponentSource,
|
|
5294
7238
|
carry,
|
|
5295
7239
|
carryDrive,
|
|
5296
7240
|
carryOptionsSchema,
|
|
5297
|
-
cascade,
|
|
5298
7241
|
clothConfigSchema,
|
|
7242
|
+
colonnade,
|
|
5299
7243
|
composeSurface,
|
|
5300
7244
|
contentSchema,
|
|
5301
7245
|
contentText,
|
|
5302
7246
|
coreStateNames,
|
|
5303
7247
|
cornerNames,
|
|
5304
7248
|
createSheetGeometry,
|
|
7249
|
+
createWalkPath,
|
|
7250
|
+
crumple,
|
|
7251
|
+
crumpleBehavior,
|
|
7252
|
+
crumpleBehaviorOptionsSchema,
|
|
7253
|
+
crumpleOptionsSchema,
|
|
5305
7254
|
curl,
|
|
5306
7255
|
curlOptionsSchema,
|
|
5307
7256
|
dampTo,
|
|
5308
|
-
deck,
|
|
5309
7257
|
deformerInstanceSchema,
|
|
5310
7258
|
describeConfig,
|
|
5311
7259
|
describeFieldConfig,
|
|
7260
|
+
describeStage,
|
|
5312
7261
|
diffConfig,
|
|
5313
7262
|
diffFieldProps,
|
|
7263
|
+
diffStage,
|
|
5314
7264
|
displacePoint,
|
|
5315
7265
|
distinctFieldPresets,
|
|
7266
|
+
drape,
|
|
7267
|
+
drapeOptionsSchema,
|
|
5316
7268
|
drawBacking,
|
|
5317
7269
|
fall,
|
|
5318
7270
|
fallOptionsSchema,
|
|
7271
|
+
fan,
|
|
7272
|
+
fieldBounds,
|
|
5319
7273
|
fieldKeyboardStep,
|
|
7274
|
+
fitCamera,
|
|
5320
7275
|
flattenNumeric,
|
|
5321
7276
|
flight,
|
|
5322
7277
|
flightOptionsSchema,
|
|
@@ -5333,12 +7288,13 @@ export {
|
|
|
5333
7288
|
getLayout,
|
|
5334
7289
|
getLightingPreset,
|
|
5335
7290
|
getPreset,
|
|
7291
|
+
getStagePreset,
|
|
5336
7292
|
getStock,
|
|
7293
|
+
getWalk,
|
|
5337
7294
|
groupFieldPapers,
|
|
5338
7295
|
gust,
|
|
5339
7296
|
hang,
|
|
5340
7297
|
hangOptionsSchema,
|
|
5341
|
-
helix,
|
|
5342
7298
|
idleNames,
|
|
5343
7299
|
idlePresets,
|
|
5344
7300
|
isBuiltinPreset,
|
|
@@ -5351,6 +7307,7 @@ export {
|
|
|
5351
7307
|
listDeformers,
|
|
5352
7308
|
listLayouts,
|
|
5353
7309
|
listPresets,
|
|
7310
|
+
listStagePresets,
|
|
5354
7311
|
makeGoboTexture,
|
|
5355
7312
|
mergeConfig,
|
|
5356
7313
|
mergeWithDeletes,
|
|
@@ -5364,8 +7321,11 @@ export {
|
|
|
5364
7321
|
peelOptionsSchema,
|
|
5365
7322
|
physicsNames,
|
|
5366
7323
|
physicsSchema,
|
|
7324
|
+
pile,
|
|
7325
|
+
qualityNames,
|
|
5367
7326
|
quantizeProgress,
|
|
5368
7327
|
quantizeTime,
|
|
7328
|
+
rack,
|
|
5369
7329
|
receiptContentSchema,
|
|
5370
7330
|
receiptTotals,
|
|
5371
7331
|
recordStateOverride,
|
|
@@ -5374,6 +7334,7 @@ export {
|
|
|
5374
7334
|
registerLayout,
|
|
5375
7335
|
registerPreset,
|
|
5376
7336
|
resolveConfig,
|
|
7337
|
+
resolveDeformerStack,
|
|
5377
7338
|
resolveFieldSlotConfig,
|
|
5378
7339
|
resolveMode,
|
|
5379
7340
|
resolveSegments,
|
|
@@ -5382,7 +7343,6 @@ export {
|
|
|
5382
7343
|
roll,
|
|
5383
7344
|
rollOptionsSchema,
|
|
5384
7345
|
runParityHarness,
|
|
5385
|
-
scatter,
|
|
5386
7346
|
sceneSchema,
|
|
5387
7347
|
serializePreset,
|
|
5388
7348
|
sheet,
|
|
@@ -5390,20 +7350,28 @@ export {
|
|
|
5390
7350
|
sheetLayoutSchema,
|
|
5391
7351
|
sheetSchema,
|
|
5392
7352
|
sheetSlotXY,
|
|
7353
|
+
shotNames,
|
|
5393
7354
|
silhouetteRects,
|
|
7355
|
+
spill,
|
|
7356
|
+
spread,
|
|
5394
7357
|
stackMinSegments,
|
|
5395
7358
|
stackUniformValues,
|
|
7359
|
+
stagePresets,
|
|
7360
|
+
stageSchema,
|
|
5396
7361
|
stateDefSchema,
|
|
5397
7362
|
stateEventTransitions,
|
|
5398
7363
|
stateTransitionSchema,
|
|
5399
7364
|
stockNames,
|
|
5400
7365
|
stockSchema,
|
|
5401
7366
|
stocks,
|
|
7367
|
+
stringifyStage,
|
|
5402
7368
|
stripStates,
|
|
5403
7369
|
supportsWebGL,
|
|
5404
7370
|
surfaceSchema,
|
|
7371
|
+
sweep,
|
|
5405
7372
|
tornEdgesOnDetach,
|
|
5406
|
-
|
|
7373
|
+
translucencyUniforms,
|
|
7374
|
+
translucencyValues,
|
|
5407
7375
|
uniquePresetName,
|
|
5408
7376
|
unregisterPreset,
|
|
5409
7377
|
unroll,
|
|
@@ -5411,6 +7379,9 @@ export {
|
|
|
5411
7379
|
useContentAtlas,
|
|
5412
7380
|
usePaperStates,
|
|
5413
7381
|
usePrefersReducedMotion,
|
|
7382
|
+
walkNameFor,
|
|
7383
|
+
walkNames,
|
|
7384
|
+
walks,
|
|
5414
7385
|
wall,
|
|
5415
7386
|
wave,
|
|
5416
7387
|
waveOptionsSchema,
|