天天看點

React 熱區元件

React 熱區元件

概念:啥是熱區元件

是指在一張圖檔上選取一些區域,每個區域連結到指定的位址。可以在圖檔上設定多個熱區區域并配置相應的資料。

在正式開始之前,先了解幾個概念

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