rsrc/cs2/pattern
2026-09-09 03:29:09 +00:00

100 lines
9.2 KiB
Plaintext

<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>spray viewer</title><style>*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#000;color:#fff;font:12px system-ui,Arial,sans-serif}.pane{position:fixed;inset:0;overflow:hidden}.pane.a{right:50%;border-right:1px solid #333}.pane.b{left:50%}canvas{display:block;width:100%;height:100%}.hide{display:none!important}#ui{position:fixed;z-index:5;top:12px;left:12px;right:12px;display:flex;align-items:center;gap:13px;flex-wrap:wrap;opacity:1;transition:opacity .13s ease}.group{display:flex;align-items:center;gap:10px}.group:before{content:"x";font-size:10px;letter-spacing:.06em;color:rgba(253,253,253,0.5);}button,select,input{font:inherit}button{border:0;background:#000;color:#555;padding:5px 0;cursor:pointer}button:hover,button:focus-visible,button.active{color:#fff}button:disabled{opacity:.3;cursor:wait}select{appearance:none;background:#000;color:#fff;border:0;border-bottom:1px solid #333;padding:4px 4px 4px 0;cursor:pointer}select:hover,select:focus-visible{border-bottom-color:#fff}button:focus-visible,select:focus-visible,input:focus-visible{outline:1px solid #fff;outline-offset:3px}.range{display:flex;align-items:center;gap:5px;color:#555}.range input{width:72px;height:9px;margin:0;padding:0;background:#000;border:1px solid #444;border-radius:2px;accent-color:#fff;cursor:pointer}.weapon{position:fixed;z-index:6;bottom:9px;left:12px}.weapon.b{left:calc(50% + 12px)}#status,#ammo{position:fixed;z-index:6;bottom:10px;font-size:11px;pointer-events:none}.data-t{color:transparent;} #status{left:150px;color:#777;max-width:calc(100vw - 270px);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}#ammo{right:12px;color:#fff;opacity:.72}body.idle #ui{opacity:0;pointer-events:none}</style><div id=ui><div class=group data-t=PLAY><button id=loop class=active>loop</button><button id=restart>restart</button>
</div><br>
<div class=group data-t=VIEW>
<button id=split>split</button>
<button id=points class=active>points</button>
<button id=numbers>numbers</button>
<button id=invert class=active>invert y</button></div>
<br><div class=group data-t=COLOR>
<button id=color class=active>color</button>
<button id=random>random</button>
<button id=resetcolor>reset</button></div><br>
<div class=group data-t=GRID>
<button id=grid>grid</button>
<label id=op class="range hide">opacity<input id=alpha type=range min=0 max=100 value=10></label>
<label class=range>speed<input id=speed type=range min=25 max=200 value=100>
<button id=resetspeed>reset</button></label>
</div></div><div id=p1 class=pane><canvas id=c1></canvas></div><div id=p2 class="pane b hide"><canvas id=c2></canvas></div><select id=w1 class=weapon aria-label="Weapon one"></select><select id=w2 class="weapon b hide" aria-label="Weapon two"></select><div id=status>Loading AK-47…</div><div id=ammo>30/30</div><script>
const B="https://raw.githubusercontent.com/comercialtoddy/Artanis-RCS-cs2/refs/heads/main/patterns/",W={AK47:["AK-47","ak47",30],AUG:["AUG","aug",30],FAMAS:["FAMAS","famas",25],GALIL:["Galil AR","galil",35],M4A1:["M4A1-S","m4a1",20],M4A4:["M4A4","m4a4",30],SG553:["SG 553","sg553",30],BIZON:["PP-Bizon","bizon",64],CZ75:["CZ75-Auto","cz75",12],M249:["M249","m249",100],MAC10:["MAC-10","mac10",30],MP5SD:["MP5-SD","mp5sd",30],MP7:["MP7","mp7",30],MP9:["MP9","mp9",30],P90:["P90","p90",50],UMP45:["UMP-45","ump45",25]},$=s=>document.querySelector(s),U=$("#ui"),S=$("#status"),A=$("#ammo"),CACHE=new Map;
const C1=$("#c1"),C2=$("#c2"),P1=$("#p1"),P2=$("#p2"),Q1=$("#w1"),Q2=$("#w2"),L=$("#loop"),R=$("#restart"),SP=$("#speed"),RS=$("#resetspeed"),SL=$("#split"),PT=$("#points"),NU=$("#numbers"),IV=$("#invert"),CO=$("#color"),RA=$("#random"),RC=$("#resetcolor"),GR=$("#grid"),OP=$("#op"),AL=$("#alpha");
for(const[k,v]of Object.entries(W)){Q1.add(new Option(v[0],k));Q2.add(new Option(v[0],k))}Q2.value="M4A4";
let loop=1,pts=1,nums=0,inv=1,col=1,grid=0,split=0,speed=1,lc="#1685ff",pc="#f33",idle=0,lastFrame=0;
function wake(){document.body.classList.remove("idle");clearTimeout(idle);idle=setTimeout(()=>{if(!U.matches(":hover")&&!Q1.matches(":hover")&&!Q2.matches(":hover"))document.body.classList.add("idle")},1500)}
["pointermove","pointerdown","keydown","touchstart","wheel"].forEach(e=>addEventListener(e,wake,{passive:true}));
U.onmouseenter=Q1.onmouseenter=Q2.onmouseenter=wake;wake();
function active(el,on){el.classList.toggle("active",!!on)}
function parseCSV(t){return t.replace(/^\uFEFF/,"").trim().split(/\r?\n/).map(x=>x.trim()).filter(Boolean).map(x=>x.split(/[,\s;]+/).map(Number)).filter(x=>x.length>1&&Number.isFinite(x[0])&&Number.isFinite(x[1])).map(x=>[x[0],x[1],Number.isFinite(x[2])&&x[2]>=0?x[2]:100])}
function makePane(canvas,select){
let g=canvas.getContext("2d"),v={canvas,g,select,id:select.value,raw:[],pos:[],tm:[],dur:1,start:performance.now(),req:0,good:0,b:[-1,1,-1,1]};
v.size=()=>{let r=canvas.getBoundingClientRect(),d=devicePixelRatio;canvas.width=Math.max(1,Math.round(r.width*d));canvas.height=Math.max(1,Math.round(r.height*d));g.setTransform(d,0,0,d,0,0)};
v.prep=()=>{
let x=0,y=0,t=0,x0=0,x1=0,y0=0,y1=0;v.pos=[[0,0]];v.tm=[];
for(const p of v.raw){x+=p[0];y+=inv?-p[1]:p[1];v.pos.push([x,y]);v.tm.push(t+=p[2]);x0=Math.min(x0,x);x1=Math.max(x1,x);y0=Math.min(y0,y);y1=Math.max(y1,y)}
v.dur=t||1;let xr=Math.max(x1-x0,1),yr=Math.max(y1-y0,1),z=Math.max(xr,yr);if(xr<z*.2)xr=z*.3;if(yr<z*.2)yr=z*.3;let xc=(x0+x1)/2,yc=(y0+y1)/2;v.b=[xc-xr*.7,xc+xr*.7,yc-yr*.7,yc+yr*.7];v.start=performance.now();v.good=!!v.raw.length
};
v.load=async()=>{
const request=++v.req,key=v.id,file=W[key][1];S.textContent=`Loading ${W[key][0]}…`;
try{
let data=CACHE.get(file);
if(!data){let r=await fetch(B+file+".csv",{cache:"force-cache"});if(!r.ok)throw Error(`HTTP ${r.status}`);data=parseCSV(await r.text());if(!data.length)throw Error("empty pattern");CACHE.set(file,data)}
if(request!==v.req)return;
v.raw=data;v.prep();S.textContent=`${W[key][0]} · ${data.length} recoil deltas · ${Math.round(v.dur)} ms`;
}catch(e){
if(request!==v.req)return;
S.textContent=`Failed loading ${W[key][0]}: ${e.message}`;
if(!v.good){v.raw=[[0,0,100]];v.prep()}
}
};
select.onchange=()=>{wake();v.id=select.value;v.load()};v.size();v.load();return v
}
let V1=makePane(C1,Q1),V2=makePane(C2,Q2);
function resize(){V1.size();V2.size()}addEventListener("resize",resize,{passive:true});
function shotCount(v,e){let a=0,b=v.tm.length;while(a<b){let m=(a+b)>>1;v.tm[m]<=e?a=m+1:b=m}return a}
function current(v,e){
if(e<=0)return v.pos[0]||[0,0];
let i=shotCount(v,e);
if(i>=v.raw.length)return v.pos.at(-1)||[0,0];
let a=v.pos[i],b=v.pos[i+1],t=i?v.tm[i-1]:0,d=v.tm[i]-t,f=d>0?(e-t)/d:1;
return[a[0]+(b[0]-a[0])*f,a[1]+(b[1]-a[1])*f]
}
function niceStep(r){let q=r/14,b=10**Math.floor(Math.log10(Math.max(q,.001))),n=q/b;return(n<1.5?1:n<3?2:n<7?5:10)*b}
function draw(v,now){
let{g,canvas,b,pos}=v,r=canvas.getBoundingClientRect(),w=r.width,h=r.height;g.clearRect(0,0,w,h);if(!v.good||w<1||h<1)return 0;
let[x0,x1,y0,y1]=b,z=Math.min(w/(x1-x0),h/(y1-y0)),ox=(w-(x1-x0)*z)/2-x0*z,oy=(h-(y1-y0)*z)/2+y1*z,to=p=>[ox+p[0]*z,oy-p[1]*z],e=(now-v.start)*speed;
e=loop?(e%v.dur+v.dur)%v.dur:Math.min(e,v.dur);
let fired=shotCount(v,e),n=Math.min(pos.length,fired+1),q=to(current(v,e));
if(grid){
let d=niceStep(Math.max(x1-x0,y1-y0)),o=AL.value/100;g.strokeStyle=`rgba(255,255,255,${o})`;g.lineWidth=1;
for(let x=Math.floor(x0/d)*d;x<=x1+d;x+=d){let a=to([x,y0]),b=to([x,y1]);g.beginPath();g.moveTo(...a);g.lineTo(...b);g.stroke()}
for(let y=Math.floor(y0/d)*d;y<=y1+d;y+=d){let a=to([x0,y]),b=to([x1,y]);g.beginPath();g.moveTo(...a);g.lineTo(...b);g.stroke()}
}
g.strokeStyle=col?lc:"#fff";g.lineWidth=2;g.lineCap=g.lineJoin="round";g.beginPath();
for(let i=0;i<n;i++){let p=to(pos[i]);i?g.lineTo(...p):g.moveTo(...p)}g.lineTo(...q);g.stroke();
if(pts){g.fillStyle=col?pc:"#fff";for(let i=0;i<n;i++){let p=to(pos[i]);g.beginPath();g.arc(...p,5,0,Math.PI*2);g.fill();if(nums){g.fillStyle="#fff";g.fillText(i,p[0]+7,p[1]-7);g.fillStyle=col?pc:"#fff"}}}
g.fillStyle="#fff";g.beginPath();g.arc(...q,6,0,Math.PI*2);g.fill();return fired
}
function frame(now){
requestAnimationFrame(frame);
if(now-lastFrame<16)return;lastFrame=now;
let a=draw(V1,now),b=split?draw(V2,now):0,v=split?V2:V1,f=split?b:a,clip=W[v.id][2];
A.textContent=`${Math.max(0,clip-Math.min(clip,f))}/${clip}`
}
function hit(fn){return()=>{wake();fn()}}
L.onclick=hit(()=>{loop=!loop;active(L,loop);V1.start=V2.start=performance.now()});
R.onclick=hit(()=>V1.start=V2.start=performance.now());
PT.onclick=hit(()=>{pts=!pts;active(PT,pts)});
NU.onclick=hit(()=>{nums=!nums;active(NU,nums)});
IV.onclick=hit(()=>{inv=!inv;active(IV,inv);V1.prep();V2.prep()});
CO.onclick=hit(()=>{col=!col;active(CO,col)});
RA.onclick=hit(()=>{lc=`hsl(${Math.random()*360|0} 100% 65%)`;pc=`hsl(${Math.random()*360|0} 100% 65%)`});
RC.onclick=hit(()=>{lc="#1685ff";pc="#f33"});
GR.onclick=hit(()=>{grid=!grid;active(GR,grid);OP.classList.toggle("hide",!grid)});
SL.onclick=hit(()=>{split=!split;active(SL,split);P1.classList.toggle("a",split);P2.classList.toggle("hide",!split);Q2.classList.toggle("hide",!split);resize()});
SP.oninput=()=>{wake();speed=SP.value/100};
RS.onclick=hit(()=>{SP.value=100;speed=1});
requestAnimationFrame(frame);
</script></html>