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, c...
Installs into .claude/skills of the current project.
Are you the author of Resize?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/imgly-resize)
---
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
};
}
return cap;
}
async function reapplyFill(id, cap) {
if (!cap) return;
await engine.design.setProps(id, {
contentFill: {
mode: cap.mode,
horizontalAlignment: cap.hAlign, // ROI focus for Cover/Contain
verticalAlignment: cap.vAlign
}
});
if (cap.mode === 'Crop' && cap.crop) {
const c = cap.crop;
await engine.design.setProps(id, {
crop: { rotation: c.rot, translationX: c.tx, translationY: c.ty }
});
await engine.design.adjustCropToFillFrame(id, c.sr); // refill gaps, keep scale ≥ source
}
}
```
**Order matters:** `captureFill` **before** you write the block's `width`/`height`, `reapplyFill`
**after**. `setProps(id, { crop: null })` forces the mode back to `Cover`, so never call it in a resize
path. Default to keeping the ROI **centered**; when the brief or the asset's metadata names a focal point, bias
`alignment` (Cover) or `translation` (Crop) toward it. `preview` each format and confirm the subject
is still framed — the render is the only place ROI drift shows.
**Crop invariants — the three properties every fill-in-a-frame must hold** (checked in `preview`;
they fail independently of ROI focus, and a shape frame — circle/rounded card — makes them easy to
miss):
1. **Full coverage, zero gap.** The fill covers its frame completely — no sliver of empty frame at
any edge (the "gap at the top of the circle" defect). If the aspect change exposed a gap,
`adjustCropToFillFrame` (Crop) or the Cover mode refills it; never leave the frame partly unfilled.
2. **Uniform scale — no stretch.** The image scales the **same on both axes**; a fill stretched to
fit a new aspect (`Contain` into a non-matching frame, or a hand-set non-uniform crop) distorts
the subject. Scale is one ratio, not two.
3. **Subject inside the window.** After 1–2, the focal point is still within the visible crop, not
pushed out by the refill. This is the ROI check above — do it last, after coverage is guaranteed.
**Re-group after re-composition.** Resize moves and resizes loose blocks; before you finish, restore
the source's **group hierarchy** on the new canvas (handbook §4 "Group blocks & hierarchy") so each
format ships the same grouped, nested structure — not a flattened tree.
## Video and animated designs
Applies when the page has a `playback.duration`, blocks carry `playback.timeOffset` /
`playback.duration`, `animations`, an `audio` block, or a `captionTrack`. A resize re-composes the
**frame**; the **timeline** is invariant.
- **Fork, move, resize — never recreate a timed block.** A page resize keeps every block's
`timeOffset`, `duration`, animations, audio and video trims; a re-created block loses all of them.
Walk the whole tree, not just the page's children: timed blocks can sit under `track`s and
captions under a `captionTrack` (the capture `walk` above already recurses).
- **Re-aim every entrance for the new layout.** `slide` / `pan` `direction` is the **direction of
travel** in radians, y pointing down: `0` enters from the left moving right, `π/2` from the top
moving down, `π` from the right, `3π/2` from the bottom moving up. A slide that entered from the
photo's side on 16:9 crosses the photo once 9:16 stacks text under it — aim it along the new
reading axis. Patch in place with the **same type**, which keeps the animation's duration:
`setProps(id, { animations: { in: { type: 'slide', slide: { direction: (3 * Math.PI) / 2 } } } })`
(a different type replaces the animation and resets its duration). The duration itself is not in
the spec: `engine.block.setDuration(engine.block.getInAnimation(id), s) // engine.block: animation duration`.
- **Scaling entrances overdraw.** `zoom` / `grow` scale around the block's centre, so a
full-height photo spills over its neighbours during the entrance — check the first frames after
every re-layout.
- **Vertical safe zones hold for the whole timeline.** Story / reel / TikTok / Shorts UI covers the
top and bottom bands (and the right edge on TikTok / Shorts) — `../formats/ig-story.md`,
`../formats/tiktok.md`, `../formats/yt-shorts.md`. Captions and CTAs belong inside
the safe band, and so does every block's position _mid-entrance_.
- **Captions carry their own frame, per caption.** Writing the first caption's position or size
does not reach the others — set `position`, `width`, `height`, `caption/fontSize` on **every**
caption, with `caption/automaticFontSizeEnabled` off (otherwise the text grows past its frame
and over the layout). Keep the frame's bottom at or above `H − bottom safe band`.
- **A fill's region of interest moves over time.** Capture / re-apply the ROI as above, then check
it at the start, middle and end of every video clip and of every `ken_burns` / `pan` move — a
crop that frames the subject at 0 s can cut it at 3 s.
- **Preview at several `time`s, per edition.** One frame of a video proves nothing about the
rest: `preview({ revision, time })` at the first frame, mid-entrance of each timed block, each
hold, each caption change and the last second. Then `export({ format: 'mp4', blockId: page })`
for the real motion. `preview` accepts `time` on a Video-mode scene only; if it answers "not a
video scene", switch it once — `engine.scene.setMode('Video')`, move the page under the scene
(`engine.design.appendChild(engine.scene.get(), page)`) and destroy the now-empty `stack`.
## No-goes / red flags
- **Dead void / letterboxing** — the headline failure: content clustered on one side of a
taller/wider canvas, an empty band elsewhere. Fill or balance the group; respect safe zones.
- **Design drift** — re-composition that loses the design's identity: margins wander, the type
scale or hierarchy shifts, the accent is misused, or decoration loses its role. Re-compose
_within_ the system, don't redesign.
- **Stretch / squish** — scaling the canvas contents non-proportionally to force-fit. Never distort
type or shapes; re-flow instead.
- **Broken margin rhythm** — margins not re-derived from the new shortest dimension, or asymmetric.
- **Off-canvas / clipping / overflow** — elements pushed past the new edges; decoration over-cropped;
text overflowing the new column.
- **Decoration mis-scale** — a ghost glyph or accent kept at the source's absolute size instead of
scaling with the canvas.
- **ROI drift** — a resized image/video fill re-centered and cropped the subject out of frame
(face, product, logo, focal point). Capture and re-apply the fill's region of interest.
- **Missing elements** — the reformat re-composed from an impression and silently dropped source
content ("too simplistic / elements missing from the original"). Inventory every source cluster
against the new canvas; omit only deliberately, never by oversight.
- **Appearance drift** — a block's fill/gradient/effect/corner-radius/overlay differs from the
source, or an overlay was added/removed the source didn't have (a cover that "became darker").
Appearance is inherited by forking; don't re-create or restyle it by eye.
- **Crop broken** — a fill leaves a gap inside its frame, or was stretched non-uniformly. See the
three crop invariants (coverage, uniform scale, subject-in-window).
- **Flattened hierarchy** — the resize dropped the source's group blocks and shipped a flat list of
siblings. Re-group each cluster (nested where nested) on the new canvas.
- **Ignoring platform safe zones** — vital content under story/reel UI chrome.
- **Timeline drift (video)** — a timed block re-created instead of moved, an entrance still aimed
across the old layout, or a caption left in the platform's UI band.
- **tofu / overflow at new wrap widths** — a new measure can push a glyph the font lacks, or wrap to
a line count that overflows; only `preview` catches it.
## Before reporting done
Run the **resize composition checklist** below against each format's render, then close with a `judge` run per format (the `judge` skill; composition and hierarchy are the axes reformatting most often breaks). Score honestly — a residual void is a composition fail, say so.
### Resize composition checklist
Any "no" on an applicable line drops the axis below 8.
**Composition (§6.4)**
- [ ] Margins 8–12% of the new shortest dimension; on the shared grid / one alignment
- [ ] Whitespace balanced — **no lopsided dead void / letterbox**; content occupies or balances the canvas
- [ ] Consistent spacing scale (§6.4 ladder); nothing off-canvas, clipped, or overlapping
- [ ] Primary content within the format's safe band (per its `formats` file; story/reel: clear of top ~250px / bottom ~320px)
**Hierarchy (§6.3) & identity**
- [ ] One dominant element preserved; reading order intact; ≈3 levels
- [ ] Palette + single accent, type-scale bands (§6.1.2), and each decoration's role carried over
- [ ] Decoration scaled proportionally to the canvas (not the source's absolute size)
- [ ] Every image/video fill keeps its **region of interest** — subject/focal point still framed, nothing important cropped out or newly empty (checked in `preview`)
- [ ] Source's **group hierarchy** restored on the new canvas (grouped clusters, nested where nested — not a flat list)
- [ ] **Element inventory complete** — every source content cluster (text, image, chip, icon, decoration) has a counterpart on the new canvas; any omission is deliberate and nameable, not an oversight
- [ ] **Appearance parity** — every block's fill/gradient, effect/blur, opacity, corner radius, stroke, shadow, and font weight match the source; no overlay added or removed the source didn't have (checked against the source render)
**Craft / baseline**
- [ ] No stretch/squish; type re-fit within its band, not distorted
- [ ] **Crop invariants** hold on every fill-in-frame — full coverage (no gap), uniform scale (no stretch), subject inside the window (checked in `preview`)
- [ ] Zero tofu / overflow at the new wrap width, checked in `preview`
- [ ] Native export dimensions (the page's own pixel size)
**Video / animated (when the design has a timeline)**
- [ ] Every block's `timeOffset` / `duration`, animations and audio carried over — moved, not re-created
- [ ] Every entrance re-aimed for the new layout (`direction` = direction of travel); no block crosses the photo or leaves the safe band mid-entrance
- [ ] Every caption framed and sized on the new canvas, inside the safe band, auto font size off
- [ ] Checked at several `time`s (entrances, holds, caption changes, last second), not one frame
## Cross-references
- **`formats` skill** — per-format W×H, aspect, and platform safe zones (one file per format id).
- handbook §6.3 Hierarchy, §6.4 Composition & grid, §6.1.2 type-scale bands — the general composition criteria this skill operationalizes; §6.1.2's canvas table for type sizing by % of page height; and the handbook's engine mechanics (the `text.font` declaration, auto-reshape, `loadResources`, dirty-on-throw).
- **`judge`** — the acceptance gate every produced format must pass.
- **`localize`** — the sibling for _translating_ a design at the _same_ size (snap, don't re-compose).