React Hooks 使用 Fabric.js

前言

公司项目需求,实现在图片上框选多个多边形,获取多边形坐标点及其相对于图片的位置,本文React使用Hooks写法


一、环境

1、React 16.12.0
2、Fabrci.js 5.3.0
3、Antd design 4.20.0

二、实现步骤

1.引入Fabric.js

npm install fabric

2. 实现

我是在弹窗(Modal)中,如果大家只是默认渲染页,更简单

1、JSX 文件
import React, { forwardRef, useState, useImperativeHandle, useEffect, useRef } from "react";
import { Modal, Checkbox, message, Tooltip, Switch, Button } from "antd";
import { CheckOutlined } from "@ant-design/icons";
import List from "@/components/list";
import "./analysis.less";
import deleteIcon from "@/assets/img/delete-icon.svg";
import noCheckedEvent from "@/assets/img/noData.svg";
import { cloneDeep, uniqBy } from "lodash";
import { fabric } from "fabric";
import { uuid, imgUrlEffective } from "@/util";
import { isIntersect, convertFormat } from "@/util/lines";
import { polygonPositionHandler, anchorWrapper, actionHandler, onMoving } from "./fabric";
import deviceDao from "./service";

const Analysis = (props, ref) => {
  // 使用了forwardRef,事件需抛出给父组件,父组件才可以调用
  useImperativeHandle(ref, () => ({
    getPage,
  }));

  const imgRef = useRef(null); //图片dom ref

  const [open, setOpen] = useState(false);
  const [editData, setEditData] = useState(null);

  const [list, setList] = useState([]); //列表
  const [listActive, setListActive] = useState(null); //列表选中
  const [allowDraw, setAllowDraw] = useState(false); //是否允许编辑

  const [imgUrl, setImgUrl] = useState(null); //图片
  const [boxStyle, setBoxStyle] = useState({ width: 0, height: 0 }); // 画布宽高

  // 框相关
  const canvas = useRef(null); //画布实例
  const canvasPolygon = useRef(null); //当前绘制控件
  const pointList = useRef(null); //点集合

  useEffect(() => {
    //初始化画布
    const { width, height } = boxStyle;
    if (width && height) {
      initCanvas();
    }
  }, [boxStyle]);

  useEffect(() => {
    // 判断图片是否有效
    const checkImgUrl = async () => {
      if (imgUrl) {
        try {
          await imgUrlEffective(imgUrl);
          setAllowDraw(true);
        } catch {
          setAllowDraw(false);
        }
      }
    };
    checkImgUrl();
  }, [imgUrl]);

  // 框回显
  useEffect(() => {
    const { width, height } = boxStyle;

    if (listActive !== null && width && height && list.length) {
      const findObj = cloneDeep(list).find((v) => v.id === listActive);
      if (findObj) {
        const relationList = findObj.relationList.map((v) => {
          return {
            ...v,
            points: findObj.isService ? v.points.map((h) => ({ x: h.x * width, y: h.y * height })) : v.points,
          };
        });

        // console.log("列表切换", relationList);

        // 先清空画布已有框
        canvas.current.clear();

        pointList.current = relationList;

        //回显框
        generatePolygon(relationList);
      }
    }
  }, [listActive, boxStyle, list]);

  // 打开弹窗
  const getPage = (data) => {
    setOpen(true);
    setEditData(data); //当前表格数据
    getList(data); //查询事件类型列表
    getImgUrl(data); //查询图片地址
  };
  //查询底图地址
  const getImgUrl = async (data) => {
    const res = await deviceDao.getImgUrl({ pathParams: { deviceId: data.id } });
    if (res && res.status.code === 200) {
      setImgUrl(res.data);
    }
  };
  // 查询列表数据
  const getList = async (data) => {
    const res = await deviceDao.eventTypeList();
    if (res && res.status.code === 200) {
      const mapList = res.data.map((v) => ({ id: v.id, label: v.name }));
      // 查询所有列表数据的框
      getBox(data, mapList);
    }
  };
  // 查询所有框
  const getBox = async (data, typeList) => {
    const res = await deviceDao.getDeviceEventRs({
      pathParams: { deviceId: data.id },
    });
    if (res && res.status.code === 200) {
      const boxList = res.data.map((v) => ({ ...v, relationList: JSON.parse(v.relationJson || "[]") }));

      const eventList = typeList.map((v) => {
        const typeBox = boxList.find((h) => h.eventTypeId === v.id);
        return {
          ...v,
          isService: true,
          state: typeBox?.state === 1 ?? false,
          relationList: typeBox ? typeBox.relationList : [],
        };
      });

      setList(eventList);
      slideChange(eventList[0]);
    }
  };

  // 列表选中切换
  const slideChange = (data) => {
    setListActive(data.id); //设置左侧列表选中
  };

  // 开关切换
  const enable = (id, checked) => {
    const editIndex = list.findIndex((v) => v.id === id);
    const mapList = cloneDeep(list);
    mapList[editIndex].state = checked;
    setList(mapList);
  };
  // 单个保存
  const save = async (e, activeData) => {
    e.stopPropagation();

    const points = cloneDeep(pointList.current || []);

    const { intersectResult: isIntersect, overlapIds } = hasIntersect(points);

    // 线段相交后边框变为红色且不允许保存,否则为默认蓝色
    if (isIntersect) {
      const allPolygons = canvas.current.getObjects();
      allPolygons.forEach((v) => {
        const isOverlap = overlapIds.includes(v.id); //是否单个多边形内有线段相交
        const color = isOverlap ? "#D32F2F" : "#409eff"; //如果相交,则改变边框颜色
        v.set({
          stroke: color, //线颜色
          cornerColor: color, //拖拽控件颜色
        });
      });

      canvas.current.requestRenderAll(); // 重新渲染canvas

      return message.error("单个多边形不允许线段相交,已为您标为红色,请修改后重试!!!");
    }

    const mapList = cloneDeep(list);
    const editIndex = mapList.findIndex((v) => v.id === activeData.id);

    mapList[editIndex].relationList = pointList.current;
    mapList[editIndex].isService = false;

    setList(mapList);
    message.success("保存成功");
  };
  // 判断线段是否相交
  const hasIntersect = (data = []) => {
    const points = cloneDeep(data);
    const overlapIds = [];

    points.forEach(({ id, points }) => {
      const lines = convertFormat(points);
      // 将起点和终点连接,形成闭合多边形
      lines.push({ start: lines[0].start, end: lines[lines.length - 1].end });

      // 使用标记变量来跟踪是否存在相交线段
      let linesOverlap = lines.some((line, i) => {
        return lines.slice(i + 1).some((otherLine) => isIntersect(line, otherLine));
      });

      // 如果存在相交线段,记录多边形ID
      if (linesOverlap) {
        overlapIds.push(id);
      }
    });

    return { intersectResult: overlapIds.length > 0, overlapIds };
  };

  // 初始化画布
  const initCanvas = () => {
    canvas.current = new fabric.Canvas("canvasRef", {
      defaultCursor: "pointer", //画布默认cursor
      selectionColor: "transparent", //选择时颜色
      selectionBorderColor: "transparent", //选择时边框色
      hoverCursor: "transparent", //鼠标在画布上的样式
      FX_DURATION: 100, //删除元素的动画时长
      fireRightClick: true, // 启用右键
      stopContextMenu: true, // 禁止默认右键菜单
      hasBorders: false,
    });

    canvas.current.on("mouse:down", onMouseDown); //鼠标点击,打点开始绘制
    canvas.current.on("mouse:move", onMouseMove); //鼠标移动,开始绘制
    canvas.current.on("object:moving", onMoving); //边界处理,防止拖出去
    canvas.current.on("mouse:dblclick", finishPolygon); //双击结束绘制
    canvas.current.on("object:added", onObjectAdded); //监听canvas新增框
    canvas.current.on("object:modified", dragPllygon); //拖动位置结束,更新数据
  };
  // 监听鼠标按下事件,打点
  const onMouseDown = (opt) => {
    var target = canvas.current.findTarget(opt.e);

    // 在已有框点击而不是空白区域
    if (target && target.id) {
      canvas.current.setActiveObject(target); //当前控件设置为活跃状态
      opt.e.stopPropagation(); // 阻止事件冒泡
      return;
    }
    if (canvasPolygon.current === null) {
      createPolygon(opt); //新画框
    } else {
      changeCurrentPolygon(opt); //已点击过一次,连点更新框
    }
  };
  // 监听鼠标移动事件,开始绘制
  const onMouseMove = (opt) => {
    if (canvasPolygon.current) {
      changePolygonBelt(opt);
    }
  };
  // 创建多边形
  const createPolygon = (opt) => {
    canvasPolygon.current = new fabric.Polygon(
      [
        { x: opt.absolutePointer.x, y: opt.absolutePointer.y },
        { x: opt.absolutePointer.x, y: opt.absolutePointer.y },
      ],
      {
        fill: "rgba(255, 255, 255, 0.2)", //绘制时背景色
        stroke: "#409eff", //绘制时边框色
        objectCaching: false, //不使用缓存
        hasControls: false, //绘制时不添加control控件
      }
    );
    canvas.current.add(canvasPolygon.current);
  };
  // 线
  const changePolygonBelt = (opt) => {
    let points = canvasPolygon.current.points;
    // console.log("正在移动", points);
    points[points.length - 1].x = opt.absolutePointer.x;
    points[points.length - 1].y = opt.absolutePointer.y;
    canvas.current.requestRenderAll();
  };
  // 连续画线
  const changeCurrentPolygon = (opt) => {
    let points = canvasPolygon.current.points;
    // 右键撤销
    if (opt.button === 3) {
      if (points.length) {
        points.pop(); //从后边删一个
      }
    }
    // 默认新增
    else {
      points.push({
        x: opt.absolutePointer.x,
        y: opt.absolutePointer.y,
      });
    }

    canvas.current.requestRenderAll();
  };
  // 绘制完成元素事件
  const onObjectAdded = (opt) => {
    const target = opt.target;
    const mapList = cloneDeep(pointList.current || []);

    const points = target.points;
    // 至少三个点,否则是一条线或一个点
    if (points.length > 2) {
      const newBox = {
        id: target.id,
        points,
      };
      mapList.push(newBox);
      const unibyList = uniqBy(mapList, "id");
      pointList.current = unibyList;
    }
  };
  // 完成多边形绘制
  const finishPolygon = (opt) => {
    let points = canvasPolygon.current.points;

    points[points.length - 1].x = opt.absolutePointer.x;
    points[points.length - 1].y = opt.absolutePointer.y;

    points.pop();
    points.pop();

    // console.log("绘制完了2", points);

    canvas.current.remove(canvasPolygon.current);
    if (points.length > 2) {
      const data = [
        {
          id: uuid(8, 10),
          points: points,
        },
      ];
      generatePolygon(data);
    } else {
      message.warning("请绘制至少三个点!!!");
    }
    canvasPolygon.current = null;
    canvas.current.requestRenderAll();
  };
  // 生成多边形并绑定事件
  const generatePolygon = (data) => {
    if (!data || !data.length) {
      return;
    }
    // 以数组为基础,渲染框
    data.forEach((v) => {
      // 创建多边形
      const polygon = new fabric.Polygon(v.points, {
        id: v.id,
        stroke: "#409eff", //渲染边框色
        fill: "rgba(255, 255, 255, 0.2)", //背景色
        selectable: false, //禁用位置及大小修改
        hoverCursor: "move", //鼠标移入显示拖拽位置手势
        transparentCorners: false, //操作顶点填充
        padding: 0, // 多边形距离外侧基准框距离
        cornerStyle: "circle", //控件类型(圆)
        cornerColor: "#409eff", //控件颜色(蓝色)
        hasBorders: false, //不显示控件边框
        objectCaching: false, //不使用缓存,此属性如为true,则拖拽顶点时边框不会实时更新边框位置
      });
      // 添加多边形控件
      const lastControl = polygon.points.length - 1;
      // 拖拽大小控件
      polygon.controls = polygon.points.reduce(function (acc, point, index) {
        acc["p" + index] = new fabric.Control({
          positionHandler: polygonPositionHandler,
          actionHandler: anchorWrapper(index > 0 ? index - 1 : lastControl, actionHandler),
          actionName: "modifyPolygon",
          pointIndex: index,
        });
        return acc;
      }, {});
      // 删除控件
      polygon.controls.deleteControl = new fabric.Control({
        x: 0.5,
        y: -0.5,
        offsetY: -16,
        offsetX: 16,
        cornerSize: 24, //图标大小
        cursorStyle: "pointer", // 移入鼠标样式
        mouseUpHandler: deleteObject, // 点击鼠标抬起事件
        render: deleteRenderIcon, //渲染红色叉号
      });

      canvas.current.add(polygon);
    });

    // 刷新画布
    canvas.current.requestRenderAll();
  };
  // 多边形位置拖动
  const dragPllygon = (options) => {
    const polygon = options.target;
    const { id } = polygon;
    // 获取最新值
    const matrix = polygon.calcTransformMatrix();
    const newPoints = polygon
      .get("points")
      .map(function (p) {
        return new fabric.Point(p.x - polygon.pathOffset.x, p.y - polygon.pathOffset.y);
      })
      .map(function (p) {
        return fabric.util.transformPoint(p, matrix);
      })
      .map((h) => {
        return {
          x: h.x < 0 ? 0 : h.x,
          y: h.y < 0 ? 0 : h.y,
        };
      });

    // console.log("拖完了", newPoints);

    // 修改位置数据
    const mapList = cloneDeep(pointList.current).map((v) => {
      return {
        ...v,
        points: v.id === id ? newPoints : v.points,
      };
    });

    const uniByList = uniqBy(mapList, "id");

    pointList.current = uniByList;
  };
  // 删除图标
  const deleteRenderIcon = (ctx, left, top, styleOverride, fabricObject) => {
    const img = document.createElement("img");
    const size = 24;

    img.src = deleteIcon;

    ctx.save();
    ctx.translate(left, top);
    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
    ctx.drawImage(img, -size / 2, -size / 2, size, size);
    ctx.restore();
  };
  // 以ID为唯一标识,删除框
  const deleteObject = (eventData, transform) => {
    let target = transform.target;
    let canvas = target.canvas;
    const { id } = target;

    // 更新数据
    const mapList = cloneDeep(pointList.current).filter((v) => v.id !== id);
    pointList.current = mapList;

    // 删除元素,带动画
    canvas.fxRemove(target, {
      onChange() {
        // console.log("在动画的每一步调用");
      },
      onComplete() {
        // console.log("删除成功后调用");
      },
    });

    // canvas.requestRenderAll();
  };
  // 图片加载完毕
  const imgLoad = (e) => {
    e.persist();
    const { offsetWidth, offsetHeight } = e.target;
    setBoxStyle({
      width: offsetWidth,
      height: offsetHeight,
    });
  };

  // 提交
  const handleSubmit = async () => {
    const { width, height } = boxStyle;

    const requestData = {
      deviceId: editData.id,
      deviceEventRsList: cloneDeep(list).map((v) => {
        const mapList = v.isService
          ? v.relationList
          : v.relationList.map((k) => {
              return {
                ...k,
                points: k.points.map((h) => {
                  return { x: h.x / width, y: h.y / height };
                }),
              };
            });

        return {
          deviceId: editData.id,
          eventTypeId: v.id,
          state: v.state ? 1 : 0,
          relationJson: JSON.stringify(mapList),
        };
      }),
    };

    // console.log("request", requestData);

    const res = await deviceDao.saveDeviceEventRs({ data: requestData });

    if (res && res.status.code === 200) {
      message.success("保存成功");

      handleCancel(); //清除数据
    } else {
      message.error("保存失败,请重试");
    }
  };
  // 关闭弹窗
  const handleCancel = () => {
    // 销毁state
    canvas.current == null;
    canvasPolygon.current = null;
    pointList.current = null;
    setAllowDraw(false);
    setImgUrl(null);
    setBoxStyle({ width: 0, height: 0 });
    // 关闭弹窗
    setOpen(false);
  };

  // 左侧列表render
  const listRender = (v) => {
    return (
      <div className="left-render">
        <div className="item-left">
          <p title={v.label}>{v.label}</p>
        </div>
        <div className="item-right">
          <Tooltip title="启用/禁用">
            <Switch
              size="small"
              checked={v.state}
              onChange={(checked) => {
                enable(v.id, checked);
              }}
            />
          </Tooltip>
          <Tooltip title="保存">
            <Button
              type="primary"
              shape="circle"
              icon={<CheckOutlined />}
              disabled={!allowDraw}
              onClick={(e) => {
                save(e, v);
              }}
            />
          </Tooltip>
        </div>
      </div>
    );
  };

  return (
    <Modal
      title="事件配置"
      className="analysis"
      width="100vw"
      style={{
        maxWidth: "100vw",
        top: 0,
        paddingBottom: 0,
        margin: 0,
      }}
      bodyStyle={{
        height: "calc(100vh - 108px )",
        overflowY: "auto",
      }}
      maskClosable={false}
      keyboard={false}
      visible={open}
      destroyOnClose
      okText="确定"
      cancelText="取消"
      onOk={handleSubmit}
      onCancel={handleCancel}
    >
      <div className="content">
        <div className="content-top">
          <List title="事件类型" filterable renderList={listRender} active={listActive} data={list} onChange={slideChange}></List>
        </div>
        <div className="content-bottom-box">
          <div className="content-bottom">
            <img src={imgUrl} ref={imgRef} alt="" onLoad={imgLoad} />
            <div className="canvas-box" style={boxStyle}>
              <canvas id="canvasRef" width={boxStyle.width} height={boxStyle.height} />
            </div>
          </div>
          {/* 图片无效则不允许画框 */}
          {(!allowDraw || !list.length) && (
            <div className="no-img" style={{ height: boxStyle.height ? boxStyle.height : "100%" }}>
              <img src={noCheckedEvent} alt="" />
              <p>{!list.length ? "暂无事件类型" : "暂无设备图片"}</p>
            </div>
          )}
        </div>
      </div>
    </Modal>
  );
};
export default forwardRef(Analysis);


