castle-web-sdk 0.4.43 → 0.4.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1008,6 +1008,55 @@ Host-specific layout flags such as `CastleEmbed.feed` are deprecated.
1008
1008
  New code should let Castle hosts provide the frame and let the SDK infer
1009
1009
  whether it is running standalone or embedded.
1010
1010
 
1011
+ ### Filling the screen: `<meta name="castle-display">`
1012
+
1013
+ By default a deck is a 5:7 portrait card. A deck that can lay itself out at
1014
+ other shapes opts out of the card in its own `index.html`:
1015
+
1016
+ ```html
1017
+ <meta name="castle-display" content="layout=fill">
1018
+ <!-- or narrow the shapes it accepts (width / height, as 16/9, 16:9 or 1.78) -->
1019
+ <meta name="castle-display" content="layout=fill, min-aspect=9/16, max-aspect=16/9">
1020
+ ```
1021
+
1022
+ A `fill` deck gets the host's whole frame, edge to edge with no rounded
1023
+ corners, whenever that frame's aspect is inside its range (default 9/21 to
1024
+ 21/9); outside it, the deck gets the largest in-range box, centered. Castle's
1025
+ web player gives a fill deck a bigger stage and a fullscreen button. Hosts
1026
+ that don't know the tag still give it a 5:7 frame, so a fill deck should look
1027
+ right at 5:7 as well.
1028
+
1029
+ Lay out against the stage: the element `initCard()` returns, or
1030
+ `--castle-card-w`/`--castle-card-h` in CSS, with a resize listener. Not the
1031
+ window: the stage is smaller than the window when the host frame is outside
1032
+ the deck's range, and inside `castle-web serve`'s own page. `getDisplay()` returns the parsed declaration and `fitStage(w, h,
1033
+ display)` the box the SDK would pick.
1034
+
1035
+ Size HUD, buttons and text in the card unit, not in `vw` or `cqw`:
1036
+
1037
+ ```css
1038
+ .score { font-size: calc(5 * var(--castle-unit)); top: calc(3 * var(--castle-unit)); }
1039
+ ```
1040
+
1041
+ `--castle-unit` is 1% of the width of the largest 5:7 card that fits in the
1042
+ stage. On a card, or a stage taller than a card, it is 1% of the stage width
1043
+ (the same as `1vw`); on a wide stage it stops growing, so a HUD keeps its card
1044
+ size while the playfield gets the extra room. Anything sized in `vw` grows with
1045
+ the stage instead, and a 16:9 monitor makes it huge. Canvas code gets the same
1046
+ number from `cardUnit(width, height)` (px). The SDK sets the exact value for
1047
+ its own stage; in edit mode, and in a kit's scene editor that draws the stage
1048
+ smaller than the window, it falls back to the window's.
1049
+
1050
+ A 3D camera tuned for the card keeps its framing with `fovForAspect(fov,
1051
+ aspect)`: it returns `fov` at 5:7 and wider (a wide stage shows more to the
1052
+ sides) and widens it on a stage narrower than 5:7, so a tall phone still sees
1053
+ the card's full width.
1054
+
1055
+ ```js
1056
+ camera.fov = fovForAspect(50, width / height);
1057
+ camera.updateProjectionMatrix();
1058
+ ```
1059
+
1011
1060
  ### `CARD_RATIO`
1012
1061
 
1013
1062
  The card aspect ratio (`5 / 7`). Use this if you need to size something
package/dist/castle.d.ts CHANGED
@@ -29,6 +29,8 @@ export { Portal } from "./portal";
29
29
  export type { CastlePortalApi, PortalOpenResult, PortalOpenStatus, PortalPrefetchResult, PortalPrefetchStatus, } from "./portal";
30
30
  export { CARD_RATIO, deleteFile, fileUrl, initCard, onBeforeRestart, onFilesChanged, onSaveReloadState, openFile, renameFile, requestReload, setup, takeReloadState, writeFileOnce, } from "./runtime";
31
31
  export type { FileChange, FilesChangedEvent } from "./runtime";
32
+ export { cardUnit, fitStage, fovForAspect, getDisplay } from "./display";
33
+ export type { DeckDisplay, DeckLayout } from "./display";
32
34
  export { flushSaves, hasPendingSave, onSaveState, writeFile, } from "./saveQueue";
33
35
  export type { SaveState } from "./saveQueue";
34
36
  export { SharedStorage, Storage } from "./storage";
package/dist/castle.js CHANGED
@@ -15,6 +15,7 @@ export { Pass } from "./passes";
15
15
  export { Snapshot } from "./snapshots";
