rg/naloga_2/main.js
2024-11-22 18:15:18 +01:00

283 lines
7.0 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 {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 p = getClosestEndPoint(curves[selected][0][0], curves[selected][0][1]);
if (p === null) return;
let [curveIndex, pointIndex] = p;
curves[selected][0] = curves[curveIndex][pointIndex];
p = getClosestEndPoint(curves[selected][3][0], curves[selected][3][1]);
if (p === null) return;
[curveIndex, pointIndex] = p;
curves[selected][3] = curves[curveIndex][pointIndex];
draw();
});
c1Button.addEventListener('click', () => {
if (curves.length <= 1 || selected === null) return;
let [curveIndex, pointIndex] = getClosestEndPoint(
curves[selected][0][0],
curves[selected][0][1],
);
// Move first point of selected curve to closest found point
curves[selected][0] = curves[curveIndex][pointIndex];
// Get control point of found point
const h1 = curves[curveIndex][pointIndex === 0 ? 1 : 2];
// Set control point to location mirrored over the first point
curves[selected][1] = [
2 * curves[selected][0][0] - h1[0],
2 * curves[selected][0][1] - h1[1],
];
[curveIndex, pointIndex] = getClosestEndPoint(
curves[selected][3][0],
curves[selected][3][1],
);
curves[selected][3] = curves[curveIndex][pointIndex];
const h2 = curves[curveIndex][pointIndex === 0 ? 1 : 2];
curves[selected][2] = [
2 * curves[selected][3][0] - h2[0],
2 * curves[selected][3][1] - h2[1],
];
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 points
* @param {number} t
* @param {number[]} p array of points
*/
function lerpCurve(t, p) {
if (p.length === 1) return p[0];
return lerp(t, lerpCurve(t, p.slice(0, -1)), lerpCurve(t, p.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();
}
}
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 p1 = curve[j];
const p2 = curve[j + 1];
const p3 = curve[j + 2];
const p4 = curve[j + 3];
context.beginPath();
if (selected === i) {
context.moveTo(p1[0], p1[1]);
context.lineTo(p2[0], p2[1]);
context.moveTo(p3[0], p3[1]);
context.lineTo(p4[0], p4[1]);
}
context.moveTo(p1[0], p1[1]);
for (let t = 0; t <= 100; t++) {
const tt = t / 100;
const x = lerpCurve(tt, [p1[0], p2[0], p3[0], p4[0]]);
const y = lerpCurve(tt, [p1[1], p2[1], p3[1], p4[1]]);
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[0]) ** 2 + (y - point[1]) ** 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[0]) ** 2 + (y - point[1]) ** 2),
);
if (dist <= 10) {
return i;
}
}
return null;
}
/**
* Gets the closest end point
* @param {number} x
* @param {number} y
* @returns {number[]|null}
*/
function getClosestEndPoint(x, y) {
/** @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((x - curves[i][0][0]) ** 2 + (y - curves[i][0][1]) ** 2),
);
if (minDist === null || dist < minDist) {
minDist = dist;
minCurveIndex = i;
minPointIndex = 0;
}
dist = Math.abs(
Math.sqrt((x - curves[i][3][0]) ** 2 + (y - curves[i][3][1]) ** 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] = [e.pageX, e.pageY];
draw();
});
canvas.addEventListener('mouseup', () => {
mouseDown = false;
movePointIndex = null;
});
window.addEventListener('resize', resizeCanvas);
resizeCanvas();