如何在 React 中使用 Fabric.js?

IT技术 reactjs fabricjs react-canvas
2021-03-24 08:27:01

我有一个通过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 操作、事件处理等)?

6个回答

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

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

在每次状态更改时调用 toObject 听起来有点矫枉过正。它不会让你的应用程序变得非常缓慢吗?
2021-05-26 08:27:01
谢谢你的例子。您能否详细说明在上面的示例(或任何用于绘图的应用程序)中添加 Redux 商店的优点?商店只保存一个结构状态的副本,应用程序的其余部分可以从中读取,但所有更改/操作都在 fabricCanvas 上完成。(我是这方面的新手)
2021-06-04 08:27:01
是否可以将您的示例添加到 jsfiddle 或其他任何地方,因为它现在返回 404?谢谢你。
2021-06-19 08:27:01
不。这是一个非常快的调用。此外,当最后发生任何变化时,您也不会调用它因此,如果您正在拖动一个形状,您只能toObject在鼠标向上时在最后调用
2021-06-21 08:27:01

我已经将 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 已准备就绪。(但props有时不是,以防万一你使用 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 与组件状态或props混合

您可以让 Fabric 实例对任何组件props或状态更新做出react。要使其工作,只需在componentDidUpdate. 简单地依赖render函数调用不会真正有用,因为渲染的元素都不会在新props或新状态上发生变化。像这样的东西:

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

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

似乎您对 reactjs 和 fabricjs 很有经验。我需要你的帮助。我可以设置背景图像但无法设置背景颜色。stackoverflow.com/questions/37565041/…你能看到这个吗?
2021-05-22 08:27:01
你能帮我解决这个问题吗?我无法在画布中设置背景图像stackoverflow.com/questions/41581511/...
2021-05-31 08:27:01
我认为我的答案已经过时了。如果你用最新版本的 react-fabricjs 解决了这个问题,你介意编辑答案还是写一个新的?
2021-06-03 08:27:01
我有点困惑这是如何工作的。在任何地方都看不到react-fabricjs暴露的fabric对象。所以这一行:import { fabric } from 'react-fabricjs';为我创造了一个错误。
2021-06-17 08:27:01

使用 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>
}
不,因为你可以懒惰地渲染一个 div,例如return someCondition ? <div ref={ref}>...</div>: null- 也显示在官方文档中:reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
2021-05-27 08:27:01
你不能这样做--- ``` const useFabric = ( opts: { width, number }, onChange ) => { const ref = useRef(null); useEffect(() => { const fabricInst = new fabric.Canvas(ref.current); onChange(fabricInst); }, [onChange, opts.height, opts.width]); 返回参考;}; ``
2021-05-30 08:27:01
我正在尝试使用它,但我不够聪明:当我稍后尝试访问 ref.current(来自上面的 FabricDemo)时,它始终为空。但是画布在页面上显示正常
2021-06-12 08:27:01
没错,但是useEffect用这种方式应该相当于componentDidMount。如果我没记错的话,ref 肯定会在那时存在
2021-06-16 08:27:01
如果 ref 是null第一次useEffect运行,这将不起作用,因为更改 ref 将不会useEffect再次执行
2021-06-17 08:27:01

我在这里没有看到任何使用 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;

还有react-fabricjs包,它允许您使用结构对象作为react组件。实际上,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。

另请参阅我对连接 fabric.js 和 redux 的问题的回答,其中我使用普通的 fabricjs 并将其内容保存到 redux 存储中:stackoverflow.com/questions/37742465/...
2021-05-26 08:27:01
该评论来自 2016 年,从那时起该项目似乎没有得到太多维护,但仍然可以在这里找到:github.com/neverlan/react-fabricjs不管怎样,在 2020 年,我建议从用户 jantimon 探索上面的答案.
2021-05-26 08:27:01
看起来 react-fabricjs 项目已经死了(github 链接显示了 404)。
2021-06-02 08:27:01