@remix-gg/three 0.1.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 (67) hide show
  1. package/dist/assets.d.ts +36 -0
  2. package/dist/assets.d.ts.map +1 -0
  3. package/dist/assets.js +100 -0
  4. package/dist/audio.d.ts +42 -0
  5. package/dist/audio.d.ts.map +1 -0
  6. package/dist/audio.js +150 -0
  7. package/dist/camera.d.ts +72 -0
  8. package/dist/camera.d.ts.map +1 -0
  9. package/dist/camera.js +120 -0
  10. package/dist/collide.d.ts +111 -0
  11. package/dist/collide.d.ts.map +1 -0
  12. package/dist/collide.js +321 -0
  13. package/dist/forgiveness.d.ts +71 -0
  14. package/dist/forgiveness.d.ts.map +1 -0
  15. package/dist/forgiveness.js +85 -0
  16. package/dist/game.d.ts +69 -0
  17. package/dist/game.d.ts.map +1 -0
  18. package/dist/game.js +209 -0
  19. package/dist/hud/index.d.ts +72 -0
  20. package/dist/hud/index.d.ts.map +1 -0
  21. package/dist/hud/index.js +142 -0
  22. package/dist/hud/styles.d.ts +14 -0
  23. package/dist/hud/styles.d.ts.map +1 -0
  24. package/dist/hud/styles.js +142 -0
  25. package/dist/index.d.ts +30 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +27 -0
  28. package/dist/input/gestures.d.ts +124 -0
  29. package/dist/input/gestures.d.ts.map +1 -0
  30. package/dist/input/gestures.js +171 -0
  31. package/dist/input/index.d.ts +30 -0
  32. package/dist/input/index.d.ts.map +1 -0
  33. package/dist/input/index.js +121 -0
  34. package/dist/juice.d.ts +151 -0
  35. package/dist/juice.d.ts.map +1 -0
  36. package/dist/juice.js +237 -0
  37. package/dist/loop.d.ts +27 -0
  38. package/dist/loop.d.ts.map +1 -0
  39. package/dist/loop.js +30 -0
  40. package/dist/platform/index.d.ts +50 -0
  41. package/dist/platform/index.d.ts.map +1 -0
  42. package/dist/platform/index.js +177 -0
  43. package/dist/platform/sdk-contract.d.ts +113 -0
  44. package/dist/platform/sdk-contract.d.ts.map +1 -0
  45. package/dist/platform/sdk-contract.js +18 -0
  46. package/dist/ramp.d.ts +39 -0
  47. package/dist/ramp.d.ts.map +1 -0
  48. package/dist/ramp.js +24 -0
  49. package/dist/random.d.ts +128 -0
  50. package/dist/random.d.ts.map +1 -0
  51. package/dist/random.js +160 -0
  52. package/dist/scene/dispose.d.ts +46 -0
  53. package/dist/scene/dispose.d.ts.map +1 -0
  54. package/dist/scene/dispose.js +108 -0
  55. package/dist/scene/lighting.d.ts +42 -0
  56. package/dist/scene/lighting.d.ts.map +1 -0
  57. package/dist/scene/lighting.js +118 -0
  58. package/dist/scene/pool.d.ts +36 -0
  59. package/dist/scene/pool.d.ts.map +1 -0
  60. package/dist/scene/pool.js +70 -0
  61. package/dist/three.d.ts +2 -0
  62. package/dist/three.d.ts.map +1 -0
  63. package/dist/three.js +11 -0
  64. package/dist/viewport.d.ts +86 -0
  65. package/dist/viewport.d.ts.map +1 -0
  66. package/dist/viewport.js +174 -0
  67. package/package.json +43 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pure pointer-gesture state machine. No DOM, no three, no clock of its own —
