Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion entry_types/scrolled/config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -775,11 +775,21 @@ de:
id:
label: Animation
playbackMode:
inline_help_html: Bestimmt, wie die Animation abgespielt wird, sobald sie sichtbar wird:<ul><li><strong>Endlosschleife:</strong> Immer wieder, solange sie sichtbar ist.</li><li><strong>Einmal abspielen:</strong> Einmal von Anfang bis Ende.</li></ul>
inline_help_html: Bestimmt, wie die Animation abgespielt wird, sobald sie sichtbar wird:<ul><li><strong>Endlosschleife:</strong> Immer wieder, solange sie sichtbar ist.</li><li><strong>Einmal abspielen:</strong> Einmal von Anfang bis Ende.</li><li><strong>Scrollposition:</strong> Bild für Bild beim Scrollen. Über die Einstellung „Scroll-Bereich“ kann gewählt werden, wann das Scrollen der Seite die Animation antreibt.</li></ul>
label: Wiedergabe-Modus
values:
loop: Endlosschleife
playOnce: Einmal abspielen
scroll: Scrollposition
scrollRange:
inline_help_html: Bestimmt, wann das Scrollen der Seite die Animation antreibt:<ul><li><strong>Während sichtbar:</strong> Von dem Moment, in dem das Element in den Viewport scrollt, bis es ihn wieder vollständig verlassen hat.</li><li><strong>Während vollständig sichtbar:</strong> Solange das Element vollständig im Viewport ist.</li><li><strong>Beim Passieren der Viewport-Mitte:</strong> Das Scrollen der Seite treibt die Animation an, während das Element die vertikale Mitte des Viewports passiert. Verfügbar für alle Elemente außer solchen mit Position „Neben dem Text (Sticky)“ oder „Stand-Alone“.</li><li><strong>Während eingerastet:</strong> Das Scrollen der Seite treibt die Animation an, solange das Element stehenbleibt. Bleibt das Element nicht stehen - in der Mobil-Darstellung, in der solche Elemente im Text verankert werden, oder wenn nicht genug Text daneben steht -, treibt das Scrollen die Animation stattdessen beim Passieren der Viewport-Mitte an. Verfügbar für Elemente mit Position „Neben dem Text (Sticky)“ oder „Stand-Alone“.</li><li><strong>Beim Hereinscrollen:</strong> Solange das Element in den Viewport scrollt.</li></ul>
label: Scroll-Bereich
values:
contain: Während vollständig sichtbar
cover: Während sichtbar
entry: Beim Hereinscrollen
inFocus: Beim Passieren der Viewport-Mitte
inFocusWhenPinned: Während eingerastet
description: Eine als dotLottie-Datei exportierte Animation einbinden
name: Lottie-Animation
tabs:
Expand Down
12 changes: 11 additions & 1 deletion entry_types/scrolled/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -760,11 +760,21 @@ en:
id:
label: Animation
playbackMode:
inline_help_html: Determines how the animation is played once it becomes visible:<ul><li><strong>Loop:</strong> Again and again as long as it is visible.</li><li><strong>Play once:</strong> Once from start to end.</li></ul>
inline_help_html: Determines how the animation is played once it becomes visible:<ul><li><strong>Loop:</strong> Again and again as long as it is visible.</li><li><strong>Play once:</strong> Once from start to end.</li><li><strong>Scroll position:</strong> Frame by frame while scrolling. Use the scroll range setting to choose when scrolling the page drives the animation.</li></ul>
label: Playback Mode
values:
loop: Loop
playOnce: Play once
scroll: Scroll position
scrollRange:
inline_help_html: Determines when scrolling the page drives the animation:<ul><li><strong>While visible:</strong> From the moment the element starts entering the viewport until it has completely left it.</li><li><strong>While completely visible:</strong> While the element is completely inside the viewport.</li><li><strong>While crossing the viewport center:</strong> Scrolling the page drives the animation while the element crosses the vertical center of the viewport. Available for all elements except those with position "Alongside (sticky)" or "Stand alone".</li><li><strong>While locked in place:</strong> Scrolling the page drives the animation while the element stays in place. If the element does not stay in place - in mobile view, where such elements are anchored in the text, or if there is not enough text next to it - scrolling drives the animation while crossing the viewport center instead. Available for elements with position "Alongside (sticky)" or "Stand alone".</li><li><strong>While entering:</strong> While the element is entering the viewport.</li></ul>
label: Scroll Range
values:
contain: While completely visible
cover: While visible
entry: While entering
inFocus: While crossing the viewport center
inFocusWhenPinned: While locked in place
description: Embed an animation exported as dotLottie file
name: Lottie animation
tabs:
Expand Down
110 changes: 107 additions & 3 deletions entry_types/scrolled/doc/creating_content_element_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ registering the content element type.
it to start media playback that should remain active even when the
element is not fully centered.

