Illustration Framed Panel
Meng ToCommunityCreated by Meng To
- Skill
- Content generator
- Design and creative
- MIT
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill produces original SVG illustrations in a specific framed-panel cartoon style. Each scene features a semi-real character engaged in design or coding tasks, set within a violet panel on a pastel card. The style is defined by thin navy outlines, specific pastel color palettes, and elements like UI cards or speech bubbles that break out over the frame edge to create a dynamic composition.
What it does
- Creates 480x360 SVG illustrations of characters working with laptops, screens, and UI elements.
- Applies a consistent framed-panel aesthetic with navy outlines and pastel fills.
- Incorporates breakout elements like speech bubbles and UI cards that extend beyond the frame.
- Provides a structured workflow for generating, linting, and rendering the SVG cards.
- Includes a kit for consistent character features, poses, and props.
- Ensures technical consistency through a provided linting and rendering toolset.
When to use it
- When you need custom illustrations for blog headers, feature spots, or marketing cards.
- For creating empty-state, onboarding, or 404 page artwork.
- When designing visuals for support, about, or team pages.
- When a consistent, outlined, pastel-violet cartoon style is required for product documentation or UI.
Advantages
- Maintains a high level of visual consistency across multiple illustrations.
- Provides a clear, deterministic workflow for generating complex SVG scenes.
- Includes built-in validation to ensure style adherence and technical correctness.
- Offers a modular approach to character and prop placement.
Usage examples
- “Generate a framed panel illustration of a developer sitting on a beanbag and typing on a laptop.”
- An SVG file featuring a character in the specified pose, with breakout UI elements and the correct pastel color scheme.
- “Create a 404 page illustration showing a person kneeling and pressing a button on a component board.”
- An SVG illustration depicting the kneeling pose, grid board, and breakout UI cards consistent with the framed-panel style.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/mengto/illustration-framed-panel/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip illustration-framed-panel.zip -d .agents/skills
Documentation by Meng To
Illustration: Framed panel
A cartoon person at work inside a violet "window", painted on a pastel card. The panel gives the scene a stage, and the UI bubbles and bushes that spill over its edge make it feel lively rather than boxed. Use it for friendly product and team moments: support, live streams, design work, onboarding.
Boundary: for people without a frame on a plain white background with inked outlines and big heads, use illustration-outlined-cartoon. For flat shapes with no outlines and floating confetti, use illustration-flat. For a single object with no person, use illustration-teal-spot.
Extracted from a set of 42 original design-and-coding cards in 14 styles; the three cards in examples/ are this style's shipped cards.
The look in one sentence
A fixed violet panel with a thin navy frame sits on a pastel card, with outline clouds and a dashed lavender floor inside it, and the scene's white bubbles, UI cards and leaves break out across its edge.
Palette
Measured from the examples. Every card uses the navy line, the panel set and the bubble set; pick one card pastel.
| Role | Hex | Where it goes |
|---|---|---|
| Card: peach / orchid / sky cyan | #F7D8B5 / #DFA5D4 / #A7E0ED | full-bleed card, one per card |
| Bracket and outside marks | #9C878C / #A3739B / #6E9FAD | brackets on the matching pastel |
| Navy line | #2E2A5C | every outline, clouds, hatch marks, eyes, bold UI text |
| Panel | #8A80EA | panel fill (also a UI accent: slider, swatch) |
| Floor band / floor dashes | #D0CCF4 / #8F88D6 | floor rect y 256–312, its short dashes |
| Contact shadow | #B5AEEA | flat ellipses under feet, chairs, beanbags |
| Bushes | #BACCA6, #A9C094 front layer | scalloped bushes on the floor line |
| Hair / hair highlight | #433D71 / #7C74C4 | all hair; highlight strokes; folds on dark fabric |
| Skin (three tones) | #EDB0A2, #F0BFA4, #D49A80 | face, neck, hands |
| Blush | #E8878A at opacity 0.5–0.6 (on #D49A80 skin: #C46A5C at 0.55) | cheek ellipses, tongue line |
| Mouth | #9A3E52 (#8A3A48 on tan skin) | open smile |
| Tops | mustard #EDB24F (cuff #E0A23E), mint #95D1B3 (shade #79BC9C), orange #EE8A6E (shade #D9735A) | sweaters, hoodies, cardigans |
| Trousers | #6A63B0 (folds #A29DDA), #4A4488 (folds #7C74C4) | jeans |
| Cream / white | #FCF6EA, #FCFAF3 / #FFFFFF | tees, socks, shoe uppers, code windows / bubbles and UI cards |
| Red accent / its shadow | #E35D5B / #B8434A | shoe soles, tags, buttons, heart, errors |
| Pale red, pink | #E9A3A0, #F8CCC8, #F4E0E8, #F29CA0 | secondary UI lines, error rows, drop slots, code tokens |
| Green / dark green / pale green | #5FA77A / #3E8E5E / #CDEBD6 | check badges, toggles, success rows |
| Mint, sage | #8FD0B0, #6FB585, #79C2A8 | icon chips, image placeholders, avatar shirts |
| Yellow, orange | #EDB24F, #F09A5E | swatches, earrings, highlighted code |
| UI greys | #BDB8D6 lines, #6E6A8E / #7D79A0 dark lines, #C9C5DE raised-shadow, #CFCBE0 line numbers | text placeholders inside bubbles |
| Devices | #C9C7DA body, #9F9CB8 / #B7B5CC / #A9A7C0 edges, #DCDAE8 keyboard, #A9A6C2 keys, #3A3570 screen, #524C98 active line, #6E68AA, #E8E6F8, #BDB8F0 code on dark | laptops, monitors |
| Furniture | #C9C5E8 + #A9A4D8 seams, #B9B4DE, #D8D3F0 grid, desk #F6E7D2 top / #E3B98E edge, #FFF4D6 lamp glow | chairs, drawers, boards, desks |
| One-off prop tint | e.g. plum #B2679E (the Support beanbag) | one large prop per card may take its own muted tint; lint will WARN on it |
| Avatars | #FBE3CC background, #F1B9A2 skin, #D9774E ginger hair | small faces inside bubbles |
No gradients, filters or patterns. Opacity only on blush ellipses (0.5–0.6) and on glasses lenses (
fill-opacity0.18).Never put the panel violet behind a violet top or violet trousers without a navy outline between them; the figure has to separate from the panel by value (mustard, mint, orange, cream all do).
The card pastel never appears inside the panel, and the panel violet never fills the card.
Line and fill
One outline colour, navy
#2E2A5C, always a stroke over a flat fill, round caps and joins.Widths at 480 wide: panel outline 1.35; body, clothes, furniture, bubbles 1.25; hands, shoes, bushes, neck 1.05–1.15; clouds and floor line 1.2; fold curves 0.9–1.1; hatch pairs and inner ear 0.85–0.9; finger separations 0.75–0.8; corner brackets 1.25 in the bracket colour.
Texture is drawn, never filtered: pairs of short parallel slashes (each 3–4 long, the second 1 right and 5 lower, slanting up-right, navy 0.9) scattered 2–4 pairs per garment; a few longer fold curves; on dark trousers and hair, folds and highlights in the lighter tint (
#A29DDA,#7C74C4) at 1.0–1.2.Shading is a flat darker tone with no outline, laid inside a shape along one side (
#D9735Apanels inside an#EE8A6Ehoodie;#79BC9Ccuffs on a mint sweater).Raised UI parts (buttons, toggles, tags, chips) have a darker copy 3 px straight below (
#B8434Aunder red,#3E8E5Eunder green,#C9C5DEunder white).Every white bubble and card gets one short inner corner tick (
Qcurve, 1.0) 5–7 px inside a top corner: the "sketchy" hand.Text:
ui-sans-serif, system-ui, sans-serif, weight 700–800, 10–16 px, navy or white. At most three words per card ("Fixed it!", "+1", "LIVE", "Aa").
Characters
One figure per card, seated or kneeling: the panel interior is 266 tall and every example figure spans 195–215 px from hair top to floor. A standing adult (about 5.5 heads) does not fit; crouch, sit, kneel or crop at a desk.
Head: use
face()fromscripts/panel-kit.mjs; it is the same face in all three examples. 3/4 view, 36 wide and 46 tall (brow to chin), chin at (232, 166.5) before your transform. Near eye a navy ellipse rx 1.8 ry 2.1 with an upper-lid arc and a lash flick to the outside; far eye smaller (rx 1.5 ry 1.9); arched brows 1.2; a single hook nose on the far side of the face; an open smile filled#9A3E52with a pink tongue line; blush ellipse 7.6 × 4.2 under the near eye and a small one at the far cheek edge; ear with one inner curve; a gold earring.Placing the head: wrap face, neck and hair in one
<g transform="translate(dx dy) rotate(a 232 166)">; tilt 4–8° into the action. Mirror withscale(-1 1)to look left. Never scale the head: the line weight would change.Hair: navy-violet
#433D71masses with 1.25 outline: a back mass drawn before the face and a fringe or cap after it. Add 3–5 highlight strokes#7C74C4(1.1–1.2) following the hair direction and 1–2 navy inner strokes (0.9–1.0). The examples show a bob with a side fringe, a high bun with a scrunchie, and a curly crop.Body: torso 60–70 wide, soft trapezoid, sleeves as separate shapes overlapping the torso. Necklines and hems drawn as a second line 3–4 px inside (ribbed hem: a line plus short vertical ticks every 6–7 px). Cardigans have 2 buttons (r 1.5); hoodies have cream drawstrings (1.6) with tiny rectangular tips.
Hands: about 22–26 px long, a little shorter than the face. Built as cuff band (the sleeve's shade tone, 5–6 wide, one centre line at 0.8) → forearm in skin → wrist → palm → fingers separated by short inner curves at 0.75–1.0, knuckle bumps on the outline. Thumbs are a separate small shape. Templates:
typeHand()(flat or typing, back of hand),pointHand()+pointCuff()(index extended, others curled, thumb across),gripHand()(a right fist round a handle, back to the viewer, thumb over the top on the business end) andkneeHand()(the far arm of a right-facing seated figure, hand palm-down over a knee). Before placing any of them, decide left or right, palm or back, finger direction, and check the thumb side with the table inreferences/craft.md; mirror the template withscale(1 -1)when the table says the thumb belongs on the other edge.Legs and feet: jeans taper from hip to ankle with a knee bend; when both legs overlap, give the far leg the darker jean
#4A4488(folds#7C74C4) behind a#6A63B0near leg; hems as a single line; a sock or bare ankle 2–3 px; white sneakers with a 3-px red sole (sneaker()), a toes-tucked shoe for kneeling (tuckedShoe()). Shoes always touch the floor band or a footrest, with a contact ellipse under them.Poses that work: cross-legged on a beanbag leaning into a laptop (the body rotated −5° about the hips), kneeling on one knee while pressing a button into a board, seated on a chair pointing at a monitor while looking back at the viewer. Each has a lean, a twist or a bent knee.
Decor and props
Inside the panel: 2 cloud doodles (a big one ~50 wide and a mid one ~44 wide, each with a 3-px dash to its right), optionally a small one (~17 wide), plus 2–4 tiny single-arc ticks (~9 wide), all outline only, in the sky area and never behind the head; one wall prop at most (a code window, a ring light, a grid board).
Floor: the band from y 256 to 312 with a navy line at 256 and 6–7 dashes (10–26 long, some followed by a 4-px dash after a 5-px gap) near its top corners and bottom edge.
Bushes: 1–2, standing on the floor line at a bottom corner; at least one crosses the panel's side edge.
Breakouts: exactly 2 white UI pieces (speech bubble, UI card, type specimen) that cross the frame: one upper-left across the left edge (x about 62–166, y 58–134), one on the right across the right edge (x about 336–436, y 150–250). Optional third: a small red tag across the top-left corner. Each holds an icon or avatar plus 2–4 text lines; no paragraphs.
Outside the panel: the two corner brackets (top-right and bottom-left, fixed coordinates) and 2–3 parenthesis marks: a double arc beside one side, a single arc beside the other, in the bracket colour. 0–2 navy sparkle ticks (three short radiating strokes,
ticks()) beside a breakout.Never: confetti, stars, gradients, drop shadows, a second figure, a frame in any colour other than navy, decor in the card margins beyond the brackets and marks.
Composition
Card: square full-bleed rect, no
rx. Panel:x 110 y 46 w 262 h 266(55 % of the width, 74 % of the height), fill#8A80EA, navy outline 1.35, identical in every card.Draw order: card → brackets and marks → panel fill → clouds → wall props → floor band, line, dashes → props wholly inside the panel → panel outline → bushes that cross the edge → furniture and figure → breakout bubbles and cards → sparkle ticks. Anything that crosses the frame is drawn after the outline so it sits on top of it.
The figure's centre line sits at x 205–235, slightly left of the panel centre (241), facing the prop on the right; the head is in the upper half of the panel with 30–60 px of sky above the hair.
Weight: the left breakout high, the right breakout at mid height, the bushes low at the corners, so the three non-figure masses form a loose diagonal.
Breakouts cross the frame by 40–65 px (measured: 44–64 in the examples). A card that only clips the edge by 10–20 px reads as misplaced, not as breaking out.
The lower half needs one furniture mass 120–200 px wide (beanbag, desk, component board, or a seat plus a side table). A figure on a thin stool alone leaves a violet hole under the right breakout.
Margins: everything stays inside x 40–450 and y 30–325.
Techniques
scripts/panel-kit.mjs holds every repeated part, lifted from the examples (no dependencies, deterministic). Generate the frame, then hand-draw the figure and props in between. node scripts/panel-kit.mjs demo "$WORK/kit.svg" draws every part on one card so you can see them before using them.
The frame.
scaffold({ prefix, bg, label, clouds, wall, inside, front })returns the whole card with the layers in the order above.cloud(x, y, 'big'|'mid'|'small'|'tick')andbush(x, 'big'|'mid'|'small', { mirror })place the doodles;floorShadow(cx, cy, rx)puts a contact ellipse on the floor.node scripts/panel-kit.mjs scaffold "$WORK/card.svg" --bg orchid --prefix xx- --label "Card Name"Breakouts.
bubble({ x, y, w, h, tail: 'down'|'left'|'right', at })draws a white speech bubble (radius 8, 11-px tail) with its corner tick; wrap it in a<g stroke="#2E2A5C" stroke-width="1.25">.raised(x, y, w, h, rx, face, under)andtextLine(x, y, len, colour)fill it.Head and shoes.
face({ skin, blush, blushOpacity, mouth, earring, smile })andneck(skin)in template coordinates;sneaker(x, y, { mirror }),tuckedShoe(bx, by).Texture.
hatch(x, y)returns a pair of slashes:<path d="${hatch(186, 214)} ${hatch(214, 196)}" stroke="#2E2A5C" stroke-width="0.9" fill="none" stroke-linecap="round"/>.Holding things. Draw the held object first, then
gripHand()over it, so the curled fingers and thumb overlap the handle; rotate the fist so its fingers follow the forearm, and tilt the handle up to 45° off the thumb axis for wrist bend. To show a lens or screen magnifying what is behind it, redraw that content inside aclipPathof the lens, scaled 1.6 about the lens centre, then a whitefill-opacity0.18 glass and two short white glare strokes; give the rim a light metal#C9C7DA.Moving traced paths.
xf(d, { ox, oy, tx, ty, s, sx, sy })moves, scales or mirrors path data while keeping stroke widths exact; use it instead oftransform="scale()"on anything outlined.
Failure modes
Typing hand without a thumb. Both judges marked the Support card's first typing hand for it. Every hand shows its thumb or is turned so the thumb is plainly hidden behind the palm; check with the thumb proof.
Thumbs on the wrong side. A designer rejected a whole pass of hands across the set for this. Write down left/right, palm/back and finger direction for each hand before drawing it.
Upright on a soft seat. "She sits upright, with no sink into the beanbag": lean the torso 5° into the work, drop the hips below the cushion's rim and dent the cushion outline around them.
Too clean a line. Judges found the line "cleaner and more even than the pack's sketchy line". Mix widths (0.75–1.35 as listed), add the bubble corner ticks, hatch pairs and short tick marks at seams.
UI kit instead of doodles. Breakouts that are full mini-interfaces read as a UI kit. Keep each to an icon, an avatar or a big glyph plus 2–4 placeholder lines.
Too many bubbles. Live Coding's early drafts stacked a heart bubble and a "+1" bubble on the left; the shipped card (scored 9) merges them into one "+1 ♥" bubble and balances the bottom corners with a bush on each side.
A lone prop clutters the floor. A PC tower beside the desk was cut from Live Coding; keep the floor to the furniture the figure uses plus bushes.
Simplified crossed legs. The Support card's crossed legs and socks were called simplified: draw the far foot tucked under the near knee, the near shin crossing in front, a visible sock band and the sole of the near foot.
Figure merges with the panel. Violet clothes on the violet panel lose the silhouette; dress the figure in mustard, mint, orange or cream, and keep dark trousers below the floor line where the band is pale.
Floating feet. Shoes need a
#B5AEEAcontact ellipse and must sit on y 295–304 in the floor band, or visibly on a footrest.A foot reaching for the stool ring. On a stool whose footring sits about 35 px under the seat, the knees are too far forward for a foot to reach it; a tucked far leg drawn there plugged its shin into the shoe's toe. Plant both feet: the far shin drops from under the near thigh, its shoe tucked behind the near shoe with the sole about 3 px higher. Draw a footring as a true ellipse (two cubic halves, not two quadratics that meet in points), with the back half drawn before the post and the front half after it.
A dark lens reads as a frying pan. The self-test's first magnifier had a hair-navy rim and grey glass; a light
#C9C7DArim and a magnified view of the card under it made it read at once.Glyphs that turn into digits. Open hook quote marks at 10 px read as "66" (self-test); draw quotes as two filled teardrops and check every small glyph at 1x.
A handle pasted over the fist. Drawn after the hand, a handle sits on top of the fingers and the grip reads as a sticker; the handle goes first, the fingers and thumb over it.
Examples
examples/support.svg: Support (peach). Cross-legged on a plum beanbag, typing on a laptop on her lap, leaning in; a user's question bubble breaks out left, a "Fixed it!" reply breaks out right, a code window with a bug row and a fixed row hangs on the panel wall. Shows the typing hand, crossed legs, beanbag compression, kicked-off sneakers.examples/design-system.svg: Design System (orchid). Kneeling on one knee, pressing a red button into its dashed slot on a grid board of components (swatches, toggles, slider, avatar chip); a selected component card with handles and a cursor breaks out top-left, an "Aa" type card breaks out right. Shows kneeling legs, a toes-tucked shoe, a two-handed push, raised UI parts.examples/live-coding.svg: Live Coding (sky cyan). Seated at a desk on a swivel chair, pointing at code on a monitor while grinning at the viewer; a mic on a boom arm, a ring light, a mug; a red LIVE tag crosses the top-left corner, a "+1 ♥" chat bubble breaks out left, a code snippet bubble breaks out right. Shows the pointing hand, a mirrored head with glasses, a hoodie with strings, desk drawers.
Workflow
Run commands from this skill's folder. WORK is your own work folder (any path); the card is $WORK/card.svg with id prefix xx-.
Brief. Name the subject and write one line each: the figure's action (verb + object), the pose (sit, kneel, crouch), the wall prop, the two breakouts and what each says, the card pastel.
Pose and hands first. Sketch the figure as boxes on paper or in comments: hip point, knee, feet on the floor band, the shoulder line and where each hand lands. For every hand write down left or right, palm or back, finger direction and the thumb side from
references/craft.md.Scaffold:
mkdir -p "$WORK" && cp scripts/panel-kit.mjs "$WORK/" && node scripts/panel-kit.mjs scaffold "$WORK/card.svg" --bg peach --prefix xx- --label "Card Name". For anything beyond the empty frame, write a small generator in$WORKthat imports./panel-kit.mjsand callsscaffold({ ..., front })with your figure.Block the furniture, torso, legs and head (via
face()), render, and check the silhouette and the lean at 1x before any detail.Draw clothes, hands, hair, props and breakouts; then add folds, hatch pairs, corner ticks.
Render (headless, 2x):
node scripts/render.mjs "$WORK/card.svg" "$WORK/card.png"Sheet next to the examples:
node scripts/render.mjs --sheet "$WORK/sheet.png" examples/*.svg "$WORK/card.svg". Ask "same hand, same set?": frame, line weight, how much breaks out, the face.Zoom every hand, the face, the feet and every contact at 5–8x, re-rendered from vector:
node scripts/render.mjs "$WORK/card.svg" "$WORK/hand.png" --zoom x,y,w,h --scale 8. Do the thumb proof fromreferences/craft.mdon a throwaway copy.Critique against the five criteria in
references/craft.md, fix, and repeat. Expect four or more rounds.Lint:
node scripts/lint.mjs "$WORK/card.svg" --prefix xx- --palette style.json. Fix every ERROR; each WARN must be a deliberate tint.
Verify
[ ] Card is one full-bleed pastel rect (peach, orchid or sky cyan) with no
rx.[ ] Panel at exactly
110, 46, 262 × 266, fill#8A80EA, navy outline 1.35, drawn after the inside props and before the breakouts.[ ] Corner brackets at top-right and bottom-left in the card's bracket colour, plus a double and a single parenthesis mark outside the sides.
[ ] Two outline clouds with their tail dashes and 2–4 tiny arc ticks inside the panel; nothing filled in the sky.
[ ] Floor band y 256–312 with its navy line and 6–7 dashes; every foot and chair leg on it with a contact ellipse.
[ ] Exactly two white breakouts crossing the left and right frame edges, each with a corner tick; at least one bush crossing a side edge.
[ ] All outlines navy
#2E2A5C; widths in the listed bands (no stroke over 1.35 except deliberate UI bars).[ ] The face is the shared template (eye sizes, lash flick, hook nose, blush), placed by translate and rotate only.
[ ] Every hand has a cuff band, a wrist narrower than the palm, separated fingers and a thumb on the side the handedness table gives.
[ ] The pose has a lean, twist or bent knee; the seat or floor visibly takes the weight.
[ ] 2–4 hatch pairs per garment, hair highlight strokes, at least one flat shade panel or darker cuff.
[ ] Text is three words or fewer, 700–800 weight, navy or white.
[ ]
lint.mjs --palette style.jsonpasses; any WARN is a tint you chose on purpose.
Files
- ATTRIBUTION.mdAdded by AutoGineer874 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md21.7 KiB
- agents/openai.yaml301 B
- demo/PROMPT.md1.9 KiB
- demo/index.html76.2 KiB
- demo/preview.jpg122.1 KiB
- examples/design-system.svg22.5 KiB
- examples/live-coding.svg22.0 KiB
- examples/support.svg22.6 KiB
- references/craft.md10.3 KiB
- scripts/lint.mjsScript3.0 KiB
- scripts/panel-kit.mjsScript26.1 KiB
- scripts/render.mjsScript5.0 KiB
- style.json9.2 KiB
scripts/panel-kit.mjs
// panel-kit.mjs: the repeated parts of every Framed panel card, lifted from the three examples so a new card
// starts on the same frame, clouds, floor, bushes, bubbles, face, shoes and hands. No dependencies, no randomness.
//
// node scripts/panel-kit.mjs demo out.svg writes a demo card that uses every part
// node scripts/panel-kit.mjs scaffold out.svg --bg peach --prefix xx-
// writes an empty frame to draw into (bg: peach | orchid | cyan)
//
// From a generator: import { scaffold, cloud, bush, bubble, face, neck, sneaker, tuckedShoe, hatch, typeHand, pointHand, gripHand, kneeHand, xf, NAVY } from './panel-kit.mjs';
// All coordinates are card units (480 x 360). Strokes are written at their final width: the kit moves and
// scales path coordinates itself (xf), so outlines never thin or thicken under a transform.
export const NAVY = '#2E2A5C';
export const BG = { // card pastel -> its bracket / outside-mark colour
peach: { card: '#F7D8B5', mark: '#9C878C' },
orchid: { card: '#DFA5D4', mark: '#A3739B' },
cyan: { card: '#A7E0ED', mark: '#6E9FAD' },
};
export const PANEL = { x: 110, y: 46, w: 262, h: 266, floorY: 256, fill: '#8A80EA', floor: '#D0CCF4', dash: '#8F88D6', shadow: '#B5AEEA' };
const r1 = n => +(+n).toFixed(2);
// ---------- move / scale / mirror path data without touching stroke widths ----------
// maps absolute points (x, y) -> (tx + (x - ox) * sx, ty + (y - oy) * sy); relative segments are scaled only.
export function xf(d, { ox = 0, oy = 0, tx = 0, ty = 0, s = 1, sx = s, sy = s } = {}) {
const toks = d.match(/[MLCQSTHVZAmlcqsthvza]|-?\d*\.?\d+(?:e-?\d+)?/g);
const out = []; let cmd = null, i = 0;
const X = v => r1(tx + (v - ox) * sx), Y = v => r1(ty + (v - oy) * sy);
const n = () => parseFloat(toks[i++]);
const per = { M: 2, L: 2, C: 6, Q: 4, S: 4, T: 2, H: 1, V: 1, Z: 0 };
while (i < toks.length) {
if (/[A-Za-z]/.test(toks[i])) { cmd = toks[i++]; out.push(cmd); if (/z/i.test(cmd)) continue; }
const C = cmd.toUpperCase(), rel = cmd !== C;
if (C === 'A') throw new Error('xf: arcs are not supported');
const k = per[C];
for (let j = 0; j < k; j++) {
const v = n();
if (C === 'H') out.push(rel ? r1(v * sx) : X(v));
else if (C === 'V') out.push(rel ? r1(v * sy) : Y(v));
else out.push(j % 2 === 0 ? (rel ? r1(v * sx) : X(v)) : (rel ? r1(v * sy) : Y(v)));
}
}
return out.join(' ').replace(/ ([MLCQSTHVZmlcqsthvz]) /g, ' $1').replace(/^([A-Za-z]) /, '$1');
}
// ---------- decor inside the panel ----------
const CLOUDS = {
big: { d: 'M196 86 C196 80 202 77 207 79 C209 72 218 69 224 73 C227 68 236 69 238 75 C243 74 247 79 245 84', tail: 'M249 84 L252 84', ox: 196, oy: 86 },
mid: { d: 'M128 152 C128 147 133 144 137 146 C139 140 147 138 152 142 C156 138 164 140 165 146 C169 146 172 150 170 154', tail: 'M174.6 154 L177.6 154', ox: 128, oy: 152 },
small: { d: 'M182 58 C182 54 186 52 189 54 C191 50 197 50 199 55', tail: '', ox: 182, oy: 58 },
tick: { d: 'M150 186 Q154 181 159 184', tail: '', ox: 150, oy: 186 },
};
// outline-only cloud doodle; (x, y) = its left base point. kinds: big (51 wide), mid (44), small (17), tick (9)
export const cloud = (x, y, kind = 'mid') => { const c = CLOUDS[kind]; const o = { ox: c.ox, oy: c.oy, tx: x, ty: y }; return `<path d="${xf(c.d, o)}"/>` + (c.tail ? `<path d="${xf(c.tail, o)}"/>` : ''); };
const BUSHES = {
// two-layer bush (support card, left); origin = left end of its base on the floor line
big: { ox: 84, layers: [
['M92 256 C84 250 85 236 96 234 C93 222 106 213 117 219 C121 207 138 205 144 216 C155 213 162 224 157 233 C164 236 165 248 158 256 Z', '#BACCA6'],
['M124 256 C118 250 120 240 130 240 C131 231 146 229 150 237 C160 235 168 245 163 256 Z', '#A9C094']],
marks: 'M98 242 q2.4 2.8 5.2 0.8 M114 227 q2.2 2.6 4.8 0.6 M131 219 q2.4 2.8 5.2 0.8 M107 251.6 l3 -2.4 M110.4 253 l3 -2.4 M144 228 l3 -2.4 M147.4 229.4 l3 -2.4 M135 246 q2.4 2.8 5.2 0.8 M152 241 q2 2.4 4.4 0.6 M121 236 l2.6 -2.2' },
// single bush (live coding, left); 59 wide
mid: { ox: 90, layers: [['M96 256 C90 252 91 242 99 240 C98 232 108 227 116 231 C120 223 132 223 135 231 C143 230 148 238 145 245 C150 248 149 254 146 256 Z', '#BACCA6']],
marks: 'M102 247 q2.2 2.6 4.8 0.6 M118 236 q2.2 2.6 4.8 0.6 M132 242 l3 -2.4 M135.4 243.4 l3 -2.4 M124 250 q2 2.4 4.4 0.6' },
// small front bush (live coding); 34 wide
small: { ox: 325, layers: [['M330 256 C325 252 327 245 334 245 C335 239 345 238 348 243 C355 242 360 249 356 256 Z', '#A9C094']], marks: 'M338 250 q2 2.4 4.4 0.6' },
};
// scalloped bush standing on the floor line; x = left end of its base. mirror flips it left-right about its own base.
export function bush(x, kind = 'big', { mirror = false, floorY = PANEL.floorY } = {}) {
const b = BUSHES[kind];
const w = Math.max(...b.layers[0][0].match(/\d+\.?\d*/g).filter((_, i) => i % 2 === 0).map(Number)) - b.ox;
const o = mirror ? { ox: b.ox, oy: 256, tx: x + w, ty: floorY, sx: -1, sy: 1 } : { ox: b.ox, oy: 256, tx: x, ty: floorY };
return `<g stroke="${NAVY}" stroke-width="1.15" stroke-linejoin="round" stroke-linecap="round">` +
b.layers.map(([d, fill]) => `<path d="${xf(d, o)}" fill="${fill}"/>`).join('') +
`<path d="${xf(b.marks, o)}" fill="none" stroke-width="0.95"/></g>`;
}
// ---------- the frame ----------
// Draw order (the examples all follow it): card, brackets + outside marks, panel fill, clouds, `wall` (things on
// the panel's back wall), floor band + line + dashes, `inside` (props wholly inside the panel), panel outline,
// `front` (bushes that break the frame, the figure, furniture, then bubbles and cards that break out), sparks.
export function scaffold({ prefix = 'xx-', bg = 'peach', label = 'Untitled', defs = '', clouds = null, wall = '', inside = '', front = '',
marks = { left: 128, right: 180, double: 'left' }, dashes = null }) {
const B = BG[bg] || BG.peach;
const cl = clouds || [cloud(128, 152, 'mid'), cloud(196, 86, 'big'), cloud(182, 58, 'small'), cloud(150, 186, 'tick'), cloud(164, 124, 'tick')];
const dl = dashes || ['M120 268 H136', 'M118 308 H132', 'M170 306 H192 M198 306 H202', 'M238 305 H264', 'M296 304 H318', 'M330 308 H350', 'M354 266 H366'];
const L = marks.left, R = marks.right;
const leftArc = `<path d="M60 ${L} Q54 ${L + 8} 57 ${L + 18}"/>` + (marks.double === 'left' ? `<path d="M53 ${L + 5} Q50 ${L + 10} 51 ${L + 14}"/>` : '');
const rightArc = `<path d="M433 ${R} Q439 ${R + 8} 436 ${R + 18}"/>` + (marks.double === 'right' ? `<path d="M440 ${R + 5} Q443 ${R + 10} 442 ${R + 14}"/>` : '');
return `<svg role="img" aria-label="${label}, Framed panel style" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 360">
<defs>${defs}</defs>
<rect id="${prefix}card" x="0" y="0" width="480" height="360" fill="${B.card}"/>
<g id="${prefix}brackets" fill="none" stroke="${B.mark}" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round">
<path d="M362 35 H379 Q384 35 384 40 V52 M384 57 V66"/>
<path d="M98 291 V299 M98 304 V316 Q98 321 103 321 H121"/>
${leftArc}${rightArc}
</g>
<rect id="${prefix}panel" x="110" y="46" width="262" height="266" fill="${PANEL.fill}"/>
<g id="${prefix}clouds" fill="none" stroke="${NAVY}" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">${cl.join('')}</g>
${wall}
<rect id="${prefix}floor" x="110" y="256" width="262" height="56" fill="${PANEL.floor}"/>
<path d="M110 256 H372" stroke="${NAVY}" stroke-width="1.2"/>
<g id="${prefix}floordash" stroke="${PANEL.dash}" stroke-width="1.15" stroke-linecap="round">${dl.map(d => `<path d="${d}"/>`).join('')}</g>
${inside}
<rect x="110" y="46" width="262" height="266" fill="none" stroke="${NAVY}" stroke-width="1.35"/>
${front}
</svg>
`;
}
// flat contact shadow on the floor band under feet, furniture and bushes
export const floorShadow = (cx, cy, rx, ry = 2.6) => `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="${PANEL.shadow}"/>`;
// ---------- speech bubbles and UI cards ----------
// white rounded box (corner radius 8) with a tail; tail: 'down' (from the bottom edge near `at`), 'left' or 'right'
// (from that side near `at`). Includes the short inner corner tick the examples put just inside the top-left corner.
export function bubble({ x, y, w, h, tail = 'down', at = null, r = 8, fill = '#FFFFFF', tick = 'left' }) {
const X2 = x + w, Y2 = y + h;
let d;
if (tail === 'down') {
const a = at ?? X2 - 22; // tail base from a to a-10, tip 2 right of a, 11 below
d = `M${x + r} ${y} H${X2 - r} Q${X2} ${y} ${X2} ${y + r} V${Y2 - r} Q${X2} ${Y2} ${X2 - r} ${Y2} H${a} L${a + 2} ${Y2 + 11} L${a - 10} ${Y2} H${x + r} Q${x} ${Y2} ${x} ${Y2 - r} V${y + r} Q${x} ${y} ${x + r} ${y} Z`;
} else if (tail === 'left') {
const a = at ?? y + h / 2 + 8; // tail from a-10 to a, tip 11 left
d = `M${x + r} ${y} H${X2 - r} Q${X2} ${y} ${X2} ${y + r} V${Y2 - r} Q${X2} ${Y2} ${X2 - r} ${Y2} H${x + r} Q${x} ${Y2} ${x} ${Y2 - r} V${a} L${x - 11} ${a + 5} L${x} ${a - 10} V${y + r} Q${x} ${y} ${x + r} ${y} Z`;
} else {
const a = at ?? y + h / 2 - 8;
d = `M${x + r} ${y} H${X2 - r} Q${X2} ${y} ${X2} ${y + r} V${a} L${X2 + 11} ${a + 15} L${X2} ${a + 10} V${Y2 - r} Q${X2} ${Y2} ${X2 - r} ${Y2} H${x + r} Q${x} ${Y2} ${x} ${Y2 - r} V${y + r} Q${x} ${y} ${x + r} ${y} Z`;
}
const tk = tick === 'left' ? `<path d="M${x + 7} ${y + 6} Q${x + 5} ${y + 7} ${x + 5} ${y + 10}" fill="none" stroke-width="1"/>`
: tick === 'right' ? `<path d="M${X2 - 5} ${y + 5} Q${X2 - 3} ${y + 6} ${X2 - 3} ${y + 9}" fill="none" stroke-width="1"/>` : '';
return `<path d="${d}" fill="${fill}"/>${tk}`;
}
// a raised pill or button: a darker copy 3 px lower, then the face. e.g. raised(248, 198, 72, 26, 13, '#E35D5B', '#B8434A')
export const raised = (x, y, w, h, rx, face, under) => `<rect x="${x}" y="${y + 3}" width="${w}" height="${h}" rx="${rx}" fill="${under}"/><rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${rx}" fill="${face}"/>`;
// UI text line: a round-capped stroke, 2.2 wide (2.4-2.8 for headings). Grey #BDB8D6, dark #6E6A8E, accents red/green/yellow.
export const textLine = (x, y, len, col = '#BDB8D6', w = 2.2) => `<path d="M${x} ${y} H${x + len}" stroke="${col}" stroke-width="${w}" stroke-linecap="round"/>`;
// ---------- characters ----------
// The shared head: a 3/4 face looking RIGHT (ear on the left, nose hook on the right), chin at (232, 166.5),
// face 36 wide x 46 tall. Wrap face() and your hair in one group and place it with a transform, e.g.
// <g transform="translate(-16 4) rotate(8 232 166)"> hair-back, face(...), fringe </g>
// Mirror it to look LEFT with translate(464 0) scale(-1 1) (plus your offset). Never scale it: the line weight would change.
export function face({ skin = '#EDB0A2', blush = '#E8878A', blushOpacity = 0.6, mouth = '#9A3E52', tongue = '#E8878A', earring = 'stud', smile = 'open' } = {}) {
const ear = earring === 'stud' ? `<path d="M213 151 L213 153.4" fill="none" stroke-width="1"/><circle cx="213" cy="157" r="3.4" fill="#EDB24F" stroke-width="1.1"/><path d="M211.4 155.6 Q212 154.6 213.2 154.5" fill="none" stroke="#FCF6EA" stroke-width="0.9"/>`
: earring === 'hoop' ? `<circle cx="212.6" cy="153.6" r="2.6" fill="none" stroke="#EDB24F" stroke-width="1.3"/>` : '';
const m = smile === 'open'
? `<path d="M233 153.6 C234.8 158.6 240.2 159 242.4 153.4 C239.2 154.6 236 154.6 233 153.6 Z" fill="${mouth}" stroke-width="1"/><path d="M235.6 156.8 Q237.8 155.8 240.2 156.8" fill="none" stroke="${tongue}" stroke-width="1.2"/>`
: `<path d="M233.4 154 Q237.6 157.4 242 153.6" fill="none" stroke-width="1.15"/>`;
return `<g stroke="${NAVY}" stroke-width="1.25" stroke-linejoin="round" stroke-linecap="round">
<path d="M216 127 C213 136 212.5 147 215.5 154.5 C219.5 162 226 167 232 166.5 C238 166 243 161 246 154 C247.4 150.6 248.4 146.4 248.2 141 C248.4 136 248 131 246 126 C238 118 222 118 216 127 Z" fill="${skin}"/>
<path d="M216.5 139.5 C212 137.5 208.6 140.6 209.4 145 C210 149 213 151.4 216.6 150.4" fill="${skin}"/>
<path d="M214.4 142 Q212.4 144.4 214.6 147" fill="none" stroke-width="0.9"/>${ear}
<ellipse cx="230" cy="142.4" rx="1.8" ry="2.1" fill="${NAVY}" stroke="none"/>
<path d="M227.4 141 Q230 139.4 232.6 140.8" fill="none" stroke-width="1.1"/>
<ellipse cx="243" cy="141.9" rx="1.5" ry="1.9" fill="${NAVY}" stroke="none"/>
<path d="M241 140.6 Q243 139.2 245 140.4" fill="none" stroke-width="1.1"/>
<path d="M227.6 141 L225.4 139.8" fill="none" stroke-width="1"/>
<path d="M226.5 134.4 Q230 132.4 233.5 133.8" fill="none" stroke-width="1.2"/>
<path d="M240.6 133.4 Q243.6 132.2 246.2 133.2" fill="none" stroke-width="1.2"/>
<path d="M241.4 144.8 C243.4 147 243.6 148.8 240.8 149.6" fill="none" stroke-width="1.15"/>
${m}
<ellipse cx="228.4" cy="149.4" rx="3.8" ry="2.1" fill="${blush}" opacity="${blushOpacity}" stroke="none"/>
<ellipse cx="245.6" cy="150.4" rx="1.4" ry="1.5" fill="${blush}" opacity="${blushOpacity - 0.1}" stroke="none"/>
</g>`;
}
// neck for the shared head (same coordinates): sits under the chin, its base hidden by the collar
export const neck = (skin = '#EDB0A2') => `<path d="M223.4 156 C223.6 166 223.2 173 220 179.6 Q229.6 185.4 239.6 179.6 C237 173 236.6 166 236.6 157 Z" fill="${skin}" stroke="${NAVY}" stroke-width="1.15" stroke-linejoin="round"/><path d="M223.6 167.6 Q229.6 172 236.4 169.2" fill="none" stroke="${NAVY}" stroke-width="0.9" stroke-linecap="round"/>`;
// white sneaker with a red sole, toe pointing right (mirror: toe left). (x, y) = heel end of the sole's underside.
export function sneaker(x, y, { mirror = false, upper = '#FCFAF3', sole = '#E35D5B' } = {}) {
const o = mirror ? { ox: 132, oy: 300, tx: x, ty: y, sx: -1, sy: 1 } : { ox: 132, oy: 300, tx: x, ty: y };
return `<g stroke="${NAVY}" stroke-width="1.15" stroke-linejoin="round" stroke-linecap="round">` +
`<path d="${xf('M132 297 L132 290 C132 286 134.5 284 138 284 L143 284 C146 287 150 289 155 290 C160 291 162 294 161.5 297 Z', o)}" fill="${upper}"/>` +
`<path d="${xf('M132 297 H161.5 V298.6 Q161.5 300 160 300 H133.5 Q132 300 132 298.6 Z', o)}" fill="${sole}"/>` +
`<path d="${xf('M143 286.6 l3 2.4 M146.4 286.6 l2.4 2.8', o)}" fill="none" stroke-width="0.9"/></g>`;
}
// toes-tucked sneaker for a kneeling or crouching foot: ball of the foot at (Bx, By) on the floor, heel lifted
// up-left, toe pointing right
export function tuckedShoe(Bx, By, { s = 1, sole = '#E35D5B', upper = '#FCFAF3' } = {}) {
const P = pts => pts.map(p => typeof p === 'string' ? p : `${r1(Bx + p[0] * s)} ${r1(By - p[1] * s)}`).join(' ');
const soleD = P(['M', [10.8, 3.2], 'C', [11.8, 2.2], [11.6, 0], [9.6, 0], 'L', [0, 0], 'L', [-11.4, 16.2], 'C', [-12.4, 17.8], [-11.8, 19.6], [-10, 19.6], 'L', [-8.6, 17.8], 'L', [1.4, 3.2], 'Z']);
const upD = P(['M', [-8.6, 17.8], 'C', [-10, 20], [-8, 22.8], [-4.4, 23], 'L', [-0.4, 23.4], 'L', [5.2, 15.4], 'L', [6.8, 17.6], 'C', [8, 14], [8.8, 10], [9.8, 6.6], 'C', [10.6, 5], [11.2, 4], [10.8, 3.2], 'L', [1.4, 3.2], 'Z']);
const lines = P(['M', [-7.4, 18.4], 'C', [-6.4, 20.4], [-4.6, 21.2], [-2.4, 21.2], 'M', [3.8, 3.4], 'C', [5, 5.4], [7, 6.6], [9.6, 6.4]]);
const lace = `M${r1(Bx + 5.4 * s)} ${r1(By - 12.6 * s)} l${r1(2.6 * s)} ${r1(1.2 * s)} M${r1(Bx + 6.4 * s)} ${r1(By - 9.6 * s)} l${r1(2.6 * s)} ${r1(1 * s)}`;
return `<g stroke="${NAVY}" stroke-width="1.15" stroke-linejoin="round" stroke-linecap="round"><path d="${upD}" fill="${upper}"/><path d="${soleD}" fill="${sole}"/><path d="${lines}" fill="none" stroke-width="0.9"/><path d="${lace}" fill="none" stroke-width="0.9"/></g>`;
}
// the style's texture: pairs of short parallel slashes inside a shape (0.85-0.9 navy, or a lighter tint of the fill
// at 1.0 on dark fabric). (x, y) = start of the first slash; each slash runs (dx, dy); the second starts `step` lower.
export const hatch = (x, y, { n = 2, dx = 3, dy = -3, step = [1, 5] } = {}) => Array.from({ length: n }, (_, i) => `M${r1(x + step[0] * i)} ${r1(y + step[1] * i)} l${dx} ${dy}`).join(' ');
// ---------- hands (local frame: wrist at the origin, fingers along +x, y down) ----------
// Place with transform="translate(wx wy) rotate(a)" on the group; never scale above 1.2. Mirror with scale(1 -1)
// to swap which edge the thumb is on, then check handedness with the table and thumb proof in references/craft.md.
// typeHand: a hand resting flat or typing, seen from the back. The thumb tip shows above the index knuckle
// (local -y edge), the four fingertips curl down onto the surface along +y. Support card: her right hand on the keys.
export const typeHand = ({ skin = '#EDB0A2', cuff = '#E0A23E' } = {}) => `<g stroke="${NAVY}" stroke-width="1.05" stroke-linejoin="round" stroke-linecap="round">
<path d="M-13 -5.5 C-8.4 -5.3 -3.8 -5.4 2.2 -5.6 L2.2 1.5 C-3.8 2.5 -8.4 4.2 -13 5.6 Z" fill="${skin}"/>
<path d="M16.4 -7.4 C17 -8.6 18.2 -9.4 19.4 -9.3 C20.4 -9.2 20.8 -8.4 20.5 -7.6 C20.3 -7 19.9 -6.5 19.5 -6.1 Z" fill="${skin}"/>
<path d="M1.4 1.5 C2.4 2 3 2.5 4 2.6 C5.4 2.7 6.4 1.7 7.6 1.4 C9 1.1 10.4 1.8 11.3 2.8 C11.8 3.4 12 4.3 12.7 4.4 C13.4 4.5 14 4 14.1 3.2 C14.5 3.6 15.2 3.7 15.9 3.4 C16.7 3.1 17.2 2.4 17.1 1.6 C17.6 2 18.4 2.1 19.1 1.8 C19.9 1.5 20.3 0.8 20.2 0 C20.9 -0.1 21.6 -0.6 21.8 -1.4 C22.1 -2.5 21.7 -3.8 20.8 -5 C19.6 -6.6 17.4 -7.8 15 -8.4 C13.8 -8.7 12.4 -8.8 11 -8.8 C7.8 -8.8 4.4 -7.6 1.4 -5.6" fill="${skin}"/>
<path d="M14.2 -6.6 C13 -5.6 12 -4.2 11.6 -2.6 M14.1 3.2 C13.8 2.1 13.1 1.2 12.2 0.7 M17.1 1.6 C16.8 0.3 16 -0.7 15 -1.3 M20.2 0 C19.9 -1.4 19 -2.8 17.8 -3.6" fill="none" stroke-width="0.75"/>
<path d="M-16.2 -6.6 C-14.5 -6.8 -12.8 -6.8 -11.2 -6.6 L-11.2 6.6 C-12.8 6.8 -14.5 6.8 -16.2 6.6 Z" fill="${cuff}"/>
<path d="M-13.7 -5.8 V5.8" fill="none" stroke-width="0.8"/>
</g>`;
// pointHand: index finger extended along the upper edge (local -y side), the other three fingers curled below it
// with their knuckles showing, the thumb folded across them from the lower edge. Live Coding: pointing at the screen.
// Cuff is drawn separately so it can sit at the sleeve angle: pointCuff().
export const pointHand = ({ skin = '#F0BFA4' } = {}) => `<g stroke="${NAVY}" stroke-width="1.15" stroke-linejoin="round" stroke-linecap="round">
<path d="M-6.7 -1.8 C-6.0 -5.6 -2.6 -7.8 1.6 -8.0 C5.0 -8.3 8.6 -8.5 11.2 -8.2 C12.6 -8.0 13.4 -7.4 14.6 -7.2 C16.8 -7.0 18.8 -6.9 20.4 -6.7 C22.6 -6.5 23.2 -4.6 22.8 -3.6 C22.4 -2.6 21.4 -2.3 20.2 -2.3 C18.2 -2.3 16.2 -2.4 14.4 -2.5 C17.0 -2.5 18.0 -1.0 17.8 0.4 C17.6 1.6 16.6 2.2 15.2 2.1 C16.4 2.6 17.0 4.0 16.7 5.0 C16.4 6.0 15.4 6.4 14.2 6.3 C15.2 6.9 15.4 8.4 14.8 9.1 C14.2 9.8 13.0 10.0 11.8 9.8 C11.4 9.8 11.1 9.7 10.8 9.6 C7.4 9.4 3.6 8.6 0.8 7.4 C-0.8 6.8 -2.0 6.2 -3.5 6.0 Z" fill="${skin}"/>
<path d="M14.4 -2.5 Q12.2 -2.4 10.6 -3.0 M14.2 6.3 Q12.8 6.5 11.8 6.1" fill="none"/>
<path d="M19.8 -6.7 Q21.6 -6.5 22.4 -5.4" fill="none" stroke-width="0.8"/>
<path d="M2.4 3.8 C5.4 2.2 9.0 0.6 12.4 0.0 C14.4 -0.3 15.8 0.8 15.6 2.2 C15.4 3.4 14.0 3.9 12.6 4.0 C10.6 4.2 8.4 4.8 7.0 5.8" fill="${skin}"/>
</g>`;
// ribbed sleeve cuff, centred on the origin, 6 wide x 13 tall; rotate it to sit across the wrist
export const pointCuff = (cuff = '#D9735A') => `<g stroke="${NAVY}" stroke-width="1.15" stroke-linejoin="round"><path d="M-5 -6.4 C-3 -6.7 -1.2 -6.7 0.8 -6.4 L0.8 6.4 C-1.2 6.7 -3 6.7 -5 6.4 Z" fill="${cuff}"/><path d="M-2.1 -5.8 V5.8" fill="none" stroke-width="0.8"/></g>`;
// gripHand: a fist round a handle (magnifier, mug handle, stylus, mic stand), seen from the back. Fingers point
// along +x and are curled round a handle that runs along local y through x = 13; the thumb wraps the top (-y) edge,
// so the business end of the handle (lens, head) points to -y. As drawn this is a RIGHT hand with the back to the
// viewer. Draw the held object FIRST, then this group: the curled fingers and the thumb must overlap the handle.
export const gripHand = ({ skin = '#F0BFA4', cuff = '#D9735A' } = {}) => `<g stroke="${NAVY}" stroke-width="1.1" stroke-linejoin="round" stroke-linecap="round">
<path d="M-6 -6.2 C-2 -6.6 2 -7 5 -7.4 L5.4 6.4 C2 6.8 -2 6.8 -6 6.4 Z" fill="${skin}"/>
<path d="M4.6 -7.4 C8 -8.6 12.6 -8.8 16.4 -7.6 C19.4 -6.6 21 -4.6 21.2 -1.8 C21.4 1.6 21 4.6 19.4 6.6 C17.4 8.8 13 9.4 8.6 8.6 C7 8.4 6 7.6 5.4 6.4 Z" fill="${skin}"/>
<path d="M14 -4 Q17.6 -4.6 21 -2.8 M13.6 0 Q17.4 -0.4 21.2 0.8 M13.2 3.8 Q16.6 3.6 20.2 5" fill="none" stroke-width="0.8"/>
<path d="M8.6 -7.4 C9.6 -3 9.6 3 8.6 8.2" fill="none" stroke-width="0.8"/>
<path d="M3.6 -6.6 C6 -10.4 10.6 -12.6 15.4 -12 C18 -11.6 18.6 -9.6 17 -8.4 C14.4 -7.6 11.2 -7.2 8.4 -6.4 Z" fill="${skin}"/>
<path d="M-10 -7.2 C-8 -7.5 -6 -7.5 -4 -7.2 L-4 7.2 C-6 7.5 -8 7.5 -10 7.2 Z" fill="${cuff}"/>
<path d="M-7 -6.6 V6.6" fill="none" stroke-width="0.8"/>
</g>`;
// kneeHand: the far arm of a RIGHT-facing seated or kneeling figure (her LEFT arm) hanging down the far side, the hand
// resting palm-down on a knee with the fingers draping forward over the kneecap (Design System). Absolute card
// coordinates: the sleeve starts at (222-232, 190) and the fingertips end at about (255, 250). Move it with dx, dy so
// the fingertips sit just past the front of the knee; draw it after the legs and before the torso.
export const kneeHand = ({ skin = '#D49A80', sleeve = '#95D1B3', cuff = '#79BC9C', dx = 0, dy = 0 } = {}) => `<g transform="translate(${dx} ${dy})" stroke="${NAVY}" stroke-width="1.25" stroke-linejoin="round" stroke-linecap="round">
<path d="M222 190 C232 198 236 212 237.4 222 L240.4 229.6 L230 233 C228.6 226 226 216 220 206 Z" fill="${sleeve}"/>
<path d="M228.6 229.6 C232 228.6 236.4 227.4 240.6 226.6 L242 232.4 L228.4 236.36 Z" fill="${cuff}"/>
<path d="M235 228 L236.4 234" fill="none" stroke-width="0.9"/>
<g stroke-width="1.15">
<path d="M240.6 232.81 C241.23 232.82 243.18 232.74 244.4 232.9 C245.62 233.06 246.82 233.33 247.9 233.8 C248.98 234.27 250.02 234.93 250.9 235.7 C251.78 236.47 252.58 237.42 253.2 238.4 C253.82 239.38 254.27 240.57 254.6 241.6 C254.93 242.63 255.13 243.67 255.2 244.6 C255.27 245.53 255.03 246.77 255 247.2 C254.92 247.43 254.75 248.3 254.5 248.6 C254.25 248.9 253.77 249.05 253.5 249 C253.23 248.95 253 248.42 252.9 248.3 C252.9 248.55 253.05 249.4 252.9 249.8 C252.75 250.2 252.33 250.6 252 250.7 C251.67 250.8 251.12 250.63 250.9 250.4 C250.68 250.17 250.73 249.48 250.7 249.3 C250.63 249.45 250.53 250.03 250.3 250.2 C250.07 250.37 249.55 250.45 249.3 250.3 C249.05 250.15 248.88 249.8 248.8 249.3 C248.72 248.8 248.85 248.03 248.8 247.3 C248.75 246.57 248.7 245.67 248.5 244.9 C248.3 244.13 247.97 243.37 247.6 242.7 C247.23 242.03 246.8 241.35 246.3 240.9 C245.8 240.45 244.88 240.15 244.6 240 C244.73 240.28 245.2 241.1 245.4 241.7 C245.6 242.3 245.78 242.98 245.8 243.6 C245.82 244.22 245.68 244.93 245.5 245.4 C245.32 245.87 245.02 246.27 244.7 246.4 C244.38 246.53 243.97 246.47 243.6 246.2 C243.23 245.93 242.98 245.4 242.5 244.8 C242.02 244.2 241.38 243.3 240.7 242.6 C240.02 241.9 239.22 241.27 238.4 240.6 C237.58 239.93 236.65 239.22 235.8 238.6 C234.95 237.98 234 237.43 233.3 236.9 C232.6 236.37 231.88 235.67 231.6 235.42 Z" fill="${skin}"/>
<path d="M252.9 248.3 C252.93 247.9 253.1 246.72 253.1 245.9 C253.1 245.08 252.93 243.82 252.9 243.4 M250.7 249.3 C250.73 248.9 250.9 247.72 250.9 246.9 C250.9 246.08 250.73 244.82 250.7 244.4" fill="none" stroke-width="0.95"/>
<path d="M239.9 238.5 C240.28 238.63 241.57 239.03 242.2 239.3 C242.83 239.57 243.45 239.97 243.7 240.1" fill="none" stroke-width="0.95"/>
</g>
</g>`;
// ---------- sparkle ticks ----------
// three short strokes radiating from a point, navy 1.2: draws attention to a breakout element
export const ticks = (x, y) => `<g fill="none" stroke="${NAVY}" stroke-width="1.2" stroke-linecap="round"><path d="M${x} ${y} l3 -5 M${x + 6} ${y + 3} l5 -2 M${x - 6} ${y} l-1 -5"/></g>`;
// ---------- CLI ----------
if (import.meta.url === `file://${process.argv[1]}`) {
const fs = await import('node:fs');
const [mode, out = 'panel.svg'] = process.argv.slice(2);
const arg = (k, d) => { const i = process.argv.indexOf(k); return i > 0 ? process.argv[i + 1] : d; };
if (mode === 'scaffold') {
fs.writeFileSync(out, scaffold({ prefix: arg('--prefix', 'xx-'), bg: arg('--bg', 'peach'), label: arg('--label', 'Untitled'),
front: `<g id="${arg('--prefix', 'xx-')}bushL">${bush(84, 'big')}</g>\n <!-- figure, furniture, then breakout bubbles go here -->` }));
console.log('wrote', out);
} else if (mode === 'demo') {
const P = 'kd-';
const front = [
`<g id="${P}bushL">${bush(84, 'big')}</g>`,
`<g id="${P}bushR">${bush(336, 'mid', { mirror: true })}</g>`,
floorShadow(150, 304.5, 22), floorShadow(214, 304.5, 22),
`<g id="${P}shoes">${sneaker(132, 300)}${sneaker(196, 300)}${tuckedShoe(290, 298)}</g>`,
`<g id="${P}head" transform="translate(4 40)">${neck()}${face()}</g>`,
`<g id="${P}head2" transform="translate(500 40) scale(-1 1)">${neck('#D49A80')}${face({ skin: '#D49A80', blush: '#C46A5C', blushOpacity: 0.55, earring: 'hoop', smile: 'closed' })}</g>`,
`<g transform="translate(160 236)">${typeHand()}</g>`,
`<g transform="translate(300 236) rotate(-30)">${pointHand()}</g><g transform="translate(297.7 240.8) rotate(-52)">${pointCuff()}</g>`,
`<g transform="translate(240 230)"><path d="M10 -30 H16 V14 H10 Z" fill="#E35D5B" stroke="${NAVY}" stroke-width="1.15"/>${gripHand()}</g>`,
kneeHand({ dx: -60, dy: 10 }),
`<g id="${P}ask" stroke="${NAVY}" stroke-width="1.25" stroke-linejoin="round" stroke-linecap="round">${bubble({ x: 64, y: 68, w: 102, h: 50, tail: 'down', at: 154 })}${textLine(80, 84, 40, '#E35D5B')}${textLine(80, 93, 60, '#6E6A8E')}${textLine(80, 102, 30)}</g>`,
`<g id="${P}reply" stroke="${NAVY}" stroke-width="1.25" stroke-linejoin="round" stroke-linecap="round">${bubble({ x: 344, y: 166, w: 82, h: 36, tail: 'left', at: 190, tick: 'right' })}${raised(356, 178, 30, 12, 6, '#5FA77A', '#3E8E5E')}${textLine(392, 184, 24)}</g>`,
ticks(418, 160),
`<path d="${hatch(250, 150)}" stroke="${NAVY}" stroke-width="0.9" fill="none" stroke-linecap="round"/>`,
].join('\n ');
fs.writeFileSync(out, scaffold({ prefix: P, bg: arg('--bg', 'cyan'), label: 'Kit demo', front }));
console.log('wrote', out);
} else {
console.log('usage: node panel-kit.mjs demo out.svg [--bg cyan] | scaffold out.svg --bg peach --prefix xx- [--label Name]');
}
}
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Node.js
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Meng To
- Publisher
- Meng To (Community)
- Source
- MengTo/Skills · agent-skills/illustration/illustration-framed-panel · commit 5ea1661
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Meng To or Meng To.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download illustration-framed-panel.zip