记·在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。

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

最后

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

小透明唯一的骄傲。

相关推荐
Watermelo6177 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248948 分钟前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_7482356120 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript