diff --git a/demo/scroll-timeline-css/anonymous-scroll-timeline-with-range.html b/demo/scroll-timeline-css/anonymous-scroll-timeline-with-range.html new file mode 100644 index 00000000..e907aaac --- /dev/null +++ b/demo/scroll-timeline-css/anonymous-scroll-timeline-with-range.html @@ -0,0 +1,43 @@ + +Anonymous Scroll Progress Timeline + + + + + +
+
+
+
+ + + \ No newline at end of file diff --git a/src/intersection-based-offset.js b/src/intersection-based-offset.js deleted file mode 100644 index b997710c..00000000 --- a/src/intersection-based-offset.js +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { parseLength } from "./utils"; - -let IntersectionOptions = new WeakMap(); - -// Margin is stored as a 4 element array [top, right, bottom, left] but can be -// specified using anywhere from 1 - 4 elements. This map defines how to convert -// various length inputs to their components. -const TOP = 0; -const RIGHT = 1; -const BOTTOM = 2; -const LEFT = 3; -const MARGIN_MAP = [ - // 1 length maps to all positions. - [[TOP, RIGHT, BOTTOM, LEFT]], - // 2 lengths maps to vertical and horizontal margins. - [ - [TOP, BOTTOM], - [RIGHT, LEFT], - ], - // 3 lengths maps to top, horizontal, bottom margins. - [[TOP], [RIGHT, LEFT], [BOTTOM]], - // 4 lengths maps to each component. - [[TOP], [RIGHT], [BOTTOM], [LEFT]], -]; - -class IntersectionBasedOffset { - constructor(value) { - IntersectionOptions.set(this, { - target: null, - edge: "start", - threshold: 0, - rootMargin: [ - [0, "px"], - [0, "px"], - [0, "px"], - [0, "px"], - ], - }); - this.target = value.target; - this.edge = value.edge || "start"; - this.threshold = value.threshold || 0; - this.rootMargin = value.rootMargin || "0px 0px 0px 0px"; - this.clamp = value.clamp || false; - } - - set target(element) { - if (!(element instanceof Element)) { - IntersectionOptions.get(this).target = null; - throw Error("Intersection target must be an element."); - } - IntersectionOptions.get(this).target = element; - } - - get target() { - return IntersectionOptions.get(this).target; - } - - set edge(value) { - if (["start", "end"].indexOf(value) == -1) return; - IntersectionOptions.get(this).edge = value; - } - - get edge() { - return IntersectionOptions.get(this).edge; - } - - set threshold(value) { - let threshold = parseFloat(value); - // Throw a TypeError for a parse error. - if (threshold != threshold) - throw TypeError("Invalid threshold."); - // TODO(https://crbug.com/1136516): This should throw a RangeError - // consistent with the intersection observer spec but the current - // test expectations are looking for a TypeError. - if (threshold < 0 || threshold > 1) - throw TypeError("threshold must be in the range [0, 1]"); - IntersectionOptions.get(this).threshold = threshold; - } - - get threshold() { - return IntersectionOptions.get(this).threshold; - } - - set rootMargin(value) { - let margins = value.split(/ +/); - if (margins.length < 1 || margins.length > 4) - throw TypeError( - "rootMargin must contain between 1 and 4 length components" - ); - let parsedMargins = [[], [], [], []]; - for (let i = 0; i < margins.length; i++) { - let parsedValue = parseLength(margins[i], true); - if (!parsedValue) throw TypeError("Unrecognized rootMargin length"); - let positions = MARGIN_MAP[margins.length - 1][i]; - for (let j = 0; j < positions.length; j++) { - parsedMargins[positions[j]] = [ - parseFloat(parsedValue.value), - parsedValue.unit, - ]; - } - } - IntersectionOptions.get(this).rootMargin = parsedMargins; - } - - get rootMargin() { - // TODO: Simplify to the shortest matching specification for the given margins. - return IntersectionOptions.get(this) - .rootMargin.map((margin) => { - return margin.join(""); - }) - .join(" "); - } - - set clamp(value) { - // This is just for testing alternative proposals - not intended to be part - // of the specification. - IntersectionOptions.get(this).clamp = !!value; - } -} - -export function parseOffset(value) { - if (value.target) return new IntersectionBasedOffset(value); -} - -function resolveLength(length, containerSize) { - if (length[1] == "percent") return (length[0] * containerSize) / 100; - // Assumption is only px or % will be passed in. - // TODO: Support other length types (e.g. em, vh, etc). - return length[0]; -} - -export function calculateOffset(source, axis, offset, startOrEnd) { - // TODO: Support other writing directions. - if (axis == "block") axis = "y"; - else if (axis == "inline") axis = "x"; - let originalViewport = - source == document.scrollingElement - ? { - left: 0, - right: source.clientWidth, - top: 0, - bottom: source.clientHeight, - width: source.clientWidth, - height: source.clientHeight, - } - : source.getBoundingClientRect(); - - // Resolve margins and offset viewport. - let parsedMargins = IntersectionOptions.get(offset).rootMargin; - let computedMargins = []; - for (let i = 0; i < 4; i++) { - computedMargins.push( - resolveLength( - parsedMargins[i], - i % 2 == 0 ? originalViewport.height : originalViewport.width - ) - ); - } - let viewport = { - left: originalViewport.left - computedMargins[LEFT], - right: originalViewport.right + computedMargins[RIGHT], - width: - originalViewport.right - - originalViewport.left + - computedMargins[LEFT] + - computedMargins[RIGHT], - top: originalViewport.top - computedMargins[TOP], - bottom: originalViewport.bottom + computedMargins[BOTTOM], - height: - originalViewport.bottom - - originalViewport.top + - computedMargins[TOP] + - computedMargins[BOTTOM], - }; - - let clamped = IntersectionOptions.get(offset).clamp; - let target = offset.target.getBoundingClientRect(); - let threshold = offset.threshold; - // Invert threshold for start position. - if (offset.edge == "start") threshold = 1 - threshold; - // Projected point into the scroller scroll range. - if (axis == "y") { - let point = - target.top + - target.height * threshold - - viewport.top + - source.scrollTop; - if (clamped) { - if (offset.edge == "end") return Math.max(0, point - viewport.height); - return Math.min(point, source.scrollHeight - viewport.height); - } else { - if (offset.edge == "end") return point - viewport.height; - return point; - } - } else { - // axis == 'x' - let point = - target.left + - target.width * threshold - - viewport.left + - source.scrollLeft; - if (clamped) { - if (offset.edge == "end") return Math.max(0, point - viewport.width); - return Math.min(point, source.scrollWidth - viewport.width); - } else { - if (offset.edge == "end") return point - viewport.width; - return point; - } - } -} diff --git a/src/proxy-animation.js b/src/proxy-animation.js index f61a2a89..79b7c952 100644 --- a/src/proxy-animation.js +++ b/src/proxy-animation.js @@ -1,8 +1,9 @@ import { + ANIMATION_RANGE_NAMES, ScrollTimeline, addAnimation, removeAnimation, - relativePosition + fractionalOffset, } from "./scroll-timeline-base"; const nativeDocumentGetAnimations = document.getAnimations; @@ -10,7 +11,6 @@ const nativeElementGetAnimations = window.Element.prototype.getAnimations; const nativeElementAnimate = window.Element.prototype.animate; const nativeAnimation = window.Animation; -export const ANIMATION_RANGE_NAMES = ['entry', 'exit', 'cover', 'contain', 'entry-crossing', 'exit-crossing']; const rangeNameRegExp = new RegExp(`(${ANIMATION_RANGE_NAMES.join('|')})(?!-)`); class PromiseWrapper { @@ -822,26 +822,20 @@ function createProxyEffect(details) { // Computes the start delay as a fraction of the active cover range. function fractionalStartDelay(details) { - if (!(details.timeline instanceof ViewTimeline)) - return 0; - - let startTime = details.animationRange.start; - if (startTime === 'normal') { - startTime = {rangeName: 'cover', offset: CSS.percent(0)}; + if (!details.animationRange) return 0; + if (details.animationRange.start === 'normal') { + details.animationRange.start = getNormalStartRange(details.timeline); } - return relativePosition(details.timeline, startTime.rangeName, startTime.offset); + return fractionalOffset(details.timeline, details.animationRange.start); } // Computes the ends delay as a fraction of the active cover range. function fractionalEndDelay(details) { - if (!(details.timeline instanceof ViewTimeline)) - return 0; - - let endTime = details.animationRange.end; - if (endTime === 'normal') { - endTime = {rangeName: 'cover', offset: CSS.percent(100)}; + if (!details.animationRange) return 0; + if (details.animationRange.end === 'normal') { + details.animationRange.end = getNormalEndRange(details.timeline); } - return 1 - relativePosition(details.timeline, endTime.rangeName, endTime.offset); + return 1 - fractionalOffset(details.timeline, details.animationRange.end); } // Map from an instance of ProxyAnimation to internal details about that animation. @@ -912,6 +906,147 @@ function autoAlignStartTime(details) { } } +function unsupportedTimeline(timeline) { + throw new Error('Unsupported timeline class'); +} + +function getNormalStartRange(timeline) { + if (timeline instanceof ViewTimeline) { + return { rangeName: 'cover', offset: CSS.percent(0) }; + } + + if (timeline instanceof ScrollTimeline) { + return CSS.percent(0); + } + + unsupportedTimeline(timeline); +} + +function getNormalEndRange(timeline) { + if (timeline instanceof ViewTimeline) { + return { rangeName: 'cover', offset: CSS.percent(100) }; + } + + if (timeline instanceof ScrollTimeline) { + return CSS.percent(100); + } + + unsupportedTimeline(timeline); +} + +function parseAnimationRange(timeline, value) { + const animationRange = { + start: getNormalStartRange(timeline), + end: getNormalEndRange(timeline), + }; + + if (!value) + return animationRange; + + if (timeline instanceof ViewTimeline) { + // Format: + // + // --> 0% 100% + // --> + // + // --> cover cover + // TODO: Support all formatting options once ratified in the spec. + const parts = value.split(/\s+/); + const rangeNames = []; + const offsets = []; + + parts.forEach(part => { + if (part.endsWith('%')) + offsets.push(parseFloat(part)); + else + rangeNames.push(part); + }); + + if (rangeNames.length > 2 || offsets.length > 2 || offsets.length == 1) { + throw TypeError("Invalid time range or unsupported time range format."); + } + + if (rangeNames.length) { + animationRange.start.rangeName = rangeNames[0]; + animationRange.end.rangeName = rangeNames.length > 1 ? rangeNames[1] : rangeNames[0]; + } + + // TODO: allow calc() in the offsets + if (offsets.length > 1) { + animationRange.start.offset = CSS.percent(offsets[0]); + animationRange.end.offset = CSS.percent(offsets[1]); + } + + return animationRange; + } + + if (timeline instanceof ScrollTimeline) { + // @TODO: Play nice with only 1 offset being set + // @TODO: Play nice with expressions such as `calc(50% + 10px) 100%` + const parts = value.split(' '); + if (parts.length != 2) { + throw TypeError("Invalid time range or unsupported time range format."); + } + + animationRange.start = CSSNumericValue.parse(parts[0]); + animationRange.end = CSSNumericValue.parse(parts[1]); + + return animationRange; + } + + unsupportedTimeline(timeline); +} + +function parseTimelineRangePart(timeline, value, position) { + if (!value || value === 'normal') return 'normal'; + + if (timeline instanceof ViewTimeline) { + // Extract parts from the passed in value. + 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) { + rangeName = value.rangeName; + } + + if (value.offset !== undefined) { + offset = value.offset; + } + } + // Author passed in something like `"cover 100%"` + else { + const parts = value.split(rangeNameRegExp).map(part => part.trim()).filter(Boolean); + + 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]); + } + } + + // Validate rangeName + if (!ANIMATION_RANGE_NAMES.includes(rangeName)) { + throw TypeError("Invalid range name"); + } + + return { rangeName, offset }; + } + + if (timeline instanceof ScrollTimeline) { + // The value is a standalone offset, so simply parse it. + return CSSNumericValue.parse(value); + } + + unsupportedTimeline(timeline); +} + // Create an alternate Animation class which proxies API requests. // TODO: Create a full-fledged proxy so missing methods are automatically // fetched from Animation. @@ -958,10 +1093,9 @@ export class ProxyAnimation { // Effect proxy that performs the necessary time conversions when using a // progress-based timelines. effect: null, - // Range when using a view-timeline. The default range is cover 0% to - // 100%. - animationRange: timeline instanceof ViewTimeline ? - parseAnimationRange(animOptions['animation-range']) : null, + // The animation attachment range, restricting the animation’s + // active interval to that range of a timeline + animationRange: isScrollAnimation ? parseAnimationRange(timeline, animOptions['animation-range']) : null, proxy: this }); } @@ -1342,7 +1476,7 @@ export class ProxyAnimation { } get rangeStart() { - return proxyAnimations.get(this).animationRange.start ?? 'normal'; + return proxyAnimations.get(this).animationRange?.start ?? 'normal'; } set rangeStart(value) { @@ -1351,9 +1485,9 @@ export class ProxyAnimation { return details.animation.rangeStart = value; } - if (details.timeline instanceof ViewTimeline) { + if (details.timeline instanceof ScrollTimeline) { const animationRange = details.animationRange; - animationRange.start = parseTimelineRangeOffset(value, 'start'); + animationRange.start = parseTimelineRangePart(details.timeline, value, 'start'); // Additional polyfill step to ensure that the native animation has the // correct value for current time. @@ -1363,7 +1497,7 @@ export class ProxyAnimation { } get rangeEnd() { - return proxyAnimations.get(this).animationRange.end ?? 'normal'; + return proxyAnimations.get(this).animationRange?.end ?? 'normal'; } set rangeEnd(value) { @@ -1372,9 +1506,9 @@ export class ProxyAnimation { return details.animation.rangeEnd = value; } - if (details.timeline instanceof ViewTimeline) { + if (details.timeline instanceof ScrollTimeline) { const animationRange = details.animationRange; - animationRange.end = parseTimelineRangeOffset(value, 'end'); + animationRange.end = parseTimelineRangePart(details.timeline, value, 'end'); // Additional polyfill step to ensure that the native animation has the // correct value for current time. @@ -1761,98 +1895,6 @@ export class ProxyAnimation { } }; -// Parses an individual TimelineRangeOffset -// TODO: Support all formatting options -function parseTimelineRangeOffset(value, position) { - if(!value || value === 'normal') return 'normal'; - - // Extract parts from the passed in value. - 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) { - rangeName = value.rangeName; - } - - if (value.offset !== undefined) { - offset = value.offset; - } - } - // Author passed in something like `"cover 100%"` - else { - const parts = value.split(rangeNameRegExp).map(part => part.trim()).filter(Boolean); - - 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]); - } - } - - // Validate rangeName - if (!ANIMATION_RANGE_NAMES.includes(rangeName)) { - throw TypeError("Invalid range name"); - } - - return { rangeName, offset }; -} - -// Parses a given animation-range value (string) -function parseAnimationRange(value) { - if (!value) - return { - start: 'normal', - end: 'normal' - }; - - const animationRange = { - start: { rangeName: 'cover', offset: CSS.percent(0) }, - end: { rangeName: 'cover', offset: CSS.percent(100) }, - }; - - // Format: - // - // --> 0% 100% - // --> - // - // --> cover cover - // TODO: Support all formatting options once ratified in the spec. - const parts = value.split(' '); - const rangeNames = []; - const offsets = []; - - parts.forEach(part => { - if (part.endsWith('%')) - offsets.push(parseFloat(part)); - else - rangeNames.push(part); - }); - - if (rangeNames.length > 2 || offsets.length > 2 || offsets.length == 1) { - throw TypeError("Invalid time range or unsupported time range format."); - } - - if (rangeNames.length) { - animationRange.start.rangeName = rangeNames[0]; - animationRange.end.rangeName = rangeNames.length > 1 ? rangeNames[1] : rangeNames[0]; - } - - // TODO: allow calc() in the offsets - if (offsets.length > 1) { - animationRange.start.offset = CSS.percent(offsets[0]); - animationRange.end.offset = CSS.percent(offsets[1]); - } - - return animationRange; -} - export function animate(keyframes, options) { const timeline = options.timeline; @@ -1864,14 +1906,13 @@ export function animate(keyframes, options) { if (timeline instanceof ScrollTimeline) { animation.pause(); - if (timeline instanceof ViewTimeline) { - const details = proxyAnimations.get(proxyAnimation); - details.animationRange = { - start: parseTimelineRangeOffset(options.rangeStart, 'start'), - end: parseTimelineRangeOffset(options.rangeEnd, 'end'), - }; - } + const details = proxyAnimations.get(proxyAnimation); + details.animationRange = { + start: parseTimelineRangePart(timeline, options.rangeStart, 'start'), + end: parseTimelineRangePart(timeline, options.rangeEnd, 'end'), + }; + proxyAnimation.play(); } diff --git a/src/scroll-timeline-base.js b/src/scroll-timeline-base.js index 698effec..ce71cc49 100644 --- a/src/scroll-timeline-base.js +++ b/src/scroll-timeline-base.js @@ -14,6 +14,7 @@ import {installCSSOM} from "./proxy-cssom.js"; import {simplifyCalculation} from "./simplify-calculation"; +import {normalizeAxis} from './utils.js'; installCSSOM(); @@ -22,6 +23,8 @@ const DEFAULT_TIMELINE_AXIS = 'block'; let scrollTimelineOptions = new WeakMap(); let sourceDetails = new WeakMap(); +export const ANIMATION_RANGE_NAMES = ['entry', 'exit', 'cover', 'contain', 'entry-crossing', 'exit-crossing']; + function scrollEventSource(source) { if (source === document.scrollingElement) return document; return source; @@ -58,11 +61,8 @@ function directionAwareScrollOffset(source, axis) { // TODO: sideways-lr should flow bottom to top, but is currently unsupported // in Chrome. // http://drafts.csswg.org/css-writing-modes-4/#block-flow - const horizontalWritingMode = style.writingMode == 'horizontal-tb'; let currentScrollOffset = sourceMeasurements.scrollTop; - if (axis == 'x' || - (axis == 'inline' && horizontalWritingMode) || - (axis == 'block' && !horizontalWritingMode)) { + if (normalizeAxis(axis, style) === 'x') { // Negative values are reported for scrollLeft when the inline text // direction is right to left or for vertical text with a right to left // block flow. This is a consequence of shifting the scroll origin due to @@ -159,6 +159,10 @@ function validateAnonymousSource(timeline) { updateSource(timeline, source); } +function isValidAxis(axis) { + return ["block", "inline", "x", "y"].includes(axis); +} + /** * Read measurements of source element * @param {HTMLElement} source @@ -397,7 +401,7 @@ export class ScrollTimeline { if ((options && options.axis !== undefined) && (options.axis != DEFAULT_TIMELINE_AXIS)) { - if (!ScrollTimeline.isValidAxis(options.axis)) { + if (!isValidAxis(options.axis)) { throw TypeError("Invalid axis"); } @@ -417,7 +421,7 @@ export class ScrollTimeline { } set axis(axis) { - if (!ScrollTimeline.isValidAxis(axis)) { + if (!isValidAxis(axis)) { throw TypeError("Invalid axis"); } @@ -481,10 +485,6 @@ export class ScrollTimeline { get __polyfill() { return true; } - - static isValidAxis(axis) { - return ["block", "inline", "x", "y"].includes(axis); - } } // Methods for calculation of the containing block. @@ -599,7 +599,7 @@ export function getScrollParent(node) { // specific phase on a view timeline. // TODO: Track changes to determine when associated animations require their // timing to be renormalized. -function range(timeline, phase) { +export function range(timeline, phase) { const details = scrollTimelineOptions.get(timeline); const subjectMeasurements = details.subjectMeasurements const sourceMeasurements = sourceDetails.get(details.source).sourceMeasurements @@ -620,16 +620,13 @@ export function calculateRange(phase, sourceMeasurements, subjectMeasurements, a // Determine the view and container size based on the scroll direction. // The view position is the scroll position of the logical starting edge // of the view. - const horizontalWritingMode = sourceMeasurements.writingMode == 'horizontal-tb'; const rtl = sourceMeasurements.direction == 'rtl' || sourceMeasurements.writingMode == 'vertical-rl'; let viewSize = undefined; let viewPos = undefined; let sizes = { fontSize: subjectMeasurements.fontSize }; - if (axis == 'x' || - (axis == 'inline' && horizontalWritingMode) || - (axis == 'block' && !horizontalWritingMode)) { + if (normalizeAxis(axis, sourceMeasurements) === 'x') { viewSize = subjectMeasurements.offsetWidth; viewPos = subjectMeasurements.left; sizes.scrollPadding = [sourceMeasurements.scrollPaddingLeft, sourceMeasurements.scrollPaddingRight]; @@ -783,16 +780,36 @@ function calculateInset(value, sizes) { return { start, end }; } +// Calculate the fractional offset of a range value relative to the normal range. +export function fractionalOffset(timeline, value) { + if (timeline instanceof ViewTimeline) { + const { rangeName, offset } = value; + + const phaseRange = range(timeline, rangeName); + const coverRange = range(timeline, 'cover'); -// Calculate the fractional offset of a (phase, percent) pair relative to the -// full cover range. -export function relativePosition(timeline, phase, offset) { - const phaseRange = range(timeline, phase); - const coverRange = range(timeline, 'cover'); - return calculateRelativePosition(phaseRange, offset, coverRange, timeline.subject); -} + return calculateRelativePosition(phaseRange, offset, coverRange, timeline.subject); + } + if (timeline instanceof ScrollTimeline) { + const { axis, source } = timeline; + const { sourceMeasurements } = sourceDetails.get(source); + + let sourceScrollDistance = undefined; + if (normalizeAxis(axis, sourceMeasurements) === 'x') { + sourceScrollDistance = source.scrollWidth; + } else { + sourceScrollDistance = source.scrollHeight; + } + + const position = resolvePx(value, sourceScrollDistance); + const fractionalOffset = position / sourceScrollDistance; + + return fractionalOffset; + } + unsupportedTimeline(timeline); +} export function calculateRelativePosition(phaseRange, offset, coverRange, subject) { if (!phaseRange || !coverRange) diff --git a/src/scroll-timeline-css-parser.js b/src/scroll-timeline-css-parser.js index 8e9edad8..7720a099 100644 --- a/src/scroll-timeline-css-parser.js +++ b/src/scroll-timeline-css-parser.js @@ -1,5 +1,4 @@ -import { ANIMATION_RANGE_NAMES } from './proxy-animation'; -import { getAnonymousSourceElement } from './scroll-timeline-base'; +import { ANIMATION_RANGE_NAMES, getAnonymousSourceElement } from './scroll-timeline-base'; // This is also used in scroll-timeline-css.js export const RegexMatcher = { diff --git a/src/scroll-timeline-css.js b/src/scroll-timeline-css.js index d31f0c1f..3c30461b 100644 --- a/src/scroll-timeline-css.js +++ b/src/scroll-timeline-css.js @@ -56,7 +56,7 @@ function relativePosition(phase, container, target, axis, optionsInset, percent) const subjectMeasurements = measureSubject(container, target) const phaseRange = calculateRange(phase, sourceMeasurements, subjectMeasurements, axis, optionsInset); const coverRange = calculateRange('cover', sourceMeasurements, subjectMeasurements, axis, optionsInset); - return calculateRelativePosition(phaseRange, percent, coverRange); + return calculateRelativePosition(phaseRange, percent, coverRange, target); } function createScrollTimeline(anim, animationName, target) { diff --git a/src/utils.js b/src/utils.js index 4b5d18b8..73a65387 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,20 +1,24 @@ -export function parseLength(obj, acceptStr) { - if (obj instanceof CSSUnitValue || obj instanceof CSSMathSum) - return obj; - if (!acceptStr) - return null; - let matches = obj.trim().match(/^(-?[0-9]*\.?[0-9]*)(px|%)$/); - if (matches) { - let value = matches[1]; - // The unit for % is percent. - let unit = matches[2] == '%' ? 'percent' : matches[2]; - return new CSSUnitValue(value, unit); - } - return null; -} - const canonicalUnits = new Set(["px", "deg", "s", "hz", "dppx", "number", "fr"]); export function isCanonical(unit) { return canonicalUnits.has(unit.toLowerCase()); +} + +export function normalizeAxis(axis, computedStyle) { + if (['x','y'].includes(axis)) return axis; + + if (!computedStyle) { + throw new Error('To determine the normalized axis the computedStyle of the source is required.'); + } + + const horizontalWritingMode = computedStyle.writingMode == 'horizontal-tb'; + if (axis === "block") { + axis = horizontalWritingMode ? "y" : "x"; + } else if (axis === "inline") { + axis = horizontalWritingMode ? "x" : "y"; + } else { + throw new TypeError(`Invalid axis “${axis}”`); + } + + return axis; } \ No newline at end of file diff --git a/test/expected.txt b/test/expected.txt index 17bdc65c..9630d169 100644 --- a/test/expected.txt +++ b/test/expected.txt @@ -919,14 +919,14 @@ FAIL /scroll-animations/scroll-timelines/scroll-animation.html Sending animation PASS /scroll-animations/scroll-timelines/scroll-timeline-invalidation.html Animation current time and effect local time are updated after scroller content size changes. PASS /scroll-animations/scroll-timelines/scroll-timeline-invalidation.html Animation current time and effect local time are updated after scroller size changes. FAIL /scroll-animations/scroll-timelines/scroll-timeline-invalidation.html If scroll animation resizes its scroll timeline scroller, layout reruns once per frame. -FAIL /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with percentage range [JavaScript API] +PASS /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with percentage range [JavaScript API] FAIL /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with px range [JavaScript API] FAIL /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with calculated range [JavaScript API] FAIL /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with EM range [JavaScript API] -FAIL /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with percentage range [CSS] -FAIL /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with px range [CSS] -FAIL /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with calculated range [CSS] -FAIL /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with EM range [CSS] +TIMEOUT /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with percentage range [CSS] +NOTRUN /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with px range [CSS] +NOTRUN /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with calculated range [CSS] +NOTRUN /scroll-animations/scroll-timelines/scroll-timeline-range.html Scroll timeline with EM range [CSS] PASS /scroll-animations/scroll-timelines/scroll-timeline-snapshotting.html ScrollTimeline current time is updated after programmatic animated scroll. PASS /scroll-animations/scroll-timelines/setting-current-time.html Setting animation current time to null throws TypeError. FAIL /scroll-animations/scroll-timelines/setting-current-time.html Setting the current time to an absolute time value throws exception