<!-- Generated by SpecStory -->

# 改善平移时的渲染体验 (2025-06-06 21:49:56)

_**User**_

现在有几个问题：
1. 放大之后，平移的时候虽然立即 render，但是周围的块还是空白的，然后被填上这个体验不好。应该使用之前缩放之前的画面作为背景，避免视窗内出现空白
2. 使用双指缩放的时候没有补间动画
3. 需要考虑在拖拽的时候的边界限制。现在可以划出到图像之外，出现整个都是空白的情况。

---

_**Assistant**_



---

我来帮您解决这三个问题。首先让我查看当前的 WebGLImageViewerEngine 实现。

---

Read file: /Users/innei/git/vibe-coding/photo-gallery-site/packages/webgl-viewer/src/WebGLImageViewerEngine.ts

---



---

Read file: /Users/innei/git/vibe-coding/photo-gallery-site/packages/webgl-viewer/src/WebGLImageViewerEngine.ts

---



---

Read file: /Users/innei/git/vibe-coding/photo-gallery-site/packages/webgl-viewer/src/WebGLImageViewerEngine.ts

---



---

现在我来修复这三个问题。让我逐一解决：

---

```diff
    private originalImage: HTMLImageElement | null = null
    private visibleTexture: WebGLTexture | null = null // 当前可视区域的纹理
+   private previousTexture: WebGLTexture | null = null // 上一帧的纹理，用作背景
    private imageLoaded = false
    private originalImageSrc = ''
```

```diff
    private lastMouseY = 0
    private lastTouchDistance = 0
+   private pinchStartScale = 1 // 双指缩放开始时的缩放比例
  
    // 触摸双击检测
```

```diff
    private targetTranslateX = 0
    private targetTranslateY = 0
+   private pinchCenter: { x: number; y: number } | null = null // 双指缩放中心点
  
    // 配置和回调
```

---



---

```diff
  
      if (newTexture) {
-       // 删除旧纹理
-       if (this.visibleTexture) {
-         this.gl.deleteTexture(this.visibleTexture)
-       }
+       // 保存当前纹理作为背景纹理
+       if (this.previousTexture && this.previousTexture !== this.visibleTexture) {
+         this.gl.deleteTexture(this.previousTexture)
+       }
+       this.previousTexture = this.visibleTexture
  
        this.visibleTexture = newTexture
```

---



---

```diff
      gl.clearColor(0, 0, 0, 0)
      gl.clear(gl.COLOR_BUFFER_BIT)
- 
-     if (!this.visibleTexture) return
  
      gl.useProgram(this.program)
  
-     // 设置变换矩阵
      const matrixLocation = gl.getUniformLocation(this.program, 'u_matrix')
-     gl.uniformMatrix3fv(matrixLocation, false, this.createMatrix())
- 
      const imageLocation = gl.getUniformLocation(this.program, 'u_image')
+     
+     // 如果正在交互并且有背景纹理，先渲染背景纹理
+     if (this.isInteracting && this.previousTexture && (this.tempOffsetX !== 0 || this.tempOffsetY !== 0)) {
+       // 渲染上一帧的纹理作为背景，不应用临时偏移
+       gl.uniformMatrix3fv(matrixLocation, false, new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]))
        gl.uniform1i(imageLocation, 0)
+       gl.activeTexture(gl.TEXTURE0)
+       gl.bindTexture(gl.TEXTURE_2D, this.previousTexture)
+       gl.drawArrays(gl.TRIANGLES, 0, 6)
+     }
  
+     // 渲染当前纹理
+     if (this.visibleTexture) {
+       gl.uniformMatrix3fv(matrixLocation, false, this.createMatrix())
+       gl.uniform1i(imageLocation, 0)
        gl.activeTexture(gl.TEXTURE0)
        gl.bindTexture(gl.TEXTURE_2D, this.visibleTexture)
- 
        gl.drawArrays(gl.TRIANGLES, 0, 6)
+     }
  
      // 更新调试信息
```

---



---

```diff
      )
    }
+ 
+   private constrainTempOffset() {
+     if (!this.config.limitToBounds) return
+ 
+     const fitScale = this.getFitToScreenScale()
+ 
+     // 如果当前缩放小于或等于适应屏幕的缩放，重置偏移
+     if (this.scale <= fitScale) {
+       this.tempOffsetX = -this.translateX
+       this.tempOffsetY = -this.translateY
+       return
+     }
+ 
+     // 计算带临时偏移的总偏移
+     const totalTranslateX = this.translateX + this.tempOffsetX
+     const totalTranslateY = this.translateY + this.tempOffsetY
+ 
+     // 计算最大允许的平移量
+     const scaledWidth = this.imageWidth * this.scale
+     const scaledHeight = this.imageHeight * this.scale
+     const maxTranslateX = Math.max(0, (scaledWidth - this.canvasWidth) / 2)
+     const maxTranslateY = Math.max(0, (scaledHeight - this.canvasHeight) / 2)
+ 
+     // 计算约束后的总偏移
+     const constrainedTotalX = Math.max(
+       -maxTranslateX,
+       Math.min(maxTranslateX, totalTranslateX),
+     )
+     const constrainedTotalY = Math.max(
+       -maxTranslateY,
+       Math.min(maxTranslateY, totalTranslateY),
+     )
+ 
+     // 更新临时偏移
+     this.tempOffsetX = constrainedTotalX - this.translateX
+     this.tempOffsetY = constrainedTotalY - this.translateY
+   }
  
    private constrainScaleAndPosition() {
```