2、fabric.js文件(本地文件,放官网方法,本地新建fabric.js直接copy代码放进去即可)
import { fabric } from "fabric";

// 大小修改,来自官网方法
export function polygonPositionHandler(dim, finalMatrix, fabricObject) {
  var x = fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x,
    y = fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y;
  return fabric.util.transformPoint(
    { x: x, y: y },
    fabric.util.multiplyTransformMatrices(fabricObject.canvas.viewportTransform, fabricObject.calcTransformMatrix())
  );
}
export function actionHandler(eventData, transform, x, y) {
  var polygon = transform.target,
    currentControl = polygon.controls[polygon.__corner],
    mouseLocalPosition = polygon.toLocalPoint(new fabric.Point(x, y), "center", "center"),
    polygonBaseSize = getObjectSizeWithStroke(polygon),
    size = polygon._getTransformedDimensions(0, 0),
    finalPointPosition = {
      x: (mouseLocalPosition.x * polygonBaseSize.x) / size.x + polygon.pathOffset.x,
      y: (mouseLocalPosition.y * polygonBaseSize.y) / size.y + polygon.pathOffset.y,
    };
  polygon.points[currentControl.pointIndex] = finalPointPosition;
  return true;
}
export function getObjectSizeWithStroke(object) {
  var stroke = new fabric.Point(object.strokeUniform ? 1 / object.scaleX : 1, object.strokeUniform ? 1 / object.scaleY : 1).multiply(object.strokeWidth);
  return new fabric.Point(object.width + stroke.x, object.height + stroke.y);
}

