# 图片合成功能实现文档

## 功能概述

图片合成功能允许用户将生成的二维码添加到背景图片上，并提供完整的编辑功能，包括拖拽、缩放和旋转。该功能通过 `ImageComposerView` 实现，支持从 `QRCodeSavedView` 选择背景图片，然后进行二维码的精确编辑。

## 界面设计

### 导航栏
- **标题**: "Add to Picture" (英文界面)
- **返回按钮**: 使用 `Image(systemName: "chevron.left")`，蓝色，18pt semibold 字体
- **保存按钮**: 蓝色文字，16pt semibold 字体，无背景和圆角，与应用其他界面保持一致

### 编辑区域
- **背景图片**: 自适应显示，保持宽高比
- **二维码层**: 包含二维码图片、选择框和操作图标
- **选择框**: 青色边框，2pt 宽度，仅在选中时显示
- **操作图标**: 位于二维码右下角，青色圆形背景，白色图标
- **重置按钮**: 浮动按钮，位于编辑区域右下角

## 核心功能

### 1. 背景图片选择
- 从 `QRCodeSavedView` 的"添加到图片"按钮触发
- 使用 `ImagePicker` 选择背景图片
- 选择后自动跳转到 `ImageComposerView`

### 2. 二维码拖拽
- 支持在背景图片范围内自由拖拽
- 使用 `constrainPositionToImage` 函数限制移动范围
- 确保二维码不会超出背景图片边界

### 3. 缩放和旋转
- **操作图标**: 位于二维码右下角，支持手势操作
- **缩放**: 垂直拖拽操作图标，向上缩小，向下放大
- **旋转**: 水平拖拽操作图标，向左顺时针旋转，向右逆时针旋转
- **范围限制**: 缩放 0.3-2.5，旋转 -180° 到 180°

### 4. 边界约束
```swift
private func constrainPositionToImage(_ position: CGPoint) -> CGPoint {
    let qrCodeSize = CGSize(width: 100 * qrCodeScale, height: 100 * qrCodeScale)
    let halfWidth = qrCodeSize.width / 2
    let halfHeight = qrCodeSize.height / 2
    
    let boundarySize = actualEditingAreaSize != .zero ? actualEditingAreaSize : UIScreen.main.bounds.size
    
    let minX = halfWidth
    let maxX = boundarySize.width - halfWidth
    let minY = halfHeight
    let maxY = boundarySize.height - halfHeight
    
    let constrainedX = max(minX, min(maxX, position.x))
    let constrainedY = max(minY, min(maxY, position.y))
    
    return CGPoint(x: constrainedX, y: constrainedY)
}
```

### 5. 手势处理
```swift
private func handleOperationGesture(_ value: DragGesture.Value) {
    let translation = value.translation
    
    // 设置最小移动阈值，避免微小移动触发操作
    let minMovementThreshold: CGFloat = 3.0
    if abs(translation.width) < minMovementThreshold && abs(translation.height) < minMovementThreshold {
        return
    }
    
    // 判断主要操作方向
    if abs(translation.width) > abs(translation.height) {
        // 水平移动 - 旋转操作
        isRotating = true
        isScaling = false
        
        // 使用基于中心点的角度计算算法
        let center = CGPoint(x: 100 * qrCodeScale / 2, y: 100 * qrCodeScale / 2)
        let currentTouch = CGPoint(x: translation.width, y: translation.height)
        let previousTouch = CGPoint.zero // 对于实时旋转，前一个位置为原点
        
        // 计算相对于中心点的角度
        let angle1 = atan2(previousTouch.y - center.y, previousTouch.x - center.x)
        let angle2 = atan2(currentTouch.y - center.y, currentTouch.x - center.x)
        
        let angleDelta = (angle2 - angle1) * 180 / .pi // 转换为角度
        qrCodeRotation += angleDelta
        
        // 限制旋转角度在 -180° 到 180° 之间
        if qrCodeRotation > 180 {
            qrCodeRotation -= 360
        } else if qrCodeRotation < -180 {
            qrCodeRotation += 360
        }
        
    } else {
        // 垂直移动 - 缩放操作
        isScaling = true
        isRotating = false
        
        let scaleSensitivity: CGFloat = 0.005
        let distance = sqrt(translation.width * translation.width + translation.height * translation.height)
        
        let scaleDirection = translation.height > 0 ? 1.0 : -1.0
        let scaleDelta = distance * scaleDirection * scaleSensitivity
        
        let newScale = qrCodeScale - scaleDelta
        qrCodeScale = max(0.3, min(2.5, newScale))
    }
}
```

