diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2021-01-27 14:33:22 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2021-01-27 14:41:09 +0000 |
| commit | 52f5c716fb09d0be315338703caf22c6261690ba (patch) | |
| tree | f98cea42d411c8fd48cca00c093401d797e44495 /src | |
| parent | node collapsing (w/o relayouting) (diff) | |
| download | fedidag-52f5c716fb09d0be315338703caf22c6261690ba.tar.gz fedidag-52f5c716fb09d0be315338703caf22c6261690ba.zip | |
actually visually collapse nodes
Diffstat (limited to 'src')
| -rw-r--r-- | src/graph.json | 1 | ||||
| -rw-r--r-- | src/index.js | 56 | ||||
| -rw-r--r-- | src/layout.js | 118 | ||||
| -rw-r--r-- | src/ui.js | 11 | ||||
| -rw-r--r-- | src/viz.es.js | 131 |
5 files changed, 100 insertions, 217 deletions
diff --git a/src/graph.json b/src/graph.json index 5c3fcbf..a1f67d5 100644 --- a/src/graph.json +++ b/src/graph.json @@ -3,6 +3,7 @@ "id": "https://dag.s-ol.nu/discussions/1", "type": "Document", "name": "DiscDAG's Future", + "first": "https://dag.s-ol.nu/users/ColinWright/notes/20200510151146a", "items": [ { "id": "https://dag.s-ol.nu/users/ColinWright/notes/20200510151146a", diff --git a/src/index.js b/src/index.js index 111a7f6..6a20a0c 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; import * as jsonld from 'jsonld'; import * as dom from './dom'; -import * as layout from './layout'; +import Layout from './layout'; import { Note } from './ui'; window.jsonld = jsonld; @@ -62,6 +62,7 @@ const loadData = async () => { { '@context': context, type: 'Document', + first: { '@embed': '@never' }, items: { context: { '@embed': '@never' }, replies: { '@embed': '@never' }, @@ -103,12 +104,12 @@ const createNotes = (graph, discussion) => { }; const layoutNodes = async (graph) => { - let state = layout.init(); + const layout = new Layout(); for (const node of Object.values(graph)) - state = layout.addNode(state, node); + layout.addNode(graph, node); - await layout.render(state, canvas, + await layout.render(canvas, (id, x, y) => { const dom = graph[id].dom; dom.style.left = `${x - dom.offsetWidth/2}px`; @@ -128,22 +129,15 @@ const update = async (graph) => { createNotes(graph, discussion); await layoutNodes(graph); -}; -const scrollToRoot = (graph) => { - for (const node of Object.values(graph)) { - if (node.data.inReplyTo.length === 0) { - node.dom.scrollIntoView({ block: 'nearest', inline: 'nearest' }); - return; - } - } -} + const root = graph[discussion.first]; + root.dom.scrollIntoView({ block: 'nearest', inline: 'nearest' }); +}; // update collapsed nodes starting with id // returns list of (potentially) affected nodes -const updateCollapsed = (graph, id, updated=[]) => { - updated.push(id); - const node = graph[id]; +const updateCollapsed = (graph, node, updated=[]) => { + updated.push(node.data.id); if (node.collapsed !== 'explicit') { node.collapsed = @@ -151,8 +145,8 @@ const updateCollapsed = (graph, id, updated=[]) => { && node.data.inReplyTo.every(p => graph[p.id].collapsed); } - for (const child of graph[id].data.replies) - updateCollapsed(graph, child.id, updated); + for (const child of node.data.replies) + updateCollapsed(graph, graph[child.id], updated); return updated; }; @@ -161,20 +155,26 @@ const graph = {}; window.graph = graph; update(graph) - .then(() => scrollToRoot(graph)) .catch(err => console.error(err)); const hideNote = (e) => { - const node = graph[e.currentTarget.dataset.id]; - - node.collapsed = node.collapsed ? false : 'explicit'; - - for (const id of updateCollapsed(graph, node.data.id)) { - graph[id].dom.style.opacity = graph[id].collapsed ? 0.2 : 1.0; + const clicked = graph[e.currentTarget.dataset.id]; + clicked.collapsed = clicked.collapsed ? false : 'explicit'; + + for (const id of updateCollapsed(graph, clicked)) { + const node = graph[id]; + + if (node.collapsed) { + const noParents = node.data.inReplyTo.every(p => graph[p.id].collapsed); + const noChildren = node.data.replies.every(c => graph[c.id].collapsed); + node.dom.className = noParents && noChildren ? 'hidden' : 'collapsed'; + } else { + node.dom.className = ''; + } } - // layoutNodes(graph) - // .then(() => scrollToRoot(graph)) - // .catch(err => console.error(err)); + layoutNodes(graph) + .then(() => clicked.dom.scrollIntoView({ block: 'nearest', inline: 'nearest' })) + .catch(err => console.error(err)); }; diff --git a/src/layout.js b/src/layout.js index e256681..bda2886 100644 --- a/src/layout.js +++ b/src/layout.js @@ -1,74 +1,76 @@ import Viz from './viz.es'; -const graphviz = new Viz({ workerURL: 'lib/lite.render.js' }); - -export const init = () => { - return 'digraph { node [shape="rectangle"];\n'; -} +const toNumber = (i) => Number(i); +const gv = new Viz({ workerURL: 'lib/lite.render.js' }); -export const addNode = (dot, node) => { - if (node.collapsed) - return dot; +export default class Layout { + constructor() { + this.src = 'digraph { node [shape="rectangle"];\n'; + } - const id = node.data.id; - dot += ` "${id}" [fixedsize=true; width=${node.dom.offsetWidth / 72}; height=${node.dom.offsetHeight / 72}];\n`; + addNode(graph, node) { + if (node.dom.className === 'hidden') + return; - for (const reply of node.data.replies) - dot += ` "${id}" -> "${reply.id}";\n`; + const id = node.data.id; + this.src += ` "${id}" [fixedsize=true; width=${node.dom.offsetWidth / 72}; height=${node.dom.offsetHeight / 72}];\n`; - return dot; -}; + for (const reply of node.data.replies) { + if (node.collapsed && graph[reply.id].collapsed) + continue; -const toNumber = (i) => Number(i); + this.src += ` "${id}" -> "${reply.id}";\n`; + } + } -export const render = async (dot, canvas, setPosition, setSize) => { - dot += '}'; + async render(canvas, setPosition, setSize) { + this.src += '}'; - const info = await graphviz.renderJSONObject(dot, { format: 'json0' }); - window.info = info; + const info = await gv.renderJSONObject(this.src, { format: 'json0' }); - const [_x, _y, width, height] = info.bb.split(',').map(toNumber); + const [_x, _y, width, height] = info.bb.split(',').map(toNumber); - setSize(width, height); + setSize(width, height); - for (const object of info.objects) { - const [x, y] = object.pos.split(',').map(toNumber); - setPosition(object.name, x, height - y); - } + for (const object of info.objects) { + const [x, y] = object.pos.split(',').map(toNumber); + setPosition(object.name, x, height - y); + } - const ctx = canvas.getContext('2d'); - ctx.lineWidth = 4; - ctx.lineCap = 'round'; - ctx.lineJoin = 'round'; - ctx.strokeStyle = '#696969'; - ctx.fillStyle = '#696969'; - - for (const edge of info.edges) { - const pos = edge.pos.slice(2); - const points = pos.split(' ').map(p => { - const [x, y] = p.split(','); - return [toNumber(x), height - toNumber(y)]; - }); - - ctx.beginPath(); - ctx.moveTo(...points[1]); - for (let i = 4; i < points.length; i += 3) { - const [ax, ay] = points[i - 2]; - const [bx, by] = points[i - 1]; - const [x, y] = points[i]; - ctx.bezierCurveTo(ax, ay, bx, by, x, y); + const ctx = canvas.getContext('2d'); + ctx.lineWidth = 4; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.strokeStyle = '#696969'; + ctx.fillStyle = '#696969'; + + for (const edge of info.edges) { + const pos = edge.pos.slice(2); + const points = pos.split(' ').map(p => { + const [x, y] = p.split(','); + return [toNumber(x), height - toNumber(y)]; + }); + + ctx.beginPath(); + ctx.moveTo(...points[1]); + for (let i = 4; i < points.length; i += 3) { + const [ax, ay] = points[i - 2]; + const [bx, by] = points[i - 1]; + const [x, y] = points[i]; + ctx.bezierCurveTo(ax, ay, bx, by, x, y); + } + ctx.stroke(); + + const [lx, ly] = points[points.length - 1]; + const [tx, ty] = points[0]; + const [dx, dy] = [tx - lx, ty - ly]; + const [hx, hy] = [dy*0.7, dx*-0.7]; + + ctx.beginPath(); + ctx.moveTo(lx + dx*0.8, ly + dy*0.8); + ctx.lineTo(lx - dx*0.5 + hx, ly - dy*0.5 + hy); + ctx.lineTo(lx - dx*0.5 - hx, ly - dy*0.5 - hy); + ctx.fill(); } - ctx.stroke(); - - const [lx, ly] = points[points.length - 1]; - const [tx, ty] = points[0]; - const [dx, dy] = [tx - lx, ty - ly]; - const [hx, hy] = [dy*0.7, dx*-0.7]; - - ctx.beginPath(); - ctx.moveTo(lx + dx*0.8, ly + dy*0.8); - ctx.lineTo(lx - dx*0.5 + hx, ly - dy*0.5 + hy); - ctx.lineTo(lx - dx*0.5 - hx, ly - dy*0.5 - hy); - ctx.fill(); } } @@ -96,6 +96,17 @@ section > main { section.tombstone > main { font-family: inherit; } + +section.hidden { + display: none; +} + +section.collapsed { + width: auto; +} +section.collapsed > main { + display: none; +} `; const colors = new ColorHash({ lightness: 0.8 }); diff --git a/src/viz.es.js b/src/viz.es.js index 3c63e69..56f6a98 100644 --- a/src/viz.es.js +++ b/src/viz.es.js @@ -123,110 +123,6 @@ var ModuleWrapper = function ModuleWrapper(module, render) { // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding - -function b64EncodeUnicode(str) { - return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { - return String.fromCharCode('0x' + p1); - })); -} - -function defaultScale() { - if ('devicePixelRatio' in window && window.devicePixelRatio > 1) { - return window.devicePixelRatio; - } else { - return 1; - } -} - -function svgXmlToImageElement(svgXml) { - var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, - _ref$scale = _ref.scale, - scale = _ref$scale === undefined ? defaultScale() : _ref$scale, - _ref$mimeType = _ref.mimeType, - mimeType = _ref$mimeType === undefined ? "image/png" : _ref$mimeType, - _ref$quality = _ref.quality, - quality = _ref$quality === undefined ? 1 : _ref$quality; - - return new Promise(function (resolve, reject) { - var svgImage = new Image(); - - svgImage.onload = function () { - var canvas = document.createElement('canvas'); - canvas.width = svgImage.width * scale; - canvas.height = svgImage.height * scale; - - var context = canvas.getContext("2d"); - context.drawImage(svgImage, 0, 0, canvas.width, canvas.height); - - canvas.toBlob(function (blob) { - var image = new Image(); - image.src = URL.createObjectURL(blob); - image.width = svgImage.width; - image.height = svgImage.height; - - resolve(image); - }, mimeType, quality); - }; - - svgImage.onerror = function (e) { - var error; - - if ('error' in e) { - error = e.error; - } else { - error = new Error('Error loading SVG'); - } - - reject(error); - }; - - svgImage.src = 'data:image/svg+xml;base64,' + b64EncodeUnicode(svgXml); - }); -} - -function svgXmlToImageElementFabric(svgXml) { - var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, - _ref2$scale = _ref2.scale, - scale = _ref2$scale === undefined ? defaultScale() : _ref2$scale, - _ref2$mimeType = _ref2.mimeType, - mimeType = _ref2$mimeType === undefined ? 'image/png' : _ref2$mimeType, - _ref2$quality = _ref2.quality, - quality = _ref2$quality === undefined ? 1 : _ref2$quality; - - var multiplier = scale; - - var format = void 0; - if (mimeType == 'image/jpeg') { - format = 'jpeg'; - } else if (mimeType == 'image/png') { - format = 'png'; - } - - return new Promise(function (resolve, reject) { - fabric.loadSVGFromString(svgXml, function (objects, options) { - // If there's something wrong with the SVG, Fabric may return an empty array of objects. Graphviz appears to give us at least one <g> element back even given an empty graph, so we will assume an error in this case. - if (objects.length == 0) { - reject(new Error('Error loading SVG with Fabric')); - } - - var element = document.createElement("canvas"); - element.width = options.width; - element.height = options.height; - - var canvas = new fabric.Canvas(element, { enableRetinaScaling: false }); - var obj = fabric.util.groupSVGElements(objects, options); - canvas.add(obj).renderAll(); - - var image = new Image(); - image.src = canvas.toDataURL({ format: format, multiplier: multiplier, quality: quality }); - image.width = options.width; - image.height = options.height; - - resolve(image); - }); - }); -} - var Viz = function () { function Viz() { var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, @@ -277,33 +173,6 @@ var Viz = function () { return this.wrapper.render(src, { format: format, engine: engine, files: files, images: images, yInvert: yInvert, nop: nop }); } }, { - key: 'renderSVGElement', - value: function renderSVGElement(src) { - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) { - var parser = new DOMParser(); - return parser.parseFromString(str, 'image/svg+xml').documentElement; - }); - } - }, { - key: 'renderImageElement', - value: function renderImageElement(src) { - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - var scale = options.scale, - mimeType = options.mimeType, - quality = options.quality; - - - return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) { - if ((typeof fabric === 'undefined' ? 'undefined' : _typeof(fabric)) === "object" && fabric.loadSVGFromString) { - return svgXmlToImageElementFabric(str, { scale: scale, mimeType: mimeType, quality: quality }); - } else { - return svgXmlToImageElement(str, { scale: scale, mimeType: mimeType, quality: quality }); - } - }); - } - }, { key: 'renderJSONObject', value: function renderJSONObject(src) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; |
