/* 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()