Compare commits

...

4 Commits

Author SHA1 Message Date
Yoone 44a2ee66f0 chore: 删除不再使用的雪景特效相关资源文件 2025-12-14 23:32:31 +08:00
Yoone b07307b365 chore 2025-12-14 23:31:18 +08:00
Yoone 44431c6780 refactor: 优化.gitignore并调整SnowAnimator初始化顺序
更新.gitignore文件,简化忽略目录配置
将SnowAnimator中的system初始化移至resize函数之前,避免重复计算
添加lib/index.html作为预览页面
2025-12-14 23:30:35 +08:00
Yoone 73288f1d19 refactor: migrate to lib/dist outputs; add index.js export; update plugin to load dist; implement admin settings page and i18n; fix media weights keys; add watch script 2025-12-14 16:41:52 +08:00
49 changed files with 2075 additions and 1900 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
.DS_Store
release
node_modules
dist

View File

@ -1,15 +0,0 @@
(function(){
// 注册圣诞拐杖形状渲染函数 使用 SVG 图像参考
window.YooneSnowShapeRenderers = window.YooneSnowShapeRenderers || {};
window.YooneSnowShapeRenderers.candy_cane = function(context, positionX, positionY, baseSize){
// 从设置中获取资源 URL 并加载图像 使用缓存避免重复加载
const assets = (window.YooneSnowSettings && window.YooneSnowSettings.assetsMap) ? window.YooneSnowSettings.assetsMap : {};
const url = assets['candy_cane'];
const record = window.YooneSnowGetOrLoadImage ? window.YooneSnowGetOrLoadImage(url) : { img: null, ready: false };
// 条件判断 如果图像尚未准备则跳过本次绘制
if (!record || !record.ready){ return; }
const targetHeight = baseSize * 9; // 目标高度基于基础尺寸缩放
const targetWidth = targetHeight * 0.6; // 拐杖较瘦 调整宽高比
window.YooneSnowDrawCenteredImage(context, record.img, positionX, positionY, targetWidth, targetHeight);
};
})();

View File

@ -1,15 +0,0 @@
(function(){
// 注册圣诞果形状渲染函数 使用 SVG 图像参考
window.YooneSnowShapeRenderers = window.YooneSnowShapeRenderers || {};
window.YooneSnowShapeRenderers.christmas_berry = function(context, positionX, positionY, baseSize){
// 从设置中获取资源 URL 并加载图像 使用缓存避免重复加载
const assets = (window.YooneSnowSettings && window.YooneSnowSettings.assetsMap) ? window.YooneSnowSettings.assetsMap : {};
const url = assets['christmas_berry'];
const record = window.YooneSnowGetOrLoadImage ? window.YooneSnowGetOrLoadImage(url) : { img: null, ready: false };
// 条件判断 如果图像尚未准备则跳过本次绘制
if (!record || !record.ready){ return; }
const targetHeight = baseSize * 6.5; // 圣诞果较小 调低高度
const targetWidth = targetHeight; // 使用方形比例
window.YooneSnowDrawCenteredImage(context, record.img, positionX, positionY, targetWidth, targetHeight);
};
})();

View File

@ -1,15 +0,0 @@
(function(){
// 注册圣诞袜形状渲染函数 使用 SVG 图像参考
window.YooneSnowShapeRenderers = window.YooneSnowShapeRenderers || {};
window.YooneSnowShapeRenderers.christmas_sock = function(context, positionX, positionY, baseSize){
// 从设置中获取资源 URL 并加载图像 使用缓存避免重复加载
const assets = (window.YooneSnowSettings && window.YooneSnowSettings.assetsMap) ? window.YooneSnowSettings.assetsMap : {};
const url = assets['christmas_sock'];
const record = window.YooneSnowGetOrLoadImage ? window.YooneSnowGetOrLoadImage(url) : { img: null, ready: false };
// 条件判断 如果图像尚未准备则跳过本次绘制
if (!record || !record.ready){ return; }
const targetHeight = baseSize * 8; // 目标高度基于基础尺寸缩放
const targetWidth = targetHeight * 0.8; // 袜子稍宽 调整宽高比
window.YooneSnowDrawCenteredImage(context, record.img, positionX, positionY, targetWidth, targetHeight);
};
})();

View File

@ -1,15 +0,0 @@
(function(){
// 注册圣诞树形状渲染函数 使用 SVG 图像参考
window.YooneSnowShapeRenderers = window.YooneSnowShapeRenderers || {};
window.YooneSnowShapeRenderers.christmas_tree = function(context, positionX, positionY, baseSize){
// 从设置中获取资源 URL 并加载图像 使用缓存避免重复加载
const assets = (window.YooneSnowSettings && window.YooneSnowSettings.assetsMap) ? window.YooneSnowSettings.assetsMap : {};
const url = assets['christmas_tree'];
const record = window.YooneSnowGetOrLoadImage ? window.YooneSnowGetOrLoadImage(url) : { img: null, ready: false };
// 条件判断 如果图像尚未准备则跳过本次绘制
if (!record || !record.ready){ return; }
const targetHeight = baseSize * 9; // 圣诞树较高 使用更大高度
const targetWidth = targetHeight * 0.8; // 按比例缩放保证不太宽
window.YooneSnowDrawCenteredImage(context, record.img, positionX, positionY, targetWidth, targetHeight);
};
})();

View File

@ -1,24 +0,0 @@
(function(){
// 注册铜钱形状渲染函数 外圆加中间正方形孔
window.YooneSnowShapeRenderers = window.YooneSnowShapeRenderers || {};
window.YooneSnowShapeRenderers.coin = function(context, positionX, positionY, baseSize){
// 函数用于绘制铜钱形状 使用合成模式扣出孔
const outerRadius = baseSize * 3; // 将基础尺寸放大为外圆半径
context.save();
context.beginPath();
context.fillStyle = '#FFD700';
context.arc(positionX, positionY, outerRadius, 0, Math.PI * 2);
context.fill();
const holeSize = outerRadius * 0.6; // 正方形孔边长基于外圆半径
context.globalCompositeOperation = 'destination-out';
context.beginPath();
context.rect(
positionX - holeSize / 2,
positionY - holeSize / 2,
holeSize,
holeSize
);
context.fill();
context.restore();
};
})();

View File

@ -1,12 +0,0 @@
(function(){
// 注册 dot 形状渲染函数 使用填充圆形作为雪花
window.YooneSnowShapeRenderers = window.YooneSnowShapeRenderers || {};
window.YooneSnowShapeRenderers.dot = function(context, positionX, positionY, baseSize){
// 函数用于在指定位置绘制圆点形状 雪花颜色为白色
const finalRadius = baseSize; // 使用基础半径作为圆点大小
context.beginPath();
context.arc(positionX, positionY, finalRadius, 0, Math.PI * 2);
context.fillStyle = 'rgba(255,255,255,0.9)';
context.fill();
};
})();

View File

@ -1,35 +0,0 @@
(function(){
// 注册雪花形状渲染函数 恢复为原始六角雪花绘制样式
window.YooneSnowShapeRenderers = window.YooneSnowShapeRenderers || {};
window.YooneSnowShapeRenderers.flake = function(context, positionX, positionY, baseSize){
// 将基础尺寸转换为原始函数中的 size 参数 以保持形状大小一致
const branchSize = baseSize * 3;
context.save();
context.translate(positionX, positionY);
context.fillStyle = 'rgba(255,255,255,0.9)';
context.strokeStyle = 'rgba(255,255,255,0.9)';
context.lineWidth = branchSize * 0.15;
// 循环绘制六个分支 每次旋转 60 度
for (let branchIndex = 0; branchIndex < 6; branchIndex++) {
context.rotate(Math.PI / 3);
context.beginPath();
context.moveTo(0, 0);
context.lineTo(0, branchSize);
context.stroke();
// 在主分支上绘制三个小分叉 保持与原始实现一致
context.beginPath();
context.moveTo(0, branchSize * 0.3);
context.lineTo(branchSize * 0.3, branchSize * 0.5);
context.stroke();
context.beginPath();
context.moveTo(0, branchSize * 0.5);
context.lineTo(-branchSize * 0.3, branchSize * 0.7);
context.stroke();
context.beginPath();
context.moveTo(0, branchSize * 0.7);
context.lineTo(branchSize * 0.3, branchSize * 0.9);
context.stroke();
}
context.restore();
};
})();

View File

@ -1,7 +0,0 @@
(function(){
// 初始化全局渲染注册表 用于统一管理形状渲染函数
if (!window.YooneSnowShapeRenderers) {
// 条件判断 如果不存在则创建空对象
window.YooneSnowShapeRenderers = {};
}
})();

View File

@ -1,15 +0,0 @@
(function(){
// 注册麋鹿形状渲染函数 使用 SVG 图像参考
window.YooneSnowShapeRenderers = window.YooneSnowShapeRenderers || {};
window.YooneSnowShapeRenderers.reindeer = function(context, positionX, positionY, baseSize){
// 从设置中获取资源 URL 并加载图像 使用缓存避免重复加载
const assets = (window.YooneSnowSettings && window.YooneSnowSettings.assetsMap) ? window.YooneSnowSettings.assetsMap : {};
const url = assets['reindeer'];
const record = window.YooneSnowGetOrLoadImage ? window.YooneSnowGetOrLoadImage(url) : { img: null, ready: false };
// 条件判断 如果图像尚未准备则跳过本次绘制
if (!record || !record.ready){ return; }
const targetHeight = baseSize * 8.5; // 麋鹿高度设置略大
const targetWidth = targetHeight; // 使用接近方形的比例
window.YooneSnowDrawCenteredImage(context, record.img, positionX, positionY, targetWidth, targetHeight);
};
})();

View File

