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