概念:啥是熱區元件
是指在一張圖檔上選取一些區域,每個區域連結到指定的位址。可以在圖檔上設定多個熱區區域并配置相應的資料。
在正式開始之前,先了解幾個概念
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