Как я могу использовать Fabric.js с React?

У меня есть приложение, использующее в основном HTML5 canvas через Fabric.js. Приложение написано поверх Angular 1.x, и я планирую перенести его в React. Мое приложение позволяет писать текст и рисовать линии, прямоугольники и эллипсы. Также можно перемещать, увеличивать, уменьшать, выбирать, вырезать, копировать и вставлять один или несколько таких объектов. Также можно увеличивать и панорамировать холст с помощью различных ярлыков. Короче говоря, мое приложение в полной мере использует Fabric.js.

Я не смог найти много информации о том, как использовать Fabric.js вместе с React, поэтому меня беспокоит то, что: 1. возможно ли это без значительных изменений, и 2. имеет ли это смысл или я должен вместо этого использовать какую-то другую обширную библиотеку canvas, которая имеет лучшую поддержку React?

Единственным примером React+Fabric.js, который я смог найти, был Reaction-Komik, который, однако, намного проще, чем мое приложение. Моя главная задача - обработка событий и манипулирование DOM в Fabric.js, а также их влияние на React.

Кажется, есть и библиотека холста для React, называемая реагирующим холстом, но, по-видимому, ей не хватает многих функций по сравнению с Fabric.js.

Что я должен учитывать (в отношении манипулирования DOM, обработки событий и т. Д.) При использовании Fabric.js в приложении React?

8 ответов

У нас была такая же проблема в нашем приложении, как использовать Fabric.js внутри реакции. Я рекомендую рассматривать ткань как неконтролируемый компонент. Имейте экземпляр фабрики, с которым может разговаривать все ваше приложение и делать звонки фабрики, а затем, когда что-то изменяется .toObject() позвоните, чтобы поместить все состояние матрицы в ваш магазин Redux. Тогда ваше приложение React может читать состояние фабрики из вашего глобального состояния Redux, как если бы вы это делали в любом обычном приложении React.

Я не могу получить пример работы в редакторе кода Stackru, но вот пример JSFiddle, который реализует шаблон, который я рекомендую.

Я использовал Fabric для проверки концепции, и общая идея такая же, как, скажем, для D3. Помните, что Fabric работает с элементами DOM, а React отображает данные в DOM, и обычно последний откладывается. Есть две вещи, которые помогут вам убедиться, что ваш код работает:

Подождите, пока компонент не будет смонтирован

Для этого поместите свой экземпляр Fabric в componentDidMount:

import React, { Component } from 'react';
import { fabric } from 'react-fabricjs';
import styles from './MyComponent.css';

class MyComponent extends Component {
  componentWillMount() {
    // dispatch some actions if you use Redux
  }

  componentDidMount() {
    const canvas = new fabric.Canvas('c');

    // do some stuff with it
  }

  render() {
    return (
      <div className={styles.myComponent}>
        <canvas id="c" />
      </div>
    )
  }
}

Размещение конструктора ткани в componentDidMount гарантирует, что он не потерпит неудачу, потому что к моменту выполнения этого метода DOM готов. (но реквизит иногда нет, на случай, если вы используете Redux)

Используйте ссылки для расчета фактической ширины и высоты

Ссылки являются ссылками на фактические элементы DOM. С помощью ссылок вы можете сделать то же, что и с элементами DOM, используя DOM API: выбрать дочерних элементов, найти родителя, назначить свойства стиля, рассчитать innerHeight а также innerWidth, Последнее именно то, что вам нужно:

componentDidMount() {
  const canvas = new fabric.Canvas('c', {
    width: this.refs.canvas.clientWidth,
    height: this.refs.canvas.clientHeight
  });

  // do some stuff with it
}

Не забудьте определить refs собственностью this, Для этого вам понадобится конструктор. Все это будет выглядеть

import React, { Component } from 'react';
import { fabric } from 'react-fabricjs';
import styles from './MyComponent.css';

class MyComponent extends Component {
  constructor() {
    super()
    this.refs = {
      canvas: {}
    };
  }

  componentWillMount() {
    // dispatch some actions if you use Redux
  }

  componentDidMount() {
    const canvas = new fabric.Canvas('c', {
      width: this.refs.canvas.clientWidth,
      height: this.refs.canvas.clientHeight
    });

    // do some stuff with it
  }

