The recent project to achieve a similar pixel style drawing board, pixel small grid can be erased, box selection color, can erase a variety of graphics, such a small project seems to be simple, contains a lot of things.

Draw a pixel grid

Let’s start by defining the pixel grid class

Pixel = function (option) {
    this.x = option.x;
    this.y = option.y;
    this.shape = option.shape;
    this.size = option.size || 8;
}
Copy the code

X and y are the coordinates of the center point, and this is what I did at the beginning. Let’s define the path

	createPath: function (ctx) {
			if (this.shape === 'circle') {
				this.createCircle(ctx);
			} else if (this.shape === 'rect') {
				this.createRect(ctx);
			} else {
				this.createCircle(ctx); }},createCircle: function (ctx) {
			var radius = this.size / 2;
			ctx.arc(this.x,this.y,radius,0.Math.PI*2);
		},

		createRect: function (ctx) {
			var points = this.getPoints();
            points.forEach(function (point, i) {
                ctx[i == 0 ? 'moveTo' : 'lineTo'](point.x, point.y);
            })
            ctx.lineTo(points[0].x, points[0].y);
		},
Copy the code

The pixel grid supports circles and rectangles, and the path is defined and then drawn

draw: function (ctx) {
			ctx.save();
			ctx.lineWidth=this.lineWidth;
			ctx.strokeStyle=this.strokeStyle;
			ctx.fillStyle=this.fillStyle;
			ctx.beginPath();
			this.createPath(ctx);
			ctx.stroke();
			if(this.isFill){ctx.fill(); } ctx.restore(); }Copy the code

Then create pixel meshes in batches through a loop:

for (var i = stepX + . 5; i < canvas.width; i+=stepX) {
		for (var j = stepY + . 5; j < canvas.height; j+=stepY) {
			var pixel = new Pixel({
				x: i,
				y: j,
				shape: 'circle'}) box.push(pixel); pixel.draw(ctx); }}Copy the code

Every pixel is drawn back to the context, changing the state of the canvas each time. This will result in poor rendering performance because there are many pixels. If the canvas is large, the performance will be terrible, and there are some operations on the canvas. It is not appropriate to change the canvas state so often.

Therefore, the correct approach is: we should define all paths, preferably in a one-time batch draw to canvas;

// Define the position of the pixel
for (var i = stepX + . 5; i < canvas.width; i+=stepX) {
		for (var j = stepY + . 5; j < canvas.height; j+=stepY) {
			var pixel = new Pixel({
				x: i,
				y: j,
				shape: 'circle'}) box.push(pixel); }}// Batch render
	console.time('time');
	ctx.beginPath();
	for (var c = 0; c < box.length; c++) {
		var circle = box[c];
		ctx.moveTo(circle.x + 3, circle.y);
		circle.createPath(ctx);
	}
	ctx.closePath();
	ctx.stroke();
	
	console.timeEnd('time');
Copy the code

It can be seen that this rendering is very efficient and changes the state of the canvas as little as possible, because every time the state of the context is changed, the canvas will be redrawn, which is a global state.

Pixel grid interaction

The requirements of the project, press on the canvas mouse, can erase pixels, which contains two points, one is how to get the mouse on the path of the pixel grid, two is a performance issue, because we are the requirements of this requirement is to draw eighty thousand points, if nothing else, the light cycle to dozens of hundreds of milliseconds, and map rendering. Let’s start with the first question:

Gets the mesh under the mouse movement path

See this problem, it is easy to think of, write a function, through the mouse to obtain the position of the location containing the grid, then every move to update position calculation, it is can complete requirements, but if the mouse moves too fast, can’t do it, the position of each point can be calculated, the effect will be inconsistent. We change the idea, the route of the mouse, we can clearly know the starting and end point, we imagine the whole rendering path sections of line segments, so the question becomes, the one of the intersection of a line with the original algorithm, the line is thick, brush line the route is the path of the mouse movement, and the circle is the need to change the style of the intersection of the grid. The code looks like this:

function sqr(x) { return x * x }

    function dist2(p1, p2) { return sqr(p1.x - p2.x) + sqr(p1.y - p2.y) }

    function distToSegmentSquared(p, v, w) {
        var l2 = dist2(v, w);
        if (l2 == 0) return dist2(p, v);
        var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
        if (t < 0) return dist2(p, v);
        if (t > 1) return dist2(p, w);
        return dist2(p, {
            x: v.x + t * (w.x - v.x),
            y: v.y + t * (w.y - v.y)
        });
    }

	@param {x: num, y: num} @param {x: num, y: num} v: param {x: num, y: num} v: param {x: num, y: num} Num} w end */
    function distToSegment(p, v, w) {
        var offset = pathHeight;
        var minX = Math.min(v.x, w.x) - offset;
        var maxX = Math.max(v.x, w.x) + offset;
        var minY = Math.min(v.y, w.y) - offset;
        var maxY = Math.max(v.y, w.y) + offset;

        if ((p.x < minX || p.x > maxX) && (p.y < minY || p.y > maxY)) {
            return Number.MAX_VALUE;
        }

        return Math.sqrt(distToSegmentSquared(p, v, w));
    }
Copy the code

Specific logic will not be detailed, you can see the code. Then get the intersecting grid, delete the data in the box, render again, you can see the effect.

In the same way, we can do a coloring effect, and we can probably do a little demo of a Canvas pixel artboard. Each pixel must be an object, because the state of each object is independent. However, there is no need to worry about performance. There are not many pixels, so there is no lag. The effect is as follows:

Recently I have been a little lazy, so I have time to add a upload picture, picture pixel painting function and export function.

Poor eyes, forgot to post the project address: github.com/lspCoder/ca…