精华 [开源] SpriteJS -- 一款简单的跨终端 canvas 绘图框架
发布于 6 年前 作者 welefen 4874 次浏览 来自 分享

SpriteJS是一款由360奇舞团开源的跨终端canvas绘图库,可以基于canvas快速绘制结构化UI、动画和交互效果,并发布到任何拥有canvas环境的平台上(比如浏览器、小程序和node)。

我们知道,Canvas Api可以很灵活地绘制各种矢量图形到画布上,但是Canvas Api本身比较低级,比如我们要在画布中央绘制一个带有圆角的红色矩形,使用Canvas原生的Api,需要这样:

JSBIN

const canvas = document.getElementById('paper'),
      context = canvas.getContext('2d')

const [x, y, w, h, r] = [200, 200, 200, 200, 50]

context.fillStyle = 'red'
context.beginPath()
context.moveTo(x + r, y)
context.arcTo(x + w, y, x + w, y + h, r)
context.arcTo(x + w, y + h, x, y + h, r)
context.arcTo(x, y + h, x, y, r)
context.arcTo(x, y, x + w, y, r)
context.closePath()
context.fill()

如果实现相同的效果,使用SpriteJS是这样写:

JSBIN

const scene = new spritejs.Scene('#container'),
      layer = scene.layer()

const s = new spritejs.Sprite({
  anchor: 0.5,
  bgcolor: 'red',
  pos: [300, 300],
  size: [200, 200],
  borderRadius: 50,
})

layer.append(s)

Sprite为图形创建类似于DOM的对象模型,因此我们可以像创建DOM元素一样,创建Sprite元素,并将它们append到layer上,从而将元素呈现到画布上。

SpriteJS 的特点

  • 基于canvas绘制的文档对象模型
  • 四种基本精灵类型:Sprite、Path、Label、Group
  • 支持基础和高级的精灵属性,精灵盒模型、属性与CSS3具有高度一致性。
  • 简便而强大的Transition、Animation API
  • 支持雪碧图和资源预加载
  • 可扩展的事件机制
  • 高性能的缓存策略
  • 对D3、Matter-js、Proton和其他第三方库友好
  • 跨平台,支持node-canvas、微信小程序

Sprite 文档结构

Sprite文档结构

SpriteJS支持设置精灵元素常用的基本属性,包括:

  • archor: 锚点,定义元素坐标的参考点,[0,0]是左上角,[1,1]是右下角
  • size([x,y]): 精灵元素的大小
  • pos([width,height]): 精灵元素的位置
  • id: 元素的ID
  • name: 元素的name
  • bgcolor: 背景颜色
  • border: 边框
  • borderRadius: 圆角
  • padding: 同css的padding
  • zIndex: 同css的zIndex
  • textures: 精灵图片
  • filter: 滤镜
  • offsetPath: 同css3的offsetPath
  • offsetDistance: 同css3的offsetDistance
  • rotate: 旋转角度
  • scale: 缩放
  • translate: 平移
  • skew: 倾斜
  • transform: 同css3的transform

如果只是绘制静态图形,SpriteJS还体现不出优势,但如果要给图形增加动画效果,那么SpriteJS内置了Transition API和标准的Web Animation API

比如我们要让上面的圆角矩形的颜色从红色过度到绿色,只需要:

JSBIN

const scene = new spritejs.Scene('#container'),
      layer = scene.layer()

const s = new spritejs.Sprite({
  anchor: 0.5,
  bgcolor: 'red',
  pos: [300, 300],
  size: [200, 200],
  borderRadius: 50,
})

layer.append(s)

s.transition(2.0).attr({bgcolor: 'green'})

我们可以同时对多个属性应用Transition:

JSBIN

const scene = new spritejs.Scene('#container'),
      layer = scene.layer()

const s = new spritejs.Sprite({
  anchor: 0.5,
  bgcolor: 'red',
  pos: [300, 300],
  size: [200, 200],
  borderRadius: 50,
})

layer.append(s)

s.transition(2.0)
 .attr({
  bgcolor: 'green',
  width: width => width + 100,
 })

而且Transition本身返回Promise,所以我们可以方便地实现连续的Transition动画:

JSBIN

const scene = new spritejs.Scene('#container'),
      layer = scene.layer()

const s = new spritejs.Sprite({
  anchor: 0.5,
  bgcolor: 'red',
  pos: [300, 300],
  size: [200, 200],
  borderRadius: 50,
})

layer.append(s)

