@gogospace/burn-in

Canvas fire-and-smoke reveal effects for real DOM text, images, canvas, and Vue components.
903 1
Install
npm install @gogospace/burn-in
Latest Version:0.3.0
Vue:3
License:MIT
Last Updated:Sep 27, 2026
Links: GitHub  ·  npm
Maintainer: gogospace

Burn-In.js

npm version CI license types

Canvas fire-and-smoke reveal effects for real DOM text, images, canvas, and Vue components.

Live playground · npm · GitHub

Burn-In.js animated preview

Burn-In.js adds a dramatic fire-and-smoke reveal to real web content. Point it at a DOM element, image, canvas, or Vue slot, then choose a preset or tune the masks, particles, timing, and seed. The original content stays in the document after the reveal, so text can remain selectable and accessible.

Features

  • Burn real DOM text while keeping it selectable and accessible after reveal.
  • Use alpha, luminance, text, or bounds masks for different source types.
  • Pick from six presets: soft, wildfire, smolder, flash, ritual, and psycho.
  • Replay deterministic particle patterns with seed.
  • Scale geometry with the target using referenceSize, without increasing particle counts.
  • Use the framework-neutral TypeScript core or the optional Vue 3 component.
  • Cancel effects and wait for completion with a typed controller.

Installation

npm install @gogospace/burn-in
pnpm add @gogospace/burn-in
yarn add @gogospace/burn-in

Vue is an optional peer dependency. Install Vue only when you use the @gogospace/burn-in/vue adapter.

Quick Start

import { burn } from "@gogospace/burn-in";

const logoElement = document.querySelector(".logo");

if (logoElement instanceof HTMLElement) {
  const controller = burn(logoElement, {
    preset: "ritual",
    fire: { intensity: 1.2 },
    smoke: { intensity: 0.8 },
    seed: "launch"
  });

  await controller.done;
}

Vue

<script setup lang="ts">
import { ref } from "vue";
import { BurnIn } from "@gogospace/burn-in/vue";

const replayKey = ref(0);
</script>

<template>
  <button type="button" @click="replayKey++">Replay</button>

  <BurnIn
    active
    :replay-key="replayKey"
    :options="{ preset: 'smolder', mask: { source: 'text' }, smoke: { intensity: 1.4 } }"
  >
    <strong>Burn me in</strong>
  </BurnIn>

  <BurnIn
    active
    target-selector="img"
    :replay-key="replayKey"
    :options="{ mask: { source: 'alpha' } }"
  >
    <img src="https://raw.githubusercontent.com/gogoSpace/burn-in.js/HEAD/logo.png" alt="Logo" />
  </BurnIn>
</template>

Vue Props and Events

Prop Description
active Starts the effect when true and cancels it when false.
options A BurnOptions object passed to burn().
as Host element tag name. Defaults to span.
replayKey Change this value to replay the effect while active is true.
contentClass Class applied to the wrapped slot target.
targetSelector Selector for a concrete descendant to burn instead of the wrapper.
Event Description
start Emitted before a new effect starts.
done Emitted when the effect finishes.
cancel Emitted when a running effect is cancelled.

The component also exposes play() and cancel() for imperative control.

Configuration

Burn-In.js keeps the easy path short and the expert path deep. Start with a preset and override only what you need.

import { burn } from "@gogospace/burn-in";

burn(headingElement, {
  preset: "wildfire",
  seed: "same-effect-every-time",
  timing: {
    delayMs: 420,
    igniteMs: 420,
    burnMs: 1000,
    fadeMs: 900,
    smokeMs: 2600
  },
  fire: {
    intensity: 1.4,
    lift: 0.014,
    spread: 0.56,
    turbulence: 0.04,
    particleSize: [10, 30],
    colors: [
      { at: 0, color: "rgba(255, 255, 230, 0.42)" },
      { at: 0.24, color: "rgba(255, 180, 50, 0.28)" },
      { at: 0.62, color: "rgba(255, 60, 10, 0.14)" },
      { at: 1, color: "rgba(0, 0, 0, 0)" }
    ]
  },
  smoke: {
    intensity: 0.9,
    drift: 0.6,
    expansion: 2,
    particleLife: [72, 140]
  },
  reveal: {
    startsAt: "burn",
    durationMs: 980
  }
});

seed controls the random particle pattern. Use a stable string or number when you want the same element and options to replay with the same fire and smoke behavior. Omit it when every burn should feel slightly different.

Target-relative Scaling

Set referenceSize to the measurement, in CSS pixels, at which your particle settings are tuned. scaleBasis selects that measurement and defaults to the shorter side of the target:

burn(targetElement, {
  referenceSize: 100,
  seed: "responsive-reveal",
  fire: { particleSize: [10, 28] },
  smoke: { particleSize: [28, 64] }
});

The uniform scale is Math.min(targetWidth, targetHeight) / referenceSize, using the target's rendered bounding rectangle. A 300 × 100 target uses scale 1; the same target at 150 × 50 uses 0.5, and at 600 × 200 uses 2. Both axes use the same scale, so particles retain their shape. The shorter side is used instead of area, diagonal, or width: adding characters to a wide, single-line heading does not enlarge its flames while its height stays the same. Narrow or multiline targets also follow that rule by default. For wrapping text, use the font-size basis below so layout changes do not enlarge particles.

Select a different basis when layout dimensions do not represent content scale:

burn(headingElement, {
  referenceSize: 120,
  scaleBasis: "font-size"
});

Here the scale is the target's computed CSS font-size divided by 120. A 120 px heading keeps the same particle sizes and motion when it wraps into two lines. A change to 90 px scales them to 0.75. The mask still follows the actual lines.

scaleBasis Measurement in CSS pixels
"short-side" (default) Smaller rendered bounding-rectangle dimension.
"width" Rendered bounding-rectangle width.
"height" Rendered bounding-rectangle height.
"font-size" Computed font size of the target element.
(target: HTMLElement) => number Custom synchronous measurement of the target.

For example, a custom function can measure a representative child in a mixed content container. Return a positive finite CSS-pixel length in the same units as referenceSize; the result is a measurement, not the final scale multiplier. The font-size basis reads the target itself, not differently styled descendants, and does not include CSS transforms. Use a callback for those custom semantics.

This works with text, image, canvas, and generic element masks. Particle sizes, initial velocities, lift, spread, turbulence, drift, mask offsets, and canvas margins all scale together. Timing, lifetime, opacity, colors, expansion factors, and the input/output shape of liftCurve remain unchanged. spriteSize still sets the sprite texture resolution; its contribution to canvas margins scales.

Mask sampling and emission run in reference coordinates. mask.stepPx and mask.offset are therefore measured at the reference size, and particlesPerPixel applies to reference-mask coverage. Uniform enlargement keeps the same emitter grid, emission schedule, and particle budget instead of adding particles with the displayed area. Changing shape or aspect ratio can change coverage, but existing maxParticles limits still apply. Text uses normalized font metrics for rasterization. Browser rounding of DOM text ranges can still shift a mask by roughly a reference pixel at some font sizes, changing edge samples, emitter weights, and seeded birth positions. Motion and fading from each particle's birth keep the same scale and lifetime rules.

The effect checks target dimensions and the selected measurement each animation frame and on viewport changes. A callback may therefore run each frame; keep it fast and free of side effects. Changes rebuild the mask and scale live particle geometry while preserving ages and velocities in reference coordinates. Font size or callback changes are detected even inside a fixed-size container. The animation does not restart. Content changes without a dimension or measurement change require replaying the effect.

Keep referenceSize fixed across responsive layouts and replays. Computing it from the current target before every call would always select scale 1. To keep a particular existing layout as your baseline, measure its selected basis once and use that value as the reference. A smaller reference makes the effect larger relative to the target; it also lowers normalized mask coverage and emission.

Omit referenceSize for the existing pixel-based behavior. Non-number, non-finite, zero, or negative reference values also use scale 1. A zero, negative, or non-finite target dimension uses scale 1, with invalid sampling dimensions reduced to at least one pixel; a later valid resize is measured again. An unknown basis, a non-number/non-finite/non-positive measurement, or an error from a callback also uses legacy sizing for that layout check. Without a valid referenceSize, the basis is not evaluated. canvas.pixelRatio remains independent: it controls backing-store resolution, not particle geometry. Its existing minimum of 1 is unchanged.

Custom Lift Curves

Both fire and smoke accept liftCurve?: (particleProgress: number) => number in their shared BurnParticleOptions. Use any synchronous function to control how lift changes over each particle's lifetime:

burn(headingElement, {
  fire: {
    lift: 0.1,
    liftCurve: (particleProgress) => 0.2 + 1.8 * particleProgress ** 2
  },
  smoke: {
    lift: 0.05,
    liftCurve: (particleProgress) => 1 - particleProgress
  }
});

particleProgress is the normalized age of that individual particle: 0 at birth and 1 at expiry, independent of the overall animation timing. The callback runs once per live particle per animation frame. Expired particles are removed before it runs, so callbacks receive values in [0, 1) and are not called at 1.

The returned number multiplies the configured lift for that step, before the existing velocity damping. Without a curve, the multiplier remains exactly 1 + particleProgress * 0.5. A custom curve replaces that multiplier. For example, () => 2 applies twice the base lift throughout the particle's life; it does not also apply the default ramp.

Zero disables lift acceleration for that step, while a negative multiplier reverses it. Neither resets the particle's existing velocity. NaN, positive or negative Infinity, non-number results from JavaScript callers, and thrown errors fall back to the default multiplier for that step. The callback is tried again on the next step, and the effect can still complete or be cancelled.

