/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; import deepEqual from "../../helpers/object/deepEqual"; import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl"; import mediaSizes, { ScreenSize } from "../../helpers/mediaSizes"; type ChatBackgroundPatternRendererInitOptions = { url: string, width: number, height: number, mask?: boolean }; export default class ChatBackgroundPatternRenderer { private static INSTANCES: ChatBackgroundPatternRenderer[] = []; // private pattern: CanvasPattern; private objectUrl: string; private options: ChatBackgroundPatternRendererInitOptions; private canvases: Set; // private createCanvasPatternPromise: Promise; // private exportCanvasPatternToImagePromise: Promise; private renderImageFromUrlPromise: Promise; private img: HTMLImageElement; constructor() { this.canvases = new Set(); } public static getInstance(options: ChatBackgroundPatternRendererInitOptions) { let instance = this.INSTANCES.find((instance) => { return deepEqual(instance.options, options); }); if(!instance) { instance = new ChatBackgroundPatternRenderer(); instance.init(options); this.INSTANCES.push(instance); } return instance; } public init(options: ChatBackgroundPatternRendererInitOptions) { // if(this.options) { // if(this.options.width !== options.width || this.options.height !== options.height) { // this.createCanvasPatternPromise = // this.pattern = // this.exportCanvasPatternToImagePromise = // undefined; // } // } this.options = options; } public renderToCanvas(canvas: HTMLCanvasElement) { // return this.createCanvasPattern(canvas).then(() => { // return this.fillCanvas(canvas); // }); return this.renderImageFromUrl(this.options.url).then(() => { return this.fillCanvas(canvas); }); } private renderImageFromUrl(url: string) { if(this.renderImageFromUrlPromise) return this.renderImageFromUrlPromise; const img = this.img = document.createElement('img'); img.crossOrigin = 'anonymous'; return this.renderImageFromUrlPromise = renderImageFromUrlPromise(img, url, false).then(() => img); } /* private createCanvasPattern(canvas: HTMLCanvasElement) { if(this.createCanvasPatternPromise) return this.createCanvasPatternPromise; return this.createCanvasPatternPromise = this.renderImageFromUrl(this.options.url).then((img) => { let createPatternFrom: HTMLImageElement | HTMLCanvasElement; if(IS_SAFARI) { const canvas = createPatternFrom = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); } else { createPatternFrom = img; } const perf = performance.now(); this.pattern = canvas.getContext('2d').createPattern(createPatternFrom, 'repeat-x'); console.warn('creating pattern time:', performance.now() - perf); return this.pattern; }); } public exportCanvasPatternToImage(canvas: HTMLCanvasElement) { if(this.exportCanvasPatternToImagePromise) return this.exportCanvasPatternToImagePromise; return this.exportCanvasPatternToImagePromise = new Promise((resolve) => { canvas.toBlob((blob) => { const newUrl = this.objectUrl = URL.createObjectURL(blob); resolve(newUrl); }, 'image/png'); }); } */ public cleanup(canvas: HTMLCanvasElement) { this.canvases.delete(canvas); if(!this.canvases.size) { indexOfAndSplice(ChatBackgroundPatternRenderer.INSTANCES, this); if(this.objectUrl) { URL.revokeObjectURL(this.objectUrl); } } } public fillCanvas(canvas: HTMLCanvasElement) { const context = canvas.getContext('2d'); const {width, height} = canvas; // const perf = performance.now(); // if(context.fillStyle instanceof CanvasPattern) { // context.clearRect(0, 0, width, height); // } const img = this.img; let imageWidth = img.width, imageHeight = img.height; // if(imageHeight < height) { let patternHeight = 2960; // * correct // if(+canvas.dataset.originalHeight !== height) hhh *= 2 / 3; // * but have to make it good if(+canvas.dataset.originalHeight !== height) patternHeight *= .875; const ratio = patternHeight / imageHeight; imageWidth *= ratio; imageHeight = patternHeight; // } if(this.options.mask) { context.fillStyle = '#000'; context.fillRect(0, 0, width, height); context.globalCompositeOperation = 'destination-out'; } else { context.globalCompositeOperation = 'source-over'; } const d = (y: number) => { for(let x = 0; x < width; x += imageWidth) { context.drawImage(img, x, y, imageWidth, imageHeight); } }; const centerY = height / 2 - imageHeight / 2; d(centerY); if(centerY > 0) { let topY = centerY; do { d(topY -= imageHeight); } while(topY >= 0); } const endY = height - 1; for(let bottomY = centerY + imageHeight; bottomY < endY; bottomY += imageHeight) { d(bottomY); } // for(let x = 0; x < width; x += imageWidth) { // for(let y = 0; y < height; y += imageHeight) { // context.drawImage(img, x, y, imageWidth, imageHeight); // } // } // context.fillStyle = this.pattern; // context.fillRect(0, 0, width, height); // console.warn('fill canvas time', performance.now() - perf); } public setCanvasDimensions(canvas: HTMLCanvasElement) { const devicePixelRatio = Math.min(2, window.devicePixelRatio); let width = this.options.width * devicePixelRatio, height = this.options.height * devicePixelRatio; canvas.dataset.originalHeight = '' + height; if(mediaSizes.activeScreen === ScreenSize.large) height *= 1.5; canvas.width = width; canvas.height = height; } public createCanvas() { const canvas = document.createElement('canvas'); this.canvases.add(canvas); this.setCanvasDimensions(canvas); return canvas; } public resize(width: number, height: number) { this.init({ ...this.options, width, height }); const promises: Promise[] = []; for(const canvas of this.canvases) { this.setCanvasDimensions(canvas); promises.push(this.renderToCanvas(canvas)); } return Promise.all(promises); } public static resizeInstances(width: number, height: number) { return Promise.all(this.INSTANCES.map((instance) => instance.resize(width, height))); } /* public setResizeMode(resizing: boolean) { const canvases = Array.from(this.canvases); const canvas = canvases[canvases.length - 1]; canvas.style.display = resizing ? 'none' : ''; const img = this.img; img.style.display = resizing ? '' : 'none'; return {img, canvas}; } */ }