simulationjs
Version:
A simple graphics library for 2d and 3d graphics on html canvas
1,480 lines • 61.1 kB
JavaScript
export class LightSource {
pos;
id;
intensity;
constructor(pos, intensity = 1, id = '') {
this.pos = pos;
this.id = id;
this.intensity = intensity;
}
}
export class Camera {
pos;
rot;
constructor(pos, rot) {
this.pos = pos;
rot.x = radToDeg(rot.x);
rot.y = radToDeg(rot.y);
rot.z = radToDeg(rot.z);
this.rot = rot;
}
}
export class Vector3 {
x;
y;
z;
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
format() {
return `(${this.x}, ${this.y}, ${this.z})`;
}
clone() {
return new Vector3(this.x, this.y, this.z);
}
rotateX(val) {
const initialY = this.y;
const initialZ = this.z;
this.y = initialY * Math.cos(val) - initialZ * Math.sin(val);
this.z = initialY * Math.sin(val) + initialZ * Math.cos(val);
}
rotateY(val) {
const initialX = this.x;
const initialZ = this.z;
this.x = initialX * Math.cos(val) + initialZ * Math.sin(val);
this.z = -initialX * Math.sin(val) + initialZ * Math.cos(val);
}
rotateZ(val) {
const initialX = this.x;
const initialY = this.y;
this.x = initialX * Math.cos(val) - initialY * Math.sin(val);
this.y = initialX * Math.sin(val) + initialY * Math.cos(val);
return this;
}
rotate(vec) {
this.rotateZ(degToRad(vec.z));
this.rotateX(degToRad(vec.x));
this.rotateY(degToRad(vec.y));
return this;
}
multiply(val) {
this.x *= val;
this.y *= val;
this.z *= val;
return this;
}
divide(val) {
this.x /= val;
this.y /= val;
this.z /= val;
return this;
}
add(vec) {
this.x += vec.x;
this.y += vec.y;
this.z += vec.z;
return this;
}
sub(vec) {
this.x -= vec.x;
this.y -= vec.y;
this.z -= vec.z;
return this;
}
getMag() {
return pythag(pythag(this.x, this.y), this.z);
}
getRotation() {
const ay = radToDeg(Math.atan2(this.x, this.z));
const ax = radToDeg(Math.atan2(this.y, this.z));
return new Vector(ax, ay);
}
dot(vec) {
return this.x * vec.x + this.y * vec.y + this.z * vec.z;
}
normalize() {
const mag = this.getMag();
this.x /= mag;
this.y /= mag;
this.z /= mag;
return this;
}
cross(vec) {
const i = [this.y, this.z, vec.y, vec.z];
const j = [this.x, this.z, vec.x, vec.z];
const k = [this.x, this.y, vec.x, vec.y];
const determinantI = i[0] * i[3] - i[1] * i[2];
const determinantJ = j[0] * j[3] - j[1] * j[2];
const determinantK = k[0] * k[3] - k[1] * k[2];
return new Vector3(determinantI, -determinantJ, determinantK);
}
}
export class Vector {
x;
y;
constructor(x, y) {
this.x = x;
this.y = y;
}
getRotation() {
return radToDeg(Math.atan2(this.y, this.x));
}
getMag() {
return pythag(this.x, this.y);
}
rotate(deg) {
const rotation = this.getRotation();
const mag = this.getMag();
this.x = Math.cos(degToRad(rotation + deg)) * mag;
this.y = Math.sin(degToRad(rotation + deg)) * mag;
return this;
}
draw(c, pos = new Vector(0, 0), color = new Color(0, 0, 0), thickness = 1) {
c.beginPath();
c.strokeStyle = color.toHex();
c.lineWidth = thickness;
c.moveTo(pos.x, pos.y);
c.lineTo(pos.x + this.x, pos.y + this.y);
c.stroke();
c.closePath();
}
normalize() {
const mag = this.getMag();
if (mag != 0) {
this.x /= mag;
this.y /= mag;
}
return this;
}
multiply(n) {
this.x *= n;
this.y *= n;
return this;
}
sub(v) {
this.x -= v.x;
this.y -= v.y;
return this;
}
add(v) {
this.x += v.x;
this.y += v.y;
return this;
}
divide(n) {
this.x /= n;
this.y /= n;
return this;
}
appendMag(value) {
const mag = this.getMag();
if (mag != 0) {
const newMag = mag + value;
this.normalize();
this.multiply(newMag);
}
return this;
}
dot(vec) {
return this.x * vec.x + this.y * vec.y;
}
clone() {
return new Vector(this.x, this.y);
}
format() {
return `(${this.x}, ${this.y})`;
}
}
export class SimulationElement {
pos;
color;
type;
running;
_3d = false;
id;
constructor(pos, color = new Color(0, 0, 0), type = null, id = '') {
this.pos = pos;
this.color = color;
this.type = type;
this.running = true;
this.id = id;
}
onFrame() { }
end() {
this.running = false;
}
draw(_) { }
setId(id) {
this.id = id;
}
fill(color, t = 0, f) {
const currentColor = new Color(this.color.r, this.color.g, this.color.b, this.color.a);
const colorClone = color.clone();
const changeR = colorClone.r - this.color.r;
const changeG = colorClone.g - this.color.g;
const changeB = colorClone.b - this.color.b;
const changeA = colorClone.a - this.color.a;
const func = () => {
this.color = colorClone;
};
return transitionValues(func, (p) => {
currentColor.r += changeR * p;
currentColor.g += changeG * p;
currentColor.b += changeB * p;
currentColor.a += changeA * p;
this.color.r = currentColor.r;
this.color.g = currentColor.g;
this.color.b = currentColor.b;
this.color.a = currentColor.a;
return this.running;
}, func, t, f);
}
moveTo(p, t = 0, f) {
const changeX = p.x - this.pos.x;
const changeY = p.y - this.pos.y;
return transitionValues(() => {
this.pos = p;
}, (p) => {
this.pos.x += changeX * p;
this.pos.y += changeY * p;
return this.running;
}, () => {
this.pos.x = p.x;
this.pos.y = p.y;
}, t, f);
}
move(p, t = 0, f) {
const changeX = p.x;
const changeY = p.y;
const startPos = new Vector(this.pos.x, this.pos.y);
return transitionValues(() => {
this.pos.x += p.x;
this.pos.y += p.y;
}, (p) => {
this.pos.x += changeX * p;
this.pos.y += changeY * p;
return this.running;
}, () => {
this.pos.x = startPos.x + p.x;
this.pos.y = startPos.y + p.y;
}, t, f);
}
}
export class Color {
r;
g;
b;
a;
constructor(r, g, b, a = 1) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
clone() {
return new Color(this.r, this.g, this.b, this.a);
}
compToHex(c) {
const hex = Math.round(c).toString(16);
return hex.length == 1 ? '0' + hex : hex;
}
toHex() {
return ('#' +
this.compToHex(this.r) +
this.compToHex(this.g) +
this.compToHex(this.b) +
this.compToHex(this.a * 255));
}
}
// extend SimulationElement so it can be added to the
// Simulation scene
export class SceneCollection extends SimulationElement {
name;
scene;
_isSceneCollection = true;
camera;
displaySurface;
lightSources;
ambientLighting;
planesSortFunc;
constructor(name = '') {
super(new Vector(0, 0));
this.name = name;
this.scene = [];
this.camera = new Camera(new Vector3(0, 0, 0), new Vector3(0, 0, 0));
this.displaySurface = new Vector3(0, 0, 0);
this.lightSources = [];
this.ambientLighting = 0;
this.planesSortFunc = sortPlanes;
}
onFrame() {
this.scene.forEach((item) => item.onFrame());
}
setSortFunc(func) {
this.planesSortFunc = func;
this.scene.forEach((element) => {
if (element._isSceneCollection) {
element.setSortFunc(func);
}
});
}
set3dObjects(cam, displaySurface) {
this.camera = cam;
this.displaySurface = displaySurface;
}
setAmbientLighting(val) {
this.ambientLighting = val;
this.scene.forEach((obj) => {
if (obj._isSceneCollection) {
obj.setAmbientLighting(this.ambientLighting);
}
});
}
end() {
super.end();
this.scene.forEach((item) => item.end());
}
add(element, id = null) {
if (id !== null) {
element.setId(id);
}
if (element._isSceneCollection) {
element.set3dObjects(this.camera, this.displaySurface);
element.setSortFunc(this.planesSortFunc);
}
this.scene.push(element);
}
updateSceneLightSources() {
this.scene.forEach((obj) => {
if (obj._isSceneCollection) {
obj.setLightSources(this.lightSources);
}
});
}
setLightSources(sources) {
this.lightSources = sources;
this.updateSceneLightSources();
}
addLightSource(source) {
this.lightSources.push(source);
this.updateSceneLightSources();
}
removeLightSourceWithId(id) {
this.lightSources = this.lightSources.filter((source) => source.id !== id);
this.updateSceneLightSources();
}
getLightSourceWithId(id) {
for (let i = 0; i < this.lightSources.length; i++) {
if (this.lightSources[i].id === id)
return this.lightSources[i];
}
return null;
}
removeWithId(id) {
this.scene = this.scene.filter((item) => item.id !== id);
}
removeWithObject(element) {
this.scene = this.scene.filter((item) => item === element);
}
draw(c) {
let planes = [];
for (const element of this.scene) {
if (element._3d) {
if (element.type === 'plane') {
planes.push(element);
}
else {
element.draw(c, this.camera, this.displaySurface, this.lightSources, this.ambientLighting);
}
}
else {
element.draw(c);
}
}
planes = this.planesSortFunc(planes, this.camera);
planes.forEach((plane) => {
plane.draw(c, this.camera, this.displaySurface, this.lightSources, this.ambientLighting);
});
}
empty() {
this.scene = [];
}
}
export class SimulationElement3d {
pos;
color;
type;
running;
_3d = true;
id;
lighting;
constructor(pos, color = new Color(0, 0, 0), lighting = false, type = null, id = '') {
this.pos = pos;
this.color = color;
this.type = type;
this.running = true;
this.id = id;
this.lighting = lighting;
}
onFrame() { }
setLighting(val) {
this.lighting = val;
}
setId(id) {
this.id = id;
}
end() {
this.running = false;
}
draw(_ctx, _camera, _displaySurface, _lightSources, _ambientLighting) { }
fill(color, t = 0, f) {
const currentColor = new Color(this.color.r, this.color.g, this.color.b, this.color.a);
const colorClone = color.clone();
const changeR = colorClone.r - this.color.r;
const changeG = colorClone.g - this.color.g;
const changeB = colorClone.b - this.color.b;
const changeA = colorClone.a - this.color.a;
const func = () => {
this.color = colorClone;
};
return transitionValues(func, (p) => {
currentColor.r += changeR * p;
currentColor.g += changeG * p;
currentColor.b += changeB * p;
currentColor.a += changeA * p;
this.color.r = currentColor.r;
this.color.g = currentColor.g;
this.color.b = currentColor.b;
this.color.a = currentColor.a;
return this.running;
}, func, t, f);
}
moveTo(p, t = 0, f) {
const changeX = p.x - this.pos.x;
const changeY = p.y - this.pos.y;
const changeZ = p.z - this.pos.z;
return transitionValues(() => {
this.pos = p;
}, (p) => {
this.pos.x += changeX * p;
this.pos.y += changeY * p;
this.pos.z += changeZ * p;
return this.running;
}, () => {
this.pos.x = p.x;
this.pos.y = p.y;
this.pos.z = p.z;
}, t, f);
}
move(p, t = 0, f) {
const changeX = p.x;
const changeY = p.y;
const changeZ = p.z;
const startPos = new Vector3(this.pos.x, this.pos.y, this.pos.z);
return transitionValues(() => {
this.pos.x += p.x;
this.pos.y += p.y;
this.pos.z += p.z;
}, (p) => {
this.pos.x += changeX * p;
this.pos.y += changeY * p;
this.pos.z += changeZ * p;
return this.running;
}, () => {
this.pos.x = startPos.x + p.x;
this.pos.y = startPos.y + p.y;
this.pos.z = startPos.z + p.z;
}, t, f);
}
}
export class Line extends SimulationElement {
startPoint;
endPoint;
thickness;
constructor(p1, p2, color = new Color(0, 0, 0), thickness = 1) {
super(new Vector(0, 0), color, 'line');
this.startPoint = p1;
this.endPoint = p2;
this.thickness = thickness;
}
clone() {
return new Line(this.startPoint.clone(), this.endPoint.clone(), this.color.clone(), this.thickness);
}
setStart(p, t = 0, f) {
const xChange = p.x - this.startPoint.x;
const yChange = p.y - this.startPoint.y;
return transitionValues(() => {
this.startPoint = p;
}, (p) => {
this.startPoint.x += xChange * p;
this.startPoint.y += yChange * p;
return this.running;
}, () => {
this.startPoint = p;
}, t, f);
}
setEnd(p, t = 0, f) {
const xChange = p.x - this.endPoint.x;
const yChange = p.y - this.endPoint.y;
return transitionValues(() => {
this.endPoint = p;
}, (p) => {
this.endPoint.x += xChange * p;
this.endPoint.y += yChange * p;
return this.running;
}, () => {
this.endPoint = p;
}, t, f);
}
moveTo(p, t = 0) {
return new Promise(async (resolve) => {
await Promise.all([this.setStart(p, t), this.setEnd(this.endPoint.clone().add(p), t)]);
resolve();
});
}
move(v, t = 0) {
return this.moveTo(this.startPoint.clone().add(v), t);
}
draw(c) {
c.beginPath();
c.lineWidth = this.thickness;
c.strokeStyle = this.color.toHex();
c.moveTo(this.startPoint.x, this.startPoint.y);
c.lineTo(this.endPoint.x, this.endPoint.y);
c.stroke();
c.closePath();
}
}
export class Circle extends SimulationElement {
radius;
startAngle;
endAngle;
counterClockwise;
thickness;
rotation;
fillCircle;
constructor(pos, radius, color = new Color(0, 0, 0), startAngle = 0, endAngle = 360, thickness = 1, rotation = 0, fill = true, counterClockwise = false) {
super(pos, color, 'circle');
this.radius = radius;
this.startAngle = startAngle;
this.endAngle = endAngle;
this.counterClockwise = counterClockwise;
this.thickness = thickness;
this.rotation = rotation;
this.fillCircle = fill;
}
setCounterClockwise(val) {
this.counterClockwise = val;
}
setFillCircle(val) {
this.fillCircle = val;
}
draw(c) {
c.beginPath();
c.strokeStyle = this.color.toHex();
c.fillStyle = this.color.toHex();
c.lineWidth = this.thickness;
c.arc(this.pos.x, this.pos.y, this.radius, degToRad(this.startAngle + this.rotation), degToRad(this.endAngle + this.rotation), this.counterClockwise);
if (this.endAngle > 0 && this.startAngle + 360 > this.endAngle) {
c.lineTo(this.pos.x, this.pos.y);
c.moveTo(this.pos.x, this.pos.y);
c.lineTo(this.pos.x + Math.cos(degToRad(this.rotation)) * this.radius, this.pos.y + Math.sin(degToRad(this.rotation)) * this.radius);
}
c.stroke();
if (this.fillCircle) {
c.fill();
}
c.closePath();
}
contains(p) {
return distance(p, this.pos) < this.radius;
}
scaleRadius(scale, t = 0, f) {
const initialRadius = this.radius;
const scaleChange = this.radius * scale - this.radius;
return transitionValues(() => {
this.radius *= scale;
}, (p) => {
this.radius += scaleChange * p;
this.radius = Math.max(0, this.radius);
return this.running;
}, () => {
this.radius = initialRadius * scale;
}, t, f);
}
setRadius(value, t = 0, f) {
const radChange = value - this.radius;
return transitionValues(() => {
this.radius = value;
}, (p) => {
this.radius += radChange * p;
this.radius = Math.max(0, this.radius);
return this.running;
}, () => {
this.radius = value;
}, t, f);
}
setThickness(val, t = 0, f) {
const thicknessChange = val - this.thickness;
return transitionValues(() => {
this.thickness = val;
}, (p) => {
this.thickness += thicknessChange * p;
return this.running;
}, () => {
this.thickness = val;
}, t, f);
}
setStartAngle(angle, t = 0, f) {
const angleChange = angle - this.startAngle;
return transitionValues(() => {
this.startAngle = angle;
}, (p) => {
this.startAngle += angleChange * p;
return this.running;
}, () => {
this.startAngle = angle;
}, t, f);
}
setEndAngle(angle, t = 0, f) {
const angleChange = angle - this.endAngle;
return transitionValues(() => {
this.endAngle = angle;
}, (p) => {
this.endAngle += angleChange * p;
return this.running;
}, () => {
this.endAngle = angle;
}, t, f);
}
rotate(amount, t = 0, f) {
const initialRotation = this.rotation;
const rotationChange = this.rotation + amount - this.rotation;
return transitionValues(() => {
this.rotation += amount;
}, (p) => {
this.rotation += rotationChange * p;
return this.running;
}, () => {
this.rotation = initialRotation + amount;
}, t, f);
}
rotateTo(deg, t = 0, f) {
const rotationChange = deg - this.rotation;
return transitionValues(() => {
this.rotation = deg;
}, (p) => {
this.rotation += rotationChange * p;
return this.running;
}, () => {
this.rotation = deg;
}, t, f);
}
clone() {
return new Circle(this.pos.clone(), this.radius, this.color.clone(), this.startAngle, this.endAngle, this.thickness, this.rotation, this.counterClockwise);
}
}
export class Polygon extends SimulationElement {
offsetPoint;
points;
rotation;
constructor(pos, points, color = new Color(0, 0, 0), r = 0, offsetPoint = new Vector(0, 0)) {
super(pos, color, 'polygon');
this.offsetPoint = offsetPoint;
this.points = points.map((p) => {
return new Vector(p.x + this.offsetPoint.x, p.y + this.offsetPoint.y);
});
this.rotation = r;
}
setPoints(points, t = 0, f) {
const lastPoint = this.points.length > 0 ? this.points[this.points.length - 1] : new Vector(0, 0);
if (points.length > this.points.length) {
while (points.length > this.points.length) {
this.points.push(new Vector(lastPoint.x, lastPoint.y));
}
}
const initial = this.points.map((p) => p.clone());
const changes = [
...points.map((p, i) => p.clone().sub(this.points[i])),
...this.points
.slice(points.length, this.points.length)
.map((point) => (points[points.length - 1] || new Vector(0, 0)).clone().sub(point))
];
return transitionValues(() => {
this.points = points.map((p) => new Vector(p.x + this.offsetPoint.x, p.y + this.offsetPoint.y));
}, (p) => {
this.points = this.points.map((point, i) => {
point.x += (changes[i]?.x || 0) * p;
point.y += (changes[i]?.y || 0) * p;
return point;
});
return this.running;
}, () => {
this.points = initial.map((p, i) => {
p.x += changes[i].x;
p.y += changes[i].y;
return p.clone();
});
this.points.splice(points.length, this.points.length);
}, t, f);
}
clone() {
return new Polygon(this.pos.clone(), [...this.points.map((p) => p.clone())], this.color.clone(), this.rotation, this.offsetPoint.clone());
}
rotate(deg, t = 0, f) {
const newRotation = this.rotation + deg;
return transitionValues(() => {
this.rotation = newRotation;
}, (p) => {
this.rotation += deg * p;
return this.running;
}, () => {
this.rotation = newRotation;
}, t, f);
}
rotateTo(deg, t = 0, f) {
const rotationChange = deg - this.rotation;
return transitionValues(() => {
this.rotation = deg;
}, (p) => {
this.rotation += rotationChange * p;
return this.running;
}, () => {
this.rotation = deg;
}, t, f);
}
draw(c) {
const points = this.points.map((p) => p.clone().rotate(this.rotation));
c.beginPath();
c.fillStyle = this.color.toHex();
if (points.length > 0) {
c.moveTo(points[0].x + this.pos.x, points[0].y + this.pos.y);
for (let i = 1; i < points.length; i++) {
c.lineTo(points[i].x + this.pos.x, points[i].y + this.pos.y);
}
}
c.fill();
c.closePath();
}
}
export class Plane extends SimulationElement3d {
points;
wireframe;
fillPlane;
constructor(pos, points, color = new Color(0, 0, 0), fill = true, wireframe = false, lighting = false) {
super(pos, color, lighting, 'plane');
this.points = points;
this.fillPlane = fill;
this.wireframe = wireframe;
}
clone() {
return new Plane(this.pos.clone(), this.points.map((p) => p.clone()), this.color.clone(), this.fillPlane, this.wireframe);
}
setPoints(points, t = 0, f) {
const lastPoint = this.points.length > 0 ? this.points[this.points.length - 1] : new Vector3(0, 0, 0);
if (points.length > this.points.length) {
while (points.length > this.points.length) {
this.points.push(new Vector3(lastPoint.x, lastPoint.y, lastPoint.z));
}
}
const initial = this.points.map((p) => p.clone());
const changes = [
...points.map((p, i) => p.clone().sub(this.points[i])),
...this.points
.slice(points.length, this.points.length)
.map((point) => (points[points.length - 1] || new Vector3(0, 0, 0)).clone().sub(point))
];
return transitionValues(() => {
this.points = points.map((p) => new Vector3(p.x, p.y, p.z));
}, (p) => {
this.points = this.points.map((point, i) => {
point.x += (changes[i]?.x || 0) * p;
point.y += (changes[i]?.y || 0) * p;
point.z += (changes[i]?.z || 0) * p;
return point;
});
return this.running;
}, () => {
this.points = initial.map((p, i) => {
p.x += changes[i].x;
p.y += changes[i].y;
p.z += changes[i].z;
return p.clone();
});
this.points.splice(points.length, this.points.length);
}, t, f);
}
draw(c, camera, displaySurface, lightSources, ambientLighting) {
let dampen = 0;
const maxDampen = 2;
if (this.lighting) {
for (let i = 0; i < lightSources.length; i++) {
const center = this.getCenter();
const normals = this.getNormals();
let normal;
if (angleBetweenVector3(camera.pos.clone().sub(center), normals[0]) > 90) {
normal = normals[1];
}
else {
normal = normals[0];
}
const vec = new Vector3(lightSources[i].pos.x, lightSources[i].pos.y, lightSources[i].pos.z);
const angle = angleBetweenVector3(normal, vec);
dampen += Math.max(ambientLighting, Math.sqrt(Math.max(0, 90 - Math.abs(angle)) / 90) * lightSources[i].intensity);
dampen = Math.min(dampen, maxDampen);
}
}
c.beginPath();
c.strokeStyle = '#000000';
const tempColor = this.color.clone();
if (this.lighting) {
tempColor.r *= dampen;
tempColor.g *= dampen;
tempColor.b *= dampen;
tempColor.r = clamp(tempColor.r, 0, 255);
tempColor.g = clamp(tempColor.g, 0, 255);
tempColor.b = clamp(tempColor.b, 0, 255);
}
c.fillStyle = tempColor.toHex();
c.lineWidth = 2;
for (let i = 0; i < this.points.length; i++) {
let p1;
let p2;
if (i === this.points.length - 1) {
p1 = projectPoint(this.points[i].clone().add(this.pos), camera, displaySurface);
p2 = projectPoint(this.points[0].clone().add(this.pos), camera, displaySurface);
}
else {
p1 = projectPoint(this.points[i].clone().add(this.pos), camera, displaySurface);
p2 = projectPoint(this.points[i + 1].clone().add(this.pos), camera, displaySurface);
}
if (!p1.behindCamera && !p2.behindCamera) {
if (i === 0) {
c.moveTo(p1.point.x, p1.point.y);
}
c.lineTo(p2.point.x, p2.point.y);
}
}
if (this.wireframe)
c.stroke();
if (this.fillPlane)
c.fill();
c.closePath();
}
getNormals() {
if (this.points.length >= 3) {
const vec1 = this.points[0].clone().sub(this.points[1]);
const vec2 = this.points[1].clone().sub(this.points[2]);
const res = vec1.cross(vec2).normalize();
return [res, res.clone().multiply(-1)];
}
return [new Vector3(0, 0, 0), new Vector3(0, 0, 0)];
}
getCenter() {
const avgVec = this.points.reduce((acc, curr) => acc.add(curr), new Vector3(0, 0, 0));
avgVec.divide(this.points.length);
return avgVec;
}
}
export class Cube extends SimulationElement3d {
width;
height;
depth;
planes = [];
points = [];
rotation;
fillCube;
wireframe;
constructor(pos, width, height, depth, color = new Color(0, 0, 0), rotation = new Vector3(0, 0, 0), fill = true, wireframe = false, lighting = false) {
super(pos, color, lighting, 'cube');
this.width = width / window.devicePixelRatio;
this.height = height / window.devicePixelRatio;
this.depth = depth / window.devicePixelRatio;
this.wireframe = wireframe;
this.fillCube = fill;
this.rotation = rotation;
this.generatePoints();
this.generatePlanes();
}
generatePoints() {
this.points = [
new Vector3(-this.width / 2, -this.height / 2, -this.depth / 2),
new Vector3(this.width / 2, -this.height / 2, -this.depth / 2),
new Vector3(this.width / 2, this.height / 2, -this.depth / 2),
new Vector3(-this.width / 2, this.height / 2, -this.depth / 2),
new Vector3(-this.width / 2, -this.height / 2, this.depth / 2),
new Vector3(this.width / 2, -this.height / 2, this.depth / 2),
new Vector3(this.width / 2, this.height / 2, this.depth / 2),
new Vector3(-this.width / 2, this.height / 2, this.depth / 2)
];
}
generatePlanes() {
const points = this.points.map((p) => p.clone().rotate(this.rotation).add(this.pos));
this.planes = [
new Plane(this.pos, [points[0], points[1], points[2], points[3]], this.color, this.fillCube, this.wireframe, this.lighting),
new Plane(this.pos, [points[0], points[1], points[5], points[4]], this.color, this.fillCube, this.wireframe, this.lighting),
new Plane(this.pos, [points[4], points[5], points[6], points[7]], this.color, this.fillCube, this.wireframe, this.lighting),
new Plane(this.pos, [points[3], points[2], points[6], points[7]], this.color, this.fillCube, this.wireframe, this.lighting),
new Plane(this.pos, [points[0], points[3], points[7], points[4]], this.color, this.fillCube, this.wireframe, this.lighting),
new Plane(this.pos, [points[2], points[1], points[5], points[6]], this.color, this.fillCube, this.wireframe, this.lighting)
];
}
updatePoints() {
const newPointValues = [
[-this.width / 2, -this.height / 2, -this.depth / 2],
[this.width / 2, -this.height / 2, -this.depth / 2],
[this.width / 2, this.height / 2, -this.depth / 2],
[-this.width / 2, this.height / 2, -this.depth / 2],
[-this.width / 2, -this.height / 2, this.depth / 2],
[this.width / 2, -this.height / 2, this.depth / 2],
[this.width / 2, this.height / 2, this.depth / 2],
[-this.width / 2, this.height / 2, this.depth / 2]
];
newPointValues.forEach((val, i) => {
this.points[i].x = val[0];
this.points[i].y = val[1];
this.points[i].z = val[2];
});
}
updatePlanes() {
const points = this.points.map((p) => p.clone().rotate(this.rotation));
const pointsArr = [
[points[0], points[1], points[2], points[3]],
[points[0], points[1], points[5], points[4]],
[points[4], points[5], points[6], points[7]],
[points[3], points[2], points[6], points[7]],
[points[0], points[3], points[7], points[4]],
[points[2], points[1], points[5], points[6]]
];
this.planes.forEach((plane, index) => {
plane.setPoints(pointsArr[index]);
});
}
rotate(amount, t = 0, f) {
const initial = this.rotation.clone();
return transitionValues(() => {
this.rotation.x = initial.x + amount.x;
this.rotation.y = initial.y + amount.y;
this.rotation.z = initial.z + amount.z;
}, (p) => {
this.rotation.x += amount.x * p;
this.rotation.y += amount.y * p;
this.rotation.z += amount.z * p;
return this.running;
}, () => {
this.rotation.x = initial.x + amount.x;
this.rotation.y = initial.y + amount.y;
this.rotation.z = initial.z + amount.z;
}, t, f);
}
rotateTo(amount, t = 0, f) {
const changeX = amount.x - this.rotation.x;
const changeY = amount.y - this.rotation.y;
const changeZ = amount.z - this.rotation.z;
return transitionValues(() => {
this.rotation.x = amount.x;
this.rotation.y = amount.y;
this.rotation.z = amount.z;
}, (p) => {
this.rotation.x += changeX * p;
this.rotation.y += changeY * p;
this.rotation.z += changeZ * p;
return this.running;
}, () => {
this.rotation.x = amount.x;
this.rotation.y = amount.y;
this.rotation.z = amount.z;
}, t, f);
}
setHeight(amount, t = 0, f) {
const heightChange = amount - this.height;
return transitionValues(() => {
this.height = amount;
this.updatePoints();
}, (p) => {
this.height += heightChange * p;
this.updatePoints();
return this.running;
}, () => {
this.height = amount;
this.updatePoints();
}, t, f);
}
setDepth(amount, t = 0, f) {
const depthChange = amount - this.depth;
return transitionValues(() => {
this.depth = amount;
this.updatePoints();
}, (p) => {
this.depth += depthChange * p;
this.updatePoints();
return this.running;
}, () => {
this.depth = amount;
this.updatePoints();
}, t, f);
}
setWidth(amount, t = 0, f) {
const widthChange = amount - this.width;
return transitionValues(() => {
this.width = amount;
this.updatePoints();
}, (p) => {
this.width += widthChange * p;
this.updatePoints();
return this.running;
}, () => {
this.width = amount;
this.updatePoints();
}, t, f);
}
scaleHeight(amount, t = 0, f) {
const height = this.height * amount;
return this.setHeight(height, t, f);
}
scaleWidth(amount, t = 0, f) {
const width = this.width * amount;
return this.setWidth(width, t, f);
}
scaleDepth(amount, t = 0, f) {
const depth = this.depth * amount;
return this.setDepth(depth, t, f);
}
draw(c, camera, displaySurface, lightSources, ambientLighting) {
this.planes.forEach((plane) => {
plane.color = this.color;
});
this.updatePlanes();
this.planes = sortPlanes(this.planes, camera);
for (let i = 0; i < this.planes.length; i++) {
this.planes[i].draw(c, camera, displaySurface, lightSources, ambientLighting);
}
}
}
export class Square extends SimulationElement {
width;
height;
rotation;
showNodeVectors;
hovering;
offsetPoint;
topLeft;
topRight;
bottomLeft;
bottomRight;
constructor(pos, width, height, color = new Color(0, 0, 0), offsetPoint = new Vector(0, 0), rotation = 0) {
super(pos, color, 'square');
this.width = width;
this.height = height;
this.rotation = rotation;
this.showNodeVectors = false;
this.hovering = false;
this.topLeft = new Vector(0, 0);
this.topRight = new Vector(0, 0);
this.bottomLeft = new Vector(0, 0);
this.bottomRight = new Vector(0, 0);
this.offsetPoint = offsetPoint;
this.updateOffsetPosition(offsetPoint);
}
generateVectors() {
this.topLeft = new Vector(-this.width / 2 - this.offsetPoint.x, -this.height / 2 - this.offsetPoint.y);
this.topRight = new Vector(this.width / 2 - this.offsetPoint.x, -this.height / 2 - this.offsetPoint.y);
this.bottomLeft = new Vector(-this.width / 2 - this.offsetPoint.x, this.height / 2 - this.offsetPoint.y);
this.bottomRight = new Vector(this.width / 2 - this.offsetPoint.x, this.height / 2 - this.offsetPoint.y);
}
updateOffsetPosition(p) {
this.offsetPoint = p.clone();
this.generateVectors();
}
setNodeVectors(show) {
this.showNodeVectors = show;
}
rotate(deg, t = 0, f) {
const initial = this.rotation;
return transitionValues(() => {
this.rotation = initial + deg;
}, (p) => {
this.rotation += deg * p;
return this.running;
}, () => {
this.rotation = initial + deg;
}, t, f);
}
rotateTo(deg, t = 0, f) {
const rotationChange = deg - this.rotation;
return transitionValues(() => {
this.rotation = deg;
}, (p) => {
this.rotation += rotationChange * p;
return this.running;
}, () => {
this.rotation = deg;
}, t, f);
}
draw(c) {
this.generateVectors();
const topRight = this.topRight.clone().rotate(this.rotation);
const topLeft = this.topLeft.clone().rotate(this.rotation);
const bottomRight = this.bottomRight.clone().rotate(this.rotation);
const bottomLeft = this.bottomLeft.clone().rotate(this.rotation);
c.beginPath();
c.fillStyle = this.color.toHex();
c.moveTo(this.pos.x + topLeft.x + this.offsetPoint.x, this.pos.y + topLeft.y + this.offsetPoint.y);
c.lineTo(this.pos.x + topRight.x + this.offsetPoint.x, this.pos.y + topRight.y + this.offsetPoint.y);
c.lineTo(this.pos.x + bottomRight.x + this.offsetPoint.x, this.pos.y + bottomRight.y + this.offsetPoint.y);
c.lineTo(this.pos.x + bottomLeft.x + this.offsetPoint.x, this.pos.y + bottomLeft.y + this.offsetPoint.y);
c.fill();
c.closePath();
if (this.showNodeVectors) {
this.topLeft.draw(c, new Vector(this.pos.x + this.offsetPoint.x, this.pos.y + this.offsetPoint.y));
this.topRight.draw(c, new Vector(this.pos.x + this.offsetPoint.x, this.pos.y + this.offsetPoint.y));
this.bottomLeft.draw(c, new Vector(this.pos.x + this.offsetPoint.x, this.pos.y + this.offsetPoint.y));
this.bottomRight.draw(c, new Vector(this.pos.x + this.offsetPoint.x, this.pos.y + this.offsetPoint.y));
}
}
scale(value, t = 0, f) {
return new Promise(async (resolve) => {
await Promise.all([this.scaleWidth(value, t, f), this.scaleHeight(value, t, f)]);
resolve();
});
}
scaleWidth(value, t = 0, f) {
const width = this.width * value;
return this.setWidth(width, t, f);
}
scaleHeight(value, t = 0, f) {
const height = this.height * value;
return this.setHeight(height, t, f);
}
setWidth(value, t = 0, f) {
const initial = this.width;
const change = value - initial;
return transitionValues(() => {
this.width = value;
}, (p) => {
this.width += change * p;
return this.running;
}, () => {
this.width = value;
}, t, f);
}
setHeight(value, t = 0, f) {
const initial = this.height;
const change = value - initial;
return transitionValues(() => {
this.height = value;
}, (p) => {
this.height += change * p;
return this.running;
}, () => {
this.height = value;
}, t, f);
}
contains(p) {
const topLeftVector = this.topLeft.clone();
const topRightVector = this.topRight.clone();
const bottomLeftVector = this.bottomLeft.clone();
const cursorVector = new Vector(p.x - this.pos.x - this.offsetPoint.x, p.y - this.pos.y - this.offsetPoint.y);
cursorVector.rotate(-this.rotation);
if (cursorVector.x > bottomLeftVector.x &&
cursorVector.x < topRightVector.x &&
cursorVector.y > topLeftVector.y &&
cursorVector.y < bottomLeftVector.y) {
return true;
}
return false;
}
clone() {
return new Square(this.pos.clone(), this.width, this.height, this.color.clone(), this.offsetPoint.clone(), this.rotation);
}
}
class Event {
event;
callback;
constructor(event, callback) {
this.event = event;
this.callback = callback;
}
}
export class Line3d extends SimulationElement3d {
p1;
p2;
thickness;
constructor(p1, p2, color = new Color(0, 0, 0), thickness = 1, lighting = false, id = '') {
super(p1, color, lighting, 'line', id);
this.p1 = p1;
this.p2 = p2;
this.thickness = thickness;
}
draw(ctx, camera, displaySurface) {
const p1 = projectPoint(this.p1, camera, displaySurface);
const p2 = projectPoint(this.p2, camera, displaySurface);
if (!p1.behindCamera && !p2.behindCamera) {
ctx.beginPath();
ctx.lineWidth = this.thickness;
ctx.strokeStyle = this.color.toHex();
ctx.moveTo(p1.point.x, p1.point.y);
ctx.lineTo(p2.point.x, p2.point.y);
ctx.stroke();
ctx.closePath();
}
}
}
export class Simulation {
scene;
fitting;
bgColor;
canvas = null;
width = 0;
height = 0;
running;
_prevReq;
events;
ctx = null;
camera;
center;
displaySurface;
forward = new Vector3(0, 0, 1);
backward = new Vector3(0, 0, -1);
left = new Vector3(-1, 0, 0);
right = new Vector3(1, 0, 0);
up = new Vector3(0, -1, 0);
down = new Vector3(0, 1, 0);
lightSources;
ambientLighting;
planesSortFunc;
constructor(el, cameraPos = new Vector3(0, 0, -200), cameraRot = new Vector3(0, 0, 0), displaySurfaceDepth, center = new Vector(0, 0), displaySurfaceSize) {
this.scene = [];
this.fitting = false;
this.bgColor = new Color(255, 255, 255);
this.running = true;
this._prevReq = 0;
this.events = [];
this.camera = new Camera(cameraPos, cameraRot);
this.center = center;
this.displaySurface = new Vector3(0, 0, 0);
this.lightSources = [];
this.ambientLighting = 0.25;
this.planesSortFunc = sortPlanes;
this.setDirections();
const defaultDepth = 2000;
this.canvas = typeof el === 'string' ? document.getElementById(el) : el;
if (!this.canvas) {
console.error(`Canvas with id "${el}" not found`);
return;
}
window.addEventListener('resize', () => this.resizeCanvas());
this.resizeCanvas();
if (displaySurfaceSize) {
this.displaySurface = new Vector3(displaySurfaceSize.x, displaySurfaceSize.y, displaySurfaceDepth || defaultDepth);
}
else {
this.displaySurface = new Vector3(this.width / 2, this.height / 2, displaySurfaceDepth || defaultDepth);
}
}
start() {
if (!this.canvas)
return;
const ctx = this.canvas.getContext('2d');
if (!ctx)
return;
ctx.scale(devicePixelRatio, devicePixelRatio);
this.ctx = ctx;
this.render(ctx);
}
setSortFunc(func) {
this.planesSortFunc = func;
this.scene.forEach((element) => {
if (element._isSceneCollection) {
element.setSortFunc(func);
}
});
}
updateSceneLightSources() {
this.scene.forEach((obj) => {
if (obj._isSceneCollection) {
obj.setLightSources(this.lightSources);
}
});
}
setLightSources(sources) {
this.lightSources = sources;
this.updateSceneLightSources();
}
addLightSource(source) {
this.lightSources.push(source);
this.updateSceneLightSources();
}
removeLightSourceWithId(id) {
this.lightSources = this.lightSources.filter((source) => source.id !== id);
this.updateSceneLightSources();
}
getLightSourceWithId(id) {
for (let i = 0; i < this.lightSources.length; i++) {
if (this.lightSources[i].id === id)
return this.lightSources[i];
}
return null;
}
setAmbientLighting(val) {
this.ambientLighting = val;
this.scene.forEach((obj) => {
if (obj._isSceneCollection) {
obj.setAmbientLighting(this.ambientLighting);
}
});
}
setDirections() {
const degRotation = vector3RadToDeg(new Vector3(this.camera.rot.x, this.camera.rot.y, this.camera.rot.z));
this.forward = new Vector3(0, 0, 1).rotate(degRotation);
this.backward = this.forward.clone().multiply(-1);
this.left = new Vector3(-1, 0, 0).rotate(degRotation);
this.right = this.left.clone().multiply(-1);
}
render(c) {
if (!this.canvas)
return;
c.clearRect(0, 0, this.canvas.width, this.canvas.height);
c.beginPath();
c.fillStyle = this.bgColor.toHex();
c.fillRect(0, 0, this.canvas.width, this.canvas.height);
c.closePath();
let planes = [];
this.scene.forEach((element) => {
if (element._3d) {
if (element.type === 'plane') {
planes.push(element);
}
else {
element.draw(c, this.camera, this.displaySurface, this.lightSources, this.ambientLighting);
}
}
else {
element.draw(c);
}
element.onFrame();
});
planes = this.planesSortFunc(planes, this.camera);
planes.forEach((plane) => {
plane.draw(c, this.camera, this.displaySurface, this.lightSources, this.ambientLighting);
});
if (this.running) {
this._prevReq = window.requestAnimationFrame(() => this.render(c));
}
}
end() {
this.running = false;
for (let i = 0; i < this.scene.length; i++) {
this.scene[i].end();
}
window.removeEventListener('resize', () => this.resizeCanvas());
window.cancelAnimationFrame(this._prevReq);
}
add(element, id = null) {
if (!this.canvas)
return;
if (id !== null) {
element.setId(id);
}
if (element._isSceneCollection) {
element.set3dObjects(this.camera, this.displaySurface);
element.setAmbientLighting(this.ambientLighting);
element.setSortFunc(this.planesSortFunc);
}
this.scene.push(element);
}
removeWithId(id) {
this.scene = this.scene.filter((item) => item.id !== id);
}
removeWithObject(element) {
this.scene = this.scene.filter((item) => item === element);
}
on(event, callback) {
if (!this.canvas)
return;
this.events.push(new Event(event, callback));
// @ts-ignore
this.canvas.addEventListener(event, callback);
}
removeListener(event, callback) {
this.events = this.events.filter((e) => {
if (e.event === event && e.callback == callback) {
if (this.canvas) {
// @ts-ignore
this.canvas.removeEventListener(e.event, e.callback);
}
return false;
}
return true;
});
}
fitElement() {
if (!this.canvas)
return;
this.fitting = true;
this.resizeCanvas();
}
setSize(x, y) {
if (!this.canvas)
return;
this.canvas.width = x * devicePixelRatio;
this.canvas.height = y * devicePixelRatio;
this.canvas.style.width = x + 'px';
this.canvas.style.height = y + 'px';
this.width = x;
this.height = y;
this.fitting = false;
}
setBgColor(color) {
this.bgColor = color.clone();
}
resizeCanvas() {
if (!this.canvas)
return;
let width = this.canvas.width;
let height = this.canvas.height;
if (this.fitting && this.canvas.parentElement) {
this.width = this.canvas.parentElement.clientWidth;
this.height = this.canvas.parentElement.clientHeight;
width = this.width;
height = this.height;
this.canvas.width = width * devicePixelRatio;
this.canvas.height = height * devicePixelRatio;
this.canvas.style.width = width + 'px';
this.canvas.style.height = height + 'px';
this.displaySurface.x = this.width / 2;
this.displaySurface.y = this.height / 2;
if (this.ctx) {
this.ctx.scale(devicePixelRatio, devicePixelRatio);
}
}
}
empty() {
this.scene = [];
}
moveCamera(v, t = 0, f) {
const initial = this.camera.pos.clone();
return transitionValues(() => {
this.camera.pos.add(v);
}, (p) => {
this.camera.pos.x += v.x * p;
this.camera.pos.y += v.y * p;
this.camera.pos.z += v.z * p;
return this.running;
}, () => {
this.camera.pos = initial.add(v);
}, t, f);
}
moveCameraTo(v, t = 0, f) {
const changeX = v.x - this.camera.pos.x;
const changeY = v.y - this.camera.pos.y;
const changeZ = v.z - this.camera.pos.z;
return transitionValues(() => {
this.camera.pos = v.clone();
}, (p) => {
this.camera.pos.x += changeX * p;
this.camera.pos.y += changeY * p;
this.camera.pos.z += changeZ * p;
re