Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,19 @@ jobs:

- name: Validate Expo config
run: pnpm run example:build

build-docs:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3

- name: Setup
uses: ./.github/actions/setup

- name: Check documentation
run: pnpm run docs:check

- name: Build documentation
run: pnpm run docs:build
11 changes: 10 additions & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": ["lib", "node_modules", "ios", "android", "dist", "**/*.md"],
"ignorePatterns": [
"lib",
"node_modules",
"ios",
"android",
"dist",
"doc_build",
".rspress",
"**/*.md"
],
"sortImports": {
"groups": [
"type-import",
Expand Down
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@
"globals": {
"__DEV__": "readonly"
},
"ignorePatterns": ["lib", "node_modules", "ios", "android", "dist"]
"ignorePatterns": ["lib", "node_modules", "ios", "android", "dist", "doc_build", ".rspress"]
}
2 changes: 2 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# @react-native-motion-kit/text-motion

한영 Rspress 문서: [개발 및 빌드 안내](./docs/README.md). `pnpm docs:dev`로 로컬 문서를 실행할 수 있습니다.

React Native에서 고성능 split text animation을 만들기 위한 라이브러리입니다. Reanimated 기반으로 동작하고, 확장 가능한 renderer boundary를 염두에 두고 있습니다.

English documentation: [README.md](./README.md)
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Korean documentation: [README.ko.md](./README.ko.md)

Package documentation: [packages/text-motion/README.md](./packages/text-motion/README.md)

Bilingual Rspress documentation: [development and build instructions](./docs/README.md). Run `pnpm docs:dev` to browse locally.

This repository is a pnpm workspace for the `@react-native-motion-kit/text-motion` package and Expo example app.

## Package
Expand Down
6 changes: 6 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
doc_build/
.rspress/
.cache/
*.local
*.log
45 changes: 45 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# React Native Text Motion documentation

This pnpm workspace uses Rspress 2, following the structure of the Gesture Image
Viewer documentation. It documents the current public `0.1.0` API in English and
Korean, with one set of routes and no historical version selector.

## Develop

Run commands from the repository root:

```sh
pnpm install
pnpm docs:dev
```

English is served at `/`; Korean is served at `/ko/`. Use the language menu to
switch to the corresponding page. The home page extends the default theme with
package-manager installation tabs.

## Verify and build

```sh
pnpm docs:check
pnpm docs:typecheck
pnpm docs:build
pnpm docs:preview
```

The static build is written to `docs/doc_build/`. A static host such as Cloudflare
Pages can use `pnpm docs:build` as its build command and `docs/doc_build` as its
output directory, with the repository root as the working directory. Deployment
and a production domain must be configured separately.

## Maintain

- Keep matching pages under `docs/en/` and `docs/ko/` in sync. `docs:check` checks
page parity, local links, and navigation metadata.
- Edit `_nav.json` for top navigation and `_meta.json` for sidebar ordering.
- Keep code samples aligned with the public exports and the Expo example app.
- Use `docs/public/` for shared static files and `theme/` for theme extensions.
- Rspress generates `llms.txt` and full documentation text for AI tools.
- Link to official setup instructions for Reanimated and Worklets; the package's
peer minimums are not a complete native dependency compatibility matrix.
- Hosted demos are not configured. The examples page provides working recipes
and instructions for running the local Expo app.
4 changes: 4 additions & 0 deletions docs/docs/en/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
{ "type": "dir", "name": "guide", "label": "Guide" },
{ "type": "dir", "name": "api", "label": "API Reference" }
]
5 changes: 5 additions & 0 deletions docs/docs/en/_nav.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
{ "text": "Guide", "link": "/guide/getting-started/overview.html", "activeMatch": "/guide/" },
{ "text": "API", "link": "/api/recipe.html", "activeMatch": "/api/" },
{ "text": "Examples", "link": "/guide/usage/examples.html" }
]
32 changes: 32 additions & 0 deletions docs/docs/en/api/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[
{
"type": "file",
"name": "recipe",
"label": "Recipe"
},
{
"type": "file",
"name": "splitters",
"label": "Splitters"
},
{
"type": "file",
"name": "renderers",
"label": "Renderers"
},
{
"type": "file",
"name": "timelines",
"label": "Timelines"
},
{
"type": "file",
"name": "effects",
"label": "Effects"
},
{
"type": "file",
"name": "component-props",
"label": "Component Props"
}
]
85 changes: 85 additions & 0 deletions docs/docs/en/api/component-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Component Props

Calling a recipe's `.component()` returns a `TextMotionComponent`. Its props are `TextMotionComponentProps`, shared by both built-in renderers.

## Text and styling