@ -1,15 +0,0 @@
(function(){
// 注册圣诞帽形状渲染函数 使用 SVG 图像参考
window.YooneSnowShapeRenderers = window.YooneSnowShapeRenderers || {};
window.YooneSnowShapeRenderers.santa_hat = function(context, positionX, positionY, baseSize){
// 从设置中获取资源 URL 并加载图像 使用缓存避免重复加载
const assets = (window.YooneSnowSettings && window.YooneSnowSettings.assetsMap) ? window.YooneSnowSettings.assetsMap : {};
const url = assets['santa_hat'];
const record = window.YooneSnowGetOrLoadImage ? window.YooneSnowGetOrLoadImage(url) : { img: null, ready: false };
// 条件判断 如果图像尚未准备则跳过本次绘制
if (!record || !record.ready){ return; }
const targetHeight = baseSize * 8; // 目标高度基于基础尺寸缩放
const targetWidth = targetHeight; // 按方形比例绘制 保持居中
window.YooneSnowDrawCenteredImage(context, record.img, positionX, positionY, targetWidth, targetHeight);
};
})();

View File

@ -1,80 +0,0 @@
(function(){
// 全局图像缓存对象 存放已加载的图像资源
window.YooneSnowImageCache = window.YooneSnowImageCache || {};
// 获取或加载图像 根据 URL 返回图像对象和加载状态
window.YooneSnowGetOrLoadImage = function(imageUrl){
// 条件判断 如果未提供 URL 则返回空
if (!imageUrl || typeof imageUrl !== 'string'){
return { img: null, ready: false };
}
const existing = window.YooneSnowImageCache[imageUrl];
// 条件判断 如果已存在缓存则直接返回
if (existing && existing.ready){
return existing;
}
if (existing && !existing.ready){
// 条件判断 如果正在加载则返回当前状态
return existing;
}
// 创建新的图像对象 并开始加载
const img = new Image();
const record = { img: img, ready: false };
window.YooneSnowImageCache[imageUrl] = record;
try { img.decoding = 'async'; } catch(e) {}
try { img.fetchPriority = 'low'; } catch(e) {}
img.onload = function(){ record.ready = true; };
img.onerror = function(){
// 加载失败 从缓存移除避免重复错误
delete window.YooneSnowImageCache[imageUrl];
};
img.src = imageUrl;
return record;
};
window.YooneSnowLoadAssetViaFetch = function(imageUrl, onReady){
if (!imageUrl || typeof imageUrl !== 'string'){
if (typeof onReady === 'function'){ onReady(false); }
return;
}
var existing = window.YooneSnowImageCache[imageUrl];
if (existing && existing.ready){
if (typeof onReady === 'function'){ onReady(true); }
return;
}
if (!existing){ window.YooneSnowImageCache[imageUrl] = { img: null, ready: false }; }
if (typeof fetch === 'function' && typeof createImageBitmap === 'function'){
fetch(imageUrl, { cache: 'force-cache' }).then(function(resp){ return resp.blob(); }).then(function(blob){
return createImageBitmap(blob);
}).then(function(bmp){
window.YooneSnowImageCache[imageUrl] = { img: bmp, ready: true };
if (typeof onReady === 'function'){ onReady(true); }
}).catch(function(){
var rec = window.YooneSnowGetOrLoadImage(imageUrl);
var fired = false;
if (rec && rec.img){
var markReady = function(){ rec.ready = true; };
rec.img.onload = function(){ if (!fired){ fired = true; markReady(); if (typeof onReady === 'function'){ onReady(true); } } };
rec.img.onerror = function(){ if (!fired){ fired = true; if (typeof onReady === 'function'){ onReady(false); } } };
}
});
} else {
var rec2 = window.YooneSnowGetOrLoadImage(imageUrl);
var fired2 = false;
if (rec2 && rec2.img){
var markReady2 = function(){ rec2.ready = true; };
rec2.img.onload = function(){ if (!fired2){ fired2 = true; markReady2(); if (typeof onReady === 'function'){ onReady(true); } } };
rec2.img.onerror = function(){ if (!fired2){ fired2 = true; if (typeof onReady === 'function'){ onReady(false); } } };
}
}
};
// 居中绘制图像 根据目标中心点和宽高进行缩放绘制
window.YooneSnowDrawCenteredImage = function(context, img, centerX, centerY, width, height){
// 条件判断 如果图像不存在则不绘制
if (!img) { return; }
const drawX = centerX - width / 2;
const drawY = centerY - height / 2;
context.drawImage(img, drawX, drawY, width, height);
};
})();

View File

@ -1,56 +0,0 @@
(function(){
// 注册元宝形状渲染函数 使用 SVG 路径或贝塞尔曲线近似
window.YooneSnowShapeRenderers = window.YooneSnowShapeRenderers || {};
const outerPath = (typeof Path2D !== 'undefined') ? new Path2D('M947.4 326.8l0.4-0.1H804.2C814 346 819 364.4 819 381.5c0 18.6-6 46.5-34.3 73.8-17.4 16.7-41.1 31.2-70.5 43.2-54.7 22.1-126.5 34.4-202.3 34.4s-147.6-12.2-202.3-34.4c-29.4-12-53.1-26.5-70.5-43.2-28.3-27.2-34.3-55.2-34.3-73.8 0-17.2 4.9-35.5 14.4-54.8h-150c-29.3 0-53.1 27.1-53.1 60.6C46 634.4 256.5 826 511.7 826c258.1 0 470.3-195.7 496.4-447-0.7-29-27.6-52.2-60.7-52.2z') : null;
const topPath = (typeof Path2D !== 'undefined') ? new Path2D('M512 488.4c144.7 0 262.7-47.4 262.7-107.1 0-57.7-118-183.2-262.7-183.2S249.3 321.7 249.3 381.4c0 59.7 118 107 262.7 107z') : null;
window.YooneSnowShapeRenderers.yuanbao = function(context, positionX, positionY, baseSize){
// 函数用于绘制元宝形状 根据基础尺寸进行缩放
if (outerPath && topPath){
// 条件判断 如果支持 Path2D 则使用 SVG 路径绘制
context.save();
context.translate(positionX, positionY);
const scaleFactor = (baseSize * 4.2) / 512;
context.scale(scaleFactor, scaleFactor);
context.translate(-512, -512);
context.fillStyle = '#FFC003';
context.fill(outerPath);
context.fill(topPath);
context.restore();
return;
}
// 回退方案 使用贝塞尔曲线近似绘制元宝
context.save();
context.translate(positionX, positionY);
context.fillStyle = '#FFC003';
const totalWidth = baseSize * 2.6 * 1.4; // 调整比例保证大小一致
const halfWidth = totalWidth / 2;
const upperHeight = baseSize * 1.1 * 1.4;
const lowerHeight = baseSize * 0.85 * 1.4;
const rimLift = upperHeight * 0.25;
context.beginPath();
context.moveTo(-halfWidth, -rimLift);
context.bezierCurveTo(
-halfWidth * 0.65, -upperHeight * 1.0,
-halfWidth * 0.25, -upperHeight * 1.25,
0, -upperHeight
);
context.bezierCurveTo(
halfWidth * 0.25, -upperHeight * 1.25,
halfWidth * 0.65, -upperHeight * 1.0,
halfWidth, -rimLift
);
context.bezierCurveTo(
halfWidth * 0.65, lowerHeight * 0.95,
halfWidth * 0.25, lowerHeight * 1.1,
0, lowerHeight
);
context.bezierCurveTo(
-halfWidth * 0.25, lowerHeight * 1.1,
-halfWidth * 0.65, lowerHeight * 0.95,
-halfWidth, -rimLift
);
context.closePath();
context.fill();
context.restore();
};
})();

View File

