adds HorizontalScroller
313
src/components/HorizontalScroller/Debug.svelte
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
<script lang="ts">
|
||||
const { componentState } = $props();
|
||||
|
||||
let isMoving = $state(false);
|
||||
let preventDetails = $state(false);
|
||||
let position = $state({ x: 8, y: 8 });
|
||||
|
||||
const fmt = new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
isMoving = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (isMoving) {
|
||||
position = {
|
||||
x: position.x + e.movementX,
|
||||
y: position.y + e.movementY,
|
||||
};
|
||||
preventDetails = true;
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onMouseUp(e: MouseEvent) {
|
||||
if (isMoving) {
|
||||
isMoving = false;
|
||||
setTimeout(() => {
|
||||
preventDetails = false;
|
||||
}, 5);
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onClick(e: MouseEvent) {
|
||||
if (preventDetails) {
|
||||
e.preventDefault();
|
||||
}
|
||||
isMoving = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onmousemove={onMouseMove} />
|
||||
|
||||
{#snippet triggerPoints()}
|
||||
{#if componentState.triggerStops.length > 0}
|
||||
{#if componentState.scrubbed}
|
||||
{@const totalStops = componentState.triggerStops.length}
|
||||
{#each Array(totalStops) as _, index}
|
||||
<span
|
||||
class="stops"
|
||||
style={`left: ${((index + 1) / (totalStops + 1)) * 100}%;`}>|</span
|
||||
>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each componentState.triggerStops as stop, index}
|
||||
{#if index < componentState.triggerStops.length - 1}
|
||||
<span
|
||||
class="stops"
|
||||
style={`left: ${(stop + (componentState.triggerStops[index + 1] ?? componentState.triggerStops[componentState.triggerStops.length - 1])) * 0.5 * 100}%;`}
|
||||
>|</span
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<div
|
||||
style="position: absolute; top: {position.y}px; left: {position.x}px; z-index: 5; user-select: none;"
|
||||
role="region"
|
||||
>
|
||||
<details class="debug-info" open>
|
||||
<summary
|
||||
class="text-xxs font-sans font-bold title"
|
||||
style="grid-column: span 2;"
|
||||
onmousedown={onMouseDown}
|
||||
onmouseup={onMouseUp}
|
||||
onclick={onClick}
|
||||
>
|
||||
CONSOLE
|
||||
</summary>
|
||||
<div class="state-debug">
|
||||
<!-- -->
|
||||
<p>Scroll progress:</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<p class="state-value progress-value">
|
||||
{@render triggerPoints()}
|
||||
<span
|
||||
class="progress-stop"
|
||||
style={`left: ${componentState.scrollProgress * 100}%; transform: translateX(-50%);`}
|
||||
>{fmt.format(componentState.scrollProgress)}</span
|
||||
>
|
||||
|
||||
</p>
|
||||
<div id="video-progress-bar">
|
||||
<div
|
||||
style="width: {componentState.scrollProgress * 100}%; height: 100%;"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- -->
|
||||
<p>Progress:</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<p class="state-value progress-value">
|
||||
{#if componentState.stops.length > 0}
|
||||
{#each componentState.stops as stop, index}
|
||||
<span class="stops" style={`left: ${stop * 100}%;`}>{stop}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
<span
|
||||
class="progress-stop"
|
||||
style={`left: ${componentState.progress * 100}%; transform: translateX(-50%);`}
|
||||
>{fmt.format(componentState.progress)}</span
|
||||
>
|
||||
|
||||
</p>
|
||||
<div id="video-progress-bar">
|
||||
<div
|
||||
style="width: {componentState.progress * 100}%; height: 100%;"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- -->
|
||||
<p>Direction:</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.direction}</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- -->
|
||||
{#if componentState.stops.length > 0}
|
||||
<p>Stops:</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<p
|
||||
class="state-value"
|
||||
style="display: flex; gap: 4px; flex-wrap: wrap;"
|
||||
>
|
||||
{#each componentState.stops as stop, index}
|
||||
<span class="tag">{stop}</span>
|
||||
{/each}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- -->
|
||||
<p>Handle scroll:</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.handleScroll}</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- -->
|
||||
<p>Scrubbed:</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.scrubbed}</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- -->
|
||||
<p>Easing:</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<p class="state-value">
|
||||
{componentState.easing}
|
||||
</p>
|
||||
</div>
|
||||
<!-- -->
|
||||
<p>
|
||||
Duration:
|
||||
{#if componentState.scrubbed}
|
||||
<span class="tag not-applicable">NA</span>
|
||||
{/if}
|
||||
</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<p class="state-value">
|
||||
<span class="tag">{componentState.duration}</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- -->
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
* {
|
||||
font-family: monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
z-index: 3;
|
||||
margin: 0;
|
||||
width: 50vmin;
|
||||
min-width: 50vmin;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
resize: horizontal;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.3s ease;
|
||||
filter: drop-shadow(0 0 16px rgba(0, 0, 0, 0.5));
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
interpolate-size: allow-keywords;
|
||||
}
|
||||
|
||||
&::details-content {
|
||||
opacity: 0;
|
||||
block-size: 0;
|
||||
overflow-y: clip;
|
||||
transition:
|
||||
content-visibility 0.4s allow-discrete,
|
||||
opacity 0.4s,
|
||||
block-size 0.4s cubic-bezier(0.87, 0, 0.13, 1);
|
||||
}
|
||||
|
||||
&[open]::details-content {
|
||||
opacity: 1;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.debug-info[open] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.state-debug {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
padding: 8px 8px 16px 8px;
|
||||
grid-template-columns: 20vmin 1fr;
|
||||
align-items: center;
|
||||
gap: 0.75rem 0.25rem;
|
||||
background-color: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--theme-font-size-xxs);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 100%;
|
||||
font-variant: tabular-nums;
|
||||
}
|
||||
|
||||
.state-value {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stops {
|
||||
position: absolute;
|
||||
opacity: 0.5;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.progress-stop {
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
#video-progress-bar {
|
||||
width: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
height: 2px;
|
||||
border-radius: 50px;
|
||||
// margin: auto;
|
||||
|
||||
div {
|
||||
background-color: white;
|
||||
border-radius: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 0.1rem 0.2rem;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.not-applicable {
|
||||
background-color: rgba(255, 0, 0, 0.2);
|
||||
color: rgba(255, 0, 0, 0.8);
|
||||
}
|
||||
</style>
|
||||
288
src/components/HorizontalScroller/HorizontalScroller.mdx
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
import * as HorizontalScrollerStories from './HorizontalScroller.stories.svelte';
|
||||
|
||||
<Meta of={HorizontalScrollerStories} />
|
||||
|
||||
# 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
|
||||
<script lang="ts">
|
||||
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
|
||||
</script>
|
||||
|
||||
<!-- Optionally set `height` to adjust scroll height -->
|
||||
<HorizontalScroller height="400lvh">
|
||||
<div style="width: 200vw; height: 100lvh;">
|
||||
<!-- Content wider than 100vw -->
|
||||
<!-- Only the top 100lvh will be visible -->
|
||||
<img
|
||||
src="path/to/wide-image.jpg"
|
||||
alt="alt text"
|
||||
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0;"
|
||||
/>
|
||||
</div>
|
||||
</HorizontalScroller>
|
||||
```
|
||||
|
||||
## 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
|
||||
<script lang="ts">
|
||||
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
|
||||
import { quartInOut } from 'svelte/easing';
|
||||
</script>
|
||||
|
||||
<!-- Optionally set `height` to adjust scroll height -->
|
||||
<HorizontalScroller
|
||||
height="200lvh"
|
||||
stops={[0.2, 0.5, 0.6, 0.7]}
|
||||
duration={400}
|
||||
scrubbed={false}
|
||||
easing={quartInOut}
|
||||
showDebugInfo
|
||||
direction="right"
|
||||
>
|
||||
<div style="width: 200vw; height: 100lvh;">
|
||||
<!-- Content wider than 100vw -->
|
||||
<!-- Only the top 100lvh will be visible -->
|
||||
<img
|
||||
src="path/to/wide-image.jpg"
|
||||
alt="alt text"
|
||||
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0;"
|
||||
/>
|
||||
</div>
|
||||
</HorizontalScroller>
|
||||
```
|
||||
|
||||
## 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
|
||||
<script lang="ts">
|
||||
import {
|
||||
Block,
|
||||
DatawrapperChart,
|
||||
Headline,
|
||||
HorizontalScroller,
|
||||
} from '@reuters-graphics/graphics-components';
|
||||
import AiChart from './ai2svelte/ai-chart.svelte';
|
||||
import { quartInOut } from 'svelte/easing';
|
||||
</script>
|
||||
|
||||
<HorizontalScroller
|
||||
height="200lvh"
|
||||
stops={[0.2, 0.5, 0.6, 0.7]}
|
||||
duration={400}
|
||||
scrubbed={false}
|
||||
easing={quartInOut}
|
||||
direction="right"
|
||||
showDebugInfo
|
||||
>
|
||||
<div id="horizontal-stack">
|
||||
<div style="width: 100vw;">
|
||||
<DatawrapperChart
|
||||
title="Global abortion access"
|
||||
ariaLabel="map"
|
||||
id="abortion-rights-map"
|
||||
src="https://graphics.reuters.com/USA-ABORTION/lgpdwggnwvo/media-embed.html"
|
||||
frameTitle=""
|
||||
scrolling="no"
|
||||
textWidth="normal"
|
||||
width="wider"
|
||||
/>
|
||||
</div>
|
||||
<div style="width: 100vw;">
|
||||
<Headline
|
||||
hed="Reuters Graphics Interactive"
|
||||
dek="The beginning of a beautiful page"
|
||||
section="World News"
|
||||
/>
|
||||
</div>
|
||||
<div style="width: 100vw;">
|
||||
<Block width="normal">
|
||||
<AiChart />
|
||||
</Block>
|
||||
</div>
|
||||
</div>
|
||||
</HorizontalScroller>
|
||||
|
||||
<style lang="scss">
|
||||
#horizontal-stack {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
gap: 10vw;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 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
|
||||
<script lang="ts">
|
||||
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
|
||||
import AiGraphic from './ai2svelte/ai-graphic.svelte';
|
||||
import { sineInOut } from 'svelte/easing';
|
||||
|
||||
// bind scrollProgress for advanced interactivity
|
||||
let scrollProgress: number = $state(0);
|
||||
</script>
|
||||
|
||||
<HorizontalScroller
|
||||
width="fluid"
|
||||
height="800lvh"
|
||||
direction="right"
|
||||
bind:scrollProgress
|
||||
easing={sineInOut}
|
||||
showDebugInfo
|
||||
>
|
||||
<AiGraphic />
|
||||
</HorizontalScroller>
|
||||
|
||||
<style lang="scss">
|
||||
#horizontal-stack {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
gap: 10vw;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 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
|
||||
<script lang="ts">
|
||||
import {
|
||||
HorizontalScroller,
|
||||
ScrollerBase,
|
||||
} from '@reuters-graphics/graphics-components';
|
||||
import AiGraphic from './ai2svelte/ai-graphic.svelte';
|
||||
import { circInOut } from 'svelte/easing';
|
||||
import { circInOut } from 'svelte/easing';
|
||||
|
||||
// Optional: Bind your own variables to use them in your code.
|
||||
let count = $state(1);
|
||||
let index = $state(0);
|
||||
let offset = $state(0);
|
||||
let progress = $state(0);
|
||||
let top = $state(0);
|
||||
let threshold = $state(0.5);
|
||||
let bottom = $state(1);
|
||||
</script>
|
||||
|
||||
<ScrollerBase
|
||||
{top}
|
||||
{threshold}
|
||||
{bottom}
|
||||
bind:count
|
||||
bind:index
|
||||
bind:offset
|
||||
bind:progress
|
||||
query="div.step-foreground-container"
|
||||
>
|
||||
{#snippet backgroundSnippet()}
|
||||
<!-- Make sure to set height to `100lvh` -->
|
||||
<!-- and handleScroll to false to avoid scroll conflicts -->
|
||||
<HorizontalScroller
|
||||
width="fluid"
|
||||
height="100lvh"
|
||||
direction="right"
|
||||
bind:scrollProgress={progress}
|
||||
scrubbed
|
||||
stops={[0.5]}
|
||||
handleScroll={false}
|
||||
easing={circInOut}
|
||||
showDebugInfo
|
||||
>
|
||||
<Demo />
|
||||
</HorizontalScroller>
|
||||
{/snippet}
|
||||
{#snippet foregroundSnippet()}
|
||||
<!-- Add custom foreground HTML or component -->
|
||||
<div class="step-foreground-container"><p>Step 1</p></div>
|
||||
<div class="step-foreground-container"><p>Step 2</p></div>
|
||||
<div class="step-foreground-container"><p>Step 3</p></div>
|
||||
<div class="step-foreground-container"><p>Step 4</p></div>
|
||||
<div class="step-foreground-container"><p>Step 5</p></div>
|
||||
{/snippet}
|
||||
</ScrollerBase>
|
||||
|
||||
<style lang="scss">
|
||||
.step-foreground-container {
|
||||
height: 100vh;
|
||||
width: 50%;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
padding: 1em;
|
||||
margin: 0 auto 10px 0;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
p {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import HorizontalScroller from './HorizontalScroller.svelte';
|
||||
import { quartInOut } from 'svelte/easing';
|
||||
import DemoComponent from './demo/Demo.svelte';
|
||||
import DemoSnippetBlock from './demo/DemoSnippet.svelte';
|
||||
import CustomChildrenBlock from './demo/CustomChildrenSnippet.svelte';
|
||||
import ScrollableGraphic from './demo/ScrollableGraphic.svelte';
|
||||
import WithScrollerBaseComponent from './demo/withScrollerBase.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Graphics/HorizontalScroller',
|
||||
component: HorizontalScroller,
|
||||
tags: ['autodocs'],
|
||||
});
|
||||
|
||||
let width: number = $state(0);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={width} />
|
||||
|
||||
{#snippet DemoSnippet()}
|
||||
<DemoSnippetBlock />
|
||||
{/snippet}
|
||||
|
||||
{#snippet CustomChildrenSnippet()}
|
||||
<CustomChildrenBlock />
|
||||
{/snippet}
|
||||
|
||||
<Story
|
||||
name="Demo"
|
||||
args={{
|
||||
children: DemoSnippet,
|
||||
height: '200lvh',
|
||||
}}
|
||||
>
|
||||
{#snippet children(args)}
|
||||
<DemoComponent {...args}></DemoComponent>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="With stops"
|
||||
args={{
|
||||
children: DemoSnippet,
|
||||
height: '200lvh',
|
||||
stops: [0.2, 0.5, 0.6, 0.7],
|
||||
duration: 400,
|
||||
scrubbed: false,
|
||||
easing: quartInOut,
|
||||
showDebugInfo: true,
|
||||
direction: 'left',
|
||||
}}
|
||||
>
|
||||
{#snippet children(args)}
|
||||
<DemoComponent {...args}></DemoComponent>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="Custom children"
|
||||
args={{
|
||||
children: CustomChildrenSnippet,
|
||||
height: '200lvh',
|
||||
stops: [0.5],
|
||||
duration: 400,
|
||||
scrubbed: false,
|
||||
easing: quartInOut,
|
||||
showDebugInfo: true,
|
||||
direction: 'right',
|
||||
}}
|
||||
>
|
||||
{#snippet children(args)}
|
||||
<DemoComponent {...args}></DemoComponent>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Scrollable ai2svelte">
|
||||
<ScrollableGraphic />
|
||||
</Story>
|
||||
|
||||
<Story name="With ScrollerBase">
|
||||
<WithScrollerBaseComponent />
|
||||
</Story>
|
||||
278
src/components/HorizontalScroller/HorizontalScroller.svelte
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
<script lang="ts">
|
||||
import Block from '../Block/Block.svelte';
|
||||
import type { ContainerWidth } from '../@types/global';
|
||||
import { untrack, type Snippet } from 'svelte';
|
||||
import { Tween } from 'svelte/motion';
|
||||
import type { Action } from 'svelte/action';
|
||||
import { clamp, map } from './utils';
|
||||
import Debug from './Debug.svelte';
|
||||
|
||||
interface Props {
|
||||
/** Optional id for the scroller container */
|
||||
id?: string;
|
||||
/** Optional additional classes for the scroller container */
|
||||
class?: string;
|
||||
/** Width of the scroller container*/
|
||||
width?: ContainerWidth;
|
||||
/** Height of the scroller container in CSS `vh` units. Set it to `100lvh` when using inside ScrollerBase. */
|
||||
height?: string;
|
||||
/** Bindable progress value. Ideal range: `[0-1]`. Bind ScrollerBase's progress to this prop. */
|
||||
scrollProgress?: number;
|
||||
/** Direction of movement*/
|
||||
direction?: 'left' | 'right';
|
||||
/** Content to scroll*/
|
||||
children?: Snippet;
|
||||
/** Array of numbers desired as stops for the scroller */
|
||||
stops?: number[];
|
||||
/** Should the component handle scroll events? Set it to `false` when using inside ScrollerBase. */
|
||||
handleScroll?: boolean;
|
||||
/** Whether the stops should be scrubbed */
|
||||
scrubbed?: boolean;
|
||||
/** Easing function for the progress/stops */
|
||||
easing?: (t: number) => number;
|
||||
/** Duration of the easing animation in milliseconds. Effective only when scrubbed is false. */
|
||||
duration?: number;
|
||||
/** Whether to show debug info */
|
||||
showDebugInfo?: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
/** End value for clamping. Only effective when clampedProgress is true. */
|
||||
clampEnd?: number;
|
||||
}
|
||||
|
||||
interface UnclampedProps extends Props {
|
||||
/** Whether the progress value should be clamped */
|
||||
clampedProgress?: false;
|
||||
clampStart?: never;
|
||||
clampEnd?: never;
|
||||
}
|
||||
|
||||
let {
|
||||
id = '',
|
||||
class: cls = '',
|
||||
width = 'fluid',
|
||||
height = '200lvh',
|
||||
direction = 'right',
|
||||
scrollProgress = $bindable(0),
|
||||
clampedProgress = true,
|
||||
clampStart = 0,
|
||||
clampEnd = 1,
|
||||
children,
|
||||
stops = [],
|
||||
handleScroll = true,
|
||||
scrubbed = true,
|
||||
easing: ease = (t) => t,
|
||||
duration = 400,
|
||||
showDebugInfo = false,
|
||||
}: ClampedProps | UnclampedProps = $props();
|
||||
|
||||
let componentState = $derived.by(() => ({
|
||||
scrollProgress,
|
||||
progress: progressTween.current,
|
||||
direction,
|
||||
clampedProgress,
|
||||
clampStart,
|
||||
clampEnd,
|
||||
triggerStops: scrubbed ? stops : unscrubbedStops,
|
||||
stops: stops,
|
||||
handleScroll,
|
||||
scrubbed,
|
||||
easing: ease,
|
||||
duration,
|
||||
}));
|
||||
let progressTween: Tween<number> = $state(
|
||||
new Tween(0, { duration, easing: ease })
|
||||
);
|
||||
let container: HTMLDivElement | undefined = $state(undefined);
|
||||
let containerHeight: number = $state(0);
|
||||
let containerWidth: number = $state(0);
|
||||
let content: HTMLDivElement | undefined = $state(undefined);
|
||||
let contentWidth: number = $state(0);
|
||||
let screenHeight: number = $state(0);
|
||||
let unscrubbedStops: number[] = $derived(
|
||||
[...stops, 0, 1].sort((a, b) => a - b)
|
||||
);
|
||||
let divisions: number[] = $derived(
|
||||
[...stops, clampStart ?? 0, clampEnd ?? 1].sort((a, b) => a - b)
|
||||
);
|
||||
let divisionsCount: number = $derived.by(() => divisions.length - 1);
|
||||
|
||||
let translateX: number = $derived.by(() => {
|
||||
let processedProgress = progressTween.current;
|
||||
if (clampedProgress) {
|
||||
processedProgress = Math.min(
|
||||
Math.max(progressTween.current, clampStart),
|
||||
clampEnd
|
||||
);
|
||||
}
|
||||
|
||||
const normalisedProgress =
|
||||
direction === 'right' ? processedProgress : 1 - processedProgress;
|
||||
|
||||
const translate = -(contentWidth - containerWidth) * normalisedProgress;
|
||||
|
||||
return translate;
|
||||
});
|
||||
|
||||
const scrollListener: Action = () => {
|
||||
if (handleScroll) {
|
||||
window.addEventListener('scroll', handleScrollFunction, {
|
||||
passive: true,
|
||||
});
|
||||
} else {
|
||||
window.addEventListener('scroll', () => handleStops(scrollProgress), {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function handleScrollFunction() {
|
||||
if (!container) return;
|
||||
|
||||
const rawProgress =
|
||||
(-container?.offsetTop + window?.scrollY) /
|
||||
(containerHeight - screenHeight);
|
||||
|
||||
handleStops(rawProgress);
|
||||
}
|
||||
|
||||
// $effect(() => {
|
||||
// if (handleScroll && progressTween.current) {
|
||||
// untrack(() => {
|
||||
// progress = progressTween.target;
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
function handleStops(rawProgress: number) {
|
||||
scrollProgress =
|
||||
clampedProgress ? clamp(rawProgress, clampStart, clampEnd) : rawProgress;
|
||||
if (!stops || stops.length === 0) {
|
||||
progressTween.set(ease(clamp(rawProgress, 0, 1)), {
|
||||
duration: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scrubbed) {
|
||||
for (let i = 0; i < unscrubbedStops.length; i++) {
|
||||
if (
|
||||
rawProgress > unscrubbedStops[i] &&
|
||||
rawProgress <=
|
||||
(unscrubbedStops[i + 1] ??
|
||||
unscrubbedStops[unscrubbedStops.length - 1])
|
||||
) {
|
||||
const midPoint =
|
||||
unscrubbedStops[i] +
|
||||
((unscrubbedStops[i + 1] ??
|
||||
unscrubbedStops[unscrubbedStops.length - 1]) -
|
||||
unscrubbedStops[i]) *
|
||||
0.5;
|
||||
if (
|
||||
rawProgress >= midPoint &&
|
||||
progressTween.target !==
|
||||
(unscrubbedStops[i + 1] ??
|
||||
unscrubbedStops[unscrubbedStops.length - 1])
|
||||
) {
|
||||
progressTween.set(
|
||||
unscrubbedStops[i + 1] ??
|
||||
unscrubbedStops[unscrubbedStops.length - 1]
|
||||
);
|
||||
return;
|
||||
} else if (
|
||||
rawProgress < midPoint &&
|
||||
progressTween.target !== unscrubbedStops[i]
|
||||
) {
|
||||
progressTween.set(unscrubbedStops[i]);
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
rawProgress <
|
||||
unscrubbedStops[0] + (unscrubbedStops[1] ?? 0) * 0.5
|
||||
) {
|
||||
if (progressTween.target !== unscrubbedStops[0]) {
|
||||
progressTween.set(unscrubbedStops[0]);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < divisions.length; i++) {
|
||||
let oneByDivCount = 1 / divisionsCount;
|
||||
|
||||
let normalStart = i == 0 ? 0 : oneByDivCount * i;
|
||||
let normalEnd = i == divisionsCount - 1 ? 1 : oneByDivCount * (i + 1);
|
||||
|
||||
if (rawProgress >= normalStart && rawProgress < normalEnd) {
|
||||
let stopStart = divisions[i];
|
||||
let stopEnd = divisions[i + 1] ?? 1;
|
||||
let newProgressVal =
|
||||
stopStart +
|
||||
ease(map(rawProgress, normalStart, normalEnd, 0, 1)) *
|
||||
(stopEnd - stopStart);
|
||||
|
||||
progressTween.set(newProgressVal, { duration: 0 });
|
||||
return;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight={screenHeight} />
|
||||
|
||||
<Block {width}>
|
||||
<div
|
||||
{id}
|
||||
class={`horizontal-scroller-container ${cls}`}
|
||||
style="height: {height};"
|
||||
bind:this={container}
|
||||
bind:clientHeight={containerHeight}
|
||||
bind:clientWidth={containerWidth}
|
||||
>
|
||||
<div
|
||||
class="horizontal-scroller-content"
|
||||
bind:this={content}
|
||||
bind:clientWidth={contentWidth}
|
||||
style="transform: translateX({translateX}px);"
|
||||
use:scrollListener
|
||||
>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
{#if showDebugInfo}
|
||||
<div
|
||||
class="debug-info"
|
||||
style={`position: absolute; left: ${-translateX}px; top: 0px;`}
|
||||
>
|
||||
<Debug {componentState} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<style lang="scss">
|
||||
.horizontal-scroller-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
contain: paint;
|
||||
}
|
||||
|
||||
.horizontal-scroller-content {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
max-height: 100lvh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<script lang="ts">
|
||||
import DatawrapperChart from '../../DatawrapperChart/DatawrapperChart.svelte';
|
||||
import Headline from '../../Headline/Headline.svelte';
|
||||
import AiChart from './graphic/ai2svelte/ai-chart.svelte';
|
||||
import Block from '../../Block/Block.svelte';
|
||||
</script>
|
||||
|
||||
<div id="horizontal-stack">
|
||||
<div style="width: 100vw;">
|
||||
<DatawrapperChart
|
||||
title="Global abortion access"
|
||||
ariaLabel="map"
|
||||
id="abortion-rights-map"
|
||||
src="https://graphics.reuters.com/USA-ABORTION/lgpdwggnwvo/media-embed.html"
|
||||
frameTitle=""
|
||||
scrolling="no"
|
||||
textWidth="normal"
|
||||
width="wider"
|
||||
/>
|
||||
</div>
|
||||
<div style="width: 100vw;">
|
||||
<Headline
|
||||
hed="Reuters Graphics Interactive"
|
||||
dek="The beginning of a beautiful page"
|
||||
section="World News"
|
||||
/>
|
||||
</div>
|
||||
<div style="width: 100vw;">
|
||||
<Block width="normal">
|
||||
<AiChart />
|
||||
</Block>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
#horizontal-stack {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
gap: 10vw;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
15
src/components/HorizontalScroller/demo/Demo.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import ScrollerHorizontal from '../HorizontalScroller.svelte';
|
||||
import BodyText from '../../BodyText/BodyText.svelte';
|
||||
|
||||
let { ...args } = $props();
|
||||
|
||||
const foobarText: string =
|
||||
'In the mystical land of Foobaristan, the legendary hero Foo set out on an epic quest to find his missing semicolon, only to discover that Bar had accidentally used it as a bookmark inside a JSON file. Naturally, the entire kingdom crashed immediately. As the villagers panicked, Foo and Bar tried to fix the situation by turning everything off and on again, but all that did was anger the ancient deity known as “The Build System,” which now demanded three sacrifices: a clean cache, a fresh node_modules folder, and someone’s weekend. And thus began the saga nobody asked for, yet every developer somehow relates to.';
|
||||
</script>
|
||||
|
||||
<BodyText text={foobarText} />
|
||||
|
||||
<ScrollerHorizontal {...args} />
|
||||
|
||||
<BodyText text={foobarText} />
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<div style="width: 200vw; height: 100lvh;">
|
||||
<img
|
||||
src="https://picsum.photos/1200/640?t=1"
|
||||
alt="Sample"
|
||||
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0;"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<script lang="ts">
|
||||
import Demo from './graphic/ai2svelte/demo.svelte';
|
||||
import BodyText from '../../BodyText/BodyText.svelte';
|
||||
import HorizontalScroller from '../HorizontalScroller.svelte';
|
||||
import { map } from '../utils';
|
||||
import { sineInOut } from 'svelte/easing';
|
||||
|
||||
const foobarText: string =
|
||||
'In the mystical land of Foobaristan, the legendary hero Foo set out on an epic quest to find his missing semicolon, only to discover that Bar had accidentally used it as a bookmark inside a JSON file. Naturally, the entire kingdom crashed immediately. As the villagers panicked, Foo and Bar tried to fix the situation by turning everything off and on again, but all that did was anger the ancient deity known as “The Build System,” which now demanded three sacrifices: a clean cache, a fresh node_modules folder, and someone’s weekend. And thus began the saga nobody asked for, yet every developer somehow relates to.';
|
||||
|
||||
let scrollProgress: number = $state(0);
|
||||
let pngLayer: HTMLElement | null;
|
||||
let captions: HTMLElement[] | null;
|
||||
let threshold = 0.8;
|
||||
let screenWidth: number = $state(0);
|
||||
|
||||
function handleScroll() {
|
||||
if (pngLayer) {
|
||||
pngLayer.style.transform = `translateX(${map(scrollProgress, 0, 1, -400, 400)}px)`;
|
||||
}
|
||||
|
||||
if (captions?.length) {
|
||||
captions.forEach((caption, index) => {
|
||||
let captionWidth = caption.getBoundingClientRect().width;
|
||||
let captionMidpoint =
|
||||
caption.getBoundingClientRect().left + captionWidth / 2;
|
||||
|
||||
if (captionMidpoint < screenWidth * threshold) {
|
||||
caption.style.opacity = '1';
|
||||
} else {
|
||||
caption.style.opacity = '0';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onArtboardChange(artboard: HTMLElement) {
|
||||
pngLayer = artboard.querySelector('.g-png-layer-overlay');
|
||||
captions = Array.from(artboard.querySelectorAll('.g-captions'));
|
||||
|
||||
if (pngLayer) {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.addEventListener('scroll', handleScroll, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={screenWidth} />
|
||||
|
||||
<BodyText text={foobarText} />
|
||||
|
||||
<HorizontalScroller
|
||||
width="fluid"
|
||||
height="800lvh"
|
||||
direction="right"
|
||||
bind:scrollProgress
|
||||
easing={sineInOut}
|
||||
showDebugInfo
|
||||
>
|
||||
<Demo
|
||||
{onArtboardChange}
|
||||
debugTaggedText
|
||||
taggedText={{
|
||||
htext: {
|
||||
captions: {
|
||||
caption1:
|
||||
'<div class="scroller-caption"><strong>Destruction!</strong><br/>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>',
|
||||
caption2:
|
||||
'<div class="scroller-caption"><strong>Destruction!</strong><br/>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>',
|
||||
caption3:
|
||||
'<div class="scroller-caption"><strong>Destruction!</strong><br/>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>',
|
||||
caption4:
|
||||
'<div class="scroller-caption"><strong>Destruction!</strong><br/>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</HorizontalScroller>
|
||||
|
||||
<BodyText text={foobarText} />
|
||||
|
||||
<style lang="scss">
|
||||
:global(.scroller-caption) {
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 8px;
|
||||
filter: drop-shadow(0px 2px 16px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,630 @@
|
|||
<script lang="ts">
|
||||
// For demo purposes only, hard-wiring img paths from Vite
|
||||
// @ts-ignore img
|
||||
import chartXs from '../imgs/ai-chart-xs.png';
|
||||
// @ts-ignore img
|
||||
import chartSm from '../imgs/ai-chart-sm.png';
|
||||
// @ts-ignore img
|
||||
import chartMd from '../imgs/ai-chart-md.png';
|
||||
|
||||
let width = $state<number>();
|
||||
</script>
|
||||
|
||||
<!-- Generated by ai2html v0.100.0 - 2021-09-29 12:37 -->
|
||||
|
||||
<div id="g-_ai-chart-box" bind:clientWidth={width}>
|
||||
<!-- Artboard: xs -->
|
||||
{#if width && width >= 0 && width < 510}
|
||||
<div id="g-_ai-chart-xs" class="g-artboard" style="">
|
||||
<div style="padding: 0 0 91.7004% 0;"></div>
|
||||
<div
|
||||
id="g-_ai-chart-xs-img"
|
||||
class="g-aiImg"
|
||||
style={`background-image: url(${chartXs});`}
|
||||
></div>
|
||||
<div
|
||||
id="g-ai0-1"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:3.216%;margin-top:-7.7px;left:0.5952%;width:99px;"
|
||||
>
|
||||
<p class="g-pstyle0">Shake intensity</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-2"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:9.8251%;margin-top:-7.7px;left:4.9821%;width:47px;"
|
||||
>
|
||||
<p class="g-pstyle0">Light</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-3"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:15.7733%;margin-top:-7.7px;left:4.9821%;width:69px;"
|
||||
>
|
||||
<p class="g-pstyle0">Moderate</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-4"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:16.4343%;margin-top:-7.7px;left:79.0675%;width:82px;"
|
||||
>
|
||||
<p class="g-pstyle0">Cap-Haitien</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-5"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:21.7216%;margin-top:-7.7px;left:4.9821%;width:55px;"
|
||||
>
|
||||
<p class="g-pstyle0">Strong</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-6"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:28.0002%;margin-top:-7.7px;left:4.9821%;width:78px;"
|
||||
>
|
||||
<p class="g-pstyle0">Very strong</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-7"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:28.9916%;margin-top:-7.7px;left:62.2348%;width:68px;"
|
||||
>
|
||||
<p class="g-pstyle0">Gonaïves</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-8"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:39.9449%;margin-top:-14.9px;left:28.714%;margin-left:-36.5px;width:73px;"
|
||||
>
|
||||
<p class="g-pstyle1">Caribbean</p>
|
||||
<p class="g-pstyle1">Sea</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-9"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:42.6579%;margin-top:-10.1px;left:68.5061%;width:77px;"
|
||||
>
|
||||
<p class="g-pstyle2">HAITI</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-10"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:59.0632%;margin-top:-7.7px;left:11.2526%;width:63px;"
|
||||
>
|
||||
<p class="g-pstyle0">Jeremie</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-11"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:61.1155%;margin-top:-8.9px;left:70.5455%;width:106px;"
|
||||
>
|
||||
<p class="g-pstyle3">Port-au-Prince</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-12"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:62.1069%;margin-top:-8.9px;left:32.6015%;width:77px;"
|
||||
>
|
||||
<p class="g-pstyle3">Epicenter</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-13"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:78.8906%;margin-top:-7.7px;left:63.9138%;width:58px;"
|
||||
>
|
||||
<p class="g-pstyle0">Jacmel</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-14"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:80.2124%;margin-top:-7.7px;left:22.5649%;width:71px;"
|
||||
>
|
||||
<p class="g-pstyle0">Les Cayes</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-15"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:87.8129%;margin-top:-7.7px;left:0.6179%;width:49px;"
|
||||
>
|
||||
<p class="g-pstyle0">50 mi</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-16"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:91.0202%;margin-top:-11.4px;right:10.4418%;width:70px;"
|
||||
>
|
||||
<p class="g-pstyle4">Dominican</p>
|
||||
<p class="g-pstyle4">Republic</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai0-17"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:93.7611%;margin-top:-7.7px;left:0.6179%;width:52px;"
|
||||
>
|
||||
<p class="g-pstyle0">50 km</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Artboard: sm -->
|
||||
{#if width && width >= 510 && width < 660}
|
||||
<div id="g-_ai-chart-sm" class="g-artboard" style="">
|
||||
<div style="padding: 0 0 82.703% 0;"></div>
|
||||
<div
|
||||
id="g-_ai-chart-sm-img"
|
||||
class="g-aiImg"
|
||||
style={`background-image: url(${chartSm});`}
|
||||
></div>
|
||||
<div
|
||||
id="g-ai1-1"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:3.8773%;margin-top:-9.4px;left:0.3278%;width:111px;"
|
||||
>
|
||||
<p class="g-pstyle0">Shake intensity</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-2"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:9.0933%;margin-top:-9.4px;left:3.0258%;width:52px;"
|
||||
>
|
||||
<p class="g-pstyle0">Light</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-3"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:13.5979%;margin-top:-9.4px;left:3.0259%;width:77px;"
|
||||
>
|
||||
<p class="g-pstyle0">Moderate</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-4"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:16.6801%;margin-top:-9.4px;left:70.3255%;width:92px;"
|
||||
>
|
||||
<p class="g-pstyle0">Cap-Haitien</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-5"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:18.3397%;margin-top:-9.4px;left:3.0258%;width:61px;"
|
||||
>
|
||||
<p class="g-pstyle0">Strong</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-6"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:22.6073%;margin-top:-9.4px;left:3.0258%;width:88px;"
|
||||
>
|
||||
<p class="g-pstyle0">Very strong</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-7"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:28.5344%;margin-top:-9.4px;left:55.9181%;width:76px;"
|
||||
>
|
||||
<p class="g-pstyle0">Gonaïves</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-8"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:38.8091%;margin-top:-17.7px;left:27.2818%;margin-left:-41px;width:82px;"
|
||||
>
|
||||
<p class="g-pstyle1">Caribbean</p>
|
||||
<p class="g-pstyle1">Sea</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-9"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:39.9724%;margin-top:-8.6px;left:61.2858%;width:67px;"
|
||||
>
|
||||
<p class="g-pstyle2">HAITI</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-10"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:56.985%;margin-top:-9.4px;left:12.2815%;width:69px;"
|
||||
>
|
||||
<p class="g-pstyle0">Jeremie</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-11"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:59.1569%;margin-top:-9.5px;left:63.0314%;width:112px;"
|
||||
>
|
||||
<p class="g-pstyle3">Port-au-Prince</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-12"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:60.1053%;margin-top:-9.5px;left:30.5543%;width:81px;"
|
||||
>
|
||||
<p class="g-pstyle3">Epicenter</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-13"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:62.7194%;margin-top:-16.5px;left:91.2282%;margin-left:-57px;width:114px;"
|
||||
>
|
||||
<p class="g-pstyle4">Dominican</p>
|
||||
<p class="g-pstyle4">Republic</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-14"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:75.4778%;margin-top:-9.4px;left:57.3552%;width:64px;"
|
||||
>
|
||||
<p class="g-pstyle0">Jacmel</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-15"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:76.6632%;margin-top:-9.4px;left:21.9639%;width:79px;"
|
||||
>
|
||||
<p class="g-pstyle0">Les Cayes</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-16"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:85.5251%;margin-top:-7.7px;left:0.1344%;width:49px;"
|
||||
>
|
||||
<p class="g-pstyle5">50 mi</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai1-17"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:90.0297%;margin-top:-7.7px;left:0.1344%;width:52px;"
|
||||
>
|
||||
<p class="g-pstyle5">50 km</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Artboard: md -->
|
||||
{#if width && width >= 660}
|
||||
<div id="g-_ai-chart-md" class="g-artboard" style="">
|
||||
<div style="padding: 0 0 79.6009% 0;"></div>
|
||||
<div
|
||||
id="g-_ai-chart-md-img"
|
||||
class="g-aiImg"
|
||||
style={`background-image: url(${chartMd});`}
|
||||
></div>
|
||||
<div
|
||||
id="g-ai2-1"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:2.3515%;margin-top:-9.4px;left:0.3608%;width:111px;"
|
||||
>
|
||||
<p class="g-pstyle0">Shake intensity</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-2"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:7.6811%;margin-top:-9.4px;left:2.6603%;width:52px;"
|
||||
>
|
||||
<p class="g-pstyle0">Light</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-3"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:12.2494%;margin-top:-9.4px;left:2.6604%;width:77px;"
|
||||
>
|
||||
<p class="g-pstyle0">Moderate</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-4"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:15.4852%;margin-top:-9.4px;left:70.3606%;width:92px;"
|
||||
>
|
||||
<p class="g-pstyle0">Cap-Haitien</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-5"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:17.1983%;margin-top:-9.4px;left:2.6603%;width:61px;"
|
||||
>
|
||||
<p class="g-pstyle0">Strong</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-6"
|
||||
class="g-legend g-aiAbs g-aiPointText"
|
||||
style="top:21.7666%;margin-top:-9.4px;left:2.6603%;width:88px;"
|
||||
>
|
||||
<p class="g-pstyle0">Very strong</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-7"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:27.6672%;margin-top:-9.4px;left:55.993%;width:76px;"
|
||||
>
|
||||
<p class="g-pstyle0">Gonaïves</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-8"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:38.0099%;margin-top:-17.7px;left:27.2388%;margin-left:-41px;width:82px;"
|
||||
>
|
||||
<p class="g-pstyle1">Caribbean</p>
|
||||
<p class="g-pstyle1">Sea</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-9"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:42.7626%;margin-top:-10.7px;left:62.8914%;width:80px;"
|
||||
>
|
||||
<p class="g-pstyle2">HAITI</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-10"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:50.0029%;margin-top:-17.7px;left:92.295%;margin-left:-60.5px;width:121px;"
|
||||
>
|
||||
<p class="g-pstyle3">Dominican</p>
|
||||
<p class="g-pstyle3">Republic</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-11"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:57.3608%;margin-top:-9.4px;left:12.2815%;width:69px;"
|
||||
>
|
||||
<p class="g-pstyle0">Jeremie</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-12"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:60.2742%;margin-top:-10.7px;left:30.6995%;width:89px;"
|
||||
>
|
||||
<p class="g-pstyle4">Epicenter</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-13"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:62.5583%;margin-top:-10.7px;left:66.3403%;width:125px;"
|
||||
>
|
||||
<p class="g-pstyle4">Port-au-Prince</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-14"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:75.6338%;margin-top:-9.4px;left:57.8174%;width:64px;"
|
||||
>
|
||||
<p class="g-pstyle0">Jacmel</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-15"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:77.3469%;margin-top:-9.4px;left:22.5239%;width:79px;"
|
||||
>
|
||||
<p class="g-pstyle0">Les Cayes</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-16"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:86.936%;margin-top:-7.7px;left:0.1678%;width:49px;"
|
||||
>
|
||||
<p class="g-pstyle5">50 mi</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-ai2-17"
|
||||
class="g-map-labels g-aiAbs g-aiPointText"
|
||||
style="top:91.5043%;margin-top:-7.7px;left:0.1678%;width:52px;"
|
||||
>
|
||||
<p class="g-pstyle5">50 km</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- End ai2html - 2021-09-29 12:37 -->
|
||||
|
||||
<!-- ai file: _ai-chart.ai -->
|
||||
<style lang="scss">
|
||||
#g-_ai-chart-box,
|
||||
#g-_ai-chart-box .g-artboard {
|
||||
margin: 0 auto;
|
||||
}
|
||||
#g-_ai-chart-box p {
|
||||
margin: 0;
|
||||
}
|
||||
#g-_ai-chart-box .g-aiAbs {
|
||||
position: absolute;
|
||||
}
|
||||
#g-_ai-chart-box .g-aiImg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
#g-_ai-chart-box .g-aiPointText p {
|
||||
white-space: nowrap;
|
||||
}
|
||||
#g-_ai-chart-xs {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#g-_ai-chart-xs p {
|
||||
font-family:
|
||||
'Source Sans Pro',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
letter-spacing: 0em;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
color: rgb(51, 51, 51);
|
||||
text-transform: none;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
mix-blend-mode: normal;
|
||||
font-style: normal;
|
||||
position: static;
|
||||
}
|
||||
#g-_ai-chart-xs .g-pstyle0 {
|
||||
height: 14px;
|
||||
}
|
||||
#g-_ai-chart-xs .g-pstyle1 {
|
||||
font-style: italic;
|
||||
height: 14px;
|
||||
text-align: center;
|
||||
color: rgb(113, 115, 117);
|
||||
}
|
||||
#g-_ai-chart-xs .g-pstyle2 {
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
height: 18px;
|
||||
letter-spacing: 0.25em;
|
||||
font-size: 15px;
|
||||
}
|
||||
#g-_ai-chart-xs .g-pstyle3 {
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
height: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
#g-_ai-chart-xs .g-pstyle4 {
|
||||
line-height: 11px;
|
||||
height: 11px;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
#g-_ai-chart-sm {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#g-_ai-chart-sm p {
|
||||
font-family:
|
||||
'Source Sans Pro',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 17px;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
letter-spacing: 0em;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
color: rgb(51, 51, 51);
|
||||
text-transform: none;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
mix-blend-mode: normal;
|
||||
font-style: normal;
|
||||
position: static;
|
||||
}
|
||||
#g-_ai-chart-sm .g-pstyle0 {
|
||||
height: 17px;
|
||||
}
|
||||
#g-_ai-chart-sm .g-pstyle1 {
|
||||
font-style: italic;
|
||||
height: 17px;
|
||||
text-align: center;
|
||||
color: rgb(113, 115, 117);
|
||||
}
|
||||
#g-_ai-chart-sm .g-pstyle2 {
|
||||
font-weight: 700;
|
||||
line-height: 15px;
|
||||
height: 15px;
|
||||
letter-spacing: 0.25em;
|
||||
font-size: 12px;
|
||||
}
|
||||
#g-_ai-chart-sm .g-pstyle3 {
|
||||
font-weight: 700;
|
||||
height: 17px;
|
||||
}
|
||||
#g-_ai-chart-sm .g-pstyle4 {
|
||||
font-weight: 300;
|
||||
line-height: 16px;
|
||||
height: 16px;
|
||||
letter-spacing: 0.25em;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
color: rgb(134, 136, 139);
|
||||
}
|
||||
#g-_ai-chart-sm .g-pstyle5 {
|
||||
line-height: 14px;
|
||||
height: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
#g-_ai-chart-md {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#g-_ai-chart-md p {
|
||||
font-family:
|
||||
'Source Sans Pro',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 17px;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
letter-spacing: 0em;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
color: rgb(51, 51, 51);
|
||||
text-transform: none;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
mix-blend-mode: normal;
|
||||
font-style: normal;
|
||||
position: static;
|
||||
}
|
||||
#g-_ai-chart-md .g-pstyle0 {
|
||||
height: 17px;
|
||||
}
|
||||
#g-_ai-chart-md .g-pstyle1 {
|
||||
font-style: italic;
|
||||
height: 17px;
|
||||
text-align: center;
|
||||
color: rgb(113, 115, 117);
|
||||
}
|
||||
#g-_ai-chart-md .g-pstyle2 {
|
||||
font-weight: 700;
|
||||
line-height: 19px;
|
||||
height: 19px;
|
||||
letter-spacing: 0.25em;
|
||||
font-size: 16px;
|
||||
}
|
||||
#g-_ai-chart-md .g-pstyle3 {
|
||||
font-weight: 300;
|
||||
height: 17px;
|
||||
letter-spacing: 0.25em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
color: rgb(134, 136, 139);
|
||||
}
|
||||
#g-_ai-chart-md .g-pstyle4 {
|
||||
font-weight: 700;
|
||||
line-height: 19px;
|
||||
height: 19px;
|
||||
font-size: 16px;
|
||||
}
|
||||
#g-_ai-chart-md .g-pstyle5 {
|
||||
line-height: 14px;
|
||||
height: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Custom CSS */
|
||||
</style>
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
<script>
|
||||
// For demo purposes only, hard-wiring img paths from Vite
|
||||
// @ts-ignore img
|
||||
import imageXl from '../imgs/demo-xl.jpg';
|
||||
// @ts-ignore img
|
||||
import imageLg from '../imgs/demo-lg.jpg';
|
||||
// @ts-ignore img
|
||||
import imagePngOverlayXl from '../imgs/layer-overlay-xl.png';
|
||||
|
||||
let {
|
||||
assetsPath = '/',
|
||||
onAiMounted = () => {},
|
||||
onArtboardChange = () => {},
|
||||
taggedText = { text: {}, htext: {} },
|
||||
debugTaggedText = false,
|
||||
artboardWidth = $bindable(undefined),
|
||||
} = $props();
|
||||
import { onMount, untrack } from 'svelte';
|
||||
let aiBox;
|
||||
let screenWidth = $state(0);
|
||||
let aiBoxWidth = $derived(artboardWidth ?? screenWidth);
|
||||
let activeArtboard = $state(undefined);
|
||||
onMount(() => {
|
||||
onAiMounted();
|
||||
});
|
||||
$effect(() => {
|
||||
if (aiBoxWidth) {
|
||||
const currentArtboard = aiBox.querySelectorAll('.g-artboard')[0];
|
||||
if (currentArtboard?.id !== activeArtboard?.id) {
|
||||
activeArtboard = untrack(() => currentArtboard);
|
||||
onArtboardChange(activeArtboard);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={screenWidth} />
|
||||
|
||||
<div
|
||||
id="g-demo-box"
|
||||
class="ai2svelte"
|
||||
bind:this={aiBox}
|
||||
style:--debug-tagged-text={debugTaggedText ? 'visible' : 'hidden'}
|
||||
>
|
||||
<!-- Artboard: lg -->
|
||||
{#if aiBoxWidth && aiBoxWidth >= 0 && aiBoxWidth < 800}
|
||||
<div
|
||||
id="g-demo-lg"
|
||||
class="g-artboard"
|
||||
style="max-width: 799px;aspect-ratio: 2.75483870967742;"
|
||||
data-aspect-ratio="2.755"
|
||||
data-min-width="0"
|
||||
data-max-width="799"
|
||||
>
|
||||
<div
|
||||
id="g-demo-lg-img"
|
||||
class="g-demo-lg-img g-aiImg"
|
||||
alt=""
|
||||
style="background-image: url({imageLg});"
|
||||
loading="lazy"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Artboard: xl -->
|
||||
{#if aiBoxWidth && aiBoxWidth >= 800}
|
||||
<div
|
||||
id="g-demo-xl"
|
||||
class="g-artboard"
|
||||
style="min-width: 800px;aspect-ratio: 6.6758064516129;"
|
||||
data-aspect-ratio="6.676"
|
||||
data-min-width="800"
|
||||
>
|
||||
<div
|
||||
id="g-demo-xl-img"
|
||||
class="g-demo-xl-img g-aiImg"
|
||||
alt=""
|
||||
style="background-image: url({imageXl});"
|
||||
loading="lazy"
|
||||
></div>
|
||||
<div
|
||||
id="g-png-layer-overlay-xl"
|
||||
class="g-png-layer-overlay g-aiImg"
|
||||
alt=""
|
||||
style="opacity:1;;background-image: url({imagePngOverlayXl});"
|
||||
loading="lazy"
|
||||
></div>
|
||||
<div
|
||||
id="g-caption2"
|
||||
class="g-captions g-aiAbs"
|
||||
style="top:38.5484%;left:23.6715%;width:5.2428%;"
|
||||
>
|
||||
<p
|
||||
class="g-pstyle0 g-taggedText g-htext"
|
||||
data-tagged-type="HTEXT"
|
||||
data-tagged-prop="captions.caption2"
|
||||
>
|
||||
{@html taggedText?.htext?.captions?.caption2 || ''}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-caption3"
|
||||
class="g-captions g-aiAbs"
|
||||
style="top:38.5484%;left:49.4749%;width:5.2428%;"
|
||||
>
|
||||
<p
|
||||
class="g-pstyle0 g-taggedText g-htext"
|
||||
data-tagged-type="HTEXT"
|
||||
data-tagged-prop="captions.caption3"
|
||||
>
|
||||
{@html taggedText?.htext?.captions?.caption3 || ''}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-caption4"
|
||||
class="g-captions g-aiAbs"
|
||||
style="top:38.5484%;left:83.976%;width:5.2428%;"
|
||||
>
|
||||
<p
|
||||
class="g-pstyle0 g-taggedText g-htext"
|
||||
data-tagged-type="HTEXT"
|
||||
data-tagged-prop="captions.caption4"
|
||||
>
|
||||
{@html taggedText?.htext?.captions?.caption4 || ''}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
id="g-caption1"
|
||||
class="g-captions g-aiAbs"
|
||||
style="top:38.5484%;left:2.966%;width:3.4791%;"
|
||||
>
|
||||
<p
|
||||
class="g-pstyle0 g-taggedText g-htext"
|
||||
data-tagged-type="HTEXT"
|
||||
data-tagged-prop="captions.caption1"
|
||||
>
|
||||
{@html taggedText?.htext?.captions?.caption1 || ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
|
||||
TAGGED TEXT PROPS
|
||||
|
||||
taggedText={{
|
||||
{
|
||||
"text": {
|
||||
|
||||
},
|
||||
"htext": {
|
||||
"captions": {
|
||||
"caption1": "",
|
||||
"caption2": "",
|
||||
"caption3": "",
|
||||
"caption4": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
-->
|
||||
<!-- End ai2svelte - 2026-01-07 11:23 -->
|
||||
|
||||
<!-- Generated by ai2svelte v1.0.3 - 2026-01-07 11:23 -->
|
||||
<!-- ai file: demo.ai -->
|
||||
<style lang="scss">
|
||||
#g-demo-box,
|
||||
#g-demo-box .g-artboard {
|
||||
margin: 0 auto;
|
||||
}
|
||||
#g-demo-box .g-artboard {
|
||||
margin: 0 auto;
|
||||
min-height: 100%;
|
||||
min-width: unset !important;
|
||||
max-width: unset !important;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
#g-demo-box p {
|
||||
margin: 0;
|
||||
}
|
||||
#g-demo-box .g-aiAbs {
|
||||
position: absolute;
|
||||
}
|
||||
#g-demo-box .g-aiImg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
#g-demo-box .g-aiSymbol {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#g-demo-box .g-aiPointText p {
|
||||
white-space: nowrap;
|
||||
}
|
||||
#g-demo-box {
|
||||
height: 100%;
|
||||
}
|
||||
#g-demo-lg {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#g-demo-xl {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#g-demo-xl p {
|
||||
font-family: var(--theme-font-family-sans-serif), Knowledge, sans-serif;
|
||||
font-weight: regular;
|
||||
line-height: 22px;
|
||||
opacity: 1;
|
||||
letter-spacing: 0em;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
color: rgb(0, 0, 0);
|
||||
text-transform: none;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
mix-blend-mode: normal;
|
||||
font-style: normal;
|
||||
height: auto;
|
||||
position: static;
|
||||
}
|
||||
#g-demo-xl .g-pstyle0 {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Custom CSS */
|
||||
|
||||
.g-taggedText:empty::before {
|
||||
content: attr(data-tagged-type);
|
||||
padding: 0px 4px;
|
||||
font-size: 0.6rem;
|
||||
color: #fff;
|
||||
background-color: black;
|
||||
display: block;
|
||||
font-weight: 800;
|
||||
visibility: var(--debug-tagged-text, hidden);
|
||||
}
|
||||
|
||||
.g-taggedText:empty::after {
|
||||
content: attr(data-tagged-prop);
|
||||
padding: 0px 4px;
|
||||
font-size: 0.8rem;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
visibility: var(--debug-tagged-text, hidden);
|
||||
}
|
||||
|
||||
.g-taggedText:empty {
|
||||
background-color: #00000088;
|
||||
outline: 2px solid black;
|
||||
visibility: var(--debug-tagged-text, hidden);
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 618 KiB |
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 226 KiB |
BIN
src/components/HorizontalScroller/demo/graphic/imgs/demo-lg.jpg
Normal file
|
After Width: | Height: | Size: 1 MiB |
BIN
src/components/HorizontalScroller/demo/graphic/imgs/demo-xl.jpg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
BIN
src/components/HorizontalScroller/demo/graphic/placeholder.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts">
|
||||
import Demo from './graphic/ai2svelte/demo.svelte';
|
||||
import BodyText from '../../BodyText/BodyText.svelte';
|
||||
import HorizontalScroller from '../HorizontalScroller.svelte';
|
||||
import ScrollerBase from '../../ScrollerBase/ScrollerBase.svelte';
|
||||
import { circInOut } from 'svelte/easing';
|
||||
|
||||
const foobarText: string =
|
||||
'In the mystical land of Foobaristan, the legendary hero Foo set out on an epic quest to find his missing semicolon, only to discover that Bar had accidentally used it as a bookmark inside a JSON file. Naturally, the entire kingdom crashed immediately. As the villagers panicked, Foo and Bar tried to fix the situation by turning everything off and on again, but all that did was anger the ancient deity known as “The Build System,” which now demanded three sacrifices: a clean cache, a fresh node_modules folder, and someone’s weekend. And thus began the saga nobody asked for, yet every developer somehow relates to.';
|
||||
|
||||
// Optional: Bind your own variables to use them in your code.
|
||||
let count = $state(1);
|
||||
let index = $state(0);
|
||||
let offset = $state(0);
|
||||
let progress = $state(0);
|
||||
let top = $state(0);
|
||||
let threshold = $state(0.5);
|
||||
let bottom = $state(1);
|
||||
</script>
|
||||
|
||||
<BodyText text={foobarText} />
|
||||
|
||||
<ScrollerBase
|
||||
{top}
|
||||
{threshold}
|
||||
{bottom}
|
||||
bind:count
|
||||
bind:index
|
||||
bind:offset
|
||||
bind:progress
|
||||
query="div.step-foreground-container"
|
||||
>
|
||||
{#snippet backgroundSnippet()}
|
||||
<!-- Add custom background HTML or component -->
|
||||
<HorizontalScroller
|
||||
width="fluid"
|
||||
height="100lvh"
|
||||
direction="right"
|
||||
bind:scrollProgress={progress}
|
||||
scrubbed
|
||||
stops={[0.5]}
|
||||
handleScroll={false}
|
||||
easing={circInOut}
|
||||
showDebugInfo
|
||||
>
|
||||
<Demo />
|
||||
</HorizontalScroller>
|
||||
{/snippet}
|
||||
{#snippet foregroundSnippet()}
|
||||
<!-- Add custom foreground HTML or component -->
|
||||
<div class="step-foreground-container"><p>Step 1</p></div>
|
||||
<div class="step-foreground-container"><p>Step 2</p></div>
|
||||
<div class="step-foreground-container"><p>Step 3</p></div>
|
||||
<div class="step-foreground-container"><p>Step 4</p></div>
|
||||
<div class="step-foreground-container"><p>Step 5</p></div>
|
||||
{/snippet}
|
||||
</ScrollerBase>
|
||||
|
||||
<style lang="scss">
|
||||
.step-foreground-container {
|
||||
height: 100vh;
|
||||
width: 50%;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
padding: 1em;
|
||||
margin: 0 auto 10px 0;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
p {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
40
src/components/HorizontalScroller/utils.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||