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