draw outer and inner border around any canvas shape - javascript

How to draw outer and inner border around any canvas shape?
I'm drawing several stroke-only shapes on an html canvas, and I would like to draw an inner and outer border around them.
draft example:
Is there a generic why to do it for any shape (assuming it's a closed stroke-only shape)?

Two methods
There is no inbuilt way to do this and there are two programmatic ways that I use. The first is complicated and involves expanding and contracting the path then drawing along that path. This works for most situations but will fail in complex situation, and the solution has many variables and options to account for these complications and how to handle them.
The better of the two
The second and easiest way that I present below is by using the ctx.globalCompositeOperation setting to mask out what you want drawn or not. As the stroke is drawn along the center and the fill fills up to the center you can draw the stroke at twice the desired width and then either mask in or mask out the inner or outer part.
This does become problematic when you start to create very complex images as the masking (Global Composite Operation) will interfere with what has already been drawn.
To simplify the process you can create a second canvas the same size as the original as a scratch space. You can then draw the shape on he scratch canvas do the masking and then draw the scratch canvas onto the working one.
Though this method is not as fast as computing the expanded or shrunk path, it does not suffer from the ambiguities faced by moving points in the path. Nor does this method create the lines with the correct line join or mitering for the inside or outside edges, for that you must use a the other method. For most purposes the masking it is a good solution.
Below is a demo of the masking method to draw an inner or outer path. If you modify the mask by including drawing a stroke along with the fill you can also set an offset so that the outline or inline will be offset by a number of pixels. I have left that for you. (hint add stroke and set the line width to twice the offset distance when drawing the mask).
var demo = function(){
/** fullScreenCanvas.js begin **/
var canvas = ( function () {
canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** CreateImage.js begin **/
// creates a blank image with 2d context
var createImage = function(w,h){
var image = document.createElement("canvas");
image.width = w;
image.height =h;
image.ctx = image.getContext("2d");
return image;
}
/** CreateImage.js end **/
// define a shape for demo
var shape = [0.1,0.1,0.9,0.1,0.5,0.5,0.8,0.9,0.1,0.9];
// draws the shape as a stroke
var strokeShape = function (ctx) {
var w, h, i;
w = canvas.width;
h = canvas.height;
ctx.beginPath();
ctx.moveTo(shape[0] *w, shape[1] *h)
for (i = 2; i < shape.length; i += 2) {
ctx.lineTo(shape[i] * w, shape[i + 1] * h);
}
ctx.closePath();
ctx.stroke();
}
// draws the shape as filled
var fillShape = function (ctx) {
var w, h, i;
w = canvas.width;
h = canvas.height;
ctx.beginPath();
ctx.moveTo(shape[0] * w,shape[1] * h)
for (i = 2; i < shape.length; i += 2) {
ctx.lineTo(shape[i]*w,shape[i+1]*h);
}
ctx.closePath();
ctx.fill();
}
var drawInOutStroke = function(width,style,where){
// clear the workspace
workCtx.ctx.globalCompositeOperation ="source-over";
workCtx.ctx.clearRect(0, 0, workCtx.width, workCtx.height);
// set the width to double
workCtx.ctx.lineWidth = width*2;
workCtx.ctx.strokeStyle = style;
// fill colour does not matter here as its not seen
workCtx.ctx.fillStyle = "white";
// can use any join type
workCtx.ctx.lineJoin = "round";
// draw the shape outline at double width
strokeShape(workCtx.ctx);
// set comp to in.
// in means leave only pixel that are both in the source and destination
if (where.toLowerCase() === "in") {
workCtx.ctx.globalCompositeOperation ="destination-in";
} else {
// out means only pixels on the destination that are not part of the source
workCtx.ctx.globalCompositeOperation ="destination-out";
}
fillShape(workCtx.ctx);
ctx.drawImage(workCtx, 0, 0);
}
// clear in case of resize
ctx.globalCompositeOperation ="source-over";
ctx.clearRect(0,0,canvas.width,canvas.height);
// create the workspace canvas
var workCtx = createImage(canvas.width, canvas.height);
// draw the outer stroke
drawInOutStroke((canvas.width + canvas.height) / 45, "black", "out");
// draw the inner stroke
drawInOutStroke((canvas.width + canvas.height) / 45, "red", "in");
// draw the shape outline just to highlight the effect
ctx.strokeStyle = "white";
ctx.lineJoin = "round";
ctx.lineWidth = (canvas.width + canvas.height) / 140;
strokeShape(ctx);
};
// run the demo
demo();
// incase fullscreen redraw it all
window.addEventListener("resize",demo)

Related

Clip part of canvas to another canvas

So I have this piece of code that I use for erasing and restoring parts of an image with a (for example) removed background. Erasing from the main canvas is simple and the user can erase a circular shape with a line between points.
if(removeMode) {
ctxs[index].globalCompositeOperation = 'destination-out';
ctxs[index].beginPath();
ctxs[index].arc(x, y, radius, 0, 2 * Math.PI);
ctxs[index].fill();
ctxs[index].lineWidth = 2 * radius;
ctxs[index].beginPath();
ctxs[index].moveTo(old.x, old.y);
ctxs[index].lineTo(x, y);
ctxs[index].stroke();
}
The problem is with the restoring. Currently I am able to copy parts of the original image to the main canvas but only in a rectangular shape using the getImageData() and putImageData() functions.
ctxs[index].globalCompositeOperation = 'source-out';
ctxs[0].putImageData(ctxs[1].getImageData(x-radius, y-radius, 2*radius, 2*radius), x-radius, y-radius);
Ideally I would like to clip a part of the original image canvas to the main canvas with a shape similar to the erasing feature. I have tried the clip() function but honestly I am not sure how to go about it. Here is what I initially tried to clip a part of a canvas.
ctxs[index].beginPath();
ctxs[index].arc(x, y, radius, 0, Math.PI * 2);
ctxs[index].fill();
ctxs[index].lineWidth = 2 * radius;
ctxs[index].beginPath();
ctxs[index].moveTo(old.x, old.y);
ctxs[index].lineTo(x, y);
ctxs[index].stroke();
ctxs[index].clip();
How do I copy a custom shape from a canvas to another canvas?
Thanks in advance,
Edit:
I have also thought of using a mask where I would create the mask as such (example using numpy in python):
Y, X = np.ogrid[:canvas_height, :canvas_width]
# Y, X are matrix values and x, y are coordinates of the cursor within the image
center_dist = np.sqrt((X - x)**2 + (Y-y)**2)
# create mask
mask = center_dist <= radius
# omit everything except circular shape from mask
circular_img = original_img.copy()
circular_img[~mask] = 0
# combine images
new_img = np.maximum(original_img, new_img)
Example of what I have now
Simpler solution
Every shape fits into a rectangle.
Proof Your canvas is a rectangle and already contains the shape.
As a result, you can determine the smallest possible rectangle that contains the full shape and store that. It will necessarily contain your shape. Upon reload you will need to know the shape's boundaries inside the copy though, so that info will also be needed.
Harder, but more precise solution
You can create a structure and store the content, point-by-point (yet, this will be not very performant):
const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
let myShape = [];
for (let x = 0; x < canvas.width; x++) {
for (let y = 0; y < canvas.height; y++) {
if (inShape(x, y, canvas)) {
myShape.push({x, y, content: data});
}
}
}
The snippet above assumes that you have properly implemented inShape.
Homogeneous shape
If all the points inside the shape are similar, then you will need to only know where the boundaries of the shape were. If you have a convex polygon, for example, then you will need to know where its center is and what the boundaries are. If you have a filled circle, then you will only need its center and radius. The geometrical data you need largely depend on what shape you have.
Keep using composite operations.
"destination-out" will indeed remove the previous pixels that do overlap with the new ones.
If you use the inverse "destination-in", only the previous pixels that do overlap with the new ones are kept.
So you store your original image intact, and then use one of these modes to render it given the action you want to perform.
Here since it seems we are in a paint-like configuration, I guess it makes more sense to erase the final result and restore the original canvas. For this we need a third canvas, detached where we'll draw the "restoration" part on its own before drawing that back to the visible canvas:
(async () => {
// the main, visible canvas
const canvas = document.querySelector("canvas");
canvas.width = 500;
canvas.height = 250;
const ctx = canvas.getContext("2d");
// a detached canvas context to do the compositing
const detached = canvas.cloneNode().getContext("2d");
// the "source" canvas (here just an ImageBitmap)
const originalCanvas = await loadImage();
ctx.lineWidth = detached.lineWidth = 8;
// we store every drawing in its own Path2D object
const paths = [];
let down = false;
const checkbox = document.querySelector("input");
canvas.onmousedown = (evt) => {
down = true;
const newPath = new Path2D();
newPath.isEraser = !checkbox.checked;
paths.push(newPath);
};
canvas.onmouseup = (evt) => { down = false; };
canvas.onmousemove = (evt) => {
if (!down) { return; }
const {x, y} = parseMouseEvent(evt);
paths[paths.length - 1].lineTo(x, y);
redraw();
};
redraw();
function redraw() {
// clear the visible context
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(originalCanvas, 0, 0);
paths.forEach((path) => {
if (path.isEraser) {
// erase the current content
ctx.globalCompositeOperation = "destination-out";
ctx.stroke(path);
}
else {
// to restore
// we do the compositing on the detached canvas
detached.globalCompositeOperation = "source-over";
detached.drawImage(originalCanvas, 0, 0);
detached.globalCompositeOperation = "destination-in";
detached.stroke(path);
// draw the result on the main context
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(detached.canvas, 0, 0);
}
});
}
})().catch(console.error);
async function loadImage() {
const url = "https://picsum.photos/500/250";
const req = await fetch(url);
const blob = req.ok && await req.blob();
return createImageBitmap(blob);
}
function parseMouseEvent(evt) {
const rect = evt.target.getBoundingClientRect();
return {x: evt.clientX - rect.left, y: evt.clientY - rect.top };
}
canvas { border: 1px solid; vertical-align: top }
<label>erase/restore <input type="checkbox"></label>
<canvas></canvas>
Note that here I do create new paths every time, but you could very well use the same ones for both erasing and restoring (and even any other graphic source).

Efficient HTML5 Canvas Glow Effect without shape

I am creating a game using the HTML5 Canvas element, and as one of the visual effects I would like to create a glow (like a light) effect. Previously for glow effects I found solutions involving creating shadows of shapes, but these require a solid shape or object to cast the shadow. What I am looking for is a way to create something like an ambient light glow with a source location but no object at the position.
Something I have thought of was to define a centerpoint x and y and create hundreds of concentric circles, each 1px larger than the last and each with a very low opacity, so that together they create a solid center and a transparent edge. However, this is very computationally heavy and does not seem elegant at all, as the resulting glow looks awkward.
While this is all that I am asking of and I would be more than happy to stop here, bonus points if your solution is A) computationally light, B) modifiable to create a focused direction of light, or even better, C) if there was a way to create an "inverted" light system in which the entire screen is darkened by a mask and the shade is lifted where there is light.
I have done several searches, but none have turned up any particularly illuminating results.
So I'm not quite sure what you want, but I hope the following snippet will help.
Instead of creating a lot of concentric circles, create one radialGradient.
Then you can combine this radial gradient with some blending, and even filters to modify the effect as you wish.
var img = new Image();
img.onload = init;
img.src = "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/car.svg";
var ctx = c.getContext('2d');
var gradCtx = c.cloneNode().getContext('2d');
var w, h;
var ratio;
function init() {
w = c.width = gradCtx.canvas.width = img.width;
h = c.height = gradCtx.canvas.height = img.height;
draw(w / 2, h / 2)
updateGradient();
c.onmousemove = throttle(handleMouseMove);
}
function updateGradient() {
var grad = gradCtx.createRadialGradient(w / 2, h / 2, w / 8, w / 2, h / 2, 0);
grad.addColorStop(0, 'transparent');
grad.addColorStop(1, 'white');
gradCtx.fillStyle = grad;
gradCtx.filter = "blur(5px)";
gradCtx.fillRect(0, 0, w, h);
}
function handleMouseMove(evt) {
var rect = c.getBoundingClientRect();
var x = evt.clientX - rect.left;
var y = evt.clientY - rect.top;
draw(x, y);
}
function draw(x, y) {
ctx.clearRect(0, 0, w, h);
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(gradCtx.canvas, x - w / 2, y - h / 2);
ctx.globalCompositeOperation = 'lighten';
ctx.fillRect(0, 0, w, h);
}
function throttle(callback) {
var active = false; // a simple flag
var evt; // to keep track of the last event
var handler = function() { // fired only when screen has refreshed
active = false; // release our flag
callback(evt);
}
return function handleEvent(e) { // the actual event handler
evt = e; // save our event at each call
if (!active) { // only if we weren't already doing it
active = true; // raise the flag
requestAnimationFrame(handler); // wait for next screen refresh
};
}
}
<canvas id="c"></canvas>

