Bootstrap
博客内容搜索
Kaysama's Blog

无意中看到miro的画布有一个无缝滚动+缩放的功能:https://miro.com/app/board/uXjVMR5OCp8=/?share_link_id=751850163166

看到“无缝滚动”这个关键词,当时一开始想着是不是用类似pixi.js里类似TilingSprite的实现方式,把网格图片平铺开来。但是miro画布缩放的时候依然能保持网格线清晰,于是使用renderTexture绘制网格,并在缩放过程中动态修改网格的尺寸,重新生成renderTexture给TilingSprite使用。
虽然可以做到网格线清晰,但是renderTexture在绘制边缘线后在TilingSprite平铺开来时,会有线段闪烁的问题,具体原因不详,于是试着老老实实用Graphics的方式来逐行逐列绘制网格线,并通过网格间距与位移模拟缩放。

首先,我们使用pixi实现《以鼠标为锚点平滑缩放元素》这篇博客的缩放功能,具体原理参考该博客,完整代码如下:

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>缩放</title>
<style>
html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}

body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}

#canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="./lib/pixi_v6.2.2.min.js"></script>
<script src="./lib/stats.min.js"></script>
<script>
(function () {
window.PIXI = PIXI
PIXI.utils.skipHello()
const stats = new Stats()
stats.showPanel(0) // 0: fps, 1: ms, 2: mb, 3+: custom
stats.dom.style.transformOrigin = '0 0'
stats.dom.style.transform = 'translate(0,100px) scale(1.2)'
document.body.appendChild(stats.dom)

let pageWidth = 0
let pageHeight = 0
let canvasWidth = 0
let canvasHeight = 0
let renderer = null

function onResize (e) {
pageWidth = window.innerWidth
pageHeight = window.innerHeight
canvasWidth = pageWidth
canvasHeight = pageHeight
renderer && renderer.resize(canvasWidth, canvasHeight)
}

onResize()

const $canvas = document.querySelector('#canvas')
renderer = new PIXI.Renderer({
view: $canvas,
width: canvasWidth,
height: canvasHeight,
resolution: window.devicePixelRatio, // 放大 devicePixelRatio 倍
autoDensity: true, // 缩小 devicePixelRatio 倍
backgroundAlpha: 1,
backgroundColor: 0xeef2f8,
antialias: true
})
const stage = new PIXI.Container()
stage.name = 'stage'

const ticker = new PIXI.Ticker()

let mouse = { x: 0, y: 0 }
let moving = false
let mouseDown = false
let scaling = false
let lastScale = 1
let scale = 1
let translateX = 0
let translateY = 0

let targetScale = 1
let targetTranslateX = 0
let targetTranslateY = 0

class Gay extends PIXI.Sprite {
constructor () {
super()
this.name = 'Gay'
this.texture = PIXI.Texture.from('./gay.jpg')
this.anchor.set(0.5)
this._startX = canvasWidth * 0.5
this._startY = canvasHeight * 0.5
this.dirty = false
this.position.set(this._startX, this._startY)
}

setTranslate () {
this.position.set(this._startX + translateX, this._startY + translateY)
}

setScale () {
this.scale.set(scale, scale)
this.position.x = mouse.x + (this.position.x - mouse.x) / lastScale * scale
this.position.y = mouse.y + (this.position.y - mouse.y) / lastScale * scale
this._startX = this.position.x - translateX
this._startY = this.position.y - translateY
}
}

const gay = new Gay()
stage.addChild(gay)

stage.interactive = true

let startPos = { x: 0, y: 0 }
let startMouse = { x: 0, y: 0 }
$canvas.addEventListener('mousedown', function (e) {
mouse = {
x: e.pageX,
y: e.pageY
}
mouseDown = true
moving = true
// 终止缩放
scaling = false
targetScale = scale

targetTranslateX = translateX
targetTranslateY = translateY
startMouse = {
x: mouse.x,
y: mouse.y
}
startPos = {
x: translateX,
y: translateY
}
})
$canvas.addEventListener('mousemove', function (e) {
if (mouseDown) {
mouse = {
x: e.pageX,
y: e.pageY
}
targetTranslateX = startPos.x + (mouse.x - startMouse.x)
targetTranslateY = startPos.y + (mouse.y - startMouse.y)
}
})
$canvas.addEventListener('mouseup', function (e) {
mouseDown = false
})
$canvas.onwheel = function (e) {
mouse = {
x: e.pageX,
y: e.pageY
}
scaling = true
// 终止移动
moving = false
targetTranslateX = translateX
targetTranslateY = translateY

const delta = e.wheelDelta ? e.wheelDelta : -e.deltaY
targetScale = delta > 0 ? scale * 1.4 : scale / 1.4
}

const loop = () => {
stats.begin()
if (moving) {
const deltaX = (targetTranslateX - translateX) * 0.1
const deltaY = (targetTranslateY - translateY) * 0.1
// 差值小于0.001并且鼠标抬起,标为移动结束
if (Math.abs(deltaX) <= 0.001 && Math.abs(deltaY) <= 0.001 && !mouseDown) {
moving = false
}
else {
translateX += deltaX
translateY += deltaY
}
gay.setTranslate()
}
if (scaling) {
lastScale = scale
let deltaScale = (targetScale - scale) * 0.1
// 差值小于0.001,标为缩放结束
if (Math.abs(deltaScale) <= 0.001) {
scaling = false
}
else {
scale += deltaScale
}
gay.setScale()
}
renderer.render(stage)
stats.end()
}

ticker.add(loop)
ticker.start()

renderer.render(stage)

// 供pixi的inspector使用
globalThis.__PIXI_STAGE__ = stage
globalThis.__PIXI_RENDERER__ = renderer

window.onresize = onResize
})()
</script>
</body>
</html>