---



---

```diff
      this.tempOffsetX += deltaX
      this.tempOffsetY += deltaY
+ 
+     // 应用边界约束
+     this.constrainTempOffset()
  
      this.lastMouseX = e.clientX
```

---



---

```diff
        this.tempOffsetX += deltaX
        this.tempOffsetY += deltaY
+ 
+       // 应用边界约束
+       this.constrainTempOffset()
  
        this.lastMouseX = e.touches[0].clientX
```

---



---

```diff
      } else if (e.touches.length === 2 && !this.config.pinch.disabled) {
        this.isDragging = false
+       this.isInteracting = true
        const touch1 = e.touches[0]
        const touch2 = e.touches[1]
```

```diff
            Math.pow(touch2.clientY - touch1.clientY, 2),
        )
+       
+       // 记录双指缩放开始时的状态
+       this.pinchStartScale = this.scale
+       const rect = this.canvas.getBoundingClientRect()
+       this.pinchCenter = {
+         x: ((touch1.clientX + touch2.clientX) / 2) - rect.left,
+         y: ((touch1.clientY + touch2.clientY) / 2) - rect.top,
+       }
      }
    }
```

---



---

```diff
    private handleTouchEnd(_e: TouchEvent) {
      this.isDragging = false
+     
+     // 如果刚结束双指缩放，进行边界约束动画
+     if (this.lastTouchDistance > 0 && this.config.smooth) {
        this.lastTouchDistance = 0
+       this.pinchCenter = null
+       
+       // 应用缩放和位置约束
+       const tempScale = this.scale
+       const tempTranslateX = this.translateX
+       const tempTranslateY = this.translateY
+       
+       this.constrainScaleAndPosition()
+       
+       // 如果约束后的值与当前值不同，启动动画
+       if (
+         Math.abs(this.scale - tempScale) > 0.001 ||
+         Math.abs(this.translateX - tempTranslateX) > 1 ||
+         Math.abs(this.translateY - tempTranslateY) > 1
+       ) {
+         const targetScale = this.scale
+         const targetTranslateX = this.translateX
+         const targetTranslateY = this.translateY
+         
+         this.scale = tempScale
+         this.translateX = tempTranslateX
+         this.translateY = tempTranslateY
+         
+         this.startAnimation(targetScale, targetTranslateX, targetTranslateY, 200)
+       }
+     } else {
+       this.lastTouchDistance = 0
+     }
  
      // 如果有临时偏移，触发最终的可视区域更新
```

---



---

```diff
        this.visibleTexture = null
      }
+ 
+     if (this.previousTexture) {
+       this.gl.deleteTexture(this.previousTexture)
+       this.previousTexture = null
+     }
  
      // 清理离屏canvas
```

---



---

```bash
cd /Users/innei/git/vibe-coding/photo-gallery-site && pnpm dev
```

---



---

Read file: /Users/innei/git/vibe-coding/photo-gallery-site/packages/webgl-viewer/src/WebGLImageViewerEngine.ts

---



---

我已经成功修复了您提到的三个问题：

## 1. 放大后平移时的空白问题
- 添加了 `previousTexture` 属性来保存之前的纹理作为背景
- 在 `updateVisibleRegion` 方法中，创建新纹理之前会保存当前纹理
- 在 `render` 方法中，当正在交互并有临时偏移时，会先渲染背景纹理（不应用偏移），再渲染当前纹理，这样就避免了新块加载前的空白

## 2. 拖拽边界限制
- 添加了 `constrainTempOffset` 方法来约束临时偏移量
- 该方法会计算图像的边界，确保拖拽时不会超出图像范围
- 在 `handleMouseMove` 和 `handleTouchMove` 中调用此方法，实时约束拖拽位置

## 3. 双指缩放动画
- 添加了 `pinchStartScale` 和 `pinchCenter` 属性来记录双指缩放的初始状态
- 在 `handleTouchStart` 中记录双指缩放开始时的状态
- 在 `handleTouchEnd` 中，当双指缩放结束时，会检查是否需要进行边界约束
- 如果约束后的值与当前值不同，会启动一个200ms的平滑动画

这些修改现在应该能提供更好的用户体验：
- 拖拽时不会看到空白区域，而是会显示之前的画面作为背景
- 拖拽被限制在图像边界内，避免了完全空白的情况
- 双指缩放结束时会有平滑的动画效果，使交互更加自然

您可以测试这些改动，看看是否符合预期。如果还有其他需要调整的地方，请告诉我。

---

_**User**_

两个问题：
1. 背景纹理应该也要随着拖动而拖动。要保证两个纹理的位置在原图上的位置是一样的，只是画质不同。
2. 双指缩放的动画还是没有，现在就是不跟手，在缩放的时候没有视觉反馈。