### 6. 状态管理
- **选择状态**: `isSelected` 控制选择框和操作图标的显示
- **操作状态**: `isScaling`、`isRotating`、`isDragging` 防止手势冲突
- **防卡死机制**: 定时器检查操作状态，自动重置异常状态

### 7. 图片合成
```swift
private func composeImages(background: UIImage, qrCode: UIImage) -> UIImage {
    let renderer = UIGraphicsImageRenderer(size: background.size)
    
    return renderer.image { context in
        // 绘制背景图片
        background.draw(in: CGRect(origin: .zero, size: background.size))
        
        // 计算二维码在背景图片中的位置和大小
        let qrCodeSize = CGSize(width: 100 * qrCodeScale, height: 100 * qrCodeScale)
        let qrCodeRect = CGRect(
            x: qrCodePosition.x - qrCodeSize.width / 2,
            y: qrCodePosition.y - qrCodeSize.height / 2,
            width: qrCodeSize.width,
            height: qrCodeSize.height
        )
        
        // 应用旋转和缩放变换
        context.cgContext.saveGState()
        context.cgContext.translateBy(x: qrCodeRect.midX, y: qrCodeRect.midY)
        context.cgContext.rotate(by: CGFloat(qrCodeRotation) * .pi / 180)
        context.cgContext.scaleBy(x: qrCodeScale, y: qrCodeScale)
        context.cgContext.translateBy(x: -qrCodeRect.midX, y: -qrCodeRect.midY)
        
        // 绘制二维码
        qrCode.draw(in: qrCodeRect)
        context.cgContext.restoreGState()
    }
}
```

## 技术实现

### 1. 状态变量
```swift
@State private var qrCodePosition = CGPoint.zero
@State private var qrCodeScale: CGFloat = 1.0
@State private var qrCodeRotation: Double = 0.0
@State private var isDragging = false
@State private var isScaling = false
@State private var isRotating = false
@State private var isSelected = true
@State private var lastGestureTime = Date()
@State private var actualEditingAreaSize: CGSize = .zero
```

### 2. 视图结构
```swift
var body: some View {
    NavigationView {
        VStack {
            editingArea
        }
        .navigationTitle("Add to Picture")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .navigationBarLeading) {
                Button("返回") {
                    dismiss()
                }
            }
            ToolbarItem(placement: .navigationBarTrailing) {
                Button("保存") {
                    composeAndSave()
                }
            }
        }
    }
}
```

### 3. 编辑区域
```swift
private var editingArea: some View {
    GeometryReader { geometry in
        ZStack {
            // 背景图片
            Image(uiImage: backgroundImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            
            // 二维码层
            qrCodeLayer
                .position(qrCodePosition)
            
            // 重置按钮
            resetButton
        }
        .onAppear {
            actualEditingAreaSize = geometry.size
            // 设置初始位置
            let imageSize = getImageDisplaySize()
            let centerPosition = CGPoint(x: imageSize.width / 2, y: imageSize.height / 2)
            qrCodePosition = constrainPositionToImage(centerPosition)
        }
        .onChange(of: geometry.size) { newSize in
            actualEditingAreaSize = newSize
        }
    }
}
```

