Javascript find pixels while canvas is rotated - javascript

I'm trying to write rotated text (on various angles) on canvas but wish not to overlap the texts. Therefore after rotating the canvas and before filling the text I tried to test the text background using measureText().width and getImageData() to see that there is no text already there to get messed with new. I fail to find the text (coloured pixels) while the canvas is rotated. Here is a simplified version (using rectangle) to my problem. I wonder why no coloured pixels are found?
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="300" height="150" style="border:1px solid black;">
Your browser does not support the HTML5 canvas tag.</canvas>
<script>
var cWidth=300, cHeight= 150;
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
// Rotate context around centre point
ctx.translate( cWidth/2, cHeight/2);
ctx.rotate(20 * Math.PI / 180);
// Draw 100x50px rectangle at centre of rotated ctx
ctx.fillStyle = "yellow";
ctx.fillRect(-50, -25, 100, 50);
// Is my rotated rectangle really there?
// i.e. any coloured pixels at rotated cxt centre
imgData = ctx.getImageData(-50, -25, 100, 50);
// All rectangle pixels should be coloured
for (var j=0; j<imgData.data.length; j++){
if (imgData.data[j] > 0){
alert ("Coloured");
break;
};
};
// Why none is found?
</script>
</body>
</html>
The yellow rectangle should be at the same spot and angle as the tested image data area is. What went wrong? How I can test the colour of a rotated area? Being novice to Javascript I try to avoid libraries at this stage.
pekka

The problem is the translate function. You need to account for the displacement.
Try this:
imgData = ctx.getImageData(-50+cWidth/2,-25+cHeight/2,100,50);

The issue with your code is that getImageData() was targeting the wrong portion of the canvas. The x and y coordinates should have the same value of the translate() function. This is how your code should look like:
// Translate the rectangle, rotate it and fill it
ctx.translate(cWidth/2, cHeight/2);
ctx.rotate(20 * Math.PI / 180);
ctx.fillStyle = "yellow";
ctx.fillRect(-50, -25, 100, 50);
// Get the rectangle rotation
var imgData = ctx.getImageData(cWidth/2, cHeight/2, 100, 50);
And this is the JSfiddle with the complete code. I hope that my answer helps you!

