Pick average color of chat background image

This commit is contained in:
Eduard Kuzmenko 2021-02-06 19:18:53 +02:00
parent 424964e7cd
commit 565cb2d599
8 changed files with 139 additions and 25 deletions

View File

@ -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);

View File

@ -0,0 +1,41 @@
import { renderImageFromUrl } from "../components/misc";
export const averageColor = (imageUrl: string): Promise<Uint8ClampedArray> => {
const img = document.createElement('img');
return new Promise<Uint8ClampedArray>((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);
});
});
};

31
src/helpers/color.ts Normal file
View File

@ -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,
});
}

View File

@ -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<void> = Promise.resolve();
let promisesInQueue = 0;
const log = console.log.bind(console.log, '[HEAVY-ANIMATION]:');
export const dispatchHeavyAnimationEvent = (promise: Promise<any>, timeout?: number) => {
if(!isAnimating) {
heavyAnimationPromise = deferredPromise<void>();
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<any>, 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');
}
});

View File

@ -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);

View File

@ -58,6 +58,7 @@ export type State = Partial<{
background: {
type: 'color' | 'image' | 'default',
blur: boolean,
highlightningColor?: string,
color?: string,
slug?: string,
}

View File

@ -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) & {

View File

@ -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);