11 KiB
title | description | sections | tags | scripts | styles | canonical_url | url_translated | title_translated | date | lang | |||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Spinning spatial cross | We are writing an algorithm for rotating a three-dimensional figure by an angle around its center along all three axes at once. In the previous example... |
|
|
|
|
/en/2023/01/16/spinning-spatial-cross.html | /ru/2023/01/15/spinning-spatial-cross.html | Вращаем пространственный крест | 2023.01.16 | en |
We are writing an algorithm for rotating a three-dimensional figure by an angle around its center along all three axes at once. In the previous example, we [rotated cube in space]({{ '/en/2023/01/11/spinning-cube-in-space.html' | relative_url }}) — now there are a lot of cubes, the algorithm is almost the same and we use the same formulas. We draw two variants of the figure: spatial cross and cross-cube in two types of projections, consider the difference.
Testing the experimental interface: [Volumetric tetris]({{ '/en/2023/01/22/volumetric-tetris.html' | relative_url }}).
Spatial cross
Your browser does not support Canvas
Your browser does not support Canvas
Cross-cube
Your browser does not support Canvas
Your browser does not support Canvas
Parallel projection — all cubes are the same size.
Perspective projection — the cubes look shrinking in the distance.
Experimental model
Slightly complicated version from the previous example — now there are a lot of cubes. In addition to the previous settings there can be changed: figure variant — spatial cross or cross-cube, face sorting direction — linear perspective or reverse perspective and transparency of the cube walls.
Your browser does not support Canvas
Algorithm description
We prepare a matrix of zeros and ones, where one means a cube in a certain place of the figure. Then we bypass this matrix and fill in the array of cubes with the corresponding coordinates of the vertices. After that, we start the rotation along all three axes at once. At each step, we bypass the array of cubes and get projections of their faces. Then we sort the array of faces by remoteness from the projection center, bypass this array and throw away the same pairs from it — these are the adjacent walls between neighboring cubes inside the figure. After that we draw cube faces with a translucent color — first the distant and then the near ones, so that the distant faces can be seen through the near ones.
Implementation in JavaScript
{% include classes-point-cube-en.md -%}
Create objects according to templates and draw their projections on the plane.
'use strict';
// matrices-templates for cubes
const shape1 = [ // spatial cross
[[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
[[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
[[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
[[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
[[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]]];
const shape2 = [ // cross-cube
[[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
[[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
[[1,1,1,1,1], [1,0,0,0,1], [1,0,0,0,1], [1,0,0,0,1], [1,1,1,1,1]],
[[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
[[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]]];
// cube size, number of cubes in a row, indent
const size = 40, row = 5, gap = 50;
// arrays for cubes
const cubes1 = [], cubes2 = [];
// bypass the matrices, fill the arrays with cubes
for (let x=0; x<row; x++)
for (let y=0; y<row; y++)
for (let z=0; z<row; z++) {
if (shape1[x][y][z]==1)
cubes1.push(new Cube(x*size+gap,y*size+gap,z*size+gap,size));
if (shape2[x][y][z]==1)
cubes2.push(new Cube(x*size+gap,y*size+gap,z*size+gap,size));
}
// figure center, we'll perform a rotation around it
const t0 = new Point(150,150,150);
// remoteness of the projection center
const d = 300;
// observer screen position
const tv = new Point(150,150,125);
// rotation angle in degrees
const deg = {x:1,y:1,z:1};
// we'll draw two pictures for each figure
const canvas1 = document.getElementById('canvas1');
const canvas2 = document.getElementById('canvas2');
const canvas3 = document.getElementById('canvas3');
const canvas4 = document.getElementById('canvas4');
// image refresh
function repaint() {
// spatial cross
processFigure(cubes1,canvas1,canvas2);
// cross-cube
processFigure(cubes2,canvas3,canvas4);
}
// rotate the figure and get projections
function processFigure(cubes,cnv1,cnv2) {
// arrays of projections of faces of cubes
let parallel = [], perspective = [];
// rotate the cubes and get projections
for (let cube of cubes) {
cube.rotate(deg, t0);
parallel = parallel.concat(cube.projection('parallel',tv,d));
perspective = perspective.concat(cube.projection('perspective',tv,d));
}
// we do not draw adjacent walls between neighboring cubes
noAdjacent(parallel);
noAdjacent(perspective);
// sort the faces of different cubes by remoteness and inside one cube by tilt
parallel.sort((a,b)=>Math.abs(b.dist-a.dist)>size ? b.dist-a.dist : b.clock-a.clock);
// sort the faces by remoteness from the projection center
perspective.sort((a,b)=>b.dist-a.dist);
// draw parallel projection
drawFigure(cnv1, parallel);
// draw perspective projection
drawFigure(cnv2, perspective);
}
// do not draw adjacent walls between neighboring cubes
function noAdjacent(array) {
// sort the faces by remoteness
array.sort((a,b) => b.dist-a.dist);
// remove the adjacent walls between cubes
for (let i=0, j=1; i<array.length-1; j=++i+1)
while (j<array.length && Cube.pEquidistant(array[i],array[j]))
if (Cube.pAdjacent(array[i],array[j])) {
array.splice(j,1);
array.splice(i,1);
i--; j=array.length;
} else j++;
}
// draw a figure by points from an array
function drawFigure(canvas, proj, alpha=0.8) {
const context = canvas.getContext('2d');
// clear the entire canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// bypass the array of cube faces
for (let i = 0; i < proj.length; i++) {
// bypass the array of points and link them with lines
context.beginPath();
for (let j = 0; j < proj[i].length; j++) {
if (j == 0) {
context.moveTo(proj[i][j].x, proj[i][j].y);
} else {
context.lineTo(proj[i][j].x, proj[i][j].y);
}
}
context.closePath();
// draw the face of the cube along with the edges
context.lineWidth = 1.9;
context.lineJoin = 'round';
context.fillStyle = 'rgba(200,230,201,'+alpha+')';
context.strokeStyle = 'rgba(102,187,106,'+(0.2+alpha)+')';
context.fill();
context.stroke();
}
}
// after loading the page, set the image refresh rate at 20 Hz
document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));