@ray-js/ipc-player-integration 0.0.50-beta.2 → 0.0.50-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
1
2
  /** absolute 槽位 id:转盘浮层 */
2
3
  export const zoomTurntableDockId = 'zoomTurntableDockId';
3
4
 
@@ -36,15 +37,20 @@ export const LONG_PRESS_MS = 500;
36
37
  /** 长按期间允许的位移容差(px),超出视为横滑并取消长按 */
37
38
  export const MOVE_TOLERANCE = 10;
38
39
 
39
- /** switchChannel 下发超时(ms):回调始终不来时不知道切没切成,超时回滚读数 */
40
- export const SWITCH_LENS_TIMEOUT = 3000;
40
+ /**
41
+ * switchChannel 下发超时(ms):回调始终不来时不知道切没切成,超时回滚读数。
42
+ *
43
+ * 取 10s:设备切镜头本身就慢,回调回来得晚是常态;给短了会在回调还在路上时
44
+ * 先把读数回滚掉,随后回调又落地,反而多一次跳变。
45
+ */
46
+ export const SWITCH_LENS_TIMEOUT = 10000;
41
47
 
42
48
  /**
43
49
  * switchChannel 已 success 后,等 channelChange 报「切到位」的时长(ms)。
44
50
  * 超时按下发结果自己收尾,**不回滚** —— 指令已被受理,物理镜头其实已经切了,
45
51
  * 回滚读数只会让读数和画面对不上。
46
52
  *
47
- * 取 10s,比 SWITCH_LENS_TIMEOUT 宽得多:设备真正切完镜头本身要一会儿,
53
+ * 取 10s,与 SWITCH_LENS_TIMEOUT 同档:设备真正切完镜头本身要一会儿,
48
54
  * 给短了会在事件晚到时提前自己收尾,等于白订阅。
49
55
  * 真机现状:设备侧暂未推 channelChange,切镜头基本都靠这条超时收尾。
50
56
  */
@@ -70,67 +76,209 @@ export const GESTURE_SWITCH_DEBOUNCE = 500;
70
76
  export const ZOOM_DOCK_Z_INDEX = 120;
71
77
 
72
78
  /**
73
- * 竖屏截取比例:从椭圆上弧顶往下截取多少。
74
- * 0.5 = 完整上半椭圆(截面底边落在最宽处);> 0.5 会带出两侧下弯段。
75
- * 取 0.5 让背景一路铺到播放器底边、不留空隙,多出来的部分靠 ZOOM_DOCK_BOTTOM 探出被裁掉。
79
+ * 竖屏转盘占播放器高度的比例:1/3。
80
+ * 播放器容器按 16:9,所以高度由容器宽度推导,见 getVerticalPlayerHeight。
81
+ */
82
+ const ZOOM_HEIGHT_RATIO = 1 / 3;
83
+
84
+ /**
85
+ * 椭圆纵半径与背景带高度的比值,决定弧的**弯度**。
86
+ *
87
+ * 弧顶到屏幕两侧的落差 = ry × (1 − √(1 − (屏幕半宽 / 横半径)²));横半径也按容器宽缩放
88
+ * (见 ELLIPSE_RX_TO_WIDTH),所以括号里恒为 0.727,落差恒为 ry × 0.313。
89
+ *
90
+ * 取 1.6:74px 高的带子(393 宽机型)对应 37px 落差,与设计稿量出来的弧度一致。
91
+ * 曾用 1.83(落差 43px),弧偏弯。比值调大 = 更弯,调小 = 更平;与带高解耦,
92
+ * 改一个不影响另一个。
93
+ */
94
+ const ELLIPSE_RY_TO_HEIGHT = 1.6;
95
+
96
+ /**
97
+ * 椭圆横半径与容器宽度的比值(= 设计稿 258 / 375)。
98
+ *
99
+ * 椭圆比播放器宽约 37%,两端伸到播放器之外,背景才能左右铺满、两侧不露直角缺口。
100
+ * 必须跟着容器宽度缩放:弧顶到屏幕两侧的落差取决于「屏幕半宽 / 横半径」这个比值,
101
+ * 横半径写死 258 的话,宽屏机型上比值变大、弧明显更弯(430 宽时两侧只剩 15px)。
102
+ */
103
+ const ELLIPSE_RX_TO_WIDTH = 258 / 375;
104
+
105
+ /** 兜底容器宽度:getSystemInfoSync 在 onLoad 之前可能取不到值 */
106
+ const FALLBACK_WINDOW_WIDTH = 375;
107
+
108
+ /**
109
+ * 竖屏播放器容器宽度 = 窗口宽(竖屏播放器满宽铺开)。
110
+ *
111
+ * 用全局 `ty` 而不是 import `@ray-js/ray`:本模块被 lensSkill / lensModel 一路引到单测里,
112
+ * 一旦引入运行时依赖,jest 解析不了这个包,整条测试链会跟着挂。
113
+ */
114
+ const getContainerWidth = () => {
115
+ try {
116
+ var _ty, _ty$getSystemInfoSync;
117
+ return ((_ty = ty) === null || _ty === void 0 || (_ty$getSystemInfoSync = _ty.getSystemInfoSync) === null || _ty$getSystemInfoSync === void 0 || (_ty$getSystemInfoSync = _ty$getSystemInfoSync.call(_ty)) === null || _ty$getSystemInfoSync === void 0 ? void 0 : _ty$getSystemInfoSync.windowWidth) || FALLBACK_WINDOW_WIDTH;
118
+ } catch (err) {
119
+ console.warn('[zoomTurntable] getSystemInfoSync 失败,容器宽按 375 兜底', err);
120
+ return FALLBACK_WINDOW_WIDTH;
121
+ }
122
+ };
123
+
124
+ /**
125
+ * 竖屏播放器高度:容器按 16:9 推导。
126
+ * 不用固定值,pad / 折叠屏 / 大屏机型上转盘才能始终占到 1/3。
76
127
  */
