[HN Gopher] Instant flood fill with HTML Canvas
       ___________________________________________________________________
        
       Instant flood fill with HTML Canvas
        
       Author : shaneos
       Score  : 59 points
       Date   : 2023-05-23 19:18 UTC (3 hours ago)
        
 (HTM) web link (shaneosullivan.wordpress.com)
 (TXT) w3m dump (shaneosullivan.wordpress.com)
        
       | ianlevesque wrote:
       | 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.
        
         | shaneos wrote:
         | Very cute! I started out with basically this, and my kids kept
         | asking me for more and more features :-). Recently they asked
         | to make animations, and that took a chunk of time to get right.
         | I'm looking forward to whatever they ask for as they grow more
         | advanced!
        
         | sharikous wrote:
         | Wonderful, the fact that it runs as expected in multi touch
         | devices has not gone unnoticed!
        
       | Waterluvian wrote:
       | 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.
        
       | zachrip wrote:
       | This works well for the problem space - does it work well for a
       | drawing app where the canvas is always changing?
        
         | shaneos wrote:
         | It can be made work. Precompute the image when the flood fill
         | tool is activated and it's fine, since you only have to do it
         | once
        
         | ZiiS wrote:
         | Tbh adding a background pixel only needs to check its four
         | surrounding to see if it joined seperate areas, adding a
         | fourground is harder I expect you need Dijkstra.
        
         | rattray wrote:
         | Interesting - what behavior would you want there? Statically
         | filled area, or a shifting area depending on the other pixels?
        
           | RandallBrown wrote:
           | For an app like this I would expect it to be static.
           | Basically the same behavior MS Paint has had for decades.
        
       | kragen wrote:
       | 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)
        
         | thrashh wrote:
         | There's no reason a JIT'd runtime is going to find this job
         | challenging or slow at all.
         | 
         | I don't know what OP's original code is but maybe OP was
         | calling a Canvas API method for every pixel (super bad).
        
           | shaneos wrote:
           | I wasn't calling per pixel :-) even when just visiting each
           | pixel once it would still take 50 - 100ms to fill a large
           | image which the user perceives as lag. I wanted it to be
           | instant
        
         | shaneos wrote:
         | If a wasm algorithm can run in under 16ms for arbitrarily large
         | images that would be amazing. Hard to beat pre-computing all
         | possible fills however, as the user perceives it as instant.
         | 
         | If you've seen a really fast wasm solution I'd love to replace
         | the fill portion with it though.
        
           | irskep wrote:
           | I think people in this thread are being much too optimistic
           | about browser performance. Writing a loop isn't the issue;
           | memory access patterns are. Canvas implementations are just
           | not well suited for the flood fill problem space. (To be
           | fair, I last spent some time with flood fills + canvas ten
           | years ago when I was working on the now-dead library
           | Literally Canvas, but I assume that experience is pretty far
           | out of date now.)
           | 
           | Precomputing fill areas is a really good idea for the
           | situation you're in!
           | 
           | Edit: actually it was 7 years
           | https://github.com/irskep/literallycanvas-pro-
           | tools/blob/mas...
        
             | kragen wrote:
             | you're calling fillRect in your inner loop
             | 
             | it also has 10 other method and function calls in it, one
             | of which allocates a new 4-element array
             | 
             | also it's maintaining a stack of pixels to paint instead of
             | looping over a scan line
             | 
             | each iteration of the loop paints one pixel
             | 
             | i don't think canvas memory access patterns are the root of
             | the performance problem
             | 
             | i'm not saying it's bad code, just that it doesn't justify
             | your conclusion
        
           | kragen wrote:
           | you say
           | 
           | > _If a wasm algorithm can run in under 16ms for arbitrarily
           | large images that would be amazing_
           | 
           | that would indeed be amazing, because nothing that might
           | change every pixel can run in any fixed period of time on a
           | fixed number of processors for arbitrarily large images; it's
           | necessarily linear time in the worst case, if not worse
           | 
           | so there is no way that can be a sincere sentence
        
             | shaneos wrote:
             | Not trolling. I haven't played with wasm at all. Perhaps if
             | it can fill a 2k x 2k pixel image super fast it would be
             | indistinguishable from my solution. If someone had coded
             | this and open sourced it I'd love to use it
        
               | gfd wrote:
               | i used opencv.js a few years ago and it was fast enough
               | to process videos frame by frame for stuff way more
               | complicated than just floodfill. See https://docs.opencv.
               | org/4.7.0/d5/d10/tutorial_js_root.html
        
       | ninepoints wrote:
       | 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.).
        
       | shaneos wrote:
       | 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.
        
       | TazeTSchnitzel wrote:
       | 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.
        
         | shaneos wrote:
         | The speed is achieved by getting use the fillRect() command
         | with globalCompositeOperation. This is a single draw operation
         | rather than a per pixel algorithm while the user waits.
         | Anything more involved than with would, I think, be slower as
         | perceived by the user
        
       | chrisweekly wrote:
       | 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.
        
       | RandallBrown wrote:
       | 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.
        
         | occamrazor wrote:
         | The naive method is slow because it repaints the image several
         | times. A faster way is a sort of double buffering: copy the
         | image to an array, perform the fill on the array, copy the
         | array to the canvas image data.
         | 
         | The method in TFA is however faster for static images, after
         | the preprocessing.
        
           | shaneos wrote:
           | There's a trade off between speed and showing progress to the
           | user. I find it better to show slow progress rather than wait
           | 500 - 1000ms to do it faster, but it depends on the use case
        
         | evan_ wrote:
         | Wikipedia lists a bunch of Flood Fill algorithms. It looks like
         | they all involve some form of checking every pixel one-by-one:
         | 
         | https://en.wikipedia.org/wiki/Flood_fill
         | 
         | I was hoping the author had figured out some clever solution
         | involving some implementation detail of HTML Canvas.
        
           | londons_explore wrote:
           | For a typical users use case, I bet you could downscale the
           | image 16x (using the GPU, and therefore very fast), flood
           | fill on the much smaller image (using slow javascript over
           | each pixel), then expand the image up to the original size
           | and reprocess just the border pixels (and there aren't many
           | border pixels compared to the total filled area).
           | 
           | This method might not produce perfect results when the paint
           | can 'escape' through a tiny gap that isn't visible on the
           | downscaled version... But at the same time, I suspect most
           | users don't actually want the paint to escape through a tiny
           | gap, so that might be the right behaviour.
        
             | yetanotherloser wrote:
             | Been a long time since I was a kid playing with Deluxe
             | Paint, but "fill except where I missed a tiny gap" would
             | have been exactly what I wished the fill tool would do! I
             | don't think I've used a fill tool in earnest since :-(
        
         | dvh wrote:
         | There is a option in getContext called willReadFrequently that
         | specifies that pixels are read often maybe that would help.
        
           | shaneos wrote:
           | Author here: in the app I use that, it seems to help alright
        
             | acqq wrote:
             | What are your thoughts on this?
             | 
             | http://www.adammil.net/blog/v126_A_More_Efficient_Flood_Fil
             | l...
        
           | londons_explore wrote:
           | willReadFrequently disables the use of the GPU. All canvas
           | operations will be software rendered on the javascript
           | thread. The actual operations happen not when you make the
           | draw() call, but when you read any pixel of the canvas.
        
       ___________________________________________________________________
       (page generated 2023-05-23 23:00 UTC)