记·在React中手写热区组件的一周

热区,一个让人有爱又恨的狗东西。是时候搞定它了。

需求给了一周的时间,心想这玩意还用一周?直到...

任务需求

  1. React + Ts实现热区
  2. 热区可以拖拽移动
  3. 热区可以放大缩小
  4. 热区不允许重叠
  5. B端创建的热区要在移动端展示
  6. 不允许有误差

Day-01

浏览各个论坛,看看各位大佬们都用的什么插件,穿梭在githubstackoverflowjuejinzhihu的知识海洋中...

搜索到很多很多看起来"完美"的插件...

当然还有纯原生实现的...

2024年了,我最大的优点就是懒。造轮子?死也不可能造!

一杯茶,一根烟,一个Bug改一天,Day1就这么过去了。

Day-02-AM

打开掘金,和JYM吹吹牛逼,点上那么几个小赞,开始新一天的工(摸)作(鱼)生活。

继续畅游在知识的海洋中,寻找大佬造好的轮子。

突然,产品经理来袭,say:上线提前,最好明天下班前提测

心里一句国粹,腿开始不自觉抖动...

干饭先。

Day-02-PM

轮子?批轮!开造!

需求梳理

热区?

  • 在一张图片上鼠标画个框。
  • 框的信息可以获取这个框起点的坐标(x, y),宽高(w, h)
  • 这个框可以移动,缩放
  • 收集移动缩放后框的信息
  • 移动端绘制这个框

思考?

  • 这个框画在哪?
  • 框肯定不能超出图片吧?(边界计算)
  • 怎么让框移动,缩放?
  • 如何返回框的信息?
  • 多个框怎么计算重叠?

画个框

两种方案:

  1. 原生
  2. 使用fabric

原生

  1. div画个框放张背景图
  2. 绝对定位8个点 上下左右,左上左下,右上右下
  3. 使用cursor
arduino 复制代码
[
    {
      style:
        'position: absolute; user-select: none; width: 100%; height: 10px; top: -5px; left: 0px; cursor: row-resize;',
      position: 'top',
    },
    {
      style:
        'position: absolute; user-select: none; width: 10px; height: 100%; top: 0px; right: -5px; cursor: col-resize;',
      position: 'right',
    },
    {
      style:
        'position: absolute; user-select: none; width: 100%; height: 10px; bottom: -5px; left: 0px; cursor: row-resize;',
      position: 'bottom',
    },
    {
      style:
        'position: absolute; user-select: none; width: 10px; height: 100%; top: 0px; left: -5px; cursor: col-resize;',
      position: 'left',
    },
    {
      style:
        'position: absolute; user-select: none; width: 20px; height: 20px; right: -10px; top: -10px; cursor: ne-resize;',
      position: 'topRight',
    },
    {
      style:
        'position: absolute; user-select: none; width: 20px; height: 20px; right: -10px; bottom: -10px; cursor: se-resize;',
      position: 'bottomRight',
    },
    {
      style:
        'position: absolute; user-select: none; width: 20px; height: 20px; left: -10px; bottom: -10px; cursor: sw-resize;',
      position: 'bottomLeft',
    },
    {
      style:
        'position: absolute; user-select: none; width: 20px; height: 20px; left: -10px; top: -10px; cursor: nw-resize;',
      position: 'topLeft',
    },
]
  1. 使用onmousemove,onmouseup实现画框缩放
  2. 使用drag实现移动

写了点,写不下去了。吐了, 什么狗东西。dogthing...

最后开始忍着恶心写完了,感兴趣私信我,我发你...

羞耻到不敢贴出...

fabricjs

我们先来看看官方的定义:

Fabric.js is a framework that makes it easy to work with HTML5 canvas element. It is an interactive object model on top of canvas element. It is also an SVG-to-canvas parser. Fabric.js 是一个可以让 HTML5 Canvas 开发变得简单的框架 。 它是一种基于 Canvas 元素的 可交互 对象模型,也是一个 SVG 到 Canvas 的解 析器(让SVG 渲染到 Canvas 上)。

