diff --git a/app.html b/app.html
new file mode 100644
index 0000000..d9b64ef
--- /dev/null
+++ b/app.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ WASD to pan or move tree (+ Q to speed up)
+ Source available on
GitHub
+
+
+
+ Input a project ID or URL
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/css/app.css b/css/app.css
new file mode 100644
index 0000000..9b68323
--- /dev/null
+++ b/css/app.css
@@ -0,0 +1,115 @@
+#area-main2 {
+ width: 100vw;
+ overflow-y: visible;
+ overflow-x: visible;
+ height: 1000px;
+ position: relative;
+}
+
+:root {
+ font-family: 'Open Sans', sans-serif;
+ color: var(--black);
+
+ --black: rgb(25,28,35);
+ --semiblack: rgb(105,108,115);
+ --semiwhite: rgb(215,218,225);
+ --white: rgb(255,255,255);
+}
+
+body {
+ overflow-x: scroll;
+ width: 100vw;
+ height: 100vh;
+
+ background: var(--white);
+ background-attachment: fixed;
+
+ margin: 0;
+ padding: 0;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ overflow: hidden;
+}
+
+#area-main a {
+ font-size: 24px;
+ width: 120px;
+ height: 90px;
+ border-radius: 15px;
+ overflow: hidden;
+ position: absolute;
+ background: var(--semiwhite);
+}
+
+#area-main img {
+ border-radius: 15px;
+}
+
+#area-main path {
+ z-index: -1;
+ stroke: var(--semiblack);
+ stroke-width: 10px;
+
+ overflow: visible;
+}
+
+#area-main div:before {
+ content: ' ';
+ display: block;
+ width: 20px;
+ height: 20px;
+ border-radius: 100%;
+ background: var(--semiblack);
+
+ margin-top: -8px;
+ margin-left: -8px;
+
+ filter: drop-shadow(0 0 15px var(--semiblack));
+}
+
+#ui-wrap {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ width: calc(100vw - 20px);
+ padding-top: 10px;
+
+ z-index: 1;
+}
+
+#ui-right {
+ text-align: right;
+}
+
+input, #area-main img {
+ border: solid var(--semiwhite) 2px;
+ width: 116px;
+ height: 86px;
+}
+
+
+input {
+ border-radius: 1rem;
+ height: 1rem;
+}
+
+#ui-wrap, input {
+ font-size: 16px;
+}
+
+text {
+ font-size: 5px;
+ transition: ease-in-out 0.3s all;
+}
+
+a:hover text {
+ font-size: 8px;
+}
+
+.red text {
+ fill: red;
+}
\ No newline at end of file
diff --git a/js/app.js b/js/app.js
new file mode 100644
index 0000000..d5cccb1
--- /dev/null
+++ b/js/app.js
@@ -0,0 +1,144 @@
+
+function genPos(id, fetchData) {
+ let queue = [id];
+
+ let queueHead;
+
+ let status = fetchData[id].visibility == 'visible'
+ && (fetchData[id].moderation_status == 'safe'
+ || fetchData[id].moderation_status == 'notreviewed'
+ || fetchData[id].moderation_status == 'notsafe');
+
+ let pos = [{
+ "id": id,
+ "name": fetchData[id].title,
+ "date": fetchData[id].datetime_shared,
+ "user": fetchData[id].username,
+ "visible": status,
+ "x": '0',
+ "y": 0,
+ "offbranch": 0,
+ "end": fetchData[id].children.length
+ }];
+
+ while (queue.length > 0) {
+ queueHead = queue[0];
+
+ let lastPosI = pos.findIndex(function (posEntry) {
+ return posEntry.id == queueHead;
+ });
+
+ let children = fetchData[queueHead].children;
+ queue.shift();
+ if (!children || children.length == 0) continue;
+
+ let lastPos;
+ if (lastPosI > -1) {
+ lastPos = pos[lastPosI];
+ } else {
+ continue;
+ }
+
+ children.forEach(function (child, i) {
+ queue.push(child);
+
+ let status = fetchData[child].visibility == 'visible'
+ && (fetchData[child].moderation_status == 'safe'
+ || fetchData[child].moderation_status == 'notreviewed'
+ || fetchData[child].moderation_status == 'notsafe');
+
+ console.log(fetchData[child].moderation_status);
+
+ pos.push({
+ "id": child,
+ "name": fetchData[child].title,
+ "date": fetchData[child].datetime_shared,
+ "user": fetchData[child].username,
+ "visible": status,
+ "x": lastPos.x + '|' + i,
+ "y": lastPos.y + 1,
+ "offbranch": 0,
+ "end": (fetchData[child].children) ? fetchData[child].children.length : 0
+ })
+
+ });
+ }
+
+ return pos;
+}
+
+function sortPos(a, b) {
+ let aSplit = [];
+ let bSplit = [];
+
+ a[0].split('|').forEach(function (x) {
+ aSplit.push(Number(x))
+ });
+ b[0].split('|').forEach(function (x) {
+ bSplit.push(Number(x))
+ });
+
+ let h = 0;
+ aSplit.forEach(function (x, i) {
+ if (h == 0 && x - bSplit[i] != 0) {
+ h = x - bSplit[i];
+ }
+ })
+
+ if (h == 0) {
+ return (aSplit.length > bSplit.length) ? 1 : -1;
+ }
+
+ return h;
+}
+
+
+async function main(fetchData) {
+ let rootId = fetchData['root_id'];
+
+ let pos = genPos(rootId, fetchData);
+
+ let posX = [];
+
+ pos.forEach(function (posEntry) {
+ posX.push([posEntry.x, posEntry.end]);
+ })
+
+ let posReduc = posX.reduce(function (a, b) {
+ if (a.indexOf(b[0]) < 0) a.push(b);
+ return a;
+ }, []);
+
+
+ posReduc.sort(sortPos);
+
+ let i = 0;
+ posReduc.forEach(function (posEntry, j) {
+ posReduc[j] = [posEntry[0], i, posEntry[1]];
+ let splitty = posEntry[0].split('|');
+ let lastSplitty = splitty.pop();
+
+ if (lastSplitty != '0') {
+ i++;
+ posReduc[j][1] = i;
+ }
+ });
+
+ pos.forEach(function (posEntry, j) {
+ let i = posReduc.findIndex(function (x) {
+ return x[0] == posEntry.x
+ });
+ if (i != -1) {
+ pos[j].x = posReduc[i][1];
+ let extended = posReduc[i][0] + '|' + (posReduc[i][2] - 1);
+ let indexB = posReduc.findIndex(function (x) {
+ return x[0] == extended
+ });
+ if (indexB > -1) {
+ pos[j].offbranch = (posReduc[indexB][1] - posReduc[i][1]);
+ }
+ }
+ })
+
+ return pos;
+}
\ No newline at end of file
diff --git a/js/motion.js b/js/motion.js
new file mode 100644
index 0000000..7dc752c
--- /dev/null
+++ b/js/motion.js
@@ -0,0 +1,92 @@
+
+let area2 = document.querySelector('#area-out');
+
+
+let pos = [];
+let mpos = [0, 0];
+let keys = [];
+let vel = [0, 0];
+let translate = '';
+let width = 0;
+let height = 0;
+let center = [0, 0];
+let lastPos = [0, 0];
+let isMouse = false;
+let zoom = 0.8;
+
+let id = '';
+
+let gen = (async () => {
+ pos = await genTree(id);
+});
+
+if (area) {
+ gen();
+}
+
+function down(e) {
+ keys[e.key.toLowerCase()] = true;
+};
+
+function up(e) {
+ keys[e.key.toLowerCase()] = false;
+};
+
+function mouseMove(e) {
+ lastPos = [e.clientX, e.clientY];
+ if (!isMouse) return;
+ mpos[0] += e.clientX - center[0];
+ mpos[1] += e.clientY - center[1];
+ center = lastPos;
+}
+
+function mouseUp(e) {
+ isMouse = false;
+}
+
+function mouseDown(e) {
+ if (e.button == '2') return;
+ isMouse = true;
+ center = [e.clientX, e.clientY];
+}
+
+function wheel(e) {
+ mpos[0] -= lastPos[0];
+ mpos[1] -= lastPos[1];
+
+ mpos[0] *= (1.005 ** -e.deltaY)
+ mpos[1] *= (1.005 ** -e.deltaY)
+
+ mpos[0] += lastPos[0];
+ mpos[1] += lastPos[1];
+
+ zoom *= (1.005 ** -e.deltaY)
+}
+
+function move() {
+ width = area2.clientWidth;
+ height = area.clientHeight;
+
+ var isShift = keys['q'];
+ vel[0] += ((keys['a'] ? 1 : 0) - (keys['d'] ? 1 : 0)) * (isShift ? 5 : 1);
+ vel[1] += ((keys['w'] ? 1 : 0) - (keys['s'] ? 1 : 0)) * (isShift ? 5 : 1);
+
+ vel[0] *= 0.9;
+ vel[1] *= 0.9;
+
+ mpos[0] += vel[0];
+ mpos[1] += vel[1];
+
+ translate = `translate(${mpos[0]}px,${mpos[1]}px) scale(${zoom},${zoom})`;
+
+ area.style.transform = translate;
+}
+
+setInterval(move, 10);
+
+window.addEventListener('keydown', down);
+window.addEventListener('keyup', up);
+window.addEventListener('wheel', wheel);
+window.addEventListener('mousemove', mouseMove);
+window.addEventListener('mousedown', mouseDown);
+window.addEventListener('mouseup', mouseUp);
\ No newline at end of file
diff --git a/js/tree.js b/js/tree.js
new file mode 100644
index 0000000..2a052cc
--- /dev/null
+++ b/js/tree.js
@@ -0,0 +1,139 @@
+let area = document.querySelector('#area-main');
+const treeWidth = 2;
+const treeHeight = 120;
+const magConst = 150;
+
+const itemWidth = 120;
+const itemHeight = 90;
+
+function genElement(type, area, content, attribs) {
+ var elem = document.createElementNS("http://www.w3.org/2000/svg",type);
+
+ var directAttribs = attribs.direct || {};
+ var styleAttribs = attribs.style || {};
+
+ var fullStyle = "";
+
+ Object.keys(styleAttribs).forEach(function (attrib) {
+ fullStyle += `${attrib}: ${styleAttribs[attrib]};`
+ });
+
+ directAttribs.style = fullStyle;
+
+ Object.keys(directAttribs).forEach(function (attrib) {
+ elem.setAttribute(attrib, directAttribs[attrib]);
+ });
+
+ elem.textContent = content;
+
+ area.appendChild(elem);
+
+ return elem;
+}
+
+function genEntry(posEntry, area, fetchData) {
+ var data = fetchData[posEntry.id] || {};
+
+ var x = posEntry.x * magConst;
+ var y = posEntry.y * treeHeight;
+
+ if (y > 0) {
+ genElement('path', area, '', {
+ 'direct': {
+ 'class': 'line',
+ 'd': `M ${x + (itemWidth / 2)} ${y - treeHeight + itemHeight / 2} v ${itemHeight} z`
+ }
+ });
+ }
+
+ if (posEntry.offbranch >= 0) {
+ genElement('path', area, '', {
+ 'direct': {
+ 'class': 'line',
+ 'd': `M ${x + (itemWidth / 2)} ${y + itemHeight / 2} h ${magConst * (posEntry.offbranch)} z`
+ }
+ });
+ }
+
+ let a = genElement('a', area, '', {
+ 'direct': {
+ 'href': `https://scratch.mit.edu/projects/${posEntry.id}`,
+ 'class': `${posEntry.visible ? '' : 'red'}`,
+ },
+ 'style': {
+ 'transform': `translate(${x}px, ${y}px)`
+ }
+ });
+
+ genElement('image', a, '', {
+ 'direct': {
+ 'href': `https://uploads.scratch.mit.edu/get_image/project/${posEntry.id}_1920x1080.png`,
+ 'x': '10',
+ 'y': '-15',
+ 'width': '100'
+ }
+ });
+
+ let text = [
+ `${posEntry.name}`,
+ `${posEntry.y} proj. deep`,
+ `${(posEntry.date) ? (new Date(posEntry.date.$date) + '').split('GMT')[0] : 'Date not available'}`,
+ `${posEntry.user}`
+ ];
+
+ for (let i in text) {
+
+ genElement('text', a, text[i], {
+ 'direct': {
+ 'x': '75',
+ 'y': 75 + 8 * i
+ }
+ });
+ }
+
+
+ /*
+ {#if Math.abs((mpos[0] + elem.x * zoom) - width/2) < width/2 * 1.2 && Math.abs((mpos[1] + elem.y * zoom) - height/2) < height/2 * 1.2 }
+
+ {:else}
+
+ {/if}
+ {elem.name}
+ {elem.y / treeHeight} proj. deep
+ {(elem.date) ? (new Date(elem.date.$date) + '').split('GMT')[0] : 'Date not available'}
+ {elem.user}
+
+ */
+}
+
+
+async function genTree(id) {
+ let params = new URLSearchParams(window.location.search);
+
+ let treeId = params.get("id").replace(/[^0-9]/g, '');
+ if (id) treeId = id;
+
+ let fetchData = await fetch(`https://scratch.mit.edu/projects/${treeId}/remixtree/bare`)
+ .then(x => x.json());
+
+ let keys = Object.keys(fetchData);
+
+ keys.forEach(id => {
+ if (id == fetchData.root_id || id == 'root_id') return;
+
+ let allChild = keys.map(x => fetchData[x].children).flat();
+
+ if (allChild.indexOf(id) == -1) {
+ fetchData[fetchData.root_id].children.push(id);
+ }
+
+ });
+
+ let pos = await main(fetchData);
+
+ pos.forEach(function (posEntry) {
+ genEntry(posEntry, area, fetchData);
+ })
+
+ return pos;
+}
\ No newline at end of file