概念:啥是热区组件
是指在一张图片上选取一些区域,每个区域链接到指定的地址。可以在图片上设置多个热区区域并配置相应的数据。
在正式开始之前,先了解几个概念
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