diff --git a/src/components/HorizontalScroller/Debug.svelte b/src/components/HorizontalScroller/Debug.svelte new file mode 100644 index 00000000..8170b937 --- /dev/null +++ b/src/components/HorizontalScroller/Debug.svelte @@ -0,0 +1,313 @@ + + + + +{#snippet triggerPoints()} + {#if componentState.triggerStops.length > 0} + {#if componentState.scrubbed} + {@const totalStops = componentState.triggerStops.length} + {#each Array(totalStops) as _, index} + | + {/each} + {:else} + {#each componentState.triggerStops as stop, index} + {#if index < componentState.triggerStops.length - 1} + | + {/if} + {/each} + {/if} + {/if} +{/snippet} + +
+
+ + CONSOLE + +
+ +

Scroll progress:

+
+

+ {@render triggerPoints()} + {fmt.format(componentState.scrollProgress)} +   +

+
+
+
+
+ +

Progress:

+
+

+ {#if componentState.stops.length > 0} + {#each componentState.stops as stop, index} + {stop} + {/each} + {/if} + {fmt.format(componentState.progress)} +   +

+
+
+
+
+ +

Direction:

+
+

+ {componentState.direction} +

+
+ + {#if componentState.stops.length > 0} +

Stops:

+
+

+ {#each componentState.stops as stop, index} + {stop} + {/each} +

+
+ {/if} + +

Handle scroll:

+
+

+ {componentState.handleScroll} +

+
+ +

Scrubbed:

+
+

+ {componentState.scrubbed} +

+
+ +

Easing:

+
+

+ {componentState.easing} +

+
+ +

+ Duration: + {#if componentState.scrubbed} + NA + {/if} +

+
+

+ {componentState.duration} +

+
+ +
+
+
+ + diff --git a/src/components/HorizontalScroller/HorizontalScroller.mdx b/src/components/HorizontalScroller/HorizontalScroller.mdx new file mode 100644 index 00000000..436f6c6d --- /dev/null +++ b/src/components/HorizontalScroller/HorizontalScroller.mdx @@ -0,0 +1,288 @@ +import { Meta } from '@storybook/blocks'; + +import * as HorizontalScrollerStories from './HorizontalScroller.stories.svelte'; + + + +# HorizontalScroller + +The `HorizontalScroller` component is helpful in making horizontal scrolling sections that respond to vertical scroll input. It is flexible in a way that it can horizontally scroll any children content wider than 100vw from one end to the other. + +To scroll any DOM layout wider than the viewport, wrap the content inside the `HorizontalScroller` component. The component will take care of the rest. + +## Basic demo + +To use the `HorizontalScroller` component, import it and provide the children content to scroll. The scroll height defaults to `200lvh`, but you can adjust this to any valid CSS height value such as `1200px` or `200lvh` with the `height` prop. + +> 💡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. + +[Demo](?path=/story/components-graphics-horizontalscroller--demo) + +```svelte + + + + +
+ + + alt text +
+
+``` + +## With stops + +The `HorizontalScroller` also allows you to define a set of points to stop or slow down the scrolling at specific intervals using the `stops` prop. This is useful for creating step-based horizontal scrolling experiences. + +The `scrubbed` prop can be used to define whether the scrolling experience should be smooth or tied directly to the scroll position. Setting `scrubbed` to `true` will make the horizontal scroll position directly correspond to the vertical scroll position, while setting it to `false` will create a smooth scrolling effect. + +If `scrubbed` is set to `false` and `stops` are defined, the scroller will transition smoothly to the next stop when the scrollProgress reaches the midpoint between two stops. The transition speed is controlled by the `duration` prop (in milliseconds) and the `easing` prop (which accepts any easing function from `svelte/easing` or a custom function based on signature `(t: number) => number`). + +If `scrubbed` is set to `true` and `stops` are defined, all the stops are traversed at equal distance but based on the easing function provided. + +Feel free to toggle `scrubbed` prop here to see the difference. + +[Demo](?path=/story/components-graphics-horizontalscroller--demo) + +```svelte + + + + +
+ + + alt text +
+
+``` + +## With custom child components + +You can create a horizontal stack of any components and pass it as children to the `HorizontalScroller`. Here's an example of using `DatawrapperChart`, `Headline` and ai2svelte components inside the scroller. + +[Demo](?path=/story/components-graphics-horizontalscroller--custom-children) + +```svelte + + + +
+
+ +
+
+ +
+
+ + + +
+
+
+ + +``` + +## With ai2svelte components + +With ai2svelte v1.0.3 onwards, you can export your ai2svelte graphic with a wider-than-viewport layout and use it directly inside the `HorizontalScroller` component to create horizontal scrolling graphics. + +To do that, follow these steps: + +1. In Illustrator, rename your artboard with a tag indicating breakpoint width for that artboard to be visible on page. For example, to make the XL artboard visible on viewports wider than 1200px, rename the artboard to `xl:1200`. You can have more than one artboard with different breakpoint widths. +2. In ai2svelte settings, set these properties and run ai2svelte to export the component. + +```yaml +include_resizer_css: false +respect_height: true +allow_overflow: true +``` + +This can be useful to even transition tagged content inside the ai2svelte graphic as part of the horizontal scrolling experience. For example, caption boxes exported as `htext` tagged layers can be animated to fade in/out or move in/out of view based on the scroll progress. Or one could even use tagged `png` layers to create parallax effects. + +[Demo](?path=/story/components-graphics-horizontalscroller--scrollable-ai-2-svelte) + +```svelte + + + + + + + +``` + +## With ScrollerBase + +You can also integrate HorizontalScroller with `ScrollerBase` for a horizontal scroll with vertical captions experience. + +[Demo](?path=/story/components-graphics-horizontalscroller--scrollable-ai-2-svelte) + +```svelte + + + + {#snippet backgroundSnippet()} + + + + + + {/snippet} + {#snippet foregroundSnippet()} + +

Step 1

+

Step 2

+

Step 3

+

Step 4

+

Step 5

+ {/snippet} +
+ + +``` diff --git a/src/components/HorizontalScroller/HorizontalScroller.stories.svelte b/src/components/HorizontalScroller/HorizontalScroller.stories.svelte new file mode 100644 index 00000000..a6744213 --- /dev/null +++ b/src/components/HorizontalScroller/HorizontalScroller.stories.svelte @@ -0,0 +1,87 @@ + + + + + + +{#snippet DemoSnippet()} + +{/snippet} + +{#snippet CustomChildrenSnippet()} + +{/snippet} + + + {#snippet children(args)} + + {/snippet} + + + + {#snippet children(args)} + + {/snippet} + + + + {#snippet children(args)} + + {/snippet} + + + + + + + + + diff --git a/src/components/HorizontalScroller/HorizontalScroller.svelte b/src/components/HorizontalScroller/HorizontalScroller.svelte new file mode 100644 index 00000000..299acb47 --- /dev/null +++ b/src/components/HorizontalScroller/HorizontalScroller.svelte @@ -0,0 +1,278 @@ + + + + + +
+
+ {#if children} + {@render children()} + {/if} + {#if showDebugInfo} +
+ +
+ {/if} +
+
+
+ + diff --git a/src/components/HorizontalScroller/demo/CustomChildrenSnippet.svelte b/src/components/HorizontalScroller/demo/CustomChildrenSnippet.svelte new file mode 100644 index 00000000..fb2749c7 --- /dev/null +++ b/src/components/HorizontalScroller/demo/CustomChildrenSnippet.svelte @@ -0,0 +1,44 @@ + + +
+
+ +
+
+ +
+
+ + + +
+
+ + diff --git a/src/components/HorizontalScroller/demo/Demo.svelte b/src/components/HorizontalScroller/demo/Demo.svelte new file mode 100644 index 00000000..5f0fd9d5 --- /dev/null +++ b/src/components/HorizontalScroller/demo/Demo.svelte @@ -0,0 +1,15 @@ + + + + + + + diff --git a/src/components/HorizontalScroller/demo/DemoSnippet.svelte b/src/components/HorizontalScroller/demo/DemoSnippet.svelte new file mode 100644 index 00000000..eea00ad1 --- /dev/null +++ b/src/components/HorizontalScroller/demo/DemoSnippet.svelte @@ -0,0 +1,7 @@ +
+ Sample +
diff --git a/src/components/HorizontalScroller/demo/ScrollableGraphic.svelte b/src/components/HorizontalScroller/demo/ScrollableGraphic.svelte new file mode 100644 index 00000000..d0dcf2f2 --- /dev/null +++ b/src/components/HorizontalScroller/demo/ScrollableGraphic.svelte @@ -0,0 +1,92 @@ + + + + + + + + Destruction!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + caption2: + '
Destruction!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + caption3: + '
Destruction!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + caption4: + '
Destruction!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + }, + }, + }} + /> +
+ + + + diff --git a/src/components/HorizontalScroller/demo/graphic/ai2svelte/ai-chart.svelte b/src/components/HorizontalScroller/demo/graphic/ai2svelte/ai-chart.svelte new file mode 100644 index 00000000..704b6eaf --- /dev/null +++ b/src/components/HorizontalScroller/demo/graphic/ai2svelte/ai-chart.svelte @@ -0,0 +1,630 @@ + + + + +
+ + {#if width && width >= 0 && width < 510} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Jeremie

+
+
+

Port-au-Prince

+
+
+

Epicenter

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

Dominican

+

Republic

+
+
+

50 km

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Jeremie

+
+
+

Port-au-Prince

+
+
+

Epicenter

+
+
+

Dominican

+

Republic

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

50 km

+
+
+ {/if} + + {#if width && width >= 660} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Dominican

+

Republic

+
+
+

Jeremie

+
+
+

Epicenter

+
+
+

Port-au-Prince

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

50 km

+
+
+ {/if} +
+ + + + + diff --git a/src/components/HorizontalScroller/demo/graphic/ai2svelte/demo.svelte b/src/components/HorizontalScroller/demo/graphic/ai2svelte/demo.svelte new file mode 100644 index 00000000..f6d72542 --- /dev/null +++ b/src/components/HorizontalScroller/demo/graphic/ai2svelte/demo.svelte @@ -0,0 +1,263 @@ + + + + +
+ + {#if aiBoxWidth && aiBoxWidth >= 0 && aiBoxWidth < 800} +
+
+
+ {/if} + + {#if aiBoxWidth && aiBoxWidth >= 800} +
+
+
+
+

+ {@html taggedText?.htext?.captions?.caption2 || ''} +

+
+
+

+ {@html taggedText?.htext?.captions?.caption3 || ''} +

+
+
+

+ {@html taggedText?.htext?.captions?.caption4 || ''} +

+
+
+

+ {@html taggedText?.htext?.captions?.caption1 || ''} +

+
+
+ {/if} +
+ + + + + + + diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-md.png b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-md.png new file mode 100644 index 00000000..7f62d44c Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-md.png differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-sm.png b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-sm.png new file mode 100644 index 00000000..49c06f65 Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-sm.png differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-xs.png b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-xs.png new file mode 100644 index 00000000..15d640df Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-xs.png differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/demo-lg.jpg b/src/components/HorizontalScroller/demo/graphic/imgs/demo-lg.jpg new file mode 100644 index 00000000..88cc6e6d Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/demo-lg.jpg differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/demo-xl.jpg b/src/components/HorizontalScroller/demo/graphic/imgs/demo-xl.jpg new file mode 100644 index 00000000..b638cc6e Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/demo-xl.jpg differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/layer-overlay-xl.png b/src/components/HorizontalScroller/demo/graphic/imgs/layer-overlay-xl.png new file mode 100644 index 00000000..f9c9a54d Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/layer-overlay-xl.png differ diff --git a/src/components/HorizontalScroller/demo/graphic/placeholder.png b/src/components/HorizontalScroller/demo/graphic/placeholder.png new file mode 100644 index 00000000..ca521649 Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/placeholder.png differ diff --git a/src/components/HorizontalScroller/demo/withScrollerBase.svelte b/src/components/HorizontalScroller/demo/withScrollerBase.svelte new file mode 100644 index 00000000..671081eb --- /dev/null +++ b/src/components/HorizontalScroller/demo/withScrollerBase.svelte @@ -0,0 +1,82 @@ + + + + + + {#snippet backgroundSnippet()} + + + + + {/snippet} + {#snippet foregroundSnippet()} + +

Step 1

+

Step 2

+

Step 3

+

Step 4

+

Step 5

+ {/snippet} +
+ + diff --git a/src/components/HorizontalScroller/utils.ts b/src/components/HorizontalScroller/utils.ts new file mode 100644 index 00000000..06807f9b --- /dev/null +++ b/src/components/HorizontalScroller/utils.ts @@ -0,0 +1,40 @@ +/** + * Clamp a number `n` to the inclusive range [low, high]. + */ +export function clamp(n: number, low: number, high: number): number { + // Ensure low <= high even if caller swaps them + const min = Math.min(low, high); + const max = Math.max(low, high); + return Math.max(min, Math.min(n, max)); +} + +/** + * Linearly maps a value `n` from range [inStart, inEnd] to [outStart, outEnd]. + * + * @param {number} n - The input value to map. + * @param {number} inStart - Input range start. + * @param {number} inEnd - Input range end. + * @param {number} outStart - Output range start. + * @param {number} outEnd - Output range end. + * @param {boolean} withinBounds - If true, clamp the mapped value to [outStart, outEnd]. + * @returns {number} - Mapped (and optionally clamped) value. + */ +export function map( + n: number, + inStart: number, + inEnd: number, + outStart: number, + outEnd: number, + withinBounds: boolean = true +): number { + // Avoid division by zero: when input range is degenerate, return outStart + const inSpan = inEnd - inStart; + if (inSpan === 0) { + return withinBounds ? clamp(outStart, outStart, outEnd) : outStart; + } + + const t = (n - inStart) / inSpan; // normalized 0..1 in input space (or beyond) + const out = t * (outEnd - outStart) + outStart; + + return withinBounds ? clamp(out, outStart, outEnd) : out; +} diff --git a/src/index.ts b/src/index.ts index 790e9306..5dd53bd7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ export { default as GraphicBlock } from './components/GraphicBlock/GraphicBlock. export { default as Headline } from './components/Headline/Headline.svelte'; export { default as Headpile } from './components/Headpile/Headpile.svelte'; export { default as HeroHeadline } from './components/HeroHeadline/HeroHeadline.svelte'; +export { default as HorizontalScroller } from './components/HorizontalScroller/HorizontalScroller.svelte'; export { default as EndNotes } from './components/EndNotes/EndNotes.svelte'; export { default as InfoBox } from './components/InfoBox/InfoBox.svelte'; export { default as InlineAd } from './components/AdSlot/InlineAd.svelte';