Skip to content

Commit

Permalink
try to untangle graph layout from pan zoom aspects of note canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
lmorchard committed May 16, 2024
1 parent fdbe24a commit 49154dc
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 223 deletions.
4 changes: 2 additions & 2 deletions public/canvas.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
<script type="module" src="./canvas.js"></script>
</head>
<body>
<sticky-notes-canvas id="notes-canvas" zoom="1.0" originX="100" originY="100">
<sticky-notes-canvas id="notes-canvas" zoom="1.0" originX="0" originY="0">
<sticky-note id="example-1" x="-200" y="-200" width="100" height="100" color="#a8a">
Note 1
</sticky-note>
<sticky-note id="example-2" x="-200" y="200" width="100" height="100" color="#aa8">
<sticky-note id="example-2" x="-200" y="0" width="0" height="100" color="#aa8">
Note 2
</sticky-note>
<sticky-note id="example-3" x="200" y="200" width="100" height="100" color="#8aa">
Expand Down
10 changes: 8 additions & 2 deletions public/index.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
body {
background: #333;
}

sticky-notes-canvas {
background: #fff;
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
width: 100vw;
height: 100vh;
width: 90vw;
height: 90vh;
border: 4px solid black;
margin: 1em;
}
1 change: 1 addition & 0 deletions public/lib/components/StickyNote.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class StickyNote extends StickyNotesCanvasChildDraggableMixin() {
align-items: center;
border: 1px solid black;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
z-index: 10;
}
:host .container {
}
Expand Down
261 changes: 43 additions & 218 deletions public/lib/components/StickyNotesCanvas.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { html, BaseElement } from "../dom.js";
import { DraggableMixin } from "../mixins/DraggableMixin.js";
import Springy from "../springy.js";
import { StickyNotesClusterTopic, StickyNotesClusterLink } from "./StickyNotesClusterTopic.js";
import { StickyNote } from "./StickyNote.js";
import { html, BaseElement, $ } from "../dom.js";
import { PanZoomableMixin } from "../mixins/PanZoomableMixin.js";
import { GraphLayoutMixin } from "../mixins/GraphLayoutMixin.js";

export class StickyNotesCanvas extends DraggableMixin(BaseElement) {
export class StickyNotesCanvas extends GraphLayoutMixin(
PanZoomableMixin(BaseElement)
) {
static observedAttributes = [
"originx",
"originy",
"zoom",
"minzoom",
"maxzoom",
"wheelfactor",
...PanZoomableMixin.observedAttributes,
...GraphLayoutMixin.observedAttributes,
];

static template = html`
Expand All @@ -20,227 +16,56 @@ export class StickyNotesCanvas extends DraggableMixin(BaseElement) {
display: block;
overflow: hidden;
}
.container {
position: relative;
.viewport {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.canvas {
position: absolute;
top: 0;
left: 0;
width: 1000px;
height: 1000px;
transform: translate(50%, 50%);
/*
--dot-bg: rgba(250, 250, 250, 1.0);
--dot-color: #000;
--dot-size: 1px;
--dot-space: 22px;
background: linear-gradient(
90deg,
var(--dot-bg) calc(var(--dot-space) - var(--dot-size)),
transparent 1%
)
center / var(--dot-space) var(--dot-space),
linear-gradient(
var(--dot-bg) calc(var(--dot-space) - var(--dot-size)),
transparent 1%
)
center / var(--dot-space) var(--dot-space),
var(--dot-color);
*/
}
</style>
<div class="container">
<div class="inner-canvas">
<div class="viewport">
<div class="canvas">
<slot></slot>
</div>
</div>
`;

constructor() {
super();

this.minZoom = 0.1;
this.maxZoom = 5;
this.wheelFactor = 0.001;
this.zoomOrigin = { x: 0, y: 0 };

this.graphLayoutScale = 50;
this._rendering = false;

this.mutationObserver = new MutationObserver(this.handleMutations.bind(this));
}

connectedCallback() {
super.connectedCallback();

this.addEventListener("wheel", this.onWheel);

this.graph = new Springy.Graph();

this.layout = new Springy.Layout.ForceDirected(
this.graph,
300.0, // Spring stiffness
200.0, // Node repulsion
0.5, // Damping
0.5 // minEnergyThreshold
);

// HACK: wedge in a handler to fire just before layout tick
const layoutTick = this.layout.tick.bind(this.layout);
this.layout.tick = (timestep) => {
this.rendererBeforeTick(timestep);
layoutTick(timestep);
};

this.renderer = new Springy.Renderer(
this.layout,
() => { }, // clear
this.rendererDrawEdge.bind(this),
this.rendererDrawNode.bind(this),
() => { this.rendering = false },
() => { this.rendering = true },
);

this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true });

for (const noteEl of this.querySelectorAll("sticky-note")) {
this.upsertGraphNode(noteEl);
}
for (const topicEl of this.querySelectorAll("sticky-notes-cluster-topic")) {
this.addTopic(topicEl);
}
}

rendererBeforeTick() {
this.layout.eachNode((node, point) => {
const el = this.ownerDocument.getElementById(node.id);
if (!el) return;
point.p.x = parseFloat(el.attributes.x.value) / this.graphLayoutScale;
point.p.y = parseFloat(el.attributes.y.value) / this.graphLayoutScale;
});
}