export function anchorWrapper(anchorIndex, fn) {
  return function (eventData, transform, x, y) {
    var fabricObject = transform.target,
      absolutePoint = fabric.util.transformPoint(
        {
          x: fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x,
          y: fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y,
        },
        fabricObject.calcTransformMatrix()
      ),
      actionPerformed = fn(eventData, transform, x, y),
      newDim = fabricObject._setPositionDimensions({}),
      polygonBaseSize = getObjectSizeWithStroke(fabricObject),
      newX = (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) / polygonBaseSize.x,
      newY = (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) / polygonBaseSize.y;

    fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);

    return actionPerformed;
  };
}

// 边界处理,防止拖出画布
export function onMoving(e) {
  let padding = 0; // 内容距离画布的空白宽度,主动设置
  var obj = e.target;
  if (obj.currentHeight > obj.canvas.height - padding * 2 || obj.currentWidth > obj.canvas.width - padding * 2) {
    return;
  }
  obj.setCoords();
  if (obj.getBoundingRect().top < padding || obj.getBoundingRect().left < padding) {
    obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top + padding);
    obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left + padding);
  }
  if (
    obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height - padding ||
    obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width - padding
  ) {
    obj.top = Math.min(obj.top, obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top - padding);
    obj.left = Math.min(obj.left, obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left - padding);
  }
}

