@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.
- package/README.md +119 -0
- package/dist/authoring/collectorInfo.d.ts +17 -0
- package/dist/authoring/collectorInfo.js +62 -0
- package/dist/authoring/index.d.ts +2 -0
- package/dist/authoring/index.js +2 -0
- package/dist/authoring/layoutDefaults.d.ts +20 -2
- package/dist/authoring/layoutDefaults.js +48 -13
- package/dist/authoring/opacityStops.d.ts +8 -0
- package/dist/authoring/opacityStops.js +17 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/loadFrame.js +1 -1
- package/dist/cli/main.js +88 -0
- package/dist/cli/sourceWatcher.d.ts +40 -0
- package/dist/cli/sourceWatcher.js +100 -0
- package/dist/cli/studio/boxSets.d.ts +33 -0
- package/dist/cli/studio/boxSets.js +90 -0
- package/dist/cli/studio/edit/applyEdits.d.ts +71 -0
- package/dist/cli/studio/edit/applyEdits.js +177 -0
- package/dist/cli/studio/edit/impact.d.ts +53 -0
- package/dist/cli/studio/edit/impact.js +87 -0
- package/dist/cli/studio/edit/resolveModule.d.ts +10 -0
- package/dist/cli/studio/edit/resolveModule.js +55 -0
- package/dist/cli/studio/edit/resolvePath.d.ts +53 -0
- package/dist/cli/studio/edit/resolvePath.js +606 -0
- package/dist/cli/studio/edit/sourceIndex.d.ts +29 -0
- package/dist/cli/studio/edit/sourceIndex.js +73 -0
- package/dist/cli/studio/edit/ts.d.ts +14 -0
- package/dist/cli/studio/edit/ts.js +34 -0
- package/dist/cli/studio/serve.d.ts +23 -0
- package/dist/cli/studio/serve.js +516 -0
- package/dist/cli/studio/session.d.ts +73 -0
- package/dist/cli/studio/session.js +154 -0
- package/dist/cli/trim.d.ts +62 -0
- package/dist/cli/trim.js +251 -0
- package/dist/cli/watch.d.ts +3 -0
- package/dist/cli/watch.js +20 -89
- package/dist/layout/assetPlacement.d.ts +71 -0
- package/dist/layout/assetPlacement.js +104 -0
- package/dist/layout/border.d.ts +27 -0
- package/dist/layout/border.js +31 -0
- package/dist/layout/boxOverrides.d.ts +28 -0
- package/dist/layout/boxOverrides.js +95 -0
- package/dist/layout/color.d.ts +23 -0
- package/dist/layout/color.js +53 -0
- package/dist/layout/cutouts.d.ts +28 -0
- package/dist/layout/cutouts.js +29 -0
- package/dist/layout/decorations.d.ts +73 -0
- package/dist/layout/decorations.js +104 -0
- package/dist/layout/frameLayers.d.ts +163 -0
- package/dist/layout/frameLayers.js +433 -0
- package/dist/layout/index.d.ts +27 -0
- package/dist/layout/index.js +27 -0
- package/dist/layout/paint.d.ts +36 -0
- package/dist/layout/paint.js +148 -0
- package/dist/layout/powerToughnessRow.d.ts +26 -0
- package/dist/layout/powerToughnessRow.js +13 -0
- package/dist/layout/resolveFrameAsset.d.ts +37 -0
- package/dist/layout/resolveFrameAsset.js +66 -0
- package/dist/layout/rulesSlide.d.ts +13 -0
- package/dist/layout/rulesSlide.js +33 -0
- package/dist/layout/settings.d.ts +26 -0
- package/dist/layout/settings.js +62 -0
- package/dist/layout/textStyle.d.ts +29 -0
- package/dist/layout/textStyle.js +28 -0
- package/dist/layout/turn.d.ts +54 -0
- package/dist/layout/turn.js +107 -0
- package/dist/manifest/contract.d.ts +6103 -9063
- package/dist/manifest/contract.js +48 -1
- package/dist/manifest/testFrame.js +9 -0
- package/dist/schema/collectorInfo.d.ts +95 -0
- package/dist/schema/collectorInfo.js +80 -0
- package/dist/schema/condition.d.ts +67 -0
- package/dist/schema/condition.js +47 -0
- package/dist/schema/frame.d.ts +17213 -24450
- package/dist/schema/frame.js +418 -14
- package/dist/schema/frameAssets.d.ts +56 -34
- package/dist/schema/frameAssets.js +96 -2
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/layout.d.ts +29 -4
- package/dist/schema/layout.js +27 -2
- package/dist/schema/paint.d.ts +202 -0
- package/dist/schema/paint.js +109 -0
- package/dist/studio/assets/PlantinMTProRgIt-D0FB51kb.woff2 +0 -0
- package/dist/studio/assets/beleren-b-B3lFvodX.ttf +0 -0
- package/dist/studio/assets/beleren-bsc-yvL6mU7X.ttf +0 -0
- package/dist/studio/assets/index-BnhNwm_r.js +74 -0
- package/dist/studio/index.html +12 -0
- 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
|
+
}
|