从它的官方定义可以看出来,它是一个用 Canvas 实现的对象模型。如果你需要用 HTML Canvas 来绘制一些东西,并且这些东西可以响应用户的交互,比如:拖动、变形、旋转等 操作。 那用 fabric.js 是非常合适的,因为它内部不仅实现了 Canvas 对象模型,还将一 些常用的交互操作封装好了,可以说是开箱即用。

内部集成的主要功能如下:

  • 几何图形绘制,如:形状(圆形、方形、三角形)、路径
  • 位图加载、滤镜
  • 自由画笔工具,笔刷
  • 文本、富文本渲染
  • 模式图像
  • 对象动画
  • Canvas 对象之间的序列化与反序列化

这不是撞枪口上了吗!这不是!

Canvas 开发原理

如果你之前没有过 Canvas 的相关开发经验(只有 JavaScript 网页开发经验),刚开始 入 门会觉得不好懂,不理解 Canvas 开发的逻辑。这个很正常,因为这表示你正在从传统 的 JavaScript 开发转到图形图像 GUI 图形图像、动画开发。 虽然语言都是 JavaScript 但是开发理念和用到的编程范式完全不同。

传统的客户端 JavaScript 开发一般可以认为是 事件驱动的编程模型 (Event-driven programming),这个时候你需要关注事件的触发者和监听者 Canvas 开发通常是 面向对象的编程模型,需要把绘制的物体抽象为对象,通过对 象的方法维护自身的属性,通常会使用一个全局的事件总线来处理对象之间的交互 这两种开发方式各有各的优势,比如:

有的功能在 HTML 里一行代码就能实现的功能放到 Canvas 中需要成千行的代码去实现。 比如:textarea, contenteditable 相反,有的功能在 Canvas 里面只需要一行代码实现的,使用 HTML 却几乎无法实现。比 如:截图、录制

愉快的一天结束了,还没开始写...
加班?批班!

Day-03

撸起袖子加油干

初始化和状态管理

js 复制代码
const canvasRef = useRef<any>(null);
const [canvas, setCanvas] = useState<any>(null);
const imgWidth = useRef(0);
const imgHeight = useRef(0);
const canvasWidth = useRef(0);
const canvasHeight = useRef(0);
const currentSelection = useRef({
startX: 0,
startY: 0,
});
const [selectedRectId, setSelectedRectId] = useState('');
// 将热区配置数据映射为编辑组件的内部状态
const [rectConfig, setRectConfig] = useState(
data.hotZoneList.map((config) => ({
  id: config.rectId,
  url: config.url,
  width: config.width,
  height: config.height,
  x: config.left,
  y: config.top,
})),
);

让我们逐个介绍上述代码中的每个部分:

canvasRef 和 setCanvas:

js 复制代码
const canvasRef = useRef<any>(null);
const [canvas, setCanvas] = useState<any>(null);
  • canvasRef 是一个用于引用React组件中的canvas元素的useRef对象。
  • setCanvas 是一个用于更新canvas状态的useState hook,该状态保存着fabric.js中的canvas实例。

imgWidth 和 imgHeight:

ini 复制代码
const imgWidth = useRef(0);
const imgHeight = useRef(0);
  • imgWidth 和 imgHeight 是用于存储图片的实际宽度和高度的useRef对象。 canvasWidth 和 canvasHeight:
ini 复制代码
const canvasWidth = useRef(0);
const canvasHeight = useRef(0);
  • canvasWidth 和 canvasHeight 是用于存储canvas元素的宽度和高度的useRef对象。

currentSelection:

ini 复制代码
const currentSelection = useRef({
  startX: 0,
  startY: 0,
});
  • currentSelection 是一个useRef对象,用于存储当前选择热区的起始坐标。

selectedRectId:

