Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correctly handle TimelineRangeOffsets (rangeStart / rangeEnd) #121

Merged
merged 8 commits into from
Aug 17, 2023
Prev Previous commit
Next Next commit
Refactor timeRange/delay/endDelay to animationRange/rangeStart/rangeEnd
  • Loading branch information
bramus committed Aug 17, 2023
commit df771cb38e301d98664d7dda08b66cc713f4cb6c
17 changes: 9 additions & 8 deletions demo/view-timeline/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,16 @@
"use strict";

const progressBars = document.querySelectorAll('.progress-bar-progress');
const createProgressAnimation = (bar, range, axis) => {
const createProgressAnimation = (bar, rangeStart, rangeEnd, axis) => {
const target = document.getElementById('target');
const viewTimeline = new ViewTimeline({
'subject': target,
'axis': axis
});
bar.animate( { width: ['0px', '200px' ] }, {
timeline: viewTimeline,
timeRange: `${range}`,
rangeStart: `${rangeStart}`,
rangeEnd: `${rangeEnd}`,
fill: 'both'
});
}
Expand All @@ -140,12 +141,12 @@
document.getAnimations().forEach(anim => {
anim.cancel();
});
createProgressAnimation(progressBars[0], 'cover', axis);
createProgressAnimation(progressBars[1], 'contain', axis);
createProgressAnimation(progressBars[2], 'entry', axis);
createProgressAnimation(progressBars[3], 'exit', axis);
createProgressAnimation(progressBars[4], 'contain 25% 75%', axis);
createProgressAnimation(progressBars[5], 'entry 150% exit -50%',
createProgressAnimation(progressBars[0], 'cover', 'cover', axis);
createProgressAnimation(progressBars[1], 'contain', 'contain', axis);
createProgressAnimation(progressBars[2], 'entry', 'entry', axis);
createProgressAnimation(progressBars[3], 'exit', 'exit', axis);
createProgressAnimation(progressBars[4], 'contain 25%', 'contain 75%', axis);
createProgressAnimation(progressBars[5], 'entry 150%', 'exit -50%',
axis);
};

Expand Down
15 changes: 7 additions & 8 deletions demo/view-timeline/with-inset.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"use strict";

const progressBars = document.querySelectorAll('.progress-bar-progress');
const createProgressAnimation = (bar, delay, endDelay, axis, inset=null) => {
const createProgressAnimation = (bar, rangeStart, rangeEnd, axis, inset = 'auto') => {
const subject = document.getElementById('subject');
const viewTimeline = new ViewTimeline({
'subject': subject,
Expand All @@ -100,9 +100,8 @@
});
bar.animate( { width: ['0px', '200px' ] }, {
timeline: viewTimeline,
// timeRange: `${range}`,
delay: delay,
endDelay: endDelay,
rangeStart: rangeStart,
rangeEnd: rangeEnd,
fill: 'both'
});
}
Expand All @@ -111,10 +110,10 @@
document.getAnimations().forEach(anim => {
anim.cancel();
});
createProgressAnimation(progressBars[0], { phase: 'cover', percent: CSS.percent(0) },
{ phase: 'cover', percent: CSS.percent(100) }, axis);
createProgressAnimation(progressBars[1], { phase: 'cover', percent: CSS.percent(0) },
{ phase: 'cover', percent: CSS.percent(100) }, axis, '100px 200px');
createProgressAnimation(progressBars[0], { rangeName: 'cover', offset: CSS.percent(0) },
{ rangeName: 'cover', offset: CSS.percent(100) }, axis);
createProgressAnimation(progressBars[1], { rangeName: 'cover', offset: CSS.percent(0) },
{ rangeName: 'cover', offset: CSS.percent(100) }, axis, '100px 200px');
};

createAnimations();
Expand Down
113 changes: 48 additions & 65 deletions src/proxy-animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,7 @@ function createProxyEffect(details) {
let timing = Object.assign({}, details.specifiedTiming);

const timeline = details.timeline;
// TODO: These delays most likely need to be rewritten to rangeStart/rangeEnd
let computedDelays = false;
let startDelay;
let endDelay;
Expand Down Expand Up @@ -753,7 +754,7 @@ function fractionalStartDelay(details) {
if (!(details.timeline instanceof ViewTimeline))
return 0;

const startTime = details.timeRange.start;
const startTime = details.animationRange.start;
return relativePosition(details.timeline, startTime.name, startTime.offset);
}

Expand All @@ -762,7 +763,7 @@ function fractionalEndDelay(details) {
if (!(details.timeline instanceof ViewTimeline))
return 0;

const endTime = details.timeRange.end;
const endTime = details.animationRange.end;
return 1 - relativePosition(details.timeline, endTime.name, endTime.offset);
}

Expand Down Expand Up @@ -816,7 +817,8 @@ export class ProxyAnimation {
effect: null,
// Range when using a view-timeline. The default range is cover 0% to
// 100%.
timeRange: timeline instanceof ViewTimeline ? parseAnimationDelays(animOptions) : null,
animationRange: timeline instanceof ViewTimeline ?
parseAnimationRange(animOptions['animation-range']) : null,
proxy: this
});
}
Expand Down Expand Up @@ -1596,17 +1598,33 @@ export class ProxyAnimation {
}
};