(async function(){
  await s.transition(2.0)
    .attr({
      bgcolor: 'green',
      width: width => width + 100,
     })
  await s.transition(1.0)
    .attr({
      bgcolor: 'yellow',
      height: height => height + 100,    
    })
}())

除了简单的Transition动画之外,我们可以使用浏览器标准的Web Animation API来实现更加复杂的动画,对于Web Animation API或CSS3 Animation比较熟悉的同学应该对它比较熟悉:

JSBIN

const scene = new spritejs.Scene('#container'),
      layer = scene.layer()

const s = new spritejs.Sprite({
  anchor: 0.5,
  bgcolor: 'red',
  pos: [300, 300],
  size: [200, 200],
  borderRadius: 50,
})

layer.append(s)

s.animate([
  {rotate: 0, borderRadius: 50, bgcolor: 'red'},
  {rotate: 360, borderRadius: 0, bgcolor: 'green', offset: 0.7},
  {rotate: 720, borderRadius: 50, bgcolor: 'blue'}
], {
  duration: 3000,
  iterations: Infinity,
  direction: 'alternate',
  easing: 'ease-in-out',
})

SpriteJS支持所有的标准Web Animation API的timing选项,另外我们也可以使用规范中定义的animaton.finished的Promise方便地实现顺序的动画:

JSBIN

const scene = new spritejs.Scene('#container'),
      layer = scene.layer()

const s = new spritejs.Sprite({
  anchor: 0.5,
  bgcolor: 'red',
  pos: [100, 100],
  size: [50, 50],
})

const s2 = s.cloneNode(true)
s2.attr({
  y: 500,
})

layer.append(s, s2)

;(async function(){
  await s.animate([
    {x: 500, offset: 0.5},
    {rotate: 45, bgcolor: 'blue'},
  ], {
    duration: 2000,
    fill: 'forwards',
  }).finished

  await s2.animate([
    {x: 500, offset: 0.5},
    {rotate: 45, bgcolor: 'green'},
  ], {
    duration: 2000,
    fill: 'forwards',
  }).finished
 
  await Promise.all([s.animate([
    {y: 500}
  ], {
    duration: 2000,
    fill: 'forwards',
  }).finished, s2.animate([
    {y: 100}
  ], {
    duration: 2000,
    fill: 'forwards',
  }).finished])
  
  await Promise.all([s, s2].map(s => s.animate([
    {x: 100}
  ], {
    duration: 2000,
    fill: 'forwards',    
  }).finished))
  
  await s.animate([
    {rotate: -360, bgcolor: 'red'}
  ], {
    duration: 1000,
    fill: 'forwards',    
  }).finished

  await s2.animate([
    {rotate: -360, bgcolor: 'red'}
  ], {
    duration: 1000,
    fill: 'forwards',    
  }).finished
}())

Sprite texture

我们可以给Sprite元素绑定图片,只需要将图片的URL作为元素的textures属性。

JSBIN

const scene = new spritejs.Scene('#container'),
      layer = scene.layer()

const s = new spritejs.Sprite({
  textures: 'https://p0.ssl.qhimg.com/t01a72262146b87165f.png',
  anchor: 0.5,
  pos: [300, 300],
  scale: 0.5,
})

layer.append(s)

注意这里使用textures而不是texture作为属性名,因为sprite对象允许我们传多张图片进去,绘制的时候会将这些图片依次叠加。

我们还可以给多张图片指定rect和srcRect:

JSBIN

const scene = new spritejs.Scene('#container'),
      layer = scene.layer()

const texture = 'https://p5.ssl.qhimg.com/t01a2bd87890397464a.png'

const s = new spritejs.Sprite()
s.attr({
  anchor: 0.5,
  textures: [
    {src: texture, rect: [0, 0, 190, 268], srcRect: [0, 0, 190, 268]},
    {src: texture, rect: [200, 278, 190, 268], srcRect: [191, 269, 190, 268]},
    {src: texture, rect: [0, 278, 190, 268], srcRect: [0, 269, 190, 268]},
    {src: texture, rect: [200, 0, 190, 268], srcRect: [191, 0, 190, 268]},
  ],
  border: [2, 'grey'],
  pos: [300, 300],
})

layer.append(s)

使用textures的时候,我们支持资源的预加载和雪碧图,我们可以将资源使用TexturePacker进行打包(可使用TP的免费版,导出配置文件格式为JSON Hash)

例子

