portfolio/assets/js/portfolio.js

1422 lines
48 KiB
JavaScript
Raw Normal View History

2019-11-22 21:51:44 +01:00
'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var data;
function getLabelAttribute(node) {
if (typeof node['https://schema.org/name'] !== "undefined") {
return 'https://schema.org/name';
}
switch (node['@type']) {
case "https://schema.org/WebSite":
if (typeof node['https://schema.org/url'] !== "undefined") {
return 'https://schema.org/url';
}
break;
case "https://schema.org/ImageObject":
if (typeof node['https://schema.org/caption'] !== "undefined") {
return 'https://schema.org/caption';
}
if (typeof node['https://schema.org/contentUrl'] !== "undefined") {
return 'https://schema.org/contentUrl';
}
break;
case "https://schema.org/PostalAddress":
if (typeof node['https://schema.org/addressLocality'] !== "undefined") {
return 'https://schema.org/addressLocality';
}
break;
}
return '@id';
}
function getNodeLabel(node) {
var labelAttr = getLabelAttribute(node);
var label = node[labelAttr];
if (typeof label == "undefined") label = node["@id"];
if (typeof label == "undefined") label = "";
return label;
}
function getNodeYear(n) {
if (typeof n['https://schema.org/dateCreated'] !== 'undefined') {
return n['https://schema.org/dateCreated'].substr(0, 4);
}
if (typeof n['https://schema.org/datePublished'] !== 'undefined') {
return n['https://schema.org/datePublished'].substr(0, 4);
}
if (typeof n['https://schema.org/startDate'] !== 'undefined') {
// console.log(n['https://schema.org/startDate']);
2019-11-22 21:51:44 +01:00
return n['https://schema.org/startDate'].substr(0, 4);
}
if (typeof n['https://schema.org/endDate'] !== 'undefined') {
return n['https://schema.org/endDate'].substr(0, 4);
}
if (typeof n['https://schema.org/foundingDate'] !== 'undefined') {
return n['https://schema.org/foundingDate'].substr(0, 4);
}
2020-01-28 07:05:25 +01:00
if (typeof n['https://schema.org/temporalCoverage'] !== 'undefined') {
if (n['https://schema.org/temporalCoverage'].match(/\d{4}-\d{4}/)) {
return n['https://schema.org/temporalCoverage'].substr(5, 4);
}
}
2019-11-22 21:51:44 +01:00
return null;
}
function getDisplayAttr(attr) {
return attr.replace(/.*[#|\/]/, "");
}
/**
Transform a flattened jsonld into a d3 compatible graph
@param Object data flattened jsonld data
@return Object graph has keys "nodes" and "links"
*/
function jsonLdToGraph(data) {
var nodes = {};
var links = [];
// collect all nodes
for (var nodeId in data) {
// data[nodeId]["@type"][0] = data[nodeId]["@type"][0];
nodes[data[nodeId]["@id"]] = data[nodeId];
}
// collect all links (separate loop as we need to check nodes)
for (var _nodeId in data) {
var node = data[_nodeId];
var currentId = node["@id"];
for (var key in node) {
var nodeAttr = Array.isArray(node[key]) ? node[key] : [node[key]];
// // relations should always be lists (eases assumptions)
// if(typeof node[key] !== "Array" && typeof node[key]['id'] !== "undefined") {
// node[key] = [node[key]];
// }
// every attribute is an Array after flatten(), loop them
for (var i in nodeAttr) {
if (key !== "@id" && typeof nodeAttr[i] === "string" && nodes[nodeAttr[i]]) {
links[links.length] = {
"source": currentId,
"target": nodeAttr[i],
"name": key
};
} else if (typeof nodeAttr[i]["@id"] !== "undefined") {
// if there is just one item, flatten/expand has turned urls in objects with just an id
// reverse this, as we don't want these separate for this project
if (Object.keys(nodeAttr[i]).length == 1 && typeof nodes[nodeAttr[i]["@id"]] === "undefined") {
// skip
// nodeAttr = nodeAttr[i]["id"];
} else {
links[links.length] = {
"source": currentId,
"target": nodeAttr[i]["@id"],
"name": key
};
}
}
}
}
}
return {
"nodes": Object.values(nodes),
"links": links
};
}
var graph;
// map nodes to their ID
var nodeMap = {};
var linkMap = {};
var breadcrumbs = {};
var weights = {};
2019-11-22 21:51:44 +01:00
// load the flattened jsonld file
var requestPromise = fetch('/assets/js/rubenvandeven.jsonld').then(function (r) {
return r.json();
});
var rankingPromise = fetch('/assets/js/ranking.json').then(function (r) {
return r.json();
});
Promise.all([requestPromise, rankingPromise]).then(function (values) {
if (values[0].hasOwnProperty('@graph')) {
data = values[0];
weights = values[1];
} else {
data = values[1];
weights = values[0];
}
2019-11-22 21:51:44 +01:00
graph = jsonLdToGraph(data['@graph']);
// create a map of nodes by id.
for (var i in graph.nodes) {
nodeMap[graph.nodes[i]['@id']] = graph.nodes[i];
}
startGraph(graph);
});
function inCircle(dx, dy, r) {
// fastest check if in circle: https://stackoverflow.com/a/7227057
var dxAbs = Math.abs(dx);
var dyAbs = Math.abs(dy);
if (dxAbs > r || dyAbs > r) {
return false;
} else if (dxAbs + dyAbs <= r) {
return true;
} else if (Math.pow(dx, 2) + Math.pow(dy, 2) <= Math.pow(r, 2)) {
return true;
} else {
return false;
}
}
function createLinkMap(graph) {
var linkMap = {};
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = graph['links'][Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var link = _step.value;
if (typeof linkMap[link['source']] == 'undefined') {
linkMap[link['source']] = [];
}
linkMap[link['source']][linkMap[link['source']].length] = { 'id': link['target'], 'name': link['name'] };
if (typeof linkMap[link['target']] == 'undefined') {
linkMap[link['target']] = [];
}
linkMap[link['target']][linkMap[link['target']].length] = { 'id': link['source'], 'name': link['name'] };
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
return linkMap;
}
// config
var nodeSize = 40;
var selectedNodeSize = 140;
var firstNodeId = "https://rubenvandeven.com/";
function getSizeForNode(node) {
if (node.hasOwnProperty('https://schema.org/thumbnailUrl')) return nodeSize;
if (weights[node['@id']]) return nodeSize * weights[node['@id']];
2019-11-22 21:51:44 +01:00
if (node['@id'] == firstNodeId) return nodeSize * 1.2;
// everynode has at least one link. these should equal 1
return nodeSize * (.7 + Math.min(20, linkMap[node['@id']].length) / 40);
return nodeSize;
}
// TODO: make sure, 'shortest' path is favoured.
function createBreadcrumbs(linkMap, srcId) {
var crumbs = {};
var createBreadcrumbLayer = function createBreadcrumbLayer(srcId) {
var path = crumbs[srcId];
var newPath = path.slice();
newPath.push(srcId);
var nextSrcIds = [];
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = linkMap[srcId][Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var link = _step2.value;
if (typeof crumbs[link['id']] !== 'undefined') continue;
crumbs[link['id']] = newPath;
nextSrcIds.push(link['id']);
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
return nextSrcIds;
};
crumbs[srcId] = [];
var nextIds = [srcId];
while (nextIds.length > 0) {
var newNextIds = [];
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = nextIds[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var nextId = _step3.value;
var r = createBreadcrumbLayer(nextId);
newNextIds = newNextIds.concat(r);
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
nextIds = newNextIds;
}
return crumbs;
}
var nodePositions = {};
function startGraph(graph) {
// set some vars
var currentNodeIdx = 0;
var currentNodePositionRadius = 0;
var types = {};
linkMap = createLinkMap(graph);
breadcrumbs = createBreadcrumbs(linkMap, firstNodeId);
for (var nodeIdx in graph['nodes']) {
var type = graph['nodes'][nodeIdx]["@type"];
if (typeof types[type] == 'undefined') {
types[type] = [];
}
types[type].push(nodeIdx);
}
var graphControlsEl = document.getElementById('graphControls');
var typeLinksEl = document.getElementById('typeLinks');
var showMoreTypeLinksEl = document.getElementById('showMoreTypeLinks');
var moreTypeLinksEl = document.getElementById('moreTypeLinks');
var relLinksEl = document.getElementById('relLinks');
// sort types by count:
var typeCounts = Object.keys(types).map(function (key) {
return [key, types[key].length];
});
typeCounts.sort(function (first, second) {
return second[1] - first[1];
});
// make controls
var i = 0;
var _loop = function _loop(typeCountIdx) {
var typeName = typeCounts[typeCountIdx][0];
var typeLinkEl = document.createElement("li");
var typeLinkAEl = document.createElement("a");
var typeLinkCountEl = document.createElement("span");
typeLinkCountEl.innerHTML = typeCounts[typeCountIdx][1];
typeLinkCountEl.classList.add('typeCount');
typeLinkAEl.innerHTML = getDisplayAttr(typeName);
typeLinkAEl.title = typeName;
typeLinkAEl.addEventListener('click', function () {
centerByType(typeName);
// positionNodesInCenter(types[typeName]);
});
typeLinkAEl.addEventListener('mouseover', function () {
var typeNodeEls = document.getElementsByClassName(typeName);
var _iteratorNormalCompletion7 = true;
var _didIteratorError7 = false;
var _iteratorError7 = undefined;
try {
for (var _iterator7 = typeNodeEls[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
var typeNodeEl = _step7.value;
typeNodeEl.classList.add('typeHighlight');
}
} catch (err) {
_didIteratorError7 = true;
_iteratorError7 = err;
} finally {
try {
if (!_iteratorNormalCompletion7 && _iterator7.return) {
_iterator7.return();
}
} finally {
if (_didIteratorError7) {
throw _iteratorError7;
}
}
}
});
typeLinkAEl.addEventListener('mouseout', function () {
var typeNodeEls = document.getElementsByClassName(typeName);
var _iteratorNormalCompletion8 = true;
var _didIteratorError8 = false;
var _iteratorError8 = undefined;
try {
for (var _iterator8 = typeNodeEls[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
var typeNodeEl = _step8.value;
typeNodeEl.classList.remove('typeHighlight');
}
} catch (err) {
_didIteratorError8 = true;
_iteratorError8 = err;
} finally {
try {
if (!_iteratorNormalCompletion8 && _iterator8.return) {
_iterator8.return();
}
} finally {
if (_didIteratorError8) {
throw _iteratorError8;
}
}
}
});
typeLinkEl.append(typeLinkAEl);
typeLinkEl.append(typeLinkCountEl);
(i < 5 ? typeLinksEl : moreTypeLinksEl).appendChild(typeLinkEl);
i++;
// typeLinksEl.appendChild(typeLinkEl);
};
for (var typeCountIdx in typeCounts) {
_loop(typeCountIdx);
}
showMoreTypeLinksEl.addEventListener('click', function () {
document.body.classList.add('showMoreLinks');
var hideMoreTypeLinks = function hideMoreTypeLinks(e) {
e.preventDefault();
e.stopPropagation();
document.body.removeEventListener('mouseup', hideMoreTypeLinks, true);
document.body.classList.remove('showMoreLinks');
};
document.body.addEventListener('mouseup', hideMoreTypeLinks, true);
}, false);
// make svg
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var container = svg.append("g").attr("id", "container");
var simulation = d3.forceSimulation().force("link", d3.forceLink().id(function (d) {
return d["@id"];
}).strength(.005)).force("charge", d3.forceManyBody()) // doesn't seem necessary?
.force("collision", d3.forceCollide(function (d) {
return getSizeForNode(d) * 1.1; // avoid overlapping nodes
}))
// .force("center", d3.forceCenter(width / 2, height / 2)) // position around center
// .force("x", d3.forceX())
// .force("y", d3.forceY())
// .force("y", d3.forceY())
;
var link = container.append("g").attr("class", "links").selectAll(".relationship").data(graph['links']).enter().append("g").attr("class", function (l) {
return "relationship " + l.name;
});
var linkLine = link
// .append("line");
.append("line").attr("marker-end", "url(#arrowHead)");
var linkText = link.append("text").text(function (l) {
// l == Object { source: "https://rubenvandeven.com/#codesandmodes", target: "_:b34", name: "https://schema.org/location" }
return getDisplayAttr(l.name);
});
var node = container.append("g").attr("class", "nodes").selectAll(".node").data(graph.nodes).enter().append("g").attr("class", function (d) {
var baseClasses = 'node ' + d['@type'];
if (d['@type']) {
var slashpos = d['@type'].lastIndexOf('/');
if (slashpos > -1) {
baseClasses += ' ' + d['@type'].substr(slashpos + 1);
}
}
return baseClasses;
});
var getViewbox = function getViewbox() {
return svg.attr("viewBox").split(" ").map(parseFloat);
};
var positionNodesInCenter = function positionNodesInCenter(idxs) {
setViewboxForceCenter(); // sets forceCx & forceCy
if ((typeof idxs === 'undefined' ? 'undefined' : _typeof(idxs)) == "object" && idxs !== null && idxs.length == 1) {
idxs = idxs[0];
}
nodePositions = {}; // reset
if (idxs === null) {
return;
} else if ((typeof idxs === 'undefined' ? 'undefined' : _typeof(idxs)) == "object") {
// array or object -> each
// calculate grid:
// let itemsX = 4;
// let itemsY = Math.ceil(idxs.length/itemsX);
// console.log(itemsX,itemsY);
// let rowDiffX = viewBox[3] * (1/(itemsX+1));
// let rowDiffY = viewBox[2] * (1/(itemsY+1));
// console.log(rowDiffX, rowDiffY);
// for (var i = 0; i < idxs.length; i++) {
// nodePositions[idxs[i]] = [
// cx - itemsX/2*rowDiffX + rowDiffX * ((i % itemsX)),
// cy - itemsY/2*rowDiffY + rowDiffY * (Math.floor(i / itemsX))
// ];
// }
positionNodesInCircle(idxs);
// console.log(nodePositions);
} else {
nodePositions[idxs] = [forceCx, forceCy];
// console.log("singleNode", idxs, nodePositions);
}
node.each(function (d, nIdx, nodeEls) {
if (typeof nodePositions[nIdx] != 'undefined') {
nodeEls[nIdx].classList.add('centeredNode');
nodeEls[nIdx].classList.add('visibleNode');
} else {
nodeEls[nIdx].classList.remove('centeredNode');
nodeEls[nIdx].classList.remove('visibleNode');
}
});
// restart animation (they call that 'alpha' in d3 force)
simulation.alpha(1);
simulation.restart();
};
var positionNodesInCircle = function positionNodesInCircle(idxs, r) {
var viewBox = getViewbox();
var zoom = getZoomValues();
setViewboxForceCenter(); // sets forceCx & forceCy
if (typeof r == 'undefined') {
if (idxs.length == 1) {
r = viewBox[2] / 6;
} else {
r = viewBox[2] / (4 + Math.max(0, 2.5 - idxs.length));
}
}
currentNodePositionRadius = r;
var forceCx = viewBox[0] + viewBox[2] / 2 - zoom['dx'];
var forceCy = viewBox[1] + viewBox[3] / 2 - zoom['dy'];
var stepSize = 2 * Math.PI / idxs.length;
for (var i = 0; i < idxs.length; i++) {
nodePositions[idxs[i]] = [forceCx + Math.sin(stepSize * i) * r, forceCy + Math.cos(stepSize * i) * r];
}
// restart animation (they call that 'alpha' in d3 force)
simulation.alpha(1);
simulation.restart();
};
var centerByType = function centerByType(types, updateHistory) {
if (typeof updateHistory == 'undefined') {
updateHistory = true;
}
if (!Array.isArray(types)) {
types = [types];
}
var idxs = [];
for (var idx in graph.nodes) {
if (types.indexOf(graph.nodes[idx]['@type']) > -1) {
idxs[idxs.length] = idx;
}
}
deselectNode();
if (updateHistory) {
// TODO: working
// console.log(types[0], getDisplayAttr(types[0]),types.map(getDisplayAttr));
2019-11-22 21:51:44 +01:00
history.pushState({ types: types }, "", "/@type/" + types.map(getDisplayAttr).join("+"));
} else {
history.replaceState({ types: types }, "", "/@type/" + types.map(getDisplayAttr).join("+"));
}
positionNodesInCenter(idxs.length ? idxs : null);
};
var selectedNodeTransition = d3.transition().duration(750).ease(d3.easeLinear);
var nodeDetailEl = document.getElementById("nodeDetails");
var createRelationshipEl = function createRelationshipEl(relNode, i) {
var el = document.createElement("dd");
el.classList.add('relLink');
var titleEl = document.createElement('a');
titleEl.innerHTML = getNodeLabel(relNode);
var year = getNodeYear(relNode);
if (year !== null) {
titleEl.innerHTML += '<span class=\'nodeYear\'>' + getNodeYear(relNode) + '</span>';
}
titleEl.classList.add('nodeTitle');
titleEl.classList.add('nodeTitleNr' + i);
titleEl.addEventListener('click', function (e) {
var idx = graph.nodes.indexOf(relNode);
selectNode(idx);
});
var typeEl = document.createElement('a');
typeEl.classList.add('nodeType');
typeEl.innerHTML = getDisplayAttr(relNode['@type']);
typeEl.title = relNode['@type'];
typeEl.addEventListener('click', function (e) {
centerByType(relNode['@type']);
});
el.appendChild(titleEl);
el.appendChild(typeEl);
return el;
};
var setDetails = function setDetails(nodeDatum, nodeIdx) {
document.body.classList.add("detailsOpen");
scrollToY(0, 4000);
while (nodeDetailEl.hasChildNodes()) {
nodeDetailEl.removeChild(nodeDetailEl.lastChild);
}
// TODO: replace relUp & relDown with linkMap
var relUp = [];
var relDown = [];
var pageTitles = [];
var nodeDetailScalerEl = document.createElement('div');
// nodeDetailScalerEl.innerHTML = `<div id='scalarbar'></div>`;
nodeDetailScalerEl.id = 'nodeDetailsScaler';
nodeDetailScalerEl.addEventListener('mousedown', function (e) {
// console.log('go');
var drag = function drag(e) {
// 5px for padding
nodeDetailEl.style.width = window.innerWidth - e.clientX + 5 + 'px';
};
document.body.addEventListener('mousemove', drag);
document.body.addEventListener('mouseup', function () {
document.body.removeEventListener('mousemove', drag);
});
});
nodeDetails.appendChild(nodeDetailScalerEl);
var breadcrumbsEl = document.createElement('ul');
breadcrumbsEl.classList.add('breadcrumbs');
var _loop2 = function _loop2(crumbNodeId) {
var crumbWrapEl = document.createElement('li');
var crumbEl = document.createElement('span');
crumbEl.classList.add('crumb');
crumbEl.addEventListener('click', function (e) {
var idx = graph.nodes.indexOf(nodeMap[crumbNodeId]);
selectNode(idx);
});
crumbEl.innerHTML = '' + getNodeLabel(nodeMap[crumbNodeId]);
var nodeYear = getNodeYear(nodeMap[crumbNodeId]);
if (nodeYear !== null) {
crumbEl.innerHTML += '<span class=\'nodeYear\'>' + nodeYear + '</span>';
}
crumbWrapEl.appendChild(crumbEl);
breadcrumbsEl.appendChild(crumbWrapEl);
pageTitles.push(getNodeLabel(nodeMap[crumbNodeId]));
};
var _iteratorNormalCompletion4 = true;
var _didIteratorError4 = false;
var _iteratorError4 = undefined;
try {
for (var _iterator4 = breadcrumbs[nodeDatum['@id']][Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
var crumbNodeId = _step4.value;
_loop2(crumbNodeId);
}
} catch (err) {
_didIteratorError4 = true;
_iteratorError4 = err;
} finally {
try {
if (!_iteratorNormalCompletion4 && _iterator4.return) {
_iterator4.return();
}
} finally {
if (_didIteratorError4) {
throw _iteratorError4;
}
}
}
nodeDetailEl.appendChild(breadcrumbsEl);
pageTitles.push(getNodeLabel(nodeDatum));
var titleAttr = getLabelAttribute(nodeDatum);
var titleEl = document.createElement('h2');
titleEl.innerHTML = getNodeLabel(nodeDatum);
var typeEl = document.createElement('span');
typeEl.classList.add('nodeType');
typeEl.innerHTML = getDisplayAttr(nodeDatum['@type']);
typeEl.title = nodeDatum['@type'];
typeEl.addEventListener('click', function (e) {
centerByType(nodeDatum['@type']);
});
titleEl.appendChild(typeEl);
nodeDetailEl.appendChild(titleEl);
var listEl = document.createElement("dl");
// listEl.innerHTML += `<dt>type</dt><dd>${nodeDatum['@type']}</dd>`;
var skipNodeAttributes = ['@id', 'x', 'y', 'index', '@type', 'vy', 'vx', 'fx', 'fy', 'leftX', 'rightX'];
if (titleAttr !== 'https://schema.org/contentUrl') {
skipNodeAttributes[skipNodeAttributes.length] = titleAttr;
}
for (var attr in nodeDatum) {
if (skipNodeAttributes.indexOf(attr) != -1) {
continue;
}
// approach all as array
var nodeAttr = Array.isArray(nodeDatum[attr]) ? nodeDatum[attr] : [nodeDatum[attr]];
for (var _i in nodeAttr) {
// check if relationship:
if (typeof nodeAttr[_i] === "string" && nodeMap[nodeAttr[_i]]) {
continue;
} else if (typeof nodeAttr[_i]['@id'] !== 'undefined') {
continue;
}
if (attr == 'https://schema.org/url' || attr == 'http://www.w3.org/2000/01/rdf-schema#seeAlso') {
listEl.innerHTML += '<dt class=\'dt-' + getDisplayAttr(attr) + '\' title=\'' + attr + '\'>' + getDisplayAttr(attr) + '</dt><dd class=\'dd-' + getDisplayAttr(attr) + '\'><a href=\'' + nodeAttr[_i] + '\'>' + nodeAttr[_i] + '</a></dd>';
} else if (attr == 'https://schema.org/embedUrl') {
listEl.innerHTML += '<dt class=\'dt-' + getDisplayAttr(attr) + '\' title=\'' + attr + '\'>' + getDisplayAttr(attr) + '</dt><dd class=\'dd-' + getDisplayAttr(attr) + '\'><a href=\'' + nodeAttr[_i] + '\'>' + nodeAttr[_i] + '</a></dd>';
listEl.innerHTML += '<dd class=\'dd-embed\'><embed src=\'' + nodeAttr[_i] + '\'></embed></dd>';
} else if (attr == 'https://schema.org/contentUrl') {
listEl.innerHTML += '<dt class=\'dt-' + getDisplayAttr(attr) + '\' title=\'' + attr + '\'>' + getDisplayAttr(attr) + '</dt><dd class=\'dd-' + getDisplayAttr(attr) + '\'><a href=\'' + nodeAttr[_i] + '\'>' + nodeAttr[_i] + '</a></dd>';
if (nodeDatum['@type'] == 'https://schema.org/VideoObject') {
var videoType = nodeAttr['https://schema.org/encodingFormat'] ? 'type=\'' + nodeAttr['https://schema.org/encodingFormat'] + '\'' : "";
var poster = nodeAttr['https://schema.org/thumbnailUrl'] ? 'poster=\'' + nodeAttr['https://schema.org/thumbnailUrl'] + '\'' : "";
listEl.innerHTML += '<dd class=\'dd-contentobject\'><video controls ' + poster + ' autoplay><source src=\'' + nodeAttr[_i] + '\' ' + videoType + '></video></dd>';
} else {
listEl.innerHTML += '<dd class=\'dd-contentobject\'><object data=\'' + nodeAttr[_i] + '\'></object></dd>';
}
} else {
var valueHtml = nodeAttr[_i].replace(/\n/g, "<br>");
listEl.innerHTML += '<dt class=\'dt-' + getDisplayAttr(attr) + '\' title=\'' + attr + '\'>' + getDisplayAttr(attr) + '</dt><dd class=\'dd-' + getDisplayAttr(attr) + '\'>' + valueHtml + '</dd>';
}
}
}
nodeDetailEl.appendChild(listEl);
// let relTitleEl = document.createElement("h4");
// relTitleEl.classList.add('linkTitle');
// relTitleEl.innerHTML = "links";
// nodeDetailEl.appendChild(relTitleEl);
var relsEl = document.createElement("dl");
// collect relationships
for (var i = 0; i < graph.links.length; i++) {
var _link = graph.links[i];
if (_link['source']['@id'] == nodeDatum['@id']) {
if (typeof relDown[_link['name']] == "undefined") {
relDown[_link['name']] = [];
}
relDown[_link['name']][relDown[_link['name']].length] = _link['target'];
}
if (_link['target']['@id'] == nodeDatum['@id']) {
if (typeof relUp[_link['name']] == "undefined") {
relUp[_link['name']] = [];
}
relUp[_link['name']][relUp[_link['name']].length] = _link['source'];
}
}
// relationships / links incomming <dl>
2019-11-22 21:51:44 +01:00
for (var _attr in relDown) {
var attrEl = document.createElement("dt");
attrEl.innerHTML = getDisplayAttr(_attr);
relsEl.appendChild(attrEl);
// highest pagerank first:
relDown[_attr].sort(function (a, b) {
return weights[b['@id']] - weights[a['@id']];
});
2019-11-22 21:51:44 +01:00
for (var _i2 in relDown[_attr]) {
var rel = relDown[_attr][_i2];
relsEl.appendChild(createRelationshipEl(rel));
if (typeof rel['https://schema.org/contentUrl'] != 'undefined') {
var ddEl = document.createElement('dd');
ddEl.classList.add('dd-contentobject');
if (rel['@type'] == 'https://schema.org/VideoObject') {
var _videoType = rel['https://schema.org/encodingFormat'] ? 'type=\'' + rel['https://schema.org/encodingFormat'] + '\'' : "";
var _poster = rel['https://schema.org/thumbnailUrl'] ? 'poster=\'' + rel['https://schema.org/thumbnailUrl'] + '\'' : "";
ddEl.innerHTML += '<video controls preload="none" ' + _poster + '><source src=\'' + rel['https://schema.org/contentUrl'] + '\' ' + _videoType + '></video>';
} else {
ddEl.innerHTML = '<object data=\'' + rel['https://schema.org/contentUrl'] + '\'></object>';
}
relsEl.appendChild(ddEl);
}
}
}
// relationships / links outgoing <dl>
2019-11-22 21:51:44 +01:00
for (var _attr2 in relUp) {
var _attrEl = document.createElement("dt");
_attrEl.innerHTML = getDisplayAttr(_attr2);
relsEl.appendChild(_attrEl);
// highest pagerank first:
relUp[_attr2].sort(function (a, b) {
return weights[b['@id']] - weights[a['@id']];
});
2019-11-22 21:51:44 +01:00
for (var _i3 in relUp[_attr2]) {
var _rel = relUp[_attr2][_i3];
relsEl.appendChild(createRelationshipEl(_rel, _i3));
if (typeof _rel['https://schema.org/contentUrl'] != 'undefined') {
var _ddEl = document.createElement('dd');
_ddEl.classList.add('dd-contentobject');
_ddEl.innerHTML = '<object data=\'' + _rel['https://schema.org/contentUrl'] + '\'></object>';
relsEl.appendChild(_ddEl);
}
}
}
nodeDetailEl.appendChild(relsEl);
node.each(function (d, nIdx, nodeEls) {
if (nIdx == nodeIdx) {
nodeEls[nIdx].classList.add('selectedNode');
} else {
nodeEls[nIdx].classList.remove('selectedNode');
}
});
// TODO: update history & title
document.title = pageTitles.join(" :: ");
};
var closeDetails = function closeDetails() {
document.body.classList.remove("detailsOpen");
scrollToY(0, 4000); // for mobile
};
/**
* Select a node, and center it + show details
* @param int idx The index of the node in the graph.nodes array
* @param Element|null nodeEl Optional, provide node element, so loop doesn't have to be used to change the Element
* @return void
*/
var selectNode = function selectNode(idx, updateHistory) {
if (typeof updateHistory == 'undefined') {
updateHistory = true;
}
var nodeEl = null;
var nodeDatum = null;
node.each(function (d, nIdx, nodeEls) {
if (nIdx == idx) {
nodeEl = nodeEls[idx];
nodeDatum = d;
}
});
if (!nodeEl) {
return;
}
if (true) {
// always set history state, but replace instead of update on 'updatehistory'
var id = null;
if (nodeDatum['@id'].startsWith( /*location.origin*/'https://rubenvandeven.com/')) {
id = nodeDatum['@id'].substr(26);
} else {
id = '?id=' + nodeDatum['@id'];
}
if (updateHistory) {
history.pushState({ node: idx }, getNodeLabel(nodeDatum), "/" + id);
} else {
history.replaceState({ node: idx }, getNodeLabel(nodeDatum), "/" + id);
}
}
// set global var
positionNodesInCenter(idx);
var currentCrumbs = breadcrumbs[nodeDatum['@id']].slice();
currentCrumbs[currentCrumbs.length] = nodeDatum['@id'];
// set active links.
var linkedIdxs = [];
link.each(function (d, idx, linkEls, q) {
// set nodes 'visible'/highlighted when linked to active node
if (d.source == nodeDatum || d.target == nodeDatum) {
linkEls[idx].classList.add('activeLink', 'visibleLink');
linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHeadSelected)");
node.filter(function (a, fnodeIdx) {
var r = a['@id'] == d.source['@id'] || a['@id'] == d.target['@id']; //connected node: true/false
if (r && linkedIdxs.indexOf(fnodeIdx) === -1) {
linkedIdxs[linkedIdxs.length] = fnodeIdx;
}
return r;
}).classed('visibleNode', true);
} else {
linkEls[idx].classList.remove('activeLink');
linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHead)");
}
// check if link is part of breadcrumb trail
var posSrc = currentCrumbs.indexOf(d.source['@id']);
var posTrg = currentCrumbs.indexOf(d.target['@id']);
if (posSrc > -1 && posTrg > -1 && Math.abs(posSrc - posTrg) == 1) {
linkEls[idx].classList.add('breadcrumbLink');
linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHeadCrumbTrail)");
} else {
linkEls[idx].classList.remove('breadcrumbLink');
}
});
var i = linkedIdxs.indexOf(idx);
if (i !== -1) {
linkedIdxs.splice(i, 1);
}
positionNodesInCircle(linkedIdxs);
setDetails(nodeDatum, idx);
};
var deselectNode = function deselectNode() {
positionNodesInCenter(null);
link.each(function (d, idx, linkEls, q) {
linkEls[idx].classList.remove('activeLink');
linkEls[idx].classList.remove('breadcrumbLink');
linkEls[idx].getElementsByTagName("line")[0].setAttribute("marker-end", "url(#arrowHead)");
});
closeDetails();
};
window.addEventListener('popstate', function (event) {
if (event.state.hasOwnProperty('node')) {
selectNode(event.state['node'], false);
} else {
// if not sure what to do, fall back to first node (also used to return to opening page)
var firstNode = graph['nodes'].find(function (n) {
return n['@id'] === firstNodeId;
});
selectNode(graph['nodes'].indexOf(firstNode), false);
}
});
var forceCx, forceCy;
var setViewboxForceCenter = function setViewboxForceCenter() {
var viewBox = getViewbox();
var zoom = getZoomValues();
forceCx = viewBox[0] + viewBox[2] / 2 - zoom['dx'];
forceCy = viewBox[1] + viewBox[3] / 2 - zoom['dy'];
};
var getZoomValues = function getZoomValues() {
var zoomContainer = document.getElementById("container");
var dx = 0,
dy = 0,
scale = 1;
if (zoomContainer.transform.baseVal.length > 0) {
var _iteratorNormalCompletion5 = true;
var _didIteratorError5 = false;
var _iteratorError5 = undefined;
try {
for (var _iterator5 = zoomContainer.transform.baseVal[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
var transform = _step5.value;
if (transform.type == SVGTransform.SVG_TRANSFORM_TRANSLATE) {
dx += transform.matrix.e;
dy += transform.matrix.f;
} else if (transform.type == SVGTransform.SVG_TRANSFORM_SCALE) {
scale *= transform.matrix.a; // assume simple scale
}
}
} catch (err) {
_didIteratorError5 = true;
_iteratorError5 = err;
} finally {
try {
if (!_iteratorNormalCompletion5 && _iterator5.return) {
_iterator5.return();
}
} finally {
if (_didIteratorError5) {
throw _iteratorError5;
}
}
}
}
return { 'dx': dx, 'dy': dy, 'scale': scale };
};
setViewboxForceCenter(); // sets forceCx & forceCy
var graphInitialised = false;
simulation.force('centerActive', function force(alpha) {
// let currentNode = node.selectAll('.detail');
// console.log(currentNode);
// console.log(forceCx, forceCy);
node.each(function (d, idx, nodes) {
var n = d;
var k = alpha * 0.1;
n.fx = null;
n.fy = null;
if (typeof nodePositions[idx] != 'undefined') {
if (graphInitialised == false) {
n.x = nodePositions[idx][0];
n.y = nodePositions[idx][1];
n.vx = 0;
n.vy = 0;
} else {
n.vx -= (n.x - nodePositions[idx][0]) * k * 5;
n.vy -= (n.y - nodePositions[idx][1]) * k * 5;
}
} else {
// if it's not positioned, move it out of the circle
if (currentNodePositionRadius < 1) {
return;
}
var dx = n.x - forceCx;
var dy = n.y - forceCy;
if (!inCircle(dx, dy, currentNodePositionRadius)) {
return;
}
if (graphInitialised == false) {
// on init, fixate items outside of circle
n.fx = n.x + dx * (2 + Math.random());
n.fy = n.y + dy * (2 + Math.random());
} else {
// if initialised, gradually move them outwards
n.vx += dx * k * 4;
n.vy += dy * k * 4;
}
}
});
});
//path to curve the tile
var nodePath = node.append("path").attr("id", function (d, idx) {
return "nodePath" + idx;
}).attr("d", function (d) {
var r = getSizeForNode(d) * 0.9;
var startX = getSizeForNode(d);
// M cx cy
// m -r, 0
// a r,r 0 1,0 (r * 2),0
// a r,r 0 1,0 -(r * 2),0
// return 'M' + nodeSize/2 + ' ' + nodeSize/2 + ' ' +
return 'M' + 0 + ' ' + 0 + ' ' + 'm -' + r + ', 0' + ' ' + 'a ' + r + ',' + r + ' 0 1,0 ' + r * 2 + ',0 ' + 'a ' + r + ',' + r + ' 0 1,0 -' + r * 2 + ',0';
// return 'm' + startX + ',' + nodeSize + ' ' +
// 'a' + r + ',' + r + ' 0 0 0 ' + (2*r) + ',0';
});
node.call(d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended)).on("click", function (d, idx, nodes) {
var node = nodes[idx];
selectNode(idx, node, d);
}).on('mouseover', function (n, nIdx) {
link.each(function (l, idx, linkEls, q) {
// set nodes 'visible'/highlighted when linked to active node
if (l.source == n || l.target == n) {
linkEls[idx].classList.add('hoverLink');
}
});
}).on('mouseout', function () {
var hoverLinkEls = document.getElementsByClassName('hoverLink');
while (hoverLinkEls.length > 0) {
hoverLinkEls[0].classList.remove('hoverLink');
}
});
// svg.call(d3.drag()
// .on("start", function(d){
// if(d3.event.sourceEvent.type == 'touchstart' && d3.event.sourceEvent.touches.length > 1) {
// } else {
// d3.event.sourceEvent.stopPropagation();
// svg.node().classList.add("dragging");
// }
// })
// .on("drag", function(){
// moveViewboxPx(d3.event.dx, d3.event.dy);
// })
// .on("end", function(){
// svg.node().classList.remove("dragging");
// }));
svg.call(d3.zoom().scaleExtent([0.3, 3]).on("start", function () {
svg.node().classList.add("dragging");
}).on("end", function () {
svg.node().classList.remove("dragging");
}).on("zoom", function (a, b, c) {
container.attr("transform", d3.event.transform);
}));
// svg.call(d3.zoom.transform, d3.zoomIdentity);
node.append('circle').attr("r", function (d) {
return getSizeForNode(d);
}).attr("class", "nodeBg");
node.append('circle').attr("r", function (d) {
return getSizeForNode(d) * 1.08;
}) // nodeSize + margin
.attr("class", "highlightCircle");
node.append('text').attr("class", "nodeType").text(function (n) {
return n['@type'];
});
node.append('text').attr("class", "nodeYear").attr("y", "22").text(function (n) {
return getNodeYear(n);
});
var splitText = function splitText(text) {
var characters = [" ", "-", '\xAD'];
var charSplitPos = {};
var mid = Math.floor(text.length / 2);
var splitPos = false;
var splitPosChar = false;
// split sentences
var _iteratorNormalCompletion6 = true;
var _didIteratorError6 = false;
var _iteratorError6 = undefined;
try {
for (var _iterator6 = characters[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
var char = _step6.value;
if (text.indexOf(char) < 0) {
continue;
}
var tmid = text.substr(0, mid).lastIndexOf(char);
if (tmid === -1) {
tmid = text.indexOf(char);
}
tmid += 1; // we want to cut _after_ the character
// console.log("Char", char, tmid);
if (splitPos === false || Math.abs(tmid - mid) < Math.abs(splitPos - mid)) {
// console.log("least!");
splitPos = tmid;
splitPosChar = char;
}
}
// console.log("pos",splitPos)
} catch (err) {
_didIteratorError6 = true;
_iteratorError6 = err;
} finally {
try {
if (!_iteratorNormalCompletion6 && _iterator6.return) {
_iterator6.return();
}
} finally {
if (_didIteratorError6) {
throw _iteratorError6;
}
}
}
if (splitPos === false) {
return false;
}
var text1 = text.substr(0, splitPos).trim();
var text2 = text.substr(splitPos).trim();
if (splitPosChar == '\xAD') {
text1 += "-";
}
// find most equal split
return [text1, text2];
};
var nodeTitle = node.append('text').attr("class", "nodeTitle").attr("y", "5");
nodeTitle
// .append("textPath")
// .attr( "xlink:href",function(d, idx){return '#nodePath'+idx;})
// .text(getNodeLabel)
.each(function (node, nodes) {
var textLength = void 0;
var self = d3.select(this);
var titleText = getNodeLabel(node);
var titleTexts = false;
if (titleText.length > 20) {
titleTexts = splitText(titleText);
}
if (titleTexts !== false) {
var tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "-10").attr("x", "0");
var tspan = self.append("tspan").text(titleTexts[1]).attr("y", "10").attr("x", "0");
var textLength1 = tspan.node().getComputedTextLength();
var textLength2 = tspan.node().getComputedTextLength();
textLength = Math.max(textLength1, textLength2);
} else {
self.text(titleText);
textLength = self.node().getComputedTextLength();
}
// scale according to text length:
if (textLength > getSizeForNode(node) * 2) {
self.attr('transform', 'scale(' + getSizeForNode(node) * 2 / textLength / 1.05 + ')');
}
});
node.each(function (d) {
if (!d['https://schema.org/thumbnailUrl']) {
return;
}
d3.select(this).append('svg:image').attr("xlink:href", d['https://schema.org/thumbnailUrl']).attr("width", function (d) {
return getSizeForNode(d) * 2;
}).attr("height", function (d) {
return getSizeForNode(d) * 2;
}).attr("transform", function (d) {
return "translate(-" + getSizeForNode(d) + " -" + getSizeForNode(d) + ")";
}).attr("clip-path", "url(#clipNodeImage)").attr("preserveAspectRatio", "xMidYMid slice");
});
simulation.nodes(graph.nodes).on("tick", ticked);
simulation.force("link").links(graph.links).distance(function (l) {
switch (l.name) {
// case 'publishedAt':
// return 200;
// case 'image':
// return 200;
default:
return 300;
}
}) // distance between the nodes / link length
// .charge(-100)
;
// run on each draw
function ticked() {
graph.nodes.forEach(function (d, idx) {
d.leftX = d.rightX = d.x;
// fix first node on center
// if(idx === 0) {
// d.fx = width/2;
// d.fy = height/2;
// return;
// }
});
linkLine.each(function (d) {
var sourceX, targetX, midX, dx, dy, angle;
// This mess makes the arrows exactly perfect.
// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
if (d.source.rightX < d.target.leftX) {
sourceX = d.source.rightX;
targetX = d.target.leftX;
} else if (d.target.rightX < d.source.leftX) {
targetX = d.target.rightX;
sourceX = d.source.leftX;
} else if (d.target.isCircle) {
targetX = sourceX = d.target.x;
} else if (d.source.isCircle) {
targetX = sourceX = d.source.x;
} else {
midX = (d.source.x + d.target.x) / 2;
if (midX > d.target.rightX) {
midX = d.target.rightX;
} else if (midX > d.source.rightX) {
midX = d.source.rightX;
} else if (midX < d.target.leftX) {
midX = d.target.leftX;
} else if (midX < d.source.leftX) {
midX = d.source.leftX;
}
targetX = sourceX = midX;
}
dx = targetX - sourceX;
dy = d.target.y - d.source.y;
angle = Math.atan2(dx, dy);
/* DISABLED
srcSize = (typeof nodePositions[d.source.index] != 'undefined') ? selectedNodeSize : nodeSize;
tgtSize = (typeof nodePositions[d.target.index] != 'undefined') ? selectedNodeSize : nodeSize;
*/
var srcSize = getSizeForNode(d.source) + 3.2;
var tgtSize = getSizeForNode(d.target) + 3.2;
// Compute the line endpoint such that the arrow
// is touching the edge of the node rectangle perfectly.
d.sourceX = sourceX + Math.sin(angle) * srcSize;
d.targetX = targetX - Math.sin(angle) * tgtSize;
d.sourceY = d.source.y + Math.cos(angle) * srcSize;
d.targetY = d.target.y - Math.cos(angle) * tgtSize;
}).attr("x1", function (d) {
return d.sourceX;
}).attr("y1", function (d) {
return d.sourceY;
}).attr("x2", function (d) {
return d.targetX;
}).attr("y2", function (d) {
return d.targetY;
});
linkText.attr("transform", function (d) {
var dx = (d.target.x - d.source.x) / 2;
var dy = (d.target.y - d.source.y) / 2;
var x = d.source.x + dx;
var y = d.source.y + dy;
var deg = Math.atan(dy / dx) * 180 / Math.PI;
// if dx/dy == 0/0 -> deg == NaN
if (isNaN(deg)) {
return "";
}
return "translate(" + x + " " + y + ") rotate(" + deg + ") translate(0, -10)";
});
node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
function dragstarted(d, idx, nodes) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
var nodeEl = nodes[idx];
d.fx = d.x;
d.fy = d.y;
// nodeEl.style.fill = '#00f';
nodeEl.classList.add('drag');
}
// use to validate drag
// function validate(x, a, b) {
// if (x =< a) return a;
// return b;
// }
function dragged(d, idx) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d, idx, nodes) {
if (!d3.event.active) simulation.alphaTarget(0);
var nodeEl = nodes[idx];
d.fx = null;
d.fy = null;
nodeEl.classList.remove('drag');
}
function moveViewboxPx(dx, dy) {
var viewBox = svg.attr("viewBox").split(" ").map(parseFloat);
viewBox[0] -= dx * 1;
viewBox[1] -= dy * 1;
svg.attr("viewBox", viewBox.join(" "));
}
// start by selecting the first node :-)
// selectNode(currentNodeIdx+1);
// positionNodesInCenter(currentNodeIdx);
if (location.pathname.startsWith('/@type/')) {
for (var t in types) {
if (getDisplayAttr(t) == location.pathname.substr(7)) {
centerByType(t, false);
}
}
} else {
var startNodeId = location.search.startsWith("?id=") ? location.search.substr(4) : 'https://rubenvandeven.com' + location.pathname;
var firstNode = graph['nodes'].find(function (n) {
return n['@id'] === startNodeId;
});
selectNode(graph['nodes'].indexOf(firstNode), false);
}
// closeDetails(); // hide details at first
// positionNodesInCenter(currentNodeIdx+1);
// setTimeout(function(){
// document.body.classList.add('graphInitialised');
// }, 10);
var initPlaceholder = document.getElementById('initPlaceholder');
svg.node().removeChild(initPlaceholder);
setTimeout(function () {
graphInitialised = true;
document.body.classList.add('graphInitialised');
}, 500);
}
// Detect request animation frame
var reqAnimFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame ||
// IE Fallback, you can even fallback to onscroll
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
// all credits go to https://stackoverflow.com/a/26798337
function scrollToY(scrollTargetY, speed, easing, finishFunction) {
// scrollTargetY: the target scrollY property of the window
// speed: time in pixels per second
// easing: easing equation to use
var scrollY = window.scrollY,
scrollTargetY = scrollTargetY || 0,
speed = speed || 2000,
easing = easing || 'easeOutSine',
currentTime = 0,
finishFunction = finishFunction || false;
// min time .1, max time .8 seconds
var time = Math.max(.1, Math.min(Math.abs(scrollY - scrollTargetY) / speed, .8));
// easing equations from https://github.com/danro/easing-js/blob/master/easing.js
var PI_D2 = Math.PI / 2,
easingEquations = {
easeOutSine: function easeOutSine(pos) {
return Math.sin(pos * (Math.PI / 2));
},
easeInOutSine: function easeInOutSine(pos) {
return -0.5 * (Math.cos(Math.PI * pos) - 1);
},
easeInOutQuint: function easeInOutQuint(pos) {
if ((pos /= 0.5) < 1) {
return 0.5 * Math.pow(pos, 5);
}
return 0.5 * (Math.pow(pos - 2, 5) + 2);
}
};
// add animation loop
function tick() {
currentTime += 1 / 60;
var p = currentTime / time;
var t = easingEquations[easing](p);
if (p < 1) {
reqAnimFrame(tick);
window.scrollTo(0, scrollY + (scrollTargetY - scrollY) * t);
} else {
window.scrollTo(0, scrollTargetY);
if (finishFunction) {
finishFunction();
}
}
}
// call it once to get started
tick();
}