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 =...