1014 lines
31 KiB
Markdown
1014 lines
31 KiB
Markdown
# 移除光栅卡硬件陀螺仪 · 实施计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 彻底移除光栅卡/全息卡对硬件陀螺仪/加速度计的依赖(含 Android 原生插件),改用触摸拖拽 + 手动滑块双输入,简化 ~28KB 硬件传感器代码、消除 iOS/Android 权限摩擦。
|
||
|
||
**Architecture:** 在 `useLenticularCraftTiltPreview` 暴露 `simulateTilt(dx, dy)`(度)作为唯一倾斜入口;触摸拖拽通过 `simulateTiltFromNormalized` 转换;新增独立组件 `<ManualTiltSlider>` 通过卡片内置 `showSlider` prop 透出;v-model:sliderValue 双向绑定。所有硬件相关代码、依赖、权限、原生插件全部删除。
|
||
|
||
**Tech Stack:** uni-app 3.x + Vue 3 组合式 API + uni-app 原生 `<slider>` 组件;H5 Chrome 作为开发期验证平台。
|
||
|
||
## Global Constraints
|
||
|
||
- **测试原则**:前端不写 Vitest(memory `share-impl-test-policy.md`),所有验证通过 H5 Chrome 手动 + DevTools console + `console.log` 完成
|
||
- **平台目标**:以 app-plus(Android + iOS)为主,H5 同样支持;H5 验证为开发期主线
|
||
- **包大小目标**:删除 ≥28KB 硬件代码 + 1 个原生插件目录
|
||
- **提交规范**:每个 Task 一个 commit,commit 末尾追加 `Co-Authored-By: Claude <noreply@anthropic.com>`
|
||
- **分支**:当前在 `feat/actvity`(或显示为 `feat/len`),**不要** checkout 到 main
|
||
- **数值约定**(spec §3.5):
|
||
- 触摸 emit 实际 [-1.1, +1.1],有效 [-1, +1]
|
||
- 滑块 value: 0..1,中位 0.5 = 0°
|
||
- `simulateTilt(dx, dy)`: -45..+45 度
|
||
- 滑块→度: `dx = (v - 0.5) * 90`
|
||
- 触摸→度: `dx = x * 45`
|
||
- **错误处理**(spec §4.2):`simulateTilt` 入口 `Number.isFinite()` 校验;`layersRef.length < 2` 兜底保留
|
||
- **Phase 5 grep 校验**(必须 0 命中):`useLenticularStudioTilt` / `useOneEuroFilter` / `imengyu-UniAndroidGyro` / `BODY_SENSORS` / `NSMotionUsageDescription`
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
| 文件 | 操作 | 职责 |
|
||
|------|------|------|
|
||
| `frontend/composables/useLenticularCraftTiltPreview.js` | 改 | 新增 `simulateTilt*` 入口;最终删除 `useLenticularStudioTilt` 引用 |
|
||
| `frontend/composables/useLenticularStudioTilt.js` | 删 | 28KB 硬件传感器模块 |
|
||
| `frontend/composables/useOneEuroFilter.js` | 删 | 仅被 StudioTilt 使用 |
|
||
| `frontend/composables/useHolographicPreview.js` | 改 | 删 DeviceOrientationEvent 路径;保留 `simulate(x, y)` |
|
||
| `frontend/components/lenticular/ManualTiltSlider.vue` | 新增 | 独立滑块组件 |
|
||
| `frontend/components/lenticular/LenticularCard.vue` | 改 | 加 slider 接入 + touchend 不发 (0,0) |
|
||
| `frontend/components/lenticular/HolographicCard.vue` | 改 | 同上 |
|
||
| `frontend/components/lenticular/CastloveLenticularPreview.vue` | 改 | 透传 show-slider prop |
|
||
| `frontend/pages/castlove/lenticular/lenticular-result.vue` | 改 | 加 show-slider + 改 simulate 接收器 + 删 scheduleTiltStart |
|
||
| `frontend/nativePlugins/imengyu-UniAndroidGyro/` | 删 | 原生插件目录(AAR + iOS Framework) |
|
||
| `frontend/manifest.json` | 改 | 删 nativePlugins 段 + BODY_SENSORS + NSMotionUsageDescription |
|
||
|
||
---
|
||
|
||
## Task 1: 在 useLenticularCraftTiltPreview 新增手动倾斜入口
|
||
|
||
**Files:**
|
||
- Modify: `frontend/composables/useLenticularCraftTiltPreview.js:176-213`
|
||
|
||
**Interfaces:**
|
||
- Consumes: 现有的 `useLenticularStudioTilt`(仍保留运行,本 Task 不动)
|
||
- Produces: 新增 3 个公共方法 `simulateTilt(dx, dy)` / `simulateTiltFromNormalized(x, y)` / `simulateTiltReset()`
|
||
|
||
- [ ] **Step 1: 在 useLenticularCraftTiltPreview 顶部添加数值常量**
|
||
|
||
打开 `frontend/composables/useLenticularCraftTiltPreview.js`,在第 12 行附近(`FLOW_PHYSICS` 之前)插入:
|
||
|
||
```js
|
||
// 手动模拟倾斜:归一化 → 度数 转换系数(spec §3.5)
|
||
const NORMALIZED_TO_DEG = 45
|
||
```
|
||
|
||
- [ ] **Step 2: 在 `useLenticularCraftTiltPreview` 函数体内(`return` 语句之前)新增 3 个方法**
|
||
|
||
定位到文件末尾 `return { ... }` 之前(大约 line 210),在 `stopTiltPreview` 函数定义之后插入:
|
||
|
||
```js
|
||
// ====================================================================
|
||
// 手动模拟倾斜入口(spec §3.1)
|
||
// ====================================================================
|
||
|
||
/**
|
||
* 滑块/外部入口:直接传度数(-45..+45)
|
||
* 内部转给 simulateFromSignedDegrees 走现有离散档位逻辑
|
||
*/
|
||
function simulateTilt(dx, dy) {
|
||
if (!Number.isFinite(dx) || !Number.isFinite(dy)) {
|
||
console.warn('[CraftTilt] simulateTilt: non-finite input', dx, dy)
|
||
return
|
||
}
|
||
simulateFromSignedDegrees(dx, dy)
|
||
}
|
||
|
||
/**
|
||
* 触摸入口:-1..+1 归一化转度数(系数 45)
|
||
* 由 LenticularCard.vue / HolographicCard.vue 的 @simulate 事件触发
|
||
*/
|
||
function simulateTiltFromNormalized(x, y) {
|
||
const dx = x * NORMALIZED_TO_DEG
|
||
const dy = (y != null ? y : 0) * NORMALIZED_TO_DEG
|
||
simulateTilt(dx, dy)
|
||
}
|
||
|
||
/**
|
||
* 滑块点"复位"时调用:归零 + 清空离散档位状态
|
||
*/
|
||
function simulateTiltReset() {
|
||
lockPreviewStill()
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 在 return 对象中暴露这 3 个方法**
|
||
|
||
修改文件末尾的 `return { ... }`(约 line 215-225),添加这 3 个键:
|
||
|
||
```js
|
||
return {
|
||
physics,
|
||
layerTransforms,
|
||
simulate,
|
||
relax,
|
||
gyroSourceLabel,
|
||
scheduleTiltStart,
|
||
stopTiltPreview,
|
||
lockPreviewStill,
|
||
// 新增:手动模拟入口(spec §3.1)
|
||
simulateTilt,
|
||
simulateTiltFromNormalized,
|
||
simulateTiltReset,
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: H5 Chrome 手动验证(在 DevTools console)**
|
||
|
||
启动 H5 dev server(具体命令见项目 README),打开 `lenticular-result` 页面,浏览器 DevTools console 执行:
|
||
|
||
```js
|
||
// 从 Vue 组件实例拿 useLenticularCraftTiltPreview 返回的 simulateTilt
|
||
//(具体取法:组件实例 proxy 上找,或者在 setup 顶层临时挂到 window 调试)
|
||
window.__test_simulateTilt?.(45, 0) // 应触发右边硬切
|
||
window.__test_simulateTilt?.(0, 0) // 应触发归中
|
||
window.__test_simulateTilt?.(NaN, 0) // 应 console.warn 不崩
|
||
```
|
||
|
||
> 注:如果没有 `window.__test_*` 暴露,可以临时在组件 setup 末尾加 `window.__test_simulateTilt = simulateTilt` 调试,验证完删除。
|
||
|
||
**预期**:
|
||
- 前两次调用:卡片视觉有响应(γ → ±1)
|
||
- 第三次:DevTools console 出现 `[CraftTilt] simulateTilt: non-finite input` 警告,卡片无变化
|
||
|
||
- [ ] **Step 5: 提交**
|
||
|
||
```bash
|
||
git add frontend/composables/useLenticularCraftTiltPreview.js
|
||
git commit -m "feat(craft-tilt): 新增 simulateTilt 手动模拟入口
|
||
|
||
- simulateTilt(dx, dy) 单位度,校验 NaN/Infinity
|
||
- simulateTiltFromNormalized(x, y) 触摸归一化转度(系数 45)
|
||
- simulateTiltReset() 滑块复位用
|
||
|
||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: 新增 ManualTiltSlider.vue 独立组件
|
||
|
||
**Files:**
|
||
- Create: `frontend/components/lenticular/ManualTiltSlider.vue`
|
||
|
||
**Interfaces:**
|
||
- Consumes: 无(独立组件)
|
||
- Produces: `<ManualTiltSlider value disabled showReset min max step @change @reset>`
|
||
|
||
- [ ] **Step 1: 创建组件文件,写入完整模板和 script**
|
||
|
||
新建 `frontend/components/lenticular/ManualTiltSlider.vue`:
|
||
|
||
```vue
|
||
<template>
|
||
<view v-if="showReset" class="tilt-slider-row">
|
||
<view v-if="showReset" class="tilt-slider-reset" @tap="handleReset">
|
||
<text class="tilt-slider-reset-icon">↺</text>
|
||
</view>
|
||
<slider
|
||
class="tilt-slider"
|
||
:min="min"
|
||
:max="max"
|
||
:step="step"
|
||
:value="value"
|
||
:disabled="disabled"
|
||
:activeColor="activeColor"
|
||
:backgroundColor="backgroundColor"
|
||
:show-value="false"
|
||
@change="handleChange"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
const props = defineProps({
|
||
value: { type: Number, default: 0.5, validator: v => v >= 0 && v <= 1 },
|
||
disabled: { type: Boolean, default: false },
|
||
showReset: { type: Boolean, default: true },
|
||
min: { type: Number, default: 0 },
|
||
max: { type: Number, default: 1 },
|
||
step: { type: Number, default: 0.01 },
|
||
activeColor: { type: String, default: '#6E7AFF' },
|
||
backgroundColor: { type: String, default: '#E5E7EB' },
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
change: [v: number]
|
||
reset: []
|
||
}>()
|
||
|
||
function handleChange(e) {
|
||
const v = Number(e.detail.value)
|
||
if (!Number.isFinite(v)) {
|
||
console.warn('[ManualTiltSlider] non-finite value', e.detail.value)
|
||
return
|
||
}
|
||
if (v < 0 || v > 1) {
|
||
console.warn('[ManualTiltSlider] value out of [0,1]', v)
|
||
return
|
||
}
|
||
emit('change', v)
|
||
}
|
||
|
||
function handleReset() {
|
||
emit('reset')
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.tilt-slider-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
padding: 8rpx 16rpx;
|
||
height: 48rpx;
|
||
}
|
||
.tilt-slider-reset {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.tilt-slider-reset-icon {
|
||
font-size: 28rpx;
|
||
color: #6B7280;
|
||
}
|
||
.tilt-slider {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
- [ ] **Step 2: 临时挂载到 H5 测试页面验证**
|
||
|
||
找一个测试用页面(如 `pages/index/index.vue`)临时挂载:
|
||
|
||
```vue
|
||
<ManualTiltSlider :value="0.5" @change="v => console.log('change', v)" @reset="console.log('reset')" />
|
||
```
|
||
|
||
H5 Chrome 打开该页面,验证:
|
||
- 拖动滑块 → console 打印 `change 0.42` 之类的数值
|
||
- 点击 ↺ 按钮 → console 打印 `reset`
|
||
- 滑块无法拖到 0..1 范围外(uniapp `<slider>` 组件自带 clamp)
|
||
|
||
- [ ] **Step 3: 还原测试挂载**
|
||
|
||
如果步骤 2 加了临时代码,删除或还原。
|
||
|
||
- [ ] **Step 4: 提交**
|
||
|
||
```bash
|
||
git add frontend/components/lenticular/ManualTiltSlider.vue
|
||
git commit -m "feat(lenticular): 新增 ManualTiltSlider 独立组件
|
||
|
||
- uniapp <slider> 包装,0..1 双向绑定
|
||
- 内置 ↺ 复位按钮
|
||
- value validator 拦截 0..1 范围外输入
|
||
|
||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: LenticularCard.vue 加 slider 接入 + 改 touchend 行为
|
||
|
||
**Files:**
|
||
- Modify: `frontend/components/lenticular/LenticularCard.vue:54-63, 209-212`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Task 2 产出的 `<ManualTiltSlider>`
|
||
- Produces: 新增 props `showSlider` / `sliderValue`;新增 emit `update:sliderValue` / `sliderReset`;touchend 不再 emit (0,0)
|
||
|
||
- [ ] **Step 1: 添加 showSlider / sliderValue props**
|
||
|
||
在 `frontend/components/lenticular/LenticularCard.vue` 的 `defineProps({...})` 块(line 54-63)末尾追加:
|
||
|
||
```js
|
||
// 新增:手动滑块接入(spec §3.3)
|
||
showSlider: { type: Boolean, default: false },
|
||
sliderValue: { type: Number, default: 0.5 },
|
||
```
|
||
|
||
- [ ] **Step 2: 在 defineEmits 中添加 update:sliderValue / sliderReset**
|
||
|
||
找到 `defineEmits`(如果有)或 `<script setup>` 顶部,添加:
|
||
|
||
```js
|
||
const emit = defineEmits(['simulate', 'update:sliderValue', 'sliderReset'])
|
||
```
|
||
|
||
> 注:如果 LenticularCard.vue 原本没有 `defineEmits`,需要新建一行。
|
||
|
||
- [ ] **Step 3: 导入并使用 ManualTiltSlider**
|
||
|
||
在 `<script setup>` 顶部 `import` 区添加:
|
||
|
||
```js
|
||
import ManualTiltSlider from './ManualTiltSlider.vue'
|
||
```
|
||
|
||
在模板中(`</view>` 结束卡片框架之后),追加:
|
||
|
||
```vue
|
||
<ManualTiltSlider
|
||
v-if="showSlider"
|
||
:value="sliderValue"
|
||
@change="v => emit('update:sliderValue', v)"
|
||
@reset="emit('sliderReset')"
|
||
/>
|
||
```
|
||
|
||
- [ ] **Step 4: 修复 touchend emit (0,0) bug**
|
||
|
||
修改 `frontend/components/lenticular/LenticularCard.vue` 的 `onFrameTouchEnd` 函数(line 209-212):
|
||
|
||
```js
|
||
function onFrameTouchEnd() {
|
||
if (props.skipBuiltInTouch) return
|
||
// 修改前:emit('simulate', 0, 0) ← 与滑块数据驱动冲突
|
||
// 修改后:触摸结束不再强制发 (0,0),由调用方决定
|
||
// (spec §4.1 修复方案)
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: H5 Chrome 验证**
|
||
|
||
打开任意挂载 `<LenticularCard>` 的页面(H5 dev server),DevTools 验证:
|
||
|
||
```js
|
||
// 1. touchend 后卡片不强制回中
|
||
// 操作:拖动卡片到右边 → 松开 → 卡片应保持右边位置
|
||
|
||
// 2. v-model:sliderValue 双向绑定
|
||
// 临时挂载(dev):<LenticularCard :show-slider="true" v-model:sliderValue="..." />
|
||
// 操作:拖动滑块 → 卡片视觉跟随
|
||
```
|
||
|
||
- [ ] **Step 6: 提交**
|
||
|
||
```bash
|
||
git add frontend/components/lenticular/LenticularCard.vue
|
||
git commit -m "feat(lenticular-card): 接入 ManualTiltSlider + 修 touchend 行为
|
||
|
||
- 新增 showSlider / sliderValue props
|
||
- 新增 update:sliderValue / sliderReset emits(v-model 配套)
|
||
- 触摸结束不再 emit (0,0),与滑块数据驱动相容
|
||
|
||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: HolographicCard.vue 同步 Task 3 改动
|
||
|
||
**Files:**
|
||
- Modify: `frontend/components/lenticular/HolographicCard.vue:43-67, 129-132`
|
||
|
||
**Interfaces:** 与 Task 3 相同的 prop / emit 接口
|
||
|
||
- [ ] **Step 1: 添加 props**
|
||
|
||
在 `HolographicCard.vue` 的 `defineProps({...})` 块(line 43-67)末尾追加:
|
||
|
||
```js
|
||
// 新增:手动滑块接入(与 LenticularCard 对齐)
|
||
showSlider: { type: Boolean, default: false },
|
||
sliderValue: { type: Number, default: 0.5 },
|
||
```
|
||
|
||
- [ ] **Step 2: 在 defineEmits 中添加 update:sliderValue / sliderReset**
|
||
|
||
修改 `const emit = defineEmits([...])`(line 67):
|
||
|
||
```js
|
||
const emit = defineEmits(['simulate', 'update:sliderValue', 'sliderReset', 'ready', 'error', 'fpsUpdate'])
|
||
```
|
||
|
||
- [ ] **Step 3: 导入 ManualTiltSlider 并在模板中渲染**
|
||
|
||
`<script setup>` 顶部添加:
|
||
|
||
```js
|
||
import ManualTiltSlider from './ManualTiltSlider.vue'
|
||
```
|
||
|
||
模板中(`</view>` 结束 `holo-frame` 之后)添加:
|
||
|
||
```vue
|
||
<ManualTiltSlider
|
||
v-if="showSlider"
|
||
:value="sliderValue"
|
||
@change="v => emit('update:sliderValue', v)"
|
||
@reset="emit('sliderReset')"
|
||
/>
|
||
```
|
||
|
||
- [ ] **Step 4: 修复 touchend emit (0,0) bug**
|
||
|
||
修改 `onTouchEnd` 函数(line 129-132):
|
||
|
||
```js
|
||
function onTouchEnd() {
|
||
if (props.skipBuiltInTouch) return
|
||
// 修改前:emit('simulate', 0, 0)
|
||
// 修改后:触摸结束不再发 (0,0)
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: 提交**
|
||
|
||
```bash
|
||
git add frontend/components/lenticular/HolographicCard.vue
|
||
git commit -m "feat(holographic-card): 接入 ManualTiltSlider + 修 touchend 行为
|
||
|
||
- 与 LenticularCard 对齐的 showSlider / sliderValue / v-model 接口
|
||
- 触摸结束不再 emit (0,0)
|
||
|
||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: CastloveLenticularPreview.vue 加 show-slider prop 透传
|
||
|
||
**Files:**
|
||
- Modify: `frontend/components/lenticular/CastloveLenticularPreview.vue:4-14`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Task 3 产出的 `<LenticularCard :show-slider :slider-value>`
|
||
- Produces: 透传 prop 让调用方控制
|
||
|
||
- [ ] **Step 1: 添加 showSlider prop**
|
||
|
||
修改 `CastloveLenticularPreview.vue` 的 `defineProps({...})` 块(line 22-28),在 `imageSrc` 后追加:
|
||
|
||
```js
|
||
showSlider: { type: Boolean, default: false },
|
||
```
|
||
|
||
- [ ] **Step 2: 透传到 LenticularCard**
|
||
|
||
修改模板(line 4-12):
|
||
|
||
```vue
|
||
<LenticularCard
|
||
:layers="lenticularLayers"
|
||
:transforms="layerTransforms"
|
||
gyro-source="simulation"
|
||
:approximate-preview="true"
|
||
tilt-hint-text="滑动卡片预览光栅效果"
|
||
:show-slider="showSlider"
|
||
@simulate="simulate"
|
||
/>
|
||
```
|
||
|
||
- [ ] **Step 3: 提交**
|
||
|
||
```bash
|
||
git add frontend/components/lenticular/CastloveLenticularPreview.vue
|
||
git commit -m "feat(castlove-preview): 透传 showSlider prop
|
||
|
||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: lenticular-result.vue 切换到手动模式
|
||
|
||
**Files:**
|
||
- Modify: `frontend/pages/castlove/lenticular/lenticular-result.vue:31-34, 66, 102-109`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Task 1 产出的 `simulateTilt / simulateTiltFromNormalized / simulateTiltReset`;Task 3 产出的卡片 v-model 接口
|
||
- Produces: 移除 `scheduleTiltStart/stopTiltPreview` 调用;加 show-slider;**触摸 ↔ 滑块 双向同步**(用户最新澄清要求)
|
||
|
||
**关键设计:触摸 ↔ 滑块 双向同步**
|
||
- 触摸 x ∈ [-1, +1] → 滑块 value ∈ [0, 1]:`slider = (x + 1) / 2`(clamp 到 [0,1])
|
||
- 滑块 value ∈ [0, 1] → 触摸角度:滑块拖动时 `simulateTilt((v - 0.5) * 90, 0)`,让卡片视觉跟到那个角度
|
||
- 双向都通过 `tiltValue` 这个 ref 作为 single source of truth
|
||
|
||
- [ ] **Step 1: 从 useLenticularCraftTiltPreview 解构 simulateTilt / simulateTiltFromNormalized / simulateTiltReset**
|
||
|
||
修改 `useLenticularCraftTiltPreview` 解构(line 102-109):
|
||
|
||
```js
|
||
const {
|
||
layerTransforms,
|
||
simulateTilt,
|
||
simulateTiltFromNormalized,
|
||
simulateTiltReset,
|
||
} = useLenticularCraftTiltPreview(lenticularLayers)
|
||
```
|
||
|
||
- [ ] **Step 2: 添加 tiltValue 状态(用于 v-model:sliderValue,触摸/滑块 single source of truth)**
|
||
|
||
在 `lenticularLayers` ref 声明(line 102)之后,添加:
|
||
|
||
```js
|
||
const tiltValue = ref(0.5)
|
||
```
|
||
|
||
- [ ] **Step 3: 添加 3 个事件处理函数(触摸/滑块双向同步)**
|
||
|
||
在 `tiltValue` ref 声明后,添加 3 个函数:
|
||
|
||
```js
|
||
/**
|
||
* 触摸 → 滑块 + 引擎
|
||
* 把触摸 x 投影到滑块 0..1(用户最新要求:手滑时滑块也动)
|
||
*/
|
||
function onTouchSimulate(x, y) {
|
||
const sliderPos = Math.max(0, Math.min(1, (x + 1) / 2))
|
||
tiltValue.value = sliderPos
|
||
simulateTiltFromNormalized(x, y)
|
||
}
|
||
|
||
/**
|
||
* 滑块 → 引擎(tiltValue 已被 v-model 自动更新,这里只驱动引擎)
|
||
* 用户最新要求:滑块滑动,相关的手滑/卡片也到那个角度
|
||
*/
|
||
function onSliderChange(v) {
|
||
const dx = (v - 0.5) * 90
|
||
simulateTilt(dx, 0)
|
||
}
|
||
|
||
/**
|
||
* 复位按钮:滑块回 0.5 + 引擎回中
|
||
*/
|
||
function onSliderReset() {
|
||
tiltValue.value = 0.5
|
||
simulateTiltReset()
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 改 LenticularCard 标签(接 3 个新事件处理)**
|
||
|
||
修改 `<LenticularCard>` 标签(line 31-34):
|
||
|
||
```vue
|
||
<LenticularCard v-if="lenticularLayers.length > 0" class="craft-lenticular-card"
|
||
:layers="lenticularLayers" :transforms="layerTransforms"
|
||
:show-slider="true" v-model:slider-value="tiltValue"
|
||
tilt-hint-text="晃动或滑动查看" :shimmer-mid-opacity="0.16"
|
||
@simulate="onTouchSimulate"
|
||
@update:slider-value="onSliderChange"
|
||
@slider-reset="onSliderReset" />
|
||
```
|
||
|
||
注意改动点:
|
||
- 删 `:skip-built-in-touch="true"`(让触摸事件生效)
|
||
- 加 `:show-slider="true"`
|
||
- 加 `v-model:slider-value="tiltValue"`(双向)
|
||
- `@simulate` 改为 `onTouchSimulate`(同步触摸 → 滑块 + 引擎)
|
||
- 加 `@update:slider-value="onSliderChange"`(同步滑块 → 引擎)
|
||
- 加 `@slider-reset="onSliderReset"`(复位)
|
||
- 改 `tilt-hint-text` 文案
|
||
|
||
- [ ] **Step 5: 删除 scheduleTiltStart / stopTiltPreview / lockPreviewStill 引用**
|
||
|
||
搜索整个 `lenticular-result.vue`:
|
||
|
||
```bash
|
||
grep -n "scheduleTiltStart\|stopTiltPreview\|gyroSourceLabel" frontend/pages/castlove/lenticular/lenticular-result.vue
|
||
```
|
||
|
||
预期命中 3-4 处(line 107、713、769、774、778 等)。逐处删除:
|
||
- 解构中的 `gyroSourceLabel / scheduleTiltStart / stopTiltPreview` 已通过 Step 1 覆盖
|
||
- 函数调用处的 `scheduleTiltStart();` 和 `stopTiltPreview();` 直接删除
|
||
- 模板中如有 `{{ gyroSourceLabel }}` 引用也删除
|
||
|
||
- [ ] **Step 6: H5 Chrome 端到端验证(重点验证双向同步)**
|
||
|
||
H5 dev server 打开 `lenticular-result` 页面:
|
||
1. 卡片正常显示
|
||
2. **手滑 → 滑块跟动**:拖动卡片到最右 → 滑块应自动滑到右边(接近 1.0)
|
||
3. **手滑 → 滑块跟动(中间)**:拖动卡片到中心 → 滑块应回到 0.5
|
||
4. **手滑 → 卡片**:拖动卡片到最左 → 滑块到 0、卡片到最左
|
||
5. **滑块 → 卡片**:拖动滑块到 0.7 → 卡片光栅应到对应位置
|
||
6. **松开不强制回中**:手指松开后滑块保持在拖到的位置(不回 0.5)
|
||
7. **复位**:点击 ↺ → 滑块回 0.5,卡片回中间
|
||
8. DevTools console 无 error
|
||
|
||
- [ ] **Step 7: 提交**
|
||
|
||
```bash
|
||
git add frontend/pages/castlove/lenticular/lenticular-result.vue
|
||
git commit -m "feat(lenticular-result): 切换为手动倾斜模式 + 触摸滑块双向同步
|
||
|
||
- @simulate → onTouchSimulate:触摸 x 投影到滑块 + 驱动引擎
|
||
- @update:slider-value → onSliderChange:滑块拖动驱动引擎
|
||
- @slider-reset → onSliderReset:复位滑块+引擎
|
||
- v-model:slider-value 双向
|
||
- 删 3 处 scheduleTiltStart / stopTiltPreview / gyroSourceLabel 引用
|
||
- 触摸结束不强制回中
|
||
|
||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: 删原生插件目录 imengyu-UniAndroidGyro
|
||
|
||
**Files:**
|
||
- Delete: `frontend/nativePlugins/imengyu-UniAndroidGyro/`(含 android/ ios/ package.json .DS_Store)
|
||
|
||
- [ ] **Step 1: 先确认目录无外部引用**
|
||
|
||
```bash
|
||
grep -rn "imengyu" frontend/ 2>/dev/null
|
||
```
|
||
|
||
**预期**:仅 `manifest.json`(line 31、33、35)和 `useLenticularStudioTilt.js`(line 183、194)有引用。前者将在 Task 8 清理,后者将在 Task 9 清理。
|
||
|
||
- [ ] **Step 2: 备份确认(防止误删)**
|
||
|
||
```bash
|
||
ls -la frontend/nativePlugins/imengyu-UniAndroidGyro/
|
||
```
|
||
|
||
**预期**:
|
||
```
|
||
.DS_Store
|
||
android/
|
||
ios/
|
||
package.json
|
||
```
|
||
|
||
确认是 imengyu 的陀螺仪插件目录。
|
||
|
||
- [ ] **Step 3: 删除目录**
|
||
|
||
```bash
|
||
rm -rf frontend/nativePlugins/imengyu-UniAndroidGyro/
|
||
```
|
||
|
||
- [ ] **Step 4: 验证删除**
|
||
|
||
```bash
|
||
ls frontend/nativePlugins/ 2>/dev/null
|
||
```
|
||
|
||
**预期**:为空目录,或目录已被 `uniapp` 工具链清理。
|
||
|
||
```bash
|
||
[ -d frontend/nativePlugins/imengyu-UniAndroidGyro ] && echo "STILL EXISTS" || echo "DELETED OK"
|
||
```
|
||
|
||
**预期**:`DELETED OK`
|
||
|
||
- [ ] **Step 5: 提交**
|
||
|
||
```bash
|
||
git add -A frontend/nativePlugins/
|
||
git commit -m "chore(native-plugin): 移除 imengyu-UniAndroidGyro 陀螺仪插件
|
||
|
||
- 28KB AAR + iOS Framework + package.json 全部删除
|
||
- 与 useLenticularStudioTilt.js 同步(Task 9 跟进)
|
||
|
||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: 改 manifest.json(删 3 段)
|
||
|
||
**Files:**
|
||
- Modify: `frontend/manifest.json:30-44, 69, 82`
|
||
|
||
- [ ] **Step 1: 删除 nativePlugins.imengyu-UniAndroidGyro 段(line 30-44)**
|
||
|
||
打开 `frontend/manifest.json`,删除以下整段(注意包含外层逗号):
|
||
|
||
```json
|
||
"nativePlugins" : {
|
||
"imengyu-UniAndroidGyro" : {
|
||
"__plugin_info__" : {
|
||
"name" : "imengyu-UniAndroidGyro",
|
||
"description" : "APP端陀螺仪数据采集",
|
||
"platforms" : "Android,iOS",
|
||
"url" : "",
|
||
"android_package_name" : "",
|
||
"ios_bundle_id" : "",
|
||
"isCloud" : false,
|
||
"bought" : -1,
|
||
"pid" : "",
|
||
"parameters" : {}
|
||
}
|
||
}
|
||
},
|
||
/* 应用发布信息 */
|
||
```
|
||
|
||
改为只剩:
|
||
|
||
```json
|
||
/* 应用发布信息 */
|
||
```
|
||
|
||
- [ ] **Step 2: 删除 BODY_SENSORS 权限(line 69)**
|
||
|
||
删除这一行:
|
||
|
||
```xml
|
||
"<uses-permission android:name=\"android.permission.BODY_SENSORS\"/>"
|
||
```
|
||
|
||
注意末尾的逗号:删除后需确认上一行末尾**没有**多余的逗号(如果原文件 BODY_SENSORS 已经是最后一行,删除后前一行末尾的逗号也要去掉)。
|
||
|
||
- [ ] **Step 3: 删除 NSMotionUsageDescription(line 82)**
|
||
|
||
删除整行:
|
||
|
||
```json
|
||
"NSMotionUsageDescription" : "我们需要访问运动传感器以实现光栅卡倾斜预览功能"
|
||
```
|
||
|
||
注意逗号:如果前一行没有逗号则不需处理;如果删除后 `privacyDescription` 块为空,需保留 `{}`。
|
||
|
||
- [ ] **Step 4: 验证 JSON 合法**
|
||
|
||
```bash
|
||
node -e "JSON.parse(require('fs').readFileSync('frontend/manifest.json', 'utf8'))" && echo "JSON OK"
|
||
```
|
||
|
||
**预期**:`JSON OK`
|
||
|
||
- [ ] **Step 5: grep 验证**
|
||
|
||
```bash
|
||
grep -n "imengyu\|BODY_SENSORS\|NSMotionUsageDescription" frontend/manifest.json
|
||
```
|
||
|
||
**预期**:0 命中
|
||
|
||
- [ ] **Step 6: 提交**
|
||
|
||
```bash
|
||
git add frontend/manifest.json
|
||
git commit -m "chore(manifest): 移除 imengyu 插件注册与传感器权限
|
||
|
||
- 删 nativePlugins.imengyu-UniAndroidGyro 段(30-44 行)
|
||
- 删 BODY_SENSORS 权限(69 行)
|
||
- 删 NSMotionUsageDescription 隐私描述(82 行)
|
||
|
||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: 删 useLenticularStudioTilt.js + useOneEuroFilter.js + 清理 CraftTiltPreview 引用
|
||
|
||
**Files:**
|
||
- Delete: `frontend/composables/useLenticularStudioTilt.js`
|
||
- Delete: `frontend/composables/useOneEuroFilter.js`
|
||
- Modify: `frontend/composables/useLenticularCraftTiltPreview.js:12-13, 176-185, 215-225`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Task 1 留下的 simulateTilt* 入口
|
||
- Produces: 不再依赖硬件传感器路径
|
||
|
||
- [ ] **Step 1: 确认 useLenticularStudioTilt 仅 1 处引用**
|
||
|
||
```bash
|
||
grep -rn "useLenticularStudioTilt" frontend/ 2>/dev/null
|
||
```
|
||
|
||
**预期**:仅 `useLenticularCraftTiltPreview.js` 命中 1 处。
|
||
|
||
- [ ] **Step 2: 确认 useOneEuroFilter 仅 1 处引用**
|
||
|
||
```bash
|
||
grep -rn "useOneEuroFilter" frontend/ 2>/dev/null
|
||
```
|
||
|
||
**预期**:仅 `useLenticularStudioTilt.js` 命中 1 处(即将删除,所以最终 0 命中)。
|
||
|
||
- [ ] **Step 3: 从 useLenticularCraftTiltPreview 移除 useLenticularStudioTilt 引用**
|
||
|
||
打开 `frontend/composables/useLenticularCraftTiltPreview.js`,删除以下 2 处:
|
||
|
||
1. **顶部 import**(约 line 12-13):
|
||
```js
|
||
import { useLenticularStudioTilt } from '@/composables/useLenticularStudioTilt.js'
|
||
```
|
||
|
||
2. **函数体内**(约 line 176-185):
|
||
```js
|
||
const { start: startTilt, stop: stopTilt } = useLenticularStudioTilt({
|
||
simulate,
|
||
simulateFromSignedDegrees,
|
||
gyroSourceLabel,
|
||
onTiltDriverFallback: () => {
|
||
lastLayerIdx = -1
|
||
dirSign = 0
|
||
dirOppositeStreak = 0
|
||
},
|
||
})
|
||
```
|
||
|
||
以及相关状态变量(`let entryTiltTimer = null` 和 `cancelScheduledTiltStart / scheduleTiltStart / stopTiltPreview` 三个函数定义)。
|
||
|
||
最终 `return` 对象同步清理(去掉 `scheduleTiltStart / stopTiltPreview / gyroSourceLabel`):
|
||
|
||
```js
|
||
return {
|
||
physics,
|
||
layerTransforms,
|
||
simulate,
|
||
relax,
|
||
lockPreviewStill,
|
||
// 新增:手动模拟入口
|
||
simulateTilt,
|
||
simulateTiltFromNormalized,
|
||
simulateTiltReset,
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 删除 useLenticularStudioTilt.js 和 useOneEuroFilter.js**
|
||
|
||
```bash
|
||
rm frontend/composables/useLenticularStudioTilt.js frontend/composables/useOneEuroFilter.js
|
||
```
|
||
|
||
- [ ] **Step 5: 验证无残留**
|
||
|
||
```bash
|
||
grep -rn "useLenticularStudioTilt\|useOneEuroFilter" frontend/ 2>/dev/null
|
||
```
|
||
|
||
**预期**:0 命中
|
||
|
||
- [ ] **Step 6: H5 Chrome 验证 lenticular-result 仍正常**
|
||
|
||
打开 `lenticular-result` 页面,验证:
|
||
- 卡片正常显示
|
||
- 触摸 + 滑块都工作
|
||
- DevTools console 无 `useLenticularStudioTilt is not defined` 错误
|
||
|
||
- [ ] **Step 7: 提交**
|
||
|
||
```bash
|
||
git add frontend/composables/
|
||
git commit -m "refactor(craft-tilt): 删除硬件传感器模块
|
||
|
||
- 删 useLenticularStudioTilt.js (28KB, 894 行)
|
||
- 删 useOneEuroFilter.js (75 行)
|
||
- useLenticularCraftTiltPreview 移除对二者的引用
|
||
- 保留手动模拟入口 simulateTilt / simulateTiltFromNormalized / simulateTiltReset
|
||
|
||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: 清理 useHolographicPreview.js DeviceOrientationEvent 路径
|
||
|
||
**Files:**
|
||
- Modify: `frontend/composables/useHolographicPreview.js:7, 26-31, 44-96, 102-105`
|
||
|
||
**Interfaces:**
|
||
- Consumes: 现有 `simulate(x, y)`
|
||
- Produces: 删除所有传感器路径,`simulate(x, y)` 成为唯一驱动入口
|
||
|
||
- [ ] **Step 1: 删除 import 中的 onUnmounted(如果不再使用)**
|
||
|
||
打开 `frontend/composables/useHolographicPreview.js`:
|
||
|
||
第 7 行:
|
||
```js
|
||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||
```
|
||
改为:
|
||
```js
|
||
import { ref, reactive } from 'vue'
|
||
```
|
||
|
||
- [ ] **Step 2: 删除传感器相关局部状态**
|
||
|
||
删除 line 26-31:
|
||
```js
|
||
let accelHandler = null
|
||
let accelSmoothed = 0
|
||
let accelBaselineReady = false
|
||
let accelBaseX = 0
|
||
let accelBaseY = 0
|
||
```
|
||
|
||
- [ ] **Step 3: 删除 startGyro / startDeviceOrientation / stopGyro 函数(line 44-96)**
|
||
|
||
整段删除:
|
||
|
||
```js
|
||
function startGyro() { ... }
|
||
function startDeviceOrientation() { ... }
|
||
function stopGyro() { ... }
|
||
```
|
||
|
||
- [ ] **Step 4: 简化 onMounted 钩子(如果保留)**
|
||
|
||
如果 onMounted 不再需要任何操作,删除整个 `onMounted(() => { ... })` 块(line 102-105)。否则保留空函数体。
|
||
|
||
- [ ] **Step 5: 清理 onUnmounted**
|
||
|
||
删除 `onUnmounted(() => { stopGyro() })`(line 105)。
|
||
|
||
- [ ] **Step 6: 从 return 对象中删除 startGyro / stopGyro / gyroSource**
|
||
|
||
修改末尾 `return { ... }`(line 107-113),删除 `gyroSource, startGyro, stopGyro` 三项。保留 `physics, viewAngle, isWebGLReady, hasWebGLError, fps, simulate, relax, onWebGLReady, onWebGLError, onFPSUpdate, detectPerformanceTier, HOLO_PERFORMANCE_PRESETS`。
|
||
|
||
- [ ] **Step 7: 验证无残留**
|
||
|
||
```bash
|
||
grep -n "DeviceOrientation\|requestPermission\|accelHandler\|startGyro\|stopGyro\|gyroSource" frontend/composables/useHolographicPreview.js
|
||
```
|
||
|
||
**预期**:0 命中
|
||
|
||
- [ ] **Step 8: H5 Chrome 验证全息卡**
|
||
|
||
如有任何页面挂载 `<HolographicCard>`(grep 验证),H5 dev server 打开该页面:
|
||
- 卡片正常显示
|
||
- 触摸拖拽有效
|
||
- DevTools console 无 `DeviceOrientationEvent is not defined` 之类的错误
|
||
|
||
- [ ] **Step 9: 提交**
|
||
|
||
```bash
|
||
git add frontend/composables/useHolographicPreview.js
|
||
git commit -m "refactor(holographic): 移除 DeviceOrientationEvent 路径
|
||
|
||
- 删 startGyro / startDeviceOrientation / stopGyro
|
||
- 删 accelHandler / accelBaselineReady 等基线追踪状态
|
||
- simulate(x, y) 成为唯一驱动入口
|
||
- 保留 onMounted / onUnmounted 钩子(如果外部有需要可保留,否则删)
|
||
|
||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11: 终验 — grep 校验清单 + 验收清单
|
||
|
||
**Files:** 无(验证性任务)
|
||
|
||
- [ ] **Step 1: 全量 grep 校验(spec 附录)**
|
||
|
||
```bash
|
||
grep -rn "useLenticularStudioTilt" frontend/ 2>/dev/null
|
||
grep -rn "useOneEuroFilter" frontend/ 2>/dev/null
|
||
grep -rn "imengyu-UniAndroidGyro" frontend/ 2>/dev/null
|
||
grep -rn "imengyu" frontend/ 2>/dev/null
|
||
grep -rn "BODY_SENSORS" frontend/ 2>/dev/null
|
||
grep -rn "NSMotionUsageDescription" frontend/ 2>/dev/null
|
||
```
|
||
|
||
**预期**:**全部 0 命中**
|
||
|
||
- [ ] **Step 2: H5 Chrome 完整验收清单**
|
||
|
||
启动 H5 dev server,打开 `lenticular-result` 页面,按 spec §4.3 验收清单逐项过:
|
||
|
||
1. 打开页面,卡片显示正常
|
||
2. **触摸拖拽**:手指在卡片上左右拖动,卡片光栅效果跟随
|
||
3. **松开不回中**:触摸结束卡片不强制回中(验证 4.1 修复)
|
||
4. **滑块拖动**:拖动滑块 0 → 1,卡片光栅从最左切到最右
|
||
5. **复位按钮**:点击 ↺,滑块回 0.5,卡片回中间
|
||
6. **组合**:拖动滑块到 0.3,然后触摸卡片到最右 → 触摸胜出
|
||
7. `lenticular-create` / `lenticular-thinking` 页面无回归(无报错)
|
||
|
||
- [ ] **Step 3: 移动端构建检查(如有真机)**
|
||
|
||
```bash
|
||
# 触发 HBuilderX 重新打包 App
|
||
# 真机扫描:
|
||
adb logcat | grep -iE "sensors|gyroscope|accelerometer" | head -20
|
||
# 预期:无 SENSOR_SERVICE / BODY_SENSORS 申请日志
|
||
```
|
||
|
||
iOS 真机:扫描 `console.app` 或 Xcode device log,**不应**出现 `NSMotionUsageDescription` 弹窗。
|
||
|
||
- [ ] **Step 4: 提交验收报告(如需要)**
|
||
|
||
验收不通过则 revert 对应 Task;通过则无需新 commit,结束于前一个 Task。
|
||
|
||
---
|
||
|
||
## 执行建议
|
||
|
||
按 **Task 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11** 顺序执行。前 6 个 Task 是新增/修改(每步可独立 revert),Task 7-10 是清理(不可逆但可整体 revert),Task 11 是终验。
|
||
|
||
预计开发 2-3 小时,QA 1 天。
|