General purpose answer.
This answer will work for any type of transformation and relies on the fact that the 2D context transform is mirrored in javascript. It gets the pixels one by one. Another way is to get the pixels as a block by transforming the corners of the rendered box and finding the bounding box that holds the transformed box. Use that box to get the image data. You will still have to transform each pixel address you want to check as the image data will also include extra pixels outside the rendered box. This may be quicker than the solution presented below if the searched for pixels are far and few between, The solution below is better if the odds of finding the pixel you want are high and you can break out of the iterations early.
You will need to use the transform API at the bottom of this answer
var mMatrix = new Transform(); // creates a default matrix
// define the bounds
const box = {x : -50, y : -25, w : 100, h : 50}; // our box
ctx.translate( cWidth/2, cHeight/2); // set the context transform
ctx.rotate(20 * Math.PI / 180);
// draw the stuff
ctx.fillStyle = "yellow";
ctx.fillRect(box.x, box.y, box.w, box.h);
// mirror the ctx transformations
mMatrix.translate(cWidth/2, cHeight/2).rotate(20 * Math.PI / 180);
var ix,iy,x,y;
var v = new Transform.Vec(); // create a working vec to save memory usage and anyoning GC hits
for(iy = 0; iy < box.h; iy ++){ // for each vertical pixel
for(ix = 0; iy < box.w; ix ++){ // for each horizontal
v.x = ix + 0.5 + box.x; // use pixel center
v.y = iy + 0.5 + box.y;
mMatrix.applyToVec(v); // transform into screen coords.
v.x = Math.floor(v.x); // get rid of grogens
v.y = Math.floor(v.y);
// now we have the pixel address corresponding to the box coordinate ix,iy
// get one pixel first check if it is on the canvas
if(v.x >= 0 && v.x < ctx.canvas.width && v.y >= 0 && v.y < ctx.canvas.height){
var pD = ctx.getImageData(v.x,v.y,1,1).data;
var red = pD[0];
var green = pD[1];
var blue = pD[2];
var alpha = pD[3];
// now you have the RGB values
... do what ever you want with that info
}
}
}
Transform API
This is the transform API required by the answer. It is a cut down of a API written by me and you can do with what you want (apart from evil things). See answer for usage. It is just the very basics, you can find more comprehensive Transform API on the net (but I don't think you will find one much faster)
See comment at the bottom for method details. Most functions are chainable.
Code snippet runs nothing.
var Transform = (function () {
var tx, ty, v1, v2, v3, mat;
// work vecs and transform provide pre assigned working memory
v1 = new Vec();
v2 = new Vec();
v3 = new Vec();
mat = new Transform();
ty = tx = 0;
function Transform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) {
if (xAxisX === undefined) { // create identity matrix
this.xAxis = new Vec(); // Default vec is 1,0
this.yAxis = new Vec(0, 1);
this.origin = new Vec(0, 0);
} else if (yAxisY === undefined) { // if only 3 arguments assume that the 3 arguments are vecs
this.xAxis = new Vec(xAxisX.x, xAxisX.y); //
this.yAxis = new Vec(xAxisY.x, xAxisY.y);
this.origin = new Vec(yAxisY.x, xAxisY.y);
} else {
this.xAxis = new Vec(xAxisX, xAxisY); // Default vec is 1,0
this.yAxis = new Vec(yAxisX, yAxisY);
this.origin = new Vec(originX, originY);
}
};
function Vec(x, y) {
if (x === undefined || x === null) {
this.x = 1;
this.y = 0;
} else {
this.x = x;
this.y = y;
}
};
Vec.prototype = {
copy : function () {
return new Vec(this.x, this.y);
},
setAs : function (vec, y) { // set this to the value of vec, or if two arguments vec is x and y is y
if (y !== undefined) {
this.x = vec;
this.y = y;
return this;
}
this.x = vec.x;
this.y = vec.y;
return this;
}
}
Transform.prototype = {
xAxis : undefined,
yAxis : undefined,
origin : undefined,
Vec : Vec, // expose the Vec interface
copy : function () {
return new Transform(this.xAxis, this.yAxis, this.origin);
},
setAs : function (transform) {
this.xAxis.x = transform.xAxis.x;
this.xAxis.y = transform.xAxis.y;
this.yAxis.x = transform.yAxis.x;
this.yAxis.y = transform.yAxis.y;
this.origin.x = transform.origin.x;
this.origin.y = transform.origin.y;
return;
},
reset : function () { // resets this to the identity transform
this.xAxis.x = 1;
this.xAxis.y = 0;
this.yAxis.x = 0;
this.yAxis.y = 1;
this.origin.x = 0;
this.origin.y = 0;
return this;
},
apply : function (x, y) { // returns an object {x : trabsformedX, y : trabsformedY} the returned object does not have the Vec prototype
return {
x : x * this.xAxis.x + y * this.yAxis.x + this.origin.x,
y : x * this.xAxis.y + y * this.yAxis.y + this.origin.y
};
},
applyToVec : function (vec) { // WARNING returns this not the vec.
tx = vec.x * this.xAxis.x + vec.y * this.yAxis.x + this.origin.x;
vec.y = vec.x * this.xAxis.y + vec.y * this.yAxis.y + this.origin.y;
vec.x = tx;
return this;
},
invert : function () { // inverts the transform
// first check if just a scale translated identity matrix and invert that as it is quicker
if (this.xAxis.y === 0 && this.yAxis.x === 0 && this.xAxis.x !== 0 && this.yAxis.y !== 0) {
this.xAxis.x = 1 / this.xAxis.x;
this.xAxis.y = 0;
this.yAxis.x = 0;
this.yAxis.y = 1 / this.yAxis.y;
this.origin.x = -this.xAxis.x * this.origin.x;
this.origin.y = -this.yAxis.y * this.origin.y;
return this;
}
var cross = this.xAxis.x * this.yAxis.y - this.xAxis.y * this.yAxis.x;
v1.x = this.yAxis.y / cross;
v1.y = -this.xAxis.y / cross;
v2.x = -this.yAxis.x / cross;
v2.y = this.xAxis.x / cross;
v3.x = (this.yAxis.x * this.origin.y - this.yAxis.y * this.origin.x) / cross;
v3.y = - (this.xAxis.x * this.origin.y - this.xAxis.y * this.origin.x) / cross;
this.xAxis.x = v1.x;
this.xAxis.y = v1.y;
this.yAxis.x = v2.x;
this.yAxis.y = v2.y;
this.origin.x = v3.x;
this.origin.y = v3.y;
return this;
},
asInverse : function (transform) { // creates a new or uses supplied transform to return the inverse of this matrix
if (transform === undefined) {
transform = new Transform();
}
if (this.xAxis.y === 0 && this.yAxis.x === 0 && this.xAxis.x !== 0 && this.yAxis.y !== 0) {
transform.xAxis.x = 1 / this.xAxis.x;
transform.xAxis.y = 0;
transform.yAxis.x = 0;
transform.yAxis.y = 1 / this.yAxis.y;
transform.origin.x = -transform.xAxis.x * this.origin.x;
transform.origin.y = -transform.yAxis.y * this.origin.y;
return transform;
}
var cross = this.xAxis.x * this.yAxis.y - this.xAxis.y * this.yAxis.x;
transform.xAxis.x = this.yAxis.y / cross;
transform.xAxis.y = -this.xAxis.y / cross;
transform.yAxis.x = -this.yAxis.x / cross;
transform.yAxis.y = this.xAxis.x / cross;
transform.origin.x = (this.yAxis.x * this.origin.y - this.yAxis.y * this.origin.x) / cross;
transform.origin.y = - (this.xAxis.x * this.origin.y - this.xAxis.y * this.origin.x) / cross;
return transform;
},
multiply : function (transform) { // multiplies this with transform
var tt = transform;
var t = this;
v1.x = tt.xAxis.x * t.xAxis.x + tt.yAxis.x * t.xAxis.y;
v1.y = tt.xAxis.y * t.xAxis.x + tt.yAxis.y * t.xAxis.y;
v2.x = tt.xAxis.x * t.yAxis.x + tt.yAxis.x * t.yAxis.y;
v2.y = tt.xAxis.y * t.yAxis.x + tt.yAxis.y * t.yAxis.y;
v3.x = tt.xAxis.x * t.origin.x + tt.yAxis.x * t.origin.y + tt.origin.x;
v3.y = tt.xAxis.y * t.origin.x + tt.yAxis.y * t.origin.y + tt.origin.y;
t.xAxis.x = v1.x;
t.xAxis.y = v1.y;
t.yAxis.x = v2.x;
t.yAxis.y = v2.y;
t.origin.x = v3.x;
t.origin.y = v3.y;
return this;
},
rotate : function (angle) { // Multiply matrix by rotation matrix at angle
var xdx = Math.cos(angle);
var xdy = Math.sin(angle);
v1.x = xdx * this.xAxis.x + (-xdy) * this.xAxis.y;
v1.y = xdy * this.xAxis.x + xdx * this.xAxis.y;
v2.x = xdx * this.yAxis.x + (-xdy) * this.yAxis.y;
v2.y = xdy * this.yAxis.x + xdx * this.yAxis.y;
v3.x = xdx * this.origin.x + (-xdy) * this.origin.y;
v3.y = xdy * this.origin.x + xdx * this.origin.y;
this.xAxis.x = v1.x;
this.xAxis.y = v1.y;
this.yAxis.x = v2.x;
this.yAxis.y = v2.y;
this.origin.x = v3.x;
this.origin.y = v3.y;
return this;
},
scale : function (scaleX, scaleY) { // Multiply the matrix by scaleX and scaleY
this.xAxis.x *= scaleX;
this.xAxis.y *= scaleY;
this.yAxis.x *= scaleX;
this.yAxis.y *= scaleY;
this.origin.x *= scaleX;
this.origin.y *= scaleY;
return this;
},
translate : function (x, y) { // Multiply the matrix by translate Matrix
this.origin.x += x;
this.origin.y += y;
return this;
},
setTransform : function (xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) {
this.xAxis.x = xAxisX;
this.xAxis.y = xAxisY;
this.yAxis.x = yAxisX;
this.yAxis.y = yAxisY;
this.origin.x = originX;
this.origin.y = originY;
return this;
},
transform : function (xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) {
var t = this;
v1.x = xAxisX * t.xAxis.x + yAxisX * t.xAxis.x;
v1.y = xAxisY * t.xAxis.x + yAxisY * t.xAxis.y;
v2.x = xAxisX * t.yAxis.x + yAxisX * t.yAxis.x;
v2.y = xAxisY * t.yAxis.x + yAxisY * t.yAxis.y;
v3.x = xAxisX * t.origin.x + yAxisX * t.origin.y + originX;
v3.y = xAxisY * t.origin.x + yAxisY * t.origin.y + originY;
t.xAxis.x = v1.x;
t.xAxis.y = v1.y;
t.yAxis.x = v2.x;
t.yAxis.y = v2.y;
t.origin.x = v3.x;
t.origin.y = v3.y;
return this;
},
contextTransform : function (ctx) {
ctx.transform(this.xAxis.x, this.xAxis.y, this.yAxis.x, this.yAxis.y, this.origin.x, this.origin.y);
return this;
},
contextSetTransform : function (ctx) {
ctx.Settransform(this.xAxis.x, this.xAxis.y, this.yAxis.x, this.yAxis.y, this.origin.x, this.origin.y);
return this;
},
setFromCurrentContext : function(ctx){
if(ctx && typeof ctx.currentTransform === "object"){
var mat = ctx.currentTransform;
this.xAxis.x = mat.a;
this.xAxis.y = mat.b;
this.yAxis.x = mat.c;
this.yAxis.y = mat.d;
this.origin.x = mat.e;
this.origin.y = mat.f;
}
return this;
}
}
if(typeof document.createElement("canvas").getContext("2d").currentTransform !== "object"){
Transform.prototype.setFromCurrentContext = undefined;
}
return Transform;
})();
/*
rotate(angle) // Multiply matrix by rotation matrix at angle. Same as ctx.rotate
scale(scaleX, scaleY) // Multiply the matrix by scaleX and scaleY. Same as ctx.scale
translate(x, y) // Multiply the matrix by translate Matrix. Same as ctx.translate
setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) //Replaces the current reansform with the new values. Same as ctx.setTransform
transform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) // multiplies this transform with the supplied transform. Same as ctx.transform
Transform.xAxis // Vec object defining the direction and scale of the x Axis. Values are in canvas pixel coordinates
Transform.yAxis // Vec object defining the direction and scale of the y Axis. Values are in canvas pixel coordinates
Transform.origin // Vec object defining canvas pixel coordinates of the origin
Transform.Vec // interface to a vec object with basic interface needed to support Transform
Transform.reset() // resets the transform to the identity matrix (the default matrix used by 2D context)
Transform.copy() // creates a new copy of this object
Transform.setAs(transform) // sets the content of this to the values of the argument transform
Transform.apply(x, y) { // Transforms the coords x,y by multiplying them with this. Returns an object {x : trabsformedX, y : trabsformedY} the returned object does not have the Vec prototype
Transform.applyToVec(vec) // transforms the point vec. WARNING returns this not the vec.
Transform.invert() // inverts the transform
Transform.asInverse(transform) // creates a new or uses supplied transform to return the inverse of this matrix
Transform.multiply(transform) // multiplies this with transform
Transform.contextTransform(ctx) // multiplies the supplied context (ctx) transform by this.
Transform.contextSetTransform(ctx) // set the supplied context (ctx) transform to this
Transform.setFromCurrentContext(ctx) // Only for supported browser. Sets this to the supplied context current transformation. May not be available if there is no browser support
There is also access to the very simple Vec object. To create a vec `new Transform.Vec(x,y)`
*/

Related

How to detect collision between object made of bezier curves and a circle?

