使用spritejs操作canvas
大概是一个多月前,从掘金推荐上了解到spritejs这个canvas库,碰巧中秋活动需要做一个比较复杂的canvas游戏,因此决定在项目中尝试使用spritejs。
这篇文章并不是介绍spritejs相关api或者使用方法,而是用于记录使用经验,包括相关项目以及踩过的坑,除此之外还有一些在canvas项目中常见的问题。
使用spritejs实现的两个项目
- 中秋海报活动,这是一个通过用户选择素材,自定义祝福海报的中秋活动,由于是公司项目,因此不能开发相关源码
- 衬衫定制,这是一个在文化衫上定制不同图案的需求,目前还没有完工,后面应该会进行开源,链接我后面再补上了~
关于spritejs,参考:
几个概念
由于之前接触过cocos和phaser等游戏引擎,因此对于spritejs中场景、精灵等概念并不是十分陌生,但是spritejs和游戏引擎最大的区别在于:它十分轻量,接口也比较友好。
在spritejs中,我们需要理解的有下面几个概念。
精灵 Sprite
在canvas动画之绘制思路这篇博客中提到过,我们可以使用面向对象的思维来绘制图形。在spritejs中,精灵就是最基本的对象(从其命名就可见一斑)。
画布上每个独立的元素都可以看做是一个精灵,Sprite类提供了操作精灵相关的接口,
- attr,修改属性,如背景色、图片资源、边框、位置、尺寸等,跟
$.attr()
类似,这使得我们可以使用类似DOM的方法操作精灵 - animate,在原生的canvas中实现动画是比较麻烦的,spritejs封装了动画的接口,可以使用类似于css3动画属性控制精灵的动画效果
- on,spritejs代理了容器上的mouse和touch事件,我们可以直接给精灵注册相关事件处理函数,此外还支持自定义事件
- 可以使用内置
afterdraw
事件,控制绘制的图片
- 可以使用内置
借助这些接口,可以在Layer上快速绘制和修改精灵。
此外,也可以继承Sprite类,实现自定义精灵,在上面的项目中,通过继承Sprite,实现了可展示选择效果的SelectSprite
类,在点击时会出现高亮边框和缩放、删除等按钮。
分组 Group
如果是一组相关的精灵(比如一组具有相同动画的元素),可以将他们进行分组,方法是创建一个group对象,然后通过group.append(sp)
将元素添加到group里,参考文档。
group对象和精灵具有相同的操作接口,此外还可以通过clip
属性裁剪精灵。
层 Layer
一个layer
就是就是一个canvas画布,层是由场景进行管理的,可以调用scene.layer
实例化一个层,其实质就是根据配置参数,创建了一个canvas节点。
一个scene可以创建多个layer。不同的层属于不同的canvas,因此其画布状态不会相互干扰,可以完成各自特定的工作。下面是文化衫定制项目中的一段代码,其中
- scrawlLayer 用于实现涂鸦效果,涂鸦完成后会导出图片并绘制到baseLayer上
- baseLayer用于绘制基本的图片、文字等素材
- shadeLayer用于实现相框和遮罩效果,其层级最高,但不影响baseLayer的操作
this.scene = scene
const scrawlLayer = scene.layer('scrawl', {autoRender: false});
const baseLayer = scene.layer('base');
const shadeLayer = scene.layer('shade');
scrawlLayer.zIndex = 2
this.scrawlLayer = scrawlLayer
layer.zIndex = 3
this.layer = baseLayer
shadeLayer.zIndex = 4
this.shadeLayer = scrawlLayer
有了layer之后,可以通过layer.append(sp)
,将实例化的精灵添加到对应的layer上。精灵在层上的状态,由精灵本身的属性和状态控制,层并不需要处理这些细节。
如果需要操作原生的dom节点,可以通过
layer.canvas
获取到对应的canvas节点,比如为canvas加个不同的边框啥的layer.context
获取到绘制上下文,在某些需要直接使用上下文的地方(比如getImageData
)派得上用场
场景 Scene
场景用来管理多个层,实例化时需要指定一个dom容器,后续创建的layer,都将挂载到这个容器节点下。
关于场景,一个比较重要的地方就是分辨率的问题。
const scene = new Scene('#container', {
resolution: [600, 600],
viewport: [300, 300],
})
我们知道,canvas画布本身的大小和canvas在文档流中css样式的大小时可以不一致的(类似于img标签)在构造函数的配置参数中,
- resolution 用来指定画布的分辨率大小
- viewport 用来指定画布的css样式大小
如果resolution
和viewport
不是按照一定比例指定尺寸,则绘制的图形会发生变形。这跟spritejs没关系,是由canvas本身决定的。
在项目中用到了一个scene.snapshot
的方法,导出某个scene下所有layer的画布合成的图片,如果需要导出大分辨率且图案清晰的图片,则除了指定比较大的resolution
之外,还需要指定比较大的viewport
(导出的图片尺寸由viewport决定,而清晰度由resolution决定),这两个属性是可以在初始化之后继续修改的。
此外,spritejs实现了屏幕适配的功能,以及flex布局,接口和配置参数等于css十分相似。
遇见的问题
精灵texturesSize为0
有时候需要获取精灵的原始图片尺寸,使用sp.texturesSize
获取到的却是[0,0]
const robot = new Sprite('https://p5.ssl.qhimg.com/t01c33383c0e168c3c4.png');
robot.texturesSize // [0,0]
这是因为图片的加载都是异步的,在同步代码中无法直接获取图片的尺寸。解决办法是使用scene。proload
方法进行资源预加载。
下面是封装的一个预加载的方法,图片如果已经加载完毕,则可以使用texturesSize
获取到对应的尺寸
preloadTexture(src) {
let scene = this.preloadScene
let id = src // id直接使用src进行命名,多个scene之间可以共用图片缓存
return scene.preload({
id,
src,
}).then(() => {
return id
})
}
图片预加载的问题
在常规的前端项目中,我们也可以使用背景图、new Image
等方法进行图片预加载
function preload(list) {
let i = 0;
let load = () => {
if (!lisfut[i]) {
return
}
this.diy.preloadTexture(list[i]).then(() => {
i++
if (i < list.length) {
load()
}
})
}
load()
}
发现的问题是:貌似spritejs的预加载会不会使用图片缓存,需要重新请求一次图片源文件。因此
暂时的处理方式是只使用scene.proload
进行预加载,而不是自己实现的预加载。这个问题没有经过严格的验证~
源码打包
spritejs
使用的是typescript进行开发,因此使用npm安装进行打包时,需要注意相关语法 在通过继承实现新的精灵类型,会提示下面错误
Class constructor Sprite cannot be invoked without 'new'
但是看起来代码是正常的,查资料发现可能是文件路径的问题
但这个应该不是问题的关键,后面发现sprite源码时ts写的,通过npm安装引用的也是对应的源码。
临时的解决办法是引用浏览器版本的spritejs,解决这个问题。通过安装babel相关插件,应该也可以解决,暂时没有进一步研究。
截图导出
在实例化layer时,配置 autoRender
为false
后,导致scene无法导出,在导出前需要将其移除
// BUGFIX
const scrawlLayer = scene.layer('scrawl', {autoRender: false});
猜测其内部实现是需要等待所有canvas绘制完毕调用完毕
this.scene.removeLayer(this.scrawlLayer)
关于这个问题我去项目下提了一个issue,月影大神给出了回复
你配置了layer的autoRender是false的话,就要在snapshot之后自己手工调用一下layer.draw,因为snapshot需要等待layer绘制完成
分组点击事件定位不准
上面提到通过自定义精灵实现了一个SelectSprite
的类,在点击时会出现高亮边框和缩放、删除等按钮,这个功能最开始是通过分组来实现的,即将边框、删除、缩放按钮都通过精灵实现,然后与原本的精灵放在同一个分组下。
该方法在视觉效果上已达到了预期目标,在进行事件处理时发现分组的点击事件中,e.x
和e.y
的位置都有较大偏差,尚不清楚是框架问题还是我自身代码的bug....,后面使用的自定义精灵类实现,之前的代码找不到了~
canvas其他问题
图片跨域与nginx跨域配置
由于浏览器的同源策略限制,canvas的图片资源也存在跨域的问题。这个问题之前也提到过,我在项目中使用过两种处理办法
- 如果跨域图片的数量较少,且为静态图片,则可以使用内联base64文件进行处理
- 静态资源服务器配置CORS
其中第一种处理方法已在canvas动画之粒子效果这篇提到过,这里主要记录一下nginx配置cors的方法。
nginx配置cors
通过nginx的add_header
指令,可以添加cors响应头。
server {
listen 80;
listen 443;
ssl on;
ssl_certificate /usr/local/etc/nginx/config/server.crt;
ssl_certificate_key /usr/local/etc/nginx/config/server_nopwd.key;
root /Users/Txm/Desktop/test/app;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
}
上面是我在本地开发时进行的nginx静态资源服务器的配置,由于单条add_header Access-Control-Allow-Origin
只能配置*
或者单个域名,如果需要支持多个域名跨域,则可以使用map
结构指定,配置如下
# 静态资源
map $http_origin $corsHost {
default *;
"~http://g.shymean.com" http://g.shymean.com;
"~https://g.shymean.com" https://g.shymean.com;
}
server {
# 其他配置同上
add_header Access-Control-Allow-Origin $corsHost;
}
另外一种思路是通过location
指令进行过滤,只对指定来源的请求返回cors请求头,此时$add_header
设置为*
或者具体域名也就无所谓了。
关于nginx跨域配置,可以参考Nginx 允许多个域名跨域访问。
图片配置crossOrigin
配置了cors后,图片还需要进行设置crossOrigin
才行
let closeImg = new Image()
closeImg.crossOrigin = 'Anonymous' // 表示元素的跨域资源请求不需要凭证标志设置
closeImg.src = 'https://img.shymean.com/shirt/close.png'
实际上只要crossOrigin
的属性值不是use-credentials
,全部都会解析为anonymous
,包括空字符串,包括类似'xxx'
这样的字符。
参考: 解决canvas图片getImageData,toDataURL跨域问题
canvas 绘制自定义字体
在项目中遇见了一个需要绘制自定义字体的功能。解决办法是:通过css定义字体名
@font-face {
font-family: "_________"; //下划线填字体名称
src: url("_________"); //下划线填字体文件
}
然后在canvas中使用对应的字体名即可,需注意
- 必须再等到字体下载完成之后再去渲染canvas,字体才能有作用
- canvas中所引用的字体必须在文档流中有标签(span,p等)引用改字体(该问题待验证
参考:利用font-face定义的字体怎么在canvas里应用。
十六进制与rgb颜色转换
在项目中,由于接口传回的是十六进制的像素值,在画布上操作像素值时getImageData
获取到的是rgba形式的色值,有时候需要进行单位换算,下面是一个十六进制颜色转rgb的JavaScript实现。
function hex2Rgb(hexColor) {
var sColor = hexColor.toLowerCase();
if (sColor.length === 4) {
var sColorNew = "#";
for (let i = 1; i < 4; i += 1) {
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
}
sColor = sColorNew;
}
//处理六位的颜色值
var sColorChange = [];
for (let i = 1; i < 7; i += 2) {
sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2)));
}
return sColorChange
}
颜色替换
有一个需求是对指定区域内的图片颜色进行替换,实现类似于滤镜的效果。其大致思路是获取指定区域的ImageData,然后替换成指定的颜色。
这里的问题是区域内每个像素点的色值是不同的,不能简单地理解为把改区域所有的像素点都替换成统一的颜色,而是需要进行翻转
参考:試試看Canvas (2),調整Canvas圖片色調,其核心代码是
function draw2(imgObj,w,h){
ctx2.drawImage(imgObj,x,y);
var imgData = ctx2.getImageData(x, y, imgObj.width, imgObj.height);
var data = imgData.data;
for(var i = 0; i < data.length; i += 4) {
// 這邊以下將會套用新的RGB色彩
// red
data[i] = _colorR - (255 - data[i]);
// green
data[i + 1] = _colorG - (255 - data[i + 1]);
// blue
data[i + 2] = _colorB - (255 -data[i + 2]);
}
ctx2.putImageData(imgData,0,0)
}
小结
这篇博客有点标题党的嫌疑:虽然用spritejs作为标题,实际上却没有详细介绍其使用方法。
由于之前花了挺长一段时间学习canvas,更早之前还使用了cocos等引擎,因此在使用spritejs时,基本上就是对着文档找接口,与其通篇介绍API的使用,不如把自己遇见的问题记下来。
总之,spritejs比较轻量,接口也比较友好,用于实现一些小规模的项目还是不错的。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。