A Line Is Harder Than It Looks
@mdcrty/paint began as an excuse to learn one HTML element
Every computer I used as a kid had MS Paint on it. It wasn't good software by any measure that mattered later, but it was the first program I ever used that did exactly what I told it to and nothing else. You dragged the mouse, and paint appeared. No file format to understand, no menu to learn first. It's still the clearest example I know of an interface that gets out of the way.
I'd technically used Canvas before this, for the ripple effect in @mdcrty/color, but that was reaching for a tool because the problem had outgrown CSS rather than sitting down to learn one. I used a single feature and moved on. Paint was the first time I treated Canvas as what it actually is - a surface with an input and an output, that you can read from as well as write to, that you can draw image files onto and then pull the pixels back out of.
Click and drag felt like the most direct way in. It's an input everyone already understands, wired to an output that has to appear instantly or the whole thing feels broken.
It also had to be fun. Everything I build ends up with some element of play in it, and I've stopped treating that as a distraction from the engineering. Play is a demanding requirement - it means the thing has to respond correctly at 60 frames a second, or it isn't playful, it's just slow.
Drawing a line badly
The first version was about fifteen lines. Listen for pointer events, and every time the pointer moves while it's down, draw a straight line from where it was to where it is now. moveTo, lineTo, stroke.
Move the mouse slowly and it looks perfect.
Move it quickly and the illusion collapses. The browser doesn't fire a pointer event for every pixel your hand travels through - it fires them as fast as it can, which on a fast stroke might be every twenty or thirty pixels. Connect those with straight lines and a curve becomes a polygon. Your signature comes out looking like it was drawn with a ruler by someone in a hurry.
The fix is to stop drawing lines between the points and start drawing curves through them. That's quadraticCurveTo, and I'll be honest that the maths behind it still isn't fully intuitive to me. I understand what a control point does and why the interpolation produces a smooth arc, but I can't hold the whole thing in my head at once. The technique that finally worked was using the previous pointer position as the control point and the midpoint between the two positions as the endpoint, so each segment bends toward where the stroke was heading rather than cornering at every sample.
Here's the part I want to defend, though. Not understanding it perfectly turned out not to matter, because once the calculation was correct, everything after it was easy. I could change the smoothing, the width, the sampling, and watch what each change did in real time.
That's the difference between maths in a browser and maths in a notebook. At school I solved quadratic equations by hand, over and over, and had no idea what any of them looked like. Here the equation is the drawing. You tweak a coefficient and the line changes shape under your cursor. I'd have been considerably better at high school maths if someone had let me do it this way.
Seven lines that felt like magic
The single most satisfying moment in the project wasn't the curves. It was the download button.
Getting the canvas out of the browser and onto the device as a real PNG file - with the alpha channel intact, so the transparent areas stayed transparent instead of turning into a white rectangle - takes about seven lines of code. Convert the canvas to a data URL, hang it off an anchor element with a download attribute, click it programmatically.
Seven lines. But it's the first time the thing stopped being a demo. You draw something, you press a button, and there's a file on your computer that you made. The whole loop closes, and closing the loop is what turns a toy into a tool.
Freedom from my own website
Like the colour picker, moving Paint into an npm package was straightforward - the pipeline already existed by then, and the component was self-contained enough that it mostly just moved.
What surprised me was what happened afterwards. Once the code wasn't tied to one page on my site, I got noticeably braver with it. Features I'd been putting off for months went in within weeks. I think it's because a package has to be good in general rather than good in one place, and that framing gave me permission to build things I'd previously talked myself out of as over-engineering.
Two came out of that.
The first was a bucket fill, because MS Paint had one and it was always the best button. A flood fill is conceptually simple - start at a pixel, spread outward, stop when the colour changes - and immediately ran into a problem I hadn't anticipated. If you only fill pixels that exactly match the one you clicked, you get halos. Every brush stroke is antialiased, so its edges are a gradient of dozens of near-matching colours, and an exact-match fill stops dead at the first of them and leaves a fuzzy outline behind. Hence fillTolerance, which defaults to 80 out of a possible 128 - deliberately generous, because a fill that leaks slightly is a much better experience than a fill that leaves a ghost around everything you've drawn.
The second is the feature I'd now point at if someone asked which part of this is most me. Pointer Events give you a pressure value, which is meaningless for a mouse and real for a stylus, so on an iPad with an Apple Pencil the stroke width varies with how hard you press. Press lightly and you get a hairline; lean in and you get the full width. The taper is proportional rather than absolute - minWidthRatio sets the lightest stroke as a fraction of whatever the size slider is currently on - so it behaves consistently whether you're sketching at two pixels or twenty.
It feels like ink. On a free component, in a browser tab, with no app to install. I've never seen anyone else bother.
What it can't do
There's one thing the package genuinely cannot own, and I've documented it rather than faked it.
The component sets touch-action: none on its own canvases, so a finger or a Pencil draws instead of scrolling the page. That part is mine to control. But when the drawing surface fills the screen on iOS, Safari's rubber-band and pull-to-refresh can still trigger if a drag begins just outside the canvas - and stopping that requires locking scroll on html and body, which are elements a well-behaved component has absolutely no business touching.
So the README tells you to do it yourself, on the page, and to scope it so the rest of your site still scrolls. It's less impressive than solving it, but a component that quietly disables scrolling across someone's entire application is a worse outcome than one that asks.
A free playground
What I want from this is fairly small. I want someone to land on a page and realise they can draw on it. Not sign up, not install, not watch anything first - just draw, in whatever colour they like, and download it if they want to keep it.
Maybe it grows. There's a long list of things it doesn't do, and each one is now a version bump rather than a rewrite. But whatever it becomes, it should stay the thing MS Paint was on those old machines: open, immediate, and free, with nothing between you and the canvas.