(async function () {
  const {Scene, Sprite} = spritejs
  const scene = new Scene('#paper', {viewport: ['auto', 'auto'], resolution: [1200, 1200]})

  await scene.preload([
    'https://p3.ssl.qhimg.com/t010ded517024020e10.png',
    'https://s1.ssl.qhres.com/static/6ead70a354da7aa4.json',
  ])
  
  ...
  
  const layer = scene.layer('fglayer')
	
  ...
  
  const head = new Sprite('head.png')
  head.attr({
    pos: [606, 0],
  })

  const neck = new Sprite('neck.png')
  neck.attr({
    pos: [626, 68],
    zIndex: -1,
  })

  const body = new Sprite('body.png')
  body.attr({
    pos: [606, 73],
  })

  const leftArm = new Sprite('arm-1.png')
  leftArm.attr({
    pos: [600, 73],
  })

  const rightArm = new Sprite('arm-2.png')
  rightArm.attr({
    pos: [675, 73],
  })
  
  ...
}())

绘制矢量图

除了普通的Sprite类型之外,SpriteJS提供绘制矢量图的Path类型

例子

const scene = new Scene('#svgpath', {viewport: ['auto', 'auto'], resolution: [1540, 600]})
const layer = scene.layer('fglayer')

const p1 = new Path()
p1.attr({
  path: {
    d: 'M280,250A200,200,0,1,1,680,250A200,200,0,1,1,280,250Z',
    transform: {
      scale: 0.5,
    },
    trim: true,
  },
  strokeColor: '#033',
  fillColor: '#839',
  lineWidth: 12,
  pos: [100, 50],
})

layer.appendChild(p1)

const p2 = new Path()
p2.attr({
  path: {
    d: 'M480,50L423.8,182.6L280,194.8L389.2,289.4L356.4,430L480,355.4L480,355.4L603.6,430L570.8,289.4L680,194.8L536.2,182.6Z',
    transform: {
      rotate: 45,
    },
    trim: true,
  },
  fillColor: '#ed8',
  pos: [450, 100],
})
layer.appendChild(p2)

const p3 = new Path()
p3.attr({
  path: {
    d: 'M480,437l-29-26.4c-103-93.4-171-155-171-230.6c0-61.6,48.4-110,110-110c34.8,0,68.2,16.2,90,41.8C501.8,86.2,535.2,70,570,70c61.6,0,110,48.4,110,110c0,75.6-68,137.2-171,230.8L480,437z',
    trim: true,
  },
  strokeColor: '#f37',
  lineWidth: 20,
  lineJoin: 'round',
  lineCap: 'round',
  pos: [1000, 100],
})
layer.appendChild(p3)

Path能够支持使用SVG Path来在Canvas上绘制矢量图形:

SVG和Sprite Path:

<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <path d="M 10,30
           A 20,20 0,0,1 50,30
           A 20,20 0,0,1 90,30
           Q 90,60 50,90
           Q 10,60 10,30 z"/>
</svg>

JSBIN

const p = new spritejs.Path(`M 10,30
           A 20,20 0,0,1 50,30
           A 20,20 0,0,1 90,30
           Q 90,60 50,90
           Q 10,60 10,30 z`)

p.attr({
	fillColor: 'red',
	pos: [200, 200],
})

layer.append(p)

SpriteJS还能够支持Path的transition(或animate)

JSBIN

const scene = new spritejs.Scene('#container'),
      layer = scene.layer()

