Merge Aseprite's layout and icons with Catppuccin's colours - #2
Merged
Merged
Conversation
The references are Aseprite themed with Catppuccin - the file in them is called catppuccin.ase - so this is two separable things, and worth naming which is which. The metrics are measured off Aseprite: a 12px menu bar, an 11px tab, a 17px tool options row, 15px tool slots in an 18px column, an 80px timeline, a 15px status bar, all at 1x with the interface magnified twice on screen. The colours are Catppuccin, which is a theme someone put on Aseprite rather than Aseprite's own. They arrived as lossy WebP: thirty thousand colours in a screenshot of an interface that uses about thirty. Nothing can be recovered from that the way Picotron's palette was recovered, because the damage is not a function - the same true colour comes back differently depending on what surrounds it. It does not have to be recovered, only identified. Catppuccin is published, and five of its values survive the compression at distance nought - #1e1e2e, #eff1f5, #dce0e8, #fe640b, #a6e3a1 - with the rest landing within 1 to 3. What was uncertain was which palette it is, not what the palette contains. The framebuffer target is a parameter now rather than 270. Aseprite's chrome fills 960x540 at 1x and the references magnify it twice; Picotron's filled 480x270. Hard-coding either renders the other at half or double its intended density, and both themes are kept. The control icons are redrawn at 16x16 with two tones. Aseprite's are not flat silhouettes - the pencil has a lit body and a dark tip - and the second tone is half alpha in the same sheet, so tinting multiplies it down to a shade and one sheet still serves every theme and state. Four of them were redrawn twice: the first bucket read as a diamond and the first eraser as a second pencil, which only rendering the sheet showed. The linter now refuses a raw hex in a theme that claims a palette. Forty hand-picked colours that only nearly agree is most of what "messy and inconsistent" meant, and a colour two off a real entry looks fine alone and wrong beside everything else. Both listed exceptions are measured facts: Aseprite paints its transparency checker itself, in the same two greys, under both the light and the dark reference. 121 assertions pass, the linter is clean, and Picotron's eight pixel tiles still match - that theme stays first-class and verified, it is just no longer the default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QF46RxyogNPLNX7DajyKM
…rstated
The left panel now runs palette, saturation-value field, hue strip, foreground
and background chips - Aseprite's order, and the right way round: the palette
is what you use constantly and belongs where the eye lands, the picker is for
when the palette has not got the colour, the chips record the last two
decisions.
This is the first thing in Studio that constructs a colour rather than choosing
one from a fixed set, so it is the first that needs HSV. The conversion is a
pure function with tests on all six sector boundaries and on the clamping,
which matters because a drag that runs off the field hands back a component
slightly out of range and unclamped that wraps 256 to black - the picker would
appear to break exactly when the pointer left it.
The field is cached in an off-screen Target. Drawn per pixel per frame it is
six thousand rectangles sixty times a second to produce an image that only
changes when the hue does.
Two rendering faults, neither visible in the code. The Target blit came out a
near-black smear because a blit is multiplied by the current draw colour and
the last thing set was the well's own background. And the marker ring hung two
pixels above the gradient, because full saturation and value puts its centre on
the top-right pixel - which is the default, so it was wrong on first sight.
The important part is the papercut. I described it in an earlier commit as "a
local whose name matches a method of the same class shadows that method for the
whole call". That is wrong twice over, and the narrow rule let a second
instance straight through. Reduced:
class Probe {
gap() { return 7 }
first() { gap = 99; return gap }
second() { return this.gap() }
}
p = new Probe(); p.second(); p.first(); p.second()
prints 7, then raises. A local does not shadow a method for a call - it
destroys that method on the object permanently, from any other method, and the
same call works before the poisoning line runs and fails after. It is
time-dependent, so testing cannot be relied on to surface it.
The linter now flags any local sharing a name with any method of its class,
which found the one this rule was rewritten for and one more: fillRounded
reassigned its `radius` parameter, which would have destroyed painter.radius()
- called by Window and Colorbar for their corners - for the rest of the
session. It is latent rather than live only because the Picotron rebuild left
fillRounded with no callers.
132 assertions pass, the linter is clean, Picotron's eight tiles still match.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QF46RxyogNPLNX7DajyKM
…ld them Each layer row now carries a visibility eye and a lock, to the left of the name rather than after it - Aseprite's order, and the useful one, because the eye is what gets clicked most and belongs where the pointer already is instead of after a name of unknown length. Both are drawn dim when off rather than hidden: a control that vanishes when inactive cannot be turned back on by anyone who has not already learnt it is there. Adding them reintroduced, exactly, the fault that made the old Picotron icon sheet unusable - an icon taller than the row containing it. A 16px eye in a 12px slot drew four pixels wider than its box and overlapped the lock beside it. So a timeline row is now derived from the icon rather than from the text row, and the frame header and cell grid derive from the same number so they stay aligned with the layer rows instead of drifting by four pixels each. Knowing the shape of that bug from the last rebuild did not stop me writing it again; only rendering it did. The menus are deliberately left as File, Edit, View and Tools rather than matched to Aseprite's eight. There are no Sprite, Layer, Frame or Select commands to put in them, and a menu that opens onto nothing is worse than one that is not there. 132 assertions pass, the linter is clean, Picotron's eight tiles still match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QF46RxyogNPLNX7DajyKM
tools/make-icons.py holds the art as ASCII so that changing an icon is a readable diff rather than a binary blob nobody can review, and the PNGs under resources/ are build output. That is only true while the two agree. A hand-edited PNG would quietly become the real source and leave the ASCII a decorative lie that still looks authoritative in review. So CI regenerates and fails on any difference. Verified by committing a PNG with one pixel poked and watching the check go red - the first attempt at that test was wrong, because poking the working file and regenerating simply overwrote the poke, which proves nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QF46RxyogNPLNX7DajyKM
kaidesu
marked this pull request as ready for review
September 3, 2026 00:57
kaidesu
pushed a commit
that referenced
this pull request
Sep 4, 2026
PR #2 was squash-merged, so its four commits are not ancestors of main even though their content is. Merging the old branch head brings the history forward without changing a single file - git diff between them is empty - which lets this push fast-forward instead of needing a force. # Conflicts: # .github/workflows/ci.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on the Picotron rebuild rather than replacing it: the framebuffer, painter primitives, widget tree and pixel-verification tooling all stay, retargeted.
The references are two things, not one
They are Aseprite themed with Catppuccin — the file in them is called
catppuccin.ase— so this PR takes the metrics and layout from Aseprite and the colours from Catppuccin. Worth separating, because Aseprite's own stock theme looks nothing like this and swapping to it later is a palette change, not a rewrite.Lossy input, and why it did not matter
They arrived as lossy WebP: 32,000 colours in a screenshot of an interface that uses about thirty. Nothing can be recovered from that the way Picotron's palette was recovered from PNGs — the damage is not a function, so the same true colour comes back differently depending on what surrounds it, and it cannot be inverted.
It does not need recovering, only identifying. Catppuccin's values are published, and five survive the compression at distance 0.0 —
#1e1e2e,#eff1f5,#dce0e8,#fe640b,#a6e3a1— with the rest landing within 1–3. What was uncertain was which palette it is, not what the palette contains.Both flavours ship: Mocha (dark, default) and Latte (light). They share every token name, because Catppuccin runs its greys in opposite directions —
textis darkest in Latte and lightest in Mocha — so "text on base" is legible in both with no conditional. Latte is a palette swap, not a second theme.Measured off Aseprite
The framebuffer target is a parameter now rather than a constant. Aseprite's chrome fills 960×540; Picotron's filled 480×270. Hard-coding either renders the other at half or double its intended density, and both themes are kept — Picotron's eight pixel tiles still match exactly, because
verify.shrenders that theme explicitly.Icons redrawn at 16×16, two-tone
Aseprite's tool icons are not flat silhouettes — the pencil has a lit body and a dark tip. The second tone is half alpha in the same sheet: tinting multiplies, so it lands as the tint at 50% over the ground. One sheet, one draw call, no second colour threaded through the theme, and it still recolours per flavour.
Four were redrawn twice — the first bucket read as a diamond and the first eraser as a second pencil. Only rendering the sheet showed that.
A colour picker
The left panel now runs palette → saturation-value field → hue strip → foreground/background chips, which is Aseprite's order and the right way round. This is the first thing in Studio that constructs a colour rather than choosing from a fixed set, so it is the first that needs HSV — a pure function, tested on all six sector boundaries and on clamping, because a drag off the field returns a component slightly out of range and unclamped that wraps 256 to black.
The field is cached in an off-screen
Target. Drawn per pixel per frame it is six thousand rectangles, sixty times a second, for an image that only changes when the hue does.A Ghost papercut I had documented wrongly
An earlier commit described it as "a local whose name matches a method of the same class shadows that method for the whole call." Wrong twice, and the narrow rule let a second instance straight through. Reduced:
prints
7, then raises. A local does not shadow a method for a call — it destroys that method on the object permanently, from any other method. The same call works before the poisoning line runs and fails after, so it is time-dependent and testing cannot be relied on to surface it.The linter now flags any local sharing a name with any method of its class. That caught the instance it was rewritten for, plus one more:
fillRoundedreassigned itsradiusparameter, which would have destroyedpainter.radius()— called byWindowandColorbarfor their corners — for the rest of the session. Latent only because the Picotron rebuild leftfillRoundedwith no callers.What rendering found
Colorbarnever calledsuper.paint, which was harmless until it had a child.Checks
132 assertions, a clean linter, Picotron's eight pixel tiles matching. The linter now also refuses a raw hex in a theme that claims a palette — forty hand-picked colours that only nearly agree is most of what "messy and inconsistent" meant. Both listed exceptions are measured facts: Aseprite paints its transparency checker itself, in
#c0c0c0over#808080, under both the light and the dark reference.Known gaps
sheet.png; that file would let me take them exactly, as Picotron's icon browser did for the file icons.docs/tutorial.htmlstill documents the pre-Picotron interface and says so at the top.🤖 Generated with Claude Code
https://claude.ai/code/session_019QF46RxyogNPLNX7DajyKM
Generated by Claude Code