@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
package/dist/game.js ADDED
@@ -0,0 +1,209 @@
1
+ import { ACESFilmicToneMapping, Color, SRGBColorSpace, Scene, WebGLRenderer, } from 'three';
2
+ import { createAssets } from './assets.js';
3
+ import { createAudio } from './audio.js';
4
+ import { portraitRig } from './camera.js';
5
+ import { createHud } from './hud/index.js';
6
+ import { createInput } from './input/index.js';
7
+ import { stepClock } from './loop.js';
8
+ import { awaitSdk, createPlatform } from './platform/index.js';
9
+ import { DisposalScope } from './scene/dispose.js';
10
+ import { createViewport } from './viewport.js';
11
+ /**
12
+ * The one bootstrap call. A game never constructs a `WebGLRenderer` itself,
13
+ * because every one of the settings below is a decision that only looks
14
+ * cosmetic until it costs 30 fps on a phone.
15
+ */
16
+ export async function createGame(options) {
17
+ const container = options.container ?? document.body;
18
+ const fixedStep = options.fixedStep ?? 1 / 60;
19
+ const maxStepsPerFrame = options.maxStepsPerFrame ?? 5;
20
+ prepareContainer(container);
21
+ const canvas = document.createElement('canvas');
22
+ canvas.style.display = 'block';
23
+ canvas.style.width = '100%';
24
+ canvas.style.height = '100%';
25
+ container.append(canvas);
26
+ const viewport = createViewport(canvas, {
27
+ design: options.design,
28
+ fit: options.fit,
29
+ maxPixelRatio: options.maxPixelRatio ?? 2,
30
+ host: container,
31
+ });
32
+ const renderer = new WebGLRenderer({
33
+ canvas,
34
+ // Antialiasing is redundant once the DPR is above 1 — the downsample
35
+ // already does it — and it costs a multisampled framebuffer.
36
+ antialias: viewport.pixelRatio < 2,
37
+ stencil: false,
38
+ powerPreference: 'high-performance',
39
+ alpha: false,
40
+ });
41
+ renderer.outputColorSpace = SRGBColorSpace;
42
+ // ACES is what carries the look without post-processing: it keeps emissives
43
+ // and bright lights from clipping to flat white.
44
+ renderer.toneMapping = ACESFilmicToneMapping;
45
+ renderer.toneMappingExposure = 1;
46
+ const scene = new Scene();
47
+ if (options.background !== null) {
48
+ scene.background =
49
+ options.background === undefined
50
+ ? new Color(0x060b0f)
51
+ : typeof options.background === 'object'
52
+ ? options.background
53
+ : new Color(options.background);
54
+ }
55
+ const rig = options.camera ?? portraitRig();
56
+ const audio = createAudio({ sound: (key) => assets.sound(key) });
57
+ const assets = createAssets({ decodeAudio: (data) => audio.decode(data) });
58
+ const hud = createHud(container, viewport);
59
+ const input = createInput(canvas, viewport, rig, scene);
60
+ const sdk = await awaitSdk();
61
+ const platform = createPlatform(sdk);
62
+ function applyViewport() {
63
+ renderer.setPixelRatio(viewport.pixelRatio);
64
+ renderer.setSize(viewport.width, viewport.height, false);
65
+ rig.resize(viewport);
66
+ }
67
+ const offResize = viewport.on('resize', applyViewport);
68
+ applyViewport();
69
+ const transient = new DisposalScope(assets.owned);
70
+ const time = { elapsed: 0, delta: 0, frame: 0 };
71
+ let accumulator = 0;
72
+ let lastTime = 0;
73
+ let running = false;
74
+ let disposed = false;
75
+ const game = {
76
+ renderer,
77
+ scene,
78
+ rig,
79
+ get camera() {
80
+ return rig.camera;
81
+ },
82
+ viewport,
83
+ input,
84
+ assets,
85
+ audio,
86
+ hud,
87
+ platform,
88
+ transient,
89
+ time,
90
+ get running() {
91
+ return running;
92
+ },
93
+ gameOver(score) {
94
+ platform.gameOver(score);
95
+ // The loop keeps rendering — a frozen canvas behind the host's game-over
96
+ // sheet looks broken — but simulation stops.
97
+ running = false;
98
+ },
99
+ restart() {
100
+ // Order matters: free the previous run's GPU resources BEFORE the game
101
+ // builds the next one, or peak memory is two levels wide.
102
+ transient.dispose();
103
+ // The HUD half of the same rule. `setup` built the score, the scene and
104
+ // the lights once, and a restart rebuilds a *run*, not the game — so it
105
+ // drops only what the run added. Clearing the whole HUD here is what left
106
+ // the flagship kit writing its score into a detached node from the first
107
+ // play-again onwards.
108
+ hud.resetRun();
109
+ platform.rearm();
110
+ accumulator = 0;
111
+ time.elapsed = 0;
112
+ time.frame = 0;
113
+ running = true;
114
+ options.restart?.(game);
115
+ },
116
+ pause() {
117
+ running = false;
118
+ },
119
+ resume() {
120
+ lastTime = performance.now();
121
+ running = true;
122
+ },
123
+ dispose() {
124
+ if (disposed)
125
+ return;
126
+ disposed = true;
127
+ renderer.setAnimationLoop(null);
128
+ offResize();
129
+ offPlayAgain();
130
+ offMute();
131
+ offInfo();
132
+ document.removeEventListener('visibilitychange', onVisibility);
133
+ transient.dispose();
134
+ input.dispose();
135
+ hud.dispose();
136
+ audio.dispose();
137
+ viewport.dispose();
138
+ renderer.dispose();
139
+ canvas.remove();
140
+ },
141
+ };
142
+ // The first press is the only moment a mobile browser will let us start
143
+ // audio, so it is wired here rather than left to every game to remember.
144
+ input.on('press', () => {
145
+ void audio.unlock();
146
+ });
147
+ const offPlayAgain = platform.onPlayAgain(() => game.restart());
148
+ const offMute = platform.onToggleMute((muted) => audio.setMuted(muted));
149
+ const offInfo = platform.onGameInfo((info) => viewport.setSafeArea(info.contentSafeAreaInset));
150
+ const onVisibility = () => {
151
+ if (document.visibilityState === 'hidden') {
152
+ running = false;
153
+ }
154
+ else {
155
+ // Reset the clock: without this the accumulator sees the whole
156
+ // backgrounded interval as one frame.
157
+ lastTime = performance.now();
158
+ running = true;
159
+ }
160
+ };
161
+ document.addEventListener('visibilitychange', onVisibility);
162
+ if (options.assets)
163
+ await assets.load(options.assets, options.onProgress);
164
+ const info = await platform.ready();
165
+ viewport.setSafeArea(info.contentSafeAreaInset);
166
+ await options.setup(game);
167
+ // Everything the game built during `setup` — HUD included — outlives a run.
168
+ hud.sealSetup();
169
+ lastTime = performance.now();
170
+ running = true;
171
+ renderer.setAnimationLoop((now) => {
172
+ const dt = Math.max((now - lastTime) / 1000, 0);
173
+ lastTime = now;
174
+ time.delta = dt;
175
+ let alpha = 1;
176
+ if (running) {
177
+ const result = stepClock(accumulator, dt, fixedStep, maxStepsPerFrame);
178
+ accumulator = result.accumulator;
179
+ alpha = result.alpha;
180
+ for (let i = 0; i < result.steps; i++) {
181
+ time.elapsed += fixedStep;
182
+ options.update?.(game, fixedStep);
183
+ }
184
+ }
185
+ rig.update(dt);
186
+ options.render?.(game, dt, alpha);
187
+ renderer.render(scene, rig.camera);
188
+ time.frame++;
189
+ });
190
+ return game;
191
+ }
192
+ /** Full-bleed, non-scrolling, non-selectable — the shape a game needs. */
193
+ function prepareContainer(container) {
194
+ const style = document.documentElement.style;
195
+ style.setProperty('height', '100%');
196
+ document.body.style.margin = '0';
197
+ document.body.style.height = '100%';
198
+ document.body.style.overflow = 'hidden';
199
+ document.body.style.overscrollBehavior = 'none';
200
+ document.body.style.userSelect = 'none';
201
+ document.body.style.setProperty('-webkit-tap-highlight-color', 'transparent');
202
+ if (container === document.body) {
203
+ container.style.position = 'relative';
204
+ return;
205
+ }
206
+ container.style.position = 'relative';
207
+ container.style.width = '100%';
208
+ container.style.height = '100%';
209
+ }
@@ -0,0 +1,72 @@
1
+ import type { Viewport } from '../viewport.js';
2
+ export type HudAnchor = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right';
3
+ export type HudLayerOptions = {
4
+ interactive?: boolean;
5
+ };
6
+ export type HudScore = {
7
+ el: HTMLDivElement;
8
+ set(value: number): void;
9
+ };
10
+ export type HudChip = {
11
+ el: HTMLDivElement;
12
+ set(text: string): void;
13
+ };
14
+ export type HudBanner = {
15
+ el: HTMLDivElement;
16
+ dismiss(): void;
17
+ };
18
+ /**
19
+ * DOM overlay HUD.
20
+ *
21
+ * DOM rather than canvas text: an SDF atlas is heavier, blurrier, and cannot
22
+ * use the design system. The overlay is anchored to the platform's content safe
23
+ * area, so nothing the player must read ever sits under a notch, a home
24
+ * indicator, or the Remix shell's own chrome.
25
+ */
26
+ export type Hud = {
27
+ readonly root: HTMLDivElement;
28
+ layer(anchor: HudAnchor, options?: HudLayerOptions): HTMLDivElement;
29
+ score(options?: {
30
+ anchor?: HudAnchor;
31
+ format?: (value: number) => string;
32
+ }): HudScore;
33
+ chip(text: string, options?: {
34
+ anchor?: HudAnchor;
35
+ }): HudChip;
36
+ button(label: string, onTap: () => void, options?: {
37
+ anchor?: HudAnchor;
38
+ variant?: 'default' | 'cta' | 'ghost';
39
+ }): HTMLButtonElement;
40
+ banner(title: string, subtitle?: string): HudBanner;
41
+ /**
42
+ * Removes everything the game added, including the elements built in `setup`.
43
+ *
44
+ * `restart()` does NOT call this — it clears only what the run added. See
45
+ * `HudRuntime`.
46
+ */
47
+ clear(): void;
48
+ dispose(): void;
49
+ };
50
+ /**
51
+ * What `createGame` drives; a game only ever sees `Hud`.
52
+ *
53
+ * The HUD has the same persistent/transient split the scene has, and for the
54
+ * same reason. `restart()` used to call `clear()`, which detached everything —
55
+ * including the score built in `setup`, which is where every kit and every LLM
56
+ * following one builds it. The handle kept working, the writes kept landing,
57
+ * and they landed on a node that was no longer in the document: the second run
58
+ * of a game silently had no score, on the platform's most-pressed button.
59
+ *
60
+ * So `setup`-time elements are persistent, exactly like the meshes a game adds
61
+ * to the scene in `setup`, and anything created during a run is transient,
62
+ * exactly like the meshes a game registers in `game.transient`. One rule for
63
+ * both halves of a game means one thing to remember.
64
+ */
65
+ export type HudRuntime = Hud & {
66
+ /** Everything created from here on belongs to the run, not to the game. */
67
+ sealSetup(): void;
68
+ /** Drops what the run added and leaves the `setup`-time HUD attached. */
69
+ resetRun(): void;
70
+ };
71
+ export declare function createHud(container: HTMLElement, viewport: Viewport): HudRuntime;
72
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hud/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAG9C,MAAM,MAAM,SAAS,GACjB,UAAU,GACV,KAAK,GACL,WAAW,GACX,MAAM,GACN,QAAQ,GACR,OAAO,GACP,aAAa,GACb,QAAQ,GACR,cAAc,CAAA;AAElB,MAAM,MAAM,eAAe,GAAG;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAEvD,MAAM,MAAM,QAAQ,GAAG;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AACvE,MAAM,MAAM,OAAO,GAAG;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AACrE,MAAM,MAAM,SAAS,GAAG;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,OAAO,IAAI,IAAI,CAAA;CAAE,CAAA;AAE/D;;;;;;;GAOG;AACH,MAAM,MAAM,GAAG,GAAG;IAChB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,cAAc,CAAA;IACnE,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,SAAS,CAAC;QAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;KAAE,GAAG,QAAQ,CAAA;IACrF,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,SAAS,CAAA;KAAE,GAAG,OAAO,CAAA;IAC7D,MAAM,CACJ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,IAAI,EACjB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,SAAS,GAAG,KAAK,GAAG,OAAO,CAAA;KAAE,GACtE,iBAAiB,CAAA;IACpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnD;;;;;OAKG;IACH,KAAK,IAAI,IAAI,CAAA;IACb,OAAO,IAAI,IAAI,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,UAAU,GAAG,GAAG,GAAG;IAC7B,2EAA2E;IAC3E,SAAS,IAAI,IAAI,CAAA;IACjB,yEAAyE;IACzE,QAAQ,IAAI,IAAI,CAAA;CACjB,CAAA;AAOD,wBAAgB,SAAS,CAAC,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,GAAG,UAAU,CAqJhF"}
@@ -0,0 +1,142 @@
1
+ import { HUD_CSS } from './styles.js';
2
+ const STYLE_ID = 'rx-hud-style';
3
+ /** Marks a node as belonging to the current run rather than to `setup`. */
4
+ const RUN_ATTRIBUTE = 'data-rx-run';
5
+ export function createHud(container, viewport) {
6
+ if (!document.getElementById(STYLE_ID)) {
7
+ const style = document.createElement('style');
8
+ style.id = STYLE_ID;
9
+ style.textContent = HUD_CSS;
10
+ document.head.append(style);
11
+ }
12
+ const root = document.createElement('div');
13
+ root.className = 'rx-hud';
14
+ container.append(root);
15
+ const layers = new Map();
16
+ let sealed = false;
17
+ /**
18
+ * Ownership is an attribute on the node, not a list held here. A game removes
19
+ * HUD elements by hand (`banner.dismiss()`, `el.remove()`), and a list would
20
+ * keep every one of those detached nodes alive until the next restart.
21
+ */
22
+ function own(el) {
23
+ if (sealed)
24
+ el.setAttribute(RUN_ATTRIBUTE, '');
25
+ return el;
26
+ }
27
+ function sync() {
28
+ const { safeArea, hudScale } = viewport;
29
+ root.style.setProperty('--rx-safe-top', `${safeArea.top}px`);
30
+ root.style.setProperty('--rx-safe-right', `${safeArea.right}px`);
31
+ root.style.setProperty('--rx-safe-bottom', `${safeArea.bottom}px`);
32
+ root.style.setProperty('--rx-safe-left', `${safeArea.left}px`);
33
+ // The HUD scales with the rendered design box, so a 900px-wide desktop
34
+ // window and a 393px phone show the same layout at proportional size.
35
+ root.style.setProperty('--rx-hud-scale', hudScale.toFixed(4));
36
+ }
37
+ sync();
38
+ const offResize = viewport.on('resize', sync);
39
+ function layerFor(anchor, options = {}) {
40
+ const existing = layers.get(anchor);
41
+ if (existing) {
42
+ if (options.interactive)
43
+ existing.dataset.interactive = 'true';
44
+ return existing;
45
+ }
46
+ const layer = own(document.createElement('div'));
47
+ layer.className = 'rx-hud-layer';
48
+ layer.dataset.anchor = anchor;
49
+ layer.style.gridArea = anchor;
50
+ if (options.interactive)
51
+ layer.dataset.interactive = 'true';
52
+ root.append(layer);
53
+ layers.set(anchor, layer);
54
+ return layer;
55
+ }
56
+ return {
57
+ root,
58
+ layer: layerFor,
59
+ score(options = {}) {
60
+ const format = options.format ?? ((value) => String(Math.floor(value)));
61
+ const el = own(document.createElement('div'));
62
+ el.className = 'rx-hud-score';
63
+ el.textContent = format(0);
64
+ layerFor(options.anchor ?? 'top').append(el);
65
+ return {
66
+ el,
67
+ set(value) {
68
+ const next = format(value);
69
+ // Guard the write: assigning identical text still invalidates layout,
70
+ // and this runs every frame in most games.
71
+ if (el.textContent !== next)
72
+ el.textContent = next;
73
+ },
74
+ };
75
+ },
76
+ chip(text, options = {}) {
77
+ const el = own(document.createElement('div'));
78
+ el.className = 'rx-hud-chip';
79
+ el.textContent = text;
80
+ layerFor(options.anchor ?? 'top-left').append(el);
81
+ return {
82
+ el,
83
+ set(next) {
84
+ if (el.textContent !== next)
85
+ el.textContent = next;
86
+ },
87
+ };
88
+ },
89
+ button(label, onTap, options = {}) {
90
+ const el = own(document.createElement('button'));
91
+ el.type = 'button';
92
+ el.className = 'rx-hud-button';
93
+ el.dataset.variant = options.variant ?? 'default';
94
+ el.textContent = label;
95
+ // pointerup, not click: a click fires ~300ms later on some mobile
96
+ // WebViews and after the game has already moved on.
97
+ el.addEventListener('pointerup', (event) => {
98
+ event.preventDefault();
99
+ event.stopPropagation();
100
+ onTap();
101
+ });
102
+ layerFor(options.anchor ?? 'bottom', { interactive: true }).append(el);
103
+ return el;
104
+ },
105
+ banner(title, subtitle) {
106
+ const el = own(document.createElement('div'));
107
+ el.className = 'rx-hud-banner';
108
+ const titleEl = document.createElement('div');
109
+ titleEl.className = 'rx-hud-banner-title';
110
+ titleEl.textContent = title;
111
+ el.append(titleEl);
112
+ if (subtitle) {
113
+ const subtitleEl = document.createElement('div');
114
+ subtitleEl.className = 'rx-hud-banner-subtitle';
115
+ subtitleEl.textContent = subtitle;
116
+ el.append(subtitleEl);
117
+ }
118
+ root.append(el);
119
+ return { el, dismiss: () => el.remove() };
120
+ },
121
+ sealSetup() {
122
+ sealed = true;
123
+ },
124
+ resetRun() {
125
+ for (const el of Array.from(root.querySelectorAll(`[${RUN_ATTRIBUTE}]`)))
126
+ el.remove();
127
+ for (const [anchor, layer] of layers) {
128
+ if (layer.hasAttribute(RUN_ATTRIBUTE))
129
+ layers.delete(anchor);
130
+ }
131
+ },
132
+ clear() {
133
+ root.replaceChildren();
134
+ layers.clear();
135
+ },
136
+ dispose() {
137
+ offResize();
138
+ root.remove();
139
+ layers.clear();
140
+ },
141
+ };
142
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The single stylesheet the HUD injects.
3
+ *
4
+ * The Remix tokens are literal values, not an import of
5
+ * `@farcade/design-tokens`: this package publishes to npm, and the repo rule is
6
+ * that a public package must not carry a `workspace:` dependency.
7
+ *
8
+ * Fonts are named, not embedded. Outfit is the design system's body face and
9
+ * resolves in the Remix shell and the desktop preview; the fallback stack keeps
10
+ * a bare-file build from rendering in a serif. Embedding a subset would remove
11
+ * the fallback risk entirely at ~55 KB — see AGENTS.md.
12
+ */
13
+ export declare const HUD_CSS = "\n.rx-hud {\n --rx-black: #060b0f;\n --rx-white: #fafafa;\n --rx-lime: #b6ff00;\n --rx-section: #171717;\n --rx-secondary: #27272a;\n --rx-muted: #a1a1aa;\n --rx-font: 'Outfit', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n --rx-hud-scale: 1;\n --rx-safe-top: 0px;\n --rx-safe-right: 0px;\n --rx-safe-bottom: 0px;\n --rx-safe-left: 0px;\n\n position: absolute;\n inset: 0;\n display: grid;\n grid-template-rows: auto 1fr auto;\n grid-template-columns: auto 1fr auto;\n grid-template-areas:\n 'top-left top top-right'\n 'left center right'\n 'bottom-left bottom bottom-right';\n /* env() first so the notch is respected from frame one, then the platform's\n own contentSafeAreaInset lands ~a few frames later and transitions in. The\n 150ms makes the correction read as motion instead of a snap. */\n padding-top: max(var(--rx-safe-top), env(safe-area-inset-top, 0px));\n padding-right: max(var(--rx-safe-right), env(safe-area-inset-right, 0px));\n padding-bottom: max(var(--rx-safe-bottom), env(safe-area-inset-bottom, 0px));\n padding-left: max(var(--rx-safe-left), env(safe-area-inset-left, 0px));\n transition: padding 150ms ease-out;\n pointer-events: none;\n user-select: none;\n -webkit-user-select: none;\n font-family: var(--rx-font);\n color: var(--rx-white);\n z-index: 10;\n}\n\n.rx-hud-layer {\n display: flex;\n flex-direction: column;\n gap: calc(8px * var(--rx-hud-scale));\n padding: calc(12px * var(--rx-hud-scale));\n pointer-events: none;\n}\n.rx-hud-layer[data-interactive='true'] { pointer-events: auto; }\n\n.rx-hud-layer[data-anchor^='top'] { align-self: start; }\n.rx-hud-layer[data-anchor^='bottom'] { align-self: end; }\n.rx-hud-layer[data-anchor$='left'] { align-items: flex-start; justify-self: start; }\n.rx-hud-layer[data-anchor$='right'] { align-items: flex-end; justify-self: end; }\n.rx-hud-layer[data-anchor='top'],\n.rx-hud-layer[data-anchor='bottom'],\n.rx-hud-layer[data-anchor='center'] { align-items: center; justify-self: center; }\n.rx-hud-layer[data-anchor='left'] { align-items: flex-start; align-self: center; }\n.rx-hud-layer[data-anchor='right'] { align-items: flex-end; align-self: center; }\n.rx-hud-layer[data-anchor='center'] { align-self: center; }\n\n.rx-hud-score {\n font-size: calc(40px * var(--rx-hud-scale));\n font-weight: 600;\n line-height: 1;\n letter-spacing: -0.02em;\n font-variant-numeric: tabular-nums;\n}\n\n.rx-hud-chip {\n font-size: calc(14px * var(--rx-hud-scale));\n font-weight: 500;\n line-height: 1;\n padding: calc(6px * var(--rx-hud-scale)) calc(10px * var(--rx-hud-scale));\n border-radius: calc(6px * var(--rx-hud-scale));\n background: color-mix(in srgb, var(--rx-black) 60%, transparent);\n border: 1px solid var(--rx-secondary);\n color: var(--rx-white);\n}\n\n.rx-hud-button {\n font-family: var(--rx-font);\n font-size: calc(16px * var(--rx-hud-scale));\n font-weight: 600;\n line-height: 1;\n padding: calc(12px * var(--rx-hud-scale)) calc(20px * var(--rx-hud-scale));\n border-radius: calc(6px * var(--rx-hud-scale));\n border: none;\n background: var(--rx-white);\n color: var(--rx-black);\n pointer-events: auto;\n cursor: pointer;\n touch-action: manipulation;\n transition: transform 150ms ease-out, opacity 150ms ease-out;\n /* 44px is the smallest reliable touch target; scaled type must not shrink\n below it or the button becomes a coin flip on a phone. */\n min-height: calc(44px * var(--rx-hud-scale));\n}\n.rx-hud-button:active { transform: scale(0.98); }\n.rx-hud-button[data-variant='cta'] { background: var(--rx-lime); color: var(--rx-black); }\n.rx-hud-button[data-variant='ghost'] {\n background: transparent;\n color: var(--rx-white);\n border: 1px solid var(--rx-secondary);\n}\n\n.rx-hud-banner {\n grid-area: center;\n align-self: center;\n justify-self: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: calc(8px * var(--rx-hud-scale));\n padding: calc(24px * var(--rx-hud-scale));\n border-radius: calc(16px * var(--rx-hud-scale));\n background: color-mix(in srgb, var(--rx-section) 92%, transparent);\n border: 1px solid var(--rx-secondary);\n text-align: center;\n pointer-events: auto;\n}\n.rx-hud-banner-title {\n font-size: calc(30px * var(--rx-hud-scale));\n font-weight: 600;\n line-height: 1.25;\n}\n.rx-hud-banner-subtitle {\n font-size: calc(16px * var(--rx-hud-scale));\n color: var(--rx-muted);\n}\n";
14
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/hud/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,03IAiInB,CAAA"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The single stylesheet the HUD injects.
3
+ *
4
+ * The Remix tokens are literal values, not an import of
5
+ * `@farcade/design-tokens`: this package publishes to npm, and the repo rule is
6
+ * that a public package must not carry a `workspace:` dependency.
7
+ *
8
+ * Fonts are named, not embedded. Outfit is the design system's body face and
9
+ * resolves in the Remix shell and the desktop preview; the fallback stack keeps
10
+ * a bare-file build from rendering in a serif. Embedding a subset would remove
11
+ * the fallback risk entirely at ~55 KB — see AGENTS.md.
12
+ */
13
+ export const HUD_CSS = `
14
+ .rx-hud {
15
+ --rx-black: #060b0f;
16
+ --rx-white: #fafafa;
17
+ --rx-lime: #b6ff00;
18
+ --rx-section: #171717;
19
+ --rx-secondary: #27272a;
20
+ --rx-muted: #a1a1aa;
21
+ --rx-font: 'Outfit', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
22
+ --rx-hud-scale: 1;
23
+ --rx-safe-top: 0px;
24
+ --rx-safe-right: 0px;
25
+ --rx-safe-bottom: 0px;
26
+ --rx-safe-left: 0px;
27
+
28
+ position: absolute;
29
+ inset: 0;
30
+ display: grid;
31
+ grid-template-rows: auto 1fr auto;
32
+ grid-template-columns: auto 1fr auto;
33
+ grid-template-areas:
34
+ 'top-left top top-right'
35
+ 'left center right'
36
+ 'bottom-left bottom bottom-right';
37
+ /* env() first so the notch is respected from frame one, then the platform's
38
+ own contentSafeAreaInset lands ~a few frames later and transitions in. The
39
+ 150ms makes the correction read as motion instead of a snap. */
40
+ padding-top: max(var(--rx-safe-top), env(safe-area-inset-top, 0px));
41
+ padding-right: max(var(--rx-safe-right), env(safe-area-inset-right, 0px));
42
+ padding-bottom: max(var(--rx-safe-bottom), env(safe-area-inset-bottom, 0px));
43
+ padding-left: max(var(--rx-safe-left), env(safe-area-inset-left, 0px));
44
+ transition: padding 150ms ease-out;
45
+ pointer-events: none;
46
+ user-select: none;
47
+ -webkit-user-select: none;
48
+ font-family: var(--rx-font);
49
+ color: var(--rx-white);
50
+ z-index: 10;
51
+ }
52
+
53
+ .rx-hud-layer {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: calc(8px * var(--rx-hud-scale));
57
+ padding: calc(12px * var(--rx-hud-scale));
58
+ pointer-events: none;
59
+ }
60
+ .rx-hud-layer[data-interactive='true'] { pointer-events: auto; }
61
+
62
+ .rx-hud-layer[data-anchor^='top'] { align-self: start; }
63
+ .rx-hud-layer[data-anchor^='bottom'] { align-self: end; }
64
+ .rx-hud-layer[data-anchor$='left'] { align-items: flex-start; justify-self: start; }
65
+ .rx-hud-layer[data-anchor$='right'] { align-items: flex-end; justify-self: end; }
66
+ .rx-hud-layer[data-anchor='top'],
67
+ .rx-hud-layer[data-anchor='bottom'],
68
+ .rx-hud-layer[data-anchor='center'] { align-items: center; justify-self: center; }
69
+ .rx-hud-layer[data-anchor='left'] { align-items: flex-start; align-self: center; }
70
+ .rx-hud-layer[data-anchor='right'] { align-items: flex-end; align-self: center; }
71
+ .rx-hud-layer[data-anchor='center'] { align-self: center; }
72
+
73
+ .rx-hud-score {
74
+ font-size: calc(40px * var(--rx-hud-scale));
75
+ font-weight: 600;
76
+ line-height: 1;
77
+ letter-spacing: -0.02em;
78
+ font-variant-numeric: tabular-nums;
79
+ }
80
+
81
+ .rx-hud-chip {
82
+ font-size: calc(14px * var(--rx-hud-scale));
83
+ font-weight: 500;
84
+ line-height: 1;
85
+ padding: calc(6px * var(--rx-hud-scale)) calc(10px * var(--rx-hud-scale));
86
+ border-radius: calc(6px * var(--rx-hud-scale));
87
+ background: color-mix(in srgb, var(--rx-black) 60%, transparent);
88
+ border: 1px solid var(--rx-secondary);
89
+ color: var(--rx-white);
90
+ }
91
+
92
+ .rx-hud-button {
93
+ font-family: var(--rx-font);
94
+ font-size: calc(16px * var(--rx-hud-scale));
95
+ font-weight: 600;
96
+ line-height: 1;
97
+ padding: calc(12px * var(--rx-hud-scale)) calc(20px * var(--rx-hud-scale));
98
+ border-radius: calc(6px * var(--rx-hud-scale));
99
+ border: none;
100
+ background: var(--rx-white);
101
+ color: var(--rx-black);
102
+ pointer-events: auto;
103
+ cursor: pointer;
104
+ touch-action: manipulation;
105
+ transition: transform 150ms ease-out, opacity 150ms ease-out;
106
+ /* 44px is the smallest reliable touch target; scaled type must not shrink
107
+ below it or the button becomes a coin flip on a phone. */
108
+ min-height: calc(44px * var(--rx-hud-scale));
109
+ }
110
+ .rx-hud-button:active { transform: scale(0.98); }
111
+ .rx-hud-button[data-variant='cta'] { background: var(--rx-lime); color: var(--rx-black); }
112
+ .rx-hud-button[data-variant='ghost'] {
113
+ background: transparent;
114
+ color: var(--rx-white);
115
+ border: 1px solid var(--rx-secondary);
116
+ }
117
+
118
+ .rx-hud-banner {
119
+ grid-area: center;
120
+ align-self: center;
121
+ justify-self: center;
122
+ display: flex;
123
+ flex-direction: column;
124
+ align-items: center;
125
+ gap: calc(8px * var(--rx-hud-scale));
126
+ padding: calc(24px * var(--rx-hud-scale));
127
+ border-radius: calc(16px * var(--rx-hud-scale));
128
+ background: color-mix(in srgb, var(--rx-section) 92%, transparent);
129
+ border: 1px solid var(--rx-secondary);
130
+ text-align: center;
131
+ pointer-events: auto;
132
+ }
133
+ .rx-hud-banner-title {
134
+ font-size: calc(30px * var(--rx-hud-scale));
135
+ font-weight: 600;
136
+ line-height: 1.25;
137
+ }
138
+ .rx-hud-banner-subtitle {
139
+ font-size: calc(16px * var(--rx-hud-scale));
140
+ color: var(--rx-muted);
141
+ }
142
+ `;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `@remix-gg/three` — the Remix Three.js SDK for portrait mobile games.
3
+ *
4
+ * This barrel deliberately does NOT re-export the three namespace. Reach three
5
+ * only through the dedicated subpath:
6
+ *
7
+ * import * as THREE from '@remix-gg/three/three'
8
+ *
9
+ * See `src/three.ts` for the measured reason.
10
+ */
11
+ export { createGame, type RemixGame, type RemixGameOptions } from './game.js';
12
+ export { stepClock, MAX_FRAME_DELTA, type StepResult } from './loop.js';
13
+ export { createViewport, fitFrustum, safeRectOf, DEFAULT_DESIGN, type DesignBox, type FitMode, type FrustumFit, type Rect, type Viewport, } from './viewport.js';
14
+ export { fitBounds, followRig, orthoRig, portraitRig, type CameraRig, type FollowRigOptions, type GameCamera, type OrthoRigOptions, type PortraitRigOptions, } from './camera.js';
15
+ export { createGestureState, reduceGestures, DEFAULT_THRESHOLDS, type GestureEvent, type GestureEventMap, type GestureState, type GestureThresholds, type PointerSample, } from './input/gestures.js';
16
+ export type { DragEvent, Input, PinchEvent, Pointer, SwipeDirection, SwipeEvent, TapEvent, } from './input/index.js';
17
+ export { createAssets, type AssetManifest, type Assets } from './assets.js';
18
+ export { createEscalatingChance, createPityTimer, createShuffleBag, seededRandom, type EscalatingChance, type EscalatingChanceOptions, type PityTimer, type PityTimerOptions, type ShuffleBag, type ShuffleBagEntry, type ShuffleBagOptions, } from './random.js';
19
+ export { createHitStop, createTweens, easings, shakeRig, type Easing, type EasingName, type HitStop, type ShakeRig, type ShakeRigOptions, type TweenHandle, type TweenOptions, type Tweens, } from './juice.js';
20
+ export { createRamp, type RampOptions } from './ramp.js';
21
+ export { createGraceWindow, createInputBuffer, type GraceWindow, type InputBuffer, } from './forgiveness.js';
22
+ export { createAudio, type AudioBus, type PlayMusicOptions, type PlaySfxOptions, } from './audio.js';
23
+ export { destroy, disposeObject, DisposalScope, type Disposable, type DisposeOptions, } from './scene/dispose.js';
24
+ export { Pool, type PoolOptions } from './scene/pool.js';
25
+ export { applyLighting, blobShadow, type BlobShadow, type Lighting, type LightingOptions, type LightingPreset, } from './scene/lighting.js';
26
+ export { createHud, type Hud, type HudAnchor, type HudBanner, type HudChip, type HudScore, } from './hud/index.js';
27
+ export { HUD_CSS } from './hud/styles.js';
28
+ export { awaitSdk, createPlatform, type Platform } from './platform/index.js';
29
+ export type { FarcadeSDKGlobal, GameInfo, GameState, HapticFeedbackType, InventoryItem, Player, SafeAreaInset, ShopItem, ViewContext, } from './platform/sdk-contract.js';
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `@remix-gg/three` — the Remix Three.js SDK for portrait mobile games.
3
+ *
4
+ * This barrel deliberately does NOT re-export the three namespace. Reach three
5
+ * only through the dedicated subpath:
6
+ *
7
+ * import * as THREE from '@remix-gg/three/three'
8
+ *
9
+ * See `src/three.ts` for the measured reason.
10
+ */
11
+ export { createGame } from './game.js';
12
+ export { stepClock, MAX_FRAME_DELTA } from './loop.js';
13
+ export { createViewport, fitFrustum, safeRectOf, DEFAULT_DESIGN, } from './viewport.js';
14
+ export { fitBounds, followRig, orthoRig, portraitRig, } from './camera.js';
15
+ export { createGestureState, reduceGestures, DEFAULT_THRESHOLDS, } from './input/gestures.js';
16
+ export { createAssets } from './assets.js';
17
+ export { createEscalatingChance, createPityTimer, createShuffleBag, seededRandom, } from './random.js';
18
+ export { createHitStop, createTweens, easings, shakeRig, } from './juice.js';
19
+ export { createRamp } from './ramp.js';
20
+ export { createGraceWindow, createInputBuffer, } from './forgiveness.js';
21
+ export { createAudio, } from './audio.js';
22
+ export { destroy, disposeObject, DisposalScope, } from './scene/dispose.js';
23
+ export { Pool } from './scene/pool.js';
24
+ export { applyLighting, blobShadow, } from './scene/lighting.js';
25
+ export { createHud, } from './hud/index.js';
26
+ export { HUD_CSS } from './hud/styles.js';
27
+ export { awaitSdk, createPlatform } from './platform/index.js';