|
const bgCol = "#F2F0E7"; |
|
const accentCol = "#fd4578"; |
|
|
|
hljs.initHighlightingOnLoad(); |
|
|
|
const updateTargetDims = () => { |
|
|
|
|
|
return [windowWidth * (1 / 2), windowHeight]; |
|
}; |
|
|
|
const setCodeAndPlan = (code, plan) => { |
|
const codeElm = document.getElementById("code"); |
|
if (codeElm) { |
|
|
|
codeElm.innerHTML = hljs.highlight(code, { language: "python" }).value; |
|
} |
|
|
|
const planElm = document.getElementById("plan"); |
|
if (planElm) { |
|
|
|
planElm.innerHTML = hljs.highlight(plan, { language: "plaintext" }).value; |
|
} |
|
}; |
|
|
|
windowResized = () => { |
|
resizeCanvas(...updateTargetDims()); |
|
awaitingPostResizeOps = true; |
|
}; |
|
|
|
const animEase = (t) => 1 - (1 - Math.min(t, 1.0)) ** 5; |
|
|
|
|
|
|
|
const globalAnimSpeed = 1.1; |
|
const scaleFactor = 0.57; |
|
|
|
|
|
|
|
let globalTime = 0; |
|
let manualSelection = false; |
|
|
|
let currentElemInd = 0; |
|
|
|
let treeStructData = <placeholder> |
|
|
|
let lastClick = 0; |
|
let firstFrameTime = undefined; |
|
|
|
let nodes = []; |
|
let edges = []; |
|
|
|
let lastScrollPos = 0; |
|
|
|
setup = () => { |
|
canvas = createCanvas(...updateTargetDims()); |
|
}; |
|
|
|
class Node { |
|
x; |
|
y; |
|
size; |
|
xT; |
|
yT; |
|
xB; |
|
yB; |
|
treeInd; |
|
color; |
|
relSize; |
|
animationStart = Number.MAX_VALUE; |
|
animationProgress = 0; |
|
isStatic = false; |
|
hasChildren = false; |
|
isRootNode = true; |
|
isStarred = false; |
|
selected = false; |
|
renderSize = 10; |
|
edges = []; |
|
bgCol; |
|
|
|
constructor(x, y, relSize, treeInd) { |
|
const minSize = 35; |
|
const maxSize = 60; |
|
|
|
const maxColor = 10; |
|
const minColor = 125; |
|
|
|
this.relSize = relSize; |
|
this.treeInd = treeInd; |
|
this.size = minSize + (maxSize - minSize) * relSize; |
|
this.color = minColor + (maxColor - minColor) * relSize; |
|
this.bgCol = Math.round(Math.max(this.color / 2, 0)); |
|
|
|
this.x = x; |
|
this.y = y; |
|
this.xT = x; |
|
this.yT = y - this.size / 2; |
|
this.xB = x; |
|
this.yB = y + this.size / 2; |
|
|
|
nodes.push(this); |
|
} |
|
|
|
startAnimation = (offset = 0) => { |
|
if (this.animationStart == Number.MAX_VALUE) |
|
this.animationStart = globalTime + offset; |
|
}; |
|
|
|
child = (node) => { |
|
let edge = new Edge(this, node); |
|
this.edges.push(edge); |
|
edges.push(edge); |
|
this.hasChildren = true; |
|
node.isRootNode = false; |
|
return node; |
|
}; |
|
|
|
render = () => { |
|
if (globalTime - this.animationStart < 0) return; |
|
|
|
const mouseXlocalCoords = (mouseX - width / 2) / scaleFactor; |
|
const mouseYlocalCoords = (mouseY - height / 2) / scaleFactor; |
|
const isMouseOver = |
|
dist(mouseXlocalCoords, mouseYlocalCoords, this.x, this.y) < |
|
this.renderSize / 1.5; |
|
if (isMouseOver) cursor(HAND); |
|
if (isMouseOver && mouseIsPressed) { |
|
nodes.forEach((n) => (n.selected = false)); |
|
this.selected = true; |
|
setCodeAndPlan( |
|
treeStructData.code[this.treeInd], |
|
treeStructData.plan[this.treeInd], |
|
); |
|
manualSelection = true; |
|
} |
|
|
|
this.renderSize = this.size; |
|
if (!this.isStatic) { |
|
this.animationProgress = animEase( |
|
(globalTime - this.animationStart) / 1000, |
|
); |
|
if (this.animationProgress >= 1) { |
|
this.isStatic = true; |
|
} else { |
|
this.renderSize = |
|
this.size * |
|
(0.8 + |
|
0.2 * |
|
(-3.33 * this.animationProgress ** 2 + |
|
4.33 * this.animationProgress)); |
|
} |
|
} |
|
|
|
fill(this.color); |
|
if (this.selected) { |
|
fill(accentCol); |
|
} |
|
|
|
noStroke(); |
|
square( |
|
this.x - this.renderSize / 2, |
|
this.y - this.renderSize / 2, |
|
this.renderSize, |
|
10, |
|
); |
|
|
|
noStroke(); |
|
textAlign(CENTER, CENTER); |
|
textSize(this.renderSize / 2); |
|
fill(255); |
|
|
|
text("{ }", this.x, this.y - 1); |
|
|
|
|
|
|
|
|
|
const dotAnimThreshold = 0.85; |
|
if (this.isStarred && this.animationProgress >= dotAnimThreshold) { |
|
let dotAnimProgress = |
|
(this.animationProgress - dotAnimThreshold) / (1 - dotAnimThreshold); |
|
textSize( |
|
((-3.33 * dotAnimProgress ** 2 + 4.33 * dotAnimProgress) * |
|
this.renderSize) / |
|
2, |
|
); |
|
if (this.selected) { |
|
fill(0); |
|
stroke(0); |
|
} else { |
|
fill(accentCol); |
|
stroke(accentCol); |
|
} |
|
strokeWeight((-(dotAnimProgress ** 2) + dotAnimProgress) * 2); |
|
text("*", this.x + 20, this.y - 11); |
|
noStroke(); |
|
} |
|
|
|
if (!this.isStatic) { |
|
fill(bgCol); |
|
const progressAnimBaseSize = this.renderSize + 5; |
|
rect( |
|
this.x - progressAnimBaseSize / 2, |
|
this.y - |
|
progressAnimBaseSize / 2 + |
|
progressAnimBaseSize * this.animationProgress, |
|
progressAnimBaseSize, |
|
progressAnimBaseSize * (1 - this.animationProgress), |
|
); |
|
} |
|
if (this.animationProgress >= 0.9) { |
|
this.edges |
|
.sort((a, b) => a.color() - b.color()) |
|
.forEach((e, i) => { |
|
e.startAnimation((i / this.edges.length) ** 2 * 1000); |
|
}); |
|
} |
|
}; |
|
} |
|
|
|
class Edge { |
|
nodeT; |
|
nodeB; |
|
animX = 0; |
|
animY = 0; |
|
animationStart = Number.MAX_VALUE; |
|
animationProgress = 0; |
|
isStatic = false; |
|
weight = 0; |
|
|
|
constructor(nodeT, nodeB) { |
|
this.nodeT = nodeT; |
|
this.nodeB = nodeB; |
|
this.weight = 2 + nodeB.relSize * 1; |
|
} |
|
|
|
color = () => this.nodeB.color; |
|
|
|
startAnimation = (offset = 0) => { |
|
if (this.animationStart == Number.MAX_VALUE) |
|
this.animationStart = globalTime + offset; |
|
}; |
|
|
|
render = () => { |
|
if (globalTime - this.animationStart < 0) return; |
|
|
|
if (!this.isStatic) { |
|
this.animationProgress = animEase( |
|
(globalTime - this.animationStart) / 1000, |
|
); |
|
if (this.animationProgress >= 1) { |
|
this.isStatic = true; |
|
this.animX = this.nodeB.xT; |
|
this.animY = this.nodeB.yT; |
|
} else { |
|
this.animX = bezierPoint( |
|
this.nodeT.xB, |
|
this.nodeT.xB, |
|
this.nodeB.xT, |
|
this.nodeB.xT, |
|
this.animationProgress, |
|
); |
|
|
|
this.animY = bezierPoint( |
|
this.nodeT.yB, |
|
(this.nodeT.yB + this.nodeB.yT) / 2, |
|
(this.nodeT.yB + this.nodeB.yT) / 2, |
|
this.nodeB.yT, |
|
this.animationProgress, |
|
); |
|
} |
|
} |
|
if (this.animationProgress >= 0.97) { |
|
this.nodeB.startAnimation(); |
|
} |
|
|
|
strokeWeight(this.weight); |
|
noFill(); |
|
stroke( |
|
lerpColor(color(bgCol), color(accentCol), this.nodeB.relSize * 1 + 0.7), |
|
); |
|
bezier( |
|
this.nodeT.xB, |
|
this.nodeT.yB, |
|
this.nodeT.xB, |
|
(this.nodeT.yB + this.nodeB.yT) / 2, |
|
this.animX, |
|
(this.nodeT.yB + this.nodeB.yT) / 2, |
|
this.animX, |
|
this.animY, |
|
); |
|
}; |
|
} |
|
|
|
draw = () => { |
|
cursor(ARROW); |
|
frameRate(120); |
|
if (!firstFrameTime && frameCount <= 1) { |
|
firstFrameTime = millis(); |
|
} |
|
|
|
const initialSpeedScalingEaseIO = |
|
(cos(min((millis() - firstFrameTime) / 8000, 1.0) * PI) + 1) / 2; |
|
const initialSpeedScalingEase = |
|
(cos(min((millis() - firstFrameTime) / 8000, 1.0) ** (1 / 2) * PI) + 1) / 2; |
|
const initAnimationSpeedFactor = 1.0 - 0.4 * initialSpeedScalingEaseIO; |
|
|
|
globalTime += globalAnimSpeed * initAnimationSpeedFactor * deltaTime; |
|
|
|
if (nodes.length == 0) { |
|
const spacingHeight = height * 1.3; |
|
const spacingWidth = width * 1.3; |
|
treeStructData.layout.forEach((lay, index) => { |
|
new Node( |
|
spacingWidth * lay[0] - spacingWidth / 2, |
|
20 + spacingHeight * lay[1] - spacingHeight / 2, |
|
1 - treeStructData.metrics[index], |
|
index, |
|
); |
|
}); |
|
treeStructData.edges.forEach((ind) => { |
|
nodes[ind[0]].child(nodes[ind[1]]); |
|
}); |
|
nodes.forEach((n) => { |
|
if (n.isRootNode) n.startAnimation(); |
|
}); |
|
nodes[0].selected = true; |
|
setCodeAndPlan( |
|
treeStructData.code[0], |
|
treeStructData.plan[0], |
|
) |
|
} |
|
|
|
const staticNodes = nodes.filter( |
|
(n) => n.isStatic || n.animationProgress >= 0.7, |
|
); |
|
if (staticNodes.length > 0) { |
|
const largestNode = staticNodes.reduce((prev, current) => |
|
prev.relSize > current.relSize ? prev : current, |
|
); |
|
if (!manualSelection) { |
|
if (!largestNode.selected) { |
|
setCodeAndPlan( |
|
treeStructData.code[largestNode.treeInd], |
|
treeStructData.plan[largestNode.treeInd], |
|
); |
|
} |
|
staticNodes.forEach((node) => { |
|
node.selected = node === largestNode; |
|
}); |
|
} |
|
} |
|
background(bgCol); |
|
|
|
translate(width / 2, height / 2); |
|
scale(scaleFactor); |
|
|
|
|
|
|
|
edges.forEach((e) => e.render()); |
|
nodes.forEach((n) => n.render()); |
|
|
|
}; |
|
|