This project was completed as part of a group learning exercise.
instadraw_demo.mp4
https://instadraw.netlify.app/
This project allows multiple users to view the pictures that other users have drawn. Once a user uses their discord account to login, they can also create or edit images.
- ✅ User can draw in a canvas using the mouse
- ✅ User can change the color
- ✅ User can change the size of the tool
- ✅ User can press a button to clear the canvas
- ✅ User can save the artwork as an image (.png, .jpg, etc format)
- ✅ User can comment on an existing post
- ✅ User can add to an existing drawing started by another user
Remix provides a foundational framework for the web application. The client loads the PixiJS library and uses it for the rendering of the drawings.
- remix
- pixi.js
- supabase/js
- tailwindcss
- postgres
Run three processes in separate tabs or concurrently.
$ npm run dev:netlify
$ npm run dev
$ npm run watch:css
$ npm run build
$ netlify deploy --prod
The rendering is handled by the PixiJS library. PixiJS will render using WebGL, but can fallback to canvas rendering if GL is unavailable. It is used for browser game development, and can be found in engines like Phaser.
When the user clicks the event listener for mousedown
fires and sets some state to indicate that drawing is in progress. This state is called isDrawing
.
We only care about the mousemove
event when isDrawing
is set to true
.
useMouseMove(
canvasRef.current,
useCallback(
(event: MouseEvent) => {
if (isDrawing && rendererRef.current) {
/* create a Point object that describes where the
mouse is now */
const to = new PIXI.Point(event.offsetX, event.offsetY);
/* lookup the drawing layers container, it is easier
to group these in a Container provided by PIXI */
const layers = rendererRef.current.stage.getChildAt(
ZINDEX.LAYER
) as Container;
/* in the layers container, look for the last graphic
which is our current drawing layer */j
const graphics = layers.getChildAt(
layers.children.length - 1
) as Graphics;
/* any drawing should be the color that is chosen
as the current brush */
graphics.beginFill(parseInt(color, 16));
/* helper function to draw a circle at x, y */
const draw = (x: number, y: number) =>
graphics.drawCircle(x, y, brush);
/* if this is the second frame of dragging we can
interpolate between two points. this gives a
smoother line */
if (previousPointRef.current) {
interpolateDirect(draw, previousPointRef.current, to);
} else {
draw(to.x, to.y);
}
/* stop filling the drawing */
graphics.endFill();
/* keep track of where we were, so that the next
mouse move can be interpolated */
previousPointRef.current = to;
}
},
[isDrawing, color, brush]
)
);
We want to store the data for the image as a png
file. When we store objects using the Supabase API we can send them as an ArrayBuffer
.
With PixiJS there is an included plugin that can convert the rendered layers to a base64
encoded image. Decoding this image to an ArrayBuffer
allows us to match the type of data
that supabase expects.
return decode(
rendererRef.current?.renderer.plugins.extract
.base64(layers)
.replace(/^data:image\/\w+;base64,/, "")
);
When an image is saved, we store it using the supabase storage API.
/* give the object a unique name */
const name = `${uuid()}.png`;
/* store the object into the "layers" bucket */
const uploadResponse = await supabase?.storage
.from("layers")
.upload(name, data, {
contentType: "image/png",
});