Yang03 / blog

0 stars 0 forks source link

pinch-zoom #25

Open Yang03 opened 4 years ago

Yang03 commented 4 years ago
/* https://github.com/GoogleChromeLabs/pinch-zoom */
import React, { Component } from 'react';
import PointerTracker, { Pointer } from 'pointer-tracker';
import { Point } from './PropsType';

let cachedSvg: SVGSVGElement;

function getSVG(): SVGSVGElement {
  if (cachedSvg) {
    return cachedSvg;
  }
  cachedSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  return cachedSvg;
}

function createMatrix(): SVGMatrix {
  return getSVG().createSVGMatrix();
}

function createPoint(): SVGPoint {
  return getSVG().createSVGPoint();
}

function getDistance(a: Point, b?: Point): number {
  if (!b) return 0;
  return Math.sqrt((b.clientX - a.clientX) ** 2 + (b.clientY - a.clientY) ** 2);
}

function getMidpoint(a: Point, b?: Point): Point {
  if (!b) return a;

  return {
    clientX: (a.clientX + b.clientX) / 2,
    clientY: (a.clientY + b.clientY) / 2,
  };
}

export interface PinchZoomProps {
  prefixCls?: string;
  className?: string;
  onChange?: Function;
}

export default class PinchZoom extends Component<PinchZoomProps, any> {
  private _container;
  private _transform: SVGMatrix = createMatrix();
  private _positioningEl?: Element;

  constructor(props) {
    super(props);
    this._container = React.createRef();
  }

  get x() {
    return this._transform.e;
  }

  get scale() {
    return this._transform.a;
  }

  get y() {
    return this._transform.f;
  }

  componentDidMount() {
    this._positioningEl = this._container.current.children[0];
    const pointerTracker: PointerTracker = new PointerTracker(this._container.current, {
      start: (_pointer, event) => {
        // We only want to track 2 pointers at most
        if (pointerTracker.currentPointers.length === 2 || !this._positioningEl) return false;
        event.preventDefault();
        return true;
      },
      move: (previousPointers, _changePointers, event) => {
        // event.stopPropagation();
        if (this.scale === 1) return
        this._onPointerMove(previousPointers, pointerTracker.currentPointers);
        // return fasle
      },
      // end: ()
    });
    this._container.current.addEventListener('wheel', (event) => this._onWheel(event));
  }
  _onWheel = ( event ) => {
    event.preventDefault();

    const currentRect = this._positioningEl!.getBoundingClientRect();
    let { deltaY } = event;
    const { ctrlKey, deltaMode } = event;

    if (deltaMode === 1) { // 1 is "lines", 0 is "pixels"
      // Firefox uses "lines" for some types of mouse
      deltaY *= 15;
    }

    // ctrlKey is true when pinch-zooming on a trackpad.
    const divisor = ctrlKey ? 100 : 300;
    const scaleDiff = 1 - deltaY / divisor;
    this.applyChange({
      scaleDiff,
      originX: event.clientX - currentRect.left,
      originY: event.clientY - currentRect.top,
      allowChangeEvent: true,
    });
  };

  private _onPointerMove(previousPointers: Pointer[], currentPointers: Pointer[]) {
    if (!this._positioningEl) return;

    // Combine next points with previous points
    const currentRect = this._positioningEl.getBoundingClientRect();

    // For calculating panning movement
    const prevMidpoint = getMidpoint(previousPointers[0], previousPointers[1]);
    const newMidpoint = getMidpoint(currentPointers[0], currentPointers[1]);

    // Midpoint within the element
    const originX = prevMidpoint.clientX - currentRect.left;
    const originY = prevMidpoint.clientY - currentRect.top;

    // Calculate the desired change in scale
    const prevDistance = getDistance(previousPointers[0], previousPointers[1]);
    const newDistance = getDistance(currentPointers[0], currentPointers[1]);
    const scaleDiff = prevDistance ? newDistance / prevDistance : 1;

    this.applyChange({
      originX, originY, scaleDiff,
      panX: newMidpoint.clientX - prevMidpoint.clientX,
      panY: newMidpoint.clientY - prevMidpoint.clientY,
    });
  }