const paths = [
  "M280,250A200,200,0,1,1,680,250A200,200,0,1,1,280,250Z",
  "M480,50L423.8,182.6L280,194.8L389.2,289.4L356.4,430L480,355.4L480,355.4L603.6,430L570.8,289.4L680,194.8L536.2,182.6Z",
  "M480,437l-29-26.4c-103-93.4-171-155-171-230.6c0-61.6,48.4-110,110-110c34.8,0,68.2,16.2,90,41.8C501.8,86.2,535.2,70,570,70c61.6,0,110,48.4,110,110c0,75.6-68,137.2-171,230.8L480,437z",
  "M595,82.1c1,1-1,2-1,2s-6.9,2-8.9,4.9c-2,2-4.9,8.8-4.9,8.8c3.9-1,8.9-2,13.8-4c1,0,2,1,3,2c1,0-11.8,4.9-14.8,6.9c-2,2-11.8,9.9-14.8,9.9c-2.9,0-9.9,1-9.9,1c1,2,2,3.9,3.9,6.9c0,0-6.9,4-6.9,4.9c-1,1-5.9,6.9-5.9,6.9s17.7,1.9,23.6-7.9c-5.9,9.8-19.7,19.7-48.2,19.7c-29.5,0-53.1-11.8-68.9-17.7c-16.7-6.9-38.4-14.8-56.1-14.8c-16.7,0-36.4,4.9-49.2,16.8c-22.6-8.8-54.1-4-68.9,9.8c-13.8,13.8-27.5,30.5-29.5,42.3c-2.9,12.9-9.8,43.3-19.7,57.2c-13.8,22.5-29.5,28.5-34.5,38.3c-4.9,9.9-4.9,30.5-4,30.5c2,1,8.9,0,12.8-2c7.9-2.9,29.5-25.6,37.4-36.4c7.9-10.9,34.5-58.1,38.4-74.8s7.9-33.5,19.7-42.3c12.8-8.8,28.5-4.9,28.5-3.9c0,0-14.7,11.8-15.7,44.3s18.7,28.6,8.8,49.2c-9.9,17.7-39.3,5.9-49.2,16.7c-7.9,8.9,0,40.3,0,46.2c0,6-3,33.5-4.9,40.4c-1,5.9,0,9.8-1,13.8c-1,3,6,3.9,6,3.9s-6,7.8-8.9,5.9c-2.9-1-4.9-1-6.9,0c-2,0-5.9,1.9-9.9,0L232.9,401c2,1,4.9,1.9,7.9,1c4-1,23.6-9.9,25.6-11.9c2.9-1,19.7-10.8,22.6-16.7c2-5.9,5.9-24.6,5.9-30.5c1-6,2-24.6,2-29.5s-1-13.8,0-17.7c2-2.9,4.9-6.9,8.9-8.9c4.9-1,10.8-1,11.8-1c2,0,18.7,2,21.6,2c3.9,0,19.7-2.9,23.6-5c4.9-0.9,7.8,0,8.9,2c2,1.9-2,4.9-2,5.9c-1,1-8.8,10.8-10.8,14.7c-2,4.9-8.8,13.8-6.9,17.7c2,3.9,2,4.9,7.8,7.9c5.9,1.9,28.5,13.8,41.3,25.6c13.8,12.7,26.6,28.4,28.6,36.4c2.9,8.9,7.8,9.8,10.8,9.8c3,1,8.9,2,8.9,5.9s-1,8.8-1,8.8l36.4,13.8c0,0,0-12.8-1-17.7c-1-5.9-6.9-11.8-11.8-17.7c-4.9-6.9-56-57.1-61-61c-4.9-3-8.9-6.9-9.8-14.7c-1-7.9,8.8-13.8,14.8-20.6c3.9-4.9,14.7-27.6,16.7-30.6c2-2.9,8.9-10.8,12.8-10.8c4.9,0,15.8,6.9,29.5,11.8c5.9,2,48.2,12.8,54.1,14.8c5.9,1,18.6,0,22.6,3.9c3.9,2.9,0,10.9-1,15.8c-1,5.9-11.8,27.5-11.8,27.5s2,7.8,2,13.8c0,6.9-2.9,31.5-5.9,39.3c-2,8.9-15.8,31.6-18.7,35.5c-2,2.9-4.9,4.9-4.9,9.9c0,4.9,8.8,6,11.8,9.8c4,3,1,8.8,0,14.8l39.4,16.7c0-2.9,2-7.9,0-9.9c-1-2.9-5.9-8.8-8.8-12.8c-2-2.9-8.9-13.8-10.8-15.8c-2-2.9-2-8.8,0-13.8c1-4.9,13.8-38.3,14.7-42.3c2-4.9,20.7-44.3,22.6-49.2c2-5.9,17.7-34.4,19.7-39.4c2-5.9,14.8-10.8,18.7-10.8c4.9,0,29.5,8.8,33.4,10.8c2.9,1,25.6,10.9,29.5,12.8c4.9,1.9,2,5.9-1,6.9c-2.9,1.9-39.4,26.5-42.3,27.5c-2.9,1-5.9,3.9-7.9,3.9c-2.9,0-6.9,3.1-6.9,4c0,2-1,5.9-5.9,5.9c-3.9,0-11.8-5.9-16.7-11.8c-6.9,3.9-11.8,6.9-14.8,12.8c-4.9,4.9-6.9,8.9-9.8,15.8c2,2,5.9,2.9,8.8,2.9h31.5c3,0,6.9-0.9,9.9-1.9c2.9-2,80.7-53.1,80.7-53.1s12.8-9.9,12.8-18.7c0-6.9-5.9-8.9-7.9-11.8c-3-1.9-20.7-13.8-23.6-15.7c-4-2.9-17.7-10.9-21.6-12.9c-3-1.9-13.8-5.8-13.8-5.8c3-8.9,5-15.8,5.9-17.7c1-2,1-19.7,2-22.7c0-2.9,5-15.7,6.9-17.7c2-2,6.9-17.7,7.9-20.7c1-1.9,8.8-24.6,12.8-24.6c3.9-1,7.9,2.9,11.8,2.9c4,1,18.7-1,26.6,0c6.9,1,15.8,9.9,17.7,10.8c2.9,1,9.8,3.9,11.8,3.9c1,0,10.8-6.9,10.8-8.8c0-2-6.9-5.9-7.9-5.9c-1-1-7.8-4.9-7.8-4.9c0,1,2.9-1.9,7.8-1.9c3.9,0,7.9,3.9,8.8,4.9c2,1,6.9,3.9,7.9,1.9c1-1,4.9-5.9,4.9-8.9c0-4-3.9-8.8-5.9-10.8s-24.6-23.6-26.6-24.6c-2.9-1-14.7-11.8-14.7-14.7c-1-2-6.9-6.9-7.9-7.9s-30.5-21.6-34.5-24.6c-3.9-2.9-7.9-7.8-7.9-12.7s-2-17.7-2-17.7s-6.9-1-9.8,1.9c-2.9,2-9.8,17.8-13.8,17.8c-10.9-2-24.6,1-24.6,2.9c1,2.9,10.8,1,10.8,1c0,1-3.9,5.9-6.9,5.9c-2,0-7.8,2-8.8,2.9c-2,0-5.9,3.1-5.9,3.1c2.9,0,5.9,0,9.8,0.9c0,0-5.9,4-8.9,4c-2.9,0-12.8,2.9-15.7,3.9c-2,1.9-9.9,7.9-9.9,7.9H589l1,2h4.9L595,82.1L595,82.1z",
  "M638.9,259.3v-23.8H380.4c-0.7-103.8-37.3-200.6-37.3-200.6s-8.5,0-22.1,0C369.7,223,341.4,465,341.4,465h22.1c0,0,11.4-89.5,15.8-191h210.2l11.9,191h22.1c0,0-5.3-96.6-0.6-205.7H638.9z",
  "M345.47,250L460.94,450L230,450Z M460.94,50L576.41,250L345.47,250Z M576.41,250L691.88,450L460.94,450Z",
]
const p = new spritejs.Path({d: paths[0], scale: 0.5})