@ -1,625 +0,0 @@
(function(){
// 初始化函数 用于启动雪花效果
function init(){
const canvas = document.getElementById('effectiveAppsSnow');
// 条件判断 如果未找到画布元素则不执行
if (!canvas) return;
const prefersReducedMotion = (typeof window.matchMedia === 'function') && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) { canvas.style.display = 'none'; return; }
const context = canvas.getContext('2d');
let viewportWidth = window.innerWidth;
let viewportHeight = window.innerHeight;
const devicePixelRatio = window.devicePixelRatio || 1;
// 读取首页显示时长设置 单位为秒 0 表示无限
const displayDurationSeconds = (window.YooneSnowSettings && typeof window.YooneSnowSettings.displayDurationSeconds !== 'undefined')
? Math.max(0, parseInt(window.YooneSnowSettings.displayDurationSeconds, 10) || 0)
: 0;
// 记录启动时间 用于计算已运行时间
const startTimestamp = performance.now();
// 标记是否已达到显示时长 到达后不再生成新粒子并不再重生
let hasReachedDuration = false;
const selectedShapes = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.selectedShapes) && window.YooneSnowSettings.selectedShapes.length > 0)
? window.YooneSnowSettings.selectedShapes
: [];
const mediaItems = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.mediaItems))
? window.YooneSnowSettings.mediaItems
: [];
const emojiItems = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.emojiItems))
? window.YooneSnowSettings.emojiItems
: [];
const textItems = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.textItems))
? window.YooneSnowSettings.textItems
: [];
const defaultShapeWeights = { dot: 1, flake: 4, yuanbao: 1, coin: 1, santa_hat: 1, candy_cane: 1, christmas_sock: 1, christmas_tree: 1, reindeer: 1, christmas_berry: 1 };
const shapeWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.shapeWeights && typeof window.YooneSnowSettings.shapeWeights === 'object')
? window.YooneSnowSettings.shapeWeights
: {};
const mediaWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.mediaWeights && typeof window.YooneSnowSettings.mediaWeights === 'object')
? window.YooneSnowSettings.mediaWeights
: {};
const emojiWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.emojiWeights && typeof window.YooneSnowSettings.emojiWeights === 'object')
? window.YooneSnowSettings.emojiWeights
: {};
const textWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.textWeights && typeof window.YooneSnowSettings.textWeights === 'object')
? window.YooneSnowSettings.textWeights
: {};
const shapeWeights = {};
for (let key in defaultShapeWeights){
const val = typeof shapeWeightsRaw[key] !== 'undefined' ? parseInt(shapeWeightsRaw[key], 10) : defaultShapeWeights[key];
shapeWeights[key] = isNaN(val) ? defaultShapeWeights[key] : Math.max(0, val);
}
const assetsMap = (window.YooneSnowSettings && window.YooneSnowSettings.assetsMap && typeof window.YooneSnowSettings.assetsMap === 'object')
? window.YooneSnowSettings.assetsMap
: {};
const assetShapeKeys = Object.keys(assetsMap || {});
window.YooneSnowAssetsReady = window.YooneSnowAssetsReady || false;
window.YooneSnowAssetQueue = window.YooneSnowAssetQueue || [];
window.YooneSnowAssetQueueRunning = window.YooneSnowAssetQueueRunning || false;
function enqueueAsset(u){
if (!u || typeof u !== 'string'){ return; }
for (var i = 0; i < window.YooneSnowAssetQueue.length; i++){ if (window.YooneSnowAssetQueue[i] === u) return; }
window.YooneSnowAssetQueue.push(u);
}
function runAssetQueue(){
if (window.YooneSnowAssetQueueRunning){ return; }
window.YooneSnowAssetQueueRunning = true;
function next(){
if (window.YooneSnowAssetQueue.length === 0){ window.YooneSnowAssetQueueRunning = false; window.YooneSnowAssetsReady = true; return; }
var u = window.YooneSnowAssetQueue.shift();
if (typeof window.YooneSnowLoadAssetViaFetch === 'function'){
window.YooneSnowLoadAssetViaFetch(u, function(){ next(); });
} else if (typeof window.YooneSnowGetOrLoadImage === 'function'){
var rec = window.YooneSnowGetOrLoadImage(u);
if (rec && rec.img){
var prevOnload = rec.img.onload;
var prevOnerror = rec.img.onerror;
rec.img.onload = function(){ try{ if (typeof prevOnload === 'function'){ prevOnload(); } }catch(e){} next(); };
rec.img.onerror = function(){ try{ if (typeof prevOnerror === 'function'){ prevOnerror(); } }catch(e){} next(); };
} else { next(); }
} else {
next();
}
}
next();
}
window.YooneSnowEnqueueAsset = enqueueAsset;
function scheduleWarmLoad(){
var urls = [];
for (var i = 0; i < assetShapeKeys.length; i++){ var k = assetShapeKeys[i]; var u = assetsMap[k]; if (typeof u === 'string' && u){ urls.push(u); } }
for (var j = 0; j < urls.length; j++){ enqueueAsset(urls[j]); }
runAssetQueue();
}
scheduleWarmLoad();
// 移除单独的尺寸与偏移缩放 直接使用最小半径与最小摆动作为缩放系数
const radiusMinRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.radiusMin !== 'undefined')
? parseFloat(window.YooneSnowSettings.radiusMin)
: 1.0;
const radiusMaxRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.radiusMax !== 'undefined')
? parseFloat(window.YooneSnowSettings.radiusMax)
: 3.0;
const driftMinRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.driftMin !== 'undefined')
? parseFloat(window.YooneSnowSettings.driftMin)
: 0.4;
const driftMaxRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.driftMax !== 'undefined')
? parseFloat(window.YooneSnowSettings.driftMax)
: 1.0;
const swingMinRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.swingMin !== 'undefined')
? parseFloat(window.YooneSnowSettings.swingMin)
: 0.2;
const swingMaxRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.swingMax !== 'undefined')
? parseFloat(window.YooneSnowSettings.swingMax)
: 1.0;
const radiusMin = isNaN(radiusMinRaw) ? 1 : Math.max(0, radiusMinRaw);
const radiusMax = isNaN(radiusMaxRaw) ? 3 : Math.max(radiusMin, radiusMaxRaw);
const driftMin = isNaN(driftMinRaw) ? 0.4 : Math.max(0, driftMinRaw);
const driftMax = isNaN(driftMaxRaw) ? 1.0 : Math.max(driftMin, driftMaxRaw);
const swingMin = isNaN(swingMinRaw) ? 0.2 : Math.max(0, swingMinRaw);
const swingMax = isNaN(swingMaxRaw) ? 1.0 : Math.max(swingMin, swingMaxRaw);
// 组件与精灵类封装 基于 Cocos 风格
class Component {
constructor(){ }
init(engine, sprite){}
update(engine, sprite, dt){}
}
class DownwardMoveComponent extends Component {
init(engine, sprite){}
update(engine, sprite, dt){
// 使用基于时间的更新 通过帧因子平滑帧率波动
const factor = Math.max(0.5, Math.min(2.0, dt * 60));
const vy = (sprite.driftSpeed * 2 + sprite.radius * 0.25) * factor;
sprite.positionY += vy;
}
}
class SwingComponent extends Component {
init(engine, sprite){}
update(engine, sprite, dt){
const factor = Math.max(0.5, Math.min(2.0, dt * 60));
const vx = Math.sin(sprite.positionY * 0.01) * sprite.swingAmplitude * factor;
sprite.positionX += vx;
}
}
class LifetimeComponent extends Component {
init(engine, sprite){}
update(engine, sprite, dt){
// 超出视口则标记移出 引擎将回收
if (sprite.positionY > engine.getViewportHeight() + 5){ sprite.outOfView = true; }
}
}
class Sprite {
constructor(props){
this.positionX = props.positionX;
this.positionY = props.positionY;
this.radius = props.radius;
this.driftSpeed = props.driftSpeed;
this.swingAmplitude = props.swingAmplitude;
this.shapeType = props.shapeType;
this.imageUrl = props.imageUrl || null;
this.emojiText = props.emojiText || null;
this.outOfView = false;
this.components = [];
}
addComponent(comp){ this.components.push(comp); }
init(engine){ for (let i = 0; i < this.components.length; i++){ try{ this.components[i].init(engine, this); }catch(e){} } }
update(engine, dt){ for (let i = 0; i < this.components.length; i++){ try{ this.components[i].update(engine, this, dt); }catch(e){} } }
render(engine){
const ctx = engine.context;
const registry = window.YooneSnowShapeRenderers || {};
const renderer = registry[this.shapeType] || registry['dot'];
if (typeof renderer === 'function'){ renderer(ctx, this.positionX, this.positionY, this.radius); }
}
}
class Snow extends Sprite {
constructor(props){ super(props); }
render(engine){
const ctx = engine.context;
if (this.shapeType === 'media_image' && this.imageUrl){
const record = window.YooneSnowGetOrLoadImage ? window.YooneSnowGetOrLoadImage(this.imageUrl) : { img: null, ready: false };
if (record && record.ready){
const targetHeight = this.radius * 8;
const targetWidth = targetHeight;
window.YooneSnowDrawCenteredImage(ctx, record.img, this.positionX, this.positionY, targetWidth, targetHeight);
}
return;
}
if (this.shapeType === 'emoji_text' && this.emojiText){
ctx.save();
const fontSize = Math.max(12, this.radius * 6);
ctx.font = String(Math.floor(fontSize)) + 'px system-ui, Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(this.emojiText), this.positionX, this.positionY);
ctx.restore();
return;
}
if (this.shapeType === 'text_label' && this.emojiText){
ctx.save();
const fontSize = Math.max(12, this.radius * 5.5);
ctx.font = String(Math.floor(fontSize)) + 'px system-ui, -apple-system, Segoe UI, Roboto, Noto Sans';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.fillText(String(this.emojiText), this.positionX, this.positionY);
ctx.restore();
return;
}
const registry = window.YooneSnowShapeRenderers || {};
const renderer = registry[this.shapeType] || registry['dot'];
if (typeof renderer === 'function'){ renderer(ctx, this.positionX, this.positionY, this.radius); }
}
}
// 简易引擎引用 用于组件获取上下文与视口尺寸 始终返回最新值
const engineRef = {
getViewportWidth: function(){ return viewportWidth; },
getViewportHeight: function(){ return viewportHeight; },
context: context
};
class Animator {
constructor(onFrame, isDone, onStopped){
this.onFrame = onFrame;
this.isDone = isDone;
this.onStopped = onStopped;
this.lastTs = performance.now();
this.rafId = null;
this.running = false;
this.loop = this.loop.bind(this);
}
init(){
this.lastTs = performance.now();
this.running = false;
}
loop(){
if (!this.running) return;
const nowTs = performance.now();
const deltaSeconds = Math.max(0, (nowTs - this.lastTs) / 1000);
this.lastTs = nowTs;
if (typeof this.onFrame === 'function'){ this.onFrame(deltaSeconds); }
const done = typeof this.isDone === 'function' ? this.isDone() : false;
if (done){
this.stop();
if (typeof this.onStopped === 'function'){ this.onStopped(); }
return;
}
this.rafId = requestAnimationFrame(this.loop);
}
update(){
this.running = true;
this.rafId = requestAnimationFrame(this.loop);
}
stop(){
if (this.rafId !== null && typeof cancelAnimationFrame === 'function'){
try{ cancelAnimationFrame(this.rafId); }catch(e){}
this.rafId = null;
}
this.running = false;
}
}
class SnowAnimator extends Animator {
constructor(){
super(function(dt){ if (typeof this.onFrameImpl === 'function'){ this.onFrameImpl(dt); } }, function(){ return typeof this.isDoneImpl === 'function' ? this.isDoneImpl() : false; }, function(){ if (typeof this.onStoppedImpl === 'function'){ this.onStoppedImpl(); } });
this.onFrameImpl = null;
this.isDoneImpl = null;
this.onStoppedImpl = null;
this.targetCount = 0;
}
computeTargetCount(){
this.targetCount = snowflakesTargetCount;
return this.targetCount;
}
init(){
this.computeTargetCount();
initSnowPrefill();
this.onFrameImpl = function(deltaSeconds){ updateSystem(deltaSeconds); renderSystem(); };
this.isDoneImpl = function(){ return shouldStop(); };
this.onStoppedImpl = function(){ cleanupStop(); };
super.init();
}
update(){
super.update();
}
stop(){
super.stop();
}
}
// 函数 调整画布尺寸并设置像素比 保证清晰显示
function resizeCanvas(){
viewportWidth = window.innerWidth;
viewportHeight = window.innerHeight;
const displayWidth = viewportWidth;
const displayHeight = viewportHeight;
canvas.style.width = displayWidth + 'px';
canvas.style.height = displayHeight + 'px';
canvas.width = Math.floor(displayWidth * devicePixelRatio);
canvas.height = Math.floor(displayHeight * devicePixelRatio);
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
}
resizeCanvas();
// 读取分屏最大数量设置 0 表示自动 根据屏幕最小边界划分为 small medium large
function computeAutoCount(kind){
const area = viewportWidth * viewportHeight;
if (kind === 'small'){
const est = area / 36000;
return Math.floor(Math.min(80, Math.max(40, est)));
}
if (kind === 'medium'){
// 中屏适中限制到 [100 200]
const est = area / 18000;
return Math.floor(Math.min(200, Math.max(100, est)));
}
// 大屏较多但仍限制到 [140 300]
const est = area / 12000;
return Math.floor(Math.min(300, Math.max(140, est)));
}
const minDim = Math.min(viewportWidth, viewportHeight);
const rawSmall = (window.YooneSnowSettings && typeof window.YooneSnowSettings.maxCountSmall !== 'undefined') ? parseInt(window.YooneSnowSettings.maxCountSmall, 10) : 0;
const rawMedium = (window.YooneSnowSettings && typeof window.YooneSnowSettings.maxCountMedium !== 'undefined') ? parseInt(window.YooneSnowSettings.maxCountMedium, 10) : 0;
const rawLarge = (window.YooneSnowSettings && typeof window.YooneSnowSettings.maxCountLarge !== 'undefined') ? parseInt(window.YooneSnowSettings.maxCountLarge, 10) : 0;
const fallbackSingle = (window.YooneSnowSettings && typeof window.YooneSnowSettings.maxCount !== 'undefined') ? parseInt(window.YooneSnowSettings.maxCount, 10) : 0;
let snowflakesTargetCount = 0;
if (minDim <= 480){
snowflakesTargetCount = (isNaN(rawSmall) || rawSmall <= 0) ? computeAutoCount('small') : rawSmall;
} else if (minDim <= 960){
snowflakesTargetCount = (isNaN(rawMedium) || rawMedium <= 0) ? computeAutoCount('medium') : rawMedium;
} else {
snowflakesTargetCount = (isNaN(rawLarge) || rawLarge <= 0) ? computeAutoCount('large') : rawLarge;
}
if (snowflakesTargetCount <= 0){
// 回退到旧单值设置 仍支持 0 自动
snowflakesTargetCount = (isNaN(fallbackSingle) || fallbackSingle <= 0) ? computeAutoCount(minDim <= 480 ? 'small' : (minDim <= 960 ? 'medium' : 'large')) : fallbackSingle;
}
snowflakesTargetCount = Math.max(1, snowflakesTargetCount);
const snowflakes = [];
// 定义连续生成控制参数 使用时间积累的方式平滑新增
let spawnAccumulator = 0;
let lastUpdateTimestamp = performance.now();
let rafId = null;
// 定义黄金比例相位用于水平位置分布避免聚集
let spawnPhase = Math.random();
const goldenRatio = 0.61803398875;
var yooneLogEntries = Array.isArray(window.YooneSnowLogEntries) ? window.YooneSnowLogEntries : [];
window.YooneSnowLogEntries = yooneLogEntries;
function yooneLogPush(entry){
try {
yooneLogEntries.push(Object.assign({ ts: performance.now() }, entry));
if (yooneLogEntries.length > 1500){ yooneLogEntries.shift(); }
// 关闭默认的控制台输出 避免在高频帧中阻塞主线程导致动画卡顿
} catch(e){}
}
window.YooneSnowGetLog = function(){ return yooneLogEntries.slice(); };
// 函数 按权重选择形状或媒体图像
function selectWeightedItem(){
const items = [];
for (let sIndex = 0; sIndex < selectedShapes.length; sIndex++){
const shapeKey = selectedShapes[sIndex];
const weightVal = typeof shapeWeights[shapeKey] !== 'undefined' ? shapeWeights[shapeKey] : 1;
if (weightVal > 0){
if (assetShapeKeys.indexOf(shapeKey) >= 0){
const aurl = assetsMap[shapeKey];
if (aurl && typeof window.YooneSnowEnqueueAsset === 'function'){ window.YooneSnowEnqueueAsset(aurl); }
}
items.push({ kind: 'shape', key: shapeKey, weight: weightVal });
}
}
// Emoji 候選獨立加入 不依賴 Shapes 是否包含 emoji
if (emojiItems.length > 0){
for (let eIndex = 0; eIndex < emojiItems.length; eIndex++){
const ch = String(emojiItems[eIndex] || '').trim();
if (ch === ''){ continue; }
const ewRaw = typeof emojiWeightsRaw[ch] !== 'undefined' ? parseInt(emojiWeightsRaw[ch], 10) : 1;
const ew = isNaN(ewRaw) ? 1 : Math.max(0, ewRaw);
if (ew > 0){ items.push({ kind: 'emoji', text: ch, weight: ew }); }
}
}
if (textItems.length > 0){
for (let tIndex = 0; tIndex < textItems.length; tIndex++){
const tx = String(textItems[tIndex] || '').trim();
if (tx === ''){ continue; }
const twRaw = typeof textWeightsRaw[tx] !== 'undefined' ? parseInt(textWeightsRaw[tx], 10) : 1;
const tw = isNaN(twRaw) ? 1 : Math.max(0, twRaw);
if (tw > 0){ items.push({ kind: 'text', text: tx, weight: tw }); }
}
}
for (let mIndex = 0; mIndex < mediaItems.length; mIndex++){
const mediaUrl = mediaItems[mIndex];
const mediaWeight = typeof mediaWeightsRaw[mediaUrl] !== 'undefined' ? parseInt(mediaWeightsRaw[mediaUrl], 10) : 1;
const finalMediaWeight = isNaN(mediaWeight) ? 1 : Math.max(0, mediaWeight);
if (finalMediaWeight > 0){
const rec = (typeof window.YooneSnowGetOrLoadImage === 'function' && mediaUrl) ? window.YooneSnowGetOrLoadImage(mediaUrl) : null;
if (rec && rec.ready){ items.push({ kind: 'media', url: mediaUrl, weight: finalMediaWeight }); }
}
}
if (items.length === 0){
return null;
}
let totalWeight = 0;
for (let i = 0; i < items.length; i++){
totalWeight += items[i].weight;
}
const r = Math.random() * totalWeight;
let acc = 0;
for (let i = 0; i < items.length; i++){
acc += items[i].weight;
// 条件判断 如果随机值落在当前累计权重内则选择该项
if (r <= acc){
if (items[i].kind === 'shape'){
return { type: items[i].key, url: null, text: null };
} else {
if (items[i].kind === 'media'){
return { type: 'media_image', url: items[i].url, text: null };
} else {
if (items[i].kind === 'emoji'){
return { type: 'emoji_text', url: null, text: items[i].text };
} else {
return { type: 'text_label', url: null, text: items[i].text };
}
}
}
}
}
return null;
}
function createSnowflake(preferredX, preferredY){
const picked = selectWeightedItem();
if (!picked){ return null; }
let chosenType = picked.type;
let chosenImageUrl = picked.url;
let chosenEmojiText = picked.text;
const sprite = new Snow({
positionX: typeof preferredX === 'number' ? preferredX : Math.random() * viewportWidth,
positionY: typeof preferredY === 'number' ? preferredY : (-1 - Math.random() * 4),
radius: (Math.random() * (radiusMax - radiusMin) + radiusMin) * radiusMin,
driftSpeed: Math.random() * (driftMax - driftMin) + driftMin,
swingAmplitude: (Math.random() * (swingMax - swingMin) + swingMin) * swingMin,
shapeType: chosenType,
imageUrl: chosenImageUrl,
emojiText: chosenEmojiText
});
sprite.addComponent(new DownwardMoveComponent());
sprite.addComponent(new SwingComponent());
sprite.addComponent(new LifetimeComponent());
sprite.init(engineRef);
return sprite;
}
// 计算平均垂直速度 辅助估算生成速率 保证视觉连续
function computeAverageVerticalSpeed(){
let countInView = 0;
let speedSum = 0;
for (let idx = 0; idx < snowflakes.length; idx++){
const flake = snowflakes[idx];
if (flake.outOfView){ continue; }
const verticalSpeed = (flake.driftSpeed * 2 + flake.radius * 0.25) * 60;
speedSum += verticalSpeed;
countInView++;
}
if (countInView > 0){
return speedSum / countInView;
}
const driftAverage = (driftMin + driftMax) * 0.5;
const radiusAverage = ((radiusMin + radiusMax) * 0.5) * radiusMin;
return (driftAverage * 2 + radiusAverage * 0.25) * 60;
}
// 函数 根据视口高度 目标最大数量 与平均速度估算初始化预填充数量
function estimateInitialPrefillCount(){
const averageVerticalSpeed = computeAverageVerticalSpeed();
const averageLifeSeconds = (viewportHeight + 5) / Math.max(0.001, averageVerticalSpeed);
const supplyRatePerSecond = snowflakesTargetCount / Math.max(0.001, averageLifeSeconds);
const warmupSeconds = Math.min(1.2, Math.max(0.6, averageLifeSeconds * 0.2));
const rawCount = Math.floor(supplyRatePerSecond * warmupSeconds);
const maxInitialFraction = 0.45;
const capByFraction = Math.floor(snowflakesTargetCount * maxInitialFraction);
const boundedCount = Math.max(8, Math.min(capByFraction, rawCount));
return boundedCount;
}
// 函数 封装添加雪花的操作 支持指定水平与垂直位置
function pushSnowflake(preferredX, preferredY){
var flake = createSnowflake(preferredX, preferredY);
if (flake){ snowflakes.push(flake); return true; }
return false;
}
// 函数 初始化预填充雪花 保持从顶部下落的感觉并避免过量
function initSnowPrefill(){
// 条件判断 仅在未达到时长且当前未生成过雪花时执行
if (hasReachedDuration){ return; }
if (snowflakes.length > 0){ return; }
const initialCount = estimateInitialPrefillCount();
yooneLogPush({ kind: 'prefill', count: initialCount, target: snowflakesTargetCount });
for (let spawnIndex = 0; spawnIndex < initialCount; spawnIndex++){
// 使用黄金比例相位分布水平位置 减少聚集
const preferredX = (spawnPhase % 1) * viewportWidth;
spawnPhase = (spawnPhase + goldenRatio) % 1;
// 预填充垂直位置覆盖顶部到中段 减少中段稀疏
const preferredY = -Math.random() * (viewportHeight * 0.4);
pushSnowflake(preferredX, preferredY);
}
}
// 調用初始化預填充
initSnowPrefill();
// 函数 更新雪花位置与视口状态
function updateSnowflakes(deltaSeconds){
for (let j = 0; j < snowflakes.length; j++){
const flake = snowflakes[j];
if (flake && typeof flake.update === 'function'){
flake.update(engineRef, deltaSeconds);
} else {
const frameFactor = Math.max(0.5, Math.min(2.0, deltaSeconds * 60));
flake.positionY += (flake.driftSpeed * 2 + flake.radius * 0.25) * frameFactor;
flake.positionX += Math.sin(flake.positionY * 0.01) * flake.swingAmplitude * frameFactor;
if (flake.positionY > viewportHeight + 5){ flake.outOfView = true; }
}
}
}
// 使用全局渲染注册表 根据形状类型选择渲染函数
// 函数 清空画布并绘制可见雪花
function drawSnowflakes(){
context.clearRect(0, 0, viewportWidth, viewportHeight);
for (let k = 0; k < snowflakes.length; k++){
const flake = snowflakes[k];
if (flake.outOfView){ continue; }
if (flake && typeof flake.render === 'function'){
try{ flake.render(engineRef); }catch(e){}
continue;
}
const registry = window.YooneSnowShapeRenderers || {};
const renderer = registry[flake.shapeType] || registry['dot'];
if (typeof renderer === 'function'){
renderer(context, flake.positionX, flake.positionY, flake.radius);
}
}
}
// 清理停止函数 用于在达到设定时长后停止动画并释放资源
function cleanupStop(){
// 条件判断 如果存在窗口尺寸事件监听则移除
if (typeof onResize === 'function') {
window.removeEventListener('resize', onResize);
}
// 清空画布并隐藏元素
context.clearRect(0, 0, viewportWidth, viewportHeight);
canvas.style.display = 'none';
}
// 函数 更新系统 执行移除与补给并更新位置
function updateSystem(deltaSeconds){
for (let idx = snowflakes.length - 1; idx >= 0; idx--){
// 条件判断 仅当雪花已移出视口时才从数组移除 保证雪花下到窗口底部才消失
if (snowflakes[idx].outOfView){ snowflakes.splice(idx, 1); }
}
if (!hasReachedDuration){
// 依据当前平均速度以及视口高度估算雪花平均生命周期和补给速率
const averageVerticalSpeed = computeAverageVerticalSpeed();
const averageLifeSeconds = (viewportHeight + 5) / Math.max(0.001, averageVerticalSpeed);
const supplyRatePerSecond = snowflakesTargetCount / Math.max(0.001, averageLifeSeconds);
spawnAccumulator += supplyRatePerSecond * Math.max(0, deltaSeconds);
// 条件判断 根据累积值决定此次生成数量 保证均匀补给并确保达到最大设定数量
const availableSlots = Math.max(0, snowflakesTargetCount - snowflakes.length);
let spawnCount = Math.min(availableSlots, Math.floor(spawnAccumulator));
// 条件判断 若仍有缺口但累积量不足一整个单位 则至少补充 1 个
if (spawnCount === 0 && availableSlots > 0){ spawnCount = 1; }
// 条件判断 限制每帧最大生成数量避免瞬时爆发
const maxPerFrame = Math.max(1, Math.floor(snowflakesTargetCount * 0.05));
if (spawnCount > maxPerFrame){ spawnCount = maxPerFrame; }
if (spawnCount > 0){
yooneLogPush({ kind: 'frame', count: spawnCount, availableSlots: availableSlots, length: snowflakes.length, target: snowflakesTargetCount, accumulator: spawnAccumulator });
var added = 0;
for (let s = 0; s < spawnCount; s++){
const preferredX = (spawnPhase % 1) * viewportWidth;
spawnPhase = (spawnPhase + goldenRatio) % 1;
if (pushSnowflake(preferredX, undefined)) { added++; }
}
if (added > 0){ spawnAccumulator = Math.max(0, spawnAccumulator - added); }
}
}
updateSnowflakes(deltaSeconds);
}
// 函数 渲染系统 清空画布并绘制
function renderSystem(){
drawSnowflakes();
}
// 函数 动画主循环 包含生成 渲染 与停止逻辑
function shouldStop(){
if (displayDurationSeconds > 0 && !hasReachedDuration){
const elapsedSeconds = (performance.now() - startTimestamp) / 1000;
if (elapsedSeconds >= displayDurationSeconds){ hasReachedDuration = true; }
}
if (hasReachedDuration){
const allOut = snowflakes.every(function(f){ return f.outOfView; });
if (allOut){ return true; }
}
return false;
}
const animator = new SnowAnimator();
// 定义窗口尺寸事件处理器 以便在停止时移除
function onResize(){
// 条件判断 保证画布尺寸与视口一致
resizeCanvas();
viewportWidth = window.innerWidth;
viewportHeight = window.innerHeight;
}
window.addEventListener('resize', onResize);
animator.init();
animator.update();
}
// 条件判断 如果文档尚未加载则等待 DOMContentLoaded 事件
if (document.readyState === 'complete'){
init();
} else {
window.addEventListener('load', init);
}
})();

