konva 支持 动态 webp 图片

21 阅读3分钟

Konva 支持 动态 webp 图片

ScreenShot_2026-04-02_105815_094.png

自从用了konva一直纠结图片引用动图的问题,目前有两个设想;

  1. 解析图片,逐帧播放;
  2. 再画板上面放一层dom ,将图片视频等相对定位上面;

这里我用的第一种方式,第二种感觉应该也可以,后面有时间再尝试,gif格式的图片,因为官方给了个例子所以很好弄,这里简单看下:

// index.html
<script src="/gifler.min.js"></script>

// 使用
const gifler = window.gifler

// 处理GIF
if(src.indexOf('.gif')>-1){
  const canvas = document.createElement('canvas')
  canvas.width = dom.naturalWidth
  canvas.height = dom.naturalHeight

  const gif = gifler(src)
  gif.frames(canvas, (ctx: CanvasRenderingContext2D, frame: {
    buffer: HTMLCanvasElement
  }) => {
    ctx.drawImage(frame.buffer, 0, 0)
    layer?.batchDraw()
  })
  
  node.image(canvas);

}

使用webp,用插件先解析图片,然后,获取每一帧,再利用 requestAnimationFrame 播放,这里涉及到一个问题,就是在销毁node的时候,要取消掉 requestAnimationFrame 所以做了一个存和销毁的方法,切记要销毁!!

修改vite.config.ts

server: {
  // 配置 wasm 文件的 MIME 类型
  fs: {
    strict: false,
  },
},
// 优化依赖,排除 @jsquash/webp 让其自行处理 wasm
optimizeDeps: {
  exclude: ['@jsquash/webp'],
},
// 配置 worker 格式
worker: {
  format: 'es',
},

安装解析工具

npm i @jsquash/webp @1.5.0-animated-webp-support

//
"@jsquash/webp": "^1.5.0-animated-webp-support",

创建一个class方法


import Konva from "konva";
import { decodeAnimated } from "@jsquash/webp";

// @ts-ignore
const gifler = window.gifler

// 获取图片
export class ImgGetTool {
  static readonly name = 'ImgGetTool'

  // 存储动画帧ID,用于销毁 - 使用唯一ID作为键
  animationFrames: Map<string, number> = new Map()
  // 存储canvas与唯一ID的映射
  canvasToIdMap: Map<HTMLCanvasElement, string> = new Map()

  constructor() {}
  
  // 加载图片 返回图片dom
  public loadImgSrcReDom = (src: string): Promise<undefined | HTMLImageElement> => {
    return new Promise(resolve => {
     if (!src) {
       resolve(undefined)
       return undefined
     }
     const imageObj1 = new Image();
     imageObj1.src = src;
     imageObj1.onload = function () {
       resolve(imageObj1)
     };
    })
 }

  // 判断是不是动画图(gif或webp)并加载
  async getAnimatedDom(dom:any,layer:Konva.Layer){
    if(dom){
      let src = dom.getAttribute('src')
      if(src){
        // 处理GIF
        if(src.indexOf('.gif')>-1){
          const canvas = document.createElement('canvas')
          canvas.width = dom.naturalWidth
          canvas.height = dom.naturalHeight

          const gif = gifler(src)
          gif.frames(canvas, (ctx: CanvasRenderingContext2D, frame: {
            buffer: HTMLCanvasElement
          }) => {
            ctx.drawImage(frame.buffer, 0, 0)
            layer?.batchDraw()
          })

          return {
            type:true,
            canvas:canvas
          }
        }

        // 处理WebP - 使用 @jsquash/webp 库解析成多帧然后播放
        if(src.indexOf('.webp')>-1){
          const canvas = await this.getWebpCanvas(src, dom, layer)
          // 返回Canvas对象
          return {
            type:true,
            canvas:canvas
          }
        }

      }
      return {
        type:false,
        canvas:dom
      }
    }
    return {
      type:false,
      canvas:''
    }
  }

  // 处理webp
  private async getWebpCanvas(src:any, dom:any, layer:any){
    const canvas = document.createElement('canvas')
    // 生成唯一ID
    const uniqueId = `webp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
    // 存储canvas与唯一ID的映射
    this.canvasToIdMap.set(canvas, uniqueId)

    // 失败的
    const errHandle = () => {
      // 失败时使用静态图片
      canvas.width = dom.naturalWidth
      canvas.height = dom.naturalHeight
      const ctx = canvas.getContext('2d')
      if (ctx) {
        ctx.drawImage(dom, 0, 0)
        layer?.batchDraw()
      }
    }

    // 获取WebP文件并使用 @jsquash/webp 解析帧
    await fetch(src)
        .then(res => res.arrayBuffer())
        .then(async (buffer) => {
          try {
            // 使用 @jsquash/webp 解码动画帧
            const frames:any = await decodeAnimated(buffer)

            if (frames && frames.length > 0) {
              // 设置Canvas尺寸为第一帧的尺寸
              canvas.width = frames[0].imageData.width
              canvas.height = frames[0].imageData.height

              const ctx = canvas.getContext('2d')
              if (ctx) {
                let currentFrame = 0
                let lastTime = 0

                // 播放动画帧
                const playFrame = (timestamp: number) => {
                  if (!lastTime) lastTime = timestamp
                  const elapsed = timestamp - lastTime

                  const frame:any = frames[currentFrame]

                  // 使用帧的duration来控制播放速度(duration是毫秒)
                  const frameDuration = frame.duration || 100

                  // 检查是否需要切换到下一帧
                  if (elapsed >= frameDuration) {
                    // 绘制当前帧的图像数据
                    ctx.clearRect(0, 0, canvas.width, canvas.height)
                    ctx.putImageData(frame.imageData, 0, 0)
                    layer?.batchDraw()

                    // 切换到下一帧
                    currentFrame = (currentFrame + 1) % frames.length
                    lastTime = timestamp
                  }

                  // 存储动画帧ID
                  const frameId = requestAnimationFrame(playFrame)
                  // 存储到动画帧Map中,使用唯一ID作为键
                  this.animationFrames.set(uniqueId, frameId)
                }
                // 开始播放
                const initialFrameId = requestAnimationFrame(playFrame)
                // 存储初始动画帧ID,使用唯一ID作为键
                this.animationFrames.set(uniqueId, initialFrameId)
              }
            } else {
              errHandle()
            }
          } catch (err) {
            errHandle()
          }
        })
        .catch(() => {
          errHandle()
        })
    return canvas
  }

  // 销毁动画
  public destroyAnimation(canvas: HTMLCanvasElement) {
    // 通过canvas找到唯一ID
    const uniqueId = this.canvasToIdMap.get(canvas)
    if (uniqueId) {
      // 取消动画帧
      const frameId = this.animationFrames.get(uniqueId)
      if (frameId) {
        cancelAnimationFrame(frameId)
        this.animationFrames.delete(uniqueId)
      }
      // 从映射中移除
      this.canvasToIdMap.delete(canvas)
    }
  }

  // 销毁所有动画
  public destroyAllAnimations() {
    // 取消所有动画帧
    this.animationFrames.forEach((frameId) => {
      cancelAnimationFrame(frameId)
    })
    this.animationFrames.clear()
    // 清空canvas到ID的映射
    this.canvasToIdMap.clear()
    // 清空缓存
    this.imgNodeGifMap = {}
  }
}

使用时候就是 new class ,测试时候发现性能还可以,后面有什么建议,望大家指点,感谢点赞关注!