mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-13 18:16:13 +00:00
245 lines
8.3 KiB
JavaScript
245 lines
8.3 KiB
JavaScript
import {describe, expect, test} from 'vitest';
|
|
import {
|
|
estimateTextWidth,
|
|
getLabelFontSize,
|
|
getLabelLines,
|
|
getLabelTrack,
|
|
getRadialLabelLayout,
|
|
} from '../src/remotion/wheel-label-layout.js';
|
|
|
|
describe('wheel label layout', () => {
|
|
const readableExample = 'Chiều nay uống CraneTea';
|
|
const supportedSize = 512;
|
|
const supportedRadius = supportedSize * 0.41;
|
|
const supportedHubRadius = supportedSize * 0.09;
|
|
|
|
/**
|
|
* @param {object} [overrides]
|
|
* @param {number} [overrides.optionCount]
|
|
* @param {string} [overrides.text]
|
|
*/
|
|
const get512Layout = ({optionCount = 8, text = readableExample} = {}) =>
|
|
getRadialLabelLayout({
|
|
center: supportedSize / 2,
|
|
radius: supportedRadius,
|
|
hubRadius: supportedHubRadius,
|
|
centerDegrees: 0,
|
|
optionCount,
|
|
text,
|
|
});
|
|
|
|
test('centers radial label tracks on the slice center angle', () => {
|
|
const center = 256;
|
|
const radius = 210;
|
|
const hubRadius = 46;
|
|
const centerDegrees = 45;
|
|
const layout = getRadialLabelLayout({
|
|
center,
|
|
radius,
|
|
hubRadius,
|
|
centerDegrees,
|
|
optionCount: 8,
|
|
text: 'alice',
|
|
});
|
|
const track = getLabelTrack(radius, hubRadius);
|
|
const radians = (centerDegrees * Math.PI) / 180;
|
|
|
|
expect(layout.x).toBeCloseTo(center + Math.cos(radians) * track.midRadius);
|
|
expect(layout.y).toBeCloseTo(center + Math.sin(radians) * track.midRadius);
|
|
expect(layout.width).toBe(track.width);
|
|
expect(layout.rotation).toBe(centerDegrees);
|
|
});
|
|
|
|
test('keeps left-side text aligned with the slice instead of flipping it upright', () => {
|
|
const layout = getRadialLabelLayout({
|
|
center: 256,
|
|
radius: 210,
|
|
hubRadius: 46,
|
|
centerDegrees: 180,
|
|
optionCount: 8,
|
|
text: 'left side',
|
|
});
|
|
|
|
expect(layout.rotation).toBe(180);
|
|
});
|
|
|
|
test('keeps short labels on one line at the base size', () => {
|
|
const layout = getRadialLabelLayout({
|
|
center: 256,
|
|
radius: 210,
|
|
hubRadius: 46,
|
|
centerDegrees: 0,
|
|
optionCount: 8,
|
|
text: 'alice',
|
|
});
|
|
|
|
expect(layout.lines).toEqual(['alice']);
|
|
expect(layout.fontSize).toBe(getLabelFontSize(210, 8));
|
|
});
|
|
|
|
test('fits smoke-fixture labels on one line at the minimum supported size', () => {
|
|
const size = 384;
|
|
const radius = size * 0.41;
|
|
const hubRadius = size * 0.09;
|
|
const expectedFontSizes = new Map([
|
|
['alpha', 21],
|
|
['beta', 22],
|
|
['gamma', 20],
|
|
['delta', 21],
|
|
]);
|
|
|
|
for (const [text, expectedFontSize] of expectedFontSizes) {
|
|
const layout = getRadialLabelLayout({
|
|
center: size / 2,
|
|
radius,
|
|
hubRadius,
|
|
centerDegrees: 0,
|
|
optionCount: 4,
|
|
text,
|
|
});
|
|
|
|
expect(layout.lines).toEqual([text]);
|
|
expect(layout.fontSize).toBe(expectedFontSize);
|
|
}
|
|
});
|
|
|
|
test('wraps long phrases into balanced word-boundary lines', () => {
|
|
const lines = getLabelLines('a much longer wheel entry label', 24, 140, 3);
|
|
const widths = lines.map((line) => estimateTextWidth(line, 24));
|
|
|
|
expect(lines).toHaveLength(3);
|
|
expect(lines.join(' ')).toBe('a much longer wheel entry label');
|
|
expect(Math.max(...widths) - Math.min(...widths)).toBeLessThan(80);
|
|
});
|
|
|
|
test('sizes wrapped labels from their longest rendered line', () => {
|
|
const layout = getRadialLabelLayout({
|
|
center: 256,
|
|
radius: 210,
|
|
hubRadius: 46,
|
|
centerDegrees: 0,
|
|
optionCount: 8,
|
|
text: 'a much longer wheel entry label',
|
|
});
|
|
const longestLine = layout.lines.reduce((longest, line) =>
|
|
estimateTextWidth(line, 24) > estimateTextWidth(longest, 24) ? line : longest,
|
|
'');
|
|
|
|
expect(layout.lines.length).toBeGreaterThan(1);
|
|
expect(layout.fontSize).toBe(getLabelFontSize(210, 8, longestLine, layout.contentWidth));
|
|
expect(layout.fontSize).toBeGreaterThan(
|
|
getLabelFontSize(210, 8, 'a much longer wheel entry label', layout.contentWidth),
|
|
);
|
|
expect(layout.height).toBeLessThanOrEqual((2 * Math.PI * getLabelTrack(210, 46).midRadius) / 8);
|
|
});
|
|
|
|
test.each([2, 8, 12, 16])(
|
|
'wraps the Vietnamese readability example when geometry permits at %i options',
|
|
(optionCount) => {
|
|
const layout = get512Layout({optionCount});
|
|
const availableHeight =
|
|
(2 * Math.PI * getLabelTrack(supportedRadius, supportedHubRadius).midRadius) / optionCount;
|
|
|
|
expect(layout.lines.length).toBeGreaterThanOrEqual(2);
|
|
expect(layout.lines.join(' ')).toBe(readableExample);
|
|
expect(layout.fontSize).toBeGreaterThanOrEqual(14);
|
|
expect(layout.height).toBeLessThanOrEqual(availableHeight);
|
|
expect(layout.lines.every((line) => estimateTextWidth(line, layout.fontSize) <= layout.contentWidth)).toBe(true);
|
|
},
|
|
);
|
|
|
|
test.each([
|
|
[9, 'WWWWWWWWWWWWWWWW'],
|
|
[10, 'WWWWWWWWWWWWWW'],
|
|
[11, 'WWWWWWWWWWWWW'],
|
|
[12, 'WWWWWWWWWWWW'],
|
|
[13, 'WWWWWWWWWWW'],
|
|
])('wraps an unbroken token whose one-line fit is %ipx', (oneLineFontSize, text) => {
|
|
const layout = get512Layout({text});
|
|
const track = getLabelTrack(supportedRadius, supportedHubRadius);
|
|
const contentWidth = track.width - Math.max(4, supportedRadius * 0.02) * 2;
|
|
|
|
expect(getLabelFontSize(supportedRadius, 8, text, contentWidth)).toBe(oneLineFontSize);
|
|
expect(layout.lines.length).toBeGreaterThanOrEqual(2);
|
|
expect(layout.lines.join('')).toBe(text);
|
|
expect(layout.fontSize).toBeGreaterThanOrEqual(14);
|
|
expect(layout.lines.every((line) => estimateTextWidth(line, layout.fontSize) <= layout.contentWidth)).toBe(true);
|
|
});
|
|
|
|
test('keeps an exactly 14px one-line fit on one line', () => {
|
|
const text = 'WWWWWWWWWW';
|
|
const layout = get512Layout({text});
|
|
|
|
expect(getLabelFontSize(supportedRadius, 8, text, layout.contentWidth)).toBe(14);
|
|
expect(layout.lines).toEqual([text]);
|
|
expect(layout.fontSize).toBe(14);
|
|
});
|
|
|
|
test.each([24, 32])('keeps the complete Vietnamese example bounded on one line at %i options', (optionCount) => {
|
|
const layout = get512Layout({optionCount});
|
|
const availableHeight =
|
|
(2 * Math.PI * getLabelTrack(supportedRadius, supportedHubRadius).midRadius) / optionCount;
|
|
|
|
expect(layout.lines).toEqual([readableExample]);
|
|
expect(layout.fontSize).toBeLessThan(14);
|
|
expect(layout.height).toBeLessThanOrEqual(availableHeight);
|
|
expect(estimateTextWidth(layout.lines[0] ?? '', layout.fontSize)).toBeLessThanOrEqual(layout.contentWidth);
|
|
});
|
|
|
|
test('keeps supported dense layouts to one bounded line', () => {
|
|
const size = 384;
|
|
const radius = size * 0.41;
|
|
const hubRadius = size * 0.09;
|
|
const optionCount = 32;
|
|
const layout = getRadialLabelLayout({
|
|
center: size / 2,
|
|
radius,
|
|
hubRadius,
|
|
centerDegrees: 0,
|
|
optionCount,
|
|
text: 'a label that reaches the configured limit',
|
|
});
|
|
const availableHeight = (2 * Math.PI * getLabelTrack(radius, hubRadius).midRadius) / optionCount;
|
|
|
|
expect(layout.lines).toEqual(['a label that reaches the configured limit']);
|
|
expect(layout.fontSize).toBe(8);
|
|
expect(layout.height).toBeLessThanOrEqual(availableHeight);
|
|
});
|
|
|
|
test('keeps the border box on the radial track and fits text to its padded content width', () => {
|
|
const radius = 384 * 0.41;
|
|
const hubRadius = 384 * 0.09;
|
|
const layout = getRadialLabelLayout({
|
|
center: 192,
|
|
radius,
|
|
hubRadius,
|
|
centerDegrees: 0,
|
|
optionCount: 4,
|
|
text: 'alpha',
|
|
});
|
|
|
|
const track = getLabelTrack(radius, hubRadius);
|
|
|
|
expect(layout.width).toBe(track.width);
|
|
expect(layout.horizontalPadding).toBe(Math.max(4, radius * 0.02));
|
|
expect(layout.contentWidth).toBe(layout.width - layout.horizontalPadding * 2);
|
|
expect(estimateTextWidth(layout.lines[0] ?? '', layout.fontSize)).toBeLessThanOrEqual(layout.contentWidth);
|
|
});
|
|
|
|
test('normalizes whitespace and splits unbroken text without breaking graphemes', () => {
|
|
expect(getLabelLines(' alpha beta\n gamma ', 24, 90, 3).join(' ')).toBe('alpha beta gamma');
|
|
|
|
const token = 'winner🏆winner🏆winner';
|
|
const lines = getLabelLines(token, 24, 90, 3);
|
|
|
|
expect(lines.length).toBeGreaterThan(1);
|
|
expect(lines.join('')).toBe(token);
|
|
expect(lines.every((line) => {
|
|
const firstCodeUnit = line.charCodeAt(0);
|
|
const lastCodeUnit = line.charCodeAt(line.length - 1);
|
|
|
|
return !(firstCodeUnit >= 0xdc00 && firstCodeUnit <= 0xdfff) && !(lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff);
|
|
})).toBe(true);
|
|
});
|
|
});
|