@cardanvil/frame-kit 0.5.0 → 0.7.0

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 (90) hide show
  1. package/README.md +119 -0
  2. package/dist/authoring/collectorInfo.d.ts +17 -0
  3. package/dist/authoring/collectorInfo.js +62 -0
  4. package/dist/authoring/index.d.ts +2 -0
  5. package/dist/authoring/index.js +2 -0
  6. package/dist/authoring/layoutDefaults.d.ts +20 -2
  7. package/dist/authoring/layoutDefaults.js +48 -13
  8. package/dist/authoring/opacityStops.d.ts +8 -0
  9. package/dist/authoring/opacityStops.js +17 -0
  10. package/dist/cli/index.d.ts +1 -0
  11. package/dist/cli/index.js +1 -0
  12. package/dist/cli/loadFrame.js +1 -1
  13. package/dist/cli/main.js +88 -0
  14. package/dist/cli/sourceWatcher.d.ts +40 -0
  15. package/dist/cli/sourceWatcher.js +100 -0
  16. package/dist/cli/studio/boxSets.d.ts +33 -0
  17. package/dist/cli/studio/boxSets.js +90 -0
  18. package/dist/cli/studio/edit/applyEdits.d.ts +71 -0
  19. package/dist/cli/studio/edit/applyEdits.js +177 -0
  20. package/dist/cli/studio/edit/impact.d.ts +53 -0
  21. package/dist/cli/studio/edit/impact.js +87 -0
  22. package/dist/cli/studio/edit/resolveModule.d.ts +10 -0
  23. package/dist/cli/studio/edit/resolveModule.js +55 -0
  24. package/dist/cli/studio/edit/resolvePath.d.ts +53 -0
  25. package/dist/cli/studio/edit/resolvePath.js +606 -0
  26. package/dist/cli/studio/edit/sourceIndex.d.ts +29 -0
  27. package/dist/cli/studio/edit/sourceIndex.js +73 -0
  28. package/dist/cli/studio/edit/ts.d.ts +14 -0
  29. package/dist/cli/studio/edit/ts.js +34 -0
  30. package/dist/cli/studio/serve.d.ts +23 -0
  31. package/dist/cli/studio/serve.js +516 -0
  32. package/dist/cli/studio/session.d.ts +73 -0
  33. package/dist/cli/studio/session.js +154 -0
  34. package/dist/cli/trim.d.ts +62 -0
  35. package/dist/cli/trim.js +251 -0
  36. package/dist/cli/watch.d.ts +3 -0
  37. package/dist/cli/watch.js +20 -89
  38. package/dist/layout/assetPlacement.d.ts +71 -0
  39. package/dist/layout/assetPlacement.js +104 -0
  40. package/dist/layout/border.d.ts +27 -0
  41. package/dist/layout/border.js +31 -0
  42. package/dist/layout/boxOverrides.d.ts +28 -0
  43. package/dist/layout/boxOverrides.js +95 -0
  44. package/dist/layout/color.d.ts +23 -0
  45. package/dist/layout/color.js +53 -0
  46. package/dist/layout/cutouts.d.ts +28 -0
  47. package/dist/layout/cutouts.js +29 -0
  48. package/dist/layout/decorations.d.ts +73 -0
  49. package/dist/layout/decorations.js +104 -0
  50. package/dist/layout/frameLayers.d.ts +163 -0
  51. package/dist/layout/frameLayers.js +433 -0
  52. package/dist/layout/index.d.ts +27 -0
  53. package/dist/layout/index.js +27 -0
  54. package/dist/layout/paint.d.ts +36 -0
  55. package/dist/layout/paint.js +148 -0
  56. package/dist/layout/powerToughnessRow.d.ts +26 -0
  57. package/dist/layout/powerToughnessRow.js +13 -0
  58. package/dist/layout/resolveFrameAsset.d.ts +37 -0
  59. package/dist/layout/resolveFrameAsset.js +66 -0
  60. package/dist/layout/rulesSlide.d.ts +13 -0
  61. package/dist/layout/rulesSlide.js +33 -0
  62. package/dist/layout/settings.d.ts +26 -0
  63. package/dist/layout/settings.js +62 -0
  64. package/dist/layout/textStyle.d.ts +29 -0
  65. package/dist/layout/textStyle.js +28 -0
  66. package/dist/layout/turn.d.ts +54 -0
  67. package/dist/layout/turn.js +107 -0
  68. package/dist/manifest/contract.d.ts +6103 -9063
  69. package/dist/manifest/contract.js +48 -1
  70. package/dist/manifest/testFrame.js +9 -0
  71. package/dist/schema/collectorInfo.d.ts +95 -0
  72. package/dist/schema/collectorInfo.js +80 -0
  73. package/dist/schema/condition.d.ts +67 -0
  74. package/dist/schema/condition.js +47 -0
  75. package/dist/schema/frame.d.ts +17213 -24450
  76. package/dist/schema/frame.js +418 -14
  77. package/dist/schema/frameAssets.d.ts +56 -34
  78. package/dist/schema/frameAssets.js +96 -2
  79. package/dist/schema/index.d.ts +3 -0
  80. package/dist/schema/index.js +3 -0
  81. package/dist/schema/layout.d.ts +29 -4
  82. package/dist/schema/layout.js +27 -2
  83. package/dist/schema/paint.d.ts +202 -0
  84. package/dist/schema/paint.js +109 -0
  85. package/dist/studio/assets/PlantinMTProRgIt-D0FB51kb.woff2 +0 -0
  86. package/dist/studio/assets/beleren-b-B3lFvodX.ttf +0 -0
  87. package/dist/studio/assets/beleren-bsc-yvL6mU7X.ttf +0 -0
  88. package/dist/studio/assets/index-BnhNwm_r.js +74 -0
  89. package/dist/studio/index.html +12 -0
  90. package/package.json +16 -5
