@react-native-motion-kit/text-motion 0.0.0-stage → 0.1.1

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.
Files changed (252) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +20 -0
  3. package/README.ko.md +104 -0
  4. package/README.md +103 -2
  5. package/lib/module/accessibility/index.js +4 -0
  6. package/lib/module/accessibility/index.js.map +1 -0
  7. package/lib/module/accessibility/policies.js +39 -0
  8. package/lib/module/accessibility/policies.js.map +1 -0
  9. package/lib/module/controls/brand.js +4 -0
  10. package/lib/module/controls/brand.js.map +1 -0
  11. package/lib/module/controls/controls.js +28 -0
  12. package/lib/module/controls/controls.js.map +1 -0
  13. package/lib/module/controls/descriptors.js +55 -0
  14. package/lib/module/controls/descriptors.js.map +1 -0
  15. package/lib/module/controls/index.js +4 -0
  16. package/lib/module/controls/index.js.map +1 -0
  17. package/lib/module/effects/compose.js +48 -0
  18. package/lib/module/effects/compose.js.map +1 -0
  19. package/lib/module/effects/fade.js +15 -0
  20. package/lib/module/effects/fade.js.map +1 -0
  21. package/lib/module/effects/index.js +12 -0
  22. package/lib/module/effects/index.js.map +1 -0
  23. package/lib/module/effects/lineReveal.js +17 -0
  24. package/lib/module/effects/lineReveal.js.map +1 -0
  25. package/lib/module/effects/pulse.js +14 -0
  26. package/lib/module/effects/pulse.js.map +1 -0
  27. package/lib/module/effects/rise.js +14 -0
  28. package/lib/module/effects/rise.js.map +1 -0
  29. package/lib/module/effects/scale.js +15 -0
  30. package/lib/module/effects/scale.js.map +1 -0
  31. package/lib/module/effects/shake.js +14 -0
  32. package/lib/module/effects/shake.js.map +1 -0
  33. package/lib/module/effects/slide.js +15 -0
  34. package/lib/module/effects/slide.js.map +1 -0
  35. package/lib/module/effects/typewriter.js +19 -0
  36. package/lib/module/effects/typewriter.js.map +1 -0
  37. package/lib/module/effects/validation.js +10 -0
  38. package/lib/module/effects/validation.js.map +1 -0
  39. package/lib/module/index.js +10 -0
  40. package/lib/module/index.js.map +1 -0
  41. package/lib/module/package.json +1 -0
  42. package/lib/module/presets/index.js +35 -0
  43. package/lib/module/presets/index.js.map +1 -0
  44. package/lib/module/recipe/component.js +60 -0
  45. package/lib/module/recipe/component.js.map +1 -0
  46. package/lib/module/recipe/defineTextMotion.js +9 -0
  47. package/lib/module/recipe/defineTextMotion.js.map +1 -0
  48. package/lib/module/recipe/descriptors.js +60 -0
  49. package/lib/module/recipe/descriptors.js.map +1 -0
  50. package/lib/module/recipe/index.js +5 -0
  51. package/lib/module/recipe/index.js.map +1 -0
  52. package/lib/module/recipe/recipe.js +163 -0
  53. package/lib/module/recipe/recipe.js.map +1 -0
  54. package/lib/module/renderers/index.js +5 -0
  55. package/lib/module/renderers/index.js.map +1 -0
  56. package/lib/module/renderers/nativeText.js +494 -0
  57. package/lib/module/renderers/nativeText.js.map +1 -0
  58. package/lib/module/renderers/nativeTextControls.js +96 -0
  59. package/lib/module/renderers/nativeTextControls.js.map +1 -0
  60. package/lib/module/renderers/nativeTextLineLayout.js +290 -0
  61. package/lib/module/renderers/nativeTextLineLayout.js.map +1 -0
  62. package/lib/module/renderers/nativeTextPlayback.js +91 -0
  63. package/lib/module/renderers/nativeTextPlayback.js.map +1 -0
  64. package/lib/module/renderers/overlayText.js +543 -0
  65. package/lib/module/renderers/overlayText.js.map +1 -0
  66. package/lib/module/renderers/overlayTextLayout.js +182 -0
  67. package/lib/module/renderers/overlayTextLayout.js.map +1 -0
  68. package/lib/module/renderers/rendererMotion.js +264 -0
  69. package/lib/module/renderers/rendererMotion.js.map +1 -0
  70. package/lib/module/split/createToken.js +19 -0
  71. package/lib/module/split/createToken.js.map +1 -0
  72. package/lib/module/split/custom.js +57 -0
  73. package/lib/module/split/custom.js.map +1 -0
  74. package/lib/module/split/graphemes.js +25 -0
  75. package/lib/module/split/graphemes.js.map +1 -0
  76. package/lib/module/split/index.js +7 -0
  77. package/lib/module/split/index.js.map +1 -0
  78. package/lib/module/split/lines.js +43 -0
  79. package/lib/module/split/lines.js.map +1 -0
  80. package/lib/module/split/segmenter.js +74 -0
  81. package/lib/module/split/segmenter.js.map +1 -0
  82. package/lib/module/split/words.js +28 -0
  83. package/lib/module/split/words.js.map +1 -0
  84. package/lib/module/timeline/index.js +7 -0
  85. package/lib/module/timeline/index.js.map +1 -0
  86. package/lib/module/timeline/parallel.js +21 -0
  87. package/lib/module/timeline/parallel.js.map +1 -0
  88. package/lib/module/timeline/sequence.js +18 -0
  89. package/lib/module/timeline/sequence.js.map +1 -0
  90. package/lib/module/timeline/stagger.js +43 -0
  91. package/lib/module/timeline/stagger.js.map +1 -0
  92. package/lib/module/timeline/validation.js +24 -0
  93. package/lib/module/timeline/validation.js.map +1 -0
  94. package/lib/module/timeline/wave.js +25 -0
  95. package/lib/module/timeline/wave.js.map +1 -0
  96. package/lib/module/types/index.js +4 -0
  97. package/lib/module/types/index.js.map +1 -0
  98. package/lib/module/types/recipe.js +4 -0
  99. package/lib/module/types/recipe.js.map +1 -0
  100. package/lib/module/types/renderer.js +27 -0
  101. package/lib/module/types/renderer.js.map +1 -0
  102. package/lib/module/types/token.js +2 -0
  103. package/lib/module/types/token.js.map +1 -0
  104. package/lib/typescript/package.json +1 -0
  105. package/lib/typescript/src/accessibility/index.d.ts +2 -0
  106. package/lib/typescript/src/accessibility/index.d.ts.map +1 -0
  107. package/lib/typescript/src/accessibility/policies.d.ts +13 -0
  108. package/lib/typescript/src/accessibility/policies.d.ts.map +1 -0
  109. package/lib/typescript/src/controls/brand.d.ts +2 -0
  110. package/lib/typescript/src/controls/brand.d.ts.map +1 -0
  111. package/lib/typescript/src/controls/controls.d.ts +28 -0
  112. package/lib/typescript/src/controls/controls.d.ts.map +1 -0
  113. package/lib/typescript/src/controls/descriptors.d.ts +14 -0
  114. package/lib/typescript/src/controls/descriptors.d.ts.map +1 -0
  115. package/lib/typescript/src/controls/index.d.ts +3 -0
  116. package/lib/typescript/src/controls/index.d.ts.map +1 -0
  117. package/lib/typescript/src/effects/compose.d.ts +11 -0
  118. package/lib/typescript/src/effects/compose.d.ts.map +1 -0
  119. package/lib/typescript/src/effects/fade.d.ts +10 -0
  120. package/lib/typescript/src/effects/fade.d.ts.map +1 -0
  121. package/lib/typescript/src/effects/index.d.ts +10 -0
  122. package/lib/typescript/src/effects/index.d.ts.map +1 -0
  123. package/lib/typescript/src/effects/lineReveal.d.ts +11 -0
  124. package/lib/typescript/src/effects/lineReveal.d.ts.map +1 -0
  125. package/lib/typescript/src/effects/pulse.d.ts +8 -0
  126. package/lib/typescript/src/effects/pulse.d.ts.map +1 -0
  127. package/lib/typescript/src/effects/rise.d.ts +8 -0
  128. package/lib/typescript/src/effects/rise.d.ts.map +1 -0
  129. package/lib/typescript/src/effects/scale.d.ts +10 -0
  130. package/lib/typescript/src/effects/scale.d.ts.map +1 -0
  131. package/lib/typescript/src/effects/shake.d.ts +8 -0
  132. package/lib/typescript/src/effects/shake.d.ts.map +1 -0
  133. package/lib/typescript/src/effects/slide.d.ts +10 -0
  134. package/lib/typescript/src/effects/slide.d.ts.map +1 -0
  135. package/lib/typescript/src/effects/typewriter.d.ts +13 -0
  136. package/lib/typescript/src/effects/typewriter.d.ts.map +1 -0
  137. package/lib/typescript/src/effects/validation.d.ts +2 -0
  138. package/lib/typescript/src/effects/validation.d.ts.map +1 -0
  139. package/lib/typescript/src/index.d.ts +15 -0
  140. package/lib/typescript/src/index.d.ts.map +1 -0
  141. package/lib/typescript/src/presets/index.d.ts +10 -0
  142. package/lib/typescript/src/presets/index.d.ts.map +1 -0
  143. package/lib/typescript/src/recipe/component.d.ts +3 -0
  144. package/lib/typescript/src/recipe/component.d.ts.map +1 -0
  145. package/lib/typescript/src/recipe/defineTextMotion.d.ts +4 -0
  146. package/lib/typescript/src/recipe/defineTextMotion.d.ts.map +1 -0
  147. package/lib/typescript/src/recipe/descriptors.d.ts +15 -0
  148. package/lib/typescript/src/recipe/descriptors.d.ts.map +1 -0
  149. package/lib/typescript/src/recipe/index.d.ts +4 -0
  150. package/lib/typescript/src/recipe/index.d.ts.map +1 -0
  151. package/lib/typescript/src/recipe/recipe.d.ts +67 -0
  152. package/lib/typescript/src/recipe/recipe.d.ts.map +1 -0
  153. package/lib/typescript/src/renderers/index.d.ts +3 -0
  154. package/lib/typescript/src/renderers/index.d.ts.map +1 -0
  155. package/lib/typescript/src/renderers/nativeText.d.ts +13 -0
  156. package/lib/typescript/src/renderers/nativeText.d.ts.map +1 -0
  157. package/lib/typescript/src/renderers/nativeTextControls.d.ts +16 -0
  158. package/lib/typescript/src/renderers/nativeTextControls.d.ts.map +1 -0
  159. package/lib/typescript/src/renderers/nativeTextLineLayout.d.ts +84 -0
  160. package/lib/typescript/src/renderers/nativeTextLineLayout.d.ts.map +1 -0
  161. package/lib/typescript/src/renderers/nativeTextPlayback.d.ts +21 -0
  162. package/lib/typescript/src/renderers/nativeTextPlayback.d.ts.map +1 -0
  163. package/lib/typescript/src/renderers/overlayText.d.ts +13 -0
  164. package/lib/typescript/src/renderers/overlayText.d.ts.map +1 -0
  165. package/lib/typescript/src/renderers/overlayTextLayout.d.ts +36 -0
  166. package/lib/typescript/src/renderers/overlayTextLayout.d.ts.map +1 -0
  167. package/lib/typescript/src/renderers/rendererMotion.d.ts +53 -0
  168. package/lib/typescript/src/renderers/rendererMotion.d.ts.map +1 -0
  169. package/lib/typescript/src/split/createToken.d.ts +11 -0
  170. package/lib/typescript/src/split/createToken.d.ts.map +1 -0
  171. package/lib/typescript/src/split/custom.d.ts +17 -0
  172. package/lib/typescript/src/split/custom.d.ts.map +1 -0
  173. package/lib/typescript/src/split/graphemes.d.ts +4 -0
  174. package/lib/typescript/src/split/graphemes.d.ts.map +1 -0
  175. package/lib/typescript/src/split/index.d.ts +5 -0
  176. package/lib/typescript/src/split/index.d.ts.map +1 -0
  177. package/lib/typescript/src/split/lines.d.ts +4 -0
  178. package/lib/typescript/src/split/lines.d.ts.map +1 -0
  179. package/lib/typescript/src/split/segmenter.d.ts +18 -0
  180. package/lib/typescript/src/split/segmenter.d.ts.map +1 -0
  181. package/lib/typescript/src/split/words.d.ts +4 -0
  182. package/lib/typescript/src/split/words.d.ts.map +1 -0
  183. package/lib/typescript/src/timeline/index.d.ts +5 -0
  184. package/lib/typescript/src/timeline/index.d.ts.map +1 -0
  185. package/lib/typescript/src/timeline/parallel.d.ts +4 -0
  186. package/lib/typescript/src/timeline/parallel.d.ts.map +1 -0
  187. package/lib/typescript/src/timeline/sequence.d.ts +4 -0
  188. package/lib/typescript/src/timeline/sequence.d.ts.map +1 -0
  189. package/lib/typescript/src/timeline/stagger.d.ts +11 -0
  190. package/lib/typescript/src/timeline/stagger.d.ts.map +1 -0
  191. package/lib/typescript/src/timeline/validation.d.ts +4 -0
  192. package/lib/typescript/src/timeline/validation.d.ts.map +1 -0
  193. package/lib/typescript/src/timeline/wave.d.ts +11 -0
  194. package/lib/typescript/src/timeline/wave.d.ts.map +1 -0
  195. package/lib/typescript/src/types/index.d.ts +6 -0
  196. package/lib/typescript/src/types/index.d.ts.map +1 -0
  197. package/lib/typescript/src/types/recipe.d.ts +123 -0
  198. package/lib/typescript/src/types/recipe.d.ts.map +1 -0
  199. package/lib/typescript/src/types/renderer.d.ts +51 -0
  200. package/lib/typescript/src/types/renderer.d.ts.map +1 -0
  201. package/lib/typescript/src/types/token.d.ts +35 -0
  202. package/lib/typescript/src/types/token.d.ts.map +1 -0
  203. package/package.json +117 -3
  204. package/src/accessibility/index.ts +7 -0
  205. package/src/accessibility/policies.ts +60 -0
  206. package/src/controls/brand.ts +3 -0
  207. package/src/controls/controls.ts +41 -0
  208. package/src/controls/descriptors.ts +83 -0
  209. package/src/controls/index.ts +2 -0
  210. package/src/effects/compose.ts +98 -0
  211. package/src/effects/fade.ts +22 -0
  212. package/src/effects/index.ts +9 -0
  213. package/src/effects/lineReveal.ts +28 -0
  214. package/src/effects/pulse.ts +19 -0
  215. package/src/effects/rise.ts +19 -0
  216. package/src/effects/scale.ts +22 -0
  217. package/src/effects/shake.ts +19 -0
  218. package/src/effects/slide.ts +22 -0
  219. package/src/effects/typewriter.ts +16 -0
  220. package/src/effects/validation.ts +9 -0
  221. package/src/index.tsx +46 -0
  222. package/src/presets/index.ts +42 -0
  223. package/src/recipe/component.tsx +88 -0
  224. package/src/recipe/defineTextMotion.ts +6 -0
  225. package/src/recipe/descriptors.ts +158 -0
  226. package/src/recipe/index.ts +3 -0
  227. package/src/recipe/recipe.ts +359 -0
  228. package/src/renderers/index.ts +2 -0
  229. package/src/renderers/nativeText.tsx +813 -0
  230. package/src/renderers/nativeTextControls.ts +131 -0
  231. package/src/renderers/nativeTextLineLayout.ts +495 -0
  232. package/src/renderers/nativeTextPlayback.ts +204 -0
  233. package/src/renderers/overlayText.tsx +945 -0
  234. package/src/renderers/overlayTextLayout.ts +312 -0
  235. package/src/renderers/rendererMotion.ts +483 -0
  236. package/src/split/createToken.ts +26 -0
  237. package/src/split/custom.ts +92 -0
  238. package/src/split/graphemes.ts +27 -0
  239. package/src/split/index.ts +9 -0
  240. package/src/split/lines.ts +35 -0
  241. package/src/split/segmenter.ts +111 -0
  242. package/src/split/words.ts +28 -0
  243. package/src/timeline/index.ts +4 -0
  244. package/src/timeline/parallel.ts +30 -0
  245. package/src/timeline/sequence.ts +29 -0
  246. package/src/timeline/stagger.ts +60 -0
  247. package/src/timeline/validation.ts +29 -0
  248. package/src/timeline/wave.ts +34 -0
  249. package/src/types/index.ts +34 -0
  250. package/src/types/recipe.ts +195 -0
  251. package/src/types/renderer.ts +91 -0
  252. package/src/types/token.ts +39 -0
