@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/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,100 @@ 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
|
+
|
|
182
|
+
### `frame-kit studio`
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
frame-kit studio [--root <dir>] [--port <n>] [--frame <slug>] [--no-write]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
A visual box editor, served from `dist/studio` and opened at
|
|
189
|
+
<http://localhost:4620>. It loads frames through the same Vite SSR loader
|
|
190
|
+
`validate` uses, so what it draws is the validated frame, and it reloads when a
|
|
191
|
+
source file changes.
|
|
192
|
+
|
|
193
|
+
The card name and type line are previewed in Beleren at the authored size and
|
|
194
|
+
flagged when they overflow — one line each, with no wrapping, symbols or
|
|
195
|
+
auto-shrink, so the app remains the authority on a full render.
|
|
196
|
+
|
|
197
|
+
Working on the studio's own UI: `--dev` serves it from source with hot
|
|
198
|
+
reloading, on the same port as the API, so one command covers both. It needs
|
|
199
|
+
the UI's source, which only exists inside the frames workspace.
|
|
200
|
+
|
|
201
|
+
Dragging a box rewrites the literal it came from, in place — nothing is
|
|
202
|
+
reprinted, so comments and formatting survive. Editing needs `typescript`
|
|
203
|
+
installed (an optional peer); without it the studio still runs read-only. Values
|
|
204
|
+
that are not written as literals are reported as such rather than edited.
|
|
205
|
+
|
|
206
|
+
### `@cardanvil/frame-kit/layout`
|
|
207
|
+
|
|
208
|
+
How a frame is drawn for one card, as pure functions:
|
|
209
|
+
|
|
210
|
+
- `selectFrameLayers(details, options)` — the base frame and section overlays a
|
|
211
|
+
set of `FrameDetails` selects. Split out of Card Anvil's `getFrameLayers`, so
|
|
212
|
+
the studio and the renderer cannot drift on two-colour splits, hybrid halves
|
|
213
|
+
or the coloured-artifact fallback.
|
|
214
|
+
- `frameDetailsFor(shape)` — those details from a plain description of a card
|
|
215
|
+
(colours plus artifact / vehicle / land / enchantment / hybrid / devoid),
|
|
216
|
+
for callers with no Scryfall card to parse.
|
|
217
|
+
- `placeAsset(path, config)` — where a piece of art is drawn. Most frame art is
|
|
218
|
+
full-sheet and centres, but crowns, nicknames and PT plates come from the
|
|
219
|
+
layout's own render config; centring those puts a legendary crown in the
|
|
220
|
+
middle of the card.
|
|
221
|
+
- `resolveFrameAsset` / `resolveBaseFrame` — exact per-family lookup, and the
|
|
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.
|
|
247
|
+
|
|
248
|
+
Card Anvil still owns the other half — reading a Scryfall card into
|
|
249
|
+
`FrameDetails` — because that is where the Scryfall types live.
|
|
250
|
+
|
|
132
251
|
## Type it, then validate it
|
|
133
252
|
|
|
134
253
|
Typing a config as `Frame` is weaker than the schema a loading app enforces. The clearest example:
|
|
@@ -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
|
@@ -9,6 +9,8 @@ import { PackagingError } from "./errors.js";
|
|
|
9
9
|
import { Reporter, inGitHubActions } from "./report.js";
|
|
10
10
|
import { SCHEMA_NAMES, toJsonSchema } from "./schema.js";
|
|
11
11
|
import { stableStringify } from "./stableJson.js";
|
|
12
|
+
import { runStudio } from "./studio/serve.js";
|
|
13
|
+
import { trimImages } from "./trim.js";
|
|
12
14
|
import { watchFrames } from "./watch.js";
|
|
13
15
|
const USAGE = `frame-kit — package Card Anvil frames
|
|
14
16
|
|
|
@@ -17,6 +19,9 @@ Usage:
|
|
|
17
19
|
frame-kit build --out <dir> [options] check every frame, then pack each one
|
|
18
20
|
frame-kit schema <name> [--out <file>] print a format as JSON Schema
|
|
19
21
|
(${SCHEMA_NAMES.join(" | ")})
|
|
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
|
|
20
25
|
frame-kit --help
|
|
21
26
|
frame-kit --version
|
|
22
27
|
|
|
@@ -35,6 +40,27 @@ build only:
|
|
|
35
40
|
in the index
|
|
36
41
|
--watch rebuild a frame whenever its source changes
|
|
37
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
|
+
|
|
49
|
+
studio only:
|
|
50
|
+
--port <n> port to serve on (default: 4620)
|
|
51
|
+
--host <h> interface to bind (default: 127.0.0.1)
|
|
52
|
+
--open open the studio in your browser
|
|
53
|
+
--dev serve the studio's own UI from source, with hot
|
|
54
|
+
reloading (only inside the frames workspace)
|
|
55
|
+
--no-write serve read-only; refuse every edit
|
|
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
|
+
|
|
61
|
+
The studio edits the literals in your source when you drag a box, so run it on
|
|
62
|
+
a clean working tree. It needs TypeScript installed to find them.
|
|
63
|
+
|
|
38
64
|
A frame is any directory containing a frame.meta.json. One with "private": true
|
|
39
65
|
is checked by validate but never built.
|
|
40
66
|
`;
|
|
@@ -161,6 +187,62 @@ async function runBuild(root, only, values) {
|
|
|
161
187
|
}
|
|
162
188
|
return reporter.ok ? EXIT.ok : EXIT.failed;
|
|
163
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
|
+
}
|
|
164
246
|
function isSchemaName(name) {
|
|
165
247
|
return SCHEMA_NAMES.includes(name);
|
|
166
248
|
}
|
|
@@ -208,6 +290,12 @@ async function main(argv) {
|
|
|
208
290
|
if (command === "schema") {
|
|
209
291
|
return await runSchema(argv.slice(1));
|
|
210
292
|
}
|
|
293
|
+
if (command === "studio") {
|
|
294
|
+
return await runStudio(argv.slice(1));
|
|
295
|
+
}
|
|
296
|
+
if (command === "trim") {
|
|
297
|
+
return await runTrim(argv.slice(1));
|
|
298
|
+
}
|
|
211
299
|
if (command !== "validate" && command !== "build") {
|
|
212
300
|
console.error(`Unknown command "${command}".\n\n${USAGE}`);
|
|
213
301
|
return EXIT.usage;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long to wait for changes to stop before reacting.
|
|
3
|
+
*
|
|
4
|
+
* One save can produce several events — a write, a rename, an attribute
|
|
5
|
+
* change — and a tool upstream may rewrite a whole directory.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEBOUNCE_MS = 200;
|
|
8
|
+
/** Whether a path is build output, editor debris, or inside a skipped dir. */
|
|
9
|
+
export declare function isIgnored(file: string): boolean;
|
|
10
|
+
export interface SourceWatcherOptions {
|
|
11
|
+
/** Absolute path the watch is rooted at. Only used for error messages. */
|
|
12
|
+
root: string;
|
|
13
|
+
/** Directories to watch recursively. Overlapping entries are fine. */
|
|
14
|
+
dirs: readonly string[];
|
|
15
|
+
/**
|
|
16
|
+
* Called with each batch of changed paths, debounced and never overlapping.
|
|
17
|
+
*
|
|
18
|
+
* Anything that arrives while a call is in flight is queued and delivered in
|
|
19
|
+
* the next batch, so a slow handler cannot drop a change.
|
|
20
|
+
*/
|
|
21
|
+
onChange: (changed: readonly string[]) => void | Promise<void>;
|
|
22
|
+
/** Ignored on top of the built-in list — a build's own output directory, say. */
|
|
23
|
+
ignore?: (file: string) => boolean;
|
|
24
|
+
/** Defaults to `DEBOUNCE_MS`. */
|
|
25
|
+
debounceMs?: number;
|
|
26
|
+
/** Where an unwatchable directory or a throwing handler is reported. */
|
|
27
|
+
onError?: (message: string) => void;
|
|
28
|
+
}
|
|
29
|
+
export interface SourceWatcher {
|
|
30
|
+
close: () => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Watches directories and delivers debounced, non-overlapping change batches.
|
|
34
|
+
*
|
|
35
|
+
* Shared by `frame-kit build --watch` and `frame-kit studio`, which need the
|
|
36
|
+
* same three things and get them wrong in the same three ways otherwise:
|
|
37
|
+
* coalescing a burst of events from one save, never running two handlers at
|
|
38
|
+
* once, and surviving a handler that throws.
|
|
39
|
+
*/
|
|
40
|
+
export declare function startSourceWatcher(options: SourceWatcherOptions): SourceWatcher;
|