react-native-ai-animations 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,7 +41,7 @@ prompts, on the [website](https://nami-motion-web.vercel.app/).
41
41
  | Component | Entry | What it does |
42
42
  | --- | --- | --- |
43
43
  | Shimmer Text | `/text` | A light band sweeping across text while a response is being generated. |
44
- | Streaming Text | `/text` | Words of a streaming answer fade and rise in, with a blinking caret. |
44
+ | Streaming Text | `/text` | A streaming answer fades in chunk by chunk with native text layout and a blinking caret. |
45
45
  | Spoken Text | `/text` | Highlights the word being spoken and dims the words already read aloud, for voice replies. |
46
46
  | Spinning Mark | `/brand` | Spins any icon you pass (your logo) with optional breathing and entrance. |
47
47
  | Status Row | `/chat` | Pulsing icon, shimmering label and elapsed timer for agent status. |
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
 
3
- import { isValidElement, memo, useEffect, useMemo } from "react";
3
+ import { isValidElement, memo, useEffect, useMemo, useState } from "react";
4
4
  import { StyleSheet, Text, View } from "react-native";
5
- import Animated, { cancelAnimation, Easing, useAnimatedStyle, useReducedMotion, useSharedValue, withRepeat, withSequence, withTiming } from "react-native-reanimated";
5
+ import Animated, { cancelAnimation, useAnimatedStyle, useReducedMotion, useSharedValue, withRepeat, withSequence, withTiming } from "react-native-reanimated";
6
+ import { withAlpha } from "../internal/color.js";
6
7
  import { useMotionPalette } from "../theme/provider.js";
7
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
9
  const DEFAULT_TEXT_STYLE = {
@@ -10,14 +11,85 @@ const DEFAULT_TEXT_STYLE = {
10
11
  lineHeight: 24
11
12
  };
12
13
  const DEFAULT_FADE = 360;
13
- const DEFAULT_RISE = 6;
14
+ const FRAME_MS = 32;
14
15
  const CARET_BLINK = 530;
15
- function tokenize(paragraph) {
16
- const parts = paragraph.match(/\S+\s*/g) ?? [];
17
- return parts.map((value, key) => ({
18
- value,
19
- key
20
- }));
16
+ function easeOut(progress) {
17
+ const clamped = Math.min(1, Math.max(0, progress));
18
+ return 1 - (1 - clamped) ** 3;
19
+ }
20
+ function commonPrefix(a, b) {
21
+ const limit = Math.min(a.length, b.length);
22
+ let index = 0;
23
+ while (index < limit && a.charCodeAt(index) === b.charCodeAt(index)) index += 1;
24
+ return index;
25
+ }
26
+ function paragraphRanges(text) {
27
+ const ranges = [];
28
+ const breaks = /\n{2,}/g;
29
+ let start = 0;
30
+ let match = breaks.exec(text);
31
+ while (match) {
32
+ ranges.push({
33
+ start,
34
+ end: match.index
35
+ });
36
+ start = match.index + match[0].length;
37
+ match = breaks.exec(text);
38
+ }
39
+ ranges.push({
40
+ start,
41
+ end: text.length
42
+ });
43
+ return ranges;
44
+ }
45
+ function settledReveal(text) {
46
+ return {
47
+ text,
48
+ settled: text.length,
49
+ chunks: []
50
+ };
51
+ }
52
+ function nextReveal(previous, text, born) {
53
+ if (text === previous.text) return previous;
54
+ const common = commonPrefix(previous.text, text);
55
+ const chunks = previous.chunks.filter(chunk => chunk.start < common).map(chunk => chunk.end > common ? {
56
+ ...chunk,
57
+ end: common
58
+ } : chunk);
59
+ if (text.length > common) chunks.push({
60
+ start: common,
61
+ end: text.length,
62
+ born
63
+ });
64
+ return {
65
+ text,
66
+ settled: Math.min(previous.settled, common),
67
+ chunks
68
+ };
69
+ }
70
+ function revealPieces(reveal, length, now, fadeDuration) {
71
+ const pieces = [];
72
+ if (reveal.settled > 0) pieces.push({
73
+ start: 0,
74
+ end: reveal.settled,
75
+ alpha: 1
76
+ });
77
+ let cursor = reveal.settled;
78
+ for (const chunk of reveal.chunks) {
79
+ const end = Math.min(chunk.end, length);
80
+ if (end > cursor) pieces.push({
81
+ start: cursor,
82
+ end,
83
+ alpha: easeOut((now - chunk.born) / fadeDuration)
84
+ });
85
+ cursor = Math.max(cursor, end);
86
+ }
87
+ if (cursor < length) pieces.push({
88
+ start: cursor,
89
+ end: length,
90
+ alpha: 0
91
+ });
92
+ return pieces;
21
93
  }
22
94
  export function StreamingText({
23
95
  text,
@@ -25,17 +97,53 @@ export function StreamingText({
25
97
  textStyle = DEFAULT_TEXT_STYLE,
26
98
  paragraphGap = 12,
27
99
  fadeDuration = DEFAULT_FADE,
28
- rise = DEFAULT_RISE,
29
100
  caret = true,
101
+ selectable = false,
30
102
  colors,
31
103
  style
32
104
  }) {
33
105
  const palette = useMotionPalette(colors);
34
106
  const reduceMotion = useReducedMotion();
35
- const paragraphs = useMemo(() => text.split(/\n{2,}/), [text]);
36
- const animate = streaming && !reduceMotion;
107
+ const animate = !reduceMotion && fadeDuration > 0;
108
+ const [reveal, setReveal] = useState(() => streaming && animate ? settledReveal("") : settledReveal(text));
109
+ const [now, setNow] = useState(() => Date.now());
110
+ const fading = reveal.chunks.length > 0;
37
111
  const color = palette.text;
38
- const caretNode = caret === false ? null : /*#__PURE__*/isValidElement(caret) ? caret : /*#__PURE__*/_jsx(Caret, {
112
+ useEffect(() => {
113
+ if (!streaming || !animate) {
114
+ setReveal(previous => previous.text === text ? previous : settledReveal(text));
115
+ return;
116
+ }
117
+ const born = Date.now();
118
+ setNow(born);
119
+ setReveal(previous => nextReveal(previous, text, born));
120
+ }, [text, streaming, animate]);
121
+ useEffect(() => {
122
+ if (!fading) return;
123
+ const timer = setInterval(() => {
124
+ const time = Date.now();
125
+ setNow(time);
126
+ setReveal(previous => {
127
+ const live = previous.chunks.filter(chunk => time - chunk.born < fadeDuration);
128
+ if (live.length === previous.chunks.length) return previous;
129
+ const done = previous.chunks[previous.chunks.length - live.length - 1];
130
+ return {
131
+ ...previous,
132
+ settled: Math.max(previous.settled, done.end),
133
+ chunks: live
134
+ };
135
+ });
136
+ }, FRAME_MS);
137
+ return () => clearInterval(timer);
138
+ }, [fading, fadeDuration]);
139
+ const current = streaming ? nextReveal(reveal, text, Number.POSITIVE_INFINITY) : reveal.text === text ? reveal : settledReveal(text);
140
+ const pieces = animate ? revealPieces(current, text.length, now, fadeDuration) : [{
141
+ start: 0,
142
+ end: text.length,
143
+ alpha: 1
144
+ }];
145
+ const paragraphs = useMemo(() => paragraphRanges(text), [text]);
146
+ const caretNode = !streaming || caret === false ? null : /*#__PURE__*/isValidElement(caret) ? caret : /*#__PURE__*/_jsx(Caret, {
39
147
  color: color,
40
148
  blink: !reduceMotion,
41
149
  textStyle: textStyle
@@ -44,60 +152,48 @@ export function StreamingText({
44
152
  style: [{
45
153
  gap: paragraphGap
46
154
  }, style],
47
- children: paragraphs.map((paragraph, index) => {
155
+ children: paragraphs.map((range, index) => {
48
156
  const last = index === paragraphs.length - 1;
49
- if (!animate || !last) {
50
- return /*#__PURE__*/_jsxs(Text, {
51
- style: [textStyle, {
52
- color
53
- }],
54
- children: [paragraph, last && streaming && caretNode]
55
- }, index);
56
- }
57
- return /*#__PURE__*/_jsxs(View, {
58
- accessible: true,
59
- accessibilityRole: "text",
60
- accessibilityLabel: paragraph,
61
- style: {
62
- flexDirection: "row",
63
- flexWrap: "wrap",
64
- alignItems: "flex-end"
65
- },
66
- children: [tokenize(paragraph).map(token => /*#__PURE__*/_jsx(StreamingWord, {
67
- value: token.value,
157
+ const inside = pieces.filter(piece => piece.end > range.start && piece.start < range.end);
158
+ const plain = inside.length <= 1 && (inside[0]?.alpha ?? 1) >= 1;
159
+ if (plain && !last) {
160
+ return /*#__PURE__*/_jsx(PlainParagraph, {
161
+ value: text.slice(range.start, range.end),
68
162
  textStyle: textStyle,
69
163
  color: color,
70
- fadeDuration: fadeDuration,
71
- rise: rise
72
- }, token.key)), caretNode]
164
+ selectable: selectable
165
+ }, index);
166
+ }
167
+ return /*#__PURE__*/_jsxs(Text, {
168
+ style: [textStyle, {
169
+ color
170
+ }],
171
+ selectable: selectable,
172
+ children: [inside.map(piece => {
173
+ const value = text.slice(Math.max(piece.start, range.start), Math.min(piece.end, range.end));
174
+ if (piece.alpha >= 1) return value;
175
+ return /*#__PURE__*/_jsx(Text, {
176
+ style: {
177
+ color: withAlpha(color, piece.alpha)
178
+ },
179
+ children: value
180
+ }, piece.start);
181
+ }), last && caretNode]
73
182
  }, index);
74
183
  })
75
184
  });
76
185
  }
77
- const StreamingWord = /*#__PURE__*/memo(function StreamingWord({
186
+ const PlainParagraph = /*#__PURE__*/memo(function PlainParagraph({
78
187
  value,
79
188
  textStyle,
80
189
  color,
81
- fadeDuration,
82
- rise
190
+ selectable
83
191
  }) {
84
- const progress = useSharedValue(0);
85
- useEffect(() => {
86
- progress.value = withTiming(1, {
87
- duration: fadeDuration,
88
- easing: Easing.out(Easing.cubic)
89
- });
90
- }, [fadeDuration, progress]);
91
- const animatedStyle = useAnimatedStyle(() => ({
92
- opacity: progress.value,
93
- transform: [{
94
- translateY: rise * (1 - progress.value)
95
- }]
96
- }));
97
- return /*#__PURE__*/_jsx(Animated.Text, {
192
+ return /*#__PURE__*/_jsx(Text, {
98
193
  style: [textStyle, {
99
194
  color
100
- }, animatedStyle],
195
+ }],
196
+ selectable: selectable,
101
197
  children: value
102
198
  });
103
199
  });
@@ -1 +1 @@
1
- {"version":3,"names":["isValidElement","memo","useEffect","useMemo","StyleSheet","Text","View","Animated","cancelAnimation","Easing","useAnimatedStyle","useReducedMotion","useSharedValue","withRepeat","withSequence","withTiming","useMotionPalette","jsx","_jsx","jsxs","_jsxs","DEFAULT_TEXT_STYLE","fontSize","lineHeight","DEFAULT_FADE","DEFAULT_RISE","CARET_BLINK","tokenize","paragraph","parts","match","map","value","key","StreamingText","text","streaming","textStyle","paragraphGap","fadeDuration","rise","caret","colors","style","palette","reduceMotion","paragraphs","split","animate","color","caretNode","Caret","blink","gap","children","index","last","length","accessible","accessibilityRole","accessibilityLabel","flexDirection","flexWrap","alignItems","token","StreamingWord","progress","duration","easing","out","cubic","animatedStyle","opacity","transform","translateY","flat","flatten","height","Math","round","width","borderRadius","backgroundColor","marginLeft","marginBottom"],"sourceRoot":"../../../src","sources":["text/streaming-text.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["isValidElement","memo","useEffect","useMemo","useState","StyleSheet","Text","View","Animated","cancelAnimation","useAnimatedStyle","useReducedMotion","useSharedValue","withRepeat","withSequence","withTiming","withAlpha","useMotionPalette","jsx","_jsx","jsxs","_jsxs","DEFAULT_TEXT_STYLE","fontSize","lineHeight","DEFAULT_FADE","FRAME_MS","CARET_BLINK","easeOut","progress","clamped","Math","min","max","commonPrefix","a","b","limit","length","index","charCodeAt","paragraphRanges","text","ranges","breaks","start","match","exec","push","end","settledReveal","settled","chunks","nextReveal","previous","born","common","filter","chunk","map","revealPieces","reveal","now","fadeDuration","pieces","alpha","cursor","StreamingText","streaming","textStyle","paragraphGap","caret","selectable","colors","style","palette","reduceMotion","animate","setReveal","setNow","Date","fading","color","timer","setInterval","time","live","done","clearInterval","current","Number","POSITIVE_INFINITY","paragraphs","caretNode","Caret","blink","gap","children","range","last","inside","piece","plain","PlainParagraph","value","slice","opacity","flat","flatten","height","round","duration","animatedStyle","width","borderRadius","backgroundColor","marginLeft","marginBottom"],"sourceRoot":"../../../src","sources":["text/streaming-text.tsx"],"mappings":"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","ignoreList":[]}
@@ -7,10 +7,10 @@ export type StreamingTextProps = {
7
7
  textStyle?: StyleProp<TextStyle>;
8
8
  paragraphGap?: number;
9
9
  fadeDuration?: number;
10
- rise?: number;
11
10
  caret?: boolean | ReactNode;
11
+ selectable?: boolean;
12
12
  colors?: Partial<MotionPalette>;
13
13
  style?: StyleProp<ViewStyle>;
14
14
  };
15
- export declare function StreamingText({ text, streaming, textStyle, paragraphGap, fadeDuration, rise, caret, colors, style, }: StreamingTextProps): import("react").JSX.Element;
15
+ export declare function StreamingText({ text, streaming, textStyle, paragraphGap, fadeDuration, caret, selectable, colors, style, }: StreamingTextProps): import("react").JSX.Element;
16
16
  //# sourceMappingURL=streaming-text.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"streaming-text.d.ts","sourceRoot":"","sources":["../../../../src/text/streaming-text.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA4C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjF,OAAO,EAA0B,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAWtG,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAkB,CAAC;AAQtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AASF,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,SAAS,EACT,SAA8B,EAC9B,YAAiB,EACjB,YAA2B,EAC3B,IAAmB,EACnB,KAAY,EACZ,MAAM,EACN,KAAK,GACN,EAAE,kBAAkB,+BA4CpB"}
1
+ {"version":3,"file":"streaming-text.d.ts","sourceRoot":"","sources":["../../../../src/text/streaming-text.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3F,OAAO,EAA0B,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAWtG,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAkB,CAAC;AAQtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AA+DF,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,SAAS,EACT,SAA8B,EAC9B,YAAiB,EACjB,YAA2B,EAC3B,KAAY,EACZ,UAAkB,EAClB,MAAM,EACN,KAAK,GACN,EAAE,kBAAkB,+BA0EpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ai-animations",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Animated React Native and Expo components for AI chat apps: streaming text, shimmer loading text, thinking and status indicators, voice orbs and waveforms, image and video generation loaders, question cards and reasoning effort bars. Built on Reanimated and Skia.",
5
5
  "license": "MIT",
6
6
  "author": "Ahmet Kar",
@@ -1,8 +1,7 @@
1
- import { isValidElement, memo, useEffect, useMemo, type ReactNode } from "react";
1
+ import { isValidElement, memo, useEffect, useMemo, useState, type ReactNode } from "react";
2
2
  import { StyleSheet, Text, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native";
3
3
  import Animated, {
4
4
  cancelAnimation,
5
- Easing,
6
5
  useAnimatedStyle,
7
6
  useReducedMotion,
8
7
  useSharedValue,
@@ -10,12 +9,13 @@ import Animated, {
10
9
  withSequence,
11
10
  withTiming,
12
11
  } from "react-native-reanimated";
12
+ import { withAlpha } from "../internal/color";
13
13
  import { type MotionPalette } from "../theme/palette";
14
14
  import { useMotionPalette } from "../theme/provider";
15
15
 
16
16
  const DEFAULT_TEXT_STYLE: TextStyle = { fontSize: 16, lineHeight: 24 };
17
17
  const DEFAULT_FADE = 360;
18
- const DEFAULT_RISE = 6;
18
+ const FRAME_MS = 32;
19
19
  const CARET_BLINK = 530;
20
20
 
21
21
  export type StreamingTextProps = {
@@ -24,17 +24,71 @@ export type StreamingTextProps = {
24
24
  textStyle?: StyleProp<TextStyle>;
25
25
  paragraphGap?: number;
26
26
  fadeDuration?: number;
27
- rise?: number;
28
27
  caret?: boolean | ReactNode;
28
+ selectable?: boolean;
29
29
  colors?: Partial<MotionPalette>;
30
30
  style?: StyleProp<ViewStyle>;
31
31
  };
32
32
 
33
- type Token = { value: string; key: number };
33
+ type Chunk = { start: number; end: number; born: number };
34
34
 
35
- function tokenize(paragraph: string): Token[] {
36
- const parts = paragraph.match(/\S+\s*/g) ?? [];
37
- return parts.map((value, key) => ({ value, key }));
35
+ type Reveal = { text: string; settled: number; chunks: Chunk[] };
36
+
37
+ type Range = { start: number; end: number };
38
+
39
+ type Piece = Range & { alpha: number };
40
+
41
+ function easeOut(progress: number) {
42
+ const clamped = Math.min(1, Math.max(0, progress));
43
+ return 1 - (1 - clamped) ** 3;
44
+ }
45
+
46
+ function commonPrefix(a: string, b: string) {
47
+ const limit = Math.min(a.length, b.length);
48
+ let index = 0;
49
+ while (index < limit && a.charCodeAt(index) === b.charCodeAt(index)) index += 1;
50
+ return index;
51
+ }
52
+
53
+ function paragraphRanges(text: string): Range[] {
54
+ const ranges: Range[] = [];
55
+ const breaks = /\n{2,}/g;
56
+ let start = 0;
57
+ let match = breaks.exec(text);
58
+ while (match) {
59
+ ranges.push({ start, end: match.index });
60
+ start = match.index + match[0].length;
61
+ match = breaks.exec(text);
62
+ }
63
+ ranges.push({ start, end: text.length });
64
+ return ranges;
65
+ }
66
+
67
+ function settledReveal(text: string): Reveal {
68
+ return { text, settled: text.length, chunks: [] };
69
+ }
70
+
71
+ function nextReveal(previous: Reveal, text: string, born: number): Reveal {
72
+ if (text === previous.text) return previous;
73
+ const common = commonPrefix(previous.text, text);
74
+ const chunks = previous.chunks
75
+ .filter((chunk) => chunk.start < common)
76
+ .map((chunk) => (chunk.end > common ? { ...chunk, end: common } : chunk));
77
+ if (text.length > common) chunks.push({ start: common, end: text.length, born });
78
+ return { text, settled: Math.min(previous.settled, common), chunks };
79
+ }
80
+
81
+ function revealPieces(reveal: Reveal, length: number, now: number, fadeDuration: number): Piece[] {
82
+ const pieces: Piece[] = [];
83
+ if (reveal.settled > 0) pieces.push({ start: 0, end: reveal.settled, alpha: 1 });
84
+ let cursor = reveal.settled;
85
+ for (const chunk of reveal.chunks) {
86
+ const end = Math.min(chunk.end, length);
87
+ if (end > cursor) pieces.push({ start: cursor, end, alpha: easeOut((now - chunk.born) / fadeDuration) });
88
+ cursor = Math.max(cursor, end);
89
+ }
90
+ if (cursor < length) pieces.push({ start: cursor, end: length, alpha: 0 });
91
+ return pieces;
38
92
  }
39
93
 
40
94
  export function StreamingText({
@@ -43,77 +97,99 @@ export function StreamingText({
43
97
  textStyle = DEFAULT_TEXT_STYLE,
44
98
  paragraphGap = 12,
45
99
  fadeDuration = DEFAULT_FADE,
46
- rise = DEFAULT_RISE,
47
100
  caret = true,
101
+ selectable = false,
48
102
  colors,
49
103
  style,
50
104
  }: StreamingTextProps) {
51
105
  const palette = useMotionPalette(colors);
52
106
  const reduceMotion = useReducedMotion();
53
- const paragraphs = useMemo(() => text.split(/\n{2,}/), [text]);
54
- const animate = streaming && !reduceMotion;
107
+ const animate = !reduceMotion && fadeDuration > 0;
108
+ const [reveal, setReveal] = useState<Reveal>(() => (streaming && animate ? settledReveal("") : settledReveal(text)));
109
+ const [now, setNow] = useState(() => Date.now());
110
+ const fading = reveal.chunks.length > 0;
55
111
  const color = palette.text;
56
- const caretNode = caret === false ? null : isValidElement(caret) ? caret : <Caret color={color} blink={!reduceMotion} textStyle={textStyle} />;
112
+
113
+ useEffect(() => {
114
+ if (!streaming || !animate) {
115
+ setReveal((previous) => (previous.text === text ? previous : settledReveal(text)));
116
+ return;
117
+ }
118
+ const born = Date.now();
119
+ setNow(born);
120
+ setReveal((previous) => nextReveal(previous, text, born));
121
+ }, [text, streaming, animate]);
122
+
123
+ useEffect(() => {
124
+ if (!fading) return;
125
+ const timer = setInterval(() => {
126
+ const time = Date.now();
127
+ setNow(time);
128
+ setReveal((previous) => {
129
+ const live = previous.chunks.filter((chunk) => time - chunk.born < fadeDuration);
130
+ if (live.length === previous.chunks.length) return previous;
131
+ const done = previous.chunks[previous.chunks.length - live.length - 1];
132
+ return { ...previous, settled: Math.max(previous.settled, done.end), chunks: live };
133
+ });
134
+ }, FRAME_MS);
135
+ return () => clearInterval(timer);
136
+ }, [fading, fadeDuration]);
137
+
138
+ const current = streaming ? nextReveal(reveal, text, Number.POSITIVE_INFINITY) : reveal.text === text ? reveal : settledReveal(text);
139
+ const pieces = animate ? revealPieces(current, text.length, now, fadeDuration) : [{ start: 0, end: text.length, alpha: 1 }];
140
+ const paragraphs = useMemo(() => paragraphRanges(text), [text]);
141
+ const caretNode =
142
+ !streaming || caret === false ? null : isValidElement(caret) ? caret : <Caret color={color} blink={!reduceMotion} textStyle={textStyle} />;
57
143
 
58
144
  return (
59
145
  <View style={[{ gap: paragraphGap }, style]}>
60
- {paragraphs.map((paragraph, index) => {
146
+ {paragraphs.map((range, index) => {
61
147
  const last = index === paragraphs.length - 1;
62
- if (!animate || !last) {
148
+ const inside = pieces.filter((piece) => piece.end > range.start && piece.start < range.end);
149
+ const plain = inside.length <= 1 && (inside[0]?.alpha ?? 1) >= 1;
150
+ if (plain && !last) {
63
151
  return (
64
- <Text key={index} style={[textStyle, { color }]}>
65
- {paragraph}
66
- {last && streaming && caretNode}
67
- </Text>
152
+ <PlainParagraph
153
+ key={index}
154
+ value={text.slice(range.start, range.end)}
155
+ textStyle={textStyle}
156
+ color={color}
157
+ selectable={selectable}
158
+ />
68
159
  );
69
160
  }
70
161
  return (
71
- <View
72
- key={index}
73
- accessible
74
- accessibilityRole="text"
75
- accessibilityLabel={paragraph}
76
- style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "flex-end" }}
77
- >
78
- {tokenize(paragraph).map((token) => (
79
- <StreamingWord
80
- key={token.key}
81
- value={token.value}
82
- textStyle={textStyle}
83
- color={color}
84
- fadeDuration={fadeDuration}
85
- rise={rise}
86
- />
87
- ))}
88
- {caretNode}
89
- </View>
162
+ <Text key={index} style={[textStyle, { color }]} selectable={selectable}>
163
+ {inside.map((piece) => {
164
+ const value = text.slice(Math.max(piece.start, range.start), Math.min(piece.end, range.end));
165
+ if (piece.alpha >= 1) return value;
166
+ return (
167
+ <Text key={piece.start} style={{ color: withAlpha(color, piece.alpha) }}>
168
+ {value}
169
+ </Text>
170
+ );
171
+ })}
172
+ {last && caretNode}
173
+ </Text>
90
174
  );
91
175
  })}
92
176
  </View>
93
177
  );
94
178
  }
95
179
 
96
- type StreamingWordProps = {
180
+ type PlainParagraphProps = {
97
181
  value: string;
98
182
  textStyle: StyleProp<TextStyle>;
99
183
  color: string;
100
- fadeDuration: number;
101
- rise: number;
184
+ selectable: boolean;
102
185
  };
103
186
 
104
- const StreamingWord = memo(function StreamingWord({ value, textStyle, color, fadeDuration, rise }: StreamingWordProps) {
105
- const progress = useSharedValue(0);
106
-
107
- useEffect(() => {
108
- progress.value = withTiming(1, { duration: fadeDuration, easing: Easing.out(Easing.cubic) });
109
- }, [fadeDuration, progress]);
110
-
111
- const animatedStyle = useAnimatedStyle(() => ({
112
- opacity: progress.value,
113
- transform: [{ translateY: rise * (1 - progress.value) }],
114
- }));
115
-
116
- return <Animated.Text style={[textStyle, { color }, animatedStyle]}>{value}</Animated.Text>;
187
+ const PlainParagraph = memo(function PlainParagraph({ value, textStyle, color, selectable }: PlainParagraphProps) {
188
+ return (
189
+ <Text style={[textStyle, { color }]} selectable={selectable}>
190
+ {value}
191
+ </Text>
192
+ );
117
193
  });
118
194
 
119
195
  function Caret({ color, blink, textStyle }: { color: string; blink: boolean; textStyle: StyleProp<TextStyle> }) {