p.attr({
  fillColor: 'blue',
  pos: [0, 0],
})

layer.append(p)

let i = 0

function wait(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms)
  })
}

(async function(){
  // noprotected
  for(let i = 0; i < 100; i++) {
    await p.transition(2.0)
     .attr({d: paths[++i % paths.length]})
    await wait(1000)
  }  
}())

分组

除了Sprite、Path外,SpriteJS支持Group元素,可以将多个Sprite设置成一组,进行统一的动画。

例子

(function () {
  const {Scene, Path, Group} = spritejs
  const scene = new Scene('#paper', {viewport: ['auto', 'auto'], resolution: [1200, 1200]})

  const layer = scene.layer('fglayer')
  layer.canvas.style.backgroundColor = '#9cd470'

  const d = 'M235.946483,75.0041277 C229.109329,53.4046689 214.063766,34.845093 195.469876,22.3846101 C175.428247,8.9577702 151.414895,2 127.314132,2 C75.430432,2 31.6212932,32.8626807 18.323944,74.9130141 C8.97646468,77.1439182 2,85.5871171 2,95.7172992 C2,104.709941 7.49867791,112.371771 15.2700334,115.546944 C15.8218133,115.773348 16.6030463,122.336292 16.8270361,123.236385 C22.1235768,144.534892 35.4236577,163.530709 52.5998558,176.952027 C52.6299032,176.976876 52.6626822,177.001726 52.6954612,177.026575 C72.5513428,192.535224 98.5478246,202 127.043705,202 C152.034964,202 176.867791,194.597706 197.428422,180.146527 C215.659011,167.335395 230.201962,148.621202 236.52831,126.969284 C237.566312,123.421373 238.549682,119.685713 239.038636,116.019079 C239.044099,115.983185 239.074146,115.444787 239.082341,115.442025 C246.673412,112.184022 252,104.580173 252,95.7172992 C252,85.6892748 245.15192,77.3371896 235.946483,75.0041277'
  const shadowD = 'M82.1534529,43 C127.525552,43 164.306906,33.6283134 164.306906,21.663753 C164.306906,9.6991926 127.525552,0 82.1534529,0 C36.7813537,0 0,9.6991926 0,21.663753 C0,33.6283134 36.7813537,43 82.1534529,43 Z'
  const shadow = new Path()
  shadow.attr({
    path: shadowD,
    fillColor: '#000000',
    opacity: 0.05,
    pos: [500, 734],
    anchor: 0.5,
    scale: [1.3, 1.2]
  })
  layer.append(shadow)

  const lemon = new Path()
  lemon.attr({
    path: {d},
    anchor: 0.5,
    pos: [500, 600],
    fillColor: '#fed330',
    scale: 1.4
  })
  layer.append(lemon)

  const lemonGroup = new Group()
  lemonGroup.attr({
    anchor: 0.5,
    pos: [610, 600],
    size: [180, 180],
    bgcolor: '#faee35',
    border: [6, '#fdbd2c'],
    borderRadius: 90,
    scale: 1.5
  })
  layer.append(lemonGroup)

  const d2 = 'M0,0L0,100A15,15,0,0,0,50,86.6z'
  for(let i = 0; i < 12; i++) {
    const t = new Path()
    t.attr({
      path: {d: d2, transform: {scale: 0.65}},
      pos: [90, 90],
      lineWidth: 2,
      lineCap: 'round',
      lineJoin: 'round',
      strokeColor: '#fff',
      fillColor: '#f8c32d',
      rotate: 30 * i,
    })
    lemonGroup.append(t)
  }

  lemonGroup.animate([
    {rotate: 360},
  ], {
    duration: 10000,
    iterations: Infinity,
  })

  lemonGroup.on('mouseenter', (evt) => {
    layer.timeline.playbackRate = 3.0
  })
  lemonGroup.on('mouseleave', (evt) => {
    layer.timeline.playbackRate = 1.0
  })
}())