scss 复制代码
const [selectedRectId, setSelectedRectId] = useState('');
  • selectedRectId 是一个useState hook,用于存储当前选定的热区的ID。

rectConfig 和 setRectConfig:

arduino 复制代码
const [rectConfig, setRectConfig] = useState(
  data.hotZoneList.map((config) => ({
    id: config.rectId,
    url: config.url,
    width: config.width,
    height: config.height,
    x: config.left,
    y: config.top,
  })),
);
  • rectConfig 是一个存储热区配置的useState hook。它将data.hotZoneList中的热区配置数据映射为内部状态,包括ID、URL、宽度、高度、X和Y坐标。

Canvas初始化和背景图片加载

js 复制代码
const initCanvas = () => {
  // 创建背景图片对象
  const bgImg = new fabric.Image(document.getElementById(`bgImg${dataIndex}`));
  bgImg.scaleToWidth(canvasWidth.current);

  // 创建新的fabric.Canvas实例
  const newCanvas = new fabric.Canvas(canvasRef.current, {
    backgroundImage: bgImg,
    selectionColor: 'rgba(255, 255, 255, 0)',
    selectionLineWidth: BORDER_WIDTH,
    selectionBorderColor: ACTIVE_COLOR,
  });

  // 添加事件监听器
  newCanvas.on('selection:cleared', (options) => onSelectRect(options, newCanvas));
  newCanvas.on('selection:updated', (options) => onSelectRect(options, newCanvas));
  newCanvas.on('selection:created', (options) => onSelectRect(options, newCanvas));

  newCanvas.on('mouse:down', (options) => {
    currentSelection.current.startX = options.e.offsetX;
    currentSelection.current.startY = options.e.offsetY;
  });

  newCanvas.on('mouse:up', (options) => {
    // 处理鼠标释放事件,添加热区
    // ...
  });

  // 设置Canvas的大小
  setCanvas(newCanvas);

  // 如果已经存在热区数据,则初始化显示
  if (data.hotZoneList.length) {
    initDefaultConfig(data.hotZoneList, newCanvas);
  }
};

// 图片加载完成后调用initCanvas
const onImgLoad = (e) => {
  // 获取图片尺寸信息
  const { width, height, naturalWidth, naturalHeight } = e.target;

  // 存储图片尺寸和canvas尺寸
  imgWidth.current = naturalWidth;
  imgHeight.current = naturalHeight;
  canvasWidth.current = width;
  canvasHeight.current = height;

  // 初始化Canvas
  initCanvas();
};

该方法包含了两个主要的方法,initCanvas,onImageLoad

initCanvasonImageLoad 这两个函数在这个文件中起着至关重要的作用,主要涉及初始化canvas以及处理图片加载的逻辑。

  1. onImageLoad 函数

作用

  • 当图片加载完成时触发的事件处理函数。
  • 获取图像的尺寸信息,包括原始尺寸和实际渲染尺寸。
  • 存储图片尺寸信息和 canvas 尺寸信息。
  • 调用 initCanvas 函数来初始化 canvas。

重要性

  • 图片加载完成后,我们需要知道图像的尺寸以及 canvas - - 的尺寸,以便正确地进行后续的初始化操作。
  • 将图片尺寸和 canvas 尺寸存储在 imgWidth, imgHeight, canvasWidth, canvasHeight 中,以备后续使用。
  1. initCanvas 函数

作用:

  • 初始化 canvas,并设置其背景图像。
  • 注册事件监听器,处理选择、拖拽等事件。
  • 根据已存在的热区数据,初始化显示这些热区。
  • 将新创建的 canvas 实例设置为组件的状态。

重要性:

  • 创建 fabric.js 的 Image 和 Canvas - 实例,将背景图像设置到 canvas 中。
  • 注册了一系列的事件监听器,用于处理选择、拖拽、热区添加等交互行为。
  • 将初始化好的 canvas 实例通过 setCanvas 存储在组件的状态中,以便后续对 canvas 的操作。

