diff --git a/entry_types/scrolled/package/spec/frontend/features/backdropAnimationEffects-spec.js b/entry_types/scrolled/package/spec/frontend/features/backdropAnimationEffects-spec.js index 1081783e5..181f01ff3 100644 --- a/entry_types/scrolled/package/spec/frontend/features/backdropAnimationEffects-spec.js +++ b/entry_types/scrolled/package/spec/frontend/features/backdropAnimationEffects-spec.js @@ -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(); @@ -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', @@ -194,23 +199,17 @@ 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: [ @@ -218,39 +217,19 @@ describe('backdrop animation effects', () => { 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: [ @@ -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: [ @@ -298,6 +277,6 @@ describe('backdrop animation effects', () => { getSectionByPermaId(1).simulateScrollingIntoView(); - expect(animateMock).not.toHaveBeenCalled(); + expect(container.querySelector(`.${effectsStyles.autoZoom}`)).toBeNull(); }); }); diff --git a/entry_types/scrolled/package/spec/frontend/v1/Backdrop/Effects-spec.js b/entry_types/scrolled/package/spec/frontend/v1/Backdrop/Effects-spec.js index 4b80e3136..341c08b0d 100644 --- a/entry_types/scrolled/package/spec/frontend/v1/Backdrop/Effects-spec.js +++ b/entry_types/scrolled/package/spec/frontend/v1/Backdrop/Effects-spec.js @@ -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(); + }); }); diff --git a/entry_types/scrolled/package/src/frontend/Backdrop.module.css b/entry_types/scrolled/package/src/frontend/Backdrop.module.css index 0de6598b3..5c30db1a9 100644 --- a/entry_types/scrolled/package/src/frontend/Backdrop.module.css +++ b/entry_types/scrolled/package/src/frontend/Backdrop.module.css @@ -11,10 +11,6 @@ background-color: #333; } -.effects { - height: 100%; -} - @media print { .Backdrop { page-break-inside: avoid; diff --git a/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.js b/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.js index 05f71326f..c5ea2a89d 100644 --- a/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.js +++ b/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.js @@ -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'; @@ -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; @@ -34,6 +37,7 @@ export function Effects({file, children}) { fill: 'forwards', timeline: sectionViewTimeline, rangeStart: 'cover 0%', + composite: 'add', rangeEnd: 'cover 100%' } ); @@ -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 (
+ className={classNames(styles.effects, + {[styles.autoZoom]: autoZoomRunning})} + style={{filter: getFilter(file?.effects || []), + ...getAutoZoomProperties(autoZoomValue, file)}}> {children}
); @@ -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)`; } @@ -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` + }; } diff --git a/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.module.css b/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.module.css new file mode 100644 index 000000000..18a84d111 --- /dev/null +++ b/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.module.css @@ -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)); + } +}