Canvas - clipping multiple images - javascript

I want to clip a bunch of images into hexagon shapes.
I have it sort of working, but the clipping is across all the hexes instead of each image clipping to only one hex. What am I doing wrong?
Here's a live demo:
http://codepen.io/tev/pen/iJaHB
Here's the js in question:
function polygon(ctx, x, y, radius, sides, startAngle, anticlockwise, img, imgX, imgY) {
if (sides < 3) return;
var a = (Math.PI * 2)/sides;
a = anticlockwise?-a:a;
ctx.save();
ctx.translate(x,y);
ctx.rotate(startAngle);
ctx.moveTo(radius,0);
for (var i = 1; i < sides; i++) {
ctx.lineTo(radius*Math.cos(a*i),radius*Math.sin(a*i));
}
ctx.closePath();
// add stroke
ctx.lineWidth = 5;
ctx.strokeStyle = '#056e96';
ctx.stroke();
// add stroke
ctx.lineWidth = 4;
ctx.strokeStyle = '#47b6c8';
ctx.stroke();
// add stroke
ctx.lineWidth = 2;
ctx.strokeStyle = '#056e96';
ctx.stroke();
// Clip to the current path
ctx.clip();
ctx.drawImage(img, imgX, imgY);
ctx.restore();
}
// Grab the Canvas and Drawing Context
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
// Create an image element
var img = document.createElement('IMG');
var img2 = document.createElement('IMG');
// When the image is loaded, draw it
img.onload = function () {
polygon(ctx, 120,120,100,6, 0,0,img, -120,-170);
}
img2.onload = function () {
polygon(ctx, 280,212,100,6, 0,0,img2, -150,-120);
}
// Specify the src to load the image
img.src = "http://farm8.staticflickr.com/7381/9601443923_051d985646_n.jpg";
img2.src = "http://farm6.staticflickr.com/5496/9585303170_d005d2aaa9_n.jpg";

You need to add this to your polygon() method:
ctx.beginPath();
See modified pen here
function polygon(ctx, x, y, radius, sides, startAngle, anticlockwise, img, ...
if (sides < 3) return;
var a = (Math.PI * 2)/sides;
a = anticlockwise?-a:a;
ctx.save();
ctx.translate(x,y);
ctx.rotate(startAngle);
ctx.beginPath(); /// for example here, before moveTo/lineTo
ctx.moveTo(radius,0);
...
If not the lines will accumulate so the second time you call polygon the previous polygon will still exist. That's why you see the image partly inside the first hexagon as well.

Related

Remove context Arc from canvas

I am working on a project where user upload structural diagram(engineering diagram). When I user double click on the intended location on canvas the speech to text engine turns on and listen for user comments and then it draw a small circle with different colors and fill text (count) on that location. I am saving comments, counts, coordinates of arc and other things in react state and displaying the list in a component with edit and delete button. When user press the delete button. comment and other property gets deleted from the state.
I want to remove the drawn arc from the canvas. How can I do it?
I have tried clearRect. But it is not working in this case.
Please let me know.
componentDidMount() {
const img = this.refs.image;
const canvas = this.refs.canvas;
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
img.onload = () => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
ctx.font = "40px Courier";
ctx.fillText('Drawing 1', 200, 100);
}
}
drawCircle(x, y, getcolor) {
const canvas = this.refs.canvas;
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(x, y, 8, 0, Math.PI * 2, false);
ctx.strokeStyle = "black";
ctx.stroke();
ctx.fillStyle = getcolor;
ctx.fill();
ctx.closePath();
// Text
ctx.beginPath();
ctx.font = "20px Arial"
ctx.fillStyle = "#00538a";
ctx.textAlign = "center";
ctx.fillText(this.state.count, x , y - 20);
ctx.fill();
ctx.closePath();
}
remove(id) {
this.setState({
comments: this.state.comments.filter(c => c.id !== id)
});
const canvas = this.refs.canvas;
const ctx = canvas.getContext('2d');
const arc = this.state.comments.filter(c => c.id === id);
let x = arc[0].coordinates.x;
let y = arc[0].coordinates.y
console.log("TCL: Drawing -> remove -> arc", arc[0].coordinates);
ctx.beginPath();
ctx.arc(x, y, 8, 0, Math.PI * 2, false);
ctx.clip();
ctx.clearRect(x-8, y-8, 16,16);
}
Thanks
Meet
As I mentioned in my comments the way you're trying to remove a circle from the canvas ain't gonna work.
If you call clearRect() on the canvas, it will essentially overwrite the target area including your original background image.
Instead you need to keep track of the circles - more precisely the position at which those should be drawn - using an array.
If you click the canvas -> add a circle element to an array -> clear the canvas -> draw the diagram again -> loop over the array to draw the circles on top
If you click the remove button of a circle -> search the array for this particular circle -> remove it from the array -> clear the canvas -> draw the diagram again -> loop over the array to draw the circles on top
Here's an example to illustrate what I'm talking about:
var comments = new Array();
var canvas = document.createElement("canvas");
canvas.style="float:left;"
canvas.width = 400;
canvas.height = 200;
document.body.appendChild(canvas);
var ctx = canvas.getContext("2d");
function updateCanvas() {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
ctx.font = "40px Courier";
ctx.fillText('Drawing 1', 200, 100);
for (var a = 0; a < comments.length; a++) {
ctx.beginPath();
ctx.arc(comments[a].x, comments[a].y, 8, 0, Math.PI * 2, false);
ctx.strokeStyle = "black";
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
}
}
var img = new Image();
img.onload = () => {
updateCanvas();
}
img.src = "https://picsum.photos/id/59/400/200";
function addCircle(e) {
var div = document.createElement("div");
div.innerHTML = "remove" + comments.length;
document.body.appendChild(div);
div.addEventListener("click", function(e) {
for (var a = 0; a < comments.length; a++) {
if (comments[a].div == e.target) {
comments.splice(a, 1);
break;
}
}
document.body.removeChild(e.target);
updateCanvas();
});
comments.push({
x: e.clientX,
y: e.clientY,
div: div
});
updateCanvas();
}
canvas.addEventListener("click", addCircle);
Everytime you click on the picture a 'remove' div will be created to the right of the canvas. if you click it, the associated circle will be removed.

