2021-08-27 21:05:46 +03:00

376 lines
11 KiB
TypeScript

import { ApiMessage, ApiDimensions } from '../../../api/types';
import { MediaViewerOrigin } from '../../../types';
import { ANIMATION_END_DELAY } from '../../../config';
import {
calculateDimensions,
getMediaViewerAvailableDimensions,
MEDIA_VIEWER_MEDIA_QUERY,
REM,
} from '../../common/helpers/mediaDimensions';
import windowSize from '../../../util/windowSize';
const ANIMATION_DURATION = 200;
export function animateOpening(
hasFooter: boolean,
origin: MediaViewerOrigin,
bestImageData: string,
dimensions: ApiDimensions,
isVideo: boolean,
message?: ApiMessage,
) {
const { mediaEl: fromImage } = getNodes(origin, message);
if (!fromImage) {
return;
}
const { width: windowWidth } = windowSize.get();
const {
width: availableWidth, height: availableHeight,
} = getMediaViewerAvailableDimensions(hasFooter, isVideo);
const { width: toWidth, height: toHeight } = calculateDimensions(
availableWidth, availableHeight, dimensions.width, dimensions.height,
);
const toLeft = (windowWidth - toWidth) / 2;
const toTop = getTopOffset(hasFooter) + (availableHeight - toHeight) / 2;
let {
top: fromTop, left: fromLeft, width: fromWidth, height: fromHeight,
} = fromImage.getBoundingClientRect();
if ([
MediaViewerOrigin.SharedMedia,
MediaViewerOrigin.Album,
MediaViewerOrigin.ScheduledAlbum,
MediaViewerOrigin.SearchResult,
].includes(origin)) {
const uncovered = uncover(toWidth, toHeight, fromTop, fromLeft, fromWidth, fromHeight);
fromTop = uncovered.top;
fromLeft = uncovered.left;
fromWidth = uncovered.width;
fromHeight = uncovered.height;
}
const fromTranslateX = (fromLeft + fromWidth / 2) - (toLeft + toWidth / 2);
const fromTranslateY = (fromTop + fromHeight / 2) - (toTop + toHeight / 2);
const fromScaleX = fromWidth / toWidth;
const fromScaleY = fromHeight / toHeight;
const ghost = createGhost(bestImageData || fromImage);
applyStyles(ghost, {
top: `${toTop}px`,
left: `${toLeft}px`,
width: `${toWidth}px`,
height: `${toHeight}px`,
transform: `translate3d(${fromTranslateX}px, ${fromTranslateY}px, 0) scale(${fromScaleX}, ${fromScaleY})`,
});
applyShape(ghost, origin);
document.body.classList.add('ghost-animating');
requestAnimationFrame(() => {
document.body.appendChild(ghost);
requestAnimationFrame(() => {
ghost.style.transform = '';
clearShape(ghost);
setTimeout(() => {
requestAnimationFrame(() => {
if (document.body.contains(ghost)) {
document.body.removeChild(ghost);
}
document.body.classList.remove('ghost-animating');
});
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
});
});
}
export function animateClosing(origin: MediaViewerOrigin, bestImageData: string, message?: ApiMessage) {
const { container, mediaEl: toImage } = getNodes(origin, message);
if (!toImage) {
return;
}
const fromImage = document.getElementById('MediaViewer')!.querySelector<HTMLImageElement>(
'.active .media-viewer-content img, .active .media-viewer-content video',
);
if (!fromImage || !toImage) {
return;
}
const {
top: fromTop, left: fromLeft, width: fromWidth, height: fromHeight,
} = fromImage.getBoundingClientRect();
const {
top: targetTop, left: toLeft, width: toWidth, height: toHeight,
} = toImage.getBoundingClientRect();
let toTop = targetTop;
if (!isElementInViewport(container)) {
const { height: windowHeight } = windowSize.get();
toTop = targetTop < fromTop ? -toHeight : windowHeight;
}
const fromTranslateX = (fromLeft + fromWidth / 2) - (toLeft + toWidth / 2);
const fromTranslateY = (fromTop + fromHeight / 2) - (toTop + toHeight / 2);
let fromScaleX = fromWidth / toWidth;
let fromScaleY = fromHeight / toHeight;
const shouldFadeOut = (
[MediaViewerOrigin.Inline, MediaViewerOrigin.ScheduledInline].includes(origin)
&& !isMessageImageFullyVisible(container, toImage)
) || (
[MediaViewerOrigin.Album, MediaViewerOrigin.ScheduledAlbum].includes(origin)
&& !isMessageImageFullyVisible(container, toImage)
);
if ([
MediaViewerOrigin.SharedMedia,
MediaViewerOrigin.Album,
MediaViewerOrigin.ScheduledAlbum,
MediaViewerOrigin.SearchResult,
].includes(origin)) {
if (fromScaleX > fromScaleY) {
fromScaleX = fromScaleY;
} else if (fromScaleY > fromScaleX) {
fromScaleY = fromScaleX;
}
}
const existingGhost = document.getElementsByClassName('ghost')[0] as HTMLDivElement;
const ghost = existingGhost || createGhost(bestImageData || toImage, origin);
if (!existingGhost) {
applyStyles(ghost, {
top: `${toTop}px`,
left: `${toLeft}px`,
width: `${toWidth}px`,
height: `${toHeight}px`,
transform: `translate3d(${fromTranslateX}px, ${fromTranslateY}px, 0) scale(${fromScaleX}, ${fromScaleY})`,
});
}
requestAnimationFrame(() => {
if (existingGhost) {
const {
top,
left,
width,
height,
} = existingGhost.getBoundingClientRect();
const scaleX = width / toWidth;
const scaleY = height / toHeight;
applyStyles(ghost, {
transition: 'none',
top: `${toTop}px`,
left: `${toLeft}px`,
transformOrigin: 'top left',
transform: `translate3d(${left - toLeft}px, ${top - toTop}px, 0) scale(${scaleX}, ${scaleY})`,
width: `${toWidth}px`,
height: `${toHeight}px`,
});
}
document.body.classList.add('ghost-animating');
if (!existingGhost) document.body.appendChild(ghost);
requestAnimationFrame(() => {
if (existingGhost) {
existingGhost.style.transition = '';
}
ghost.style.transform = '';
if (shouldFadeOut) {
ghost.style.opacity = '0';
}
applyShape(ghost, origin);
setTimeout(() => {
requestAnimationFrame(() => {
if (document.body.contains(ghost)) {
document.body.removeChild(ghost);
}
document.body.classList.remove('ghost-animating');
});
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
});
});
}
function createGhost(source: string | HTMLImageElement | HTMLVideoElement, origin?: MediaViewerOrigin) {
const ghost = document.createElement('div');
ghost.classList.add('ghost');
const img = new Image();
if (typeof source === 'string') {
img.src = source;
} else if (source instanceof HTMLVideoElement) {
img.src = source.poster;
} else {
img.src = source.src;
}
ghost.appendChild(img);
if (origin === MediaViewerOrigin.ProfileAvatar || origin === MediaViewerOrigin.SettingsAvatar) {
ghost.classList.add('ProfileInfo');
if (origin === MediaViewerOrigin.SettingsAvatar) {
ghost.classList.add('self');
}
const profileInfo = document.querySelector(
origin === MediaViewerOrigin.ProfileAvatar
? '#RightColumn .ProfileInfo .info'
: '#Settings .ProfileInfo .info',
);
if (profileInfo) {
ghost.appendChild(profileInfo.cloneNode(true));
}
}
return ghost;
}
function uncover(realWidth: number, realHeight: number, top: number, left: number, width: number, height: number) {
if (realWidth === realHeight) {
const size = Math.max(width, height) * (realWidth / realHeight);
left -= (size - width) / 2;
top -= (size - height) / 2;
width = size;
height = size;
} else if (realWidth > realHeight) {
const srcWidth = width;
width = height * (realWidth / realHeight);
left -= (width - srcWidth) / 2;
} else if (realHeight > realWidth) {
const srcHeight = height;
height = width * (realHeight / realWidth);
top -= (height - srcHeight) / 2;
}
return {
top, left, width, height,
};
}
function isElementInViewport(el: HTMLElement) {
if (el.style.display === 'none') {
return false;
}
const rect = el.getBoundingClientRect();
const { height: windowHeight } = windowSize.get();
return (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
}
function isMessageImageFullyVisible(container: HTMLElement, imageEl: HTMLElement) {
const messageListElement = document.querySelector<HTMLDivElement>('.active > .MessageList')!;
let imgOffsetTop = container.offsetTop + imageEl.closest<HTMLDivElement>('.content-inner, .WebPage')!.offsetTop;
if (container.id.includes('album-media-')) {
imgOffsetTop += container.parentElement!.offsetTop + container.closest<HTMLDivElement>('.Message')!.offsetTop;
}
return imgOffsetTop > messageListElement.scrollTop
&& imgOffsetTop + imageEl.offsetHeight < messageListElement.scrollTop + messageListElement.offsetHeight;
}
function getTopOffset(hasFooter: boolean) {
const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY);
let topOffsetRem = 4.125;
if (hasFooter) {
topOffsetRem += mql.matches ? 0.875 : 3.375;
}
return topOffsetRem * REM;
}
function applyStyles(element: HTMLElement, styles: Record<string, string>) {
Object.assign(element.style, styles);
}
function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
let containerSelector;
let mediaSelector;
switch (origin) {
case MediaViewerOrigin.Album:
case MediaViewerOrigin.ScheduledAlbum:
containerSelector = `.active > .MessageList #album-media-${message!.id}`;
mediaSelector = '.full-media';
break;
case MediaViewerOrigin.SharedMedia:
containerSelector = `#shared-media${message!.id}`;
mediaSelector = 'img';
break;
case MediaViewerOrigin.SearchResult:
containerSelector = `#search-media${message!.id}`;
mediaSelector = 'img';
break;
case MediaViewerOrigin.MiddleHeaderAvatar:
containerSelector = '.MiddleHeader .ChatInfo .Avatar';
mediaSelector = 'img.avatar-media';
break;
case MediaViewerOrigin.SettingsAvatar:
containerSelector = '#Settings .ProfileInfo .active .ProfilePhoto';
mediaSelector = 'img.avatar-media';
break;
case MediaViewerOrigin.ProfileAvatar:
containerSelector = '#RightColumn .ProfileInfo .active .ProfilePhoto';
mediaSelector = 'img.avatar-media';
break;
case MediaViewerOrigin.ScheduledInline:
case MediaViewerOrigin.Inline:
default:
containerSelector = `.active > .MessageList #message${message!.id}`;
mediaSelector = '.message-content .full-media, .message-content .thumbnail';
}
const container = document.querySelector<HTMLElement>(containerSelector)!;
const mediaEls = container && container.querySelectorAll<HTMLImageElement | HTMLVideoElement>(mediaSelector);
return {
container,
mediaEl: mediaEls?.[mediaEls.length - 1],
};
}
function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) {
switch (origin) {
case MediaViewerOrigin.Album:
case MediaViewerOrigin.ScheduledAlbum:
case MediaViewerOrigin.Inline:
case MediaViewerOrigin.ScheduledInline:
ghost.classList.add('rounded-corners');
break;
case MediaViewerOrigin.SharedMedia:
case MediaViewerOrigin.SettingsAvatar:
case MediaViewerOrigin.ProfileAvatar:
case MediaViewerOrigin.SearchResult:
(ghost.firstChild as HTMLElement).style.objectFit = 'cover';
break;
case MediaViewerOrigin.MiddleHeaderAvatar:
ghost.classList.add('circle');
break;
}
}
function clearShape(ghost: HTMLDivElement) {
(ghost.firstChild as HTMLElement).style.objectFit = 'default';
ghost.classList.remove('rounded-corners', 'circle');
}