这两个函数协同工作,确保在图像加载完成后,可以正确地初始化 canvas,并设置好事件监听器,使得用户能够通过鼠标进行交互操作,例如绘制热区。这对于实现一个交互式的编辑组件是至关重要的。

initCanvas

主要使用了fabricjs来进行初始化

fabric.Image:

  • 用于创建一个fabric.js的图像实例。
  • document.getElementById(bgImg${dataIndex}) 获取具有相应ID的HTML元素,其中dataIndex是从上下文中获取的。
  • bgImg.scaleToWidth(canvasWidth.current) 将图像的宽度缩放到canvasWidth的当前值。

fabric.Canvas:

  • 创建一个fabric.js的Canvas实例,将其附加到canvasRef.current上。
  • backgroundImage: bgImg 将背景图片设置为之前创建的图像实例。
  • selectionColor, selectionLineWidth, selectionBorderColor 用于设置选择热区时的样式。

事件监听器:

  • on('selection:cleared', ...), on('selection:updated', ...), on('selection:created', ...) 监听Canvas上选择热区的事件,并分别调用onSelectRect函数。

on('mouse:down', ...)

  • 监听鼠标按下事件,记录鼠标按下的坐标。

on('mouse:up', ...):

  • 监听鼠标释放事件,处理热区的添加逻辑。

setCanvas:

  • 使用setCanvas将新创建的Canvas实例设置为组件的状态。

initDefaultConfig: 如果存在热区数据,通过initDefaultConfig将热区配置初始化显示在Canvas上。

onImageLoad

获取图片尺寸信息:

  • e.target 是触发加载事件的图像元素。
  • naturalWidth, naturalHeight 是图片的原始尺寸。
  • width, height 是图像在页面中的实际渲染尺寸。

存储图片尺寸和canvas尺寸:

  • 使用useRef对象(imgWidth, imgHeight, canvasWidth, canvasHeight)存储图片和canvas的尺寸信息。

初始化Canvas:

  • 调用之前定义的initCanvas 函数,根据图片尺寸和canvas尺寸创建并初始化Canvas。

绘制热区和热区事件处理

js 复制代码
const createRect = ({ top, left, width, height, rectId }) => {
  // 使用fabric.Rect创建热区对象
  const rect = new fabric.Rect({
    top,
    left,
    width,
    height,
    fill: 'rgba(255, 255, 255, 0)',
    stroke: ACTIVE_COLOR,
    strokeWidth: BORDER_WIDTH,
    transparentCorners: false,
    lockRotation: true,
  }).setControlVisible('mtr', false);

  // 设置热区ID并添加事件监听器
  rect.rectId = rectId || `${Date.now()}`;
  rect.on('moving', () => onRectMoving(rect));
  rect.on('mouseup', () => onRectMouseup(rect));

  return rect;
};

// 添加新热区
const addRect = ({ x, y }, newCanvas) => {
  // 计算矩形宽高
  // ...

  // 创建热区对象
  const rect = createRect({
    top: height < 0 ? startY - Math.abs(height) : startY,
    left: width < 0 ? startX - Math.abs(width) : startX,
    width: Math.abs(width),
    height: Math.abs(height),
    rectId: `${Date.now()}`,
  });

  // 检查热区是否重叠
  // ...

  // 添加热区到Canvas并更新状态
  newCanvas.add(rect);
  newCanvas.setActiveObject(rect);
  setRectConfig((prevRectConfig: any) => [...prevRectConfig, { id: rect.rectId, url: '', x, y, width, height }]);
};

createRect

new fabric.Rect:

  • 使用 fabric.js 提供的 Rect 构造函数创建一个矩形对象。

