yoone-snow/js/snow-canvas.js

626 lines
29 KiB
JavaScript

(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);
}
})();