分组可以嵌套,这样我们就可以用分组元素组合成复杂的UI组件。

响应事件

SpriteJS不只是能够给精灵元素添加动画,还能像操作DOM元素那样够给精灵元素注册事件。精灵元素支持基本的mouse事件和touch事件,这些事件被SpriteJS的scene代理给对应的精灵:

例子

const scene = new Scene('#dom-events', {viewport: ['auto', 'auto'], resolution: [1540, 600]})
const layer = scene.layer('fglayer')

const s1 = new Sprite()
s1.attr({
  anchor: [0.5, 0.5],
  pos: [770, 300],
  size: [300, 300],
  rotate: 45,
  bgcolor: '#3c7',
})

layer.append(s1)

s1.on('mouseenter', (evt) => {
  s1.attr('border', [4, 'blue'])
})
s1.on('mouseleave', (evt) => {
  s1.attr('border', [0, ''])
})

const anchorCross = new Path('M0,10H10,20M10,0V10,20')
anchorCross.attr({
  anchor: [0.5, 0.5],
  pos: [770, 300],
  strokeColor: 'red',
  rotate: 45,
  lineWidth: 4,
})

layer.append(anchorCross)

const label = new Label('鼠标位置:')

label.attr({
  pos: [20, 50],
  font: '32px Arial',
  lineHeight: 56,
})

layer.append(label)

layer.on('mousemove', (evt) => {
  const {x, y, targetSprites} = evt

  label.text = `鼠标位置:\n相对于 layer: ${Math.round(x)}, ${Math.round(y)}`

  if(targetSprites.length && targetSprites.includes(s1)) {
    const [offsetX, offsetY] = s1.pointToOffset(x, y).map(Math.round)
    label.text += `\n相对于元素:${offsetX}, ${offsetY}`
  }
})

默认代理的事件:

  • mousedown
  • mouseup
  • mousemove
  • mouseenter
  • mouseleave
  • touchstart
  • touchend
  • touchmove

SpriteJS还可以代理其他事件,比如keyboard事件,我们可以为keyboard事件改写精灵的事件检测函数pointCollision

例子

const scene = new Scene('#event-delegate', {viewport: ['auto', 'auto'], resolution: [1540, 600]})
const layer = scene.layer()

class KeyButton extends Label {
  pointCollision(evt) {
    return evt.originalEvent.key === this.text
  }
}
KeyButton.defineAttributes({
  init(attr) {
    attr.setDefault({
      font: '42px Arial',
      border: {width: 4, color: 'black', style: 'solid'},
      width: 50,
      height: 50,
      anchor: [0.5, 0.5],
      textAlign: 'center',
      lineHeight: 50,
    })
  },
})

