251 lines
11 KiB
JavaScript
251 lines
11 KiB
JavaScript
(function(){
|
|
// 初始化函数 用于启动雪花效果
|
|
function init(){
|
|
const canvas = document.getElementById('effectiveAppsSnow');
|
|
// 条件判断 如果未找到画布元素则不执行
|
|
if (!canvas) 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
|
|
: ['dot','flake'];
|
|
const mediaItems = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.mediaItems))
|
|
? window.YooneSnowSettings.mediaItems
|
|
: [];
|
|
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 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 sizeScaleRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.sizeScale !== 'undefined')
|
|
? parseFloat(window.YooneSnowSettings.sizeScale)
|
|
: 1.0;
|
|
const offsetScaleRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.offsetScale !== 'undefined')
|
|
? parseFloat(window.YooneSnowSettings.offsetScale)
|
|
: 1.0;
|
|
const sizeScale = isNaN(sizeScaleRaw) ? 1.0 : Math.max(0.1, sizeScaleRaw);
|
|
const offsetScale = isNaN(offsetScaleRaw) ? 1.0 : Math.max(0, offsetScaleRaw);
|
|
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.0 : Math.max(0, radiusMinRaw);
|
|
const radiusMax = isNaN(radiusMaxRaw) ? 3.0 : 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);
|
|
|
|
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();
|
|
|
|
const snowflakesTargetCount = Math.floor(Math.min(400, Math.max(120, (viewportWidth * viewportHeight) / 12000)));
|
|
const snowflakes = [];
|
|
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){
|
|
items.push({ kind: 'shape', key: shapeKey, weight: weightVal });
|
|
}
|
|
}
|
|
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){
|
|
items.push({ kind: 'media', url: mediaUrl, weight: finalMediaWeight });
|
|
}
|
|
}
|
|
if (items.length === 0){
|
|
return { type: 'dot', url: 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 };
|
|
} else {
|
|
return { type: 'media_image', url: items[i].url };
|
|
}
|
|
}
|
|
}
|
|
return { type: 'dot', url: null };
|
|
}
|
|
function createSnowflake(){
|
|
const picked = selectWeightedItem();
|
|
let chosenType = picked.type;
|
|
let chosenImageUrl = picked.url;
|
|
return {
|
|
positionX: Math.random() * viewportWidth,
|
|
positionY: -5 - Math.random() * 20,
|
|
radius: (Math.random() * (radiusMax - radiusMin) + radiusMin) * sizeScale,
|
|
driftSpeed: Math.random() * (driftMax - driftMin) + driftMin,
|
|
swingAmplitude: (Math.random() * (swingMax - swingMin) + swingMin) * offsetScale,
|
|
shapeType: chosenType,
|
|
imageUrl: chosenImageUrl,
|
|
// 标记该粒子是否已经移出视口 用于停止后清理
|
|
outOfView: false
|
|
};
|
|
}
|
|
const spawnIntervalMs = Math.max(10, Math.floor(4000 / Math.max(1, snowflakesTargetCount)));
|
|
let nextSpawnTimestamp = performance.now();
|
|
|
|
function updateSnowflakes(){
|
|
for (let j = 0; j < snowflakes.length; j++){
|
|
const flake = snowflakes[j];
|
|
flake.positionY += flake.driftSpeed * 2 + flake.radius * 0.25;
|
|
flake.positionX += Math.sin(flake.positionY * 0.01) * flake.swingAmplitude;
|
|
if (flake.positionY > viewportHeight + 5){
|
|
// 条件判断 如果尚未达到时长则重生 如果已达到则标记为移出视口
|
|
if (!hasReachedDuration){
|
|
flake.positionY = -5;
|
|
flake.positionX = Math.random() * viewportWidth;
|
|
flake.radius = (Math.random() * (radiusMax - radiusMin) + radiusMin) * sizeScale;
|
|
flake.driftSpeed = Math.random() * (driftMax - driftMin) + driftMin;
|
|
flake.swingAmplitude = (Math.random() * (swingMax - swingMin) + swingMin) * offsetScale;
|
|
} else {
|
|
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.shapeType === 'media_image' && flake.imageUrl){
|
|
const record = window.YooneSnowGetOrLoadImage ? window.YooneSnowGetOrLoadImage(flake.imageUrl) : { img: null, ready: false };
|
|
if (record && record.ready){
|
|
const targetHeight = flake.radius * 8;
|
|
const targetWidth = targetHeight;
|
|
window.YooneSnowDrawCenteredImage(context, record.img, flake.positionX, flake.positionY, targetWidth, targetHeight);
|
|
}
|
|
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 animate(){
|
|
if (!hasReachedDuration && snowflakes.length < snowflakesTargetCount){
|
|
const nowTs = performance.now();
|
|
while (snowflakes.length < snowflakesTargetCount && nowTs >= nextSpawnTimestamp){
|
|
snowflakes.push(createSnowflake());
|
|
nextSpawnTimestamp += spawnIntervalMs;
|
|
}
|
|
}
|
|
updateSnowflakes();
|
|
drawSnowflakes();
|
|
// 条件判断 如果设置了有限时长则在达到时长后不再生成新粒子并等待自然落出
|
|
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){
|
|
cleanupStop();
|
|
return;
|
|
}
|
|
}
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
// 定义窗口尺寸事件处理器 以便在停止时移除
|
|
function onResize(){
|
|
// 条件判断 保证画布尺寸与视口一致
|
|
resizeCanvas();
|
|
viewportWidth = window.innerWidth;
|
|
viewportHeight = window.innerHeight;
|
|
}
|
|
window.addEventListener('resize', onResize);
|
|
|
|
animate();
|
|
}
|
|
|
|
// 条件判断 如果文档尚未加载则等待 DOMContentLoaded 事件
|
|
if (document.readyState === 'loading'){
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|