feat: 修改自动领取收益关闭,修复下架时间的bug,修改的光栅卡陀螺仪
This commit is contained in:
parent
ce4fd85926
commit
2855cd512d
@ -491,10 +491,9 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) (
|
|||||||
if exhibitionStartTime == 0 {
|
if exhibitionStartTime == 0 {
|
||||||
exhibitionStartTime = asset.CreatedAt // 兜底
|
exhibitionStartTime = asset.CreatedAt // 兜底
|
||||||
}
|
}
|
||||||
earnings = calculateRealtimeEarnings(asset.LikeCount, exhibitionStartTime, time.Now().UnixMilli())
|
// 获取展出过期时间(先获取,用于计算收益)
|
||||||
|
|
||||||
// 获取展出过期时间
|
|
||||||
exhibitionExpireAt, _ = s.assetRepo.GetExhibitionExpireTime(asset.ID)
|
exhibitionExpireAt, _ = s.assetRepo.GetExhibitionExpireTime(asset.ID)
|
||||||
|
earnings = calculateRealtimeEarnings(asset.LikeCount, exhibitionStartTime, time.Now().UnixMilli(), exhibitionExpireAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6.5 从 asset_registry 表获取 grade
|
// 6.5 从 asset_registry 表获取 grade
|
||||||
@ -759,9 +758,16 @@ func calculateHourlyEarnings(likeCount int32) float64 {
|
|||||||
// calculateRealtimeEarnings 实时计算展示收益
|
// calculateRealtimeEarnings 实时计算展示收益
|
||||||
// 公式:R1 = R0 × T × [100% + Buff(n)]
|
// 公式:R1 = R0 × T × [100% + Buff(n)]
|
||||||
// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算
|
// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算
|
||||||
func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 {
|
// 注意:使用 min(now, expireAt) 确保过期后收益不再增长
|
||||||
|
func calculateRealtimeEarnings(likeCount int32, startTime, now, expireAt int64) int64 {
|
||||||
|
// 计算有效截止时间(展览结束时间 vs 当前时间,取较小值)
|
||||||
|
endTime := now
|
||||||
|
if expireAt > 0 && expireAt < now {
|
||||||
|
endTime = expireAt
|
||||||
|
}
|
||||||
|
|
||||||
// 计算上架时长(毫秒转小时)
|
// 计算上架时长(毫秒转小时)
|
||||||
T := (now - startTime) / 3600000
|
T := (endTime - startTime) / 3600000
|
||||||
if T <= 0 {
|
if T <= 0 {
|
||||||
T = 1 // 最少1小时
|
T = 1 // 最少1小时
|
||||||
}
|
}
|
||||||
|
|||||||
@ -452,7 +452,7 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
|
|||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount)
|
item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount)
|
||||||
item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now)
|
item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now, item.ExpireAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return items, total, nil
|
return items, total, nil
|
||||||
@ -496,7 +496,7 @@ func (r *galleryRepository) GetUserExhibitedAssets(userID, starID int64, page, p
|
|||||||
// 实时计算每个资产的收益
|
// 实时计算每个资产的收益
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount)
|
item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount)
|
||||||
item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now)
|
item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now, item.ExpireAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return items, total, nil
|
return items, total, nil
|
||||||
@ -660,9 +660,16 @@ func calculateHourlyEarnings(likeCount int32) float64 {
|
|||||||
// calculateRealtimeEarnings 实时计算展示收益
|
// calculateRealtimeEarnings 实时计算展示收益
|
||||||
// 公式:R1 = R0 × T × [100% + Buff(n)]
|
// 公式:R1 = R0 × T × [100% + Buff(n)]
|
||||||
// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算
|
// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算
|
||||||
func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 {
|
// 注意:使用 min(now, expireAt) 确保过期后收益不再增长
|
||||||
|
func calculateRealtimeEarnings(likeCount int32, startTime, now, expireAt int64) int64 {
|
||||||
|
// 计算有效截止时间(展览结束时间 vs 当前时间,取较小值)
|
||||||
|
endTime := now
|
||||||
|
if expireAt > 0 && expireAt < now {
|
||||||
|
endTime = expireAt
|
||||||
|
}
|
||||||
|
|
||||||
// 计算上架时长(毫秒转小时)
|
// 计算上架时长(毫秒转小时)
|
||||||
T := (now - startTime) / 3600000
|
T := (endTime - startTime) / 3600000
|
||||||
if T <= 0 {
|
if T <= 0 {
|
||||||
T = 1 // 最少1小时
|
T = 1 // 最少1小时
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,7 +111,7 @@ func main() {
|
|||||||
logger.Logger.Info("Services initialized")
|
logger.Logger.Info("Services initialized")
|
||||||
|
|
||||||
// 7. Init worker(goroutine 中启动)
|
// 7. Init worker(goroutine 中启动)
|
||||||
resetWorker := worker.NewDailyResetWorker(dailyRepo, revenueRepo, userRPCClient)
|
resetWorker := worker.NewDailyResetWorker(dailyRepo, revenueRepo, userRPCClient, galleryRPCClient)
|
||||||
go resetWorker.Start()
|
go resetWorker.Start()
|
||||||
logger.Logger.Info("Reset worker started")
|
logger.Logger.Info("Reset worker started")
|
||||||
|
|
||||||
|
|||||||
@ -80,6 +80,7 @@ func (s *revenueService) GetExhibitionRevenue(ctx context.Context, userID, starI
|
|||||||
CycleStartTime: r.CycleStartTime,
|
CycleStartTime: r.CycleStartTime,
|
||||||
CycleEndTime: r.CycleEndTime,
|
CycleEndTime: r.CycleEndTime,
|
||||||
Status: r.Status,
|
Status: r.Status,
|
||||||
|
CanClaim: r.Status == "claimable",
|
||||||
}
|
}
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ type DailyResetWorker struct {
|
|||||||
dailyRepo repository.DailyTaskRepository
|
dailyRepo repository.DailyTaskRepository
|
||||||
revenueRepo repository.RevenueRepository
|
revenueRepo repository.RevenueRepository
|
||||||
userClient client.UserServiceClient
|
userClient client.UserServiceClient
|
||||||
|
galleryClient client.GalleryServiceClient
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
@ -26,11 +27,13 @@ func NewDailyResetWorker(
|
|||||||
dailyRepo repository.DailyTaskRepository,
|
dailyRepo repository.DailyTaskRepository,
|
||||||
revenueRepo repository.RevenueRepository,
|
revenueRepo repository.RevenueRepository,
|
||||||
userClient client.UserServiceClient,
|
userClient client.UserServiceClient,
|
||||||
|
galleryClient client.GalleryServiceClient,
|
||||||
) *DailyResetWorker {
|
) *DailyResetWorker {
|
||||||
return &DailyResetWorker{
|
return &DailyResetWorker{
|
||||||
dailyRepo: dailyRepo,
|
dailyRepo: dailyRepo,
|
||||||
revenueRepo: revenueRepo,
|
revenueRepo: revenueRepo,
|
||||||
userClient: userClient,
|
userClient: userClient,
|
||||||
|
galleryClient: galleryClient,
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,8 +101,8 @@ func (w *DailyResetWorker) doResetAndAutoClaim() {
|
|||||||
logger.Logger.Info(fmt.Sprintf("DailyResetWorker: daily tasks reset: %d records updated", resetCount))
|
logger.Logger.Info(fmt.Sprintf("DailyResetWorker: daily tasks reset: %d records updated", resetCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 自动发放展示收益
|
// 2. 自动发放展示收益(暂时关闭)
|
||||||
w.autoClaimExhibitionRevenue()
|
// w.autoClaimExhibitionRevenue()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *DailyResetWorker) autoClaimExhibitionRevenue() {
|
func (w *DailyResetWorker) autoClaimExhibitionRevenue() {
|
||||||
@ -128,6 +131,16 @@ func (w *DailyResetWorker) autoClaimExhibitionRevenue() {
|
|||||||
zap.Int64("record_id", record.ID), zap.Error(err))
|
zap.Int64("record_id", record.ID), zap.Error(err))
|
||||||
}
|
}
|
||||||
totalClaimed++
|
totalClaimed++
|
||||||
|
|
||||||
|
// 自动下架:调用 GalleryService 下架展览
|
||||||
|
if w.galleryClient != nil && record.AssetID > 0 {
|
||||||
|
if err := w.galleryClient.RemoveExhibitionByAsset(context.Background(), record.AssetID); err != nil {
|
||||||
|
logger.Logger.Warn("DailyResetWorker: failed to remove exhibition",
|
||||||
|
zap.Int64("asset_id", record.AssetID),
|
||||||
|
zap.Int64("revenue_record_id", record.ID),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
lastErr = err
|
lastErr = err
|
||||||
|
|||||||
@ -0,0 +1,83 @@
|
|||||||
|
# 光栅卡陀螺仪交互优化设计方案
|
||||||
|
|
||||||
|
**日期:** 2026-05-22
|
||||||
|
**文件:** `docs/superpowers/specs/2026-05-22-lenticular-gyro-optimization-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 问题描述
|
||||||
|
|
||||||
|
设备使用陀螺仪倾斜查看光栅卡时,图片在左右视图之间快速来回切换,无法稳定在某一侧。
|
||||||
|
|
||||||
|
**期望行为:**
|
||||||
|
- 设备居中(不倾斜)→ 显示中性图
|
||||||
|
- 往左倾斜 → 稳定显示左视图
|
||||||
|
- 往右倾斜 → 稳定显示右视图
|
||||||
|
|
||||||
|
**当前行为:**
|
||||||
|
- 轻微倾斜就触发切换,且图片在左右之间来回振荡
|
||||||
|
- 没有平衡点让图片稳定
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 根本原因
|
||||||
|
|
||||||
|
**原因:缺少中性阈值区(Dead Zone)**
|
||||||
|
|
||||||
|
陀螺仪数据直接映射到图片切换阈值,没有任何"容忍区"。
|
||||||
|
|
||||||
|
当设备略微偏离中心时:
|
||||||
|
- `gamma` 值微小的正负变化
|
||||||
|
- 直接导致 `x` 方向变化
|
||||||
|
- 触发 opacity 切换
|
||||||
|
|
||||||
|
加上陀螺仪本身的噪声和设备轻微晃动,导致图片不断在左右之间来回切换。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 修复方案
|
||||||
|
|
||||||
|
**文件:** `frontend/composables/useHolographicPreview.js`
|
||||||
|
|
||||||
|
### 修改:增大中性阈值区
|
||||||
|
|
||||||
|
当前代码已有 dead zone 判断逻辑,只需增大 `db` 系数即可:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 第79行
|
||||||
|
const db = 0.08 * dead // 从 0.016 改为 0.08
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明:** 增大 db 系数 = 中性区变宽 = 更难触发切换 = 图片更稳定
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 参数调整
|
||||||
|
|
||||||
|
| 参数 | 当前值 | 修改后 | 说明 |
|
||||||
|
|------|--------|--------|------|
|
||||||
|
| db (dead zone) | 0.016 | 0.08 | 中性区阈值,越大越难触发切换 |
|
||||||
|
|
||||||
|
**建议值范围:** 0.05 ~ 0.12
|
||||||
|
- 值越大:中性区越宽,越稳定但响应越慢
|
||||||
|
- 值越小:越敏感,但容易误触发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 验收标准
|
||||||
|
|
||||||
|
| 测试场景 | 期望结果 |
|
||||||
|
|---------|---------|
|
||||||
|
| 设备静止居中 | 显示中性图,稳定不动 |
|
||||||
|
| 轻微倾斜 | 保持中性,不触发切换 |
|
||||||
|
| 明显往左倾斜 | 显示左视图,稳定 |
|
||||||
|
| 明显往右倾斜 | 显示右视图,稳定 |
|
||||||
|
| 从倾斜缓慢回到居中 | 平滑过渡,无振荡 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 影响范围
|
||||||
|
|
||||||
|
- 修改文件:`frontend/composables/useHolographicPreview.js`
|
||||||
|
- 不影响触摸滑动交互
|
||||||
|
- 不涉及后端或数据库
|
||||||
@ -76,7 +76,7 @@ export function useHolographicPreview() {
|
|||||||
const dy = (beta - accelBaseY) / 45 * sens
|
const dy = (beta - accelBaseY) / 45 * sens
|
||||||
accelSmoothed = lerp(accelSmoothed, dx, k)
|
accelSmoothed = lerp(accelSmoothed, dx, k)
|
||||||
const dead = physics.sensorDeadzoneStrength || 1
|
const dead = physics.sensorDeadzoneStrength || 1
|
||||||
const db = 0.016 * dead
|
const db = 0.08 * dead
|
||||||
simulate(
|
simulate(
|
||||||
Math.abs(accelSmoothed) < db ? 0 : clamp(accelSmoothed, -1, 1),
|
Math.abs(accelSmoothed) < db ? 0 : clamp(accelSmoothed, -1, 1),
|
||||||
clamp(dy, -1, 1)
|
clamp(dy, -1, 1)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const FLOW_PHYSICS = {
|
|||||||
angleStability: 52,
|
angleStability: 52,
|
||||||
transitionSmoothness: 40,
|
transitionSmoothness: 40,
|
||||||
tiltSensitivity: 96,
|
tiltSensitivity: 96,
|
||||||
sensorDeadzoneStrength: 0,
|
sensorDeadzoneStrength: 1,
|
||||||
parallaxDepth: 18,
|
parallaxDepth: 18,
|
||||||
lenticularAnchorFloor: 0.1,
|
lenticularAnchorFloor: 0.1,
|
||||||
lenticularNonDominantResidualMin: 0.092,
|
lenticularNonDominantResidualMin: 0.092,
|
||||||
@ -18,7 +18,7 @@ const FLOW_PHYSICS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DISCRETE_STEP_DEG = 13
|
const DISCRETE_STEP_DEG = 13
|
||||||
const DISCRETE_STEP_HYST_DEG = 5
|
const DISCRETE_STEP_HYST_DEG = 9
|
||||||
const TILT_MAG_TAU_MS = 150
|
const TILT_MAG_TAU_MS = 150
|
||||||
const TILT_MAG_MAX_RISE_DPS = 82
|
const TILT_MAG_MAX_RISE_DPS = 82
|
||||||
const TILT_MAG_MAX_FALL_DPS = 168
|
const TILT_MAG_MAX_FALL_DPS = 168
|
||||||
@ -51,6 +51,7 @@ export function useLenticularCraftTiltPreview(layersRef) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function simulateFromSignedDegrees(tiltMagDeg, signedDegHint) {
|
function simulateFromSignedDegrees(tiltMagDeg, signedDegHint) {
|
||||||
|
console.log('[DEBUG simulateFromSignedDegrees] called, tiltMagDeg:', tiltMagDeg, 'signedDegHint:', signedDegHint)
|
||||||
const ls = getLayersArray()
|
const ls = getLayersArray()
|
||||||
const n = Math.max(1, ls.length)
|
const n = Math.max(1, ls.length)
|
||||||
const raw = Math.abs(Number(tiltMagDeg) || 0)
|
const raw = Math.abs(Number(tiltMagDeg) || 0)
|
||||||
@ -69,6 +70,7 @@ export function useLenticularCraftTiltPreview(layersRef) {
|
|||||||
const maxFall = TILT_MAG_MAX_FALL_DPS * sec
|
const maxFall = TILT_MAG_MAX_FALL_DPS * sec
|
||||||
|
|
||||||
if (Number.isFinite(signedDegHint)) {
|
if (Number.isFinite(signedDegHint)) {
|
||||||
|
console.log('[DEBUG simulateFromSignedDegrees] signedDegHint is finite:', signedDegHint)
|
||||||
if (lastSignedDegHint != null && sec > 1e-6) {
|
if (lastSignedDegHint != null && sec > 1e-6) {
|
||||||
const dAbsSignedDt =
|
const dAbsSignedDt =
|
||||||
(Math.abs(signedDegHint) - Math.abs(lastSignedDegHint)) / sec
|
(Math.abs(signedDegHint) - Math.abs(lastSignedDegHint)) / sec
|
||||||
@ -94,8 +96,34 @@ export function useLenticularCraftTiltPreview(layersRef) {
|
|||||||
const MARGIN = DISCRETE_STEP_HYST_DEG
|
const MARGIN = DISCRETE_STEP_HYST_DEG
|
||||||
const stepBefore = discreteStableStep.value
|
const stepBefore = discreteStableStep.value
|
||||||
let s = stepBefore
|
let s = stepBefore
|
||||||
while (absDeg >= (s + 1) * STEP + MARGIN) s++
|
|
||||||
while (s > 0 && absDeg <= s * STEP - MARGIN) s--
|
// 二值模式:仅允许 0(中性) 或 1(倾斜),防止同方向重复递增档位
|
||||||
|
const ENTER_THRESHOLD = STEP + MARGIN
|
||||||
|
const EXIT_THRESHOLD = MARGIN
|
||||||
|
|
||||||
|
if (s === 0) {
|
||||||
|
// 中性状态:仅当倾斜超过阈值才进入档位1
|
||||||
|
if (absDeg >= ENTER_THRESHOLD) {
|
||||||
|
s = 1
|
||||||
|
console.log('[DEBUG] 进入档位1, absDeg:', absDeg.toFixed(2), 'signedDegHint:', signedDegHint)
|
||||||
|
}
|
||||||
|
} else if (s === 1) {
|
||||||
|
// 档位1状态:检测方向变化才允许退回
|
||||||
|
// 如果 signedDegHint 符号改变(反方向倾斜),立即回到中性
|
||||||
|
if (lastSignedDegHint !== null && Number.isFinite(signedDegHint)) {
|
||||||
|
if (signedDegHint * lastSignedDegHint < 0) {
|
||||||
|
// 方向改变,回到中性
|
||||||
|
s = 0
|
||||||
|
lastSignedDegHint = signedDegHint
|
||||||
|
console.log('[DEBUG] 方向改变,回到中性, signedDegHint:', signedDegHint.toFixed(2))
|
||||||
|
}
|
||||||
|
} else if (absDeg <= EXIT_THRESHOLD) {
|
||||||
|
// 没有方向信息时,使用角度阈值
|
||||||
|
s = 0
|
||||||
|
console.log('[DEBUG] 角度不足,回到中性, absDeg:', absDeg.toFixed(2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
discreteStableStep.value = s
|
discreteStableStep.value = s
|
||||||
const idx = s % n
|
const idx = s % n
|
||||||
const sens = physics.tiltSensitivity / 100
|
const sens = physics.tiltSensitivity / 100
|
||||||
@ -134,13 +162,17 @@ export function useLenticularCraftTiltPreview(layersRef) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleTiltStart() {
|
function scheduleTiltStart() {
|
||||||
|
console.log('[DEBUG useLenticularCraftTiltPreview] scheduleTiltStart called')
|
||||||
cancelScheduledTiltStart()
|
cancelScheduledTiltStart()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
relax(0.86)
|
relax(0.86)
|
||||||
lockPreviewStill()
|
lockPreviewStill()
|
||||||
|
console.log('[DEBUG useLenticularCraftTiltPreview] entryTiltTimer set, delay:', LENTICULAR_TILT_START_DELAY_MS)
|
||||||
entryTiltTimer = setTimeout(() => {
|
entryTiltTimer = setTimeout(() => {
|
||||||
entryTiltTimer = null
|
entryTiltTimer = null
|
||||||
|
console.log('[DEBUG useLenticularCraftTiltPreview] startTilt about to be called')
|
||||||
startTilt()
|
startTilt()
|
||||||
|
console.log('[DEBUG useLenticularCraftTiltPreview] startTilt called')
|
||||||
}, LENTICULAR_TILT_START_DELAY_MS)
|
}, LENTICULAR_TILT_START_DELAY_MS)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -361,6 +361,7 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startAccelInternal(options = {}) {
|
function startAccelInternal(options = {}) {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] startAccelInternal called, options:', options)
|
||||||
const fromNativeFallback = options.fromNativeFallback === true
|
const fromNativeFallback = options.fromNativeFallback === true
|
||||||
mode = 'accel'
|
mode = 'accel'
|
||||||
stopAccel()
|
stopAccel()
|
||||||
@ -379,34 +380,45 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
|
|
||||||
if (useStudioAccelDirect) {
|
if (useStudioAccelDirect) {
|
||||||
if (typeof simulateFromSignedDegrees === 'function') {
|
if (typeof simulateFromSignedDegrees === 'function') {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] 使用 simulateFromSignedDegrees')
|
||||||
resetStudioAccelBaseline()
|
resetStudioAccelBaseline()
|
||||||
}
|
}
|
||||||
accelHandler = (res) => {
|
accelHandler = (res) => {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] accelHandler called, x:', res.x, 'y:', res.y, 'z:', res.z)
|
||||||
const ax = Number(res.x) || 0
|
const ax = Number(res.x) || 0
|
||||||
const ay = Number(res.y) || 0
|
const ay = Number(res.y) || 0
|
||||||
const az = Number(res.z) || 0
|
const az = Number(res.z) || 0
|
||||||
const gMag = Math.hypot(ax, ay, az)
|
const gMag = Math.hypot(ax, ay, az)
|
||||||
if (gMag < 0.12) return
|
console.log('[DEBUG useLenticularStudioTilt] gMag:', gMag.toFixed(3))
|
||||||
|
if (gMag < 0.12) {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] gMag too small, returning')
|
||||||
|
return
|
||||||
|
}
|
||||||
/* 竖屏常见握持:左右倾斜主要反映为 ax/az 与重力的关系(免 warmup,避免长期无输出) */
|
/* 竖屏常见握持:左右倾斜主要反映为 ax/az 与重力的关系(免 warmup,避免长期无输出) */
|
||||||
if (typeof simulateFromSignedDegrees === 'function') {
|
if (typeof simulateFromSignedDegrees === 'function') {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] 调用 simulateFromSignedDegrees')
|
||||||
const tiltRad = Math.atan2(-ax, az)
|
const tiltRad = Math.atan2(-ax, az)
|
||||||
if (studioAccelBaseRad == null) {
|
if (studioAccelBaseRad == null) {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] 正在采集 baseline')
|
||||||
studioAccelBaselineRads.push(tiltRad)
|
studioAccelBaselineRads.push(tiltRad)
|
||||||
if (studioAccelBaselineRads.length < STUDIO_ACCEL_BASELINE_FRAMES) {
|
if (studioAccelBaselineRads.length < STUDIO_ACCEL_BASELINE_FRAMES) {
|
||||||
simulate(0, 0)
|
simulate(0, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
studioAccelBaseRad = circularMeanRad(studioAccelBaselineRads)
|
studioAccelBaseRad = circularMeanRad(studioAccelBaselineRads)
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] baseline 采集完成:', studioAccelBaseRad)
|
||||||
studioAccelBaselineRads = []
|
studioAccelBaselineRads = []
|
||||||
}
|
}
|
||||||
let delta = tiltRad - studioAccelBaseRad
|
let delta = tiltRad - studioAccelBaseRad
|
||||||
while (delta > Math.PI) delta -= 2 * Math.PI
|
while (delta > Math.PI) delta -= 2 * Math.PI
|
||||||
while (delta < -Math.PI) delta += 2 * Math.PI
|
while (delta < -Math.PI) delta += 2 * Math.PI
|
||||||
const signedDeg = delta * (180 / Math.PI)
|
const signedDeg = delta * (180 / Math.PI)
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] delta:', delta.toFixed(4), 'signedDeg:', signedDeg.toFixed(2))
|
||||||
const rawAbs = Math.abs(signedDeg)
|
const rawAbs = Math.abs(signedDeg)
|
||||||
const prevLp = studioAccelDiscreteDegLp
|
const prevLp = studioAccelDiscreteDegLp
|
||||||
const kLp = rawAbs >= prevLp ? 0.27 : 0.4
|
const kLp = rawAbs >= prevLp ? 0.27 : 0.4
|
||||||
studioAccelDiscreteDegLp += (rawAbs - studioAccelDiscreteDegLp) * kLp
|
studioAccelDiscreteDegLp += (rawAbs - studioAccelDiscreteDegLp) * kLp
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] calling simulateFromSignedDegrees, mag:', studioAccelDiscreteDegLp.toFixed(2), 'signedDeg:', signedDeg.toFixed(2))
|
||||||
simulateFromSignedDegrees(studioAccelDiscreteDegLp, signedDeg)
|
simulateFromSignedDegrees(studioAccelDiscreteDegLp, signedDeg)
|
||||||
} else {
|
} else {
|
||||||
const tiltRaw = Math.atan2(-ax, az) / (Math.PI / 5)
|
const tiltRaw = Math.atan2(-ax, az) / (Math.PI / 5)
|
||||||
@ -446,13 +458,17 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
uni.onAccelerometerChange(accelHandler)
|
uni.onAccelerometerChange(accelHandler)
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] onAccelerometerChange 注册成功')
|
||||||
const applyStart = (interval) => {
|
const applyStart = (interval) => {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] 调用 startAccelerometer, interval:', interval)
|
||||||
uni.startAccelerometer({
|
uni.startAccelerometer({
|
||||||
interval,
|
interval,
|
||||||
success: () => {
|
success: () => {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] startAccelerometer 成功')
|
||||||
gyroSourceLabel.value = 'accelerometer'
|
gyroSourceLabel.value = 'accelerometer'
|
||||||
},
|
},
|
||||||
fail: () => {
|
fail: (err) => {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] startAccelerometer 失败:', JSON.stringify(err))
|
||||||
if (interval === 'game') {
|
if (interval === 'game') {
|
||||||
applyStart('normal')
|
applyStart('normal')
|
||||||
return
|
return
|
||||||
@ -463,11 +479,13 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
}
|
}
|
||||||
applyStart('game')
|
applyStart('game')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] try onAccelerometerChange failed:', e)
|
||||||
gyroSourceLabel.value = 'simulation'
|
gyroSourceLabel.value = 'simulation'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNativeAngleFrame(x, y, z) {
|
function onNativeAngleFrame(x, y, z) {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] onNativeAngleFrame:', x, y, z)
|
||||||
const vx = Number(x)
|
const vx = Number(x)
|
||||||
const vy = Number(y)
|
const vy = Number(y)
|
||||||
const vz = Number(z)
|
const vz = Number(z)
|
||||||
@ -517,22 +535,27 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startNativeInternal() {
|
function startNativeInternal() {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] startNativeInternal called')
|
||||||
stopNative()
|
stopNative()
|
||||||
stopAccel()
|
stopAccel()
|
||||||
resetNativeBaseline()
|
resetNativeBaseline()
|
||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
gyroModule = tryRequireImengyuGyro()
|
gyroModule = tryRequireImengyuGyro()
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] gyroModule:', gyroModule ? 'found' : 'not found')
|
||||||
if (!gyroModule) {
|
if (!gyroModule) {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] gyroModule not found, falling back to accel')
|
||||||
mode = 'accel'
|
mode = 'accel'
|
||||||
startAccelInternal()
|
startAccelInternal()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mode = 'native'
|
mode = 'native'
|
||||||
const myGen = tiltGen
|
const myGen = tiltGen
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] native gyro mode set, calling startNativeGyro')
|
||||||
/** 与官方示例一致:normal / ui / game / fastest,game≈50Hz */
|
/** 与官方示例一致:normal / ui / game / fastest,game≈50Hz */
|
||||||
const startOpts = { interval: 'game' }
|
const startOpts = { interval: 'game' }
|
||||||
|
|
||||||
scheduleNativeGyroStallFallback = () => {
|
scheduleNativeGyroStallFallback = () => {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] scheduleNativeGyroStallFallback called')
|
||||||
if (nativeWatchdogTimer != null) {
|
if (nativeWatchdogTimer != null) {
|
||||||
try {
|
try {
|
||||||
clearTimeout(nativeWatchdogTimer)
|
clearTimeout(nativeWatchdogTimer)
|
||||||
@ -541,18 +564,9 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
}
|
}
|
||||||
nativeWatchdogTimer = null
|
nativeWatchdogTimer = null
|
||||||
}
|
}
|
||||||
nativeWatchdogTimer = setTimeout(() => {
|
console.log('[DEBUG useLenticularStudioTilt] native gyro stalled, fallback to accelerometer')
|
||||||
nativeWatchdogTimer = null
|
|
||||||
if (myGen !== tiltGen) return
|
|
||||||
if (mode !== 'native') return
|
|
||||||
console.warn(
|
|
||||||
'[useLenticularStudioTilt] native gyro stalled (no angle frames for ' +
|
|
||||||
NATIVE_GYRO_STALL_FALLBACK_MS +
|
|
||||||
'ms), falling back to accelerometer'
|
|
||||||
)
|
|
||||||
stopNative()
|
stopNative()
|
||||||
startAccelInternal({ fromNativeFallback: true })
|
startAccelInternal({ fromNativeFallback: true })
|
||||||
}, NATIVE_GYRO_STALL_FALLBACK_MS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function invokeStartNativeGyro() {
|
function invokeStartNativeGyro() {
|
||||||
@ -582,6 +596,7 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
if (useSync) {
|
if (useSync) {
|
||||||
const v = mod.getGyroValueSync()
|
const v = mod.getGyroValueSync()
|
||||||
if (v && hasAnglePayload(v)) {
|
if (v && hasAnglePayload(v)) {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] getGyroValueSync:', JSON.stringify(v))
|
||||||
onNativeAngleFrame(v.x, v.y, v.z)
|
onNativeAngleFrame(v.x, v.y, v.z)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -589,6 +604,7 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
mod.getGyroValue((v) => {
|
mod.getGyroValue((v) => {
|
||||||
if (myGen !== tiltGen || !gyroModule || !v) return
|
if (myGen !== tiltGen || !gyroModule || !v) return
|
||||||
if (hasAnglePayload(v)) {
|
if (hasAnglePayload(v)) {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] getGyroValue:', JSON.stringify(v))
|
||||||
onNativeAngleFrame(v.x, v.y, v.z)
|
onNativeAngleFrame(v.x, v.y, v.z)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -602,6 +618,7 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
|
|
||||||
let kickOnce = false
|
let kickOnce = false
|
||||||
const kick = () => {
|
const kick = () => {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] kick called, kickOnce:', kickOnce)
|
||||||
if (kickOnce) return
|
if (kickOnce) return
|
||||||
if (myGen !== tiltGen || !gyroModule) return
|
if (myGen !== tiltGen || !gyroModule) return
|
||||||
kickOnce = true
|
kickOnce = true
|
||||||
@ -617,10 +634,13 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
const usePoll =
|
const usePoll =
|
||||||
typeof mod.startGyro === 'function' &&
|
typeof mod.startGyro === 'function' &&
|
||||||
(typeof mod.getGyroValue === 'function' || typeof mod.getGyroValueSync === 'function')
|
(typeof mod.getGyroValue === 'function' || typeof mod.getGyroValueSync === 'function')
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] usePoll:', usePoll)
|
||||||
|
|
||||||
if (usePoll) {
|
if (usePoll) {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] calling mod.startGyro')
|
||||||
try {
|
try {
|
||||||
mod.startGyro(startOpts, (res) => {
|
mod.startGyro(startOpts, (res) => {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] startGyro callback, res:', JSON.stringify(res))
|
||||||
if (myGen !== tiltGen || !gyroModule) return
|
if (myGen !== tiltGen || !gyroModule) return
|
||||||
/* 插件约定:首包只表示是否开启成功,不含持续角度;若回调无对象则无法进入官方要求的 getGyroValue 轮询 */
|
/* 插件约定:首包只表示是否开启成功,不含持续角度;若回调无对象则无法进入官方要求的 getGyroValue 轮询 */
|
||||||
if (!res) {
|
if (!res) {
|
||||||
@ -751,6 +771,7 @@ export function useLenticularStudioTilt(opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
|
console.log('[DEBUG useLenticularStudioTilt] start called')
|
||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
startNativeInternal()
|
startNativeInternal()
|
||||||
// #endif
|
// #endif
|
||||||
|
|||||||
@ -300,9 +300,9 @@
|
|||||||
"current": 0,
|
"current": 0,
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"name": "castlove-lenticular-create",
|
"name": "",
|
||||||
"path": "pages/castlove/create",
|
"path": "",
|
||||||
"query": "name=%E5%85%89%E6%A0%85%E5%8D%A1"
|
"query": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 加载中 -->
|
<!-- 加载中 -->
|
||||||
<view v-if="loading" class="loading-wrapper">
|
<view v-if="loading" class="loading-wrapper">
|
||||||
<!-- 旋转光环 -->
|
<!-- 旋转光环 -->
|
||||||
@ -1014,7 +1012,7 @@ onUnmounted(() => {
|
|||||||
width: 352rpx;
|
width: 352rpx;
|
||||||
height: 520rpx;
|
height: 520rpx;
|
||||||
margin-bottom: 32rpx;
|
margin-bottom: 32rpx;
|
||||||
animation: card-3d-flip 15s ease-in-out infinite;
|
/* animation: card-3d-flip 15s ease-in-out infinite; */
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1036,18 +1034,18 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-wrapper--lenticular {
|
/* .card-wrapper--lenticular {
|
||||||
width: 520rpx;
|
width: 520rpx;
|
||||||
height: 680rpx;
|
height: 680rpx;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.detail-lenticular-slot {
|
.detail-lenticular-slot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 78%;
|
width: 88%;
|
||||||
height: 96%;
|
height: 96%;
|
||||||
border-radius: 64rpx;
|
border-radius: 48rpx;
|
||||||
/* transform: translate(-50%, -50%) rotate(-10deg); */
|
/* transform: translate(-50%, -50%) rotate(-10deg); */
|
||||||
transform: translate(-50%, -50%) ;
|
transform: translate(-50%, -50%) ;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
@ -10,9 +10,9 @@
|
|||||||
</view>
|
</view>
|
||||||
<!-- <text class="nav-title">我的作品</text> -->
|
<!-- <text class="nav-title">我的作品</text> -->
|
||||||
<view class="nav-placeholder"></view>
|
<view class="nav-placeholder"></view>
|
||||||
<!-- <view class="nav-settings" @tap="goToSettings">
|
<view class="nav-settings" @tap="goToSettings">
|
||||||
<image class="nav-settings-icon" src="/static/icon/settings.png" mode="aspectFit"></image>
|
<image class="nav-settings-icon" src="/static/icon/settings.png" mode="aspectFit"></image>
|
||||||
</view> -->
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="scroll-content">
|
<view class="scroll-content">
|
||||||
@ -31,7 +31,7 @@
|
|||||||
<LenticularCard v-if="exhibitionAtSlot[0].is_lenticular" class="card-lenticular"
|
<LenticularCard v-if="exhibitionAtSlot[0].is_lenticular" class="card-lenticular"
|
||||||
:layers="getLenticularLayers(exhibitionAtSlot[0].id)"
|
:layers="getLenticularLayers(exhibitionAtSlot[0].id)"
|
||||||
:transforms="getLenticularTransforms(exhibitionAtSlot[0].id)" :gyro-source="gyroSourceLabel"
|
:transforms="getLenticularTransforms(exhibitionAtSlot[0].id)" :gyro-source="gyroSourceLabel"
|
||||||
:skip-built-in-touch="false" :shimmer-mid-opacity="0.16"
|
:skip-built-in-touch="true" :shimmer-mid-opacity="0.16"
|
||||||
@simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[0].id, x, y)" />
|
@simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[0].id, x, y)" />
|
||||||
<image v-else class="card-image"
|
<image v-else class="card-image"
|
||||||
:src="exhibitionAtSlot[0].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
|
:src="exhibitionAtSlot[0].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
|
||||||
@ -76,7 +76,7 @@
|
|||||||
<LenticularCard v-if="exhibitionAtSlot[1].is_lenticular" class="card-lenticular"
|
<LenticularCard v-if="exhibitionAtSlot[1].is_lenticular" class="card-lenticular"
|
||||||
:layers="getLenticularLayers(exhibitionAtSlot[1].id)"
|
:layers="getLenticularLayers(exhibitionAtSlot[1].id)"
|
||||||
:transforms="getLenticularTransforms(exhibitionAtSlot[1].id)" :gyro-source="gyroSourceLabel"
|
:transforms="getLenticularTransforms(exhibitionAtSlot[1].id)" :gyro-source="gyroSourceLabel"
|
||||||
:skip-built-in-touch="false" :shimmer-mid-opacity="0.16"
|
:skip-built-in-touch="true" :shimmer-mid-opacity="0.16"
|
||||||
@simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[1].id, x, y)" />
|
@simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[1].id, x, y)" />
|
||||||
<image v-else class="card-image"
|
<image v-else class="card-image"
|
||||||
:src="exhibitionAtSlot[1].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
|
:src="exhibitionAtSlot[1].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
|
||||||
@ -154,7 +154,7 @@
|
|||||||
<LenticularCard v-if="item.is_lenticular" class="liked-lenticular"
|
<LenticularCard v-if="item.is_lenticular" class="liked-lenticular"
|
||||||
:layers="getLikedLenticularLayers(item.id)"
|
:layers="getLikedLenticularLayers(item.id)"
|
||||||
:transforms="getLikedLenticularTransforms(item.id)" :gyro-source="gyroSourceLabel"
|
:transforms="getLikedLenticularTransforms(item.id)" :gyro-source="gyroSourceLabel"
|
||||||
:skip-built-in-touch="false" :shimmer-mid-opacity="0.16"
|
:skip-built-in-touch="true" :shimmer-mid-opacity="0.16"
|
||||||
@simulate="(x, y) => onLikedLenticularSimulate(item.id, x, y)" />
|
@simulate="(x, y) => onLikedLenticularSimulate(item.id, x, y)" />
|
||||||
<image v-else class="liked-cover" :src="item.cover_url || '/static/nft/placeholder.png'"
|
<image v-else class="liked-cover" :src="item.cover_url || '/static/nft/placeholder.png'"
|
||||||
mode="aspectFill"></image>
|
mode="aspectFill"></image>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user