Keep curves fast and free of side effects: they run for every live particle, potentially thousands of times per frame. Seeded replays stay deterministic when the curve itself is deterministic. Particle counts, palettes, lifetimes, initial velocities, and rendering are unchanged.

Presets

Preset Best for
soft Default profile with balanced timing and density.
wildfire Stronger fire and dense smoke.
smolder Slower reveal with long smoke.
flash Short, bright ignition.
ritual Warm theatrical fire with expressive smoke.
psycho Saturated color, heavy turbulence, and long colored smoke.

Masks

Images and canvas elements can use pixel data automatically. Text elements can use a generated alpha mask while staying real selectable DOM text. Generic DOM elements use their bounds, which means any element can be burned into view without extra dependencies.

burn(imageElement, {
  mask: {
    source: "alpha",
    alphaThreshold: 150,
    stepPx: 3
  }
});
burn(headingElement, {
  mask: {
    source: "text",
    offset: {
      y: 6
    }
  }
});

Text masks are useful for headings, spans, paragraphs, and other real DOM text. The effect uses an internal alpha mask for the fire and smoke, while the original text remains selectable and accessible after reveal.

Use mask.offset.x and mask.offset.y to fine-tune emitter alignment. Positive y moves the burn mask downward, which can make rising fire and smoke feel more natural over text.

Supported mask sources:

  • auto
  • alpha
  • luminance
  • bounds
  • text

API

Export Description
burn(target, options) Starts the effect and returns a BurnController.
BurnInEffect Class API for direct construction when needed.
BurnController { cancel(): void; done: Promise<void> }.
BurnOptions Typed configuration for presets, reference size, timing, particles, masks, reveal, canvas, and hooks.
burnPresets Preset option map used by the resolver.
resolveBurnOptions(options) Resolves partial options into complete internal options.

Option Groups

Group Controls
referenceSize Optional reference measurement in CSS pixels for uniform spatial scaling and normalized emission.
scaleBasis "short-side", "width", "height", "font-size", or a target callback. Used only with a valid referenceSize.
timing Delay, ignition, burn, fade, ember, smoke, and easing durations.
fire Fire particle density, colors, lift and lift curve, spread, turbulence, size, lifetime, and blending.
smoke Smoke particle density, colors, lift and lift curve, drift, expansion, size, lifetime, and blending.
mask Source, thresholds, sampling step, padding, offset, and edge bias.
reveal Target opacity, reveal phase, duration, and easing.
canvas Overlay class, z-index, pixel ratio, and pointer events.
hooks onStart, onIgnite, onReveal, onDone, and onCancel.

Accessibility and Motion

Avoid autoplaying heavy effects when prefers-reduced-motion is active. Keep the underlying content meaningful without the animation, then start Burn-In.js only when motion is allowed or explicitly requested.

import { burn, type BurnOptions } from "@gogospace/burn-in";

const titleElement = document.querySelector(".title");
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

const burnOptions: BurnOptions = {
  preset: "ritual",
  seed: "launch",
  mask: { source: "text" }
};

if (titleElement instanceof HTMLElement && !prefersReducedMotion) {
  const controller = burn(titleElement, burnOptions);

  controller.done.then(() => {
    // Continue after the reveal finishes.
  });
}

Browser Notes

  • The effect requires Canvas 2D and requestAnimationFrame.
  • Reading pixels from cross-origin images needs valid CORS headers.
  • Text masks are generated from DOM text and preserve the final real text in the page.
  • Server-rendered apps should start the effect only after the target element exists in the browser.

Use Cases

  • Dramatic logo, wordmark, or heading reveals on launch pages and product microsites.
  • Game, horror, fantasy, metal, event, or campaign title cards.
  • Image, badge, poster, or canvas artwork reveals using alpha or luminance masks.
  • Vue component reveals with replay controls for onboarding, demos, easter eggs, or state transitions.
  • Deterministic visual demos, Storybook examples, or recorded UI captures using seed.

Local Development

npm install
npm run dev
npm test
npm run build

The demo runs through Vite on port 5177.

For release checks, run the build before npm run pack:dry so the generated dist files are present.

License

MIT

Related Packages

@fireworks-js/vue

A simple fireworks library!

40,221 1,387
vue-pose

A declarative animation library for Vue

99,563 20,150
@dumbshow/vue

The Vue editor for dumbshow: ComposerApp and its panes over @dumbshow/core — mou...

883 0
thinking-orbs-vue3

Vue 3 port of the dotted thought-orb loading indicators for AI and agent UIs

1,087
@lottiefiles/dotlottie-vue

Vue wrapper around the dotlottie-web library

1,837,720 929

Version History

Version Released Vue License
0.3.0 ^3.4.0 || ^3.5.0 MIT
0.2.0 ^3.4.0 || ^3.5.0 MIT
0.1.2 ^3.4.0 || ^3.5.0 MIT
0.1.1 ^3.4.0 || ^3.5.0 MIT
0.1.0 ^3.4.0 || ^3.5.0 MIT