Related
I have a flat image that I want to transform so it looks like a projection on an elliptic cylinder.
I've found the great script from Blindman67 for a regular cylinder at https://stackoverflow.com/a/40999097/4273683
However I don't understand, how to change the script to get an elliptic result. Any idea?
Thanks a lot for help.
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
var canvas = createImage(400,400);
var ctx = canvas.ctx;
document.body.appendChild(canvas)
ctx.clearRect(0,0,500,500)
var image = createImage(400,200);
image.ctx.font = "60px arial";
image.ctx.textAlign = "center";
image.ctx.fillStyle = "#7F5";
image.ctx.fillRect(0,0,image.width,image.height)
image.ctx.fillStyle = "white";
image.ctx.fillText("Wrap around",200,60)
image.ctx.fillText("Some images",200,140)
function draw(ang,tilt, perspective){
var step = 1/(Math.max(image.width,400));
for(var i = 0; i < 1; i += step){
var a = i * Math.PI;
var a1 = (i+ step*2) * Math.PI ;
var ix = i * image.width*1.2;
var iw = step * image.width*1.2;
a += ang * Math.PI * 2;
a1 += ang * Math.PI * 2;
a = Math.PI -a;
a1 = Math.PI -a1;
var x = canvas.width * 0.5;
var y = canvas.height * 0.1;
var x1 = x + Math.cos(a1) * 110;
var y1 = y + Math.sin(a) * tilt;
x += Math.cos(a) * 110;
y += Math.sin(a) * tilt;
var s = Math.sin(a);
var s1 = Math.sin(a1);
if(s > 0 || s1 > 0){
ctx.drawImage(image,ix,0,iw,image.height,x1,y- s * perspective*0.5,x-x1,200 + s * perspective)
}
}
}
var w = canvas.width;
var h = canvas.height;
// main update function
function update(timer){
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.fillStyle = "black"
ctx.fillRect(0,0,w,h);
draw(timer / 2000, 40,30)
requestAnimationFrame(update);
}
requestAnimationFrame(update);
I am visualising flight paths with D3 and Canvas. In short, I have data for each flight's origin and destination
as well as the airport coordinates. The ideal end state is to have an indiviudal circle representing a plane moving
along each flight path from origin to destination. The current state is that each circle gets visualised along the path,
yet the removal of the previous circle along the line does not work as clearRect gets called nearly constantly.
Current state:
Ideal state (achieved with SVG):
The Concept
Conceptually, an SVG path for each flight is produced in memory using D3's custom interpolation with path.getTotalLength() and path.getPointAtLength() to move the circle along the path.
The interpolator returns the points along the path at any given time of the transition. A simple drawing function takes these points and draws the circle.
Key functions
The visualisation gets kicked off with:
od_pairs.forEach(function(el, i) {
fly(el[0], el[1]); // for example: fly('LHR', 'JFK')
});
The fly() function creates the SVG path in memory and a D3 selection of a circle (the 'plane') - also in memory.
function fly(origin, destination) {
var pathElement = document.createElementNS(d3.namespaces.svg, 'path');
var routeInMemory = d3.select(pathElement)
.datum({
type: 'LineString',
coordinates: [airportMap[origin], airportMap[destination]]
})
.attr('d', path);
var plane = custom.append('plane');
transition(plane, routeInMemory.node());
}
The plane gets transitioned along the path by the custom interpolater in the delta() function:
function transition(plane, route) {
var l = route.getTotalLength();
plane.transition()
.duration(l * 50)
.attrTween('pointCoordinates', delta(plane, route))
// .on('end', function() { transition(plane, route); });
}
function delta(plane, path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
draw([p.x, p.y]);
};
};
}
... which calls the simple draw() function
function draw(coords) {
// contextPlane.clearRect(0, 0, width, height); << how to tame this?
contextPlane.beginPath();
contextPlane.arc(coords[0], coords[1], 1, 0, 2*Math.PI);
contextPlane.fillStyle = 'tomato';
contextPlane.fill();
}
This results in an extending 'path' of circles as the circles get drawn yet not removed as shown in the first gif above.
Full code here: http://blockbuilder.org/larsvers/8e25c39921ca746df0c8995cce20d1a6
My question is, how can I achieve to draw only a single, current circle while the previous circle gets removed without interrupting other circles being drawn on the same canvas?
Some failed attempts:
The natural answer is of course context.clearRect(), however, as there's a time delay (roughly a milisecond+) for each circle to be drawn as it needs to get through the function pipeline clearRect gets fired almost constantly.
I tried to tame the perpetual clearing of the canvas by calling clearRect only at certain intervals (Date.now() % 10 === 0 or the like) but that leads to no good either.
Another thought was to calculate the previous circle's position and remove the area specifically with a small and specific clearRect definition within each draw() function.
Any pointers very much appreciated.
Handling small dirty regions, especially if there is overlap between objects quickly becomes very computationally heavy.
As a general rule, a average Laptop/desktop can easily handle 800 animated objects if the computation to calculate position is simple.
This means that the simple way to animate is to clear the canvas and redraw every frame. Saves a lot of complex code that offers no advantage over the simple clear and redraw.
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
function createIcon(drawFunc){
const icon = document.createElement("canvas");
icon.width = icon.height = 10;
drawFunc(icon.getContext("2d"));
return icon;
}
function drawPlane(ctx){
const cx = ctx.canvas.width / 2;
const cy = ctx.canvas.height / 2;
ctx.beginPath();
ctx.strokeStyle = ctx.fillStyle = "red";
ctx.lineWidth = cx / 2;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.moveTo(cx/2,cy)
ctx.lineTo(cx * 1.5,cy);
ctx.moveTo(cx,cy/2)
ctx.lineTo(cx,cy*1.5)
ctx.stroke();
ctx.lineWidth = cx / 4;
ctx.moveTo(cx * 1.7,cy * 0.6)
ctx.lineTo(cx * 1.7,cy*1.4)
ctx.stroke();
}
const planes = {
items : [],
icon : createIcon(drawPlane),
clear(){
planes.items.length = 0;
},
add(x,y){
planes.items.push({
x,y,
ax : 0, // the direction of the x axis of this plane
ay : 0,
dir : Math.random() * Math.PI * 2,
speed : Math.random() * 0.2 + 0.1,
dirV : (Math.random() - 0.5) * 0.01, // change in direction
})
},
update(){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
p.dir += p.dirV;
p.ax = Math.cos(p.dir);
p.ay = Math.sin(p.dir);
p.x += p.ax * p.speed;
p.y += p.ay * p.speed;
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.setTransform(-p.ax,-p.ay,p.ay,-p.ax,x,y);
ctx.drawImage(planes.icon,-planes.icon.width / 2,-planes.icon.height / 2);
}
}
}
const ctx = canvas.getContext("2d");
function mainLoop(){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(800,()=>{ planes.add(Math.random() * canvas.width, Math.random() * canvas.height) })
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update();
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
800 animated points
As pointed out in the comments some machines may be able to draw a circle if one colour and all as one path slightly quicker (not all machines). The point of rendering an image is that it is invariant to the image complexity. Image rendering is dependent on the image size but colour and alpha setting per pixel have no effect on rendering speed. Thus I have changed the circle to show the direction of each point via a little plane icon.
Path follow example
I have added a way point object to each plane that in the demo has a random set of way points added. I called it path (could have used a better name) and a unique path is created for each plane.
The demo is to just show how you can incorporate the D3.js interpolation into the plane update function. The plane.update now calls the path.getPos(time) which returns true if the plane has arrived. If so the plane is remove. Else the new plane coordinates are used (stored in the path object for that plane) to set the position and direction.
Warning the code for path does little to no vetting and thus can easily be made to throw an error. It is assumed that you write the path interface to the D3.js functionality you want.
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
function createIcon(drawFunc){
const icon = document.createElement("canvas");
icon.width = icon.height = 10;
drawFunc(icon.getContext("2d"));
return icon;
}
function drawPlane(ctx){
const cx = ctx.canvas.width / 2;
const cy = ctx.canvas.height / 2;
ctx.beginPath();
ctx.strokeStyle = ctx.fillStyle = "red";
ctx.lineWidth = cx / 2;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.moveTo(cx/2,cy)
ctx.lineTo(cx * 1.5,cy);
ctx.moveTo(cx,cy/2)
ctx.lineTo(cx,cy*1.5)
ctx.stroke();
ctx.lineWidth = cx / 4;
ctx.moveTo(cx * 1.7,cy * 0.6)
ctx.lineTo(cx * 1.7,cy*1.4)
ctx.stroke();
}
const path = {
wayPoints : null, // holds way points
nextTarget : null, // holds next target waypoint
current : null, // hold previously passed way point
x : 0, // current pos x
y : 0, // current pos y
addWayPoint(x,y,time){
this.wayPoints.push({x,y,time});
},
start(){
if(this.wayPoints.length > 1){
this.current = this.wayPoints.shift();
this.nextTarget = this.wayPoints.shift();
}
},
getNextTarget(){
this.current = this.nextTarget;
if(this.wayPoints.length === 0){ // no more way points
return;
}
this.nextTarget = this.wayPoints.shift(); // get the next target
},
getPos(time){
while(this.nextTarget.time < time && this.wayPoints.length > 0){
this.getNextTarget(); // get targets untill the next target is ahead in time
}
if(this.nextTarget.time < time){
return true; // has arrivecd at target
}
// get time normalised ove time between current and next
var timeN = (time - this.current.time) / (this.nextTarget.time - this.current.time);
this.x = timeN * (this.nextTarget.x - this.current.x) + this.current.x;
this.y = timeN * (this.nextTarget.y - this.current.y) + this.current.y;
return false; // has not arrived
}
}
const planes = {
items : [],
icon : createIcon(drawPlane),
clear(){
planes.items.length = 0;
},
add(x,y){
var p;
planes.items.push(p = {
x,y,
ax : 0, // the direction of the x axis of this plane
ay : 0,
path : Object.assign({},path,{wayPoints : []}),
})
return p; // return the plane
},
update(time){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
if(p.path.getPos(time)){ // target reached
planes.items.splice(i--,1); // remove
}else{
p.dir = Math.atan2(p.y - p.path.y, p.x - p.path.x) + Math.PI; // add 180 because i drew plane wrong way around.
p.ax = Math.cos(p.dir);
p.ay = Math.sin(p.dir);
p.x = p.path.x;
p.y = p.path.y;
}
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.setTransform(-p.ax,-p.ay,p.ay,-p.ax,x,y);
ctx.drawImage(planes.icon,-planes.icon.width / 2,-planes.icon.height / 2);
}
}
}
const ctx = canvas.getContext("2d");
function mainLoop(time){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(810,()=>{
var p = planes.add(Math.random() * canvas.width, Math.random() * canvas.height);
// now add random number of way points
var timeP = time;
// info to create a random path
var dir = Math.random() * Math.PI * 2;
var x = p.x;
var y = p.y;
doFor(Math.floor(Math.random() * 80 + 12),()=>{
var dist = Math.random() * 5 + 4;
x += Math.cos(dir) * dist;
y += Math.sin(dir) * dist;
dir += (Math.random()-0.5)*0.3;
timeP += Math.random() * 1000 + 500;
p.path.addWayPoint(x,y,timeP);
});
// last waypoin at center of canvas.
p.path.addWayPoint(canvas.width / 2,canvas.height / 2,timeP + 5000);
p.path.start();
})
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update(time);
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
800 animated points
#Blindman67 is correct, clear and redraw everything, every frame.
I'm here just to say that when dealing with such primitive shapes as arc without too many color variations, it's actually better to use the arc method than drawImage().
The idea is to wrap all your shapes in a single path declaration, using
ctx.beginPath(); // start path declaration
for(i; i<shapes.length; i++){ // loop through our points
ctx.moveTo(pt.x + pt.radius, pt.y); // default is lineTo and we don't want it
// Note the '+ radius', arc starts at 3 o'clock
ctx.arc(pt.x, pt.y, pt.radius, 0, Math.PI*2);
}
ctx.fill(); // a single fill()
This is faster than drawImage, but the main caveat is that it works only for single-colored set of shapes.
I've made an complex plotting app, where I do draw a lot (20K+) of entities, with animated positions. So what I do, is to store two sets of points, one un-sorted (actually sorted by radius), and one
sorted by color. I then do use the sorted-by-color one in my animations loop, and when the animation is complete, I draw only the final frame with the sorted-by-radius (after I filtered the non visible entities). I achieve 60fps on most devices. When I tried with drawImage, I was stuck at about 10fps for 5K points.
Here is a modified version of Blindman67's good answer's snippet, using this single-path approach.
/* All credits to SO user Blindman67 */
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
const planes = {
items : [],
clear(){
planes.items.length = 0;
},
add(x,y){
planes.items.push({
x,y,
rad: 2,
dir : Math.random() * Math.PI * 2,
speed : Math.random() * 0.2 + 0.1,
dirV : (Math.random() - 0.5) * 0.01, // change in direction
})
},
update(){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
p.dir += p.dirV;
p.x += Math.cos(p.dir) * p.speed;
p.y += Math.sin(p.dir) * p.speed;
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
ctx.beginPath();
ctx.fillStyle = 'red';
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.moveTo(x + p.rad, y)
ctx.arc(x, y, p.rad, 0, Math.PI*2);
}
ctx.fill();
}
}
const ctx = canvas.getContext("2d");
function mainLoop(){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(8000,()=>{ planes.add(Math.random() * canvas.width, Math.random() * canvas.height) })
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update();
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
z-index: -1;
}
<canvas id=canvas></canvas>
8000 animated points
Not directly related but in case you've got part of your drawings that don't update at the same rate as the rest (e.g if you want to highlight an area of your map...) then you might also consider separating your drawings in different layers, on offscreen canvases. This way you'd have one canvas for the planes, that you'd clear every frame, and other canvas for other layers that you would update at different rate. But that's an other story.
function initCircles() {
circles = [];
for (var i = 0; i < 100; i++) {
var circle = new createjs.Shape();
var r = 7;
var x = window.innerWidth * Math.random();
var y = window.innerHeight * Math.random();
var color = colors[Math.floor(i % colors.length)];
var alpha = 0.2 + Math.random() * 0.5;
circle.alpha = alpha;
circle.radius = r;
circle.graphics.beginFill(color).drawCircle(0, 0, r);
circle.x = x;
circle.y = y;
circles.push(circle);
circle.movement = 'float';
circle.addEventListener("mouseover", function(event) {
circle.graphics.clear().beginFill("red").drawRect(0, 0, 50, 60).endFill();
stage.update(event);
});
stage.addChild(circle);
}
}
I'm trying to add a mouseover listener on the little circles I create on the page, I hope that once I place the cursor on the circle, it becomes a rectangle. However, the rectangle always appear where some other circle exists rather than the circle I point to.
Save your beginFill command, and change it later:
// your other code above
var fillCmd = circle.graphics.beginFill(color).command; // Note the .command at the end
circle.graphics.drawCircle(0, 0, r);
// your other code below
// Later
fillCmd.style = "ff0000";
Here is an article about it, and here are the docs -
Admittedly, this could be documented better. :)
The problem is that you are referencing a mutable variable inside of the closure. There are a couple of ways to solve that.
1) Either somehow reference the circle from the event variable inside of the nested function (if the event has support for that), or
2) Bind the value of the variable inside another function, e.g:
function initCircles() {
circles = [];
for (var i = 0; i < 100; i++) {
var circle = new createjs.Shape();
var r = 7;
var x = window.innerWidth * Math.random();
var y = window.innerHeight * Math.random();
var color = colors[Math.floor(i % colors.length)];
var alpha = 0.2 + Math.random() * 0.5;
circle.alpha = alpha;
circle.radius = r;
circle.graphics.beginFill(color).drawCircle(0, 0, r);
circle.x = x;
circle.y = y;
circles.push(circle);
circle.movement = 'float';
addEventListener(circle);
stage.addChild(circle);
}
function addEventListener(circle) {
circle.addEventListener("mouseover", function(event) {
circle.graphics.clear().beginFill("red").drawRect(0, 0, 50, 60).endFill();
stage.update(event);
});
}
}
I am trying to imitate a pattern I found on the internet, but I get weird lines in the middle and when trying to connect another set of circles on top.
Also, when I try to fill, it becomes fully black.
console.log("grid");
var canvas = document.getElementById("canvas");
var image_b = document.getElementById("brown");
var image_g = document.getElementById("grey");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext("2d");
var side = 160;
var side2 = 150;
ctx.strokeStyle = 'black';
ctx.fillStyle = 'white';
function draw() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
var widthNbr = Math.ceil(window.innerWidth / side) + 1;
var heightNbr = Math.ceil(window.innerHeight / side) + 1;
var counter = 0;
for (var i = 0; i < widthNbr; i++) {
for (var j = 0; j < heightNbr; j++) {
ctx.beginPath();
var x = side * i + side / 2;
var y = side * j + side / 2;
var a = side * i + side / 2;
var s = side * j + side / 2;
var d = side * i + side / 2;
var f = side * j + side / 2;
var g = side * i + side / 2;
var h = side * j + side / 2;
var q = side * i + side / 2;
var w = side * j + side / 2;
var o = side * i + side / 2;
var p = side * j + side / 2;
var x1 = side2 * i + side2;
var y1 = side2 * j + side2;
var a1 = side2 * i + side2;
var s1 = side2 * j + side2;
var d1 = side2 * i + side2;
var f1 = side2 * j + side2;
var g1 = side2 * i + side2;
var h1 = side2 * j + side2;
var q1 = side2 * i + side2;
var w1 = side2 * j + side2;
var o1 = side2 * i + side2;
var p1 = side2 * j + side2;
ctx.arc(x, y, side / 2, 0, Math.PI * 2);
ctx.arc(a, s, side / 2.5, 0, Math.PI * 2);
ctx.arc(d, f, side / 3.5, 0, Math.PI * 2);
ctx.arc(g, h, side / 5.3, 0, Math.PI * 2);
ctx.arc(q, w, side / 9, 0, Math.PI * 2);
ctx.arc(o, p, side / 18, 0, Math.PI * 2);
ctx.lineWidth = 5;
ctx.arc(x1, y1, side2 / 2, 0, Math.PI * 2);
ctx.arc(a1, s1, side2 / 2.5, 0, Math.PI * 2);
ctx.arc(d1, f1, side2 / 3.5, 0, Math.PI * 2);
ctx.arc(g1, h1, side2 / 5.3, 0, Math.PI * 2);
ctx.arc(q1, w1, side2 / 9, 0, Math.PI * 2);
ctx.arc(o1, p1, side2 / 18, 0, Math.PI * 2);
ctx.stroke();
// ctx.fill();
ctx.closePath();
counter++;
}
}
}
draw();
<canvas id="canvas"></canvas>
You have to think about canvas Path drawings as pencil drawing on a paper :
Just after the path declaration (beginPath), when you say ctx.arc(x, y, rad, 0, Math.PI*2) your pen goes to coordinates (x, y), and because x and y are the center position of your arc it will be putted at a rad distance from this center to draw the circle. Your 0 tells it to start at 3 o'clock, so in this case, we just need to add this rad to the x value.
At this moment, your pen is on the paper.
It draws the arc, and when you tell it arc(x1, y1, rad, ...), it goes directly to coordinates (x1+rad, y1) and draws the new arc.
The problem here is that you never told it to raise the pencil from the paper, so you can see the line that goes from the last point on the first arc to the first point on the next one.
Fortunately, Canvas API comes with a handy set of operations, and the "Raise_the_pen_and_move_to_coordinates_x,y_without_ruining_my_paper" is simply called moveTo.
By telling the context to gently raise the pencil and to move to the next first drawing point, before actually drawing the arc, you'll avoid all these trailing lines.
So basically, for three arcs it would be :
// initialize a new drawing
ctx.beginPath();
// here we can set it directly because the pen is not on the paper yet
ctx.arc(x, y, rad, 0, Math.PI*2);
// tell it to raise the pen off the paper
// and to go to the next starting point (3 o'clock in our case)
ctx.moveTo(x1 + rad, y1);
ctx.arc(x1, y1, rad, 0, Math.PI*2);
// once again
ctx.moveTo(x2 + rad, y2);
ctx.arc(x2, y2, rad, 0, Math.PI*2);
// now we've got clear independents arcs
ctx.stroke();
And with your code (That you could clean a lot by using arrays btw)
var canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext("2d");
var side = 160;
var side2 = 150;
ctx.strokeStyle = 'black';
ctx.fillStyle = 'white';
function draw() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
var widthNbr = Math.ceil(window.innerWidth / side) + 1;
var heightNbr = Math.ceil(window.innerHeight / side) + 1;
var counter = 0;
for (var i = 0; i < widthNbr; i++) {
for (var j = 0; j < heightNbr; j++) {
ctx.beginPath();
var x = side * i + side / 2;
var y = side * j + side / 2;
var a = side * i + side / 2;
var s = side * j + side / 2;
var d = side * i + side / 2;
var f = side * j + side / 2;
var g = side * i + side / 2;
var h = side * j + side / 2;
var q = side * i + side / 2;
var w = side * j + side / 2;
var o = side * i + side / 2;
var p = side * j + side / 2;
var x1 = side2 * i + side2;
var y1 = side2 * j + side2;
var a1 = side2 * i + side2;
var s1 = side2 * j + side2;
var d1 = side2 * i + side2;
var f1 = side2 * j + side2;
var g1 = side2 * i + side2;
var h1 = side2 * j + side2;
var q1 = side2 * i + side2;
var w1 = side2 * j + side2;
var o1 = side2 * i + side2;
var p1 = side2 * j + side2;
ctx.moveTo(x + side / 2, y);
ctx.arc(x, y, side / 2, 0, Math.PI * 2);
ctx.moveTo(a + side / 2.5, s);
ctx.arc(a, s, side / 2.5, 0, Math.PI * 2);
ctx.moveTo(d + side / 3.5, f)
ctx.arc(d, f, side / 3.5, 0, Math.PI * 2);
ctx.moveTo(g + side / 5.3, h)
ctx.arc(g, h, side / 5.3, 0, Math.PI * 2);
ctx.moveTo(q + side / 9, w)
ctx.arc(q, w, side / 9, 0, Math.PI * 2);
ctx.moveTo(o + side / 18, p)
ctx.arc(o, p, side / 18, 0, Math.PI * 2);
ctx.lineWidth = 5;
ctx.moveTo(x1 + side2 / 2, y1)
ctx.arc(x1, y1, side2 / 2, 0, Math.PI * 2);
ctx.moveTo(a1 + side2 / 2.5, s1)
ctx.arc(a1, s1, side2 / 2.5, 0, Math.PI * 2);
ctx.moveTo(d1 + side2 / 3.5, f1)
ctx.arc(d1, f1, side2 / 3.5, 0, Math.PI * 2);
ctx.moveTo(g1 + side2 / 5.3, h1)
ctx.arc(g1, h1, side2 / 5.3, 0, Math.PI * 2);
ctx.moveTo(q1 + side2 / 9, w1)
ctx.arc(q1, w1, side2 / 9, 0, Math.PI * 2);
ctx.moveTo(o1 + side2 / 18, p1)
ctx.arc(o1, p1, side2 / 18, 0, Math.PI * 2);
ctx.stroke();
counter++;
}
}
}
draw();
<canvas id="canvas"></canvas>
As correctly noted by Spencer Wieczorek in comments above, to get the result you wanted, you'll also have to white-fill the largest arcs, but I let you find the way to do it as a training.
Also, a small note on closePath() that you were using in your code, his name might be quite confusing when we see the number of people misusing it, but note that it doesn't ends your Path declaration. All it does is a lineTo(last_time_I_putted_the_pencil). In the case of closed circle, it doesn't have any effect because last_time_I_putted_the_pencil === current_pencil_position_on_the_paper, but it's often the source of a lot of problems.
And an other small note, for users a bit more experienced (probably OP in few days / weeks) :
Other operations allow us to raise our pencil from the paper : the transformation commands.
(mainly setTransform, and its subsets transform, translate, rotate and scale).
These operations will first raise the pen, and then move the paper rather than the pen. This comes handy in a lot of situations.
And to set it back to its normal position, you just have to call setTransform(1,0,0,1,0,0).
As I was far too slow in responding to this question please consider this an addendum to the excellent answer already provided by Kaiido.
When thinking about problems like this it is sometimes useful to separate the calculation of values from their application. Of course, this kind of understanding only comes with experience, unless that is we can plug into the experience of others - which is exactly what sites like StackOverflow are for! :)
The image we're intending to make is made entirely of circles, so we can greatly reduce repetition in our code by creating a function that deals with just that one thing for us. Something like...
/* draw circle */
function drawCircle(x, y, r, LW) {
context.lineWidth = LW;
context.beginPath();
context.arc(x, y, r, 0, Math.PI*2, true);
context.fill();
context.stroke();
}
Although this function only draws a single circle to the canvas we can pass it values that can be used to draw every element we need.
The reference image is built out of sets of circles, where each circle-set has the same (x,y) position. If we know those starting co-ordinates for X & Y, and the radius of the largest circle in the set, then we can create a function to calculate the values we need and pass them into the drawCircles() function above...
function circleSet(X, Y, Radius) {
var count = 5; /* number of circles in each set */
var step = Radius / count;
var ln_width;
var rad;
while (count > 0) {
ln_width = count > 3 ? 3 : (count > 1 ? 2 : 1);
rad = count * step;
drawCircle(X, Y, rad, ln_width);
count--;
}
}
The while-loop reduces the count variable from 5 to 1 and line width and radius values are calculated before being passed into the aforementioned drawCircle() function, along with (x,y) co-ordinates.
The conditional in the first line of the while-loop says:
if count is 4 or 5 then ln_width equals 3;
or else if count is 2 or 3 then ln_width equals 2;
or else ln_width equals 1.
The rad variable holds a value that starts at Radius and is decreased by 1 x steps for each iteration of the loop.
Now that the circle-set parameters are ready to be calculated all that's needed is to calculate the values to pass to the circleSet() function, that is; the (x,y) co-ordinates and starting radius of each set.
As your original code snippet showed, we can use two nested for-loops for this. one to deal with the vertical and one to deal with the horizontal, but first we need to make some decisions.
If we want to have 10 circle-sets across the canvas then the width of each set would be...
var circleSet_Size = canvas.width / 10;
...and the maximum radius of each circle-set would therefore be...
var circleSet_Radius = circleSet_Size / 2;
We also need to draw two lots of circle-sets to make something like the reference image, one lot for the 'background' and one lot for the 'foreground'. So we need to determine the starting (x,y) co-ordinates of each pass as well as width/height of the area we want to draw over. We can create a function for that too. Something like...
function loopXYPosition(startX, startY) {
for (var y = startY; y < (canvas.height + circleSet_Radius); y += circleSet_Size) {
for (var x = startX; x < (canvas.width + circleSet_Radius); x += circleSet_Size) {
circleSet(x, y, setRadius);
}
}
}
/* 'background' pass */
loopXYPosition(0, 0);
/* 'foreground' pass */
loopXYPosition(circleSet_Radius, circleSet_Radius);
With all that in place we can collate our functions into a single script. But before we do that it's worth taking note of those values which need to be calculated each time a function is called and which values remain static once calculated. Bearing all that in mind we end up with something like...
var circlePattern = (function() {
/* define variables available
to all functions */
var canvas
, ctx
, cWidth
, cHeight
, circleCount
, circleSet_Size
, circleSet_Radius
, P360;
/* draw each cicle */
function drawCircle(x, y, r, LW) {
ctx.beginPath();
ctx.lineWidth = LW;
ctx.arc(x, y, r, 0, P360, true);
ctx.fill();
ctx.stroke();
}
/* calculate each set of circles (circle-set) */
function circleSet(X, Y, R) {
var count = circleCount,
radiusSteps = R / count,
ln_width,
rad;
while (count > 0) {
ln_width = count > 3 ? 2.5 : (count > 1 ? 2 : 1.5);
rad = count * radiusSteps;
drawCircle(X, Y, rad, ln_width);
count--;
}
}
function loopXYPosition(startX, startY) {
/* add circleSet_Radius to canvas width
and height to make sure we draw right
up to the edges of the canvas */
var cHcR = cHeight + circleSet_Radius;
var cWcR = cWidth + circleSet_Radius;
/* to get the effect we want we need to create
a little padding around each circle-set,
therefore we reduce the circelSet_Radius value
by 5% before passing to drawCircleSet() */
var setRadius = circleSet_Radius * 0.95;
/* step across and down canvas in
increments of 'circleSet_Size' */
for (var y = startY; y < cHcR; y += circleSet_Size) {
for (var x = startX; x < cWcR; x += circleSet_Size) {
circleSet(x, y, setRadius);
}
}
}
function begin(NoC, cCount) {
var numberOfCircles = NoC;
/* set variables needed later */
circleSet_Size = cWidth / numberOfCircles;
circleSet_Radius = circleSet_Size / 2;
circleCount = cCount;
/* draw rows of circles */
loopXYPosition(0, 0);
loopXYPosition(circleSet_Radius, circleSet_Radius);
}
/* initialise canvas */
function init(e) {
/* Set variables to use later */
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
cWidth = canvas.width;
cHeight = canvas.height;
P360 = Math.PI * 2;
/* fillStyle & strokeStyle are properties
of the Canvas, so we only need to set
them once here */
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#000';
/* fill canvas (background) */
ctx.fillRect(0, 0, cWidth, cHeight);
/* first argument: number of horizontal circle-sets
second argument: number of circles in each set
!Try different values here! */
begin(7, 6);
}
return {
go: init
};
}());
window.onload = circlePattern.go;
body {
color:#000;
background-color:#fff;
font-family:sans-serif;
}
div.box {
width:100%;
height:auto;
text-align:center;
margin:2em auto;
display:block;
}
div.box h3 {
font-size:1.3em;
line-height:2.3em;
}
#refimg, #canvas {
width:600px;
margin:0 auto;
clear:both;
display:block;
}
#refimg {
height:360px;
}
#canvas {
height:400px;
}
<div class="box">
<h3>Refernece Image</h3>
<img id="refimg" src="http://i.stack.imgur.com/Uxm2Z.jpg)" alt="image" title="reference image" />
</div>
<div class="box">
<h3>Canvas Image</h3>
<canvas id="canvas" width="600" height="400" title="Canvas image"></canvas>
</div>
Not exactly the same results, though pretty close. With the exception of the (x,y) co-ordinates which pass through it the circleSet() function produces the same sequence of values each time it is called, so those values could be calculated once and stored in an Object, but I've included it here for simplicity and to highlight the sequence of events.
Feeding different values into the begin() function and playing with setRadius in the loopXYPosition() function yields some interesting results.
I hope the process outlined here gives you some hints in your continuing exploration of the HTML5 Canvas API.
;)
I would like to create a reusable JavaScript component out of the following canvas spinner. Never done this before. How to achieve it and how to use the component?
http://codepen.io/anon/pen/tkpqc
HTML:
<canvas id="spinner"></canvas>
JS:
var canvas = document.getElementById('spinner');
var context = canvas.getContext('2d');
var start = new Date();
var lines = 8,
cW = context.canvas.width,
cH = context.canvas.height;
centerX = canvas.width / 2;
centerY = canvas.height / 2;
radius = 20;
var draw = function() {
var rotation = parseInt(((new Date() - start) / 1000) * lines) % lines;
context.save();
context.clearRect(0, 0, cW, cH);
for (var i = 0; i < lines; i++) {
context.beginPath();
//context.rotate(Math.PI * 2 / lines);
var rot = 2*Math.PI/lines;
var space = 2*Math.PI/(lines * 12);
context.arc(centerX,centerY,radius,rot * (i) + space,rot * (i+1) - space);
if (i == rotation)
context.strokeStyle="#ED3000";
else
context.strokeStyle="#CDCDCD";
context.lineWidth=10;
context.stroke();
}
context.restore();
};
window.setInterval(draw, 1000 / 30);
EDIT - SOLUTION:
Here comes a solution if anybody is interested
http://codepen.io/anon/pen/tkpqc
There are any number of ways to do this. Javascript is an object oriented language so you can easily write like:
var Spinner = function(canvas_context)
{
this.context = canvas_context;
// Whatever other properties you needed to create
this.timer = false;
}
Spinner.prototype.draw = function()
{
// Draw spinner
}
Spinner.prototype.start = function()
{
this.timer = setInterval(this.start, 1000 / 30);
}
Spinner.prototype.stop = function() {
clearInterval(this.timer);
}
Now you can use this object like so:
var canvas = document.getElementById('#canvas');
var context = canvas.getContext('2d');
var spinner = new Spinner(context);
spinner.start();
Basically, you are creating a class whose sole purpose in life is to draw a spinner on a canvas. In this example, you'll note that you're passing in the canvas's context into the object, since the details of the canvas itself is not relevant to this class's interest.