xxxxxxxxxx
362
let tree;
let template = {
label: 'Root',
diameter: 0.07,
x: 0,
y: 0,
children: [
{
label: 'Role A',
diameter: 0.1,
x: 0.2,
y: 0.2,
hue: 120,
children: [
{
label: 'Role E',
diameter: 0.1,
x: 0.4,
y: 0.4,
hue: 150,
},
{
label: 'Role F',
diameter: 0.1,
x: 0,
y: 0.4,
hue: 210,
},
{
label: 'Role G',
diameter: 0.1,
x: 0.4,
y: 0,
hue: 270,
},
],
},
{
label: 'Role B',
diameter: 0.1,
x: -0.2,
y: 0.2,
hue: 180,
},
{
label: 'Role C',
diameter: 0.1,
x: -0.2,
y: -0.2,
hue: 240,
children: [
{
label: 'Role H',
diameter: 0.1,
x: -0.4,
y: -0.4,
hue: 150,
},
{
label: 'Role I',
diameter: 0.1,
x: 0,
y: -0.4,
hue: 210,
},
{
label: 'Role J',
diameter: 0.1,
x: -0.4,
y: 0,
hue: 270,
},
],
},
{
label: 'Role D',
diameter: 0.1,
x: 0.2,
y: -0.2,
hue: 320,
},
]
};
function setup() {
createCanvas(windowWidth, windowHeight);
// const baseScale = min(width, height);
tree = Node.loadTemplate(template);
tree.adaptCanvas(width, height);
}
function draw() {
background(0);
tree.display();
}
function mouseMoved(event) {
tree.moveMouse(event);
}
function mousePressed(event) {
tree.pressMouse(event);
}
function mouseReleased(event) {
tree.releaseMouse(event);
}
function mouseDragged(event) {
tree.dragMouse(event);
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
tree.adaptCanvas(width, height);
}
class Node {
constructor(x, y, d, options = {}) {
this.normalizedX = x;
this.normalizedY = y;
this.normalizedD = d;
this.normalizedLinkThickness = options.linkThickness?? 0.01;
this.baseScale = options.baseScale?? 500;
this.offsetX = options.offsetX?? 0;
this.offsetY = options.offsetY?? 0;
this.label = options.label;
this.hue = options.hue?? 0;
this.parent = options.parent;
this.children = [];
this.value = 1;
this.isPressed = false;
}
static loadTemplate(template) {
const x = template.x ?? 0;
const y = template.y ?? 0;
const d = template.diameter ?? 0.1;
const options = {};
options.label = template.label;
options.hue = template.hue;
const node = new Node(x, y, d, options);
if (template.children) {
for (let childTemplate of template.children) {
node.addChild(Node.loadTemplate(childTemplate));
}
}
return node;
}
// create and add child based on parameters
makeChild(x, y, d) {
const child = new Node(x, y, d, {baseScale: this.baseScale});
child.parent = this;
child.updateValue();
this.children.push(child);
return child;
}
// add node as child
addChild(node) {
node.parent = this;
node.updateValue();
node.updateBaseScale(this.baseScale);
this.children.push(node);
return node;
}
// drawing functions
display() {
// invert display order so it matches interaction order
// smaller i is drawn on top of bigger i
// smaller i has priority for interaction
for (let i = this.children.length - 1; i >= 0; i--) {
const child = this.children[i];
this.drawLink(child);
child.display();
}
this.drawNode();
this.displayText();
}
displayText() {
push();
translate(this.offsetX, this.offsetY);
textAlign(CENTER, CENTER);
textSize(this.d/4);
let textToDisplay = '';
if (this.label !== undefined) textToDisplay += `${this.label}`;
if (this.value !== undefined) textToDisplay += `\n${this.value}`;
text(textToDisplay, this.x, this.y);
pop();
}
drawNode() {
push();
noStroke();
colorMode(HSL);
fill(this.hue, this.saturation, this.lightness);
translate(this.offsetX, this.offsetY);
circle(this.x, this.y, this.d);
pop();
}
drawLink(other) {
push();
translate(this.offsetX, this.offsetY);
// stroke(255);
// strokeWeight(this.linkThickness);
// line(this.x, this.y, other.x, other.y);
colorMode(HSL);
noStroke();
const length = dist(this.x, this.y, other.x, other.y);
const diameter = this.linkThickness;
const step = diameter*1.5;
const thisColor = color(this.hue, 100, this.lightness);
const otherColor = color(other.hue, other.saturation, other.lightness);
for (let i = 0; i < length; i += step) {
const x = map(i,0,length,this.x,other.x);
const y = map(i,0,length,this.y,other.y);
fill(lerpColor(thisColor, otherColor, i/length));
circle(x, y, this.linkThickness);
}
pop();
}
// handle mouse events
moveMouse(event) {
this.checkHover(event);
}
pressMouse(event) {
if (dist(this.x+this.offsetX, this.y+this.offsetY, event.x, event.y) <= this.d/2) {
this.isPressed = true;
return true;
} else {
for (let child of this.children) {
if (child.pressMouse(event)) return true;
}
}
return false;
}
releaseMouse(event) {
this.isPressed = false;
for (let child of this.children) {
child.releaseMouse(event);
}
this.checkHover(event);
}
dragMouse(event) {
if (this.isPressed) {
this.moveBy(event.movementX, event.movementY);
return true;
} else {
for (let child of this.children) {
if (child.dragMouse(event)) return true;
}
}
return false;
}
moveBy(dx, dy) {
this.x += dx;
this.y += dy;
this.updateValue();
}
checkHover(event) {
this.unhover();
if (dist(this.x+this.offsetX, this.y+this.offsetY, event.x, event.y) <= this.d/2) {
this.isHovered = true;
return true;
} else {
for (let child of this.children) {
if (child.checkHover(event)) return true;
}
}
return false;
}
unhover() {
this.isHovered = false;
for (let child of this.children) {
child.unhover();
}
}
// helper functions
adaptCanvas(w, h) {
this.updateOffset(w/2,h/2);
this.updateBaseScale(min(w,h));
}
updateOffset(offsetX, offsetY) {
this.offsetX = offsetX;
this.offsetY = offsetY;
for (let child of this.children) {
child.updateOffset(offsetX, offsetY);
}
}
updateValue() {
if (this.parent) {
const distFromParent = dist(this.x, this.y, this.parent.x, this.parent.y);
const rawValue = this.roundDecimal(distFromParent/this.baseScale, 2);
this.value = constrain(rawValue, 0, 1);
}
for (let child of this.children) {
child.updateValue();
}
}
updateBaseScale(baseScale) {
this.baseScale = baseScale;
for (let child of this.children) {
child.updateBaseScale(baseScale);
}
}
roundDecimal(value, digitsNum) {
const helperNum = 10**digitsNum;
return floor(value*helperNum)/helperNum;
}
set x(x) {
this.normalizedX = x/this.baseScale;
}
set y(y) {
this.normalizedY = y/this.baseScale;
}
get x() {
return this.normalizedX*this.baseScale;
}
get y() {
return this.normalizedY*this.baseScale;
}
get d() {
return this.normalizedD*this.baseScale;
}
get linkThickness() {
return this.normalizedLinkThickness*this.baseScale;
}
get saturation() {
return 100*this.value;
}
get lightness() {
return this.isHovered ? 50 : 70;
}
}