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