diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index e06c686e..d75f8c94 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -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) => { diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index a8ca5dde..7c2ac709 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -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; 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 { + public setBackground(url: string, skipAnimation?: boolean): Promise { 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((resolve) => { + if(patternRenderer) { + const setOpacityTo = isDarkPattern ? gradientCanvas : patternCanvas; + setOpacityTo.style.setProperty('--opacity-max', '' + Math.abs(intensity)); + } + + const promise = new Promise((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; + 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; diff --git a/src/components/chat/gradientRenderer.ts b/src/components/chat/gradientRenderer.ts new file mode 100644 index 00000000..9f4ed8ae --- /dev/null +++ b/src/components/chat/gradientRenderer.ts @@ -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}; + } +} diff --git a/src/components/chat/patternRenderer.ts b/src/components/chat/patternRenderer.ts new file mode 100644 index 00000000..61fa1551 --- /dev/null +++ b/src/components/chat/patternRenderer.ts @@ -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; + private createCanvasPatternPromise: Promise; + private exportCanvasPatternToImagePromise: Promise; + // 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((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; + } +} diff --git a/src/components/sidebarLeft/tabs/background.ts b/src/components/sidebarLeft/tabs/background.ts index 287a83b0..eb3962f3 100644 --- a/src/components/sidebarLeft/tabs/background.ts +++ b/src/components/sidebarLeft/tabs/background.ts @@ -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 = new Set(); private blurCheckboxField: CheckboxField; + private wallpapersByElement: Map = new Map(); + private elementsByKey: Map = 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, 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(); - deferred.addNotifyListener = download.addNotifyListener; - deferred.cancel = download.cancel; + let download: Promise | 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; + 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; } diff --git a/src/components/sidebarLeft/tabs/backgroundColor.ts b/src/components/sidebarLeft/tabs/backgroundColor.ts index b78d4834..36561430 100644 --- a/src/components/sidebarLeft/tabs/backgroundColor.ts +++ b/src/components/sidebarLeft/tabs/backgroundColor.ts @@ -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); diff --git a/src/helpers/averageColor.ts b/src/helpers/averageColor.ts index d7ad0bbf..699fe21d 100644 --- a/src/helpers/averageColor.ts +++ b/src/helpers/averageColor.ts @@ -6,7 +6,28 @@ import renderImageFromUrl from "./dom/renderImageFromUrl"; -export const averageColor = (imageUrl: string): Promise => { +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((resolve) => { renderImageFromUrl(img, imageUrl, () => { @@ -25,23 +46,7 @@ export const averageColor = (imageUrl: string): Promise => { 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)); }); }); }; diff --git a/src/helpers/blob/blobSafeMimeType.ts b/src/helpers/blob/blobSafeMimeType.ts index aacff84e..36d1bb12 100644 --- a/src/helpers/blob/blobSafeMimeType.ts +++ b/src/helpers/blob/blobSafeMimeType.ts @@ -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', diff --git a/src/helpers/color.ts b/src/helpers/color.ts index b502090d..7766cb09 100644 --- a/src/helpers/color.ts +++ b/src/helpers/color.ts @@ -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)); diff --git a/src/helpers/dom/renderImageFromUrl.ts b/src/helpers/dom/renderImageFromUrl.ts index 8571e644..5078273c 100644 --- a/src/helpers/dom/renderImageFromUrl.ts +++ b/src/helpers/dom/renderImageFromUrl.ts @@ -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(); + }); } } } diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index df6cc80c..77337141 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -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} = {}; + private backgroundPromises: {[slug: string]: Promise}; 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 { + 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(() => { diff --git a/src/lib/appManagers/appNotificationsManager.ts b/src/lib/appManagers/appNotificationsManager.ts index d3b3968e..d5a47b33 100644 --- a/src/lib/appManagers/appNotificationsManager.ts +++ b/src/lib/appManagers/appNotificationsManager.ts @@ -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}: { diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index 3d05a138..c7820088 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -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 = { @@ -185,18 +187,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', @@ -504,6 +511,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; diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index 13424942..e190b123 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -272,6 +272,12 @@ export class ApiFileManager { return cryptoWorker.invokeCrypto('gzipUncompress', bytes.slice().buffer, true) as Promise; }; + 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; + }; + private convertWebp = (bytes: Uint8Array, fileName: string) => { const convertPromise = deferredPromise(); @@ -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') { diff --git a/src/pages/pageIm.ts b/src/pages/pageIm.ts index acc2f6c8..235df455 100644 --- a/src/pages/pageIm.ts +++ b/src/pages/pageIm.ts @@ -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((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); - - 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); + //alert('pageIm!'); - 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); diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index ae47d1cd..71e18a52 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -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; + } } } diff --git a/src/scss/partials/_chatlist.scss b/src/scss/partials/_chatlist.scss index cde13792..3f772e3b 100644 --- a/src/scss/partials/_chatlist.scss +++ b/src/scss/partials/_chatlist.scss @@ -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 { diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index d887deef..ebde58c5 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -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 { diff --git a/src/scss/partials/_scrollable.scss b/src/scss/partials/_scrollable.scss index f1af90a0..0c021e82 100644 --- a/src/scss/partials/_scrollable.scss +++ b/src/scss/partials/_scrollable.scss @@ -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; } }