Changelog#
On this page
- [1.0.0] - 2026-10-12
- [1.0.0-alpha.5.0.0] - 2026-10-05
- [1.0.0-alpha.4.6.0] - 2026-10-01
- [1.0.0-alpha.4.5.0] - 2026-09-26
- [1.0.0-alpha.4.4.0] - 2026-09-25
- [1.0.0-alpha.4.3.0] - 2026-09-23
- [1.0.0-alpha.4.2.2] - 2026-09-13
- [1.0.0-alpha.4.2.1] - 2026-09-12
- [1.0.0-alpha.4.2.0] - 2026-09-12
- [1.0.0-alpha.4.1.2] - 2026-09-01
- [1.0.0-alpha.4.1.1] - 2026-09-01
- [1.0.0-alpha.4.1.0] - 2026-08-30
- [1.0.0-alpha.4.0.0] - 2026-08-28
- [1.0.0-alpha.3.3.0] - 2026-08-22
- [1.0.0-alpha.3.2.2] - 2026-08-17
- [1.0.0-alpha.3.2.1] - 2026-08-17
- [1.0.0-alpha.3.2.0] - 2026-08-16
- [1.0.0-alpha.3.1.0] - 2026-08-08
- [1.0.0-alpha.3.0.1] - 2026-07-30
- [1.0.0-alpha.3.0.0] - 2026-07-12
- [1.0.0-alpha.2.1.0] - 2026-07-12
- [1.0.0-alpha.2.0.0] - 2026-07-10
- [1.0.0-alpha.1.0.0] - 2026-06-28
- [1.0.0-alpha.0.0.0] - 2026-06-06
All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[1.0.0] - 2026-10-12#
Direct Select picks, moves and deletes points as a vector editor's Direct
Selection does: a box picks the anchors it holds across every path it
reaches, or the segments it crosses when it holds none, the arrow keys move
what is picked, and Delete takes it out of the path, opening a closed one.
Electron moves from 31 to 44 and electron-builder from 24 to 26, the
install errors seen on Linux and macOS are written up, and a workflow and
Dependabot check the repository on Linux, macOS and Windows. The app icon is
the one kept in assets/, used as it is. A selection exports as exactly the
box the canvas frames it with, a PNG asks what goes under its ink, the
Layers panel has keys for the top and the bottom of a group and for the row
above and below, and the Transform box is up by default.
This is the first release on npm, as two packages under the isocialpractice
organization. @isocialpractice/napkin-sketch is the command line, the API
and its entries, the embeddable editor, the API pages an agent reads, and the
three AI helper plugins, which are experimental and say so wherever they are
named. Its bundles are minified and share one core, and it carries no
Electron, no source maps, no embed script, no changelog and no reference
sheets or their data, which the vectors skills now fetch by address at this
version's tag: 277 files and 1 MB packed, from 484 files and 12 MB.
@isocialpractice/napkin-sketch-gui is the desktop app, with Electron as its
dependency, and napkin-sketch --install-gui adds it to an install. The
installers are built from a clone, as before, and the Windows one is about 100
MB, from 120. This is also the first GitHub release since 1.0.0-alpha.4.6.0,
so it carries the effects, type and styles of 1.0.0-alpha.5.0.0, which the
changelog sets out under that version.
Added#
Two packages on npm.
npm install -g @isocialpractice/napkin-sketchputsnapkin-sketch draw,check,renderandverbson the PATH with no Electron, andnpm install @isocialpractice/napkin-sketchputs the API in a project.@isocialpractice/napkin-sketch-guiis the desktop app, with Electron as its one dependency, andnpm run pack-guilays it out inrelease/gui/from the build and the site.--install-gui,--uninstall-guiand--here.napkin-sketch --install-guiinstalls the desktop app with npm at the command's own version, saying first that it fetches Electron, about 100 MB, and showing the download;--uninstall-guiremoves it, and--heredoes either in the current project rather than globally. The command starts a clone's own build when it runs from a clone and the installed app otherwise, and says how to add the app when there is none.--versionadds a line saying where the app is, and warns when it is another version than the command.The reference sheets by address. The
vectorsskills' sheets and their two data files stay in the repository, and each skill'sassets/uri.md, written bynpm run ai-helper, lists their addresses at this version's tag, with the same path onmainas the fallback. The skills say how to fetch a sheet that is not beside them.pack-checkholds both packages to their line. It fails when the API's tarball packs to more than 1.5 MB or the app's to more than 4 MB, when a file in either names one the tarball lacks - a declaration's import, a shared chunk, a stylesheet'surl(), an app page's script - and when a browser-safe entry reaches a Node built-in. It installs both, and asks the installed command line what it finds.A PNG's background. The Export dialog asks what goes under the ink of a PNG - Transparent, White or Black - for the page, for every page and for the selection, and remembers the choice as the next PNG's first. A PNG with nothing under it is transparent at last: the page export used to carry the paper's color, the dot texture, the page outline and the selection boxes of the screen, and a selection's PNG came out black where the paper was, because the screen's canvas has no alpha channel and a picture read from it has none either; a transparent export is now read from the ink canvas, which keeps it. Every export draws the ink on its paper, on a color, or on nothing, and no more.
Keys for the Layers panel.
Ctrl+Shift+]andCtrl+Shift+[take the selected rows to the top or the bottom of their group in one step, under Layers > Move Layer;Alt+]andAlt+[make the row above or below the active one the selection, as the panel shows its rows, under a new Layers > Select Layer submenu. All four answer to the Layers panel and ask for it when it is hidden.Transform box on selection, a switch in Quick Settings and in Verbose Settings, on by default: the Transform box comes up by itself around whatever the Select tool selects, as a vector editor's bounding box does, and goes when nothing is selected or another tool is taken.
EscapeorCtrl+Tput it away for that selection, and the next selection brings it back. A 3D mark keeps its gumball, which the box would hide, and a path text's brackets, which stand where the box's handles do, are what a press on them grabs.Direct Select's box. A drag over empty canvas with Direct Select picks what the box holds, across every path it reaches:
- the anchor points inside it, and only those, when it holds any;
- when it holds none, every segment it passes through - the stretch of path between two anchors - drawn blue while its anchors stay hollow;
- nothing, when it catches nothing: the picks stay as they were.
A press that does not travel is still a click, which drops the edit, and
Shiftadds what the box picks to what was picked. Text is left out of the box.window.napkinCheck.directSelectState()gains every stroke's picked anchors and segments.Moving a Direct Select selection. A drag on a picked anchor or segment moves every pick, on every path; a segment carries the anchors at both of its ends. With no button down the arrow keys move the picks a step,
Shiftten, in the units the position fields show, as a distance on the page whatever the zoom. A drag is one undo step, and so is each press of an arrow key, its repeats included.Deleting anchor points and segments. With anchors or segments picked,
Delete,Backspaceand the menus' Delete take them out of the path and leave the rest. A deleted anchor takes both of its segments with it and a deleted segment goes alone, so a closed path opens - running on from just after the gap - and an open path splits in two. A lone anchor left over is dropped, and a path with nothing left goes. It is one undo step however many paths the picks were on, and Track History lists it as Direct Select's step. A text's frame or path keeps its points, and a report says so. With only the path picked, Delete takes the whole path, as before.Checks on every pull request. A workflow type-checks, builds and runs the unit tests on Linux, macOS and Windows for every pull request and every push to
main, each from a cleannpm install.Dependabot. Weekly upgrade pull requests for npm packages, Electron and electron-builder together, and for the GitHub Actions the workflows use.
--no-sandbox. The launcher's flag for starting Electron without Chromium's sandbox, for development on a Linux system that blocks it:npm start -- --no-sandbox. The sandbox stays on unless it is asked for.Troubleshooting install errors, on the install page: the Linux SUID sandbox error and its three fixes, the macOS
spawn ... ENOENT, and the macOS malware warning about Electron.
Changed#
- The package is
@isocialpractice/napkin-sketch, scoped to the organization, withpublishConfigmaking it public andrepository,homepageandbugsfor its npm page. The command is stillnapkin-sketch, and every import in the docs, the helpers and the tests names the scoped package. - The package ships what its entries need, minified. Its
fileslist leaves out the app's Electron bundles, the source maps, the changelog and the reference sheets, andmainis the API's entry. Every bundle is minified, and the entries and the command line are built together as ES modules, so the code they share is written once, indist/chunks/, and loaded once. - The command line is an ES module that shares one core with the entries, so a code language installed through any of them is installed for all.
- Every AI helper is labeled experimental: the three plugins' manifests
and READMEs, the marketplace, the app's Animation Mode row and banner, the
--promptand--helperoptions, the API pages and the manual. Each runs an AI tool you install and sign in to yourself, on your account and at its cost, and its commands, forms and files may change in a minor release. - The README's links are absolute, so they work on the npm page.
- The install page starts from npm: the two packages,
--install-gui, and where a global install puts Electron for the Linux sandbox fixes. The installers are built from a clone, under Desktop installers. - The quickstarts start from npm, with a clone as the other way. The AI helpers' tour still runs in a clone, whose test assets it draws with.
engines.nodeis>=22.12for both packages: the version Electron's installer asks for.- The installers carry the app alone. electron-builder takes the main
process, the window and the assets, without source maps or declarations,
with Electron's English locale only and at its maximum compression, and
build.extraMetadatakeeps the installed app's own name and entry. The Windows installer is about 100 MB, from 120. - An exported selection is cut to exactly the box the canvas frames it with - the Transform box's - with no margin round it, no page outline and no paper texture. It was grown by half the widest stroke on every side, which put a margin of paper round a fill-only graphic, and drawn as a sized page, with the dashed outline and the dot texture of the screen. A mark that paints an outline still keeps the half of its width past its centerline, and a mark's effects what they paint beyond it. Animation frames are cut the same way.
- The Transform box is up by default, through Transform box on selection, above.
- Electron 31 to 44, and electron-builder 24 to 26. Building from source needs Node.js 22.12 or newer, which Electron's installer asks for; the published packages ask for the same.
- The clipboard is Electron's asynchronous one. Every read and write of the system clipboard is awaited. Under Electron 44 the old reads tested the promise rather than the text, so SVG copied in another editor would no longer have pasted.
- The documentation window steps back and forward through Electron's navigation history, in place of the calls Electron 44 deprecates.
- A missing Electron says how to put it back. When the binary is not
there, the launcher names the path it looked for and the commands that
reinstall Electron, in place of a bare
spawn ... ENOENT; on macOS it adds that XProtect may have moved the app to the Trash. - The app icon is the one kept in
assets/. The build copies it and never draws or resizes one.assets/icon.pngis a 1024 x 1024 square made fromassets/icon.svg, and a test holds it to a square of at least 512. - The Direct Select row in Edit Tool Types and the cheatsheet now reads "picks, moves and deletes anchor points and segments".
Removed#
- The
./embedexport and its script-tag bundle,dist/embed/napkin-sketch.js. A page with no bundler loads the editor with a module script and an import map, as the embedding page shows. - The changelog, the source maps and the reference sheets from the package. The changelog is on GitHub and the site, and the sheets are fetched by address.
npm run iconand its script, which drew the old icon over the one inassets/on every build.
Fixed#
- The cheatsheet's AI helpers table names all three plugins; it named
two, and left out
scripting. - A build no longer replaces the app icon with the old generated tile.
- Undo and redo drop Direct Select's picks, which named anchors by their place in a path that the step may have reshaped.
- The unit tests pass on Windows checkouts that turn line ends to CRLF.
.gitattributes, which keeps the golden scripts and their SVG at LF, had never been committed, so the Check workflow's Windows job - Git there hascore.autocrlfon - read every golden, and the written triangle script, as CRLF, and 13 tests failed while Linux and macOS passed. It is committed now, and marks the PNG golden binary, which as text Git reported changed when it was not. The site test reads its pages with CRLF folded to LF, as its check against the generator already did.
[1.0.0-alpha.5.0.0] - 2026-10-05#
Effects, type and styles, as a vector editor has them, make this a major
release. Sixteen blend modes go on layers and groups under napkin names -
Stack Blend for Multiply - and the Appearance panel and the Effect menu put
Feather, Bevel, Emboss, the inner and outer shadows and glows, Splatter,
Warp, and 3D Extrude and Revolve, turned with a gumball, on a mark's fill,
its stroke or the whole of it. Text is set as a vector editor sets it,
typed on the canvas, in a box, in a shape or along a path - Sideless, Sheet
and Path Typing - with a Type panel of character and paragraph attributes,
Text Configurations and Expand Text to Shape; Markdown Flow shows Markdown
as it renders, and Clean Language Accents color code as its language reads
it. Composition Styles restyle a page by its layers' names in one click, a
report toast says what an action could not do, and a book whose fonts are
not installed opens with a report of them. Enter makes a new line in
text and Esc puts it down, a text can be edited again, effects follow a
scale, a turn or a mirror, and a PDF draws text as shapes. Each piece runs
on an engine the API exports and napkin script has words for, written up in
The drawing engines. The .skbk format is
version 4; files of versions 1 to 3 open unchanged.
Added#
Report toasts. A notice in the bottom-right corner for what an action could not do, line by line:
- a one-line title with a warning or an information mark;
- Details, which opens to the rows, each of which can carry a suggestion and a button that puts it right;
- a close button.
It fades after 15 seconds, but not while the pointer or the keyboard focus is on it, or while its details are open. It gets its full time again once they leave, and Escape closes the one with the focus. Up to three stack upward, newest at the bottom; a fourth takes the place of the oldest that nobody is holding. A report with the name of one on screen replaces it where it stands. The stack sits clear of the status bar, and of the nib indicator while it shows.
Reports fade after, in Verbose Settings > Extras, sets the time: 5 to 60 seconds, or 0 to keep reports until they are closed. The features that report through it come later in this release.
window.napkinCheckgainsshowReportToast(spec)andreportToasts()for the check, andnpm run gui-check -- report-toastruns it.Napkin Blends. A vector editor's sixteen blend modes, from its Transparency panel, under napkin names; Normal keeps its own:
Napkin Blend Vector editor Soak Blend Darken Stack Blend Multiply Scorch Blend Color Burn Dry Blend Lighten Sheer Blend Screen Bleach Blend Color Dodge Fold Blend Overlay Light Blend Soft Light Heavy Blend Hard Light Messy Blend Difference Avoidance Blend Exclusion Tidy Blend Hue Bright Blend Saturation Dye Blend Color Cover Blend Luminosity - Where. The Blend select under Opacity in the Layers panel, and
Layers > Blend (in the panel's right-click menu too), set the mode of
the selected layers, one undo step for them all.
Shift+PlusandShift+-step through the list. A toast names the mode, Layers > Blend checks it, and a blended layer's row carries a mark of two circles. - What it mixes with. What is under the layer, the paper included: a Stack Blend darkens into the paper, grain and all, while a layer at Normal covers it as before. A group with a blend paints what it holds as one picture and blends that. Isolate Blending, on a group, keeps the blends inside it from reaching past it. A group with an opacity below 100% that holds a blended layer is isolated as well, as SVG and PDF composite it.
- The exports. The SVG writes
mix-blend-modewith the napkin name indata-blend, and an isolated group'sisolation. The PDF prints a blended layer, or a blended or isolated group, as a transparency group mixed by/BM. The Illustrator script setsblendingMode, andisIsolatedwhere Illustrator knows it.renderSketch's PNG, drawn with no canvas, mixes by the same formulas as the canvas, the W3C's. - Imports. The SVG importer reads napkin's
data-blend, and another editor'smix-blend-modeandisolation, inline or from a<style>rule. - Scripts. Napkin script's
blend <mode>gives the next layer or group a blend, by its napkin name or the vector editor's, andunused-blendreports one nothing took. Generate Script writes a page's blends back, and notes the Isolate Blending it cannot say. - The API.
BLEND_MODES,blendModeOf,blendColor,blendPixeland the rest join it. The graphic-design API's elements take ablend, and its groupsisolate.
npm run gui-check -- blendruns it.- Where. The Blend select under Opacity in the Layers panel, and
Layers > Blend (in the panel's right-click menu too), set the mode of
the selected layers, one undo step for them all.
The appearance effects. Feather, Bevel, Emboss, Inner Shadow, Outer Glow and Inner Glow join the effects a mark, a layer or a group can carry, each starting as a vector editor starts it, and the drop shadow - Outer Shadow - gains a spread, an opacity and a blend:
Effect What it does Feather Fades the picture from clear at its edge to solid its radius inside Bevel A slope inside the shape, or outside it, lit from an angle and an altitude: its light screens and its shade multiplies Emboss The slope straddling the edge, or, as Pillow Emboss, dipping into a groove round it Inner Shadow A shadow cast inside the shape from its edge, as if the shape were a hole Outer Glow A glow around the shape Inner Glow A glow inside the shape, from its edge or from its center - On the fill or the stroke. Any effect can be aimed at a mark's fill or its stroke alone, as a vector editor's Appearance panel aims it: a filled shape's fill can glow inside its outline and leave the outline clean. Each part draws through its own effects, the stroke over the fill, before the whole mark's. An effect can also be kept without being drawn, for the Appearance panel's eye to come.
- A shadow that blends mixes with what is under its mark. An Outer Shadow or Outer Glow at the end of a mark's list, blending by anything but Normal, is laid under the mark on its own and mixes with what is drawn under it, the paper included: a blue shadow that multiplies darkens the paper and the layers it falls on. A translucent mark, and a layer that is translucent, blended or erased, keeps it in its picture.
- One graph for every output. Each appearance effect is a small graph of
SVG filter primitives - blurs, component transfers, offsets, floods,
composites, blends and diffuse lighting. The SVG writes the graph, and a
new pixel engine runs it for the canvas and the PNG, so the three agree:
against the SVG as
rsvg-convertdraws it, each appearance effect's PNG differs by under half a level in 255 on average. None usesfeMorphology, whose radius librsvg holds to ten pixels: a spread or a choke is a levels step on the blurred matte, as Photoshop's is. In the SVG, a shadow that blends is a<use>of its mark laid under it, and a mark whose parts take effects of their own is a group of the two, which the importer reads back as one mark. - The canvas draws a mark through its appearance effects from a picture of its own, worked out once and kept while the mark, its effects and the zoom stay the same, so a pan works none out again; a zoom under way draws the last one stretched until it holds still.
- The PDF and the Illustrator script still leave effects out, and now name each one. The app's PDF export lists what it left out - an effect, a color that is not one - in a report in the corner of the window, where it reached no one before.
- Scripts.
effectgainsfeather,bevel,emboss,inner-shadow,outer-glowandinner-glow, anddrop-shadowgainsspread,blendandopacity; anyeffectline takeson filloron stroke, andhidden. Generate Script writes them all back, each field only where it is not the default. A hand-drawn filled shape takes an effect aimed at its fill on the fill, and one aimed at its stroke on each pass of its line. - The API.
EFFECT_DEFAULTS,EFFECT_LABELS,defaultEffect,activeEffects,effectPlan,effectLayout,runEffectsand their kin join it, and the graphic-design API's elements take an appearance effect with only the fields it changes.
npm run gui-check -- effectsruns it.The Appearance panel and the Effect menu. Effects are added and changed in the app now, as a vector editor's Appearance panel and Effect menu add and change them:
- The Effect menu, between Transform and Sketch: Apply Last Effect
(
Ctrl+Shift+E) and Last Effect (Ctrl+Alt+Shift+E); Blur > Gaussian Blur; Adjust Colors > Brightness, Contrast, Saturate, Grayscale, Sepia, Invert, Hue Rotate and Opacity; Stylize > Feather, Outer Shadow, Inner Shadow, Outer Glow, Inner Glow, Bevel and Emboss; Expand Appearance; and Clear Appearance, which takes every effect off the selection and leaves its fill and stroke. Apply Last Effect, Last Effect, Expand Appearance and Clear Appearance are at the end of the canvas's right-click menu too. - The Appearance panel (
Shift+F6, View > Toggle Appearance Panel) opens docked beside the canvas and lists the selection's Stroke and the effects on it, its Fill and the effects on it, the effects on the whole of it, and the Opacity and blend of its layers. An effect's row has an eye, its name with where a vector or photo editor has it, and an fx button for its options. Rows drag to reorder, and into another section to move there;Alt+UpandAlt+Downmove one from the keyboard; a right-click offers Duplicate, Delete and Move to Fill, Stroke or Object. A selected Fill or Stroke row aims the next effect at that part, and a lit group row in the Layers panel puts the group's own effects in the panel instead. - One options popup for every effect, made from the effect's fields: lengths, percents, an angle with a dial, colors, blend modes and choices. Preview is on to begin with and paints as each field changes; OK keeps the effect as one undo step, and Cancel leaves no trace, not even an undo step.
- The Properties panel gains the Opacity and blend of the selection's layers, an fx button that drops the Effect menu's rows down, and the number of effects, which opens the Appearance panel.
- Expand Appearance bakes the effects that change what a mark draws into marks of their own and leaves the others live: Splatter, Warp and 3D, below. The row is greyed while nothing selected has one.
- Use Preview Bounds, a new setting in Verbose Settings > Sketch Support, off by default as in a vector editor: a selected mark's box, the area a click picks it by, and the Properties panel's position and size take in what its effects paint past its ink.
The guide gains Effects and the Appearance panel, which Help > Tool Types > Effect opens.
window.napkinCheckgainsappearanceState(),effectOptionsState(),setEffectOption(key, value)andlastEffect(), andnpm run gui-check -- appearanceruns it.- The Effect menu, between Transform and Sketch: Apply Last Effect
(
Splatter. A vector editor's Scribble, as Effect > Stylize > Splatter: a mark's fill drawn as one loose, continuous hatch, back and forth across the shape at an angle, as a pen scribbles a shape in. Its options are the Scribble Options' - Angle, Path Overlap, Stroke Width, Curviness and Spacing, the overlap, the curviness and the spacing each with a variation - and a Seed, so a hatch draws the same every time; and eleven presets, Default, Childlike, Dense, Loose, Moire, Sharp, Sketch, Snarl, Swash, Tight and Zig-zag, which the options popup's Settings sets every field from. Default is the vector editor's own; the others are this app's reading of each one's look, since their published values were not to be had.
- What it hatches. The fill, on the whole mark or aimed at the fill; aimed at the stroke, the stroke's own ink, a thick outline turned to scribble. The hatch is the color of what it stands for, and a gradient fill's hatch takes the gradient across it. A hole stays empty: the hatch splits round it into runs, and no pass crosses it. A group has no shape of its own to hatch, so it takes none; its marks can. Nor does an element of the graphic-design API, which leaves it out and says so.
- The first geometry effect. It changes what a mark draws rather than its picture, so it works before any other effect, and a mark is found where its hatch is: its bounds take in a hatch carried past the edge, and a click between the passes misses it. Expand Appearance bakes it into a group of the mark's outline and its hatch, a mark for each run of it, as one undo step.
- Every output draws the hatch as lines: the canvas, PNG, JPEG, SVG, PDF and the Illustrator script. The SVG keeps the effect as well, with the mark it stands for - a Copic's nib and a Pencil's lead and smudges among it - and napkin-sketch reads it back as that mark with a live Splatter.
- Scripts.
effect splatter [preset <name>] [angle <degrees>] [overlap <n> [<variation>]] [width <n>] [curviness <percent> [<variation>]] [spacing <n> [<variation>]] [seed <n>], withon fill,on strokeandhiddenas every effect has them. Generate Script names a preset the values match, and writes only what differs from it. A mark the hand-drawn pass draws as several strokes takes it on its fill, or on each pass of its line, and a library shape of several parts on each part, since the layer their other effects go on has no shape to hatch. - The engine is
src/core/splatter.ts:splatterPaths(region, options)turns closed contours into one open path of Bezier anchors a run, an anchor a fold and no more.
npm run gui-check -- splatterruns it.Warp. A vector editor's Effect > Warp, as Effect > Warp: what takes it bent by a style laid over its box, and live - the mark keeps its own anchors, and the bend is drawn anew as they change. Twelve styles, Arc, Arch, Unroll, Shell, Windy, Breezy, Curvy, Chart, Puffy, Expand, Cram and Twirl - the vector editor's Arc, Arch, Bulge, Shell, Flag, Wave, Fish, Rise, Fisheye, Inflate, Squeeze and Twist - with its Warp Options: horizontal or vertical, a bend, and a horizontal and a vertical distortion, each -100% to 100%, and for Arc and Shell the Part they bend, which gives the vector editor's Arc Lower, Arc Upper, Shell Lower and Shell Upper. The options show the style bending a grid as the fields are set. Effect > Warp > Twirl is not Liquify's Twirl brush, and the menus say so.
- What it bends. A mark, over its own box; several selected marks, each over its own, as the options say; a group or a layer lit in the Layers panel, every mark it holds over the box of all of them, as one. Aimed at the fill or the stroke, that part bends alone. A text bends through its glyphs' outlines (Type: glyph outlines, below). Pictures take none: with only those selected, the Warp rows are greyed, and the in-app menus say why. Splatter's row is greyed for them too.
- Few anchors. A straight edge bends into a curve with an anchor at
each end and as few between as the bend needs, its handles in line where
they meet: the art is carried along the style's tangents.
PointMapgains an optionaljacobian, which the warp carrier uses when a map gives one; Mesh Warp and Liquify carry as they did. - With the other effects. It bends before any raster effect works on the picture; with a Splatter, the two work in their list's order, a hatch bent or a bent shape hatched. A mark is found where it is bent, and its box is where the bend puts it.
- Expand Appearance bakes a mark's Warp into its own anchors, as one mark, and a lit group's into the marks it holds, each mark's own geometry effects first, as one undo step.
- Every output draws the bent shape: the canvas, PNG, JPEG, SVG, PDF and the Illustrator script. The SVG keeps the mark as it was and the Warp itself, on the mark or on its group, and napkin-sketch reads it back live. The graphic-design API's elements take none, which it says.
- Scripts.
effect warp <style> [bend <percent>] [horizontal|vertical] [distort <percent> [<percent>]] [part whole|lower|upper], the style by either name, and a percent there may carry a%. On a layer or a group it bends all it holds; on a hand-drawn mark or a library shape of several parts it goes on their layer of their own, so they bend as one. Generate Script writes it back. - The engine is
src/core/warp-styles.ts:warpStyleMap(style, box, options), each style a closed-form map over a box, with its derivative.
npm run gui-check -- warp-effectruns it.3D. A vector editor's Classic 3D, as Effect > 3D > Extrude and Revolve: a mark built in depth and drawn as flat, shaded faces - vector shapes in every format. Extrude sets the shape deep, a front and a back cap and a side an edge; Revolve sweeps it round an upright axis at its left or right edge, an offset off it, through an angle in as many steps as it needs to look round. Both turn about X, then Y, then Z - Off-Axis Front, -18, -26 and 8, to begin with - with a perspective up to 160 degrees, a move and a scale along each axis, and Plastic, Diffuse, No Shading or Wireframe under a light set by its direction, its height, its intensity and the ambient light. Cap off leaves an extrusion hollow, or a part revolve open.
- What it builds from. A filled mark's fill, holes and all - an
A's counter stays open - in its fill's color, the front cap outlined in its stroke; a mark with no fill, its path in its stroke's color: a closed one a tube, an open one a ribbon, or revolved, the surface a potter turns. - Drawn far first, exactly. The faces are put in order from back to front by a BSP tree over their convex pieces, built once for a shape and walked from the eye, so they overlap as they should from any side; a cap is always one shape, and each face is hemmed in its own color so no seam of paper shows between two. A solid shows no face turned away.
- With the other effects. It builds the mark as the effects before it left it - a Warp above it bends the shape first - and a Warp after it bends the faces. A raster effect works on the whole picture of faces, and the mark's opacity falls on it once. A mark is found where its faces are, its box taking in its depth. A group has no shape of its own and takes none, and pictures take none; a text is built from its glyphs' outlines (Type: glyph outlines, below).
- Expand Appearance turns it into a group of its faces, each a filled shape, as one undo step.
- Every output draws the faces: the canvas, PNG, JPEG, SVG, PDF and the Illustrator script. The SVG keeps the mark as it was and the effect itself, and napkin-sketch reads it back live.
- Scripts.
effect extrude [<depth>]andeffect revolve [angle <degrees>] [offset <n>] [axis left|right] [segments <n>], each with[rotate <x> <y> <z>] [perspective <degrees>] [shading <kind>] [hollow] [light <azimuth> <elevation> [<intensity> [<ambient>]]] [position <x> <y> <z>] [scale <x> <y> <z>]. Generate Script writes them back. - The engine is
src/core/three-d/:mesh.tsbuilds the meshes - ear clipping with the holes bridged in, extrusions and revolves - andrender.tsdraws them;render3d(stroke, effect)gives a mark's faces. - Its options are 3D Options, below, which sets them live.
Illustrator's bevels and mapped artwork are not drawn.
npm run gui-check -- 3d-effectruns it.- What it builds from. A filled mark's fill, holes and all - an
The 3D gumball and 3D Options. A mark built in 3D, selected alone, shows the gumball, drawn as Blender and Rhino draw theirs - X red, Y green, Z blue - at the solid's middle along its own axes, in place of a Transform Box: an arrow moves it along an axis, a square between two moves it in their plane (with
Alt, scales it there), an arc turns it about an axis (withShift, 15 degrees a step), a box past each arrow scales it along that axis, the ball moves it across the view (withShift, scales it whole) and the ring round it all turns it freely, as a trackball does.- Each drag is one undo step, recorded as 3D Options, and
Escapeduring it puts the 3D back. A part under the pointer is lit amber and the pointer says what it does. The gumball is the same size at any zoom, and an axis pointing at the eye hides its arrow. A revolve is drawn in fewer steps round while a drag turns it, and in full when it lets go. - The 2D tools still work. The Transform Box (
Ctrl+T) takes the gumball's place while it is up and edits the shape the 3D is built from; Move and the arrow keys move the whole mark. - 3D Options (Effect > 3D > 3D Options, a 3D's row in the Appearance
panel, or a double-click on the ball) sets the selection's 3D live, in a
panel that stays up and docks: a cube turned and lit as the 3D is, which a
drag or the arrow keys turn, and every field, Position and Surface each a
section that folds. A choice is one undo step; a number typed is one when
the field is left, and a dial or the cube one when the drag lets go;
Escapetakes back a change not yet kept. Several marks are set together, a field they differ in reading Mixed; with nothing built, it offers Extrude and Revolve. - Effect > 3D > Extrude and Revolve build at once, as one undo step, with 3D Options up on the selection; on a mark built already they change the kind. Apply Last Effect and Last Effect put a 3D in the place of a mark's own, as only one builds a mark, and the last change made in 3D Options or with the gumball is the last effect.
- Show the 3D gumball, in Verbose Settings > Sketch Support, on to begin with: off, 3D Options is the only control.
- Underneath.
src/renderer/gumball.tsplaces the handles, finds the one a press takes and works each drag from the press, through the viewsrc/core/three-d/render.tsnow exports (viewOf,viewPoint,projectPointandthreeDView);src/renderer/three-d-panel.tsis the panel. The check hooks gaingumballState()andthreeDPanelState().
npm run gui-check -- gumballruns it.- Each drag is one undo step, recorded as 3D Options, and
Type: the model and the layout engine. A text mark keeps its text, size, family and color as its base, and gains an optional
typeset: runs of its characters in styles of their own, attributes for each paragraph, and its kind - Sideless Typing from its anchor, Sheet Typing in a frame, or Path Typing along a path - and its orientation. Old boxed text reads as Sheet Typing in a rectangle that grows to fit, as its box did.- One layout for every output.
src/core/type/layout.tssets a text mark: its lines, each character's place, size and turn, its underlines, strikethroughs and highlights, and whether any of it did not fit. The canvas sets it with the fonts the system has, so lines break where they are drawn; the command line, the API and the PNG with the built-in face. - Per character: a face, tracking and kerning by hand, the font's own kerning, leading, word spacing, all caps and small caps, horizontal and vertical scale, a baseline shift, a superscript or a subscript, a turn of its own, an underline, a strikethrough and a highlight.
- Per paragraph: left, centre, right or justified, indents, the space before and after it, Liang hyphenation over the en-US patterns at Illustrator's limits, and bulleted and numbered lists.
- Sheet Typing fills any closed shape a band at a time, and says when text is left over. Path Typing sets a line along any path, its characters turned by the rainbow, skew, ribbon, stair or gravity effect, and flipped to the other side when asked. Vertical text runs in upright columns, right to left.
- Out and back. The SVG writes runs as tspans, a rainbow path as a
textPath, and the whole typeset asdata-typeset, which the importer reads first; another editor's styled tspans come in as runs, atextPathas Path Typing, and a vertical writing mode as vertical text. The PDF draws each glyph as its outline (Type: glyph outlines, below), or sets each stretch in Helvetica, bold and oblique where a run is; the Illustrator script makes point, area and path type and sets each run's characters and each paragraph; the PNG draws the layout. - In a script,
characterandparagraphset the character and paragraph attributes of text after them until areset, andtextgainsinside "<path data>",along "<path data>"withstart,flipandeffect,vertical,align justifyandruns (...). A page's text is written back the same way. - Underneath.
src/core/arc-length.tsmeasures paths by length for Path Typing, and Split, Liquify and Mesh Warp take its Bezier evaluator;src/core/type/hyphenate.tsholds the patterns. The check hooks gaintypesetOf(markId).
npm run gui-check -- typeruns it.- One layout for every output.
Type: the editor. Text is typed and edited where it stands on the canvas, as in a vector editor, for every kind of type: runs, Sheet Typing in a frame, Path Typing, vertical text.
- The canvas draws it all: the text from its layout, and over it the caret, the selection's highlight, an input method's composition, underlined, and a box's frame or a path as a faint guide. A hidden text field takes the keys, the input method and the clipboard, and is what a screen reader reads, named "Text:" and the first words.
- Editing begins with a click of the Text tool, on empty paper for a new text or on a text to edit it, or with a double-click on a text with the Select or Direct Select tool; the caret goes where the click was.
- The caret stands on the line, as tall as what typing there would set, across a path or a column, and keeps its place through a pan, a zoom or a resized window. It blinks at Windows' rate.
- Selecting: Shift with a click or an arrow key, a drag, a
double-click for a word, a triple-click for a paragraph,
Ctrl+Afor all. The arrows move by character and line in the order the text is read,Ctrlby word and paragraph;HomeandEndgo to the line's ends and, withCtrl, the text's. Entermakes a new line.EscorCtrl+Enterputs the text down, as do another tool, a click off the text with a selection tool, and aCtrl-click elsewhere through the heldCtrl's selection tool.- A burst of typing is one undo step: it ends after a second with no key, at a caret jump, at an editor key or when the text is put down. A new text is on the page from its first character, in that step, and one put down empty leaves nothing.
- The clipboard: a copy carries the text and its styles
(
text/x-napkin-type) beside plain text; a paste of it keeps the styles, anything else takes the style at the caret, andCtrl+Shift+Vpastes plain. Edit > Cut, Copy, Paste, Delete and Select All act on the text while it is edited. - The editor keys, as a vector editor's: size by 2 pt (10 with
Alt), leading, kerning or tracking by 20 (100 withCtrl), baseline shift, alignment, All Caps, horizontal scale back to 100% and spacing back to none. With only the caret they set what the next typed text takes, and the caret shows its size. - Underneath.
src/core/type/caret.tsis the caret model with no DOM;src/renderer/type-editor.tsthe editor. The store gainssetTypeset,addStrokeLive,removeMarkandonPageLeave; the layout's glyphs say where a caret before them stands; the bridge gainsreadClipboardText; the check hooks gaintypeEditorState(). The new guide page Type and the Cheatsheet's Text editing table list the keys.
npm run gui-check -- type-editorruns it.Type: the Type panel. A vector editor's Character and Paragraph panels in one, a tab each: View > Toggle Character Panel (
Ctrl+Shift+T) and Toggle Paragraph Panel (Ctrl+Alt+T), which work while a text is typed.- Character: the font family, searched as it is typed, each name in
its own face and the fonts chosen lately first, with a button that reads
the fonts again; the face; size; leading - Auto, a length, or a share of
the size such as
140%; kerning - Auto, None, or by hand at the caret; tracking and word spacing; vertical and horizontal scale; baseline shift and rotation; All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; horizontal or vertical; and the color. - Paragraph: the alignment; bulleted and numbered lists, each with its
style - a bullet, or
1.,a.ori.- and its level; the left, right and first-line indents; the space before and after; hyphenation. - What it changes, a vector editor's rule: the selected characters of a text being typed, or with only the caret what is typed next; the texts selected with a selection tool, all of each; and with none, the next text's font, size and color. A field they differ in is blank.
- Fields take a value with its unit -
12pt,3mm,16px- or a bare one in points; Up and Down step them, Shift by ten, and a drag on a field's name changes the value as it goes and is one undo step when let go. Every change is one undo step. - It comes up by itself while the Text tool is in hand and a text is selected or being typed, and goes when another tool is chosen; the new Verbose Settings section Type has Show the Type panel with the text tool (on) to turn that off, and Font for new text, the last font chosen in the panel.
- Fonts are this computer's, read once a session with the Local Font Access API. A book whose text uses a font that is not installed opens with a report - "1 font is not installed" - a row a font, each with Find text, which selects the texts that use it; they are drawn in a default face and keep their font.
- The swatches and the color well color the text being typed, and the texts the Text tool has selected, as they color the Select tool's.
- Underneath.
src/core/type/panel.tsis the panel's model, with no DOM;src/renderer/type-panel.tsthe panel andsrc/renderer/font-list.tsthe fonts. The store gainssetTypesets; the type editortarget(),setTypingAttrsand achangedcall; the settingstypePanelAutoShowandtextFont; the check hookstypePanelState()andfontList().
npm run gui-check -- type-panelruns it.- Character: the font family, searched as it is typed, each name in
its own face and the fonts chosen lately first, with a button that reads
the fonts again; the face; size; leading - Auto, a length, or a share of
the size such as
Type: the typing modes. The Text tool reads what it clicks and what is selected, as a vector editor's Type, Area Type and Type on a Path tools would between them. Only a selected path turns into type: a path the tool merely clicks on stays a path, and the text is typed beside it.
- Sideless Typing: a click on the canvas, or on art that is not selected, types a text that grows as it is typed.
- Sheet Typing: a click inside, or on, a selected closed shape sets the text in it, each line as wide as the shape is where the line falls; a drag on the canvas sets it in a new rectangle the size of the drag.
- Path Typing: a click on a selected open path sets the text along it,
from the click.
Altswaps the two: along a selected closed shape's outline, or in the region a selected open path closes. - Paste on path:
Ctrl+Vwith the Text tool, a path selected, text on the clipboard and no text being typed sets the clipboard's text along the path, from its start. - The pointer shows what a click makes: an I-beam, an I-beam in a dotted box over a selected closed shape, or an I-beam on a wavy line over a selected path.
- The shape or the path goes into the text with its first character, in the same undo step. A shape's fill and stroke are the frame's own, drawn under the text; a path's are dropped, as a vector editor drops them.
- Direct Select reaches a text's frame or path, its anchors and handles, as it reaches any path's: the text reflows, and its characters keep their size.
- Brackets. A text along a path, selected alone with the Select tool, shows its start, center and end brackets. The start and the end dragged along the path set where the text runs, and the center dragged across the path flips the text; each drag is one undo step.
- Text that runs out of room shows a red plus in a small box, at a frame's lower right corner or at a path text's end bracket.
- Spacing: a path text's characters drawn together round a sharp turn, by the path's curvature there, as a vector editor's Type on a Path Spacing does.
- Underneath.
src/core/type/tool.tsis the Text tool's decision table and the brackets, with no DOM; the type model gains a path'sspacingandscaleTypeShape; the storereplaceMark; three cursors are new insrc/assets/; and the check hooktypeShapes()reads the frames, paths, brackets and red plus the canvas marks.
npm run gui-check -- typing-modesruns it.The Type menu. A top-level menu after Effect, as a vector editor has:
- Font, the Type panel on Character with its font field ready to type a name in;
- Size, a vector editor's sizes from 6 to 72 pt, the text's own checked, and Other, the panel's size field;
- Character... and Paragraph..., the Type panel on that tab;
- Type on a Path > Type on a Path Options... - the effect, Align to Path, Spacing and Flip - and Flip;
- Type Orientation > Horizontal and Vertical, the text's own checked;
- Area Type Options... - Inset Spacing, Auto Size, and the frame's own fill, stroke and stroke weight.
The rows act on what the Type panel acts on, and grey when there is nothing for them. Both options forms preview as they are set, keep their change as one undo step on OK, and on several texts change only the fields changed. Track History names a change for the row that made it.
src/core/type/options.tsholds the forms' fields, with no DOM, andsrc/renderer/type-options.tsis the form; the check hookstypeOptionsState()andsetTypeOption(key, value)read and set it.Type: glyph outlines and Expand Text to Shape. Where a text has to be shapes, it is drawn as its font's own outlines, read from the font's file on this computer.
- The font reader is
src/core/type/sfnt.ts, with no DOM: TrueType and OpenType files and font collections; TrueType's quadratic outlines, composite glyphs among them, raised exactly to cubics, and CFF's cubic ones, CID-keyed fonts among them; the character maps of Unicode and of symbol fonts; thekerntable's pairs; and a variable font's default instance. Its outlines match fontTools' point for point on ninety-odd installed faces. - The outline engine is
src/core/type/outline.ts:outlineText(text, layout, faces)gives the shapes a text is drawn as - a shape a glyph, its counters holes, in its run's color, where the type layout set it, and the frame's own paint, the highlights, the list markers, the underlines and the strikethroughs - or why it cannot: a font not read yet, one with no glyph for a character, or a file it does not read. The app reads installed fonts through the browser's local font access, a headless render the font files it is given. - Expand Text to Shape (Type > Expand Text to Shape, Transform >
Expand Text to Shape,
Ctrl+Shift+O), a vector editor's Create Outlines: each selected text, or the one being typed, becomes a group named after its first words, a shape a glyph on a layer named after its letter, with the text's effects, blend, opacity and place in the layers, as one undo step. A text whose font cannot be read stays text, and a report says which font and why, with Find text to select it. - Warp, Splatter and 3D on text. They work on a text through its glyphs, and the text stays text, typed and set as before: a Warp bends them all over one box, a Splatter hatches each in its own color, and 3D builds every glyph as one solid in the text's first color. The text being typed is drawn plain, so its caret stands where its characters do, and a group's Warp bends a text it holds with the rest. Where the font cannot be read, the text is drawn without the effect, and a report says why. Expand Appearance bakes it into shapes; the SVG keeps the text and its effects, and napkin-sketch reads them back live.
- Scripts and the API.
napkin-sketch drawandrendertake--font <family>=<file>, repeated for more families and faces;drawFile,drawToFiles,renderSketch,renderBookandwriteBooktakefonts, andloadFonts(fonts, from)reads them from paths. Text in those families is laid out by the fonts' own advances and kerning, and a PNG and a PDF draw its glyphs as shapes; text in a family not given is drawn in the built-in face, and the report says so. - The check hook
pagePdf()gives the page as the PDF export writes it, with what it left out.
No shaping is done - no ligatures, and no scripts whose letters join - and a bold or an italic the family lacks is drawn in its nearest face rather than made up. The test fonts in
test/fonts/are subsets of two fonts under the SIL Open Font License, renamed as it asks.npm run gui-check -- outlinesruns it.- The font reader is
Type: Text Configurations. A vector editor's Character Styles: named sets of character attributes kept in the book, which runs of text follow. A run's attributes are the text's base, then its configuration's, then its own - its overrides - so a configuration changed changes every text that follows it, on every page.
- The Type panel's third tab, Configurations, and Type > Text
Configurations...: the Default Configuration, a text's own attributes,
and the book's configurations, each name drawn in its own style, with a
+after the one the text follows when it has overrides. A click applies one to the selected characters, the caret's next typing or the selected texts, keeping the overrides; Alt and a click clears them. - Its menu, and its New and Delete buttons: New Text Configuration,
from what the selected text has beyond its base, which the text then
follows; Duplicate; Delete, which asks of text that follows it whether
to Break Link, keeping its look, or Revert to Default; Redefine, from
the selected text; Text Configuration Options..., its name and each
attribute it sets, blank where it sets none, with Preview; Clear
Overrides; Break Link to Configuration; and Load Text Configurations...,
another book's, merged by name, one that clashes numbered
Callout 2. - One undo step each. A change to a configuration reaches the text on every page, and its undo step holds every page it reached.
- Where they live.
SketchBook.textConfigs, and in each text a copy of those it follows (TypeSpec.configs), so a text looks the same pasted into another book, exported or rendered alone. A text pasted or imported follows the book's configuration of its name, or brings its own into the list. - Out and back. The SVG keeps the link and the definition in
data-typeset, and napkin-sketch reads them back. The Illustrator script makes each one a Character Style of its document and applies it, then each run's own attributes. A script defines one withtextconfig "<name>"andcharacter's clauses, with a size, a family and a color, and a text or a run follows it withconfig "<name>"; a name none defined isunknown-config, and Generate Script writes each one before the first text that follows it. - Underneath is
src/core/type/configs.ts, with no DOM:resolveRunAttrs,overridesOf,applyTextConfig,captureConfigAttrs,mergeTextConfigs,adoptTextConfigsandsyncBookConfigs, among the rest. The check hookstextConfigs()andloadTextConfigs(path)read and load them.
npm run gui-check -- text-configsruns it.- The Type panel's third tab, Configurations, and Type > Text
Configurations...: the Default Configuration, a text's own attributes,
and the book's configurations, each name drawn in its own style, with a
Type: Markdown Flow. A text typed as Markdown and shown as it renders, with no HTML made or read: headings, strong, emphasized and struck text, code and code blocks, lists nested and numbered, task lists, quotes, rules, links, a picture's words and tables. The Markdown is the text, kept and saved as typed; what it shows is made from it.
- Type > Markdown Flow (
Ctrl+Alt+M) makes the selected text, or the one being typed, Markdown Flow, its characters its Markdown, and is checked while it is. Off, it asks, and the text keeps what it shows as plain text. New text starts in Markdown Flow, in the Type section of Verbose Settings (off to begin with), starts every new text so. - Typing. The block the caret or the selection is in shows its Markdown, its own characters muted, and every other block renders; the caret keeps its place through both. Enter carries a list or a quote on, ends an empty item, and is a newline alone in a fenced code block. A single Enter is a line break. Esc renders everything.
- Restyled through Text Configurations. Headings, code, quotes and links follow Markdown Heading 1 to 6, Markdown Code, Markdown Code Block, Markdown Quote and Markdown Link, which the book takes in with its first Markdown Flow text. A heading's size is its level's share of the text's.
- Out and back. Every export draws every block rendered, wherever the
caret is: the SVG with the Markdown in
data-textanddata-typesetto read back, the PDF and the PNG as drawn, and the Illustrator script as the rendered text and its styles. A script makes one withtext "<markdown>" at <x> <y> markdown; Generate Script writes the flag, and a Markdown configuration restyled as atextconfig. - The API.
markdownToType(source, theme, options)gives the display text, its runs and paragraph attributes and the source map, read with the lexer it is given, so the core imports no Markdown reader. The new entrynapkin-sketch/markdownregisters marked's lexer withnapkin-sketchand reads with it; the command line andnapkin-sketch/noderead with it already.MARKDOWN_THEME,setMarkdownLexer,markdownEnterandwithMarkdownConfigsare exported beside it. - Underneath is
src/core/type/markdown-flow.ts, with no DOM. The layout lays out the display, drawing a quote's bars and a rule from two new paragraph attributes,barandrule, andlayoutForEditinggives the editor each glyph placed in the Markdown. The check hooktypesetOf(markId)adds what a text shows, its shades and its markers.
npm run gui-check -- markdown-flowruns it.- Type > Markdown Flow (
Type: Clean Language Accents. Code colored as its language reads it - keywords, strings, numbers, comments, names and types - by highlight.js, whose HTML is read into colors and never drawn as HTML.
- In Markdown Flow, a fenced block that names its language is colored as it is typed. One that names none is plain unless Detect the language of unlabelled code (Verbose Settings, Type; off to begin with) is on; one whose language is not installed is plain, and while the text is edited a line under it says so.
- In any text, Type > Clean Language Accents > Apply... (
Ctrl+Alt+L) accents the paragraphs of the selected text as an installed language or Auto, Use a monospace font setting them in Markdown Code too; Remove Accents takes the accent off. One undo step each. The colors are worked out as the text is laid out, never stored, so they follow every edit. - Languages. The 36 of highlight.js's common set come installed.
Languages... lists all 193 in the configuration popup, the installed
ones checked, and installs the checked ones: each beyond the common set
is a script of the app's own in
accents/, loaded when it is installed. The settingaccentLanguageskeeps the list. - Themes. Napkin Light and Napkin Dark, every color at least 4.5:1 against its own code shade, follow each page's paper unless Code colors (Verbose Settings, Type) picks one; a Markdown code block sits on its theme's shade.
- Out and back. Exports draw the colors, and the SVG keeps each
paragraph's accent, so a text read back stays live. A script colors the
next text with
accent <language>- an id, a name it goes by, a name in quotes,autoornone- andunused-accentsays when nothing takes one; Generate Script writes it back. - The API.
accentRuns,hljsHtmlToRuns,ACCENT_LANGUAGES,ACCENT_THEMES,setAccentHighlighterand their kin are exported, andnapkin-sketch/markdowngives the API highlight.js with the common set andregisterAccentLanguagefor the rest. - Underneath is
src/core/type/accents.ts, with no DOM and no highlight.js: the catalog (npm run accent-languageswrites it from the installed highlight.js), the themes, the HTML reader, the highlighter registry andaccentShown, which the layout colors a text through. The check hooktypeEditorState()adds the lines said under the text.
npm run gui-check -- accentsruns it.Composition Styles. Rules keyed by layer name that restyle a page in one click: a style says, for each layer it names, what to set there.
- Three kinds of rule. A layer rule sets the layer's opacity, blend, Isolate Blending, visibility and effects, and a fill and a stroke color for the marks under it. A text rule sets the text under its layer: any character or paragraph attribute, a Text Configuration by name, Markdown Flow and a Clean Language Accent. A stroke rule sets its paths. A name matches trimmed and in any case, a rule reaches every layer under its own, the nearer rule wins, and what a rule leaves out stays as it was.
- The Composition Styles panel (
Shift+F5, View > Toggle Composition Styles Panel) lists your styles, each with a thumbnail of the page as it would make it, and a click applies one as one undo step. New records the page's named layers as a style, which applied again changes nothing; Duplicate, Delete, Import, Export and Edit sit beside it. Edit opens the rules in the configuration popup, a row a rule, each rule's fields in a form of their own. - The report. A rule that matched no layer is said at the bottom right with the nearest layer name, two letters swapped counting as one slip, and Rename layer gives that layer the rule's name and applies the style again, one undo step for both. Text rules on layers with no text, stroke rules on layers with no paths, configurations the book does not have and values held to their range are said too.
- The file. A style is JSON, kept one file a style in the app's own
folder, so every book can use it. A JSON Schema of it is in
docs/api/schema/. - Everywhere else.
napkin-sketch draw --composition-style <style.json>restyles every page a script draws, a rule that matched nothing being a warning. The API exportsapplyCompositionStyle, which restyles a page, or a graphic-design composition by its elements' names;captureCompositionStyle;validateCompositionStyle, every reason a style is not one a sentence;nameSuggestionsand their kin. The embedded editor gainsapplyCompositionStyle(style). - Underneath is
src/core/composition-style.ts, with no DOM. The configuration popup gains a field over its table, buttons in a row, names a text cell suggests, and rows added and taken out as it is used.
npm run gui-check -- composition-stylesruns it.Read-outs for the GUI checks of effects and panels. The
window.napkinCheckhook, which the page puts up only when the app is started for a check, gains two:effectsOf(markId)(a mark's effects, and those of its layer and of every group above it, nearest first) andpanelState(name)(whether the pages, layers and properties panels, Quick Settings or the dock column are open and where they are drawn; and for a popup, whether it is open, docked or placed, its place in the dock and its box). The popup manager gainsdescribe(dialogId), which the second reads. Nothing changes for anyone using the app; the checks of the panels that come next read them.The API reference, and the AI helpers, cover the release. The engines page gains a section, each with an example that runs, for blending, the appearance stack, effects as pixels, Splatter, Warp, 3D, measuring along a path, setting type, fonts as shapes, Text Configurations and composition styles on a page. The drawing page gains the blend modes, the
characterandparagraphclauses and a list; the compose page gains blend modes, Isolate Blending and effects in its shared table; and a tenth category, Markdown Flow and Clean Language Accents, coversnapkin-sketch/markdown. The barrel exports the engines by name: the appearance stack and its pixel passes, Splatter, Warp and 3D, the 3D gumball's sums, arc length, the type engine with its font reader, and Text Configurations; andnapkin-sketch/nodegainsreadCompositionStyle. The napkin-script and graphic-design-api skills say what the language and the composition API now do, and no longer say there are no gradients, filters or shadows.npm run pack-checkcalls the new engines from the installed tarball, withnapkin-sketch/markdown, draws with--composition-style, and fails should the package ever name a dependency.Two golden scripts take in the release.
every-verb.napkingains every effect, the blends by napkin name and before a group or a clip, and every way of setting text: a box, a path's flip and effects, a whole text on a Text Configuration, Markdown, letters as marks, every character and paragraph clause, both kinds of list and the three ways to name an accent. A newtype-and-appearance.napkindraws a poster - a headline, a column of justified, hyphenated body, type along a path, a warped banner, an extruded block, a blended band and a glowing badge - to a golden SVG and a golden PNG, compared a pixel at a time within a step. The script writer writes both back to the same drawing.Help > About napkin-sketch. A row of its own at the foot of Help opens the About page in the documentation window: the version, the license, and a link to the third-party notices.
Third-party notices.
THIRD-PARTY-NOTICES.mdholds the license of each package and data file napkin-sketch bundles -marked,marked-highlight, highlight.js and the en-US hyphenation patterns - and points at the test fonts' license, which only the repository carries. The npm package and the desktop app ship it, the documentation site has it as a page, and the README and the About page link it.
Changed#
A PDF draws text as shapes. Each glyph is its font's outline, so the page looks as it does on the canvas wherever it is opened. A text whose font cannot be read is set in Helvetica, as all text was, and the report after the export says which font and why. Draw text as shapes in PDF, in the Type section of Verbose Settings (on to begin with), turns it off, for text a reader can select and search;
textAsShapes: falsedoes insketchesToPdf.A PDF fills a closed vector shape along its curves. A filled shape drawn with Bezier anchors was filled along a polyline through samples of them, and is now filled along the curves themselves, as its outline is drawn.
A tool may stand in two menus. Expand Text to Shape is a row of both Transform and Type, since both take its type; the row greys and runs alike in each, and its shortcut is claimed once.
Text is set by the type layout. What was drawn as one string a line is now set character by character, and a few things land differently:
- a word longer than its box breaks where the box ends, as a vector editor's area type breaks it, rather than running past it;
- a text mark's box, and what picks it, are the layout's: its lines' boxes, a line and a quarter of its size each as before, and for Path Typing its characters and its path;
- the SVG sets each line on its baseline and carries the text as typed and
its anchor as
data-textanddata-anchor, where it hung each line from its top; the PDF places each stretch with a text matrix; and the Illustrator script places point text on its first baseline; strokeBounds(stroke, measure)takes a type measure in place of a function that measured a text item's box; with none, the built-in face's.
Scripts align text by its paragraphs.
text ... align centerkeeps its anchor where it is put and centres its lines on it, as point type does, where it moved the anchor to the line's left end. What is drawn is the same, and the app keeps it centred as it is edited.Sketch books are version 4. A text mark may carry a
typeset, and a book itstextConfigs; files of versions 1 to 3 open unchanged, and a file a newer napkin-sketch saved opens with a report that some of it may not show.Two flags that set one field cannot both be given in a script, such as
superandsub: the parser says so.activeEffectsgives the raster effects alone. It leaves out the geometry effects, which change what a mark draws rather than its picture, as every raster path wants;activeGeometryEffectsgives them.Effects follow their mark through a transform. While Scale Strokes and Effects, a new setting in Verbose Settings > Sketch Support, is on, as it is to begin with, a scale scales a mark's stroke width and its effects' lengths by the mean of its two scales, and a turn or a mirror turns or mirrors a shadow's offset and a bevel's or an emboss's light, so the light still falls on the same side of the mark. Off, a transform leaves the width and the effects as they were. Before, a scale scaled the width and none of the effects, and a turn or a mirror left a shadow falling where it fell.
A run of paragraphs in one shade is one shade. Paragraphs one after another with the same background and no space between them share one, as wide as their widest line or the frame's inside, where each had its own.
Type > Markdown Flow and Clean Language Accents > Apply... claim their chords in the menu bar, as Expand Text to Shape does, so
Ctrl+Alt+MandCtrl+Alt+Lreach the text being typed.The Type panel has three tabs, Character, Paragraph and Configurations, and its tab keys go round all three.
The menu bar has eleven menus, Effect and Type among them, and Help > Tool Types lists Effect and Type after Automate. The Edit Keyboard Shortcuts and Edit Tool Types popups offer the Effect and Type types beside the others.
Three packages for Markdown and code coloring, bundled at build time.
marked18.0.14,marked-highlight2.2.4 andhighlight.js11.12.0 join the devDependencies, pinned to exact versions because esbuild inlines what the code imports intodist/rather than npm installing it beside the app. The published package still has no dependencies to fetch. Markdown Flow reads withmarked's lexer, and Clean Language Accents withmarked-highlightand highlight.js.The text box is gone. The textarea laid over the canvas for typing is replaced by the editor:
Entermakes a new line rather than putting the text down,Esckeeps what was typed rather than throwing it away - Undo does that now - and a text is no longer trimmed of the spaces it was typed with.Undo is offered while an edit is held open. A burst of typing, or a preview, that has changed the page counts as a step to undo before it is kept, so Edit > Undo and the toolbar's button no longer grey out in a new document's first second of typing.
A numbered list counts the way its style says. A list's marker of
1.,a.ori.sets how a numbered list counts; with none, its level does, as before.Typed text no longer takes its neighbour's kerning by hand. Kerning by hand is between two characters, so a character typed beside a kerned one starts with none.
Ctrl+Shift+TandCtrl+Alt+Ttoggle the Type panel, from a text field too, where before the window swallowed them as a browser's new-tab key. A chord the menu bar claims - a panel toggle, Save - runs from the text being typed when the text does not take it.A drag with the Text tool draws a frame that keeps its size, as a vector editor's area type does: text that does not fit shows the red plus. A drag too flat to hold one line still gives a box as wide as the drag that grows down to fit.
The Transform Box scales a text's frame or path, not its text. A text in a box, in a shape or along a path keeps its size when scaled, and reflows; a text that grows as it is typed scales its characters, as before.
Fixed#
An imported group's own marks took its effects twice. The marks a file keeps on a group itself go on a layer of their own inside it, and that layer was given the group's effects as well as the group: a group's drop shadow fell a second time under them. The layer now takes none, as it takes none of the group's opacity.
The Properties panel showed the gradient editor for every selection. Its ramp, stops and angle stood under Fill for a flat fill too, with nothing to edit, because the stylesheet had no rule to hide it. It shows only for a gradient fill now, as the code meant it to.
The canvas is measured again when the dock column's width lands. The dock opens and closes over 160 ms, as the side panels do, and the canvas was measured only as it began, so it could be left sized for the stage as it had been. It is measured again when the width settles, as it already was for the side panels.
A text could not be edited again. A click with the Text tool, or a double-click with the Select or Direct Select tool, opens it with the caret at the character clicked.
The text box ignored the size and the canvas. Its caret kept one size whatever the text's, and it stayed where it was when the window was resized or the canvas panned or zoomed. The editor's caret does both.
A character set apart said its font twice. A scaled or slanted character - Path Typing's skew, or a run's own horizontal or vertical scale - is a
<text>of its own in an SVG, and it carried the text's size, family and color and then its own over them: the same attribute twice, which a strict XML reader refuses. It says each once, its own, and every golden SVG is held to saying no attribute twice.A list's own bullet or numbering was left out of a written script without a word. One chosen in the Type panel has no word in the language yet; the script writer writes the list by its kind and level, as before, and its notes now say what it left out.
[1.0.0-alpha.4.6.0] - 2026-10-01#
Pressing that always lets go, and the tools a vector editor reaches for next,
make this a minor release; it was planned as a 4.5.1 patch until the tools
joined it. A press no longer sticks and stops every tool, the Eraser cuts
what it covers with no mark or layer of its own, Select and Direct Select
pick the mark under the pointer, and translucent ink lies flat rather than
as a string of overlapping circles. Space and Ctrl give the hand and
the last selection tool with any drawing tool in hand, and View > Zoom In
and Zoom Out now zoom the canvas, in as far as one page pixel. New are the
Shape Eraser and Apply Erasers, Shift-click lines, the Vector Path's close
indicator, the Transform button, Previous and Next Page, and three settings
(Select sensitivity, Freehand fidelity and Wipe animation). So are the Shape
Stacker and the Wipe Stacks, a vector editor's Shape Builder and Pathfinder;
Split; clipping masks; a Pencil with a drawing class's kit, and the Smear to
shade with it; Liquify; and X and Shift+X for the fill and the stroke.
The Pen is now the Brush, on B, and Vector Path is on P. Each tool runs
on an engine the API exports and napkin script has a verb for, written up in
The drawing engines. The .skbk format stays
at version 3, and an older file's eraser marks still paint as they did.
Added#
Read-outs for the GUI checks of the input and the marks. The
window.napkinCheckhook, which the page puts up only when the app is started for a check, gains five:inputState()(the pointer a press owns, every per-press field still set, the held keys and the tool),strokeSummary()andlayerRows()(the page's marks and layers),selectionBoxes()(the dashed boxes around the selection) andviewState()(zoom, pan and the zoom limits). The canvas now paints the selection's boxes from the same function the hook reads, so a check reads what is on the screen. Nothing changes for anyone using the app; the checks that come next use them to prove that a press always lets go of the pointer.Select sensitivity. A setting in Verbose Settings > Sketch Support: how far from a mark's ink a click with the Select tool may land and still pick it, in screen pixels, so it reaches as far at every zoom. 4 unless changed, from 1 to 20. Before, the reach was 8 page units, which no setting changed and which grew on screen with the zoom.
A Transform button in the toolbar, beside Mirror: it puts the Transform box round the selection, as
Ctrl+Tand Transform > Transform Box do, and stays pressed while the box is up. Its tooltip shows the shortcut, which follows Edit Keyboard Shortcuts like every other.Pages > Previous Page and Next Page, on
PageUpandPageDown. The page bar's arrows have turned pages since the first release, but the menus never had rows for it - the one gap a read of every Added entry in this log found against the menus 1.0.0-alpha.4.5.0 generated. Each row is greyed at its end of the book, and the arrows' tooltips show the keys.Freehand fidelity. A setting in Verbose Settings > Sketch Support: how far a Brush, Marker or Copic stroke's fitted curves may stray from what was drawn, in screen pixels - 1.5 unless changed, from 0.5 to 8. Smaller keeps more of the hand's detail, in more anchors.
The Shift-click line. Click or draw with the Brush, Marker or Copic, then hold
Shiftand click: a straight line joins the end of what was drawn to the click at once, as part of the same mark - so a translucent line does not darken where the two meet - and each Shift-click after it adds another, a polyline, one undo per line. A Shift-press that goes on as a drag carries on freehand from the click, andSpaceorCtrl + Spaceduring it gives a straight line or a quick curve from there. The line starts a mark of its own instead when the mark is painted otherwise than the tool paints now, is on another layer, is closed, or symmetry copies are being made; a page turn, an undo or redo, another tool taken in hand or the mark moved forgets where the next line would start (src/renderer/held-keys.ts).Shiftwhile placing a Vector Path. It keeps the next point, and the rubber band that previews it, level, plumb or at 45 degrees from the last point, and a handle being pulled out of a smooth point to the same eight directions about that point - a horizontal, vertical or 45-degree tangent. Going down or up, it moves the band or the handle at once.The Vector Path close indicator. Once a path has two points, the pointer within reach of the first - the Direct Select sensitivity, 8 screen pixels unless changed, which is where a click has always closed it - shows
src/assets/close-path-indicator.svgon that point, 20 screen pixels across, and the rubber band ends there, previewing the closing segment. A click closes the path,Shiftor not.Sketch > Apply Erasers. A file made before this release may hold the old Eraser's marks. They still paint as they did; Apply Erasers turns every one into the cut it paints - each cut out of the marks before it on its own layer, what the canvas showed - and takes it away, in one undo step. An eraser mark on a locked or hidden layer is left as it is.
Erasing as geometry, in the core.
src/core/boolean.tsjoins, subtracts and intersects regions under either fill rule - the stroke profiles' boundary engine, lifted out with a second operand, their outlines the same to the last digit - andsrc/core/erase.tssays what an eraser, a swath or a closed shape, leaves of each mark, as plain marks with no mask: a filled shape, a Copic stroke or a profiled one less the eraser's region, its surviving outline cut from its own curves and the eraser's edge fitted; a plain line taken away wherever its painted width would reach into the region, what is left the subpaths of the same line. The Eraser is built on it next.The wipes, in the core.
src/core/wipe.tscombines marks as regions, as a vector editor's Pathfinder does: Wipe In (the union), Subtract Top from Below, Subtract Below from Top, Mid Wipe (where all of them overlap), Outer Wipes (where an odd number do) and Clean Wipe (every face of their arrangement, a mark each). A shape - filled, or a closed outline - is its inside, and a line, a Copic stroke or a profiled one its painted ink; a result keeps the paint of the mark it is painted as, an ink's as a filled shape in its colour, and what the wipe never reached keeps its anchors exactly, the rest fitted as the Eraser fits a cut. The page takes a wipe as one undo step, each face of a Clean Wipe on a layer of its own. The Wipe Stacks are built on it next;wipeMarksandarrangeFacesjoin the API, and so do the boolean engine (booleanOp,booleanRegions) and erasing (eraseMarks,eraseRegionOf).The Shape Eraser (
Shift+E, beside the Eraser). Select the marks to cut, choose a Rectangle, Ellipse, Square or Circle from the panel that opens under its button, and drag it over them: what it covers is cut out of every selected mark by the Eraser's rules - the cut already showing before the release - in one undo step, with no mark or layer added. Top Path, across the bottom of the panel, cuts the other selected marks with the topmost closed one and takes it away.Shiftsquares a Rectangle and rounds an Ellipse; the arrow keys,EnterandEscapework the panel.Notices that say why nothing happened. With nothing selected, only the one path, or an open path on top, the Shape Eraser says so in a small dialog with Do not show this notice again; ticked, the notice is a toast for the rest of the session, and a new session shows it again.
npm run gui-check -- shape-eraserruns the Shape Eraser and its notices.Fill and stroke, and
X. The color well in the toolbar is a fill and stroke control: the fill box and the stroke box, overlapping, the one in front being the one the colors paint, as a vector editor's toolbar has them.X(Sketch > Fill in Front) puts the other one in front, and so does a click on the box behind; a click on the box in front opens the color picker.CandShift+Cstep the one in front through the Quick Access Colors - the fill's steps take in None - and a swatch or the picker sets it. A new Rectangle, Ellipse or closed Vector Path is filled with the fill, and the Paint Bucket and Fill Color paint with it; the fill is none until one is picked.Shift+X(Sketch > Swap Fill and Stroke, or the arrow at the control's corner) swaps the two: the selected closed shapes' fill and outline, or with nothing selected the tool's ink and fill.paintPatchandswapPaintjoin the API.npm run gui-check -- fill-strokeruns it.The Wipe Stacks (Transform > Wipe Stacks, after Close Shape, and the canvas's right-click menu). Six rows combine the selected shapes as Illustrator's Pathfinder does: Wipe In unites them, Wipe Out > Subtract Top from Below and Subtract Below from Top take one from the rest, Mid Wipe keeps where all of them overlap, Outer Wipes where an odd number do, and Clean Wipe cuts every piece of their overlaps into a shape of its own, each on a layer of its own. The rows are greyed until two shapes are selected, and have no keys, as Pathfinder's have none. A wipe is one undo step, made at once; then a paper-coloured napkin wipes across the result in a quarter of a second, the picture from before ahead of it. A press, a key or the wheel ends it, reduced motion skips it, and Wipe animation in Verbose Settings > Extras turns it off. In the right-click menu Wipe Out's two rows are laid out in the Wipe Stacks panel, since the window opens one panel beside a menu. Scripts get the block verb
wipe in|out-front|out-back|mid|outer|clean { ... }, which combines the marks its block draws and draws what is left in their place, with the warningswipe-skipped,wipe-emptyandwipe-failed.npm run gui-check -- wipe-stacksruns it, withwipeState()for the check.The Shape Stacker (
Shift+M, beside the Shape Eraser). A vector editor's Shape Builder: with two or more shapes selected it sees their pieces - every place where they overlap and every place where they do not- and shades the one under the pointer with a light mesh. A drag across
pieces merges them into one shape, painted as the topmost shape where the
drag began, each shape keeping the rest; a click makes one piece a shape of
its own;
Altat the press - the cursor's plus turns to a minus - takes the pieces away from every shape instead;Shiftdrags a box that takes every piece it touches. One undo step each,Escapedrops a drag, and a shape's own curves are kept wherever a piece runs along them. Its panel holds the Wipe Stacks as six tiles, greyed with fewer than two shapes selected, and with fewer than two a press says so in a notice. Scripts getstack merge|remove <points> { ... }, which stacks the marks its block draws at the pieces under its points, with astack-missedwarning for a point on none; the wipe's warnings cover it too.stackFaces,stackArrangement,stackEdit,faceAt,facesAlongandfacesInBoxjoin the API.npm run gui-check -- shape-stackerruns it, withshapeStackerState()for the check.
- and shades the one under the pointer with a light mesh. A drag across
pieces merges them into one shape, painted as the topmost shape where the
drag began, each shape keeping the rest; a click makes one piece a shape of
its own;
Split (
J, beside the Shape Stacker). A vector editor's Scissors: a click on a path cuts it where it lands, and nothing moves. An open path becomes two, the second piece on a new layer just above; a closed one opens there, one open path starting and ending at the click, which a second click divides; a shape with holes gives up the ring that was clicked, as an open mark of its own. A click on an anchor cuts there and adds none; anywhere else a curve is divided by de Casteljau, so both pieces trace it exactly, and a freehand stroke's pressure carries through the cut. The pointer is a pair of scissors, a small ring marks where a click would cut, and both pieces keep every paint property, selected, in one undo step. Text, pictures and an older file's eraser marks say "Split cuts paths and lines." Scripts getsplit <x> <y>, which cuts the topmost mark drawn so far within 4 px of the point, with asplit-missedwarning when there is none.nearestOnMark,splitMarkandsplitTargetjoin the API.npm run gui-check -- splitruns it, withsplitState()for the check.Clipping masks (Layers > Clipping Mask,
Ctrl+7andCtrl+Alt+7). As a vector editor's Make Clipping Mask: the layers the selection is on go into a Clip Group that shows only inside the closed path on top, its clipping path, which paints nothing while it clips and moves to the top of the group. What is hidden cannot be picked or caught in a selection box, and the dashed boxes box what shows; Direct Select still reaches every mark. Release takes the clip off and keeps the group, the clipping path painting again; each is one undo step. One mark selected, or an open path on top, gives a notice. The layers panel marks the group with a ◘ and the clipping path with a clip badge. A clip group can hold another, and a clip group's effects come after its clip. The SVG export clips the group with a<clipPath>, the PDF with a clipping path, the Illustrator script with a clipped group and the composition with the group'sclip; an SVG group with aclip-pathimports as a clip group, its<clipPath>'s shapes the clipping path. The file saves it as the group'sclip. Scripts getclip ["<name>"] { ... }, clipped by the last closed shape its block draws, with aclip-openwarning when there is none.canClip,clipIndex,clipMarkOf,clipRegionOf,clippedAt,makeClip,normalizeClips,releaseClip,shownBoundsandCLIP_GROUP_NAMEjoin the API.npm run gui-check -- clippingruns it, with each row'sclipinlayerRows()and the page's SVG frompageSvg()for the check.The Pencil (
N, after the Copic). A pencil from a drawing class's kit, modelled on a real one rather than on any editor's tool: its button opens the drawing kit - graphite 4H to 8B, charcoal pencils HB to 6B, vine charcoal hard, medium and soft, compressed charcoal 2B to 6B - each chip drawing its own lead;Ntakes it up, andNagain opens the kit. Each grade has a tone, graphite a cool grey that never reaches black and charcoal a warm matte black, and a lead a softer grade wears broader. Every line is drawn through the paper's tooth, a seeded tile fixed to the page: pressure darkens it far more than it widens it, and passes laid over one another catch the same tooth and build tone toward the grade's darkest, never past it. The ink color does not change a pencil; a recolored Pencil mark is a colored pencil. The canvas works out each mark's picture once a scale, and a live stroke only where it grew, so 500 pencil circles redraw in about the time 500 Brush circles take. The SVG fills each line with the grain, carrying the tooth once a file, and napkin reads it back as Pencil marks; a PNG has the canvas's pixels; PDF and the Illustrator script draw the mean tone. Files keep the mark'spencil, and an older app reads it as a Brush mark in its tone. Scripts gettool pencilandpencil <grade> [<color>].PENCIL_GRADES,PENCIL_KIT,pencilPaint,pencilCoverage,grainTile,rasterizePenciland their kin join the API.npm run gui-check -- pencilruns it, withpencilState()andrenderTime()for the check.The Smear (
Shift+N, after the Pencil). A blending stump for the Pencil's graphite: dragged over pencil marks - the selected ones, or with none selected every one it passes - it spreads their graphite along the drag, so tone moves from where it is thick to where it is thin, the paper's tooth fills in, edges soften, and hatching turns into shading. The stump trades graphite with the paper as density, so the darkness is kept. With the Smear in hand Quick Width sizes the stump and Quick Opacity sets its strength, and a stylus pressed harder smears more. Each mark keeps its pass (Stroke.smudges), cut to the part of the drag that reached it: no mark or layer is added, the mark's box grows to take in its graphite, and the pass moves, turns, mirrors, scales and warps with the mark - one undo step. Other marks are left alone, with a toast once a session. The SVG and the PDF write a smeared mark as its picture at twice the page's resolution, which napkin's importer reads back as the mark; a PNG has the canvas's pixels. Scripts getsmear <width> <strength> <points>, with asmear-missedwarning.smudgeBuffer,smudgeFor,mapSmudgesandpencilPicturejoin the API.npm run gui-check -- smearruns it, withsmearState()for the check.Liquify (
Shift+R, Transform > Liquify, or its button after Mesh Warp's). Illustrator's Warp tools, for every mark but a pencil's: a brush, drawn as a ring round the pointer, bends the marks under it. Its panel has four: Warp pushes the outline along the drag, like clay; Twirl turns what is under the brush about its centre; Pucker draws it in toward the centre, and Bloat pushes it out. Twirl, Pucker and Bloat keep working while the press is held, faster under a pen pressed harder. The bend falls off smoothly to nothing at the ring.Alt-drag sizes the brush, and so do[and]. It bends the selected marks, or with none selected every mark under it. Pencil marks are left to the Smear, which a toast says once a session; the Smear's own toast now names Liquify. Marks ride on Mesh Warp's carrier, so anchors and handles move and a segment is split only where it must be; a drawn rectangle or ellipse bends as a path, its straight sides too. At the release each bent mark is fitted again at the Freehand fidelity, so its anchors stay few; a straight side, or the part of one the brush never reached, stays dead straight, and a Vector Path the brush never split keeps its own anchors. One drag is one undo step, andEscapeduring it puts everything back. Scripts getwarp <x> <y> <radius> <dx> <dy>,twirl,puckerandbloat <x> <y> <radius> <amount>, with aliquify-missedwarning.liquifyField,liquifyMarks,refitLiquifiedand their kin join the API, and Mesh Warp's carrier takes anyPointMap.npm run gui-check -- liquifyruns it, withliquifyState()for the check.Six more read-outs for the GUI checks.
hitAt()gives the mark a Select click at a point would pick,directSelectState()what Direct Select is editing,strokeGeometry(id)a mark's points and anchors,lineStart()where the next Shift-click line starts,vectorPathState()the Vector Path being placed and whether a click would close it, andstrokeSummary()now gives where each mark starts and ends, its ink, and whether its outline is off.npm run gui-check -- vector-pathruns the Vector Path'sShiftand its close indicator.GUI checks in the background (
npm run gui-check -- --background). The checks can run while the computer is in use. Every window they open - the drawing window, Settings and the documentation window - is shown without the focus, past the left edge of every screen and out of the taskbar, where neither the mouse nor the keyboard reaches it, and it keeps drawing there. Each page is told it has the focus, and the drawing window's page is pinned to the size and pixel scale the checks were measured at - 1494 by 837 at 150%, whichNAPKIN_GUI_SIZEandNAPKIN_GUI_SCALEchange - so a check's numbers hold on any screen. The three held-keys sequences that watch a bareAlthand the real focus to the menu bar cannot run there; they are listed as skipped, and the summary counts them. A plainnpm run gui-checkruns as before.The drawing engines, written up. A ninth category of the API documentation, The drawing engines, with its quickstart and cheatsheet. It has a section for each engine the app's tools stand on, each with a worked example that runs as it is:
booleanOp,eraseMarks,wipeMarks,arrangeFacesandstackFaces,splitMark, the clipping helpers,PENCIL_GRADES,pencilPaintandgrainTile,smudgeBuffer, andliquifyFieldandliquifyMarks. There is also a helper that applies an engine's edit to a page.test/api-engines.test.tsruns every example, and checks it prints what its page says. The hub anddocs/api/INDEX.jsonlist the category, and the drawing reference gains an "Erasing" section. The scripting helper's skill knows the verbs that combine, cut, clip, smear and bend.A golden script of every verb.
test/scripts/every-verb.napkindraws with each of the language's 66 verbs at least once, on two pages, and the golden test fails when a verb is missing from it.npm run pack-checknow draws the new verbs and calls fifteen engines by name from the installed package.npm run test:graphic-design-apichecks them in the built API.
Changed#
- The Pen is called the Brush, on
B, with a brush for its icon, and Vector Path is onP. Nothing it draws changes, and nothing saved does: files, scripts, settings and shortcut files still name itpen, and a script'stooland the embedded editor'ssetTooltakebrushfor it. New marks land on layers named "Brush N"; a file's "Pen N" layers keep their names. A key you gave either tool yourself stays yours.npm run gui-check -- brushruns the rename. - A color picked with a selection paints the one of fill and stroke in
front. Fill Shape filled the selected closed shapes and recolored the
rest, whatever was meant. With the stroke in front - as the app starts - a
swatch or the color picker recolors every selected outline and text, and a
shape with no outline gains one; with the fill in front (
X) it fills the selected closed shapes, in place of a gradient, and leaves open lines as they are.npm run gui-check -- color-pickerruns both. - A tool chosen during a press takes over once the press is released. A
shortcut key pressed during a drag waits for the release, and so does the
drawing tool
Ctrlgives back when it comes up during a drag begun with the selection tool it lent (below): the drag finishes with the tool it began with. A press also cancels the Copic nib-rotate's hold, which used to fire under a drag and change the tool beneath it - one of the ways every tool could stop working (see Fixed). - Losing the window ends a press where it stands. Switching to another
program, a dialog from another program, the Task Manager or a minimised
window keeps a stroke as far as it was drawn, places a line or a shape as
far as it went, and leaves a drag where it was put; a curve is dropped, as
Escdrops it.Space,CtrlandAltare let go with the window, since their release goes to the other one: aSpaceleft held turned every brush stroke into a straight line. SpaceandCtrlbefore a press mean what they mean in most drawing programs. With no press under way, holdingSpacegives the hand on every tool - a drag pans the canvas, over a Transform box or with the Rotate dialog open too - and holdingCtrlon a drawing tool gives Select or Direct Select, whichever was chosen last, untilCtrlcomes up. It comes up once the pointer moves or presses, or after a fifth of a second, so a chord such asCtrl+Zleaves the drawing tool in hand. The eyedropper's ownCtrl-for-Select is now this rule; Vector Path and Mesh Warp keep their ownCtrl. The straight line and the quick curve moved after the press: start a stroke, then holdSpacefor a line from where it began, orCtrl + Spacefor the quick curve. Before, they wereSpaceorCtrl + Spaceheld before the press, which is now the hand.- The Copic nib-rotate's
Ctrlhold is a still hold. It arms only with a drawing tool in hand and no press under way, and moving the pointer, pressing or typing another key cancels it, so aiming the selection tool withCtrlnever turns the nib. Held still for the hold time, it takes over from the selection tool as before. - A bare
Althands the keyboard to the menu bar only when it was meant for it: when no other key was pressed in the five seconds before it and nothing was done with it - no scroll, drag or press.Alt+ scroll to zoom,Alt+ drag to copy, and anAlta moment after other keys used to take the keyboard away from the drawing.Altwith a menu's underlined letter still opens that menu. - A click picks the mark whose ink is under it, the one on top first. A
line is as wide as it is painted, pressure included, and so are a Copic's
nib and a profiled stroke; a fill counts as the shape, and text and images
count by their boxes. Where marks overlap, the one painted over the others
wins: the order is the Layers panel's stack, so a layer moved up picks on
top as it paints on top. Failing that, the nearest mark within the Select
sensitivity is picked. It used to be the first mark in the page's list
within reach of its centre line, which after a layer was moved was often
the one underneath. The right-click menu, Fill Color, the Paint Bucket and
Mesh Warp pick in the same order (
src/core/hit-test.ts,src/core/paint-order.ts). - A rubber band takes every mark whose ink its box meets, a line it only crosses included. It used to need one of a mark's points inside the box, so a box across the edge of a rectangle, which has only its corners, took nothing.
- Direct Select's reach is its setting, at every pick. The Direct Select sensitivity reaches the first click that picks a stroke, every anchor, handle and path, and the Vector Path tool's anchors and closing click. It used to be held to at least 8 pixels on a path with Bezier anchors, so the slider did nothing below 8 there. Its default is now 8, and a 3 saved before this release - the old default - reads as 8. A path is grabbed by its painted edge as well as its middle, and a click inside a filled shape takes the whole path.
- A Direct Select click is not an edit. Nothing moves and no undo step is added until the pointer has travelled 4 screen pixels, as with the Select tool. A click that only picked an anchor used to leave an undo step that undid nothing.
- The eyedropper's setting is called Eyedropper sensitivity. It was labelled "Select pixel sensitivity", which it never was.
- The canvas zooms in until one page pixel spans its shorter side: on a
canvas 900 pixels tall, 90,000%. It stopped at 800%, which a canvas too
small to go deeper still reaches, and a window made smaller zooms out to
what its canvas can show.
Alt+ wheel zooms 10% a notch as before; a trackpad zooms by as much of a notch as it scrolls, and a fast wheel that sends several notches at once zooms that much further, where it used to count every event as one notch (src/renderer/zoom.ts). - View > Zoom In and Zoom Out zoom the canvas, by a quarter about its
middle, and say where the zoom stands. They were Electron's own rows,
which zoom the whole window - the panels and the toolbar with the drawing.
Ctrl++andCtrl+-run them as before, and their shortcuts can now be changed in Edit Keyboard Shortcuts. - What is meant as a distance on the screen is one at any zoom. A brush stroke keeps a sample every three quarters of a screen pixel, where it kept one every three quarters of a page pixel - so a stroke drawn at 400 times kept two points. The Curve tool's and the quick curve's shortest chord, the Vector Path tool's handle drag and double click, the rubber band's and a text box's least drag, and a selection's dashed box, 6 screen pixels off its mark and drawn thin, are measured on the screen too.
- A magnified image shows its pixels. Once one of its pixels covers 4 screen pixels, an image is drawn with them crisp rather than smoothed.
- A freehand stroke is kept as a few Bézier curves. When a Brush, Marker or
Copic stroke is lifted, its samples - one every three quarters of a screen
pixel, 150 to 400 across the page - are fitted with the fewest cubic
curves within the Freehand fidelity of every one (Schneider's method,
src/core/fit-curve.ts): a drawn S curve keeps four anchors. Direct Select shows those anchors with their handles, and the Vector Path tool can edit them; the SVG and the other exports write the curves; a drawn corner stays a corner; and a stylus's pressure is kept at every anchor, with an anchor more wherever it swells or eases, so its width still follows the hand. The stroke's points are sampled back from the anchors. Two-point lines, shapes and erasers stay as they were, and so do strokes drawn before this release. - Sharpen keeps a few anchors too. The auto-sharpen rebuild, and Sharpen Selection's, drew ten points for every one they kept; each result is now fitted within a third of a pixel, which keeps the hand-drawn wobble. A joined stroke, a stroke joined on an endpoint snap, and a symmetry copy stay fitted.
- The Eraser cuts the drawing. It adds no mark of its own and no layer: what the swath covers is cut out of the marks - every selected one, a selected group's included, or with nothing selected every mark it touches on a layer that can be drawn on - in one undo step, and the canvas shows the cut as the swath goes. A filled shape, a Copic stroke or a profiled one loses the area; a plain line is cut short wherever its painted width would reach the swath and stays a line, in pieces. Text and images are passed over, with a toast. An older file's eraser marks still paint, but are never picked, boxed or counted in a box; they ride along with their layer's selection, so a move takes their cuts with it.
Shiftholds a straight line to 45 degrees as well. UnderShiftthe straight line (a press, thenSpace) kept to level or plumb; it now keeps to the nearest of eight directions, its end where the pointer falls along that line.- A script's
tool erasercuts, as the Eraser does. The lines drawn after it cut their swath out of the marks drawn before them on their layer, and draw nothing of their own. Before, they drew the old kind of eraser mark, which the app now shows only from older files. In awipeor astackblock, an eraser cuts the block's own marks. Text and pictures are passed over, with a newerase-skippedwarning, and where the geometry cannot make a cut the old mark is drawn, so the picture still shows it. Generate Script still writes an older file's eraser mark astool eraser, now with a note that it reads back as the cut it paints. - Generate Script keeps clipping masks. A clip group is written as a
clipblock, and the script draws it back clipped. Where aclipcannot say the group - its clip mark is not the last closed shape in it, or the group has an opacity, is hidden or is locked - it is written as a plain group, with a note.
Fixed#
- The tools no longer stop working after a quick curve.
Escduring aCtrl + SpaceorCtrl + Shift + Spacequick curve, or losing the window during one, left the canvas holding a pointer that had gone, and from then on every press on the canvas was ignored, whatever the tool, until the app was restarted. The same happened whenCtrlwas held through a Select or a Text drag long enough for the Copic nib-rotate to switch the tool, when a shortcut key chose another tool mid-drag, and whenEscclosed a Transform box, a Rotate drag or a Mesh Warp in the middle of a drag. A press is now a record of what it is doing and the tool it began with (src/renderer/press-state.ts): its release acts on that record, whatever ends a press lets go of the pointer, and a press still on record when a new one arrives is finished rather than turning the new one away. The pointer capture taken away mid-press and the page being hidden end the press too.npm run gui-check -- held-keysruns each of these sequences. - A click where an eraser has cut a mark away picks nothing. It used to pick the eraser's own mark, which is invisible, and a click beside the cut could still pick the mark whose ink was gone. Erasers are never picked now.
- A rectangle no longer tears open when Direct Select drags the corner it began at. That corner is the rectangle's first point and its last; the drag moved only the last. Both move now, on any closed shape whose ends meet.
Shiftconstrains every Direct Select drag. It pinned a vector path's anchors and handles to the nearest axis or diagonal, but not a freehand stroke's points or tangent handles.npm run gui-check -- select-accuracyruns these and the changes above.- A blur or a shadow at the edge of the canvas no longer fades out. A layer, a group or a mark with effects was worked on a picture cut off at the canvas's edge, so a blurred layer thinned toward the edges, more the further in the zoom; at the new deepest zoom it would have vanished. Such a picture is now painted past the view by as far as its effects reach, at most half the view's diagonal, with a blur held to a third of that; and it is not painted at all when nothing it holds reaches the view, where deep in its blur cost as much as on the screen.
- No grey line along the bottom of the canvas. At a device pixel ratio
of 1.5 a canvas 617.67 pixels tall has a last row the paper only half
covered, over black; a group's drop shadow reaching it made it darker
still. The paper now covers every pixel of the canvas.
npm run gui-check -- zoomruns these and the zoom changes above. - Translucent ink lies flat. A plain stroke was painted a segment at a time, each with its own round ends at the stroke's opacity, so wherever two met they overlapped and darkened: below full opacity a line read as a string of beads. A stroke is now one mark - one path, or one filled outline where a stylus changes its width - so its tone is even from end to end.
- Join no longer keeps the first stroke's curve. Joining a stroke with
Bézier anchors (a Vector Path's, and now any freehand stroke's) to others
kept the first one's anchors, so the joined stroke exported as the first
piece alone. The joined run is fitted afresh.
npm run gui-check -- freehandruns the freehand changes above. - An erase cuts every mark it is aimed at. The Eraser's mark went on the layer in use and cut only that layer, and every mark has a layer of its own, so one erase cut one mark at most - and often not the one under it.
- Erasing with a group row picked adds no layer. It made an empty "Layer N" inside the group for the eraser's mark, which cut nothing.
- Eraser marks draw no boxes. Selecting a layer selected its eraser
marks too, each with a dashed box of its own, and every box built on the
selection - Transform, Rotate, Move, the X and Y fields, Fit All in View -
stretched over the ground they had erased. A layer left with only eraser
marks is empty now, and goes with them.
npm run gui-check -- eraserruns the Eraser's changes. - A tool added in a release keeps its place in a rearranged toolbar. The Vector Path button's place was never saved: the default tool order left it out, and a saved order kept only the tools it listed. It is listed now, with the Shape Eraser, and a tool a saved order does not know yet goes in after the tool it follows by default rather than at the end.
- A straight line's end and a quick curve's far end snap to a stroke's end
again. They stopped when
Shiftbecame the line's axis lock and the curve's apex key. They now snap wheneverShiftis not held, within the snap sensitivity and with its ring, as a freehand stroke's end does - and never back onto their own start. The Curve tool's Free variant still keeps its ends where they fall.npm run gui-check -- linesruns these, the 45 degrees and the Shift-click line.
[1.0.0-alpha.4.5.0] - 2026-09-26#
Menus made from the tools' own types, and scripts made from the drawing,
make this a minor release. Every command the app has is a row in two files -
its tool type and its key - and the menu bar, the right-click menus, the
tooltips and the shortcut tables are all made from them: nine menus now,
with Transform, Sketch, Layers, Pages, Automate and Help joining File, Edit
and View. Edit > Edit Keyboard Shortcuts and Edit > Edit Tool Types
change any tool's keys and the menus that list it, in files of your own.
Automate writes napkin script from a file, from the selected layers, or
from the steps Track History recorded, leaving out any step you untick.
The manual moved to a documentation site in docs/, which Help opens
with no network, and the README became its front door. The .skbk format
stays at version 3, and everything the API exported is still there.
Added#
- The menus, as data. Every command the app has is now a row in
src/core/menu/tool-types.jsonwith a tool type that says what kind of tool it is -Composition:edit:mixedfor the clipboard rows,Draw:Modify:elementfor Rotate - andsrc/core/menu/shortcuts.jsongives each one its keyboard shortcut. A menu lists the types it accepts, and a tool is in every menu that accepts its type, so the clipboard reaches Edit, the canvas and the layers panel from one type, and a new command appears in the right menus by saying what it is.src/core/menu/registry.tsbuilds the menu bar, the three right-click menus and the three toolbar dropdowns from the two files with no DOM, and merges the user's own copies of them, which hold only what differs and never overwrite the shipped ones. The menu bar, the right-click menus and the toolbar dropdowns are all built from it.- One spelling for a shortcut.
src/core/menu/chords.tsreads a shortcut as a person types it (ctrl+shift+z,Cmd+O,Ctrl++), writes it one way, turns it into an Electron accelerator, and matches a keypress both by the character it made and by the key that made it, soCtrl+Shift+]matches on any keyboard layout andShift+Cmatches whatever Caps Lock did to the letter. - Held to the menus it replaces.
test/menu-registry.test.tswrites out the old File, Edit and View menus and the old right-click lists verbatim, applies each change this release makes in a named step, and requires the generated menus to match exactly. It also spells out the new menu bar, all nine menus, row for row.test/chords.test.tsandtest/menu-overrides.test.tscover the shortcuts and the user's files, and the headless contract now builds every menu with the DOM poisoned.test/menu-dispatch.test.tsholds each place a row becomes an action - the native menu's clicks, the drawing window's command table, the right-click items - to the id the row carries, andtest/gui/check-context-menus.mjsreads the menu bar back from Electron and drives every right-click menu and dropdown in the built app.
- One spelling for a shortcut.
- Tooltips and tables that name the real keys. The 45 toolbar and panel
buttons and switches that run a command say which one, and each tooltip
takes its shortcut from the menu files when the app starts: a key changed
in the user's
shortcuts.jsonshows in the tooltip, and a command with no key shows none. The layer rows' rename hint and the grouping toasts do the same. The README's In-app controls opens with a table of every command that has a shortcut - its menu, its keys, a note - and the cheatsheet's Tools table lists every tool;npm run menu-docswrites both from the registry, andtest/menu-docs.test.tsfails while either is stale. The held keys and gestures the registry does not know stay in a hand-written table beside it.test/keys.test.tssends every shipped shortcut through the key lookup, andtest/gui/check-shortcuts.mjsruns the app with a shortcuts file of its own - in a user-data folder the check makes and removes, through the newNAPKIN_USER_DATA- and presses the keys. - The configuration popup the menu editors are built on.
src/renderer/config-dialog.tsdraws one form for editing a list: a search box that keeps the rows containing what is typed, radio buttons that keep one type of row, a bordered table that scrolls under headings that stay put, a line that says what the last edit did, and Accept and Cancel. A cell is words, a text box, a drop-down, a check box, or a shortcut box that takes the next keypress as its chord. The box turns green for a free chord, amber for one another row holds, and red for a key the app keeps for itself, such as Space or Tab, which it refuses. Each theme has its own green, amber and red, at 4.5:1 or better. A row that the search or a filter hides keeps its edits, and Accept gets every row back. While the popup is up it keeps its keys: a letter typed in it picks no tool, Ctrl+Z does not undo the drawing behind it, and Tab goes round its own controls. Escape cancels, and Enter in a field accepts. Nothing in the menus opens it yet; Edit Keyboard Shortcuts and Edit Tool Types are built on it next.test/config-dialog.test.tscovers the search, the filters, the verdicts and the edits, andtest/gui/check-config-dialog.mjsopens the popup in the built app with a spec of its own, through awindow.napkinCheckhook the page puts up only when the app is started for a check (NAPKIN_GUI_CHECK=1). - Edit > Edit Keyboard Shortcuts. The configuration popup lists every
tool, toolbar-only ones too, as Tool, Type and Keyboard Shortcut, with
the search, a radio for each main type, and the tool's place in the
menus as its tooltip. Click a shortcut and press the keys: the box turns
green for a shortcut no tool has, and amber for one another tool has,
which Accept takes from that tool; the other row turns amber too, saying
it will be left with none. Space, Escape and Tab, Enter for anything but
Move, and the keys of the rows Electron draws (Zoom In, Zoom Out, Toggle
Full Screen, Toggle Developer Tools) are refused in place with the
reason, and those rows' own shortcuts are shown greyed. Accept saves only
what differs from the app's own shortcuts, in a
shortcuts.jsonof your own in the app's settings folder, and the menus, the tooltips and the keys change at once. The shippedsrc/core/menu/shortcuts.jsonis never written. Reset to defaults puts the app's own shortcuts in the table, and accepting them removes your file. A file of yours that cannot be read is ignored, and the app says so once it has opened, after anything else it says as it opens. The main process checks what it is sent before writing (planUserFilesinsrc/core/menu/overrides.ts) and writes nothing the menus would refuse or quietly change.test/menu-editors.test.tsand four new cases intest/menu-overrides.test.tscover the rows, the checks, the settling and the saving, andtest/gui/check-shortcuts-dialog.mjsstarts the app on a broken file, then saves, warns, refuses, resets and cancels in the built app. - Edit > Edit Tool Types. The same popup lists every tool with its
type in a drop-down: a group for each main type holding the sub-types
the menus take, and Not in a menu first. The shortcut is shown
greyed beside it. A type decides which menus list a tool and nothing
else, and the drop-down says where a choice would list it before
anything is kept: "Rotate will be listed in Edit, Layers panel, Canvas".
Taking a tool out of every menu turns the cell amber, with the key that
still runs it, and a toolbar-only tool can be put in a menu. The rows
that cannot move are listed with the drop-down greyed and the reason as
its tooltip: the two editors, which stay in the Edit menu where they can
always be found, a menu's own rows, Electron's rows, a submenu's rows and
the tools kept on the keyboard. Accept saves only what differs from the
app's own types, in a
tool-types.jsonof your own beside your shortcuts, and every menu is made again from it at once; the shippedsrc/core/menu/tool-types.jsonis never written. Reset to defaults puts the app's own types in the table, and accepting them removes your file.test/menu-editors.test.tscovers the rows, the choices and the saving,test/menu-registry.test.tsholds where a type is listed to where the menus show a tool of it, andtest/gui/check-tool-types-dialog.mjsmoves Rotate to the clipboard's type, finds it in the layers panel's right-click menu and runs it from the canvas's, then resets it, places a toolbar tool and cancels. - A drawing written as napkin script.
src/core/script/writer.tswrites a page, a part of one or a whole book as the script that draws it back: its layer tree, its marks aspathsteps from their Bezier anchors, text, images and linked files, their effects, and their paint written only where it changes, so a drawing in one color sayscoloronce. A freehand line is written through its points, pruned to a tenth of a pixel as the SVG export prunes it, and an opacity of its own is scoped withpushandpop. Marks keep their paint order wherever the layer tree allows. Coordinates are written to two decimals unless every digit is asked for, and then the script draws the page back exactly. What the language cannot say - pen pressure, a mirrored Wave profile, a layer name used twice in one group - is written as near as it goes and said in a note. Asked to fit the page, it writes a page the size of the ink, with every mark moved so the ink starts at the corner. Automate > Generate Script is built on it.test/script-writer.test.tswrites every SVG fixture and every page of every golden script, draws the script back, and requires the same SVG byte for byte. The only difference it allows is a filled shape the source left open, which is closed, since a script fills only a closed path. At two decimals the same pixels are required instead. The headless test writes its page of every kind of mark with the DOM poisoned and gets the same PNG back. tool eraserin napkin script. Marks after it take away what is under them on their own layer, as the app's eraser does. An eraser takes no fill, outline style or effect; aneffectbefore one waits for the next mark that shows. It is an addition to version 1, and the language reference, the schema and the helper skill list it.- Automate > Generate Script. Two rows of the Automate menu write a
drawing as napkin script and show it before anything uses it. From
Media File picks a file as File > Import does: an SVG is read into the
layer tree an import would add and written whole, and a PDF's pages are
written one after another, a
newpagebefore each after the first. A picture - PNG, JPEG, GIF or WebP - is not traced: the script makes a page the picture's size and places the file withlink, by its file name, or with Embed the image data ticked, carries the picture itself. Selected Layers writes the rows lit in the Layers panel, or the layers of the selected marks, with the groups above them, on the page as it is or on one fitted to the selection. The Generated script dialog shows the whole script in a box that scrolls and can be selected, a line counting its instructions, marks and layers, and the writer's notes on anything the language cannot say as the drawing does; a picture's data is shortened in the box, and everything else takes all of it. Copy, Save As (a.napkinfile), Open as New Page (the script run into pages of their own, after the page in view) and Cancel. Every script opens with a comment saying what it was written from, and the notes follow as comments, so a saved script carries both. While the dialog is up it keeps its keys, as the configuration popup does.src/core/script/generate.tswrites the scripts with no DOM, andsrc/core/imported-sketch.tsbuilds an imported file's layers for both File > Import and the generator, so the two cannot disagree about the tree a file makes.test/script-generate.test.tsholds a generated page to the tree the import builds, runs a PDF's pages back (the walking figure's to the same pixels), runs a picture's linked and embedded scripts, and writes a selection kept and fitted.test/gui/check-generate-script.mjspicks two parts of an imported figure in the Layers panel, writes them, fits the page and opens it, then writes an SVG and a PNG from their files, embeds, copies, and closes the dialog with Escape and Cancel in the built app. - Automate > Track History and History Limit. With Track History on,
every step of the drawing's history is recorded: each edit that can be
undone, and each undo and redo, with the marks it added, removed and
changed, the paint order when it moved, and what happened to the layers.
A step is named for what made it - the command that ran ("Rotate"), the
tool whose press on the canvas made it ("Pen stroke"), or, when nothing
named it, what it changed ("Changed 2 marks") - with the type the menu
files give it. A drag is one step, holding where it ended, as it is one
undo. It is off by default and shows a check mark when on. History
Limit opens Verbose Settings at a new Automate section, which holds
the same switch, the limit (50 to 5,000 steps in steps of 50, 500 unless
changed, the oldest going first) and how many steps are kept and roughly
what they hold. The steps last as long as the document: a new or opened
document starts a new history, and turning tracking off clears it. Zoom,
pan, the panels and the tool in hand are not recorded. From Session
History writes the steps as a script, and is greyed while there are none.
The store closes each step at the next history boundary
(
Store.onHistory) and does nothing when no one listens;src/core/history-diff.tscompares the pages, andsrc/renderer/history-tracker.tsnames and keeps the steps.test/history-diff.test.tsandtest/history-tracker.test.tscover the comparison, the store's hook, the names and the limit, andtest/gui/check-track-history.mjsturns tracking on from the menu, draws two strokes, rotates one and undoes, reads the four steps back, and uses the Automate section's slider and switch in the built app. - Automate > Generate Script > From Session History. The configuration
popup lists the steps Track History recorded on the page in view, as the
mockup draws it: "History Limit" across the top of the table, a row per
step with its index, tool type and command, every one ticked, a search
over the type and the command, and a radio button for each main type.
Untick a step and the script is the drawing as it would be had that step
not happened: a removal left out brings its mark back, an addition left
out drops its mark and every change to it, and a change left out takes
back only the properties it changed, so a later step keeps its own. The
script draws what the session drew - a mark from before the history began
only when a ticked step changed it - with each step's comment before the
first mark it drew,
# 7 Draw:Add:mark Pen stroke (2026-09-26 14:03), and a line for what it did to other marks and to the layers ("modified mark from step 1", "removed in step 12"). Its first line names the app's version, the page and the time, and the steps left out. Accept shows the script in the Generated script dialog.src/core/script/history-script.tsreplays the steps with no DOM; Track History's diff now keeps what each changed property was, so a change can be taken back alone.formatScripttakescomments, lines to write before given instructions, and the script writer can trace each mark's first instruction.test/history-script.test.tsrecords sessions through the store and plays them back with steps left out, andtest/gui/check-history-script.mjsdraws, turns a stroke, unticks the turn in the popup, and opens the pages both scripts draw in the built app. - A documentation site.
docs/is a static site of 61 pages for GitHub Pages, at https://isocialpractice.github.io/napkin-sketch/ once it is published: a landing page, install, five task quickstarts (draw, transform, layers, pages, automate), sixteen pages on using the app, the 27 napkin script and API pages, embedding, the helpers, building and testing, the changelog and the license.npm run sitewrites every page from Markdown - the manual's own sources indocs/site-src/, anddocs/api/,QUICKSTART.md,CHEATSHEET.md,CHANGELOG.mdandai-helper/README.mdwhere they are - with one top bar, menu and footer stamped into each, a light and a dark theme, a menu that slides in on a phone, and only relative links, so the pages open from a folder as well.src/docs/site.tsrenders the Markdown these pages use with no dependency, and the tables in them come from the code: the settings, the npm scripts, the page lists, and the shortcutsnpm run menu-docswrites.npm run site -- --checkandtest/site.test.tsfail when a page is behind its source, when the chrome differs between pages, or when a link or#anchorreaches nothing, and the test runs the napkin examples the pages hold..github/workflows/pages.ymldeploysdocs/on a push tomain.DESIGN_LANGUAGE.mdrecords the site's colors, drawn from the icon and the app's own, with each one's contrast against its background. - Help opens the documentation. Help > Verbose opens the site's
pages in a window of their own, at Using the app, and each Help >
Tool Types row opens its quickstart there: Transform, Draw, Pages,
Layers or Automate. The pages are the app's own copy - the installer
carries them, and a checkout reads
docs/- so they need no network. A link to another page stays in the window, a web link opens in the browser, and nothing else is followed; the window has no menu bar and no way into the app.Alt + LeftandAlt + Rightgo back and forward, as a mouse's side buttons do, andCtrl + Wcloses it. Help > Source Docs, the published site, stays out of the menu until its deployment has been checked.src/main/docs.tsmakes the window's decisions without Electron (test/docs-window.test.ts), andtest/gui/check-help-menu.mjsdrives the menu and the window in the built app. - Animation Mode's frame names, in one place. The Animation Mode page gains a table of how a frame is named from the one it is drawn from, and the Automate quickstart that Help > Tool Types > Automate opens leads to it and to the six assemblies.
- Transform > Transform Box and Sketch > Stroke Profile. Checking the
changelog against the new menus found two tools they had left out. The
Transform box (
Ctrl+T, from 1.0.0-alpha.4.2.2) had the Transform menu's own type but was filed with the toolbar-only tools, though it has no toolbar button; it now heads the Transform menu's second block, with a check mark while the box is up. Stroke Profile (from 1.0.0-alpha.4.3.0) was not in the menu files at all, so no menu listed it and no key could be given to it; it now opens its picker from the Sketch menu, and Edit > Edit Keyboard Shortcuts can give it a key, which its toolbar control's tooltip then shows.
Changed#
- The keyboard runs through the menu files. One lookup in
src/renderer/keys.tsreplaces the key handler's forty-odd branches, so a shortcut in the user'sshortcuts.jsonchanges what the key does, not only what the menus show. The modes still come first - Escape backing out, Enter finishing a path, digits feeding a quick entry - and Enter and Delete still stand aside when there is nothing to move or delete. Ctrl+R, Ctrl+Shift+R and F5 are still swallowed wherever the focus is, since Chromium reloads on them; outside a text field they run what the shortcuts give them, and a reload key with nothing to run still explains itself. Shift with a letter or a named key still means the key when no shortcut uses the Shift form, so Shift+P picks the pen as it always has; Alt with a letter no longer does. The keys only the menu bar answered before (Ctrl+N, Ctrl+O, Ctrl+0, Ctrl+, and Ctrl+Alt+,) are answered by the window too, with the same commands. - Nine menus in the menu bar. File, Edit and View are joined by Transform (Vector Path, Transform Box, Move, Rotate, Join, Close Shape with its Sharp and Smooth joins, Mirror, Sharpen, Mesh Warp), Sketch (Pen, Marker, Eraser, Text, Copic, Direct, Stroke Profile), Layers, Pages, Automate and Help. Rotate and Mirror moved from Edit to Transform, and File > Export lists PNG, SVG, JPEG, PDF. Every Automate row works: Generate Script's From Media File, Selected Layers and From Session History, Track History and History Limit. Help's rows open the documentation, and Help > Source Code opens the project's page.
- The menu bar greys what there is nothing to act on, as the right-click menus always have: Cut, Copy, Duplicate and Delete with nothing selected, Paste in Place with nothing copied, Undo and Redo with nothing to undo or redo, Ungroup when the active layer is not a group, Delete Page with one page, and Hide Layers Panel or Hide Pages Panel when that panel is already hidden. The drawing window tells the menu bar when an answer changes, and the rows are updated where they stand rather than the menu rebuilt.
- Every right-click row shows its shortcut at its right edge, and each right-click menu holds what its menu in the menu bar holds. The layers panel's Delete Layer(s) is Delete Layer, and its two restack rows are a Move submenu of Layer Up and Layer Down; the pages panel's Add Page opens Default New Page, Custom New Page and From Selection, the order the page menu under the hamburger now takes too; the Export button lists its formats in the new File > Export order.
- One command, however it is reached. A menu bar row, a right-click row
and a toolbar button run the same command by the same id, through one
table in the drawing window (
src/renderer/commands.ts) whose type is every command it owns, so a command added to the menu files without a handler does not compile. The main process runs the few it owns itself: Verbose Settings and the Help rows.MenuAction, the list of actions the menu bar used to send the drawing window, is gone: the bridge'sonMenuActionnow hands the window a command's id from the menu files (new-sketchwhere it wasnew). - File > Import takes GIF and WebP pictures, and so does
-ion the command line; each is placed as a PNG or JPEG is. - The README is a front door. It keeps a paragraph for each feature,
the install commands and the license, and each heading links to the page
on the site that now holds the full text; the link under the title opens
the documentation. The shortcut table
npm run menu-docswrites moved with its section todocs/site-src/guide/menus-and-shortcuts.md. - The banner is a picture. The README shows
assets/bannerImage.png- the logo beside the app - which replaces the hand-drawnassets/screenshot.svg, and the site's home page opens ondocs/assets/banner.gif, which plays once and holds its last frame as the page's hero.test/site.test.tsfails if the GIF is given a loop block again, or the banner leaves its place under the title. - The npm package ships
docs/apiwithout the site's pages beside it.filesleaves outdocs/api/**/*.html, andnpm run pack-checkfails if one slips into the tarball. The desktop installer carries the whole site, for the app to open without a connection. - Every GUI check runs in a user-data folder of its own, made when it
launches the app and removed when it stops it, unless the check brings
its own. Twelve checks used to share
%APPDATA%/Electronwith each other and with the app run from the checkout by hand, so a setting one of them saved was the next one's starting point. The app is launched with Chromium's occlusion tracking off, too: a check window Windows opened behind another stopped drawing, and a check could wait on it for ever.
Fixed#
- A right-click on the canvas acted like a left click first. The pointer handler never looked at which button was pressed, so a right press drew a dot with the pen, sampled a colour with the eyedropper, or dropped the selection with the Select tool before the canvas menu opened - which left that menu's Cut, Copy and Delete greyed with a selection in hand. Only the primary button draws, selects and edits now.
- The
.skbkformat's documentation showed version 2, while the app writes version 3. The page now says what version 3 adds (group rows,parent, a stroke'sfill) and that older files open unchanged, and the project structure lists the source files it had left out.
[1.0.0-alpha.4.4.0] - 2026-09-25#
Drawing from written instructions, with no pointer and no window, makes this a
minor release. Napkin script is a small language that a person, an AI tool or a
program in any language writes. It runs into the same pages, layers and Bezier
anchors the app draws by hand; one call writes the drawing as SVG, PNG, PDF, a
.skbk the app opens, or a script that rebuilds it in Adobe Illustrator; and
napkin-sketch draw, check, render and verbs do all of it from a
shell, with no Electron. With draw --prompt and a third AI helper, a
sentence becomes the script, and for the four animations with a measured cycle
a character's frames draw with no AI at all. A drawing can link a file made
elsewhere rather than import it, a mark, a layer or an element can carry CSS
filter effects - a blur, a drop shadow, a color shift - and the API's
documentation is rebuilt as a hub over eight categories. What existed keeps
working as it did - the modules that moved are still exported from where they
were - and the .skbk format stays at version 3.
Added#
- The napkin script contract.
src/core/script/instructions.tsdefines the object form every front end of the coming instruction language lowers to: 55 verbs in nine categories - document, layers, paint, shapes, paths, transforms, control, text and media, and output. Each instruction is a plain object naming itsverb, so a program in any language can build a script as JSON.verbs.jsonbeside it is the verb table: every verb's category, a one-line summary, an example that runs as written, and the argument grammar the text parser will follow, with the 31 diagnostic codes and their levels. The parser, the documentation tables, the CLI's verb listing and the helper skill are all to be generated from that one table.- The table and the types cannot drift. A field table typed from the
instruction union compiles only when it names exactly the fields each
instruction has and says correctly which are optional.
test/script-instructions.test.tsholdsverbs.jsonto it verb by verb and field by field, and checks the table's choice lists - units, tools, dash styles, profiles and paper sizes - against the model they choose from. - The budget was measured before anything could hang.
SCRIPT_LIMITScaps one evaluation at 200,000 instructions, 50,000 marks, 250,000 anchors, 1,000,000 sampled points and 64 nested blocks. Sampled points are the cost that matters: sampled at 24 points a curved segment, 50,000 circles came to 4.85 million points, more than the.skbkwriter can hold in one string, while a million points build, export to SVG and save in about two seconds. - The hand-drawn pass is calibrated.
src/core/script/rough.tsholds the constantsrough <amount>runs on, read off a contact sheet of thetest/imports/fixtures at three strengths, and what the sheet showed: straight segments have to bow, the bow grows with the square root of their length, and a closed mark reopens along the tangent it leaves by.
- The table and the types cannot drift. A field table typed from the
instruction union compiles only when it names exactly the fields each
instruction has and says correctly which are optional.
- A headless contract.
test/headless.test.tsinstalls a DOM that exists and throws on first touch, then runs every entry point documented as DOM-free:Surface.toSVGon a page holding every kind of mark, cropped and transparent too, the PDF writer, the.skbkround trip, the sharpen engine, the path-data parser, and both composition renderers. Adocument.added inside any of them used to pass every suite, because Node has no document; now it fails and names what it touched. The same suite checks that the DOM-free core imports nothing from the renderer, the main process or Node. - Newer marks degrade to their picture. A test pins that an image item carrying a field this build does not know loads with its picture, size and position intact and survives a save, which is how a file from a newer build will show its linked graphics on an older one.
- Napkin script reads, checks and writes itself.
parseScriptreads the text form into instructions,validateScriptchecks a script built as JSON the same way, andformatScriptwrites either back as canonical text. The parser walks the verb table's forms rather than carrying a function per verb, so the grammar and every listing of it cannot disagree. Neither reader throws: each returns the instructions it could read, the errors and warnings with their stable codes, and whether there were any errors. A script read from text and the same script built as JSON come out equal, and every verb's example survives a round trip through the formatter.- Three typos report three. Each diagnostic names its line and column,
or its index path in a JSON script, and prints the way editors and CI read
it:
card.napkin:3:1: error unknown-verb: .... A misspelled verb, color or choice suggests the one that was meant -circlnamescircle,rectanglenamesrect,greannamesgreen. - Written the way people and helpers write. A newline or
;ends an instruction, and a block may open and close on one line. Paint instructions share a line by their verbs,color #1f2328 width 3 fill #ffe08a.#followed by a space starts a comment while#1f2328is a color, and keywords ignore case. - Expressions in parentheses, with
+ - * / %,letnames,repeatcounters, the page'swidthandheight, units and shares of the page inside them, andsin,cosandtanin degrees withsqrt,abs,round,floor,ceil,minandmax. A malformed one is reported at its column before anything runs. - Three diagnostic codes joined the table:
missing-argument,expected-block, and the warningunknown-fieldfor a JSON field a verb does not have.
- Three typos report three. Each diagnostic names its line and column,
or its index path in a JSON script, and prints the way editors and CI read
it:
- Napkin script draws.
evaluateruns a script - text, or JSON built as the object form - and returns a sketch book: sized pages, the layer tree the script'slayerandgroupinstructions describe, and marks that are the app's ownStrokeobjects, Bezier anchors invectorwith the points the canvas paints sampled from them. A generated drawing opens in the app as editable work and exports through the app's own writers.- Shapes and paths as anchors.
rectwith or without rounded corners,circle,ellipse,line,polyline,polygon,star,arc,spiral, andpathfrom SVG path data or frommove,to,by,curve,smoothandclose. The circle, the ellipse and the rounded rectangle are built with the SVG importer's own code, so a generated circle and an imported one are the same curve. - Paint, transforms and units carry from one instruction to the next.
Transforms are baked into the anchors, a stroke's width scales with them,
and a copic nib and a gradient's axis turn with them.
pushandpopsave and restore all three. Agroupor a placed definition keeps its changes to itself; arepeatbuilds on its own passes. - Names and definitions.
letsets the nearest name or makes a new one.defineis hoisted, and a definition reads the names in force where it is placed, so a name set beforeplaceworks as a parameter. A definition that places itself is refused. - Errors are values. An instruction that cannot run - an unknown name, a
value out of range, a
popwith nopush- is reported and skipped, and the run goes on. Only the budget stops a run, and the book keeps what was drawn. A new code,unsupported-verb, is left for a host that does not draw an instruction. - One script, one book. Ids are counted rather than random and the time
stamped on the book can be fixed, so a run is the same byte for byte every
time.
test/script-evaluate.test.tschecks that, and that every verb's example runs, and that a generated book loads back exactly as it was built. - Curves through points.
throughdraws a smooth curve through every point it is given, with an anchor at each, so the curve reshapes point by point in the app. It runs along the line from the point before to the point after, each handle a third of its own segment, so points spaced unevenly do not throw it into loops, and it ends in parabolas. Inside apathit carries on from the current point, smoothly after a curve. - Rounded polygons at any angle.
polygon ... r 12rounds every corner with the arc that touches both of its sides, held to half of either side. A rounded square is the rounded rectangle, anchor for anchor. - A shape library.
shape "cube-isometric" at 40 40 size 120draws one of 22 named shapes: a square, a rectangle, a circle, an ellipse, two triangles, a hexagon and a star, lines at set angles, an arc and a spiral, and a wheel, a sphere, a cube and a cylinder, each isometric and in perspective. One size sets the longer side and keeps the proportions; two stretch the shape to fit. Each part shows only what its source showed, so the objects are line drawings until afillmakes them solid. A name the library lacks is reported with the names it most likely meant:cubenamescube-isometricandcube-perspective.
- Shapes and paths as anchors.
- The shape library is generated, and checked.
npm run shape-libraryreadsshapes.svg,isometric-objects.svgandperspective-objects.svgfrom the vector-graphics skill's assets intosrc/core/script/shape-library.json, with the SVG importer's own element and path code and no browser.npm run shape-library -- --checkfails when the committed file is out of date, and so does a test. The reader stops at anything it does not read - a transform, a<text>, a style rule other than a class - and names it, rather than drawing a shape wrong. - Geometry has its own suite.
test/script-geometry.test.tsholds the shapes to their rules: a circle's handles, an arc against the importer's reading of the same SVG arc, the fillets of a rounded polygon, a curve through points, and every library shape filling the box it is given. - Napkin script draws by hand.
rough <amount>runs the hand-drawn pass on every mark after it, from barely there atrough 0.3to clearly drawn by hand atrough 1. Straight segments bow by the square root of their length, anchors close together drift together, the curves a shape was drawn with turn and stretch with their joins kept smooth, and lines run past their ends, a closed one opening at its start so the join shows. What comes out is anchors like any other mark, editable in the app.- Filled shapes are coloured in. A closed shape with a fill and a line
comes out as two marks, the fill roughened half as much and then the line,
as a hand colours a shape in and draws round it.
passes 2restates the line on a wobble of its own, thinner and lighter. - Seeded, and local. One script and one seed draw the same bytes on every run, and another seed draws the drawing by another hand. Each mark's wobble comes from the seed and the mark itself, so adding or moving one mark leaves every other mark's wobble as it was. The noise is integer arithmetic, so a seed gives the same wobble in any JavaScript engine.
- Measured, and documented from the measurements. How far each part
reaches is a table on the drawing reference generated from the constants,
and
test/script-rough.test.tsholds the pass to them.
- Filled shapes are coloured in. A closed shape with a fill and a line
comes out as two marks, the fill roughened half as much and then the line,
as a hand colours a shape in and draws round it.
- Napkin script sets text, places images and copies documents in, none of
it reading a file or needing a DOM.
textplaces a text item the app edits as text, in the fontfontset, aligned on itsatpoint by the measure of the built-in face the composition renderers use.boxwraps it at a width, and\nbreaks a line. Like the app's own text, it stays upright when the drawing turns.text ... as marksdraws the letters as the built-in face's pen strokes, one mark in the current paint, so lettering turns with the drawing and takes the hand-drawn pass. A character the face lacks is left as a gap and reported.imageplaces a data URL, or an image the host passed by name inoptions.assets. Its own size, and its proportions when one size is given, are read from its header: PNG, JPEG, GIF, WebP or SVG. A path or a web address is refused, with how to hand the image over instead.usecopies a page or a book the host passed inoptions.documentsinto a group of its own: its layers with their names, opacity, visibility and lock, and its marks, each with a new id, placed atatand scaled.- Two warnings joined the diagnostic table:
image-size-unknownandglyph-missing.test/script-text.test.tscovers all four verbs.
- Linked graphics. A drawing or a composition can place a file by
reference - one selectable item and one layer row that stand for the file,
its contents neither drawn in nor broken into layers - which is what a
design tool calls a linked file, as against an import.
- In a sketch a link is an image item with a
linkfield. Its picture is a placeholder, a dashed box with the file's name, which the app draws and moves like any image, and which a build without links shows instead. The loader keeps the field, so a book keeps its links through a save. - Outputs keep the reference. The SVG export writes
<image href="assets/logo.svg" data-link="true">, and the importer reads it back - any image that names a file rather than carrying data now imports as a link, where before it was skipped. A PDF draws the placeholder, since embedding the file would make it an import. - Compositions link too. An
imageelement withlink: truekeeps its path in the SVG. The rasterizer draws the file only through aresolveLinkfunction the host passes - an SVG from its shapes, fitted and clipped to the box, a PNG from its pixels - and otherwise draws the placeholder and says so inwarnings. - The host reads, inside one folder.
resolveLinkFromDir(folder), in the Node file helpers, refuses absolute paths, drive letters, UNC shares,..steps, web addresses and symbolic links out of the folder. A script only names a file. linkin napkin script places one on a layer of its own, sized like animageand read for its size when the run is givenresolveLink. Every verb in the table now draws.test/link.test.tscovers all of it, andtest/gui/check-link.mjsopens a book with a link, moves it, saves it, and imports an SVG that places a file by reference.
- In a sketch a link is an image item with a
- The drawing reference.
docs/api/drawing/README.mdcovers running a script and what each instruction draws: the shape library in a table made from the library itself, curves through points, the hand-drawn pass, text, images, copied documents and linked graphics. The language page gained the budget. Every script on both pages is run by the documentation test, and must come out clean.API.mddocuments linked files in compositions. - The language has its reference page.
docs/api/language/README.mdcovers the syntax, the values, expressions, the version line, the object form, every verb with its fields in JSON, and every diagnostic code, with a section for agents. Its tables are generated from the verb table, andtest/script-docs.test.tsholds every page underdocs/api/to the code: each script shown must read, each error example must produce the codes it names and the printout it shows, and each table must be current. - Every format from one call.
renderSketch(page, options)writes a page andrenderBook(book, options)a book, as SVG, PNG, PDF or.skbk, with no DOM and no dependency, in a browser or in plain Node.- PNG without a canvas.
sketchToCompositionlowers a sketch into the graphic-design composition model mark for mark as the SVG export writes it - the same path data, round caps and joins, dashes, gradients, profiled and Copic outlines, layers as groups at their opacity, erasers cutting their own layer - and the composition rasterizer draws it. Against the SVG asrsvg-convertdraws it, at least 99.78% of the ink agrees on every fixture intest/imports/, andtest/sketch-composition.test.tsholds it to 99% whereverrsvg-convertis installed. - One box for every format.
crop: 'auto'cuts to the ink - the marks on visible layers, grown by half the widest line, the box the Selection export cuts to - with an optional pad; a box is used as given; andregistrationcuts every page of a book to one box, so frames drawn by one script line up. A script'scropandregistrationapply when itsoutputis spread into the options. The box is the SVG's view box, the PNG's page and the PDF's media box, whichsketchesToPdfnow takes page by page. - A
.skbkkeeps the book's own time, so one script writes the same bytes every run, and it opens in the app with its layers and anchors, whichtest/gui/check-script.mjsproves by opening one, saving it and reading the anchors back. - Warnings, not failures.
onWarningis told what a format left out or drew as a stand-in - an image the PDF cannot embed, a gradient it prints flat, a link the PNG drew as its placeholder, a color that is not one - once a page, and the rest of the page is drawn. test/script-render.test.tscovers the calls, and the headless suite writes every format with the DOM poisoned.
- PNG without a canvas.
- Compositions gain gradients and erase shapes. A
fillcan be{ type: 'linear' | 'radial', angle?, stops }, which the SVG writer writes as a paint server in user space and the rasterizer works out pixel by pixel, placed where the sketch export has always placed a shape's gradient. A group'seraseshapes clear it wherever they paint - a<mask>in the SVG, a cut in the PNG - which is how an eraser reaches the PNG.API.mddocuments both, and its limits no longer say gradients are not modelled. - The output reference.
docs/api/output/README.mdcovers the formats, their options, the box, the warnings and the lowering behind the PNG, with a section for agents. - The language is in the public API.
napkin-sketchexports napkin script beside the editor and the composition API, all of it browser-safe:parseScript,validateScript,formatScriptandformatDiagnostic,evaluate,renderSketch,renderBook,inkBox,renderBoxandsketchToComposition, the verb and diagnostic tables,SCRIPT_VERSIONandSCRIPT_LIMITS, and the types their calls take and give. Nothing the entry exported before is renamed or removed, andtest/api-surface.test.tsholds it to the list.drawSvgruns a script and gives back its first page as SVG in one call, cut as the script'scropasks, with the diagnostics and whether it wasok.
napkin-sketch/node, the file half. A new Node-only entry:drawFileanddrawToFilesrun a script and write each format asked for into a folder - a file a page for SVG and PNG, one file for PDF and.skbk- and give back{ ok, files, diagnostics, stats, warnings };writeBookwrites a book already drawn;readScriptreads napkin text, or the object form from a.jsonfile;loadAssetsandloadDocumentsread what a script names, by name. The composition's file helpers are exported here too, so a Node program has one import for the disk.- Safe to hand a script. A script names its own pages, so a name is
made a file name before anything is written - every character a file name
cannot hold becomes
-- and no file lands outside the output folder. A script's links are read inside its own folder, orbase, and nowhere else. - Errors are values; the disk throws. A script with errors still writes
what it drew, with
okfalse and the diagnostics saying why, andstrictwrites nothing when anything at all was reported. Every file is rendered before any is written, and each is written whole or not at all. - It builds to
dist/node/index.js, with its declarations named in the package map, andtest/script-files.test.tscovers it on a real file system.
- Safe to hand a script. A script names its own pages, so a name is
made a file name before anything is written - every character a file name
cannot hold becomes
- The Node reference.
docs/api/node/README.mdcovers the two entries, ES modules and CommonJS, the types, drawing and writing, the result object, and what throws. The examples on the other pages import fromnapkin-sketch. - The command line draws.
napkin-sketch draw <script | ->writes a napkin script to SVG, PNG, PDF or.skbkfiles with no window;checkreads and runs a script and writes nothing;renderwrites a sketch book as files, all of it or one page; andverbslists the language by category, with the shape library. A first word that names a command selects it, and every flag the app had keeps its meaning.- Any language can call it.
-reads the script from standard input, and--jsonprints one line of JSON on standard output -{ ok, exitCode, files, diagnostics, warnings, stats, version, language }- and nothing else. Without it, problems print asfile:line:column: level code: messageon standard error, and the files written on standard output. - Exit codes a caller can branch on:
0drew,1the arguments were wrong,2the script had errors - what could be drawn is still written, unless--strict- and3a file could not be read or written, a linked file included. - No Electron on the way. The commands run before anything can load
Electron, so they work on an install where the GUI cannot start; a test
runs the built CLI with
require('electron')made to throw. - Callers in five languages under
docs/api/cli/examples/- a shell script, a batch file, Node, Python and C - andtest/cli-examples.test.tsruns each wherever its shell, interpreter or compiler is installed.docs/api/cli/README.mdis the reference. drawToFilesgaineddefaultNameandwriteBookgainedstrict, which the commands use.
- Any language can call it.
- The API documentation, in eight categories.
API.mdis now the hub: what the API is and is not, how it fits together, the ways in, who the pages are for, and the language's version policy.API-QUICKSTART.mdis the whole API in five steps, and the newAPI-CHEATSHEET.mdputs every verb, command, render option and exit code on one page. Underdocs/api/, eight categories - language, drawing, compose, output, cli, node, interop and ai - each have a reference, a quickstart that runs as written on a fresh clone, and a cheatsheet of tables, and every page links to its hub and its two siblings in its first lines.- Working with other programs (
docs/api/interop/) is new: what napkin's SVG carries for Illustrator and Inkscape, how the importer names another editor's layers, what survives a round trip each way, and linking a file rather than importing or embedding it. docs/api/INDEX.jsonlists every page with its category, kind, first paragraph and size, so an agent can pick the smallest page that answers.docs/api/schema/instructions.schema.jsonis the object form as a JSON Schema, draft-07, made from the verb table, so a script built as JSON can be checked before it is handed over. It is stricter thanvalidateScript: a field a verb does not have fails it.npm run api-docswrites the generated parts - the verb, command, flag and exit code tables, the schema and the index - from the tables the code reads (src/docs/api-docs.ts), and-- --checkfails when any is out of date. The command line's--helplists its commands from the same table, andEXIT_SUMMARIESgives each exit code its line.- Tests.
test/api-docs.test.tsholds the pages to their shapes - a reference's headings, a quickstart's steps and length, a cheatsheet's length, the links at the top, every link to a heading - and the schema to the verb table: it accepts every verb's own example and refuses what the object form refuses.test/script-docs.test.tsnow runs the scripts and follows the links on the three hubs too.
- Working with other programs (
- Effects: the CSS filter functions, on marks, layers, groups and
elements.
blur,brightness,contrast,saturate,grayscale,sepia,invert,hue-rotate,opacityanddrop-shadow, as data -effectson a sketch's marks and layers and on any composition element - and drawn by every output that can draw them.- In napkin script,
effect <name> <arguments>gives the next layer, group or mark the script makes an effect, and several stack in order; a mark the hand-drawn pass draws as several strokes, a library shape of several parts, and a placed definition take them whole, on a layer or a group of their own. An effect nothing takes is the new warningunused-effect. Outside parentheses a hyphen now joins a word, sodrop-shadowandhue-rotateread as the names they are. - The SVG writers write a
<filter>of SVG 1.1 primitives in sRGB, over a region the element's own box grown by the effects' reach, and the sketch writer the list itself asdata-effects, which the importer reads back. - The PNG draws the element or the layer on a layer of its own, runs each
effect over it - a Gaussian blur as the Filter Effects specification draws
one, the color matrices it defines - and lays it down; against
rsvg-convertthe two differ by under one level in 255 on average. - The app and the canvas painter paint them through
ctx.filter, a group with effects as one picture. A PDF prints what carries them plain, and says so. EFFECT_TYPES,readEffects,cssFilterand theEffecttype are exported, andtest/effects.test.ts,test/script-effects.test.tsandtest/gui/check-effects.mjshold them.
- In napkin script,
- An Illustrator script:
--to jsx.renderSketch(page, { format: 'jsx' }),renderBook, the newsketchesToJsx, andnapkin-sketch drawandrenderwith--to jsxwrite one ExtendScript file that, run in Adobe Illustrator with File > Scripts > Other Script, rebuilds the drawing out of Illustrator's own objects rather than handing it an SVG to read.- A page is a document, its artboard the page or the box it is cut to, a
page pixel a point as in the PDF, with its paper on a locked
Backgroundlayer. A top-level layer is a layer and the layers in a group are named groups inside it, hidden and locked ones too, made so once what is on them is drawn. - Marks keep their anchors. A curve's anchors and handles become its
path's points one to one, a point
SMOOTHwhere its handles are in line; a mark of several contours is a compound path; a profiled or Copic mark is the outline it fills. Paint isRGBColors, gradients with their stops, dashes, and opacity with a color's own alpha folded in. - Text is text frames, a box as area text, in the first family of the
item's list that Illustrator has. An image rides in the script and is
embedded; a link is a
PlacedItemlinked to the file, found from the script's folder, whichwriteBookanddrawToFilespoint at the links throughbase, and the render optionlinkFoldernames outright. - What cannot be made is said. The writer warns that an effect is left off and that an eraser is drawn in the paper's color. The script draws a link it cannot find or place as its placeholder, and ends by naming those and any font it could not find, or the page, layer, mark and line it stopped at. It is ES3 and ASCII, and returns its report as its result for a caller with no window.
test/illustrator.test.tsruns each script against a stand-in for Illustrator's scripting objects that holds it to Illustrator's rules, and reads back what it built. The script has not yet been run in Illustrator itself; that run is filed in TODO.
- A page is a document, its artboard the page or the box it is cut to, a
page pixel a point as in the PDF, with its paper on a locked
- A script from a sentence:
draw --promptand thescriptinghelper.napkin-sketch draw --prompt "a three-box flowchart with arrows"has an AI tool write the script, then draws it like any other.- The
scriptinghelper is the third plugin inai-helper/: thenapkin-scriptskill, which teaches the language - the two rules the parser holds a script to, page layout, three complete scripts that check clean - with a verb referencenpm run api-docsgenerates from the verb table; the/scripting:drawcommand; and the contract it answers to. The installer, the app's registry and the marketplace list it, andtest/ai-helper.test.tschecks the three together and the skill's scripts. - The bridge,
src/core/script/ai-bridge.ts, writes the form - the request, the rules, the images and documents--assetand--usehand over - reads back the script the tool saved to_temp/script-out.napkin, or printed, checks it ascheckdoes, and on errors runs the tool once more with the script and its diagnostics. It starts no process itself: the command line hands it a runner,src/cli/helper.ts, that runs the helper command in the working folder through the shell, as Animation Mode does. - The command line:
--prompt, and--helperorNAPKIN_SCRIPT_HELPERfor the tool (Claude Code by default). The script is kept beside the files as<name>.napkinand carried in the JSON report asscript, and a tool that is missing, signed out, or saves no script ends the command with the new exit code 4,error.reasonsaying which. test/ai-bridge.test.tsruns the loop against a stand-in helper, andtest/cli-draw.test.tsrunsdraw --promptagainst a stand-in AI tool as a real process. No real AI tool was run for these tests.
- The
- Animation frames from a measured cycle, with no AI. For the four
character types with a measured cycle - walk, run, idle and knocked down -
the frames can now be drawn in-process, all at once, rather than by the AI
helper one at a time.
- The generator,
src/core/script/animation.ts, measures the figure as Animation Mode does - each assembly's joint, the figure's height and base, its facing from its feet - and writes a napkin script: a page a frame, and on it each part of the figure copied in withuseand turned about its joint by the cycle's total up to that frame, every page cut to oneregistrationbox, the union of the frames' ink. The parts go toevaluateas documents, so the script holds no geometry and reads as what it does. A frame is posed from the source rather than from the frame before it, so a joint stays put however long the sequence runs, and a looping cycle's last frame is the source pose again. - In the app, Animation Mode's setup dialog gains Draw measured
frames beside Next for those types. Once the assemblies are mapped, it
shows the script before anything is drawn; Draw frames then lands the
whole sequence - one row per frame, side by side, each saved to
animations/as a helper's frame is - and one undo takes it back. Disable API hides the button, since a figure with layers no assembly holds is what the helper is for. - On the command line,
napkin-sketch render hero.skbk --animate walkwrites the figure on the book's first page, or on--page, as a file a frame,hero-walk-1.svgon.--framesspreads the cycle across more frames or fewer,--facingsays which way the figure travels when its feet should not decide, and the JSON report carries the script asscript.text. A page with none of the assemblies exits 2.idleandknocked down, the names the app shows, are read as the idsidealandknocked-down. test/script-animation.test.tsdraws the figure intest/imports/walk.svgthrough the cycles,test/cli-draw.test.tsrunsrender --animate, andtest/gui/check-measured-frames.mjspresses the button in the app, draws the frames and undoes them.
- The generator,
- Golden scripts. Six scripts in
test/scripts/- the goal example, a shape sheet, compound paths with holes, a sketch drawn by hand, a two-page deck with a link, and auseof an imported fixture - are drawn to SVG and compared byte for byte with the files beside them, twice in one process and once in a fresh one.npm test -- --update-goldenrewrites them and nothing else does, and.gitattributeskeeps them LF on every checkout. npm run pack-checkpacks the package, installs the tarball into an empty folder, and draws with it: from standard input with the installednapkin-sketch draw -, and withnapkin-sketchandnapkin-sketch/nodeimported by name. It fails when the install brings Electron, when the tarball lacks a file an entry needs, or when it holds one machine's state.- More of the release is held by tests. A generated circle written as SVG reads back as its four anchors to two decimals, and the no-DOM suite lowers a sketch into a composition, adds to it, and renders it.
Changed#
- Path data is read in
src/core/path-data.ts.parsePathD,parseVectorD, the arc converter and the subpath builder need no DOM, and now live beside the model, where code running in plain Node reaches them without importing the SVG importer. The importer uses them from there and still exports the two parsers, so nothing that imported them changes. - Generated marks are sampled to their size, as imported ones are. The
importer's sampler moved to
src/core/path-data.ts, beside a wrapper for marks with several subpaths, and the evaluator samples with it: a curved segment gives about one point for each pixel along its handles, 4 at the least and 24 at the most, where every generated segment used to give 24. A circle of radius 10 is 73 points rather than 97, so the points budget goes further on drawings of small curves. The importer's code for basic shapes - circles, ellipses, rectangles, lines and polygons - moved there too, reading attributes through a function rather than a DOM element, so the shape library builds its shapes with it. What the importer reads does not change. - The sketch SVG writer is in
src/core/sketch-svg.ts.sketchToSvg, with the compact path writer (src/core/svg-path.ts) andstrokeBounds(src/core/bounds.ts), moved out of the renderer to beside the model, so code running in plain Node reaches it with no DOM.Surface.toSVGcalls it andsurface.tsstill exportsstrokeBounds, so nothing that imported them changes, and every export test passes on the same bytes. - The API documentation moved into
docs/api/. The graphic-design reference that wasAPI.mdisdocs/api/compose/README.md, under the fixed headings every reference page has and with two more worked examples: a gradient under a ring with a hole in it, and a drawing with a band added in code. The helper half ofAPI-QUICKSTART.mdisdocs/api/ai/README.mdanddocs/api/ai/QUICKSTART.md, and its composition halfdocs/api/compose/QUICKSTART.md. README, CHEATSHEET, the twographic-designerskills and the code comments that named the old pages name the new ones; README gains Drawing from a script, and CHEATSHEET the twenty verbs most scripts use.- Two statements the old page made are corrected: a color a composition
cannot read stops the PNG render with an error that names it, and the SVG
writes it as given, where the page said the call that set it throws; and
inlineSvgdraws a gradient as its first stop, where the page said it only noted one.
- Two statements the old page made are corrected: a color a composition
cannot read stops the PNG render with an error that names it, and the SVG
writes it as given, where the page said the call that set it throws; and
Fixed#
- An inlined SVG paints what it can and says what it cannot.
inlineSvg, which places a vector asset in a composition and now draws a linked SVG in a PNG, read a gradient fill as no paint and fell back to SVG's default, so a gradient-filled logo came out as a black silhouette, and it said nothing of masks, group opacity or dashes. A gradient is now drawn as its first stop's colour, a group's opacity is folded into its shapes, dashes, caps, joins, stroke opacity andtext-anchorcarry over, and masks, clip paths, filters and patterns are reported innotes. - A composition image with no
fitfills its box in the SVG as well.fitdefaults tofill, and the rasterizer stretched such an image to its box, but the SVG writer leftpreserveAspectRatioout, so a browser letterboxed it. The writer now saysnone, as forfit: 'fill'. - An image stretched in the app stays stretched in its SVG export. The
canvas draws a placed image to fill its box, but the SVG writer left
preserveAspectRatioat its default, so a browser shrank a stretched image back to its own proportions inside the box. The writer now sayspreserveAspectRatio="none", and reading the file back is unchanged. - A PNG paints every CSS color name. The rasterizer knew 39 named colors,
so a composition filled with
steelblueorcornflowerbluewrote a correct SVG and then failed its PNG withunsupported color.parseColornow reads all 148 CSS names. Nothing that painted before changes, since the 39 it knew already had the CSS values, and the script language accepts exactly this set, so a script cannot name a color some output cannot paint. - The PDF writer paints every color the PNG does. Named colors,
eight-digit hex,
hsl()and a transparent page printed black. Colors are read with the composition model's parser now: a color's alpha is its opacity, a transparent page prints no paper, and a value that is not a color is left out and reported rather than printed black. - The PDF writer leaves a switched-off outline off, and dashes a dashed one. A fill-only shape printed with the outline the canvas and the SVG leave off, and a dashed or dotted line printed solid.
- A text box wraps in SVG and PDF exports. The SVG writer split a text
item at its newlines only, so a boxed caption exported as one long line. It
now breaks the box where the built-in face breaks it, as the composition
writers do, and keeps the text as typed and the box's width in
data-textanddata-box, which the importer reads back as the box. The PDF breaks it at the same words. - A composition group's opacity covers the group as one picture. The rasterizer drew each child at the group's opacity, so where two children overlapped, the one below showed through; the canvas painter did the same. A group is now drawn on a layer of its own and laid down once, as SVG draws it, and a translucent shape's fill no longer shows through its own outline.
- A composition path fills its open subpaths, as SVG fills them. The rasterizer and the canvas painter filled closed subpaths only, so an open path with a fill came out as its outline alone in a PNG.
- A script's group sits above the layers it holds. The sketch sink put a group's row below its children in the stack, so the Layers panel drew a generated group under what it held; the app keeps a group's row above them, and so does a generated book now. Paint order is unchanged.
napkin-sketch/graphic-design/filesis typed. Its entry in the package's exports map named the bundle alone, so a TypeScript project that imported it found no declarations. The entry names them now, as the main entry's does.- The GUI checks end the whole app. A check stopped the app with
child.kill(), which on Windows ends the main process alone, and the renderer it started could outlive it, orphaned and spinning a core. Runs piled them up until the machine slowed enough for checks to fail on timing.stop()intest/gui/cdp.mjsends the process tree withtaskkill /T, and every check uses it. - The package no longer ships this machine's Animation Mode install
record.
ai-helper/installed.jsonis local state, and git ignores it, butfilespacked it with the rest ofai-helper/, so a published tarball would carry one machine's record - which AI tool Animation Mode was installed for, and when - into every install, wherescripts/animation-mode.mjsreads it.package.jsonleaves it out now, andnpm run pack-checkfails if it comes back.
[1.0.0-alpha.4.3.0] - 2026-09-23#
Three features - Mirror Selection, Stroke Profiles and Mesh Warp - make this a minor release. It also carries the fixes that were to have been the 1.0.0-alpha.4.2.3 patch, which never shipped on its own.
Added#
- Mirror Selection: reflect the selection, in place or as a copy beside it.
A Mirror button after Clear, Edit > Mirror… and
Oopen a palette with the two orientations the mockup draws: Horizontal swaps left and right, Vertical swaps top and bottom, and both together make a half turn. Below them are Create Copy, Live preview and Show Selection Borders.- A copy is reflected about the selection's trailing edge, so it lands beside the original as its mirror image and the two meet there: half of something symmetric, mirrored, is the whole of it. Without a copy the selection flips where it stands.
- The reflection is exact on vector geometry. A reflection is affine, so
mapping a curve's anchors and both of their handles maps the curve itself:
nothing is resampled, and a curve keeps its control points. A Copic nib and
a linear gradient's direction turn with the drawing. Text stays readable,
its box moving to where its mirror image would be, and a placed image's
pixels are flipped. The rules live in one pure function,
mirrorStroke, with a test for each kind of element. npm run gui-check -- mirrorchecks it in the running app. A triangle that points right points left afterwards, with its box unmoved. A mirrored copy doubles the ink one box-width further right. OneCtrl+Zundoes either, and a cancelled preview leaves Redo exactly as it was.
- An edit transaction in the store. Mirror's live preview can add a copy,
which the Move and Rotate previews cannot express: they are reversible deltas,
applied and then taken back.
beginTransactionsnapshots the page, and everything the preview does runs without history.commitTransactionmakes it one undo step, or none at all if the page came back unchanged.rollbackTransactionputs the page, the selection and the saved state back. An edit that keeps history, an undo or redo, or a page change settles an open transaction by keeping it, since it is what the page shows, and then tells its owner. So dragging a previewed copy moves the copy, and the palette steps aside. - Stroke Profiles: how a stroke's width runs along its length. A Stroke
Profile control above the Width slider shows a picture of the current
profile and opens a picker of four. Default is the constant width every
stroke already had. Rounded is pointed at both ends and full in the
middle. Tapered is full at a round start and narrows to 0.3. Wave is a
band that snakes about the path as it swells. Select sets the profile new
pen and marker strokes take, and with the Select tool it reshapes the
selection too, as one undo step. The Properties panel's Profile select
sets one element's alone.
- The profiles were measured from the design drawings, each shape's thickness read column by column at 4x, rather than described by eye. Rounded is sin(πt) to within 3%, and Tapered is 1 - 0.7·t^1.6 to within 0.04. Wave is a table for each side, joined by monotone cubics that never overshoot a measurement or dip below zero.
- Pen and marker marks take a profile, and so does everything that commits as
a pen stroke. A ruled line has only its two ends to read a profile at, both
of zero width for Rounded, so a long run is cut into steps first. Dashes
keep the width the stroke has where they fall, and the pen's pressure still
scales the width underneath the profile. Mirror reflects a profiled stroke
exactly: Wave, the one profile that leans, swaps its sides
(
profileMirrored) so its mirror image leans the mirrored way. - Neither the canvas nor SVG has a variable-width stroke, so both draw the
shape. The canvas fills a union of convex pieces with one non-zero fill.
The exporter writes that union's boundary: it traces a chain whose winding
about every point is the number of pieces covering it, then keeps only the
edges with fill on one side and none on the other. So the two agree exactly
however tightly a path bends, and a test rasterizes both on twelve shapes -
from a hairpin tighter than the stroke to a ring smaller than it - holding
them to 99.9% of the same pixels. The stroke rides along as data
(
data-profile, the centreline indata-d), so napkin reads back the stroke rather than its outline. A profiled shape with a fill exports as a group of the fill and its outline, and PDF export fills the same outline. npm run gui-check -- profilechecks it in the running app. A Rounded line is under half as thick a tenth of the way along as in its middle, and oneCtrl+Zundoes it. The Properties select tapers a stroke without changing the tool, andEndthenEnterpicks Wave for the next stroke. A fixture the exporter wrote imports with every profile, fill and dash intact.
- Mesh Warp: bend art by pins, as Illustrator's Puppet Warp does. A Mesh
Warp tool sits below the others in the side rail, set off by a heavy rule.
Hovering outlines in green the art a click would pick, with "(click to select
art)" beside the pointer. A click meshes it and puts two pins along its long
axis. Click the mesh to add a pin, drag pins to bend the art, and press
Deleteto take pins out.Enterkeeps the warp as one undo step, andEscapethrows it away.- The mesh comes from what the art paints. Its silhouette, grown by 3 px, is traced by marching squares, filled with a hexagonal lattice and triangulated (Bowyer-Watson Delaunay). Any triangle that strays across a notch or a hole is dropped, and so is any sliver Delaunay lays along the hull between two pieces. The mesh has about a thousand vertices whatever the art's size. Show mesh in Quick Settings hides it.
- The bend is As-Rigid-As-Possible shape manipulation (Igarashi, Moscovich and Hughes, 2005). Its two matrices are factored once when the pins change, so a drag frame is a few triangular solves: about a millisecond at a thousand vertices, and about 4 ms a frame on a figure's leg assembly with the art carried along. A piece with one pin follows it rigidly, and a piece with none stays put.
- The art keeps its structure. A Vector Path's anchors and handles move, and a curve is split only where it would otherwise stray more than a fifth of a pixel from the bend. A warp nobody pulled gives back the very anchors it was handed. Text and images move unbent, a Copic nib turns with the art, and no width stretches.
npm run gui-check -- mesh-warpchecks it with real pointer events on a bar. Hovering outlines it, a click meshes it with two pins, and dragging the right pin 80 px down drops the right half while the left holds.Escaperestores the bar exactly,Ctrl+Zinside a warp steps back,Deletetakes a pin rather than the bar, and a kept warp is oneCtrl+Zfrom where it began.
npm run gui-check: scripted checks against the running app. The first two defects fixed in this release could only be seen in the running app, one because the importer needs a DOM and the other because the color picker is a native popup.test/gui/now holds the DevTools-protocol driver that the 1.0.0-alpha.4.1.0 menu work proved out (cdp.mjs), a runner that launches the built app once percheck-*.mjs(run.mjs), and seven checks: gradient import, the color picker and the Width slider reaching a selection, Mirror, Stroke Profiles drawn and imported, and Mesh Warp. The checks assert on what the page shows (canvas pixels, the toast, the layer rows) rather than on the code that produced it, and the first two were each run against the unfixed build and failed there. They drivedist/, so build first.npm run gui-check -- gradientnarrows a run to checks whose names match.
Fixed#
- Gradient-painted artwork imported as an invisible ghost.
importSvgrestored a gradient only fromdata-gradient, napkin's own export attribute, and a foreignurl(#...)paint was dropped on purpose. A drawing whose base shapes were all gradient-filled therefore came back with correct geometry, a correct layer tree, and nothing painted - and since the shapes were all still there, nothing about it said why. Worse,normalizeColorpasses through anything that is notrgb(...), so the same paint was stored as the stroke's colour and written back out asstroke="url(#skinLimb)": a reference to a definition the export does not carry.- The importer now reads the referenced
<linearGradient>or<radialGradient>out of the document's defs, follows anhrefto another gradient's stops where one is used, derives a linear gradient's angle from its axis, and hands the result tonormalizeGradient. What it does not carry is the paint server's own coordinate system, so a gradient lands as its stops along its own axis: approximate for the unusual cases and right for the rest. - A paint that still resolves to nothing falls back to a stop colour rather
than to the
url(...)string, so the worst case is a flat colour from the right ramp rather than a shape nobody can see. That holds for every way a paint server can fail to become a gradient, not only for an outline. A one-stop gradient paints its stop's flat colour, as SVG specifies. A reference to a gradient the document never defines, or to one with no stops, paints the fallback colour written after it (url(#skin) #c48a5f), or napkin's default ink when there is none: visibly wrong and one click from fixed, where a browser would paint nothing. A fallback written beside a gradient that does exist no longer hides that gradient; the reference used to be read only when it ended at its closing parenthesis. npm run gui-checkgained a check for it, against a fixture whose every shape is gradient-painted. This is the bug a unit test could not have caught: the importer needs a DOM, and the symptom is pixels. The check measures how much of the painted area's bounding box is filled rather than counting painted samples, because the ghost is not blank: it leaves thin outlines behind, and a raw count left it two samples short of passing. Filled, the fixture covers 80% of its box; the ghost covered 12%.- Anything an editor or a generator paints with gradients was affected. An AI redraw asked for modelled form reaches for gradients first, so this is what one looked like on arrival: a figure drawn correctly and then erased on the way in.
- The importer now reads the referenced
- The custom color picker never reached the selection. With the Select
tool active and something selected, a swatch fills the selected closed shapes
and recolors the selected open strokes. The color well beside the swatches
only ever set the ink for the next mark, so a fill or stroke color picked
there left the selection as it was. Because the pick recorded no history
either, the next
Ctrl+Zundid whatever came before it.- The well now takes the swatches' path. One
applyInkToSelectionserves both, so the two cannot drift apart again, and both report what they did in the same toast. - Chromium's picker fires an
inputfor every color the pointer crosses and onechangewhen it closes. The selection follows the drag live, and the whole drag is one undo step:Store.fillSelectedtakes the samehistoryflagsetStrokePropsalready did. The toast waits for the color actually chosen. npm run gui-checkcovers it: a closed shape and an open stroke, selected withCtrl+A, taken through a three-color drag. Afterwards nothing on the page is left in the old ink, and oneCtrl+Zputs every sample back.
- The well now takes the swatches' path. One
- The Width slider never reached the selection either. With the Select
tool active and an element selected, moving the slider only set the width
of the next mark. The selected element kept its own until another was
drawn, so the only way to change it was the Properties panel.
- The slider now takes the color well's path. With the Select tool and a
selection, the selected outlines follow the slider as it is dragged, and
the whole drag is one undo step: its first change opens the step, and
setStrokePropsfolds the rest into it. A width typed with Quick Width (W, then a number) reaches the selection the same way. Text and placed images have no outline to widen, so they are left as they are. With a drawing tool in hand, the slider is still only for the next mark, whatever is selected. npm run gui-check -- stroke-widthcovers it on a 4 px line. The line is selected withCtrl+Aand dragged through 8, 14 and 20: it becomes five times as thick, oneCtrl+Zrestores it, Quick Width12makes it three times, and with the Pen in hand the slider leaves it alone.
- The slider now takes the color well's path. With the Select tool and a
selection, the selected outlines follow the slider as it is dragged, and
the whole drag is one undo step: its first change opens the step, and
- Undo and redo stitched the holes shut in compound shapes. Every history
snapshot copied a vector anchor as its point and two handles and left out
move, the flag that starts a new subpath. The canvas draws the sampled points, which kept theirs, so nothing looked wrong until export. A square with a square hole exported asM0 0H20V20H0V0ZM5 5H15V15H5V5Zbefore a move and an undo, and asM0 0H20V20H0L5 5H15V15H5L0 0Zafter, with the hole joined into the outline. A snapshot copies the whole page, so one undo did this to every letter with a counter, ring and outlined stroke on it. Placing several imports in a grid rebuilt anchors the same way, so those arrived already stripped. Both now keep the flag, and a snapshot now holds its own copy of a gradient rather than sharing the live one. - A stroke with subpath breaks but no Bézier anchors exported as one line.
The exporter's polyline branch wrote a
lineTofor every point after the first, so a compound stroke that had lost its anchors came out with its contours joined, and its simplification ran straight across the break. Each run between breaks is now its own subpath, simplified on its own.
[1.0.0-alpha.4.2.2] - 2026-09-13#
Added#
Disable API: a posing choice for a drawing the rig does not fit. Animation Mode measures the source figure's own geometry and hands the AI helper a finished angle per assembly. Measuring only reaches a layer whose name one of the six required assemblies answers to, and an illustrated character has more layers than that. On a real eleven-layer walk frame five of them - the skirt, the shirt, the glove and the two jacket halves - match nothing at all, so the measuring can never write them an angle and they come out of every frame frozen. Four layers moved and seven held still: the cardboard cut-out the skill has warned about since 1.0.0-alpha.4.1.4, arriving by way of the app rather than by way of the helper.
- A Posing radio pair in the animation setup dialog: Measure the joints, which is the default and unchanged, or Disable API.
- Disabled, the form carries no angles, says the silence was chosen rather than missing, names the layers no assembly can reach, and asks the helper to pose every layer from the drawing, the note and the skill's measured movement budgets.
- The dialog counts the unreachable layers for the figure in front of you - "5 of 11 layers here match no assembly" - so the choice can be made before a sequence is drawn rather than after eight frames of it.
- What the setting does not touch is the rule that a frame is posed and never redrawn. It changes where the angles come from, not what may carry them, and the form says so in both modes.
Every form carries its type's measured travel band. Switching the measuring off used to leave the helper with no numbers at all, and it showed: a walk came back with the arms travelling 10.6% of the figure's height, past the 9.4% that is the widest any of the 70 drawn steps managed, and with barely any bob. The form now prints that type's own band -
legs 8.9 (3.2-26.2) arms 4.8 (1.2-9.4) bob 0.6 (0-1.9)for a walk - for the six types the studies cover. Both modes get it, for opposite jobs: with the measuring off the numbers are the target, and with it on they are a check on angles already chosen, since a frame landing far outside what every drawn frame does went wrong in the editing rather than in the measuring. The measuring mode gets the short version, because that form is read before every frame of every sequence and every line of it is time spent before the first edit. A type the studies never measured gets no numbers rather than invented ones.A looping type is told its bob has to change direction. A band gives a size and bob has a sign, which is the one thing a band cannot carry: a walk that bobs 1.3, then 0.7, then 0.6 sits comfortably inside its band on every step and is still a figure sinking rather than one walking. Five of the six drawn walks change the sign of their bob somewhere in the cycle - BadGirl's own goes 0.6, then -1.3 - and a cycle that only ever drops cannot arrive back at the pose it started from. Types that run from a start to an end are not given the rule, because they have no pose to come back to.
A Transform tool, on
Ctrl+T. One box around everything selected, with a handle on each corner and the middle of each side, and a drag on any of them scales the selection. The model already knew how to do this —scaleStrokesmaps points, Bézier anchors and both tangent handles, and carries a text item's size and an image's box with them — so what was missing was the box and the four rules that make a handle mean something:- The handle decides which axes move, and no modifier changes that: sides across, top and bottom down, corners both, each axis following the pointer on its own. The handle opposite the one in hand stays put, so a drag reads as pulling that edge of the box.
Shiftmakes the two factors agree, so the selection keeps its shape. On a corner the axis the pointer committed to wins; on a side, the one axis that handle can measure drives both — which is how a side handle scales the whole selection instead of stretching it.Altmoves the point the scale is measured from to the centre of the box, so both sides travel together and the selection grows in place. It changes the anchor and nothing else: a corner underAltalone still scales each axis independently.Shift + Altis both, and either can be pressed or released during a drag — every factor is measured from the box the gesture started on, so a modifier halfway through re-reads the whole drag rather than bending what is left of it.- The box stays up until
Ctrl+TorEscape, a press off a handle still belongs to the tool underneath, a whole drag is one undo step, and a press that grabs a handle and lets go again costs nothing. - Dragging a handle through its anchor stops at 1% instead of flipping.
A negative factor mirrors geometry correctly but
scaleStrokesreads a font size and an image width as magnitudes, so a flip would mirror the shapes and destroy the text. Stopping is the honest failure until that is handled; it is on the TODO with skew, distort, perspective and puppet warp, which is the list this one box exists to grow into.
A click on the canvas puts the rotation centre anywhere. The nine presets reach the corners and the middle of the selection's box, which is where a pivot usually goes and nowhere near where an interesting one does - a shoulder, a heel, a point off the shape entirely. Those needed the crosshair dragged or two numbers typed. A press that turns nothing already did nothing, so that is the gesture this takes: click to place the pivot, drag to turn about it, told apart by the same few pixels that separate a click from a drag everywhere else in the app. The presets are untouched.
The Rotate tool stays active once a rotation has been applied. It is a workbench - an angle, a direction, a snap, a pivot that took placing - and dismissing it on the first answer threw all of that away with the answer, so every further turn cost a reopen and a re-place. Two things used to dismiss it: the Apply button, and letting go of a canvas drag when
Ctrl+Rhad opened it. Neither does now. The code already half agreed about the first -applyRotateis documented as leaving the dialog open and Enter has always done exactly that; only the button disagreed. Which kind a tool panel is now lives in one place:toolRemainsInViewfor Rotate,toolGoesOutOfViewfor Move, which asks one question and goes once it is answered - so a third panel of either kind is a line rather than a fourth Apply handler with an opinion, and the flag that used to carry theCtrl+Rexception is gone.
Fixed#
Characters that walk left had their legs swinging backwards. Every signed angle Animation Mode hands out - the cycle tables in
animation-cycles.ts, the transforms built from them, and the copy of the walk table thevector-animationsskill carries - was measured from the wireframe skeletons, and every one of those walks to the right. Nothing anywhere said so. Mirroring a figure negates every rotation in it, so the table applied as written to a left-facing character inverts the whole cycle, and the result is a defect that passes every check there is: the travel numbers land in range, arms and legs still oppose each other, no layer is frozen, and the figure moonwalks.Measured on a real case. BadGirl's generated walk turned her front leg +12.3° and her back leg -13.5° -
WALK_CYCLE[0]copied verbatim, which is what the helper does with the skill's table when the measuring is off. The artist's own drawn BadGirl walk turns them -17.7° and +16.8°: the same cycle, every sign the other way. A hand-edited reference of the pose wanted came out at -40.1°/+33.1° - the same signs as the artist, the opposite of the app.- A Facing choice in the setup dialog, Travels right or Travels left, preselected from the figure's own feet. A foot sticks out in front of the ankle, which makes it the one part of a figure that cannot be read two ways; two feet pointing the same way are a profile, and two pointing opposite ways are a drawing with no facing at all. The note says which it found, so a preselection is visibly a reading rather than a finding.
- Travels left mirrors the cycle -
mirrorPoseStep, which negates every angle and leaves the bob alone, because that is exactly what mirroring art about a vertical axis does. Saying nothing still means right, so no existing sequence changes. - The premise is now written down where each set of numbers lives: the generated table says it (from the generator, so it survives a regeneration), the skill says it twice - once where the sign convention is introduced and once in the walk table itself - and the form says which way it read this figure in both modes. With the measuring off the form tells the helper to flip the signs itself, because there the helper is reading the skill's tables directly and is the only participant that can see the drawing.
A press inside a selection was losing the selection. Hit-testing is forgiving on purpose: every mark is widened by a few screen pixels, or a thin line could not be clicked at all. That forgiveness outranked the selection, and a selection of several elements is usually several elements close together with other marks among them - so pressing in the middle of what you were about to drag landed "on" some unselected neighbour, replaced the whole selection with it, and dragged that one thing instead. The selection was lost most reliably in precisely the case the surrounding code was written to protect. The selection now keeps the press; the element under the pointer gets it back on release, if the pointer never moved, which is what a click on it always meant.
Shift and Ctrl in the layers panel were the wrong way round. Shift-click toggled one row and range select was on Ctrl+Shift - a chord for the common gesture, and Shift doing the thing every other layer panel gives to Ctrl. Ctrl/Cmd now picks rows out one at a time and Shift takes the run between the last row selected and this one. The range is drawn on the rows the panel is showing rather than on the layer stack: the stack inlines every group's children, so a range across a collapsed group used to select forty rows behind three visible ones.
Anything imported unfolded the whole layers panel. One illustrated figure is a group of assemblies, each a group of parts, each a group of outlines - sixty-odd rows arriving expanded, and two of them leave the panel showing nothing but itself with the page scrolled off the top. Whatever arrives is one thing to the person who imported it, so it now lands folded: a generated animation frame, an SVG opened from the menu or the CLI, one pasted from another editor, and a sheet of graphics placed together. Every group inside is folded too, so opening one shows its assemblies rather than spilling its whole tree, and the row left active is the one the import produced rather than the last leaf it happened to write - which was inside the fold, where the panel showed no active row at all. An import that is a flat run of layers with no group among them still fills the panel: there is nothing there to fold.
The form asked for 92 KB of reading before the first edit. "Full contract and references ... (read before editing)" stood over three documents: the skill, the same contract again as canonical instructions, and the 24 KB curve skill - named four lines after the form had already said to reach for that one only when a frame needs new geometry, which a pose never does. Step 1 then said to open a 60 KB drawing that is four fifths
d="..."path data the frame must not touch. Reading all of it is a defensible way to spend a run, and one run spent 520 seconds doing exactly that and was cancelled having edited nothing at all. One read is required now - the skill, which is the whole contract - with the instructions and the curve skill moved under Reach for the rest only when this frame turns out to need them. Step 1 says to search the drawing fordata-name=and read a window around the group being posed rather than the file. Required reading drops from 92 KB to 55 KB, the drawing stops being read whole, and the form pays 250 bytes for it.The form never said how the finished frame reaches its output path. It forbade copying the source to that path and editing it there - the mistake that puts the previous pose on the page - and then never mentioned the other order. What is left is the reading a helper will actually take: pull the document in, hand it back out, which is the whole 60 KB of path data through the helper twice for a file that is already correct on disk, and it is the exact failure the rest of the form is built to avoid. The step now says to copy the file, with a shell copy, and why. (The one run in four since the app started noticing that did edit its source in place was also the fastest, 158s against 287s, 306s and the 520s cancellation - four runs is a hint, not a finding, and the missing instruction is worth closing either way.)
Nothing told the helper how long a frame was supposed to take. The app's limits only ever kill a run; a limit is not a budget, and the helper is the only one who can spend one. The form and the frame agent now say a frame should take under five minutes and that reading is what spends it - five being the round number above the 158s, 260s, 287s and 306s the last four finished frames cost. The app's own limit stays deliberately looser: a slow frame that arrives beats a fast failure.
A reference picture the helper could not reliably see. The skill's pictures are black line art, and
assets/illustrated-single-character-actions.pngwas 89.8% transparent - a drawing with no ground under it, which flattens against whatever the viewer puts behind it and disappears entirely against a dark one. A white-background re-export existed but had landed beside it asillustrated-single-character-actions.svg.png, a name nothing pointed at, so the skill went on serving the transparent copy. The opaque export now holds the name the skill uses, and all six of the skill's pictures are opaque. The SVGs stay transparent on purpose: a background rectangle in an SVG is a real layer, and a real layer is one more thing the helper has to pose.The stall limit killed helpers that were still reading. Even counting writes as activity, five minutes is not long enough for the half of the job that writes nothing: the helper loads a 46 KB skill, 13 KB of instructions, the form, and a reference sheet that is half a megabyte as a picture, then finds every layer it has to pose inside a 60 KB drawing. That was survivable while a frame meant four assemblies and stopped being survivable at eleven layers - a second sequence died at 302s having never touched the frame. The limit is now split by phase: ten minutes before the first edit, five after it, because silence before the helper starts editing means it is reading and silence afterwards means it stopped. The status line and the log say which phase a run is in, so a stall names what the helper failed to do rather than only how long it was quiet.
The five-minute "stall" was a hard cap on the whole run. The limit was refreshed by helper output alone, and
claude -pprints nothing until it exits, so a run that was working looked exactly like a hung one. It went unnoticed while a frame took two minutes and surfaced the moment the job got bigger: posing eleven layers instead of four took 199s, then 260s, then the third frame was killed at 301s mid-edit. A sequence asked for four frames and stopped at two. Writes now count as activity - the helper edits the source once per layer it poses and renders a preview beside it to grade itself - so a working run is never mistaken for a hung one, while a helper that has genuinely stopped still gets killed on time.The helper's reply was thrown away on every run the app has ever made. The child was killed the instant the frame file appeared, which is a moment before it says what it saved and what grade it finished on. Every
run end: stdout head:in the log is blank, including the runs that came back wrong, so a frame with a known problem arrived with the sentence naming that problem already discarded. The frame now waits up to four seconds for that line, and resolves the moment it arrives.
Changed#
- The reference sheet names its characters, and the skill indexes them.
illustrated-multiple-character-actionsnow titles each figure on the page - Character I to Character V - and brackets each strip with the action it draws. The skill carries the index that turns a title into something searchable (Character IV isBadGirl; Take Damage is thedamagestrip), together with what each character actually has drawn, and says to go to the one strip rather than to the page. This is reading time, and reading time is the half of a frame that has no output to show for itself: it is what the ten-minute setup window above is spent on. - The
vector-animationsskill and the frame agent both read an angle-less form. A form saying the measured transforms are switched off is an instruction to pose every layer, not a gap to improvise into; the skill's movement budgets are the yardstick precisely because nothing else is.
[1.0.0-alpha.4.2.1] - 2026-09-12#
Added#
Brand resources for generated graphics. A design language is measured from one asset, so it can record where a logo goes and never which logo - that is a property of whoever is drawing next, not of the file that was measured.
references/resources.mdcarries the second half: a markdown list of- key: valuethat a generated skill reads before it draws.- The format is a list and nothing more, because the file is edited by
whoever owns the brand rather than whoever owns the build. Headings, prose,
>notes and fenced examples are ignored, so the file can explain itself and still parse. A value with a folder separator or a media extension is a path; anything else is text to draw, which is the whole rule behindexample.comprinting as a domain andassets/logo.svgbeing drawn. - In a
GLOBAL_ASSETSfolder the file name is the slot.logo.svgfills the logo,footer.pngthe footer,logo-mark.svgfillslogoMark. No manifest, and therefore no second place to fall out of step: adding an asset to the folder is the whole of adding it to the brand. - Nothing has to ask for the assets. A caller never writes "include the linked assets". If the declared paths resolve, every graphic the skill draws carries them.
- An unconfigured file is a working state. Each brand slot falls back to a
mark in the design language - a monogram of the brand name on the accent, or
a set wordmark - so a page with no brand is still a finished page, and
obviously a placeholder to anyone holding the real logo. A generated
resources.mdships with values the parser rejects (path/to/...,TBD) so a fresh skill is unconfigured by construction rather than by accident.
- The format is a list and nothing more, because the file is edited by
whoever owns the brand rather than whoever owns the build. Headings, prose,
Vector assets are inlined, not linked (
inlineSvg,placeBrand). The rasterizer decodes PNG and nothing else, so an SVG logo placed as animagerenders in the SVG export and is a hole in the PNG - silently, with the reason inwarningswhere nobody looking at the picture would find it. The asset's shapes are read into the composition instead, so both renderers draw the same mark and it stays vector, scaling without going soft.rect,circle,ellipse,line,polygon,polyline,path,textand data-URLimageare handled, painted by presentation attribute, inlinestyle, or a class in a<style>block - which is what a design tool writes. Nested<g>transforms are composed and applied. Gradients, patterns, filters,<use>and elliptical arcs land innotesrather than being approximated.Brand position detection (
detectBrandSlots,scanBrandBands,findBrandSlots), reported by the analyzer and written into everyDESIGN_LANGUAGE.mdas a table.- By layer name first. A designer who called a layer
logohas already marked the slot, and reading it costs one pass over the markup. Names are read fromid,data-name,inkscape:label,serif:idandaria-label, so Illustrator, Inkscape, Affinity and Figma exports are covered without asking which tool wrote the file. Each slot reports a box, a region, a page share and a confidence. - By scanning second. Failing a name, the media is read a quarter of the
page at a time, top first, stopping at the first band holding a compact
mark: ink that covers a little of the band and is gathered rather than
spread across it the way a line of text is. That spread test is what
separates a logo from a headline. It is a heuristic with no idea what a
logo looks like, it says so in
notes, and its confidence never reaches a name's.
- By layer name first. A designer who called a layer
generate-skill.mjs. Measuring an asset, writing down what was measured and scaffolding a script against those numbers is arithmetic, and arithmetic should not need a model. The generator writes the skill, its design language, itsresources.mdand its brand resolver, then prints how to point it at a brand. Two overwrite rules that differ on purpose: a design language file is never overwritten (a second asset getsDESIGN_LANGUAGE-<stem>.md, because a reader's corrections are the most valuable thing in the first one), and a skill is overwritten only after asking (--forceanswers in advance, which is what a test passes).npm run test:graphic-design-api. An end-to-end check of the road to a graphic rather than of the graphic: read the reference asset, generate a skill from it, point it at the symlinked brand assets through aresources.md, draw a card and a cheatsheet that carry them intotest/graphic-design-api/generated-graphics/, and make 36 assertions about what came out. Every step is a production entry point invoked as the documentation says to invoke it, so a pass means a clone followingAPI-QUICKSTART.mdworks. It needs a file system, a symlinked folder and a generated script, none of which a bundlednode:testsuite can exercise honestly.test/brand-resources.test.ts. Twelve standards across the three mechanisms that can each fail quietly: reading the file, inlining a vector, and falling back. The inlining test is the one worth keeping - it renders the same logo twice, inlined and linked, and asserts that the inlined page has the logo's green in its pixels while the linked one is blank paper.napkin-sketch/graphic-design/files. The Node-only file helpers now build todist/graphic-design/files.jsand are reachable by that subpath. They were documented as a deep import intodist/core/, which the package'sexportsmap blocked and which the build never emitted - a documented import that existed in neither form. Every doc now names the path that works.A generated skill registers itself, so using it needs no paths.
generate-skill.mjswritesreferences/graphic-design-api.jsonas its last act: which script draws, whichresources.mdit draws with, where output goes, the modes the script can draw, and adefaultRequestfor when the request does not say. That file is the difference between a skill that exists and one that can be used without a manual.- A request becomes a command by string matching, deliberately. Each mode
carries its own keywords -
cheatsheetanswers to "cheatsheet", "syntax", "html", "code" - and the default wins when none appear. That is why a baregenerateworks: it contains no mode keyword, so it falls through to the registered request. Anything cleverer would need a model, and the point of this file is to be the part that does not. - The project's
resources.mdwins over the skill's copy. A generated skill carries a snapshot so it runs anywhere on its own; a registered command passes the project's file explicitly, because that is the one somebody edits. --no-registeropts out,--default-requestsets what a bare request means, and--registrationonbrand-resources.mjsprints the whole wiring plus the exact command it produces.
- A request becomes a command by string matching, deliberately. Each mode
carries its own keywords -
The generated script draws a cheatsheet as well as a card.
--cheatsheetswaps the paragraph for a panel of syntax-coloured code, advanced by the monospace metric rather than bymeasureText, with the plate above it sized from the heading it actually holds. The syntax colours are taken from what the source declared and kept clear of the four structural roles, so a code panel cannot quietly reuse the accent and leave the page with two things claiming to be the important one.- This is also the honest test of the claim that a design language is not a template: the source asset carried no code at all, and the language carries a cheatsheet without being stretched.
Every reference asset ships as a picture as well as a source, and Animation Mode was taught to use it.
<name>.pngnow sits beside<name>.svgin bothvectorsskills - twelve sheets in all - and the two answer different questions: the picture says what a movement looks like, the source says where the anchors are and what the layers are called.- The frame agent looks before it poses. A new first step picks the preview for what is being drawn - the wireframe rig for a cycle, the illustrated sheets for a character action, the object rigs for a break or a bounce - and reads it. The travel tables already said how far a part moves; nothing said what the result was supposed to look like, which is exactly the half that went wrong. A frame with every angle inside budget can still read as a stumble rather than a stride, and now there is something to check it against. The existing "look at what you drew" pass compares against that same reference rather than against an idea of it.
- The two illustrated sheets stop being unreadable. They are 1.5 MB and
2.5 MB, and the skill's standing instruction was simply not to open them - a
reference nobody can look at. Their previews are 396 KB and 601 KB as
pictures, one
Readeach, and every number in them was already measured intoillustrated-frames.json. Picture plus numbers is now the whole of what those files have to give, and the instruction is "look at this" rather than "do not touch that". - The split is written down rather than implied. Both skills carry a table
of which question goes to which file, and the rule that a small sheet like
shapes.svgis cheaper to read outright than to look at - the source is the half with the coordinates in it.
test/ai-helper.test.tsfails if an asset arrives without its preview, or a preview outlives its drawing. The failure it guards is silent: an asset with no picture is one a tool quietly stops looking at, and nothing about the resulting frame says why it got worse. A second standard checks each PNG is a real image with usable dimensions, and measures the claim the skills make about the two illustrated sheets - "look, do not read" only holds while the picture is the cheaper of the two, so that is asserted rather than assumed.Animation Mode grades a frame and acts on the grade.
npm run frame-preview -- <frame> --against <previous> --gradenow ends with a verdict -pass,reviseorsave- an ordered list of what to fix, and aVERDICT:line a caller can branch on (--strictputs it in the exit code too). The measuring was already there; what was missing was a decision, so the report was printed and the run walked away from it.- The stop flag is a flag.
--passes(two by default) turnsreviseintosaveonce the budget is spent. A run that keeps polishing is a run the app kills at five minutes with nothing on disk, so the last pass saves what it has and reports the defects it knows about rather than hiding them. - Findings are ordered, and only the first is worth a pass. The rest are usually its symptoms, and splitting one remaining pass across a cause and its own smoke fixes neither.
- The stop flag is a flag.
A redrawn frame is now caught, by reading the file rather than measuring the drawing. A frame is posed - the movement lives in
transformattributes and the path data is untouched. Re-emitting the geometry instead produces a figure that is complete, layered, opens fine, and has its limbs drifted off their joints; it reads as a bad drawing rather than as a broken process, which is exactly why it survives a look and reaches the strip. The signature is exact and needs no tolerance: a posed frame shares its path data with the frame it came from, a redrawn one shares none of it and carries no transform.Animation Mode asks what the animation is for, and passes the answer to the helper. A new optional field in the wizard's setup step - "What is this animation for?" - is carried into the form as an
<animation-note>block and handed to the AI helper with every frame of the sequence.- It is the one input in the form that was not measured. Everything else describes the figure and the step: the assemblies, the layer inventory, the pivots, a finished transform per assembly, how far each part may travel. None of it says why the sequence exists - what the character is doing, what it is carrying, where it is going - and that is exactly what settles a choice the travel numbers rate equally.
- It outranks the animation type. Both answer the same question - what is this sequence - and the dropdown answers it with one word out of fourteen while the note answers it in the user's own sentences. Where the two disagree the note decides and the type's template yields to it, and the form says which way round that goes twice: once beside the note, once beside the template it outranks. Asking the question and then ranking the answer below a dropdown would have wasted the asking - the field's own placeholder, "carrying something heavy in her right hand, so that arm barely swings", was an instruction the first precedence rule forbade acting on.
- Two things it does not outrank, and neither is about what the frame
shows. The measured transforms keep their joints: with a note in the form
they stop being a dictation and become the amounts to start from, so a note
about weight moves an angle - by the smallest amount that reads, about the
same pivot, and named in the reply. And no note licenses a redraw: the path
data stays exactly as it is and the movement lives in
transformattributes whatever the note says. That second rule is the one every failing run has broken, and keeping it out of the note's reach is precisely what makes widening the rest of this safe. A note reading "just redraw her however looks right" is the case, and it has a test. - Read with the skills, never instead of them. The note says what the movement is; the skills still say how a frame is made. A helper that cannot honour part of a note says so in its reply rather than dropping the measured pose, and a note describing a movement the chosen type cannot be is reported rather than half-drawn.
- It describes the sequence, not the step, so the same note reaches frame six as reached frame one.
- The block is tagged rather than quoted, and the closing tag is
neutralised before the note is written. That one is a correctness fix
rather than tidiness: a note carrying
</animation-note>would close the block early and everything after it would read with the authority of the form rather than as a note inside it - the difference between describing an animation and rewriting the job. Control characters are stripped, blank runs collapse, and the whole note is capped at 600 characters so it cannot crowd out the steps it is meant to be read beside. normalizeAnimationPromptdoes the cleaning where the text is read rather than where it is used, so the one place that knows it came from a person is the one that tidies it.
Changed#
The
vector-graphicscharacter and projection sheets were re-exported.isometric-objects.svgandperspective-objects.svggained whitebackgroundrectangles so their previews are opaque rather than transparent;male-character-elements.svggained named head and beard parts (bald-head,goatee,neck-bald-head);female-character-elements.svgis the same drawing, smaller.npm run test:graphic-design-apiinstalls what it tests. It generates into the AI tool's real skills folder rather than into.tmp/, refreshes the helper first so the skill answering a request is the one this repository ships, and writes the project wiring - so the command is plug-and-play by the time it finishes rather than merely green.--forceremains the only concession to being a script: interactive use still asks before replacing a skill, and a script cannot answer.The project's brand wiring moved to
references/at the repository root, fromtest/graphic-design-api/references/. That is where the resolver already looks - it walks up from the working directory - so the previous location was only ever reachable by passing--resourcesby hand. It is gitignored for the same reasonai-helper/installed.jsonis: it names whichever assets a checkout points at, and the registration beside it carries a timestamp.The composition skill checks whether it is already wired up before composing anything from scratch.
/graphic-design-api <request>now reads the registration, matches the request to a mode, and runs the generated script, rather than writing a new script that agrees with the captured language by luck. It is also told, in as many words, not to ask a caller to request the linked assets: that has never been something a caller had to supply.Colour roles are decided by area, not by reference count. A vector palette counts how often each colour is referenced, so hundreds of small white glyphs outrank one navy field covering the page, and the mechanical role guess names white the ground - exactly backwards. The analyzer now also inlines the asset, renders it small, and counts pixels, reporting that as
areaPaletteand aPalette by areatable. The declared palette is unchanged, because it answers a different question and a reader comparing two vectors wants it. This is the correction the shipped worked example had to make by hand; it is mechanical, so it is made for you now.The generated composing script sizes its plate from its content. Both renderers lay text out from the same table, so a plate can be measured from what it will hold rather than stretched to the space available and left two thirds empty.
The frozen fixture skill carries brand slots.
composeandcomposeCheatsheettake an optionalbrand; without one, every slot falls back to exactly what the script drew before, so its output is byte-identical and the existing standards intest/generated-skill.test.tsstill hold.
Fixed#
A three-frame
walkcame back as a stretch, and every check passed it. The run that prompted this re-emitted all 121 paths in each of its three frames and set no transform anywhere, so the legs swung through wild arcs while the torso, skirt, shirt, head and both jackets sat frozen. The grader had already printedarm OVERandclothing UNDERon two of the three frames, and7 layer(s) did not move at allon two more - and nothing was required to act on any of it. The measurements were right, the loop around them did not exist.inlineSvgdropped atransformwritten on the shape itself, keeping only the ones on ancestor<g>elements. An editor writestranslate(...) rotate(...)straight onto a rotated rect as often as it wraps one in a group, so the shape landed in the right place at the wrong angle - silently, and looking like a drawing that was always that way. Found by rendering thevector-animationswireframe rig, where every hand and foot is a rotated square and came out axis-aligned.- Measuring had to learn the same lesson twice:
detectBrandSlotsgave up on a shape that arrived wrapped in its own transform, so a named layer of rotated parts measured as nothing at all, and then - once it followed the wrapper in - counted the shape's transform a second time on top of the one it had already composed, which put the box off the page. Bounds now recurse through the wrapper, and a container's transform is the only one composed by the walker.
- Measuring had to learn the same lesson twice:
encodePngencoded a blank image instead of refusing a size it could not use. Passing it a wholeRasterResultrather than that object's three fields - an easy mistake, since it carries exactlydata,widthandheight- leftwidthundefined, made the strideNaN, and sailed past the "buffer too small" guard, because every comparison againstNaNis false. The result was a 65-byte PNG of nothing and no error anywhere. It now checks the size before the buffer.The brand asset symlinks resolved nowhere.
test/graphic-design-api/test-assets/logo.svgandfooter.pngstored targets written from the repository root rather than from the link's own directory, which is where a symlink is resolved from. They resolved on no clone at all, and because a missing brand asset falls back to a drawn mark rather than failing, the graphics would have come out looking plausible and carrying no brand. Both are now../reference-graphics/links/..., committed as mode120000with forward slashes, with a standing test.The fixture symlinks under
test/graphic-design-api/skill/broke when the graphics moved intoreference-graphics/, and with them thirteen tests that read through those links or the oldlinks/andcreated-*paths. Targets and test paths are corrected.A relative path in a copied
resources.mdis rewritten for its new location. Moving the file without rewriting them breaks every asset it names, silently, for the same fallback reason as above.
[1.0.0-alpha.4.2.0] - 2026-09-12#
Added#
- A test suite for generated skills.
test/generated-skill.test.tsrenders eight variations of a graphic through a frozen copy of a skill the graphic-designer helper generated, and measures each one. No model runs: the helper's job ends when the skill is generated, and everything after that is deterministic code, so the suite costs one Node subprocess and about two seconds.- Four tiers. Structural (the page decodes at the declared size, two renders are byte-identical, the page is neither blank nor solid), language conformance (the three roles are present and inside their share bands, no colour is painted that the language does not declare, every type size is on the scale and every stroke on the declared weights), legibility and containment (WCAG contrast, nothing off-page, no code row under the footer, no two runs overlapping, raster strokes at least a device pixel). Those three fail. The fourth reports drift from the source asset and never fails, because a cheatsheet is legitimately denser than the card it was drawn from and a test that failed on that is one people learn to ignore.
- Each format is read on its own terms. The vector's text runs are parsed and measured in the family the SVG names; the raster's pixels are decoded and quantized. Two of the defects this suite was written for are visible in exactly one of the two formats, so a check against a single output would have passed both.
- A negative control. One variation renders at 1x and is asserted to fail the legibility standard, which is how that standard is shown to have teeth rather than to pass everything put in front of it.
- The share bands are evidence-backed, drawn around eight measured variations and widened well past them. The defect that prompted them measured 83 / 11 / 2.5 against bands of 50-80 / 12-35 / 1-10, and fails two of the three.
API-QUICKSTART.md. The path from a fresh clone to a graphic drawn from a script, to a plugged-in helper, to a design language captured off an asset you already have - with the two ways to install the helper and what actually differs between them, and a troubleshooting table that starts withUnknown command.API.mdstays the reference; this is the route through it.- A second AI helper:
graphic-designer. It answers a different question from Animation Mode - not "draw the next frame" but "what design language is this graphic in, and how do I make more like it". Its/graphic-designer:design-languagecommand reads a media file, writes aDESIGN_LANGUAGE.mddescribing what it found, and generates a lightweight skill named after the asset, carrying that language and a script that composes new work in it through the graphic-design API. The point is that a repeated graphics job - a series of social posts, a run of thumbnails - stops being a brief somebody re-explains each time and becomes a script with a name.- It measures before it judges.
analyze-media.mjsis a dependency-free CLI (and an importableanalyzeMedia) that reports what a file actually says: an SVG yields colors weighted by how often each class is referenced, plus font families, sizes, stroke widths, corner radii and an element census; a PNG is decoded and quantized into a palette and yields colors only; JPEG, GIF and WebP have no decoder here, exactly as the rasterizer has none. - An empty palette means "not measured", never "no colors". Every report
carries a
noteslist naming what the format could not say, and the contract forbids writing down a value the file did not give. A design language whose numbers were guessed is worse than none, because the next asset gets drawn to it. - Two skills.
design-languagecarries the procedure, theDESIGN_LANGUAGE.mdnaming rules (DESIGN_LANGUAGE-<stem>.mdwhen one already exists) and the generated-skill collision rule (<stem>_0, never an overwrite).graphic-design-apiis the general design skill pointed at this API: the element vocabulary, the grid arithmetic, the 60-30-10 ratio, type scales, WCAG contrast on a static page, and the limits the API is honest about. - The check that matters is run on the output: analyze the generated asset and compare its palette against the source's. Agreement on paper, ink and accent is the only mechanical evidence that the captured language is the one the asset was drawn in.
- It measures before it judges.
- Graphic-design API. A composition is a page and a list of simple
elements - rectangles, circles, ellipses, triangles, polygons, polylines,
lines, paths, styled text, placed media, groups and clipping masks - and it
renders to SVG or PNG.
createComposition()opens a 360 by 360 pixel page; a size can be named instead, and coordinates are read in pixels unless the page asks for inches, millimetres or points. It is headless: the app's own canvas is drawn into only when it is explicitly handed over, so a script that renders a graphic has nothing to do with whatever sketch is open. The reference, every element's properties, and worked examples are in the new API.md.- Both formats come off one document, which is what makes them the same graphic. The page size, the elements, their order, their geometry, their colors, their transforms, their masks and the text layout are shared by construction rather than kept in step by hand; the difference between the two files is the media export format and nothing else.
- PNG with no dependency and no canvas. Node has neither a rasterizer nor a font, and the repository has no runtime dependencies, so the raster path is written here: scanline coverage with four sub-rows a pixel and exact horizontal spans, stroke outlining, clipping masks as multiplied coverage, bilinear image sampling, a PNG encoder that picks a row filter per row, and the DEFLATE codec underneath both directions of it. A composition rendered twice produces identical bytes.
- Text is laid out once and read by both renderers. The SVG writes live
<text>in whatever font family the element named; the rasterizer draws a built-in single-stroke alphabet, because a PNG needs a font engine and there is none to ask. Both measure with the same table, so the line breaks, the alignment and the block's extent match to the unit in the two files and only the glyph shapes differ. Character styling covers family, size, weight, style, letter and word spacing, decoration and case; paragraph styling covers alignment including justification, line height, wrap width, paragraph spacing, first-line indent and whatymeasures. - Media placements carry their own clipping. A
clipis either a mask defined once withdefineClipand shared, or a shape written inline on the element, which the renderer promotes to a<clipPath>of its own. The SVG embeds JPEG, PNG, GIF and SVG verbatim; the rasterizer decodes PNG itself and takes adecodeImagehook for the rest, skipping a placement it cannot read and reporting it inwarningsrather than failing the other forty elements. - A canvas back end for the GUI,
paintComposition, draws a composition into a 2D context with the host's real fonts and the shared layout, and leaves the context exactly as it found it. - Node file helpers stay in their own module, the way the PDF importer
does, so importing the API never drags
node:fsinto a web build.imageDataUrlreads an asset into a data URL andwriteCompositionwrites the SVG and the PNG of one document in one call - a caller cannot export the vector of one revision beside the raster of another.
- The form measures every layer, so a part can be identified without a name.
Animation Mode now sends the AI helper an inventory of the source frame's
layers, each as a fraction of the figure's own box, together with its paint
order. A drawing whose layers are called
g830andpath4521measures the same as one named by hand: the group across the top of the figure is the head, two similar boxes either side of the middle are a pair of limbs, and a small box at the far end of a limb is its hand or foot.- Fractions rather than pixels, because what identifies a part is its share of the figure rather than its size on a page.
- Paint order comes with it, which is what says which of a mirrored pair is the near one - and what has to change when the pose puts the other one in front.
npm run frame-previewrenders a frame so whatever drew it can look at it. Animation Mode's helper worked blind: it set transforms, saved, and never saw the picture. A frame could satisfy every instruction in the skill and still have an arm off its shoulder - which is exactly what the last run produced, in all three frames, while every named rule was followed.- It rasterizes with the code already in the repository:
parsePathData, the matrix stack and the anti-aliased scanline fill behind the graphic-design API, encoded by the dependency-free PNG writer that API already ships. Nothing is installed to get it. - Group transforms are resolved into the path data before drawing, so a frame
posed with
transformattributes renders as it will look once imported. A rotation is affine, so carrying the control points carries the curve exactly. --against <previous.svg>prints how far each part travelled between the two frames beside what drawn frames do, marking eachok,OVERorUNDER, and names any layer that did not move at all. Picture and numbers from one call.- Stylesheets are read as well as attributes: an illustrator's export paints
through
class="cls-30", and without that the reference frames rendered as a blank page.
- It rasterizes with the code already in the repository:
- Two illustrated assets, and the movement budgets measured out of them.
illustrated-multiple-character-actions.svgholds five characters - BadGirl, SassyGirl, JammingJabber, CrimeGuy, JumpingJunkie - each with drawn frames for walking, attacking, taking damage, going down and getting back up;illustrated-single-character-actions.svgholds one character in six action strips, including an eight-frame walk and a nine-frame run. Unlike the wireframe rig these are finished drawings, so they answer the question the rig cannot: not how far a joint turns, but how far each part of a figure actually travels between one frame and the next. npm run illustrated-framesmeasures them - 25 sequences, 70 steps - and writesassets/illustrated-frames.json: per animation type, how far the legs, arms, head and clothing move per frame as a percent of the figure's own height, and how much of the figure is redrawn rather than repositioned.npm run illustrated-frames -- --check <file.svg>measures any SVG the same way and prints it beside the drawn studies, so "the legs swing too far" can be settled rather than argued. It names any layer that never moves in any frame of the sequence.test/illustrated-frames.test.ts: the path tracing, the part naming, and a check that the budget table in the skill still matches the generated asset, so the numbers quoted to the AI helper cannot quietly go stale.- The form measures every layer, so a part can be identified without a name.
Animation Mode sends the AI helper an inventory of the source frame's layers,
each as a fraction of the figure's own box, together with its paint order. A
drawing whose layers are called
g830andpath4521measures the same as one named by hand: the group across the top of the figure is the head, two similar boxes either side of the middle are a pair of limbs, and a small box at the far end of a limb is its hand or foot.- Fractions rather than pixels, because what identifies a part is its share of the figure rather than its size on a page.
- Paint order comes with it, which is what says which of a mirrored pair is the near one - and what has to change when the pose puts the other one in front.
Changed#
/graphic-designer:design-languagetakes an optional output directory. The command captured a design language and left where to put it unstated, which meant the most obvious way to ask - "writeDESIGN_LANGUAGE.mdtodocs/design/" - was answered by guesswork. It is now the second argument, and a destination named in plain words means the same thing. The already-exists check that picksDESIGN_LANGUAGE-<stem>.mdis per directory, so one asset documented in two places gets the plain name in both.ai-helper/is a container now, not a plugin. It held thevectorsplugin at its root, and two plugins cannot share one root, so the existing helper moved to its own exclusive folder atai-helper/vectors/and the new one sits beside it. The marketplace lists both,npm run ai-helper -- --listnames both, and aHELPERSregistry in the installer replaced the five hard-coded constants that used to spell out one plugin's payload.- An existing install keeps working. A record written when
--to pluginmeantai-helperis migrated as it is read, so the app, the status output and the uninstall sweep all resolve it toai-helper/vectors. Without that the sweep would have been pointed at the container root. npm run ai-helpernow installs every helper rather than the one that used to be the only one.--helper <name>narrows it. Animation Mode's own script always namesvectorsexplicitly, sonpm run animation-mode -- --installand--uninstallreach exactly what they always did.- The install record stays at
ai-helper/installed.json..gitignoreexcludes that exact path, so a record one folder deeper would have been un-ignored and started committing a local install state.
- An existing install keeps working. A record written when
npm testtakes--keep-graphics. The graphic-design suite draws a reference composition and several variations of it, writes both formats of each to.tmp/, and deletes them when it finishes, so a run leaves the working tree as it found it. The one thing a graphics test cannot assert is whether the picture looks right, and the flag is how to look: the files stay and the suite prints where.- The animation skill works like an image generator rather than a transform
applier. The job is the next picture in a sequence; the layer tree is how
that picture is stored, and a frame that satisfies every naming rule while
looking wrong has failed. Three things follow from that, all new to
vector-animations:- Pose from where the subject is now, not from how an asset demonstrated the movement once. Which foot carries the weight and how far through the stride this is are read from the current frame; the cycle tables and the studies inform that reading rather than replacing it.
- Identify parts from geometry when the names do not help - a table of what a head, hair, torso, arms, legs, hands and clothing look like in the measured inventory, plus the three rules that do most of the work: a mirrored pair is a pair of limbs, a box inside another part belongs to it, and a box spanning several parts is clothing. The reading is stated in a sentence before drawing, because a wrong one is easier to catch there than in a frame.
- Decide depth from the pose just drawn, not the pose started from. After the new angles are worked out, each overlapping pair is asked which is nearer now, and the layers are reordered where the answer changed - limbs crossing the midline, a turning figure whose far arm goes behind the torso, a head turning past a shoulder, an object crossing on an arc.
- The frame subagent looks at its own work before saving. A new step between posing and saving renders the frame, opens the PNG, and checks the travel - for a limb detached from its socket, a leg through a skirt, a stride that reads as a stumble, and for parts that swung further than a drawn frame ever does. Two passes at most, because the app kills a run that goes quiet for five minutes and a decent frame saved beats a perfect one that never arrives.
- The skill's drawing procedure says to look, not only to measure. Step 5 was "measure the frame before you save it"; it is now "look at the frame before you save it", with the measuring alongside.
- The animation skill now says how far a part moves, not only how far a joint turns. A new "How Far a Part Moves" section carries the measured budgets - a walk's legs travel about 9% of the figure's height per frame and its arms about 5%; a run's bob is more than ten times a walk's - with the typical value to aim at and the observed range behind it.
- "Nothing Stays Frozen", the rule the budgets exist to enforce. Across the 70 drawn steps, 91-100% of layers are redrawn between frames and not one layer in a drawn walk is left frozen in place. A generated frame that sets a transform on the six assemblies and leaves the rest alone does the opposite: two thirds of the figure sits at identical coordinates frame after frame while the limbs swing around it, which reads as a cardboard cut-out with hinged arms. The rule is that no layer may keep the same shape at the same place in every frame of a sequence.
- The drawing procedure covers the whole figure. "Drawing the Next Frame" previously ended at "set one transform per assembly group", which is the instruction that produced the frozen torso. It now carries the rest of the figure with the assemblies, and measures the frame against the budgets before saving.
- The animation skill says not to open the two illustrated SVGs during a run.
They are 1.5 MB and 2.5 MB - a run that reads one spends its whole budget
before it poses anything. Everything a frame needs from them is in the 54 KB
illustrated-frames.jsonand the tables in the skill, and the same is true of the wireframe rig andskeleton-cycles.json. - The layer-naming section cites six characters' worth of real names.
dressfor a torso,upper-bodyfor everything above the hips,left-vestfor clothing,upperarmandforearmas siblings with no assembly around them,jacket-keftfor a mistypedjacket-left, and a defeat frame with no child groups at all. CrimeGuy is built differently in different frames of his own walk, which is why the parts have to be worked out from the frame in hand rather than from a map built off the previous one. - The animation skill works like an image generator rather than a transform
applier. The job is the next picture in a sequence; the layer tree is how
that picture is stored, and a frame that satisfies every naming rule while
looking wrong has failed. Three things follow, all new to
vector-animations:- Pose from where the subject is now, not from how an asset demonstrated the movement once. Which foot carries the weight and how far through the stride this is are read from the current frame; the cycle tables and the studies inform that reading rather than replacing it.
- Identify parts from geometry when the names do not help - a table of what a head, hair, torso, arms, legs, hands and clothing look like in the measured inventory, plus the three rules that do most of the work: a mirrored pair is a pair of limbs, a box inside another part belongs to it, and a box spanning several parts is clothing.
- Decide depth from the pose just drawn, not the pose started from. After the new angles are worked out, each overlapping pair is asked which is nearer now, and the layers are reordered where the answer changed.
Fixed#
- Code tokens no longer overlap in a generated SVG. They were advanced by
measureText, which reports the API's built-in alphabet - the font the rasterizer draws - while the SVG named Courier New, which is wider by up to 10 units on a 6-unit line. Every token landed short of where the real font would end it and the next one printed over the last. Invisible in the PNG, which is drawn in the very font that was measured, so only a reader opening the SVG would ever have seen it. Named as a limit inAPI.mdand in thegraphic-design-apiskill, because it will bite anyone laying out runs side by side in a family they named. - Small text in a generated PNG is legible. A 6-unit glyph is drawn with a
0.45-unit stroke, under one device pixel at
scale: 1, so a code panel anti-aliased into grey texture. Generated scripts now raster at 3x by default; the composition is unchanged, only the sampling. - An overlong title degrades instead of running off the page. It steps down the type scale first - only sizes the design language declares - and is truncated with an ellipsis when even the smaller step does not fit. Found by the new suite on its first run, which is the argument for the suite.
- The helper's slash command reaches a dot-folder install. Installing to
.claudeor.githubcopied the skills and the contract and dropped thecommands/andagents/folders on the floor, so/graphic-designer:design-languageexisted only for someone who had loaded the plugin - while the skill, the README and the contract all described it as the way to start the job. A command reachable under one delivery and missing under the other is a command whose documentation is wrong half the time.- Commands now land in
<target>/commands/<helper>/, so the copy answers to the same/<helper>:<command>spelling the plugin gives it. ${CLAUDE_PLUGIN_ROOT}is resolved on copy to the helper's repository-relative folder. That variable is defined only for a loaded plugin, so a copied command that kept it pointed every path in its own instructions at nothing.- Uninstalling removes what installing added, commands and subagents included.
- Commands now land in
import('napkin-sketch')works from Node.dist/api/index.jsis an ESM bundle, but the package is"type": "commonjs", so Node read a bare.jsthere as CommonJS and refused to load it - which meant the documentedimport { createComposition } from 'napkin-sketch'failed for anyone outside a bundler. The build now writes a one-keydist/api/package.jsonscoping that folder to ESM. Found by the graphic-designer helper's PNG decoding, which is the first thing in the repository to import the API as a plain Node script.- The graphic-design fixture symlinks resolve.
test/graphic-design-api/skill/carries two links to the same graphic in two formats; both were written relative to the repository root rather than to their own directory, and the PNG named a file that does not exist. Git had them as proper mode-120000 symlinks all along, so only the targets needed fixing, with forward slashes so a Linux or macOS clone resolves them too. - Measuring a posed frame read it as unchanged. The movement figures come from
path data, and a frame carrying its pose in
transformattributes has the same path data as the frame it came from - so a freshly posed frame measured as though nothing had moved, which is the one answer that check must never give. Transforms are now resolved before measuring, the way the app resolves them on import. - The frame subagent could not finish, so a delegated frame stalled the run.
animation-framewas givenRead, Edit, Write, Glob, Grepand no shell. It poses the source frame with a few small edits, which is cheap - but the posed file then has to reachanimations/<name>.svg, and with no way to copy it the only route left was handing all 50-80 KB of the document to aWritecall. That is the one move the agent's own instructions warn costs tens of thousands of tokens, and the app kills a helper that has produced no frame and no output for five minutes. The agent has shipped this way since it was introduced, and it only bites when the helper happens to delegate rather than doing the job inline, which is why a two-frame walk could produce the first frame in 53 seconds and then stall on the second.Writeis now gone from the agent's tools andBash/PowerShellare in. RemovingWriteis the fix rather than an extra: with no way to emit the document there is no expensive path left to take.- The agent now says to copy the posed file to its destination with one shell command, and why - pose first, copy second, because the app takes the file the moment it appears.
- A test asserts both halves. The failure is invisible from outside the app - the helper just stops - so the tool list is checked rather than trusted.
- The
graphic-designerplugin manifest tracked the wrong version. A version bump syncedvectorsand left the second plugin behind on the release before it, failing the manifest test. - The helper-root test failed on any machine that had run the installer.
ai-helper/installed.jsonis the install record, gitignored and written bynpm install's postinstall, but the test that keeps the helper container tidy allowed only a README beside the helper folders.
[1.0.0-alpha.4.1.2] - 2026-09-01#
Added#
- Show Selection Borders. A switch in the Move palette, beside its
live preview, draws or drops the dashed blue outline around the selected
elements. The selection is unchanged either way - it still moves, copies,
rotates and exports the same, and the layers panel still shows what is in
it; only the outline on the canvas goes. What it is for is judging a drawing
with something selected, which is the one time the border sits exactly where
the eye wants nothing: a few pixels off the marks being looked at. The Move
palette is where it lives because that is where a selection is being worked
on; Quick Settings (
Ctrl+,) and the Verbose Settings window's Sketch Support section carry the same switch, and all three move together, as doesCtrl/Cmd + H- which is the one that matters while drawing, because the border is in the way exactly when a selection is being looked at and reaching a switch otherwise means opening a palette over the drawing being judged. On by default, and persisted like every other setting. - Editing panels dock. Move, Rotate, Page Settings, and Sharpen Selection
each carry a small button in their title bar that parks the panel in a dock
beside the layers and properties panels, and takes it back out again. A
docked panel is a column of the workspace rather than something over the
drawing, so the canvas gives up the width instead of being covered - which
matters most for exactly these panels, since the thing they edit is
underneath them.
- Undocking puts the panel back where it floated, at the position it was dragged to, so trying the dock costs nothing.
- The dock remembers across a close. A docked panel that is closed stays docked; reopening finds it where it was left rather than floating again.
- The dock takes no space when it is empty - no column, and not even the pixel its border would cost - so a session that never docks anything is laid out exactly as before.
- Rotate: turn a selection by hand or by the degree. A Rotate button
beside Move, and
Ctrl+R, open a palette that turns the selection about a centre point. Positive degrees turn clockwise and negative counterclockwise, which is the same sign the canvas drag counts in, so the number in the field is always the number the gesture measured.Ctrl+Ris a gesture, not a panel. Opened from the keyboard, letting go of a canvas drag accepts the rotation and puts the palette away - press, swing, release, done, with no button to find afterwards. A press that turned nothing is not a gesture and leaves it up, so a stray click cannot dismiss it. Opening from the Rotate button or the Edit menu is the other intent - a panel wanted for typed angles, presets, and repeated turns - and that one stays put until it is dismissed. The panel says which of the two it is, since the difference only shows at the end of a drag.- Drag on the canvas to turn it. With the palette open the canvas is a rotation handle: press anywhere and swing. The pointer's bearing from the centre is sampled each frame and the shortest way round added to a running total, so the gesture crosses the seam at half a turn without flipping sign, and a second lap counts as a second lap rather than undoing the first. A dashed lever runs from the centre to the pointer while it goes. Releasing bakes the turn as one undo step - not one per pointer event - and the field returns to zero.
- The centre of rotation moves. Drag the crosshair on the canvas to put the pivot anywhere, type an exact Centre x / y in any of the usual units, or pick one of the nine handles of the selection's box from the preset grid. The pointer turns into a grab where the marker can be picked up, so the one thing that says the pivot is movable is on the pivot. A centre that has been dragged or typed matches no preset, and the grid shows none lit, which is itself the reading that the centre is custom.
- A live preview, off by default, shows the typed angle on the canvas before it is committed - the real rotation made and unmade, none of it taking a history step, exactly as the Move dialog's preview works. A drag always previews whatever the checkbox says: a gesture that turned nothing until it was released would be no gesture at all. Moving the centre while a preview is showing takes it off and re-makes it about the new point, since a rotation cannot be adjusted from one pivot to another.
- Common rotate controls: a signed angle field, a CW / CCW pair that
re-signs whatever magnitude is typed (and lights to show which way the
current angle turns), the nine-handle centre grid, and a Snap to 15°
toggle.
Shiftsnaps for one gesture without the toggle; arrow keys step the angle by a degree, or by 15 withShift. - Bezier paths turn as paths. Anchors and both tangent handles rotate with the geometry, so a turned Vector Path stays editable and bows exactly as it did. A Copic stroke's broad nib turns with the mark, keeping its bearing relative to the stroke instead of staying pinned to the page. Text and images have no orientation in the model, so they orbit the centre without tipping; an image orbits by its middle rather than by the top-left corner its anchor records, which is what keeps it where the eye expects.
- The palette opens out of the way, in the top-right rather than centred,
because the canvas under the selection is where the rotation is dragged and
a centred panel would be sitting on exactly those pixels. It is otherwise
the Move palette's twin: dragged by its title or border, resized from its
corner, and left where it was put. A closed palette is out of the way
outright -
visibility: hiddenrather than only transparent, so the panel neither takes presses meant for the canvas underneath nor keeps its fields in the tab order.
- Move by an exact distance. A Move button beside Join, and
Enterwith something selected, open a dialog that shifts the selection by typedxandydistances in any of the usual units. The Properties panel already sets an absolute position, so this one is relative - which is what "nudge it ten to the right" wants. Positivexgoes right, positiveygoes down.- Arrow keys step a field by its own unit, and
Shifttakes the coarse step: 1 and 10 for px and pt, an eighth of an inch and a whole inch for in and mm alike, so changing a field's unit does not change how far one press actually moves the drawing. The browser's spinner only knows a fixedstepattribute, which cannot grow with a modifier, so both arrows are handled directly. Entercommits the move and closes, the same as the Move button: a typed field says it is finished with Enter, and the palette has one job.Escapecloses without moving.- Changing a field's unit converts what is in it, so the distance stays the same and only the way it is written changes: an inch becomes 25.4 mm rather than 1 mm, and a live preview does not jump.
- The dialog is a floating palette, dragged by its title or by the
padded border ring around its contents, and resized from its corner, so it
can be put somewhere that is not covering the selection it is moving. The
border shows a crosshair move cursor (
src/assets/move-popup.svg, sized to 32 px with its hotspot at the centre) wherever the press would drag rather than land on a control, and the resize grip keeps its own corner. The panel keeps its place between openings and is pulled back on screen if the window has since shrunk. It also stops dimming and blurring the page behind it, which is what makes the live preview worth watching while the distance is typed. - A live preview toggle shows the move on the canvas while the distance
is still being typed. The preview is the real move made and unmade, none
of it taking a history step; committing puts it back first and then moves
once for real, so the whole thing stays a single undo, and Cancel or
Escapeleaves nothing behind.
- Arrow keys step a field by its own unit, and
- A frame count that paces a sequence instead of batching it. Animation
Mode's setup gains a Frames in the sequence field, and it does not change
how many frames are drawn - they are still drawn one at a time, and the
sequence still runs for as long as Keep and draw next is pressed. It sets how
far one frame moves: a cycle's whole movement is spread across that many
frames, so fewer frames move further each and more frames move less.
- Measured cycles are resampled, not repeated. A cycle is a list of per-step deltas measured from a fixed number of drawn skeletons, and deltas cannot be stretched on their own. They are turned into running totals, sampled at the boundaries of the requested frame, and differenced again - so the pose part-way between two skeletons is interpolated rather than invented, and the totals come out unchanged. A walk paced to 2, 4, 8, 16, 24, or 60 frames closes exactly; a knockdown paced to 3, 6, 12, or 20 lands at the same angle it was drawn landing at.
- The default is the length the cycle was drawn at, so each type opens at one drawn frame per drawn skeleton and any other number reads as a deliberate choice. The note under the field says which way a change moves the pose.
- Types with no measured cycle get the count as context. Their prompt template now carries the sequence length and says this frame is worth about one Nth of the movement, which is the only pacing an AI-posed type could otherwise take a guess at.
- Step values are rounded to two decimals rather than one, matching the transform writer, so resampling a short cycle into many small steps no longer accumulates rounding into visible drift.
- Animation Mode announces a finished sequence. Once as many frames have been kept as the setup asked for, an Animation Graphics Completed prompt says how many were generated and offers Generate New Animation - which runs the wizard again from the top, so the next sequence picks its own type and length - or Done. The frame before it reads "Keep and finish" rather than "Keep and draw next", so the last one is not a surprise.
- The wizard's dialogs are movable and resizable too, by the same border drag and corner grip as the Move dialog, so a step can be pushed aside to see the frame it is talking about.
Changed#
- The animation skills read the drawing before they trust the names. A new
Read the Drawing Before the Names section opens
vector-animationswith the order the work actually goes in: see what the SVG draws and which way the figure faces, read the layer names as evidence about that picture rather than as instruction, map every layer to a part, and only then pose the frame. All of it is taken from a real hand-drawn document rather than invented.- Names will not match the rig.
head-assemblyfor the head, uniquifiers landing after a frame index so the name stops parsing (BadGirl_walk_2-2is frame 2, not frame 22), and a first frame carrying no number at all (BadGirl). Match on meaning; where a name is ambiguous the geometry decides. - Layers the rig does not name still have to move. Clothing follows the
part it hangs on, and a part can be a sibling rather than a child -
front-glovesits besidefront-arm-assembly, so rotating the arm alone leaves the glove hanging in the air. A figure that comes apart at the wrist is always this. - Paint order is part of the pose. Document order is the only depth a flat drawing has, and it is a per-frame decision: when the legs cross, the two swap. Keeping one order for the whole sequence is what makes a walk read as a figure sliding rather than striding, and it is the foreshortening cue a still frame has.
vector-graphicsgains the general half of that as Document Order Is Depth, plus a note that a copy marker can land on a name that already ends in a meaningful number.- The assets are framed as coursework: the rigs are the exercises, the studies are the sketchbooks, and their careless layer naming is itself part of what they teach.
- Names will not match the rig.
- The
vector-animationsskill covers animation physics. A short Animation Physics section in the skill and a fullreferences/animation-physics.mdbeside it, because a cycle table says what angle a joint takes but nothing about how far apart two frames sit - which is the only dial a posed frame has, and the one that decides whether a sequence reads as real. It matters most for the types with no measured skeleton: jump, fall down, knocked down, break, explode, move, and anything arriving as a study.- Gravity by the odd-number rule is the usable part: from rest, successive frames cover 1, 3, 5, 7, 9 units, so a five-frame drop sits at 1, 4, 9, 16, 25 units down rather than five even steps. With arcs even across and accelerating down, frames bunch at the apex, and that bunching is the hang time.
- Bounce height falls as
e^2per contact and each arc needs fewer frames than the last; weight is frame count rather than distance, so a heavy thing spends several frames starting and stopping where a light one spends one and overshoots. - The reference also covers what a real solver would do instead - mass-spring systems, the finite difference approximation, explicit against semi-implicit against implicit Euler, and the cost in control that comes with them - so a request for simulation gets an accurate answer rather than a guess. Every figure in it was checked against its own arithmetic.
- The AI skills' asset lists match what is actually there, and say how far
each sheet can be trusted. Both skills now group their assets by how
organized they are, because a helper that treats a study like a rig will read
names off it that were never meant to hold.
vector-animationsdocumentscharacter-wireframes.svgand the generatedskeleton-cycles.jsonas rigs to measure;breaking-objects.svgas partly organized - its frame numbering is reliable, but frame_0of both sequences carriespotential_*andobsoletesgroups that are working scraps rather than parts of the drawing; andbouncing-object.svgandcharacter-study-throwing-and-walking.svgas studies to read rather than derive from.vector-graphicsreplaces the removedobjects.svgwithisometric-objects.svgandperspective-objects.svg- one wheel, sphere and cube drawn twice with matching group names, so a drawing can be moved between projections a face at a time - and addsmale-character-elements.svgandfemale-character-elements.svgas very loose studies, withalphabet.svgandshapes.svgunchanged.- Both skills say when a study is the right answer: a request the rigs do not cover - a pose the wireframes do not hold, an object that travels rather than comes apart, or anything reaching the API or the app's AI helper without a preset behind it.
- Three references to
assets/object-animations.svg, a file that does not exist, are corrected tobreaking-objects.svg, and the cross-skill links fromvector-animationsno longer point at the removedobjects.svg. Every asset path in both skills now resolves.
- The editing popups share one implementation. Dragging by the title,
dragging by the border band, the corner resize grip, and staying on screen
when the window shrinks were a set of private methods that only the Move
dialog used; Rotate then wanted all of it. They now live in
src/renderer/popup.tsas a small manager, and each popup declares which capabilities it wants - moveable, resizable, dockable - rather than inheriting whatever the routine happened to do. The wizard's steps stay moveable and resizable but are deliberately not dockable. The module reads the sameis-hiddenclass the app already toggles to open and close a dialog, so no opener or closer had to change to gain any of this. - Reload is gone, and
Ctrl/Cmd + Rrotates in its place. Reload discarded the sketch without a word, from a shortcut that sits next to half the editing keys. The View menu's Reload row is removed and both key presses are taken before Chromium sees them - including while a text field has focus, which is where a strayCtrl+Rduring a layer rename would otherwise still have gone to the browser and taken the drawing with it.Ctrl+Rthen has a job rather than only a refusal: it opens Rotate for the selection, the wayEnteropens Move.F5keeps the explanation, since nothing else has ever wanted it. - Closing with unsaved changes asks first. The window close is held for the standard three-option prompt - Save, Don't Save, Cancel - and only goes through on an answer that allows it. Save runs the app's own save, so a sketch with no file still gets its dialog, and cancelling that leaves the window open rather than losing the work the prompt was protecting.
- Drawing a busy page is no longer slower the busier it gets. Every mark
commits to a layer of its own, so a page's layer stack is as long as its
drawing, and the three places that walk that stack - the canvas, the SVG
export, and the PDF export - each resolved a layer's marks by rescanning the
whole page. Walking the stack that way cost the cube of the page's size, and
the canvas paid it on every frame of every drag, pan, and zoom. Each mark's
layer is now resolved once per page instead of once per layer per mark, and
the SVG export reads paint order from a table rather than searching the
stroke list for each mark. A two-hundred-element page went from roughly eight
million layer comparisons per frame to a single pass. Nothing about the
output moved: the same layer resolution, the same paint order, byte-identical
exports.
- Selecting, clicking, and rubber-banding got the same treatment. Working out whether a mark can be picked up meant resolving its layer and every group above it, and the hit test asked that of every mark on the page - on every pointer move, to decide which cursor to show. Picking a click out of a crowded page did it three times over. The whole layer stack now resolves in one pass and the pickable marks in one more, both built fresh for each gesture step so nothing can go stale, and the three passes of a within-bounds pick share the one answer. Endpoint snapping, Select All, Sharpen All, the layers panel, and the page thumbnails all resolve once now too.
- The panels no longer rebuild faster than the canvas repaints. Dragging a selection changes the document once per pointer event, and every one of those changes rebuilt every row of the layers panel and every field of the properties panel, in full, immediately - several times between one frame and the next, with all but the last thrown away. Those rebuilds now coalesce into one per frame, like the canvas redraw beside them. Nothing reads back what a rebuild writes except starting a layer rename, which asks for its row and so flushes the pending rebuild first.
- A placed image is no longer held onto forever. Decoded images were cached by their data URL and nothing ever dropped one, so every picture ever painted stayed in memory for the life of the window - across page turns, across opening a different sketch book, and past deleting the mark that put it there. The cache is now emptied whenever the whole document is replaced, and by the throwaway surface that Export All builds for each page, which was quietly keeping one alive per page of the export.
- Two IPC paths that nothing could reach are gone. A channel for entering
rearrange mode had a sender, a subscriber, a bridge method, and a handler,
and the main process never sent it - rearrange mode has always travelled the
same route the Edit menu's own row takes. An
open-app-settingsaction was handled in the renderer and dispatched from nowhere. Neither cost anything at runtime and both cost the next person to read the code, who would have found a handler that cannot run. - The roadmap gained a section for an instruction-driven graphics API.
Everything the app knows about drawing - the sharpen engine, the Bezier
model, the layer tree, the SVG writer - is already browser-safe and already
exported, and none of it can be reached without a pointer.
TODO.mdnow carries API Implementation: thirty-four entries planning a headless path from a small instruction language through an evaluator that emits the sameStrokeandLayershapes the GUI commits, and out to SVG or PDF, listed in the order the pipeline runs rather than by size. It is scoped as a minor because every entry in it is additive - no existing export, signature, or file format moves - and it is a plan rather than an implementation, which is why this release stays at 1.0.0-alpha.4.1.2. - And a section for the tool that writes those instructions. Recording a
drawing session, replaying it through the app's own API, generating the
Animation Mode helper script from the layers a user assembled, and
organizing a selection into named, nested groups by rule are four
capabilities and one mechanism: a script the app writes and then either
hands to a tool or runs itself.
TODO.mdnow carries Automation and Scripting Tool, thirty-three entries, filed as++.y.zbecause recording every document change means routing every document change through one observable path - a new contract for a store that mutates through roughly fifty methods called from the whole of the renderer. The undo stack is not that path, and the section refuses the shortcut up front: it holds page snapshots, not the actions between them. A plan rather than an implementation, so this release stays at 1.0.0-alpha.4.1.2 as well. - And a section for Animation Mode itself, led by a typed prompt. The
wizard offers a fixed list of movements, so a user who wants something not
on the list has no way to ask.
TODO.mdnow carries Animation Mode, forty entries under five features, of which User Prompt is sixteen: a textarea in the setup dialog whose text reaches the AI helper. Two findings shape it. The form already tolerates a type it does not recognize -animationTypeSpecreturns null andbuildAnimationFormfalls through to a generic branch - so the feature is largely about making that branch good rather than adding one. And the form's first principle - that a frame is a file edit and not a redraw - is precisely what a free-text prompt invites somebody to break, and that is the failure which made earlier runs run out of time. The other four features are the gaps a few runs make obvious: a prompt worth keeping, a frame worth fixing rather than redrawing, a step worth trying twice, and a sequence worth managing as a sequence. Additive against the mode as it stands, sox.++.z- and a plan again, so the release is still 1.0.0-alpha.4.1.2.
Fixed#
- The head was never found in a hand-named document.
matchesAssemblytookheadliterally, so a drawing that groups the head the way it groups the limbs -head-assembly, which is what an artist actually writes - resolved no head at all: it got no pivot, no transform, and stayed put while the rest of the figure moved. The-assemblysuffix is now accepted on the two names that lack one,headandbody. The four that already carry it still match exactly, sofront-arm-assemblycan never be read as a barefront-arm. - The space bar toggles a focused checkbox again. Tabbing to Live
preview or Show Selection Borders and pressing space did nothing: the
space bar arms straight-line drawing, and that shortcut called
preventDefaulton its way past, cancelling the toggle the browser was about to perform. A checkbox is passed over by the "a focused text field owns its keys" guard on purpose - it consumes no letters, so the tool shortcuts keep working while one has the focus - but space is not a letter, and a checkbox has no other key. A focused checkbox or radio now keeps the space bar; everything else, the canvas included, still gives it to straight-line mode. Buttons are deliberately left out: they answer to Enter as well, so they lose nothing, and taking space from them would make a toolbar button that still held the focus from being clicked fire again instead of arming a drag. - A palette that opens on a field now actually opens on it. The Move
palette meant to open with its Horizontal (x) field focused and its value
selected, so a distance can be typed the moment it appears; it was landing
the focus nowhere, leaving the toolbar button still focused and typed digits
going to the canvas as tool shortcuts. The dialog fades in from
visibility: hidden, and an element that is not visible cannot take focus - the fade putvisibilityon a 160ms transition, so for the whole of it the panel still computed as hidden and the focus call was a no-op. Opening is now instant forvisibilitywhile the opacity still fades, and only opening: closing keeps the transition, so a dismissed panel still fades out rather than vanishing. Rotate's angle field and Page Settings' width field were focused the same way and were failing the same way; all three work now. - Move moved the selection twice when the live preview was on. The preview
is a real move, made and unmade, and the Move button committed the typed
distance and then put the preview straight back on top of it - so a request
for 100px landed the selection 200px along, with only the first half in the
history step. Confirmed by driving the app: preview off gave 100px, preview
on gave 200px. The button now commits the move the preview was already
showing and closes the palette, which is the one step it was always meant to
be.
Enterdoes the same, for the same reason: it carried the doubled move too, and re-showing a preview over a committed move is what produced it.- A close can no longer strand a preview on the canvas.
closeMoveDialogtook a flag saying whether to take the preview back off, and the one call that passed "no" is what left the second move behind. An uncommitted move must never outlive the palette that was previewing it, so the flag is gone and every close reverts - which costs nothing after a commit, since the commit has already cleared it.
- A close can no longer strand a preview on the canvas.
- Selecting an element with the Select tool nudged it a few pixels. The
press committed to a move drag immediately, so the first pointer movement
after it carried the element along one pixel for one pixel - and a click
made by a hand always carries some. Driving the app confirmed the ratio
exactly: a click travelling 3px left the element 3px from where it started.
A press now only arms the drag, and the move begins once the pointer has
travelled 4 screen pixels. Below that the gesture is a click and the drawing
is left exactly as it was found; past it the element still follows the
pointer the full distance, so nothing is lost to the slack.
- A click that only selects no longer costs an undo press. The history
step was pushed on the press for the same reason, so clicking an element
to select it left a step that undid nothing. It is pushed when the drag
commits instead. (This was the second of the two Select-tool defects
recorded in
TODO.md; both had the one cause.) - An Alt-click that never moves no longer leaves a hidden duplicate. The
Alt-drag copy was made on the press too, so an Alt-click that went nowhere
stacked a copy exactly on top of the original, where nothing showed it.
The copy is made when the drag commits, and
Altis read at that moment rather than remembered from the press.
- A click that only selects no longer costs an undo press. The history
step was pushed on the press for the same reason, so clicking an element
to select it left a step that undid nothing. It is pushed when the drag
commits instead. (This was the second of the two Select-tool defects
recorded in
- Grabbing the Page Settings or Sharpen title threw the panel off screen.
A placed panel was positioned
absolute, which measures from whatever box its overlay happens to be. The two palettes whose overlay fills the viewport never showed it, but the corner variants are anchored bottom-right and are only as wide as the panel: writing the panel's viewport position intostyle.leftmoved it that far again from the overlay's own corner. Pressing the Page Settings title moved the panel 1471px right and 813px down, out of the window and beyond recall. A placed panel isfixednow, so its coordinates are the viewport's - which is what every number the popup manager drags, parks and clamps in already assumed. - The Move palette opened over the drawing it was about to move, three quarters of the screen wide. It had no width of its own, so it opened at whatever its contents wanted - 1129px of a 1494px window for two number fields and a note - centred, on top of the marks its live preview was about to show moving. It now starts at 340px, the width Rotate already uses, and parks itself clear in the top-right corner on its first opening the way Rotate does. A palette dragged somewhere on purpose still stays there.
- Undocking a resized panel brought back its position but not its size. The dock has to clear the inline width and height the resize grip wrote so the column can set its own; only the position was noted down first, so a panel pulled bigger, docked, and taken back out returned at the default size. Both are remembered now, which is what the dock button promised: a round trip that costs nothing.
- Page Settings and Sharpen are pulled back on screen when they open. Move and Rotate each asked for that at their own call site; the other two never did, so a panel left near an edge could reopen off screen after the window had shrunk. The popup manager watches the class that opens a dialog and clamps every one of them - and it watches with one window-resize listener for all nine popups rather than one listener each.
- The AI tool sign-in button could not open a terminal on Windows. Pressing
Open sign-in raised "Windows cannot find 'sign-in\'" instead of starting
the tool. The window title was passed as one entry of an argv array, and Node
escapes an entry that already carries quotes, so
startreceived""\"napkin-sketch sign-in\"" cmd /k claude- it read"\"napkin-sketchas the title and tried to run the rest of the title as a command. The launch is now a single command string handed tocmd /d /s /c, whose/sstrips the outer quotes and leaves the rest exactly as written. Only the wrapper shell is hidden; the terminalstartopens is a process of its own and stays up. - Sign-in on a tool that is not installed says so rather than opening a terminal that closes again: the executable is checked before the launch, the same way a generation run checks it.
Enterno longer opened the Move dialog on top of another one. The shortcut only checked that something was selected, so pressing Enter to confirm an Animation Mode step - the obvious thing to press - put the Move dialog over the wizard. That one gap is what made Move look present but broken inside Animation Mode, made the wizard's first prompts look buggy, and left the move cursor showing afterwards. Enter now stands down while any other dialog is open and while Animation Mode is on, the Move button says so rather than opening, entering Animation Mode closes the dialog and takes any live preview back off the canvas, and closing it clears a half-finished drag so no cursor is left behind.- The move cursor took the whole border ring. It now follows the same three-pixel band the drag does, so what shows the cursor is exactly what can be grabbed, and the resize corner keeps its own.
- A group or multi-element selection is no longer easy to lose. Pressing
inside a selection of several elements now moves it, even where the press
lands in a gap between the marks. It used to demand an exact hit on ink:
pressing the space between two strokes of the very thing being dragged
cleared the selection and started a rubber band instead.
- The press no longer drops the selection either. A press on empty canvas with several elements selected keeps them until the gesture says what it is: the selection goes once the pointer moves (a real rubber band) or when it is released without moving (a click). A mis-aimed grab that never became either leaves the selection intact.
- The grab area around a selection is wider, twelve screen pixels rather than four. A press aimed at a group of scattered marks often lands just outside the box enclosing them, and starting the move a few pixels off is a far better outcome than losing the whole selection.
- Export All wrote its pages with no name. Clearing the extension in the
save dialog and typing a plain name left the files called
_1.png,_2.png, and so on: the stem was taken by slicing the extension off the end, and slicing nothing off the end of a string leaves nothing at all. The whole name is now kept when there is no extension to drop. Windows' own dialog appends one, which is why this only ever showed on the dialogs that do not. - A fully transparent mark came back solid. Opacity zero was rejected on
load as though it were missing, and a missing opacity means the tool's
default - which for a pen is fully opaque. Anything set to zero with Quick
Opacity, or imported from an element the source file had at
opacity="0", turned into ink the next time the book was opened. Zero is now read as the opacity it is; anything outside zero to one still falls back to the default. - A save that could not land left its scratch file behind. Saving writes a temporary file and renames it over the document, so an interrupted save can never corrupt the original - but when the rename itself failed, which is what happens on Windows if anything else holds the file open, the temporary was left sitting in the folder, and every retry added another. The failure is still reported and the original still untouched; the scratch file now goes with it.
- Page thumbnails drew lines across the holes in a shape. A ring, a letter with a counter, or any imported shape with an inner contour showed a line ruled from the end of the outer contour to the start of the inner one in the pages panel, and a shape with its outline switched off was drawn with one anyway. The thumbnail painter is a simplified copy of the canvas painter and had drifted from it on both counts; it now lifts the pen between contours and leaves a fill-only shape without an outline, the way the canvas and both exports always did.
napkin-sketch --newfailed to start from some terminals. Editor and agent terminals often exportELECTRON_RUN_AS_NODE, and the CLI passed its whole environment to the window it launched. With that variable set the GUI runs as plain Node, whererequire('electron')answers with nothing and startup dies naming an Electron internal rather than the cause. The variable is now dropped from the child's environment, since the window is never meant to run that way.- One bad id in an imported file could abort the whole import. A layer's
eraser mask is found by the id in its
mask="url(#...)"attribute, and that id went into a lookup unescaped - so a quote or a bracket in it produced an invalid query that threw, and the file failed to import at all rather than losing one mask. The id is escaped now. Every other unreadable thing in an SVG already degraded gracefully; this one is the last that did not. - Imported artwork could paint in the wrong order. napkin's own exports
record each mark's paint order in a
data-iattribute, and geometry from another editor carries none - which was supposed to fall back to the order the elements appear in the document. It never did: a missing attribute reads as the number zero, which is a valid order, so every foreign element was filed at the very bottom of the stack and the fallback beside it could not be reached. Only a document mixing both kinds of mark on one layer showed it, where the foreign artwork sank underneath geometry it had been drawn on top of. - The AI helper's log setting could point outside the folder it names. The
path is documented as relative to the helper's working directory, and it was
taken as typed - so an absolute path replaced that directory outright,
..climbed out of it, and the app created whatever folders it found itself needing. It is checked now: an empty value still switches the log off, a relative path is kept, and anything that would escape falls back to the default rather than being silently rewritten, so a path meant to go somewhere else fails where it can be seen. A settings file moves between machines, so a path that is absolute on any platform is refused on all of them. - The animation wizard's frame count said skeletons and meant steps. A sequence opens at its animation's natural length, and the note explaining that number described it as the count of drawn poses the cycle was measured from. It is the count of steps between them, which for an animation that loops is the same number and for one that does not is one fewer - so the generated table showed "7 frames" above a six-entry list and read like a defect. The number was always right (a run draws the frames after the one already on the page, so six drawn frames plus the source is the seven poses that were measured); the wording is now right too, and the generated tables give both counts.
[1.0.0-alpha.4.1.1] - 2026-09-01#
Added#
The run is a measured cycle.
Run-Animationwas drawn intocharacter-wireframes.svg- ten skeletons, one per frame - sorunmoves off the written template and onto measured angles like walk, idle, and knocked down.npm run wireframe-cyclesreads it, and the wizard offers it without the Work in Progress mark.- The measurement shows what a run is, rather than a walk with bigger
numbers: the front leg swings 40.9 degrees at its widest against a walk's
20.2 and travels around 80 degrees end to end against a walk's 50,
shiftYPercentlifts the figure on the passing steps where a walk's neck holds one height, and the spine tips 14 degrees forward into the drive and comes back the same 14 on the next step. - All four columns still sum to zero across the ten steps, so the cycle closes and loops. A test pins each of those three: the deeper swing, the lift, and the lean returning - a run that tipped forward and stayed there would walk the figure onto its face after two repeats.
- The measurement shows what a run is, rather than a walk with bigger
numbers: the front leg swings 40.9 degrees at its widest against a walk's
20.2 and travels around 80 degrees end to end against a walk's 50,
object-animations.svg, the object counterpart of the skeleton rig, for the two types that cannot be posed with a transform:Box_Breaking-Animation(3 frames, a box cracking and shedding pieces) andCloud_ImpactEffect-Animation(5 frames, an impact cloud dispersing).- Four group names carry the convention the skill now documents:
baseis what is left of the whole, onestray-piecegroup is one separated piece,potential_stray-piecesmarks on the intact frame what will come away, andobsoletesnames what the previous frame had and this one does not. - Two readings the drawing gives that a transform-minded one would not: piece count is not monotonic (the cloud runs 3, 5, 4, 4 across frames 1 to 4 as puffs merge), and a separated piece is new geometry rather than a moved copy, because its outline changes as it tumbles.
- The
breakandexplodeguidance in the app now names the drawing, so a template-posed object frame is written against it.
- Four group names carry the convention the skill now documents:
A serif typeface in
alphabet.svg. The asset now holds two typefaces rather than one:Sans-serif-typefaceandserif-typeface, 26 letter-pair groups apiece. The pair is a worked example of the skill's own naming rules- the serif set carries the editor's
-2uniquifier throughout (Aa-2is stillAa) and names its paths for the letters (A,a) where the sans-serif set names them for the roles (upper-case,lower-case).
- the serif set carries the editor's
ai-helper/is now thevectorsplugin, not a folder a plugin gets built out of. The manifest, a command, a subagent, both skills, and the helper contract are the folder's own contents, and one manifest at the repository root is the marketplace that lists it:.claude-plugin/marketplace.json lists vectors, source ./ai-helper ai-helper/ .claude-plugin/plugin.json the manifest, versioned from package.json commands/animation-mode.md /vectors:animation-mode - draw one frame agents/animation-frame.md the same job as a subagent, in its own context skills/vector-animations/ assemblies, pivots, cycles, transform recipe skills/vector-graphics/ Bezier formulas, layer structure, path scripting instructions/ the contract a helper follows/plugin marketplace add . # from a clone /plugin marketplace add isocialPractice/napkin-sketch # without one /plugin install vectors@napkin-sketch- A plugin is more than the two skills.
/vectors:animation-moderuns a frame from the form the app wrote, andvectors:animation-frameis a subagent that does the same job in a context of its own, so a frame's SVG never lands in the main conversation. Both defer to the skills rather than restating them. - The tree is tracked source, and there is one of it. The dot-folder
installs copy out of the same folder the plugin loads from, so a skill can
no longer be current in one delivery and stale in the other - which the
generated
plugins/copy already was by the time it was replaced. - Installable without a clone, which the built folder never was: the
marketplace manifest is committed, so
/plugin marketplace addreaches it straight from GitHub. - Both manifests validate under
claude plugin validate --strict.
- A plugin is more than the two skills.
The app knows which delivery it is talking to. A plugin renames what it carries - inside one, a skill answers to
vectors:<skill>- so a form that named the bare skill would name something the tool cannot find. The install record'splugintool id now travels throughAnimationModeStatusto the renderer and intobuildAnimationFormas adelivery, and a plugin form namesvectors:vector-animations,vectors:vector-graphics, and/vectors:animation-modein place of theai-helper/skills/...paths that only a copied install has. Omitting the delivery still writes the file-path form every tool understands.A test guards against a skill existing twice. It walks the tracked tree, reads the
name:out of everySKILL.md, and fails on a repeat; it also checks the two manifests agree with each other and withpackage.json, and that the command, the subagent, the skills, and the instructions are all where the app says they are. Installed copies under an ignored dot-folder do not count - a duplicate there is the install working.
Changed#
- The walk table in the skill was re-read from the asset. Four cells had drifted from the drawing by a tenth of a degree (steps 3, 5, 6, and 7). The generated cycle was always right; the hand-kept table beside it was not.
- The
plugininstall target no longer builds anything. It used to generateplugins/vectors/, a second copy of both skills that went stale the moment either changed.ai-helper/is the plugin now, so the target checks that tree over, syncs the manifest version topackage.json, and prints the/plugincommands that load it - readying a plugin was never what made its skills reachable.--to pluginresolves toai-helper, andplugins/is deleted, dropped from.gitignore, and swept off disk by the target if an older build left one behind.- Uninstalling a plugin install deletes nothing, because what it
installed is tracked source the repository needs either way. It prints
/plugin uninstalland/plugin marketplace removeinstead. - The two plugin manifests are exempted from the ignore rules. A global
"ignore every dot-entry" rule would leave
.claude-plugin/uncommitted, and an unpublished manifest is a plugin nobody can add.
- Uninstalling a plugin install deletes nothing, because what it
installed is tracked source the repository needs either way. It prints
- Switching delivery removes the old install first.
--install --to pluginover an existing.claudeinstall used to leave that dot-folder's copy of both skills in place, so a tool loading the plugin saw every skill twice. The install now uninstalls a previous target that is not this one. - The
vector-graphicsskill now covers SVG layer management. A new "Layer Management" section records the structural rules the import/export work settled on, so the AI helper draws and edits with them instead of rediscovering them: nested<g>groups are the layer tree (document order is z-order, group opacity multiplies down, wrappers are never flattened); one layer name is written three ways (idfor Illustrator,inkscape:label+groupmodefor Inkscape,data-nameverbatim) with the-Nuniquifier and_xHH_escapes undone on read and tag-plus-digits auto-ids reading as unnamed; compound paths keep their contours together so an outlined stroke stays a sliver instead of a blob; fill-only shapes staystroke="none"; and round trips preserve Bézier anchors rather than samples, at precision matched to the artwork's units. The skill's description and When-to-Use list now name these triggers, and the installed.claude/skills/copy is synced. - The
svg-animationsskill is nowvector-animations. The name pairs it with its companionvector-graphicsand describes what it works on rather than the file format it happens to emit. The rename reaches the skill folder, itsname:frontmatter and heading, the helper contract (animation-mode.instructions.md),ANIMATION_SKILL_NAMEinsrc/core/animation.ts(which is what every generated form names), the wireframe-cycles script's asset paths, and the docs. Nothing about the skill's content changed.- An earlier install is cleaned up on upgrade. The installer now sweeps
a list of retired skill folder names out of the target before copying, so
a dot-folder that already holds
svg-animationsdoes not end up carrying two copies of the same skill under two names. Uninstall sweeps them too.
- An earlier install is cleaned up on upgrade. The installer now sweeps
a list of retired skill folder names out of the target before copying, so
a dot-folder that already holds
[1.0.0-alpha.4.1.0] - 2026-08-30#
Added#
- Copy and paste, reachable three ways: the native Edit menu, a
right-click on the canvas, and the keyboard.
Ctrl+Ccopy,Ctrl+Xcut,Ctrl+Vpaste,Ctrl+Shift+Vpaste in place,Ctrl+Dduplicate. The Edit menu also gained Delete and Select All, which the keyboard already had.- Paste aims at the pointer. With the pointer over the canvas the graphic's top-left lands on it; with the pointer elsewhere each paste steps 16px down-right from the last, so repeats stack visibly rather than piling into one spot.
- Paste in Place returns the elements to the coordinates they were copied from. The clipboard belongs to the app rather than to a page, so this is how a graphic moves to another page without drifting.
- Pasted elements become the selection and the Select tool takes over, so the copy can be dragged immediately.
- A copied group keeps its layers. Copying a group and pasting rebuilds
the whole tree instead of merging it onto one layer: every nested layer
comes back with its own name, opacity, visibility, and lock, and the marks
are restacked in the paint order they had, so the pasted graphic is the
one that was copied. Exported side by side, the original and the copy are
byte-identical SVG.
- The copy lands as a sibling of the original, at that layer's own nesting level, rather than nested inside the group it came from.
- Only the pasted root takes the " - Copy" suffix. The layers under it keep the names the source file gave them, which is what leaves an imported SVG readable after a copy.
- Pasted onto another page there is no original to sit beside, so the tree goes in at the top level.
- A plain canvas selection still pastes flat onto one layer: a few marks picked out with the rubber band are not a structure worth rebuilding. The split is decided by whether the selected layer rows include a group, which is exactly the gesture that means "this whole graphic".
Ctrl+Dduplicate follows the same rule.
- The right-click menu selects what it was opened on: right-clicking an element that is not part of the selection picks it first, so Copy means the thing just clicked rather than whatever was selected beforehand. The layers panel's menu carries the same rows, since a lit row is a selection.
- The system clipboard is included. A copy also goes out as SVG (cropped to its own ink, transparent), so it pastes into Illustrator or Inkscape; and SVG copied in another editor pastes in here through the same importer File > Import uses, layer tree and all. The SVG written on copy is remembered, so an in-app copy is never replaced by its own lower-fidelity echo, while a newer outside copy does win.
- The clipboard shortcuts stay out of text fields. The Edit menu items
carry
registerAccelerator: false, which displays the shortcut without claiming it, leaving the keypress to the page - where the renderer already ignores shortcuts while a text field has focus. Claiming them in the menu would have takenCtrl+Caway from the layer-rename box and the property fields.
- The Pages panel has a menu, and there are now three ways to start a page.
A hamburger button beside
+ Pageopens them:- From Selection measures whatever is selected, gives the new page those dimensions, and carries a copy of the selection onto it: the graphic gets a page that fits it rather than the other way round, and arrives on that page rather than being left behind on the old one. This copies rather than moves - the originals stay where they were - and the copies land at the new page's origin and become the selection, so the marks that were selected before the page turned are the marks selected after it. Greyed out with nothing selected.
- Default New Page matches the page in view, which is what
+ Pagehas always done and still does. - Custom New Page… opens Page Settings with Sized page already applied and the width field focused, so a size can be typed for a page that does not exist yet. The dialog's title reads New Page and its button reads Add Page; the page is only added when that button is pressed, so closing the dialog leaves no empty page behind.
- Export can export just the selection, cut to its own dimensions. The
Export dropdown gained a Selection row that opens the same four formats
(PNG, JPEG, SVG, PDF) in a panel beside it, and exports only the selected
marks on a document sized to their bounds - no page-sized margin of empty
space around the graphic. The bounds are grown by half the widest outline,
because stroke bounds follow centerlines and a box drawn on them alone would
slice the outer edge of the ink off.
- PNG and SVG come out transparent, since a graphic cropped to its ink is one about to be dropped into a composition. JPEG and PDF keep the page background, having no usable transparency of their own.
- A lit layer row counts as a selection. With nothing selected on the canvas but rows highlighted in the Layers panel, those layers and their descendants are what gets exported, so selecting a layer and exporting it is the same gesture either way round.
- The SVG route offsets the viewBox rather than moving the marks, so every coordinate is the one a full-page export would have written.
- Menus can hold nested entries. The shared context menu grew submenus: a row carrying nested items shows a chevron and opens them in a panel beside itself on hover, on focus, or on a click, flipping to the row's left when a panel on the right would overrun the window.
- Shift pins a drag to an axis or a 45-degree diagonal. Held during a
drag, it constrains the movement to the nearest of the eight compass
directions.
- The axis comes from the pointer's own travel since the drag began, and is re-chosen on every move, so swinging around the start point swaps the drag onto the line it now points down.
- The movement is projected onto that line rather than having its off-axis component zeroed, so the thing being dragged keeps level with the pointer's component along the line instead of lagging at its perpendicular foot. Letting Shift go hands the drag back to the pointer, because the drag tracks where it actually put things rather than where the pointer is.
- Applied to moving a selection (and so to the Alt-drag copy, which shares that path), to Direct Select / Vector Path drags of an anchor, a handle, or a whole path, and to the Space + drag pan. Drags where Shift already means something keep that meaning: the rubber-band marquee (add to selection), drawing (endpoint snap), the quick curve (swing the apex), and the shape tools.
- The axes are a fixed table of unit vectors rather than
cos/sinof a snapped angle, so a straight-across drag stays exactly straight instead of picking up the 6e-17 of vertical thatMath.cos(Math.PI / 2)returns. constrainDraglives insharpen/geometry.tswith the other pure geometry, and is covered by unit tests.
- A Shift-press on an already-selected element no longer drops it before it can be dragged. Shift-click has always toggled selection membership, which meant a Shift-press on a selected element removed it and returned without starting a drag - leaving nothing for the new constraint to act on, and making the most obvious gesture for it (Shift, then drag what is selected) do the opposite of what it looks like. The removal now waits for the release: a Shift-press that never moves is still a Shift-click, and one that moves is a constrained drag.
- An Alt-drag copy says so in the pointer. Holding Alt over a selection with the Select tool - and for as long as the copy is being dragged - swaps the arrow for two: the usual one at the hotspot, a second stepped out beside it in the inverse fill, and a node square beside them. The offset arrow sits clear to the right rather than laid over the first, because two arrows sharing a diagonal tangle into one thick smear. Alt is tracked for every tool now (it used to be read only by the Vector Path tool) and is re-read from each pointer event, so the pointer stays right even when the keypress landed in another window.
- A button that drops a menu now toggles it. Pressing Export, the pages panel's hamburger, or Close Shape a second time puts its menu away, and a third press brings it back. The button reads as pressed for as long as its menu is out, so it is clear which button the panel belongs to. The press that lands on the owning button is exempted from the outside-press dismissal that closes the menu, or that dismissal and the toggle would cancel each other out and the menu would never close.
Fixed#
- The layers panel moved only one row of a multi-row selection. Select
several layers, press Move Up, and only the active one shifted. Every
selected row moves now.
- Each travels as a block - the layer plus everything nested under it - and moves among its own siblings, so a layer never leaks out of the group it lives in. (The old single-layer move swapped with whatever sat next in the flat stack, which could carry a layer across a group boundary without changing its parent.)
- Blocks move destination-first, which keeps the selection's internal order and lets a block that has reached the end hold the ones behind it instead of letting them pile through.
- A selected group can be restacked now, carrying its contents. It used to be refused outright ("Group rows cannot be restacked"), and the panel's move buttons greyed out whenever a group row was active. They now grey out only where the selection genuinely has nowhere to go.
- The Alt-drag copy lost a graphic's groups when the selection came from the
canvas. Selecting a whole imported graphic with
Ctrl+Aor a rubber band and Alt-dragging produced a flat pile of" - copy"leaves scattered inside the original groups, with every nested group gone; the same drag started from the group's panel row kept the tree. The two now behave alike.- The copy decides what a group is from the marks, not from which rows
happen to be lit: a group joins the copy when every one of its
mark-carrying layers is in the copy already. That makes a rubber band
around a whole graphic the same gesture as clicking the row above it, and
it is the rule for
Ctrl+C/Ctrl+V,Ctrl+D, and Alt-drag alike. - Alt-drag now runs through the same tree-rebuilding paste the clipboard
uses, so its copies land as a sibling of the original with only the root
suffixed. The suffix is
" - Copy"everywhere now; Alt-drag used to spell it" - copy".
- The copy decides what a group is from the marks, not from which rows
happen to be lit: a group joins the copy when every one of its
mark-carrying layers is in the copy already. That makes a rubber band
around a whole graphic the same gesture as clicking the row above it, and
it is the rule for
- Paste refused onto a group, calling it "locked or hidden". Selecting an
imported graphic by clicking its group row - the obvious way to grab the
whole thing - made a group the active layer, and paste, duplicate, and
placing an imported raster all read that as a layer they could not draw on.
A group is neither locked nor hidden: it simply holds no marks itself, so a
fresh layer now drops inside it and the operation carries on, which is what
the drawing tools have always done in the same situation.
- The check the drawing tools use is now shared rather than half-copied, so all four paths behave alike and the message says which of locked or hidden actually applies instead of naming both.
- A nested menu panel could not be reached with the pointer. Hovering
Export > Selection opened the panel, but it vanished before the pointer got
to it, which made the whole row unusable.
- Its own rows were dismissing it. Both panels are built by the same
routine, so every row in the nested panel carried the "a plain row was
entered, put the nested panel away" handler that belongs only to rows in
the parent menu. Entering the panel's first item therefore started its
own dismissal. The panel element's
pointerentercould not undo that: it fires once on the way in and never again as the pointer moves between the rows inside. Rows in a nested panel now cancel the dismissal instead of starting it. - Dismissal waits a moment. The nested panel is a separate element sitting beside the parent menu, so a pointer travelling toward it crosses rows it is not aiming at. Entering a parent row now schedules the close after a short grace period rather than closing outright, and reaching the panel cancels it. Resting on another row still puts the panel away.
- Its own rows were dismissing it. Both panels are built by the same
routine, so every row in the nested panel carried the "a plain row was
entered, put the nested panel away" handler that belongs only to rows in
the parent menu. Entering the panel's first item therefore started its
own dismissal. The panel element's
Changed#
- An imported SVG re-exports smaller than it arrived, instead of nearly
double. The exporter now writes path data at its shortest exact spelling,
and the geometry is unchanged to the coordinate. Four reductions, none of
which moves a curve:
- Absolute or relative, whichever is shorter, per command. A repeated
command letter is dropped, an axis-aligned line collapses onto
H/V, and a cubic whose incoming handle mirrors the outgoing handle before it collapses ontoS- the same curve in two numbers instead of four. Relative deltas are measured from the rounded current point, so a reader reconstructs the absolute coordinate exactly and nothing drifts along a long path. - Numbers drop what nobody needs to read: two decimals as before, but no
trailing zeros, no leading zero on a fraction (
.5, not0.5), and no separator where the next number already delimits itself. - Shared paint is stated once, on the root element.
fill="none", round caps and round joins, and whicheverstroke-widthmost marks share ride on the<svg>and inherit; only the odd mark out names its own. - Defaults go unwritten: a fully opaque mark says nothing about
opacity, and a width of 1 is what SVG already assumes. - Measured on the test fixtures and the vector-graphics skill's assets, a round trip lands between 0.27x and 1.05x of the source, against 1.14x to 1.49x before, and all 77 paths across those files parse back to byte-identical anchors and handles.
- Absolute or relative, whichever is shorter, per command. A repeated
command letter is dropped, an axis-aligned line collapses onto
- The importer reads the compacted spellings.
parsePolylineDandparseVectorDare built on the full path parser now instead of their own narrow regexes, so relative commands,H/Vruns, reflectedShandles, and elided command letters all read back the way absoluteM/L/Cdid. Without this a napkin export would have re-imported through the sampling fallback and stopped round-tripping losslessly. Surface.rendercan leave the paper off. A newtransparentoption skips the background, the paper texture, and the sized-page outline, which is what lets a cropped raster export of a selection land on transparency.
[1.0.0-alpha.4.0.0] - 2026-08-28#
Added#
- A generated frame can no longer be a copy of the frame before it. The app
collects the helper's output file the moment it appears, so a helper that
copied the source to that path and only then began posing it had its copy
taken and its work killed - and the frame that landed on the page was the
previous frame over again, shifted right by the strip placement.
- A copy no longer ends the run. When the output file turns up matching its source, the app keeps waiting and says so ("Helper saved a copy; waiting for the pose…"), which turns a copy-then-edit helper from a failure into a success.
- A copy is refused rather than imported. If the helper exits having written nothing but the source over again, the run fails with that in words instead of putting a duplicate layer on the page.
- The check reads path data and transforms together, because posing a frame leaves the path data untouched: a posed frame and a copy differ only by the transforms, so comparing geometry alone would call them the same.
- The helper contract now says outright not to copy the source to the output path and edit it there.
- Animation cycles are measured from the wireframe skeleton, not invented.
character-wireframes.svgis a rig: one stick-figure skeleton per frame, carrying the same assembly group names a drawn character does, with each limb a polyline that runs joint-first.npm run wireframe-cyclesreads the angle of every limb in every frame and writes the step-by-step rotations the app poses frames with, so the numbers come off drawn poses instead of guesswork.- It fixed a walk that swung the wrong way. The hand-written table had the arms and legs rotating opposite to the drawn skeleton on every step - a walk whose limbs contradict each other, which is what made generated frames look wrong. The measured cycle also shows the drawing has no bob and asymmetric arms, neither of which the invented table had.
- Two more types became measured:
knocked-downfromKnockdown-AnimationandidealfromIdeal_Stance-Animation. They are no longer "work in progress" in the wizard, and the app hands the helper finished transforms for them the way it already did forwalk. - A repeated skeleton is not measurable.
Punch-Animationdraws the same pose for its last two frames, so the step between them moves nothing and a frame generated from it copies its source.attackis offered from its written template instead until the punch gets a distinct last pose, the generator warns when a skeleton produces a still step, and a step that moves nothing is never handed over as finished transforms - the form falls back to the type's template rather than telling the helper to change nothing. - Limb angles are read against the spine, so a limb that moved only
because the whole figure tipped reads as no joint rotation; the tipping is
a new
figureRotateapplied to every assembly about the figure's base. That is most of what a knockdown is, and it is why the two do not double-count. - A cycle that does not loop no longer closes. Knockdown has seven skeletons and six steps: the wrap-around step that would snap the figure back upright is not generated for a type that runs start to end.
- The readings ship as an asset too:
skeleton-cycles.jsonbeside the wireframes names which skeleton drives which type and lists every step, so an AI helper posing a type by hand can follow the same guide.
- Every animation type is selectable: all fourteen - ten character types
and four object types - with everything except
walkmarked (Work in Progress) rather than held back as a disabled option.- A prompt template per type is what makes them usable before their
cycle tables exist. Each type carries one sentence describing what a
single step of that movement does - which joints turn, which way, how far
- and whether the sequence loops, and the form hands that to the AI helper
in place of exact angles.
walkstill runs off its measured eight-step cycle.
- and whether the sequence loops, and the form hands that to the AI helper
in place of exact angles.
- One table drives both the dropdown and the prompt, so an option can
never appear without the template behind it: the wizard builds its lists
from
ANIMATION_TYPESat runtime instead of from hand-kept HTML. - Object animations no longer ask for arms and legs. Setup now runs before the assembly mapping, because the category decides whether the six character assemblies are needed at all; an object animation skips the mapping step entirely and moves the frame's root group. The banner says which animations the assemblies are for rather than reporting a blocker.
- A prompt template per type is what makes them usable before their
cycle tables exist. Each type carries one sentence describing what a
single step of that movement does - which joints turn, which way, how far
- The
vector-graphicsskill is plugged into the helper prompt. The form names it besidesvg-animationsand points at its installed copy, so a frame that needs real curve work rather than a joint rotation - the fracture lines of abreak, the piece outlines of anexplode- has somewhere to go. - Animation Mode installs and uninstalls separately. It is the only
feature that needs software the app does not ship - an agentic AI
command-line tool, with its own installation, account, and usually cost - so
it is no longer part of a default install.
npm installadds nothing: no Edit-menu entry, noCtrl + Shift + N, no banner, and no AI tool needed to run the app at all.- One command adds or removes it:
npm run animation-mode -- --install [--to claude|copilot|codex|gemini|…],--uninstall, and--status. Installing copies the helper's skills and instructions into that tool's dot-folder and writesai-helper/installed.json; uninstalling removes exactly what it installed - anything else in that dot-folder is left alone - and deletes the record. - One record decides, so the feature is genuinely plugged in and out
rather than hidden: the main process reads
ai-helper/installed.jsonbefore the menu is built, and with no record the Edit entry, the accelerator, the keyboard shortcut, and every path into the wizard are all shut. A damaged record reads as "not installed", so a bad file costs the add-on rather than the app. - Uninstalling keeps everything else intact: sketching, layers, export, import, pages, and the rest are untouched.
- One command adds or removes it:
- A sign-in walkthrough for the AI tool. The two ways a run can fail
before any drawing starts - the tool is not on the PATH, or nobody has
signed in to it - are now told apart and answered instead of being reported
as a stalled run.
- The tool's executable is checked before the run starts, so a missing tool names itself immediately.
- A failed run is classified from its exit code and output, and an authentication failure opens a walkthrough naming the tool, with an Open sign-in button that starts that tool in a terminal of its own so it can run its own sign-in.
- No credential is ever read, requested, or stored by napkin-sketch. The app starts the tool and steps out of the way; the install record names which tool was chosen and nothing about the account behind it.
- The helper command's executable is resolved the same way whatever its
shape (
claude,"C:\bin\claude.exe",/usr/local/bin/codex), so the right tool is named in dialogs and started for sign-in.
- Animation Mode (
Ctrl + Shift + Nor Edit > Animation Mode): a new app mode that turns a page's layer tree into frame-by-frame animation material with the help of an AI tool. Entering the mode switches to the Select tool, opens the Layers panel, and shows a banner that validates the page live against the six required character assemblies (front-arm-assembly,body,front-leg-assembly,back-leg-assembly,back-arm-assembly,Head- matched case-insensitively, editor uniqueness suffixes ignored). The mode is per-session and always starts off when the app opens; it only opens from the Edit menu or its shortcut.- Generation wizard behind the banner's Generate… button. Setup comes first, then - for character animations whose page does not already validate - a Cancel / Back / Next dialog per missing assembly asks which layers make it up and groups them under a group named for the assembly. Setup collects the animation category and type and names the frame the first run will draw. There is no frame count: a sequence is as long as the user keeps making it.
- One frame per run. Each run hands the AI helper a single pose and asks for a single step of movement, which is small enough for a helper to finish. The drawn frame imports immediately as a group layer and the dialog offers Redraw (discard it and draw the same index again), Keep and draw next (that frame becomes the source for the one after it), and Done. The status line carries the helper's latest note, the elapsed time, and the frames kept so far.
- Frames stand side by side. A drawn frame is a copy of its source with the assemblies turned, so left where it lands it would sit exactly on top of that source and hide the new pose entirely. Each frame is placed one gap to the right of the frame it came from (15 percent of the source's width, with an 8 px floor) and the view fits the page afterwards, so a sequence builds left to right as an animation strip and every frame can be seen before Keep or Redraw. Only the horizontal position moves - the cycle's vertical bob is part of the pose - and the placement shares the import's history step, so one undo takes the whole frame back off.
- Frame files are sprites. The SVG kept in
animations/is sized to the ink rather than to the napkin-sketch page, and carries no paper rect, so a frame drops into an animation composition as it stands - no page-sized margin of empty space around the drawing and no opaque rectangle behind it. The box is the frame's stroke bounds grown by half the widest stroke, so the outline is not shaved off, and it is reached by offsetting the document'sviewBoxrather than moving the geometry, so every coordinate still matches what the app holds. The app rewrites the file from the frame it imported, which makes the sizing exact whatever shape the AI helper happened to save. - Frames are transforms, not redrawn documents. An SVG group transform
rotates every anchor and its Bezier handles together about a chosen
pivot, which is exactly the rigid joint rotation a frame needs, and
napkin-sketch resolves group transforms when it imports a frame. So the
helper sets one
transformper assembly rather than re-emitting 50 to 80 KB of path data - one attribute instead of tens of thousands of output tokens. - The app measures the pose. Each assembly's joint pivot is taken from
its own bounds (shoulder and hip at the top, neck at the bottom of the
head; the body only bobs), and the form hands the helper finished
transformvalues to copy character for character. Nothing about the geometry has to be worked out by an AI. - A ready-made walk cycle. Eight steps carry frame 0 through a stride
and back: arms swing against the legs, the head counter-rotates, the
figure bobs by 1.5 percent of its height, and every column sums to zero so
the cycle closes and loops. Frame
nuses step((n - 1) mod 8) + 1. A type with no cycle asks the helper to choose the angles with the same mechanics. - AI helper integration: the source frame is written to
_temp/animation-source.svgfor the helper to edit in place and a short form (under 3 KB, no geometry in it) to_temp/animation-form.txt, which is handed to a configurable shell command (defaultclaude -p --model sonnet --dangerously-skip-permissions < _temp/animation-form.txt- a Sonnet-class model is pinned because setting six attributes in a file is mechanical work a mid-size model does quickly; any agentic CLI works - Copilot, Codex, a plain LLM tool). The finished frame is saved to theanimations/folder (created if missing) as<base>_<n>.svg. The app watches for that file and ends the run the moment a complete document appears, instead of waiting for the helper process to exit - an agentic CLI keeps working and talking well past its last write, and that wait is what made runs look hung. There is no overall time limit - instead a stall guard kills a run with no frame and no output for 5 minutes, so it can never hang - and Cancel kills the helper's process tree immediately. A helper that prints its SVG instead of saving it still counts: the markup is taken from stdout when the run ends, saved to the path the form named, and imported like any other frame. The command is the newanimationHelperCommandsetting (a persisted copy of an outdated default upgrades itself on load), and the temp folder is cleared when the wizard ends. Every helper run is recorded inlogs/animation-helper.log(theanimationLogFilesetting; folder auto-created, kept across temp cleanup): command, the frame it asked for, exit, duration, stderr, and the head of stdout - the place to look when frames do not arrive. - Frame naming: a source frame already carrying the animation type
(
hero-walk_0) continues its sequence (hero-walk_1, …); anything else startsanimationLayer-<type>_<n>. The file stem, the root group'sid, and itsdata-nameare all the same string, and each generated frame mirrors its source's nested group structure. - AI-tool support files: a new
svg-animationsskill (SKILL.md with the joint-pivot and walk-cycle tables and the transform recipe, Bezier-curve and animation-essentials references, and wireframe pose assets) plusanimation-mode.instructions.mddefining the helper contract, including a worked example of the exact edit a frame requires. Both live canonically in the version-trackedai-helper/folder; thenpm run ai-helper -- --to <claude|github|...>script (also wired as an env-gatedpostinstall:NAPKIN_AI_HELPER=claude npm install) copies them into the gitignored dot-folder an AI tool reads, and every generated form names the skill and points the helper at both locations. vector-graphicsskill: a second, general-purpose skill installed besidesvg-animations, covering how to draw a vector graphic from the Bezier formulas rather than by dragging control points. It is written for any project, not for this one: nothing in it assumes napkin-sketch or Animation Mode.- References for each degree -
linear-bezier-curve.md(parametric versus implicit form, De Casteljau's base case, degree elevation),quadratic-bezier-curve.md(the spline, tangent-vector, and Bezier forms and the conversions between them; the method of splines),cubic-bezier-curve.md(Hermite conversion, coincident-point pull, tool paths, continuity grades, subdivision, and CSS easing) - each with mermaid diagrams, the SVG commands it maps to, and a worked example checked against the textbook derivation. scripts/matlib-script.js, a dependency-free CLI and library that takes control points of any degree and emits evaluated points, the parametric polynomial equations, an SVGdstring, or a complete SVG document. It solves control points from on-curve points (--through), splits and elevates curves, approximates a circle with four cubics, and reduces degree 4 and above to a cubic chain by flatness. It uses no imports, so it loads under both CommonJS and ESM projects on Node 18+.scripts/template.mdis the procedure that drives it.- Degree elevation section in SKILL.md: the algorithm that raises a
curve's degree without changing its shape
($Q_i = \frac{i}{n+1}P_{i-1} + (1 - \frac{i}{n+1})P_i$, endpoints kept),
why it reads as cutting the corner at every original control point, and
the fact that repeated elevation walks the control polyline onto the
curve as a limiting position. Backed by the script's
--elevateflag, which leaves the emitted path data byte-identical. - Shared drawing assets moved here from
svg-animations(shapes.svg,objects.svg,alphabet.svg), since they are drawing material rather than animation material; the wireframe pose skeletons stay withsvg-animations. Both skills now install side by side, so the cross-links between them resolve after install as they do in the repository.
- References for each degree -
Changed#
- Default window opens maximized. The GUI window used to open at its
1280x860 default size; it now opens maximized. Maximized is deliberately not
the same as full screen: it fills the screen while keeping the title bar's
minimize, restore-down, and close buttons in view, and restore-down returns
the window to 1280x860.
-f, --full-screenstill opens true full screen, where those buttons are not in view.
Fixed#
- SVG import keeps the source geometry instead of sampling it. Generic
path data used to be walked with
getPointAtLengthinto a polyline - one sample per unit of length - which at a sprite's scale (a 43-unit viewBox) turned every curve into a handful of straight facets, exported as a larger file than the source, and threw the Bézier structure away. The importer now parses path data itself (M L H V C S Q T A Z, absolute and relative, implicit repeats, Illustrator's packed numbers) into the cubic anchors the Vector Path tool edits: quadratics are degree-elevated to the identical cubic, arcs become quarter-turn cubic pieces with 4/3·tan(Δθ/4) handles, andcircle,ellipse,rect(rounded corners included),line,polyline, andpolygonare built from their attributes. Export then writes the same few control points the source held. A compound path stays one stroke: its contours are separate subpaths (amovemarker on the first anchor and point of each later contour) so an outlined stroke's inner contour, a ring's hole, or a letter's counter fills as a hole on the canvas, in PDF, and in the exported… Z M …path data, instead of each contour becoming a solid shape that blacks out what sits beneath it. Data the parser cannot read falls back to length sampling. - Fill-only shapes no longer grow an outline on import. A source shape
with
filland nostrokewas imported as a filled stroke painted with a 1-unit outline in the fill color - half a unit of growth all round, which at small scales visibly fattened every shape and exported asstroke="…". Such shapes now import with the outline switched off (noStroke) and export asstroke="none", the SVG spelling of what the source said. - Export precision is two decimals, up from one, so artwork authored in
small user units round-trips without its coordinates being quantised to a
tenth;
stroke-widthis formatted the same way, so an untransformed element no longer exports as0.9999999999999999. Computedrgb(r, g, b)colors are written back as#rrggbb. Import widths floor at 0.1 instead of 0.5, so hairlines in small-unit artwork keep their proportion. - Grid import (
-m) now scales and offsets vector anchors with their points; previously only the sampled points moved, so a vector stroke's export would have drawn the curve where it was before placement.
[1.0.0-alpha.3.3.0] - 2026-08-22#
Fixed#
- Closing a side panel now hands its space back to the canvas. The pages, layers, and properties panels animate their width over 160 ms, but the canvas was resized on the next frame - mid-transition, when the stage still had (nearly) its old size - so closing a panel left the drawing surface sized as if the panel were still open. The canvas now resizes again when the width transition lands.
Added#
- Close Shape: a Sketch Support button that joins a selected stroke's two end points; pressing it offers Sharp (a straight bridge) or Smooth (a Catmull-Rom blend that continues each end's drawn direction through the seam). Strokes with Bezier anchor structure close through their anchor model - the seam stays Vector Path-editable and exports as a true closing segment, with a sharp close dropping the two end handles that would otherwise bend it - and already-closed, too-short, text, image, and eraser strokes are skipped.
- Alt-drag copies the selection. Holding
Altas a selection drag starts duplicates the selected elements and the drag moves the copies while the originals stay put. The duplicates land on new layer rows named<layer name> - copy; a copied group keeps its inner layer names, with only the group row taking the suffix. The copy and the move share one history step, so a single undo removes both. Deleteremoves the selected layer rows. Selected elements still delete first (their emptied layers prune away as before); with no element selected,Deletenow removes the highlighted layer rows - the keyboard twin of the panel's Delete button, and the only way empty layers and groups could be deleted without the mouse.- Properties panel (
Ctrl + P, the View menu, or the toolbar's Properties button): a third right-hand dock that edits the selected element rather than the tool that drew it. Because selecting a layer row selects that layer's elements, it doubles as a layer property sheet - the stroke width of everything on a layer is now adjustable in one place.- Position - the X and Y of the selection's top-left corner, each with
its own unit:
px(default),in,mm, orpt. Conversions run through the CSS reference of 96 pixels per inch, the same ratio the SVG and PDF exports assume, so a value typed in millimetres reads back as the same millimetres. - Appearance - fill color, No fill, or a gradient, edited with
the ramp-and-handles control gradient editors have settled on: drag a
handle to move a stop, double-click the ramp to add one where you
clicked,
+/−to add and remove, and a Linear (with an angle slider) or Radial type. Stroke gains a width, a Solid / Dashed / Dotted style, and No stroke for a fill-only shape. - Scale - X and Y as a
%factor (the default) or as an exactpx/in/mm/ptsize, with uniform scaling on by default. Scaling is anchored at the selection's top-left corner so the Position fields above hold steady, and line weight scales with the shape.
- Position - the X and Y of the selection's top-left corner, each with
its own unit:
- Gradient fills and dash styles are part of the document. They save and
load with a
.skbk, and export as real SVG<linearGradient>/<radialGradient>paint servers andstroke-dasharrayvalues so other editors render them, with napkin's own editable structure riding along indata-attributes for a lossless round trip. A fill-only shape exports asstroke="none"with its outline color and width kept aside. Ctrl + ]andCtrl + [restack the active layer one step up or down, matching the Layers panel's move buttons. Both the shortcuts and the buttons now say why a move did not happen - a group row carries no paint order of its own, and the ends of the stack have nowhere to go - instead of failing silently.F2renames the active layer. Renaming used to be reachable only by double-clicking a layer's name label or by picking Rename from the layers panel's right-click menu, with nothing on the keyboard.F2- the rename key of file managers and vector editors alike - now opens the same inline edit on the active row. It opens the layers panel first when the panel is hidden, unfolds any collapsed group the layer is nested in, and scrolls the row into view, so the shortcut works from anywhere rather than only when the row already happens to be on screen.- Double-clicking anywhere on a layer row starts the rename, not just the narrow name label. The visibility, lock, and group-caret buttons keep their own single-click jobs and are skipped, so a quick double toggle of an eye no longer has to dodge the rename.
Changed#
- A gradient stop recolors on double-click. Double-clicking a handle on the gradient ramp selects that stop and opens the color picker directly, alongside the existing drag-to-move and double-click-the-ramp-to-add gestures.
- Joining strokes now leaves one element on one layer. Every new element gets its own layer, so joining three strokes used to leave the merged stroke on the first one's layer and two empty rows behind it. The merge lands on the first stroke's layer and the layers the other pieces vacated are pruned with them, in the same history step. A layer that still holds other elements is left alone. The Join-on-snap setting consolidates the same way.
- A focused text field now keeps its keystrokes. The tool shortcuts,
Delete, and the document's undo fired while a number or text field had focus, so typing a page width could switch tools or delete the selection. Text areas, selects, and text-like inputs now consume their own keys; sliders, checkboxes, and color wells, which consume no letters, still pass the shortcuts through. - Both rename gestures open the input with the current name already highlighted, so a new name can be typed straight over it and an arrow key drops the caret into the old one instead of clearing it.
- The rename edit is now looked up by layer id rather than by holding on to the row element the gesture started from. Selecting a row redraws the whole panel, so the first click of a double-click could replace the very row the second click landed on; a rename already in flight now re-focuses its input instead of starting a second edit over the top of the first.
- A layer row's tooltip reads
(double-click or F2 to rename).
[1.0.0-alpha.3.2.2] - 2026-08-17#
Fixed#
- Exported SVGs keep their layer names. A layer's group used to be
written out as
id="layer-0"with the real name only in the privatedata-nameattribute, so a napkin export opened in Inkscape or Illustrator arrived as a stack of anonymous groups. Each group now carries its name three ways:data-name,inkscape:labelalongsideinkscape:groupmode="layer", and theiditself. Characters an XML id may not hold are escaped as_xHH_and repeated names take the-2,-3, … suffix editors expect - both of which the importer already undoes, so the names round-trip unchanged. - Page thumbnails are no longer blurry. The pages-panel thumbnails were drawn into a fixed 150-pixel canvas and then stretched to the panel's width, so they were upscaled twice over on HiDPI displays. They now draw at the panel's actual width times the device pixel ratio, and redraw after the panel is resized.
Changed#
- The eraser cursor is a dashed circle the size of the eraser, matching
the circle preview the drawing tools already carry, instead of the
fixed-size
cellpointer that gave no hint of what a stroke would clear. - The mandala symmetry guide fades in and out over 220 ms when rotational symmetry is switched on or off, instead of the axes appearing and vanishing between frames. The axis count is held through a fade-out, so the guide dissolves as the shape it was.
prefers-reduced-motionnow covers keyframe animations. The reduced-motion rule only collapsedtransition-duration, which left the 360 ms page-turn animation (ananimation, not a transition) running at full swing. Animations, delays, andscroll-behaviorare now included, the page turn is switched off outright, and the renderer skips both the page-turn class and the symmetry fade when the OS asks for reduced motion.
[1.0.0-alpha.3.2.1] - 2026-08-17#
Fixed#
- Exported SVGs are far smaller. Importing an SVG samples its curves at
one point per path unit, and every sample used to be written back out as
its own
Lcommand — so a file that arrived at a few kilobytes could leave orders of magnitude larger without a single edit. Two changes close the gap while keeping the graphic intact:- Strokes that carry Bézier anchor structure (Vector Path, Curve, and
quick-curve commits) now export as their exact cubic Bézier
segments — a handful of
Ccommands instead of hundreds of sampled points — which is both smaller and geometrically truer than the polyline it replaces. - Sampled polylines (freehand strokes, imported artwork, eraser masks, and the Copic chisel outline) shed the samples that sit within a tenth of a pixel of the line through their neighbours — below the file's own one-decimal coordinate precision, so nothing visible changes.
- Coordinates drop the redundant trailing
.0(10instead of10.0).
- Strokes that carry Bézier anchor structure (Vector Path, Curve, and
quick-curve commits) now export as their exact cubic Bézier
segments — a handful of
Changed#
- A vector stroke's anchor structure now survives an SVG round-trip:
the importer rebuilds the editable anchors (handles, corner points, and
closure) from napkin's exported cubic paths, so a re-imported Vector Path
stroke opens for anchor editing exactly as it was drawn. Previously the
structure was kept only in
.skbkfiles and a re-imported stroke fell back to a plain freehand polyline.
[1.0.0-alpha.3.2.0] - 2026-08-16#
Added#
- Panel context menus: right-clicking the Layers panel offers Add, Group, Delete, Move Up/Down, and Hide Panel; right-clicking the Pages panel offers Add Page, Delete Page, Page Settings…, and Hide Panel — each menu carries the tools relative to its panel, styled to match the GUI.
- Sized pages (Page Settings, via the Pages panel's right-click menu):
a page can now be
sized— pinned to an exact width and height, shown with a dashed page outline — or left as the defaultendlesspage that fills the window. Switching back to endless re-enables window tracking and disables the dimension fields. The mode round-trips through.skbkfiles. - Resizable panels: drag the layers panel's or the pages panel's inner edge to set its width (160–480 px).
- The layers panel's right-click menu also offers Ungroup (enabled on a group row) and Rename, which opens the same inline rename as double-clicking the layer's name.
- Fit All in View (View menu,
Ctrl+0): zooms and pans so every graphic on the page is in view at once, replacing the stock Actual Size zoom reset that did not act on the drawing. - Panel state on the toolbar: the Pages and Layers buttons fill in with the "Panel in View" treatment while their panel is open and sit flat ("Panel inactive") when it is hidden.
- Export button in the top toolbar, beside Import: drops down the same four formats as File > Export (PNG, JPEG, SVG, PDF), each opening the usual page/all-pages export dialog.
npm run import-tree -- <file.svg>(dev tool): prints the layer tree an SVG would import as, running the real importer in a hidden Electron window — verify a file's imported layer names without opening the GUI.npm run todo(dev tool): archives every checked item in TODO.md — wrapped lines and nested sub-items move along with it — into a## Completesection kept at the bottom of the file, creating the section on first use. Each archived item gains a nestedFrom: <section>line recording the roadmap section it came from; an item that already carries its ownFrom:note (e.g. one pulled into Current from another section) keeps that origin instead. Re-running is a no-op.
Changed#
- The layers panel opens by default, and the Select tool is active on startup instead of the Pen.
- Top toolbar restyle: a separator bar now distinguishes the Pages and Layers toggles from the file actions, and the Save button uses the same background as its neighbours (bold label and strong border keep it prominent); the filled treatment it used to have now marks open panels.
- The layer group collapse caret is drawn at 200% of its former size for an easier target.
- SVG import samples curves at one point per path unit (previously one per two units, capped far lower), so imported artwork keeps the source file's smoothness instead of arriving with faceted outlines.
- A named top-level wrapper group in an imported SVG (e.g.
<g id="circles">around the whole drawing) now imports as the top layer group under its name; previously it was flattened away and its name lost. Anonymous export wrappers are still descended as before. - Every unnamed imported element gets its own tag-named layer: geometry
without an
idinside a group now imports as a layer named after its SVG tag (path,line,rect, …) instead of merging invisibly onto the group's layer, so each element in the source file has a row in the panel. Only napkin-sketch's own exported marks (data-tool) still merge, which keeps napkin's exports round-tripping as clean single layers. An SVG with no<g>elements at all imports as one top group named after the file, holding a tag-named layer per element, and a lone named top-level group keeps the author's name in grid (-m) imports rather than being renamed to the file. - Anonymous groups are never lost on import: an unnamed
<g>imports as a<Group>layer (nested ones included) instead of being flattened away or labeledLayer N, so the imported stack always mirrors the source document — named groups keep their names, unnamed ones read<Group>, loose geometry reads its tag name, and a group-less document arrives under the file's name. - Drawing on a selected group row now works: instead of only warning that a group holds no marks, napkin-sketch adds a fresh layer inside the group, makes it active, and lands the stroke there — the toast names the created layer (locked or hidden groups still just warn).
- Fill Color selects what you click: with the Fill Color tool, a click anywhere within an element's dimensions — its interior or bounding box, not just near its outline — selects that element (highlighting it in the Layers panel) and fills it with the selected ink color. Clicking empty canvas still fills the current selection.
- Select and Direct Select treat a filled shape's interior as the shape: clicking anywhere on its painted fill selects it, where previously only clicks near the outline registered. Unfilled outlines stay click-through in the middle, so rubber-band selection over empty canvas is unaffected.
- CLI import (
-i, --import <file>): launches the GUI with an SVG, PDF, PNG, or JPEG imported into the opening sketch, exactly as the File > Import menu item would place it — SVGs keep their layers, PDFs add their pages, and raster images land centered on the active layer. Missing files and unsupported types fail fast with a clear error before the window opens. - CLI multiple import (
-m, --multiple-imports <file,file,…>): imports a comma-separated list of files in one go, laid out in a grid. Each file's graphic size is measured against the page first, then the graphics fill a row left to right and wrap to a new row whenever the next one would overrun the page width; oversized graphics scale down to fit the page. Every imported file becomes its own named layer (multi-layer SVGs keep their layers grouped under the file's name, PDF pages arrive one layer per page). Quoted names with spaces and spaces after commas both parse.
[1.0.0-alpha.3.1.0] - 2026-08-08#
Added#
- Strokes committed by the Vector Path, Curve, and quick-curve tools carry
their Bézier anchor structure in the
.skbkfile (an optionalvectorfield with anchors and absolute-position handles), so paths stay editable across saves. Unknown or malformed data is dropped on load and the stroke still renders from its sampled points. - Vector Path edit mode: with no path in progress, clicking a committed
vector stroke opens it for anchor editing. A plain click on a segment adds
an anchor there (de Casteljau split, shape preserved) and on an anchor
removes it;
Ctrldrags a single anchor, one of its handles, or the corner-rounding target;Alt-clicking an anchor toggles its handles (smooth point to corner and back). Every change resamples the stroke from its anchors, andEscor an empty-canvas click puts the path down. - Corner rounding: with
Ctrlheld, the selected anchor of an edited path shows a target icon inside its corner — dragging it rounds the corner into a circular fillet, clamped to half the shorter adjacent chord — and a Radius field in the top toolbar (visible while the Vector Path tool is active) applies an exact radius to the selected anchor. - Sharpen Selection (toolbar button): smooths and simplifies the
selected strokes, in the spirit of a vector editor's simplify command. A
corner dialog with Smooth and Simplify sliders previews the result
live on the canvas; Apply commits one undo step, Cancel or
Escrestores the original geometry exactly. - Vector Path tool (
B, in the Sketch Support toolbar): an Illustrator-style pen. Click to place corner points joined by straight segments; click-drag to place a smooth point and pull out symmetric Bézier direction handles; the next segment previews live as a rubber band. Click the first point to close the path,Enteror double-click to finish it open,Escto abandon. Paths commit as editable pen strokes with the current color, width, and opacity, are never auto-sharpened, and respect symmetry mode; Direct Select can rework the committed points.
Changed#
- The Curve tool now branches on pointer type: a mouse keeps the two-phase chord-and-bend flow, while pen and touch input — which cannot hover between clicks — draw with the quick curve's single-gesture quarter arc. The former stylus freehand mode is replaced by that flow.
- A finished quick curve is now exactly one cubic Bézier — two anchors and two control points (the circle-constant construction, deviation under a thousandth of the radius) — instead of an anchor per sample, so editing it shows four points rather than dozens. The mouse Curve tool's bend likewise commits its quadratic lifted to a cubic with two control points.
- Direct Select pans with
Space+ drag, matching the Select tool, and shows the same grab cursor while panning. - Finishing a Vector Path is more forgiving: switching tools accepts the
pending path — a tool shortcut (
Sfor Select,Pfor Pen, …) or a toolbar click commits the curve as drawn, as does the window losing focus.Enter, double-click, and closing on the first point still work, andEscremains the only way to abandon a path. - Dragging a direction handle on a smooth anchor keeps the opposite handle collinear through the anchor (the smooth reflection H' = 2P − H, with each handle keeping its own length), so the curve bends smoothly instead of creasing into a cusp at the anchor.
- Direct Select edits Bézier-structured strokes through their anchors: a stroke from the Vector Path, Curve, or quick-curve tools shows its few anchor points (a quick curve shows two) with the selected anchor's curvature handles, instead of a square on every sample. Dragging an anchor carries its handles, dragging a handle bends the curve with the smooth collinear reflection, and whole-path moves (Direct Select or the Select tool) carry the anchor structure along so it never drifts from the drawn points. Freehand strokes keep the sampled-point editing and tangent-handle bend.
- Tool pointers follow the vector-editor convention: the Select tool shows a
black arrow and Direct Select a white arrow. In Vector Path edit
mode the pointer telegraphs the click: a "−" badge over an anchor
(removable), a "+" badge over the path between anchors (insertable —
Ctrl-clicking a bare segment now inserts too), the black Select arrow whileCtrlis held over anything grabbable, and a stemless arrowhead whileAltis held for handle toggling. Modifier presses restyle the pointer immediately, without waiting for the mouse to move. - Quick curve (
Ctrl + Space+ drag) now draws a quarter ellipse in a single gesture instead of a chord you bend afterwards. The arc leaves the press point with a flat tangent and meets the pointer with an upright one, bowing through the far corner of the drag, and it reshapes live as the pointer moves; the release (mouse-up or touch-up) places its far end. Pen, touch, and mouse all take the same path, so the quick curve no longer borrows the Curve tool's bend phase or its stylus special case. - Holding
Altduring a quick curve makes the arc a quarter circle, with the shorter drag axis setting the radius (matchingShifton the Ellipse tool).Altis tracked for as long as the drag runs, so pressing or releasing it mid-drag toggles between circle and ellipse. - Each
Shiftpress during a quick curve swings its apex — the bowed-out belly of the arc — a further 90 degrees clockwise. Both ends stay exactly where the drag put them, so the start remains anchored to whatever the curve was begun on and the far end remains under the pointer; only the side the curve bellies out to moves. Two presses mirror the sweep across its chord and four bring the apex back around; on anAltquarter circle (or a square drag) the odd stops flatten the arc onto its chord, since a quarter arc pinned at both ends can only bow two ways. Key auto-repeat is ignored, so holdingShiftparks the apex at one angle rather than spinning it. - Because
Shiftnow aims a quick curve's apex, the quick curve's far end no longer snaps to stroke endpoints;Shift+Ctrl+Spacestill snaps its start on pointer-down. Endpoint snap is untouched everywhere else. - The quick straight line (
Space+ drag) locks strictly horizontal or vertical whileShiftis held mid-drag — whichever axis the drag favours — and frees again the momentShiftis released, without waiting for the pointer to move. The line's end therefore no longer snaps to stroke endpoints (Shift+Spaceat press still snaps its start). - Direct Select's lone-anchor handles are now real tangent handles: hollow tips on guide lines reaching a fixed screen distance out along the path each way, instead of the raw neighbour samples (which sit a pixel or two from the anchor on a dense stroke — too close to see or grab). Dragging a handle bends the stroke around the pinned anchor: the span between anchor and handle turns rigidly so the tip tracks the pointer, the bend eases smoothly into the untouched remainder, and pulling the handle longer or shorter stretches the span. Sparse strokes (a two-point line) swing rigidly, so the handles work on every stroke shape.
Fixed#
CtrlandAltnow work reliably in Vector Path edit mode. HoldingCtrlarmed the Copic quick nib-rotate hold (its default hold key), which switched tools after a second and collapsed the edit; the hold no longer arms while the Vector Path tool is active. AnAltpress could focus the native menu bar, blurring the canvas and dropping the edit before the click landed;Altis consumed while the tool is active.- Anchors, handles, and the rounding target were nearly impossible to click: the hit radius followed the Direct Select sensitivity (default 3 screen pixels). Vector-edit targets now use a radius of at least 8 screen pixels.
The Curve tool (V) is unchanged: drag a chord, bend, click to place.
[1.0.0-alpha.3.0.1] - 2026-07-30#
Fixed#
- SVG import now rebuilds nested layers the way the source editor shows them.
Illustrator (and any editor that writes object names into
id) exports a named object as a loose<path id="outline">beside its sibling groups; those paths were flattened onto their parent group's layer, so names such asoutline,glove,beard,teeth, orwhite/blackwere lost and the marks jumped in front of or behind the groups they sat between. Named geometry now imports as its own layer, in its document position, at every nesting depth. - Layer names imported from an
iddrop the-2,-3, … suffix editors add to keep XML ids unique, and undo_xHH_escaping, so rows readstrokes,outline, andarminstead ofstrokes-10,outline-5, andarm-2. A trailing-0or-1is kept, anddata-name/inkscape:labelnames are always taken verbatim. - The ids Inkscape hands to unnamed elements (
path4521,g830,rect12) no longer become layer names — a tag name followed by digits reads as unnamed, so Inkscape files import as layers rather than as a wall of ids. A layer the author actually calledtextorlinekeeps its name. - A group that mixed loose marks with nested groups produced a child row
carrying the parent's own name (
shirt-assembly > shirt-assembly). Groups no longer duplicate their name; a group's own leftover marks land on a row named<name> contents. - Unnamed geometry keeps its z-order relative to its named siblings: a run of
adjacent unnamed elements imports as one
<Path>layer in place, rather than being pooled onto the parent layer.
Added#
importSvg(text, { unnamedElements: 'split' })gives every unnamed element its own<Path>layer, mirroring an Illustrator layers panel exactly. The default,'merge', keeps unnamed marks as strokes on their group's layer so stroke-heavy artwork does not explode into hundreds of rows.
[1.0.0-alpha.3.0.0] - 2026-07-12#
Added#
- Endpoint snap: hold
Shiftwhile drawing with the pen, marker, or Copic marker to snap the stroke to the nearest endpoint of an existing stroke on a visible layer — the start point snaps on pen-down and the end point on pen-up, and straight lines (Space+ drag) snap both ends live. A ring marks the endpoint in range. The snap sensitivity is measured in screen pixels (1-20px, default 10px) so zooming in gives finer control; the sensitivity and an on/off switch (on by default) live under "Quick Features" in the settings window. - Join stroke (off by default, under "Quick Features"): when on, a stroke whose snapped start or end lands on another stroke's endpoint (same tool and color) merges with that stroke into one continuous stroke.
- Sketch Support toolbar section with new tools:
- Join strokes (
Ctrl+Jor the Join button): merges two or more selected strokes end-to-end (nearest endpoints first) into one stroke. - Rectangle (
R): drag to draw a rectangle; holdShiftfor a uniform square. Commits as an editable pen stroke. - Ellipse (
L): drag to draw an ellipse; holdShiftfor a uniform circle. - Curve (
V): drag a chord, release, move to bend the quadratic curve, then click to place it (Esccancels). Also available as a quick feature: holdCtrl+Spaceand drag with any drawing tool. Endpoint snap works in both quick modes —Shift+Spacesnaps straight lines andShift+Ctrl+Spacesnaps curves. - Paint bucket (
G): click inside an enclosed path or shape to fill it with the current ink color, added as a new selectable shape. - Eyedropper (
I): click the canvas to pick the color under the pointer (averaged over the configurable "Select pixel sensitivity", 1-36px, default 10px); the pick becomes the ink color and fills the selected shape when one is selected. HoldCtrlto temporarily switch to the Select tool and pick a shape; releasingCtrlreturns to the eyedropper.
- Join strokes (
- Fill Shape: with the Select tool active and a shape selected, clicking a quick-access color fills the selected closed shape with that color (open strokes and text are recolored instead).
- Filled shapes: strokes can now carry a
fillcolor, honored by the canvas, thumbnails, SVG export/import (viadata-fill), and PDF export. Fill-only shapes in imported SVGs now import filled instead of as thin outlines. - Group Layers (
Ctrl+Gor the Group button in the Layers panel): wraps the active layer in a group; groups nest, and their visibility, lock, and opacity apply to every layer inside.Ctrl+Shift+Gungroups. Deleting a group deletes the layers inside it. - Illustrator-style layer management in the Layers panel:
- Every new drawn element (stroke, shape, text, image, bucket fill) gets its own layer, named after the tool (an empty active layer is reused); eraser strokes stay on the active layer so they keep cutting its content.
Shift-click selects multiple layer rows; the Delete button removes every selected layer together with its elements.- Selecting elements on the canvas highlights their layer rows, and selecting a layer row highlights its elements on the canvas (groups include everything nested inside them).
- Group rows carry a disclosure caret to expand/collapse their nested layers.
- Layer rows drag-and-drop to reposition in the stack or drop onto a group row to nest inside it (insertion edges and a drop-into outline preview the target).
- Deleting an element deletes its layer once the layer is empty (and any group that empties with it); deleting a layer still deletes its elements.
- Direct Select tool (
A, toolbar button): click an element to show its anchor points, then drag an anchor to reshape the stroke's path (undo restores the original shape;Escdrops the edit). - Fill Color tool (toolbar button): fills the selected element(s) with the selected ink color, or the element under the click when nothing is selected (open strokes and text are recolored).
- Curve tool flyout: click and hold the Curve button to choose between the default "start and end at endpoints" variant and the "free ends" sibling, Illustrator-style; a corner marker on the button signals the flyout.
- SVG import keeps nested
<g>groups as nested layer groups, so imported structure can expand/collapse and reorganize in the Layers panel. - Alt + mouse-wheel zoom toward the pointer (scroll up zooms in, scroll down zooms out); the direction is configurable under "Pan & Zoom" in the Verbose Settings window ("Invert Alt + scroll-wheel zoom direction").
- Quick Zoom: press
Zthen a digit within the quick-feature timer to zoom to that level —1–9set 10%–90% and0sets 100% (centered on the canvas). - Mouse-wheel pan: scroll the wheel to pan vertically and
Ctrl+Shift+ wheel to pan horizontally; the direction is configurable ("Invert scroll-wheel pan direction" under "Pan & Zoom"). - Select tool: hold
Spaceand drag to pan the canvas (direction configurable via "Invert Select + Space drag pan direction"). - Select all:
Ctrl+Aselects every editable element on the page. - Layer range select:
Ctrl/Cmd+Shift-click a layer row to select every layer between it and the active layer, so a range can be grouped at once. - Direct Select gains multi-anchor selection (
Shift-click), draggable anchor-point handles, whole-path selection and move, and a configurable grab sensitivity (1–20px, default 3px, under "Sketch Support"). Selected anchors and paths render blue. - Ungroup layers:
Ctrl+Shift+Gdissolves the active group (also available from the Layers panel). - Deselect all:
Ctrl+Shift+Aclears the current selection.
Changed#
- BREAKING: the
.skbkschema is now version 3 — layers may carrygroup/parentfields and strokes an optionalfillcolor. Version 1 and 2 files still open unchanged (saving re-writes them as version 3). - Endpoint snap default sensitivity changed from 5px to 10px.
- Settings unified into two synced views: the in-app panel (
Ctrl+,) is now "Quick Settings" and the settings window (Ctrl+Alt+,, Edit menu) is "Verbose Settings". The verbose window gained the Quick Settings section (live sharpen, wobble, smoothing, circle snap, taper, symmetry, text size), and both views edit the same persisted values. - The Curve tool now snaps its chord's start and end to nearby stroke endpoints by default (the previous free-ends behavior remains available from the tool's click-and-hold flyout).
- The "Fill" toolbar tool is renamed "Paint Bucket" and carries the common paint-bucket icon; "Fill Color" is a separate new tool.
- The toolbar button that opens the in-app settings panel is labeled "Quick Settings", and the gear icon's hover text now reads "Verbose Settings".
- Toolbar rearrange mode now covers every tool in both toolbar groups and allows dragging tools between the groups, matching common vector-editor toolbar customization; the saved tool order persists across both groups.
- The Select tool supports
Shift-click to add elements to (or remove them from) the current selection.
Fixed#
- Group (
Ctrl+G/ the Group button) now groups every selected layer into one group, not just the active layer, keeping their relative order and nesting. - Curve tool with a stylus: a pen no longer produces a straight line. Because a stylus cannot hover between the tool's two clicks (select the chord, then move to bend), one pen gesture now draws the whole curve directly from the pen's path, so a curved pen stroke stays a curve.
- SVG import: documents whose content is wrapped in a single top-level group (as many editors export, often named after the file) now import each inner group as its own layer instead of one flattened layer; the wrapper's opacity carries into the imported layers.
[1.0.0-alpha.2.1.0] - 2026-07-12#
Added#
- Copic marker tool (
K, toolbar button): simulates an alcohol-ink marker's flat broad nib. Strokes are thick across the nib and thin along it, the nib angle is stored per stroke (nibAngle, schema-compatible with version 2 files), and the cursor previews the nib as a rotated bar. Copic strokes render as filled chisel outlines in the canvas, SVG export (with lossless round-trip via data attributes), and PDF export. - Quick nib rotate: hold
Ctrlfor the configured hold time (1s by default) to show a rotation indicator in the bottom-right corner, then holdAltto rotate the broad nib clockwise orShiftto rotate it counter-clockwise at the configured speed; releasingCtrlends the mode. On by default. Activating the mode switches to the Copic marker with the stroke width scaled by the configurable width multiplier (1-4x, default 2x, capped at the 40px maximum), remembering the tool and width in use; deactivating restores that last-used tool and width unless the user explicitly changed either while the mode was active. - Settings window section "Copic Marker": quick-feature on/off, hold time
(0.5-2s), hold key, both rotate keys (
ctrl/alt/shift, kept distinct automatically), rotation speed (15-360 degrees per second), and the quick-rotate width multiplier (1-4x). - Rotational symmetry (mandala mode) rotates the Copic nib angle with each mirrored copy so every arm shows the same thick/thin behaviour.
[1.0.0-alpha.2.0.0] - 2026-07-10#
Added#
- Layers: every page now has a layer stack with per-layer opacity, visibility,
lock, rename, and up/down reordering, managed from a new Layers panel
(
Ctrl+L, View menu, or the toolbar button). Drawing, erasing, selection, and Sharpen All respect the active layer, and the eraser only cuts holes in its own layer. Layer opacity is honored by the canvas, thumbnails, and every export format. - PDF export: File > Export > PDF Document writes a vector PDF with no added dependencies. "Export all pages" produces a single multi-page document; layer and stroke opacity are preserved, and text exports as real PDF text.
- Import (
Ctrl+I, File > Import, or the toolbar button):- SVG files import as editable strokes with top-level groups becoming layers; napkin-sketch's own SVG exports round-trip losslessly (tools, paint order, and eraser masks are recovered from data attributes). Generic SVG shapes and beziers are sampled into freehand strokes.
- PDF files import best-effort: each page's vector paths and text become a new sketch page (napkin-sketch's own PDF exports round-trip).
- PNG/JPEG files import as movable, selectable image items placed on the active layer and scaled to fit the page.
- SVG export now wraps each layer in a named
<g>group and renders eraser strokes as a black-on-white layer mask, so erasing no longer paints opaque background color over content beneath. - Embeddable API:
NapkinSketch#toSVG()and#toPDF(), plus new browser-safe exportssketchesToPdf,importSvg,createLayer,layerOf,strokesOnLayer,isImageStroke, and theLayertype.
Changed#
- BREAKING: the
.skbkschema is now version 2 - each sketch carries alayersarray and each stroke alayerid. Version 1 files still open: they are migrated in place by giving each page a single default layer (no action needed; saving re-writes the file as version 2). - BREAKING: the
Sketchtype (public API) gained a requiredlayersfield, andToolgained the'image'variant used by placed raster imports. Code constructing sketches viacreateSketch()is unaffected.
[1.0.0-alpha.1.0.0] - 2026-06-28#
Added#
- Pan and zoom: a two-finger gesture pans within +/-72px of the initial finger distance and zooms in or out beyond it, scaled by the configured sensitivities and an optional inverted-zoom direction.
- Straight line: hold
Spaceand drag with a single pointer to draw a clean straight line, with a dashed preview shown until release. - Quick Width: press
Wthen type a number to set the current tool width once the quick-feature timer elapses. - Quick Opacity: press
Qthen type a number to set the current tool opacity;0maps to 100% and00maps to 0.1%. - Color change: press
Cto cycle the Quick Access Colors left to right (orShift+Cfor right to left), wrapping around at the ends; the active color shows the same bold border as a mouse selection. - Settings window (opened from the Edit menu or the gear icon) with zoom and pan sensitivity, inverted zoom, quick-feature timer (0.5-3s), Quick Access Color count (2-20) and editable colors, toolbar placement (top / side / both), rearrange mode with drag-and-drop tool and color ordering, color theme (light / dark / sepia), and an auto-save interval.
- Settings persistence: kept in memory across launches, exportable to a JSON file, and importable from one (the last loaded file is remembered).
- New
-f,--full-screenCLI option to open the GUI window full screen. - Per-stroke opacity in the data model, honored by the canvas, thumbnails, and SVG export.
[1.0.0-alpha.0.0.0] - 2026-06-06#
Added#
- Command-line interface (
napkin-sketch) with--help,--version,--book,--new, and--sharpenoptions plus bare-path opening. - Electron-based drawing GUI with pen, marker, and eraser tools.
- Pressure-aware, variable-width stroke rendering on a high-DPI canvas.
- Mouse, touchscreen, and stylus input via Pointer Events with coalesced sampling.
- Auto-sharpen engine: line / circle / polygon / freeform recognition, idealized shape rebuilding, and organic hand-drawn wobble with end taper.
- Live sharpen (per-stroke on pen-up) and "Sharpen all" (whole page) modes.
- Headless
--sharpenpass that beautifies and re-saves a.skbkfile. - Multi-page sketch books with a portable, human-readable
.skbkJSON format and atomic (write-then-rename) saves. - Undo / redo history, clear, custom ink colors, and keyboard shortcuts.
- Select tool for moving and deleting existing strokes.
- Text tool with editable, resizable on-canvas text boxes.
- Sketchbook pages panel with thumbnail previews, page-turn animation, and add / delete / navigate controls.
- Native application menus: File (New, Open, Save, Save As, Export PNG, Export JPEG) and Edit (Undo, Redo).
- Image export to PNG (transparent paper) and JPEG (flattened background).
- Auto-sharpen settings panel exposing wobble, smoothing, circle snap, end taper, rotational symmetry (mandala mode), and text size. Live sharpen now defaults to off.
- Embeddable, browser-safe API (
NapkinSketch) for websites, WordPress blocks, and VS Code webviews, shipped as both ESM and IIFE bundles. - Generated application icon and electron-builder packaging (Windows NSIS installer with Start-menu and desktop shortcuts, plus macOS/Linux targets).
- Unit test suites (Node built-in runner) for geometry, the sharpen engine,
.skbkserialization, CLI parsing, and the launch contract. - Accessible, calm UI following a 60-30-10 cool/neutral palette with a single warm accent, WCAG-AA contrast, and reduced-motion support.
Fixed#
- Eraser now reveals the paper beneath strokes (two-layer compositing) instead of painting over them when Live sharpen is off.
- Saved file name is shown in the status bar and window title instead of always reading "unnamed".
- Added thumbnail-based page navigation so earlier pages can be revisited after adding a new page.