So I've wrote a microbe animation.
It's all cool, but I think that it would be even better, if the microbe would be able to eat diatoms, and to destroy bubbles.
The issue is that the microbe is made of bezier curves.
I have no idea how to check collision between object made of bezier curves, and a circle in a reasonable way.
The only thing that comes to my mind, is to paint the microbe shape and bubbles a hidden canvas, and then check if they paint to the same pixels. But that would cause big performance issues IMHO.
Code: https://codepen.io/michaelKurowski/pen/opWeKY
class Cell is the cell, while class CellWallNode is a node of bezier curve, in case if somebody needs to look up the implementation.
The bubbles and diatoms can be easily simplified to circles.
Solution to bounds testing object defined by beziers
Below is an example solution to finding if a circle is inside an object defined by a center point and a set of beziers defining the perimeter.
The solution has only been tested for non intersecting cubic beziers. Also will not work if there are more than two intercepts between the object being tested and the center of the cell. However all you need to solve for the more complex bounds is there in the code.
The method
Define a center point to test from as a 2D point
Define the test point as a 2D point
Define a line from the center to the test point
For each bezier
Translate bezier so first point is at start of line
Rotate the bezier such that the line is aligned to the x axis
Solve the bezier polynomials to find the roots (location of x axis intercepts)
Use the roots to find position on bezier curve of line intercept.
Use the closest intercept to the point to find distance from center to perimeter.
If perimeter distance is greater than test point distance plus radius then inside.
Notes
The test is to a point along a line to the center not to a circle which would be a area defined by a triangle. As long as the circle radius is small compared to the size of the beziers the approximation works well.
Not sure if you are using cubic or quadratic beziers so the solution covers both cubic and quadratic beziers.
Example
The snippet creates a set of beziers (cubic) around a center point. the object theBlob holds the animated beziers. The function testBlob tests the mouse position and returns true if inside theBlob. The object bezHelper contains all the functionality needed to solve the problem.
The cubic root solver was derived from github intersections cube root solver.
const bezHelper = (()=>{
// creates a 2D point
const P2 = (x=0, y= x === 0 ? 0 : x.y + (x = x.x, 0)) => ({x, y});
const setP2As = (p,pFrom) => (p.x = pFrom.x, p.y = pFrom.y, p);
// To prevent heap thrashing close over some pre defined 2D points
const v1 = P2();
const v2 = P2();
const v3 = P2();
const v4 = P2();
var u,u1,u2;
// solves quadratic for bezier 2 returns first root
function solveBezier2(A, B, C){
// solve the 2nd order bezier equation.
// There can be 2 roots, u,u1 hold the results;
// 2nd order function a+2(-a+b)x+(a-2b+c)x^2
a = (A - 2 * B + C);
b = 2 * ( - A + B);
c = A;
a1 = 2 * a;
c = b * b - 4 * a * c;
if(c < 0){
u = Infinity;
u1 = Infinity;
return u;
}else{
b1 = Math.sqrt(c);
}
u = (-b + b1) / a1;
u1 = (-b - b1) / a1;
return u;
}
// solves cubic for bezier 3 returns first root
function solveBezier3(A, B, C, D){
// There can be 3 roots, u,u1,u2 hold the results;
// Solves 3rd order a+(-2a+3b)t+(2a-6b+3c)t^2+(-a+3b-3c+d)t^3 Cardano method for finding roots
// this function was derived from http://pomax.github.io/bezierinfo/#intersections cube root solver
// Also see https://en.wikipedia.org/wiki/Cubic_function#Cardano.27s_method
function crt(v) {
if(v<0) return -Math.pow(-v,1/3);
return Math.pow(v,1/3);
}
function sqrt(v) {
if(v<0) return -Math.sqrt(-v);
return Math.sqrt(v);
}
var a, b, c, d, p, p3, q, q2, discriminant, U, v1, r, t, mp3, cosphi,phi, t1, sd;
u2 = u1 = u = -Infinity;
d = (-A + 3 * B - 3 * C + D);
a = (3 * A - 6 * B + 3 * C) / d;
b = (-3 * A + 3 * B) / d;
c = A / d;
p = (3 * b - a * a) / 3;
p3 = p / 3;
q = (2 * a * a * a - 9 * a * b + 27 * c) / 27;
q2 = q / 2;
a /= 3;
discriminant = q2 * q2 + p3 * p3 * p3;
if (discriminant < 0) {
mp3 = -p / 3;
r = sqrt(mp3 * mp3 * mp3);
t = -q / (2 * r);
cosphi = t < -1 ? -1 : t > 1 ? 1 : t;
phi = Math.acos(cosphi);
t1 = 2 * crt(r);
u = t1 * Math.cos(phi / 3) - a;
u1 = t1 * Math.cos((phi + 2 * Math.PI) / 3) - a;
u2 = t1 * Math.cos((phi + 4 * Math.PI) / 3) - a;
return u;
}
if(discriminant === 0) {
U = q2 < 0 ? crt(-q2) : -crt(q2);
u = 2 * U - a;
u1 = -U - a;
return u;
}
sd = sqrt(discriminant);
u = crt(sd - q2) - crt(sd + q2) - a;
return u;
}
// get a point on the bezier at pos ( from 0 to 1 values outside this range will be outside the bezier)
// p1, p2 are end points and cp1, cp2 are control points.
// ret is the resulting point. If given it is set to the result, if not given a new point is created
function getPositionOnBez(pos,p1,p2,cp1,cp2,ret = P2()){
if(pos === 0){
ret.x = p1.x;
ret.y = p1.y;
return ret;
}else
if(pos === 1){
ret.x = p2.x;
ret.y = p2.y;
return ret;
}
v1.x = p1.x;
v1.y = p1.y;
var c = pos;
if(cp2 === undefined){
v2.x = cp1.x;
v2.y = cp1.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (p2.x - v2.x) * c;
v2.y += (p2.y - v2.y) * c;
ret.x = v1.x + (v2.x - v1.x) * c;
ret.y = v1.y + (v2.y - v1.y) * c;
return ret;
}
v2.x = cp1.x;
v2.y = cp1.y;
v3.x = cp2.x;
v3.y = cp2.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
v3.x += (p2.x - v3.x) * c;
v3.y += (p2.y - v3.y) * c;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
ret.x = v1.x + (v2.x - v1.x) * c;
ret.y = v1.y + (v2.y - v1.y) * c;
return ret;
}
const cubicBez = 0;
const quadraticBez = 1;
const none = 2;
var type = none;
// working bezier
const p1 = P2();
const p2 = P2();
const cp1 = P2();
const cp2 = P2();
// rotated bezier
const rp1 = P2();
const rp2 = P2();
const rcp1 = P2();
const rcp2 = P2();
// translate and rotate bezier
function transformBez(pos,rot){
const ax = Math.cos(rot);
const ay = Math.sin(rot);
var x = p1.x - pos.x;
var y = p1.y - pos.y;
rp1.x = x * ax - y * ay;
rp1.y = x * ay + y * ax;
x = p2.x - pos.x;
y = p2.y - pos.y;
rp2.x = x * ax - y * ay;
rp2.y = x * ay + y * ax;
x = cp1.x - pos.x;
y = cp1.y - pos.y;
rcp1.x = x * ax - y * ay;
rcp1.y = x * ay + y * ax;
if(type === cubicBez){
x = cp2.x - pos.x;
y = cp2.y - pos.y;
rcp2.x = x * ax - y * ay;
rcp2.y = x * ay + y * ax;
}
}
function getPosition2(pos,ret){
return getPositionOnBez(pos,p1,p2,cp1,undefined,ret);
}
function getPosition3(pos,ret){
return getPositionOnBez(pos,p1,p2,cp1,cp2,ret);
}
const API = {
getPosOnQBez(pos,p1,cp1,p2,ret){
return getPositionOnBez(pos,p1,p2,cp1,undefined,ret);
},
getPosOnCBez(pos,p1,cp1,cp2,p2,ret){
return getPositionOnBez(pos,p1,p2,cp1,cp2,ret);
},
set bezQ(points){
setP2As(p1, points[0]);
setP2As(cp1, points[1]);
setP2As(p2, points[2]);
type = quadraticBez;
},
set bezC(points){
setP2As(p1, points[0]);
setP2As(cp1, points[1]);
setP2As(cp2, points[2]);
setP2As(p2, points[3]);
type = cubicBez;
},
isInside(center, testPoint, pointRadius){
drawLine(testPoint , center);
v1.x = (testPoint.x - center.x);
v1.y = (testPoint.y - center.y);
const pointDist = Math.sqrt(v1.x * v1.x + v1.y * v1.y)
const dir = -Math.atan2(v1.y,v1.x);
transformBez(center,dir);
if(type === cubicBez){
solveBezier3(rp1.y, rcp1.y, rcp2.y, rp2.y);
if (u < 0 || u > 1) { u = u1 }
if (u < 0 || u > 1) { u = u2 }
if (u < 0 || u > 1) { return }
getPosition3(u, v4);
}else{
solveBezier2(rp1.y, rcp1.y, rp2.y);
if (u < 0 || u > 1) { u = u1 }
if (u < 0 || u > 1) { return }
getPosition2(u, v4);
}
drawCircle(v4);
const dist = Math.sqrt((v4.x - center.x) ** 2 + (v4.y - center.y) ** 2);
const dist1 = Math.sqrt((v4.x - testPoint.x) ** 2 + (v4.y - testPoint.y) ** 2);
return dist1 < dist && dist > pointDist - pointRadius;
}
}
return API;
})();
const ctx = canvas.getContext("2d");
const m = {x : 0, y : 0};
document.addEventListener("mousemove",e=>{
var b = canvas.getBoundingClientRect();
m.x = e.pageX - b.left - scrollX - 2;
m.y = e.pageY - b.top - scrollY - 2;
});
function drawCircle(p,r = 5,col = "black"){
ctx.beginPath();
ctx.strokeStyle = col;
ctx.arc(p.x,p.y,r,0,Math.PI*2)
ctx.stroke();
}
function drawLine(p1,p2,r = 5,col = "black"){
ctx.beginPath();
ctx.strokeStyle = col;
ctx.lineTo(p1.x,p1.y);
ctx.lineTo(p2.x,p2.y);
ctx.stroke();
}
const w = 400;
const h = 400;
const diag = Math.sqrt(w * w + h * h);
// creates a 2D point
const P2 = (x=0, y= x === 0 ? 0 : x.y + (x = x.x, 0)) => ({x, y});
const setP2As = (p,pFrom) => (p.x = pFrom.x, p.y = pFrom.y, p);
// random int and double
const randI = (min, max = min + (min = 0)) => (Math.random()*(max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const theBlobSet = [];
const theBlob = [];
function createCubicBlob(segs){
const step = Math.PI / segs;
for(var i = 0; i < Math.PI * 2; i += step){
const dist = rand(diag * (1/6), diag * (1/5));
const ang = i + rand(-step * 0.2,step * 0.2);
const p = P2(
w / 2 + Math.cos(ang) * dist,
h / 2 + Math.sin(ang) * dist
);
theBlobSet.push(p);
theBlob.push(P2(p));
}
theBlobSet[theBlobSet.length -1] = theBlobSet[0];
theBlob[theBlobSet.length -1] = theBlob[0];
}
createCubicBlob(8);
function animateTheBlob(time){
for(var i = 0; i < theBlobSet.length-1; i++){
const ang = Math.sin(time + i) * 6;
theBlob[i].x = theBlobSet[i].x + Math.cos(ang) * diag * 0.04;
theBlob[i].y = theBlobSet[i].y + Math.sin(ang) * diag * 0.04;
}
}
function drawTheBlob(){
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.beginPath();
var i = 0;
ctx.moveTo(theBlob[i].x,theBlob[i++].y);
while(i < theBlob.length){
ctx.bezierCurveTo(
theBlob[i].x,theBlob[i++].y,
theBlob[i].x,theBlob[i++].y,
theBlob[i].x,theBlob[i++].y
);
}
ctx.stroke();
}
var center = P2(w/2,h/2);
function testBlob(){
var i = 0;
while(i < theBlob.length-3){
bezHelper.bezC = [theBlob[i++], theBlob[i++], theBlob[i++], theBlob[i]];
if(bezHelper.isInside(center,m,6)){
return true;
}
}
return false;
}
// main update function
function update(timer){
ctx.clearRect(0,0,w,h);
animateTheBlob(timer/1000)
drawTheBlob();
if(testBlob()){
ctx.strokeStyle = "red";
}else{
ctx.strokeStyle = "black";
}
ctx.beginPath();
ctx.arc(m.x,m.y,5,0,Math.PI*2)
ctx.stroke();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas" width = "400" height = "400"></canvas>
I had created an animation of bubbles in which al the circle will expand which are 50px neer to the mouse.
so here is the trick. you can just simply change mouseX,mouseY with your microbe's X and Y coordinates and 50 to the radius of your microbe.
And when my bubbles get bigger, so there you can destroy you air bubbles.
here is the link to my Animation.
https://ankittorenzo.github.io/canvasAnimations/Elements/Bubbles/
here is the link to my GitHub Code.
https://github.com/AnkitTorenzo/canvasAnimations/blob/master/Elements/Bubbles/js/main.js
Let Me Know if you have any problem.

html5 canvas triangle with rounded corners

I'm new to HTML5 Canvas and I'm trying to draw a triangle with rounded corners.
I have tried
ctx.lineJoin = "round";
ctx.lineWidth = 20;
but none of them are working.
Here's my code:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
var x = 18 / 2;
var y = 0;
var triangleWidth = 18;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
Could you help me?
Rounding corners
An invaluable function I use a lot is rounded polygon. It takes a set of 2D points that describe a polygon's vertices and adds arcs to round the corners.
The problem with rounding corners and keeping within the constraint of the polygons area is that you can not always fit a round corner that has a particular radius.
In these cases you can either ignore the corner and leave it as pointy or, you can reduce the rounding radius to fit the corner as best possible.
The following function will resize the corner rounding radius to fit the corner if the corner is too sharp and the lines from the corner not long enough to get the desired radius in.
Note the code has comments that refer to the Maths section below if you want to know what is going on.
roundedPoly(ctx, points, radius)
// ctx is the context to add the path to
// points is a array of points [{x :?, y: ?},...
// radius is the max rounding radius
// this creates a closed polygon.
// To draw you must call between
// ctx.beginPath();
// roundedPoly(ctx, points, radius);
// ctx.stroke();
// ctx.fill();
// as it only adds a path and does not render.
function roundedPoly(ctx, points, radiusAll) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut,radius;
// convert 2 points into vector form, polar form, and normalised
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
radius = radiusAll;
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
// for each point
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
//-----------------------------------------
// Part 1
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA < -1 ? -1 : sinA > 1 ? 1 : sinA);
//-----------------------------------------
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
if(p2.radius !== undefined){
radius = p2.radius;
}else{
radius = radiusAll;
}
//-----------------------------------------
// Part 2
halfAngle = angle / 2;
//-----------------------------------------
//-----------------------------------------
// Part 3
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
//-----------------------------------------
//-----------------------------------------
// Special part A
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
//-----------------------------------------
// Part 4
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
//-----------------------------------------
// Part 5
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
//-----------------------------------------
// Part 6
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
//-----------------------------------------
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
You may wish to add to each point a radius eg {x :10,y:10,radius:20} this will set the max radius for that point. A radius of zero will be no rounding.
The maths
The following illistration shows one of two possibilities, the angle to fit is less than 90deg, the other case (greater than 90) just has a few minor calculation differences (see code).
The corner is defined by the three points in red A, B, and C. The circle radius is r and we need to find the green points F the circle center and D and E which will define the start and end angles of the arc.
First we find the angle between the lines from B,A and B,C this is done by normalising the vectors for both lines and getting the cross product. (Commented as Part 1) We also find the angle of line BC to the line at 90deg to BA as this will help determine which side of the line to put the circle.
Now we have the angle between the lines, we know that half that angle defines the line that the center of the circle will sit F but we do not know how far that point is from B (Commented as Part 2)
There are two right triangles BDF and BEF which are identical. We have the angle at B and we know that the side DF and EF are equal to the radius of the circle r thus we can solve the triangle to get the distance to F from B
For convenience rather than calculate to F is solve for BD (Commented as Part 3) as I will move along the line BC by that distance (Commented as Part 4) then turn 90deg and move up to F (Commented as Part 5) This in the process gives the point D and moving along the line BA to E
We use points D and E and the circle center F (in their abstract form) to calculate the start and end angles of the arc. (done in the arc function part 6)
The rest of the code is concerned with the directions to move along and away from lines and which direction to sweep the arc.
The code section (special part A) uses the lengths of both lines BA and BC and compares them to the distance from BD if that distance is greater than half the line length we know the arc can not fit. I then solve the triangles to find the radius DF if the line BD is half the length of shortest line of BA and BC
Example use.
The snippet is a simple example of the above function in use. Click to add points to the canvas (needs a min of 3 points to create a polygon). You can drag points and see how the corner radius adapts to sharp corners or short lines. More info when snippet is running. To restart rerun the snippet. (there is a lot of extra code that can be ignored)
The corner radius is set to 30.
const ctx = canvas.getContext("2d");
const mouse = {
x: 0,
y: 0,
button: false,
drag: false,
dragStart: false,
dragEnd: false,
dragStartX: 0,
dragStartY: 0
}
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
const lb = mouse.button;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
if (lb !== mouse.button) {
if (mouse.button) {
mouse.drag = true;
mouse.dragStart = true;
mouse.dragStartX = mouse.x;
mouse.dragStartY = mouse.y;
} else {
mouse.drag = false;
mouse.dragEnd = true;
}
}
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const pointOnLine = {x:0,y:0};
function distFromLines(x,y,minDist){
var index = -1;
const v1 = {};
const v2 = {};
const v3 = {};
const point = P2(x,y);
eachOf(polygon,(p,i)=>{
const p1 = polygon[(i + 1) % polygon.length];
v1.x = p1.x - p.x;
v1.y = p1.y - p.y;
v2.x = point.x - p.x;
v2.y = point.y - p.y;
const u = (v2.x * v1.x + v2.y * v1.y)/(v1.y * v1.y + v1.x * v1.x);
if(u >= 0 && u <= 1){
v3.x = p.x + v1.x * u;
v3.y = p.y + v1.y * u;
dist = Math.hypot(v3.y - point.y, v3.x - point.x);
if(dist < minDist){
minDist = dist;
index = i;
pointOnLine.x = v3.x;
pointOnLine.y = v3.y;
}
}
})
return index;
}
function roundedPoly(ctx, points, radius) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut;
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA); // warning you should guard by clampling
// to -1 to 1. See function roundedPoly in answer or
// Math.asin(Math.max(-1, Math.min(1, sinA)))
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
halfAngle = angle / 2;
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
const eachOf = (array, callback) => { var i = 0; while (i < array.length && callback(array[i], i++) !== true); };
const P2 = (x = 0, y = 0) => ({x, y});
const polygon = [];
function findClosestPointIndex(x, y, minDist) {
var index = -1;
eachOf(polygon, (p, i) => {
const dist = Math.hypot(x - p.x, y - p.y);
if (dist < minDist) {
minDist = dist;
index = i;
}
});
return index;
}
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var dragPoint;
var globalTime;
var closestIndex = -1;
var closestLineIndex = -1;
var cursor = "default";
const lineDist = 10;
const pointDist = 20;
var toolTip = "";
// main update function
function update(timer) {
globalTime = timer;
cursor = "crosshair";
toolTip = "";
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth - 4 || h !== innerHeight - 4) {
cw = (w = canvas.width = innerWidth - 4) / 2;
ch = (h = canvas.height = innerHeight - 4) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
if (mouse.drag) {
if (mouse.dragStart) {
mouse.dragStart = false;
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex === -1){
polygon.push(dragPoint = P2(mouse.x, mouse.y));
}else{
polygon.splice(closestLineIndex+1,0,dragPoint = P2(mouse.x, mouse.y));
}
}else{
dragPoint = polygon[closestIndex];
}
}
dragPoint.x = mouse.x;
dragPoint.y = mouse.y
cursor = "none";
}else{
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex > -1){
toolTip = "Click to cut line and/or drag to move.";
}
}else{
toolTip = "Click drag to move point.";
closestLineIndex = -1;
}
}
ctx.lineWidth = 4;
ctx.fillStyle = "#09F";
ctx.strokeStyle = "#000";
ctx.beginPath();
roundedPoly(ctx, polygon, 30);
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.lineWidth = 0.5;
eachOf(polygon, p => ctx.lineTo(p.x,p.y) );
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = "orange";
ctx.lineWidth = 1;
eachOf(polygon, p => ctx.strokeRect(p.x-2,p.y-2,4,4) );
if(closestIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
dragPoint = polygon[closestIndex];
ctx.strokeRect(dragPoint.x-4,dragPoint.y-4,8,8);
cursor = "move";
}else if(closestLineIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
var p = polygon[closestLineIndex];
var p1 = polygon[(closestLineIndex + 1) % polygon.length];
ctx.beginPath();
ctx.lineTo(p.x,p.y);
ctx.lineTo(p1.x,p1.y);
ctx.stroke();
ctx.strokeRect(pointOnLine.x-4,pointOnLine.y-4,8,8);
cursor = "pointer";
}
if(toolTip === "" && polygon.length < 3){
toolTip = "Click to add a corners of a polygon.";
}
canvas.title = toolTip;
canvas.style.cursor = cursor;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
border: 2px solid black;
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
I started by using #Blindman67 's answer, which works pretty well for basic static shapes.
I ran into the problem that when using the arc approach, having two points right next to each other is very different than having just one point. With two points next to each other, it won't be rounded, even if that is what your eye would expect. This is extra jarring if you are animating the polygon points.
I fixed this by using Bezier curves instead. IMO this is conceptually a little cleaner as well. I just make each corner with a quadratic curve where the control point is where the original corner was. This way, having two points in the same spot is virtually the same as only having one point.
I haven't compared performance but seems like canvas is pretty good at drawing Beziers.
As with #Blindman67 's answer, this doesn't actually draw anything so you will need to call ctx.beginPath() before and ctx.stroke() after.
/**
* Draws a polygon with rounded corners
* #param {CanvasRenderingContext2D} ctx The canvas context
* #param {Array} points A list of `{x, y}` points
* #radius {number} how much to round the corners
*/
function myRoundPolly(ctx, points, radius) {
const distance = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
const lerp = (a, b, x) => a + (b - a) * x
const lerp2D = (p1, p2, t) => ({
x: lerp(p1.x, p2.x, t),
y: lerp(p1.y, p2.y, t)
})
const numPoints = points.length
let corners = []
for (let i = 0; i < numPoints; i++) {
let lastPoint = points[i]
let thisPoint = points[(i + 1) % numPoints]
let nextPoint = points[(i + 2) % numPoints]
let lastEdgeLength = distance(lastPoint, thisPoint)
let lastOffsetDistance = Math.min(lastEdgeLength / 2, radius)
let start = lerp2D(
thisPoint,
lastPoint,
lastOffsetDistance / lastEdgeLength
)
let nextEdgeLength = distance(nextPoint, thisPoint)
let nextOffsetDistance = Math.min(nextEdgeLength / 2, radius)
let end = lerp2D(
thisPoint,
nextPoint,
nextOffsetDistance / nextEdgeLength
)
corners.push([start, thisPoint, end])
}
ctx.moveTo(corners[0][0].x, corners[0][0].y)
for (let [start, ctrl, end] of corners) {
ctx.lineTo(start.x, start.y)
ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y)
}
ctx.closePath()
}
Styles for joining of lines such as ctx.lineJoin="round" apply to the stroke operation on paths - which is when their width, color, pattern, dash/dotted and similar line style attributes are taken into account.
Line styles do not apply to filling the interior of a path.
So to affect line styles a stroke operation is needed. In the following adaptation of posted code, I've translated canvas output to see the result without cropping, and stroked the triangle's path but not the rectangles below it:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
ctx.translate( 18, 12);
var x = 18 / 2;
var y = 0;
var triangleWidth = 48;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
// stroke the triangle path.
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.strokeStyle = "orange";
ctx.stroke();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>

