🔥文中有预览地址,图片拖动切割

586 阅读5分钟

预览链接aicoding.juejin.cn/aicoding/wo…

有一天做blender 3d的时候,看到了这么一个效果,觉得有点意思,于是想着来做了,做了就是做了,就是这么个事:

据我看下面这个效果,用一个块加3张图,然后进行交互式,去控制三张不同图片的显示区域,就能成:

1.gif

思路是这样哈:

三张图叠在一起,同框展示:侧区域显示原始图片,右上区域显示环境渲染图,右下区域显示几何渲染图。

这是我写的效果:

1.gif

动态分割控制

  • 可拖动垂直分割线调整左右区域比例
  • 可拖动水平分割线调整右上右下区域比例
  • 可拖动中心交叉点同时调整两个方向的分割比例

图片切割拖动效果

实现点

🎯 核心功能

  • 可拖动分割线:垂直和水平分割线可以独立拖动
  • 交叉点拖动:中心交叉点可以同时调整两个方向的分割位置
  • 实时比例调整:四个区域会根据分割线位置实时调整大小
  • 数值控制:通过输入框可以精确设置分割位置(10%-90%范围)
  • 重置功能:一键恢复到默认的50%:50%分割比例

1. HTML结构设计

<div class="image-container" id="imageContainer">
    <!-- 三层图片采用相同的容器结构 -->
    <div class="image-layer left-image" id="leftImage">
        <img src="original.png" alt="原始图片">
    </div>
    
    <div class="image-layer top-right-image" id="topRightImage">
        <img src="env-render.png" alt="环境渲染">
    </div>
    
    <div class="image-layer bottom-right-image" id="bottomRightImage">
        <img src="geo-render.png" alt="几何渲染">
    </div>
    
    <!-- 交互控制元素 -->
    <div class="divider vertical-divider" id="verticalDivider"></div>
    <div class="divider horizontal-divider" id="horizontalDivider"></div>
    <div class="intersection" id="intersection"></div>
    
    <!-- 区域标签 -->
    <div class="section-label label-left">原始图片</div>
    <div class="section-label label-top-right">环境渲染</div>
    <div class="section-label label-bottom-right">几何渲染</div>
</div>

设计要点

  • 使用三层完全重叠的图片容器作为基础
  • 独立的DOM元素实现分割线和控制点
  • 标签元素使用绝对定位悬浮在对应区域

2. CSS样式深度解析

核心样式代码(带详细注释):
/* 基础容器样式 - 创建定位上下文和固定尺寸 */
.image-container {
    position: relative; /* 创建定位上下文 */
    width: 100%;
    height: 500px; /* 固定高度确保比例一致 */
    border-radius: 12px; /* 圆角美观设计 */
    overflow: hidden; /* 隐藏溢出内容 */
    background: #333; /* 默认背景色 */
}

