Skip to content
Deasy Corporation
Logos
Theos
Work
Deasy Corporation
2026 AD
Games
Multi-Select and Group Move Rigid Formation POC
Back to Gaming Concepts
<html> <head> <meta charset="utf-8"> <title>Multi-Select + Group Move</title> <style> html,body{margin:0;height:100%;background:#000;overflow:hidden;font-family:system-ui} canvas{display:block;width:100%;height:100%;touch-action:none} #banner{ position:absolute;top:12px;left:50%;transform:translateX(-50%); padding:10px 18px;border-radius:999px; background:rgba(0,0,0,.7); border:2px solid rgba(120,220,255,.9); color:#e9f8ff;font-weight:800;letter-spacing:.4px; pointer-events:none } </style> </head> <body> <canvas id="c" width="1200" height="1200"></canvas> <div id="banner">DRAG EMPTY SPACE TO BOX-SELECT • DRAG SELECTED TO MOVE</div> <script> (()=>{ const c=document.getElementById("c"),ctx=c.getContext("2d"); const VW=c.width,VH=c.height,W=4096,H=4096; let camX=W/2,camY=H/2,zoom=.7; let mx=VW/2,my=VH/2,mwx=camX,mwy=camY; const objs=[]; let id=1; const sel=new Set(); let box=false,bx0=0,by0=0,bx1=0,by1=0; let dragging=false,anchorX=0,anchorY=0,offsets=null; function sw(px,py){return{x:camX+(px-VW/2)/zoom,y:camY+(py-VH/2)/zoom}} function ws(wx,wy){return{x:(wx-camX)*zoom+VW/2,y:(wy-camY)*zoom+VH/2}} function clampCam(){ const hw=(VW/2)/zoom,hh=(VH/2)/zoom; camX=Math.max(hw,Math.min(W-hw,camX)); camY=Math.max(hh,Math.min(H-hh,camY)); } function add(wx,wy){ objs.push({id:id++,x:wx-70,y:wy-50,w:140,h:100}); } function hit(wx,wy){ for(let i=objs.length-1;i>=0;i--){ const o=objs[i]; if(wx>=o.x&&wx<=o.x+o.w&&wy>=o.y&&wy<=o.y+o.h) return o.id; } return null; } function startGroup(){ dragging=true; anchorX=mwx; anchorY=mwy; offsets=new Map(); sel.forEach(i=>{ const o=objs.find(o=>o.id===i); offsets.set(i,{dx:o.x-anchorX,dy:o.y-anchorY}); }); } function updateGroup(){ sel.forEach(i=>{ const o=objs.find(o=>o.id===i); const off=offsets.get(i); o.x=mwx+off.dx; o.y=mwy+off.dy; }); } function drawGrid(){ ctx.strokeStyle="rgba(255,255,255,.06)"; ctx.lineWidth=1; ctx.beginPath(); const step=128; for(let x=0;x<=W;x+=step){ const s=ws(x,0).x; ctx.moveTo(s,0); ctx.lineTo(s,VH); } for(let y=0;y<=H;y+=step){ const s=ws(0,y).y; ctx.moveTo(0,s); ctx.lineTo(VW,s); } ctx.stroke(); } function drawObjects(){ objs.forEach(o=>{ const p=ws(o.x,o.y),w=o.w*zoom,h=o.h*zoom; ctx.fillStyle="rgba(255,255,255,.18)"; ctx.strokeStyle=sel.has(o.id)?"rgba(120,220,255,1)":"rgba(255,255,255,.8)"; ctx.lineWidth=sel.has(o.id)?5:2; ctx.fillRect(p.x,p.y,w,h); ctx.strokeRect(p.x,p.y,w,h); ctx.fillStyle="#fff"; ctx.font="14px system-ui"; ctx.fillText(o.id,p.x+8,p.y+18); }); } function drawGroupCenter(){ if(sel.size<2) return; let cx=0,cy=0; sel.forEach(i=>{ const o=objs.find(o=>o.id===i); cx+=o.x+o.w/2; cy+=o.y+o.h/2; }); cx/=sel.size; cy/=sel.size; const p=ws(cx,cy); ctx.strokeStyle="rgba(0,255,255,1)"; ctx.lineWidth=3; ctx.beginPath(); ctx.moveTo(p.x-15,p.y); ctx.lineTo(p.x+15,p.y); ctx.moveTo(p.x,p.y-15); ctx.lineTo(p.x,p.y+15); ctx.stroke(); } function drawBox(){ if(!box) return; const x=Math.min(bx0,bx1),y=Math.min(by0,by1); const w=Math.abs(bx1-bx0),h=Math.abs(by1-by0); ctx.fillStyle="rgba(120,180,255,.25)"; ctx.strokeStyle="rgba(120,220,255,1)"; ctx.lineWidth=3; ctx.fillRect(x,y,w,h); ctx.strokeRect(x,y,w,h); } c.addEventListener("pointerdown",e=>{ const r=c.getBoundingClientRect(); mx=e.clientX-r.left; my=e.clientY-r.top; const w=sw(mx,my); mwx=w.x; mwy=w.y; if(e.button===0){ const h=hit(mwx,mwy); if(h&&sel.has(h)){ startGroup(); }else if(h){ sel.clear(); sel.add(h); startGroup(); }else{ sel.clear(); box=true; bx0=mx; by0=my; bx1=mx; by1=my; } } }); c.addEventListener("pointermove",e=>{ const r=c.getBoundingClientRect(); mx=e.clientX-r.left; my=e.clientY-r.top; const w=sw(mx,my); mwx=w.x; mwy=w.y; if(box){ bx1=mx; by1=my; sel.clear(); const a=sw(Math.min(bx0,bx1),Math.min(by0,by1)); const b=sw(Math.max(bx0,bx1),Math.max(by0,by1)); objs.forEach(o=>{ if(o.x>=a.x&&o.y>=a.y&&o.x+o.w<=b.x&&o.y+o.h<=b.y) sel.add(o.id); }); } if(dragging) updateGroup(); }); c.addEventListener("pointerup",()=>{ box=false; dragging=false; offsets=null; }); c.addEventListener("wheel",e=>{ e.preventDefault(); const before=sw(mx,my); zoom=Math.max(.2,Math.min(3,zoom*(e.deltaY>0?.9:1.1))); const after=sw(mx,my); camX+=before.x-after.x; camY+=before.y-after.y; clampCam(); },{passive:false}); window.addEventListener("keydown",e=>{ if(e.key==="n") add(mwx,mwy); if(e.key==="c"){objs.length=0; sel.clear();} }); add(W/2-200,H/2-120); add(W/2+200,H/2-120); add(W/2-200,H/2+120); add(W/2+200,H/2+120); function loop(){ ctx.clearRect(0,0,VW,VH); drawGrid(); drawObjects(); drawGroupCenter(); drawBox(); requestAnimationFrame(loop); } loop(); })(); </script> </body> </html>' ></iframe> </div>