Isometric simple sorting with xdim and ydim

I have a simple isometric sorting system with this function (code is in Typescript/Javascript) :
public Sort(a: PIXI.Sprite, b: PIXI.Sprite) {
return ((a.IsoZ - b.IsoZ) == 0 ? (a.TileZ - b.TileZ == 0 ? (a.Tile2Z ? (a.Tile2Z < b.Tile2Z ? -1 : (a.Tile2Z > b.Tile2Z ? 1 : 0)) : 0) : a.TileZ - b.TileZ) : (a.IsoZ - b.IsoZ));
}
It depends on three parameters:
IsoZ: the first sorting variables, used to sort tiles
TileZ: the tile
sorting variable, used if a.IsoZ == b.IsoZ
Tile2Z: used if a.TileZ == b.TileZ
Here's how IsoZ is basically calculated for most objects:
this.Position is an array of x and y coordinates
this.Position[0] + this.Position[1] + 1000;
now I want to support object x and y dimensions, so how can I implement something like this in this expression?
x and y dimensions values are for example (2, 2) for a cube or (2, 4) for a cuboid
this.Position[0] + this.Position[1] + 1000 // + x dimension + y dimension ???
Isometric visual occlusion sort (depth sort)
Defining depth:
Higher depths values are closer to the screen. Unlike 3D perspective projection where depth is distance from the front plane, this answer uses depth as distance towards the screen.
Iso projection
If you have a iso projection
const P2 = (x = 0,y = 0) => ({x, y});
const isoProjMat = {
xAxis : P2(1 , 0.5),
yAxis : P2(-0.5, 1 ),
zAxis : P2(0 , -1 ),
}
That takes a 3d point and projects to screen space
const P3 = (x = 0, y = 0, z = 0) => ({x, y, z});
isoProjMat.project = function (p, retP = P2()) { // p is 3D point
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y;
return retP;
}
You can add the depth of a point as the z value of the 2D projected point. You need to add a transform axis for the depth.
isoProjMat.depth = P3(0.5,1, 1 );
For x move closer by half its size, y * 1 and z * 1.
The modified project now adds z to the returned point.
isoProjMat.project = function (p, retP = P3()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y;
retP.z = p.x * this.depth.x + p.y * this.depth.y + p.z * this.depth.z;
return retP;
}
Thus for a set of points in 3D space projected to 2D iso screen space you sort on the z
const points = mySetOfPoints(); // what ever your points come from
const projected = points.map(p => isoProjMat.project(p));
projected.sort((a,b) => a.z - b.z);
All good for points but for sprites which occupy a 3D volume this does not work.
What you need to do is add a bounding volume ie a square. If your projection is static then we can simplify the bounding volume to the nearest point. For the box that is the vertex at the top bottom right eg sprite at (0,0,0) has a size (10,10,20) the nearest point in 3d is at (10,10,20).
I can not work your sort out as there is not enough info in the question but I am guessing sprite.Iso is the base origin of the sprite and sprite.Tile & Tile2 represent bounding box.
Thus to get the nearest point
const depthProj = P3(0.5,1, 1 ); // depth projection matrix
// get the depth of each sprite adding the property depth
sprites.forEach(spr => {
const p = {
x : spr.IsoX + Math.max(spr.TileX,spr.Tile2X),
y : spr.IsoY + Math.max(spr.TileY,spr.Tile2Y),
z : spr.IsoZ + Math.max(spr.TileZ,spr.Tile2Z)
};
spr.depth = p.x * depthProj.x + p.y * depthProj.y + p.z * depthProj.z;
})
sprites.sort((a,b) => a.depth - b.depth);
Then render from index 0 up.
An example.
The following is not fully applicable as it sorts by polygons and uses the polygons mean depth rather than its max depth (really should use max but cant be bothered ATM)
I add it only to show how the above code for the isoProjMat is used. It draws stacked boxes from pixel alpha and color rendered on a canvas.
Click rendered result to switch projections from bi-morphic to tri-morphic (as you did not specify the type of projection you used this shows how the depth transform changes between two types of parallel projection.
const ctx = canvas.getContext("2d");
var count = 0;
var firstRun = 0;
function doIt(){
// 3d 2d points
const P3 = (x=0, y=0, z=0) => ({x,y,z});
const P2 = (x=0, y=0) => ({x, y});
// isomorphic projection matrix
const isoProjMat = {
xAxis : count ? P2(1 , 0.5) : P2(1 , 0.5) ,
yAxis : count ? P2(-0.5, 1) : P2(-1 , 0.5) ,
zAxis : count ? P2(0 , -1) : P2(0 , -1) ,
depth : count ? P3(0.5,1, 1) : P3(0.5,0.5,1) , // projections have z as depth
origin : P2(), // (0,0) default 2D point
project (p, retP = P3()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
retP.z = p.x * this.depth.x + p.y * this.depth.y + p.z * this.depth.z;
return retP;
}
}
// isomorphic mesh shape as vertices and polygons
const isoMesh = (()=>{
const polygon = {
inds : null,
depth : 0,
fillStyle : "#888",
lineWidth : 0.5,
strokeStyle : "#000",
setStyle(ctx) {
ctx.fillStyle = this.fillStyle;
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.strokeStyle;
},
}
const isoShape = {
verts : null,
pVerts : null, // projected verts
polys : null,
addVert(p3 = P3()) { this.verts.push(p3); return p3 },
addPoly(poly = isoShape.createPoly()) { this.polys.push(poly); return poly },
createPoly(options = {}) { return Object.assign({}, polygon, {inds : []}, options) },
render(ctx,mat = isoProjMat) {
var i,j,d;
const pv = this.pVerts === null ? this.pVerts = [] : this.pVerts;
const v = this.verts;
const ps = this.polys;
for(i = 0; i < v.length; i += 1){ pv[i] = mat.project(v[i], pv[i]) }
for(i = 0; i < ps.length; i += 1) {
const p = ps[i];
j = 0; d = 0;
while(j < p.inds.length) { d += pv[p.inds[j++]].z }
p.depth = d / p.inds.length;
}
ps.sort((a,b)=>a.depth - b.depth);
for(i = 0; i < ps.length; i += 1) {
const p = ps[i];
p.setStyle(ctx);
ctx.beginPath();
j = 0;
while(j < p.inds.length) { ctx.lineTo(pv[p.inds[j]].x, pv[p.inds[j++]].y) }
if (p.fillStyle !== "") { ctx.fill() }
if (p.strokeStyle !== "" && p.lineWidth !== 0) {ctx.closePath(); ctx.stroke() }
}
}
}
return () => Object.assign({},isoShape,{verts : [], polys : []});
})();
// Lazy coding I am using Point3 (P3) to hold RGB values
function createBoxMesh(box = isoMesh(), pos = P3(), size = P3(10,10,10), rgb = P3(128,128,128)){ // x,y,z are sizes in those directions
const PA3 = (x,y,z) => P3(x + pos.x, y + pos.y, z + pos.z);
const RGB = (s) => `rgb(${(rgb.x * s) | 0},${(rgb.y * s) | 0},${(rgb.z * s) | 0})`;
const indA = (inds) => inds.map(ind => ind + i);
const i = box.verts.length; // get top vert index
if(typeof size === "number") { size = P3(size,size,size) }
const x = size.x / 2;
const y = size.y / 2;
const z = size.z;
box.addVert(PA3(-x,-y, 0)); // ind 0
box.addVert(PA3( x,-y, 0));
box.addVert(PA3( x, y, 0));
box.addVert(PA3(-x, y, 0));
box.addVert(PA3(-x,-y, z)); // ind 4
box.addVert(PA3( x,-y, z));
box.addVert(PA3( x, y, z));
box.addVert(PA3(-x, y, z));
// box.addPoly(box.createPoly({ inds : indA([0,1,5,4]), fillStyle : RGB(0.5) }));
box.addPoly(box.createPoly({ inds : indA([1,2,6,5]), fillStyle : RGB(0.7) }));
box.addPoly(box.createPoly({ inds : indA([2,3,7,6]), fillStyle : RGB(1) }));
// box.addPoly(box.createPoly({ inds : indA([3,0,4,7]), fillStyle : RGB(0.8) }));
box.addPoly(box.createPoly({ inds : indA([4,5,6,7]), fillStyle : RGB(1.5) }));
return box;
}
function createDrawable(w,h){
const c = document.createElement("canvas");
c.width = w;
c.height = h;
c.ctx = c.getContext("2d");
return c;
}
const map = createDrawable(40,30);
map.ctx.font = "20px arial";
map.ctx.textAlign = "center";
map.ctx.textBaseline = "middle";
map.ctx.fillStyle = "rgba(0,128,0,0.5)";
map.ctx.strokeStyle = "rgba(255,0,0,0.5)";
map.ctx.lineWidth = 2;
map.ctx.fillRect(1,1,map.width - 2, map.height - 2);
map.ctx.strokeRect(1,1,map.width - 2, map.height - 2);
map.ctx.fillStyle = "#AAA";
map.ctx.strokeStyle = "rgba(255,128,0,0.5)";
map.ctx.strokeText("text",map.width / 2, map.height / 2);
map.ctx.fillText("text",map.width / 2, map.height / 2);
var dat = map.ctx.getImageData(0, 0, map.width , map.height).data;
ctx.setTransform(1,0,0,1,0,0);
// get total projection area and size canvas so that the iso projection fits
const boxSize = P3(10,10,5);
const topLeft = isoProjMat.project(P3(0,0,10 * boxSize.z));
const botRight = isoProjMat.project(P3(map.width * boxSize.x,map.height * boxSize.y,0));
const topRight = isoProjMat.project(P3(map.width * boxSize.x,0,0));
const botLeft = isoProjMat.project(P3(0,map.height * boxSize.y,0));
canvas.width = ((topRight.x - botLeft.x) + 10)|0;
canvas.height = ((botRight.y - topLeft.y) + 10)|0;
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.font = "32px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Rendering will take a moment.",Math.min(innerWidth,canvas.width)/2,Math.min(innerHeight,canvas.height)/2)
setTimeout(function(){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.setTransform(1,0,0,1,-botLeft.x+10,-topLeft.y+10);
const alphaThresh = 100;
const boxes = isoMesh();
for(var y = 0; y < map.height; y ++){
for(var x = 0; x < map.width; x ++){
const ind = (x + y * map.width) * 4;
if(dat[ind + 3] > alphaThresh){
const h = (((dat[ind + 3]-alphaThresh)/(255-alphaThresh)) * 10) | 0;
for(var z = 0; z < h; z++){
createBoxMesh(
boxes,
P3(x * boxSize.x,y * boxSize.y, z * boxSize.z),
boxSize,
P3(dat[ind],dat[ind+1],dat[ind+2])
);
}
}
}
}
boxes.render(ctx);
if(firstRun === 0){
firstRun = 1;
ctx.setTransform(1,0,0,1,0,0);
ctx.font = "24px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "black";
ctx.fillText("Bimorphic projection. Click for Trimorphic projection..",canvas.width/2,30)
canvas.onclick =()=>{
count += 1;
count %= 2;
doIt();
};
}
},0);
};
doIt();
canvas {
border : 2px solid black;
}
<canvas id="canvas"></canvas>

