Recently, I am making a photo wall web page to show my pixel painting of tomorrow’s ark. I hope this web page can drag and pan with the mouse and zoom with the scroll wheel at the current position of the mouse.

I searched the Internet and found that there are many implementations based on Vue and React, but since this page is so simple, I decided to use the native implementation.

The result: columns-wings.oss-cn-hangzhou.aliyuncs.com/illusion/

encapsulation

For reuse, I wrapped it into a class that exposes two members, a container element for mounting to the document and a content element for adding content. Base members include:

  • $container: Container element
  • $Content: The content element
  • X: abscissa
  • Y: ordinate
  • S: Scale

Bind mousemove and mousewheel events (browser compatibility aside) to the container and calculate them in the event handler.

translation

Translation can be added to the element’s shift by reading movementX and movementY in the mouse move event using the CSS’s Transform: translate property:

/ * * *@name Handle mouse drag *@param {Object} Ev event object */
handle_move(ev) {
  if (ev.buttons === 1) { // Determine whether the left mouse button is pressed
    this.x += (ev.movementX / this.s)
    this.y += (ev.movementY / this.s)

    this.translate()
  }
}
/ * * *@name Pan * /
translate() {
  this.$content.style.transform = `translate(The ${this.x}px, The ${this.y}px)`
}
Copy the code

The zoom

Scaling can be set using the CSS transform: scale property:

/ * * *@name Handle mouse wheel *@param {Object} Ev event object */
handle_wheel(ev) {
    let delta = -(ev.deltaY / 2000)
    this.s *= 1 + delta

    this.scale()
}
/ * * *@name Zoom * /
scale() {
  this.$content.style.transform = `scale(The ${this.s}) `
}
Copy the code

here

Zoom center

The zoom center can be set using transform-Origin, and the original idea was to set this property while scrolling the scroll wheel. However, transform-Origin will affect the transform, and the setting will cause the mutation of the element position.

One way to do this is to analyze the calculations inside the Transform and transform-Origin, and then externally give the transform: translate compensation, that is, to fix as much as it mutates. But because of its internal calculation method is more complex, thinking and trying for a long time has not succeeded. So I decided to compute the transformation matrix myself and set the transform-Origin to 0, the upper left corner of the content element.

Also, to simplify logic and computation, use two intermediate containers to wrap the content elements, one for translation and one for scaling. So the two transformations are in different coordinate Spaces, so they don’t interact. The two elements are defined here as members $translate and $scale.

Matrix transformation

Here, row vectors are used to represent the position of content elements, because translation requires a third-order matrix, so the third of the vector is set to 1, which is actually meaningless.

If the scaling is centered on the origin, then we only need to multiply the horizontal and vertical coordinates by the scaling coefficient, and the matrix is expressed as:

Where s is the scaling factor.

If you scale at any point, you can directly use the corresponding matrix formula, which requires some calculation, or you can do a translation that translates the elements to the origin, scales around the origin, and then translates the same magnitude but in the opposite direction by an offset. The offset vector is the vector of the current element position and is placed in the translation matrix:

Where ox and OY are scaling center coordinates.

So once you have your transformation matrix, you multiply your position vector by all three of these, and you get your transformed position vector.

/ * * *@name Scale the origin *@param {Number} Delta Change in scaling factor *@param {Number} Ox Scale center abscissa *@param {Number} Oy Ordinate of scaling center */
origin(delta, ox, oy) {
  let v = new Matrix(1.3The [[this.x, this.y, 1]])
  let tf = new Matrix(3.3The [[1.0.0],
    [0.1.0],
    [-ox, -oy, 1]])let sc = new Matrix(3.3The [[1 + delta, 0.0],
    [0.1 + delta, 0],
    [0.0.1]])let tb = new Matrix(3.3The [[1.0.0],
    [0.1.0],
    [ox, oy, 1]])let r = v.multiplyD(tf).multiplyD(sc).multiplyD(tb)

  this.x = r[0] [0]
  this.y = r[0] [1]
  this.translate()
}
Copy the code

Matrix is a Matrix class, which only needs to implement the dot product method. See the code for details.

It is important to note here that the transform: translate setting uses the absolute value, but the scaling factor in the matrix transform is a relative quantity, which is calculated and handled differently.

The effect is as follows:

code

/ * * *@name Matrix * /
class Matrix {
  / * * *@name Construction method *@description The row vectors represent. row * column *@param {Number} The row number of rows *@param {Number} The column number of columns@param {Array} The value value * /
  constructor(row, column, value) {
    this.r = row
    this.c = column

    for (let i = 0; i < row; i++) {
      this[i] = []
    }

    if (value) {
      for (let i = 0; i < this.r; i++) {
        for (let j = 0; j < this.c; j++) {
          this[i][j] = value[i][j] ?? this[i][j]
        }
      }
    }
  }