rendererDrawEdge(edge, fromPointP, toPointP) { }

rendererDrawNode(node, pointP) {
const el = this.ownerDocument.getElementById(node.id);
if (!el || el.dragging) return;
el.attributes.x.value = pointP.x * this.graphLayoutScale;
el.attributes.y.value = pointP.y * this.graphLayoutScale;
}

disconnectedCallback() {
super.disconnectedCallback();
this.mutationObserver.disconnect();
}

get zoom() {
return parseFloat(this.attributes.zoom.value);
}

set zoom(value) {
this.attributes.zoom.value = value;
this.update();
}

onWheel(ev) {
ev.preventDefault();
this.zoomOrigin = { x: ev.clientX, y: ev.clientY };
this.zoom = Math.min(
this.maxZoom,
Math.max(this.minZoom, this.zoom + ev.deltaY * this.wheelFactor)
);
}

getDragStartPosition() {
return {
x: parseInt(this.attributes.originx.value),
y: parseInt(this.attributes.originy.value),
};
}

onDragged(sx, sy, dx, dy) {
this.attributes.originx.value = sx - dx;
this.attributes.originy.value = sy - dy;
}

update() {
const attrs = this.getObservedAttributes();
const zoom = parseFloat(attrs.zoom);
const originX = parseFloat(attrs.originx);
const originY = parseFloat(attrs.originy);

const parentEl = this;
const container = this.$(".container");

const parentHalfWidth = parentEl.clientWidth / 2;
const parentHalfHeight = parentEl.clientHeight / 2;

const translateX = parentHalfWidth - originX;
const translateY = parentHalfHeight - originY;

container.style.transformOrigin = `${parentHalfWidth}px ${parentHalfHeight}px`;

container.style.transform = `
scale(${zoom})
translate(${translateX}px, ${translateY}px)
`;
get viewport() {
return this.$(".viewport");
}

handleMutations(records) {
for (const record of records) {
if (record.type == 'attributes') {
if (!this.rendering) this.renderer.start();
} else if (record.type == 'childList') {
for (const node of record.removedNodes) {
if (node instanceof StickyNotesClusterLink) {
this.removeTopicLink(record.target, node);
} else if (node instanceof StickyNotesClusterTopic) {
this.graph.removeNode({ id: node.id });
} else if (node instanceof StickyNote) {
this.graph.removeNode({ id: node.id });
}
}
for (const node of record.addedNodes) {
if (node instanceof StickyNotesClusterLink) {
this.addTopicLink(record.target, node);
} else if (node instanceof StickyNotesClusterTopic) {
this.addTopic(node);
} else if (node instanceof StickyNote) {
this.upsertGraphNode(node);
}
}
}
}
}

addTopic(topicEl) {
this.upsertGraphNode(topicEl);
for (const linkEl of topicEl.querySelectorAll('sticky-notes-cluster-link')) {
this.addTopicLink(topicEl, linkEl);
}
}

addTopicLink(topicEl, linkEl) {
const linkedId = linkEl.getAttribute("href");
const linkedEl = this.ownerDocument.getElementById(linkedId);
if (!linkedEl) return;
let linkedNode = this.upsertGraphNode(linkedEl);
this.upsertGraphEdge(topicEl.id, linkedNode.id);
}

removeTopicLink(topicEl, linkEl) {
const linkedId = linkEl.getAttribute("href");
const edgeId = `${topicEl.id}-${linkedId}`;
let edge = this.graph.edges.find((e) => e.id === edgeId);
if (edge) this.graph.removeEdge(edge);
}

upsertGraphNode(nodeEl) {
const nodeId = nodeEl.getAttribute("id");
let node = this.graph.nodeSet[nodeId];
if (!node) {
const mass = (nodeEl.tagName === "STICKY-NOTES-CLUSTER-TOPIC") ? 10000 : 1;
const x = parseFloat(nodeEl.attributes.x.value) / this.graphLayoutScale;
const y = parseFloat(nodeEl.attributes.y.value) / this.graphLayoutScale;
node = new Springy.Node(nodeId, { mass, x, y });
this.graph.addNode(node);
}
return node;
}

upsertGraphEdge(fromId, toId) {
const edgeId = `${fromId}-${toId}`;
let edge = this.graph.edges.find((e) => e.id === edgeId);
if (!edge) {
const fromNode = this.graph.nodeSet[fromId];
const toNode = this.graph.nodeSet[toId];
edge = new Springy.Edge(edgeId, fromNode, toNode, {});
this.graph.addEdge(edge);
}
constructor() {
super();
}

}

customElements.define("sticky-notes-canvas", StickyNotesCanvas);
2 changes: 1 addition & 1 deletion public/lib/components/StickyNotesClusterTopic.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class StickyNotesClusterTopic extends StickyNotesCanvasChildDraggableMixi
align-items: center;
border: 1px solid black;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
z-index: -100;
z-index: 0;
}
</style>
<span class="title"></span>
Expand Down

0 comments on commit 49154dc

Please sign in to comment.