@limrun/ui 0.16.0 → 0.17.0-rc.1

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.
Files changed (58) hide show
  1. package/README.md +49 -0
  2. package/dist/GLTFLoader-BKnsBFsj.js +1 -0
  3. package/dist/GLTFLoader-DW6wpvyR.mjs +1679 -0
  4. package/dist/components/duo-button-hinge.d.ts +16 -0
  5. package/dist/components/duo-controls.d.ts +21 -0
  6. package/dist/components/duo-flat-frame.d.ts +7 -0
  7. package/dist/components/duo-flat-geometry.d.ts +52 -0
  8. package/dist/components/duo-fold-motion.d.ts +27 -0
  9. package/dist/components/duo-fold-renderer.d.ts +19 -0
  10. package/dist/components/duo-fold-timing.d.ts +1 -0
  11. package/dist/components/duo-frame.d.ts +16 -0
  12. package/dist/components/duo-hardware.d.ts +20 -0
  13. package/dist/components/duo-model.d.ts +19 -0
  14. package/dist/components/duo-screen-handoff.d.ts +4 -0
  15. package/dist/components/duo-view.d.ts +4 -0
  16. package/dist/components/remote-control.d.ts +5 -1
  17. package/dist/core/duo.d.ts +63 -0
  18. package/dist/duo-fold-renderer-BXejNHcY.mjs +257 -0
  19. package/dist/duo-fold-renderer-DdltAnF7.js +53 -0
  20. package/dist/duo-frame-BzyYljix.mjs +26767 -0
  21. package/dist/duo-frame-DjfA4gG-.js +4059 -0
  22. package/dist/duo-view-COZg8ieG.js +1 -0
  23. package/dist/duo-view-o1yxIzxu.mjs +654 -0
  24. package/dist/index.cjs +1 -1
  25. package/dist/index.css +1 -1
  26. package/dist/index.js +2 -2
  27. package/dist/lite.cjs +1 -1
  28. package/dist/lite.js +1 -1
  29. package/dist/remote-control-CQjSEVDF.mjs +2673 -0
  30. package/dist/remote-control-DVRXfCR2.js +1 -0
  31. package/package.json +4 -2
  32. package/src/components/duo-button-hinge.test.tsx +138 -0
  33. package/src/components/duo-button-hinge.ts +75 -0
  34. package/src/components/duo-controls.tsx +142 -0
  35. package/src/components/duo-flat-frame.tsx +145 -0
  36. package/src/components/duo-flat-geometry.test.ts +63 -0
  37. package/src/components/duo-flat-geometry.ts +97 -0
  38. package/src/components/duo-fold-motion.test.tsx +245 -0
  39. package/src/components/duo-fold-motion.tsx +192 -0
  40. package/src/components/duo-fold-renderer.test.ts +29 -0
  41. package/src/components/duo-fold-renderer.ts +397 -0
  42. package/src/components/duo-fold-timing.test.ts +44 -0
  43. package/src/components/duo-fold-timing.ts +9 -0
  44. package/src/components/duo-frame.tsx +579 -0
  45. package/src/components/duo-hardware.test.ts +61 -0
  46. package/src/components/duo-hardware.ts +61 -0
  47. package/src/components/duo-model.test.ts +243 -0
  48. package/src/components/duo-model.ts +607 -0
  49. package/src/components/duo-screen-handoff.ts +52 -0
  50. package/src/components/duo-view.test.tsx +104 -0
  51. package/src/components/duo-view.tsx +315 -0
  52. package/src/components/remote-control.css +310 -0
  53. package/src/components/remote-control.tsx +150 -14
  54. package/src/core/duo.test.ts +142 -0
  55. package/src/core/duo.ts +156 -0
  56. package/src/demo.tsx +14 -0
  57. package/dist/remote-control-BRgbD7yo.mjs +0 -2516
  58. package/dist/remote-control-VISrP1zI.js +0 -1
