Browse Source

Pick average color of chat background image

master
Eduard Kuzmenko 3 years ago
parent
commit
1edd1db4f6
  1. 62
      src/components/sidebarLeft/tabs/background.ts
  2. 41
      src/helpers/averageColor.ts
  3. 31
      src/helpers/color.ts
  4. 11
      src/hooks/useHeavyAnimationCheck.ts
  5. 8
      src/lib/appManagers/appImManager.ts
  6. 1
      src/lib/appManagers/appStateManager.ts
  7. 9
      src/scss/partials/_chatBubble.scss
  8. 1
      src/scss/style.scss

62
src/components/sidebarLeft/tabs/background.ts

@ -1,6 +1,8 @@
import { generateSection } from ".."; import { generateSection } from "..";
import { averageColor } from "../../../helpers/averageColor";
import blur from "../../../helpers/blur"; import blur from "../../../helpers/blur";
import { deferredPromise } from "../../../helpers/cancellablePromise"; import { deferredPromise } from "../../../helpers/cancellablePromise";
import { rgbToHsl } from "../../../helpers/color";
import { attachClickEvent, findUpClassName } from "../../../helpers/dom"; import { attachClickEvent, findUpClassName } from "../../../helpers/dom";
import { AccountWallPapers, WallPaper } from "../../../layer"; import { AccountWallPapers, WallPaper } from "../../../layer";
import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager"; import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager";
@ -51,16 +53,28 @@ export default class AppBackgroundTab extends SliderSuperTab {
const grid = document.createElement('div'); const grid = document.createElement('div');
grid.classList.add('grid'); grid.classList.add('grid');
const saveToCache = (url: string) => { const saveToCache = (slug: string, url: string) => {
fetch(url).then(response => { 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) => { const setBackgroundDocument = (slug: string, doc: MyDocument) => {
rootScope.settings.background.slug = slug; let _tempId = ++tempId;
rootScope.settings.background.type = 'image'; const middleware = () => _tempId === tempId;
appStateManager.pushToState('settings', rootScope.settings);
const download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0); 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; deferred.cancel = download.cancel;
download.then(() => { 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; 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) { if(rootScope.settings.background.blur) {
setTimeout(() => { setTimeout(() => {
blur(doc.url, 12, 4) blur(doc.url, 12, 4)
.then(url => { .then(url => {
if(rootScope.settings.background.slug !== slug || rootScope.settings.background.type !== 'image') { if(!middleware()) {
return; return;
} }
saveToCache(url); onReady(url);
return appImManager.setBackground(url); });
})
.then(deferred.resolve);
}, 200); }, 200);
} else { } else {
saveToCache(doc.url); onReady(doc.url);
appImManager.setBackground(doc.url).then(deferred.resolve);
} }
}); });
@ -183,10 +215,10 @@ export default class AppBackgroundTab extends SliderSuperTab {
load(); load();
console.log(doc); //console.log(doc);
}); });
console.log(accountWallpapers); //console.log(accountWallpapers);
}); });
this.scrollable.append(grid); this.scrollable.append(grid);

41
src/helpers/averageColor.ts

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

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

11
src/hooks/useHeavyAnimationCheck.ts

