Trail Log

Every release, newest first — features, performance work, and fixes. Press a month to narrow the log, or All to see everything. Each month opens with what it amounted to, and the commits behind it.

Filter releases by month

2026

Commits per dayLessMore

223 releases · all time

All of 2026

157 features · 223 releases · 589 commits · 249 fixes

  • v8.75Shapes duplicate in any direction, the Stroke Stabilizer steadies every brush, and the lists stop disagreeing with each other
  • v0.9.12Review panel, 360° bubble tails, dev tier switcher
  • v0.9.8Smart export, batch text, and a unified button system
  • v0.9.10June optimization pass — faster Rust, smaller WASM, Squoosh-style resize, shape edit boxes
  • v0.9.3Color picker + font family
  • v0.9.27Security tab, snapped-window layout, reduce-motion fixes, and a keyboard-accessibility pass
  • v0.9.21Settings menu, pick-your-format downloads, and a hardening pass
  • v0.9.15AI tools go live (Replicate) + Stripe billing
v8.7511 September 2026latest

Shapes duplicate in any direction, the Stroke Stabilizer steadies every brush, and the lists stop disagreeing with each other

  • feature

    Pick a rectangle or a circle out of Reselect and press the d-pad on its row: four plus buttons appear around the shape on the canvas. Press one and you get another copy of the same size, clear of the original, in that direction. Press the left one twice and you get two marching left. Each side counts on its own, so a press upward afterwards goes above the original rather than above the last copy — which is what you want when you are building a diagram out of repeated boxes.

  • feature

    Any placed text or shape can be duplicated straight from its row in Reselect. The copy is made by cloning the object inside the engine rather than rebuilding it from a list of properties, so a shadow, a rotation, a background or a perspective warp cannot be quietly left behind.

  • feature

    The Stroke Stabilizer used to reach only the Paint brush. It now steadies the Eraser, the blur brush, pixelate, redact and the clone stamp, from the one setting — turn it on because your hand shakes and it is on everywhere. The Eraser had been honouring it all along; there was simply no control in the panel to switch it on.

  • rust

    On the clone stamp the source offset is kept exactly: a lagging tip samples a source lagging by the same vector, so the smoothing changes the path and nothing else. The stabilizer is now its own engine module with its leash table exported to the browser, so no second copy of the numbers can drift.

  • ui

    History, Reselect and the Layers list are one component instead of three that had drifted apart. Row buttons sit together in one cluster and the coloured dots are gone in favour of the numbers already beside them. History and Reselect keep their buttons out of sight until you hover or tab into a row; Layers keeps its visible, because those get used constantly and the eye is reporting a state rather than only offering an action.

  • ui

    The gallery bar's header is three columns — the count on the left, the compress buttons centred, the actions on the right — and the compress pair is centred on the bar rather than on the space left over, so it stops shifting when a selection appears.

  • fix

    Dropping an image with nothing open goes straight to the gallery instead of opening a dialog whose other two choices both need an image already open. The status bar's second number is labelled. The Eraser panel says Eraser rather than Brush above a field called Brush Size.

  • feature

    The "+" on any colour swatch opens a real colour picker — a hue wheel or a saturation/brightness rectangle, with hex, RGB and HSL fields that all track each other and a before/after split over a checkerboard. Colours you keep land in a palette that follows you: saved on your device when you are logged out, synced to your account when you are signed in.

  • feature

    The mobile version can save a photo to your device, which the notice now says — it previously claimed only uploading and browsing.

v8.7410 September 2026

Photos develop as they land, the gallery bar says what it will act on, and phone widths get a real mobile version

  • feature

    A pasted or dropped photo used to sit as a grey block until its thumbnail was ready. It comes up like a Polaroid now — white paper that sweeps open from the top — and every tile takes at least a quarter of a second even when the pixels are instant, because a thirty-millisecond flash reads as a glitch. Several at once come up left to right. A slow photo slows the develop rather than freezing it.

  • feature

    Below 600 pixels wide Image Horse is a real mobile version: add photos from your library or camera, browse the gallery, and pick the editing up on a bigger screen. It replaces the notice that used to say the window was too small.

  • ui

    The gallery bar has three arrangements — nothing, one, many selected — and the scope is always in the button text. Auto Compress & Resize moved out of the Enhance panel and into the bar, beside the photos it acts on. The confirm for one photo says one photo. Compact thumbnails have room between them.

  • feature

    Create AI Image opens a real dialog — prompt, reference images, and a plain sentence about what leaves your tab. Generate is off and says why: no model is chosen yet, and a button that does nothing is worse than one that tells you.

  • fix

    The font selector stops promising faces the engine cannot draw. It lists the one it has, and the text you type previews in that face, so nothing snaps on commit.

  • fix

    Four resize controls that read as "spin button" to a screen reader have names, and clicking the word focuses the field.

  • infra

    A third matched-pair guardrail ties the engine blur to the GPU shader — the edge that nearly shipped a one-bit difference in production. All four CodeQL alerts closed: one pattern, a literal reaching a name containing salt in a deterministic random generator.

v8.739 September 2026

The GPU blur matches the engine exactly, and A/B compare stops locking itself

  • fix

    Compare after a compress or resize used to go dark when you switched tools and came back. It was asking whether you had edited, using state that a tool switch or a page reload wipes. It asks whether there is an original to compare against now, which is what it actually needs, so it stays available.

  • feature

    The GPU blur is wired up behind a flag that is still off. On a real GPU it produces the same bytes as the engine, at every size tested, and falls back to the processor on every failure — no adapter, a software one, a lost device. Turning it on is a separate decision.

  • fix

    The reference implementation the shader was checked against had never actually matched the engine: it added numbers at double precision where the engine uses single, and the disagreement only appears above 64×64 — which was the largest size the check ever ran. It passed for its whole life by never looking where the problem was.

  • fix

    Rotated text is anchored at its top-left corner now. It was pivoting around the middle of its own box, and the box grows as you type, so the words drifted. Unrotated text was always correct and still is.

  • fix

    Two loads landing at once no longer cancel each other. Opening a photo while another was still loading made both fail.

  • perf

    The Diagnostics window was being fed on every single frame whether or not it was open — about fourteen round trips to the engine each time, one of them a full-image scan. It is fed when you are looking at it.

  • feature

    Fonts are served from the app itself rather than fetched from Google, so the logged-out editor really does make no network calls.

  • fix

    Several text fields asked for colours that were never defined — thirteen places requesting a colour that did not exist.

  • ui

    The toaster moved up, out of the way of a dialog's header.

v8.729 September 2026

The panels close from their corner, and the workspace makes room on three sides

  • feature

    Hover Tools, Gallery or Review and a small close appears on the panel's top corner, half outside it — on the edge rather than over the first button. It shows for forty seconds after you enter the panel and then stops offering, and comes back when you do. The top bar's own toggle brings a closed panel back. Below tablet width the panels keep their tabs and this does not appear.

  • ui

    When the window is narrow enough for the top bar to drop its labels, that bar and the compact master bar are both one flat row — no button groups, the controls spread evenly across.

  • ui

    The checkerboard behind the canvas already moved aside for Tools and Review. It moves for the Gallery now too, and its edges feather instead of ending on a hard rectangle.

  • fix

    Text fields had a black outline in light mode. They were never given a colour at all, so they fell back to the text colour — near-white in dark, which looked deliberate, and black in light, which did not. They wear the app's warm accent now, in both themes.

  • fix

    In compact mode the tool panel no longer sits beside the welcome screen offering Compress and Resize for an image that does not exist yet.

  • infra

    The service worker had five tests covering registration, its precache, an offline reload and both build-skew cases — and nothing ever ran them. They run in CI now, on both a default build and a service-worker build, because either half alone is misleading. Nothing is activated; the worker still ships dark.

  • mock

    Two decisions are written down rather than built. The text box moving as you type turns out to be two separate defects, and only the rotated one is a bug — unrotated text has always been correct. And custom fonts need engine work rather than a settings page: the font picker that ships today changes the preview and not the result.

v8.718 September 2026

Resize and Compress are one tile, and the panels close from their corner again

  • ui

    Enhance → Resize & Compress is one panel now, ordered the way the pixels go: the two scores on top, then Resize, then Compress. Both halves move the same two numbers — Web Performance Gain and PageSpeed Insights Score read the dimensions and the format and quality together — so splitting them had put the readout under one tile and half of its inputs under the other. Squoosh keeps the whole pipeline on one panel for the same reason.

  • ui

    There is one Apply button, and it says what it will do: Apply Resize when only the dimensions changed, Apply Compression when only the quality, format or method did, Apply Compression & Resize when both did. Undo puts the quality slider back as well as the pixels.

  • feature

    Hover Tools, Gallery or Review and a small close appears on the panel's top corner, half outside it — the same X the Layers list uses, on the edge rather than over the first button. It shows for forty seconds after you enter the panel and then stops offering, and comes back when you do. The top bar's toggle brings a closed panel back. Below tablet width the panels keep their tabs and this does not appear.

  • ui

    When the window is narrow enough for the top bar to drop its labels, the bar is one flat row — no button groups, the controls spread evenly across it.

  • fix

    The icons pop when you hover a tool tile. That was already true; it now lives in one place, and the gallery thumbnails and the Review panel's toggles do it too. It also respects Reduce Motion, which the old version quietly did not.

  • ui

    The Enhance panel's spacing was measured against Select → Magic Wand → Tolerance and made to match: sixteen pixels from each heading to its first control, a rule between the scores, Resize and Compress, and the Scale slider as close to width and height as the Method dropdown sits to Quality.

v8.708 September 2026

Security headers ship, and the policy watches before it blocks

  • infra

    Three security headers now go out with every response. X-Content-Type-Options stops the browser guessing at a file's type, a Referrer-Policy keeps full URLs from leaking to other sites, and a Permissions-Policy turns off camera, microphone, geolocation and ad-topic sharing — none of which this app has ever asked for. Nothing about the editor changes.

  • infra

    The content security policy only reports for now. A wrong policy is invisible to every gate here, because nothing local serves headers, and then it breaks the editor in production and nowhere else — so it watches first. That paid off immediately: the first real page load turned up three things reading the bundle had missed, the best of them Clerk quietly building its own workers from blob URLs at runtime.

  • fix

    One address came back out of the policy. cdn.jsdelivr.net was allowed because the emoji picker looked like it fetched its data from there. Opening the picker settles it — Create, then Stamp, then Emoji — and the whole set renders without a single request leaving the page, because the data is compiled in.

  • infra

    The deploy check that guards the engine now fails closed. It compares the WebAssembly the site serves against a record the build writes about itself, and a missing record used to count as a pass so the first deploy would not be blocked. One is live now, so absence is a failure.

  • perf

    The GPU blur was measured against the engine's own, which had never been done. On this laptop the GPU wins at every size tried: 93 ms against 8 at 1024 pixels, and 645 ms against 12 at the widest blur radius. Almost none of that is the shader — it is the cost of moving pixels to the card and back. Nothing uses it yet; the numbers exist so the decision can be made on numbers.

v8.697 September 2026

A panel that fits, and a status bar that stays put

  • ui

    Edit → Rulers is now Rulers and Grid, which is what it has always done. The half-built Measure entry beside it is gone — it was a placeholder from before the overlay shipped, and two things called Ruler and Rulers sitting next to each other was never going to help anyone.

  • fix

    That panel was too wide for the sidebar it lives in: the third grid layout button hung off the right edge, and a paragraph of explanation above each control pushed everything down. The paragraphs now live behind the lightbulb icons, where the rest of the app keeps its explanations, and the controls use the same button groups the paint tools do. It fits.

  • ui

    The status bar has a fixed shape now. On a desktop it shows six hints: two for the tool you are holding, two that rotate through the rest, and the last two — Alt+/ for the shortcut list and Alt+, for the command palette — always in the same place. On a tablet-width window it drops to two, and Alt+/ keeps the last slot. The point is that the two ways into everything else stop moving around.

  • fix

    Layer rows got their annotation counts back out. They arrived last release as small numbers on every row, and a row that already carries a name, an eye, five buttons, a mask badge and an opacity slider has no room for two more. The counts now sit in one line under the list, describing the layer you actually have selected.

v8.686 September 2026

Layer rows say what they hold

  • feature

    Every layer row now shows how many shapes and text boxes it carries, beside the mask badge it already had. The numbers only appear when there is something to count, so an ordinary photo layer stays as quiet as before.

  • ui

    The Colour Overlay tints a layer clipped to its own pixels, which means on a layer with nothing on it the swatches were offering an effect that could not happen — you picked a colour and nothing changed. They are disabled there now, with a line saying why rather than just going grey. Paint something on the layer and they come back; undo that paint and they go away again.

  • rust

    The engine binary now comes out byte-for-byte identical whether it is compiled on a laptop, in CI, or on the deploy server. That sounds like housekeeping and is not: until now nobody could say for certain that the engine running on the site was the engine that had been tested, because three machines produced three slightly different binaries from the same source. They differed only in the build machine's own directory paths, baked into the file by the compiler. Those are gone, the tool that optimises the binary is pinned to one version, and the deploy now publishes a fingerprint of what it built so the site can be checked against it.

  • infra

    That new check caught something on its first real run: the build server and the laptop agreed, and CI did not. One path had been missed when the fix went in. The check was right, the correction was one line, and the whole point is that it was noticed at all — the previous check could not see a difference of this size, and a separate audit this week found three checks that had been passing because they were incapable of failing.

v8.675 September 2026

Shapes stack the way you tell them to

  • feature

    Shapes now have a front-to-back order you control. Right-click any shape for Bring to Front, Bring Forward, Send Backward and Send to Back, or use Ctrl+Shift+↑ and Ctrl+Shift+↓ — every bracket chord was already taken. The menu only offers the moves that can actually happen: the topmost shape gets no forward options, the bottom one no backward ones.

  • fix

    The first version of that shipped a bug worth describing. Reorder a shape, press undo, and the newest shape was gone — not moved back, gone, with the reorder still applied. Undo tracks edits by pairing each one with a snapshot, and a reorder was taking a snapshot without registering an edit, so one press rewound the wrong step. It never reached a release; it was caught the same day it was written. The safety check that was supposed to catch it could not: it compares what the canvas looks like before and after, and two shapes of the same colour swapping places look identical.

  • fix

    A layer file with a damaged opacity value could empty the entire layers panel. Importing an OpenRaster file reads opacity out of the document, and a non-numeric value became NaN. Clamping does not fix NaN — it hands it straight back — and the panel reads its layers as JSON, which has no way to write that number. One bad character in one attribute and every layer disappeared from the list. The engine now cleans the value where it comes in.

  • rust

    The engine stopped depending on which machine compiled it. Rust embeds the build machine's own directory paths in the binary, so the same source produced different bytes here and on the build server — 139 of them, which made it impossible to say whether a shipped engine was the one that had been tested. Those paths are now rewritten to a fixed name, and local and production builds come out byte-for-byte identical.

v8.664 September 2026

The hole you erased stops coming back black

  • fix

    Erase something — the Magic Eraser, the eraser brush, a layer mask — then export as JPEG, and the hole came out as a black patch. JPEG has no transparency, so every see-through pixel has to be painted onto a colour before the file is written, and nothing on the export path was doing that. The browser encoder does not ask what you wanted; it writes black. Holes now become white, and PNG, WebP and AVIF keep their transparency untouched. Black was never a wrong colour choice, incidentally — it is what you get when nothing chooses at all.

  • ui

    Rulers and the grid overlay have moved out of Settings and into Edit → Rulers, where you reach for them mid-edit instead of going to configure the app. The tick labels can now read in pixels, inches or centimetres. Inches and centimetres are worked out at 96 DPI — a web image has no real-world size of its own, so that is a stated convention rather than a promise about print, and the panel says so where you choose.

  • fix

    Undo and Redo were greyed out so far they disappeared. Measured against the bar behind them, the disabled icons came to 1.33:1 in light mode — invisible rather than dimmed. Fading them was never going to work: the enabled colour was itself only 3:1, so every step down from it landed below the floor. Both states move up together instead, and a disabled button now reads as quiet rather than absent.

  • infra

    Three backlog items were opened this week expecting a small afternoon of TypeScript, and all three turned out to need the Rust engine instead: a per-layer annotation badge, shape z-order, and a layer-emptiness check. Rather than half-start any of them, each was measured and written up with the exact function it needs and why the TypeScript route is closed. They will ship together, sharing one engine rebuild, because spending a whole verification cycle on two integers is the wrong trade and spending it on three primitives is the right one.

  • infra

    Five dependency updates landed, and the Netlify build settings turned out to hold a stale copy of the build command — one that would compile the engine without its features, exactly the fault that shipped for ten releases in July. The file in the repository wins today, so nothing is broken; it is written down as a loaded gun rather than quietly left.

v8.653 September 2026

Skewed is the rare case

  • feature

    Dragging a corner of a selected image or layer now keeps its proportions, and holding Shift frees it. That is the opposite of what it used to do, and the reason is that stretching a photo out of shape is the rare thing to want and the one that visibly damages the picture — so it is the one that costs a modifier. You cannot do it by accident any more, and the common case needs no keyboard at all. Crop and the shapes are deliberately unchanged: choosing a rectangle is not the same as scaling a photograph, so an arbitrary shape stays the no-modifier case there. Edge handles are still free everywhere, because a single-axis drag has no second axis to reconcile.

  • fix

    The two thumbnails on the "Welcome back" screen showed the browser's broken-image icon instead of your photos. The screen built its preview images and then threw them away a moment later, before the page had drawn them — so every return visit greeted you with two grey placeholders. They are built and released together now, and the same fault is fixed in the resume dialog.

  • ui

    The New Image panel is laid out as tiles — three across, icon above the label, matching the tool panels rather than looking like a different app. There is a fifth tile for Create AI Image, deliberately visible and disabled, so the shape of what is coming is honest rather than hidden. The row underneath is four plain icon buttons at one size: sign in, the website, GitHub and Codeberg. The sign-in button that used to float in the corner is gone — it lives in that row now, where the other buttons are.

  • infra

    Two investigations that produced no code are written down in the repository instead of being lost. Neither site sends a Content-Security-Policy — filed rather than patched, because a policy here has to allow the WebAssembly engine and its workers and would break image loading in production if guessed at. And the long-running report that painting hit every layer was driven and measured at last: after cutting a selection to a new layer, a brush stroke changed exactly one layer's pixels, and the other two were byte-identical. Cutting produces a full-size layer that sits on top, so a stroke on it covers everything below and reads as if it went everywhere. The pixels were always going to the right place.

v8.642 September 2026

The status bar stops whispering, and undo stops moving your layer in silence

  • ui

    Half the status bar was one grey and half was another, on the same row at the same 10px — the file size, dimensions and zoom looked right while the brand, the shortcut labels and their key chips looked washed out beside them. That was not a taste problem. Measured against the bar's own background, the dim half came to 3.65:1 in light mode, under the 4.5:1 that WCAG AA asks for at this size, and the shortcut hints were dimmed TWICE because a 70% opacity sat on top of the faint colour. Everything in the bar now uses the same value the readouts already used: 6.99:1 in light, 8.56:1 in dark.

  • fix

    Undo could move which layer you had selected, without saying so. It is not restoring "the layer that operation belonged to" — no such link exists in the engine, where exactly one of sixteen operation types records a layer at all. What it restores is whichever layer was active when the snapshot was taken. So if you run an operation, pick a different layer, then undo, your choice is quietly discarded. The row you land on now flashes for a moment, so the jump is something you see rather than something you discover later. Selecting a layer yourself stays silent — only a change you did not ask for flashes.

  • infra

    The swap itself is deliberately still there. Making undo remember which layer an operation belonged to means teaching the engine an ownership it has never had, which is a much larger change than a flash; the honest version of this release is that the behaviour is unchanged and only its visibility is fixed.

  • infra

    AppShell.tsx lost another 96 lines — the internal save that re-encodes your canvas over its stored copy moved into a file of its own. Nothing about the app behaves differently. Worth recording alongside it: the whole file was inventoried while picking what to move, and the numbers say this approach cannot finish the job. Of 247 blocks in it, only 34 are big enough to be worth relocating, and moving every one of them would still leave roughly 2,560 lines. The remaining work is two different changes, and they are written up rather than started.

v8.632 September 2026

The bar buttons line up, and the active layer says which one it is

  • ui

    The five buttons across the top bar — New, Tools, Gallery, Review, Export — each sized themselves to their own text, so "New" came out 71.6px against Gallery's 100.4. Twenty-eight pixels of difference between two things that are meant to read as peers. They are all one width now, the width of the widest label. The group costs 57.6px more and grows symmetrically, so the centred cluster does not move.

  • fix

    In the Review panel's layer list, which layer was active lived in a CSS class and nowhere else. It looked right and announced nothing — a screen reader heard four identical "Select <name>, button" rows with no way to tell them apart. The active row now carries aria-current. Not aria-pressed: a layer cannot be un-selected, so "pressed" would advertise a state you cannot reach.

  • fix

    New and Export in the mobile bar announced themselves as unpressed toggle buttons. They are actions, not toggles — there is no "export mode" to be in — and a comment two lines away in the same file said exactly that. The bar was passing aria-pressed=false explicitly, overriding the button component's own rule of leaving the attribute off when it means nothing.

  • infra

    CI installed whatever wasm-pack "latest" happened to resolve to. On September 2nd that was v0.15.0 at 03:04 UTC and v0.9.1 at 03:22 — same workflow, same commit, eighteen minutes apart, one green and one red. The old one ships a 2020-era wasm-opt that cannot parse the engine's WebAssembly, so the build died with nothing wrong in the repository, and a plain re-run went green again. That is the worst shape this kind of bug takes: it reads as flake and it is an unpinned dependency. The Netlify build had been pinned to 0.15.0 for months, with a comment predicting this exact failure. GitHub CI never got the same pin. It has one now.

  • infra

    The marketing site's architecture page promised "a service worker that precaches the shell and the WASM binary" in visible copy, fifty lines below a source comment correctly stating that no service worker ships in a build. It credited the codec worker with the OffscreenCanvas the engine worker owns, listed a rayon worker pool as planned when it had been measured and rejected, and never mentioned the engine worker at all — which has been the default since v8.32 and is the largest structural fact about the app.

v8.621 September 2026

The architecture doc caught up to the architecture

  • infra

    Nothing in the app changed this release. What changed is that the documentation stopped describing a version of this project that stopped existing around v7.8. The architecture doc's status line said v7.8 and its diagram showed the Rust engine running on the main thread — it has not run there since v8.32, when it moved into a Web Worker that draws straight onto a transferred canvas and took the main thread's blocking time per heavy operation from 129-137 ms to zero. That was the single most wrong thing in the docs. The diagram is redrawn around it.

  • infra

    Rather than bump version numbers, every claim in the doc was re-read against the code, and seven were wrong. The tool registry existed when the doc said it did not, and was unwired when a working note said it was finished. The service worker was written up as "investigated only, nothing wired, no ADR" — it has been in the tree since July under an accepted decision record, shipping switched off, which is a third state the doc had no room for and now has a section for. Undo was still written as a plan plus its correction rather than as the one system it has been since v7.36. AppShell was cited at 2,930 lines; it is 3,813.

  • infra

    The README's changelog went on a diet. It carried 88 release sections against its own "latest release below" rule — every release added one, none ever removed one — and it carries one now, which took it from 1,874 lines to 118. All 87 that were cut were confirmed present in the full dated history first.

  • fix

    Every relative link in all 80 tracked Markdown files was checked against the path it actually resolves to. Two were broken, both pointing at docs that moved into the archive in August. Both fixed, and the count is now zero.

August 2026

10 features · 100 releases · 187 commits · 120 fixes

  • v8.54The black border was going into the saved file too, and the canvas goes back to opt-in
  • v8.43Perspective — put a photo, or your words, onto a surface that recedes
  • v8.57Ctrl+Z had been bound twice for the app's entire life
  • v8.56The ring marking the current photo was painted underneath it
  • v8.41The brushes keep up
  • v8.38Layers become a targeted thing
  • v8.40Text boxes resize, and the words rearrange to fit
v8.6131 August 2026

Export moved to the bar, and the Tools panel got a row back

  • ui

    The way to save your work was a full-width "Download & Share JPEGs" button pinned to the bottom of the Tools panel — a large button under its own top border, so a 252-pixel sidebar spent a whole row plus padding on one action, under every tool, whether or not you were anywhere near finishing. It is now the fifth item in the bar you already use to move around the app: New, Tools, Gallery, Review, Export, in the wide top bar and the compact one alike. Same dialog, same Alt + E, no footer.

  • ui

    Two things had to give for five buttons to fit the compact bar's fixed 252 pixels. The divider after New is gone — with Export added, New and Export are both actions while the middle three are tabs, so a rule after New fenced one action in with the tabs and left the other outside it. And the gaps went from four pixels to two, which is what they were the last time this row ran out of room. Measured in a browser rather than assumed: the row comes to exactly its container, nothing clipped.

  • fix

    Export is the only button in that group that is not a toggle. It fires an action rather than turning a panel on, so it never lights up, and it greys out when there is no image instead of pretending to be available. Its tooltip says Alt + E because that shortcut was checked in the code before it was written down — the first draft said Ctrl + S, which is not bound to anything.

v8.6031 August 2026

The celebration's highlights caught up, and its comment stopped quoting numbers

  • ui

    Ctrl + \ counts the month's shipping. The counts have been generated since v8.58, but the short list of highlights beside them is hand-written on purpose — a headline is a judgement about what mattered, and the log's feature tag does not carry that; August's biggest change shipped tagged infra. The list now includes the two things from v8.59 a user can actually notice: the mask tile saying it picks up the brush, and the upload dialog no longer drawing a progress bar out of thin air. The lockfile repair and the nav change are deliberately left off — a highlight in the editor for something you cannot find there reads as a missing feature.

  • fix

    The note above that list had been making its case with the month's totals typed inline, and they were two releases out of date — the same hand-typed-number problem the generated counts were built to end, sitting in a comment directly beside them. The live figures are gone from the prose rather than refreshed, because refreshing them would have restored the trap. July's stay, since July cannot change.

  • ui

    The highlights render two to a row, so an odd number leaves the last one alone in a half-empty row. The list is kept even and the comment now says so out loud, which is cheaper than the next person rediscovering it.

