no-svelte port

This commit is contained in:
zeno 2024-02-25 09:31:53 -05:00
parent ef483e62c3
commit 096dacd6fe
5 changed files with 538 additions and 0 deletions

48
app.html Normal file
View 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
View 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
View 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
View 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
View 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;
}