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