How to optimize canvas particle system with particle objects

so I made a simple particle system with canvas and javascript (some jQuery) but I can't seem to make it run at more than 8fps on my old computer, this is the code:
var starList = [];
function Star(){
this.x = getRandomInt(0, canvas.width);
this.y = getRandomInt(0, canvas.height);
this.vx = getRandomInt(2,5);
this.size = this.vx/5;
this.opacity = getRandomInt(0, 5000) / 10000;
this.color = getRandomFromArray(["239, 207, 174", "162, 184, 229", "255, 255, 255"]);
this.draw = function(){
ctx.fillStyle = "rgba("+this.color+","+this.opacity+")";
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
},
this.move = function(){
this.x = this.x - this.vx;
if(this.x < 0) {
this.x = canvas.width;
this.opacity = getRandomInt(0, 5000) / 10000;
this.color = getRandomFromArray(["239, 207, 174", "162, 184, 229", "255, 255, 255"]);
this.y = getRandomInt(0, canvas.height);
this.size = this.vx/5;
this.vx = getRandomInt(2,5);
}
}
}
var canvas, ctx;
function setCanvas(){
canvas = $('canvas')[0];
ctx = canvas.getContext("2d");
canvas.width = $(window).width()/5;
canvas.height = $(window).height()/5;
}
setCanvas();
function generateStars(){
for(var i = 0; i < 5000; i++){
var star = new Star();
starList.push(star);
}
for(var i = 0; i < starList.length; i++) {
star = starList[i];
star.draw();
}
}
generateStars();
function loop() {
window.requestAnimationFrame(loop);
//clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
//draw and move stars
for(var i = 0; i < starList.length; i++) {
star = starList[i];
star.draw();
star.move();
}
}
I assume using objects for the particles (stars) and looping through the 5000 index array of objects, and executing those two functions is hard on the processor/gpu but how can I optimize this code?
I've seen that others avoid using functions on the constructor, and move and draw the particles when they loop through the array. Will that make it faster?
EDIT: Ignore the getRandomInt and similar functions, they are simple functions I use to generate random stuff.
The slowest part of your code is the path drawing commands:
ctx.fillStyle = "rgba("+this.color+","+this.opacity+")";
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
Canvas draws very quickly, but 5000 drawings will take some time.
Instead...
Create a spritesheet containing all the star variations you want to display.
Copying pixels from the spritesheet to the display canvas is much faster than executing drawing commands. This is especially true of drawing arcs where many points must be calculated around the circumference.
Importantly!
Limit the star variations -- the viewers won't notice that your stars are not infinitely random.
Then use the clipping version of drawimage to quickly draw each desired star-sprite from the spritesheet:
// set the global alpha
ctx.globalAlpha = getRandomInt(0, 5000) / 10000;
// cut the desired star-sprite from the spritesheet
// and draw it on the visible canvas
ctx.drawImage( spritesheet, // take from the spritesheet
this.sheetX, this.sheetY, this.width, this.height, // at this sprite's x,y
this.x, this.y, this.width, this.height) // and draw sprite to canvas
The spritesheet
You can use a second in-memory canvas as your spritesheet and create your star-sprites on the client-side when your app first starts up. The drawImage command will accept your second in-memory canvas as an image source(!).
var spritesheet=document.createElement('canvas');
var spriteContext=spriteSheet.getContext('2d');
...
// draw every variation of your stars on the spritesheet canvas
...

