I've just submitted this app for Hashnode Christmas hackathon so I wanted to talk about it here in more detail.
I didn't have a chance to work with HTML canvas and the Canvas API, so this hackathon gave me a nice reason to dive right into it.
I also wanted to add something unique to make the result more magic, so I added a dynamic color brush and dynamic width brush. Turns out that this effect indeed looks more magic and dream-like.
Tech stack
- React (with custom React hooks)
- Canvas API
- Native color picker and range inputs
- Font awesome icons
- Netlify hosting
Intro screen
Since I'm primarily a frontend developer and I want to pay special attention to design and details, I've wanted to create a nice splash screen for the app. I was inspired by the watercolor and paint set box designs.
I remember when I was buying paint sets for school, I was impressed by the images on the boxes. They showed a beautiful painting and were basically communicating "You can paint this beautiful image with this set". So I wanted to mimic that feeling with the splash screen.
If you wonder how I managed to overlay a gradient on the heading text, here is a code snippet.
background: linear-gradient(
90deg,
hsl(0, 100%, 50%),
hsl(211, 100%, 50%) 50%,
hsl(108, 100%, 40%)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
Custom hook
I've added the painting functionality with event listeners and Canvas API using a custom hook that returns a bunch of states and functions that are required for switching brushes, setting up a Canvas ref, and keeping track of active states.
Dynamic color and brush width
This is where the magic happens. In the magic brush mode, I'm shifting the Hue value of HSL color for each paint event. The resulting effect is a wonderful color gradient. I've also added controls to change the color gradient saturation and lightness for more options and moods.
ctx.current.strokeStyle = `hsl(${hue.current},${selectedSaturation.current}%,${selectedLightness.current}%)`;
ctx.current.globalCompositeOperation = "source-over";
hue.current++;
if (hue.current >= 360) hue.current = 0;
Similar to the magic brush mode, I've also added a dynamic width mode that changes brush size value up and down between the minimum and maximum value. When combined with the magic brush mode, you can create some awesome art and effects.
const dynamicLineWidth = useCallback(() => {
if (!ctx || !ctx.current) {
return;
}
if (ctx.current.lineWidth > 90 || ctx.current.lineWidth < 10) {
direction.current = !direction.current;
}
direction.current ? ctx.current.lineWidth++ : ctx.current.lineWidth--;
}, []);
App demo
https://magic-painter.netlify.app/
Source code
Post the art you create with the app in the comments! :)
If you enjoyed this post on my hackathon project for Hashnode, check out my hackathon project for DEV x DigitalOcean
My Resolve 2021 - DO Hackathon submission
Adrian Bece ・ Jan 4 '21
These articles are fueled by coffee. So if you enjoy my work and found it useful, consider buying me a coffee! I would really appreciate it.
Thank you for taking the time to read this post. If you've found this useful, please give it a ❤️ or 🦄, share and comment.
Top comments (31)
Thank you!
can you please explain this line from source code?
Sure. I'm replacing the custor with an dynamic background-image svg that is a circle with a certain radius.
got it. thanks.
I've had to do a similar project during my studies, which made me notice something I struggled fixing when I coded it : when you resize the window, the cursor no longer paints where you click but a few (or several) pixels away.
It's up to you to try and fix that if you want, but good job anyway :)
yes if your canvas is not clipped to the border top and border left, you must to use something as this :
to compute where is your point on the canvas and this point with the border, and on a android tablet i use this :
x = Math.floor( event.touches[0].clientX - Id("canvasToDraw").getBoundingClientRect().left );
y = Math.floor( event.touches[0].clientY + scrolledY - Id("canvasToDraw").getBoundingClientRect().top );
and you must manage the scroll, so the scroll is at 0 with variable scrollY.
make a real software is fun, but that can become a real headache sometime...
My work and my paint app of nears 10 thousand of lines of code in js/html5/css : essaie.fr/
But me I use the fact that html can create a matrix of pixels, to work pixels by pixels, and I created a system at a moment where if you click on a canvas you create a matrix of pixels to be used on another matrix of pixels to making your own brush, but it's slower that your solution, my app is in black and white to be used with a eink screens/ereader, and I tried to implements all of tools than an artist can image, and finished by to try to reinvent the wheel. I search few scripts in C to translate in JS to implements more features as an antialiasing post filter, but it's difficult to find documentation to do something beyond the documentation as MDN documentation and others tutorials on the web.
Wow, that is impressive! I don't think JS can handle advanced transforms like antialiasing, but keep working on it. It is very sophisticated!
My app is a bit simpler since it's done for a hackathon in a few days, but I could also add more functionalities later down the line.
Nice! Adding configurable boundaries for the oscillating color/size of the magic brush would increase the possibilities a lot! A color change is very useful, but if it always goes through the whole rainbow spectrum, you'll always have a kids room's wall painting in the end ;)
Tidy - it looks like you might have fun with ZIM - it is a canvas framework with components built in at zimjs.com - there is a Gen Art section on the front that has results of various drawing works including from zimjs.com/genpen. You may also want to apply damping to your motion to make the drawing smoother. You can see that at work at zimjs.com/angels - there is a book of Angels there but also a link to the tool on the first page at left. Cheers!
Very Nice
Amazing! Great job! 👌
Amazing, conngrats
Beautiful!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.