Published
Edited
Mar 23, 2020
4 stars
Insert cell
Insert cell
{
const math = await require('mathjs@6.2.1/dist/math.js');

// Cube vertex black magic
const vertices = [];
for(let i = 0; i < 8; i++) {
vertices.push([
Math.pow(-1, Math.floor(i / 2)),
Math.pow(-1, Math.floor(i / 2) + i),
Math.pow(-1, Math.floor(i / 4))
]);
}
// Pointy arrow black magic
let arrow = vertices.length;
vertices.push([0, 0, -1], [0, 0, -2.8], [0, 0, -3]);
for(let i = 0; i < 4; i++) {
vertices.push([
0.05 * Math.pow(-1, Math.floor(i / 2)),
0.05 * Math.pow(-1, Math.floor(i / 2) + i),
-2.8
]);
}
const segments = [];
for(let x = 0; x < 4; x++) {
for(let y = 0; y < 2; y++) {
segments.push([4 * y + x, 4 * y + ((x + 1) % 4)]);
}
segments.push([x, x + 4]);
}
segments.push([arrow, arrow + 1]);
arrow += 2;
const arrowTip = arrow++;
for(let i = 0; i < 4; i++) {
segments.push([arrowTip, arrow + i]);
segments.push([arrow + i, arrow + ((i + 1) % 4)]);
}

function rotateX(a) {
const c = Math.cos(a);
const s = Math.sin(a);
return [
[ 1, 0, 0, 0],
[ 0, c, -s, 0],
[ 0, s, c, 0],
[ 0, 0, 0, 1]
];
}

function rotateY(a) {
const c = Math.cos(a);
const s = Math.sin(a);
return [
[ c, 0, s, 0],
[ 0, 1, 0, 0],
[-s, 0, c, 0],
[ 0, 0, 0, 1]
];
}
function unit(v) {
return math.divide(v, math.norm(v));
}
function lookAt(eye, target, up) {
const zaxis = unit(math.subtract(eye, target)); // The "forward" vector.
const xaxis = unit(math.cross(up, zaxis)); // The "right" vector.
const yaxis = math.cross(zaxis, xaxis); // The "up" vector.

// Create a 4x4 view matrix from the right, up, forward and eye position vectors
const viewMatrix = [
[ xaxis[0], yaxis[0], zaxis[0], 0 ],
[ xaxis[1], yaxis[1], zaxis[1], 0 ],
[ xaxis[2], yaxis[2], zaxis[2], 0 ],
[-math.dot( xaxis, eye ), -math.dot( yaxis, eye ), -math.dot( zaxis, eye ), 1 ]
];

return viewMatrix;
}

function project(r, t, n, f) {
return [
[n/r, 0, 0, 0 ],
[0, n/t, 0, 0 ],
[0, 0, -(f+n)/(f-n), -(2*f*n)/(f-n) ],
[0, 0, -1, 0 ]
];
}
const projMat = project(0.5, 0.5, 1, 10);
const height = 512;
const context = DOM.context2d(width, height);

const scale = 0.5 * Math.min(width, height);
const canvasMat = [
[scale, 0, 0, 0.5 * width ],
[0, -scale, 0, 0.5 * height],
[0, 0, 1, 0 ],
[0, 0, 0, 1 ]
];

const translate = [0, 0, 4];
const tranMat = [
[1, 0, 0, translate[0]],
[0, 1, 0, translate[1]],
[0, 0, 1, translate[2]],
[0, 0, 0, 1 ]
];

context.fillStyle = 'black';
context.strokeStyle = 'black';
return Generators.observe(notify => {
const repaint = (mx, my) => {
const mp = math.multiply(
math.inv(math.multiply(
canvasMat,
projMat,
tranMat
)),
[mx, my, 0, 1]
);
const [lx, ly, lz] = mp;
const rotMat = math.inv(lookAt([0, 0, 0], [lx, ly, lz], [0, 1, 0]));
const outVertices = vertices
.map(v => {
const projected = math.multiply(
projMat,
tranMat,
rotMat,
[...v, 1]
);

let [, , , w] = projected;
return math.multiply(canvasMat, math.divide(projected, w));
})

context.clearRect(0, 0, width, height);
context.beginPath();
for(let [ai, bi] of segments) {
let [ax, ay] = outVertices[ai];
let [bx, by] = outVertices[bi];
context.moveTo(ax, ay);
context.lineTo(bx, by);
}
context.stroke();
context.fillText(`${lx.toFixed(2)} ${ly.toFixed(2)} ${lz.toFixed(2)}`, 0, 50);

notify(context.canvas);
};
const mouseMoved = event => {
const domRect = context.canvas.getBoundingClientRect();
repaint(
event.pageX - window.scrollX - domRect.left,
event.pageY - window.scrollY - domRect.top
);
};
window.addEventListener('mousemove', mouseMoved);
repaint(width / 2, height / 2);
return () => window.removeEventListener('mousemove', mouseMoved);
});
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more