Skip to content

Latest commit

 

History

History
537 lines (390 loc) · 12.7 KB

File metadata and controls

537 lines (390 loc) · 12.7 KB

@duxapp/react-native-canvas

@duxapp/react-native-canvas 是一个基于 @shopify/react-native-skia 的 React Native canvas 绘图库,提供 CanvasPath2DOffscreenCanvasImage 以及接近 Canvas 2D 的绘制 API。当前只支持 2d,不支持 WebGL 或其他 3D 渲染上下文。

目录

主要特性

  • React Native Canvas 组件
  • 类 Canvas 2D context API
  • 仅支持 2D,不支持 WebGL / 3D 上下文
  • Path2D
  • OffscreenCanvas
  • Image
  • useClickable
  • 文本绘制与测量
  • 变换、状态栈
  • 渐变、图案、阴影、混合模式
  • toDataURL()
  • 推荐开启 picture 模式,可明显提升大多数绘制场景下的帧率
  • 内部已经处理 DPI,不要再额外做 PixelRatio / devicePixelRatio 缩放

安装

yarn add @duxapp/react-native-canvas @shopify/react-native-skia

npm install @duxapp/react-native-canvas @shopify/react-native-skia

必须安装的依赖:

  • @shopify/react-native-skia

这个包本身是纯 JS,但宿主工程必须正确安装 @shopify/react-native-skia

快速开始

import { useEffect } from 'react'
import { Canvas, useCanvasRef } from '@duxapp/react-native-canvas'

export default function Demo() {
  const ref = useCanvasRef()

  useEffect(() => {
    let mounted = true

    ref.current?.getCanvas().then(({ canvas, size }) => {
      if (!mounted) return

      const ctx = canvas.getContext('2d')

      ctx.fillStyle = '#f5f5f5'
      ctx.fillRect(0, 0, size.width, size.height)

      ctx.beginPath()
      ctx.moveTo(20, 20)
      ctx.lineTo(180, 20)
      ctx.lineTo(100, 120)
      ctx.closePath()

      ctx.fillStyle = '#2f80ed'
      ctx.strokeStyle = '#0f4aa1'
      ctx.lineWidth = 4
      ctx.fill()
      ctx.stroke()

      ctx.font = 'bold 20px sans-serif'
      ctx.fillStyle = '#111'
      ctx.fillText('Hello Canvas', 20, 170)
    })

    return () => {
      mounted = false
    }
  }, [ref])

  return <Canvas ref={ref} style={{ flex: 1, minHeight: 240 }} />
}

示例

import { useEffect } from 'react'
import { Canvas, Path2D, useCanvasRef } from '@duxapp/react-native-canvas'

export default function Example() {
  const ref = useCanvasRef()

  useEffect(() => {
    ref.current?.getCanvas().then(({ canvas }) => {
      const ctx = canvas.getContext('2d')
      const path = new Path2D()

      path.moveTo(40, 40)
      path.lineTo(160, 40)
      path.lineTo(100, 140)
      path.closePath()

      ctx.fillStyle = '#e8f1ff'
      ctx.fillRect(0, 0, canvas.width, canvas.height)

      ctx.fillStyle = '#2f80ed'
      ctx.strokeStyle = '#1456b8'
      ctx.lineWidth = 3
      ctx.fill(path)
      ctx.stroke(path)
    })
  }, [ref])

  return <Canvas ref={ref} style={{ flex: 1 }} />
}

Picture + 动画示例

import { useEffect } from 'react'
import { Canvas, useCanvasRef } from '@duxapp/react-native-canvas'

export default function PictureAnimationExample() {
  const ref = useCanvasRef()

  useEffect(() => {
    let frameId = 0
    let active = true

    ref.current?.getCanvas().then(({ canvas, size }) => {
      if (!active) return

      const ctx = canvas.getContext('2d')
      let x = 30
      let direction = 1

      const render = () => {
        if (!active) return

        ctx.fillStyle = '#f7f8fa'
        ctx.fillRect(0, 0, size.width, size.height)

        ctx.beginPath()
        ctx.arc(x, size.height / 2, 24, 0, Math.PI * 2)
        ctx.fillStyle = '#2f80ed'
        ctx.fill()

        x += direction * 2
        if (x >= size.width - 30 || x <= 30) {
          direction *= -1
        }

        frameId = requestAnimationFrame(render)
      }

      render()
    })

    return () => {
      active = false
      cancelAnimationFrame(frameId)
    }
  }, [ref])

  return <Canvas ref={ref} picture style={{ flex: 1, minHeight: 240 }} />
}

