402 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			402 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
<!doctype html>
 | 
						||
<html lang="en"><head>
 | 
						||
  <meta charset="utf-8">
 | 
						||
  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
						||
  <title>Protocols 🌱 Digital Garden</title>
 | 
						||
  <meta name="description" content="
 | 
						||
">
 | 
						||
  <link rel="stylesheet" href="/assets/css/style.css"><link type="application/atom+xml" rel="alternate" href="/feed.xml" title="Digital Garden" /></head><body class="dark-green garamond links-dark-green bg-washed-green"><header class="f-6 flex"><a class="no-underline-hover" rel="author" href="/">Digital Garden</a><nav>, <a class="no-underline-hover" href="/about/">About</a></nav></header><main aria-label="Content">
 | 
						||
      <div class="flex">
 | 
						||
  <article class="w-50">
 | 
						||
    <div>
 | 
						||
      <h1>Protocols</h1>
 | 
						||
      <time datetime="2021-03-11T03:39:29+00:00">
 | 
						||
        Last updated on March 11, 2021
 | 
						||
        
 | 
						||
      </time>
 | 
						||
    </div>
 | 
						||
 | 
						||
    <div id="notes-entry-container">
 | 
						||
      <content>
 | 
						||
        
 | 
						||
 | 
						||
      </content>
 | 
						||
 | 
						||
      <side style="font-size: 0.9em">
 | 
						||
        <h3 style="margin-bottom: 1em">Notes mentioning this note</h3>
 | 
						||
        
 | 
						||
        <div style="display: grid; grid-gap: 1em; grid-template-columns: repeat(1fr);">
 | 
						||
        
 | 
						||
          <div class="backlink-box">
 | 
						||
          <a class="internal-link" href="/statement-of-intent">Statement of Intent</a><br>
 | 
						||
          <div style="font-size: 0.9em">Hypha’s practice is situated across many topics that are present in the theme of Adaptive Reuse & Creative Misuse. Drawing...</div>
 | 
						||
          </div>
 | 
						||
        
 | 
						||
        </div>
 | 
						||
        
 | 
						||
      </side>
 | 
						||
    </div>
 | 
						||
  </article>
 | 
						||
 | 
						||
  <div class="w-50">
 | 
						||
 | 
						||
  <p>Here are all the notes in this garden, along with their links, visualized as a graph.</p>
 | 
						||
 | 
						||
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"
 | 
						||
  integrity="sha512-FHsFVKQ/T1KWJDGSbrUhTJyS1ph3eRrxI228ND0EGaEp6v4a/vGwPWd3Dtd/+9cI7ccofZvl/wulICEurHN1pg=="
 | 
						||
  crossorigin="anonymous"></script>
 | 
						||
 | 
						||
<div id="zoom"></div>
 | 
						||
