326 lines
9.2 KiB
TypeScript
326 lines
9.2 KiB
TypeScript
/*
|
|
* https://github.com/morethanwords/tweb
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
*/
|
|
|
|
import Scrollable from '../components/scrollable';
|
|
import rootScope from '../lib/rootScope';
|
|
import {animate} from './animation';
|
|
import {drawCircleFromStart} from './canvas/drawCircle';
|
|
import roundRect from './canvas/roundRect';
|
|
import Shimmer from './canvas/shimmer';
|
|
import customProperties from './dom/customProperties';
|
|
import easeInOutSine from './easing/easeInOutSine';
|
|
import liteMode from './liteMode';
|
|
import mediaSizes from './mediaSizes';
|
|
|
|
export default class DialogsPlaceholder {
|
|
private canvas: HTMLCanvasElement;
|
|
private ctx: CanvasRenderingContext2D;
|
|
private shimmer: Shimmer;
|
|
private tempId: number;
|
|
private detachTime: number;
|
|
|
|
private length: number;
|
|
private dialogHeight: number;
|
|
private availableLength: number;
|
|
|
|
private avatarSize: number;
|
|
private marginVertical: number;
|
|
private totalHeight: number;
|
|
private lineHeight: number;
|
|
private lineBorderRadius: number;
|
|
private lineMarginVertical: number;
|
|
private statusWidth: number;
|
|
private generatedValues: {
|
|
firstLineWidth: number,
|
|
secondLineWidth: number,
|
|
statusWidth: number
|
|
}[];
|
|
|
|
private getRectFrom: () => DOMRectEditable;
|
|
private onRemove: () => void;
|
|
private blockScrollable: Scrollable;
|
|
|
|
constructor(sizes: Partial<{
|
|
avatarSize: number,
|
|
marginVertical: number,
|
|
totalHeight: number
|
|
}> = {}) {
|
|
this.shimmer = new Shimmer();
|
|
this.tempId = 0;
|
|
this.canvas = document.createElement('canvas');
|
|
this.canvas.classList.add('dialogs-placeholder-canvas');
|
|
this.ctx = this.canvas.getContext('2d');
|
|
|
|
this.generatedValues = [];
|
|
this.avatarSize = sizes.avatarSize ?? 54;
|
|
this.marginVertical = sizes.marginVertical ?? 9;
|
|
this.totalHeight = sizes.totalHeight ?? (this.avatarSize + this.marginVertical * 2);
|
|
this.lineHeight = 10;
|
|
this.lineBorderRadius = 6;
|
|
this.lineMarginVertical = 8;
|
|
this.statusWidth = 24;
|
|
}
|
|
|
|
public attach({container, rect, getRectFrom, onRemove, blockScrollable}: {
|
|
container: HTMLElement,
|
|
rect?: ReturnType<DialogsPlaceholder['getRectFrom']>,
|
|
getRectFrom?: HTMLElement | DialogsPlaceholder['getRectFrom'],
|
|
onRemove?: DialogsPlaceholder['onRemove'],
|
|
blockScrollable?: DialogsPlaceholder['blockScrollable']
|
|
}) {
|
|
const {canvas} = this;
|
|
|
|
this.onRemove = onRemove;
|
|
this.getRectFrom = typeof(getRectFrom) === 'function' ? getRectFrom : (getRectFrom || container).getBoundingClientRect.bind(getRectFrom || container);
|
|
if(this.blockScrollable = blockScrollable) {
|
|
blockScrollable.container.style.overflowY = 'hidden';
|
|
}
|
|
|
|
this.updateCanvasSize(rect);
|
|
this.startAnimation();
|
|
container.append(canvas);
|
|
}
|
|
|
|
public detach(availableLength: number) {
|
|
if(this.detachTime) {
|
|
return;
|
|
}
|
|
|
|
this.availableLength = availableLength;
|
|
this.detachTime = Date.now();
|
|
|
|
if(!liteMode.isAvailable('animations')) {
|
|
this.remove();
|
|
}
|
|
}
|
|
|
|
public remove() {
|
|
this.stopAnimation();
|
|
|
|
if(this.canvas.parentElement) {
|
|
this.canvas.remove();
|
|
|
|
if(this.blockScrollable) {
|
|
this.blockScrollable.container.style.overflowY = '';
|
|
this.blockScrollable = undefined;
|
|
}
|
|
}
|
|
|
|
this.onRemove?.();
|
|
this.onRemove = undefined;
|
|
}
|
|
|
|
private updateCanvasSize(rect = this.getRectFrom()) {
|
|
const {canvas} = this;
|
|
const dpr = canvas.dpr = window.devicePixelRatio;
|
|
canvas.width = rect.width * dpr;
|
|
canvas.height = rect.height * dpr;
|
|
canvas.style.width = rect.width + 'px';
|
|
canvas.style.height = rect.height + 'px';
|
|
}
|
|
|
|
private renderDetachAnimationFrame() {
|
|
const {
|
|
canvas,
|
|
ctx,
|
|
detachTime,
|
|
length,
|
|
availableLength
|
|
} = this;
|
|
|
|
if(!detachTime) {
|
|
return;
|
|
} else if(!liteMode.isAvailable('animations')) {
|
|
this.remove();
|
|
return;
|
|
}
|
|
|
|
const {width} = canvas;
|
|
|
|
ctx.globalCompositeOperation = 'destination-out';
|
|
|
|
// ctx.fillStyle = 'rgba(0, 0, 0, 0)';
|
|
// ctx.fillRect(0, 0, width, height);
|
|
|
|
// const DURATION = 500;
|
|
// const DELAY = DURATION;
|
|
const DURATION = 150;
|
|
const DELAY = 15;
|
|
const elapsedTime = Date.now() - detachTime;
|
|
let completed = true;
|
|
for(let i = 0; i < length; ++i) {
|
|
const delay = availableLength < length && i >= availableLength ? DELAY * (availableLength - 1) : DELAY * i;
|
|
const elapsedRowTime = elapsedTime - delay;
|
|
if(elapsedRowTime <= 0) {
|
|
completed = false;
|
|
continue;
|
|
}
|
|
|
|
const progress = easeInOutSine(elapsedRowTime, 0, 1, DURATION);
|
|
|
|
ctx.beginPath();
|
|
ctx.rect(0, this.dialogHeight * i, width, this.dialogHeight);
|
|
ctx.fillStyle = `rgba(0, 0, 0, ${progress})`;
|
|
ctx.fill();
|
|
|
|
if(progress < 1) {
|
|
completed = false;
|
|
}
|
|
}
|
|
|
|
// const totalRadius = Math.sqrt(width ** 2 + height ** 2);
|
|
// const gradient = ctx.createRadialGradient(
|
|
// 0, 0, 0,
|
|
// 0, 0, totalRadius);
|
|
// gradient.addColorStop(0, 'rgba(0, 0, 0, 1)');
|
|
// gradient.addColorStop(progress, 'rgba(0, 0, 0, 0)');
|
|
// gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
|
|
// const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
// gradient.addColorStop(0, 'rgba(0, 0, 0, 1)');
|
|
// gradient.addColorStop(progress, 'rgba(0, 0, 0, 0)');
|
|
// gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
|
|
// ctx.fillStyle = gradient;
|
|
// ctx.fillRect(0, 0, width, height);
|
|
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
|
|
if(completed) {
|
|
this.remove();
|
|
}
|
|
}
|
|
|
|
private renderFrame() {
|
|
this.shimmer.on();
|
|
this.renderDetachAnimationFrame();
|
|
}
|
|
|
|
private startAnimation() {
|
|
const {canvas, shimmer} = this;
|
|
const tempId = ++this.tempId;
|
|
const pattern = this.createPattern();
|
|
|
|
shimmer.settings({
|
|
canvas,
|
|
fillStyle: pattern
|
|
});
|
|
|
|
const middleware = () => {
|
|
return this.tempId === tempId;
|
|
};
|
|
|
|
this.renderFrame();
|
|
animate(() => {
|
|
if(!middleware()) {
|
|
return false;
|
|
}
|
|
|
|
// ! should've removed the loop if animations are disabled
|
|
if(liteMode.isAvailable('animations')) {
|
|
this.renderFrame();
|
|
}
|
|
|
|
// ! tempId can be changed during renderFrame
|
|
return middleware();
|
|
});
|
|
|
|
rootScope.addEventListener('theme_change', this.onThemeChange);
|
|
mediaSizes.addEventListener('resize', this.onResize);
|
|
}
|
|
|
|
private stopAnimation() {
|
|
++this.tempId;
|
|
rootScope.removeEventListener('theme_change', this.onThemeChange);
|
|
mediaSizes.removeEventListener('resize', this.onResize);
|
|
}
|
|
|
|
private onThemeChange = () => {
|
|
this.stopAnimation();
|
|
this.startAnimation();
|
|
};
|
|
|
|
private onResize = () => {
|
|
const {canvas} = this;
|
|
const {width, height, dpr} = canvas;
|
|
this.updateCanvasSize();
|
|
if(canvas.width === width && canvas.height === height && canvas.dpr === dpr) {
|
|
return;
|
|
}
|
|
|
|
this.stopAnimation();
|
|
this.startAnimation();
|
|
};
|
|
|
|
private createPattern() {
|
|
const {canvas, ctx} = this;
|
|
|
|
const patternCanvas = document.createElement('canvas');
|
|
const patternContext = patternCanvas.getContext('2d');
|
|
const dpr = canvas.dpr;
|
|
patternCanvas.dpr = dpr;
|
|
patternCanvas.width = canvas.width;
|
|
patternCanvas.height = canvas.height;
|
|
|
|
patternContext.fillStyle = customProperties.getProperty('surface-color');
|
|
patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height);
|
|
|
|
patternContext.fillStyle = '#000';
|
|
patternContext.globalCompositeOperation = 'destination-out';
|
|
|
|
const dialogHeight = this.dialogHeight = this.totalHeight * dpr;
|
|
const length = this.length = Math.ceil(canvas.height / dialogHeight);
|
|
for(let i = 0; i < length; ++i) {
|
|
this.drawChat(patternContext, i, i * dialogHeight);
|
|
}
|
|
|
|
return ctx.createPattern(patternCanvas, 'no-repeat');
|
|
}
|
|
|
|
private drawChat(ctx: CanvasRenderingContext2D, i: number, y: number) {
|
|
let generatedValues = this.generatedValues[i];
|
|
if(!generatedValues) {
|
|
generatedValues = this.generatedValues[i] = {
|
|
firstLineWidth: 40 + Math.random() * 100, // 120
|
|
secondLineWidth: 120 + Math.random() * 130, // 240
|
|
statusWidth: 24 + Math.random() * 16
|
|
};
|
|
}
|
|
|
|
const {
|
|
firstLineWidth,
|
|
secondLineWidth,
|
|
statusWidth
|
|
} = generatedValues;
|
|
|
|
const {canvas} = ctx;
|
|
const {dpr} = canvas;
|
|
y /= dpr;
|
|
|
|
const {
|
|
avatarSize,
|
|
marginVertical,
|
|
lineHeight,
|
|
lineBorderRadius,
|
|
lineMarginVertical
|
|
} = this;
|
|
|
|
let marginLeft = 17;
|
|
|
|
if(avatarSize) {
|
|
drawCircleFromStart(ctx, marginLeft, y + marginVertical, avatarSize / 2, true);
|
|
marginLeft += avatarSize + 10;
|
|
}
|
|
|
|
// 9 + 54 - 10 - 8 = 45 ........ 72 - 9 - 10 - 8
|
|
roundRect(ctx, marginLeft, y + marginVertical + lineMarginVertical, firstLineWidth, lineHeight, lineBorderRadius, true);
|
|
// roundRect(ctx, marginLeft, y + marginVertical + avatarSize - lineHeight - lineMarginVertical, secondLineWidth, lineHeight, lineBorderRadius, true);
|
|
roundRect(ctx, marginLeft, y + this.totalHeight - marginVertical - lineHeight - lineMarginVertical, secondLineWidth, lineHeight, lineBorderRadius, true);
|
|
|
|
roundRect(ctx, canvas.width / dpr - 24 - statusWidth, y + marginVertical + lineMarginVertical, statusWidth, lineHeight, lineBorderRadius, true);
|
|
}
|
|
}
|