77
- export const CROP_RATIO_BOTTOM = 0.5;
128
+ export const getVerticalPlayerHeight = () => getContainerWidth() * 9 / 16;
78
129
 
79
- /** 基准椭圆全高,与 rjs 里的 BASE_RY * 2 保持一致 */
80
- const BASE_ELLIPSE_HEIGHT = 351;
130
+ /** 竖屏遮罩椭圆横半径:跟着容器宽度缩放,各机型弧形一致 */
131
+ export const getEllipseRxBottom = () => Math.round(getContainerWidth() * ELLIPSE_RX_TO_WIDTH);
132
+
133
+ /** 竖屏背景弧顶到播放器底边的可见高度:播放器高的 1/3(375 宽机型约 70px) */
134
+ export const getZoomVisibleHeightBottom = () => Math.round(getVerticalPlayerHeight() * ZOOM_HEIGHT_RATIO);
135
+
136
+ /** 竖屏遮罩椭圆纵半径:跟着可见高度按 ELLIPSE_RY_TO_HEIGHT 缩放,各机型弯度一致 */
137
+ export const getEllipseRyBottom = () => Math.round(getZoomVisibleHeightBottom() * ELLIPSE_RY_TO_HEIGHT);
138
+
139
+ /**
140
+ * 竖屏截取比例:从椭圆上弧顶往下截多深。
141
+ *
142
+ * = 可见高度 / 椭圆全高 = 1 / (2 × ELLIPSE_RY_TO_HEIGHT) ≈ 0.273,与机型无关
143
+ * (可见高与纵半径同比例缩放)。**关键**:截面高度就等于要显示的高度,
144
+ * 不再靠「椭圆探出播放器底边、多出部分被裁」——
145
+ * CoverView 里 canvas 的负 margin 并不可靠,一旦不生效,探出去的那截就会原地显示出来,
146
+ * 表现就是转盘整体被顶高一截(实测占到播放器高的 1/2)、底边还留一条透明缝。
147
+ * 现在 canvas 底边 = 内容底边 = 播放器底边,不依赖负 margin 与裁切。
148
+ */
149
+ export const CROP_RATIO_BOTTOM = 1 / (2 * ELLIPSE_RY_TO_HEIGHT);
150
+
151
+ /**
152
+ * 轨道虚线弧与外圈遮罩弧之间的等距间距(px,scale=1)。
153
+ *
154
+ * rjs 侧按「横纵半径各减同一个绝对值」内收,所以这个值就是两条弧线处处的间距,
155
+ * 两条弧因此弧度一致、看着是一对平行弧(旧的按比例内收会让弧顶间距远小于两侧)。
156
+ *
157
+ * 用绝对值而不是跟着带高缩放:竖屏背景带本身就只有 70px 上下,间距按比例缩会失真。
158
+ *
159
+ * 竖屏取 32:它同时决定「文案下缘到虚线弧顶」的距离 —— 文案占掉
160
+ * LABEL_TOP_GAP_BOTTOM + 字高(11 + 12 = 23px),剩下 9px 就是两者之间的留白,
161
+ * 与设计稿一致(28 时只剩 5px,文字贴着刻度线)。
162
+ * 横屏取 40,与改动前的弧顶间距(39.25)保持一致,横屏观感不变。
163
+ */
164
+ export const TRACK_EDGE_INSET_BOTTOM = 32;
165
+ export const TRACK_EDGE_INSET_RIGHT = 40;
166
+
167
+ /**
168
+ * 竖屏刻度文案「顶边到背景弧」的留白(px,scale=1)。
169
+ *
170
+ * 文案位置以**遮罩弧**为基准、与它等距内收,而不是以轨道弧往外推:
171
+ * 这样调轨道间距(TRACK_EDGE_INSET_BOTTOM)文案不会跟着上下窜,
172
+ * 当前倍率(12px)与刻度数字(10px)两种字号的顶边也落在同一条线上。
173
+ *
174
+ * 取 11 = 最初的 6 再加 5:文字与外边缘贴太近显得局促。
175
+ */
176
+ export const LABEL_TOP_GAP_BOTTOM = 11;
177
+
178
+ /**
179
+ * 竖屏虚线弧「扎出播放器底边的位置」距左右边的距离(px)。
180
+ *
181
+ * 设计稿里刻度线两端是**插进播放器底部**的,而且插出去的位置离左右边还有一段距离 ——
182
+ * 不是贴着左右下角擦出去(那样看着就是「顶到最左最右」)。
183
+ */
184
+ export const TRACK_SIDE_GAP_BOTTOM = 28;
185
+
186
+ /**
187
+ * 竖屏轨道椭圆的横半径:由「虚线弧在距左右边 TRACK_SIDE_GAP_BOTTOM 处正好降到播放器底边」反推。
188
+ *
189
+ * 设 x = 屏幕半宽 − 留白、弧顶到底边的高度 h = 可见高 − 间距、纵半径 ry:
190
+ * ry × (1 − √(1 − (x / rx)²)) = h ⟹ rx = x / √(1 − (1 − h/ry)²)
191
+ *
192
+ * 算出来的 rx 比「遮罩横半径 − 间距」小一截,所以虚线弧比遮罩弧更弯 ——
193
+ * 两条弧只在弧顶附近保持 TRACK_EDGE_INSET_BOTTOM 的间距,往两侧虚线弧下坠得更快,
194
+ * 这正是设计稿的样子(背景横向铺满、刻度线扎进底部)。
195
+ */
196
+ export const getTrackRxBottom = () => {
197
+ const x = getContainerWidth() / 2 - TRACK_SIDE_GAP_BOTTOM;
198
+ const trackRy = getEllipseRyBottom() - TRACK_EDGE_INSET_BOTTOM;
199
+ const arcTopToBottom = getZoomVisibleHeightBottom() - TRACK_EDGE_INSET_BOTTOM;
200
+ if (trackRy <= 0 || arcTopToBottom <= 0) return 0;
201
+ const k = 1 - arcTopToBottom / trackRy;
202
+ const denom = Math.sqrt(Math.max(0.0001, 1 - k * k));
203
+ return Math.round(x / denom);
204
+ };
81
205
 