@@ -0,0 +1,26 @@
1
+ import type { PowerToughnessRow } from "../schema/frame.js";
2
+ /** How wide each part of a `powerToughnessRow` is drawn, in sheet px. */
3
+ export interface PowerToughnessRowWidths {
4
+ power: number;
5
+ toughness: number;
6
+ /** The widest image in the power's `ptSlot`; 0, or left out, for none. */
7
+ powerSymbol?: number;
8
+ /** The widest image in the toughness's `ptSlot`; 0, or left out, for none. */
9
+ toughnessSymbol?: number;
10
+ }
11
+ /** Where a `powerToughnessRow` puts each part, in sheet x. */
12
+ export interface PowerToughnessRowPlacement {
13
+ /** The power number's left edge. */
14
+ power: number;
15
+ /** The toughness number's left edge. */
16
+ toughness: number;
17
+ /** Where the power's symbols end: their right edge. */
18
+ powerSymbol: number;
19
+ /** Where the toughness's symbols end: their right edge. */
20
+ toughnessSymbol: number;
21
+ }
22
+ /**
23
+ * Lays out a `powerToughnessRow` from its right edge: the toughness, its
24
+ * symbol, the power, then the power's symbol, each left of the one before.
25
+ */
26
+ export declare function powerToughnessRow(row: PowerToughnessRow, widths: PowerToughnessRowWidths): PowerToughnessRowPlacement;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Lays out a `powerToughnessRow` from its right edge: the toughness, its
3
+ * symbol, the power, then the power's symbol, each left of the one before.
4
+ */
5
+ export function powerToughnessRow(row, widths) {
6
+ const toughness = row.right - widths.toughness;
7
+ const toughnessSymbol = toughness - row.gap;
8
+ const power = toughnessSymbol -
9
+ (widths.toughnessSymbol ?? 0) -
10
+ row.spacing -
11
+ widths.power;
12
+ return { power, toughness, powerSymbol: power - row.gap, toughnessSymbol };
13
+ }
@@ -0,0 +1,37 @@
1
+ import type { FrameAssets, FrameRef } from "../schema/frameAssets.js";
2
+ /**
3
+ * Exact per-family lookup. The only alias is base `c2` → base `c`, for a frame
4
+ * with no distinct hybrid wash.
5
+ *
6
+ * Deliberately no cross-family fallback: a missing `land_*` or `nyx_*` variant
7
+ * has to resolve to `undefined` so the overlay that needed it is dropped
8
+ * rather than drawn in the wrong family. Falling back on the *base* frame is a
9
+ * separate, explicit decision — see {@link resolveBaseFrame}.
10
+ *
11
+ * Mirrors Card Anvil's `utils/resolveFrameAsset.ts`.
12
+ */
13
+ export declare function resolveFrameAsset(assets: FrameAssets, ref: FrameRef): string | undefined;
14
+ /**
15
+ * Whether a layout has frame art at all. One whose `base` is empty has none:
16
+ * its art runs to the edges, and whatever it puts over the art — gradients,
17
+ * tinted images, text — is all there is.
18
+ */
19
+ export declare function hasFrameArt(assets: FrameAssets): boolean;
20
+ export interface ResolvedBaseFrame {
21
+ ref: FrameRef;
22
+ url: string;
23
+ /**
24
+ * True when the requested ref was missing and another colour stood in.
25
+ * Callers drop every overlay in that case — the overlays were composed for
26
+ * the frame that is not there.
27
+ */
28
+ usedFallback: boolean;
29
+ }
30
+ /**
31
+ * The base frame, with the fallback chain the renderer uses: the requested
32
+ * family and colour, then the same colour in `base`, then (`l` → `c`), then
33
+ * `m`, then `w`, then whatever the frame does ship.
34
+ *
35
+ * `undefined` only when a frame ships no base art at all.
36
+ */
37
+ export declare function resolveBaseFrame(assets: FrameAssets, ref: FrameRef): ResolvedBaseFrame | undefined;
@@ -0,0 +1,66 @@
1
+ import { FRAME_COLORS } from "../schema/frameAssets.js";
2
+ /**
3
+ * Exact per-family lookup. The only alias is base `c2` → base `c`, for a frame
4
+ * with no distinct hybrid wash.
5
+ *
6
+ * Deliberately no cross-family fallback: a missing `land_*` or `nyx_*` variant
7
+ * has to resolve to `undefined` so the overlay that needed it is dropped
8
+ * rather than drawn in the wrong family. Falling back on the *base* frame is a
9
+ * separate, explicit decision — see {@link resolveBaseFrame}.
10
+ *
11
+ * Mirrors Card Anvil's `utils/resolveFrameAsset.ts`.
12
+ */
13
+ export function resolveFrameAsset(assets, ref) {
14
+ switch (ref.family) {
15
+ case "base":
16
+ return ref.color === "c2"
17
+ ? (assets.base.c2 ?? assets.base.c)
18
+ : assets.base[ref.color];
19
+ case "land":
20
+ return assets.land?.[ref.color];
21
+ case "nyx":
22
+ return assets.nyx?.[ref.color];
23
+ case "tall":
24
+ return assets.tall?.[ref.color];
25
+ case "creature":
26
+ return assets.creature?.[ref.color];
27
+ }
28
+ }
29
+ /**
30
+ * Whether a layout has frame art at all. One whose `base` is empty has none:
31
+ * its art runs to the edges, and whatever it puts over the art — gradients,
32
+ * tinted images, text — is all there is.
33
+ */
34
+ export function hasFrameArt(assets) {
35
+ return Object.values(assets.base).length > 0;
36
+ }
37
+ /**
38
+ * The base frame, with the fallback chain the renderer uses: the requested
39
+ * family and colour, then the same colour in `base`, then (`l` → `c`), then
40
+ * `m`, then `w`, then whatever the frame does ship.
41
+ *
42
+ * `undefined` only when a frame ships no base art at all.
43
+ */
44
+ export function resolveBaseFrame(assets, ref) {
45
+ const direct = resolveFrameAsset(assets, ref);
46
+ if (direct !== undefined) {
47
+ return { ref, url: direct, usedFallback: false };
48
+ }
49
+ const readBase = (color) => color === "c2" ? (assets.base.c2 ?? assets.base.c) : assets.base[color];
50
+ const chain = ref.color === "l" ? [ref.color, "c", "m", "w"] : [ref.color, "m", "w"];
51
+ for (const color of chain) {
52
+ const url = readBase(color);
53
+ if (url !== undefined) {
54
+ return { ref: { family: "base", color }, url, usedFallback: true };
55
+ }
56
+ }
57
+ // Last resort: the first colour the frame ships, in canonical order rather
58
+ // than whatever order the object happens to be written in.
59
+ for (const color of FRAME_COLORS) {
60
+ const url = assets.base[color];
61
+ if (url !== undefined) {
62
+ return { ref: { family: "base", color }, url, usedFallback: true };
63
+ }
64
+ }
65
+ return undefined;
66
+ }
@@ -0,0 +1,13 @@
1
+ import type { CardBoxes, RulesSlide } from "../schema/frame.js";
2
+ /**
3
+ * How far a `rulesSlide` moves the boxes for rules text `textHeight` tall in
4
+ * a `rules` box `boxHeight` tall: the room left over less `padding`, rounded
5
+ * down to whole steps, at most `maxSteps` of them.
6
+ */
7
+ export declare function rulesSlideOffset(slide: RulesSlide, boxHeight: number, textHeight: number): number;
8
+ /**
9
+ * The boxes once a `rulesSlide` has moved them down by `offset`: the listed
10
+ * boxes moved whole, and the `rules` box's top moved with them while its
11
+ * bottom stays put.
12
+ */
13
+ export declare function slideBoxes(boxes: CardBoxes, slide: RulesSlide, offset: number): CardBoxes;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * How far a `rulesSlide` moves the boxes for rules text `textHeight` tall in
3
+ * a `rules` box `boxHeight` tall: the room left over less `padding`, rounded
4
+ * down to whole steps, at most `maxSteps` of them.
5
+ */
6
+ export function rulesSlideOffset(slide, boxHeight, textHeight) {
7
+ const room = boxHeight - textHeight - slide.padding;
8
+ const steps = Math.min(slide.maxSteps, Math.floor(room / slide.step));
9
+ return steps > 0 ? steps * slide.step : 0;
10
+ }
11
+ /**
12
+ * The boxes once a `rulesSlide` has moved them down by `offset`: the listed
13
+ * boxes moved whole, and the `rules` box's top moved with them while its
14
+ * bottom stays put.
15
+ */
16
+ export function slideBoxes(boxes, slide, offset) {
17
+ if (offset === 0) {
18
+ return boxes;
19
+ }
20
+ const slid = { ...boxes };
21
+ for (const key of slide.boxes) {
22
+ const box = boxes[key];
23
+ Object.assign(slid, { [key]: { ...box, y: box.y + offset } });
24
+ }
25
+ if (boxes.rules) {
26
+ slid.rules = {
27
+ ...boxes.rules,
28
+ y: boxes.rules.y + offset,
29
+ height: boxes.rules.height - offset,
30
+ };
31
+ }
32
+ return slid;
33
+ }
@@ -0,0 +1,26 @@
1
+ import type { Frame } from "../schema/frame.js";
2
+ /** One template setting's value. */
3
+ export type SettingValue = boolean | string | number;
4
+ /**
5
+ * A frame's template settings as an app stores them: frame-level values as
6
+ * top-level keys, and each layout's own settings as a sub-object under the
7
+ * layout's name.
8
+ */
9
+ export type TemplateSettingValues = Record<string, SettingValue | Record<string, SettingValue>>;
10
+ /**
11
+ * A frame's declared defaults, in that shape. Only `config.layouts`
12
+ * contributes layout settings — an alternate layout shares its base layout's.
13
+ */
14
+ export declare function templateSettingDefaults(frame: Frame): TemplateSettingValues;
15
+ /**
16
+ * Merges a sparse override onto a base, one level deep so per-layout
17
+ * sub-objects merge by key rather than being replaced wholesale.
18
+ */
19
+ export declare function mergeTemplateSettings(base: TemplateSettingValues | undefined, override: TemplateSettingValues | undefined): TemplateSettingValues | undefined;
20
+ /**
21
+ * The settings in effect for one layout — what the renderer reads, and what a
22
+ * text box's `when.settings` matches: the frame-level values with the
23
+ * layout's own sub-object laid over them. Other layouts' sub-objects are left
24
+ * out.
25
+ */
26
+ export declare function layoutTemplateSettings(settings: TemplateSettingValues | undefined, layout: string): Record<string, SettingValue>;
@@ -0,0 +1,62 @@
1
+ function isPlainObject(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ /**
5
+ * A frame's declared defaults, in that shape. Only `config.layouts`
6
+ * contributes layout settings — an alternate layout shares its base layout's.
7
+ */
8
+ export function templateSettingDefaults(frame) {
9
+ const defaults = {};
10
+ for (const [key, setting] of Object.entries(frame.templateSettings ?? {})) {
11
+ defaults[key] = setting.defaultValue;
12
+ }
13
+ for (const [layout, config] of Object.entries(frame.config.layouts)) {
14
+ const settings = config.templateSettings;
15
+ if (!settings) {
16
+ continue;
17
+ }
18
+ const layoutDefaults = {};
19
+ for (const [key, setting] of Object.entries(settings)) {
20
+ layoutDefaults[key] = setting.defaultValue;
21
+ }
22
+ defaults[layout] = layoutDefaults;
23
+ }
24
+ return defaults;
25
+ }
26
+ /**
27
+ * Merges a sparse override onto a base, one level deep so per-layout
28
+ * sub-objects merge by key rather than being replaced wholesale.
29
+ */
30
+ export function mergeTemplateSettings(base, override) {
31
+ if (!override) {
32
+ return base;
33
+ }
34
+ if (!base) {
35
+ return override;
36
+ }
37
+ const result = { ...base };
38
+ for (const [key, value] of Object.entries(override)) {
39
+ const baseValue = result[key];
40
+ result[key] =
41
+ isPlainObject(value) && isPlainObject(baseValue)
42
+ ? { ...baseValue, ...value }
43
+ : value;
44
+ }
45
+ return result;
46
+ }
47
+ /**
48
+ * The settings in effect for one layout — what the renderer reads, and what a
49
+ * text box's `when.settings` matches: the frame-level values with the
50
+ * layout's own sub-object laid over them. Other layouts' sub-objects are left
51
+ * out.
52
+ */
53
+ export function layoutTemplateSettings(settings, layout) {
54
+ const inEffect = {};
55
+ for (const [key, value] of Object.entries(settings ?? {})) {
56
+ if (!isPlainObject(value)) {
57
+ inEffect[key] = value;
58
+ }
59
+ }
60
+ const own = settings?.[layout];
61
+ return isPlainObject(own) ? { ...inEffect, ...own } : inEffect;
62
+ }
@@ -0,0 +1,29 @@
1
+ import type { TextBox } from "../schema/frame.js";
2
+ /** The color plain-text drop shadows are drawn in. */
3
+ export declare const TEXT_SHADOW_COLOR = "#17191b";
4
+ export interface TextOutline {
5
+ color: string;
6
+ width: number;
7
+ }
8
+ export interface TextShadow {
9
+ color: string;
10
+ /** In canvas pixels. */
11
+ offsetX: number;
12
+ offsetY: number;
13
+ /** In canvas pixels; 0 is a hard edge. */
14
+ blur: number;
15
+ }
16
+ type StyledBox = Pick<TextBox, "fontSize" | "outlineColor" | "outlineWidth" | "shadow" | "shadowOffsetX" | "shadowOffsetY" | "shadowBlur">;
17
+ /** A box's outline, when it sets both an `outlineColor` and an `outlineWidth`. */
18
+ export declare function textOutlineFor(box: StyledBox): TextOutline | undefined;
19
+ /**
20
+ * The drop shadow a plain-text box — title, type, P/T, nickname title,
21
+ * collector info — is drawn with: only when it asks for one with `shadow`,
22
+ * and never alongside an outline, which always wins. The offsets default to
23
+ * fractions of the box's `fontSize`, unless the box sets its own.
24
+ *
25
+ * Symbols read `shadow` differently (on unless turned off), so this is for
26
+ * plain text only.
27
+ */
28
+ export declare function textShadowFor(box: StyledBox): TextShadow | undefined;
29
+ export {};
@@ -0,0 +1,28 @@
1
+ /** The color plain-text drop shadows are drawn in. */
2
+ export const TEXT_SHADOW_COLOR = "#17191b";
3
+ /** A box's outline, when it sets both an `outlineColor` and an `outlineWidth`. */
4
+ export function textOutlineFor(box) {
5
+ return box.outlineColor && box.outlineWidth
6
+ ? { color: box.outlineColor, width: box.outlineWidth }
7
+ : undefined;
8
+ }
9
+ /**
10
+ * The drop shadow a plain-text box — title, type, P/T, nickname title,
11
+ * collector info — is drawn with: only when it asks for one with `shadow`,
12
+ * and never alongside an outline, which always wins. The offsets default to
13
+ * fractions of the box's `fontSize`, unless the box sets its own.
14
+ *
15
+ * Symbols read `shadow` differently (on unless turned off), so this is for
16
+ * plain text only.
17
+ */
18
+ export function textShadowFor(box) {
19
+ if (!box.shadow || textOutlineFor(box)) {
20
+ return undefined;
21
+ }
22
+ return {
23
+ color: TEXT_SHADOW_COLOR,
24
+ offsetX: box.shadowOffsetX ?? -(box.fontSize * 0.029),
25
+ offsetY: box.shadowOffsetY ?? box.fontSize * 0.075,
26
+ blur: box.shadowBlur ?? 0,
27
+ };
28
+ }
@@ -0,0 +1,54 @@
1
+ import type { Bounds, Rotation } from "../schema/frame.js";
2
+ /**
3
+ * The geometry of a turned box set — one whose `rotation` says the card is
4
+ * turned to read it (see `CardBoxesSchema`).
5
+ *
6
+ * Two spaces are in play. The **sheet** is the canvas as printed, and never
7
+ * turns. A set's **turned space** is that sheet as it lies once the card has
8
+ * been turned `rotation` degrees clockwise, which is where the set's boxes are
9
+ * measured. Everything here maps the second onto the first.
10
+ */
11
+ export interface Size {
12
+ width: number;
13
+ height: number;
14
+ }
15
+ export interface Point {
16
+ x: number;
17
+ y: number;
18
+ }
19
+ /**
20
+ * A 2D affine transform in the order a canvas takes one:
21
+ * `x' = a·x + c·y + e`, `y' = b·x + d·y + f`.
22
+ */
23
+ export type Matrix = readonly [
24
+ a: number,
25
+ b: number,
26
+ c: number,
27
+ d: number,
28
+ e: number,
29
+ f: number
30
+ ];
31
+ /** The sheet as a turned set sees it: a quarter turn swaps its sides. */
32
+ export declare function turnedSheetSize(sheet: Size, rotation: Rotation | undefined): Size;
33
+ /**
34
+ * Where a turned set's origin lands on the sheet and how far it is rotated
35
+ * there, in degrees clockwise — the `x`, `y` and `rotation` of a node that
36
+ * draws the set. Turning the card clockwise to read a set means the set
37
+ * itself sits counter-clockwise on the sheet, hence the opposite sign.
38
+ */
39
+ export declare function turnPlacement(sheet: Size, rotation: Rotation | undefined): Point & {
40
+ rotation: number;
41
+ };
42
+ /** {@link turnPlacement} as a matrix taking turned space to the sheet. */
43
+ export declare function turnMatrix(sheet: Size, rotation: Rotation | undefined): Matrix;
44
+ /** A point in a matrix's source space, in its target space. */
45
+ export declare function applyMatrix(matrix: Matrix, point: Point): Point;
46
+ /** The rectangle a turned set's box covers on the sheet. */
47
+ export declare function turnedBoundsOnSheet(bounds: Bounds, sheet: Size, rotation: Rotation | undefined): Bounds;
48
+ /**
49
+ * A direction in a turned set's space, as a direction on the sheet. Only the
50
+ * turn applies; a direction has no position to translate.
51
+ */
52
+ export declare function turnedVectorOnSheet(vector: Point, rotation: Rotation | undefined): Point;
53
+ /** The reverse of {@link turnedVectorOnSheet}. */
54
+ export declare function sheetVectorInTurned(vector: Point, rotation: Rotation | undefined): Point;
@@ -0,0 +1,107 @@
1
+ /** The sheet as a turned set sees it: a quarter turn swaps its sides. */
2
+ export function turnedSheetSize(sheet, rotation) {
3
+ return rotation === 90 || rotation === 270
4
+ ? { width: sheet.height, height: sheet.width }
5
+ : { width: sheet.width, height: sheet.height };
6
+ }
7
+ /**
8
+ * Where a turned set's origin lands on the sheet and how far it is rotated
9
+ * there, in degrees clockwise — the `x`, `y` and `rotation` of a node that
10
+ * draws the set. Turning the card clockwise to read a set means the set
11
+ * itself sits counter-clockwise on the sheet, hence the opposite sign.
12
+ */
13
+ export function turnPlacement(sheet, rotation) {
14
+ switch (rotation) {
15
+ case 90:
16
+ return { x: 0, y: sheet.height, rotation: -90 };
17
+ case 180:
18
+ return { x: sheet.width, y: sheet.height, rotation: 180 };
19
+ case 270:
20
+ return { x: sheet.width, y: 0, rotation: 90 };
21
+ default:
22
+ return { x: 0, y: 0, rotation: 0 };
23
+ }
24
+ }
25
+ /** {@link turnPlacement} as a matrix taking turned space to the sheet. */
26
+ export function turnMatrix(sheet, rotation) {
27
+ switch (rotation) {
28
+ case 90:
29
+ return [0, -1, 1, 0, 0, sheet.height];
30
+ case 180:
31
+ return [-1, 0, 0, -1, sheet.width, sheet.height];
32
+ case 270:
33
+ return [0, 1, -1, 0, sheet.width, 0];
34
+ default:
35
+ return [1, 0, 0, 1, 0, 0];
36
+ }
37
+ }
38
+ /** A point in a matrix's source space, in its target space. */
39
+ export function applyMatrix(matrix, point) {
40
+ const [a, b, c, d, e, f] = matrix;
41
+ return {
42
+ x: a * point.x + c * point.y + e,
43
+ y: b * point.x + d * point.y + f,
44
+ };
45
+ }
46
+ /** The rectangle a turned set's box covers on the sheet. */
47
+ export function turnedBoundsOnSheet(bounds, sheet, rotation) {
48
+ switch (rotation) {
49
+ case 90:
50
+ return {
51
+ x: bounds.y,
52
+ y: sheet.height - bounds.x - bounds.width,
53
+ width: bounds.height,
54
+ height: bounds.width,
55
+ };
56
+ case 180:
57
+ return {
58
+ x: sheet.width - bounds.x - bounds.width,
59
+ y: sheet.height - bounds.y - bounds.height,
60
+ width: bounds.width,
61
+ height: bounds.height,
62
+ };
63
+ case 270:
64
+ return {
65
+ x: sheet.width - bounds.y - bounds.height,
66
+ y: bounds.x,
67
+ width: bounds.height,
68
+ height: bounds.width,
69
+ };
70
+ default:
71
+ return {
72
+ x: bounds.x,
73
+ y: bounds.y,
74
+ width: bounds.width,
75
+ height: bounds.height,
76
+ };
77
+ }
78
+ }
79
+ /**
80
+ * A direction in a turned set's space, as a direction on the sheet. Only the
81
+ * turn applies; a direction has no position to translate.
82
+ */
83
+ export function turnedVectorOnSheet(vector, rotation) {
84
+ switch (rotation) {
85
+ case 90:
86
+ return { x: vector.y, y: -vector.x };
87
+ case 180:
88
+ return { x: -vector.x, y: -vector.y };
89
+ case 270:
90
+ return { x: -vector.y, y: vector.x };
91
+ default:
92
+ return { x: vector.x, y: vector.y };
93
+ }
94
+ }
95
+ /** The reverse of {@link turnedVectorOnSheet}. */
96
+ export function sheetVectorInTurned(vector, rotation) {
97
+ switch (rotation) {
98
+ case 90:
99
+ return { x: -vector.y, y: vector.x };
100
+ case 180:
101
+ return { x: -vector.x, y: -vector.y };
102
+ case 270:
103
+ return { x: vector.y, y: -vector.x };
104
+ default:
105
+ return { x: vector.x, y: vector.y };
106
+ }
107
+ }