自定义元素
实际上我们在前面一些例子里已经看到过,我们能够继承Sprite一系列类来扩展新的精灵类型。很多例子里我们创建了一些简单的精灵类型。现在我们尝试创建一类更复杂的UI元素。
我们可以很容易制作一组进度条UI组件,在这里我简单写了一个可以有三种展现类型的ProgressBar类(当然也可以将它拆分成3个不同的子类),可以看到通过spritejs实现UI组件是一件很容易的事情。
const scene = new Scene('#progressbar', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
const layer = scene.layer();
class ProgressBar extends Sprite {
get contentSize() {
let [width, height] = this.attr('size');
const {slotLength, slotWidth, type} = this.attr();
if(type === 'bar') {
if(width === '') {
width = slotLength;
}
if(height === '') {
height = slotWidth;
}
} else if(type === 'circle') {
if(width === '') {
width = Math.round(slotLength / Math.PI + slotWidth);
}
if(height === '') {
height = Math.round(slotLength / Math.PI + slotWidth);
}
} else if(type === 'hourglass') {
if(width === '') {
width = slotWidth;
}
if(height === '') {
height = slotLength;
}
}
return [width, height];
}
render(t, context) {
super.render(t, context);
const progress = this.attr('progress'),
slotColor = this.attr('slotColor'),
progressColor = this.attr('progressColor'),
type = this.attr('type');
const p = progress / 100;
const [width, height] = this.contentSize;
if(type === 'bar') {
context.beginPath();
context.rect(0, 0, width, height);
context.fillStyle = slotColor;
context.fill();
const progressRect = [0, 0, width * p, height];
context.beginPath();
context.rect(...progressRect);
context.fillStyle = progressColor;
context.fill();
} else if(type === 'circle') {
const slotWidth = this.attr('slotWidth');
const r = width / 2;
context.beginPath();
context.arc(r, r, r - slotWidth / 2, 0, 2 * Math.PI);
context.lineWidth = slotWidth;
context.strokeStyle = slotColor;
context.stroke();
context.beginPath();
context.arc(r, r, r - slotWidth / 2, -0.5 * Math.PI, (2 * p - 0.5) * Math.PI);
context.lineWidth = slotWidth;
context.strokeStyle = progressColor;
context.stroke();
} else if(type === 'hourglass') {
context.beginPath();
context.moveTo(width / 2, height / 2);
context.lineTo(0, 0);
context.lineTo(width, 0);
context.closePath();
context.fillStyle = slotColor;
context.fill();
context.beginPath();
context.moveTo(width / 2, height / 2);
context.lineTo(0, height);
context.lineTo(width, height);
context.closePath();
context.fillStyle = progressColor;
context.fill();
const dx = (1 - p ** 2) * width / 2,
dy = (1 - p ** 2) * height / 2;
context.beginPath();
context.moveTo(width / 2, height / 2);
context.lineTo(width / 2 - dx, height / 2 - dy);
context.lineTo(width / 2 + dx, height / 2 - dy);
context.closePath();
context.fillStyle = progressColor;
context.fill();
context.beginPath();
context.moveTo(width / 2, height / 2);
context.lineTo(width / 2 - dx, height / 2 + dy);
context.lineTo(width / 2 + dx, height / 2 + dy);
context.closePath();
context.fillStyle = slotColor;
context.fill();
}
}
}
ProgressBar.defineAttributes({
init(attr) {
attr.setDefault({
progress: 0,
slotColor: 'grey',
progressColor: 'green',
type: 'bar', // bar, circle, hourglass
slotLength: 200,
slotWidth: 25,
});
},
progress(attr, val) {
attr.clearCache();
attr.set('progress', val);
},
slotColor(attr, val) {
attr.clearCache();
attr.set('slotColor', parseColorString(val));
},
progressColor(attr, val) {
attr.clearCache();
attr.set('progressColor', parseColorString(val));
},
type(attr, val) {
attr.clearCache();
attr.set('type', val);
},
slotLength(attr, val) {
attr.clearCache();
attr.set('slotLength', val);
},
slotWidth(attr, val) {
attr.clearCache();
attr.set('slotWidth', val);
},
});
const p1 = new ProgressBar();
p1.attr({
anchor: [0.5, 0.5],
progress: 45,
pos: [250, 300],
slotLength: 500,
type: 'circle',
});
layer.append(p1);
p1.animate([
{progress: 0},
{progress: 100},
], {
duration: 10000,
iterations: Infinity,
easing: 'ease-in-out',
});
const label = new Label('0%');
label.attr({
anchor: [0.5, 0.5],
pos: [250, 300],
font: '36px Arial',
});
layer.append(label);
p1.on('update', (evt) => {
const progress = evt.target.attr('progress');
label.text = `${progress.toFixed(0)}%`;
});
const p2 = new ProgressBar();
p2.attr({
anchor: [0.5, 0.5],
progress: 0,
pos: [770, 300],
slotLength: 300,
slotWidth: 45,
progressColor: 'rgb(192,0,0)',
borderRadius: 20,
});
layer.append(p2);
Effects.progressColor = Effects.color;
Effects.slotColor = Effects.color;
p2.animate([
{progress: 0, progressColor: 'rgb(192,0,0)'},
{progress: 50, progressColor: 'rgb(192, 192, 0)'},
{progress: 100, progressColor: 'rgb(0, 192, 0)'},
], {
duration: 5000,
iterations: Infinity,
});
const p3 = new ProgressBar();
p3.attr({
anchor: [0.5, 0.5],
progress: 0.5,
pos: [1150, 300],
slotLength: 100,
slotWidth: 50,
progressColor: '#cc6',
type: 'hourglass',
});
layer.append(p3);
p3.attr({
progress: 0.5,
});
p3.animate([
{progress: 0},
{progress: 100},
], {
duration: 3000,
iterations: Infinity,
});
我们可以看到,要扩展Sprite类,只需要继承Sprite、Label、Path或Group这四个类,通过静态方法defineAttributes
定义一些新的属性。
Sprite.defineAttributes({
init(attr) {
// 这是构造函数,在这里可以通过 setDefault 给属性设置初始值
},
foo(attr, val) {
// 添加一个叫做 foo 的属性
// 如果不需要其他处理,只需要将它保存在 attribute 对象中即可
attr.set('foo', val)
attr.clearCache() // 如果这个属性需要清缓存,则调用 clearCache,什么属性需要清缓存,具体见“缓存策略”一节
}
})
扩展了属性,我们要实现继承类的一些方法。一般来说,我们只要重写get contentSize
和render
方法,前者负责在不给元素设置size的情况下计算元素的大小,后者负责元素具体的渲染过程。
class MyElement extends Sprite {
get contentSize() {
let [width, height] = this.attr('size')
if(width === '') {
// 当宽度为默认值时,处理
}
if(height === '') {
// 当宽度为默认值时,处理
}
return [width, height]
}
render(t, context) {
super.render(t, context)
// 处理具体绘制过程
}
}
这样我们就可以很方便地扩展我们需要的新元素了。
除了initAttributes方法外,如果我们通过babel和webpack编译,我们还可以使用decorate来定义属性,我们可以继承Sprite.Attr类。
定义属性特殊操作
我们定义元素属性的时候,需要理解和使用一些概念:
reflow: 当定义了一个新属性,该属性改变会引起元素的
contentSize
、clientSize
、offsetSize
等box大小变化的时候,由于spritejs默认缓存了这些属性计算值,因此需要显式调用attr.clearFlow()
来通知引擎清除这些元素缓存的值。cache: 当定义了一个新属性,该属性改变会引起元素的外观呈现发生变化(包括改变size、color、border、shape等)时,由于spritejs缓存策略可能会将元素缓存,因此需要显式调用
attr.clearCache()
来通知引擎清除缓存。在大部分情况下,元素的属性都引起外观改变(除了pos、transform、layout相关的这类只做位置变换的属性),因此通常情况下自定义的属性都要进行clearCache操作。quietSet: 如果定义一个属性,既不影响元素外观,又不影响box大小(如Path的bounding属性只影响事件的hit判断),那么可以使用
attr.quietSet()
来代替attr.set()
设置元素属性,这样元素的这个属性变化的时候,不会通知layer做update操作,能够减少消耗,提升性能。relayout: 当继承一个Group,定义的新属性如果是layout相关的属性,那么需要显式调用
attr.subject.relayout()
来通知元素清除layout,这样在绘制的时候才能重新计算layout。
插件封装
我们可以自定义spritejs的插件库,spritejs提供了use
操作,能够载入并初始化插件。
use(component, options, merge = true)
方法支持三个参数,返回插件对象。
- component 要使用的组件,该组件暴露一个
function
或者{install:function}
的接口。 - options 传给接口的配置。
- merge 默认值true,将该接口返回的内容merge到spritejs对象上。
import * as MyElement from 'my-element';
spritejs.use(MyElement);
const el = new spritejs.MyElement();
...
MyElement插件的定义
export function install({Sprite, registerNodeType}, options) {
class MyAttr extends Sprite.Attr {
constructor() {
this.setDefault({
// 设置默认属性
})
}
@attr
foo(val) {
this.set('foo', val)
this.clearCache()
}
}
class MyElement extends Sprite {
...
}
registerNodeType('myelement', MyElement); // 定义新的nodeType名为myelement
return {MyElement}
}
注意由于spritejs使用babel-transform-decorators-runtime
和babel-decorators-runtime
两个扩展来支持ES6的decorators特性,所以需要安装这两个依赖:
npm i -D babel-transform-decorators-runtime
npm i -S babel-decorators-runtime
对应的.babelrc推荐配置为:
{
"presets": ["env"],
"plugins": ["transform-runtime",
"transform-decorators-runtime",
"transform-class-properties"]
}
一个例子,通过继承Group类实现矢量图形的绘制。