184 lines
5.3 KiB
JavaScript
184 lines
5.3 KiB
JavaScript
/*
|
||
Samuel Russell
|
||
Captured Sun
|
||
12.29.2025
|
||
*/
|
||
|
||
class Canvas {
|
||
STROKE_COLOR = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim();
|
||
c = document.createElement("canvas")
|
||
ctx;
|
||
|
||
/* -----------------------------
|
||
Camera
|
||
----------------------------- */
|
||
camera = {
|
||
x: 0,
|
||
y: 0,
|
||
scale: 1,
|
||
ZOOM_SPEED: 0.016,
|
||
PAN_SPEED: 1.5,
|
||
FOCUS_THRESHOLD: 4.0
|
||
}
|
||
|
||
/* -----------------------------
|
||
Rectangle
|
||
----------------------------- */
|
||
rects = [];
|
||
|
||
resize = () => {
|
||
// Make Canvas Fill Screen
|
||
this.c.style.width = window.innerWidth + "px";
|
||
this.c.style.height = window.innerHeight + "px";
|
||
|
||
// Set Internal Render Size by DPR
|
||
const dpr = window.devicePixelRatio || 1;
|
||
this.c.width = window.innerWidth * dpr;
|
||
this.c.height = window.innerHeight * dpr;
|
||
this.ctx.scale(dpr, dpr);
|
||
}
|
||
|
||
onWheel = (e) => {
|
||
e.preventDefault();
|
||
let camera = this.camera
|
||
|
||
const rectCanvas = this.c.getBoundingClientRect();
|
||
const mouseX = e.clientX - rectCanvas.left;
|
||
const mouseY = e.clientY - rectCanvas.top;
|
||
|
||
if (!e.ctrlKey) {
|
||
const dpr = window.devicePixelRatio || 1;
|
||
|
||
// Two-finger pan in world coordinates
|
||
camera.x += (e.deltaX * dpr) * camera.PAN_SPEED / camera.scale;
|
||
camera.y += (e.deltaY * dpr) * camera.PAN_SPEED / camera.scale;
|
||
return;
|
||
}
|
||
|
||
const dpr = window.devicePixelRatio || 1;
|
||
|
||
const worldX = (
|
||
mouseX -
|
||
(this.c.width / dpr) / 2 // X coordinate of canvas center in CSS pixels
|
||
) // Mouse offset from canvas center in CSS pixels
|
||
/ camera.scale // Account for Zoom: shift by camera's zoom position
|
||
+ camera.x; // Account for Pan: shift by camera’s world X position
|
||
|
||
const worldY = (
|
||
mouseY -
|
||
(this.c.height / dpr) / 2
|
||
)
|
||
/ camera.scale
|
||
+ camera.y;
|
||
|
||
// Apply zoom
|
||
const zoomFactor = Math.exp(-e.deltaY * camera.ZOOM_SPEED);
|
||
camera.scale *= zoomFactor;
|
||
camera.scale = Math.max(0.04, Math.min(10, camera.scale));
|
||
|
||
// Adjust camera to keep cursor fixed
|
||
camera.x = worldX - (mouseX - (this.c.width / dpr) / 2) / camera.scale;
|
||
camera.y = worldY - (mouseY - (this.c.height / dpr) / 2) / camera.scale;
|
||
}
|
||
|
||
draw = () => {
|
||
let ctx = this.ctx
|
||
let rect = this.rect
|
||
let camera = this.camera
|
||
let scale = camera.scale
|
||
requestAnimationFrame(this.draw);
|
||
|
||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||
ctx.clearRect(0, 0, this.c.width, this.c.height);
|
||
|
||
let drawMenus = () => {
|
||
ctx.fillStyle = "#FEB279";
|
||
ctx.strokeRect(20,20,220,100);
|
||
|
||
ctx.fillStyle = "#000";
|
||
ctx.font = "60px arial";
|
||
// ctx.fillText("Button 1", 20, 100);
|
||
}
|
||
|
||
let drawSpaces = () => {
|
||
ctx.translate(this.c.width / 2, this.c.height / 2);
|
||
ctx.scale(scale, scale);
|
||
ctx.translate(-camera.x, -camera.y);
|
||
|
||
ctx.strokeStyle = this.STROKE_COLOR;
|
||
ctx.lineWidth = 0.5 / scale;
|
||
|
||
const baseRadius = 40; // distance of first ring
|
||
const ringSpacing = 70; // distance between rings
|
||
const dotRadius = 18; // circle size
|
||
|
||
let index = 0;
|
||
let ring = 0;
|
||
|
||
while (index < this.rects.length) {
|
||
// how many items fit on this ring
|
||
const circumference = 2 * Math.PI * (baseRadius + ring * ringSpacing);
|
||
const itemsInRing = Math.max(6, Math.floor(circumference / (dotRadius * 2.5)));
|
||
|
||
for (let i = 0; i < itemsInRing && index < this.rects.length; i++) {
|
||
const angle = (i / itemsInRing) * Math.PI * 2;
|
||
|
||
const r = baseRadius + ring * ringSpacing;
|
||
const x = Math.cos(angle) * r;
|
||
const y = Math.sin(angle) * r;
|
||
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, dotRadius, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
|
||
const cssDiameter = dotRadius * 2 * scale;
|
||
|
||
if (cssDiameter >= 30) {
|
||
ctx.fillStyle = "#000";
|
||
ctx.font = `${3}px Arial`;
|
||
ctx.textAlign = "center";
|
||
ctx.textBaseline = "middle";
|
||
ctx.fillText(this.rects[index].name, x, y);
|
||
}
|
||
|
||
|
||
index++;
|
||
}
|
||
|
||
ring++;
|
||
}
|
||
};
|
||
|
||
|
||
ctx.save()
|
||
|
||
drawSpaces()
|
||
|
||
ctx.restore()
|
||
|
||
drawMenus(ctx);
|
||
}
|
||
|
||
async fetchDownloads() {
|
||
let res = await fetch("/downloads", {method: "GET"})
|
||
console.log(res)
|
||
let json = await res.json()
|
||
console.log(json)
|
||
this.rects = json
|
||
}
|
||
|
||
constructor() {
|
||
document.body.appendChild(this.c)
|
||
this.ctx = this.c.getContext("2d");
|
||
|
||
window.addEventListener("resize", this.resize);
|
||
this.resize();
|
||
|
||
this.c.addEventListener("wheel", this.onWheel, { passive: false });
|
||
this.draw()
|
||
|
||
this.fetchDownloads()
|
||
}
|
||
}
|
||
|
||
new Canvas() |