css-particle-migration
<!-- Loaded by combat-effects-upgrade when task involves DOM particle replacement, element pooling, or CSS @keyframes for effects.ts -->
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
<!-- Loaded by combat-effects-upgrade when task involves DOM particle replacement, element pooling, or CSS @keyframes for effects.ts -->
Agent definition
css-particle-migration.mdCSS Particle Migration Reference
<!-- Loaded by combat-effects-upgrade when task involves DOM particle replacement, element pooling, or CSS @keyframes for effects.ts -->
Current failure mode in `effects.ts`:
// ANTI-PATTERN — runs on every effect call
const el = document.createElement('div');
Object.assign(el.style, { position: 'fixed', left: x + 'px', top: y + 'px' });
document.body.appendChild(el);
setTimeout(() => el.remove(), duration);Problems: GC pressure from allocation/deallocation + `appendChild`/`remove` forces style recalculation. Fix: pre-allocated pool with CSS class toggling.
---
ParticlePool: TypeScript Implementation
// src/lib/ParticlePool.ts
type PoolOptions = {
count: number;
className: string;
container?: HTMLElement;
};
export class ParticlePool {
private readonly pool: HTMLDivElement[] = [];
private readonly className: string;
private readonly container: HTMLElement;
constructor({ count, className, container }: PoolOptions) {
this.className = className;
this.container = container ?? document.body;
// Pre-allocate all elements at construction time
for (let i = 0; i < count; i++) {
const el = document.createElement('div');
el.style.position = 'fixed';
el.style.pointerEvents = 'none';
el.style.willChange = 'transform, opacity'; // hint GPU layer — set here, not inline per-effect
el.setAttribute('aria-hidden', 'true');
this.container.appendChild(el);
this.pool.push(el);
}
}
/**
* Acquire a particle from the pool.
* Returns null if all particles are in use — caller should degrade gracefully.
*/
acquire(x: number, y: number): HTMLDivElement | null {
const el = this.pool.find(p => !p.classList.contains('particle-active'));
if (!el) return null;
// Reset stale values from previous animation
el.style.transform = '';
el.style.opacity = '';
el.style.left = x + 'px';
el.style.top = y + 'px';
el.classList.add('particle-active', this.className);
// Auto-return on animation end — synchronous, no timer drift
const onEnd = () => {
el.classList.remove('particle-active', this.className);
el.removeEventListener('animationend', onEnd);
};
el.addEventListener('animationend', onEnd, { once: true });
return el;
}
/** Force-return all elements (e.g. on combat scene unmount) */
releaseAll(): void {
for (const el of this.pool) {
el.classList.remove('particle-active', this.className);
}
}
/** Clean up — call in useEffect cleanup when component unmounts */
destroy(): void {
this.releaseAll();
for (const el of this.pool) {
el.remove();
}
this.pool.length = 0;
}
}---
Pool Registry: Shared Pools Per Effect Type
// src/lib/effectPools.ts
import { ParticlePool } from './ParticlePool';
// Pools are module-level singletons — created once, shared across all effect calls
// Pool sizes: effect max count + 25% buffer
let _pools: Record<string, ParticlePool> | null = null;
export function getEffectPools(): Record<string, ParticlePool> {
if (_pools) return _pools;
_pools = {
impact: new ParticlePool({ count: 8, className: 'particle-impact' }),
confetti: new ParticlePool({ count: 24, className: 'particle-confetti' }),
gold: new ParticlePool({ count: 16, className: 'particle-gold' }),
sparkle: new ParticlePool({ count: 16, className: 'particle-sparkle' }),
heal: new ParticlePool({ count: 8, className: 'particle-heal' }),
finisher: new ParticlePool({ count: 16, className: 'particle-finisher' }),
damage: new ParticlePool({ count: 4, className: 'particle-damage' }),
block: new ParticlePool({ count: 4, className: 'particle-damage' }), // reuses damage class
floating: new ParticlePool({ count: 4, className: 'particle-floating' }),
buff: new ParticlePool({ count: 4, className: 'particle-buff' }),
debuff: new ParticlePool({ count: 4, className: 'particle-debuff' }),
};
return _pools;
}
export function destroyEffectPools(): void {
if (!_pools) return;
for (const pool of Object.values(_pools)) {
pool.destroy();
}
_pools = null;
}---
Migrated Effect Functions
Before / After: `createImpactBurst`
// BEFORE — 5 createElement calls per hit
export function createImpactBurst(x: number, y: number, type: 'strike' | 'grapple' | 'aerial'): void {
const colors = { strike: '#ff4444', grapple: '#ff8800', aerial: '#4488ff' };
for (let i = 0; i < 5; i++) {
setTimeout(() => {
const el = document.createElement('div');
const angle = (i / 5) * Math.PI * 2;
Object.assign(el.style, {
position: 'fixed', left: x + 'px', top: y + 'px',
width: '8px', height: '8px', borderRadius: '50%',
background: colors[type],
transform: `translate(${Math.cos(angle) * 40}px, ${Math.sin(angle) * 40}px)`,
});
document.body.appendChild(el);
setTimeout(() => el.remove(), 600);
}, i * 30);
}
}
// AFTER — pool acquire, CSS class drives animation
export function createImpactBurst(x: number, y: number, type: 'strike' | 'grapple' | 'aerial'): void {
const pools = getEffectPools();
const angleStep = (Math.PI * 2) / 5;
for (let i = 0; i < 5; i++) {
const angle = i * angleStep;
const el = pools.impact.acquire(x, y);
if (!el) continue; // pool exhausted — degrade gracefully
// CSS custom properties drive per-particle direction
el.style.setProperty('--angle', angle.toString());
el.style.setProperty('--delay', `${i * 30}ms`);
el.dataset['type'] = type; // for CSS attr() or data selectors
}
}---
CSS @keyframes: Complete Definitions
Add to your global stylesheet or a `particles.css` module. All animations use only GPU-composited properties: `transform` and `opacity`.
/* Timing presets — match original effect durations */
:r
Read more
CSS Particle Migration Reference
<!-- Loaded by combat-effects-upgrade when task involves DOM particle replacement, element pooling, or CSS @keyframes for effects.ts -->
Current failure mode in `effects.ts`:
// ANTI-PATTERN — runs on every effect call
const el = document.createElement('div');
Object.assign(el.style, { position: 'fixed', left: x + 'px', top: y + 'px' });
document.body.appendChild(el);
setTimeout(() => el.remove(), duration);Problems: GC pressure from allocation/deallocation + `appendChild`/`remove` forces style recalculation. Fix: pre-allocated pool with CSS class toggling.
---
ParticlePool: TypeScript Implementation
// src/lib/ParticlePool.ts
type PoolOptions = {
count: number;
className: string;
container?: HTMLElement;
};
export class ParticlePool {
private readonly pool: HTMLDivElement[] = [];
private readonly className: string;
private readonly container: HTMLElement;
constructor({ count, className, container }: PoolOptions) {
this.className = className;
this.container = container ?? document.body;
// Pre-allocate all elements at construction time
for (let i = 0; i < count; i++) {
const el = document.createElement('div');
el.style.position = 'fixed';
el.style.pointerEvents = 'none';
el.style.willChange = 'transform, opacity'; // hint GPU layer — set here, not inline per-effect
el.setAttribute('aria-hidden', 'true');
this.container.appendChild(el);
this.pool.push(el);
}
}
/**
* Acquire a particle from the pool.
* Returns null if all particles are in use — caller should degrade gracefully.
*/
acquire(x: number, y: number): HTMLDivElement | null {
const el = this.pool.find(p => !p.classList.contains('particle-active'));
if (!el) return null;
// Reset stale values from previous animation
el.style.transform = '';
el.style.opacity = '';
el.style.left = x + 'px';
el.style.top = y + 'px';
el.classList.add('particle-active', this.className);
// Auto-return on animation end — synchronous, no timer drift
const onEnd = () => {
el.classList.remove('particle-active', this.className);
el.removeEventListener('animationend', onEnd);
};
el.addEventListener('animationend', onEnd, { once: true });
return el;
}
/** Force-return all elements (e.g. on combat scene unmount) */
releaseAll(): void {
for (const el of this.pool) {
el.classList.remove('particle-active', this.className);
}
}
/** Clean up — call in useEffect cleanup when component unmounts */
destroy(): void {
this.releaseAll();
for (const el of this.pool) {
el.remove();
}
this.pool.length = 0;
}
}---
Pool Registry: Shared Pools Per Effect Type
// src/lib/effectPools.ts
import { ParticlePool } from './ParticlePool';
// Pools are module-level singletons — created once, shared across all effect calls
// Pool sizes: effect max count + 25% buffer
let _pools: Record<string, ParticlePool> | null = null;
export function getEffectPools(): Record<string, ParticlePool> {
if (_pools) return _pools;
_pools = {
impact: new ParticlePool({ count: 8, className: 'particle-impact' }),
confetti: new ParticlePool({ count: 24, className: 'particle-confetti' }),
gold: new ParticlePool({ count: 16, className: 'particle-gold' }),
sparkle: new ParticlePool({ count: 16, className: 'particle-sparkle' }),
heal: new ParticlePool({ count: 8, className: 'particle-heal' }),
finisher: new ParticlePool({ count: 16, className: 'particle-finisher' }),
damage: new ParticlePool({ count: 4, className: 'particle-damage' }),
block: new ParticlePool({ count: 4, className: 'particle-damage' }), // reuses damage class
floating: new ParticlePool({ count: 4, className: 'particle-floating' }),
buff: new ParticlePool({ count: 4, className: 'particle-buff' }),
debuff: new ParticlePool({ count: 4, className: 'particle-debuff' }),
};
return _pools;
}
export function destroyEffectPools(): void {
if (!_pools) return;
for (const pool of Object.values(_pools)) {
pool.destroy();
}
_pools = null;
}---
Migrated Effect Functions
Before / After: `createImpactBurst`
// BEFORE — 5 createElement calls per hit
export function createImpactBurst(x: number, y: number, type: 'strike' | 'grapple' | 'aerial'): void {
const colors = { strike: '#ff4444', grapple: '#ff8800', aerial: '#4488ff' };
for (let i = 0; i < 5; i++) {
setTimeout(() => {
const el = document.createElement('div');
const angle = (i / 5) * Math.PI * 2;
Object.assign(el.style, {
position: 'fixed', left: x + 'px', top: y + 'px',
width: '8px', height: '8px', borderRadius: '50%',
background: colors[type],
transform: `translate(${Math.cos(angle) * 40}px, ${Math.sin(angle) * 40}px)`,
});
document.body.appendChild(el);
setTimeout(() => el.remove(), 600);
}, i * 30);
}
}
// AFTER — pool acquire, CSS class drives animation
export function createImpactBurst(x: number, y: number, type: 'strike' | 'grapple' | 'aerial'): void {
const pools = getEffectPools();
const angleStep = (Math.PI * 2) / 5;
for (let i = 0; i < 5; i++) {
const angle = i * angleStep;
const el = pools.impact.acquire(x, y);
if (!el) continue; // pool exhausted — degrade gracefully
// CSS custom properties drive per-particle direction
el.style.setProperty('--angle', angle.toString());
el.style.setProperty('--delay', `${i * 30}ms`);
el.dataset['type'] = type; // for CSS attr() or data selectors
}
}---
CSS @keyframes: Complete Definitions
Add to your global stylesheet or a `particles.css` module. All animations use only GPU-composited properties: `transform` and `opacity`.
/* Timing presets — match original effect durations */ :r
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