82
206
  /**
83
207
  * 触摸响应余量:canvas 是唯一能收 touch 的元素,把它向外扩一圈,
84
208
  * **整个转盘范围(椭圆截面 + 上方文案区 + 两侧空白)都能拖动**,而不是只有刻度线那一带。
85
209
  *
86
- * 绘制坐标由 renderer 按同样的余量补偿,视觉不变;下面的 ZOOM_DOCK_BOTTOM /
87
- * ZOOM_DOCK_RIGHT 也都从「内容边距」反推,改这两个值不会让转盘位置偏移。
210
+ * 绘制坐标由 renderer 按同样的余量补偿,视觉不变;下面的 dock 位置也都从「内容边距」反推,
211
+ * 改这两个值不会让转盘位置偏移。
88
212
  */
89
213
  export const HIT_PADDING_X = 48;
90
214
  export const HIT_PADDING_Y = 56;
91
215
 
92
216
  /**
93
- * 竖屏 dock 高度:必须 >= canvas 实际高度(椭圆截面 + 上下触摸余量),
94
- * 否则承载它的 CoverView 会把转盘裁掉一截。随 CROP_RATIO_BOTTOM / HIT_PADDING_Y 自动跟随,留 8px 余量。
217
+ * **`absolutePosition` 里的长度一律写成带单位的 px 字符串**(`'48px'`),不要写裸数字。
218
+ *
219
+ * 裸数字会被宿主按 **rpx**(750 设计稿基准)解释。实测(393 宽机型):给 `height: 194`
220
+ * 量到 101px、给 `bottom: -56` 量到 -29px,比例正好 393 / 750。历史上那条
221
+ * 「dock 可命中高度只有 155、canvas 高 287.5,顶部溢出被裁」就是这么来的 ——
222
+ * 当时 dock 高写 296,实际只有 296 × 393/750 ≈ 155px,dock 只有 canvas 一半高,
223
+ * canvas 顶部永远露在 CoverView 外、收不到 touch。
224
+ *
225
+ * canvas 自己的尺寸是 rjs 里用 `'...px'` 设的、本来就是真 px;dock 也统一用 px 字符串之后,
226
+ * 两边同一套单位,不再需要任何换算,也不受 rpx 取整误差影响
227
+ * (横屏尤其明显:它的 rpx 基准是屏幕长边,机型间差 150px 以上)。
95
228
  */
