Skip to content

Instantly share code, notes, and snippets.

@matthewharwood
Last active December 8, 2025 05:38
Show Gist options
  • Select an option

  • Save matthewharwood/515bc72412ae1d5878372ee4c3ee4c4a to your computer and use it in GitHub Desktop.

Select an option

Save matthewharwood/515bc72412ae1d5878372ee4c3ee4c4a to your computer and use it in GitHub Desktop.
A collection of Skills for Claude Code
name description
animejs-v4
Anime.js 4.0 animations for Web Components — drag-drop, click feedback, swaps, cancelable motion.

Anime.js 4.0

Installation & Imports

npm i animejs
import { animate, createTimeline, stagger, createSpring, createDraggable } from 'animejs';
import { createDrawable, createMotionPath, morphTo, splitText, onScroll } from 'animejs';

Basic Animation

animate('.el', {
  x: 250, y: 100, rotate: 90, scale: 1.5, opacity: 0.5,
  backgroundColor: '#FFF',
  duration: 1000, delay: 200, ease: 'outQuad',
  loop: true, alternate: true
});

Per-Property Control

animate('.el', {
  x: { from: -100, to: 100, duration: 800 },
  rotate: { from: 0, to: 360, ease: 'inOutCirc' },
  scale: { to: 1.5, delay: 200 }
});

Keyframes

animate('.el', { y: [0, -50, 0], scale: [1, 1.2, 1], duration: 1000 });

Stagger

animate('.items', {
  y: -20, opacity: [0, 1],
  delay: stagger(100),                     // Sequential
  delay: stagger(100, { from: 'center' }), // From center
  delay: stagger(100, { reversed: true }), // Reversed
  delay: stagger(50, { grid: [5, 5] })     // Grid layout
});

Timeline

const tl = createTimeline({ loop: true, alternate: true });
tl.add('.box1', { x: 100 })
  .add('.box2', { y: 50 }, '-=200')    // 200ms before previous ends
  .add('.box3', { scale: 2 }, '+=100') // 100ms after previous
  .add('.box4', { rotate: 90 }, 500);  // At 500ms absolute

tl.label('myLabel', 1000);
tl.call(() => console.log('done'), 2000);

Playback Control & Cancellation

const anim = animate('.el', { x: 100, autoplay: false });

anim.play();
anim.pause();
anim.reverse();
anim.restart();
anim.seek(500);      // Seek to 500ms
anim.seek('50%');    // Seek to 50%
Method Behavior
cancel() Stop immediately, keep current inline styles
revert() Stop and remove all inline styles
complete() Jump to final values immediately

Callbacks & Promises

animate('.el', {
  x: 100,
  onBegin: (anim) => {},
  onUpdate: (anim) => {},
  onComplete: (anim) => {}
});

await animate('.el', { x: 100 });  // Promise-based

Easings

Built-in: linear, in, out, inOut, outIn (with power: out(3)). Named: inQuad, outQuad, inOutQuad, inCubic, outExpo, inOutElastic, outBounce.

import { createSpring } from 'animejs';
animate('.el', { x: 100, ease: createSpring({ stiffness: 400, damping: 25 }) });

Function-Based Values

animate('.items', {
  x: (el, i, total) => i * 50,
  rotate: (el, i) => i % 2 === 0 ? 45 : -45
});

Animation Management Pattern

Always cancel existing animations before starting new ones:

class AnimationManager {
  #anims = new WeakMap();

  animate(el, props, key = 'main') {
    const map = this.#anims.get(el) || {};
    map[key]?.cancel();  // Cancel existing
    
    const anim = animate(el, props);
    map[key] = anim;
    this.#anims.set(el, map);
    return anim;
  }

