rg/naloga_2/main.js
2024-11-24 20:20:39 +01:00

330 lines
8.4 KiB
JavaScript

const addButton = document.getElementById('add');
const removeButton = document.getElementById('remove');
const c0Button = document.getElementById('c0');
const c1Button = document.getElementById('c1');
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
let adding = false;
/** @type {number|null} */
let selected = null;
let mouseDown = false;
/** @type {number|null} */
let movePointIndex = null;
/** @type {{x: number, y: number}[][]} */
const curves = [];
addButton.addEventListener('click', () => {
adding = true;
curves.push([]);
selected = curves.length - 1;
});
removeButton.addEventListener('click', () => {
if (selected === null) return;
curves.splice(selected, 1);
selected = null;
draw();
});
c0Button.addEventListener('click', () => {
if (curves.length <= 1 || selected === null) return;
let [ci, pi] = getClosestEndPoint(curves[selected][0]);
if (ci === null || pi === null) return;
curves[selected][0] = curves[ci][pi];
[ci, pi] = getClosestEndPoint(curves[selected][3]);
if (ci === null || pi === null) return;
curves[selected][3] = curves[ci][pi];
draw();
});
c1Button.addEventListener('click', () => {
if (curves.length <= 1 || selected === null) return;
let [ci, pi] = getClosestEndPoint(curves[selected][0]);
if (ci === null || pi === null) return;
// Move first point of selected curve to closest found point
curves[selected][0] = curves[ci][pi];
// Get control point of found end point
const handle1 = curves[ci][pi === 0 ? 1 : 2];
// Set control point to location mirrored over the point
curves[selected][1] = {
x: 2 * curves[selected][0].x - handle1.x,
y: 2 * curves[selected][0].y - handle1.y,
};
[ci, pi] = getClosestEndPoint(curves[selected][3]);
if (ci === null || pi === null) return;
curves[selected][3] = curves[ci][pi];
const handle2 = curves[ci][pi === 0 ? 1 : 2];
curves[selected][2] = {
x: 2 * curves[selected][3].x - handle2.x,
y: 2 * curves[selected][3].y - handle2.y,
};
draw();
});
/**
* @param {number} x
* @param {number} y
*/
function addPoint(x, y) {
const curve = curves[curves.length - 1];
curve.push({ x, y });
if (curve.length === 4) {
adding = false;
}
draw();
}
/**
* @param {number} t
* @param {number} a
* @param {number} b
* @returns {number}
*/
function lerp(t, a, b) {
return (1 - t) * a + t * b;
}
/**
* interpolates the values
* @param {number} t
* @param {number[]} v array of values
*/
function lerpArray(t, v) {
if (v.length === 1) return v[0];
return lerp(t, lerpArray(t, v.slice(0, -1)), lerpArray(t, v.slice(1)));
}
function drawCircles() {
if (selected === null) return;
for (const { x, y } of curves[selected]) {
context.beginPath();
context.ellipse(x, y, 10, 10, 0, 0, Math.PI * 2);
context.stroke();
}
}
// Failed attempt at adaptive line length
/**
* @param {number} tStart
* @param {number} tEnd
* @param {{x: number, y: number}[]} points
* @returns {{x: number, y: number}[]}
*/
function calcPoints(tStart, tEnd, points) {
const xPoints = points.map((p) => p.x);
const yPoints = points.map((p) => p.y);
const x1 = lerpArray(tStart, xPoints);
const y1 = lerpArray(tStart, yPoints);
const x2 = lerpArray((tStart + tEnd) / 2, xPoints);
const y2 = lerpArray((tStart + tEnd) / 2, yPoints);
const x3 = lerpArray(tEnd, xPoints);
const y3 = lerpArray(tEnd, yPoints);
const surface =
(1 / 2) * Math.abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));
// const d12 = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
// const d13 = Math.sqrt((x1 - x3) ** 2 + (y1 - y3) ** 2);
// const d23 = Math.sqrt((x2 - x3) ** 2 + (y2 - y3) ** 2);
//
// const angle = Math.acos((d12 ** 2 + d13 ** 2 - d23 ** 2) / (2 * d12 * d13));
if (surface < 20) {
return [{ x: x3, y: y3 }];
}
return [
...calcPoints(tStart, (tStart + tEnd) / 2, points),
...calcPoints((tStart + tEnd) / 2, tEnd, points),
];
}
function drawLines() {
for (let i = 0; i < curves.length; i++) {
const curve = curves[i];
for (let j = 0; j < curve.length - 3; j += 3) {
const points = curve.slice(j, j + 4);
context.beginPath();
if (selected === i) {
context.moveTo(points[0].x, points[0].y);
context.lineTo(points[1].x, points[1].y);
context.moveTo(points[2].x, points[2].y);
context.lineTo(points[3].x, points[3].y);
}
context.moveTo(points[0].x, points[0].y);
//
// const points = calcPoints(0, 1, curve);
//
// for (const { x, y } of points) {
// context.lineTo(x, y);
// }
for (let t = 0; t <= 100; t++) {
const tt = t / 100;
const x = lerpArray(
tt,
points.map((p) => p.x),
);
const y = lerpArray(
tt,
points.map((p) => p.y),
);
context.lineTo(x, y);
}
context.stroke();
}
}
}
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
drawCircles();
drawLines();
}
function resizeCanvas() {
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
draw();
}
/**
* Gets the index of the closest curve to the point
* @param {number} x
* @param {number} y
* @returns {number|null}
*/
function getClosestCurveIndex(x, y) {
/** @type {number|null} */
let minDist = null;
/** @type {number|null} */
let minIndex = null;
for (let i = 0; i < curves.length; i++) {
for (const point of curves[i]) {
const dist = Math.abs(
Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2),
);
if (!minDist || dist < minDist) {
minDist = dist;
minIndex = i;
}
}
}
return minIndex;
}
/**
* Gets the selected curve point under the mouse
* @param {number} x
* @param {number} y
* @returns {number|null}
*/
function getSelectedPoint(x, y) {
if (selected === null) return null;
for (let i = 0; i < curves[selected].length; i++) {
const point = curves[selected][i];
const dist = Math.abs(
Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2),
);
if (dist <= 10) {
return i;
}
}
return null;
}
/**
* Gets the closest end point
* @param {{x: number, y: number}} point
* @returns {[number|null, number|null]} curveIndex and pointIndex
*/
function getClosestEndPoint(point) {
/** @type {number|null} */
let minDist = null,
minCurveIndex = null,
minPointIndex = null;
for (let i = 0; i < curves.length; i++) {
if (i === selected) continue;
let dist = Math.abs(
Math.sqrt(
(point.x - curves[i][0].x) ** 2 +
(point.y - curves[i][0].y) ** 2,
),
);
if (minDist === null || dist < minDist) {
minDist = dist;
minCurveIndex = i;
minPointIndex = 0;
}
dist = Math.abs(
Math.sqrt(
(point.x - curves[i][3].x) ** 2 +
(point.y - curves[i][3].y) ** 2,
),
);
if (minDist === null || dist < minDist) {
minDist = dist;
minCurveIndex = i;
minPointIndex = 3;
}
}
return [minCurveIndex, minPointIndex];
}
canvas.addEventListener('mousedown', (e) => {
mouseDown = true;
if (adding) {
addPoint(e.pageX, e.pageY);
return;
}
const point = getSelectedPoint(e.pageX, e.pageY);
if (point !== null) {
movePointIndex = point;
return;
}
const i = getClosestCurveIndex(e.pageX, e.pageY);
if (i === null) return;
selected = i;
draw();
});
canvas.addEventListener('mousemove', (e) => {
if (!mouseDown) return;
if (movePointIndex === null) return;
curves[selected][movePointIndex] = { x: e.pageX, y: e.pageY };
draw();
});
canvas.addEventListener('mouseup', () => {
mouseDown = false;
movePointIndex = null;
});
window.addEventListener('resize', resizeCanvas);
resizeCanvas();