96
- export const ZOOM_DOCK_HEIGHT = Math.ceil(BASE_ELLIPSE_HEIGHT * CROP_RATIO_BOTTOM) + HIT_PADDING_Y * 2 + 8;
229
+ const px = value => `${Math.round(value)}px`;
97
230
 
98
231
  /**
99
- * 绘制内容(椭圆截面)顶边到播放器底边的距离 —— 真正的视觉基准。
100
- *
101
- * canvas dock 内是底部对齐的,内容在 canvas 里又偏移了一份 HIT_PADDING_Y,
102
- * 所以位置要从内容边界反推、而不是从 dock 边界反推,否则一改触摸余量转盘就整体挪位。
103
- * 内容顶距播放器底 = 截面高 + HIT_PADDING_Y + ZOOM_DOCK_BOTTOM
232
+ * 竖屏底部工具条高度的兜底值(px):实测不到时用它。
233
+ * 对应 ui.tsx 里 `.ipc-player-bottom-content` 竖屏分支的 `height: '48px'`;
234
+ * `-full` 变体还叠了 safe-area padding,所以正常路径走实测(measureBottomBarHeight)。
104
235
  */
105
- const ZOOM_CONTENT_TOP_GAP = 137;
236
+ export const BOTTOM_BAR_HEIGHT_VERTICAL = 48;
106
237
 
107
238
  /**
108
- * 竖屏 dock 距播放器底边的距离,固定 0(贴底,不再探出)。
109
- *
110
- * 这里曾用负值让浮层整体下沉、把椭圆下半探出播放器底边。副作用是 CoverView 顶边
111
- * 跟着被压低,canvas 顶部(含刻度线那一带)露在 CoverView 之外被裁掉 —— 原生层裁掉的
112
- * 部分收不到 touch,表现就是「只有刻度尺下方能滑,上半部分直接拖到播放器」。
113
- * 实测:dock 可命中高度只有 155,而 canvas 287.5,顶部溢出 132.5。
239
+ * 竖屏 canvas 高度(px)。两种布局的底部触摸余量不同:
240
+ * - 竖屏全屏:底部余量 0 —— canvas 底边就是工具条顶边,再留余量会盖住按钮并吃掉点击
241
+ * (canvas CoverView 里,层级高于普通 View);
242
+ * - 普通竖屏:工具条已被藏掉,余量上下对称,底部那一份靠 dock 的负 bottom 沉到播放器外。
243
+ */
244
+ const getZoomCanvasHeightBy = isVerticalFull => getZoomVisibleHeightBottom() + HIT_PADDING_Y * (isVerticalFull ? 1 : 2);
245
+
246
+ /**
247
+ * 竖屏 dock 距播放器底边的距离(px 字符串)。
114
248
  *
115
- * 现在改由 canvas 自己用负 margin 向下探出(见 ZOOM_CANVAS_OVERSHOOT_BOTTOM),
116
- * 浮层顶边因此上移,canvas 完整落在可命中区内,而内容视觉位置不变。
249
+ * - 竖屏全屏:**正值**,抬起一整条底部工具条的高度(优先用实测值,避免与 ui.tsx 写死的
250
+ * 48px 以及 `-full` 变体的 safe-area padding 不同步),于是 canvas 底边 = 工具条顶边;
251
+ * - 普通竖屏:**负值**,下沉一份底部触摸余量,让绘制内容底边正好落在播放器底边上。
252
+ * (负 bottom 是生效的;canvas 的负 marginBottom 才是无效的那个,别再往回改。)
117
253
  */
118
- export const ZOOM_DOCK_BOTTOM = 0;
254
+ export const getZoomDockBottom = (isVerticalFull, measuredBarHeight) => isVerticalFull ? px(measuredBarHeight && measuredBarHeight > 0 ? measuredBarHeight : BOTTOM_BAR_HEIGHT_VERTICAL) : px(-HIT_PADDING_Y);
119
255
 
120
256
  /**
121
- * 竖屏 canvas 底边探出播放器底边的距离。
122
- * canvas 在 dock 内底部对齐,靠这个负 margin 下移,使绘制内容仍然铺到播放器底边:
123
- * 内容底探出 = ZOOM_CANVAS_OVERSHOOT_BOTTOM - HIT_PADDING_Y
124
- * 探出去的部分由 .ipc-player-content 的 overflow: hidden 裁掉,与改动前的观感一致。
257
+ * 竖屏 dock 高度(px 字符串):必须 >= canvas 高度,否则承载它的 CoverView 会把转盘裁掉一截,
258
+ * 被裁掉的部分收不到 touch(「只有刻度尺下方能滑」/「手势完全不生效」都是它)。
259
+ * 普通竖屏还要把下沉的那一份底部余量算进来,所以 canvas 高本身就带两份余量。
125
260
  */
