Animation App — v1.4

by Sharxxy

HTML

<div class="app">
  <div class="panel toolbar">
    <h3 style="margin:0 0 8px 0">Animator v1.4 — brush + export</h3>

    <div>
      <label>Tool</label>
      <div class="tools-row">
        <button id="pencilBtn" class="small">Brush</button>
        <button id="bucketBtn" class="small">Bucket</button>
        <button id="eraserBtn" class="small">Eraser</button>
        <button id="textBtn" class="small">Text</button>
        <button id="undoBtn" class="small">Undo</button>
        <button id="clearBtn" class="small">Clear</button>
      </div>
    </div>

    <div>
      <label>Brush Type</label>
      <select id="brushType">
        <option value="hard">Hard round</option>
        <option value="ink">Ink</option>
        <option value="air">Airbrush</option>
        <option value="charcoal">Textured charcoal</option>
        <option value="pattern">Textured pattern</option>
        <option value="shadow">EFX — Shadow</option>
        <option value="glow">EFX — Glow</option>
        <option value="blur">EFX — Blur</option>
      </select>
    </div>

    <div>
      <label>Size & Opacity</label>
      <input id="size" type="range" min="1" max="120" value="16">
      <input id="opacity" type="range" min="0.05" max="1" step="0.05" value="0.95">
    </div>

    <div>
      <label>Color</label>
      <input id="color" class="color-input" type="color" value="#0b0b0b">
      <label style="margin-top:6px">Pattern (for textured pattern brush)</label>
      <input id="patternURL" placeholder="image URL (optional)" style="width:100%">
    </div>

    <div>
      <label>Smoothing / Pressure sim</label>
      <div class="tools-row">
        <input id="pressure" type="checkbox"> <label style="display:inline-block; margin:0 8px 0 4px">Pressure sim</label>
        <input id="smoothing" type="checkbox" checked> <label style="display:inline-block; margin:0 8px 0 4px">Smoothing</label>
      </div>
    </div>

    <div>
     ...

CSS

:root { --bg:#0f1720; --panel:#0b1220; --muted:#9aa8b2; --accent:#7dd3fc; }
  html,body{height:100%;margin:0;font-family:Inter,system-ui,Segoe UI,Roboto,'Helvetica Neue';background:var(--bg);color:#e6eef6}
  .app {display:grid;grid-template-columns:320px 1fr; height:100vh; gap:12px;padding:12px;}
  .panel {background:var(--panel);padding:12px;border-radius:12px;box-shadow:0 6px 18px rgba(0,0,0,.6);}
  .toolbar {display:grid;gap:8px;}
  button,input,select{font-size:13px;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,.06);background:transparent;color:inherit}
  .canvas-wrap{position:relative;border-radius:12px;overflow:hidden;display:flex;align-items:center;justify-content:center;}
  canvas{background:#fff;width:100%;height:100%;display:block;touch-action:none;}
  .overlay-text{position:absolute;left:0;top:0;pointer-events:none}
  .tools-row{display:flex;gap:6px;flex-wrap:wrap;}
  .small{font-size:12px;padding:6px}
  label{display:block;color:var(--muted);font-size:12px;margin-bottom:6px}
  .color-input{height:40px;width:60px;padding:3px}
  .hide{display:none}
  .footer{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-top:8px;color:var(--muted);font-size:12px}

JavaScript

/* =========================
   Animator v1.4 - single file
   Features: brushes, efx, bucket, text, export (MediaRecorder & GIF), undo
   Notes: This is a baseline implementation. Adapt into your app architecture.
   ========================= */

const canvas = document.getElementById('c');
const panel = document.getElementById('canvasPanel');
const overlayText = document.getElementById('overlayText');
const ctx = canvas.getContext('2d', { willReadFrequently: true });

let state = {
  tool: 'brush', // brush | bucket | eraser | text
  brushType: 'hard',
  size: 16,
  opacity: 0.95,
  color: '#0b0b0b',
  smoothing: true,
  pressureSim: false,
  patternImg: null,
  isDrawing: false,
  lastPos: null,
  dpr: window.devicePixelRatio || 1,
  undoStack: [],
  maxUndo: 20,
};

function fitCanvas() {
  const r = panel.getBoundingClientRect();
  const dpr = state.dpr;
  canvas.width = Math.max(600, Math.floor(r.width * dpr));
  canvas.height = Math.max(400, Math.floor((window.innerHeight - 60) * dpr));
  canvas.style.width = r.width + 'px';
  canvas.style.height = (window.innerHeight - 60) + 'px';
  ctx.setTransform(dpr,0,0,dpr,0,0);
  ctx.lineJoin = ctx.lineCap = 'round';
  ctx.fillStyle = '#fff';
  // only fill if empty:
  if (!state.initialized) {
    ctx.fillRect(0,0,canvas.width,canvas.height);
    state.initialized = true;
  }
}
fitCanvas();
window.addEventListener('resize', fitCanvas);

/* ---------- UI Hooks ---------- */
document.getElementById('brushType').addEventListener('change', e => {
  state.brushType = e.target.value;
  updateStatus();
});
document.getElementById('size').addEventListener('input', e => state.size = +e.target.value);
document.getElementById('opacity').addEventListener('input', e => state.opacity = +e.target.value);
document.getElementById('color').addEventListener('input', e => state.color = e.target.value);
document.getElementById('patternURL').addEventListener('change', async (e) =>...