Circle line segment collision

I need to detect collision circle with any line. I have array with verticles of polygon (x, y) and draw this polygon in loop. For detection I use algorithm, which calculate triangle height. Then I check if this height < 0, then circle collided with line.
The picture, that describe this method:
But I have unexpected result. My circle collide with transparent line (what?). I can't explain how it happens.
Demo at jsfiddle: https://jsfiddle.net/f458rdz6/1/
Function, which check the collisions and response it:
var p = polygonPoints;
for (var i = 0, n = p.length; i < n; i++) {
var start = i;
var end = (i + 1) % n;
var x0 = p[start].x;
var y0 = p[start].y;
var x1 = p[end].x;
var y1 = p[end].y;
// detection collision
var dx = x1 - x0;
var dy = y1 - y0;
var len = Math.sqrt(dx * dx + dy * dy);
var dist = (dx * (this.y - y0) - dy * (this.x - x0)) / len;
if (dist < this.radius) {
continue;
}
// calculate reflection, because collided
var wallAngle = Math.atan2(dy, dx);
var wallNormalX = Math.sin(wallAngle);
var wallNormalY = -Math.cos(wallAngle);
var d = 2 * (this.velocityX * wallNormalX + this.velocityY * wallNormalY);
this.x -= d * wallNormalX;
this.y -= d * wallNormalY;
}
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var polygonPoints = [
{
x: 240,
y: 130
},
{
x: 140,
y: 100
},
{
x: 180,
y: 250
},
{
x: 320,
y: 280
},
{
x: 400,
y: 50
}
];
var game = {
ball: new Ball()
};
function Ball() {
this.x = canvas.width / 2;
this.y = canvas.height - 100;
this.oldX = this.x - 1;
this.oldY = this.y + 1;
this.velocityX = 0;
this.velocityY = 0;
this.radius = 8;
};
Ball.prototype.draw = function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = '#0095DD';
ctx.fill();
ctx.closePath();
};
Ball.prototype.update = function() {
var x = this.x;
var y = this.y;
this.velocityX = this.x - this.oldX;
this.velocityY = this.y - this.oldY;
this.x += this.velocityX;
this.y += this.velocityY;
this.oldX = x;
this.oldY = y;
};
Ball.prototype.collision = function() {
var p = polygonPoints;
for (var i = 0, n = p.length; i < n; i++) {
var start = i;
var end = (i + 1) % n;
var x0 = p[start].x;
var y0 = p[start].y;
var x1 = p[end].x;
var y1 = p[end].y;
// detection collision
var dx = x1 - x0;
var dy = y1 - y0;
var len = Math.sqrt(dx * dx + dy * dy);
var dist = (dx * (this.y - y0) - dy * (this.x - x0)) / len;
if (dist < this.radius) {
continue;
}
// calculate reflection, because collided
var wallAngle = Math.atan2(dy, dx);
var wallNormalX = Math.sin(wallAngle);
var wallNormalY = -Math.cos(wallAngle);
var d = 2 * (this.velocityX * wallNormalX + this.velocityY * wallNormalY);
this.x -= d * wallNormalX;
this.y -= d * wallNormalY;
}
};
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI*2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawPolygon() {
ctx.beginPath();
ctx.strokeStyle = '#333';
ctx.moveTo(polygonPoints[0].x, polygonPoints[0].y);
for (var i = 1, n = polygonPoints.length; i < n; i++) {
ctx.lineTo(polygonPoints[i].x, polygonPoints[i].y);
}
ctx.lineTo(polygonPoints[0].x, polygonPoints[0].y);
ctx.stroke();
ctx.closePath();
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawPolygon();
game.ball.draw();
game.ball.update();
game.ball.collision();
window.requestAnimationFrame(render);
}
render();
canvas {
border: 1px solid #333;
}
<canvas id="myCanvas" width="480" height="320"></canvas>
What the problem? Maybe I need use other method for detect collision? I tried to use this one, but if my circle has high speed this method not working.
Thank you.
Circle line segment intercept
UPDATE
This answer includes line line intercept, moving a line along its normal, distance point (circle) to line, and circle line intercept.
The circle is
var circle = {
radius : 500,
center : point(1000,1000),
}
The line segment is
var line = {
p1 : point(500,500),
p2 : point(2000,1000),
}
A point is
var point = {
x : 100,
y : 100,
}
Thus the function to find the intercept of a line segment width a circle
The function returns an array of up to two point on the line segment. If no points found returns an empty array.
function inteceptCircleLineSeg(circle, line){
var a, b, c, d, u1, u2, ret, retP1, retP2, v1, v2;
v1 = {};
v2 = {};
v1.x = line.p2.x - line.p1.x;
v1.y = line.p2.y - line.p1.y;
v2.x = line.p1.x - circle.center.x;
v2.y = line.p1.y - circle.center.y;
b = (v1.x * v2.x + v1.y * v2.y);
c = 2 * (v1.x * v1.x + v1.y * v1.y);
b *= -2;
d = Math.sqrt(b * b - 2 * c * (v2.x * v2.x + v2.y * v2.y - circle.radius * circle.radius));
if(isNaN(d)){ // no intercept
return [];
}
u1 = (b - d) / c; // these represent the unit distance of point one and two on the line
u2 = (b + d) / c;
retP1 = {}; // return points
retP2 = {}
ret = []; // return array
if(u1 <= 1 && u1 >= 0){ // add point if on the line segment
retP1.x = line.p1.x + v1.x * u1;
retP1.y = line.p1.y + v1.y * u1;
ret[0] = retP1;
}
if(u2 <= 1 && u2 >= 0){ // second add point if on the line segment
retP2.x = line.p1.x + v1.x * u2;
retP2.y = line.p1.y + v1.y * u2;
ret[ret.length] = retP2;
}
return ret;
}
UPDATE
Line line intercept.
Returns a point if found else returns undefined.
function interceptLines(line,line1){
var v1, v2, c, u;
v1 = {};
v2 = {};
v3 = {};
v1.x = line.p2.x - line.p1.x; // vector of line
v1.y = line.p2.y - line.p1.y;
v2.x = line1.p2.x - line1.p1.x; //vector of line2
v2.y = line1.p2.y - line1.p1.y;
var c = v1.x * v2.y - v1.y * v2.x; // cross of the two vectors
if(c !== 0){
v3.x = line.p1.x - line1.p1.x;
v3.y = line.p1.y - line1.p1.y;
u = (v2.x * v3.y - v2.y * v3.x) / c; // unit distance of intercept point on this line
return {x : line.p1.x + v1.x * u, y : line.p1.y + v1.y * u};
}
return undefined;
}
Lift Line
Move line along its normal
function liftLine(line,dist){
var v1,l
v1 = {};
v1.x = line.p2.x - line.p1.x; // convert line to vector
v1.y = line.p2.y - line.p1.y;
l = Math.sqrt(v1.x * v1.x + v1.y * v1.y); // get length;
v1.x /= l; // Assuming you never pass zero length lines
v1.y /= l;
v1.x *= dist; // set the length
v1.y *= dist;
// move the line along its normal the required distance
line.p1.x -= v1.y;
line.p1.y += v1.x;
line.p2.x -= v1.y;
line.p2.y += v1.x;
return line; // if needed
}
Distance circle (or point) to a line segment
Returns the closest distance to the line segment. It is just the circle center that I am using. So you can replace circle with a point
function circleDistFromLineSeg(circle,line){
var v1, v2, v3, u;
v1 = {};
v2 = {};
v3 = {};
v1.x = line.p2.x - line.p1.x;
v1.y = line.p2.y - line.p1.y;
v2.x = circle.center.x - line.p1.x;
v2.y = circle.center.y - line.p1.y;
u = (v2.x * v1.x + v2.y * v1.y) / (v1.y * v1.y + v1.x * v1.x); // unit dist of point on line
if(u >= 0 && u <= 1){
v3.x = (v1.x * u + line.p1.x) - circle.center.x;
v3.y = (v1.y * u + line.p1.y) - circle.center.y;
v3.x *= v3.x;
v3.y *= v3.y;
return Math.sqrt(v3.y + v3.x); // return distance from line
}
// get distance from end points
v3.x = circle.center.x - line.p2.x;
v3.y = circle.center.y - line.p2.y;
v3.x *= v3.x; // square vectors
v3.y *= v3.y;
v2.x *= v2.x;
v2.y *= v2.y;
return Math.min(Math.sqrt(v2.y + v2.x), Math.sqrt(v3.y + v3.x)); // return smaller of two distances as the result
}