@ -6,6 +6,7 @@ import ListenerSetter from '../helpers/listenerSetter';
import { CancellablePromise, deferredPromise } from '../helpers/cancellablePromise'; import { CancellablePromise, deferredPromise } from '../helpers/cancellablePromise';
import { pause } from '../helpers/schedulers'; import { pause } from '../helpers/schedulers';
import rootScope from '../lib/rootScope'; import rootScope from '../lib/rootScope';
import { DEBUG } from '../lib/mtproto/mtproto_config';
const ANIMATION_START_EVENT = 'event-heavy-animation-start'; const ANIMATION_START_EVENT = 'event-heavy-animation-start';
const ANIMATION_END_EVENT = 'event-heavy-animation-end'; const ANIMATION_END_EVENT = 'event-heavy-animation-end';
@ -14,16 +15,18 @@ let isAnimating = false;
let heavyAnimationPromise: CancellablePromise<void> = Promise.resolve(); let heavyAnimationPromise: CancellablePromise<void> = Promise.resolve();
let promisesInQueue = 0; let promisesInQueue = 0;
const log = console.log.bind(console.log, '[HEAVY-ANIMATION]:');
export const dispatchHeavyAnimationEvent = (promise: Promise<any>, timeout?: number) => { export const dispatchHeavyAnimationEvent = (promise: Promise<any>, timeout?: number) => {
if(!isAnimating) { if(!isAnimating) {
heavyAnimationPromise = deferredPromise<void>(); heavyAnimationPromise = deferredPromise<void>();
rootScope.broadcast(ANIMATION_START_EVENT); rootScope.broadcast(ANIMATION_START_EVENT);
isAnimating = true; isAnimating = true;
console.log('dispatchHeavyAnimationEvent: start'); DEBUG && log('start');
} }
++promisesInQueue; ++promisesInQueue;
console.log('dispatchHeavyAnimationEvent: attach promise, length:', promisesInQueue, timeout); DEBUG && log('attach promise, length:', promisesInQueue, timeout);
const promises = [ const promises = [
timeout !== undefined ? pause(timeout) : undefined, timeout !== undefined ? pause(timeout) : undefined,
@ -33,14 +36,14 @@ export const dispatchHeavyAnimationEvent = (promise: Promise<any>, timeout?: num
const perf = performance.now(); const perf = performance.now();
Promise.race(promises).then(() => { Promise.race(promises).then(() => {
--promisesInQueue; --promisesInQueue;
console.log('dispatchHeavyAnimationEvent: promise end, length:', promisesInQueue, performance.now() - perf); DEBUG && log('promise end, length:', promisesInQueue, performance.now() - perf);
if(!promisesInQueue) { if(!promisesInQueue) {
isAnimating = false; isAnimating = false;
promisesInQueue = 0; promisesInQueue = 0;
rootScope.broadcast(ANIMATION_END_EVENT); rootScope.broadcast(ANIMATION_END_EVENT);
heavyAnimationPromise.resolve(); heavyAnimationPromise.resolve();
console.log('dispatchHeavyAnimationEvent: end'); DEBUG && log('end');
} }
}); });

8
src/lib/appManagers/appImManager.ts

@ -169,7 +169,7 @@ export class AppImManager {
const isDefaultBackground = rootScope.settings.background.blur === AppStateManager.STATE_INIT.settings.background.blur && const isDefaultBackground = rootScope.settings.background.blur === AppStateManager.STATE_INIT.settings.background.blur &&
rootScope.settings.background.slug === AppStateManager.STATE_INIT.settings.background.slug; rootScope.settings.background.slug === AppStateManager.STATE_INIT.settings.background.slug;
if(!isDefaultBackground) { 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); this.setBackground(URL.createObjectURL(blob), false);
}, () => { // * if NO_ENTRY_FOUND }, () => { // * if NO_ENTRY_FOUND
this.setBackground(''); this.setBackground('');
@ -223,6 +223,12 @@ export class AppImManager {
private setSettings() { private setSettings() {
document.documentElement.style.setProperty('--messages-text-size', rootScope.settings.messagesTextSize + 'px'); 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-0', !rootScope.settings.animationsEnabled);
document.body.classList.toggle('animation-level-1', false); document.body.classList.toggle('animation-level-1', false);
document.body.classList.toggle('animation-level-2', rootScope.settings.animationsEnabled); document.body.classList.toggle('animation-level-2', rootScope.settings.animationsEnabled);

1
src/lib/appManagers/appStateManager.ts

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

9
src/scss/partials/_chatBubble.scss

@ -51,7 +51,6 @@ $bubble-margin: .25rem;
--background-color: #fff; --background-color: #fff;
--accent-color: $color-blue; --accent-color: $color-blue;
--secondary-color: $color-gray; --secondary-color: $color-gray;
--highlightning-color: rgba(77, 142, 80, .4);
&.is-highlighted, &.is-selected, /* .bubbles.is-selecting */ & { &.is-highlighted, &.is-selected, /* .bubbles.is-selecting */ & {
&:after { &:after {
@ -88,7 +87,7 @@ $bubble-margin: .25rem;
&.is-highlighted:after { &.is-highlighted:after {
//background-color: rgba(0, 132, 255, .3); //background-color: rgba(0, 132, 255, .3);
background-color: var(--highlightning-color); background-color: var(--message-highlightning-color);
body:not(.animation-level-0) & { body:not(.animation-level-0) & {
animation: bubbleSelected 2s linear; animation: bubbleSelected 2s linear;
@ -125,7 +124,7 @@ $bubble-margin: .25rem;
&.is-selected { &.is-selected {
&:after { &:after {
background-color: rgba(77, 142, 80, .4); background-color: var(--message-highlightning-color);
} }
body:not(.animation-level-0) & { body:not(.animation-level-0) & {
@ -989,7 +988,7 @@ $bubble-margin: .25rem;
&.is-highlighted { &.is-highlighted {
.document-selection { .document-selection {
background-color: var(--highlightning-color); background-color: var(--message-highlightning-color);
} }
body:not(.animation-level-0) & { body:not(.animation-level-0) & {
@ -1001,7 +1000,7 @@ $bubble-margin: .25rem;
&.is-selected { &.is-selected {
.document-selection { .document-selection {
background-color: rgba(77, 142, 80, .4); background-color: var(--message-highlightning-color);
} }
body:not(.animation-level-0) & { body:not(.animation-level-0) & {

1
src/scss/style.scss

@ -56,6 +56,7 @@ $chat-padding-handhelds: .5rem;
--message-handhelds-margin: 5.5625rem; --message-handhelds-margin: 5.5625rem;
--message-beside-button-margin: 2.875rem; --message-beside-button-margin: 2.875rem;
--message-time-background: rgba(0, 0, 0, .35); --message-time-background: rgba(0, 0, 0, .35);
--message-highlightning-color: rgba(77, 142, 80, .4);
--messages-container-width: #{$messages-container-width}; --messages-container-width: #{$messages-container-width};
--messages-text-size: 16px; --messages-text-size: 16px;
--messages-secondary-text-size: calc(var(--messages-text-size) - 1px); --messages-secondary-text-size: calc(var(--messages-text-size) - 1px);

Loading…
Cancel
Save