49
lib/index.html Normal file
View File

@ -0,0 +1,49 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Yoone Snow Preview</title>
</head>
<body style="margin:0;padding:0;">
<canvas id="effectiveAppsSnow" aria-hidden="true"></canvas>
<script type="module">
import santa_hat from '/src/assets/圣诞雪帽.svg'
import candy_cane from '/src/assets/圣诞拐杖.svg'
import christmas_sock from '/src/assets/圣诞袜子.svg'
import christmas_tree from '/src/assets/圣诞树.svg'
import reindeer from '/src/assets/圣诞麋鹿.svg'
import christmas_berry from '/src/assets/圣诞果.svg'
window.YooneSnowSettings = {
selectedShapes: ['dot','flake','yuanbao','coin','santa_hat','candy_cane','christmas_sock','christmas_tree','reindeer','christmas_berry'],
mediaItems: [],
emojiItems: [],
textItems: [],
shapeWeights: { dot:1, flake:4, yuanbao:1, coin:1, santa_hat:1, candy_cane:1, christmas_sock:1, christmas_tree:1, reindeer:1, christmas_berry:1 },
mediaWeights: {},
emojiWeights: {},
textWeights: {},
radiusMin: 1.0,
radiusMax: 3.0,
driftMin: 0.4,
driftMax: 1.0,
swingMin: 0.2,
swingMax: 1.0,
displayDurationSeconds: 0,
maxCount: 0,
maxCountSmall: 0,
maxCountMedium: 0,
maxCountLarge: 0,
assetsMap: {
santa_hat,
candy_cane,
christmas_sock,
christmas_tree,
reindeer,
christmas_berry
}
}
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

