1

Gần đây tôi đã thử nghiệm với hình ảnh âm thanh trong React trên blog Twilio . Trong khi tôi có ý định dạy bản thân nhiều hơn về API âm thanh web, tôi thấy rằng tôi đã chọn một vài kỹ thuật để tạo hiệu ứng trong canvas trong một dự án React. Nếu bạn đang tạo một hoạt hình canvas trong React thì có lẽ điều này cũng sẽ giúp bạn.

Tài liệu tham khảo tốt

Trước tiên, nếu bạn đã sử dụng React trước khi bạn biết rằng bạn nên tránh chạm vào DOM và để React xử lý nó. Nếu bạn đã từng làm việc với HTML5 <canvas>trước đây, bạn cũng sẽ biết rằng để có được một bối cảnh sẽ vẽ trên khung vẽ, bạn cần gọi trực tiếp vào chính phần tử canvas. Rất may đây là một trường hợp cạnh mà React hỗ trợ thông qua refs .

Để có được một ref cho một phần tử canvas bên trong một thành phần React, trước tiên bạn cần tạo ref trong constructor bằng cách sử dụng React.createRef. Khi bạn đến để kết xuất phần tử canvas, thêm một prop được gọi là reftrỏ tới ref bạn đã tạo.

class Animation extends React.Component {
  constructor(props) {
    super(props);
    this.canvasRef = React.createRef();
  }

  render() {
    return (
      <div>
        <canvas ref={this.canvasRef} />
      </div>
    );
  }
}

Khi bạn đã thiết lập nó theo cách này, bạn có thể tham khảo phần tử canvas thông qua thuộc tính của ref current, ví dụ như trong componentDidMount:

componentDidMount() {
    const canvas = this.canvasRef.current;
    const context = canvas.getContext('2d');
    context.fillRect(0, 0, canvas.width, canvas.height);
  }

Bây giờ bạn có bối cảnh bạn có thể vẽ và hoạt hình tất cả những gì bạn thích.

Tách hoạt hình và vẽ

Rất nhiều tòa nhà với React là về việc duy trì trạng thái của chế độ xem. Lần đầu tiên tôi hoạt hình một cái gì đó trên một khung vẽ trong React, tôi giữ trạng thái và mã để vẽ nó trong cùng một thành phần. Sau khi duyệt các ví dụ trực tuyến, tôi đã xem qua quảng trường xoay này trên CodePen . Điều tôi thực sự thích về ví dụ này là cách trạng thái được tách ra khỏi bản vẽ với việc sử dụng hai thành phần. Trạng thái của bản vẽ sau đó được chuyển từ thành phần hoạt hình sang thành phần vẽ thông qua các đạo cụ.

Tôi đã tạo lại bản gốc để hiển thị sự tách biệt.

Đầu tiên, bạn xác định một Canvasthành phần vẽ hình ảnh bằng cách sử dụng các đạo cụ làm tham số.

class Canvas extends React.Component {
  constructor(props) {
    super(props);
    this.canvasRef = React.createRef();
  }

  componentDidUpdate() {
    // Draws a square in the middle of the canvas rotated
    // around the centre by this.props.angle
    const { angle } = this.props;
    const canvas = this.canvasRef.current;
    const ctx = canvas.getContext('2d');
    const width = canvas.width;
    const height = canvas.height;
    ctx.save();
    ctx.beginPath();
    ctx.clearRect(0, 0, width, height);
    ctx.translate(width / 2, height / 2);
    ctx.rotate((angle * Math.PI) / 180);
    ctx.fillStyle = '#4397AC';
    ctx.fillRect(-width / 4, -height / 4, width / 2, height / 2);
    ctx.restore();
  }

  render() {
    return <canvas width="300" height="300" ref={this.canvasRef} />;
  }
}

Sau đó, bạn tạo một Animationthành phần chạy một vòng lặp hoạt hình bằng cách sử dụng requestAnimationFrame. Mỗi khi vòng lặp hoạt hình chạy, bạn cập nhật các tham số của hình ảnh động ở trạng thái và để React kết xuất Canvasvới các đạo cụ được cập nhật.

Đừng quên thực hiện componentWillUnmountđể dừng requestAnimationFramevòng lặp quá.

class Animation extends React.Component {
  constructor(props) {
    super(props);
    this.state = { angle: 0 };
    this.updateAnimationState = this.updateAnimationState.bind(this);
  }

  componentDidMount() {
    this.rAF = requestAnimationFrame(this.updateAnimationState);
  }

  updateAnimationState() {
    this.setState(prevState => ({ angle: prevState.angle + 1 }));
    this.rAF = requestAnimationFrame(this.updateAnimationState);
  }