Split one quadratic bezier curve into two

So I have an imaginary circle divided into multiple parts (I use 8 for simplicity, but in the end, I would like to divide it to 16 or 32 parts).
Then I have N number of quadratic bezier curves, that is between 2 nearest segments. It may rest upon the circle or further from the center, but not nearer than the circle.
I know how to find, what in witch line I should look for intersection in, but I do not know how to split it into two parts... I know, that if I looked for intersection of the line and curve I should get the point that the previous curve should end and the next should start, and that by derivation I may be able to get the vector, but I do not know how to do it.
Example image where I have only 8 parts for easier problem solving.
The point is, to make "progress" bar using bezier curves. Side note: The curves will change every frame, as they are part of music visualization.
If there is a better way to spit color a curve, I am all for it!
Spliting cubic and quadratic Beziers
Splitting a bezier is relatively easy. As there is already an answer I will just copy the functions needed to split a single bezier, cubic or quadratic at a position along its path range from 0 to 1. The function Bezier.splitAt takes a position (0 to 1) and depending on start = true returns the from 0 to position or the if start = false returns the bezier from position to 1. It will handle both 2nd order (quadratic) and 3rd order (cubic) Beziers
Example usage
var bezier = createBezierCubic( 146, 146, 134, 118, 184, 103, 217, 91 );
// split in two
var startingHalf = bezier.splitAt(0.5, true);
var endingHalf = bezier.splitAt(0.5, false);
// split into four.
var quart1 = startingHalf.splitAt(0.5, true)
var quart2 = startingHalf.splitAt(0.5, false)
var quart3 = endingHalf.splitAt(0.5, true)
var quart4 = endingHalf.splitAt(0.5, false)
// getting a segment
var startFrom = 0.3;
var endAt = 0.8;
var section = bezier.splitAt(startFrom, false).splitAt((endAt - startFrom) / (1 - startFrom), true);
The bezier is made up of a start and end point p1, p2 and one or two control points cp1, cp2. If the bezier is 2nd order then cp2 is undefined. The points are Vec and take the from Vec.x, Vec.y
To render a 2nd order
ctx.moveTo(bezier.p1.x, bezier.p1.y);
ctx.quadraticCurveTo(bezier.cp1.x, bezier.cp1.y, bezier.p2.x, bezier.p2.y);
To render the 3rd order
ctx.moveTo(bezier.p1.x, bezier.p1.y);
ctx.bezierCurveTo(bezier.cp1.x, bezier.cp1.y, bezier.cp2.x, bezier.cp2.y, bezier.p2.x, bezier.p2.y);
The code with dependencies.
As you are all programmers see the code for more info in usage. Warning there could be typos as this has been pulled from a more extensive geometry interface.
var geom = (function(){
function Vec(x,y){ // creates a vector
if(x === undefined){
x = 1;
y = 0;
}
this.x = x;
this.y = y;
}
Vec.prototype.set = function(x,y){
this.x = x;
this.y = y;
return this;
};
// closure vars to stop constant GC
var v1 = Vec();
var v2 = Vec();
var v3 = Vec();
var v4 = Vec();
var v5 = Vec();
const BEZIER_TYPES = {
cubic : "cubic",
quadratic : "quadratic",
};
// creates a bezier p1 and p2 are the end points as vectors.
// if p1 is a string then returns a empty bezier object.
// with the type as quadratic (default) or cubic
// cp1, [cp2] are the control points. cp2 is optional and if omitted will create a quadratic
function Bezier(p1,p2,cp1,cp2){
if(typeof p1 === 'string'){
this.p1 = new Vec();
this.p2 = new Vec();
this.cp1 = new Vec();
if(p1 === BEZIER_TYPES.cubic){
this.cp2 = new Vec();
}
}else{
this.p1 = p1 === undefined ? new Vec() : p1;
this.p2 = p2 === undefined ? new Vec() : p2;
this.cp1 = cp1 === undefined ? new Vec() : cp1;
this.cp2 = cp2;
}
}
Bezier.prototype.type = function(){
if(this.cp2 === undefined){
return BEZIER_TYPES.quadratic;
}
return BEZIER_TYPES.cubic;
}
Bezier.prototype.splitAt = function(position,start){ // 0 <= position <= 1 where to split. Start if true returns 0 to position and else from position to 1
var retBezier,c;
if(this.cp2 !== undefined){ retBezier = new Bezier(BEZIER_TYPES.cubic); }
else{ retBezier = new Bezier(BEZIER_TYPES.quadratic); }
v1.x = this.p1.x;
v1.y = this.p1.y;
c = Math.max(0, Math.min(1, position)); // clamp for safe use in Stack Overflow answer
if(start === true){
retBezier.p1.x = this.p1.x;
retBezier.p1.y = this.p1.y;
}else{
retBezier.p2.x = this.p2.x;
retBezier.p2.y = this.p2.y;
}
if(this.cp2 === undefined){ // returns a quadratic
v2.x = this.cp1.x;
v2.y = this.cp1.y;
if(start){
retBezier.cp1.x = (v1.x += (v2.x - v1.x) * c);
retBezier.cp1.y = (v1.y += (v2.y - v1.y) * c);
v2.x += (this.p2.x - v2.x) * c;
v2.y += (this.p2.y - v2.y) * c;
retBezier.p2.x = v1.x + (v2.x - v1.x) * c;
retBezier.p2.y = v1.y + (v2.y - v1.y) * c;
retBezier.cp2 = undefined;
}else{
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
retBezier.cp1.x = (v2.x += (this.p2.x - v2.x) * c);
retBezier.cp1.y = (v2.y += (this.p2.y - v2.y) * c);
retBezier.p1.x = v1.x + (v2.x - v1.x) * c;
retBezier.p1.y = v1.y + (v2.y - v1.y) * c;
retBezier.cp2 = undefined;
}
return retBezier;
}
v2.x = this.cp1.x;
v3.x = this.cp2.x;
v2.y = this.cp1.y;
v3.y = this.cp2.y;
if(start){
retBezier.cp1.x = (v1.x += (v2.x - v1.x) * c);
retBezier.cp1.y = (v1.y += (v2.y - v1.y) * c);
v2.x += (v3.x - v2.x) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
v3.x += (this.p2.x - v3.x) * c;
v3.y += (this.p2.y - v3.y) * c;
retBezier.cp2.x = (v1.x += (v2.x - v1.x) * c);
retBezier.cp2.y = (v1.y += (v2.y - v1.y) * c);
retBezier.p2.y = v1.y + (v2.y - v1.y) * c;
retBezier.p2.x = v1.x + (v2.x - v1.x) * c;
}else{
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
retBezier.cp2.x = (v3.x += (this.p2.x - v3.x) * c);
retBezier.cp2.y = (v3.y += (this.p2.y - v3.y) * c);
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
retBezier.cp1.x = (v2.x += (v3.x - v2.x) * c);
retBezier.cp1.y = (v2.y += (v3.y - v2.y) * c);
retBezier.p1.x = v1.x + (v2.x - v1.x) * c;
retBezier.p1.y = v1.y + (v2.y - v1.y) * c;
}
return retBezier;
}
return {
Vec : Vec,
Bezier : Bezier,
bezierTypes : BEZIER_TYPES,
};
})();
// helper function
// Returns second order quadratic from points in the same order as most rendering api take then
// The second two coordinates x1,y1 are the control points
function createBezierQuadratic(x, y, x1, y1, x2, y2){
var b = new geom.Bezier(geom.bezierTypes.quadratic);
b.p1.set(x, y);
b.p2.set(x2, y2);
b.cp1.set(x1, y1);
return b;
}
// Returns third order cubic from points in the same order as most rendering api take then
// The coordinates x1, y1 and x2, y2 are the control points
function createBezierCubic(x, y, x1, y1, x2, y2, x3, y3){
var b = new geom.Bezier(geom.bezierTypes.cubic);
b.p1.set(x, y);
b.p2.set(x3, y3);
b.cp1.set(x1, y1);
b.cp2.set(x2, y2);
return b;
}
[Edit]
The algo for getting the length is still not working, it seems I forgot to calculate the last path, if someone wants to point me to the solution that would be very nice since I don't have time right now. (Otherwise, I'll try to find it in the weekend...)
Since you don't need support for older IE (<=11), one easy way is to use the setLineDash() method.
This will allow you to only draw your path once, and to only have to get the full length of your path.
To do so, I use a js implementation of this algo made by tunght13488. There may be better implementations of it.
var ctx = c.getContext('2d');
var percent = 90;
var length = 0;
// all our quadraticCurves points
var curves = [
[146, 146, 134, 118, 184, 103],
[217, 91, 269, 81, 271, 107],
[263, 155, 381, 158, 323, 173],
[279, 182, 314, 225, 281, 223],
[246, 219, 247, 274, 207, 236],
[177, 245, 133, 248, 137, 211],
[123, 238, 10, 145, 130, 150]
];
// get the full length of our spline
curves.forEach(function(c) {
length += quadraticBezierLength.apply(null, c);
});
// that's still not it...
length += quadraticBezierLength.apply(null,curves[curves.length-1]);
var anim = function() {
var offset = (percent / 100) * length;
ctx.clearRect(0, 0, c.width, c.height);
ctx.beginPath();
ctx.moveTo(133, 150);
// draw our splines
curves.forEach(function(c) {
ctx.bezierCurveTo.apply(ctx, c);
})
ctx.closePath();
// the non completed part
ctx.strokeStyle = "gray";
// this will make the part from 0 to offset non drawn
ctx.setLineDash([0, offset, length]);
ctx.stroke();
// the completed part
ctx.setLineDash([offset, length]);
ctx.strokeStyle = "blue";
ctx.stroke();
percent = (percent + .25) % 100;
requestAnimationFrame(anim);
}
// modified from https://gist.github.com/tunght13488/6744e77c242cc7a94859
function Point(x, y) {
this.x = x;
this.y = y;
}
function quadraticBezierLength(p0x, p0y, p1x, p1y, p2x, p2y) {
var a = new Point(
p0x - 2 * p1x + p2x,
p0y - 2 * p1y + p2y
);
var b = new Point(
2 * p1x - 2 * p0x,
2 * p1y - 2 * p0y
);
var A = 4 * (a.x * a.x + a.y * a.y);
var B = 4 * (a.x * b.x + a.y * b.y);
var C = b.x * b.x + b.y * b.y;
var Sabc = 2 * Math.sqrt(A + B + C);
var A_2 = Math.sqrt(A);
var A_32 = 2 * A * A_2;
var C_2 = 2 * Math.sqrt(C);
var BA = B / A_2;
return (A_32 * Sabc + A_2 * B * (Sabc - C_2) + (4 * C * A - B * B) * Math.log((2 * A_2 + BA + Sabc) / (BA + C_2))) / (4 * A_32);
};
anim();
<canvas width="500" height="500" id="c"></canvas>
To anyone still landing on this page, take a look at Bezier.js (https://github.com/Pomax/bezierjs), especially at the API: https://pomax.github.io/bezierjs/
You can extract a quadratic Bezier curve between t = 0.25 and t = 0.75 like so:
var curve = new Bezier(150,40 , 80,30 , 105,150);
var segment_curve = curve.split(0.25, 0.75);
context.moveTo(segment_curve.points[0].x, segment_curve.points[0].y);
context.quadraticCurveTo(segment_curve.points[1].x, segment_curve.points[1].y, segment_curve.points[2].x, segment_curve.points[2].y);
context.stroke();

Categories

Resources