957
lib/package-lock.json generated Normal file
View File

@ -0,0 +1,957 @@
{
"name": "yoone-snow-lib",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "yoone-snow-lib",
"version": "0.1.0",
"devDependencies": {
"typescript": "^5.6.3",
"vite": "^5.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
"integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
"integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "4.53.3",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.53.3.tgz",
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.53.3",
"@rollup/rollup-android-arm64": "4.53.3",
"@rollup/rollup-darwin-arm64": "4.53.3",
"@rollup/rollup-darwin-x64": "4.53.3",
"@rollup/rollup-freebsd-arm64": "4.53.3",
"@rollup/rollup-freebsd-x64": "4.53.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
"@rollup/rollup-linux-arm-musleabihf": "4.53.3",
"@rollup/rollup-linux-arm64-gnu": "4.53.3",
"@rollup/rollup-linux-arm64-musl": "4.53.3",
"@rollup/rollup-linux-loong64-gnu": "4.53.3",
"@rollup/rollup-linux-ppc64-gnu": "4.53.3",
"@rollup/rollup-linux-riscv64-gnu": "4.53.3",
"@rollup/rollup-linux-riscv64-musl": "4.53.3",
"@rollup/rollup-linux-s390x-gnu": "4.53.3",
"@rollup/rollup-linux-x64-gnu": "4.53.3",
"@rollup/rollup-linux-x64-musl": "4.53.3",
"@rollup/rollup-openharmony-arm64": "4.53.3",
"@rollup/rollup-win32-arm64-msvc": "4.53.3",
"@rollup/rollup-win32-ia32-msvc": "4.53.3",
"@rollup/rollup-win32-x64-gnu": "4.53.3",
"@rollup/rollup-win32-x64-msvc": "4.53.3",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

15
lib/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "yoone-snow-lib",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite",
"watch": "vite build --watch"
},
"devDependencies": {
"typescript": "^5.6.3",
"vite": "^5.0.0"
}
}

View File

