tweb/src/components/stickerViewer.ts

354 lines
12 KiB
TypeScript

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import IS_TOUCH_SUPPORTED from '../environment/touchSupport';
import cancelEvent from '../helpers/dom/cancelEvent';
import {simulateClickEvent, attachClickEvent} from '../helpers/dom/clickEvent';
import findUpAsChild from '../helpers/dom/findUpAsChild';
import findUpClassName from '../helpers/dom/findUpClassName';
import getVisibleRect from '../helpers/dom/getVisibleRect';
import ListenerSetter from '../helpers/listenerSetter';
import {makeMediaSize} from '../helpers/mediaSize';
import {getMiddleware, Middleware} from '../helpers/middleware';
import {doubleRaf} from '../helpers/schedulers';
import pause from '../helpers/schedulers/pause';
import windowSize from '../helpers/windowSize';
import {MyDocument} from '../lib/appManagers/appDocsManager';
import getStickerEffectThumb from '../lib/appManagers/utils/stickers/getStickerEffectThumb';
import wrapEmojiText from '../lib/richTextProcessor/wrapEmojiText';
import {CustomEmojiElement} from '../lib/richTextProcessor/wrapRichText';
import lottieLoader from '../lib/rlottie/lottieLoader';
import RLottiePlayer from '../lib/rlottie/rlottiePlayer';
import rootScope from '../lib/rootScope';
import animationIntersector, {AnimationItemGroup} from './animationIntersector';
import SetTransition from './singleTransition';
import wrapSticker from './wrappers/sticker';
import {STICKER_EFFECT_MULTIPLIER} from './wrappers/sticker';
let hasViewer = false;
export default function attachStickerViewerListeners({listenTo, listenerSetter, selector, findTarget: originalFindTarget}: {
listenerSetter: ListenerSetter,
listenTo: HTMLElement,
selector?: string,
findTarget?: (e: MouseEvent) => HTMLElement
}) {
if(IS_TOUCH_SUPPORTED) {
return;
}
const findTarget = (e: MouseEvent, checkForParent?: boolean) => {
let el: HTMLElement;
if(originalFindTarget) el = originalFindTarget(e);
else {
const s = selector || `.media-sticker-wrapper`;
el = (e.target as HTMLElement).closest(s) as HTMLElement;
}
return el && (!checkForParent || findUpAsChild(el, listenTo)) ? el : undefined;
};
const managers = rootScope.managers;
listenerSetter.add(listenTo)('mousedown', (e) => {
if(hasViewer || e.buttons > 1 || e.button !== 0) return;
let mediaContainer = findTarget(e);
if(!mediaContainer) {
return;
}
// const img: HTMLImageElement = mediaContainer.querySelector('img.media-sticker');
const docId = mediaContainer.dataset.docId;
if(!docId) {
return;
}
const className = 'sticker-viewer';
const group: AnimationItemGroup = 'STICKER-VIEWER';
const openDuration = 200;
const switchDuration = 200;
const previousGroup = animationIntersector.getOnlyOnePlayableGroup();
const _middleware = getMiddleware();
let container: HTMLElement, previousTransformer: HTMLElement;
const doThatSticker = async({mediaContainer, doc, middleware, lockGroups, isSwitching}: {
mediaContainer: HTMLElement,
doc: MyDocument,
middleware: Middleware,
lockGroups?: boolean,
isSwitching?: boolean
}) => {
const effectThumb = getStickerEffectThumb(doc);
const mediaRect: DOMRect = mediaContainer.getBoundingClientRect();
const s = makeMediaSize(doc.w, doc.h);
const size = effectThumb ? 280 : 360;
const boxSize = makeMediaSize(size, size);
const fitted = mediaRect.width === mediaRect.height ? boxSize : s.aspectFitted(boxSize);
const bubble = findUpClassName(mediaContainer, 'bubble');
const isOut = bubble ? bubble.classList.contains('is-out') : true;
const transformer = document.createElement('div');
transformer.classList.add(className + '-transformer');
const stickerContainer = document.createElement('div');
stickerContainer.classList.add(className + '-sticker');
/* transformer.style.width = */stickerContainer.style.width = fitted.width + 'px';
/* transformer.style.height = */stickerContainer.style.height = fitted.height + 'px';
const stickerEmoji = document.createElement('div');
stickerEmoji.classList.add(className + '-emoji');
stickerEmoji.append(wrapEmojiText(doc.stickerEmojiRaw));
if(effectThumb) {
const margin = (size * STICKER_EFFECT_MULTIPLIER - size) / 3 * (isOut ? 1 : -1);
transformer.classList.add('has-effect');
// const property = `--margin-${isOut ? 'right' : 'left'}`;
// stickerContainer.style.setProperty(property, `${margin * 2}px`);
transformer.style.setProperty('--translateX', `${margin}px`);
stickerEmoji.style.setProperty('--translateX', `${-margin}px`);
}
const overflowElement = findUpClassName(mediaContainer, 'scrollable');
const visibleRect = getVisibleRect(mediaContainer, overflowElement, true, mediaRect);
if(visibleRect.overflow.vertical || visibleRect.overflow.horizontal) {
stickerContainer.classList.add('is-overflow');
}
// if(img) {
// const ratio = img.naturalWidth / img.naturalHeight;
// if((mediaRect.width / mediaRect.height).toFixed(1) !== ratio.toFixed(1)) {
// mediaRect = mediaRect.toJSON();
// }
// }
const rect = mediaContainer.getBoundingClientRect();
const scaleX = rect.width / fitted.width;
const scaleY = rect.height / fitted.height;
const transformX = rect.left - (windowSize.width - rect.width) / 2;
const transformY = rect.top - (windowSize.height - rect.height) / 2;
transformer.style.transform = `translate(${transformX}px, ${transformY}px) scale(${scaleX}, ${scaleY})`;
if(isSwitching) transformer.classList.add('is-switching');
transformer.append(stickerContainer, stickerEmoji);
container.append(transformer);
const o = await wrapSticker({
doc,
div: stickerContainer,
group,
width: fitted.width,
height: fitted.height,
play: false,
loop: true,
middleware,
managers,
needFadeIn: false,
isOut,
withThumb: false,
relativeEffect: true,
loopEffect: true
}).then(({render}) => render);
if(!middleware()) return;
if(!container.parentElement) {
document.body.append(container);
}
const player = Array.isArray(o) ? o[0] : o;
const firstFramePromise = player instanceof RLottiePlayer ?
new Promise<void>((resolve) => player.addEventListener('firstFrame', resolve, {once: true})) :
Promise.resolve();
await Promise.all([firstFramePromise, doubleRaf()]);
await pause(0); // ! need it because firstFrame will be called just from the loop
if(!middleware()) return;
if(lockGroups) {
animationIntersector.setOnlyOnePlayableGroup(group);
animationIntersector.checkAnimations2(true);
}
if(player instanceof RLottiePlayer) {
const prevPlayer = mediaContainer instanceof CustomEmojiElement ?
mediaContainer.player as RLottiePlayer :
lottieLoader.getAnimation(mediaContainer);
if(prevPlayer) {
player.curFrame = prevPlayer.curFrame;
player.play();
await new Promise<void>((resolve) => {
let i = 0;
const c = () => {
if(++i === 2) {
resolve();
player.removeEventListener('enterFrame', c);
}
};
player.addEventListener('enterFrame', c);
});
if(!middleware()) return;
player.pause();
}
} else if(player instanceof HTMLVideoElement) {
const prevPlayer = mediaContainer.querySelector<HTMLVideoElement>('video');
if(prevPlayer) {
player.currentTime = prevPlayer.currentTime;
}
}
return {
ready: () => {
if(player instanceof RLottiePlayer || player instanceof HTMLVideoElement) {
player.play();
}
if(effectThumb) {
simulateClickEvent(stickerContainer);
}
},
transformer
};
};
const timeout = window.setTimeout(async() => {
document.removeEventListener('mousemove', onMousePreMove);
container = document.createElement('div');
container.classList.add(className);
hasViewer = true;
const middleware = _middleware.get();
const doc = await managers.appDocsManager.getDoc(docId);
if(!middleware()) return;
let result: Awaited<ReturnType<typeof doThatSticker>>;
try {
result = await doThatSticker({
doc,
mediaContainer,
middleware,
lockGroups: true
});
if(!result) return;
} catch(err) {
return;
}
const {ready, transformer} = result;
previousTransformer = transformer;
SetTransition({
element: container,
className: 'is-visible',
forwards: true,
duration: openDuration,
onTransitionEnd: () => {
if(!middleware()) return;
ready();
}
});
document.addEventListener('mousemove', onMouseMove);
}, 125);
const onMouseMove = async(e: MouseEvent) => {
const newMediaContainer = findTarget(e, true);
if(!newMediaContainer || mediaContainer === newMediaContainer) {
return;
}
const docId = newMediaContainer.dataset.docId;
if(!docId) {
return;
}
mediaContainer = newMediaContainer;
_middleware.clean();
const middleware = _middleware.get();
const doc = await managers.appDocsManager.getDoc(docId);
if(!middleware()) return;
let r: Awaited<ReturnType<typeof doThatSticker>>;
try {
r = await doThatSticker({
doc,
mediaContainer,
middleware,
isSwitching: true
});
if(!r) return;
} catch(err) {
console.error('sticker viewer error', err);
return;
}
const {ready, transformer} = r;
const _previousTransformer = previousTransformer;
SetTransition({
element: _previousTransformer,
className: 'is-switching',
forwards: true,
duration: switchDuration,
onTransitionEnd: () => {
_previousTransformer.remove();
}
});
previousTransformer = transformer;
SetTransition({
element: transformer,
className: 'is-switching',
forwards: false,
duration: switchDuration,
onTransitionEnd: () => {
if(!middleware()) return;
ready();
}
});
};
const onMousePreMove = (e: MouseEvent) => {
if(!findUpAsChild(e.target as HTMLElement, mediaContainer)) {
onMouseUp();
}
};
const onMouseUp = () => {
clearTimeout(timeout);
_middleware.clean();
if(container) {
SetTransition({
element: container,
className: 'is-visible',
forwards: false,
duration: openDuration,
onTransitionEnd: () => {
container.remove();
animationIntersector.setOnlyOnePlayableGroup(previousGroup);
animationIntersector.checkAnimations2(false);
hasViewer = false;
}
});
attachClickEvent(document.body, cancelEvent, {capture: true, once: true});
}
document.removeEventListener('mousemove', onMousePreMove);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp, {capture: true});
};
document.addEventListener('mousemove', onMousePreMove);
document.addEventListener('mouseup', onMouseUp, {once: true, capture: true});
});
}