无缝平移缩放的网格标注线

无意中看到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的网格尺寸,用于模拟缩放。

 完整代码戳这里

在线演示 

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