### 4. 二维码层
```swift
private var qrCodeLayer: some View {
    ZStack {
        // 二维码图片
        Image(uiImage: qrCodeImage)
            .resizable()
            .frame(width: 100 * qrCodeScale, height: 100 * qrCodeScale)
            .onTapGesture {
                isSelected.toggle()
                isScaling = false
                isRotating = false
            }
        
        // 选择框
        if isSelected {
            RoundedRectangle(cornerRadius: 4)
                .stroke(Color.teal, lineWidth: 2)
                .frame(width: 140 * qrCodeScale, height: 140 * qrCodeScale)
        }
        
        // 操作图标
        if isSelected {
            operationIcon
        }
    }
    .rotationEffect(.degrees(qrCodeRotation))
    .gesture(
        DragGesture()
            .onChanged { value in
                if !isScaling && !isRotating {
                    lastGestureTime = Date()
                    let newLocation = CGPoint(
                        x: qrCodePosition.x + value.translation.width,
                        y: qrCodePosition.y + value.translation.height
                    )
                    qrCodePosition = constrainPositionToImage(newLocation)
                }
            }
    )
}
```

### 5. 操作图标
```swift
private var operationIcon: some View {
    Image(systemName: "arrow.up.left.and.arrow.down.right")
        .font(.system(size: 16, weight: .medium))
        .foregroundColor(.white)
        .padding(6)
        .background(Color.teal)
        .clipShape(Circle())
        .scaleEffect(qrCodeScale)
        .shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
        .offset(x: 68 * qrCodeScale, y: 68 * qrCodeScale)
        .gesture(
            DragGesture()
                .onChanged { value in
                    lastGestureTime = Date()
                    handleOperationGesture(value)
                }
                .onEnded { _ in
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                        isScaling = false
                        isRotating = false
                    }
                }
        )
}
```

## 用户体验优化

### 1. 防卡死机制
```swift
private func startLightweightAntiStuckCheck() {
    Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { _ in
        if Date().timeIntervalSince(lastGestureTime) > 2.0 {
            if isScaling || isRotating || isDragging {
                DispatchQueue.main.async {
                    isScaling = false
                    isRotating = false
                    isDragging = false
                }
            }
        }
    }
}
```

### 2. 重置功能
```swift
private func resetQRCode() {
    isScaling = false
    isRotating = false
    isDragging = false
    
    withAnimation(.easeInOut(duration: 0.3)) {
        let imageSize = getImageDisplaySize()
        let centerPosition = CGPoint(x: imageSize.width / 2, y: imageSize.height / 2)
        qrCodePosition = constrainPositionToImage(centerPosition)
        qrCodeScale = 1.0
        qrCodeRotation = 0.0
        isSelected = true
    }
}
```

### 3. 保存功能
```swift
private func composeAndSave() {
    isComposing = true
    
    DispatchQueue.global(qos: .userInitiated).async {
        let composedImage = composeImages(background: backgroundImage, qrCode: qrCodeImage)
        
        DispatchQueue.main.async {
            self.composedImage = composedImage
            self.isComposing = false
            self.showingSaveSheet = true
        }
    }
}
```

## 功能特点

1. **精确控制**: 支持像素级精度的二维码定位和编辑
2. **边界约束**: 自动限制二维码在背景图片范围内
3. **手势分离**: 拖拽、缩放、旋转功能独立，避免冲突
4. **状态管理**: 完善的状态管理机制，确保操作稳定性
5. **防卡死**: 自动检测和修复异常状态
6. **用户友好**: 直观的操作方式，符合用户习惯
7. **性能优化**: 异步图片合成，避免界面卡顿
8. **丝滑旋转**: 实时跟随拖动的旋转体验

## 旋转功能优化

### 问题描述
最初的旋转功能基于累积的移动距离计算旋转角度，导致旋转不够丝滑，用户体验不佳。

### 解决方案
将旋转算法从累积模式改为实时模式：
- **累积模式**: `qrCodeRotation += rotationDelta` - 基于总移动距离
- **实时模式**: `qrCodeRotation = rotationDelta` - 基于当前拖动位置

### 技术改进
1. **提高灵敏度**: 将 `rotationSensitivity` 从 0.5 提升到 0.8
2. **实时响应**: 旋转角度直接跟随拖动位置，提供更直观的反馈
3. **保持限制**: 继续限制旋转角度在 -180° 到 180° 范围内

