From 8de459b1319fef77b1c3bec67dc86745f8f16163 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Wed, 22 Jul 2026 14:34:01 +0700 Subject: [PATCH] fix(renderer): use consistent wheel label colors --- renderer/src/remotion/WheelComposition.jsx | 5 +-- renderer/src/remotion/themes.js | 4 +-- renderer/src/remotion/wheel-label-layout.js | 22 ------------- renderer/test/wheel-label-layout.test.js | 6 ---- renderer/test/wheel-themes.test.js | 34 +++++++++++++++++++++ 5 files changed, 39 insertions(+), 32 deletions(-) create mode 100644 renderer/test/wheel-themes.test.js diff --git a/renderer/src/remotion/WheelComposition.jsx b/renderer/src/remotion/WheelComposition.jsx index f7ef090..58fd3fa 100644 --- a/renderer/src/remotion/WheelComposition.jsx +++ b/renderer/src/remotion/WheelComposition.jsx @@ -9,7 +9,7 @@ import { getWheelRotationDegrees, rightPointerClipPath, } from './wheel-layout.js'; -import {getContrastingTextColor, getRadialLabelLayout} from './wheel-label-layout.js'; +import {getRadialLabelLayout} from './wheel-label-layout.js'; import {getTheme} from './themes.js'; const baseFont = @@ -124,7 +124,7 @@ export const WheelComposition = (props) => { style={{ alignItems: 'center', boxSizing: 'border-box', - color: getContrastingTextColor(color), + color: theme.text, display: 'flex', fontSize: label.fontSize, flexDirection: 'column', @@ -138,6 +138,7 @@ export const WheelComposition = (props) => { padding: `0 ${label.horizontalPadding}px`, position: 'absolute', textAlign: 'center', + textShadow: `0 1px 0 ${theme.textHalo}, 0 -1px 0 ${theme.textHalo}, 1px 0 0 ${theme.textHalo}, -1px 0 0 ${theme.textHalo}`, top: label.y, transform: `translate(-50%, -50%) rotate(${label.rotation}deg)`, transformOrigin: '50% 50%', diff --git a/renderer/src/remotion/themes.js b/renderer/src/remotion/themes.js index 3a185cb..b348b6b 100644 --- a/renderer/src/remotion/themes.js +++ b/renderer/src/remotion/themes.js @@ -21,7 +21,7 @@ export const themes = { text: '#1f2937', textHalo: 'rgba(255, 251, 235, 0.92)', winnerGlow: 'rgba(251, 146, 60, 0.5)', - slices: ['#f43f5e', '#f59e0b', '#84cc16', '#06b6d4', '#8b5cf6', '#ec4899', '#22c55e', '#fb923c'], + slices: ['#fb7185', '#f59e0b', '#84cc16', '#06b6d4', '#a78bfa', '#f472b6', '#22c55e', '#fb923c'], }, mono: { background: '#f4f4f5', @@ -33,7 +33,7 @@ export const themes = { text: '#18181b', textHalo: 'rgba(250, 250, 250, 0.9)', winnerGlow: 'rgba(161, 161, 170, 0.45)', - slices: ['#e4e4e7', '#a1a1aa', '#d4d4d8', '#71717a', '#f4f4f5', '#52525b'], + slices: ['#e4e4e7', '#a1a1aa', '#d4d4d8', '#b8b8bf', '#f4f4f5', '#c4c4ca'], }, }; diff --git a/renderer/src/remotion/wheel-label-layout.js b/renderer/src/remotion/wheel-label-layout.js index 6de9797..dd7c0c4 100644 --- a/renderer/src/remotion/wheel-label-layout.js +++ b/renderer/src/remotion/wheel-label-layout.js @@ -43,7 +43,6 @@ export const estimateTextWidth = (text, fontSize) => { return (widthUnits + 2) * fontSize; }; - /** * @param {string[]} units * @param {number} lineCount @@ -211,24 +210,3 @@ export const getRadialLabelLayout = ({center, radius, hubRadius, centerDegrees, lines, }; }; - -/** - * @param {string} color - * @returns {'#111827' | '#ffffff'} - */ -export const getContrastingTextColor = (color) => { - const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/iu.exec(color); - if (!match) { - return '#111827'; - } - - const redHex = match[1] ?? '00'; - const greenHex = match[2] ?? '00'; - const blueHex = match[3] ?? '00'; - const red = Number.parseInt(redHex, 16) / 255; - const green = Number.parseInt(greenHex, 16) / 255; - const blue = Number.parseInt(blueHex, 16) / 255; - const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; - - return luminance > 0.56 ? '#111827' : '#ffffff'; -}; diff --git a/renderer/test/wheel-label-layout.test.js b/renderer/test/wheel-label-layout.test.js index 45da613..00630fc 100644 --- a/renderer/test/wheel-label-layout.test.js +++ b/renderer/test/wheel-label-layout.test.js @@ -1,7 +1,6 @@ import {describe, expect, test} from 'vitest'; import { estimateTextWidth, - getContrastingTextColor, getLabelFontSize, getLabelLines, getLabelTrack, @@ -242,9 +241,4 @@ describe('wheel label layout', () => { return !(firstCodeUnit >= 0xdc00 && firstCodeUnit <= 0xdfff) && !(lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff); })).toBe(true); }); - - test('uses contrast text colors for light and dark slices', () => { - expect(getContrastingTextColor('#facc15')).toBe('#111827'); - expect(getContrastingTextColor('#7f1d1d')).toBe('#ffffff'); - }); }); diff --git a/renderer/test/wheel-themes.test.js b/renderer/test/wheel-themes.test.js new file mode 100644 index 0000000..4c7c0e0 --- /dev/null +++ b/renderer/test/wheel-themes.test.js @@ -0,0 +1,34 @@ +import {describe, expect, test} from 'vitest'; +import {themes} from '../src/remotion/themes.js'; + +/** + * @param {string} color + */ +const getRelativeLuminance = (color) => { + const channelHexValues = color.slice(1).match(/.{2}/gu) ?? []; + const channels = channelHexValues + .map((channel) => Number.parseInt(channel, 16) / 255) + .map((channel) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4)); + const [red = 0, green = 0, blue = 0] = channels; + + return 0.2126 * red + 0.7152 * green + 0.0722 * blue; +}; + +/** + * @param {string} foreground + * @param {string} background + */ +const getContrastRatio = (foreground, background) => { + const luminances = [getRelativeLuminance(foreground), getRelativeLuminance(background)].sort( + (left, right) => right - left, + ); + const [lighter = 0, darker = 0] = luminances; + + return (lighter + 0.05) / (darker + 0.05); +}; + +describe('wheel themes', () => { + test.each(Object.entries(themes))('%s uses one readable label color across every slice', (_name, theme) => { + expect(theme.slices.every((slice) => getContrastRatio(theme.text, slice) >= 4.5)).toBe(true); + }); +});