@cardanvil/frame-kit 0.6.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 +75 -1
- 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 +72 -0
- package/dist/cli/studio/boxSets.js +11 -8
- package/dist/cli/studio/serve.js +4 -1
- package/dist/cli/trim.d.ts +62 -0
- package/dist/cli/trim.js +251 -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 +1 -6
- package/dist/layout/decorations.js +7 -11
- package/dist/layout/frameLayers.d.ts +29 -2
- package/dist/layout/frameLayers.js +145 -3
- package/dist/layout/index.d.ts +17 -6
- package/dist/layout/index.js +17 -6
- 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 +6 -0
- package/dist/layout/resolveFrameAsset.js +8 -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 +6074 -9392
- package/dist/manifest/contract.js +46 -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 +17291 -25435
- package/dist/schema/frame.js +402 -14
- package/dist/schema/frameAssets.d.ts +53 -33
- package/dist/schema/frameAssets.js +77 -5
- 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/index-BnhNwm_r.js +74 -0
- package/dist/studio/index.html +1 -1
- package/package.json +5 -3
- package/dist/studio/assets/index-CNzyV22G.js +0 -76
|
@@ -18,7 +18,44 @@ export const LAYERS = {
|
|
|
18
18
|
};
|
|
19
19
|
/** WUBRG in canonical order, which every multi-colour string is sorted into. */
|
|
20
20
|
export const COLOR_ORDER = ["W", "U", "B", "R", "G"];
|
|
21
|
-
|
|
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
|
+
}
|
|
22
59
|
/** Whether two refs point at the same family and colour. */
|
|
23
60
|
function sameRef(a, b) {
|
|
24
61
|
return a.family === b.family && a.color === b.color;
|
|
@@ -186,6 +223,26 @@ export function selectFrameLayers(details, options = {}) {
|
|
|
186
223
|
!hasPinlineMask &&
|
|
187
224
|
details.background === LAYERS.ARTIFACT &&
|
|
188
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
|
+
}
|
|
189
246
|
const baseColor = colorToken(family === "land" || usesArtifactFallback
|
|
190
247
|
? details.identity
|
|
191
248
|
: details.background);
|
|
@@ -196,10 +253,95 @@ export function selectFrameLayers(details, options = {}) {
|
|
|
196
253
|
...(overlays.length > 0 ? { overlays } : {}),
|
|
197
254
|
};
|
|
198
255
|
}
|
|
199
|
-
/**
|
|
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
|
+
*/
|
|
200
342
|
export function orderColors(colors) {
|
|
201
343
|
const wanted = new Set(colors.map((color) => color.toUpperCase()));
|
|
202
|
-
return COLOR_ORDER.filter((letter) => wanted.has(letter)).join("");
|
|
344
|
+
return printedColorOrder(COLOR_ORDER.filter((letter) => wanted.has(letter)).join(""));
|
|
203
345
|
}
|
|
204
346
|
/**
|
|
205
347
|
* The frame details a card shape implies.
|
package/dist/layout/index.d.ts
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* How a frame
|
|
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.
|
|
3
5
|
*
|
|
4
6
|
* Card Anvil owns the card parsing — reading a Scryfall card into
|
|
5
|
-
* `FrameDetails` — and this owns everything downstream of it
|
|
6
|
-
*
|
|
7
|
-
* in the app; this one moved here so the studio and the renderer cannot drift
|
|
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
|
|
8
9
|
* on it.
|
|
9
10
|
*
|
|
10
11
|
* A separate entry point from the main one: nothing on the packaging path
|
|
11
12
|
* needs it, and frames themselves never import it.
|
|
12
13
|
*/
|
|
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";
|
|
14
19
|
export { type BannerColors, type DecorationCard, type DecorationFacts, type DecorationState, crownColors, decorationsFor, identityColors, nicknameColors, ptBoxColor, resolveCrownAsset, resolveCrownNicknameAsset, resolveNicknameAsset, resolveNyxInsertAsset, resolvePtBoxAsset, } from "./decorations.js";
|
|
15
|
-
export { COLOR_ORDER, type CardShape, type FrameDetails, type FrameFamily, type FrameLayers, type FrameOverlay, LAYERS, type OverlayMaskName, type SelectOptions, colorToken, familyFor, frameDetailsFor, frameFamily, makeFamilyRef, orderColors, selectFrameLayers, splitTwoColor, } from "./frameLayers.js";
|
|
16
|
-
export { type
|
|
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";
|
package/dist/layout/index.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* How a frame
|
|
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.
|
|
3
5
|
*
|
|
4
6
|
* Card Anvil owns the card parsing — reading a Scryfall card into
|
|
5
|
-
* `FrameDetails` — and this owns everything downstream of it
|
|
6
|
-
*
|
|
7
|
-
* in the app; this one moved here so the studio and the renderer cannot drift
|
|
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
|
|
8
9
|
* on it.
|
|
9
10
|
*
|
|
10
11
|
* A separate entry point from the main one: nothing on the packaging path
|
|
11
12
|
* needs it, and frames themselves never import it.
|
|
12
13
|
*/
|
|
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";
|
|
14
19
|
export { crownColors, decorationsFor, identityColors, nicknameColors, ptBoxColor, resolveCrownAsset, resolveCrownNicknameAsset, resolveNicknameAsset, resolveNyxInsertAsset, resolvePtBoxAsset, } from "./decorations.js";
|
|
15
|
-
export { COLOR_ORDER, LAYERS, colorToken, familyFor, frameDetailsFor, frameFamily, makeFamilyRef, orderColors, selectFrameLayers, splitTwoColor, } from "./frameLayers.js";
|
|
16
|
-
export {
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -11,6 +11,12 @@ import type { FrameAssets, FrameRef } from "../schema/frameAssets.js";
|
|
|
11
11
|
* Mirrors Card Anvil's `utils/resolveFrameAsset.ts`.
|
|
12
12
|
*/
|
|
13
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;
|
|
14
20
|
export interface ResolvedBaseFrame {
|
|
15
21
|
ref: FrameRef;
|
|
16
22
|
url: string;
|
|
@@ -26,6 +26,14 @@ export function resolveFrameAsset(assets, ref) {
|
|
|
26
26
|
return assets.creature?.[ref.color];
|
|
27
27
|
}
|
|
28
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
|
+
}
|
|
29
37
|
/**
|
|
30
38
|
* The base frame, with the fallback chain the renderer uses: the requested
|
|
31
39
|
* family and colour, then the same colour in `base`, then (`l` → `c`), then
|
|
@@ -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 {};
|