paperlab 0.5.0 → 0.5.2
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 +4 -2
- package/dist/{chunk-6IADJZX5.js → chunk-HRXQTJFS.js} +45 -28
- package/dist/chunk-HRXQTJFS.js.map +1 -0
- package/dist/index.cjs +107 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/stage.cjs +258 -234
- package/dist/stage.cjs.map +1 -1
- package/dist/stage.js +17 -8
- package/dist/stage.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-6IADJZX5.js.map +0 -1
package/README.md
CHANGED
|
@@ -144,7 +144,7 @@ And it is navigable rather than a video. It drifts on its own until you touch it
|
|
|
144
144
|
- **Physics** — curated idle motion (`float`, `tumble`, `dangle`, `taped`, `breeze`) that composes with behaviors, and a verlet **cloth** mode: pin the top edge, add wind, grab the sheet and pull. Cloth and behaviors are mutually exclusive by schema — cloth owns the vertices.
|
|
145
145
|
- **Interaction states** — a preset can carry `states`: overrides-on-base diffs keyed `rest` / `hover` / `pressed` / `picked` / `placed`, with the triggers built in. Drag a stamp past its threshold and it tears off its sheet (the perforation edges facing its neighbours flip to torn), release it over a `<DropZone>` and it settles, release it anywhere else and it flutters home. The whole flow is reachable from the keyboard: focus a paper, Enter picks, arrows move between zones, Enter places, Escape returns it.
|
|
146
146
|
- **Hardware that holds the paper up** — thread to the ceiling or a rod across the top edge, gripped by a clip or a peg. A hung thing that shows what holds it stops reading as a rectangle that happens to float.
|
|
147
|
-
- **Presets** —
|
|
147
|
+
- **Presets** — 16 paper presets and 6 stage presets, and everything serializes to `.paper` JSON validated by a zod schema. Diffable, forkable, shareable.
|
|
148
148
|
- **Agent-first export** — the editor's **Copy for AI** button produces a self-contained brief you paste into a coding agent: install line, inlined component, placement contract, and a verification step the agent can self-check. See [AGENTS.md](https://github.com/NourMtir0722/Paperlab/blob/main/AGENTS.md) and [docs/llms.txt](https://github.com/NourMtir0722/Paperlab/blob/main/docs/llms.txt).
|
|
149
149
|
- **Accessible by default** — `prefers-reduced-motion` freezes behaviors at their pose and disables physics and entrances, a hidden DOM mirror carries the content for screen readers and find-in-page, and a flat DOM fallback renders when WebGL isn't available.
|
|
150
150
|
|
|
@@ -221,13 +221,15 @@ pnpm test # 700+ unit tests — deformer math, schema, cloth, layouts,
|
|
|
221
221
|
pnpm test:parity # 37 golden-vector cases: every deformer's GLSL twin vs its JS twin
|
|
222
222
|
pnpm test:drive # the stage really walks when you drag, wheel or arrow it
|
|
223
223
|
pnpm test:share # sculpt → link → a browser that has never seen the paper
|
|
224
|
+
pnpm test:dropdown # every dropdown option is reachable, including below the fold
|
|
225
|
+
pnpm test:route # the site root sends each device to the app built for it
|
|
224
226
|
pnpm typecheck
|
|
225
227
|
pnpm lint
|
|
226
228
|
pnpm knip # dead code and unused exports
|
|
227
229
|
pnpm build
|
|
228
230
|
```
|
|
229
231
|
|
|
230
|
-
Anything that needs a real GPU, real pointer events or a second browser profile is a browser harness in `tools/` rather than a unit test. All
|
|
232
|
+
Anything that needs a real GPU, real pointer events or a second browser profile is a browser harness in `tools/` rather than a unit test. All six test commands are CI gates, along with `publint` and `are-the-types-wrong` on the published package.
|
|
231
233
|
|
|
232
234
|
### Measurement
|
|
233
235
|
|
|
@@ -5664,8 +5664,23 @@ registerLayout(sheet);
|
|
|
5664
5664
|
// src/PaperField.tsx
|
|
5665
5665
|
import * as THREE16 from "three";
|
|
5666
5666
|
import { gsap as gsap5 } from "gsap";
|
|
5667
|
-
import { Canvas, useFrame as useFrame5, useThree as
|
|
5668
|
-
import { forwardRef as forwardRef2, useEffect as
|
|
5667
|
+
import { Canvas, useFrame as useFrame5, useThree as useThree6 } from "@react-three/fiber";
|
|
5668
|
+
import { forwardRef as forwardRef2, useEffect as useEffect14, useMemo as useMemo9, useRef as useRef8 } from "react";
|
|
5669
|
+
|
|
5670
|
+
// src/scene/release.tsx
|
|
5671
|
+
import { useThree as useThree4 } from "@react-three/fiber";
|
|
5672
|
+
import { useEffect as useEffect9 } from "react";
|
|
5673
|
+
function ReleaseContextOnUnmount() {
|
|
5674
|
+
const gl = useThree4((s) => s.gl);
|
|
5675
|
+
useEffect9(
|
|
5676
|
+
() => () => {
|
|
5677
|
+
gl.forceContextLoss();
|
|
5678
|
+
gl.dispose();
|
|
5679
|
+
},
|
|
5680
|
+
[gl]
|
|
5681
|
+
);
|
|
5682
|
+
return null;
|
|
5683
|
+
}
|
|
5669
5684
|
|
|
5670
5685
|
// src/field/slots.ts
|
|
5671
5686
|
var EMPTY_SET = /* @__PURE__ */ new Set();
|
|
@@ -5716,12 +5731,12 @@ function resolveFieldSlotConfig(slot, fallback, index, layoutId, layoutOptions)
|
|
|
5716
5731
|
import * as THREE13 from "three";
|
|
5717
5732
|
import { gsap as gsap3 } from "gsap";
|
|
5718
5733
|
import { useFrame as useFrame3 } from "@react-three/fiber";
|
|
5719
|
-
import { useEffect as
|
|
5734
|
+
import { useEffect as useEffect11, useMemo as useMemo6, useRef as useRef6 } from "react";
|
|
5720
5735
|
import CustomShaderMaterial2 from "three-custom-shader-material";
|
|
5721
5736
|
|
|
5722
5737
|
// src/content/atlas.ts
|
|
5723
5738
|
import * as THREE12 from "three";
|
|
5724
|
-
import { useEffect as
|
|
5739
|
+
import { useEffect as useEffect10, useState as useState5 } from "react";
|
|
5725
5740
|
var MAX_ATLAS = 4096;
|
|
5726
5741
|
function atlasGrid(count, aspect = 1) {
|
|
5727
5742
|
const cols = Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * Math.max(aspect, 0.01)))));
|
|
@@ -5730,7 +5745,7 @@ function atlasGrid(count, aspect = 1) {
|
|
|
5730
5745
|
function useContentAtlas(contents, sheet2, stock) {
|
|
5731
5746
|
const [atlas, setAtlas] = useState5(null);
|
|
5732
5747
|
const stableContents = useStable(contents);
|
|
5733
|
-
|
|
5748
|
+
useEffect10(() => {
|
|
5734
5749
|
let disposed = false;
|
|
5735
5750
|
const aspect = sheet2.height / sheet2.width;
|
|
5736
5751
|
const { cols, rows } = atlasGrid(contents.length, aspect);
|
|
@@ -5982,7 +5997,7 @@ function FieldGroup({
|
|
|
5982
5997
|
geo.setAttribute("aBias", new THREE13.InstancedBufferAttribute(bias, 1));
|
|
5983
5998
|
return geo;
|
|
5984
5999
|
}, [JSON.stringify(config.sheet), structureKey, count, autoCeiling]);
|
|
5985
|
-
|
|
6000
|
+
useEffect11(() => () => geometry.dispose(), [geometry]);
|
|
5986
6001
|
const atlas = useContentAtlas(contents, config.sheet, stock);
|
|
5987
6002
|
const shader = useMemo6(() => {
|
|
5988
6003
|
const composed = buildDisplacementGLSL(initialStack, config.sheet);
|
|
@@ -6013,19 +6028,19 @@ function FieldGroup({
|
|
|
6013
6028
|
config.surface.showThrough,
|
|
6014
6029
|
config.surface.translucency
|
|
6015
6030
|
]);
|
|
6016
|
-
|
|
6031
|
+
useEffect11(() => {
|
|
6017
6032
|
const values = translucencyValues(config.surface.translucency ?? stock.translucency, rig);
|
|
6018
6033
|
shader.uniforms.uTranslucency.value = values.translucency;
|
|
6019
6034
|
shader.uniforms.uBackLightDir.value.copy(values.direction);
|
|
6020
6035
|
shader.uniforms.uBackLightColor.value.copy(values.color);
|
|
6021
6036
|
shader.uniforms.uAmbientTransmission.value = values.ambient;
|
|
6022
6037
|
}, [shader, rig, config.surface.translucency, stock.translucency]);
|
|
6023
|
-
|
|
6038
|
+
useEffect11(() => {
|
|
6024
6039
|
if (!atlas) return;
|
|
6025
6040
|
shader.uniforms.uAtlas.value = atlas.texture;
|
|
6026
6041
|
shader.uniforms.uAtlasGrid.value.set(atlas.cols, atlas.rows);
|
|
6027
6042
|
}, [atlas, shader]);
|
|
6028
|
-
|
|
6043
|
+
useEffect11(() => {
|
|
6029
6044
|
if (!behavior || shared.reduced) return;
|
|
6030
6045
|
const state = { p: progressRef.current };
|
|
6031
6046
|
const tween = gsap3.to(state, {
|
|
@@ -6190,7 +6205,7 @@ var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
|
|
6190
6205
|
|
|
6191
6206
|
// src/field/backingSheet.tsx
|
|
6192
6207
|
import * as THREE14 from "three";
|
|
6193
|
-
import { useEffect as
|
|
6208
|
+
import { useEffect as useEffect12, useMemo as useMemo7 } from "react";
|
|
6194
6209
|
|
|
6195
6210
|
// src/content/backing.ts
|
|
6196
6211
|
function silhouetteRects(o, count) {
|
|
@@ -6273,12 +6288,12 @@ function BackingSheet({
|
|
|
6273
6288
|
}, [width, height]);
|
|
6274
6289
|
const texture = useMemo7(() => canvas ? new THREE14.CanvasTexture(canvas) : null, [canvas]);
|
|
6275
6290
|
const removedKey = [...removed].sort((a, b) => a - b).join(",");
|
|
6276
|
-
|
|
6291
|
+
useEffect12(() => {
|
|
6277
6292
|
if (!canvas || !texture) return;
|
|
6278
6293
|
drawBacking(canvas, { options, count, tint: BACKING_TINT, removed });
|
|
6279
6294
|
texture.needsUpdate = true;
|
|
6280
6295
|
}, [canvas, texture, JSON.stringify(options), count, removedKey]);
|
|
6281
|
-
|
|
6296
|
+
useEffect12(() => () => texture?.dispose(), [texture]);
|
|
6282
6297
|
return /* @__PURE__ */ jsxs4("mesh", { receiveShadow: true, children: [
|
|
6283
6298
|
/* @__PURE__ */ jsx8("planeGeometry", { args: [width, height] }),
|
|
6284
6299
|
/* @__PURE__ */ jsx8("meshStandardMaterial", { map: texture, color: "#ffffff", roughness: 0.92, metalness: 0 })
|
|
@@ -6288,8 +6303,8 @@ function BackingSheet({
|
|
|
6288
6303
|
// src/field/interactiveField.tsx
|
|
6289
6304
|
import * as THREE15 from "three";
|
|
6290
6305
|
import { gsap as gsap4 } from "gsap";
|
|
6291
|
-
import { useFrame as useFrame4, useThree as
|
|
6292
|
-
import { useContext as useContext3, useEffect as
|
|
6306
|
+
import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber";
|
|
6307
|
+
import { useContext as useContext3, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
|
|
6293
6308
|
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
6294
6309
|
var PICK_BEHAVIORS = /* @__PURE__ */ new Set(["peel", "carry"]);
|
|
6295
6310
|
function InteractiveField(props) {
|
|
@@ -6299,9 +6314,9 @@ function InteractiveField(props) {
|
|
|
6299
6314
|
const reduced = usePrefersReducedMotion(props.reducedMotion);
|
|
6300
6315
|
const contextRegistry = useContext3(DropZoneContext);
|
|
6301
6316
|
const registry4 = useMemo8(() => contextRegistry ?? new DropZoneRegistry(), [contextRegistry]);
|
|
6302
|
-
const camera =
|
|
6303
|
-
const gl =
|
|
6304
|
-
const controls =
|
|
6317
|
+
const camera = useThree5((s) => s.camera);
|
|
6318
|
+
const gl = useThree5((s) => s.gl);
|
|
6319
|
+
const controls = useThree5((s) => s.controls);
|
|
6305
6320
|
const [removed, setRemoved] = useState6(EMPTY_SET);
|
|
6306
6321
|
const [slotPatches, setSlotPatches] = useState6({});
|
|
6307
6322
|
const [slotStates, setSlotStates] = useState6({});
|
|
@@ -6510,7 +6525,7 @@ function InteractiveField(props) {
|
|
|
6510
6525
|
if (zone) settleInto(carried, zone);
|
|
6511
6526
|
else returnHome(carried);
|
|
6512
6527
|
};
|
|
6513
|
-
|
|
6528
|
+
useEffect13(() => {
|
|
6514
6529
|
const move = (e) => onPointerMoveRef.current(e);
|
|
6515
6530
|
const up = (e) => onPointerUpRef.current(e);
|
|
6516
6531
|
window.addEventListener("pointermove", move);
|
|
@@ -6583,7 +6598,7 @@ function InteractiveField(props) {
|
|
|
6583
6598
|
zoneIds: () => zonesLive().map((z25) => z25.id),
|
|
6584
6599
|
slotState: (slot) => slotStates[slot] ?? "rest"
|
|
6585
6600
|
};
|
|
6586
|
-
|
|
6601
|
+
useEffect13(() => {
|
|
6587
6602
|
const ref = props.a11yRef;
|
|
6588
6603
|
if (!ref) return;
|
|
6589
6604
|
ref.current = {
|
|
@@ -6814,11 +6829,11 @@ var PaperFieldMesh = forwardRef2(
|
|
|
6814
6829
|
const mountTimeRef = useRef8(-1);
|
|
6815
6830
|
const morphRef = useRef8({ from: null, t: 1 });
|
|
6816
6831
|
const prevLayout = useRef8({ id: layoutId, options: layoutOptions });
|
|
6817
|
-
const gl =
|
|
6832
|
+
const gl = useThree6((s) => s.gl);
|
|
6818
6833
|
const driver = reduced ? "none" : props.motion?.driver ?? "autoplay";
|
|
6819
6834
|
const speed = props.motion?.speed ?? 0.5;
|
|
6820
6835
|
const entranceType = reduced ? "none" : props.entrance?.type ?? "rise";
|
|
6821
|
-
|
|
6836
|
+
useEffect14(() => {
|
|
6822
6837
|
const prev = prevLayout.current;
|
|
6823
6838
|
if (prev.id !== layoutId || JSON.stringify(prev.options) !== JSON.stringify(layoutOptions)) {
|
|
6824
6839
|
morphRef.current = { from: prev, t: 0 };
|
|
@@ -6826,7 +6841,7 @@ var PaperFieldMesh = forwardRef2(
|
|
|
6826
6841
|
prevLayout.current = { id: layoutId, options: layoutOptions };
|
|
6827
6842
|
}
|
|
6828
6843
|
}, [layoutId, layoutOptions]);
|
|
6829
|
-
|
|
6844
|
+
useEffect14(() => {
|
|
6830
6845
|
if (driver !== "drag") return;
|
|
6831
6846
|
const el = gl.domElement;
|
|
6832
6847
|
let lastX = null;
|
|
@@ -6910,9 +6925,9 @@ var PaperFieldMesh = forwardRef2(
|
|
|
6910
6925
|
}
|
|
6911
6926
|
);
|
|
6912
6927
|
function FitCamera(meshProps) {
|
|
6913
|
-
const camera =
|
|
6914
|
-
const width =
|
|
6915
|
-
const height =
|
|
6928
|
+
const camera = useThree6((s) => s.camera);
|
|
6929
|
+
const width = useThree6((s) => s.size.width);
|
|
6930
|
+
const height = useThree6((s) => s.size.height);
|
|
6916
6931
|
const papersProp = useStable(meshProps.papers ?? null);
|
|
6917
6932
|
const imagesProp = useStable(meshProps.images ?? null);
|
|
6918
6933
|
const presetProp = useStable(meshProps.preset ?? null);
|
|
@@ -6925,7 +6940,7 @@ function FitCamera(meshProps) {
|
|
|
6925
6940
|
const options = resolveLayoutOptions(layoutId, layout, optionsProp, sheet2);
|
|
6926
6941
|
return { layout, n: papers.length, options, sheet: sheet2 ?? DEFAULT_SHEET };
|
|
6927
6942
|
}, [papersProp, imagesProp, presetProp, meshProps.layout, optionsProp]);
|
|
6928
|
-
|
|
6943
|
+
useEffect14(() => {
|
|
6929
6944
|
if (!(camera instanceof THREE16.PerspectiveCamera)) return;
|
|
6930
6945
|
const { position, target } = fitCamera(
|
|
6931
6946
|
field.layout,
|
|
@@ -6961,7 +6976,8 @@ var PaperField = forwardRef2(function PaperField2({ children, className, style,
|
|
|
6961
6976
|
/* @__PURE__ */ jsx11(PaperBackdrop, { backdrop: rig.backdrop }),
|
|
6962
6977
|
/* @__PURE__ */ jsx11(PaperLighting, { preset: rig.lighting, light: rig.light, floor: -2.4, scale: 14 }),
|
|
6963
6978
|
/* @__PURE__ */ jsx11(PaperFieldMesh, { ref, a11yControllerRef: a11yRef, ...meshProps }),
|
|
6964
|
-
children
|
|
6979
|
+
children,
|
|
6980
|
+
/* @__PURE__ */ jsx11(ReleaseContextOnUnmount, {})
|
|
6965
6981
|
] }),
|
|
6966
6982
|
interactive && /* @__PURE__ */ jsx11(FieldKeyboardMirror, { papers, controller: a11yRef })
|
|
6967
6983
|
] }) });
|
|
@@ -7197,6 +7213,7 @@ export {
|
|
|
7197
7213
|
cssColorOr,
|
|
7198
7214
|
PaperLighting,
|
|
7199
7215
|
PaperBackdrop,
|
|
7216
|
+
ReleaseContextOnUnmount,
|
|
7200
7217
|
DropZone,
|
|
7201
7218
|
sheetLayoutSchema,
|
|
7202
7219
|
buildDisplacementGLSL,
|
|
@@ -7214,4 +7231,4 @@ export {
|
|
|
7214
7231
|
describeConfig,
|
|
7215
7232
|
buildAgentPayload
|
|
7216
7233
|
};
|
|
7217
|
-
//# sourceMappingURL=chunk-
|
|
7234
|
+
//# sourceMappingURL=chunk-HRXQTJFS.js.map
|