性能说明

当前性能大致可以这样理解:

  • 和现有的 React Native canvas 插件,例如 react-native-canvasreact-native-gcanvas 相比,在相同类型的绘制场景下,这个库通常会有非常明显的性能提升
  • 但和原生 canvas 相比,仍然还有很大的差距
  • 在相同场景下,目前的表现通常还是明显低于原生 canvas 的一半

这意味着:

  • 如果你现在使用的是较旧的 RN canvas 方案,迁移过来通常会有很明显的收益
  • 如果你的目标是接近原生 canvas 性能,目前仍然要预期存在较大的差距
  • 在渲染模型允许的情况下,强烈建议开启 picture 模式,以尽可能发挥当前实现的帧率表现

详细文档说明

下面的章节会继续说明这个包的 API、渲染模式、支持的绘制源、兼容性说明以及已知限制。

基本使用方式

1. 使用 useCanvasRef 创建 ref

const ref = useCanvasRef()

这个 ref 直接传给 Canvas

<Canvas ref={ref} style={{ flex: 1 }} />

2. 获取 canvas 实例

const { canvas, size } = await ref.current.getCanvas()

返回值说明:

  • canvas:canvas 风格对象
  • size:当前布局信息 { width, height, x, y }

3. 获取 2D context

const ctx = canvas.getContext('2d')

导出内容

import {
  Canvas,
  Image,
  OffscreenCanvas,
  Path2D,
  defineCanvas,
  defineCanvasContext,
  useCanvasRef,
  useClickable
} from '@duxapp/react-native-canvas'

说明:

  • Canvas:React Native canvas 组件
  • useCanvasRef:返回 Canvas 所需 ref 的 hook
  • Path2D:路径对象
  • OffscreenCanvas:离屏画布
  • Image:用于 drawImage() 的图片对象
  • useClickable:独立的 hook,用于在 React Native 上模拟网页端触摸事件
  • defineCanvas / defineCanvasContext:为了兼容保留下来的 identity helper

useClickable

useClickable 是从原始 RN 实现中抽出来的独立 hook,它的作用是在 React Native 上模拟网页端触摸事件,让一些库可以更少改动地运行。它没有接入 Canvas 内部。

import { Canvas, useCanvasRef, useClickable } from '@duxapp/react-native-canvas'

export default function Demo() {
  const ref = useCanvasRef()
  const clickable = useClickable({
    onClick: e => {
      console.log('click', e.detail.x, e.detail.y)
    },
    onLongPress: e => {
      console.log('longpress', e)
    }
  })

  return <Canvas ref={ref} {...clickable} style={{ flex: 1, minHeight: 240 }} />
}

支持这些回调:

  • onClick
  • onLongPress
  • onTouchStart
  • onTouchMove
  • onTouchEnd
  • onTouchCancel

API 概览

Canvas 组件

Props:

  • style
  • onLayout
  • picture

picture 用于启用 React Native 的 picture 渲染路径,并且在大多数场景下都推荐开启。

Canvas Ref

ref.current.getCanvas(): Promise<{
  canvas: CanvasElement
  size: {
    width: number
    height: number
    x: number
    y: number
  }
}>

Canvas Element

返回的 canvas 支持:

  • getContext('2d')
  • createImageData(width, height)
  • toDataURL(type?, encoderOptions?)
  • width
  • height

当前只支持 getContext('2d')。不支持 WebGL、WebGL2 以及其他 3D 渲染上下文。

DPI 处理说明

设备像素比已经在内部处理完成。

不要再手动对 canvas 尺寸、坐标或变换额外乘这些值:

  • PixelRatio.get()
  • window.devicePixelRatio
  • 自己定义的 DPR 缩放系数

正常情况下,直接按逻辑布局尺寸进行绘制即可。

2D Context

当前支持的能力分组:

  • 状态:saverestore
  • 变换:translatescalerotatetransformsetTransformresetTransformgetTransform
  • 路径:beginPathclosePathmoveTolineToarcarcTobezierCurveToquadraticCurveTorectroundRectellipse
  • 绘制:fillstrokeclipisPointInPath
  • 矩形:fillRectstrokeRectclearRect
  • 文本:fillTextstrokeTextmeasureText
  • 图像:drawImage
  • 像素:getImageDataputImageData
  • 样式:fillStylestrokeStylelineWidthlineCaplineJoinmiterLimitsetLineDashlineDashOffset
  • 透明度与阴影:globalAlphashadowColorshadowBlurshadowOffsetXshadowOffsetY
  • 文本样式:fonttextAligntextBaselinedirection
  • 合成:globalCompositeOperation
  • 渐变与图案:createLinearGradientcreateRadialGradientcreatePattern