16
16
  export { Portal } from "./portal";
17
17
  export { CARD_RATIO, deleteFile, fileUrl, initCard, onBeforeRestart, onFilesChanged, onSaveReloadState, openFile, renameFile, requestReload, setup, takeReloadState, writeFileOnce, } from "./runtime";
18
+ export { cardUnit, fitStage, fovForAspect, getDisplay } from "./display";
18
19
  export { flushSaves, hasPendingSave, onSaveState, writeFile, } from "./saveQueue";
19
20
  export { SharedStorage, Storage } from "./storage";
20
21
  export { Store } from "./store";
@@ -0,0 +1,36 @@
1
+ export type DeckLayout = "card" | "fill";
2
+ export interface DeckDisplay {
3
+ layout: DeckLayout;
4
+ /** Narrowest width / height the deck accepts. */
5
+ minAspect: number;
6
+ /** Widest width / height the deck accepts. */
7
+ maxAspect: number;
8
+ }
9
+ export declare const CARD_ASPECT: number;
10
+ /** Default fill range: a tall 21:9 phone held upright to a 21:9 monitor. */
11
+ export declare const FILL_MIN_ASPECT: number;
12
+ export declare const FILL_MAX_ASPECT: number;
13
+ export declare const CARD_DISPLAY: DeckDisplay;
14
+ export declare function parseDisplayContent(content: string | null | undefined): DeckDisplay;
15
+ /** The display this deck declared in its index.html (a card when it declared none). */
16
+ export declare function getDisplay(): DeckDisplay;
17
+ /**
18
+ * The card unit, in px, for a stage of `width` x `height`: 1% of the width of
19
+ * the largest 5:7 card that fits in it. On a card it is 1% of the card's width;
20
+ * on a wider stage it stops growing, so HUD and text sized in it stay the size
21
+ * they are on a card while the playfield gets the extra room. CSS gets the same
22
+ * value as `var(--castle-unit)`.
23
+ */
24
+ export declare function cardUnit(width: number, height: number): number;
25
+ /**
26
+ * A vertical field of view (degrees) for a camera tuned at 5:7. On a stage
27
+ * narrower than 5:7 (a tall phone) it widens so the card's horizontal extent
28
+ * stays in view; at 5:7 and wider it returns `fovDeg` unchanged, so a wide
29
+ * stage just shows more to the sides. Capped at 120 degrees.
30
+ */
31
+ export declare function fovForAspect(fovDeg: number, aspect: number): number;
32
+ /** The largest box inside `width` x `height` whose aspect is in the display's range. */
33
+ export declare function fitStage(width: number, height: number, display: DeckDisplay): {
34
+ w: number;
35
+ h: number;
36
+ };
@@ -0,0 +1,96 @@
1
+ // How much of the host's frame a deck may use.
2
+ //
3
+ // A deck declares it in its own index.html, the way a page declares its
4
+ // viewport:
5
+ //
6
+ // <meta name="castle-display" content="layout=fill">
7
+ // <meta name="castle-display" content="layout=fill, min-aspect=9/16, max-aspect=16/9">
8
+ //
9
+ // With no tag the deck is a `card`: the classic 5:7 portrait card, exactly as
10
+ // before. A `fill` deck takes whatever box the host gives it, as long as the
11
+ // box's aspect (width / height) is inside [minAspect, maxAspect]; outside that
12
+ // range it gets the largest box in range, centered. A card is just the range
13
+ // [5/7, 5/7].
14
+ //
15
+ // Hosts read the same tag out of the bundle to size the frame before the deck
16
+ // loads (castle-www common/deckDisplay.ts mirrors this parser). A host that
17
+ // knows nothing about it still gives the deck a 5:7 frame, so a fill deck
18
+ // should render well at 5:7 too; one whose range leaves 5/7 out (landscape
19
+ // only) is letterboxed inside such a frame.
20
+ export const CARD_ASPECT = 5 / 7;
21
+ /** Default fill range: a tall 21:9 phone held upright to a 21:9 monitor. */
22
+ export const FILL_MIN_ASPECT = 9 / 21;
23
+ export const FILL_MAX_ASPECT = 21 / 9;
24
+ export const CARD_DISPLAY = {
25
+ layout: "card",
26
+ minAspect: CARD_ASPECT,
27
+ maxAspect: CARD_ASPECT,
28
+ };
29
+ // "16/9", "16:9" or "1.78"; anything else (or a non-positive value) is null.
30
+ function parseAspect(value) {
31
+ const ratio = value.match(/^\s*(\d+(?:\.\d+)?)\s*[/:]\s*(\d+(?:\.\d+)?)\s*$/);
32
+ const n = ratio ? Number(ratio[1]) / Number(ratio[2]) : Number(value);
33
+ return Number.isFinite(n) && n > 0 ? n : null;
34
+ }
35
+ export function parseDisplayContent(content) {
36
+ if (!content)
37
+ return CARD_DISPLAY;
38
+ const fields = {};
39
+ for (const part of content.split(/[,;]/)) {
40
+ const eq = part.indexOf("=");
41
+ if (eq < 0)
42
+ continue;
43
+ fields[part.slice(0, eq).trim().toLowerCase()] = part.slice(eq + 1).trim();
44
+ }
45
+ if (fields.layout?.toLowerCase() !== "fill")
46
+ return CARD_DISPLAY;
47
+ let minAspect = parseAspect(fields["min-aspect"] ?? "") ?? FILL_MIN_ASPECT;
48
+ let maxAspect = parseAspect(fields["max-aspect"] ?? "") ?? FILL_MAX_ASPECT;
49
+ if (minAspect > maxAspect)
50
+ [minAspect, maxAspect] = [maxAspect, minAspect];
51
+ return { layout: "fill", minAspect, maxAspect };
52
+ }
53
+ let cached = null;
54
+ /** The display this deck declared in its index.html (a card when it declared none). */
55
+ export function getDisplay() {
56
+ if (cached)
57
+ return cached;
58
+ if (typeof document === "undefined")
59
+ return CARD_DISPLAY;
60
+ const meta = document.querySelector('meta[name="castle-display"]');
61
+ cached = parseDisplayContent(meta?.content);
62
+ return cached;
63
+ }
64
+ /**
65
+ * The card unit, in px, for a stage of `width` x `height`: 1% of the width of
66
+ * the largest 5:7 card that fits in it. On a card it is 1% of the card's width;
67
+ * on a wider stage it stops growing, so HUD and text sized in it stay the size
68
+ * they are on a card while the playfield gets the extra room. CSS gets the same
69
+ * value as `var(--castle-unit)`.
70
+ */
71
+ export function cardUnit(width, height) {
72
+ return Math.min(Math.max(0, width), Math.max(0, height) * CARD_ASPECT) / 100;
73
+ }
74
+ /**
75
+ * A vertical field of view (degrees) for a camera tuned at 5:7. On a stage
76
+ * narrower than 5:7 (a tall phone) it widens so the card's horizontal extent
77
+ * stays in view; at 5:7 and wider it returns `fovDeg` unchanged, so a wide
78
+ * stage just shows more to the sides. Capped at 120 degrees.
79
+ */
80
+ export function fovForAspect(fovDeg, aspect) {
81
+ if (!(aspect > 0) || aspect >= CARD_ASPECT)
82
+ return fovDeg;
83
+ const half = Math.atan((Math.tan((fovDeg * Math.PI) / 360) * CARD_ASPECT) / aspect);
84
+ return Math.min(120, (half * 360) / Math.PI);
85
+ }
86
+ /** The largest box inside `width` x `height` whose aspect is in the display's range. */
87
+ export function fitStage(width, height, display) {
88
+ const aw = Math.max(1, width);
89
+ const ah = Math.max(1, height);
90
+ const aspect = aw / ah;
91
+ if (aspect > display.maxAspect)
92
+ return { w: ah * display.maxAspect, h: ah };
93
+ if (aspect < display.minAspect)
94
+ return { w: aw, h: aw / display.minAspect };
95
+ return { w: aw, h: ah };
96
+ }
package/dist/runtime.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CASTLE_SDK_PROTOCOL, } from "./commands";
2
2
  import { getCastleEmbed, isEdit } from "./context";