/* 图片层通用样式 */
.image-layer {
    position: absolute; /* 绝对定位实现重叠 */
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

/* 图片元素样式 - 确保完美填充 */
.image-layer img {
    width: 100%;
    height: 100%;
    object-fit: cover; /* 保持比例填充整个容器 */
    display: block;
    -webkit-user-drag: none; /* 禁止拖动干扰 */
    user-drag: none;
}

/* 左侧图片裁剪样式 - 使用CSS变量实现动态更新 */
.left-image {
    /* 
     * inset()函数定义矩形裁剪区域
     * 参数顺序:上 右 下 左 
     * 使用calc()计算动态值
     */
    clip-path: var(--left-clip-path, inset(0px calc(100% - 250px) 0px 0px));
}

/* 分割线基础样式 */
.divider {
    position: absolute;
    background: white; /* 醒目白色 */
    z-index: 10; /* 确保在图片上方 */
}

/* 垂直分割线特定样式 */
.vertical-divider {
    width: 2px; /* 细线设计 */
    height: 100%; /* 贯穿整个高度 */
    cursor: ew-resize; /* 东西方向调整光标 */
}

/* 交叉点控制元素样式 */
.intersection {
    position: absolute;
    width: 20px;
    height: 20px;
    background: white;
    border-radius: 50%; /* 圆形设计 */
    cursor: move; /* 移动光标 */
    z-index: 20; /* 最高层级 */
    transform: translate(50%, -50%); /* 精确居中定位 */
}

3. js交互逻辑完整解析

核心类结构:
class ClipPathSplitter {
    constructor() {
        // 获取所有DOM元素引用
        this.container = document.getElementById('imageContainer');
        this.verticalDivider = document.getElementById('verticalDivider');
        this.horizontalDivider = document.getElementById('horizontalDivider');
        this.intersection = document.getElementById('intersection');
        this.verticalPosInput = document.getElementById('verticalPos');
        this.horizontalPosInput = document.getElementById('horizontalPos');
        
        // 图片层引用
        this.leftImage = document.getElementById('leftImage');
        this.topRightImage = document.getElementById('topRightImage');
        this.bottomRightImage = document.getElementById('bottomRightImage');
        
        // 状态变量
        this.isDragging = false; // 是否正在拖动
        this.dragType = null; // 当前拖动类型
        this.containerRect = null; // 容器尺寸缓存
        
        // 初始化
        this.init();
    }
    
    // ...其他方法...
}
完整方法实现(带详细注释):
/**
 * 初始化方法 - 绑定事件监听器
 */
init() {
    this.updateLayout(); // 初始布局计算
    this.bindEvents(); // 事件绑定
}

/**
 * 事件绑定方法
 */
bindEvents() {
    // 垂直分割线鼠标按下事件
    this.verticalDivider.addEventListener('mousedown', (e) => {
        this.startDrag(e, 'vertical');
    });
    
    // 水平分割线鼠标按下事件
    this.horizontalDivider.addEventListener('mousedown', (e) => {
        this.startDrag(e, 'horizontal');
    });
    
    // 交叉点鼠标按下事件
    this.intersection.addEventListener('mousedown', (e) => {
        this.startDrag(e, 'both');
    });
    
    // 文档级鼠标移动事件
    document.addEventListener('mousemove', (e) => this.onMouseMove(e));
    
    // 文档级鼠标释放事件
    document.addEventListener('mouseup', () => this.stopDrag());
    
    // 输入框变化事件
    this.verticalPosInput.addEventListener('input', () => this.updateFromInputs());
    this.horizontalPosInput.addEventListener('input', () => this.updateFromInputs());
    
    // 窗口大小变化事件
    window.addEventListener('resize', () => {
        this.containerRect = this.container.getBoundingClientRect();
    });
}

/**
 * 开始拖动处理
 * @param {Event} e 鼠标事件对象 
 * @param {string} type 拖动类型 ('vertical'|'horizontal'|'both')
 */
startDrag(e, type) {
    e.preventDefault();
    this.isDragging = true;
    this.dragType = type;
    this.containerRect = this.container.getBoundingClientRect();
    
    // 根据拖动类型设置不同光标样式
    document.body.style.cursor = 
        type === 'vertical' ? 'ew-resize' :
        type === 'horizontal' ? 'ns-resize' : 'move';
}

/**
 * 鼠标移动处理
 * @param {Event} e 鼠标事件对象
 */
onMouseMove(e) {
    if (!this.isDragging || !this.containerRect) return;
    
    // 计算相对于容器的坐标
    const x = e.clientX - this.containerRect.left;
    const y = e.clientY - this.containerRect.top;
    
    // 限制在容器范围内
    const verticalPos = Math.max(0, Math.min(800, x));
    const horizontalPos = Math.max(0, Math.min(500, y));
    
    // 根据拖动类型更新对应值
    if (this.dragType === 'vertical' || this.dragType === 'both') {
        this.verticalPosInput.value = Math.round(verticalPos);
    }
    
    if (this.dragType === 'horizontal' || this.dragType === 'both') {
        this.horizontalPosInput.value = Math.round(horizontalPos);
    }
    
    this.updateLayout(); // 更新界面布局
}

/**
 * 停止拖动处理
 */
stopDrag() {
    this.isDragging = false;
    this.dragType = null;
    document.body.style.cursor = 'default'; // 恢复默认光标
}

/**
 * 从输入框更新布局
 */
updateFromInputs() {
    this.updateLayout();
}

/**
 * 更新整个布局
 */
updateLayout() {
    // 获取当前分割位置
    const verticalPos = parseFloat(this.verticalPosInput.value);
    const horizontalPos = parseFloat(this.horizontalPosInput.value);
    
    // 获取容器尺寸
    const containerWidth = this.container.offsetWidth;
    const containerHeight = this.container.offsetHeight;
    
    // 更新分割线位置和样式
    this.verticalDivider.style.left = `${verticalPos}px`;
    this.horizontalDivider.style.top = `${horizontalPos}px`;
    this.horizontalDivider.style.left = `${verticalPos}px`;
    this.horizontalDivider.style.width = `${containerWidth - verticalPos}px`;
    
    // 更新交叉点位置(考虑元素自身尺寸)
    this.intersection.style.left = `${verticalPos - 10}px`;
    this.intersection.style.top = `${horizontalPos}px`;
    
    // 计算各区域裁剪路径
    const rightWidth = containerWidth - verticalPos;
    const bottomHeight = containerHeight - horizontalPos;
    
    /* 
     * clip-path: inset() 参数详解:
     * inset(上剪切距离 右剪切距离 下剪切距离 左剪切距离)
     * 例如:inset(0px 100px 0px 0px) 表示从右侧向内剪切100px
     */
    const leftClipPath = `inset(0px ${rightWidth}px 0px 0px)`;
    const topRightClipPath = `inset(0px 0px ${bottomHeight}px ${verticalPos}px)`;
    const bottomRightClipPath = `inset(${horizontalPos}px 0px 0px ${verticalPos}px)`;
    
    // 应用裁剪路径
    this.leftImage.style.setProperty('--left-clip-path', leftClipPath);
    this.topRightImage.style.setProperty('--top-right-clip-path', topRightClipPath);
    this.bottomRightImage.style.setProperty('--bottom-right-clip-path', bottomRightClipPath);
}

优化

1. 性能优化实践

1.1 减少重绘与回流

// 在频繁操作时使用requestAnimationFrame
onMouseMove(e) {
    if (!this.isDragging) return;
    requestAnimationFrame(() => {
        // 计算和更新逻辑...
    });
}

1.2 尺寸缓存策略

// 在resize事件中使用防抖
let resizeTimer;
window.addEventListener('resize', () => {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(() => {
        this.containerRect = this.container.getBoundingClientRect();
    }, 100);
});

2. 响应式设计增强

/* 添加媒体查询适应不同屏幕 */
@media (max-width: 768px) {
    .image-container {
        height: 300px; /* 小屏幕降低高度 */
    }
    
    .controls {
        flex-direction: column; /* 垂直排列控制元素 */
    }
    
    .divider {
        width: 4px; /* 触控设备增加可点击区域 */
    }
}

3. 触摸屏支持扩展

// 添加触摸事件支持
this.verticalDivider.addEventListener('touchstart', (e) => {
    this.startDrag(e.changedTouches[0], 'vertical');
});

document.addEventListener('touchmove', (e) => {
    if (!this.isDragging) return;
    e.preventDefault();
    this.onMouseMove(e.changedTouches[0]);
});

document.addEventListener('touchend', () => this.stopDrag());

技术点

  1. 纯CSS裁剪技术:完全依赖clip-path实现图片裁剪,无需Canvas或SVG

    • 优势:硬件加速、性能优异、代码简洁
    • 创新点:结合CSS变量实现动态更新
  2. 分层渲染架构

    • 视觉层:三张图片绝对定位叠加
    • 控制层:独立的分割线DOM元素
    • 交互层:统一的事件处理系统
  3. 精准的坐标计算

    // 考虑元素自身尺寸的精确定位
    this.intersection.style.left = `${verticalPos - 10}px`; // 10是元素宽度的一半
    this.intersection.style.top = `${horizontalPos}px`;
    
  4. 可扩展的设计模式

    • 易于添加更多分割区域
    • 支持动态更换图片源
    • 可集成到任何现有系统中

在哪里用得到

  1. 专业图像处理

    • 照片编辑前后对比
    • 3D渲染效果比对
    • 医学影像分析
  2. 电商产品展示

    <!-- 产品多角度展示示例 -->
    <div class="image-layer left-image">
        <img src="product-front.jpg" alt="正面">
    </div>
    <div class="image-layer top-right-image">
        <img src="product-side.jpg" alt="侧面">
    </div>
    <div class="image-layer bottom-right-image">
        <img src="product-detail.jpg" alt="细节">
    </div>
    
  3. 数据可视化仪表盘

    • 多图表联动分析
    • 时间序列对比
    • 地理信息叠加

预览链接aicoding.juejin.cn/pens/751976…