Spaces:
Running
Running
File size: 4,338 Bytes
c9e39f3 76646ca c9e39f3 76646ca c9e39f3 76646ca c9e39f3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | /** Bounded download/decode queue, with at most two image insertions per frame. */
export function createImageLoader({ concurrency = 12, cacheSize = 128 } = {}) {
const queue = [], completed = [];
const active = new Set(), cache = new Map();
let disposed = false, frame = 0, scheduled = false;
function remember(image) {
if (!image?.naturalWidth || !image.src || disposed) return;
const key = image.src;
cache.delete(key);
cache.set(key, image);
if (cache.size > cacheSize) cache.delete(cache.keys().next().value);
}
function flush() {
frame = 0;
for (let i = 0; i < 4 && completed.length; i++) {
const job = completed.shift();
if (job.cancelled) remember(job.image);
else job.receive(job.image);
}
if (completed.length) frame = requestAnimationFrame(flush);
}
function deliver(job) {
completed.push(job);
if (!frame) frame = requestAnimationFrame(flush);
}
function pump() {
if (disposed) return;
queue.sort((a, b) => a.priority - b.priority);
while (active.size < concurrency && queue.length) {
const job = queue.shift();
if (job.cancelled) continue;
active.add(job);
const photo = job.image = new Image();
photo.decoding = 'async';
photo.fetchPriority = job.priority < 24 ? 'high' : 'low';
photo.src = job.url;
// Decoding completes before this element is attached to the animated field.
photo.decode().then(() => {
if (disposed) return;
if (job.cancelled) remember(photo);
else deliver(job);
}).catch(() => { /* Keep the BlurHash if an image is unavailable. */ })
.finally(() => { active.delete(job); pump(); });
}
}
return {
request(url, receive, priority = 1000) {
const job = { url, receive, priority, cancelled: false, image: null };
const cached = cache.get(url);
if (cached) { cache.delete(url); job.image = cached; deliver(job); }
else {
queue.push(job);
// Batch same-turn requests so sorting prioritizes the strongest matches.
if (!scheduled) { scheduled = true; queueMicrotask(() => { scheduled = false; pump(); }); }
}
return {
cancel() { job.cancelled = true; const i = queue.indexOf(job); if (i >= 0) queue.splice(i, 1); },
prioritize(value) { job.priority = value; },
};
},
release: remember,
dispose() {
disposed = true;
cancelAnimationFrame(frame);
for (const job of active) { job.cancelled = true; job.image.removeAttribute('src'); }
queue.length = completed.length = 0;
cache.clear();
},
};
}
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~';
export function averageColor(hash) {
if (!hash || hash.length < 6) return '#252823';
let color = 0;
for (const c of hash.slice(2, 6)) {
const n = alphabet.indexOf(c);
if (n < 0) return '#252823';
color = color * 83 + n;
}
return `#${(color & 0xffffff).toString(16).padStart(6, '0')}`;
}
export function thumbnailWidth(width, scale = 1, dpr = 1) {
return Math.min(512, Math.max(256, Math.ceil(width * Math.max(scale, 1.25) * Math.min(dpr, 2) / 128) * 128));
}
export function createPlaceholderLoader() {
const cache = new Map(), pending = new Map();
let worker;
try { worker = new Worker(new URL('./placeholder.worker.js', import.meta.url), { type: 'module' }); } catch { /* Average color remains usable. */ }
if (worker) {
worker.onmessage = ({ data: { hash, url } }) => {
cache.delete(hash); cache.set(hash, url);
if (cache.size > 512) cache.delete(cache.keys().next().value);
for (const receive of pending.get(hash) || []) receive(url);
pending.delete(hash);
};
worker.onerror = () => { worker.terminate(); worker = null; pending.clear(); };
}
return {
request(hash, receive) {
if (!hash || !worker) return () => {};
if (cache.has(hash)) { receive(cache.get(hash)); return () => {}; }
let listeners = pending.get(hash);
if (!listeners) { listeners = new Set(); pending.set(hash, listeners); worker.postMessage(hash); }
listeners.add(receive);
return () => listeners.delete(receive);
},
dispose() { worker?.terminate(); pending.clear(); cache.clear(); },
};
}
|