@@ -0,0 +1,16 @@
1
+ type Fold = {
2
+ from: number;
3
+ target: number;
4
+ startedAt: number;
5
+ };
6
+ /** Drive button folds through the slider's native angle queue. */
7
+ export declare function useDuoButtonHinge(nativeAngle: number, send: (angle: number) => Promise<void>, failed: (error: string | undefined) => void): {
8
+ motion: import('react').RefObject<Fold | undefined>;
9
+ target: number | undefined;
10
+ fold: () => void;
11
+ cancelFold: () => void;
12
+ changeAngle: (value: number) => void;
13
+ angle: number;
14
+ interacting: import('react').RefObject<boolean>;
15
+ };
16
+ export {};
@@ -0,0 +1,21 @@
1
+ import { default as React } from 'react';
2
+ import { DuoButton } from '../core/duo';
3
+ export declare function useDuoHinge(nativeAngle: number, send: (angle: number) => Promise<void>, failed: (error: string | undefined) => void): {
4
+ angle: number;
5
+ changeAngle: (value: number) => void;
6
+ interacting: React.RefObject<boolean>;
7
+ };
8
+ /** Both renderers use the same native fold and rotation controls. */
9
+ export declare function DuoControls({ angle, changeAngle, interacting, rotate, onFold, foldTarget, onHingeDrag, children, }: {
10
+ angle: number;
11
+ changeAngle: (value: number) => void;
12
+ interacting: React.MutableRefObject<boolean>;
13
+ rotate: () => void;
14
+ onFold?: () => void;
15
+ foldTarget?: number;
16
+ onHingeDrag?: () => void;
17
+ children?: React.ReactNode;
18
+ }): React.JSX.Element;
19
+ export declare function DuoHardwareIcon({ button }: {
20
+ button: DuoButton;
21
+ }): React.JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { default as React } from 'react';
2
+ /** Static silver rails and glass seals follow the same front silhouette as the 3D frame. */
3
+ export declare function DuoFlatFrame({ inner, width, height }: {
4
+ inner: boolean;
5
+ width: number;
6
+ height: number;
7
+ }): React.JSX.Element;
@@ -0,0 +1,52 @@
1
+ import { DuoButton } from '../core/duo';
2
+ type Edge = 'top' | 'right' | 'bottom' | 'left';
3
+ /** A front projection of the 3D body's dimensions, with room for its edge controls. */
4
+ export declare function flatFrameGeometry(inner: boolean, displayTurns: number, width: number, height: number): {
5
+ bodyWidth: number;
6
+ bodyHeight: 117.8;
7
+ screenWidth: number;
8
+ screenHeight: number;
9
+ scale: number;
10
+ turns: number;
11
+ buttons: {
12
+ button: DuoButton;
13
+ x: number;
14
+ y: number;
15
+ width: number;
16
+ height: number;
17
+ edge: Edge;
18
+ }[];
19
+ guides: {
20
+ targetX: number;
21
+ targetY: number;
22
+ x: number;
23
+ y: number;
24
+ button: DuoButton;
25
+ edge: Edge;
26
+ }[];
27
+ rect: {
28
+ min: {
29
+ x: number;
30
+ y: number;
31
+ };
32
+ max: {
33
+ x: number;
34
+ y: number;
35
+ };
36
+ };
37
+ iconSize: number;
38
+ };
39
+ /** Scale the glyph with the device while its surrounding click target stays 44 pixels. */
40
+ export declare function hardwareIconSize(rect: {
41
+ min: {
42
+ x: number;
43
+ y: number;
44
+ };
45
+ max: {
46
+ x: number;
47
+ y: number;
48
+ };
49
+ }): number;
50
+ /** The hover band joins the metal edge to its icons without covering the display. */
51
+ export declare function flatHoverEdge(x: number, y: number, rect: ReturnType<typeof flatFrameGeometry>['rect']): Edge | undefined;
52
+ export {};
@@ -0,0 +1,27 @@
1
+ import { default as React } from 'react';
2
+ import { flatFrameGeometry } from './duo-flat-geometry';
3
+ import { DuoOrientation } from '../core/duo';
4
+ type Frame = ReturnType<typeof flatFrameGeometry>;
5
+ type Pose = {
6
+ inner: boolean;
7
+ frame: Frame;
8
+ };
9
+ export type FoldMotion = {
10
+ from: Pose;
11
+ to: Pose;
12
+ before: HTMLCanvasElement;
13
+ after?: HTMLCanvasElement;
14
+ ready: boolean;
15
+ };
16
+ /** Animate confirmed display changes; resizing, rotation and reduced motion cancel the transition. */
17
+ export declare function useDuoFoldMotion(video: React.RefObject<HTMLVideoElement | null>, inner: boolean, frame: Frame, orientation?: DuoOrientation): {
18
+ motion: FoldMotion | undefined;
19
+ finish: () => void;
20
+ prepare: (targetInner: boolean) => void;
21
+ };
22
+ /** Load the tiny folding renderer only for a confirmed display change. */
23
+ export declare function DuoFoldMotion({ motion, onFinish }: {
24
+ motion: FoldMotion;
25
+ onFinish: () => void;
26
+ }): React.JSX.Element;
27
+ export {};
@@ -0,0 +1,19 @@
1
+ import { FoldMotion } from './duo-fold-motion';
2
+ export declare const FOLD_DURATION = 1200;
3
+ type Kind = 'metal' | 'seal' | 'inner' | 'outer' | 'back';
4
+ export type FoldMesh = {
5
+ side: -1 | 0 | 1;
6
+ kind: Kind;
7
+ vertices: Float32Array;
8
+ };
9
+ /** Low-segment beveled rails, three caps, glass seals and three screen polygons. */
10
+ export declare function foldMeshes(): FoldMesh[];
11
+ /** Both leaves turn toward the viewer before settling flat, following the reference's shallow V. */
12
+ export declare function foldPose(progress: number): {
13
+ left: number;
14
+ right: number;
15
+ shift: number;
16
+ };
17
+ /** The renderer owns its context, buffers and textures, and releases all of them after one fold. */
18
+ export declare function renderFold(canvas: HTMLCanvasElement, motion: FoldMotion, finished: () => void): () => void;
19
+ export {};
@@ -0,0 +1 @@
1
+ export declare function duoFoldAngle(from: number, target: number, seconds: number): number;
@@ -0,0 +1,16 @@
1
+ import { default as React } from 'react';
2
+ import { DuoButton, DuoState, DuoOrientation } from '../core/duo';
3
+ export interface DuoFrameProps {
4
+ modelUrl?: string;
5
+ outer: HTMLVideoElement | null;
6
+ inner: HTMLVideoElement | null;
7
+ state: DuoState;
8
+ setAngle: (degrees: number) => Promise<void>;
9
+ setOrientation: (orientation: DuoOrientation) => Promise<void>;
10
+ button: (button: DuoButton, down: boolean) => Promise<void>;
11
+ touch: (action: number, screenId: number, x: number, y: number) => void;
12
+ onKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
13
+ onKeyUp: React.KeyboardEventHandler<HTMLDivElement>;
14
+ }
15
+ /** A procedural titanium frame. Each glass panel carries its own native video texture. */
16
+ export default function DuoFrame(props: DuoFrameProps): React.JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { DuoButton } from '../core/duo';
2
+ import * as THREE from 'three';
3
+ type Edge = 'top' | 'bottom' | 'left' | 'right';
4
+ export type HardwareGuide = {
5
+ button: DuoButton;
6
+ edge: Edge;
7
+ x: number;
8
+ y: number;
9
+ };
10
+ /** Keep button hints outside the silhouette as the device folds, rotates and changes size. */
11
+ export declare function hardwareLayout(anchors: {
12
+ button: DuoButton;
13
+ point: THREE.Vector3;
14
+ outward: THREE.Vector3;
15
+ }[], bounds: THREE.Box3, camera: THREE.Camera, width: number, height: number): {
16
+ guides: HardwareGuide[];
17
+ rect: THREE.Box2;
18
+ iconSize: number;
19
+ };
20
+ export { flatHoverEdge as hardwareHoverEdge } from './duo-flat-geometry';
@@ -0,0 +1,19 @@
1
+ import { DuoButton } from '../core/duo';
2
+ import * as THREE from 'three';
3
+ /** Independent of WebGL so fold geometry and touch occlusion can be tested directly. */
4
+ export declare function createDuoModel(outerTexture: THREE.Texture, innerTexture: THREE.Texture, environment?: THREE.Texture): {
5
+ device: THREE.Group<THREE.Object3DEventMap>;
6
+ hitObjects: THREE.Object3D<THREE.Object3DEventMap>[];
7
+ setAngle: (degrees: number) => void;
8
+ buttonAnchors: () => {
9
+ button: DuoButton;
10
+ point: THREE.Vector3;
11
+ outward: THREE.Vector3;
12
+ }[];
13
+ getBounds: (bounds: THREE.Box3) => THREE.Box3;
14
+ pick: (ray: THREE.Raycaster) => THREE.Intersection<THREE.Object3D<THREE.Object3DEventMap>>;
15
+ loadAppearance: (url: string) => Promise<void>;
16
+ highlightButton: (name?: DuoButton, pressed?: boolean) => void;
17
+ animateButtons: (dt: number) => boolean;
18
+ dispose: () => void;
19
+ };
@@ -0,0 +1,4 @@
1
+ /** Capture two bounded stills per fold, without retaining a canvas or rendering loop. */
2
+ export declare function captureDuoFrame(video: HTMLVideoElement | null): HTMLCanvasElement | undefined;
3
+ /** Capture the first incoming frame without delaying for native fades or brightness changes. */
4
+ export declare function waitForDuoFrame(video: HTMLVideoElement | null, ready: (image: HTMLCanvasElement | undefined) => void): () => void;
@@ -0,0 +1,4 @@
1
+ import { default as React } from 'react';
2
+ import { DuoFrameProps } from './duo-frame';
3
+ /** Native video is the default; only an explicit 3D selection loads the renderer and body asset. */
4
+ export default function DuoView(props: DuoFrameProps): React.JSX.Element;
@@ -15,6 +15,10 @@ export interface RemoteControlProps {
15
15
  sessionId?: string;
16
16
  openUrl?: string;
17
17
  showFrame?: boolean;
18
+ /** Enables the official iPhone Duo's native displays and interactive folding frame. */
19
+ deviceModel?: 'iphone-duo';
20
+ /** Optional prepared Duo GLB, supplied and licensed by the host application. */
21
+ duoModelUrl?: string;
18
22
  autoReconnect?: boolean;
19
23
  /**
20
24
  * Fires once when the component concludes the instance is permanently
@@ -247,7 +251,7 @@ export interface ImperativeKeyboardEvent {
247
251
  export interface RemoteControlHandle {
248
252
  openUrl: (url: string) => void;
249
253
  sendKeyEvent: (event: ImperativeKeyboardEvent) => void;
250
- screenshot: () => Promise<ScreenshotData>;
254
+ screenshot: (display?: 'outer' | 'inner') => Promise<ScreenshotData>;
251
255
  terminateApp: (bundleId: string) => Promise<void>;
252
256
  reconnect: () => void;
253
257
  refreshAxTree: () => Promise<AxSnapshot>;
@@ -0,0 +1,63 @@
1
+ /** Native iPhone Duo display metadata. Pixel geometry comes from CoreSimulator. */
2
+ export interface DuoDisplay {
3
+ id: 'outer' | 'inner';
4
+ screenId: number;
5
+ trackId: string;
6
+ width: number;
7
+ height: number;
8
+ scale: number;
9
+ orientation: number;
10
+ }
11
+ export type DuoOrientation = 'portrait' | 'pud' | 'landscape-left' | 'landscape-right';
12
+ export interface DuoState {
13
+ orientation: DuoOrientation;
14
+ angleDegrees: number;
15
+ minAngleDegrees: number;
16
+ maxAngleDegrees: number;
17
+ displays: DuoDisplay[];
18
+ }
19
+ export declare const DUO_DIMENSIONS: {
20
+ readonly width: 164.6;
21
+ readonly height: 117.8;
22
+ readonly depth: 5.2;
23
+ };
24
+ /** Invert the CSS display rotation before sending native panel coordinates. */
25
+ export declare function flatDisplayTouch(x: number, y: number, turns: number): {
26
+ x: number;
27
+ y: number;
28
+ };
29
+ export declare function validHingeAngle(angle: number): boolean;
30
+ /** Mesh UVs use the native portrait panel; HID has a top-left origin. */
31
+ export declare function panelTouch(u: number, v: number): {
32
+ x: number;
33
+ y: number;
34
+ };
35
+ /** Locking the view never suppresses screen input, including accidental Alt presses. */
36
+ export declare function duoPointerMode(locked: boolean, screenHit: boolean, altKey: boolean): 'touch' | 'orbit' | 'ignore';
37
+ export declare function createDisplayTouchMessage(action: number, screenId: number, x: number, y: number): ArrayBuffer;
38
+ /** One request in flight; intermediate slider positions are replaced by the newest position. */
39
+ export declare class HingeSender {
40
+ private send;
41
+ private failed;
42
+ private latest;
43
+ private running;
44
+ private stopped;
45
+ constructor(send: (angle: number) => Promise<void>, failed: (error: unknown) => void);
46
+ set(angle: number): void;
47
+ stop(): void;
48
+ private flush;
49
+ }
50
+ export type DuoButton = 'side' | 'volumeUp' | 'volumeDown';
51
+ export declare const DUO_BUTTONS: Record<DuoButton, string>;
52
+ /** Keeps button edges ordered, including releases after cancellation or a failed press. */
53
+ export declare class DuoButtonSender {
54
+ private send;
55
+ private failed;
56
+ private held?;
57
+ private tail;
58
+ private timeout?;
59
+ constructor(send: (button: DuoButton, down: boolean) => Promise<void>, failed: (error: unknown) => void);
60
+ press(button: DuoButton): void;
61
+ release(): void;
62
+ private enqueue;
63
+ }
@@ -0,0 +1,257 @@
1
+ const C = 1200;
2
+ function U(p, l, T, e, t = 0) {
3
+ const o = [];
4
+ for (const [u, r, c, h] of [
5
+ [p / 2 - e, l / 2 - e, e, 0],
6
+ [-p / 2 + T, l / 2 - T, T, Math.PI / 2],
7
+ [-p / 2 + T, -l / 2 + T, T, Math.PI],
8
+ [p / 2 - e, -l / 2 + e, e, Math.PI * 1.5]
9
+ ])
10
+ for (let n = 0; n <= 8; n++) {
11
+ const i = h + n * Math.PI / 16, a = Math.cos(i), s = Math.sin(i);
12
+ o.push({ x: u + (c - t) * a, y: r + (c - t) * s, nx: a, ny: s });
13
+ }
14
+ return o;
15
+ }
16
+ function B() {
17
+ const p = [], l = (t, o, u) => p.push({ side: t, kind: o, vertices: new Float32Array(u) }), T = (t, o, u, r, c, h, n, i, a, s) => {
18
+ const x = [], d = (m, g, v, _) => x.push(m.x + u, m.y + r, g, m.nx * _, m.ny * _, v, 0, 0), E = Math.min(0.32, i / 3, a / 2, s / 2), I = [
19
+ [E, n, 0.707, 0.707],
20
+ [0, n - E, 0, 1],
21
+ [0, n - i + E, 0, 1],
22
+ [E, n - i, -0.707, 0.707]
23
+ ];
24
+ for (let m = 0; m < I.length - 1; m++) {
25
+ const g = I[m], v = I[m + 1], _ = U(c, h, a, s, g[0]), H = U(c, h, a, s, v[0]);
26
+ for (let F = 0; F < _.length; F++) {
27
+ const L = (F + 1) % _.length;
28
+ for (const [D, M] of [
29
+ [_[F], g],
30
+ [H[F], v],
31
+ [_[L], g],
32
+ [_[L], g],
33
+ [H[F], v],
34
+ [H[L], v]
35
+ ])
36
+ d(D, M[1], M[2], M[3]);
37
+ }
38
+ }
39
+ const A = U(c, h, a, s, E);
40
+ for (const [m, g] of [
41
+ [n, 1],
42
+ [n - i, -1]
43
+ ])
44
+ for (let v = 0; v < A.length; v++)
45
+ x.push(u, r, m, 0, 0, g, 0, 0), d(A[v], m, g, 0), d(A[(v + 1) % A.length], m, g, 0);
46
+ l(t, o, x);
47
+ }, e = (t, o, u, r, c, h, n, i) => {
48
+ const a = [], s = U(r, c, n, i), x = (d, E) => {
49
+ const I = d + u;
50
+ let A = 0, m = 0;
51
+ o === "inner" && (A = 0.5 - E / 110.8, m = 0.5 + I / 157.5), o === "outer" && (A = 0.5 - d / r, m = 0.5 + E / c), a.push(I, E, h, 0, 0, o === "outer" || o === "back" ? -1 : 1, A, m);
52
+ };
53
+ for (let d = 0; d < s.length; d++)
54
+ x(0, 0), x(s[d].x, s[d].y), x(s[(d + 1) % s.length].x, s[(d + 1) % s.length].y);
55
+ l(t, o, a);
56
+ };
57
+ for (const t of [-1, 1]) {
58
+ const o = t === -1 ? 8.4 : 1.25, u = t === 1 ? 8.4 : 1.25;
59
+ T(t, "metal", t * 82.3 / 2, 0, 82.3, 117.8, 0, 5.2, o, u), e(
60
+ t,
61
+ "seal",
62
+ t * 82.3 / 2,
63
+ 82.3 - 2.24,
64
+ 117.8 - 2.24,
65
+ 0.12,
66
+ t === -1 ? 7.15 : 0.2,
67
+ t === 1 ? 7.15 : 0.2
68
+ ), e(
69
+ t,
70
+ "inner",
71
+ t * (157.5 / 4 + 0.55),
72
+ 157.5 / 2 - 1.1,
73
+ 110.8,
74
+ 0.24,
75
+ t === -1 ? 5.3 : 0,
76
+ t === 1 ? 5.3 : 0
77
+ );
78
+ }
79
+ for (const [t, o, u] of [
80
+ ["metal", 117.8, -0.12],
81
+ ["seal", 117.8 - 2.24, 0.12],
82
+ ["inner", 110.8, 0.24]
83
+ ]) {
84
+ const r = [], c = (h, n) => r.push(h, n, u, 0, 0, 1, 0.5 - n / 110.8, 0.5 + h / 157.5);
85
+ for (let h = 0; h < 6; h++) {
86
+ const n = -1.1 + h * 2.2 / 6, i = n + 2.2 / 6;
87
+ for (const [a, s] of [
88
+ [n, -o / 2],
89
+ [i, -o / 2],
90
+ [n, o / 2],
91
+ [n, o / 2],
92
+ [i, -o / 2],
93
+ [i, o / 2]
94
+ ])
95
+ c(a, s);
96
+ }
97
+ l(0, t, r);
98
+ }
99
+ e(-1, "seal", -82.3 / 2, 82.3 - 2.24, 117.8 - 2.24, -5.2 - 0.12, 7.15, 0.65), e(-1, "outer", -82.3 / 2, 77.1, 112.2, -5.2 - 0.24, 6.6, 0.8), e(1, "back", 82.3 / 2, 82.3 - 1, 117.8 - 1, -5.2 - 0.12, 0.8, 8);
100
+ for (const t of [82.3 - 37, 82.3 - 25])
101
+ T(1, "metal", t, 117.8 / 2 + 0.4, 9.5, 0.8, -1.4, 2, 0.35, 0.35);
102
+ return T(1, "metal", 82.3 + 0.4, 21, 0.8, 13, -1.4, 2, 0.35, 0.35), p;
103
+ }
104
+ function N(p) {
105
+ const l = [
106
+ [0, 180, 0, -0.5],
107
+ [0.12, 176, -4, -0.46],
108
+ [0.24, 150, -8, -0.4],
109
+ [0.35, 90, -12, -0.32],
110
+ [0.48, 48, -22, -0.14],
111
+ [0.62, 30, -20, -0.04],
112
+ [0.8, 14, -10, 0],
113
+ [1, 0, 0, 0]
114
+ ], T = Math.max(0, Math.min(1, p)), e = Math.max(0, l.findIndex((c) => c[0] >= T) - 1), t = l[e], o = l[e + 1], u = Math.max(0, Math.min(1, (T - t[0]) / (o[0] - t[0]))), r = (c) => t[c] + (o[c] - t[c]) * u;
115
+ return { left: r(1) * Math.PI / 180, right: r(2) * Math.PI / 180, shift: r(3) * 82.3 };
116
+ }
117
+ const k = `
118
+ attribute vec3 position;
119
+ attribute vec3 normal;
120
+ attribute vec2 uv;
121
+ uniform vec2 viewport;
122
+ uniform float scale;
123
+ uniform vec2 leaf;
124
+ uniform vec2 roll;
125
+ uniform vec4 hinge;
126
+ uniform int bend;
127
+ uniform float shift;
128
+ uniform float level;
129
+ varying vec3 worldPosition;
130
+ varying vec3 worldNormal;
131
+ varying vec2 texcoord;
132
+ void main() {
133
+ vec3 p = position - vec3(0.,0.,${0.6});
134
+ p = vec3(leaf.x*p.x + leaf.y*p.z, p.y, -leaf.y*p.x + leaf.x*p.z);
135
+ if (bend==1) {
136
+ vec2 a=vec2(-1.1*hinge.x+(position.z-${0.6})*hinge.y,1.1*hinge.y+(position.z-${0.6})*hinge.x);
137
+ vec2 b=vec2(1.1*hinge.z+(position.z-${0.6})*hinge.w,-1.1*hinge.w+(position.z-${0.6})*hinge.z);
138
+ p.xz=mix(a,b,(position.x+1.1)/2.2);
139
+ }
140
+ p += vec3(shift,0.,${0.6});
141
+ vec3 n = vec3(leaf.x*normal.x + leaf.y*normal.z, normal.y, -leaf.y*normal.x + leaf.x*normal.z);
142
+ p.xy = vec2(roll.x*p.x-roll.y*p.y, roll.y*p.x+roll.x*p.y);
143
+ n.xy = vec2(roll.x*n.x-roll.y*n.y, roll.y*n.x+roll.x*n.y);
144
+ p.z -= level;
145
+ worldPosition=p; worldNormal=n; texcoord=uv;
146
+ float w = 1. - p.z/600.;
147
+ gl_Position=vec4(p.xy*scale*2./viewport, -p.z/180., w);
148
+ }`, X = `
149
+ precision highp float;
150
+ uniform int kind;
151
+ uniform sampler2D screen;
152
+ varying vec3 worldPosition;
153
+ varying vec3 worldNormal;
154
+ varying vec2 texcoord;
155
+ void main() {
156
+ if (kind==2) { gl_FragColor=texture2D(screen,texcoord); return; }
157
+ vec3 n=normalize(worldNormal);
158
+ vec3 eye=normalize(vec3(0.,0.,600.)-worldPosition);
159
+ vec3 r=normalize(reflect(-eye,n)+worldPosition/190.);
160
+ float longitude=atan(r.z,r.x);
161
+ float latitude=asin(clamp(r.y,-1.,1.));
162
+ float strip=pow(.5+.5*sin(4.*longitude+1.8*sin(latitude)),12.);
163
+ float fill=pow(.5+.5*cos(2.*longitude-3.*latitude),5.);
164
+ float highlight=pow(max(0.,dot(n,normalize(vec3(-.4,.65,1.)))),90.);
165
+ vec3 silver=vec3(.30,.32,.34)+vec3(.85,.86,.87)*strip+vec3(.48)*fill+vec3(.7)*highlight;
166
+ if(kind==1) silver=vec3(.022,.026,.030)+silver*.035;
167
+ if(kind==3) silver=vec3(.93,.93,.91)*(0.88+.12*max(n.z,0.));
168
+ gl_FragColor=vec4(clamp(silver,0.,1.),1.);
169
+ }`;
170
+ function V(p, l, T) {
171
+ const e = p.getContext("webgl", { alpha: !0, antialias: !0, powerPreference: "low-power" });
172
+ if (!e) throw new Error("Fold animation WebGL unavailable");
173
+ const t = [], o = [], u = [];
174
+ let r = null, c = 0, h = !1;
175
+ const n = () => {
176
+ if (!h) {
177
+ h = !0, cancelAnimationFrame(c);
178
+ for (const i of o) e.deleteBuffer(i);
179
+ for (const i of u) e.deleteTexture(i);
180
+ for (const i of t) e.deleteShader(i);
181
+ r && e.deleteProgram(r), e.getExtension("WEBGL_lose_context")?.loseContext();
182
+ }
183
+ };
184
+ try {
185
+ for (const [f, P] of [
186
+ [e.VERTEX_SHADER, k],
187
+ [e.FRAGMENT_SHADER, X]
188
+ ]) {
189
+ const R = e.createShader(f);
190
+ if (t.push(R), e.shaderSource(R, P), e.compileShader(R), !e.getShaderParameter(R, e.COMPILE_STATUS))
191
+ throw new Error("Fold shader compilation failed");
192
+ }
193
+ r = e.createProgram();
194
+ for (const f of t) e.attachShader(r, f);
195
+ if (e.linkProgram(r), !e.getProgramParameter(r, e.LINK_STATUS)) throw new Error("Fold shader link failed");
196
+ e.useProgram(r);
197
+ const i = (f) => e.getUniformLocation(r, f), a = Object.fromEntries(
198
+ ["viewport", "scale", "leaf", "roll", "shift", "level", "kind", "screen", "hinge", "bend"].map((f) => [
199
+ f,
200
+ i(f)
201
+ ])
202
+ ), s = l.to.inner, x = s ? l.to : l.from, d = s ? l.from : l.to, E = x.frame.rect.min.x + x.frame.rect.max.x, I = x.frame.rect.min.y + x.frame.rect.max.y, A = Math.min(window.devicePixelRatio || 1, 1.5, 1280 / Math.max(E, I));
203
+ p.width = Math.round(E * A), p.height = Math.round(I * A), e.viewport(0, 0, p.width, p.height), e.uniform2f(a.viewport, E, I), e.uniform1i(a.screen, 0), e.enable(e.DEPTH_TEST), e.clearColor(0, 0, 0, 0);
204
+ const m = {
205
+ inner: s ? l.after : l.before,
206
+ outer: s ? l.before : l.after
207
+ }, g = {};
208
+ for (const f of ["inner", "outer"]) {
209
+ const P = e.createTexture();
210
+ u.push(P), g[f] = P, e.bindTexture(e.TEXTURE_2D, P), e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL, 1), e.texImage2D(e.TEXTURE_2D, 0, e.RGBA, e.RGBA, e.UNSIGNED_BYTE, m[f]), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MIN_FILTER, e.LINEAR), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MAG_FILTER, e.LINEAR), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_S, e.CLAMP_TO_EDGE), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_T, e.CLAMP_TO_EDGE);
211
+ }
212
+ const v = ["position", "normal", "uv"].map((f) => e.getAttribLocation(r, f)), _ = B().map((f) => {
213
+ const P = e.createBuffer();
214
+ return o.push(P), e.bindBuffer(e.ARRAY_BUFFER, P), e.bufferData(e.ARRAY_BUFFER, f.vertices, e.STATIC_DRAW), { ...f, buffer: P };
215
+ }), H = performance.now(), F = d.frame.turns * -Math.PI / 2, L = (x.frame.turns * -Math.PI / 2 - F + Math.PI * 3) % (Math.PI * 2) - Math.PI;
216
+ let D = -1 / 0;
217
+ const M = (f) => {
218
+ if (h) return;
219
+ if (f - D < 1e3 / 60 - 0.5 && f - H < 1200) {
220
+ c = requestAnimationFrame(M);
221
+ return;
222
+ }
223
+ D = f;
224
+ const P = Math.min(1, (f - H) / 1200), R = s ? P : 1 - P, b = N(R), z = R * R * (3 - 2 * R), O = F + L * z;
225
+ e.clear(e.COLOR_BUFFER_BIT | e.DEPTH_BUFFER_BIT), e.uniform1f(a.scale, d.frame.scale + (x.frame.scale - d.frame.scale) * z), e.uniform2f(a.roll, Math.cos(O), Math.sin(O)), e.uniform1f(a.shift, b.shift), e.uniform1f(a.level, (5.2 + 0.6 * 2 + 0.24) * (1 - z)), e.uniform4f(
226
+ a.hinge,
227
+ Math.cos(b.left),
228
+ Math.sin(b.left),
229
+ Math.cos(b.right),
230
+ Math.sin(b.right)
231
+ );
232
+ for (const y of _) {
233
+ e.uniform1i(a.bend, y.side === 0 ? 1 : 0);
234
+ const S = y.side === -1 ? b.left : b.right;
235
+ e.uniform2f(a.leaf, Math.cos(S), Math.sin(S));
236
+ const G = y.kind === "inner" || y.kind === "outer";
237
+ e.uniform1i(
238
+ a.kind,
239
+ G ? 2 : y.kind === "metal" ? 0 : y.kind === "seal" ? 1 : 3
240
+ ), G && e.bindTexture(e.TEXTURE_2D, g[y.kind]), e.bindBuffer(e.ARRAY_BUFFER, y.buffer);
241
+ for (let w = 0; w < v.length; w++)
242
+ e.enableVertexAttribArray(v[w]), e.vertexAttribPointer(v[w], w === 2 ? 2 : 3, e.FLOAT, !1, 32, w === 2 ? 24 : w * 12);
243
+ e.drawArrays(e.TRIANGLES, 0, y.vertices.length / 8);
244
+ }
245
+ P < 1 ? c = requestAnimationFrame(M) : (n(), T());
246
+ };
247
+ return M(H), n;
248
+ } catch (i) {
249
+ throw n(), i;
250
+ }
251
+ }
252
+ export {
253
+ C as FOLD_DURATION,
254
+ B as foldMeshes,
255
+ N as foldPose,
256
+ V as renderFold
257
+ };
@@ -0,0 +1,53 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const G=1200,y=82.3,U=117.8,B=5.2,S=.6;function N(p,l,v,e,t=0){const o=[];for(const[u,r,c,h]of[[p/2-e,l/2-e,e,0],[-p/2+v,l/2-v,v,Math.PI/2],[-p/2+v,-l/2+v,v,Math.PI],[p/2-e,-l/2+e,e,Math.PI*1.5]])for(let n=0;n<=8;n++){const i=h+n*Math.PI/16,a=Math.cos(i),s=Math.sin(i);o.push({x:u+(c-t)*a,y:r+(c-t)*s,nx:a,ny:s})}return o}function W(){const p=[],l=(t,o,u)=>p.push({side:t,kind:o,vertices:new Float32Array(u)}),v=(t,o,u,r,c,h,n,i,a,s)=>{const g=[],d=(m,T,x,R)=>g.push(m.x+u,m.y+r,T,m.nx*R,m.ny*R,x,0,0),E=Math.min(.32,i/3,a/2,s/2),M=[[E,n,.707,.707],[0,n-E,0,1],[0,n-i+E,0,1],[E,n-i,-.707,.707]];for(let m=0;m<M.length-1;m++){const T=M[m],x=M[m+1],R=N(c,h,a,s,T[0]),I=N(c,h,a,s,x[0]);for(let b=0;b<R.length;b++){const L=(b+1)%R.length;for(const[k,F]of[[R[b],T],[I[b],x],[R[L],T],[R[L],T],[I[b],x],[I[L],x]])d(k,F[1],F[2],F[3])}}const _=N(c,h,a,s,E);for(const[m,T]of[[n,1],[n-i,-1]])for(let x=0;x<_.length;x++)g.push(u,r,m,0,0,T,0,0),d(_[x],m,T,0),d(_[(x+1)%_.length],m,T,0);l(t,o,g)},e=(t,o,u,r,c,h,n,i)=>{const a=[],s=N(r,c,n,i),g=(d,E)=>{const M=d+u;let _=0,m=0;o==="inner"&&(_=.5-E/110.8,m=.5+M/157.5),o==="outer"&&(_=.5-d/r,m=.5+E/c),a.push(M,E,h,0,0,o==="outer"||o==="back"?-1:1,_,m)};for(let d=0;d<s.length;d++)g(0,0),g(s[d].x,s[d].y),g(s[(d+1)%s.length].x,s[(d+1)%s.length].y);l(t,o,a)};for(const t of[-1,1]){const o=t===-1?8.4:1.25,u=t===1?8.4:1.25;v(t,"metal",t*y/2,0,y,U,0,B,o,u),e(t,"seal",t*y/2,y-2.24,U-2.24,.12,t===-1?7.15:.2,t===1?7.15:.2),e(t,"inner",t*(157.5/4+.55),157.5/2-1.1,110.8,.24,t===-1?5.3:0,t===1?5.3:0)}for(const[t,o,u]of[["metal",U,-.12],["seal",U-2.24,.12],["inner",110.8,.24]]){const r=[],c=(h,n)=>r.push(h,n,u,0,0,1,.5-n/110.8,.5+h/157.5);for(let h=0;h<6;h++){const n=-1.1+h*2.2/6,i=n+2.2/6;for(const[a,s]of[[n,-o/2],[i,-o/2],[n,o/2],[n,o/2],[i,-o/2],[i,o/2]])c(a,s)}l(0,t,r)}e(-1,"seal",-y/2,y-2.24,U-2.24,-B-.12,7.15,.65),e(-1,"outer",-y/2,77.1,112.2,-B-.24,6.6,.8),e(1,"back",y/2,y-1,U-1,-B-.12,.8,8);for(const t of[y-37,y-25])v(1,"metal",t,U/2+.4,9.5,.8,-1.4,2,.35,.35);return v(1,"metal",y+.4,21,.8,13,-1.4,2,.35,.35),p}function $(p){const l=[[0,180,0,-.5],[.12,176,-4,-.46],[.24,150,-8,-.4],[.35,90,-12,-.32],[.48,48,-22,-.14],[.62,30,-20,-.04],[.8,14,-10,0],[1,0,0,0]],v=Math.max(0,Math.min(1,p)),e=Math.max(0,l.findIndex(c=>c[0]>=v)-1),t=l[e],o=l[e+1],u=Math.max(0,Math.min(1,(v-t[0])/(o[0]-t[0]))),r=c=>t[c]+(o[c]-t[c])*u;return{left:r(1)*Math.PI/180,right:r(2)*Math.PI/180,shift:r(3)*y}}const Y=`
2
+ attribute vec3 position;
3
+ attribute vec3 normal;
4
+ attribute vec2 uv;
5
+ uniform vec2 viewport;
6
+ uniform float scale;
7
+ uniform vec2 leaf;
8
+ uniform vec2 roll;
9
+ uniform vec4 hinge;
10
+ uniform int bend;
11
+ uniform float shift;
12
+ uniform float level;
13
+ varying vec3 worldPosition;
14
+ varying vec3 worldNormal;
15
+ varying vec2 texcoord;
16
+ void main() {
17
+ vec3 p = position - vec3(0.,0.,${S});
18
+ p = vec3(leaf.x*p.x + leaf.y*p.z, p.y, -leaf.y*p.x + leaf.x*p.z);
19
+ if (bend==1) {
20
+ vec2 a=vec2(-1.1*hinge.x+(position.z-${S})*hinge.y,1.1*hinge.y+(position.z-${S})*hinge.x);
21
+ vec2 b=vec2(1.1*hinge.z+(position.z-${S})*hinge.w,-1.1*hinge.w+(position.z-${S})*hinge.z);
22
+ p.xz=mix(a,b,(position.x+1.1)/2.2);
23
+ }
24
+ p += vec3(shift,0.,${S});
25
+ vec3 n = vec3(leaf.x*normal.x + leaf.y*normal.z, normal.y, -leaf.y*normal.x + leaf.x*normal.z);
26
+ p.xy = vec2(roll.x*p.x-roll.y*p.y, roll.y*p.x+roll.x*p.y);
27
+ n.xy = vec2(roll.x*n.x-roll.y*n.y, roll.y*n.x+roll.x*n.y);
28
+ p.z -= level;
29
+ worldPosition=p; worldNormal=n; texcoord=uv;
30
+ float w = 1. - p.z/600.;
31
+ gl_Position=vec4(p.xy*scale*2./viewport, -p.z/180., w);
32
+ }`,j=`
33
+ precision highp float;
34
+ uniform int kind;
35
+ uniform sampler2D screen;
36
+ varying vec3 worldPosition;
37
+ varying vec3 worldNormal;
38
+ varying vec2 texcoord;
39
+ void main() {
40
+ if (kind==2) { gl_FragColor=texture2D(screen,texcoord); return; }
41
+ vec3 n=normalize(worldNormal);
42
+ vec3 eye=normalize(vec3(0.,0.,600.)-worldPosition);
43
+ vec3 r=normalize(reflect(-eye,n)+worldPosition/190.);
44
+ float longitude=atan(r.z,r.x);
45
+ float latitude=asin(clamp(r.y,-1.,1.));
46
+ float strip=pow(.5+.5*sin(4.*longitude+1.8*sin(latitude)),12.);
47
+ float fill=pow(.5+.5*cos(2.*longitude-3.*latitude),5.);
48
+ float highlight=pow(max(0.,dot(n,normalize(vec3(-.4,.65,1.)))),90.);
49
+ vec3 silver=vec3(.30,.32,.34)+vec3(.85,.86,.87)*strip+vec3(.48)*fill+vec3(.7)*highlight;
50
+ if(kind==1) silver=vec3(.022,.026,.030)+silver*.035;
51
+ if(kind==3) silver=vec3(.93,.93,.91)*(0.88+.12*max(n.z,0.));
52
+ gl_FragColor=vec4(clamp(silver,0.,1.),1.);
53
+ }`;function V(p,l,v){const e=p.getContext("webgl",{alpha:!0,antialias:!0,powerPreference:"low-power"});if(!e)throw new Error("Fold animation WebGL unavailable");const t=[],o=[],u=[];let r=null,c=0,h=!1;const n=()=>{if(!h){h=!0,cancelAnimationFrame(c);for(const i of o)e.deleteBuffer(i);for(const i of u)e.deleteTexture(i);for(const i of t)e.deleteShader(i);r&&e.deleteProgram(r),e.getExtension("WEBGL_lose_context")?.loseContext()}};try{for(const[f,P]of[[e.VERTEX_SHADER,Y],[e.FRAGMENT_SHADER,j]]){const A=e.createShader(f);if(t.push(A),e.shaderSource(A,P),e.compileShader(A),!e.getShaderParameter(A,e.COMPILE_STATUS))throw new Error("Fold shader compilation failed")}r=e.createProgram();for(const f of t)e.attachShader(r,f);if(e.linkProgram(r),!e.getProgramParameter(r,e.LINK_STATUS))throw new Error("Fold shader link failed");e.useProgram(r);const i=f=>e.getUniformLocation(r,f),a=Object.fromEntries(["viewport","scale","leaf","roll","shift","level","kind","screen","hinge","bend"].map(f=>[f,i(f)])),s=l.to.inner,g=s?l.to:l.from,d=s?l.from:l.to,E=g.frame.rect.min.x+g.frame.rect.max.x,M=g.frame.rect.min.y+g.frame.rect.max.y,_=Math.min(window.devicePixelRatio||1,1.5,1280/Math.max(E,M));p.width=Math.round(E*_),p.height=Math.round(M*_),e.viewport(0,0,p.width,p.height),e.uniform2f(a.viewport,E,M),e.uniform1i(a.screen,0),e.enable(e.DEPTH_TEST),e.clearColor(0,0,0,0);const m={inner:s?l.after:l.before,outer:s?l.before:l.after},T={};for(const f of["inner","outer"]){const P=e.createTexture();u.push(P),T[f]=P,e.bindTexture(e.TEXTURE_2D,P),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,1),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,m[f]),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE)}const x=["position","normal","uv"].map(f=>e.getAttribLocation(r,f)),R=W().map(f=>{const P=e.createBuffer();return o.push(P),e.bindBuffer(e.ARRAY_BUFFER,P),e.bufferData(e.ARRAY_BUFFER,f.vertices,e.STATIC_DRAW),{...f,buffer:P}}),I=performance.now(),b=d.frame.turns*-Math.PI/2,L=(g.frame.turns*-Math.PI/2-b+Math.PI*3)%(Math.PI*2)-Math.PI;let k=-1/0;const F=f=>{if(h)return;if(f-k<1e3/60-.5&&f-I<G){c=requestAnimationFrame(F);return}k=f;const P=Math.min(1,(f-I)/G),A=s?P:1-P,D=$(A),O=A*A*(3-2*A),X=b+L*O;e.clear(e.COLOR_BUFFER_BIT|e.DEPTH_BUFFER_BIT),e.uniform1f(a.scale,d.frame.scale+(g.frame.scale-d.frame.scale)*O),e.uniform2f(a.roll,Math.cos(X),Math.sin(X)),e.uniform1f(a.shift,D.shift),e.uniform1f(a.level,(B+S*2+.24)*(1-O)),e.uniform4f(a.hinge,Math.cos(D.left),Math.sin(D.left),Math.cos(D.right),Math.sin(D.right));for(const w of R){e.uniform1i(a.bend,w.side===0?1:0);const C=w.side===-1?D.left:D.right;e.uniform2f(a.leaf,Math.cos(C),Math.sin(C));const H=w.kind==="inner"||w.kind==="outer";e.uniform1i(a.kind,H?2:w.kind==="metal"?0:w.kind==="seal"?1:3),H&&e.bindTexture(e.TEXTURE_2D,T[w.kind]),e.bindBuffer(e.ARRAY_BUFFER,w.buffer);for(let z=0;z<x.length;z++)e.enableVertexAttribArray(x[z]),e.vertexAttribPointer(x[z],z===2?2:3,e.FLOAT,!1,32,z===2?24:z*12);e.drawArrays(e.TRIANGLES,0,w.vertices.length/8)}P<1?c=requestAnimationFrame(F):(n(),v())};return F(I),n}catch(i){throw n(),i}}exports.FOLD_DURATION=G;exports.foldMeshes=W;exports.foldPose=$;exports.renderFold=V;