### 用户体验提升
- **更丝滑**: 旋转操作现在完全跟随手指拖动
- **更直观**: 用户可以直接看到旋转效果与拖动方向的关系
- **更精确**: 实时响应提供更精确的旋转控制

## 基于中心点的角度计算算法

### 算法原理
使用 `atan2` 函数计算触摸点相对于二维码中心的角度变化，实现更精确的旋转控制。

### 核心实现
```swift
// 判断主要操作方向
if abs(translation.width) > abs(translation.height) {
    // 水平移动 - 旋转操作
    isRotating = true
    isScaling = false
    
    // 使用基于中心点的角度计算算法
    let center = CGPoint(x: 100 * qrCodeScale / 2, y: 100 * qrCodeScale / 2)
    let currentTouch = CGPoint(x: translation.width, y: translation.height)
    let previousTouch = CGPoint.zero // 对于实时旋转，前一个位置为原点
    
    // 计算相对于中心点的角度
    let angle1 = atan2(previousTouch.y - center.y, previousTouch.x - center.x)
    let angle2 = atan2(currentTouch.y - center.y, currentTouch.x - center.x)
    
    let angleDelta = (angle2 - angle1) * 180 / .pi // 转换为角度
    qrCodeRotation += angleDelta
    
    // 保持角度在 0-360 范围内
    qrCodeRotation = qrCodeRotation.truncatingRemainder(dividingBy: 360)
    
    // 转换为 -180 到 180 范围
    if qrCodeRotation > 180 {
        qrCodeRotation -= 360
    } else if qrCodeRotation < -180 {
        qrCodeRotation += 360
    }
    
} else {
    // 垂直移动 - 缩放操作
    isScaling = true
    isRotating = false
    
    // 缩放逻辑...
}
```

### 算法优势
1. **精确计算**: 基于数学角度计算，比简单的线性移动更准确
2. **中心点参考**: 以二维码中心为旋转参考点，符合用户直觉
3. **角度累积**: 支持连续旋转，角度值会累积计算
4. **数学基础**: 使用标准的三角函数，计算稳定可靠
5. **方向判断**: 通过比较水平和垂直移动距离来区分旋转和缩放操作

### 技术细节
- **atan2 函数**: 计算点相对于中心的角度，返回弧度值
- **角度转换**: 将弧度转换为角度 (乘以 180/π)
- **角度累积**: 使用 `+=` 操作符累积旋转角度
- **范围限制**: 保持角度在 -180° 到 180° 范围内
- **操作分离**: 水平移动触发旋转，垂直移动触发缩放

## 旋转前翻转问题修复

### 问题描述
用户报告"每次旋转前都是先翻转"的问题，这是由于移除了方向判断逻辑导致的。

### 问题原因
- 移除了 `if abs(translation.width) > abs(translation.height)` 的方向判断
- 导致每次操作都同时执行旋转和缩放
- 缩放操作影响了旋转的视觉效果，造成"翻转"的错觉

### 解决方案
1. **恢复方向判断**: 重新添加水平和垂直移动的判断逻辑
2. **操作分离**: 水平移动专门处理旋转，垂直移动专门处理缩放
3. **状态管理**: 确保 `isRotating` 和 `isScaling` 状态互斥

### 修复效果
- ✅ 旋转操作不再受到缩放干扰
- ✅ 操作更加精确和可预测
- ✅ 用户体验得到显著改善

## 旋转算法优化修复

### 问题描述
用户报告"旋转存在bug"，需要修复旋转算法的问题。

### 问题分析
1. **角度跳跃问题**: 当角度差超过180度时，会出现突然的角度跳跃
2. **中心点计算错误**: 使用了错误的中心点坐标
3. **角度累积问题**: 角度计算不够平滑