126
- export const ZOOM_CANVAS_OVERSHOOT_BOTTOM = Math.ceil(BASE_ELLIPSE_HEIGHT * CROP_RATIO_BOTTOM) + HIT_PADDING_Y - ZOOM_CONTENT_TOP_GAP;
261
+ export const getZoomDockHeight = isVerticalFull => px(getZoomCanvasHeightBy(isVerticalFull) + 8);
262
+
263
+ /** 竖屏 canvas / dock 的 px 尺寸,仅用于布局诊断日志与文档核对 */
264
+ export const getZoomLayoutDebugPx = isVerticalFull => ({
265
+ 可见高: getZoomVisibleHeightBottom(),
266
+ canvas高: Math.round(getZoomCanvasHeightBy(isVerticalFull)),
267
+ dock高: Math.round(getZoomCanvasHeightBy(isVerticalFull) + 8)
268
+ });
127
269
 
128
270
  /**
129
- * 横屏内容右边相对播放器右边的内缩距离(视觉基准)。
271
+ * 横屏内容右边相对播放器右边的内缩距离(视觉基准),取 0 = **贴齐播放器右边缘**。
272
+ *
130
273
  * canvas 在 dock 内右对齐,内容右边又内缩一份 HIT_PADDING_Y(横屏画布旋转 90°):
131
- * 内容右边内缩 = ZOOM_DOCK_RIGHT + HIT_PADDING_Y
274
+ * 内容右边内缩 = dock right(px) + HIT_PADDING_Y = 这个值
275
+ * 写进 absolutePosition 的 px 字符串由 getZoomDockRight 产出。
276
+ *
277
+ * 原来是 14 —— 遮罩右侧因此始终差 14px 铺不到边,画面从那条缝里透出来,
278
+ * 就是「横屏右边有空白」。遮罩的平底边(横屏映射到屏幕右缘)另有 EDGE_OVERFILL
279
+ * 往外多铺一点点,配合这里的 0 才真正做到无缝。
132
280
  */
133
- const ZOOM_CONTENT_RIGHT_INSET = 14;
281
+ const ZOOM_CONTENT_RIGHT_INSET = 0;
134
282
 
135
283
  /**
136
284
  * 拖动转盘时连续下发缩放的节流间隔(ms)。
@@ -139,31 +287,54 @@ const ZOOM_CONTENT_RIGHT_INSET = 14;
139
287
  */
140
288
  export const ZOOM_PREVIEW_THROTTLE = 80;
141
289
 
142
- /** 横屏 dock 距播放器右边的距离,负值时向右探出 */
143
- export const ZOOM_DOCK_RIGHT = ZOOM_CONTENT_RIGHT_INSET - HIT_PADDING_Y;
290
+ /**
291
+ * 横屏 dock 距播放器右边的距离(px 字符串,负值向右探出)。
292
+ *
293
+ * 之前写的是裸数字、被当 rpx 解释,而横屏的 rpx 基准是屏幕**长边**、机型间差 150px 以上,
294
+ * 换算下来内容内缩量在 3.8~12.3px 之间飘(窄屏留明显缝、宽屏几乎贴边,
295
+ * 就是「有些机型右边有 1px 空白」的来源)。写成 px 之后各机型恒为 14px。
296
+ *
297
+ * 内容右边内缩 = 本函数的 px 值 + HIT_PADDING_Y = ZOOM_CONTENT_RIGHT_INSET = 14px
298
+ */
299
+ export const getZoomDockRight = () => px(ZOOM_CONTENT_RIGHT_INSET - HIT_PADDING_Y);
144
300
 
145
301
  /** 横屏截取比例(右侧展示空间较窄,截得更深) */
146
302
  export const CROP_RATIO_RIGHT = 0.45;
147
303
 
148
- /** 布局沉降兜底:onShow / onResize 后的重挂载延时 */
149
- export const RELAYOUT_DELAYS = [80, 260];
150
-
151
- /** 刻度与文案整体下移量(角度不变,仅平移绘制坐标系) */
304
+ /**
305
+ * 横屏刻度与文案的整体下移量(角度不变,仅平移绘制坐标系)。
306
+ * 竖屏不用它(截面已精确等于可见区,再平移会把底边裁掉一截),固定传 0。
307
+ */
152
308
  export const CONTENT_OFFSET_Y = 10;
153
309
 
154
- /** 刻度与标记值向椭圆内侧收的距离,加大与遮罩边缘的间距 */
155
- export const TRACK_EDGE_INSET = 10;
310
+ /** 布局沉降兜底:onShow / onResize 后的重挂载延时 */
311
+ export const RELAYOUT_DELAYS = [80, 260];
156
312
 