@ -0,0 +1,38 @@
export class Animator {
private lastTs = performance.now()
private rafId: number | null = null
private running = false
constructor(
private onFrame: (dt: number) => void,
private isDone: () => boolean,
private onStopped?: () => void
){}
init(){
this.lastTs = performance.now()
this.running = false
}
private loop = () => {
if (!this.running) return
const nowTs = performance.now()
const deltaSeconds = Math.max(0, (nowTs - this.lastTs) / 1000)
this.lastTs = nowTs
this.onFrame(deltaSeconds)
if (this.isDone()){
this.stop()
if (this.onStopped) this.onStopped()
return
}
this.rafId = requestAnimationFrame(this.loop)
}
update(){
this.running = true
this.rafId = requestAnimationFrame(this.loop)
}
stop(){
if (this.rafId !== null && typeof cancelAnimationFrame === 'function'){
try { cancelAnimationFrame(this.rafId) } catch {}
this.rafId = null
}
this.running = false
}
}

View File

@ -0,0 +1,32 @@
import { Animator } from './Animator'
import { createSystem, SystemSettings } from '../system/snowSystem'
export function runSnow(canvas: HTMLCanvasElement, settings: SystemSettings){
const context = canvas.getContext('2d') as CanvasRenderingContext2D
let viewportWidth = window.innerWidth
let viewportHeight = window.innerHeight
const dpr = window.devicePixelRatio || 1
const system = createSystem(context, () => viewportWidth, () => viewportHeight, settings)
function resize(){
viewportWidth = window.innerWidth
viewportHeight = window.innerHeight
canvas.style.width = `${viewportWidth}px`
canvas.style.height = `${viewportHeight}px`
canvas.width = Math.floor(viewportWidth * dpr)
canvas.height = Math.floor(viewportHeight * dpr)
context.setTransform(dpr, 0, 0, dpr, 0, 0)
system.setViewport(viewportWidth, viewportHeight)
system.recomputeTarget()
}
resize()
const animator = new Animator(
(dt) => { system.updateSystem(dt); system.renderSystem() },
() => system.shouldStop(),
() => { context.clearRect(0, 0, viewportWidth, viewportHeight); canvas.style.display = 'none' }
)
function onResize(){ resize() }
window.addEventListener('resize', onResize)
animator.init()
animator.update()
return { stop(){ animator.stop(); window.removeEventListener('resize', onResize) } }
}

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,48 @@
export class Component {
init(_engine: EngineRef, _sprite: Sprite): void {}
update(_engine: EngineRef, _sprite: Sprite, _dt: number): void {}
}
export interface EngineRef {
getViewportWidth(): number
getViewportHeight(): number
context: CanvasRenderingContext2D
}
export interface SpriteProps {
positionX: number
positionY: number
radius: number
driftSpeed: number
swingAmplitude: number
shapeType: string
imageUrl?: string | null
emojiText?: string | null
}
export class Sprite {
positionX: number
positionY: number
radius: number
driftSpeed: number
swingAmplitude: number
shapeType: string
imageUrl: string | null
emojiText: string | null
outOfView = false
components: Component[] = []
constructor(props: SpriteProps){
this.positionX = props.positionX
this.positionY = props.positionY
this.radius = props.radius
this.driftSpeed = props.driftSpeed
this.swingAmplitude = props.swingAmplitude
this.shapeType = props.shapeType
this.imageUrl = props.imageUrl || null
this.emojiText = props.emojiText || null
}
addComponent(c: Component){ this.components.push(c) }
init(engine: EngineRef){ for (let i = 0; i < this.components.length; i++){ try{ this.components[i].init(engine, this) }catch{} } }
update(engine: EngineRef, dt: number){ for (let i = 0; i < this.components.length; i++){ try{ this.components[i].update(engine, this, dt) }catch{} } }
}

View File

@ -0,0 +1,9 @@
import { Component, EngineRef, Sprite } from './Component'
export class DownwardMoveComponent extends Component {
update(engine: EngineRef, sprite: Sprite, dt: number){
const factor = Math.max(0.5, Math.min(2.0, dt * 60))
const vy = (sprite.driftSpeed * 2 + sprite.radius * 0.25) * factor
sprite.positionY += vy
}
}

View File

@ -0,0 +1,7 @@
import { Component, EngineRef, Sprite } from './Component'
export class LifetimeComponent extends Component {
update(engine: EngineRef, sprite: Sprite, _dt: number){
if (sprite.positionY > engine.getViewportHeight() + 5){ sprite.outOfView = true }
}
}

43
lib/src/engine/Snow.ts Normal file
View File