3
+ import { cardUnit, fitStage, getDisplay } from "./display";
3
4
  import { appliedNotice, applyTunedParams, } from "./paramsRegistry";
4
5
  export const CARD_RATIO = 5 / 7;
5
6
  // Host -> deck capture request, and the deck's answer. Kept in sync with the
@@ -31,6 +32,7 @@ export function setup() {
31
32
  initHostCapture();
32
33
  initPlaySelection();
33
34
  initEditSelection();
35
+ initCardUnitFallback();
34
36
  initPlayCard();
35
37
  logPanelLoad();
36
38
  }
@@ -120,7 +122,7 @@ export function initCard() {
120
122
  body { display: flex; align-items: center; justify-content: center; }
121
123
  #castle-card {
122
124
  position: relative;
123
- border-radius: 4% / calc(4% * (5 / 7));
125
+ border-radius: ${cardRadius()};
124
126
  overflow: hidden;
125
127
  background: #121213;
126
128
  }
@@ -129,10 +131,12 @@ export function initCard() {
129
131
  const card = document.createElement("div");
130
132
  card.id = "castle-card";
131
133
  document.body.appendChild(card);
134
+ initCardUnitFallback();
132
135
  function resize() {
133
136
  const { w, h } = computeCardSize();
134
137
  card.style.width = w + "px";
135
138
  card.style.height = h + "px";
139
+ setCardUnit(w, h);
136
140
  }
137
141
  resize();
138
142
  window.addEventListener("resize", resize);
@@ -231,8 +235,10 @@ function initEditSelection() {
231
235
  selection.removeAllRanges();
232
236
  });