<div id="graph-wrapper">
 | 
						||
  <script>
 | 
						||
    const MINIMAL_NODE_SIZE = 8;
 | 
						||
    const MAX_NODE_SIZE = 12;
 | 
						||
    const ACTIVE_RADIUS_FACTOR = 1.5;
 | 
						||
    const STROKE = 1;
 | 
						||
    const FONT_SIZE = 16;
 | 
						||
    const TICKS = 200;
 | 
						||
    const FONT_BASELINE = 40;
 | 
						||
    const MAX_LABEL_LENGTH = 50;
 | 
						||
 | 
						||
    const graphData = {"edges":[{"source":"229320080329284392969231993","target":"50678562812897"},{"source":"229320080329284392969231993","target":"72697617314704"},{"source":"229320080329284392969231993","target":"36028"}],"nodes":[{"id":"50678562812897","path":"/hypertext","label":"Hypertext"},{"id":"72697617314704","path":"/protocols","label":"Protocols"},{"id":"36028","path":"/rss","label":"RSS"},{"id":"229320080329284392969231993","path":"/statement-of-intent","label":"Statement of Intent"}]}
 | 
						||
    let nodesData = graphData.nodes;
 | 
						||
    let linksData = graphData.edges;
 | 
						||
 | 
						||
    const nodeSize = {};
 | 
						||
 | 
						||
    const updateNodeSize = () => {
 | 
						||
      nodesData.forEach((el) => {
 | 
						||
        let weight =
 | 
						||
          3 *
 | 
						||
          Math.sqrt(
 | 
						||
            linksData.filter((l) => l.source === el.id || l.target === el.id)
 | 
						||
              .length + 1
 | 
						||
          );
 | 
						||
        if (weight < MINIMAL_NODE_SIZE) {
 | 
						||
          weight = MINIMAL_NODE_SIZE;
 | 
						||
        } else if (weight > MAX_NODE_SIZE) {
 | 
						||
          weight = MAX_NODE_SIZE;
 | 
						||
        }
 | 
						||
        nodeSize[el.id] = weight;
 | 
						||
      });
 | 
						||
    };
 | 
						||
 | 
						||
    const onClick = (d) => {
 | 
						||
      window.location = d.path
 | 
						||
    };
 | 
						||
 | 
						||
    const onMouseover = function (d) {
 | 
						||
      const relatedNodesSet = new Set();
 | 
						||
      linksData
 | 
						||
        .filter((n) => n.target.id == d.id || n.source.id == d.id)
 | 
						||
        .forEach((n) => {
 | 
						||
          relatedNodesSet.add(n.target.id);
 | 
						||
          relatedNodesSet.add(n.source.id);
 | 
						||
        });
 | 
						||
 | 
						||
      node.attr("class", (node_d) => {
 | 
						||
        if (node_d.id !== d.id && !relatedNodesSet.has(node_d.id)) {
 | 
						||
          return "inactive";
 | 
						||
        }
 | 
						||
        return "";
 | 
						||
      });
 | 
						||
 | 
						||
      link.attr("class", (link_d) => {
 | 
						||
        if (link_d.source.id !== d.id && link_d.target.id !== d.id) {
 | 
						||
          return "inactive";
 | 
						||
        }
 | 
						||
        return "";
 | 
						||
      });
 | 
						||
 | 
						||
      link.attr("stroke-width", (link_d) => {
 | 
						||
        if (link_d.source.id === d.id || link_d.target.id === d.id) {
 | 
						||
          return STROKE * 4;
 | 
						||
        }
 | 
						||
        return STROKE;
 | 
						||
      });
 | 
						||
      text.attr("class", (text_d) => {
 | 
						||
        if (text_d.id !== d.id && !relatedNodesSet.has(text_d.id)) {
 | 
						||
          return "inactive";
 | 
						||
        }
 | 
						||
        return "";
 | 
						||
      });
 | 
						||
    };
 | 
						||
 | 
						||
    const onMouseout = function (d) {
 | 
						||
      node.attr("class", "");
 | 
						||
      link.attr("class", "");
 | 
						||
      text.attr("class", "");
 | 
						||
      link.attr("stroke-width", STROKE);
 | 
						||
    };
 | 
						||
 | 
						||
    const sameNodes = (previous, next) => {
 | 
						||
      if (next.length !== previous.length) {
 | 
						||
        return false;
 | 
						||
      }
 | 
						||
 | 
						||
      const map = new Map();
 | 
						||
      for (const node of previous) {
 | 
						||
        map.set(node.id, node.label);
 | 
						||
      }
 | 
						||
 | 
						||
      for (const node of next) {
 | 
						||
        const found = map.get(node.id);
 | 
						||
        if (!found || found !== node.title) {
 | 
						||
          return false;
 | 
						||
        }
 | 
						||
      }
 | 
						||
 | 
						||
      return true;
 | 
						||
    };
 | 
						||
 | 
						||
    const sameEdges = (previous, next) => {
 | 
						||
      if (next.length !== previous.length) {
 | 
						||
        return false;
 | 
						||
      }
 | 
						||
 | 
						||
      const set = new Set();
 | 
						||
      for (const edge of previous) {
 | 
						||
        set.add(`${edge.source.id}-${edge.target.id}`);
 | 
						||
      }
 | 
						||
 | 
						||
      for (const edge of next) {
 | 
						||
        if (!set.has(`${edge.source}-${edge.target}`)) {
 | 
						||
          return false;
 | 
						||
        }
 | 
						||
      }
 | 
						||
 | 
						||
      return true;
 | 
						||
    };
 | 
						||
 | 
						||
    const graphWrapper = document.getElementById('graph-wrapper')
 | 
						||
    const element = document.createElementNS("http://www.w3.org/2000/svg", "svg");
 | 
						||
    element.setAttribute("width", graphWrapper.getBoundingClientRect().width);
 | 
						||
    element.setAttribute("height", window.innerHeight * 0.8);
 | 
						||
    graphWrapper.appendChild(element);
 | 
						||
 | 
						||
    const reportWindowSize = () => {
 | 
						||
      element.setAttribute("width", window.innerWidth);
 | 
						||
      element.setAttribute("height", window.innerHeight);
 | 
						||
    };
 | 
						||
 | 
						||
    window.onresize = reportWindowSize;
 | 
						||
 | 
						||
    const svg = d3.select("svg");
 | 
						||
    const width = Number(svg.attr("width"));
 | 
						||
    const height = Number(svg.attr("height"));
 | 
						||
    let zoomLevel = 1;
 | 
						||
 | 
						||
    const simulation = d3
 | 
						||
      .forceSimulation(nodesData)
 | 
						||
      .force("forceX", d3.forceX().x(width / 2))
 | 
						||
      .force("forceY", d3.forceY().y(height / 2))
 | 
						||
      .force("charge", d3.forceManyBody())
 | 
						||
      .force(
 | 
						||
        "link",
 | 
						||
        d3
 | 
						||
          .forceLink(linksData)
 | 
						||
          .id((d) => d.id)
 | 
						||
          .distance(70)
 | 
						||
      )
 | 
						||
      .force("center", d3.forceCenter(width / 2, height / 2))
 | 
						||
      .force("collision", d3.forceCollide().radius(80))
 | 
						||
      .stop();
 | 
						||
 | 
						||
    const g = svg.append("g");
 | 
						||
    let link = g.append("g").attr("class", "links").selectAll(".link");
 | 
						||
    let node = g.append("g").attr("class", "nodes").selectAll(".node");
 | 
						||
    let text = g.append("g").attr("class", "text").selectAll(".text");
 | 
						||
 | 
						||
    const resize = () => {
 | 
						||
      if (d3.event) {
 | 
						||
        const scale = d3.event.transform;
 | 
						||
        zoomLevel = scale.k;
 | 
						||
        g.attr("transform", scale);
 | 
						||
      }
 | 
						||
 | 
						||
      const zoomOrKeep = (value) => (zoomLevel >= 1 ? value / zoomLevel : value);
 | 
						||
 | 
						||
      const font = Math.max(Math.round(zoomOrKeep(FONT_SIZE)), 1);
 | 
						||
 | 
						||
      text.attr("font-size", (d) => font);
 | 
						||
      text.attr("y", (d) => d.y - zoomOrKeep(FONT_BASELINE) + 8);
 | 
						||
      link.attr("stroke-width", zoomOrKeep(STROKE));
 | 
						||
      node.attr("r", (d) => {
 | 
						||
        return zoomOrKeep(nodeSize[d.id]);
 | 
						||
      });
 | 
						||
      svg
 | 
						||
        .selectAll("circle")
 | 
						||
        .filter((_d, i, nodes) => d3.select(nodes[i]).attr("active"))
 | 
						||
        .attr("r", (d) => zoomOrKeep(ACTIVE_RADIUS_FACTOR * nodeSize[d.id]));
 | 
						||
 | 
						||
      document.getElementById("zoom").innerHTML = zoomLevel.toFixed(2);
 | 
						||
    };
 | 
						||
 | 
						||
    const ticked = () => {
 | 
						||
      node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
 | 
						||
      text
 | 
						||
        .attr("x", (d) => d.x)
 | 
						||
        .attr("y", (d) => d.y - (FONT_BASELINE - nodeSize[d.id]) / zoomLevel);
 | 
						||
      link
 | 
						||
        .attr("x1", (d) => d.source.x)
 | 
						||
        .attr("y1", (d) => d.source.y)
 | 
						||
        .attr("x2", (d) => d.target.x)
 | 
						||
        .attr("y2", (d) => d.target.y);
 | 
						||
    };
 | 
						||
 | 
						||
    const restart = () => {
 | 
						||
      updateNodeSize();
 | 
						||
      node = node.data(nodesData, (d) => d.id);
 | 
						||
      node.exit().remove();
 | 
						||
      node = node
 | 
						||
        .enter()
 | 
						||
        .append("circle")
 | 
						||
        .attr("r", (d) => {
 | 
						||
          return nodeSize[d.id];
 | 
						||
        })
 | 
						||
        .on("click", onClick)
 | 
						||
        .on("mouseover", onMouseover)
 | 
						||
        .on("mouseout", onMouseout)
 | 
						||
        .merge(node);
 | 
						||
 | 
						||
      link = link.data(linksData, (d) => `${d.source.id}-${d.target.id}`);
 | 
						||
      link.exit().remove();
 | 
						||
      link = link.enter().append("line").attr("stroke-width", STROKE).merge(link);
 | 
						||
 | 
						||
      text = text.data(nodesData, (d) => d.label);
 | 
						||
      text.exit().remove();
 | 
						||
      text = text
 | 
						||
        .enter()
 | 
						||
        .append("text")
 | 
						||
        .text((d) => shorten(d.label.replace(/_*/g, ""), MAX_LABEL_LENGTH))
 | 
						||
        .attr("font-size", `${FONT_SIZE}px`)
 | 
						||
        .attr("text-anchor", "middle")
 | 
						||
        .attr("alignment-baseline", "central")
 | 
						||
        .on("click", onClick)
 | 
						||
        .on("mouseover", onMouseover)
 | 
						||
        .on("mouseout", onMouseout)
 | 
						||
        .merge(text);
 | 
						||
 | 
						||
      node.attr("active", (d) => isCurrentPath(d.path) ? true : null);
 | 
						||
      text.attr("active", (d) => isCurrentPath(d.path) ? true : null);
 | 
						||
 | 
						||
      simulation.nodes(nodesData);
 | 
						||
      simulation.force("link").links(linksData);
 | 
						||
      simulation.alpha(1).restart();
 | 
						||
      simulation.stop();
 | 
						||
 | 
						||
      for (let i = 0; i < TICKS; i++) {
 | 
						||
        simulation.tick();
 | 
						||
      }
 | 
						||
 | 
						||
      ticked();
 | 
						||
    };
 | 
						||
 | 
						||
    const zoomHandler = d3.zoom().scaleExtent([0.2, 3]).on("zoom", resize);
 | 
						||
 | 
						||
    zoomHandler(svg);
 | 
						||
    restart();
 | 
						||
 | 
						||
    function isCurrentPath(notePath) {
 | 
						||
      return window.location.pathname.includes(notePath)
 | 
						||
    }
 | 
						||
 | 
						||
    function shorten(str, maxLen, separator = ' ') {
 | 
						||
      if (str.length <= maxLen) return str;
 | 
						||
      return str.substr(0, str.lastIndexOf(separator, maxLen)) + '...';
 | 
						||
    }
 | 
						||
  </script>
 | 
						||
