Creating a poster-making tool (part 1)
This is the first article in a series of articles digging into the building blocks of a poster-making tool. It will be most useful for anyone who wants to geek into the underlying layers of image editors and for developers who are interested in creative technology.
What and why?
We had a chance to create a poster-making tool for a client. The tool lets users create a promo poster in a custom style for social media sharing. The basic features included uploading an image or taking a picture, repositioning the image to fit posts for various social media, applying color filters, adding content, and downloading the result in three sizes.
The main editor was created using Html5 canvas, the UI was created in React and TypeScript added the types to our app. Here is how the app looked like at the end.
Some of the questions that came up at the beginning of the project are:
- How do we modify the canvas with React?
- How do we let users upload and download the created posters?
- How do we create color filter functionality?
- How to make image repositioning functionality?
- How to make it all work from different devices?
Each of the questions can have its article, as they are complex and fun challenges to deal with. But we will start by introducing the first question in this article, and more will come!
Start with app structure
First, we created the base of the application using React. Here is the simplified structure that we will refer to throughout this article:
Where the poster
is a React component that contains the CanvasContainer
class that encapsulates all canvas drawing tools.
What is canvas?
Canvas is an Html element used to create 2D graphics on the web via JavaScript. Canvas API is relatively low-level, and drawing advanced graphics can be quite complicated. There are libraries like easel.js, fabric.js, and more that facilitate the creation of more complex forms and interactions with canvas. But we did not use external libraries as our tool is relatively simple.
Output the canvas in React
Now let’s see how we can modify the canvas from React.
In a typical React application, the parent component passes down data (props) to its children. But that won’t work with canvas because its API is entirely imperative. So the canvas should be modified outside of the typical React dataflow.
Find the canvas with Refs
: To be able to modify/communicate/read/write data from/to the actual canvas element, and not the JSX element, we need to create a reference to the actual DOM element. To do that, we will use Refs
. As one can conclude from React docs when things have to be done imperatively, it is a good case for Refs
.
Let’s look at the code.
Disclaimer: Most of the code uses the old lifecycle methods in React. We’re aware that the recommended way to do it now is to use hooks, and it would be very interesting if any of you have done some work on canvas on hooks and can share it with us.
Code snippet #1: Rendering canvas in React (Poster/index.tsx
)
class Poster extends React.Component<Props> {
// declare properties
canvas: CanvasContainer;
canvasRef: React.RefObject<HTMLCanvasElement>;
constructor(props) {
super(props);
// create a ref to store the canvas DOM element
this.canvasRef = React.createRef();
}
componentDidMount(): void {
this.canvas = new CanvasContainer(this.canvasRef.current, {
// options
width: this.props.width,
height: this.props.height,
backgroundColor: this.props.backgroundColor,
});
}
render = (): JSX.Element => {
// tell React that we want to associate the <canvas> ref
// with the canvasRef that we created in the constructor
return (
<canvas ref={this.canvasRef} id='canvas'/>
);
};
}
Let’s look closer at some lines.
this.canvasRef = React.createRef();
Here we make a reference to store canvas DOM element. We do it in the constructor so that it can be referenced throughout the component.
this.canvas = new CanvasContainer(this.canvasRef.current, { some options }
Here we create an instance of the CanvasContainer
object and passing two parameters: the reference and the options (eg width, height, background color, font-size). To access the reference to the node we use the current
attribute of the ref
. Now if we output the value of the reference, we get the created elements:
In the render method, we tell React that we want to associate the canvas ref
with the reference to CanvasContainer:
<canvas ref={this.canvasRef} id='canvas'/>
Alternative: We could modify the canvas from React by using some external libraries that bind canvas drawing APIs to React, e.g. Flipboard/react-canvas, but that is not the approach we have taken.
CanvasContainer class
All the tools for drawing the canvas are encapsulated in the CanvasContainer
object where the parameters are sent from the poster
component. Let’s look at the CanvasContainer
constructor:
Code snippet #2: CanvasContainer constructor (Poster/canvasContainer.ts
)
constructor(root, options = {}) {
// root is the canvas element
this.root_ = root;
// canvas context
this.ctx = this.root_.getContext('2d', {alpha: false});
// assign default parameters
Object.assign(this, {
width: 0,
height: 0,
// other parameters
});
}
If you are unfamiliar with canvas, getContext
is a new concept here. CanvasRenderingContext2D
provides 2D rendering context for the drawing surface of the <canvas>
element. It is used to draw shapes, text, images, and other objects. To get the context, we call getContext ()
on the <canvas>
element and provide 2d
as the argument. Having ctx
(context) in hand, we can draw anything we like.
Time to paint 👩🏽🎨
We call the paint function (it is a set of functions) when the component is mounted and not in the rendering method as we only need to call it once.
Code snippet #3: Call paint function (Poster/index.tsx
)
componentDidMount(): void {
this.canvas = new CanvasContainer(this.canvasRef.current, {
// options
});
this.canvas.paint();
}
Let’s look at what the paint function consists of.
Code snippet #4: paint()
function (Poster/canvasContainer.ts
)
paint = (): void => {
// clear canvas before paint
this.clear();
// paint background image
this.paintBackgroundImage();
// paint user-uploaded image
if (this.image_) this.paintImage();
// apply color to the image
if (this.gradientColor) this.paintGradient();
};
Let’s see what’s clear
and what the paintBackgroundImage
functions are doing.
1. Clear
We start by clearing the canvas before we paint. The clear
function will remove everything from the canvas before repainting.
Code snippet #5: Clear the canvas (Poster/canvasContainer.ts
)
clear = (): void => {
const { ctx, width, height } = this;
// save the default state
ctx.save();
// clear the canvas bitmap
ctx.clearRect(0, 0, width * window.devicePixelRatio, height * window.devicePixelRatio);
};
- The
save()
method saves the entire state of the canvas by pushing the current state onto a stack. More about what thesave()
method does with clear and crispy examples: html5.litten. - The
clearRect()
method erases the pixels in a rectangular area by setting them to transparent black. According to html5canvastutorials and StackOverflowclearRect()
“performs much better than other techniques for clearing the canvas, such as resetting the canvas’s width and height, or destroying the canvas element and then recreating it.”
A remark about the width & height parameters.
We use window.devicePixelRatio
when we compute the width and height of the canvas. This is because some displays have different dpi. To ensure that the canvas scales correctly, we need to know the ratio of a current display device. You can use thewindow.devicePixelRatio
property to detect dpi. On standard devices, the value will be equal to 1, and on devices, with a high dpi, it is greater than 1.
2. Paint background & image
First, we paint the canvas in black as a fallback color.
Code snippet #6: Paint background (Poster/canvasContainer.ts
)
paintBackgroundImage = (): void => {
const {
ctx,
width,
height,
} = this;
// fallback to black background color
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width * window.devicePixelRatio, height * window.devicePixelRatio);
};
Then we check if the image was uploaded by a user and paint the image on the canvas.
Codesnipet #7: Paint image (Poster/canvasContainer.ts
)
paintImage = (): void => {
const {
ctx,
scaleRatio,
image_,
imageOffset,
} = this;
ctx.drawImage(image_, 0, 0,
image_.width,
image_.height,
imageOffset.x,
imageOffset.y,
image_.width * scaleRatio,
image_.height * scaleRatio);
};
It was quite straightforward to paint the image on the canvas. The crux part was to load the image into the canvas and to make it work from different devices. Let’s look at that in the next article.
In this first article, we aimed to give you an overview of the basic features used to create the poster maker tool. Hopefully, you got a better understanding of how to set up a canvas and how to modify data on the canvas using React. Any comments are appreciated. We are here to learn from each other ✌🏾