  cancel(el, key = 'main') { this.#anims.get(el)?.[key]?.cancel(); }
  revert(el, key = 'main') { this.#anims.get(el)?.[key]?.revert(); }
}

Drag-and-Drop Animations

Drag Start (Lift)

function animateDragStart(card) {
  return animate(card, {
    scale: 1.12, rotate: 8,
    boxShadow: '0 20px 50px rgba(0,0,0,0.3)',
    duration: 150, ease: 'out(3)'
  });
}

During Drag — Use Direct Transform (60fps)

// Don't use animate() for pointer tracking
function updateDragPosition(el, x, y) {
  el.style.transform = `translate(${x}px, ${y}px) scale(1.12) rotate(8deg)`;
}

Drop Animation

function animateDrop(card) {
  return animate(card, {
    x: 0, y: 0,
    scale: [1.12, 0.95, 1],
    rotate: [8, -2, 0],
    boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
    duration: 350, ease: 'outBack'
  });
}

Return to Origin

function animateReturn(card) {
  return animate(card, {
    x: 0, y: 0, scale: 1, rotate: 0,
    duration: 400, ease: 'outBack'
  });
}

Card Swap

async function animateSwap(cardA, cardB, slotA, slotB) {
  const rectA = cardA.getBoundingClientRect();
  const rectB = cardB.getBoundingClientRect();

  // DOM swap first
  slotA.appendChild(cardB);
  slotB.appendChild(cardA);

  const newRectA = cardA.getBoundingClientRect();
  const newRectB = cardB.getBoundingClientRect();

  // Animate from old positions
  await Promise.all([
    animate(cardA, {
      x: [rectA.left - newRectA.left, 0],
      y: [rectA.top - newRectA.top, 0],
      scale: [1.1, 0.95, 1],
      duration: 400, ease: 'outBack'
    }),
    animate(cardB, {
      x: [rectB.left - newRectB.left, 0],
      y: [rectB.top - newRectB.top, 0],
      scale: [1, 1.05, 1],
      duration: 400, ease: 'outBack'
    })
  ]);
}

Slot Hover Feedback

function animateSlotHover(slot, isOver) {
  animate(slot, {
    scale: isOver ? 1.03 : 1,
    borderColor: isOver ? '#4CAF50' : '#999',
    duration: 150, ease: 'out(2)'
  });
}

Click Animations

// Basic bounce
animate(card, {
  scale: [1, 1.15, 1],
  rotate: [0, 5, -5, 0],
  duration: 400, ease: 'outElastic(1, 0.5)'
});

// Disabled shake
animate(card, { x: [0, -8, 8, -6, 6, 0], duration: 300, ease: 'linear' });

Spring Presets

const springs = {
  click:  createSpring({ stiffness: 600, damping: 30 }),
  move:   createSpring({ stiffness: 300, damping: 25 }),
  drop:   createSpring({ stiffness: 400, damping: 20 }),
  settle: createSpring({ stiffness: 200, damping: 25 }),
  bounce: createSpring({ stiffness: 500, damping: 10 })
};

Scroll-Driven Animation

animate('.el', {
  x: 300,
  autoplay: onScroll({ target: '.el', enter: 'bottom', leave: 'top', sync: true })
});

SVG

animate(createDrawable('path'), { draw: '0 1' });           // Draw path
animate('.el', { ...createMotionPath('.path') });           // Motion path
animate('.shape1', { d: morphTo('.shape2') });              // Morph shapes

Text Splitting

const { chars } = splitText('.text', { chars: true });
animate(chars, { y: [20, 0], opacity: [0, 1], delay: stagger(30) });

Draggable (Built-in)

createDraggable('.el', {
  x: true, y: true, snap: 50, container: '.bounds',
  releaseEase: createSpring({ stiffness: 120, damping: 6 }),
  onDrag: (d) => {}, onRelease: (d) => {}
});

Web Component Pattern

class CardComponent extends HTMLElement {
  #anim = null;

  animate(props) {
    this.#anim?.cancel();
    this.#anim = animate(this, props);
    return this.#anim;
  }

  disconnectedCallback() {
    this.#anim?.revert();
  }
}

V3 → V4 Migration

V3 V4
anime({ targets: '.el' }) animate('.el', {...})
easing: 'easeOutQuad' ease: 'outQuad'
direction: 'alternate' alternate: true
translateX: 100 x: 100
anime.stagger(100) stagger(100)
.finished .then() or await

html-boilerplate.md

IDB

---
name: idb-state-persistence
description: Use IndexedDB via the `idb` library for persistent browser state that survives refreshes. Use when building rapid prototypes, games, or SPAs that need client-side state persistence beyond localStorage limitations.
---

# IDB State Persistence for Rapid Prototyping

## When to Use This Skill

Use IDB when you need:
- **State that survives browser refresh** (the primary use case)
- **Structured data storage** (objects, arrays, not just strings)
- **Async non-blocking storage** (localStorage is synchronous)
- **Larger storage limits** (~50MB+ vs localStorage's ~5MB)
- **Indexed queries** on stored data

## Quick Decision: localStorage vs IDB

localStorage: Simple key-value, <100 items, strings only, sync OK IDB: Structured data, many records, async required, need queries


**Rule of thumb**: If you're doing `JSON.parse(localStorage.getItem(...))` more than twice, switch to IDB.

---

## Installation

```bash
npm install idb

Or CDN for rapid prototyping:

<script type="module">
  import { openDB } from 'https://cdn.jsdelivr.net/npm/idb@8/+esm';
</script>

Pattern 1: Simple State Store (Most Common)

The keyval pattern - drop-in replacement for localStorage with async API:

// services/StateStore.js
import { openDB } from 'idb';

const DB_NAME = 'app-state';
const STORE_NAME = 'state';
const DB_VERSION = 1;

let dbPromise = null;

function getDB() {
  if (!dbPromise) {
    dbPromise = openDB(DB_NAME, DB_VERSION, {
      upgrade(db) {
        if (!db.objectStoreNames.contains(STORE_NAME)) {
          db.createObjectStore(STORE_NAME);
        }
      },
    });
  }
  return dbPromise;
}

export const StateStore = {
  async get(key) {
    const db = await getDB();
    return db.get(STORE_NAME, key);
  },

  async set(key, value) {
    const db = await getDB();
    return db.put(STORE_NAME, value, key);
  },

  async delete(key) {
    const db = await getDB();
    return db.delete(STORE_NAME, key);
  },

  async clear() {
    const db = await getDB();
    return db.clear(STORE_NAME);
  },

  async keys() {
    const db = await getDB();
    return db.getAllKeys(STORE_NAME);
  },

  async getAll() {
    const db = await getDB();
    const keys = await db.getAllKeys(STORE_NAME);
    const values = await db.getAll(STORE_NAME);
    return Object.fromEntries(keys.map((k, i) => [k, values[i]]));
  },

  async getOrDefault(key, defaultValue) {
    const value = await this.get(key);
    return value !== undefined ? value : defaultValue;
  },

  async update(key, updater) {
    const current = await this.get(key);
    const updated = updater(current);
    await this.set(key, updated);
    return updated;
  }
};

Usage

import { StateStore } from './services/StateStore.js';

// Set values (no JSON.stringify needed!)
await StateStore.set('playerName', 'ACE');
await StateStore.set('gameState', { 
  streak: 5, 
  round: 12, 
  mode: 'greater'
});

// Get values
const name = await StateStore.get('playerName');
const state = await StateStore.get('gameState');

// Get with default
const volume = await StateStore.getOrDefault('volume', 0.8);

// Update atomically
await StateStore.update('gameState', (state) => ({
  ...state,
  streak: state.streak + 1
}));

Pattern 2: Typed State Manager (Recommended for Games/Apps)

// services/GameStateManager.js
import { openDB } from 'idb';

const DB_NAME = 'game-db';
const DB_VERSION = 1;

const DEFAULT_STATE = {
  playerName: '',
  streak: 0,
  round: 1,
  mode: 'greater',
  settings: {
    ttsEnabled: false,
    musicEnabled: false,
    language: 'en',
    volume: 0.8
  }
};

class GameStateManager {
  constructor() {
    this._db = null;
    this._cache = null;
    this._listeners = new Map();
    this._ready = this._init();
  }

  async _init() {
    this._db = await openDB(DB_NAME, DB_VERSION, {
      upgrade(db) {
        db.createObjectStore('state');
      },
    });
    await this._loadCache();
    return this;
  }

  async _loadCache() {
    const keys = await this._db.getAllKeys('state');
    const values = await this._db.getAll('state');
    this._cache = { ...DEFAULT_STATE };
    keys.forEach((key, i) => {
      this._cache[key] = values[i];
    });
  }

  async ready() {
    await this._ready;
    return this;
  }

  // Sync getter (uses cache)
  get(key) {
    if (!this._cache) {
      return DEFAULT_STATE[key];
    }
    return this._cache[key] ?? DEFAULT_STATE[key];
  }

  // Async setter (persists to IDB)
  async set(key, value) {
    await this._ready;
    const oldValue = this._cache[key];
    this._cache[key] = value;
    await this._db.put('state', value, key);
    this._notifyListeners(key, value, oldValue);
    return value;
  }

  async setMany(updates) {
    await this._ready;
    const tx = this._db.transaction('state', 'readwrite');
    const changes = [];
    
    for (const [key, value] of Object.entries(updates)) {
      const oldValue = this._cache[key];
      this._cache[key] = value;
      tx.store.put(value, key);
      changes.push({ key, value, oldValue });
    }
    
    await tx.done;
    changes.forEach(({ key, value, oldValue }) => {
      this._notifyListeners(key, value, oldValue);
    });
  }

  onChange(key, callback) {
    if (!this._listeners.has(key)) {
      this._listeners.set(key, new Set());
    }
    this._listeners.get(key).add(callback);
    return () => this._listeners.get(key)?.delete(callback);
  }

  _notifyListeners(key, newValue, oldValue) {
    this._listeners.get(key)?.forEach(cb => cb(newValue, oldValue));
    this._listeners.get('*')?.forEach(cb => cb(key, newValue, oldValue));
  }

  async reset() {
    await this._ready;
    await this._db.clear('state');
    this._cache = { ...DEFAULT_STATE };
  }
}

export const gameState = new GameStateManager();

Usage in Components

import { gameState } from './services/GameStateManager.js';

class GameContainer extends HTMLElement {
  async connectedCallback() {
    await gameState.ready();
    
    // Sync reads after ready()
    this._streak = gameState.get('streak');
    this._mode = gameState.get('mode');
    
    // Subscribe to changes
    this._unsubStreak = gameState.onChange('streak', (newVal) => {
      this._streak = newVal;
      this.updateUI();
    });
    
    this.render();
  }

  disconnectedCallback() {
    this._unsubStreak?.();
  }

  async handleWin() {
    await gameState.set('streak', this._streak + 1);
  }
}

Pattern 3: Migration from localStorage

// services/StateMigration.js
import { StateStore } from './StateStore.js';

const MIGRATION_KEY = '__idb_migrated__';

export async function migrateFromLocalStorage(keyMap = null) {
  const migrated = await StateStore.get(MIGRATION_KEY);
  if (migrated) return false;

  const keys = keyMap ? Object.keys(keyMap) : Object.keys(localStorage);
  
  for (const key of keys) {
    const lsValue = localStorage.getItem(key);
    if (lsValue === null) continue;
    
    let value;
    try {
      value = JSON.parse(lsValue);
    } catch {
      value = lsValue;
    }
    
    const idbKey = keyMap?.[key] || key;
    await StateStore.set(idbKey, value);
  }

  await StateStore.set(MIGRATION_KEY, Date.now());
  return true;
}

Critical Gotchas

1. Transaction Lifetime

// ❌ WRONG - transaction closes during fetch
const tx = db.transaction('store', 'readwrite');
const val = await tx.store.get('key');
const newVal = await fetch('/api'); // Transaction closes!
await tx.store.put(newVal, 'key'); // ERROR!

// ✅ CORRECT - keep IDB operations together
const val = await db.get('store', 'key');
const newVal = await fetch('/api');
await db.put('store', newVal, 'key');

2. Version Upgrades

// ✅ CORRECT - bump version for schema changes
openDB('db', 2, {
  upgrade(db, oldVersion) {
    if (oldVersion < 1) {
      db.createObjectStore('users');
    }
    if (oldVersion < 2) {
      db.createObjectStore('posts');
    }
  }
});

3. Incognito Mode Fallback

async function getStorage() {
  try {
    const db = await openDB('test', 1, {
      upgrade(db) { db.createObjectStore('test'); }
    });
    await db.put('test', 'test', 'test');
    await db.delete('test', 'test');
    return 'idb';
  } catch {
    console.warn('IDB unavailable, falling back to memory');
    return 'memory';
  }
}

Debugging Tips

// View all IDB databases
const databases = await indexedDB.databases();
console.log('Available databases:', databases);

// Clear everything (useful in dev)
await indexedDB.deleteDatabase('my-app-db');

// Dev tools: Application > IndexedDB

Summary

Pattern Use Case
Simple StateStore Drop-in localStorage replacement
Typed StateManager Games/apps with structured state
Migration helper Moving from localStorage to IDB

Key principles:

  1. Initialize once, cache for sync access
  2. Use tx.done for transaction completion
  3. Bump version for schema changes
  4. Handle errors and fallbacks
  5. Clean up listeners on disconnect

Just Library and/or Browser Utitlies

When writing JavaScript, scan the "Package" column for your need. If it's a native API (backticks), use it directly—zero dependencies. If it's a just-* package, install it for a tested, zero-dep micro-utility. The "When to Use" column describes the exact trigger condition. Import using ESM syntax.

Collections {}[]

Package When to Use
just-diff Get a JSON-patch style diff between two objects or arrays
just-diff-apply Apply a diff/patch object to mutate a target object
just-compare Deep equality check for objects, arrays, primitives, NaN-safe
structuredClone(obj) Deep copy objects, arrays, maps, sets, dates, regexes
arr.map(x => x.prop) Extract one property from every item in array
arr.filter(x => x != null) Remove null/undefined from arrays

Objects {}

Package When to Use
just-extend Deep merge objects into target; use spread for shallow
{...a, ...b} or Object.assign() Shallow merge objects
Object.values(obj) Get object values as array
Object.entries(obj) Get object [key, value] pairs
just-pick Copy object keeping only specified keys
just-omit Copy object excluding specified keys
Object.fromEntries(Object.entries(obj).filter(...)) Filter object properties by predicate
Object.fromEntries(Object.entries(obj).map(([k,v]) => [k, fn(v)])) Transform object values
Object.fromEntries(Object.entries(obj).map(([k,v]) => [fn(k), v])) Transform object keys
just-deep-map-values Recursively map values at all depths of nested object
Object.entries(obj).reduce((acc, [k,v]) => ..., init) Reduce object to single value
just-is-empty Check if object/array/map/set/string has no enumerable content
just-is-circular Detect circular references in object
typeof x !== 'object' || x === null Check if value is primitive
obj?.a?.b?.c Get nested property safely without throwing
just-safe-set Set nested property, creating intermediate objects as needed
just-typeof Better typeof: distinguishes array, object, null, regexp, date
Object.fromEntries(Object.entries(obj).map(([k,v]) => [v,k])) Swap keys and values
obj?.a?.b !== undefined Check if nested property path exists

Arrays []

Package When to Use
just-cartesian-product Generate all combinations from arrays of options
[...new Set(arr)] Dedupe array of primitives
arr.flat(depth) Flatten nested arrays to specified depth
Object.fromEntries(arr.map(x => [x[key], x])) Convert array to object keyed by property
arr.toSpliced(i, 0, ...items) Insert elements at index immutably
arr1.filter(x => arr2.includes(x)) Get elements present in both arrays
arr.filter(Boolean) Remove falsy values
arr.at(-1) Get last element of array
arr.slice(1) Get all elements except first
just-random Pick random element from array
just-shuffle Randomize array order, optional guarantee all items move
just-split Chunk array into groups of n
[arr.slice(0, i), arr.slice(i)] Split array into two at given index
just-order-by Sort by multiple properties with asc/desc per property
arr.toSorted((a,b) => a.prop - b.prop) Immutable sort by single property
just-partition Split array into [matches, nonMatches] by predicate
just-permutations Generate all orderings of array elements
just-range Generate number sequence with start, end, step
arr.filter(x => !remove.includes(x)) Remove elements of second array from first
[...new Set([...a, ...b])] Combine arrays and dedupe
just-zip-it Group nth elements from multiple arrays together
Object.groupBy(arr, fn) Group array items into object by classifier function

Statistics Σ

Package When to Use
arr.reduce((a,b) => a+b, 0) / arr.length Calculate average of numbers
just-median Find middle value of sorted numbers
just-mode Find most frequent value(s)
just-percentile Value at given percentile using linear interpolation
just-variance Measure of spread from the mean
just-standard-deviation Square root of variance
just-skewness Measure asymmetry of distribution

Strings ""

Package When to Use
just-template Interpolate {{a.b.c}} placeholders from nested object paths
just-truncate Cut string to length with suffix like ...
just-prune Truncate at word boundary with suffix
str.replaceAll(' ', '') Remove all whitespace from string
str.padStart(n, char) Pad string start to reach target length
str.padEnd(n, char) Pad string end to reach target length
just-camel-case Convert to camelCase from any format
just-kebab-case Convert to kebab-case from any format
just-snake-case Convert to snake_case from any format
just-pascal-case Convert to PascalCase from any format
str[0].toUpperCase() + str.slice(1) Uppercase first character only
str.replaceAll(find, replace) Replace all occurrences of substring

Numbers +-

Package When to Use
Math.min(Math.max(n, min), max) Constrain number to min/max range
just-is-prime Check if number is prime
just-modulo True modulo for negatives (JS % is remainder, not modulo)
Math.floor(Math.random() * (max - min + 1)) + min Random integer within inclusive range

Functions =>

Package When to Use
just-compose Right-to-left function composition: f(g(h(x)))
just-curry-it Convert function to curried form with partial application
Function.prototype.call.bind(method) Convert method to standalone function
just-flip Swap first two arguments of a function
just-partial-it Fix arguments with _ placeholder for unfixed positions
just-pipe Left-to-right function composition: h(g(f(x)))
just-debounce-it Delay execution until pause in calls, with cancel/flush
just-memoize Cache function results by arguments
just-memoize-last Cache only the single most recent call
just-throttle Limit execution to once per interval
just-once Function executes only on first call

Rapid Prototyping Libraries

In this AI world, there are so many oppertunties to build amazing prototypes; however, we want to make sure that the prototypes that we're making are still conforming to best practices. Below is a list of libraries that we should use building rapid prototypes.

  • AnimeJS.md
  • javascript-utils.md
  • idb.md
  • html5boilerplate.md
  • utopia.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment