feat(雪花效果): 新增多种圣诞主题雪花形状和媒体选择功能

新增了多种圣诞主题的雪花形状渲染函数,包括硬币、麋鹿、圣诞帽等
添加了媒体选择功能,支持上传图片作为雪花形状
重构了雪花渲染逻辑,使用全局注册表管理形状渲染函数
更新了.gitignore文件,添加release目录忽略
新增了媒体管理相关的JavaScript文件和SVG资源文件
This commit is contained in:
tikkhun 2025-12-11 10:10:07 +08:00
parent a95b3e2362
commit 1415a37dcb
22 changed files with 574 additions and 97 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
.DS_Store .DS_Store
release

1
assets/圣诞拐杖.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.2 KiB

1
assets/圣诞果.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

1
assets/圣诞树.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

1
assets/圣诞袜子.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

1
assets/圣诞雪帽.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

1
assets/圣诞麋鹿.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

74
js/admin-media.js Normal file
View File

@ -0,0 +1,74 @@
(function(){
// 后台媒体选择交互脚本 用于在设置页选择媒体项目
function initAdminMedia(){
// 条件判断 如果没有媒体按钮则不执行
var addButton = document.getElementById('yooneSnowAddMedia');
var listContainer = document.getElementById('yooneSnowMediaList');
if (!addButton || !listContainer) { return; }
// 绑定移除按钮事件 使用事件委托处理动态元素
listContainer.addEventListener('click', function(event){
// 条件判断 如果点击的是移除按钮则执行删除
var target = event.target;
if (target && target.classList.contains('yoone-snow-remove-media')){
var item = target.closest('.yoone-snow-media-item');
if (item) { item.remove(); }
}
});
// 打开媒体选择器 支持多选 图片和 SVG
addButton.addEventListener('click', function(){
// 条件判断 如果 wp.media 不可用则终止
if (typeof wp === 'undefined' || !wp.media) { return; }
var frame = wp.media({
title: 'Select images or SVG',
multiple: true,
library: { type: [ 'image', 'image/svg+xml' ] }
});
frame.on('select', function(){
var selection = frame.state().get('selection');
selection.each(function(attachment){
var data = attachment.toJSON();
var id = data.id;
var url = data.sizes && data.sizes.thumbnail ? data.sizes.thumbnail.url : data.url;
// 创建预览项与隐藏输入 保存附件 ID
var wrapper = document.createElement('div');
wrapper.className = 'yoone-snow-media-item';
wrapper.setAttribute('data-attachment-id', String(id));
wrapper.style.border = '1px solid #ddd';
wrapper.style.padding = '8px';
wrapper.style.display = 'flex';
wrapper.style.flexDirection = 'column';
wrapper.style.alignItems = 'center';
var img = document.createElement('img');
img.src = url;
img.alt = 'media';
img.style.width = '72px';
img.style.height = '72px';
img.style.objectFit = 'contain';
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'yoone_snow_media_items[]';
input.value = String(id);
var removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'button yoone-snow-remove-media';
removeBtn.textContent = 'Remove';
removeBtn.style.marginTop = '6px';
wrapper.appendChild(img);
wrapper.appendChild(input);
wrapper.appendChild(removeBtn);
listContainer.appendChild(wrapper);
});
});
frame.open();
});
}
// 条件判断 如果文档尚未加载则等待 DOMContentLoaded 事件
if (document.readyState === 'loading'){
document.addEventListener('DOMContentLoaded', initAdminMedia);
} else {
initAdminMedia();
}
})();

15
js/shapes/candy_cane.js Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
(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);
};
})();

24
js/shapes/coin.js Normal file
View File

@ -0,0 +1,24 @@
(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();
};
})();

12
js/shapes/dot.js Normal file
View File

@ -0,0 +1,12 @@
(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();
};
})();

35
js/shapes/flake.js Normal file
View File

@ -0,0 +1,35 @@
(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();
};
})();

7
js/shapes/index.js Normal file
View File

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

15
js/shapes/reindeer.js Normal file
View File

@ -0,0 +1,15 @@
(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);
};
})();

15
js/shapes/santa_hat.js Normal file
View File

@ -0,0 +1,15 @@
(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);
};
})();

44
js/shapes/utils.js Normal file
View File

@ -0,0 +1,44 @@
(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;
img.onload = function(){
// 加载成功 标记为可用
record.ready = true;
};
img.onerror = function(){
// 加载失败 从缓存移除避免重复错误
delete window.YooneSnowImageCache[imageUrl];
};
img.src = imageUrl;
return record;
};
// 居中绘制图像 根据目标中心点和宽高进行缩放绘制
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);
};
})();

56
js/shapes/yuanbao.js Normal file
View File

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

View File

@ -1,22 +1,28 @@
(function(){ (function(){
// 初始化函数 用于启动雪花效果 // 初始化函数 用于启动雪花效果
function init(){ function init(){
var canvas = document.getElementById('effectiveAppsSnow'); const canvas = document.getElementById('effectiveAppsSnow');
// 条件判断 如果未找到画布元素则不执行 // 条件判断 如果未找到画布元素则不执行
if (!canvas) return; if (!canvas) return;
var context = canvas.getContext('2d'); const context = canvas.getContext('2d');
var viewportWidth = window.innerWidth; let viewportWidth = window.innerWidth;
var viewportHeight = window.innerHeight; let viewportHeight = window.innerHeight;
var devicePixelRatio = window.devicePixelRatio || 1; const devicePixelRatio = window.devicePixelRatio || 1;
// 从后端配置读取雪花形状 默认值为 dot // 读取选中的形状集合 简化为仅使用复选框集合
var configuredShape = (window.YooneSnowSettings && window.YooneSnowSettings.shape) ? window.YooneSnowSettings.shape : 'dot'; 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
: [];
function resizeCanvas(){ function resizeCanvas(){
viewportWidth = window.innerWidth; viewportWidth = window.innerWidth;
viewportHeight = window.innerHeight; viewportHeight = window.innerHeight;
var displayWidth = viewportWidth; const displayWidth = viewportWidth;
var displayHeight = viewportHeight; const displayHeight = viewportHeight;
canvas.style.width = displayWidth + 'px'; canvas.style.width = displayWidth + 'px';
canvas.style.height = displayHeight + 'px'; canvas.style.height = displayHeight + 'px';
canvas.width = Math.floor(displayWidth * devicePixelRatio); canvas.width = Math.floor(displayWidth * devicePixelRatio);
@ -26,16 +32,22 @@
resizeCanvas(); resizeCanvas();
var snowflakeCount = Math.floor(Math.min(400, Math.max(120, (viewportWidth * viewportHeight) / 12000))); const snowflakeCount = Math.floor(Math.min(400, Math.max(120, (viewportWidth * viewportHeight) / 12000)));
var snowflakes = []; const snowflakes = [];
for (var i = 0; i < snowflakeCount; i++){ for (let i = 0; i < snowflakeCount; i++){
// 为每个雪花确定形状类型 如果是 mixed 则随机赋值 dot 或 flake // 为每个雪花确定形状类型 从选中集合与媒体列表的合并池中随机选择
var snowflakeShapeType; const poolSize = selectedShapes.length + mediaItems.length;
if (configuredShape === 'mixed') { const randomIndex = Math.floor(Math.random() * Math.max(1, poolSize));
// 条件判断 随机选择雪花形状 let chosenType = 'dot';
snowflakeShapeType = Math.random() < 0.5 ? 'dot' : 'flake'; let chosenImageUrl = null;
} else { if (randomIndex < selectedShapes.length) {
snowflakeShapeType = configuredShape; // 条件判断 使用普通形状类型
chosenType = selectedShapes[randomIndex];
} else if (mediaItems.length > 0) {
// 条件判断 使用媒体图片作为形状 并记录其 URL
const mediaIndex = randomIndex - selectedShapes.length;
chosenType = 'media_image';
chosenImageUrl = mediaItems[mediaIndex % mediaItems.length];
} }
snowflakes.push({ snowflakes.push({
positionX: Math.random() * viewportWidth, positionX: Math.random() * viewportWidth,
@ -43,13 +55,14 @@
radius: Math.random() * 2 + 1, radius: Math.random() * 2 + 1,
driftSpeed: Math.random() * 0.6 + 0.4, driftSpeed: Math.random() * 0.6 + 0.4,
swingAmplitude: Math.random() * 0.8 + 0.2, swingAmplitude: Math.random() * 0.8 + 0.2,
shapeType: snowflakeShapeType shapeType: chosenType,
imageUrl: chosenImageUrl
}); });
} }
function updateSnowflakes(){ function updateSnowflakes(){
for (var j = 0; j < snowflakes.length; j++){ for (let j = 0; j < snowflakes.length; j++){
var 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){
@ -62,60 +75,28 @@
} }
} }
// 绘制雪花形状 使用线段绘制六角雪花 // 使用全局渲染注册表 根据形状类型选择渲染函数
function drawSnowflake(x, y, size) {
context.save();
context.translate(x, y);
context.fillStyle = 'rgba(255,255,255,0.9)';
context.strokeStyle = 'rgba(255,255,255,0.9)';
context.lineWidth = size * 0.15;
// 循环绘制六个分支
for (var i = 0; i < 6; i++) {
context.rotate(Math.PI / 3);
context.beginPath();
context.moveTo(0, 0);
context.lineTo(0, size);
context.stroke();
// 绘制分支上的小分叉
context.beginPath();
context.moveTo(0, size * 0.3);
context.lineTo(size * 0.3, size * 0.5);
context.stroke();
context.beginPath();
context.moveTo(0, size * 0.5);
context.lineTo(-size * 0.3, size * 0.7);
context.stroke();
context.beginPath();
context.moveTo(0, size * 0.7);
context.lineTo(size * 0.3, size * 0.9);
context.stroke();
}
context.restore();
}
// 绘制圆点形状 使用填充圆形作为雪花
function drawDot(x, y, size) {
context.beginPath();
context.arc(x, y, size, 0, Math.PI * 2);
context.fillStyle = 'rgba(255,255,255,0.9)';
context.fill();
}
function drawSnowflakes(){ function drawSnowflakes(){
context.clearRect(0, 0, viewportWidth, viewportHeight); context.clearRect(0, 0, viewportWidth, viewportHeight);
for (var k = 0; k < snowflakes.length; k++){ for (let k = 0; k < snowflakes.length; k++){
var flake = snowflakes[k]; const flake = snowflakes[k];
// 条件判断 根据形状类型绘制雪花 // 条件判断 如果是媒体图片类型则使用通用图像渲染
if (flake.shapeType === 'dot') { if (flake.shapeType === 'media_image' && flake.imageUrl){
drawDot(flake.positionX, flake.positionY, flake.radius); const record = window.YooneSnowGetOrLoadImage ? window.YooneSnowGetOrLoadImage(flake.imageUrl) : { img: null, ready: false };
} else { if (record && record.ready){
drawSnowflake(flake.positionX, flake.positionY, flake.radius * 3); 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);
} }
} }
} }

View File

@ -19,22 +19,100 @@ function yoone_snow_enqueue_assets() {
wp_register_style($style_handle, $style_src, array(), '1.1.0', 'all'); wp_register_style($style_handle, $style_src, array(), '1.1.0', 'all');
wp_enqueue_style($style_handle); wp_enqueue_style($style_handle);
// 注册形状渲染脚本 保证主脚本之前加载
$shape_index_handle = 'yoone-snow-shapes-index';
wp_register_script($shape_index_handle, plugins_url('js/shapes/index.js', __FILE__), array(), '1.1.0', true);
$shape_utils_handle = 'yoone-snow-shapes-utils';
wp_register_script($shape_utils_handle, plugins_url('js/shapes/utils.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$shape_dot_handle = 'yoone-snow-shapes-dot';
wp_register_script($shape_dot_handle, plugins_url('js/shapes/dot.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$shape_flake_handle = 'yoone-snow-shapes-flake';
wp_register_script($shape_flake_handle, plugins_url('js/shapes/flake.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$shape_yuanbao_handle = 'yoone-snow-shapes-yuanbao';
wp_register_script($shape_yuanbao_handle, plugins_url('js/shapes/yuanbao.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$shape_coin_handle = 'yoone-snow-shapes-coin';
wp_register_script($shape_coin_handle, plugins_url('js/shapes/coin.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$shape_santa_handle = 'yoone-snow-shapes-santa-hat';
wp_register_script($shape_santa_handle, plugins_url('js/shapes/santa_hat.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$shape_cane_handle = 'yoone-snow-shapes-candy-cane';
wp_register_script($shape_cane_handle, plugins_url('js/shapes/candy_cane.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$shape_sock_handle = 'yoone-snow-shapes-christmas-sock';
wp_register_script($shape_sock_handle, plugins_url('js/shapes/christmas_sock.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$shape_tree_handle = 'yoone-snow-shapes-christmas-tree';
wp_register_script($shape_tree_handle, plugins_url('js/shapes/christmas_tree.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$shape_reindeer_handle = 'yoone-snow-shapes-reindeer';
wp_register_script($shape_reindeer_handle, plugins_url('js/shapes/reindeer.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$shape_berry_handle = 'yoone-snow-shapes-christmas-berry';
wp_register_script($shape_berry_handle, plugins_url('js/shapes/christmas_berry.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
// 注册并加载主脚本 设置依赖确保顺序正确
$script_handle = 'yoone-snow-script'; $script_handle = 'yoone-snow-script';
$script_src = plugins_url('js/snow-canvas.js', __FILE__); $script_src = plugins_url('js/snow-canvas.js', __FILE__);
wp_register_script($script_handle, $script_src, array(), '1.1.0', true); wp_register_script(
$script_handle,
$script_src,
array(
$shape_index_handle,
$shape_dot_handle,
$shape_flake_handle,
$shape_yuanbao_handle,
$shape_coin_handle,
$shape_santa_handle,
$shape_cane_handle,
$shape_sock_handle,
$shape_tree_handle,
$shape_reindeer_handle,
$shape_berry_handle
),
'1.1.0',
true
);
wp_enqueue_script($script_handle); wp_enqueue_script($script_handle);
// 将后端设置传递到前端脚本 变量名称为 YooneSnowSettings // 将后端设置传递到前端脚本 变量名称为 YooneSnowSettings
$shape = get_option('yoone_snow_shape', 'dot'); // 简化设置 仅保留复选框选择的形状集合
if (!in_array($shape, array('dot', 'flake', 'mixed'), true)) { $mixed_items_option = get_option('yoone_snow_mixed_items', array('dot','flake'));
// 如果选项值不合法则回退到默认值 dot if (is_string($mixed_items_option)) {
$shape = 'dot'; $mixed_items_option = array_filter(array_map('trim', explode(',', $mixed_items_option)));
}
$allowed_shapes = array('dot','flake','yuanbao','coin','santa_hat','candy_cane','christmas_sock','christmas_tree','reindeer','christmas_berry');
$mixed_items_sanitized = array_values(array_unique(array_intersect($mixed_items_option, $allowed_shapes)));
if (empty($mixed_items_sanitized)) { $mixed_items_sanitized = array('dot','flake'); }
// 读取媒体形状集合 并映射为可用的 URL 列表
$media_ids = get_option('yoone_snow_media_items', array());
if (!is_array($media_ids)) { $media_ids = array(); }
$media_urls = array();
foreach ($media_ids as $mid) {
$url = wp_get_attachment_url(intval($mid));
if ($url) { $media_urls[] = $url; }
} }
wp_localize_script($script_handle, 'YooneSnowSettings', array( wp_localize_script($script_handle, 'YooneSnowSettings', array(
'shape' => $shape, 'selectedShapes' => $mixed_items_sanitized,
'mediaItems' => $media_urls,
// 传递资源基础映射 用于前端按需加载 SVG 图像
'assetsMap' => array(
'santa_hat' => plugins_url('assets/圣诞雪帽.svg', __FILE__),
'candy_cane' => plugins_url('assets/圣诞拐杖.svg', __FILE__),
'christmas_sock' => plugins_url('assets/圣诞袜子.svg', __FILE__),
'christmas_tree' => plugins_url('assets/圣诞树.svg', __FILE__),
'reindeer' => plugins_url('assets/圣诞麋鹿.svg', __FILE__),
'christmas_berry' => plugins_url('assets/圣诞果.svg', __FILE__),
),
)); ));
} }
// 在后台设置页面加载媒体库脚本和交互脚本 用于选择 SVG 或图片
function yoone_snow_admin_enqueue($hook) {
// 条件判断 仅在插件设置页面加载脚本 保持性能
if ($hook !== 'settings_page_yoone_snow') { return; }
// 加载媒体库脚本 以便使用 wp.media 选择器
wp_enqueue_media();
// 注册并加载后台交互脚本
$admin_script_handle = 'yoone-snow-admin-media';
wp_register_script($admin_script_handle, plugins_url('js/admin-media.js', __FILE__), array(), '1.1.0', true);
wp_enqueue_script($admin_script_handle);
}
function yoone_snow_render_overlay() { function yoone_snow_render_overlay() {
if (!yoone_snow_is_enabled()) { return; } if (!yoone_snow_is_enabled()) { return; }
static $yoone_snow_rendered = false; static $yoone_snow_rendered = false;
@ -49,16 +127,22 @@ add_action('wp_footer', 'yoone_snow_render_overlay', 100);
// 注册设置页面和设置项 用于选择雪花形状 // 注册设置页面和设置项 用于选择雪花形状
function yoone_snow_register_settings() { function yoone_snow_register_settings() {
// 注册设置项 选项名称为 yoone_snow_shape 默认值为 dot // 移除形状下拉选项 仅保留复选集合设置
register_setting('yoone_snow_options', 'yoone_snow_shape', array(
'type' => 'string', // 注册 mixed 形状集合设置项 默认包含 dot 和 flake
register_setting('yoone_snow_options', 'yoone_snow_mixed_items', array(
'type' => 'array',
'sanitize_callback' => function($value) { 'sanitize_callback' => function($value) {
// 对提交的值进行校验 只允许 dot flake mixed 三种 // 将输入统一为数组 并过滤到允许的集合
$allowed = array('dot', 'flake', 'mixed'); $allowed = array('dot','flake','yuanbao','coin','santa_hat','candy_cane','christmas_sock','christmas_tree','reindeer','christmas_berry');
if (!is_string($value)) { return 'dot'; } if (is_string($value)) {
return in_array($value, $allowed, true) ? $value : 'dot'; $value = array_filter(array_map('trim', explode(',', $value)));
}
if (!is_array($value)) { $value = array('dot','flake'); }
$filtered = array_values(array_unique(array_intersect($value, $allowed)));
return !empty($filtered) ? $filtered : array('dot','flake');
}, },
'default' => 'dot', 'default' => array('dot','flake'),
)); ));
// 添加设置分区 标题为 Snow Settings // 添加设置分区 标题为 Snow Settings
@ -72,19 +156,81 @@ function yoone_snow_register_settings() {
'yoone_snow' 'yoone_snow'
); );
// 添加设置字段 下拉选择雪花形状 // 移除下拉字段 保留复选框作为唯一选择入口
// 添加形状复选集合 用于选择参与渲染的形状
add_settings_field( add_settings_field(
'yoone_snow_shape', 'yoone_snow_mixed_items',
'Snow Shape', 'Shapes',
function() { function() {
// 渲染选择框 选项值 dot flake mixed $current_list = get_option('yoone_snow_mixed_items', array('dot','flake'));
$current = get_option('yoone_snow_shape', 'dot'); if (is_string($current_list)) {
echo '<select name="yoone_snow_shape" id="yoone_snow_shape">'; $current_list = array_filter(array_map('trim', explode(',', $current_list)));
echo '<option value="dot"' . selected($current, 'dot', false) . '>Dot</option>'; }
echo '<option value="flake"' . selected($current, 'flake', false) . '>Snowflake</option>'; $options = array(
echo '<option value="mixed"' . selected($current, 'mixed', false) . '>Mixed</option>'; 'dot' => 'Dot',
echo '</select>'; 'flake' => 'Snowflake',
echo '<p class="description">Choose dot snowflake or mixed</p>'; 'yuanbao' => 'Yuanbao',
'coin' => 'Coin',
'santa_hat' => 'Santa Hat',
'candy_cane' => 'Candy Cane',
'christmas_sock' => 'Christmas Sock',
'christmas_tree' => 'Christmas Tree',
'reindeer' => 'Reindeer',
'christmas_berry' => 'Christmas Berry',
);
foreach ($options as $key => $label) {
$checked = in_array($key, $current_list, true) ? 'checked' : '';
echo '<label style="margin-right:12px;"><input type="checkbox" name="yoone_snow_mixed_items[]" value="' . esc_attr($key) . '" ' . $checked . ' /> ' . esc_html($label) . '</label>';
}
echo '<p class="description">Choose shapes to render</p>';
},
'yoone_snow',
'yoone_snow_section'
);
// 注册媒体形状集合 设置项保存为附件 ID 数组
register_setting('yoone_snow_options', 'yoone_snow_media_items', array(
'type' => 'array',
'sanitize_callback' => function($value) {
// 将输入统一为整数 ID 数组 并过滤无效值
if (is_string($value)) {
$value = array_filter(array_map('trim', explode(',', $value)));
}
if (!is_array($value)) { $value = array(); }
$ids = array();
foreach ($value as $item) {
$id = intval($item);
if ($id > 0) { $ids[] = $id; }
}
return array_values(array_unique($ids));
},
'default' => array(),
));
// 添加媒体形状选择字段 支持从媒体库选择图片或 SVG
add_settings_field(
'yoone_snow_media_items',
'Media Shapes',
function() {
// 读取当前已选择的附件 ID 列表 并渲染缩略图列表与添加按钮
$current_media = get_option('yoone_snow_media_items', array());
if (!is_array($current_media)) { $current_media = array(); }
echo '<div id="yooneSnowMediaList" style="display:flex;flex-wrap:wrap;gap:12px;">';
foreach ($current_media as $attachment_id) {
$url = wp_get_attachment_thumb_url($attachment_id);
if (!$url) { $url = wp_get_attachment_url($attachment_id); }
$safe_id = intval($attachment_id);
$safe_url = esc_url($url);
echo '<div class="yoone-snow-media-item" data-attachment-id="' . $safe_id . '" style="border:1px solid #ddd;padding:8px;display:flex;flex-direction:column;align-items:center;">';
echo '<img src="' . $safe_url . '" alt="media" style="width:72px;height:72px;object-fit:contain;" />';
echo '<input type="hidden" name="yoone_snow_media_items[]" value="' . $safe_id . '" />';
echo '<button type="button" class="button yoone-snow-remove-media" style="margin-top:6px;">Remove</button>';
echo '</div>';
}
echo '</div>';
echo '<p><button type="button" class="button button-primary" id="yooneSnowAddMedia">Add Images</button></p>';
echo '<p class="description">Choose images or SVG from media library to render</p>';
}, },
'yoone_snow', 'yoone_snow',
'yoone_snow_section' 'yoone_snow_section'
@ -115,6 +261,7 @@ function yoone_snow_add_settings_page() {
// 在 admin 初始化时注册设置 在 admin 菜单挂载页面 // 在 admin 初始化时注册设置 在 admin 菜单挂载页面
add_action('admin_init', 'yoone_snow_register_settings'); add_action('admin_init', 'yoone_snow_register_settings');
add_action('admin_menu', 'yoone_snow_add_settings_page'); add_action('admin_menu', 'yoone_snow_add_settings_page');
add_action('admin_enqueue_scripts', 'yoone_snow_admin_enqueue');
// 在插件列表行添加 Settings 链接 指向设置页面 // 在插件列表行添加 Settings 链接 指向设置页面
function yoone_snow_plugin_action_links($links) { function yoone_snow_plugin_action_links($links) {