Get rid of ClampedProgress
This commit is contained in:
parent
b354ce267b
commit
83ab2f9224
5 changed files with 115 additions and 88 deletions
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { map } from './utils';
|
||||||
|
|
||||||
const { componentState } = $props();
|
const { componentState } = $props();
|
||||||
|
|
||||||
let isMoving = $state(false);
|
let isMoving = $state(false);
|
||||||
|
|
@ -43,6 +45,36 @@
|
||||||
}
|
}
|
||||||
isMoving = false;
|
isMoving = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let normalisedScrollProgress = $derived(
|
||||||
|
map(
|
||||||
|
componentState.scrollProgress,
|
||||||
|
componentState.clampStart ?? 0,
|
||||||
|
componentState.clampEnd ?? 1,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let normalisedProgress = $derived(
|
||||||
|
map(
|
||||||
|
componentState.progress,
|
||||||
|
componentState.clampStart ?? 0,
|
||||||
|
componentState.clampEnd ?? 1,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
function mappedStop(stop: number): number {
|
||||||
|
return map(
|
||||||
|
stop,
|
||||||
|
componentState.clampStart ?? 0,
|
||||||
|
componentState.clampEnd ?? 1,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onmousemove={onMouseMove} />
|
<svelte:window onmousemove={onMouseMove} />
|
||||||
|
|
@ -58,11 +90,12 @@
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
{#each componentState.triggerStops as stop, index}
|
{@const stops = componentState.triggerStops.map((x) => mappedStop(x))}
|
||||||
{#if index < componentState.triggerStops.length - 1}
|
{#each stops as stop, index}
|
||||||
|
{#if index < stops.length - 1}
|
||||||
<span
|
<span
|
||||||
class="stops"
|
class="stops"
|
||||||
style={`left: ${(stop + (componentState.triggerStops[index + 1] ?? componentState.triggerStops[componentState.triggerStops.length - 1])) * 0.5 * 100}%;`}
|
style={`left: ${(stop + (stops[index + 1] ?? stops[stops.length - 1])) * 0.5 * 100}%;`}
|
||||||
>|</span
|
>|</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -86,6 +119,14 @@
|
||||||
CONSOLE
|
CONSOLE
|
||||||
</summary>
|
</summary>
|
||||||
<div class="state-debug">
|
<div class="state-debug">
|
||||||
|
<!-- -->
|
||||||
|
<p>Raw progress:</p>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||||
|
<p class="state-value">
|
||||||
|
<span class="tag">{componentState.rawProgress}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- -->
|
||||||
<!-- -->
|
<!-- -->
|
||||||
<p>Scroll progress:</p>
|
<p>Scroll progress:</p>
|
||||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||||
|
|
@ -93,14 +134,14 @@
|
||||||
{@render triggerPoints()}
|
{@render triggerPoints()}
|
||||||
<span
|
<span
|
||||||
class="progress-stop"
|
class="progress-stop"
|
||||||
style={`left: ${componentState.scrollProgress * 100}%; transform: translateX(-50%);`}
|
style={`left: ${normalisedScrollProgress * 100}%; transform: translateX(-50%);`}
|
||||||
>{fmt.format(componentState.scrollProgress)}</span
|
>{fmt.format(componentState.scrollProgress)}</span
|
||||||
>
|
>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<div id="video-progress-bar">
|
<div id="video-progress-bar">
|
||||||
<div
|
<div
|
||||||
style="width: {componentState.scrollProgress * 100}%; height: 100%;"
|
style="width: {normalisedScrollProgress * 100}%; height: 100%;"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -110,20 +151,20 @@
|
||||||
<p class="state-value progress-value">
|
<p class="state-value progress-value">
|
||||||
{#if componentState.stops.length > 0}
|
{#if componentState.stops.length > 0}
|
||||||
{#each componentState.stops as stop}
|
{#each componentState.stops as stop}
|
||||||
<span class="stops" style={`left: ${stop * 100}%;`}>{stop}</span>
|
<span class="stops" style={`left: ${mappedStop(stop) * 100}%;`}
|
||||||
|
>{stop}</span
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="progress-stop"
|
class="progress-stop"
|
||||||
style={`left: ${componentState.progress * 100}%; transform: translateX(-50%);`}
|
style={`left: ${normalisedProgress * 100}%; transform: translateX(-50%);`}
|
||||||
>{fmt.format(componentState.progress)}</span
|
>{fmt.format(componentState.progress)}</span
|
||||||
>
|
>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<div id="video-progress-bar">
|
<div id="video-progress-bar">
|
||||||
<div
|
<div style="width: {normalisedProgress * 100}%; height: 100%;"></div>
|
||||||
style="width: {componentState.progress * 100}%; height: 100%;"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- -->
|
<!-- -->
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ To use the `HorizontalScroller` component, import it and provide the children co
|
||||||
|
|
||||||
> 💡TIP: Use `lvh` or `svh` units instead of `vh` unit for the height, as [these units](https://www.w3.org/TR/css-values-4/#large-viewport-size) are more reliable on mobile or other devices where elements such as the address bar toggle between being shown and hidden.
|
> 💡TIP: Use `lvh` or `svh` units instead of `vh` unit for the height, as [these units](https://www.w3.org/TR/css-values-4/#large-viewport-size) are more reliable on mobile or other devices where elements such as the address bar toggle between being shown and hidden.
|
||||||
|
|
||||||
|
Use `showDebugInfo` prop to visualize the scroll progress and other useful debug information. The `Raw progress` indicates the vertical progress with values in the range 0...1 indicating the content being locked. The `Scroll Progress` value indicates the vertical progress mapped to clampStart and clampEnd values. By default these are 0 and 1 respectively. Finally, the `Progress` value indicates the horizontal scroll progress after applying stops and easing (if any).
|
||||||
|
|
||||||
[Demo](?path=/story/components-graphics-horizontalscroller--demo)
|
[Demo](?path=/story/components-graphics-horizontalscroller--demo)
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,16 @@
|
||||||
</Story>
|
</Story>
|
||||||
|
|
||||||
<Story
|
<Story
|
||||||
name="Extended demo"
|
name="Extended boundaries"
|
||||||
args={{
|
args={{
|
||||||
children: DemoSnippet,
|
children: DemoSnippet,
|
||||||
height: '200lvh',
|
height: '200lvh',
|
||||||
clampedProgress: true,
|
clampStart: -0.5,
|
||||||
clampStart: -1,
|
clampEnd: 1.5,
|
||||||
clampEnd: 2,
|
|
||||||
showDebugInfo: true,
|
showDebugInfo: true,
|
||||||
|
scrubbed: true,
|
||||||
|
stops: [0, 1],
|
||||||
|
easing: quartInOut,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#snippet children(args)}
|
{#snippet children(args)}
|
||||||
|
|
@ -67,7 +69,7 @@
|
||||||
height: '200lvh',
|
height: '200lvh',
|
||||||
stops: [0.2, 0.5, 0.6, 0.7],
|
stops: [0.2, 0.5, 0.6, 0.7],
|
||||||
duration: 400,
|
duration: 400,
|
||||||
scrubbed: false,
|
scrubbed: true,
|
||||||
easing: quartInOut,
|
easing: quartInOut,
|
||||||
showDebugInfo: true,
|
showDebugInfo: true,
|
||||||
direction: 'left',
|
direction: 'left',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import { Tween } from 'svelte/motion';
|
import { Tween } from 'svelte/motion';
|
||||||
import type { Action } from 'svelte/action';
|
import type { Action } from 'svelte/action';
|
||||||
import { clamp, map } from './utils/index';
|
import { clamp, map } from './utils/index';
|
||||||
|
|
@ -30,31 +30,18 @@
|
||||||
duration?: number;
|
duration?: number;
|
||||||
/** Whether to show debug info */
|
/** Whether to show debug info */
|
||||||
showDebugInfo?: boolean;
|
showDebugInfo?: boolean;
|
||||||
}
|
/** Modified starting scale. Default is 0 */
|
||||||
|
|
||||||
interface ClampedProps extends Props {
|
|
||||||
/** Whether the progress value should be clamped */
|
|
||||||
clampedProgress: true;
|
|
||||||
/** Start value for clamping. Only effective when clampedProgress is true. */
|
|
||||||
clampStart?: number;
|
clampStart?: number;
|
||||||
/** End value for clamping. Only effective when clampedProgress is true. */
|
/** Modified ending scale. Default is 1 */
|
||||||
clampEnd?: number;
|
clampEnd?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UnclampedProps extends Props {
|
|
||||||
/** Whether the progress value should be clamped */
|
|
||||||
clampedProgress?: false;
|
|
||||||
clampStart?: never;
|
|
||||||
clampEnd?: never;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
id = '',
|
id = '',
|
||||||
class: cls = '',
|
class: cls = '',
|
||||||
height = '200lvh',
|
height = '200lvh',
|
||||||
direction = 'right',
|
direction = 'right',
|
||||||
scrollProgress = $bindable(0),
|
scrollProgress = $bindable(0),
|
||||||
clampedProgress = true,
|
|
||||||
clampStart = 0,
|
clampStart = 0,
|
||||||
clampEnd = 1,
|
clampEnd = 1,
|
||||||
children,
|
children,
|
||||||
|
|
@ -64,24 +51,25 @@
|
||||||
easing: ease = (t) => t,
|
easing: ease = (t) => t,
|
||||||
duration = 400,
|
duration = 400,
|
||||||
showDebugInfo = false,
|
showDebugInfo = false,
|
||||||
}: ClampedProps | UnclampedProps = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let componentState = $derived.by(() => ({
|
let componentState = $derived.by(() => ({
|
||||||
scrollProgress,
|
scrollProgress,
|
||||||
progress: progressTween.current,
|
progress: progressTween.current,
|
||||||
direction,
|
direction,
|
||||||
clampedProgress,
|
|
||||||
clampStart,
|
clampStart,
|
||||||
clampEnd,
|
clampEnd,
|
||||||
triggerStops: scrubbed ? stops : unscrubbedStops,
|
triggerStops: scrubbed ? stops : divisions,
|
||||||
stops: stops,
|
stops: stops,
|
||||||
handleScroll,
|
handleScroll,
|
||||||
scrubbed,
|
scrubbed,
|
||||||
easing: ease,
|
easing: ease,
|
||||||
duration,
|
duration,
|
||||||
|
rawProgress,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let progressTween: Tween<number> = $state(
|
let progressTween: Tween<number> = $state(
|
||||||
new Tween(0, { duration, easing: ease })
|
new Tween(clampStart, { duration, easing: ease })
|
||||||
);
|
);
|
||||||
let container: HTMLDivElement | undefined = $state(undefined);
|
let container: HTMLDivElement | undefined = $state(undefined);
|
||||||
let containerHeight: number = $state(0);
|
let containerHeight: number = $state(0);
|
||||||
|
|
@ -89,108 +77,101 @@
|
||||||
let content: HTMLDivElement | undefined = $state(undefined);
|
let content: HTMLDivElement | undefined = $state(undefined);
|
||||||
let contentWidth: number = $state(0);
|
let contentWidth: number = $state(0);
|
||||||
let screenHeight: number = $state(0);
|
let screenHeight: number = $state(0);
|
||||||
let unscrubbedStops: number[] = $derived(
|
|
||||||
[...stops, 0, 1].sort((a, b) => a - b)
|
|
||||||
);
|
|
||||||
let divisions: number[] = $derived(
|
let divisions: number[] = $derived(
|
||||||
[...stops, clampStart ?? 0, clampEnd ?? 1].sort((a, b) => a - b)
|
[...stops, clampStart, clampEnd].sort((a, b) => a - b)
|
||||||
);
|
);
|
||||||
let divisionsCount: number = $derived.by(() => divisions.length - 1);
|
let divisionsCount: number = $derived.by(() => divisions.length - 1);
|
||||||
|
|
||||||
let translateX: number = $derived.by(() => {
|
let rawProgress: number | 'user defined' = $state(0);
|
||||||
let processedProgress = progressTween.current;
|
|
||||||
let normalisedProgress = processedProgress;
|
|
||||||
if (clampedProgress) {
|
|
||||||
processedProgress = Math.min(
|
|
||||||
Math.max(progressTween.current, clampStart),
|
|
||||||
clampEnd
|
|
||||||
);
|
|
||||||
|
|
||||||
processedProgress = map(processedProgress, 0, 1, clampStart, clampEnd);
|
// handles horizontal translation of the content
|
||||||
normalisedProgress =
|
let translateX: number = $derived.by(() => {
|
||||||
direction === 'right' ? processedProgress : (
|
let processedProgress = clamp(progressTween.current, clampStart, clampEnd);
|
||||||
clampEnd - processedProgress
|
let normalisedProgress = processedProgress;
|
||||||
);
|
|
||||||
} else {
|
normalisedProgress =
|
||||||
normalisedProgress =
|
direction === 'right' ? processedProgress : clampEnd - processedProgress;
|
||||||
direction === 'right' ? processedProgress : 1 - processedProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
const translate = -(contentWidth - containerWidth) * normalisedProgress;
|
const translate = -(contentWidth - containerWidth) * normalisedProgress;
|
||||||
|
|
||||||
return translate;
|
return translate;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Initialize scrollProgress to clampStart on mount
|
||||||
|
scrollProgress = clampStart;
|
||||||
|
});
|
||||||
|
|
||||||
const scrollListener: Action = () => {
|
const scrollListener: Action = () => {
|
||||||
if (handleScroll) {
|
if (handleScroll) {
|
||||||
window.addEventListener('scroll', handleScrollFunction, {
|
window.addEventListener('scroll', handleScrollFunction, {
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// set rawProgress to user defined when handleScroll is false
|
||||||
|
rawProgress = 'user defined';
|
||||||
window.addEventListener('scroll', () => handleStops(scrollProgress), {
|
window.addEventListener('scroll', () => handleStops(scrollProgress), {
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// calculates distance scrolled inside the container
|
||||||
function handleScrollFunction() {
|
function handleScrollFunction() {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const rawProgress =
|
rawProgress =
|
||||||
(-container?.offsetTop + window?.scrollY) /
|
(-container?.offsetTop + window?.scrollY) /
|
||||||
(containerHeight - screenHeight);
|
(containerHeight - screenHeight);
|
||||||
|
|
||||||
handleStops(rawProgress);
|
handleStops(rawProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updates progressTween based on stops and scrubbed settings
|
||||||
function handleStops(rawProgress: number) {
|
function handleStops(rawProgress: number) {
|
||||||
scrollProgress =
|
scrollProgress = map(rawProgress, 0, 1, clampStart, clampEnd);
|
||||||
clampedProgress ? clamp(rawProgress, clampStart, clampEnd) : rawProgress;
|
|
||||||
if (!stops || stops.length === 0) {
|
if (!stops || stops.length === 0) {
|
||||||
progressTween.set(ease(clamp(rawProgress, 0, 1)), {
|
progressTween.set(ease(map(rawProgress, 0, 1, clampStart, clampEnd)), {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scrubbed) {
|
if (!scrubbed) {
|
||||||
for (let i = 0; i < unscrubbedStops.length; i++) {
|
for (let i = 0; i < divisions.length; i++) {
|
||||||
if (
|
if (
|
||||||
rawProgress > unscrubbedStops[i] &&
|
scrollProgress > divisions[i] &&
|
||||||
rawProgress <=
|
scrollProgress <=
|
||||||
(unscrubbedStops[i + 1] ??
|
(divisions[i + 1] ?? divisions[divisions.length - 1])
|
||||||
unscrubbedStops[unscrubbedStops.length - 1])
|
|
||||||
) {
|
) {
|
||||||
const midPoint =
|
const midPoint =
|
||||||
unscrubbedStops[i] +
|
divisions[i] +
|
||||||
((unscrubbedStops[i + 1] ??
|
((divisions[i + 1] ?? divisions[divisions.length - 1]) -
|
||||||
unscrubbedStops[unscrubbedStops.length - 1]) -
|
divisions[i]) *
|
||||||
unscrubbedStops[i]) *
|
|
||||||
0.5;
|
0.5;
|
||||||
if (
|
if (
|
||||||
rawProgress >= midPoint &&
|
scrollProgress >= midPoint &&
|
||||||
progressTween.target !==
|
progressTween.target !==
|
||||||
(unscrubbedStops[i + 1] ??
|
(divisions[i + 1] ?? divisions[divisions.length - 1])
|
||||||
unscrubbedStops[unscrubbedStops.length - 1])
|
|
||||||
) {
|
) {
|
||||||
progressTween.set(
|
progressTween.set(
|
||||||
unscrubbedStops[i + 1] ??
|
divisions[i + 1] ?? divisions[divisions.length - 1]
|
||||||
unscrubbedStops[unscrubbedStops.length - 1]
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
rawProgress < midPoint &&
|
scrollProgress < midPoint &&
|
||||||
progressTween.target !== unscrubbedStops[i]
|
progressTween.target !== divisions[i]
|
||||||
) {
|
) {
|
||||||
progressTween.set(unscrubbedStops[i]);
|
progressTween.set(divisions[i]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
rawProgress <
|
scrollProgress <
|
||||||
unscrubbedStops[0] + (unscrubbedStops[1] ?? 0) * 0.5
|
divisions[0] + (divisions[1] ?? clampStart) * 0.5
|
||||||
) {
|
) {
|
||||||
if (progressTween.target !== unscrubbedStops[0]) {
|
if (progressTween.target !== divisions[0]) {
|
||||||
progressTween.set(unscrubbedStops[0]);
|
progressTween.set(divisions[0]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -201,15 +182,16 @@
|
||||||
for (let i = 0; i < divisions.length; i++) {
|
for (let i = 0; i < divisions.length; i++) {
|
||||||
let oneByDivCount = 1 / divisionsCount;
|
let oneByDivCount = 1 / divisionsCount;
|
||||||
|
|
||||||
let normalStart = i == 0 ? 0 : oneByDivCount * i;
|
let normalStart = i == 0 ? clampStart : oneByDivCount * i;
|
||||||
let normalEnd = i == divisionsCount - 1 ? 1 : oneByDivCount * (i + 1);
|
let normalEnd =
|
||||||
|
i == divisionsCount - 1 ? clampEnd : oneByDivCount * (i + 1);
|
||||||
|
|
||||||
if (rawProgress >= normalStart && rawProgress < normalEnd) {
|
if (scrollProgress >= normalStart && scrollProgress < normalEnd) {
|
||||||
let stopStart = divisions[i];
|
let stopStart = divisions[i];
|
||||||
let stopEnd = divisions[i + 1] ?? 1;
|
let stopEnd = divisions[i + 1] ?? clampEnd;
|
||||||
let newProgressVal =
|
let newProgressVal =
|
||||||
stopStart +
|
stopStart +
|
||||||
ease(map(rawProgress, normalStart, normalEnd, 0, 1)) *
|
ease(map(scrollProgress, normalStart, normalEnd, 0, 1)) *
|
||||||
(stopEnd - stopStart);
|
(stopEnd - stopStart);
|
||||||
|
|
||||||
progressTween.set(newProgressVal, { duration: 0 });
|
progressTween.set(newProgressVal, { duration: 0 });
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<div style="width: 400vw; height: 100lvh; border: 2px solid red;">
|
<div style="width: 400vw; height: 100lvh;">
|
||||||
<img
|
<img
|
||||||
src="https://picsum.photos/1200/640?t=1"
|
src="https://picsum.photos/1200/640?t=1"
|
||||||
alt="Sample"
|
alt="Sample"
|
||||||
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0;"
|
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0; background-color: #ccc;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue