xiaosong
5/11/2018 - 6:52 PM

App.js

import React from 'react';
import PropTypes from 'prop-types';

const MIN_SCALE = 1;
const MAX_SCALE = 4;
const SETTLE_RANGE = 0.001;
const ADDITIONAL_LIMIT = 0.2;
const DOUBLE_TAP_THRESHOLD = 300;
const ANIMATION_SPEED = 0.04;
const RESET_ANIMATION_SPEED = 0.08;
const INITIAL_X = 0;
const INITIAL_Y = 0;
const INITIAL_SCALE = 1;

const settle = (val, target, range) => {
  const lowerRange = val > target - range && val < target;
  const upperRange = val < target + range && val > target;
  return lowerRange || upperRange ? target : val;
};

const inverse = (x) => x * -1;

const getPointFromTouch = (touch, element) => {
  const rect = element.getBoundingClientRect(); 
  return {
    x: touch.clientX - rect.left,
    y: touch.clientY - rect.top,
  };
};

const getMidpoint = (pointA, pointB) => ({
    x: (pointA.x + pointB.x) / 2,
    y: (pointA.y + pointB.y) / 2,
});

const getDistanceBetweenPoints = (pointA, pointB) => (
  Math.sqrt(Math.pow(pointA.y - pointB.y, 2) + Math.pow(pointA.x - pointB.x, 2))
);

const between = (min, max, value) => Math.min(max, Math.max(min, value));

class PinchZoomPan extends React.Component {
  constructor() {
    super(...arguments);
    this.state = this.getInititalState();

    this.handleTouchStart = this.handleTouchStart.bind(this);
    this.handleTouchMove = this.handleTouchMove.bind(this);
    this.handleTouchEnd = this.handleTouchEnd.bind(this);
  }

  zoomTo(scale, midpoint) {
    const frame = () => {
      if (this.state.scale === scale) return null;

      const distance = scale - this.state.scale;
      const targetScale = this.state.scale + (ANIMATION_SPEED * distance);

      this.zoom(settle(targetScale, scale, SETTLE_RANGE), midpoint);
      this.animation = requestAnimationFrame(frame);
    };

    this.animation = requestAnimationFrame(frame);
  }

  reset() {
    const frame = () => {
      if (this.state.scale === INITIAL_SCALE && this.state.x === INITIAL_X && this.state.y === INITIAL_Y) return null;
      const distance = INITIAL_SCALE - this.state.scale;
      const distanceX = INITIAL_X - this.state.x;
      const distanceY = INITIAL_Y - this.state.y;

      const targetScale = settle(this.state.scale + (RESET_ANIMATION_SPEED * distance), INITIAL_SCALE, SETTLE_RANGE);
      const targetX = settle(this.state.x + (RESET_ANIMATION_SPEED * distanceX), INITIAL_X, SETTLE_RANGE);
      const targetY = settle(this.state.y + (RESET_ANIMATION_SPEED * distanceY), INITIAL_Y, SETTLE_RANGE);

      const nextWidth = this.props.width * targetScale;
      const nextHeight = this.props.height * targetScale;

      this.setState({
        x: targetX,
        y: targetY,
        scale: targetScale,
        width: nextWidth,
        height: nextHeight,
      }, () => {
        this.animation = requestAnimationFrame(frame);
      });
    };

    this.animation = requestAnimationFrame(frame);
  }

  getInititalState() {
    return {
      x: INITIAL_X,
      y: INITIAL_Y,
      scale: INITIAL_SCALE,
      width: this.props.width,
      height: this.props.height,
    };
  }

  handleTouchStart(event) {
    this.animation && cancelAnimationFrame(this.animation);
    if (event.touches.length == 2) this.handlePinchStart(event);
    if (event.touches.length == 1) this.handleTapStart(event);
  }

  handleTouchMove(event) {
    if (event.touches.length == 2) this.handlePinchMove(event);
    if (event.touches.length == 1) this.handlePanMove(event);
  }

  handleTouchEnd(event) {
    if (event.touches.length > 0) return null;
    
    if (this.state.scale > MAX_SCALE) return this.zoomTo(MAX_SCALE, this.lastMidpoint);
    if (this.state.scale < MIN_SCALE) return this.zoomTo(MIN_SCALE, this.lastMidpoint);

    if (this.lastTouchEnd && this.lastTouchEnd + DOUBLE_TAP_THRESHOLD > event.timeStamp) {
      this.reset();
    }

    this.lastTouchEnd = event.timeStamp;
  }

  handleTapStart(event) {
    this.lastPanPoint = getPointFromTouch(event.touches[0], this.container);
  }

  handlePanMove(event) {
    if (this.state.scale === 1) return null;

    event.preventDefault();

    const point = getPointFromTouch(event.touches[0], this.container);
    const nextX = this.state.x + point.x - this.lastPanPoint.x;
    const nextY = this.state.y + point.y - this.lastPanPoint.y;

    this.setState({
      x: between(this.props.width - this.state.width, 0, nextX),
      y: between(this.props.height - this.state.height, 0, nextY),
    });
    
    this.lastPanPoint = point;
  }

  handlePinchStart(event) {
    const pointA = getPointFromTouch(event.touches[0], this.container);
    const pointB = getPointFromTouch(event.touches[1], this.container);
    this.lastDistance = getDistanceBetweenPoints(pointA, pointB);
  }

  handlePinchMove(event) {
    event.preventDefault();
    const pointA = getPointFromTouch(event.touches[0], this.container);
    const pointB = getPointFromTouch(event.touches[1], this.container);
    const distance = getDistanceBetweenPoints(pointA, pointB);
    const midpoint = getMidpoint(pointA, pointB);
    const scale = between(MIN_SCALE - ADDITIONAL_LIMIT, MAX_SCALE + ADDITIONAL_LIMIT, this.state.scale * (distance / this.lastDistance));

    this.zoom(scale, midpoint);

    this.lastMidpoint = midpoint;
    this.lastDistance = distance;
  }

  zoom(scale, midpoint) {
    const nextWidth = this.props.width * scale;
    const nextHeight = this.props.height * scale;
    const nextX = this.state.x + (inverse(midpoint.x * scale) * (nextWidth - this.state.width) / nextWidth);
    const nextY = this.state.y + (inverse(midpoint.y * scale) * (nextHeight - this.state.height) / nextHeight);

    this.setState({
      width: nextWidth,
      height: nextHeight,
      x: nextX,
      y: nextY,
      scale,
    });
  }

  render() {
    return (
      <div 
        ref={(ref) => this.container = ref}
        onTouchStart={this.handleTouchStart}
        onTouchMove={this.handleTouchMove}
        onTouchEnd={this.handleTouchEnd}
        style={{
          overflow: 'hidden',
          width: this.props.width,
          height: this.props.height,
        }}
      >
        {this.props.children(this.state.x, this.state.y, this.state.scale)} 
      </div>
    );
  }
}

PinchZoomPan.propTypes = {
  children: PropTypes.func.isRequired,
};

export default PinchZoomPan;
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import PinchZoomPan from './PinchZoomPan';



const Usage = ({width, height}) => (
  <div>
    <PinchZoomPan width={width} height={height}>
    {(x, y, scale) => (
        <img
          src={`https://placekitten.com/${width}/${height}`}
          style={{
            pointerEvents: scale === 1 ? 'auto' : 'none',
            transform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`,
            transformOrigin: '0 0',
          }} />
    )}
    </PinchZoomPan>
  </div>
);




class App extends Component {
  render() {
    return (
      <div className="App">
      <Usage width={300} height={300}/>
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
          
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        
        
     
        
      </div>
    );
  }
}

export default App;