'Creating a pattern from a CSS-resized image and drawing it on a canvas
I'm working on an app that allows the user to select an image and define a custom clipping region for that image. The user can place blue dots on the canvas containing the image, which will represent the clipping path. These points are stored in the coordinates array. This is the function that clips the image to the selected area:
function crop()
{
ctx.beginPath();
ctx.moveTo(coordinates[0].x, coordinates[0].y);
for(let i=1; i<coordinates.length; i++)
{
ctx.lineTo(coordinates[i].x, coordinates[i].y);
}
ctx.lineTo(coordinates[0].x, coordinates[0].y);
ctx.stroke();
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.restore();
ctx.clip();
//redraw the image in the cropped area
let pattern = ctx.createPattern(image, "repeat");
ctx.fillStyle = pattern;
ctx.fill();
ctx.closePath();
}
This works perfectly fine when I let the image keep its natural dimensions (in this case 1200x600):
But if I give the image max-height and max-width properties, each set to 80vmin, the cropping stops working correctly:
Instead of cropping the image around the eye area, like in the previous image, it clips a different part of the image to the selected area.
I suspect createPattern is for some reason not loading the image properly, with its updated dimensions. So I tried passing it a new canvas object (which is basically a copy of the canvas I'm working on in its original form, with the resized image painted on and nothing else) instead of the image itself, but that didn't seem to work either, I was getting the same result. Is there anything else I could try?
Update: here is a JSFiddle containing the whole code
const image = document.getElementById("pic");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
//draw the image on the canvas
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0, image.width, image.height);
//get mouse coordinates
let coordinates = [];
canvas.onclick = function clickEvent(e)
{
let rect = e.target.getBoundingClientRect();
let x = e.clientX - rect.left;
let y = e.clientY - rect.top;
let point = {
"x": x,
"y": y
};
coordinates.push(point);
ctx.fillStyle = "blue";
ctx.fillRect(x, y, 10, 10);
}
//debugging
console.log(coordinates);
//crop the image
function crop()
{
ctx.beginPath();
ctx.moveTo(coordinates[0].x, coordinates[0].y);
for(let i=1; i<coordinates.length; i++)
{
ctx.lineTo(coordinates[i].x, coordinates[i].y);
}
ctx.lineTo(coordinates[0].x, coordinates[0].y);
ctx.stroke();
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.restore();
ctx.clip();
//redraw the image in the cropped area
let pattern = ctx.createPattern(image, "repeat");
ctx.fillStyle = pattern;
ctx.fill();
ctx.closePath();
}
function save()
{
let newCanvas = document.getElementById('canvas2');
//get new dimensions
let top = canvas.height;
let left = canvas.width;
let right = -canvas.width;
let bottom = -canvas.height;
for(let i=0; i<coordinates.length; i++)
{
if(coordinates[i].y < top)
top = coordinates[i].y;
if(coordinates[i].x < left)
left = coordinates[i].x;
if(coordinates[i].x > right)
right = coordinates[i].x;
if(coordinates[i].y > bottom)
bottom = coordinates[i].y;
}
newCanvas.width = right-left;
newCanvas.height = bottom-top;
//save the new image
newCanvas.getContext("2d").drawImage(canvas, left, top, newCanvas.width, newCanvas.height, 0, 0, newCanvas.width, newCanvas.height);
let downloadLink = document.createElement("a");
downloadLink.download = "crop.png";
downloadLink.href = newCanvas.toDataURL("image/png");
downloadLink.click();
}
h1 {
color: blue;
margin-bottom: 0px;
margin-top: 0px;
}
.image-div img {
max-width: 80vmin;
max-height: 80vmin;
border: 1px solid black;
}
.canvas-div {
margin-top: 240px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
</head>
<body>
<h1>Image:</h1>
<div class="image-div">
<img id="pic" src="https://icatcare.org/app/uploads/2018/07/Helping-your-new-cat-or-kitten-settle-in-1.png">
</div>
<div class="canvas-div">
<h1>Canvas:</h1>
<canvas id="canvas" style="border: 1px solid blue;"></canvas>
</div>
<!-- for exporting the final result -->
<canvas id="canvas2" style="display: none;"></canvas>
<button onclick="crop()">Crop</button>
<button onclick="save()">Save</button>
<br><br><br>
<!-- script -->
<script src="script.js"></script>
</body>
</html>
Solution 1:[1]
Before reading the explanation try the following:
ctx.beginPath();
ctx.moveTo(coordinates[0].x, coordinates[0].y);
for(let i=1; i<coordinates.length; i++)
{
ctx.lineTo(coordinates[i].x, coordinates[i].y);
}
ctx.lineTo(coordinates[0].x, coordinates[0].y);
ctx.stroke();
ctx.closePath();
ctx.clip();
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.restore();
You will see that it does an invert cropping, that is, it clears the points you want to not clear and preserves the points you wanted to clear. So, to solve your problem, assuming that the crop will be a convex shape, you need the following:
- get all the lines involved
- create a polygon of the "outside" of the polygons
- clear
Since you can have many different types of shapes and they can be convex or concave alike, it would be far-fetched for the purpose of a stackoverflow answer to implement the full logic, but this is how you can do it relatively easily:
Make sure that the lines are continuous, so if you draw the lines by hand from coordinates[0] to coordinates1 and then coordinates1 to coordinates[2], etc. then you will end up drawing the correct polygon (so each line between coordinates[i-1] and coordinates[i]) is a side of your polygon rather than a diagonal.
moveTocoordinates[0] and have a lineto for all coordinates, almost like you did, except for the very last one. So, you are effectively almost drawing the polygon except the very last line.continue with lineto to a side of the canvas (careful though, not to cross the internal of the polygon) and then to all the edges of the canvas (the corners), so you are effectively defining a polygon that's a. outside your polygon b. ends at your last coordinate c. does not include your last line
clear
moveTo your penultimate coordinate, lineTo your last coordinate and then proceed with lineTo calls to draw the
The snippet below is a proof-of-concept that is a good starting point, but it is not a proper solution yet.
const image = document.getElementById("pic");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
//draw the image on the canvas
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0, image.width, image.height);
//get mouse coordinates
let coordinates = [];
canvas.onclick = function clickEvent(e)
{
let rect = e.target.getBoundingClientRect();
let x = e.clientX - rect.left;
let y = e.clientY - rect.top;
let point = {
"x": x,
"y": y
};
coordinates.push(point);
ctx.fillStyle = "blue";
ctx.fillRect(x, y, 10, 10);
}
//debugging
console.log(coordinates);
//crop the image
function crop()
{
ctx.beginPath();
for(let i=0; i<coordinates.length - 1; i++)
{
ctx.lineTo(coordinates[i].x, coordinates[i].y);
}
ctx.lineTo(canvas.width, canvas.height);
ctx.lineTo(canvas.width, 0);
ctx.lineTo(0, 0);
ctx.lineTo(0, canvas.height);
ctx.lineTo(coordinates[0].x, coordinates[0].y);
ctx.stroke();
ctx.closePath();
ctx.clip();
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
function save()
{
let newCanvas = document.getElementById('canvas2');
//get new dimensions
let top = canvas.height;
let left = canvas.width;
let right = -canvas.width;
let bottom = -canvas.height;
for(let i=0; i<coordinates.length; i++)
{
if(coordinates[i].y < top)
top = coordinates[i].y;
if(coordinates[i].x < left)
left = coordinates[i].x;
if(coordinates[i].x > right)
right = coordinates[i].x;
if(coordinates[i].y > bottom)
bottom = coordinates[i].y;
}
newCanvas.width = right-left;
newCanvas.height = bottom-top;
//save the new image
newCanvas.getContext("2d").drawImage(canvas, left, top, newCanvas.width, newCanvas.height, 0, 0, newCanvas.width, newCanvas.height);
let downloadLink = document.createElement("a");
downloadLink.download = "crop.png";
downloadLink.href = newCanvas.toDataURL("image/png");
downloadLink.click();
}
h1 {
color: blue;
margin-bottom: 0px;
margin-top: 0px;
}
.image-div img {
max-width: 80vmin;
max-height: 80vmin;
border: 1px solid black;
}
.canvas-div {
margin-top: 240px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
</head>
<body>
<h1>Image:</h1>
<div class="image-div">
<img id="pic" src="https://icatcare.org/app/uploads/2018/07/Helping-your-new-cat-or-kitten-settle-in-1.png">
</div>
<div class="canvas-div">
<h1>Canvas:</h1>
<canvas id="canvas" style="border: 1px solid blue;"></canvas>
</div>
<!-- for exporting the final result -->
<canvas id="canvas2" style="display: none;"></canvas>
<button onclick="crop()">Crop</button>
<button onclick="save()">Save</button>
<br><br><br>
<!-- script -->
<script src="script.js"></script>
</body>
</html>
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Lajos Arpad |