how do i create a puzzle piece with bevel effect in edged in canvas html5

i am trying to create the puzzle pieces with the effect in the image but i am unable to get it through.
Can anyone help me how to create the pieces with canvas and html5.
thanks
You can draw a beveled edge puzzle piece this way.
Draw the part of the image you want to use for the piece positioned correctly
Build a path for the puzzle piece
Punch out the piece using composite mode destination-in
Add rect/bounding box to path
Define dark shadow and fill using composite mode source-atop
Move shadow and change color to bright, fill again
What you need to do in addition is to calculate the offset for the image you're using for each piece. You could also draw each piece into a separate canvas and extract the canvas as image which you later use to draw/show the pieces.
Example
var ctx = c.getContext("2d"), img = new Image;
img.onload = demo;
img.src = "https://pixeloceanblog.files.wordpress.com/2016/05/pres_frontcorner.jpg";
function demo() {
ctx.translate(-20,-90); // just to compensate for demo puzzle piece position
// 1) Draw puzzle pieze graphics
ctx.drawImage(this, -220, -110);
// 2) build path for puzzle piece
puzzlePath(ctx);
// 3) Punch out piece
ctx.globalCompositeOperation = "destination-in";
ctx.fill();
// 4) Add rect to make stencil
ctx.rect(0, 0, c.width, c.height);
// 5) Build dark shadow
ctx.shadowBlur = 7;
ctx.shadowOffsetX = -7;
ctx.shadowOffsetY = -7;
ctx.shadowColor = "rgba(0,0,0,0.8)";
// 6) Draw stencil with shadow but only on non-transparent pixels
ctx.globalCompositeOperation = "source-atop";
ctx.fill();
// 7) move shadow and change color to white transparent
ctx.shadowOffsetX = 7;
ctx.shadowOffsetY = 7;
ctx.shadowColor = "rgba(255,255,255,0.8)";
ctx.fill();
// DONE!
// Puzzle path for demo
function puzzlePath(ctx) {
ctx.beginPath();
ctx.moveTo(36.421871,256.82809);
ctx.bezierCurveTo(36.981411,272.02753,42.075181,289.07672,52.984371,298.17184);
ctx.bezierCurveTo(81.172101,311.4011,68.157281,279.26413,87.671871,275.48434);
ctx.bezierCurveTo(107.18646,271.70455,108.26562,294.39059,108.26562,294.39059);
ctx.lineTo(108.26562,294.39059);
ctx.lineTo(108.26562,392.14059);
ctx.bezierCurveTo(108.26562,392.14059,205.20313,392.01559,205.20312,392.01559);
ctx.bezierCurveTo(205.20312,392.01559,227.88915,390.93642,224.10937,371.42184);
ctx.bezierCurveTo(221.03829,355.56624,199.23133,361.18908,198.26562,348.73434);
ctx.bezierCurveTo(198.04276,345.86017,198.94138,342.01954,201.42187,336.73434);
ctx.bezierCurveTo(210.51699,325.82514,227.56618,320.73138,242.76562,320.17184);
ctx.bezierCurveTo(248.37657,319.96528,253.65356,320.45514,258.26562,321.42184);
ctx.bezierCurveTo(258.47031,321.44665,258.68579,321.48913,258.89062,321.51559);
ctx.bezierCurveTo(272.00503,323.2095,285.30842,328.27739,292.98437,337.48434);
ctx.bezierCurveTo(306.21364,365.67208,274.07665,352.65725,270.29687,372.17184);
ctx.bezierCurveTo(267.54294,386.39004,278.77521,390.78229,285.10937,392.14059);
ctx.lineTo(388.01562,392.14059);
ctx.lineTo(388.01562,290.54684);
ctx.bezierCurveTo(389.23388,284.34562,393.50469,272.30575,408.14062,275.14059);
ctx.bezierCurveTo(427.65522,278.92038,414.64038,311.02611,442.82812,297.79684);
ctx.bezierCurveTo(452.03508,290.1209,457.10296,276.8175,458.79687,263.70309);
ctx.bezierCurveTo(458.82334,263.49827,458.86581,263.28278,458.89062,263.07809);
ctx.bezierCurveTo(459.85733,258.46605,460.34718,253.18904,460.14062,247.57809);
ctx.bezierCurveTo(459.58109,232.37865,454.48732,215.32946,443.57812,206.23434);
ctx.bezierCurveTo(415.3904,193.00508,428.40521,225.14205,408.89062,228.92184);
ctx.bezierCurveTo(389.37605,232.70164,388.29687,210.01559,388.29687,210.01559);
ctx.lineTo(388.01562,112.82809);
ctx.bezierCurveTo(388.01562,112.82809,289.76561,113.4531,289.76562,113.45309);
ctx.bezierCurveTo(289.76562,113.45309,267.07957,114.53227,270.85937,134.04684);
ctx.bezierCurveTo(274.63916,153.56143,306.77612,140.54662,293.54687,168.73434);
ctx.bezierCurveTo(284.45175,179.64354,267.40256,184.73731,252.20312,185.29684);
ctx.bezierCurveTo(246.59217,185.5034,241.31517,185.01355,236.70312,184.04684);
ctx.bezierCurveTo(236.49843,184.02203,236.28293,183.97956,236.07812,183.95309);
ctx.bezierCurveTo(222.96371,182.25918,209.6603,177.1913,201.98437,167.98434);
ctx.bezierCurveTo(199.50388,162.69914,198.60526,158.85851,198.82812,155.98434);
ctx.bezierCurveTo(199.79383,143.5296,221.56954,149.15245,224.64062,133.29684);
ctx.bezierCurveTo(228.16732,115.08894,209.3009,113.03555,206.67187,112.82809);
ctx.lineTo(108.26562,112.82809);
ctx.lineTo(108.26562,214.98434);
ctx.bezierCurveTo(106.78368,221.38993,102.27374,231.94857,88.421871,229.26559);
ctx.bezierCurveTo(68.907281,225.48581,81.922111,193.38007,53.734371,206.60934);
ctx.bezierCurveTo(44.527431,214.28529,39.459541,227.58868,37.765621,240.70309);
ctx.bezierCurveTo(37.739161,240.90792,37.696681,241.1234,37.671871,241.32809);
ctx.bezierCurveTo(36.705171,245.94014,36.215311,251.21714,36.421871,256.82809);
}
}
body {background:#777}
<canvas id=c width=500 height=500></canvas>
Using masking to Apply a inner bevel FX
A very quick answer. I am sure someone can improve on this and are welcome to.
Function takes an image and returns a new image with a bevel applied.
Function arguments
image. The image to apply the bevel to
amount. The strength of the bevel 0 is none and 1 is full
offsetX
offsetY. The offset of the bevel.
blur. The blur. Must be greater or equal to 0
type. The type of bevel as string, ethier, 'shadow', 'light' or as a colour eg
'black'
Returns an image (as canvas)
Example usage
var image = new Image();
image.src = "imageURL.png";
image.onload = function(){
var bevImage = innerBevel(this,0.5,-4,-4,4,"shadow"); // bevel shadow
bevImage = innerBevel(bevImage,0.5,4,4,4,"light"); // bevel highlight
if(typeof ctx !== 'undefined'){
ctx.drawImage(bevImage,0,0); // draw the image to the current context
}
}
The function
function innerBevel(image, amount, offsetX, offsetY, blur, type){
var c = document.createElement("canvas");
c.width = image.width + (Math.abs(offsetX) + blur) * 2; // cludge could be a better fit
c.height = image.height + (Math.abs(offsetY) + blur) * 2; // cludge could be a better fit
var ctx1 = c.getContext("2d");
ctx1.fillRect(0,0,c.width,c.height); // fill pixels
// create the shadow mask
ctx1.globalCompositeOperation = "destination-out";
ctx1.drawImage(image, Math.abs(offsetX)+blur, Math.abs(offsetY)+blur); // create inverse mask
// create second image.
var c1 = document.createElement("canvas");
c1.width = image.width;
c1.height = image.height;
var ctx2 = c1.getContext("2d");
// copy the image
ctx2.drawImage(image,0,0);
// create the shadow and draw it as a shadow from the mask
ctx2.save();
if(type === "shadow"){
ctx2.shadowColor = "black";
ctx2.globalCompositeOperation = "multiply";
}else
if(type === "light"){
ctx2.shadowColor = "White";
ctx2.globalCompositeOperation = "lighter";
}else{
ctx2.shadowColor = type;
}
ctx2.globalAlpha = amount;
ctx2.shadowOffsetX = offsetX;
ctx2.shadowOffsetY = offsetY;
ctx2.shadowBlur = blur;
ctx2.drawImage(c,-(Math.abs(offsetX) + blur), -(Math.abs(offsetY) + blur)); // create inverse mask
ctx2.restore(); // remove the shadow settings
// mask out the unwanted pixels
ctx2.globalCompositeOperation = "destination-out";
ctx2.drawImage(c,-(Math.abs(offsetX) + blur), -(Math.abs(offsetY) + blur));
ctx2.globalCompositeOperation = "source-over";
return c1; // return the new beveled image;
}

html5 canvas redraw on resize

I have two canvas elements and need them to be resized on buttons click.
<div class="sDetails"><div>
<div id="canvasDiv" style="width: 310px;"><canvas id="canvasGraph"></canvas></div></div>
<div class="kDetails"><div><div>
<div id="canvasDiv" style="width: 310px; height: 240px;"><canvas id="canvasGraph"></canvas></div></div>
and the script:
var sketch;var sketch_sl;var onPaint;var canvas=null;var ctx=null;var tmp_ctx=null;
function drawCanvas(div) {
canvas = document.querySelector(div + " #canvasGraph");
ctx = canvas.getContext('2d');
sketch = document.querySelector(div + " #canvasDiv");
sketch_sl = getComputedStyle(sketch);
canvas.width = parseInt(sketch_style.getPropertyValue('width'));
canvas.height = parseInt(sketch_style.getPropertyValue('height'));
tmp_canvas = document.createElement('canvas');
tmp_ctx = tmp_canvas.getContext('2d');
tmp_canvas.id = 'tmp_canvas';
tmp_canvas.width = canvas.width;
tmp_canvas.height = canvas.height;
sketch.appendChild(tmp_canvas);
the redraw function:
// here I must redraw my lines resized 2 times ( *cScale ) where cScale=2 or =1
function drawScales(ctx, canvas)
ctx.strokeStyle = 'green';
ctx.fillStyle = 'green';
ctx.beginPath();
ctx.moveTo(5, 0);
ctx.lineTo(0, canvas.height);
scaleStep = 24*cScale;
for some reason it works really bad, old positions stay.
Is there a way to completely delete the whole canvas and append it or redraw it completely?
I tried canvas.width=canvas.width, tried ctx.clearRect(0, 0, canvas.width, canvas.height);tmp_ctx.clearRect(0, 0, canvas.width, canvas.height);, tried $(".sDetails #canvasGraph")[0].reset();
logically, drawCanvas(".sDetails");drawLines(ctx, canvas); should redraw it from scratch but it will not.
Resize the canvas element's width & height and use context.scale to redraw the original drawings at their newly scaled size.
Resizing the canvas element will automatically clear all drawings off the canvas.
Resizing will also automatically reset all context properties back to their default values.
Using context.scale is useful because then the canvas will automatically rescale the original drawings to fit on the newly sized canvas.
Important: Canvas will not automatically redraw the original drawings...you must re-issue the original drawing commands.
Illustration with 2 canvases at same size (their sizes are controlled by range controls)
Illustration with left canvas resized larger
Illustration with right canvas resized larger
Here's example code and a Demo. This demo uses range elements to control the resizing, but you can also do the resizing+redrawing inside window.onresize
var canvas1=document.getElementById("canvas1");
var ctx1=canvas1.getContext("2d");
var canvas2=document.getElementById("canvas2");
var ctx2=canvas2.getContext("2d");
var originalWidth=canvas1.width;
var originalHeight=canvas1.height;
var scale1=1;
var scale2=1;
$myslider1=$('#myslider1');
$myslider1.attr({min:50,max:200}).val(100);
$myslider1.on('input change',function(){
var scale=parseInt($(this).val())/100;
scale1=scale;
redraw(ctx1,scale);
});
$myslider2=$('#myslider2');
$myslider2.attr({min:50,max:200}).val(100);
$myslider2.on('input change',function(){
var scale=parseInt($(this).val())/100;
scale2=scale;
redraw(ctx2,scale);
});
draw(ctx1);
draw(ctx2);
function redraw(ctx,scale){
// Resizing the canvas will clear all drawings off the canvas
// Resizing will also automatically clear the context
// of all its current values and set default context values
ctx.canvas.width=originalWidth*scale;
ctx.canvas.height=originalHeight*scale;
// context.scale will scale the original drawings to fit on
// the newly resized canvas
ctx.scale(scale,scale);
draw(ctx);
// always clean up! Reverse the scale
ctx.scale(-scale,-scale);
}
function draw(ctx){
// note: context.scale causes canvas to do all the rescaling
// math for us, so we can always just draw using the
// original sizes and x,y coordinates
ctx.beginPath();
ctx.moveTo(150,50);
ctx.lineTo(250,150);
ctx.lineTo(50,150);
ctx.closePath();
ctx.stroke();
ctx.fillStyle='skyblue';
ctx.beginPath();
ctx.arc(150,50,20,0,Math.PI*2);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.arc(250,150,20,0,Math.PI*2);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();;
ctx.arc(50,150,20,0,Math.PI*2);
ctx.fill();
ctx.stroke();
}
$("#canvas1, #canvas2").mousemove(function(e){handleMouseMove(e);});
var $mouse=$('#mouse');
function handleMouseMove(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
var bb=e.target.getBoundingClientRect();
mouseX=parseInt(e.clientX-bb.left);
mouseY=parseInt(e.clientY-bb.top);
if(e.target.id=='canvas1'){
$mouse.text('Mouse1: '+mouseX/scale1+' / '+mouseY/scale1+' (scale:'+scale1+')');
}else{
$mouse.text('Mouse2: '+mouseX/scale2+' / '+mouseY/scale2+' (scale:'+scale2+')');
}
}
body{ background-color: ivory; }
canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div>Resize left canvas</div>
<input id=myslider1 type=range><br>
<div>Resize right canvas</div>
<input id=myslider2 type=range><br>
<h4 id=mouse>Mouse coordinates:</h4>
<canvas id="canvas1" width=300 height=300></canvas>
<canvas id="canvas2" width=300 height=300></canvas>
If you need scale-independent positions you could use normalized values ([0, 1]) instead and use the size of canvas as the scale factor. This way you can scale and store values without too much concern about the actual target size.
You would also be able to use the mouse positions almost as is and normalize by just dividing them on canvas size.
For example:
When rendering, a point of (1,1) will always draw in lower-right corner as you would do (1 * canvas.width, 1 * canvas.height).
When you store a point you would use the mouse position and divide it on the canvas dimension, for example, if I click in the lower right corner of a canvas of size 400x200, the points would be 400/400 = 1, 200/200 = 1.
Note that width and height would be exclusive (ie. width-1 etc.), but for sake of simplicity...
Example
In this example you can start with any size of the canvas, draw points which are normalized, change size of canvas and have the points redrawn proportionally relative to the original position.
var rng = document.querySelector("input"),
c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
points = [];
// change canvas size and redraw all points
rng.onchange = function() {
c.width = +this.value;
render();
};
// add a new normalized point to array
c.onclick = function(e) {
var r = this.getBoundingClientRect(), // to adjust mouse position
x = e.clientX - r.left,
y = e.clientY - r.top;
points.push({
x: x / c.width, // normalize value to range [0, 1]
y: y / c.height
}); // store point
render(); // redraw (for demo)
};
function render() {
ctx.clearRect(0, 0, c.width, c.height); // clear canvas
ctx.beginPath(); // clear path
for(var i = 0, p; p = points[i]; i++) { // draw points as fixed-size circles
var x = p.x * c.width, // normalized to absolute values
y = p.y * c.height;
ctx.moveTo(x + 5, y);
ctx.arc(x, y, 5, 0, 6.28);
ctx.closePath();
}
ctx.stroke();
}
canvas {background:#ddd}
<h3>Click on canvas to add points, then resize</h3>
<label>Width: <input type="range" min=50 max=600 value=300></label><br>
<canvas></canvas>
I decided to use a scale variable to resize my scales. I resize the canvas canvas.width *= 2; and then I redraw my scales.
var scaleStep;
and use add it into the code: ctx.lineTo(12*24*cScale+12, canvas.height-24); where the scaling needs to be done.
The scaleStep is 2 when maximizing the canvas and 1 when returning to the original size.

Canvas Animate Clipping Mask Path

I want to (I have accomplished 1-3):
grab an image from the page
add it to a canvas element
clip the image to a mask
move (animate) the mask around the image
I have accomplished the first 3, but cannot figure out how to move the mask.. Please help!
// get the canvas
var canvas = document.getElementById('c');
var context = canvas.getContext('2d');
// get a div from the page
var stuff = document.getElementsByName('testElem')[0];
// add a class to the div
stuff.setAttribute('class', 'pleaseWork');
var newImage = new Image();
// get the background image of the div
var bgClass = document.getElementsByClassName('pleaseWork')[0].style.backgroundImage;
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = 75;
var offset = 50;
// clip the context to create a circular clipping mask
context.save();
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
context.clip();
// on load put the image into the clipping mask
newImage.onload = function () {
context.drawImage(newImage,0,0);
}
// put the background image from the div into the canvas (without the url())
newImage.src = bgClass.replace(/^url|[\(\)]/g, '');
How can I move (animate) the clipping mask from the canvas to show different parts of the clipped image?
Thanks for any ideas!
You can put your clip+drawing code in a function and call that function inside an animation loop:
Example code and a Demo: http://jsfiddle.net/m1erickson/07mzbat9/
Drawing Function:
function draw(){
// clear the canvas
ctx.clearRect(0,0,cw,ch);
// save the unclipped context
ctx.save();
// draw a circular path
ctx.beginPath();
ctx.arc(cx,cy,radius,0,PI2);
ctx.closePath();
ctx.stroke();
// create a clipping path from the circular path
// use the clipping path to restrict drawing
ctx.clip();
ctx.drawImage(img,0,0);
// restore the unclipped context (to remove clip)
ctx.restore();
}
Animation loop:
var cw=canvas.width;
var ch=canvas.height;
var cx=50;
var cy=50;
var radius=35;
var PI2=Math.PI*2;
// animate the mask across the canvas
function animate(time){
if(cx-radius>cw || cy-radius>ch){return;}
requestAnimationFrame(animate);
cx+=0.2;
cy+=0.2;
draw();
}

Categories

Resources