Skip to content
Draft
115 changes: 77 additions & 38 deletions dist/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ function debounce (fn, interval) {
};
}

/**
* parses offsetString of the format calc(<length> + <length>)
* @param {string|undefined} offsetString
* @param {AbsoluteOffsetContext} absoluteOffsetContext
*/
function parseOffsetCalc(offsetString, absoluteOffsetContext) {
const match = offsetString.match(/^calc\s*\(\s*(-?\d+((px)|(vh)|(vw)))\s*\+\s*(-?\d+((px)|(vh)|(vw)))\s*\)\s*$/);
return transformAbsoluteOffsetToNumber(match[1], absoluteOffsetContext) + transformAbsoluteOffsetToNumber(match[6], absoluteOffsetContext);
}

/**
* Convert an absolute offset as string to number of pixels
*
Expand All @@ -100,7 +110,9 @@ function transformAbsoluteOffsetToNumber (offsetString, absoluteOffsetContext) {
? parseInt(offsetString) * absoluteOffsetContext.viewportHeight / 100
: /^-?\d+vw$/.test(offsetString)
? parseInt(offsetString) * absoluteOffsetContext.viewportWidth / 100
: parseInt(offsetString) || 0
: /^calc\s*\(\s*-?\d+((px)|(vh)|(vw))\s*\+\s*-?\d+((px)|(vh)|(vw))\s*\)\s*$/.test(offsetString)
? parseOffsetCalc(offsetString)
: parseInt(offsetString) || 0
: 0;
}

Expand Down Expand Up @@ -137,6 +149,10 @@ function transformRangeToPosition (range, viewportSize, rect) {
startPosition = Math.max(start, end - viewportSize);
duration = Math.min(viewportSize, height);
}
else if (name === 'exit-crossing') {
startPosition = start;
duration = height;
}
else if (name === 'cover') {
startPosition = start - viewportSize;
duration = height + viewportSize;
Expand Down Expand Up @@ -218,6 +234,7 @@ function computeStickinessIntoFullRange(offsetTree, absoluteStartOffset, absolut
}
}
});

return newAbsoluteRange;
}

Expand All @@ -244,36 +261,46 @@ function transformSceneRangesToOffsets (scene, rect, viewportSize, isHorizontal,
if (typeof duration === 'string') {
startRange = { name: duration, offset: 0 };
endRange = { name: duration, offset: 100 };

startOffset = transformRangeToPosition(startRange, viewportSize, rect);
endOffset = transformRangeToPosition(endRange, viewportSize, rect);
overrideDuration = endOffset - startOffset;

const newAbsoluteRange = computeStickinessIntoFullRange(offsetTree, startOffset, endOffset, viewportSize, isHorizontal);

startOffset = newAbsoluteRange.start;
endOffset = newAbsoluteRange.end;
}
else {
if (startRange || start?.name) {
startRange = startRange || start;

const startAdd = transformAbsoluteOffsetToNumber(startRange.add, absoluteOffsetContext);
const absoluteStartOffset = transformRangeToPosition({...startRange, offset: 0}, viewportSize, rect);
const absoluteEndOffset = transformRangeToPosition({...startRange, offset: 100}, viewportSize, rect);
// we take 0% to 100% of the named range for start, and we compute the position by adding the sticky addition for the given start offset
const newAbsoluteRange = computeStickinessIntoFullRange(offsetTree, absoluteStartOffset, absoluteEndOffset, viewportSize, isHorizontal);

startOffset = newAbsoluteRange.start + (startRange.offset / 100) * (newAbsoluteRange.end - newAbsoluteRange.start) + startAdd;
}

if (endRange || end?.name) {
endRange = endRange || end;

const endAdd = transformAbsoluteOffsetToNumber(endRange.add, absoluteOffsetContext);
const absoluteStartOffset = transformRangeToPosition({...endRange, offset: 0}, viewportSize, rect);
const absoluteEndOffset = transformRangeToPosition({...endRange, offset: 100}, viewportSize, rect);
// we take 0% to 100% of the named range for end, and we compute the position by adding the sticky addition for the given end offset
const newAbsoluteRange = computeStickinessIntoFullRange(offsetTree, absoluteStartOffset, absoluteEndOffset, viewportSize, isHorizontal);

endOffset = newAbsoluteRange.start + (endRange.offset / 100) * (newAbsoluteRange.end - newAbsoluteRange.start) + endAdd;
}
else if (typeof duration === 'number') {
endOffset = startOffset + duration;
}
}

return {...scene, start: startOffset, end: endOffset, startRange, endRange, duration: overrideDuration || duration };
return {...scene, start: startOffset, end: endOffset, startRange, endRange, duration: overrideDuration || duration || (endOffset - startOffset) };
}

/**
Expand Down Expand Up @@ -355,15 +382,15 @@ function getStickyData (style, isHorizontal) {
/**
* Returns a converted scene data from ranges into offsets in pixels.
*
* @param {ScrollScene} scene
* @param {ScrollScene[]} scene
* @param {Window|HTMLElement} root
* @param {number} viewportSize
* @param {boolean} isHorizontal
* @param {AbsoluteOffsetContext} absoluteOffsetContext
* @return {ScrollScene}
* @return {ScrollScene[]}
*/
function getTransformedScene (scene, root, viewportSize, isHorizontal, absoluteOffsetContext) {
const element = scene.viewSource;
function getTransformedSceneGroup (scenes, root, viewportSize, isHorizontal, absoluteOffsetContext) {
const element = scenes[0].viewSource;
const elementStyle = window.getComputedStyle(element);
const isElementSticky = getIsSticky(elementStyle);
const elementStickiness = isElementSticky ? getStickyData(elementStyle, isHorizontal) : undefined;
Expand Down Expand Up @@ -417,19 +444,17 @@ function getTransformedScene (scene, root, viewportSize, isHorizontal, absoluteO

offsetTree.reverse();

const transformedScene = transformSceneRangesToOffsets(
const transformedScenes = scenes.map(scene => transformSceneRangesToOffsets(
scene,
{start: elementLayoutStart, end: elementLayoutStart + size},
viewportSize,
isHorizontal,
absoluteOffsetContext,
offsetTree
);
));
transformedScenes.forEach(scene => {scene.isFixed = isFixed;});

transformedScene.isFixed = isFixed;


return transformedScene;
return transformedScenes;
}

const VIEWPORT_RESIZE_INTERVAL = 100;
Expand Down Expand Up @@ -523,58 +548,70 @@ function getController (config) {
/*
* Prepare scenes data.
*/
_config.scenes = config.scenes.map((scene, index) => {
scene.index = index;

if (scene.viewSource && (typeof scene.duration === 'string' || scene.start?.name)) {
scene = getTransformedScene(scene, root, viewportSize, horizontal, absoluteOffsetContext);
_config.scenes = Object.values(
config.scenes.reduce((acc, scene, index) => {
const key = scene.groupId || String(index);
if (acc[key]) {
acc[key].push(scene);
} else {
acc[key] = [scene];
}
return acc;
},
{})
).flatMap(sceneGroup => {
if (sceneGroup.every(scene => (scene.viewSource && (typeof scene.duration === 'string' || scene.start?.name)))) {
sceneGroup = getTransformedSceneGroup(sceneGroup, root, viewportSize, horizontal, absoluteOffsetContext);

if (_config.observeSourcesResize) {
rangesToObserve.push(scene);
rangesToObserve.push(sceneGroup);
}
}
else if (scene.end == null) {
scene.end = scene.start + scene.duration;
}

if (scene.duration == null) {
scene.duration = scene.end - scene.start;
} else {
sceneGroup.forEach(scene => {
if (scene.end == null) {
scene.end = scene.start + scene.duration;
}
if (scene.duration == null) {
scene.duration = scene.end - scene.start;
}
});
}

return scene;
return sceneGroup;
});
_config.scenes.forEach((scene, index) => {scene.index = index;});

if (rangesToObserve.length) {
if (window.ResizeObserver) {
const targetToScene = new Map();
const targetToSceneGroup = new Map();

rangesResizeObserver = new window.ResizeObserver(function (entries) {
entries.forEach(entry => {
const scene = targetToScene.get(entry.target);
const sceneGroup = targetToSceneGroup.get(entry.target);
// TODO: try to optimize by using `const {blockSize, inlineSize} = entry.borderBoxSize[0]`
_config.scenes[scene.index] = getTransformedScene(scene, root, viewportSize, horizontal, absoluteOffsetContext);
const transformedSceneGroup = getTransformedSceneGroup(sceneGroup, root, viewportSize, horizontal, absoluteOffsetContext);
transformedSceneGroup.forEach((scene, localIndex) => {_config.scenes[scene.index] = transformedSceneGroup[localIndex];});

// replace the old object from the cache with the new one
rangesToObserve.splice(rangesToObserve.indexOf(scene), 1, _config.scenes[scene.index]);
rangesToObserve.splice(rangesToObserve.indexOf(sceneGroup), 1, transformedSceneGroup);
});
});

rangesToObserve.forEach(scene => {
rangesResizeObserver.observe(scene.viewSource, {box: 'border-box'});
targetToScene.set(scene.viewSource, scene);
rangesToObserve.forEach(sceneGroup => {
rangesResizeObserver.observe(sceneGroup[0].viewSource, {box: 'border-box'});
targetToSceneGroup.set(sceneGroup[0].viewSource, sceneGroup);
});
}

if (_config.observeViewportResize) {
viewportResizeHandler = debounce(function () {
viewportSize = getViewportSize(root, horizontal);

const newRanges = rangesToObserve.map(scene => {
const newScene = getTransformedScene(scene, root, viewportSize, horizontal, absoluteOffsetContext);

_config.scenes[scene.index] = newScene;
const newRanges = rangesToObserve.map(sceneGroup => {
const newSceneGroup = getTransformedSceneGroup(sceneGroup, root, viewportSize, horizontal, absoluteOffsetContext);
newSceneGroup.forEach((scene, localIndex) => {_config.scenes[scene.index] = newSceneGroup[localIndex];});

return newScene;
return newSceneGroup;
});

// reset cache
Expand Down Expand Up @@ -912,6 +949,8 @@ class Scroll {
* @property {boolean} [disabled] whether to perform updates on the scene. Defaults to false.
* @property {Element} [viewSource] an element to be used for observing intersection with viewport for disabling/enabling the scene, or the source of a ViewTimeline if scene start/end are provided as ranges.
* @property {function} [destroy] a function clean up the scene when it's controller is destroyed.
* @property {string} [groupId] a string id to group together scenes
* @property {Promise} [ready] a promise that is resolved once the effects are ready.
*/

/**
Expand Down
6 changes: 3 additions & 3 deletions docs/reference/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ <h3 class='mb0 no-anchor'>fizban</h3>
<li><a
href='#gettransformedscene'
class="">
getTransformedScene
getTransformedSceneGroup

</a>

Expand Down Expand Up @@ -2659,7 +2659,7 @@ <h3 class='fl m0' id='getrectstart'>
<div class='clearfix'>

<h3 class='fl m0' id='gettransformedscene'>
getTransformedScene
getTransformedSceneGroup
</h3>


Expand All @@ -2668,7 +2668,7 @@ <h3 class='fl m0' id='gettransformedscene'>

<p>Returns a converted scene data from ranges into offsets in pixels.</p>

<div class='pre p1 fill-light mt0'>getTransformedScene(scene: <a href="#scrollscene">ScrollScene</a>, root: (<a href="https://developer.mozilla.org/docs/Web/API/Window">Window</a> | <a href="https://developer.mozilla.org/docs/Web/HTML/Element">HTMLElement</a>), viewportSize: <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>, isHorizontal: <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>, absoluteOffsetContext: <a href="#absoluteoffsetcontext">AbsoluteOffsetContext</a>): <a href="#scrollscene">ScrollScene</a></div>
<div class='pre p1 fill-light mt0'>getTransformedSceneGroup(scene: <a href="#scrollscene">ScrollScene</a>, root: (<a href="https://developer.mozilla.org/docs/Web/API/Window">Window</a> | <a href="https://developer.mozilla.org/docs/Web/HTML/Element">HTMLElement</a>), viewportSize: <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>, isHorizontal: <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>, absoluteOffsetContext: <a href="#absoluteoffsetcontext">AbsoluteOffsetContext</a>): <a href="#scrollscene">ScrollScene</a></div>



Expand Down
2 changes: 2 additions & 0 deletions src/Scroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ export class Scroll {
* @property {boolean} [disabled] whether to perform updates on the scene. Defaults to false.
* @property {Element} [viewSource] an element to be used for observing intersection with viewport for disabling/enabling the scene, or the source of a ViewTimeline if scene start/end are provided as ranges.
* @property {function} [destroy] a function clean up the scene when it's controller is destroyed.
* @property {string} [groupId] a string id to group together scenes
* @property {Promise} [ready] a promise that is resolved once the effects are ready.
*/

/**
Expand Down
66 changes: 39 additions & 27 deletions src/controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { debounce, defaultTo } from './utilities.js';
import { getTransformedScene } from './view.js';
import { getTransformedSceneGroup } from './view.js';

const VIEWPORT_RESIZE_INTERVAL = 100;

Expand Down Expand Up @@ -92,58 +92,70 @@ export function getController (config) {
/*
* Prepare scenes data.
*/
_config.scenes = config.scenes.map((scene, index) => {
scene.index = index;

if (scene.viewSource && (typeof scene.duration === 'string' || scene.start?.name)) {
scene = getTransformedScene(scene, root, viewportSize, horizontal, absoluteOffsetContext);
_config.scenes = Object.values(
config.scenes.reduce((acc, scene, index) => {
const key = scene.groupId || String(index);
if (acc[key]) {
acc[key].push(scene)
} else {
acc[key] = [scene];
}
return acc;
},
{})
).flatMap(sceneGroup => {
if (sceneGroup.every(scene => (scene.viewSource && (typeof scene.duration === 'string' || scene.start?.name)))) {
sceneGroup = getTransformedSceneGroup(sceneGroup, root, viewportSize, horizontal, absoluteOffsetContext);

if (_config.observeSourcesResize) {
rangesToObserve.push(scene);
rangesToObserve.push(sceneGroup);
}
}
else if (scene.end == null) {
scene.end = scene.start + scene.duration;
}

if (scene.duration == null) {
scene.duration = scene.end - scene.start;
} else {
sceneGroup.forEach(scene => {
if (scene.end == null) {
scene.end = scene.start + scene.duration;
}
if (scene.duration == null) {
scene.duration = scene.end - scene.start;
}
});
}

return scene;
return sceneGroup;
});
_config.scenes.forEach((scene, index) => {scene.index = index;})

if (rangesToObserve.length) {
if (window.ResizeObserver) {
const targetToScene = new Map();
const targetToSceneGroup = new Map();

rangesResizeObserver = new window.ResizeObserver(function (entries) {
entries.forEach(entry => {
const scene = targetToScene.get(entry.target);
const sceneGroup = targetToSceneGroup.get(entry.target);
// TODO: try to optimize by using `const {blockSize, inlineSize} = entry.borderBoxSize[0]`
_config.scenes[scene.index] = getTransformedScene(scene, root, viewportSize, horizontal, absoluteOffsetContext);
const transformedSceneGroup = getTransformedSceneGroup(sceneGroup, root, viewportSize, horizontal, absoluteOffsetContext);
transformedSceneGroup.forEach((scene, localIndex) => {_config.scenes[scene.index] = transformedSceneGroup[localIndex];});

// replace the old object from the cache with the new one
rangesToObserve.splice(rangesToObserve.indexOf(scene), 1, _config.scenes[scene.index]);
rangesToObserve.splice(rangesToObserve.indexOf(sceneGroup), 1, transformedSceneGroup);
})
});

rangesToObserve.forEach(scene => {
rangesResizeObserver.observe(scene.viewSource, {box: 'border-box'});
targetToScene.set(scene.viewSource, scene);
rangesToObserve.forEach(sceneGroup => {
rangesResizeObserver.observe(sceneGroup[0].viewSource, {box: 'border-box'});
targetToSceneGroup.set(sceneGroup[0].viewSource, sceneGroup);
});
}

if (_config.observeViewportResize) {
viewportResizeHandler = debounce(function () {
viewportSize = getViewportSize(root, horizontal);

const newRanges = rangesToObserve.map(scene => {
const newScene = getTransformedScene(scene, root, viewportSize, horizontal, absoluteOffsetContext);

_config.scenes[scene.index] = newScene;
const newRanges = rangesToObserve.map(sceneGroup => {
const newSceneGroup = getTransformedSceneGroup(sceneGroup, root, viewportSize, horizontal, absoluteOffsetContext);
newSceneGroup.forEach((scene, localIndex) => {_config.scenes[scene.index] = newSceneGroup[localIndex];});

return newScene;
return newSceneGroup;
});

// reset cache
Expand Down
Loading