We coded a spinning 3d shape in js. There's a flicker in the render of the top triangle, we think it's because the z sorting is not working correctly. How do we resolve this?
Here's a jsfiddle.
Here's the z sorting code:
// z sorting
// dots_for_rendering.sort((a,b) => Math.sqrt((b.x)**2 + (b.y)**2) - Math.sqrt((a.x)**2 + (a.y)**2))
for (var i = 0; i < polygons.length; i++) {
polygons[i].maxz = -Infinity;
polygons[i].minz = Infinity;
polygons[i].midz = 0;
for (var j = 0; j < polygons[i].verticies.length; j++) {
var z = rotated_verticies[polygons[i].verticies[j]].vector[2];
if (z > polygons[i].maxz) {
polygons[i].maxz = z;
}
if (z < polygons[i].minz) {
polygons[i].minz = z;
}
polygons[i].midz += z;
}
polygons[i].midz /= polygons[i].verticies.length;
}
polygons.sort((a, b) => b.midz - a.midz)
// polygons.sort((a,b) => Math.max(b.maxz - a.minz, b.minz - a.maxz))
// polygons.sort((a,b) => {
// if (a.minz < b.maxz) {
// return 0;
// }
// if (b.minz < a.maxz) {
// return -1;
// }
// return 0;
// })
Here's a code snippet:
class Tensor {
constructor(){
var input = this.takeInput(...arguments);
this.vector = input;
}
takeInput() {
var a = true;
for (var arg of arguments) {
if (typeof arg !== "number"){
a = false
}
}
if (a && arguments[2] !== true){
return new Array(...arguments);
}
else {
if (arguments[0] instanceof Tensor){
return arguments[0].vector;
}
else {
if (typeof arguments[0] === "number" && typeof arguments[1] === "number" && arguments[2] === true) {
var res = [];
for (var i = 0; i < arguments[0]; i++) {
res.push(arguments[1]);
}
return res;
}
}
}
}
// used for + - * /
change(f, input){
for (var i in this.vector) {
this.vector[i] = f(this.vector[i], input[i]);
}
return this;
}
copy() {
return new Tensor(...this.vector);
}
dimentions() {
return this.vector.length;
}
//-----------
len() {
var s = 0;
for (var dim of this.vector) {
s += dim ** 2;
}
return Math.sqrt(s);
}
norm() {
return this.div(this.dimentions(), this.len(), true)
}
add() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x + y, input);
}
sub() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x - y, input);
}
mult() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x * y, input);
}
div() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x / y, input);
}
dot() {
var input = this.takeInput(...arguments);
var res = 0;
for (var i in this.vector) {
res += this.vector[i] * input[i]
}
return res;
}
rotate() {
// WARNING: only for 3D currently!!!
var input = this.takeInput(...arguments);
var [x, y, z] = this.vector;
// rotate Z
var t_x = x * Math.cos(input[2]) - y * Math.sin(input[2])
y = y * Math.cos(input[2]) + x * Math.sin(input[2])
x = t_x
// rotate X
var t_y = y * Math.cos(input[0]) - z * Math.sin(input[0])
z = z * Math.cos(input[0]) + y * Math.sin(input[0])
y = t_y
// rotate Y
t_x = x * Math.cos(input[1]) + z * Math.sin(input[1])
z = z * Math.cos(input[1]) - x * Math.sin(input[1])
x = t_x
this.vector = [x, y, z];
return this;
}
}
var canvas = document.getElementById('canvas')
var ctx = canvas.getContext("2d")
w = 300
h = 286
fov = 0.1
scale = 65;
offset = new Tensor(w / 2 - 5, h / 2 - 92, 0.1);
light = new Tensor(3.5, 0.5, 1).norm();
canvas.width = w;
canvas.height = h;
var verticies = [];
verticies.push(new Tensor(0.5, 1, 0))
verticies.push(new Tensor(0.5, -1, 0))
verticies.push(new Tensor(-1, 0, 0))
verticies.push(new Tensor(0, 0, 2))
var polygons = [];
polygons.push({
verticies: [0, 3, 1],
color: 'red',
nf: 1
});
polygons.push({
verticies: [2, 3, 0],
color: 'blue',
nf: 1
});
polygons.push({
verticies: [2, 3, 1],
color: 'green',
nf: -1
});
polygons.push({
verticies: [0, 1, 2],
color: 'yellow',
nf: -1
});
for (var i = 0; i < polygons.length; i++) {
polygons[i].id = i;
}
theta = new Tensor(1.5 * Math.PI, 0, 1.5 * Math.PI);
function loop() {
ctx.clearRect(0, 0, w, h);
rotated_verticies = [];
for (var i = 0; i < verticies.length; i++) {
rotated_verticies.push(verticies[i].copy().rotate(theta));
}
// z sorting
// dots_for_rendering.sort((a,b) => Math.sqrt((b.x)**2 + (b.y)**2) - Math.sqrt((a.x)**2 + (a.y)**2))
for (var i = 0; i < polygons.length; i++) {
polygons[i].maxz = -Infinity;
polygons[i].minz = Infinity;
polygons[i].midz = 0;
for (var j = 0; j < polygons[i].verticies.length; j++) {
var z = rotated_verticies[polygons[i].verticies[j]].vector[2];
// z += 1 * (Math.random() * 2 - 1)
if (z > polygons[i].maxz) {
polygons[i].maxz = z;
}
if (z < polygons[i].minz) {
polygons[i].minz = z;
}
polygons[i].midz += z;
}
polygons[i].midz /= polygons[i].verticies.length;
}
polygons.sort((a, b) => b.midz - a.midz)
// polygons.sort((a,b) => Math.max(b.maxz - a.minz, b.minz - a.maxz))
// polygons.sort((a,b) => {
// if (a.minz < b.maxz) {
// return 0;
// }
// if (b.minz < a.maxz) {
// return -1;
// }
// return 0;
// })
for (var i = 0; i < polygons.length; i++) {
var polygon_2 = [];
for (var j = 0; j < polygons[i].verticies.length; j++) {
var v = rotated_verticies[polygons[i].verticies[j]]
polygon_2.push(v.vector);
}
var norm = getNormal(polygon_2, polygons[i].nf);
// var rotated_light = light.copy().rotate(theta);
var brightness = Math.max(0, norm.dot(light))
//ctx.fillStyle = "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)";
ctx.fillStyle = "hsl(190, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)";
// ctx.fillStyle = polygons[i].color
ctx.beginPath();
for (var j = 0; j < polygons[i].verticies.length; j++) {
var vertex = rotated_verticies[polygons[i].verticies[j]].copy();
vertex.mult(scale, scale, 1);
vertex.add(offset);
var n = 1 + vertex.vector[2] * fov;
vertex.div(n, n, 1)
// console.log(vertex.vector)
if (j == 0) {
ctx.moveTo(vertex.vector[0], vertex.vector[1]);
} else {
ctx.lineTo(vertex.vector[0], vertex.vector[1]);
}
}
ctx.closePath();
ctx.fill()
// ctx.stroke()
polygons[i].mid = new Tensor(3, 0, true);
for (var k = 0; k < polygons[i].verticies.length; k++) {
var vertex = rotated_verticies[polygons[i].verticies[k]].copy();
vertex.mult(scale, scale, 1);
vertex.add(offset);
var n = 1 + vertex.vector[2] * fov;
vertex.div(n, n, 1)
polygons[i].mid.add(vertex);
}
polygons[i].mid.div(3, polygons[i].verticies.length, true);
ctx.fillStyle = "red"
ctx.font = '50px serif';
// ctx.fillText(polygons[i].id + ", " + polygons[i].nf, polygons[i].mid.vector[0], polygons[i].mid.vector[1])
}
// theta.add(theta.vector[0] + (0.01*mouseY - theta.vector[0]) * 0.1, 0, theta.vector[2] + (-0.01*mouseX - theta.vector[2]) * 0.1)
theta.add(0, -0.0375, 0);
// fov = (mouseX - w/2) * 0.001
requestAnimationFrame(loop);
}
loop();
// setInterval(loop, 1000 / 60)
function getNormal(polygon, nf) {
var Ax = polygon[1][0] - polygon[0][0];
var Ay = polygon[1][1] - polygon[0][1];
var Az = polygon[1][2] - polygon[0][2];
var Bx = polygon[2][0] - polygon[0][0];
var By = polygon[2][1] - polygon[0][1];
var Bz = polygon[2][2] - polygon[0][2];
var Nx = Ay * Bz - Az * By
var Ny = Az * Bx - Ax * Bz
var Nz = Ax * By - Ay * Bx
return new Tensor(nf * Nx, nf * Ny, nf * Nz);
}
function len(p1, p2) {
return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2);
}
mouseX = 0
mouseY = 0
onmousemove = (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name=”ad.size” content=”width=300,height=600”>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>...</title>
</head>
<body>
<canvas id="canvas" width="300" height="238"></canvas>
</body>
</html>
** Edit:
Ok, we've significantly edited the code, see this fiddle, and the following code snippet below. It's still not working correctly, we think it's something to do with the first line of this piece of code, any ideas?
if (polygons[i].mid.copy().sub(camera).dot(norm) < 0) {
var pathelem = document.createElementNS("http://www.w3.org/2000/svg", "path");
pathelem.setAttribute("d", path);
pathelem.setAttribute("fill", "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)");
svg.appendChild(pathelem);
}
class Tensor {
constructor(){
var input = this.takeInput(...arguments);
this.vector = input;
}
takeInput() {
var a = true;
for (var arg of arguments) {
if (typeof arg !== "number"){
a = false
}
}
if (a && arguments[2] !== true){
return new Array(...arguments);
}
else {
if (arguments[0] instanceof Tensor){
return arguments[0].vector;
}
else {
if (typeof arguments[0] === "number" && typeof arguments[1] === "number" && arguments[2] === true) {
var res = [];
for (var i = 0; i < arguments[0]; i++) {
res.push(arguments[1]);
}
return res;
}
}
}
}
// used for + - * /
change(f, input){
for (var i in this.vector) {
this.vector[i] = f(this.vector[i], input[i]);
}
return this;
}
copy() {
return new Tensor(...this.vector);
}
dimentions() {
return this.vector.length;
}
//-----------
len() {
var s = 0;
for (var dim of this.vector) {
s += dim ** 2;
}
return Math.sqrt(s);
}
norm() {
return this.div(this.dimentions(), this.len(), true)
}
add() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x + y, input);
}
sub() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x - y, input);
}
mult() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x * y, input);
}
div() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x / y, input);
}
dot() {
var input = this.takeInput(...arguments);
var res = 0;
for (var i in this.vector) {
res += this.vector[i] * input[i]
}
return res;
}
rotate() {
// WARNING: only for 3D currently!!!
var input = this.takeInput(...arguments);
var [x, y, z] = this.vector;
// rotate Z
var t_x = x * Math.cos(input[2]) - y * Math.sin(input[2])
y = y * Math.cos(input[2]) + x * Math.sin(input[2])
x = t_x
// rotate X
var t_y = y * Math.cos(input[0]) - z * Math.sin(input[0])
z = z * Math.cos(input[0]) + y * Math.sin(input[0])
y = t_y
// rotate Y
t_x = x * Math.cos(input[1]) + z * Math.sin(input[1])
z = z * Math.cos(input[1]) - x * Math.sin(input[1])
x = t_x
this.vector = [x, y, z];
return this;
}
}
var svg = document.getElementById('svg')
w = 300
h = 286
fov = 0.1
scale = 65;
camera = new Tensor(-w / 2 + 5, -h / 2 + 92, 0.1);
light = new Tensor(3.5, 0.5, 1).norm();
svg.setAttribute('width', w);
svg.setAttribute('height', h);
var vertices = [
new Tensor(0.5, 1, 0),
new Tensor(0.5, -1, 0),
new Tensor(-1, 0, 0),
new Tensor(0, 0, 2)
];
var polygons = [];
polygons.push({
vertices: [0, 3, 1],
color: 'red',
nf: 1
});
polygons.push({
vertices: [2, 3, 0],
color: 'blue',
nf: 1
});
polygons.push({
vertices: [2, 3, 1],
color: 'green',
nf: -1
});
polygons.push({
vertices: [0, 1, 2],
color: 'yellow',
nf: 1
});
for (var i = 0; i < polygons.length; i++) {
polygons[i].id = i;
}
theta = new Tensor(1.5 * Math.PI, 0, 1.5 * Math.PI);
function loop() {
// ctx.clearRect(0, 0, w, h);
svg.innerHTML = "";
rotated_vertices = [];
for (var i = 0; i < vertices.length; i++) {
rotated_vertices.push(vertices[i].copy().rotate(theta));
}
// z sorting
// dots_for_rendering.sort((a,b) => Math.sqrt((b.x)**2 + (b.y)**2) - Math.sqrt((a.x)**2 + (a.y)**2))
for (var i = 0; i < polygons.length; i++) {
polygons[i].maxz = -Infinity;
polygons[i].minz = Infinity;
polygons[i].midz = 0;
for (var j = 0; j < polygons[i].vertices.length; j++) {
var z = rotated_vertices[polygons[i].vertices[j]].vector[2];
// z += 1 * (Math.random() * 2 - 1)
if (z > polygons[i].maxz) {
polygons[i].maxz = z;
}
if (z < polygons[i].minz) {
polygons[i].minz = z;
}
polygons[i].midz += z;
}
polygons[i].midz /= polygons[i].vertices.length;
polygons[i].mid = new Tensor(3, 0, true);
for (var k = 0; k < polygons[i].vertices.length; k++) {
var vertex = rotated_vertices[polygons[i].vertices[k]].copy();
vertex.mult(scale, scale, 1);
vertex.sub(camera);
var n = 1 + vertex.vector[2] * fov;
vertex.div(n, n, 1)
polygons[i].mid.add(vertex);
}
polygons[i].mid.div(3, polygons[i].vertices.length, true);
}
polygons.sort((a, b) => b.midz - a.midz)
// polygons.sort((a,b) => Math.max(b.maxz - a.minz, b.minz - a.maxz))
// polygons.sort((a,b) => {
// if (a.minz < b.maxz) {
// return 0;
// }
// if (b.minz < a.maxz) {
// return -1;
// }
// return 0;
// })
for (var i = 0; i < polygons.length; i++) {
var polygons_embedded_point_coords = [];
for (var j = 0; j < polygons[i].vertices.length; j++) {
var v = rotated_vertices[polygons[i].vertices[j]]
polygons_embedded_point_coords.push(v.vector);
}
var norm = getNormal(polygons_embedded_point_coords, polygons[i].nf);
// var rotated_light = light.copy().rotate(theta);
var brightness = Math.max(0, norm.dot(light))
// ctx.fillStyle = "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)";
// ctx.fillStyle = polygons[i].color
// ctx.beginPath();
var path = [];
for (var j = 0; j < polygons[i].vertices.length; j++) {
var vertex = rotated_vertices[polygons[i].vertices[j]].copy();
vertex.mult(scale, scale, 1);
vertex.sub(camera);
var n = 1 + vertex.vector[2] * fov;
vertex.div(n, n, 1)
// console.log(vertex.vector)
if (j == 0) {
// ctx.moveTo(vertex.vector[0], vertex.vector[1]);
path.push("M "+vertex.vector[0]+" "+vertex.vector[1]);
} else {
path.push("L "+vertex.vector[0]+" "+vertex.vector[1]);
// ctx.lineTo(vertex.vector[0], vertex.vector[1]);
}
}
// that should work
if (polygons[i].mid.copy().sub(camera).dot(norm) < 0) {
var pathelem = document.createElementNS("http://www.w3.org/2000/svg", "path");
pathelem.setAttribute("d", path);
pathelem.setAttribute("fill", "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)");
svg.appendChild(pathelem);
}
// ctx.fillStyle = "red"
// ctx.font = '15px serif';
//
// ctx.fillText(polygons[i].id + ", " + polygons[i].nf, polygons[i].mid.vector[0], polygons[i].mid.vector[1])
}
// theta.add(theta.vector[0] + (0.01*mouseY - theta.vector[0]) * 0.1, 0, theta.vector[2] + (-0.01*mouseX - theta.vector[2]) * 0.1)
theta.add(0, -0.0375, 0);
// fov = (mouseX - w/2) * 0.001
requestAnimationFrame(loop);
}
loop();
// setInterval(loop, 1000 / 60)
function getNormal(vertices, nf) {
var Ax = vertices[1][0] - vertices[0][0];
var Ay = vertices[1][1] - vertices[0][1];
var Az = vertices[1][2] - vertices[0][2];
var Bx = vertices[2][0] - vertices[0][0];
var By = vertices[2][1] - vertices[0][1];
var Bz = vertices[2][2] - vertices[0][2];
var Nx = Ay * Bz - Az * By
var Ny = Az * Bx - Ax * Bz
var Nz = Ax * By - Ay * Bx
return new Tensor(nf * Nx, nf * Ny, nf * Nz);
}
function len(p1, p2) {
return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2);
}
mouseX = 0
mouseY = 0
onmousemove = (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
}
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
<script src="Tensor.js"></script>
<script src="script-tensors-svg.js" async defer></script>
</head>
<body>
<!-- <canvas id="canvas"></canvas> -->
<svg id="svg" xmlns="http://www.w3.org/2000/svg"></svg>
</body>
</html>
Sorting by average Z just doesn't give you a reliable rendering order. Since your shape is convex, though, you don't need to sort at all.
Make sure the vertices of each triangle are sorted so that you can consistently get a surface normal that points outward. Then, just don't render any triangles with normals that point away from the camera, i.e.:
if (vector_from_camera_to_poly_midpoint \dot poly_normal < 0) {
//render the poly
}
Now you will only render the side of the object that is facing the camera -- none of the polygons will overlap, so you can render them in any order.
For fun I am trying to create a visualization of different sorting algorithms, but I've run into an issue with Canvas animations.
I assumed I would just be able to call a draw function within the sorter method, but this causes the browser to lock up until the array is fully sorted and then draws some middle frame.
How would I go about animating from within the sorting methods? Below is the code I have so far, I wouldn't run this snippet as it will hang the tab for a few seconds.
N = 250; // Array Size
XYs = 5; // Element Visual Size
Xp = 1; // Start Pos X
Yp = 1; // Start Pos Y
var canvas;
var l = Array.apply(null, {
length: N
}).map(Number.call, Number);
Array.prototype.shuffle = function() {
var i = this.length,
j, temp;
if (i == 0) return this;
while (--i) {
j = Math.floor(Math.random() * (i + 1));
temp = this[i];
this[i] = this[j];
this[j] = temp;
}
return this;
}
function map_range(x, in_min, in_max, out_min, out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
function rainbow(x) {
var m = map_range(x, 0, N, 0, 359);
return 'hsl(' + m + ',100%,50%)';
}
function init() {
canvas = document.getElementById('canvas');
l.shuffle();
draw();
bubbleSort(l);
}
function draw() {
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
for (var i = 0; i < l.length; i++) {
ctx.fillStyle = rainbow(l[i]);
ctx.fillRect((Xp * i) * XYs, Yp * XYs, XYs, XYs);
}
}
}
function bubbleSort(a) {
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
var temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
draw();
setTimeout(function() {}, 10);
}
}
} while (swapped);
}
<html>
<body onload="init();">
<canvas id="canvas" width="1500" height="1500"></canvas>
</body>
</html>
One solution is the ES6 Generator function* along with its yield statement.
That allows you to pause a function and restart it later where it has been paused:
N = 100; // Array Size
XYs = 5; // Element Visual Size
Xp = 1; // Start Pos X
Yp = 1; // Start Pos Y
var canvas;
var l = Array.apply(null, {
length: N
}).map(Number.call, Number);
Array.prototype.shuffle = function() {
var i = this.length,
j, temp;
if (i == 0) return this;
while (--i) {
j = Math.floor(Math.random() * (i + 1));
temp = this[i];
this[i] = this[j];
this[j] = temp;
}
return this;
}
function map_range(x, in_min, in_max, out_min, out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
function rainbow(x) {
var m = map_range(x, 0, N, 0, 359);
return 'hsl(' + m + ',100%,50%)';
}
function init() {
canvas = document.getElementById('canvas');
l.shuffle();
var sort = bubbleSort(l);
// an anim function triggered every 60th of a second
function anim(){
requestAnimationFrame(anim);
draw();
sort.next(); // call next iteration of the bubbleSort function
}
anim();
}
function draw() {
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
for (var i = 0; i < l.length; i++) {
ctx.fillStyle = rainbow(l[i]);
ctx.fillRect((Xp * i) * XYs, Yp * XYs, XYs, XYs);
}
}
}
function* bubbleSort(a) { // * is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
var temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
yield swapped; // pause here
}
}
} while (swapped);
}
init();
<canvas id="canvas" width="500" height="20">
You can also now use async/await for a more readable code achieving the same goal:
const N = 100; // Array Size
const XYs = 5; // Element Visual Size
const Xp = 1; // Start Pos X
const Yp = 1; // Start Pos Y
let canvas;
const l = Array.from({ length: N }, (_,i) => i);
Array.prototype.shuffle = function() {
let i = this.length;
if (i == 0) return this;
while (--i) {
const j = Math.floor(Math.random() * (i + 1));
const temp = this[i];
this[i] = this[j];
this[j] = temp;
}
return this;
}
function map_range(x, in_min, in_max, out_min, out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
function rainbow(x) {
const m = map_range(x, 0, N, 0, 359);
return 'hsl(' + m + ',100%,50%)';
}
function init() {
canvas = document.getElementById('canvas');
l.shuffle();
bubbleSort(l);
}
function draw() {
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
for (let i = 0; i < l.length; i++) {
ctx.fillStyle = rainbow(l[i]);
ctx.fillRect((Xp * i) * XYs, Yp * XYs, XYs, XYs);
}
}
}
function drawNextFrame() {
return new Promise((res) => requestAnimationFrame(res))
.then(draw);
}
async function bubbleSort(a) { // async is magic
let swapped;
do {
swapped = false;
for (let i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
const temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
await drawNextFrame(); // pause here
}
}
} while (swapped);
}
init();
<canvas id="canvas" width="500" height="20">
You need a render loop. requestAnimationFrame() is your friend here. With this method you give the browser a callback that is called before the next frame draw.
Within that callback you draw your stuff and call requestAnimationFrame() again with your same callback like this:
requestAnimationFrame(renderLoop);
function renderLoop()
{
// visualize your array `l` at this point
// call the render callback for the next frame draw
requestAnimationFrame(renderLoop);
}
https://developer.mozilla.org/de/docs/Web/API/window/requestAnimationFrame
You get a smooth animation with usually a framerate of 60 fps.
For integrating that approach in your code:
Delete the lines
draw();
setTimeout(function() {}, 10);
call the initial
requestAnimationFrame(renderLoop);
when your program start.
I've been trying to procedurally generate a planet for the game I'm making in Javascript. It generated a random noise grid and then iterates through it using cellular automata rules. This takes longer than 30 seconds for worlds larger than 1000x1000, so I was wondering if there was a way to generate it from a seed when it is loaded, like in Minecraft. Is there a way to generate the planet without having to do it all at once, or at least a faster way?
Here's the code, it generates the terrain and then displays it on the canvas.
scale = 0.25;
rngCount = 0;
function* RNG() {
let seed = Math.floor(Math.random() * 10000) / 10000;
console.log(seed);
let factor = 297;
while(true) {
seed = (seed * factor) - Math.floor(seed * factor);
yield Math.floor(seed * 10000) / 10000;
};
};
let generationRNG = RNG();
function clamp(min, max, value) {
if(value < min) {
return min;
} else if(value > max) {
return max;
} else {
return value;
};
};
let map = [];
let mapW = 4096;
let mapH = 4096;
function iterate(e, map) {
let height = map.length;
let width = map[0].length;
let updatedMap;
let mapRow;
let neighborCount;
let iy;
let ix;
for(i = 0; i < e; i++) {
updatedMap = [];
for(y = 0; y < height; y++) {
mapRow = [];
for(x = 0; x < width; x++) {
neighborCount = 0;
for(k = -1; k <= 1; k++) {
for(l = -1 ; l <= 1; l++) {
if(y + k < 0) {
iy = height - 1;
} else if(y + k >= height) {
iy = 0;
} else {
iy = y + k;
};
if(x + l < 0) {
ix = width - 1;
} else if(x + l >= width) {
ix = 0;
} else {
ix = x + l;
};
if(!(l == 0 && k == 0)) {
neighborCount += map[iy][ix];
};
};
};
if(neighborCount >= 5) {
mapRow.push(1);
} else if(neighborCount <= 3) {
mapRow.push(0);
} else {
mapRow.push(map[y][x]);
};
};
updatedMap.push(mapRow);
};
map = updatedMap;
};
return map;
};
function terrainGeneration() {
let width = mapW / 16;
let height = mapH / 16;
let mapRow;
let factor = 4;
let fillPercent = 0.45
let updatedMap;
let map = [];
for(y = 0; y < height; y++) {
mapRow = [];
for(x = 0; x < width; x++) {
if(generationRNG.next().value <= fillPercent) {
mapRow.push(1);
} else {
mapRow.push(0);
};
};
map.push(mapRow);
};
map = iterate(10, map);
for(a = 0; a < 2; a++) {
if(a == 0) {
fillPercent = 0.33;
} else if (a == 1) {
fillPercent = 0.2;
};
width *= factor;
height *= factor;
updatedMap = [];
for(y = 0; y < height; y++) {
mapRow = [];
for(x = 0; x < width; x++) {
mapRow.push(map[Math.floor(y / factor)][Math.floor(x / factor)]);
};
updatedMap.push(mapRow);
};
map = updatedMap;
updatedMap = [];
for(y = 0; y < height; y++) {
mapRow = [];
for(x = 0; x < width; x++) {
if(map[y][x] == 1) {
if(generationRNG.next().value <= fillPercent) {
mapRow.push(0);
} else {
mapRow.push(1);
};
} else {
if(generationRNG.next().value <= fillPercent) {
mapRow.push(1);
} else {
mapRow.push(0);
};
};
};
updatedMap.push(mapRow);
};
map = updatedMap;
map = iterate(5, map);
};
return map;
};
map = terrainGeneration();
for(i = 0; i < map.length; i++) {
for(j = 0; j < map[i].length; j++) {
c.fillStyle = "rgba(" + map[i][j] * 255 + ", " + map[i][j] * 255 + ", " + map[i][j] * 255 + ", 1)";
c.fillRect(j * scale, i * scale, scale, scale);
};
};
c.fillStyle = "rgba(0, 0, 255, 1)";
c.fillRect(0, 0, 40 * scale, 40 * scale);
is there a way to generate only a section of it from a seed and still have it coherently fit together? After I add biomes, the generation will take even longer, so I want to try to fix it now before I add more code. To test out the section generation I was going to have it generate the area around the cursor only.
here's an example of a 4096x4096 map that took 3 and a half minutes to generate.
Me and a friend are playing around with fractals and wanted to make an interactive website where you can change values that generate the fractal, and you can see how its affected live. On small resolution tests, the website it quite responsive but still slow.
drawFractal = () => {
for (let x = 0; x < this.canvas.width; x++) {
for (let y = 0; y < this.canvas.height; y++) {
const belongsToSet = this.checkIfBelongsToMandelbrotSet(x / this.state.magnificationFactor - this.state.panX, y / this.state.magnificationFactor - this.state.panY);
if (belongsToSet === 0) {
this.ctx.clearRect(x,y, 1,1);
} else {
this.ctx.fillStyle = `hsl(80, 100%, ${belongsToSet}%)`;
// Draw a colorful pixel
this.ctx.fillRect(x,y, 1,1);
}
}
}
}
checkIfBelongsToMandelbrotSet = (x,y) => {
let realComponentOfResult = x;
let imaginaryComponentOfResult = y;
// Set max number of iterations
for (let i = 0; i < this.state.maxIterations; i++) {
const tempRealComponent = realComponentOfResult * realComponentOfResult - imaginaryComponentOfResult * imaginaryComponentOfResult + x;
const tempImaginaryComponent = this.state.imaginaryConstant * realComponentOfResult * imaginaryComponentOfResult + y;
realComponentOfResult = tempRealComponent;
imaginaryComponentOfResult = tempImaginaryComponent;
// Return a number as a percentage
if (realComponentOfResult * imaginaryComponentOfResult > 5) {
return (i / this.state.maxIterations * 100);
}
}
// Return zero if in set
return 0;
}
This is the algorithm that handles the generation of the fractal. However we iterate over every pixel of the canvas which is quite inefficient. As a result the whole website is really slow. I wanted to ask whether it's a good idea to use html canvas or are there more efficient alternatives? Or can I optimise the drawFractal() function to be able to be more efficient? I have no idea how to continue from this point as i am inexperienced and would appreciate any feedback!
Avoid painting operations as much as you can.
When you do fillRect(x, y, 1, 1) the browser has to go from the CPU to the GPU once per pixel, and that's very inefficient.
In your case, since you are drawing every pixels on their own, you can simply set all these pixels on an ImageBitmap and put the full image once per frame.
To improve a bit the color setting, I generated an Array of a hundred values before hand, you can make it more granular if you wish.
There might be improvements to do in your Mandelbrot, I didn't checked it, but this would be more suited to CodeReview than StackOverflow.
Here is a simple demo using a 800x600px canvas:
const state = {
magnificationFactor: 5000,
imaginaryConstant: 1,
maxIterations: 20,
panX: 1,
panY: 1
};
const canvas = document.getElementById('canvas');
const width = canvas.width = 800;
const height = canvas.height = 600;
const ctx = canvas.getContext('2d');
// the ImageData on which we will draw
const img = new ImageData( width, height );
// create an Uint32 view so that we can set one pixel in one op
const img_data = new Uint32Array( img.data.buffer );
const drawFractal = () => {
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const belongsToSet = checkIfBelongsToMandelbrotSet(x / state.magnificationFactor - state.panX, y / state.magnificationFactor - state.panY);
// setthe value in our ImageData's data
img_data[ y * width + x] = getColor( belongsToSet );
}
}
// only now we paint
ctx.putImageData( img, 0, 0 );
};
checkIfBelongsToMandelbrotSet = (x,y) => {
let realComponentOfResult = x;
let imaginaryComponentOfResult = y;
// Set max number of iterations
for (let i = 0; i < state.maxIterations; i++) {
const tempRealComponent = realComponentOfResult * realComponentOfResult - imaginaryComponentOfResult * imaginaryComponentOfResult + x;
const tempImaginaryComponent = state.imaginaryConstant * realComponentOfResult * imaginaryComponentOfResult + y;
realComponentOfResult = tempRealComponent;
imaginaryComponentOfResult = tempImaginaryComponent;
// Return a number as a percentage
if (realComponentOfResult * imaginaryComponentOfResult > 5) {
return (i / state.maxIterations * 100);
}
}
// Return zero if in set
return 0;
}
// we generate all the colors at init instead of generating every frame
const colors = Array.from( { length: 100 }, (_,i) => {
if( !i ) { return 0; }
return hslToRgb( 80/360, 100/100, i/100 );
} );
function getColor( ratio ) {
if( ratio === 0 ) { return 0; }
return colors[ Math.round( ratio ) ];
}
function anim() {
state.magnificationFactor -= 10;
drawFractal();
requestAnimationFrame( anim );
}
requestAnimationFrame( anim );
// original by mjijackson.com
// borrowed from https://stackoverflow.com/a/9493060/3702797
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
// we want 0xAABBGGRR format
function toHex( val ) {
return Math.round( val * 255 ).toString(16);
}
return Number( '0xFF' + toHex(b) + toHex(g) + toHex(r) );
}
<canvas id="canvas"></canvas>
jsfiddle: https://jsfiddle.net/nragrkb8/3/
I'm trying to get a spectrogram drawn, works in chrome but draws nothing in all other browsers. I'm using getImageData and putImageData to move existing contents down by a pixel every time new data is pushed in. That new data is then drawn into an offscreen image and then putimagedata to apply it to the full canvas.
Spectrogram = (function() {
//constructor function
var Spectrogram = function(options) {
//compose options
this.canvas = options.canvas;
this.width = options.size; //the size of the FFT
this.height = options.length; //the number of FFT to keep
if (typeof options.min !== 'undefined' && typeof options.max !== 'undefined') {
this.rangeType = 'static';
this.min = options.min;
this.max = options.max;
} else {
this.rangeType = 'auto';
this.min = Infinity;
this.max = -Infinity;
}
//set the function used to determine a value's color
this.colorScaleFn = options.colorScaleFn || defaultColorScaling;
//precalculate the range used in color scaling
this.range = this.max - this.min;
//the 2d drawing context
this.ctx = this.canvas.getContext('2d', { alpha: false });
//single pixel image used to draw new data points
this.im = this.ctx.createImageData(this.width, 1);
this.canvas.width = this.width;
this.canvas.height = this.height;
//adds a new row of data to the edge of the spectrogram, shifting existing
//contents by 1px
this.addData = function (newData) {
if (newData.length != this.width) {
console.error('Unmatched dataset size. Expected ' + this.width + ', was + ' + newData.length);
return;
}
if (this.rangeType == 'auto') {
var changed = false;
for (var i = 0; i < newData.length; ++i) {
if (newData[i] < this.min) {
this.min = newData[i];
changed = true;
} else if (newData[i] > this.max) {
this.max = newData[i];
changed = true;
}
}
if (changed) {
this.range = this.max - this.min;
}
}
//move the current contents by 1px
var im = this.ctx.getImageData(0, 0, this.width, this.height - 1);
this.ctx.putImageData(im, 0, 1);
//draw the new data values into the temporary image
var j = 0;
for (var i = 0; i < newData.length; ++i) {
var c = this.colorScaleFn(newData[i]);
this.im.data[j] = c[0];
this.im.data[j + 1] = c[1];
this.im.data[j + 2] = c[2];
j += 4;
}
//put the new data colors into the full canvas
this.ctx.putImageData(this.im, 0, 0);
}
}
function defaultColorScaling(value) {
var x = (value - this.min) / this.range;
if (x < 0) {
x = 0;
} else if (x > 1) {
x = 1;
}
var g = Math.pow(x, 2);
var b = 0.75 - 1.5 * Math.abs(x - 0.45);
var r = 0;
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
return Spectrogram;
})();
const size = 1024;
const length = 200;
const dataMax = 100;
const velMax = 1;
const canvas = document.getElementById('spec');
canvas.width = size;
canvas.height = length;
const spec = new Spectrogram({
canvas: canvas,
size: size,
length: length
});
const data = [];
const vel = [];
for (var i = 0; i < size; ++i) {
data.push(0);
vel.push(0);
}
setInterval(function() {
for (var i = 0; i < size; ++i) {
data[i] += vel[i];
if (i > 0) data[i] += vel[i - 1];
if (i < size - 1) data[i] += vel[i + 1];
if (data[i] >= dataMax) {
data[i] = dataMax;
vel[i] = 0;
} else if (data[i] <= -dataMax) {
data[i] = -dataMax;
vel[i] = 0;
}
if (vel[i] == 0) {
vel[i] = (Math.random() - 0.5) * 100;
}
}
spec.addData(data);
}, 100);
Your problem was induced by a misconception:
getContext('2d', {alpha: false}) should have no incidence on createImageData function, so you still have to set the alpha value of your ImageData to something else than 0.
There are also other things in your code that don't go well, even if unrealted with your current issue:
You should avoid at all costs an high frequency setInterval where you perform a task that can take more than the interval (and in your case it can), but rather use something like a requestAnimationFrame loop.
Don't use getImageData + putImageData to draw your context on itself, simply use ctx.drawImage(ctx.canvas, x, y).
There might be other things to fix in your code, but here is an update with these three major things:
Spectrogram = (function() {
//constructor function
var Spectrogram = function(options) {
//compose options
this.canvas = options.canvas;
this.width = options.size; //the size of the FFT
this.height = options.length; //the number of FFT to keep
if (typeof options.min !== 'undefined' && typeof options.max !== 'undefined') {
this.rangeType = 'static';
this.min = options.min;
this.max = options.max;
} else {
this.rangeType = 'auto';
this.min = Infinity;
this.max = -Infinity;
}
//set the function used to determine a value's color
this.colorScaleFn = options.colorScaleFn || defaultColorScaling;
//precalculate the range used in color scaling
this.range = this.max - this.min;
//the 2d drawing context
this.ctx = this.canvas.getContext('2d', {
alpha: false
});
//single pixel image used to draw new data points
this.im = this.ctx.createImageData(this.width, 1);
this.canvas.width = this.width;
this.canvas.height = this.height;
//adds a new row of data to the edge of the spectrogram, shifting existing
//contents by 1px
this.addData = function(newData) {
if (newData.length != this.width) {
console.error('Unmatched dataset size. Expected ' + this.width + ', was + ' + newData.length);
return;
}
if (this.rangeType == 'auto') {
var changed = false;
for (var i = 0; i < newData.length; ++i) {
if (newData[i] < this.min) {
this.min = newData[i];
changed = true;
} else if (newData[i] > this.max) {
this.max = newData[i];
changed = true;
}
}
if (changed) {
this.range = this.max - this.min;
}
}
//move the current contents by 1px
this.ctx.drawImage(this.ctx.canvas, 0, 1)
//draw the new data values into the temporary image
var j = 0;
for (var i = 0; i < newData.length; ++i) {
var c = this.colorScaleFn(newData[i]);
this.im.data[j] = c[0];
this.im.data[j + 1] = c[1];
this.im.data[j + 2] = c[2];
// don't forget the alpha channel, createImageData is always full of zeroes
this.im.data[j + 3] = 255;
j += 4;
}
this.ctx.putImageData(this.im, 0, 0);
}
}
function defaultColorScaling(value) {
var x = (value - this.min) / this.range;
if (x < 0) {
x = 0;
} else if (x > 1) {
x = 1;
}
var g = Math.pow(x, 2);
var b = 0.75 - 1.5 * Math.abs(x - 0.45);
var r = 0;
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
return Spectrogram;
})();
const size = 1024;
const length = 200;
const dataMax = 100;
const velMax = 1;
const canvas = document.getElementById('spec');
canvas.width = size;
canvas.height = length;
const spec = new Spectrogram({
canvas: canvas,
size: size,
length: length
});
const data = [];
const vel = [];
for (var i = 0; i < size; ++i) {
data.push(0);
vel.push(0);
}
// our animation loop
function draw() {
for (var i = 0; i < size; ++i) {
data[i] += vel[i];
if (i > 0) data[i] += vel[i - 1];
if (i < size - 1) data[i] += vel[i + 1];
if (data[i] >= dataMax) {
data[i] = dataMax;
vel[i] = 0;
} else if (data[i] <= -dataMax) {
data[i] = -dataMax;
vel[i] = 0;
}
if (vel[i] == 0) {
vel[i] = (Math.random() - 0.5) * 100;
}
}
spec.addData(data);
requestAnimationFrame(draw);
}
draw();
<div style="width: 100%; height: 100%; padding: 0; margin: 0;">
<canvas style="width: 100%; height: 100%;" id="spec"></canvas>
</div>