@ -0,0 +1,43 @@
import { Sprite, SpriteProps } from './Component'
import { drawCenteredImage, getOrLoadImage } from '../utils/image'
export class Snow extends Sprite {
render(engine: { context: CanvasRenderingContext2D }){
const ctx = engine.context
if (this.shapeType === 'media_image' && this.imageUrl){
const rec = getOrLoadImage(this.imageUrl)
if (rec && rec.ready && rec.img){
const h = this.radius * 8
const w = h
drawCenteredImage(ctx, rec.img, this.positionX, this.positionY, w, h)
}
return
}
if (this.shapeType === 'emoji_text' && this.emojiText){
ctx.save()
const fontSize = Math.max(12, this.radius * 6)
ctx.font = `${Math.floor(fontSize)}px system-ui, Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(String(this.emojiText), this.positionX, this.positionY)
ctx.restore()
return
}
if (this.shapeType === 'text_label' && this.emojiText){
ctx.save()
const fontSize = Math.max(12, this.radius * 5.5)
ctx.font = `${Math.floor(fontSize)}px system-ui, -apple-system, Segoe UI, Roboto, Noto Sans`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillStyle = 'rgba(255,255,255,0.9)'
ctx.fillText(String(this.emojiText), this.positionX, this.positionY)
ctx.restore()
return
}
const registry = window.YooneSnowShapeRenderers || {}
const renderer = registry[this.shapeType] || registry['dot']
if (typeof renderer === 'function'){
renderer(ctx, this.positionX, this.positionY, this.radius)
}
}
}

View File

@ -0,0 +1,9 @@
import { Component, EngineRef, Sprite } from './Component'
export class SwingComponent extends Component {
update(engine: EngineRef, sprite: Sprite, dt: number){
const factor = Math.max(0.5, Math.min(2.0, dt * 60))
const vx = Math.sin(sprite.positionY * 0.01) * sprite.swingAmplitude * factor
sprite.positionX += vx
}
}

15
lib/src/global.ts Normal file
View File

@ -0,0 +1,15 @@
export type ShapeRenderer = (context: CanvasRenderingContext2D, x: number, y: number, r: number) => void
declare global {
interface Window {
YooneSnowShapeRenderers?: Record<string, ShapeRenderer>
YooneSnowSettings?: Record<string, unknown>
}
}
export function ensureRendererRegistry(): Record<string, ShapeRenderer> {
if (!window.YooneSnowShapeRenderers) {
window.YooneSnowShapeRenderers = {}
}
return window.YooneSnowShapeRenderers
}

5
lib/src/index.ts Normal file
View File

@ -0,0 +1,5 @@
export { runSnow } from './animator/SnowAnimator'
export type { SystemSettings } from './system/snowSystem'
export { ensureRendererRegistry } from './global'
export { getOrLoadImage, loadAssetViaFetch, drawCenteredImage } from './utils/image'
export { initYooneSnow } from './main'

53
lib/src/main.ts Normal file
View File

@ -0,0 +1,53 @@
import './styles/snow.css'
import './global'
import './utils/image'
import './shapes/dot'
import './shapes/flake'
import './shapes/yuanbao'
import './shapes/coin'
import './shapes/santa_hat'
import './shapes/candy_cane'
import './shapes/christmas_sock'
import './shapes/christmas_tree'
import './shapes/reindeer'
import './shapes/christmas_berry'
import { runSnow } from './animator/SnowAnimator'
function init(){
const canvas = document.getElementById('effectiveAppsSnow') as HTMLCanvasElement | null
if (!canvas) return
const prefersReducedMotion = (typeof window.matchMedia === 'function') && window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (prefersReducedMotion) { canvas.style.display = 'none'; return }
const s = (window.YooneSnowSettings || {}) as any
const settings = {
selectedShapes: Array.isArray(s.selectedShapes) ? s.selectedShapes : [],
mediaItems: Array.isArray(s.mediaItems) ? s.mediaItems : [],
emojiItems: Array.isArray(s.emojiItems) ? s.emojiItems : [],
textItems: Array.isArray(s.textItems) ? s.textItems : [],
shapeWeights: s.shapeWeights || {},
mediaWeights: s.mediaWeights || {},
emojiWeights: s.emojiWeights || {},
textWeights: s.textWeights || {},
radiusMin: typeof s.radiusMin === 'number' ? s.radiusMin : 1.0,
radiusMax: typeof s.radiusMax === 'number' ? s.radiusMax : 3.0,
driftMin: typeof s.driftMin === 'number' ? s.driftMin : 0.4,
driftMax: typeof s.driftMax === 'number' ? s.driftMax : 1.0,
swingMin: typeof s.swingMin === 'number' ? s.swingMin : 0.2,
swingMax: typeof s.swingMax === 'number' ? s.swingMax : 1.0,
displayDurationSeconds: typeof s.displayDurationSeconds === 'number' ? s.displayDurationSeconds : 0,
maxCount: typeof s.maxCount === 'number' ? s.maxCount : 0,
maxCountSmall: typeof s.maxCountSmall === 'number' ? s.maxCountSmall : 0,
maxCountMedium: typeof s.maxCountMedium === 'number' ? s.maxCountMedium : 0,
maxCountLarge: typeof s.maxCountLarge === 'number' ? s.maxCountLarge : 0,
assetsMap: s.assetsMap || {}
}
runSnow(canvas, settings)
}
if (document.readyState === 'complete'){
init()
} else {
window.addEventListener('load', init)
}
export function initYooneSnow(){ window.dispatchEvent(new Event('load')) }

View File

@ -0,0 +1,14 @@
import { ensureRendererRegistry } from '../global'
import { getOrLoadImage, drawCenteredImage } from '../utils/image'
const registry = ensureRendererRegistry()
registry.candy_cane = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){
const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record<string,string> : {}
const url = assets['candy_cane'] || ''
const rec = getOrLoadImage(url)
if (!rec || !rec.ready || !rec.img) return
const h = r * 8
const w = h
drawCenteredImage(context, rec.img, x, y, w, h)
}

View File

@ -0,0 +1,14 @@
import { ensureRendererRegistry } from '../global'
import { getOrLoadImage, drawCenteredImage } from '../utils/image'
const registry = ensureRendererRegistry()
registry.christmas_berry = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){
const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record<string,string> : {}
const url = assets['christmas_berry'] || ''
const rec = getOrLoadImage(url)
if (!rec || !rec.ready || !rec.img) return
const h = r * 8
const w = h
drawCenteredImage(context, rec.img, x, y, w, h)
}

View File

@ -0,0 +1,14 @@
import { ensureRendererRegistry } from '../global'
import { getOrLoadImage, drawCenteredImage } from '../utils/image'
const registry = ensureRendererRegistry()
registry.christmas_sock = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){
const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record<string,string> : {}
const url = assets['christmas_sock'] || ''
const rec = getOrLoadImage(url)
if (!rec || !rec.ready || !rec.img) return
const h = r * 8
const w = h
drawCenteredImage(context, rec.img, x, y, w, h)
}

View File

@ -0,0 +1,14 @@
import { ensureRendererRegistry } from '../global'
import { getOrLoadImage, drawCenteredImage } from '../utils/image'
const registry = ensureRendererRegistry()
registry.christmas_tree = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){
const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record<string,string> : {}
const url = assets['christmas_tree'] || ''
const rec = getOrLoadImage(url)
if (!rec || !rec.ready || !rec.img) return
const h = r * 8
const w = h
drawCenteredImage(context, rec.img, x, y, w, h)
}

13
lib/src/shapes/coin.ts Normal file
View File

@ -0,0 +1,13 @@
import { ensureRendererRegistry } from '../global'
const registry = ensureRendererRegistry()
registry.coin = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){
const grd = context.createRadialGradient(x, y, r * 0.2, x, y, r)
grd.addColorStop(0, 'rgba(255,230,120,0.95)')
grd.addColorStop(1, 'rgba(240,180,60,0.85)')
context.fillStyle = grd
context.beginPath()
context.arc(x, y, r * 2.5, 0, Math.PI * 2)
context.fill()
}

10
lib/src/shapes/dot.ts Normal file
View File

@ -0,0 +1,10 @@
import { ensureRendererRegistry } from '../global'
const registry = ensureRendererRegistry()
registry.dot = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){
context.beginPath()
context.arc(x, y, r, 0, Math.PI * 2)
context.fillStyle = 'rgba(255,255,255,0.9)'
context.fill()
}

32
lib/src/shapes/flake.ts Normal file
View File

@ -0,0 +1,32 @@
import { ensureRendererRegistry } from '../global'
const registry = ensureRendererRegistry()
registry.flake = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){
const branchSize = r * 3
context.save()
context.translate(x, y)
context.fillStyle = 'rgba(255,255,255,0.9)'
context.strokeStyle = 'rgba(255,255,255,0.9)'
context.lineWidth = branchSize * 0.15
for (let i = 0; i < 6; i++){
context.rotate(Math.PI / 3)
context.beginPath()
context.moveTo(0, 0)
context.lineTo(0, branchSize)
context.stroke()
context.beginPath()
context.moveTo(0, branchSize * 0.3)
context.lineTo(branchSize * 0.3, branchSize * 0.5)
context.stroke()
context.beginPath()
context.moveTo(0, branchSize * 0.5)
context.lineTo(-branchSize * 0.3, branchSize * 0.7)
context.stroke()
context.beginPath()
context.moveTo(0, branchSize * 0.7)
context.lineTo(branchSize * 0.3, branchSize * 0.9)
context.stroke()
}
context.restore()
}

View File

@ -0,0 +1,14 @@
import { ensureRendererRegistry } from '../global'
import { getOrLoadImage, drawCenteredImage } from '../utils/image'
const registry = ensureRendererRegistry()
registry.reindeer = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){
const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record<string,string> : {}
const url = assets['reindeer'] || ''
const rec = getOrLoadImage(url)
if (!rec || !rec.ready || !rec.img) return
const h = r * 8
const w = h
drawCenteredImage(context, rec.img, x, y, w, h)
}

View File

@ -0,0 +1,14 @@
import { ensureRendererRegistry } from '../global'
import { getOrLoadImage, drawCenteredImage } from '../utils/image'
const registry = ensureRendererRegistry()
registry.santa_hat = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){
const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record<string,string> : {}
const url = assets['santa_hat'] || ''
const rec = getOrLoadImage(url)
if (!rec || !rec.ready || !rec.img) return
const h = r * 8
const w = h
drawCenteredImage(context, rec.img, x, y, w, h)
}

19
lib/src/shapes/yuanbao.ts Normal file
View File

@ -0,0 +1,19 @@
import { ensureRendererRegistry } from '../global'
const registry = ensureRendererRegistry()
registry.yuanbao = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){
context.save()
context.translate(x, y)
context.scale(r * 0.08, r * 0.08)
context.fillStyle = 'rgba(255,215,0,0.9)'
context.beginPath()
context.moveTo(-20, 0)
context.quadraticCurveTo(0, -12, 20, 0)
context.quadraticCurveTo(0, 12, -20, 0)
context.fill()
context.beginPath()
context.arc(0, 0, 6, 0, Math.PI * 2)
context.fill()
context.restore()
}

View File

@ -0,0 +1,238 @@
import { EngineRef, Sprite } from '../engine/Component'
import { Snow } from '../engine/Snow'
import { DownwardMoveComponent } from '../engine/DownwardMoveComponent'
import { SwingComponent } from '../engine/SwingComponent'
import { LifetimeComponent } from '../engine/LifetimeComponent'
export interface SystemSettings {
selectedShapes: string[]
mediaItems: string[]
emojiItems: string[]
textItems: string[]
shapeWeights: Record<string, number>
mediaWeights: Record<string, number>
emojiWeights: Record<string, number>
textWeights: Record<string, number>
radiusMin: number
radiusMax: number
driftMin: number
driftMax: number
swingMin: number
swingMax: number
displayDurationSeconds: number
maxCount: number
maxCountSmall: number
maxCountMedium: number
maxCountLarge: number
assetsMap: Record<string,string>
}
export function createSystem(context: CanvasRenderingContext2D, getViewportWidth: () => number, getViewportHeight: () => number, settings: SystemSettings){
let viewportWidth = getViewportWidth()
let viewportHeight = getViewportHeight()
const engine: EngineRef = { getViewportWidth: () => viewportWidth, getViewportHeight: () => viewportHeight, context }
const snowflakes: Snow[] = []
let spawnAccumulator = 0
let spawnPhase = Math.random()
const goldenRatio = 0.61803398875
const startTs = performance.now()
let hasReachedDuration = false
function computeTargetCount(): number {
const minDim = Math.min(viewportWidth, viewportHeight)
const auto = (kind: 'small'|'medium'|'large') => {
const area = viewportWidth * viewportHeight
if (kind === 'small') return Math.floor(Math.min(80, Math.max(40, area / 36000)))
if (kind === 'medium') return Math.floor(Math.min(200, Math.max(100, area / 18000)))
return Math.floor(Math.min(300, Math.max(140, area / 12000)))
}
let target = 0
if (minDim <= 480){ target = settings.maxCountSmall > 0 ? settings.maxCountSmall : auto('small') }
else if (minDim <= 960){ target = settings.maxCountMedium > 0 ? settings.maxCountMedium : auto('medium') }
else { target = settings.maxCountLarge > 0 ? settings.maxCountLarge : auto('large') }
if (target <= 0){ target = settings.maxCount > 0 ? settings.maxCount : auto(minDim <= 480 ? 'small' : (minDim <= 960 ? 'medium' : 'large')) }
return Math.max(1, target)
}
let targetCount = computeTargetCount()
function selectWeightedItem(): { type: string, url: string | null, text: string | null } | null {
const items: Array<{ kind: 'shape'|'media'|'emoji'|'text', key?: string, url?: string, text?: string, weight: number }> = []
for (let i = 0; i < settings.selectedShapes.length; i++){
const k = settings.selectedShapes[i]
const w = typeof settings.shapeWeights[k] !== 'undefined' ? settings.shapeWeights[k] : 1
if (w > 0){ items.push({ kind: 'shape', key: k, weight: w }) }
}
for (let i = 0; i < settings.emojiItems.length; i++){
const ch = String(settings.emojiItems[i] || '').trim()
if (ch === '') continue
const ew = settings.emojiWeights[ch] ?? 1
if (ew > 0) items.push({ kind: 'emoji', text: ch, weight: ew })
}
for (let i = 0; i < settings.textItems.length; i++){
const tx = String(settings.textItems[i] || '').trim()
if (tx === '') continue
const tw = settings.textWeights[tx] ?? 1
if (tw > 0) items.push({ kind: 'text', text: tx, weight: tw })
}
for (let i = 0; i < settings.mediaItems.length; i++){
const url = settings.mediaItems[i]
const mw = settings.mediaWeights[url] ?? 1
if (mw > 0) items.push({ kind: 'media', url, weight: mw })
}
if (items.length === 0) return null
let total = 0
for (let i = 0; i < items.length; i++){ total += items[i].weight }
const r = Math.random() * total
let acc = 0
for (let i = 0; i < items.length; i++){
acc += items[i].weight
if (r <= acc){
const it = items[i]
if (it.kind === 'shape') return { type: String(it.key), url: null, text: null }
if (it.kind === 'media') return { type: 'media_image', url: String(it.url), text: null }
if (it.kind === 'emoji') return { type: 'emoji_text', url: null, text: String(it.text) }
return { type: 'text_label', url: null, text: String(it.text) }
}
}
return null
}
function createSnowflake(preferredX?: number, preferredY?: number): Snow | null {
const picked = selectWeightedItem()
if (!picked) return null
const sprite = new Snow({
positionX: typeof preferredX === 'number' ? preferredX : Math.random() * viewportWidth,
positionY: typeof preferredY === 'number' ? preferredY : (-1 - Math.random() * 4),
radius: (Math.random() * (settings.radiusMax - settings.radiusMin) + settings.radiusMin) * settings.radiusMin,
driftSpeed: Math.random() * (settings.driftMax - settings.driftMin) + settings.driftMin,
swingAmplitude: (Math.random() * (settings.swingMax - settings.swingMin) + settings.swingMin) * settings.swingMin,
shapeType: picked.type,
imageUrl: picked.url,
emojiText: picked.text
})
sprite.addComponent(new DownwardMoveComponent())
sprite.addComponent(new SwingComponent())
sprite.addComponent(new LifetimeComponent())
sprite.init(engine)
return sprite
}
function computeAverageVerticalSpeed(): number {
let count = 0
let sum = 0
for (let i = 0; i < snowflakes.length; i++){
const f = snowflakes[i]
if (f.outOfView) continue
const v = (f.driftSpeed * 2 + f.radius * 0.25) * 60
sum += v
count++
}
if (count > 0) return sum / count
const driftAvg = (settings.driftMin + settings.driftMax) * 0.5
const radiusAvg = ((settings.radiusMin + settings.radiusMax) * 0.5) * settings.radiusMin
return (driftAvg * 2 + radiusAvg * 0.25) * 60
}
function estimateInitialPrefillCount(): number {
const avgV = computeAverageVerticalSpeed()
const lifeSec = (viewportHeight + 5) / Math.max(0.001, avgV)
const supplyPerSec = targetCount / Math.max(0.001, lifeSec)
const warmSec = Math.min(1.2, Math.max(0.6, lifeSec * 0.2))
const raw = Math.floor(supplyPerSec * warmSec)
const cap = Math.floor(targetCount * 0.45)
return Math.max(8, Math.min(cap, raw))
}
function pushSnowflake(px?: number, py?: number){
const flake = createSnowflake(px, py)
if (flake){ snowflakes.push(flake); return true }
return false
}
function initSnowPrefill(){
if (hasReachedDuration) return
if (snowflakes.length > 0) return
const initial = estimateInitialPrefillCount()
for (let i = 0; i < initial; i++){
const px = (spawnPhase % 1) * viewportWidth
spawnPhase = (spawnPhase + goldenRatio) % 1
const py = -Math.random() * (viewportHeight * 0.4)
pushSnowflake(px, py)
}
}
initSnowPrefill()
function updateSnowflakes(dt: number){
for (let i = 0; i < snowflakes.length; i++){
const f = snowflakes[i]
if (f && typeof (f as any).update === 'function'){
f.update(engine, dt)
} else {
const factor = Math.max(0.5, Math.min(2.0, dt * 60))
f.positionY += (f.driftSpeed * 2 + f.radius * 0.25) * factor
f.positionX += Math.sin(f.positionY * 0.01) * f.swingAmplitude * factor
if (f.positionY > viewportHeight + 5){ f.outOfView = true }
}
}
}
function drawSnowflakes(){
context.clearRect(0, 0, viewportWidth, viewportHeight)
for (let i = 0; i < snowflakes.length; i++){
const f = snowflakes[i]
if (f.outOfView) continue
(f as any).render ? (f as any).render(engine) : null
if (!(f as any).render){
const reg = window.YooneSnowShapeRenderers || {}
const renderer = reg[f.shapeType] || reg['dot']
if (typeof renderer === 'function'){
renderer(context, f.positionX, f.positionY, f.radius)
}
}
}
}
function updateSystem(dt: number){
for (let i = snowflakes.length - 1; i >= 0; i--){ if (snowflakes[i].outOfView) snowflakes.splice(i, 1) }
if (!hasReachedDuration){
const avgV = computeAverageVerticalSpeed()
const lifeSec = (viewportHeight + 5) / Math.max(0.001, avgV)
const supplyPerSec = targetCount / Math.max(0.001, lifeSec)
spawnAccumulator += supplyPerSec * Math.max(0, dt)
const slots = Math.max(0, targetCount - snowflakes.length)
let count = Math.min(slots, Math.floor(spawnAccumulator))
if (count === 0 && slots > 0) count = 1
const maxPerFrame = Math.max(1, Math.floor(targetCount * 0.05))
if (count > maxPerFrame) count = maxPerFrame
let added = 0
for (let s = 0; s < count; s++){
const px = (spawnPhase % 1) * viewportWidth
spawnPhase = (spawnPhase + goldenRatio) % 1
if (pushSnowflake(px, undefined)) added++
}
if (added > 0){ spawnAccumulator = Math.max(0, spawnAccumulator - added) }
}
updateSnowflakes(dt)
}
function renderSystem(){ drawSnowflakes() }
function shouldStop(): boolean {
if (settings.displayDurationSeconds > 0 && !hasReachedDuration){
const elapsed = (performance.now() - startTs) / 1000
if (elapsed >= settings.displayDurationSeconds){ hasReachedDuration = true }
}
if (hasReachedDuration){ return snowflakes.every(f => f.outOfView) }
return false
}
return {
engine,
recomputeTarget(){ targetCount = computeTargetCount() },
updateSystem,
renderSystem,
shouldStop,
setViewport(width: number, height: number){ viewportWidth = width; viewportHeight = height }
}
}

72
lib/src/utils/image.ts Normal file
View File

@ -0,0 +1,72 @@
import type { ShapeRenderer } from '../global'
declare global {
interface Window {
YooneSnowImageCache?: Record<string, { img: HTMLImageElement | ImageBitmap | null, ready: boolean }>
YooneSnowGetOrLoadImage?: (url: string) => { img: HTMLImageElement | ImageBitmap | null, ready: boolean }
YooneSnowLoadAssetViaFetch?: (url: string, cb: (ok: boolean) => void) => void
YooneSnowDrawCenteredImage?: (ctx: CanvasRenderingContext2D, img: HTMLImageElement | ImageBitmap, cx: number, cy: number, w: number, h: number) => void
}
}
export function getOrLoadImage(url: string): { img: HTMLImageElement | ImageBitmap | null, ready: boolean } {
if (!url) return { img: null, ready: false }
const cache = (window.YooneSnowImageCache = window.YooneSnowImageCache || {})
const existing = cache[url]
if (existing && existing.ready) return existing
if (existing && !existing.ready) return existing
const img = new Image()
const record = { img, ready: false }
cache[url] = record
try { ;(img as any).decoding = 'async' } catch {}
try { ;(img as any).fetchPriority = 'low' } catch {}
img.onload = function(){ record.ready = true }
img.onerror = function(){ delete cache[url] }
img.src = url
return record
}
export function loadAssetViaFetch(url: string, onReady: (ok: boolean) => void): void {
if (!url) { onReady(false); return }
const cache = (window.YooneSnowImageCache = window.YooneSnowImageCache || {})
const existing = cache[url]
if (existing && existing.ready) { onReady(true); return }
if (!existing) cache[url] = { img: null, ready: false }
if (typeof fetch === 'function' && typeof (window as any).createImageBitmap === 'function') {
fetch(url, { cache: 'force-cache' }).then(r => r.blob()).then(b => (window as any).createImageBitmap(b)).then((bmp: ImageBitmap) => {
cache[url] = { img: bmp, ready: true }
onReady(true)
}).catch(() => {
const rec = getOrLoadImage(url)
let fired = false
if (rec && rec.img instanceof HTMLImageElement) {
const markReady = () => { rec.ready = true }
rec.img.onload = function(){ if (!fired){ fired = true; markReady(); onReady(true) } }
rec.img.onerror = function(){ if (!fired){ fired = true; onReady(false) } }
} else {
onReady(false)
}
})
} else {
const rec = getOrLoadImage(url)
let fired = false
if (rec && rec.img instanceof HTMLImageElement) {
const markReady = () => { rec.ready = true }
rec.img.onload = function(){ if (!fired){ fired = true; markReady(); onReady(true) } }
rec.img.onerror = function(){ if (!fired){ fired = true; onReady(false) } }
} else {
onReady(false)
}
}
}
export function drawCenteredImage(context: CanvasRenderingContext2D, img: HTMLImageElement | ImageBitmap, centerX: number, centerY: number, width: number, height: number): void {
const dx = centerX - width / 2
const dy = centerY - height / 2
context.drawImage(img as any, dx, dy, width, height)
}
// 挂载到 window 以兼容旧逻辑
window.YooneSnowGetOrLoadImage = getOrLoadImage
window.YooneSnowLoadAssetViaFetch = loadAssetViaFetch
window.YooneSnowDrawCenteredImage = drawCenteredImage

16
lib/tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"noImplicitAny": true,
"moduleResolution": "Bundler",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM"],
"types": []
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

31
lib/vite.config.ts Normal file
View File

@ -0,0 +1,31 @@
import { defineConfig } from 'vite'
export default defineConfig({
root: '.',
publicDir: false,
build: {
outDir: '../dist',
assetsDir: 'assets',
emptyOutDir: true,
rollupOptions: {
input: {
snow: 'src/main.ts',
index: 'src/index.ts',
'assets/圣诞雪帽': 'src/assets/圣诞雪帽.svg',
'assets/圣诞拐杖': 'src/assets/圣诞拐杖.svg',
'assets/圣诞袜子': 'src/assets/圣诞袜子.svg',
'assets/圣诞树': 'src/assets/圣诞树.svg',
'assets/圣诞麋鹿': 'src/assets/圣诞麋鹿.svg',
'assets/圣诞果': 'src/assets/圣诞果.svg'
},
output: {
entryFileNames: (chunk) => chunk.name === 'snow' ? 'snow.js' : '[name].js',
assetFileNames: (info) => {
const name = info.name || ''
if (name.endsWith('.css')) return 'snow.css'
return 'assets/[name][extname]'
}
}
}
}
})

File diff suppressed because it is too large Load Diff