@@ -0,0 +1,483 @@
1
+ import {
2
+ ReduceMotion,
3
+ withDelay,
4
+ withSpring,
5
+ withTiming,
6
+ type SharedValue,
7
+ } from 'react-native-reanimated';
8
+
9
+ import type {
10
+ TextMotionAnyEffect,
11
+ TextMotionAnyEffectDescriptor,
12
+ TextMotionEffectDescriptorOptions,
13
+ TextMotionInternalRecipeConfig,
14
+ TextMotionMotionConfig,
15
+ } from '../types/recipe';
16
+
17
+ import {
18
+ flattenTextMotionEffectDescriptors,
19
+ readTextMotionTimelineDescriptor,
20
+ } from '../recipe/descriptors';
21
+
22
+ export const TEXT_MOTION_CONTROLLED_ITEM_TIMELINE_SPAN = 1;
23
+
24
+ export type TextMotionStyleTransformState = {
25
+ opacity: number;
26
+ scale: number;
27
+ translateX: number;
28
+ translateY: number;
29
+ };
30
+
31
+ export type TextMotionStyleTransformStatePair = {
32
+ initial: TextMotionStyleTransformState;
33
+ pulseScale: number;
34
+ revealOnStart?: true;
35
+ target: TextMotionStyleTransformState;
36
+ };
37
+
38
+ export type TextMotionOverlayStyleTransformStatePair = {
39
+ frame: TextMotionStyleTransformStatePair;
40
+ revealContent: TextMotionStyleTransformStatePair;
41
+ };
42
+
43
+ export type TextMotionItemMotion = {
44
+ delayMs: number;
45
+ initial: TextMotionStyleTransformState;
46
+ target: TextMotionStyleTransformState;
47
+ motion?: TextMotionMotionConfig;
48
+ pulseScale: number;
49
+ reducedMotion: 'final-state' | 'system';
50
+ revealOnStart?: true;
51
+ };
52
+
53
+ export type TextMotionControlledProgressPlan = {
54
+ itemDelaySeconds: number;
55
+ totalTimelineSpan: number;
56
+ };
57
+
58
+ type TextMotionControlledProgressInput = TextMotionControlledProgressPlan & {
59
+ progress: number;
60
+ };
61
+
62
+ type TextMotionNumberTransform = (value: number) => number;
63
+ type TextMotionStyleStateTransform = (
64
+ state: TextMotionStyleTransformState,
65
+ ) => TextMotionStyleTransformState;
66
+
67
+ type EffectStyleStateChange = {
68
+ initial?: TextMotionStyleStateTransform;
69
+ pulseScale?: TextMotionNumberTransform;
70
+ revealOnStart?: true;
71
+ target?: TextMotionStyleStateTransform;
72
+ };
73
+
74
+ type EffectStyleStateResolver = (effect: TextMotionAnyEffectDescriptor) => EffectStyleStateChange;
75
+ type TextMotionStyleNumberKey = keyof TextMotionStyleTransformState;
76
+
77
+ const DEFAULT_STYLE_STATE: TextMotionStyleTransformState = {
78
+ opacity: 1,
79
+ scale: 1,
80
+ translateX: 0,
81
+ translateY: 0,
82
+ };
83
+
84
+ const REDUCE_MOTION_CONFIG_BY_POLICY = {
85
+ 'final-state': ReduceMotion.Never,
86
+ system: ReduceMotion.System,
87
+ } as const satisfies Record<TextMotionItemMotion['reducedMotion'], ReduceMotion>;
88
+
89
+ const preserveStyleState: TextMotionStyleStateTransform = (state) => state;
90
+ const preserveNumber: TextMotionNumberTransform = (value) => value;
91
+
92
+ const effectStyleStateResolvers: Record<string, EffectStyleStateResolver> = {
93
+ typewriter() {
94
+ return { revealOnStart: true };
95
+ },
96
+ fade(effect) {
97
+ return {
98
+ initial: setStyleNumber('opacity', numberOption(effect.options, 'from', 0)),
99
+ target: setStyleNumber('opacity', numberOption(effect.options, 'to', 1)),
100
+ };
101
+ },
102
+ lineReveal(effect) {
103
+ return {
104
+ initial: composeStyleTransforms(
105
+ setStyleNumber('opacity', numberOption(effect.options, 'fromOpacity', 0)),
106
+ mapStyleNumber(
107
+ 'translateY',
108
+ (translateY) => translateY + numberOption(effect.options, 'y', 16),
109
+ ),
110
+ ),
111
+ target: preserveStyleState,
112
+ };
113
+ },
114
+ pulse(effect) {
115
+ return {
116
+ pulseScale: (scale) => scale * numberOption(effect.options, 'scale', 1.04),
117
+ };
118
+ },
119
+ rise(effect) {
120
+ return {
121
+ initial: mapStyleNumber(
122
+ 'translateY',
123
+ (translateY) => translateY + numberOption(effect.options, 'y', 12),
124
+ ),
125
+ target: preserveStyleState,
126
+ };
127
+ },
128
+ scale(effect) {
129
+ return {
130
+ initial: mapStyleNumber(
131
+ 'scale',
132
+ (scale) => scale * numberOption(effect.options, 'from', 0.96),
133
+ ),
134
+ target: mapStyleNumber('scale', (scale) => scale * numberOption(effect.options, 'to', 1)),
135
+ };
136
+ },
137
+ shake(effect) {
138
+ return {
139
+ initial: mapStyleNumber(
140
+ 'translateX',
141
+ (translateX) => translateX + numberOption(effect.options, 'x', 4),
142
+ ),
143
+ target: preserveStyleState,
144
+ };
145
+ },
146
+ slide(effect) {
147
+ return {
148
+ initial: composeStyleTransforms(
149
+ mapStyleNumber(
150
+ 'translateX',
151
+ (translateX) => translateX + numberOption(effect.options, 'x', 0),
152
+ ),
153
+ mapStyleNumber(
154
+ 'translateY',
155
+ (translateY) => translateY + numberOption(effect.options, 'y', 12),
156
+ ),
157
+ ),
158
+ target: preserveStyleState,
159
+ };
160
+ },
161
+ };
162
+
163
+ const overlayFrameEffectStyleStateResolvers: Record<string, EffectStyleStateResolver> = {
164
+ ...effectStyleStateResolvers,
165
+ lineReveal(effect) {
166
+ return {
167
+ initial: setStyleNumber('opacity', numberOption(effect.options, 'fromOpacity', 0)),
168
+ target: preserveStyleState,
169
+ };
170
+ },
171
+ };
172
+
173
+ const overlayRevealContentEffectStyleStateResolvers: Record<string, EffectStyleStateResolver> = {
174
+ fade: preserveEffectStyleState,
175
+ lineReveal(effect) {
176
+ return {
177
+ initial: mapStyleNumber(
178
+ 'translateY',
179
+ (translateY) => translateY + numberOption(effect.options, 'y', 16),
180
+ ),
181
+ target: preserveStyleState,
182
+ };
183
+ },
184
+ pulse: preserveEffectStyleState,
185
+ rise: preserveEffectStyleState,
186
+ scale: preserveEffectStyleState,
187
+ shake: preserveEffectStyleState,
188
+ slide: preserveEffectStyleState,
189
+ };
190
+
191
+ function numberOption(
192
+ options: TextMotionEffectDescriptorOptions | undefined,
193
+ key: string,
194
+ fallback: number,
195
+ ): number {
196
+ const value = options?.[key];
197
+
198
+ if (value === undefined) {
199
+ return fallback;
200
+ }
201
+
202
+ if (typeof value === 'number' && Number.isFinite(value)) {
203
+ return value;
204
+ }
205
+
206
+ throw new Error(
207
+ `@react-native-motion-kit/text-motion effect option "${key}" must be a finite number.`,
208
+ );
209
+ }
210
+
211
+ function validateDelaySeconds(delay: number): number {
212
+ if (Number.isFinite(delay) && delay >= 0) {
213
+ return delay;
214
+ }
215
+
216
+ throw new Error(
217
+ '@react-native-motion-kit/text-motion timeline delay must be a finite number greater than or equal to 0.',
218
+ );
219
+ }
220
+
221
+ function createDelayMs(delaySeconds: number): number {
222
+ const delayMs = Math.round(delaySeconds * 1000);
223
+
224
+ if (Number.isSafeInteger(delayMs)) {
225
+ return delayMs;
226
+ }
227
+
228
+ throw new Error(
229
+ '@react-native-motion-kit/text-motion timeline delay must convert to a safe integer millisecond value.',
230
+ );
231
+ }
232
+
233
+ function createDefaultStyleStatePair(): TextMotionStyleTransformStatePair {
234
+ return {
235
+ initial: { ...DEFAULT_STYLE_STATE },
236
+ pulseScale: 1,
237
+ target: { ...DEFAULT_STYLE_STATE },
238
+ };
239
+ }
240
+
241
+ function mapStyleNumber(
242
+ key: TextMotionStyleNumberKey,
243
+ mapValue: (value: number) => number,
244
+ ): TextMotionStyleStateTransform {
245
+ return (state) => {
246
+ const nextState: TextMotionStyleTransformState = {
247
+ ...state,
248
+ };
249
+
250
+ nextState[key] = mapValue(state[key]);
251
+
252
+ return nextState;
253
+ };
254
+ }
255
+
256
+ function setStyleNumber(
257
+ key: TextMotionStyleNumberKey,
258
+ value: number,
259
+ ): TextMotionStyleStateTransform {
260
+ return mapStyleNumber(key, () => value);
261
+ }
262
+
263
+ function composeStyleTransforms(
264
+ ...transforms: readonly TextMotionStyleStateTransform[]
265
+ ): TextMotionStyleStateTransform {
266
+ return (state) => transforms.reduce((current, transform) => transform(current), state);
267
+ }
268
+
269
+ function preserveEffectStyleState(): EffectStyleStateChange {
270
+ return {};
271
+ }
272
+
273
+ function resolveEffectStyleStateChange(
274
+ effect: TextMotionAnyEffectDescriptor,
275
+ unsupportedEffectSubject: string,
276
+ resolvers: Record<string, EffectStyleStateResolver> = effectStyleStateResolvers,
277
+ ): EffectStyleStateChange {
278
+ const resolver = resolvers[effect.name];
279
+
280
+ if (resolver) {
281
+ return resolver(effect);
282
+ }
283
+
284
+ throw new Error(
285
+ `${unsupportedEffectSubject} does not implement the "${effect.name}" effect. Use a built-in style-transform effect or a renderer that handles this effect.`,
286
+ );
287
+ }
288
+
289
+ function applyResolvedEffectStyleStateChange(
290
+ styleState: TextMotionStyleTransformStatePair,
291
+ effectStyleStateChange: EffectStyleStateChange,
292
+ ): TextMotionStyleTransformStatePair {
293
+ const initial = effectStyleStateChange.initial ?? preserveStyleState;
294
+ const pulseScale = effectStyleStateChange.pulseScale ?? preserveNumber;
295
+ const target = effectStyleStateChange.target ?? preserveStyleState;
296
+
297
+ return {
298
+ ...styleState,
299
+ ...(effectStyleStateChange.revealOnStart ? { revealOnStart: true } : {}),
300
+ initial: initial(styleState.initial),
301
+ pulseScale: pulseScale(styleState.pulseScale),
302
+ target: target(styleState.target),
303
+ };
304
+ }
305
+
306
+ function applyDelay(animation: number, delayMs: number, reduceMotion: ReduceMotion): number {
307
+ return delayMs > 0 ? withDelay(delayMs, animation, reduceMotion) : animation;
308
+ }
309
+
310
+ export function createTextMotionStyleTransformStatePair(
311
+ effects: readonly TextMotionAnyEffect[],
312
+ unsupportedEffectSubject = 'style-transform renderer',
313
+ ): TextMotionStyleTransformStatePair {
314
+ return createResolvedTextMotionStyleTransformStatePair(
315
+ effects,
316
+ unsupportedEffectSubject,
317
+ effectStyleStateResolvers,
318
+ );
319
+ }
320
+
321
+ function createResolvedTextMotionStyleTransformStatePair(
322
+ effects: readonly TextMotionAnyEffect[],
323
+ unsupportedEffectSubject: string,
324
+ resolvers: Record<string, EffectStyleStateResolver>,
325
+ ): TextMotionStyleTransformStatePair {
326
+ return flattenTextMotionEffectDescriptors(effects).reduce<TextMotionStyleTransformStatePair>(
327
+ (styleState, effect) =>
328
+ applyResolvedEffectStyleStateChange(
329
+ styleState,
330
+ resolveEffectStyleStateChange(effect, unsupportedEffectSubject, resolvers),
331
+ ),
332
+ createDefaultStyleStatePair(),
333
+ );
334
+ }
335
+
336
+ export function createOverlayTextStyleTransformStatePair(
337
+ effects: readonly TextMotionAnyEffect[],
338
+ ): TextMotionOverlayStyleTransformStatePair {
339
+ return {
340
+ frame: createResolvedTextMotionStyleTransformStatePair(
341
+ effects,
342
+ 'overlayText()',
343
+ overlayFrameEffectStyleStateResolvers,
344
+ ),
345
+ revealContent: createResolvedTextMotionStyleTransformStatePair(
346
+ effects,
347
+ 'overlayText()',
348
+ overlayRevealContentEffectStyleStateResolvers,
349
+ ),
350
+ };
351
+ }
352
+
353
+ export function createTextMotionDelaySecondsByItemIndex(
354
+ recipe: TextMotionInternalRecipeConfig,
355
+ count: number,
356
+ ): readonly number[] {
357
+ const timeline = recipe.timeline ? readTextMotionTimelineDescriptor(recipe.timeline) : undefined;
358
+
359
+ return Array.from({ length: count }, (_, index) =>
360
+ validateDelaySeconds(timeline?.delayFor(index, count) ?? 0),
361
+ );
362
+ }
363
+
364
+ export function createTextMotionItemMotion(
365
+ recipe: TextMotionInternalRecipeConfig,
366
+ styleState: TextMotionStyleTransformStatePair,
367
+ delaySeconds: number,
368
+ ): TextMotionItemMotion {
369
+ return {
370
+ ...styleState,
371
+ delayMs: createDelayMs(delaySeconds),
372
+ motion: recipe.motion,
373
+ reducedMotion: recipe.accessibility?.reducedMotion ?? 'system',
374
+ };
375
+ }
376
+
377
+ export function createTextMotionControlledProgressPlan(
378
+ itemIndex: number,
379
+ delaySecondsByItemIndex: readonly number[],
380
+ totalTimelineSpan: number,
381
+ ): TextMotionControlledProgressPlan {
382
+ return {
383
+ itemDelaySeconds: delaySecondsByItemIndex[itemIndex] ?? 0,
384
+ totalTimelineSpan,
385
+ };
386
+ }
387
+
388
+ export function clampTextMotionProgress(progress: number): number {
389
+ 'worklet';
390
+
391
+ if (!Number.isFinite(progress)) {
392
+ return 0;
393
+ }
394
+
395
+ return Math.min(1, Math.max(0, progress));
396
+ }
397
+
398
+ export function createTextMotionControlledTimelineSpan(
399
+ delaySecondsByItemIndex: readonly number[],
400
+ ): number {
401
+ const maxDelaySeconds = delaySecondsByItemIndex.reduce(
402
+ (maxDelay, delaySeconds) => Math.max(maxDelay, delaySeconds),
403
+ 0,
404
+ );
405
+
406
+ return maxDelaySeconds + TEXT_MOTION_CONTROLLED_ITEM_TIMELINE_SPAN;
407
+ }
408
+
409
+ export function mapTextMotionControlledProgressToItemProgress({
410
+ progress,
411
+ itemDelaySeconds,
412
+ totalTimelineSpan,
413
+ }: TextMotionControlledProgressInput): number {
414
+ 'worklet';
415
+
416
+ const virtualTime = clampTextMotionProgress(progress) * totalTimelineSpan;
417
+ const itemProgress = (virtualTime - itemDelaySeconds) / TEXT_MOTION_CONTROLLED_ITEM_TIMELINE_SPAN;
418
+
419
+ return clampTextMotionProgress(itemProgress);
420
+ }
421
+
422
+ export function resolveTextMotionReduceMotionConfig(
423
+ reducedMotion: TextMotionItemMotion['reducedMotion'],
424
+ ) {
425
+ return REDUCE_MOTION_CONFIG_BY_POLICY[reducedMotion];
426
+ }
427
+
428
+ export function shouldRenderTextMotionFinalState(
429
+ reducedMotionEnabled: boolean,
430
+ reducedMotion: TextMotionItemMotion['reducedMotion'],
431
+ ): boolean {
432
+ return reducedMotionEnabled && reducedMotion !== 'system';
433
+ }
434
+
435
+ export function createTextMotionProgressAnimation({
436
+ delayMs,
437
+ motion,
438
+ reducedMotion,
439
+ }: Pick<TextMotionItemMotion, 'delayMs' | 'motion' | 'reducedMotion'>): number {
440
+ const reduceMotion = resolveTextMotionReduceMotionConfig(reducedMotion);
441
+
442
+ if (motion?.kind === 'spring') {
443
+ const animation = withSpring(1, {
444
+ ...motion.options,
445
+ reduceMotion,
446
+ });
447
+
448
+ return applyDelay(animation, delayMs, reduceMotion);
449
+ }
450
+
451
+ const timingOptions = motion?.kind === 'timing' ? motion.options : undefined;
452
+ const animation = withTiming(1, {
453
+ duration: 300,
454
+ ...timingOptions,
455
+ reduceMotion,
456
+ });
457
+
458
+ return applyDelay(animation, delayMs, reduceMotion);
459
+ }
460
+
461
+ export function readTextMotionProgressForAnimatedStyle({
462
+ controlledProgressPlan,
463
+ progress,
464
+ renderFinalState,
465
+ }: {
466
+ controlledProgressPlan?: TextMotionControlledProgressPlan;
467
+ progress: SharedValue<number>;
468
+ renderFinalState: boolean;
469
+ }): number {
470
+ 'worklet';
471
+
472
+ const rawProgress = renderFinalState ? 1 : progress.value;
473
+
474
+ if (!controlledProgressPlan) {
475
+ return Number.isFinite(rawProgress) ? rawProgress : 0;
476
+ }
477
+
478
+ return mapTextMotionControlledProgressToItemProgress({
479
+ progress: rawProgress,
480
+ itemDelaySeconds: controlledProgressPlan.itemDelaySeconds,
481
+ totalTimelineSpan: controlledProgressPlan.totalTimelineSpan,
482
+ });
483
+ }
@@ -0,0 +1,26 @@
1
+ import type { TextMotionSourceRange, TextMotionToken, TextMotionTokenUnit } from '../types';
2
+
3
+ type CreateTokenInput<Unit extends TextMotionTokenUnit> = {
4
+ index: number;
5
+ metadata?: Record<string, unknown>;
6
+ sourceRange: TextMotionSourceRange;
7
+ text: string;
8
+ unit: Unit;
9
+ };
10
+
11
+ export function createTextMotionToken<Unit extends TextMotionTokenUnit>({
12
+ index,
13
+ metadata,
14
+ sourceRange,
15
+ text,
16
+ unit,
17
+ }: CreateTokenInput<Unit>): TextMotionToken<Unit> {
18
+ return {
19
+ id: `${unit}-${index}-${sourceRange.start}-${sourceRange.end}`,
20
+ index,
21
+ metadata,
22
+ sourceRange,
23
+ text,
24
+ unit,
25
+ };
26
+ }
@@ -0,0 +1,92 @@
1
+ import type {
2
+ TextMotionSourceRange,
3
+ TextMotionSplitContext,
4
+ TextMotionSplitter,
5
+ TextMotionToken,
6
+ } from '../types';
7
+
8
+ import { createTextMotionSplitterHandle } from '../recipe/descriptors';
9
+ import { createTextMotionToken } from './createToken';
10
+
11
+ /** Token object returned by a custom splitter when source metadata matters. */
12
+ export type TextMotionCustomSplitToken = {
13
+ /** Optional metadata copied into the generated token. */
14
+ metadata?: Record<string, unknown>;
15
+ /** Explicit source range in the original input. */
16
+ sourceRange?: TextMotionSourceRange;
17
+ /** Text content for the token. */
18
+ text: string;
19
+ };
20
+
21
+ /** Result item returned by a custom splitter. */
22
+ export type TextMotionCustomSplitResult = string | TextMotionCustomSplitToken;
23
+
24
+ /** Custom split function used by {@link custom}. */
25
+ export type TextMotionCustomSplit = (
26
+ input: string,
27
+ context?: TextMotionSplitContext,
28
+ ) => readonly TextMotionCustomSplitResult[];
29
+
30
+ type CustomSplitAccumulator = {
31
+ cursor: number;
32
+ tokens: TextMotionToken<'custom'>[];
33
+ };
34
+
35
+ function findSourceRange(input: string, text: string, cursor: number) {
36
+ const start = input.indexOf(text, cursor);
37
+ const safeStart = start >= 0 ? start : cursor;
38
+
39
+ return {
40
+ end: safeStart + text.length,
41
+ start: safeStart,
42
+ };
43
+ }
44
+
45
+ function normalizeCustomSplitResult(
46
+ input: string,
47
+ result: TextMotionCustomSplitResult,
48
+ index: number,
49
+ cursor: number,
50
+ ) {
51
+ const text = typeof result === 'string' ? result : result.text;
52
+ const sourceRange =
53
+ typeof result === 'string' || !result.sourceRange
54
+ ? findSourceRange(input, text, cursor)
55
+ : result.sourceRange;
56
+
57
+ return {
58
+ cursor: sourceRange.end,
59
+ token: createTextMotionToken({
60
+ index,
61
+ metadata: typeof result === 'string' ? undefined : result.metadata,
62
+ sourceRange,
63
+ text,
64
+ unit: 'custom',
65
+ }),
66
+ };
67
+ }
68
+
69
+ /** Build a splitter from an app-defined split function. */
70
+ export function custom(split: TextMotionCustomSplit): TextMotionSplitter<'custom'> {
71
+ return createTextMotionSplitterHandle({
72
+ kind: 'custom',
73
+ split(input, context): readonly TextMotionToken<'custom'>[] {
74
+ const initialAccumulator: CustomSplitAccumulator = {
75
+ cursor: 0,
76
+ tokens: [],
77
+ };
78
+
79
+ return split(input, context).reduce((accumulator, result, index) => {
80
+ const normalized = normalizeCustomSplitResult(input, result, index, accumulator.cursor);
81
+
82
+ // Keep the token accumulator mutable so custom split normalization stays linear.
83
+ accumulator.tokens.push(normalized.token);
84
+
85
+ return {
86
+ cursor: normalized.cursor,
87
+ tokens: accumulator.tokens,
88
+ };
89
+ }, initialAccumulator).tokens;
90
+ },
91
+ });
92
+ }
@@ -0,0 +1,27 @@
1
+ import type { TextMotionSplitter } from '../types';
2
+
3
+ import { createTextMotionSplitterHandle } from '../recipe/descriptors';
4
+ import { createTextMotionToken } from './createToken';
5
+ import { createSegmenter } from './segmenter';
6
+
7
+ /** Split text into grapheme-aware tokens for character-level motion. */
8
+ export function graphemes(): TextMotionSplitter<'grapheme'> {
9
+ return createTextMotionSplitterHandle({
10
+ kind: 'grapheme',
11
+ split(input, context) {
12
+ const segmenter = createSegmenter('grapheme', context?.locale);
13
+
14
+ return Array.from(segmenter.segment(input), (segment, index) =>
15
+ createTextMotionToken({
16
+ index,
17
+ sourceRange: {
18
+ start: segment.index,
19
+ end: segment.index + segment.segment.length,
20
+ },
21
+ text: segment.segment,
22
+ unit: 'grapheme',
23
+ }),
24
+ );
25
+ },
26
+ });
27
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ custom,
3
+ type TextMotionCustomSplit,
4
+ type TextMotionCustomSplitResult,
5
+ type TextMotionCustomSplitToken,
6
+ } from './custom';
7
+ export { graphemes } from './graphemes';
8
+ export { lines } from './lines';
9
+ export { words } from './words';
@@ -0,0 +1,35 @@
1
+ import type { TextMotionSplitter, TextMotionToken } from '../types';
2
+
3
+ import { createTextMotionSplitterHandle } from '../recipe/descriptors';
4
+ import { createTextMotionToken } from './createToken';
5
+
6
+ const LINE_SEGMENT_PATTERN = /[^\n]*\n|[^\n]+/g;
7
+
8
+ function lineParts(input: string) {
9
+ if (input.length === 0) {
10
+ return [{ start: 0, text: '' }];
11
+ }
12
+
13
+ return Array.from(input.matchAll(LINE_SEGMENT_PATTERN), (match) => ({
14
+ start: match.index ?? 0,
15
+ text: match[0] ?? '',
16
+ }));
17
+ }
18
+
19
+ /** Split on newline boundaries. This is experimental and does not measure rendered line wraps. */
20
+ export function lines(): TextMotionSplitter<'line'> {
21
+ return createTextMotionSplitterHandle({
22
+ kind: 'line',
23
+ split(input): readonly TextMotionToken<'line'>[] {
24
+ return lineParts(input).map(({ start, text }, index) =>
25
+ createTextMotionToken({
26
+ index,
27
+ metadata: { experimental: true, mapping: 'newline-only' },
28
+ sourceRange: { start, end: start + text.length },
29
+ text,
30
+ unit: 'line',
31
+ }),
32
+ );
33
+ },
34
+ });
35
+ }