233
237
  }
234
- // Constrains whatever the deck renders into #root to a 5:7 card in play mode.
235
- // Hosts own max size and padding; the SDK only preserves the card aspect ratio.
238
+ // Constrains whatever the deck renders into #root to the deck's stage in play
239
+ // mode: a 5:7 card, or for a `fill` deck (see display.ts) the host's whole frame
240
+ // within the deck's aspect range. Hosts own max size and padding; the SDK only
241
+ // preserves the aspect.
236
242
  function initPlayCard() {
237
243
  if (isEdit())
238
244
  return;
@@ -247,7 +253,7 @@ function initPlayCard() {
247
253
  transform: translate(-50%, -50%) !important;
248
254
  width: var(--castle-card-w, 100vw) !important;
249
255
  height: var(--castle-card-h, 100vh) !important;
250
- border-radius: 4% / calc(4% * (5 / 7)) !important;
256
+ border-radius: ${cardRadius()} !important;
251
257
  overflow: hidden !important;
252
258
  }
253
259
  `;
@@ -276,25 +282,37 @@ function initPlayCard() {
276
282
  const { w, h } = computeCardSize();
277
283
  document.documentElement.style.setProperty("--castle-card-w", w + "px");
278
284
  document.documentElement.style.setProperty("--castle-card-h", h + "px");
285
+ setCardUnit(w, h);
279
286
  }
280
287
  resize();
281
288
  window.addEventListener("resize", resize);
282
289
  }
290
+ // `--castle-unit`: 1% of the width of the largest 5:7 card inside the stage
291
+ // (display.ts cardUnit). A deck sizes HUD and text in it so they keep card
292
+ // proportions on a wide fill stage. Play mode sets the exact px value on every
293
+ // resize; the stylesheet fallback covers edit mode and the moment before the
294
+ // first resize, and follows the window -- which is not the stage where a kit's
295
+ // scene editor draws the deck smaller inside the editor.
296
+ function setCardUnit(w, h) {
297
+ document.documentElement.style.setProperty("--castle-unit", cardUnit(w, h) + "px");
298
+ }
299
+ let cardUnitFallbackInstalled = false;
300
+ function initCardUnitFallback() {
301
+ if (cardUnitFallbackInstalled)
302
+ return;
303
+ cardUnitFallbackInstalled = true;
304
+ const style = document.createElement("style");
305
+ style.textContent = `:root { --castle-unit: min(1vw, 1vh * 5 / 7); }`;
306
+ document.head.appendChild(style);
307
+ }
283
308
  function computeCardSize() {
284
309
  const pad = shouldUseStandaloneChrome() ? 20 : 0;
285
- const aw = Math.max(1, window.innerWidth - pad * 2);
286
- const ah = Math.max(1, window.innerHeight - pad * 2);
287
- let w;
288
- let h;
289
- if (aw / ah < CARD_RATIO) {
290
- w = aw;
291
- h = w / CARD_RATIO;
292
- }
293
- else {
294
- h = ah;
295
- w = h * CARD_RATIO;
296
- }
297
- return { w, h };
310
+ return fitStage(window.innerWidth - pad * 2, window.innerHeight - pad * 2, getDisplay());
311
+ }
312
+ // A card keeps its rounded corners; a fill deck runs edge to edge and the host
313
+ // decides how its frame looks.
314
+ function cardRadius() {
315
+ return getDisplay().layout === "fill" ? "0" : "4% / calc(4% * (5 / 7))";
298
316
  }
299
317
  function shouldUseStandaloneChrome() {
300
318
  if (window.parent !== window)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.43",
3
+ "version": "0.4.44",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/castle-xyz/castle-experimental-web.git"