| Prop | Type | Behavior |
| --- | --- | --- |
| `children` | `string` | Required plain text. Nested elements, arrays, and rich text are not supported. |
| `style` | `StyleProp<TextStyle>` | Text styling. `nativeText()` applies it to token text; `overlayText()` uses it for the measured paragraph. |
| `testID` | `string` | Identifies the parent container. |
| `nativeID` | `string` | Passed to the parent container. |
| `allowFontScaling` | React Native `TextProps['allowFontScaling']` | Forwarded to text rendering. |
| `maxFontSizeMultiplier` | React Native `TextProps['maxFontSizeMultiplier']` | Forwarded to text rendering. |

The component requires a string at runtime as well as in TypeScript. To include authored newlines, use a string expression such as `{'First line\nSecond line'}`. Use a surrounding `View` for external layout constraints instead of assuming all `Text` or `View` props are forwarded.

```tsx
import {
defineTextMotion,
fade,
nativeText,
words,
} from '@react-native-motion-kit/text-motion';
import { View } from 'react-native';

const Title = defineTextMotion()
.split(words())
.layout(nativeText())
.effect(fade())
.component();

export function Heading() {
return (
<View style={{ maxWidth: 320 }}>
<Title
testID="heading"
allowFontScaling
maxFontSizeMultiplier={1.5}
style={{ fontSize: 28, lineHeight: 36, color: '#234' }}
>
Make room for readable motion
</Title>
</View>
);
}
```

## Playback

| Prop | Type | Behavior |
| --- | --- | --- |
| `controls` | `TextMotionControls` | Event-driven `play`, `replay`, `reset`, and `stop`; internal playback uses the recipe's `.motion()` config. |
| `progress` | Reanimated `SharedValue<number>` | App-owned whole-text progress; internal autoplay and `.motion()` do not drive that instance. |

**`controls` and `progress` are mutually exclusive.** TypeScript rejects the combination and both renderers also throw at runtime. With neither prop, animated content autoplays on mount.

`progress` is normalized: `0` is initial and `1` is final. Finite values outside that range are clamped, and non-finite values are treated as `0`. Timeline delays map it to per-unit progress, so it is not a visible-character percentage. See [Playback](../guide/usage/playback.md) for complete controls and progress examples.

## Accessibility

The supported accessibility props are:

- `accessibilityLabel`
- `accessibilityHint`
- `accessibilityRole`
- `accessibilityState`
- `accessibilityValue`
- `accessibilityActions`
- `accessibilityLanguage`
- `onAccessibilityAction`

The default parent label is the complete original string; `accessibilityLabel` overrides it. The policy owns whether the parent is accessible and whether decorative nodes are hidden. Configure it through `.accessibility(parentLabelPolicy(...))`, rather than passing an `accessible` prop. See [Accessibility](../guide/usage/accessibility.md).

## Lifecycle

Ordinary parent rerenders with the same text and recipe preserve in-flight playback. Text changes are enter-only: old content is not retained for an exit, crossfade, or token diff. In controls mode, later commands target the current text. In external-progress mode, the current shared value determines the new text's state.

Changes to effect, timeline, or motion configuration can reset affected animations. Keep recipe components and function-valued motion options stable when playback should survive parent rerenders.

## Deliberately narrow surface

These components are not full React Native `Text` replacements. Props such as `numberOfLines`, `ellipsizeMode`, `lineBreakMode`, `onTextLayout`, `selectable`, text `onPress`, and nested styled text are outside the current contract. Put interaction or external layout on an appropriate surrounding component.

`TextMotionComponent`, `TextMotionComponentProps`, `TextMotionComponentTextProps`, and `TextMotionComponentAccessibilityProps` are exported from the root for typed application code. Controls use `useTextMotionControls()` and the exported `TextMotionControls` type; there is no public component-ref or context/provider playback API.
113 changes: 113 additions & 0 deletions docs/docs/en/api/effects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Effects

Effects describe the visual change of each motion unit. Import them from `@react-native-motion-kit/text-motion` and pass a compatible effect to `.effect(...)` after selecting a renderer.

## Factories and defaults

Every option is optional; `typewriter()` takes no options. All numeric effect inputs must be finite. Directional offsets can be negative. Offsets use React Native layout units, opacity is a scalar, and scale values are multipliers.

| Factory | Options and defaults | Behavior | Renderer |
| --- | --- | --- | --- |
| `fade(options?)` | `from: 0`, `to: 1` | Changes opacity from `from` to `to` | Both |
| `rise(options?)` | `y: 12` | Starts offset downward by `y`, then moves into place | Both |
| `slide(options?)` | `x: 0`, `y: 12` | Starts at an x/y offset, then moves into place | Both |
| `scale(options?)` | `from: 0.96`, `to: 1` | Changes the scale multiplier | Both |
| `pulse(options?)` | `scale: 1.04` | Peaks around the midpoint, then returns to the composed final scale | Both |
| `shake(options?)` | `x: 4` | Starts with a horizontal offset for a shake-like entrance | Both |
| `lineReveal(options?)` | `y: 16`, `fromOpacity: 0` | Reveals rendered lines through masks | `overlayText()` |
| `typewriter()` | None | Hides a token until its local progress becomes positive | `nativeText()` |