  render() {
    return (
      <div className={styles.myComponent}>
        <canvas
          id="c"
          ref={node => {
            this.refs.canvas = node;
          } />
      </div>
    )
  }
}

Смешайте ткань с компонентом состояния или реквизита

Вы можете заставить ваш экземпляр Fabric реагировать на любые реквизиты компонента или обновления состояния. Чтобы заставить его работать, просто обновите свой экземпляр Fabric (который, как вы могли видеть, вы можете сохранить как часть собственных свойств компонента) в componentDidUpdate, Просто полагаясь на render вызовы функций не будут действительно полезны, потому что ни один из представленных элементов никогда не изменится при новом реквизите или новом состоянии. Что-то вроде этого:

import React, { Component } from 'react';
import { fabric } from 'react-fabricjs';
import styles from './MyComponent.css';

class MyComponent extends Component {
  constructor() {
    this.refs = {
      canvas: {}
    };
  }

  componentWillMount() {
    // dispatch some actions if you use Redux
  }

  componentDidMount() {
    const canvas = new fabric.Canvas('c', {
      width: this.refs.canvas.clientWidth,
      height: this.refs.canvas.clientHeight
    });

    this.fabric = canvas;

    // do some initial stuff with it
  }

  componentDidUpdate() {
    const {
      images = []
    } = this.props;
    const {
      fabric
    } = this;

    // do some stuff as new props or state have been received aka component did update
    images.map((image, index) => {
      fabric.Image.fromURL(image.url, {
        top: 0,
        left: index * 100 // place a new image left to right, every 100px
      });
    });
  }

  render() {
    return (
      <div className={styles.myComponent}>
        <canvas
          id="c"
          ref={node => {
            this.refs.canvas = node;
          } />
      </div>
    )
  }
}

Просто замените рендеринг изображения на нужный вам код, который зависит от состояния нового компонента или реквизита. Не забудьте очистить холст, прежде чем рендерить новые объекты на нем!

В response 16.8 или новее вы также можете создать собственный хук:

import React, { useRef, useCallback } from 'react';
const useFabric = (onChange) => {
    const fabricRef = useRef();
    const disposeRef = useRef();
    return useCallback((node) => {
        if (node) {
            fabricRef.current = new fabric.Canvas(node);
            if (onChange) {
                disposeRef.current = onChange(fabricRef.current);
            }
        }
        else if (fabricRef.current) {
            fabricRef.current.dispose();
            if (disposeRef.current) {
                disposeRef.current();
                disposeRef.current = undefined;
            }
        }
    }, []);
};

Применение

const FabricDemo = () => {
  const ref = useFabric((fabricCanvas) => {
    console.log(fabricCanvas)
  });
  return <div style={{border: '1px solid red'}}>
    <canvas ref={ref} width={300} height={200} />
  </div>
}

Я последовал ответу StefanHayden , и вот тестовый код.

Создать объект холста ткани

Создайте собственный хук для возврата FabricRef(обратных вызовов Refs ) :

