AquaMotion — Upgraded

by Sharxxy

HTML

<div id="toolbar">
  <img id="logo" src="https://via.placeholder.com/160x30?text=AquaMotion" alt="AquaMotion logo">
  <button class="btn tool active" data-tool="pencil">✏️ Pencil (B)</button>
  <button class="btn tool" data-tool="eraser">🩹 Eraser (E)</button>
  <button class="btn tool" data-tool="line">➖ Line</button>
  <button class="btn tool" data-tool="rect">⬛ Rectangle</button>
  <button class="btn tool" data-tool="circle">⭕ Circle</button>
  <label class="mutedText">Color:</label><input type="color" id="colorPicker" value="#00bfff">
  <label class="mutedText">Size:</label><input type="range" id="brushSize" min="1" max="60" value="5">
  <button class="btn" id="undo">↩️ Undo</button>
  <button class="btn" id="redo">↪️ Redo</button>
  <button class="btn" id="playPause">▶️ Play (Space)</button>
  <label class="mutedText">FPS:</label><input type="number" id="fps" value="12" min="1" max="60" style="width:60px;">
  <label class="mutedText">W:</label><input type="number" id="canvasWidth" value="800" min="100" max="2000" style="width:80px;">
  <label class="mutedText">H:</label><input type="number" id="canvasHeight" value="600" min="100" max="2000" style="width:80px;">
  <button class="btn" id="resizeCanvas">Resize</button>
</div>

<div id="main">
  <div id="canvasColumn">
    <div id="canvasWrap">
      <div id="canvasContainer"></div>
    </div>
    <div id="controlsRow">
      <div class="small">Onion: <label><input type="checkbox" id="onionPrev" checked> Prev</label> <label><input type="checkbox" id="onionNext"> Next</label></div>
      <div class="small">Onion Opacity: <input type="range" id="onionOpacity" min="0.05" max="0.9" step="0.05" value="0.45"></div>
      <div style="flex:1"></div>
      <button class="btn" id="addLayer">+ Layer</button>
      <button class="btn" id="delLayer">− Layer</button>
    </div>
  </div>

  <aside id="sidebar">
    <h3>Frames</h3>
    <div id="framesList"></div>
    <div...

CSS

/* ---------- THEME / LAYOUT ---------- */
  :root{
    --accent: #4fc3f7;
    --bg-1: #0b1e3d;
    --bg-2: #07213f;
    --panel: #0c3a6b;
    --muted: rgba(255,255,255,0.08);
    --glass: rgba(255,255,255,0.06);
    --white: #ffffff;
  }
  html,body{height:100%; margin:0; font-family:Inter,system-ui,Segoe UI,Roboto,"Helvetica Neue",Arial; background:var(--bg-1); color:var(--white); -webkit-font-smoothing:antialiased;}
  #toolbar{display:flex;align-items:center;gap:8px;padding:8px;background:linear-gradient(90deg,#1e3c72,#2a5298);border-bottom:2px solid var(--accent);flex-wrap:wrap;}
  #logo{height:30px;}
  .btn{padding:7px 10px;background:var(--glass);border:none;color:var(--white);border-radius:6px;cursor:pointer;}
  .btn.active{background:var(--accent); color:#042033;}
  input[type=color], input[type=range], input[type=number], select, input[type=file]{background:var(--glass);color:var(--white);border:none;border-radius:4px;padding:5px;}
  #main{display:flex;height:calc(100vh - 64px);}
  #canvasColumn{flex:3;display:flex;flex-direction:column;gap:8px;padding:12px;background:var(--bg-2);border-right:2px solid var(--accent);box-sizing:border-box;}
  #canvasWrap{flex:1;display:flex;justify-content:center;align-items:center; background: #04122a;border:1px solid rgba(255,255,255,0.03);position:relative;overflow:hidden;}
  #canvasContainer{position:relative; width:fit-content;height:fit-content;}
  .layerCanvas{position:absolute;left:0;top:0; image-rendering:pixelated; border:1px solid var(--muted); box-shadow:0 6px 30px rgba(0,0,0,0.6);}
  #controlsRow{display:flex;gap:8px;align-items:center;}
  #sidebar{flex:1;background:linear-gradient(#0c3a6b,#0b1e3d);padding:12px;display:flex;flex-direction:column;gap:10px;overflow-y:auto;border-left:2px solid var(--accent);box-sizing:border-box;}
  h3{margin:6px 0;color:var(--accent);font-size:14px;}
 ...

JavaScript

/* ===========================
   AquaMotion — core app logic
   =========================== */

const canvasContainer = document.getElementById('canvasContainer');
const canvasWidthInput = document.getElementById('canvasWidth');
const canvasHeightInput = document.getElementById('canvasHeight');
const resizeCanvasBtn = document.getElementById('resizeCanvas');

let app = {
  width: parseInt(canvasWidthInput.value,10) || 800,
  height: parseInt(canvasHeightInput.value,10) || 600,
  currentFrame: 0,
  frames: [], // each frame: { layers: [ImageBitmap|dataURL], audios: [{url, start, duration}], layerCount, layerMeta: [{visible, name}], undoStacks: [[],...], redoStacks: [...] }
  currentLayerIndex: 0,
  playing:false,
  fps: 12,
  playIntervalId: null,
  tool: 'pencil',
  color: '#00bfff',
  size: 5,
  onionPrev: true,
  onionNext: false,
  onionOpacity: 0.45,
};

/* ---------- UTIL ---------- */
function createEmptyImageData(w,h){
  const off = document.createElement('canvas');
  off.width = w; off.height = h;
  const ctx = off.getContext('2d');
  ctx.clearRect(0,0,w,h);
  return off.toDataURL();
}
function dataURLToImage(dataURL){
  return new Promise((res,rej)=>{
    const img = new Image();
    img.onload = ()=>res(img);
    img.onerror = rej;
    img.src = dataURL;
  });
}
function makeCanvasLayer(w,h, zIndex=1){
  const c = document.createElement('canvas');
  c.className = 'layerCanvas';
  c.width = w;
  c.height = h;
  c.style.width = w + 'px';
  c.style.height = h + 'px';
  c.style.zIndex = zIndex;
  return c;
}

/* ---------- INITIAL FRAME SETUP ---------- */
function newEmptyFrame(layerCount=2){
  const layers = [];
  const layerMeta = [];
  const undoStacks = [];
  const redoStacks = [];
  for(let i=0;i<layerCount;i++){
    layers.push(createEmptyImageData(app.width, app.height));
    layerMeta.push({visible:true, name: 'Layer ' + (i+1)});
    undoStacks.push([]);
    redoStacks.push([]);
  }
...