@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,433 @@
1
+ /**
2
+ * The subset of Proxyshop's `LAYERS` enum the frame logic needs: canonical
3
+ * colour letters plus the identity labels. Mirrors Card Anvil's
4
+ * `utils/frameLogic.ts`, which is where the card-parsing half still lives.
5
+ */
6
+ export const LAYERS = {
7
+ WHITE: "W",
8
+ BLUE: "U",
9
+ BLACK: "B",
10
+ RED: "R",
11
+ GREEN: "G",
12
+ ARTIFACT: "Artifact",
13
+ COLORLESS: "Colorless",
14
+ LAND: "Land",
15
+ GOLD: "Gold",
16
+ VEHICLE: "Vehicle",
17
+ HYBRID: "Hybrid",
18
+ };
19
+ /** WUBRG in canonical order, which every multi-colour string is sorted into. */
20
+ export const COLOR_ORDER = ["W", "U", "B", "R", "G"];
21
+ /** The five colour letters, for checking a string is made of them. */
22
+ export const COLOR_LETTERS = new Set(COLOR_ORDER);
23
+ /**
24
+ * Two- and three-colour combinations in the order cards print them: allied
25
+ * pairs then enemy pairs, shards then wedges. Mirrors Card Anvil's
26
+ * `frameLogic.ts` lookup.
27
+ */
28
+ const PRINTED_ORDERS = [
29
+ "WU",
30
+ "UB",
31
+ "BR",
32
+ "RG",
33
+ "GW",
34
+ "WB",
35
+ "BG",
36
+ "GU",
37
+ "UR",
38
+ "RW",
39
+ "GWU",
40
+ "WUB",
41
+ "UBR",
42
+ "BRG",
43
+ "RGW",
44
+ "WBG",
45
+ "URW",
46
+ "BGU",
47
+ "RWB",
48
+ "GUR",
49
+ ];
50
+ const sortedKey = (colors) => Array.from(colors).sort().join("");
51
+ const PRINTED_ORDER_BY_KEY = new Map(PRINTED_ORDERS.map((combo) => [sortedKey(combo), combo]));
52
+ /**
53
+ * Two or three colours in the order cards print them (`UW` → `WU`, `UGW` →
54
+ * `GWU`); anything else as it came.
55
+ */
56
+ export function printedColorOrder(colors) {
57
+ return PRINTED_ORDER_BY_KEY.get(sortedKey(colors)) ?? colors;
58
+ }
59
+ /** Whether two refs point at the same family and colour. */
60
+ function sameRef(a, b) {
61
+ return a.family === b.family && a.color === b.color;
62
+ }
63
+ /** Maps a `LAYERS` value to a single frame colour token. */
64
+ export function colorToken(value) {
65
+ switch (value) {
66
+ case LAYERS.WHITE:
67
+ return "w";
68
+ case LAYERS.BLUE:
69
+ return "u";
70
+ case LAYERS.BLACK:
71
+ return "b";
72
+ case LAYERS.RED:
73
+ return "r";
74
+ case LAYERS.GREEN:
75
+ return "g";
76
+ case LAYERS.ARTIFACT:
77
+ return "a";
78
+ case LAYERS.COLORLESS:
79
+ return "c";
80
+ case LAYERS.LAND:
81
+ return "l";
82
+ case LAYERS.VEHICLE:
83
+ return "v";
84
+ case LAYERS.GOLD:
85
+ return "m";
86
+ case "":
87
+ return "c";
88
+ default:
89
+ // Three or more colours share the gold frame.
90
+ return "m";
91
+ }
92
+ }
93
+ /** Splits a two-colour combo like `WU`, or `null` if it is not one. */
94
+ export function splitTwoColor(value) {
95
+ const [first, second] = value;
96
+ if (value.length === 2 &&
97
+ first !== undefined &&
98
+ second !== undefined &&
99
+ COLOR_LETTERS.has(first) &&
100
+ COLOR_LETTERS.has(second)) {
101
+ return [
102
+ first.toLowerCase(),
103
+ second.toLowerCase(),
104
+ ];
105
+ }
106
+ return null;
107
+ }
108
+ /**
109
+ * The ref for a colour within a family.
110
+ *
111
+ * Families do not all carry every colour, and the fallbacks are deliberate:
112
+ * nyx has no vehicle frame, and a land that is not one of WUBRG or gold falls
113
+ * back to the colourless land in the base family.
114
+ */
115
+ export function makeFamilyRef(color, family) {
116
+ if (family === "normal") {
117
+ return { family: "base", color };
118
+ }
119
+ if (family === "nyx") {
120
+ switch (color) {
121
+ case "w":
122
+ case "u":
123
+ case "b":
124
+ case "r":
125
+ case "g":
126
+ case "m":
127
+ case "a":
128
+ case "c":
129
+ case "l":
130
+ return { family: "nyx", color };
131
+ case "v":
132
+ return { family: "nyx", color: "a" }; // there is no nyx vehicle frame
133
+ default:
134
+ return { family: "nyx", color: "c" };
135
+ }
136
+ }
137
+ switch (color) {
138
+ case "w":
139
+ case "u":
140
+ case "b":
141
+ case "r":
142
+ case "g":
143
+ case "m":
144
+ return { family: "land", color };
145
+ default:
146
+ // a / c / l / v / c2 → the colourless land, which lives in `base`.
147
+ return { family: "base", color: "l" };
148
+ }
149
+ }
150
+ /** The family a type line implies. */
151
+ export function frameFamily(typeLine, useNyxBorder = true) {
152
+ const lower = typeLine.toLowerCase();
153
+ if (lower.includes("enchantment") && useNyxBorder) {
154
+ return "nyx";
155
+ }
156
+ return lower.includes("land") ? "land" : "normal";
157
+ }
158
+ /** Section overlays that recolour pinlines, text box and twins. */
159
+ function buildSectionOverlays(details, family, baseRef) {
160
+ const overlays = [];
161
+ const pinlineSplit = splitTwoColor(details.pinlines);
162
+ if (pinlineSplit) {
163
+ // Two colours: the left colour runs full width, the right is masked to
164
+ // its half, for both the pinlines and the text box.
165
+ const first = makeFamilyRef(pinlineSplit[0], family);
166
+ const second = makeFamilyRef(pinlineSplit[1], family);
167
+ overlays.push({ frame: first, mask: "pinlines", preserveAlpha: true }, {
168
+ frame: second,
169
+ mask: "pinlines",
170
+ secondaryMask: "rightHalf",
171
+ preserveAlpha: true,
172
+ }, { frame: first, mask: "rules", preserveAlpha: true }, {
173
+ frame: second,
174
+ mask: "rules",
175
+ secondaryMask: "rightHalf",
176
+ preserveAlpha: true,
177
+ });
178
+ }
179
+ else {
180
+ const pinlines = makeFamilyRef(colorToken(details.pinlines), family);
181
+ if (!sameRef(pinlines, baseRef)) {
182
+ overlays.push({ frame: pinlines, mask: "pinlines", preserveAlpha: true }, { frame: pinlines, mask: "rules", preserveAlpha: true });
183
+ }
184
+ }
185
+ const twins = makeFamilyRef(colorToken(details.twins), family);
186
+ if (!sameRef(twins, baseRef)) {
187
+ overlays.push({ frame: twins, mask: "titleAndType", preserveAlpha: true });
188
+ }
189
+ // The border follows the background, which the base frame already supplies.
190
+ return overlays;
191
+ }
192
+ /**
193
+ * The art to composite for a set of frame details.
194
+ *
195
+ * The selection half of Card Anvil's `getFrameLayers`, with the card parsing
196
+ * lifted out: mono and three-plus-colour cards are a single base frame, gold
197
+ * two-colour cards get split pinlines, and two-colour hybrids render as two
198
+ * halves under a colourless wash.
199
+ */
200
+ export function selectFrameLayers(details, options = {}) {
201
+ const { family = "normal", hybridTitleMask = "titleAndType", hasPinlineMask = true, } = options;
202
+ if (details.isHybrid) {
203
+ const split = splitTwoColor(details.pinlines);
204
+ if (split) {
205
+ return {
206
+ baseFrame: makeFamilyRef(split[0], family),
207
+ overlays: [
208
+ {
209
+ frame: makeFamilyRef(split[1], family),
210
+ mask: "rightHalf",
211
+ preserveAlpha: true,
212
+ },
213
+ {
214
+ frame: makeFamilyRef("c2", family),
215
+ mask: hybridTitleMask,
216
+ preserveAlpha: true,
217
+ },
218
+ ],
219
+ };
220
+ }
221
+ }
222
+ const usesArtifactFallback = family !== "land" &&
223
+ !hasPinlineMask &&
224
+ details.background === LAYERS.ARTIFACT &&
225
+ details.identity.length > 0;
226
+ // Without a pinlines mask the split pinline overlays can never draw, so a
227
+ // two-colour card would fall to the gold frame; halve the base instead.
228
+ if (family !== "land" &&
229
+ !hasPinlineMask &&
230
+ !details.isColorless &&
231
+ (details.background === LAYERS.GOLD || usesArtifactFallback)) {
232
+ const split = splitTwoColor(details.identity);
233
+ if (split) {
234
+ return {
235
+ baseFrame: makeFamilyRef(split[0], family),
236
+ overlays: [
237
+ {
238
+ frame: makeFamilyRef(split[1], family),
239
+ mask: "rightHalf",
240
+ preserveAlpha: true,
241
+ },
242
+ ],
243
+ };
244
+ }
245
+ }
246
+ const baseColor = colorToken(family === "land" || usesArtifactFallback
247
+ ? details.identity
248
+ : details.background);
249
+ const baseRef = makeFamilyRef(baseColor, family);
250
+ const overlays = buildSectionOverlays(details, family, baseRef);
251
+ return {
252
+ baseFrame: baseRef,
253
+ ...(overlays.length > 0 ? { overlays } : {}),
254
+ };
255
+ }
256
+ /**
257
+ * Overlays colouring one page of a storybook: a colour on its own, two with
258
+ * the second faded in through the page's `Right` mask, gold at three or more.
259
+ * A one-colour page that matches the base frame needs nothing — unless the
260
+ * base was halved, which runs the second colour into the pages too.
261
+ */
262
+ function pageOverlays(colors, mask, rightMask, family, baseRef, halved) {
263
+ const split = splitTwoColor(colors);
264
+ if (split) {
265
+ return [
266
+ { frame: makeFamilyRef(split[0], family), mask, preserveAlpha: true },
267
+ {
268
+ frame: makeFamilyRef(split[1], family),
269
+ mask: rightMask,
270
+ preserveAlpha: true,
271
+ },
272
+ ];
273
+ }
274
+ const page = makeFamilyRef(colorToken(colors), family);
275
+ return sameRef(page, baseRef) && !halved
276
+ ? []
277
+ : [{ frame: page, mask, preserveAlpha: true }];
278
+ }
279
+ /**
280
+ * The art to composite for a storybook Adventure, whose text box is an open
281
+ * book: the card's rules on the right page, its adventure on the left.
282
+ *
283
+ * Everything outside the book is selected as for any card, as though the
284
+ * layout had pinlines, with one exception: the book has no pinline art, so a
285
+ * two-colour gold card halves its base frame between its colours, which
286
+ * splits the frame's own pinlines, and lays gold over the title and type bars
287
+ * as the regular frame has them, and over the book's page edges (`wings`).
288
+ * Each page is then coloured on its own: the right by the card's pinline
289
+ * colours, the left by `adventureColors`, the adventure's colours in the
290
+ * order cards print them (see `printedColorOrder`). An adventure with no
291
+ * colours takes the card's.
292
+ */
293
+ export function selectStorybookFrameLayers(details, adventureColors, options = {}) {
294
+ const family = options.family ?? "normal";
295
+ const goldSplit = details.background === LAYERS.GOLD &&
296
+ !details.isHybrid &&
297
+ !details.isColorless
298
+ ? splitTwoColor(details.pinlines)
299
+ : null;
300
+ const card = goldSplit
301
+ ? {
302
+ baseFrame: makeFamilyRef(goldSplit[0], family),
303
+ overlays: [
304
+ {
305
+ frame: makeFamilyRef(goldSplit[1], family),
306
+ mask: "rightHalf",
307
+ preserveAlpha: true,
308
+ },
309
+ {
310
+ frame: makeFamilyRef(colorToken(details.twins), family),
311
+ mask: "titleAndType",
312
+ preserveAlpha: true,
313
+ },
314
+ {
315
+ frame: makeFamilyRef("m", family),
316
+ mask: "wings",
317
+ preserveAlpha: true,
318
+ },
319
+ ],
320
+ }
321
+ : selectFrameLayers(details, { ...options, hasPinlineMask: true });
322
+ // The book replaces the text box and has no pinlines, so the overlays meant
323
+ // for those are dropped and the pages coloured instead.
324
+ const kept = (card.overlays ?? []).filter((overlay) => overlay.mask !== "rules" && overlay.mask !== "pinlines");
325
+ const halved = kept.some((overlay) => overlay.mask === "rightHalf");
326
+ const overlays = [
327
+ ...kept,
328
+ ...pageOverlays(details.pinlines, "rules", "rulesRight", family, card.baseFrame, halved),
329
+ ...pageOverlays(adventureColors || details.pinlines, "adventureRules", "adventureRulesRight", family, card.baseFrame, halved),
330
+ ];
331
+ return {
332
+ baseFrame: card.baseFrame,
333
+ ...(overlays.length > 0 ? { overlays } : {}),
334
+ };
335
+ }
336
+ /**
337
+ * A card's colours as one string, in the order cards print them: `GW`, not
338
+ * `WG`. Card Anvil orders a card's identity the same way, and a two-colour
339
+ * split reads its left half from the first letter, so a plain WUBRG sort
340
+ * would swap the halves of GW, GU and RW cards. Four colours stay WUBRG.
341
+ */
342
+ export function orderColors(colors) {
343
+ const wanted = new Set(colors.map((color) => color.toUpperCase()));
344
+ return printedColorOrder(COLOR_ORDER.filter((letter) => wanted.has(letter)).join(""));
345
+ }
346
+ /**
347
+ * The frame details a card shape implies.
348
+ *
349
+ * Mirrors the branch order of Card Anvil's `getFrameDetailsNonland` so the
350
+ * studio and the app agree on the awkward cases: devoid multicolour keeps gold
351
+ * plates over a colourless body, a colourless card goes colourless throughout,
352
+ * vehicles and artifacts override the background while their colours stay in
353
+ * the pinlines, and a hybrid pair takes land-coloured twins.
354
+ *
355
+ * Lands are simplified on purpose: the app reads basic-land types and fetch
356
+ * text to infer a land's colours, which a picker states outright.
357
+ */
358
+ export function frameDetailsFor(shape) {
359
+ const identity = orderColors(shape.colors ?? []);
360
+ const details = {
361
+ background: identity,
362
+ pinlines: identity,
363
+ twins: identity,
364
+ identity,
365
+ isColorless: false,
366
+ isHybrid: false,
367
+ };
368
+ if (shape.isLand === true) {
369
+ // `selectFrameLayers` takes a land's base colour from `identity`; the
370
+ // background is always Land.
371
+ details.background = LAYERS.LAND;
372
+ if (identity.length === 0) {
373
+ details.pinlines = LAYERS.LAND;
374
+ details.twins = LAYERS.LAND;
375
+ }
376
+ else if (identity.length > 2) {
377
+ details.pinlines = LAYERS.GOLD;
378
+ details.twins = LAYERS.GOLD;
379
+ }
380
+ return details;
381
+ }
382
+ const devoid = shape.isDevoid === true && identity.length > 0;
383
+ const colorless = devoid ||
384
+ shape.isColorless === true ||
385
+ (identity.length === 0 && shape.isArtifact !== true);
386
+ if (colorless) {
387
+ if (devoid && identity.length > 1) {
388
+ details.twins = LAYERS.GOLD;
389
+ details.background = LAYERS.GOLD;
390
+ }
391
+ else if (!devoid) {
392
+ details.twins = LAYERS.COLORLESS;
393
+ details.background = LAYERS.COLORLESS;
394
+ details.pinlines = LAYERS.COLORLESS;
395
+ }
396
+ details.isColorless = true;
397
+ return details;
398
+ }
399
+ const hybrid = shape.isHybrid === true && identity.length === 2;
400
+ details.isHybrid = hybrid;
401
+ if (shape.isVehicle === true) {
402
+ details.background = LAYERS.VEHICLE;
403
+ }
404
+ else if (shape.isArtifact === true) {
405
+ details.background = LAYERS.ARTIFACT;
406
+ }
407
+ else if (identity.length >= 2 && !hybrid) {
408
+ details.background = LAYERS.GOLD;
409
+ }
410
+ if (identity.length === 0) {
411
+ details.pinlines = LAYERS.ARTIFACT;
412
+ }
413
+ else if (identity.length > 2) {
414
+ details.pinlines = LAYERS.GOLD;
415
+ }
416
+ if (identity.length === 0) {
417
+ details.twins = LAYERS.ARTIFACT;
418
+ }
419
+ else if (hybrid) {
420
+ details.twins = LAYERS.LAND;
421
+ }
422
+ else if (identity.length >= 2) {
423
+ details.twins = LAYERS.GOLD;
424
+ }
425
+ return details;
426
+ }
427
+ /** The frame family a card shape implies, without a type line to parse. */
428
+ export function familyFor(shape, useNyxBorder = true) {
429
+ if (shape.isEnchantment === true && useNyxBorder) {
430
+ return "nyx";
431
+ }
432
+ return shape.isLand === true ? "land" : "normal";
433
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * How a frame is drawn for one card: which art a card selects and where each
3
+ * piece goes, which cutouts and border recolor apply, which template settings
4
+ * are in effect, and how text boxes resolve and are styled.
5
+ *
6
+ * Card Anvil owns the card parsing — reading a Scryfall card into
7
+ * `FrameDetails` — and this owns everything downstream of it. It all used to
8
+ * live in the app; it moved here so the studio and the renderer cannot drift
9
+ * on it.
10
+ *
11
+ * A separate entry point from the main one: nothing on the packaging path
12
+ * needs it, and frames themselves never import it.
13
+ */
14
+ export { CROWN_BLACK_BAR, DEFAULT_PT_IMAGE, NYX_INSERT_FALLBACK_X, type Placed, type Placement, type PlacementConfig, placeAsset, placementOrigin, } from "./assetPlacement.js";
15
+ export { type BorderStateInput, borderStateFor, ringMaskFor, } from "./border.js";
16
+ export { type OverrideState, overrideMatches, resolveBoxOverrides, resolveSecondFaceBoxOverrides, } from "./boxOverrides.js";
17
+ export { type RgbColor, getReadableTextColor, parseNormalizedCssColor, relativeLuminance, } from "./color.js";
18
+ export { type FrameCutoutInput, frameCutoutMasks } from "./cutouts.js";
19
+ export { type BannerColors, type DecorationCard, type DecorationFacts, type DecorationState, crownColors, decorationsFor, identityColors, nicknameColors, ptBoxColor, resolveCrownAsset, resolveCrownNicknameAsset, resolveNicknameAsset, resolveNyxInsertAsset, resolvePtBoxAsset, } from "./decorations.js";
20
+ export { COLOR_ORDER, type CardShape, type FrameDetails, type FrameFamily, type FrameLayers, type FrameOverlay, LAYERS, type OverlayMaskName, type SelectOptions, colorToken, familyFor, frameDetailsFor, frameFamily, makeFamilyRef, orderColors, printedColorOrder, selectFrameLayers, selectStorybookFrameLayers, splitTwoColor, } from "./frameLayers.js";
21
+ export { type PaintStop, cardColorParts, gradientOffsetY, gradientStops, paintedFor, tintColor, } from "./paint.js";
22
+ export { type ResolvedBaseFrame, hasFrameArt, resolveBaseFrame, resolveFrameAsset, } from "./resolveFrameAsset.js";
23
+ export { type PowerToughnessRowPlacement, type PowerToughnessRowWidths, powerToughnessRow, } from "./powerToughnessRow.js";
24
+ export { rulesSlideOffset, slideBoxes } from "./rulesSlide.js";
25
+ export { type SettingValue, type TemplateSettingValues, layoutTemplateSettings, mergeTemplateSettings, templateSettingDefaults, } from "./settings.js";
26
+ export { TEXT_SHADOW_COLOR, type TextOutline, type TextShadow, textOutlineFor, textShadowFor, } from "./textStyle.js";
27
+ export { type Matrix, type Point, type Size, applyMatrix, sheetVectorInTurned, turnMatrix, turnPlacement, turnedBoundsOnSheet, turnedSheetSize, turnedVectorOnSheet, } from "./turn.js";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * How a frame is drawn for one card: which art a card selects and where each
3
+ * piece goes, which cutouts and border recolor apply, which template settings
4
+ * are in effect, and how text boxes resolve and are styled.
5
+ *
6
+ * Card Anvil owns the card parsing — reading a Scryfall card into
7
+ * `FrameDetails` — and this owns everything downstream of it. It all used to
8
+ * live in the app; it moved here so the studio and the renderer cannot drift
9
+ * on it.
10
+ *
11
+ * A separate entry point from the main one: nothing on the packaging path
12
+ * needs it, and frames themselves never import it.
13
+ */
14
+ export { CROWN_BLACK_BAR, DEFAULT_PT_IMAGE, NYX_INSERT_FALLBACK_X, placeAsset, placementOrigin, } from "./assetPlacement.js";
15
+ export { borderStateFor, ringMaskFor, } from "./border.js";
16
+ export { overrideMatches, resolveBoxOverrides, resolveSecondFaceBoxOverrides, } from "./boxOverrides.js";
17
+ export { getReadableTextColor, parseNormalizedCssColor, relativeLuminance, } from "./color.js";
18
+ export { frameCutoutMasks } from "./cutouts.js";
19
+ export { crownColors, decorationsFor, identityColors, nicknameColors, ptBoxColor, resolveCrownAsset, resolveCrownNicknameAsset, resolveNicknameAsset, resolveNyxInsertAsset, resolvePtBoxAsset, } from "./decorations.js";
20
+ export { COLOR_ORDER, LAYERS, colorToken, familyFor, frameDetailsFor, frameFamily, makeFamilyRef, orderColors, printedColorOrder, selectFrameLayers, selectStorybookFrameLayers, splitTwoColor, } from "./frameLayers.js";
21
+ export { cardColorParts, gradientOffsetY, gradientStops, paintedFor, tintColor, } from "./paint.js";
22
+ export { hasFrameArt, resolveBaseFrame, resolveFrameAsset, } from "./resolveFrameAsset.js";
23
+ export { powerToughnessRow, } from "./powerToughnessRow.js";
24
+ export { rulesSlideOffset, slideBoxes } from "./rulesSlide.js";
25
+ export { layoutTemplateSettings, mergeTemplateSettings, templateSettingDefaults, } from "./settings.js";
26
+ export { TEXT_SHADOW_COLOR, textOutlineFor, textShadowFor, } from "./textStyle.js";
27
+ export { applyMatrix, sheetVectorInTurned, turnMatrix, turnPlacement, turnedBoundsOnSheet, turnedSheetSize, turnedVectorOnSheet, } from "./turn.js";
@@ -0,0 +1,36 @@
1
+ import type { CardBoxes, LayoutConfig } from "../schema/frame.js";
2
+ import type { FrameColor } from "../schema/frameAssets.js";
3
+ import type { Gradient, TintedImage } from "../schema/paint.js";
4
+ import { type OverrideState } from "./boxOverrides.js";
5
+ import { type FrameDetails } from "./frameLayers.js";
6
+ /**
7
+ * The colors a card paints side by side, at most `maxParts` of them; see
8
+ * `CardColorsSchema`. Read from the pinlines, as Proxyshop's frames do, with
9
+ * one difference: a three-color card's pinlines are gold, so its three come
10
+ * from its identity.
11
+ */
12
+ export declare function cardColorParts(details: FrameDetails, maxParts?: number): FrameColor[];
13
+ /** A gradient stop as a canvas takes it: a position, and a CSS color. */
14
+ export interface PaintStop {
15
+ readonly at: number;
16
+ readonly color: string;
17
+ }
18
+ /**
19
+ * A gradient's stops for one card, color and opacity merged into one list a
20
+ * canvas can draw — a stop wherever either changes — or `undefined` when it
21
+ * paints nothing (its palette has no color for the card).
22
+ */
23
+ export declare function gradientStops(gradient: Gradient, details: FrameDetails): PaintStop[] | undefined;
24
+ /**
25
+ * How far down from its own `y` a gradient is painted for a card drawn with
26
+ * `boxes`, which a `rulesSlide` has moved `slideOffset`: its anchor box's top
27
+ * if it has one, otherwise `slideOffset` if it `slides`, else 0.
28
+ */
29
+ export declare function gradientOffsetY(gradient: Gradient, boxes: CardBoxes, slideOffset: number): number;
30
+ /**
31
+ * A layout as one render paints it: only the gradients and tinted images
32
+ * whose `when` holds for `state`. Everything else passes through untouched.
33
+ */
34
+ export declare function paintedFor<Config extends Pick<LayoutConfig, "gradients" | "tintedImages">>(config: Config, state: OverrideState): Config;
35
+ /** The color a tinted image is painted for one card, if its palette has one. */
36
+ export declare function tintColor(image: TintedImage, details: FrameDetails): string | undefined;
@@ -0,0 +1,148 @@
1
+ import { overrideMatches } from "./boxOverrides.js";
2
+ import { parseNormalizedCssColor } from "./color.js";
3
+ import { COLOR_LETTERS, LAYERS, colorToken, printedColorOrder, } from "./frameLayers.js";
4
+ /** Whether `value` is two or three distinct mana colors, e.g. `UW`. */
5
+ function isColorCombo(value) {
6
+ return ((value.length === 2 || value.length === 3) &&
7
+ new Set(value).size === value.length &&
8
+ Array.from(value).every((letter) => COLOR_LETTERS.has(letter)));
9
+ }
10
+ /**
11
+ * The colors a card paints side by side, at most `maxParts` of them; see
12
+ * `CardColorsSchema`. Read from the pinlines, as Proxyshop's frames do, with
13
+ * one difference: a three-color card's pinlines are gold, so its three come
14
+ * from its identity.
15
+ */
16
+ export function cardColorParts(details, maxParts = 1) {
17
+ const combo = isColorCombo(details.pinlines)
18
+ ? details.pinlines
19
+ : details.pinlines === LAYERS.GOLD &&
20
+ details.identity.length === 3 &&
21
+ isColorCombo(details.identity)
22
+ ? details.identity
23
+ : undefined;
24
+ if (combo === undefined) {
25
+ return [colorToken(details.pinlines)];
26
+ }
27
+ if (combo.length > maxParts) {
28
+ return ["m"];
29
+ }
30
+ return Array.from(printedColorOrder(combo), (letter) => colorToken(letter));
31
+ }
32
+ /** The palette's color for each of the card's parts, or none if any is missing. */
33
+ function paletteColors(colors, details) {
34
+ const hexes = cardColorParts(details, colors.maxParts).map((part) => colors.palette[part]);
35
+ return hexes.every((hex) => hex !== undefined) ? hexes : [];
36
+ }
37
+ function rgbOf(hex) {
38
+ return parseNormalizedCssColor(hex) ?? { r: 0, g: 0, b: 0 };
39
+ }
40
+ /** Linear interpolation over stops sorted by `at`, held flat past either end. */
41
+ function interpolate(stops, at, mix) {
42
+ const first = stops[0];
43
+ const last = stops[stops.length - 1];
44
+ if (!first || !last) {
45
+ throw new Error("interpolate: no stops");
46
+ }
47
+ if (at <= first.at) {
48
+ return first.value;
49
+ }
50
+ for (let i = 1; i < stops.length; i++) {
51
+ const before = stops[i - 1];
52
+ const after = stops[i];
53
+ if (before && after && at <= after.at) {
54
+ const span = after.at - before.at;
55
+ return mix(before.value, after.value, span > 0 ? (at - before.at) / span : 1);
56
+ }
57
+ }
58
+ return last.value;
59
+ }
60
+ const mixNumber = (a, b, t) => a + (b - a) * t;
61
+ const mixRgb = (a, b, t) => ({
62
+ r: mixNumber(a.r, b.r, t),
63
+ g: mixNumber(a.g, b.g, t),
64
+ b: mixNumber(a.b, b.b, t),
65
+ });
66
+ /**
67
+ * Where each color runs along a gradient split into equal parts, as color
68
+ * stops: flat across its part, blending into the next over `blend` (0–1 of
69
+ * the length) centered on where they meet.
70
+ */
71
+ function partColorStops(colors, blend) {
72
+ const half = Math.min(blend, 1 / colors.length) / 2;
73
+ return colors.flatMap((value, i) => {
74
+ const start = i / colors.length + (i > 0 ? half : 0);
75
+ const end = (i + 1) / colors.length - (i < colors.length - 1 ? half : 0);
76
+ return [
77
+ { at: start, value },
78
+ { at: end, value },
79
+ ];
80
+ });
81
+ }
82
+ const round = (value, places) => Number(value.toFixed(places));
83
+ /**
84
+ * A gradient's stops for one card, color and opacity merged into one list a
85
+ * canvas can draw — a stop wherever either changes — or `undefined` when it
86
+ * paints nothing (its palette has no color for the card).
87
+ */
88
+ export function gradientStops(gradient, details) {
89
+ const hexes = typeof gradient.color === "string"
90
+ ? [gradient.color]
91
+ : paletteColors(gradient.color, details);
92
+ if (hexes.length === 0) {
93
+ return undefined;
94
+ }
95
+ const length = gradient.direction === "horizontal" ? gradient.width : gradient.height;
96
+ const blend = typeof gradient.color === "string" || length <= 0
97
+ ? 0
98
+ : (gradient.color.blend ?? 0) / length;
99
+ const colorStops = partColorStops(hexes.map(rgbOf), blend);
100
+ const opacityStops = [...gradient.opacity]
101
+ .sort((a, b) => a.at - b.at)
102
+ .map(({ at, opacity }) => ({ at, value: opacity }));
103
+ const positions = [
104
+ ...new Set([...colorStops, ...opacityStops].map(({ at }) => at)),
105
+ ].sort((a, b) => a - b);
106
+ return positions.map((at) => {
107
+ const { r, g, b } = interpolate(colorStops, at, mixRgb);
108
+ const opacity = interpolate(opacityStops, at, mixNumber);
109
+ const channels = [Math.round(r), Math.round(g), Math.round(b)];
110
+ return {
111
+ at,
112
+ color: `rgba(${channels.join(", ")}, ${String(round(opacity, 4))})`,
113
+ };
114
+ });
115
+ }
116
+ /**
117
+ * How far down from its own `y` a gradient is painted for a card drawn with
118
+ * `boxes`, which a `rulesSlide` has moved `slideOffset`: its anchor box's top
119
+ * if it has one, otherwise `slideOffset` if it `slides`, else 0.
120
+ */
121
+ export function gradientOffsetY(gradient, boxes, slideOffset) {
122
+ if (gradient.anchor) {
123
+ return boxes[gradient.anchor].y;
124
+ }
125
+ return gradient.slides === true ? slideOffset : 0;
126
+ }
127
+ /**
128
+ * A layout as one render paints it: only the gradients and tinted images
129
+ * whose `when` holds for `state`. Everything else passes through untouched.
130
+ */
131
+ export function paintedFor(config, state) {
132
+ const holds = ({ when }) => when === undefined || overrideMatches(when, state);
133
+ return {
134
+ ...config,
135
+ ...(config.gradients && { gradients: config.gradients.filter(holds) }),
136
+ ...(config.tintedImages && {
137
+ tintedImages: config.tintedImages.filter(holds),
138
+ }),
139
+ };
140
+ }
141
+ /** The color a tinted image is painted for one card, if its palette has one. */
142
+ export function tintColor(image, details) {
143
+ if (typeof image.color === "string") {
144
+ return image.color;
145
+ }
146
+ const hexes = paletteColors(image.color, details);
147
+ return hexes[Math.min(image.color.part ?? 0, hexes.length - 1)];
148
+ }