mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
fix(renderer): calibrate macOS invisible overlay spacing
This commit is contained in:
@@ -163,7 +163,7 @@ test('applyTypography applies full mpv letter spacing scale on macOS', () => {
|
||||
assert.equal(ctx.dom.subtitleRoot.style.getPropertyValue('letter-spacing'), '3px');
|
||||
});
|
||||
|
||||
test('applyTypography uses tighter macOS line-height for invisible multiline alignment', () => {
|
||||
test('applyTypography uses macOS multiline-tuned line-height for invisible overlay', () => {
|
||||
const ctx = createContext({
|
||||
isMacOSPlatform: true,
|
||||
lineCount: 3,
|
||||
@@ -178,7 +178,7 @@ test('applyTypography uses tighter macOS line-height for invisible multiline ali
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(ctx.dom.subtitleRoot.style.getPropertyValue('line-height'), '0.96');
|
||||
assert.equal(ctx.dom.subtitleRoot.style.getPropertyValue('line-height'), '1.62');
|
||||
});
|
||||
|
||||
test('applyVerticalPosition uses subtitle position margin and baseline compensation', () => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { MpvSubtitleRenderMetrics } from '../../types';
|
||||
import type { RendererContext } from '../context';
|
||||
|
||||
const INVISIBLE_MACOS_VERTICAL_NUDGE_PX = 5;
|
||||
const INVISIBLE_MACOS_LINE_HEIGHT_SINGLE = '1.08';
|
||||
const INVISIBLE_MACOS_LINE_HEIGHT_MULTI = '1.35';
|
||||
const INVISIBLE_MACOS_LINE_HEIGHT_MULTI_DENSE = '1.48';
|
||||
|
||||
let fontMetricsCanvas: HTMLCanvasElement | null = null;
|
||||
|
||||
export function applyContainerBaseLayout(
|
||||
@@ -112,6 +117,13 @@ function resolveFontFamily(rawFont: string): string {
|
||||
: `"${rawFont}", sans-serif`;
|
||||
}
|
||||
|
||||
export function resolveInvisibleLineHeight(lineCount: number, isMacOSPlatform: boolean): string {
|
||||
if (!isMacOSPlatform) return 'normal';
|
||||
if (lineCount >= 3) return INVISIBLE_MACOS_LINE_HEIGHT_MULTI_DENSE;
|
||||
if (lineCount >= 2) return INVISIBLE_MACOS_LINE_HEIGHT_MULTI;
|
||||
return INVISIBLE_MACOS_LINE_HEIGHT_SINGLE;
|
||||
}
|
||||
|
||||
function resolveLetterSpacing(
|
||||
spacing: number,
|
||||
pxPerScaledPixel: number,
|
||||
@@ -123,10 +135,6 @@ function resolveLetterSpacing(
|
||||
return '0px';
|
||||
}
|
||||
|
||||
function resolveInvisibleLineHeight(isMacOSPlatform: boolean): string {
|
||||
return isMacOSPlatform ? '0.96' : '1';
|
||||
}
|
||||
|
||||
function measureFontDescentPx(ctx: RendererContext): number | null {
|
||||
if (typeof document === 'undefined') return null;
|
||||
const computedStyle = getComputedStyle(ctx.dom.subtitleRoot);
|
||||
@@ -148,6 +156,42 @@ function measureFontDescentPx(ctx: RendererContext): number | null {
|
||||
return metrics.actualBoundingBoxDescent;
|
||||
}
|
||||
|
||||
function applyComputedLineHeightCompensation(
|
||||
ctx: RendererContext,
|
||||
effectiveFontSize: number,
|
||||
): void {
|
||||
const computedLineHeight = parseFloat(getComputedStyle(ctx.dom.subtitleRoot).lineHeight);
|
||||
if (!Number.isFinite(computedLineHeight) || computedLineHeight <= effectiveFontSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const halfLeading = (computedLineHeight - effectiveFontSize) / 2;
|
||||
if (halfLeading <= 0.5) return;
|
||||
|
||||
const currentBottom = parseFloat(ctx.dom.subtitleContainer.style.bottom);
|
||||
if (Number.isFinite(currentBottom)) {
|
||||
ctx.dom.subtitleContainer.style.bottom = `${Math.max(0, currentBottom - halfLeading)}px`;
|
||||
}
|
||||
|
||||
const currentTop = parseFloat(ctx.dom.subtitleContainer.style.top);
|
||||
if (Number.isFinite(currentTop)) {
|
||||
ctx.dom.subtitleContainer.style.top = `${Math.max(0, currentTop - halfLeading)}px`;
|
||||
}
|
||||
}
|
||||
|
||||
function applyMacOSAdjustments(ctx: RendererContext): void {
|
||||
const isMacOSPlatform = ctx.platform.isMacOSPlatform;
|
||||
if (!isMacOSPlatform) return;
|
||||
|
||||
const currentBottom = parseFloat(ctx.dom.subtitleContainer.style.bottom);
|
||||
if (!Number.isFinite(currentBottom)) return;
|
||||
|
||||
ctx.dom.subtitleContainer.style.bottom = `${Math.max(
|
||||
0,
|
||||
currentBottom + INVISIBLE_MACOS_VERTICAL_NUDGE_PX,
|
||||
)}px`;
|
||||
}
|
||||
|
||||
export function applyTypography(
|
||||
ctx: RendererContext,
|
||||
params: {
|
||||
@@ -157,11 +201,14 @@ export function applyTypography(
|
||||
},
|
||||
): void {
|
||||
const isMacOSPlatform = ctx.platform.isMacOSPlatform;
|
||||
const lineCount = Math.max(1, ctx.state.currentInvisibleSubtitleLineCount);
|
||||
const invisibleLineHeight = resolveInvisibleLineHeight(lineCount, isMacOSPlatform);
|
||||
|
||||
ctx.dom.subtitleRoot.style.setProperty('--invisible-sub-line-height', invisibleLineHeight);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
'line-height',
|
||||
resolveInvisibleLineHeight(isMacOSPlatform),
|
||||
'important',
|
||||
invisibleLineHeight,
|
||||
isMacOSPlatform ? 'important' : '',
|
||||
);
|
||||
ctx.dom.subtitleRoot.style.fontFamily = resolveFontFamily(params.metrics.subFont);
|
||||
ctx.dom.subtitleRoot.style.setProperty(
|
||||
@@ -175,4 +222,7 @@ export function applyTypography(
|
||||
ctx.dom.subtitleRoot.style.transform = '';
|
||||
ctx.dom.subtitleRoot.style.transformOrigin = '';
|
||||
ctx.state.invisibleMeasuredDescentPx = measureFontDescentPx(ctx);
|
||||
|
||||
applyComputedLineHeightCompensation(ctx, params.effectiveFontSize);
|
||||
applyMacOSAdjustments(ctx);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,9 @@ export function createMpvSubtitleLayoutController(
|
||||
options.applyInvisibleSubtitleOffsetPosition();
|
||||
options.updateInvisiblePositionEditHud();
|
||||
|
||||
console.log('[invisible-overlay] Applied mpv subtitle render metrics from', source);
|
||||
if (source !== 'subtitle-change') {
|
||||
console.log('[invisible-overlay] Applied mpv subtitle render metrics from', source);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -240,6 +240,9 @@ function runGuardedAsync(action: string, fn: () => Promise<void> | void): void {
|
||||
|
||||
async function init(): Promise<void> {
|
||||
document.body.classList.add(`layer-${ctx.platform.overlayLayer}`);
|
||||
if (ctx.platform.isMacOSPlatform) {
|
||||
document.body.classList.add('platform-macos');
|
||||
}
|
||||
|
||||
window.electronAPI.onSubtitle((data: SubtitleData) => {
|
||||
runGuarded('subtitle:update', () => {
|
||||
@@ -252,7 +255,7 @@ async function init(): Promise<void> {
|
||||
if (ctx.platform.isInvisibleLayer && ctx.state.mpvSubtitleRenderMetrics) {
|
||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(
|
||||
ctx.state.mpvSubtitleRenderMetrics,
|
||||
'subtitle',
|
||||
'subtitle-change',
|
||||
);
|
||||
}
|
||||
measurementReporter.schedule();
|
||||
|
||||
@@ -279,7 +279,7 @@ body {
|
||||
#subtitleRoot {
|
||||
text-align: center;
|
||||
font-size: 35px;
|
||||
line-height: 1.5;
|
||||
line-height: var(--visible-sub-line-height, 1.32);
|
||||
color: #cad3f5;
|
||||
--subtitle-known-word-color: #a6da95;
|
||||
--subtitle-n-plus-one-color: #c6a0f6;
|
||||
@@ -426,7 +426,12 @@ body.settings-modal-open #subtitleContainer {
|
||||
#subtitleRoot br {
|
||||
display: block;
|
||||
content: '';
|
||||
margin-bottom: 0.3em;
|
||||
margin-bottom: var(--visible-sub-line-gap, 0.08em);
|
||||
}
|
||||
|
||||
body.platform-macos.layer-visible #subtitleRoot {
|
||||
--visible-sub-line-height: 1.64;
|
||||
--visible-sub-line-gap: 0.54em;
|
||||
}
|
||||
|
||||
#subtitleRoot.has-selection .word:hover,
|
||||
@@ -452,7 +457,7 @@ body.layer-invisible #subtitleRoot .c {
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
background: transparent !important;
|
||||
caret-color: transparent !important;
|
||||
line-height: normal !important;
|
||||
line-height: var(--invisible-sub-line-height, normal) !important;
|
||||
font-kerning: auto;
|
||||
letter-spacing: normal;
|
||||
font-variant-ligatures: normal;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
normalizeSubtitle,
|
||||
shouldRenderTokenizedSubtitle,
|
||||
} from './subtitle-render.js';
|
||||
import { resolveInvisibleLineHeight } from './positioning/invisible-layout-helpers.js';
|
||||
|
||||
function createToken(overrides: Partial<MergedToken>): MergedToken {
|
||||
return {
|
||||
@@ -328,4 +329,32 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
||||
);
|
||||
assert.match(block, /color:\s*var\(/);
|
||||
}
|
||||
|
||||
const invisibleBlock = extractClassBlock(
|
||||
cssText,
|
||||
'body.layer-invisible #subtitleRoot',
|
||||
);
|
||||
assert.match(
|
||||
invisibleBlock,
|
||||
/line-height:\s*var\(--invisible-sub-line-height,\s*normal\)\s*!important;/,
|
||||
);
|
||||
|
||||
const visibleMacBlock = extractClassBlock(
|
||||
cssText,
|
||||
'body.platform-macos.layer-visible #subtitleRoot',
|
||||
);
|
||||
assert.match(visibleMacBlock, /--visible-sub-line-height:\s*1\.64;/);
|
||||
assert.match(visibleMacBlock, /--visible-sub-line-gap:\s*0\.54em;/);
|
||||
});
|
||||
|
||||
test('invisible overlay uses looser line height on macOS for multi-line subtitles', () => {
|
||||
assert.equal(resolveInvisibleLineHeight(1, true), '1.08');
|
||||
assert.equal(resolveInvisibleLineHeight(2, true), '1.5');
|
||||
assert.equal(resolveInvisibleLineHeight(3, true), '1.62');
|
||||
});
|
||||
|
||||
test('invisible overlay keeps default line height on non-macOS platforms', () => {
|
||||
assert.equal(resolveInvisibleLineHeight(1, false), 'normal');
|
||||
assert.equal(resolveInvisibleLineHeight(2, false), 'normal');
|
||||
assert.equal(resolveInvisibleLineHeight(4, false), 'normal');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user