@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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/dist/authoring/index.d.ts +2 -0
  4. package/dist/authoring/index.js +2 -0
  5. package/dist/authoring/layoutDefaults.d.ts +6 -0
  6. package/dist/authoring/layoutDefaults.js +80 -0
  7. package/dist/authoring/omit.d.ts +2 -0
  8. package/dist/authoring/omit.js +14 -0
  9. package/dist/cli/api.d.ts +34 -0
  10. package/dist/cli/api.js +188 -0
  11. package/dist/cli/build.d.ts +36 -0
  12. package/dist/cli/build.js +222 -0
  13. package/dist/cli/bundle.d.ts +23 -0
  14. package/dist/cli/bundle.js +58 -0
  15. package/dist/cli/devUrl.d.ts +10 -0
  16. package/dist/cli/devUrl.js +41 -0
  17. package/dist/cli/discover.d.ts +25 -0
  18. package/dist/cli/discover.js +116 -0
  19. package/dist/cli/emitSchemas.d.ts +2 -0
  20. package/dist/cli/emitSchemas.js +13 -0
  21. package/dist/cli/errors.d.ts +8 -0
  22. package/dist/cli/errors.js +11 -0
  23. package/dist/cli/frameIndex.d.ts +115 -0
  24. package/dist/cli/frameIndex.js +104 -0
  25. package/dist/cli/hash.d.ts +14 -0
  26. package/dist/cli/hash.js +32 -0
  27. package/dist/cli/imageSize.d.ts +13 -0
  28. package/dist/cli/imageSize.js +72 -0
  29. package/dist/cli/index.d.ts +18 -0
  30. package/dist/cli/index.js +18 -0
  31. package/dist/cli/loadFrame.d.ts +24 -0
  32. package/dist/cli/loadFrame.js +90 -0
  33. package/dist/cli/main.d.ts +2 -0
  34. package/dist/cli/main.js +228 -0
  35. package/dist/cli/report.d.ts +35 -0
  36. package/dist/cli/report.js +74 -0
  37. package/dist/cli/schema.d.ts +33 -0
  38. package/dist/cli/schema.js +51 -0
  39. package/dist/cli/stableJson.d.ts +11 -0
  40. package/dist/cli/stableJson.js +27 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +2 -0
  43. package/dist/manifest/contract.d.ts +10503 -0
  44. package/dist/manifest/contract.js +87 -0
  45. package/dist/manifest/deserialize.d.ts +19 -0
  46. package/dist/manifest/deserialize.js +23 -0
  47. package/dist/manifest/index.d.ts +4 -0
  48. package/dist/manifest/index.js +4 -0
  49. package/dist/manifest/serialize.d.ts +35 -0
  50. package/dist/manifest/serialize.js +44 -0
  51. package/dist/manifest/testFrame.d.ts +9 -0
  52. package/dist/manifest/testFrame.js +53 -0
  53. package/dist/manifest/walk.d.ts +21 -0
  54. package/dist/manifest/walk.js +111 -0
  55. package/dist/schema/assetUrl.d.ts +27 -0
  56. package/dist/schema/assetUrl.js +28 -0
  57. package/dist/schema/frame.d.ts +26467 -0
  58. package/dist/schema/frame.js +312 -0
  59. package/dist/schema/frameAssets.d.ts +539 -0
  60. package/dist/schema/frameAssets.js +183 -0
  61. package/dist/schema/frameMeta.d.ts +24 -0
  62. package/dist/schema/frameMeta.js +48 -0
  63. package/dist/schema/index.d.ts +5 -0
  64. package/dist/schema/index.js +5 -0
  65. package/dist/schema/layout.d.ts +101 -0
  66. package/dist/schema/layout.js +59 -0
  67. package/package.json +62 -0
  68. package/schema/frame-index.schema.json +233 -0
  69. 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
+ }