How to curve a unit mesh between 2 unit vectors - javascript

I'm trying to draw 2 unit vectors and then draw an arc between them. I'm not looking for any solution, rather I want to know why my specific solution is not working.
First I pick 2 unit vectors at random.
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return Math.random() * (max - min) + min;
}
var points = [{},{}];
points[0].direction = normalize([rand(-1, 1), rand(-1, 1), 0]);
points[1].direction = normalize([rand(-1, 1), rand(-1, 1), 0]);
Note: the math here is in 3D but I'm using a 2d example by just keeping the vectors in the XY plane
I can draw those 2 unit vectors in a canvas
// move to center of canvas
var scale = ctx.canvas.width / 2 * 0.9;
ctx.transform(ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.scale(scale, scale); // expand the unit fill the canvas
// draw a line for each unit vector
points.forEach(function(point) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(point.direction[0], point.direction[1]);
ctx.strokeStyle = point.color;
ctx.stroke();
});
That works.
Next I want to make a matrix that puts the XY plane with its Y axis aligned with the first unit vector and in the same plane as the plane described by the 2 unit vectors
var zAxis = normalize(cross(points[0].direction, points[1].direction));
var xAxis = normalize(cross(zAxis, points[0].direction));
var yAxis = points[0].direction;
I then draw a unit grid using that matrix
ctx.setTransform(
xAxis[0] * scale, xAxis[1] * scale,
yAxis[0] * scale, yAxis[1] * scale,
ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.beginPath();
for (var y = 0; y < 20; ++y) {
var v0 = (y + 0) / 20;
var v1 = (y + 1) / 20;
for (var x = 0; x < 20; ++x) {
var u0 = (x + 0) / 20;
var u1 = (x + 1) / 20;
ctx.moveTo(u0, v0);
ctx.lineTo(u1, v0);
ctx.moveTo(u0, v0);
ctx.lineTo(u0, v1);
}
}
ctx.stroke();
That works too. Run the sample below and see the pink unit grid is always aligned with the green unit vector and facing in the direction of the red unit vector.
Finally using the data for the unit grid I want to bend it the correct amount to fill the space between the 2 unit vectors. Given it's a unit grid it seems like I should be able to do this
var cosineOfAngleBetween = dot(points[0].direction, points[1].direction);
var expand = (1 + -cosineOfAngleBetween) / 2 * Math.PI;
var angle = x * expand; // x goes from 0 to 1
var newX = sin(angle) * y; // y goes from 0 to 1
var newY = cos(angle) * y;
And if I plot newX and newY for every grid point it seems like I should get the correct arc between the 2 unit vectors.
Taking the dot product of the two unit vectors should give me the cosine of the angle between them which goes from 1 if they are coincident to -1 if they are opposite. In my case I need expand to go from 0 to PI so (1 + -dot(p0, p1)) / 2 * PI seems like it should work.
But it doesn't. See the blue arc which is the unit grid points as input to the code above.
Some things I checked. I checked zAxis is correct. It's always either [0,0,1] or [0,0,-1] which is correct. I checked xAxis and yAxis are unit vectors. They are. I checked manually setting expand to PI * .5, PI, PI * 2 and it does exactly what I expect. PI * .5 gets a 90 degree arc, 1/4th of the way around from the blue unit vector. PI gets a half circle exactly as I expect. PI * 2 gets a full circle.
That makes it seem like dot(p0,p1) is wrong but looking at the dot function it seems correct and if test it with various easy vectors it returns what I expect dot([1,0,0], [1,0,0]) returns 1. dot([-1,0,0],[1,0,0]) returns -1. dot([1,0,0],[0,1,0]) returns 0. dot([1,0,0],normalize([1,1,0])) returns 0.707...
What am I missing?
Here's the code live
function cross(a, b) {
var dst = []
dst[0] = a[1] * b[2] - a[2] * b[1];
dst[1] = a[2] * b[0] - a[0] * b[2];
dst[2] = a[0] * b[1] - a[1] * b[0];
return dst;
}
function normalize(a) {
var dst = [];
var lenSq = a[0] * a[0] + a[1] * a[1] + a[2] * a[2];
var len = Math.sqrt(lenSq);
if (len > 0.00001) {
dst[0] = a[0] / len;
dst[1] = a[1] / len;
dst[2] = a[2] / len;
} else {
dst[0] = 0;
dst[1] = 0;
dst[2] = 0;
}
return dst;
}
function dot(a, b) {
return (a[0] * b[0]) + (a[1] * b[1]) + (a[2] * b[2]);
}
var canvas = document.querySelector("canvas");
canvas.width = 200;
canvas.height = 200;
var ctx = canvas.getContext("2d");
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return Math.random() * (max - min) + min;
}
var points = [
{
direction: [0,0,0],
color: "green",
},
{
direction: [0,0,0],
color: "red",
},
];
var expand = 1;
var scale = ctx.canvas.width / 2 * 0.8;
function pickPoints() {
points[0].direction = normalize([rand(-1, 1), rand(-1, 1), 0]);
points[1].direction = normalize([rand(-1, 1), rand(-1, 1), 0]);
expand = (1 + -dot(points[0].direction, points[1].direction)) / 2 * Math.PI;
console.log("expand:", expand);
render();
}
pickPoints();
function render() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.scale(scale, scale);
ctx.lineWidth = 3 / scale;
points.forEach(function(point) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(point.direction[0], point.direction[1]);
ctx.strokeStyle = point.color;
ctx.stroke();
});
var zAxis = normalize(cross(points[0].direction, points[1].direction));
var xAxis = normalize(cross(zAxis, points[0].direction));
var yAxis = points[0].direction;
ctx.setTransform(
xAxis[0] * scale, xAxis[1] * scale,
yAxis[0] * scale, yAxis[1] * scale,
ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.lineWidth = 0.5 / scale;
ctx.strokeStyle = "pink";
drawPatch(false);
ctx.strokeStyle = "blue";
drawPatch(true);
function drawPatch(curved) {
ctx.beginPath();
for (var y = 0; y < 20; ++y) {
var v0 = (y + 0) / 20;
var v1 = (y + 1) / 20;
for (var x = 0; x < 20; ++x) {
var u0 = (x + 0) / 20;
var u1 = (x + 1) / 20;
if (curved) {
var a0 = u0 * expand;
var x0 = Math.sin(a0) * v0;
var y0 = Math.cos(a0) * v0;
var a1 = u1 * expand;
var x1 = Math.sin(a1) * v0;
var y1 = Math.cos(a1) * v0;
var a2 = u0 * expand;
var x2 = Math.sin(a0) * v1;
var y2 = Math.cos(a0) * v1;
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.moveTo(x0, y0);
ctx.lineTo(x2, y2);
} else {
ctx.moveTo(u0, v0);
ctx.lineTo(u1, v0);
ctx.moveTo(u0, v0);
ctx.lineTo(u0, v1);
}
}
}
ctx.stroke();
}
ctx.restore();
}
window.addEventListener('click', pickPoints);
canvas {
border: 1px solid black;
}
div {
display: flex;
}
<div><canvas></canvas><p> Click for new points</p></div>

