|
|
|
/*
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
}
|