  componentWillUnmount() {
    cancelAnimationFrame(this.rAF);
  }

  render() {
    return <Canvas angle={this.state.angle} />;
  }
}

Bạn có thể thấy điều này trong hành động trong cây bút này .

Đăng ký lại

Một mối quan tâm khi hoạt hình hoặc thực hiện các cập nhật hình ảnh chuyên sâu khác trong React là việc đăng ký lại các yếu tố con quá thường xuyên, gây ra trò đùa . Khi chúng ta vẽ trên khung vẽ, chúng ta không bao giờ muốn chính phần tử canvas được đăng ký lại. Vậy cách tốt nhất để gợi ý cho React rằng chúng ta không muốn điều đó xảy ra là gì?

Bạn có thể nghĩ về shouldComponentUpdatephương pháp vòng đời. Trở về falsetừ shouldComponentUpdatesẽ cho React biết rằng thành phần này không cần thay đổi. Tuy nhiên, nếu chúng ta đang sử dụng các mô hình trên, trở về falsetừ shouldComponentUpdatesẽ bỏ chạy componentDidUpdatevà đó là trách nhiệm của chúng tôi vẽ.

Cuối cùng tôi đã bắt gặp câu trả lời này từ Dan Abramov cho một câu hỏi trên StackOverflow . Chúng ta có thể tạo một PureCanvasthành phần thực hiện shouldComponentUpdatevà trả về falsevà sử dụng tham chiếu gọi lại để lấy tham chiếu đến phần tử canvas trong Canvasthành phần cha .

Lưu ý: trong câu trả lời của Dan, ông nói rằng sử dụng mẫu ở trên sẽ ổn và kỹ thuật sau có thể chỉ cần thiết nếu bạn đã định hình ứng dụng của mình và thấy nó tạo ra sự khác biệt.

Cập nhật ví dụ trên, chúng tôi chia Canvasthành phần thành a Canvasvà a PureCanvas. Đầu tiên, PureCanvassử dụng tham chiếu gọi lại và gọi lại được cung cấp thông qua các đạo cụ để trả về bối cảnh canvas cho thành phần cha. Nó cũng làm cho chính phần tử canvas.

class PureCanvas extends React.Component {
  shouldComponentUpdate() {
    return false;
  }

  render() {
    return (
      <canvas
        width="300"
        height="300"
        ref={node =>
          node ? this.props.contextRef(node.getContext('2d')) : null
        }
      />
    );
  }
}

Sau đó, Canvasthành phần này chuyển một hàm gọi lại saveContext, như là chỗ dựa contextRefkhi kết xuất PureCanvas. Khi hàm được gọi, chúng ta lưu bối cảnh (và lưu trữ chiều rộng và chiều cao của phần tử canvas). Phần còn lại của sự khác biệt từ trước đang chuyển tham chiếu ctxđến this.ctx.

class Canvas extends React.Component {
  constructor(props) {
    super(props);
    this.saveContext = this.saveContext.bind(this);
  }

  saveContext(ctx) {
    this.ctx = ctx;
    this.width = this.ctx.canvas.width;
    this.height = this.ctx.canvas.height;
  }

  componentDidUpdate() {
    const { angle } = this.props;
    this.ctx.save();
    this.ctx.beginPath();
    this.ctx.clearRect(0, 0, this.width, this.height);
    this.ctx.translate(this.width / 2, this.height / 2);
    this.ctx.rotate((angle * Math.PI) / 180);
    this.ctx.fillStyle = '#4397AC';
    this.ctx.fillRect(
      -this.width / 4,
      -this.height / 4,
      this.width / 2,
      this.height / 2
    );
    this.ctx.restore();
  }

  render() {
    return <PureCanvas contextRef={this.saveContext} />;
  }
}

Mặc dù không cần thiết, tôi thấy sự tách biệt này giữa hoạt hình, vẽ và hiển thị chính phần tử canvas khá dễ chịu. Bạn cũng có thể thấy ví dụ này hoạt động trên CodePen .

Canvas so với React

Đó là một hành trình thú vị làm việc với một yếu tố canvas trong React. Cách họ làm việc cảm thấy rất khác biệt với nhau, do đó, việc đồng bộ hóa chúng không nhất thiết phải đơn giản. Hy vọng, nếu bạn có vấn đề này thì những kỹ thuật này có thể giúp bạn.

Nếu bạn quan tâm đến các hình ảnh động khác trong React, vui lòng xem bài viết của tôi về trực quan hóa âm thanh trong React .

|