fancy-ui-svelte 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -74
- package/dist/fancy-ui/dock/index.d.ts +1 -1
- package/dist/fancy-ui/dock/index.js +0 -1
- package/dist/fancy-ui/editorial-engine/EditorialEngine.svelte +181 -0
- package/dist/fancy-ui/editorial-engine/EditorialEngine.svelte.d.ts +26 -0
- package/dist/fancy-ui/editorial-engine/README.md +56 -0
- package/dist/fancy-ui/editorial-engine/engine.d.ts +7 -0
- package/dist/fancy-ui/editorial-engine/engine.js +581 -0
- package/dist/fancy-ui/editorial-engine/index.d.ts +2 -0
- package/dist/fancy-ui/editorial-engine/index.js +1 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte +202 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte.d.ts +20 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.test.d.ts +1 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.test.js +48 -0
- package/dist/fancy-ui/frosted-glass/README.md +59 -0
- package/dist/fancy-ui/frosted-glass/index.d.ts +1 -0
- package/dist/fancy-ui/frosted-glass/index.js +1 -0
- package/dist/fancy-ui/index.d.ts +5 -0
- package/dist/fancy-ui/index.js +5 -0
- package/dist/fancy-ui/line-reveal/LineReveal.svelte +168 -0
- package/dist/fancy-ui/line-reveal/LineReveal.svelte.d.ts +33 -0
- package/dist/fancy-ui/line-reveal/README.md +42 -0
- package/dist/fancy-ui/line-reveal/index.d.ts +2 -0
- package/dist/fancy-ui/line-reveal/index.js +1 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.svelte +323 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.svelte.d.ts +19 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.test.d.ts +1 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.test.js +123 -0
- package/dist/fancy-ui/noise-reveal/index.d.ts +2 -0
- package/dist/fancy-ui/noise-reveal/index.js +1 -0
- package/dist/fancy-ui/registry.js +196 -11
- package/dist/fancy-ui/themes.d.ts +38 -0
- package/dist/fancy-ui/themes.js +356 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils/animation.d.ts +109 -0
- package/dist/utils/animation.js +198 -0
- package/dist/utils/color.d.ts +128 -0
- package/dist/utils/color.js +314 -0
- package/dist/utils/geometry.d.ts +125 -0
- package/dist/utils/geometry.js +250 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.js +39 -0
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Beautiful animation and UI components for **Svelte 5**.
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|

|
|
8
|
-

