diff --git a/js/admin-media.js b/js/admin-media.js index 2febeb1..a72a04e 100644 --- a/js/admin-media.js +++ b/js/admin-media.js @@ -1,15 +1,33 @@ (function(){ // 后台媒体选择交互脚本 用于在设置页选择媒体项目 function initAdminMedia(){ + // 将后台资源映射注入到前端设置对象 以便 shapes 脚本复用 + if (typeof window !== 'undefined'){ + // 条件判断 如果前端设置对象不存在则创建 + window.YooneSnowSettings = window.YooneSnowSettings || {}; + // 条件判断 若未设置 assetsMap 且后台提供则注入 + if (!window.YooneSnowSettings.assetsMap && window.YooneSnowAdmin && window.YooneSnowAdmin.assetsMap){ + window.YooneSnowSettings.assetsMap = window.YooneSnowAdmin.assetsMap; + } + } // 条件判断 如果没有媒体按钮则不执行 var addButton = document.getElementById('yooneSnowAddMedia'); var listContainer = document.getElementById('yooneSnowMediaList'); + var emojiListContainer = document.getElementById('yooneSnowEmojiList'); + var emojiInput = document.getElementById('yooneSnowAddEmojiInput'); + var emojiAddButton = document.getElementById('yooneSnowAddEmoji'); + var emojiSuggestBox = document.getElementById('yooneSnowEmojiSuggest'); + var shapeListContainer = document.getElementById('yooneSnowShapeList'); + var shapeAddSelect = document.getElementById('yooneSnowAddShapeSelect'); + var shapeAddButton = document.getElementById('yooneSnowAddShapeBtn'); + var emojiSelect = document.getElementById('yooneSnowEmojiSelect'); if (!addButton || !listContainer) { return; } // 形状与媒体权重容器元素 获取以便动态更新 var weightsContainer = document.getElementById('yooneSnowWeightsContainer'); var shapeWeightsBox = document.getElementById('yooneSnowShapeWeights'); var mediaWeightsBox = document.getElementById('yooneSnowMediaWeights'); + var emojiWeightsBox = document.getElementById('yooneSnowEmojiWeights'); // 形状标签映射用于显示友好名称 var shapeLabelsMap = { @@ -25,47 +43,151 @@ christmas_berry: 'Christmas Berry' }; - // 同步形状权重输入 根据勾选状态增删对应输入 - function ensureShapeWeightInputs(){ - // 条件判断 权重容器不存在则跳过 - if (!shapeWeightsBox) { return; } - var checkboxes = document.querySelectorAll('input[name="yoone_snow_mixed_items[]"]'); - var presentKeys = {}; - // 收集已存在的标签记录用于对比 - var existingLabels = shapeWeightsBox.querySelectorAll('label[data-shape-key]'); - existingLabels.forEach(function(el){ - presentKeys[el.getAttribute('data-shape-key')] = el; - }); - checkboxes.forEach(function(cb){ - var key = cb.value; - if (cb.checked){ - // 条件判断 如果不存在对应输入则创建 默认权重值为 1 - if (!presentKeys[key]){ - var lab = document.createElement('label'); - lab.setAttribute('data-shape-key', key); - lab.style.display = 'flex'; - lab.style.flexDirection = 'column'; - lab.style.minWidth = '160px'; - var title = document.createElement('span'); - title.textContent = shapeLabelsMap[key] || key; - var input = document.createElement('input'); - input.type = 'number'; - input.min = '0'; - input.name = 'yoone_snow_shape_weights[' + key + ']'; - input.value = '1'; - input.style.width = '120px'; - lab.appendChild(title); - lab.appendChild(input); - shapeWeightsBox.appendChild(lab); + // 创建形状预览元素 直接复用 shapes 渲染器 实现一致预览 + function createShapePreviewElement(shapeKey){ + var registry = (typeof window !== 'undefined' && window.YooneSnowShapeRenderers) ? window.YooneSnowShapeRenderers : {}; + var renderer = registry[shapeKey]; + // 条件判断 如果找到渲染器则使用 canvas 调用渲染函数 + if (typeof renderer === 'function'){ + var canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + canvas.style.marginRight = '6px'; + // 为白色形状提供可见的暗底与边框 提升可读性 + canvas.style.background = '#2d3748'; + canvas.style.border = '1px solid #ddd'; + canvas.style.borderRadius = '4px'; + var ctx = canvas.getContext('2d'); + // 根据形状选择更合适的基础尺寸 保证预览清晰 + var previewBaseSizeMap = { + dot: 6, + flake: 6, + yuanbao: 3, + coin: 3.5, + santa_hat: 2.8, + candy_cane: 2.8, + christmas_sock: 2.8, + christmas_tree: 2.8, + reindeer: 2.8, + christmas_berry: 2.8 + }; + var baseSize = previewBaseSizeMap[shapeKey] || 3.2; + // 调用渲染器 使用画布中心作为位置 + try { renderer(ctx, 16, 16, baseSize); } catch(e){} + // 对基于圖像的形狀進行補繪 當圖像加載完成時重試一次 + var assets = (window.YooneSnowSettings && window.YooneSnowSettings.assetsMap) ? window.YooneSnowSettings.assetsMap : {}; + var url = assets[shapeKey]; + if (url && typeof window.YooneSnowGetOrLoadImage === 'function'){ + var record = window.YooneSnowGetOrLoadImage(url); + if (!record || !record.ready){ + var tries = 0; + var timer = setInterval(function(){ + tries++; + var check = window.YooneSnowGetOrLoadImage(url); + if (check && check.ready){ + try { + ctx.clearRect(0,0,canvas.width,canvas.height); + renderer(ctx, 16, 16, 2.5); + } catch(err){} + clearInterval(timer); + } + if (tries > 50){ clearInterval(timer); } + }, 100); } - } else { - // 条件判断 如果取消勾选则移除对应输入 - var exist = shapeWeightsBox.querySelector('label[data-shape-key="' + key + '"]'); - if (exist) { exist.remove(); } + } + return canvas; + } + // 条件判断 若无渲染器则回退为资源图预览 保证可见性 + if (typeof window !== 'undefined' && window.YooneSnowAdmin && window.YooneSnowAdmin.assetsMap && window.YooneSnowAdmin.assetsMap[shapeKey]){ + var img = document.createElement('img'); + img.src = String(window.YooneSnowAdmin.assetsMap[shapeKey]); + img.alt = shapeKey; + img.style.width = '28px'; + img.style.height = '28px'; + img.style.objectFit = 'contain'; + img.style.marginRight = '6px'; + return img; + } + // 默认降级 使用文本首字母表示 + var fallback = document.createElement('canvas'); + fallback.width = 32; + fallback.height = 32; + fallback.style.marginRight = '6px'; + var fctx = fallback.getContext('2d'); + fctx.fillStyle = '#555'; + fctx.font = '12px system-ui'; + fctx.textAlign = 'center'; + fctx.textBaseline = 'middle'; + var txt = (shapeKey || 'S').substring(0, 1).toUpperCase(); + fctx.fillText(txt, 16, 16); + return fallback; + } + + // 为形状复选框添加图形预览 替代纯文字展示 + function renderShapeCheckboxPreviews(){ + // 查找所有形状复选框 遍历其父级标签容器 + var inputs = document.querySelectorAll('input[name="yoone_snow_mixed_items[]"]'); + inputs.forEach(function(cb){ + var label = cb.closest('label'); + if (!label) { return; } + // 条件判断 若已绘制过预览则跳过 + if (label.querySelector('.yoone-shape-preview')) { return; } + var key = cb.value; + // 创建预览节点并插入在文本前 + var wrap = document.createElement('span'); + wrap.className = 'yoone-shape-preview'; + wrap.style.display = 'inline-flex'; + wrap.style.alignItems = 'center'; + wrap.style.marginRight = '6px'; + var previewEl = createShapePreviewElement(key); + wrap.appendChild(previewEl); + // 插入到复选框后的位置便于对齐 + cb.insertAdjacentElement('afterend', wrap); + // 同步更新文字标签 保留友好名称 + var nameSpan = label.querySelector('span'); + if (!nameSpan){ + nameSpan = document.createElement('span'); + nameSpan.textContent = shapeLabelsMap[key] || key; + nameSpan.style.marginLeft = '4px'; + wrap.insertAdjacentElement('afterend', nameSpan); } }); } + // 同步形状权重输入 根据勾选状态增删对应输入 + function ensureShapeWeightInputs(){ + if (!shapeWeightsBox) { return; } + var presentKeys = {}; + var existingLabels = shapeWeightsBox.querySelectorAll('label[data-shape-key]'); + existingLabels.forEach(function(el){ presentKeys[el.getAttribute('data-shape-key')] = el; }); + var items = shapeListContainer ? shapeListContainer.querySelectorAll('.yoone-snow-shape-item') : []; + items.forEach(function(item){ + var key = item.getAttribute('data-shape-key'); + if (!presentKeys[key]){ + var lab = document.createElement('label'); + lab.setAttribute('data-shape-key', key); + lab.style.display = 'flex'; + lab.style.flexDirection = 'column'; + lab.style.minWidth = '160px'; + var title = document.createElement('span'); + title.textContent = shapeLabelsMap[key] || key; + var input = document.createElement('input'); + input.type = 'number'; + input.min = '0'; + input.name = 'yoone_snow_shape_weights[' + key + ']'; + input.value = '1'; + input.style.width = '120px'; + lab.appendChild(title); + lab.appendChild(input); + shapeWeightsBox.appendChild(lab); + } + }); + for (var k in presentKeys){ + var stillSelected = shapeListContainer && shapeListContainer.querySelector('.yoone-snow-shape-item[data-shape-key="' + k + '"]'); + if (!stillSelected){ presentKeys[k].remove(); } + } + } + // 添加媒体权重输入 根据附件 ID 创建输入 默认值为 1 function addMediaWeightInput(attachmentId){ if (!mediaWeightsBox) { return; } @@ -89,6 +211,39 @@ mediaWeightsBox.appendChild(lab); } + // 添加 emoji 權重輸入 根據字符創建輸入 默認值為 1 + function addEmojiWeightInput(emojiChar){ + if (!emojiWeightsBox) { return; } + var key = String(emojiChar); + if (key.trim() === '') { return; } + var exist = emojiWeightsBox.querySelector('label[data-emoji-char="' + key + '"]'); + if (exist) { return; } + var lab = document.createElement('label'); + lab.setAttribute('data-emoji-char', key); + lab.style.display = 'flex'; + lab.style.flexDirection = 'column'; + lab.style.minWidth = '160px'; + var title = document.createElement('span'); + title.textContent = key; + var input = document.createElement('input'); + input.type = 'number'; + input.min = '0'; + input.name = 'yoone_snow_emoji_weights[' + key + ']'; + input.value = '1'; + input.style.width = '120px'; + lab.appendChild(title); + lab.appendChild(input); + emojiWeightsBox.appendChild(lab); + } + + // 移除 emoji 權重輸入 根據字符刪除對應輸入 + function removeEmojiWeightInput(emojiChar){ + if (!emojiWeightsBox) { return; } + var key = String(emojiChar); + var exist = emojiWeightsBox.querySelector('label[data-emoji-char="' + key + '"]'); + if (exist) { exist.remove(); } + } + // 移除媒体权重输入 根据附件 ID 删除对应输入 function removeMediaWeightInput(attachmentId){ if (!mediaWeightsBox) { return; } @@ -98,12 +253,59 @@ // 初始化时同步一次形状权重输入 ensureShapeWeightInputs(); + // 初始化时为形状复选框添加图形预览 + renderShapeCheckboxPreviews(); + ensureShapeWeightInputs(); + + // 添加形状卡片 并插入预览与隐藏输入 用于保存选择 + function addShapeBox(shapeKey){ + if (!shapeListContainer) { return; } + var key = String(shapeKey); + if (key.trim() === '') { return; } + var exist = shapeListContainer.querySelector('.yoone-snow-shape-item[data-shape-key="' + key + '"]'); + if (exist) { return; } + var wrapper = document.createElement('div'); + wrapper.className = 'yoone-snow-shape-item'; + wrapper.setAttribute('data-shape-key', key); + wrapper.style.border = '1px solid #ddd'; + wrapper.style.padding = '8px'; + wrapper.style.display = 'flex'; + wrapper.style.flexDirection = 'column'; + wrapper.style.alignItems = 'center'; + wrapper.style.minWidth = '96px'; + var previewHost = document.createElement('div'); + previewHost.className = 'yoone-snow-shape-preview'; + previewHost.style.width = '32px'; + previewHost.style.height = '32px'; + previewHost.style.marginBottom = '6px'; + var nameSpan = document.createElement('span'); + nameSpan.textContent = shapeLabelsMap[key] || key; + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'yoone_snow_mixed_items[]'; + input.value = key; + var cancelBtn = document.createElement('button'); + cancelBtn.type = 'button'; + cancelBtn.className = 'button yoone-snow-cancel-shape'; + cancelBtn.textContent = 'Cancel'; + cancelBtn.style.marginTop = '6px'; + wrapper.appendChild(previewHost); + wrapper.appendChild(nameSpan); + wrapper.appendChild(input); + wrapper.appendChild(cancelBtn); + shapeListContainer.appendChild(wrapper); + var previewEl = createShapePreviewElement(key); + if (previewEl) { previewHost.appendChild(previewEl); } + ensureShapeWeightInputs(); + } // 监听形状复选框变化事件 动态刷新权重输入 document.addEventListener('change', function(event){ var t = event.target; if (t && t.name === 'yoone_snow_mixed_items[]'){ ensureShapeWeightInputs(); + // 复选状态变更后也尝试补齐预览 + renderShapeCheckboxPreviews(); } }); @@ -122,6 +324,22 @@ } }); + // 綁定移除 emoji 按鈕事件 使用事件委託處理動態元素 + if (emojiListContainer){ + emojiListContainer.addEventListener('click', function(event){ + var target = event.target; + if (target && target.classList.contains('yoone-snow-remove-emoji')){ + var item = target.closest('.yoone-snow-emoji-item'); + if (item){ + var ch = item.getAttribute('data-emoji-char'); + item.remove(); + // 同步移除對應 emoji 權重輸入 + if (ch) { removeEmojiWeightInput(ch); } + } + } + }); + } + // 打开媒体选择器 支持多选 图片和 SVG addButton.addEventListener('click', function(){ // 条件判断 如果 wp.media 不可用则终止 @@ -171,6 +389,199 @@ }); frame.open(); }); + + // Emoji 别名映射 用於文本搜索到 emoji 字符 + var emojiAliasMap = { + smile: '🙂', + happy: '😊', + grin: '😁', + laugh: '😂', + joy: '😂', + wink: '😉', + blush: '☺️', + heart: '❤️', + love: '❤️', + star: '⭐', + fire: '🔥', + cool: '😎', + cry: '😢', + sad: '🙁', + angry: '😠', + thumbs_up: '👍', + ok: '👌', + clap: '👏', + pray: '🙏', + tada: '🎉', + gift: '🎁', + snow: '❄️', + tree: '🌲', + bell: '🔔', + candy: '🍬', + sock: '🧦', + deer: '🦌', + berry: '🍓' + }; + + // 填充下拉選單 以別名和字符組合顯示 + function populateEmojiSelect(){ + if (!emojiSelect) { return; } + // 清空除第一個提示項之外的選項 + while (emojiSelect.options.length > 1){ emojiSelect.remove(1); } + for (var key in emojiAliasMap){ + var ch = emojiAliasMap[key]; + var opt = document.createElement('option'); + opt.value = ch; + opt.textContent = ch + ' ' + key; + emojiSelect.appendChild(opt); + } + } + + // 綁定下拉選擇事件 選擇後添加對應 emoji 並重置選中 + if (emojiSelect){ + populateEmojiSelect(); + emojiSelect.addEventListener('change', function(){ + var val = emojiSelect.value; + if (String(val).trim() !== ''){ + addEmojiByChar(val); + emojiSelect.value = ''; + } + }); + } + + // 顯示建議列表 根據查詢文本匹配別名 + function showEmojiSuggestions(query){ + if (!emojiSuggestBox) { return; } + var q = String(query || '').toLowerCase().trim(); + emojiSuggestBox.innerHTML = ''; + if (q === '') { return; } + var max = 12; + var count = 0; + for (var key in emojiAliasMap){ + if (key.indexOf(q) !== -1){ + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'button'; + btn.textContent = emojiAliasMap[key] + ' ' + key; + (function(ch){ + btn.addEventListener('click', function(){ addEmojiByChar(ch); }); + })(emojiAliasMap[key]); + emojiSuggestBox.appendChild(btn); + count++; + if (count >= max){ break; } + } + } + } + + // 將 emoji 字符添加到列表 並創建隱藏輸入 + function addEmojiByChar(ch){ + if (!emojiListContainer) { return; } + var key = String(ch); + if (key.trim() === '') { return; } + var exist = emojiListContainer.querySelector('.yoone-snow-emoji-item[data-emoji-char="' + key + '"]'); + if (exist) { return; } + var wrapper = document.createElement('div'); + wrapper.className = 'yoone-snow-emoji-item'; + wrapper.setAttribute('data-emoji-char', key); + wrapper.style.border = '1px solid #ddd'; + wrapper.style.padding = '8px'; + wrapper.style.display = 'flex'; + wrapper.style.flexDirection = 'column'; + wrapper.style.alignItems = 'center'; + var preview = document.createElement('div'); + preview.textContent = key; + preview.style.fontSize = '28px'; + preview.style.lineHeight = '32px'; + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'yoone_snow_emoji_items[]'; + input.value = key; + var removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.className = 'button yoone-snow-remove-emoji'; + removeBtn.textContent = 'Cancel'; + removeBtn.style.marginTop = '6px'; + wrapper.appendChild(preview); + wrapper.appendChild(input); + wrapper.appendChild(removeBtn); + emojiListContainer.appendChild(wrapper); + // 同步添加 emoji 權重輸入 默認權重為 1 + addEmojiWeightInput(key); + } + + // 綁定 emoji 添加按鈕 點擊後優先按別名匹配 否則直接添加字符 + if (emojiAddButton){ + emojiAddButton.addEventListener('click', function(){ + var q = emojiInput ? String(emojiInput.value || '') : ''; + var trimmed = q.trim(); + if (trimmed === '') { return; } + var lower = trimmed.toLowerCase(); + if (emojiAliasMap[lower]){ + addEmojiByChar(emojiAliasMap[lower]); + } else { + addEmojiByChar(trimmed); + } + }); + } + + // 監聽輸入變化 顯示建議列表 支持即時搜索 + if (emojiInput){ + emojiInput.addEventListener('input', function(){ + showEmojiSuggestions(emojiInput.value); + }); + emojiInput.addEventListener('keydown', function(e){ + if (e && e.key === 'Enter'){ + e.preventDefault(); + var q = String(emojiInput.value || '').trim(); + if (q !== ''){ + var lower = q.toLowerCase(); + if (emojiAliasMap[lower]){ addEmojiByChar(emojiAliasMap[lower]); } + else { addEmojiByChar(q); } + } + } + }); + } + + // 初始化時為現有 emoji 列表補齊權重輸入 + if (emojiListContainer){ + var existing = emojiListContainer.querySelectorAll('.yoone-snow-emoji-item'); + existing.forEach(function(node){ + var ch = node.getAttribute('data-emoji-char'); + if (ch) { addEmojiWeightInput(ch); } + }); + } + if (shapeListContainer){ + shapeListContainer.addEventListener('click', function(event){ + var target = event.target; + if (target && target.classList.contains('yoone-snow-cancel-shape')){ + var item = target.closest('.yoone-snow-shape-item'); + if (item){ + var key = item.getAttribute('data-shape-key'); + item.remove(); + ensureShapeWeightInputs(); + } + } + }); + } + + if (shapeAddSelect){ + shapeAddSelect.addEventListener('change', function(){ + var val = shapeAddSelect.value; + if (String(val).trim() !== ''){ + addShapeBox(val); + shapeAddSelect.value = ''; + } + }); + } + + if (shapeAddButton){ + shapeAddButton.addEventListener('click', function(){ + var val = shapeAddSelect ? String(shapeAddSelect.value || '') : ''; + if (val.trim() !== ''){ + addShapeBox(val); + shapeAddSelect.value = ''; + } + }); + } } // 条件判断 如果文档尚未加载则等待 DOMContentLoaded 事件 diff --git a/js/snow-canvas.js b/js/snow-canvas.js index ae44156..653ff00 100644 --- a/js/snow-canvas.js +++ b/js/snow-canvas.js @@ -24,6 +24,9 @@ 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 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 @@ -31,6 +34,9 @@ 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 shapeWeights = {}; for (let key in defaultShapeWeights){ const val = typeof shapeWeightsRaw[key] !== 'undefined' ? parseInt(shapeWeightsRaw[key], 10) : defaultShapeWeights[key]; @@ -97,9 +103,20 @@ const shapeKey = selectedShapes[sIndex]; const weightVal = typeof shapeWeights[shapeKey] !== 'undefined' ? shapeWeights[shapeKey] : 1; if (weightVal > 0){ + // 条件判断 普通形状直接加入 不含 emoji 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 }); } + } + } for (let mIndex = 0; mIndex < mediaItems.length; mIndex++){ const mediaUrl = mediaItems[mIndex]; const mediaWeight = typeof mediaWeightsRaw[mediaUrl] !== 'undefined' ? parseInt(mediaWeightsRaw[mediaUrl], 10) : 1; @@ -110,7 +127,7 @@ } // 条件判断 如果没有可选项则回退为点形状 if (items.length === 0){ - return { type: 'dot', url: null }; + return { type: 'dot', url: null, text: null }; } let totalWeight = 0; for (let i = 0; i < items.length; i++){ @@ -123,18 +140,23 @@ // 条件判断 如果随机值落在当前累计权重内则选择该项 if (r <= acc){ if (items[i].kind === 'shape'){ - return { type: items[i].key, url: null }; + return { type: items[i].key, url: null, text: null }; } else { - return { type: 'media_image', url: items[i].url }; + if (items[i].kind === 'media'){ + return { type: 'media_image', url: items[i].url, text: null }; + } else { + return { type: 'emoji_text', url: null, text: items[i].text }; + } } } } - return { type: 'dot', url: null }; + return { type: 'dot', url: null, text: null }; } function createSnowflake(preferredX, preferredY){ const picked = selectWeightedItem(); let chosenType = picked.type; let chosenImageUrl = picked.url; + let chosenEmojiText = picked.text; return { positionX: typeof preferredX === 'number' ? preferredX : Math.random() * viewportWidth, positionY: typeof preferredY === 'number' ? preferredY : (-1 - Math.random() * 4), @@ -145,6 +167,7 @@ swingAmplitude: (Math.random() * (swingMax - swingMin) + swingMin) * swingMin, shapeType: chosenType, imageUrl: chosenImageUrl, + emojiText: chosenEmojiText, // 标记该粒子是否已经移出视口 用于停止后清理 outOfView: false }; @@ -240,6 +263,18 @@ } continue; } + // 条件判断 如果是 emoji 文本类型則使用文本繪制 + if (flake.shapeType === 'emoji_text' && flake.emojiText){ + context.save(); + // 設置字體大小與居中對齊 基於半徑縮放 + const fontSize = Math.max(12, flake.radius * 6); + context.font = String(Math.floor(fontSize)) + 'px system-ui, Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji'; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillText(String(flake.emojiText), flake.positionX, flake.positionY); + context.restore(); + continue; + } // 否则执行注册表中的形状渲染函数 const registry = window.YooneSnowShapeRenderers || {}; const renderer = registry[flake.shapeType] || registry['dot']; diff --git a/yoone-snow.php b/yoone-snow.php index 61cfcc4..962bfc8 100644 --- a/yoone-snow.php +++ b/yoone-snow.php @@ -142,6 +142,32 @@ function yoone_snow_enqueue_assets() { ), 'shapeWeights' => $shape_weights, 'mediaWeights' => $media_weights_by_url, + 'emojiItems' => (function(){ + // 读取已保存的 emoji 列表 返回字符串数组 + $items = get_option('yoone_snow_emoji_items', array()); + if (!is_array($items)) { $items = array(); } + $clean = array(); + foreach ($items as $it) { + $s = trim((string)$it); + if ($s !== '') { $clean[] = $s; } + } + return $clean; + })(), + 'emojiWeights' => (function(){ + // 读取已保存的 emoji 权重 映射为字符到权重 + $map = get_option('yoone_snow_emoji_weights', array()); + if (!is_array($map)) { $map = array(); } + $clean = array(); + foreach ($map as $k => $v) { + $key = trim((string)$k); + $num = intval($v); + if ($key !== '') { + if ($num < 0) { $num = 0; } + $clean[$key] = $num; + } + } + return $clean; + })(), )); } @@ -151,9 +177,75 @@ function yoone_snow_admin_enqueue($hook) { if ($hook !== 'settings_page_yoone_snow') { return; } // 加载媒体库脚本 以便使用 wp.media 选择器 wp_enqueue_media(); - // 注册并加载后台交互脚本 + + // 在后台也注册并加载 shapes 渲染脚本 以供预览复用 + // 形状索引与工具脚本 提供全局渲染器与图像加载能力 + $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); + + // 在后台本页入队 shapes 相关脚本 以便预览复用前端实现 + wp_enqueue_script($shape_index_handle); + wp_enqueue_script($shape_utils_handle); + wp_enqueue_script($shape_dot_handle); + wp_enqueue_script($shape_flake_handle); + wp_enqueue_script($shape_yuanbao_handle); + wp_enqueue_script($shape_coin_handle); + wp_enqueue_script($shape_santa_handle); + wp_enqueue_script($shape_cane_handle); + wp_enqueue_script($shape_sock_handle); + wp_enqueue_script($shape_tree_handle); + wp_enqueue_script($shape_reindeer_handle); + wp_enqueue_script($shape_berry_handle); + + // 为后台也提供 YooneSnowSettings 的 assetsMap 以满足形状脚本的资源需求 + wp_localize_script($shape_utils_handle, 'YooneSnowSettings', array( + '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__), + ) + )); + + // 注册并加载后台交互脚本 该脚本复用 shapes 渲染器进行预览 $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_register_script($admin_script_handle, plugins_url('js/admin-media.js', __FILE__), array($shape_index_handle, $shape_utils_handle), '1.1.0', true); + // 传递资源映射用于后台形状预览 保持与前端一致 + wp_localize_script($admin_script_handle, 'YooneSnowAdmin', array( + '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__), + ) + )); wp_enqueue_script($admin_script_handle); } @@ -202,7 +294,6 @@ function yoone_snow_register_settings() { // 移除下拉字段 保留复选框作为唯一选择入口 - // 添加形状复选集合 用于选择参与渲染的形状 add_settings_field( 'yoone_snow_mixed_items', 'Shapes', @@ -211,6 +302,7 @@ function yoone_snow_register_settings() { if (is_string($current_list)) { $current_list = array_filter(array_map('trim', explode(',', $current_list))); } + if (!is_array($current_list)) { $current_list = array('dot','flake'); } $options = array( 'dot' => 'Dot', 'flake' => 'Snowflake', @@ -223,16 +315,62 @@ function yoone_snow_register_settings() { 'reindeer' => 'Reindeer', 'christmas_berry' => 'Christmas Berry', ); - foreach ($options as $key => $label) { - $checked = in_array($key, $current_list, true) ? 'checked' : ''; - echo ''; + echo '
'; + foreach ($current_list as $key) { + if (!isset($options[$key])) { continue; } + echo '
'; + echo '
'; + echo '' . esc_html($options[$key]) . ''; + echo ''; + echo ''; + echo '
'; } + echo '
'; + echo '
'; + echo ''; + echo ''; + echo '
'; echo '

Choose shapes to render

'; }, 'yoone_snow', 'yoone_snow_section' ); + // 添加 emoji 形狀選擇區域 放置在 Media Shapes 之前 + add_settings_field( + 'yoone_snow_emoji_items', + 'Emoji Shapes', + function() { + // 渲染當前 emoji 列表與添加搜索框 + $current_emojis = get_option('yoone_snow_emoji_items', array()); + if (!is_array($current_emojis)) { $current_emojis = array(); } + echo '
'; + foreach ($current_emojis as $emoji_char) { + $label = trim((string)$emoji_char); + if ($label === '') { continue; } + echo '
'; + echo '
' . esc_html($label) . '
'; + echo ''; + echo ''; + echo '
'; + } + echo '
'; + echo '
'; + echo ''; + echo ''; + echo ''; + echo '
'; + echo '
'; + echo '

Search by alias or paste emoji character then add

'; + }, + 'yoone_snow', + 'yoone_snow_section' + ); + register_setting('yoone_snow_options', 'yoone_snow_shape_weights', array( 'type' => 'array', 'sanitize_callback' => function($value) { @@ -363,6 +501,7 @@ function yoone_snow_register_settings() { 'christmas_tree' => 'Christmas Tree', 'reindeer' => 'Reindeer', 'christmas_berry' => 'Christmas Berry', + 'emoji' => 'Emoji', ); $shape_weights_current = get_option('yoone_snow_shape_weights', array()); if (!is_array($shape_weights_current)) { $shape_weights_current = array(); } @@ -393,6 +532,22 @@ function yoone_snow_register_settings() { echo ''; } echo ''; + // 渲染 emoji 权重 输入为字符键 默认 1 + $emoji_items_current = get_option('yoone_snow_emoji_items', array()); + if (!is_array($emoji_items_current)) { $emoji_items_current = array(); } + $emoji_weights_current = get_option('yoone_snow_emoji_weights', array()); + if (!is_array($emoji_weights_current)) { $emoji_weights_current = array(); } + echo '
'; + foreach ($emoji_items_current as $emojiChar) { + $label = trim((string)$emojiChar); + if ($label === '') { continue; } + $val = isset($emoji_weights_current[$label]) ? intval($emoji_weights_current[$label]) : 1; + echo ''; + } + echo '
'; echo '

Higher value increases selection probability 0 disables default is 1

'; echo ''; // 绑定交互脚本 通过已有 admin 脚本实现动态刷新 @@ -401,6 +556,46 @@ function yoone_snow_register_settings() { 'yoone_snow_section' ); + // 注册 emoji 列表设置项 保存为字符串数组 + register_setting('yoone_snow_options', 'yoone_snow_emoji_items', array( + 'type' => 'array', + 'sanitize_callback' => function($value) { + // 将输入统一为非空字符串数组 并去重 + if (is_string($value)) { + $value = array_filter(array_map('trim', explode(',', $value))); + } + if (!is_array($value)) { $value = array(); } + $out = array(); + foreach ($value as $item) { + $s = trim((string)$item); + if ($s !== '') { $out[] = $s; } + } + return array_values(array_unique($out)); + }, + 'default' => array(), + )); + + + + // 注册 emoji 权重设置项 映射字符到非负整数权重 + register_setting('yoone_snow_options', 'yoone_snow_emoji_weights', array( + 'type' => 'array', + 'sanitize_callback' => function($value) { + if (!is_array($value)) { $value = array(); } + $clean = array(); + foreach ($value as $ch => $num) { + $key = trim((string)$ch); + $weight = intval($num); + if ($key !== '') { + if ($weight < 0) { $weight = 0; } + $clean[$key] = $weight; + } + } + return $clean; + }, + 'default' => array(), + )); + // 注册首页显示时长设置 项为整数秒 0 表示无限 register_setting('yoone_snow_options', 'yoone_snow_home_duration', array( 'type' => 'integer',