from Hacker News

Instant flood fill with HTML Canvas

by shaneos on 5/23/23, 7:18 PM with 122 comments

  • by andersrs on 5/24/23, 4:56 AM

    > The idea was to build something fun that never shows adverts to them or tricks them into sneaky purchases by “accident”.

    This part really resonated with me. Google promotes the absolute worst spammy shit these days. I have to browse tech forums to find clean simple games.

    Last week I was trying to find a simple memory cards game and the search results were complete dog shit so I started building my own memory cards game. In a couple hours I made https://memorycardsgame.com/. Then browsing a svelte forum I found https://toddler-games.com which is much further ahead of mine but I'd never be able to find stuff like this by searching for it. Of course none of the good games are filled with a bunch of SEO spam which is kind of the point. Since Steve Jobs didn't let his kid use an iPad we've decided the game isn't needed now.

    Nice app.

  • by shaneos on 5/23/23, 10:59 PM

    Author here: I'm loving all the suggestions of ways that this might be achievable in either a faster or simpler method! The code is open source at https://github.com/shaneosullivan/example-canvas-fill and I'd love to see you hackers improve on it and share it with everyone. Any and all improvements I'll happily use going forward in my app.
  • by Camillo on 5/24/23, 12:27 AM

    You could do instant flood-fill on a slow PC in the early 90s. There is no need to precompute this. The possible issues are:

    1. You're using a slow algorithm. This is almost certainly the case, looking at how slowly it run and at the order in which pixels get painted. The Wikipedia page on flood fill is enough to find a good algorithm.

    2. Possibly, the overhead of individual canvas operations is high, so setting each pixel at once is slow. If this is the case, it would be partially ameliorated by using a span-based algorithm, but you could also run the algorithm on an offscreen byte array and then blit the result to canvas.

    I would bet money that a 90s state-of-the-art algorithm running in JavaScript on an offscreen array will be perceived as instantaneous on a modern computer.

  • by ianlevesque on 5/23/23, 9:19 PM

    Ha, painting tablet app feels like a rite of passage for programmer parents. Here's mine from that era https://paints.netlify.app/

    Where it got really interesting later was when one of my kids asked how I made it, how it worked, how they could add new features. So it grew a bunch of adorable haphazard hacks that my oldest implemented.

  • by Waterluvian on 5/23/23, 10:13 PM

    When it comes to performance, I find you really need to experiment with HTML Canvas. Some of the interfaces can be hundreds to thousands of times slower for manipulating the canvas than others.
  • by kragen on 5/23/23, 9:28 PM

    the summary is that they preprocess the image to partition it into a disjoint set of fillable regions, but i feel like this maybe has to be redone whenever you mutate the image

    maybe other optimization approaches would yield a flood-fill algorithm that just runs fast enough in js; wasm can surely do it

    like i feel like you can probably loop over the pixels in a scan line until you encounter an obstacle at several tens of megapixels per second

    as the dumbest possible test of js perf, this code (admittedly not accessing an imagedata object) gets about 250 million iterations per second on my 2.6 gigahertz palmtop in firefox

        function tri(n) { let x = 0, y = n; while(y--) x += y; return x } 
        s = new Date(); console.log(tri(10_000_000)); console.log(new Date() - s)
  • by joeldo on 5/23/23, 11:32 PM

    For vector style graphics (like in the article) - You can achieve this quite simply on the web with SVG DOM.

    You can add event listeners to the SVG's paths/groups and apply fill when clicked.

  • by ninepoints on 5/23/23, 10:58 PM

    Surprised "jump flooding" hasn't been mentioned yet, which is the common technique to accelerate this sort of thing (used for color fills, signed distance field construction, outlines, etc.).
  • by robomartin on 5/24/23, 2:01 AM

    I've played with and implemented a range of area fill algorithms in the past. Optimized span-filling (dating back to the 1990's) works very well, is fast, buffer friendly and delivers great results. Subtleties surface when you have to consider filling around dithered edges and partially transparent elements. Fur stuff.

    https://en.wikipedia.org/wiki/Flood_fill

  • by TazeTSchnitzel on 5/23/23, 9:37 PM

    Why does it need separate mask images? Couldn't you use the indices in the alpha layer to do the flood-fill by looping over every pixel in the image and changing the colour if the index matches? It would save memory, right? That per-pixel loop should be fast enough, since there's just one pass and it's simple enough any JS JIT ought to be able to optimise it.
  • by TeffenEllis on 5/24/23, 2:01 AM

    This is very clever! I found out about the canvas composite modes far too late in an animated ASCII art project.

    IMO, the canvas APIs are a little too esoteric for their own good. It’s almost like a stateful object, but all the commands are executed as side effects. Isn’t front end fun?

    Please have a look at my project and let me know what you think! The source code is fully annotated for another brave soul who’s working with the dark arts of high performance canvas rendering.

    - Project page: https://asciify.sister.software

    - Demo: https://asciify.sister.software/demo/3d/

  • by danbruc on 5/24/23, 2:42 PM

    I tried the usual route using getImageData() and am getting between 50 ms for a small region and 100 ms for the full 2048 x 2048 canvas on a 8th generation mobile i5 in Chrome. In Firefox it is 50 ms and 150 ms, all on Windows 10. So the overhead alone of getting the image data from the canvas and writing it back can eat up most of the entire 50 ms budget if not exceed it. Or maybe I did it not in the proper way, I am totally not a web developer. There is also certainly some room for improvement in my implementation of the core algorithm. But unless there are more tricks than specifying willReadFrequently, it seem hard to get below 50 ms.
  • by RandallBrown on 5/23/23, 8:49 PM

    Seems like there must be a better way?

    https://paintz.app seems to be able to do this without precomputing all of the spaces you could flood.

  • by eyelidlessness on 5/24/23, 1:51 AM

    Apart from algorithm commentary by folks who are certainly wiser than me on the topic: since you’re already using a worker, you would probably benefit from using OffscreenCanvas[0], which is transferrable to worker threads and otherwise should be optimized for heavier computations.

    0: https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCa...

  • by chrisweekly on 5/23/23, 9:40 PM

    This reminds me of an iOS puzzle game I really enjoyed a year or two ago called "Kami 2". Picture a canvas with geometric shapes in a few different colors. The goal is to paint the whole canvas in one color, armed with a limited number of "fill" operations. Highly recommended.
  • by primitivesuave on 5/24/23, 6:34 AM

    This is a very neat implementation and explanation of a disjoint set algorithm: https://en.wikipedia.org/wiki/Disjoint-set_data_structure
  • by stkdump on 5/24/23, 2:54 AM

    In the past we had indexed color screen modes. You could draw all pixels to the screen once and give each pixel an index into a color palette. When you would start, they would all be mapped to white, but as you start with coloring, you would then just change the color of the palette entry (truly a O(1) operation) and your graphics card would then do the work at scan time. I guess the closest equivalent nowadays would be to do this inside the pixel shader.
  • by DaveSapien on 5/24/23, 11:20 AM

    This has convinced me to add prepossessed fill areas on my kids colouring book for iOS. (no IAP, subscription, no ads, etc, like the general sentiment here) Been thinking about this for a while, but things are fast enough on an iPad that it took a back seat, nice to be inspired to go that extra mile :)
  • by zachrip on 5/23/23, 8:37 PM

    This works well for the problem space - does it work well for a drawing app where the canvas is always changing?
  • by shaan7 on 5/24/23, 8:40 AM

    Oh man this brings back memories. When I was young we did not have Internet at home, so I made a VB6 app for my sister so she could load outlines and color them. So much fun! For both of us :D

    EDIT: Later I also made a simple LOGO interpreter in VB6 for the same reasons. Turtle fun!

  • by rep_movsd on 5/24/23, 4:25 PM

    A better way to do it would be to maintain canvas Path2d objects to define the enclosed regions, which can be instantly filled with context.fill()

    No need for complex pixel manipulation stuff

    This also lets you do things like let the user move things around.

  • by gatkinso on 5/24/23, 12:25 AM

    I liked the slow version. more fun.
  • by PretzelPirate on 5/24/23, 1:02 PM

    It seems hypocritical to make an app that won't show ads and talk about it's implementation on a site full of ads (I see 5 on the page).

    Sure the target for this content is developers and not specifically kids, but plenty of 10 year olds program and may be seeing these ads when trying to write their own painting program.