From 73288f1d192384f1cdace5f979e094ac93d790f6 Mon Sep 17 00:00:00 2001 From: Yoone Date: Sun, 14 Dec 2025 16:41:52 +0800 Subject: [PATCH] refactor: migrate to lib/dist outputs; add index.js export; update plugin to load dist; implement admin settings page and i18n; fix media weights keys; add watch script --- .gitignore | 2 + dist/assets/SnowAnimator-Dem4j_A7.js | 1 + {assets => dist/assets}/圣诞拐杖.svg | 0 {assets => dist/assets}/圣诞果.svg | 0 {assets => dist/assets}/圣诞树.svg | 0 {assets => dist/assets}/圣诞袜子.svg | 0 {assets => dist/assets}/圣诞雪帽.svg | 0 {assets => dist/assets}/圣诞麋鹿.svg | 0 dist/index.js | 1 + dist/snow.css | 1 + dist/snow.js | 1 + js/shapes/candy_cane.js | 15 - js/shapes/christmas_berry.js | 15 - js/shapes/christmas_sock.js | 15 - js/shapes/christmas_tree.js | 15 - js/shapes/coin.js | 24 - js/shapes/dot.js | 12 - js/shapes/flake.js | 35 - js/shapes/index.js | 7 - js/shapes/reindeer.js | 15 - js/shapes/santa_hat.js | 15 - js/shapes/utils.js | 80 -- js/shapes/yuanbao.js | 56 - js/snow-canvas.js | 625 ------------ lib/package-lock.json | 957 +++++++++++++++++ lib/package.json | 15 + lib/src/animator/Animator.ts | 38 + lib/src/animator/SnowAnimator.ts | 32 + lib/src/assets/圣诞拐杖.svg | 1 + lib/src/assets/圣诞果.svg | 1 + lib/src/assets/圣诞树.svg | 1 + lib/src/assets/圣诞袜子.svg | 1 + lib/src/assets/圣诞雪帽.svg | 1 + lib/src/assets/圣诞麋鹿.svg | 1 + lib/src/engine/Component.ts | 48 + lib/src/engine/DownwardMoveComponent.ts | 9 + lib/src/engine/LifetimeComponent.ts | 7 + lib/src/engine/Snow.ts | 43 + lib/src/engine/SwingComponent.ts | 9 + lib/src/global.ts | 15 + lib/src/index.ts | 5 + lib/src/main.ts | 53 + lib/src/shapes/candy_cane.ts | 14 + lib/src/shapes/christmas_berry.ts | 14 + lib/src/shapes/christmas_sock.ts | 14 + lib/src/shapes/christmas_tree.ts | 14 + lib/src/shapes/coin.ts | 13 + lib/src/shapes/dot.ts | 10 + lib/src/shapes/flake.ts | 32 + lib/src/shapes/reindeer.ts | 14 + lib/src/shapes/santa_hat.ts | 14 + lib/src/shapes/yuanbao.ts | 19 + {css => lib/src/styles}/snow.css | 0 lib/src/system/snowSystem.ts | 238 +++++ lib/src/utils/image.ts | 72 ++ lib/tsconfig.json | 16 + lib/vite.config.ts | 31 + yoone-snow.php | 1249 +++++------------------ 58 files changed, 2036 insertions(+), 1900 deletions(-) create mode 100644 dist/assets/SnowAnimator-Dem4j_A7.js rename {assets => dist/assets}/圣诞拐杖.svg (100%) rename {assets => dist/assets}/圣诞果.svg (100%) rename {assets => dist/assets}/圣诞树.svg (100%) rename {assets => dist/assets}/圣诞袜子.svg (100%) rename {assets => dist/assets}/圣诞雪帽.svg (100%) rename {assets => dist/assets}/圣诞麋鹿.svg (100%) create mode 100644 dist/index.js create mode 100644 dist/snow.css create mode 100644 dist/snow.js delete mode 100644 js/shapes/candy_cane.js delete mode 100644 js/shapes/christmas_berry.js delete mode 100644 js/shapes/christmas_sock.js delete mode 100644 js/shapes/christmas_tree.js delete mode 100644 js/shapes/coin.js delete mode 100644 js/shapes/dot.js delete mode 100644 js/shapes/flake.js delete mode 100644 js/shapes/index.js delete mode 100644 js/shapes/reindeer.js delete mode 100644 js/shapes/santa_hat.js delete mode 100644 js/shapes/utils.js delete mode 100644 js/shapes/yuanbao.js delete mode 100644 js/snow-canvas.js create mode 100644 lib/package-lock.json create mode 100644 lib/package.json create mode 100644 lib/src/animator/Animator.ts create mode 100644 lib/src/animator/SnowAnimator.ts create mode 100644 lib/src/assets/圣诞拐杖.svg create mode 100644 lib/src/assets/圣诞果.svg create mode 100644 lib/src/assets/圣诞树.svg create mode 100644 lib/src/assets/圣诞袜子.svg create mode 100644 lib/src/assets/圣诞雪帽.svg create mode 100644 lib/src/assets/圣诞麋鹿.svg create mode 100644 lib/src/engine/Component.ts create mode 100644 lib/src/engine/DownwardMoveComponent.ts create mode 100644 lib/src/engine/LifetimeComponent.ts create mode 100644 lib/src/engine/Snow.ts create mode 100644 lib/src/engine/SwingComponent.ts create mode 100644 lib/src/global.ts create mode 100644 lib/src/index.ts create mode 100644 lib/src/main.ts create mode 100644 lib/src/shapes/candy_cane.ts create mode 100644 lib/src/shapes/christmas_berry.ts create mode 100644 lib/src/shapes/christmas_sock.ts create mode 100644 lib/src/shapes/christmas_tree.ts create mode 100644 lib/src/shapes/coin.ts create mode 100644 lib/src/shapes/dot.ts create mode 100644 lib/src/shapes/flake.ts create mode 100644 lib/src/shapes/reindeer.ts create mode 100644 lib/src/shapes/santa_hat.ts create mode 100644 lib/src/shapes/yuanbao.ts rename {css => lib/src/styles}/snow.css (100%) create mode 100644 lib/src/system/snowSystem.ts create mode 100644 lib/src/utils/image.ts create mode 100644 lib/tsconfig.json create mode 100644 lib/vite.config.ts diff --git a/.gitignore b/.gitignore index e693165..211dd33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .DS_Store release +lib/node_modules/ +wordpress-plugin/ diff --git a/dist/assets/SnowAnimator-Dem4j_A7.js b/dist/assets/SnowAnimator-Dem4j_A7.js new file mode 100644 index 0000000..5eecf81 --- /dev/null +++ b/dist/assets/SnowAnimator-Dem4j_A7.js @@ -0,0 +1 @@ +function g(){return window.YooneSnowShapeRenderers||(window.YooneSnowShapeRenderers={}),window.YooneSnowShapeRenderers}function p(t){if(!t)return{img:null,ready:!1};const o=window.YooneSnowImageCache=window.YooneSnowImageCache||{},n=o[t];if(n&&n.ready||n&&!n.ready)return n;const e=new Image,i={img:e,ready:!1};o[t]=i;try{e.decoding="async"}catch{}try{e.fetchPriority="low"}catch{}return e.onload=function(){i.ready=!0},e.onerror=function(){delete o[t]},e.src=t,i}function X(t,o){if(!t){o(!1);return}const n=window.YooneSnowImageCache=window.YooneSnowImageCache||{},e=n[t];if(e&&e.ready){o(!0);return}if(e||(n[t]={img:null,ready:!1}),typeof fetch=="function"&&typeof window.createImageBitmap=="function")fetch(t,{cache:"force-cache"}).then(i=>i.blob()).then(i=>window.createImageBitmap(i)).then(i=>{n[t]={img:i,ready:!0},o(!0)}).catch(()=>{const i=p(t);let s=!1;if(i&&i.img instanceof HTMLImageElement){const a=()=>{i.ready=!0};i.img.onload=function(){s||(s=!0,a(),o(!0))},i.img.onerror=function(){s||(s=!0,o(!1))}}else o(!1)});else{const i=p(t);let s=!1;if(i&&i.img instanceof HTMLImageElement){const a=()=>{i.ready=!0};i.img.onload=function(){s||(s=!0,a(),o(!0))},i.img.onerror=function(){s||(s=!0,o(!1))}}else o(!1)}}function M(t,o,n,e,i,s){const a=n-i/2,c=e-s/2;t.drawImage(o,a,c,i,s)}window.YooneSnowGetOrLoadImage=p;window.YooneSnowLoadAssetViaFetch=X;window.YooneSnowDrawCenteredImage=M;const D=g();D.dot=function(t,o,n,e){t.beginPath(),t.arc(o,n,e,0,Math.PI*2),t.fillStyle="rgba(255,255,255,0.9)",t.fill()};const H=g();H.flake=function(t,o,n,e){const i=e*3;t.save(),t.translate(o,n),t.fillStyle="rgba(255,255,255,0.9)",t.strokeStyle="rgba(255,255,255,0.9)",t.lineWidth=i*.15;for(let s=0;s<6;s++)t.rotate(Math.PI/3),t.beginPath(),t.moveTo(0,0),t.lineTo(0,i),t.stroke(),t.beginPath(),t.moveTo(0,i*.3),t.lineTo(i*.3,i*.5),t.stroke(),t.beginPath(),t.moveTo(0,i*.5),t.lineTo(-i*.3,i*.7),t.stroke(),t.beginPath(),t.moveTo(0,i*.7),t.lineTo(i*.3,i*.9),t.stroke();t.restore()};const z=g();z.yuanbao=function(t,o,n,e){t.save(),t.translate(o,n),t.scale(e*.08,e*.08),t.fillStyle="rgba(255,215,0,0.9)",t.beginPath(),t.moveTo(-20,0),t.quadraticCurveTo(0,-12,20,0),t.quadraticCurveTo(0,12,-20,0),t.fill(),t.beginPath(),t.arc(0,0,6,0,Math.PI*2),t.fill(),t.restore()};const E=g();E.coin=function(t,o,n,e){const i=t.createRadialGradient(o,n,e*.2,o,n,e);i.addColorStop(0,"rgba(255,230,120,0.95)"),i.addColorStop(1,"rgba(240,180,60,0.85)"),t.fillStyle=i,t.beginPath(),t.arc(o,n,e*2.5,0,Math.PI*2),t.fill()};const U=g();U.santa_hat=function(t,o,n,e){const s=(window.YooneSnowSettings&&window.YooneSnowSettings.assetsMap?window.YooneSnowSettings.assetsMap:{}).santa_hat||"",a=p(s);if(!a||!a.ready||!a.img)return;const c=e*8,m=c;M(t,a.img,o,n,m,c)};const q=g();q.candy_cane=function(t,o,n,e){const s=(window.YooneSnowSettings&&window.YooneSnowSettings.assetsMap?window.YooneSnowSettings.assetsMap:{}).candy_cane||"",a=p(s);if(!a||!a.ready||!a.img)return;const c=e*8,m=c;M(t,a.img,o,n,m,c)};const B=g();B.christmas_sock=function(t,o,n,e){const s=(window.YooneSnowSettings&&window.YooneSnowSettings.assetsMap?window.YooneSnowSettings.assetsMap:{}).christmas_sock||"",a=p(s);if(!a||!a.ready||!a.img)return;const c=e*8,m=c;M(t,a.img,o,n,m,c)};const G=g();G.christmas_tree=function(t,o,n,e){const s=(window.YooneSnowSettings&&window.YooneSnowSettings.assetsMap?window.YooneSnowSettings.assetsMap:{}).christmas_tree||"",a=p(s);if(!a||!a.ready||!a.img)return;const c=e*8,m=c;M(t,a.img,o,n,m,c)};const N=g();N.reindeer=function(t,o,n,e){const s=(window.YooneSnowSettings&&window.YooneSnowSettings.assetsMap?window.YooneSnowSettings.assetsMap:{}).reindeer||"",a=p(s);if(!a||!a.ready||!a.img)return;const c=e*8,m=c;M(t,a.img,o,n,m,c)};const J=g();J.christmas_berry=function(t,o,n,e){const s=(window.YooneSnowSettings&&window.YooneSnowSettings.assetsMap?window.YooneSnowSettings.assetsMap:{}).christmas_berry||"",a=p(s);if(!a||!a.ready||!a.img)return;const c=e*8,m=c;M(t,a.img,o,n,m,c)};class K{constructor(o,n,e){this.onFrame=o,this.isDone=n,this.onStopped=e,this.lastTs=performance.now(),this.rafId=null,this.running=!1,this.loop=()=>{if(!this.running)return;const i=performance.now(),s=Math.max(0,(i-this.lastTs)/1e3);if(this.lastTs=i,this.onFrame(s),this.isDone()){this.stop(),this.onStopped&&this.onStopped();return}this.rafId=requestAnimationFrame(this.loop)}}init(){this.lastTs=performance.now(),this.running=!1}update(){this.running=!0,this.rafId=requestAnimationFrame(this.loop)}stop(){if(this.rafId!==null&&typeof cancelAnimationFrame=="function"){try{cancelAnimationFrame(this.rafId)}catch{}this.rafId=null}this.running=!1}}class I{init(o,n){}update(o,n,e){}}class Q{constructor(o){this.outOfView=!1,this.components=[],this.positionX=o.positionX,this.positionY=o.positionY,this.radius=o.radius,this.driftSpeed=o.driftSpeed,this.swingAmplitude=o.swingAmplitude,this.shapeType=o.shapeType,this.imageUrl=o.imageUrl||null,this.emojiText=o.emojiText||null}addComponent(o){this.components.push(o)}init(o){for(let n=0;no.getViewportHeight()+5&&(n.outOfView=!0)}}function ie(t,o,n,e){let i=o(),s=n();const a={getViewportWidth:()=>i,getViewportHeight:()=>s,context:t},c=[];let m=0,S=Math.random();const x=.61803398875,_=performance.now();let Y=!1;function C(){const r=Math.min(i,s),l=u=>{const h=i*s;return Math.floor(u==="small"?Math.min(80,Math.max(40,h/36e3)):u==="medium"?Math.min(200,Math.max(100,h/18e3)):Math.min(300,Math.max(140,h/12e3)))};let d=0;return r<=480?d=e.maxCountSmall>0?e.maxCountSmall:l("small"):r<=960?d=e.maxCountMedium>0?e.maxCountMedium:l("medium"):d=e.maxCountLarge>0?e.maxCountLarge:l("large"),d<=0&&(d=e.maxCount>0?e.maxCount:l(r<=480?"small":r<=960?"medium":"large")),Math.max(1,d)}let y=C();function P(){const r=[];for(let h=0;h0&&r.push({kind:"shape",key:f,weight:w})}for(let h=0;h0&&r.push({kind:"emoji",text:f,weight:w})}for(let h=0;h0&&r.push({kind:"text",text:f,weight:w})}for(let h=0;h0&&r.push({kind:"media",url:f,weight:w})}if(r.length===0)return null;let l=0;for(let h=0;h0)return l/r;const d=(e.driftMin+e.driftMax)*.5,u=(e.radiusMin+e.radiusMax)*.5*e.radiusMin;return(d*2+u*.25)*60}function j(){const r=b(),l=(s+5)/Math.max(.001,r),d=y/Math.max(.001,l),u=Math.min(1.2,Math.max(.6,l*.2)),h=Math.floor(d*u),f=Math.floor(y*.45);return Math.max(8,Math.min(f,h))}function k(r,l){const d=A(r,l);return d?(c.push(d),!0):!1}function V(){if(Y||c.length>0)return;const r=j();for(let l=0;ls+5&&(d.outOfView=!0)}}}function W(){t.clearRect(0,0,i,s);for(let r=0;r=0;l--)c[l].outOfView&&c.splice(l,1);if(!Y){const l=b(),d=(s+5)/Math.max(.001,l),u=y/Math.max(.001,d);m+=u*Math.max(0,r);const h=Math.max(0,y-c.length);let f=Math.min(h,Math.floor(m));f===0&&h>0&&(f=1);const w=Math.max(1,Math.floor(y*.05));f>w&&(f=w);let T=0;for(let v=0;v0&&(m=Math.max(0,m-T))}$(r)}function F(){W()}function O(){return e.displayDurationSeconds>0&&!Y&&(performance.now()-_)/1e3>=e.displayDurationSeconds&&(Y=!0),Y?c.every(r=>r.outOfView):!1}return{engine:a,recomputeTarget(){y=C()},updateSystem:L,renderSystem:F,shouldStop:O,setViewport(r,l){i=r,s=l}}}function oe(t,o){const n=t.getContext("2d");let e=window.innerWidth,i=window.innerHeight;const s=window.devicePixelRatio||1;function a(){e=window.innerWidth,i=window.innerHeight,t.style.width=`${e}px`,t.style.height=`${i}px`,t.width=Math.floor(e*s),t.height=Math.floor(i*s),n.setTransform(s,0,0,s,0,0),c.setViewport(e,i),c.recomputeTarget()}a();const c=ie(n,()=>e,()=>i,o),m=new K(x=>{c.updateSystem(x),c.renderSystem()},()=>c.shouldStop(),()=>{n.clearRect(0,0,e,i),t.style.display="none"});function S(){a()}return window.addEventListener("resize",S),m.init(),m.update(),{stop(){m.stop(),window.removeEventListener("resize",S)}}}export{oe as r}; diff --git a/assets/圣诞拐杖.svg b/dist/assets/圣诞拐杖.svg similarity index 100% rename from assets/圣诞拐杖.svg rename to dist/assets/圣诞拐杖.svg diff --git a/assets/圣诞果.svg b/dist/assets/圣诞果.svg similarity index 100% rename from assets/圣诞果.svg rename to dist/assets/圣诞果.svg diff --git a/assets/圣诞树.svg b/dist/assets/圣诞树.svg similarity index 100% rename from assets/圣诞树.svg rename to dist/assets/圣诞树.svg diff --git a/assets/圣诞袜子.svg b/dist/assets/圣诞袜子.svg similarity index 100% rename from assets/圣诞袜子.svg rename to dist/assets/圣诞袜子.svg diff --git a/assets/圣诞雪帽.svg b/dist/assets/圣诞雪帽.svg similarity index 100% rename from assets/圣诞雪帽.svg rename to dist/assets/圣诞雪帽.svg diff --git a/assets/圣诞麋鹿.svg b/dist/assets/圣诞麋鹿.svg similarity index 100% rename from assets/圣诞麋鹿.svg rename to dist/assets/圣诞麋鹿.svg diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..912d5c3 --- /dev/null +++ b/dist/index.js @@ -0,0 +1 @@ +import{r as a}from"./assets/SnowAnimator-Dem4j_A7.js";function i(){const t=document.getElementById("effectiveAppsSnow");if(!t)return;if(typeof window.matchMedia=="function"&&window.matchMedia("(prefers-reduced-motion: reduce)").matches){t.style.display="none";return}const e=window.YooneSnowSettings||{},n={selectedShapes:Array.isArray(e.selectedShapes)?e.selectedShapes:[],mediaItems:Array.isArray(e.mediaItems)?e.mediaItems:[],emojiItems:Array.isArray(e.emojiItems)?e.emojiItems:[],textItems:Array.isArray(e.textItems)?e.textItems:[],shapeWeights:e.shapeWeights||{},mediaWeights:e.mediaWeights||{},emojiWeights:e.emojiWeights||{},textWeights:e.textWeights||{},radiusMin:typeof e.radiusMin=="number"?e.radiusMin:1,radiusMax:typeof e.radiusMax=="number"?e.radiusMax:3,driftMin:typeof e.driftMin=="number"?e.driftMin:.4,driftMax:typeof e.driftMax=="number"?e.driftMax:1,swingMin:typeof e.swingMin=="number"?e.swingMin:.2,swingMax:typeof e.swingMax=="number"?e.swingMax:1,displayDurationSeconds:typeof e.displayDurationSeconds=="number"?e.displayDurationSeconds:0,maxCount:typeof e.maxCount=="number"?e.maxCount:0,maxCountSmall:typeof e.maxCountSmall=="number"?e.maxCountSmall:0,maxCountMedium:typeof e.maxCountMedium=="number"?e.maxCountMedium:0,maxCountLarge:typeof e.maxCountLarge=="number"?e.maxCountLarge:0,assetsMap:e.assetsMap||{}};a(t,n)}document.readyState==="complete"?i():window.addEventListener("load",i); diff --git a/dist/snow.css b/dist/snow.css new file mode 100644 index 0000000..5bda517 --- /dev/null +++ b/dist/snow.css @@ -0,0 +1 @@ +#effectiveAppsSnow{display:block;position:fixed;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:1000} diff --git a/dist/snow.js b/dist/snow.js new file mode 100644 index 0000000..912d5c3 --- /dev/null +++ b/dist/snow.js @@ -0,0 +1 @@ +import{r as a}from"./assets/SnowAnimator-Dem4j_A7.js";function i(){const t=document.getElementById("effectiveAppsSnow");if(!t)return;if(typeof window.matchMedia=="function"&&window.matchMedia("(prefers-reduced-motion: reduce)").matches){t.style.display="none";return}const e=window.YooneSnowSettings||{},n={selectedShapes:Array.isArray(e.selectedShapes)?e.selectedShapes:[],mediaItems:Array.isArray(e.mediaItems)?e.mediaItems:[],emojiItems:Array.isArray(e.emojiItems)?e.emojiItems:[],textItems:Array.isArray(e.textItems)?e.textItems:[],shapeWeights:e.shapeWeights||{},mediaWeights:e.mediaWeights||{},emojiWeights:e.emojiWeights||{},textWeights:e.textWeights||{},radiusMin:typeof e.radiusMin=="number"?e.radiusMin:1,radiusMax:typeof e.radiusMax=="number"?e.radiusMax:3,driftMin:typeof e.driftMin=="number"?e.driftMin:.4,driftMax:typeof e.driftMax=="number"?e.driftMax:1,swingMin:typeof e.swingMin=="number"?e.swingMin:.2,swingMax:typeof e.swingMax=="number"?e.swingMax:1,displayDurationSeconds:typeof e.displayDurationSeconds=="number"?e.displayDurationSeconds:0,maxCount:typeof e.maxCount=="number"?e.maxCount:0,maxCountSmall:typeof e.maxCountSmall=="number"?e.maxCountSmall:0,maxCountMedium:typeof e.maxCountMedium=="number"?e.maxCountMedium:0,maxCountLarge:typeof e.maxCountLarge=="number"?e.maxCountLarge:0,assetsMap:e.assetsMap||{}};a(t,n)}document.readyState==="complete"?i():window.addEventListener("load",i); diff --git a/js/shapes/candy_cane.js b/js/shapes/candy_cane.js deleted file mode 100644 index c26ce05..0000000 --- a/js/shapes/candy_cane.js +++ /dev/null @@ -1,15 +0,0 @@ -(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 deleted file mode 100644 index 6729ef5..0000000 --- a/js/shapes/christmas_berry.js +++ /dev/null @@ -1,15 +0,0 @@ -(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 deleted file mode 100644 index b41b546..0000000 --- a/js/shapes/christmas_sock.js +++ /dev/null @@ -1,15 +0,0 @@ -(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 deleted file mode 100644 index 1ea18e5..0000000 --- a/js/shapes/christmas_tree.js +++ /dev/null @@ -1,15 +0,0 @@ -(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 deleted file mode 100644 index bd41b61..0000000 --- a/js/shapes/coin.js +++ /dev/null @@ -1,24 +0,0 @@ -(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 deleted file mode 100644 index 6360ec8..0000000 --- a/js/shapes/dot.js +++ /dev/null @@ -1,12 +0,0 @@ -(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 deleted file mode 100644 index 6634832..0000000 --- a/js/shapes/flake.js +++ /dev/null @@ -1,35 +0,0 @@ -(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 deleted file mode 100644 index e86eaea..0000000 --- a/js/shapes/index.js +++ /dev/null @@ -1,7 +0,0 @@ -(function(){ - // 初始化全局渲染注册表 用于统一管理形状渲染函数 - if (!window.YooneSnowShapeRenderers) { - // 条件判断 如果不存在则创建空对象 - window.YooneSnowShapeRenderers = {}; - } -})(); diff --git a/js/shapes/reindeer.js b/js/shapes/reindeer.js deleted file mode 100644 index 921e228..0000000 --- a/js/shapes/reindeer.js +++ /dev/null @@ -1,15 +0,0 @@ -(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 deleted file mode 100644 index c240378..0000000 --- a/js/shapes/santa_hat.js +++ /dev/null @@ -1,15 +0,0 @@ -(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 deleted file mode 100644 index 9bccb4c..0000000 --- a/js/shapes/utils.js +++ /dev/null @@ -1,80 +0,0 @@ -(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; - try { img.decoding = 'async'; } catch(e) {} - try { img.fetchPriority = 'low'; } catch(e) {} - img.onload = function(){ record.ready = true; }; - img.onerror = function(){ - // 加载失败 从缓存移除避免重复错误 - delete window.YooneSnowImageCache[imageUrl]; - }; - img.src = imageUrl; - return record; - }; - - window.YooneSnowLoadAssetViaFetch = function(imageUrl, onReady){ - if (!imageUrl || typeof imageUrl !== 'string'){ - if (typeof onReady === 'function'){ onReady(false); } - return; - } - var existing = window.YooneSnowImageCache[imageUrl]; - if (existing && existing.ready){ - if (typeof onReady === 'function'){ onReady(true); } - return; - } - if (!existing){ window.YooneSnowImageCache[imageUrl] = { img: null, ready: false }; } - if (typeof fetch === 'function' && typeof createImageBitmap === 'function'){ - fetch(imageUrl, { cache: 'force-cache' }).then(function(resp){ return resp.blob(); }).then(function(blob){ - return createImageBitmap(blob); - }).then(function(bmp){ - window.YooneSnowImageCache[imageUrl] = { img: bmp, ready: true }; - if (typeof onReady === 'function'){ onReady(true); } - }).catch(function(){ - var rec = window.YooneSnowGetOrLoadImage(imageUrl); - var fired = false; - if (rec && rec.img){ - var markReady = function(){ rec.ready = true; }; - rec.img.onload = function(){ if (!fired){ fired = true; markReady(); if (typeof onReady === 'function'){ onReady(true); } } }; - rec.img.onerror = function(){ if (!fired){ fired = true; if (typeof onReady === 'function'){ onReady(false); } } }; - } - }); - } else { - var rec2 = window.YooneSnowGetOrLoadImage(imageUrl); - var fired2 = false; - if (rec2 && rec2.img){ - var markReady2 = function(){ rec2.ready = true; }; - rec2.img.onload = function(){ if (!fired2){ fired2 = true; markReady2(); if (typeof onReady === 'function'){ onReady(true); } } }; - rec2.img.onerror = function(){ if (!fired2){ fired2 = true; if (typeof onReady === 'function'){ onReady(false); } } }; - } - } - }; - - // 居中绘制图像 根据目标中心点和宽高进行缩放绘制 - 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 deleted file mode 100644 index 09538ca..0000000 --- a/js/shapes/yuanbao.js +++ /dev/null @@ -1,56 +0,0 @@ -(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 deleted file mode 100644 index 6c962a5..0000000 --- a/js/snow-canvas.js +++ /dev/null @@ -1,625 +0,0 @@ -(function(){ - // 初始化函数 用于启动雪花效果 - function init(){ - const canvas = document.getElementById('effectiveAppsSnow'); - // 条件判断 如果未找到画布元素则不执行 - if (!canvas) return; - const prefersReducedMotion = (typeof window.matchMedia === 'function') && window.matchMedia('(prefers-reduced-motion: reduce)').matches; - if (prefersReducedMotion) { canvas.style.display = 'none'; return; } - const context = canvas.getContext('2d'); - let viewportWidth = window.innerWidth; - let viewportHeight = window.innerHeight; - const devicePixelRatio = window.devicePixelRatio || 1; - - // 读取首页显示时长设置 单位为秒 0 表示无限 - const displayDurationSeconds = (window.YooneSnowSettings && typeof window.YooneSnowSettings.displayDurationSeconds !== 'undefined') - ? Math.max(0, parseInt(window.YooneSnowSettings.displayDurationSeconds, 10) || 0) - : 0; - // 记录启动时间 用于计算已运行时间 - const startTimestamp = performance.now(); - // 标记是否已达到显示时长 到达后不再生成新粒子并不再重生 - let hasReachedDuration = false; - - const selectedShapes = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.selectedShapes) && window.YooneSnowSettings.selectedShapes.length > 0) - ? window.YooneSnowSettings.selectedShapes - : []; - const mediaItems = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.mediaItems)) - ? window.YooneSnowSettings.mediaItems - : []; - const emojiItems = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.emojiItems)) - ? window.YooneSnowSettings.emojiItems - : []; - const textItems = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.textItems)) - ? window.YooneSnowSettings.textItems - : []; - const defaultShapeWeights = { dot: 1, flake: 4, yuanbao: 1, coin: 1, santa_hat: 1, candy_cane: 1, christmas_sock: 1, christmas_tree: 1, reindeer: 1, christmas_berry: 1 }; - const shapeWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.shapeWeights && typeof window.YooneSnowSettings.shapeWeights === 'object') - ? window.YooneSnowSettings.shapeWeights - : {}; - const mediaWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.mediaWeights && typeof window.YooneSnowSettings.mediaWeights === 'object') - ? window.YooneSnowSettings.mediaWeights - : {}; - const emojiWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.emojiWeights && typeof window.YooneSnowSettings.emojiWeights === 'object') - ? window.YooneSnowSettings.emojiWeights - : {}; - const textWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.textWeights && typeof window.YooneSnowSettings.textWeights === 'object') - ? window.YooneSnowSettings.textWeights - : {}; - const shapeWeights = {}; - for (let key in defaultShapeWeights){ - const val = typeof shapeWeightsRaw[key] !== 'undefined' ? parseInt(shapeWeightsRaw[key], 10) : defaultShapeWeights[key]; - shapeWeights[key] = isNaN(val) ? defaultShapeWeights[key] : Math.max(0, val); - } - const assetsMap = (window.YooneSnowSettings && window.YooneSnowSettings.assetsMap && typeof window.YooneSnowSettings.assetsMap === 'object') - ? window.YooneSnowSettings.assetsMap - : {}; - const assetShapeKeys = Object.keys(assetsMap || {}); - window.YooneSnowAssetsReady = window.YooneSnowAssetsReady || false; - window.YooneSnowAssetQueue = window.YooneSnowAssetQueue || []; - window.YooneSnowAssetQueueRunning = window.YooneSnowAssetQueueRunning || false; - function enqueueAsset(u){ - if (!u || typeof u !== 'string'){ return; } - for (var i = 0; i < window.YooneSnowAssetQueue.length; i++){ if (window.YooneSnowAssetQueue[i] === u) return; } - window.YooneSnowAssetQueue.push(u); - } - function runAssetQueue(){ - if (window.YooneSnowAssetQueueRunning){ return; } - window.YooneSnowAssetQueueRunning = true; - function next(){ - if (window.YooneSnowAssetQueue.length === 0){ window.YooneSnowAssetQueueRunning = false; window.YooneSnowAssetsReady = true; return; } - var u = window.YooneSnowAssetQueue.shift(); - if (typeof window.YooneSnowLoadAssetViaFetch === 'function'){ - window.YooneSnowLoadAssetViaFetch(u, function(){ next(); }); - } else if (typeof window.YooneSnowGetOrLoadImage === 'function'){ - var rec = window.YooneSnowGetOrLoadImage(u); - if (rec && rec.img){ - var prevOnload = rec.img.onload; - var prevOnerror = rec.img.onerror; - rec.img.onload = function(){ try{ if (typeof prevOnload === 'function'){ prevOnload(); } }catch(e){} next(); }; - rec.img.onerror = function(){ try{ if (typeof prevOnerror === 'function'){ prevOnerror(); } }catch(e){} next(); }; - } else { next(); } - } else { - next(); - } - } - next(); - } - window.YooneSnowEnqueueAsset = enqueueAsset; - function scheduleWarmLoad(){ - var urls = []; - for (var i = 0; i < assetShapeKeys.length; i++){ var k = assetShapeKeys[i]; var u = assetsMap[k]; if (typeof u === 'string' && u){ urls.push(u); } } - for (var j = 0; j < urls.length; j++){ enqueueAsset(urls[j]); } - runAssetQueue(); - } - scheduleWarmLoad(); - // 移除单独的尺寸与偏移缩放 直接使用最小半径与最小摆动作为缩放系数 - const radiusMinRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.radiusMin !== 'undefined') - ? parseFloat(window.YooneSnowSettings.radiusMin) - : 1.0; - const radiusMaxRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.radiusMax !== 'undefined') - ? parseFloat(window.YooneSnowSettings.radiusMax) - : 3.0; - const driftMinRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.driftMin !== 'undefined') - ? parseFloat(window.YooneSnowSettings.driftMin) - : 0.4; - const driftMaxRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.driftMax !== 'undefined') - ? parseFloat(window.YooneSnowSettings.driftMax) - : 1.0; - const swingMinRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.swingMin !== 'undefined') - ? parseFloat(window.YooneSnowSettings.swingMin) - : 0.2; - const swingMaxRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.swingMax !== 'undefined') - ? parseFloat(window.YooneSnowSettings.swingMax) - : 1.0; - const radiusMin = isNaN(radiusMinRaw) ? 1 : Math.max(0, radiusMinRaw); - const radiusMax = isNaN(radiusMaxRaw) ? 3 : Math.max(radiusMin, radiusMaxRaw); - const driftMin = isNaN(driftMinRaw) ? 0.4 : Math.max(0, driftMinRaw); - const driftMax = isNaN(driftMaxRaw) ? 1.0 : Math.max(driftMin, driftMaxRaw); - const swingMin = isNaN(swingMinRaw) ? 0.2 : Math.max(0, swingMinRaw); - const swingMax = isNaN(swingMaxRaw) ? 1.0 : Math.max(swingMin, swingMaxRaw); - - // 组件与精灵类封装 基于 Cocos 风格 - class Component { - constructor(){ } - init(engine, sprite){} - update(engine, sprite, dt){} - } - class DownwardMoveComponent extends Component { - init(engine, sprite){} - update(engine, sprite, dt){ - // 使用基于时间的更新 通过帧因子平滑帧率波动 - const factor = Math.max(0.5, Math.min(2.0, dt * 60)); - const vy = (sprite.driftSpeed * 2 + sprite.radius * 0.25) * factor; - sprite.positionY += vy; - } - } - class SwingComponent extends Component { - init(engine, sprite){} - update(engine, sprite, dt){ - const factor = Math.max(0.5, Math.min(2.0, dt * 60)); - const vx = Math.sin(sprite.positionY * 0.01) * sprite.swingAmplitude * factor; - sprite.positionX += vx; - } - } - class LifetimeComponent extends Component { - init(engine, sprite){} - update(engine, sprite, dt){ - // 超出视口则标记移出 引擎将回收 - if (sprite.positionY > engine.getViewportHeight() + 5){ sprite.outOfView = true; } - } - } - class Sprite { - constructor(props){ - this.positionX = props.positionX; - this.positionY = props.positionY; - this.radius = props.radius; - this.driftSpeed = props.driftSpeed; - this.swingAmplitude = props.swingAmplitude; - this.shapeType = props.shapeType; - this.imageUrl = props.imageUrl || null; - this.emojiText = props.emojiText || null; - this.outOfView = false; - this.components = []; - } - addComponent(comp){ this.components.push(comp); } - init(engine){ for (let i = 0; i < this.components.length; i++){ try{ this.components[i].init(engine, this); }catch(e){} } } - update(engine, dt){ for (let i = 0; i < this.components.length; i++){ try{ this.components[i].update(engine, this, dt); }catch(e){} } } - render(engine){ - const ctx = engine.context; - const registry = window.YooneSnowShapeRenderers || {}; - const renderer = registry[this.shapeType] || registry['dot']; - if (typeof renderer === 'function'){ renderer(ctx, this.positionX, this.positionY, this.radius); } - } - } - class Snow extends Sprite { - constructor(props){ super(props); } - render(engine){ - const ctx = engine.context; - if (this.shapeType === 'media_image' && this.imageUrl){ - const record = window.YooneSnowGetOrLoadImage ? window.YooneSnowGetOrLoadImage(this.imageUrl) : { img: null, ready: false }; - if (record && record.ready){ - const targetHeight = this.radius * 8; - const targetWidth = targetHeight; - window.YooneSnowDrawCenteredImage(ctx, record.img, this.positionX, this.positionY, targetWidth, targetHeight); - } - return; - } - if (this.shapeType === 'emoji_text' && this.emojiText){ - ctx.save(); - const fontSize = Math.max(12, this.radius * 6); - ctx.font = String(Math.floor(fontSize)) + 'px system-ui, Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(String(this.emojiText), this.positionX, this.positionY); - ctx.restore(); - return; - } - if (this.shapeType === 'text_label' && this.emojiText){ - ctx.save(); - const fontSize = Math.max(12, this.radius * 5.5); - ctx.font = String(Math.floor(fontSize)) + 'px system-ui, -apple-system, Segoe UI, Roboto, Noto Sans'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = 'rgba(255,255,255,0.9)'; - ctx.fillText(String(this.emojiText), this.positionX, this.positionY); - ctx.restore(); - return; - } - const registry = window.YooneSnowShapeRenderers || {}; - const renderer = registry[this.shapeType] || registry['dot']; - if (typeof renderer === 'function'){ renderer(ctx, this.positionX, this.positionY, this.radius); } - } - } - // 简易引擎引用 用于组件获取上下文与视口尺寸 始终返回最新值 - const engineRef = { - getViewportWidth: function(){ return viewportWidth; }, - getViewportHeight: function(){ return viewportHeight; }, - context: context - }; - - class Animator { - constructor(onFrame, isDone, onStopped){ - this.onFrame = onFrame; - this.isDone = isDone; - this.onStopped = onStopped; - this.lastTs = performance.now(); - this.rafId = null; - this.running = false; - this.loop = this.loop.bind(this); - } - init(){ - this.lastTs = performance.now(); - this.running = false; - } - loop(){ - if (!this.running) return; - const nowTs = performance.now(); - const deltaSeconds = Math.max(0, (nowTs - this.lastTs) / 1000); - this.lastTs = nowTs; - if (typeof this.onFrame === 'function'){ this.onFrame(deltaSeconds); } - const done = typeof this.isDone === 'function' ? this.isDone() : false; - if (done){ - this.stop(); - if (typeof this.onStopped === 'function'){ this.onStopped(); } - return; - } - this.rafId = requestAnimationFrame(this.loop); - } - update(){ - this.running = true; - this.rafId = requestAnimationFrame(this.loop); - } - stop(){ - if (this.rafId !== null && typeof cancelAnimationFrame === 'function'){ - try{ cancelAnimationFrame(this.rafId); }catch(e){} - this.rafId = null; - } - this.running = false; - } - } - class SnowAnimator extends Animator { - constructor(){ - super(function(dt){ if (typeof this.onFrameImpl === 'function'){ this.onFrameImpl(dt); } }, function(){ return typeof this.isDoneImpl === 'function' ? this.isDoneImpl() : false; }, function(){ if (typeof this.onStoppedImpl === 'function'){ this.onStoppedImpl(); } }); - this.onFrameImpl = null; - this.isDoneImpl = null; - this.onStoppedImpl = null; - this.targetCount = 0; - } - computeTargetCount(){ - this.targetCount = snowflakesTargetCount; - return this.targetCount; - } - init(){ - this.computeTargetCount(); - initSnowPrefill(); - this.onFrameImpl = function(deltaSeconds){ updateSystem(deltaSeconds); renderSystem(); }; - this.isDoneImpl = function(){ return shouldStop(); }; - this.onStoppedImpl = function(){ cleanupStop(); }; - super.init(); - } - update(){ - super.update(); - } - stop(){ - super.stop(); - } - } - - // 函数 调整画布尺寸并设置像素比 保证清晰显示 - function resizeCanvas(){ - viewportWidth = window.innerWidth; - viewportHeight = window.innerHeight; - const displayWidth = viewportWidth; - const displayHeight = viewportHeight; - canvas.style.width = displayWidth + 'px'; - canvas.style.height = displayHeight + 'px'; - canvas.width = Math.floor(displayWidth * devicePixelRatio); - canvas.height = Math.floor(displayHeight * devicePixelRatio); - context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); - } - - resizeCanvas(); - - // 读取分屏最大数量设置 0 表示自动 根据屏幕最小边界划分为 small medium large - function computeAutoCount(kind){ - const area = viewportWidth * viewportHeight; - if (kind === 'small'){ - const est = area / 36000; - return Math.floor(Math.min(80, Math.max(40, est))); - } - if (kind === 'medium'){ - // 中屏适中限制到 [100 200] - const est = area / 18000; - return Math.floor(Math.min(200, Math.max(100, est))); - } - // 大屏较多但仍限制到 [140 300] - const est = area / 12000; - return Math.floor(Math.min(300, Math.max(140, est))); - } - const minDim = Math.min(viewportWidth, viewportHeight); - const rawSmall = (window.YooneSnowSettings && typeof window.YooneSnowSettings.maxCountSmall !== 'undefined') ? parseInt(window.YooneSnowSettings.maxCountSmall, 10) : 0; - const rawMedium = (window.YooneSnowSettings && typeof window.YooneSnowSettings.maxCountMedium !== 'undefined') ? parseInt(window.YooneSnowSettings.maxCountMedium, 10) : 0; - const rawLarge = (window.YooneSnowSettings && typeof window.YooneSnowSettings.maxCountLarge !== 'undefined') ? parseInt(window.YooneSnowSettings.maxCountLarge, 10) : 0; - const fallbackSingle = (window.YooneSnowSettings && typeof window.YooneSnowSettings.maxCount !== 'undefined') ? parseInt(window.YooneSnowSettings.maxCount, 10) : 0; - let snowflakesTargetCount = 0; - if (minDim <= 480){ - snowflakesTargetCount = (isNaN(rawSmall) || rawSmall <= 0) ? computeAutoCount('small') : rawSmall; - } else if (minDim <= 960){ - snowflakesTargetCount = (isNaN(rawMedium) || rawMedium <= 0) ? computeAutoCount('medium') : rawMedium; - } else { - snowflakesTargetCount = (isNaN(rawLarge) || rawLarge <= 0) ? computeAutoCount('large') : rawLarge; - } - if (snowflakesTargetCount <= 0){ - // 回退到旧单值设置 仍支持 0 自动 - snowflakesTargetCount = (isNaN(fallbackSingle) || fallbackSingle <= 0) ? computeAutoCount(minDim <= 480 ? 'small' : (minDim <= 960 ? 'medium' : 'large')) : fallbackSingle; - } - snowflakesTargetCount = Math.max(1, snowflakesTargetCount); - const snowflakes = []; - // 定义连续生成控制参数 使用时间积累的方式平滑新增 - let spawnAccumulator = 0; - let lastUpdateTimestamp = performance.now(); - let rafId = null; - // 定义黄金比例相位用于水平位置分布避免聚集 - let spawnPhase = Math.random(); - const goldenRatio = 0.61803398875; - var yooneLogEntries = Array.isArray(window.YooneSnowLogEntries) ? window.YooneSnowLogEntries : []; - window.YooneSnowLogEntries = yooneLogEntries; - function yooneLogPush(entry){ - try { - yooneLogEntries.push(Object.assign({ ts: performance.now() }, entry)); - if (yooneLogEntries.length > 1500){ yooneLogEntries.shift(); } - // 关闭默认的控制台输出 避免在高频帧中阻塞主线程导致动画卡顿 - } catch(e){} - } - window.YooneSnowGetLog = function(){ return yooneLogEntries.slice(); }; - // 函数 按权重选择形状或媒体图像 - function selectWeightedItem(){ - const items = []; - for (let sIndex = 0; sIndex < selectedShapes.length; sIndex++){ - const shapeKey = selectedShapes[sIndex]; - const weightVal = typeof shapeWeights[shapeKey] !== 'undefined' ? shapeWeights[shapeKey] : 1; - if (weightVal > 0){ - if (assetShapeKeys.indexOf(shapeKey) >= 0){ - const aurl = assetsMap[shapeKey]; - if (aurl && typeof window.YooneSnowEnqueueAsset === 'function'){ window.YooneSnowEnqueueAsset(aurl); } - } - items.push({ kind: 'shape', key: shapeKey, weight: weightVal }); - } - } - // Emoji 候選獨立加入 不依賴 Shapes 是否包含 emoji - if (emojiItems.length > 0){ - for (let eIndex = 0; eIndex < emojiItems.length; eIndex++){ - const ch = String(emojiItems[eIndex] || '').trim(); - if (ch === ''){ continue; } - const ewRaw = typeof emojiWeightsRaw[ch] !== 'undefined' ? parseInt(emojiWeightsRaw[ch], 10) : 1; - const ew = isNaN(ewRaw) ? 1 : Math.max(0, ewRaw); - if (ew > 0){ items.push({ kind: 'emoji', text: ch, weight: ew }); } - } - } - if (textItems.length > 0){ - for (let tIndex = 0; tIndex < textItems.length; tIndex++){ - const tx = String(textItems[tIndex] || '').trim(); - if (tx === ''){ continue; } - const twRaw = typeof textWeightsRaw[tx] !== 'undefined' ? parseInt(textWeightsRaw[tx], 10) : 1; - const tw = isNaN(twRaw) ? 1 : Math.max(0, twRaw); - if (tw > 0){ items.push({ kind: 'text', text: tx, weight: tw }); } - } - } - for (let mIndex = 0; mIndex < mediaItems.length; mIndex++){ - const mediaUrl = mediaItems[mIndex]; - const mediaWeight = typeof mediaWeightsRaw[mediaUrl] !== 'undefined' ? parseInt(mediaWeightsRaw[mediaUrl], 10) : 1; - const finalMediaWeight = isNaN(mediaWeight) ? 1 : Math.max(0, mediaWeight); - if (finalMediaWeight > 0){ - const rec = (typeof window.YooneSnowGetOrLoadImage === 'function' && mediaUrl) ? window.YooneSnowGetOrLoadImage(mediaUrl) : null; - if (rec && rec.ready){ items.push({ kind: 'media', url: mediaUrl, weight: finalMediaWeight }); } - } - } - if (items.length === 0){ - return null; - } - let totalWeight = 0; - for (let i = 0; i < items.length; i++){ - totalWeight += items[i].weight; - } - const r = Math.random() * totalWeight; - let acc = 0; - for (let i = 0; i < items.length; i++){ - acc += items[i].weight; - // 条件判断 如果随机值落在当前累计权重内则选择该项 - if (r <= acc){ - if (items[i].kind === 'shape'){ - return { type: items[i].key, url: null, text: null }; - } else { - if (items[i].kind === 'media'){ - return { type: 'media_image', url: items[i].url, text: null }; - } else { - if (items[i].kind === 'emoji'){ - return { type: 'emoji_text', url: null, text: items[i].text }; - } else { - return { type: 'text_label', url: null, text: items[i].text }; - } - } - } - } - } - return null; - } - function createSnowflake(preferredX, preferredY){ - const picked = selectWeightedItem(); - if (!picked){ return null; } - let chosenType = picked.type; - let chosenImageUrl = picked.url; - let chosenEmojiText = picked.text; - const sprite = new Snow({ - positionX: typeof preferredX === 'number' ? preferredX : Math.random() * viewportWidth, - positionY: typeof preferredY === 'number' ? preferredY : (-1 - Math.random() * 4), - radius: (Math.random() * (radiusMax - radiusMin) + radiusMin) * radiusMin, - driftSpeed: Math.random() * (driftMax - driftMin) + driftMin, - swingAmplitude: (Math.random() * (swingMax - swingMin) + swingMin) * swingMin, - shapeType: chosenType, - imageUrl: chosenImageUrl, - emojiText: chosenEmojiText - }); - sprite.addComponent(new DownwardMoveComponent()); - sprite.addComponent(new SwingComponent()); - sprite.addComponent(new LifetimeComponent()); - sprite.init(engineRef); - return sprite; - } - // 计算平均垂直速度 辅助估算生成速率 保证视觉连续 - function computeAverageVerticalSpeed(){ - let countInView = 0; - let speedSum = 0; - for (let idx = 0; idx < snowflakes.length; idx++){ - const flake = snowflakes[idx]; - if (flake.outOfView){ continue; } - const verticalSpeed = (flake.driftSpeed * 2 + flake.radius * 0.25) * 60; - speedSum += verticalSpeed; - countInView++; - } - if (countInView > 0){ - return speedSum / countInView; - } - const driftAverage = (driftMin + driftMax) * 0.5; - const radiusAverage = ((radiusMin + radiusMax) * 0.5) * radiusMin; - return (driftAverage * 2 + radiusAverage * 0.25) * 60; - } - - // 函数 根据视口高度 目标最大数量 与平均速度估算初始化预填充数量 - function estimateInitialPrefillCount(){ - const averageVerticalSpeed = computeAverageVerticalSpeed(); - const averageLifeSeconds = (viewportHeight + 5) / Math.max(0.001, averageVerticalSpeed); - const supplyRatePerSecond = snowflakesTargetCount / Math.max(0.001, averageLifeSeconds); - const warmupSeconds = Math.min(1.2, Math.max(0.6, averageLifeSeconds * 0.2)); - const rawCount = Math.floor(supplyRatePerSecond * warmupSeconds); - const maxInitialFraction = 0.45; - const capByFraction = Math.floor(snowflakesTargetCount * maxInitialFraction); - const boundedCount = Math.max(8, Math.min(capByFraction, rawCount)); - return boundedCount; - } - - // 函数 封装添加雪花的操作 支持指定水平与垂直位置 - function pushSnowflake(preferredX, preferredY){ - var flake = createSnowflake(preferredX, preferredY); - if (flake){ snowflakes.push(flake); return true; } - return false; - } - - // 函数 初始化预填充雪花 保持从顶部下落的感觉并避免过量 - function initSnowPrefill(){ - // 条件判断 仅在未达到时长且当前未生成过雪花时执行 - if (hasReachedDuration){ return; } - if (snowflakes.length > 0){ return; } - const initialCount = estimateInitialPrefillCount(); - yooneLogPush({ kind: 'prefill', count: initialCount, target: snowflakesTargetCount }); - for (let spawnIndex = 0; spawnIndex < initialCount; spawnIndex++){ - // 使用黄金比例相位分布水平位置 减少聚集 - const preferredX = (spawnPhase % 1) * viewportWidth; - spawnPhase = (spawnPhase + goldenRatio) % 1; - // 预填充垂直位置覆盖顶部到中段 减少中段稀疏 - const preferredY = -Math.random() * (viewportHeight * 0.4); - pushSnowflake(preferredX, preferredY); - } - } - - // 調用初始化預填充 - initSnowPrefill(); - - // 函数 更新雪花位置与视口状态 - function updateSnowflakes(deltaSeconds){ - for (let j = 0; j < snowflakes.length; j++){ - const flake = snowflakes[j]; - if (flake && typeof flake.update === 'function'){ - flake.update(engineRef, deltaSeconds); - } else { - const frameFactor = Math.max(0.5, Math.min(2.0, deltaSeconds * 60)); - flake.positionY += (flake.driftSpeed * 2 + flake.radius * 0.25) * frameFactor; - flake.positionX += Math.sin(flake.positionY * 0.01) * flake.swingAmplitude * frameFactor; - if (flake.positionY > viewportHeight + 5){ flake.outOfView = true; } - } - } - } - - // 使用全局渲染注册表 根据形状类型选择渲染函数 - - // 函数 清空画布并绘制可见雪花 - function drawSnowflakes(){ - context.clearRect(0, 0, viewportWidth, viewportHeight); - for (let k = 0; k < snowflakes.length; k++){ - const flake = snowflakes[k]; - if (flake.outOfView){ continue; } - if (flake && typeof flake.render === 'function'){ - try{ flake.render(engineRef); }catch(e){} - continue; - } - const registry = window.YooneSnowShapeRenderers || {}; - const renderer = registry[flake.shapeType] || registry['dot']; - if (typeof renderer === 'function'){ - renderer(context, flake.positionX, flake.positionY, flake.radius); - } - } - } - - // 清理停止函数 用于在达到设定时长后停止动画并释放资源 - function cleanupStop(){ - // 条件判断 如果存在窗口尺寸事件监听则移除 - if (typeof onResize === 'function') { - window.removeEventListener('resize', onResize); - } - // 清空画布并隐藏元素 - context.clearRect(0, 0, viewportWidth, viewportHeight); - canvas.style.display = 'none'; - } - - // 函数 更新系统 执行移除与补给并更新位置 - function updateSystem(deltaSeconds){ - for (let idx = snowflakes.length - 1; idx >= 0; idx--){ - // 条件判断 仅当雪花已移出视口时才从数组移除 保证雪花下到窗口底部才消失 - if (snowflakes[idx].outOfView){ snowflakes.splice(idx, 1); } - } - if (!hasReachedDuration){ - // 依据当前平均速度以及视口高度估算雪花平均生命周期和补给速率 - const averageVerticalSpeed = computeAverageVerticalSpeed(); - const averageLifeSeconds = (viewportHeight + 5) / Math.max(0.001, averageVerticalSpeed); - const supplyRatePerSecond = snowflakesTargetCount / Math.max(0.001, averageLifeSeconds); - spawnAccumulator += supplyRatePerSecond * Math.max(0, deltaSeconds); - // 条件判断 根据累积值决定此次生成数量 保证均匀补给并确保达到最大设定数量 - const availableSlots = Math.max(0, snowflakesTargetCount - snowflakes.length); - let spawnCount = Math.min(availableSlots, Math.floor(spawnAccumulator)); - // 条件判断 若仍有缺口但累积量不足一整个单位 则至少补充 1 个 - if (spawnCount === 0 && availableSlots > 0){ spawnCount = 1; } - // 条件判断 限制每帧最大生成数量避免瞬时爆发 - const maxPerFrame = Math.max(1, Math.floor(snowflakesTargetCount * 0.05)); - if (spawnCount > maxPerFrame){ spawnCount = maxPerFrame; } - if (spawnCount > 0){ - yooneLogPush({ kind: 'frame', count: spawnCount, availableSlots: availableSlots, length: snowflakes.length, target: snowflakesTargetCount, accumulator: spawnAccumulator }); - var added = 0; - for (let s = 0; s < spawnCount; s++){ - const preferredX = (spawnPhase % 1) * viewportWidth; - spawnPhase = (spawnPhase + goldenRatio) % 1; - if (pushSnowflake(preferredX, undefined)) { added++; } - } - if (added > 0){ spawnAccumulator = Math.max(0, spawnAccumulator - added); } - } - } - updateSnowflakes(deltaSeconds); - } - - // 函数 渲染系统 清空画布并绘制 - function renderSystem(){ - drawSnowflakes(); - } - - // 函数 动画主循环 包含生成 渲染 与停止逻辑 - function shouldStop(){ - if (displayDurationSeconds > 0 && !hasReachedDuration){ - const elapsedSeconds = (performance.now() - startTimestamp) / 1000; - if (elapsedSeconds >= displayDurationSeconds){ hasReachedDuration = true; } - } - if (hasReachedDuration){ - const allOut = snowflakes.every(function(f){ return f.outOfView; }); - if (allOut){ return true; } - } - return false; - } - const animator = new SnowAnimator(); - - // 定义窗口尺寸事件处理器 以便在停止时移除 - function onResize(){ - // 条件判断 保证画布尺寸与视口一致 - resizeCanvas(); - viewportWidth = window.innerWidth; - viewportHeight = window.innerHeight; - } - window.addEventListener('resize', onResize); - animator.init(); - animator.update(); - } - - // 条件判断 如果文档尚未加载则等待 DOMContentLoaded 事件 - if (document.readyState === 'complete'){ - init(); - } else { - window.addEventListener('load', init); - } -})(); diff --git a/lib/package-lock.json b/lib/package-lock.json new file mode 100644 index 0000000..92f119b --- /dev/null +++ b/lib/package-lock.json @@ -0,0 +1,957 @@ +{ + "name": "yoone-snow-lib", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "yoone-snow-lib", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^5.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/lib/package.json b/lib/package.json new file mode 100644 index 0000000..1fd86d3 --- /dev/null +++ b/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "yoone-snow-lib", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite", + "watch": "vite build --watch" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^5.0.0" + } +} diff --git a/lib/src/animator/Animator.ts b/lib/src/animator/Animator.ts new file mode 100644 index 0000000..8d12f0d --- /dev/null +++ b/lib/src/animator/Animator.ts @@ -0,0 +1,38 @@ +export class Animator { + private lastTs = performance.now() + private rafId: number | null = null + private running = false + constructor( + private onFrame: (dt: number) => void, + private isDone: () => boolean, + private onStopped?: () => void + ){} + init(){ + this.lastTs = performance.now() + this.running = false + } + private loop = () => { + if (!this.running) return + const nowTs = performance.now() + const deltaSeconds = Math.max(0, (nowTs - this.lastTs) / 1000) + this.lastTs = nowTs + this.onFrame(deltaSeconds) + if (this.isDone()){ + this.stop() + if (this.onStopped) this.onStopped() + return + } + this.rafId = requestAnimationFrame(this.loop) + } + update(){ + this.running = true + this.rafId = requestAnimationFrame(this.loop) + } + stop(){ + if (this.rafId !== null && typeof cancelAnimationFrame === 'function'){ + try { cancelAnimationFrame(this.rafId) } catch {} + this.rafId = null + } + this.running = false + } +} diff --git a/lib/src/animator/SnowAnimator.ts b/lib/src/animator/SnowAnimator.ts new file mode 100644 index 0000000..86fa087 --- /dev/null +++ b/lib/src/animator/SnowAnimator.ts @@ -0,0 +1,32 @@ +import { Animator } from './Animator' +import { createSystem, SystemSettings } from '../system/snowSystem' + +export function runSnow(canvas: HTMLCanvasElement, settings: SystemSettings){ + const context = canvas.getContext('2d') as CanvasRenderingContext2D + let viewportWidth = window.innerWidth + let viewportHeight = window.innerHeight + const dpr = window.devicePixelRatio || 1 + function resize(){ + viewportWidth = window.innerWidth + viewportHeight = window.innerHeight + canvas.style.width = `${viewportWidth}px` + canvas.style.height = `${viewportHeight}px` + canvas.width = Math.floor(viewportWidth * dpr) + canvas.height = Math.floor(viewportHeight * dpr) + context.setTransform(dpr, 0, 0, dpr, 0, 0) + system.setViewport(viewportWidth, viewportHeight) + system.recomputeTarget() + } + resize() + const system = createSystem(context, () => viewportWidth, () => viewportHeight, settings) + const animator = new Animator( + (dt) => { system.updateSystem(dt); system.renderSystem() }, + () => system.shouldStop(), + () => { context.clearRect(0, 0, viewportWidth, viewportHeight); canvas.style.display = 'none' } + ) + function onResize(){ resize() } + window.addEventListener('resize', onResize) + animator.init() + animator.update() + return { stop(){ animator.stop(); window.removeEventListener('resize', onResize) } } +} diff --git a/lib/src/assets/圣诞拐杖.svg b/lib/src/assets/圣诞拐杖.svg new file mode 100644 index 0000000..b3115f8 --- /dev/null +++ b/lib/src/assets/圣诞拐杖.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/src/assets/圣诞果.svg b/lib/src/assets/圣诞果.svg new file mode 100644 index 0000000..34dafd5 --- /dev/null +++ b/lib/src/assets/圣诞果.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/src/assets/圣诞树.svg b/lib/src/assets/圣诞树.svg new file mode 100644 index 0000000..15db561 --- /dev/null +++ b/lib/src/assets/圣诞树.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/src/assets/圣诞袜子.svg b/lib/src/assets/圣诞袜子.svg new file mode 100644 index 0000000..037ce21 --- /dev/null +++ b/lib/src/assets/圣诞袜子.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/src/assets/圣诞雪帽.svg b/lib/src/assets/圣诞雪帽.svg new file mode 100644 index 0000000..bb5ea13 --- /dev/null +++ b/lib/src/assets/圣诞雪帽.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/src/assets/圣诞麋鹿.svg b/lib/src/assets/圣诞麋鹿.svg new file mode 100644 index 0000000..2e18d37 --- /dev/null +++ b/lib/src/assets/圣诞麋鹿.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/src/engine/Component.ts b/lib/src/engine/Component.ts new file mode 100644 index 0000000..69e1427 --- /dev/null +++ b/lib/src/engine/Component.ts @@ -0,0 +1,48 @@ +export class Component { + init(_engine: EngineRef, _sprite: Sprite): void {} + update(_engine: EngineRef, _sprite: Sprite, _dt: number): void {} +} + +export interface EngineRef { + getViewportWidth(): number + getViewportHeight(): number + context: CanvasRenderingContext2D +} + +export interface SpriteProps { + positionX: number + positionY: number + radius: number + driftSpeed: number + swingAmplitude: number + shapeType: string + imageUrl?: string | null + emojiText?: string | null +} + +export class Sprite { + positionX: number + positionY: number + radius: number + driftSpeed: number + swingAmplitude: number + shapeType: string + imageUrl: string | null + emojiText: string | null + outOfView = false + components: Component[] = [] + + constructor(props: SpriteProps){ + this.positionX = props.positionX + this.positionY = props.positionY + this.radius = props.radius + this.driftSpeed = props.driftSpeed + this.swingAmplitude = props.swingAmplitude + this.shapeType = props.shapeType + this.imageUrl = props.imageUrl || null + this.emojiText = props.emojiText || null + } + addComponent(c: Component){ this.components.push(c) } + init(engine: EngineRef){ for (let i = 0; i < this.components.length; i++){ try{ this.components[i].init(engine, this) }catch{} } } + update(engine: EngineRef, dt: number){ for (let i = 0; i < this.components.length; i++){ try{ this.components[i].update(engine, this, dt) }catch{} } } +} diff --git a/lib/src/engine/DownwardMoveComponent.ts b/lib/src/engine/DownwardMoveComponent.ts new file mode 100644 index 0000000..2c04964 --- /dev/null +++ b/lib/src/engine/DownwardMoveComponent.ts @@ -0,0 +1,9 @@ +import { Component, EngineRef, Sprite } from './Component' + +export class DownwardMoveComponent extends Component { + update(engine: EngineRef, sprite: Sprite, dt: number){ + const factor = Math.max(0.5, Math.min(2.0, dt * 60)) + const vy = (sprite.driftSpeed * 2 + sprite.radius * 0.25) * factor + sprite.positionY += vy + } +} diff --git a/lib/src/engine/LifetimeComponent.ts b/lib/src/engine/LifetimeComponent.ts new file mode 100644 index 0000000..d8eba9a --- /dev/null +++ b/lib/src/engine/LifetimeComponent.ts @@ -0,0 +1,7 @@ +import { Component, EngineRef, Sprite } from './Component' + +export class LifetimeComponent extends Component { + update(engine: EngineRef, sprite: Sprite, _dt: number){ + if (sprite.positionY > engine.getViewportHeight() + 5){ sprite.outOfView = true } + } +} diff --git a/lib/src/engine/Snow.ts b/lib/src/engine/Snow.ts new file mode 100644 index 0000000..75544e8 --- /dev/null +++ b/lib/src/engine/Snow.ts @@ -0,0 +1,43 @@ +import { Sprite, SpriteProps } from './Component' +import { drawCenteredImage, getOrLoadImage } from '../utils/image' + +export class Snow extends Sprite { + render(engine: { context: CanvasRenderingContext2D }){ + const ctx = engine.context + if (this.shapeType === 'media_image' && this.imageUrl){ + const rec = getOrLoadImage(this.imageUrl) + if (rec && rec.ready && rec.img){ + const h = this.radius * 8 + const w = h + drawCenteredImage(ctx, rec.img, this.positionX, this.positionY, w, h) + } + return + } + if (this.shapeType === 'emoji_text' && this.emojiText){ + ctx.save() + const fontSize = Math.max(12, this.radius * 6) + ctx.font = `${Math.floor(fontSize)}px system-ui, Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji` + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(String(this.emojiText), this.positionX, this.positionY) + ctx.restore() + return + } + if (this.shapeType === 'text_label' && this.emojiText){ + ctx.save() + const fontSize = Math.max(12, this.radius * 5.5) + ctx.font = `${Math.floor(fontSize)}px system-ui, -apple-system, Segoe UI, Roboto, Noto Sans` + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillStyle = 'rgba(255,255,255,0.9)' + ctx.fillText(String(this.emojiText), this.positionX, this.positionY) + ctx.restore() + return + } + const registry = window.YooneSnowShapeRenderers || {} + const renderer = registry[this.shapeType] || registry['dot'] + if (typeof renderer === 'function'){ + renderer(ctx, this.positionX, this.positionY, this.radius) + } + } +} diff --git a/lib/src/engine/SwingComponent.ts b/lib/src/engine/SwingComponent.ts new file mode 100644 index 0000000..13b64e0 --- /dev/null +++ b/lib/src/engine/SwingComponent.ts @@ -0,0 +1,9 @@ +import { Component, EngineRef, Sprite } from './Component' + +export class SwingComponent extends Component { + update(engine: EngineRef, sprite: Sprite, dt: number){ + const factor = Math.max(0.5, Math.min(2.0, dt * 60)) + const vx = Math.sin(sprite.positionY * 0.01) * sprite.swingAmplitude * factor + sprite.positionX += vx + } +} diff --git a/lib/src/global.ts b/lib/src/global.ts new file mode 100644 index 0000000..eab76e7 --- /dev/null +++ b/lib/src/global.ts @@ -0,0 +1,15 @@ +export type ShapeRenderer = (context: CanvasRenderingContext2D, x: number, y: number, r: number) => void + +declare global { + interface Window { + YooneSnowShapeRenderers?: Record + YooneSnowSettings?: Record + } +} + +export function ensureRendererRegistry(): Record { + if (!window.YooneSnowShapeRenderers) { + window.YooneSnowShapeRenderers = {} + } + return window.YooneSnowShapeRenderers +} diff --git a/lib/src/index.ts b/lib/src/index.ts new file mode 100644 index 0000000..6c8c468 --- /dev/null +++ b/lib/src/index.ts @@ -0,0 +1,5 @@ +export { runSnow } from './animator/SnowAnimator' +export type { SystemSettings } from './system/snowSystem' +export { ensureRendererRegistry } from './global' +export { getOrLoadImage, loadAssetViaFetch, drawCenteredImage } from './utils/image' +export { initYooneSnow } from './main' diff --git a/lib/src/main.ts b/lib/src/main.ts new file mode 100644 index 0000000..772e047 --- /dev/null +++ b/lib/src/main.ts @@ -0,0 +1,53 @@ +import './styles/snow.css' +import './global' +import './utils/image' +import './shapes/dot' +import './shapes/flake' +import './shapes/yuanbao' +import './shapes/coin' +import './shapes/santa_hat' +import './shapes/candy_cane' +import './shapes/christmas_sock' +import './shapes/christmas_tree' +import './shapes/reindeer' +import './shapes/christmas_berry' +import { runSnow } from './animator/SnowAnimator' + +function init(){ + const canvas = document.getElementById('effectiveAppsSnow') as HTMLCanvasElement | null + if (!canvas) return + const prefersReducedMotion = (typeof window.matchMedia === 'function') && window.matchMedia('(prefers-reduced-motion: reduce)').matches + if (prefersReducedMotion) { canvas.style.display = 'none'; return } + const s = (window.YooneSnowSettings || {}) as any + const settings = { + selectedShapes: Array.isArray(s.selectedShapes) ? s.selectedShapes : [], + mediaItems: Array.isArray(s.mediaItems) ? s.mediaItems : [], + emojiItems: Array.isArray(s.emojiItems) ? s.emojiItems : [], + textItems: Array.isArray(s.textItems) ? s.textItems : [], + shapeWeights: s.shapeWeights || {}, + mediaWeights: s.mediaWeights || {}, + emojiWeights: s.emojiWeights || {}, + textWeights: s.textWeights || {}, + radiusMin: typeof s.radiusMin === 'number' ? s.radiusMin : 1.0, + radiusMax: typeof s.radiusMax === 'number' ? s.radiusMax : 3.0, + driftMin: typeof s.driftMin === 'number' ? s.driftMin : 0.4, + driftMax: typeof s.driftMax === 'number' ? s.driftMax : 1.0, + swingMin: typeof s.swingMin === 'number' ? s.swingMin : 0.2, + swingMax: typeof s.swingMax === 'number' ? s.swingMax : 1.0, + displayDurationSeconds: typeof s.displayDurationSeconds === 'number' ? s.displayDurationSeconds : 0, + maxCount: typeof s.maxCount === 'number' ? s.maxCount : 0, + maxCountSmall: typeof s.maxCountSmall === 'number' ? s.maxCountSmall : 0, + maxCountMedium: typeof s.maxCountMedium === 'number' ? s.maxCountMedium : 0, + maxCountLarge: typeof s.maxCountLarge === 'number' ? s.maxCountLarge : 0, + assetsMap: s.assetsMap || {} + } + runSnow(canvas, settings) +} + +if (document.readyState === 'complete'){ + init() +} else { + window.addEventListener('load', init) +} + +export function initYooneSnow(){ window.dispatchEvent(new Event('load')) } diff --git a/lib/src/shapes/candy_cane.ts b/lib/src/shapes/candy_cane.ts new file mode 100644 index 0000000..29e4556 --- /dev/null +++ b/lib/src/shapes/candy_cane.ts @@ -0,0 +1,14 @@ +import { ensureRendererRegistry } from '../global' +import { getOrLoadImage, drawCenteredImage } from '../utils/image' + +const registry = ensureRendererRegistry() + +registry.candy_cane = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){ + const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record : {} + const url = assets['candy_cane'] || '' + const rec = getOrLoadImage(url) + if (!rec || !rec.ready || !rec.img) return + const h = r * 8 + const w = h + drawCenteredImage(context, rec.img, x, y, w, h) +} diff --git a/lib/src/shapes/christmas_berry.ts b/lib/src/shapes/christmas_berry.ts new file mode 100644 index 0000000..9c123ab --- /dev/null +++ b/lib/src/shapes/christmas_berry.ts @@ -0,0 +1,14 @@ +import { ensureRendererRegistry } from '../global' +import { getOrLoadImage, drawCenteredImage } from '../utils/image' + +const registry = ensureRendererRegistry() + +registry.christmas_berry = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){ + const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record : {} + const url = assets['christmas_berry'] || '' + const rec = getOrLoadImage(url) + if (!rec || !rec.ready || !rec.img) return + const h = r * 8 + const w = h + drawCenteredImage(context, rec.img, x, y, w, h) +} diff --git a/lib/src/shapes/christmas_sock.ts b/lib/src/shapes/christmas_sock.ts new file mode 100644 index 0000000..2c79897 --- /dev/null +++ b/lib/src/shapes/christmas_sock.ts @@ -0,0 +1,14 @@ +import { ensureRendererRegistry } from '../global' +import { getOrLoadImage, drawCenteredImage } from '../utils/image' + +const registry = ensureRendererRegistry() + +registry.christmas_sock = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){ + const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record : {} + const url = assets['christmas_sock'] || '' + const rec = getOrLoadImage(url) + if (!rec || !rec.ready || !rec.img) return + const h = r * 8 + const w = h + drawCenteredImage(context, rec.img, x, y, w, h) +} diff --git a/lib/src/shapes/christmas_tree.ts b/lib/src/shapes/christmas_tree.ts new file mode 100644 index 0000000..51e6ac0 --- /dev/null +++ b/lib/src/shapes/christmas_tree.ts @@ -0,0 +1,14 @@ +import { ensureRendererRegistry } from '../global' +import { getOrLoadImage, drawCenteredImage } from '../utils/image' + +const registry = ensureRendererRegistry() + +registry.christmas_tree = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){ + const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record : {} + const url = assets['christmas_tree'] || '' + const rec = getOrLoadImage(url) + if (!rec || !rec.ready || !rec.img) return + const h = r * 8 + const w = h + drawCenteredImage(context, rec.img, x, y, w, h) +} diff --git a/lib/src/shapes/coin.ts b/lib/src/shapes/coin.ts new file mode 100644 index 0000000..f1895e2 --- /dev/null +++ b/lib/src/shapes/coin.ts @@ -0,0 +1,13 @@ +import { ensureRendererRegistry } from '../global' + +const registry = ensureRendererRegistry() + +registry.coin = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){ + const grd = context.createRadialGradient(x, y, r * 0.2, x, y, r) + grd.addColorStop(0, 'rgba(255,230,120,0.95)') + grd.addColorStop(1, 'rgba(240,180,60,0.85)') + context.fillStyle = grd + context.beginPath() + context.arc(x, y, r * 2.5, 0, Math.PI * 2) + context.fill() +} diff --git a/lib/src/shapes/dot.ts b/lib/src/shapes/dot.ts new file mode 100644 index 0000000..804c3bb --- /dev/null +++ b/lib/src/shapes/dot.ts @@ -0,0 +1,10 @@ +import { ensureRendererRegistry } from '../global' + +const registry = ensureRendererRegistry() + +registry.dot = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){ + context.beginPath() + context.arc(x, y, r, 0, Math.PI * 2) + context.fillStyle = 'rgba(255,255,255,0.9)' + context.fill() +} diff --git a/lib/src/shapes/flake.ts b/lib/src/shapes/flake.ts new file mode 100644 index 0000000..c899e57 --- /dev/null +++ b/lib/src/shapes/flake.ts @@ -0,0 +1,32 @@ +import { ensureRendererRegistry } from '../global' + +const registry = ensureRendererRegistry() + +registry.flake = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){ + const branchSize = r * 3 + context.save() + context.translate(x, y) + context.fillStyle = 'rgba(255,255,255,0.9)' + context.strokeStyle = 'rgba(255,255,255,0.9)' + context.lineWidth = branchSize * 0.15 + for (let i = 0; i < 6; i++){ + 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/lib/src/shapes/reindeer.ts b/lib/src/shapes/reindeer.ts new file mode 100644 index 0000000..772046f --- /dev/null +++ b/lib/src/shapes/reindeer.ts @@ -0,0 +1,14 @@ +import { ensureRendererRegistry } from '../global' +import { getOrLoadImage, drawCenteredImage } from '../utils/image' + +const registry = ensureRendererRegistry() + +registry.reindeer = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){ + const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record : {} + const url = assets['reindeer'] || '' + const rec = getOrLoadImage(url) + if (!rec || !rec.ready || !rec.img) return + const h = r * 8 + const w = h + drawCenteredImage(context, rec.img, x, y, w, h) +} diff --git a/lib/src/shapes/santa_hat.ts b/lib/src/shapes/santa_hat.ts new file mode 100644 index 0000000..79080fc --- /dev/null +++ b/lib/src/shapes/santa_hat.ts @@ -0,0 +1,14 @@ +import { ensureRendererRegistry } from '../global' +import { getOrLoadImage, drawCenteredImage } from '../utils/image' + +const registry = ensureRendererRegistry() + +registry.santa_hat = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){ + const assets = (window.YooneSnowSettings && (window.YooneSnowSettings as any).assetsMap) ? (window.YooneSnowSettings as any).assetsMap as Record : {} + const url = assets['santa_hat'] || '' + const rec = getOrLoadImage(url) + if (!rec || !rec.ready || !rec.img) return + const h = r * 8 + const w = h + drawCenteredImage(context, rec.img, x, y, w, h) +} diff --git a/lib/src/shapes/yuanbao.ts b/lib/src/shapes/yuanbao.ts new file mode 100644 index 0000000..ea1a459 --- /dev/null +++ b/lib/src/shapes/yuanbao.ts @@ -0,0 +1,19 @@ +import { ensureRendererRegistry } from '../global' + +const registry = ensureRendererRegistry() + +registry.yuanbao = function(context: CanvasRenderingContext2D, x: number, y: number, r: number){ + context.save() + context.translate(x, y) + context.scale(r * 0.08, r * 0.08) + context.fillStyle = 'rgba(255,215,0,0.9)' + context.beginPath() + context.moveTo(-20, 0) + context.quadraticCurveTo(0, -12, 20, 0) + context.quadraticCurveTo(0, 12, -20, 0) + context.fill() + context.beginPath() + context.arc(0, 0, 6, 0, Math.PI * 2) + context.fill() + context.restore() +} diff --git a/css/snow.css b/lib/src/styles/snow.css similarity index 100% rename from css/snow.css rename to lib/src/styles/snow.css diff --git a/lib/src/system/snowSystem.ts b/lib/src/system/snowSystem.ts new file mode 100644 index 0000000..8a89634 --- /dev/null +++ b/lib/src/system/snowSystem.ts @@ -0,0 +1,238 @@ +import { EngineRef, Sprite } from '../engine/Component' +import { Snow } from '../engine/Snow' +import { DownwardMoveComponent } from '../engine/DownwardMoveComponent' +import { SwingComponent } from '../engine/SwingComponent' +import { LifetimeComponent } from '../engine/LifetimeComponent' + +export interface SystemSettings { + selectedShapes: string[] + mediaItems: string[] + emojiItems: string[] + textItems: string[] + shapeWeights: Record + mediaWeights: Record + emojiWeights: Record + textWeights: Record + radiusMin: number + radiusMax: number + driftMin: number + driftMax: number + swingMin: number + swingMax: number + displayDurationSeconds: number + maxCount: number + maxCountSmall: number + maxCountMedium: number + maxCountLarge: number + assetsMap: Record +} + +export function createSystem(context: CanvasRenderingContext2D, getViewportWidth: () => number, getViewportHeight: () => number, settings: SystemSettings){ + let viewportWidth = getViewportWidth() + let viewportHeight = getViewportHeight() + const engine: EngineRef = { getViewportWidth: () => viewportWidth, getViewportHeight: () => viewportHeight, context } + const snowflakes: Snow[] = [] + let spawnAccumulator = 0 + let spawnPhase = Math.random() + const goldenRatio = 0.61803398875 + const startTs = performance.now() + let hasReachedDuration = false + + function computeTargetCount(): number { + const minDim = Math.min(viewportWidth, viewportHeight) + const auto = (kind: 'small'|'medium'|'large') => { + const area = viewportWidth * viewportHeight + if (kind === 'small') return Math.floor(Math.min(80, Math.max(40, area / 36000))) + if (kind === 'medium') return Math.floor(Math.min(200, Math.max(100, area / 18000))) + return Math.floor(Math.min(300, Math.max(140, area / 12000))) + } + let target = 0 + if (minDim <= 480){ target = settings.maxCountSmall > 0 ? settings.maxCountSmall : auto('small') } + else if (minDim <= 960){ target = settings.maxCountMedium > 0 ? settings.maxCountMedium : auto('medium') } + else { target = settings.maxCountLarge > 0 ? settings.maxCountLarge : auto('large') } + if (target <= 0){ target = settings.maxCount > 0 ? settings.maxCount : auto(minDim <= 480 ? 'small' : (minDim <= 960 ? 'medium' : 'large')) } + return Math.max(1, target) + } + let targetCount = computeTargetCount() + + function selectWeightedItem(): { type: string, url: string | null, text: string | null } | null { + const items: Array<{ kind: 'shape'|'media'|'emoji'|'text', key?: string, url?: string, text?: string, weight: number }> = [] + for (let i = 0; i < settings.selectedShapes.length; i++){ + const k = settings.selectedShapes[i] + const w = typeof settings.shapeWeights[k] !== 'undefined' ? settings.shapeWeights[k] : 1 + if (w > 0){ items.push({ kind: 'shape', key: k, weight: w }) } + } + for (let i = 0; i < settings.emojiItems.length; i++){ + const ch = String(settings.emojiItems[i] || '').trim() + if (ch === '') continue + const ew = settings.emojiWeights[ch] ?? 1 + if (ew > 0) items.push({ kind: 'emoji', text: ch, weight: ew }) + } + for (let i = 0; i < settings.textItems.length; i++){ + const tx = String(settings.textItems[i] || '').trim() + if (tx === '') continue + const tw = settings.textWeights[tx] ?? 1 + if (tw > 0) items.push({ kind: 'text', text: tx, weight: tw }) + } + for (let i = 0; i < settings.mediaItems.length; i++){ + const url = settings.mediaItems[i] + const mw = settings.mediaWeights[url] ?? 1 + if (mw > 0) items.push({ kind: 'media', url, weight: mw }) + } + if (items.length === 0) return null + let total = 0 + for (let i = 0; i < items.length; i++){ total += items[i].weight } + const r = Math.random() * total + let acc = 0 + for (let i = 0; i < items.length; i++){ + acc += items[i].weight + if (r <= acc){ + const it = items[i] + if (it.kind === 'shape') return { type: String(it.key), url: null, text: null } + if (it.kind === 'media') return { type: 'media_image', url: String(it.url), text: null } + if (it.kind === 'emoji') return { type: 'emoji_text', url: null, text: String(it.text) } + return { type: 'text_label', url: null, text: String(it.text) } + } + } + return null + } + + function createSnowflake(preferredX?: number, preferredY?: number): Snow | null { + const picked = selectWeightedItem() + if (!picked) return null + const sprite = new Snow({ + positionX: typeof preferredX === 'number' ? preferredX : Math.random() * viewportWidth, + positionY: typeof preferredY === 'number' ? preferredY : (-1 - Math.random() * 4), + radius: (Math.random() * (settings.radiusMax - settings.radiusMin) + settings.radiusMin) * settings.radiusMin, + driftSpeed: Math.random() * (settings.driftMax - settings.driftMin) + settings.driftMin, + swingAmplitude: (Math.random() * (settings.swingMax - settings.swingMin) + settings.swingMin) * settings.swingMin, + shapeType: picked.type, + imageUrl: picked.url, + emojiText: picked.text + }) + sprite.addComponent(new DownwardMoveComponent()) + sprite.addComponent(new SwingComponent()) + sprite.addComponent(new LifetimeComponent()) + sprite.init(engine) + return sprite + } + + function computeAverageVerticalSpeed(): number { + let count = 0 + let sum = 0 + for (let i = 0; i < snowflakes.length; i++){ + const f = snowflakes[i] + if (f.outOfView) continue + const v = (f.driftSpeed * 2 + f.radius * 0.25) * 60 + sum += v + count++ + } + if (count > 0) return sum / count + const driftAvg = (settings.driftMin + settings.driftMax) * 0.5 + const radiusAvg = ((settings.radiusMin + settings.radiusMax) * 0.5) * settings.radiusMin + return (driftAvg * 2 + radiusAvg * 0.25) * 60 + } + + function estimateInitialPrefillCount(): number { + const avgV = computeAverageVerticalSpeed() + const lifeSec = (viewportHeight + 5) / Math.max(0.001, avgV) + const supplyPerSec = targetCount / Math.max(0.001, lifeSec) + const warmSec = Math.min(1.2, Math.max(0.6, lifeSec * 0.2)) + const raw = Math.floor(supplyPerSec * warmSec) + const cap = Math.floor(targetCount * 0.45) + return Math.max(8, Math.min(cap, raw)) + } + + function pushSnowflake(px?: number, py?: number){ + const flake = createSnowflake(px, py) + if (flake){ snowflakes.push(flake); return true } + return false + } + + function initSnowPrefill(){ + if (hasReachedDuration) return + if (snowflakes.length > 0) return + const initial = estimateInitialPrefillCount() + for (let i = 0; i < initial; i++){ + const px = (spawnPhase % 1) * viewportWidth + spawnPhase = (spawnPhase + goldenRatio) % 1 + const py = -Math.random() * (viewportHeight * 0.4) + pushSnowflake(px, py) + } + } + + initSnowPrefill() + + function updateSnowflakes(dt: number){ + for (let i = 0; i < snowflakes.length; i++){ + const f = snowflakes[i] + if (f && typeof (f as any).update === 'function'){ + f.update(engine, dt) + } else { + const factor = Math.max(0.5, Math.min(2.0, dt * 60)) + f.positionY += (f.driftSpeed * 2 + f.radius * 0.25) * factor + f.positionX += Math.sin(f.positionY * 0.01) * f.swingAmplitude * factor + if (f.positionY > viewportHeight + 5){ f.outOfView = true } + } + } + } + + function drawSnowflakes(){ + context.clearRect(0, 0, viewportWidth, viewportHeight) + for (let i = 0; i < snowflakes.length; i++){ + const f = snowflakes[i] + if (f.outOfView) continue + (f as any).render ? (f as any).render(engine) : null + if (!(f as any).render){ + const reg = window.YooneSnowShapeRenderers || {} + const renderer = reg[f.shapeType] || reg['dot'] + if (typeof renderer === 'function'){ + renderer(context, f.positionX, f.positionY, f.radius) + } + } + } + } + + function updateSystem(dt: number){ + for (let i = snowflakes.length - 1; i >= 0; i--){ if (snowflakes[i].outOfView) snowflakes.splice(i, 1) } + if (!hasReachedDuration){ + const avgV = computeAverageVerticalSpeed() + const lifeSec = (viewportHeight + 5) / Math.max(0.001, avgV) + const supplyPerSec = targetCount / Math.max(0.001, lifeSec) + spawnAccumulator += supplyPerSec * Math.max(0, dt) + const slots = Math.max(0, targetCount - snowflakes.length) + let count = Math.min(slots, Math.floor(spawnAccumulator)) + if (count === 0 && slots > 0) count = 1 + const maxPerFrame = Math.max(1, Math.floor(targetCount * 0.05)) + if (count > maxPerFrame) count = maxPerFrame + let added = 0 + for (let s = 0; s < count; s++){ + const px = (spawnPhase % 1) * viewportWidth + spawnPhase = (spawnPhase + goldenRatio) % 1 + if (pushSnowflake(px, undefined)) added++ + } + if (added > 0){ spawnAccumulator = Math.max(0, spawnAccumulator - added) } + } + updateSnowflakes(dt) + } + + function renderSystem(){ drawSnowflakes() } + + function shouldStop(): boolean { + if (settings.displayDurationSeconds > 0 && !hasReachedDuration){ + const elapsed = (performance.now() - startTs) / 1000 + if (elapsed >= settings.displayDurationSeconds){ hasReachedDuration = true } + } + if (hasReachedDuration){ return snowflakes.every(f => f.outOfView) } + return false + } + + return { + engine, + recomputeTarget(){ targetCount = computeTargetCount() }, + updateSystem, + renderSystem, + shouldStop, + setViewport(width: number, height: number){ viewportWidth = width; viewportHeight = height } + } +} diff --git a/lib/src/utils/image.ts b/lib/src/utils/image.ts new file mode 100644 index 0000000..5349074 --- /dev/null +++ b/lib/src/utils/image.ts @@ -0,0 +1,72 @@ +import type { ShapeRenderer } from '../global' + +declare global { + interface Window { + YooneSnowImageCache?: Record + YooneSnowGetOrLoadImage?: (url: string) => { img: HTMLImageElement | ImageBitmap | null, ready: boolean } + YooneSnowLoadAssetViaFetch?: (url: string, cb: (ok: boolean) => void) => void + YooneSnowDrawCenteredImage?: (ctx: CanvasRenderingContext2D, img: HTMLImageElement | ImageBitmap, cx: number, cy: number, w: number, h: number) => void + } +} + +export function getOrLoadImage(url: string): { img: HTMLImageElement | ImageBitmap | null, ready: boolean } { + if (!url) return { img: null, ready: false } + const cache = (window.YooneSnowImageCache = window.YooneSnowImageCache || {}) + const existing = cache[url] + if (existing && existing.ready) return existing + if (existing && !existing.ready) return existing + const img = new Image() + const record = { img, ready: false } + cache[url] = record + try { ;(img as any).decoding = 'async' } catch {} + try { ;(img as any).fetchPriority = 'low' } catch {} + img.onload = function(){ record.ready = true } + img.onerror = function(){ delete cache[url] } + img.src = url + return record +} + +export function loadAssetViaFetch(url: string, onReady: (ok: boolean) => void): void { + if (!url) { onReady(false); return } + const cache = (window.YooneSnowImageCache = window.YooneSnowImageCache || {}) + const existing = cache[url] + if (existing && existing.ready) { onReady(true); return } + if (!existing) cache[url] = { img: null, ready: false } + if (typeof fetch === 'function' && typeof (window as any).createImageBitmap === 'function') { + fetch(url, { cache: 'force-cache' }).then(r => r.blob()).then(b => (window as any).createImageBitmap(b)).then((bmp: ImageBitmap) => { + cache[url] = { img: bmp, ready: true } + onReady(true) + }).catch(() => { + const rec = getOrLoadImage(url) + let fired = false + if (rec && rec.img instanceof HTMLImageElement) { + const markReady = () => { rec.ready = true } + rec.img.onload = function(){ if (!fired){ fired = true; markReady(); onReady(true) } } + rec.img.onerror = function(){ if (!fired){ fired = true; onReady(false) } } + } else { + onReady(false) + } + }) + } else { + const rec = getOrLoadImage(url) + let fired = false + if (rec && rec.img instanceof HTMLImageElement) { + const markReady = () => { rec.ready = true } + rec.img.onload = function(){ if (!fired){ fired = true; markReady(); onReady(true) } } + rec.img.onerror = function(){ if (!fired){ fired = true; onReady(false) } } + } else { + onReady(false) + } + } +} + +export function drawCenteredImage(context: CanvasRenderingContext2D, img: HTMLImageElement | ImageBitmap, centerX: number, centerY: number, width: number, height: number): void { + const dx = centerX - width / 2 + const dy = centerY - height / 2 + context.drawImage(img as any, dx, dy, width, height) +} + +// 挂载到 window 以兼容旧逻辑 +window.YooneSnowGetOrLoadImage = getOrLoadImage +window.YooneSnowLoadAssetViaFetch = loadAssetViaFetch +window.YooneSnowDrawCenteredImage = drawCenteredImage diff --git a/lib/tsconfig.json b/lib/tsconfig.json new file mode 100644 index 0000000..470743d --- /dev/null +++ b/lib/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "strict": true, + "noImplicitAny": true, + "moduleResolution": "Bundler", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "lib": ["ES2020", "DOM"], + "types": [] + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/lib/vite.config.ts b/lib/vite.config.ts new file mode 100644 index 0000000..170b234 --- /dev/null +++ b/lib/vite.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + root: '.', + publicDir: false, + build: { + outDir: '../dist', + assetsDir: 'assets', + emptyOutDir: true, + rollupOptions: { + input: { + snow: 'src/main.ts', + index: 'src/index.ts', + 'assets/圣诞雪帽': 'src/assets/圣诞雪帽.svg', + 'assets/圣诞拐杖': 'src/assets/圣诞拐杖.svg', + 'assets/圣诞袜子': 'src/assets/圣诞袜子.svg', + 'assets/圣诞树': 'src/assets/圣诞树.svg', + 'assets/圣诞麋鹿': 'src/assets/圣诞麋鹿.svg', + 'assets/圣诞果': 'src/assets/圣诞果.svg' + }, + output: { + entryFileNames: (chunk) => chunk.name === 'snow' ? 'snow.js' : '[name].js', + assetFileNames: (info) => { + const name = info.name || '' + if (name.endsWith('.css')) return 'snow.css' + return 'assets/[name][extname]' + } + } + } + } +}) diff --git a/yoone-snow.php b/yoone-snow.php index 734b510..3891c37 100644 --- a/yoone-snow.php +++ b/yoone-snow.php @@ -2,7 +2,7 @@ /* Plugin Name: Yoone Snow Description: 首页 canvas 雪花效果 -Version: 1.9.0 +Version: 2.0.0 Author: Yoone */ @@ -10,17 +10,11 @@ if (!defined('ABSPATH')) { exit; } function yoone_snow_is_enabled() { $mode = get_option('yoone_snow_display_routes_mode', 'home'); - if ($mode === 'all') { - return true; - } + if ($mode === 'all') { return true; } if ($mode === 'match') { $routes = get_option('yoone_snow_display_routes', array()); - if (is_string($routes)) { - $routes = array_filter(array_map('trim', explode("\n", $routes))); - } - if (!is_array($routes)) { - $routes = array(); - } + if (is_string($routes)) { $routes = array_filter(array_map('trim', explode("\n", $routes))); } + if (!is_array($routes)) { $routes = array(); } $requestUri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : ''; $path = parse_url($requestUri, PHP_URL_PATH); $path = is_string($path) ? $path : ''; @@ -29,14 +23,8 @@ function yoone_snow_is_enabled() { if ($r === '') { continue; } if (substr($r, -1) === '*') { $prefix = substr($r, 0, -1); - if ($prefix === '' || strpos($path, $prefix) === 0) { - return true; - } - } else { - if ($path === $r) { - return true; - } - } + if ($prefix === '' || strpos($path, $prefix) === 0) { return true; } + } else { if ($path === $r) { return true; } } } return false; } @@ -46,97 +34,23 @@ function yoone_snow_is_enabled() { function yoone_snow_enqueue_assets() { if (!yoone_snow_is_enabled()) { return; } $style_handle = 'yoone-snow-style'; - $style_src = plugins_url('css/snow.css', __FILE__); - $style_ver = @filemtime(plugin_dir_path(__FILE__) . 'css/snow.css'); - wp_register_style($style_handle, $style_src, array(), $style_ver ? (string)$style_ver : '1.1.0', 'all'); + $style_src = plugins_url('dist/snow.css', dirname(__FILE__)); + $style_ver = @filemtime(plugin_dir_path(dirname(__FILE__)) . 'dist/snow.css'); + wp_register_style($style_handle, $style_src, array(), $style_ver ? (string)$style_ver : '2.0.0', 'all'); wp_enqueue_style($style_handle); - // 注册形状渲染脚本 保证主脚本之前加载 - $shape_index_handle = 'yoone-snow-shapes-index'; - $ver_shapes_index = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/index.js'); - wp_register_script($shape_index_handle, plugins_url('js/shapes/index.js', __FILE__), array(), $ver_shapes_index ? (string)$ver_shapes_index : '1.1.0', true); - $shape_utils_handle = 'yoone-snow-shapes-utils'; - $ver_shapes_utils = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/utils.js'); - wp_register_script($shape_utils_handle, plugins_url('js/shapes/utils.js', __FILE__), array($shape_index_handle), $ver_shapes_utils ? (string)$ver_shapes_utils : '1.1.0', true); - $shape_dot_handle = 'yoone-snow-shapes-dot'; - $ver_shapes_dot = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/dot.js'); - wp_register_script($shape_dot_handle, plugins_url('js/shapes/dot.js', __FILE__), array($shape_index_handle), $ver_shapes_dot ? (string)$ver_shapes_dot : '1.1.0', true); - $shape_flake_handle = 'yoone-snow-shapes-flake'; - $ver_shapes_flake = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/flake.js'); - wp_register_script($shape_flake_handle, plugins_url('js/shapes/flake.js', __FILE__), array($shape_index_handle), $ver_shapes_flake ? (string)$ver_shapes_flake : '1.1.0', true); - $shape_yuanbao_handle = 'yoone-snow-shapes-yuanbao'; - $ver_shapes_yuanbao = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/yuanbao.js'); - wp_register_script($shape_yuanbao_handle, plugins_url('js/shapes/yuanbao.js', __FILE__), array($shape_index_handle), $ver_shapes_yuanbao ? (string)$ver_shapes_yuanbao : '1.1.0', true); - $shape_coin_handle = 'yoone-snow-shapes-coin'; - $ver_shapes_coin = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/coin.js'); - wp_register_script($shape_coin_handle, plugins_url('js/shapes/coin.js', __FILE__), array($shape_index_handle), $ver_shapes_coin ? (string)$ver_shapes_coin : '1.1.0', true); - $shape_santa_handle = 'yoone-snow-shapes-santa-hat'; - $ver_shapes_santa = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/santa_hat.js'); - wp_register_script($shape_santa_handle, plugins_url('js/shapes/santa_hat.js', __FILE__), array($shape_utils_handle), $ver_shapes_santa ? (string)$ver_shapes_santa : '1.1.0', true); - $shape_cane_handle = 'yoone-snow-shapes-candy-cane'; - $ver_shapes_cane = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/candy_cane.js'); - wp_register_script($shape_cane_handle, plugins_url('js/shapes/candy_cane.js', __FILE__), array($shape_utils_handle), $ver_shapes_cane ? (string)$ver_shapes_cane : '1.1.0', true); - $shape_sock_handle = 'yoone-snow-shapes-christmas-sock'; - $ver_shapes_sock = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_sock.js'); - wp_register_script($shape_sock_handle, plugins_url('js/shapes/christmas_sock.js', __FILE__), array($shape_utils_handle), $ver_shapes_sock ? (string)$ver_shapes_sock : '1.1.0', true); - $shape_tree_handle = 'yoone-snow-shapes-christmas-tree'; - $ver_shapes_tree = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_tree.js'); - wp_register_script($shape_tree_handle, plugins_url('js/shapes/christmas_tree.js', __FILE__), array($shape_utils_handle), $ver_shapes_tree ? (string)$ver_shapes_tree : '1.1.0', true); - $shape_reindeer_handle = 'yoone-snow-shapes-reindeer'; - $ver_shapes_reindeer = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/reindeer.js'); - wp_register_script($shape_reindeer_handle, plugins_url('js/shapes/reindeer.js', __FILE__), array($shape_utils_handle), $ver_shapes_reindeer ? (string)$ver_shapes_reindeer : '1.1.0', true); - $shape_berry_handle = 'yoone-snow-shapes-christmas-berry'; - $ver_shapes_berry = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_berry.js'); - wp_register_script($shape_berry_handle, plugins_url('js/shapes/christmas_berry.js', __FILE__), array($shape_utils_handle), $ver_shapes_berry ? (string)$ver_shapes_berry : '1.1.0', true); - - // 注册并加载主脚本 设置依赖确保顺序正确 $script_handle = 'yoone-snow-script'; - $script_src = plugins_url('js/snow-canvas.js', __FILE__); - $script_ver = @filemtime(plugin_dir_path(__FILE__) . 'js/snow-canvas.js'); - $deps = array($shape_index_handle); - $needs_utils = false; - $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(is_array($mixed_items_option) ? $mixed_items_option : array('dot','flake'), $allowed_shapes))); - if (empty($mixed_items_sanitized)) { $mixed_items_sanitized = array('dot','flake'); } - $media_urls = array(); - foreach ($mixed_items_sanitized as $key) { - if ($key === 'dot') { $deps[] = $shape_dot_handle; } - if ($key === 'flake') { $deps[] = $shape_flake_handle; } - if ($key === 'yuanbao') { $deps[] = $shape_yuanbao_handle; } - if ($key === 'coin') { $deps[] = $shape_coin_handle; } - if ($key === 'santa_hat') { $deps[] = $shape_santa_handle; $needs_utils = true; } - if ($key === 'candy_cane') { $deps[] = $shape_cane_handle; $needs_utils = true; } - if ($key === 'christmas_sock') { $deps[] = $shape_sock_handle; $needs_utils = true; } - if ($key === 'christmas_tree') { $deps[] = $shape_tree_handle; $needs_utils = true; } - if ($key === 'reindeer') { $deps[] = $shape_reindeer_handle; $needs_utils = true; } - if ($key === 'christmas_berry') { $deps[] = $shape_berry_handle; $needs_utils = true; } - } - if (!empty($media_urls)) { $needs_utils = true; } - if ($needs_utils) { $deps[] = $shape_utils_handle; } - $deps = array_values(array_unique($deps)); - wp_register_script( - $script_handle, - $script_src, - $deps, - $script_ver ? (string)$script_ver : '1.1.0', - true - ); + $script_src = plugins_url('dist/snow.js', dirname(__FILE__)); + $script_ver = @filemtime(plugin_dir_path(dirname(__FILE__)) . 'dist/snow.js'); + wp_register_script($script_handle, $script_src, array(), $script_ver ? (string)$script_ver : '2.0.0', true); wp_enqueue_script($script_handle); - // 将后端设置传递到前端脚本 变量名称为 YooneSnowSettings - // 简化设置 仅保留复选框选择的形状集合 $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))); - } + 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(); @@ -147,36 +61,22 @@ function yoone_snow_enqueue_assets() { $url = wp_get_attachment_url(intval($mid)); if ($url) { $media_urls[] = $url; - $w = isset($media_weights_option[intval($mid)]) ? intval($media_weights_option[intval($mid)]) : 1; + $intKey = intval($mid); + $strKey = (string)$intKey; + $w = 1; + if (isset($media_weights_option[$intKey])) { $w = intval($media_weights_option[$intKey]); } + elseif (isset($media_weights_option[$strKey])) { $w = intval($media_weights_option[$strKey]); } if ($w < 0) { $w = 0; } $media_weights_by_url[$url] = $w; } } - // 形状权重默认值 映射为形状键到非负整数权重 - // 权重用于控制每次生成时的相对概率 权重越大被选中越频繁 - // 权重为 0 表示可选但不参与随机生成 - $default_weights = array( - '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, - ); + + $default_weights = array('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); $saved_weights = get_option('yoone_snow_shape_weights', $default_weights); if (!is_array($saved_weights)) { $saved_weights = array(); } - // 合并用户保存的权重与默认值 并规范为非负整数 $shape_weights = array(); - foreach ($default_weights as $k => $v) { - $val = isset($saved_weights[$k]) ? intval($saved_weights[$k]) : intval($v); - if ($val < 0) { $val = 0; } - $shape_weights[$k] = $val; - } - // 读取尺寸组合设置 包含最小与最大半径 如未设置则回退到旧选项与默认值 + foreach ($default_weights as $k => $v) { $val = isset($saved_weights[$k]) ? intval($saved_weights[$k]) : intval($v); if ($val < 0) { $val = 0; } $shape_weights[$k] = $val; } + $size_group = get_option('yoone_snow_size', array('min' => 1.0, 'max' => 3.0)); $radius_min_val = isset($size_group['min']) ? floatval($size_group['min']) : floatval(get_option('yoone_snow_radius_min', 1.0)); $radius_max_val = isset($size_group['max']) ? floatval($size_group['max']) : floatval(get_option('yoone_snow_radius_max', 3.0)); @@ -188,29 +88,9 @@ function yoone_snow_enqueue_assets() { 'mediaItems' => $media_urls, 'displayDurationSeconds' => intval(get_option('yoone_snow_home_duration', 0)), 'maxCount' => intval(get_option('yoone_snow_max_count', 0)), - 'renderAccelerationEnabled' => (function(){ $v = get_option('yoone_snow_render_acceleration', 0); return intval($v) === 1; })(), - // 新增分屏最大数量设置 分别针对小中大屏 0 表示自动 - 'maxCountSmall' => (function(){ - $grp = get_option('yoone_snow_max_count_breakpoints', array('small' => 0, 'medium' => 0, 'large' => 0)); - $val = isset($grp['small']) ? intval($grp['small']) : 0; - if ($val < 0) { $val = 0; } - if ($val > 1000) { $val = 1000; } - return $val; - })(), - 'maxCountMedium' => (function(){ - $grp = get_option('yoone_snow_max_count_breakpoints', array('small' => 0, 'medium' => 0, 'large' => 0)); - $val = isset($grp['medium']) ? intval($grp['medium']) : 0; - if ($val < 0) { $val = 0; } - if ($val > 1000) { $val = 1000; } - return $val; - })(), - 'maxCountLarge' => (function(){ - $grp = get_option('yoone_snow_max_count_breakpoints', array('small' => 0, 'medium' => 0, 'large' => 0)); - $val = isset($grp['large']) ? intval($grp['large']) : 0; - if ($val < 0) { $val = 0; } - if ($val > 1000) { $val = 1000; } - return $val; - })(), + 'maxCountSmall' => (function(){ $grp = get_option('yoone_snow_max_count_breakpoints', array('small' => 0, 'medium' => 0, 'large' => 0)); $val = isset($grp['small']) ? intval($grp['small']) : 0; return max(0, min(1000, $val)); })(), + 'maxCountMedium' => (function(){ $grp = get_option('yoone_snow_max_count_breakpoints', array('small' => 0, 'medium' => 0, 'large' => 0)); $val = isset($grp['medium']) ? intval($grp['medium']) : 0; return max(0, min(1000, $val)); })(), + 'maxCountLarge' => (function(){ $grp = get_option('yoone_snow_max_count_breakpoints', array('small' => 0, 'medium' => 0, 'large' => 0)); $val = isset($grp['large']) ? intval($grp['large']) : 0; return max(0, min(1000, $val)); })(), 'radiusMin' => $radius_min_val, 'radiusMax' => $radius_max_val, 'driftMin' => floatval(get_option('yoone_snow_drift_min', 0.4)), @@ -218,204 +98,66 @@ function yoone_snow_enqueue_assets() { 'swingMin' => floatval(get_option('yoone_snow_swing_min', 0.2)), 'swingMax' => floatval(get_option('yoone_snow_swing_max', 1.0)), '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__), + 'santa_hat' => plugins_url('dist/assets/圣诞雪帽.svg', dirname(__FILE__)), + 'candy_cane' => plugins_url('dist/assets/圣诞拐杖.svg', dirname(__FILE__)), + 'christmas_sock' => plugins_url('dist/assets/圣诞袜子.svg', dirname(__FILE__)), + 'christmas_tree' => plugins_url('dist/assets/圣诞树.svg', dirname(__FILE__)), + 'reindeer' => plugins_url('dist/assets/圣诞麋鹿.svg', dirname(__FILE__)), + 'christmas_berry' => plugins_url('dist/assets/圣诞果.svg', dirname(__FILE__)), ), '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; - })(), - 'textItems' => (function(){ - $items = get_option('yoone_snow_text_items', array()); - if (!is_array($items)) { $items = array(); } - $clean = array(); - foreach ($items as $it) { - $s = trim((string)$it); - if ($s !== '') { $clean[] = $s; } - } - return $clean; - })(), - 'textWeights' => (function(){ - $map = get_option('yoone_snow_text_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; - })(), + 'emojiItems' => (function(){ $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(){ $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; })(), + 'textItems' => (function(){ $items = get_option('yoone_snow_text_items', array()); if (!is_array($items)) { $items = array(); } $clean = array(); foreach ($items as $it) { $s = trim((string)$it); if ($s !== '') { $clean[] = $s; } } return $clean; })(), + 'textWeights' => (function(){ $map = get_option('yoone_snow_text_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; })(), )); } -// 在后台设置页面加载媒体库脚本和交互脚本 用于选择 SVG 或图片 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'; - $ver_shapes_index = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/index.js'); - wp_register_script($shape_index_handle, plugins_url('js/shapes/index.js', __FILE__), array(), $ver_shapes_index ? (string)$ver_shapes_index : '1.1.0', true); - $shape_utils_handle = 'yoone-snow-shapes-utils'; - $ver_shapes_utils = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/utils.js'); - wp_register_script($shape_utils_handle, plugins_url('js/shapes/utils.js', __FILE__), array($shape_index_handle), $ver_shapes_utils ? (string)$ver_shapes_utils : '1.1.0', true); - // 各具体形状脚本 注册为依赖以保证顺序 - $shape_dot_handle = 'yoone-snow-shapes-dot'; - $ver_shapes_dot = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/dot.js'); - wp_register_script($shape_dot_handle, plugins_url('js/shapes/dot.js', __FILE__), array($shape_index_handle), $ver_shapes_dot ? (string)$ver_shapes_dot : '1.1.0', true); - $shape_flake_handle = 'yoone-snow-shapes-flake'; - $ver_shapes_flake = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/flake.js'); - wp_register_script($shape_flake_handle, plugins_url('js/shapes/flake.js', __FILE__), array($shape_index_handle), $ver_shapes_flake ? (string)$ver_shapes_flake : '1.1.0', true); - $shape_yuanbao_handle = 'yoone-snow-shapes-yuanbao'; - $ver_shapes_yuanbao = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/yuanbao.js'); - wp_register_script($shape_yuanbao_handle, plugins_url('js/shapes/yuanbao.js', __FILE__), array($shape_index_handle), $ver_shapes_yuanbao ? (string)$ver_shapes_yuanbao : '1.1.0', true); - $shape_coin_handle = 'yoone-snow-shapes-coin'; - $ver_shapes_coin = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/coin.js'); - wp_register_script($shape_coin_handle, plugins_url('js/shapes/coin.js', __FILE__), array($shape_index_handle), $ver_shapes_coin ? (string)$ver_shapes_coin : '1.1.0', true); - $shape_santa_handle = 'yoone-snow-shapes-santa-hat'; - $ver_shapes_santa = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/santa_hat.js'); - wp_register_script($shape_santa_handle, plugins_url('js/shapes/santa_hat.js', __FILE__), array($shape_utils_handle), $ver_shapes_santa ? (string)$ver_shapes_santa : '1.1.0', true); - $shape_cane_handle = 'yoone-snow-shapes-candy-cane'; - $ver_shapes_cane = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/candy_cane.js'); - wp_register_script($shape_cane_handle, plugins_url('js/shapes/candy_cane.js', __FILE__), array($shape_utils_handle), $ver_shapes_cane ? (string)$ver_shapes_cane : '1.1.0', true); - $shape_sock_handle = 'yoone-snow-shapes-christmas-sock'; - $ver_shapes_sock = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_sock.js'); - wp_register_script($shape_sock_handle, plugins_url('js/shapes/christmas_sock.js', __FILE__), array($shape_utils_handle), $ver_shapes_sock ? (string)$ver_shapes_sock : '1.1.0', true); - $shape_tree_handle = 'yoone-snow-shapes-christmas-tree'; - $ver_shapes_tree = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_tree.js'); - wp_register_script($shape_tree_handle, plugins_url('js/shapes/christmas_tree.js', __FILE__), array($shape_utils_handle), $ver_shapes_tree ? (string)$ver_shapes_tree : '1.1.0', true); - $shape_reindeer_handle = 'yoone-snow-shapes-reindeer'; - $ver_shapes_reindeer = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/reindeer.js'); - wp_register_script($shape_reindeer_handle, plugins_url('js/shapes/reindeer.js', __FILE__), array($shape_utils_handle), $ver_shapes_reindeer ? (string)$ver_shapes_reindeer : '1.1.0', true); - $shape_berry_handle = 'yoone-snow-shapes-christmas-berry'; - $ver_shapes_berry = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_berry.js'); - wp_register_script($shape_berry_handle, plugins_url('js/shapes/christmas_berry.js', __FILE__), array($shape_utils_handle), $ver_shapes_berry ? (string)$ver_shapes_berry : '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( + $bundle_handle = 'yoone-snow-bundle'; + $bundle_src = plugins_url('dist/snow.js', dirname(__FILE__)); + $bundle_ver = @filemtime(plugin_dir_path(dirname(__FILE__)) . 'dist/snow.js'); + wp_register_script($bundle_handle, $bundle_src, array(), $bundle_ver ? (string)$bundle_ver : '2.0.0', true); + wp_enqueue_script($bundle_handle); + wp_localize_script($bundle_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__), - ) - )); - - // 注册并加载后台交互脚本 该脚本复用 shapes 渲染器进行预览 - $admin_script_handle = 'yoone-snow-admin-media'; - // 设置 admin 脚本依赖所有形状脚本 以保证渲染器已就绪 - wp_register_script( - $admin_script_handle, - plugins_url('js/admin-media.js', __FILE__), - array( - $shape_index_handle, - $shape_utils_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 - ), - (@filemtime(plugin_dir_path(__FILE__) . 'js/admin-media.js') ? (string)@filemtime(plugin_dir_path(__FILE__) . 'js/admin-media.js') : '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__), + 'santa_hat' => plugins_url('dist/assets/圣诞雪帽.svg', dirname(__FILE__)), + 'candy_cane' => plugins_url('dist/assets/圣诞拐杖.svg', dirname(__FILE__)), + 'christmas_sock' => plugins_url('dist/assets/圣诞袜子.svg', dirname(__FILE__)), + 'christmas_tree' => plugins_url('dist/assets/圣诞树.svg', dirname(__FILE__)), + 'reindeer' => plugins_url('dist/assets/圣诞麋鹿.svg', dirname(__FILE__)), + 'christmas_berry' => plugins_url('dist/assets/圣诞果.svg', dirname(__FILE__)), ), 'shapeLabels' => array( - 'dot' => esc_html__('Dot', 'yoone-snow'), - 'flake' => esc_html__('Snowflake', 'yoone-snow'), - 'yuanbao' => esc_html__('Yuanbao', 'yoone-snow'), - 'coin' => esc_html__('Coin', 'yoone-snow'), - 'santa_hat' => esc_html__('Santa Hat', 'yoone-snow'), - 'candy_cane' => esc_html__('Candy Cane', 'yoone-snow'), - 'christmas_sock' => esc_html__('Christmas Sock', 'yoone-snow'), - 'christmas_tree' => esc_html__('Christmas Tree', 'yoone-snow'), - 'reindeer' => esc_html__('Reindeer', 'yoone-snow'), - 'christmas_berry' => esc_html__('Christmas Berry', 'yoone-snow'), + 'dot' => __('Dot', 'yoone-snow'), + 'flake' => __('Snowflake', 'yoone-snow'), + 'yuanbao' => __('Yuanbao', 'yoone-snow'), + 'coin' => __('Coin', 'yoone-snow'), + 'santa_hat' => __('Santa Hat', 'yoone-snow'), + 'candy_cane' => __('Candy Cane', 'yoone-snow'), + 'christmas_sock' => __('Christmas Sock', 'yoone-snow'), + 'christmas_tree' => __('Christmas Tree', 'yoone-snow'), + 'reindeer' => __('Reindeer', 'yoone-snow'), + 'christmas_berry' => __('Christmas Berry', 'yoone-snow'), ), 'i18n' => array( - 'cancel' => esc_html__('Cancel', 'yoone-snow'), - 'remove' => esc_html__('Remove', 'yoone-snow'), - 'select_images_or_svg' => esc_html__('Select images or SVG', 'yoone-snow'), - 'type' => esc_html__('Type', 'yoone-snow'), - 'default' => esc_html__('Default', 'yoone-snow'), - 'emoji' => esc_html__('Emoji', 'yoone-snow'), - 'media' => esc_html__('Media', 'yoone-snow'), - 'text' => esc_html__('Text', 'yoone-snow'), - 'select_shape' => esc_html__('Select shape', 'yoone-snow'), - 'select_emoji' => esc_html__('Select emoji', 'yoone-snow'), - 'add_images' => esc_html__('Add Images', 'yoone-snow'), - 'type_text' => esc_html__('Type text', 'yoone-snow'), - 'type_emoji_or_alias' => esc_html__('Type emoji or alias', 'yoone-snow'), - 'add_shapes_all_in_one' => esc_html__('Add shapes by type all in one list', 'yoone-snow'), - 'shapes' => esc_html__('Shapes', 'yoone-snow') - ) + 'cancel' => __('Cancel', 'yoone-snow'), + 'remove' => __('Remove', 'yoone-snow'), + 'select_images_or_svg' => __('Select images or SVG', 'yoone-snow'), + ), )); + + $admin_script_handle = 'yoone-snow-admin-media'; + wp_register_script( + $admin_script_handle, + plugins_url('js/admin-media.js', dirname(__FILE__)), + array($bundle_handle), + (@filemtime(plugin_dir_path(dirname(__FILE__)) . 'js/admin-media.js') ? (string)@filemtime(plugin_dir_path(dirname(__FILE__)) . 'js/admin-media.js') : '2.0.0'), + true + ); wp_enqueue_script($admin_script_handle); } @@ -431,650 +173,215 @@ add_action('wp_enqueue_scripts', 'yoone_snow_enqueue_assets'); add_action('wp_body_open', 'yoone_snow_render_overlay'); add_action('wp_footer', 'yoone_snow_render_overlay', 100); -// 注册设置页面和设置项 用于选择雪花形状 -function yoone_snow_register_settings() { - // 移除形状下拉选项 仅保留复选集合设置 - - // 注册 mixed 形状集合设置项 默认包含 dot 和 flake - register_setting('yoone_snow_options', 'yoone_snow_mixed_items', array( - 'type' => 'array', - 'sanitize_callback' => function($value) { - // 将输入统一为数组 并过滤到允许的集合 - $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' => array('dot','flake'), - )); - - // 添加设置分区 标题为 Snow Settings - add_settings_section( - 'yoone_snow_section', - esc_html__('Snow Settings', 'yoone-snow'), - function() { - echo '

' . esc_html__('Configure snow appearance', 'yoone-snow') . '

'; - }, - 'yoone_snow' - ); - - // 移除下拉字段 保留复选框作为唯一选择入口 - - add_settings_field( - 'yoone_snow_mixed_items', - esc_html__('Shapes', 'yoone-snow'), - function() { - // 读取默认形状列表 用于初始渲染卡片 - $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))); - } - if (!is_array($current_list)) { $current_list = array('dot','flake'); } - $options = array( - 'dot' => esc_html__('Dot', 'yoone-snow'), - 'flake' => esc_html__('Snowflake', 'yoone-snow'), - 'yuanbao' => esc_html__('Yuanbao', 'yoone-snow'), - 'coin' => esc_html__('Coin', 'yoone-snow'), - 'santa_hat' => esc_html__('Santa Hat', 'yoone-snow'), - 'candy_cane' => esc_html__('Candy Cane', 'yoone-snow'), - 'christmas_sock' => esc_html__('Christmas Sock', 'yoone-snow'), - 'christmas_tree' => esc_html__('Christmas Tree', 'yoone-snow'), - 'reindeer' => esc_html__('Reindeer', 'yoone-snow'), - 'christmas_berry' => esc_html__('Christmas Berry', 'yoone-snow'), - ); - // 形状描述映射 用于在界面提示 使用国际化函数 - $shape_descriptions = array( - 'dot' => __('Basic dot shape simple and lightweight', 'yoone-snow'), - 'flake' => __('Snowflake shape more decorative', 'yoone-snow'), - 'yuanbao' => __('Yuanbao shape festive theme', 'yoone-snow'), - 'coin' => __('Coin shape festive theme', 'yoone-snow'), - 'santa_hat' => __('Santa hat shape seasonal theme', 'yoone-snow'), - 'candy_cane' => __('Candy cane shape seasonal theme', 'yoone-snow'), - 'christmas_sock' => __('Christmas sock shape seasonal theme', 'yoone-snow'), - 'christmas_tree' => __('Christmas tree shape seasonal theme', 'yoone-snow'), - 'reindeer' => __('Reindeer shape seasonal theme', 'yoone-snow'), - 'christmas_berry' => __('Christmas berry shape seasonal theme', 'yoone-snow'), - ); - // 读取已选择的媒体与 emoji 列表 用于统一卡片渲染 - $current_media = get_option('yoone_snow_media_items', array()); - if (!is_array($current_media)) { $current_media = array(); } - $current_emojis = get_option('yoone_snow_emoji_items', array()); - if (!is_array($current_emojis)) { $current_emojis = array(); } - $current_texts = get_option('yoone_snow_text_items', array()); - if (!is_array($current_texts)) { $current_texts = array(); } - // 读取当前权重值 用于在卡片中预填 - $shape_weights_current = get_option('yoone_snow_shape_weights', array()); - if (!is_array($shape_weights_current)) { $shape_weights_current = array(); } - $media_weights_current = get_option('yoone_snow_media_weights', array()); - if (!is_array($media_weights_current)) { $media_weights_current = array(); } - $emoji_weights_current = get_option('yoone_snow_emoji_weights', array()); - if (!is_array($emoji_weights_current)) { $emoji_weights_current = array(); } - $text_weights_current = get_option('yoone_snow_text_weights', array()); - if (!is_array($text_weights_current)) { $text_weights_current = array(); } - - echo '
'; - // 渲染默认形状卡片 - foreach ($current_list as $key) { - if (!isset($options[$key])) { continue; } - echo '
'; - echo '
'; - echo '' . esc_html($options[$key]) . ''; - $shapeWeightVal = isset($shape_weights_current[$key]) ? intval($shape_weights_current[$key]) : 1; - echo ''; - echo ''; - echo ''; - echo '
'; - } - // 渲染 emoji 卡片 - foreach ($current_emojis as $emoji_char) { - $label = trim((string)$emoji_char); - if ($label === '') { continue; } - echo '
'; - echo '
' . esc_html($label) . '
'; - $emojiWeightVal = isset($emoji_weights_current[$label]) ? intval($emoji_weights_current[$label]) : 1; - echo ''; - echo ''; - echo ''; - 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'; - $mediaWeightVal = isset($media_weights_current[$safe_id]) ? intval($media_weights_current[$safe_id]) : 1; - echo ''; - echo ''; - echo ''; - echo '
'; - } - // 文本卡片 - foreach ($current_texts as $text_item) { - $label = trim((string)$text_item); - if ($label === '') { continue; } - echo '
'; - echo '
' . esc_html($label) . '
'; - $textWeightVal = isset($text_weights_current[$label]) ? intval($text_weights_current[$label]) : 1; - echo ''; - echo ''; - echo ''; - echo '
'; - } - echo '
'; - echo '
'; - echo ''; - echo '
'; - echo '
'; - echo ''; - echo ''; - echo ''; - echo '
'; - echo ''; - echo ''; - echo ''; - // 权重说明 提示用户权重影响概率且为非负整数 - echo '

' . esc_html__('Add shapes by type all in one list', 'yoone-snow') . '

'; - echo '
'; - echo '
'; - echo '

' . esc_html__('Weight controls relative probability Weight is a non negative integer Weight 0 disables a shape Probability equals shape weight divided by the sum of all shape weights Example dot 1 flake 4 flake has about four times the chance of dot', 'yoone-snow') . '

'; - }, - 'yoone_snow', - 'yoone_snow_section' - ); - - // 移除單獨 emoji 字段 渲染與交互已整合到 Shapes 字段 - - register_setting('yoone_snow_options', 'yoone_snow_shape_weights', array( - 'type' => 'array', - 'sanitize_callback' => function($value) { - $allowed = array('dot','flake','yuanbao','coin','santa_hat','candy_cane','christmas_sock','christmas_tree','reindeer','christmas_berry'); - $defaults = array( - '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, - ); - if (!is_array($value)) { $value = array(); } - $clean = array(); - foreach ($allowed as $key) { - $num = isset($value[$key]) ? intval($value[$key]) : (isset($defaults[$key]) ? intval($defaults[$key]) : 1); - if ($num < 0) { $num = 0; } - $clean[$key] = $num; - } - return $clean; - }, - 'default' => array( - '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, - ), - )); - - // 形状权重字段将移动到媒体形状字段之后 以满足界面顺序需求 - - // 注册媒体形状集合 设置项保存为附件 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(), - )); - - // 移除單獨 Media 字段 渲染與交互已整合到 Shapes 字段 - - // 注册媒体权重设置项 将附件 ID 映射到权重数值 默认 1 - register_setting('yoone_snow_options', 'yoone_snow_media_weights', array( - 'type' => 'array', - 'sanitize_callback' => function($value) { - // 将输入统一为附件 ID 到非负整数权重的映射 - if (!is_array($value)) { $value = array(); } - $clean = array(); - foreach ($value as $id => $num) { - $aid = intval($id); - $weight = intval($num); - if ($aid > 0) { - if ($weight < 0) { $weight = 0; } - $clean[$aid] = $weight; - } - } - return $clean; - }, - 'default' => array(), - )); - - // 移除單獨 Weights 字段 權重輸入已置於 Shapes 字段下方 - - // 注册 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(), - )); - - register_setting('yoone_snow_options', 'yoone_snow_text_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(), - )); - - register_setting('yoone_snow_options', 'yoone_snow_text_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', - 'sanitize_callback' => function($value) { - // 将输入转换为非负整数 单位为秒 0 表示无限 - $num = intval($value); - if ($num < 0) { $num = 0; } - return $num; - }, - 'default' => 0, - )); - - // 添加首页显示时长字段 输入为数字最小值为 0 - add_settings_field( - 'yoone_snow_home_duration', - esc_html__('Display Duration Seconds', 'yoone-snow'), - function() { - // 读取当前设置值 并渲染数字输入框 - $current = intval(get_option('yoone_snow_home_duration', 0)); - echo ''; - echo '

' . esc_html__('Duration in seconds for snow on home 0 means infinite', 'yoone-snow') . '

'; - }, - 'yoone_snow', - 'yoone_snow_section' - ); - - // 注册目标在屏最大数量设置项 0 表示自动根据视口面积 - register_setting('yoone_snow_options', 'yoone_snow_max_count', array( - 'type' => 'integer', - 'sanitize_callback' => function($value) { - $num = intval($value); - if ($num < 0) { $num = 0; } - // 上限保护 防止过大影响性能 - if ($num > 1000) { $num = 1000; } - return $num; - }, - 'default' => 0, - )); - - // 添加输入字段 用于设置在屏最大数量 0 表示自动 - add_settings_field( - 'yoone_snow_max_count', - esc_html__('Max Snowflakes On Screen', 'yoone-snow'), - function() { - $current = intval(get_option('yoone_snow_max_count', 0)); - echo ''; - echo '

' . esc_html__('0 means auto based on viewport area upper bound 1000', 'yoone-snow') . '

'; - }, - 'yoone_snow', - 'yoone_snow_section' - ); - - // 渲染加速开关 设置是否跳过双门限并立即队列加载 - register_setting('yoone_snow_options', 'yoone_snow_render_acceleration', array( - 'type' => 'integer', - 'sanitize_callback' => function($value) { - $num = intval($value); - return $num === 1 ? 1 : 0; - }, - 'default' => 0, - )); - add_settings_field( - 'yoone_snow_render_acceleration', - esc_html__('Render Acceleration', 'yoone-snow'), - function() { - $enabled = intval(get_option('yoone_snow_render_acceleration', 0)) === 1; - echo ''; - echo '

' . esc_html__('If enabled assets load immediately with sequential queue may increase early network usage', 'yoone-snow') . '

'; - }, - 'yoone_snow', - 'yoone_snow_section' - ); - - // 新增分屏最大数量设置 0 表示自动 根据屏幕大小采用不同默认计算 - register_setting('yoone_snow_options', 'yoone_snow_max_count_breakpoints', array( - 'type' => 'array', - 'sanitize_callback' => function($value) { - if (!is_array($value)) { $value = array(); } - $s = isset($value['small']) ? intval($value['small']) : 0; - $m = isset($value['medium']) ? intval($value['medium']) : 0; - $l = isset($value['large']) ? intval($value['large']) : 0; - if ($s < 0) { $s = 0; } if ($m < 0) { $m = 0; } if ($l < 0) { $l = 0; } - if ($s > 1000) { $s = 1000; } if ($m > 1000) { $m = 1000; } if ($l > 1000) { $l = 1000; } - return array('small' => $s, 'medium' => $m, 'large' => $l); - }, - 'default' => array('small' => 0, 'medium' => 0, 'large' => 0), - )); - add_settings_field( - 'yoone_snow_max_count_breakpoints', - esc_html__('Max Count by Screen Size', 'yoone-snow'), - function() { - $grp = get_option('yoone_snow_max_count_breakpoints', array('small' => 0, 'medium' => 0, 'large' => 0)); - $s = isset($grp['small']) ? intval($grp['small']) : 0; - $m = isset($grp['medium']) ? intval($grp['medium']) : 0; - $l = isset($grp['large']) ? intval($grp['large']) : 0; - echo ''; - echo ''; - echo ''; - echo '

' . esc_html__('0 uses default auto by viewport small<=480 medium<=960 large>960', 'yoone-snow') . '

'; - }, - 'yoone_snow', - 'yoone_snow_section' - ); - - // 尺寸组合设置 使用单一选项 snow size 存储最小与最大半径 - register_setting('yoone_snow_options', 'yoone_snow_size', array( - 'type' => 'array', - 'sanitize_callback' => function($value) { - // 将输入统一为包含 min 与 max 的数值数组 并保证非负与 max 不小于 min - if (!is_array($value)) { $value = array(); } - $min = isset($value['min']) ? floatval($value['min']) : 1.0; - $max = isset($value['max']) ? floatval($value['max']) : 3.0; - if ($min < 0) { $min = 0.0; } - if ($max < $min) { $max = $min; } - return array('min' => $min, 'max' => $max); - }, - 'default' => array('min' => 1.0, 'max' => 3.0), - )); - - add_settings_field( - 'yoone_snow_size', - esc_html__('Snow Size', 'yoone-snow'), - function() { - // 渲染组合输入 使用同一选项保存最小与最大半径 - $grp = get_option('yoone_snow_size', array('min' => 1.0, 'max' => 3.0)); - $min = isset($grp['min']) ? floatval($grp['min']) : 1.0; - $max = isset($grp['max']) ? floatval($grp['max']) : 3.0; - echo ''; - echo ''; - echo '

' . esc_html__('Random radius in [min max] single option', 'yoone-snow') . '

'; - }, - 'yoone_snow', - 'yoone_snow_section' - ); - - // 漂移速度与摆动幅度的随机范围设置 保持独立选项 - - register_setting('yoone_snow_options', 'yoone_snow_drift_min', array( - 'type' => 'number', - 'sanitize_callback' => function($value) { - $num = floatval($value); - if ($num < 0) { $num = 0; } - return $num; - }, - 'default' => 0.4, - )); - register_setting('yoone_snow_options', 'yoone_snow_drift_max', array( - 'type' => 'number', - 'sanitize_callback' => function($value) { - $min = floatval(get_option('yoone_snow_drift_min', 0.4)); - $num = floatval($value); - if ($num < $min) { $num = $min; } - return $num; - }, - 'default' => 1.0, - )); - add_settings_field( - 'yoone_snow_drift_range', - esc_html__('Drift Speed Random Range', 'yoone-snow'), - function() { - $min = floatval(get_option('yoone_snow_drift_min', 0.4)); - $max = floatval(get_option('yoone_snow_drift_max', 1.0)); - echo ''; - echo ''; - echo '

' . esc_html__('Random vertical drift speed base in [min max]', 'yoone-snow') . '

'; - }, - 'yoone_snow', - 'yoone_snow_section' - ); - - register_setting('yoone_snow_options', 'yoone_snow_swing_min', array( - 'type' => 'number', - 'sanitize_callback' => function($value) { - $num = floatval($value); - if ($num < 0) { $num = 0; } - return $num; - }, - 'default' => 0.2, - )); - register_setting('yoone_snow_options', 'yoone_snow_swing_max', array( - 'type' => 'number', - 'sanitize_callback' => function($value) { - $min = floatval(get_option('yoone_snow_swing_min', 0.2)); - $num = floatval($value); - if ($num < $min) { $num = $min; } - return $num; - }, - 'default' => 1.0, - )); - add_settings_field( - 'yoone_snow_swing_range', - esc_html__('Swing Amplitude Random Range', 'yoone-snow'), - function() { - $min = floatval(get_option('yoone_snow_swing_min', 0.2)); - $max = floatval(get_option('yoone_snow_swing_max', 1.0)); - echo ''; - echo ''; - echo '

' . esc_html__('Random horizontal swing amplitude base in [min max] before offset scale', 'yoone-snow') . '

'; - }, - 'yoone_snow', - 'yoone_snow_section' - ); - - // 路由显示模式设置 - register_setting('yoone_snow_options', 'yoone_snow_display_routes_mode', array( - 'type' => 'string', - 'sanitize_callback' => function($value) { - $val = strtolower(trim((string)$value)); - $allowed = array('home','all','match'); - return in_array($val, $allowed, true) ? $val : 'home'; - }, - 'default' => 'home', - )); - add_settings_field( - 'yoone_snow_display_routes_mode', - esc_html__('Display Routes Mode', 'yoone-snow'), - function() { - $current = get_option('yoone_snow_display_routes_mode', 'home'); - echo ''; - echo ''; - echo ''; - echo '

' . esc_html__('Default Home Only choose All Pages to enable globally choose Match URL Path to enable only for matched routes', 'yoone-snow') . '

'; - }, - 'yoone_snow', - 'yoone_snow_section' - ); - // 路由匹配规则列表设置 - register_setting('yoone_snow_options', 'yoone_snow_display_routes', array( - 'type' => 'array', - 'sanitize_callback' => function($value) { - if (is_string($value)) { - $value = preg_split('/\\r?\\n/', $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(), - )); - add_settings_field( - 'yoone_snow_display_routes', - esc_html__('Match Routes', 'yoone-snow'), - function() { - $current = get_option('yoone_snow_display_routes', array()); - if (is_string($current)) { - $current = preg_split('/\\r?\\n/', $current); - } - if (!is_array($current)) { $current = array(); } - $text = implode("\n", array_map('strval', $current)); - $placeholder = esc_attr__("Example /about\n/blog/*\n/shop", 'yoone-snow'); - echo ''; - echo '

' . esc_html__('One rule per line compare against request path example exact path /about prefix match with trailing star /blog/*', 'yoone-snow') . '

'; - }, - 'yoone_snow', - 'yoone_snow_section' - ); -} - -// 添加设置页面到后台菜单 条目在设置菜单下 -function yoone_snow_add_settings_page() { - add_options_page( - esc_html__('Yoone Snow', 'yoone-snow'), - esc_html__('Yoone Snow', 'yoone-snow'), - 'manage_options', - 'yoone_snow', - function() { - // 渲染设置页面 表单提交到 options.php - echo '
'; - echo '

' . esc_html__('Yoone Snow', 'yoone-snow') . '

'; - echo '
'; - settings_fields('yoone_snow_options'); - do_settings_sections('yoone_snow'); - submit_button(); - echo '
'; - echo '
'; - } - ); -} - -// 在 admin 初始化时注册设置 在 admin 菜单挂载页面 -add_action('admin_init', 'yoone_snow_register_settings'); -add_action('admin_menu', 'yoone_snow_add_settings_page'); +function yoone_snow_load_textdomain() { load_plugin_textdomain('yoone-snow', false, dirname(plugin_basename(__FILE__)) . '/languages'); } +add_action('plugins_loaded', 'yoone_snow_load_textdomain'); add_action('admin_enqueue_scripts', 'yoone_snow_admin_enqueue'); -// 在插件列表行添加 Settings 链接 指向设置页面 -function yoone_snow_plugin_action_links($links) { - // 构造设置页面链接 使用 admin_url 保证后台路径正确 - $settingsUrl = admin_url('options-general.php?page=yoone_snow'); - $settingsLink = '' . esc_html__('Settings', 'yoone-snow') . ''; - // 将设置链接插入到最前面 便于用户点击 - array_unshift($links, $settingsLink); - return $links; +function yoone_snow_admin_menu() { + add_options_page(__('Yoone Snow', 'yoone-snow'), __('Yoone Snow', 'yoone-snow'), 'manage_options', 'yoone_snow', 'yoone_snow_render_settings_page'); } +add_action('admin_menu', 'yoone_snow_admin_menu'); -// 绑定到当前插件的 action links 钩子 使用 plugin_basename 计算插件标识 -add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'yoone_snow_plugin_action_links'); - -function yoone_snow_load_textdomain() { - load_plugin_textdomain('yoone-snow', false, dirname(plugin_basename(__FILE__)) . '/languages'); +function yoone_snow_render_settings_page() { + if (!current_user_can('manage_options')) { wp_die(__('Sorry, you are not allowed to access this page.')); } + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['yoone_snow_save_settings'])) { + check_admin_referer('yoone_snow_save_settings'); + $mixed = isset($_POST['yoone_snow_mixed_items']) && is_array($_POST['yoone_snow_mixed_items']) ? array_values(array_filter(array_map('sanitize_text_field', $_POST['yoone_snow_mixed_items']))) : array(); + update_option('yoone_snow_mixed_items', $mixed); + $shape_weights = isset($_POST['yoone_snow_shape_weights']) && is_array($_POST['yoone_snow_shape_weights']) ? array_map('intval', $_POST['yoone_snow_shape_weights']) : array(); + foreach ($shape_weights as $k => $v) { if (!is_string($k)) { unset($shape_weights[$k]); } else { $shape_weights[$k] = max(0, intval($v)); } } + update_option('yoone_snow_shape_weights', $shape_weights); + $media_ids = isset($_POST['yoone_snow_media_items']) && is_array($_POST['yoone_snow_media_items']) ? array_map('intval', $_POST['yoone_snow_media_items']) : array(); + update_option('yoone_snow_media_items', $media_ids); + $media_weights = isset($_POST['yoone_snow_media_weights']) && is_array($_POST['yoone_snow_media_weights']) ? array_map('intval', $_POST['yoone_snow_media_weights']) : array(); + foreach ($media_weights as $k => $v) { $media_weights[$k] = max(0, intval($v)); } + update_option('yoone_snow_media_weights', $media_weights); + $emoji_items = isset($_POST['yoone_snow_emoji_items']) && is_array($_POST['yoone_snow_emoji_items']) ? array_values(array_filter(array_map('sanitize_text_field', $_POST['yoone_snow_emoji_items']))) : array(); + update_option('yoone_snow_emoji_items', $emoji_items); + $emoji_weights = isset($_POST['yoone_snow_emoji_weights']) && is_array($_POST['yoone_snow_emoji_weights']) ? array_map('intval', $_POST['yoone_snow_emoji_weights']) : array(); + foreach ($emoji_weights as $k => $v) { if (!is_string($k)) { unset($emoji_weights[$k]); } else { $emoji_weights[$k] = max(0, intval($v)); } } + update_option('yoone_snow_emoji_weights', $emoji_weights); + $text_items = isset($_POST['yoone_snow_text_items']) && is_array($_POST['yoone_snow_text_items']) ? array_values(array_filter(array_map('sanitize_text_field', $_POST['yoone_snow_text_items']))) : array(); + update_option('yoone_snow_text_items', $text_items); + $text_weights = isset($_POST['yoone_snow_text_weights']) && is_array($_POST['yoone_snow_text_weights']) ? array_map('intval', $_POST['yoone_snow_text_weights']) : array(); + foreach ($text_weights as $k => $v) { if (!is_string($k)) { unset($text_weights[$k]); } else { $text_weights[$k] = max(0, intval($v)); } } + update_option('yoone_snow_text_weights', $text_weights); + $display_duration = isset($_POST['yoone_snow_home_duration']) ? intval($_POST['yoone_snow_home_duration']) : 0; + update_option('yoone_snow_home_duration', max(0, $display_duration)); + $max_count = isset($_POST['yoone_snow_max_count']) ? intval($_POST['yoone_snow_max_count']) : 0; + update_option('yoone_snow_max_count', max(0, min(1000, $max_count))); + $max_breakpoints = array( + 'small' => isset($_POST['yoone_snow_max_count_small']) ? intval($_POST['yoone_snow_max_count_small']) : 0, + 'medium' => isset($_POST['yoone_snow_max_count_medium']) ? intval($_POST['yoone_snow_max_count_medium']) : 0, + 'large' => isset($_POST['yoone_snow_max_count_large']) ? intval($_POST['yoone_snow_max_count_large']) : 0 + ); + foreach ($max_breakpoints as $k => $v) { $max_breakpoints[$k] = max(0, min(1000, intval($v))); } + update_option('yoone_snow_max_count_breakpoints', $max_breakpoints); + $radius_min = isset($_POST['yoone_snow_radius_min']) ? floatval($_POST['yoone_snow_radius_min']) : 1.0; + $radius_max = isset($_POST['yoone_snow_radius_max']) ? floatval($_POST['yoone_snow_radius_max']) : 3.0; + if ($radius_min < 0) { $radius_min = 0.0; } + if ($radius_max < $radius_min) { $radius_max = $radius_min; } + update_option('yoone_snow_size', array('min' => $radius_min, 'max' => $radius_max)); + $drift_min = isset($_POST['yoone_snow_drift_min']) ? floatval($_POST['yoone_snow_drift_min']) : 0.4; + $drift_max = isset($_POST['yoone_snow_drift_max']) ? floatval($_POST['yoone_snow_drift_max']) : 1.0; + update_option('yoone_snow_drift_min', $drift_min); + update_option('yoone_snow_drift_max', $drift_max); + $swing_min = isset($_POST['yoone_snow_swing_min']) ? floatval($_POST['yoone_snow_swing_min']) : 0.2; + $swing_max = isset($_POST['yoone_snow_swing_max']) ? floatval($_POST['yoone_snow_swing_max']) : 1.0; + update_option('yoone_snow_swing_min', $swing_min); + update_option('yoone_snow_swing_max', $swing_max); + $mode = isset($_POST['yoone_snow_display_routes_mode']) ? sanitize_text_field($_POST['yoone_snow_display_routes_mode']) : 'home'; + if (!in_array($mode, array('home','all','match'), true)) { $mode = 'home'; } + update_option('yoone_snow_display_routes_mode', $mode); + $routes_raw = isset($_POST['yoone_snow_display_routes']) ? (string)$_POST['yoone_snow_display_routes'] : ''; + update_option('yoone_snow_display_routes', $routes_raw); + echo '

' . esc_html__('Settings saved', 'yoone-snow') . '

'; + } + $mixed_items = get_option('yoone_snow_mixed_items', array('dot','flake')); + if (is_string($mixed_items)) { $mixed_items = array_filter(array_map('trim', explode(',', $mixed_items))); } + $shape_weights2 = get_option('yoone_snow_shape_weights', array()); + $emoji_items2 = get_option('yoone_snow_emoji_items', array()); + $emoji_weights2 = get_option('yoone_snow_emoji_weights', array()); + $text_items2 = get_option('yoone_snow_text_items', array()); + $text_weights2 = get_option('yoone_snow_text_weights', array()); + $media_ids2 = get_option('yoone_snow_media_items', array()); + $media_weights2 = get_option('yoone_snow_media_weights', array()); + $size_group2 = get_option('yoone_snow_size', array('min' => 1.0, 'max' => 3.0)); + $radius_min2 = isset($size_group2['min']) ? floatval($size_group2['min']) : 1.0; + $radius_max2 = isset($size_group2['max']) ? floatval($size_group2['max']) : 3.0; + $home_duration2 = intval(get_option('yoone_snow_home_duration', 0)); + $max_count2 = intval(get_option('yoone_snow_max_count', 0)); + $breakpoints2 = get_option('yoone_snow_max_count_breakpoints', array('small'=>0,'medium'=>0,'large'=>0)); + $display_mode2 = get_option('yoone_snow_display_routes_mode', 'home'); + $display_routes2 = get_option('yoone_snow_display_routes', ''); + ?> +
+

+
+ + +

+

+
+
+ +
+ + +
+ + + + +
+ +
+
+ + + + +
+ + +
+
+ + + +
+ + +
+ media + + + + +
+ + +
+
+ + + +
+ +
+

+ +

+ +

+ + + +

+

+

+ + +

+

Drift Speed Random Range

+

+ + +

+

Swing Amplitude Random Range

+

+ + +

+

+

+ + + +

+

+ +

+

+
+
+