  applyChange = (opts) => {
    const {
      panX = 0, panY = 0,
      originX = 0, originY = 0,
      scaleDiff = 1,
    } = opts;

    const matrix = createMatrix()
      // Translate according to panning.
      .translate(panX, panY)
      // Scale about the origin.
      .translate(originX, originY)
      // Apply current translate
      .translate(this.x, this.y)
      .scale(scaleDiff)
      .translate(-originX, -originY)
      // Apply current scale.
      .scale(this.scale);

    // Convert the transform into basic translate & scale.
    this.setTransform({
      scale: matrix.a,
      x: matrix.e,
      y: matrix.f,
    });
  }

  /**
  * Update the stage with a given scale/x/y.
  */
  setTransform(opts) {
    const {
      scale = this.scale,
    } = opts;

    let {
      x = this.x,
      y = this.y,
    } = opts;

    // If we don't have an element to position, just set the value as given.
    // We'll check bounds later.
    if (!this._positioningEl) {
      this._updateTransform(scale, x, y);
      return;
    }

    // Get current layout
    const thisBounds = this._container.current.getBoundingClientRect();
    const positioningElBounds = this._positioningEl.getBoundingClientRect();

    // Not displayed. May be disconnected or display:none.
    // Just take the values, and we'll check bounds later.
    if (!thisBounds.width || !thisBounds.height) {
      this._updateTransform(scale, x, y);
      return;
    }

    // Create points for _positioningEl.
    let topLeft = createPoint();
    topLeft.x = positioningElBounds.left - thisBounds.left;
    topLeft.y = positioningElBounds.top - thisBounds.top;
    let bottomRight = createPoint();
    bottomRight.x = positioningElBounds.width + topLeft.x;
    bottomRight.y = positioningElBounds.height + topLeft.y;

    // Calculate the intended position of _positioningEl.
    const matrix = createMatrix()
      .translate(x, y)
      .scale(scale)
      // Undo current transform
      .multiply(this._transform.inverse());

    topLeft = topLeft.matrixTransform(matrix);
    bottomRight = bottomRight.matrixTransform(matrix);

    // Ensure _positioningEl can't move beyond out-of-bounds.
    // Correct for x
    if (topLeft.x > thisBounds.width) {
      x += thisBounds.width - topLeft.x;
    } else if (bottomRight.x < 0) {
      x += -bottomRight.x;
    }

    // Correct for y
    if (topLeft.y > thisBounds.height) {
      y += thisBounds.height - topLeft.y;
    } else if (bottomRight.y < 0) {
      y += -bottomRight.y;
    }

    let x1 = x;
    let y1 = y;
    let s = scale;
    if (s <= 1) {
      s = 1;
      x1 = 0;
      y1 = 0;
    }

    this._updateTransform(s, x1, y1);
  }

  /**
   * Update transform values without checking bounds. This is only called in setTransform.
   */
  private _updateTransform(scale: number, x: number, y: number) {
    // Avoid scaling to zero
    // Return if there's no change
    if (
      scale === this.scale &&
      x === this.x &&
      y === this.y
    ) return;

    this._transform.e = x;
    this._transform.f = y;
    this._transform.d = this._transform.a = scale;

    this._container.current.style.setProperty('--x', this.x + 'px');
    this._container.current.style.setProperty('--y', this.y + 'px');
    this._container.current.style.setProperty('--scale', this.scale + '');
    const { onChange } = this.props;
    if (typeof onChange === 'function') {
      onChange({
        x,
        y,
        scale,
      });
    }
  }

  render() {
    const { children, className } = this.props;
    return (<div ref={this._container} className={`${className} pinch-zoom`}>{children}</div>);
  }
}
Yang03 commented 4 years ago

发现 ios 不支持,且不能阻止冒泡