@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
package/README.md
CHANGED
|
@@ -43,6 +43,30 @@ export const myFrame = {
|
|
|
43
43
|
`as const satisfies Frame` is the idiom: `satisfies` checks the object against the contract, `as const`
|
|
44
44
|
keeps the literal types.
|
|
45
45
|
|
|
46
|
+
### The collector line
|
|
47
|
+
|
|
48
|
+
What the collector box prints is data too: its `lines`, each a row of elements (`rarity`, `cardNumber`,
|
|
49
|
+
`setCode`, `artist`, …) with their own spacing, sizing, fonts and alignment. `withCollectorInfoDefaults`
|
|
50
|
+
fills in `defaultCollectorInfoLines` unless the bounds bring their own. Pass `retroCollectorInfoLines`
|
|
51
|
+
for the Retro frame's centered artist and creator lines, or write your own:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const collectorBounds = {
|
|
55
|
+
x: 460,
|
|
56
|
+
y: 3880,
|
|
57
|
+
width: 2330,
|
|
58
|
+
height: 200,
|
|
59
|
+
fontSize: 58,
|
|
60
|
+
lines: [
|
|
61
|
+
{ elements: [{ type: "artist", prefix: "Illus. " }], align: "center" },
|
|
62
|
+
{
|
|
63
|
+
elements: [{ type: "setCode", spacingAfter: 24 }, { type: "cardNumber" }],
|
|
64
|
+
align: "center",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
} as const satisfies CollectorInfoBox;
|
|
68
|
+
```
|
|
69
|
+
|
|
46
70
|
## Entry points
|
|
47
71
|
|
|
48
72
|
| Import | What it gives you |
|
|
@@ -58,6 +82,7 @@ frame-kit validate # check every frame, write nothing
|
|
|
58
82
|
frame-kit build --out dist # check, then pack each one
|
|
59
83
|
frame-kit build --out dist --watch # …and repack whenever a source changes
|
|
60
84
|
frame-kit schema meta # a format as JSON Schema
|
|
85
|
+
frame-kit trim --shared a.png b.png # cut art down to what is visible
|
|
61
86
|
frame-kit --help
|
|
62
87
|
```
|
|
63
88
|
|
|
@@ -129,6 +154,31 @@ Packaging needs Vite — it is what turns `import w from "./w.png"` into a file
|
|
|
129
154
|
so it is an **optional** peer dependency, loaded only when packaging runs. The
|
|
130
155
|
schemas above work under plain Node and in a browser without it.
|
|
131
156
|
|
|
157
|
+
### `frame-kit trim`
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
frame-kit trim [--shared] [--out <dir>] [--dry-run] [--json] <png...>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Cuts PNGs down to their visible pixels and prints where each now sits on the
|
|
164
|
+
sheet. Art is authored on the full sheet so it lines up by itself, but a
|
|
165
|
+
renderer decodes, caches and composites every pixel of an image, transparent or
|
|
166
|
+
not — a Saga banner is under 5% of its sheet, and trimmed it costs a twentieth
|
|
167
|
+
as much on every card.
|
|
168
|
+
|
|
169
|
+
`--shared` cuts every image given to one rectangle, the smallest holding all of
|
|
170
|
+
their visible pixels, so images drawn together — a banner and the masks cut from
|
|
171
|
+
it — keep one position. Put that position in the frame wherever a field asks for
|
|
172
|
+
one, such as `saga.bannerOrigin`; only fields that take a position can use
|
|
173
|
+
trimmed art.
|
|
174
|
+
|
|
175
|
+
The pixels kept are byte-for-byte the originals, and the chunks that tell a
|
|
176
|
+
decoder how to show them — colour profile, gamma — are carried over, so the art
|
|
177
|
+
draws exactly as before. Each trimmed image also records its position in an
|
|
178
|
+
`oFFs` chunk, so trimming it again reports where it sits on the sheet rather
|
|
179
|
+
than in itself. Without `--out` each image replaces its original. Only 8-bit
|
|
180
|
+
PNGs are trimmed: decoding 16 bits a channel would lose detail.
|
|
181
|
+
|
|
132
182
|
### `frame-kit studio`
|
|
133
183
|
|
|
134
184
|
```bash
|
|
@@ -155,7 +205,7 @@ that are not written as literals are reported as such rather than edited.
|
|
|
155
205
|
|
|
156
206
|
### `@cardanvil/frame-kit/layout`
|
|
157
207
|
|
|
158
|
-
How a frame
|
|
208
|
+
How a frame is drawn for one card, as pure functions:
|
|
159
209
|
|
|
160
210
|
- `selectFrameLayers(details, options)` — the base frame and section overlays a
|
|
161
211
|
set of `FrameDetails` selects. Split out of Card Anvil's `getFrameLayers`, so
|
|
@@ -170,6 +220,30 @@ How a frame's art is chosen and placed, as pure functions:
|
|
|
170
220
|
middle of the card.
|
|
171
221
|
- `resolveFrameAsset` / `resolveBaseFrame` — exact per-family lookup, and the
|
|
172
222
|
base-frame fallback chain.
|
|
223
|
+
- `frameCutoutMasks(input)` — the masks cut out of a face's frame body, in the
|
|
224
|
+
renderer's order: "No Border", the legendary crown's space, the extended-art
|
|
225
|
+
silhouette, the colorless nickname banner.
|
|
226
|
+
- `ringMaskFor(masks, useFullBorder)` and `borderStateFor(input)` — the mask a
|
|
227
|
+
border color recolors through, and the border state that results:
|
|
228
|
+
`default`, `light`, `dark` or `none`.
|
|
229
|
+
- `resolveBoxOverrides(boxes, state)` — a box set with each text box's
|
|
230
|
+
`overrides` merged in for one render's border state and template settings,
|
|
231
|
+
so everything that draws a frame agrees on what a condition matches.
|
|
232
|
+
`overrideMatches` tests a single rule, and
|
|
233
|
+
`resolveSecondFaceBoxOverrides` does the same for a `secondFaceBoxes` set.
|
|
234
|
+
- `turnPlacement(sheet, rotation)` / `turnMatrix(sheet, rotation)` — where a
|
|
235
|
+
turned box set (one with a `rotation`) sits on the sheet, as a node's
|
|
236
|
+
position and rotation or as a matrix. `turnedSheetSize` is the sheet as such
|
|
237
|
+
a set measures it, `turnedBoundsOnSheet` one of its boxes back on the sheet
|
|
238
|
+
as printed, and `turnedVectorOnSheet` / `sheetVectorInTurned` a direction
|
|
239
|
+
taken one way or the other.
|
|
240
|
+
- `templateSettingDefaults(frame)`, `mergeTemplateSettings` and
|
|
241
|
+
`layoutTemplateSettings(settings, layout)` — a frame's settings the way Card
|
|
242
|
+
Anvil stores them, and the values in effect for one layout.
|
|
243
|
+
- `textOutlineFor(box)` / `textShadowFor(box)` — how a plain-text box is
|
|
244
|
+
outlined or shadowed.
|
|
245
|
+
- `getReadableTextColor` and friends — the contrast rule that decides whether
|
|
246
|
+
a border color reads as light or dark.
|
|
173
247
|
|
|
174
248
|
Card Anvil still owns the other half — reading a Scryfall card into
|
|
175
249
|
`FrameDetails` — because that is where the Scryfall types live.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CollectorInfoLine } from "../schema/collectorInfo.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default collector info layout with two lines, left-aligned.
|
|
4
|
+
* Line 1: rarity | cardNumber | creatorName
|
|
5
|
+
* Line 2: setCode | separator | language | artistSymbol | artist
|
|
6
|
+
*
|
|
7
|
+
* `withCollectorInfoDefaults` gives these to collector info unless its bounds
|
|
8
|
+
* bring their own `lines`, and the renderer falls back to them for a collector
|
|
9
|
+
* box without any.
|
|
10
|
+
*/
|
|
11
|
+
export declare const defaultCollectorInfoLines: readonly CollectorInfoLine[];
|
|
12
|
+
/**
|
|
13
|
+
* Retro collector info layout: just artist (top line) and creator name
|
|
14
|
+
* (bottom line), both centered in the box instead of flowing from its left
|
|
15
|
+
* edge like the default lines do.
|
|
16
|
+
*/
|
|
17
|
+
export declare const retroCollectorInfoLines: readonly CollectorInfoLine[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default collector info layout with two lines, left-aligned.
|
|
3
|
+
* Line 1: rarity | cardNumber | creatorName
|
|
4
|
+
* Line 2: setCode | separator | language | artistSymbol | artist
|
|
5
|
+
*
|
|
6
|
+
* `withCollectorInfoDefaults` gives these to collector info unless its bounds
|
|
7
|
+
* bring their own `lines`, and the renderer falls back to them for a collector
|
|
8
|
+
* box without any.
|
|
9
|
+
*/
|
|
10
|
+
export const defaultCollectorInfoLines = [
|
|
11
|
+
{
|
|
12
|
+
elements: [
|
|
13
|
+
{ type: "rarity", spacingAfter: 34 }, // Extra space before card number
|
|
14
|
+
{ type: "cardNumber", spacingAfter: 40, letterSpacing: 8 }, // Extra space before creator name
|
|
15
|
+
{ type: "creatorName", minXFromElement: "artistSymbol" },
|
|
16
|
+
],
|
|
17
|
+
spacing: 10, // Space between elements
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
elements: [
|
|
21
|
+
{ type: "setCode", spacingAfter: 24, letterSpacing: 6 }, // Extra space before language
|
|
22
|
+
{
|
|
23
|
+
type: "separator",
|
|
24
|
+
useManaFont: true,
|
|
25
|
+
manaSymbol: "•",
|
|
26
|
+
fontSizeMultiplierDot: 1.9,
|
|
27
|
+
fontSizeMultiplierStar: 1.4,
|
|
28
|
+
yOffsetDot: 5,
|
|
29
|
+
yOffsetStar: 20,
|
|
30
|
+
spacingAfter: 10,
|
|
31
|
+
},
|
|
32
|
+
{ type: "language", spacingAfter: 38, letterSpacing: 4 }, // Extra space before artist symbol
|
|
33
|
+
{
|
|
34
|
+
type: "artistSymbol",
|
|
35
|
+
useManaFont: true,
|
|
36
|
+
manaSymbol: "a",
|
|
37
|
+
fontSizeMultiplier: 1.24,
|
|
38
|
+
},
|
|
39
|
+
{ type: "artist", fontSizeMultiplier: 1.1, letterSpacing: 0 },
|
|
40
|
+
],
|
|
41
|
+
spacing: 10,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* Retro collector info layout: just artist (top line) and creator name
|
|
46
|
+
* (bottom line), both centered in the box instead of flowing from its left
|
|
47
|
+
* edge like the default lines do.
|
|
48
|
+
*/
|
|
49
|
+
export const retroCollectorInfoLines = [
|
|
50
|
+
{
|
|
51
|
+
elements: [{ type: "artist", prefix: "Illus. " }],
|
|
52
|
+
align: "center",
|
|
53
|
+
fontSizeMultiplier: 1.5,
|
|
54
|
+
fontFamily: "MPlantin",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
elements: [{ type: "creatorName" }],
|
|
58
|
+
align: "center",
|
|
59
|
+
fontSizeMultiplier: 1.3,
|
|
60
|
+
fontFamily: "MPlantin",
|
|
61
|
+
},
|
|
62
|
+
];
|
package/dist/authoring/index.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
import { LayoutConfig, TemplateSettingsConfig,
|
|
1
|
+
import { CollectorInfoBox, LayoutConfig, TemplateSettingsConfig, TextBoxOverride } from "../schema/frame.js";
|
|
2
|
+
/**
|
|
3
|
+
* `overrides` for text printed on the frame's outer border ring — on most
|
|
4
|
+
* frames, the collector line. The text turns black once a light border color
|
|
5
|
+
* is picked, and gains a black outline when "No Border" cuts the ring away and
|
|
6
|
+
* leaves it over the art.
|
|
7
|
+
*
|
|
8
|
+
* `withCollectorInfoDefaults` gives these to collector info unless its bounds
|
|
9
|
+
* bring their own.
|
|
10
|
+
*/
|
|
11
|
+
export declare const onBorderTextOverrides: readonly TextBoxOverride[];
|
|
2
12
|
/**
|
|
3
13
|
* Applies default collector info bounds to a layout config if not specified.
|
|
14
|
+
*
|
|
15
|
+
* The collector info this fills in is taken to sit on the border ring, so it
|
|
16
|
+
* gets `onBorderTextOverrides` — unless `defaultCollectorInfoBounds` brings its
|
|
17
|
+
* own `overrides`, with `[]` for a collector line printed where the border
|
|
18
|
+
* settings never reach. It prints `defaultCollectorInfoLines` unless the bounds
|
|
19
|
+
* bring their own `lines` — `retroCollectorInfoLines`, or a layout of the
|
|
20
|
+
* frame's own. A collector box the layout defines itself is left exactly as
|
|
21
|
+
* written.
|
|
4
22
|
*/
|
|
5
|
-
export declare function withCollectorInfoDefaults<T extends LayoutConfig>(config: T, defaultCollectorInfoBounds:
|
|
23
|
+
export declare function withCollectorInfoDefaults<T extends LayoutConfig>(config: T, defaultCollectorInfoBounds: CollectorInfoBox): T;
|
|
6
24
|
export declare function withSettingsDefaults<T extends LayoutConfig>(config: T, defaultSettings: TemplateSettingsConfig): T;
|
|
@@ -1,62 +1,97 @@
|
|
|
1
|
+
import { defaultCollectorInfoLines } from "./collectorInfo.js";
|
|
2
|
+
/**
|
|
3
|
+
* `overrides` for text printed on the frame's outer border ring — on most
|
|
4
|
+
* frames, the collector line. The text turns black once a light border color
|
|
5
|
+
* is picked, and gains a black outline when "No Border" cuts the ring away and
|
|
6
|
+
* leaves it over the art.
|
|
7
|
+
*
|
|
8
|
+
* `withCollectorInfoDefaults` gives these to collector info unless its bounds
|
|
9
|
+
* bring their own.
|
|
10
|
+
*/
|
|
11
|
+
export const onBorderTextOverrides = [
|
|
12
|
+
{
|
|
13
|
+
when: { border: "none" },
|
|
14
|
+
style: { outlineColor: "black", outlineWidth: 19 },
|
|
15
|
+
},
|
|
16
|
+
{ when: { border: "light" }, style: { color: "black" } },
|
|
17
|
+
];
|
|
1
18
|
/**
|
|
2
19
|
* Applies default collector info bounds to a layout config if not specified.
|
|
20
|
+
*
|
|
21
|
+
* The collector info this fills in is taken to sit on the border ring, so it
|
|
22
|
+
* gets `onBorderTextOverrides` — unless `defaultCollectorInfoBounds` brings its
|
|
23
|
+
* own `overrides`, with `[]` for a collector line printed where the border
|
|
24
|
+
* settings never reach. It prints `defaultCollectorInfoLines` unless the bounds
|
|
25
|
+
* bring their own `lines` — `retroCollectorInfoLines`, or a layout of the
|
|
26
|
+
* frame's own. A collector box the layout defines itself is left exactly as
|
|
27
|
+
* written.
|
|
3
28
|
*/
|
|
4
29
|
export function withCollectorInfoDefaults(config, defaultCollectorInfoBounds) {
|
|
30
|
+
const collectorInfo = {
|
|
31
|
+
...defaultCollectorInfoBounds,
|
|
32
|
+
overrides: defaultCollectorInfoBounds.overrides ?? onBorderTextOverrides,
|
|
33
|
+
lines: defaultCollectorInfoBounds.lines ?? defaultCollectorInfoLines,
|
|
34
|
+
};
|
|
5
35
|
return {
|
|
6
36
|
...config,
|
|
7
37
|
boxes: {
|
|
8
38
|
...config.boxes,
|
|
9
|
-
collectorInfo: config.boxes.collectorInfo ??
|
|
39
|
+
collectorInfo: config.boxes.collectorInfo ?? collectorInfo,
|
|
10
40
|
},
|
|
11
41
|
tallBoxes: config.tallBoxes
|
|
12
42
|
? {
|
|
13
43
|
...config.tallBoxes,
|
|
14
|
-
collectorInfo: config.tallBoxes.collectorInfo ??
|
|
44
|
+
collectorInfo: config.tallBoxes.collectorInfo ?? collectorInfo,
|
|
45
|
+
}
|
|
46
|
+
: undefined,
|
|
47
|
+
// Planeswalkers with two abilities swap `boxes` for these (see
|
|
48
|
+
// renderCardToCanvas), which would otherwise drop collector info.
|
|
49
|
+
twoAbilityBoxes: config.twoAbilityBoxes
|
|
50
|
+
? {
|
|
51
|
+
...config.twoAbilityBoxes,
|
|
52
|
+
collectorInfo: config.twoAbilityBoxes.collectorInfo ?? collectorInfo,
|
|
15
53
|
}
|
|
16
54
|
: undefined,
|
|
17
55
|
backBoxes: config.backBoxes
|
|
18
56
|
? {
|
|
19
57
|
...config.backBoxes,
|
|
20
|
-
collectorInfo: config.backBoxes.collectorInfo ??
|
|
58
|
+
collectorInfo: config.backBoxes.collectorInfo ?? collectorInfo,
|
|
21
59
|
}
|
|
22
60
|
: undefined,
|
|
23
61
|
sagaFrontBoxes: config.sagaFrontBoxes
|
|
24
62
|
? {
|
|
25
63
|
...config.sagaFrontBoxes,
|
|
26
|
-
collectorInfo: config.sagaFrontBoxes.collectorInfo ??
|
|
64
|
+
collectorInfo: config.sagaFrontBoxes.collectorInfo ?? collectorInfo,
|
|
27
65
|
}
|
|
28
66
|
: undefined,
|
|
29
67
|
sagaBackBoxes: config.sagaBackBoxes
|
|
30
68
|
? {
|
|
31
69
|
...config.sagaBackBoxes,
|
|
32
|
-
collectorInfo: config.sagaBackBoxes.collectorInfo ??
|
|
70
|
+
collectorInfo: config.sagaBackBoxes.collectorInfo ?? collectorInfo,
|
|
33
71
|
}
|
|
34
72
|
: undefined,
|
|
35
73
|
creatureBoxes: config.creatureBoxes
|
|
36
74
|
? {
|
|
37
75
|
...config.creatureBoxes,
|
|
38
|
-
collectorInfo: config.creatureBoxes.collectorInfo ??
|
|
76
|
+
collectorInfo: config.creatureBoxes.collectorInfo ?? collectorInfo,
|
|
39
77
|
}
|
|
40
78
|
: undefined,
|
|
41
79
|
backCreatureBoxes: config.backCreatureBoxes
|
|
42
80
|
? {
|
|
43
81
|
...config.backCreatureBoxes,
|
|
44
|
-
collectorInfo: config.backCreatureBoxes.collectorInfo ??
|
|
45
|
-
defaultCollectorInfoBounds,
|
|
82
|
+
collectorInfo: config.backCreatureBoxes.collectorInfo ?? collectorInfo,
|
|
46
83
|
}
|
|
47
84
|
: undefined,
|
|
48
85
|
sagaFrontCreatureBoxes: config.sagaFrontCreatureBoxes
|
|
49
86
|
? {
|
|
50
87
|
...config.sagaFrontCreatureBoxes,
|
|
51
|
-
collectorInfo: config.sagaFrontCreatureBoxes.collectorInfo ??
|
|
52
|
-
defaultCollectorInfoBounds,
|
|
88
|
+
collectorInfo: config.sagaFrontCreatureBoxes.collectorInfo ?? collectorInfo,
|
|
53
89
|
}
|
|
54
90
|
: undefined,
|
|
55
91
|
sagaBackCreatureBoxes: config.sagaBackCreatureBoxes
|
|
56
92
|
? {
|
|
57
93
|
...config.sagaBackCreatureBoxes,
|
|
58
|
-
collectorInfo: config.sagaBackCreatureBoxes.collectorInfo ??
|
|
59
|
-
defaultCollectorInfoBounds,
|
|
94
|
+
collectorInfo: config.sagaBackCreatureBoxes.collectorInfo ?? collectorInfo,
|
|
60
95
|
}
|
|
61
96
|
: undefined,
|
|
62
97
|
// Saga layouts swap `boxes` for `flipsidePtBoxes` on face 0 when the
|
|
@@ -65,7 +100,7 @@ export function withCollectorInfoDefaults(config, defaultCollectorInfoBounds) {
|
|
|
65
100
|
flipsidePtBoxes: config.flipsidePtBoxes
|
|
66
101
|
? {
|
|
67
102
|
...config.flipsidePtBoxes,
|
|
68
|
-
collectorInfo: config.flipsidePtBoxes.collectorInfo ??
|
|
103
|
+
collectorInfo: config.flipsidePtBoxes.collectorInfo ?? collectorInfo,
|
|
69
104
|
}
|
|
70
105
|
: undefined,
|
|
71
106
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OpacityStop } from "../schema/paint.js";
|
|
2
|
+
/**
|
|
3
|
+
* Opacity stops sampled from `curve`, which maps 0–1 along a gradient to an
|
|
4
|
+
* opacity 0–1 — for writing a gradient's `opacity` from a formula. The stops
|
|
5
|
+
* are spaced evenly, `count` of them (at least two). A canvas draws straight
|
|
6
|
+
* lines between them, so a tight curve wants more.
|
|
7
|
+
*/
|
|
8
|
+
export declare function opacityStopsFrom(curve: (t: number) => number, count?: number): OpacityStop[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const round = (value, places) => Number(value.toFixed(places));
|
|
2
|
+
/**
|
|
3
|
+
* Opacity stops sampled from `curve`, which maps 0–1 along a gradient to an
|
|
4
|
+
* opacity 0–1 — for writing a gradient's `opacity` from a formula. The stops
|
|
5
|
+
* are spaced evenly, `count` of them (at least two). A canvas draws straight
|
|
6
|
+
* lines between them, so a tight curve wants more.
|
|
7
|
+
*/
|
|
8
|
+
export function opacityStopsFrom(curve, count = 24) {
|
|
9
|
+
const n = Math.max(2, Math.round(count));
|
|
10
|
+
return Array.from({ length: n }, (_, i) => {
|
|
11
|
+
const at = i / (n - 1);
|
|
12
|
+
return {
|
|
13
|
+
at: round(at, 6),
|
|
14
|
+
opacity: round(Math.min(1, Math.max(0, curve(at))), 4),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -16,3 +16,4 @@ export { type FrameServer, loadFrameExport, startFrameServer, } from "./loadFram
|
|
|
16
16
|
export { type Problem, Reporter, type Severity, formatProblem, inGitHubActions, } from "./report.js";
|
|
17
17
|
export { SCHEMA_NAMES, SHIPPED_SCHEMAS, type SchemaName, schemaFilename, toJsonSchema, } from "./schema.js";
|
|
18
18
|
export { stableStringify } from "./stableJson.js";
|
|
19
|
+
export { type Pixels, type Rect, type TrimOptions, type TrimResult, type TrimmedImage, cropPixels, opaqueBounds, trimImages, unionBounds, } from "./trim.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -16,3 +16,4 @@ export { loadFrameExport, startFrameServer, } from "./loadFrame.js";
|
|
|
16
16
|
export { Reporter, formatProblem, inGitHubActions, } from "./report.js";
|
|
17
17
|
export { SCHEMA_NAMES, SHIPPED_SCHEMAS, schemaFilename, toJsonSchema, } from "./schema.js";
|
|
18
18
|
export { stableStringify } from "./stableJson.js";
|
|
19
|
+
export { cropPixels, opaqueBounds, trimImages, unionBounds, } from "./trim.js";
|
package/dist/cli/loadFrame.js
CHANGED
|
@@ -32,7 +32,7 @@ async function importVite() {
|
|
|
32
32
|
* loop would appear to run and do nothing.
|
|
33
33
|
*/
|
|
34
34
|
export function invalidateFrameModules(frameServer) {
|
|
35
|
-
//
|
|
35
|
+
// Every supported Vite major exposes the SSR module graph here; the legacy
|
|
36
36
|
// top-level accessor is deprecated.
|
|
37
37
|
frameServer.server.environments.ssr.moduleGraph.invalidateAll();
|
|
38
38
|
}
|
package/dist/cli/main.js
CHANGED
|
@@ -10,6 +10,7 @@ import { Reporter, inGitHubActions } from "./report.js";
|
|
|
10
10
|
import { SCHEMA_NAMES, toJsonSchema } from "./schema.js";
|
|
11
11
|
import { stableStringify } from "./stableJson.js";
|
|
12
12
|
import { runStudio } from "./studio/serve.js";
|
|
13
|
+
import { trimImages } from "./trim.js";
|
|
13
14
|
import { watchFrames } from "./watch.js";
|
|
14
15
|
const USAGE = `frame-kit — package Card Anvil frames
|
|
15
16
|
|
|
@@ -19,6 +20,8 @@ Usage:
|
|
|
19
20
|
frame-kit schema <name> [--out <file>] print a format as JSON Schema
|
|
20
21
|
(${SCHEMA_NAMES.join(" | ")})
|
|
21
22
|
frame-kit studio [options] open the visual box editor
|
|
23
|
+
frame-kit trim <png...> [options] cut images down to their visible
|
|
24
|
+
pixels, and say where they sit
|
|
22
25
|
frame-kit --help
|
|
23
26
|
frame-kit --version
|
|
24
27
|
|
|
@@ -37,6 +40,12 @@ build only:
|
|
|
37
40
|
in the index
|
|
38
41
|
--watch rebuild a frame whenever its source changes
|
|
39
42
|
|
|
43
|
+
trim only:
|
|
44
|
+
--shared cut every image to one rectangle holding all of their
|
|
45
|
+
visible pixels, so they keep one position on the sheet
|
|
46
|
+
--out <dir> write trimmed images here instead of replacing them
|
|
47
|
+
--dry-run say what would be cut, write nothing
|
|
48
|
+
|
|
40
49
|
studio only:
|
|
41
50
|
--port <n> port to serve on (default: 4620)
|
|
42
51
|
--host <h> interface to bind (default: 127.0.0.1)
|
|
@@ -45,6 +54,10 @@ studio only:
|
|
|
45
54
|
reloading (only inside the frames workspace)
|
|
46
55
|
--no-write serve read-only; refuse every edit
|
|
47
56
|
|
|
57
|
+
A trimmed image keeps its pixels exactly, and records where it sits on the
|
|
58
|
+
sheet so a second trim still reports that position. Put the position in the
|
|
59
|
+
frame where the field asks for one, such as a Saga's bannerOrigin.
|
|
60
|
+
|
|
48
61
|
The studio edits the literals in your source when you drag a box, so run it on
|
|
49
62
|
a clean working tree. It needs TypeScript installed to find them.
|
|
50
63
|
|
|
@@ -174,6 +187,62 @@ async function runBuild(root, only, values) {
|
|
|
174
187
|
}
|
|
175
188
|
return reporter.ok ? EXIT.ok : EXIT.failed;
|
|
176
189
|
}
|
|
190
|
+
function describeRect(rect) {
|
|
191
|
+
return `${String(rect.width)} × ${String(rect.height)} at (${String(rect.x)}, ${String(rect.y)})`;
|
|
192
|
+
}
|
|
193
|
+
async function runTrim(argv) {
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = parseArgs({
|
|
197
|
+
args: argv,
|
|
198
|
+
options: {
|
|
199
|
+
shared: { type: "boolean" },
|
|
200
|
+
out: { type: "string" },
|
|
201
|
+
"dry-run": { type: "boolean" },
|
|
202
|
+
json: { type: "boolean" },
|
|
203
|
+
},
|
|
204
|
+
allowPositionals: true,
|
|
205
|
+
strict: true,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch (cause) {
|
|
209
|
+
console.error(`${cause instanceof Error ? cause.message : String(cause)}\n\n${USAGE}`);
|
|
210
|
+
return EXIT.usage;
|
|
211
|
+
}
|
|
212
|
+
const { values, positionals: files } = parsed;
|
|
213
|
+
if (files.length === 0) {
|
|
214
|
+
console.error(`trim needs at least one PNG.\n\n${USAGE}`);
|
|
215
|
+
return EXIT.usage;
|
|
216
|
+
}
|
|
217
|
+
const result = await trimImages(files, {
|
|
218
|
+
shared: values.shared === true,
|
|
219
|
+
...(values.out !== undefined ? { outDir: values.out } : {}),
|
|
220
|
+
dryRun: values["dry-run"] === true,
|
|
221
|
+
});
|
|
222
|
+
if (values.json) {
|
|
223
|
+
console.log(JSON.stringify(result, null, 2));
|
|
224
|
+
return EXIT.ok;
|
|
225
|
+
}
|
|
226
|
+
for (const image of result.images) {
|
|
227
|
+
const from = `${String(image.width)} × ${String(image.height)}`;
|
|
228
|
+
const what = image.kept
|
|
229
|
+
? `${from} → ${describeRect(image.kept)}`
|
|
230
|
+
: `${from}, nothing visible, left as it is`;
|
|
231
|
+
const nothingToCut = image.kept?.width === image.width && image.kept.height === image.height;
|
|
232
|
+
const action = image.written
|
|
233
|
+
? ""
|
|
234
|
+
: nothingToCut
|
|
235
|
+
? " (already trimmed)"
|
|
236
|
+
: values["dry-run"] && image.kept
|
|
237
|
+
? " (dry run)"
|
|
238
|
+
: "";
|
|
239
|
+
console.log(`${image.file} ${what}${action}`);
|
|
240
|
+
}
|
|
241
|
+
if (result.shared) {
|
|
242
|
+
console.log(`\nEvery image was cut to ${describeRect(result.shared)} on the sheet.`);
|
|
243
|
+
}
|
|
244
|
+
return EXIT.ok;
|
|
245
|
+
}
|
|
177
246
|
function isSchemaName(name) {
|
|
178
247
|
return SCHEMA_NAMES.includes(name);
|
|
179
248
|
}
|
|
@@ -224,6 +293,9 @@ async function main(argv) {
|
|
|
224
293
|
if (command === "studio") {
|
|
225
294
|
return await runStudio(argv.slice(1));
|
|
226
295
|
}
|
|
296
|
+
if (command === "trim") {
|
|
297
|
+
return await runTrim(argv.slice(1));
|
|
298
|
+
}
|
|
227
299
|
if (command !== "validate" && command !== "build") {
|
|
228
300
|
console.error(`Unknown command "${command}".\n\n${USAGE}`);
|
|
229
301
|
return EXIT.usage;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CardBoxesSchema, LayoutConfigSchema } from "../../schema/frame.js";
|
|
1
|
+
import { CardBoxesSchema, LayoutConfigSchema, SecondFaceBoxesSchema, } from "../../schema/frame.js";
|
|
2
2
|
import { FrameAssetsSchema, LayoutMasksSchema, } from "../../schema/frameAssets.js";
|
|
3
3
|
/** Strips `.optional()` and friends to reach the schema that carries identity. */
|
|
4
4
|
function unwrap(schema) {
|
|
@@ -23,12 +23,16 @@ function unwrap(schema) {
|
|
|
23
23
|
}
|
|
24
24
|
const KIND_BY_SCHEMA = new Map([
|
|
25
25
|
[CardBoxesSchema, "boxes"],
|
|
26
|
+
// A second face's set is boxes like any other, only fewer of them.
|
|
27
|
+
[SecondFaceBoxesSchema, "boxes"],
|
|
26
28
|
[FrameAssetsSchema, "assets"],
|
|
27
29
|
[LayoutMasksSchema, "masks"],
|
|
28
30
|
]);
|
|
29
31
|
/**
|
|
30
|
-
* Whether a schema is
|
|
31
|
-
* (`planeswalkerConfig`, `sagaConfig`, `
|
|
32
|
+
* Whether a schema is an object of numbers — a layout's render knobs
|
|
33
|
+
* (`planeswalkerConfig`, `sagaConfig`, `sectionConfig`, `crownConfig`,
|
|
34
|
+
* `nicknameConfig`, `powerToughnessRow`). A group may also hold objects, such
|
|
35
|
+
* as `planeswalkerConfig.abilityDivider`; the studio edits only its numbers.
|
|
32
36
|
*
|
|
33
37
|
* Recognised by shape rather than by name, so a knob group added to the
|
|
34
38
|
* contract is editable in the studio without a change here. Records and enums
|
|
@@ -41,11 +45,10 @@ function isNumericObject(schema) {
|
|
|
41
45
|
return false;
|
|
42
46
|
}
|
|
43
47
|
const fields = Object.values(node.shape);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}));
|
|
48
|
+
const types = fields.map((field) => unwrap(field).inner.def.type);
|
|
49
|
+
const isKnob = (type) => type === "number" || type === "boolean";
|
|
50
|
+
return (types.some(isKnob) &&
|
|
51
|
+
types.every((type) => isKnob(type) || type === "object"));
|
|
49
52
|
}
|
|
50
53
|
/**
|
|
51
54
|
* Every box / asset / mask / knob slot the frame contract defines, read off
|
package/dist/cli/studio/serve.js
CHANGED
|
@@ -117,7 +117,10 @@ async function startUiDevServer(srcDir, httpServer) {
|
|
|
117
117
|
const server = await vite.createServer({
|
|
118
118
|
root: srcDir,
|
|
119
119
|
appType: "spa",
|
|
120
|
-
server
|
|
120
|
+
// `ws.server` is Vite 8's spelling (`hmr.server` is deprecated there, and
|
|
121
|
+
// Vite 7 has no `ws` options at all). Only the workspace runs --dev, and it
|
|
122
|
+
// pins Vite 8, so the published peer range never reaches this line.
|
|
123
|
+
server: { middlewareMode: true, ws: { server: httpServer } },
|
|
121
124
|
});
|
|
122
125
|
return {
|
|
123
126
|
middlewares: (req, res) => {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** A rectangle in pixels. */
|
|
2
|
+
export interface Rect {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
/** Decoded pixels, four bytes (RGBA) a pixel, row after row. */
|
|
9
|
+
export interface Pixels {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
data: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The smallest rectangle holding every pixel that is not fully transparent,
|
|
16
|
+
* or `undefined` when the image has none.
|
|
17
|
+
*/
|
|
18
|
+
export declare function opaqueBounds({ width, height, data, }: Pixels): Rect | undefined;
|
|
19
|
+
/** The smallest rectangle holding all of `rects`, or `undefined` for none. */
|
|
20
|
+
export declare function unionBounds(rects: readonly Rect[]): Rect | undefined;
|
|
21
|
+
/** `pixels` cut down to `rect`, which must lie inside them. */
|
|
22
|
+
export declare function cropPixels(pixels: Pixels, rect: Rect): Pixels;
|
|
23
|
+
export interface TrimOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Cut every image to one rectangle: the smallest holding all of their
|
|
26
|
+
* visible pixels. They must all be the same size, and they then share one
|
|
27
|
+
* position on the sheet — which is what images drawn together, such as a
|
|
28
|
+
* banner and the masks cut from it, need.
|
|
29
|
+
*/
|
|
30
|
+
shared?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Where trimmed images are written, under their own names. Without it each
|
|
33
|
+
* one replaces its original.
|
|
34
|
+
*/
|
|
35
|
+
outDir?: string;
|
|
36
|
+
/** Work everything out but write nothing. */
|
|
37
|
+
dryRun?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface TrimmedImage {
|
|
40
|
+
file: string;
|
|
41
|
+
/** Where it was written; `undefined` when nothing was. */
|
|
42
|
+
written: string | undefined;
|
|
43
|
+
/** Its size before trimming. */
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
/**
|
|
47
|
+
* The part kept, with `x` and `y` on the sheet. `undefined` when the image
|
|
48
|
+
* has no visible pixel, and is left as it was.
|
|
49
|
+
*/
|
|
50
|
+
kept: Rect | undefined;
|
|
51
|
+
}
|
|
52
|
+
export interface TrimResult {
|
|
53
|
+
images: TrimmedImage[];
|
|
54
|
+
/** With `shared`: the one rectangle every image was cut to, on the sheet. */
|
|
55
|
+
shared: Rect | undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Trims each PNG in `files` to its visible pixels, or with `shared` all of
|
|
59
|
+
* them to one rectangle, and reports where each now sits on its sheet. An
|
|
60
|
+
* image already as small as it can be is not rewritten.
|
|
61
|
+
*/
|
|
62
|
+
export declare function trimImages(files: readonly string[], options?: TrimOptions): Promise<TrimResult>;
|