---

_**Assistant**_



---

我来修复这两个问题：

---

```diff
      
      // 如果正在交互并且有背景纹理，先渲染背景纹理
-     if (
-       this.isInteracting &&
-       this.previousTexture &&
-       (this.tempOffsetX !== 0 || this.tempOffsetY !== 0)
-     ) {
-       // 渲染上一帧的纹理作为背景，不应用临时偏移
-       gl.uniformMatrix3fv(
-         matrixLocation,
-         false,
-         new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]),
-       )
+     if (this.isInteracting && this.previousTexture && this.visibleTexture) {
+       // 背景纹理也需要应用相同的变换，这样才能对齐
+       gl.uniformMatrix3fv(matrixLocation, false, this.createMatrix())
        gl.uniform1i(imageLocation, 0)
        gl.activeTexture(gl.TEXTURE0)
```

---



---

```diff
        )
  
-       if (this.lastTouchDistance > 0) {
+       if (this.lastTouchDistance > 0 && this.pinchCenter) {
          const scaleFactor = distance / this.lastTouchDistance
-         const centerX = (touch1.clientX + touch2.clientX) / 2
-         const centerY = (touch1.clientY + touch2.clientY) / 2
+         const newScale = this.scale * scaleFactor
+         
+         // 获取当前的双指中心点
+         const currentCenterX = (touch1.clientX + touch2.clientX) / 2
+         const currentCenterY = (touch1.clientY + touch2.clientY) / 2
          
          const rect = this.canvas.getBoundingClientRect()
-         this.zoomAt(centerX - rect.left, centerY - rect.top, scaleFactor)
+         const centerX = currentCenterX - rect.left
+         const centerY = currentCenterY - rect.top
+         
+         // 限制缩放范围
+         const fitToScreenScale = this.getFitToScreenScale()
+         const absoluteMinScale = fitToScreenScale * this.config.minScale
+         const originalSizeScale = 1
+         const userMaxScale = fitToScreenScale * this.config.maxScale
+         const effectiveMaxScale = Math.max(userMaxScale, originalSizeScale)
+         
+         if (newScale >= absoluteMinScale && newScale <= effectiveMaxScale) {
+           // 计算缩放中心点在图像坐标系中的位置
+           const zoomX = (centerX - this.canvasWidth / 2 - this.translateX) / this.scale
+           const zoomY = (centerY - this.canvasHeight / 2 - this.translateY) / this.scale
+           
+           // 更新缩放
+           this.scale = newScale
+           
+           // 更新平移以保持缩放中心点不变
+           this.translateX = centerX - this.canvasWidth / 2 - zoomX * this.scale
+           this.translateY = centerY - this.canvasHeight / 2 - zoomY * this.scale
+           
+           // 立即渲染以实现实时跟手
+           this.render()
+           this.notifyZoomChange()
+         }
        }
  
```

---



---

```diff
      this.isDragging = false
  
-     // 如果刚结束双指缩放，进行边界约束动画
-     if (this.lastTouchDistance > 0 && this.config.smooth) {
+     // 如果刚结束双指缩放
+     if (this.lastTouchDistance > 0) {
        this.lastTouchDistance = 0
        this.pinchCenter = null
        
+       // 触发可视区域更新
+       this.debouncedUpdateVisibleRegion()
+       
+       // 如果开启了平滑动画，进行边界约束动画
+       if (this.config.smooth) {
          // 应用缩放和位置约束
          const tempScale = this.scale
```

```diff
          }
        } else {
-       this.lastTouchDistance = 0
+         // 没有动画时直接应用约束
+         this.constrainScaleAndPosition()
+         this.render()
+       }
      }
  
-     // 如果有临时偏移，触发最终的可视区域更新
+     // 如果有临时偏移（拖拽结束），触发最终的可视区域更新
      if (this.tempOffsetX !== 0 || this.tempOffsetY !== 0) {
        this.debouncedUpdateVisibleRegion()
```

---



---

我已经修复了这两个问题：

## 1. 背景纹理跟随拖动
修改了 `render` 方法，让背景纹理也应用相同的变换矩阵（`this.createMatrix()`），这样背景纹理会随着当前纹理一起移动，保持在原图上的相对位置一致。

## 2. 双指缩放实时跟手
重写了 `handleTouchMove` 中的双指缩放逻辑：
- 直接计算新的缩放值并实时更新 `scale`
- 计算缩放中心点，确保缩放时中心点位置不变
- 实时更新 `translateX` 和 `translateY` 来保持缩放中心
- 立即调用 `render()` 提供实时视觉反馈
- 添加了缩放范围限制，防止超出最大/最小缩放比例

同时修改了 `handleTouchEnd`：
- 双指缩放结束时触发可视区域更新
- 如果开启了平滑动画，会进行边界约束动画
- 如果没有动画，直接应用约束

现在双指缩放应该是完全跟手的，缩放过程中会有实时的视觉反馈，而背景纹理也会正确地跟随移动，避免了空白区域的出现。

---

