psychotherapy/src/main/js/Main.ts

280 lines
8.0 KiB
TypeScript
Raw Normal View History

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[];
2021-10-31 17:55:13 +01:00
color: string;
2021-10-31 15:12:56 +01:00
thickness: number;
/**
* Constructs a new `Line`.
*
2021-10-31 17:55:13 +01:00
* @param color the color to draw the line in
2021-10-31 15:12:56 +01:00
* @param thickness the relative thickness of the line
*/
2021-10-31 17:55:13 +01:00
constructor(color: string, thickness: number) {
2021-10-31 15:12:56 +01:00
this.segments = [];
2021-10-31 17:55:13 +01:00
this.color = color;
2021-10-31 15:12:56 +01:00
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]))
2021-10-31 17:55:13 +01:00
&& this.color == other.color && this.thickness === other.thickness;
2021-10-31 15:12:56 +01:00
}
}
2021-10-31 16:14:57 +01:00
/**
* Global constants.
*/
const settings = {
2021-10-31 17:55:13 +01:00
// Radius of the central dot
2021-10-31 17:07:11 +01:00
dotRadius: 20,
2021-10-31 17:55:13 +01:00
// How much percent of the window width should be filled in either direction before zooming starts
fillGoal: 0.6,
// Colors to assign to the lines
lineColors: ["#0072bd", "#d95319", "#edb120", "#7e2f8e", "#77ac30"],
// Length of an individual line piece
lineLength: 5,
2021-10-31 17:55:13 +01:00
// Desired thickness of a line, based on the current level
2021-10-31 17:07:11 +01:00
lineThicknesses: [1, 5, 15],
2021-10-31 17:55:13 +01:00
// Number of milliseconds between each new line segment
stepTime: 25,
2021-10-31 17:55:13 +01:00
// Number of steps before performing induction
stepsPerLevel: 15,
2021-10-31 17:55:13 +01:00
// How responsive zooming should be. Lower values are smoother but change zooming speed more often
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 18:09:11 +01:00
const lineColorOffset = Math.floor(Math.random() * settings.lineColors.length);
2021-10-31 17:55:13 +01:00
const lines: Line[] = [
2021-10-31 18:09:11 +01:00
new Line(settings.lineColors[lineColorOffset], settings.lineThicknesses[0]),
new Line(settings.lineColors[(lineColorOffset + 1) % settings.lineColors.length], settings.lineThicknesses[1])
2021-10-31 17:55:13 +01:00
];
2021-10-31 15:42:30 +01:00
let step = 0;
2021-10-31 15:12:56 +01:00
setInterval(() => {
const currentLevel = lines.findIndex(it => it.segments.length !== settings.stepsPerLevel ** 2);
2021-10-31 17:01:50 +01:00
// Add low-level segment
const length = settings.lineLength ** (2 + currentLevel);
2021-10-31 17:01:50 +01:00
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 % settings.stepsPerLevel === 0) {
2021-10-31 17:01:50 +01:00
if (lines.length <= inductionLevel) {
2021-10-31 17:55:13 +01:00
lines[inductionLevel] = new Line(
2021-10-31 18:09:11 +01:00
settings.lineColors[(lineColorOffset + inductionLevel) % settings.lineColors.length],
2021-10-31 17:55:13 +01:00
settings.lineThicknesses[level] / zoomFactor
);
2021-10-31 17:01:50 +01:00
}
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(
2021-10-31 17:55:13 +01:00
(zoomFactor * maxPoint.x) / (canvas.width / 2 * settings.fillGoal),
(zoomFactor * maxPoint.y) / (canvas.height / 2 * settings.fillGoal)
2021-10-31 17:01:50 +01:00
);
if (excessFactor > 1) {
2021-10-31 17:07:11 +01:00
zoomFactor /= excessFactor ** settings.zoomSpeed;
2021-10-31 16:31:59 +01:00
}
2021-10-31 17:07:11 +01:00
ctx.scale(zoomFactor, zoomFactor);
2021-10-30 17:30:21 +02:00
// Lines
ctx.lineCap = "round";
2021-10-30 17:30:21 +02:00
ctx.strokeStyle = "#ffffff";
2021-10-31 15:12:56 +01:00
for (const line of lines) {
2021-10-31 17:55:13 +01:00
ctx.strokeStyle = line.color;
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();
}