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
26 changes: 21 additions & 5 deletions app/assets/stylesheets/pageflow/editor/background_positioning.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@

.image {
outline: solid 1px var(--ui-on-surface-color-light);
background-size: cover;
}

.label {
Expand All @@ -58,15 +57,32 @@
position: relative;
height: 100%;

img {
// Shrinks to the size of whatever renders the file, so the sliders
// line up with its edges.
.file {
--positioning-max-width: 600px;
--positioning-max-height: 300px;

display: inline-block;
outline: solid 1px var(--ui-on-surface-color-light);
display: block;
max-height: 300px;
max-width: 600px;
line-height: 0;
cursor: crosshair;
}
}

&-image-contain {
display: block;
max-width: var(--positioning-max-width, 100%);
max-height: var(--positioning-max-height, none);
}

&-image-cover {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}

.slider {
position: absolute;
border: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,27 @@ describe('LottieAnimation', () => {
it('contains animation inside its intrinsic aspect ratio by default', () => {
renderLottieAnimation();

expect(players[0].config.layout).toEqual({fit: 'contain'});
expect(players[0].config.layout.fit).toEqual('contain');
});

it('centers animation by default', () => {
renderLottieAnimation();

expect(players[0].config.layout.align).toEqual([0.5, 0.5]);
});

it('aligns animation according to crop position', () => {
renderLottieAnimation({
configuration: {
id: 100,
imageModifiers: [
{name: 'crop', value: 'wide'}
],
cropPosition: {x: 20, y: 30}
}
});

expect(players[0].config.layout.align).toEqual([0.2, 0.3]);
});

describe('crop image modifier', () => {
Expand Down Expand Up @@ -166,7 +186,7 @@ describe('LottieAnimation', () => {
}
});

expect(players[0].config.layout).toEqual({fit: 'cover'});
expect(players[0].config.layout.fit).toEqual('cover');
});

it('forces 1:1 aspect ratio for circle crop', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {editor} from 'pageflow-scrolled/editor';
import {SelectInput, useFakeFeatures} from 'pageflow/testHelpers';
import {FileInput, SelectInput, useFakeFeatures} from 'pageflow/testHelpers';

import {renderContentElementConfigurationEditor, useEditorGlobals} from 'support';

Expand Down Expand Up @@ -77,6 +77,26 @@ describe('lottieAnimation/editor', () => {
expect(configurationEditor.visibleInputPropertyNames())
.not.toContain('imageModifiers');
});

it('offers to position animation inside crop', () => {
const configurationEditor = renderConfigurationEditor({
lottieFiles: [{perma_id: 100, state: 'uploaded'}],
configuration: {id: 100, imageModifiers: [{name: 'crop', value: 'wide'}]}
});

expect(FileInput.findByPropertyName('id', {inView: configurationEditor}).menuItemNames())
.toContain('edit_background_positioning');
});

it('does not offer to position animation that is not cropped', () => {
const configurationEditor = renderConfigurationEditor({
lottieFiles: [{perma_id: 100, state: 'uploaded'}],
configuration: {id: 100}
});

expect(FileInput.findByPropertyName('id', {inView: configurationEditor}).menuItemNames())
.not.toContain('edit_background_positioning');
});
});

it('registers lottie file type for lottie_files collection', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import '@testing-library/jest-dom/extend-expect';

import {renderBackboneView as render} from 'pageflow/testHelpers';

import {LottieFile} from 'contentElements/lottieAnimation/editor/models/LottieFile';
import {
LottieFilePlayerView
} from 'contentElements/lottieAnimation/editor/views/LottieFilePlayerView';

import {fakeDotLottiePlayers} from 'support/fakeDotLottiePlayers';

jest.mock('@lottiefiles/dotlottie-web', () => ({
DotLottie: Object.assign(jest.fn(), {setWasmUrl: jest.fn()})
}));

describe('LottieFilePlayerView', () => {
const {players} = fakeDotLottiePlayers();

const TestView = LottieFilePlayerView.extend({
template: () => '<canvas></canvas>'
});

function playerView(View = TestView) {
return new View({
model: new LottieFile({
state: 'uploaded',
original_url: '/animation.lottie'
})
});
}

it('hides view while the animation is loading', () => {
const view = playerView();

render(view);

expect(view.el).not.toBeVisible();
});

it('keeps view in the layout while the animation is loading', () => {
const view = playerView();

render(view);

expect(window.getComputedStyle(view.el).display).not.toEqual('none');
});

it('displays view once the animation has loaded', () => {
const view = playerView();

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

expect(view.el).toBeVisible();
});

it('resizes the player between sizing and displaying the view', () => {
const steps = [];
const view = playerView(TestView.extend({
onAnimationLoad: () => steps.push('size')
}));

render(view);
players[0].resize.mockImplementation(() => {
steps.push('resize');
expect(view.el).not.toBeVisible();
});
players[0].emit('load');

expect(steps).toEqual(['size', 'resize']);
});

it('lets sub classes size the view before it is displayed', () => {
const onAnimationLoad = jest.fn(function() {
expect(this.el).not.toBeVisible();
});
const view = playerView(TestView.extend({onAnimationLoad}));

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

expect(onAnimationLoad).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {renderBackboneView as render} from 'pageflow/testHelpers';

import {LottieFile} from 'contentElements/lottieAnimation/editor/models/LottieFile';
import {
LottieFilePositioningView
} from 'contentElements/lottieAnimation/editor/views/LottieFilePositioningView';

import {fakeDotLottiePlayers} from 'support/fakeDotLottiePlayers';

jest.mock('@lottiefiles/dotlottie-web', () => ({
DotLottie: Object.assign(jest.fn(), {setWasmUrl: jest.fn()})
}));

describe('LottieFilePositioningView', () => {
const {players, setAnimationSize} = fakeDotLottiePlayers();

function positioningView(options) {
return new LottieFilePositioningView({
model: new LottieFile({
state: 'uploaded',
original_url: '/animation.lottie'
}),
fit: 'contain',
...options
});
}

it('renders animation of the file in a canvas', () => {
const view = positioningView();

render(view);

expect(players).toHaveLength(1);
expect(players[0].config.src).toEqual('/animation.lottie');
expect(players[0].config.canvas).toBe(view.el.querySelector('canvas'));
});

it('plays the animation in a loop', () => {
render(positioningView());

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

it('applies the given fit', () => {
render(positioningView({fit: 'cover'}));

expect(players[0].config.layout.fit).toEqual('cover');
});

it('aligns the animation according to the position', () => {
const view = positioningView({fit: 'cover'});

render(view);
players[0].emit('load');
view.setPosition(20, 30);

expect(players[0].setLayout).toHaveBeenLastCalledWith({fit: 'cover', align: [0.2, 0.3]});
});

it('applies position set while the animation was still loading', () => {
const view = positioningView({fit: 'cover'});

render(view);
view.setPosition(20, 30);
players[0].setLayout.mockClear();
players[0].emit('load');

expect(players[0].setLayout).toHaveBeenCalledWith({fit: 'cover', align: [0.2, 0.3]});
});

it('sizes itself to the animation once it has loaded', () => {
setAnimationSize({width: 200, height: 100});
const view = positioningView();

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

expect(view.el.style.getPropertyValue('--positioning-aspect-ratio')).toEqual('2');
expect(view.el.style.getPropertyValue('--positioning-width')).toEqual('200px');
});

it('destroys player when closed', () => {
const view = positioningView();
render(view);

view.close();

expect(players[0].destroy).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ describe('LottieFileThumbnailView', () => {
expect(players[0].setFrame).toHaveBeenCalledWith(59);
});

it('crops the animation to fill the thumbnail', () => {
const view = thumbnailView();

render(view);

expect(players[0].config.layout).toEqual({fit: 'cover'});
});

it('does not play the animation', () => {
const view = thumbnailView();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export function fakeDotLottiePlayers({act = fn => fn()} = {}) {
play: jest.fn(),
pause: jest.fn(),
setFrame: jest.fn(),
setLayout: jest.fn(),
resize: jest.fn(),
destroy: jest.fn(),
animationSize: jest.fn(() => animationSize),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export function LottieAnimation({configuration}) {
loop={configuration.playbackMode !== 'playOnce'}
play={isVisible}
fit={aspectRatio ? 'cover' : 'contain'}
cropPositionX={configuration.cropPosition?.x}
cropPositionY={configuration.cropPosition?.y}
onAspectRatioChange={setAnimationAspectRatio} />}
</ContentElementBox>
<InlineFileRights configuration={configuration}
Expand All @@ -57,7 +59,9 @@ export function LottieAnimation({configuration}) {
);
}

function Player({lottieFile, loop, play, fit, onAspectRatioChange}) {
function Player({
lottieFile, loop, play, fit, cropPositionX = 50, cropPositionY = 50, onAspectRatioChange
}) {
const canvasRef = useRef();
const dotLottieRef = useRef();

Expand All @@ -69,7 +73,7 @@ function Player({lottieFile, loop, play, fit, onAspectRatioChange}) {
canvas: canvasRef.current,
src: lottieFile.urls.original,
loop,
layout: {fit},
layout: {fit, align: [cropPositionX / 100, cropPositionY / 100]},
autoplay: false,
renderConfig: {autoResize: true}
});
Expand All @@ -94,7 +98,7 @@ function Player({lottieFile, loop, play, fit, onAspectRatioChange}) {
dotLottieRef.current = null;
dotLottie.destroy();
};
}, [lottieFile.urls.original, loop, fit, onAspectRatioChange]);
}, [lottieFile.urls.original, loop, fit, cropPositionX, cropPositionY, onAspectRatioChange]);

useEffect(() => {
if (play) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import {editor, ImageModifierListInputView, InlineFileRightsMenuItem} from 'pageflow-scrolled/editor';
import {processImageModifiers} from 'pageflow-scrolled/frontend';
import {FileInputView} from 'pageflow/editor';
import {SelectInputView, SeparatorView} from 'pageflow/ui';

import {LottieFile} from './models/LottieFile';
import {LottieFilePositioningView} from './views/LottieFilePositioningView';
import {LottieFilePreviewView} from './views/LottieFilePreviewView';
import {LottieFileThumbnailView} from './views/LottieFileThumbnailView';

import pictogram from './pictogram.svg';

editor.fileTypes.register('lottie_files', {
model: LottieFile,
positioningView: LottieFilePositioningView,
previewView: LottieFilePreviewView,
thumbnailView: LottieFileThumbnailView,

Expand Down Expand Up @@ -41,7 +44,15 @@ editor.contentElementTypes.register('lottieAnimation', {
this.input('id', FileInputView, {
collection: 'lottie_files',
fileSelectionHandler: 'contentElementConfiguration',
positioning: false,
positioning: imageModifiers => !!processImageModifiers(imageModifiers).aspectRatio,
positioningBinding: 'imageModifiers',
positioningOptions: () => {
const {aspectRatio} = processImageModifiers(this.model.get('imageModifiers'));

return {
preview: aspectRatio && (1 / entry.getAspectRatio(aspectRatio))
};
},
dropDownMenuItems: [InlineFileRightsMenuItem]
});
this.input('imageModifiers', ImageModifierListInputView, {
Expand Down
Loading
Loading