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