      const useFabric = () => {
  const canvas = React.createRef(null);
  const fabricRef = React.useCallback((element) => {
    if (!element) return canvas.current?.dispose();

    canvas.current = new fabric.Canvas(element, {backgroundColor: '#eee'});
    canvas.current.add(new fabric.Rect(
      {top: 100, left: 100, width: 100, height: 100, fill: 'red'}
    ));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return fabricRef;
};

Обратите внимание, что:

Наконец, создайте холст и передайте ссылку:

      function App() {
  const fabricRef = useFabric();
  return <canvas ref={fabricRef} width={640} height={360}/>;
}

Но мы не можем использовать FabricCanvas в другом месте, я объясню в следующей главе.

Кодепен

Совместное использование объекта Fabric Canvas по контексту

По контексту , в котором хранится переменную экземпляраизменяемое значение Refs , мы можем использовать объект холста ткани где угодно.

Во-первых, мы определяем контекст, который принимает ссылку как значение:

      const FabricContext = React.createContext();

function App() {
  return (
    <FabricContext.Provider value={React.createRef()}>
      <MyToolKit />
      <MyFabric />
    </FabricContext.Provider>
  );
}

Затем мы можем использовать его в пользовательском хуке. Мы не создаем ссылку сейчас, а используем ссылку в контексте:

      const useFabric = () => {
  const canvas = React.useContext(FabricContext);
  const fabricRef = React.useCallback((element) => {
    if (!element) return canvas.current?.dispose();

    canvas.current = new fabric.Canvas(element, {backgroundColor: '#eee'});
    canvas.current.add(new fabric.Rect(
      {top: 100, left: 100, width: 100, height: 100, fill: 'red'}
    ));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return fabricRef;
};

function MyFabric() {
  const fabricRef = useFabric();
  return <canvas ref={fabricRef} width={640} height={360} />;
}

И мы также могли бы использовать его где угодно, только когда доступен контекст:

      function MyToolKit() {
  const canvas = React.useContext(FabricContext);
  const drawRect = () => {
    canvas.current?.add(new fabric.Rect(
      {top: 100, left: 100, width: 100, height: 100, fill: 'red'}
    ));
  };
  return <button onClick={drawRect}>Draw</button>;
}

На протяжении всего жизненного цикла приложения теперь можно использовать объект холста ткани в любом месте.

Кодепен

Поделиться объектом Canvas Canvas от реквизита

Если вы не хотите делиться контекстом, например глобальными переменными, мы также можем поделиться с родителем с помощью реквизита.

КодПен

Поделитесь объектом Fabric Canvas от forwardRef

Если вы не хотите делиться по контексту, как глобальные переменные, мы также можем поделиться с родителем по forwardRef.

КодПен

Я не вижу здесь никаких ответов с использованием функциональных компонентов React, и я не мог заставить работать хук от @jantimon. Вот мой подход. У меня есть размеры холста, взятые из макета страницы, а затем есть "backgroundObject" в точке (0,0), который действует как область рисования для пользователя.

import React, { useState, useRef, useEffect } from 'react';

const { fabric } = window;

// this component takes one prop, data, which is an object with data.sizePixels, an array that represents the width,height of the "backgroundObject"

const CanvasComponent = ({ data }) => {
  const canvasContainer = useRef();
  const canvasRef = useRef();
  const fabricRef = useRef();
  const [error, setError] = useState();

  const initCanvas = ({ canvas, size }) => {
    console.log('*** canvas init');
    console.log(canvas);

    let rect = new fabric.Rect({
      width: size[0],
      height: size[1],
      fill: 'white',
      left: 0,
      top: 0,
      selectable: false,
      excludeFromExport: true,
    });
    canvas.add(rect);

    rect = new fabric.Rect({
      width: 100,
      height: 100,
      fill: 'red',
      left: 100,
      top: 100,
    });
    canvas.add(rect);
  };

  // INIT
  useEffect(() => {
    if (!canvasRef.current) return;
    if (fabricRef.current) return;
    const canvas = new fabric.Canvas('canvas', {
      width: canvasContainer.current.clientWidth,
      height: canvasContainer.current.clientHeight,
      selection: false, // disables drag-to-select
      backgroundColor: 'lightGrey',
    });
    fabricRef.current = canvas;
    initCanvas({ canvas, size: data.sizePixels });
  });

  // INPUT CHECKING
  if (!data || !Array.isArray(data.sizePixels)) setError('Sorry, we could not open this item.');

  if (error) {
    return (
      <p>{error}</p>
    );
  }

  return (
    <>
      <div style={{
        display: 'flex',
        flexDirection: 'row',
        width: '100%',
        minHeight: '60vh',
      }}
      >
        <div style={{ flex: 3, backgroundColor: 'green' }}>
          Controls
        </div>
        <div ref={canvasContainer} style={{ flex: 7, backgroundColor: 'white' }}>
          <canvas id="canvas" ref={canvasRef} style={{ border: '1px solid red' }} />
        </div>
      </div>

      <div>
        <button
          type="button"
          onClick={() => {
            const json = fabricRef.current.toDatalessJSON();
            setDebugJSON(JSON.stringify(json, null, 2));
          }}
        >
          Make JSON
        </button>
      </div>
    </>
  );
};

export default CanvasComponent;

Существует также пакет response-fabricjs, который позволяет использовать тканевые объекты в качестве реагирующих компонентов. На самом деле, ответ Ришата включает в себя этот пакет, но я не понимаю, как он должен работать, так как нет никакого объекта "fabric" в реагирующем устройстве (он, вероятно, имел в виду пакет "фабричный материал"). Пример простого компонента "Hello world":

import React from 'react';
import {Canvas, Text} from 'react-fabricjs';

const HelloFabric = React.createClass({
  render: function() {
    return (
      <Canvas
        width="900"
        height="900">
          <Text
            text="Hello World!"
            left={300}
            top={300}
            fill="#000000"
            fontFamily="Arial"
          />
      </Canvas>
    );
  }
});

export default HelloFabric;

Даже если вы не хотите использовать этот пакет, изучение его кода может помочь вам понять, как реализовать Fabric.js в React самостоятельно.

Я создал редактор формы реакции для аналогичного варианта использования, в котором Fabric.js имел все необходимые мне функции, но было больно поддерживать синхронизацию с моим кодом React/Redux. У него не так много функций, как у Fabric, но он может быть хорошей заменой для некоторых случаев использования.

Упс!

Если вы пришли сюда в ожидании ответов о Microsoft Fabric, вы попали не туда. Вы должны искать офис-UI-ткань.

(Нерелевантный и слегка смущающий оригинальный ответ удален.)

Другие вопросы по тегам