v8.5931 August 2026

Every build broke on a lockfile nobody had edited

  • fix

    Two dependency updates landed back to back, and each rewrote the lockfile. The second had been branched before the first, so it carried its own copy — and because the two edits sat in different parts of the file, git merged them without reporting a conflict. What came out listed acorn, brace-expansion and minimatch twice each, which is not valid YAML. pnpm refused it outright, and with it went the frontend build, the Convex checks, the marketing build, the dependency audit and the Vercel deploy. The site stayed up the whole time on its last good deploy, but nothing new could ship until the duplicates came out.

  • infra

    GitHub calling a pull request mergeable means it found no textual conflict. It says nothing about whether the file it produced still parses, and a lockfile is exactly the file where that distinction bites. The repair is 23 deleted lines and no additions: every duplicate block was byte-identical to the one kept, so nothing about which versions resolve changed. Regenerating the lockfile instead would also have cleared the error, and would have quietly carried two packages to versions nobody had reviewed — an upgrade smuggled inside a repair.

  • infra

    The config gap that caused it is closed too. Dependabot had been filing every package on its own, so it raised react to 19.2.8 and left react-dom at 19.2.7. React checks those two match at runtime and throws when they don't, so the tests died the moment one mounted a component. The four React packages are grouped now — they move together or not at all.

  • ui

    "Add mask" says what it is about to do. The tile creates the mask and then switches you to the Paint brush, which is correct, since a mask is something you paint. But it did that silently, and the switch closes the Layers panel, so you ended up somewhere else with no explanation of how you got there. It now says so, the same way the "Paint mask" tile beside it always has. Nothing about the behaviour changed — only the silence.

  • fix

    The upload dialog's progress bar was not measuring anything. It filled by a random amount every tenth of a second, stopped at ninety percent, and jumped to full when the load finished. Whether you saw it at all came down to whether the photo decoded faster than the timer, which is why it seemed to appear only sometimes. The gallery already shows the real thing — a thumbnail per photo, as each one decodes — so the bar is gone, and the dialog stops re-rendering ten times a second while it loads.

  • ui

    The button in this site's nav reads "Beta" instead of "Demo". The long form still lives on the hero, the pricing page and the ⌘K palette, where there is room for it. Its accessible name stays "Open the demo (Beta)", because a screen reader reading the button on its own needs a destination, not a status.

v8.5831 August 2026

Apply Resize was showing up while you were compressing

  • fix

    The Resize tool's two tiles — Compress and Resize — share one row of buttons underneath them, and that row sat outside the control that switches tiles. So the resize-only button rendered under Compress too, offering itself in the middle of a job it explicitly does not do; its own tooltip says it leaves the quality slider alone. It hid for months because it is greyed out unless you have changed the width or height, and those fields live in the other tile, so a greyed button read as "not for me right now" rather than "wrong panel". The percent slider under Compress changes dimensions too, though, so it was reachable and live from the wrong place. Apply Compression & Resize stays under both tiles on purpose: it is the button that commits everything and the one that unlocks A/B Compare.

  • fix

    The shipping celebration had been stuck on July since July. Ctrl + \ pops a confetti burst counting what shipped this month, and those numbers were typed in by hand. The note above them said to re-derive rather than guess, but nothing ever did — so on the last day of August it was still reporting July's 205 entries across 61 releases while August had quietly shipped 281 across 96. The comment had predicted the exact failure it then had.

  • infra

    Those numbers now come out of the release log itself, generated by the same script that already redraws the trail-log squares and the feature list every release, so they cannot drift again. It throws rather than writing zeros if it parses no releases, so a change to the log's shape fails loudly instead of shipping an empty popper. Deriving it beat adding a step to the release routine: a routine step is a rule that depends on someone following it, and the comment that went stale already was that rule.

  • ui

    The celebration also stops leading with the feature count. A month can ship more than the one before it and carry fewer things tagged as features — August did exactly that, because the work went into moving the engine into a worker and the fixes around it. Counting only features would have rendered the busiest month in the log as the quietest, so it leads with everything that shipped and keeps features as a secondary line.

v8.5730 August 2026

Ctrl+Z had been bound twice for the app's entire life

  • fix

    Two separate places listened for Ctrl+Z on the window, so every press undid two things. It had been that way since the shortcut existed. Nobody caught it because the only end-to-end test covering undo built a one-entry history — the second undo found nothing to do and returned quietly, so the test passed on a broken binding. One press is now one step.

  • fix

    Undo was dead after you touched any slider. The duplicate listener that got removed was the one with no guard on it; the one that survived treats a focused input as the user typing and steps aside — and a range slider is an input. So dragging Opacity or a brush size stopped undo answering until you clicked elsewhere. Sliders, checkboxes and radios now let undo and redo through and nothing else, so a focused slider's arrow keys still belong to the slider.

  • fix

    Committing a line of text wrote two history entries instead of one. The text tool compared its drop-shadow settings before and after and counted a change even when the shadow was off on both sides, because a new annotation starts at all zeros while the panel's idea of off carries a default colour and offset. Off to off is a no-op now. Text with the shadow actually on is still two steps — that one needs the engine to group operations.

  • feature

    Colour Overlay — Photoshop's layer style, under Layer Mask in the Layers tool. Pick a colour to turn it on, drag Strength to blend it back toward the real colours, Apply to bake it in or Remove to drop it. It tints what the layer actually contains, so transparent stays transparent with no coloured fringe at the edges, and it sits under the mask, so masking a tinted layer hides the tint with it. One undo removes the whole thing however long you spent dragging the slider. It survives merging, flattening, exports and the thumbnail; it does not survive a reload yet — it is session-lived, exactly like the layer mask it sits under.

  • fix

    Delete and Backspace remove the selected shape. There was no handler for either key anywhere except the reselect list, so the obvious way to get rid of a shape did nothing at all.

  • fix

    Drawing inside an unfilled rectangle now draws inside it. The empty middle of an outlined shape used to count as part of it, so starting a drag there re-selected the outer shape instead of drawing a new one. The hit area for an unfilled rectangle, circle or hand-drawn circle is the outline itself now. Filled shapes, pins and curves are unchanged — clicking the middle of a filled shape still picks it up.

  • fix

    Ctrl+Z on a shape you have drawn but not committed discards that shape. It used to undo the previous action instead and leave the new shape sitting there, which read as undo skipping a step.

  • rust

    The engine grew 3,533 bytes to 816,185 — overlay 3,149, the ring hit-test 349, the shadow comparison 35 — and stays inside the size band the build watches. The static guardrails caught 166 lines of engine tests that had been written into lib.rs while every documented local gate was green; they moved into the layer module and one helper collapsed two hand-built snapshot initializers, taking lib.rs from 5,213 lines to 5,183.

v8.5628 August 2026