3
+ * every decision is a function of the samples you feed it, which is what makes
4
+ * the thresholds testable instead of aspirational.
5
+ *
6
+ * Coordinates are DESIGN units (720x1080), so the thresholds mean the same
7
+ * physical distance on every screen.
8
+ */
9
+ export type PointerPhase = 'down' | 'move' | 'up' | 'cancel';
10
+ export type PointerSample = {
11
+ id: number;
12
+ x: number;
13
+ y: number;
14
+ /** Milliseconds, monotonic. */
15
+ time: number;
16
+ phase: PointerPhase;
17
+ };
18
+ export type GestureThresholds = {
19
+ tapMaxMs: number;
20
+ tapMaxTravel: number;
21
+ dragMinTravel: number;
22
+ swipeMinTravel: number;
23
+ /** Design units per second. */
24
+ swipeMinSpeed: number;
25
+ };
26
+ export declare const DEFAULT_THRESHOLDS: GestureThresholds;
27
+ export type Pointer = {
28
+ readonly id: number;
29
+ readonly x: number;
30
+ readonly y: number;
31
+ /** Movement since the previous sample. */
32
+ readonly dx: number;
33
+ readonly dy: number;
34
+ readonly startX: number;
35
+ readonly startY: number;
36
+ /** Milliseconds held so far. */
37
+ readonly duration: number;
38
+ /** Path length, not displacement — a circle back to the start is not a tap. */
39
+ readonly travel: number;
40
+ readonly dragging: boolean;
41
+ };
42
+ /** A live pointer plus the bookkeeping the reducer needs. */
43
+ export type TrackedPointer = Pointer & {
44
+ readonly startTime: number;
45
+ };
46
+ export type GestureState = {
47
+ readonly pointers: readonly TrackedPointer[];
48
+ readonly pinchStart: number | null;
49
+ readonly pinchLast: number | null;
50
+ };
51
+ export type TapEvent = {
52
+ pointerId: number;
53
+ x: number;
54
+ y: number;
55
+ duration: number;
56
+ };
57
+ export type DragEvent = {
58
+ pointerId: number;
59
+ x: number;
60
+ y: number;
61
+ dx: number;
62
+ dy: number;
63
+ totalX: number;
64
+ totalY: number;
65
+ };
66
+ export type SwipeDirection = 'up' | 'down' | 'left' | 'right';
67
+ export type SwipeEvent = {
68
+ pointerId: number;
69
+ x: number;
70
+ y: number;
71
+ dx: number;
72
+ dy: number;
73
+ direction: SwipeDirection;
74
+ /** Design units per second. */
75
+ speed: number;
76
+ };
77
+ export type PinchEvent = {
78
+ /** Distance relative to the start of this two-finger gesture. */
79
+ scale: number;
80
+ /** Distance relative to the previous sample. */
81
+ delta: number;
82
+ distance: number;
83
+ centerX: number;
84
+ centerY: number;
85
+ };
86
+ export type GestureEvent = {
87
+ type: 'press';
88
+ data: TapEvent;
89
+ } | {
90
+ type: 'release';
91
+ data: TapEvent;
92
+ } | {
93
+ type: 'tap';
94
+ data: TapEvent;
95
+ } | {
96
+ type: 'drag';
97
+ data: DragEvent;
98
+ } | {
99
+ type: 'swipe';
100
+ data: SwipeEvent;
101
+ } | {
102
+ type: 'pinch';
103
+ data: PinchEvent;
104
+ };
105
+ export type GestureEventMap = {
106
+ press: TapEvent;
107
+ release: TapEvent;
108
+ tap: TapEvent;
109
+ drag: DragEvent;
110
+ swipe: SwipeEvent;
111
+ pinch: PinchEvent;
112
+ };
113
+ export declare function createGestureState(): GestureState;
114
+ /**
115
+ * Folds one pointer sample into the gesture state.
116
+ *
117
+ * Returns a new state — the input is never mutated, so a caller can replay
118
+ * samples in a test without the previous run bleeding through.
119
+ */
120
+ export declare function reduceGestures(state: GestureState, sample: PointerSample, thresholds?: GestureThresholds): {
121
+ state: GestureState;
122
+ events: GestureEvent[];
123
+ };
124
+ //# sourceMappingURL=gestures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gestures.d.ts","sourceRoot":"","sources":["../../src/input/gestures.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Pure pointer-gesture state machine. No DOM, no three, no clock of its own —
3
+ * every decision is a function of the samples you feed it, which is what makes
4
+ * the thresholds testable instead of aspirational.
5
+ *
6
+ * Coordinates are DESIGN units (720x1080), so the thresholds mean the same
7
+ * physical distance on every screen.
8
+ */
9
+ export const DEFAULT_THRESHOLDS = {
10
+ tapMaxMs: 250,
11
+ tapMaxTravel: 12,
12
+ dragMinTravel: 8,
13
+ swipeMinTravel: 60,
14
+ swipeMinSpeed: 200,
15
+ };
16
+ export function createGestureState() {
17
+ return { pointers: [], pinchStart: null, pinchLast: null };
18
+ }
19
+ function distance(a, b) {
20
+ return Math.hypot(a.x - b.x, a.y - b.y);
21
+ }
22
+ function directionOf(dx, dy) {
23
+ if (Math.abs(dx) >= Math.abs(dy))
24
+ return dx >= 0 ? 'right' : 'left';
25
+ return dy >= 0 ? 'down' : 'up';
26
+ }
27
+ /**
28
+ * Folds one pointer sample into the gesture state.
29
+ *
30
+ * Returns a new state — the input is never mutated, so a caller can replay
31
+ * samples in a test without the previous run bleeding through.
32
+ */
33
+ export function reduceGestures(state, sample, thresholds = DEFAULT_THRESHOLDS) {
34
+ const pointers = state.pointers.map((p) => ({ ...p }));
35
+ const events = [];
36
+ const index = pointers.findIndex((p) => p.id === sample.id);
37
+ if (sample.phase === 'down') {
38
+ // A duplicate `down` for a live id means we missed the `up` (pointer
39
+ // capture lost, view swapped). Restart the pointer rather than tracking two.
40
+ if (index >= 0)
41
+ pointers.splice(index, 1);
42
+ pointers.push({
43
+ id: sample.id,
44
+ x: sample.x,
45
+ y: sample.y,
46
+ dx: 0,
47
+ dy: 0,
48
+ startX: sample.x,
49
+ startY: sample.y,
50
+ startTime: sample.time,
51
+ duration: 0,
52
+ travel: 0,
53
+ dragging: false,
54
+ });
55
+ events.push({
56
+ type: 'press',
57
+ data: { pointerId: sample.id, x: sample.x, y: sample.y, duration: 0 },
58
+ });
59
+ }
60
+ else if (index >= 0) {
61
+ const pointer = pointers[index];
62
+ const dx = sample.x - pointer.x;
63
+ const dy = sample.y - pointer.y;
64
+ pointer.x = sample.x;
65
+ pointer.y = sample.y;
66
+ pointer.dx = dx;
67
+ pointer.dy = dy;
68
+ pointer.travel += Math.hypot(dx, dy);
69
+ pointer.duration = sample.time - pointer.startTime;
70
+ const displacement = Math.hypot(pointer.x - pointer.startX, pointer.y - pointer.startY);
71
+ if (sample.phase === 'move') {
72
+ if (!pointer.dragging && displacement > thresholds.dragMinTravel)
73
+ pointer.dragging = true;
74
+ if (pointer.dragging) {
75
+ events.push({
76
+ type: 'drag',
77
+ data: {
78
+ pointerId: pointer.id,
79
+ x: pointer.x,
80
+ y: pointer.y,
81
+ dx,
82
+ dy,
83
+ totalX: pointer.x - pointer.startX,
84
+ totalY: pointer.y - pointer.startY,
85
+ },
86
+ });
87
+ }
88
+ }
89
+ else {
90
+ // Native views and trackpads may coalesce a short drag into a down/up
91
+ // pair whose endpoints differ. Preserve that terminal movement: without
92
+ // it a control built on `input.on('drag')` receives no drag at all even
93
+ // though the pointer travelled across the game.
94
+ if (sample.phase === 'up' &&
95
+ displacement > thresholds.dragMinTravel &&
96
+ (dx !== 0 || dy !== 0)) {
97
+ events.push({
98
+ type: 'drag',
99
+ data: {
100
+ pointerId: pointer.id,
101
+ x: pointer.x,
102
+ y: pointer.y,
103
+ dx,
104
+ dy,
105
+ totalX: pointer.x - pointer.startX,
106
+ totalY: pointer.y - pointer.startY,
107
+ },
108
+ });
109
+ }
110
+ pointers.splice(index, 1);
111
+ const tapLike = {
112
+ pointerId: pointer.id,
113
+ x: pointer.x,
114
+ y: pointer.y,
115
+ duration: pointer.duration,
116
+ };
117
+ events.push({ type: 'release', data: tapLike });
118
+ // A cancel is the system taking the pointer away (a call, a system
119
+ // gesture). It must never read as intent.
120
+ if (sample.phase === 'up') {
121
+ const speed = pointer.duration > 0 ? (displacement / pointer.duration) * 1000 : 0;
122
+ if (displacement >= thresholds.swipeMinTravel && speed >= thresholds.swipeMinSpeed) {
123
+ events.push({
124
+ type: 'swipe',
125
+ data: {
126
+ pointerId: pointer.id,
127
+ x: pointer.x,
128
+ y: pointer.y,
129
+ dx: pointer.x - pointer.startX,
130
+ dy: pointer.y - pointer.startY,
131
+ direction: directionOf(pointer.x - pointer.startX, pointer.y - pointer.startY),
132
+ speed,
133
+ },
134
+ });
135
+ }
136
+ else if (pointer.duration <= thresholds.tapMaxMs &&
137
+ pointer.travel <= thresholds.tapMaxTravel) {
138
+ events.push({ type: 'tap', data: tapLike });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ let pinchStart = state.pinchStart;
144
+ let pinchLast = state.pinchLast;
145
+ const [first, second] = pointers;
146
+ if (first && second && pointers.length === 2) {
147
+ const current = distance(first, second);
148
+ if (pinchStart === null || pinchLast === null) {
149
+ pinchStart = current;
150
+ pinchLast = current;
151
+ }
152
+ else if (sample.phase === 'move') {
153
+ events.push({
154
+ type: 'pinch',
155
+ data: {
156
+ scale: pinchStart > 0 ? current / pinchStart : 1,
157
+ delta: pinchLast > 0 ? current / pinchLast : 1,
158
+ distance: current,
159
+ centerX: (first.x + second.x) / 2,
160
+ centerY: (first.y + second.y) / 2,
161
+ },
162
+ });
163
+ pinchLast = current;
164
+ }
165
+ }
166
+ else {
167
+ pinchStart = null;
168
+ pinchLast = null;
169
+ }
170
+ return { state: { pointers, pinchStart, pinchLast }, events };
171
+ }
@@ -0,0 +1,30 @@
1
+ import { type Plane, Vector3 } from 'three';
2
+ import type { Intersection, Object3D } from 'three';
3
+ import type { CameraRig } from '../camera.js';
4
+ import type { Viewport } from '../viewport.js';
5
+ import { type GestureEventMap, type GestureThresholds, type Pointer } from './gestures.js';
6
+ export type { Pointer, GestureThresholds };
7
+ export type { DragEvent, GestureEvent, GestureEventMap, PinchEvent, SwipeDirection, SwipeEvent, TapEvent, } from './gestures.js';
8
+ /**
9
+ * Unified pointer input in DESIGN units, plus scene and ground-plane picking.
10
+ *
11
+ * Mobile is the target, so there is deliberately no keyboard and no hover path:
12
+ * a game built around either is unplayable on the device most Remix players
13
+ * use. A mouse arrives here as a single pointer and behaves like a finger.
14
+ */
15
+ export type Input = {
16
+ readonly pointers: readonly Pointer[];
17
+ readonly primary: Pointer | null;
18
+ enabled: boolean;
19
+ on<K extends keyof GestureEventMap>(event: K, cb: (data: GestureEventMap[K]) => void): () => void;
20
+ /** Ray-cast from a pointer. Defaults to the primary pointer and the whole scene. */
21
+ pick(objects?: Object3D[], pointer?: Pointer | null): Intersection[];
22
+ /** Where a pointer's ray meets a plane — the ground-truth for tap-to-move. */
23
+ pickPlane(plane: Plane, out?: Vector3, pointer?: Pointer | null): Vector3 | null;
24
+ dispose(): void;
25
+ };
26
+ export type InputOptions = {
27
+ thresholds?: Partial<GestureThresholds>;
28
+ };
29
+ export declare function createInput(canvas: HTMLCanvasElement, viewport: Viewport, rig: CameraRig, scene: Object3D, options?: InputOptions): Input;
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/input/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAsB,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,iBAAiB,EACtB,KAAK,OAAO,EAGb,MAAM,eAAe,CAAA;AAEtB,YAAY,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAA;AAC1C,YAAY,EACV,SAAS,EACT,YAAY,EACZ,eAAe,EACf,UAAU,EACV,cAAc,EACd,UAAU,EACV,QAAQ,GACT,MAAM,eAAe,CAAA;AAEtB;;;;;;GAMG;AACH,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAA;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IAChC,OAAO,EAAE,OAAO,CAAA;IAChB,EAAE,CAAC,CAAC,SAAS,MAAM,eAAe,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IACjG,oFAAoF;IACpF,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,YAAY,EAAE,CAAA;IACpE,8EAA8E;IAC9E,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,GAAG,IAAI,CAAA;IAChF,OAAO,IAAI,IAAI,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAA;CACxC,CAAA;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,iBAAiB,EACzB,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,SAAS,EACd,KAAK,EAAE,QAAQ,EACf,OAAO,GAAE,YAAiB,GACzB,KAAK,CAyHP"}
@@ -0,0 +1,121 @@
1
+ import { Raycaster, Vector2, Vector3 } from 'three';
2
+ import { DEFAULT_THRESHOLDS, createGestureState, reduceGestures, } from './gestures.js';
3
+ export function createInput(canvas, viewport, rig, scene, options = {}) {
4
+ const thresholds = options.thresholds
5
+ ? { ...DEFAULT_THRESHOLDS, ...options.thresholds }
6
+ : undefined;
7
+ const listeners = new Map();
8
+ const raycaster = new Raycaster();
9
+ const ndc = new Vector2();
10
+ const design = new Vector2();
11
+ let state = createGestureState();
12
+ let enabled = true;
13
+ // Without `touch-action: none` the browser eats the second pointermove of
14
+ // every drag to decide whether it is a scroll.
15
+ canvas.style.touchAction = 'none';
16
+ function dispatch(sample) {
17
+ const result = reduceGestures(state, sample, thresholds);
18
+ state = result.state;
19
+ for (const event of result.events) {
20
+ const set = listeners.get(event.type);
21
+ if (!set)
22
+ continue;
23
+ for (const cb of [...set])
24
+ cb(event.data);
25
+ }
26
+ }
27
+ function sampleOf(event, phase) {
28
+ viewport.toDesign(event.clientX, event.clientY, design);
29
+ return { id: event.pointerId, x: design.x, y: design.y, time: event.timeStamp, phase };
30
+ }
31
+ const onDown = (event) => {
32
+ if (!enabled)
33
+ return;
34
+ event.preventDefault();
35
+ // Capture so a finger that slides off the canvas keeps reporting; without
36
+ // it a drag that leaves the edge sticks forever in the "down" state.
37
+ canvas.setPointerCapture?.(event.pointerId);
38
+ dispatch(sampleOf(event, 'down'));
39
+ };
40
+ const onMove = (event) => {
41
+ if (!enabled)
42
+ return;
43
+ dispatch(sampleOf(event, 'move'));
44
+ };
45
+ const onUp = (event) => {
46
+ if (!enabled)
47
+ return;
48
+ canvas.releasePointerCapture?.(event.pointerId);
49
+ dispatch(sampleOf(event, 'up'));
50
+ };
51
+ const onCancel = (event) => {
52
+ if (!enabled)
53
+ return;
54
+ dispatch(sampleOf(event, 'cancel'));
55
+ };
56
+ canvas.addEventListener('pointerdown', onDown);
57
+ canvas.addEventListener('pointermove', onMove);
58
+ canvas.addEventListener('pointerup', onUp);
59
+ canvas.addEventListener('pointercancel', onCancel);
60
+ function resolve(pointer) {
61
+ return pointer ?? state.pointers[0] ?? null;
62
+ }
63
+ function aim(pointer) {
64
+ viewport.designToNdc(pointer.x, pointer.y, ndc);
65
+ raycaster.setFromCamera(ndc, rig.camera);
66
+ }
67
+ return {
68
+ get pointers() {
69
+ return state.pointers;
70
+ },
71
+ get primary() {
72
+ return state.pointers[0] ?? null;
73
+ },
74
+ get enabled() {
75
+ return enabled;
76
+ },
77
+ set enabled(value) {
78
+ enabled = value;
79
+ // Dropping the tracked pointers matters: re-enabling mid-drag would
80
+ // otherwise emit a drag with a huge delta from a stale origin.
81
+ if (!value)
82
+ state = createGestureState();
83
+ },
84
+ on(event, cb) {
85
+ let set = listeners.get(event);
86
+ if (!set) {
87
+ set = new Set();
88
+ listeners.set(event, set);
89
+ }
90
+ const entry = cb;
91
+ set.add(entry);
92
+ return () => {
93
+ set?.delete(entry);
94
+ };
95
+ },
96
+ pick(objects, pointer) {
97
+ const active = resolve(pointer);
98
+ if (!active)
99
+ return [];
100
+ aim(active);
101
+ return objects
102
+ ? raycaster.intersectObjects(objects, true)
103
+ : raycaster.intersectObjects(scene.children, true);
104
+ },
105
+ pickPlane(plane, out, pointer) {
106
+ const active = resolve(pointer);
107
+ if (!active)
108
+ return null;
109
+ aim(active);
110
+ return raycaster.ray.intersectPlane(plane, out ?? new Vector3());
111
+ },
112
+ dispose() {
113
+ canvas.removeEventListener('pointerdown', onDown);
114
+ canvas.removeEventListener('pointermove', onMove);
115
+ canvas.removeEventListener('pointerup', onUp);
116
+ canvas.removeEventListener('pointercancel', onCancel);
117
+ listeners.clear();
118
+ state = createGestureState();
119
+ },
120
+ };
121
+ }
@@ -0,0 +1,151 @@
1
+ import type { CameraRig } from './camera.js';
2
+ /**
3
+ * Game feel: tweens, screen shake, hit-stop.
4
+ *
5
+ * The mechanisms behind "juice" — the layer of feedback that makes a hit feel
6
+ * like a hit. They live in the SDK because two of them physically cannot be
7
+ * hand-rolled well here: screen shake fights the camera rigs (a rig like
8
+ * `followRig` rewrites the camera transform every frame, so a game adding its
9
+ * own offset either loses it or double-applies it), and hit-stop is a pause of
10
+ * the fixed-timestep simulation, which only the update loop can express.
11
+ *
12
+ * What stays with the game is taste. These are parameterised mechanisms, not a
13
+ * house style: a shake's magnitude belongs to the event that caused it and a
14
+ * calm puzzle game calls none of this. The guide teaches matching intensity to
15
+ * tone; this file only makes the mechanism correct.
16
+ */
17
+ export type Easing = (t: number) => number;
18
+ /**
19
+ * The curated set. Linear motion everywhere is the tell of an untweened game —
20
+ * real objects accelerate — but a menagerie of forty easings is choice with no
21
+ * information. These seven cover the moves portrait games make: `quadOut` for
22
+ * almost everything that moves on screen, `backOut` for pops that overshoot,
23
+ * `elasticOut` for the rare springy emphasis, the rest for symmetry and ramps.
24
+ */
25
+ export declare const easings: {
26
+ readonly linear: (t: number) => number;
27
+ readonly quadIn: (t: number) => number;
28
+ readonly quadOut: (t: number) => number;
29
+ readonly quadInOut: (t: number) => number;
30
+ readonly cubicOut: (t: number) => number;
31
+ readonly backOut: (t: number) => number;
32
+ readonly elasticOut: (t: number) => number;
33
+ };
34
+ export type EasingName = keyof typeof easings;
35
+ /** Numeric properties of `T` — the only thing a tween can drive. */
36
+ type NumericKeys<T> = {
37
+ [K in keyof T]: T[K] extends number ? K : never;
38
+ }[keyof T];
39
+ export type TweenOptions = {
40
+ /** Seconds for one leg. A yoyo tween takes 2 × duration for the round trip. */
41
+ duration: number;
42
+ /** Defaults to 'quadOut', the one that suits most motion. */
43
+ easing?: EasingName | Easing;
44
+ /** Return to the starting values after reaching the target. */
45
+ yoyo?: boolean;
46
+ /** Runs after each update — e.g. pushing a tweened counter into the HUD. */
47
+ onUpdate?: () => void;
48
+ /** Runs exactly once, when the tween lands (not when it is cancelled). */
49
+ onComplete?: () => void;
50
+ };
51
+ export type TweenHandle = {
52
+ cancel(): void;
53
+ readonly done: boolean;
54
+ };
55
+ export type Tweens = {
56
+ /**
57
+ * Animate `target`'s numeric properties to `to`. Start values are read NOW,
58
+ * at the call — a tween started from mid-flight starts from mid-flight.
59
+ */
60
+ to<T>(target: T, to: Partial<Pick<T, NumericKeys<T>>>, options: TweenOptions): TweenHandle;
61
+ /** Advance every tween. Call once per fixed step, from `update`. */
62
+ update(step: number): void;
63
+ /** Drop everything mid-flight, no callbacks. For the run reset. */
64
+ cancelAll(): void;
65
+ readonly active: number;
66
+ };
67
+ /**
68
+ * A tween runner stepped by the game's own fixed timestep.
69
+ *
70
+ * Stepped, not clocked: it advances only when `update(step)` is called, so
71
+ * tweens pause with the simulation (including during hit-stop, which is the
72
+ * behaviour that reads as impact), run identically on 60 Hz and 120 Hz
73
+ * displays, and are deterministic under test.
74
+ *
75
+ * Create it once at module scope, call `tweens.update(step)` first thing in
76
+ * the game's update, and `tweens.cancelAll()` from the run reset — a tween
77
+ * from the previous run landing on a freshly reset object is a classic ghost.
78
+ */
79
+ export declare function createTweens(): Tweens;
80
+ export type ShakeRigOptions = {
81
+ /** Camera offset in world units at full trauma. Characters are ~1 unit. */
82
+ maxOffset?: number;
83
+ /** Roll in radians at full trauma. A little goes a long way. */
84
+ maxRoll?: number;
85
+ /** Trauma lost per second. 1.5 means a full shake settles in ~0.7s. */
86
+ decay?: number;
87
+ /** The random source, for deterministic tests. Defaults to Math.random. */
88
+ rng?: () => number;
89
+ };
90
+ export type ShakeRig = CameraRig & {
91
+ /**
92
+ * Add trauma, clamped to 1. Scale it to the event: a pickup is ~0.15, a hit
93
+ * ~0.35, a death ~0.7. Perceived shake is trauma squared, so small events
94
+ * barely register and big ones dominate — which is the point.
95
+ */
96
+ shake(trauma: number): void;
97
+ readonly trauma: number;
98
+ };
99
+ /**
100
+ * Wraps any camera rig with trauma-based screen shake.
101
+ *
102
+ * A decorator, because ownership is the entire problem: rigs own the camera
103
+ * transform, and `followRig` rewrites it every frame while `portraitRig` never
104
+ * touches it after boot. Adding an offset directly therefore either vanishes
105
+ * next frame or accumulates forever, depending on which rig the game picked.
106
+ * The wrapper removes last frame's offset, lets the inner rig do its work,
107
+ * then applies a fresh one — correct against both behaviours.
108
+ *
109
+ * Trauma-squared with linear decay (Squirrel Eiserloh's model): shake feels
110
+ * proportional to events this way, and it composes — two quick hits raise
111
+ * trauma additively instead of restarting a fixed animation.
112
+ *
113
+ * Shake runs on render time (`rig.update(dt)`), so it keeps moving during
114
+ * hit-stop and after `gameOver` — a dead-still camera over a game-over sheet
115
+ * reads as a hang, and shake-through-freeze is the classic impact combo.
116
+ *
117
+ * const rig = shakeRig(portraitRig())
118
+ * await createGame({ camera: rig, ... })
119
+ * rig.shake(0.4)
120
+ */
121
+ export declare function shakeRig(rig: CameraRig, options?: ShakeRigOptions): ShakeRig;
122
+ export type HitStop = {
123
+ /** Freeze the simulation for `seconds`. Overlapping triggers take the max. */
124
+ trigger(seconds: number): void;
125
+ /**
126
+ * Consume one fixed step of freeze. The gate at the top of update:
127
+ *
128
+ * update(game, step) {
129
+ * if (hitStop.frozen(step)) return
130
+ * ...
131
+ * }
132
+ */
133
+ frozen(step: number): boolean;
134
+ readonly active: boolean;
135
+ };
136
+ /**
137
+ * Hit-stop: the few-frame freeze that sells an impact.
138
+ *
139
+ * It cannot be a sleep and cannot skip rendering — the loop keeps drawing the
140
+ * frozen state, which is exactly the effect. What stops is simulation, and the
141
+ * game owns its update, so this is a countdown the game gates on rather than
142
+ * something the SDK can impose.
143
+ *
144
+ * Keep it short and rare: 0.05–0.12s, on the few biggest moments (a death, a
145
+ * heavy hit). On every minor event it stops reading as impact and starts
146
+ * reading as jank. Screen shake keeps moving through it (see `shakeRig`) —
147
+ * freeze-plus-shake is the combination that reads as force.
148
+ */
149
+ export declare function createHitStop(): HitStop;
150
+ export {};
151
+ //# sourceMappingURL=juice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"juice.d.ts","sourceRoot":"","sources":["../src/juice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C;;;;;;;;;;;;;;GAcG;AAKH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;AAE1C;;;;;;GAMG;AACH,eAAO,MAAM,OAAO;yBACN,MAAM;yBACN,MAAM;0BACL,MAAM;4BACJ,MAAM;2BACP,MAAM;0BACP,MAAM;6BACH,MAAM;CAEmB,CAAA;AAE3C,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,OAAO,CAAA;AAgB7C,oEAAoE;AACpE,KAAK,WAAW,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,GAAG,KAAK;CAAE,CAAC,MAAM,CAAC,CAAC,CAAA;AAElF,MAAM,MAAM,YAAY,GAAG;IACzB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAA;IAChB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAA;IAC5B,+DAA+D;IAC/D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,IAAI,IAAI,CAAA;IACd,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,MAAM,GAAG;IACnB;;;OAGG;IACH,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,GAAG,WAAW,CAAA;IAC1F,oEAAoE;IACpE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,mEAAmE;IACnE,SAAS,IAAI,IAAI,CAAA;IACjB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,CAAA;AAgBD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAyFrC;AAKD,MAAM,MAAM,eAAe,GAAG;IAC5B,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG;IACjC;;;;OAIG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,GAAE,eAAoB,GAAG,QAAQ,CAkDhF;AAKD,MAAM,MAAM,OAAO,GAAG;IACpB,8EAA8E;IAC9E,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CACzB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAoBvC"}