|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -23,7 +23,7 @@ Beautiful animation and UI components for **Svelte 5**.
|
|
|
23
23
|
- **Svelte 5 Runes** — Built with `$state`, `$derived`, `$effect`, and `$props`
|
|
24
24
|
- **Tailwind CSS 4** — Utility-first styling with theme tokens
|
|
25
25
|
- **TypeScript** — Fully typed props and events
|
|
26
|
-
- **
|
|
26
|
+
- **56 Components** — Buttons, text animations, backgrounds, effects, and more
|
|
27
27
|
- **Dark Mode** — All components support light and dark themes
|
|
28
28
|
- **Tested** — Component tests with Vitest and Testing Library
|
|
29
29
|
|
|
@@ -31,88 +31,88 @@ Beautiful animation and UI components for **Svelte 5**.
|
|
|
31
31
|
|
|
32
32
|
### Buttons
|
|
33
33
|
|
|
34
|
-
| Component
|
|
35
|
-
|
|
36
|
-
| RainbowButton
|
|
37
|
-
| RippleButton
|
|
38
|
-
| ShimmerButton
|
|
39
|
-
| GradientButton
|
|
34
|
+
| Component | Description | Demo |
|
|
35
|
+
| ---------------------- | ---------------------------------------- | --------------------------------------------------------------- |
|
|
36
|
+
| RainbowButton | Animated rainbow gradient border effect | [Demo](https://fancy-ui.rama.app/demo/rainbow-button) |
|
|
37
|
+
| RippleButton | Ripple click effect | [Demo](https://fancy-ui.rama.app/demo/ripple-button) |
|
|
38
|
+
| ShimmerButton | Rotating conic-gradient shimmer border | [Demo](https://fancy-ui.rama.app/demo/shimmer-button) |
|
|
39
|
+
| GradientButton | Rotating conic-gradient rainbow border | [Demo](https://fancy-ui.rama.app/demo/gradient-button) |
|
|
40
40
|
| InteractiveHoverButton | Hover effect revealing alternate content | [Demo](https://fancy-ui.rama.app/demo/interactive-hover-button) |
|
|
41
41
|
|
|
42
42
|
### Cards
|
|
43
43
|
|
|
44
|
-
| Component
|
|
45
|
-
|
|
46
|
-
| Card3D
|
|
47
|
-
| CardSpotlight
|
|
44
|
+
| Component | Description | Demo |
|
|
45
|
+
| ------------------- | -------------------------------------------- | ------------------------------------------------------------ |
|
|
46
|
+
| Card3D | 3D tilt card with mouse tracking | [Demo](https://fancy-ui.rama.app/demo/card-3d) |
|
|
47
|
+
| CardSpotlight | Spotlight effect following mouse | [Demo](https://fancy-ui.rama.app/demo/card-spotlight) |
|
|
48
48
|
| DirectionAwareHover | Overlay slides in from mouse entry direction | [Demo](https://fancy-ui.rama.app/demo/direction-aware-hover) |
|
|
49
|
-
| FlipCard
|
|
50
|
-
| GlareCard
|
|
51
|
-
| TextRevealCard
|
|
49
|
+
| FlipCard | Two-sided card with flip animation | [Demo](https://fancy-ui.rama.app/demo/flip-card) |
|
|
50
|
+
| GlareCard | Glare reflection effect on hover | [Demo](https://fancy-ui.rama.app/demo/glare-card) |
|
|
51
|
+
| TextRevealCard | Text reveal on hover | [Demo](https://fancy-ui.rama.app/demo/text-reveal-card) |
|
|
52
52
|
|
|
53
53
|
### Text & Typography
|
|
54
54
|
|
|
55
|
-
| Component
|
|
56
|
-
|
|
57
|
-
| BlurReveal
|
|
58
|
-
| BoxReveal
|
|
59
|
-
| ColourfulText
|
|
60
|
-
| ContainerTextFlip
|
|
61
|
-
| FlipWords
|
|
62
|
-
| HyperText
|
|
63
|
-
| LetterPullup
|
|
64
|
-
| LineShadowText
|
|
65
|
-
| NumberTicker
|
|
66
|
-
| SparklesText
|
|
67
|
-
| TextGenerateEffect | Word-by-word text generation effect
|
|
55
|
+
| Component | Description | Demo |
|
|
56
|
+
| ------------------ | ---------------------------------------------- | ----------------------------------------------------------- |
|
|
57
|
+
| BlurReveal | Scroll-triggered blur-to-clear reveal | [Demo](https://fancy-ui.rama.app/demo/blur-reveal) |
|
|
58
|
+
| BoxReveal | Sliding colored box reveal animation | [Demo](https://fancy-ui.rama.app/demo/box-reveal) |
|
|
59
|
+
| ColourfulText | Per-character color animation | [Demo](https://fancy-ui.rama.app/demo/colourful-text) |
|
|
60
|
+
| ContainerTextFlip | Text flipping inside a container | [Demo](https://fancy-ui.rama.app/demo/container-text-flip) |
|
|
61
|
+
| FlipWords | Cycling word animation with per-letter effects | [Demo](https://fancy-ui.rama.app/demo/flip-words) |
|
|
62
|
+
| HyperText | Character scramble on hover | [Demo](https://fancy-ui.rama.app/demo/hyper-text) |
|
|
63
|
+
| LetterPullup | Staggered letter pull-up animation | [Demo](https://fancy-ui.rama.app/demo/letter-pullup) |
|
|
64
|
+
| LineShadowText | Text with animated line shadow | [Demo](https://fancy-ui.rama.app/demo/line-shadow-text) |
|
|
65
|
+
| NumberTicker | Animated number counter with easing | [Demo](https://fancy-ui.rama.app/demo/number-ticker) |
|
|
66
|
+
| SparklesText | Animated SVG sparkle stars overlay | [Demo](https://fancy-ui.rama.app/demo/sparkles-text) |
|
|
67
|
+
| TextGenerateEffect | Word-by-word text generation effect | [Demo](https://fancy-ui.rama.app/demo/text-generate-effect) |
|
|
68
68
|
|
|
69
69
|
### Backgrounds
|
|
70
70
|
|
|
71
|
-
| Component
|
|
72
|
-
|
|
73
|
-
| FallingStarsBg
|
|
74
|
-
| FlickeringGrid
|
|
75
|
-
| InteractiveGridPattern | SVG grid with hover highlights
|
|
76
|
-
| StarsBackground
|
|
71
|
+
| Component | Description | Demo |
|
|
72
|
+
| ---------------------- | -------------------------------------- | --------------------------------------------------------------- |
|
|
73
|
+
| FallingStarsBg | Canvas 3D starfield with motion trails | [Demo](https://fancy-ui.rama.app/demo/bg-falling-stars) |
|
|
74
|
+
| FlickeringGrid | Canvas grid with flickering opacity | [Demo](https://fancy-ui.rama.app/demo/flickering-grid) |
|
|
75
|
+
| InteractiveGridPattern | SVG grid with hover highlights | [Demo](https://fancy-ui.rama.app/demo/interactive-grid-pattern) |
|
|
76
|
+
| StarsBackground | Starfield with parallax mouse tracking | [Demo](https://fancy-ui.rama.app/demo/bg-stars) |
|
|
77
77
|
|
|
78
78
|
### Effects & Animations
|
|
79
79
|
|
|
80
|
-
| Component
|
|
81
|
-
|
|
82
|
-
| AnimatedBeam
|
|
83
|
-
| BorderBeam
|
|
84
|
-
| Confetti
|
|
85
|
-
| FluidCursor
|
|
86
|
-
| GlowBorder
|
|
87
|
-
| GlowingEffect
|
|
80
|
+
| Component | Description | Demo |
|
|
81
|
+
| ---------------- | ----------------------------------------- | --------------------------------------------------------- |
|
|
82
|
+
| AnimatedBeam | SVG beams connecting elements | [Demo](https://fancy-ui.rama.app/demo/animated-beam) |
|
|
83
|
+
| BorderBeam | Beam effect traveling around borders | [Demo](https://fancy-ui.rama.app/demo/border-beam) |
|
|
84
|
+
| Confetti | Configurable confetti burst | [Demo](https://fancy-ui.rama.app/demo/confetti) |
|
|
85
|
+
| FluidCursor | WebGL fluid simulation following cursor | [Demo](https://fancy-ui.rama.app/demo/fluid-cursor) |
|
|
86
|
+
| GlowBorder | Animated glowing border with gradients | [Demo](https://fancy-ui.rama.app/demo/glow-border) |
|
|
87
|
+
| GlowingEffect | Glowing highlight on hover | [Demo](https://fancy-ui.rama.app/demo/glowing-effect) |
|
|
88
88
|
| ImageTrailCursor | Cursor-following image trail (8 variants) | [Demo](https://fancy-ui.rama.app/demo/image-trail-cursor) |
|
|
89
|
-
| LiquidGlass
|
|
90
|
-
| Meteors
|
|
91
|
-
| NeonBorder
|
|
92
|
-
| Ripple
|
|
93
|
-
| SmoothCursor
|
|
94
|
-
| Sparkles
|
|
95
|
-
| TracingBeam
|
|
89
|
+
| LiquidGlass | Liquid glass morphism effect | [Demo](https://fancy-ui.rama.app/demo/liquid-glass) |
|
|
90
|
+
| Meteors | Animated meteor shower effect | [Demo](https://fancy-ui.rama.app/demo/meteors) |
|
|
91
|
+
| NeonBorder | Dual-color neon glow border | [Demo](https://fancy-ui.rama.app/demo/neon-border) |
|
|
92
|
+
| Ripple | Expanding ripple rings | [Demo](https://fancy-ui.rama.app/demo/ripple) |
|
|
93
|
+
| SmoothCursor | Smooth lagging cursor follower | [Demo](https://fancy-ui.rama.app/demo/smooth-cursor) |
|
|
94
|
+
| Sparkles | Particle sparkle canvas | [Demo](https://fancy-ui.rama.app/demo/sparkles) |
|
|
95
|
+
| TracingBeam | Scroll-driven tracing beam | [Demo](https://fancy-ui.rama.app/demo/tracing-beam) |
|
|
96
96
|
|
|
97
97
|
### Layout
|
|
98
98
|
|
|
99
|
-
| Component
|
|
100
|
-
|
|
101
|
-
| BentoGrid
|
|
102
|
-
| Book
|
|
103
|
-
| ContainerScroll | 3D scroll perspective container
|
|
104
|
-
| Focus
|
|
105
|
-
| Marquee
|
|
99
|
+
| Component | Description | Demo |
|
|
100
|
+
| --------------- | --------------------------------------------- | ------------------------------------------------------- |
|
|
101
|
+
| BentoGrid | Bento-style responsive grid layout | [Demo](https://fancy-ui.rama.app/demo/bento-grid) |
|
|
102
|
+
| Book | 3D book flip animation | [Demo](https://fancy-ui.rama.app/demo/book) |
|
|
103
|
+
| ContainerScroll | 3D scroll perspective container | [Demo](https://fancy-ui.rama.app/demo/container-scroll) |
|
|
104
|
+
| Focus | Focus-expand card layout | [Demo](https://fancy-ui.rama.app/demo/focus) |
|
|
105
|
+
| Marquee | Infinite scrolling for text, images, or cards | [Demo](https://fancy-ui.rama.app/demo/marquee) |
|
|
106
106
|
|
|
107
107
|
### Navigation & Display
|
|
108
108
|
|
|
109
|
-
| Component
|
|
110
|
-
|
|
111
|
-
| AnimatedTooltip | Avatar row with mouse-following tooltips
|
|
112
|
-
| Compare
|
|
113
|
-
| Dock
|
|
114
|
-
| LogoCloud
|
|
115
|
-
| Timeline
|
|
109
|
+
| Component | Description | Demo |
|
|
110
|
+
| --------------- | --------------------------------------------- | ------------------------------------------------------- |
|
|
111
|
+
| AnimatedTooltip | Avatar row with mouse-following tooltips | [Demo](https://fancy-ui.rama.app/demo/animated-tooltip) |
|
|
112
|
+
| Compare | Before/after image comparison slider | [Demo](https://fancy-ui.rama.app/demo/compare) |
|
|
113
|
+
| Dock | macOS-style dock with icon magnification | [Demo](https://fancy-ui.rama.app/demo/dock) |
|
|
114
|
+
| LogoCloud | Marquee, grid, and icon logo layouts | [Demo](https://fancy-ui.rama.app/demo/logo-cloud) |
|
|
115
|
+
| Timeline | Vertical timeline with scroll-driven progress | [Demo](https://fancy-ui.rama.app/demo/timeline) |
|
|
116
116
|
|
|
117
117
|
## Quick Start
|
|
118
118
|
|
|
@@ -125,7 +125,7 @@ npm install fancy-ui-svelte
|
|
|
125
125
|
Then import any component:
|
|
126
126
|
|
|
127
127
|
```ts
|
|
128
|
-
import { Marquee, BorderBeam, Confetti } from
|
|
128
|
+
import { Marquee, BorderBeam, Confetti } from "fancy-ui-svelte";
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
Add the Tailwind integration to your app's CSS (e.g. `src/app.css`):
|
|
@@ -146,9 +146,11 @@ This single import tells Tailwind v4 to scan the library's components so all uti
|
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
The path is relative to your CSS file location.
|
|
149
|
+
|
|
149
150
|
</details>
|
|
150
151
|
|
|
151
152
|
**Or browse and copy a component:**
|
|
153
|
+
|
|
152
154
|
1. Find the component you need in the [live demo](https://fancy-ui.rama.app)
|
|
153
155
|
2. Copy the source from `src/lib/fancy-ui/[component-name]/`
|
|
154
156
|
3. Paste into your project
|
|
@@ -162,6 +164,25 @@ pnpm install
|
|
|
162
164
|
pnpm dev
|
|
163
165
|
```
|
|
164
166
|
|
|
167
|
+
## Using with AI agents (Claude Code, Cursor, Copilot)
|
|
168
|
+
|
|
169
|
+
The docs site serves LLM-friendly documentation following the [llms.txt](https://llmstxt.org) convention:
|
|
170
|
+
|
|
171
|
+
- [fancy-ui.rama.app/llms.txt](https://fancy-ui.rama.app/llms.txt) — setup guide, usage rules, and component index
|
|
172
|
+
- [fancy-ui.rama.app/llms-full.txt](https://fancy-ui.rama.app/llms-full.txt) — full reference with every component's props
|
|
173
|
+
|
|
174
|
+
To make your coding agent use fancy-ui correctly, add this to your project's `CLAUDE.md`, `AGENTS.md`, or `.cursorrules`:
|
|
175
|
+
|
|
176
|
+
```markdown
|
|
177
|
+
## UI components
|
|
178
|
+
|
|
179
|
+
Use fancy-ui-svelte (Svelte 5 + Tailwind CSS v4) for animated UI components.
|
|
180
|
+
Full component and props reference: https://fancy-ui.rama.app/llms-full.txt
|
|
181
|
+
Key rules: overlay/effect components (BorderBeam, GlowBorder, backgrounds, ...)
|
|
182
|
+
need a parent with `relative overflow-hidden`; cursor effects (FluidCursor,
|
|
183
|
+
SmoothCursor) mount once in the root +layout.svelte; Svelte 5 syntax only.
|
|
184
|
+
```
|
|
185
|
+
|
|
165
186
|
## Development
|
|
166
187
|
|
|
167
188
|
```bash
|
|
@@ -170,8 +191,11 @@ pnpm check # Run Svelte type checker
|
|
|
170
191
|
pnpm test # Run tests
|
|
171
192
|
pnpm test:watch # Run tests in watch mode
|
|
172
193
|
pnpm build # Production build
|
|
194
|
+
pnpm storybook # Component workshop on :6006
|
|
173
195
|
```
|
|
174
196
|
|
|
197
|
+
Component stories live in `src/stories/` — see the [Storybook section in CONTRIBUTING.md](./CONTRIBUTING.md#storybook).
|
|
198
|
+
|
|
175
199
|
## Project Structure
|
|
176
200
|
|
|
177
201
|
```
|
|
@@ -195,7 +219,7 @@ src/
|
|
|
195
219
|
|
|
196
220
|
## Contributing
|
|
197
221
|
|
|
198
|
-
Contributions are welcome!
|
|
222
|
+
Contributions are welcome! 56 components and counting — PRs for new components, bug fixes, and improvements are appreciated.
|
|
199
223
|
|
|
200
224
|
### Adding a new component
|
|
201
225
|
|
|
@@ -207,15 +231,15 @@ Contributions are welcome! 52 components and counting — PRs for new components
|
|
|
207
231
|
|
|
208
232
|
## Tech Stack
|
|
209
233
|
|
|
210
|
-
| Technology
|
|
211
|
-
|
|
212
|
-
| [Svelte](https://svelte.dev)
|
|
213
|
-
| [SvelteKit](https://svelte.dev/docs/kit)
|
|
214
|
-
| [Tailwind CSS](https://tailwindcss.com)
|
|
215
|
-
| [TypeScript](https://www.typescriptlang.org) | 5
|
|
216
|
-
| [Vitest](https://vitest.dev)
|
|
217
|
-
| [bits-ui](https://bits-ui.com)
|
|
218
|
-
| [GSAP](https://gsap.com)
|
|
234
|
+
| Technology | Version | Purpose |
|
|
235
|
+
| -------------------------------------------- | ------- | ------------------- |
|
|
236
|
+
| [Svelte](https://svelte.dev) | 5 | UI framework |
|
|
237
|
+
| [SvelteKit](https://svelte.dev/docs/kit) | 2 | App framework |
|
|
238
|
+
| [Tailwind CSS](https://tailwindcss.com) | 4 | Styling |
|
|
239
|
+
| [TypeScript](https://www.typescriptlang.org) | 5 | Type safety |
|
|
240
|
+
| [Vitest](https://vitest.dev) | 4 | Testing |
|
|
241
|
+
| [bits-ui](https://bits-ui.com) | 2 | Headless primitives |
|
|
242
|
+
| [GSAP](https://gsap.com) | 3 | Advanced animations |
|
|
219
243
|
|
|
220
244
|
## Credits
|
|
221
245
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { default as Dock } from "./Dock.svelte";
|
|
2
2
|
export { default as DockIcon } from "./DockIcon.svelte";
|
|
3
3
|
export { default as DockSeparator } from "./DockSeparator.svelte";
|
|
4
|
-
export
|
|
4
|
+
export type { DataOrientation, Direction, DockContext } from "./types.js";
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Props for EditorialEngine
|
|
4
|
+
*
|
|
5
|
+
* Live magazine layout: multi-column text flow with cursor handoff,
|
|
6
|
+
* auto-fitted headline, drop cap, pullquotes, and body text reflowing in
|
|
7
|
+
* real time around draggable animated orbs. All line breaking is computed
|
|
8
|
+
* by @chenglou/pretext — the render loop performs zero DOM reads.
|
|
9
|
+
*
|
|
10
|
+
* Inspired by the pretext "editorial engine" demo
|
|
11
|
+
* (https://chenglou.me/pretext/editorial-engine/).
|
|
12
|
+
*/
|
|
13
|
+
export interface EditorialEngineProps {
|
|
14
|
+
/** Headline, auto-sized to the widest font that breaks no word */
|
|
15
|
+
headline?: string;
|
|
16
|
+
/** Body copy (plain text, paragraphs separated by blank lines) */
|
|
17
|
+
body?: string;
|
|
18
|
+
/** Up to two pullquotes embedded in the columns */
|
|
19
|
+
pullquotes?: string[];
|
|
20
|
+
/** Font family stack used for all text */
|
|
21
|
+
fontFamily?: string;
|
|
22
|
+
/** Additional CSS classes for the stage */
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
import { cn } from "../../utils.js";
|
|
29
|
+
import { createEditorialEngine } from "./engine.js";
|
|
30
|
+
|
|
31
|
+
const DEFAULT_HEADLINE = "TEXT THAT FLOWS AROUND ANYTHING";
|
|
32
|
+
|
|
33
|
+
const DEFAULT_BODY = `Print designers have flowed text around images, illustrations, and odd-shaped figures for more than a century. The web never caught up. CSS gives us float, and more recently shape-outside, but both only push text away from one side of one static box. Flowing text through multiple columns, around several moving obstacles at once, filling the gaps on both sides — the browser has no answer for that.
|
|
34
|
+
|
|
35
|
+
The reason is not a lack of imagination. It is the cost of asking. Every question about text — how wide is this word, where does this line break, how tall is this paragraph — must go through the browser's layout engine, and the layout engine answers by reflowing the document. Ask too often and the interface stutters. So we learned not to ask, and our text stayed in rigid rectangles.
|
|
36
|
+
|
|
37
|
+
This page asks hundreds of times per frame. Every line of text you are reading was positioned by arithmetic, not by the browser. The pretext library measured every word once, up front, against a canvas font engine. After that, layout is pure math: given a width, it returns line breaks, line widths, and a cursor pointing at exactly where the text stopped.
|
|
38
|
+
|
|
39
|
+
That cursor is what makes columns work. The first column consumes text until it runs out of vertical space, then hands its cursor to the second column, which continues mid-sentence without duplication or gaps. Newspapers have done this on paper forever; on the web it has always required hidden elements and fragile content splitting.
|
|
40
|
+
|
|
41
|
+
The orbs drifting across the page are obstacles, not decoration. Each frame, the engine intersects every text line's vertical band with every orb, subtracts the blocked intervals from the column width, and fills every remaining slot — text flows on both sides of an orb at once, something shape-outside cannot express at all.
|
|
42
|
+
|
|
43
|
+
Drag an orb and the paragraphs part around it in real time. Click one and it pauses. Resize the window and the headline re-fits itself, searching for the largest font size that breaks no word across lines — a binary search that costs microseconds because each probe is arithmetic, not reflow.
|
|
44
|
+
|
|
45
|
+
The drop cap at the top of this article is another obstacle: a rectangle carved out of the first column's first three lines. The pullquotes are the same trick at a larger scale. Everything you see is one mechanism — carve the blocked intervals out of each line's band, lay out text in whatever space remains.
|
|
46
|
+
|
|
47
|
+
When the engine finishes a frame, it writes positions and text content to a pool of absolutely positioned elements, and nothing else. The browser never computes layout for this page's body text, because every coordinate is already known. A full reflow of every line around five moving obstacles typically costs a fraction of a millisecond.
|
|
48
|
+
|
|
49
|
+
Cheap text measurement changes what interfaces can be. Virtualized lists can know every item's exact height before rendering it. Chat bubbles can shrink-wrap to their tightest width. Labels can dodge chart elements as you zoom. Documents can flow around widgets that other people are dragging in real time.
|
|
50
|
+
|
|
51
|
+
None of this needs a new browser API or a standards process. It needs cached font metrics, a line breaker, and the willingness to stop asking the DOM questions it answers slowly. The text on this page stopped asking. And so it flows.`;
|
|
52
|
+
|
|
53
|
+
const DEFAULT_PULLQUOTES = [
|
|
54
|
+
"“Text flows on both sides of a moving obstacle at once — something shape-outside cannot express at all.”",
|
|
55
|
+
"“Every coordinate is already known. The browser never computes layout for this text.”",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const DEFAULT_FONT_FAMILY =
|
|
59
|
+
'"Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino, serif';
|
|
60
|
+
|
|
61
|
+
let {
|
|
62
|
+
headline = DEFAULT_HEADLINE,
|
|
63
|
+
body = DEFAULT_BODY,
|
|
64
|
+
pullquotes = DEFAULT_PULLQUOTES,
|
|
65
|
+
fontFamily = DEFAULT_FONT_FAMILY,
|
|
66
|
+
class: className,
|
|
67
|
+
}: EditorialEngineProps = $props();
|
|
68
|
+
|
|
69
|
+
let stageRef: HTMLDivElement;
|
|
70
|
+
let ready = $state(false);
|
|
71
|
+
|
|
72
|
+
// Re-created whenever the text props change, so the prepared measurements
|
|
73
|
+
// always match the rendered content. Effects never run during SSR.
|
|
74
|
+
$effect(() => {
|
|
75
|
+
const options = { headline, body, pullquotes, fontFamily };
|
|
76
|
+
let destroy: (() => void) | null = null;
|
|
77
|
+
let cancelled = false;
|
|
78
|
+
|
|
79
|
+
// pretext measures against the canvas font engine — fonts must be
|
|
80
|
+
// loaded first or widths come from the fallback font.
|
|
81
|
+
document.fonts.ready.then(() => {
|
|
82
|
+
if (cancelled) return;
|
|
83
|
+
destroy = createEditorialEngine(stageRef, options);
|
|
84
|
+
ready = true;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
cancelled = true;
|
|
89
|
+
destroy?.();
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<div
|
|
95
|
+
bind:this={stageRef}
|
|
96
|
+
class={cn("ee-stage", className)}
|
|
97
|
+
style:font-family={fontFamily}
|
|
98
|
+
class:ready
|
|
99
|
+
>
|
|
100
|
+
{#if !ready}
|
|
101
|
+
<!-- SSR / pre-engine fallback: keeps the content in the HTML -->
|
|
102
|
+
<div class="ee-fallback" aria-hidden="false">
|
|
103
|
+
<h2>{headline}</h2>
|
|
104
|
+
<p>{body}</p>
|
|
105
|
+
</div>
|
|
106
|
+
{/if}
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<style>
|
|
110
|
+
.ee-stage {
|
|
111
|
+
position: relative;
|
|
112
|
+
width: 100%;
|
|
113
|
+
height: 100%;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
background: radial-gradient(ellipse at 50% 40%, #0f0f14 0%, #0a0a0c 100%);
|
|
116
|
+
color: #e8e4dc;
|
|
117
|
+
touch-action: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.ee-fallback {
|
|
121
|
+
position: absolute;
|
|
122
|
+
inset: 0;
|
|
123
|
+
padding: 48px;
|
|
124
|
+
overflow: hidden;
|
|
125
|
+
opacity: 0.4;
|
|
126
|
+
font-size: 14px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.ee-stage :global(.ee-line) {
|
|
130
|
+
position: absolute;
|
|
131
|
+
white-space: pre;
|
|
132
|
+
z-index: 1;
|
|
133
|
+
color: #e8e4dc;
|
|
134
|
+
user-select: text;
|
|
135
|
+
-webkit-user-select: text;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.ee-stage :global(.ee-headline-line) {
|
|
139
|
+
position: absolute;
|
|
140
|
+
white-space: pre;
|
|
141
|
+
font-weight: 700;
|
|
142
|
+
color: #fff;
|
|
143
|
+
letter-spacing: -0.5px;
|
|
144
|
+
z-index: 2;
|
|
145
|
+
user-select: text;
|
|
146
|
+
-webkit-user-select: text;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.ee-stage :global(.ee-drop-cap) {
|
|
150
|
+
position: absolute;
|
|
151
|
+
pointer-events: none;
|
|
152
|
+
z-index: 2;
|
|
153
|
+
font-weight: 700;
|
|
154
|
+
color: #c4a35a;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.ee-stage :global(.ee-orb) {
|
|
158
|
+
position: absolute;
|
|
159
|
+
border-radius: 50%;
|
|
160
|
+
pointer-events: none;
|
|
161
|
+
z-index: 10;
|
|
162
|
+
will-change: transform;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.ee-stage :global(.ee-pullquote-box) {
|
|
166
|
+
position: absolute;
|
|
167
|
+
pointer-events: none;
|
|
168
|
+
z-index: 3;
|
|
169
|
+
border-left: 3px solid #6b5a3d;
|
|
170
|
+
padding-left: 14px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.ee-stage :global(.ee-pullquote-line) {
|
|
174
|
+
position: absolute;
|
|
175
|
+
white-space: pre;
|
|
176
|
+
font-style: italic;
|
|
177
|
+
color: #b8a070;
|
|
178
|
+
user-select: text;
|
|
179
|
+
-webkit-user-select: text;
|
|
180
|
+
}
|
|
181
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props for EditorialEngine
|
|
3
|
+
*
|
|
4
|
+
* Live magazine layout: multi-column text flow with cursor handoff,
|
|
5
|
+
* auto-fitted headline, drop cap, pullquotes, and body text reflowing in
|
|
6
|
+
* real time around draggable animated orbs. All line breaking is computed
|
|
7
|
+
* by @chenglou/pretext — the render loop performs zero DOM reads.
|
|
8
|
+
*
|
|
9
|
+
* Inspired by the pretext "editorial engine" demo
|
|
10
|
+
* (https://chenglou.me/pretext/editorial-engine/).
|
|
11
|
+
*/
|
|
12
|
+
export interface EditorialEngineProps {
|
|
13
|
+
/** Headline, auto-sized to the widest font that breaks no word */
|
|
14
|
+
headline?: string;
|
|
15
|
+
/** Body copy (plain text, paragraphs separated by blank lines) */
|
|
16
|
+
body?: string;
|
|
17
|
+
/** Up to two pullquotes embedded in the columns */
|
|
18
|
+
pullquotes?: string[];
|
|
19
|
+
/** Font family stack used for all text */
|
|
20
|
+
fontFamily?: string;
|
|
21
|
+
/** Additional CSS classes for the stage */
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
declare const EditorialEngine: import("svelte").Component<EditorialEngineProps, {}, "">;
|
|
25
|
+
type EditorialEngine = ReturnType<typeof EditorialEngine>;
|
|
26
|
+
export default EditorialEngine;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# EditorialEngine
|
|
2
|
+
|
|
3
|
+
Live magazine layout powered by [@chenglou/pretext](https://github.com/chenglou/pretext):
|
|
4
|
+
multi-column text flow with cursor handoff, auto-fitted headline, drop cap,
|
|
5
|
+
pullquotes, and body text reflowing in real time around draggable animated
|
|
6
|
+
orbs. The render loop performs **zero DOM reads** — every line position is
|
|
7
|
+
computed arithmetically from cached font metrics, and the only DOM operations
|
|
8
|
+
are writes (`left`, `top`, `textContent`).
|
|
9
|
+
|
|
10
|
+
Inspired by the pretext
|
|
11
|
+
[editorial engine demo](https://chenglou.me/pretext/editorial-engine/).
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
- `prepareWithSegments(body, font)` measures the text once against the canvas
|
|
16
|
+
font engine.
|
|
17
|
+
- Each frame, every text line's vertical band is intersected with each orb;
|
|
18
|
+
blocked horizontal intervals are subtracted from the column width and
|
|
19
|
+
`layoutNextLine` fills every remaining slot — text flows on **both sides**
|
|
20
|
+
of an obstacle, which CSS `shape-outside` cannot do.
|
|
21
|
+
- Columns hand a `LayoutCursor` to one another, so text continues
|
|
22
|
+
mid-sentence across column boundaries like print.
|
|
23
|
+
- The headline binary-searches the largest font size that breaks no word
|
|
24
|
+
(`walkLineRanges` to detect intra-word breaks).
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```svelte
|
|
29
|
+
<script lang="ts">
|
|
30
|
+
import { EditorialEngine } from "$lib/fancy-ui/editorial-engine";
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="h-[85vh]">
|
|
34
|
+
<EditorialEngine />
|
|
35
|
+
</div>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The stage fills its parent — give the parent an explicit height.
|
|
39
|
+
|
|
40
|
+
## Props
|
|
41
|
+
|
|
42
|
+
| Prop | Type | Default | Description |
|
|
43
|
+
| ------------ | ---------- | -------------------- | ------------------------------------------------- |
|
|
44
|
+
| `headline` | `string` | built-in copy | Auto-sized headline (largest size, no word break) |
|
|
45
|
+
| `body` | `string` | built-in copy | Body text, paragraphs separated by blank lines |
|
|
46
|
+
| `pullquotes` | `string[]` | built-in quotes | Up to two pullquotes embedded in the columns |
|
|
47
|
+
| `fontFamily` | `string` | Palatino serif stack | Font stack for all text |
|
|
48
|
+
| `class` | `string` | — | Additional CSS classes for the stage |
|
|
49
|
+
|
|
50
|
+
## Interactions
|
|
51
|
+
|
|
52
|
+
- **Drag** an orb — text reparts around it every frame.
|
|
53
|
+
- **Click** an orb — pauses/resumes its drift.
|
|
54
|
+
- **Resize** — headline re-fits, column count adapts (1/2/3), text reflows.
|
|
55
|
+
- Text stays selectable; rendering pauses while a selection is active.
|
|
56
|
+
- `prefers-reduced-motion` starts all orbs paused (still draggable).
|