3、less文件(注意下 .canvas-box 的z-index需大于img的z-index就行,盖在img上边)
.analysis {
  -webkit-user-select: none; /* Safari 3.1+ */
  -moz-user-select: none; /* Firefox 2+ */
  -ms-user-select: none; /* IE10+ */
  user-select: none; //不允许页面选中
  .ant-modal-body {
    padding: 12px;
    .content {
      width: 100%;
      height: 100%;
      .content-top {
        width: 100%;
        height: 50px;
        border-bottom: 1px solid #ebeef5;
        display: flex;
        align-items: center;
        p {
          width: 100px;
          height: 100%;
          font-size: 16px;
          font-weight: bold;
          padding-left: 16px;
          display: flex;
          align-items: center;
        }
        .ant-checkbox-group {
          margin-left: 14px;
        }
      }
      .content-bottom {
        width: 100%;
        height: calc(100% - 50px);
        position: relative;
        img {
          max-width: 100%;
        }
        .canvas-box {
          position: absolute;
          left: 0;
          top: 0;
          z-index: 10;
        }
      }
    }
  }
}

三、效果

React hooks 使用fabric.js 绘制多边形


总结

1、功能:手动画多边形,默认渲染回显多边形,获取到移动点和框位置后的最新坐标,点击编辑,删除,边界处理(不能拖到img/画布 外边)右键撤销上一次绘制,双击结束绘制等
2、代码都有通俗的注释,目的是为了大家都看得懂,官网是英文文档,看起来真难受
3、如果你是画圆或者矩形,思路差不多,只不过不用polygon,用Rect
4、fabric.js 使用思路 和openlayers 差不多,都是千层饼一层层摞上去的概念
5、如果需要手动拖动绘制矩形并框选区域,并获取坐标点,不想用fabric.js的话,请移步我另一个文章: React Hooks 手动绘制矩形
6、如有问题,欢迎评论区讨论,大家早下班,打游戏不好吗?

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
React Fabric.js是一个用于在React应用中使用Fabric.js库的封装。Fabric.js是一个强大的JavaScript库,用于创建和操作基于HTML5 Canvas的图形对象。通过使用React Fabric.js,您可以轻松地在React应用中创建和操作图形对象,如圆形、矩形、文本等。 要在React应用中使用React Fabric.js,您需要引入Fabric.jsReact Fabric.js的依赖,并在应用程序中创建相应的组件。首先,您需要在您的HTML文件中引入Fabric.js的库文件。然后,您可以使用React的组件生命周期方法,比如componentDidMount,来初始化Fabric.js并在Canvas上绘制图形对象。您还可以使用React的状态管理来更新图形对象的属性。 在React Fabric.js中,您可以使用Fabric.js的API来创建和操作图形对象。例如,您可以使用Fabric.js的Circle类来创建一个圆形对象,并设置其属性,如半径、位置和颜色。然后,您可以将该圆形对象添加到Canvas上,并在React组件中进行渲染。 下面是一个简单的示例代码,展示了如何在React应用中使用React Fabric.js来创建一个圆形对象: ```jsx import React, { Component } from 'react'; import { fabric } from 'fabric'; import './App.css'; class App extends Component { componentDidMount() { const canvas = new fabric.Canvas('canvas'); const circle = new fabric.Circle({ radius: 50, left: 100, top: 100, fill: 'red', }); canvas.add(circle); } render() { return ( <div className="App"> <canvas id="canvas" width={500} height={500}></canvas> </div> ); } } export default App; ``` 在上面的代码中,我们在组件的componentDidMount方法中初始化了Fabric.js的Canvas,并创建了一个半径为50、位于(100,100)的红色圆形对象。然后,我们将该圆形对象添加到Canvas上,并在组件的render方法中渲染Canvas。 通过使用React Fabric.js,您可以方便地在React应用中创建和操作图形对象,实现丰富的可视化效果和交互性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值