搞复古风网页设计的时候,总想整点那种点一下图片就能跳转的骚操作,像老早以前那些卡通风格导航栏。这就要聊到图像映射(image maps)这个老伙计了,它能让一张图里不同区域变成可点击的链接,比如地图上点不同城市弹出介绍。但这玩意儿搁现在用起来有没有坑?下面直接上实操,从翻车到真香全程拆解。
啥是图像映射
图像映射是HTML 3.2时代就存在的标签组合,核心三件套:<map>、<area>和图片的usemap属性。<map>像一张透明画布,里面用<area>圈出一个个可点区域,每个区域可以设置形状(圆形、矩形、多边形)和坐标。举个栗子,一张1024×1024的地图图片,想让左上角半径35像素的圆形区域变成按钮,坐标就是35,35,35(圆心x,圆心y,半径)。<area>的shape属性支持circle、rect、poly三种,多边形需要一串坐标点。
| 形状 | shape值 | coords示例 | 说明 |
|---|---|---|---|
| 圆形 | circle | 35,35,35 | 圆心x,圆心y,半径 |
| 矩形 | rect | 10,10,50,50 | 左上x,左上y,右下x,右下y |
| 多边形 | poly | 10,10,50,20,40,50 | 每个点的x,y依次排列 |
使用图像映射时,图片要加上usemap="#地图名",<map>的name属性跟这个对应。代码长这样:
<img src="world-map.png" usemap="#clickable-map" alt="世界地图">
<map name="clickable-map">
<area shape="circle" coords="120,80,30" href="/asia" alt="亚洲">
<area shape="rect" coords="300,150,450,250" href="/europe" alt="欧洲">
</map>传统方案实操
拿一个带编号圆点的项目地图来举例,需求是点击不同编号弹出对应项目详情。传统图像映射上手最快,但有几个暗坑需要提前知道。
第一步:准备图片和生成坐标
先有一张完整的项目地图图片,比如projects.svg。用在线工具(搜索“image map generator”)上传图片,然后手动在图上画圈圈——工具界面一般会显示图片,鼠标拖拽就能画矩形或圆形。画完每个区域后,工具会自动生成coords数值。把生成的<map>代码复制出来,放进HTML里。如果图片尺寸是固定的,这一步就完事了。但要是图片宽度在不同屏幕会缩放,坐标就会错位,因为coords里存的是绝对像素值,图片一缩就全乱套。
第二步:写JS让坐标跟着图片缩放
需要一段脚本来实时重算坐标。原理是监听窗口加载和缩放事件,获取图片当前显示宽度和原始宽度的比例,然后把每个<area>的原始坐标乘以这个比例。注意一个细节:必须在页面加载完图片自然宽高之后才能计算,否则拿不到原始尺寸。另外每次缩放都重新计算,性能开销不大,但不要忘记在窗口resize时用防抖,不然拖拽窗口边缘会疯狂触发。代码可以这样写(变量名改过):
function refreshMapCoords() {
const targetImg = document.getElementById("project-map");
const linkedMap = document.querySelector("map[name='project-areas']");
if (!targetImg || !linkedMap || !targetImg.naturalWidth) return;
const scaleRate = targetImg.clientWidth / targetImg.naturalWidth;
linkedMap.querySelectorAll("area").forEach(area => {
if (!area.dataset.rawCoords) {
area.dataset.rawCoords = area.getAttribute("coords");
}
const newCoords = area.dataset.rawCoords.split(",")
.map(coord => Math.round(parseFloat(coord) * scaleRate))
.join(",");
area.setAttribute("coords", newCoords);
});
}
window.addEventListener("load", refreshMapCoords);
window.addEventListener("resize", () => {
setTimeout(refreshMapCoords, 100);
});这里有个容易翻车的地方:如果图片原始宽度是0(比如图片没加载完),naturalWidth就是0,除法直接报错。所以前面加了个判断,没加载好就退出。另外setTimeout是为了等浏览器渲染完再计算,直接同步调用可能拿到旧的尺寸。
第三步:处理不规则形状的点击区域
地图上的点击区域不一定是圆形或矩形,比如某个国家边界线弯弯曲曲。这时候要用shape="poly",坐标是一长串x1,y1,x2,y2,…的序列。获取SVG路径的坐标点很麻烦,可以借助PathToPoints这类工具:上传SVG文件,设定点密度,工具会把<path>的d属性转换成一组多边形顶点。把得到的坐标串直接粘到coords里就行。但注意多边形坐标点越多,HTML文件越臃肿,而且JS缩放时计算量也会变大。建议坐标点控制在30对以内,超出的话可以简化路径。
SVG内联方案
传统图像映射最大的硬伤是鼠标悬停时没有任何视觉反馈——光标变小手而已,不能加高亮、不能加动画。想要那种hover时圆圈发光、弹出预览图的效果,就得换路子:用内联SVG加透明可点区域。
第一步:把SVG代码直接写进HTML
不再用<img src="...">外链,而是把整个SVG标签贴到页面里。这样做的好处是所有内部元素都能用CSS控制。先放一个SVG容器,viewBox设为0 0 1024 1024,里面先画上可见的背景、地图线条、那些带编号的圆圈。
第二步:创建更大的隐形点击路径
原图的点击区域只是小圆圈,但想要点整个片区都能触发。那就新建一批<path>,每个path覆盖一大片区域(比如某个国家的轮廓),并且填充颜色跟背景一致或者干脆透明。把这些新path放到SVG源码的最后面,确保它们叠在其他可见元素之上(因为后面画的元素在上层)。每个<path>外面套一个<a>标签:
<svg viewBox="0 0 1024 1024">
<!-- 可见内容:地图线条、编号圆圈 -->
<g id="visible-layer">
<circle cx="120" cy="80" r="30" fill="#941B2F"/>
<text x="120" y="85" fill="#FFF">1</text>
</g>
<!-- 可点区域:透明大路径 -->
<g id="clickable-layer">
<a href="/project1">
<path fill="#B48F4C" d="M100,50 L200,50 L180,120 L90,110 Z"/>
</a>
<a href="/project2">
<path fill="#6FA676" d="M250,60 L350,40 L340,130 L240,140 Z"/>
</a>
</g>
</svg>第三步:用CSS控制悬停反馈
设置#clickable-layer a path的透明度为0,这样平时看不见这些区域。然后加一个过渡效果,鼠标悬停时透明度变成0.3或者1,同时可以添加滤镜阴影或者改变相邻元素的样式。比如让对应的编号圆圈在悬停时放大:
#clickable-layer a path {
opacity: 0;
transition: opacity 0.2s ease;
}
#clickable-layer a:hover path {
opacity: 0.4;
fill: gold;
}
/* 悬停时让可见层里对应编号圆圈发光 */
#clickable-layer a:hover ~ #visible-layer circle {
stroke: #ffcc00;
stroke-width: 4;
}注意那个~选择器只在兄弟元素有效,如果结构层级不同,可以用JS加类名。或者干脆把编号圆圈也包进同一个<a>里,但那样点圆圈区域也会触发,倒是更符合直觉。如果想让悬停时弹出一个预览小窗,可以在<a>里塞一个隐藏的<image>或<text>,平时透明度0,hover时显示出来。
第四步:解决坐标和响应式
因为用的是SVG的viewBox,坐标本身就是相对值,不需要JS去重新计算。SVG会按比例自动缩放,所有<path>的d属性里的坐标都是基于viewBox的,完美适配不同屏幕。但要注意viewBox的宽高比必须跟实际渲染宽度一致,否则会拉伸。设置SVG的width="100%"和height="auto",再配合preserveAspectRatio="xMidYMid meet",就能像一张图片那样自适应。
这个方法也有一个坑:<a>包裹<path>在部分旧版浏览器(比如IE)里可能没反应,但现代移动端和桌面浏览器都支持。另外SVG内联会让HTML代码变长,建议把SVG单独放到一个文件里,用服务器端包含(SSI)或前端fetch加载,但要注意这样就不能直接用CSS控制内部元素了。折中方案是用<object>引入SVG,然后通过contentDocument操作内部样式,比较折腾,不如直接内联省心。
两种方案怎么选
| 对比项 | 传统图像映射 | 内联SVG方案 |
|---|---|---|
| 上手速度 | 快,工具生成坐标 | 慢,需手画路径 |
| 悬停反馈 | 无,只变光标 | 完全可控,任意CSS |
| 响应式 | 需要JS重算 | 原生支持 |
| 不规则区域 | 支持多边形,坐标多 | 支持路径,更灵活 |
| 代码体积 | 小 | 大(内联SVG) |
如果项目赶时间、点击区域都是规整的圆或方块、不需要任何花哨反馈,直接怼图像映射加一段JS缩放代码就行。但如果想要那种老式Flash网站一样的悬停音效感、区域发光、图片预览,那就乖乖用内联SVG方案。实际操作中,可以先用图像映射快速搭个原型,等设计师确认完区域位置后,再转成SVG内联慢慢打磨动效。
