fad9760265
- 新增 Node 服务端(Hono + Hocuspocus + node:sqlite),数据保存在服务器 - 基于 Yjs 的多端实时同步,本机 IndexedDB 离线缓存,撤销只针对本机操作 - 空间(标签)有独立地址,增删改通过接口同步到所有设备 - 口令登录(签名 Cookie 会话、登录限流、同源校验) - 图片上传到服务器,过大照片上传前自动压缩 - 历史版本(自动/手动保存、一键恢复)、跨空间搜索、在线设备 - 随机油画加载动画(Painting Loaders,本地收录 p5 / p5.brush) - 选中样式与拖拽性能优化:拖动结束后才同步位置 - Docker / docker-compose 部署,README 与 CLAUDE.md 更新
73 lines
3.2 KiB
JavaScript
73 lines
3.2 KiB
JavaScript
/* =====================================================================
|
||
* loader.js — 无界笺的加载动画外壳。
|
||
*
|
||
* 绘画引擎与 12 幅画作来自 Painting Loaders
|
||
* (https://painterly.design-tools.workers.dev,作者 Sasha / x.com/aleksksaa),
|
||
* engine.js 与 style-*.js 原样保留。这里只负责:随机挑一幅(不与上次重复),
|
||
* 把画布铺满本页,并通知父页面已经开始作画。
|
||
*
|
||
* 本页通过 iframe 嵌入,p5 的全局模式不会污染主应用。
|
||
* ===================================================================== */
|
||
|
||
const PAINTINGS = [
|
||
{ key: 'garden', style: 'impressionismGarden', name: '花园', label: 'Garden' },
|
||
{ key: 'dawn', style: 'impressionismDawn', name: '晨曦', label: 'Dawn' },
|
||
{ key: 'willow', style: 'impressionismWillow', name: '垂柳', label: 'Willow' },
|
||
{ key: 'wisteria', style: 'impressionismWisteria', name: '紫藤', label: 'Wisteria' },
|
||
{ key: 'orchard', style: 'impressionismOrchard', name: '果园', label: 'Orchard' },
|
||
{ key: 'irises', style: 'impressionismIrises', name: '鸢尾', label: 'Irises' },
|
||
{ key: 'poppies', style: 'impressionismPoppies', name: '虞美人', label: 'Poppies' },
|
||
{ key: 'koi', style: 'impressionismKoi', name: '锦鲤', label: 'Koi' },
|
||
{ key: 'roses', style: 'impressionismRoses', name: '玫瑰', label: 'Roses' },
|
||
{ key: 'oranges', style: 'impressionismOranges', name: '橘子', label: 'Oranges' },
|
||
{ key: 'sail', style: 'impressionismSail', name: '远帆', label: 'Sail' },
|
||
{ key: 'dusk', style: 'abstractionDusk', name: '黄昏', label: 'Dusk' },
|
||
];
|
||
|
||
const LAST_KEY = 'boundless-last-painting';
|
||
|
||
function choosePainting() {
|
||
const wanted = new URLSearchParams(location.search).get('p');
|
||
const exact = PAINTINGS.find((item) => item.key === wanted);
|
||
if (exact) return exact;
|
||
|
||
let last = null;
|
||
try { last = localStorage.getItem(LAST_KEY); } catch (e) { /* 隐私模式 */ }
|
||
const pool = PAINTINGS.filter((item) => item.key !== last);
|
||
const picked = pool[Math.floor(Math.random() * pool.length)] || PAINTINGS[0];
|
||
try { localStorage.setItem(LAST_KEY, picked.key); } catch (e) { /* 隐私模式 */ }
|
||
return picked;
|
||
}
|
||
|
||
const painting = choosePainting();
|
||
const config = window.STYLES[painting.style];
|
||
document.documentElement.style.background = config.palette.ground;
|
||
|
||
function notifyParent(type) {
|
||
if (window.parent === window) return;
|
||
window.parent.postMessage(
|
||
{ source: 'boundless-painterly', type, key: painting.key, name: painting.name, label: painting.label, ground: config.palette.ground },
|
||
location.origin,
|
||
);
|
||
}
|
||
|
||
notifyParent('chosen');
|
||
|
||
let engine = null;
|
||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
|
||
// p5 全局模式:页面加载后若 window 上有 setup / draw 就会自动开始。
|
||
// 系统要求减少动态效果时不作画,只留下画纸底色。
|
||
if (!reduceMotion) {
|
||
window.setup = function () {
|
||
// 画布内部固定 600×600,再由 CSS 缩放;不跟随设备像素比可以省下不少显存
|
||
pixelDensity(1);
|
||
engine = createEngine(config, { mount: document.body });
|
||
engine.setup();
|
||
notifyParent('start');
|
||
};
|
||
window.draw = function () {
|
||
if (engine) engine.draw();
|
||
};
|
||
}
|