const keys = [
  'qwertyuiop',
  'asdfghjkl',
  'zxcvbnm',
]
for(let i = 0; i < 3; i++) {
  const keyButtons = [...keys[i]]
  for(let j = 0; j < keyButtons.length; j++) {
    const key = new KeyButton(keyButtons[j])
    key.attr({
      pos: [250 + j * 80, 200 + i * 100],
    })
    key.on('keydown', (evt) => {
      key.attr({
        bgcolor: 'grey',
        fillColor: 'white',
      })
    })
    key.on('keyup', (evt) => {
      key.attr({
        bgcolor: 'transparent',
        fillColor: 'black',
      })
    })
    layer.append(key)
  }
}

const label = new Label('轻敲键盘')
label.attr({
  anchor: [0.5, 0],
  pos: [770, 50],
  font: '42px Arial',
})
layer.append(label)

scene.delegateEvent('keydown', document)
scene.delegateEvent('keyup', document)

关于事件处理更多的内容,可以查看文档

SpriteJS和D3

由于SpriteJS的Api和DOM拥有高度一致性,因此它对D3友好,可以方便地使用D3+SpriteJS实现数据可视化展现。

用SpriteJS+D3实现柱状图

(function () {
  const paper = new spritejs.Scene('#paper', {
    viewport: ['auto', 'auto'],
    resolution: [1600, 1200],
    stickMode: 'width',
  })

  const dataset = [125, 121, 127, 193, 309]

  const linear = d3.scaleLinear()
    .domain([100, d3.max(dataset)])
    .range([0, 500])

  const colors = ['#fe645b', '#feb050', '#c2af87', '#81b848', '#55abf8']

  const s = d3.select(paper).append('fglayer')

  const chart = s.selectAll('sprite')
    .data(dataset)
    .enter()
    .append('sprite')
    .attr('x', 450)
    .attr('y', (d, i) => {
      return 200 + i * 95
    })
    .attr('width', 0)
    .attr('height', 80)
    .attr('bgcolor', '#ccc')

  chart.transition()
    .duration(2000)
    .attr('width', (d, i) => {
      return linear(d)
    })
    .attr('bgcolor', (d, i) => {
      return colors[i]
    })

  s.append('axis')
    .attr('ticks', [100, 200, 300, 400])
    .attr('axisScales', [linear])
    .attr('direction', 'bottom')
    .attr('pos', [450, 700])
    .attr('color', '#666')

  chart.on('click', (data) => {
    /* eslint-disable no-console */
    console.log(data, d3.event)
    /* eslint-enable no-console */
  })
}())

更多的例子见:SpriteJS+D3

物理引擎

SpriteJS的扩展让它能够支持matter-js

例子

// module aliases
const {Engine, World, Composites, Composite, Bodies} = Matter

// create an engine
const engine = Engine.create()
// engine.world.gravity.scale = 0; //turn off gravity (it's added back in later)

const stackA = Composites.stack(100, 100, 6, 6, 0, 0, (x, y) => {
  return Bodies.rectangle(x, y, 15, 15, {
    // friction: 0,
    // frictionAir: 0,
    // frictionStatic: 0,
    // restitution: 1
  })
})

const wall = Bodies.rectangle(400, 300, 500, 20, {
  isStatic: true,
})

World.add(engine.world, [stackA, wall])

const offset = 5
World.add(engine.world, [
  Bodies.rectangle(400, -offset, 800 + 2 * offset, 50, {
    isStatic: true,
  }),
  Bodies.rectangle(400, 600 + offset, 800 + 2 * offset, 50, {
    isStatic: true,
  }),
  Bodies.rectangle(800 + offset, 300, 50, 600 + 2 * offset, {
    isStatic: true,
  }),
  Bodies.rectangle(-offset, 300, 50, 600 + 2 * offset, {
    isStatic: true,
  }),
])

const scene = new Scene('#simple-demo', {viewport: ['auto', 'auto'], resolution: [800, 600]})
const fglayer = scene.layer('fglayer')

const blocks = []

function render() {
  Engine.update(engine, 16)
  const bodies = Composite.allBodies(engine.world)
  // console.log(bodies)
  for(let i = 0; i < bodies.length; i++) {
    const body = bodies[i],
      {position, angle} = body
    const pos = [
        Math.round(position.x * 10) / 10,
        Math.round(position.y * 10) / 10,
      ],
      rotate = Math.round(180 * angle * 10 / Math.PI) / 10

    let block = blocks[i]
    if(!block) {
      const {min, max} = body.bounds
      block = new Sprite()
      block.attr({
        anchor: 0.5,
        size: [max.x - min.x, max.y - min.y],
        pos,
        rotate,
        bgcolor: body.render.fillStyle,
      })
      blocks[i] = block
      fglayer.append(block)
    } else {
      block.attr({
        pos,
        rotate,
      })
    }
  }
  window.requestAnimationFrame(render)
}

render()

