loopwind 0.22.0 → 0.23.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/dist/lib/render-core.d.ts +63 -0
- package/dist/lib/render-core.d.ts.map +1 -0
- package/dist/lib/render-core.js +65 -0
- package/dist/lib/render-core.js.map +1 -0
- package/dist/lib/renderer.d.ts.map +1 -1
- package/dist/lib/renderer.js +10 -7
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/resvg-init.d.ts +15 -0
- package/dist/lib/resvg-init.d.ts.map +1 -0
- package/dist/lib/resvg-init.js +55 -0
- package/dist/lib/resvg-init.js.map +1 -0
- package/dist/lib/tailwind/colors.d.ts +8 -0
- package/dist/lib/tailwind/colors.d.ts.map +1 -0
- package/dist/lib/tailwind/colors.js +102 -0
- package/dist/lib/tailwind/colors.js.map +1 -0
- package/dist/lib/tailwind/index.d.ts +10 -0
- package/dist/lib/tailwind/index.d.ts.map +1 -0
- package/dist/lib/tailwind/index.js +9 -0
- package/dist/lib/tailwind/index.js.map +1 -0
- package/dist/lib/tailwind/resolvers.d.ts +28 -0
- package/dist/lib/tailwind/resolvers.d.ts.map +1 -0
- package/dist/lib/tailwind/resolvers.js +94 -0
- package/dist/lib/tailwind/resolvers.js.map +1 -0
- package/dist/lib/tailwind/types.d.ts +29 -0
- package/dist/lib/tailwind/types.d.ts.map +1 -0
- package/dist/lib/tailwind/types.js +8 -0
- package/dist/lib/tailwind/types.js.map +1 -0
- package/dist/lib/tailwind-config-loader.d.ts +8 -45
- package/dist/lib/tailwind-config-loader.d.ts.map +1 -1
- package/dist/lib/tailwind-config-loader.js +6 -429
- package/dist/lib/tailwind-config-loader.js.map +1 -1
- package/dist/lib/tailwind.d.ts +1 -1
- package/dist/lib/tailwind.d.ts.map +1 -1
- package/dist/lib/tailwind.js +1 -1
- package/dist/lib/tailwind.js.map +1 -1
- package/dist/lib/video-renderer.d.ts.map +1 -1
- package/dist/lib/video-renderer.js +6 -5
- package/dist/lib/video-renderer.js.map +1 -1
- package/dist/sdk/edge/index.d.ts +91 -0
- package/dist/sdk/edge/index.d.ts.map +1 -0
- package/dist/sdk/edge/index.js +187 -0
- package/dist/sdk/edge/index.js.map +1 -0
- package/dist/sdk/workers/index.d.ts +135 -0
- package/dist/sdk/workers/index.d.ts.map +1 -0
- package/dist/sdk/workers/index.js +271 -0
- package/dist/sdk/workers/index.js.map +1 -0
- package/dist/sdk/workers/tailwind-config.d.ts +48 -0
- package/dist/sdk/workers/tailwind-config.d.ts.map +1 -0
- package/dist/sdk/workers/tailwind-config.js +187 -0
- package/dist/sdk/workers/tailwind-config.js.map +1 -0
- package/dist/sdk/workers/tailwind.d.ts +9 -0
- package/dist/sdk/workers/tailwind.d.ts.map +1 -0
- package/dist/sdk/workers/tailwind.js +8 -0
- package/dist/sdk/workers/tailwind.js.map +1 -0
- package/package.json +6 -2
- package/test-cloudflare-worker/README.md +64 -0
- package/test-cloudflare-worker/dist/README.md +1 -0
- package/test-cloudflare-worker/dist/index.js +23743 -0
- package/test-cloudflare-worker/dist/index.js.map +8 -0
- package/test-cloudflare-worker/package-lock.json +1773 -0
- package/test-cloudflare-worker/package.json +25 -0
- package/test-cloudflare-worker/test-sdk.mjs +75 -0
- package/test-cloudflare-worker/wrangler.toml +14 -0
- package/test-video-720p.mjs +96 -0
- package/test-video-breakdown.mjs +98 -0
- package/test-video-perf-1080.mjs +67 -0
- package/test-video-perf.mjs +56 -0
- package/test-worker-1080p.mjs +103 -0
- package/test-worker-viability.mjs +140 -0
- package/website/astro.config.mjs +4 -9
- package/website/dist/_astro/PlaygroundEditor.DzFavsm8.js +26 -0
- package/website/dist/_astro/VideoPreviewClient.BrajhYmh.js +1 -0
- package/website/dist/_astro/agents.CZXv4DCM.css +1 -0
- package/website/dist/_astro/client.BHSq4mdQ.js +33 -0
- package/website/dist/_astro/index.CTbGshLK.js +9 -0
- package/website/dist/_astro/jsx-runtime.BjG_zV1W.js +9 -0
- package/website/dist/_routes.json +1 -0
- package/website/dist/_worker.js/_@astrojs-ssr-adapter.mjs +4 -4
- package/website/dist/_worker.js/_astro-internal_middleware.mjs +2 -2
- package/website/dist/_worker.js/chunks/Logo_Cud5QvBJ.mjs +22 -0
- package/website/dist/_worker.js/chunks/_@astro-renderers_-YVK7NHa.mjs +15015 -0
- package/website/dist/_worker.js/chunks/astro/{server_Y5_QHO8v.mjs → server_CsUrSZgd.mjs} +113 -2
- package/website/dist/_worker.js/chunks/{astro-designed-error-pages_BNTLO-TA.mjs → astro-designed-error-pages_1ELXm5Tt.mjs} +1 -1
- package/website/dist/_worker.js/chunks/{index_C1UTDwYg.mjs → index_BDWR1Q-q.mjs} +2 -2
- package/website/dist/_worker.js/chunks/{noop-middleware_DlWGj5t5.mjs → noop-middleware_B8fH5jha.mjs} +1 -1
- package/website/dist/_worker.js/index.js +38 -30
- package/website/dist/_worker.js/manifest_Bk6136-u.mjs +98 -0
- package/website/dist/_worker.js/pages/_image.astro.mjs +1 -1
- package/website/dist/_worker.js/pages/api/playground/render.astro.mjs +25562 -0
- package/website/dist/_worker.js/pages/api/playground/templates.astro.mjs +92 -0
- package/website/dist/_worker.js/pages/api/raw-markdown/_---path_.astro.mjs +1 -1
- package/website/dist/_worker.js/pages/playground/_example_.astro.mjs +95 -0
- package/website/dist/_worker.js/pages/playground.astro.mjs +1 -0
- package/website/dist/_worker.js/renderers.mjs +1 -56
- package/website/dist/agents/index.html +4 -2
- package/website/dist/animation/index.html +629 -3
- package/website/dist/config/index.html +4 -2
- package/website/dist/fonts/index.html +4 -2
- package/website/dist/getting-started/index.html +4 -2
- package/website/dist/helpers/index.html +196 -10
- package/website/dist/images/index.html +4 -2
- package/website/dist/index.html +4 -3
- package/website/dist/llm.txt +870 -20
- package/website/dist/playground/index.html +6 -0
- package/website/dist/preview/index.html +4 -2
- package/website/dist/sdk/index.html +639 -127
- package/website/dist/sitemap.xml +12 -12
- package/website/dist/styling/index.html +4 -2
- package/website/dist/templates/index.html +4 -2
- package/website/dist/video/index.html +47 -12
- package/website/package-lock.json +11 -1
- package/website/package.json +3 -1
- package/website/wrangler.toml +9 -0
- package/_dsgn/templates/dashed-stroke-test/template.tsx +0 -73
- package/_dsgn/templates/path-follow-test/template.tsx +0 -176
- package/_dsgn/templates/path-simple-test/template.tsx +0 -98
- package/_dsgn/templates/stroke-dash-test/meta.json +0 -12
- package/_dsgn/templates/stroke-dash-test/template.tsx +0 -53
- package/website/dist/_astro/agents.Yx-L_igG.css +0 -1
- package/website/dist/_worker.js/manifest_CT_D-YDe.mjs +0 -98
package/website/dist/llm.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# loopwind - Complete Documentation for LLMs
|
|
2
2
|
|
|
3
3
|
This is a comprehensive, single-file documentation for loopwind, optimized for LLM consumption.
|
|
4
|
-
Generated: 2025-
|
|
4
|
+
Generated: 2025-12-15T21:23:12.229Z
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -750,8 +750,8 @@ Create animated videos programmatically using React components. Perfect for auto
|
|
|
750
750
|
# Render a video template with inline props
|
|
751
751
|
loopwind render video-intro '{"title":"Welcome!"}' --out intro.mp4
|
|
752
752
|
|
|
753
|
-
#
|
|
754
|
-
loopwind render video-intro '{"title":"Welcome!"}' --
|
|
753
|
+
# Faster encoding with FFmpeg (requires FFmpeg installed)
|
|
754
|
+
loopwind render video-intro '{"title":"Welcome!"}' --ffmpeg
|
|
755
755
|
|
|
756
756
|
# Or use a props file
|
|
757
757
|
loopwind render video-intro props.json --out intro.mp4
|
|
@@ -918,29 +918,52 @@ export const meta = {
|
|
|
918
918
|
|
|
919
919
|
## Encoding Options
|
|
920
920
|
|
|
921
|
-
|
|
921
|
+
loopwind supports two encoding backends:
|
|
922
|
+
|
|
923
|
+
### WASM Encoder (Default)
|
|
922
924
|
|
|
923
925
|
```bash
|
|
924
926
|
loopwind render video-intro '{"title":"Welcome"}'
|
|
925
927
|
```
|
|
926
928
|
|
|
927
|
-
|
|
929
|
+
Pure JavaScript/WASM H.264 encoder. **Serverless-compatible** - works on Vercel, Cloudflare Workers, AWS Lambda, and anywhere JavaScript runs.
|
|
928
930
|
|
|
929
|
-
###
|
|
931
|
+
### FFmpeg Encoder (Faster)
|
|
930
932
|
|
|
931
933
|
```bash
|
|
932
|
-
loopwind render video-intro '{"title":"Welcome"}' --
|
|
934
|
+
loopwind render video-intro '{"title":"Welcome"}' --ffmpeg
|
|
933
935
|
```
|
|
934
936
|
|
|
935
|
-
|
|
937
|
+
Uses FFmpeg for **2x faster encoding** and smaller file sizes. Requires FFmpeg installed on your system.
|
|
938
|
+
|
|
939
|
+
**Install FFmpeg:**
|
|
940
|
+
```bash
|
|
941
|
+
# macOS
|
|
942
|
+
brew install ffmpeg
|
|
943
|
+
|
|
944
|
+
# Ubuntu/Debian
|
|
945
|
+
sudo apt install ffmpeg
|
|
946
|
+
|
|
947
|
+
# Windows
|
|
948
|
+
winget install ffmpeg
|
|
949
|
+
```
|
|
936
950
|
|
|
937
|
-
###
|
|
951
|
+
### Quality Settings
|
|
938
952
|
|
|
939
953
|
```bash
|
|
940
|
-
|
|
954
|
+
# Higher quality (lower CRF = better quality, default: 23)
|
|
955
|
+
loopwind render video-intro '{"title":"Welcome"}' --crf 18
|
|
956
|
+
|
|
957
|
+
# Lower quality, smaller files
|
|
958
|
+
loopwind render video-intro '{"title":"Welcome"}' --crf 28
|
|
941
959
|
```
|
|
942
960
|
|
|
943
|
-
|
|
961
|
+
### When to Use Each
|
|
962
|
+
|
|
963
|
+
| Encoder | Use Case |
|
|
964
|
+
|---------|----------|
|
|
965
|
+
| **WASM (default)** | Serverless, CI/CD, no dependencies needed |
|
|
966
|
+
| **FFmpeg (`--ffmpeg`)** | Local development, faster iteration, smaller files |
|
|
944
967
|
|
|
945
968
|
## Performance
|
|
946
969
|
|
|
@@ -1551,6 +1574,122 @@ Add an easing class **before** the animation class to control the timing curve.
|
|
|
1551
1574
|
| `ease-out-quart` | Very strong fast start | Punchy entrances |
|
|
1552
1575
|
| `ease-in-out-quart` | Very strong both ends | Maximum drama |
|
|
1553
1576
|
|
|
1577
|
+
### Per-Animation-Type Easing
|
|
1578
|
+
|
|
1579
|
+
You can apply **different easing functions** to enter, exit, and loop animations on the same element using `enter-ease-*`, `exit-ease-*`, and `loop-ease-*` classes.
|
|
1580
|
+
|
|
1581
|
+
```tsx
|
|
1582
|
+
// Different easing for enter and exit
|
|
1583
|
+
<h1 style={tw('enter-ease-out-cubic enter-fade-in/0/500 exit-ease-in exit-fade-out/2500/500')}>
|
|
1584
|
+
Smooth entrance, sharp exit
|
|
1585
|
+
</h1>
|
|
1586
|
+
|
|
1587
|
+
// Loop with linear easing, enter with bounce
|
|
1588
|
+
<div style={tw('enter-ease-out enter-bounce-in/0/400 loop-ease-linear loop-fade/1000')}>
|
|
1589
|
+
Bouncy entrance, linear loop
|
|
1590
|
+
</div>
|
|
1591
|
+
|
|
1592
|
+
// Default easing still works (applies to all animations)
|
|
1593
|
+
<div style={tw('ease-in-out enter-fade-in/0/500 exit-fade-out/2500/500')}>
|
|
1594
|
+
Same easing for both
|
|
1595
|
+
</div>
|
|
1596
|
+
|
|
1597
|
+
// Mix default with specific overrides
|
|
1598
|
+
<div style={tw('ease-out enter-fade-in/0/500 exit-ease-in-cubic exit-fade-out/2500/500')}>
|
|
1599
|
+
Default ease-out for enter, cubic-in for exit
|
|
1600
|
+
</div>
|
|
1601
|
+
```
|
|
1602
|
+
|
|
1603
|
+
**How it works:**
|
|
1604
|
+
|
|
1605
|
+
1. **Default easing** (`ease-*`) applies to ALL animations if no specific override is set
|
|
1606
|
+
2. **Specific easing** (`enter-ease-*`, `exit-ease-*`, `loop-ease-*`) overrides the default for that animation type
|
|
1607
|
+
3. If both are present, specific easing takes priority for its animation type
|
|
1608
|
+
|
|
1609
|
+
**Available easing classes:**
|
|
1610
|
+
|
|
1611
|
+
| Default (all animations) | Enter only | Exit only | Loop only |
|
|
1612
|
+
|--------------------------|------------|-----------|-----------|
|
|
1613
|
+
| `ease-in` | `enter-ease-in` | `exit-ease-in` | `loop-ease-in` |
|
|
1614
|
+
| `ease-out` | `enter-ease-out` | `exit-ease-out` | `loop-ease-out` |
|
|
1615
|
+
| `ease-in-out` | `enter-ease-in-out` | `exit-ease-in-out` | `loop-ease-in-out` |
|
|
1616
|
+
| `ease-in-cubic` | `enter-ease-in-cubic` | `exit-ease-in-cubic` | `loop-ease-in-cubic` |
|
|
1617
|
+
| `ease-out-cubic` | `enter-ease-out-cubic` | `exit-ease-out-cubic` | `loop-ease-out-cubic` |
|
|
1618
|
+
| `ease-in-out-cubic` | `enter-ease-in-out-cubic` | `exit-ease-in-out-cubic` | `loop-ease-in-out-cubic` |
|
|
1619
|
+
| `ease-in-quart` | `enter-ease-in-quart` | `exit-ease-in-quart` | `loop-ease-in-quart` |
|
|
1620
|
+
| `ease-out-quart` | `enter-ease-out-quart` | `exit-ease-out-quart` | `loop-ease-out-quart` |
|
|
1621
|
+
| `ease-in-out-quart` | `enter-ease-in-out-quart` | `exit-ease-in-out-quart` | `loop-ease-in-out-quart` |
|
|
1622
|
+
| `linear` | `enter-ease-linear` | `exit-ease-linear` | `loop-ease-linear` |
|
|
1623
|
+
| `ease-spring` | `enter-ease-spring` | `exit-ease-spring` | `loop-ease-spring` |
|
|
1624
|
+
|
|
1625
|
+
### Spring Easing
|
|
1626
|
+
|
|
1627
|
+
Spring easing creates natural, physics-based bouncy animations. Use the built-in `ease-spring` easing or create custom springs with configurable parameters.
|
|
1628
|
+
|
|
1629
|
+
```tsx
|
|
1630
|
+
// Default spring easing
|
|
1631
|
+
<h1 style={tw('ease-spring enter-bounce-in/0/500')}>Bouncy spring!</h1>
|
|
1632
|
+
|
|
1633
|
+
// Per-animation-type spring
|
|
1634
|
+
<div style={tw('enter-ease-spring enter-fade-in/0/500 exit-ease-out exit-fade-out/2500/500')}>
|
|
1635
|
+
Spring entrance, smooth exit
|
|
1636
|
+
</div>
|
|
1637
|
+
|
|
1638
|
+
// Custom spring with parameters: ease-spring/mass/stiffness/damping
|
|
1639
|
+
<h1 style={tw('ease-spring/1/100/10 enter-scale-in/0/800')}>
|
|
1640
|
+
Custom spring (mass=1, stiffness=100, damping=10)
|
|
1641
|
+
</h1>
|
|
1642
|
+
|
|
1643
|
+
// More bouncy spring (lower damping)
|
|
1644
|
+
<div style={tw('ease-spring/1/170/8 enter-bounce-in-up/0/600')}>
|
|
1645
|
+
Extra bouncy!
|
|
1646
|
+
</div>
|
|
1647
|
+
|
|
1648
|
+
// Stiffer spring (higher stiffness, faster)
|
|
1649
|
+
<div style={tw('ease-spring/1/200/12 enter-fade-in-up/0/400')}>
|
|
1650
|
+
Snappy spring
|
|
1651
|
+
</div>
|
|
1652
|
+
|
|
1653
|
+
// Per-animation-type custom springs
|
|
1654
|
+
<div style={tw('enter-ease-spring/1/150/10 enter-fade-in/0/500 exit-ease-spring/1/100/15 exit-fade-out/2500/500')}>
|
|
1655
|
+
Different springs for enter and exit
|
|
1656
|
+
</div>
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
**Spring parameters:**
|
|
1660
|
+
|
|
1661
|
+
| Parameter | Description | Effect when increased | Default |
|
|
1662
|
+
|-----------|-------------|----------------------|---------|
|
|
1663
|
+
| **mass** | Mass of the spring | Slower, more inertia | 1 |
|
|
1664
|
+
| **stiffness** | Spring stiffness | Faster, snappier | 100 |
|
|
1665
|
+
| **damping** | Damping coefficient | Less bounce, smoother | 10 |
|
|
1666
|
+
|
|
1667
|
+
**Common spring presets:**
|
|
1668
|
+
|
|
1669
|
+
```tsx
|
|
1670
|
+
// Gentle bounce (default)
|
|
1671
|
+
ease-spring/1/100/10
|
|
1672
|
+
|
|
1673
|
+
// Extra bouncy
|
|
1674
|
+
ease-spring/1/170/8
|
|
1675
|
+
|
|
1676
|
+
// Snappy (no bounce)
|
|
1677
|
+
ease-spring/1/200/15
|
|
1678
|
+
|
|
1679
|
+
// Slow and bouncy
|
|
1680
|
+
ease-spring/2/100/8
|
|
1681
|
+
|
|
1682
|
+
// Fast and tight
|
|
1683
|
+
ease-spring/0.5/300/20
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
**How spring works:**
|
|
1687
|
+
|
|
1688
|
+
1. **Default `ease-spring`** - Uses a pre-calculated spring curve optimized for most use cases
|
|
1689
|
+
2. **Custom `ease-spring/mass/stiffness/damping`** - Generates a physics-based spring curve using the [damped harmonic oscillator](https://www.kvin.me/css-springs) formula
|
|
1690
|
+
3. The spring automatically calculates its ideal duration to reach the final state
|
|
1691
|
+
4. Works with all animation types: `ease-spring`, `enter-ease-spring`, `exit-ease-spring`, `loop-ease-spring`
|
|
1692
|
+
|
|
1554
1693
|
## Combining Enter and Exit
|
|
1555
1694
|
|
|
1556
1695
|
You can use both enter and exit animations on the same element:
|
|
@@ -1793,7 +1932,7 @@ export default function CustomEasing({ tw, progress, title }) {
|
|
|
1793
1932
|
### When to Use Programmatic Animations
|
|
1794
1933
|
|
|
1795
1934
|
Use `progress`/`frame` instead of animation classes when you need:
|
|
1796
|
-
- **Custom easing functions** (elastic,
|
|
1935
|
+
- **Custom easing functions** (elastic, bounce with specific curves beyond built-in ease-spring)
|
|
1797
1936
|
- **Color cycling or gradients** based on time
|
|
1798
1937
|
- **Mathematical animations** (sine waves, spirals, etc.)
|
|
1799
1938
|
- **Complex multi-property animations** that need precise coordination
|
|
@@ -1801,6 +1940,498 @@ Use `progress`/`frame` instead of animation classes when you need:
|
|
|
1801
1940
|
|
|
1802
1941
|
For everything else, prefer animation classes - they're simpler and more maintainable.
|
|
1803
1942
|
|
|
1943
|
+
### Animating Along Paths
|
|
1944
|
+
|
|
1945
|
+
Animate elements along SVG paths with proper rotation using built-in **path helpers**:
|
|
1946
|
+
|
|
1947
|
+
```tsx
|
|
1948
|
+
export default function PathFollowing({ tw, progress, path }) {
|
|
1949
|
+
// Follow a quadratic Bezier curve - one line!
|
|
1950
|
+
const rocket = path.followQuadratic(
|
|
1951
|
+
{ x: 200, y: 400 }, // Start point
|
|
1952
|
+
{ x: 960, y: 150 }, // Control point
|
|
1953
|
+
{ x: 1720, y: 400 }, // End point
|
|
1954
|
+
progress
|
|
1955
|
+
);
|
|
1956
|
+
|
|
1957
|
+
return (
|
|
1958
|
+
<div style={{ display: 'flex', ...tw('relative w-full h-full bg-gray-900') }}>
|
|
1959
|
+
{/* Draw the path (optional) */}
|
|
1960
|
+
<svg width="1920" height="1080" style={{ position: 'absolute' }}>
|
|
1961
|
+
<path
|
|
1962
|
+
d="M 200 400 Q 960 150 1720 400"
|
|
1963
|
+
stroke="rgba(255,255,255,0.2)"
|
|
1964
|
+
strokeWidth={2}
|
|
1965
|
+
fill="none"
|
|
1966
|
+
/>
|
|
1967
|
+
</svg>
|
|
1968
|
+
|
|
1969
|
+
{/* Element following the path */}
|
|
1970
|
+
<div
|
|
1971
|
+
style={{
|
|
1972
|
+
position: "absolute",
|
|
1973
|
+
left: rocket.x,
|
|
1974
|
+
top: rocket.y,
|
|
1975
|
+
transform: `translate(-50%, -50%) rotate(${rocket.angle}deg)`,
|
|
1976
|
+
fontSize: '48px'
|
|
1977
|
+
}}
|
|
1978
|
+
>
|
|
1979
|
+
🚀
|
|
1980
|
+
</div>
|
|
1981
|
+
</div>
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
```
|
|
1985
|
+
|
|
1986
|
+
### Text Path Animations
|
|
1987
|
+
|
|
1988
|
+
Combine `textPath` helpers with animation classes to create animated text along curves:
|
|
1989
|
+
|
|
1990
|
+
**Rotating text around a circle:**
|
|
1991
|
+
```tsx
|
|
1992
|
+
export default function RotatingCircleText({ tw, textPath, progress }) {
|
|
1993
|
+
return (
|
|
1994
|
+
<div style={tw('relative w-full h-full bg-black')}>
|
|
1995
|
+
{/* Text rotates around circle using progress */}
|
|
1996
|
+
{textPath.onCircle(
|
|
1997
|
+
"SPINNING TEXT • AROUND • ",
|
|
1998
|
+
960, // center x
|
|
1999
|
+
540, // center y
|
|
2000
|
+
400, // radius
|
|
2001
|
+
progress, // rotation offset (0-1 animates full rotation)
|
|
2002
|
+
{
|
|
2003
|
+
fontSize: "3xl",
|
|
2004
|
+
fontWeight: "bold",
|
|
2005
|
+
color: "yellow-300"
|
|
2006
|
+
}
|
|
2007
|
+
)}
|
|
2008
|
+
</div>
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
```
|
|
2012
|
+
|
|
2013
|
+
**Animated text reveal along a path:**
|
|
2014
|
+
```tsx
|
|
2015
|
+
export default function PathTextReveal({ tw, textPath, progress }) {
|
|
2016
|
+
// Create custom path follower that animates position
|
|
2017
|
+
const pathFollower = (t) => {
|
|
2018
|
+
// Only show characters up to current progress
|
|
2019
|
+
const visibleProgress = progress * 1.5; // Extend range for smooth reveal
|
|
2020
|
+
const opacity = t < visibleProgress ? 1 : 0;
|
|
2021
|
+
|
|
2022
|
+
// Follow quadratic curve
|
|
2023
|
+
const pos = {
|
|
2024
|
+
x: (1 - t) * (1 - t) * 200 + 2 * (1 - t) * t * 960 + t * t * 1720,
|
|
2025
|
+
y: (1 - t) * (1 - t) * 400 + 2 * (1 - t) * t * 150 + t * t * 400,
|
|
2026
|
+
angle: 0
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
return { ...pos, opacity };
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
return (
|
|
2033
|
+
<div style={tw('relative w-full h-full bg-gray-900')}>
|
|
2034
|
+
{textPath.onPath(
|
|
2035
|
+
"REVEALING TEXT",
|
|
2036
|
+
pathFollower,
|
|
2037
|
+
{
|
|
2038
|
+
fontSize: "4xl",
|
|
2039
|
+
fontWeight: "bold",
|
|
2040
|
+
color: "blue-300"
|
|
2041
|
+
}
|
|
2042
|
+
).map((char, i) => (
|
|
2043
|
+
<div key={i} style={{ ...char.props.style, opacity: char.props.style.opacity || 1 }}>
|
|
2044
|
+
{char}
|
|
2045
|
+
</div>
|
|
2046
|
+
))}
|
|
2047
|
+
</div>
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
```
|
|
2051
|
+
|
|
2052
|
+
**Staggered character entrance:**
|
|
2053
|
+
```tsx
|
|
2054
|
+
export default function StaggeredCircleText({ tw, textPath }) {
|
|
2055
|
+
const text = "HELLO WORLD";
|
|
2056
|
+
|
|
2057
|
+
return (
|
|
2058
|
+
<div style={tw('relative w-full h-full bg-slate-900')}>
|
|
2059
|
+
{textPath.onCircle(
|
|
2060
|
+
text,
|
|
2061
|
+
960, 540, 400, 0,
|
|
2062
|
+
{ fontSize: "4xl", fontWeight: "bold", color: "white" }
|
|
2063
|
+
).map((char, i) => {
|
|
2064
|
+
// Stagger fade-in: each character starts 50ms later
|
|
2065
|
+
const staggerDelay = i * 50;
|
|
2066
|
+
return (
|
|
2067
|
+
<div
|
|
2068
|
+
key={i}
|
|
2069
|
+
style={{
|
|
2070
|
+
...char.props.style,
|
|
2071
|
+
...tw(`enter-fade-in/${staggerDelay}/300 enter-scale-100/${staggerDelay}/300`)
|
|
2072
|
+
}}
|
|
2073
|
+
>
|
|
2074
|
+
{char.props.children}
|
|
2075
|
+
</div>
|
|
2076
|
+
);
|
|
2077
|
+
})}
|
|
2078
|
+
</div>
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
```
|
|
2082
|
+
|
|
2083
|
+
**Text with bounce entrance along arc:**
|
|
2084
|
+
```tsx
|
|
2085
|
+
export default function BouncyArcText({ tw, textPath }) {
|
|
2086
|
+
return (
|
|
2087
|
+
<div style={tw('relative w-full h-full bg-gradient-to-br from-purple-600 to-blue-500')}>
|
|
2088
|
+
{/* Draw the arc path */}
|
|
2089
|
+
<svg width="1920" height="1080" style={{ position: 'absolute' }}>
|
|
2090
|
+
<path
|
|
2091
|
+
d="M 300 900 A 600 600 0 0 1 1620 900"
|
|
2092
|
+
stroke="rgba(255,255,255,0.2)"
|
|
2093
|
+
strokeWidth={2}
|
|
2094
|
+
fill="none"
|
|
2095
|
+
strokeDasharray="5 5"
|
|
2096
|
+
/>
|
|
2097
|
+
</svg>
|
|
2098
|
+
|
|
2099
|
+
{/* Text follows arc with staggered bounce */}
|
|
2100
|
+
{textPath.onArc(
|
|
2101
|
+
"BOUNCING ON ARC",
|
|
2102
|
+
960, // cx
|
|
2103
|
+
300, // cy
|
|
2104
|
+
600, // radius
|
|
2105
|
+
180, // start angle
|
|
2106
|
+
360, // end angle
|
|
2107
|
+
{ fontSize: "3xl", fontWeight: "bold", color: "white" }
|
|
2108
|
+
).map((char, i) => (
|
|
2109
|
+
<div
|
|
2110
|
+
key={i}
|
|
2111
|
+
style={{
|
|
2112
|
+
...char.props.style,
|
|
2113
|
+
...tw(`ease-out enter-bounce-in-up/${i * 80}/500`)
|
|
2114
|
+
}}
|
|
2115
|
+
>
|
|
2116
|
+
{char.props.children}
|
|
2117
|
+
</div>
|
|
2118
|
+
))}
|
|
2119
|
+
</div>
|
|
2120
|
+
);
|
|
2121
|
+
}
|
|
2122
|
+
```
|
|
2123
|
+
|
|
2124
|
+
**Loop animation with text on curve:**
|
|
2125
|
+
```tsx
|
|
2126
|
+
export default function LoopingCurveText({ tw, textPath, frame }) {
|
|
2127
|
+
// Calculate wave effect using frame
|
|
2128
|
+
const waveOffset = Math.sin(frame / 30 * Math.PI * 2) * 0.1;
|
|
2129
|
+
|
|
2130
|
+
return (
|
|
2131
|
+
<div style={tw('relative w-full h-full bg-black')}>
|
|
2132
|
+
{textPath.onQuadratic(
|
|
2133
|
+
"WAVY TEXT",
|
|
2134
|
+
{ x: 200, y: 400 },
|
|
2135
|
+
{ x: 960, y: 150 },
|
|
2136
|
+
{ x: 1720, y: 400 },
|
|
2137
|
+
{ fontSize: "4xl", fontWeight: "bold", color: "pink-300" }
|
|
2138
|
+
).map((char, i) => (
|
|
2139
|
+
<div
|
|
2140
|
+
key={i}
|
|
2141
|
+
style={{
|
|
2142
|
+
...char.props.style,
|
|
2143
|
+
transform: `${char.props.style.transform} translateY(${Math.sin((i + frame) / 5) * 10}px)`
|
|
2144
|
+
}}
|
|
2145
|
+
>
|
|
2146
|
+
{char.props.children}
|
|
2147
|
+
</div>
|
|
2148
|
+
))}
|
|
2149
|
+
</div>
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
```
|
|
2153
|
+
|
|
2154
|
+
**Tips for animating text paths:**
|
|
2155
|
+
1. **Use `progress` for smooth rotation** on circles and arcs
|
|
2156
|
+
2. **Map over returned characters** to apply individual animations
|
|
2157
|
+
3. **Combine with animation classes** like `enter-fade-in`, `enter-bounce-in`, etc.
|
|
2158
|
+
4. **Stagger character animations** by calculating delays: `i * delayMs`
|
|
2159
|
+
5. **Use `frame` for continuous effects** like waves or pulsing
|
|
2160
|
+
6. **Preserve the original transform** when adding animations: `transform: '${char.props.style.transform} ...'`
|
|
2161
|
+
|
|
2162
|
+
**Common path types:**
|
|
2163
|
+
|
|
2164
|
+
**Quadratic Bezier** (Q command):
|
|
2165
|
+
```tsx
|
|
2166
|
+
// Position: (1-t)²·P0 + 2(1-t)t·P1 + t²·P2
|
|
2167
|
+
function pointOnQuadraticBezier(p0, p1, p2, t) {
|
|
2168
|
+
const x = (1 - t) * (1 - t) * p0.x + 2 * (1 - t) * t * p1.x + t * t * p2.x;
|
|
2169
|
+
const y = (1 - t) * (1 - t) * p0.y + 2 * (1 - t) * t * p1.y + t * t * p2.y;
|
|
2170
|
+
return { x, y };
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// Tangent angle
|
|
2174
|
+
function angleOnQuadraticBezier(p0, p1, p2, t) {
|
|
2175
|
+
const dx = 2 * (1 - t) * (p1.x - p0.x) + 2 * t * (p2.x - p1.x);
|
|
2176
|
+
const dy = 2 * (1 - t) * (p1.y - p0.y) + 2 * t * (p2.y - p1.y);
|
|
2177
|
+
return Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2178
|
+
}
|
|
2179
|
+
```
|
|
2180
|
+
|
|
2181
|
+
**Cubic Bezier** (C command):
|
|
2182
|
+
```tsx
|
|
2183
|
+
// Position: (1-t)³·P0 + 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³·P3
|
|
2184
|
+
function pointOnCubicBezier(p0, p1, p2, p3, t) {
|
|
2185
|
+
const mt = 1 - t;
|
|
2186
|
+
const mt2 = mt * mt;
|
|
2187
|
+
const mt3 = mt2 * mt;
|
|
2188
|
+
const t2 = t * t;
|
|
2189
|
+
const t3 = t2 * t;
|
|
2190
|
+
const x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x;
|
|
2191
|
+
const y = mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y;
|
|
2192
|
+
return { x, y };
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// Tangent angle
|
|
2196
|
+
function angleOnCubicBezier(p0, p1, p2, p3, t) {
|
|
2197
|
+
const mt = 1 - t;
|
|
2198
|
+
const mt2 = mt * mt;
|
|
2199
|
+
const t2 = t * t;
|
|
2200
|
+
const dx = -3 * mt2 * p0.x + 3 * mt2 * p1.x - 6 * mt * t * p1.x - 3 * t2 * p2.x + 6 * mt * t * p2.x + 3 * t2 * p3.x;
|
|
2201
|
+
const dy = -3 * mt2 * p0.y + 3 * mt2 * p1.y - 6 * mt * t * p1.y - 3 * t2 * p2.y + 6 * mt * t * p2.y + 3 * t2 * p3.y;
|
|
2202
|
+
return Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2203
|
+
}
|
|
2204
|
+
```
|
|
2205
|
+
|
|
2206
|
+
**Circle**:
|
|
2207
|
+
```tsx
|
|
2208
|
+
function pointOnCircle(cx, cy, radius, angleRadians) {
|
|
2209
|
+
return {
|
|
2210
|
+
x: cx + radius * Math.cos(angleRadians),
|
|
2211
|
+
y: cy + radius * Math.sin(angleRadians)
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// Usage
|
|
2216
|
+
const angleRadians = progress * Math.PI * 2;
|
|
2217
|
+
const pos = pointOnCircle(300, 300, 100, angleRadians);
|
|
2218
|
+
const tangentAngle = (angleRadians * 180 / Math.PI) + 90; // Tangent is perpendicular
|
|
2219
|
+
```
|
|
2220
|
+
|
|
2221
|
+
**Tips:**
|
|
2222
|
+
- Use `progress` (0-1) for smooth animation
|
|
2223
|
+
- The `translate(-50%, -50%)` centers the element on the path
|
|
2224
|
+
- Combine rotation with the translate: `translate(-50%, -50%) rotate(${angle}deg)`
|
|
2225
|
+
- For text following a path, you can animate individual characters at different progress values
|
|
2226
|
+
|
|
2227
|
+
## SVG Stroke Animations
|
|
2228
|
+
|
|
2229
|
+
Animate SVG path strokes with the **stroke-dash** classes, perfect for drawing or erasing line art, icons, and illustrations.
|
|
2230
|
+
|
|
2231
|
+
### How It Works
|
|
2232
|
+
|
|
2233
|
+
SVG stroke animations use `strokeDasharray` and `strokeDashoffset` CSS properties to create drawing effects:
|
|
2234
|
+
|
|
2235
|
+
1. **Enter animations** - Draw the stroke from start to finish
|
|
2236
|
+
2. **Exit animations** - Erase the stroke from finish to start
|
|
2237
|
+
3. **Loop animations** - Continuously draw and erase
|
|
2238
|
+
|
|
2239
|
+
### Format
|
|
2240
|
+
|
|
2241
|
+
All stroke-dash animations require the **path length** in brackets:
|
|
2242
|
+
|
|
2243
|
+
```tsx
|
|
2244
|
+
enter-stroke-dash-[length]/start/duration
|
|
2245
|
+
exit-stroke-dash-[length]/start/duration
|
|
2246
|
+
loop-stroke-dash-[length]/duration
|
|
2247
|
+
```
|
|
2248
|
+
|
|
2249
|
+
### Basic Examples
|
|
2250
|
+
|
|
2251
|
+
```tsx
|
|
2252
|
+
export default function SVGAnimation({ tw }) {
|
|
2253
|
+
return (
|
|
2254
|
+
<svg width="400" height="200" viewBox="0 0 400 200">
|
|
2255
|
+
{/* Draw a curve over 1 second */}
|
|
2256
|
+
<path
|
|
2257
|
+
d="M10 150 Q 95 10 180 150"
|
|
2258
|
+
stroke="black"
|
|
2259
|
+
strokeWidth={4}
|
|
2260
|
+
fill="none"
|
|
2261
|
+
style={tw('enter-stroke-dash-[300]/0/1000')}
|
|
2262
|
+
/>
|
|
2263
|
+
</svg>
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
```
|
|
2267
|
+
|
|
2268
|
+
### Enter Animations (Drawing)
|
|
2269
|
+
|
|
2270
|
+
Draw strokes from 0% to 100%:
|
|
2271
|
+
|
|
2272
|
+
```tsx
|
|
2273
|
+
// Draw a 300px path over 1 second
|
|
2274
|
+
<path style={tw('enter-stroke-dash-[300]/0/1000')} />
|
|
2275
|
+
|
|
2276
|
+
// Draw with spring easing
|
|
2277
|
+
<path style={tw('ease-spring enter-stroke-dash-[500]/0/1500')} />
|
|
2278
|
+
|
|
2279
|
+
// Stagger multiple paths
|
|
2280
|
+
<path style={tw('enter-stroke-dash-[200]/0/600')} />
|
|
2281
|
+
<path style={tw('enter-stroke-dash-[200]/200/600')} />
|
|
2282
|
+
<path style={tw('enter-stroke-dash-[200]/400/600')} />
|
|
2283
|
+
```
|
|
2284
|
+
|
|
2285
|
+
### Exit Animations (Erasing)
|
|
2286
|
+
|
|
2287
|
+
Erase strokes from 100% to 0%:
|
|
2288
|
+
|
|
2289
|
+
```tsx
|
|
2290
|
+
// Erase starting at 2000ms, lasting 500ms
|
|
2291
|
+
<path style={tw('exit-stroke-dash-[300]/2000/500')} />
|
|
2292
|
+
|
|
2293
|
+
// Draw then erase the same path
|
|
2294
|
+
<path style={tw('enter-stroke-dash-[400]/0/800 exit-stroke-dash-[400]/2200/800')} />
|
|
2295
|
+
```
|
|
2296
|
+
|
|
2297
|
+
### Loop Animations
|
|
2298
|
+
|
|
2299
|
+
Continuously draw and erase:
|
|
2300
|
+
|
|
2301
|
+
```tsx
|
|
2302
|
+
// Loop every 2 seconds (draws in first half, erases in second half)
|
|
2303
|
+
<path style={tw('loop-stroke-dash-[300]/2000')} />
|
|
2304
|
+
|
|
2305
|
+
// Faster loop
|
|
2306
|
+
<path style={tw('loop-stroke-dash-[200]/1000')} />
|
|
2307
|
+
```
|
|
2308
|
+
|
|
2309
|
+
### Getting Path Length
|
|
2310
|
+
|
|
2311
|
+
To find the path length for your SVG:
|
|
2312
|
+
|
|
2313
|
+
```tsx
|
|
2314
|
+
// In browser console or component:
|
|
2315
|
+
const path = document.querySelector('path');
|
|
2316
|
+
const length = path.getTotalLength();
|
|
2317
|
+
console.log(length); // e.g., 347.89
|
|
2318
|
+
```
|
|
2319
|
+
|
|
2320
|
+
Then use that value:
|
|
2321
|
+
|
|
2322
|
+
```tsx
|
|
2323
|
+
<path style={tw('enter-stroke-dash-[347.89]/0/1000')} />
|
|
2324
|
+
```
|
|
2325
|
+
|
|
2326
|
+
### Complete Example
|
|
2327
|
+
|
|
2328
|
+
```tsx
|
|
2329
|
+
export default function DrawingEffect({ tw }) {
|
|
2330
|
+
return (
|
|
2331
|
+
<div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>
|
|
2332
|
+
<svg width="600" height="400" viewBox="0 0 600 400">
|
|
2333
|
+
{/* Checkmark icon drawn in sequence */}
|
|
2334
|
+
<path
|
|
2335
|
+
d="M100 200 L 200 300 L 400 100"
|
|
2336
|
+
stroke="#10b981"
|
|
2337
|
+
strokeWidth={8}
|
|
2338
|
+
fill="none"
|
|
2339
|
+
strokeLinecap="round"
|
|
2340
|
+
strokeLinejoin="round"
|
|
2341
|
+
style={tw('ease-out enter-stroke-dash-[600]/0/1200')}
|
|
2342
|
+
/>
|
|
2343
|
+
|
|
2344
|
+
{/* Circle drawn after checkmark */}
|
|
2345
|
+
<circle
|
|
2346
|
+
cx="250"
|
|
2347
|
+
cy="200"
|
|
2348
|
+
r="150"
|
|
2349
|
+
stroke="#10b981"
|
|
2350
|
+
strokeWidth={6}
|
|
2351
|
+
fill="none"
|
|
2352
|
+
style={tw('ease-out enter-stroke-dash-[942]/1000/1000')}
|
|
2353
|
+
/>
|
|
2354
|
+
</svg>
|
|
2355
|
+
</div>
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
```
|
|
2359
|
+
|
|
2360
|
+
### Combining with Other Animations
|
|
2361
|
+
|
|
2362
|
+
Stroke animations work alongside other animation classes:
|
|
2363
|
+
|
|
2364
|
+
```tsx
|
|
2365
|
+
// Fade in while drawing
|
|
2366
|
+
<path style={tw('enter-stroke-dash-[300]/0/1000 enter-fade-in/0/1000')} />
|
|
2367
|
+
|
|
2368
|
+
// Draw with pulsing color
|
|
2369
|
+
<svg>
|
|
2370
|
+
<path
|
|
2371
|
+
stroke="url(#gradient)"
|
|
2372
|
+
style={tw('enter-stroke-dash-[500]/0/1500')}
|
|
2373
|
+
/>
|
|
2374
|
+
<defs>
|
|
2375
|
+
<linearGradient id="gradient">
|
|
2376
|
+
<stop offset="0%" stopColor="#8b5cf6" />
|
|
2377
|
+
<stop offset="100%" stopColor="#ec4899" />
|
|
2378
|
+
</linearGradient>
|
|
2379
|
+
</defs>
|
|
2380
|
+
</svg>
|
|
2381
|
+
```
|
|
2382
|
+
|
|
2383
|
+
### Animated Dashed Strokes (Marching Ants)
|
|
2384
|
+
|
|
2385
|
+
For **marching ants** or **animated dashed patterns**, use `frame` or `progress` directly instead of animation classes:
|
|
2386
|
+
|
|
2387
|
+
```tsx
|
|
2388
|
+
export default function MarchingAnts({ tw, frame }) {
|
|
2389
|
+
// Calculate animated offset (loops every 30 frames)
|
|
2390
|
+
const dashOffset = -(frame % 30) * 2;
|
|
2391
|
+
|
|
2392
|
+
return (
|
|
2393
|
+
<div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>
|
|
2394
|
+
<svg width="600" height="400" viewBox="0 0 600 400">
|
|
2395
|
+
{/* Marching ants border */}
|
|
2396
|
+
<rect
|
|
2397
|
+
x="50"
|
|
2398
|
+
y="50"
|
|
2399
|
+
width="500"
|
|
2400
|
+
height="300"
|
|
2401
|
+
fill="none"
|
|
2402
|
+
stroke="#3b82f6"
|
|
2403
|
+
strokeWidth={3}
|
|
2404
|
+
strokeDasharray="10 5"
|
|
2405
|
+
strokeDashoffset={dashOffset}
|
|
2406
|
+
/>
|
|
2407
|
+
|
|
2408
|
+
{/* Animated circle with different speed */}
|
|
2409
|
+
<circle
|
|
2410
|
+
cx="300"
|
|
2411
|
+
cy="200"
|
|
2412
|
+
r="80"
|
|
2413
|
+
fill="none"
|
|
2414
|
+
stroke="#10b981"
|
|
2415
|
+
strokeWidth={4}
|
|
2416
|
+
strokeDasharray="15 8"
|
|
2417
|
+
strokeDashoffset={dashOffset * 1.5}
|
|
2418
|
+
/>
|
|
2419
|
+
</svg>
|
|
2420
|
+
</div>
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
```
|
|
2424
|
+
|
|
2425
|
+
**Tips:**
|
|
2426
|
+
- `strokeDasharray="10 5"` - 10px dash, 5px gap
|
|
2427
|
+
- `strokeDashoffset={dashOffset}` - animates the pattern position
|
|
2428
|
+
- Negative offset moves forward, positive moves backward
|
|
2429
|
+
- Different speeds: multiply by different values (e.g., `dashOffset * 2`)
|
|
2430
|
+
|
|
2431
|
+
This technique is different from `stroke-dash` classes:
|
|
2432
|
+
- **`stroke-dash` classes** - Draw/erase the stroke (reveal animation)
|
|
2433
|
+
- **Marching ants** - Move a dashed pattern along the stroke
|
|
2434
|
+
|
|
1804
2435
|
## Performance Tips
|
|
1805
2436
|
|
|
1806
2437
|
1. **Use Tailwind classes** when possible - they're optimized for the renderer
|
|
@@ -1986,27 +2617,246 @@ export default function BrandedTemplate({ tw, config, title }) {
|
|
|
1986
2617
|
|
|
1987
2618
|
This allows templates to adapt to user preferences and brand guidelines.
|
|
1988
2619
|
|
|
2620
|
+
## Text on Path
|
|
2621
|
+
|
|
2622
|
+
Render text along curves, circles, and custom paths with automatic character positioning and rotation.
|
|
2623
|
+
|
|
2624
|
+
### Usage
|
|
2625
|
+
|
|
2626
|
+
```tsx
|
|
2627
|
+
export default function CircleText({ tw, textPath, message }) {
|
|
2628
|
+
return (
|
|
2629
|
+
<div style={tw('relative w-full h-full bg-slate-900')}>
|
|
2630
|
+
{textPath.onCircle(
|
|
2631
|
+
message,
|
|
2632
|
+
960, // center x
|
|
2633
|
+
540, // center y
|
|
2634
|
+
400, // radius
|
|
2635
|
+
0, // rotation offset (0-1)
|
|
2636
|
+
{
|
|
2637
|
+
fontSize: "4xl",
|
|
2638
|
+
fontWeight: "bold",
|
|
2639
|
+
color: "white",
|
|
2640
|
+
letterSpacing: 0.05
|
|
2641
|
+
}
|
|
2642
|
+
)}
|
|
2643
|
+
</div>
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
```
|
|
2647
|
+
|
|
2648
|
+
### Available Functions
|
|
2649
|
+
|
|
2650
|
+
All `textPath` functions return an array of positioned character elements:
|
|
2651
|
+
|
|
2652
|
+
**`textPath.onCircle(text, cx, cy, radius, offset, options?)`**
|
|
2653
|
+
```tsx
|
|
2654
|
+
// Text around a circle
|
|
2655
|
+
textPath.onCircle("HELLO WORLD", 960, 540, 400, 0, {
|
|
2656
|
+
fontSize: "4xl",
|
|
2657
|
+
color: "white"
|
|
2658
|
+
})
|
|
2659
|
+
```
|
|
2660
|
+
|
|
2661
|
+
**`textPath.onPath(text, pathFollower, options?)`**
|
|
2662
|
+
```tsx
|
|
2663
|
+
// Text along any custom path
|
|
2664
|
+
textPath.onPath("CUSTOM PATH", (t) => ({
|
|
2665
|
+
x: 100 + t * 800,
|
|
2666
|
+
y: 200 + Math.sin(t * Math.PI) * 100,
|
|
2667
|
+
angle: Math.cos(t * Math.PI) * 20
|
|
2668
|
+
}), {
|
|
2669
|
+
fontSize: "2xl",
|
|
2670
|
+
fontWeight: "semibold"
|
|
2671
|
+
})
|
|
2672
|
+
```
|
|
2673
|
+
|
|
2674
|
+
**`textPath.onQuadratic(text, p0, p1, p2, options?)`**
|
|
2675
|
+
```tsx
|
|
2676
|
+
// Text along a quadratic Bezier curve
|
|
2677
|
+
textPath.onQuadratic(
|
|
2678
|
+
"CURVED TEXT",
|
|
2679
|
+
{ x: 200, y: 400 }, // start
|
|
2680
|
+
{ x: 960, y: 100 }, // control point
|
|
2681
|
+
{ x: 1720, y: 400 }, // end
|
|
2682
|
+
{ fontSize: "3xl", color: "blue-300" }
|
|
2683
|
+
)
|
|
2684
|
+
```
|
|
2685
|
+
|
|
2686
|
+
**`textPath.onCubic(text, p0, p1, p2, p3, options?)`**
|
|
2687
|
+
```tsx
|
|
2688
|
+
// Text along a cubic Bezier curve
|
|
2689
|
+
textPath.onCubic(
|
|
2690
|
+
"S-CURVE",
|
|
2691
|
+
{ x: 200, y: 600 }, // start
|
|
2692
|
+
{ x: 600, y: 400 }, // control 1
|
|
2693
|
+
{ x: 1320, y: 800 }, // control 2
|
|
2694
|
+
{ x: 1720, y: 600 }, // end
|
|
2695
|
+
{ fontSize: "3xl", color: "purple-300" }
|
|
2696
|
+
)
|
|
2697
|
+
```
|
|
2698
|
+
|
|
2699
|
+
**`textPath.onArc(text, cx, cy, radius, startAngle, endAngle, options?)`**
|
|
2700
|
+
```tsx
|
|
2701
|
+
// Text along a circular arc
|
|
2702
|
+
textPath.onArc(
|
|
2703
|
+
"ARC TEXT",
|
|
2704
|
+
960, // center x
|
|
2705
|
+
540, // center y
|
|
2706
|
+
400, // radius
|
|
2707
|
+
0, // start angle (degrees)
|
|
2708
|
+
180, // end angle (degrees)
|
|
2709
|
+
{ fontSize: "2xl", color: "pink-300" }
|
|
2710
|
+
)
|
|
2711
|
+
```
|
|
2712
|
+
|
|
2713
|
+
### Options
|
|
2714
|
+
|
|
2715
|
+
All `textPath` functions accept an optional `options` object:
|
|
2716
|
+
|
|
2717
|
+
```typescript
|
|
2718
|
+
{
|
|
2719
|
+
fontSize?: string; // Tailwind size: "xl", "2xl", "4xl", etc.
|
|
2720
|
+
fontWeight?: string; // Tailwind weight: "bold", "semibold", etc.
|
|
2721
|
+
color?: string; // Tailwind color: "white", "blue-500", etc.
|
|
2722
|
+
letterSpacing?: number; // Space between characters (0-1, default: 0)
|
|
2723
|
+
style?: any; // Additional inline styles
|
|
2724
|
+
}
|
|
2725
|
+
```
|
|
2726
|
+
|
|
2727
|
+
### Examples
|
|
2728
|
+
|
|
2729
|
+
**Animated rotating text:**
|
|
2730
|
+
```tsx
|
|
2731
|
+
export default function RotatingText({ tw, textPath, progress }) {
|
|
2732
|
+
return (
|
|
2733
|
+
<div style={tw('relative w-full h-full bg-black')}>
|
|
2734
|
+
{textPath.onCircle(
|
|
2735
|
+
"SPINNING • TEXT • ",
|
|
2736
|
+
960, 540, 400,
|
|
2737
|
+
progress, // Rotate based on video progress
|
|
2738
|
+
{ fontSize: "3xl", color: "yellow-300" }
|
|
2739
|
+
)}
|
|
2740
|
+
</div>
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
```
|
|
2744
|
+
|
|
2745
|
+
**Multiple text paths:**
|
|
2746
|
+
```tsx
|
|
2747
|
+
export default function MultiPath({ tw, textPath }) {
|
|
2748
|
+
return (
|
|
2749
|
+
<div style={tw('relative w-full h-full bg-gradient-to-br from-slate-900 to-slate-700')}>
|
|
2750
|
+
{/* Text on outer circle */}
|
|
2751
|
+
{textPath.onCircle(
|
|
2752
|
+
"OUTER RING",
|
|
2753
|
+
960, 540, 500, 0,
|
|
2754
|
+
{ fontSize: "5xl", fontWeight: "bold", color: "white" }
|
|
2755
|
+
)}
|
|
2756
|
+
|
|
2757
|
+
{/* Text on inner circle */}
|
|
2758
|
+
{textPath.onCircle(
|
|
2759
|
+
"inner ring",
|
|
2760
|
+
960, 540, 300, 0.5, // offset by 50% for rotation
|
|
2761
|
+
{ fontSize: "2xl", color: "white/60" }
|
|
2762
|
+
)}
|
|
2763
|
+
</div>
|
|
2764
|
+
);
|
|
2765
|
+
}
|
|
2766
|
+
```
|
|
2767
|
+
|
|
2768
|
+
**Text following a drawn path:**
|
|
2769
|
+
```tsx
|
|
2770
|
+
export default function PathText({ tw, textPath }) {
|
|
2771
|
+
return (
|
|
2772
|
+
<div style={tw('relative w-full h-full bg-gray-900')}>
|
|
2773
|
+
{/* Draw the path */}
|
|
2774
|
+
<svg width="1920" height="1080" style={{ position: 'absolute' }}>
|
|
2775
|
+
<path
|
|
2776
|
+
d="M 200 400 Q 960 150 1720 400"
|
|
2777
|
+
stroke="rgba(255,255,255,0.2)"
|
|
2778
|
+
strokeWidth={2}
|
|
2779
|
+
fill="none"
|
|
2780
|
+
/>
|
|
2781
|
+
</svg>
|
|
2782
|
+
|
|
2783
|
+
{/* Text following the path */}
|
|
2784
|
+
{textPath.onQuadratic(
|
|
2785
|
+
"FOLLOWING THE CURVE",
|
|
2786
|
+
{ x: 200, y: 400 },
|
|
2787
|
+
{ x: 960, y: 150 },
|
|
2788
|
+
{ x: 1720, y: 400 },
|
|
2789
|
+
{ fontSize: "3xl", fontWeight: "bold", color: "blue-300" }
|
|
2790
|
+
)}
|
|
2791
|
+
</div>
|
|
2792
|
+
);
|
|
2793
|
+
}
|
|
2794
|
+
```
|
|
2795
|
+
|
|
2796
|
+
For animated text paths, see [Text Path Animations](/animation#text-path-animations).
|
|
2797
|
+
|
|
2798
|
+
## Reserved Prop Names
|
|
2799
|
+
|
|
2800
|
+
The following prop names are **reserved** and cannot be used in your template's `meta.props`:
|
|
2801
|
+
|
|
2802
|
+
- `tw`, `qr`, `image`, `template` - Core helpers
|
|
2803
|
+
- `path`, `textPath` - Path and text helpers
|
|
2804
|
+
- `config`, `frame`, `progress` - System props
|
|
2805
|
+
|
|
2806
|
+
**Why?** These names are used for loopwind's built-in helpers. Using them as prop names would cause conflicts.
|
|
2807
|
+
|
|
2808
|
+
**Example:**
|
|
2809
|
+
```tsx
|
|
2810
|
+
// ❌ BAD - 'image' is reserved
|
|
2811
|
+
export const meta = {
|
|
2812
|
+
props: {
|
|
2813
|
+
title: "string",
|
|
2814
|
+
image: "string" // Error!
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
|
|
2818
|
+
// ✅ GOOD - Use descriptive alternatives
|
|
2819
|
+
export const meta = {
|
|
2820
|
+
props: {
|
|
2821
|
+
title: "string",
|
|
2822
|
+
imageUrl: "string", // or imageSrc, photoUrl, etc.
|
|
2823
|
+
logoUrl: "string"
|
|
2824
|
+
}
|
|
2825
|
+
};
|
|
2826
|
+
```
|
|
2827
|
+
|
|
2828
|
+
If you try to use a reserved name, you'll get a helpful error:
|
|
2829
|
+
```
|
|
2830
|
+
Template uses reserved prop names: image
|
|
2831
|
+
|
|
2832
|
+
Try renaming: "image" → "imageUrl" or "imageSrc"
|
|
2833
|
+
|
|
2834
|
+
Reserved names: tw, qr, image, template, path, textPath, config, frame, progress
|
|
2835
|
+
```
|
|
2836
|
+
|
|
1989
2837
|
## All Props Reference
|
|
1990
2838
|
|
|
1991
2839
|
Every template receives these props:
|
|
1992
2840
|
|
|
1993
2841
|
```tsx
|
|
1994
|
-
export default function MyTemplate({
|
|
1995
|
-
// Core helpers
|
|
2842
|
+
export default function MyTemplate({
|
|
2843
|
+
// Core helpers (RESERVED - cannot be used as prop names)
|
|
1996
2844
|
tw, // Tailwind class converter
|
|
1997
2845
|
qr, // QR code generator (this page)
|
|
1998
2846
|
template, // Template composer (this page)
|
|
1999
2847
|
config, // User config from loopwind.json (this page)
|
|
2000
|
-
|
|
2001
|
-
|
|
2848
|
+
textPath, // Text on path helpers (this page)
|
|
2849
|
+
|
|
2850
|
+
// Media helpers (RESERVED)
|
|
2002
2851
|
image, // Image embedder → see /images
|
|
2852
|
+
path, // Path following → see /animation
|
|
2003
2853
|
|
|
2004
|
-
// Video-specific (only in video templates)
|
|
2854
|
+
// Video-specific (RESERVED - only in video templates)
|
|
2005
2855
|
frame, // Current frame number → see /video
|
|
2006
2856
|
progress, // Animation progress 0-1 → see /video
|
|
2007
|
-
|
|
2008
|
-
// Your custom props
|
|
2009
|
-
...props // Any props from your props
|
|
2857
|
+
|
|
2858
|
+
// Your custom props (use any names EXCEPT the reserved ones above)
|
|
2859
|
+
...props // Any props from your meta.props
|
|
2010
2860
|
}) {
|
|
2011
2861
|
// Your template code
|
|
2012
2862
|
}
|