The ring marking the current photo was painted underneath it

  • fix

    A gallery thumbnail lights one ring for the photo open on the canvas and another for photos ticked for a batch job. The inner one was drawn on the tile's background layer, which sits below everything inside the tile, so the photo covered it. What survived were four corner slivers where the rounded corners pull apart — which is why the current photo read as brackets — and in the vertical grid, where the image fills the tile, nothing at all. The check that passed it read the style property, and the property was set correctly the whole time.

  • feature

    Ctrl+Shift+] and Ctrl+Shift+[ send the active layer to the front or the back. The shifted pair was already taken by accident: the handler never tested for Shift, so all four bracket chords changed brush size while the shortcuts table documented only the unshifted two. The engine already knew how to move a layer and already clamps the ends, so a layer still cannot be sent underneath the canvas.

  • ui

    Magic Eraser stops borrowing Magic Wand's icon. Two different tools in two different groups drew the same glyph, which reads as one feature appearing twice. Magic Eraser gets a broom; Select's Magic Wand is unchanged.

  • ui

    Both bars now use one button size. The top bar ran three corner radii and two button heights across four clusters, and two of its controls sat in no container at all. Everything is now the 30px button in a 38px group that the Review panel's toggles already used.

  • fix

    AI Rename lists every token it actually accepts. The panel offered nine; the renamer honours eleven — {palette} and {contrast} worked and appeared in no help text anywhere. The list is generated from the same table the renamer substitutes from, and a test fails if the two ever disagree.

  • rust

    The engine lost 755 bytes of code nothing called: five Rust functions, two WASM exports, a React component that was never imported, and six dead TypeScript exports. Three new checks keep the next batch from settling in quietly — a file-length ratchet pinned to today's sizes, a dead-export scan, and a line cap on the engine's largest file. Each was tested by breaking it on purpose first.

v8.5519 August 2026

Clicking a layer you weren't on used to make a new one

  • fix

    Text and shapes belong to a layer, but the canvas shows every layer at once — so you could see a caption perfectly well, click it, and have the editor decide there was nothing there and start a fresh empty one on top of it. It looked like the app was duplicating your work at random. It was answering a different question: is there anything here on the layer you have selected? A miss now stays a miss.

  • fix

    Three places had the same fault, not one: text, the pin tool, and the shape and arrow tools — the last of which would start dragging a new box across whatever you were pointing at.

  • fix

    Hiding a layer used to survive until you reloaded. Visibility, opacity, name and which layer is selected were never written down, so a layer you hid came back visible and the selection had quietly moved to whichever layer was on top. The app only schedules a save when something it watches changes, and it was watching a yes/no flag that was already yes — so only the first such edit in a session would ever have saved. It counts edits now.

  • ui

    Selected and current are two different things, and a thumbnail can be both. The photo open on the canvas is marked inside its own edge; photos ticked for a batch job are marked outside it. Before, whichever the browser drew last erased the other, so a photo that was both looked like it was only one. Keyboard focus keeps its own dashed outline and no longer competes with either.

  • fix

    A failed import blames the right thing. Opening the app over plain http from something that isn't localhost — a phone testing against a laptop — makes the browser withhold the crypto needed to store originals, and every import failed with "Couldn't open your-file.png". That sends you to inspect a file that was never the problem. It now says the page isn't a secure context, and names the address.

  • infra

    The deploy installed whatever Rust shipped that day and whatever wasm-pack was newest. Both are written down now, so the engine binary is a function of the repository and a compiler upgrade is a commit you can see and undo. Pinning the packer pins the optimiser with it.

v8.5418 August 2026

The black border was going into the saved file too, and the canvas goes back to opt-in

  • fix

    Last release stopped the black border on the way out. It was also going in. Three export surfaces learned to leave a transparent backing canvas out of a JPEG; saving was a fourth surface, and it was the one that got away. Applying compression with JPEG selected wrote the padded canvas into the stored working file, border and all — and from that moment the border was real pixels, so every later export carried it whatever the export setting said.

  • fix

    Saving now refuses on the same terms, and it does not consult your export preference first. Writing invented black into a file you are keeping is not a setting.

  • feature

    The backing canvas is left out of exports again by default. It had been included since July; the argument then was that what you see on screen is what you get on export. The canvas is padding you never asked for, it arrives on every import, and it quietly changes the dimensions of every file you save — a 240×160 photo was exporting at 260×180, so the numbers in the resize panel were not the numbers you got. If you already changed that setting, you keep what you chose.

  • feature

    Apply Resize does only that. The panel had one button for two decisions — how many pixels the image has, and how hard it is compressed — so wanting the first meant accepting wherever the quality slider happened to sit. Apply Resize resamples and stops, re-encoding in the photo's own format at full quality. Resizing is not the moment to change a file's type.

  • ui

    The gallery badge tells you when a file got bigger. It only ever counted savings, so anything that grew — an upscale, or quality raised past the original — showed no badge and looked untouched. Growth now shows in amber with a plus, and it is not capped: a file two and a half times the upload reads +150%.

  • ui

    The status bar shows the size you uploaded next to the size the canvas is now, so you can see what a resize actually did.

  • feature

    Layers go from three to eight for everyone, and paid gets sixteen. Three was a limit people hit doing ordinary work, which made it feel like a paywall rather than a ceiling — layers are local editing and stay off the paywall. Paid moves from unlimited to sixteen, which is a reduction on paper and an honest one: an .ora export holds every layer's pixels at once and the engine's memory never shrinks, so the far end of unlimited arrived as a tab that ran out of memory.

  • ui

    The brush-size shortcuts were filed under Stamps (Clone) and read as clone-only. They work for every brush that draws a size ring, and now say so. The ring itself no longer appears on tools that do not have a brush.

v8.5318 August 2026

A black border on every JPEG, and the compiler that kept changing the engine's size

  • fix

    Every JPEG export came out with a black border. A photo opens on a slightly larger backing canvas, that canvas ships with the export, and its default fill is transparent — which PNG stores perfectly well, so nothing looked wrong there. JPEG cannot store transparency, and rather than say so it fills those pixels with black. A transparent canvas is now left out of a JPEG; an opaque canvas colour still exports, because that border is something you can actually see.

  • fix

    The setting behind it had been describing the wrong default in Settings for about a year — the text said the padding was off while the code had it on. Both looked correct on their own, which is why nobody read either twice. The text now says what the app does.

  • ui

    A/B compare says which side is which. The two labels used to sit centred on the divider, one top and one bottom, so each hung half over each picture and named neither. They now sit fully on their own side and point outwards, as two tabs attached to the divider. Compare also switches itself off when you leave the Enhance tools instead of staying stuck on the canvas.

  • infra

    Nothing pinned the compiler before this. The build servers fetched whatever version was current the day they ran, so the shipped file could change size with no commit from anyone — and it did. That is what a 6,579-byte discrepancy in the records turned out to be: two numbers for the same engine, both correct, five compiler releases apart.

  • rust

    The version is now written down in the repository, so upgrading becomes a change you can see, review and undo. Yesterday's release put a size limit around that number; a limit around a number that moves by itself is a limit around nothing.

  • fix

    One trap worth recording: the deploy server also set an environment variable naming the version, and an environment variable overrides the file. The pin would have quietly done nothing on the one machine that actually publishes the site — which is worse than the original problem, because everything else would have looked fixed.

v8.5218 August 2026

The build tool jumps two majors, and swaps its bundler

  • infra

    Vite 6 to Vite 8, which quietly means a different bundler underneath — Rolldown in place of Rollup. That is the piece that takes several hundred source files and produces the handful a browser downloads. Nothing about the app changes; everything about how it is assembled does.

  • perf

    It costs 126 KB on the main bundle, about 19 KB after compression. That is not defended as a win. It is the price of not drifting further from a fast-moving ecosystem, and the gap only grows with waiting.

  • fix

    The React plugin moves with it. The old version said it supported Vite 4 through 7, so running it under 8 worked but was outside what its author promised — and a build that works while ignoring a stated incompatibility is a bug waiting for a reason. With that, the dependency queue is empty: eight updates, six of which were fine the whole time.

v8.5118 August 2026

The last two updates, and why they looked broken

  • infra

    CodeQL's action goes to version 4 and the pnpm setup step to version 6. Both are build-pipeline pieces; nothing in the app changes.

  • fix

    They had sat since 3 August marked as failing, but that verdict was stale — the check they failed was one that was failing for every branch at the time, including the main one. It was fixed two releases ago; their branches were never rebuilt, so the red mark stayed on screen describing a world that no longer existed.

v8.5018 August 2026

Five dependency updates land, one is held back

  • infra

    Five updates that had been sitting unmerged since July: Serde, Clerk, two Radix UI packages, and the Node setup step CI uses. Nothing changes on screen — the point is that a queue of pending updates stops being a queue.

  • infra

    A sixth was held back on purpose. Vite 6 to 8 reads like a version bump but replaces the bundler underneath. It passes everything — typecheck, 591 unit tests, 11 end-to-end tests, both builds — and it also grows the app bundle by 126 KB and resolves outside what the React plugin says it supports. That lands on its own, where a problem has one possible cause instead of six.

v8.4918 August 2026

The last failing build check goes green

  • infra

    A check refuses to publish the engine if its size falls outside a band — too small means a build shipped missing half its features, too large means something unexpected got in. It had failed on every release since the perspective tools landed.

  • infra

    Two sizes were on record for the same engine, 6,579 bytes apart, and the gap was not code. The copy built here used Rust 1.92; the copy that actually ships is built by whatever Rust is current on the day, now 1.97. Five compiler releases are worth about 6 KB. Both binaries carry the identical feature set.

  • fix

    The floor moved UP, not down. It sat at 700,000 bytes while a stripped-down build had quietly grown to 723,755 — so the check written to catch a broken build would have waved one straight through. Nothing about the app changes.

v8.4818 August 2026

A build check that was failing on tests, and one failing on English

  • infra

    The engine has a guard that counts every place it can crash. It had been over its limit since 7 August — and not one of the new ones could reach you. They were checks inside tests, where crashing is how a test reports a failure. The guard could not tell a test from the engine, so writing tests was setting off the alarm.

  • infra

    61 test lines are now marked as deliberate and the limit comes DOWN, from 67 to 47 — lowered, not raised. What is left is 45 real engine sites, most of them low-level image routines that predate all of this. The number means what it says again.

  • fix

    A second check was failing on English. It looked for a particular unsafe cast and found the words inside an ordinary code comment. There was no such cast anywhere in the app. Comments are no longer counted as code.

v8.4718 August 2026

The icon library moves to 1.0

  • infra

    The icon library the app draws its icons from went to version 1.0, and that release drops every brand icon. The app used one of them — the GitHub mark on the start screen.

  • fix

    The GitHub mark is now drawn by the app itself, the same way the Codeberg mountain beside it already was. The two render identically, nothing new was installed to replace it, and nothing moves on screen.

v8.4617 August 2026

Three perspective tools again — and each one lights up

  • ui

    Distort, Perspective and Skew are three separate tools in Edit again, and each highlights when you pick it. They shipped as three tools that never highlighted, so you could not tell which you were in; the next release read that as a layout problem and merged them into one tool with buttons in the panel.

  • fix

    It was not a layout problem. The code that works out which tool is lit kept its own private copy of the list of tool modes, and that copy had never heard of Perspective — so it lit the first of the three no matter which you clicked. That copy is gone, three tools work as intended, and the rest of the toolbar keeps one consistent rule for where a tool's options live.

v8.4517 August 2026

Writing down why the toolbar is shaped the way it is

  • infra

    Nothing changes on screen. The last release moved Perspective's three rules into its panel, which was right for Perspective but left the app with two different places a tool's modes can live and no stated rule for which to use. The rule now: modes that are separate things you pick up — the six ways to select, the four brushes — keep their own icons in the sidebar; modes that are three ways of doing one thing to one object get a single icon and a row of buttons in the panel.

v8.4417 August 2026

Perspective is one tool again, and its icon lights up

  • ui

    Yesterday's release put Distort, Perspective and Skew in Edit as three separate tools. Edit now has a single Perspective tool, and the three rules are buttons inside its panel: Edit → Perspective → perspective | distort | skew.

  • fix

    None of the three tiles ever highlighted when you picked one, so there was no way to tell which you were in. The code that works out which tool is lit kept its own private copy of the list of tool modes — a fourth copy — and that copy had never heard of perspective, so it lit the first one in the group no matter which you clicked. The copy is gone; there is one list now.

v8.4317 August 2026

Perspective — put a photo, or your words, onto a surface that recedes

  • feature

    Drag the four corners and the image follows. Three tools in Edit share one box and differ only in what the other corners do when you drag one: Distort moves each corner on its own, Perspective mirrors the opposite edge so the shape stays a proper vanishing-point view, and Skew slides the edge keeping opposite sides parallel.

  • feature

    It works on text the way it works in Photoshop. Lay type down a car door and it recedes with the door — and it is still text. Click it again and the box comes back where you left it, because the four corners are stored against the words rather than baked into the picture. Change the wording or the size afterwards and the same perspective is reapplied to the new text instead of being stretched a second time.

  • rust

    The engine solves the projection and resamples the pixels; only the drag rules stay in the browser. That split is deliberate — the rules run on every mouse movement, up to 420 times a second, and sending each one to the engine is exactly the lag the brushes spent five releases getting rid of.

  • fix

    Two bugs got through every automated check and were found only by using the thing: the drag was being reset to a rectangle by an unrelated redraw, so applying it did nothing at all; and re-applying to already-warped text measured against the warped copy, compounding the distortion with no way back.

v8.4217 August 2026

One brush engine instead of three

  • infra

    Nothing you can see, which is the point. All three brushes that draw a stroke — paint, the clone stamp and blur — now run the same code for keeping up with your cursor. Paint had its own copy, written first and left in place when the other two were fixed. One copy is now the only copy, so the three cannot drift apart and get fixed one at a time again.

  • perf

    Measured before and after at 420 pointer events a second: no change to how fast a stroke settles. That was the point of measuring — a tidy-up that quietly costs speed is not a tidy-up.

v8.4117 August 2026

The brushes keep up

  • perf

    Clone stamp and blur used to fall behind the cursor and then catch up in a rush after you let go — worst at the big brush sizes, where a single clone-stamp stroke could bank almost eleven seconds of backlog. A stroke now keeps one request in flight and drops the positions it has already been overtaken by. Clone stamp at a 200 px brush went from 10,754 ms to 142 ms; blur at its default size from 2,509 ms to 492 ms. Pixelate and redact run through the same path as blur, so they got the same fix.

  • feature

    Text boxes resize up and down. v8.40 gave the box a width; the four corner handles now set a height as well. Text sits at the top of the box, and the box never shrinks below the words inside it.

  • fix

    Pick another shape and the one you just drew stays put. Drawing a circle and then clicking Rectangle used to retype the circle you had already placed. The shape is fixed at the moment you finish drawing it.

  • ui

    Guides have a colour — cyan by default, ten to pick from, and the choice is remembered between sessions.

  • infra

    The save format moved again, for the second time in a week. As before, files written by every previous version still load. A guard that would have silently dropped last week's saves the moment this one landed was caught by a test before it shipped.

  • fix

    Two checks were repaired, both the same shape: a check that only ever ran one way reports on that one way, not on the code. The regression test guarding an old save-data bug had looked for a button that got renamed — it found nothing and timed out instead of checking anything, dark for about eighteen releases. And a new engine test was never registered as a build target, so it would not compile in the configuration the push check uses. Both fixed, and the release checks are now a script rather than a list somebody has to remember.

v8.4014 August 2026

Text boxes resize, and the words rearrange to fit

  • feature

    Six handles — four corners and two sides — make a text box wider or narrower, and the text re-wraps inside it at the same size. For anyone who writes more than a few words: set the column you want and keep typing. The font-size handle is still there on its own stem, and still the only thing that changes the size.

  • rust

    Widen a box you narrowed earlier and the text flows back out. That works because the app stores what you typed, not where the lines happened to break — the breaks are recomputed by the engine every time, never baked in.

  • infra

    Adding this changed the format the app saves edits in. The change was built so files written by every previous version still load, so nobody loses the undo history attached to their photos — proven by tests that fail if that promise is ever broken.

v8.3914 August 2026

The Layers panel matches every other panel

  • ui

    The mask actions were built as toggle-style buttons that could never light up, so they read as disabled. They are the same one-shot tiles the rest of the app uses now, the section divider matches, and the layer dropdown gets the label its neighbours have.

  • fix

    That pass turned up something worse: two mask controls moved into the Layers tool yesterday belong to the brush, and the app switches mask painting off the moment you leave the brush — so on that panel they could never appear at all. Hide/Reveal and Done now live on the Paint panel, where the strokes actually happen; Layers keeps Paint mask, Invert, Apply and Remove.

v8.3814 August 2026

Layers become a targeted thing

  • feature

    The move/resize tab is now the Layers tool: pick a layer from a dropdown and everything below — Move, Resize, the mask controls — acts on that one layer. Verified at the engine level: the dropdown's value is the engine's active layer, both directions.

  • ui

    The mask buttons used to repeat on every row of the Review panel's layer list. They now exist once, in the tool, acting on the selected layer; rows show a small badge when a layer carries a mask, and a gear next to the layer count routes to the tool.

  • infra

    Before building any of it: does painting actually land on the layer you selected? Proven with a hidden layer — a stroke vanishes with its layer and returns with it, and the engine's per-layer pixel counts agree. Already true; now the UI shows the selection.

v8.3714 August 2026

Two handles that do what they look like they do

  • ui

    The text box had eight handles and every one changed the font size. Now one does — a square on a stem, left of the box. Corners are reserved for resizing the box itself, which needs text reflow the model can't store yet; that work is scoped and filed rather than faked.

  • fix

    The layer resize box drew itself 26 pixels inside the canvas while the engine believed it was the full canvas — a 2-pixel nudge snapped the whole layer 26 pixels inward. The engine now decides the rectangle: the tightest box around the layer's actual pixels, and the overlay draws exactly that. Dragging scales your content, not the invisible padding around it.

  • ui

    The move/resize tab wears the layers icon.

v8.3613 August 2026

The refresh fix, finished

  • fix

    v8.35 fixed the save; the loss survived on the restore side. On reload the app preferred replaying its edit log over the saved pixels — and clone stamp, emoji and Magic Eraser were never in the log. The app now counts: every edit deepens undo, only recorded edits grow the log, and a mismatch means the saved pixels carry the reload.

  • fix

    The detection could lose a race to a save already mid-write, which then marked the discredited log clean behind its back. Closed on both sides of the write and mutation-tested — six of six deliberately broken variants caught.

  • rust

    Reopening a photo showed undo as unavailable even when history was right there: the depth counter still counted the artboard as a disqualifying layer, a rule the recorder itself dropped back in the artboard rework. Undo depth now survives the reload.

  • infra

    Verified the way it failed: paint, stamp, refresh inside one second — both strokes present, undo live, on both engine modes. The bug predates the background engine; it just finally got tested this hard.

v8.3513 August 2026

Clone stamp, emoji and Magic Eraser survive a quick refresh

  • fix

    Paint and pens replay from the edit log on reload; clone stamp, emoji and Magic Eraser exist only as pixels, saved by an autosave that waited 2.5 seconds. Refresh before it fired and the work was gone — the old last-chance save at page close silently stopped working on the background thread. Reproduced: stamp, refresh, stroke gone.

  • fix

    The app now notices when a document holds edits the log does not cover and saves 300 milliseconds after the stroke instead. Demanding coverage matters: the first attempt checked whether the log was recording, and a log can be recording and still not contain the stamp.

  • infra

    The real fix — recording these tools in the log so they replay like paint — is filed with a number.

v8.3413 August 2026

The brush follows the cursor. Actually, this time

  • fix

    The morning's fix helped the wrong bottleneck. The real one: the old blocking brush had accidental flow control — the browser delivered pointer positions only as fast as the engine could absorb them. The background engine never blocks, so a real mouse's hundreds of positions per second became queued work: a 1.4-second stroke measured 17 seconds of ink lag.

  • fix

    Flow control is now explicit: one paint operation in flight, the newest position replaces any waiting one, one screen refresh per frame. Same stroke after: 0.26 seconds behind at release, level with the cursor during. No detail lost — the engine draws the segment between landed points, the same input the blocking version always fed it. Clone stamp had the same flaw, same fix.

  • infra

    Why every gate missed it: test automation moves a polite 25 times a second; a hand does not. Hot-path checks here now drive events at hardware rates.

v8.3313 August 2026

The brush follows the cursor again when signed in

  • fix

    Signed-in accounts autosave to the cloud 2.5 seconds after each edit, and that autosave reads the entire document — 29.5 MB with strokes on it. On the background thread that answer blocked the queue brush strokes travel through, right as the next stroke began: a call that takes a third of a millisecond measured at 407 ms stuck behind it. Autosave now waits until the pointer is up. The data is no fresher mid-stroke, so waiting costs nothing.

  • perf

    Measured while chasing it: the background-thread brush is smoother than the old one ever was — a clean 60 fps logged out, against about 27 before.

  • infra

    Also: three places still described the background engine as switched off and waiting. Accurate for eighteen releases, wrong the morning the default flipped. They now say what is true.

v8.3213 August 2026

Heavy operations no longer freeze the interface

  • perf

    The engine now runs on a background thread by default, and draws the photo from there. Sharpening a large photo used to lock the page for about 130 milliseconds; it now blocks the interface for zero. The work takes as long as it ever did — the app just stays responsive while it happens.

  • rust

    Eighteen releases of groundwork shipped dark before this switch was thrown: every engine read made safe to wait for, related answers bundled so they cannot describe two different moments, and the canvas moved to the background thread so drawing never crosses back.

  • infra

    The escape hatch: set ih_engine_worker to 0 and reload to put the engine back on the main thread. Takes effect on the next load, like every kill switch here.

v8.3113 August 2026

The ledger, closed out

  • infra

    No app code changed. The background-engine project's decision record still described finished work as mid-flight; its table now says what happened, with the numbers that mattered — about 130 ms of interface blocking down to zero.

  • perf

    One number had never been measured: the first heavy operation of a fresh session, on the theory a background thread starts cold. It does not — 231 ms against 206, inside each other's ordinary variation. Loading the photo warms it as a side effect.

  • infra

    The one remaining decision — on by default — now has its full pre-flight written down, so making it is a twenty-minute job. It stays off today.

v8.3013 August 2026

The off switch took the app with it

  • fix

    The switch that turns the background engine off was documented as working immediately. It did not: flipping it while a photo was open closed the whole editor — not a blank canvas, an empty page.

  • fix

    The drawing code asked the setting where to draw instead of asking where the photo actually was. It now asks where the photo is, which is a question that cannot be answered wrongly.

  • infra

    The switch takes effect the next time the app loads, like every other switch here. Flipping it mid-session is now harmless. Three comments that promised more have been corrected, and a test fails if that promise creeps back.

  • infra

    Nine internal checks resolved file paths relative to wherever the test runner was started, so from the wrong place they read no files and passed while checking nothing. Fixed and verified by planting a real violation.

v8.2913 August 2026

Everything the background thread had never been asked to do

  • infra

    The engine can run on a background thread, switched off by default. Last release was the first time it had ever been driven end to end, and it found two faults in one sitting — so this release drove the rest of the app that way too.

  • fix

    Every export format, saving and reloading, undo and redo twenty-two deep, layers added and duplicated and deleted and reordered and hidden, rotate, text, the pen, the Magic Eraser and the batch editor — all with the background thread doing the work. Nothing broke.

  • infra

    Partway through, reloading appeared to lose two edits. The test itself caused it by reaching past the app to the engine underneath, and the app had already said so in the console. A clean re-run restored correctly.

v8.2813 August 2026

The background thread was drawing to a canvas nobody could see

  • fix

    Opening a second photo left the canvas blank. A canvas can be handed to a background thread exactly once, and the app was building a new thread for every photo — destroying the one that held the canvas. The thread is now kept and the photo swapped inside it.

  • fix

    The undo counter, image dimensions and layer list stopped updating. The engine answers those in one bundle so they cannot disagree, and a bundle is a handle into the engine's memory, which means nothing on the other side of a thread. Bundles are now unpacked before they cross.

  • perf

    Measured: sharpening a 4.3-megapixel photo blocks the main thread for 119 ms today and 0 ms on the background thread. Longest gap between frames drops from 191 ms to 25 ms — one dropped frame instead of eleven.

  • infra

    The background engine still ships switched off. Two faults this size, found the first time it was driven end to end in a browser, are a reason to let it sit before turning it on.

v8.2712 August 2026

A bug that was never a bug

  • fix

    For a week the app appeared to hang on “Loading your workspace…” whenever it was opened for testing, and it was blamed in turn on signing in, on the cloud sync, and on how many photos were stored. All three were wrong. A browser gives an inactive tab no animation frames at all — none, not merely fewer; measured here as zero in nearly four seconds. Every animation runs on those frames, so in a tab nobody is looking at they all stop partway through.

  • fix

    The startup screen begins fading out and stops fading, so it sits there forever. The tool sidebar begins sliding in and stops off the left edge of the screen, which is why clicking a tool appeared to do nothing — the button was not where it looked. Instrumenting every step of startup showed it finishing in about two seconds. Nothing was hanging; a picture had frozen over the top of a working editor.

  • infra

    Also: when the background thread declines to draw — because the canvas it was given has been replaced — it now says so instead of quietly doing nothing. A silent refusal and a refusal that never happens look identical, which is exactly the confusion above, one layer down.

v8.2612 August 2026

The test that could have said no said yes

  • infra

    No code changed; this is the missing proof from the last release. Moving the engine to a background thread rests on one assumption: that when the app fires off a batch of edits, they arrive in the order they were sent. If they do not, your undo history stops matching what you did — not with an error, just quietly wrong. That assumption had never been tested, only read off the source and believed. It now holds: sixteen edits fired at once, with no waiting in between, produced a recorded history byte-for-byte identical to the same sixteen run the ordinary way.

  • fix

    The last release could not run this test at all. The history only records an edit when the picture is next redrawn, and the test never redrew it — so it was comparing an empty history against an empty history and calling that a match. The fix was one line; noticing was the work.

  • infra

    And the test was tested first. Running the same eight edits in reverse order produces a completely different fingerprint, which is what makes “identical” mean anything — a measurement that cannot tell two orders apart cannot prove the order was kept. One check is still outstanding before the switch can be turned on: what happens when the canvas is replaced mid-session.

v8.2512 August 2026

The engine ran on a background thread, and drew the photo from there

  • perf

    Off by default, but it ran: the engine worked on a background thread and painted the picture from there. The proof is structural rather than a claim — once the canvas is handed over, the main thread physically cannot draw to it, the browser refuses. So a photo appearing on screen can only have been painted by the other thread. It appears.

  • fix

    Turning it on immediately found two bugs, both of the kind that show nothing. The canvas is created when the editor opens, but the background thread is not started until you open a photo — so handing the canvas over at the moment it appeared never happened, and the other thread drew into nowhere. It now waits until there is somewhere to send it.

  • fix

    The second is worse and older. When the canvas is replaced — which happens crossing into Batch — the background thread kept a handle to the old one as well, and would have carried on painting into a canvas that no longer exists. Nothing thrown, nothing logged, just a blank picture from an ordinary action. That is the exact failure an earlier release built a safety net for, and the net had a hole: its check was satisfied by the file merely mentioning the rule. Both are fixed.

  • infra

    What is not yet proven, stated plainly: the most important test — that the undo history comes out byte-for-byte identical on both threads — did not run. The operations reachable from a console are not the ones the history records, so the comparison would have been empty against empty, which proves nothing. The picture comparison did pass, on an eighteen-step sequence of drawing, undoing and redoing. The switch stays off until the missing test has actually run.

v8.2412 August 2026

The engine is created in one place now, not five

  • infra

    Nothing you can see. Opening a photo, pasting one, getting one back from an AI tool, restoring one from its edit history — each of those built the engine itself, and each would have needed its own answer for “what if the engine lives on another thread”. They now all go through one function that makes that decision once. The switch is still off and the local path does exactly what it did before, verified by drawing on a photo and watching the stroke land.

  • fix

    The code for talking to a background thread had never actually been part of the app — written, tested against a stand-in, then removed by the bundler because nothing imported it. The moment something did, the build failed outright: the app packages background workers in a format that cannot handle a worker which loads code on demand, and the engine worker does exactly that. It had never been able to build. Now it can.

  • fix

    Found a guard with no test behind it. Moving the engine somewhere else means shutting down the previous one, because the memory it holds is never given back — a forgotten instance makes the tab permanently heavier for the rest of the session. The shutdown was written correctly and nothing checked it, which turned up by deliberately breaking it and watching every test still pass. There are tests now.

v8.2312 August 2026

Three reasons the next step could not work the way it was planned

  • infra

    Nothing you can see. The next step of moving the engine to a background thread was written up as a small one — swap the body of a single function, change nothing else. Checking it against the code first found three reasons that cannot work.

  • fix

    The engine gets built in the wrong place. The function meant to be the swap point receives an engine already created and loaded with your photo on the main thread, and a background thread cannot adopt that — it would have to build a second one, which means two copies of your undo history, the exact thing this change exists to avoid.

  • fix

    A stand-in that forwards everything says yes to everything. Four features check whether the engine can do something by asking whether the method exists, and a forwarding stand-in answers yes on every build, including ones that genuinely cannot. Fixed, and the fix is better than the original: the background thread now reports what it can actually do, so the list can never drift out of date — the same drift that caused the wrong numbers two releases ago.

  • perf

    And the fast path for drawing cannot cross a thread boundary at all. Drawing works by pointing directly at the engine's memory rather than copying it, and a pointer into one thread's memory means nothing on another. So moving the canvas across has to happen at the same time as everything else rather than afterwards.

v8.2212 August 2026

Every read from the engine now waits for the answer

  • fix

    Thirty-six more places converted — the undo log's save and restore paths, the tile flush, the Magic Eraser's removal, the Remove Object action. Nothing you can click behaves differently. What changes is that when the engine moves to a background thread none of these will quietly start lying, and a surprising number of them would have: un-awaited, a check like “is this log still describing the document?” stops being able to answer no, because the unfinished answer itself counts as a yes.

  • fix

    One of them caught me out, which is the useful part. The log's trustworthiness check has two callers and I updated one. The type checker was happy — asking about an unfinished answer is legal code — and the migration's own counter was happy too, because it counts calls into the engine and this was a call into a helper. An ordinary persistence test caught it instead, asserting that a broken log never gets written to disk. The dangerous conversions are the ones where nothing complains.

  • perf

    The last of the mouse-move handlers are converted, so that tally is at zero for the first time. Verified the way it has to be: a paint drag leaves one continuous stroke, which is only true if every dab landed and none was discarded for arriving late.

v8.2112 August 2026

The measuring tape could not see 31 of the engine's own methods

  • fix

    Nothing about the app changed; the number everyone has been steering by did. The tool that counts how much of the engine migration is left reads the engine's method list out of a hand-maintained file sitting next to the real generated one — and that file had drifted, listing 249 methods where the engine has 280. A method missing from it is not counted wrong, it is invisible. Thirty-six call sites had never been counted in any release. The real figure is 29, not 5.

  • infra

    Fourth time in this project a number has turned out to be a statement about the instrument rather than the code, and the fix is the same each time: read from the source of truth instead of a copy, and say so loudly when the two disagree.

  • perf

    Eleven of the mouse-move sites are converted — the lasso's live wire, the paintbrush, the blur brush, the eyedropper magnifier, the text hover, the Magic Eraser overlay, the selection preview and the histogram. Each needed a decision rather than a keystroke, because two mouse-moves can now be in flight at once: a preview that arrives late should be thrown away, and a paint dab that arrives late absolutely should not.

  • ui

    The Trail Log's August card was rendering empty. It ranks a month's highlights by how many features each release carried, and August carried none — fifty-nine releases of engine work, not one of them a feature. A busy month looked like a dead one. It now counts everything a release shipped when a month has no features at all.

v8.2012 August 2026

The counter said done, and it was not counting everything

  • infra

    Nothing changed in the app. The engine has been moving to a background thread in stages since June, and the next stage was recorded as ready to start. It is not — this release is the correction and the evidence for it.

  • fix

    The migration tracks how many places still read from the engine in a way that would break on a background thread, and that counter hit its floor last release. It was never counting one category: the calls that run on every mouse-move, deliberately left until last. Fifteen of those eighteen still read the old way, and all fifteen would fail quietly — the lasso's “am I drawing?” check unable to say no, the eyedropper reading colour out of nothing, the brush thinking every mouse-move changed the picture.

  • perf

    The obvious worry — that waiting on a background thread every mouse-move would make drawing laggy — was measured back at the start and is not true. The round trip is a tenth of a millisecond, about half a percent of one frame. So this is work, not a redesign. The real catch is different: a mouse-move handler that waits can be handed the next mouse-move before it has finished the last one, which is the same trap the pen tool hit two releases ago.

v8.1912 August 2026

The pen waits for the answer, and still draws on the first frame

  • fix

    Finishing a pen path now waits for the engine to confirm it before keeping that path selected. It is the last of ninety-one changes of this kind, and the one left until the end, because the answer here is the whole point rather than a formality — the id that comes back is what keeps the colour and Background controls pointing at the thing you just drew.

  • fix

    Clicking the canvas does not wait. It drops the anchor immediately, and if it turns out you clicked a path that already existed, the anchor is thrown away and the path opens instead. Nothing is committed until you finish, so there is nothing to undo. Click-drag on that first anchor still pulls the curve handles out of it on the same frame, which is what doing this the obvious way would have cost.

  • fix

    Holding Enter now finishes a path once. Because finishing waits, it could be asked to finish twice before it had finished once — two identical paths stacked exactly on top of each other, one undo step each, looking like one path until you delete it and the other is still there.

  • infra

    That closes the stage: every engine read that hands a value back now waits for it, except five that stop crossing the boundary at all when the canvas moves into the worker. A type on the way in had been quietly declaring it returned nothing, which would have let the whole change pass its checks while paths stopped staying selected.

  • infra

    One thing this does not settle, written down rather than quietly counted as passed: leaving the pen mid-path commits it on the way out, and that still works — but the engine answers instantly today, so the gap this is supposed to be risky in does not exist yet. It appears when the engine moves to a background thread. The reasoning for why it stays safe is recorded; running it is the next step's job.

v8.1812 August 2026

Four things the code said about itself that were no longer true

  • fix

    Four places in the codebase described how far the engine-in-a-worker migration had got, and all four had drifted. One had been wrong for ten releases while the number it quoted fell by 89. The one that matters is the description under the “Engine in a Worker” switch in Features, because that is the one a person actually reads. They now all say the same thing, and it is the true one.

  • fix

    A fifth called itself “the last truthy trap in the codebase” when it shipped — and was disproved the next day by another one hiding a line below its own call. It now records that instead: “none left” is a statement about the tool doing the looking, not about the code.

  • infra

    The pen tool's last two engine calls are now designed rather than just deferred. They are the only thing standing between this migration and the end of its current stage, and they are not the “make it wait for the answer” job the other 89 were — so the design, the eight gestures that have to be tested, and the four ways to prove those tests actually work are written down before anyone starts.

v8.1712 August 2026

The readout that waits for the answer

  • infra

    The readout that mirrors the open document — image size, zoom, the undo and redo counts, the layer list — now waits for the engine's answer. It runs after almost every edit you make, which makes it the most-travelled call of the lot, and the last one of these that was a straightforward conversion.

  • fix

    It needed a guard. Switching photos puts the old document down, but the old engine is still alive underneath, so a reply already on its way still arrives — carrying the previous photo's size and undo history, landing on top of the photo you just opened. Nothing would break and nothing would be logged; the numbers beside your new photo would simply be the old photo's until your next edit corrected them. Every reply is now checked against the document that asked for it, and dropped if you have moved on.

  • infra

    The remaining two are not this kind of work. Both belong to the pen tool: one hands back the id of the path you just finished so it stays selected, the other decides on mousedown whether you are re-opening an existing path or starting a new one. Neither can simply wait — the overlay has to learn a waiting state first, and one of them runs while the overlay is being torn down, where waiting is not available at all.

v8.1611 August 2026

The check that was hiding one line up

  • infra

    Four more of the app shell's engine calls now wait for the answer: placing an object into a grid cell, saving the working copy of a photo, the canvas checksum in the info panel, and the “photo only” share export.

  • fix

    The placement one had a check that would have quietly stopped checking. It asks the engine “did that actually move anything?” and skips the redraw when the answer is no — but the question sits a line above the answer, which is enough to hide it from the tool that finds this class of bug. Found by hand and fixed; the tool's report of “none left” turns out to describe the tool, not the code.

  • infra

    Two calls in this file are deliberately not done yet. Both belong to the pen tool, and converting them means reworking how the pen overlay finishes a path — including what happens when you switch away mid-draw, which cannot wait for an answer at all. That is its own piece of work rather than the tail of this one.

v8.1511 August 2026

The last check that could stop checking

  • infra

    Exporting a layered .ora file now waits for the engine's answer — the last six of these conversions, and the last of a particular kind of bug with them.

  • fix

    That kind: a check written as “if the engine says yes”. Left half-converted it stops being a check at all and always says yes. This one decided whether your live text and shapes had been baked into pixels for the export — and baking them clears your redo history, so the app tells you when it happens. Broken, it would have told you that on every .ora export, including ones where nothing was baked. There are now none of these left anywhere in the app.

  • infra

    What this release does not do is make the export atomic. The file is still assembled across several moments, and this change slightly widens that window rather than closing it — closing it needs the export to notice the picture changed underneath it and refuse, which was decided four releases ago and is not built yet. Checked by exporting a real two-layer file and taking the archive apart: both layers present, and the dimensions recorded inside it match the picture.

v8.1411 August 2026

The measurement arrives before the drawing does

  • infra

    The last piece of the text-measuring work, and the one that was written down as the plan two weeks ago and never built. Laying out the text box needs to know how wide your text is and where its ink starts — and that happens while the screen is being drawn, which is the one place that cannot wait for an answer. So it now asks just before, off to the side, and the drawing reads what was already worked out.

  • fix

    It was built last on purpose. Four other places also asked for those measurements and could not cope with “don't know” — the batch stamp in particular refuses and skips a photo rather than guess. Removing the old path before those four had moved would have broken them silently, so they moved first, in the two releases before this one.

v8.1311 August 2026

The batch stamp refuses properly now

  • infra

    The batch stamp — logo and text, applied across a whole gallery in one pass — now waits for the engine's answer.

  • fix

    This was the riskiest piece left. The text stamp measures your text in the engine so it can corner-align it, and it is written to refuse and skip a photo rather than guess, because guessing would put the text in the wrong corner of every image at once. That refusal was about to stop working: the check it used to spot a missing measurement would have started passing in exactly the situation it was written for, and the whole batch would have been stamped at a nonsense position instead of being skipped and reported. Rather than teach it to handle that, the measurement now simply waits — so there is nothing to miss.

  • perf

    Checked by running a real four-photo batch and reading back every coordinate the engine was handed: all finite, none nonsense — and five stamps of the same text cost a single measurement, because the result is remembered.

v8.1211 August 2026

Text waits for the answer, and doesn't drift

  • infra

    The text tool now waits for the engine's answer — adding text, re-opening it to edit, clicking an existing text, and refreshing the list after an undo.

  • fix

    Two of those calls measure where a letter's ink actually starts inside its tile, which is what stops committed text landing a few pixels off from the preview you typed against. They now wait properly rather than accepting “don't know” for an answer — they never needed to guess, because unlike the drawing code they aren't running while the screen is being painted.

  • fix

    Checked by round-trip rather than by eye: place a text, click it to re-open it, then commit it again unchanged. It must not move by a single pixel, and it doesn't — placing and re-opening are exact inverses of each other, so either one being wrong would show up immediately.

v8.1111 August 2026

The layered-export decision, measured

  • infra

    A decision open since the background-thread work started: whether exporting a layered .ora file should ask the engine for everything in one go, so the file can't be assembled from two different moments. Measured, the answer is no — reading the layers one at a time peaks at about 5 MB however many layers there are, while asking for them all at once holds 11 MB for two layers and 27 MB for five, climbing with no ceiling. The browser never gives that memory back, and unlimited layers is a paid feature, so the worst case lands on someone paying.

  • fix

    Instead the export will check whether the document changed underneath it and refuse to write a file it can no longer vouch for. A failed export you can retry beats a corrupt one you keep. Two problems were also separated that had been filed as one: baking annotations into pixels before an export cannot tear the file, because it finishes first — but it does clear your redo history as a side effect of exporting, which is worth fixing on its own.

v8.1011 August 2026

A safety claim nobody had checked

  • infra

    One of the remaining pieces of the background-thread migration was documented as safe on the strength of a claim nobody had tested: that every place asking for text measurements already copes with not getting an answer. Two of the six do not — the batch text stamp is written to refuse rather than guess, on purpose, because guessing would corner-align a whole gallery of photos to the wrong place.

  • fix

    Worse, it would not actually refuse. The check it uses to spot a missing answer stops working in exactly the situation it was written for, and a whole batch would be stamped at a nonsense position instead of being skipped and reported. Nothing changed in the app: the claim is corrected where it was written, and the corrected plan recorded — the two places that genuinely cannot wait keep their fallback, and the four that can wait should simply wait.

v8.911 August 2026

Shapes, arrows and pins wait for the answer

  • infra

    Seven more engine calls converted — but the work was the wiring between them rather than the calls themselves. Committing a shape and reloading the shape list are each triggered from four or five places (Enter, clicking away, switching tools, undo), and two of those had to keep their order: clicking an existing shape re-selects it only because the pending edit is committed first, and the check for “what did I click” reads the list that commit writes.

  • fix

    The clearest proof it still works is the pins: drop two and they come out numbered 1 and 2. The second one only knows it is the second because it reads the first back from the engine — so that sequence is the conversion working, observed rather than argued.

v8.811 August 2026

The whole Select tool waits for the answer

  • infra

    Wand, edge-aware, colour range, magnetic lasso and both marquees — thirteen engine calls in one file, the largest batch of this run, and five of them were checks written as “if the engine says yes”.

  • fix

    Every mode was then driven in a real browser, and each left the right entry in the history: Magic Wand, Edge Select, Colour Range, Magnetic Lasso, Marquee, Ellipse Marquee, Select All, Delete Selection, Selection to Layer.

  • perf

    The live wire that follows your cursor while a lasso is open was deliberately left alone. It runs on every mouse-move, and it is the one place in this file where waiting for an answer would cost a frame.

v8.711 August 2026

The check that runs on every tool switch

  • infra

    The two AI buttons and the layer-resize box now wait for the engine's answer — four more calls converted for the background-thread move.

  • fix

    Both AI buttons were then checked on a real signed-in account, with the upload step blocked so no credit was spent: each hands the model a full-size PNG of the current picture, and Remove Object opens its window with the image already loaded — which is the part that breaks first if the wait is left out.

  • fix

    Dropping a resize box and pressing Enter is a “commit”, and the same commit runs quietly every time you switch tools, on the understanding that it does nothing when no box is open. That “does nothing” is a check, and the check is the call converted here — left half-done it would have stopped saying no, and every tool switch would have added a step to your undo history for a box that was never there. Checked in the browser: six tool switches with nothing pending reached the engine zero times, and a real box still commits on the first Enter.

v8.611 August 2026

The check that would have stopped checking

  • infra

    The last three files with a single engine call left in them — the export dialog's size label, the eyedropper and the clone stamp — now wait for the engine's answer. All three needed the surrounding code rearranged rather than one keyword added.

  • fix

    The clone stamp's call is the check for whether a source point has been set yet, and that check sits inside an `if`. Converted carelessly it would have stopped rejecting anything and the stamp would have painted from wherever the engine happened to be, with nothing thrown and nothing logged. Verified in the browser both ways round: a click with no source still does nothing, and a source-then-stamp still leaves a stroke.

v8.511 August 2026

Four more calls wait for the answer

  • infra

    Copying a selection to the clipboard, both of the paths that save an edited photo, and the OCR button that pulls text out of an image now wait for the engine's answer before using it.

  • infra

    This batch was picked for being dull. Each of the four already sat inside code that waits for something else, so each was a single keyword. The three remaining files with one call left in them are not like that — every one needs the surrounding code rearranged first — so they are queued as their own batch instead.

v8.411 August 2026

The histogram was doing a full pass every frame

  • infra

    Copy-to-clipboard and the single-photo download now wait for the engine's answer before using it.

  • perf

    The fourth call in that file turned out not to be ordinary work at all: the histogram under the image is redrawn from a full pass over the whole picture — eleven million samples — and the code that asks for it runs once per animation frame until the bars settle. It is now filed with the other frame-rate work rather than queued for a change that would have made it wait for a round trip each time.

v8.310 August 2026

The export paths wait for the answer

  • infra

    Saving a photo, sharing a link and building the .zip of a whole gallery now wait for the engine's answer before using it — six more calls converted for the background-thread move.

  • fix

    One of them could not simply wait: the thumbnail helper checked its result for emptiness inside a hand-rolled promise, and once the thing it was checking became a promise itself, that check would have quietly stopped working. Rewritten so the check still fires. Verified against the real thing rather than only by tests — PNG, JPEG and WebP each came back as genuinely that format, and a twelve-photo .zip built correctly with an edited photo among them.

v8.210 August 2026

Four calls that were on the redraw path all along

  • infra

    Four more engine calls turned out to sit on the path that repaints the canvas — not because of anything in the functions themselves, but because the redraw reaches into two other files to call them. Their names give nothing away, and the check that finds this class of problem only ever looks at a function's own name, never at who calls it from somewhere else. Left in the ordinary queue, the next batch would have made the redraw wait for a round trip on every frame.

v8.110 August 2026

A source file that no search could see

  • fix

    One source file contained two invisible NUL characters, used as a separator inside a cache key. That is enough to make every search tool classify the file as binary and skip it silently — so searching the project for anything defined in it returned a confident zero. It surfaced by contradiction: the history said an identifier had been added and never removed, while the search said it did not exist. The characters are now written as an escape: identical behaviour, and the file is searchable again.

  • infra

    Also three more engine calls converted for the background-thread move — flip-horizontal, flip-vertical and copy-region now wait for the engine's answer before using it. Checked in a browser: flip both ways, then three undos, landing back on a pixel-for-pixel identical image.

v8.010 August 2026

The bookkeeping, corrected the other way

  • infra

    Yesterday's release fixed a rule that was filing live drawing code as ordinary work. This one retires that rule entirely, because it was wrong far more often in the opposite direction: of the 21 calls it alone marked as “drawing hot path”, 19 were ordinary once-per-click actions — commit, cancel, apply-crop, drop-a-pin, mouse-down, mouse-up. They had been set aside to be done last, which meant nobody was looking at them.

  • fix

    So the list of remaining work goes up, not down. That is the honest number, and the first one the rest of this migration can actually be planned against. Two calls really do belong on the fast path despite ordinary-sounding names, and they are now listed individually with the reason, rather than caught by a pattern that happened to match.

v7.9910 August 2026

A checklist that could never reach zero

  • infra

    The checklist tracking the background-thread migration had a target it could never reach: five of the calls it was counting are ones the plan says must not be changed, because a later stage removes that code path entirely. The checklist wanted them at zero; the plan forbade touching them.

  • fix

    Left alone that doesn't just stall, it pushes the wrong way — sooner or later someone grinds the number down, meets those five, and “finishes the job” by changing the one piece of code that repaints the canvas on every frame. They are now named with the reason, and changing them makes the test suite fail loudly instead of looking like progress.

v7.9810 August 2026

Undo waits for the answer before it repaints

  • infra

    Undo, redo, jump-to-step and delete-step now wait for the engine's answer before repainting. Four of the five were the same shape — “if the engine says it undid something, repaint” — and once the engine answers from a background thread that question stops being answerable on the spot. Left alone, every Ctrl+Z would have repainted and re-synced whether or not anything was actually undone.

  • fix

    Checked in a browser rather than only by tests, because a broken undo button is not something a type checker can see. Undo and redo work from both the toolbar and the keyboard, and pressing Ctrl+Z with nothing left to undo correctly does nothing at all.

v7.9710 August 2026

Correcting the instrument, not the app

  • infra

    The tool that decides which engine calls sit on a drawing hot path had six of them filed as ordinary work — including the blur brush's drag, the eyedropper's magnifier and the text tool's hover highlight. The next batch of background-thread work would have made those wait for a round trip on every mouse movement, which is a dropped frame in exactly the places you would notice one. Nothing in the app changed; the list it was working from did.

  • fix

    The old check looked for keywords in the six lines around a call, which meant it could not see which handler the call was inside. It had split a single mouse-move handler across two categories — twice — and in one case the only thing marking a line as hot was the word “preview” appearing inside a nearby variable name. It now reads the enclosing function's name instead.

v7.9610 August 2026

The editor stopped answering a question nobody asked

  • perf

    Every time the editor refreshed itself — after a brush stroke, an undo, a layer change — it rebuilt the entire image from scratch and checked every single pixel, to work out whether the picture had any transparency in it. On a 1385x2068 photo that was about 30 ms, and it was the whole cost of the refresh: the other ten things it collects took no measurable time at all. Nothing was using the answer. The canvas checkerboard used to ask and stopped in June when it became always-on; the question kept being asked anyway. Refreshing now costs nothing measurable.

  • rust

    The engine can still answer the question — it was taken off the path that runs after every edit, not deleted. If something needs it cheaply one day, that is the point to design it properly, with a real user of the answer to say what correct means.

v7.9510 August 2026

Saving a layered project describes one document, not several

  • rust

    Exporting a layered .ora project asked the engine for the canvas size and the layer list as four separate questions, and the routine that flattens each layer first asked three more. Both sets describe one document and get written into one file, so once the engine moves to a background thread, a resize arriving mid-question could put a canvas size from before it next to a layer list from after — a broken .ora saved to disk with no error at the time. Each set is one question now.

  • perf

    The new engine call deliberately does not reuse the one the editor already had, which would have needed no new code: that one works out whether the image has transparency by rebuilding the entire picture and checking every pixel. Fine when the editor needs it, pure waste for a caller that wants a canvas size and a list of layer names.

v7.9410 August 2026

One question instead of two, where the gap would have mattered

  • rust

    Clicking a pen path to re-edit it used to ask the engine two questions: which path is under this point, then give me every path so I can look that one up. Two questions with a gap between them. The gap is empty today; once the engine moves to a background thread it stops being, and a path deleted in that gap would make the click do nothing at all — no error, no message, just a click that does not work. It is one question now, and the engine keeps the rule the app used to apply itself: a rectangle drawn over a pen path still means “no pen path here” rather than reaching through it.

  • infra

    A second place that looked like exactly the same problem turned out not to be, and was deliberately left alone: clicking a text annotation commits whatever you were typing in between its two reads, so the gap there is intentional rather than accidental. Converting it would have changed what happens when you empty a text box and click where it was.

v7.9310 August 2026

The one number the background-thread plan assumed, measured

  • perf

    The plan to move the engine onto a background thread rested on a number nobody had measured: what it costs to get finished pixels onto the screen from over there. It is 22 ms on a 3.1 megapixel photo, against 23 ms for the main thread doing the same work — no penalty, so the reason for picking this approach over the alternative holds up. Getting the number needed a real engine running inside a background thread that owns the canvas; two earlier experiments had each done one half of that and never both at once.

  • infra

    Two more things fell out of the run. The first operation after switching costs about 1.8x the ones after it, so the switch has to warm the thread up before handing it work. And the old thread has to be shut down rather than left idle, because the memory it holds is never given back. Nothing changes for you — the mode is still off.

v7.929 August 2026

An off switch that works while you are using it

  • infra

    More groundwork for moving the engine onto a background thread. The switch that turns that mode off now takes effect mid-session instead of only on reload: it swaps in a fresh drawing surface rather than trying to reclaim one it can no longer draw to. Nothing changes for you — the mode is still off.

v7.919 August 2026

Knowing which canvas is the real one

  • infra

    Groundwork for moving the engine onto a background thread. The app now tracks which drawing surface is live and gives each one a number, so a later change can refuse work aimed at a surface that has already been replaced. It matters because switching in and out of Batch replaces the canvas underneath — and a background thread that kept painting into the old one would show a blank screen with no error at all.

v7.909 August 2026

One question, one answer

  • perf

    Creating a share link with “Photo only” set was building the whole image twice to work out two numbers. It builds it once now, and skips a step it never needed — 40 ms down to 17 ms on a 1385×2068 photo.

  • infra

    The eleven values the editor reads to redraw itself — size, zoom, layers, undo history, export quality — now come back in a single call instead of eleven, so they can never describe two different moments. More groundwork for moving the engine onto a background thread.

v7.899 August 2026

Cover for the Download All fix

  • infra

    The v7.81 fix — Download All shipping your original files instead of your edits — now has a regression test. That bug only appeared after a page reload, which is why it lasted two months and why the fix went out without one. Nothing changes for you; the fix is simply harder to lose now.

v7.889 August 2026

Photo-only exports stop doing the work three times

  • perf

    Exporting with the canvas background left out was rebuilding the whole image three times to answer one question — once for the pixels, once for the width, once for the height. It does it once now: 69 ms down to 20 ms on a 1385×2068 photo, with the same bytes coming out. Copying to the clipboard, saving a single photo and batch export all took the slow path.

  • infra

    Nine places that read an image and the dimensions describing it now read both in one call, so nothing can change in between. More of the groundwork for moving the engine onto a background thread.

v7.879 August 2026

Notes for the next hands on the migration

  • infra

    The background-thread migration's design record now lists which files have been checked and what turned up, so the next session starts from findings rather than re-deriving them. Two more places were caught where several reads have to describe the same moment. Documentation only.

v7.869 August 2026

A mis-filed hot path in the migration tracker

  • infra

    The tool tracking the background-thread migration was treating the lasso's live preview as ordinary work. It recomputes on every mouse move, where waiting on a background thread would cost a frame, so it belongs with the cases handled last and separately. Nothing in the app changed.

v7.859 August 2026

Layer operations get ready for the background thread

  • infra

    Add, remove, rename, reorder, merge, show and hide, opacity and masks are all ready for an engine that answers over a message queue instead of instantly. Behaviour is unchanged — every one of them was driven and the layer stack checked afterwards.

v7.849 August 2026

Saving a photo now happens in one read, not eighteen

  • rust

    Saving used to ask the engine eighteen separate questions about your photo — its canvas, its history, its layers. It now asks once. The bytes on disk are identical, verified through a save, reload and restore, but the read can no longer be interrupted halfway, which is what would have let switching photos mid-save mix one photo's canvas with another's history once the engine moves to a background thread.

v7.839 August 2026

A tidier button set and a sharper nav underline

  • ui

    The button-set image on the home page dropped its caption strip and got 34% smaller, and the two tiles that had no name — Undo and Layers — now say what they are.

  • fix

    The underline that follows your cursor across the nav looked smeared while it moved. It was a one-pixel line stretched by the graphics card; it is now drawn at its real width, so every frame is sharp.

v7.829 August 2026

The engine can hand over a whole save in one go

  • rust

    Saving a photo reads its canvas, its history and its layers as a set. The engine can now hand all of that over in a single call instead of eighteen — so once the engine moves onto a background thread, switching photos mid-save can't mix half of one photo with half of another. Nothing uses it yet; nothing visible changes.

v7.819 August 2026

Download All stops throwing your edits away

  • fix

    Edit a photo, reload the page, pick "Resume editing" — your work is there on the canvas, but downloading everything as a ZIP shipped the untouched originals instead. Every stroke and annotation, missing from the archive with nothing to show it. Downloading a single photo was always correct, and the ZIP only did this after a reload, which is why it looked random.

v7.809 August 2026

Text layout stops re-asking the engine the same question

  • perf

    Measuring where text sits used to go to the engine on every redraw. Those measurements depend only on the words, the size and the weight — nothing else — so the answer is now remembered and reused. Identical results, less work, and one more piece of groundwork for moving the engine off the main thread.

v7.798 August 2026

Groundwork for moving the engine off the main thread

  • infra

    The tool that tracks the engine-in-a-worker migration now measures how much of it has actually been done, not just how big it is — and a test pins that number so it can only go down. Nothing in the app changed.

v7.788 August 2026

A correction to the last release's own notes

  • infra

    The v7.77 write-up said the export slowdown got several times worse on large photos. It doesn't — every image is scaled to 2048px on its long edge when it opens, so the editor never holds a photo big enough for that to be true. Documentation only; nothing in the app changed.

v7.778 August 2026

Exporting stops doing work it doesn't need to

  • perf

    With exports set to "Photo only", the app was compositing the whole image twice on every redraw just to work out how big the export would be — even with the download dialog shut. It now works that out once, when you open the dialog.

  • perf

    JPEG, WebP and AVIF exports read the image from the engine instead of scraping it back off the canvas, and the encoding moves off the main thread where the browser allows it.

v7.768 August 2026

Dragging a shape no longer redraws your photo

  • perf

    The rubber band you drag out for a shape, arrow or crop box used to be painted onto the image itself — the whole canvas copied on mouse-down, copied back on every mouse-move to erase the last frame, and copied back again on release. On a 12-megapixel photo that is a lot of pixels moved to draw a rectangle. It now draws on its own transparent layer and the image underneath is never touched.

  • fix

    Copying to the clipboard stops rewriting the document. It was flattening the live image first; the copy never needed it, because text and shapes are already drawn into what gets copied.

v7.758 August 2026

The gallery grid stops reserving space it doesn't use

  • ui

    v7.74 stopped the gallery tiles stretching, which cleared a slab of bare checkerboard under every thumbnail. The row heights were untouched, so the slab came back as empty background — about 117px per row, worst at tablet width. Both halves are fixed.

  • infra

    The parking lot — the running list of known-but-deferred problems — is now part of the repo instead of one machine's disk.

v7.748 August 2026

Emptying the gallery gets your storage back

  • fix

    Delete All removed the photos and left every original file behind — 108.6 MiB stranded in one click. Deleting photos one at a time was always correct; only the bulk path skipped the cleanup.

  • ui

    Gallery tiles stopped stretching to fill their row. A short photo could sit above 188px of bare checkerboard. It still shows through genuinely transparent pixels, which is what it is for.

  • rust

    Two decisions moved out of JavaScript and into the engine: flattening text and shapes now reports whether it did anything, and a whole-image blur works out its own geometry.

  • infra

    Groundwork for running the image engine off the main thread — the message protocol, the queue that keeps edits in order, and a test that fails if anything reaches the engine outside it. Off by default, nothing user-visible yet.

v7.737 August 2026

Downloading a whole gallery respects the setting the rest of the app already did

  • fix

    Settings has a switch for whether the backing canvas is baked into what you export. Share, Copy and the single Download all obeyed it. Downloading the whole gallery as a zip did not, so a batch export always carried the padded border.

  • fix

    It could not have obeyed it, either. The zip rebuilt each photo from a flattened image with no layers — and with no layers there is no canvas to leave out. It restores the real layer stack now.

  • infra

    That restore is one piece of code shared with the editor rather than a second copy of it. The last time this codebase kept two copies of a save routine, they drifted, and the cloud one quietly stopped saving drop shadows.

v7.726 August 2026

The gallery shows transparency on every thumbnail, not just the PNGs

  • fix

    The checkerboard behind a gallery thumbnail was switched on by the format of the file you opened. PNG got it, JPEG never did — which had nothing to do with the picture, because every thumbnail is re-encoded to WebP on import. A photo that picked up transparency from the canvas or the eraser showed none of it.

  • ui

    It is unconditional now. Nothing is needed to hide it either: the image paints over the checkerboard, so a thumbnail that fills its tile covers it completely and only the letterbox bars of a portrait or landscape shot show through.

v7.716 August 2026

The architecture page stops claiming something the app cannot do

  • fix

    The system map described the browser as "fully functional offline". It is not — there is no service worker in a shipped build, so opening the app with no connection fails. It now says what is actually true: no server in the edit path.

  • fix

    The architecture doc made the same claim about the engine. Working without an account is true; working with no network from cold was not.

v7.706 August 2026

The homepage shows the buttons, not a screenshot you cannot read

  • ui

    New image on the homepage: nine of the app's controls, each one rendered from the app's own stylesheet rather than redrawn. It replaces a screenshot of the command palette that was unreadable at the size it actually displayed.

  • ui

    The closing section is two columns now — the no-wifi mark on the left, everything you can read or click on the right.

  • ui

    A line about what the network is actually for: once the app has loaded, dropping your connection mid-edit stops nothing, because none of the work was leaving your machine.

  • fix

    A stray gap above that paragraph. The headline was stretching to 488px tall for a single line of text — a leftover flex rule from when the section was laid out sideways.

  • ui

    Clone Stamp's icon was a pair of sheets, which says duplicate rather than clone from a point. It takes the stamp mark now, and the red marker presets take a badge — matching what the feature list on the site already showed.

v7.696 August 2026

Export names the file after what is in it

  • fix

    Exporting as AVIF wrote a PNG and called it .avif. Chrome cannot encode AVIF and the browser substitutes PNG without saying so — no error, no warning — so the file you got did not match its own name. The name now comes from the bytes.

  • fix

    The format picker said the file would be saved as PNG while the performance figures beside it still promised AVIF's savings. Both numbers now describe the format that actually lands.

  • fix

    The download dialog is a second format picker, and it was still advertising AVIF as "Smallest, modern" with a button that handed over a PNG.

  • ui

    Picking PNG now says it is lossless and will be larger than the source, rather than leaving you to discover that on disk. For an app about compression, a silent surprise was the wrong default.

  • fix

    Resize Layer opened its box flush against the edge of the picture, so every handle sat on the border and nothing appeared to happen when you clicked it. The tool always worked; it just gave you nothing to grab. It opens inset now.

  • ui

    On a large display the site pinned everything to the left edge with a wide empty strip down the right. The content column is centred, on every page rather than just the homepage.

  • ui

    New screenshot on the homepage and the README. The old one was taken in June and still showed a menu two revisions out of date — the site was advertising an app that no longer existed.

  • infra

    The contribution squares counted whichever branch the generator happened to run in, so work could show up before it shipped or vanish until it merged. It reads the merged history now, and says so when it is running ahead of it.

v7.686 August 2026

Every edit shows up as an edit

  • fix

    Adding text, dragging a slider, or selecting the whole photo all changed it — and the gallery still showed it as untouched. The dot and the save had drifted apart, so the app could write a photo to disk while telling you nothing had happened to it. They ask the same question now.

  • fix

    Changing export quality recorded an undo step that put nothing back: the pixels were identical, so undo consumed the step and left the slider where it was. Quality now travels with the step, and it is undoable whether or not you press Apply.

  • fix

    While you are drawing a pen path, Ctrl+Z takes back one point instead of deleting the whole path. It used to reach past the path you were drawing and undo the last one you finished.

  • fix

    A photo that fails to reach the cloud no longer leaves anything behind. The upload happened before the record that points at it, so any failure in between stranded a file that nothing would ever reference and nothing would ever clean up. It is collected the moment it happens now.

  • perf

    An upload that gets rate-limited is retried rather than dropped. Your edits are always written to this browser first, so nothing was ever at risk — but the copy in the cloud could quietly fall behind.

  • rust

    Export quality moved into the engine so it can ride the undo history properly. The engine grew by 625 bytes and is pinned by 207 tests.

v7.675 August 2026

Photos no longer overwrite each other's edits

  • fix

    Switching away from a photo saved whatever was on the canvas at that moment, without checking the canvas still held that photo. If a switch stalled partway, the next one wrote the old photo's picture into a different photo's saved edits. Four photos ended up holding the same painted canvas, and three of them had never been touched. Saving now refuses when the canvas is holding someone else.

  • fix

    It refuses only when it is certain. If it cannot tell which photo the canvas is holding, it saves anyway — a refused save loses work you actually did, and that is worse than the problem it prevents.

  • perf

    Switching photos no longer waits for the upload. It used to sit through the whole round trip, about thirteen seconds on a slow connection, and that wait is what made switches stall to begin with. Your edits go to this browser first and the upload follows on its own time.

  • perf

    Photos that have not changed are no longer re-uploaded, and there is now a ceiling on how often the app can upload at all, so a future bug cannot run away with it. One brush stroke used to cost twenty-eight uploads. It now costs five.

  • infra

    Reproduced under test fixtures before it was fixed, then the same tests inverted to pin the fix. Measured again in a real browser afterwards: nineteen photo switches, not one landing on another photo's edits, and none stalling.

v7.665 August 2026

Deleting a photo actually frees its space

  • fix

    Removing a photo used to delete its edit history and take it off the screen — and quietly keep its full-size original in your browser's storage, forever. Up to two copies per deleted photo, and originals are the biggest thing the app stores. They are now cleaned up the moment you delete.

  • fix

    Carefully, though. Duplicates share their bytes with the photo they were copied from, so the cleanup first checks that nothing else still needs them. Deleting a copy never touches the photo it came from. When in doubt, it keeps — extra bytes are recoverable, a deleted photo is not.

  • fix

    If storage cleanup fails for any reason, the delete still succeeds and the leftover bytes wait for next time. A full disk never turns into a broken gallery.

  • infra

    The leak was reproduced under test fixtures before it was fixed, and the fix is pinned by twelve tests — including the one where two photos share bytes and one of them is deleted.

v7.654 August 2026

The Paste button tells you when it can't paste

  • fix

    Clicking "Paste (Ctrl+V)" on the start screen could do nothing at all, with no message, in three different situations: the browser blocked the clipboard, there was no image on the clipboard, or the read never came back because the window was not focused. All three now say what happened.

  • fix

    The third one is why this went unnoticed for so long. The read never failed — it just never finished, so there was nothing to report and nothing even to log. It now gives up after four seconds and tells you.

  • ui

    Every message points at Ctrl+V, which takes a different route to your clipboard and still works when the button cannot.

v7.644 August 2026

Recolouring a shape works from the panel it actually shows

  • fix

    The last release made a placed shape recolourable, but only if the colour you clicked was different from the one the panel happened to be showing. Click a shape you drew in orange while the panel still reads purple from earlier, click purple because purple is what you want, and nothing happened — the panel had not changed, so the app thought you had not asked for anything.

  • ui

    The panel now loads the shape's own settings when you click it, so it shows you what the selected shape is instead of whatever you last used. A click on any colour is a real change.

  • fix

    Changing only the stroke width no longer quietly drags an old panel colour along with it.

  • fix

    Stroke width, the arrow style and the fill controls were stuck for the same reason and are fixed alongside the colour. Reselect a pin and recolour it and it stays a pin, instead of turning into a plain circle.

  • ui

    A run of changes to one shape is a single undo step, not one per click — pick a colour, nudge the width, change the fill, and Ctrl+Z takes all of it back at once.

  • infra

    Two regression tests named for the symptom that was reported, so this exact case cannot come back quietly.

v7.634 August 2026

Change the colour of a shape you already placed

  • fix

    Click a square or a circle you drew earlier, pick a different colour, and it changes. Until now it did nothing at all — the shape kept whatever colour it was drawn with, and the only way to change your mind was to delete it and draw it again. The bug was seven weeks old.

  • fix

    Stroke width, the arrow style and the fill controls were stuck in exactly the same way, for exactly the same reason, and they are fixed with it.

  • fix

    The parts you would check next were checked rather than assumed. Recolouring is one undo step, so Ctrl+Z puts the old colour back. The new colour survives closing the picture and opening it again. And it is the new colour that comes out in the file you export, not just the one on screen.

  • ui

    Dialogs keep the keyboard inside them. With a dialog open, Tab used to walk straight out of it and carry on through the page behind, and closing one left you back at the top of the page instead of on the button you opened it from. Every dialog now holds the keyboard while it is open and hands it back where you left it.

  • ui

    Screen readers are told the rest of the app is inactive while a dialog is up, which they were not before. That covers every dialog at once — the delete confirmations, Settings, the shortcut list and the update prompt all share one piece of code, and the fix went there.

  • infra

    Seventeen new tests, and both fixes were measured in a real browser rather than reasoned about — including a control run against the old code to confirm the keyboard really did escape before, and really does not now.

v7.621 August 2026

Shapes and text stay where you put them when you resize

  • fix

    Add a shape or some text, then resize the picture, and they used to slide off to one side. They were never moving — the picture was moving out from under them. Shapes, text, pen paths and their outlines now shrink and grow with the picture, so they stay exactly where you put them.

  • fix

    Text scales with the picture too, instead of staying the same size and swallowing a photo you have just made smaller.

  • fix

    A layer mask used to stop working altogether after a resize. The mask is stored at the picture's size, and resizing left it at the old one, at which point the app quietly ignored it and the layer went back to fully visible. Same cause as the first one, found while fixing it.

  • fix

    Cropping had the same blind spot in a different place. It moved things to the right position but never told the screen to redraw them, so they looked wrong until you undid something or switched tools. Crop, resize, canvas size and the canvas border all say so now.

  • infra

    Nine tests pin it down, including the exact case that was reported: a shape centred on a picture is still centred after the picture is halved.

July 2026

35 features · 61 releases · 179 commits · 66 fixes

  • v0.9.67Smarter selections — and a wand that knows where objects end
  • v0.9.69A lasso that finds the edge for you — and a link to every view
  • v0.9.52Pasting big images finally behaves — and SVGs just work
  • v7.61Name a whole gallery from what's in the pictures
  • v0.9.88See a selection before you make it
  • v0.9.86The Selection tool grows up — magnetic lasso and layers
  • v0.9.87Select gets its own button — and stops lying about its size
  • v7.58The ring stops lying about which tool is live
v7.6131 July 2026

Name a whole gallery from what's in the pictures

  • feature

    Batch has a fourth tool: AI Rename. It reads every loaded photo and names it from what it sees — the dominant colour, whether it is bright or dark, whether it is a photograph, a graphic or a screenshot, and a rough read on the subject. Scan once, then edit the naming pattern and the whole list re-previews as you type. It runs on your own machine, so it works signed out and costs nothing per picture.

  • feature

    It describes a picture rather than recognising what is in it. You get dark-blue-portrait, not golden-retriever. That is a real limit, and the panel says so instead of pretending otherwise.

  • ui

    Drop or paste a stack of images and they go straight to the gallery. The three-way "where should this go" question only makes sense for a single picture, so now it only shows up for a single picture.

  • fix

    Dropping several images at once used to keep the first one and throw the rest away — no message, nothing to tell you they had gone. Pasting several did the same. Both now take every image you hand them, as many as your plan has room for, and say so when the batch had to be trimmed.

  • rust

    The reading is done in the engine rather than the browser. It samples a fixed grid whatever the picture's size, so a 24-megapixel photo costs the same to look at as a thumbnail.

  • fix

    Two colours came out wrong and the tests caught it before release: pure blue was being called "sky" and green foliage "lime", because the colour wheel was labelled one notch off.

  • infra

    Thirty-three new tests cover the naming, including the case that matters most — twenty photos that honestly describe the same still have to end up with twenty different filenames.

v7.6030 July 2026

Edits made just before switching photos could be lost

  • fix

    Your drawing is saved on a short delay, and if you switched photos while one of those saves was still writing, the next save was dropped instead of queued — silently, with nothing left to retry it. The strokes since the last completed save never reached disk. Saves now queue behind each other, so switching photos waits for the write instead of racing it.

  • fix

    That was worse than it sounds: when reopening a photo the app trusts that record over its other copy, so it would hand back an older version that looked perfectly intact. Nothing about the saved format changed — only whether the save runs.

  • infra

    Reproduced under test fixtures before being fixed: four tests that failed first, and the twenty-three existing save-and-restore tests still pass untouched.

  • infra

    Four architecture decision records written for calls already made — how the toolbar is organised, how the focus ring works, how non-React code talks to the UI, and how shared image data is cleaned up. They record decisions rather than making them.

  • infra

    Audited all fifteen documentation pages. Four were saying things that are no longer true — the keyboard-shortcut table still listed the old tool keys, and the file-format page still called a shipped feature a plan — and the security page was missing its most urgent open item. Nothing was deleted; everything listed still earns its place.

  • infra

    Chased down whether the paid-tier mix-up had a billing record tangled in it. It does not: nobody has ever subscribed, so there is nothing attached to the wrong account and nothing to migrate. Read-only investigation — no account, subscription or backend setting was touched.

  • infra

    A structural health report on ten releases of drift, measured rather than guessed. The good news: a big refactor from ten releases ago held, and the file it produced is exactly the size it was left at. The less good news: the file it was split out of grew anyway while being actively dismantled, and a second oversized file has been quietly getting bigger. Nothing was changed off the back of it — the numbers are the point.

  • fix

    Four smaller ways storage could quietly stop working, all closed. A single failed connection to browser storage used to be remembered for the rest of the session, so one bad moment wedged that store until you reloaded. Browsers also close idle connections on their own and the app kept using the closed one, after which every save failed. A failed upload was read as if it had succeeded. And an unreadable cloud archive was treated as an old-format image, hiding the real problem behind a confusing one.

  • fix

    Drop shadows on text vanished when a photo came back from the cloud. Saving locally and saving to the cloud each had their own copy of the same list of things to keep, and the cloud copy was missing all nine shadow settings — so the same photo kept its shadows on the machine you drew it on and came back flat anywhere else. There is one copy of that list now, and a test that fails if the two ever disagree again.

  • ui

    The keyboard shortcuts list is a real dialog now. Escape closes it — it used to ignore the key entirely. Tab cycles inside it instead of wandering off into the page behind it, and when it closes you land back on whatever you were on before you opened it. Screen readers are told it is a dialog and read its title. Alt+/ still opens and closes it, and the × and clicking outside still work.

  • infra

    It got there by being rebuilt on the same dialog the delete confirmations use, rather than by bolting accessibility onto a one-off overlay — one less thing that can drift. Everything it lists is unchanged, and still generated from the tool list rather than typed out.

  • infra

    Three tools were each carrying their own copy of the same canvas maths. The duplicate-code report called it the biggest repeated block in the project at eighty-nine lines; reading it, twelve were actually identical and the rest only looked alike. The twelve are now written once, the block is gone from the report, and the parts that merely resembled each other were left alone — three honest copies beat one function with a switch for every difference.

v7.5929 July 2026

Compressing one photo could delete another photo's original

  • fix

    Originals are stored once and shared — that is what makes duplicating a photo instant, since the copy points at the same bytes. But the cleanup that ran after compressing only checked whether that photo still needed the old bytes, never whether anything else did. Compress a photo, duplicate it, compress the duplicate, and the first photo's original was deleted out from under it while it still pointed there. There is no backup to fall back on.

  • fix

    Cleanup now asks whether anything at all still needs the bytes, and keeps them if anything does. It leans toward keeping: leftover bytes are something we can measure and clean up later, while a deleted photo is gone.

  • infra

    Reproduced under test fixtures before it was fixed, including a test that performs the old delete on purpose — so the reproduction is known to be real rather than a story about the code.

  • infra

    The July shipping popper (Ctrl+\) had drifted two releases behind its own changelog — it was still counting through v7.57. Its numbers are counted from this trail log rather than typed in, so they are caught up, and the two that were hard-coded in the markup now read from the same place as the rest.

  • ui

    Your export format and quality stick now. They were held in component state, so every reload quietly put them back to JPEG at 75 — a choice you had to re-make every visit. They live with the rest of the remembered preferences now.

  • fix

    Signing in on the live site works again: the backend now trusts both sign-in providers, which is what was refusing share links. A paid account still reads as free there, and that turned out not to be an auth problem at all — signing in through two different providers creates two separate accounts on the backend, and the live site signs you into the one without the subscription. Diagnosed and written up; the fix is a decision about which provider to keep.

  • fix

    A future update can no longer wedge the app on an old tab. Browsers refuse to upgrade a database while an older tab still has it open, and nothing was listening for that — so the next schema change would have left the new tab waiting forever, with every save waiting behind it. Old tabs now step aside when an upgrade arrives, and if something still holds on, it says so instead of hanging in silence.

v7.5828 July 2026

The ring stops lying about which tool is live

  • ui

    Two tiles could claim to be the current tool. Click a tool with the mouse, then switch with a keyboard shortcut, and the one you left kept a ring while the new one grew one too — because the keyboard-focus ring was the same warm accent, at the same width, as "this is selected". Focus is now neutral ink and dashed: it reads as the keyboard being somewhere, not as the live tool.

  • ui

    In the gallery the two states were painting identical CSS, so a keyboard-focused thumbnail and a multi-selected one could not be told apart at all. Selection now also marks its own edge, so it stays visible when the keyboard lands on it.

  • fix

    The fix was not to remove the ring. That ring is what makes the app usable without a mouse, and it got easier to see rather than harder — the contrast of the focus outline went from 2.67:1 to 14.3:1 on the light theme.

  • fix

    The status bar was naming a key that did nothing. On Adjustments it said "8", on Shapes "7", on Batch "0" — digits left over from before the toolbar became five groups, bound to nothing. On Crop it said "2", which was worse: 2 is Select, so the hint meant to tell you where your tool lives took you out of it.

  • ui

    It also named the group instead of the tool, so Resize called itself "compress" and Pen called itself "brush". The bar now reads both the key and the name off the toolbar itself, which is how the Select tools got their hint back too.

  • feature

    A new version asks before it takes over: "Update to the latest version?" — Yes or No, in the same kind of dialog as "Delete this image?", instead of a toast in the corner with a Reload link. It says what Yes does, too: the tab reloads, and your photos and edits stay where they are. No means no, and the offer comes back later rather than never.

  • ui

    The confirm buttons say what they are. "Delete image" was red text on a red tint, which measured 3.99:1 — under what small text needs to be readable. It is a white label on solid red now, and the update dialog's Yes is white on deep warm brown.

  • infra

    There is now a number for how much dead weight sits in browser storage. A read-only audit walks every local store and reports what a cleanup pass would find, deleting nothing: on a real twelve-photo gallery, nothing was stranded. It also confirmed two suspected leaks — Auto Compress, and deleting a photo — and found a worse one pointing the other way, where tidying up after a duplicated photo can remove bytes the original still needs. Written up rather than patched at three in the morning.

v7.5727 July 2026

One tab at a time, and a save that can't wedge the gallery

  • fix

    Changing photos could stop working entirely, until you deleted the photo you were on. Saving an edited photo to the cloud waits on the server, and a Convex request that never answers — neither succeeding nor failing — left that wait running forever with the gallery behind it. Every cloud step now gives up after eight seconds.

  • fix

    Nothing was lost when that happened, and nothing is lost now: your edit is written to the browser's own storage before the upload is even attempted. Giving up on the upload costs freshness, not work.

  • feature

    Image Horse open in two tabs now asks which one you mean, the way Google Messages does. The others park behind a "Use here" button instead of quietly writing over each other — every tab shares one local database, so two at once could overwrite work with no warning.

  • ui

    Clicking a photo says so immediately. Saving an edited photo can take a few seconds, and for that whole time nothing moved — no highlight, no progress — which read as a broken gallery rather than a busy one.

v7.5627 July 2026

Signed in, and told to sign in

  • fix

    Share links told signed-in people to sign in. The live site signs you in with one Clerk instance and asks a Convex backend that only trusted a different one, so the token was refused every time — while Clerk went on reporting you as signed in. Both instances are trusted now.

  • fix

    The button no longer guesses. "Sign in to create share links" was shown for three different situations, including to people already signed in; it now tells them apart — still connecting, actually signed out, or signed in but refused by the backend.

  • infra

    The same silence covered every account-backed feature, not just sharing: cloud edit persistence, preference sync, recent texts, and the user record the paid tier is read from. Whether paid accounts are being served free limits on the live site is written up as the first thing to check.

v7.5527 July 2026

A pen path stays put while you style it

  • fix

    Reaching for the Pen panel deselected the path you had just drawn. The "click away to finish" rule read raw coordinates, so every click on the panel counted as away — including the click on the colour swatch you opened it to reach. The path was gone before the picker appeared.

  • ui

    Finishing a pen path now leaves it selected, so Stroke and Background restyle the thing you just drew. Changing a path's colour used to mean finding it in the Reselect list first, which is a lot to ask of anyone who hasn't found that list.

  • ui

    The ring on your first point says whether the ends are joined: dashed while the path is open, solid blue when clicking there would connect them, and solid once they are. An open path that happened to finish near its start used to look exactly like a closed one.

  • ui

    Esc is the way out — it bakes the path and deselects. Undo still steps back through the restyle and then the path itself, one at a time.

v7.5427 July 2026

The headline says what it is, and every page gets air at the top

  • ui

    The homepage headline led with "That's Rust, compiled to WASM" — an engine note aimed at people who already know what those words mean. It now reads "Crop it, compress it, annotate it, gallop. Free in your browser. No account." Price, place and friction, which is what someone decides on.

  • ui

    Every page starts lower. Home cleared the floating nav by 56px and the sub-pages by 66px; both are 80px now, and Pricing — which had its own third header style — was the worst at 18px.

  • fix

    On a phone the heading sat 18px under the nav bar, because the mobile rule assumed the bar shrinks on a small screen. It doesn't: it's 62px tall at 390px wide exactly as it is at 1440.

  • ui

    The bar says "Image Horse" on a phone again, instead of leaving the logo to do it alone. The wordmark had been pulled out because at 20px it shoved the menu button off the edge of a 375px screen; at 16px, next to a shorter button, it fits with room to spare.

  • ui

    The bar's button is "Demo" rather than "Open the demo". It sits beside a horse and the words Image Horse, on a page about the demo — the verb wasn't carrying anything. The full phrase stays on the hero and Pricing buttons, where there's room for it to work.

  • fix

    The underline under the current menu item was measured in whole pixels while the links sit on fractions — Pricing is 41.25px wide — so the rule landed up to a quarter-pixel off, by a different amount on each link. It now measures the real geometry and sits exactly under the word.

  • fix

    The Ctrl+\ shipping celebration was still counting July at the 22nd: 42 releases and 109 entries. July actually ran to 53 releases and 151 entries, a third of everything ever shipped, and the popper's feature chips missed the whole five-group toolbar.

  • ui

    Ctrl+\ is listed in the keyboard shortcuts now. It was bound but undocumented — a key combination nothing in the app admitted existed.

  • infra

    The contribution squares are regenerated after the release commit rather than before it, so the day you shipped on isn't blank. Monday the 27th started a fresh week column and had nothing in it, because the generator reads git log and had run a commit too early.

v7.5326 July 2026

The tab, the search, and the feature list catch up

  • fix

    Object Removal had become unreachable: the AI panel's mode picker moved into the sub-tool header, and the restructure then filled that header with the group's sub-tools. Background and Object Removal now sit together.

  • ui

    The browser title leads with the line the homepage opens on, instead of 'local-first image editing in the browser'.

  • feature

    Command-K searches all 45 features, each jumping to its own anchor. Derived from the same generated list the Features page renders, so the two can't disagree.

  • fix

    Eight feature entries still described the eleven-tool layout — 'Effects > Color Picker tab', 'the Arrows sub-tab inside the Shapes tool'. They now name the sub-tool you'd actually click.

  • ui

    Top bar buttons match the tool rail: same border, hover ring and icon proportion. The panel and Review toggles are one component and now look it.

v7.5226 July 2026

Links name the tool you're actually looking at

  • feature

    Routes are now #/create/brush and #/edit/color-picker. Crop, Transform and the Eyedropper are one tool underneath, so they used to share a single URL that couldn't say which it meant.

  • fix

    Thirty-five legacy URL shapes redirect to the sub-tool they meant, each pinned by its own test. Old bookmarks keep working.

  • fix

    #/tool/select/edge was landing on Magic Wand — 'select' is both a group and a legacy tool slug, and the group reading won. The legacy reading now wins under that prefix.

  • fix

    The command palette listed rows like 'Paint > Paint'. Entries now read 'Create > Brush', built from the same registry as the toolbar and the keyboard.

  • ui

    Ruler sits on the rail beside Guides as a disabled placeholder, like Perspective — the slot is held, the measuring isn't built.

v7.5126 July 2026

Eleven tools became five groups

  • ui

    The toolbar is now five groups — Enhance, Select, Create, Edit, Batch — with everything you had one level down as a sub-tool. Nothing dropped, no tool id renamed, so old links still resolve.

  • fix

    The sub-tool decides what the canvas does. Anything without its own case used to inherit the clone stamp's handlers, which is how a selection drag could nearly clone-stamp the image.

  • ui

    Crop, Transform and the Eyedropper each get their own panel instead of sharing one. Same for Resize Layer, Canvas Size and Guides.

  • feature

    The eyedropper remembers: picked colours land in a Recent Colors list you can click to re-apply.

  • ui

    OCR moved out of the Text panel and onto the rail; Text's background and bubble controls moved up beside the colour swatch.

  • ui

    Digits 1-5 select the five groups, derived from the registry so the keys, the tooltips and the shortcut sheet can't disagree.

v0.9.9326 July 2026

Paid accounts were getting free limits

  • fix

    If you pay for Image Horse, the app was giving you the free tier. Signing in resolved every account to free, so a paid subscription got a 24-photo gallery instead of 100, 100 MB of storage instead of 5 GB, three layers per image instead of unlimited — and the AI tools, the thing you're actually paying for, stayed switched off. The server knew your real tier the whole time; only the interface was wrong. It now reads that tier and unlocks live, with no reload.

  • ui

    The toolbar has a second row. Sub-tools used to live inside each tool's settings panel as wide word-tiles, below a column of square icon tiles — two shapes of button in one narrow strip, and the row scrolled away with the panel. They now sit directly under the tool rail in tiles of the same shape, so the header grows by exactly one row when a tool has sub-tools.

  • ui

    Every tool is on the number row now: 1 through 9, then 0 for the tenth and - for the eleventh, running in reading order across the rail. Select takes 3 — it lost its S key two releases ago, and this is the renumbering that was promised.

v0.9.9225 July 2026

The clone stamp is finally just the clone stamp

  • infra

    Nothing user-visible changes in this one. For most of this app's life, a single 1,467-line file owned everything the image engine does — because the app started as a clone stamp tool, and every feature since moved into the file that held the engine. It's now six files with honest names: the engine core, history, layers, export, transforms, and the clone stamp itself at 229 lines.

  • infra

    Nothing moved but code, and that claim was tested against the built app from a fresh profile: paint, undo and redo byte-exact in both directions, layer add and visibility toggles exact, flips exact, export produced a real file on disk, and a reload restored the session pixel-for-pixel.

v0.9.9125 July 2026

Nothing changes — less can go wrong

  • fix

    Undo and redo can no longer take the editor down. Six places in the image engine assumed the edit history was present rather than checking. None of them could actually fire today, but they sat far enough from the check that one careless edit would have turned a dead assumption into a crash mid-edit. They check now, and fall back to ordinary undo if the history isn't there.

  • infra

    The build can no longer lie about what shipped. For five weeks the live site served an image engine missing half its machinery and nothing caught it — the app quietly worked around the gap instead of failing, so every build looked fine. There's now a check that fetches the running site's engine, looks inside it, and fails if it's the wrong one.

  • infra

    The project's own code checks went from advisory to blocking. They used to print complaints and pass anyway. They can fail the build now, and the count of existing problems can only go down — a new one stops the build immediately.

v0.9.9025 July 2026

The Select tool is one list of six

  • ui

    Wand, Edge-aware, Magnetic Lasso, Color Range, Rectangle and Ellipse now sit in a single group with one of them on at a time. Rectangle and Ellipse used to be a separate "drag shape" setting running alongside whichever mode you had picked, so dragging swept a rectangle no matter what the panel said was selected — and nothing on screen told you the two halves were different things. Now the mode decides both what gets selected and how you ask for it.

  • feature

    Click for the first four, drag for Rectangle and Ellipse. A click in Rectangle does nothing and a drag in Wand does nothing, so a slip of the hand can't hand you a selection you didn't ask for. The trade is a mode switch before you can drag a box, where before you could always just drag — that's the cost of being able to read the panel.

  • ui

    Rect is now Rectangle and Magnetic is now Magnetic Lasso. Each mode has its own address, so a link can point at one. Select has no keyboard shortcut for the moment — it gets a number in a UI change that's coming.

v0.9.8924 July 2026

The Magic Eraser is live

  • feature

    Brush over something you want gone and release — it's removed and filled in from the surrounding image, entirely on your device. No upload, no sign-in. You can also select the object first and hit Remove Object. One rule: cover the whole object — a partial stroke lets the fill rebuild it from its own leftovers. Large areas can come out soft for now; undo brings everything back, selection included.

  • fix

    A deploy bug meant some of the engine's newer machinery never actually reached the live site — the app's own safety checks quietly worked around the gap instead of crashing, which is why nobody noticed. Fixed: the full engine now ships, so features like the editing history that survives a reload finally work on the live site the way they always did in development.

  • rust

    The removal runs a PatchMatch kernel compiled into the WebAssembly engine. Undoing a removal restores the pixels and what you had selected, and the whole path is pinned by tests from two directions.

v0.9.8824 July 2026

See a selection before you make it

  • feature

    With the Select tool, hover over the image and hold a modifier: the region a click would select lights up as a filled zone — green while you hold Shift (what you'd add), red while you hold Alt (what you'd subtract). It re-floods live from the pixel under your cursor. Click and it commits; the zone becomes the real selection. Purely there to help you aim — it changes nothing on its own.

  • feature

    Works for the wand, the edge-aware wand, and color range (the magnetic lasso is anchor-based, so it has no hover preview). The preview runs the exact same flood the real click runs, so what you see is what you get.

  • rust

    The preview is a new read-only engine call that shares one mask core with the committing selection, so the two can never disagree about what a click grabs. It touches neither your selection nor your undo history, and the recompute is throttled to one flood per frame.

v0.9.8723 July 2026

Select gets its own button — and stops lying about its size

  • feature

    Select is a real tool now, with its own button (press S). It used to hide inside Adjust & Select behind a Click-to-select toggle you had to arm first; now you pick the tool and the canvas just works — click to select, drag to sweep a rectangle or ellipse marquee. Hold Shift to add to a selection, Alt to cut away, and the cursor shows a little + or − so you know which one you're about to do.

  • fix

    On photos bigger than the window, the marching-ants outline drew two to three times larger than the actual selection — the overlay was sized to the image's raw pixels while the canvas was scaled to fit. The selection underneath was always right; only the outline was wrong. Fixed, along with the magnetic lasso's wire, which had the same bug.

  • feature

    Every selection is now a step in History: each select, add, subtract, Select All and Deselect shows up by name and undoes with Ctrl+Z. Undoing a Delete Selection or a Cut-to-layer brings back what you had selected, not just the pixels. A click that changes nothing records nothing.

  • ui

    The Select panel got tidier: one Selection header with the explanations behind its lightbulb, five actions in two neat rows (All, Deselect, Delete, then Copy and Cut), and the drag shape — rectangle or ellipse — is a two-button choice. Old links to the combined tool still land in the right place.

  • rust

    The rectangle and ellipse marquees are new engine producers riding the same combine pipeline as every other selection kind, and undo snapshots now carry the selection mask — selection-only steps never touch the op log, pinned by tests.

v0.9.8623 July 2026

The Selection tool grows up — magnetic lasso and layers

  • feature

    The magnetic lasso is live. Click your way around an object and the line snaps to the edge between each click, so you stop tracing outlines by hand. It sits alongside the wand, the edge-aware wand, and color-range select in one panel that now looks like the Paint tool — four tiles, each with its own explanation behind the lightbulb.

  • feature

    You can now lift a selection onto its own layer: Copy it to a new one with Ctrl+J, or Cut it out onto one with Ctrl+Shift+J — the move Photoshop has had forever. The pixel work runs in the Rust engine on a new SIMD path, so it stays instant even on a large photo.

  • fix

    Copying a selection now copies what you actually see. It used to grab only the active layer, so a selection over a caption pasted a blank rectangle; it now takes the whole visible image — text, shapes, every layer.

  • fix

    Guides and rulers no longer flash across the whole screen when you open the Batch editor, and a text drop-shadow set to “Box” with nothing behind it now casts from the letters instead of doing nothing.

v0.9.8522 July 2026

A check that was never running, now runs

  • infra

    Nothing you can see changed in this one. The project's checklist has required a code linter to pass before anything ships, and it turned out the linter had never actually run — it was never installed, and there was no configuration for it to read, so every attempt quietly failed and got treated as a pass. That is worse than not having the check at all, because everyone assumed it was working.

  • fix

    It runs now, and the first real pass over 207 files found 26 things worth fixing — all of them fixed here. Mostly harmless leftovers: values computed and then thrown away before anything read them, a few places where the code had stopped describing what type it was really handling. The test files turned out to have had no automated checking of any kind until now.

v0.9.8419 July 2026

The app learns to cache itself — switch still off

  • infra

    Every visit re-downloads about 3.6 MB of the app, the Rust engine included, and losing your connection mid-session means the next boot fails outright — odd behaviour for an editor whose photos and edits already live on your own machine. A service worker fixes both. This one only caches the app's own files: signing in, cloud sync and share links always go straight to the network, so nothing about your account or your documents can be served stale.

  • infra

    It ships turned off, and a default build contains none of it — nothing registered, no bytes. Switching it on is a separate decision, because a misbehaving service worker is the worst thing this app could ship: it leaves people on an old version without ever saying so. Once on, a new build waits for you to click Reload rather than swapping code out from under an open edit.

v0.9.8318 July 2026

Preferences stop trusting storage blindly

  • infra

    The three stores that remember your preferences across a reload — which tool sub-mode you were on, your command-palette habits, the savings badge — used to trust whatever came back out of browser storage as-is. They now check it against what the app actually understands before using it, and fall back to a sane default for anything they don't recognize.

v0.9.8218 July 2026

The AI tool becomes the Eraser tool

  • ui

    The AI tool is now the Eraser tool — same spot on the toolbar, new icon, one panel. Brush Eraser moved here from Paint. Magic Eraser is a new slot for the local PatchMatch removal that just landed, marked Coming Soon until it's wired in. Background Removal and Object Removal are unchanged. Two placeholder cards that never did anything, Smart Crop and Auto-Enhance, are gone.

v0.9.8118 July 2026

Object removal lands — local, free, still dark

  • rust

    A local object-removal kernel merged in: select a region and hit Remove Object, and it reconstructs the hole from the rest of the image — no sign-in, no network, runs entirely on your device. It's PatchMatch (Barnes et al.), single-resolution for now, so a real photo will look a little smeary; that's day one of a few. Ships behind a flag that's off by default — dogfooding before it becomes anyone's default.

v0.9.8018 July 2026

The Features page gets a sidebar worth using

  • ui

    The Features page's sidebar is rebuilt: an icon on every group and every one of the 40 features, a count badge instead of a bare number, and a filled row marking whatever you're reading instead of a thin underline. It sits as an inset panel in the page's own margin now, not a flush column of text.

  • fix

    Its two groups used to force themselves open no matter the screen size, so a phone opened onto all 40 items stacked above the content before you saw a word of the page. They now open closed on mobile and open on desktop, and switch live if you resize across that line.

v0.9.7917 July 2026

The new undo history is on for everyone

  • feature

    The undo-history feature that's been building for weeks is now on by default. Every edit is recorded as a small operation instead of a full snapshot, your work is saved a couple of seconds after you stop, and a reload brings back exactly what you left — canvas, border, and all. Anything the recorder can't handle yet quietly falls back to the old undo, so there's no way to get stranded.

  • rust

    Before flipping the switch it had to pass a four-part test on the real app: same build with the feature off and on must produce identical documents, then a paint stroke and an AI background-removal each have to survive a full save-and-reload. All four passed, checked down to the byte.

  • fix

    The test nearly failed on a ghost: after a reload the canvas looked gone. The data was fine — the checkerboard pattern behind the image was drawn by a separate element that didn't shrink when the image did, so on big photos it drifted out from under the picture. It's now painted by the image element itself and physically can't misalign. The same bug explains the stray checkerboard strip some sessions showed beside the photo.

v0.9.7816 July 2026

A new site — and this page now counts its own commits

  • ui

    The whole marketing site is new: five pages instead of one, including this release log, a full feature list, and an architecture map you can filter down to whichever kind of user you are. The old site's headline claimed your pixels never leave the tab. That's true in the demo and not true once you sign in, so it now says what it can actually back up — and the table proving it sits right underneath.

  • infra

    The squares above are generated straight from the project's commit history every time a release goes out, rather than typed in by hand. A number nobody re-counts is a number that quietly goes stale — the first run already found a day that had been missed.

  • fix

    Three things that were broken and invisible until measured: the search box (⌘K) opened underneath the navigation bar instead of on top of it; the features page highlighted the wrong entry in its sidebar as you scrolled, always running one ahead of what you were reading; and searching then hitting Enter reloaded the whole site instead of just moving to the page.

v0.9.7714 July 2026

Caught a second data-loss bug before anyone saw it

  • rust

    Behind the scenes: before turning on the new undo-history feature by default, it got tested one more time — same build, feature off vs. on, nothing else changed. Import a photo, don't touch it, reload. Off, you get the photo back exactly as imported. On, you got it back cropped, with the border and background gone. That's a real bug, caught before it ever reached a real user.

  • fix

    The cause: the history feature takes its first snapshot a moment too early — before the app finishes setting up a new import — so a photo you hadn't edited yet got remembered in an unfinished state. Fixed so the snapshot always matches what you're actually looking at, and as a second layer of protection, an empty history entry is never saved or restored at all — there's nothing in it a normal reload can't already recover the ordinary way.

  • infra

    The undo-history feature is still off by default while this gets one more real-world check. Nothing changes for anyone today.

v0.9.7613 July 2026

Your edits are now actually saved

  • fix

    This is the big one: if you edited a photo and reloaded the page without switching to a different photo first, your work was gone — the app quietly gave you back the original. Strokes, layers, all of it. That's fixed. Your edits are now saved a couple of seconds after you stop working, and again when you close or reload the tab.

  • fix

    A second, meaner version of the same problem: when you were signed in, the app tried to save to the cloud first and only saved to your own machine afterwards. If the cloud call stalled — no error, just silence — the local save never happened either. It now always saves to your machine first, and treats the cloud as a bonus.

  • rust

    Behind the scenes: this was found by testing something else entirely. The undo-history feature was blamed, and it turned out to be innocent — it had correctly stepped aside and handed off to a backup copy that nobody was writing.

v0.9.7513 July 2026

The diagnostics panel stopped being cryptic

  • rust

    Behind the scenes: the Alt+Delete diagnostics window now explains itself. Where it used to show a bare counter sitting at zero — which could mean anything from "nothing's happened yet" to "this is completely broken" — it now says which, in plain words.

  • rust

    It also shows the shape of the document you're working on, and which layer an edit would actually land on. Two bugs this month were invisible for exactly that reason: everything looked normal, and the thing that mattered wasn't on screen anywhere.

v0.9.7413 July 2026

A stroke that was saved, and nobody said so

  • rust

    Behind the scenes: while testing the new undo-that-survives-a-reload, the counter that shows how much has been recorded kept reading zero — even though the edits were being recorded. The recording worked; the part that announces "a stroke just finished" only ran in one specific case.

  • fix

    That had a real consequence, not just a wrong number: the save for your most recent brush stroke wasn't being scheduled, so reloading immediately after painting could quietly lose it. It saves at the end of every stroke now.

  • rust

    Behind the scenes: the tests never caught it because they built their test image a different way than the app does. There are now tests that go through the exact path the app takes when you open a photo.

v0.9.7313 July 2026

The pen tool's closed shapes actually close now

  • fix

    Draw a shape with the pen, click back on the first point to close it, and… nothing happened. No outline, no fill. It turns out the little dot you click to close the path was swallowing the click, so the path never finished. It closes now — and a closed shape fills, the way it always should have.

  • fix

    Picking a pen path back out of the Reselect list used to make it disappear. It now comes back with its curve and its handles, ready to edit.

  • fix

    Tooltips were hiding behind the gallery strip at the bottom of the screen. They sit on top now — everywhere in the app, not just the one you noticed.

  • fix

    "Auto Compress" is now called "Auto Compress & Resize", because that's what it does: big photos get scaled down as well as compressed. There's a lightbulb next to it explaining what it aims for and how small it's willing to go.

  • fix

    The web address no longer claims you're using a tool when you haven't opened a photo yet.

v0.9.7213 July 2026

The engine was missing a piece we thought we'd shipped

  • rust

    Behind the scenes: the work-in-progress undo history — the one meant to survive a reload — turned out not to be inside the engine the app actually downloads. It had been built and tested for months, but a single line in the build recipe left it out of the final file. It's in there now, and we watched it record an edit, save it, and bring it back after a reload.

  • rust

    The engine file is bigger as a result — about 70 KB more — because the code that saves your edit history now genuinely ships with it. Worth being upfront about: that's a real cost on first load, and it buys undo history that survives closing the tab.

  • fix

    It's still switched off by default while it gets a few days of real use. Nothing changes for you yet.

v0.9.7113 July 2026

Undo history finally works on an ordinary photo

  • rust

    Behind the scenes: the undo history that's meant to survive a reload had a quiet problem — it only ever worked on photos with a single layer, and since every photo you open gets a canvas behind it, that meant it worked on almost nothing. The engine now understands the canvas as part of the document rather than as an extra layer, so the history records properly on a normal photo for the first time.

  • rust

    Behind the scenes: the engine used to work out which layer was the canvas by checking whether it was named "Background" — a name that unhelpfully meant two different things depending on how the photo was opened. It's now tracked explicitly, which closes off a way you could have renamed a layer and had the app restore the wrong picture.

  • fix

    Two safeguards came with it: if you paint directly on the canvas layer, or add a second real layer, the app quietly steps back to ordinary undo rather than recording something it can't faithfully replay. It would rather be slower than wrong.

v0.9.7013 July 2026

What you see is what you download

  • feature

    Exports now include the canvas behind your photo — the padding and background colour you can see on screen come with the download. Before, exports quietly cropped to just the photo. If you preferred it that way, there's a switch: Settings → "Canvas background on export", or just hide the canvas layer in the Layers panel.

  • fix

    "Blank Canvas" is now called "New Canvas" when you start a document. Small thing — it just means the word "canvas" refers to one thing throughout the app instead of two.

  • rust

    Behind the scenes: the canvas is now properly understood as part of the document rather than as a stray layer, which clears the road for undo history that survives a reload. That feature was quietly unreachable on any photo with a canvas — which, on the default settings, is every photo.

  • rust

    Behind the scenes: a long-standing question about running the engine on multiple CPU cores in the browser is finally settled — it works, and signing in still works alongside it. That opens the door to making the new Magnetic Lasso and Smart Brush faster on large photos.

v0.9.6913 July 2026

A lasso that finds the edge for you — and a link to every view

  • feature

    The Magnetic Lasso is here. Click a few anchors loosely around an object and the line snaps to its edge between them, following the outline you meant rather than the one you drew. Double-click to close it and you have a selection like any other. It's switched off by default while it gets a few days of real use.

  • feature

    New Smart Brush: paint right up to the edge of something and the stroke stays where you put it instead of bleeding across the outline. Same edges the lasso uses — built once, used twice.

  • feature

    Every view now has a web address. Pick a tool, open a settings pane, and the URL updates; paste that link and you land exactly there. Back and forward work the way you'd expect. There's a new "Copy link to this view" in the command palette (Alt+,) for sharing exactly what you're looking at.

  • rust

    Behind the scenes: the lasso finds the cheapest path along a map of the image's edges, searched within a bounded window so it keeps up with your cursor — about 1 to 6 milliseconds per move on a large photo. No AI, no server: it runs in your tab.

v0.9.6813 July 2026

Undo that survives a reload — and the two bugs standing in its way

  • rust

    Behind the scenes: work continues on undo history that survives closing the tab. It's still switched off by default, and this release is about earning the right to switch it on — two bugs were found that could quietly hand you back the wrong picture after a reload.

  • fix

    The first: after an AI edit (like Remove Background), a reload could restore the photo as it was BEFORE the AI ran, or as a blend of two different edit histories. The saved history couldn't tell an old recording from a new one, so it wrote the new edits on top of the old ones.

  • fix

    The second: a photo with saved history that you'd since added a layer to would come back with its layers gone. The history replayed without complaint — it just replayed a picture that only had one layer. It now recognises it's out of date and steps aside for the real file.

  • rust

    The Diagnostics window (Alt+Delete) now shows what's actually stored for the current photo, and says plainly when a saved history has been stood down.

v0.9.6713 July 2026

Smarter selections — and a wand that knows where objects end

  • feature

    "Edit and Transform" is now "Adjust & Select", split in two: Adjust for cropping, flipping and rotating, and Select for choosing what you're working on. The magic wand moved in here (it used to live under Layer Settings, oddly), and it brought company.

  • feature

    New Edge-aware wand: it selects like the normal wand but stops at an object's outline instead of bleeding out into the background through a soft gradient — the exact spot the old wand always let you down.

  • feature

    New Color Range: click a colour and take every pixel of it anywhere in the photo, not just the connected patch under your cursor. One click gets all the sky, including the bits peeking between buildings.

  • rust

    Behind the scenes: a proper edge-detection engine now ships, and it notices colour boundaries a brightness-only detector would miss entirely (a red shape on a green background of the same brightness, say). It's built to be shared — the magnetic lasso and a future smart brush will use exactly the same edges.

  • fix

    The little green savings badge on a compressed photo ("-95%") used to vanish the moment you reloaded the page. It stays put now — it was only ever held in memory, even though the photo it belonged to was being restored properly.

v0.9.6613 July 2026

Real compression targets, working alignment, and shadows you can actually see

  • feature

    Compress Image(s) now works toward an actual size target — about 200 KB — stepping quality down first, then dimensions if it has to, instead of doing one pass and hoping. Images over 2500px get resized as part of the job (the progress toast tells you when that's happening). In testing, a 9.9 MB photo came out at exactly 200 KB.

  • fix

    The nine Align buttons now work no matter how you picked the thing you're aligning. Before, they only woke up if you'd selected the object from the Reselect list — pick it on the canvas and they sat there greyed out. Placing text also used to look like it did nothing; it was moving, you just couldn't see it.

  • fix

    Shadows are visible in dark mode. Menus, tooltips, toasts and dialogs were rendering flat against the dark background — they now lift off the page properly, with a subtle edge. (Two panels turned out to have had no shadow at all, in either theme.)

  • fix

    Text in a background box or speech bubble now lands exactly where you place it. Plain text was fixed last release; the bubble was still drifting — by quite a lot, as it turns out — because of the space its tail reserves.

  • ui

    Selecting a batch of photos got quicker: tick one checkbox, hold Shift, click another — everything between them selects in one go, just like a file manager.

  • ui

    The command palette (Alt+,) got a proper home: it now looks like the Settings and Diagnostics windows — a real search bar up top, tabs for All / Tools / Settings / Actions, and a Most Used grid of the ten commands you actually reach for most (it starts with sensible picks and learns from there). You can now open it from the right-click menu too, and it's listed in the keyboard-shortcuts sheet and the status bar, so you can actually find it.

  • ui

    The Resize tool's Compress/Resize switcher now matches Paint's — the same icon-and-title toggle every multi-mode tool is moving to.

v0.9.6512 July 2026

Text lands exactly where you typed it

  • fix

    Committing a text bubble used to drop it slightly below and to the right of where you'd typed — barely noticeable with small text, but the bigger the font, the bigger the jump. The editor and its typing box now agree, to the pixel, on where your text goes. Re-editing a bubble and committing it again no longer nudges it either.

  • rust

    Behind the scenes: the engine now reports exactly where a line of text's ink begins, straight from the font's real measurements — that's what makes the typing box and the final result line up at any size.

v0.9.6412 July 2026

A command palette — press Alt+, and just type

  • feature

    Press Alt+, anywhere and start typing: jump to any tool or straight into a sub-mode (type "arrow" and land in Shapes with Arrows selected), flip rulers, grid, or the theme live, open the exact Settings tab you want, undo, redo, export. Your recent picks surface first, labels update with state ("Show Grid" becomes "Hide Grid"), and it's fully keyboard-driven.

  • ui

    The palette knows about every tool today and gets smarter over time — as tools move onto the new registry, their sub-modes and settings show up automatically.

v0.9.6312 July 2026

Quiet groundwork for smarter tool panels

  • infra

    Behind the scenes: the pattern Paint uses for its sub-modes — icons on top, panel below — is now a shared building block, and the first piece of a proper tool registry landed with it. Nothing looks different today; it's the foundation that lets every multi-mode tool get the same clean switcher, one release at a time.

  • ui

    This site's home page got a truth pass — the AI section now says object removal and text extraction are live (they have been for a while), and a few stale numbers and confusing lines were corrected.

v0.9.6212 July 2026

Copy any selection, and the stamp tool learns to let go

  • feature

    Ctrl+C now copies whatever selection you have active — a crop box, a shape's bounds, a magic-wand selection — and Ctrl+V pastes it back through the same movable placement box you already know. There's a "Copy Selection" option in the right-click menu too. Before this, copy only worked on the whole canvas at once.

  • fix

    Leaving the stamp tool — or switching between its Clone, Stamps, and Emoji modes — now actually puts the stamp down. Before, the last stamp you'd picked kept firing on every click, even after you'd moved on to a different mode.

  • ui

    The toolbar button formerly called "Clone Stamp" is now just "Stamps." It's done clone, emoji, and batch stamping for a long while — the name finally caught up. Same button, same shortcut.

v0.9.6111 July 2026

Groundwork for edits that survive a reload — and a sturdier photo list

  • fix

    Your photo list can no longer vanish on its own. It used to be possible — rare, but real — for the "Welcome back" list to get cleared by a bad reload. Now it only ever clears when you actually delete your photos.

  • infra

    Behind the scenes: the editor can now record every brush stroke, blur, crop, and text change into a compact log, rebuild your exact image from it, and keep undo working across a page reload. A single brush stroke saves as about 75 bytes instead of rewriting megabytes. It's not switched on yet — it's being tested against real galleries first — but the pixel-for-pixel proof is done.

v0.9.6011 July 2026

The performance monitor gets smarter about when to check

  • infra

    Behind the scenes: the performance monitor (Alt+Delete → Resources) used to poll everything on one flat timer. Now it only reads the expensive stuff (tab memory) when you hit Refresh, checks memory every 1.5 seconds while the panel's open, and tracks frame rate only while you're actually drawing — freezing on your last stroke instead of measuring nothing. It's also the tab you land on first now.

v0.9.5910 July 2026

Laying the groundwork for multi-core editing

  • rust

    Behind the scenes: blur can now split its work across every core on your machine instead of just one, roughly 7.8x faster in testing. It's not switched on for anyone yet — turning it on for real needs a few more pieces in place first — but the hard part (proving the parallel version produces the exact same result, pixel for pixel, as the original) is done.

v0.9.5810 July 2026

Strip just your photo's location, and a freeze fixed

  • feature

    When you strip metadata on export, you can now choose "Location only" instead of everything — keeps your camera and lens info, drops just the GPS coordinates. Handy when you want the technical details but not your home address baked into the file.

  • fix

    Opening the diagnostics window (Alt+Delete) before loading a photo could freeze the whole app solid with no way to close it. Fixed.

  • infra

    Behind the scenes: a new test suite proves the editor's operation log replays pixel-for-pixel identical to the original edit, every time — the safety net future undo work builds on.

v0.9.579 July 2026

A setting moved to where it belongs, and cleaner canvas removal

  • ui

    The "include the canvas backdrop in exports" setting moved from General settings to the Layers and Canvas tab, right next to the other canvas-backdrop controls. Same setting, just where you'd actually go looking for it.

  • fix

    Removing the canvas backdrop now also trims the document back down to your actual photo. Before this, a removed backdrop could leave invisible extra space around your image that showed up again in exports.

v0.9.569 July 2026

A cleaner way to drop the canvas, and clearer paid buttons

  • feature

    Next to Resize Canvas, there's now a Remove Canvas button. It deletes the background layer outright, so you can keep everything you've drawn or pasted above it without a blank backdrop underneath.

  • ui

    The AI tool buttons (background removal, text extraction, object removal) now show a small padlock when you're not signed in or not on the paid plan, instead of just going faintly gray for no visible reason.

v0.9.559 July 2026

Save and open real projects — full layer stacks, no flattening

  • feature

    You can now export your whole project — every layer — as an OpenRaster (.ora) file, the same format Krita and GIMP use. Open it back up here, or in one of those, and every layer comes back intact. Importing an .ora adds it as a new photo, so it never touches whatever you're already working on.

  • fix

    A few toolbar icons in the Review panel (History, Layers, Reselect, Histogram) had lost their hover tooltips somewhere along the way — you'd see a bare icon with no label. Fixed, and while I was in there, made sure those buttons work properly with a keyboard or screen reader too, not just a mouse.

v0.9.549 July 2026

Four new editing tools, plus two real bugs fixed

  • feature

    Four new ways to adjust a photo, right next to Brightness, Contrast, and Blur: Saturation dials color intensity up or down without shifting hue. Shadows and Highlights let you brighten just the dark parts of a photo or pull back just the bright parts, instead of moving the whole image at once. Sharpen adds crispness back to a soft or slightly blurry photo.

  • fix

    If you're on a paid plan, the app now recognizes that the moment you sign in. Some paid accounts were staying locked out of their own features because the app was only checking whether you were signed in, not what plan you were actually on.

  • fix

    If you ran an AI tool — background removal, text extraction, object removal — and got back a blank or broken image, that's fixed. A browser quirk was reporting the result as zero pixels wide right after processing finished, and that zero was making it onto your canvas.

  • infra

    Corrected some inaccurate claims on the public Architecture page — it was describing API routes and storage that never actually existed.

v0.9.539 July 2026

A green light for faster processing

  • rust

    The next building block for undo landed in the codebase — a tile-based way of tracking edits instead of full-image snapshots. It's off by default and not connected to anything yet, so nothing changes for you today.

  • infra

    Tested whether a browser security setting I need for faster multi-core image processing would break signing in. It doesn't — so that's clear to build on next. Also gave the internal docs that explain how the app is built a full pass to match what's actually shipped.

v0.9.528 July 2026

Pasting big images finally behaves — and SVGs just work

  • feature

    Paste an image as a new layer and you now get a movable, resizable placement box — an image bigger than your canvas arrives scaled to fit instead of getting cropped with no way back. Press Enter to place it, or Esc to cancel — and cancelling cleans up the layer it would have landed on, too.

  • feature

    Resizing or moving a pasted image now shows up in History as its own step. Undo peels back just the resize first, then the paste — instead of nuking the whole thing in one go. The image is re-rendered from the original every time, so there's no quality loss from resizing twice.

  • feature

    You can drop, paste, or open SVG files now. They're converted to regular pixels the moment they come in — safely, so nothing inside the file can run — and from there they edit like any other image.

  • ui

    The Compress panel's buttons are regrouped: Apply Compression & Resize and A/B Compare sit together at the top, with the one-click Auto Compress actions below.

v0.9.512 July 2026

Exports and imports stay smooth

  • perf

    Encoding images for export and making gallery thumbnails now happen on a background thread, so the app doesn't freeze up when you export a large image or drop a big batch of photos in at once. If that background worker ever can't run, the app just does the work the normal way instead — nothing breaks.

v0.9.502 July 2026

Pipeline housekeeping

  • infra

    Maintenance on the build pipeline — updated the CI Node version and fixed a permissions hiccup in the Rust dependency audit. Nothing changes in the app.

v0.9.492 July 2026

Sturdier photo storage, invisibly

  • infra

    Started moving where your original photos are stored onto a more robust database layer. It happens quietly as you open photos — nothing changes for you, and your existing photos keep working. The old storage stays untouched as a safety net, and the whole thing can be switched back with one flag if anything ever looks off.

v0.9.482 July 2026

The loading spinner spins again

  • fix

    The loading spinner no longer sits frozen when you have Reduced Motion turned on (in your OS or in the app). A spinner that doesn't spin looks like the app is stuck, so it keeps turning now — it's essential feedback, not decoration. Other, decorative animations still respect your Reduced Motion setting.

  • infra

    Tidied up a panel animation that had been copy-pasted into nine places down to one shared definition. No visible change.

v0.9.472 July 2026

Housekeeping under the hood

  • infra

    Internal cleanup with no change to how the app works. The big central component that wires the editor together was split into smaller, focused pieces, and the last of the app's global browser events moved into its shared state stores. Nothing looks or behaves differently — it just makes the code easier to work on.

v0.9.462 July 2026

Layers are free, and the pen fills what you drew

  • feature

    Layers no longer require an account. They run entirely on your device, so they shouldn't sit behind a login — the free no-login tier now gets 3 layers per image, same as a logged-in account. Signing in and going Pro is about the cloud stuff (saved projects, storage, sharing, AI), not the editing itself. Crop, blur, resize, paint, and the histogram were already free.

  • fix

    The pen tool's Background fill can now be applied to a path you already drew — reselect the path and change the Background and it fills, instead of only working if you set it before drawing.

v0.9.452 July 2026

Crop keeps your annotations put

  • fix

    Fixed a bug where cropping a photo made text and shapes slide off their spots — the pixels moved but the annotations didn't. They now travel together, wherever you crop.

  • ui

    Under-the-hood consistency pass: every dialog in the app now shares one system (same focus handling, same animation), and the button zoo was consolidated into a single primitive — fixing a few font-size mismatches along the way.

v0.9.442 July 2026

Resize a layer in place, and a cleaner, friendlier settings panel

  • feature

    New “Resize Layer” tool — drag a bounding box around any layer's content (a pasted photo, a sticker, a speech bubble) to scale and reposition it in place, non-destructively, before committing. Lives in the Layers tab next to Move.

  • ui

    Every tool's settings panel got a pass: section titles now sit next to a small lightbulb you can hover for an explanation and any keyboard shortcuts, instead of paragraphs of instructions taking up space. The Paint tool is now a 2×2 icon grid — Paint, Blur, Pen, and Eraser (moved here from Edit & Transform, where the Color Picker now lives instead).

  • ui

    The status bar now cycles through more useful shortcut hints — two tied to whatever tool you're using, one general tip, and Alt+/ (open the full shortcut list) always pinned last.

  • fix

    Fixed a visual bug where the app's background checkerboard and the photo's own transparency checkerboard were two subtly different patterns fighting each other — they're unified now, along with the thumbnail checkerboards.

  • ui

    Holding Shift while dragging an arrow, shape, or pasted image now locks the angle or direction to a clean 90° — handy for perfectly straight connector arrows.

  • ui

    Renamed the confusing three-button “Add this image” dialog to Stack as layer / Merge into layer / New gallery image, each with a plain-language explanation of what it does.

June 2026

74 features · 36 releases · 80 commits · 32 fixes

  • v0.9.12Review panel, 360° bubble tails, dev tier switcher
  • v0.9.8Smart export, batch text, and a unified button system
  • v0.9.10June optimization pass — faster Rust, smaller WASM, Squoosh-style resize, shape edit boxes
  • v0.9.27Security tab, snapped-window layout, reduce-motion fixes, and a keyboard-accessibility pass
  • v0.9.21Settings menu, pick-your-format downloads, and a hardening pass
  • v0.9.15AI tools go live (Replicate) + Stripe billing
  • v0.9.9Non-destructive text + speech bubbles + AI panel
  • v0.9.20Stroke stabilizer, lettered pins, and a big UI consistency pass
v0.9.4330 June 2026

A canvas that stays the right size

  • fix

    Fixed a bug where opening a photo could blow the canvas up to a giant size. The canvas border is now exact and repeatable — your document is always the photo plus the border you chose, it never balloons, and a too-big canvas snaps right back. The live border updates cleanly no matter how a photo was opened.

  • ui

    Spinner polish — loading spinners now scale to the size we ask for, the Settings spinner sits neatly above the panel, and the spinning comet keeps a faint trail so it stays visible on the light theme.

v0.9.4230 June 2026

Guides, and a canvas that resizes

  • feature

    Image guides, like a desktop editor. From Layer Settings you can drop horizontal and vertical guide lines onto the canvas, drag them anywhere, lock them so they don't move, and select or delete them from a list. Add a few and they space themselves out evenly. They show whether or not the rulers are on.

  • fix

    “Resize canvas” now actually resizes the canvas behind your photo instead of scaling the whole image — your photo keeps its native size and the backing canvas grows or crops around it. Changing the canvas border updates a loaded photo live.

  • feature

    A new “Layers and Canvas” settings section. Photos now open on a canvas + photo by default with a 10px border, and you can pick the backing color from a palette — defaulting to the familiar transparent checkerboard. Canvas Size lives here now.

  • ui

    Cleaner loading states throughout — placeholder “skeletons” while content loads, and a refreshed spinner with a bright leading edge that respects reduced-motion settings.

v0.9.4129 June 2026

Start a canvas your way

  • ui

    The Blank Canvas / “New Document” screen is now organized by what you're making — Social, Web, Video, and Paper tabs each offer ready-made sizes. Instagram, LinkedIn, Facebook, YouTube thumbnails and banners, FHD and 4K, A4 and Letter, photo prints, and more. Pick a tab, pick a size, start designing.

  • feature

    New “Canvas on import” setting (Settings → General) can open each photo Photoshop-style — on a slightly larger backing canvas, split into a Background and a Photo layer, so you have room to work around the image. It's optional and off by default, which keeps the classic exact-size load.

v0.9.4029 June 2026

Snappier photo switching, privacy + safer share links

  • perf

    Switching between photos is now near-instant. The app used to re-read each photo from local storage and fully re-decode it every time you clicked it; it now keeps recently-viewed photos decoded in memory, so jumping around the gallery feels immediate.

  • feature

    Privacy by default: exported images now strip camera metadata (EXIF — GPS location, capture time, device) unless you turn it back on in Settings → Security.

  • fix

    Share links are harder to guess — view tokens now come from a cryptographically-secure generator instead of a basic random function. Existing links keep working.

  • infra

    Added an image-upload safety check (validates real file bytes, caps absurd sizes, rejects scriptable SVGs) plus more behind-the-scenes state and storage groundwork.

v0.9.3929 June 2026

Behind the scenes — state-management foundation + storage groundwork

  • infra

    Began lifting the editor's UI, tool, and gallery state into dedicated Zustand stores, untangling a single 3,000-line component. This is plumbing — nothing changes in how the app looks or behaves yet — but it's the groundwork that makes future features quicker and safer to build.

  • infra

    Added a tidy IndexedDB layer (via Dexie) for the heavy data — your original images, edited versions, and gallery list — alongside a small adapter that remembers UI preferences locally. Your photos still never leave your device.

  • infra

    Wrote up three engineering notes — on state management, on why we use IndexedDB, and on a future service-worker that would cache the app for instant repeat loads and offline editing.

v0.9.3828 June 2026

Gallery selection + PgUp/PgDn fixes

  • fix

    Switching photos quickly — rapid thumbnail clicks, or holding PgUp/PgDn — could leave the canvas showing one photo while the gallery highlighted another. The displayed image now always matches the selected thumbnail (a latest-wins guard makes the most recent selection win).

  • fix

    PgUp/PgDn now step through every photo reliably and wrap around — they advance from the truly-current photo instead of a value that lagged behind the image load, so repeated presses no longer get stuck on one.

v0.9.3728 June 2026

Behind the scenes — docs, CI safety nets, faster checks

  • infra

    Reorganized the project documentation into a docs/ folder (Architecture, File Map, Features, Getting Started, Keyboard Shortcuts, CI, and one dated Change Summary) and slimmed the README down to the essentials.

  • infra

    Added a CI pipeline — build, security, and dependency-audit jobs — plus an advisory “guardrails” pass that flags design-token drift (raw colors, off-scale type, stray z-index) without blocking the build.

  • infra

    Added native git hooks that run formatting, lint, and type checks before every push, so a change can’t quietly break the build.

v0.9.3628 June 2026

WASM SIMD128 — the heavy pixel ops got faster

  • perf

    The image engine's hottest pixel loops now use explicit WebAssembly SIMD128 (processing four channels at once): Gaussian blur, brightness, contrast, pixelate, and image resize (bilinear / Lanczos / Catmull-Rom). Measured in-browser, resize runs ~1.6× faster (bilinear) up to ~3.9× (Lanczos) — and the output is bit-for-bit identical, so nothing about your edits changes; they just land quicker.

  • rust

    Every kernel keeps a matching scalar fallback (used where SIMD isn't available) and shares one set of load/store helpers, all consolidated under a new src/simd/ module.

  • infra

    Fixed the marketing deploy: a root vercel.json now pins Vercel to build only the marketing site — previously it ran the app build, which needs a WebAssembly step Vercel doesn’t perform, so the deploy kept failing.

v0.9.3528 June 2026

Tidier notice dialogs and a calmer cursor

  • ui

    The idle “paused to save power” screen, the small-window notice, and the resume prompt now share one compact card — a mid-size icon, a line of copy, and a single button — for a consistent look across the app’s lightweight notices.

  • fix

    The brush-size ring now appears only for the brush tools (Paint / Blur / Eraser and the Effects blur). Tools that don’t paint — Resize, Layer Settings, AI — and the side panels now keep the normal arrow cursor instead of a stray paint ring.

v0.9.3428 June 2026

A big engine cleanup so features ship faster

  • rust

    The WASM image engine had grown into one ~4,800-line file — a single object with ~150 methods piled together. It's now split into focused modules (layers, annotations, paint, effects, selection, and shared helpers), shrinking the main file by about 60%. Not a single pixel changes — same tools, same speed — but the code is far easier to work in, which means new features land faster and with fewer bugs.

  • perf

    Smaller, focused modules let the Rust compiler and editor tooling keep up, so each change is quicker to build and check.

v0.9.3328 June 2026

A compact master bar for narrow & split-screen windows

  • feature

    Snap the window narrow (≤1000px) — or open Image Horse on a tablet — and the whole interface folds into one left “master bar”: a New button plus Tools / Gallery / Review tabs that swap its contents, with settings and your account in the top row. The horizontal top bar disappears and the canvas takes the rest of the screen.

  • ui

    The Gallery tab is the full gallery turned vertical — two square thumbnails per row, scrolled with up/down arrows instead of a scrollbar, with all the same select / export / duplicate / delete controls and the photo count pinned to the bottom.

  • ui

    A one-time “Use compact version” notice greets the narrow layout so it’s clear the app intentionally reshaped for the smaller window.

  • perf

    The master bar is code-split — desktop sessions never download it; its bundle only loads the first time you go narrow.

  • ui

    Settings → Import / Export (renamed from Export) now lists Import .ora and Export .ora next to the existing options (both coming soon), and Resize shares its Scale / W×H / aspect-lock control with a new Layer-Settings “Canvas Size”.

  • fix

    The Selection-marker cursor no longer shows the move icon, and the canvas backdrop checkerboard now extends exactly 10px past the image and follows the light/dark theme.

  • rust

    Added a WASM panic hook (console_error_panic_hook) so a Rust panic surfaces a real message in the console instead of an opaque “unreachable”.

v0.9.3227 June 2026

Tool shelf reshuffle, drag-and-drop import, and a snappier histogram

  • feature

    Drag an image anywhere onto the app — or paste it (Ctrl+V) — and a full-window glow frames the window, then a dialog asks where it should go: its own new layer, on top of the image you're editing, or as a new image in the gallery.

  • ui

    Tool shelf reshuffle: “Edit and Move” is now “Edit and Transform”, and the Eraser moved out of Paint to the bottom of it. “Move” became “Layer Settings” — a Move-layer toggle (Ctrl+M) plus the magic-wand Selection marker, together in one place.

  • fix

    The histogram now drops cleanly when you switch photos and rises only once the new image is actually ready — no more flashing or stale graphs mid-load.

  • ui

    The download dialog’s main button now says exactly what it’ll do — “Download & Share JPEG/PNG/WEBP” — tracking your chosen format.

  • feature

    A little confetti: press Ctrl+\ for a popper celebrating the month’s shipped features (67 in June, 90 all-time). 🐎

v0.9.3127 June 2026

Non-destructive layer masks

  • feature

    Layer masks — hide or reveal parts of a layer without erasing anything. Add a mask from the Layers panel, then paint it: black hides, white brings it back. Fully reversible until you Apply it.

  • rust

    Masks live in Rust and reuse the existing brush engine (soft dabs, hardness, stroke stabilizer), so painting a mask feels exactly like the paint brush — and every composite, export, and thumbnail honours the mask.

  • feature

    Mask actions in the layer row: Invert (swap hidden/shown), Apply (bake it in permanently), and Remove. Merging and flattening bake masks in correctly.

  • ui

    Where a mask hides pixels you now see the transparency checkerboard through them, matching the eraser.

v0.9.3027 June 2026

An eraser, a softer brush, and a selection marker that finally stays put

  • feature

    New Eraser in the Paint tool — the sub-modes are now Paint · Blur · Pen · Eraser. The eraser scrubs the active layer back to transparent (revealing whatever is beneath it), with its own size, strength, and hardness; lower strength erases gradually.

  • feature

    The paint brush has a real Hardness control now (0–100%) — a crisp edge at the top, a soft skirt lower down — shared by the eraser.

  • rust

    Both the eraser and the brush-edge hardness live in Rust, reusing the same stroke engine the paint brush already used: soft dabs, true per-stroke opacity, and the lazy-mouse stabilizer.

  • ui

    The histogram now drops its bars to the floor when you switch photos and holds them down until the new photo is ready, then they rise into its shape — instead of flashing “no image”. Editing the same photo still morphs smoothly.

  • fix

    The magic-wand selection marker no longer drifts away when you zoom or pan — it now rides the exact same transform as the image. The marker is also a crisp marching-ants outline (drawn in Rust) instead of a flat blue wash that hid your pixels.

  • ui

    A transparency checkerboard now sits behind every image, so erased areas and transparent PNGs read as an empty grid right away instead of going black.

v0.9.2926 June 2026

Unified button system, share links, and a snappier, smarter placement grid

  • feature

    Share a read-only link to your image — Download → “Share link” uploads a snapshot and copies a public URL anyone can open (sign-in required).

  • ui

    One unified button system across the settings: Shapes, Pins, Arrows, Crop ratios, the Effects Quick-Adjust, the Edit & Move Transform actions, and the Download options are all icon-on-top tiles now, with a warm hover ring — all from one shared component.

  • feature

    The placement grid now drops a text or shape into the CENTER of the chosen ninth of the canvas (not jammed into a corner), computed in Rust as a single undo step. Numpad 1-9 maps to the nine cells.

  • perf

    Cold start no longer blocks on the sign-in service before showing anything — it boots and reveals the New / Welcome-back screen fast, with a capped fallback so a slow sign-in can’t hang the splash.

  • ui

    Download dialog is now “Download, Copy, or Share” with a checkbox-style format picker; the AI tools panel was trimmed of its walls of text, and Object Removal opens above the gallery.

v0.9.2826 June 2026

A full-page start experience and a 3×3 placement grid

  • ui

    Cold start is now one branded screen: the logo and a spinner settle in while the app checks for a saved session, then the logo eases upward and reveals either the New panel or “Welcome back” — decided before anything paints, so there’s no flash. The spinner always completes a turn, even on instant loads.

  • feature

    “Welcome back” is full-page too now, sharing that same logo-eases-up entrance: two thumbnails plus a “+N” tile and Resume / Start fresh.

  • ui

    The idle “paused to save power” screen got the same treatment, and the brand logo on all three screens is larger.

  • feature

    New 3×3 placement grid — nine buttons for the nine spots on the canvas (corners, edge-centers, center). It replaces the old alignment row and now lives in Text, Shapes, and the Batch editor; pick a text or shape and drop it into any anchor. Numpad 1-9 maps to the grid spatially.

  • fix

    Bézier pen: turning on a Background fill now previews live as you draw, instead of only appearing once the path is committed.

v0.9.2725 June 2026

Security tab, snapped-window layout, reduce-motion fixes, and a keyboard-accessibility pass

  • feature

    EXIF keep/strip moved out of the Compress panel into a new Settings → Security tab — and it’s now a saved preference, not a per-session toggle, applied to every export.

  • feature

    The “Welcome back” resume dialog was rebuilt on the shared modal (title in the header, the two actions in the footer), shows five thumbnails plus a “+N” tile, and its close ✕ now shakes instead of dismissing — you pick Resume or Start fresh.

  • feature

    Snapped / narrow windows: below ~900px the side panels stop crushing the canvas and float as overlay drawers (with a scrim, one open at a time); below ~600px a friendly “needs a wider window” notice appears. One shared breakpoint hook drives it all.

  • fix

    Reduce Motion now also stops the panel / canvas / gallery slide animations — those animate layout (margin/width), which had been slipping past the motion-reduction wrapper.

  • feature

    Keyboard-accessibility pass: a “Skip to canvas” link, landmark roles + labels on the toolbar / panels / canvas, real accessible names on the tool buttons, and Escape-to-close + dialog semantics on the modals.

v0.9.2624 June 2026

Text drop shadows, a magic-wand selection tool, and pen fixes

  • feature

    Text can now cast a soft drop shadow — on the letters, the background box, or both (Text → Background → Drop Shadow), with color, opacity, offset and blur. It’s saved with your edit and survives photo switches and reloads.

  • rust

    The shadow is rendered in Rust: the chosen silhouette is offset and Gaussian-ish blurred, painted behind everything, and the text tile grows to fit — so the Align tool’s bounding box includes the shadow too.

  • feature

    New Selection Marker (magic-wand) in the Edit & Move tool, just above Align: click a region to flood-select similar colors, then delete it. Alt+A selects everything, Alt+D deselects.

  • rust

    The selection flood-fill, the mask, and the delete all run in Rust; a translucent overlay shows exactly what’s selected.

  • feature

    The Batch Image Editor’s text overlay gained Bold (applied for real to every image) and a font-family picker.

  • fix

    Bézier pen: a Background color now fills any path — an open curve or a full circle — not just explicitly-closed ones. And committed pen paths now show up as “Pen Path” in the Reselect list.

v0.9.2524 June 2026

Faster engine, an Edit & Move tool, and safer deletes

  • perf

    Histograms now compute in Rust straight from the image buffer instead of re-sampling the canvas every time the picture changes — smoother, lighter, and much easier on your battery.

  • perf

    All the pixel blending — shapes, text, and layers — was rewritten to use fast integer math instead of slower floating-point. Same result, far less work per pixel.

  • feature

    “Crop & Transform” is now “Edit and Move”. Crop comes first, then Transform (flip / rotate), and a brand-new Align section with six buttons to snap a selected text or shape to any edge or the center of the canvas.

  • rust

    Aligning is computed in Rust (a new `align_annotation` export): it measures the object’s bounding box and moves it precisely, as a single undo step.

  • ui

    Cleaner panels — the Tools, Review, and Gallery headers dropped their titles and close buttons; the buttons themselves are the header now (close panels from the top bar). The Gallery keeps its photo count and all delete / select actions.

  • ui

    The Review panel gained a live Histogram section and switched its section switcher to compact icons.

  • feature

    Deleting a photo now asks first — the trash icons, the right-click “Delete image”, “Delete Selected”, and “Delete All” all confirm before anything is removed.

  • feature

    New Reduce Motion toggle (Settings → Appearance → Motion) minimizes panel slides and transitions for a calmer, faster interface — saved with your account.

  • fix

    The emoji picker now sits flush with the tools-panel edges.

v0.9.2424 June 2026

Rulers & grids — line up your edits with on-canvas guides

  • feature

    New Rulers & Grids settings (Settings → Rulers & Grids). Turn on top + left pixel rulers, and overlay a grid to line things up — choose a square pixel grid, golden-ratio guides, or split the image into any number of columns and rows. Pick the grid’s color and opacity too.

  • ui

    The guides sit over your photo without touching a single pixel — they track zoom and pan, and the ruler labels update as you zoom. Your settings are saved with your account.

  • rust

    The grid layout itself is computed in Rust (a new `grid_lines` WASM export) as the single source of truth, then drawn as a crisp SVG overlay — so every grid type lines up exactly with the image.

  • ui

    Alt+S now opens Settings (it used to rotate). Rotate is still a click away in the tools.

  • ui

    The Settings window has a consistent footer everywhere — your account button on the left, Restore / Apply on the right — across General, Appearance, Rulers & Grids, and Super User.

v0.9.2324 June 2026

Light mode — pick light, dark, or follow your system

  • feature

    Image Horse now has a full light theme alongside the original dark one. Choose Light, Dark, or System in Settings → Appearance — “System” follows your operating system and switches live the moment your OS does. Your choice is saved and synced to your account.

  • ui

    The whole app follows the theme — every panel, dialog, toast, the sign-in window, even the emoji picker — warm earth-tone in the dark, warm paper in the light. No flash of the wrong colors when the page loads.

  • ui

    Under the hood the entire UI moved onto one set of design tokens (color, elevation, radius, motion, and a z-index ladder), so the two themes stay perfectly in step and the interface can’t drift out of sync.

v0.9.2224 June 2026

A real Bézier pen — draw, fill, and re-edit vector paths

  • feature

    New Pen tool (Paint → Pen) — a Photoshop-style Bézier pen. Click to drop corner points, click-drag to pull smooth curve handles, and grab any point or handle to reshape the path as you go. Enter closes it, Esc leaves it open.

  • feature

    Give a path a background — flip the Pen panel’s background to Solid and a closed path fills its interior, under the stroke, curves and all.

  • feature

    Pen paths stay editable — click a finished path to re-open it with all its anchors and handles, drag to reshape, and it re-commits as a single undo step. Paths survive switching photos, reloads, and cloud sync.

  • ui

    Compress panel wording — the buttons now read “Compress Image” and “Compress All Images”, and “Compress All Images” hides when your gallery holds a single image.

  • ui

    Download dialog — the clipboard button reads “Clipboard Copy”, and the “All” button only shows when you have more than one image.

  • fix

    With the Pen active, the gallery and side panels stay clickable on tall images (the pen’s drawing layer no longer sits on top of the toolbar).

  • rust

    Under the hood: Bézier paths are a new Rust annotation kind — flattened with de Casteljau and filled with a scanline polygon fill — so they inherit history, re-edit, and persistence for free.

v0.9.2124 June 2026

Settings menu, pick-your-format downloads, and a hardening pass

  • feature

    New Settings menu — the gear by your avatar opens a tidy window with General (set your undo-history depth, and an idle screen) and Plan & Billing, all in one place.

  • feature

    Pick your format right in the Download dialog — a JPEG / PNG / WebP / AVIF card picker now lives in the box, so you can switch formats without hunting for the Compress dropdown. It stays in sync with the panel.

  • feature

    Tunable undo history — crank undo up to 1000 steps (or keep it lean) from Settings → General. More undo is yours for a little more memory.

  • feature

    Idle power-saver — leave a tab open a while and it dims to a “Continue with Image Horse” screen so your browser can throttle the tab and save battery. Your edits are kept; click to come right back.

  • ui

    Download dialog polish — a proper header and footer, and the “All” button only appears when you actually have more than one image. Export Selected now opens the same dialog so you can choose a format first.

  • fix

    Quieter, clearer errors — cloud-sync failures now surface in the Diagnostics window instead of vanishing, plus a stray image-reload fix and some debug-log cleanup under the hood.

v0.9.2024 June 2026

Stroke stabilizer, lettered pins, and a big UI consistency pass

  • feature

    Paint stroke stabilizer — turn on Low / Med / High smoothing and the brush tip trails the cursor on a leash, so quick jitters never reach the canvas (great for steady freehand lines). Off by default.

  • feature

    Pins can now be lettered — the Pins tool drops auto-sequenced callouts as numbers (1, 2, 3…) or letters (A, B, C…), each centered in its disc, sized by the stroke-width slider. Freehand pen was retired in favor of cleaner callouts.

  • feature

    Download chooser — one Download button now opens a tidy Selected / All / Cancel dialog when you have more than one image, and multi-image exports come down as a .zip.

  • ui

    The top-bar Upload button is now New (it also makes blank canvases), and its shortcut moved to Alt+N.

  • ui

    Toolbar refresh — the tool grid is calmer and more even: neutral tiles with only the active tool colored, a soft accent ring on hover, and sizes that scale cleanly at any width.

  • ui

    Across every settings panel: tighter, consistent spacing, unified slider and button controls, and pickers whose buttons all match size even when a label is long (so other languages won't break the grid).

  • ui

    Dialogs now match the rest of the app — same surface, the same little close button, and no stray focus ring.

  • fix

    Fixed a Firefox-only glitch where the canvas could turn to garbage after several brush strokes, and fixed the canvas/gallery drifting out of alignment when the toolbar was open.

  • fix

    Exported edits can keep or strip EXIF — a padlock in Compress lets photographers keep GPS/time/camera metadata or scrub it for privacy (was shipped just before this; now exposed everywhere export happens).

  • infra

    This Trail Log got a sticky month filter at the top so you can jump straight to a month.

v0.9.1923 June 2026

EXIF privacy control + a Current Image Meta panel

  • feature

    EXIF padlock on export — a lock toggle in the Compress panel decides what leaves your machine. Locked keeps your photo's metadata intact (GPS, capture time, camera and lens) — great for photographers — while unlocked strips it for privacy. It applies to Export, Export All, and Export Selected, and it closes a gap where an untouched original used to carry its GPS location into the exported ZIP.

  • feature

    Current Image Meta — a new tab in the Diagnostics Window (Alt+Delete) shows the live SHA-256 fingerprint of the current canvas (it changes with every edit), the original's SHA-256 content key, the image's dimensions and byte sizes, and its EXIF: camera, lens, capture time, exposure, and GPS as a one-click map link, with a heads-up when location is embedded.

  • ui

    The EXIF lock reuses the familiar padlock and badge styling from elsewhere in the app, with a two-line label that spells out exactly what's kept or removed on export.

v0.9.1823 June 2026

Diagnostics Window polish + a tidier shortcut menu

  • ui

    The Diagnostics Window (Alt+Delete) is now centered and taller, with a soft blur behind it, and both tabs — System Telemetry and Resources — are the same height. The event log scrolls on its own, and the count next to the tab is easier to read.

  • feature

    Alt+Delete now opens the Diagnostics Window for everyone, every time — no secret unlock required.

  • ui

    The keyboard-shortcut menu lists the Diagnostics Window under an always-visible Dev Tools section. The User / Tier Selector moved into a hidden Secret Menu that only appears — and only works — after you triple-click the status-bar button.

v0.9.1719 June 2026

Blur, pixelate & redaction tools + a Diagnostics Window

  • feature

    The blur brush now has three modes — Soften (Gaussian), Pixelate (a mosaic of big squares, adjustable block size), and Solid (paint an opaque color over something). Perfect for hiding faces, license plates, or sensitive text before you share an image.

  • feature

    Redaction boxes — drag a rectangle to cover an area with a solid color or a pixel mosaic. Because it's a real shape, you can reselect, move, resize, undo, and put it on its own layer, just like any other box.

  • rust

    Pixelate and redaction run entirely in Rust over the brushed (or boxed) region — grid-aligned mosaic averaging and opaque fill — so they stay fast and edit the active layer in place.

  • ui

    The Review panel header now shows the magnifying-glass icon, matching the Review button in the top bar (it used to show the history clock).

  • feature

    Diagnostics Window (Alt+Delete) — renamed, and now split into two tabs: System Telemetry (the event log) and Resources, a small htop-style view of FPS / main-thread load, JS memory, the WASM engine's memory, and what each subsystem is doing. Its backdrop is lighter now so your image stays visible behind it.

  • infra

    Security hardening — the AI (Replicate) webhook now verifies its signature and only pulls results from trusted hosts, upload URLs require sign-in, and subscription records can only be written by the verified billing webhook.

v0.9.1618 June 2026

Shape fill & gradients, sharper thumbnails, configurable stamps

  • feature

    Shapes can now be filled — give any rectangle or circle a solid background color or a two-color linear gradient, with From/To swatches and a direction picker (→ ↓ ↘ ↙). The outline draws on top, and the fill follows the shape when you reselect, move, or resize it.

  • rust

    Fill rendering lives entirely in Rust — a new fill_shape routine paints the solid color or per-pixel gradient under the stroke, threaded through the shape add/update/restore paths and the saved-edit format so fills round-trip through save and undo/redo.

  • ui

    The live drag preview shows the fill and gradient as you draw (via an SVG gradient), and the Fill controls reuse the app's existing swatch grid and button groups so they match the rest of the Shapes panel.

  • fix

    Reselecting a filled shape no longer swaps its fill — moving or resizing it keeps the color or gradient it was drawn with instead of picking up the panel's current setting.

  • ui

    The Review button now uses a magnifying-glass icon instead of the history clock, so it no longer looks identical to the History section.

  • rust

    Thumbnails are sharper and cleaner — downscaling now samples in linear light with premultiplied alpha, so midtones no longer darken and transparent edges no longer fringe with stray color.

  • rust

    The red rubber-stamp tilt is now a parameter (still −5° by default) instead of a hard-coded constant, ready to be made adjustable.

  • fix

    The crop-ratio helpers return a clear empty result on bad input instead of a silently-empty array, so a malformed call can't quietly produce a zero-size crop.

v0.9.1518 June 2026

AI tools go live (Replicate) + Stripe billing

  • feature

    Background Removal is live — one click runs rembg on Replicate and drops the cut-out straight back onto the canvas (paid tier).

  • feature

    Text Extract (OCR) — pull the text out of any image via Replicate OCR, shown in a copy-to-clipboard panel.

  • feature

    Object Removal — brush over an object in a mask painter and LaMa inpainting erases it and fills the gap; your image and mask are uploaded together.

  • infra

    AI pipeline: a Convex action dispatches each job to Replicate with a signed source frame and a completion webhook, pulls the result into Convex storage, and streams it back to the canvas. Text models persist their output as text.

  • feature

    Stripe billing — a Settings gear next to your avatar opens a Plan & Billing popup with the $10/mo Pro plan. Upgrade runs Stripe Checkout; subscribers get the Stripe Customer Portal to manage or cancel.

  • infra

    Billing backend: Checkout and Portal run through Convex actions, and a signature-verified Stripe webhook flips your account tier and records the subscription.

  • fix

    Signing in now creates your account record — a new hook upserts the Convex user row once you are authenticated, so tier, subscription, and AI access finally have something to read.

  • fix

    Oversized uploads can no longer crash the tab — images past ~100 MP are rejected with a toast before the full-resolution decode can blow past the WASM memory limit.

  • perf

    The anonymous-edit cleanup job now uses an indexed range scan instead of a full table scan, so it keeps reclaiming abandoned storage as data grows.

v0.9.1418 June 2026

Blank Canvas, gallery duplicate, hidden dev tools, faster uploads

  • feature

    Blank Canvas — start from scratch with a Photoshop-style New Document panel that slides in over the upload actions. Set width × height (default 1500 × 1000), pick a page-size preset (FHD, Square, Story, 4×6, 5×7, 8×10), and choose a background: white, black, any hex color, or fully transparent.

  • rust

    The blank canvas is generated entirely in Rust — a new blank_png fill-and-encode path produces the solid (or transparent) PNG with no browser <canvas> or toBlob round-trip, and the background color is parsed in Rust too.

  • feature

    Duplicate photos — select one or more in the gallery and hit Duplicate; each copy lands right after its original and carries over its edits. Because originals are content-addressed, duplicating copies zero pixels.

  • perf

    Uploads now decode once instead of twice — the gallery thumbnail is built from the already-decoded working image (downscaled in Rust) rather than decoding the source file a second time.

  • feature

    Hidden Dev Tools — three clicks on a tiny unlabeled spot in the status bar unlock the diagnostics log and the tier/user selector (and list them in the shortcuts sheet), now reachable in production builds, not just dev.

  • ui

    Shortcuts reshuffled — Tools is now Alt+T, the Review panel is Alt+R, and Rotate 90° is Alt+S. The Alt+/ reference and the top-bar tooltips were updated to match.

  • fix

    Spacebar pan works again after clicking a tool — last release's keyboard-activation change let a mouse-focused button swallow Space; it now only defers Space to keyboard-focused (Tab) controls, so hold-Space-to-pan is back.

  • ui

    Gallery counter reads cleaner — "3 of 3 — 12 max" normally and "Selected: 2 of 3" while selecting, with an (i) that explains the per-tier photo limits (logged out 12, logged in 24, paid 100).

  • fix

    Delete All dialog — the buttons now match the rest of the app, and the Cancel button's text no longer disappears on hover (a duplicated theme token was painting dark-on-dark for every outline button).

  • ui

    Upload dialog refresh — actions reordered (Browse / Paste, then Sample Images / Blank Canvas), the sign-in icon moved to the top-left corner, and the drag-and-drop area is now a dotted drop zone that highlights and nudges when you drag an image over it. "Test Images" is now "Sample Images", and the footer links out to the live site, GitHub, and Codeberg.

  • ui

    The Auto-Compress progress toast finally spans the full width — its text and progress bar now fill the toast edge-to-edge instead of bunching up in the left third.

  • ui

    The four panel toggles in the top bar are now properly centered on the bar, and the Review panel header matches the Toolbar and Gallery headers (icon + same type).

v0.9.1317 June 2026

Photoshop-style layers — per-layer tools, compositing, clipboard paste

  • feature

    Layers are live. Add, duplicate, reorder, show/hide, set per-layer opacity, merge down, and flatten — every canvas tool (paint, clone stamp, blur, brightness/contrast, text, shapes, emoji, paste) now edits the active layer, and the canvas shows all visible layers composited bottom-to-top. v1 ships opacity + visibility (normal blending). Gated to logged-in / paid tiers.

  • rust

    The WASM core is no longer a single pixel buffer — ImageHorseTool holds a Vec<Layer> stack plus an active-layer index, and each layer owns its own pixels and its own text/shape overlays. New layer API (add / duplicate / remove / set_active / move / merge_down / flatten_all / visibility / opacity / get_layers) plus a source-over compositor with a reused cache and a single-opaque-layer fast path. Export and thumbnails composite the whole stack, so the saved image always matches the screen.

  • rust

    Undo/redo now snapshots the entire layer stack, so adding, deleting, reordering, and merging layers are all undoable alongside ordinary pixel edits. Jump-to-history became an undo/redo loop and the clone-stamp engine takes a pre-built snapshot.

  • feature

    Paste an image straight from the clipboard (Ctrl/Cmd+V) into the active layer, centered, as one undoable step — guarded so it doesn't collide with the upload dialog's paste-as-new-photo.

  • infra

    Persistence v5 — the IndexedDB save and the Convex binary archive now serialize the full layer stack (per layer: pixels, name, visibility, opacity, and its own text/shape overlays) plus the active layer id. Reopening a photo rebuilds the stack; v1–v4 archives still load and collapse to a single layer. (Undo history past a reload still restores as the flattened image — a follow-up.)

  • ui

    Layers panel in the Review sidebar — the old Coming Soon placeholder is now a working stack list (top→bottom) with a visibility eye, inline rename, reorder, duplicate, merge-down, delete, and a per-layer opacity slider. Tier-gated, with a lock state for the demo tier.

  • ui

    New extra-small icon-button variant powers the dense layer-row controls — always-visible background, hover ring, light icon — while the eye keeps its own open/closed swap. Less variant sprawl, consistent feel.

  • fix

    The layer-count badge showed the tier limit instead of the actual number of layers; it now shows the real count, with the limit in the tooltip.

  • fix

    Keyboard accessibility — Tab to a button and press Space or Enter and it now activates. The global spacebar-pan handler was swallowing Space for any focused control; it now defers to buttons, links, and ARIA widgets.

  • fix

    Editing a text box no longer shows a ghosted second copy underneath the editor — the baked tile is suppressed while the textarea overlay is open, mirroring how shapes already behave.

  • fix

    Text rotation lands true. The editing overlay now rotates around the same pivot the Rust tile bakes to, and the baked tile's rotation direction was flipped to match the clockwise preview — a +90° rotate was previously coming out as −90°.

  • ui

    The keyboard-shortcuts reference (Alt+/) now lays each section out in two columns, and lists Alt+Delete to toggle the Diagnostics Log.

v0.9.1216 June 2026

Review panel, 360° bubble tails, dev tier switcher

  • feature

    AI Tools: Background Removal is live. One click hands the canvas PNG to a real Convex → Replicate pipeline (rembg model), with a phase-state button (Uploading… / Removing background…) and a reactive Convex subscription on the job row. When the webhook completes, the result image streams back, decodes to RGBA, and replaces the working image — the photo is marked modified so the change persists. Gated to the Paid tier; non-Paid users see an inline Lock notice. Other AI models (OCR, 4× Upscale, Object Removal, Alt Text) remain Coming Soon placeholders awaiting the same plumbing.

  • ui

    Auto Compress split into explicit Selected / All buttons. A centred ⚡ Auto Compress label sits over a two-button row — Selected Image (or Selected Images when more than one is checkbox-selected) and All Images — followed by an HR separator and then Apply Compression & Resize and Show A/B Compare. Selected scope compresses the checkbox multi-selection when one exists, otherwise just the active photo in the ring, so the button is always meaningful.

  • feature

    New Pens tab in the Shapes tool — sits between Shapes and Arrows. Pins mode drops auto-numbered callout discs (1, 2, 3…) on click with a Pin Size slider; click an existing pin to move it. Freehand mode draws a thick, round-capped polyline pen stroke on drag with its own Stroke Width slider. Both share the colour swatch.

  • rust

    Pens are real Rust shapes — two new kinds (5 = pin, 6 = polyline) added to ShapeAnnotation, with add_pin_annotation / add_polyline_annotation APIs, render_pin (filled AA disc + centred ab_glyph number), drawing::draw_polyline (round-capped segment loop), and drawing::fill_circle. ShapeAnnotation gained number (pin label) and points (polyline vertices); get_shape_annotations JSON, PersistedShape, and the restore path round-trip both. Polyline hit-testing uses per-segment distance; pins reuse the padded-bbox path. The live freehand preview is drawn in JS during the drag and committed to Rust on mouseup.

  • feature

    History panel rebuilt as a Review panel — one collapsible panel hosting three independent sections: History (undo timeline), Reselect (live text and shape annotations), and Layers (placeholder). Open any combination; the body splits evenly between open sections — 1 full, 2 halves, 3 thirds — each with its own scroll area and header.

  • ui

    Shared ToggleButtonGroup component drives the top bar's Upload / Tools / Gallery / Review cluster and the Review panel's History / Reselect / Layers cluster. Multi-select (each button independent), compact icon mode, label-only mode for narrow panels, evenly-spread fill option.

  • rust

    Speech-bubble tail is now a 360° angle, not five discrete directions. Drag the Tail Direction slider and the tail sweeps continuously around the bubble. Rust builds the tile with a uniform margin on all sides and projects a ray from the bubble center onto the rect edge to place the tail base; live preview uses identical math.

  • ui

    Compress is the first tab in the Resize tool — the panel now opens on Compress (the more common starting point), and the toolbar tooltip reads Compress & Resize.

  • ui

    Text panel's second tab renamed Background (was Text Background); the Background Color / Padding / etc. labels carry the rest of the context. Corner Radius simplified to three presets — Square / Rounded / Circle — so the bubble tail stays flush at any radius.

  • infra

    Centralized tier config — new lib/tiers.ts is the one place per-tier capabilities live (gallery cap, storage quota, layers per image, AI runs per day), keyed by tier. Components read from TIERS[mode] instead of hardcoding numbers. Mirrors the public Pricing matrix.

  • feature

    Dev tier switcher — Alt+L opens a small dialog to flip between No Login / Free / Paid tier modes for testing, shown only in dev builds.

  • feature

    Gallery Unselect button — when one or more photos are selected, a new Unselect button appears in the gallery header alongside Export Selected, Delete Selected, and Delete All. One click clears every checkbox.

  • fix

    Modified-dot race — clicking an unedited photo no longer briefly flashes the white modified-edit dot on it. The dot effect was attributing the outgoing photo's lingering undo count to the incoming selection; now it gates on the loading flag, which is set before any await.

  • ui

    Crop tool spacing — the Transform heading sits tighter against the Flip H / Flip V / Rotate buttons, matching the rhythm Ratio uses for its ratio buttons in the same panel.

  • rust

    drawing::rounded_rect_coverage, triangle_coverage, and blend_coverage — three public coverage helpers in src/drawing.rs that produce per-pixel α for AA rounded rects and triangles, with Porter-Duff source-over compositing. Foundation for further shape-edge AA work and the bubble-tail flushness fix.

  • ui

    Trail page renamed Trail Log — the URL is /trail-log, the nav and footer labels and the page eyebrow all read Trail Log.

v0.9.1113 June 2026

Shapes & arrows go live — reselect, move, resize, delete

  • feature

    Every shape (rect, circle, hand-drawn circle, line) and both arrow styles now commit as a live ShapeAnnotation instead of rasterizing immediately — same non-destructive overlay treatment text already had. Click a committed shape to re-select it on the canvas; drag the body to move; drag corner squares to resize; drag endpoint circles to re-angle lines and arrows.

  • feature

    Reselect panel — the right-side History panel grew a Reselect list of every live text and shape annotation. Click a row to jump the canvas selection to it; trash icon removes it. The old TextSettings Recent texts list moved here so all live overlays share one home.

  • rust

    Eight new wasm-bindgen exports drive the shape system: add / update / remove / restore / hit-test / count / set_editing_shape / get_shape_annotations — the same shape as the text annotation surface.

  • rust

    History snapshots now carry shape annotations alongside text annotations — undo/redo swap both lists in lockstep so reselect-and-edit is a normal undo entry.

  • infra

    Persistence v4 — the IDB SavedEdit and the Convex binary archive both serialize the shape annotation vec; reopening a photo restores every live overlay. v1–v3 still decode for back-compat.

  • fix

    Text rotate handle — the drag math used a stale center reference when the box was already rotated, drifting the angle on each adjustment. Smooth rotation now holds.

  • rust

    Stamp dab f32 polish — extends the June hot-loop pass to the dab kernel's edge case, removing a residual f64 cast in the inner loop.

v0.9.1012 June 2026

June optimization pass — faster Rust, smaller WASM, Squoosh-style resize, shape edit boxes

  • perf

    WASM binary down 60% — 1.10 MB to 443 KB — by subsetting the embedded Liberation Sans fonts to Latin-1 + Extended-A, plus a sweep of Rust hot-loop optimizations (f32 math, opaque-source fast paths, cached blur kernels, VecDeque undo stack).

  • perf

    Zero-copy canvas painting — the display blit reads WASM linear memory directly instead of cloning the full pixel buffer every frame, eliminating ~1 GB/s of allocator churn during brush strokes.

  • rust

    Three resampling filters join bilinear: Lanczos3 (default), Catmull-Rom, and Nearest — separable two-pass with minification-aware kernels, selectable from the new Method dropdown.

  • feature

    Squoosh-style Resize panel — Scale % slider, Dimensions with aspect lock, then a Compress section grouping Method, Format (moved out of the top bar), and Quality.

  • feature

    Apply Compression & Resize re-encodes at the chosen format and quality, swaps the stored file, and updates the status-bar size, dimensions, and gallery tooltip in place.

  • feature

    PageSpeed Insights score (renamed from Lighthouse) now models Google's real image audits: next-gen format ratios (WebP/AVIF score higher) and an oversize penalty past 1920px.

  • feature

    Shape & Arrow edit boxes — all four shapes and both arrow styles drop into a Figma-style overlay with resize squares, a move handle, and per-endpoint circles for lines/arrows. Commit still rasterizes in Rust, one history snapshot per shape.

  • feature

    Export gating — single Export disabled until the active photo has edits; Export All needs two or more photos with at least one edited, each with a tooltip explaining why.

  • fix

    A/B Compare overhauled — unlocks on any pending panel change, always compares against the immutable upload original, and the overlay now tracks zoom and pan transforms instead of drifting.

  • fix

    Levels blur slider — JS was sending a 0–1 fraction into Rust's u32 intensity (truncated to 0 → invisible blur); now mapped to the 1–30 kernel range.

  • fix

    Many small fixes — eyedropper added to the right-click menu, Delete-All canvas ghost cleared, sidebar tooltip copy refreshed, upload-dialog link styled as a LargeButton.

  • ui

    Keyboard accessibility pass — focus-visible accent ring (was globally suppressed), tabbable gallery thumbs and history entries, upload dialog focuses Browse Files on open and closes on Escape.

  • ui

    Architecture page returns — the full backend diagram (client → WASM → auth → API → storage/Convex/AI) rebuilt and re-linked in the nav and footer; pricing details stay on the Pricing section.

  • ui

    GitHub and Codeberg buttons in the nav beside Beta Version — the source lives on both forges.

  • infra

    UploadThing now also hosts the demo's Test Images set — the royalty-free photos behind the upload dialog's Test Images button.

  • infra

    Dead code removed per fallow: five unused modules, stale exports, and the unused autoprefixer dependency.

v0.9.912 June 2026

Non-destructive text + speech bubbles + AI panel

  • feature

    Live text annotations — text never commits to canvas pixels until export. Click an existing text on the canvas to re-open the input box with its content, font, color, and rotation; submit updates in place.

  • feature

    Text Background tab — add a rounded rectangle behind any text, or wrap it in a speech bubble with a tail (5 directions). Background color, padding, corner radius, and opacity sliders.

  • rust

    Eight new wasm-bindgen exports drive the annotation system: add / update / remove / hit-test / count / get-json / render-with / flatten — display always blits through render_with_annotations when count > 0.

  • rust

    drawing::fill_rounded_rect — anti-aliased rounded fill via per-pixel distance test; drawing::fill_triangle_public renders speech-bubble tails. The whole bubble rotates together (not just the text).

  • ui

    Line-and-dot handles — stem + filled circle for both move (native cursor) and rotate (custom data-URI SVG cursor with stacked black-outer + white-inner strokes for visibility on any background).

  • ui

    Sticky text input — the editing box no longer closes when you click a color swatch or font dropdown. Document-level pointerdown listener only commits on clicks truly outside the editing surface.

  • feature

    Text Extract removed and repositioned — Tesseract.js dependency dropped, Rust extract_region_png removed. The feature now lives in a new AI tool panel as a Coming Soon card alongside Background Removal, 4× Upscale, and Object Removal.

  • ui

    Unified ColorSwatchGrid — Paint, Shapes, Arrows, and Text Background tabs all share the same palette and custom-colors list (localStorage-persisted via useUserColors, parsed by Rust parse_color).

  • infra

    Annotation persistence v2 — IDB SavedEdit gained an annotations field, and the Convex binary archive bumped to v2 with trailing JSON (v1 still decodes for back-compat).

  • feature

    Gallery multi-select — checkboxes appear on hover and stay visible once at least one is selected; a Delete N selected action surfaces in the gallery header.

v0.9.812 June 2026

Smart export, batch text, and a unified button system

  • feature

    Smart Export All — each photo exports its processed result (edits, compression, or resize re-encoded at the chosen format + quality), or the untouched original when nothing changed.

  • feature

    Batch Text is live — type once and stamp text onto every photo in the gallery; rendered in Rust with the embedded font, and the active photo stays undoable.

  • feature

    Logo replace, not stack — re-applying the batch logo swaps the previous one instead of layering a second logo on top.

  • feature

    Test Free Images — load a set of 12 royalty-free sample photos straight into the editor to try things out.

  • feature

    Per-photo file size now shown in the status bar, beside the dimensions, and updates after Auto Compress.

  • rust

    Byte-aware Lighthouse score — a Rust web-performance model (log-normal curve) powers the Resize panel's Web Performance Gain and Lighthouse readouts.

  • rust

    Batch text and Export All compositing run through Rust (measure_text / commit_text and encode_png_pixels on a throwaway ImageHorseTool).

  • ui

    Unified button system — new LargeButton and TinyButton primitives give every action and icon button one consistent look (export, apply, compress, delete, close, user).

  • ui

    Status bar refresh — rotating shortcut hints with the active tool swapped in and Alt+/ pinned.

  • ui

    Auto Compress progress now appears as a toast with a progress bar instead of an inline toolbar bar.

  • ui

    Gallery polish — Delete All in the header, trash-can remove buttons, and a hover tooltip showing each photo's name, size, and dimensions.

  • ui

    Responsive layout under 1000px — the top bar collapses to icons and the toolbar slims down with smaller tool icons.

  • ui

    Clerk sign-in now uses a dark theme that matches the editor.

May 2026

8 features · 4 releases · 12 commits · 3 fixes

  • v0.9.7Batch Image Editor + grid mosaic
  • v0.9.6Image Horse rename + originals store
  • v0.9.5Text rotation + Convex archive format
  • v0.9.4Tabbed tool panels
v0.9.727 May 2026

Batch Image Editor + grid mosaic

  • feature

    Batch Image Editor — tool renamed from "Images"; real panel with Logo / Text tab toggle and a grid mosaic of the gallery.

  • feature

    Bulk logo stamp — pick a PNG/JPG/WebP/SVG, choose corner + size + opacity + margin, apply to every photo in one pass. Active photo gets an undo entry; others persist directly to IDB.

  • feature

    SVG logo support — rasterized via <img> + OffscreenCanvas with a 512×512 fallback when the SVG omits intrinsic dimensions.

  • mock

    Batch Text overlay — mock UI in place (textarea, font, color, position, opacity); apply button shows a Coming Soon badge.

  • feature

    Grid canvas mode — when Batch Image Editor is active, the canvas becomes a 5×3 mosaic; selected photo is a 2×2 hero tile, surrounded by up to 11 clickable thumbnails. Caps at 12 visible with a +N more badge.

  • ui

    Selected indicator — orange ring + pill badge on the hero tile; placeholder overlay shows when no photo is loaded.

  • fix

    Auto-select first photo — keeps the hero populated after session restore or photo deletion.

  • fix

    Canvas survives container resize — flushToCanvas re-blits the WASM buffer via a ResizeObserver. Fixes the blank-hero bug when switching tools.

  • rust

    composite_pixels — stateless RGBA alpha-compositing exposed as a free wasm-bindgen function.

  • rust

    resize_pixels — stateless bilinear resize. Batch logo scaling moves from OffscreenCanvas to Rust.

  • rust

    encode_png_pixels — stateless PNG encoding. Batched photo outputs skip canvas.convertToBlob entirely.

  • ui

    Marketing link in upload dialog footer; darker .checkerboard-dark variant for the grid surround.

v0.9.615 May 2026

Image Horse rename + originals store

  • feature

    App renamed Image Horse — was Clone Stamp App; WASM struct renamed CloneStampTool → ImageHorseTool.

  • feature

    Content-addressed originals — SHA-256-keyed IndexedDB store; originals survive photo switching and page reload at full resolution.

  • perf

    Working copies — uploads downscaled to ≤2048px long edge via createImageBitmap; 256px WebP thumbnails generated in parallel.

  • fix

    CompareSlider alignment — overlay tracks the canvas bounding box via ResizeObserver; before/after layers share one coordinate space through zoom and pan.

  • perf

    Compare URL on demand — originalUrl populated only when compare activates; revoked on cleanup.

  • ui

    Apply Resize and Quality — button renamed; disabled until width, height, or quality actually changes.

v0.9.514 May 2026

Text rotation + Convex archive format

  • feature

    Text rotate handle — SVG rotate circle above the text box; drag to rotate before committing.

  • ui

    ColorSwatchGrid — shared color swatch grid used across brush, text, arrow, and shape settings.

  • ui

    StatusBar auth mode — shows Demo or Signed In badge based on Clerk state.

  • perf

    Binary archive format for Convex edit history — canvas + undo/redo stack serialized as a compact binary archive instead of per-snapshot file uploads.

  • infra

    session_edits Convex table — 3-day expiry cron cleans up stale edits automatically.

v0.9.413 May 2026

Tabbed tool panels

  • ui

    Stamp tool — 3-tab panel (Clone / Stamps / Emojis); emojis tab houses the full @emoji-mart picker.

  • feature

    Emoji tool → Images — toolbar tool renamed (now Batch Image Editor in v0.9.7).

  • ui

    Shapes tool — Shapes / Arrows tab switcher; Arrows tab now shows full arrow settings.

  • infra

    Dual persistence — useEditPersistence routes canvas saves to Convex file storage (signed in) or IndexedDB (anonymous); useRecentTexts mirrors the same pattern.

April 2026

10 features · 3 releases · 10 commits · 4 fixes

  • v0.9.3Color picker + font family
  • v0.9.1Convex schema + pan/zoom polish
  • v0.9.2Per-photo persistence + Netlify fix
v0.9.323 April 2026

Color picker + font family

  • feature

    Brush tool split into Paint / Blur Brush tabs; canvas mouse routing controlled by sub-mode.

  • feature

    Effects tool — Levels (brightness/contrast) and Color Picker tabs.

  • feature

    Color picker pixel magnifier — WASM get_pixel_region returns 11×11 RGBA grid; floating canvas magnifier follows the cursor.

  • feature

    Font family selector — 12 browser-safe fonts; applied to the canvas text overlay, persisted in TextMemory so re-editing restores it.

  • feature

    Export All shortcut — Alt + Shift + E triggers ZIP export of all photos.

v0.9.222 April 2026

Per-photo persistence + Netlify fix

  • feature

    Per-photo edit persistence — full WASM canvas + undo/redo stack saved to IndexedDB (PNG-encoded per snapshot) when switching photos. Switching back restores the exact session.

  • fix

    Clone stamp alpha compositing — Porter-Duff source-over; stroke_src_data frozen buffer prevents feedback artifacts.

  • perf

    Paint dab compositing — squared-distance circle rejection replaces sqrt in the hot loop.

  • fix

    Crop OOB clamp — boundary guard prevents out-of-bounds read on zero-area crops.

  • fix

    Netlify build fix — removed --out-dir from wasm-pack; app/pkg is a symlink.

  • fix

    Modified-photo dot — race condition fixed; dot only appears after actual brush/tool edits.

v0.9.111 April 2026

Convex schema + pan/zoom polish

  • infra

    Convex DB + auth schema — userProfiles, projects, images, layers, annotations, history, ai_jobs, subscriptions tables defined.

  • feature

    Spacebar pan — Photoshop-style hand tool; all tool handlers bypassed during pan.

  • feature

    Alt+Scroll zoom — composes with pan offset; listener moved to window for reliable mounting.

  • feature

    PgUp / PgDn — cycle through gallery photos.

  • ui

    Blur Brush moved into the Effects panel alongside brightness + contrast.

  • feature

    Crop SVG overlay — rule-of-thirds guides and 8 draggable resize handles.

March 2026

4 features · 4 releases · 22 commits · 0 fixes

  • v0.3.0Crop, resize, A/B compare arrive
  • v0.5.0Image Horse identity + Convex foundation + brightness in Rust
v0.5.020 March 2026

Image Horse identity + Convex foundation + brightness in Rust

  • ui

    App branded Image Horse — new horse logo SVG and product name across the editor and the README.

  • infra

    Convex database integration begins — initial schema and auth wiring for projects, images, and user profiles.

  • rust

    Brightness and contrast moved into Rust for instant, allocation-free adjustments.

  • feature

    Red stamp tool — REJECTED / APPROVED / DRAFT / CONFIDENTIAL presets rendered as bordered, slightly-rotated labels.

  • ui

    Gallery polish — photo strip with smooth loading effects and tighter thumbnail layout.

v0.4.018 March 2026

Rust migration of drawing tools

  • rust

    Blur moved to Rust — Gaussian separable two-pass with brush-radius region masking.

  • rust

    Arrows and shapes drawn entirely in WASM — anti-aliased lines, rendered arrowheads, rectangles and circles in pixel space.

  • rust

    Paint and emoji tools composite through Rust pixel pipelines instead of canvas drawImage.

  • rust

    Text rendering in Rust with the embedded Liberation Sans font — no browser font round-trip.

  • rust

    Resize migrated to a Rust bilinear pass, replacing canvas-based scaling.

  • infra

    Architecture.md added — documents the WASM + React + Convex layers and why one binary shares one pixel buffer.

v0.3.016 March 2026

Crop, resize, A/B compare arrive

  • feature

    Crop tool with an interactive selection rectangle.

  • feature

    Resize & Compress controls — first version of the panel with width / height / quality.

  • feature

    A/B Resize Bar — Squoosh-style before/after divider lets you eyeball compression damage.

  • ui

    Animations layered over panel transitions via Framer Motion springs.

  • ui

    Styling polish across the editor — consistent spacing, hover states, and dark surface palette.

v0.2.015 March 2026

Layout merge + foundation cleanup

  • infra

    Repo merge brings in the Yet Another Photo App layout — proper top bar, sidebar, status bar, and gallery placeholder.

  • infra

    .gitignore added for node_modules and dist; package resolution errors resolved.

  • ui

    First real multi-panel layout — the structure all later tools dock into.

February 2026

1 feature · 1 release · 2 commits · 1 fix

  • v0.1.0Initial React conversion
v0.1.025 February 2026

Initial React conversion

  • feature

    HTML / JS prototype converted to a React + Vite + TypeScript project — the scaffold the rest of the app grows on.

  • ui

    Status bar added at the bottom of the editor — first version, before it learned to rotate shortcut hints.

  • fix

    Zoom controls fixed and working through the new React layout.