矩形属性设置:

  • top, left: 矩形的顶部和左侧位置。
  • width, height: 矩形的宽度和高度。
  • fill: 设置填充颜色为透明。
  • stroke: 设置矩形边框颜色为 ACTIVE_COLOR。
  • strokeWidth: 设置矩形边框宽度为 BORDER_WIDTH。
  • transparentCorners: 设置矩形的角为透明。
  • lockRotation: 锁定矩形的旋转。

setControlVisible('mtr', false):

  • 使用 fabric.js 提供的方法,设置矩形的旋转控制器不可见。

rectId 和事件监听器:

  • 设置热区的唯一标识 rectId,如果未提供,则使用当前时间戳。
  • 添加 moving 和 mouseup 事件监听器,分别调用 onRectMoving 和 onRectMouseup 函数。

返回矩形对象:

  • 返回创建好的矩形对象。

addRect

通过计算鼠标按下和释放的坐标,得到矩形的宽度和高度。 创建热区对象:

  • 调用 createRect
  • 函数创建热区对象,传递矩形的位置和大小。

检查热区是否重叠:

  • 使用 getRectList 获取当前画布上的所有热区对象列表。
  • 使用 isTwoRectOverlap 检查新创建的热区是否与已存在的热区重叠。

处理重叠情况:

  • 如果热区重叠,通过 Message.error 提示用户,并释放当前创建的热区对象。

添加热区到Canvas并更新状态:

  • 如果热区不重叠,将热区对象添加到画布上。
  • 使用 setActiveObject 将新创建的热区设为活动对象。
  • 更新组件状态,将新创建的热区信息添加到 rectConfig 中。

热区配置和状态更新

js 复制代码
const onSelectRect = (options, newCanvas) => {
  // 处理选择热区事件
  // ...
};

const onRectMoving = (target) => {
  // 处理热区移动事件
  // ...
};

const onRectMouseup = (target) => {
  // 处理热区释放鼠标事件
  // ...
};

热区配置和数据保存

js 复制代码
const clearAll = () => {
  // 清空所有热区
  rectConfig.forEach(() => {
    deleteObject(0, canvas);
  });
};

const saveDraw = () => {
  // 保存绘制结果
  // ...
};

填坑环节

绘制热区时不应该超出当前容器

onRectMoving 事件处理

js 复制代码
const onRectMoving = (target) => {
  const { top, left, lineCoords } = target;
  const { br, tl } = lineCoords;
  const width = br.x - tl.x;
  const height = br.y - tl.y;

  // 如果热区左侧位置小于0,则将其设置为0,防止热区超出画布左侧
  if (left < 0) {
    target.set('left', 0);
  }

  // 如果热区顶部位置小于0,则将其设置为0,防止热区超出画布顶部
  if (top < 0) {
    target.set('top', 0);
  }

  // 如果热区右侧位置加上宽度大于画布宽度,则将其左侧位置设置为合适的位置,防止热区超出画布右侧
  if (left + width >= canvasWidth.current) {
    target.set('left', canvasWidth.current - width);
  }

  // 如果热区底部位置加上高度大于画布高度,则将其顶部位置设置为合适的位置,防止热区超出画布底部
  if (top + height >= canvasHeight.current) {
    target.set('top', canvasHeight.current - height);
  }
};

onRectMouseup 事件处理

js 复制代码
const onRectMouseup = (target) => {
  const { lineCoords } = target;
  const { br, tl } = lineCoords;
  const options = {
    scaleX: 1,
    scaleY: 1,
  };

  // 如果热区的左上角横坐标小于0,则调整热区的左侧位置和宽度
  if (tl.x < 0) {
    Object.assign(options, {
      left: 0,
      width: br.x - BORDER_WIDTH,
    });
  }
  // 如果热区的右下角横坐标大于画布宽度,则调整热区的宽度
  else if (br.x > canvasWidth.current) {
    Object.assign(options, {
      width: canvasWidth.current - tl.x - BORDER_WIDTH,
    });
  }
  // 否则,按照原始计算方式设置热区的宽度
  else {
    Object.assign(options, {
      width: br.x - tl.x - BORDER_WIDTH,
    });
  }

  // 如果热区的左上角纵坐标小于0,则调整热区的顶部位置和高度
  if (tl.y < 0) {
    Object.assign(options, {
      top: 0,
      height: br.y - BORDER_WIDTH,
    });
  }
  // 如果热区的右下角纵坐标大于画布高度,则调整热区的高度
  else if (br.y > canvasHeight.current) {
    Object.assign(options, {
      height: canvasHeight.current - tl.y - BORDER_WIDTH,
    });
  }
  // 否则,按照原始计算方式设置热区的高度
  else {
    Object.assign(options, {
      height: br.y - tl.y - BORDER_WIDTH,
    });
  }

  // 应用计算后的选项,调整热区的位置和大小
  target.set(options);
};

控制原理:

  • 在 onRectMoving 中,监听热区移动事件,通过判断热区的位置,防止热区超出画布的边界,包括左侧、顶部、右侧和底部。
  • 在 onRectMouseup 中,监听热区释放鼠标事件,根据热区的左上角和右下角的坐标,调整热区的位置和大小,确保热区不会超出画布边界。

绘制热区重叠计算

通过 addRect 函数中的以下代码来控制创建热区时的重叠情况:

js 复制代码
// 检查热区是否重叠
const hasOverlap = getRectList(newCanvas).some((rect2) =>
  isTwoRectOverlap(rect.getBoundingRect(), rect2.getBoundingRect()),
);

if (hasOverlap) {
  Message.error('热区之间不可重叠框选,请调整热区!');
  rect.dispose();
  return;
}

在这段代码中,主要利用了 getRectList 函数获取当前画布上的所有热区对象列表,以及 isTwoRectOverlap 函数判断两个矩形是否重叠。

以下是相关的函数和步骤的详细解释:

getRectList 函数

js 复制代码
// 获取画布上的热区对象列表
const getRectList = (newCanvas?) => {
  const rectList: any[] = [];
  const result = canvas || newCanvas;
  if (!result) return rectList;

  result.forEachObject((obj: any) => {
    if (obj.rectId) {
      rectList.push(obj);
    }
  });

  return rectList;
};

解释:

  • getRectList 函数用于获取当前画布上的所有热区对象列表。
  • 通过 forEachObject 方法遍历画布上的所有对象,筛选出带有 rectId 属性的对象,即热区对象。
  • 返回热区对象列表。

isTwoRectOverlap 函数

js 复制代码
// 判断两个矩形是否重叠
const isTwoRectOverlap = (rect1, rect2) => {
  return (
    rect1.left < rect2.left + rect2.width &&
    rect1.left + rect1.width > rect2.left &&
    rect1.top < rect2.top + rect2.height &&
    rect1.top + rect1.height > rect2.top
  );
};

解释:

  • isTwoRectOverlap 函数用于判断两个矩形是否重叠。
  • 判断的逻辑是通过比较两个矩形的左侧、右侧、顶部和底部的坐标关系,如果有重叠则返回 true,否则返回 false。

addRect 函数中的检查重叠逻辑

js 复制代码
// 检查热区是否重叠
const hasOverlap = getRectList(newCanvas).some((rect2) =>
  isTwoRectOverlap(rect.getBoundingRect(), rect2.getBoundingRect()),
);

if (hasOverlap) {
  Message.error('热区之间不可重叠框选,请调整热区!');
  rect.dispose();
  return;
}

解释:

  • 在 addRect 函数中,通过调用 getRectList 获取当前画布上的所有热区对象列表。
  • 使用 some 方法遍历热区列表,判断新创建的热区和已存在的热区是否有重叠,如果有重叠则提示错误消息,并释放当前创建的热区对象。

总结

