AquaMotion — v1.3.1

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 v1.3.1</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">
          <button class="small toggleBtn" id="loopToggle" title="Loop playback">🔁</button>
       ...

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 — v1.3.1 modifications
   - keyboard shortcuts
   - compact dot timeline
   - layer lock & rename
   - loop toggle & FPS live update
   - undo/redo stack
   ============================ */

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,
  loop: true,
  fps: 12,
  frames: [ { layers: [ { name: 'Layer 1', visible: true, locked: false, strokes: [] } ] } ],
  currentFrame: 0,
  currentLayer: 0,
  undoStack: [],
  redoStack: [],
  _undoSnapshot: null
};

/* ---------- helpers ---------- */
function screenToWorld(clientX, clientY){
  const rect = canvas.getBoundingClientRect();
  return {
    x: (clientX - rect.left - state.offsetX) / state.scale,
    y: (clientY - rect.top - state.offsetY) / state.scale
  };
}

function saveSnapshot(){
  // push deep snapshot to undo stack and clear redo
  try {
    const snap = JSON.stringify(state.frames);
    state.undoStack.push(snap);
    if(state.undoStack.length > 50) state.undoStack.shift();
    state.redoStack = [];
  } catch(e){}
}

function undo(){
  if(state.undoStack.length === 0) { alert('Nothing to undo'); return; }
  try {
    const snap = state.undoStack.pop();
    state.redoStack.push(JSON.stringify(state.frames));
    state.frames = JSON.parse(snap);
    state.currentFrame = Math.min(state.currentFrame, state.frames.length-1);
    state.currentLayer = Math.min(state.currentLayer, state.frames[state.currentFrame].layers.length-1);
    refreshAll();
  } catch(err){
    console.error('undo failed', err);
  }
}
function redo(){
  if(state.redoStack.length === 0) { alert('Nothing to redo'); return; }
  try {
    const snap = state.redoStack.pop();
    state.undoStack.push(JSON.stringify(state.frames));
    state.frames =...