diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index be32edfb9..d46eccce5 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -22,7 +22,7 @@ import type { MediaContent, } from '../../types'; -import { numberToHexColor } from '../../../util/colors'; +import { int2hex } from '../../../util/colors'; import { pick } from '../../../util/iteratees'; import { toJSNumber } from '../../../util/numbers'; import { addDocumentToLocalDb } from '../helpers/localDb'; @@ -380,10 +380,10 @@ export function buildApiBotInfo(botInfo: GramJs.BotInfo, chatId: string): ApiBot export function buildBotAppSettings(settings: GramJs.BotAppSettings): ApiBotAppSettings { const placeholderPath = settings.placeholderPath && buildSvgPath(settings.placeholderPath); return { - backgroundColor: settings.backgroundColor ? numberToHexColor(settings.backgroundColor) : undefined, - backgroundDarkColor: settings.backgroundDarkColor ? numberToHexColor(settings.backgroundDarkColor) : undefined, - headerColor: settings.headerColor ? numberToHexColor(settings.headerColor) : undefined, - headerDarkColor: settings.headerDarkColor ? numberToHexColor(settings.headerDarkColor) : undefined, + backgroundColor: settings.backgroundColor ? int2hex(settings.backgroundColor) : undefined, + backgroundDarkColor: settings.backgroundDarkColor ? int2hex(settings.backgroundDarkColor) : undefined, + headerColor: settings.headerColor ? int2hex(settings.headerColor) : undefined, + headerDarkColor: settings.headerDarkColor ? int2hex(settings.headerDarkColor) : undefined, placeholderPath, }; } diff --git a/src/api/gramjs/apiBuilders/gifts.ts b/src/api/gramjs/apiBuilders/gifts.ts index 383d848b6..8d48ab823 100644 --- a/src/api/gramjs/apiBuilders/gifts.ts +++ b/src/api/gramjs/apiBuilders/gifts.ts @@ -12,7 +12,7 @@ import type { ApiTypeResaleStarGifts, } from '../../types'; -import { numberToHexColor } from '../../../util/colors'; +import { int2hex } from '../../../util/colors'; import { toJSNumber } from '../../../util/numbers'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { addDocumentToLocalDb } from '../helpers/localDb'; @@ -131,10 +131,10 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut backdropId, name, rarityPercent: rarityPermille / 10, - centerColor: numberToHexColor(centerColor), - edgeColor: numberToHexColor(edgeColor), - patternColor: numberToHexColor(patternColor), - textColor: numberToHexColor(textColor), + centerColor: int2hex(centerColor), + edgeColor: int2hex(edgeColor), + patternColor: int2hex(patternColor), + textColor: int2hex(textColor), }; } diff --git a/src/api/gramjs/apiBuilders/peers.ts b/src/api/gramjs/apiBuilders/peers.ts index cec9bf525..c9a67627b 100644 --- a/src/api/gramjs/apiBuilders/peers.ts +++ b/src/api/gramjs/apiBuilders/peers.ts @@ -11,7 +11,7 @@ import type { } from '../../types'; import { CHANNEL_ID_BASE } from '../../../config'; -import { numberToHexColor } from '../../../util/colors'; +import { int2hex } from '../../../util/colors'; import { buildCollectionByCallback } from '../../../util/iteratees'; type TypePeerOrInput = GramJs.TypePeer | GramJs.TypeInputPeer | GramJs.TypeInputUser | GramJs.TypeInputChannel; @@ -59,14 +59,14 @@ export function buildApiPeerColor(peerColor: GramJs.TypePeerColor): ApiPeerColor } function buildApiPeerColorSet(colorSet: GramJs.help.PeerColorSet) { - return colorSet.colors.map((color) => numberToHexColor(color)); + return colorSet.colors.map((color) => int2hex(color)); } function buildApiPeerProfileColorSet(colorSet: GramJs.help.PeerColorProfileSet): ApiPeerProfileColorSet { return { - paletteColors: colorSet.paletteColors.map((color) => numberToHexColor(color)), - bgColors: colorSet.bgColors.map((color) => numberToHexColor(color)), - storyColors: colorSet.storyColors.map((color) => numberToHexColor(color)), + paletteColors: colorSet.paletteColors.map((color) => int2hex(color)), + bgColors: colorSet.bgColors.map((color) => int2hex(color)), + storyColors: colorSet.storyColors.map((color) => int2hex(color)), }; } @@ -116,10 +116,10 @@ ApiEmojiStatusType | undefined { title: mtpEmojiStatus.title, slug: mtpEmojiStatus.slug, patternDocumentId: mtpEmojiStatus.patternDocumentId.toString(), - centerColor: numberToHexColor(mtpEmojiStatus.centerColor), - edgeColor: numberToHexColor(mtpEmojiStatus.edgeColor), - patternColor: numberToHexColor(mtpEmojiStatus.patternColor), - textColor: numberToHexColor(mtpEmojiStatus.textColor), + centerColor: int2hex(mtpEmojiStatus.centerColor), + edgeColor: int2hex(mtpEmojiStatus.edgeColor), + patternColor: int2hex(mtpEmojiStatus.patternColor), + textColor: int2hex(mtpEmojiStatus.textColor), until: mtpEmojiStatus.until, }; } diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index 6c6a1ea0e..fa2a3974e 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -15,8 +15,8 @@ import { ensureRLottie, getRLottie } from '../../lib/rlottie/RLottie.async'; import { IS_TAURI } from '../../util/browser/globalEnvironment'; import buildClassName from '../../util/buildClassName'; import buildStyle from '../../util/buildStyle'; +import { hex2rgbaObj } from '../../util/colors.ts'; import generateUniqueId from '../../util/generateUniqueId'; -import { hexToRgb } from '../../util/switchTheme'; import useColorFilter from '../../hooks/stickers/useColorFilter'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; @@ -116,7 +116,7 @@ const AnimatedSticker: FC = ({ useSyncEffect(() => { if (color && !shouldUseColorFilter) { - const { r, g, b } = hexToRgb(color); + const { r, g, b } = hex2rgbaObj(color); rgbColor.current = [r, g, b]; } else { rgbColor.current = undefined; diff --git a/src/components/common/profile/RadialPatternBackground.tsx b/src/components/common/profile/RadialPatternBackground.tsx index 0d7444572..9faa4b4ba 100644 --- a/src/components/common/profile/RadialPatternBackground.tsx +++ b/src/components/common/profile/RadialPatternBackground.tsx @@ -6,8 +6,8 @@ import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { getStickerMediaHash } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import buildStyle from '../../../util/buildStyle'; +import { hex2rgba, rgba2hex } from '../../../util/colors.ts'; import { preloadImage } from '../../../util/files'; -import { hexToRgb } from '../../../util/switchTheme.ts'; import { REM } from '../helpers/mediaDimensions'; import useLastCallback from '../../../hooks/useLastCallback'; @@ -201,17 +201,12 @@ export default memo(RadialPatternBackground); function adjustBrightness(hex: string, delta: number) { const factor = 1 + delta; - const rgba = hexToRgb(hex); - const darkenedRgba = [ - Math.min(255, Math.round(rgba.r * factor)), - Math.min(Math.round(rgba.g * factor)), - Math.min(Math.round(rgba.b * factor)), - rgba.a ?? 1, - ] as const; + const [r, g, b, a] = hex2rgba(hex); - return rgbaToHex(...darkenedRgba); -} - -function rgbaToHex(r: number, g: number, b: number, a: number) { - return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}${Math.round(a * 255).toString(16)}`; + return rgba2hex([ + Math.min(255, Math.round(r * factor)), + Math.min(255, Math.round(g * factor)), + Math.min(255, Math.round(b * factor)), + a ?? 255, + ]); } diff --git a/src/components/left/settings/SettingsGeneralBackground.tsx b/src/components/left/settings/SettingsGeneralBackground.tsx index 06ce128f8..b1a41d2ef 100644 --- a/src/components/left/settings/SettingsGeneralBackground.tsx +++ b/src/components/left/settings/SettingsGeneralBackground.tsx @@ -99,10 +99,12 @@ const SettingsGeneralBackground: FC = ({ const currentWallpaper = loadedWallpapers && loadedWallpapers.find((wallpaper) => wallpaper.slug === slug); if (currentWallpaper?.document.thumbnail) { getAverageColor(currentWallpaper.document.thumbnail.dataUri) - .then((color) => { - const patternColor = getPatternColor(color); - const rgbColor = `#${rgb2hex(color)}`; - setThemeSettings({ theme: themeRef.current!, backgroundColor: rgbColor, patternColor }); + .then((averageColor) => { + setThemeSettings({ + theme: themeRef.current!, + backgroundColor: rgb2hex(averageColor), + patternColor: getPatternColor(averageColor), + }); }); } }, [loadedWallpapers, setThemeSettings]); diff --git a/src/components/left/settings/SettingsGeneralBackgroundColor.tsx b/src/components/left/settings/SettingsGeneralBackgroundColor.tsx index b95510cd9..307472fb7 100644 --- a/src/components/left/settings/SettingsGeneralBackgroundColor.tsx +++ b/src/components/left/settings/SettingsGeneralBackgroundColor.tsx @@ -13,7 +13,7 @@ import { selectTheme, selectThemeValues } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { captureEvents } from '../../../util/captureEvents'; import { - getPatternColor, hex2rgb, hsb2rgb, rgb2hex, rgb2hsb, + getPatternColor, hex2rgb, hsv2rgb, rgb2hex, rgb2hsv, } from '../../../util/colors'; import { pick } from '../../../util/iteratees'; @@ -47,7 +47,7 @@ interface CanvasRects { }; } -const DEFAULT_HSB = rgb2hsb(hex2rgb('e6ebee')); +const DEFAULT_HSV = rgb2hsv(hex2rgb('e6ebee')); const PREDEFINED_COLORS = [ '#e6ebee', '#b2cee1', '#008dd0', '#c6e7cb', '#c4e1a6', '#60b16e', '#ccd0af', '#a6a997', '#7a7072', '#fdd7af', '#fdb76e', '#dd8851', @@ -68,12 +68,12 @@ const SettingsGeneralBackground: FC = ({ const huePickerRef = useRef(); const isFirstRunRef = useRef(true); - const [hsb, setHsb] = useState(getInitialHsb(backgroundColor)); + const [hsv, setHsv] = useState(getInitialHsv(backgroundColor)); // Cache for drag handlers - const hsbRef = useRef(hsb); + const hsvRef = useRef(hsv); useEffect(() => { - hsbRef.current = hsb; - }, [hsb]); + hsvRef.current = hsv; + }, [hsv]); const [isDragging, markIsDragging, unmarkIsDragging] = useFlag(); const [rgbInput, setRgbInput] = useState(''); @@ -102,9 +102,9 @@ const SettingsGeneralBackground: FC = ({ Math.min(Math.max(0, e.pageY! - colorRect.top + containerRef.current!.scrollTop), colorRect.height - 1), ]; - const { huePosition } = hsb2positions(hsbRef.current, rectsRef.current!); + const { huePosition } = hsv2positions(hsvRef.current, rectsRef.current!); - setHsb(positions2hsb({ colorPosition, huePosition }, rectsRef.current!)); + setHsv(positions2hsv({ colorPosition, huePosition }, rectsRef.current!)); markIsDragging(); return true; @@ -120,10 +120,10 @@ const SettingsGeneralBackground: FC = ({ }); function handleHueDrag(e: MouseEvent | RealTouchEvent) { - const { colorPosition } = hsb2positions(hsbRef.current, rectsRef.current!); + const { colorPosition } = hsv2positions(hsvRef.current, rectsRef.current!); const huePosition = Math.min(Math.max(0, e.pageX! - hueRect.offsetLeft), hueRect.width - 1); - setHsb(positions2hsb({ colorPosition, huePosition }, rectsRef.current!)); + setHsv(positions2hsv({ colorPosition, huePosition }, rectsRef.current!)); markIsDragging(); return true; @@ -139,15 +139,15 @@ const SettingsGeneralBackground: FC = ({ }); }, [markIsDragging, unmarkIsDragging]); - const { colorPosition = [0, 0], huePosition = 0 } = rectsRef.current ? hsb2positions(hsb, rectsRef.current) : {}; - const hex = rgb2hex(hsb2rgb(hsb)); - const hue = hsb[0]; - const hueHex = rgb2hex(hsb2rgb([hue, 1, 1])); + const { colorPosition = [0, 0], huePosition = 0 } = rectsRef.current ? hsv2positions(hsv, rectsRef.current) : {}; + const hex = rgb2hex(hsv2rgb(hsv)); + const hue = hsv[0]; + const hueHex = rgb2hex(hsv2rgb([hue, 1, 1])); // Save value and update inputs when HSL changes useEffect(() => { - const rgb = hsb2rgb(hsb); - const color = `#${rgb2hex(rgb)}`; + const rgb = hsv2rgb(hsv); + const color = rgb2hex(rgb); setRgbInput(rgb.join(', ')); setHexInput(color); @@ -162,7 +162,7 @@ const SettingsGeneralBackground: FC = ({ }); } isFirstRunRef.current = false; - }, [hsb, setThemeSettings]); + }, [hsv, setThemeSettings]); // Redraw color picker when hue changes useEffect(() => { @@ -179,7 +179,7 @@ const SettingsGeneralBackground: FC = ({ if (rgbValue.match(/^\d{1,3},\s?\d{1,3},\s?\d{1,3}$/)) { const rgb = rgbValue.split(',').map((channel) => Number(channel.trim())) as [number, number, number]; - setHsb(rgb2hsb(rgb)); + setHsv(rgb2hsv(rgb)); } e.currentTarget.value = rgbValue; @@ -189,14 +189,14 @@ const SettingsGeneralBackground: FC = ({ const hexValue = e.currentTarget.value.replace(/[^0-9a-fA-F]/g, '').slice(0, 6); if (hexValue.match(/^#?[0-9a-fA-F]{6}$/)) { - setHsb(rgb2hsb(hex2rgb(hexValue.replace('#', '')))); + setHsv(rgb2hsv(hex2rgb(hexValue))); } e.currentTarget.value = hexValue; }, []); const handlePredefinedColorClick = useCallback((e: React.MouseEvent) => { - setHsb(rgb2hsb(hex2rgb(e.currentTarget.dataset.color!.replace('#', '')))); + setHsv(rgb2hsv(hex2rgb(e.currentTarget.dataset.color!))); }, []); const className = buildClassName( @@ -216,14 +216,14 @@ const SettingsGeneralBackground: FC = ({
@@ -234,7 +234,7 @@ const SettingsGeneralBackground: FC = ({
{PREDEFINED_COLORS.map((color) => (
= ({ ); }; -function getInitialHsb(backgroundColor?: string) { - return backgroundColor && backgroundColor.startsWith('#') - ? rgb2hsb(hex2rgb(backgroundColor.replace('#', ''))) - : DEFAULT_HSB; +function getInitialHsv(backgroundColor?: string) { + return backgroundColor?.startsWith('#') + ? rgb2hsv(hex2rgb(backgroundColor)) + : DEFAULT_HSV; } -function hsb2positions(hsb: [number, number, number], rects: CanvasRects) { +function hsv2positions(hsv: [number, number, number], rects: CanvasRects) { return { colorPosition: [ - Math.round((hsb[1]) * (rects.colorRect.width - 1)), - Math.round((1 - hsb[2]) * (rects.colorRect.height - 1)), + Math.round((hsv[1]) * (rects.colorRect.width - 1)), + Math.round((1 - hsv[2]) * (rects.colorRect.height - 1)), ], - huePosition: Math.round(hsb[0] * (rects.hueRect.width - 1)), + huePosition: Math.round(hsv[0] * (rects.hueRect.width - 1)), }; } -function positions2hsb( +function positions2hsv( { colorPosition, huePosition }: { colorPosition: number[]; huePosition: number }, rects: CanvasRects, ): [number, number, number] { @@ -300,7 +300,7 @@ function drawColor( const imgData = ctx!.createImageData(w, h); const pixels = imgData.data; - const col = hsb2rgb([hue, 1, 1]); + const col = hsv2rgb([hue, 1, 1]); let index = 0; @@ -334,7 +334,7 @@ function drawHue(canvas: HTMLCanvasElement) { for (let x = 0; x < w; x++) { const hue = x / (w - 1); - const rgb = hsb2rgb([hue, 1, 1]); + const rgb = hsv2rgb([hue, 1, 1]); pixels[index++] = rgb[0]; pixels[index++] = rgb[1]; diff --git a/src/components/main/premium/PremiumFeatureItem.tsx b/src/components/main/premium/PremiumFeatureItem.tsx index 0115c321d..7b7f20b82 100644 --- a/src/components/main/premium/PremiumFeatureItem.tsx +++ b/src/components/main/premium/PremiumFeatureItem.tsx @@ -2,7 +2,7 @@ import { memo } from '../../../lib/teact/teact'; import type { IconName } from '../../../types/icons'; -import { hexToRgb, lerpRgb } from '../../../util/switchTheme'; +import { hex2rgbaObj, lerpRgbaObj } from '../../../util/colors.ts'; import renderText from '../../common/helpers/renderText'; import useLastCallback from '../../../hooks/useLastCallback'; @@ -30,7 +30,7 @@ type OwnProps = { const COLORS = [ '#F2862D', '#EB7B4D', '#E46D72', '#DD6091', '#CC5FBA', '#B464E7', '#9873FF', '#768DFF', '#55A5FC', '#52B0C9', '#4FBC93', '#4CC663', -].map(hexToRgb); +].map(hex2rgbaObj); const PremiumFeatureItem = ({ icon, @@ -45,7 +45,7 @@ const PremiumFeatureItem = ({ const newIndex = (index / count) * COLORS.length; const colorA = COLORS[Math.floor(newIndex)]; const colorB = COLORS[Math.ceil(newIndex)] ?? colorA; - const { r, g, b } = lerpRgb(colorA, colorB, 0.5); + const { r, g, b } = lerpRgbaObj(colorA, colorB, 0.5); const handleClick = useLastCallback(() => { onClick?.(section); diff --git a/src/components/middle/composer/hooks/useInputCustomEmojis.ts b/src/components/middle/composer/hooks/useInputCustomEmojis.ts index de4987140..b8f905270 100644 --- a/src/components/middle/composer/hooks/useInputCustomEmojis.ts +++ b/src/components/middle/composer/hooks/useInputCustomEmojis.ts @@ -12,12 +12,12 @@ import { requestMeasure } from '../../../../lib/fasterdom/fasterdom'; import { ensureRLottie } from '../../../../lib/rlottie/RLottie.async'; import { selectCustomEmoji, selectIsAlwaysHighPriorityEmoji } from '../../../../global/selectors'; import AbsoluteVideo from '../../../../util/AbsoluteVideo'; +import { hex2rgbaObj } from '../../../../util/colors.ts'; import { addCustomEmojiInputRenderCallback, getCustomEmojiMediaDataForInput, } from '../../../../util/emoji/customEmojiManager'; import { round } from '../../../../util/math'; -import { hexToRgb } from '../../../../util/switchTheme'; import { REM } from '../../../common/helpers/mediaDimensions'; import useColorFilter from '../../../../hooks/stickers/useColorFilter'; @@ -230,7 +230,7 @@ async function createPlayer({ colorFilter?: string; }): Promise { if (customEmoji.isLottie) { - const color = customEmoji.shouldUseTextColor && textColor ? hexToRgb(textColor) : undefined; + const color = customEmoji.shouldUseTextColor && textColor ? hex2rgbaObj(textColor) : undefined; const RLottie = await ensureRLottie(); const lottie = RLottie.init( mediaUrl, diff --git a/src/components/modals/webApp/WebAppModal.tsx b/src/components/modals/webApp/WebAppModal.tsx index c907a8193..9ccbd8129 100644 --- a/src/components/modals/webApp/WebAppModal.tsx +++ b/src/components/modals/webApp/WebAppModal.tsx @@ -21,8 +21,7 @@ import { import { selectSharedSettings } from '../../../global/selectors/sharedState'; import buildClassName from '../../../util/buildClassName'; import buildStyle from '../../../util/buildStyle'; -import { getColorLuma } from '../../../util/colors'; -import { hexToRgb } from '../../../util/switchTheme'; +import { getColorLuma, hex2rgbaObj } from '../../../util/colors'; import windowSize from '../../../util/windowSize'; import useInterval from '../../../hooks/schedulers/useInterval'; @@ -427,7 +426,7 @@ const WebAppModal: FC = ({ const headerTextVar = useMemo(() => { if (isMoreAppsTabActive) return 'color-text'; if (!headerColor) return undefined; - const { r, g, b } = hexToRgb(headerColor); + const { r, g, b } = hex2rgbaObj(headerColor); const luma = getColorLuma([r, g, b]); const adaptedLuma = theme === 'dark' ? 255 - luma : luma; return adaptedLuma > LUMA_THRESHOLD ? 'color-text' : 'color-background'; diff --git a/src/components/story/mediaArea/MediaAreaWeather.tsx b/src/components/story/mediaArea/MediaAreaWeather.tsx index 005ae1a12..d145d7340 100644 --- a/src/components/story/mediaArea/MediaAreaWeather.tsx +++ b/src/components/story/mediaArea/MediaAreaWeather.tsx @@ -12,7 +12,7 @@ import { requestForcedReflow, requestMutation } from '../../../lib/fasterdom/fas import { selectRestrictedEmoji } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import buildStyle from '../../../util/buildStyle'; -import { convertToRGBA, getTextColor } from '../../../util/colors'; +import { getTextColor, int2cssRgba } from '../../../util/colors'; import { formatTemperature } from '../../../util/formatTemperature'; import useLastCallback from '../../../hooks/useLastCallback'; @@ -48,7 +48,7 @@ const MediaAreaWeather: FC = ({ const { temperatureC, color } = mediaArea; - const backgroundColor = convertToRGBA(color); + const backgroundColor = int2cssRgba(color); const textColor = getTextColor(color); const updateCustomSize = useLastCallback((isImmediate?: boolean) => { diff --git a/src/hooks/stickers/useColorFilter.tsx b/src/hooks/stickers/useColorFilter.tsx index 808ae6a0f..a8782ec3c 100644 --- a/src/hooks/stickers/useColorFilter.tsx +++ b/src/hooks/stickers/useColorFilter.tsx @@ -1,8 +1,8 @@ import { useEffect } from '../../lib/teact/teact'; import { SVG_NAMESPACE } from '../../config'; +import { hex2rgbaObj } from '../../util/colors.ts'; import { addSvgDefinition, removeSvgDefinition } from '../../util/svgController'; -import { hexToRgb } from '../../util/switchTheme'; const SVG_MAP = new Map(); @@ -14,7 +14,7 @@ class SvgColorFilter { constructor(public color: string) { this.filterId = `color-filter-${color.slice(1)}`; - const rgbColor = hexToRgb(color); + const rgbColor = hex2rgbaObj(color); addSvgDefinition( RGB * input: 'xxxxxx' (ex. 'ed15fa') case-insensitive * output: [r, g, b] ([0-255, 0-255, 0-255]) */ -export function hex2rgb(param: string): [number, number, number] { +export function hex2rgb(hex: string): Number3 { + const cleanHex = clearHex(hex); + return [ - parseInt(param.substring(0, 2), 16), - parseInt(param.substring(2, 4), 16), - parseInt(param.substring(4, 6), 16), + parseInt(cleanHex.substring(0, 2), 16), + parseInt(cleanHex.substring(2, 4), 16), + parseInt(cleanHex.substring(4, 6), 16), ]; } -/** - * RGB > HEX - * input: [r, g, b] ([0-255, 0-255, 0-255]) - * output: 'xxxxxx' (ex. 'ff0000') - */ -export function rgb2hex(param: [number, number, number]) { - const p0 = param[0].toString(16); - const p1 = param[1].toString(16); - const p2 = param[2].toString(16); - return (p0.length === 1 ? '0' + p0 : p0) + (p1.length === 1 ? '0' + p1 : p1) + (p2.length === 1 ? '0' + p2 : p2); +export function hex2rgba(hex: string): Number4 { + const cleanHex = clearHex(hex); + + return [ + ...hex2rgb(cleanHex), + cleanHex.length === 8 ? parseInt(cleanHex.substring(6, 8), 16) : 255, + ]; +} + +export function hex2rgbaObj(hex: string): Rgba { + const [r, g, b, a] = hex2rgba(hex); + + return { r, g, b, a }; +} + +export function rgb2hex([r, g, b]: Number3) { + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; +} + +export function rgba2hex([r, g, b, a]: Number4) { + return `${rgb2hex([r, g, b])}${a.toString(16)}`; } /** @@ -40,16 +65,16 @@ export function rgb2hex(param: [number, number, number]) { * @param Number b The blue color value * @return Array The HSV representation */ -export function rgb2hsb([r, g, b]: [number, number, number]): [number, number, number] { +export function rgb2hsv([r, g, b]: Number3): Number3 { r /= 255; g /= 255; b /= 255; - let max = Math.max(r, g, b), min = Math.min(r, g, b); - let h!: number, s: number, v: number = max; - - let d = max - min; - s = max === 0 ? 0 : d / max; + const max = Math.max(r, g, b), min = Math.min(r, g, b); + const d = max - min; + let h!: number; + const s: number = max === 0 ? 0 : d / max; + const v: number = max; if (max === min) { h = 0; // achromatic @@ -72,6 +97,10 @@ export function rgb2hsb([r, g, b]: [number, number, number]): [number, number, n return [h, s, v]; } +export function rgba2hsva([r, g, b, a]: Number4): Number4 { + return [...rgb2hsv([r, g, b]), a]; +} + /** * Converts an HSV color value to RGB. Conversion formula * adapted from http://en.wikipedia.org/wiki/HSV_color_space. @@ -83,14 +112,14 @@ export function rgb2hsb([r, g, b]: [number, number, number]): [number, number, n * @param Number v The value * @return Array The RGB representation */ -export function hsb2rgb([h, s, v]: [number, number, number]): [number, number, number] { +export function hsv2rgb([h, s, v]: Number3): Number3 { let r!: number, g!: number, b!: number; - let i = Math.floor(h * 6); - let f = h * 6 - i; - let p = v * (1 - s); - let q = v * (1 - f * s); - let t = v * (1 - (1 - f) * s); + const i = Math.floor(h * 6); + const f = h * 6 - i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); switch (i % 6) { case 0: @@ -132,27 +161,28 @@ export function hsb2rgb([h, s, v]: [number, number, number]): [number, number, n ]; } -export async function getAverageColor(url: string): Promise<[number, number, number]> { +export function hsva2rgba([h, s, v, a]: Number4): Number4 { + return [...hsv2rgb([h, s, v]), a]; +} + +export async function getAverageColor(url: string): Promise { // Only visit every 5 pixels const blockSize = 5; - const defaultRGB: [number, number, number] = [0, 0, 0]; + const black: Number3 = [0, 0, 0]; let data; - let width; - let height; let i = -4; - let length; - let rgb: [number, number, number] = [0, 0, 0]; + const rgb: Number3 = [0, 0, 0]; let count = 0; const canvas = document.createElement('canvas'); const context = canvas.getContext && canvas.getContext('2d'); if (!context) { - return defaultRGB; + return black; } const image = await preloadImage(url); - height = image.naturalHeight || image.offsetHeight || image.height; - width = image.naturalWidth || image.offsetWidth || image.width; + const height = image.naturalHeight || image.offsetHeight || image.height; + const width = image.naturalWidth || image.offsetWidth || image.width; canvas.height = height; canvas.width = width; @@ -161,10 +191,10 @@ export async function getAverageColor(url: string): Promise<[number, number, num try { data = context.getImageData(0, 0, width, height); } catch (e) { - return defaultRGB; + return black; } - length = data.data.length; + const length = data.data.length; while ((i += blockSize * 4) < length) { if (data.data[i + 3] === 0) continue; // Ignore fully transparent pixels @@ -181,50 +211,64 @@ export async function getAverageColor(url: string): Promise<[number, number, num return rgb; } -export function getColorLuma(rgbColor: [number, number, number]) { - const [r, g, b] = rgbColor; - const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; - return luma; +export function getColorLuma([r, g, b]: Number3) { + return 0.2126 * r + 0.7152 * g + 0.0722 * b; } + // https://stackoverflow.com/a/64090995 -export function hsl2rgb([h, s, l]: [number, number, number]): [number, number, number] { - let a = s * Math.min(l, 1 - l); - let f = (n: number, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); +export function hsl2rgb([h, s, l]: Number3): Number3 { + const a = s * Math.min(l, 1 - l); + const f = (n: number, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return [f(0), f(8), f(4)]; } // Function was adapted from https://github.com/telegramdesktop/tdesktop/blob/35ff621b5b52f7e3553fb0f990ea13ade7101b8e/Telegram/SourceFiles/data/data_wall_paper.cpp#L518 -export function getPatternColor(rgbColor: [number, number, number]) { - let [hue, saturation, value] = rgb2hsb(rgbColor); +export function getPatternColor(rgb: Number3) { + const hsv = rgb2hsv(rgb); + const [h] = hsv; + let [, s, v] = hsv; - saturation = Math.min(1, saturation + 0.05 + 0.1 * (1 - saturation)); - value = value > 0.5 - ? Math.max(0, value * 0.65) - : Math.max(0, Math.min(1, 1 - value * 0.65)); + s = Math.min(1, s + 0.05 + 0.1 * (1 - s)); + v = v > 0.5 + ? Math.max(0, v * 0.65) + : Math.max(0, Math.min(1, 1 - v * 0.65)); - const rgb = hsl2rgb([hue * 360, saturation, value]); - const hex = rgb2hex(rgb.map((c) => Math.floor(c * 255)) as [number, number, number]); - return `#${hex}66`; + const newRgb = hsl2rgb([h * 360, s, v]); + const mappedRgb = newRgb.map((c) => Math.floor(c * 255)) as Number3; + + return rgba2hex([...mappedRgb, 102]); } -export const convertToRGBA = (color: number): string => { +export function int2cssRgba(color: number): string { const alpha = (color >> 24) & 0xff; const red = (color >> 16) & 0xff; const green = (color >> 8) & 0xff; const blue = color & 0xff; - const alphaFloat = alpha / 255; + return `rgba(${red}, ${green}, ${blue}, ${alphaFloat})`; -}; +} -export const numberToHexColor = (color: number): string => { +export function int2hex(color: number): string { return `#${color.toString(16).padStart(6, '0')}`; -}; +} -export const getTextColor = (color: number): string => { +export function getTextColor(color: number): string { const r = (color >> 16) & 0xff; const g = (color >> 8) & 0xff; const b = color & 0xff; const luma = getColorLuma([r, g, b]); return luma > LUMA_THRESHOLD ? 'black' : 'white'; -}; +} + +export function lerpRgbaObj(start: Rgba, end: Rgba, interpolationRatio: number): Rgba { + const r = Math.round(lerp(start.r, end.r, interpolationRatio)); + const g = Math.round(lerp(start.g, end.g, interpolationRatio)); + const b = Math.round(lerp(start.b, end.b, interpolationRatio)); + const a = start.a !== undefined + ? Math.round(lerp(start.a, end.a!, interpolationRatio)) + : undefined; + + return { r, g, b, a }; +} diff --git a/src/util/switchTheme.ts b/src/util/switchTheme.ts index c0caffe44..a40bdc961 100644 --- a/src/util/switchTheme.ts +++ b/src/util/switchTheme.ts @@ -3,19 +3,11 @@ import type { ThemeKey } from '../types'; import { requestMutation } from '../lib/fasterdom/fasterdom'; import themeColors from '../styles/themes.json'; import { animate } from './animation'; -import { lerp } from './math'; - -type RGBAColor = { - r: number; - g: number; - b: number; - a?: number; -}; +import { hex2rgbaObj, lerpRgbaObj } from './colors.ts'; let isInitialized = false; const DECIMAL_PLACES = 3; -const HEX_COLOR_REGEX = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i; const DURATION_MS = 200; const ENABLE_ANIMATION_DELAY_MS = 500; const RGB_VARIABLES = new Set([ @@ -34,7 +26,7 @@ const DISABLE_ANIMATION_CSS = ` const colors = (Object.keys(themeColors) as Array).map((property) => ({ property, - colors: [hexToRgb(themeColors[property][0]), hexToRgb(themeColors[property][1])], + colors: [hex2rgbaObj(themeColors[property][0]), hex2rgbaObj(themeColors[property][1])], })); const injectCss = (css: string) => { @@ -97,35 +89,11 @@ function transition(t: number) { return 1 - ((1 - t) ** 3.5); } -export function hexToRgb(hex: string): RGBAColor { - const result = HEX_COLOR_REGEX.exec(hex)!; - - return { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16), - a: result[4] !== undefined ? parseInt(result[4], 16) : undefined, - }; -} - -export function lerpRgb(start: RGBAColor, end: RGBAColor, interpolationRatio: number): RGBAColor { - const r = Math.round(lerp(start.r, end.r, interpolationRatio)); - const g = Math.round(lerp(start.g, end.g, interpolationRatio)); - const b = Math.round(lerp(start.b, end.b, interpolationRatio)); - const a = start.a !== undefined - ? Math.round(lerp(start.a, end.a!, interpolationRatio)) - : undefined; - - return { - r, g, b, a, - }; -} - function applyColorAnimationStep(startIndex: number, endIndex: number, interpolationRatio: number = 1) { colors.forEach(({ property, colors: propertyColors }) => { const { r, g, b, a, - } = lerpRgb(propertyColors[startIndex], propertyColors[endIndex], interpolationRatio); + } = lerpRgbaObj(propertyColors[startIndex], propertyColors[endIndex], interpolationRatio); const roundedA = a !== undefined ? Math.round((a / 255) * 10 ** DECIMAL_PLACES) / 10 ** DECIMAL_PLACES : undefined;