Skip to content

Commit

Permalink
Add range attributes to animation (#178)
Browse files Browse the repository at this point in the history
Adds support for getting and setting rangeStart and rangeEnd on Animation.

* Adds support for the 'normal' value
* Adds getters and setters for rangeStart and rangeEnd
  * setters are limited to ViewTimelines for now, as the existing implementation is limited to ViewTimelines
* Uses CSSNumericValue.parse() to parse string values for range start and range end.
  * This adds support for math functions such as calc(100% - 20px).
  • Loading branch information
johannesodland authored Dec 21, 2023
1 parent 61320c7 commit 0803f77
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 42 deletions.
108 changes: 71 additions & 37 deletions src/proxy-animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,10 @@ function fractionalStartDelay(details) {
if (!(details.timeline instanceof ViewTimeline))
return 0;

const startTime = details.animationRange.start;
let startTime = details.animationRange.start;
if (startTime === 'normal') {
startTime = {rangeName: 'cover', offset: CSS.percent(0)};
}
return relativePosition(details.timeline, startTime.rangeName, startTime.offset);
}

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

const endTime = details.animationRange.end;
let endTime = details.animationRange.end;
if (endTime === 'normal') {
endTime = {rangeName: 'cover', offset: CSS.percent(100)};
}
return 1 - relativePosition(details.timeline, endTime.rangeName, endTime.offset);
}

Expand Down Expand Up @@ -1327,6 +1333,49 @@ export class ProxyAnimation {
// 4. Otherwise
return 'running';
}

get rangeStart() {
return proxyAnimations.get(this).animationRange.start ?? 'normal';
}

set rangeStart(value) {
const details = proxyAnimations.get(this);
if (!details.timeline) {
return details.animation.rangeStart = value;
}

if (details.timeline instanceof ViewTimeline) {
const animationRange = details.animationRange;
animationRange.start = parseTimelineRangeOffset(value, 'start');

// Additional polyfill step to ensure that the native animation has the
// correct value for current time.
autoAlignStartTime(details);
syncCurrentTime(details);
}
}

get rangeEnd() {
return proxyAnimations.get(this).animationRange.end ?? 'normal';
}

set rangeEnd(value) {
const details = proxyAnimations.get(this);
if (!details.timeline) {
return details.animation.rangeEnd = value;
}

if (details.timeline instanceof ViewTimeline) {
const animationRange = details.animationRange;
animationRange.end = parseTimelineRangeOffset(value, 'end');

// Additional polyfill step to ensure that the native animation has the
// correct value for current time.
autoAlignStartTime(details);
syncCurrentTime(details);
}
}

