initial commit of site code
This commit is contained in:
parent
3c9d614db2
commit
de46a13dfb
79
index.html
Normal file
79
index.html
Normal file
@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Mydeate</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mydeate-main" class="main-div"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<script src="./js/util.js"></script>
|
||||
<script src="./js/element.js"></script>
|
||||
<script src="./js/elements.js"></script>
|
||||
<script src="./js/pointer.js"></script>
|
||||
<script src="./js/canvas.js"></script>
|
||||
<script src="./js/main.js"></script>
|
||||
<script defer>
|
||||
|
||||
// Time-stepped to support animations;
|
||||
// Can also run tasks queued by the UI, if we want;
|
||||
// Could also support arbitrary scheduled operations.
|
||||
|
||||
// Let's create an ontology for the UI, which the render loop can reference
|
||||
// What kinds of things we have?
|
||||
// Text : Consists of some text content; could have formatting; could have title;
|
||||
// May want to specialize it to support data or whatever...
|
||||
// Connection : Like a line / arrow ; May have different kinds of connections such as
|
||||
// Precedence / hierarchy tree; depencency graph;
|
||||
// May have multiple start / end points (a.k.a. may be a hyperedge)
|
||||
// Also worth thinking about matrices - outer products
|
||||
// Might as well further consider tensor products
|
||||
|
||||
// So then maybe we have an output / display node; this could be displaying things such as
|
||||
// Values of some parameters
|
||||
// Graph of (historical trends of) parameters
|
||||
// Results of logic computations / expression evaluation
|
||||
|
||||
// Utility elements to start, for displaying document time and pointer location
|
||||
|
||||
|
||||
// Add utility elements
|
||||
addElement({
|
||||
id: "time",
|
||||
classes: ["monospace"],
|
||||
detail: () => {
|
||||
const t = currentTime;
|
||||
const s = t / 1000;
|
||||
const m = s / 60;
|
||||
const h = m / 60;
|
||||
const d = h / 24;
|
||||
const timeStr = [d % 60, h % 24, m % 60, s % 60].map(x => x.toFixed(0).padStart(2, "0")).join(":");
|
||||
return `runtime: ${timeStr}`;
|
||||
},
|
||||
});
|
||||
addElement({
|
||||
id: "pointer-info",
|
||||
classes: ["monospace"],
|
||||
detail: () => (["x", "y"]
|
||||
.map(q => `${q}:${(pointer[q] ?? 0).toFixed(2).padStart(7, " ")}`)
|
||||
.join(", ")
|
||||
.replace(/ /g, " ")),
|
||||
});
|
||||
addElement({
|
||||
classes: ["monospace"],
|
||||
id: 'pointer-history-length',
|
||||
detail: () => `ptr hist len: ${pointerHistory.length}`,
|
||||
});
|
||||
addElement({
|
||||
summary: () => "Element Summary",
|
||||
detail: () => "Element Detail<br> With multile lines<br> How about that?",
|
||||
});
|
||||
|
||||
// Run main loop
|
||||
main();
|
||||
</script>
|
||||
53
js/canvas.js
Normal file
53
js/canvas.js
Normal file
@ -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);
|
||||
}
|
||||
68
js/element.js
Normal file
68
js/element.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
js/elements.js
Normal file
57
js/elements.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
js/main.js
Normal file
26
js/main.js
Normal file
@ -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);
|
||||
}
|
||||
104
js/pointer.js
Normal file
104
js/pointer.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
14
js/util.js
Normal file
14
js/util.js
Normal file
@ -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),
|
||||
);
|
||||
}
|
||||
52
style.css
Normal file
52
style.css
Normal file
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user