180 lines
5.5 KiB
JavaScript
180 lines
5.5 KiB
JavaScript
function finiteNumber(value, fallback = 0) {
|
|
const parsed = Number(value)
|
|
return Number.isFinite(parsed) ? parsed : fallback
|
|
}
|
|
|
|
function clamp(value, minimum, maximum) {
|
|
return Math.min(maximum, Math.max(minimum, value))
|
|
}
|
|
|
|
const SCENE_WIDTH = 1400
|
|
const SCENE_HEIGHT = 788
|
|
const SCENE_ASPECT_RATIO = SCENE_WIDTH / SCENE_HEIGHT
|
|
|
|
function getChapterLayoutMetrics(windowInfo = {}, menuRect = {}) {
|
|
const windowWidth = Math.max(320, finiteNumber(windowInfo.windowWidth, 844))
|
|
const windowHeight = Math.max(240, finiteNumber(windowInfo.windowHeight, 390))
|
|
const safeArea = windowInfo.safeArea || {}
|
|
const safeLeft = clamp(finiteNumber(safeArea.left, 0), 0, windowWidth / 3)
|
|
const safeRightEdge = clamp(
|
|
finiteNumber(safeArea.right, windowWidth),
|
|
windowWidth * 2 / 3,
|
|
windowWidth,
|
|
)
|
|
const safeRight = clamp(windowWidth - safeRightEdge, 0, windowWidth / 3)
|
|
const statusBarHeight = Math.max(
|
|
0,
|
|
finiteNumber(windowInfo.statusBarHeight, 0),
|
|
)
|
|
const safeTop = Math.max(
|
|
0,
|
|
statusBarHeight,
|
|
finiteNumber(safeArea.top, 0),
|
|
)
|
|
const safeBottomEdge = clamp(
|
|
finiteNumber(safeArea.bottom, windowHeight),
|
|
windowHeight * 2 / 3,
|
|
windowHeight,
|
|
)
|
|
const safeBottom = clamp(
|
|
windowHeight - safeBottomEdge,
|
|
0,
|
|
windowHeight / 3,
|
|
)
|
|
const menuLeft = finiteNumber(menuRect.left, windowWidth)
|
|
const hasExplicitMenuTop = Number.isFinite(Number(menuRect.top))
|
|
&& Number(menuRect.top) >= 0
|
|
const menuTop = hasExplicitMenuTop ? Number(menuRect.top) : 0
|
|
const menuBottom = finiteNumber(menuRect.bottom, 0)
|
|
const explicitMenuHeight = Number(menuRect.height)
|
|
const menuHeight = Number.isFinite(explicitMenuHeight)
|
|
&& explicitMenuHeight > 0
|
|
? explicitMenuHeight
|
|
: (
|
|
hasExplicitMenuTop && menuBottom > menuTop
|
|
? menuBottom - menuTop
|
|
: 32
|
|
)
|
|
const hasMenuRect = menuLeft > 0
|
|
&& menuLeft < windowWidth
|
|
&& menuBottom > 0
|
|
|
|
// 横屏 rpx 按屏幕宽度换算;576px 高的设备仍属于短高屏。
|
|
// 这里使用实际 windowHeight,而不是设备型号或像素比。
|
|
const compactHeight = windowHeight <= 620
|
|
// Every visible top-bar action has a 48px minimum target. Keep the
|
|
// calculated row at least as tall so compact landscape phones do not clip
|
|
// the button above or below the explicit top-bar height.
|
|
const rowHeight = Math.max(48, menuHeight)
|
|
const inferredMenuTop = hasMenuRect
|
|
? (
|
|
hasExplicitMenuTop
|
|
? menuTop
|
|
: Math.max(safeTop, menuBottom - menuHeight)
|
|
)
|
|
: safeTop
|
|
const topPadding = Math.max(
|
|
safeTop,
|
|
hasMenuRect
|
|
? inferredMenuTop - Math.max(0, (rowHeight - menuHeight) / 2)
|
|
: safeTop + (compactHeight ? 3 : 5),
|
|
)
|
|
const bottomPadding = compactHeight ? 4 : 6
|
|
const borderHeight = 3
|
|
const topbarHeight = Math.ceil(
|
|
topPadding + rowHeight + bottomPadding + borderHeight,
|
|
)
|
|
const leftInset = safeLeft + (compactHeight ? 10 : 14)
|
|
const capsuleReserve = hasMenuRect
|
|
? Math.max(
|
|
safeRight + (compactHeight ? 10 : 14),
|
|
windowWidth - menuLeft + (compactHeight ? 8 : 10),
|
|
)
|
|
: safeRight + (compactHeight ? 12 : 16)
|
|
const contentHeight = Math.max(
|
|
1,
|
|
windowHeight - topbarHeight - safeBottom,
|
|
)
|
|
const layoutWidth = Math.max(
|
|
1,
|
|
windowWidth - safeLeft - safeRight,
|
|
)
|
|
const sceneColumnWidth = layoutWidth * 0.6
|
|
const stageWidth = Math.max(
|
|
1,
|
|
Math.min(sceneColumnWidth, contentHeight * SCENE_ASPECT_RATIO),
|
|
)
|
|
const stageHeight = stageWidth / SCENE_ASPECT_RATIO
|
|
const markerWidth = compactHeight
|
|
? clamp(stageWidth * 0.24, 142, 170)
|
|
: clamp(windowWidth / 750 * 200, 160, 220)
|
|
const markerHeight = compactHeight
|
|
? 56
|
|
: clamp(windowWidth / 750 * 86, 64, 94)
|
|
|
|
return {
|
|
windowWidth,
|
|
windowHeight,
|
|
compactHeight,
|
|
safeLeft,
|
|
safeRight,
|
|
safeTop,
|
|
safeBottom,
|
|
topPadding,
|
|
bottomPadding,
|
|
rowHeight,
|
|
topbarHeight,
|
|
leftInset,
|
|
capsuleReserve,
|
|
modalTop: Math.max(
|
|
safeTop + (compactHeight ? 4 : 8),
|
|
hasMenuRect ? menuBottom + 4 : 0,
|
|
),
|
|
stageWidth: Math.round(stageWidth),
|
|
stageHeight: Math.round(stageHeight),
|
|
markerWidth: Math.round(markerWidth),
|
|
markerHeight: Math.round(markerHeight),
|
|
sceneWidth: SCENE_WIDTH,
|
|
sceneHeight: SCENE_HEIGHT,
|
|
sceneAspectRatio: SCENE_ASPECT_RATIO,
|
|
}
|
|
}
|
|
|
|
function getMarkerPosition(position = {}, metrics = {}) {
|
|
const widthPercent = finiteNumber(position.widthPercent, 0)
|
|
const heightPercent = finiteNumber(position.heightPercent, 0)
|
|
const rawX = finiteNumber(position.xPercent, 50) + widthPercent / 2
|
|
const rawY = finiteNumber(position.yPercent, 50) + heightPercent / 2
|
|
const stageWidth = Math.max(1, finiteNumber(metrics.stageWidth, 500))
|
|
const stageHeight = Math.max(1, finiteNumber(metrics.stageHeight, 280))
|
|
const markerWidth = finiteNumber(metrics.markerWidth, 168)
|
|
const markerHeight = finiteNumber(metrics.markerHeight, 64)
|
|
const horizontalMargin = clamp(
|
|
markerWidth / 2 / stageWidth * 100 + 1.5,
|
|
2,
|
|
48,
|
|
)
|
|
const verticalMargin = clamp(
|
|
markerHeight / 2 / stageHeight * 100 + 2,
|
|
2,
|
|
48,
|
|
)
|
|
const clampedX = clamp(rawX, horizontalMargin, 100 - horizontalMargin)
|
|
const clampedY = clamp(rawY, verticalMargin, 100 - verticalMargin)
|
|
|
|
return {
|
|
xPercent: clampedX,
|
|
yPercent: clampedY,
|
|
xPx: Math.round(clampedX / 100 * stageWidth),
|
|
yPx: Math.round(clampedY / 100 * stageHeight),
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
SCENE_WIDTH,
|
|
SCENE_HEIGHT,
|
|
SCENE_ASPECT_RATIO,
|
|
getChapterLayoutMetrics,
|
|
getMarkerPosition,
|
|
}
|