Picture 模式

开启方式:

<Canvas ref={ref} picture style={{ flex: 1 }} />

这个模式通常可以明显提升绘制帧率,建议默认开启,除非你的场景强依赖持久 canvas 状态下的增量更新语义。

推荐使用方式

  • 大多数 canvas 绘制场景都建议默认开启
  • 特别适合图表、海报、预览、编辑器整帧刷新这类场景
  • 只有在你依赖持久画布状态做局部增量更新时,才建议评估是否关闭

默认优点

  • 通常可以获得更高的绘制帧率
  • 特别适合动画和整帧重绘场景
  • 当每一帧都从头计算时,渲染模型会更简单

需要注意的问题

  • picture 绘制不会保留历史内容
  • 只会保留当前这一帧绘制出来的内容
  • 到下一个时刻开始绘制时,上一帧的内容会自动被清除
  • 绘制状态不会在帧之间自动保留
  • 变换状态不会在帧之间自动保留
  • clearRect() 不再等价于持久画布上的增量擦除

如果你的场景依赖基于历史内容的局部更新,通常不适合使用这个模式。

Path2D

import { Path2D } from '@duxapp/react-native-canvas'

const path = new Path2D()
path.moveTo(20, 20)
path.lineTo(100, 20)
path.lineTo(60, 80)
path.closePath()

ctx.fill(path)
ctx.stroke(path)

也支持通过另一个 Path2D 或 SVG path 字符串初始化。

OffscreenCanvas

import { OffscreenCanvas } from '@duxapp/react-native-canvas'

const offscreen = new OffscreenCanvas(200, 120)
const ctx = offscreen.getContext('2d')

ctx.fillStyle = '#000'
ctx.fillRect(0, 0, 200, 120)

const dataUrl = offscreen.toDataURL()

适合场景:

  • 预渲染
  • 生成纹理或缩略图
  • 作为 drawImage() 的绘制源
  • 用于 createPattern() 图案源

Image

import { Image } from '@duxapp/react-native-canvas'

const image = new Image()
image.onload = () => {
  ctx.drawImage(image, 0, 0, 120, 120)
}
image.onerror = err => {
  console.error(err)
}
image.src = 'https://example.com/example.png'

支持的 src 类型:

  • 远程 URL 字符串
  • data: URL 字符串
  • require(...) 返回的本地资源 id

当前支持的成员:

  • src
  • onload
  • onerror
  • onabort
  • alt
  • complete
  • currentSrc
  • width
  • height
  • decode()
  • addEventListener()
  • removeEventListener()

drawImage() 支持的 source

当前类型和运行时都支持:

  • Image
  • OffscreenCanvas

例如:

const offscreen = new OffscreenCanvas(100, 100)
const offscreenCtx = offscreen.getContext('2d')
offscreenCtx.fillStyle = 'red'
offscreenCtx.fillRect(0, 0, 100, 100)

ctx.drawImage(offscreen, 20, 20)

toDataURL()

主 canvas 对象和 OffscreenCanvas 都支持 toDataURL()

const url = canvas.toDataURL()
const jpeg = canvas.toDataURL('image/jpeg', 0.9)

支持的输出类型:

  • image/png
  • image/jpeg
  • image/webp

兼容性说明

这个库是 canvas-like,不是浏览器完整的 HTMLCanvasElement 实现。

和浏览器相比,有一些有意保留的差异:

  • 没有 DOM API
  • 没有完整浏览器图片加载行为
  • 没有浏览器事件系统
  • 这个包版本不包含小程序兼容 API

如果某些浏览器 API 在 React Native 里没有实际意义,就不会强行暴露。

已知限制

  • 某些边界场景下行为可能与浏览器 canvas 存在差异
  • 文本测量依赖 Skia 的字体行为
  • 图片加载依赖 React Native / Skia 运行环境
  • picture 模式针对整帧重绘优化,不适合增量更新
  • 不是所有浏览器 canvas API 都已实现

类型文件

包内提供 TypeScript 类型:

  • 英文注释版本:src/index.d.ts
  • 中文注释版本:src/index.zh-CN.d.ts

License

MIT