pixi如何实现绘画功能

背景

至于为什么会有这样的问题,起始于我一次业务开发需要实现画板功能,当做到橡皮擦功能的时候,遇到了瓶颈。原生canvas的2d环境,支持 globalCompositeOperation 属性。 这一属性决定了后续的动作是绘制(source-over)还是擦除(destination-out)。

而pixi默认是使用WebGL环境的,哪怕是用了canvas环境,也无法这样做,因为我的画板背后还有背景图案,当然还有一些其他的限制。例如PIXI.Graphics本身属于一个PIXI.Container,自身维护的数据比较多,占用内存较高。并且Container的特点是尺寸会随着内容增大,这也导致后期截取数据较为麻烦。

因此,我就在想,能不能有方法同时使用原生canvas的pixi。当然最终的解决方法也并非我另辟蹊径,只是通过反复查看官方的api文档发现了之前遗漏的功能而已。

理解PIXI.Texture.from

WX20210621-093401@2x

其实仔细看第一个参数source的Type可以发现,source可以是字符串、img标签、canvas标签、video标签,以及PIXI.BaseTexture。而我们参照一般文档进行开发,使用最多的其实是PIXI.BaseTexture。因为我们通过Loader加载的图片,默认都创建成了BaseTexture,我们new PIXI.Sprite(texture)的时候,texture就是它。而string类型,我想new PIXI.Text(str)的时候就是了。这里既然提到了canvas,那我们是不是可以尝试用一个canvas标签做真正的绘图,然后再通过pixi的WebGL环境实时展示?

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>画图</title>
<script src="https://cdn.bootcdn.net/ajax/libs/pixi.js/5.3.10/pixi.min.js"></script>
</head>

<body>
<div id="box"></div>
<button id="add">add</button>
<button id="del">del</button>
<script>
// 创建一个pixi舞台
let app = new PIXI.Application({ width: 400, height: 300, backgroundColor: 0xcccccc });
document.getElementById('box').appendChild(app.view);

// 创建绘制画布
let canvas = document.createElement('canvas')
canvas.width = app.renderer.width
canvas.height = app.renderer.height
canvas.style.background = '#aaaaaa'
let ctx = canvas.getContext('2d')
document.getElementById('box').appendChild(canvas);

// 创建交互展示画布
let tx = PIXI.Texture.from(canvas) // 重点在于这里,将原生canvas作为材质使用,每次绘画都要实时调用 tx.update()方法进行更新
let sp = new PIXI.Sprite(tx)
sp.name = 'preview'
sp.width = app.renderer.width
sp.height = app.renderer.height
sp.hitArea = new PIXI.Rectangle(0, 0, app.renderer.width, app.renderer.height)
sp.zIndex = 2
sp.interactive = true
app.stage.addChild(sp);

// 绘画模式
let mode = 'add'
// 初始坐标
let lastPoint = { x: 0, y: 0 };
// 是否按下去
let drawing = false;

document.getElementById('add').onclick = () => mode = 'add'
document.getElementById('del').onclick = () => mode = 'del'

// 交互行为
sp.on('mousedown', drawStart);
sp.on('mouseup', drawEnd);
sp.on('mouseout', drawEnd);
sp.on('mousemove', drawMove);

function drawStart(event) {
let
r = Math.random() * 1,
g = Math.random() * 1,
b = Math.random() * 1,
a = Math.random() * 1

if (mode === 'add') {
ctx.lineWidth = 7;
ctx.globalCompositeOperation = "source-over"
} else {
ctx.lineWidth = 21;
ctx.globalCompositeOperation = "destination-out"
}

ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = `rgba(${r * 255},${g * 255},${b * 255},1)`

// let { x, y } = event.data.getLocalPosition(this.parent); //获取鼠标移动的位置
let { x, y } = event.global; //获取鼠标移动的位置

lastPoint = { x, y };
drawing = true;
}
function drawMove(event) {
if (drawing == true) {

// let { x, y } = event.data.getLocalPosition(this.parent); //获取鼠标移动的位置
let { x, y } = event.global; //获取鼠标移动的位置

ctx.beginPath();
ctx.moveTo(lastPoint.x, lastPoint.y);
ctx.lineTo(x, y);
ctx.stroke()
tx.update()

// 更新坐标点
lastPoint = { x, y };
}
}

function drawEnd() {
drawing = false;
}
</script>
</body>

</html>
</script>
</body>

</html>

DEMO

两个canvas,前一个是pixi的舞台,使用WebGL。后一个是原生canvas,使用2d环境绘制。

前一个接收交互事件,计算绘图的坐标。
然后交给canvas的2d环境,使用原生api进行绘图
最后update,前者就会更新显示

更新记录

2023-02-13
新版本api更改,pixijs的sprite对象交互事件里不再有getLocalPosition方法,而是在global里即可获取实时位置

1
2
// let { x, y } = event.data.getLocalPosition(this.parent); //获取鼠标移动的位置
let { x, y } = event.global; //获取鼠标移动的位置
  1. 背景
  2. 理解PIXI.Texture.from
  3. 代码示例
  4. DEMO
  5. 更新记录