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

frame-animator

v1.0.0

Published

一个高性能的 H5 序列帧动画渲染库

Readme

像平时开发中可能会遇到需要绘制动画的需求:loading加载过渡、开屏动画、抽奖动画等等

[frame-animator](https://github.com/shenzhongchaoii/frame-animator) 是一个高性能的帧动画工具库,使用双屏canvas进行预绘制,支持播放、暂停、停止、改变帧率等,不妨一试。

📦 安装指南

选择你喜欢的包管理器安装:

# npm
npm install frame-animator@latest

# yarn
yarn add frame-animator@latest

# pnpm
pnpm add frame-animator@latest

✨ 核心优势:

  • 离屏(双屏)画布绘制
  • 简化动画帧迭代循环
  • 事件机制,如动画开始、结束、每帧事件
  • 支持多种绘制适配模式(cover、contain、width、height)、支持多种定位模式(cc、tl、tr、tc、bl、br、bc、cl、cr)
  • 提供简单易用的 API

1、离屏画布绘制

CanvasRenderingContext2D 是一个状态机的处理,几乎所有的绘制操作都会造成它的状态变化,

CanvasRenderingContext2D.drawImage(
  image,
  sX,
  sY,
  sWidth,
  sHeight,
  dX,
  dY,
  dWidth,
  dHeight
)

众所周知,drawImage 用于实现图片绘制到画布上,当需要图片与画布的尺寸绘制裁切时,频繁地变更 context 是非常恶心的,性能开销大

而通过离屏画布实现单独绘制操作,可以避免在显示画布上频繁绘制操作导致的回流与重绘,直接去中间态,一步到位。

this.offscreenCtx.drawImage(
  image,
  sX,
  0,
  frameWidth,
  frameHeight,
  this.frameOffsetX,
  this.frameOffsetY,
  this.frameScaledWidth,
  this.frameScaledHeight
)

// 将离屏画布绘制到屏幕画布
this.onscreenCtx.drawImage(this.offscreenCanvas, 0, 0)

2、简化动画帧迭代循环

通过ES6 生成器,优雅管理复杂的帧状态变化,无需维护迭代变量。

/**
 * 帧序列生成器
 */
private *_frameGenerator(
  start: number,
  total: number,
  loopIndexRange?: [number, number],
): Generator<number> {
  let frame = start

  const [loopIndexStart, loopIndexEnd] = loopIndexRange ?? [-1, -1]
  const isLoopSupported =
    loopIndexStart > -1 && loopIndexEnd > -1 && loopIndexStart < loopIndexEnd

  while (this.isPlaying) {
    if (isLoopSupported && frame > loopIndexEnd) {
      frame = loopIndexStart
    }

    if (frame >= total) break

    yield frame++
  }
}

3、状态管理

引入 事件机制,可以在动画的不同阶段注册回调函数,并在相应事件触发时调用。另外明确动画的播放、暂停、停止状态,通过状态管理来控制动画的流动,结合 事件机制 可以实现几乎 100% 的控制。

// 动画事件类型
export enum FrameAnimationEvent {
  START = 'start',
  END = 'end',
  FPS = 'fps',
}

🛠️ 配置介绍

参数介绍

// 帧动画初始化配置
export interface FrameAnimationOptions {
  container: string | HTMLCanvasElement
  width: number
  height: number
  spriteConfig: SpriteConfig
  autoPlay?: boolean // 是否自动播放
}

export interface SpriteConfig {
  // 精灵图(从左到右)的 URL 或 Image 对象
  sprites: { url: string | HTMLImageElement; frames: number }[]

  // 绘制模式配置
  fit?: FitMode
  position?: PositionMode

  // 帧尺寸配置
  frameWidth: number
  frameHeight: number
  framePadding?: number // 雪碧图间距

  // 动画控制参数
  total: number // 总帧数
  loopIndexRange?: [number, number] // 循环区间
  frameRate?: number // 帧率,默认30
}

sprites

url 雪碧图(也叫精灵图)组或者图片组,用于后续逐帧绘制;frames 则是对应的帧总数或者叫做组合图的总数。比如说:

// 第一种,使用雪碧图(从左到右)
const sprites = [
  {
    url: 'https://xxx.com/30帧组合的雪碧图.png',
    frames: 30 // 对应上边 url 30帧组合
  }
]

// 第二种,使用连续图片组
const sprites = Array.from({ length: 30 }).map((_, index) => {
  return {
    url: `https://xxx.com/第${index + 1}帧的图.png`,
    frames: 1
  }
})

fit 和 position

提供 fitposition 实现各种模式的绘制绘制,默认是 fit: 'cover'position: 'cc',也就是居中将图片的比例缩放到充满容器。

export type FitMode = 'cover' | 'contain' | 'width' | 'height'
export type PositionMode = 'cc' | 'tl' | 'tr' | 'tc' | 'bl' | 'br' | 'bc' | 'cl' | 'cr'

frameWidth、frameHeight 和 framePadding

frameWidthframeHeight 用来表示每帧绘制的宽高大小;而 framePadding 是雪碧图每帧的间隔,确保可以准确取出每一帧进行绘制。

total

total,也就是 sprites 中的所有 frames 的总数。为什么不直接逻辑计算得到呢?因为有时候并不需要完全绘制,接下去看就知道啦。

loopIndexRange

loopIndexRange 表示不断循环绘制的帧区间,这也就是为什么 total 是传值而不是自动计算的原因。

举个例子: 需求:比如有两组雪碧图,一组有90帧是用于常态不断循环播放,一组有10帧是用于当用户点击后单次播放。 解法:那么就可以这样子设置 loopIndexRange: [0, 89]total: 100,使用后边提到的 playWithIndex 就可以控制实现上边需求啦。

方法介绍

play、pause、stop、destroy

play 播放动画 pause 暂停动画 stop 停止动画,但是没有清除缓存数据,后续可以使用 play 恢复播放 destroy 完全停止动画,会清除缓存数据,后续无法使用 play 恢复播放 destroy 完全停止动画,会清除缓存数据,后续无法

playWithIndex

playWithIndex 就比较好玩。startIndex 设置从指定帧开始播放,newLoopIndexRange 设置接下来的循环播放区间。

/**
 * 从指定帧开始播放
 */
public async playWithIndex(startIndex: number, newLoopIndexRange?: [number, number]) {
  if (this.requestId) {
    cancelAnimationFrame(this.requestId)
    this.requestId = null
  }

  this.isPlaying = true // 设置动画播放状态

  this._animate(startIndex, newLoopIndexRange) // 启动动画并指定起始帧和循环范围
}

resize

有时候设备的尺寸在使用过程中发生变化,比如缩放、旋转,那么就可以使用 resize,将更新后的宽高传递。

addEventListener 和 removeEventListener

事件监听器,用于在动画的不同阶段注册回调函数,并在相应事件触发时调用

/**
  * 添加事件监听器
  */
public addEventListener(eventName: FrameAnimationEvent, callback: (param1?: any) => void) {
  if (!this.eventListeners.has(eventName)) {
    this.eventListeners.set(eventName, new Set())
  }
  const listeners = this.eventListeners.get(eventName)!
  if (!listeners.has(callback)) {
    listeners.add(callback)
  }
}

/**
  * 移除事件监听器
  */
public removeEventListener(eventName: FrameAnimationEvent, callback: (param1?: any) => void) {
  const listeners = this.eventListeners.get(eventName)
  if (listeners && listeners.has(callback)) {
    listeners.delete(callback)
    if (listeners.size === 0) {
      this.eventListeners.delete(eventName)
    }
  }
}

💡 来个实际例子

例子1

<!-- AnimationFrame.vue -->
<template>
  <canvas
    ref="AnimationFrameCanvasRef"
    id="frame-animator-canvas"
    relative
    w-full
    h-full
  />
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { useRootStore } from '@/store'
import {
  FrameAnimation,
  FrameAnimationOptions,
  FrameAnimationEvent
} from 'frame-animator'

const emit = defineEmits(['start', 'end'])

const useDraw = (
  options: FrameAnimationOptions,
  callback?: Partial<Record<FrameAnimationEvent, () => void>>
) => {
  const frameAnimation = ref<InstanceType<typeof FrameAnimation>>(null!)

  onUnmounted(() => {
    if (callback) {
      if (callback?.start) {
        frameAnimation.value.removeEventListener(
          FrameAnimationEvent.START,
          callback?.start
        )
      }
      if (callback?.end) {
        frameAnimation.value.removeEventListener(
          FrameAnimationEvent.END,
          callback?.end
        )
      }
    }
  })

  return {
    frameAnimation,
    init: async (width, height) => {
      options.width = width
      options.height = height
      frameAnimation.value = new FrameAnimation(options)
      if (callback) {
        if (callback?.start) {
          frameAnimation.value.addEventListener(
            FrameAnimationEvent.START,
            callback?.start
          )
        }
        if (callback?.end) {
          frameAnimation.value.addEventListener(
            FrameAnimationEvent.END,
            callback?.end
          )
        }
      }
    },
    play: () => {
      frameAnimation.value?.play?.()
    },
    playWithIndex: (startIndex, newLoopIndexRange?: [number, number]) => {
      frameAnimation.value?.playWithIndex?.(startIndex, newLoopIndexRange)
    },
    pause: () => {
      frameAnimation.value?.pause?.()
    },
    stop: () => {
      frameAnimation.value?.stop?.()
    },
    stopLoop: () => {
      frameAnimation.value?.stopLoop?.()
    },
    resize: (width, height) => {
      frameAnimation.value?.resize?.(width, height)
    }
  }
}

const parentWidth = ref(0)
const parentHeight = ref(0)

const AnimationFrameCanvasRef = ref<HTMLCanvasElement>()

const {
  frameAnimation,
  init,
  play,
  playWithIndex,
  pause,
  stop,
  stopLoop,
  resize
} = useDraw(
  {
    container: '#frame-animator-canvas',
    spriteConfig: props.spriteConfig,
    width: 375,
    height: 750,
    autoPlay: true
  },
  {
    start: () => {
      emit('start')
    },
    end: () => {
      emit('end')
    }
  }
)


onMounted(() => {
  const { clientWidth, clientHeight } = AnimationFrameCanvasRef.value || {}
  init(clientWidth, clientHeight)
})

onUnmounted(() => {
  stop()
})
</script>