Files
2026-09-09 14:47:29 +08:00

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,
}