2021-10-30 14:30:33 +02:00
|
|
|
// @ts-ignore
|
|
|
|
const {$, doAfterLoad} = window.fwdekker;
|
|
|
|
|
|
|
|
|
2021-10-31 15:12:56 +01:00
|
|
|
/**
|
|
|
|
* A two-dimensional point.
|
|
|
|
*/
|
|
|
|
class Point {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructs a new `Point`.
|
|
|
|
*
|
|
|
|
* @param x the X coordinate of the `Point`
|
|
|
|
* @param y the Y coordinate of the `Point`
|
|
|
|
*/
|
|
|
|
constructor(x: number, y: number) {
|
|
|
|
this.x = x;
|
|
|
|
this.y = y;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns `true` if and only if this `Point` is exactly the same as `other`.
|
|
|
|
*
|
|
|
|
* @param other the `Point` to check equality against
|
|
|
|
* @return `true` if and only if this `Point` is exactly the same as `other`
|
|
|
|
*/
|
|
|
|
equals(other: Point): boolean {
|
|
|
|
return this.x === other.x && this.y === other.y;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A segment that is part of a [Line].
|
|
|
|
*/
|
|
|
|
class LineSegment {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructs a new [LineSegment].
|
|
|
|
*
|
|
|
|
* @param x the relative horizontal translation of this `LineSegment`
|
|
|
|
* @param y the relative vertical translation of this `LineSegment`
|
|
|
|
*/
|
|
|
|
constructor(x: number, y: number) {
|
|
|
|
this.x = x;
|
|
|
|
this.y = y;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-10-31 17:01:50 +01:00
|
|
|
/**
|
|
|
|
* Returns the angle formed by this `LineSegment` in radians.
|
|
|
|
*
|
|
|
|
* @return the angle formed by this `LineSegment` in radians
|
|
|
|
*/
|
|
|
|
angle(): number {
|
|
|
|
return Math.atan2(this.y, this.x);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-10-31 15:12:56 +01:00
|
|
|
/**
|
|
|
|
* Returns `true` if and only if this `LineSegment` is exactly the same as `other`.
|
|
|
|
*
|
|
|
|
* @param other the `LineSegment` to check equality against
|
|
|
|
* @return `true` if and only if this `LineSegment` is exactly the same as `other`
|
|
|
|
*/
|
|
|
|
equals(other: LineSegment): boolean {
|
|
|
|
return this.x === other.x && this.y === other.y;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A line that is made up of [LineSegment]s.
|
|
|
|
*/
|
|
|
|
class Line {
|
|
|
|
segments: LineSegment[];
|
|
|
|
thickness: number;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructs a new `Line`.
|
|
|
|
*
|
|
|
|
* @param thickness the relative thickness of the line
|
|
|
|
*/
|
|
|
|
constructor(thickness: number) {
|
|
|
|
this.segments = [];
|
|
|
|
this.thickness = thickness;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-10-31 15:42:30 +01:00
|
|
|
/**
|
|
|
|
* Returns the sum of a slice of this `Line`'s [LineSegment]s.
|
|
|
|
*
|
|
|
|
* @param start the start of the slice to sum over
|
|
|
|
* @param end the end of the slice to sum over
|
|
|
|
* @return the sum of a slice of this `Line`'s [LineSegment]s
|
|
|
|
*/
|
|
|
|
sumSegmentsSlice(start?: number, end?: number): LineSegment {
|
|
|
|
const slice = this.segments.slice(start, end);
|
|
|
|
return new LineSegment(slice.reduce((acc, it) => acc + it.x, 0), slice.reduce((acc, it) => acc + it.y, 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-10-31 15:12:56 +01:00
|
|
|
/**
|
|
|
|
* Returns `true` if and only if this `Line` is exactly the same as `other`.
|
|
|
|
*
|
|
|
|
* @param other the `Line` to check equality against
|
|
|
|
* @return `true` if and only if this `Line` is exactly the same as `other`
|
|
|
|
*/
|
|
|
|
equals(other: Line): boolean {
|
|
|
|
return this.segments.every((_, i) => this.segments[i].equals(other.segments[i]))
|
|
|
|
&& this.thickness === other.thickness;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-10-31 16:14:57 +01:00
|
|
|
/**
|
|
|
|
* Global constants.
|
|
|
|
*/
|
|
|
|
const settings = {
|
|
|
|
dotRadius: 10,
|
2021-10-31 16:31:59 +01:00
|
|
|
fillGoal: 0.3,
|
2021-10-31 16:14:57 +01:00
|
|
|
stepTime: 250,
|
|
|
|
stepsPerLevel: 5,
|
2021-10-31 17:01:50 +01:00
|
|
|
zoomSpeed: 1 / 30,
|
2021-10-31 16:14:57 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Unhide main element
|
2021-10-31 15:12:56 +01:00
|
|
|
doAfterLoad(() => $("main").classList.remove("hidden"));
|
|
|
|
|
|
|
|
|
2021-10-31 16:14:57 +01:00
|
|
|
// Application loop(s)
|
2021-10-30 14:30:33 +02:00
|
|
|
doAfterLoad(() => {
|
2021-10-31 15:12:56 +01:00
|
|
|
const canvas = $("#art");
|
|
|
|
const ctx = canvas.getContext("2d");
|
2021-10-30 14:30:33 +02:00
|
|
|
|
2021-10-31 16:14:57 +01:00
|
|
|
|
|
|
|
// Model
|
2021-10-31 17:01:50 +01:00
|
|
|
// TODO: Dynamic thickness calculation
|
2021-10-31 16:14:57 +01:00
|
|
|
const lines: Line[] = [new Line(1), new Line(5)];
|
2021-10-31 15:42:30 +01:00
|
|
|
let step = 0;
|
2021-10-31 15:12:56 +01:00
|
|
|
setInterval(() => {
|
2021-10-31 17:01:50 +01:00
|
|
|
const currentLevel = lines.findIndex(it => it.segments.length !== 25);
|
|
|
|
|
|
|
|
// Add low-level segment
|
|
|
|
const length = settings.stepsPerLevel ** (2 + currentLevel);
|
|
|
|
let angle: number;
|
|
|
|
if (lines[currentLevel].segments.length > 0)
|
|
|
|
angle = lines[currentLevel].segments.slice(-1)[0].angle() + (random_normal() - 0.5) * Math.PI * 2;
|
|
|
|
else
|
|
|
|
angle = Math.random() * Math.PI * 2;
|
|
|
|
lines[currentLevel].segments.push(new LineSegment(Math.cos(angle) * length, Math.sin(angle) * length));
|
|
|
|
|
|
|
|
// Add induction segments
|
|
|
|
for (const level of [1, 2]) {
|
|
|
|
const inductionLevel = currentLevel + level;
|
|
|
|
const lowerLines = lines[inductionLevel - 1];
|
|
|
|
|
|
|
|
if (lowerLines.segments.length !== 0 && lowerLines.segments.length % 5 === 0) {
|
|
|
|
if (lines.length <= inductionLevel) {
|
|
|
|
lines[inductionLevel] = new Line(5 ** inductionLevel);
|
|
|
|
}
|
|
|
|
|
|
|
|
lines[inductionLevel].segments.push(lowerLines.sumSegmentsSlice(-settings.stepsPerLevel));
|
2021-10-31 15:42:30 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-31 17:01:50 +01:00
|
|
|
// Update shared info
|
2021-10-31 15:42:30 +01:00
|
|
|
step++;
|
2021-10-31 16:14:57 +01:00
|
|
|
}, settings.stepTime);
|
|
|
|
|
2021-10-31 15:12:56 +01:00
|
|
|
|
|
|
|
// Resize
|
|
|
|
const resize = () => {
|
|
|
|
canvas.width = window.innerWidth;
|
|
|
|
canvas.height = window.innerHeight;
|
|
|
|
};
|
|
|
|
window.addEventListener("resize", resize, false);
|
|
|
|
resize();
|
2021-10-30 17:30:21 +02:00
|
|
|
|
2021-10-31 16:14:57 +01:00
|
|
|
|
2021-10-30 17:30:21 +02:00
|
|
|
// Draw
|
2021-10-31 17:01:50 +01:00
|
|
|
let maxPoint = new Point(0, 0);
|
2021-10-31 16:31:59 +01:00
|
|
|
let zoomFactor = 1;
|
2021-10-30 14:30:33 +02:00
|
|
|
const draw = () => {
|
2021-10-31 16:14:57 +01:00
|
|
|
ctx.restore();
|
|
|
|
ctx.save();
|
2021-10-31 15:12:56 +01:00
|
|
|
|
|
|
|
// Background
|
2021-10-30 14:30:33 +02:00
|
|
|
ctx.fillStyle = "#000000";
|
|
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
2021-10-31 16:14:57 +01:00
|
|
|
// Draw from center
|
|
|
|
const center = new Point(canvas.width / 2, canvas.height / 2);
|
|
|
|
ctx.translate(center.x, center.y);
|
|
|
|
|
2021-10-30 17:30:21 +02:00
|
|
|
// Center dot
|
2021-10-30 14:30:33 +02:00
|
|
|
ctx.beginPath();
|
2021-10-31 16:14:57 +01:00
|
|
|
ctx.strokeStyle = "#ffffff";
|
|
|
|
ctx.arc(0, 0, settings.dotRadius, 0, 2 * Math.PI);
|
|
|
|
ctx.stroke();
|
|
|
|
|
|
|
|
// Zoom
|
2021-10-31 17:01:50 +01:00
|
|
|
const excessFactor = Math.max(
|
|
|
|
((1 / zoomFactor) * maxPoint.x) / (canvas.width * settings.fillGoal),
|
|
|
|
((1 / zoomFactor) * maxPoint.y) / (canvas.height * settings.fillGoal)
|
|
|
|
);
|
|
|
|
if (excessFactor > 1) {
|
|
|
|
zoomFactor *= excessFactor ** settings.zoomSpeed;
|
2021-10-31 16:31:59 +01:00
|
|
|
}
|
|
|
|
ctx.scale(1 / zoomFactor, 1 / zoomFactor);
|
2021-10-30 17:30:21 +02:00
|
|
|
|
|
|
|
// Lines
|
|
|
|
ctx.strokeStyle = "#ffffff";
|
2021-10-31 15:12:56 +01:00
|
|
|
for (const line of lines) {
|
2021-10-31 15:42:30 +01:00
|
|
|
ctx.lineWidth = line.thickness;
|
2021-10-31 15:12:56 +01:00
|
|
|
ctx.beginPath();
|
2021-10-31 16:14:57 +01:00
|
|
|
ctx.moveTo(0, 0);
|
2021-10-31 15:12:56 +01:00
|
|
|
|
2021-10-31 16:14:57 +01:00
|
|
|
let lastPos = new Point(0, 0);
|
2021-10-31 15:12:56 +01:00
|
|
|
for (const segment of line.segments) {
|
|
|
|
lastPos = new Point(lastPos.x + segment.x, lastPos.y + segment.y);
|
|
|
|
ctx.lineTo(lastPos.x, lastPos.y);
|
2021-10-31 16:31:59 +01:00
|
|
|
|
2021-10-31 17:01:50 +01:00
|
|
|
maxPoint = new Point(
|
|
|
|
Math.max(maxPoint.x, Math.abs(lastPos.x)),
|
|
|
|
Math.max(maxPoint.y, Math.abs(lastPos.y))
|
|
|
|
);
|
2021-10-31 15:12:56 +01:00
|
|
|
}
|
2021-10-30 17:30:21 +02:00
|
|
|
|
2021-10-31 15:12:56 +01:00
|
|
|
ctx.stroke();
|
2021-10-30 17:30:21 +02:00
|
|
|
}
|
|
|
|
|
2021-10-31 16:14:57 +01:00
|
|
|
// Repeat
|
2021-10-30 17:30:21 +02:00
|
|
|
window.requestAnimationFrame(draw);
|
2021-10-30 14:30:33 +02:00
|
|
|
};
|
2021-10-30 17:30:21 +02:00
|
|
|
window.requestAnimationFrame(draw);
|
2021-10-30 14:30:33 +02:00
|
|
|
});
|
2021-10-30 17:30:21 +02:00
|
|
|
|
2021-10-31 15:12:56 +01:00
|
|
|
|
2021-10-30 17:30:21 +02:00
|
|
|
// Taken from https://stackoverflow.com/a/49434653/
|
|
|
|
function random_normal(): number {
|
|
|
|
let u = 0;
|
|
|
|
while (u === 0) u = Math.random();
|
|
|
|
let v = 0;
|
|
|
|
while (v === 0) v = Math.random();
|
|
|
|
|
|
|
|
const num = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v) / 10.0 + 0.5;
|
|
|
|
if (num >= 0 && num < 1) return num;
|
|
|
|
return random_normal();
|
|
|
|
}
|