整体实现流程如下

  1. 响应图片加载事件:
  • 当图片加载完成后,获取图片和画布的尺寸。
  • 根据图片和画布尺寸初始化画布和设置背景。
  1. 初始化画布和背景图片:
  • 使用 fabric.Canvas 创建一个画布实例。
  • 监听画布上的鼠标事件,例如鼠标点击和释放。
  • 设置画布的背景图片,确保背景图片与画布大小匹配。
  1. 处理鼠标点击和释放事件:
  • 在鼠标点击事件中记录起始位置坐标。
  • 在鼠标释放事件中,根据起始和结束位置创建热区对象。
  • 检查新创建的热区是否与现有热区重叠。
  1. 控制热区移动和调整大小:
  • 使用 fabric.Rect 创建热区对象。
  • 设置热区的样式、边框等属性。
  • 添加热区对象到画布,并更新组件状态。
  • 监听热区的移动和释放鼠标事件,在移动过程中控制热区- - 不超出画布边界,释放鼠标后调整热区的位置和大小。
  1. 处理热区的删除和编辑:
  • 在编辑状态下,可以选择热区并编辑链接。
  • 提供删除热区的功能,删除时同时从画布和组件状态中移除。
  1. 验证和保存:
  • 对用户输入的链接进行验证。
  • 确认保存前检查热区之间是否有重叠,若有则提示错误。
  • 将热区数据保存到组件状态中,并通过回调函数传递给父组件。

看看效果

添加热区

移动端

低头一看,PM 5:00 喝了一口浓茶,发布代码,悠闲的等待着我的bug...

哦,不对,移动端代码忘写了。

pc端预览

html 复制代码
 <div key={dataIndex} className={styles.previewItem}>
  {item.hotZoneList.map((hotZone, index) => (
    <div
      key={index}
      className={styles.hotZone}
      style={{
        left: `${hotZone.left}px`,
        top: `${hotZone.top}px`,
        width: `${hotZone.width}px`,
        height: `${hotZone.height}px`,
      }}
      onClick={() => {
        console.log(hotZone.url);
      }}
    >
      热区0{index + 1}
    </div>
  ))}
</div>

移动端预览

html 复制代码
  <View key={dataIndex} className={styles.previewItem}>
    {item.hotZoneList.length > 0 && item.hotZoneList.map((hotZone: any, index: number) => (
      <View
        key={index}
        className={styles.hotZone}
        style={{
          left: `${hotZone.left * screenWidth / 375}px`,
          top: `${hotZone.top  * screenWidth / 375}px`,
          width: `${hotZone.width * screenWidth / 375}px`,
          height: `${hotZone.height  * screenWidth / 375}px`,
        }}
        onClick={() => onClickLink(hotZone.url)}
      />
    ))}
  </View>

screenWidth:当前屏幕宽度

误差问题

  • canvas的宽我定的375
  • pc预览宽375
  • 移动端根据375计算
  • hotZone.left 是热区左侧相对于设计稿(或其他基准尺寸)的距离。
  • screenWidth 是当前屏幕宽度。
  • 375 是设计稿(或其他基准尺寸)的宽度。

这段代码的目的是将热区左侧的距离按比例适配到当前屏幕上。通常,screenWidth / 375 这个比例表示当前屏幕宽度与设计稿宽度的比值。

例如,如果热区在设计稿上的左侧距离是 100,而当前屏幕宽度是 750,那么通过这个公式计算后,热区在当前屏幕上的左侧距离将会是 200。

这种做法是为了在不同屏幕宽度的设备上保持一定的排版一致性,实现屏幕的适配。这样,无论设备宽度是多少,热区相对于屏幕的位置都会按比例进行缩放

最后

如果你看到这里,还不能自己手撸,点赞评论加收藏~ 我会将完成版本代码私信发送哦~

小透明唯一的骄傲。

相关推荐
Larcher15 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐28 分钟前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭40 分钟前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信1 小时前
我们需要了解的Web Workers
前端
brzhang1 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu2 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花2 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋2 小时前
场景模拟:基础路由配置
前端
六月的可乐2 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程