* `isActive` is true if the content element is completely in the
viewport. Use it to activate some interactive behavior like an
animation or media playback.
* `isActive` is true if the content element intersects the vertical
center of the viewport. Use it to activate some interactive behavior
like an animation or media playback.

* `inForeground` is true when the storyline containing the content
element is active (not in background mode). Use it to distinguish
Expand Down Expand Up @@ -199,6 +199,110 @@ function LoopingVideo(props) {
}
```

### View Timeline Progress

While the lifecycle hook tells a content element when it enters or
leaves the viewport, the `useContentElementViewTimelineProgress` hook
tells it how far it has travelled through the viewport. The content
element acts as the subject of a view timeline, using the same
concepts as CSS scroll driven animations. Requires the `viewTimeline`
option to be set to true when registering the content element type.

The `range` option determines which part of the timeline to measure:

* `cover` (default): From the moment the content element starts
entering the viewport until it has completely left it.

* `contain`: While the content element is completely inside the
viewport. For content elements taller than the viewport, while the
content element completely covers the viewport.

* `entry`: While the content element is entering the viewport.

* `exit`: While the content element is leaving the viewport.

* `center`: While the content element intersects the vertical center of
the viewport, i.e. from its top edge passing the center until its
bottom edge does. This is the same part of the page during which the
content element counts as active (see [Content Element
Lifecycle](#content-element-lifecycle)) and autoplayed videos play.

* `pinned`: While the content element stays pinned in the viewport,
i.e. from the moment it reaches the position it is pinned at until it
starts moving with the page again. Progress stays 1 for content
elements that are not pinned at all (see below).

* `inFocus`: While the content element holds the reader's attention:
`pinned` for content elements that are pinned in the viewport,
`center` for all others. Which of the two applies can change with the
viewport width.

Progress is passed to the `onProgress` callback as a number between 0
and 1 instead of being returned by the hook. This prevents rerendering
the content element on every scroll frame. Use it to drive imperative
APIs:

```javascript
// frontend.js

frontend.contentElementTypes.register('scrollAnimation', {
viewTimeline: true,
component: Component
});

function Component() {
const playerRef = useRef();

useContentElementViewTimelineProgress({
range: 'cover',
onProgress: progress => playerRef.current.seekTo(progress)
});

// ...
}
```

Pass a falsy `onProgress` value to not observe scroll position at all,
for example if scroll coupled behavior is optional:

```javascript
useContentElementViewTimelineProgress({
onProgress: configuration.playbackMode === 'scroll' ? seek : null
});
```

Content elements that are pinned in the viewport for part of the page
keep making progress while they stick: For `standAlone` position,
progress is measured along the scroll space added around the element.
For `sticky` position, it is measured along the group of content
elements that scrolls past the element. Ranges still refer to the
element itself, so `contain` covers the page from the element being
completely inside the viewport to it starting to leave again, no matter
how long it stays pinned in between.

Progress is measured along the element itself again whenever it is not
actually pinned: On narrow viewports, where sticky elements are
rendered inline, and if there is not enough content next to a sticky
element for it to ever reach its sticky position. The `inFocus` range
therefore measures the same part of the page as `center` for those
elements.

In specs, `renderInContentElement` provides a `simulateScrollProgress`
function to invoke the callback:

```javascript
const {simulateScrollProgress} = renderInContentElement(<Component />);

