Files
workbuddy_skills/oil-ui-pro/assets/cards/particle-ring/card.js
T
admin d9445b5888 oil-ui-pro 0.15.4 → 0.22.2(上游 ui.oiloil.org 正式包)+ 记 P0-98 跨盘 rename 坑
- oil-ui-pro 升到 0.22.2:references 13→28 份(新增 16、删 imagery.md)、assets 增 cards/(22 组)与 fonts/、SKILL.md 顶层五步结构未改名。
- 升级路径:官方 `npx github:oil-oil/oil-cli update` 稳定失败(error=replace、报「检查目录权限」)。
  真身为 EXDEV —— 解压在 os.tmpdir()(C 盘)、技能目录在 E 盘,Windows 上 rename 不能跨盘;
  CLI 的 catch 把非 CliError 原生异常一律兜底成 replaceFailed,真错误一个字不吐。
  做法=借其 prepare() 只做下载+校验,再 cp 到同盘 stage、rename 交换;官方命令复验 up_to_date。
- 回归:product-planning 按名引用的 visual-review.md/tools.md/design-direction.md/style-explorer.md 全部健在;命名闸门 2061|0|27。
- session-mechanism/references/pitfalls.md 新增 P0-98(含通则:更新器把原生错误兜底成指错方向的人话)。
2026-10-14 10:21:46 +08:00

43 lines
4.5 KiB
JavaScript

import {random, simplex, loop, fitCanvas, glProgram, glNoise, glColor, watchColors, shell, clamp, mix, mixColor, rgb, TAU,staticMotion} from '../shared/runtime.js';
export function mount(element, options = {}) {
const cleanShell=shell(element,options);
const garmentGradient=element.querySelector('linearGradient');
if(garmentGradient){const old=garmentGradient.id,id='garment-'+crypto.randomUUID();garmentGradient.id=id;for(const path of element.querySelectorAll('svg [fill]'))if(path.getAttribute('fill')===`url(#${old})`)path.setAttribute('fill',`url(#${id})`);}
let canvas = element.querySelector('canvas'), palette, w = 1, h = 1, dpr = 1;
const rng=random(91), count=matchMedia('(max-width: 650px)').matches?8000:16000;
const data=Array.from({length:count},()=>[rng()*TAU,rng(),.6+Math.pow(rng(),3)*3.4,.25+.75*Math.pow(rng(),1.3)]);
const vertex=`precision highp float;attribute vec4 a_seed;uniform vec2 u_size,u_mouse;uniform float u_time,u_dpr,u_active;varying float v_alpha;varying float v_y;${glNoise}
void main(){vec2 unit=vec2(cos(a_seed.x),sin(a_seed.x));float r=u_size.x*.59;
float fine=simplex(unit*45.+a_seed.y*15.+u_time*.075),coarse=simplex(unit*8.+a_seed.y*4.+u_time*.035);
float coreWidth=mix(.008,.030,smoothstep(-.6,.6,simplex(unit*2.2+vec2(u_time*.14,-u_time*.10))));
float scatter=a_seed.y<.6?(fine*.75+coarse*.25)*coreWidth*r:(fine*150.+coarse*45.)*pow((a_seed.y-.6)/.4,2.4);
vec2 base=vec2(u_size.x*.5,u_size.y*.43)+unit*r;
float recover=(1.-smoothstep(0.,u_size.x*.29,distance(base,u_mouse)))*u_active;
vec2 p=base+unit*scatter*(1.-recover);
gl_Position=vec4(p.x/u_size.x*2.-1.,1.-p.y/u_size.y*2.,0.,1.);gl_PointSize=a_seed.z*u_dpr;v_alpha=a_seed.w;v_y=clamp(p.y/u_size.y,0.,1.);
}`;
const fragment=`precision mediump float;uniform vec3 u_a,u_b;varying float v_alpha,v_y;${glColor} void main(){float r=length(gl_PointCoord-.5);float alpha=(1.-smoothstep(.15,.5,r))*v_alpha;gl_FragColor=vec4(pigment(u_b,u_a,v_y),alpha);}`;
let gpu=glProgram(canvas,vertex,fragment),ctx=null;
if(!gpu){ const replacement=canvas.cloneNode();canvas.replaceWith(replacement);canvas=replacement;ctx=canvas.getContext('2d'); }
else gpu.attribute('a_seed',data.flat(),4);
let mouse=[-1000,-1000],target=[-1000,-1000],active=0,targetActive=0;
const still=staticMotion(element,options);
const noise=simplex(77);
const draw=(time,dt)=>{
if(!palette)return;
if(still())active=targetActive=0;
if(dt){mouse=mouse.map((n,i)=>mix(n,target[i],1-Math.exp(-dt*5.6)));active=mix(active,targetActive,1-Math.exp(-dt*3.2));}
({w,h,dpr}=fitCanvas(canvas));
if(gpu){const {gl}=gpu;gl.viewport(0,0,canvas.width,canvas.height);gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT);gl.useProgram(gpu.program);gl.enable(gl.BLEND);gl.blendFuncSeparate(gl.SRC_ALPHA,palette.dark?gl.ONE:gl.ONE_MINUS_SRC_ALPHA,gl.ONE,gl.ONE_MINUS_SRC_ALPHA);
gl.uniform2f(gpu.uniform('u_size'),w,h);gl.uniform2f(gpu.uniform('u_mouse'),...mouse);gl.uniform1f(gpu.uniform('u_active'),active);gl.uniform1f(gpu.uniform('u_time'),time+12);gl.uniform1f(gpu.uniform('u_dpr'),dpr);gl.uniform3fv(gpu.uniform('u_a'),palette.a);gl.uniform3fv(gpu.uniform('u_b'),palette.b);gl.drawArrays(gl.POINTS,0,count);
}else{ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,w,h);for(const [a,s,size,alpha]of data){const x=Math.cos(a),y=Math.sin(a),base=w*.59,f=noise(x*45+s*15+1,y*45+s*15+1),c=noise(x*8+s*4,y*8+s*4),n=clamp((noise(x*2.2+1.68,y*2.2-1.2)+.6)/1.2),width=mix(.008,.030,n*n*(3-2*n)),r=base+(s<.6?(f*.75+c*.25)*width*base:(f*150+c*45)*((s-.6)/.4)**2.4);const px=w*.5+x*r,py=h*.43+y*r;ctx.fillStyle=rgb(mixColor(palette.b,palette.a,clamp(py/h)),alpha);ctx.beginPath();ctx.arc(px,py,size*.5,0,TAU);ctx.fill();}}
};
const clock=loop(element,draw,{...options,continuous:!!gpu,settled:()=>Math.abs(active-targetActive)<.001});
const off=watchColors(element,p=>{palette=p;clock.invalidate();});
const ro=new ResizeObserver(()=>clock.invalidate());ro.observe(element.querySelector('.card-stage'));
const move=e=>{if(e.pointerType==='touch' || still())return;const r=canvas.getBoundingClientRect();target=[e.clientX-r.left,e.clientY-r.top];targetActive=1;};
const leave=()=>targetActive=0;element.addEventListener('pointermove',move);element.addEventListener('pointerleave',leave);element.addEventListener('pointercancel',leave);
return {destroy(){clock.destroy();off();ro.disconnect();gpu?.destroy();cleanShell();element.removeEventListener('pointermove',move);element.removeEventListener('pointerleave',leave);element.removeEventListener('pointercancel',leave);}};
}