tweb/src/components/topbarWeave.ts

342 lines
9.8 KiB
TypeScript

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from:
* https://github.com/evgeny-nadymov/telegram-react
* Copyright (C) 2018 Evgeny Nadymov
* https://github.com/evgeny-nadymov/telegram-react/blob/master/LICENSE
*/
import GROUP_CALL_STATE from '../lib/calls/groupCallState';
import LineBlobDrawable from './lineBlobDrawable';
export class WeavingState {
public shader: (ctx: CanvasRenderingContext2D, left: number, top: number, right: number, bottom: number) => void;
constructor(public stateId: GROUP_CALL_STATE) {
this.createGradient(stateId);
}
public createGradient(stateId: GROUP_CALL_STATE) {
this.shader = (ctx, left, top, right, bottom) => {
ctx.fillStyle = WeavingState.getGradientFromType(ctx, stateId, left, top, right, bottom);
};
}
// Android colors
static getGradientFromType(ctx: CanvasRenderingContext2D, type: GROUP_CALL_STATE, x0: number, y0: number, x1: number, y1: number) {
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
if(type === GROUP_CALL_STATE.MUTED_BY_ADMIN) {
gradient.addColorStop(0, '#F05459');
gradient.addColorStop(.4, '#766EE9');
gradient.addColorStop(1, '#57A4FE');
} else if(type === GROUP_CALL_STATE.UNMUTED) {
gradient.addColorStop(0, '#52CE5D');
gradient.addColorStop(1, '#00B1C0');
} else if(type === GROUP_CALL_STATE.MUTED) {
gradient.addColorStop(0, '#0976E3');
gradient.addColorStop(1, '#2BCEFF');
} else if(type === GROUP_CALL_STATE.CONNECTING) {
gradient.addColorStop(0, '#8599aa');
gradient.addColorStop(1, '#8599aa');
}
return gradient;
}
update(height: number, width: number, dt: number, amplitude: number) {
// TODO: move gradient here
}
}
export default class TopbarWeave {
private focused: boolean;
private resizing: boolean;
private lastUpdateTime: number;
private amplitude: number;
private amplitude2: number;
private states: Map<GROUP_CALL_STATE, WeavingState>;
private previousState: WeavingState;
private currentState: WeavingState;
private progressToState: number;
private scale: number;
private left: number;
private top: number;
private right: number;
private bottom: number;
private mounted: boolean;
private media: MediaQueryList;
private container: HTMLDivElement;
private canvas: HTMLCanvasElement;
private resizeHandler: number;
private raf: number;
private lbd: LineBlobDrawable;
private lbd1: LineBlobDrawable;
private lbd2: LineBlobDrawable;
private animateToAmplitude: number;
private animateAmplitudeDiff: number;
private animateAmplitudeDiff2: number;
constructor() {
this.focused = true;
this.resizing = false;
this.lastUpdateTime = Date.now();
this.amplitude = 0.0;
this.amplitude2 = 0.0;
this.states = new Map([
[GROUP_CALL_STATE.UNMUTED, new WeavingState(GROUP_CALL_STATE.UNMUTED)],
[GROUP_CALL_STATE.MUTED, new WeavingState(GROUP_CALL_STATE.MUTED)],
[GROUP_CALL_STATE.MUTED_BY_ADMIN, new WeavingState(GROUP_CALL_STATE.MUTED_BY_ADMIN)],
[GROUP_CALL_STATE.CONNECTING, new WeavingState(GROUP_CALL_STATE.CONNECTING)]
]);
this.previousState = null;
this.currentState = this.states.get(GROUP_CALL_STATE.CONNECTING);
this.progressToState = 1.0;
}
public componentDidMount() {
if(this.mounted) {
return;
}
this.mounted = true;
// window.addEventListener('blur', this.handleBlur);
// window.addEventListener('focus', this.handleFocus);
window.addEventListener('resize', this.handleResize);
this.media = window.matchMedia('screen and (min-resolution: 2dppx)');
this.media.addEventListener('change', this.handleDevicePixelRatioChanged);
this.setSize();
this.forceUpdate();
this.lbd = new LineBlobDrawable(3);
this.lbd1 = new LineBlobDrawable(7);
this.lbd2 = new LineBlobDrawable(8);
this.setAmplitude(this.amplitude);
this.draw();
}
public componentWillUnmount() {
this.mounted = false;
// window.removeEventListener('blur', this.handleBlur);
// window.removeEventListener('focus', this.handleFocus);
window.removeEventListener('resize', this.handleResize);
this.media.addEventListener('change', this.handleDevicePixelRatioChanged);
const {canvas} = this;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
private setSize() {
this.scale = window.devicePixelRatio;
this.top = 20 * this.scale;
this.right = (this.mounted ? this.container.offsetWidth : 1261) * this.scale;
this.bottom = (this.mounted ? this.container.offsetHeight : 68) * this.scale;
this.left = 0 * this.scale;
this.setCanvasSize();
}
private setCanvasSize() {
this.canvas.width = this.right;
this.canvas.height = this.bottom;
}
private handleDevicePixelRatioChanged = (e: Event) => {
this.setSize();
this.forceUpdate();
}
private handleResize = () => {
if(this.resizeHandler) {
clearTimeout(this.resizeHandler);
this.resizeHandler = null;
}
this.resizing = true;
this.resizeCanvas();
this.resizeHandler = window.setTimeout(() => {
this.resizing = false;
this.invokeDraw();
}, 250);
}
private resizeCanvas() {
this.scale = window.devicePixelRatio;
this.right = this.container.offsetWidth * this.scale;
this.forceUpdate();
this.invokeDraw();
}
public handleFocus = () => {
this.focused = true;
this.invokeDraw();
}
public handleBlur = () => {
this.focused = false;
}
private invokeDraw = () => {
if(this.raf) return;
this.draw();
}
private draw = (force = false) => {
this.raf = null;
if(!this.mounted) {
return;
}
const {lbd, lbd1, lbd2, scale, left, top, right, bottom, currentState, previousState, focused, resizing, canvas} = this;
if(!focused && !resizing && this.progressToState >= 1.0) {
return;
}
// console.log('[top] draw', [focused, resizing, this.mounted]);
const newTime = Date.now();
let dt = (newTime - this.lastUpdateTime);
if(dt > 20) {
dt = 17;
}
// console.log('draw start', this.amplitude, this.animateToAmplitude);
if(this.animateToAmplitude !== this.amplitude) {
this.amplitude += this.animateAmplitudeDiff * dt;
if(this.animateAmplitudeDiff > 0) {
if(this.amplitude > this.animateToAmplitude) {
this.amplitude = this.animateToAmplitude;
}
} else {
if(this.amplitude < this.animateToAmplitude) {
this.amplitude = this.animateToAmplitude;
}
}
}
if(this.animateToAmplitude !== this.amplitude2) {
this.amplitude2 += this.animateAmplitudeDiff2 * dt;
if(this.animateAmplitudeDiff2 > 0) {
if(this.amplitude2 > this.animateToAmplitude) {
this.amplitude2 = this.animateToAmplitude;
}
} else {
if(this.amplitude2 < this.animateToAmplitude) {
this.amplitude2 = this.animateToAmplitude;
}
}
}
if(previousState) {
this.progressToState += dt / 250;
if(this.progressToState > 1) {
this.progressToState = 1;
this.previousState = null;
}
}
const {amplitude, amplitude2, progressToState} = this;
const top1 = 6 * amplitude2 * scale;
const top2 = 6 * amplitude2 * scale;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
lbd.minRadius = 0;
lbd.maxRadius = (2 + 2 * amplitude) * scale;
lbd1.minRadius = 0;
lbd1.maxRadius = (3 + 9 * amplitude) * scale;
lbd2.minRadius = 0;
lbd2.maxRadius = (3 + 9 * amplitude) * scale;
lbd.update(amplitude, 0.3);
lbd1.update(amplitude, 0.7);
lbd2.update(amplitude, 0.7);
for(let i = 0; i < 2; i++) {
if(i === 0 && !previousState) {
continue;
}
let alpha = 1;
let state: WeavingState = null;
if(i === 0) {
alpha = 1 - progressToState;
state = previousState;
// previousState.setToPaint(paint);
} else {
alpha = previousState ? progressToState : 1;
currentState.update(bottom - top, right - left, dt, amplitude);
state = currentState;
// currentState.setToPaint(paint);
}
const paint1 = (ctx: CanvasRenderingContext2D) => {
ctx.globalAlpha = 0.3 * alpha;
state.shader(ctx, left, top, right, bottom);
};
const paint = (ctx: CanvasRenderingContext2D) => {
ctx.globalAlpha = i === 0 ? 1 : alpha;
state.shader(ctx, left, top, right, bottom);
};
lbd1.draw(left, top - top1, right, bottom, canvas, paint1, top, 1.0);
lbd2.draw(left, top - top2, right, bottom, canvas, paint1, top, 1.0);
lbd.draw(left, top, right, bottom, canvas, paint, top, 1.0);
}
if(!force) {
this.raf = requestAnimationFrame(() => this.draw());
}
};
public setCurrentState = (stateId: GROUP_CALL_STATE, animated: boolean) => {
const {currentState, states} = this;
if(currentState?.stateId === stateId) {
return;
}
this.previousState = animated ? currentState : null;
this.currentState = states.get(stateId);
this.progressToState = this.previousState ? 0.0 : 1.0;
};
public setAmplitude(value: number) {
const {amplitude} = this;
this.animateToAmplitude = value;
this.animateAmplitudeDiff = (value - amplitude) / 250;
this.animateAmplitudeDiff2 = (value - amplitude) / 120;
}
private forceUpdate() {
this.setCanvasSize();
}
public render(className: string) {
const container = this.container = document.createElement('div');
container.classList.add(className);
const canvas = this.canvas = document.createElement('canvas');
canvas.classList.add(className + '-canvas');
container.append(canvas);
return container;
}
}