接下来实现我们的网格背景:

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
class GridBackground extends PIXI.Graphics {
constructor () {
super()
this.name = 'GridBackground'
this.gridSize = 50
this._startX = 0
this._startY = 0
this.translateX = 0
this.translateY = 0
const gridVector = new PIXI.DisplayObject()
gridVector.name = 'gridVector'
this.gridVector = gridVector
this.setTranslate()
}

setVectorTranslate () {
this.gridVector.position.set(this._startX + translateX, this._startY + translateY)
}

setTranslate () {
this.setVectorTranslate()
const gridSize = this.gridSize
this.clear()
this.lineStyle(1, 0xdee0e3)
const gridVector = this.gridVector
const newTranslateX = gridVector.position.x % gridSize
const newTranslateY = gridVector.position.y % gridSize
// (i-1) 是为了多画一条线
// 竖向线
for (let i = 0; (i - 1) * gridSize <= canvasWidth; i++) {
this.moveTo(newTranslateX + i * gridSize, 0)
this.lineTo(newTranslateX + i * gridSize, canvasHeight)
}
// 横向线
for (let i = 0; (i - 1) * gridSize <= canvasHeight; i++) {
this.moveTo(0, newTranslateY + i * gridSize)
this.lineTo(canvasWidth, newTranslateY + i * gridSize)
}
}

setVectorScale () {
const gridVector = this.gridVector
gridVector.scale.set(scale, scale)
gridVector.position.x = mouse.x + (gridVector.position.x - mouse.x) / lastScale * scale
gridVector.position.y = mouse.y + (gridVector.position.y - mouse.y) / lastScale * scale
this._startX = gridVector.position.x - translateX
this._startY = gridVector.position.y - translateY
}

setScale () {
this.setVectorScale()
const gridVector = this.gridVector
this.gridSize = 50 * scale
this.setTranslate()
}
}

const grid = new GridBackground()
stage.addChild(grid)

这个类与Gay类最大的区别是增加了一个gridVector属性,用PIXI.DisplayObject来实例化,但是不添加到stage中,对gridVector的操作与gay一样,仅仅用于计算网格的缩放与偏移量。

当gridVector移动完以后,把其position值应用给grid的绘制偏移量。

其中 const newTranslateX = gridVector.position.x % gridSize 这个方法是为了确保绘制的偏移量始终保持在gridSize内,因为偏移 gridSize 的倍数距离在视觉上看起来就是没变化。

当gridVector缩放完以后,把其position值应用给grid的绘制偏移量,缩放倍数应用给grid的网格尺寸,用于模拟缩放。

最终效果如下:

完整代码戳这里

在线演示