51

我有一个通过Fabric.js使用大量 HTML5 画布的应用程序。该应用程序是在Angular 1.x之上编写的,我计划将其迁移到React。我的应用程序允许编写文本和绘制线条、矩形和椭圆。也可以移动、放大、缩小、选择、剪切、复制和粘贴一个或多个这样的对象。还可以使用各种快捷方式缩放和平移画布。简而言之,我的应用程序充分利用了 Fabric.js。

我找不到太多关于如何将 Fabric.js 与 React 一起使用的信息,所以我担心的是 1. 是否可以在没有重大修改的情况下进行,以及 2. 是否有意义或者我应该使用其他一些广泛的画布库对 React 有更好的支持吗?

我能找到的 React+Fabric.js 的唯一示例是react-komik,但它比我的应用程序要简单得多。我主要关心的是 Fabric.js 的事件处理和 DOM 操作,以及它们对 React 的影响。

似乎还有一个 React 的画布库,称为react-canvas,但与 Fabric.js 相比,它似乎缺少很多功能。

在 React 应用程序中使用 Fabric.js 时,我必须考虑什么(关于 DOM 操作、事件处理等)?

4

6 回答 6

54

我们在我们的应用程序中遇到了同样的问题,即如何在 react 中使用 Fabric.js。我的建议是将织物视为不受控制的组件。拥有整个应用程序可以与之通信的结构实例并进行结构调用,然后当发生任何变化时,使用.toObject()调用将整个结构状态放入您的 Redux 存储中。然后,您的 React 应用程序可以从您的全局 Redux 状态中读取结构状态,就像您在任何普通的 React 应用程序中所做的那样。

我无法在 StackOverflow 代码编辑器中获得示例,但这里有一个 JSFiddle 示例,它实现了我推荐的模式。

于 2016-08-25T17:32:54.183 回答
22

我已经将 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>
    )
  }
}

将 Fabric 构造函数放入componentDidMount确保它不会失败,因为在执行此方法时,DOM 已准备就绪。(但道具有时不是,以防万一你使用 Redux)

使用 refs 计算实际宽度和高度

Refs是对实际 DOM 元素的引用。你可以使用 refs 来做你可以使用 DOM API 来处理 DOM 元素的事情:选择子元素、查找父元素、分配样式属性、计算innerHeightinnerWidth. 后者正是您所需要的:

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>
    )
  }
}

只需用您需要的代码替换图像渲染,这取决于新的组件状态或道具。不要忘记在画布上渲染新对象之前清理画布!

于 2016-08-27T22:55:49.907 回答
9

使用 react 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>
}
于 2019-11-16T18:48:18.470 回答
2

我在这里没有看到使用 React 的功能组件的任何答案,而且我无法从 @jantimon 获得钩子来工作。这是我的方法。我让画布本身从页面布局中获取其尺寸,然后在(0,0)处有一个“backgroundObject”作为用户的可绘制区域。

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;

于 2020-09-10T00:51:14.213 回答
1

还有react-fabricjs包,它允许您使用织物对象作为反应组件。实际上,Rishat 的答案包括这个包,但我不明白它应该如何工作,因为 react-fabricjs 中没有“fabric”对象(他可能是指“fabric-webpack”包)。简单的“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;

即使你不想使用这个包,探索它的代码可能会帮助你了解如何自己在 React 中实现 Fabric.js。

于 2016-09-30T15:13:49.077 回答
0

我跟着StefanHayden的回答,这里是测试代码。

创建织物画布对象

创建一个自定义钩子以返回一个 fabricRef( Callback 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;
};

注意:

最后,创建一个画布并传递 ref:

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

但是我们不能在其他地方使用fabricCanvas,我会在下一章解释。

密码笔

按上下文共享 Fabric Canvas 对象

通过存储Refs 可变值的Context,我们可以在任何地方使用织物画布对象。

首先,我们定义上下文,它以 ref 为值:

const FabricContext = React.createContext();

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

然后,我们可以在自定义钩子中使用它。我们现在不创建 ref,而是在上下文中使用 ref:

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>;
}

在 App 的整个生命周期中,它现在可以在任何地方使用织物画布对象。

密码笔

通过道具分享 Fabric Canvas Object

如果不想通过 Context 共享,比如全局变量,我们也可以通过 props 与父级共享。

代码笔

通过 forwardRef 共享 Fabric Canvas 对象

如果不想通过 Context 共享,比如全局变量,我们也可以通过 forwardRef 与 parent 共享。

代码笔

于 2021-12-15T12:48:37.050 回答