`shake()` is an entrance offset; it does not create a repeating oscillation. `pulse()` provides midpoint emphasis; use `scale()` when the starting or ending size should change.

The root exports `FadeOptions`, `RiseOptions`, `SlideOptions`, `ScaleOptions`, `PulseOptions`, `ShakeOptions`, and `LineRevealOptions`. Their properties match the table above.

## Compose with `.and(...)`

```tsx
import {
defineTextMotion,
fade,
nativeText,
rise,
scale,
stagger,
words,
} from '@react-native-motion-kit/text-motion';

const HeroTitle = defineTextMotion()
.split(words())
.layout(nativeText())
.timeline(stagger(0.04))
.effect(rise({ y: 14 }).and(fade()).and(scale({ from: 0.98 })))
.component();

export function Heading() {
return <HeroTitle>A little motion, a clear message</HeroTitle>;
}
```

`.and(...)` returns a composed `TextMotionEffect`. Repeated `.effect(...)` calls also append effects. Transform offsets accumulate and scale multipliers multiply; opacity-setting effects are applied in composition order. Prefer one clear opacity effect when combining recipes.

Effects require compatible renderer capabilities. `lineReveal()` cannot be used with `nativeText()`, and `typewriter()` cannot be used with `overlayText()`. TypeScript checks valid combinations; runtime checks also reject incompatible effects.

## `lineReveal()`

```tsx
import {
defineTextMotion,
lineReveal,
overlayText,
stagger,
} from '@react-native-motion-kit/text-motion';

const HeroLines = defineTextMotion()
.layout(overlayText())
.timeline(stagger(0.08))
.effect(lineReveal({ y: 18, fromOpacity: 0 }))
.motion({ kind: 'timing', options: { duration: 420 } })
.component();
```

Do not add a splitter. The renderer measures actual wrapped lines. The reveal offset moves the paragraph copy inside each mask, while opacity applies to the line frame. Composed `scale()` and `pulse()` transform the measured line frame. See [Renderers](./renderers.md) for layout and measurement behavior.

## `typewriter()`

Use this for fixed-layout character reveals. The splitter chooses the reveal unit, the timeline chooses its start, and `.motion()` chooses any subsequent movement.

```tsx
import {
defineTextMotion,
graphemes,
nativeText,
parentLabelPolicy,
stagger,
typewriter,
} from '@react-native-motion-kit/text-motion';

const TypewriterText = defineTextMotion()
.split(graphemes())
.layout(nativeText())
.timeline(stagger(0.04))
.effect(typewriter())
.motion({ kind: 'timing', options: { duration: 0 } })
.accessibility(parentLabelPolicy({ reducedMotion: 'final-state' }))
.component();

export function Greeting() {
return <TypewriterText>안녕 세계! 👩🏽‍💻</TypewriterText>;
}
```

The completed phrase's space and line breaks remain in place from the start. Visible graphemes get delay slots 40ms apart; whitespace and explicit newlines keep their layout without taking a slot. Punctuation takes a slot like other visible characters. The first character appears on the first playback update, and display timing follows device frames.

`duration: 0` makes each reveal instant in autoplay or controls mode. To move a character after it appears, use a nonzero duration and compose an effect such as `typewriter().and(rise({ y: 6 }))`. Composing `fade()` intentionally makes appearance gradual. The visibility gate works in either composition order; adding `typewriter()` twice does not add another clock.

Use [controls](../guide/usage/playback.md) for replay/reset/stop. `stop()` preserves the current state; a later `play()` does not promise exact continuation of the remaining typing schedule. To type a new phrase fully from its start, remount with a new React `key`. Appending text is not a streaming API.

With external `progress`, `0` hides the animated characters and `1` shows the final state. **Progress is timeline progress, not the percentage of characters shown.** For maximum delay `D` and a character's delay `d`, local progress is `clamp(progress × (D + 1) - d, 0, 1)`. A character becomes visible when this is greater than zero. Decreasing progress past its start hides it again. `.motion()` is ignored in external-progress mode.

This effect does not grow or delete the underlying string, provide a cursor, guarantee single-`Text` shaping, or implement streaming. Use it with short UI copy and measure larger workloads.

## Public scope

Effect handles expose `.and(...)`; internal `name`, `options`, and `requiredCapabilities` fields are not public. Custom effect factories, `wipe`, `scramble`, and Skia effects are outside the current API.
Loading
Loading