Skip to content

Commit

Permalink
Workaround Chrome auto zoom animation bug
Browse files Browse the repository at this point in the history
When applying the auto-zoom effect, Chrome sometimes stopped painting
the backdrop, showing the dark default backdrop instead. Using a CSS
keyframe animation instead of calling `animate` appears to avoid the
problem.

REDMINE-20789
  • Loading branch information
tf committed Jul 23, 2024
1 parent 2ed1fe9 commit d0e2c3b
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 75 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {renderEntry, usePageObjects} from 'support/pageObjects';
import '@testing-library/jest-dom/extend-expect'

import effectsStyles from 'frontend/v1/Backdrop/Effects.module.css';

import {usePortraitOrientation} from 'frontend/usePortraitOrientation';
jest.mock('frontend/usePortraitOrientation')
jest.mock('frontend/usePortraitOrientation');

describe('backdrop animation effects', () => {
usePageObjects();
Expand Down Expand Up @@ -173,14 +175,17 @@ describe('backdrop animation effects', () => {
});

it('supports auto zoom', () => {
const {getSectionByPermaId} = renderEntry({
const {getSectionByPermaId, container} = renderEntry({
seed: {
imageFiles: [{permaId: 100}],
sections: [
{
permaId: 1,
configuration: {
backdrop: {image: 100},
backdrop: {
image: 100,
imageMotifArea: {left: 0, top: 80, width: 10, height: 20}
},
backdropEffects: [
{
name: 'autoZoom',
Expand All @@ -194,63 +199,37 @@ describe('backdrop animation effects', () => {
});

getSectionByPermaId(1).simulateScrollingIntoView();
const autoZoomElement = container.querySelector(`.${effectsStyles.autoZoom}`);

expect(viewTimelines.length).toEqual(0);
expect(animateMock).toHaveBeenCalledWith(
{
transform: [
'translate(0%, 0%) scale(1) translate(0%, 0%)',
'translate(0%, 0%) scale(1.2) translate(0%, 0%)'
]
},
expect.objectContaining({
duration: 20500
})
);
expect(autoZoomElement).not.toBeNull();
expect(autoZoomElement).toHaveStyle('--auto-zoom-duration: 20500ms;');
expect(autoZoomElement).toHaveStyle('--auto-zoom-origin-x: 45%;');
expect(autoZoomElement).toHaveStyle('--auto-zoom-origin-y: -40%;');
});

it('uses motif area as transform prigin', () => {
const {getSectionByPermaId} = renderEntry({
it('does not set auto zoom custom properties by default', () => {
const {container} = renderEntry({
seed: {
imageFiles: [{permaId: 100}],
sections: [
{
permaId: 1,
configuration: {
backdrop: {
image: 100,
imageMotifArea: {left: 0, top: 80, width: 10, height: 20}
},
backdropEffects: [
{
name: 'autoZoom',
value: 50
}
]
image: 100
}
}
}
]
}
});

getSectionByPermaId(1).simulateScrollingIntoView();

expect(viewTimelines.length).toEqual(0);
expect(animateMock).toHaveBeenCalledWith(
{
transform: [
'translate(-45%, 40%) scale(1) translate(45%, -40%)',
'translate(-45%, 40%) scale(1.2) translate(45%, -40%)'
]
},
expect.objectContaining({
duration: 20500
})
);
expect(container.querySelector('[style*="--auto-zoom"]')).toBeNull();
});

it('only triggers auto zoom once visible', () => {
renderEntry({
const {container} = renderEntry({
seed: {
imageFiles: [{permaId: 100}],
sections: [
Expand All @@ -270,13 +249,13 @@ describe('backdrop animation effects', () => {
}
});

expect(animateMock).not.toHaveBeenCalled();
expect(container.querySelector(`.${effectsStyles.autoZoom}`)).toBeNull();
});

it('does not autozoom if reduced motion is preferred', () => {
window.matchMedia.mockPrefersReducedMotion();

const {getSectionByPermaId} = renderEntry({
const {getSectionByPermaId, container} = renderEntry({
seed: {
imageFiles: [{permaId: 100}],
sections: [
Expand All @@ -298,6 +277,6 @@ describe('backdrop animation effects', () => {

getSectionByPermaId(1).simulateScrollingIntoView();

expect(animateMock).not.toHaveBeenCalled();
expect(container.querySelector(`.${effectsStyles.autoZoom}`)).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,18 @@ describe('Backdrop Effects getFilter', () => {

expect(result).toEqual('grayscale(20%)');
});

it('returns null by default', () => {
const result = getFilter([]);

expect(result).toBeNull();
});

it('returns null for animation effects', () => {
const result = getFilter([
{name: 'autoZoom', value: 70}
]);

expect(result).toBeNull();
});
});
4 changes: 0 additions & 4 deletions entry_types/scrolled/package/src/frontend/Backdrop.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
background-color: #333;
}

.effects {
height: 100%;
}

@media print {
.Backdrop {
page-break-inside: avoid;
Expand Down
60 changes: 32 additions & 28 deletions entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, {useRef} from 'react';
import React, {useRef, useState} from 'react';
import classNames from 'classnames';
import {useIsomorphicLayoutEffect} from '../../useIsomorphicLayoutEffect';

import styles from '../../Backdrop.module.css';
import styles from './Effects.module.css';

import {useSectionViewTimeline} from '../../SectionViewTimelineProvider';
import {useSectionLifecycle} from '../../useSectionLifecycle';
Expand All @@ -18,6 +19,8 @@ export function Effects({file, children}) {
const scrollParallaxValue = getEffectValue(file, 'scrollParallax');
const autoZoomValue = getEffectValue(file, 'autoZoom');

const [autoZoomRunning, setAutoZoomRunning] = useState(false);

useIsomorphicLayoutEffect(() => {
if (scrollParallaxValue && !isStaticPreview && sectionViewTimeline) {
const max = 20 * scrollParallaxValue / 100;
Expand All @@ -34,6 +37,7 @@ export function Effects({file, children}) {
fill: 'forwards',
timeline: sectionViewTimeline,
rangeStart: 'cover 0%',
composite: 'add',
rangeEnd: 'cover 100%'
}
);
Expand All @@ -42,35 +46,18 @@ export function Effects({file, children}) {
}
}, [sectionViewTimeline, scrollParallaxValue, isStaticPreview]);

const x = file?.motifArea ? 50 - (file.motifArea.left + file.motifArea.width / 2) : 0;
const y = file?.motifArea ? 50 - (file.motifArea.top + file.motifArea.height / 2) : 0;

useIsomorphicLayoutEffect(() => {
if (autoZoomValue && isVisible && !prefersReducedMotion()) {
const animation = ref.current.animate(
{
transform: [
`translate(${-x}%, ${-y}%) scale(1) translate(${x}%, ${y}%)`,
`translate(${-x}%, ${-y}%) scale(1.2) translate(${x}%, ${y}%)`,
]
},
{
iterations: 1,
fill: 'forwards',
duration: 1000 * (autoZoomValue / 100) + 40000 * (1 - autoZoomValue / 100),
composite: 'add',
easing: 'ease'
}
);

return () => animation.cancel();
}
}, [autoZoomValue, isVisible, x, y]);
useIsomorphicLayoutEffect(() => {
setAutoZoomRunning(autoZoomValue && isVisible && !prefersReducedMotion());
}, [autoZoomValue, isVisible]);

return (
<div ref={ref}
className={styles.effects}
style={{filter: getFilter(file?.effects || [])}}>
className={classNames(styles.effects,
{[styles.autoZoom]: autoZoomRunning})}
style={{filter: getFilter(file?.effects || []),
...getAutoZoomProperties(autoZoomValue, file)}}>
{children}
</div>
);
Expand All @@ -81,7 +68,7 @@ function getEffectValue(file, name) {
}

export function getFilter(effects) {
return effects.map(effect => {
const components = effects.map(effect => {
if (effect.name === 'blur') {
return `blur(${effect.value / 100 * 10}px)`;
}
Expand All @@ -94,5 +81,22 @@ export function getFilter(effects) {
else if (['grayscale', 'sepia'].includes(effect.name)) {
return `${effect.name}(${effect.value}%)`;
}
}).filter(Boolean).join(' ');
}).filter(Boolean);

return components.length ? components.join(' ') : null;
}

function getAutoZoomProperties(autoZoomValue, file) {
if (!autoZoomValue) {
return null;
}

const x = file?.motifArea ? 50 - (file.motifArea.left + file.motifArea.width / 2) : 0;
const y = file?.motifArea ? 50 - (file.motifArea.top + file.motifArea.height / 2) : 0;

return {
'--auto-zoom-origin-x': `${x}%`,
'--auto-zoom-origin-y': `${y}%`,
'--auto-zoom-duration': `${1000 * (autoZoomValue / 100) + 40000 * (1 - autoZoomValue / 100)}ms`
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.effects {
height: 100%;
}

.autoZoom {
animation: autoZoom var(--auto-zoom-duration) 1 ease;
animation-fill-mode: forwards;
}

@keyframes autoZoom {
from {
transform: translate(calc(-1 * var(--auto-zoom-origin-x)), calc(-1 * var(--auto-zoom-origin-y)))
scale(1)
translate(var(--auto-zoom-origin-x), var(--auto-zoom-origin-y));
}
to {
transform: translate(calc(-1 * var(--auto-zoom-origin-x)), calc(-1 * var(--auto-zoom-origin-y)))
scale(1.2)
translate(var(--auto-zoom-origin-x), var(--auto-zoom-origin-y));
}
}

0 comments on commit d0e2c3b

Please sign in to comment.