From 0803f77f6064cb05127d5e9450b3a8cd348e86b9 Mon Sep 17 00:00:00 2001 From: Johannes Odland Date: Thu, 21 Dec 2023 19:17:20 +0100 Subject: [PATCH] Add range attributes to animation (#178) 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). --- src/proxy-animation.js | 108 ++++++++++++++++++++++++------------ src/scroll-timeline-base.js | 11 +++- test/expected.txt | 5 +- 3 files changed, 82 insertions(+), 42 deletions(-) diff --git a/src/proxy-animation.js b/src/proxy-animation.js index 8665c4f1..49d2c487 100644 --- a/src/proxy-animation.js +++ b/src/proxy-animation.js @@ -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); } @@ -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); } @@ -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; @@ -1707,17 +1756,18 @@ 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; @@ -1725,12 +1775,17 @@ function parseTimelineRangeOffset(value, defaultValue) { } // 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]); } } @@ -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) @@ -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(); diff --git a/src/scroll-timeline-base.js b/src/scroll-timeline-base.js index 4dfe300a..c9c093f1 100644 --- a/src/scroll-timeline-base.js +++ b/src/scroll-timeline-base.js @@ -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()}'`) diff --git a/test/expected.txt b/test/expected.txt index 2c1d245a..a9b0b3de 100644 --- a/test/expected.txt +++ b/test/expected.txt @@ -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 @@ -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 pair. PASS /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as 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 @@ -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.