粒子系统

同样,通过扩展,SpriteJS支持Proton粒子

JSBIN

const {Scene, ProtonRenderer} = spritejs
const scene = new Scene('#container', {
  viewport: [600, 600],
  resolution: [600, 600],
})
const layer = scene.layer('fglayer')

const proton = new Proton()
const emitter = new Proton.Emitter()

// set Rate
emitter.rate = new Proton.Rate(Proton.getSpan(10, 20), 0.1)

// add Initialize
emitter.addInitialize(new Proton.Radius(1, 12))
emitter.addInitialize(new Proton.Life(2, 4))
emitter.addInitialize(new Proton.Velocity(3, Proton.getSpan(0, 360), 'polar'))

// add Behaviour
emitter.addBehaviour(new Proton.Color('#ff0000', 'random'))
emitter.addBehaviour(new Proton.Alpha(1, 0))

// set emitter position
emitter.p.x = layer.canvas.width / 2
emitter.p.y = layer.canvas.height / 2
emitter.emit(5)

// add emitter to the proton
proton.addEmitter(emitter)

// add canvas renderer
const renderer = new ProtonRenderer(layer)
proton.addRenderer(renderer)

// use Euler integration calculation is more accurate (default false)
Proton.USE_CLOCK = false
// proton.update()
function tick() {
  requestAnimationFrame(tick)
  proton.update()
}
tick()

更多粒子见SpriteJS+Proton

外部时钟

SpriteJS支持外部时钟,这使得它可以很容易与其他效果库一起使用,比如下面的例子演示了将SpriteJS与AlloyTeam的Curvejs一同使用:

例子

;(async function () {
  const birdsJsonUrl = 'https://s5.ssl.qhres.com/static/5f6911b7b91c88da.json'
  const birdsRes = 'https://p.ssl.qhimg.com/d/inn/c886d09f/birds.png'

  const scene = new Scene('#curvejs', {
    resolution: [1540, 600],
    viewport: 'auto',
  })
  const layer = scene.layer('fglayer', {
    autoRender: false,
  })
  await scene.preload([birdsRes, birdsJsonUrl])
  const s = new Sprite('bird1.png')

  s.attr({
    anchor: [0.5, 0.5],
    pos: [300, 100],
    transform: {
      scale: [0.5, 0.5],
    },
    offsetPath: 'M10,80 q100,120 120,20 q140,-50 160,0',
    zIndex: 200,
  })
  s.animate([
    {offsetDistance: 0},
    {offsetDistance: 1},
  ], {
    duration: 3000,
    direction: 'alternate',
    iterations: Infinity,
  })

  s.animate([
    {scale: [0.5, 0.5], offsetRotate: 'auto'},
    {scale: [0.5, -0.5], offsetRotate: 'reverse'},
    {scale: [0.5, 0.5], offsetRotate: 'auto'},
  ], {
    duration: 6000,
    iterations: Infinity,
    easing: 'step-end',
  })
  s.animate([
    {textures: 'bird1.png'},
    {textures: 'bird2.png'},
    {textures: 'bird3.png'},
  ], {
    duration: 300,
    direction: 'alternate',
    iterations: Infinity,
  })
  layer.appendChild(s)

  const util = {
    random(min, max) {
      return min + Math.floor(Math.random() * (max - min + 1))
    },
    randomColor() {
      return ['#22CAB3', '#90CABE', '#A6EFE8', '#C0E9ED', '#C0E9ED', '#DBD4B7', '#D4B879', '#ECCEB2', '#F2ADA6', '#FF7784'][util.random(0, 9)]
    },
  }

  const {Stage, Curve, motion} = curvejs

  const randomColor = util.randomColor,
    stage = new Stage(layer.canvas)

  stage.add(new Curve({
    points: [378, 123, 297, 97, 209, 174, 217, 258],
    color: randomColor(),
    motion: motion.rotate,
    data: Math.PI / 20,
  }))

  stage.add(new Curve({
    points: [378, 123, 385, 195, 293, 279, 217, 258],
    color: randomColor(),
    motion: motion.rotate,
    data: Math.PI / 20,
  }))

  function tick() {
    stage.update()
    layer.draw(false)
    requestAnimationFrame(tick)
  }

  tick()
}())

以上是SpriteJS的一些简单介绍。它还有许多神奇的功能,有兴趣的同学可以浏览SpriteJS的官方文档http://spritejs.org/

要深入了解SpriteJS或者希望给SpriteJS贡献代码,可以关注我们的GitHub仓库

1 回复

强👍

来自酷炫的 CNodeMD

回到顶部