// animation-delay or animation-end-delay should be in the form of a name and an optional percentage
function parseOneAnimationDelay(delay, defaultOffset) {
if(!delay) return null;

const parts = delay.split(' ');
// Parses an individual TimelineRangeOffset
// TODO: allow calc() in the offsets
// TODO: rename the internal .name to the specced .rangeName
// TODO: Support all formatting options
function parseTimelineRangeOffset(value, defaultValue) {
// TODO: Should this return the default value?
if(!value) return null;

// Extract parts from the passed in value.
// TODO: This needs refactoring …
let parts = [];

// Author passed in something like `{ rangeName: 'cover', offset: CSS.percent(100) }`
if ((value instanceof Object) && (value.rangeName)) {
parts[0] = value.rangeName;
parts[1] = value.offset.toString();
}
// Author passed in something like `"cover 100%"`
else {
parts = value.split(' ');
}

if(!ANIMATION_RANGE_NAMES.includes(parts[0]) ||
(parts.length == 2 && !parts[1].endsWith('%')))
throw TypeError("Invalid animation delay");
throw TypeError("Invalid range offset or unsupported offset format");

let offset = defaultOffset;
let offset = defaultValue.offset;
if(parts.length == 2) {
const percentage = parseFloat(parts[1]);
if(Number.isNaN(percentage))
Expand All @@ -1618,30 +1636,19 @@ function parseOneAnimationDelay(delay, defaultOffset) {
return { name: parts[0], offset: offset };
}

function defaultAnimationDelay() { return { name: 'cover', offset: CSS.percent(0) }; }

function defaultAnimationEndDelay() { return { name: 'cover', offset: CSS.percent(100) }; }

function parseAnimationDelays(animOptions) {
const timeRange = parseTimeRange(animOptions['animation-range']);
function defaultAnimationRangeStart() { return { rangeName: 'cover', offset: CSS.percent(0) }; }

if(animOptions['animation-delay'])
timeRange.start = parseOneAnimationDelay(animOptions['animation-delay'], defaultAnimationDelay().offset);
function defaultAnimationRangeEnd() { return { rangeName: 'cover', offset: CSS.percent(100) }; }

if(animOptions['animation-end-delay'])
timeRange.end = parseOneAnimationDelay(animOptions['animation-end-delay'], defaultAnimationEndDelay().offset);

return timeRange;
}

function parseTimeRange(value) {
const timeRange = {
start: defaultAnimationDelay(),
end: defaultAnimationEndDelay()
// Parses a given animation-range value (string)
function parseAnimationRange(value) {
const animationRange = {
start: defaultAnimationRangeStart(),
end: defaultAnimationRangeEnd()
};

if (!value)
return timeRange;
return animationRange;

// Format:
// <start-name> <start-offset> <end-name> <end-offset>
Expand All @@ -1662,20 +1669,21 @@ function parseTimeRange(value) {
});

if (names.length > 2 || offsets.length > 2 || offsets.length == 1) {
throw TypeError("Invalid time range");
throw TypeError("Invalid time range or unsupported time range format.");
}

if (names.length) {
timeRange.start.name = names[0];
timeRange.end.name = names.length > 1 ? names[1] : names[0];
animationRange.start.name = names[0];
animationRange.end.name = names.length > 1 ? names[1] : names[0];
}

// TODO: allow calc() in the offsets
if (offsets.length > 1) {
timeRange.start.offset = CSS.percent(offsets[0]);
timeRange.end.offset = CSS.percent(offsets[1]);
animationRange.start.offset = CSS.percent(offsets[0]);
animationRange.end.offset = CSS.percent(offsets[1]);
}

return timeRange;
return animationRange;
}

export function animate(keyframes, options) {
Expand All @@ -1684,43 +1692,18 @@ export function animate(keyframes, options) {
if (timeline instanceof ScrollTimeline)
delete options.timeline;

const timelineOffset = (options, property) => {
if (property in options) {
const value = options[property];
if (typeof value != 'number') {
delete options[property];
return value;
}
return null;
}
};

const updateDelay = (timelineOffset, value) => {
if (!value)
return;

// TODO(kevers): Update property names once ratified.
// https://github.com/w3c/csswg-drafts/issues/7589
if (value.phase)
timelineOffset.name = value.phase;

if (value.percent)
timelineOffset.offset = value.percent;
};

const rangeStart = timelineOffset(options, 'rangeStart');
const rangeEnd = timelineOffset(options, 'rangeEnd');

const animation = nativeElementAnimate.apply(this, [keyframes, options]);
const proxyAnimation = new ProxyAnimation(animation, timeline);

if (timeline instanceof ScrollTimeline) {
animation.pause();
if (timeline instanceof ViewTimeline) {
const details = proxyAnimations.get(proxyAnimation);
details.timeRange = parseTimeRange(options.timeRange);
updateDelay(details.timeRange.start, rangeStart);
updateDelay(details.timeRange.end, rangeEnd);

details.animationRange = {
start: parseTimelineRangeOffset(options.rangeStart, defaultAnimationRangeStart()),
end: parseTimelineRangeOffset(options.rangeEnd, defaultAnimationRangeEnd()),
};
}
proxyAnimation.play();
}
Expand Down