157
- /** 竖屏样式微调 */
158
- export const STYLE_OPTIONS_BOTTOM = {
313
+ /**
314
+ * 竖屏样式微调。纵向几何跟着容器 16:9 推导,所以是函数而不是常量对象。
315
+ *
316
+ * `contentOffsetY: 0`:椭圆截面已经精确等于可见高度,再整体下移会把底边那一截裁掉。
317
+ *
318
+ * 底部触摸余量按布局给:竖屏全屏传 0(canvas 底边就是工具条顶边,再留余量会盖住按钮
319
+ * 并吃掉它们的点击);普通竖屏工具条已藏,保持上下对称、由 dock 的负 bottom 沉出去。
320
+ * (早先无条件设 0 时手势整体失灵,根因不在它,而是 rjs/index.js 漏读 ellipseRx/ellipseRy
321
+ * + absolutePosition 的 rpx 单位,两处都已修。)
322
+ */
323
+ export const getStyleOptionsBottom = isVerticalFull => _objectSpread({
159
324
  topSafeArea: 30,
160
325
  currentLabelFontSize: 12,
161
326
  markLabelFontSize: 10,
162
- contentOffsetY: CONTENT_OFFSET_Y,
163
- trackEdgeInset: TRACK_EDGE_INSET,
327
+ contentOffsetY: 0,
328
+ trackEdgeInset: TRACK_EDGE_INSET_BOTTOM,
329
+ trackRx: getTrackRxBottom(),
330
+ labelTopGap: LABEL_TOP_GAP_BOTTOM,
331
+ ellipseRx: getEllipseRxBottom(),
332
+ ellipseRy: getEllipseRyBottom(),
164
333
  hitPaddingX: HIT_PADDING_X,
165
334
  hitPaddingY: HIT_PADDING_Y
166
- };
335
+ }, isVerticalFull ? {
336
+ hitPaddingBottom: 0
337
+ } : null);
167
338
 
168
339
  /** 横屏样式微调:压小刻度、文字与指针,避免与右侧控件重叠 */
