React 热区组件

本文介绍了如何使用 HTML `<area>` 和 `<map>` 元素创建图片热区,并展示了如何在 React 应用中动态框选区域以创建热区。通过 `<react-multi-crops>` 组件,可以实现用户交互式的热区编辑,同时利用 canvas 技术进行裁剪框的绘制。最后,文章提供了一个完整的示例,展示了一个动态创建和编辑热区的 React 组件。
摘要由CSDN通过智能技术生成

在这里插入图片描述

概念:啥是热区组件

是指在一张图片上选取一些区域,每个区域链接到指定的地址。可以在图片上设置多个热区区域并配置相应的数据。

在正式开始之前,先了解几个概念

HTML <area> 元素 在图片上定义一个热点区域,可以关联一个超链接。元素仅在元素内部使用。

了解更多:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/area

HTML <map> 属性 与 <area> 属性一起使用来定义一个图像映射(一个可点击的链接区域)。

了解更多:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/map

基本的热区实现

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <img src="https://cdn.pixabay.com/photo/2021/07/16/18/28/czech-republic-6471576__480.jpg" usemap="#planetmap"
    alt="Planets" />

  <map name="planetmap" id="planetmap">
    <area shape="rect" coords="0,0,110,260" href="https://baidu.com" target="_blank" alt="Venus" />
    <area shape="rect" coords="110,260,210,360" href="https://taobao.com" target="_blank" alt="Mercury" />
    <area shape="rect" coords="210,360,410,410" href="https://qq.com" target="_blank" alt="Sun" />
  </map>
</body>

</html>

上面的区域是我们提前在代码中编写好的,但我们更加希望可以动态的框选区域。

注:以下使用 React 框架作为演示

React 实现框选区域成为热区

分析:其实我们只需要知道 x, y, width, height ,就可以绘制热区。

/* eslint-disable no-template-curly-in-string */
import React from 'react'
import MultiCrops from 'react-multi-crops'
import img from './test.webp'

class App extends React.Component {
  state = {
    coordinates: [],
    x: 0,
    y: 0,
    width: 0,
    height: 0,
    str: '',
    imgShow: false
  }

  changeCoordinate = (coordinate, index, coordinates) => {
    this.setState({
      coordinates,
    })
    // 在变化的时候,拿到此刻裁剪区域的数据
    setTimeout(() => {
      let {x, y, width, height} = this.state.coordinates[0]
      this.setState({
        str: `${x},${y},${width},${height}`
      })
      this.setState({
        imgShow: true
      })
    }, 1000)
  }
  deleteCoordinate = (coordinate, index, coordinates) => {
    this.setState({
      coordinates,
    })
  }

  render() {
    const { str, imgShow } = this.state
    console.log(str)
    return (
      <div>
        {
          imgShow ? <img src={img} useMap="#planetmap" alt="Planets" /> : <MultiCrops
          src={img}
          coordinates={this.state.coordinates}
          onChange={this.changeCoordinate}
          onDelete={this.deleteCoordinate}
        />
        }
        <map name="planetmap" id="planetmap">
          <area shape="rect" coords={str} href="https://baidu.com" target="_blank" alt="Venus" />
        </map>
      </div>
    )
  }
}

export default App;

本着 no zuo no die 的精神,可以继续研究一下,裁剪框是怎么形成的。这里主要用到了 canvas 技术。

绘制裁剪框

import React, { useState, useEffect } from "react";

