AquaMotion — Upgraded
by Sharxxy
HTML
<div class="app">
<!-- left toolbar -->
<div id="toolbar" title="Tools">
<button class="toolBtn" data-tool="pointer" title="Pointer (V)">🖱️</button>
<button class="toolBtn active" data-tool="pencil" title="Pencil (B)">✏️</button>
<button class="toolBtn" data-tool="eraser" title="Eraser (E)">🩹</button>
<button class="toolBtn" data-tool="line" title="Line (L)">➖</button>
<button class="toolBtn" data-tool="rect" title="Rectangle (R)">⬛</button>
<button class="toolBtn" data-tool="circle" title="Circle (C)">⭕</button>
<input type="color" id="colorPicker" title="Color" value="#000000">
<input type="range" id="brushSize" min="1" max="80" value="4" title="Brush size">
<label style="font-size:11px; margin-top:6px;">Onion</label>
<input type="checkbox" id="onionToggle" title="Toggle onion skin" checked>
<label style="font-size:11px;">Prev / Next</label>
<div style="height:6px"></div>
<button class="toolBtn" id="undo" title="Undo (Ctrl+Z)">↩️</button>
<button class="toolBtn" id="redo" title="Redo (Ctrl+Y)">↪️</button>
</div>
<!-- central area -->
<div class="center">
<div id="topBar">
<div class="muted">AquaMotion — Editor</div>
<div style="flex:1"></div>
<div style="display:flex;gap:8px;align-items:center">
<label class="muted">Canvas W</label><input id="canvasW" class="inlineInput" type="number" value="800" min="100" max="3000">
<label class="muted">H</label><input id="canvasH" class="inlineInput" type="number" value="600" min="100" max="2000">
<button class="small" id="resizeCanvas">Resize</button>
<div id="playControls">
<button class="small" id="playPause">▶ Play</button>
<label class="muted">FPS</label><input id="fpsInput" class="inlineInput" type="number" min="1" max="60" value="12">
</div>
<div style="width:12px"></div>
<button class="small" id="save">💾...
CSS
:root{--accent:#4fc3f7;--bg:#0b1e3d;--panel:#07213f;--muted:rgba(255,255,255,0.08);}
html,body{height:100%;margin:0;font-family:Inter, system-ui, Arial; background:var(--bg); color:#fff; -webkit-font-smoothing:antialiased;}
/* Layout */
.app {display:flex; height:100vh; overflow:hidden;}
/* Left toolbar (vertical like Animate) */
#toolbar {
width:72px; background:linear-gradient(180deg,#19375a,#12304e); border-right:2px solid var(--accent);
display:flex; flex-direction:column; align-items:center; gap:8px; padding:10px 6px; box-sizing:border-box;
}
#toolbar .toolBtn {
width:52px; height:44px; background:rgba(255,255,255,0.06); border-radius:8px; border:none; color:#fff;
display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:18px;
}
#toolbar .toolBtn.active {background:var(--accent); color:#042033; font-weight:600;}
#toolbar input[type=color], #toolbar input[type=range] {width:52px; background:transparent; border-radius:6px; border:none;}
#toolbar label {font-size:11px; color:rgba(255,255,255,0.8); text-align:center;}
/* Main area contains canvas and panels */
.center {
flex:1; display:flex; flex-direction:column; min-width:0; /* allow overflow auto child */
}
#topBar {
height:44px; display:flex; align-items:center; gap:8px; padding:6px 10px; background:linear-gradient(90deg,#123048,#0c2236);
border-bottom:2px solid var(--accent);
}
#topBar .btn {padding:6px 10px; background:rgba(255,255,255,0.06); border:none; color:#fff; border-radius:6px; cursor:pointer;}
#topBar .muted {color:rgba(255,255,255,0.7); font-size:13px;}
#workspace {flex:1; display:flex; min-height:0;} /* allow timeline below */
#canvasArea {flex:1; display:flex; justify-content:center; align-items:center; background:var(--panel); overflow:hidden; position:relative;}
#canvasWrap {position:relative; touch-action:none;} /* where canvas sits */
canvas {background:#ffffff;...
JavaScript
/* ============================
AquaMotion — Single File App
Features: tools, layers, frames, onion skin, play, pan/zoom, save/load, import/export, PNG export
============================ */
const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d');
let state = {
scale: 1,
offsetX: 0,
offsetY: 0,
tool: 'pencil',
color: '#000000',
size: 4,
onion: true,
playing: false,
fps: 12,
frames: [ { layers: [ { name: 'Layer 1', visible: true, strokes: [] } ] } ],
currentFrame: 0,
currentLayer: 0,
undoStacks: [], // per-layer per-frame optional
redoStacks: [],
};
// helpers: transform screen -> world coords (account for pan/zoom)
function screenToWorld(clientX, clientY){
const rect = canvas.getBoundingClientRect();
const x = (clientX - rect.left - state.offsetX) / state.scale;
const y = (clientY - rect.top - state.offsetY) / state.scale;
return {x, y};
}
function worldToScreen(x,y){
const rect = canvas.getBoundingClientRect();
return { x: x * state.scale + state.offsetX + rect.left, y: y * state.scale + state.offsetY + rect.top };
}
/* ---------- render ---------- */
function clearCanvas(){
// clear with white background
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = '#ffffff';
ctx.fillRect(0,0,canvas.width,canvas.height);
}
function drawStroke(s){
if(s.type==='eraser'){
ctx.globalCompositeOperation = 'destination-out';
ctx.strokeStyle = 'rgba(0,0,0,1)';
} else {
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = s.color || '#000';
}
ctx.lineWidth = s.size || 1;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
if(s.type==='pencil' || s.type==='eraser'){
if(!s.points || s.points.length===0) return;
ctx.moveTo(s.points[0].x, s.points[0].y);
for(let p of s.points) ctx.lineTo(p.x, p.y);
ctx.stroke();
...