Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
428 lines
12 KiB
428 lines
12 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko, Artem Kolnogorov and unknown creator of the script taken from http://useless.altervista.org/gradient.html |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import {animateSingle} from '../../helpers/animation'; |
|
import {hexToRgb} from '../../helpers/color'; |
|
import {easeOutQuadApply} from '../../helpers/easing/easeOutQuad'; |
|
|
|
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, 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; |
|
private _nextPositionTail: number; |
|
private _nextPositionTails: number; |
|
private _nextPositionLeft: number; |
|
|
|
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(); |
|
positions.push(...positions.splice(0, 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 changeTailAndDraw(diff: number) { |
|
this.changeTail(diff); |
|
const curPos = this.curPosition(this._phase, this._tail); |
|
this.drawGradient(curPos); |
|
} |
|
|
|
private drawOnWheel = () => { |
|
const value = this._scrollDelta / this._scrollTails; |
|
this._scrollDelta %= this._scrollTails; |
|
const diff = value > 0 ? Math.floor(value) : Math.ceil(value); |
|
if(diff) { |
|
this.changeTailAndDraw(diff); |
|
} |
|
this._onWheelRAF = undefined; |
|
}; |
|
|
|
private drawNextPositionAnimated = (getProgress?: () => number) => { |
|
let done: boolean, id: ImageData; |
|
if(getProgress) { |
|
const value = getProgress(); |
|
done = value >= 1; |
|
const transitionValue = easeOutQuadApply(value, 1); |
|
const nextPositionTail = this._nextPositionTail ?? 0; |
|
const tail = this._nextPositionTail = this._nextPositionTails * transitionValue; |
|
const diff = tail - nextPositionTail; |
|
if(diff) { |
|
this._nextPositionLeft -= diff; |
|
this.changeTailAndDraw(diff); |
|
} |
|
} else { |
|
const frames = this._frames; |
|
id = frames.shift(); |
|
done = !frames.length; |
|
} |
|
|
|
if(id) { |
|
this.drawImageData(id); |
|
} |
|
|
|
if(done) { |
|
this._nextPositionLeft = undefined; |
|
this._nextPositionTails = undefined; |
|
this._nextPositionTail = undefined; |
|
this._animatingToNextPosition = undefined; |
|
} |
|
|
|
return !done; |
|
}; |
|
|
|
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', {alpha: false}); |
|
} |
|
|
|
this._canvas = el; |
|
this._ctx = this._canvas.getContext('2d', {alpha: false}); |
|
this.update(); |
|
} |
|
|
|
private 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(getProgress?: () => number) { |
|
if(this._colors.length < 2) { |
|
return; |
|
} |
|
|
|
if(getProgress) { |
|
this._nextPositionLeft = this._tails + (this._nextPositionLeft ?? 0); |
|
this._nextPositionTails = this._nextPositionLeft; |
|
this._nextPositionTail = undefined; |
|
this._animatingToNextPosition = true; |
|
animateSingle(this.drawNextPositionAnimated.bind(this, getProgress), this); |
|
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; |
|
animateSingle(this.drawNextPositionAnimated, this); |
|
} |
|
|
|
// public toNextPositionThrottled = throttle(this.toNextPosition.bind(this), 100, true); |
|
|
|
public scrollAnimate(start?: boolean) { |
|
return; |
|
|
|
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}; |
|
} |
|
}
|
|
|