無縫平移縮放的網格標註線

無意中看到miro的畫布有一個無縫滾動+縮放的功能:https://miro.com/app/board/uXjVMR5OCp8=/?share_link_id=751850163166

看到“無縫滾動”這個關鍵詞,當時一開始想着是不是用類似pixi.js裏類似TilingSprite的實現方式,把網格圖片平鋪開來。但是miro畫布縮放的時候依然能保持網格線清晰,於是使用renderTexture繪製網格,並在縮放過程中動態修改網格的尺寸,重新生成renderTexture給TilingSprite使用,雖然可以做到網格線清晰,但是renderTexture在繪製邊緣線後在TilingSprite平鋪開來時,會有線段閃爍的問題,具體原因不詳,於是試着老老實實用Graphics的方式來逐行逐列繪製網格線,並通過網格間距與位移模擬縮放。

首先,我們使用pixi實現《以鼠標爲錨點平滑縮放元素》這篇博客的縮放功能,具體原理參考該博客,完整代碼如下:

<!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>

接下來實現我們的網格背景:

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的網格尺寸,用於模擬縮放。

 完整代碼戳這裏

在線演示 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章