feat(雪花效果): 增加最大雪花数量配置并优化生成逻辑
添加最大雪花数量配置选项,支持自动计算或手动设置上限 重构雪花生成逻辑,使用时间积累方式平滑新增 增加初始化预填充功能,避免开场过空 优化性能,限制每帧最大生成数量
This commit is contained in:
parent
3cd9cfa987
commit
3a41a164d3
|
|
@ -62,6 +62,7 @@
|
||||||
const swingMin = isNaN(swingMinRaw) ? 0.2 : Math.max(0, swingMinRaw);
|
const swingMin = isNaN(swingMinRaw) ? 0.2 : Math.max(0, swingMinRaw);
|
||||||
const swingMax = isNaN(swingMaxRaw) ? 1.0 : Math.max(swingMin, swingMaxRaw);
|
const swingMax = isNaN(swingMaxRaw) ? 1.0 : Math.max(swingMin, swingMaxRaw);
|
||||||
|
|
||||||
|
// 函数 调整画布尺寸并设置像素比 保证清晰显示
|
||||||
function resizeCanvas(){
|
function resizeCanvas(){
|
||||||
viewportWidth = window.innerWidth;
|
viewportWidth = window.innerWidth;
|
||||||
viewportHeight = window.innerHeight;
|
viewportHeight = window.innerHeight;
|
||||||
|
|
@ -76,8 +77,20 @@
|
||||||
|
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
|
|
||||||
const snowflakesTargetCount = Math.floor(Math.min(400, Math.max(120, (viewportWidth * viewportHeight) / 12000)));
|
// 读取在屏最大数量设置 0 表示自动根据视口面积
|
||||||
|
const maxCountRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.maxCount !== 'undefined')
|
||||||
|
? parseInt(window.YooneSnowSettings.maxCount, 10)
|
||||||
|
: 0;
|
||||||
|
const computedAutoCount = Math.floor(Math.min(400, Math.max(120, (viewportWidth * viewportHeight) / 12000)));
|
||||||
|
const snowflakesTargetCount = Math.max(1, (isNaN(maxCountRaw) || maxCountRaw <= 0) ? computedAutoCount : maxCountRaw);
|
||||||
const snowflakes = [];
|
const snowflakes = [];
|
||||||
|
// 定义连续生成控制参数 使用时间积累的方式平滑新增
|
||||||
|
let spawnAccumulator = 0;
|
||||||
|
let lastUpdateTimestamp = performance.now();
|
||||||
|
// 定义黄金比例相位用于水平位置分布避免聚集
|
||||||
|
let spawnPhase = Math.random();
|
||||||
|
const goldenRatio = 0.61803398875;
|
||||||
|
// 函数 按权重选择形状或媒体图像
|
||||||
function selectWeightedItem(){
|
function selectWeightedItem(){
|
||||||
const items = [];
|
const items = [];
|
||||||
for (let sIndex = 0; sIndex < selectedShapes.length; sIndex++){
|
for (let sIndex = 0; sIndex < selectedShapes.length; sIndex++){
|
||||||
|
|
@ -95,6 +108,7 @@
|
||||||
items.push({ kind: 'media', url: mediaUrl, weight: finalMediaWeight });
|
items.push({ kind: 'media', url: mediaUrl, weight: finalMediaWeight });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 条件判断 如果没有可选项则回退为点形状
|
||||||
if (items.length === 0){
|
if (items.length === 0){
|
||||||
return { type: 'dot', url: null };
|
return { type: 'dot', url: null };
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +120,7 @@
|
||||||
let acc = 0;
|
let acc = 0;
|
||||||
for (let i = 0; i < items.length; i++){
|
for (let i = 0; i < items.length; i++){
|
||||||
acc += items[i].weight;
|
acc += items[i].weight;
|
||||||
|
// 条件判断 如果随机值落在当前累计权重内则选择该项
|
||||||
if (r <= acc){
|
if (r <= acc){
|
||||||
if (items[i].kind === 'shape'){
|
if (items[i].kind === 'shape'){
|
||||||
return { type: items[i].key, url: null };
|
return { type: items[i].key, url: null };
|
||||||
|
|
@ -116,13 +131,13 @@
|
||||||
}
|
}
|
||||||
return { type: 'dot', url: null };
|
return { type: 'dot', url: null };
|
||||||
}
|
}
|
||||||
function createSnowflake(){
|
function createSnowflake(preferredX, preferredY){
|
||||||
const picked = selectWeightedItem();
|
const picked = selectWeightedItem();
|
||||||
let chosenType = picked.type;
|
let chosenType = picked.type;
|
||||||
let chosenImageUrl = picked.url;
|
let chosenImageUrl = picked.url;
|
||||||
return {
|
return {
|
||||||
positionX: Math.random() * viewportWidth,
|
positionX: typeof preferredX === 'number' ? preferredX : Math.random() * viewportWidth,
|
||||||
positionY: -5 - Math.random() * 20,
|
positionY: typeof preferredY === 'number' ? preferredY : (-1 - Math.random() * 4),
|
||||||
// 半径使用随机范围并乘以最小半径作为缩放因子
|
// 半径使用随机范围并乘以最小半径作为缩放因子
|
||||||
radius: (Math.random() * (radiusMax - radiusMin) + radiusMin) * radiusMin,
|
radius: (Math.random() * (radiusMax - radiusMin) + radiusMin) * radiusMin,
|
||||||
driftSpeed: Math.random() * (driftMax - driftMin) + driftMin,
|
driftSpeed: Math.random() * (driftMax - driftMin) + driftMin,
|
||||||
|
|
@ -134,33 +149,80 @@
|
||||||
outOfView: false
|
outOfView: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const spawnIntervalMs = Math.max(10, Math.floor(4000 / Math.max(1, snowflakesTargetCount)));
|
// 计算平均垂直速度 辅助估算生成速率 保证视觉连续
|
||||||
let nextSpawnTimestamp = performance.now();
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 函数 根据视口高度 目标最大数量 与平均速度估算初始化预填充数量
|
||||||
|
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(2.0, Math.max(0.8, averageLifeSeconds * 0.25));
|
||||||
|
// 初始預填充數量 取補給速率乘以預熱時長 並限制在合理範圍
|
||||||
|
const rawCount = Math.floor(supplyRatePerSecond * warmupSeconds);
|
||||||
|
const boundedCount = Math.min(snowflakesTargetCount, Math.max(10, rawCount));
|
||||||
|
return boundedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 函数 封装添加雪花的操作 支持指定水平与垂直位置
|
||||||
|
function pushSnowflake(preferredX, preferredY){
|
||||||
|
snowflakes.push(createSnowflake(preferredX, preferredY));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 函数 初始化预填充雪花 保持从顶部下落的感觉并避免过量
|
||||||
|
function initSnowPrefill(){
|
||||||
|
// 条件判断 仅在未达到时长且当前未生成过雪花时执行
|
||||||
|
if (hasReachedDuration){ return; }
|
||||||
|
if (snowflakes.length > 0){ return; }
|
||||||
|
const initialCount = estimateInitialPrefillCount();
|
||||||
|
for (let spawnIndex = 0; spawnIndex < initialCount; spawnIndex++){
|
||||||
|
// 使用黄金比例相位分布水平位置 减少聚集
|
||||||
|
const preferredX = (spawnPhase % 1) * viewportWidth;
|
||||||
|
spawnPhase = (spawnPhase + goldenRatio) % 1;
|
||||||
|
// 仅在顶部附近生成 保持从顶部下落的视觉感受
|
||||||
|
const preferredY = -3 - Math.random() * 6;
|
||||||
|
pushSnowflake(preferredX, preferredY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 調用初始化預填充
|
||||||
|
initSnowPrefill();
|
||||||
|
|
||||||
|
// 函数 更新雪花位置与视口状态
|
||||||
function updateSnowflakes(){
|
function updateSnowflakes(){
|
||||||
for (let j = 0; j < snowflakes.length; j++){
|
for (let j = 0; j < snowflakes.length; j++){
|
||||||
const flake = snowflakes[j];
|
const flake = snowflakes[j];
|
||||||
flake.positionY += flake.driftSpeed * 2 + flake.radius * 0.25;
|
flake.positionY += flake.driftSpeed * 2 + flake.radius * 0.25;
|
||||||
flake.positionX += Math.sin(flake.positionY * 0.01) * flake.swingAmplitude;
|
flake.positionX += Math.sin(flake.positionY * 0.01) * flake.swingAmplitude;
|
||||||
|
// 条件判断 如果超出视口底部则标记为已移出
|
||||||
if (flake.positionY > viewportHeight + 5){
|
if (flake.positionY > viewportHeight + 5){
|
||||||
// 条件判断 如果尚未达到时长则重生 如果已达到则标记为移出视口
|
|
||||||
if (!hasReachedDuration){
|
|
||||||
flake.positionY = -5;
|
|
||||||
flake.positionX = Math.random() * viewportWidth;
|
|
||||||
// 重生时应用同样的半径缩放逻辑
|
|
||||||
flake.radius = (Math.random() * (radiusMax - radiusMin) + radiusMin) * radiusMin;
|
|
||||||
flake.driftSpeed = Math.random() * (driftMax - driftMin) + driftMin;
|
|
||||||
// 重生时应用同样的摆动缩放逻辑
|
|
||||||
flake.swingAmplitude = (Math.random() * (swingMax - swingMin) + swingMin) * swingMin;
|
|
||||||
} else {
|
|
||||||
flake.outOfView = true;
|
flake.outOfView = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 使用全局渲染注册表 根据形状类型选择渲染函数
|
// 使用全局渲染注册表 根据形状类型选择渲染函数
|
||||||
|
|
||||||
|
// 函数 清空画布并绘制可见雪花
|
||||||
function drawSnowflakes(){
|
function drawSnowflakes(){
|
||||||
context.clearRect(0, 0, viewportWidth, viewportHeight);
|
context.clearRect(0, 0, viewportWidth, viewportHeight);
|
||||||
|
|
||||||
|
|
@ -181,6 +243,7 @@
|
||||||
// 否则执行注册表中的形状渲染函数
|
// 否则执行注册表中的形状渲染函数
|
||||||
const registry = window.YooneSnowShapeRenderers || {};
|
const registry = window.YooneSnowShapeRenderers || {};
|
||||||
const renderer = registry[flake.shapeType] || registry['dot'];
|
const renderer = registry[flake.shapeType] || registry['dot'];
|
||||||
|
// 条件判断 只有在渲染器为函数时才执行绘制
|
||||||
if (typeof renderer === 'function'){
|
if (typeof renderer === 'function'){
|
||||||
renderer(context, flake.positionX, flake.positionY, flake.radius);
|
renderer(context, flake.positionX, flake.positionY, flake.radius);
|
||||||
}
|
}
|
||||||
|
|
@ -198,16 +261,50 @@
|
||||||
canvas.style.display = 'none';
|
canvas.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate(){
|
// 函数 更新系统 执行移除与补给并更新位置
|
||||||
if (!hasReachedDuration && snowflakes.length < snowflakesTargetCount){
|
function updateSystem(deltaSeconds){
|
||||||
const nowTs = performance.now();
|
for (let idx = snowflakes.length - 1; idx >= 0; idx--){
|
||||||
while (snowflakes.length < snowflakesTargetCount && nowTs >= nextSpawnTimestamp){
|
// 条件判断 仅当雪花已移出视口时才从数组移除 保证雪花下到窗口底部才消失
|
||||||
snowflakes.push(createSnowflake());
|
if (snowflakes[idx].outOfView){ snowflakes.splice(idx, 1); }
|
||||||
nextSpawnTimestamp += spawnIntervalMs;
|
}
|
||||||
|
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){
|
||||||
|
spawnAccumulator = Math.max(0, spawnAccumulator - spawnCount);
|
||||||
|
for (let s = 0; s < spawnCount; s++){
|
||||||
|
const preferredX = (spawnPhase % 1) * viewportWidth;
|
||||||
|
spawnPhase = (spawnPhase + goldenRatio) % 1;
|
||||||
|
pushSnowflake(preferredX, undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateSnowflakes();
|
updateSnowflakes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 函数 渲染系统 清空画布并绘制
|
||||||
|
function renderSystem(){
|
||||||
drawSnowflakes();
|
drawSnowflakes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 函数 动画主循环 包含生成 渲染 与停止逻辑
|
||||||
|
function animate(){
|
||||||
|
const nowTs = performance.now();
|
||||||
|
const deltaSeconds = Math.max(0, (nowTs - lastUpdateTimestamp) / 1000);
|
||||||
|
lastUpdateTimestamp = nowTs;
|
||||||
|
updateSystem(deltaSeconds);
|
||||||
|
renderSystem();
|
||||||
// 条件判断 如果设置了有限时长则在达到时长后不再生成新粒子并等待自然落出
|
// 条件判断 如果设置了有限时长则在达到时长后不再生成新粒子并等待自然落出
|
||||||
if (displayDurationSeconds > 0 && !hasReachedDuration){
|
if (displayDurationSeconds > 0 && !hasReachedDuration){
|
||||||
const elapsedSeconds = (performance.now() - startTimestamp) / 1000;
|
const elapsedSeconds = (performance.now() - startTimestamp) / 1000;
|
||||||
|
|
@ -223,6 +320,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 调用动画循环 保持持续更新
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
/*
|
/*
|
||||||
Plugin Name: Yoone Snow
|
Plugin Name: Yoone Snow
|
||||||
Description: 首页 canvas 雪花效果
|
Description: 首页 canvas 雪花效果
|
||||||
Version: 1.5.0
|
Version: 1.6.0
|
||||||
Author: Yoone
|
Author: Yoone
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -125,6 +125,7 @@ function yoone_snow_enqueue_assets() {
|
||||||
'selectedShapes' => $mixed_items_sanitized,
|
'selectedShapes' => $mixed_items_sanitized,
|
||||||
'mediaItems' => $media_urls,
|
'mediaItems' => $media_urls,
|
||||||
'displayDurationSeconds' => intval(get_option('yoone_snow_home_duration', 0)),
|
'displayDurationSeconds' => intval(get_option('yoone_snow_home_duration', 0)),
|
||||||
|
'maxCount' => intval(get_option('yoone_snow_max_count', 0)),
|
||||||
'radiusMin' => $radius_min_val,
|
'radiusMin' => $radius_min_val,
|
||||||
'radiusMax' => $radius_max_val,
|
'radiusMax' => $radius_max_val,
|
||||||
'driftMin' => floatval(get_option('yoone_snow_drift_min', 0.4)),
|
'driftMin' => floatval(get_option('yoone_snow_drift_min', 0.4)),
|
||||||
|
|
@ -426,6 +427,32 @@ function yoone_snow_register_settings() {
|
||||||
'yoone_snow_section'
|
'yoone_snow_section'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 注册目标在屏最大数量设置项 0 表示自动根据视口面积
|
||||||
|
register_setting('yoone_snow_options', 'yoone_snow_max_count', array(
|
||||||
|
'type' => 'integer',
|
||||||
|
'sanitize_callback' => function($value) {
|
||||||
|
$num = intval($value);
|
||||||
|
if ($num < 0) { $num = 0; }
|
||||||
|
// 上限保护 防止过大影响性能
|
||||||
|
if ($num > 1000) { $num = 1000; }
|
||||||
|
return $num;
|
||||||
|
},
|
||||||
|
'default' => 0,
|
||||||
|
));
|
||||||
|
|
||||||
|
// 添加输入字段 用于设置在屏最大数量 0 表示自动
|
||||||
|
add_settings_field(
|
||||||
|
'yoone_snow_max_count',
|
||||||
|
'Max Snowflakes On Screen',
|
||||||
|
function() {
|
||||||
|
$current = intval(get_option('yoone_snow_max_count', 0));
|
||||||
|
echo '<input type="number" min="0" step="1" name="yoone_snow_max_count" value="' . esc_attr($current) . '" style="width:120px;" />';
|
||||||
|
echo '<p class="description">0 means auto based on viewport area upper bound 1000</p>';
|
||||||
|
},
|
||||||
|
'yoone_snow',
|
||||||
|
'yoone_snow_section'
|
||||||
|
);
|
||||||
|
|
||||||
// 尺寸组合设置 使用单一选项 snow size 存储最小与最大半径
|
// 尺寸组合设置 使用单一选项 snow size 存储最小与最大半径
|
||||||
register_setting('yoone_snow_options', 'yoone_snow_size', array(
|
register_setting('yoone_snow_options', 'yoone_snow_size', array(
|
||||||
'type' => 'array',
|
'type' => 'array',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue