Vue实现图像对比组件:打造交互式图片比较工具

Vue实现图像对比组件:打造交互式图片比较工具

在前端开发中,图像对比功能是一个常见需求,尤其是在设计稿预览、产品前后对比、图像编辑等场景中。本文将介绍如何使用Vue 3构建一个功能完善的图像对比组件,支持鼠标拖动和触摸操作,并带有平滑的动画效果。

为什么需要图像对比组件?

图像对比组件在多种场景下都能发挥重要作用:

  • 设计与开发协作:设计师可以通过对比组件展示设计稿与实际实现的差异
  • 产品展示:电商平台可以用它展示产品原图与处理后的效果图
  • 图像编辑:用于展示图片修改前后的对比效果
  • 教育与培训:展示知识点前后变化或不同方案的对比

组件功能与特点

我们实现的Vue图像对比组件具有以下特点:

  • 支持鼠标拖动滑块进行图像对比
  • 兼容触摸设备的手势操作
  • 滑块位置实时计算与平滑过渡
  • 标签随滑块位置自动淡入淡出
  • 响应式设计,适配不同屏幕尺寸
  • 支持自定义左右图片和标签文本

核心实现原理

图像对比的核心技术在于clipPath属性的应用,它可以裁剪元素的显示区域。我们通过动态计算滑块位置,分别对左右两张图片应用不同的裁剪路径:

<!-- 左侧图片 - 只显示滑块左侧部分 -->
<img 
  :src="leftImage" 
  :style="{ clipPath: `inset(0 ${100 - sliderPosition}% 0 0)` }"
/>

<!-- 右侧图片 - 只显示滑块右侧部分 -->
<img 
  :src="rightImage" 
  :style="{ clipPath: `inset(0 0 0 ${sliderPosition}%)` }"
/>

交互逻辑则通过鼠标和触摸事件实现,核心在于计算鼠标/触摸点在容器中的相对位置,并转换为滑块的百分比位置:

const handleMouseMove = (e: MouseEvent) => {
  if (!state.isDragging || !containerRef.value) return;

  const containerRect = containerRef.value.getBoundingClientRect();
  const containerWidth = containerRect.width;

  // 计算鼠标在容器内的相对位置百分比
  let position = ((e.clientX - containerRect.left) / containerWidth) * 100;

  // 限制在 0-100% 范围内
  position = Math.max(0, Math.min(100, position));

  sliderPosition.value = position;
};

完整组件代码

下面是Vue 3图像对比组件的完整实现,采用单文件组件(SFC)格式:

<template>
  <div class="relative w-full max-w-4xl mx-auto my-8">
    <div
      ref="containerRef"
      class="relative w-full h-[400px] md:h-[500px] overflow-hidden rounded-lg border border-gray-200 shadow-md"
    >
      <!-- 左侧图片 -->
      <img
        :src="leftImage"
        alt="Left"
        class="absolute top-0 left-0 w-full h-full object-cover"
        :style="{
          clipPath: `inset(0 ${100 - sliderPosition}% 0 0)`,
        }"
      />

      <!-- 右侧图片 -->
      <img
        :src="rightImage"
        alt="Right"
        class="absolute top-0 left-0 w-full h-full object-cover"
        :style="{
          clipPath: `inset(0 0 0 ${sliderPosition}%)`,
        }"
      />

      <!-- 滑块轨道 -->
      <div
        :style="{
          transform: `translate(-50%, 0%) translateX(calc(${sliderPosition}%))`,
          zIndex: 20,
        }"
        class="absolute top-0 left-0 w-full h-full flex items-center justify-center"
      >
        <!-- 滑块竖线 -->
        <div class="w-1 h-full bg-[#FFF] shadow-md transition-all duration-150" />

        <!-- 滑块手柄 -->
        <div
          ref="sliderRef"
          class="absolute top-1/2 w-12 h-12 bg-white rounded-full shadow-lg border-2 border-[#91B493] flex items-center justify-center cursor-grab transition-transform duration-150 hover:scale-105 active:scale-95"
          @mousedown="handleMouseDown"
          @touchstart="handleTouchStart"
        >
          <!-- 双向箭头图标 -->
          <svg viewBox="0 0 24 24" class="text-[#91B493] transition-all duration-300 w-5 h-5"
               fill="none" stroke="currentColor" stroke-width="2">
            <path d="M21 12H3M3 12L9 6M3 12L9 18M21 12L15 6M21 12L15 18"></path>
          </svg>
        </div>
      </div>
    </div>

    <!-- 标签 - 位置和渐隐效果 -->
    <div class="absolute top-10 left-0 right-0 flex justify-between z-20">
      <!-- 左侧标签 -->
      <div class="ml-4">
        <span
          class="px-3 py-1 bg-white/80 backdrop-blur-sm rounded-md text-xl font-medium text-gray-800 shadow-md"
          :style="{
            opacity: sliderPosition > 95 ? 0 : 1,
            transition: 'opacity 0.3s ease',
          }"
        >
          {{ leftLabel }}
        </span>
      </div>

      <!-- 右侧标签 -->
      <div class="mr-4">
        <span
          class="px-3 py-1 bg-white/80 backdrop-blur-sm rounded-md text-xl font-medium text-gray-800 shadow-md"
          :style="{
            opacity: sliderPosition < 5 ? 0 : 1,
            transition: 'opacity 0.3s ease',
          }"
        >
          {{ rightLabel }}
        </span>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue';

interface ImageCompareProps {
  leftImage: string;
  rightImage: string;
  leftLabel?: string;
  rightLabel?: string;
}

const props = defineProps<ImageCompareProps>();

// 响应式状态
const sliderPosition = ref(50); // 初始在中间位置
const containerRef = ref<HTMLDivElement | null>(null);
const sliderRef = ref<HTMLDivElement | null>(null);
const state = reactive({
  isDragging: false
});

// 鼠标按下事件
const handleMouseDown = (e: MouseEvent) => {
  e.preventDefault();
  state.isDragging = true;
  document.addEventListener('mousemove', handleMouseMove);
  document.addEventListener('mouseup', handleMouseUp);
};

// 鼠标移动事件
const handleMouseMove = (e: MouseEvent) => {
  if (!state.isDragging || !containerRef.value) return;

  const containerRect = containerRef.value.getBoundingClientRect();
  const containerWidth = containerRect.width;

  // 计算鼠标在容器内的相对位置百分比
  let position = ((e.clientX - containerRect.left) / containerWidth) * 100;

  // 限制在 0-100% 范围内
  position = Math.max(0, Math.min(100, position));

  sliderPosition.value = position;
};

// 鼠标释放事件
const handleMouseUp = () => {
  state.isDragging = false;
  document.removeEventListener('mousemove', handleMouseMove);
  document.removeEventListener('mouseup', handleMouseUp);
};

// 触摸事件支持
const handleTouchStart = (e: TouchEvent) => {
  e.preventDefault();
  state.isDragging = true;
  document.addEventListener('touchmove', handleTouchMove);
  document.addEventListener('touchend', handleTouchEnd);
};

const handleTouchMove = (e: TouchEvent) => {
  if (!state.isDragging || !containerRef.value) return;

  const containerRect = containerRef.value.getBoundingClientRect();
  const containerWidth = containerRect.width;

  // 获取第一个触摸点
  const touch = e.touches[0];
  let position = ((touch.clientX - containerRect.left) / containerWidth) * 100;

  position = Math.max(0, Math.min(100, position));
  sliderPosition.value = position;
};

const handleTouchEnd = () => {
  state.isDragging = false;
  document.removeEventListener('touchmove', handleTouchMove);
  document.removeEventListener('touchend', handleTouchEnd);
};

// 窗口大小变化时重置位置
const handleResize = () => {
  sliderPosition.value = 50; // 重置到中间位置
};

onMounted(() => {
  window.addEventListener('resize', handleResize);
});

onUnmounted(() => {
  window.removeEventListener('resize', handleResize);
  document.removeEventListener('mousemove', handleMouseMove);
  document.removeEventListener('mouseup', handleMouseUp);
  document.removeEventListener('touchmove', handleTouchMove);
  document.removeEventListener('touchend', handleTouchEnd);
});

// 默认标签值
const leftLabel = computed(() => props.leftLabel || '原图');
const rightLabel = computed(() => props.rightLabel || '优化后');
</script>

<style scoped>
/* 基础样式已包含在模板中,如需扩展可在此添加 */
</style>

在项目中使用组件

要在Vue项目中使用这个图像对比组件,只需简单几步:

  1. 将组件文件保存为src/components/ImageCompare.vue
  2. 在需要使用的页面中导入组件:
<template>
  <div class="container">
    <h1 class="text-3xl font-bold mb-8 text-center">图像对比演示</h1>
    
    <!-- 使用图像对比组件 -->
    <ImageCompare 
      :leftImage="beforeImage" 
      :rightImage="afterImage"
      leftLabel="线稿设计"
      rightLabel="上色成品"
    />
  </div>
</template>

<script setup lang="ts">
import ImageCompare from '../components/ImageCompare.vue';

// 示例图片路径
const beforeImage = 'https://picsum.photos/800/600?random=1';
const afterImage = 'https://picsum.photos/800/600?random=2';
</script>

进阶优化与扩展

这个组件还可以进一步优化和扩展:

  1. 添加键盘导航:支持使用左右方向键调整滑块位置
  2. 自定义主题:允许通过props配置滑块颜色、标签样式等
  3. 添加动画效果:为图片切换添加淡入淡出等过渡效果
  4. 性能优化:使用requestAnimationFrame优化拖动时的重绘
  5. 事件回调:添加滑块位置变化的回调函数

完整项目搭建

如果你想快速搭建一个包含此组件的Vue项目,可以按照以下步骤操作:

  1. 安装Vue CLI:npm install -g @vue/cli
  2. 创建新项目:vue create image-comparison-app
  3. 选择Vue 3版本和TypeScript支持
  4. 安装完成后进入项目目录:cd image-comparison-app
  5. 创建组件文件夹并添加上述组件代码
  6. 修改App.vue添加演示内容
  7. 启动项目:npm run serve

总结

通过Vue 3的响应式系统和事件处理机制,我们实现了一个功能完善、交互友好的图像对比组件。该组件利用clipPath实现图像裁剪,通过鼠标和触摸事件处理实现交互功能,并添加了平滑的动画效果提升用户体验。

这个组件不仅可以用于图片对比,还可以扩展应用到更多场景,如图层混合、设计稿对比、产品效果展示等。通过合理的封装和配置,它能够很好地融入各种Vue项目中,为用户提供直观的视觉对比体验。

效果图

在这里插入图片描述
代码地址:https://gitee.com/keroyj/image_compare.git
代码地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

layman0528

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值