npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yagejs-addons/feel

v0.1.0

Published

Composable game-feel cues for YAGE: visual motion, time, camera, audio, particles, and renderer effects

Readme

@yagejs-addons/feel

Compose the small responses that make an action readable and satisfying, then play them from one named trigger.

The playable example groups the effects across four scenes. It covers impacts, trails, afterimages, highlights, springs, visibility, cue composition, scene and target time effects, animation, callbacks, shockwaves, camera modifiers, glitch, blur, implosion, dissolve, practical recipes, and a custom effect. Press N or P to move between scenes with a slide transition.

import { Feel, feelHitStop, feelParallel } from "@yagejs-addons/feel";
import {
  feelCameraShake,
  feelHitFlash,
  feelScalePunch,
  feelSquash,
} from "@yagejs-addons/feel/renderer";

enemy.add(
  new Feel({
    hit: feelParallel(
      feelSquash({ target: enemySprite, amount: 0.2 }),
      feelHitStop({ duration: 0.05 }),
      feelCameraShake({ camera, intensity: 5 }),
      feelHitFlash(enemySprite.fx, { color: 0xffffff }),
    ),
  }),
);

enemy.get(Feel).play("hit");

Install

npm install @yagejs-addons/feel @yagejs/core

# Install only the peers used by the optional entries you import:
npm install @yagejs/renderer @yagejs/effects
npm install @yagejs/audio
npm install @yagejs/particles

The root entry imports only @yagejs/core. Renderer, audio, particles, and recipes have separate entry points.

Compose cues

  • feelParallel(...nodes) starts every node together.
  • feelSequence(...nodes) starts each node after the previous one finishes.
  • feelDelay(seconds, node?) delays a node or creates an empty wait.
  • feelRepeat(node, times, gap?) repeats a finite node a fixed number of times.
  • feelLoop(node, gap?) repeats a finite node until release.
  • defineFeelEffect(duration, create) adds a game-specific effect.
  • defineFeelState(timing, create) adds an attack, hold, and release state.

Each cue also accepts trigger policy:

const feel = entity.add(
  new Feel({
    hit: {
      effect: feelScalePunch({ target: playerSprite, scale: 1.15 }),
      overlap: "restart", // "restart" (default), "ignore", or "allow"
      chance: 0.9,
      cooldown: 0.05,
      intensity: [0.9, 1.1],
    },
  }),
);

feel.play("hit", { intensity: 1.5, duration: 0.3 });
feel.stop("hit");

Chance and intensity ranges use YAGE's scene-scoped random service. Disabling or destroying Feel stops every active cue and restores active effects. A finite play-time duration scales the cue's child timings and local update clock. Intensity is a non-negative multiplier and may exceed 1. release() lets held states and loops finish their release behavior; stop() cancels immediately.

FeelPulseTiming describes the finite rise-and-fall curve shared by renderer pulses:

interface FeelPulseTiming {
  duration?: number;
  peakAt?: number;
  attackEasing?: EasingFunction;
  releaseEasing?: EasingFunction;
}

Builders validate and capture duration and peakAt when called. Most pulses, including opacity, recoil, and bounce, peak at 0.25 and use easeOutQuad for both phases. Hit flash keeps its 0.12-second linear midpoint pulse. A play-time duration scales the whole finite cue rather than replacing one builder's timing.

Core effects

The root entry supplies:

  • feelHitStop, feelSlowMotion, and feelTargetFreeze
  • feelKeyframeAnimation and feelCall

Save and load

Cue definitions, cooldown clocks, and active playbacks are runtime-only. Normal entity setup constructs Feel when the game builds a scene, with no cue in progress.

Renderer and camera modifiers, temporary filter attachments, time requests, particle-emission requests, live particles, and transient visual entities are also omitted. The saved base transform, camera, and visual properties remain unchanged by these effects.

feelCall, defineFeelEffect, feelKeyframeAnimation, and feelSpriteAnimation can run user-supplied code. If that code writes to an explicit game-owned state root, YAGE saves the resulting value normally. Use renderer modifier handles for custom visual motion that should remain a runtime effect.

Renderer effects

Import these from @yagejs-addons/feel/renderer:

  • feelSpriteAnimation
  • feelPositionPunch, feelPositionSpring, feelRecoil, and feelBounce
  • feelRotationPunch, feelRotationSpring, and feelRotationShake
  • feelScalePunch, feelScaleSpring, feelScaleShake, feelSquash, and feelTransformShake
  • feelCameraShake, feelCameraRotation, and feelCameraZoom
  • feelHitFlash and feelShockwave
  • feelGlitch, which appears quickly, holds, and refreshes its bands from the cue's seeded random source
  • feelDissolve, which advances one visual from intact to transparent
  • feelOutline, feelGlow, and feelColorize
  • feelOpacity and feelBlink
  • feelFloatingText, feelDamageNumber, and feelImpactRing
  • feelFlightLines, feelMotionTrail, and feelAfterimage
  • feelEffect, which pulses any EffectHandle from zero to its peak and back

Motion effects target a VisualComponent, such as SpriteComponent. They add render-only position and rotation offsets and multiply render-only scale. The entity's Transform, rigid body, collider, and depth-sort position remain unchanged. Overlapping effects own separate modifiers and remove only their own values.

const springHit = feelParallel(
  feelPositionSpring({ target: enemySprite, offset: { x: -12, y: 0 } }),
  feelRotationSpring({ target: enemySprite, radians: 0.15 }),
  feelScaleSpring({ target: enemySprite, scale: 1.25 }),
);

Spring cues begin at the requested visual displacement and oscillate back to the live base value. duration sets the settling time, oscillations sets the number of rebounds, and decay controls how quickly the rebounds weaken.