### 解决方案
```swift
// 计算二维码中心点
let centerX = 100 * qrCodeScale / 2
let centerY = 100 * qrCodeScale / 2

// 计算当前触摸点相对于中心的角度
let currentAngle = atan2(v.location.y - centerY, v.location.x - centerX)
let startAngle = atan2(v.startLocation.y - centerY, v.startLocation.x - centerX)

// 计算角度差
var angleDelta = (currentAngle - startAngle) * 180 / .pi

// 处理角度跳跃问题
if angleDelta > 180 {
    angleDelta -= 360
} else if angleDelta < -180 {
    angleDelta += 360
}

// 更新旋转角度
qrCodeRotation = qrCodeRotationLast + angleDelta
```

### 技术改进
1. **正确的中心点**: 使用二维码的实际中心点 (100 * qrCodeScale / 2)
2. **角度跳跃处理**: 当角度差超过±180度时，进行360度调整
3. **平滑累积**: 基于起始角度和当前角度计算差值，然后累积到总旋转角度

### 修复效果
- ✅ 旋转更加平滑自然
- ✅ 消除了角度跳跃问题
- ✅ 支持连续旋转操作
- ✅ 旋转中心点准确

## 旋转卡死问题修复

### 问题描述
用户报告"旋转存在卡死现象"，旋转操作时界面会卡死无法响应。

### 问题分析
1. **坐标系统混乱**: 在操作图标的 `DragGesture` 中使用了错误的坐标系统
2. **角度计算错误**: 使用了 `v.location` 和 `v.startLocation`，但这些坐标是相对于整个视图的，不是相对于二维码中心的
3. **状态管理问题**: 没有正确处理手势状态
4. **复杂算法**: 基于中心点的角度计算算法过于复杂，容易导致计算错误

### 解决方案
1. **简化旋转算法**: 回归到简单可靠的线性移动计算
2. **正确的手势处理**: 使用 `value.translation.width` 进行水平移动计算
3. **状态管理优化**: 添加手势时间戳和防卡死机制
4. **角度累积**: 使用 `qrCodeRotationLast` 保持旋转状态

### 核心实现
```swift
.onChanged { value in
    // 更新最后手势时间，防止卡死
    lastGestureTime = Date()
    
    // 使用简单的水平移动计算旋转
    let rotationSensitivity: Double = 0.8
    let rotationDelta = Double(value.translation.width) * rotationSensitivity
    
    // 累积旋转角度
    qrCodeRotation = qrCodeRotationLast + rotationDelta
    
    // 限制旋转角度在合理范围内
    if qrCodeRotation > 360 {
        qrCodeRotation -= 360
    } else if qrCodeRotation < -360 {
        qrCodeRotation += 360
    }
}
.onEnded { _ in
    // 保存当前旋转角度作为下次的起始角度
    qrCodeRotationLast = qrCodeRotation
    
    // 重置状态，防止卡死
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        if Date().timeIntervalSince(lastGestureTime) > 0.5 {
            // 如果超过0.5秒没有新的手势，重置状态
        }
    }
}
```

### 技术改进
1. **算法简化**: 从复杂的 `atan2` 角度计算回归到简单的线性移动
2. **防卡死机制**: 添加手势时间戳和延迟状态重置
3. **状态管理**: 正确的手势开始和结束处理
4. **角度限制**: 合理的角度范围限制，避免无限累积

### 修复效果
- ✅ **消除卡死**: 旋转操作不再卡死
- ✅ **流畅响应**: 旋转操作响应流畅
- ✅ **稳定可靠**: 使用简单可靠的算法
- ✅ **用户体验**: 旋转操作更加自然和可预测

## 使用流程

1. 在 `QRCodeSavedView` 点击"添加到图片"按钮
2. 选择背景图片
3. 进入 `ImageComposerView` 编辑界面
4. 拖拽二维码到合适位置
5. 使用操作图标进行缩放和旋转
6. 点击保存按钮完成合成
7. 选择保存到相册或分享

## 技术要点

- **SwiftUI**: 使用现代 SwiftUI 语法和状态管理
- **手势识别**: 精确的手势识别和处理
- **图片处理**: 高效的图片合成和变换
- **边界计算**: 精确的边界约束算法
- **状态同步**: 完善的状态同步机制
- **性能优化**: 异步处理和内存管理
