Merge branch 'backgrounds'
This commit is contained in:
commit
ccfd64b229
@ -689,6 +689,13 @@ export default class ChatBubbles {
|
||||
} else {
|
||||
this.renderNewMessagesByIds([mid], true);
|
||||
}
|
||||
|
||||
if(rootScope.settings.animationsEnabled) {
|
||||
const gradientRenderer = this.chat.gradientRenderer;
|
||||
if(gradientRenderer) {
|
||||
gradientRenderer.toNextPosition();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.listenerSetter.add(rootScope)('history_multiappend', (msgIdsByPeer) => {
|
||||
|
@ -24,7 +24,6 @@ import type { ServerTimeManager } from "../../lib/mtproto/serverTimeManager";
|
||||
import type { AppMessagesIdsManager } from "../../lib/appManagers/appMessagesIdsManager";
|
||||
import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager";
|
||||
import type { AppReactionsManager } from "../../lib/appManagers/appReactionsManager";
|
||||
import type { State } from "../../lib/appManagers/appStateManager";
|
||||
import type stateStorage from '../../lib/stateStorage';
|
||||
import EventListenerBase from "../../helpers/eventListenerBase";
|
||||
import { logger, LogTypes } from "../../lib/logger";
|
||||
@ -37,13 +36,15 @@ import ChatSelection from "./selection";
|
||||
import ChatTopbar from "./topbar";
|
||||
import { BOT_START_PARAM, NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config";
|
||||
import SetTransition from "../singleTransition";
|
||||
import { fastRaf } from "../../helpers/schedulers";
|
||||
import AppPrivateSearchTab from "../sidebarRight/tabs/search";
|
||||
import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl";
|
||||
import renderImageFromUrl, { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl";
|
||||
import mediaSizes from "../../helpers/mediaSizes";
|
||||
import ChatSearch from "./search";
|
||||
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
|
||||
import getAutoDownloadSettingsByPeerId, { ChatAutoDownloadSettings } from "../../helpers/autoDownload";
|
||||
import ChatBackgroundGradientRenderer from "./gradientRenderer";
|
||||
import ChatBackgroundPatternRenderer from "./patternRenderer";
|
||||
import { pause } from "../../helpers/schedulers/pause";
|
||||
|
||||
export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled';
|
||||
|
||||
@ -77,6 +78,13 @@ export default class Chat extends EventListenerBase<{
|
||||
|
||||
public isRestricted: boolean;
|
||||
public autoDownload: ChatAutoDownloadSettings;
|
||||
|
||||
public gradientRenderer: ChatBackgroundGradientRenderer;
|
||||
public patternRenderer: ChatBackgroundPatternRenderer;
|
||||
public gradientCanvas: HTMLCanvasElement;
|
||||
public patternCanvas: HTMLCanvasElement;
|
||||
public backgroundTempId: number;
|
||||
public setBackgroundPromise: Promise<void>;
|
||||
|
||||
constructor(
|
||||
public appImManager: AppImManager,
|
||||
@ -120,32 +128,107 @@ export default class Chat extends EventListenerBase<{
|
||||
|
||||
this.container.append(this.backgroundEl);
|
||||
this.appImManager.chatsContainer.append(this.container);
|
||||
|
||||
this.backgroundTempId = 0;
|
||||
}
|
||||
|
||||
public setBackground(url: string): Promise<void> {
|
||||
public setBackground(url: string, skipAnimation?: boolean): Promise<void> {
|
||||
const theme = rootScope.getTheme();
|
||||
|
||||
let item: HTMLElement;
|
||||
if(theme.background.type === 'color' && document.documentElement.style.cursor === 'grabbing') {
|
||||
const _item = this.backgroundEl.lastElementChild as HTMLElement;
|
||||
if(_item && _item.dataset.type === theme.background.type) {
|
||||
item = _item;
|
||||
}
|
||||
const isColorBackground = !!theme.background.color && !theme.background.slug && !theme.background.intensity;
|
||||
if(
|
||||
isColorBackground &&
|
||||
document.documentElement.style.cursor === 'grabbing' &&
|
||||
this.gradientRenderer &&
|
||||
!this.patternRenderer
|
||||
) {
|
||||
this.gradientCanvas.dataset.colors = theme.background.color;
|
||||
this.gradientRenderer.init(this.gradientCanvas);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const tempId = ++this.backgroundTempId;
|
||||
|
||||
const previousGradientRenderer = this.gradientRenderer;
|
||||
const previousPatternRenderer = this.patternRenderer;
|
||||
const previousGradientCanvas = this.gradientCanvas;
|
||||
const previousPatternCanvas = this.patternCanvas;
|
||||
|
||||
this.gradientRenderer =
|
||||
this.patternRenderer =
|
||||
this.gradientCanvas =
|
||||
this.patternCanvas =
|
||||
undefined;
|
||||
|
||||
const intensity = theme.background.intensity && theme.background.intensity / 100;
|
||||
const isDarkPattern = !!intensity && intensity < 0;
|
||||
|
||||
let patternRenderer: ChatBackgroundPatternRenderer;
|
||||
let patternCanvas = item?.firstElementChild as HTMLCanvasElement;
|
||||
let gradientCanvas: HTMLCanvasElement;
|
||||
if(!item) {
|
||||
item = document.createElement('div');
|
||||
item.classList.add('chat-background-item');
|
||||
item.dataset.type = theme.background.type;
|
||||
|
||||
if(url) {
|
||||
if(intensity) {
|
||||
item.classList.add('is-pattern');
|
||||
|
||||
const rect = this.appImManager.chatsContainer.getBoundingClientRect();
|
||||
patternRenderer = this.patternRenderer = ChatBackgroundPatternRenderer.getInstance({
|
||||
url,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
});
|
||||
|
||||
patternCanvas = this.patternCanvas = patternRenderer.createCanvas();
|
||||
patternCanvas.classList.add('chat-background-item-canvas', 'chat-background-item-pattern-canvas');
|
||||
} else if(theme.background.slug) {
|
||||
item.classList.add('is-image');
|
||||
}
|
||||
} else if(theme.background.color) {
|
||||
item.classList.add('is-color');
|
||||
}
|
||||
}
|
||||
|
||||
if(theme.background.type === 'color') {
|
||||
item.style.backgroundColor = theme.background.color;
|
||||
item.style.backgroundImage = 'none';
|
||||
let gradientRenderer: ChatBackgroundGradientRenderer;
|
||||
const color = theme.background.color;
|
||||
if(color) {
|
||||
// if(color.includes(',')) {
|
||||
const {canvas, gradientRenderer: _gradientRenderer} = ChatBackgroundGradientRenderer.create(color);
|
||||
gradientRenderer = this.gradientRenderer = _gradientRenderer;
|
||||
gradientCanvas = this.gradientCanvas = canvas;
|
||||
gradientCanvas.classList.add('chat-background-item-canvas', 'chat-background-item-color-canvas');
|
||||
|
||||
if(rootScope.settings.animationsEnabled) {
|
||||
gradientRenderer.scrollAnimate(true);
|
||||
}
|
||||
// } else {
|
||||
// item.style.backgroundColor = color;
|
||||
// item.style.backgroundImage = 'none';
|
||||
// }
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
if(patternRenderer) {
|
||||
const setOpacityTo = isDarkPattern ? gradientCanvas : patternCanvas;
|
||||
setOpacityTo.style.setProperty('--opacity-max', '' + Math.abs(intensity));
|
||||
}
|
||||
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
const cb = () => {
|
||||
if(this.backgroundTempId !== tempId) {
|
||||
if(patternRenderer) {
|
||||
patternRenderer.cleanup(patternCanvas);
|
||||
}
|
||||
|
||||
if(gradientRenderer) {
|
||||
gradientRenderer.cleanup();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const prev = this.backgroundEl.lastElementChild as HTMLElement;
|
||||
|
||||
if(prev === item) {
|
||||
@ -153,27 +236,57 @@ export default class Chat extends EventListenerBase<{
|
||||
return;
|
||||
}
|
||||
|
||||
const append = [gradientCanvas, isDarkPattern ? undefined : patternCanvas].filter(Boolean);
|
||||
if(append.length) {
|
||||
item.append(...append);
|
||||
}
|
||||
|
||||
this.backgroundEl.append(item);
|
||||
|
||||
// * одного недостаточно, при обновлении страницы все равно фон появляется неплавно
|
||||
// ! с requestAnimationFrame лучше, но все равно иногда моргает, так что использую два фаста.
|
||||
fastRaf(() => {
|
||||
fastRaf(() => {
|
||||
SetTransition(item, 'is-visible', true, 200, prev ? () => {
|
||||
prev.remove();
|
||||
} : null);
|
||||
});
|
||||
});
|
||||
SetTransition(item, 'is-visible', true, !skipAnimation ? 200 : 0, prev ? () => {
|
||||
if(previousPatternRenderer) {
|
||||
previousPatternRenderer.cleanup(previousPatternCanvas);
|
||||
}
|
||||
|
||||
if(previousGradientRenderer) {
|
||||
previousGradientRenderer.cleanup();
|
||||
}
|
||||
|
||||
prev.remove();
|
||||
} : null, 2);
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
if(url) {
|
||||
if(patternRenderer) {
|
||||
const renderPatternPromise = patternRenderer.renderToCanvas(patternCanvas);
|
||||
renderPatternPromise.then(() => {
|
||||
let promise: Promise<any>;
|
||||
if(isDarkPattern) {
|
||||
promise = patternRenderer.exportCanvasPatternToImage(patternCanvas).then(url => {
|
||||
if(this.backgroundTempId !== tempId) {
|
||||
return;
|
||||
}
|
||||
|
||||
gradientCanvas.style.webkitMaskImage = `url(${url})`;
|
||||
});
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
promise.then(cb);
|
||||
});
|
||||
} else if(url) {
|
||||
renderImageFromUrl(item, url, cb);
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
|
||||
return this.setBackgroundPromise = Promise.race([
|
||||
pause(500),
|
||||
promise
|
||||
]);
|
||||
}
|
||||
|
||||
public setType(type: ChatType) {
|
||||
@ -245,6 +358,19 @@ export default class Chat extends EventListenerBase<{
|
||||
this.bubbles.cleanup();
|
||||
}
|
||||
|
||||
private cleanupBackground() {
|
||||
++this.backgroundTempId;
|
||||
if(this.patternRenderer) {
|
||||
this.patternRenderer.cleanup(this.patternCanvas);
|
||||
this.patternRenderer = undefined;
|
||||
}
|
||||
|
||||
if(this.gradientRenderer) {
|
||||
this.gradientRenderer.cleanup();
|
||||
this.gradientRenderer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
//const perf = performance.now();
|
||||
|
||||
@ -253,6 +379,8 @@ export default class Chat extends EventListenerBase<{
|
||||
this.input.destroy();
|
||||
this.contextMenu && this.contextMenu.destroy();
|
||||
|
||||
this.cleanupBackground();
|
||||
|
||||
delete this.topbar;
|
||||
delete this.bubbles;
|
||||
delete this.input;
|
||||
|
393
src/components/chat/gradientRenderer.ts
Normal file
393
src/components/chat/gradientRenderer.ts
Normal file
@ -0,0 +1,393 @@
|
||||
import { animate } from "../../helpers/animation";
|
||||
import { hexToRgb } from "../../helpers/color";
|
||||
|
||||
const WIDTH = 50;
|
||||
const HEIGHT = WIDTH;
|
||||
|
||||
export default class ChatBackgroundGradientRenderer {
|
||||
private readonly _width = WIDTH;
|
||||
private readonly _height = HEIGHT;
|
||||
private _phase: number;
|
||||
private _tail: number;
|
||||
private readonly _tails = 90;
|
||||
private readonly _scrollTails = 50;
|
||||
private _frames: ImageData[];
|
||||
private _colors: {r: number, g: number, b: number}[];
|
||||
/* private readonly _curve = [
|
||||
0, 25, 50, 75, 100, 150, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900,
|
||||
1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1830, 1860, 1890, 1920,
|
||||
1950, 1980, 2010, 2040, 2070, 2100, 2130, 2160, 2190, 2220, 2250, 2280, 2310,
|
||||
2340, 2370, 2400, 2430, 2460, 2490, 2520, 2550, 2580, 2610, 2630, 2640, 2650,
|
||||
2660, 2670, 2680, 2690, 2700
|
||||
]; */
|
||||
private readonly _curve = [
|
||||
0 , 0.25 , 0.50 , 0.75 , 1 , 1.5 , 2 , 2.5 , 3 , 3.5 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ,
|
||||
13 , 14 , 15 , 16 , 17 , 18 , 18.3 , 18.6 , 18.9 , 19.2 , 19.5 , 19.8 , 20.1 , 20.4 , 20.7 ,
|
||||
21.0 , 21.3 , 21.6 , 21.9 , 22.2 , 22.5 , 22.8 , 23.1 , 23.4 , 23.7 , 24.0 , 24.3 , 24.6 ,
|
||||
24.9 , 25.2 , 25.5 , 25.8 , 26.1 , 26.3 , 26.4 , 26.5 , 26.6 , 26.7 , 26.8 , 26.9 , 27 ,
|
||||
];
|
||||
private readonly _incrementalCurve: number[];
|
||||
private readonly _positions = [
|
||||
{ x: 0.80, y: 0.10 },
|
||||
{ x: 0.60, y: 0.20 },
|
||||
{ x: 0.35, y: 0.25 },
|
||||
{ x: 0.25, y: 0.60 },
|
||||
{ x: 0.20, y: 0.90 },
|
||||
{ x: 0.40, y: 0.80 },
|
||||
{ x: 0.65, y: 0.75 },
|
||||
{ x: 0.75, y: 0.40 }
|
||||
];
|
||||
private readonly _phases = this._positions.length;
|
||||
private _onWheelRAF: number;
|
||||
private _scrollDelta: number;
|
||||
|
||||
// private _ts = 0;
|
||||
// private _fps = 15;
|
||||
// private _frametime = 1000 / this._fps;
|
||||
// private _raf: number;
|
||||
|
||||
private _canvas: HTMLCanvasElement;
|
||||
private _ctx: CanvasRenderingContext2D;
|
||||
private _hc: HTMLCanvasElement;
|
||||
private _hctx: CanvasRenderingContext2D;
|
||||
|
||||
private _addedScrollListener: boolean;
|
||||
private _animatingToNextPosition: boolean;
|
||||
|
||||
constructor() {
|
||||
const diff = this._tails / this._curve[this._curve.length - 1];
|
||||
|
||||
for(let i = 0, length = this._curve.length; i < length; ++i) {
|
||||
this._curve[i] = this._curve[i] * diff;
|
||||
}
|
||||
|
||||
this._incrementalCurve = this._curve.map((v, i, arr) => {
|
||||
return v - (arr[i - 1] ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
private hexToRgb(hex: string) {
|
||||
const result = hexToRgb(hex);
|
||||
return {r: result[0], g: result[1], b: result[2]};
|
||||
}
|
||||
|
||||
private getPositions(shift: number) {
|
||||
const positions = this._positions.slice();
|
||||
while(shift > 0) {
|
||||
positions.push(positions.shift());
|
||||
--shift;
|
||||
}
|
||||
|
||||
const result: typeof positions = [];
|
||||
for(let i = 0; i < positions.length; i += 2) {
|
||||
result.push(positions[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private getNextPositions(phase: number, curveMax: number, curve: number[]) {
|
||||
const pos = this.getPositions(phase);
|
||||
if(!curve[0] && curve.length === 1) {
|
||||
return [pos];
|
||||
}
|
||||
|
||||
const nextPos = this.getPositions(++phase % this._phases);
|
||||
const distances = nextPos.map((nextPos, idx) => {
|
||||
return {
|
||||
x: (nextPos.x - pos[idx].x) / curveMax,
|
||||
y: (nextPos.y - pos[idx].y) / curveMax,
|
||||
};
|
||||
});
|
||||
|
||||
const positions = curve.map((value) => {
|
||||
return distances.map((distance, idx) => {
|
||||
return {
|
||||
x: pos[idx].x + distance.x * value,
|
||||
y: pos[idx].y + distance.y * value
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
private curPosition(phase: number, tail: number) {
|
||||
const positions = this.getNextPositions(phase, this._tails, [tail]);
|
||||
return positions[0];
|
||||
}
|
||||
|
||||
private changeTail(diff: number) {
|
||||
this._tail += diff;
|
||||
|
||||
while(this._tail >= this._tails) {
|
||||
this._tail -= this._tails;
|
||||
if(++this._phase >= this._phases) {
|
||||
this._phase -= this._phases;
|
||||
}
|
||||
}
|
||||
|
||||
while(this._tail < 0) {
|
||||
this._tail += this._tails;
|
||||
if(--this._phase < 0) {
|
||||
this._phase += this._phases;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onWheel = (e: {deltaY: number}) => {
|
||||
if(this._animatingToNextPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._scrollDelta += e.deltaY;
|
||||
if(this._onWheelRAF === undefined) {
|
||||
this._onWheelRAF = requestAnimationFrame(this.drawOnWheel);
|
||||
}
|
||||
};
|
||||
|
||||
private drawOnWheel = () => {
|
||||
let diff = this._scrollDelta / this._scrollTails;
|
||||
this._scrollDelta %= this._scrollTails;
|
||||
diff = diff > 0 ? Math.floor(diff) : Math.ceil(diff);
|
||||
if(diff) {
|
||||
this.changeTail(diff);
|
||||
const curPos = this.curPosition(this._phase, this._tail);
|
||||
this.drawGradient(curPos);
|
||||
}
|
||||
this._onWheelRAF = undefined;
|
||||
};
|
||||
|
||||
private drawNextPositionAnimated = () => {
|
||||
const frames = this._frames;
|
||||
const id = frames.shift();
|
||||
if(id) {
|
||||
this.drawImageData(id);
|
||||
}
|
||||
|
||||
const leftLength = frames.length;
|
||||
if(!leftLength) {
|
||||
this._animatingToNextPosition = undefined;
|
||||
}
|
||||
|
||||
return !!leftLength;
|
||||
};
|
||||
|
||||
private getGradientImageData(positions: {x: number, y: number}[]) {
|
||||
const id = this._hctx.createImageData(this._width, this._height);
|
||||
const pixels = id.data;
|
||||
|
||||
let offset = 0;
|
||||
for(let y = 0; y < this._height; ++y) {
|
||||
const directPixelY = y / this._height;
|
||||
const centerDistanceY = directPixelY - 0.5;
|
||||
const centerDistanceY2 = centerDistanceY * centerDistanceY;
|
||||
|
||||
for(let x = 0; x < this._width; ++x) {
|
||||
const directPixelX = x / this._width;
|
||||
|
||||
const centerDistanceX = directPixelX - 0.5;
|
||||
const centerDistance = Math.sqrt(centerDistanceX * centerDistanceX + centerDistanceY2);
|
||||
|
||||
const swirlFactor = 0.35 * centerDistance;
|
||||
const theta = swirlFactor * swirlFactor * 0.8 * 8.0;
|
||||
const sinTheta = Math.sin(theta);
|
||||
const cosTheta = Math.cos(theta);
|
||||
|
||||
const pixelX = Math.max(0.0, Math.min(1.0, 0.5 + centerDistanceX * cosTheta - centerDistanceY * sinTheta));
|
||||
const pixelY = Math.max(0.0, Math.min(1.0, 0.5 + centerDistanceX * sinTheta + centerDistanceY * cosTheta));
|
||||
|
||||
let distanceSum = 0.0;
|
||||
|
||||
let r = 0.0;
|
||||
let g = 0.0;
|
||||
let b = 0.0;
|
||||
|
||||
for(let i = 0; i < this._colors.length; i++) {
|
||||
const colorX = positions[i].x;
|
||||
const colorY = positions[i].y;
|
||||
|
||||
const distanceX = pixelX - colorX;
|
||||
const distanceY = pixelY - colorY;
|
||||
|
||||
let distance = Math.max(0.0, 0.9 - Math.sqrt(distanceX * distanceX + distanceY * distanceY));
|
||||
distance = distance * distance * distance * distance;
|
||||
distanceSum += distance;
|
||||
|
||||
r += distance * this._colors[i].r / 255;
|
||||
g += distance * this._colors[i].g / 255;
|
||||
b += distance * this._colors[i].b / 255;
|
||||
}
|
||||
|
||||
pixels[offset++] = r / distanceSum * 255.0;
|
||||
pixels[offset++] = g / distanceSum * 255.0;
|
||||
pixels[offset++] = b / distanceSum * 255.0;
|
||||
pixels[offset++] = 0xFF;
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private drawImageData(id: ImageData) {
|
||||
this._hctx.putImageData(id, 0, 0);
|
||||
this._ctx.drawImage(this._hc, 0, 0, this._width, this._height);
|
||||
}
|
||||
|
||||
private drawGradient(positions: {x: number, y: number}[]) {
|
||||
this.drawImageData(this.getGradientImageData(positions));
|
||||
}
|
||||
|
||||
// private doAnimate = () => {
|
||||
// const now = +Date.now();
|
||||
// if(!document.hasFocus() || (now - this._ts) < this._frametime) {
|
||||
// this._raf = requestAnimationFrame(this.doAnimate);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this._ts = now;
|
||||
// this.changeTail(1);
|
||||
// const cur_pos = this.curPosition(this._phase, this._tail);
|
||||
// this.drawGradient(cur_pos);
|
||||
// this._raf = requestAnimationFrame(this.doAnimate);
|
||||
// };
|
||||
|
||||
// public animate(start?: boolean) {
|
||||
// if(!start) {
|
||||
// cancelAnimationFrame(this._raf);
|
||||
// return;
|
||||
// }
|
||||
// this.doAnimate();
|
||||
// }
|
||||
|
||||
public init(el: HTMLCanvasElement) {
|
||||
this._frames = [];
|
||||
this._phase = 0;
|
||||
this._tail = 0;
|
||||
this._scrollDelta = 0;
|
||||
if(this._onWheelRAF !== undefined) {
|
||||
cancelAnimationFrame(this._onWheelRAF);
|
||||
this._onWheelRAF = undefined;
|
||||
}
|
||||
|
||||
const colors = el.getAttribute('data-colors').split(',').reverse();
|
||||
this._colors = colors.map(color => {
|
||||
return this.hexToRgb(color);
|
||||
});
|
||||
|
||||
if(!this._hc) {
|
||||
this._hc = document.createElement('canvas');
|
||||
this._hc.width = this._width;
|
||||
this._hc.height = this._height;
|
||||
this._hctx = this._hc.getContext('2d');
|
||||
}
|
||||
|
||||
this._canvas = el;
|
||||
this._ctx = this._canvas.getContext('2d');
|
||||
this.update();
|
||||
}
|
||||
|
||||
public update() {
|
||||
if(this._colors.length < 2) {
|
||||
const color = this._colors[0];
|
||||
this._ctx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`;
|
||||
this._ctx.fillRect(0, 0, this._width, this._height);
|
||||
return;
|
||||
}
|
||||
|
||||
const pos = this.curPosition(this._phase, this._tail);
|
||||
this.drawGradient(pos);
|
||||
}
|
||||
|
||||
public toNextPosition() {
|
||||
if(this._colors.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tail = this._tail;
|
||||
const tails = this._tails;
|
||||
|
||||
let nextPhaseOnIdx: number;
|
||||
|
||||
const curve: number[] = [];
|
||||
for(let i = 0, length = this._incrementalCurve.length; i < length; ++i) {
|
||||
const inc = this._incrementalCurve[i];
|
||||
let value = (curve[i - 1] ?? tail) + inc;
|
||||
|
||||
if(+value.toFixed(2) > tails && nextPhaseOnIdx === undefined) {
|
||||
nextPhaseOnIdx = i;
|
||||
value %= tails;
|
||||
}
|
||||
|
||||
curve.push(value);
|
||||
}
|
||||
|
||||
const currentPhaseCurve = curve.slice(0, nextPhaseOnIdx);
|
||||
const nextPhaseCurve = nextPhaseOnIdx !== undefined ? curve.slice(nextPhaseOnIdx) : [];
|
||||
|
||||
[currentPhaseCurve, nextPhaseCurve].forEach((curve, idx, curves) => {
|
||||
const last = curve[curve.length - 1];
|
||||
if(last !== undefined && last > tails) {
|
||||
curve[curve.length - 1] = +last.toFixed(2);
|
||||
}
|
||||
|
||||
this._tail = last ?? 0;
|
||||
|
||||
if(!curve.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positions = this.getNextPositions(this._phase, tails, curve);
|
||||
if(idx !== (curves.length - 1)) {
|
||||
if(++this._phase >= this._phases) {
|
||||
this._phase -= this._phases;
|
||||
}
|
||||
}
|
||||
|
||||
const ids = positions.map((pos) => {
|
||||
return this.getGradientImageData(pos);
|
||||
});
|
||||
|
||||
this._frames.push(...ids);
|
||||
});
|
||||
|
||||
this._animatingToNextPosition = true;
|
||||
animate(this.drawNextPositionAnimated);
|
||||
}
|
||||
|
||||
public scrollAnimate(start?: boolean) {
|
||||
if(this._colors.length < 2 && start) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(start && !this._addedScrollListener) {
|
||||
document.addEventListener('wheel', this.onWheel);
|
||||
this._addedScrollListener = true;
|
||||
} else if(!start && this._addedScrollListener) {
|
||||
document.removeEventListener('wheel', this.onWheel);
|
||||
this._addedScrollListener = false;
|
||||
}
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
this.scrollAnimate(false);
|
||||
// this.animate(false);
|
||||
}
|
||||
|
||||
public static createCanvas(colors?: string) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = WIDTH;
|
||||
canvas.height = HEIGHT;
|
||||
if(colors !== undefined) {
|
||||
canvas.dataset.colors = colors;
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
public static create(colors?: string) {
|
||||
const canvas = this.createCanvas(colors);
|
||||
const gradientRenderer = new ChatBackgroundGradientRenderer();
|
||||
gradientRenderer.init(canvas);
|
||||
|
||||
return {gradientRenderer, canvas};
|
||||
}
|
||||
}
|
121
src/components/chat/patternRenderer.ts
Normal file
121
src/components/chat/patternRenderer.ts
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import { IS_SAFARI } from "../../environment/userAgent";
|
||||
import { indexOfAndSplice } from "../../helpers/array";
|
||||
import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl";
|
||||
import { deepEqual } from "../../helpers/object";
|
||||
|
||||
type ChatBackgroundPatternRendererInitOptions = {
|
||||
url: string,
|
||||
width: number,
|
||||
height: number
|
||||
};
|
||||
|
||||
export default class ChatBackgroundPatternRenderer {
|
||||
private static INSTANCES: ChatBackgroundPatternRenderer[] = [];
|
||||
|
||||
private pattern: CanvasPattern;
|
||||
private objectUrl: string;
|
||||
private options: ChatBackgroundPatternRendererInitOptions;
|
||||
private canvases: Set<HTMLCanvasElement>;
|
||||
private createCanvasPatternPromise: Promise<void>;
|
||||
private exportCanvasPatternToImagePromise: Promise<string>;
|
||||
// private img: HTMLImageElement;
|
||||
|
||||
constructor() {
|
||||
this.canvases = new Set();
|
||||
}
|
||||
|
||||
public static getInstance(options: ChatBackgroundPatternRendererInitOptions) {
|
||||
let instance = this.INSTANCES.find((instance) => {
|
||||
return deepEqual(instance.options, options);
|
||||
});
|
||||
|
||||
if(!instance) {
|
||||
instance = new ChatBackgroundPatternRenderer();
|
||||
instance.init(options);
|
||||
this.INSTANCES.push(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public init(options: ChatBackgroundPatternRendererInitOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public renderToCanvas(canvas: HTMLCanvasElement) {
|
||||
return this.createCanvasPattern(canvas).then(() => {
|
||||
return this.fillCanvas(canvas);
|
||||
});
|
||||
}
|
||||
|
||||
private createCanvasPattern(canvas: HTMLCanvasElement) {
|
||||
if(this.createCanvasPatternPromise) return this.createCanvasPatternPromise;
|
||||
return this.createCanvasPatternPromise = new Promise((resolve) => {
|
||||
const img = document.createElement('img');
|
||||
img.crossOrigin = 'anonymous';
|
||||
renderImageFromUrlPromise(img, this.options.url, false).then(() => {
|
||||
let createPatternFrom: HTMLImageElement | HTMLCanvasElement;
|
||||
if(IS_SAFARI) {
|
||||
const canvas = createPatternFrom = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
} else {
|
||||
createPatternFrom = img;
|
||||
}
|
||||
|
||||
// this.img = img;
|
||||
this.pattern = canvas.getContext('2d').createPattern(createPatternFrom, 'repeat-x');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public exportCanvasPatternToImage(canvas: HTMLCanvasElement) {
|
||||
if(this.exportCanvasPatternToImagePromise) return this.exportCanvasPatternToImagePromise;
|
||||
return this.exportCanvasPatternToImagePromise = new Promise<string>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
const newUrl = this.objectUrl = URL.createObjectURL(blob);
|
||||
resolve(newUrl);
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
|
||||
public cleanup(canvas: HTMLCanvasElement) {
|
||||
this.canvases.delete(canvas);
|
||||
|
||||
if(!this.canvases.size) {
|
||||
indexOfAndSplice(ChatBackgroundPatternRenderer.INSTANCES, this);
|
||||
|
||||
if(this.objectUrl) {
|
||||
URL.revokeObjectURL(this.objectUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fillCanvas(canvas: HTMLCanvasElement) {
|
||||
const context = canvas.getContext('2d');
|
||||
context.fillStyle = this.pattern;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
// context.drawImage(this.img, 0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
public setCanvasDimensions(canvas: HTMLCanvasElement) {
|
||||
canvas.width = this.options.width * window.devicePixelRatio;
|
||||
canvas.height = this.options.height * window.devicePixelRatio * 1.5;
|
||||
}
|
||||
|
||||
public createCanvas() {
|
||||
const canvas = document.createElement('canvas');
|
||||
this.canvases.add(canvas);
|
||||
this.setCanvasDimensions(canvas);
|
||||
return canvas;
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { generateSection } from "..";
|
||||
import { averageColor } from "../../../helpers/averageColor";
|
||||
import { averageColor, averageColorFromCanvas } from "../../../helpers/averageColor";
|
||||
import blur from "../../../helpers/blur";
|
||||
import { deferredPromise } from "../../../helpers/cancellablePromise";
|
||||
import { attachClickEvent } from "../../../helpers/dom/clickEvent";
|
||||
@ -14,9 +14,10 @@ import { requestFile } from "../../../helpers/files";
|
||||
import highlightningColor from "../../../helpers/highlightningColor";
|
||||
import { copy } from "../../../helpers/object";
|
||||
import sequentialDom from "../../../helpers/sequentialDom";
|
||||
import ChatBackgroundGradientRenderer from "../../chat/gradientRenderer";
|
||||
import { AccountWallPapers, PhotoSize, WallPaper } from "../../../layer";
|
||||
import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager";
|
||||
import appDownloadManager from "../../../lib/appManagers/appDownloadManager";
|
||||
import appDownloadManager, { DownloadBlob } from "../../../lib/appManagers/appDownloadManager";
|
||||
import appImManager from "../../../lib/appManagers/appImManager";
|
||||
import appPhotosManager from "../../../lib/appManagers/appPhotosManager";
|
||||
import appStateManager, { Theme, STATE_INIT } from "../../../lib/appManagers/appStateManager";
|
||||
@ -38,6 +39,9 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
private clicked: Set<DocId> = new Set();
|
||||
private blurCheckboxField: CheckboxField;
|
||||
|
||||
private wallpapersByElement: Map<HTMLElement, WallPaper> = new Map();
|
||||
private elementsByKey: Map<string, HTMLElement> = new Map();
|
||||
|
||||
init() {
|
||||
this.header.classList.add('with-border');
|
||||
this.container.classList.add('background-container', 'background-image-container');
|
||||
@ -71,12 +75,17 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
this.theme.background.blur = blurCheckboxField.input.checked;
|
||||
appStateManager.pushToState('settings', rootScope.settings);
|
||||
|
||||
const active = grid.querySelector('.active') as HTMLElement;
|
||||
if(!active) return;
|
||||
|
||||
// * wait for animation end
|
||||
setTimeout(() => {
|
||||
this.setBackgroundDocument(active.dataset.slug, appDocsManager.getDoc(active.dataset.docId));
|
||||
const active = grid.querySelector('.active') as HTMLElement;
|
||||
if(!active) return;
|
||||
|
||||
const wallpaper = this.wallpapersByElement.get(active);
|
||||
if((wallpaper as WallPaper.wallPaper).pFlags.pattern || wallpaper._ === 'wallPaperNoFile') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setBackgroundDocument(wallpaper);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
@ -164,15 +173,13 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
wallpaper = _wallpaper as WallPaper.wallPaper;
|
||||
wallpaper.document = appDocsManager.saveDoc(wallpaper.document);
|
||||
|
||||
container.dataset.docId = '' + wallpaper.document.id;
|
||||
container.dataset.slug = wallpaper.slug;
|
||||
|
||||
this.setBackgroundDocument(wallpaper.slug, wallpaper.document).then(deferred.resolve, deferred.reject);
|
||||
this.setBackgroundDocument(wallpaper).then(deferred.resolve, deferred.reject);
|
||||
}, deferred.reject);
|
||||
}, deferred.reject);
|
||||
|
||||
const key = this.getWallpaperKey(wallpaper);
|
||||
deferred.then(() => {
|
||||
this.clicked.delete(wallpaper.document.id);
|
||||
this.clicked.delete(key);
|
||||
}, (err) => {
|
||||
container.remove();
|
||||
//console.error('i saw the body drop', err);
|
||||
@ -185,7 +192,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
});
|
||||
|
||||
const container = this.addWallPaper(wallpaper, false);
|
||||
this.clicked.add(wallpaper.document.id);
|
||||
this.clicked.add(key);
|
||||
|
||||
preloader.attach(container, false, deferred);
|
||||
});
|
||||
@ -202,41 +209,100 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
}
|
||||
};
|
||||
|
||||
private addWallPaper(wallpaper: WallPaper.wallPaper, append = true) {
|
||||
if(wallpaper.pFlags.pattern ||
|
||||
!wallpaper.document ||
|
||||
(wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0) {
|
||||
private getColorsFromWallpaper(wallpaper: WallPaper) {
|
||||
return wallpaper.settings ? [
|
||||
wallpaper.settings.background_color,
|
||||
wallpaper.settings.second_background_color,
|
||||
wallpaper.settings.third_background_color,
|
||||
wallpaper.settings.fourth_background_color
|
||||
].filter(Boolean).map(color => '#' + color.toString(16)).join(',') : '';
|
||||
}
|
||||
|
||||
private getWallpaperKey(wallpaper: WallPaper) {
|
||||
return '' + wallpaper.id;
|
||||
}
|
||||
|
||||
private getWallpaperKeyFromTheme(theme: Theme) {
|
||||
return '' + theme.background.id;
|
||||
}
|
||||
|
||||
private addWallPaper(wallpaper: WallPaper, append = true) {
|
||||
const colors = this.getColorsFromWallpaper(wallpaper);
|
||||
const hasFile = wallpaper._ === 'wallPaper';
|
||||
if((hasFile && wallpaper.pFlags.pattern && !colors)/* ||
|
||||
(wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0 */) {
|
||||
return;
|
||||
}
|
||||
|
||||
wallpaper.document = appDocsManager.saveDoc(wallpaper.document);
|
||||
const isDark = !!wallpaper.pFlags.dark;
|
||||
|
||||
const doc: MyDocument = hasFile ? (wallpaper.document = appDocsManager.saveDoc(wallpaper.document)) : undefined;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('grid-item');
|
||||
|
||||
container.dataset.id = '' + wallpaper.id;
|
||||
|
||||
const key = this.getWallpaperKey(wallpaper);
|
||||
this.wallpapersByElement.set(container, wallpaper);
|
||||
this.elementsByKey.set(key, container);
|
||||
|
||||
const media = document.createElement('div');
|
||||
media.classList.add('grid-item-media');
|
||||
|
||||
const wrapped = wrapPhoto({
|
||||
photo: wallpaper.document,
|
||||
message: null,
|
||||
container: media,
|
||||
withoutPreloader: true,
|
||||
size: appPhotosManager.choosePhotoSize(wallpaper.document, 200, 200)
|
||||
});
|
||||
let wrapped: ReturnType<typeof wrapPhoto>, size: PhotoSize;
|
||||
if(hasFile) {
|
||||
size = appPhotosManager.choosePhotoSize(doc, 200, 200);
|
||||
wrapped = wrapPhoto({
|
||||
photo: doc,
|
||||
message: null,
|
||||
container: media,
|
||||
withoutPreloader: true,
|
||||
size: size,
|
||||
noFadeIn: wallpaper.pFlags.pattern
|
||||
});
|
||||
|
||||
container.dataset.docId = '' + wallpaper.document.id;
|
||||
container.dataset.slug = wallpaper.slug;
|
||||
(wrapped.loadPromises.thumb || wrapped.loadPromises.full).then(() => {
|
||||
sequentialDom.mutate(() => {
|
||||
container.append(media);
|
||||
});
|
||||
});
|
||||
|
||||
if(this.theme.background.type === 'image' && this.theme.background.slug === wallpaper.slug) {
|
||||
container.classList.add('active');
|
||||
if(wallpaper.pFlags.pattern) {
|
||||
media.classList.add('is-pattern');
|
||||
|
||||
if(isDark) {
|
||||
wrapped.images.full.style.display = 'none';
|
||||
if(wrapped.images.thumb) {
|
||||
wrapped.images.thumb.style.display = 'none';
|
||||
}
|
||||
} else if(wallpaper.settings?.intensity) {
|
||||
wrapped.images.full.style.opacity = '' + Math.abs(wallpaper.settings.intensity) / 100;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
container.append(media);
|
||||
}
|
||||
|
||||
(wrapped.loadPromises.thumb || wrapped.loadPromises.full).then(() => {
|
||||
sequentialDom.mutate(() => {
|
||||
container.append(media);
|
||||
});
|
||||
});
|
||||
if(wallpaper.settings && wallpaper.settings.background_color !== undefined) {
|
||||
const {canvas} = ChatBackgroundGradientRenderer.create(colors);
|
||||
canvas.classList.add('background-colors-canvas');
|
||||
|
||||
if(isDark && hasFile) {
|
||||
const cacheContext = appDownloadManager.getCacheContext(doc, size.type);
|
||||
wrapped.loadPromises.full.then(() => {
|
||||
canvas.style.webkitMaskImage = `url(${cacheContext.url})`;
|
||||
canvas.style.opacity = '' + Math.abs(wallpaper.settings.intensity) / 100;
|
||||
media.append(canvas);
|
||||
});
|
||||
} else {
|
||||
media.append(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.getWallpaperKeyFromTheme(this.theme) === key) {
|
||||
container.classList.add('active');
|
||||
}
|
||||
|
||||
this.grid[append ? 'append' : 'prepend'](container);
|
||||
|
||||
@ -247,19 +313,24 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
const target = findUpClassName(e.target, 'grid-item') as HTMLElement;
|
||||
if(!target) return;
|
||||
|
||||
const {docId, slug} = target.dataset;
|
||||
if(this.clicked.has(docId)) return;
|
||||
this.clicked.add(docId);
|
||||
|
||||
const wallpaper = this.wallpapersByElement.get(target);
|
||||
if(wallpaper._ === 'wallPaperNoFile') {
|
||||
this.setBackgroundDocument(wallpaper);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = this.getWallpaperKey(wallpaper);
|
||||
if(this.clicked.has(key)) return;
|
||||
this.clicked.add(key);
|
||||
|
||||
const doc = wallpaper.document as MyDocument;
|
||||
const preloader = new ProgressivePreloader({
|
||||
cancelable: true,
|
||||
tryAgainOnFail: false
|
||||
});
|
||||
|
||||
const doc = appDocsManager.getDoc(docId);
|
||||
|
||||
const load = () => {
|
||||
const promise = this.setBackgroundDocument(slug, doc);
|
||||
const promise = this.setBackgroundDocument(wallpaper);
|
||||
const cacheContext = appDownloadManager.getCacheContext(doc);
|
||||
if(!cacheContext.url || this.theme.background.blur) {
|
||||
preloader.attach(target, true, promise);
|
||||
@ -288,15 +359,20 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
});
|
||||
};
|
||||
|
||||
private setBackgroundDocument = (slug: string, doc: MyDocument) => {
|
||||
private setBackgroundDocument = (wallpaper: WallPaper) => {
|
||||
let _tempId = ++this.tempId;
|
||||
const middleware = () => _tempId === this.tempId;
|
||||
|
||||
const download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0);
|
||||
|
||||
const doc = (wallpaper as WallPaper.wallPaper).document as MyDocument;
|
||||
const deferred = deferredPromise<void>();
|
||||
deferred.addNotifyListener = download.addNotifyListener;
|
||||
deferred.cancel = download.cancel;
|
||||
let download: Promise<void> | DownloadBlob;
|
||||
if(doc) {
|
||||
download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0);
|
||||
deferred.addNotifyListener = download.addNotifyListener;
|
||||
deferred.cancel = download.cancel;
|
||||
} else {
|
||||
download = Promise.resolve();
|
||||
}
|
||||
|
||||
download.then(() => {
|
||||
if(!middleware()) {
|
||||
@ -305,27 +381,47 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
}
|
||||
|
||||
const background = this.theme.background;
|
||||
const onReady = (url: string) => {
|
||||
const onReady = (url?: string) => {
|
||||
//const perf = performance.now();
|
||||
averageColor(url).then(pixel => {
|
||||
let getPixelPromise: Promise<Uint8ClampedArray>;
|
||||
if(url && !this.theme.background.color) {
|
||||
getPixelPromise = averageColor(url);
|
||||
} else {
|
||||
const {canvas} = ChatBackgroundGradientRenderer.create(this.getColorsFromWallpaper(wallpaper));
|
||||
getPixelPromise = Promise.resolve(averageColorFromCanvas(canvas));
|
||||
}
|
||||
|
||||
getPixelPromise.then((pixel) => {
|
||||
if(!middleware()) {
|
||||
deferred.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const hsla = highlightningColor(Array.from(pixel) as any);
|
||||
// const hsla = 'rgba(0, 0, 0, 0.3)';
|
||||
//console.log(doc, hsla, performance.now() - perf);
|
||||
|
||||
const slug = (wallpaper as WallPaper.wallPaper).slug ?? '';
|
||||
background.id = wallpaper.id;
|
||||
background.intensity = wallpaper.settings?.intensity ?? 0;
|
||||
background.color = this.getColorsFromWallpaper(wallpaper);
|
||||
background.slug = slug;
|
||||
background.type = 'image';
|
||||
background.highlightningColor = hsla;
|
||||
appStateManager.pushToState('settings', rootScope.settings);
|
||||
|
||||
this.saveToCache(slug, url);
|
||||
appImManager.applyCurrentTheme(slug, url).then(deferred.resolve);
|
||||
if(slug) {
|
||||
this.saveToCache(slug, url);
|
||||
}
|
||||
|
||||
appImManager.applyCurrentTheme(slug, url, true).then(deferred.resolve);
|
||||
});
|
||||
};
|
||||
|
||||
if(!doc) {
|
||||
onReady();
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheContext = appDownloadManager.getCacheContext(doc);
|
||||
if(background.blur) {
|
||||
setTimeout(() => {
|
||||
@ -349,8 +445,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
||||
|
||||
private setActive = () => {
|
||||
const active = this.grid.querySelector('.active');
|
||||
const background = this.theme.background;
|
||||
const target = background.type === 'image' ? this.grid.querySelector(`.grid-item[data-slug="${background.slug}"]`) : null;
|
||||
const target = this.elementsByKey.get(this.getWallpaperKeyFromTheme(this.theme));
|
||||
if(active === target) {
|
||||
return;
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
|
||||
private setActive() {
|
||||
const active = this.grid.querySelector('.active');
|
||||
const background = this.theme.background;
|
||||
const target = background.type === 'color' ? this.grid.querySelector(`.grid-item[data-color="${background.color}"]`) : null;
|
||||
const target = background.color ? this.grid.querySelector(`.grid-item[data-color="${background.color}"]`) : null;
|
||||
if(active === target) {
|
||||
return;
|
||||
}
|
||||
@ -115,8 +115,10 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
|
||||
const background = this.theme.background;
|
||||
const hsla = highlightningColor(rgba);
|
||||
|
||||
background.id = '2';
|
||||
background.intensity = 0;
|
||||
background.slug = '';
|
||||
background.color = hex.toLowerCase();
|
||||
background.type = 'color';
|
||||
background.highlightningColor = hsla;
|
||||
appStateManager.pushToState('settings', rootScope.settings);
|
||||
|
||||
@ -133,14 +135,17 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
|
||||
setTimeout(() => {
|
||||
const background = this.theme.background;
|
||||
|
||||
const color = (background.color || '').split(',')[0];
|
||||
const isColored = !!color && !background.slug;
|
||||
|
||||
// * set active if type is color
|
||||
if(background.type === 'color') {
|
||||
if(isColored) {
|
||||
this.colorPicker.onChange = this.onColorChange;
|
||||
}
|
||||
|
||||
this.colorPicker.setColor(background.color || '#cccccc');
|
||||
this.colorPicker.setColor(color || '#cccccc');
|
||||
|
||||
if(background.type !== 'color') {
|
||||
if(!isColored) {
|
||||
this.colorPicker.onChange = this.onColorChange;
|
||||
}
|
||||
}, 0);
|
||||
|
@ -6,7 +6,28 @@
|
||||
|
||||
import renderImageFromUrl from "./dom/renderImageFromUrl";
|
||||
|
||||
export const averageColor = (imageUrl: string): Promise<Uint8ClampedArray> => {
|
||||
export function averageColorFromCanvas(canvas: HTMLCanvasElement) {
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
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;
|
||||
return outPixel;
|
||||
}
|
||||
|
||||
export function averageColor(imageUrl: string) {
|
||||
const img = document.createElement('img');
|
||||
return new Promise<Uint8ClampedArray>((resolve) => {
|
||||
renderImageFromUrl(img, imageUrl, () => {
|
||||
@ -25,23 +46,7 @@ export const averageColor = (imageUrl: string): Promise<Uint8ClampedArray> => {
|
||||
|
||||
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);
|
||||
resolve(averageColorFromCanvas(canvas));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ export default function blobSafeMimeType(mimeType: string) {
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/svg+xml',
|
||||
'image/webp',
|
||||
'image/bmp',
|
||||
'video/mp4',
|
||||
|
@ -104,7 +104,11 @@ export function hslaStringToRgba(hsla: string) {
|
||||
|
||||
export function hexaToRgba(hexa: string) {
|
||||
const arr: ColorRgba = [] as any;
|
||||
const offset = 1;
|
||||
const offset = hexa[0] === '#' ? 1 : 0;
|
||||
if(hexa.length === (5 + offset)) {
|
||||
hexa = (offset ? '#' : '') + '0' + hexa.slice(offset);
|
||||
}
|
||||
|
||||
if(hexa.length === (3 + offset)) {
|
||||
for(let i = offset; i < hexa.length; ++i) {
|
||||
arr.push(parseInt(hexa[i] + hexa[i], 16));
|
||||
|
@ -52,7 +52,10 @@ export default function renderImageFromUrl(
|
||||
}, {once: true});
|
||||
|
||||
if(callback) {
|
||||
loader.addEventListener('error', callback);
|
||||
loader.addEventListener('error', (err) => {
|
||||
console.error('Render image from url failed:', err, url, loader);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ import appDraftsManager from './appDraftsManager';
|
||||
import serverTimeManager from '../mtproto/serverTimeManager';
|
||||
import stateStorage from '../stateStorage';
|
||||
import appDownloadManager from './appDownloadManager';
|
||||
import { AppStateManager } from './appStateManager';
|
||||
import { AppStateManager, STATE_INIT } from './appStateManager';
|
||||
import { MOUNT_CLASS_TO } from '../../config/debug';
|
||||
import appNavigationController from '../../components/appNavigationController';
|
||||
import appNotificationsManager from './appNotificationsManager';
|
||||
@ -128,10 +128,12 @@ export class AppImManager {
|
||||
private chatsSelectTabDebounced: () => void;
|
||||
|
||||
public markupTooltip: MarkupTooltip;
|
||||
private backgroundPromises: {[slug: string]: Promise<string>} = {};
|
||||
private backgroundPromises: {[slug: string]: Promise<string>};
|
||||
|
||||
private topbarCall: TopbarCall;
|
||||
emojiAnimationContainer: HTMLDivElement;
|
||||
public emojiAnimationContainer: HTMLDivElement;
|
||||
|
||||
private lastBackgroundUrl: string;
|
||||
|
||||
get myId() {
|
||||
return rootScope.myId;
|
||||
@ -147,6 +149,14 @@ export class AppImManager {
|
||||
|
||||
this.log = logger('IM', LogTypes.Log | LogTypes.Warn | LogTypes.Debug | LogTypes.Error);
|
||||
|
||||
this.backgroundPromises = {};
|
||||
STATE_INIT.settings.themes.forEach(theme => {
|
||||
if(theme.background.slug) {
|
||||
const url = /* window.location.origin + window.location.pathname + */'assets/img/' + theme.background.slug + '.svg';
|
||||
this.backgroundPromises[theme.background.slug] = Promise.resolve(url);
|
||||
}
|
||||
});
|
||||
|
||||
this.selectTab(0);
|
||||
|
||||
window.addEventListener('blur', () => {
|
||||
@ -205,7 +215,9 @@ export class AppImManager {
|
||||
animationIntersector.checkAnimations(false);
|
||||
});
|
||||
|
||||
// setTimeout(() => {
|
||||
this.applyCurrentTheme();
|
||||
// }, 0);
|
||||
|
||||
// * fix simultaneous opened both sidebars, can happen when floating sidebar is opened with left sidebar
|
||||
mediaSizes.addEventListener('changeScreen', (from, to) => {
|
||||
@ -217,6 +229,13 @@ export class AppImManager {
|
||||
this.appendEmojiAnimationContainer(to);
|
||||
});
|
||||
|
||||
const resizeBackgroundDebounced = debounce(() => {
|
||||
this.setBackground(this.lastBackgroundUrl, false);
|
||||
}, 200, false, true);
|
||||
mediaSizes.addEventListener('resize', () => {
|
||||
resizeBackgroundDebounced();
|
||||
});
|
||||
|
||||
rootScope.addEventListener('history_focus', (e) => {
|
||||
let {peerId, threadId, mid, startParam} = e;
|
||||
if(threadId) threadId = appMessagesIdsManager.generateMessageId(threadId);
|
||||
@ -309,6 +328,11 @@ export class AppImManager {
|
||||
popup.show();
|
||||
});
|
||||
|
||||
// remove scroll listener when setting chat to tray
|
||||
rootScope.addEventListener('chat_changing', ({to}) => {
|
||||
this.toggleChatGradientAnimation(to);
|
||||
});
|
||||
|
||||
stateStorage.get('chatPositions').then((c) => {
|
||||
stateStorage.setToCache('chatPositions', c || {});
|
||||
});
|
||||
@ -551,6 +575,14 @@ export class AppImManager {
|
||||
this.attachKeydownListener();
|
||||
}
|
||||
|
||||
private toggleChatGradientAnimation(activatingChat: Chat) {
|
||||
this.chats.forEach(chat => {
|
||||
if(chat.gradientRenderer) {
|
||||
chat.gradientRenderer.scrollAnimate(rootScope.settings.animationsEnabled && chat === activatingChat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private appendEmojiAnimationContainer(screen: ScreenSize) {
|
||||
const appendTo = screen === ScreenSize.mobile ? this.columnEl : document.body;
|
||||
if(this.emojiAnimationContainer.parentElement !== appendTo) {
|
||||
@ -1005,19 +1037,19 @@ export class AppImManager {
|
||||
public setCurrentBackground(broadcastEvent = false) {
|
||||
const theme = rootScope.getTheme();
|
||||
|
||||
if(theme.background.type === 'image' || (theme.background.type === 'default' && theme.background.slug)) {
|
||||
if(theme.background.slug) {
|
||||
const defaultTheme = AppStateManager.STATE_INIT.settings.themes.find(t => t.name === theme.name);
|
||||
const isDefaultBackground = theme.background.blur === defaultTheme.background.blur &&
|
||||
theme.background.slug === defaultTheme.background.slug;
|
||||
// const isDefaultBackground = theme.background.blur === defaultTheme.background.blur &&
|
||||
// theme.background.slug === defaultTheme.background.slug;
|
||||
|
||||
if(!isDefaultBackground) {
|
||||
// if(!isDefaultBackground) {
|
||||
return this.getBackground(theme.background.slug).then((url) => {
|
||||
return this.setBackground(url, broadcastEvent);
|
||||
}, () => { // * if NO_ENTRY_FOUND
|
||||
theme.background = copy(defaultTheme.background); // * reset background
|
||||
return this.setBackground('', true);
|
||||
});
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
return this.setBackground('', broadcastEvent);
|
||||
@ -1031,6 +1063,7 @@ export class AppImManager {
|
||||
}
|
||||
|
||||
public setBackground(url: string, broadcastEvent = true): Promise<void> {
|
||||
this.lastBackgroundUrl = url;
|
||||
const promises = this.chats.map(chat => chat.setBackground(url));
|
||||
return promises[promises.length - 1].then(() => {
|
||||
if(broadcastEvent) {
|
||||
@ -1133,6 +1166,8 @@ export class AppImManager {
|
||||
}
|
||||
|
||||
I18n.setTimeFormat(rootScope.settings.timeFormat);
|
||||
|
||||
this.toggleChatGradientAnimation(this.chat);
|
||||
};
|
||||
|
||||
// * не могу использовать тут TransitionSlider, так как мне нужен отрисованный блок рядом
|
||||
@ -1433,7 +1468,7 @@ export class AppImManager {
|
||||
);
|
||||
|
||||
if(this.chats.length) {
|
||||
chat.backgroundEl.append(this.chat.backgroundEl.lastElementChild.cloneNode(true));
|
||||
chat.setBackground(this.lastBackgroundUrl, true);
|
||||
}
|
||||
|
||||
this.chats.push(chat);
|
||||
@ -1556,7 +1591,10 @@ export class AppImManager {
|
||||
// * wait for cached render
|
||||
const promise = result?.cached ? result.promise : Promise.resolve();
|
||||
if(peerId) {
|
||||
promise.then(() => {
|
||||
Promise.all([
|
||||
promise,
|
||||
chat.setBackgroundPromise
|
||||
]).then(() => {
|
||||
//window.requestAnimationFrame(() => {
|
||||
setTimeout(() => { // * setTimeout is better here
|
||||
setTimeout(() => {
|
||||
|
@ -457,7 +457,8 @@ export class AppNotificationsManager {
|
||||
}
|
||||
}
|
||||
|
||||
this.checkMuteUntilTimeout = window.setTimeout(this.checkMuteUntil, (closestMuteUntil - timestamp) * 1000);
|
||||
const timeout = Math.min(1800e3, (closestMuteUntil - timestamp) * 1000);
|
||||
this.checkMuteUntilTimeout = window.setTimeout(this.checkMuteUntil, timeout);
|
||||
};
|
||||
|
||||
public savePeerSettings({key, peerId, settings}: {
|
||||
|
@ -33,11 +33,13 @@ const STATE_VERSION = App.version;
|
||||
const BUILD = App.build;
|
||||
|
||||
export type Background = {
|
||||
type: 'color' | 'image' | 'default',
|
||||
type?: 'color' | 'image' | 'default', // ! DEPRECATED
|
||||
blur: boolean,
|
||||
highlightningColor?: string,
|
||||
color?: string,
|
||||
slug?: string,
|
||||
color?: string,
|
||||
slug?: string, // image slug
|
||||
intensity?: number, // pattern intensity
|
||||
id: string | number, // wallpaper id
|
||||
};
|
||||
|
||||
export type Theme = {
|
||||
@ -186,18 +188,23 @@ export const STATE_INIT: State = {
|
||||
themes: [{
|
||||
name: 'day',
|
||||
background: {
|
||||
type: 'image',
|
||||
blur: false,
|
||||
slug: 'ByxGo2lrMFAIAAAAmkJxZabh8eM', // * new blurred camomile,
|
||||
highlightningColor: 'hsla(85.5319, 36.9171%, 40.402%, 0.4)'
|
||||
slug: 'pattern',
|
||||
color: '#dbddbb,#6ba587,#d5d88d,#88b884',
|
||||
highlightningColor: 'hsla(86.4, 43.846153%, 45.117647%, .4)',
|
||||
intensity: 50,
|
||||
id: '1'
|
||||
}
|
||||
}, {
|
||||
name: 'night',
|
||||
background: {
|
||||
type: 'color',
|
||||
blur: false,
|
||||
color: '#0f0f0f',
|
||||
highlightningColor: 'hsla(0, 0%, 3.82353%, 0.4)'
|
||||
slug: 'pattern',
|
||||
// color: '#dbddbb,#6ba587,#d5d88d,#88b884',
|
||||
color: '#fec496,#dd6cb9,#962fbf,#4f5bd5',
|
||||
highlightningColor: 'hsla(299.142857, 44.166666%, 37.470588%, .4)',
|
||||
intensity: -50,
|
||||
id: '-1'
|
||||
}
|
||||
}],
|
||||
theme: 'system',
|
||||
@ -506,6 +513,32 @@ export class AppStateManager extends EventListenerBase<{
|
||||
result.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// * migrate backgrounds (March 13, 2022; to version 1.3.0)
|
||||
if(compareVersion(state.version, '1.3.0') === -1) {
|
||||
let migrated = false;
|
||||
state.settings.themes.forEach((theme, idx, arr) => {
|
||||
if((
|
||||
theme.name === 'day' &&
|
||||
theme.background.slug === 'ByxGo2lrMFAIAAAAmkJxZabh8eM' &&
|
||||
theme.background.type === 'image'
|
||||
) || (
|
||||
theme.name === 'night' &&
|
||||
theme.background.color === '#0f0f0f' &&
|
||||
theme.background.type === 'color'
|
||||
)) {
|
||||
const newTheme = STATE_INIT.settings.themes.find(newTheme => newTheme.name === theme.name);
|
||||
if(newTheme) {
|
||||
arr[idx] = copy(newTheme);
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(migrated) {
|
||||
this.pushToState('settings', state.settings);
|
||||
}
|
||||
}
|
||||
|
||||
if(compareVersion(state.version, STATE_VERSION) !== 0) {
|
||||
this.newVersion = STATE_VERSION;
|
||||
|
@ -272,6 +272,12 @@ export class ApiFileManager {
|
||||
return cryptoWorker.invokeCrypto('gzipUncompress', bytes.slice().buffer, true) as Promise<string>;
|
||||
};
|
||||
|
||||
private uncompressTGV = (bytes: Uint8Array, fileName: string) => {
|
||||
//this.log('uncompressTGS', bytes, bytes.slice().buffer);
|
||||
// slice нужен потому что в uint8array - 5053 length, в arraybuffer - 5084
|
||||
return cryptoWorker.invokeCrypto('gzipUncompress', bytes.slice().buffer, true) as Promise<string>;
|
||||
};
|
||||
|
||||
private convertWebp = (bytes: Uint8Array, fileName: string) => {
|
||||
const convertPromise = deferredPromise<Uint8Array>();
|
||||
|
||||
@ -324,7 +330,10 @@ export class ApiFileManager {
|
||||
|
||||
let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp'];
|
||||
|
||||
if(options.mimeType === 'image/webp' && !isWebpSupported()) {
|
||||
if(options.mimeType === 'application/x-tgwallpattern') {
|
||||
process = this.uncompressTGV;
|
||||
options.mimeType = 'image/svg+xml';
|
||||
} else if(options.mimeType === 'image/webp' && !isWebpSupported()) {
|
||||
process = this.convertWebp;
|
||||
options.mimeType = 'image/png';
|
||||
} else if(options.mimeType === 'application/x-tgsticker') {
|
||||
|
@ -8,6 +8,7 @@ import blurActiveElement from "../helpers/dom/blurActiveElement";
|
||||
import loadFonts from "../helpers/dom/loadFonts";
|
||||
import appStateManager from "../lib/appManagers/appStateManager";
|
||||
import I18n from "../lib/langPack";
|
||||
import rootScope from "../lib/rootScope";
|
||||
import Page from "./page";
|
||||
|
||||
let onFirstMount = () => {
|
||||
@ -16,9 +17,7 @@ let onFirstMount = () => {
|
||||
// ! TOO SLOW
|
||||
/* appStateManager.saveState(); */
|
||||
|
||||
import('../lib/rootScope').then(m => {
|
||||
m.default.dispatchEvent('im_mount');
|
||||
});
|
||||
rootScope.dispatchEvent('im_mount');
|
||||
|
||||
if(!I18n.requestedServerLanguage) {
|
||||
I18n.getCacheLangPack().then(langPack => {
|
||||
@ -28,64 +27,33 @@ let onFirstMount = () => {
|
||||
});
|
||||
}
|
||||
|
||||
blurActiveElement();
|
||||
return loadFonts().then(() => {
|
||||
return new Promise<void>((resolve) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
// setTimeout(() => {
|
||||
const promise = import('../lib/appManagers/appDialogsManager');
|
||||
promise.finally(async() => {
|
||||
document.getElementById('auth-pages').remove();
|
||||
//alert('pageIm!');
|
||||
resolve();
|
||||
|
||||
//AudioContext && global.navigator && global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia && global.WebAssembly;
|
||||
|
||||
/* // @ts-ignore
|
||||
var AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
|
||||
alert('AudioContext:' + typeof(AudioContext));
|
||||
// @ts-ignore
|
||||
alert('global.navigator:' + typeof(navigator));
|
||||
alert('navigator.mediaDevices:' + typeof(navigator.mediaDevices));
|
||||
alert('navigator.mediaDevices.getUserMedia:' + typeof(navigator.mediaDevices?.getUserMedia));
|
||||
alert('global.WebAssembly:' + typeof(WebAssembly)); */
|
||||
|
||||
//(Array.from(document.getElementsByClassName('rp')) as HTMLElement[]).forEach(el => ripple(el));
|
||||
});
|
||||
// }, 5e3);
|
||||
});
|
||||
})
|
||||
});
|
||||
page.pageEl.style.display = '';
|
||||
|
||||
//let promise = /* Promise.resolve() */.then(() => {//import('../lib/services').then(services => {
|
||||
/* fetch('assets/img/camomile.jpg')
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
let img = new Image();
|
||||
let url = URL.createObjectURL(blob);
|
||||
img.src = url;
|
||||
img.onload = () => {
|
||||
let id = 'chat-background-canvas';
|
||||
var canvas = document.getElementById(id) as HTMLCanvasElement;
|
||||
//URL.revokeObjectURL(url);
|
||||
//alert('pageIm!');
|
||||
|
||||
let elements = ['.chat-container'].map(selector => {
|
||||
return document.querySelector(selector) as HTMLDivElement;
|
||||
});
|
||||
|
||||
stackBlurImage(img, id, 15, 0);
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
//let dataUrl = canvas.toDataURL('image/jpeg', 1);
|
||||
let dataUrl = URL.createObjectURL(blob);
|
||||
|
||||
elements.forEach(el => {
|
||||
el.style.backgroundImage = 'url(' + dataUrl + ')';
|
||||
});
|
||||
}, 'image/jpeg', 1);
|
||||
};
|
||||
}); */
|
||||
//});
|
||||
//AudioContext && global.navigator && global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia && global.WebAssembly;
|
||||
|
||||
/* // @ts-ignore
|
||||
var AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
|
||||
alert('AudioContext:' + typeof(AudioContext));
|
||||
// @ts-ignore
|
||||
alert('global.navigator:' + typeof(navigator));
|
||||
alert('navigator.mediaDevices:' + typeof(navigator.mediaDevices));
|
||||
alert('navigator.mediaDevices.getUserMedia:' + typeof(navigator.mediaDevices?.getUserMedia));
|
||||
alert('global.WebAssembly:' + typeof(WebAssembly)); */
|
||||
|
||||
//(Array.from(document.getElementsByClassName('rp')) as HTMLElement[]).forEach(el => ripple(el));
|
||||
|
||||
blurActiveElement();
|
||||
|
||||
return Promise.all([
|
||||
loadFonts()/* .then(() => new Promise((resolve) => window.requestAnimationFrame(resolve))) */,
|
||||
import('../lib/appManagers/appDialogsManager')
|
||||
]).then(() => {
|
||||
setTimeout(() => {
|
||||
document.getElementById('auth-pages').remove();
|
||||
}, 1e3);
|
||||
});
|
||||
};
|
||||
|
||||
const page = new Page('page-chats', false, onFirstMount);
|
||||
|
@ -669,10 +669,26 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
|
||||
}
|
||||
|
||||
&-item {
|
||||
background-image: url('assets/img/bg.jpeg');
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
background-color: inherit;
|
||||
&.is-image {
|
||||
background-image: url('assets/img/bg.jpeg');
|
||||
background-position: center center;
|
||||
background-color: inherit;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
&.is-pattern {
|
||||
margin: 0 !important;
|
||||
background-image: none !important;
|
||||
background-size: contain;
|
||||
background-repeat: repeat-x;
|
||||
background-color: #000 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// mix-blend-mode: overlay;
|
||||
height: 150%;
|
||||
top: -25%;
|
||||
}
|
||||
|
||||
@include animation-level(2) {
|
||||
transition: opacity var(--transition-standard-out);
|
||||
@ -698,6 +714,29 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
|
||||
transition: transform var(--transition-standard-in), opacity var(--transition-standard-in) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-canvas {
|
||||
--opacity-max: 1;
|
||||
opacity: var(--opacity-max);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-pattern-canvas {
|
||||
mix-blend-mode: overlay;
|
||||
// height: 100%;
|
||||
}
|
||||
|
||||
&-color-canvas {
|
||||
height: 100%;
|
||||
// transform: scale(1.5);
|
||||
// transform: scaleY(1.5);
|
||||
|
||||
// mask-repeat: round;
|
||||
|
||||
mask-size: cover;
|
||||
mask-position: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -358,8 +358,8 @@ ul.chatlist {
|
||||
height: 1.25rem;
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
border-radius: .1875rem;
|
||||
margin-top: -0.1875rem;
|
||||
border-radius: .125rem;
|
||||
margin-top: -0.125rem;
|
||||
margin-right: 0.375rem;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
@ -372,7 +372,7 @@ ul.chatlist {
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
line-height: 1;
|
||||
font-size: .625rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.media-photo {
|
||||
|
@ -1197,6 +1197,14 @@
|
||||
&-media {
|
||||
transition: transform .2s ease-in-out;
|
||||
transform: scale(1);
|
||||
|
||||
&.is-pattern {
|
||||
background-color: #000;
|
||||
|
||||
.media-photo {
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1210,6 +1218,14 @@
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.background-colors-canvas {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.background-image-container {
|
||||
|
@ -41,6 +41,7 @@ html:not(.is-safari):not(.is-ios) {
|
||||
max-height: 12.5rem;
|
||||
border-radius: $border-radius-medium;
|
||||
background-color: var(--scrollbar-color);
|
||||
backdrop-filter: blur(100);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user