无意中看到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) 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, autoDensity: true, 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 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 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)
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 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的网格尺寸,用于模拟缩放。
最终效果如下:

完整代码戳这里
在线演示