240 lines
7.2 KiB
TypeScript
240 lines
7.2 KiB
TypeScript
import type { IDimensions } from '../../../types';
|
|
import { StoryViewerOrigin } from '../../../types';
|
|
|
|
import { ANIMATION_END_DELAY } from '../../../config';
|
|
import fastBlur from '../../../lib/fastBlur';
|
|
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
|
import { getPeerStoryHtmlId } from '../../../global/helpers';
|
|
import { applyStyles } from '../../../util/animation';
|
|
import stopEvent from '../../../util/stopEvent';
|
|
import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/windowEnvironment';
|
|
import windowSize from '../../../util/windowSize';
|
|
import { REM } from '../../common/helpers/mediaDimensions';
|
|
|
|
import storyRibbonStyles from '../StoryRibbon.module.scss';
|
|
import styles from '../StoryViewer.module.scss';
|
|
|
|
const ANIMATION_DURATION = 200;
|
|
const OFFSET_BOTTOM = 3.5 * REM;
|
|
const MOBILE_OFFSET = 0.5 * REM;
|
|
const MOBILE_WIDTH = 600;
|
|
|
|
export function animateOpening(
|
|
userId: string,
|
|
origin: StoryViewerOrigin,
|
|
thumb: string,
|
|
bestImageData: string | undefined,
|
|
dimensions: IDimensions,
|
|
) {
|
|
const { mediaEl: fromImage } = getNodes(origin, userId);
|
|
if (!fromImage) {
|
|
return;
|
|
}
|
|
const { width: windowWidth, height: windowHeight } = windowSize.get();
|
|
let { width: toWidth, height: toHeight } = dimensions;
|
|
|
|
const isMobile = windowWidth <= MOBILE_WIDTH;
|
|
|
|
if (isMobile) {
|
|
toWidth = windowWidth - 2 * MOBILE_OFFSET;
|
|
toHeight = windowHeight - OFFSET_BOTTOM - 2 * MOBILE_OFFSET;
|
|
|
|
const safeAreaBottom = getComputedStyle(document.documentElement).getPropertyValue('--safe-area-bottom');
|
|
if (safeAreaBottom) {
|
|
toHeight -= parseFloat(safeAreaBottom);
|
|
}
|
|
}
|
|
|
|
const toLeft = isMobile ? MOBILE_OFFSET : (windowWidth - toWidth) / 2;
|
|
const toTop = isMobile ? MOBILE_OFFSET : (windowHeight - (toHeight + OFFSET_BOTTOM)) / 2;
|
|
|
|
const {
|
|
top: fromTop, left: fromLeft, width: fromWidth, height: fromHeight,
|
|
} = fromImage.getBoundingClientRect();
|
|
|
|
const fromTranslateX = (fromLeft + fromWidth / 2) - (toLeft + toWidth / 2);
|
|
const fromTranslateY = (fromTop + fromHeight / 2) - (toTop + toHeight / 2);
|
|
const fromScaleX = fromWidth / toWidth;
|
|
const fromScaleY = fromHeight / toHeight;
|
|
|
|
requestMutation(() => {
|
|
const ghost = createGhost(bestImageData || thumb, !bestImageData);
|
|
applyStyles(ghost, {
|
|
top: `${toTop}px`,
|
|
left: `${toLeft}px`,
|
|
width: `${toWidth}px`,
|
|
height: `${toHeight}px`,
|
|
transform: `translate3d(${fromTranslateX}px, ${fromTranslateY}px, 0) scale(${fromScaleX}, ${fromScaleY})`,
|
|
});
|
|
|
|
const container = document.getElementById('StoryViewer')!;
|
|
container.appendChild(ghost);
|
|
document.body.classList.add('ghost-animating');
|
|
|
|
requestMutation(() => {
|
|
applyStyles(ghost, {
|
|
transform: '',
|
|
});
|
|
|
|
setTimeout(() => {
|
|
requestMutation(() => {
|
|
if (container.contains(ghost)) {
|
|
container.removeChild(ghost);
|
|
}
|
|
document.body.classList.remove('ghost-animating');
|
|
});
|
|
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
|
});
|
|
});
|
|
}
|
|
|
|
export function animateClosing(
|
|
userId: string,
|
|
origin: StoryViewerOrigin,
|
|
bestImageData: string,
|
|
) {
|
|
const { mediaEl: toImage } = getNodes(origin, userId);
|
|
|
|
const fromImage = document.getElementById('StoryViewer')!.querySelector<HTMLImageElement>(
|
|
`.${styles.mobileSlide} .${styles.media}, .${styles.activeSlide} .${styles.media}`,
|
|
);
|
|
if (!fromImage || !toImage) {
|
|
return;
|
|
}
|
|
const {
|
|
top: fromTop, left: fromLeft, width: fromWidth, height: fromHeight,
|
|
} = fromImage.getBoundingClientRect();
|
|
const {
|
|
top: toTop, left: toLeft, width: toWidth, height: toHeight,
|
|
} = toImage.getBoundingClientRect();
|
|
|
|
const toTranslateX = (toLeft + toWidth / 2) - (fromLeft + fromWidth / 2);
|
|
const toTranslateY = (toTop + toHeight / 2) - (fromTop + fromHeight / 2);
|
|
const toScaleX = toWidth / fromWidth;
|
|
const toScaleY = toHeight / fromHeight;
|
|
|
|
requestMutation(() => {
|
|
const ghost = createGhost(bestImageData);
|
|
applyStyles(ghost, {
|
|
top: `${fromTop}px`,
|
|
left: `${fromLeft}px`,
|
|
width: `${fromWidth}px`,
|
|
height: `${fromHeight}px`,
|
|
});
|
|
|
|
const ghost2 = createGhost(toImage.src, undefined, true);
|
|
const ghost2Top = (fromTop + fromHeight / 2) - fromWidth / 2;
|
|
applyStyles(ghost2, {
|
|
top: `${ghost2Top}px`,
|
|
left: `${fromLeft}px`,
|
|
width: `${fromWidth}px`,
|
|
height: `${fromWidth}px`,
|
|
});
|
|
|
|
const container = document.getElementById('StoryViewer')!;
|
|
container.appendChild(ghost);
|
|
document.body.appendChild(ghost2);
|
|
document.body.classList.add('ghost-animating');
|
|
|
|
requestMutation(() => {
|
|
applyStyles(ghost, {
|
|
transform: `translate3d(${toTranslateX}px, ${toTranslateY}px, 0) scale(${toScaleX}, ${toScaleY})`,
|
|
});
|
|
|
|
applyStyles(ghost2, {
|
|
transform: `translate3d(${toTranslateX}px, ${toTranslateY}px, 0) scale(${toScaleX})`,
|
|
opacity: '1',
|
|
});
|
|
|
|
setTimeout(() => {
|
|
requestMutation(() => {
|
|
if (container.contains(ghost)) {
|
|
container.removeChild(ghost);
|
|
}
|
|
|
|
if (document.body.contains(ghost2)) {
|
|
document.body.removeChild(ghost2);
|
|
}
|
|
|
|
document.body.classList.remove('ghost-animating');
|
|
});
|
|
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
|
});
|
|
});
|
|
}
|
|
|
|
const RADIUS = 2;
|
|
const ITERATIONS = 2;
|
|
|
|
function createGhost(source: string, hasBlur = false, isGhost2 = false) {
|
|
const ghost = document.createElement('div');
|
|
ghost.classList.add(!isGhost2 ? styles.ghost : styles.ghost2);
|
|
|
|
const img = new Image();
|
|
img.draggable = false;
|
|
img.oncontextmenu = stopEvent;
|
|
img.classList.add(styles.ghostImage);
|
|
|
|
if (hasBlur) {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.classList.add(styles.thumbnail);
|
|
img.onload = () => {
|
|
const ctx = canvas.getContext('2d', { alpha: false })!;
|
|
const {
|
|
width,
|
|
height,
|
|
} = img;
|
|
|
|
requestMutation(() => {
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
if (IS_CANVAS_FILTER_SUPPORTED) {
|
|
ctx.filter = `blur(${RADIUS}px)`;
|
|
}
|
|
|
|
ctx.drawImage(img, -RADIUS * 2, -RADIUS * 2, width + RADIUS * 4, height + RADIUS * 4);
|
|
|
|
if (!IS_CANVAS_FILTER_SUPPORTED) {
|
|
fastBlur(ctx, 0, 0, width, height, RADIUS, ITERATIONS);
|
|
}
|
|
});
|
|
};
|
|
img.src = source;
|
|
ghost.appendChild(canvas);
|
|
} else {
|
|
img.src = source;
|
|
ghost.appendChild(img);
|
|
}
|
|
|
|
return ghost;
|
|
}
|
|
|
|
function getNodes(origin: StoryViewerOrigin, userId: string) {
|
|
let containerSelector;
|
|
const mediaSelector = `#${getPeerStoryHtmlId(userId)}`;
|
|
|
|
switch (origin) {
|
|
case StoryViewerOrigin.StoryRibbon:
|
|
containerSelector = `#LeftColumn .${storyRibbonStyles.root}`;
|
|
break;
|
|
case StoryViewerOrigin.MiddleHeaderAvatar:
|
|
containerSelector = '.MiddleHeader .Transition_slide-active .ChatInfo';
|
|
break;
|
|
case StoryViewerOrigin.ChatList:
|
|
containerSelector = '#LeftColumn .chat-list';
|
|
break;
|
|
case StoryViewerOrigin.SearchResult:
|
|
containerSelector = '#LeftColumn .LeftSearch--container';
|
|
break;
|
|
}
|
|
|
|
const container = document.querySelector<HTMLElement>(containerSelector)!;
|
|
const mediaEls = container && container.querySelectorAll<HTMLImageElement>(`${mediaSelector} img`);
|
|
|
|
return {
|
|
container,
|
|
mediaEl: mediaEls?.[0],
|
|
};
|
|
}
|