From 1edd1db4f69da69324ecc95f87b10e70ba5f3978 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Sat, 6 Feb 2021 19:18:53 +0200 Subject: [PATCH] Pick average color of chat background image --- src/components/sidebarLeft/tabs/background.ts | 62 ++++++++++++++----- src/helpers/averageColor.ts | 41 ++++++++++++ src/helpers/color.ts | 31 ++++++++++ src/hooks/useHeavyAnimationCheck.ts | 11 ++-- src/lib/appManagers/appImManager.ts | 8 ++- src/lib/appManagers/appStateManager.ts | 1 + src/scss/partials/_chatBubble.scss | 9 ++- src/scss/style.scss | 1 + 8 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 src/helpers/averageColor.ts create mode 100644 src/helpers/color.ts diff --git a/src/components/sidebarLeft/tabs/background.ts b/src/components/sidebarLeft/tabs/background.ts index 48b0ca20..1b00cfff 100644 --- a/src/components/sidebarLeft/tabs/background.ts +++ b/src/components/sidebarLeft/tabs/background.ts @@ -1,6 +1,8 @@ import { generateSection } from ".."; +import { averageColor } from "../../../helpers/averageColor"; import blur from "../../../helpers/blur"; import { deferredPromise } from "../../../helpers/cancellablePromise"; +import { rgbToHsl } from "../../../helpers/color"; import { attachClickEvent, findUpClassName } from "../../../helpers/dom"; import { AccountWallPapers, WallPaper } from "../../../layer"; import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager"; @@ -51,16 +53,28 @@ export default class AppBackgroundTab extends SliderSuperTab { const grid = document.createElement('div'); grid.classList.add('grid'); - const saveToCache = (url: string) => { + const saveToCache = (slug: string, url: string) => { fetch(url).then(response => { - appDownloadManager.cacheStorage.save('background-image', response); + appDownloadManager.cacheStorage.save('backgrounds/' + slug, response); }); }; + // * https://github.com/TelegramMessenger/Telegram-iOS/blob/3d062fff78cc6b287c74e6171f855a3500c0156d/submodules/TelegramPresentationData/Sources/PresentationData.swift#L453 + const highlightningColor = (pixel: Uint8ClampedArray) => { + let {h, s, l} = rgbToHsl(pixel[0], pixel[1], pixel[2]); + if(s > 0.0) { + s = Math.min(1.0, s + 0.05 + 0.1 * (1.0 - s)); + } + l = Math.max(0.0, l * 0.65); + + const hsla = `hsla(${h * 360}, ${s * 100}%, ${l * 100}%, .4)`; + return hsla; + }; + + let tempId = 0; const setBackgroundDocument = (slug: string, doc: MyDocument) => { - rootScope.settings.background.slug = slug; - rootScope.settings.background.type = 'image'; - appStateManager.pushToState('settings', rootScope.settings); + let _tempId = ++tempId; + const middleware = () => _tempId === tempId; const download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0); @@ -69,26 +83,44 @@ export default class AppBackgroundTab extends SliderSuperTab { deferred.cancel = download.cancel; download.then(() => { - if(rootScope.settings.background.slug !== slug || rootScope.settings.background.type !== 'image') { + if(!middleware()) { return; } + const onReady = (url: string) => { + //const perf = performance.now(); + averageColor(url).then(pixel => { + if(!middleware()) { + return; + } + + const hsla = highlightningColor(pixel); + //console.log(doc, hsla, performance.now() - perf); + + rootScope.settings.background.slug = slug; + rootScope.settings.background.type = 'image'; + rootScope.settings.background.highlightningColor = hsla; + document.documentElement.style.setProperty('--message-highlightning-color', rootScope.settings.background.highlightningColor); + appStateManager.pushToState('settings', rootScope.settings); + + saveToCache(slug, url); + appImManager.setBackground(url).then(deferred.resolve); + }); + }; + if(rootScope.settings.background.blur) { setTimeout(() => { blur(doc.url, 12, 4) .then(url => { - if(rootScope.settings.background.slug !== slug || rootScope.settings.background.type !== 'image') { + if(!middleware()) { return; } - saveToCache(url); - return appImManager.setBackground(url); - }) - .then(deferred.resolve); + onReady(url); + }); }, 200); } else { - saveToCache(doc.url); - appImManager.setBackground(doc.url).then(deferred.resolve); + onReady(doc.url); } }); @@ -183,10 +215,10 @@ export default class AppBackgroundTab extends SliderSuperTab { load(); - console.log(doc); + //console.log(doc); }); - console.log(accountWallpapers); + //console.log(accountWallpapers); }); this.scrollable.append(grid); diff --git a/src/helpers/averageColor.ts b/src/helpers/averageColor.ts new file mode 100644 index 00000000..c056d870 --- /dev/null +++ b/src/helpers/averageColor.ts @@ -0,0 +1,41 @@ +import { renderImageFromUrl } from "../components/misc"; + +export const averageColor = (imageUrl: string): Promise => { + const img = document.createElement('img'); + return new Promise((resolve) => { + renderImageFromUrl(img, imageUrl, () => { + const canvas = document.createElement('canvas'); + const ratio = img.naturalWidth / img.naturalHeight; + const DIMENSIONS = 50; + if(ratio === 1) { + canvas.width = DIMENSIONS; + canvas.height = canvas.width / ratio; + } else if(ratio > 1) { + canvas.height = DIMENSIONS; + canvas.width = canvas.height / ratio; + } else { + canvas.width = canvas.height = DIMENSIONS; + } + + const context = canvas.getContext('2d'); + context.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height); + + const pixel = new Array(4).fill(0); + const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data; + for(let i = 0; i < pixels.length; i += 4) { + pixel[0] += pixels[i]; + pixel[1] += pixels[i + 1]; + pixel[2] += pixels[i + 2]; + pixel[3] += pixels[i + 3]; + } + + const pixelsLength = pixels.length / 4; + const outPixel = new Uint8ClampedArray(4); + outPixel[0] = pixel[0] / pixelsLength; + outPixel[1] = pixel[1] / pixelsLength; + outPixel[2] = pixel[2] / pixelsLength; + outPixel[3] = pixel[3] / pixelsLength; + resolve(outPixel); + }); + }); +}; \ No newline at end of file diff --git a/src/helpers/color.ts b/src/helpers/color.ts new file mode 100644 index 00000000..1962a022 --- /dev/null +++ b/src/helpers/color.ts @@ -0,0 +1,31 @@ +export function rgbToHsl(r: number, g: number, b: number) { + r /= 255, g /= 255, b /= 255; + let max = Math.max(r, g, b), + min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + + if(max === min) { + h = s = 0; // achromatic + } else { + let d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return ({ + h: h, + s: s, + l: l, + }); +} \ No newline at end of file diff --git a/src/hooks/useHeavyAnimationCheck.ts b/src/hooks/useHeavyAnimationCheck.ts index b9bfcf4c..9fae92e9 100644 --- a/src/hooks/useHeavyAnimationCheck.ts +++ b/src/hooks/useHeavyAnimationCheck.ts @@ -6,6 +6,7 @@ import ListenerSetter from '../helpers/listenerSetter'; import { CancellablePromise, deferredPromise } from '../helpers/cancellablePromise'; import { pause } from '../helpers/schedulers'; import rootScope from '../lib/rootScope'; +import { DEBUG } from '../lib/mtproto/mtproto_config'; const ANIMATION_START_EVENT = 'event-heavy-animation-start'; const ANIMATION_END_EVENT = 'event-heavy-animation-end'; @@ -14,16 +15,18 @@ let isAnimating = false; let heavyAnimationPromise: CancellablePromise = Promise.resolve(); let promisesInQueue = 0; +const log = console.log.bind(console.log, '[HEAVY-ANIMATION]:'); + export const dispatchHeavyAnimationEvent = (promise: Promise, timeout?: number) => { if(!isAnimating) { heavyAnimationPromise = deferredPromise(); rootScope.broadcast(ANIMATION_START_EVENT); isAnimating = true; - console.log('dispatchHeavyAnimationEvent: start'); + DEBUG && log('start'); } ++promisesInQueue; - console.log('dispatchHeavyAnimationEvent: attach promise, length:', promisesInQueue, timeout); + DEBUG && log('attach promise, length:', promisesInQueue, timeout); const promises = [ timeout !== undefined ? pause(timeout) : undefined, @@ -33,14 +36,14 @@ export const dispatchHeavyAnimationEvent = (promise: Promise, timeout?: num const perf = performance.now(); Promise.race(promises).then(() => { --promisesInQueue; - console.log('dispatchHeavyAnimationEvent: promise end, length:', promisesInQueue, performance.now() - perf); + DEBUG && log('promise end, length:', promisesInQueue, performance.now() - perf); if(!promisesInQueue) { isAnimating = false; promisesInQueue = 0; rootScope.broadcast(ANIMATION_END_EVENT); heavyAnimationPromise.resolve(); - console.log('dispatchHeavyAnimationEvent: end'); + DEBUG && log('end'); } }); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 9221b502..22ae9510 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -169,7 +169,7 @@ export class AppImManager { const isDefaultBackground = rootScope.settings.background.blur === AppStateManager.STATE_INIT.settings.background.blur && rootScope.settings.background.slug === AppStateManager.STATE_INIT.settings.background.slug; if(!isDefaultBackground) { - appDownloadManager.cacheStorage.getFile('background-image').then(blob => { + appDownloadManager.cacheStorage.getFile('backgrounds/' + rootScope.settings.background.slug).then(blob => { this.setBackground(URL.createObjectURL(blob), false); }, () => { // * if NO_ENTRY_FOUND this.setBackground(''); @@ -223,6 +223,12 @@ export class AppImManager { private setSettings() { document.documentElement.style.setProperty('--messages-text-size', rootScope.settings.messagesTextSize + 'px'); + if(rootScope.settings.background.highlightningColor) { + document.documentElement.style.setProperty('--message-highlightning-color', rootScope.settings.background.highlightningColor); + } else { + document.documentElement.style.removeProperty('--message-highlightning-color'); + } + document.body.classList.toggle('animation-level-0', !rootScope.settings.animationsEnabled); document.body.classList.toggle('animation-level-1', false); document.body.classList.toggle('animation-level-2', rootScope.settings.animationsEnabled); diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index fd779ebb..b59eb4a5 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -58,6 +58,7 @@ export type State = Partial<{ background: { type: 'color' | 'image' | 'default', blur: boolean, + highlightningColor?: string, color?: string, slug?: string, } diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index f8a6d2fb..99faeef9 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -51,7 +51,6 @@ $bubble-margin: .25rem; --background-color: #fff; --accent-color: $color-blue; --secondary-color: $color-gray; - --highlightning-color: rgba(77, 142, 80, .4); &.is-highlighted, &.is-selected, /* .bubbles.is-selecting */ & { &:after { @@ -88,7 +87,7 @@ $bubble-margin: .25rem; &.is-highlighted:after { //background-color: rgba(0, 132, 255, .3); - background-color: var(--highlightning-color); + background-color: var(--message-highlightning-color); body:not(.animation-level-0) & { animation: bubbleSelected 2s linear; @@ -125,7 +124,7 @@ $bubble-margin: .25rem; &.is-selected { &:after { - background-color: rgba(77, 142, 80, .4); + background-color: var(--message-highlightning-color); } body:not(.animation-level-0) & { @@ -989,7 +988,7 @@ $bubble-margin: .25rem; &.is-highlighted { .document-selection { - background-color: var(--highlightning-color); + background-color: var(--message-highlightning-color); } body:not(.animation-level-0) & { @@ -1001,7 +1000,7 @@ $bubble-margin: .25rem; &.is-selected { .document-selection { - background-color: rgba(77, 142, 80, .4); + background-color: var(--message-highlightning-color); } body:not(.animation-level-0) & { diff --git a/src/scss/style.scss b/src/scss/style.scss index dc0f3ada..f68d6bcb 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -56,6 +56,7 @@ $chat-padding-handhelds: .5rem; --message-handhelds-margin: 5.5625rem; --message-beside-button-margin: 2.875rem; --message-time-background: rgba(0, 0, 0, .35); + --message-highlightning-color: rgba(77, 142, 80, .4); --messages-container-width: #{$messages-container-width}; --messages-text-size: 16px; --messages-secondary-text-size: calc(var(--messages-text-size) - 1px);