169
340
  export const STYLE_OPTIONS_RIGHT = {
@@ -174,19 +345,65 @@ export const STYLE_OPTIONS_RIGHT = {
174
345
  innerLabelOffsetY: 36,
175
346
  // 横屏画布整体旋转 90°,这里的偏移沿旋转后坐标系生效(即向屏幕右侧)
176
347
  contentOffsetY: CONTENT_OFFSET_Y,
177
- trackEdgeInset: TRACK_EDGE_INSET,
348
+ // 横屏不传 ellipseRy,沿用设计稿的 175.5:右侧空间够,无需压扁
349
+ trackEdgeInset: TRACK_EDGE_INSET_RIGHT,
178
350
  hitPaddingX: HIT_PADDING_X,
179
351
  hitPaddingY: HIT_PADDING_Y
180
352
  };
181
353
 
182
354
  /**
183
- * 普通刻度震动节流(ms)。step=0.1 时整个区间有几十级,
184
- * 不节流会震得过密,反而糊成一片没有颗粒感。
355
+ * 普通刻度震动的最小间隔(ms)。
356
+ *
357
+ * 60ms 太密:拖得稍快时 onChange 就是逐帧回调,等于每秒震十几下,马达来不及回位,
358
+ * 手上是一片持续嗡鸣而不是逐格咔哒。取 110ms 接近人手能分辨两次震动的下限,
359
+ * 也和连续下发的节流(80ms)拉开,不至于两个节奏互相打架。
360
+ */
361
+ export const VIBRATE_THROTTLE = 110;
362
+
363
+ /**
364
+ * 判定「拖得很快」的跨格数:一次 onChange 跨过的刻度数超过它,就不再逐格震。
365
+ *
366
+ * 快速甩动时用户根本分辨不出逐格反馈,继续震只会糊成一片;这时只保留跨主刻度的
367
+ * medium 挡位感,反而更清楚自己甩过了哪几档。
368
+ */
369
+ export const VIBRATE_FAST_STEPS = 3;
370
+
371
+ /**
372
+ * 内部调试开关:置 true 时忽略「只提示一次」的本地记录,每次出流都展示圈选引导,
373
+ * 点「知道了」也不写入已读标记 —— 便于反复核对引导的位置与样式。
374
+ *
375
+ * 只在自测引导样式时临时打开,**提交前必须回到 false**。
376
+ * 与 useLensZoom 里的 USE_MOCK_ZOOM_SKILL 是两个独立开关:
377
+ * 设备没有 ipc_multi_zoom 时入口本身就不渲染,引导自然也不会出现,那种情况要一起打开。
378
+ */
379
+ export const FORCE_SHOW_ZOOM_TIP = false;
380
+
381
+ /**
382
+ * 圈选提示:圈选框在入口 rect 四周额外留的内边距。
383
+ * 入口是横向的倍率胶囊(宽度随镜头数变化),圈选框按实测 rect 自适应,
384
+ * 不能用固定尺寸 —— 固定值既框不住宽度,上下又会超出。
385
+ */
386
+ export const TIP_RING_PADDING = 6;
387
+
388
+ /** 圈选提示:圈选框圆角,与 multiCamera 的 tile-tip 保持一致 */
389
+ export const TIP_RING_RADIUS = 12;
390
+
391
+ /** 圈选提示:引线总长(虚线 17 + 末端圆点 4),比 multiCamera 短,文案更贴近圈选处 */
392
+ export const TIP_LINE_TOTAL = 21;
393
+
394
+ /** 圈选提示:文案 + 按钮的面板宽度与距播放器边缘的最小间距 */
395
+ export const TIP_PANEL_WIDTH = 220;
396
+ export const TIP_PANEL_EDGE_GAP = 16;
397
+
398
+ /**
399
+ * 圈选提示:出流后延迟多久再测量并展示(ms)。
400
+ * 刚 PLAYING 时左下角元素还在陆续渲染、宽度还在长,等布局沉降再画圈才不会歪;
401
+ * 也顺带避免重连一恢复引导就弹出来。
185
402
  */
186
- export const VIBRATE_THROTTLE = 60;
403
+ export const TIP_RESHOW_DELAY = 500;
187
404
 
188
- /** 圈选提示:圆环尺寸 */
189
- export const TIP_RING_SIZE = 44;
405
+ /** 圈选提示:引线末端到文案面板的间距 */
406
+ export const TIP_PANEL_LINE_GAP = 8;
190
407
 
191
408
  /** 可视区判定:避开左下角滚动容器右侧淡出带的安全距离 */
192
409
  export const FADE_SAFE_PADDING = 24;
@@ -18,5 +18,14 @@ export type AnchorRect = {
18
18
  * @returns 可见时返回入口相对视口的 rect,不可见 / 测不到返回 null
19
19
  */
20
20
  export declare const measureEntryVisible: () => Promise<AnchorRect | null>;
21
+ /**
22
+ * 竖屏底部工具条的实测高度(px),测不到返回 null。
23
+ *
24
+ * 转盘在竖屏全屏下要停在工具条之上,而这条的高度不止 ui.tsx 里写的 48px ——
25
+ * `-full` 变体还叠了 `padding-bottom: env(safe-area-inset-bottom)`,实际占位随机型变化。
26
+ * 用实测值定位,才不会压住右下角那排按钮(canvas 在 CoverView 里,层级高于按钮,
27
+ * 一压上去就把点击也吃掉了)。
28
+ */
29
+ export declare const measureBottomBarHeight: () => Promise<number | null>;
21
30
  /** 播放器容器 rect,用于把入口的视口坐标换算成容器内坐标 */
22
31
  export declare const measurePlayerRect: () => Promise<AnchorRect | null>;
@@ -62,6 +62,16 @@ export const measureEntryVisible = async () => {
62
62
  return null;
63
63
  };
64
64
 
65
+ /**
66
+ * 竖屏底部工具条的实测高度(px),测不到返回 null。
67
+ *
68
+ * 转盘在竖屏全屏下要停在工具条之上,而这条的高度不止 ui.tsx 里写的 48px ——
69
+ * `-full` 变体还叠了 `padding-bottom: env(safe-area-inset-bottom)`,实际占位随机型变化。
70
+ * 用实测值定位,才不会压住右下角那排按钮(canvas 在 CoverView 里,层级高于按钮,
71
+ * 一压上去就把点击也吃掉了)。
72
+ */
73
+ export const measureBottomBarHeight = () => queryRect('.ipc-player-bottom-content').then(rect => (rect === null || rect === void 0 ? void 0 : rect.height) > 0 ? rect.height : null);
74
+
65
75
  /** 播放器容器 rect,用于把入口的视口坐标换算成容器内坐标 */
66
76
  export const measurePlayerRect = () => queryRect('.ipc-player-content').then(rect => {
67
77
  var _rect$right, _rect$bottom;
@@ -23,6 +23,7 @@ const ZoomTurntable = props => {
23
23
  onChangeEnd,
24
24
  onTouchStart,
25
25
  onTouchEnd,
26
+ onBlankTap,
26
27
  marks,
27
28
  ticks,
28
29
  curve = 1,
@@ -73,6 +74,10 @@ const ZoomTurntable = props => {
73
74
  bindturntableTouchEnd: () => {
74
75
  console.log('[zoomTurntable/logic] turntableTouchEnd');
75
76
  onTouchEnd === null || onTouchEnd === void 0 || onTouchEnd();
77
+ },
78
+ bindblankTap: () => {
79
+ console.log('[zoomTurntable/logic] blankTap');
80
+ onBlankTap === null || onBlankTap === void 0 || onBlankTap();
76
81
  }
77
82
  });
78
83
  };
@@ -2,6 +2,13 @@
2
2
  -- 小程序原生组件层(rjs Component 壳),运行在 render 线程的 ES5 环境,
3
3
  沿用内嵌来源 ray-zoom-turntable 的写法,不套用主工程的 ES6+ 规则 */
4
4
  import Render from './index.rjs';
5
+
6
+ /**
7
+ * 注意:下面的 computeCanvasViewport 是 canvas **DOM 尺寸**的唯一来源(index.tyml 内联 style 用它),
8
+ * 与 index.rjs 的 _computeGeometry 是两份独立实现,**必须逐项对齐**。
9
+ * 任何影响画布尺寸的 styleOptions(ellipseRx/ellipseRy/hitPadding*)两边都要读,
10
+ * 否则绘制坐标系与 DOM 尺寸不一致:画面被拉伸,且溢出承载 CoverView 的部分收不到 touch。
11
+ */
5
12
  var BASE_RX = 258;
6
13
  var BASE_RY = 175.5;
7
14
 
@@ -67,7 +74,9 @@ function computeCanvasViewport(props) {
67
74
  if (cropRatio > 1) cropRatio = 1;
68
75
  // 与 renderer 的 hitPadding 保持一致,否则 canvas 的 DOM 尺寸与绘制尺寸会打架
69
76
  var hitPaddingX = styleNumber(props.styleOptions, 'hitPaddingX', 0) * inputScale;
70
- var hitPaddingY = styleNumber(props.styleOptions, 'hitPaddingY', 0) * inputScale;
77
+ var hitPaddingY = styleNumber(props.styleOptions, 'hitPaddingY', 0);
78
+ var hitPaddingTop = styleNumber(props.styleOptions, 'hitPaddingTop', hitPaddingY) * inputScale;
79
+ var hitPaddingBottom = styleNumber(props.styleOptions, 'hitPaddingBottom', hitPaddingY) * inputScale;
71
80
  var bottomSin = 2 * cropRatio - 1;
72
81
  if (bottomSin < -1) bottomSin = -1;
73
82
  if (bottomSin > 1) bottomSin = 1;
@@ -76,9 +85,19 @@ function computeCanvasViewport(props) {
76
85
  cropWidthRatio = Math.sqrt(1 - bottomSin * bottomSin);
77
86
  }
78
87
  var contentScale = inputScale;
79
- var cropHalfWidth = BASE_RX * contentScale * cropWidthRatio;
80
- var canvasWidth = cropHalfWidth * 2 + hitPaddingX * 2;
81
- var canvasHeight = BASE_RY * contentScale * 2 * cropRatio + hitPaddingY * 2;
88
+ // 椭圆半径同样要读 styleOptions —— 竖屏会按容器 16:9 传入压扁后的 ellipseRx/ellipseRy。
89
+ // 这里漏读过一次:canvas DOM 尺寸仍按设计稿 258×175.5 算(208 高),
90
+ // renderer 按传入值算(186 高),结果画面被拉伸、canvas 顶部溢出承载它的
91
+ // CoverView 被裁掉 —— 裁掉的部分收不到 touch,表现就是「手势整体不生效」。
92
+ var maskRx = styleNumber(props.styleOptions, 'ellipseRx', BASE_RX) * contentScale;
93
+ var maskRy = styleNumber(props.styleOptions, 'ellipseRy', BASE_RY) * contentScale;
94
+ var cropHalfWidth = maskRx * cropWidthRatio;
95
+
96
+ // 向上取整,与 index.rjs 的 baseWidth / baseHeight 保持一致:
97
+ // 尺寸取整不足会被原生层裁掉最外侧那一行像素(部分机型边缘露 1px 空白),
98
+ // 两边算法只要有一处不取整,DOM 尺寸与绘制坐标系就又会打架。
99
+ var canvasWidth = Math.ceil(cropHalfWidth * 2 + hitPaddingX * 2);
100
+ var canvasHeight = Math.ceil(maskRy * 2 * cropRatio + hitPaddingTop + hitPaddingBottom);
82
101
  return orientation === 'right' ? {
83
102
  canvasWidth: canvasHeight,
84
103
  canvasHeight: canvasWidth
@@ -231,6 +250,10 @@ Component({
231
250
  onChangeEnd(value) {
232
251
  this.triggerEvent('changeEnd', value);
233
252
  },
253
+ // 点在转盘空白处(未命中刻度带):等同于点画面,逻辑层据此收起转盘
254
+ onBlankTap() {
255
+ this.triggerEvent('blankTap');
256
+ },
234
257
  // 转盘触摸开始 / 结束(含 touchcancel),逻辑层据此切换 ty.nativeDisabled
235
258
  onTouchStart() {
236
259
  this.triggerEvent('turntableTouchStart');