Whiteboard
GPT 5.6 Luna Pro · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 18,233 bytes · SHA-256 29bbafd7e08c
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Canvasly Whiteboard</title>
<style>
:root{--chrome:#f4f5f7;--ink:#27313b;--muted:#6f7b87;--line:#d9dde3;--blue:#5b6ee1;--paper:#fffdfa}
*{box-sizing:border-box}html,body{margin:0;height:100%;overflow:hidden;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;color:var(--ink);background:var(--chrome)}
#app{height:100%;position:relative;background:var(--chrome)}#board{position:absolute;inset:0;width:100%;height:100%;cursor:default;touch-action:none}
.floating{position:absolute;z-index:5;background:rgba(255,255,255,.94);border:1px solid var(--line);box-shadow:0 7px 24px #26344918;border-radius:12px;display:flex;align-items:center}
#tools{top:18px;left:50%;transform:translateX(-50%);padding:6px;gap:3px}button{font:inherit;border:0;background:transparent;color:var(--ink);cursor:pointer}.tool{height:34px;min-width:36px;border-radius:8px;font-size:15px;font-weight:650}.tool span{display:block;font-size:10px;color:var(--muted);font-weight:500;margin-top:-2px}.tool.active{background:#e7ebff;color:#4254c8}.tool:hover,.icon:hover{background:#f0f2f5}.divider{width:1px;height:25px;background:var(--line);margin:0 4px}
#props{top:78px;left:50%;transform:translateX(-50%);padding:7px 10px;gap:10px;font-size:12px;color:var(--muted)}label{display:flex;align-items:center;gap:6px;white-space:nowrap}input[type=color]{width:25px;height:25px;padding:2px;border:1px solid var(--line);border-radius:6px;background:#fff}input[type=range]{width:72px;accent-color:var(--blue)}#widthLabel{color:var(--ink);font-weight:700;min-width:22px}
#brand{top:18px;left:18px;gap:9px;background:transparent;border:0;box-shadow:none;font-weight:800;letter-spacing:-.5px;font-size:18px}#brand i{width:28px;height:28px;border-radius:8px;background:var(--blue);display:grid;place-items:center;color:#fff;font-style:normal;font-size:17px}
#actions{top:18px;right:18px;padding:5px;gap:2px}.icon{width:34px;height:34px;border-radius:8px;font-size:18px}.icon:disabled{opacity:.35;cursor:not-allowed}.hint{position:absolute;bottom:18px;left:18px;color:#8b949e;font-size:11px;background:#ffffffaa;padding:7px 10px;border-radius:7px;pointer-events:none}.zoom{position:absolute;right:18px;bottom:18px;padding:5px 7px;gap:2px}.zoom button{font-size:16px;width:25px;height:25px;border-radius:5px}.zoom strong{font-size:12px;min-width:48px;text-align:center}.reset{font-size:11px!important;width:auto!important;padding:0 6px;color:var(--muted)}
#textEditor{position:absolute;z-index:10;display:none;resize:none;overflow:hidden;background:transparent;border:1px dashed var(--blue);outline:0;font:20px/1.25 cursive;color:var(--ink);padding:2px}
</style></head><body><main id="app"><canvas id="board"></canvas><div id="brand"><i>✦</i> Canvasly</div>
<div id="tools" class="floating"><button class="tool active" data-tool="select">↖<span>V</span></button><button class="tool" data-tool="pen">✎<span>P</span></button><button class="tool" data-tool="rect">▱<span>R</span></button><button class="tool" data-tool="ellipse">◯<span>O</span></button><button class="tool" data-tool="line">╱<span>L</span></button><button class="tool" data-tool="text">T<span>T</span></button><button class="tool" data-tool="eraser">⌫<span>E</span></button></div>
<div id="props" class="floating"><label>Stroke <input id="stroke" type="color" value="#344054"></label><label>Fill <input id="fill" type="color" value="#fff3d6"></label><label>Width <input id="width" type="range" min="1" max="12" value="2"><b id="widthLabel">2px</b></label></div>
<div id="actions" class="floating"><button class="icon" id="undo" title="Undo (Ctrl+Z)">↶</button><button class="icon" id="redo" title="Redo (Ctrl+Shift+Z)">↷</button><div class="divider"></div><button class="icon" id="clear" title="Clear board">⌫</button></div>
<div class="zoom floating"><button id="zoomOut">−</button><strong id="zoomText">100%</strong><button id="zoomIn">+</button><button class="reset" id="reset">Reset view</button></div><div class="hint">Space + drag to pan · Scroll to zoom · Shift for proportional resize</div><textarea id="textEditor" spellcheck="false"></textarea></main>
<script>
/* ===== Tunable constants ===== */
const CFG={minZoom:.25,maxZoom:4,grid:24,hit:8,handle:8,colors:{stroke:'#344054',fill:'#fff3d6'}};
/* ===== State and object model ===== */
const canvas=document.querySelector('#board'),ctx=canvas.getContext('2d'),scene=[];let selected=new Set(),tool='select',view={x:-window.innerWidth/2+0,y:-window.innerHeight/2+0,zoom:1},action=null,history=[],future=[],dpr=1,spaceDown=false;
const $=s=>document.querySelector(s), uid=()=>Math.random().toString(36).slice(2)+Date.now().toString(36);
function save(){localStorage.setItem('canvasly-scene',JSON.stringify(scene));localStorage.setItem('canvasly-view',JSON.stringify(view))}function load(){try{let a=JSON.parse(localStorage.getItem('canvasly-scene')||'[]');scene.push(...a);let v=JSON.parse(localStorage.getItem('canvasly-view'));if(v)view=v}catch(e){}}
function snapshot(){return JSON.stringify(scene)}function commit(before){if(before!==snapshot()){history.push(before);if(history.length>80)history.shift();future=[];save();updateButtons()}}
function restore(s){scene.splice(0,scene.length,...JSON.parse(s));selected.clear();save();draw();updateButtons()}
function undo(){if(!history.length)return;future.push(snapshot());restore(history.pop())}function redo(){if(!future.length)return;history.push(snapshot());restore(future.pop())}
function resize(){dpr=devicePixelRatio||1;canvas.width=innerWidth*dpr;canvas.height=innerHeight*dpr;canvas.style.width=innerWidth+'px';canvas.style.height=innerHeight+'px';draw()}addEventListener('resize',resize);
function pt(e){return{x:e.clientX,y:e.clientY}}function world(e){let p=pt(e);return{x:p.x/view.zoom+view.x,y:p.y/view.zoom+view.y}}
function bounds(o){if(o.type==='pen'){let xs=o.points.map(p=>p.x),ys=o.points.map(p=>p.y);return{x:Math.min(...xs),y:Math.min(...ys),w:Math.max(...xs)-Math.min(...xs),h:Math.max(...ys)-Math.min(...ys)}}if(o.type==='line')return{x:Math.min(o.x1,o.x2),y:Math.min(o.y1,o.y2),w:Math.abs(o.x2-o.x1),h:Math.abs(o.y2-o.y1)};return{x:Math.min(o.x,o.x+o.w),y:Math.min(o.y,o.y+o.h),w:Math.abs(o.w),h:Math.abs(o.h)}}
function draw(){ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,innerWidth,innerHeight);ctx.fillStyle='#fffdfa';ctx.fillRect(0,0,innerWidth,innerHeight);ctx.save();ctx.translate(-view.x*view.zoom,-view.y*view.zoom);ctx.scale(view.zoom,view.zoom);drawGrid();scene.forEach(render);if(action&&action.preview)render(action.preview,true);ctx.restore();drawSelection();$('#zoomText').textContent=Math.round(view.zoom*100)+'%'}
function drawGrid(){let step=CFG.grid,x0=Math.floor(view.x/step)*step- step*2,x1=view.x+innerWidth/view.zoom+step*2,y0=Math.floor(view.y/step)*step-step*2,y1=view.y+innerHeight/view.zoom+step*2;ctx.fillStyle='#e6e8ec';for(let x=x0;x<x1;x+=step)for(let y=y0;y<y1;y+=step){ctx.beginPath();ctx.arc(x,y,.8/view.zoom,0,7);ctx.fill()}}
function style(o){ctx.lineWidth=o.sw||2;ctx.strokeStyle=o.stroke||CFG.colors.stroke;ctx.fillStyle=o.fill||'transparent';ctx.lineCap='round';ctx.lineJoin='round'}
function render(o,preview=false){style(o);ctx.globalAlpha=preview?.7:1;ctx.beginPath();if(o.type==='rect'){ctx.rect(o.x,o.y,o.w,o.h);if(o.fill&&o.fill!=='transparent')ctx.fill();ctx.stroke()}else if(o.type==='ellipse'){ctx.ellipse(o.x+o.w/2,o.y+o.h/2,Math.abs(o.w/2),Math.abs(o.h/2),0,0,Math.PI*2);if(o.fill&&o.fill!=='transparent')ctx.fill();ctx.stroke()}else if(o.type==='line'){ctx.moveTo(o.x1,o.y1);ctx.lineTo(o.x2,o.y2);ctx.stroke();if(o.arrow){let a=Math.atan2(o.y2-o.y1,o.x2-o.x1);ctx.beginPath();ctx.moveTo(o.x2,o.y2);ctx.lineTo(o.x2-10*Math.cos(a-.5),o.y2-10*Math.sin(a-.5));ctx.moveTo(o.x2,o.y2);ctx.lineTo(o.x2-10*Math.cos(a+.5),o.y2-10*Math.sin(a+.5));ctx.stroke()}}else if(o.type==='pen'){ctx.moveTo(o.points[0].x,o.points[0].y);for(let i=1;i<o.points.length-1;i++){let p=o.points[i],n=o.points[i+1];ctx.quadraticCurveTo(p.x,p.y,(p.x+n.x)/2,(p.y+n.y)/2)}let p=o.points.at(-1);ctx.lineTo(p.x,p.y);ctx.stroke()}else if(o.type==='text'){ctx.font=o.font||'20px cursive';ctx.fillStyle=o.stroke||CFG.colors.stroke;ctx.fillText(o.text,o.x,o.y)}ctx.globalAlpha=1}
function screenRect(b){return{x:(b.x-view.x)*view.zoom,y:(b.y-view.y)*view.zoom,w:b.w*view.zoom,h:b.h*view.zoom}}
function drawSelection(){if(!selected.size)return;let bs=[...selected].map(id=>bounds(scene.find(o=>o.id===id))).reduce((a,b)=>({x:Math.min(a.x,b.x),y:Math.min(a.y,b.y),w:Math.max(a.x+a.w,b.x+b.w)-Math.min(a.x,b.x),h:Math.max(a.y+a.h,b.y+b.h)-Math.min(a.y,b.y)}));let r=screenRect(bs);ctx.setTransform(dpr,0,0,dpr,0,0);ctx.strokeStyle='#5b6ee1';ctx.lineWidth=1.5;ctx.setLineDash([5,4]);ctx.strokeRect(r.x,r.y,r.w,r.h);ctx.setLineDash([]);ctx.fillStyle='#fff';for(let [x,y] of [[r.x,r.y],[r.x+r.w,r.y],[r.x,r.y+r.h],[r.x+r.w,r.y+r.h]]){ctx.fillRect(x-4,y-4,8,8);ctx.strokeRect(x-4,y-4,8,8)}}
/* ===== Hit testing ===== */
function distSeg(p,a,b){let dx=b.x-a.x,dy=b.y-a.y,t=Math.max(0,Math.min(1,((p.x-a.x)*dx+(p.y-a.y)*dy)/(dx*dx+dy*dy||1)));return Math.hypot(p.x-(a.x+t*dx),p.y-(a.y+t*dy))}
function hit(o,p){let b=bounds(o),pad=CFG.hit/view.zoom;if(o.type==='rect')return p.x>=b.x-pad&&p.x<=b.x+b.w+pad&&p.y>=b.y-pad&&p.y<=b.y+b.h+pad&&(o.fill!=='transparent'||p.x<=b.x+pad||p.x>=b.x+b.w-pad||p.y<=b.y+pad||p.y>=b.y+b.h-pad);if(o.type==='ellipse'){let dx=(p.x-(b.x+b.w/2))/(Math.abs(b.w/2)||1),dy=(p.y-(b.y+b.h/2))/(Math.abs(b.h/2)||1);return dx*dx+dy*dy<=1+pad/Math.max(1,Math.abs(b.w/2))}if(o.type==='line')return distSeg(p,{x:o.x1,y:o.y1},{x:o.x2,y:o.y2})<=pad+4;if(o.type==='pen')return o.points.some((q,i)=>i&&distSeg(p,o.points[i-1],q)<=pad+3);if(o.type==='text')return p.x>=b.x&&p.x<=b.x+Math.max(80,o.text.length*10)&&p.y>=b.y-22&&p.y<=b.y+8;return false}
function topHit(p){for(let i=scene.length-1;i>=0;i--)if(hit(scene[i],p))return scene[i];return null}
function selectionBounds(){return [...selected].map(id=>bounds(scene.find(o=>o.id===id))).reduce((a,b)=>({x:Math.min(a.x,b.x),y:Math.min(a.y,b.y),w:Math.max(a.x+a.w,b.x+b.w)-Math.min(a.x,b.x),h:Math.max(a.y+a.h,b.y+b.h)-Math.min(a.y,b.y)}),{x:Infinity,y:Infinity,w:0,h:0})}
function handleHit(e){if(!selected.size)return null;let r=screenRect(selectionBounds()),x=e.clientX,y=e.clientY;for(let [cx,cy,name] of [[r.x,r.y,'nw'],[r.x+r.w,r.y,'ne'],[r.x,r.y+r.h,'sw'],[r.x+r.w,r.y+r.h,'se']])if(Math.hypot(x-cx,y-cy)<14)return name;return null}
function transform(o,ob,base,dx,dy,corner,keep){let nb={...base};let rx=corner.includes('e')?base.w+dx:base.w-dx,ry=corner.includes('s')?base.h+dy:base.h-dy;if(keep){let s=Math.max(Math.abs(rx/(base.w||1)),Math.abs(ry/(base.h||1)));rx=Math.sign(rx||1)*Math.abs(base.w||1)*s;ry=Math.sign(ry||1)*Math.abs(base.h||1)*s}let sx=rx/(base.w||1),sy=ry/(base.h||1);nb.x=corner.includes('w')?base.x+base.w-rx:base.x;nb.y=corner.includes('n')?base.y+base.h-ry:base.y;let map=p=>({x:nb.x+(p.x-base.x)*sx,y:nb.y+(p.y-base.y)*sy});if(o.type==='line'){let a=map({x:ob.x1,y:ob.y1}),b=map({x:ob.x2,y:ob.y2});Object.assign(o,{x1:a.x,y1:a.y,x2:b.x,y2:b.y})}else if(o.type==='pen')o.points=ob.points.map(map);else if(o.type==='text'){let p=map({x:ob.x,y:ob.y});o.x=p.x;o.y=p.y}else{Object.assign(o,{x:nb.x,y:nb.y,w:rx,h:ry})}}
/* ===== Interaction ===== */
function setTool(t){tool=t;document.querySelectorAll('.tool').forEach(b=>b.classList.toggle('active',b.dataset.tool===t));canvas.style.cursor=t==='select'?'default':t==='text'?'text':'crosshair'}
function make(type,a,b){let o={id:uid(),type,stroke:$('#stroke').value,fill:$('#fill').value,sw:+$('#width').value};if(type==='line')Object.assign(o,{x1:a.x,y1:a.y,x2:b.x,y2:b.y,arrow:true});else if(type==='pen')o.points=[];else Object.assign(o,{x:a.x,y:a.y,w:b.x-a.x,h:b.y-a.y});return o}
function start(e){if(e.button===1||spaceDown){action={pan:true,sx:e.clientX,sy:e.clientY,vx:view.x,vy:view.y};return}let p=world(e);let corner=tool==='select'?handleHit(e):null;if(corner){let before=snapshot(),base=selectionBounds();action={resize:true,before,corner,base,start:p,obs:new Map([...selected].map(id=>[id,structuredClone(scene.find(o=>o.id===id))]))};return}if(tool==='text'){beginText(e,p);return}if(tool==='eraser'){let o=topHit(p);if(o){let before=snapshot();scene.splice(scene.indexOf(o),1);selected.delete(o.id);commit(before);draw()}action={erase:true};return}if(tool==='select'){let o=topHit(p);if(o){let before=snapshot();if(e.shiftKey){selected.has(o.id)?selected.delete(o.id):selected.add(o.id)}else if(!selected.has(o.id))selected=new Set([o.id]);action={move:true,before,start:p,last:p,ids:[...selected]};draw()}else{if(!e.shiftKey)selected.clear();action={rubber:true,start:p,current:p,before:snapshot()};draw()}return}let o=make(tool,p,p);if(tool==='pen')o.points=[p];scene.push(o);action={draw:true,obj:o,start:p};draw()}
function move(e){if(!action)return;let p=world(e);if(action.pan){view.x=action.vx+(action.sx-e.clientX)/view.zoom;view.y=action.vy+(action.sy-e.clientY)/view.zoom;draw();return}if(action.resize){let dx=p.x-action.start.x,dy=p.y-action.start.y;action.obs.forEach((ob,id)=>transform(scene.find(o=>o.id===id),ob,action.base,dx,dy,action.corner,e.shiftKey));draw();return}if(action.draw){if(action.obj.type==='pen')action.obj.points.push(p);else{action.obj.w=p.x-action.start.x;action.obj.h=p.y-action.start.y; if(action.obj.type==='line'){action.obj.x2=p.x;action.obj.y2=p.y}}draw();return}if(action.move){let dx=p.x-action.last.x,dy=p.y-action.last.y;action.ids.forEach(id=>{let o=scene.find(q=>q.id===id);if(o.type==='line'){o.x1+=dx;o.x2+=dx;o.y1+=dy;o.y2+=dy}else if(o.type==='pen')o.points.forEach(q=>{q.x+=dx;q.y+=dy});else{o.x+=dx;o.y+=dy}});action.last=p;draw();return}if(action.rubber){action.current=p;draw()}}
function end(e){if(!action)return;if(action.pan){action=null;save();return}if(action.resize){commit(action.before);action=null;draw();return}if(action.draw){let o=action.obj;if(o.type==='pen'&&o.points.length<2)scene.splice(scene.indexOf(o),1);else if(o.type!=='pen'&&Math.abs(o.w)<2&&Math.abs(o.h)<2)scene.splice(scene.indexOf(o),1);commit(action.before||'');action=null;draw();return}if(action.move){commit(action.before);action=null;draw();return}if(action.rubber){let a=action.start,b=action.current||a,x=Math.min(a.x,b.x),y=Math.min(a.y,b.y),w=Math.abs(b.x-a.x),h=Math.abs(b.y-a.y);scene.forEach(o=>{let q=bounds(o);if(q.x>=x&&q.y>=y&&q.x+q.w<=x+w&&q.y+q.h<=y+h)selected.add(o.id)});action=null;draw();}}
canvas.addEventListener('pointerdown',e=>{canvas.setPointerCapture(e.pointerId);start(e)});canvas.addEventListener('pointermove',move);canvas.addEventListener('pointerup',e=>{end(e);if(spaceDown)canvas.style.cursor='grab'});canvas.addEventListener('pointercancel',end);addEventListener('keyup',e=>{if(e.code==='Space'){spaceDown=false;if(!action)setTool(tool)}});
canvas.addEventListener('wheel',e=>{e.preventDefault();let before=world(e),factor=Math.exp(-e.deltaY*.001);view.zoom=Math.max(CFG.minZoom,Math.min(CFG.maxZoom,view.zoom*factor));view.x=before.x-(e.clientX/view.zoom);view.y=before.y-(e.clientY/view.zoom);save();draw()},{passive:false});
/* ===== Text, keyboard, controls ===== */
let editor=$('#textEditor');function beginText(e,p){editor.style.display='block';editor.style.left=e.clientX+'px';editor.style.top=(e.clientY-24)+'px';editor.value='';editor.dataset.x=p.x;editor.dataset.y=p.y;editor.focus();action={text:true,before:snapshot()}}
function finishText(){if(!action?.text)return;let v=editor.value.trim();if(v)scene.push({id:uid(),type:'text',x:+editor.dataset.x,y:+editor.dataset.y,text:v,stroke:$('#stroke').value,fill:'transparent',sw:2});editor.style.display='none';commit(action.before);action=null;draw()}
editor.addEventListener('blur',finishText);document.querySelectorAll('.tool').forEach(b=>b.onclick=()=>setTool(b.dataset.tool));$('#undo').onclick=undo;$('#redo').onclick=redo;$('#clear').onclick=()=>{if(scene.length){let b=snapshot();scene.splice(0);selected.clear();commit(b);draw()}};$('#width').oninput=e=>$('#widthLabel').textContent=e.target.value+'px';['stroke','fill'].forEach(id=>$( '#'+id).oninput=()=>{if(selected.size){let b=snapshot();selected.forEach(id=>{let o=scene.find(q=>q.id===id);o[id==='stroke'?'stroke':'fill']=$('#'+id).value});commit(b);draw()}});
function zoomAt(f){let cx=innerWidth/2,cy=innerHeight/2,b={x:cx/view.zoom+view.x,y:cy/view.zoom+view.y};view.zoom=Math.max(CFG.minZoom,Math.min(CFG.maxZoom,view.zoom*f));view.x=b.x-cx/view.zoom;view.y=b.y-cy/view.zoom;save();draw()}$('#zoomIn').onclick=()=>zoomAt(1.2);$('#zoomOut').onclick=()=>zoomAt(1/1.2);$('#reset').onclick=()=>{view={x:0,y:0,zoom:1};save();draw()};function updateButtons(){$('#undo').disabled=!history.length;$('#redo').disabled=!future.length}
addEventListener('keydown',e=>{if(e.code==='Space'){spaceDown=true;if(!action)canvas.style.cursor='grab';e.preventDefault()}if(e.key==='Escape'){finishText();return}if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='z'){e.preventDefault();e.shiftKey?redo():undo();return}if(editor===document.activeElement)return;if((e.key==='Delete'||e.key==='Backspace')&&selected.size){let b=snapshot();scene.splice(0,scene.length,...scene.filter(o=>!selected.has(o.id)));selected.clear();commit(b);draw();return}if(['v','p','r','o','l','t','e'].includes(e.key.toLowerCase()))setTool({v:'select',p:'pen',r:'rect',o:'ellipse',l:'line',t:'text',e:'eraser'}[e.key.toLowerCase()]);if((e.key.startsWith('Arrow'))&&selected.size){let b=snapshot(),d=e.shiftKey?10:1,dx=e.key==='ArrowLeft'?-d:e.key==='ArrowRight'?d:0,dy=e.key==='ArrowUp'?-d:e.key==='ArrowDown'?d:0;selected.forEach(id=>{let o=scene.find(q=>q.id===id);if(o.type==='line'){o.x1+=dx;o.x2+=dx;o.y1+=dy;o.y2+=dy}else if(o.type==='pen')o.points.forEach(p=>{p.x+=dx;p.y+=dy});else{o.x+=dx;o.y+=dy}});commit(b);draw()}});
load();resize();updateButtons();
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-5.6-luna-pro","provider":"openrouter","persona":"full","sessionId":"799da0a1-1e91-493e-9ec7-982e3c67afa0","tokensIn":700830,"tokensOut":27485,"tokensTotal":728315,"turns":15,"toolCalls":14,"failedToolCalls":0,"timestamp":"2026-07-31T00:26:30.261Z"} -->