"Your team needs to create an ability for users to pick a color from canvas. The task is to build a color dropper."
- Ensure you have a node version
^16.13.0
installed, runnode --version
, if this is not the case, then try usingnvm
or a similar tool to switch to a compatible version for instance:nvm install 16.13.0
thennvm use 16.13.0
- Install the dependencies running
yarn
- Build the app with
yarn build
- Run
yarn start
to serve the app locally - Open your browser of choice and go to
localhost:3000
Use the images in the public/assets/
folder (or add your owns through the file input button)
to play with different image types, and use the "Use transparency" and "Use real image size"
configs to unlock different behaviors.
To run the tests run yarn test
I added some other features that wheren't in the problem description such as using the image real size, and using transparency on the colors, because I thought it would be a good experiment to see how the solution would perform under more "preassure" (bigger canvas)
Without getting into too many implementation details, the idea of this solution is to use the canvas api to get the image data of the canvas. With this data, we can create a color matrix and build a component that uses this color matrix to draw a grid on top of the canvas that moves along with the mouse pointer.
There are 2 approaches that I could think of when solving this challenge:
-
The first one was to generate an ImageData on mousemove with the bounds of what the ColorDropper will draw using the
getImageData
function of the canvas context. Then build a HexMatrix with this data. -
The second approach is call
getImageData
once to get the ImageData ( or whenever we draw an image into the canvas), and when the mouse position changes we can calculate the HexMatrix using this data.
For the first approach, since we use the getImageData
for the bounds of the sub-square there are
some great benefits regarding off-canvas color calculations. The downsides of this approach
is having to call getImageData
a lot.. I run some tests since calling getImageData
is not a fast operation, and it took around
~4ms see here to perform this call.
Altough using the willReadFrequently
config of the context object gave much better results
lowering the call to ~0.3ms see here.
With this setting the whole flow of moving the mouse, getting the
image data, building the HexMatrix and then rendering the ColorDropper
it takes about ~3.5ms.
This approach is in the lg/frecuently-get-image-data
branch
The second approach involved working with the ImageData directly, and reading these values
to calculate the HexMatrix everytime the mouse moves. Since the ImageData.data is of type
Uint8ClampedArray
this is an array that looks something like: [r,g,b,a,r,g,b,a,....,r,g,b,a]
it wasn't too easy to work with, specially when the mouse pointer is near an edge. Besides that
the performance was much better, the whole flow of mousemove and building the HexMatrix, and
then rendering the ColorDropper
it takes around ~2.6ms see here.
The downsides of this approach is that we need to keep the ImageData in memory.
Comparing both approaches, I think the second one is best, not because the performance
implications ~2.6ms < ~3.5ms, but because the willReadFrequently
setting is not yet available
in all browsers Safari.
In the future, as a fun test I would like to see how drawing the ColorDropper component inside a canvas
would perform instead of using plain html & css to draw the grid. I noted that using large number
of <div>
s for the grid is not so great.
Although everything that involves drawing shapes is not as easy as using css, it would be really hard to maintain!