How to draw and rotate image along with text in HTML5 canvas

I am having a problem with drawing and rotating image on my canvas. Basically, my approach is to create the wheel of fortune which allows customization based on the prizes in form of array. The data in this array makes up the segment inside the wheel based on the number of indexes.
The data is very simple. It is just a simple JSON object like this
var prizes = [
{product:"Axe FX", img: "https://mdn.mozillademos.org/files/5395/backdrop.png"},
{product:"Musicman JPX", img: "https://mdn.mozillademos.org/files/5395/backdrop.png"},
{product:"Ibanez JEM777V", img: "https://mdn.mozillademos.org/files/5395/backdrop.png"}
];
This data is used to create the segments inside the wheel. So I want to place the text which is currently working like a charm for me.
When drawing the wheel, I separate into two main functions. One to draw the wheel and another to draw the segments inside the wheel.
var drawPartial = function(key, lastAngle, angle) {
var value = prizes[key].product;
ctx.save();
ctx.beginPath();
ctx.lineWidth = 6;
ctx.fillStyle = segColors[key];
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, size, lastAngle, angle);
ctx.lineTo(centerX, centerY);
ctx.closePath();
ctx.stroke();
ctx.fill();
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate((lastAngle+angle) / 2);
ctx.fillStyle = "#000";
ctx.fillText(value.substr(0,20), size / 2 + 20, 0);
ctx.restore();
ctx.restore();
}
var draw = function() {
var len = prizes.length;
var currentAngle = outCurrentAngle;
var lastAngle = currentAngle;
ctx.strokeStyle = '#000000';
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.font = "1.4em Arial";
for(var i = 1; i <= len; i++) {
var angle = (Math.PI*2) * (i/len) + currentAngle;
drawPartial(i-1, lastAngle, angle);
lastAngle = angle;
}
ctx.beginPath();
ctx.lineWidth = 2;
ctx.fillStyle = "#fff";
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, size/7, 0, Math.PI*2);
ctx.closePath();
// ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 10;
ctx.arc(centerX, centerY, size, 0, Math.PI*2);
ctx.closePath();
// ctx.stroke();
}
With the code above, I just simple call the draw() function and the wheel and all segments will be created accordingly. However, I want to draw the image in each segment but I don't know to make it work. This is the modification of drawPartial() for rendering images along with text
var drawPartial = function(key, lastAngle, angle) {
var value = prizes[key].product;
var img = new Image();
img.src = prizes[key].img;
img.onload = function() {
ctx.save();
ctx.drawImage(img,centerX,centerY);
ctx.save();
ctx.translate(centerX,centerY);
ctx.rotate((lastAngle+angle) / 2);
ctx.drawImage(img,centerX,centerY);
ctx.restore();
ctx.restore();
}
ctx.save();
ctx.beginPath();
ctx.lineWidth = 6;
ctx.fillStyle = segColors[key];
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, size, lastAngle, angle);
ctx.lineTo(centerX, centerY);
ctx.closePath();
ctx.stroke();
ctx.fill();
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate((lastAngle+angle) / 2);
ctx.fillStyle = "#000";
ctx.fillText(value.substr(0,20), size / 2 + 20, 0);
ctx.restore();
ctx.restore();
}
You can see that I add image and its src based on the prizes object which should be called in each iteration called by the main draw() function but it never renders any image in any segment.
What I want is. In each iteration of drawPartial(), I want the image to be placed in the segment along with the text and rotated according to the angle.
Please help...
Problem
In your img.onload function you are "double translating" your centerX & centerY.
ctx.translate(centerX,centerY) will move the canvas's [0,0] origin to [centerX,centerY].
So when you ctx.drawImage(img,centerX,centerY) to draw your image, you are really double moving.
As a result your image is really being drawn at [ centerX*2, centerY*2 ].
A additional thought: Preload your images
It's best to preload all your images. That way if an image fails to load you can take reparative action before you begin drawing your wheel.
Here is how to preload all of your images so they are available when you need to draw them onto your Wheel:
// your incoming JSON
var prizesJSON='[{"product":"Axe FX","img":"https://mdn.mozillademos.org/files/5395/backdrop.png"},{"product":"Musicman JPX","img":"https://mdn.mozillademos.org/files/5395/backdrop.png"},{"product":"Ibanez JEM777V","img":"https://mdn.mozillademos.org/files/5395/backdrop.png"}]';
// the JSON converted to a JS array of objects
var prizes=JSON.parse(prizesJSON);
// preload all images
var imageURLs=[];
var imgs=[];
var imagesOK=0;
// add prize images into the image preloader
for(var i=0;i<prizes.length;i++){
imageURLs.push(prizes[i].img);
}
startLoadingAllImages(imagesAreNowLoaded);
//
function startLoadingAllImages(callback){
for (var i=0; i<imageURLs.length; i++) {
var img = new Image();
imgs.push(img);
img.onload = function(){
imagesOK++;
if (imagesOK>=imageURLs.length ) {
callback();
}
};
img.onerror=function(){alert("image load failed");}
img.src = imageURLs[i];
}
}
//
function imagesAreNowLoaded(){
// add the img objects to your prizes array objects
for(var i=0;i<prizes.length;i++){
prizes[i].imageObject=imgs[i];
// just testing (add the img to the DOM)
document.body.appendChild(imgs[i]);
}
// All images are fully loaded
// So draw your wheel now!
}
body{ background-color: ivory; }
<h4>Testing: (1) Preload all images, (2) Add imgs to DOM</h4>
I had this laying around...
I see you already have code to draw your Wheel, but I had this code in my code archive so I offer it here just in case it has some use for you.
Here is an example of how to draw a "Wheel of Fortune" with each blade containing a prize image and text. The techniques used include:
context.translate to set the rotation point to the center of the wheel.
context.rotate to rotate each blade to its desired angle.
context.textAlign & context.textBaseline to draw centered text.
context.globalAlpha to lighten each blades color so the black text has good contrast.
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var PI=Math.PI;
var PI2=PI*2;
var bladeCount=10;
var sweep=PI2/bladeCount;
var cx=cw/2;
var cy=ch/2;
var radius=130;
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/house32x32transparent.png";
function start(){
for(var i=0;i<bladeCount;i++){
drawBlade(img,'House'+i,cx,cy,radius,sweep*i,sweep);
}
}
function drawBlade(img,text,cx,cy,radius,angle,arcsweep){
// save the context state
ctx.save();
// rotate the canvas to this blade's angle
ctx.translate(cx,cy);
ctx.rotate(angle);
// draw the blade wedge
ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(0,0);
ctx.arc(0,0,radius,0,arcsweep);
ctx.closePath();
ctx.stroke();
// fill the blade, but keep the color light
// so the black text has good contrast
ctx.fillStyle='white';
ctx.fill();
ctx.fillStyle=randomColor();
ctx.globalAlpha=0.30;
ctx.fill();
ctx.globalAlpha=1.00;
// draw the text
ctx.rotate(PI/2+sweep/2);
ctx.textAlign='center';
ctx.textBaseline='middle';
ctx.fillStyle='black';
ctx.fillText(text,0,-radius+50);
// draw the img
// (resize to 32x32 so be sure orig img is square)
ctx.drawImage(img,-16,-radius+10,32,32);
// restore the context to its original state
ctx.restore();
}
function randomColor(){
return('#'+Math.floor(Math.random()*16777215).toString(16));
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=300 height=300></canvas>

Clip canvas strokes to image

I'm trying to animate a pen filling in a variable width shape, currently working with html5 canvas. Ideally, I want to be able to have the sample below both start light and get colored in dark, as well as not appear at all and get colored in as though being drawn from nothing.
source-in doesn't seem to work, at least in firefox.
The image in question is a simple-ish SVG path, so if there's a reasonable way to generate canvas clip paths from SVG bezier paths, that would work as well.
var img = new Image();
img.src = "data:image/svg+xml;base64,...";
var xRecords = [...];
var yRecords = [...];
var canvas = document.getElementById("topCanvas");
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
ctx.strokeStyle = "#0000ff";
ctx.lineWidth = 90;
ctx.beginPath();
ctx.lineCap = "round";
ctx.moveTo(xRecords[0], yRecords[0]);
for(var i = 0; i < xRecords.length; i++) {
ctx.lineTo(xRecords[i], yRecords[i]);
ctx.stroke();
}
http://codepen.io/matelich/pen/gpLmOW
Generating alternate versions of the image is not a big deal if that would help. Oh and the animation is just a sample.
Update This works for the most part on Chrome and IE, but not on Firefox: http://codepen.io/matelich/pen/pJNeRq
FF doesn't like your SVG dataURL.
Option#1:
You could use a .png image instead.
Option#2:
You can create a cubic Bezier curve (like SVG's "C") in canvas using context.moveTo and context.bezierCurveTo.
Then your compositing will work fine even in FF:
var canvas = document.getElementById("topCanvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload=start;
img.src = 'https://dl.dropboxusercontent.com/u/139992952/multple/svg2png.png';
var xRecords = [117.6970666666671, 137.5037866666671, 139.6247579166671, 138.2627966666671, 134.75555041666712, 130.4406666666671, 126.65579291666711, 124.7385766666671, 126.0266654166671, 131.8577066666671, 155.5330366666671, 191.76562666666712, 224.94953666666714, 239.47882666666712];
var yRecords = [143.95648000000128, 200.21077333333463, 232.21213000000128, 264.735546666668, 296.24260333333467, 325.19488000000126, 350.05395666666794, 369.2814133333347, 381.33883000000134, 384.687786666668, 371.9640133333346, 346.7872000000013, 322.15182666666794, 311.05237333333463];
function start(){
canvas.width=img.width;
canvas.height=img.height;
ctx.beginPath();
ctx.rect(0, 0, 640, 640);
ctx.fillStyle = 'white';
ctx.fill();
ctx.globalCompositeOperation = "darker";
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = "lighter";
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = "xor";
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = "destination-over";
ctx.strokeStyle = "#0000ff";
ctx.lineWidth = 90;
ctx.beginPath();
ctx.lineCap = "round";
ctx.moveTo(xRecords[0], yRecords[0]);
var i = 0;
function drawNext()
{
i++;
console.log(i+"!");
if(i >= xRecords.length) { return; }
ctx.lineTo(xRecords[i], yRecords[i]);
ctx.stroke();
setTimeout(drawNext, 500);
}
drawNext();
}
body{ background-color: white; }
#canvas{border:1px solid red;}
<canvas id="topCanvas" width=300 height=300></canvas>

CanvasRenderingContext2D.fill() does not seem to work

I'm making a simply polygon draw method in HTML5 canvas. It will successfully outline the shape, but it will not fill it, even though it has been instructed to:
function PhysicsObj(Position /*Vector*/,Vertices /*Vector Array*/)
{
this.Position = Position
this.Vertices = Vertices
this.Velocity = new Vector(0,0);
this.Colour = "rgb(100,100,100)";
this.draw = function()
{
ctx.beginPath();
for(var point=0; point<Vertices.length-1; point++)
{
ctx.moveTo(Vertices[point].X+Position.X,Vertices[point].Y+Position.Y);
ctx.lineTo(Vertices[point+1].X+Position.X,Vertices[point+1].Y+Position.Y);
}
ctx.moveTo(Vertices[point].X+Position.X,Vertices[point].Y+Position.Y);
ctx.lineTo(Vertices[0].X+Position.X,Vertices[0].Y+Position.Y);
ctx.closePath();
ctx.fillStyle = this.Colour;
ctx.fill();
ctx.strokeStyle = this.Colour;
ctx.stroke();
}
}
var Polygon = new PhysicsObj(new Vector(100,100),[new Vector(50,50),new Vector(-50,50), new Vector(0,125)]);
Polygon.draw();
The method simply takes several vertices, and connects them into a path. It simply will not fill; I cannot figure out how to use the fill method.
Eliminate the extra moveTo commands:
var PositionX=50;
var PositionY=50;
var Vertices=[
{X:10,Y:10},
{X:100,Y:10},
{X:50,Y:50}
];
ctx.beginPath();
ctx.moveTo(Vertices[0].X+PositionX,Vertices[0].Y+PositionY);
for(var point=1; point<Vertices.length; point++)
{
ctx.lineTo(Vertices[point].X+PositionX,Vertices[point].Y+PositionY);
}
ctx.lineTo(Vertices[0].X+PositionX,Vertices[0].Y+PositionY);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
ctx.strokeStyle = "black";
ctx.stroke();

Categories

Resources