  / * * *@name Minus dot *@param Other matrix *@return Results the * /
  multiplyD(other) {
    let result = new Matrix(this.r, other.c)
    let n = this.c
    for (let i = 0; i < result.r; i++) {
      for (let j = 0; j < result.c; j++) {
        let value = 0
        for (let k = 0; k < n; k++) {
          value += this[i][k] * other[k][j]
        }
        result[i][j] = value
      }
    }

    return result
  }
}

/ * * *@name Generates a movable, scaled element */
class Atlas {
  / * * *@name Construction method *@param {String} Width the width. CSS *@param {String} Height height. CSS *@param {Boolean} Translate can move *@param {Boolean} Scale Scales */
  constructor({ width, height, translate = true, scale = true, translateSpeed = 2, scaleSpeed = 1 } = {}) {
    this.$container = null
    this.$content = null

    this.config = {
      translate: true.scale: true.translateSpeed: 2.scaleSpeed: 1
    }
    this.x = 0
    this.y = 0
    this.s = 1
    this.$translate = null
    this.$scale = null
    this.moveDelta = 0

    let $container = document.createElement('div')
    $container.style.overflow = 'hidden'
    $container.style.position = 'relative'
    $container.style.width = width
    $container.style.height = height
    $container.addEventListener('mousemove'.this.handle_move.bind(this))
    $container.addEventListener('click'.this.handle_click.bind(this), true)
    $container.addEventListener('mousewheel'.this.handle_wheel.bind(this))

    let $translate = document.createElement('div')
    $translate.style.transformOrigin = '0 0'

    let $scale = document.createElement('div')
    $scale.style.transformOrigin = '0 0'

    let $content = document.createElement('div')
    $content.style.width = 'max-content'
    $content.style.height = 'max-content'

    $container.appendChild($translate)
    $translate.appendChild($scale)
    $scale.appendChild($content)

    this.$container = $container
    this.$translate = $translate
    this.$scale = $scale
    this.$content = $content
    this.config.translate = translate
    this.config.scale = scale
    this.config.translateSpeed = translateSpeed
    this.config.scaleSpeed = scaleSpeed
  }

  / * * *@name Mobile *@param {Number} The absolute value of ax on the abscissa@param {Number} Ay is the absolute y-coordinate */
  translateTo(ax, ay) {
    this.x = ax ?? this.x
    this.y = ay ?? this.y

    this.translate()
  }
  / * * *@name Mobile *@param {Number} Dx x offset *@param {Number} The y-coordinate offset of dy */
  translateBy(dx, dy) {
    this.x += dx ?? 0
    this.y += dy ?? 0

    this.translate()
  }
  / * * *@name Zoom *@param {Number} The absolute value of as coefficient */
  scaleTo(as) {
    this.s = as ?? this.s

    this.scale()
  }
  / * * *@name Zoom *@param {Number} Ds coefficient offset */
  scaleTo(ds) {
    this.s += ds ?? 0

    this.scale()
  }

  / * * *@name Handle mouse drag *@param {Object} Ev event object */
  handle_move(ev) {
    if (this.config.translate) {
      if (ev.buttons === 1) {
        this.x += (ev.movementX / this.s) * this.config.translateSpeed
        this.y += (ev.movementY / this.s) * this.config.translateSpeed

        this.moveDelta += Math.abs(ev.movementX + ev.movementY)

        this.translate()
      }
    }
  }
  / * * *@name Handle mouse lift *@description Block drag when clicking *@param {Object} Ev event object */
  handle_click(ev) {
    if (this.moveDelta > 10) {
      ev.preventDefault()
      ev.stopPropagation()
    }

    this.moveDelta = 0
  }
  / * * *@name Handle mouse wheel *@param {Object} Ev event object */
  handle_wheel(ev) {
    if (this.config.scale) {
      let delta = -(ev.deltaY / 2000) * this.config.scaleSpeed

      this.s *= 1 + delta

      this.origin(delta, ev.clientX, ev.clientY)
      this.scale()
    }
  }

  / * * *@name Pan * /
  translate() {
    this.$translate.style.transform = `translate(The ${this.x}px, The ${this.y}px)`
  }
  / * * *@name Scale the origin *@param {Number} Delta Change in scaling factor *@param {Number} Ox Scale center abscissa *@param {Number} Oy Ordinate of scaling center */
  origin(delta, ox, oy) {
    let v = new Matrix(1.3The [[this.x, this.y, 1]])
    let tf = new Matrix(3.3The [[1.0.0],
      [0.1.0],
      [-ox, -oy, 1]])let sc = new Matrix(3.3The [[1 + delta, 0.0],
      [0.1 + delta, 0],
      [0.0.1]])let tb = new Matrix(3.3The [[1.0.0],
      [0.1.0],
      [ox, oy, 1]])let r = v.multiplyD(tf).multiplyD(sc).multiplyD(tb)

    this.x = r[0] [0]
    this.y = r[0] [1]
    this.translate()
  }
  / * * *@name Zoom * /
  scale() {
    this.$scale.style.transform = `scale(The ${this.s}) `}}export default Atlas
Copy the code