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();
 ...