import { easeOutQuad } from "@yagejs/core";
import { bloom } from "@yagejs/effects";
import { feelEffect } from "@yagejs-addons/feel/renderer";

const bloomPulse = feelEffect(worldLayer.fx, bloom({ bloomScale: 1.5 }), {
  duration: 0.25,
  peakAt: 0.2,
  attackEasing: easeOutQuad,
  releaseEasing: easeOutQuad,
});

Use feelEffect for a renderer effect that only needs a temporary intensity pulse. feelGlitch changes its slice pattern during playback. feelDissolve moves in one direction instead of returning to zero. The renderer package also supplies zoomBlur, axisBlur, and implosion; static pulses use feelEffect.

feelHitFlash, feelOpacity, feelRecoil, and feelBounce accept the same four pulse-timing fields. A custom easing function runs through Feel's callback error boundary. The function must return a finite number before Feel writes renderer state.

Recipes

Recipes are ready-made compositions under @yagejs-addons/feel/recipes. Each recipe returns a normal FeelNode for the existing Feel component. The separate import distinguishes recipes from basic feelX nodes.

import {
  damageImpact,
  dashBurst,
  enemyDeath,
} from "@yagejs-addons/feel/recipes";

const feel = entity.add(
  new Feel({
    hurt: damageImpact({ target: enemySprite, value: () => lastDamage }),
    dash: dashBurst({
      target: playerSprite,
      direction: { x: 1, y: 0 },
      peakAt: 0.45,
    }),
    die: enemyDeath({
      target: enemySprite,
      onComplete: ({ entity }) => entity.destroy(),
    }),
  }),
);

| Recipe | Composition | | -------------- | ------------------------------------------------------------------- | | impact | Hit flash, scale punch, visual shake, and an impact ring | | damageImpact | impact plus a floating damage number | | dashBurst | Axis stretch, axis blur, and directional flight lines | | spawnPop | Scale and position springs plus a short glow | | enemyDeath | Impact flash, ring, shake, glow, and edged dissolve | | voidCollapse | Inward blur, center-expanding implosion, peak hold, and color shift |

Recipes do not add sound, camera movement, or time changes. enemyDeath requires onComplete, runs it after the temporary visual handles are removed, and does not destroy the entity itself. Stopping the cue before completion does not run the callback. Nested option objects expose each recipe's basic parts. dashBurst applies its top-level pulse curve to squash and axis blur. Its duration also controls the flight lines. The defaults are 0.3 seconds, a peak at 0.3, and easeOutQuad for both phases.

Highlights and combat callouts

Outline, glow, and colorize effects pulse a filter on one visual. Floating text, damage numbers, and impact rings spawn independent world-space visuals, so retriggers can overlap without sharing state. Feel-owned filter pulses are omitted from save snapshots.

const criticalHit = feelParallel(
  feelOutline({
    target: enemySprite,
    color: 0xffd54a,
    thickness: 3,
    duration: 0.25,
  }),
  feelGlow({ target: enemySprite, color: 0xff8800, duration: 0.3 }),
  feelDamageNumber({
    value: () => lastDamage,
    critical: () => lastHitWasCritical,
    prefix: "-",
    layer: "effects",
  }),
  feelImpactRing({ color: 0xffd54a, layer: "effects" }),
);

Flight lines, motion trails, and afterimages

feelFlightLines creates a short directional streak field. feelMotionTrail samples a live world position and draws a fading line through recent samples. feelAfterimage leaves tinted copies of a sprite's current frame behind its rendered pose. All three effects own temporary entities and leave gameplay transforms unchanged.

const dash = feelParallel(
  feelFlightLines({ direction: () => velocity, duration: 0.25 }),
  feelMotionTrail({
    position: () => player.get(Transform).worldPosition,
    duration: "held",
    lifetime: 0.18,
  }),
  feelAfterimage({
    target: playerSprite,
    count: 5,
    interval: 0.05,
    tint: 0x1e3a8a,
  }),
);

A fixed flight-line direction must be finite with a magnitude greater than 1e-6; invalid fixed values throw when feelFlightLines is called. A direction function is evaluated once when each burst starts. A finite zero or near-zero result creates no streak entity, but the empty burst still lasts for its configured duration.

Afterimages accept SpriteComponent and AnimatedSpriteComponent. Each copy captures the current animation frame, anchor, effective rendered transform, and opacity. Copies fade independently and are removed on completion or cancellation.

A held motion trail samples until release, then stays alive for one lifetime while its last points fade.

feelFloatingText and feelDamageNumber use the cue entity's world Transform by default. Pass position to spawn elsewhere. Each playback creates one temporary entity and destroys it when the effect completes or is cancelled. Active callouts are omitted from save snapshots. Use a custom pool instead when a game displays very large numbers of callouts every frame.

Audio and particles

import { feelSound } from "@yagejs-addons/feel/audio";
import {
  feelParticleBurst,
  feelParticleEmit,
} from "@yagejs-addons/feel/particles";

const impact = feelParallel(
  feelSound({ alias: "impact", speed: [0.95, 1.05] }),
  feelParticleBurst({ emitter: sparks, count: [8, 12] }),
  feelParticleEmit({ emitter: smoke, duration: 0.2 }),
);

Sounds keep the overall playback active until natural completion, release, or cancellation. A sound remains a zero-time sequence step. With once: true, each cue owns one AudioManager.requestOnce request, so releasing the cue does not stop another request or a playOnce owner. Set particle emission to duration: "held" to emit until graceful release; existing particles keep their own lifetimes.

Playback events

The host entity emits FeelStartedEvent, FeelCompletedEvent, and FeelStoppedEvent. Each payload carries the cue name and its FeelPlaybackHandle. play() returns null while the component is dormant or when chance, cooldown, or overlap: "ignore" rejects the trigger.