There's nothing wrong with your dot product function. It's the way you're using it:
expand = (1 + -dot(points[0].direction, points[1].direction)) / 2 * Math.PI;
should be:
expand = Math.acos(dot(points[0].direction, points[1].direction));
The expand variable, as you use it, is an angle (in radians). The dot product gives you the cosine of the angle, but not the angle itself. While the cosine of an angle varies between 1 and -1 for input [0,pi], that value does not map linearly back to the angle itself.
In other words, it doesn't work because the cosine of an angle cannot be transformed into the angle itself simply by scaling it. That's what arcsine is for.
Note that in general, you can often get by using your original formula (or any simple formula that maps that [-1,1] domain to a range of [0,pi]) if all you need is an approximation, but it will never give an exact angle except at the extremes.
This can be seen visually by plotting the two functions on top of each other:

Related

How to get an angle by using tangent in javascript?

The red circle is at a known angle of 130°, then I want to draw the navy line from the center to 130° using x and y of the red circle but it looks like I missed the calculation.
Currently, the angle of the Navy line is a reflection to the angle of the red line and if I add minus sign ➖ to *diffX * at line13, it'll work as expected but Why do I need to do that by myself, why can't the Calculations at line 10 and 13 figured out if x should be minus ➖ or plus.
I couldn't figure out where I was wrong..any help/suggestions are appreciated!
let ctx, W = innerWidth,
H = innerHeight;
// params for the red circle
let hypothenus = 100;
let knownAngle = (-130 * Math.PI) / 180;
let x = (W / 2) + Math.cos(knownAngle) * hypothenus;
let y = (H / 2) + Math.sin(knownAngle) * hypothenus;
// params for navy line
let diffX = x - (W / 2);
let diffY = (H / 2) - y;
let dist = Math.hypot(diffX, diffY); // pythagoras
let unknownAngle = -Math.atan2(diffY, diffX);
let newX = (W / 2) + Math.cos(unknownAngle) * dist;
let newY = (H / 2) + Math.sin(unknownAngle) * dist;
let angInDegree1 = ~~Math.abs(knownAngle * 180 / Math.PI);
let angInDegree2 = ~~Math.abs(unknownAngle * 180 / Math.PI) | 0;
const msg = document.getElementById("msg")
msg.innerHTML = `Hypothenus1: ${hypothenus}, angle: ${angInDegree1}<br>`;
msg.innerHTML +=`Hypothenus2: ${dist}, angle: ${angInDegree2}`;
// everything to be rendered to the screen
const update = () => {
if (ctx == null) return;
// drawing the red line
draw.line([W / 2, 0], [W / 2, H], 6, "red");
draw.line([0, H / 2], [W, H / 2], 6, "red");
// the red circle
draw.circle([x, y], 10, "red");
// draw line
draw.line([W / 2, H / 2], [newX, newY], 4, "navy");
}
// utility object for drawing
const draw = {
line(from, to, width, color) {
with(ctx) {
beginPath();
lineWidth = width;
strokeStyle = color;
moveTo(...from);
lineTo(...to);
stroke();
closePath();
}
},
circle(pos, radius, color) {
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(...pos, radius, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
}
}
// init function
const init = () => {
ctx = document.querySelector("#cvs").getContext("2d");
W = ctx.canvas.width = innerWidth;
H = ctx.canvas.height = innerHeight;
update();
}
window.addEventListener("load", init);
<div id="msg"></div>
<canvas id="cvs"></canvas>
Seems you are using too much minuses.
At first, you define angle -130 degrees, close to -3Pi/4. Cosine and sine values for this angle are about -0.7, using hypothenus = 100, we get x =W/2-70, y = H/2-70
diffX = x - W/2 = -70
diffY = y - H/2 = -70
atan2(-70, -70) gives -2.3561 radians = -3/4*Pi = -135 degrees
When you change sign of diffY (note - diffY formula is wrong, not difX one!), you make reflection against OX axis, and change angle sign - that is why another minus before Math.atan2 is required
Corrected code:
let diffX = x - (W / 2);
let diffY = y - (H / 2);
let dist = Math.hypot(diffX, diffY); // pythagoras
let unknownAngle = Math.atan2(diffY, diffX);

Dynamic Wavy Path/Border

There is something I need to build, but my math ability is not up to par. What I am looking to build is something like this demo, but I need it to be a hybrid of a circle and polygon instead of a line, so to speak. The black line should be dynamic and randomly generated that basically acts as a border on the page.
Currently, I am dissecting this answer with the aim of hopefully being able to transpose it into this, but I am having massive doubts that I will be able to figure this out.
Any idea how to do this or can anybody explain the mathematics?
Below are my notes about the code from the answer I linked above.
var
cw = cvs.width = window.innerWidth,
ch = cvs.height = window.innerHeight,
cx = cw / 2,
cy = ch / 2,
xs = Array(),
ys = Array(),
npts = 20,
amplitude = 87, // can be val from 1 to 100
frequency = -2, // can be val from -10 to 1 in steps of 0.1
ctx.lineWidth = 4
// creates array of coordinates that
// divides page into regular portions
// creates array of weights
for (var i = 0; i < npts; i++) {
xs[i] = (cw/npts)*i
ys[i] = 2.0*(Math.random()-0.5)*amplitude
}
function Draw() {
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
for (let x = 0; x < cw; x++) {
y = 0.0
wsum = 0.0
for (let i = -5; i <= 5; i++) {
xx = x; // 0 / 1 / 2 / to value of screen width
// creates sequential sets from [-5 to 5] to [15 to 25]
ii = Math.round(x/xs[1]) + i
// `xx` is a sliding range with the total value equal to client width
// keeps `ii` within range of 0 to 20
if (ii < 0) {
xx += cw
ii += npts
}
if (ii >= npts){
xx -= cw
ii -= npts
}
// selects eleven sequential array items
// which are portions of the screen width and height
// to create staggered inclines in increments of those portions
w = Math.abs(xs[ii] - xx)
// creates irregular arcs
// based on the inclining values
w = Math.pow(w, frequency)
// also creates irregular arcs therefrom
y += w*ys[ii];
// creates sets of inclining values
wsum += w;
}
// provides a relative position or weight
// for each y-coordinate in the total path
y /= wsum;
//y = Math.sin(x * frequency) * amplitude;
ctx.lineTo(x, y+cy);
}
ctx.stroke();
}
Draw();
This is my answer. Please read the comments in the code. I hope this is what you need.
// initiate the canvas
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let cw = (canvas.width = 600),
cx = cw / 2;
let ch = (canvas.height = 400),
cy = ch / 2;
ctx.fillStyle = "white"
// define the corners of an rectangle
let corners = [[100, 100], [500, 100], [500, 300], [100, 300]];
let amplitud = 20;// oscilation amplitude
let speed = 0.01;// the speed of the oscilation
let points = []; // an array of points to draw the curve
class Point {
constructor(x, y, hv) {
// the point is oscilating around this point (cx,cy)
this.cx = x;
this.cy = y;
// the current angle of oscilation
this.a = Math.random() * 2 * Math.PI;
this.hv = hv;// a variable to know if the oscilation is horizontal or vertical
this.update();
}
// a function to update the value of the angle
update() {
this.a += speed;
if (this.hv == 0) {
this.x = this.cx;
this.y = this.cy + amplitud * Math.cos(this.a);
} else {
this.x = this.cx + amplitud * Math.cos(this.a);
this.y = this.cy;
}
}
}
// a function to divide a line that goes from a to b in n segments
// I'm using the resulting points to create a new point object and push this new point into the points array
function divide(n, a, b) {
for (var i = 0; i <= n; i++) {
let p = {
x: (b[0] - a[0]) * i / n + a[0],
y: (b[1] - a[1]) * i / n + a[1],
hv: b[1] - a[1]
};
points.push(new Point(p.x, p.y, p.hv));
}
}
divide(10, corners[0], corners[1]);points.pop();
divide(5, corners[1], corners[2]);points.pop();
divide(10, corners[2], corners[3]);points.pop();
divide(5, corners[3], corners[0]);points.pop();
// this is a function that takes an array of points and draw a curved line through those points
function drawCurves() {
//find the first midpoint and move to it
let p = {};
p.x = (points[points.length - 1].x + points[0].x) / 2;
p.y = (points[points.length - 1].y + points[0].y) / 2;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
//curve through the rest, stopping at each midpoint
for (var i = 0; i < points.length - 1; i++) {
let mp = {};
mp.x = (points[i].x + points[i + 1].x) / 2;
mp.y = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, mp.x, mp.y);
}
//curve through the last point, back to the first midpoint
ctx.quadraticCurveTo(
points[points.length - 1].x,
points[points.length - 1].y,
p.x,
p.y
);
ctx.stroke();
ctx.fill();
}
function Draw() {
window.requestAnimationFrame(Draw);
ctx.clearRect(0, 0, cw, ch);
points.map(p => {
p.update();
});
drawCurves();
}
Draw();
canvas{border:1px solid; background:#6ab150}
<canvas></canvas>

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>

How to draw herringbone pattern on html canvas

I Have to draw Herringbone pattern on canvas and fill with image
some one please help me I am new to canvas 2d drawing.
I need to draw mixed tiles with cross pattern (Herringbone)
var canvas = this.__canvas = new fabric.Canvas('canvas');
var canvas_objects = canvas._objects;
// create a rectangle with a fill and a different color stroke
var left = 150;
var top = 150;
var x=20;
var y=40;
var rect = new fabric.Rect({
left: left,
top: top,
width: x,
height: y,
angle:45,
fill: 'rgba(255,127,39,1)',
stroke: 'rgba(34,177,76,1)',
strokeWidth:0,
originX:'right',
originY:'top',
centeredRotation: false
});
canvas.add(rect);
for(var i=0;i<15;i++){
var rectangle = fabric.util.object.clone(getLastobject());
if(i%2==0){
rectangle.left = rectangle.oCoords.tr.x;
rectangle.top = rectangle.oCoords.tr.y;
rectangle.originX='right';
rectangle.originY='top';
rectangle.angle =-45;
}else{
fabric.log('rectangle: ', rectangle.toJSON());
rectangle.left = rectangle.oCoords.tl.x;
rectangle.top = rectangle.oCoords.tl.y;
fabric.log('rectangle: ', rectangle.toJSON());
rectangle.originX='left';
rectangle.originY='top';
rectangle.angle =45;
}
//rectangle.angle -90;
canvas.add(rectangle);
}
fabric.log('rectangle: ', canvas.toJSON());
canvas.renderAll();
function getLastobject(){
var last = null;
if(canvas_objects.length !== 0){
last = canvas_objects[canvas_objects.length -1]; //Get last object
}
return last;
}
How to draw this pattern in canvas using svg or 2d,3d method. If any third party library that also Ok for me.
I don't know where to start and how to draw this complex pattern.
some one please help me to draw this pattern with rectangle fill with dynamic color on canvas.
Here is a sample of the output I need: (herringbone pattern)
I tried something similar using fabric.js library here is my JSFiddle
Trippy disco flooring
To get the pattern you need to draw rectangles one horizontal tiled one space left or right for each row down and the same for the vertical rectangle.
The rectangle has an aspect of width 2 time height.
Drawing the pattern is simple.
Rotating is easy as well the harder part is finding where to draw the tiles for the rotation.
To do that I create a inverse matrix of the rotation (it reverses a rotation). I then apply that rotation to the 4 corners of the canvas 0,0, width,0 width,height and 0,height this gives me 4 points in the rotated space that are at the edges of the canvas.
As I draw the tiles from left to right top to bottom I find the min corners for the top left, and the max corners for the bottom right, expand it out a little so I dont miss any pixels and draw the tiles with a transformation set the the rotation.
As I could not workout what angle you wanted it at the function will draw it at any angle. On is animated, the other is at 60deg clockwise.
Warning demo contains flashing content.
Update The flashing was way to out there, so have made a few changes, now colours are a more pleasing blend and have fixed absolute positions, and have tied the tile origin to the mouse position, clicking the mouse button will cycle through some sizes as well.
const ctx = canvas.getContext("2d");
const colours = []
for(let i = 0; i < 1; i += 1/80){
colours.push(`hsl(${Math.floor(i * 360)},${Math.floor((Math.sin(i * Math.PI *4)+1) * 50)}%,${Math.floor(Math.sin(i * Math.PI *8)* 25 + 50)}%)`)
}
const sizes = [0.04,0.08,0.1,0.2];
var currentSize = 0;
const origin = {x : canvas.width / 2, y : canvas.height / 2};
var size = Math.min(canvas.width * 0.2, canvas.height * 0.2);
function drawPattern(size,origin,ang){
const xAx = Math.cos(ang); // define the direction of xAxis
const xAy = Math.sin(ang);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.setTransform(xAx,xAy,-xAy,xAx,origin.x,origin.y);
function getExtent(xAx,xAy,origin){
const im = [1,0,0,1]; // inverse matrix
const dot = xAx * xAx + xAy * xAy;
im[0] = xAx / dot;
im[1] = -xAy / dot;
im[2] = xAy / dot;
im[3] = xAx / dot;
const toWorld = (x,y) => {
var point = {};
var xx = x - origin.x;
var yy = y - origin.y;
point.x = xx * im[0] + yy * im[2];
point.y = xx * im[1] + yy * im[3];
return point;
}
return [
toWorld(0,0),
toWorld(canvas.width,0),
toWorld(canvas.width,canvas.height),
toWorld(0,canvas.height),
]
}
const corners = getExtent(xAx,xAy,origin);
var startX = Math.min(corners[0].x,corners[1].x,corners[2].x,corners[3].x);
var endX = Math.max(corners[0].x,corners[1].x,corners[2].x,corners[3].x);
var startY = Math.min(corners[0].y,corners[1].y,corners[2].y,corners[3].y);
var endY = Math.max(corners[0].y,corners[1].y,corners[2].y,corners[3].y);
startX = Math.floor(startX / size) - 2;
endX = Math.floor(endX / size) + 2;
startY = Math.floor(startY / size) - 2;
endY = Math.floor(endY / size) + 2;
// draw the pattern
ctx.lineWidth = size * 0.1;
ctx.lineJoin = "round";
ctx.strokeStyle = "black";
var colourIndex = 0;
for(var y = startY; y <endY; y+=1){
for(var x = startX; x <endX; x+=1){
if((x + y) % 4 === 0){
colourIndex = Math.floor(Math.abs(Math.sin(x)*size + Math.sin(y) * 20));
ctx.fillStyle = colours[(colourIndex++)% colours.length];
ctx.fillRect(x * size,y * size,size * 2,size);
ctx.strokeRect(x * size,y * size,size * 2,size);
x += 2;
ctx.fillStyle = colours[(colourIndex++)% colours.length];
ctx.fillRect(x * size,y * size, size, size * 2);
ctx.strokeRect(x * size,y * size, size, size * 2);
x += 1;
}
}
}
}
// Animate it all
var update = true; // flag to indecate something needs updating
function mainLoop(time){
// if window size has changed update canvas to new size
if(canvas.width !== innerWidth || canvas.height !== innerHeight || update){
canvas.width = innerWidth;
canvas.height = innerHeight
origin.x = canvas.width / 2;
origin.y = canvas.height / 2;
size = Math.min(canvas.width, canvas.height) * sizes[currentSize % sizes.length];
update = false;
}
if(mouse.buttonRaw !== 0){
mouse.buttonRaw = 0;
currentSize += 1;
update = true;
}
// draw the patter
drawPattern(size,mouse,time/2000);
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
mouse = (function () {
function preventDefault(e) { e.preventDefault() }
var m; // alias for mouse
var mouse = {
x : 0, y : 0, // mouse position
buttonRaw : 0,
over : false, // true if mouse over the element
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
bounds : null,
eventNames : "mousemove,mousedown,mouseup,mouseout,mouseover".split(","),
event(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
if (t === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (t === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
else if (t === "mouseout") { m.over = false }
else if (t === "mouseover") { m.over = true }
e.preventDefault();
},
start(element) {
if (m.element !== undefined) { m.remove() }
m.element = element === undefined ? document : element;
m.eventNames.forEach(name => document.addEventListener(name, mouse.event) );
document.addEventListener("contextmenu", preventDefault, false);
},
}
m = mouse;
return mouse;
})();
mouse.start(canvas);
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
Un-animated version at 60Deg
const ctx = canvas.getContext("2d");
const colours = ["red","green","yellow","orange","blue","cyan","magenta"]
const origin = {x : canvas.width / 2, y : canvas.height / 2};
var size = Math.min(canvas.width * 0.2, canvas.height * 0.2);
function drawPattern(size,origin,ang){
const xAx = Math.cos(ang); // define the direction of xAxis
const xAy = Math.sin(ang);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.setTransform(xAx,xAy,-xAy,xAx,origin.x,origin.y);
function getExtent(xAx,xAy,origin){
const im = [1,0,0,1]; // inverse matrix
const dot = xAx * xAx + xAy * xAy;
im[0] = xAx / dot;
im[1] = -xAy / dot;
im[2] = xAy / dot;
im[3] = xAx / dot;
const toWorld = (x,y) => {
var point = {};
var xx = x - origin.x;
var yy = y - origin.y;
point.x = xx * im[0] + yy * im[2];
point.y = xx * im[1] + yy * im[3];
return point;
}
return [
toWorld(0,0),
toWorld(canvas.width,0),
toWorld(canvas.width,canvas.height),
toWorld(0,canvas.height),
]
}
const corners = getExtent(xAx,xAy,origin);
var startX = Math.min(corners[0].x,corners[1].x,corners[2].x,corners[3].x);
var endX = Math.max(corners[0].x,corners[1].x,corners[2].x,corners[3].x);
var startY = Math.min(corners[0].y,corners[1].y,corners[2].y,corners[3].y);
var endY = Math.max(corners[0].y,corners[1].y,corners[2].y,corners[3].y);
startX = Math.floor(startX / size) - 4;
endX = Math.floor(endX / size) + 4;
startY = Math.floor(startY / size) - 4;
endY = Math.floor(endY / size) + 4;
// draw the pattern
ctx.lineWidth = 5;
ctx.lineJoin = "round";
ctx.strokeStyle = "black";
for(var y = startY; y <endY; y+=1){
for(var x = startX; x <endX; x+=1){
ctx.fillStyle = colours[Math.floor(Math.random() * colours.length)];
if((x + y) % 4 === 0){
ctx.fillRect(x * size,y * size,size * 2,size);
ctx.strokeRect(x * size,y * size,size * 2,size);
x += 2;
ctx.fillStyle = colours[Math.floor(Math.random() * colours.length)];
ctx.fillRect(x * size,y * size, size, size * 2);
ctx.strokeRect(x * size,y * size, size, size * 2);
x += 1;
}
}
}
}
canvas.width = innerWidth;
canvas.height = innerHeight
origin.x = canvas.width / 2;
origin.y = canvas.height / 2;
size = Math.min(canvas.width * 0.2, canvas.height * 0.2);
drawPattern(size,origin,Math.PI / 3);
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
The best way to approach this is to examine the pattern and analyse its symmetry and how it repeats.
You can look at this several ways. For example, you could rotate the patter 45 degrees so that the tiles are plain orthogonal rectangles. But let's just look at it how it is. I am going to assume you are happy with it with 45deg tiles.
Like the tiles themselves, it turns out the pattern has a 2:1 ratio. If we repeat this pattern horizontally and vertically, we can fill the canvas with the completed pattern.
We can see there are five tiles that overlap with our pattern block. However we don't need to draw them all when we draw each pattern block. We can take advantage of the fact that blocks are repeated, and we can leave the drawing of some tiles to later rows and columns.
Let's assume we are drawing the pattern blocks from left to right and top to bottom. Which tiles do we need to draw, at a minimum, to ensure this pattern block gets completely drawn (taking into account adjacent pattern blocks)?
Since we will be starting at the top left (and moving right and downwards), we'll need to draw tile 2. That's because that tile won't get drawn by either the block below us, or the block to the right of us. The same applies to tile 3.
It turns out those two are all we'll need to draw for each pattern block. Tile 1 and 4 will be drawn when the pattern block below us draws their tile 2 and 3 respectively. Tile 5 will be drawn when the pattern block to the south-east of us draws their tile 1.
We just need to remember that we may need to draw an extra column on the right-hand side, and at the bottom, to ensure those end-of-row and end-of-column pattern blocks get completely drawn.
The last thing to work out is how big our pattern blocks are.
Let's call the short side of the tile a and the long side b. We know that b = 2 * a. And we can work out, using Pythagoras Theorem, that the height of the pattern block will be:
h = sqrt(a^2 + a^2)
= sqrt(2 * a^2)
= sqrt(2) * a
The width of the pattern block we can see will be w = 2 * h.
Now that we've worked out how to draw the pattern, let's implement our algorithm.
const a = 60;
const b = 120;
const h = 50 * Math.sqrt(2);
const w = h * 2;
const h2 = h / 2; // How far tile 1 sticks out to the left of the pattern block
// Set of colours for the tiles
const colours = ["red","cornsilk","black","limegreen","deepskyblue",
"mediumorchid", "lightgrey", "grey"]
const canvas = document.getElementById("herringbone");
const ctx = canvas.getContext("2d");
// Set a universal stroke colour and width
ctx.strokeStyle = "black";
ctx.lineWidth = 4;
// Loop through the pattern block rows
for (var y=0; y < (canvas.height + h); y+=h)
{
// Loop through the pattern block columns
for (var x=0; x < (canvas.width + w); x+=w)
{
// Draw tile "2"
// I'm just going to draw a path for simplicity, rather than
// worrying about drawing a rectangle with rotation and translates
ctx.beginPath();
ctx.moveTo(x - h2, y - h2);
ctx.lineTo(x, y - h);
ctx.lineTo(x + h, y);
ctx.lineTo(x + h2, y + h2);
ctx.closePath();
ctx.fillStyle = colours[Math.floor(Math.random() * colours.length)];
ctx.fill();
ctx.stroke();
// Draw tile "3"
ctx.beginPath();
ctx.moveTo(x + h2, y + h2);
ctx.lineTo(x + w - h2, y - h2);
ctx.lineTo(x + w, y);
ctx.lineTo(x + h, y + h);
ctx.closePath();
ctx.fillStyle = colours[Math.floor(Math.random() * colours.length)];
ctx.fill();
ctx.stroke();
}
}
<canvas id="herringbone" width="500" height="400"></canvas>

Canvas Rotating Star Field

I'm taking the following approach to animate a star field across the screen, but I'm stuck for the next part.
JS
var c = document.getElementById('stars'),
ctx = c.getContext("2d"),
t = 0; // time
c.width = 300;
c.height = 300;
var w = c.width,
h = c.height,
z = c.height,
v = Math.PI; // angle of vision
(function animate() {
Math.seedrandom('bg');
ctx.globalAlpha = 1;
for (var i = 0; i <= 100; i++) {
var x = Math.floor(Math.random() * w), // pos x
y = Math.floor(Math.random() * h), // pos y
r = Math.random()*2 + 1, // radius
a = Math.random()*0.5 + 0.5, // alpha
// linear
d = (r*a), // depth
p = t*d; // pixels per t
x = x - p; // movement
x = x - w * Math.floor(x / w); // go around when x < 0
(function draw(x,y) {
var gradient = ctx.createRadialGradient(x, y, 0, x + r, y + r, r * 2);
gradient.addColorStop(0, 'rgba(255, 255, 255, ' + a + ')');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.beginPath();
ctx.arc(x, y, r, 0, 2*Math.PI);
ctx.fillStyle = gradient;
ctx.fill();
return draw;
})(x, y);
}
ctx.restore();
t += 1;
requestAnimationFrame(function() {
ctx.clearRect(0, 0, c.width, c.height);
animate();
});
})();
HTML
<canvas id="stars"></canvas>
CSS
canvas {
background: black;
}
JSFiddle
What it does right now is animate each star with a delta X that considers the opacity and size of the star, so the smallest ones appear to move slower.
Use p = t; to have all the stars moving at the same speed.
QUESTION
I'm looking for a clearly defined model where the velocities give the illusion of the stars rotating around the expectator, defined in terms of the center of the rotation cX, cY, and the angle of vision v which is what fraction of 2π can be seen (if the center of the circle is not the center of the screen, the radius should be at least the largest portion). I'm struggling to find a way that applies this cosine to the speed of star movements, even for a centered circle with a rotation of π.
These diagrams might further explain what I'm after:
Centered circle:
Non-centered:
Different angle of vision:
I'm really lost as to how to move forwards. I already stretched myself a bit to get here. Can you please help me with some first steps?
Thanks
UPDATE
I have made some progress with this code:
// linear
d = (r*a)*z, // depth
v = (2*Math.PI)/w,
p = Math.floor( d * Math.cos( t * v ) ); // pixels per t
x = x + p; // movement
x = x - w * Math.floor(x / w); // go around when x < 0
JSFiddle
Where p is the x coordinate of a particle in uniform circular motion and v is the angular velocity, but this generates a pendulum effect. I am not sure how to change these equations to create the illusion that the observer is turning instead.
UPDATE 2:
Almost there. One user at the ##Math freenode channel was kind enough to suggest the following calculation:
// linear
d = (r*a), // depth
p = t*d; // pixels per t
x = x - p; // movement
x = x - w * Math.floor(x / w); // go around when x < 0
x = (x / w) - 0.5;
y = (y / h) - 0.5;
y /= Math.cos(x);
x = (x + 0.5) * w;
y = (y + 0.5) * h;
JSFiddle
This achieves the effect visually, but does not follow a clearly defined model in terms of the variables (it just "hacks" the effect) so I cannot see a straightforward way to do different implementations (change the center, angle of vision). The real model might be very similar to this one.
UPDATE 3
Following from Iftah's response, I was able to use Sylvester to apply a rotation matrix to the stars, which need to be saved in an array first. Also each star's z coordinate is now determined and the radius r and opacity a are derived from it instead. The code is substantially different and lenghthier so I am not posting it, but it might be a step in the right direction. I cannot get this to rotate continuously yet. Using matrix operations on each frame seems costly in terms of performance.
JSFiddle
Here's some pseudocode that does what you're talking about.
Make a bunch of stars not too far but not too close (via rejection sampling)
Set up a projection matrix (defines the camera frustum)
Each frame
Compute our camera rotation angle
Make a "view" matrix (repositions the stars to be relative to our view)
Compose the view and projection matrix into the view-projection matrix
For each star
Apply the view-projection matrix to give screen star coordinates
If the star is behind the camera skip it
Do some math to give the star a nice seeming 'size'
Scale the star coordinate to the canvas
Draw the star with its canvas coordinate and size
I've made an implementation of the above. It uses the gl-matrix Javascript library to handle some of the matrix math. It's good stuff. (Fiddle for this is here, or see below.)
var c = document.getElementById('c');
var n = c.getContext('2d');
// View matrix, defines where you're looking
var viewMtx = mat4.create();
// Projection matrix, defines how the view maps onto the screen
var projMtx = mat4.create();
// Adapted from http://stackoverflow.com/questions/18404890/how-to-build-perspective-projection-matrix-no-api
function ComputeProjMtx(field_of_view, aspect_ratio, near_dist, far_dist, left_handed) {
// We'll assume input parameters are sane.
field_of_view = field_of_view * Math.PI / 180.0; // Convert degrees to radians
var frustum_depth = far_dist - near_dist;
var one_over_depth = 1 / frustum_depth;
var e11 = 1.0 / Math.tan(0.5 * field_of_view);
var e00 = (left_handed ? 1 : -1) * e11 / aspect_ratio;
var e22 = far_dist * one_over_depth;
var e32 = (-far_dist * near_dist) * one_over_depth;
return [
e00, 0, 0, 0,
0, e11, 0, 0,
0, 0, e22, e32,
0, 0, 1, 0
];
}
// Make a view matrix with a simple rotation about the Y axis (up-down axis)
function ComputeViewMtx(angle) {
angle = angle * Math.PI / 180.0; // Convert degrees to radians
return [
Math.cos(angle), 0, Math.sin(angle), 0,
0, 1, 0, 0,
-Math.sin(angle), 0, Math.cos(angle), 0,
0, 0, 0, 1
];
}
projMtx = ComputeProjMtx(70, c.width / c.height, 1, 200, true);
var angle = 0;
var viewProjMtx = mat4.create();
var minDist = 100;
var maxDist = 1000;
function Star() {
var d = 0;
do {
// Create random points in a cube.. but not too close.
this.x = Math.random() * maxDist - (maxDist / 2);
this.y = Math.random() * maxDist - (maxDist / 2);
this.z = Math.random() * maxDist - (maxDist / 2);
var d = this.x * this.x +
this.y * this.y +
this.z * this.z;
} while (
d > maxDist * maxDist / 4 || d < minDist * minDist
);
this.dist = Math.sqrt(d);
}
Star.prototype.AsVector = function() {
return [this.x, this.y, this.z, 1];
}
var stars = [];
for (var i = 0; i < 5000; i++) stars.push(new Star());
var lastLoop = Date.now();
function loop() {
var now = Date.now();
var dt = (now - lastLoop) / 1000.0;
lastLoop = now;
angle += 30.0 * dt;
viewMtx = ComputeViewMtx(angle);
//console.log('---');
//console.log(projMtx);
//console.log(viewMtx);
mat4.multiply(viewProjMtx, projMtx, viewMtx);
//console.log(viewProjMtx);
n.beginPath();
n.rect(0, 0, c.width, c.height);
n.closePath();
n.fillStyle = '#000';
n.fill();
n.fillStyle = '#fff';
var v = vec4.create();
for (var i = 0; i < stars.length; i++) {
var star = stars[i];
vec4.transformMat4(v, star.AsVector(), viewProjMtx);
v[0] /= v[3];
v[1] /= v[3];
v[2] /= v[3];
//v[3] /= v[3];
if (v[3] < 0) continue;
var x = (v[0] * 0.5 + 0.5) * c.width;
var y = (v[1] * 0.5 + 0.5) * c.height;
// Compute a visual size...
// This assumes all stars are the same size.
// It also doesn't scale with canvas size well -- we'd have to take more into account.
var s = 300 / star.dist;
n.beginPath();
n.arc(x, y, s, 0, Math.PI * 2);
//n.rect(x, y, s, s);
n.closePath();
n.fill();
}
window.requestAnimationFrame(loop);
}
loop();
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.1/gl-matrix-min.js"></script>
<canvas id="c" width="500" height="500"></canvas>
Some links:
More on projection matrices
gl-matrix
Using view/projection matrices
Update
Here's another version that has keyboard controls. Kinda fun. You can see the difference between rotating and parallax from strafing. Works best full page. (Fiddle for this is here or see below.)
var c = document.getElementById('c');
var n = c.getContext('2d');
// View matrix, defines where you're looking
var viewMtx = mat4.create();
// Projection matrix, defines how the view maps onto the screen
var projMtx = mat4.create();
// Adapted from http://stackoverflow.com/questions/18404890/how-to-build-perspective-projection-matrix-no-api
function ComputeProjMtx(field_of_view, aspect_ratio, near_dist, far_dist, left_handed) {
// We'll assume input parameters are sane.
field_of_view = field_of_view * Math.PI / 180.0; // Convert degrees to radians
var frustum_depth = far_dist - near_dist;
var one_over_depth = 1 / frustum_depth;
var e11 = 1.0 / Math.tan(0.5 * field_of_view);
var e00 = (left_handed ? 1 : -1) * e11 / aspect_ratio;
var e22 = far_dist * one_over_depth;
var e32 = (-far_dist * near_dist) * one_over_depth;
return [
e00, 0, 0, 0,
0, e11, 0, 0,
0, 0, e22, e32,
0, 0, 1, 0
];
}
// Make a view matrix with a simple rotation about the Y axis (up-down axis)
function ComputeViewMtx(angle) {
angle = angle * Math.PI / 180.0; // Convert degrees to radians
return [
Math.cos(angle), 0, Math.sin(angle), 0,
0, 1, 0, 0,
-Math.sin(angle), 0, Math.cos(angle), 0,
0, 0, -250, 1
];
}
projMtx = ComputeProjMtx(70, c.width / c.height, 1, 200, true);
var angle = 0;
var viewProjMtx = mat4.create();
var minDist = 100;
var maxDist = 1000;
function Star() {
var d = 0;
do {
// Create random points in a cube.. but not too close.
this.x = Math.random() * maxDist - (maxDist / 2);
this.y = Math.random() * maxDist - (maxDist / 2);
this.z = Math.random() * maxDist - (maxDist / 2);
var d = this.x * this.x +
this.y * this.y +
this.z * this.z;
} while (
d > maxDist * maxDist / 4 || d < minDist * minDist
);
this.dist = 100;
}
Star.prototype.AsVector = function() {
return [this.x, this.y, this.z, 1];
}
var stars = [];
for (var i = 0; i < 5000; i++) stars.push(new Star());
var lastLoop = Date.now();
var dir = {
up: 0,
down: 1,
left: 2,
right: 3
};
var dirStates = [false, false, false, false];
var shiftKey = false;
var moveSpeed = 100.0;
var turnSpeed = 1.0;
function loop() {
var now = Date.now();
var dt = (now - lastLoop) / 1000.0;
lastLoop = now;
angle += 30.0 * dt;
//viewMtx = ComputeViewMtx(angle);
var tf = mat4.create();
if (dirStates[dir.up]) mat4.translate(tf, tf, [0, 0, moveSpeed * dt]);
if (dirStates[dir.down]) mat4.translate(tf, tf, [0, 0, -moveSpeed * dt]);
if (dirStates[dir.left])
if (shiftKey) mat4.rotate(tf, tf, -turnSpeed * dt, [0, 1, 0]);
else mat4.translate(tf, tf, [moveSpeed * dt, 0, 0]);
if (dirStates[dir.right])
if (shiftKey) mat4.rotate(tf, tf, turnSpeed * dt, [0, 1, 0]);
else mat4.translate(tf, tf, [-moveSpeed * dt, 0, 0]);
mat4.multiply(viewMtx, tf, viewMtx);
//console.log('---');
//console.log(projMtx);
//console.log(viewMtx);
mat4.multiply(viewProjMtx, projMtx, viewMtx);
//console.log(viewProjMtx);
n.beginPath();
n.rect(0, 0, c.width, c.height);
n.closePath();
n.fillStyle = '#000';
n.fill();
n.fillStyle = '#fff';
var v = vec4.create();
for (var i = 0; i < stars.length; i++) {
var star = stars[i];
vec4.transformMat4(v, star.AsVector(), viewProjMtx);
if (v[3] < 0) continue;
var d = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
v[0] /= v[3];
v[1] /= v[3];
v[2] /= v[3];
//v[3] /= v[3];
var x = (v[0] * 0.5 + 0.5) * c.width;
var y = (v[1] * 0.5 + 0.5) * c.height;
// Compute a visual size...
// This assumes all stars are the same size.
// It also doesn't scale with canvas size well -- we'd have to take more into account.
var s = 300 / d;
n.beginPath();
n.arc(x, y, s, 0, Math.PI * 2);
//n.rect(x, y, s, s);
n.closePath();
n.fill();
}
window.requestAnimationFrame(loop);
}
loop();
function keyToDir(evt) {
var d = -1;
if (evt.keyCode === 38) d = dir.up
else if (evt.keyCode === 37) d = dir.left;
else if (evt.keyCode === 39) d = dir.right;
else if (evt.keyCode === 40) d = dir.down;
return d;
}
window.onkeydown = function(evt) {
var d = keyToDir(evt);
if (d >= 0) dirStates[d] = true;
if (evt.keyCode === 16) shiftKey = true;
}
window.onkeyup = function(evt) {
var d = keyToDir(evt);
if (d >= 0) dirStates[d] = false;
if (evt.keyCode === 16) shiftKey = false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.1/gl-matrix-min.js"></script>
<div>Click in this pane. Use up/down/left/right, hold shift + left/right to rotate.</div>
<canvas id="c" width="500" height="500"></canvas>
Update 2
Alain Jacomet Forte asked:
What is your recommended method of creating general purpose 3d and if you would recommend working at the matrices level or not, specifically perhaps to this particular scenario.
Regarding matrices: If you're writing an engine from scratch on any platform, then you're unavoidably going to end up working with matrices since they help generalize the basic 3D mathematics. Even if you use OpenGL/WebGL or Direct3D you're still going to end up making a view and projection matrix and additional matrices for more sophisticated purposes. (Handling normal maps, aligning world objects, skinning, etc...)
Regarding a method of creating general purpose 3d... Don't. It will run slow, and it won't be performant without a lot of work. Rely on a hardware-accelerated library to do the heavy lifting. Creating limited 3D engines for specific projects is fun and instructive (e.g. I want a cool animation on my webpage), but when it comes to putting the pixels on the screen for anything serious, you want hardware to handle that as much as you can for performance purposes.
Sadly, the web has no great standard for that yet, but it is coming in WebGL -- learn WebGL, use WebGL. It runs great and works well when it's supported. (You can, however, get away with an awful lot just using CSS 3D transforms and Javascript.)
If you're doing desktop programming, I highly recommend OpenGL via SDL (I'm not sold on SFML yet) -- it's cross-platform and well supported.
If you're programming mobile phones, OpenGL ES is pretty much your only choice (other than a dog-slow software renderer).
If you want to get stuff done rather than writing your own engine from scratch, the defacto for the web is Three.js (which I find effective but mediocre). If you want a full game engine, there's some free options these days, the main commercial ones being Unity and Unreal. Irrlicht has been around a long time -- never had a chance to use it, though, but I hear it's good.
But if you want to make all the 3D stuff from scratch... I always found how the software renderer in Quake was made a pretty good case study. Some of that can be found here.
You are resetting the stars 2d position each frame, then moving the stars (depending on how much time and speed of each star) - this is a bad way to achieve your goal. As you discovered, it gets very complex when you try to extend this solution to more scenarios.
A better way would be to set the stars 3d location only once (at initialization) then move a "camera" each frame (depending on time). When you want to render the 2d image you then calculate the stars location on screen. The location on screen depends on the stars 3d location and the current camera location.
This will allow you to move the camera (in any direction), rotate the camera (to any angle) and render the correct stars position AND keep your sanity.

Categories

Resources