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
|
|
@ -1,2 +1,4 @@
|
|||
.DS_Store
|
||||
release
|
||||
lib/node_modules/
|
||||
wordpress-plugin/
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
|
@ -0,0 +1 @@
|
|||
import{r as a}from"./assets/SnowAnimator-Dem4j_A7.js";function i(){const t=document.getElementById("effectiveAppsSnow");if(!t)return;if(typeof window.matchMedia=="function"&&window.matchMedia("(prefers-reduced-motion: reduce)").matches){t.style.display="none";return}const e=window.YooneSnowSettings||{},n={selectedShapes:Array.isArray(e.selectedShapes)?e.selectedShapes:[],mediaItems:Array.isArray(e.mediaItems)?e.mediaItems:[],emojiItems:Array.isArray(e.emojiItems)?e.emojiItems:[],textItems:Array.isArray(e.textItems)?e.textItems:[],shapeWeights:e.shapeWeights||{},mediaWeights:e.mediaWeights||{},emojiWeights:e.emojiWeights||{},textWeights:e.textWeights||{},radiusMin:typeof e.radiusMin=="number"?e.radiusMin:1,radiusMax:typeof e.radiusMax=="number"?e.radiusMax:3,driftMin:typeof e.driftMin=="number"?e.driftMin:.4,driftMax:typeof e.driftMax=="number"?e.driftMax:1,swingMin:typeof e.swingMin=="number"?e.swingMin:.2,swingMax:typeof e.swingMax=="number"?e.swingMax:1,displayDurationSeconds:typeof e.displayDurationSeconds=="number"?e.displayDurationSeconds:0,maxCount:typeof e.maxCount=="number"?e.maxCount:0,maxCountSmall:typeof e.maxCountSmall=="number"?e.maxCountSmall:0,maxCountMedium:typeof e.maxCountMedium=="number"?e.maxCountMedium:0,maxCountLarge:typeof e.maxCountLarge=="number"?e.maxCountLarge:0,assetsMap:e.assetsMap||{}};a(t,n)}document.readyState==="complete"?i():window.addEventListener("load",i);
|
||||
|
|
@ -0,0 +1 @@
|
|||
#effectiveAppsSnow{display:block;position:fixed;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:1000}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{r as a}from"./assets/SnowAnimator-Dem4j_A7.js";function i(){const t=document.getElementById("effectiveAppsSnow");if(!t)return;if(typeof window.matchMedia=="function"&&window.matchMedia("(prefers-reduced-motion: reduce)").matches){t.style.display="none";return}const e=window.YooneSnowSettings||{},n={selectedShapes:Array.isArray(e.selectedShapes)?e.selectedShapes:[],mediaItems:Array.isArray(e.mediaItems)?e.mediaItems:[],emojiItems:Array.isArray(e.emojiItems)?e.emojiItems:[],textItems:Array.isArray(e.textItems)?e.textItems:[],shapeWeights:e.shapeWeights||{},mediaWeights:e.mediaWeights||{},emojiWeights:e.emojiWeights||{},textWeights:e.textWeights||{},radiusMin:typeof e.radiusMin=="number"?e.radiusMin:1,radiusMax:typeof e.radiusMax=="number"?e.radiusMax:3,driftMin:typeof e.driftMin=="number"?e.driftMin:.4,driftMax:typeof e.driftMax=="number"?e.driftMax:1,swingMin:typeof e.swingMin=="number"?e.swingMin:.2,swingMax:typeof e.swingMax=="number"?e.swingMax:1,displayDurationSeconds:typeof e.displayDurationSeconds=="number"?e.displayDurationSeconds:0,maxCount:typeof e.maxCount=="number"?e.maxCount:0,maxCountSmall:typeof e.maxCountSmall=="number"?e.maxCountSmall:0,maxCountMedium:typeof e.maxCountMedium=="number"?e.maxCountMedium:0,maxCountLarge:typeof e.maxCountLarge=="number"?e.maxCountLarge:0,assetsMap:e.assetsMap||{}};a(t,n)}document.readyState==="complete"?i():window.addEventListener("load",i);
|
||||
|
|
@ -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);
|
||||
};
|
||||
})();
|
||||
|
|
@ -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);
|
||||
};
|
||||
})();
|
||||
|
|
@ -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);
|
||||
};
|
||||
})();
|
||||
|
|
@ -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);
|
||||
};
|
||||
})();
|
||||
|
|
@ -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();
|
||||
};
|
||||
})();
|
||||
|
|
@ -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();
|
||||
};
|
||||
})();
|
||||
|
|
@ -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();
|
||||
};
|
||||
})();
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
(function(){
|
||||
// 初始化全局渲染注册表 用于统一管理形状渲染函数
|
||||
if (!window.YooneSnowShapeRenderers) {
|
||||
// 条件判断 如果不存在则创建空对象
|
||||
window.YooneSnowShapeRenderers = {};
|
||||
}
|
||||
})();
|
||||
|
|
@ -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);
|
||||
};
|
||||
})();
|
||||
|
|
@ -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);
|
||||
};
|
||||
})();
|
||||
|
|
@ -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);
|
||||
};
|
||||
})();
|
||||
|
|
@ -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();
|
||||
};
|
||||
})();
|
||||
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
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 system = createSystem(context, () => viewportWidth, () => viewportHeight, settings)
|
||||
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) } }
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -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{} } }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
@ -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')) }
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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]'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||