Mapping mouse coordinates with context - javascript
I have a webgl being rendered on a canvas element. After it is rendered I want to allow user to draw on it with a mouse (rect for example). Since the getContext does not work for the second time, I added another transparent canvas on top of my webgl canvas and I am want to draw a rect with a mouse on the transparent canvas. The problem is that the coordinates in a mousedown event are very different to the context corrdinates
My canvas are as below
<div id="container">
<canvas id="webglCanvas" tabindex='1'></canvas>
<canvas id="transCanvas" tabindex='1'></canvas>
</div>
to get context
var $canvas1 = document.getElementById('transCanvas');
var ctx = $canvas1.getContext("2d");
mouse down event of transCanvas. Please note that I have hard coded the rect at the moment on mouse down event. Later I will do it on mouse move etc. This works fine on my canvas and I can see the rect on my screen. But the mouse coordinates eg e.clientX and e.clientY are in hundereds and go off the screen?
function handleCanvasMouseMove(e) {
ctx.beginPath();
ctx.fillStyle = '#F30';
ctx.fillRect(75, 75, 75, 75);
}
Remember, you're converting NormalizedDeviceCoords with a range of [-1..1] in each axis to a position on screen. All the transformations you applied takes the model-space vert and essentially put it in a cube of size 2, centred on the origin.
So... I imagine you'd also like to get back mouse-coordinates in this same space. If so, it's just a matter of constructing a matrix and then multiplying the screen-space position by this matrix to get x,y in the range of [-1..1]
When I've done similar things in the past, I've used a series of transformations as follows:
function makeClipToPixMat(width,height)
{
// flip the Y
let s1 = mat4.scaling(1,-1,1);
// translate so space is now [0..2]
let t1 = mat4.translation(1,1,0);
// scale so space is now [0..width], [0..height]
let s2 = mat4.scaling(width/2,height/2,1);
// s1, then t1, then s2 are applied to the input coords
let result = mat4.matrixByMatrix(s1,t1);
result = result.multiply(s2);
return result;
}
But as you'll notice from the name, it's a mapping in the wrong direction. We want to map screen-coords to NDC, but this code does the opposite. What now then? Simple - either invert the matrix or determine the series of transforms needed and construct a matrix that will do them all in one go. It's a simple enough transform, that a matrix-inversion seems like a fantastically expensive way to do something so simple.
In fact, here's the function I use. Inversion works fine too and can decrease the code size at the expensive of run-time.
function pixelsToClipspace(width,height)
{
let scaleMat = mat4.scaling( 1/(width/2), -1/(height/2), 1);
let translateMat = mat4.translation(-width/2, -height/2, 0); //.transpose();
let mat = mat4.matrixByMatrix(translateMat, scaleMat);
return mat;
}
Since I've some time at the moment, I hacked together a quick demo for you. Shame, there's 380 lines of code for the vec4 and the matrix, yet only about 35 for the demo. :laughs: That said, it perfectly illustrates how expensive and complicated the matrix .inverse() function is.
LASTLY: please note, I do not make any claims as to the accuracy of any of the code included yet not utilized. Exercises like this one benefit each of us. You get some understanding, I get some more debug test-cases. :) The matrices are column-major (like all good GL ones should be)
"use strict";
window.addEventListener('load', onLoaded, false);
let s2Clip = null;
function onLoaded(evt)
{
let can = document.querySelector('canvas');
s2Clip = pixelsToClipspace(can.clientWidth, can.clientHeight); // use clienWidth/clientHeight to avoid CSS scaling problems
can.addEventListener('mousemove', onMouse, false);
}
function onMouse(evt)
{
var rawPos = new vec4(evt.offsetX, evt.offsetY, 0, 1);
var trPos = s2Clip.timesVector(rawPos);
document.getElementById('rawMouse').innerText = `${rawPos.x}, ${rawPos.y}`
document.getElementById('transMouse').innerText = `${trPos.x.toFixed(2)}, ${trPos.y.toFixed(2)}`
}
function pixelsToClipspace(width,height)
{
let scaleMat = mat4.scaling( 1/(width/2), -1/(height/2), 1);
let translateMat = mat4.translation(-width/2, -height/2, 0); //.transpose();
let mat = mat4.matrixByMatrix(translateMat, scaleMat);
return mat;
}
// </script>
// <script origSrc='vector.js'>
class vec4
{
// w=0 for dir (cant translate), w=1 for pos (can)
constructor(x=0,y=0,z=0,w=0){this.values = [x,y,z,w];}
clone(){ return new vec4(this.x,this.y,this.z,this.w); }
get x(){return this.values[0];}
get y(){return this.values[1];}
get z(){return this.values[2];}
get w(){return this.values[3];}
set x(x){this.values[0]=x;}
set y(y){this.values[1]=y;}
set z(z){this.values[2]=z;}
set w(w){this.values[3]=w;}
get length(){return Math.hypot( ...this.values ); }
normalize(){ var l = this.length; if (l>1e-6) {this.x/=l;this.y/=l;this.z/=l;this.w/=l;} return this;}
scaleBy(scalar){this.x*=scalar;this.y*=scalar;this.z*=scalar;this.w*=scalar;return this;}
divBy(scalar){this.x/=scalar;this.y/=scalar;this.z/=scalar;this.w/=scalar;return this;}
add(other){return new vec4(this.x+other.x, this.y+other.y, this.z+other.z, this.w+other.w);}
sub(other){return new vec4(this.x-other.x, this.y-other.y, this.z-other.z, this.w-other.w);}
get xyz(){return new vec3(this.x,this.y,this.z);}
toStringN(n){return `[${pad(this.x,n)}, ${pad(this.y,n)}, ${pad(this.z,n)}, ${pad(this.w,n)}]`;}
timesMatrix(matrix)
{
let m0 = matrix.getCol(0), m1 = matrix.getCol(1), m2 = matrix.getCol(2), m3 = matrix.getCol(3);
return new vec4(
(m0.x*this.x) + (m1.x*this.y) + m2.x*this.z + m3.x*this.w,
(m0.y*this.x) + (m1.y*this.y) + m2.y*this.z + m3.y*this.w,
(m0.z*this.x) + (m1.z*this.y) + m2.z*this.z + m3.z*this.w,
(m0.w*this.x) + (m1.w*this.y) + m2.w*this.z + m3.w*this.w
);
}
vecByMatrix(m) /// operator * (matrix, vector)
{
let mc0 = m.getCol(0), mc1=m.getCol(1), mc2=m.getCol(2), mc3=m.getCol(3);
return new vec4(
(mc0.x * this.x) + (mc1.x * this.y) + (mc2.x * this.z) + (mc3.x * this.w),
(mc0.y * this.x) + (mc1.y * this.y) + (mc2.y * this.z) + (mc3.y * this.w),
(mc0.z * this.x) + (mc1.z * this.y) + (mc2.z * this.z) + (mc3.z * this.w),
(mc0.w * this.x) + (mc1.w * this.y) + (mc2.w * this.z) + (mc3.w * this.w),
);
}
matrixByVec(m) /// operator * (vector, matrix)
{
let mCol0 = m.getCol(0), mCol1=m.getCol(1), mCol2=m.getCol(2), mCol3=m.getCol(3);
return new vec4(
this.x*mCol0.x + this.y*mCol0.y + this.z*mCol0.z + this.w*mCol0.w,
this.x*mCol1.x + this.y*mCol1.y + this.z*mCol1.z + this.w*mCol1.w,
this.x*mCol2.x + this.y*mCol2.y + this.z*mCol2.z + this.w*mCol2.w,
this.x*mCol3.x + this.y*mCol3.y + this.z*mCol3.z + this.w*mCol3.w
);
}
}
class mat4
{
constructor(xVec4=new vec4(1,0,0,0), yVec4=new vec4(0,1,0,0), zVec4=new vec4(0,0,1,0), wVec4=new vec4(0,0,0,1) )
{
this.columns = [
xVec4.clone(),
yVec4.clone(),
zVec4.clone(),
wVec4.clone()
];
}
getCol(colIndex) {return this.columns[colIndex];}
setCol(colIndex, newVec) {this.columns[colIndex] = newVec.clone();}
setIdentity()
{
let x=new vec4(1,0,0,0);
let y=new vec4(0,1,0,0);
let z=new vec4(0,0,1,0);
let w=new vec4(0,0,0,1);
this.setCol(0,x);
this.setCol(0,y);
this.setCol(0,z);
this.setCol(0,w);
return this;
}
static clone(other)
{
var result = new mat4( other.columns[0], other.columns[1], other.columns[2], other.columns[3] );
return result;
}
clone()
{
return mat4.clone(this);
}
static scaling(sx=1,sy=1,sz=1)
{
let x = new vec4(sx,0,0,);
let y = new vec4(0,sy,0,);
let z = new vec4(0,0,sz,);
let w = new vec4(0,0,0,1);
return new mat4(x,y,z,w);
}
static translation(tx=0,ty=0,tz=0)
{
let X = new vec4(1,0,0,tx);
let Y = new vec4(0,1,0,ty);
let Z = new vec4(0,0,1,tz);
let W = new vec4(0,0,0,1);
return new mat4(X,Y,Z,W);
}
static matrixByMatrix(m1, m2)
{
let mCol0 = m2.getCol(0), mCol1=m2.getCol(1), mCol2=m2.getCol(2), mCol3=m2.getCol(3);
let X = mCol0.vecByMatrix(m1);
let Y = mCol1.vecByMatrix(m1);
let Z = mCol2.vecByMatrix(m1);
let W = mCol3.vecByMatrix(m1);
return new mat4(X,Y,Z,W);
}
static matTimeMat(m1,m2)
{
let mc0=m2.getCol(0),mc1=m2.getCol(1),mc2=m2.getCol(2),mc3=m2.getCol(3);
let x = m1.timesVector(mc0);
let y = m1.timesVector(mc1);
let z = m1.timesVector(mc2);
let w = m1.timesVector(mc3);
return new mat4(x,y,z,w);
}
multiply(other,shouldPrepend=false)
{
var a=this,b=other,c;
if (shouldPrepend===true){a=other;b=this;}
c = mat4.matrixByMatrix(a,b);
this.columns = c.columns.slice();
return this;
}
translate(tx=0,ty=0,tz=0)
{
return this.multiply( mat4.translation(tx,ty,tz) );
}
setScale(sx=1,sy=1,sz=1)
{
let x = new vec4(sx,0,0,0);
let y = new vec4(0,sy,0,0);
let z = new vec4(0,0,sz,0);
let w = new vec4(0,0,0,1);
let tmp = new mat4(x,y,z,w);
this.columns = tmp.columns.slice();
return this;
}
setTrans(tx=0,ty=0,tz=0)
{
let x = new vec4( 1, 0, 0, 0);
let y = new vec4( 0, 1, 0, 0);
let z = new vec4( 0, 0, 1, 0);
let w = new vec4( tx, ty, tz, 1);
var tmp = new mat4(x,y,z,w);
this.columns = tmp.columns.slice();
return this;
}
setRotX(degrees)
{
let cosa = Math.cos(degrees * 3.141/180);
let sina = Math.sin(degrees * 3.141/180);
let x = new vec4(1,0,0,0);
let y = new vec4(0,cosa,sina,0)
let z = new vec4(0,-sina,cosa,0);
let w = new vec4(0,0,0,1);
let tmp = new mat4(x,y,z,w);
this.columns = tmp.columns.slice();
return this;
}
setRotY(degrees)
{
let cosa = Math.cos(degrees * 3.141/180);
let sina = Math.sin(degrees * 3.141/180);
let x = new vec4( cosa, 0,-sina,0);
let y = new vec4( 0, 1, 0, 0)
let z = new vec4( sina, 0,cosa, 0);
let w = new vec4( 0, 0, 0, 1);
let tmp = new mat4(x,y,z,w);
this.columns = tmp.columns.slice();
return this;
}
setRotZ(degrees)
{
let cosa = Math.cos(degrees * 3.141/180);
let sina = Math.sin(degrees * 3.141/180);
let x = new vec4(cosa,sina,0,0);
let y = new vec4(-sina,cosa,0,0)
let z = new vec4(0,0,1,0);
let w = new vec4(0,0,0,1);
let tmp = new mat4(x,y,z,w);
this.columns = tmp.columns.slice();
return this;
}
scaleEach(sX=1,sY=1,sZ=1,shouldPrepend=false)
{
let tmp = new mat4();
let X = tmp.getCol(0);
X.x = sX;
tmp.setCol(0,X);
let Y = tmp.getCol(1);
Y.y = sY;
tmp.setCol(1,Y);
let Z = tmp.getCol(2);
Z.z = sZ;
tmp.setCol(2,Z);
return this.multiply(tmp, shouldPrepend);
//return this;
}
scaleAll(sXYZ, shouldPrepend=false)
{
return this.scaleEach(sXYZ,sXYZ,sXYZ,shouldPrepend);
//return this;
}
/*
translate(tX=0, tY=0, tZ=0, shouldPrepend=false)
{
let tmp = new mat4();
let W = tmp.getCol(3);
W.x = tX;
W.y = tY;
W.z = tZ;
tmp.setCol(3,W);
return this.multiply(tmp, shouldPrepend);
}
*/
timesVector(vector)
{
let m0=this.getCol(0), m1=this.getCol(1), m2=this.getCol(2), m3=this.getCol(3);
return new vec4(
(vector.x*m0.x) + (vector.y*m0.y) + (vector.z*m0.z) + (vector.w*m0.w),
(vector.x*m1.x) + (vector.y*m1.y) + (vector.z*m1.z) + (vector.w*m1.w),
(vector.x*m2.x) + (vector.y*m2.y) + (vector.z*m2.z) + (vector.w*m2.w),
(vector.x*m3.x) + (vector.y*m3.y) + (vector.z*m3.z) + (vector.w*m3.w)
);
}
toString()
{
let result = '', row=0,col=0;
result = `[ ${this.getCol(0).x}, ${this.getCol(1).x}, ${this.getCol(2).x}, ${this.getCol(3).x} ]\n`;
result += `[ ${this.getCol(0).y}, ${this.getCol(1).y}, ${this.getCol(2).y}, ${this.getCol(3).y} ]\n`;
result += `[ ${this.getCol(0).z}, ${this.getCol(1).z}, ${this.getCol(2).z}, ${this.getCol(3).z} ]\n`;
result += `[ ${this.getCol(0).w}, ${this.getCol(1).w}, ${this.getCol(2).w}, ${this.getCol(3).w} ]\n`;
return result;
}
toStrN(n)
{
return this.toStringN(n);
}
toStringN(nDigs)
{
let result = '';
let xVec=this.getCol(0).clone(),
yVec=this.getCol(1).clone(),
zVec=this.getCol(2).clone(),
wVec=this.getCol(3).clone();
let vs=[xVec,yVec,zVec,wVec];
for (var i=0,n=vs.length; i<n; i++)
{
vs[i].x = pad(vs[i].x, nDigs);
vs[i].y = pad(vs[i].y, nDigs);
vs[i].z = pad(vs[i].z, nDigs);
vs[i].w = pad(vs[i].w, nDigs);
}
result = `[ ${xVec.x}, ${yVec.x}, ${zVec.x}, ${wVec.x} ]\n`;
result += `[ ${xVec.y}, ${yVec.y}, ${zVec.y}, ${wVec.y} ]\n`;
result += `[ ${xVec.z}, ${yVec.z}, ${zVec.z}, ${wVec.z} ]\n`;
result += `[ ${xVec.w}, ${yVec.w}, ${zVec.w}, ${wVec.w} ]\n`;
return result;
}
asRows(nDigs=2)
{
let result = '',xVec=this.getCol(0),yVec=this.getCol(1),zVec=this.getCol(2),wVec=this.getCol(3);
result = `[${xVec.x.toFixed(nDigs)}, ${xVec.y.toFixed(nDigs)}, ${xVec.z.toFixed(nDigs)}, ${xVec.w.toFixed(nDigs)}]\n`;
result += `[${yVec.x.toFixed(nDigs)}, ${yVec.y.toFixed(nDigs)}, ${yVec.z.toFixed(nDigs)}, ${yVec.w.toFixed(nDigs)}]\n`;
result += `[${zVec.x.toFixed(nDigs)}, ${zVec.y.toFixed(nDigs)}, ${zVec.z.toFixed(nDigs)}, ${zVec.w.toFixed(nDigs)}]\n`;
result += `[${wVec.x.toFixed(nDigs)}, ${wVec.y.toFixed(nDigs)}, ${wVec.z.toFixed(nDigs)}, ${wVec.w.toFixed(nDigs)}]\n`;
return result;
}
transpose()
{
let X=this.getCol(0), Y=this.getCol(1), Z=this.getCol(2), W=this.getCol(3);
let tmp = new mat4(
new vec4(X.x,Y.x,Z.x,W.x),
new vec4(X.y,Y.y,Z.y,W.y),
new vec4(X.z,Y.z,Z.z,W.z),
new vec4(X.w,Y.w,Z.w,W.w),
);
this.setCol(0,X);
this.setCol(1,Y);
this.setCol(2,Z);
this.setCol(3,W);
return tmp; //this.copy(tmp);
}
inverse()
{
let X = this.getCol(0), Y = this.getCol(1), Z = this.getCol(2), W = this.getCol(3);
let m00=X.x, m01=X.y, m02=X.z, m03=X.w,
m10=Y.x, m11=Y.y, m12=Y.z, m13=Y.w,
m20=Z.x, m21=Z.y, m22=Z.z, m23=Z.w,
m30=W.x, m31=W.y, m32=W.z, m33=W.w;
let tmp_0=m22*m33, tmp_1=m32*m23, tmp_2=m12*m33,
tmp_3=m32*m13, tmp_4=m12*m23, tmp_5=m22*m13,
tmp_6=m02*m33, tmp_7=m32*m03, tmp_8=m02*m23,
tmp_9=m22*m03, tmp_10=m02*m13,tmp_11=m12*m03,
tmp_12=m20*m31,tmp_13=m30*m21,tmp_14=m10*m31,
tmp_15=m30*m11,tmp_16=m10*m21,tmp_17=m20*m11,
tmp_18=m00*m31,tmp_19=m30*m01,tmp_20=m00*m21,
tmp_21=m20*m01,tmp_22=m00*m11,tmp_23=m10*m01;
var t0 = (tmp_0 * m11 + tmp_3 * m21 + tmp_4 * m31) - (tmp_1 * m11 + tmp_2 * m21 + tmp_5 * m31);
var t1 = (tmp_1 * m01 + tmp_6 * m21 + tmp_9 * m31) - (tmp_0 * m01 + tmp_7 * m21 + tmp_8 * m31);
var t2 = (tmp_2 * m01 + tmp_7 * m11 + tmp_10 * m31) - (tmp_3 * m01 + tmp_6 * m11 + tmp_11 * m31);
var t3 = (tmp_5 * m01 + tmp_8 * m11 + tmp_11 * m21) - (tmp_4 * m01 + tmp_9 * m11 + tmp_10 * m21);
var d = 1.0 / (m00 * t0 + m10 * t1 + m20 * t2 + m30 * t3);
let Xo = new vec4(d*t0, d*t1, d*t2, d*t3);
// d * t0,
// d * t1,
// d * t2,
// d * t3,
let Yo = new vec4(
d * ((tmp_1 * m10 + tmp_2 * m20 + tmp_5 * m30) - (tmp_0 * m10 + tmp_3 * m20 + tmp_4 * m30)),
d * ((tmp_0 * m00 + tmp_7 * m20 + tmp_8 * m30) - (tmp_1 * m00 + tmp_6 * m20 + tmp_9 * m30)),
d * ((tmp_3 * m00 + tmp_6 * m10 + tmp_11 * m30) - (tmp_2 * m00 + tmp_7 * m10 + tmp_10 * m30)),
d * ((tmp_4 * m00 + tmp_9 * m10 + tmp_10 * m20) - (tmp_5 * m00 + tmp_8 * m10 + tmp_11 * m20))
);
let Zo = new vec4(
d * ((tmp_12 * m13 + tmp_15 * m23 + tmp_16 * m33) - (tmp_13 * m13 + tmp_14 * m23 + tmp_17 * m33)),
d * ((tmp_13 * m03 + tmp_18 * m23 + tmp_21 * m33) - (tmp_12 * m03 + tmp_19 * m23 + tmp_20 * m33)),
d * ((tmp_14 * m03 + tmp_19 * m13 + tmp_22 * m33) - (tmp_15 * m03 + tmp_18 * m13 + tmp_23 * m33)),
d * ((tmp_17 * m03 + tmp_20 * m13 + tmp_23 * m23) - (tmp_16 * m03 + tmp_21 * m13 + tmp_22 * m23))
);
let Wo = new vec4(
d * ((tmp_14 * m22 + tmp_17 * m32 + tmp_13 * m12) - (tmp_16 * m32 + tmp_12 * m12 + tmp_15 * m22)),
d * ((tmp_20 * m32 + tmp_12 * m02 + tmp_19 * m22) - (tmp_18 * m22 + tmp_21 * m32 + tmp_13 * m02)),
d * ((tmp_18 * m12 + tmp_23 * m32 + tmp_15 * m02) - (tmp_22 * m32 + tmp_14 * m02 + tmp_19 * m12)),
d * ((tmp_22 * m22 + tmp_16 * m02 + tmp_21 * m12) - (tmp_20 * m12 + tmp_23 * m22 + tmp_17 * m02))
);
this.columns = [Xo,Yo,Zo,Wo];
return this;
}
}
function pad(num, n)
{
let str = num.toFixed(n);
if (num >= 0)
str = " " + str;
return str;
}
canvas
{
background-color: #333;
cursor: crosshair;
}
<body>
<canvas width='300' height='300'></canvas><br>
<div>Screen Coords of mouse: <span id='rawMouse'></span></div>
<div>(2d) NDC of mouse: <span id='transMouse'></span></div>
</body>
I've put explanations as comments in the code
// Canvas viewport (4:3)
const DRAW_WIDTH = 800;
const DRAW_HEIGHT = 600;
const RECT_SIZE = 10;
const RECT_FILL = 'black';
let canvas, ctx;
function init() {
canvas = document.querySelector("canvas");
// setup canvas drawing space
// this will give an aspect-ratio to the canvas
canvas.setAttribute('width', DRAW_WIDTH);
canvas.setAttribute('height', DRAW_HEIGHT);
ctx = canvas.getContext('2d');
// attach listener
canvas.addEventListener("click", onMouseDown);
}
function onMouseDown(e) {
// get canvas position and size infos:
const bbox = canvas.getBoundingClientRect();
const {
x: canvasX,
y: canvasY,
width: canvasW,
height: canvasH
} = bbox;
// mouse click position
const {
clientX: mouseX,
clientY: mouseY
} = e;
// compute ratio between drawing size (viewport) and actual size
const widthRatio = DRAW_WIDTH / canvasW;
// compute x relative to your canvas
const relativeX = (mouseX - canvasX);
// I advise you to use int values when drawing on canvas
// thus Math.round
const finalX = Math.round(widthRatio * relativeX);
// same for Y-axis
const heightRatio = DRAW_HEIGHT / canvasH;
const relativeY = (mouseY - canvasY);
const finalY = Math.round(heightRatio * relativeY);
// draw something with that:
ctx.fillStyle = RECT_FILL;
ctx.rect(finalX - RECT_SIZE / 2, finalY - RECT_SIZE / 2, RECT_SIZE, RECT_SIZE);
ctx.fill();
ctx.closePath();
}
init();
/* set canvas width in the document */
canvas {
width: 80vw;
margin-left: 5vw;
background: coral;
display: block;
}
<canvas></canvas>
Related
"Uncaught TypeError: Cannot read properties of undefined (reading '310')" when running Canvas animation
I'm working on another tunnel effect demo. This time I'm trying to make the tunnel move within the image. However, the function that handles rendering the tunnel always throws an error, and I'm not entirely sure why: function draw(time) { let animation = time / 1000.0; let shiftX = ~~(texWidth * animation); let shiftY = ~~(texHeight * 0.25 * animation); let shiftLookX = (screenWidth / 2) + ~~(screenWidth / 2 * Math.sin(animation)) let shiftLookY = (screenHeight / 2) + ~~(screenHeight / 2 * Math.sin(animation)) for (y = 0; y < buffer.height; y++) { for (x = 0; x < buffer.width; x++) { let id = (y * buffer.width + x) * 4; let d = ~~(distanceTable[y + shiftLookY][x + shiftLookX] + shiftX) % texWidth; let a = ~~(angleTable[y + shiftLookY][x + shiftLookX] + shiftY) % texHeight; let tex = (a * texture.width + d) * 4; buffer.data[id] = texture.data[tex]; buffer.data[id+1] = texture.data[tex+1]; buffer.data[id+2] = texture.data[tex+2]; buffer.data[id+3] = texture.data[tex+3]; } } ctx.putImageData(buffer, 0, 0); window.requestAnimationFrame(draw); } The rest of the code is viewable here, just in case the problem happens to be somewhere else. I have identified a possible cause -- if the first index used to read from distanceTable or angleTable is anything other than y, the error appears, even if it's simply a value being added to y. Unfortunately, I haven't figured out what causes it, or why the second index isn't affected by this. I've also searched for similar questions, but it seems like the people asking them all got this error for different reasons, so I'm kind of stuck.
It appears that setting the for loops to use the canvas' height and width as the upper limit instead of the pixel buffer's width and height was enough to fix it. I have absolutely no idea why, though. Was it because the buffer was twice the size of the canvas? var texWidth = 256; var texHeight = 256; var screenWidth = 640; var screenHeight = 480; var canvas = document.createElement('canvas'); canvas.width = screenWidth; canvas.height = screenHeight; var ctx = canvas.getContext("2d"); var texture = new ImageData(texWidth, texHeight); var distanceTable = []; var angleTable = []; var buffer = new ImageData(canvas.width * 2, canvas.height * 2); for (let y = 0; y < texture.height; y++) { for (let x = 0; x < texture.width; x++) { let id = (y * texture.width + x) * 4; let c = x ^ y; texture.data[id] = c; texture.data[id+1] = c; texture.data[id+2] = c; texture.data[id+3] = 255; } } for (let y = 0; y < buffer.height; y++) { distanceTable[y] = []; angleTable[y] = []; let sqy = Math.pow(y - canvas.height, 2); for (let x = 0; x < buffer.width; x++) { let sqx = Math.pow(x - canvas.width, 2); let ratio = 32.0; let distance = ~~(ratio * texHeight / Math.sqrt(sqx + sqy)) % texHeight; let angle = Math.abs(~~(0.5 * texWidth * Math.atan2(y - canvas.height, x - canvas.width)) / Math.PI); distanceTable[y][x] = distance; angleTable[y][x] = angle; } } function draw(time) { let animation = time / 1000.0; let shiftX = ~~(texWidth * animation); let shiftY = ~~(texHeight * 0.25 * animation); let shiftLookX = (screenWidth / 2) + ~~(screenWidth / 2 * Math.sin(animation)) let shiftLookY = (screenHeight / 2) + ~~(screenHeight / 2 * Math.sin(animation * 2.0)) for (y = 0; y < canvas.height; y++) { for (x = 0; x < canvas.width; x++) { let id = (y * buffer.width + x) * 4; let d = ~~(distanceTable[y + shiftLookY][x + shiftLookX] + shiftX) % texWidth; let a = ~~(angleTable[y + shiftLookY][x + shiftLookX] + shiftY) % texHeight; let tex = (a * texture.width + d) * 4; buffer.data[id] = texture.data[tex]; buffer.data[id+1] = texture.data[tex+1]; buffer.data[id+2] = texture.data[tex+2]; buffer.data[id+3] = texture.data[tex+3]; } } ctx.putImageData(buffer, 0, 0); window.requestAnimationFrame(draw); } document.body.appendChild(canvas); window.requestAnimationFrame(draw);
Converting Three.js from vanilla JavaScript to React Three Fiber
I've been trying to convert three.js written in vanilla JS to React Three fiber. `import * as THREE from 'three'; let scene, camera, renderer; //Canvas const canvas = document.querySelector('canvas') //Number of line particles let lineCount = 6000; //adding buffer Geometry let geom = new THREE.BufferGeometry(); //Giving the Buffer Geometry attributes geom.setAttribute('position', new THREE.BufferAttribute(new Float32Array(6 * lineCount), 3)); geom.setAttribute('velocity', new THREE.BufferAttribute(new Float32Array(2 * lineCount), 1)); //creating array variable for the position let pos = geom.getAttribute('position'); let posArray = pos.array; //creating array variable for the velocity let vel = geom.getAttribute('velocity'); let velArray = vel.array; //function to initiate const init = () => { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 1, 500); camera.position.z = 200; renderer = new THREE.WebGLRenderer({antialias: true, canvas: canvas}); renderer.setSize(window.innerWidth, window.innerHeight); for (let lineIndex = 0; lineIndex < lineCount; lineIndex++){ let x = Math.random() * 400 - 200; let y = Math.random() * 200 - 100; let z = Math.random() * 500 - 100; let xx = x; let yy = y; let zz = z; //line starting position posArray[6 * lineIndex] = x; posArray[6 * lineIndex + 1] = y; posArray[6 * lineIndex + 2] = z; //line ending position posArray[6 * lineIndex + 3] = xx; posArray[6 * lineIndex + 4] = yy; posArray[6 * lineIndex + 5] = zz; velArray[2 * lineIndex] = velArray[2 * lineIndex + 1] = 0; } let lineMat = new THREE.LineBasicMaterial({color: '#ffffff'}); let lines = new THREE.LineSegments(geom, lineMat); scene.add(lines); window.addEventListener('resize', () => { camera.aspect = window.innerWidth/ window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }, false); animate(); } const animate = () => { for (let lineIndex = 0; lineIndex < lineCount; lineIndex++) { velArray[2 * lineIndex] += 0.03; velArray[2 * lineIndex + 1] += 0.025; posArray[6 * lineIndex + 2] += velArray[2 * lineIndex]; posArray[6 * lineIndex + 5] += velArray[2 * lineIndex + 1]; if (posArray[6 * lineIndex + 5] > 200) { let z = Math.random() * 200 - 100; posArray[6 * lineIndex + 2] = z; posArray[6 * lineIndex + 5] = z; velArray[2 * lineIndex] = 0; velArray[2 * lineIndex + 1] = 0; } } pos.needsUpdate = true; renderer.render(scene, camera); requestAnimationFrame(animate); } init(); ` I'm having difficulty in converting the init and animate function. This is what I have so far, I've added the positions and velocity as bufferAttributes and specified the starting and ending coordinates in the for loop const StarLine = () => { const warpFieldMesh = useRef(); const bufAtPos = useRef(); const bufAtVel = useRef(); const count = 100; const [positions, velocity] = useMemo(() => { let positions = [] let velocity = [] for (let lineIndex = 0; lineIndex < count; lineIndex++){ let x = Math.random() * 400 - 200; let y = Math.random() * 200 - 100; let z = Math.random() * 500 - 100; let xx = x; let yy = y; let zz = z; //line starting position positions[6 * lineIndex] = x; positions[6 * lineIndex + 1] = y; positions[6 * lineIndex + 2] = z; //line ending position positions[6 * lineIndex + 3] = xx; positions[6 * lineIndex + 4] = yy; positions[6 * lineIndex + 5] = zz; velocity[2 * lineIndex] = velocity[2 * lineIndex + 1] = 0; } return [new Float32Array(positions), new Float32Array(velocity)] }, []) useFrame(() => { }) return ( <line ref={warpFieldMesh}> <bufferGeometry attach="geometry"> <bufferAttribute ref={bufAtPos} attachObject={["attributes", "position"]} count={positions.length / 3} array={positions} itemSize={3} /> <bufferAttribute ref={bufAtVel} attachObject={["attributes", "velocity"]} count={velocity.length / 2} array={velocity} itemSize={1} /> </bufferGeometry> <lineBasicMaterial attach="material" color={'#ffffff'} /> </line> ) } The vanilla JavaScript produces If anyone could help, that would be great!
The stuff in your animate function can go in the useFrame hook, its a r3f hook that is called every frame
Draw a line profile for an image using canvas html5
I am trying to draw intensity profile for an image with x axis as the length of the line on the image and the y-axis with intensity values along the length of the line. How can i do this on html 5 canvas? I tried the below code but I am not getting the right intensity values. Not sure where i am going wrong. private getLineIntensityVals = function (lineObj, img) { const slope = this.calculateSlopeOfLine(lineObj.upPos, lineObj.downPos); const intercept = this.calculateIntercept(lineObj.downPos, slope); const ctx = img.getContext('2d'); const coordinates = []; const intensities = []; for (let x = lineObj.downPos.x; x <= lineObj.upPos.x; x++) { const y = slope * x + intercept; const pixelData = ctx.getImageData(x, y, 1, 1).data; pixelData[0] = 255 - pixelData[0]; pixelData[1] = 255 - pixelData[1]; pixelData[2] = 255 - pixelData[2]; const intensity = ((0.299 * pixelData[0]) + (0.587 * pixelData[1]) + (0.114 * pixelData[2])); intensities.push(intensity); } return intensities; }; private calculateSlopeOfLine = function (upPos, downPos) { if (upPos.x === downPos.x || upPos.y === downPos.y) { return null; } return (downPos.y - upPos.y) / (downPos.x - upPos.x); }; private calculateIntercept = function (startPoint, slope) { if (slope === null) { return startPoint.x; } return startPoint.y - slope * startPoint.x; }; private calculateLineLength(line) { const dim = {width: Math.abs(line.downPos.x -line.upPos.x),height:Math.abs(line.downPos.y- line.upPos.y)}; length = Math.sqrt(Math.pow(dim.width, 2) + Math.pow(dim.height, 2)); return length; };
Image data Don't get the image data one pixel at a time. Gaining access to pixel data is expensive (CPU cycles), and memory is cheap. Get all the pixels once and reuse that data. Sampling the data Most lines will not fit into pixels evenly. To solve divide the line into the number of samples you want (You can use the line length) Then step to each sample in turn getting the 4 neighboring pixels values and interpolating the color at the sample point. As we are interpolating we need to ensure that we do not use the wrong color model. In this case we use sRGB. We thus get the function // imgData is the pixel date // x1,y1 and x2,y2 are the line end points // sampleRate is number of samples per pixel // Return array 3 values for each sample. function getProfile(imgData, x1, y1, x2, y2, sampleRate) { // convert line to vector const dx = x2 - x1; const dy = y2 - y1; // get length and calculate number of samples for sample rate const samples = (dx * dx + dy * dy) ** 0.5 * Math.abs(sampleRate) + 1 | 0; // Divide line vector by samples to get x, and y step per sample const nx = dx / samples; const ny = dy / samples; const w = imgData.width; const h = imgData.height; const pixels = imgData.data; const values = []; // Offset line to center of pixel var x = x1 + 0.5; var y = y1 + 0.5; var i = samples; while (i--) { // for each sample // make sure we are in the image if (x >= 0 && x < w - 1 && y >= 0 && y < h - 1) { // get 4 closest pixel indexes const idxA = ((x | 0) + (y | 0) * w) * 4; const idxB = ((x + 1 | 0) + (y | 0) * w) * 4; const idxC = ((x + 1 | 0) + (y + 1 | 0) * w) * 4; const idxD = ((x | 0) + (y + 1 | 0) * w) * 4; // Get channel data using sRGB approximation const r1 = pixels[idxA] ** 2.2; const r2 = pixels[idxB] ** 2.2; const r3 = pixels[idxC] ** 2.2; const r4 = pixels[idxD] ** 2.2; const g1 = pixels[idxA + 1] ** 2.2; const g2 = pixels[idxB + 1] ** 2.2; const g3 = pixels[idxC + 1] ** 2.2; const g4 = pixels[idxD + 1] ** 2.2; const b1 = pixels[idxA + 2] ** 2.2; const b2 = pixels[idxB + 2] ** 2.2; const b3 = pixels[idxC + 2] ** 2.2; const b4 = pixels[idxD + 2] ** 2.2; // find value at location via linear interpolation const xf = x % 1; const yf = y % 1; const rr = (r2 - r1) * xf + r1; const gg = (g2 - g1) * xf + g1; const bb = (b2 - b1) * xf + b1; /// store channels as uncompressed sRGB values.push((((r3 - r4) * xf + r4) - rr) * yf + rr); values.push((((g3 - g4) * xf + g4) - gg) * yf + gg); values.push((((b3 - b4) * xf + b4) - bb) * yf + bb); } else { // outside image values.push(0,0,0); } // step to next sample x += nx; y += ny; } return values; } Conversion to values The array hold raw sample data. There are a variety of ways to convert to a value. That is why we separate the sampling from the conversion to values. The next function takes the raw sample array and converts it to values. It returns an array of values. While it is doing the conversion it also get the max value so that the data can be plotted to fit a graph. function convertToMean(values) { var i = 0, v; const results = []; results._max = 0; while (i < values.length) { results.push(v = (values[i++] * 0.299 + values[i++] * 0.587 + values[i++] * 0.114) ** (1/2.2)); results._max = Math.max(v, results._max); } return results; } Now you can plot the data how you like. Example Click drag line on image (when loaded) Results are plotted real time. Move mouse over plot to see values. Use full page to see all. const ctx = canvas.getContext("2d"); const ctx1 = canvas1.getContext("2d"); const SCALE_IMAGE = 0.5; const PLOT_WIDTH = 500; const PLOT_HEIGHT = 150; canvas1.width = PLOT_WIDTH; canvas1.height = PLOT_HEIGHT; const line = {x1: 0, y1: 0, x2: 0, y2:0, canUse: false, haveData: false, data: undefined}; var bounds, bounds1, imgData; // ix iy image coords, px, py plot coords const mouse = {ix: 0, iy: 0, overImage: false, px: 0, py:0, overPlot: false, button : false, dragging: 0}; ["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents)); const img = new Image; img.crossOrigin = "Anonymous"; img.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Black_and_yellow_garden_spider%2C_Washington_DC.jpg/800px-Black_and_yellow_garden_spider%2C_Washington_DC.jpg"; img.addEventListener("load",() => { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img,0,0); imgData = ctx.getImageData(0,0,ctx.canvas.width, ctx.canvas.height); canvas.width = img.width * SCALE_IMAGE; canvas.height = img.height * SCALE_IMAGE; bounds = canvas.getBoundingClientRect(); bounds1 = canvas1.getBoundingClientRect(); requestAnimationFrame(update); },{once: true}); function getProfile(imgData, x1, y1, x2, y2, sampleRate) { x1 *= 1 / SCALE_IMAGE; y1 *= 1 / SCALE_IMAGE; x2 *= 1 / SCALE_IMAGE; y2 *= 1 / SCALE_IMAGE; const dx = x2 - x1; const dy = y2 - y1; const samples = (dx * dx + dy * dy) ** 0.5 * Math.abs(sampleRate) + 1 | 0; const nx = dx / samples; const ny = dy / samples; const w = imgData.width; const h = imgData.height; const pixels = imgData.data; const values = []; var x = x1 + 0.5; var y = y1 + 0.5; var i = samples; while (i--) { if (x >= 0 && x < w - 1 && y >= 0 && y < h - 1) { // get 4 closest pixel indexs const idxA = ((x | 0) + (y | 0) * w) * 4; const idxB = ((x + 1 | 0) + (y | 0) * w) * 4; const idxC = ((x + 1 | 0) + (y + 1 | 0) * w) * 4; const idxD = ((x | 0) + (y + 1 | 0) * w) * 4; // Get channel data using sRGB approximation const r1 = pixels[idxA] ** 2.2; const r2 = pixels[idxB] ** 2.2; const r3 = pixels[idxC] ** 2.2; const r4 = pixels[idxD] ** 2.2; const g1 = pixels[idxA + 1] ** 2.2; const g2 = pixels[idxB + 1] ** 2.2; const g3 = pixels[idxC + 1] ** 2.2; const g4 = pixels[idxD + 1] ** 2.2; const b1 = pixels[idxA + 2] ** 2.2; const b2 = pixels[idxB + 2] ** 2.2; const b3 = pixels[idxC + 2] ** 2.2; const b4 = pixels[idxD + 2] ** 2.2; // find value at location via linear interpolation const xf = x % 1; const yf = y % 1; const rr = (r2 - r1) * xf + r1; const gg = (g2 - g1) * xf + g1; const bb = (b2 - b1) * xf + b1; /// store channels as uncompressed sRGB values.push((((r3 - r4) * xf + r4) - rr) * yf + rr); values.push((((g3 - g4) * xf + g4) - gg) * yf + gg); values.push((((b3 - b4) * xf + b4) - bb) * yf + bb); } else { // outside image values.push(0,0,0); } x += nx; y += ny; } values._nx = nx; values._ny = ny; values._x = x1; values._y = y1; return values; } function convertToMean(values) { var i = 0, max = 0, v; const results = []; while (i < values.length) { results.push(v = (values[i++] * 0.299 + values[i++] * 0.587 + values[i++] * 0.114) ** (1/2.2)); max = Math.max(v, max); } results._max = max; results._nx = values._nx; results._ny = values._ny; results._x = values._x; results._y = values._y; return results; } function plotValues(ctx, values) { const count = values.length; const scaleX = ctx.canvas.width / count; // not using max in example // const scaleY = (ctx.canvas.height-3) / values._max; const scaleY = (ctx.canvas.height-3) / 255; ctx1.clearRect(0,0, ctx.canvas.width, ctx.canvas.height); var i = 0; ctx.beginPath(); ctx.strokeStyle = "#000"; ctx.lineWidth = 2; while (i < count) { const y = ctx.canvas.height - values[i] * scaleY + 1; ctx.lineTo(i++ * scaleX, y); } ctx.stroke(); if (!mouse.button && mouse.overPlot) { ctx.fillStyle = "#f008"; ctx.fillRect(mouse.px, 0, 1, ctx.canvas.height); const val = values[mouse.px / scaleX | 0]; info.textContent = "Value: " + (val !== undefined ? val.toFixed(2) : ""); } } function update() { ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height); ctx.drawImage(img, 0, 0, img.width * SCALE_IMAGE, img.height * SCALE_IMAGE); var atSample = 0; if (!mouse.button) { if (line.canUse) { if (line.haveData && mouse.overPlot) { const count = line.data.length; const scaleX = ctx1.canvas.width / count atSample = mouse.px / scaleX; } } } if (mouse.button) { if (mouse.dragging === 1) { // dragging line line.x2 = mouse.ix; line.y2 = mouse.iy; line.canUse = true; line.haveData = false; } else if(mouse.overImage) { mouse.dragging = 1; line.x1 = mouse.ix; line.y1 = mouse.iy; line.canUse = false; line.haveData = false; canvas.style.cursor = "none"; } } else { mouse.dragging = 0; canvas.style.cursor = "crosshair"; } if (line.canUse) { ctx.strokeStyle = "#F00"; ctx.strokeWidth = 2; ctx.beginPath(); ctx.lineTo(line.x1, line.y1); ctx.lineTo(line.x2, line.y2); ctx.stroke(); if (atSample) { ctx.fillStyle = "#FF0"; ctx.beginPath(); ctx.arc( (line.data._x + line.data._nx * atSample) * SCALE_IMAGE, (line.data._y + line.data._ny * atSample) * SCALE_IMAGE, line.data[atSample | 0] / 32, 0, Math.PI * 2 ); ctx.fill(); } if (!line.haveData) { const vals = getProfile(imgData, line.x1, line.y1, line.x2, line.y2, 1); line.data = convertToMean(vals); line.haveData = true; plotValues(ctx1, line.data); } else { plotValues(ctx1, line.data); } } requestAnimationFrame(update); } function mouseEvents(e){ if (bounds) { mouse.ix = e.pageX - bounds.left; mouse.iy = e.pageY - bounds.top; mouse.overImage = mouse.ix >= 0 && mouse.ix < bounds.width && mouse.iy >= 0 && mouse.iy < bounds.height; mouse.px = e.pageX - bounds1.left; mouse.py = e.pageY - bounds1.top; mouse.overPlot = mouse.px >= 0 && mouse.px < bounds1.width && mouse.py >= 0 && mouse.py < bounds1.height; } mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button; } canvas { border: 2px solid black; } <canvas id="canvas"></canvas> <div id="info">Click drag line over image</div> <canvas id="canvas1"></canvas> Image source: https://commons.wikimedia.org/w/index.php?curid=93680693 By BethGuay - Own work, CC BY-SA 4.0,
Significant error when approximating elliptical arcs with bezier curves on canvas with javascript
I'm trying to convert svg path to canvas in javascript, however it's really hard to map svg path elliptical arcs to canvas path. One of the ways is to approximate using multiple bezier curves. I have successfully implemented the approximation of elliptical arcs with bezier curves however the approximation isn't very accurate. My code: var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); canvas.width = document.body.clientWidth; canvas.height = document.body.clientHeight; ctx.strokeWidth = 2; ctx.strokeStyle = "#000000"; function clamp(value, min, max) { return Math.min(Math.max(value, min), max) } function svgAngle(ux, uy, vx, vy ) { var dot = ux*vx + uy*vy; var len = Math.sqrt(ux*ux + uy*uy) * Math.sqrt(vx*vx + vy*vy); var ang = Math.acos( clamp(dot / len,-1,1) ); if ( (ux*vy - uy*vx) < 0) ang = -ang; return ang; } function generateBezierPoints(rx, ry, phi, flagA, flagS, x1, y1, x2, y2) { var rX = Math.abs(rx); var rY = Math.abs(ry); var dx2 = (x1 - x2)/2; var dy2 = (y1 - y2)/2; var x1p = Math.cos(phi)*dx2 + Math.sin(phi)*dy2; var y1p = -Math.sin(phi)*dx2 + Math.cos(phi)*dy2; var rxs = rX * rX; var rys = rY * rY; var x1ps = x1p * x1p; var y1ps = y1p * y1p; var cr = x1ps/rxs + y1ps/rys; if (cr > 1) { var s = Math.sqrt(cr); rX = s * rX; rY = s * rY; rxs = rX * rX; rys = rY * rY; } var dq = (rxs * y1ps + rys * x1ps); var pq = (rxs*rys - dq) / dq; var q = Math.sqrt( Math.max(0,pq) ); if (flagA === flagS) q = -q; var cxp = q * rX * y1p / rY; var cyp = - q * rY * x1p / rX; var cx = Math.cos(phi)*cxp - Math.sin(phi)*cyp + (x1 + x2)/2; var cy = Math.sin(phi)*cxp + Math.cos(phi)*cyp + (y1 + y2)/2; var theta = svgAngle( 1,0, (x1p-cxp) / rX, (y1p - cyp)/rY ); var delta = svgAngle( (x1p - cxp)/rX, (y1p - cyp)/rY, (-x1p - cxp)/rX, (-y1p-cyp)/rY); delta = delta - Math.PI * 2 * Math.floor(delta / (Math.PI * 2)); if (!flagS) delta -= 2 * Math.PI; var n1 = theta, n2 = delta; // E(n) // cx +acosθcosη−bsinθsinη // cy +asinθcosη+bcosθsinη function E(n) { var enx = cx + rx * Math.cos(phi) * Math.cos(n) - ry * Math.sin(phi) * Math.sin(n); var eny = cy + rx * Math.sin(phi) * Math.cos(n) + ry * Math.cos(phi) * Math.sin(n); return {x: enx,y: eny}; } // E'(n) // −acosθsinη−bsinθcosη // −asinθsinη+bcosθcosη function Ed(n) { var ednx = -1 * rx * Math.cos(phi) * Math.sin(n) - ry * Math.sin(phi) * Math.cos(n); var edny = -1 * rx * Math.sin(phi) * Math.sin(n) + ry * Math.cos(phi) * Math.cos(n); return {x: ednx, y: edny}; } var n = []; n.push(n1); var interval = Math.PI/4; while(n[n.length - 1] + interval < n2) n.push(n[n.length - 1] + interval) n.push(n2); function getCP(n1, n2) { var en1 = E(n1); var en2 = E(n2); var edn1 = Ed(n1); var edn2 = Ed(n2); var alpha = Math.sin(n2 - n1) * (Math.sqrt(4 + 3 * Math.pow(Math.tan((n2 - n1)/2), 2)) - 1)/3; console.log(en1, en2); return { cpx1: en1.x + alpha*edn1.x, cpy1: en1.y + alpha*edn1.y, cpx2: en2.x - alpha*edn2.x, cpy2: en2.y - alpha*edn2.y, en1: en1, en2: en2 }; } var cps = [] for(var i = 0; i < n.length - 1; i++) { cps.push(getCP(n[i],n[i+1])); } return cps; } // M100,200 ctx.moveTo(100,200) // a25,100 -30 0,1 50,-25 var rx = 25, ry=100 ,phi = -30 * Math.PI / 180, fa = 0, fs = 1, x = 100, y = 200, x1 = x + 50, y1 = y - 25; var cps = generateBezierPoints(rx, ry, phi, fa, fs, x, y, x1, y1); var limit = 4; for(var i = 0; i < limit && i < cps.length; i++) { ctx.bezierCurveTo(cps[i].cpx1, cps[i].cpy1, cps[i].cpx2, cps[i].cpy2, i < limit - 1 ? cps[i].en2.x : x1, i < limit - 1 ? cps[i].en2.y : y1); } ctx.stroke() With the result: The red line represents the svg path elliptical arc and the black line represents the approximation How can I accurately draw any possible elliptical arc on canvas? Update: Forgot to mention the original source of the algorithm: https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/
So both bugs are simply: n2 should be declare n2 = theta + delta; The E and Ed functions should use rX rY rather than rx ry. And that fixes everything. Though the original should have obviously opted to divide up the arcs into equal sized portions rather than pi/4 sized elements and then appending the remainder. Just find out how many parts it will need, then divide the range into that many parts of equal size, seems like a much more elegant solution, and because error goes up with length it would also be more accurate. See: https://jsfiddle.net/Tatarize/4ro0Lm4u/ for working version. It's not just off in that one respect it doesn't work most anywhere. You can see that depending on phi, it does a lot of variously bad things. It's actually shockingly good there. But, broken everywhere else too. https://jsfiddle.net/Tatarize/dm7yqypb/ The reason is that the declaration of n2 is wrong and should read: n2 = theta + delta; https://jsfiddle.net/Tatarize/ba903pss/ But, fixing the bug in the indexing, it clearly does not scale up there like it should. It might be that arcs within the svg standard are scaled up so that there can certainly be a solution whereas in the relevant code they seem like they are clamped. https://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters "If rx, ry and φ are such that there is no solution (basically, the ellipse is not big enough to reach from (x1, y1) to (x2, y2)) then the ellipse is scaled up uniformly until there is exactly one solution (until the ellipse is just big enough)." Testing this, since it does properly have code that should scale it up, I changed it green when that code got called. And it turns green when it screws up. So yeah, it's failure to scale for some reason: https://jsfiddle.net/Tatarize/tptroxho/ Which means something is using rx rather than the scaled rX and it's the E and Ed functions: var enx = cx + rx * Math.cos(phi) * Math.cos(n) - ry * Math.sin(phi) * Math.sin(n); These rx references must read rX and rY for ry. var enx = cx + rX * Math.cos(phi) * Math.cos(n) - rY * Math.sin(phi) * Math.sin(n); Which finally fixes the last bug, QED. https://jsfiddle.net/Tatarize/4ro0Lm4u/ I got rid of the canvas, moved everything to svg and animated it. var svgNS = "http://www.w3.org/2000/svg"; var svg = document.getElementById("svg"); var arcgroup = document.getElementById("arcgroup"); var curvegroup = document.getElementById("curvegroup"); function doArc() { while (arcgroup.firstChild) { arcgroup.removeChild(arcgroup.firstChild); } //clear old svg data. --> var d = document.createElementNS(svgNS, "path"); //var path = "M100,200 a25,100 -30 0,1 50,-25" var path = "M" + x + "," + y + "a" + rx + " " + ry + " " + phi + " " + fa + " " + fs + " " + " " + x1 + " " + y1; d.setAttributeNS(null, "d", path); arcgroup.appendChild(d); } function doCurve() { var cps = generateBezierPoints(rx, ry, phi * Math.PI / 180, fa, fs, x, y, x + x1, y + y1); while (curvegroup.firstChild) { curvegroup.removeChild(curvegroup.firstChild); } //clear old svg data. --> var d = document.createElementNS(svgNS, "path"); var limit = 4; var path = "M" + x + "," + y; for (var i = 0; i < limit && i < cps.length; i++) { if (i < limit - 1) { path += "C" + cps[i].cpx1 + " " + cps[i].cpy1 + " " + cps[i].cpx2 + " " + cps[i].cpy2 + " " + cps[i].en2.x + " " + cps[i].en2.y; } else { path += "C" + cps[i].cpx1 + " " + cps[i].cpy1 + " " + cps[i].cpx2 + " " + cps[i].cpy2 + " " + (x + x1) + " " + (y + y1); } } d.setAttributeNS(null, "d", path); d.setAttributeNS(null, "stroke", "#000"); curvegroup.appendChild(d); } setInterval(phiClock, 50); function phiClock() { phi += 1; doCurve(); doArc(); } doCurve(); doArc();
I am unable to understand the execution of following mathematical equation using JavasScript
I am unable to understand this line of code this.position = convertlatlonToVec3(cardinal.lat, cardinal.lon).multiplyScalar(radius); used in function labelBox. How does multiplyScalar(radius) works. function convertlatlonToVec3(lat, lon) { var cosLat = Math.cos(circle.getCenter().lat() * degrees); var sinLat = Math.sin(circle.getCenter().lat() * degrees); var xSphere = radiusEquator * cosLat; var ySphere = 0; var zSphere = radiusPoles * sinLat; var rSphere = Math.sqrt(xSphere*xSphere + ySphere*ySphere + zSphere*zSphere); var tmp = rSphere * Math.cos(lat * degrees); xSphere = tmp * Math.cos((lon - circle.getCenter().lng()) * degrees); ySphere = tmp * Math.sin((lon - circle.getCenter().lng()) * degrees); zSphere = rSphere * Math.sin(lat * degrees); var x = -ySphere/circle.getRadius(); var y = (zSphere*cosLat - xSphere*sinLat)/circle.getRadius(); var z = 0; return new THREE.Vector3(x, y, z); } function labelBox(cardinal, radius, root) { this.screenvector = new THREE.Vector3(0,0,0); this.labelID = 'MovingLabel'+ cardinal.ID; this.position = convertlatlonToVec3(cardinal.lat, cardinal.lon).multiplyScalar(radius); }
Three.js documentation here For THREE.Vector3 multiplyScalar method look docs are here: .multiplyScalar ( s ) this Multiplies this vector by scalar s. Contents of this method can be found in the THREE.Vector3 class and look like this: multiplyScalar: function ( scalar ) { if ( isFinite( scalar ) ) { this.x *= scalar; this.y *= scalar; this.z *= scalar; } else { this.x = 0; this.y = 0; this.z = 0; } return this; },