无意间了解到 konva.js 库,konva 一个 2d canvas 库,作者基于该库创建一个了付费 Polotno SDK,总结了其多年工作经验,用于快速实现在线设计站点。了解它相关 api 后,有些设计思路值得借鉴。
基础
相关 api 设计
- stage.getPointerPosition() 得到当前光标所在坐标
- layer.getIntersection(pos) 得到点相交的图形
- shape.hitFunc 用于自定义命中区域,常用于增加点击区域或自定义区域,以及性能优化
相关类
- 提供 Transformer 类用于实现 scale 和 rotate 的调整
- enabledAnchors 数组,用于激活哪些锚点
- rotationSnaps 数组,用户设置捕捉那些角度
- forceUpdate
- 提供 Tween 类用于实现对对象过渡动画
- play
- 提供 Animation 类实现帧动画
- frame callback
- start/stop
drop 实现逻辑
- previousShape 存储之前 shape
- 移动过程中,当新的 shape 和 previousShape 不同时,previousShape 派发 dragleave 事件,shape 派发 dragenter 事件
- 值得借鉴的点:并不是在事件过程中去改变物体的样式,而只是手动派发事件,样式修改放在事件中去处理
性能
性能优化建议
- 优化目标
- 计算尽可能少
- 绘制尽可能少
- 相关手段
- 优化舞台大小
- 移动端设置 viewport,因为缩放非常影响性能
- 设置 pixelRatio 为 1,即使在 retina 屏
- 通过 layer 机制,将对象动静分离
- 如果 layer 中的对象不需要响应时,则设置 listening(false)
- 优化移动开销,拖动时将形状移动到专用层,完成时再移回原来的层
- 使用 shape.cache(),将对象转换为图像,通过绘制图像的方式避免了根据其绘图指令组合形状的开销,对于复杂对象或组时尤为有效
- 保持对象的顺序,通过设置 shape 的 visible 或 opacity 替换 hide 或 remove 方法
- 设置 shape.listening(false)
- 关闭 autoDrawEnabled,手动调用 batchDraw
PixiJS 默认使用 WebGL 进行渲染(如果不支持则降级使用 Canvas),在复杂动画方面性能更好
其他
关于撤销重做 story.history
- canUndo
- undo()
- canRedo
- redo()
替换内置 API 为自己 api:setAPI(key, func)
lock 机制的定义:关闭 draggable、contentEditable、styleEditable
关于定时保存,设置一个全局 timer 配合就节流逻辑
// write a function for throttle saving
let timeout = null;
const requestSave = () => {
// if save is already requested - do nothing
if (timeout) {
return;
}
// schedule saving to the backend
timeout = setTimeout(() => {
// reset timeout
timeout = null;
// export the design
const json = store.toJSON();
// save it to the backend
fetch('https://example.com/designs', {
method: 'POST',
body: JSON.stringify(json),
});
}, 1000);
};
// request saving operation on any changes
store.on('change', () => {
requestSave();
});
关于字体加载:https://fonts.googleapis.com/css?family=Kavivanar
实现小预览窗口
- 方式一:克隆后作为图片使用
- 方式二:从最新状态生成
场景设置浮层元素
- 组件支持设置 position 绝对定位
- 场景计算当前需要显示的屏幕坐标
- 使用原生 html 加载右键菜单
通过自定义锚点更新图形
function buildAnchor(x, y) {
var anchor = new Konva.Circle({
x: x,
y: y,
radius: 20,
stroke: '#666',
fill: '#ddd',
strokeWidth: 2,
draggable: true, // 复用物体移动能力
});
layer.add(anchor);
// add hover styling
anchor.on('mouseover', function () {
document.body.style.cursor = 'pointer';
this.strokeWidth(4);
});
anchor.on('mouseout', function () {
document.body.style.cursor = 'default';
this.strokeWidth(2);
});
anchor.on('dragmove', function () {
updateDottedLines();
});
return anchor;
}
// function to update line points from anchors
function updateDottedLines() {
var q = quad;
var b = bezier;
var quadLinePath = layer.findOne('#quadLinePath');
// 更新路径
quadLinePath.points([
q.start.x(),
q.start.y(),
q.control.x(),
q.control.y(),
q.end.x(),
q.end.y(),
]);
}
var quadraticLine = new Konva.Shape({
stroke: 'red',
strokeWidth: 4,
// 更新修改图形
sceneFunc: (ctx, shape) => {
ctx.beginPath();
ctx.moveTo(quad.start.x(), quad.start.y());
ctx.quadraticCurveTo(
quad.control.x(),
quad.control.y(),
quad.end.x(),
quad.end.y()
);
ctx.fillStrokeShape(shape);
},
});
var quadLinePath = new Konva.Line({
dash: [10, 10, 0, 10],
strokeWidth: 3,
stroke: 'black',
lineCap: 'round',
id: 'quadLinePath',
opacity: 0.3,
points: [0, 0],
});
// special objects to save references to anchors
var quad = {
start: buildAnchor(60, 30),
control: buildAnchor(240, 110),
end: buildAnchor(80, 160),
};