- name
- resize
- description
- Use when resizing or reformatting a CoDesign / CE.SDK design to a new size or aspect ratio (ig-post ↔ ig-square ↔ ig-story ↔ widescreen, or a custom W×H); when building the size/format editions of a design from an existing design or from a brief; or when a reformatted edition has a dead void / letterboxed empty band, drifted or broken margins, off-canvas or clipped elements, mis-scaled decoration, or a stretched / squished layout. Also for animated / video designs: timed blocks, animations, captions, and audio carried into each edition.
# resize
Point this skill at the current design and one or more target formats, and it produces or repairs the size/format editions by **re-composing** each for its new aspect. It operates on the **latest revision of the current design** (re-ground via `list()` / `history()` if unsure — handbook §1); each produced format is a new revision (a non-destructive fork of the source). Single home for the resize workflow, the re-composition technique, and the reformat **no-goes**.
Builds on two things it does _not_ restate: handbook §6.3 Hierarchy + §6.4 Composition & grid (margins as % of the shortest dimension, the spacing ladder, balanced whitespace / no dead void, nothing off-canvas) and §6.1.2 type-scale bands; and the handbook's engine mechanics (the `text.font` declaration, auto-reshape, `loadResources`). For the _same-aspect, translate-the-strings_ case, use the `localize` skill instead.
**Scoring:** reformatting is judged with the `judge` skill — every applicable axis ≥ 8. A dead void, a stretched element, a broken margin, or anything off-canvas fails **composition**.
## Reference files (load on demand)
This `SKILL.md` holds the always-needed spine: inputs, the localize-vs-resize distinction, core
principle, workflow, the re-composition routine, the no-goes, and the **resize composition
checklist** (end of this file). Per-format data lives in the **`formats` skill** — exact W×H,
aspect, and platform safe zones, one file per format: `../formats/SKILL.md` for the index,
`../formats/<id>.md` for the format(s) you're building.
Building several editions of one design, or re-applying a fix to editions you already built: the
**master → editions** workflow, `editions.md`. For an animated or video design,
read **Video and animated designs** below as well.
## Localize vs resize — snap vs re-compose
**Format is the outer distinction: a different format is a different layout; a different language is the same layout with different strings.**
- **localize** — _same_ canvas: change words + the script's font, then **snap every block
back to the reference geometry**. Any geometry deviation is drift.
- **resize** — _new_ canvas: the geometry **must** change. You **re-derive** the layout
from the design system on the new canvas. Here "drift" is not "moved from the reference" — it's
re-composition that **loses the design's identity** or leaves the canvas **unbalanced**.
Never confuse the two: don't snap a resize (that reproduces the source's proportions on the wrong
canvas → a dead void), and don't re-compose a localization (that's how localized sets drift).
## Inputs — what to resize, and into what
Two inputs, resolved before any work by the intake contract — derive, ask once, echo, invent
nothing: `../handbook/intake.md`. **R** = required, no default; **A** =
ask if underived; **D** = defaultable.
| Parameter | Kind | Chips / values | Default |
| -------------- | ------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------- |
| target formats | **R** | `Instagram post` · `Instagram story` · `LinkedIn` · `Widescreen` · `A4 print` · `Custom W×H` · Other | none |
| source design | derived | — | latest revision of the current design |
**1. The source** — the latest revision of the current design. Every target format is re-composed _from_ that source — **never resize a resize** (don't chain: fork each format from the native source revision). If the user asks to resize an earlier design, find it via `list()` and its latest revision via `history()` first. If the request is a brief (no design exists yet), **build the native base first** at the brief's canvas per handbook §6, preview and sanity-check it, then treat that revision as the source.
**2. The formats** — named formats (`ig-post`, `ig-square`, `ig-story`, `widescreen`, and the
rest of the `formats` skill's index) or a custom `W×H`. Resolve each named format's exact size,
aspect, and safe zones from the **`formats` skill** (`../formats/<id>.md`).
If the format set is not stated, **ask** — _**Which sizes do you need?**_ — with the chips above;
a format set has no sensible default, so never pick one for the user. Then echo the resolved set
("1 design → 3 formats") before building.
## Core principle — re-compose for the aspect, preserve the system
A new format is a **new layout**. Re-lay it out for the new canvas while holding the **design
system** constant:
- **Invariant (carry to every format):** palette + the single accent; type-scale _bands_ (headline
/ body / caption as % of page height, §6.1.2); margin ratio (§6.4: 8–12% of the shortest side);
spacing scale (§6.4 ladder); hierarchy order (§6.3); the **group hierarchy** (carry the source's
grouping — re-group each cluster after re-composition, don't ship a flattened tree); the **region
of interest** of every image/video fill (the subject stays framed — see the ROI section below);
each decoration's _role_ (a ghost quote stays a ghost quote); the **element inventory** (every
content cluster on the source has a counterpart on the new canvas — see below); and the
**appearance** of every block (fill/gradient, effect/blur stack, opacity, corner radius, stroke,
shadow, and font weight — carried by forking, not re-created by eye).
- **Element inventory — nothing silently dropped.** A reformat re-composes; a tight format tempts
you to drop elements, and the result reads "way too simplistic / missing elements from the
original." Before finishing, **count**: every text cluster, image, chip, icon, and decoration on
the source must have a counterpart on the new canvas. An element may be **omitted only
deliberately** (it genuinely can't earn its place on a smaller canvas) — never by oversight, and
the omission is a judgment you can name. Re-composing from an _impression_ of the source instead
of its actual block list is how elements vanish.
- **Variable (re-derive per canvas):** absolute positions, absolute sizes, wrap width, line count,
and how the content group is distributed vertically.
Never **stretch or squish** the whole design to force-fit (that distorts type and shapes — resize
by re-flow, not by scaling the canvas contents). Never leave a **lopsided void** (§6.4).
## Workflow
1. **Read the source's design system** (palette, accent, type bands, margin %, spacing rhythm,
hierarchy) and **capture its layout as fractions** of the source canvas (positions/sizes as % of
source W/H — see routine).
2. **Fork the source** per target format; **capture the region of interest** of every image/video
fill first (ROI section below); **set the page** to the new W×H and resize the full-bleed
background to cover it; then **restore each fill's ROI** so the same region of the image stays
framed — never let a `Cover` fill silently re-center the subject on the new aspect.
3. **Re-derive from the new canvas:** margins = 8–12% of the new **shortest** dimension; type sizes
from the new page **height** within their bands (§6.1.2); gaps from the spacing scale (§6.4).
4. **Proportional re-flow to fill, then balance.** Place blocks by their captured fractions × the
new canvas (this fills a taller/wider canvas instead of letterboxing it), then **center or
distribute the content group within the platform safe band** — no top-anchored void. (Safe-zone
bounds and per-format guidance: the `formats` skill, `<id>.md`.)
5. **Scale decoration proportionally** to the canvas (a ghost glyph sized as % of page height grows
on a taller format, shrinks on a shorter one) — keep its role and framing.
6. **Re-wrap & re-fit:** text re-wraps at the new column width; check **overflow / clipping /
off-canvas**; adjust size _within its band_ only.
7. **Let the size/font write carry the rich text.** A `text.fontSize` / `text.font` /
`text.lineHeight` write reshapes on its own and **preserves every per-character style range** —
color, weight, style, case, decoration (handbook §6.1.3). A resize keeps the **same string**, so
there is nothing to capture and replay. What still flattens the accent is a **whole-block**
styling write: `getProps(id, ['text.color']).text.color` reads run 0 only, and writing it back as
`text: { color }` collapses the whole block to run 0's formatting. Don't re-apply whole-block
styling in a resize; if you must, pass the runs from `getProps(id, ['text.ranges'])` back as
`text: { ranges: [...] }` in the same `setProps` — **every** run, not just the accent.
8. **`loadResources` → `preview` each format** (dead void / crop / tofu only show in the
render; for a video design, at several `time`s — see below). Each format lands as its own revision — give each a `note` naming the format (e.g.
"ig-story 1080×1920").
## Proportional re-composition routine
The resize analog of localize's snap-to-reference — but a **first pass, not the finish**. It fills
the new canvas proportionally (killing the letterbox void); you then clamp margins/bands/safe-zones
and **judge the balance in `preview`**. Resize is judgment-driven; there is no single deterministic
answer.
**Capture the source as fractions** (run on the source page):
```js
const page = engine.scene.getPages()[0];
const pg = await engine.design.getProps(page, ['width', 'height']);
const PW = pg.width,
PH = pg.height;
const out = [];
async function walk(id) {
const p = await engine.design.getProps(id, [
'type',
'width',
'height',
'position'
]);
const r = {
type: p.type, // the SHORT name: 'text', 'graphic', 'page'
fx: p.position.x / PW,
fy: p.position.y / PH,
fw: p.width / PW,
fh: p.height / PH
};
if (p.type === 'text') {
const t = (await engine.design.getProps(id, ['text'])).text;
r.s = t.string;
// the size read carries its unit ('32px', always pixels) — parse for math
r.ffs = parseFloat(t.fontSize) / PH; // fraction of page height
r.lh = t.lineHeight;
r.ls = t.letterSpacing;
r.align = t.horizontalAlignment;
}
if (id !== page) out.push(r);
for (const c of await engine.design.getChildren(id)) await walk(c);
}
await walk(page);
return { type: 'text', text: JSON.stringify({ PW, PH, blocks: out }, null, 1) };
```
**Apply to a forked source at the new size** (first pass, then clamp):
```js
const NW = 1080,
NH = 1920; // target format
const SAFE_TOP = 250,
SAFE_BOT = 320; // ig-story/reel UI chrome (authoritative per-format values: formats skill, <id>.md); 0 for post/square/widescreen
const page = engine.scene.getPages()[0];
await engine.design.setProps(page, { width: NW, height: NH });
// full-bleed background → cover the new page (don't reposition it proportionally)
let bg;
for (const id of await engine.design.getChildren(page)) {
const p = await engine.design.getProps(id, ['type', 'width']);
if (p.type === 'graphic' && p.width >= NW * 0.9) {
bg = id;
break;
}
}
if (bg !== undefined) {
const roi = await captureFill(bg); // BEFORE the aspect change — ROI section
await engine.design.setProps(bg, {
position: { x: 0, y: 0 },
width: NW,
height: NH
});
await reapplyFill(bg, roi); // keep the same image region framed on the new aspect
}
// ...and captureFill/reapplyFill around EVERY other image/video-fill block you resize.
// content blocks → position/size by captured fractions (fill), then you clamp + balance below
// for each captured fraction f matched to its block id:
// await engine.design.setProps(id, { position: { x: f.fx*NW, y: f.fy*NH } }); // fills the taller canvas
// if (f.fw) await engine.design.setProps(id, { width: f.fw*NW });
// if (f.ffs) await engine.design.setProps(id, { text: { fontSize: clampToBand(f.ffs*NH) + 'pt' } });
// // the unit is required; the write auto-reshapes, ranges preserved
//
// THEN, not optional:
// • margins: ensure left/right within 8–12% of min(NW,NH); re-center centered blocks on NW.
// • safe band: shift the whole content group so nothing vital sits above SAFE_TOP or below NH-SAFE_BOT.
// • balance: if proportional spread looks airy/crowded, distribute the group evenly or center it —
// preview and adjust. Scale decoration with the canvas; keep type within §6.1.2 bands.
return {
type: 'text',
text: 'first pass placed — now clamp margins/safe-zone/balance and preview'
};
```
Platform safe zones and per-format notes are in the `formats` skill — read `<id>.md` for each
target format.
## Preserve the region of interest (image / video / pattern fills)
Re-flowing positions is only half the job. A block backed by an **image or video fill** shows a
_window_ onto its source, and that window is recomputed when the block's frame **aspect** changes:
a `Cover` fill re-centers, a `Crop` fill exposes gaps. Left alone, the resize crops the **subject**
out of frame — the face, product, logo, or focal point that made the asset worth using. That is a
composition **and** craft failure, and it only shows in `preview`.
**The ROI is defined by** (all on the block, via the `api` skill): `contentFill.mode`
(`Crop | Cover | Contain`); for the auto modes, `contentFill.horizontalAlignment` +
`contentFill.verticalAlignment` (`Left|Center|Right`, `Top|Center|Bottom`) — this is the focal side
a `Cover` fill keeps; and for `Crop`, `crop.scaleRatio` + `crop.translationX/Y` (translation in
**% of the crop frame**) + `crop.rotation`, which pin an explicit window.
**Capture before the resize, re-apply after** — around every fill-bearing block, not just the
background:
```js
async function captureFill(id) {
// image/video/pixel fills only
const caps = await engine.design.getCapabilities(id);
if (!caps.includes('contentFillMode')) return null;
const cf = (await engine.design.getProps(id, ['contentFill'])).contentFill;
const cap = {
mode: cf.mode,
hAlign: cf.horizontalAlignment,
vAlign: cf.verticalAlignment
};
if (cap.mode === 'Crop') {
const c = (await engine.design.getProps(id, ['crop'])).crop;
cap.crop = {
sr: c.scaleRatio,
tx: c.translationX,
ty: c.translationY,
rot: c.rotation
View on GitHub