@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
package/dist/schema/frame.js
CHANGED
|
@@ -1,28 +1,56 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { AssetUrlSchema } from "./assetUrl.js";
|
|
3
|
+
import { CollectorInfoLineSchema } from "./collectorInfo.js";
|
|
4
|
+
import { RenderConditionSchema } from "./condition.js";
|
|
3
5
|
import { FrameAssetsSchema, FrameColorEnum, LayoutMasksSchema, } from "./frameAssets.js";
|
|
4
6
|
import { LayoutEnum } from "./layout.js";
|
|
7
|
+
import { GradientSchema, HexColorSchema, SlidingBoxEnum, TintedImageSchema, } from "./paint.js";
|
|
5
8
|
export const BoundsSchema = z.object({
|
|
6
9
|
x: z.number(),
|
|
7
10
|
y: z.number(),
|
|
8
11
|
width: z.number(),
|
|
9
12
|
height: z.number(),
|
|
10
13
|
});
|
|
11
|
-
|
|
14
|
+
const TextBoxFieldsSchema = BoundsSchema.extend({
|
|
12
15
|
fontSize: z.number(),
|
|
13
16
|
color: z.string().optional(),
|
|
14
17
|
outlineColor: z.string().optional(),
|
|
15
18
|
outlineWidth: z.number().optional(),
|
|
16
19
|
verticalAlign: z.enum(["top", "center"]).optional(),
|
|
17
20
|
textAlign: z.enum(["left", "center", "right"]).optional(),
|
|
21
|
+
/** Extra spacing between characters, in px (Konva `letterSpacing`),
|
|
22
|
+
* expressed at the box's own `fontSize` — each run scales it by its font
|
|
23
|
+
* size so it reads as constant tracking. Non-zero spacing switches Konva
|
|
24
|
+
* to per-character drawing with kerning off; the canvas measurement path
|
|
25
|
+
* mirrors that so measured widths match the render. */
|
|
26
|
+
letterSpacing: z.number().optional(),
|
|
18
27
|
/** Per-box font family override. When set, the renderer uses this instead
|
|
19
28
|
* of the language-resolved default (e.g. title font). */
|
|
20
29
|
fontFamily: z.string().optional(),
|
|
21
30
|
opacity: z.number().min(0).max(1).optional(),
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
31
|
+
/**
|
|
32
|
+
* Whether this box's content gets a drop-shadow.
|
|
33
|
+
*
|
|
34
|
+
* For symbol boxes (mana cost, and symbols embedded in rules/abilities
|
|
35
|
+
* text) this defaults to true when omitted, matching the original
|
|
36
|
+
* behavior — set to false to disable it (e.g. flipside mana-cost badges on
|
|
37
|
+
* light backgrounds).
|
|
38
|
+
*
|
|
39
|
+
* For plain-text boxes (title, type, P/T, etc., rendered via
|
|
40
|
+
* `buildOutlinedText`) there is no legacy default: omitting this leaves
|
|
41
|
+
* them exactly as before, and it only takes effect once set to true. It
|
|
42
|
+
* has no effect together with `outlineColor`/`outlineWidth` — an outline
|
|
43
|
+
* always wins.
|
|
44
|
+
*/
|
|
25
45
|
shadow: z.boolean().optional(),
|
|
46
|
+
/** Overrides the drop-shadow's default offset (in the box's own `fontSize`
|
|
47
|
+
* units, same convention as the default formula). Only meaningful when
|
|
48
|
+
* `shadow` is true. */
|
|
49
|
+
shadowOffsetX: z.number().optional(),
|
|
50
|
+
shadowOffsetY: z.number().optional(),
|
|
51
|
+
/** How far the drop-shadow is blurred, in px. Defaults to 0, a hard edge.
|
|
52
|
+
* Plain-text boxes only, and only meaningful when `shadow` is true. */
|
|
53
|
+
shadowBlur: z.number().nonnegative().optional(),
|
|
26
54
|
/** Shaped-text mask: a full-canvas-sized PNG whose opaque pixels define the
|
|
27
55
|
* region text in this box may occupy. Alpha is what matters (any pixel with
|
|
28
56
|
* alpha > ~12% counts as inside), so color is irrelevant. Each wrapped line
|
|
@@ -35,17 +63,111 @@ export const TextBoxSchema = BoundsSchema.extend({
|
|
|
35
63
|
* width. */
|
|
36
64
|
mask: AssetUrlSchema.optional(),
|
|
37
65
|
});
|
|
66
|
+
/**
|
|
67
|
+
* One conditional restyle of a text box: when every condition in `when` holds
|
|
68
|
+
* for a render, `style` is merged over the box. A condition left out matches
|
|
69
|
+
* anything.
|
|
70
|
+
*/
|
|
71
|
+
export const TextBoxOverrideSchema = z.object({
|
|
72
|
+
when: RenderConditionSchema,
|
|
73
|
+
/**
|
|
74
|
+
* The fields to replace. Style only: parts of the app read box geometry
|
|
75
|
+
* without resolving overrides, so a conditional size or position would put
|
|
76
|
+
* them out of step with the render.
|
|
77
|
+
*/
|
|
78
|
+
style: TextBoxFieldsSchema.pick({
|
|
79
|
+
color: true,
|
|
80
|
+
outlineColor: true,
|
|
81
|
+
outlineWidth: true,
|
|
82
|
+
shadow: true,
|
|
83
|
+
shadowOffsetX: true,
|
|
84
|
+
shadowOffsetY: true,
|
|
85
|
+
shadowBlur: true,
|
|
86
|
+
opacity: true,
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
const textBoxSchema = TextBoxFieldsSchema.extend({
|
|
90
|
+
/**
|
|
91
|
+
* Conditional restyles, applied in order at render time — where two matches
|
|
92
|
+
* set the same field, the later one wins. This is how a box adapts to the
|
|
93
|
+
* border settings: the renderer never restyles text on its own. Text printed
|
|
94
|
+
* on the border ring wants `onBorderTextOverrides`, which
|
|
95
|
+
* `withCollectorInfoDefaults` gives collector info unless its bounds bring
|
|
96
|
+
* their own — `[]` for none.
|
|
97
|
+
*/
|
|
98
|
+
overrides: z.array(TextBoxOverrideSchema).readonly().optional(),
|
|
99
|
+
});
|
|
100
|
+
export const TextBoxSchema = textBoxSchema;
|
|
101
|
+
export const CollectorInfoBoxSchema = TextBoxSchema.extend({
|
|
102
|
+
/**
|
|
103
|
+
* What the collector box prints, top line first. The renderer draws the
|
|
104
|
+
* default two-line layout (`defaultCollectorInfoLines`) when this is left
|
|
105
|
+
* out; `withCollectorInfoDefaults` fills that same default in. Build one from
|
|
106
|
+
* scratch for a frame with its own collector line, or start from
|
|
107
|
+
* `retroCollectorInfoLines`.
|
|
108
|
+
*/
|
|
109
|
+
lines: z.array(CollectorInfoLineSchema).readonly().optional(),
|
|
110
|
+
});
|
|
111
|
+
/**
|
|
112
|
+
* Where the set symbol goes. The symbol is fitted into the box, right-aligned
|
|
113
|
+
* and vertically centered. An outline is drawn around its silhouette only when
|
|
114
|
+
* both `outlineColor` and `outlineWidth` are set, centered on the edge like a
|
|
115
|
+
* text outline, so it reaches `outlineWidth / 2` past the symbol.
|
|
116
|
+
*/
|
|
117
|
+
export const SetSymbolBoxSchema = BoundsSchema.extend({
|
|
118
|
+
outlineColor: z.string().optional(),
|
|
119
|
+
outlineWidth: z.number().optional(),
|
|
120
|
+
});
|
|
121
|
+
/**
|
|
122
|
+
* How far a card is turned, in degrees clockwise, to read a box set. See
|
|
123
|
+
* `rotation` on `CardBoxesSchema`.
|
|
124
|
+
*/
|
|
125
|
+
export const RotationSchema = z.union([
|
|
126
|
+
z.literal(90),
|
|
127
|
+
z.literal(180),
|
|
128
|
+
z.literal(270),
|
|
129
|
+
]);
|
|
38
130
|
export const CardBoxesSchema = z.object({
|
|
131
|
+
/**
|
|
132
|
+
* How far the card is turned, in degrees clockwise, to read this set: 90 for
|
|
133
|
+
* a battle or a split card, whose text runs up the sheet. Left out, the set
|
|
134
|
+
* reads upright.
|
|
135
|
+
*
|
|
136
|
+
* A turned set's boxes — `art` included — are measured on the sheet as it
|
|
137
|
+
* lies after that turn, so a text box stays the upright rectangle it is
|
|
138
|
+
* everywhere else. At 90 or 270 the default sheet is therefore 4440 wide and
|
|
139
|
+
* 3264 tall. The art is drawn turned with the set.
|
|
140
|
+
*
|
|
141
|
+
* `collectorInfo` is the exception: a card prints it upright along the
|
|
142
|
+
* sheet's bottom edge whichever way the text above it reads, so it is always
|
|
143
|
+
* measured on the unturned sheet. Frame art and masks never turn either —
|
|
144
|
+
* they are the sheet as printed.
|
|
145
|
+
*/
|
|
146
|
+
rotation: RotationSchema.optional(),
|
|
39
147
|
art: BoundsSchema,
|
|
148
|
+
/**
|
|
149
|
+
* Where the card's first art goes once it has a second one, which goes in
|
|
150
|
+
* the second face's `art` (see `SecondFaceBoxesSchema`). Left out, the first
|
|
151
|
+
* art keeps `art`. A card has one art until a second is added, so `art` is
|
|
152
|
+
* still what every card is imported with.
|
|
153
|
+
*/
|
|
154
|
+
firstArt: BoundsSchema.optional(),
|
|
40
155
|
mana: TextBoxSchema,
|
|
41
156
|
title: TextBoxSchema,
|
|
42
157
|
type: TextBoxSchema,
|
|
43
|
-
setSymbol:
|
|
158
|
+
setSymbol: SetSymbolBoxSchema,
|
|
44
159
|
rules: TextBoxSchema.optional(),
|
|
45
160
|
abilities: TextBoxSchema.optional(),
|
|
46
161
|
pt: TextBoxSchema.optional(),
|
|
47
162
|
ptImage: z.object({ x: z.number(), y: z.number() }).optional(),
|
|
48
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Power and toughness each in a box of its own, for a frame that prints
|
|
165
|
+
* them apart rather than as one `power/toughness`. Both are needed; with
|
|
166
|
+
* them, `pt` is not drawn.
|
|
167
|
+
*/
|
|
168
|
+
power: TextBoxSchema.optional(),
|
|
169
|
+
toughness: TextBoxSchema.optional(),
|
|
170
|
+
collectorInfo: CollectorInfoBoxSchema.optional(),
|
|
49
171
|
startingLoyalty: TextBoxSchema.optional(),
|
|
50
172
|
nicknameTitle: TextBoxSchema.optional(),
|
|
51
173
|
keyword: TextBoxSchema.optional(),
|
|
@@ -61,6 +183,62 @@ export const CardBoxesSchema = z.object({
|
|
|
61
183
|
// when the other face is a land — lands have no mana cost, so the
|
|
62
184
|
// flipsideManaCost badge is empty and the rules text fills that space.
|
|
63
185
|
flipsideRules: TextBoxSchema.optional(),
|
|
186
|
+
// Adventure, Omen and Prepare cards print a second spell in a smaller frame
|
|
187
|
+
// inset within the text box. Its name, mana cost, type line and rules text
|
|
188
|
+
// come from the card's second face; `title`, `mana`, `type` and `rules` keep
|
|
189
|
+
// describing the card itself, with `rules` narrowed to the column the inset
|
|
190
|
+
// leaves free. Which side the inset sits on is the layout's own geometry —
|
|
191
|
+
// left for an Adventure or an Omen, right for a Prepare card — so the
|
|
192
|
+
// renderer draws these wherever they are put.
|
|
193
|
+
insetTitle: TextBoxSchema.optional(),
|
|
194
|
+
insetMana: TextBoxSchema.optional(),
|
|
195
|
+
insetType: TextBoxSchema.optional(),
|
|
196
|
+
insetRules: TextBoxSchema.optional(),
|
|
197
|
+
// A battle's defense, printed where a creature's power and toughness would
|
|
198
|
+
// be. Rules text wraps around it the way it does around `pt`.
|
|
199
|
+
defense: TextBoxSchema.optional(),
|
|
200
|
+
// A Fuse card prints its Fuse line once, in a bar under both halves, though
|
|
201
|
+
// Scryfall repeats it at the end of each half's rules. With this box the
|
|
202
|
+
// line is drawn here and dropped from both; without it, it stays in each
|
|
203
|
+
// half's rules.
|
|
204
|
+
fuse: TextBoxSchema.optional(),
|
|
205
|
+
});
|
|
206
|
+
/**
|
|
207
|
+
* Where a card's second face goes when it is printed on the same side as the
|
|
208
|
+
* first: the other half of a split card, the sideways half of an aftermath
|
|
209
|
+
* card, the upside-down half of a flip card. Its name, mana cost, type line,
|
|
210
|
+
* rules text and power/toughness come from the card's second face, each drawn
|
|
211
|
+
* where its box is and left out where there is none.
|
|
212
|
+
*
|
|
213
|
+
* A set of its own rather than more members of `CardBoxes` because it can be
|
|
214
|
+
* turned on its own: `rotation` means what it does there, and is likewise
|
|
215
|
+
* measured against the sheet, not against the first face's set. A split card
|
|
216
|
+
* turns both sets 90; an aftermath card leaves the first upright and turns
|
|
217
|
+
* this one 270; a flip card turns this one 180.
|
|
218
|
+
*
|
|
219
|
+
* The card has one art, placed by the first face's `art`, unless this set has
|
|
220
|
+
* an `art` of its own and the card has been given a second art.
|
|
221
|
+
*/
|
|
222
|
+
export const SecondFaceBoxesSchema = z.object({
|
|
223
|
+
rotation: RotationSchema.optional(),
|
|
224
|
+
/**
|
|
225
|
+
* Where a second art goes: measured like the rest of this set, and drawn
|
|
226
|
+
* turned with it, so it reads the way this face's text does. A layout whose
|
|
227
|
+
* second face has one can take a second art; while a card has one, its first
|
|
228
|
+
* art moves to the first face's `firstArt`. The two are split where they
|
|
229
|
+
* meet — a straight seam midway between them — and each runs on past its
|
|
230
|
+
* other edges under the frame, as a lone art does.
|
|
231
|
+
*/
|
|
232
|
+
art: BoundsSchema.optional(),
|
|
233
|
+
title: TextBoxSchema,
|
|
234
|
+
mana: TextBoxSchema.optional(),
|
|
235
|
+
type: TextBoxSchema,
|
|
236
|
+
setSymbol: SetSymbolBoxSchema.optional(),
|
|
237
|
+
rules: TextBoxSchema.optional(),
|
|
238
|
+
pt: TextBoxSchema.optional(),
|
|
239
|
+
/** Power and toughness apart, as on the first face's set. */
|
|
240
|
+
power: TextBoxSchema.optional(),
|
|
241
|
+
toughness: TextBoxSchema.optional(),
|
|
64
242
|
});
|
|
65
243
|
/**
|
|
66
244
|
* Configuration for planeswalker ability rendering.
|
|
@@ -77,17 +255,91 @@ export const PlaneswalkerRenderConfigSchema = z.object({
|
|
|
77
255
|
staticTextPaddingX: z.number(),
|
|
78
256
|
/** Right padding for all ability text in pixels */
|
|
79
257
|
textPaddingRight: z.number().optional(),
|
|
80
|
-
/**
|
|
258
|
+
/**
|
|
259
|
+
* Top padding before the first ability in pixels. Unused since 1.12: the
|
|
260
|
+
* abilities are spaced by `abilityFill`.
|
|
261
|
+
*/
|
|
81
262
|
topPadding: z.number().optional(),
|
|
82
|
-
/** Bottom padding after the last ability in pixels (background still fills to box bottom) */
|
|
83
|
-
bottomPadding: z.number().optional(),
|
|
84
263
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* Set lower (e.g. 0.85) to shrink sooner when abilities have more text,
|
|
88
|
-
* giving more breathing room between bands. Defaults to 1.0.
|
|
264
|
+
* Bottom padding after the last ability in pixels. Unused since 1.12: see
|
|
265
|
+
* `loyaltyReserve`.
|
|
89
266
|
*/
|
|
267
|
+
bottomPadding: z.number().optional(),
|
|
268
|
+
/** Unused since 1.12: the font is fitted to `abilityFill`. */
|
|
90
269
|
textFillThreshold: z.number().optional(),
|
|
270
|
+
/**
|
|
271
|
+
* The most of the abilities' height their text may take, 0–1, at
|
|
272
|
+
* `abilityFillSize`; the rest is shared out evenly as padding around each
|
|
273
|
+
* ability. The font shrinks in quarter printed points (3.125 of the sheet's
|
|
274
|
+
* points), from the largest at or below the abilities box's `fontSize` down
|
|
275
|
+
* to `minFontSize`, until the text fits it. Printed M15 planeswalkers fill
|
|
276
|
+
* about 0.62 at 88. Defaults to 0.62.
|
|
277
|
+
*/
|
|
278
|
+
abilityFill: z.number().optional(),
|
|
279
|
+
/**
|
|
280
|
+
* The size `abilityFill` holds at, in points. Smaller text may fill more of
|
|
281
|
+
* the height and larger text less, in proportion: printed M15 planeswalkers
|
|
282
|
+
* with little text set it larger, with more room around it. Defaults to 88.
|
|
283
|
+
*/
|
|
284
|
+
abilityFillSize: z.number().optional(),
|
|
285
|
+
/** The smallest size the abilities' font shrinks to, in points. Defaults to 70. */
|
|
286
|
+
minFontSize: z.number().optional(),
|
|
287
|
+
/**
|
|
288
|
+
* The abilities' line height as a multiple of their font size. Printed M15
|
|
289
|
+
* planeswalkers set about 1.32. Defaults to 1.32.
|
|
290
|
+
*/
|
|
291
|
+
lineHeightRatio: z.number().optional(),
|
|
292
|
+
/**
|
|
293
|
+
* The least height a loyalty ability takes, in px, however short its text,
|
|
294
|
+
* so its badge has room. Defaults to 200.
|
|
295
|
+
*/
|
|
296
|
+
badgeSlotHeight: z.number().optional(),
|
|
297
|
+
/**
|
|
298
|
+
* Height at the abilities box's bottom, in px, that the abilities are laid
|
|
299
|
+
* out above: the room beside the starting loyalty. The last ability's band
|
|
300
|
+
* still reaches the box's bottom, and its text can run into the reserve
|
|
301
|
+
* where it clears the badge. Defaults to 60.
|
|
302
|
+
*/
|
|
303
|
+
loyaltyReserve: z.number().optional(),
|
|
304
|
+
/**
|
|
305
|
+
* How far each separator sits below where its two bands meet, in px. The
|
|
306
|
+
* separator's wave is the top edge a band shows, so this moves where an
|
|
307
|
+
* ability looks to start inside its band; its text stays put. Defaults to
|
|
308
|
+
* 24, where printed M15 planeswalkers have it.
|
|
309
|
+
*/
|
|
310
|
+
separatorOffsetY: z.number().optional(),
|
|
311
|
+
/**
|
|
312
|
+
* Draw the translucent bands (and separators) behind the abilities.
|
|
313
|
+
* Defaults to true. Turn it off for a frame whose art shows through the
|
|
314
|
+
* ability box, and give the abilities box an outline so the text reads.
|
|
315
|
+
*/
|
|
316
|
+
abilityBacking: z.boolean().optional(),
|
|
317
|
+
/**
|
|
318
|
+
* A rule painted where each two abilities meet, as a layout's `gradients`
|
|
319
|
+
* are: over the art, under the text. Its `y` is measured from where they
|
|
320
|
+
* meet rather than from the sheet's top, so `-height / 2` centers it there.
|
|
321
|
+
* For a frame without bands, whose art shows through the ability box.
|
|
322
|
+
*/
|
|
323
|
+
abilityDivider: GradientSchema.omit({
|
|
324
|
+
anchor: true,
|
|
325
|
+
slides: true,
|
|
326
|
+
when: true,
|
|
327
|
+
}).optional(),
|
|
328
|
+
/**
|
|
329
|
+
* How far each loyalty badge sits below the center of its ability, in px.
|
|
330
|
+
* Defaults suit M15's badges — plus 0, minus 34, zero 16 — so a badge whose
|
|
331
|
+
* point hangs below its body sits low enough for the body to center.
|
|
332
|
+
*/
|
|
333
|
+
plusBadgeOffsetY: z.number().optional(),
|
|
334
|
+
minusBadgeOffsetY: z.number().optional(),
|
|
335
|
+
zeroBadgeOffsetY: z.number().optional(),
|
|
336
|
+
/**
|
|
337
|
+
* How far the loyalty cost on each badge sits below the center of its
|
|
338
|
+
* ability, in px — moves the number alone, not its badge. Defaults to 0.
|
|
339
|
+
*/
|
|
340
|
+
plusCostOffsetY: z.number().optional(),
|
|
341
|
+
minusCostOffsetY: z.number().optional(),
|
|
342
|
+
zeroCostOffsetY: z.number().optional(),
|
|
91
343
|
});
|
|
92
344
|
/**
|
|
93
345
|
* Default planeswalker render config values, used when a frame template
|
|
@@ -129,6 +381,92 @@ export const defaultSagaConfig = {
|
|
|
129
381
|
textPaddingX: 220,
|
|
130
382
|
badgeWidth: 180,
|
|
131
383
|
};
|
|
384
|
+
/**
|
|
385
|
+
* Configuration for a column of sections — a Class's levels, a Case's steps.
|
|
386
|
+
* Frame templates provide this via LayoutConfig.sectionConfig.
|
|
387
|
+
*/
|
|
388
|
+
export const SectionRenderConfigSchema = z.object({
|
|
389
|
+
/**
|
|
390
|
+
* Leave out a Class's opening reminder ("Gain the next level as a sorcery
|
|
391
|
+
* to add its ability.") and start the column at level 1, as
|
|
392
|
+
* `SagaRenderConfig.skipReminder` does for a Saga's.
|
|
393
|
+
*/
|
|
394
|
+
skipReminder: z.boolean().optional(),
|
|
395
|
+
});
|
|
396
|
+
/**
|
|
397
|
+
* Where one of a layout's fixed dividers goes: the frame's `sectionDivider`,
|
|
398
|
+
* drawn `length` long from (`x`, `y`) — rightwards for a horizontal one,
|
|
399
|
+
* downwards for a vertical one, turned a quarter. `x`, `y` is on the line's
|
|
400
|
+
* centre across its thickness, which the divider's own aspect ratio sets.
|
|
401
|
+
* Measured on the sheet.
|
|
402
|
+
*/
|
|
403
|
+
export const SectionDividerPlacementSchema = z.object({
|
|
404
|
+
x: z.number(),
|
|
405
|
+
y: z.number(),
|
|
406
|
+
length: z.number(),
|
|
407
|
+
orientation: z.enum(["horizontal", "vertical"]).optional(),
|
|
408
|
+
});
|
|
409
|
+
/**
|
|
410
|
+
* Moves the boxes above the rules text down when the text leaves room, so
|
|
411
|
+
* short text shows more art — in whole `step`s, never more than `maxSteps`.
|
|
412
|
+
*
|
|
413
|
+
* The room is the `rules` box's height less the fitted text's and `padding`.
|
|
414
|
+
* The listed `boxes`, every gradient anchored to one of them, and every
|
|
415
|
+
* gradient and tinted image marked `slides`, go down by the slide; the
|
|
416
|
+
* `rules` box's top goes down with them while its bottom stays, so the text,
|
|
417
|
+
* centered in it, goes down by half. Everything else — collector info, power
|
|
418
|
+
* and toughness — stays put.
|
|
419
|
+
*
|
|
420
|
+
* Only a card whose rules are drawn in the `rules` box slides: not a
|
|
421
|
+
* planeswalker, Saga, Class or Case.
|
|
422
|
+
*/
|
|
423
|
+
export const RulesSlideSchema = z.object({
|
|
424
|
+
padding: z.number().nonnegative(),
|
|
425
|
+
step: z.number().positive(),
|
|
426
|
+
maxSteps: z.number().int().nonnegative(),
|
|
427
|
+
boxes: z.array(SlidingBoxEnum).readonly(),
|
|
428
|
+
});
|
|
429
|
+
/**
|
|
430
|
+
* Power and toughness laid out as a row from its right edge, for a frame that
|
|
431
|
+
* prints them apart: the toughness ends at `right`, its symbol sits `gap`
|
|
432
|
+
* left of it, the power ends `spacing` left of that symbol, and the power's
|
|
433
|
+
* symbol sits `gap` left of the power. A wider number — `10`, `100`, `*`
|
|
434
|
+
* — moves everything left of it.
|
|
435
|
+
*
|
|
436
|
+
* The symbols are the tinted images marked `ptSlot`. The `power` and
|
|
437
|
+
* `toughness` boxes still give each number's font, size, color and height;
|
|
438
|
+
* only their `x` and `width` are worked out. First face only, as tinted
|
|
439
|
+
* images are.
|
|
440
|
+
*
|
|
441
|
+
* With no symbol drawn in either slot, say when a setting turns them off, the
|
|
442
|
+
* two print as one `P/T` ending at `right`, in the `toughness` box's style.
|
|
443
|
+
*/
|
|
444
|
+
export const PowerToughnessRowSchema = z.object({
|
|
445
|
+
right: z.number(),
|
|
446
|
+
gap: z.number(),
|
|
447
|
+
spacing: z.number(),
|
|
448
|
+
});
|
|
449
|
+
/** The five colors and colorless, the keys of a mana symbol color map. */
|
|
450
|
+
export const ManaColorEnum = z.enum(["W", "U", "B", "R", "G", "C"]);
|
|
451
|
+
/**
|
|
452
|
+
* How the card's mana symbols are drawn, wherever they appear: the mana cost,
|
|
453
|
+
* rules text, abilities, and the keyword and flipside boxes alike.
|
|
454
|
+
*/
|
|
455
|
+
export const ManaSymbolStyleSchema = z.object({
|
|
456
|
+
/**
|
|
457
|
+
* `"filled"` is the usual solid pip. `"hollow"` draws the same symbols as a
|
|
458
|
+
* ring with the glyph inside it, and a hybrid as two half rings.
|
|
459
|
+
*/
|
|
460
|
+
style: z.enum(["filled", "hollow"]).optional(),
|
|
461
|
+
/** The pip (or ring) color per mana color, in place of the app's. */
|
|
462
|
+
colors: z.partialRecord(ManaColorEnum, HexColorSchema).optional(),
|
|
463
|
+
/**
|
|
464
|
+
* The glyph color per mana color, in place of the app's black. `C` is also
|
|
465
|
+
* the glyph color of every symbol of no color: generic mana, `{C}`, `{T}`,
|
|
466
|
+
* `{X}` and the like.
|
|
467
|
+
*/
|
|
468
|
+
glyphColors: z.partialRecord(ManaColorEnum, HexColorSchema).optional(),
|
|
469
|
+
});
|
|
132
470
|
const CrownRenderConfigSchema = z.object({
|
|
133
471
|
x: z.number(),
|
|
134
472
|
y: z.number(),
|
|
@@ -155,25 +493,35 @@ export const defaultNicknameConfig = {
|
|
|
155
493
|
x: 378,
|
|
156
494
|
y: 577,
|
|
157
495
|
};
|
|
496
|
+
/**
|
|
497
|
+
* A hint rendered under a template setting's control, for anything the label
|
|
498
|
+
* alone can't carry — what the setting changes, or when to reach for it.
|
|
499
|
+
* Optional everywhere; a setting without one renders exactly as before.
|
|
500
|
+
*/
|
|
501
|
+
const helperText = z.string().optional();
|
|
158
502
|
export const TemplateSettingConfigSchema = z.union([
|
|
159
503
|
z.object({
|
|
160
504
|
type: z.literal("boolean"),
|
|
161
505
|
label: z.string(),
|
|
506
|
+
helperText,
|
|
162
507
|
defaultValue: z.boolean(),
|
|
163
508
|
}),
|
|
164
509
|
z.object({
|
|
165
510
|
type: z.literal("string"),
|
|
166
511
|
label: z.string(),
|
|
512
|
+
helperText,
|
|
167
513
|
defaultValue: z.string(),
|
|
168
514
|
}),
|
|
169
515
|
z.object({
|
|
170
516
|
type: z.literal("number"),
|
|
171
517
|
label: z.string(),
|
|
518
|
+
helperText,
|
|
172
519
|
defaultValue: z.string(),
|
|
173
520
|
}),
|
|
174
521
|
z.object({
|
|
175
522
|
type: z.literal("select"),
|
|
176
523
|
label: z.string(),
|
|
524
|
+
helperText,
|
|
177
525
|
defaultValue: z.string(),
|
|
178
526
|
options: z.array(z.string()),
|
|
179
527
|
}),
|
|
@@ -184,10 +532,37 @@ export const LayoutConfigSchema = z.object({
|
|
|
184
532
|
tallBoxes: CardBoxesSchema.optional(),
|
|
185
533
|
/** Boxes for 2-ability planeswalkers (overrides `boxes` when set). */
|
|
186
534
|
twoAbilityBoxes: CardBoxesSchema.optional(),
|
|
535
|
+
/**
|
|
536
|
+
* The second face of a card that prints both faces on one side — split,
|
|
537
|
+
* aftermath and flip cards. Drawn alongside `boxes`, which keeps describing
|
|
538
|
+
* the first face. See `SecondFaceBoxesSchema`.
|
|
539
|
+
*/
|
|
540
|
+
secondFaceBoxes: SecondFaceBoxesSchema.optional(),
|
|
187
541
|
frameAssets: FrameAssetsSchema,
|
|
188
542
|
masks: LayoutMasksSchema.optional(),
|
|
543
|
+
/**
|
|
544
|
+
* Masks for the planeswalker tall frame, used instead of `masks` wherever
|
|
545
|
+
* `tallBoxes` is. The tall frame draws no color overlays, so `noBorder` is
|
|
546
|
+
* the one that matters.
|
|
547
|
+
*/
|
|
548
|
+
tallMasks: LayoutMasksSchema.optional(),
|
|
189
549
|
planeswalkerConfig: PlaneswalkerRenderConfigSchema.optional(),
|
|
190
550
|
sagaConfig: SagaRenderConfigSchema.optional(),
|
|
551
|
+
sectionConfig: SectionRenderConfigSchema.optional(),
|
|
552
|
+
/**
|
|
553
|
+
* Dividers that stay put whatever the card says — an inset spell's rules
|
|
554
|
+
* set off from its name and type line, and the inset from the card's own
|
|
555
|
+
* rules beside it. Drawn with `frameAssets.sectionDivider`, so a layout
|
|
556
|
+
* without one draws none.
|
|
557
|
+
*/
|
|
558
|
+
sectionDividers: z.array(SectionDividerPlacementSchema).optional(),
|
|
559
|
+
/** Painted over the art and under the text, in order. See `GradientSchema`. */
|
|
560
|
+
gradients: z.array(GradientSchema).readonly().optional(),
|
|
561
|
+
/** Shapes painted in the card's colors. See `TintedImageSchema`. */
|
|
562
|
+
tintedImages: z.array(TintedImageSchema).readonly().optional(),
|
|
563
|
+
rulesSlide: RulesSlideSchema.optional(),
|
|
564
|
+
powerToughnessRow: PowerToughnessRowSchema.optional(),
|
|
565
|
+
manaSymbols: ManaSymbolStyleSchema.optional(),
|
|
191
566
|
crownConfig: CrownRenderConfigSchema.optional(),
|
|
192
567
|
nicknameConfig: NicknameRenderConfigSchema.optional(),
|
|
193
568
|
templateSettings: TemplateSettingsConfigSchema.optional(),
|
|
@@ -300,12 +675,39 @@ export const TemplateConfigSchema = z.object({
|
|
|
300
675
|
// opt-in variants — each entry is a full `LayoutConfig` for a layout that
|
|
301
676
|
// already exists in `layouts`, the same way `saga_creature` is a full
|
|
302
677
|
// standalone config. The active variant is chosen by the `frameVariant`
|
|
303
|
-
// template setting; see `getLayoutConfig`.
|
|
678
|
+
// template setting; see `getLayoutConfig`. A layout's own select setting
|
|
679
|
+
// can name one too, for that layout alone: Borderless's "Adventure" setting
|
|
680
|
+
// set to "inset" draws an Adventure from `alternateLayouts.inset.adventure`.
|
|
681
|
+
// The frame variant wins where both name a config for the card's layout.
|
|
304
682
|
alternateLayouts: z
|
|
305
683
|
.record(z.string(), z.partialRecord(LayoutEnum, LayoutConfigSchema))
|
|
306
684
|
.optional(),
|
|
307
685
|
disabledFields: z.array(z.enum(["setSymbol", "collectorInfo"])).optional(),
|
|
308
686
|
});
|
|
687
|
+
/**
|
|
688
|
+
* A font the frame ships, for its text boxes to name in `fontFamily`.
|
|
689
|
+
*
|
|
690
|
+
* `family` is the frame's own name for it and only means something inside
|
|
691
|
+
* this frame: the app keeps each frame's fonts apart, so a frame can neither
|
|
692
|
+
* replace a font the app has (a `family` of "Beleren" is still the frame's
|
|
693
|
+
* file, not the app's) nor clash with another frame's. A box naming a family
|
|
694
|
+
* the frame does not ship gets the app's font of that name, as before.
|
|
695
|
+
*
|
|
696
|
+
* Ship only a font whose license lets it be redistributed. One entry per
|
|
697
|
+
* weight and style; a family with a bold face lists it as a second entry.
|
|
698
|
+
*/
|
|
699
|
+
export const FrameFontSchema = z.object({
|
|
700
|
+
family: z.string().min(1),
|
|
701
|
+
src: AssetUrlSchema,
|
|
702
|
+
weight: z.enum(["normal", "bold"]).optional(),
|
|
703
|
+
style: z.enum(["normal", "italic"]).optional(),
|
|
704
|
+
/**
|
|
705
|
+
* The font's PostScript name, which a PSD export names its text layers by
|
|
706
|
+
* so Photoshop finds the font when it is installed. Left out, a PSD export
|
|
707
|
+
* leaves text in this font as pixels rather than a text layer.
|
|
708
|
+
*/
|
|
709
|
+
postScriptName: z.string().optional(),
|
|
710
|
+
});
|
|
309
711
|
export const FrameSchema = z.object({
|
|
310
712
|
name: z.string(),
|
|
311
713
|
description: z.string(),
|
|
@@ -313,6 +715,8 @@ export const FrameSchema = z.object({
|
|
|
313
715
|
tags: z.array(z.string()),
|
|
314
716
|
disabled: z.boolean().optional(),
|
|
315
717
|
templateSettings: TemplateSettingsConfigSchema.optional(),
|
|
718
|
+
/** Fonts the frame ships. See `FrameFontSchema`. */
|
|
719
|
+
fonts: z.array(FrameFontSchema).readonly().optional(),
|
|
316
720
|
config: TemplateConfigSchema,
|
|
317
721
|
});
|
|
318
722
|
/** Whether a frame exposes any template settings (frame-level or per-layout). */
|