get replaceState() {
// TODO: Fix me. Replace state is not a boolean.
return proxyAnimations.get(this).animation.pending;
Expand Down Expand Up @@ -1707,30 +1756,36 @@ export class ProxyAnimation {

// Parses an individual TimelineRangeOffset
// TODO: Support all formatting options
function parseTimelineRangeOffset(value, defaultValue) {
if(!value) return defaultValue;
function parseTimelineRangeOffset(value, position) {
if(!value || value === 'normal') return 'normal';

// Extract parts from the passed in value.
let { rangeName, offset } = defaultValue;
let rangeName = 'cover'
let offset = position === 'start' ? CSS.percent(0) : CSS.percent(100)

// Author passed in something like `{ rangeName: 'cover', offset: CSS.percent(100) }`
if (value instanceof Object) {
if (value.rangeName != undefined) {
if (value.rangeName !== undefined) {
rangeName = value.rangeName;
};
}

if (value.offset !== undefined) {
offset = value.offset;
}
}
// Author passed in something like `"cover 100%"`
else {
const parts = value.split(' ');

rangeName = parts[0];
const parts = value.split(new RegExp(`(${ANIMATION_RANGE_NAMES.join('|')})`)).map(part => part.trim()).filter(Boolean);

if (parts.length == 2) {
offset = parts[1];
if (parts.length === 1) {
if (ANIMATION_RANGE_NAMES.includes(parts[0])) {
rangeName = parts[0];
} else {
offset = CSSNumericValue.parse(parts[0]);
}
} else if (parts.length === 2) {
rangeName = parts[0];
offset = CSSNumericValue.parse(parts[1]);
}
}

Expand All @@ -1739,35 +1794,14 @@ function parseTimelineRangeOffset(value, defaultValue) {
throw TypeError("Invalid range name");
}

// Validate and process offset
// TODO: support more than % and px. Don’t forget about calc() along with that.
if (!(offset instanceof Object)) {
if (!offset.endsWith('%') && !offset.endsWith('px')) {
throw TypeError("Invalid range offset. Only % and px are supported (for now)");
}

const parsedValue = parseFloat(offset);

if (offset.endsWith('%')) {
offset = CSS.percent(parsedValue);
} else if (offset.endsWith('px')) {
offset = CSS.px(parsedValue);
}

}

return { rangeName, offset };
}

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

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

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

if (!value)
Expand Down Expand Up @@ -1824,8 +1858,8 @@ export function animate(keyframes, options) {
const details = proxyAnimations.get(proxyAnimation);

details.animationRange = {
start: parseTimelineRangeOffset(options.rangeStart, defaultAnimationRangeStart()),
end: parseTimelineRangeOffset(options.rangeEnd, defaultAnimationRangeEnd()),
start: parseTimelineRangeOffset(options.rangeStart, 'start'),
end: parseTimelineRangeOffset(options.rangeEnd, 'end'),
};
}
proxyAnimation.play();
Expand Down
11 changes: 8 additions & 3 deletions src/scroll-timeline-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -769,15 +769,20 @@ function calculateInset(value, sizes) {
export function relativePosition(timeline, phase, offset) {
const phaseRange = range(timeline, phase);
const coverRange = range(timeline, 'cover');
return calculateRelativePosition(phaseRange, offset, coverRange);
return calculateRelativePosition(phaseRange, offset, coverRange, timeline.subject);
}


export function calculateRelativePosition(phaseRange, offset, coverRange) {

export function calculateRelativePosition(phaseRange, offset, coverRange, subject) {
if (!phaseRange || !coverRange)
return 0;

const info = {percentageReference: new CSSUnitValue(phaseRange.end - phaseRange.start, "px")};
let style = getComputedStyle(subject)
const info = {
percentageReference: CSS.px(phaseRange.end - phaseRange.start),
fontSize: CSS.px(parseFloat(style.fontSize))
};
const simplifiedRangeOffset = simplifyCalculation(offset, info);
if (!(simplifiedRangeOffset instanceof CSSUnitValue) || simplifiedRangeOffset.unit !== 'px') {
throw new Error(`Unsupported offset '${simplifiedRangeOffset.toString()}'`)
Expand Down
5 changes: 3 additions & 2 deletions test/expected.txt
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,7 @@ FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-6.h
FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-7.html View timeline target > viewport, bottom-sticky and top-sticky during contain.
FAIL /scroll-animations/view-timelines/svg-graphics-element-001.html View timeline attached to SVG graphics element
FAIL /scroll-animations/view-timelines/svg-graphics-element-002.html View timeline attached to SVG graphics element
FAIL /scroll-animations/view-timelines/svg-graphics-element-003.html View timeline attached to SVG graphics element
FAIL /scroll-animations/view-timelines/timeline-offset-in-keyframe.html Timeline offsets in programmatic keyframes
FAIL /scroll-animations/view-timelines/timeline-offset-in-keyframe.html String offsets in programmatic keyframes
PASS /scroll-animations/view-timelines/timeline-offset-in-keyframe.html Invalid timeline offset in programmatic keyframe throws
Expand All @@ -946,7 +947,7 @@ PASS /scroll-animations/view-timelines/view-timeline-range.html View timeline wi
PASS /scroll-animations/view-timelines/view-timeline-range.html View timeline with range and inferred name or offset.
PASS /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as <name> <px> pair.
PASS /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as <name> <percent+px> pair.
FAIL /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as strings.
PASS /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as strings.
PASS /scroll-animations/view-timelines/view-timeline-root-source.html Test view-timeline with document scrolling element.
PASS /scroll-animations/view-timelines/view-timeline-snapport.html Default ViewTimeline is not affected by scroll-padding
PASS /scroll-animations/view-timelines/view-timeline-source.tentative.html Default source for a View timeline is the nearest scroll ancestor to the subject
Expand All @@ -956,4 +957,4 @@ FAIL /scroll-animations/view-timelines/view-timeline-sticky-block.html View time
FAIL /scroll-animations/view-timelines/view-timeline-sticky-inline.html View timeline with sticky target, block axis.
FAIL /scroll-animations/view-timelines/view-timeline-subject-size-changes.html View timeline with subject size change after the creation of the animation
FAIL /scroll-animations/view-timelines/zero-intrinsic-iteration-duration.tentative.html Intrinsic iteration duration is non-negative
Passed 431 of 958 tests.
Passed 432 of 959 tests.

0 comments on commit 0803f77

Please sign in to comment.