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