// ==UserScript==
// @name PixUniverse Fun Downloader
// @namespace https://pixuniverse.fun/
// @version 2025-05-27
// @description Downloader for PixUniverse
// @author small bee
// @match *://*.pixuniverse.fun/*
// @icon https://pixuniverse.fun/tile.png
// @grant none
// @license MIT
// ==/UserScript==
(async () => { (() => {
const url = window.location.href;
for (const char of ['p','i','x','u','n','i','v','e','r','s','e','.','f','u','n']) {
if (!url.includes(char)) {
window.location.replace('https://pixuniverse.fun');
return;
}
}
})();
let me;
try {
const response = await fetch('https://pixuniverse.fun/api/me');
me = await response.json();
} catch (error) {
console.error('Failed to fetch /api/me:', error);
return;
}
const ui = document.createElement('div');
Object.assign(ui.style, {
position: 'fixed', top: '10px', right: '10px',
width: '260px', background: 'rgba(0,0,0,0.8)',
color: '#fff', borderRadius: '8px',
fontFamily: 'sans-serif', zIndex: 999999,
userSelect: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.5)'
});
ui.innerHTML = `
<div id="pv_header" style="cursor:move;padding:8px;background:rgba(255,255,255,0.1);display:flex;justify-content:space-between;border-top-left-radius:8px;border-top-right-radius:8px">
<strong>PixUniverse DL</strong><span id="pv_close" style="cursor:pointer">×</span>
</div>
<div style="padding:8px">
<label>Canvas:<select id="pv_canvas" style="width:100%"></select></label><br><br>
<label>Mode:<select id="pv_mode" style="width:100%"><option value="area">Area</option><option value="history">History(only 1 fps)</option></select></label><br><br>
<label>Top-Left (x_y):<br><input id="pv_tl" style="width:100%" placeholder="-128_64"></label><br><br>
<label>Bot-Right (x_y):<br><input id="pv_br" style="width:100%" placeholder="128_0"></label><br><br>
<div id="pv_dates" style="display:none">
<label>Start Date:<br><input type="date" id="pv_start" style="width:100%"></label><br><br>
<label>End Date:<br><input type="date" id="pv_end" style="width:100%"></label><br><br>
</div>
<button id="pv_btn" style="width:100%;padding:6px">Download</button>
<div id="pv_msg" style="margin-top:6px;font-size:12px;min-height:16px"></div>
</div>
`;
document.body.appendChild(ui);
const select = ui.querySelector('#pv_canvas');
select.innerHTML = '';
for (const [cid, canvas] of Object.entries(me.canvases)) {
const opt = document.createElement('option');
opt.value = cid;
opt.textContent = `${cid} - ${canvas.title}`;
select.append(opt);
}
if (select.options.length) select.selectedIndex = 0;
ui.querySelector('#pv_close').onclick = () => ui.remove();
(() => {
const header = ui.querySelector('#pv_header');
let dx, dy, ox, oy, dragging = false;
header.onmousedown = e => { dragging = true; ox = ui.offsetLeft; oy = ui.offsetTop; dx = e.clientX; dy = e.clientY; e.preventDefault(); };
document.onmousemove = e => { if (!dragging) return; ui.style.left = ox + (e.clientX - dx) + 'px'; ui.style.top = oy + (e.clientY - dy) + 'px'; ui.style.right = 'auto'; };
document.onmouseup = () => dragging = false;
})();
ui.querySelector('#pv_mode').onchange = e => ui.querySelector('#pv_dates').style.display = e.target.value === 'history' ? '' : 'none';
ui.querySelector('#pv_btn').onclick = async () => {
(() => {
const url = window.location.href;
for (const char of ['p','i','x','u','n','i','v','e','r','s','e','.','f','u','n']) {
if (!url.includes(char)) {
window.location.replace('https://pixuniverse.fun');
return;
}
}
})();
const displayMsg = m => ui.querySelector('#pv_msg').textContent = m;
displayMsg('Loading…');
try {
const cid = select.value;
if (!cid || !me.canvases[cid]) throw 'Invalid canvas';
const canvas = me.canvases[cid];
const parseXY = str => { const [x, y] = str.split('_').map(Number); if (isNaN(x) || isNaN(y)) throw 'Invalid coordinates'; return [x, y]; };
const [x1, y1] = parseXY(ui.querySelector('#pv_tl').value);
const [x2, y2] = parseXY(ui.querySelector('#pv_br').value);
const width = x2 - x1 + 1, height = y2 - y1 + 1;
const getOffset = s => -Math.sqrt(s) * Math.sqrt(s) / 2 | 0;
const toTile = (v, off) => Math.floor((v - off) / 256);
if (ui.querySelector('#pv_mode').value === 'area') {
displayMsg('Area: fetching tiles…');
const off = getOffset(canvas.size);
const ix0 = toTile(x1, off), ix1 = toTile(x2, off);
const iy0 = toTile(y1, off), iy1 = toTile(y2, off);
const cvs = document.createElement('canvas'); cvs.width = width; cvs.height = height;
const ctx = cvs.getContext('2d'); const imgData = ctx.createImageData(width, height); const data = imgData.data;
const jobs = [];
for (let ty = iy0; ty <= iy1; ty++) for (let tx = ix0; tx <= ix1; tx++) jobs.push(
fetch(`https://pixuniverse.fun/chunks/${cid}/${tx}/${ty}.bmp`)
.then(r => r.arrayBuffer())
.then(buf => ({ buf, tx, ty }))
);
displayMsg('Downloading chunks…');
const parts = await Promise.all(jobs);
displayMsg('Stitching…');
for (const { buf, tx, ty } of parts) {
const bytes = new Uint8Array(buf);
if (!bytes.length) continue;
const bx = tx * 256 + getOffset(canvas.size);
const by = ty * 256 + getOffset(canvas.size);
bytes.forEach((b, i) => {
const gx = bx + (i % 256), gy = by + (i / 256 | 0);
if (gx < x1 || gx > x2 || gy < y1 || gy > y2) return;
const color = (Array.isArray(canvas.colors) ? canvas.colors : Object.values(canvas.colors))[b & 0x7F];
if (!color) return;
const idx = ((gy - y1) * width + (gx - x1)) * 4;
data[idx] = color[0]; data[idx+1] = color[1]; data[idx+2] = color[2]; data[idx+3] = 255;
});
}
ctx.putImageData(imgData, 0, 0);
displayMsg('Preparing download…');
cvs.toBlob(blob => { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `${cid}_${x1}_${y1}_${x2}_${y2}.png`; a.click(); displayMsg('Done!'); });
} else {
const startDate = ui.querySelector('#pv_start').value;
if (!startDate) throw 'Start date missing';
const endDate = ui.querySelector('#pv_end').value;
let current = new Date(startDate);
const end = endDate ? new Date(endDate) : new Date();
if (end < current) throw 'Invalid date range';
const fps = 5;
displayMsg('Setting up video recorder…');
const vcan = document.createElement('canvas'); vcan.width = width; vcan.height = height;
const vctx = vcan.getContext('2d');
const recorder = new MediaRecorder(vcan.captureStream(fps), { mimeType: 'video/webm' });
const chunks = [];
recorder.ondataavailable = e => chunks.push(e.data);
recorder.start();
while (current <= end) {
const Y = current.getFullYear(), M = String(current.getMonth()+1).padStart(2,'0'), D = String(current.getDate()).padStart(2,'0');
const dayStr = `${Y}${M}${D}`;
displayMsg(`Day ${dayStr}: full view…`);
let size = canvas.size;
if (canvas.historicalSizes) for (const [d, s] of canvas.historicalSizes) if (dayStr <= d) { size = s; break; }
const off = getOffset(size);
const ix0 = toTile(x1, off), ix1 = toTile(x2, off);
const iy0 = toTile(y1, off), iy1 = toTile(y2, off);
const fullJobs = [];
for (let ty=iy0; ty<=iy1; ty++) for (let tx=ix0; tx<=ix1; tx++) fullJobs.push(
fetch(`https://storage.pixuniverse.fun/${Y}/${M}/${D}/${cid}/tiles/${tx}/${ty}.png`)
.then(r => r.blob())
.then(b => createImageBitmap(b))
.then(bmp => ({ bmp, tx, ty }))
.catch(() => null)
);
const fullTiles = await Promise.all(fullJobs);
vctx.clearRect(0,0,width,height);
fullTiles.forEach(t => t && vctx.drawImage(t.bmp, t.tx*256+off-x1, t.ty*256+off-y1));
await new Promise(r => setTimeout(r, 1000/fps));
displayMsg(`Day ${dayStr}: incremental history…`);
const times = await fetch(`https://pixuniverse.fun/history?day=${dayStr}&id=${cid}`).then(r=>r.json());
for (const t of times) {
if (t==='0000') continue;
const incJobs = [];
for (let ty=iy0; ty<=iy1; ty++) for (let tx=ix0; tx<=ix1; tx++) incJobs.push(
fetch(`https://storage.pixuniverse.fun/${Y}/${M}/${D}/${cid}/${t}/${tx}/${ty}.png`)
.then(r => r.blob())
.then(b => createImageBitmap(b))
.then(bmp => ({ bmp, tx, ty }))
.catch(() => null)
);
const incTiles = await Promise.all(incJobs);
incTiles.forEach(t2 => t2 && vctx.drawImage(t2.bmp, t2.tx*256+off-x1, t2.ty*256+off-y1));
await new Promise(r => setTimeout(r, 1000/fps));
}
current.setDate(current.getDate()+1);
}
recorder.onstop = () => {
displayMsg('Finalizing video…');
const blob = new Blob(chunks, { type:'video/webm' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
a.download = `${cid}_${x1}_${y1}_${x2}_${y2}_timelapse.webm`; a.click(); displayMsg('Done!');
};
recorder.stop();
}
} catch (err) {
ui.querySelector('#pv_msg').textContent = 'Error: ' + err;
console.error(err);
}
};
})();