simulateScrollProgress(0.5);
```

Callbacks are invoked no matter which range they observe. Pass a
`range` option to only invoke callbacks observing that range:

```javascript
simulateScrollProgress(0.5, {range: 'pinned'});
```

## Using the Storybook

Pageflow Scrolled uses [Storybook](https://storybook.js.org/) to ease
Expand Down
1 change: 1 addition & 0 deletions entry_types/scrolled/package/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ toc:
- useAudioFocus
- useContentElementEditorState
- useContentElementLifecycle
- useContentElementViewTimelineProgress
- useCurrentChapter
- useCredits
- useDarkBackground
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,99 @@ describe('LottieAnimation', () => {
expect(players[0].pause).toHaveBeenCalled();
});

describe('scroll playback mode', () => {
const configuration = {id: 100, playbackMode: 'scroll'};

it('does not loop', () => {
renderLottieAnimation({configuration});

expect(players[0].config.loop).toBe(false);
});

it('does not play animation', () => {
renderLottieAnimation({configuration});

players[0].emit('load');

expect(players[0].play).not.toHaveBeenCalled();
});

it('sets frame matching scroll progress', () => {
const {simulateScrollProgress} = renderLottieAnimation({configuration});
players[0].emit('load');

simulateScrollProgress(0.5);

expect(players[0].setFrame).toHaveBeenCalledWith(4.5);
});

it('sets last frame at end of scroll progress', () => {
const {simulateScrollProgress} = renderLottieAnimation({configuration});
players[0].emit('load');

simulateScrollProgress(1);

expect(players[0].setFrame).toHaveBeenCalledWith(9);
});

it('does not set frame before animation has loaded', () => {
const {simulateScrollProgress} = renderLottieAnimation({configuration});

simulateScrollProgress(0.5);

expect(players[0].setFrame).not.toHaveBeenCalled();
});

it('applies scroll progress from before load once animation has loaded', () => {
const {simulateScrollProgress} = renderLottieAnimation({configuration});

simulateScrollProgress(0.5);
players[0].emit('load');

expect(players[0].setFrame).toHaveBeenCalledWith(4.5);
});

it('couples animation to cover range by default', () => {
const {simulateScrollProgress} = renderLottieAnimation({configuration});
players[0].emit('load');

simulateScrollProgress(0.5, {range: 'cover'});

expect(players[0].setFrame).toHaveBeenCalledWith(4.5);
});

it('couples animation to configured scroll range', () => {
const {simulateScrollProgress} = renderLottieAnimation({
configuration: {...configuration, scrollRange: 'inFocus'}
});
players[0].emit('load');

simulateScrollProgress(0.5, {range: 'inFocus'});

expect(players[0].setFrame).toHaveBeenCalledWith(4.5);
});

it('ignores progress along other ranges', () => {
const {simulateScrollProgress} = renderLottieAnimation({
configuration: {...configuration, scrollRange: 'inFocus'}
});
players[0].emit('load');

simulateScrollProgress(0.5, {range: 'cover'});

expect(players[0].setFrame).not.toHaveBeenCalledWith(4.5);
});

it('does not set frame in other playback modes', () => {
const {simulateScrollProgress} = renderLottieAnimation();
players[0].emit('load');

simulateScrollProgress(0.5);

expect(players[0].setFrame).not.toHaveBeenCalled();
});
});

it('destroys player on unmount', () => {
const {unmount} = renderLottieAnimation();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {editor} from 'pageflow-scrolled/editor';
import {FileInput, SelectInput, useFakeFeatures} from 'pageflow/testHelpers';
import {FileInput, SelectInput, useFakeFeatures, useFakeTranslations} from 'pageflow/testHelpers';

import {renderContentElementConfigurationEditor, useEditorGlobals} from 'support';
import {
renderContentElementConfigurationEditor,
scrollRangeNames,
useEditorGlobals
} from 'support';

import 'contentElements/lottieAnimation/editor';
import {LottieFile} from 'contentElements/lottieAnimation/editor/models/LottieFile';
Expand Down Expand Up @@ -39,10 +43,11 @@ describe('lottieAnimation/editor', () => {
});

describe('configuration editor', () => {
function renderConfigurationEditor({configuration, lottieFiles = []}) {
function renderConfigurationEditor({configuration, lottieFiles = [], layout}) {
const entry = createEntry({
filesAttributes: {lottie_files: lottieFiles},
contentElements: [{id: 1, typeName: 'lottieAnimation', configuration}]
sections: [{id: 1, configuration: {layout}}],
contentElements: [{id: 1, sectionId: 1, typeName: 'lottieAnimation', configuration}]
});

return renderContentElementConfigurationEditor({
Expand All @@ -51,14 +56,81 @@ describe('lottieAnimation/editor', () => {
});
}

useFakeTranslations({
'pageflow_scrolled.editor.content_elements.lottieAnimation.attributes.scrollRange.values.cover':
'While visible',
'pageflow_scrolled.editor.content_elements.lottieAnimation.attributes.scrollRange.values.contain':
'While completely visible',
'pageflow_scrolled.editor.content_elements.lottieAnimation.attributes.scrollRange.values.entry':
'While entering',
'pageflow_scrolled.editor.content_elements.lottieAnimation.attributes.scrollRange.values.inFocus':
'While crossing the viewport center',
'pageflow_scrolled.editor.content_elements.lottieAnimation.attributes.scrollRange.values.inFocusWhenPinned':
'While locked in place'
});

it('displays select to choose playback mode', () => {
const configurationEditor = renderConfigurationEditor({configuration: {}});

const input = SelectInput.findByPropertyName('playbackMode', {
inView: configurationEditor
});

expect(input.values()).toEqual(['loop', 'playOnce']);
expect(input.values()).toEqual(['loop', 'playOnce', 'scroll']);
});

it('displays select to choose scroll range in scroll playback mode', async () => {
const configurationEditor = renderConfigurationEditor({
configuration: {playbackMode: 'scroll'}
});

expect(await scrollRangeNames('scrollRange', {inView: configurationEditor})).toEqual([
'While visible',
'While completely visible',
'While crossing the viewport center',
'While entering'
]);
});

it('names in focus range after pinned phase for sticky position', async () => {
const configurationEditor = renderConfigurationEditor({
configuration: {playbackMode: 'scroll', position: 'sticky'}
});

expect(await scrollRangeNames('scrollRange', {inView: configurationEditor})).toEqual([
'While visible',
'While completely visible',
'While locked in place',
'While entering'
]);
});

it('names in focus range after pinned phase for standAlone position', async () => {
const configurationEditor = renderConfigurationEditor({
configuration: {playbackMode: 'scroll', position: 'standAlone'}
});

expect(await scrollRangeNames('scrollRange', {inView: configurationEditor}))
.toContain('While locked in place');
});

it('names in focus range after viewport center if layout inlines sticky', async () => {
const configurationEditor = renderConfigurationEditor({
layout: 'center',
configuration: {playbackMode: 'scroll', position: 'sticky'}
});

expect(await scrollRangeNames('scrollRange', {inView: configurationEditor}))
.toContain('While crossing the viewport center');
});

it('does not display select to choose scroll range in other playback modes', () => {
const configurationEditor = renderConfigurationEditor({
configuration: {playbackMode: 'loop'}
});

expect(configurationEditor.visibleInputPropertyNames())
.not.toContain('scrollRange');
});

it('displays image modifiers input if animation is present', () => {
Expand Down
Loading
Loading