</div>
 | 
						||
  </div>
 | 
						||
</div>
 | 
						||
    </main><footer>
 | 
						||
 | 
						||
</footer><!-- That file is not particularly elegant. This will need a refactor at some point. -->
 | 
						||
<div style="opacity: 0; display: none;" id='tooltip-wrapper'>
 | 
						||
  <div id='tooltip-content'>
 | 
						||
  </div>
 | 
						||
</div>
 | 
						||
 | 
						||
<iframe style="display: none; height: 0; width: 0;" id='link-preview-iframe' src="">
 | 
						||
</iframe>
 | 
						||
 | 
						||
<script>
 | 
						||
  var opacityTimeout;
 | 
						||
  var contentTimeout;
 | 
						||
  var transitionDurationMs = 100;
 | 
						||
 | 
						||
  var iframe = document.getElementById('link-preview-iframe')
 | 
						||
  var tooltipWrapper = document.getElementById('tooltip-wrapper')
 | 
						||
  var tooltipContent = document.getElementById('tooltip-content')
 | 
						||
 | 
						||
  function hideTooltip() {
 | 
						||
    opacityTimeout = setTimeout(function() {
 | 
						||
      tooltipWrapper.style.opacity = 0;
 | 
						||
      contentTimeout = setTimeout(function() {
 | 
						||
        tooltipContent.innerHTML = '';
 | 
						||
        tooltipWrapper.style.display = 'none';
 | 
						||
      }, transitionDurationMs + 1);
 | 
						||
    }, transitionDurationMs)
 | 
						||
  }
 | 
						||
 | 
						||
  function showTooltip(event) {
 | 
						||
    var elem = event.target;
 | 
						||
    var elem_props = elem.getClientRects()[elem.getClientRects().length - 1];
 | 
						||
    var top = window.pageYOffset || document.documentElement.scrollTop
 | 
						||
 | 
						||
    if (event.target.host === window.location.host) {
 | 
						||
      iframe.src = event.target.href
 | 
						||
      iframe.onload = function() {
 | 
						||
        tooltipContentHtml = ''
 | 
						||
        tooltipContentHtml += '<div style="font-weight: bold;">' + iframe.contentWindow.document.querySelector('h1').innerHTML + '</div>'
 | 
						||
        tooltipContentHtml += iframe.contentWindow.document.querySelector('content').innerHTML
 | 
						||
 | 
						||
        tooltipContent.innerHTML = tooltipContentHtml
 | 
						||
 | 
						||
        tooltipWrapper.style.display = 'block';
 | 
						||
        setTimeout(function() {
 | 
						||
          tooltipWrapper.style.opacity = 1;
 | 
						||
        }, 1)
 | 
						||
      }
 | 
						||
 | 
						||
      tooltipWrapper.style.left = elem_props.left - (tooltipWrapper.offsetWidth / 2) + (elem_props.width / 2) + "px";
 | 
						||
      if ((window.innerHeight - elem_props.top) < (tooltipWrapper.offsetHeight)) {
 | 
						||
          tooltipWrapper.style.top = elem_props.top + top - tooltipWrapper.offsetHeight - 10 + "px";
 | 
						||
      } else if ((window.innerHeight - elem_props.top) > (tooltipWrapper.offsetHeight)) {
 | 
						||
          tooltipWrapper.style.top = elem_props.top + top + 35 + "px";
 | 
						||
      }
 | 
						||
 | 
						||
      if ((elem_props.left + (elem_props.width / 2)) < (tooltipWrapper.offsetWidth / 2)) {
 | 
						||
          tooltipWrapper.style.left = "10px";
 | 
						||
      } else if ((document.body.clientWidth - elem_props.left - (elem_props.width / 2)) < (tooltipWrapper.offsetWidth / 2)) {
 | 
						||
          tooltipWrapper.style.left = document.body.clientWidth - tooltipWrapper.offsetWidth - 20 + "px";
 | 
						||
      }
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  function setupListeners(linkElement) {
 | 
						||
    linkElement.addEventListener('mouseleave', function(_event) {
 | 
						||
      hideTooltip();
 | 
						||
    });
 | 
						||
 | 
						||
    tooltipWrapper.addEventListener('mouseleave', function(_event) {
 | 
						||
      hideTooltip();
 | 
						||
    });
 | 
						||
 | 
						||
    linkElement.addEventListener('mouseenter', function(event) {
 | 
						||
      clearTimeout(opacityTimeout);
 | 
						||
      clearTimeout(contentTimeout);
 | 
						||
      showTooltip(event);
 | 
						||
    });
 | 
						||
 | 
						||
    tooltipWrapper.addEventListener('mouseenter', function(event) {
 | 
						||
      clearTimeout(opacityTimeout);
 | 
						||
      clearTimeout(contentTimeout);
 | 
						||
    });
 | 
						||
  }
 | 
						||
 | 
						||
  document.querySelectorAll('content a').forEach(setupListeners);
 | 
						||
</script>
 | 
						||
 | 
						||
  </body>
 | 
						||
</html> |