/* 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 ----------------------------- */ rect = { x: -300, y: -200, w: 600, h: 400 }; 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.setTransform(1, 0, 0, 1, 0, 0); // reset transform for UI ctx.fillStyle = "rgba(30,30,30,0.8)"; ctx.fillRect(10,10,150,100); ctx.fillStyle = "#FEB279"; ctx.fillRect(20,20,120,30); ctx.fillRect(20,60,120,30); ctx.fillStyle = "#000"; ctx.font = "60px arial"; ctx.fillText("Button 1", 20, 40); ctx.fillText("Button 2", 20, 80); } let drawSpace = () => { 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; ctx.strokeRect(rect.x, rect.y, rect.w, rect.h); } ctx.save() drawSpace() ctx.restore() drawMenus(ctx); } 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() } } new Canvas()