From de46a13dfb70df61804a05f4b52363366d6897df Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Wed, 1 Jul 2026 02:42:23 -0500 Subject: [PATCH] initial commit of site code --- index.html | 79 +++++++++++++++++++++++++++++++++++++ js/canvas.js | 53 +++++++++++++++++++++++++ js/element.js | 68 ++++++++++++++++++++++++++++++++ js/elements.js | 57 +++++++++++++++++++++++++++ js/main.js | 26 +++++++++++++ js/pointer.js | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ js/util.js | 14 +++++++ style.css | 52 +++++++++++++++++++++++++ 8 files changed, 453 insertions(+) create mode 100644 index.html create mode 100644 js/canvas.js create mode 100644 js/element.js create mode 100644 js/elements.js create mode 100644 js/main.js create mode 100644 js/pointer.js create mode 100644 js/util.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..e1ebb70 --- /dev/null +++ b/index.html @@ -0,0 +1,79 @@ + + + + + Mydeate + + + + + +
+ + + + + + + + + + \ No newline at end of file diff --git a/js/canvas.js b/js/canvas.js new file mode 100644 index 0000000..f364f37 --- /dev/null +++ b/js/canvas.js @@ -0,0 +1,53 @@ +// Background canvas + +let bgCanvas; +let fgCanvas; +let bgCtx; +let fgCtx; + +function initializeCanvas() { + initializeBackground(); + initializeForeground(); + fullscreen(); + window.addEventListener('resize', fullscreen); +} + +function initializeBackground() { + bgCanvas = document.createElement("canvas"); + mainDiv.appendChild(bgCanvas); + bgCanvas.classList.add("fullscreen"); + bgCanvas.classList.add("background"); + bgCtx = bgCanvas.getContext("2d"); + clearCanvas(bgCanvas, bgCtx); +} + +function initializeForeground() { + fgCanvas = document.createElement("canvas"); + mainDiv.appendChild(fgCanvas); + fgCanvas.classList.add("fullscreen"); + fgCanvas.classList.add("foreground"); + fgCtx = fgCanvas.getContext("2d"); + clearCanvas(fgCanvas, fgCtx); +} + +function clearCanvas(canvas, ctx) { + if (!canvas && !ctx) { + clearCanvas(bgCanvas, bgCtx); + clearCanvas(fgCanvas, fgCtx); + return; + } + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (ctx === bgCtx) { + bgCtx.fillStyle = '#ccc'; + bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height); + } +} + +function fullscreen() { + const dim = { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + }; + Object.assign(bgCanvas, dim); + Object.assign(fgCanvas, dim); +} diff --git a/js/element.js b/js/element.js new file mode 100644 index 0000000..4c4a26e --- /dev/null +++ b/js/element.js @@ -0,0 +1,68 @@ +class Element { + elements = undefined; + id = undefined; + summary = () => ""; + detail = () => ""; + el = undefined; + classes = []; + + constructor(elements, {id, summary, detail, classes, width, height, x, y}) { + this.elements = elements; + this.id = id; + this.summary = summary; + this.detail = detail; + + // Create DOM element(s) + this.el = document.createElement("div"); + this.el.id = id; + this.el.classList.add('element'); + for (const className of (classes ?? [])) { + this.el.classList.add(className); + } + if (width !== undefined && height !== undefined) { + this.setSize(width, height); + } + if (x !== undefined && y !== undefined) { + this.setPosition({x, y}); + } + if (summary) { + this.summaryEl = document.createElement("div"); + this.summaryEl.classList.add("summary"); + this.el.appendChild(this.summaryEl) + } + if (detail) { + this.detailEl = document.createElement("div"); + this.detailEl.classList.add("detail"); + this.el.appendChild(this.detailEl) + } + + // Handle pointer down to initiate drag + this.el.addEventListener("pointerdown", (e) => { + e.preventDefault(); + const {clientX: x, clientY: y} = e; + startDrag({element: this, x, y}); + }); + } + + setPosition({x, y}) { + this.el.style.position = "absolute"; + this.el.style.top = `${y}px`; + this.el.style.left = `${x}px`; + return this; + } + + setSize(width, height) { + this.el.style.width = `${width}px`; + this.el.style.height = `${height}px`; + return this; + } + + render() { + if (this.summary) { + this.summaryEl.innerHTML = this.summary(); + } + if (this.detail) { + this.detailEl.innerHTML = this.detail(); + } + } +} \ No newline at end of file diff --git a/js/elements.js b/js/elements.js new file mode 100644 index 0000000..e0873f4 --- /dev/null +++ b/js/elements.js @@ -0,0 +1,57 @@ +// globals: mainDiv, crypto +class Elements { + elements = new Map(); // id -> Element + classes = []; + div = undefined; + + constructor({classes} = {}) { + this.classes = classes; + this.div = document.createElement("div"); + this.div.classList.add("elements"); + this.div.classList.add("droptarget"); + for (const className of (classes ?? [])) { + this.div.classList.add(className); + } + mainDiv.appendChild(this.div); + } + + add(props) { + // Make sure we have a unique id + let id = props.id; + if (!id) { + if (crypto?.randomUUID) { + id = crypto.randomUUID(); + } else { + id = 1; + while (this.elements.has(id)) { + id++; + } + } + } + // Create element + const element = new Element(this, {...props, id}); + // Append to div + this.div.appendChild(element.el); + // Add to collection + this.elements.set(id, element); + return element; + } + + remove(id) { + const element = this.elements.get(id); + if (element) { + this.div.removeChild(element.el); + this.elements.delete(id); + } + } + + getElementFromDOM(target) { + return Array.from(this.elements.values()).find(({el}) => el === target); + } + + renderAll() { + for (const element of this.elements.values()) { + element.render(); + } + } +} \ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..6ce3c82 --- /dev/null +++ b/js/main.js @@ -0,0 +1,26 @@ +const mainDiv = document.getElementById("mydeate-main"); +const elements = new Elements(); +// Initialize variables for main loop +let run = true; +let currentTime; + +function main() { + initializeCanvas(); + initializePointer(); + requestAnimationFrame(loop); +} + +function loop(elapsedTime) { + if (run) { + currentTime = document.timeline.currentTime; + clearCanvas(); + updatePointerHistory({decay: true}); + drawPointerHistory(); + elements.renderAll(); + } + requestAnimationFrame(loop); +} + +function addElement(params) { + return elements.add(params); +} \ No newline at end of file diff --git a/js/pointer.js b/js/pointer.js new file mode 100644 index 0000000..c873eed --- /dev/null +++ b/js/pointer.js @@ -0,0 +1,104 @@ +// Let's set up pointer tracking. We can use the main div as the pointer target. +// Note that we may later add pointer handlers on layered elements +const pointer = {x: undefined, y: undefined}; +// Trace the path of the cursor +const pointerHistory = []; +const maxHistory = 50; +const dropTargetMaxRange = 20; +const drag = { + start: {x: undefined, y: undefined, t: undefined}, + element: undefined, + placeholder: undefined, +} + +function startDrag({element, x, y}) { + drag.element = element; + drag.start = {x, y, t: currentTime}; + // We use a placeholder to represent the new position + const {x: ox, y: oy, width, height} = element.el.getBoundingClientRect(); + drag.placeholder = elements.add({ + ...element, + id: `${this.id}-placeholder`, + classes: [...Array.from(element.el.classList.values()), "moving", "placeholder"], + width, height, x, y, + }); + drag.element.el.classList.add("moving"); + mainDiv.classList.add("dragging"); +} + +function initializePointer() { + mainDiv.addEventListener("pointermove", (e) => { + const {clientX: x, clientY: y} = e; + Object.assign(pointer, {x, y}); + + if (drag.start.t) { + const displacement = { + x: x - drag.start.x, + y: y - drag.start.y, + }; + const {x: ox, y: oy} = drag.element.el.getBoundingClientRect(); + drag.placeholder.setPosition({ + x: ox + displacement.x, + y: oy + displacement.y, + }); + + // We can check here if we're near one or more drop targets to offer. + // We should indicate which available drop target is currently active. + // If there are multiple options, we can show them. + // const placeholder; + // const nearbyTargets = Array.from(elements.values()).filter(({el}) => { + // const {x, y, width, height} = el.getBoundingClientRect(); + // const linearDist = minLinearDist(el.getBoundingClientRect(), ); + // if (linearDist <= dropTargetMaxRange) { + // Visually activate the drop target + // } + //}) + } + }); + + mainDiv.addEventListener("pointerup", (e) => { + if (!drag.start.t) return; + const {clientX: x, clientY: y} = e; + // Displacement + const d = { + x: x - drag.start.x, + y: y - drag.start.y, + }; + const {x: ox, y: oy, width, height} = drag.element.el.getBoundingClientRect(); + drag.element.setPosition({ x: ox + d.x, y: oy + d.y}).setSize(width, height); + drag.element.el.classList.remove("moving"); + elements.remove(drag.placeholder.id); + drag.start = {}; + drag.placeholder = undefined; + drag.element = undefined; + mainDiv.classList.remove("dragging"); + }); +} + +function updatePointerHistory({decay} = {decay: true}) { + if (pointer.x === undefined || pointer.y === undefined) return; + if (!pointerHistory.length) { + pointerHistory.push({...pointer, t: currentTime}); + return; + } + const lastPointer = pointerHistory[pointerHistory.length - 1]; + if (decay || pointer.x !== lastPointer.x || pointer.y !== lastPointer.y) { + pointerHistory.push({...pointer, t: currentTime}); + } + while (pointerHistory.length > maxHistory) { + pointerHistory.shift(); + } +} + +function drawPointerHistory() { + if (pointerHistory.length < 2) return; + for (let i = 1; i < pointerHistory.length; i++) { + fgCtx.beginPath(); + const opacity = i / pointerHistory.length; + fgCtx.strokeStyle = `rgba(128, 0, 0, ${opacity})`; + fgCtx.moveTo(pointerHistory[i - 1].x, pointerHistory[i - 1].y); + fgCtx.lineTo(pointerHistory[i].x, pointerHistory[i].y); + fgCtx.stroke(); + } +} + diff --git a/js/util.js b/js/util.js new file mode 100644 index 0000000..720908a --- /dev/null +++ b/js/util.js @@ -0,0 +1,14 @@ +function minLinearDist(A, B) { + return Math.min( + // vertical distances (4) between all edges + (A.y) - (B.y), + (A.y + A.height) - (B.y + B.height), + (A.y) - (B.y + B.height), + (A.y + A.height) - (B.y), + // horizontal distances (4) between all edges + (A.x) - (B.x), + (A.x + A.width) - (B.x + B.width), + (A.x) - (B.x + B.width), + (A.x + A.width) - (B.x), + ); +} \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..f986c1f --- /dev/null +++ b/style.css @@ -0,0 +1,52 @@ +.fullscreen { + position: absolute; + top: 0; + left: 0; +} +.background { + z-index: -1; +} +.foreground { + z-index: 1; + pointer-events: none; +} +.main-div { + position: absolute; + top: 0; + left: 0; + z-index: 0; +} +.main-div * { + font-family: sans-serif; + font-size: 11pt; + line-height: 13pt; +} +.main-div.dragging .droptarget { + background-color: rgba(0, 0, 200, 0.2); +} +.elements { + flex-direction: column; +} +.element { + background-color: #eee; + display: flex; + flex-direction: column; +} +.element .summary, .element .detail { + display: flex; +} +.element .summary { + font-weight: bold; +} +.main-div .monospace .detail { + font-family: monospace; +} +.element.moving { + /* border: 1px rgba(0, 128, 0, 0.7) dashed; */ + background-color: rgb(150, 100, 150); + opacity: 0.3; +} +.element.moving.placeholder { + background-color: rgb(100, 200, 100); + opacity: 0.7; +} \ No newline at end of file