I've created and some shapes (pacman looking objects) using javascript and I am animating them using matrix transformations built into the canvas. The objects can move in four directions (left, right, up, down) and the shapes essentially will move back and forth across the canvas until another button is pressed. These functionalities work properly when I use the built in canvas transformation functions; however, when I try to implement my own transformations some weird things are happening.
In the normal cases (when I use the canvas transformations) I first am translating the canvas origin to the point that is being rotated around, performing the rotation around this "new" origin, and then translating the origin back to what it was before, seen in the following code:
function renderContent(pm) {
var t = new Transform();
context.save();
context.beginPath();
context.fillStyle = "Yellow";
context.strokeStyle = "Yellow";
context.save();
var tCopy1 = t;
context.translate(pm.posX, pm.posY);
context.rotate(-pm.direction * Math.PI / 180);
context.translate(-pm.posX, -pm.posY);
context.arc(pm.posX, pm.posY, pm.size, (pm.startAngle) * Math.PI, (pm.endAngle) * Math.PI);
context.lineTo(pm.posX, pm.posY);
context.stroke();
context.fill();
var m = t.m;
t.transform(m);
context.restore();
context.restore();
}
To implement my own transformations, I have a transform class with functions including:
function Transform() {
this.identity();
}
Transform.prototype.identity = function () {
this.m = [1, 0, 0, 1, 0, 0];
};
Transform.prototype.rotate = function (rad) {
var c = Math.cos(rad);
var s = Math.sin(rad);
var m11 = this.m[0] * c + this.m[2] * s;
var m12 = this.m[1] * c + this.m[3] * s;
var m21 = this.m[0] * -s + this.m[2] * c;
var m22 = this.m[1] * -s + this.m[3] * c;
this.m[0] = m11;
this.m[1] = m12;
this.m[2] = m21;
this.m[3] = m22;
};
Transform.prototype.translate = function (x, y) {
this.m[4] += this.m[0] * x + this.m[2] * y;
this.m[5] += this.m[1] * x + this.m[3] * y;
};
Transform.prototype.scale = function (sx, sy) {
this.m[0] *= sx;
this.m[1] *= sx;
this.m[2] *= sy;
this.m[3] *= sy;
};
In the renderContext() function, I use essentially the same function calls for translate() and rotate() (which enable the pacman objects to turn around when they hit the canvas borders) but for some reason, with my implementation, the objects don't turn around (180 degrees).
For my transformations I essentially instantiate a transform object, call the transformation operations on the object, make a copy of the transform matrix and then set the canvas with the resultant matrix from the transformations:
E.G.:
var t = new Transform();
var tCopy1 = t;
tCopy1.translate(pm.posX, pm.posY);
t.rotate(-pm.direction * Math.PI / 180);
t.translate(-pm.posX, -pm.posY);
...
var m = t.m;
t.transform(m);
This is probably a lot of code to look at, but what is the correct way of implementing the rotation matrix transformation (as the canvas.rotate() is working)?
Here's the pacman rotating clockwise about the direction angle.
update
Now showing transform error in purple. Rotation looks correct but translation is off.
update 2
I think my previous test was bad. The transform looks good and there was only some minor adjustment of to_radians. The new test shows the context ranslation/rotation followed by the Transform class.
var can = document.getElementById('can');
var context = can.getContext('2d');
var to_rad = Math.PI / 180; // to radians
function main() {
context.fillStyle="black";
context.fillRect(0, 0, can.width, can.height);
var pm = {
direction: 0,
posX: 50,
posY: 100,
size: 20,
startAngle: 45,
endAngle: 315
};
var i = 0;
function loopTest() {
pm.direction = i * 90;
pm.posX = 50 * (i+1);
renderContent(pm);
setTimeout(function(){
renderContent2(pm);
}, 1000);
i++;
if (i < 4) {
setTimeout(loopTest, 2000);
}
}
loopTest();
}
function renderContent(pm) {
context.save();
context.beginPath();
context.fillStyle = "Yellow";
context.strokeStyle = "Yellow";
context.translate(pm.posX, pm.posY);
context.rotate(pm.direction * to_rad);
context.translate(-pm.posX, -pm.posY);
context.arc(pm.posX, pm.posY, pm.size, pm.startAngle * to_rad, pm.endAngle * to_rad);
context.lineTo(pm.posX, pm.posY);
context.stroke();
context.fill();
context.fillStyle="red";
context.font="16px Arial";
context.textAlign="center";
context.fillText(pm.direction, pm.posX,pm.posY+pm.size);
context.restore();
}
function renderContent2(pm) {
var t = new Transform();
context.save();
context.beginPath();
context.fillStyle = "#990099";
context.strokeStyle = "#990099";
t.translate(pm.posX, pm.posY);
t.rotate(pm.direction * to_rad);
t.translate(-pm.posX, -pm.posY);
context.transform.apply(context, t.m);
context.arc(pm.posX, pm.posY, pm.size, pm.startAngle * to_rad, pm.endAngle * to_rad);
context.lineTo(pm.posX, pm.posY);
context.stroke();
context.fill();
context.fillStyle="White";
context.font="16px Arial";
context.textAlign="center";
context.fillText(pm.direction, pm.posX,pm.posY+pm.size);
context.restore();
}
function Transform() {
this.identity();
}
Transform.prototype.identity = function () {
this.m = [1, 0, 0, 1, 0, 0];
};
Transform.prototype.rotate = function (rad) {
var c = Math.cos(rad);
var s = Math.sin(rad);
var m11 = this.m[0] * c + this.m[2] * s;
var m12 = this.m[1] * c + this.m[3] * s;
var m21 = this.m[0] * -s + this.m[2] * c;
var m22 = this.m[1] * -s + this.m[3] * c;
this.m[0] = m11;
this.m[1] = m12;
this.m[2] = m21;
this.m[3] = m22;
};
Transform.prototype.translate = function (x, y) {
this.m[4] += this.m[0] * x + this.m[2] * y;
this.m[5] += this.m[1] * x + this.m[3] * y;
};
Transform.prototype.scale = function (sx, sy) {
this.m[0] *= sx;
this.m[1] *= sx;
this.m[2] *= sy;
this.m[3] *= sy;
};
main();
canvas {
border:3px solid red;
}
<canvas id="can" width="300" height="200"></canvas>
Related
I have a need to add a bunch of dots within the confines of an ellipse. I'm trying to modify the code from the accepted solution to this question, which is set up to use a circle (same width and height) rather than an ellipse. It is also implementing via a HTML <canvas> element, and I need to get it working using p5.js.
Here is my code for the <canvas> element. It seems to be working great. i can size and position the ellipse any which way and the dots are all displayed within it no matter what.
function ellipse(context, cx, cy, rx, ry){
context.save(); // save state
context.beginPath();
context.translate(cx-rx, cy-ry);
context.scale(rx, ry);
context.arc(1, 1, 1, 0, 2 * Math.PI, false);
context.restore(); // restore to original state
context.stroke();
}
var canvas = document.getElementById("thecanvas");
var ctx = canvas.getContext('2d'),
count = 1000, // number of random points
cx = 300,
cy = 200,
w = 50,
h = 49,
radius = w;
ctx.fillStyle = '#CCCCCC';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ellipse(ctx, cx, cy, w, h);
// create random points
ctx.fillStyle = '#ffffff';
function dots() {
while (count) {
var pt_angle = Math.random() * 2 * Math.PI;
var pt_radius_sq = Math.random() * radius * radius;
var pt_x = Math.sqrt(pt_radius_sq) * Math.cos(pt_angle);
var pt_y = Math.sqrt(pt_radius_sq) * Math.sin(pt_angle);
ctx.fillRect( ((Math.sqrt(pt_radius_sq) * Math.cos(pt_angle) ) + cx), (((Math.sqrt(pt_radius_sq) * Math.sin(pt_angle))/(w/h)) + cy), 2, 2);
count--;
}
}
dots();
<canvas id="thecanvas" width="800" height="800"></canvas>
For some reason, what I believe to be a pretty straight forward to port to p5.js is not working. The dots seem to be contained within the same shaped ellipse as what is defined, but the scale seems to be off and I can't really tell what is causing it. You can see this in action at https://editor.p5js.org/dpassudetti/sketches/YRbLuwoM2 - p5.js code pasted below as well:
var count = 1000, // number of random points
cx = 300,
cy = 200,
w = 50,
h = 49,
radius = w;
function setup() {
createCanvas(800, 800);
function dots() {
fill('green');
stroke('green');
while (count) {
var pt_angle = Math.random() * 2 * Math.PI;
var pt_radius_sq = Math.random() * radius * radius;
var pt_x = Math.sqrt(pt_radius_sq) * Math.cos(pt_angle);
var pt_y = Math.sqrt(pt_radius_sq) * Math.sin(pt_angle);
square(
Math.sqrt(pt_radius_sq) * Math.cos(pt_angle) + cx,
(Math.sqrt(pt_radius_sq) * Math.sin(pt_angle)) / (w / h) + cy,
2,
2
);
count--;
}
}
fill("#CCCCCC");
ellipse(cx, cy, w, h);
dots();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
If anyone can see what's throwing the scale off for the p5.js version and wouldn't mind pointing me in the right direction, I'd be most appreciative.
The function you are using to draw the ellipse takes different parameters in the 1st and 2nd case. In one case it's taking the radiuses, and in the other the diameters, so you're off by a factor of 0.5.
I've simplified your code to consider just circles and kept just the w variable, but you can use the code below and add back the h variable if you wish to.
var count = 300, // number of random points
cx = 300,
cy = 200,
w = 50,
radius = w / 2;
function setup() {
createCanvas(800, 800);
function dots() {
fill('green');
stroke('green');
while (count) {
const pt_angle = Math.random() * 2 * Math.PI;
const px = Math.random() * radius * cos(pt_angle);
const py = Math.random() * radius * sin(pt_angle);
square(
px + cx,
py + cy,
2,
2
);
count--;
}
}
fill("#CCCCCC");
ellipse(cx, cy, w, w);
dots();
}
I created this codepen to show what I got.
I managed to generate a hexagon avatar with progressbar around it using the awesome open source Hexagon Progress jQuery Plugin from Max Lawrence.
He also helped me to improve his own code a little but I don't want to bother him again.
Maybe someone here can help me to round the corners of this hexagon.
I want it to looks something like this (from the awesome Vikinger Html template) but need to be open source because my software is all open source. I can't use the Vikinger code.
So far I read that I have to stop the line before the end and add a quadratic curve to the next line start but I could not managed to do that.
His code do something like this on line 505:
ctx.moveTo(this.coordBack[0].x + offset, this.coordBack[0].y + offset);
for(var i = 0; i < this.coordBack.length; i++) {
ctx.lineTo(this.coordBack[i].x + offset, this.coordBack[i].y + offset);
}
Unfortunatelly, I am not that good in javascript or math.
Two ways to do this. The easy way, and the long winded, lots of math way.
Easy rounded corners
To create simple rounded polygons you can use ctx.arcTo. It will do all the math for the corners.
To create the polygon the following functions create a point and a path (array of points)
const Point = (x,y) => ({x, y});
function polygon(sides, rad, rot = 0) {
var i = 0, step = Math.PI * 2 / sides, path = [];
while (i < sides) {
path.push(Point(Math.cos(i * step + rot) * rad, Math.sin((i++) * step + rot) * rad));
}
return path;
}
To create a hexagon. Note that the polygon is centered over its local origin 0,0
const hexagon = polygon(6, 100);
To render the rounded polygon you need to work from the line segment centers. The following function will stroke the path with the rounded corners.
function strokeRoundedPath(cx, cy, path, radius, style, width) {
ctx.setTransform(1,0,0,1,cx,cy);
var i = 0;
const len = path.length
var p1 = path[i++], p2 = path[i];
ctx.lineWidth = width;
ctx.lineCap = "round";
ctx.strokeStyle = style;
ctx.beginPath();
ctx.lineTo((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
while (i <= len) {
p1 = p2;
p2 = path[(++i) % len];
ctx.arcTo(p1.x, p1.y, (p1.x + p2.x) / 2, (p1.y + p2.y) / 2, radius);
}
ctx.closePath();
ctx.stroke();
ctx.setTransform(1,0,0,1,0,0);
}
strokeRoundedPath(200, 200, hexagon, 20, "#000", 18);
Progress bar
Creating a progress bar is not as simple as the starting point can not be on a rounded corner, and moving over the rounded corners will need a lot of math to get the correct coordinates. This will negate the point of using easy arcToand need us to write the equivalent in JS (Way to slack for that today)
Using line dash for progress
There is however a hack that uses the line dash to create the effect you may be happy with. The snippet demonstrates this
const barWidth = 10;
const cornerRadius = barWidth * 2 + 8;
const polyRadius = 100;
const inset = 1;
const barRadius = polyRadius - barWidth * inset;
var progress = 0.0;
const approxLineLen = barRadius * Math.PI * 2;
const hexBar = polygon(6, barRadius);
const hexPoly = polygon(6, polyRadius);
const hexPolyInner = polygon(6, polyRadius - barWidth * 2 * inset);
const ctx = canvas.getContext("2d");
ctx.setLineDash([approxLineLen]);
loop()
function point(x,y) { return {x, y} }
function polygon(sides, radius, rot = 0) {
var i = 0;
const step = Math.PI * 2 / sides, path = [];
while (i < sides) {
path.push(point(Math.cos(i * step + rot) * radius, Math.sin((i++) * step + rot) * radius));
}
return path;
}
function roundedPath(path, radius) {
var i = 0, p1 = path[i++], p2 = path[i];
const len = path.length
ctx.moveTo((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
while (i <= len) {
p1 = p2;
p2 = path[(++i) % len];
ctx.arcTo(p1.x, p1.y, (p1.x + p2.x) / 2, (p1.y + p2.y) / 2, radius);
}
}
function strokeRoundedPath(cx, cy, path, radius, style, width) {
ctx.setTransform(1,0,0,1,cx,cy);
ctx.lineWidth = width;
ctx.lineCap = "round";
ctx.strokeStyle = style;
ctx.beginPath();
roundedPath(path, radius);
ctx.closePath();
ctx.stroke();
}
function fillRoundedPath(cx, cy, path, radius, style) {
ctx.setTransform(1,0,0,1,cx,cy);
ctx.fillStyle = style;
ctx.beginPath();
roundedPath(path, radius);
ctx.fill();
}
function loop() {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
fillRoundedPath(polyRadius, polyRadius, hexPoly, cornerRadius, "#000");
fillRoundedPath(polyRadius, polyRadius, hexPolyInner, cornerRadius - barWidth * inset * 2, "#F80");
ctx.lineDashOffset = approxLineLen - (progress % 1) * approxLineLen;
strokeRoundedPath(polyRadius, polyRadius, hexBar, cornerRadius - barWidth * inset, "#09C", barWidth);
progress += 0.005;
requestAnimationFrame(loop);
}
<canvas id="canvas" width = "210" height="210"></canvas>
I started learning JavaScript a few weeks ago and I decided to try and make my first game from scratch, in the code below I am trying to make the game so the balls fall each time with different position, color and speed and after that there will be another ball that ill be able to move with my mouse and will try to dodge the balls, so my question is is there a better way than the one I did to spawn more balls , because if I want to spawn like 5-6 more the code will look so bad and I am sure there is a better way of doing that, I am still learning so if you can hit me with a simple solution and explain it.
var canvas = document.getElementById("Canv");
var ctx = canvas.getContext("2d");
var x = random(1, 801);
var x2 = random(1, 801);
var y = 10;
var y2 = 10;
var ballRadius = random(2, 51);
var ballRadius2 = random(2, 51);
var color = "#" + ((1 << 24) * Math.random() | 0).toString(16);
var color2 = "#" + ((1 << 24) * Math.random() | 0).toString(16);
var dy = random(1, 6);
var dy2 = random(1, 6);
function random(min, max) {
return Math.random() * (max - min) + min;
}
function drawBall() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
}
function drawBall2() {
ctx.beginPath();
ctx.arc(x2, y2, ballRadius2, 0, Math.PI * 2);
ctx.fillStyle = color2;
ctx.fill();
ctx.closePath();
}
function draw() {
drawBall();
drawBall2();
y += dy;
y2 += dy2;
}
setInterval(draw, 10);
I want to know methods to simplify my code so I know for future projects.
Consider the following code:
The main changes I made are the addition of the function createBall, allowing the creation of multiple new balls per tick, and the removal of balls that are out of view. I tried to add comments to the new parts, if you have any questions, let me know!
var canvas = document.getElementById("Canv");
var ctx = canvas.getContext("2d");
const desiredNumberOfBalls = 10;
let i = 0;
let balls = []; // Track each individual ball
const viewLimit = canvas.height; // The vertical distance a ball has when leaving sight
// For now we create a static paddle
const paddle = {
x: 200,
y: viewLimit - 10,
width: 50,
height: 10,
}
// This is a very rough calculation based on https://math.stackexchange.com/a/2100319 by checking first the corners and then the top edge
paddle.hitsBall = function(ball) {
let hit = false;
if (ball.y + ball.radius >= paddle.y) {
// the ball is at the same level as the paddle
if (Math.sqrt((ball.x - paddle.x) ** 2 + (ball.y - paddle.y) ** 2) < ball.radius) {
// the upper left part of the paddle touches the ball
hit = true;
} else if (Math.sqrt((ball.x - paddle.x - paddle.width) ** 2 + (ball.y - paddle.y) ** 2) < ball.radius) {
// the upper right part of the paddle touches the ball
hit = true;
} else if (ball.x >= paddle.x && ball.x <= paddle.x + paddle.width) {
// the top edge of the paddle touches the ball
hit = true;
}
}
if (hit) console.log("Hit!");
return hit;
}
function createBall() {
let ball = {
id: i++,
x: random(1, canvas.width),
y: 10,
radius: random(2, 51),
color: "#" + ((1 << 24) * Math.random() | 0).toString(16),
speed: random(1, 6)
};
return ball;
}
function random(min, max) {
return Math.random() * (max - min) + min;
}
function drawBall(ball) {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
ctx.closePath();
}
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddle.x, paddle.y, paddle.width, paddle.height);
ctx.fillStyle = 'darkred';
ctx.fill();
ctx.closePath();
}
function clearView() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function draw() {
clearView();
// Move all existing balls by their designated speed
balls.forEach((ball) => {
ball.y += ball.speed;
});
// Implicitly delete all balls that have exited the viewport
// by only retaining the balls that are still inside the viewport
balls = balls.filter((ball) => ball.y <= viewLimit);
// Implicitly delete all balls that touch the paddle
// by only retaining the balls that don't
balls = balls.filter((ball) => !paddle.hitsBall(ball));
// Create newBallsPerTick new balls
while (balls.length < desiredNumberOfBalls) {
balls.push(createBall());
}
// Draw the paddle
drawPaddle();
// Draw the position of every ball - both the pre-existing ones
// and the ones we just generated
balls.forEach((ball) => {
drawBall(ball);
});
}
setInterval(draw, 1000 / 60);
canvas {
width: 600px;
height: 400px;
}
<canvas id="Canv" height="400" width="600"></canvas>
I'm using Konva library to draw some stuff on HTML5 canvas.
I have given 2 points from user interaction by mouse click:
var A={x:'',y:''};
var B={x:'',y:''};
1) How to draw line line this?
My question is:
1) How to get perpendicular lines on each interval?
2) How to get distance from A to B point?
3) How to get all points on line from A to B?
4) How to get red points?
You have not explained what your line is so I am assuming it is a sin wave (though the image looks like circles stuck together???)
As MBo has given the basics this is just applying it to the wavy line.
// normalize a vector
function normalize(vec){
var length = Math.sqrt(vec.x * vec.x + vec.y * vec.y);
vec.x /= length;
vec.y /= length;
return vec;
}
// creates a wavy line
function wavyLine(start, end, waves, amplitude){
return ({
start,
end,
waves,
amplitude,
update(){
if(this.vec === undefined){
this.vec = {};
this.norm = {};
}
this.vec.x = this.end.x - this.start.x;
this.vec.y = this.end.y - this.start.y;
this.length = Math.sqrt(this.vec.x * this.vec.x + this.vec.y * this.vec.y);
this.norm.x = this.vec.x / this.length;
this.norm.y = this.vec.y / this.length;
return this;
}
}).update();
}
// draws a wavy line
function drawWavyLine(line) {
var x, stepSize, i, y, phase, dist;
ctx.beginPath();
stepSize = ctx.lineWidth;
ctx.moveTo(line.start.x, line.start.y);
for (i = stepSize; i < line.length; i+= stepSize) {
x = line.start.x + line.norm.x * i; // get point i pixels from start
y = line.start.y + line.norm.y * i; // get point i pixels from start
phase = (i / (line.length / line.waves)) * Math.PI * 2; // get the wave phase at this point
dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
x -= line.norm.y * dist;
y += line.norm.x * dist;
ctx.lineTo(x, y);
}
phase = line.waves * Math.PI * 2; // get the wave phase at this point
dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
ctx.lineTo(line.end.x - line.norm.y * dist, line.end.y + line.norm.x * dist);
ctx.stroke();
}
// find the closest point on a wavy line to a point returns the pos on the wave, tangent and point on the linear line
function closestPointOnLine(point,line){
var x = point.x - line.start.x;
var y = point.y - line.start.y;
// get the amount the line vec needs to be scaled so tat point is perpendicular to the line
var l = (line.vec.x * x + line.vec.y * y) / (line.length * line.length);
x = line.vec.x * l; // scale the vec
y = line.vec.y * l;
return pointAtDistance(Math.sqrt(x * x + y * y), line);
}
// find the point at (linear) distance along wavy line and return coordinate, coordinate on wave, and tangent
function pointAtDistance(distance,line){
var lenScale = line.length / line.waves; // scales the length into radians
var phase = distance * Math.PI * 2 / lenScale; // get the wave phase at this point
var dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
var slope = Math.cos(phase ) * Math.PI * 2 * line.amplitude / lenScale; // derivitive of sin(a*x) is -a*cos(a*x)
// transform tangent (slope) into a vector along the line. This vector is not a unit vector so normalize it
var tangent = normalize({
x : line.norm.x - line.norm.y * slope,
y : line.norm.y + line.norm.x * slope
});
// move from the line start to the point on the linear line at distance
var linear = {
x : line.start.x + line.norm.x * distance,
y : line.start.y + line.norm.y * distance
}
// move out perpendicular to the wavy part
return {
x : linear.x - line.norm.y * dist,
y : linear.y + line.norm.x * dist,
tangent,linear
};
}
// create a wavy line
var wLine = wavyLine({x:10,y:100},{x:300,y:100},3,50);
// draw the wavy line and show some points on it
function display(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
var radius = Math.max(ch,cw);
// set up the wavy line
wLine.waves = Math.sin(timer / 10000) * 6;
wLine.start.x = Math.cos(timer / 50000) * radius + cw;
wLine.start.y = Math.sin(timer / 50000) * radius + ch;
wLine.end.x = -Math.cos(timer / 50000) * radius + cw;
wLine.end.y = -Math.sin(timer / 50000) * radius + ch ;
wLine.update();
// draw the linear line
ctx.lineWidth = 0.5;
ctx.strokeStyle = "blue";
ctx.beginPath();
ctx.moveTo(wLine.start.x, wLine.start.y);
ctx.lineTo(wLine.end.x, wLine.end.y);
ctx.stroke();
// draw the wavy line
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
drawWavyLine(wLine);
// find point nearest mouse
var p = closestPointOnLine(mouse,wLine);
ctx.lineWidth = 1;
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.arc(p.x,p.y,5,0,Math.PI * 2);
ctx.moveTo(p.x + p.tangent.x * 20,p.y + p.tangent.y * 20);
ctx.lineTo(p.x - p.tangent.y * 10,p.y + p.tangent.x * 10);
ctx.lineTo(p.x + p.tangent.y * 10,p.y - p.tangent.x * 10);
ctx.closePath();
ctx.stroke();
// find points at equal distance along line
ctx.lineWidth = 1;
ctx.strokeStyle = "blue";
ctx.beginPath();
for(var i = 0; i < w; i += w / 10){
var p = pointAtDistance(i,wLine);
ctx.moveTo(p.x + 5,p.y);
ctx.arc(p.x,p.y,5,0,Math.PI * 2);
ctx.moveTo(p.x,p.y);
ctx.lineTo(p.linear.x,p.linear.y);
ctx.moveTo(p.x + p.tangent.x * 40, p.y + p.tangent.y * 40);
ctx.lineTo(p.x - p.tangent.x * 40, p.y - p.tangent.y * 40);
}
ctx.stroke();
}
/******************************************************************************
The code from here down is generic full page mouse and canvas boiler plate
code. As I do many examples which all require the same mouse and canvas
functionality I have created this code to keep a consistent interface. The
Code may or may not be part of the answer.
This code may or may not have ES6 only sections so will require a transpiler
such as babel.js to run on legacy browsers.
*****************************************************************************/
// V2.0 ES6 version for Stackoverflow and Groover QuickRun
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0;
// You can declare onResize (Note the capital R) as a callback that is also
// called once at start up. Warning on first call canvas may not be at full
// size.
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var resizeTimeoutHandle;
var firstRun = true;
function createCanvas () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
function resizeCanvas () {
if (canvas === undefined) { canvas = createCanvas() }
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals() }
if (typeof onResize === "function") {
clearTimeout(resizeTimeoutHandle);
if (firstRun) { onResize() }
else { resizeTimeoutHandle = setTimeout(onResize, RESIZE_DEBOUNCE_TIME) }
firstRun = false;
}
}
function setGlobals () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
var m; // alias for mouse
var mouse = {
x : 0, y : 0, // mouse position and wheel
buttonRaw : 0,
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
bounds : null,
eventNames : "mousemove,mousedown,mouseup".split(","),
event(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
if (t === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (t === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
},
start(element) {
m.element = element === undefined ? document : element;
m.eventNames.forEach(name => document.addEventListener(name, mouse.event) );
},
}
m = mouse;
return mouse;
})();
function update(timer) { // Main update loop
globalTime = timer;
display(timer); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
canvas = createCanvas();
mouse.start(canvas);
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
We have points A and B. Difference vector
D.X = B.X - A.X
D.Y = B.Y - A.Y
Length = Sqrt(D.X * D.X + D.Y * D.Y)
normalized (unit) vector
uD.X = D.X / Length
uD.Y = D.Y / Length
perpendicular unit vector
P.X = - uD.Y
P.Y = uD.X
some red point:
R.X = A.X + uD.X * Dist + P.X * SideDist * SideSign
R.Y = A.Y + uD.Y * Dist + P.Y * SideDist * SideSign
where Dist is in range 0..Length
Dist = i / N * Length for N equidistant points
SideSign is +/- 1 for left and right side
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.
;)