no-svelte port
This commit is contained in:
parent
ef483e62c3
commit
096dacd6fe
5 changed files with 538 additions and 0 deletions
48
app.html
Normal file
48
app.html
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<link rel="icon" href="/res/img/favi.svg">
|
||||||
|
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport content" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="description" content="NeoScratchTree" />
|
||||||
|
|
||||||
|
<meta property="og:title" content="NeoScratchTree" />
|
||||||
|
<meta property="og:description" content="A better remix tree viewer for Scratch" />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id='ui-wrap'>
|
||||||
|
<div>
|
||||||
|
WASD to pan or move tree (+ Q to speed up) <br/>
|
||||||
|
Source available on <a href='https://github.com/malloc62/NeoScratchTree'>GitHub</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id='ui-right'>
|
||||||
|
Input a project ID or URL
|
||||||
|
<form method="GET">
|
||||||
|
<input type='text' name='id'>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id='area-out'>
|
||||||
|
<svg id='area-main2'>
|
||||||
|
<g id='area-main'>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src='js/app.js'></script>
|
||||||
|
<script src='js/tree.js'></script>
|
||||||
|
<script src='js/motion.js'></script>
|
||||||
|
</body>
|
115
css/app.css
Normal file
115
css/app.css
Normal file
|
@ -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;
|
||||||
|
}
|
144
js/app.js
Normal file
144
js/app.js
Normal file
|
@ -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;
|
||||||
|
}
|
92
js/motion.js
Normal file
92
js/motion.js
Normal file
|
@ -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);
|
139
js/tree.js
Normal file
139
js/tree.js
Normal file
|
@ -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 }
|
||||||
|
<image href='https://uploads.scratch.mit.edu/get_image/project/{elem.id}_1920x1080.png' x='10' y='-15' width='100' />
|
||||||
|
{:else}
|
||||||
|
<circle r="25" cx="60" cy="40"></circle>
|
||||||
|
{/if}
|
||||||
|
<text x='75' y='75'>{elem.name}</text>
|
||||||
|
<text x='75' y='83'>{elem.y / treeHeight} proj. deep</text>
|
||||||
|
<text x='75' y='91'>{(elem.date) ? (new Date(elem.date.$date) + '').split('GMT')[0] : 'Date not available'}</text>
|
||||||
|
<text x='75' y='99'>{elem.user}</text>
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
Loading…
Reference in a new issue