@cardanvil/frame-kit 0.2.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/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/authoring/index.d.ts +2 -0
- package/dist/authoring/index.js +2 -0
- package/dist/authoring/layoutDefaults.d.ts +6 -0
- package/dist/authoring/layoutDefaults.js +80 -0
- package/dist/authoring/omit.d.ts +2 -0
- package/dist/authoring/omit.js +14 -0
- package/dist/cli/api.d.ts +34 -0
- package/dist/cli/api.js +188 -0
- package/dist/cli/build.d.ts +36 -0
- package/dist/cli/build.js +222 -0
- package/dist/cli/bundle.d.ts +23 -0
- package/dist/cli/bundle.js +58 -0
- package/dist/cli/devUrl.d.ts +10 -0
- package/dist/cli/devUrl.js +41 -0
- package/dist/cli/discover.d.ts +25 -0
- package/dist/cli/discover.js +116 -0
- package/dist/cli/emitSchemas.d.ts +2 -0
- package/dist/cli/emitSchemas.js +13 -0
- package/dist/cli/errors.d.ts +8 -0
- package/dist/cli/errors.js +11 -0
- package/dist/cli/frameIndex.d.ts +115 -0
- package/dist/cli/frameIndex.js +104 -0
- package/dist/cli/hash.d.ts +14 -0
- package/dist/cli/hash.js +32 -0
- package/dist/cli/imageSize.d.ts +13 -0
- package/dist/cli/imageSize.js +72 -0
- package/dist/cli/index.d.ts +18 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/loadFrame.d.ts +24 -0
- package/dist/cli/loadFrame.js +90 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +228 -0
- package/dist/cli/report.d.ts +35 -0
- package/dist/cli/report.js +74 -0
- package/dist/cli/schema.d.ts +33 -0
- package/dist/cli/schema.js +51 -0
- package/dist/cli/stableJson.d.ts +11 -0
- package/dist/cli/stableJson.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/manifest/contract.d.ts +10503 -0
- package/dist/manifest/contract.js +87 -0
- package/dist/manifest/deserialize.d.ts +19 -0
- package/dist/manifest/deserialize.js +23 -0
- package/dist/manifest/index.d.ts +4 -0
- package/dist/manifest/index.js +4 -0
- package/dist/manifest/serialize.d.ts +35 -0
- package/dist/manifest/serialize.js +44 -0
- package/dist/manifest/testFrame.d.ts +9 -0
- package/dist/manifest/testFrame.js +53 -0
- package/dist/manifest/walk.d.ts +21 -0
- package/dist/manifest/walk.js +111 -0
- package/dist/schema/assetUrl.d.ts +27 -0
- package/dist/schema/assetUrl.js +28 -0
- package/dist/schema/frame.d.ts +26467 -0
- package/dist/schema/frame.js +312 -0
- package/dist/schema/frameAssets.d.ts +539 -0
- package/dist/schema/frameAssets.js +183 -0
- package/dist/schema/frameMeta.d.ts +24 -0
- package/dist/schema/frameMeta.js +48 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/layout.d.ts +101 -0
- package/dist/schema/layout.js +59 -0
- package/package.json +62 -0
- package/schema/frame-index.schema.json +233 -0
- package/schema/frame-meta.schema.json +54 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AssetUrlSchema } from "./assetUrl.js";
|
|
3
|
+
import { FrameAssetsSchema, FrameColorEnum, LayoutMasksSchema, } from "./frameAssets.js";
|
|
4
|
+
import { LayoutEnum } from "./layout.js";
|
|
5
|
+
export const BoundsSchema = z.object({
|
|
6
|
+
x: z.number(),
|
|
7
|
+
y: z.number(),
|
|
8
|
+
width: z.number(),
|
|
9
|
+
height: z.number(),
|
|
10
|
+
});
|
|
11
|
+
export const TextBoxSchema = BoundsSchema.extend({
|
|
12
|
+
fontSize: z.number(),
|
|
13
|
+
color: z.string().optional(),
|
|
14
|
+
outlineColor: z.string().optional(),
|
|
15
|
+
outlineWidth: z.number().optional(),
|
|
16
|
+
verticalAlign: z.enum(["top", "center"]).optional(),
|
|
17
|
+
textAlign: z.enum(["left", "center", "right"]).optional(),
|
|
18
|
+
/** Per-box font family override. When set, the renderer uses this instead
|
|
19
|
+
* of the language-resolved default (e.g. title font). */
|
|
20
|
+
fontFamily: z.string().optional(),
|
|
21
|
+
opacity: z.number().min(0).max(1).optional(),
|
|
22
|
+
/** Whether mana-cost symbols in this box get a drop-shadow. Defaults to
|
|
23
|
+
* true when omitted (matches the original behavior). Set to false to
|
|
24
|
+
* disable the shadow (e.g. flipside mana-cost badges on light backgrounds). */
|
|
25
|
+
shadow: z.boolean().optional(),
|
|
26
|
+
});
|
|
27
|
+
export const CardBoxesSchema = z.object({
|
|
28
|
+
art: BoundsSchema,
|
|
29
|
+
mana: TextBoxSchema,
|
|
30
|
+
title: TextBoxSchema,
|
|
31
|
+
type: TextBoxSchema,
|
|
32
|
+
setSymbol: BoundsSchema,
|
|
33
|
+
rules: TextBoxSchema.optional(),
|
|
34
|
+
abilities: TextBoxSchema.optional(),
|
|
35
|
+
pt: TextBoxSchema.optional(),
|
|
36
|
+
ptImage: z.object({ x: z.number(), y: z.number() }).optional(),
|
|
37
|
+
collectorInfo: TextBoxSchema.optional(),
|
|
38
|
+
startingLoyalty: TextBoxSchema.optional(),
|
|
39
|
+
nicknameTitle: TextBoxSchema.optional(),
|
|
40
|
+
keyword: TextBoxSchema.optional(),
|
|
41
|
+
flipsidePt: TextBoxSchema.optional(),
|
|
42
|
+
// Modal DFC cards show the other face's type line and mana cost in small
|
|
43
|
+
// badges near the bottom of the rules area so the reader knows what the
|
|
44
|
+
// back plays like without flipping. The badge art itself is resolved from
|
|
45
|
+
// `frameAssets.flipside` (a per-color ColorSet) using the *other* face's
|
|
46
|
+
// frame color — see `borderless/mdfc/config.ts`.
|
|
47
|
+
flipsideType: TextBoxSchema.optional(),
|
|
48
|
+
flipsideManaCost: TextBoxSchema.optional(),
|
|
49
|
+
// The other face's oracle text (last paragraph, flavor stripped), shown
|
|
50
|
+
// when the other face is a land — lands have no mana cost, so the
|
|
51
|
+
// flipsideManaCost badge is empty and the rules text fills that space.
|
|
52
|
+
flipsideRules: TextBoxSchema.optional(),
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Configuration for planeswalker ability rendering.
|
|
56
|
+
* Frame templates provide this via LayoutConfig.planeswalkerConfig.
|
|
57
|
+
*/
|
|
58
|
+
export const PlaneswalkerRenderConfigSchema = z.object({
|
|
59
|
+
/** Spacing between abilities (normalized 0-1) */
|
|
60
|
+
abilitySpacing: z.number(),
|
|
61
|
+
/** Height of the separator image (normalized 0-1) */
|
|
62
|
+
separatorHeight: z.number(),
|
|
63
|
+
/** Left padding for loyalty ability text in pixels (clears the badge) */
|
|
64
|
+
textPaddingX: z.number(),
|
|
65
|
+
/** Left padding for static ability text in pixels (no badge, so smaller) */
|
|
66
|
+
staticTextPaddingX: z.number(),
|
|
67
|
+
/** Right padding for all ability text in pixels */
|
|
68
|
+
textPaddingRight: z.number().optional(),
|
|
69
|
+
/** Top padding before the first ability in pixels (background still fills from box top) */
|
|
70
|
+
topPadding: z.number().optional(),
|
|
71
|
+
/** Bottom padding after the last ability in pixels (background still fills to box bottom) */
|
|
72
|
+
bottomPadding: z.number().optional(),
|
|
73
|
+
/**
|
|
74
|
+
* Fraction of the available height at which the shrink loop starts
|
|
75
|
+
* shrinking the font. 1.0 = only shrink when text literally doesn't fit.
|
|
76
|
+
* Set lower (e.g. 0.85) to shrink sooner when abilities have more text,
|
|
77
|
+
* giving more breathing room between bands. Defaults to 1.0.
|
|
78
|
+
*/
|
|
79
|
+
textFillThreshold: z.number().optional(),
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Default planeswalker render config values, used when a frame template
|
|
83
|
+
* does not provide its own.
|
|
84
|
+
*/
|
|
85
|
+
export const defaultPlaneswalkerConfig = {
|
|
86
|
+
abilitySpacing: 0.005,
|
|
87
|
+
separatorHeight: 0,
|
|
88
|
+
textPaddingX: 150,
|
|
89
|
+
staticTextPaddingX: 50,
|
|
90
|
+
textPaddingRight: 10,
|
|
91
|
+
topPadding: 0,
|
|
92
|
+
bottomPadding: 0,
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Configuration for saga ability rendering.
|
|
96
|
+
* Frame templates provide this via LayoutConfig.sagaConfig.
|
|
97
|
+
*/
|
|
98
|
+
export const SagaRenderConfigSchema = z.object({
|
|
99
|
+
/** Spacing between abilities (normalized 0-1) */
|
|
100
|
+
abilitySpacing: z.number(),
|
|
101
|
+
/** Height of the divider image (normalized 0-1) */
|
|
102
|
+
dividerHeight: z.number(),
|
|
103
|
+
/** Left padding for ability text in pixels (after badges) */
|
|
104
|
+
textPaddingX: z.number(),
|
|
105
|
+
/** Width of the badge area */
|
|
106
|
+
badgeWidth: z.number(),
|
|
107
|
+
/** Skip rendering reminder text and start abilities at top of the abilities box */
|
|
108
|
+
skipReminder: z.boolean().optional(),
|
|
109
|
+
/** Horizontal offset applied to badge X position in pixels */
|
|
110
|
+
badgeOffsetX: z.number().optional(),
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* Default saga render config values.
|
|
114
|
+
*/
|
|
115
|
+
export const defaultSagaConfig = {
|
|
116
|
+
abilitySpacing: 0.01,
|
|
117
|
+
dividerHeight: 0.015,
|
|
118
|
+
textPaddingX: 220,
|
|
119
|
+
badgeWidth: 180,
|
|
120
|
+
};
|
|
121
|
+
const CrownRenderConfigSchema = z.object({
|
|
122
|
+
x: z.number(),
|
|
123
|
+
y: z.number(),
|
|
124
|
+
nicknameCrownX: z.number().optional(),
|
|
125
|
+
nicknameCrownY: z.number().optional(),
|
|
126
|
+
/** Absolute canvas position of the nyx crown insert. When omitted, the
|
|
127
|
+
* insert is centered horizontally on the crown and placed 2px below the
|
|
128
|
+
* crown's top edge (matching the original m15 placement). */
|
|
129
|
+
nyxInsertX: z.number().optional(),
|
|
130
|
+
nyxInsertY: z.number().optional(),
|
|
131
|
+
/** Override used in place of `nyxInsertY` when the card has a nickname
|
|
132
|
+
* (the nickname crown sits at a different height, so the insert shifts). */
|
|
133
|
+
nyxInsertNicknameY: z.number().optional(),
|
|
134
|
+
});
|
|
135
|
+
export const defaultCrownConfig = {
|
|
136
|
+
x: 192,
|
|
137
|
+
y: 220,
|
|
138
|
+
};
|
|
139
|
+
const NicknameRenderConfigSchema = z.object({
|
|
140
|
+
x: z.number(),
|
|
141
|
+
y: z.number(),
|
|
142
|
+
});
|
|
143
|
+
export const defaultNicknameConfig = {
|
|
144
|
+
x: 378,
|
|
145
|
+
y: 577,
|
|
146
|
+
};
|
|
147
|
+
export const TemplateSettingConfigSchema = z.union([
|
|
148
|
+
z.object({
|
|
149
|
+
type: z.literal("boolean"),
|
|
150
|
+
label: z.string(),
|
|
151
|
+
defaultValue: z.boolean(),
|
|
152
|
+
}),
|
|
153
|
+
z.object({
|
|
154
|
+
type: z.literal("string"),
|
|
155
|
+
label: z.string(),
|
|
156
|
+
defaultValue: z.string(),
|
|
157
|
+
}),
|
|
158
|
+
z.object({
|
|
159
|
+
type: z.literal("number"),
|
|
160
|
+
label: z.string(),
|
|
161
|
+
defaultValue: z.string(),
|
|
162
|
+
}),
|
|
163
|
+
z.object({
|
|
164
|
+
type: z.literal("select"),
|
|
165
|
+
label: z.string(),
|
|
166
|
+
defaultValue: z.string(),
|
|
167
|
+
options: z.array(z.string()),
|
|
168
|
+
}),
|
|
169
|
+
]);
|
|
170
|
+
export const TemplateSettingsConfigSchema = z.record(z.string(), TemplateSettingConfigSchema);
|
|
171
|
+
export const LayoutConfigSchema = z.object({
|
|
172
|
+
boxes: CardBoxesSchema,
|
|
173
|
+
tallBoxes: CardBoxesSchema.optional(),
|
|
174
|
+
/** Boxes for 2-ability planeswalkers (overrides `boxes` when set). */
|
|
175
|
+
twoAbilityBoxes: CardBoxesSchema.optional(),
|
|
176
|
+
frameAssets: FrameAssetsSchema,
|
|
177
|
+
masks: LayoutMasksSchema.optional(),
|
|
178
|
+
planeswalkerConfig: PlaneswalkerRenderConfigSchema.optional(),
|
|
179
|
+
sagaConfig: SagaRenderConfigSchema.optional(),
|
|
180
|
+
crownConfig: CrownRenderConfigSchema.optional(),
|
|
181
|
+
nicknameConfig: NicknameRenderConfigSchema.optional(),
|
|
182
|
+
templateSettings: TemplateSettingsConfigSchema.optional(),
|
|
183
|
+
/** Per-color art box overrides (e.g. colorless planeswalkers need different art bounds). */
|
|
184
|
+
colorArtOverrides: z.partialRecord(FrameColorEnum, BoundsSchema).optional(),
|
|
185
|
+
/**
|
|
186
|
+
/**
|
|
187
|
+
* Which mask the two-color-hybrid colorless title/type wash overlay uses.
|
|
188
|
+
* Defaults to "titleAndType" (recolors both the name and type boxes). Set
|
|
189
|
+
* to "title" for templates (e.g. Borderless) whose type box shouldn't get
|
|
190
|
+
* the colorless hybrid wash.
|
|
191
|
+
*/
|
|
192
|
+
hybridTitleMask: z.enum(["title", "titleAndType"]).optional(),
|
|
193
|
+
/**
|
|
194
|
+
* Frame assets for the back face of double-faced cards (transform, modal
|
|
195
|
+
* DFC). When present, the renderer swaps to these for `faceIndex === 1`.
|
|
196
|
+
* Omitted for single-faced layouts.
|
|
197
|
+
*/
|
|
198
|
+
backFrameAssets: FrameAssetsSchema.optional(),
|
|
199
|
+
/** Section masks for the back face of double-faced cards. */
|
|
200
|
+
backMasks: LayoutMasksSchema.optional(),
|
|
201
|
+
/** Text box positions for the back face of double-faced cards. */
|
|
202
|
+
backBoxes: CardBoxesSchema.optional(),
|
|
203
|
+
/**
|
|
204
|
+
* Boxes to use instead of `boxes`/`backBoxes` (or their saga-front/back
|
|
205
|
+
* counterparts below) when the textless creature frame is in play — it
|
|
206
|
+
* repositions the type/setSymbol/pt boxes. See `renderCardToCanvas.ts`.
|
|
207
|
+
*/
|
|
208
|
+
creatureBoxes: CardBoxesSchema.optional(),
|
|
209
|
+
backCreatureBoxes: CardBoxesSchema.optional(),
|
|
210
|
+
/**
|
|
211
|
+
* Masks to use instead of `masks`/`backMasks` (or their saga-front/back
|
|
212
|
+
* counterparts below) when the textless creature frame is in play — its
|
|
213
|
+
* baked-in PT plate changes the frame art's contours, so pinlines/border/
|
|
214
|
+
* title masks shaped for the plain textless frame don't line up. See
|
|
215
|
+
* `renderCardToCanvas.ts`.
|
|
216
|
+
*/
|
|
217
|
+
creatureMasks: LayoutMasksSchema.optional(),
|
|
218
|
+
backCreatureMasks: LayoutMasksSchema.optional(),
|
|
219
|
+
/**
|
|
220
|
+
* Boxes to use instead of `boxes` when the flipside PT box is present and
|
|
221
|
+
* will actually render (the other face has power/toughness) — e.g. gives a
|
|
222
|
+
* transforming Saga's abilities box a bit more clearance above it.
|
|
223
|
+
*/
|
|
224
|
+
flipsidePtBoxes: CardBoxesSchema.optional(),
|
|
225
|
+
/**
|
|
226
|
+
* Saga-shaped boxes/assets/masks for a transform DFC's Saga face, keyed by
|
|
227
|
+
* which physical side (front/back) that face happens to print on for a
|
|
228
|
+
* given card — e.g. Fable of the Mirror-Breaker has its Saga on the front,
|
|
229
|
+
* Clive, Ifrit's Dominant // Ifrit, Warden of Inferno has it on the back.
|
|
230
|
+
* `boxes`/`frameAssets`/`masks` and `backBoxes`/`backFrameAssets`/`backMasks`
|
|
231
|
+
* above always describe the OTHER (plain permanent) face for these layouts.
|
|
232
|
+
* The renderer picks between these four sets per-face using `faceIndex`
|
|
233
|
+
* plus whether that face's own (flattened) type line contains "Saga" —
|
|
234
|
+
* see `renderCardToCanvas.ts`.
|
|
235
|
+
*/
|
|
236
|
+
sagaFrontBoxes: CardBoxesSchema.optional(),
|
|
237
|
+
sagaFrontFrameAssets: FrameAssetsSchema.optional(),
|
|
238
|
+
sagaFrontMasks: LayoutMasksSchema.optional(),
|
|
239
|
+
sagaFrontCreatureBoxes: CardBoxesSchema.optional(),
|
|
240
|
+
sagaFrontCreatureMasks: LayoutMasksSchema.optional(),
|
|
241
|
+
sagaBackBoxes: CardBoxesSchema.optional(),
|
|
242
|
+
sagaBackFrameAssets: FrameAssetsSchema.optional(),
|
|
243
|
+
sagaBackMasks: LayoutMasksSchema.optional(),
|
|
244
|
+
sagaBackCreatureBoxes: CardBoxesSchema.optional(),
|
|
245
|
+
sagaBackCreatureMasks: LayoutMasksSchema.optional(),
|
|
246
|
+
/**
|
|
247
|
+
* Frame assets for the *second* frame in a composited front face — used by
|
|
248
|
+
* transforming MDFC cards (e.g. King T'Challa) whose front face is built
|
|
249
|
+
* from the MDFC front frame's top half composited with the transform front
|
|
250
|
+
* frame's bottom half. Only the front face (faceIndex 0) composites; the
|
|
251
|
+
* back face renders normally from `backFrameAssets`. See
|
|
252
|
+
* `drawCardToStage.ts`.
|
|
253
|
+
*/
|
|
254
|
+
compositeFrameAssets: FrameAssetsSchema.optional(),
|
|
255
|
+
/** Masks for the second frame in a composited front face (must ship
|
|
256
|
+
* `bottomHalf`). See `compositeFrameAssets` above. */
|
|
257
|
+
compositeMasks: LayoutMasksSchema.optional(),
|
|
258
|
+
});
|
|
259
|
+
/**
|
|
260
|
+
* The canvas a frame's boxes are measured against when it declares none.
|
|
261
|
+
*
|
|
262
|
+
* The full printed sheet including bleed — 2.72 x 3.7 in at 1200 DPI, or
|
|
263
|
+
* 69.09 x 93.98 mm. The card face inside it is 63 x 88 mm, leaving roughly 3 mm
|
|
264
|
+
* of bleed per edge.
|
|
265
|
+
*/
|
|
266
|
+
export const DEFAULT_CANVAS_WIDTH = 3264;
|
|
267
|
+
export const DEFAULT_CANVAS_HEIGHT = 4440;
|
|
268
|
+
export const TemplateConfigSchema = z.object({
|
|
269
|
+
/**
|
|
270
|
+
* Native pixel size this frame's box coordinates are authored in.
|
|
271
|
+
*
|
|
272
|
+
* Defaults to 3264 x 4440 when omitted: the full printed sheet *including*
|
|
273
|
+
* bleed, 2.72 x 3.7 in (69.09 x 93.98 mm) at 1200 DPI. The card face inside
|
|
274
|
+
* it is 63 x 88 mm, leaving roughly 3 mm of bleed on every edge — so box
|
|
275
|
+
* coordinates are relative to the sheet, not to the card.
|
|
276
|
+
*/
|
|
277
|
+
canvas: z
|
|
278
|
+
.object({
|
|
279
|
+
width: z.number().int().positive(),
|
|
280
|
+
height: z.number().int().positive(),
|
|
281
|
+
})
|
|
282
|
+
.optional(),
|
|
283
|
+
layouts: z.partialRecord(LayoutEnum, LayoutConfigSchema),
|
|
284
|
+
// Alternate per-layout configs used instead of `layouts` when a non-default
|
|
285
|
+
// frame variant is selected. Each key is a variant name (e.g. "textless",
|
|
286
|
+
// "short") whose value is a map of layout → full `LayoutConfig`, the same
|
|
287
|
+
// shape as `layouts`. Kept separate from `layouts` so UI that enumerates
|
|
288
|
+
// supported card layouts (e.g. FrameSelection) isn't polluted with these
|
|
289
|
+
// opt-in variants — each entry is a full `LayoutConfig` for a layout that
|
|
290
|
+
// already exists in `layouts`, the same way `saga_creature` is a full
|
|
291
|
+
// standalone config. The active variant is chosen by the `frameVariant`
|
|
292
|
+
// template setting; see `getLayoutConfig`.
|
|
293
|
+
alternateLayouts: z
|
|
294
|
+
.record(z.string(), z.partialRecord(LayoutEnum, LayoutConfigSchema))
|
|
295
|
+
.optional(),
|
|
296
|
+
disabledFields: z.array(z.enum(["setSymbol", "collectorInfo"])).optional(),
|
|
297
|
+
});
|
|
298
|
+
export const FrameSchema = z.object({
|
|
299
|
+
name: z.string(),
|
|
300
|
+
description: z.string(),
|
|
301
|
+
previewImage: AssetUrlSchema,
|
|
302
|
+
tags: z.array(z.string()),
|
|
303
|
+
disabled: z.boolean().optional(),
|
|
304
|
+
templateSettings: TemplateSettingsConfigSchema.optional(),
|
|
305
|
+
config: TemplateConfigSchema,
|
|
306
|
+
});
|
|
307
|
+
/** Whether a frame exposes any template settings (frame-level or per-layout). */
|
|
308
|
+
export function frameHasTemplateSettings(frame) {
|
|
309
|
+
const frameLevel = Boolean(frame.templateSettings && Object.keys(frame.templateSettings).length > 0);
|
|
310
|
+
const layoutLevel = Object.values(frame.config.layouts).some((l) => Boolean(l.templateSettings));
|
|
311
|
+
return frameLevel || layoutLevel;
|
|
312
|
+
}
|