Blur on images

Preloader with attach & detach animation
Fixed preloader layout
This commit is contained in:
morethanwords 2021-01-10 20:10:02 +04:00
parent 5b0f0286d9
commit 09ca70ecb7
19 changed files with 628 additions and 342 deletions

View File

@ -880,6 +880,10 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
//const maxWidth = appPhotosManager.windowW - 16;
const maxWidth = mediaSizes.isMobile ? this.pageEl.scrollWidth : this.pageEl.scrollWidth - 16;
const maxHeight = appPhotosManager.windowH - 100;
const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(media);
if(gotThumb) {
container.append(gotThumb.image);
}
const size = appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight);
// need after setAttachmentSize

View File

@ -20,7 +20,7 @@ import LazyLoadQueue from "./lazyLoadQueue";
import { renderImageFromUrl, putPreloader, formatPhoneNumber } from "./misc";
import { ripple } from "./ripple";
import Scrollable, { ScrollableX } from "./scrollable";
import { wrapDocument } from "./wrappers";
import { wrapDocument, wrapPhoto, wrapVideo } from "./wrappers";
const testScroll = false;
@ -351,99 +351,36 @@ export default class AppSearchSuper {
div.classList.add('grid-item');
//this.log(message, photo);
const isPhoto = media._ === 'photo';
const photo = isPhoto ? appPhotosManager.getPhoto(media.id) : null;
let isDownloaded: boolean;
if(photo) {
isDownloaded = photo.downloaded > 0;
let wrapped: ReturnType<typeof wrapPhoto>;
if(media._ !== 'photo') {
wrapped = wrapVideo({
doc: media,
message,
container: div,
boxWidth: 0,
boxHeight: 0,
lazyLoadQueue: this.lazyLoadQueue,
middleware,
onlyPreview: true,
withoutPreloader: true
}).thumb;
} else {
const cachedThumb = appPhotosManager.getDocumentCachedThumb(media.id);
isDownloaded = cachedThumb?.downloaded > 0;
}
//this.log('inputMessagesFilterPhotoVideo', message, media);
if(!isPhoto) {
const span = document.createElement('span');
span.classList.add('video-time');
div.append(span);
if(media.type !== 'gif') {
span.innerText = (media.duration + '').toHHMMSS(false);
/* const spanPlay = document.createElement('span');
spanPlay.classList.add('video-play', 'tgico-largeplay', 'btn-circle', 'position-center');
div.append(spanPlay); */
} else {
span.innerText = 'GIF';
}
}
const load = () => appPhotosManager.preloadPhoto(isPhoto ? media.id : media, appPhotosManager.choosePhotoSize(media, 200, 200))
.then(() => {
if(!middleware()) {
//this.log.warn('peer changed');
return;
}
const url = (photo && photo.url) || appPhotosManager.getDocumentCachedThumb(media.id).url;
if(url) {
//if(needBlur) return;
const needBlurCallback = needBlur ? () => {
//void img.offsetLeft; // reflow
img.style.opacity = '';
if(thumb) {
window.setTimeout(() => {
thumb.remove();
}, 200);
}
} : undefined;
renderImageFromUrl(img, url, needBlurCallback);
}
});
let thumb: HTMLImageElement;
const sizes = media.sizes || media.thumbs;
const willHaveThumb = !isDownloaded && sizes && sizes[0].bytes;
if(willHaveThumb) {
thumb = new Image();
thumb.classList.add('grid-item-media', 'thumbnail');
thumb.dataset.mid = '' + message.mid;
appPhotosManager.setAttachmentPreview(sizes[0].bytes, thumb, false, false);
div.append(thumb);
}
const needBlur = (!isDownloaded || !willHaveThumb) && rootScope.settings.animationsEnabled;
const img = new Image();
img.dataset.mid = '' + message.mid;
img.classList.add('grid-item-media');
if(needBlur) img.style.opacity = '0';
div.append(img);
if(isDownloaded || willHaveThumb) {
const promise = new Promise<void>((resolve, reject) => {
(thumb || img).addEventListener('load', () => {
clearTimeout(timeout);
resolve();
});
const timeout = setTimeout(() => {
//this.log('didn\'t load', thumb, media, isDownloaded, sizes);
reject();
}, 1e3);
wrapped = wrapPhoto({
photo: media,
message,
container: div,
boxWidth: 0,
boxHeight: 0,
lazyLoadQueue: this.lazyLoadQueue,
middleware,
withoutPreloader: true
});
promises.push(promise);
}
if(sizes?.length) {
if(isDownloaded) load();
else this.lazyLoadQueue.push({div, load});
}
wrapped.images.thumb && wrapped.images.thumb.classList.add('grid-item-media');
wrapped.images.full && wrapped.images.full.classList.add('grid-item-media');
promises.push(wrapped.loadPromises.thumb);
elemsToAppend.push({element: div, message});
}

View File

@ -44,6 +44,7 @@ import useHeavyAnimationCheck, { getHeavyAnimationPromise, dispatchHeavyAnimatio
import { fastRaf } from "../../helpers/schedulers";
import { deferredPromise, CancellablePromise } from "../../helpers/cancellablePromise";
const USE_MEDIA_TAILS = false;
const IGNORE_ACTIONS = ['messageActionHistoryClear'];
const TEST_SCROLL_TIMES: number = undefined;
@ -1392,7 +1393,7 @@ export default class ChatBubbles {
this.chatInner.classList.toggle('is-channel', isChannel);
}
public renderMessagesQueue(message: any, bubble: HTMLDivElement, reverse: boolean) {
public renderMessagesQueue(message: any, bubble: HTMLDivElement, reverse: boolean, promises: Promise<any>[]) {
/* let dateMessage = this.getDateContainerByMessage(message, reverse);
if(reverse) dateMessage.container.insertBefore(bubble, dateMessage.div.nextSibling);
else dateMessage.container.append(bubble);
@ -1400,47 +1401,6 @@ export default class ChatBubbles {
//this.log('renderMessagesQueue');
let promises: Promise<any>[] = [];
(Array.from(bubble.querySelectorAll('img, video')) as HTMLImageElement[]).forEach(el => {
if(el instanceof HTMLVideoElement) {
if(!el.src) {
//this.log.warn('no source', el, source, 'src', source.src);
return;
} else if(el.readyState >= 4) return;
} else if(el.complete || !el.src) return;
let promise = new Promise<void>((resolve, reject) => {
let r: () => boolean;
let onLoad = () => {
clearTimeout(timeout);
resolve();
// lol
el.removeEventListener(el instanceof HTMLVideoElement ? 'canplay' : 'load', onLoad);
};
if(el instanceof HTMLVideoElement) {
el.addEventListener('canplay', onLoad);
r = () => el.readyState >= 1;
} else {
el.addEventListener('load', onLoad);
r = () => el.complete;
}
// for safari
let c = () => r() ? onLoad() : window.requestAnimationFrame(c);
window.requestAnimationFrame(c);
let timeout = setTimeout(() => {
// @ts-ignore
//this.log.error('did not called', el, el.parentElement, el.complete, el.readyState, src);
resolve();
}, 1500);
});
promises.push(promise);
});
this.messagesQueue.push({message, bubble, reverse, promises});
this.setMessagesQueuePromise();
@ -1624,6 +1584,8 @@ export default class ChatBubbles {
bubble.dataset.mid = message.mid;
bubble.dataset.timestamp = message.date;
const loadPromises: Promise<any>[] = [];
if(message._ === 'messageService') {
let action = message.action;
let _ = action._;
@ -1636,7 +1598,7 @@ export default class ChatBubbles {
bubbleContainer.innerHTML = `<div class="service-msg">${message.rReply}</div>`;
if(updatePosition) {
this.renderMessagesQueue(message, bubble, reverse);
this.renderMessagesQueue(message, bubble, reverse, loadPromises);
}
return bubble;
@ -1832,13 +1794,14 @@ export default class ChatBubbles {
middleware: this.getMiddleware(),
isOut: our,
lazyLoadQueue: this.lazyLoadQueue,
chat: this.chat
chat: this.chat,
loadPromises
});
break;
}
const withTail = !isAndroid && !message.message && !withReplies;
const withTail = !isAndroid && !message.message && !withReplies && USE_MEDIA_TAILS;
if(withTail) bubble.classList.add('with-media-tail');
wrapPhoto({
photo,
@ -1847,7 +1810,8 @@ export default class ChatBubbles {
withTail,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware()
middleware: this.getMiddleware(),
loadPromises
});
break;
@ -1858,7 +1822,7 @@ export default class ChatBubbles {
let webpage = messageMedia.webpage;
////////this.log('messageMediaWebPage', webpage);
if(webpage._ == 'webPageEmpty') {
if(webpage._ === 'webPageEmpty') {
break;
}
@ -1895,7 +1859,8 @@ export default class ChatBubbles {
lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware(),
isOut,
group: CHAT_ANIMATION_GROUP
group: CHAT_ANIMATION_GROUP,
loadPromises
});
//}
} else {
@ -1945,7 +1910,7 @@ export default class ChatBubbles {
bubble.classList.add('photo');
const size = webpage.photo.sizes[webpage.photo.sizes.length - 1];
if(size.w == size.h && quoteTextDiv.childElementCount) {
if(size.w === size.h && quoteTextDiv.childElementCount) {
bubble.classList.add('is-square-photo');
} else if(size.h > size.w) {
bubble.classList.add('is-vertical-photo');
@ -1959,7 +1924,8 @@ export default class ChatBubbles {
boxHeight: mediaSizes.active.webpage.height,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware()
middleware: this.getMiddleware(),
loadPromises
});
}
@ -1986,7 +1952,7 @@ export default class ChatBubbles {
}
let size = bubble.classList.contains('emoji-big') ? 140 : 200;
this.appPhotosManager.setAttachmentSize(doc, attachmentDiv, size, size, true);
this.appPhotosManager.setAttachmentSize(doc, attachmentDiv, size, size);
//let preloader = new ProgressivePreloader(attachmentDiv, false);
bubbleContainer.style.height = attachmentDiv.style.height;
bubbleContainer.style.width = attachmentDiv.style.width;
@ -2019,10 +1985,11 @@ export default class ChatBubbles {
middleware: this.getMiddleware(),
isOut: our,
lazyLoadQueue: this.lazyLoadQueue,
chat: this.chat
chat: this.chat,
loadPromises
});
} else {
const withTail = !isAndroid && !isApple && doc.type != 'round' && !message.message && !withReplies;
const withTail = !isAndroid && !isApple && doc.type != 'round' && !message.message && !withReplies && USE_MEDIA_TAILS;
if(withTail) bubble.classList.add('with-media-tail');
wrapVideo({
doc,
@ -2034,7 +2001,8 @@ export default class ChatBubbles {
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware(),
group: CHAT_ANIMATION_GROUP
group: CHAT_ANIMATION_GROUP,
loadPromises
});
}
@ -2045,7 +2013,8 @@ export default class ChatBubbles {
message,
bubble,
messageDiv,
chat: this.chat
chat: this.chat,
loadPromises
});
if(newNameContainer) {
@ -2260,7 +2229,7 @@ export default class ChatBubbles {
if(updatePosition) {
this.bubbleGroups.addBubble(bubble, message, reverse);
this.renderMessagesQueue(message, bubble, reverse);
this.renderMessagesQueue(message, bubble, reverse, loadPromises);
} else {
this.bubbleGroups.updateGroupByMessageId(message.mid);
}

View File

@ -64,7 +64,7 @@ export default class GifsMasonry {
const doc = appDocsManager.getDoc(docId);
const promise = this.scrollPromise.then(() => {
const promise = wrapVideo({
const res = wrapVideo({
doc,
container: div as HTMLDivElement,
lazyLoadQueue: null,
@ -73,6 +73,7 @@ export default class GifsMasonry {
noInfo: true,
});
const promise = res.loadPromise;
promise.finally(() => {
const video = div.querySelector('video');

View File

@ -1,6 +1,8 @@
import { debounce } from "../helpers/schedulers";
import { logger, LogLevels } from "../lib/logger";
import VisibilityIntersector, { OnVisibilityChange } from "./visibilityIntersector";
import { DEBUG } from "../lib/mtproto/mtproto_config";
import { findAndSpliceAll } from "../helpers/array";
type LazyLoadElementBase = {
load: () => Promise<any>
@ -42,14 +44,16 @@ export class LazyLoadQueueBase {
public lock() {
if(this.lockPromise) return;
const perf = performance.now();
//const perf = performance.now();
this.lockPromise = new Promise((resolve, reject) => {
this.unlockResolve = resolve;
});
this.lockPromise.then(() => {
this.log('was locked for:', performance.now() - perf);
});
/* if(DEBUG) {
this.lockPromise.then(() => {
this.log('was locked for:', performance.now() - perf);
});
} */
}
public unlock() {
@ -68,7 +72,9 @@ export class LazyLoadQueueBase {
this.inProcess.add(item);
this.log('will load media', this.lockPromise, item);
/* if(DEBUG) {
this.log('will load media', this.lockPromise, item);
} */
try {
//await new Promise((resolve) => setTimeout(resolve, 2e3));
@ -81,7 +87,9 @@ export class LazyLoadQueueBase {
this.inProcess.delete(item);
this.log('loaded media', item);
/* if(DEBUG) {
this.log('loaded media', item);
} */
this.processQueue();
}
@ -104,7 +112,7 @@ export class LazyLoadQueueBase {
do {
if(item) {
this.queue.findAndSplice(i => i == item);
this.queue.findAndSplice(i => i === item);
} else {
item = this.getItem();
}
@ -168,12 +176,12 @@ export class LazyLoadQueueIntersector extends LazyLoadQueueBase {
}
protected addElement(method: 'push' | 'unshift', el: LazyLoadElement) {
const item = this.queue.find(i => i.div == el.div);
const item = this.queue.find(i => i.div === el.div && i.load === el.load);
if(item) {
return false;
} else {
for(const item of this.inProcess) {
if(item.div == el.div) {
if(item.div === el.div && item.load === el.load) {
return false;
}
}
@ -201,7 +209,8 @@ export class LazyLoadQueueIntersector extends LazyLoadQueueBase {
}
public unobserve(el: HTMLElement) {
this.queue.findAndSplice(i => i.div === el);
findAndSpliceAll(this.queue, (i) => i.div === el);
this.intersector.unobserve(el);
}
}
@ -218,12 +227,11 @@ export default class LazyLoadQueue extends LazyLoadQueueIntersector {
this.log('isIntersecting', target);
// need for set element first if scrolled
const item = this.queue.findAndSplice(i => i.div == target);
if(item) {
findAndSpliceAll(this.queue, (i) => i.div === target).forEach(item => {
item.wasSeen = true;
this.queue.unshift(item);
//this.processQueue(item);
}
});
this.setProcessQueueTimeout();
}
@ -261,11 +269,11 @@ export class LazyLoadQueueRepeat extends LazyLoadQueueIntersector {
super(parallelLimit);
this.intersector = new VisibilityIntersector((target, visible) => {
const spliced = findAndSpliceAll(this.queue, (i) => i.div === target);
if(visible) {
const item = this.queue.findAndSplice(i => i.div == target);
this.queue.unshift(item || this._queue.get(target));
} else {
this.queue.findAndSplice(i => i.div == target);
spliced.forEach(item => {
this.queue.unshift(item || this._queue.get(target));
});
}
this.onVisibilityChange && this.onVisibilityChange(target, visible);
@ -298,9 +306,11 @@ export class LazyLoadQueueRepeat2 extends LazyLoadQueueIntersector {
super(parallelLimit);
this.intersector = new VisibilityIntersector((target, visible) => {
const item = this.queue.findAndSplice(i => i.div == target);
if(visible && item) {
this.queue.unshift(item);
const spliced = findAndSpliceAll(this.queue, (i) => i.div === target);
if(visible && spliced.length) {
spliced.forEach(item => {
this.queue.unshift(item);
});
}
this.onVisibilityChange && this.onVisibilityChange(target, visible);

View File

@ -1,5 +1,8 @@
import { isInDOM, cancelEvent, attachClickEvent } from "../helpers/dom";
import { CancellablePromise } from "../helpers/cancellablePromise";
import SetTransition from "./singleTransition";
const TRANSITION_TIME = 200;
export default class ProgressivePreloader {
public preloader: HTMLDivElement;
@ -20,8 +23,8 @@ export default class ProgressivePreloader {
this.preloader.innerHTML = `
<div class="you-spin-me-round">
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-circular" viewBox="25 25 50 50">
<circle class="preloader-path-new" cx="50" cy="50" r="${streamable ? 19 : 23}" fill="none" stroke-miterlimit="10"/>
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-circular" viewBox="${streamable ? '25 25 50 50' : '27 27 54 54'}">
<circle class="preloader-path-new" cx="${streamable ? '50' : '54'}" cy="${streamable ? '50' : '54'}" r="${streamable ? 19 : 24}" fill="none" stroke-miterlimit="10"/>
</svg>
</div>`;
@ -95,16 +98,20 @@ export default class ProgressivePreloader {
}
this.detached = false;
window.requestAnimationFrame(() => {
/* window.requestAnimationFrame(() => {
if(this.detached) return;
this.detached = false;
this.detached = false; */
elem[this.attachMethod](this.preloader);
window.requestAnimationFrame(() => {
SetTransition(this.preloader, 'is-visible', true, TRANSITION_TIME);
});
if(this.cancelable && reset) {
this.setProgress(0);
}
});
//});
}
public detach() {
@ -113,14 +120,18 @@ export default class ProgressivePreloader {
//return;
if(this.preloader.parentElement) {
/* setTimeout(() => */window.requestAnimationFrame(() => {
if(!this.detached) return;
this.detached = true;
/* setTimeout(() => *///window.requestAnimationFrame(() => {
/* if(!this.detached) return;
this.detached = true; */
if(this.preloader.parentElement) {
this.preloader.remove();
}
})/* , 5e3) */;
//if(this.preloader.parentElement) {
window.requestAnimationFrame(() => {
SetTransition(this.preloader, 'is-visible', false, TRANSITION_TIME, () => {
this.preloader.remove();
});
});
//}
//})/* , 5e3) */;
}
}
@ -137,7 +148,7 @@ export default class ProgressivePreloader {
try {
const totalLength = this.circle.getTotalLength();
//console.log('setProgress', (percents / 100 * totalLength));
this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', 200';
this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', ' + totalLength;
} catch(err) {}
}
}

View File

@ -29,10 +29,11 @@ import RichTextProcessor from '../lib/richtextprocessor';
import appImManager from '../lib/appManagers/appImManager';
import Chat from './chat/chat';
import { SearchSuperContext } from './appSearchSuper.';
import rootScope from '../lib/rootScope';
const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group}: {
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises}: {
doc: MyDocument,
container?: HTMLElement,
message?: any,
@ -43,7 +44,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
middleware?: () => boolean,
lazyLoadQueue?: LazyLoadQueue,
noInfo?: true,
group?: string
group?: string,
onlyPreview?: boolean,
withoutPreloader?: boolean,
loadPromises?: Promise<any>[]
}) {
const isAlbumItem = !(boxWidth && boxHeight);
const canAutoplay = doc.type != 'video' || (doc.size <= MAX_VIDEO_AUTOPLAY_SIZE && !isAlbumItem);
@ -71,8 +75,13 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
}
}
let res: {
thumb?: typeof photoRes,
loadPromise: Promise<any>
} = {} as any;
if(doc.mime_type == 'image/gif') {
return wrapPhoto({
const photoRes = wrapPhoto({
photo: doc,
message,
container,
@ -81,8 +90,14 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
withTail,
isOut,
lazyLoadQueue,
middleware
middleware,
withoutPreloader,
loadPromises
});
res.thumb = photoRes;
res.loadPromise = photoRes.loadPromises.full;
return res;
}
/* const video = doc.type == 'round' ? appMediaPlaybackController.addMedia(doc, message.mid) as HTMLVideoElement : document.createElement('video');
@ -91,6 +106,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
} */
const video = document.createElement('video');
video.classList.add('media-video');
video.muted = true;
video.setAttribute('playsinline', 'true');
if(doc.type == 'round') {
@ -155,53 +171,37 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
} else {
video.autoplay = true; // для safari
}
let img: HTMLImageElement;
let photoRes: ReturnType<typeof wrapPhoto>;
if(message) {
if(!canAutoplay) {
return wrapPhoto({
photo: doc,
message,
container,
boxWidth,
boxHeight,
withTail,
isOut,
lazyLoadQueue,
middleware
});
photoRes = wrapPhoto({
photo: doc,
message,
container,
boxWidth,
boxHeight,
withTail,
isOut,
lazyLoadQueue,
middleware,
withoutPreloader: true,
loadPromises
});
res.thumb = photoRes;
if(!canAutoplay || onlyPreview) {
res.loadPromise = photoRes.loadPromises.full;
return res;
}
if(withTail) {
img = wrapMediaWithTail(doc, message, container, boxWidth, boxHeight, isOut);
} else {
if(boxWidth && boxHeight) { // !album
appPhotosManager.setAttachmentSize(doc, container, boxWidth, boxHeight, false, true);
}
if(doc.thumbs?.length && 'bytes' in doc.thumbs[0]) {
appPhotosManager.setAttachmentPreview(doc.thumbs[0].bytes, container, false);
}
img = container.lastElementChild as HTMLImageElement;
if(img?.tagName != 'IMG') {
container.append(img = new Image());
}
}
if(img) {
img.classList.add('thumbnail');
}
if(withTail) {
const foreignObject = img.parentElement;
const foreignObject = (photoRes.images.thumb || photoRes.images.full).parentElement;
video.width = +foreignObject.getAttributeNS(null, 'width');
video.height = +foreignObject.getAttributeNS(null, 'height');
foreignObject.append(video);
}
}
if(!img?.parentElement) {
} else { // * gifs masonry
const gotThumb = appDocsManager.getThumb(doc, false);
if(gotThumb) {
gotThumb.promise.then(() => {
@ -253,14 +253,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
//if(doc.type == 'gif'/* || true */) {
video.addEventListener(isAppleMobile ? 'loadeddata' : 'canplay', () => {
if(img?.parentElement) {
img.remove();
}
/* if(!video.paused) {
video.pause();
} */
if(doc.type != 'round' && group) {
if(doc.type !== 'round' && group) {
animationIntersector.addAnimation(video, group);
}
@ -327,7 +323,9 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
return;
} */
return /* doc.downloaded || */!lazyLoadQueue/* && false */ ? loadVideo() : (lazyLoadQueue.push({div: container, load: loadVideo/* , wasSeen: true */}), Promise.resolve());
res.loadPromise = !lazyLoadQueue ? loadVideo() : (lazyLoadQueue.push({div: container, load: loadVideo}), Promise.resolve());
return res;
}
export const formatDate = (timestamp: number, monthShort = false, withYear = true) => {
@ -344,13 +342,14 @@ export const formatDate = (timestamp: number, monthShort = false, withYear = tru
return str + ' at ' + date.getHours() + ':' + ('0' + date.getMinutes()).slice(-2);
};
export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showSender, searchContext}: {
export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showSender, searchContext, loadPromises}: {
message: any,
withTime?: boolean,
fontWeight?: number,
voiceAsMusic?: boolean,
showSender?: boolean,
searchContext?: SearchSuperContext
searchContext?: SearchSuperContext,
loadPromises?: Promise<any>[]
}): HTMLElement {
if(!fontWeight) fontWeight = 500;
@ -394,7 +393,8 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
message: null,
container: icoDiv,
boxWidth: 54,
boxHeight: 54
boxHeight: 54,
loadPromises
});
icoDiv.style.width = icoDiv.style.height = '';
}
@ -475,8 +475,12 @@ function wrapMediaWithTail(photo: MyPhoto | MyDocument, message: {mid: number, m
svg.classList.add('bubble__media-container', isOut ? 'is-out' : 'is-in');
const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", 'foreignObject');
appPhotosManager.setAttachmentSize(photo, foreignObject, boxWidth, boxHeight/* , false, true */);
const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo);
if(gotThumb) {
foreignObject.append(gotThumb.image);
}
appPhotosManager.setAttachmentSize(photo, foreignObject, boxWidth, boxHeight);
const width = +foreignObject.getAttributeNS(null, 'width');
const height = +foreignObject.getAttributeNS(null, 'height');
@ -525,7 +529,7 @@ function wrapMediaWithTail(photo: MyPhoto | MyDocument, message: {mid: number, m
return img;
}
export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size}: {
export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size, withoutPreloader, loadPromises}: {
photo: MyPhoto | MyDocument,
message: any,
container: HTMLElement,
@ -535,8 +539,23 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
isOut?: boolean,
lazyLoadQueue?: LazyLoadQueue,
middleware?: () => boolean,
size?: PhotoSize
size?: PhotoSize,
withoutPreloader?: boolean,
loadPromises?: Promise<any>[]
}) {
if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs)) {
return {
loadPromises: {
thumb: Promise.resolve(),
full: Promise.resolve()
},
images: {
thumb: null,
full: null
}
};
}
if(boxWidth === undefined) {
boxWidth = mediaSizes.active.regular.width;
}
@ -545,44 +564,50 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
boxHeight = mediaSizes.active.regular.height;
}
let loadThumbPromise: Promise<any>;
let thumbImage: HTMLImageElement;
let image: HTMLImageElement;
if(withTail) {
image = wrapMediaWithTail(photo, message, container, boxWidth, boxHeight, isOut);
} else {
image = new Image();
if(boxWidth && boxHeight) { // !album
size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, false, true);
size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight);
}
if(photo._ == 'document' || !photo.downloaded) {
const thumbs = (photo as MyPhoto).sizes || (photo as MyDocument).thumbs;
if(thumbs?.length && 'bytes' in thumbs[0]) {
appPhotosManager.setAttachmentPreview(thumbs[0].bytes, container, false);
}
}
image = container.lastElementChild as HTMLImageElement;
if(!image || image.tagName != 'IMG') {
container.append(image = new Image());
const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo);
if(gotThumb) {
loadThumbPromise = gotThumb.loadPromise;
thumbImage = gotThumb.image;
thumbImage.classList.add('media-photo');
container.append(thumbImage);
}
}
if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs)) {
return Promise.resolve();
}
image.classList.add('media-photo');
//console.log('wrapPhoto downloaded:', photo, photo.downloaded, container);
const cacheContext = appPhotosManager.getCacheContext(photo);
const needFadeIn = (thumbImage || !cacheContext.downloaded) && rootScope.settings.animationsEnabled;
if(needFadeIn) {
image.classList.add('fade-in');
}
let preloader: ProgressivePreloader;
if(message?.media?.preloader) { // means upload
message.media.preloader.attach(container, false);
} else if(!cacheContext.downloaded) {
preloader = new ProgressivePreloader(null, false, false, photo._ == 'document' ? 'prepend' : 'append');
} else if(!cacheContext.downloaded && !withoutPreloader) {
preloader = new ProgressivePreloader(null, false, false, 'prepend');
}
let loadPromise: Promise<any>;
const load = () => {
const promise = photo._ == 'document' && photo.animated ?
if(loadPromise) return loadPromise;
const promise = photo._ === 'document' && photo.mime_type === 'image/gif' ?
appDocsManager.downloadDoc(photo, undefined, lazyLoadQueue?.queueId) :
appPhotosManager.preloadPhoto(photo, size, lazyLoadQueue?.queueId);
@ -590,14 +615,56 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
preloader.attach(container, true, promise);
}
return promise.then(() => {
return loadPromise = promise.then(() => {
if(middleware && !middleware()) return;
renderImageFromUrl(image || container, cacheContext.url || photo.url);
return new Promise((resolve) => {
renderImageFromUrl(image, cacheContext.url || photo.url, () => {
container.append(image);
window.requestAnimationFrame(() => {
resolve();
});
//resolve();
if(needFadeIn) {
setTimeout(() => {
image.classList.remove('fade-in');
if(thumbImage) {
thumbImage.remove();
}
}, 200);
}
});
});
});
};
if(cacheContext.downloaded) {
loadThumbPromise = load();
}
return cacheContext.downloaded || !lazyLoadQueue ? load() : (lazyLoadQueue.push({div: container, load/* : load, wasSeen: true */}), Promise.resolve());
if(!lazyLoadQueue) {
loadPromise = load();
} else {
lazyLoadQueue.push({div: container, load});
}
if(loadPromises) {
loadPromises.push(loadThumbPromise);
}
return {
loadPromises: {
thumb: loadThumbPromise,
full: loadPromise || Promise.resolve()
},
images: {
thumb: thumbImage,
full: image
}
};
}
export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop}: {
@ -923,14 +990,15 @@ export function prepareAlbum(options: {
} */
}
export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLoadQueue, isOut, chat}: {
export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLoadQueue, isOut, chat, loadPromises}: {
groupId: string,
attachmentDiv: HTMLElement,
middleware?: () => boolean,
lazyLoadQueue?: LazyLoadQueue,
uploading?: boolean,
isOut: boolean,
chat: Chat
chat: Chat,
loadPromises?: Promise<any>[]
}) {
const items: {size: PhotoSize.photoSize, media: any, message: any}[] = [];
@ -974,7 +1042,8 @@ export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLo
isOut,
lazyLoadQueue,
middleware,
size
size,
loadPromises
});
} else {
wrapVideo({
@ -986,19 +1055,21 @@ export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLo
withTail: false,
isOut,
lazyLoadQueue,
middleware
middleware,
loadPromises
});
}
});
}
export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat}: {
export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat, loadPromises}: {
albumMustBeRenderedFull: boolean,
message: any,
messageDiv: HTMLElement,
bubble: HTMLElement,
uploading?: boolean,
chat: Chat
chat: Chat,
loadPromises?: Promise<any>[]
}) {
let nameContainer: HTMLDivElement;
const mids = albumMustBeRenderedFull ? chat.getMidsByMid(message.mid) : [message.mid];
@ -1011,7 +1082,8 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble,
const message = chat.getMessage(mid);
const doc = message.media.document;
const div = wrapDocument({
message
message,
loadPromises
});
const container = document.createElement('div');

View File

@ -13,4 +13,14 @@ export function listMergeSorted(list1: any[] = [], list2: any[] = []) {
return result;
}
export const accumulate = (arr: number[], initialValue: number) => arr.reduce((acc, value) => acc + value, initialValue);
export const accumulate = (arr: number[], initialValue: number) => arr.reduce((acc, value) => acc + value, initialValue);
export function findAndSpliceAll<T>(array: Array<T>, verify: (value: T, index: number, arr: typeof array) => boolean) {
const out: typeof array = [];
let idx = -1;
while((idx = array.findIndex(verify)) !== -1) {
out.push(array.splice(idx, 1)[0]);
}
return out;
}

34
src/helpers/blur.ts Normal file
View File

@ -0,0 +1,34 @@
import fastBlur from '../vendor/fastBlur';
import { fastRaf } from './schedulers';
const RADIUS = 2;
const ITERATIONS = 2;
export default function blur(dataUri: string, delay?: number) {
return new Promise<string>((resolve) => {
fastRaf(() => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
fastBlur(ctx, 0, 0, canvas.width, canvas.height, RADIUS, ITERATIONS);
resolve(canvas.toDataURL());
};
if(delay) {
setTimeout(() => {
img.src = dataUri;
}, delay);
} else {
img.src = dataUri;
}
});
});
}

View File

@ -8,6 +8,7 @@ import { RichTextProcessor } from '../richtextprocessor';
import webpWorkerController from '../webp/webpWorkerController';
import appDownloadManager, { DownloadBlob } from './appDownloadManager';
import appPhotosManager from './appPhotosManager';
import blur from '../../helpers/blur';
export type MyDocument = Document.document;
@ -246,7 +247,7 @@ export class AppDocsManager {
if('bytes' in thumb) {
// * exclude from state
defineNotNumerableProperties(thumb, ['url']);
thumb.url = appPhotosManager.getPreviewURLFromBytes(thumb.bytes, !!doc.sticker);
promise = blur(appPhotosManager.getPreviewURLFromBytes(thumb.bytes, !!doc.sticker)).then(url => thumb.url = url);
} else {
//return this.getFileURL(doc, false, thumb);
promise = this.downloadDoc(doc, thumb);

View File

@ -19,7 +19,7 @@ import appUsersManager from "./appUsersManager";
#ce671b 5 orange
*/
const DialogColorsFg = ['#c03d33', '#4fad2d', '#d09306', '#168acd', '#8544d6', '#cd4073', '#2996ad', '#ce671b'];
const DialogColors = ['#e17076', '#7bc862', '#e5ca77', '#65AADD', '#a695e7', '#ee7aae', '#6ec9cb', '#faa774'];
const DialogColors = ['red', 'green', 'yellow', 'blue', 'violet', 'pink', 'cyan', 'orange'];
const DialogColorsMap = [0, 7, 4, 1, 6, 3, 5];
export type PeerType = 'channel' | 'chat' | 'megagroup' | 'group' | 'saved';

View File

@ -12,6 +12,8 @@ import { MyDocument } from "./appDocsManager";
import appDownloadManager from "./appDownloadManager";
import appUsersManager from "./appUsersManager";
import { MOUNT_CLASS_TO } from "../mtproto/mtproto_config";
import blur from "../../helpers/blur";
import { renderImageFromUrl } from "../../components/misc";
export type MyPhoto = Photo.photo;
@ -106,7 +108,7 @@ export class AppPhotosManager {
bestPhotoSize = photoSize;
const {w, h} = calcImageInBox(photoSize.w, photoSize.h, width, height);
if(w == width || h == height) {
if(w === width || h === height) {
break;
}
}
@ -189,47 +191,28 @@ export class AppPhotosManager {
return thumb.url ?? (defineNotNumerableProperties(thumb, ['url']), thumb.url = this.getPreviewURLFromBytes(thumb.bytes, isSticker));
}
public setAttachmentPreview(bytes: Uint8Array | number[], element: HTMLElement | SVGForeignObjectElement, isSticker = false, background = false) {
let url = this.getPreviewURLFromBytes(bytes, isSticker);
public getImageFromStrippedThumb(thumb: PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize) {
const url = this.getPreviewURLFromThumb(thumb, false);
if(background) {
let img = new Image();
img.src = url;
img.addEventListener('load', () => {
element.style.backgroundImage = 'url(' + url + ')';
const image = new Image();
image.classList.add('thumbnail');
const loadPromise = blur(url).then(url => {
return new Promise<any>((resolve) => {
renderImageFromUrl(image, url, resolve);
});
return element;
} else {
if(element instanceof HTMLImageElement) {
element.src = url;
return element;
} else {
let img = new Image();
img.src = url;
element.append(img);
return img;
}
}
});
return {image, loadPromise};
}
public setAttachmentSize(photo: MyPhoto | MyDocument, element: HTMLElement | SVGForeignObjectElement, boxWidth: number, boxHeight: number, isSticker = false, dontRenderPreview = false) {
public setAttachmentSize(photo: MyPhoto | MyDocument, element: HTMLElement | SVGForeignObjectElement, boxWidth: number, boxHeight: number) {
const photoSize = this.choosePhotoSize(photo, boxWidth, boxHeight);
//console.log('setAttachmentSize', photo, photo.sizes[0].bytes, div);
const sizes = (photo as MyPhoto).sizes || (photo as MyDocument).thumbs;
const thumb = sizes?.length ? sizes[0] : null;
if(thumb && ('bytes' in thumb)) {
if((!photo.downloaded || (photo as MyDocument).type == 'video' || (photo as MyDocument).type == 'gif') && !isSticker && !dontRenderPreview) {
this.setAttachmentPreview(thumb.bytes, element, isSticker);
}
}
let width: number;
let height: number;
if(photo._ == 'document') {
if(photo._ === 'document') {
width = photo.w || 512;
height = photo.h || 512;
} else {
@ -250,17 +233,36 @@ export class AppPhotosManager {
return photoSize;
}
public getStrippedThumbIfNeeded(photo: MyPhoto | MyDocument): ReturnType<AppPhotosManager['getImageFromStrippedThumb']> {
if(!photo.downloaded || (photo as MyDocument).type === 'video' || (photo as MyDocument).type === 'gif') {
if(photo._ === 'document') {
const cacheContext = this.getCacheContext(photo);
if(cacheContext.downloaded) {
return null;
}
}
const sizes = (photo as MyPhoto).sizes || (photo as MyDocument).thumbs;
const thumb = sizes?.length ? sizes[0] : null;
if(thumb && ('bytes' in thumb)) {
return appPhotosManager.getImageFromStrippedThumb(thumb as any);
}
}
return null;
}
public getPhotoDownloadOptions(photo: MyPhoto | MyDocument, photoSize: PhotoSize, queueId?: number) {
const isMyDocument = photo._ == 'document';
const isMyDocument = photo._ === 'document';
if(!photoSize || photoSize._ == 'photoSizeEmpty') {
if(!photoSize || photoSize._ === 'photoSizeEmpty') {
//console.error('no photoSize by photo:', photo);
throw new Error('photoSizeEmpty!');
}
// maybe it's a thumb
const isPhoto = (photoSize._ == 'photoSize' || photoSize._ == 'photoSizeProgressive') && photo.access_hash && photo.file_reference;
const isPhoto = (photoSize._ === 'photoSize' || photoSize._ === 'photoSizeProgressive') && photo.access_hash && photo.file_reference;
const location: InputFileLocation.inputPhotoFileLocation | InputFileLocation.inputDocumentFileLocation | FileLocation = isPhoto ? {
_: isMyDocument ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation',
id: photo.id,
@ -277,12 +279,26 @@ export class AppPhotosManager {
return {url: getFileURL('photo', downloadOptions), location: downloadOptions.location};
} */
public isDownloaded(media: any) {
const isPhoto = media._ === 'photo';
const photo = isPhoto ? this.getPhoto(media.id) : null;
let isDownloaded: boolean;
if(photo) {
isDownloaded = photo.downloaded > 0;
} else {
const cachedThumb = this.getDocumentCachedThumb(media.id);
isDownloaded = cachedThumb?.downloaded > 0;
}
return isDownloaded;
}
public preloadPhoto(photoId: any, photoSize?: PhotoSize, queueId?: number): CancellablePromise<Blob> {
const photo = this.getPhoto(photoId);
// @ts-ignore
if(!photo || photo._ == 'photoEmpty') {
if(!photo || photo._ === 'photoEmpty') {
throw new Error('preloadPhoto photoEmpty!');
}
@ -325,7 +341,7 @@ export class AppPhotosManager {
}
public getCacheContext(photo: any): DocumentCacheThumb {
return photo._ == 'document' ? this.getDocumentCachedThumb(photo.id) : photo;
return photo._ === 'document' ? this.getDocumentCachedThumb(photo.id) : photo;
}
public getDocumentCachedThumb(docId: string) {
@ -351,7 +367,7 @@ export class AppPhotosManager {
public savePhotoFile(photo: MyPhoto | MyDocument, queueId?: number) {
const fullPhotoSize = this.choosePhotoSize(photo, 0xFFFF, 0xFFFF);
if(!(fullPhotoSize._ == 'photoSize' || fullPhotoSize._ == 'photoSizeProgressive')) {
if(!(fullPhotoSize._ === 'photoSize' || fullPhotoSize._ === 'photoSizeProgressive')) {
return;
}

View File

@ -1,18 +1,51 @@
avatar-element {
--size: 54px;
--multiplier: 1;
--color-top: var(--peer-avatar-blue-top);
--color-bottom: var(--peer-avatar-blue-bottom);
color: #fff;
width: var(--size);
height: var(--size);
line-height: var(--size);
border-radius: 50%;
background-color: $color-blue;
background: linear-gradient(var(--color-top), var(--color-bottom));
text-align: center;
font-size: calc(1.25rem / var(--multiplier));
/* overflow: hidden; */
position: relative;
user-select: none;
text-transform: uppercase;
font-weight: 700;
&[data-color="red"] {
--color-top: var(--peer-avatar-red-top);
--color-bottom: var(--peer-avatar-red-bottom);
}
&[data-color="orange"] {
--color-top: var(--peer-avatar-orange-top);
--color-bottom: var(--peer-avatar-orange-bottom);
}
&[data-color="violet"] {
--color-top: var(--peer-avatar-violet-top);
--color-bottom: var(--peer-avatar-violet-bottom);
}
&[data-color="green"] {
--color-top: var(--peer-avatar-green-top);
--color-bottom: var(--peer-avatar-green-bottom);
}
&[data-color="cyan"] {
--color-top: var(--peer-avatar-cyan-top);
--color-bottom: var(--peer-avatar-cyan-bottom);
}
&[data-color="pink"] {
--color-top: var(--peer-avatar-pink-top);
--color-bottom: var(--peer-avatar-pink-bottom);
}
&.tgico-savedmessages:before {
font-size: calc(25px / var(--multiplier));

View File

@ -1039,7 +1039,7 @@ $chat-helper-size: 39px;
}
&-container .preloader-circular {
background-color: var(--message-time-background);
background-color: rgba(0, 0, 0, .3);
}
}

View File

@ -597,16 +597,10 @@ $bubble-margin: .25rem;
display: flex; // lol
justify-content: center;
position: relative;
cursor: pointer;
img, video {
max-width: 100%;
cursor: pointer;
opacity: 1;
transition: opacity .3s ease;
body.animation-level-0 & {
transition: none;
}
}
.download {
@ -633,10 +627,10 @@ $bubble-margin: .25rem;
display: none;
}
}
}
.preloader-container {
z-index: 1;
}
.preloader-container {
z-index: 1;
}
&:not(.sticker) {

View File

@ -28,10 +28,24 @@
left: 0;
right: 0;
margin: auto;
width: 50px;
height: 50px;
width: 54px;
height: 54px;
display: flex;
/* cursor: pointer; */
opacity: 0;
transform: scale(0);
body:not(.animation-level-0) & {
transition: opacity .2s ease-in-out, transform .2s ease-in-out;
}
&.is-visible {
&:not(.backwards) {
opacity: 1;
transform: scale(1);
}
}
}
}
@ -52,12 +66,12 @@
}
.preloader-path-new {
stroke-dasharray: 5, 200;
stroke-dasharray: 5, 149.82;
stroke-dashoffset: 0;
transition: stroke-dasharray 400ms ease-in-out;
stroke-linecap: round;
stroke: white;
stroke-width: 1.5;
stroke-width: 2;
}
&.preloader-swing {
@ -68,9 +82,7 @@
}
.preloader-path-new {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
animation: dashNew 1.5s ease-in-out infinite/* , color 6s ease-in-out infinite */;
animation: dashNew 1.5s ease-in-out infinite;
}
}
@ -111,7 +123,7 @@
background-color: #fff;
left: 50%;
top: 50%;
transform: translate3d(-50%,-50%,0);
transform: translate3d(-50%, -50%, 0);
}
}
}
@ -139,16 +151,16 @@
@keyframes dashNew {
0% {
stroke-dasharray: 1, 200;
stroke-dasharray: 1, 149.82; // 149.82 = getTotalLength
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35px;
stroke-dasharray: 112.36, 149.82; // 112.36 = 149.82 * .75
stroke-dashoffset: -38; // bruted
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -286%;
stroke-dasharray: 112.36, 149.82;
stroke-dashoffset: -149.82; // totalLength
}
}

View File

@ -361,15 +361,6 @@
.grid-item {
overflow: hidden;
&-media {
opacity: 1;
transition: opacity .2s ease;
html:not(.is-mac) &.thumbnail {
filter: blur(7px);
}
}
}
/* span.video-play {

View File

@ -96,6 +96,22 @@ $chat-padding-handhelds: .5rem;
--messages-secondary-text-size: calc(var(--messages-text-size) - 1px);
--esg-sticker-size: 80px;
// https://github.com/overtake/TelegramSwift/blob/5cc7d2475fe4738a6aa0486c23eaf80a89d33b97/submodules/TGUIKit/TGUIKit/PresentationTheme.swift#L2054
--peer-avatar-red-top: #ff885e;
--peer-avatar-red-bottom: #ff516a;
--peer-avatar-orange-top: #ffcd6a;
--peer-avatar-orange-bottom: #ffa85c;
--peer-avatar-violet-top: #82b1ff;
--peer-avatar-violet-bottom: #665fff;
--peer-avatar-green-top: #a0de7e;
--peer-avatar-green-bottom: #54cb68;
--peer-avatar-cyan-top: #53edd6;
--peer-avatar-cyan-bottom: #28c9b7;
--peer-avatar-blue-top: #72d5fd;
--peer-avatar-blue-bottom: #2a9ef1;
--peer-avatar-pink-top: #e0a2f3;
--peer-avatar-pink-bottom: #d669ed;
@include respond-to(handhelds) {
--right-column-width: 100vw;
--esg-sticker-size: 68px;
@ -859,7 +875,6 @@ img.emoji {
top: 0;
width: 100%;
height: 100%;
-o-object-fit: cover;
object-fit: cover;
}
}
@ -1161,3 +1176,19 @@ middle-ellipsis-element {
background-color: var(--color-gray-hover);
}
}
.media-photo, .media-video {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
&.fade-in {
animation: fade-in-opacity .2s ease-in-out forwards;
}
}
.media-video {
z-index: 1; // * overflow media-photo
}

160
src/vendor/fastBlur.js vendored Normal file
View File

@ -0,0 +1,160 @@
/*
Superfast Blur - a fast Box Blur For Canvas
Version: 0.5
Author: Mario Klingemann
Contact: mario@quasimondo.com
Website: http://www.quasimondo.com/BoxBlurForCanvas
Twitter: @quasimondo
In case you find this class useful - especially in commercial projects -
I am not totally unhappy for a small donation to my PayPal account
mario@quasimondo.de
Or support me on flattr:
https://flattr.com/thing/140066/Superfast-Blur-a-pretty-fast-Box-Blur-Effect-for-CanvasJavascript
Copyright (c) 2011 Mario Klingemann
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
// eslint-disable-next-line max-len
const mul_table = [1, 57, 41, 21, 203, 34, 97, 73, 227, 91, 149, 62, 105, 45, 39, 137, 241, 107, 3, 173, 39, 71, 65, 238, 219, 101, 187, 87, 81, 151, 141, 133, 249, 117, 221, 209, 197, 187, 177, 169, 5, 153, 73, 139, 133, 127, 243, 233, 223, 107, 103, 99, 191, 23, 177, 171, 165, 159, 77, 149, 9, 139, 135, 131, 253, 245, 119, 231, 224, 109, 211, 103, 25, 195, 189, 23, 45, 175, 171, 83, 81, 79, 155, 151, 147, 9, 141, 137, 67, 131, 129, 251, 123, 30, 235, 115, 113, 221, 217, 53, 13, 51, 50, 49, 193, 189, 185, 91, 179, 175, 43, 169, 83, 163, 5, 79, 155, 19, 75, 147, 145, 143, 35, 69, 17, 67, 33, 65, 255, 251, 247, 243, 239, 59, 29, 229, 113, 111, 219, 27, 213, 105, 207, 51, 201, 199, 49, 193, 191, 47, 93, 183, 181, 179, 11, 87, 43, 85, 167, 165, 163, 161, 159, 157, 155, 77, 19, 75, 37, 73, 145, 143, 141, 35, 138, 137, 135, 67, 33, 131, 129, 255, 63, 250, 247, 61, 121, 239, 237, 117, 29, 229, 227, 225, 111, 55, 109, 216, 213, 211, 209, 207, 205, 203, 201, 199, 197, 195, 193, 48, 190, 47, 93, 185, 183, 181, 179, 178, 176, 175, 173, 171, 85, 21, 167, 165, 41, 163, 161, 5, 79, 157, 78, 154, 153, 19, 75, 149, 74, 147, 73, 144, 143, 71, 141, 140, 139, 137, 17, 135, 134, 133, 66, 131, 65, 129, 1];
// eslint-disable-next-line max-len
const shg_table = [0, 9, 10, 10, 14, 12, 14, 14, 16, 15, 16, 15, 16, 15, 15, 17, 18, 17, 12, 18, 16, 17, 17, 19, 19, 18, 19, 18, 18, 19, 19, 19, 20, 19, 20, 20, 20, 20, 20, 20, 15, 20, 19, 20, 20, 20, 21, 21, 21, 20, 20, 20, 21, 18, 21, 21, 21, 21, 20, 21, 17, 21, 21, 21, 22, 22, 21, 22, 22, 21, 22, 21, 19, 22, 22, 19, 20, 22, 22, 21, 21, 21, 22, 22, 22, 18, 22, 22, 21, 22, 22, 23, 22, 20, 23, 22, 22, 23, 23, 21, 19, 21, 21, 21, 23, 23, 23, 22, 23, 23, 21, 23, 22, 23, 18, 22, 23, 20, 22, 23, 23, 23, 21, 22, 20, 22, 21, 22, 24, 24, 24, 24, 24, 22, 21, 24, 23, 23, 24, 21, 24, 23, 24, 22, 24, 24, 22, 24, 24, 22, 23, 24, 24, 24, 20, 23, 22, 23, 24, 24, 24, 24, 24, 24, 24, 23, 21, 23, 22, 23, 24, 24, 24, 22, 24, 24, 24, 23, 22, 24, 24, 25, 23, 25, 25, 23, 24, 25, 25, 24, 22, 25, 25, 25, 24, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 23, 25, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 24, 22, 25, 25, 23, 25, 25, 20, 24, 25, 24, 25, 25, 22, 24, 25, 24, 25, 24, 25, 25, 24, 25, 25, 25, 25, 22, 25, 25, 25, 24, 25, 24, 25, 18];
export default function boxBlurCanvasRGB(context, top_x, top_y, width, height, radius, iterations) {
if (Number.isNaN(radius) || radius < 1) return;
radius |= 0;
if (Number.isNaN(iterations)) iterations = 1;
iterations |= 0;
if (iterations > 3) iterations = 3;
if (iterations < 1) iterations = 1;
const imageData = context.getImageData(top_x, top_y, width, height);
const pixels = imageData.data;
let rsum;
let gsum;
let bsum;
let x;
let y;
let i;
let p;
let p1;
let p2;
let yp;
let yi;
let yw;
let wm = width - 1;
let hm = height - 1;
let rad1 = radius + 1;
let r = [];
let g = [];
let b = [];
let mul_sum = mul_table[radius];
let shg_sum = shg_table[radius];
let vmin = [];
let vmax = [];
while (iterations-- > 0) {
yw = yi = 0;
for (y = 0; y < height; y++) {
rsum = pixels[yw] * rad1;
gsum = pixels[yw + 1] * rad1;
bsum = pixels[yw + 2] * rad1;
for (i = 1; i <= radius; i++) {
p = yw + (((i > wm ? wm : i)) << 2);
rsum += pixels[p++];
gsum += pixels[p++];
bsum += pixels[p++];
}
for (x = 0; x < width; x++) {
r[yi] = rsum;
g[yi] = gsum;
b[yi] = bsum;
if (y == 0) {
vmin[x] = ((p = x + rad1) < wm ? p : wm) << 2;
vmax[x] = ((p = x - radius) > 0 ? p << 2 : 0);
}
p1 = yw + vmin[x];
p2 = yw + vmax[x];
rsum += pixels[p1++] - pixels[p2++];
gsum += pixels[p1++] - pixels[p2++];
bsum += pixels[p1++] - pixels[p2++];
yi++;
}
yw += (width << 2);
}
for (x = 0; x < width; x++) {
yp = x;
rsum = r[yp] * rad1;
gsum = g[yp] * rad1;
bsum = b[yp] * rad1;
for (i = 1; i <= radius; i++) {
yp += (i > hm ? 0 : width);
rsum += r[yp];
gsum += g[yp];
bsum += b[yp];
}
yi = x << 2;
for (y = 0; y < height; y++) {
pixels[yi] = (rsum * mul_sum) >>> shg_sum;
pixels[yi + 1] = (gsum * mul_sum) >>> shg_sum;
pixels[yi + 2] = (bsum * mul_sum) >>> shg_sum;
if (x == 0) {
vmin[y] = ((p = y + rad1) < hm ? p : hm) * width;
vmax[y] = ((p = y - radius) > 0 ? p * width : 0);
}
p1 = x + vmin[y];
p2 = x + vmax[y];
rsum += r[p1] - r[p2];
gsum += g[p1] - g[p2];
bsum += b[p1] - b[p2];
yi += width << 2;
}
}
}
context.putImageData(imageData, top_x, top_y);
}