canva绘制图片,实现图片缩放,拖动,打标点功能

1、先看效果

1、标记点波动效果

2、png图片波动效果

2、废话不多说直接上代码

js 复制代码
<template>
  <div ref="containerRef" class="flex items-center justify-center w-full h-full overflow-hidden bg-#eee">
    <canvas ref="canvasRef" class="w-full h-full cursor-grab block"></canvas>
  </div>
</template>


<script setup>
import { ref, onMounted, onUnmounted, inject, computed, defineExpose } from 'vue';

const canvasRef = ref(null);
const containerRef = ref(null);
const imgSrc = 'xxx'; // 图片路径
let ctx, img;
let scale = 1; // 初始缩放比例
let minScale = 0.5;  // 限制缩放范围
let maxScale = 8;  // 限制缩放范围
let position = { x: 0, y: 0 }; // 画布偏移
let startDrag = null;
let animationFrameId = null;

// **标记点**(WebSocket 接收)
const markers = ref([{ x: 100, y: 150, pulse: 0 }]); // pulse 控制动画

// **绘制图片**
// const drawImage = () => {
//   if (!ctx || !img) return;

//   const { width: boxWidth, height: boxHeight } = containerRef.value.getBoundingClientRect();
//   canvasRef.value.width = boxWidth;
//   canvasRef.value.height = boxHeight;

//   ctx.clearRect(0, 0, boxWidth, boxHeight);
//   ctx.save();
//   ctx.translate(position.x, position.y);
//   ctx.scale(scale, scale);
//   ctx.drawImage(img, 0, 0, img.width, img.height);

//   // **绘制标记点**
//   markers.value.forEach((marker) => {
//     marker.pulse += 0.05;
//     const dynamicRadius = 5 + Math.sin(marker.pulse) * 2;
//     const dynamicAlpha = 0.6 + Math.abs(Math.sin(marker.pulse)) * 0.4;

//     ctx.beginPath();
//     ctx.arc(marker.x, marker.y, dynamicRadius / scale, 0, 2 * Math.PI);
//     ctx.fillStyle = `rgba(255, 0, 0, ${dynamicAlpha})`;
//     // ctx.arc(marker.x, marker.y, 5 / scale, 0, 2 * Math.PI);
//     // ctx.fillStyle = 'red';
//     ctx.fill();
//     ctx.closePath();
//   });

//   ctx.restore();
//   animationFrameId = requestAnimationFrame(drawImage);
// };


// **创建标记点图片**
const markerImg = new Image();
markerImg.src = 'xx'; // 确保路径正确
markerImg.onload = () => {
  console.log('标记点图片加载完成');
};

const drawImage = () => {
  if (!ctx || !img || !markerImg.complete) return; // 确保所有图片都加载完成

  const { width: boxWidth, height: boxHeight } = containerRef.value.getBoundingClientRect();
  canvasRef.value.width = boxWidth;
  canvasRef.value.height = boxHeight;

  ctx.clearRect(0, 0, boxWidth, boxHeight);
  ctx.save();
  ctx.translate(position.x, position.y);
  ctx.scale(scale, scale);
  ctx.drawImage(img, 0, 0, img.width, img.height);

  // **绘制标记点**
  markers.value.forEach((marker) => {
    marker.pulse += 0.05;
    const dynamicScale = 1 + Math.sin(marker.pulse) * 0.2; // 让图片动态变化
    const imgSize = 20 * dynamicScale; // 30px 基础大小

    ctx.save();
    ctx.globalAlpha = 0.6 + Math.abs(Math.sin(marker.pulse)) * 0.6; // 透明度动态变化
    ctx.drawImage(
      markerImg,
      marker.x - imgSize / 2, // 让图片中心对齐标记点
      marker.y - imgSize / 2,
      imgSize / scale,
      imgSize / scale
    );
    ctx.restore();
  });

  ctx.restore();
  animationFrameId = requestAnimationFrame(drawImage);
};



// **计算初始缩放和位置**
const initImagePosition = () => {
  const { width: boxWidth, height: boxHeight } = containerRef.value.getBoundingClientRect();
  
  // 计算高度撑满时的缩放比例
  scale = boxHeight / img.height;
  scale = Math.min(maxScale, Math.max(minScale, scale)); // 限制缩放范围

  // 居中图片
  position.x = (boxWidth - img.width * scale) / 2;
  position.y = 0; // 高度撑满,不需要额外调整 Y 轴
};

// **鼠标滚轮缩放**
const handleWheel = (event) => {
  event.preventDefault();
  const scaleFactor = event.deltaY > 0 ? 0.9 : 1.1;
  const newScale = Math.min(maxScale, Math.max(minScale, scale * scaleFactor));

  const rect = canvasRef.value.getBoundingClientRect();
  const mouseX = (event.clientX - rect.left - position.x) / scale;
  const mouseY = (event.clientY - rect.top - position.y) / scale;

  position.x -= (mouseX * newScale - mouseX * scale);
  position.y -= (mouseY * newScale - mouseY * scale);

  scale = newScale;
};

// **鼠标拖拽**
const handleMouseDown = (event) => {
  startDrag = { x: event.clientX, y: event.clientY };
};
const handleMouseMove = (event) => {
  if (!startDrag) return;
  position.x += event.clientX - startDrag.x;
  position.y += event.clientY - startDrag.y;
  startDrag = { x: event.clientX, y: event.clientY };
};
const handleMouseUp = () => {
  startDrag = null;
};

// **WebSocket 接收标记点**
let socket;
const setupWebSocket = () => {
  
};

const wsData = computed(() => 
  inject('wsData'),
  
  console.log('computed--ws', inject('wsData'))
);

const randomlyGenerateMarkerPoints = () => {
  markers.value.push({
    x: Math.random() * img.width,
    y: Math.random() * img.height,
    pulse: 0,
  })
}

// **组件挂载**
onMounted(() => {
  const canvas = canvasRef.value;
  ctx = canvas.getContext('2d');

  img = new Image();
  img.src = imgSrc;
  img.onload = () => {
    initImagePosition(); // **初始加载时让图片居中 & 高度撑满**
    drawImage();
  };

  canvas.addEventListener('wheel', handleWheel);
  canvas.addEventListener('mousedown', handleMouseDown);
  canvas.addEventListener('mousemove', handleMouseMove);
  canvas.addEventListener('mouseup', handleMouseUp);

  // setupWebSocket();
});

// **组件销毁**
onUnmounted(() => {
  const canvas = canvasRef.value;
  if (canvas) {
    canvas.removeEventListener('wheel', handleWheel);
    canvas.removeEventListener('mousedown', handleMouseDown);
    canvas.removeEventListener('mousemove', handleMouseMove);
    canvas.removeEventListener('mouseup', handleMouseUp);
  }
  
  if (socket) {
    socket.close();
  }

  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId);
  }
});

defineExpose({
  randomlyGenerateMarkerPoints,
});
</script>

tips:

我的需求是按照websocket推送的坐标去改变标点位置,部分业务代码被删掉请见谅,不影响效果 放心食用。

相关推荐
Setsuna_F_Seiei40 分钟前
前端转型 Agent 开发 04 之 MCP 与 Skill(赋予 Agent 更广工作能力)
前端·agent
刘发财2 小时前
前端2秒生成500页矢量PDF,rust真的强到没朋友
前端·javascript·rust
郑州光合科技余经理3 小时前
国际版外卖系统:税率字段怎么和订单主流程解耦
android·java·开发语言·前端·后端·php·ai编程
梦想平凡3 小时前
百游棋牌源代码开发搭建教程(十):隔离部署、备份恢复与双端验收
java·前端·javascript·数据库·源代码管理
yume_sibai4 小时前
06-Rust Web 开发实战(Axum 框架 + 数据库 + JWT 认证 + 中间件 + 部署)
前端·数据库·rust
计算机魔术师6 小时前
特朗普上台打给黄仁勋:AI末日论是骗局,我们绝不让它发生
前端
troy1286 小时前
Python 基础语法(八):Web 后端开发、数据分析与可视化、网络爬虫、人工智能 / 大模型应用
前端·python·数据分析
计算机魔术师6 小时前
CEO说要慢下来,黑客说别做梦了——同一篇论文,两种命运
前端
kyriewen7 小时前
我花3天抓了一个幽灵bug,凶手藏在第4层
前端·javascript·程序员
IT_陈寒7 小时前
Java里用Stream.parallel()翻车实录,这性能还不如单线程
前端·人工智能·后端