diff --git a/.gitignore b/.gitignore index e43b0f9..e693165 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +release diff --git a/assets/圣诞拐杖.svg b/assets/圣诞拐杖.svg new file mode 100644 index 0000000..b3115f8 --- /dev/null +++ b/assets/圣诞拐杖.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/圣诞果.svg b/assets/圣诞果.svg new file mode 100644 index 0000000..34dafd5 --- /dev/null +++ b/assets/圣诞果.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/圣诞树.svg b/assets/圣诞树.svg new file mode 100644 index 0000000..15db561 --- /dev/null +++ b/assets/圣诞树.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/圣诞袜子.svg b/assets/圣诞袜子.svg new file mode 100644 index 0000000..037ce21 --- /dev/null +++ b/assets/圣诞袜子.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/圣诞雪帽.svg b/assets/圣诞雪帽.svg new file mode 100644 index 0000000..bb5ea13 --- /dev/null +++ b/assets/圣诞雪帽.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/圣诞麋鹿.svg b/assets/圣诞麋鹿.svg new file mode 100644 index 0000000..2e18d37 --- /dev/null +++ b/assets/圣诞麋鹿.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/admin-media.js b/js/admin-media.js new file mode 100644 index 0000000..3dbf573 --- /dev/null +++ b/js/admin-media.js @@ -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(); + } +})(); diff --git a/js/shapes/candy_cane.js b/js/shapes/candy_cane.js new file mode 100644 index 0000000..c26ce05 --- /dev/null +++ b/js/shapes/candy_cane.js @@ -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); + }; +})(); diff --git a/js/shapes/christmas_berry.js b/js/shapes/christmas_berry.js new file mode 100644 index 0000000..6729ef5 --- /dev/null +++ b/js/shapes/christmas_berry.js @@ -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); + }; +})(); diff --git a/js/shapes/christmas_sock.js b/js/shapes/christmas_sock.js new file mode 100644 index 0000000..b41b546 --- /dev/null +++ b/js/shapes/christmas_sock.js @@ -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); + }; +})(); diff --git a/js/shapes/christmas_tree.js b/js/shapes/christmas_tree.js new file mode 100644 index 0000000..1ea18e5 --- /dev/null +++ b/js/shapes/christmas_tree.js @@ -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); + }; +})(); diff --git a/js/shapes/coin.js b/js/shapes/coin.js new file mode 100644 index 0000000..bd41b61 --- /dev/null +++ b/js/shapes/coin.js @@ -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(); + }; +})(); diff --git a/js/shapes/dot.js b/js/shapes/dot.js new file mode 100644 index 0000000..6360ec8 --- /dev/null +++ b/js/shapes/dot.js @@ -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(); + }; +})(); diff --git a/js/shapes/flake.js b/js/shapes/flake.js new file mode 100644 index 0000000..6634832 --- /dev/null +++ b/js/shapes/flake.js @@ -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(); + }; +})(); diff --git a/js/shapes/index.js b/js/shapes/index.js new file mode 100644 index 0000000..e86eaea --- /dev/null +++ b/js/shapes/index.js @@ -0,0 +1,7 @@ +(function(){ + // 初始化全局渲染注册表 用于统一管理形状渲染函数 + if (!window.YooneSnowShapeRenderers) { + // 条件判断 如果不存在则创建空对象 + window.YooneSnowShapeRenderers = {}; + } +})(); diff --git a/js/shapes/reindeer.js b/js/shapes/reindeer.js new file mode 100644 index 0000000..921e228 --- /dev/null +++ b/js/shapes/reindeer.js @@ -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); + }; +})(); diff --git a/js/shapes/santa_hat.js b/js/shapes/santa_hat.js new file mode 100644 index 0000000..c240378 --- /dev/null +++ b/js/shapes/santa_hat.js @@ -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); + }; +})(); diff --git a/js/shapes/utils.js b/js/shapes/utils.js new file mode 100644 index 0000000..d8bf255 --- /dev/null +++ b/js/shapes/utils.js @@ -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); + }; +})(); diff --git a/js/shapes/yuanbao.js b/js/shapes/yuanbao.js new file mode 100644 index 0000000..09538ca --- /dev/null +++ b/js/shapes/yuanbao.js @@ -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(); + }; +})(); diff --git a/js/snow-canvas.js b/js/snow-canvas.js index 58dc484..124fb2a 100644 --- a/js/snow-canvas.js +++ b/js/snow-canvas.js @@ -1,22 +1,28 @@ (function(){ // 初始化函数 用于启动雪花效果 function init(){ - var canvas = document.getElementById('effectiveAppsSnow'); + const canvas = document.getElementById('effectiveAppsSnow'); // 条件判断 如果未找到画布元素则不执行 if (!canvas) return; - var context = canvas.getContext('2d'); - var viewportWidth = window.innerWidth; - var viewportHeight = window.innerHeight; - var devicePixelRatio = window.devicePixelRatio || 1; + const context = canvas.getContext('2d'); + let viewportWidth = window.innerWidth; + let viewportHeight = window.innerHeight; + 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(){ viewportWidth = window.innerWidth; viewportHeight = window.innerHeight; - var displayWidth = viewportWidth; - var displayHeight = viewportHeight; + const displayWidth = viewportWidth; + const displayHeight = viewportHeight; canvas.style.width = displayWidth + 'px'; canvas.style.height = displayHeight + 'px'; canvas.width = Math.floor(displayWidth * devicePixelRatio); @@ -26,16 +32,22 @@ resizeCanvas(); - var snowflakeCount = Math.floor(Math.min(400, Math.max(120, (viewportWidth * viewportHeight) / 12000))); - var snowflakes = []; - for (var i = 0; i < snowflakeCount; i++){ - // 为每个雪花确定形状类型 如果是 mixed 则随机赋值 dot 或 flake - var snowflakeShapeType; - if (configuredShape === 'mixed') { - // 条件判断 随机选择雪花形状 - snowflakeShapeType = Math.random() < 0.5 ? 'dot' : 'flake'; - } else { - snowflakeShapeType = configuredShape; + const snowflakeCount = Math.floor(Math.min(400, Math.max(120, (viewportWidth * viewportHeight) / 12000))); + const snowflakes = []; + for (let i = 0; i < snowflakeCount; i++){ + // 为每个雪花确定形状类型 从选中集合与媒体列表的合并池中随机选择 + const poolSize = selectedShapes.length + mediaItems.length; + const randomIndex = Math.floor(Math.random() * Math.max(1, poolSize)); + let chosenType = 'dot'; + let chosenImageUrl = null; + if (randomIndex < selectedShapes.length) { + // 条件判断 使用普通形状类型 + chosenType = selectedShapes[randomIndex]; + } else if (mediaItems.length > 0) { + // 条件判断 使用媒体图片作为形状 并记录其 URL + const mediaIndex = randomIndex - selectedShapes.length; + chosenType = 'media_image'; + chosenImageUrl = mediaItems[mediaIndex % mediaItems.length]; } snowflakes.push({ positionX: Math.random() * viewportWidth, @@ -43,13 +55,14 @@ radius: Math.random() * 2 + 1, driftSpeed: Math.random() * 0.6 + 0.4, swingAmplitude: Math.random() * 0.8 + 0.2, - shapeType: snowflakeShapeType + shapeType: chosenType, + imageUrl: chosenImageUrl }); } function updateSnowflakes(){ - for (var j = 0; j < snowflakes.length; j++){ - var flake = snowflakes[j]; + 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){ @@ -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(){ context.clearRect(0, 0, viewportWidth, viewportHeight); - for (var k = 0; k < snowflakes.length; k++){ - var flake = snowflakes[k]; - // 条件判断 根据形状类型绘制雪花 - if (flake.shapeType === 'dot') { - drawDot(flake.positionX, flake.positionY, flake.radius); - } else { - drawSnowflake(flake.positionX, flake.positionY, flake.radius * 3); + for (let k = 0; k < snowflakes.length; k++){ + const flake = snowflakes[k]; + // 条件判断 如果是媒体图片类型则使用通用图像渲染 + 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); } } } diff --git a/yoone-snow.php b/yoone-snow.php index 61d353a..3853f85 100644 --- a/yoone-snow.php +++ b/yoone-snow.php @@ -19,22 +19,100 @@ function yoone_snow_enqueue_assets() { wp_register_style($style_handle, $style_src, array(), '1.1.0', 'all'); 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_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); // 将后端设置传递到前端脚本 变量名称为 YooneSnowSettings - $shape = get_option('yoone_snow_shape', 'dot'); - if (!in_array($shape, array('dot', 'flake', 'mixed'), true)) { - // 如果选项值不合法则回退到默认值 dot - $shape = 'dot'; + // 简化设置 仅保留复选框选择的形状集合 + $mixed_items_option = get_option('yoone_snow_mixed_items', array('dot','flake')); + if (is_string($mixed_items_option)) { + $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( - '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() { if (!yoone_snow_is_enabled()) { return; } static $yoone_snow_rendered = false; @@ -49,16 +127,22 @@ add_action('wp_footer', 'yoone_snow_render_overlay', 100); // 注册设置页面和设置项 用于选择雪花形状 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) { - // 对提交的值进行校验 只允许 dot flake mixed 三种 - $allowed = array('dot', 'flake', 'mixed'); - if (!is_string($value)) { return 'dot'; } - return in_array($value, $allowed, true) ? $value : 'dot'; + // 将输入统一为数组 并过滤到允许的集合 + $allowed = array('dot','flake','yuanbao','coin','santa_hat','candy_cane','christmas_sock','christmas_tree','reindeer','christmas_berry'); + if (is_string($value)) { + $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 @@ -72,19 +156,81 @@ function yoone_snow_register_settings() { 'yoone_snow' ); - // 添加设置字段 下拉选择雪花形状 + // 移除下拉字段 保留复选框作为唯一选择入口 + + // 添加形状复选集合 用于选择参与渲染的形状 add_settings_field( - 'yoone_snow_shape', - 'Snow Shape', + 'yoone_snow_mixed_items', + 'Shapes', function() { - // 渲染选择框 选项值 dot flake mixed - $current = get_option('yoone_snow_shape', 'dot'); - echo ''; - echo '

Choose dot snowflake or mixed

'; + $current_list = get_option('yoone_snow_mixed_items', array('dot','flake')); + if (is_string($current_list)) { + $current_list = array_filter(array_map('trim', explode(',', $current_list))); + } + $options = array( + 'dot' => 'Dot', + 'flake' => 'Snowflake', + '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 ''; + } + echo '

Choose shapes to render

'; + }, + '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 '
'; + 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 '
'; + echo 'media'; + echo ''; + echo ''; + echo '
'; + } + echo '
'; + echo '

'; + echo '

Choose images or SVG from media library to render

'; }, 'yoone_snow', 'yoone_snow_section' @@ -115,6 +261,7 @@ function yoone_snow_add_settings_page() { // 在 admin 初始化时注册设置 在 admin 菜单挂载页面 add_action('admin_init', 'yoone_snow_register_settings'); add_action('admin_menu', 'yoone_snow_add_settings_page'); +add_action('admin_enqueue_scripts', 'yoone_snow_admin_enqueue'); // 在插件列表行添加 Settings 链接 指向设置页面 function yoone_snow_plugin_action_links($links) {