Related
My program begins by drawing an n-sided polygon with circles inside it, like this: initial output. The polygon is continuously rotating around its center, and the circles have incredibly simple physics to fall down with gravity and bounce when they hit a wall.
However, I am having trouble detecting when the circles have hit a wall. Here is my current collision detection method, which involves finding the minimum distance from the line to the circle and comparing it to the circle's radius:
function collisionDetector(ball){
//calculate distance from all sides of the polygon
for(var i = 0; i < currentPolyPointsX.length -1; i++){
//get coordinates of line end points
let x1 = currentPolyPointsX[i];
let x2 = currentPolyPointsX[i+1];
let y1 = currentPolyPointsY[i];
let y2 = currentPolyPointsY[i+1];
//calculate length of line
let distanceX = x1 - x2;
let distanceY = y1 - y2;
let length = Math.sqrt( (distanceX *distanceX) + (distanceY*distanceY) );
//calculate dot product of vectors from line ends and ball
let dot = ( ((ball.x - x1) * (x2 - x1)) + ((ball.y - y1) * (y2-y1)) )
/ Math.pow(length, 2);
//calculate x and y coordinate on line (extends to infinity) closest to ball
let closestX = x1 + (dot * (x2 - x1));
let closestY = y1 + (dot * (y2 - y1));
//if those coordinates are not currently on our line, return false
if(!onLine(x1, y1, x2, y2, closestX, closestY)){
return false;
}
//calculate distance from closest coordinates to ball
distanceX = closestX - ball.x;
distanceY = closestY - ball.y;
let distance = Math.sqrt( (distanceX * distanceX) + (distanceY * distanceY) );
//if the ball is less than/equal to one radius away, it has collided
if( distance <= ball.returnRadius() ){
return true;
}else{
return false;
}
}
}
The points of the polygon are pushed into parallel arrays by this code:
function drawPolygon() { //x&y are positions, side is side number, r is size
//get current values of canvas center
var x = canvas.width /2;
var y = canvas.height /2;
var r = canvas.width /2;
//draw {sides} sided polygon
ctx.beginPath();
ctx.moveTo (x + r * Math.cos(0), y + r * Math.sin(0));
//clear currently stored points
currentPolyPointsX.length = 0;
currentPolyPointsY.length = 0;
//draw and save new polygon points in array
for (var i = 1; i <= sides; i ++) {
ctx.lineTo (x + r * Math.cos(i * 2 * Math.PI / sides),
y + r * Math.sin(i * 2 * Math.PI / sides));
currentPolyPointsX.push(x + r * Math.cos(i * 2 * Math.PI / sides)); // <----
currentPolyPointsY.push(y + r * Math.sin(i * 2 * Math.PI / sides)); // <----
}
//draw the polygon
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.stroke();
ctx.save();
}
And then modified to reflect current rotation by this code:
function rotatePolygon(x){
let centerX = canvas.height/2;
let centerY = canvas.height/2;
let angle = x * Math.PI / 180
//clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
//move rotation point to center of canvas
ctx.translate(centerX, centerY);
//rotate around center of canvas
ctx.rotate(angle);
//move back to origin
ctx.translate(-centerX, -centerY);
//draw rotated polygon
drawPolygon();
//adjust coordinates to match rotation
let cos = Math.cos(x * Math.PI / 180);
let sin = Math.sin(x * Math.PI / 180);
for(let i = 0; i < currentPolyPointsX.length; i++){ // <----
currentPolyPointsX[i] = ((cos * (currentPolyPointsX[i] - centerX)) // <----
- (sin * (currentPolyPointsY[i] - centerY)) + centerX); // <----
currentPolyPointsY[i] = ((cos * (currentPolyPointsY[i] - centerY)) // <----
+ (sin * (currentPolyPointsX[i] - centerX)) + centerY); // <----
}
//un-rotate
ctx.translate(canvas.width/2, canvas.height/2);
ctx.rotate(-x * Math.PI / 180);
ctx.translate(-canvas.width/2, -canvas.width/2);
}
For whatever reason, this only detects collision between one of the polygon's walls, which doesn't make sense to me because it is capturing all the polygon's points and should be comparing them all to the circle's positions. This is a snippet of the console output for the parallel arrays:
X values: 100.26987463346667,2.871334680692655,162.69628391854758,358.87207475054254,320.2904320167505
Y values: 250.03751023155084,132.28082275846333,92.90811041924998,186.33112343743124,283.44243315330453
What am I doing wrong? No matter how I fiddle with the code, it only ever detects one wall. Even when rotation is turned off. Even when the number of sides is changed. Even when the speed of rotation as well as the speed of gravity are adjusted. It is driving me nuts! Any help you all provide will be greatly appreciated.
Here is a full snippet of the code in question. If you play with rotation speed, it helps to see which side is operational.
let canvas = document.getElementById("mainCanvas");
let ctx = canvas.getContext("2d");
let rotationSlider = document.getElementById("rotationSlider");
let sidesSlider = document.getElementById("sidesSlider");
//# sided polygon to draw
let sides = 5;
let speed = 33;
let rotationIncrement = 1;
let sideLength = 0;
//stores current corner coordinates for polygon
let currentPolyPointsX = [];
let currentPolyPointsY = [];
const GRAVITY = 1;
const Ball = {
x: 0,
y: 0,
vector: [0, 0],
speed: 0,
gravity: 1,
//returns the ball's radius, which is set according to canvas height
returnRadius: function() {
return canvas.height * 0.01;
}
}
let ballHolder = [];
function startup() {
resizeCanvas();
main();
ballInit(10);
sideLengthInit();
}
function main() {
//get center coordinates of canvas
var center = canvas.width / 2;
//pass desired size, and center coords (x,y) to drawHexagon
rotationLoop();
ballLoop();
setTimeout(main, speed);
}
function ballInit(amount) {
//clear any residual balls
ballHolder = [];
//initialize array of objects with {amount} of balls and give each a random jitter
for (var i = 0; i < amount; i++) {
ballHolder.push(Object.create(Ball));
ballHolder[i].x = canvas.height / 2 + (Math.floor(Math.random() * 50)) * (Math.random() < 0.5 ? -1 : 1);
ballHolder[i].y = canvas.height / 2 + (Math.floor(Math.random() * 50)) * (Math.random() < 0.5 ? -1 : 1);
}
}
//calculated distance between two points via distance formula
function distanceCalc(x1, y1, x2, y2) {
return Math.sqrt(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)));
}
function ballLoop() {
//draw each ball on the canvas, and calculate bounce + collision with container
ballHolder.forEach(ball => {
//calculate gravity
ball.y = gravityCalc(ball);
ctx.beginPath();
ctx.arc((ball.x), (ball.y), canvas.height * 0.01, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = "blue";
ctx.fill();
//check for collision
if (collisionDetector(ball)) {
//calculate bounce if collision occurs
ball.gravity = ball.gravity * -1;
}
//console.log("ball " + ball.x + " " + ball.y);
});
}
function gravityCalc(ball) {
return ball.y + (GRAVITY * ball.gravity);
}
function sideLengthInit() {
sideLength = distanceCalc(currentPolyPointsX[0], currentPolyPointsY[0],
currentPolyPointsX[1], currentPolyPointsY[1])
}
function collisionDetector(ball) {
//calculate distance from all sides of the polygon
for (var i = 0; i < currentPolyPointsX.length - 1; i++) {
//get coordinates of line end points
let x1 = currentPolyPointsX[i];
let x2 = currentPolyPointsX[i + 1];
let y1 = currentPolyPointsY[i];
let y2 = currentPolyPointsY[i + 1];
//calculate length of line
let distanceX = x1 - x2;
let distanceY = y1 - y2;
let length = Math.sqrt((distanceX * distanceX) + (distanceY * distanceY));
//calculate dot product of vectors from line ends and ball
let dot = (((ball.x - x1) * (x2 - x1)) + ((ball.y - y1) * (y2 - y1))) /
Math.pow(length, 2);
//calculate x and y coordinate on line (extends to infinity) closest to ball
let closestX = x1 + (dot * (x2 - x1));
let closestY = y1 + (dot * (y2 - y1));
//if those coordinates are not currently on our line, return false
if (!onLine(x1, y1, x2, y2, closestX, closestY)) {
return false;
}
//calculate distance from closest coordinates to ball
distanceX = closestX - ball.x;
distanceY = closestY - ball.y;
let distance = Math.sqrt((distanceX * distanceX) + (distanceY * distanceY));
//if the ball is less than/equal to one radius away, it has collided
if (distance <= ball.returnRadius()) {
/*console.log("COLLISION: " + ball.x + " " + ball.y + " " + closestX + " " + closestY + " " +
"\n " + distance + "\n " + currentPolyPointsX + "\n " + currentPolyPointsY);*/
return true;
} else {
return false;
}
}
}
function onLine(x1, y1, x2, y2, px, py) {
let length = distanceCalc(x1, y1, x2, y2);
let distance1 = distanceCalc(px, py, x1, y1);
let distance2 = distanceCalc(px, py, x2, y2);
let buffer = 1;
return (distance1 + distance2 >= length - buffer && distance1 + distance2 <= length + buffer);
}
let rotationAmount = 0;
function rotationLoop() {
if (rotationAmount < 360) {
rotationAmount += parseInt(rotationIncrement);
}
if (rotationAmount >= 360) {
rotationAmount = 0;
}
rotatePolygon(rotationAmount);
}
function drawPolygon() { //x&y are positions, side is side number, r is size, color is to fill
//get current values of canvas center
var x = canvas.width / 2;
var y = canvas.height / 2;
var r = canvas.width / 2;
//draw {sides} sided polygon
ctx.beginPath();
ctx.moveTo(x + r * Math.cos(0), y + r * Math.sin(0));
//clear currently stored points
currentPolyPointsX.length = 0;
currentPolyPointsY.length = 0;
//draw and save new polygon points in array
for (var i = 1; i <= sides; i++) {
ctx.lineTo(x + r * Math.cos(i * 2 * Math.PI / sides), y + r * Math.sin(i * 2 * Math.PI / sides));
currentPolyPointsX.push(x + r * Math.cos(i * 2 * Math.PI / sides));
currentPolyPointsY.push(y + r * Math.sin(i * 2 * Math.PI / sides));
}
//draw the polygon
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.stroke();
ctx.save();
//console.log("drawn " + x + " " + y + " " + sides + " " + r);
}
function rotatePolygon(x) {
let centerX = canvas.height / 2;
let centerY = canvas.height / 2;
let angle = x * Math.PI / 180
//clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
//move rotation point to center of canvas
ctx.translate(centerX, centerY);
//rotate around center of canvas
ctx.rotate(angle);
//move back to origin
ctx.translate(-centerX, -centerY);
//draw rotated polygon
drawPolygon();
//adjust coordinates to match rotation
let cos = Math.cos(x * Math.PI / 180);
let sin = Math.sin(x * Math.PI / 180);
for (let i = 0; i < currentPolyPointsX.length; i++) {
currentPolyPointsX[i] = ((cos * (currentPolyPointsX[i] - centerX)) -
(sin * (currentPolyPointsY[i] - centerY)) + centerX);
currentPolyPointsY[i] = ((cos * (currentPolyPointsY[i] - centerY)) +
(sin * (currentPolyPointsX[i] - centerX)) + centerY);
}
/*console.log("X values: " + currentPolyPointsX + "\n" +
"Y values: " + currentPolyPointsY);*/
//un-rotate
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(-x * Math.PI / 180);
ctx.translate(-canvas.width / 2, -canvas.width / 2);
//console.log("rotated " + x);
}
function resizeCanvas() {
//gather window dimensions
var height = window.innerHeight;
var width = window.innerWidth;
//if dimensions are portrait, resize canvas based on height
if (height < width) {
var canvasSquared = window.innerHeight * .8 + "px";
} else { //if dimensions are landscape/square, resize canvas based on width
var canvasSquared = window.innerWidth * .8 + "px";
}
//commit canvas dimensions
canvas.height = parseFloat(canvasSquared);
canvas.style.height = canvasSquared;
canvas.width = parseFloat(canvasSquared);
canvas.style.width = canvasSquared;
//draw new hexagon of appropriate size
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawPolygon();
console.log("resized: " + canvasSquared + " width : " + canvas.width + " height: " + canvas.height);
}
rotationSlider.oninput = () => rotationIncrement = rotationSlider.value;
sidesSlider.oninput = () => sides = sidesSlider.value;
window.onresize = resizeCanvas;
body {
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
}
.mainCanvas {
width: 600px;
height: 600px;
border: 1px solid black;
box-shadow: 0em 0em 0.5em grey;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gravity Synth</title>
<meta name="description" content="Sounds Go Bonk">
<meta name="author" content="D.A.">
<meta property="og:title" content="Sounds Go Bonk">
<meta property="og:type" content="website">
<meta property="og:description" content="Sounds Go Bonk">
<!--
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
-->
<link rel="stylesheet" href="styles.css">
</head>
<body onload="startup()">
<canvas id="mainCanvas" class="mainCanvas"></canvas>
<div>Speed</div>
<input type="range" min=0 max=5 value=1 class="slider" id="rotationSlider">
<div>Sides</div>
<input type="range" min=3 max=9 value=5 class="slider" id="sidesSlider">
<script src="scripts.js"></script>
</body>
</html>
I have written a javascript program that uses a genetic algorithm to recreate an image only using triangles. Here's the strategy:
generate a random pool of models, each model having an array of triangles (3 points and a color)
evaluate the fitness of each model. To do so, I compare the original image's pixel array with my model's. I use Cosine Similarity to compare arrays
keep the best models, and mate them to create new models
randomly mutate some of the models
evaluate the new pool and continue
It works quite well after some iterations as you can see here:
The problem I have, is that it is very slow, most of the time is spent getting model's pixels (converting list of triangles (color + points) to a pixel array).
Here's how I do so now:
My pixel-array is a 1D array, I need to be able to convert x,y coordinates to index:
static getIndex(x, y, width) {
return 4 * (width * y + x);
}
Then I am able to draw a point:
static plot(x, y, color, img) {
let idx = this.getIndex(x, y, img.width);
let added = [color.r, color.g, color.b, map(color.a, 0, 255, 0, 1)];
let base = [img.pixels[idx], img.pixels[idx + 1], img.pixels[idx + 2], map(img.pixels[idx + 3], 0, 255, 0, 1)];
let a01 = 1 - (1 - added[3]) * (1 - base[3]);
img.pixels[idx + 0] = Math.round((added[0] * added[3] / a01) + (base[0] * base[3] * (1 - added[3]) / a01)); // red
img.pixels[idx + 1] = Math.round((added[1] * added[3] / a01) + (base[1] * base[3] * (1 - added[3]) / a01)); // green
img.pixels[idx + 2] = Math.round((added[2] * added[3] / a01) + (base[2] * base[3] * (1 - added[3]) / a01)); // blue
img.pixels[idx + 3] = Math.round(map(a01, 0, 1, 0, 255));
}
Then a line:
static line(x0, y0, x1, y1, img, color) {
x0 = Math.round(x0);
y0 = Math.round(y0);
x1 = Math.round(x1);
y1 = Math.round(y1);
let dx = Math.abs(x1 - x0);
let dy = Math.abs(y1 - y0);
let sx = x0 < x1 ? 1 : -1;
let sy = y0 < y1 ? 1 : -1;
let err = dx - dy;
do {
this.plot(x0, y0, color, img);
let e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
} while (x0 != x1 || y0 != y1);
}
And finally, a triangle:
static drawTriangle(triangle, img) {
for (let i = 0; i < triangle.points.length; i++) {
let point = triangle.points[i];
let p1 =
i === triangle.points.length - 1
? triangle.points[0]
: triangle.points[i + 1];
this.line(point.x, point.y, p1.x, p1.y, img, triangle.color);
}
this.fillTriangle(triangle, img);
}
static fillTriangle(triangle, img) {
let vertices = Array.from(triangle.points);
vertices.sort((a, b) => a.y > b.y);
if (vertices[1].y == vertices[2].y) {
this.fillBottomFlatTriangle(vertices[0], vertices[1], vertices[2], img, triangle.color);
} else if (vertices[0].y == vertices[1].y) {
this.fillTopFlatTriangle(vertices[0], vertices[1], vertices[2], img, triangle.color);
} else {
let v4 = {
x: vertices[0].x + float(vertices[1].y - vertices[0].y) / float(vertices[2].y - vertices[0].y) * (vertices[2].x - vertices[0].x),
y: vertices[1].y
};
this.fillBottomFlatTriangle(vertices[0], vertices[1], v4, img, triangle.color);
this.fillTopFlatTriangle(vertices[1], v4, vertices[2], img, triangle.color);
}
}
static fillBottomFlatTriangle(v1, v2, v3, img, color) {
let invslope1 = (v2.x - v1.x) / (v2.y - v1.y);
let invslope2 = (v3.x - v1.x) / (v3.y - v1.y);
let curx1 = v1.x;
let curx2 = v1.x;
for (let scanlineY = v1.y; scanlineY <= v2.y; scanlineY++) {
this.line(curx1, scanlineY, curx2, scanlineY, img, color);
curx1 += invslope1;
curx2 += invslope2;
}
}
static fillTopFlatTriangle(v1, v2, v3, img, color) {
let invslope1 = (v3.x - v1.x) / (v3.y - v1.y);
let invslope2 = (v3.x - v2.x) / (v3.y - v2.y);
let curx1 = v3.x;
let curx2 = v3.x;
for (let scanlineY = v3.y; scanlineY > v1.y; scanlineY--) {
this.line(curx1, scanlineY, curx2, scanlineY, img, color);
curx1 -= invslope1;
curx2 -= invslope2;
}
}
You can see full code in action here
So, I would like to know:
is it possible to optimize this code ?
if yes, what would be the best way to do so ? Maybe there is a library doing all of the drawing stuff way better than I did ? Or by using workers ?
Thanks !
I have tested your suggestions, here's the results:
Use RMS instead of Cosine Similarity: I am not sur if the measure of similarity is better, but it is definitively not worse. It seems to run a little bit faster too.
Use UInt8Array: It surely have an impact, but does not runs a lot faster. Not slower though.
Draw to invisible canvas: Definitively faster and easier! I can remove all of my drawing functions and replace it with a few lines of code, and it runs a lot faster !
Here's the code to draw to an invisible canvas:
var canvas = document.createElement('canvas');
canvas.id = "CursorLayer";
canvas.width = this.width;
canvas.height = this.height;
canvas.display = "none";
var body = document.getElementsByTagName("body")[0];
body.appendChild(canvas);
var ctx = canvas.getContext("2d");
ctx.fillStyle = "rgba(0, 0, 0, 1)";
ctx.fillRect(0, 0, this.width, this.height);
for (let i = 0; i < this.items.length; i++) {
let item = this.items[i];
ctx.fillStyle = "rgba(" +item.color.r + ','+item.color.g+','+item.color.b+','+map(item.color.a, 0, 255, 0, 1)+")";
ctx.beginPath();
ctx.moveTo(item.points[0].x, item.points[0].y);
ctx.lineTo(item.points[1].x, item.points[1].y);
ctx.lineTo(item.points[2].x, item.points[2].y);
ctx.fill();
}
let pixels = ctx.getImageData(0, 0, this.width, this.height).data;
//delete canvas
body.removeChild(canvas);
return pixels;
Before those changements, my code were running at about 1.68 iterations per second.
Now it runs at about 16.45 iterations per second !
See full code here.
Thanks again !
I want to get the rendered size (width/height) of a bézier curve in HTML5 canvas
context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
with this code, for instance
// control points
var cp1x = 200,
cp1y = 150,
cp2x = 260,
cp2y = 10;
var x = 0,
y = 0;
// calculation
var curveWidth = cp1x > x ? cp1x - x : x - cp1x,
curveHeight = cp1y > y ? cp1y - y : y - cp1y;
However, the cp2 point can increase the curve distance (length, size). I.e., suppose cp2 point is the red point in this image and its x coordinate is bigger than cp1's x, which looks to be the end point of the bézier curve:
So, how can I consider the length of cp2 point in curveWidth and in curveHeight to be exact?
To get extent of a quadratic bezier
The points
var x1 = ? // start
var y1 = ?
var x2 = ? // control
var y2 = ?
var x3 = ? // end
var y3 = ?
The extent
extent = {
minx : null,
miny : null,
maxx : null,
maxy : null,
}
The Math.
These equation apply for the x and y axis (thus two equations)
For quadratic bezier
F(u) = a(1-u)^2+2b(1-u)u+cu^2
which is more familiar in the form of a quadratic equation
Ax^2 + Bx + C = 0
so the bezier rearranged
F(u) = (a-2b+c)u^2+2(-a+b)u+a
We need the derivative so that becomes
2(a-2b+c)u-2a+2b
simplify divide common factor 2 to give
(a-2b+c)u + b - a = 0
separate out u
b-a = (a-2b + c)u
(b-a) / (a - 2b + c) = u
Then algorithm optimised for the fact the (b-a) part of (a-2b-c)
function solveB2(a,b,c){
var ba = b-a;
return ba / (ba - (c-b)); // the position on the curve of the maxima
}
var ux = solveB2(x1,x2,x3);
var uy = solveB2(y1,y2,y3);
These two values are positions along the curve so we now just have to find the coordinates of these two points. We need a function that finds a point on a quadratic bezier
function findPoint(u,x1,y1,x2,y2,x3,y3){ // returns array with x, and y at u
var xx1 = x1 + (x2 - x1) * u;
var yy1 = y1 + (y2 - y1) * u;
var xx2 = x2 + (x3 - x2) * u;
var yy2 = y2 + (y3 - y2) * u;
return [
xx1 + (xx2 - xx1) * u,
yy1 + (yy2 - yy1) * u
]
}
First check that they are on the curve and find the point at ux,uy
if(ux >= 0 && ux <= 1){
var px = findPoint(ux,x1,y1,x2,y2,x3,y3);
}
if(uy >= 0 && uy <= 1){
var py = findPoint(uy,x1,y1,x2,y2,x3,y3);
}
Now test against the extent
extent.minx = Math.min(x1,x3,px[0],py[0]);
extent.miny = Math.min(y1,y3,px[1],py[1]);
extent.maxx = Math.max(x1,x3,px[0],py[0]);
extent.maxy = Math.max(y1,y3,px[1],py[1]);
And you are done
extent has the coordinates of the box around the bezier. Top left (min) and bottom right (max)
You can also get the minimum bounding box if you rotate the bezier so that the start and end points fall on the x axis. Then do the above and the resulting rectangle is the minimum sized rectangle that can be placed around the bezier.
Cubics are much the same but just a lot more typing.
And a demo, just to make sure I got it all correct.
var canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 400;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
var x1,y1,x2,y2,x3,y3;
var ux,uy,px,py;
var extent = {
minx : null,
miny : null,
maxx : null,
maxy : null,
}
function solveB2(a,b,c){
var ba = b-a;
return ba / (ba - (c-b)); // the position on the curve of the maxima
}
function findPoint(u,x1,y1,x2,y2,x3,y3){ // returns array with x, and y at u
var xx1 = x1 + (x2 - x1) * u;
var yy1 = y1 + (y2 - y1) * u;
var xx2 = x2 + (x3 - x2) * u;
var yy2 = y2 + (y3 - y2) * u;
return [
xx1 + (xx2 - xx1) * u,
yy1 + (yy2 - yy1) * u
]
}
function update(time){
ctx.clearRect(0,0,800,400);
// create random bezier
x1 = Math.cos(time / 1000) * 300 + 400;
y1 = Math.sin(time / 2100) * 150 + 200;
x2 = Math.cos((time + 3000) / 1200) * 300 + 400;
y2 = Math.sin(time / 2300) * 150 + 200;
x3 = Math.cos(time / 1400) * 300 + 400;
y3 = Math.sin(time / 2500) * 150 + 200;
// solve for bounds
var ux = solveB2(x1,x2,x3);
var uy = solveB2(y1,y2,y3);
if(ux >= 0 && ux <= 1){
px = findPoint(ux,x1,y1,x2,y2,x3,y3);
}else{
px = [x1,y1]; // a bit of a cheat but saves having to put in extra conditions
}
if(uy >= 0 && uy <= 1){
py = findPoint(uy,x1,y1,x2,y2,x3,y3);
}else{
py = [x3,y3]; // a bit of a cheat but saves having to put in extra conditions
}
extent.minx = Math.min(x1,x3,px[0],py[0]);
extent.miny = Math.min(y1,y3,px[1],py[1]);
extent.maxx = Math.max(x1,x3,px[0],py[0]);
extent.maxy = Math.max(y1,y3,px[1],py[1]);
// draw the rectangle
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.strokeRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
ctx.fillStyle = "rgba(255,200,0,0.2)";
ctx.fillRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
// show points
ctx.fillStyle = "blue";
ctx.fillRect(x1-3,y1-3,6,6);
ctx.fillRect(x3-3,y3-3,6,6);
ctx.fillStyle = "black";
ctx.fillRect(px[0]-4,px[1]-4,8,8);
ctx.fillRect(py[0]-4,py[1]-4,8,8);
ctx.lineWidth = 3;
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.quadraticCurveTo(x2,y2,x3,y3);
ctx.stroke();
// control point
ctx.lineWidth = 1;
ctx.strokeStyle = "#0a0";
ctx.strokeRect(x2-3,y2-3,6,6);
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.lineTo(x3,y3);
ctx.stroke();
// do it all again
requestAnimationFrame(update);
}
requestAnimationFrame(update);
UPDATE
While musing over the bezier I realised that I could remove a lot of code if I assumed that the bezier was normalised (the end points start at (0,0) and end at (1,1)) because the zeros can be removed and the ones simplified.
While changing the code I also realized that I had needlessly calculated the x and y for both the x and y extent coordinates. Giving 4 values while I only need 2.
The resulting code is much simpler. I remove the function solveB2 and findPoint as the calculations seam too trivial to bother with functions.
To find the x and y maxima from quadratic bezier defined with x1, y1, x2, y2, x3, y3
// solve quadratic for bounds by normalizing equation
var brx = x3 - x1; // get x range
var bx = x2 - x1; // get x control point offset
var x = bx / brx; // normalise control point which is used to check if maxima is in range
// do the same for the y points
var bry = y3 - y1;
var by = y2 - y1
var y = by / bry;
var px = [x1,y1]; // set defaults in case maximas outside range
if(x < 0 || x > 1){ // check if x maxima is on the curve
px[0] = bx * bx / (2 * bx - brx) + x1; // get the x maxima
}
if(y < 0 || y > 1){ // same as x
px[1] = by * by / (2 * by - bry) + y1;
}
// now only need to check the x and y maxima not the coordinates of the maxima
extent.minx = Math.min(x1,x3,px[0]);
extent.miny = Math.min(y1,y3,px[1]);
extent.maxx = Math.max(x1,x3,px[0]);
extent.maxy = Math.max(y1,y3,px[1]);
And the example code which has far better performance but unlike the previous demo this version does not calculate the actual coordinates of the x and y maximas.
var canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 400;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
var x1,y1,x2,y2,x3,y3;
var ux,uy,px,py;
var extent = {
minx : null,
miny : null,
maxx : null,
maxy : null,
}
function update(time){
ctx.clearRect(0,0,800,400);
// create random bezier
x1 = Math.cos(time / 1000) * 300 + 400;
y1 = Math.sin(time / 2100) * 150 + 200;
x2 = Math.cos((time + 3000) / 1200) * 300 + 400;
y2 = Math.sin(time / 2300) * 150 + 200;
x3 = Math.cos(time / 1400) * 300 + 400;
y3 = Math.sin(time / 2500) * 150 + 200;
// solve quadratic for bounds by normalizing equation
var brx = x3 - x1; // get x range
var bx = x2 - x1; // get x control point offset
var x = bx / brx; // normalise control point which is used to check if maxima is in range
// do the same for the y points
var bry = y3 - y1;
var by = y2 - y1
var y = by / bry;
var px = [x1,y1]; // set defaults in case maximas outside range
if(x < 0 || x > 1){ // check if x maxima is on the curve
px[0] = bx * bx / (2 * bx - brx) + x1; // get the x maxima
}
if(y < 0 || y > 1){ // same as x
px[1] = by * by / (2 * by - bry) + y1;
}
// now only need to check the x and y maxima not the coordinates of the maxima
extent.minx = Math.min(x1,x3,px[0]);
extent.miny = Math.min(y1,y3,px[1]);
extent.maxx = Math.max(x1,x3,px[0]);
extent.maxy = Math.max(y1,y3,px[1]);
// draw the rectangle
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.strokeRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
ctx.fillStyle = "rgba(255,200,0,0.2)";
ctx.fillRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
// show points
ctx.fillStyle = "blue";
ctx.fillRect(x1-3,y1-3,6,6);
ctx.fillRect(x3-3,y3-3,6,6);
ctx.fillStyle = "black";
ctx.fillRect(px[0]-4,px[1]-4,8,8);
ctx.lineWidth = 3;
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.quadraticCurveTo(x2,y2,x3,y3);
ctx.stroke();
// control point
ctx.lineWidth = 1;
ctx.strokeStyle = "#0a0";
ctx.strokeRect(x2-3,y2-3,6,6);
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.lineTo(x3,y3);
ctx.stroke();
// do it all again
requestAnimationFrame(update);
}
requestAnimationFrame(update);
My goal is to wrap a image over a coffee mug virtually using HTML5 and javascript.
I have created a prototype:
http://jsfiddle.net/sandeepkum88/uamov2m7/
Currently I am cutting the image in strips of width 1px and then placing those strips on the bezier curve.
var img = new Image();
img.onload = start;
img.src = "http://in-sandeep.printvenue.org/test-images/mug-strap.png";
var pointer = 0;
function start() {
var iw = 198.97826;
var ih = img.height;
var x1 = 50;
var y1 = 40;
var x2 = 97;
var y2 = 60;
var x3 = 152;
var y3 = 40;
// calc unit value for t to calculate bezier curve
var unitT = 1 / iw;
// put image slices on the curve
for (var X = 0, t = 0; X < iw; X++, t+=unitT) {
var xTop = (1-t) * (1-t) * x1 + 2 * (1 - t) * t * x2 + t * t * x3;
var yTop = (1-t) * (1-t) * y1 + 2 * (1 - t) * t * y2 + t * t * y3;
ctx.drawImage(img, X + pointer, 0, 1, ih, xTop, yTop, 0.85, ih-188);
}
}
But I am not satisfied with the result.
Am I doing it wrong somewhere? Is there any better alternative.
What if top curve and bottom curve are different.
Is there a way to achieve this using transformation matrix?
Being relatively new to the HTML5 game, just wanted to ask if anyone knew if it was possible to animate a dashed line along a path? Think snake from Nokia days, just with a dashed line...
I've got a dashed line (which represents electrical current flow), which I'd like to animate as 'moving' to show that current is flowing to something.
Thanks to Rod's answer on this post, I've got the dashed line going, but am not sure where to start to get it moving. Anyone know where to begin?
Thanks!
Got it working here, based on this post by phrogz.
What i did:
Add a start parameter which is a number between 0 and 99
Calculate the dashSize summing the contents of the dash array
Calculate dashOffSet as a fraction of dashSize based on start percent
Subtracted the offset from x, y and added to dx, dy
Only started drawying after the offset been gone (it´s negative, remember)
Added a setInterval to update the start from 0 to 99, step of 10
Update
The original algorithm wasn't working for vertical or negative inclined lines. Added a check to use the inclination based on the y slope on those cases, and not on the x slope.
Demo here
Updated code:
if (window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype.lineTo) {
CanvasRenderingContext2D.prototype.dashedLine = function(x, y, x2, y2, dashArray, start) {
if (!dashArray) dashArray = [10, 5];
var dashCount = dashArray.length;
var dashSize = 0;
for (i = 0; i < dashCount; i++) dashSize += parseInt(dashArray[i]);
var dx = (x2 - x),
dy = (y2 - y);
var slopex = (dy < dx);
var slope = (slopex) ? dy / dx : dx / dy;
var dashOffSet = dashSize * (1 - (start / 100))
if (slopex) {
var xOffsetStep = Math.sqrt(dashOffSet * dashOffSet / (1 + slope * slope));
x -= xOffsetStep;
dx += xOffsetStep;
y -= slope * xOffsetStep;
dy += slope * xOffsetStep;
} else {
var yOffsetStep = Math.sqrt(dashOffSet * dashOffSet / (1 + slope * slope));
y -= yOffsetStep;
dy += yOffsetStep;
x -= slope * yOffsetStep;
dx += slope * yOffsetStep;
}
this.moveTo(x, y);
var distRemaining = Math.sqrt(dx * dx + dy * dy);
var dashIndex = 0,
draw = true;
while (distRemaining >= 0.1 && dashIndex < 10000) {
var dashLength = dashArray[dashIndex++ % dashCount];
if (dashLength > distRemaining) dashLength = distRemaining;
if (slopex) {
var xStep = Math.sqrt(dashLength * dashLength / (1 + slope * slope));
x += xStep
y += slope * xStep;
} else {
var yStep = Math.sqrt(dashLength * dashLength / (1 + slope * slope));
y += yStep
x += slope * yStep;
}
if (dashOffSet > 0) {
dashOffSet -= dashLength;
this.moveTo(x, y);
} else {
this[draw ? 'lineTo' : 'moveTo'](x, y);
}
distRemaining -= dashLength;
draw = !draw;
}
// Ensure that the last segment is closed for proper stroking
this.moveTo(0, 0);
}
}
var dashes = '10 20 2 20'
var c = document.getElementsByTagName('canvas')[0];
c.width = 300;
c.height = 400;
var ctx = c.getContext('2d');
ctx.strokeStyle = 'black';
var drawDashes = function() {
ctx.clearRect(0, 0, c.width, c.height);
var dashGapArray = dashes.replace(/^\s+|\s+$/g, '').split(/\s+/);
if (!dashGapArray[0] || (dashGapArray.length == 1 && dashGapArray[0] == 0)) return;
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.dashedLine(10, 0, 10, c.height, dashGapArray, currentOffset);
ctx.dashedLine(0, 10, c.width, 10, dashGapArray, currentOffset);
ctx.dashedLine(0, 0, c.width, c.height, dashGapArray, currentOffset);
ctx.dashedLine(0, c.height, c.width, 0, dashGapArray, currentOffset);
ctx.closePath();
ctx.stroke();
};
window.setInterval(dashInterval, 500);
var currentOffset = 0;
function dashInterval() {
drawDashes();
currentOffset += 10;
if (currentOffset >= 100) currentOffset = 0;
}
You can create the dashed line animation using SNAPSVG library.
Please check the tutorial here DEMO