const ImgCrop = ({ file }) => {
  const { url } = file;
  const [originImg, setOriginImg] = useState(); // 源图片
  const [contentNode, setContentNode] = useState(); // 最外层节点
  const [canvasNode, setCanvasNode] = useState(); // canvas节点
  const [startCoordinate, setStartCoordinate] = useState([0, 0]); // 开始坐标
  const [dragging, setDragging] = useState(false); // 是否可以裁剪
  const [curPoisition, setCurPoisition] = useState(null); // 当前裁剪框坐标信息
  const [hotAreaCoordStr, setHotAreaCootdStr] = useState("");
  const [imgShow, setImgShow] = useState(false);

  // 初始化
  const initCanvas = () => {
    // url为上传的图片链接
    if (url == null) {
      return;
    }
    // contentNode为最外层DOM节点
    if (contentNode == null) {
      return;
    }
    // canvasNode为canvas节点
    if (canvasNode == null) {
      return;
    }

    const image = new Image();
    setOriginImg(image); // 保存源图
    image.addEventListener("load", () => {
      const ctx = canvasNode.getContext("2d");
      // 擦除一次,否则canvas会一层层叠加,节省内存
      ctx.clearRect(0, 0, canvasNode.width, canvasNode.height);
      // 若源图宽度大于最外层节点的clientWidth,则设置canvas宽为clientWidth,否则设置为图片的宽度
      const clientW = contentNode.clientWidth;
      const size = image.width / clientW;
      if (image.width > clientW) {
        canvasNode.width = clientW;
        canvasNode.height = image.height / size;
      } else {
        canvasNode.width = image.width;
        canvasNode.height = image.height;
      }
      // 调用drawImage API将版面图绘制出来
      ctx.drawImage(image, 0, 0, canvasNode.width, canvasNode.height);
    });
    image.crossOrigin = "anonymous"; // 解决图片跨域问题
    image.src = url;
  };

  useEffect(() => {
    initCanvas();
  }, [canvasNode, url]);

  // 点击鼠标事件
  const handleMouseDownEvent = (e) => {
    // 开始裁剪
    setDragging(true);
    const { offsetX, offsetY } = e.nativeEvent;
    // 保存开始坐标
    setStartCoordinate([offsetX, offsetY]);
  };

  // 移动鼠标事件
  const handleMouseMoveEvent = (e) => {
    if (!dragging) {
      return;
    }
    const ctx = canvasNode.getContext("2d");
    // 每一帧都需要清除画布(取最后一帧绘图状态, 否则状态会累加)
    ctx.clearRect(0, 0, canvasNode.width, canvasNode.height);

    const { offsetX, offsetY } = e.nativeEvent;

    // 计算临时裁剪框的宽高
    const tempWidth = offsetX - startCoordinate[0];
    const tempHeight = offsetY - startCoordinate[1];
    // 调用绘制裁剪框的方法
    drawTrim(startCoordinate[0], startCoordinate[1], tempWidth, tempHeight);
  };

  // 松开鼠标
  const handleMouseRemoveEvent = () => {
    // 结束裁剪
    setDragging(false);

    // 处理裁剪按钮样式
    if (curPoisition == null) {
      return;
    }

    const { startX, startY, width, height } = curPoisition;
    setHotAreaCootdStr(`${startX},${startY},${width},${height}`);
    setImgShow(true);
  };

  // 绘制裁剪框的方法
  const drawTrim = (x, y, w, h) => {
    const ctx = canvasNode.getContext("2d");

    // 绘制蒙层
    ctx.save();
    ctx.fillStyle = "rgba(0,0,0,0.6)"; // 蒙层颜色
    ctx.fillRect(0, 0, canvasNode.width, canvasNode.height);

    // 将蒙层凿开
    ctx.globalCompositeOperation = "source-atop";
    // 裁剪选择框
    ctx.clearRect(x, y, w, h);

    // 绘制8个边框像素点
    ctx.globalCompositeOperation = "source-over";
    drawBorderPixel(ctx, x, y, w, h);

    // 保存当前区域坐标信息
    setCurPoisition({
      width: w,
      height: h,
      startX: x,
      startY: y,
      position: [
        (x, y),
        (x + w, y),
        (x, y + h),
        (x + w, y + h),
        (x + w / 2, y),
        (x + w / 2, y + h),
        (x, y + h / 2),
        (x + w, y + h / 2),
      ],
      canvasWidth: canvasNode.width, // 用于计算移动端版面图缩放比例
    });

    ctx.restore();

    // 再次调用drawImage将图片绘制到蒙层下方
    ctx.save();
    ctx.globalCompositeOperation = "destination-over";
    ctx.drawImage(originImg, 0, 0, canvasNode.width, canvasNode.height);
    ctx.restore();
  };

  // 绘制边框像素点的方法
  const drawBorderPixel = (ctx, x, y, w, h) => {
    ctx.fillStyle = "#f5222d";
    const size = 5; // 自定义像素点大小
    ctx.fillRect(x - size / 2, y - size / 2, size, size);
    // ...同理通过ctx.fillRect再画出其余像素点
    ctx.fillRect(x + w - size / 2, y - size / 2, size, size);
    ctx.fillRect(x - size / 2, y + h - size / 2, size, size);
    ctx.fillRect(x + w - size / 2, y + h - size / 2, size, size);

    ctx.fillRect(x + w / 2 - size / 2, y - size / 2, size, size);
    ctx.fillRect(x + w / 2 - size / 2, y + h - size / 2, size, size);
    ctx.fillRect(x - size / 2, y + h / 2 - size / 2, size, size);
    ctx.fillRect(x + w - size / 2, y + h / 2 - size / 2, size, size);
  };

  return (
    <>
      {imgShow ? (
        <img src={url} useMap="#planetmap" width={`${curPoisition.canvasWidth}`} alt="Planets" />
      ) : (
        <section ref={setContentNode}>
          <canvas
            ref={setCanvasNode}
            onMouseDown={handleMouseDownEvent}
            onMouseMove={handleMouseMoveEvent}
            onMouseUp={handleMouseRemoveEvent}
          />
        </section>
      )}
      <map name="planetmap" id="planetmap">
        <area
          shape="rect"
          coords={hotAreaCoordStr}
          href="https://baidu.com"
          target="_blank"
          alt="Venus"
        />
      </map>
    </>
  );
};

function App() {
  let obj = {
    file: {
      url: "https://cdn.pixabay.com/photo/2021/07/18/14/59/family-6475821__480.jpg",
    },
  };
  return <ImgCrop {...obj} />;
}

export default App;

参考文章:https://segmentfault.com/a/1190000022285488

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值