react-native-teleflow-prompter 1.0.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/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +195 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muhammet Atmaca (https://muhammetatmaca.com.tr)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# react-native-teleflow-prompter
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-teleflow-prompter)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://reactnative.dev/)
|
|
7
|
+
[](https://expo.dev/)
|
|
8
|
+
|
|
9
|
+
> High-performance 60 FPS teleprompter scrolling engine, Words-Per-Minute (WPM) speed calculator, speech timing math, and mirror optics transform generator for **React Native**, **Expo**, and **Web**.
|
|
10
|
+
|
|
11
|
+
Extracted from and powering the production mobile app [**TeleFlow Prompter**](https://muhammetatmaca.com.tr/apps/teleflow-prompter) by [**Muhammet Atmaca**](https://muhammetatmaca.com.tr/) ([VirelonSoft](https://www.linkedin.com/company/virelonsoft/)).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- 🏎️ **Butter-Smooth 60 FPS Scrolling**: Frame-independent delta timing via `requestAnimationFrame` preventing jitter on low-end devices.
|
|
18
|
+
- ⏱️ **Words-Per-Minute (WPM) Calculator**: Compute exact speech durations based on conversational, presentation, or reading speeds (120–180 WPM).
|
|
19
|
+
- 🪞 **Beam-Splitter Mirror Optics**: Native style transformations (`scaleX: -1`, `scaleY: -1`) for physical teleprompter glass reflection rigs.
|
|
20
|
+
- 🧩 **Zero Dependencies**: Pure TypeScript, ultra-lightweight (< 3 kB gzipped).
|
|
21
|
+
- 📱 **Universal Compatibility**: Works out of the box with React Native (Bare & Expo), React Web, Next.js, and Node.js.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# npm
|
|
29
|
+
npm install react-native-teleflow-prompter
|
|
30
|
+
|
|
31
|
+
# pnpm
|
|
32
|
+
pnpm add react-native-teleflow-prompter
|
|
33
|
+
|
|
34
|
+
# yarn
|
|
35
|
+
yarn add react-native-teleflow-prompter
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 🚀 Quick Start (React Native & Expo)
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import React, { useRef, useEffect } from 'react';
|
|
44
|
+
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
|
|
45
|
+
import { useTeleprompter, getMirrorTransform } from 'react-native-teleflow-prompter';
|
|
46
|
+
|
|
47
|
+
export default function PrompterScreen() {
|
|
48
|
+
const scrollViewRef = useRef<ScrollView>(null);
|
|
49
|
+
|
|
50
|
+
const script = `
|
|
51
|
+
Welcome to this presentation. Today we are demonstrating
|
|
52
|
+
the TeleFlow teleprompter engine running smoothly at 60 FPS
|
|
53
|
+
with automatic Words-Per-Minute speed calculation.
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const {
|
|
57
|
+
isPlaying,
|
|
58
|
+
scrollOffset,
|
|
59
|
+
progress,
|
|
60
|
+
elapsedSeconds,
|
|
61
|
+
remainingSeconds,
|
|
62
|
+
totalDurationSeconds,
|
|
63
|
+
wpm,
|
|
64
|
+
toggle,
|
|
65
|
+
reset,
|
|
66
|
+
setWpm,
|
|
67
|
+
} = useTeleprompter({
|
|
68
|
+
text: script,
|
|
69
|
+
initialWpm: 145,
|
|
70
|
+
contentHeight: 1200,
|
|
71
|
+
viewportHeight: 500,
|
|
72
|
+
onComplete: () => console.log('Speech finished!'),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Automatically scroll to calculated pixel offset
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
scrollViewRef.current?.scrollTo({ y: scrollOffset, animated: false });
|
|
78
|
+
}, [scrollOffset]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<View style={styles.container}>
|
|
82
|
+
{/* Mirror transform support for teleprompter glass rigs */}
|
|
83
|
+
<View style={[styles.viewport, getMirrorTransform('none')]}>
|
|
84
|
+
<ScrollView ref={scrollViewRef} scrollEnabled={false}>
|
|
85
|
+
<Text style={styles.scriptText}>{script}</Text>
|
|
86
|
+
</ScrollView>
|
|
87
|
+
</View>
|
|
88
|
+
|
|
89
|
+
{/* Controller HUD */}
|
|
90
|
+
<View style={styles.hud}>
|
|
91
|
+
<Text style={styles.timer}>
|
|
92
|
+
{Math.floor(elapsedSeconds / 60)}:{(elapsedSeconds % 60).toString().padStart(2, '0')} /
|
|
93
|
+
{Math.floor(totalDurationSeconds / 60)}:{(totalDurationSeconds % 60).toString().padStart(2, '0')}
|
|
94
|
+
</Text>
|
|
95
|
+
<Text style={styles.wpm}>{wpm} WPM ({(progress * 100).toFixed(0)}%)</Text>
|
|
96
|
+
|
|
97
|
+
<View style={styles.buttons}>
|
|
98
|
+
<TouchableOpacity onPress={toggle} style={styles.btn}>
|
|
99
|
+
<Text style={styles.btnText}>{isPlaying ? 'Pause' : 'Play'}</Text>
|
|
100
|
+
</TouchableOpacity>
|
|
101
|
+
<TouchableOpacity onPress={reset} style={styles.btn}>
|
|
102
|
+
<Text style={styles.btnText}>Reset</Text>
|
|
103
|
+
</TouchableOpacity>
|
|
104
|
+
<TouchableOpacity onPress={() => setWpm(wpm + 10)} style={styles.btn}>
|
|
105
|
+
<Text style={styles.btnText}>+10 WPM</Text>
|
|
106
|
+
</TouchableOpacity>
|
|
107
|
+
<TouchableOpacity onPress={() => setWpm(wpm - 10)} style={styles.btn}>
|
|
108
|
+
<Text style={styles.btnText}>-10 WPM</Text>
|
|
109
|
+
</TouchableOpacity>
|
|
110
|
+
</View>
|
|
111
|
+
</View>
|
|
112
|
+
</View>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
container: { flex: 1, backgroundColor: '#0f172a' },
|
|
118
|
+
viewport: { height: 500, padding: 24 },
|
|
119
|
+
scriptText: { color: '#f8fafc', fontSize: 32, lineHeight: 48, fontWeight: '600' },
|
|
120
|
+
hud: { padding: 20, borderTopWidth: 1, borderColor: '#334155' },
|
|
121
|
+
timer: { color: '#38bdf8', fontSize: 20, fontWeight: 'bold' },
|
|
122
|
+
wpm: { color: '#94a3b8', fontSize: 14, marginBottom: 12 },
|
|
123
|
+
buttons: { flexDirection: 'row', gap: 10 },
|
|
124
|
+
btn: { backgroundColor: '#2563eb', paddingVertical: 10, paddingHorizontal: 16, borderRadius: 8 },
|
|
125
|
+
btnText: { color: '#ffffff', fontWeight: 'bold' },
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 🛠️ Standalone Calculation Utilities
|
|
132
|
+
|
|
133
|
+
You can use the math utilities without React or React Native:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import {
|
|
137
|
+
countWords,
|
|
138
|
+
calculateDuration,
|
|
139
|
+
calculateWpm,
|
|
140
|
+
calculateScrollVelocity,
|
|
141
|
+
getMirrorTransform,
|
|
142
|
+
chunkScript
|
|
143
|
+
} from 'react-native-teleflow-prompter';
|
|
144
|
+
|
|
145
|
+
// 1. Count words accurately
|
|
146
|
+
const wordCount = countWords("Hello world! Teleprompter math is easy."); // 6
|
|
147
|
+
|
|
148
|
+
// 2. Estimate speech duration (at 150 WPM)
|
|
149
|
+
const durationSeconds = calculateDuration(wordCount, 150); // seconds
|
|
150
|
+
|
|
151
|
+
// 3. Calculate target WPM from fixed video length (e.g., 300 words in 120s)
|
|
152
|
+
const targetWpm = calculateWpm(300, 120); // 150 WPM
|
|
153
|
+
|
|
154
|
+
// 4. Calculate exact scroll velocity (pixels per second)
|
|
155
|
+
const velocity = calculateScrollVelocity(2400, 800, 120); // px/sec
|
|
156
|
+
|
|
157
|
+
// 5. Mirror transform for hardware beam-splitter glass
|
|
158
|
+
const mirrorStyle = getMirrorTransform('horizontal'); // { transform: [{ scaleX: -1 }] }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 🎙️ Words-Per-Minute (WPM) Benchmark Guide
|
|
164
|
+
|
|
165
|
+
| Speech Type | WPM Range | Typical Use Case |
|
|
166
|
+
| :--- | :--- | :--- |
|
|
167
|
+
| **Deliberate / Slow** | 100 – 125 WPM | Formal speeches, technical training, audiobooks |
|
|
168
|
+
| **Conversational** | 130 – 160 WPM | Video podcasts, YouTube videos, YouTube Shorts / Reels |
|
|
169
|
+
| **Rapid / Energetic** | 165 – 190 WPM | TV broadcast reporting, auctioneers, fast-paced marketing |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 👨💻 Author & Credits
|
|
174
|
+
|
|
175
|
+
Developed with ❤️ by **[Muhammet Atmaca](https://muhammetatmaca.com.tr)**:
|
|
176
|
+
- 🌐 **Official Website**: [https://muhammetatmaca.com.tr](https://muhammetatmaca.com.tr)
|
|
177
|
+
- 📱 **TeleFlow Prompter Mobile App**: [View on Portfolio](https://muhammetatmaca.com.tr/apps/teleflow-prompter)
|
|
178
|
+
- 🐙 **GitHub**: [@muhammetatmaca](https://github.com/muhammetatmaca)
|
|
179
|
+
- 💼 **LinkedIn**: [Muhammet Atmaca](https://www.linkedin.com/in/muhammet-atmaca-857481252/)
|
|
180
|
+
- 🏢 **Company**: [VirelonSoft](https://www.linkedin.com/company/virelonsoft/)
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 📄 License
|
|
185
|
+
|
|
186
|
+
MIT © [Muhammet Atmaca](https://muhammetatmaca.com.tr)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* react-native-teleflow-prompter
|
|
3
|
+
*
|
|
4
|
+
* High-performance 60fps teleprompter scroll engine, speech timing math,
|
|
5
|
+
* and mirror optics for React Native, Expo, and Web applications.
|
|
6
|
+
*
|
|
7
|
+
* Developed by Muhammet Atmaca (VirelonSoft)
|
|
8
|
+
* Website: https://muhammetatmaca.com.tr
|
|
9
|
+
* Portfolio App: TeleFlow Prompter (https://muhammetatmaca.com.tr/apps/teleflow-prompter)
|
|
10
|
+
* License: MIT
|
|
11
|
+
*/
|
|
12
|
+
export type MirrorMode = 'none' | 'horizontal' | 'vertical' | 'both';
|
|
13
|
+
export interface ScriptChunk {
|
|
14
|
+
id: number;
|
|
15
|
+
text: string;
|
|
16
|
+
wordCount: number;
|
|
17
|
+
estimatedDurationSeconds: number;
|
|
18
|
+
}
|
|
19
|
+
export interface TeleprompterOptions {
|
|
20
|
+
/** The text content to be displayed and spoken */
|
|
21
|
+
text: string;
|
|
22
|
+
/** Initial speech speed in Words Per Minute (typical: 130 - 170 WPM) */
|
|
23
|
+
initialWpm?: number;
|
|
24
|
+
/** Total height of the scrollable content view in pixels */
|
|
25
|
+
contentHeight?: number;
|
|
26
|
+
/** Height of the visible screen/viewport in pixels */
|
|
27
|
+
viewportHeight?: number;
|
|
28
|
+
/** Auto-scroll frame rate target (default: 60) */
|
|
29
|
+
fps?: number;
|
|
30
|
+
/** Callback fired when scrolling reaches the end */
|
|
31
|
+
onComplete?: () => void;
|
|
32
|
+
}
|
|
33
|
+
export interface TeleprompterController {
|
|
34
|
+
/** Whether the prompter is currently actively scrolling */
|
|
35
|
+
isPlaying: boolean;
|
|
36
|
+
/** Progress through the text from 0.0 to 1.0 */
|
|
37
|
+
progress: number;
|
|
38
|
+
/** Calculated scroll offset in pixels */
|
|
39
|
+
scrollOffset: number;
|
|
40
|
+
/** Elapsed time in seconds */
|
|
41
|
+
elapsedSeconds: number;
|
|
42
|
+
/** Estimated remaining time in seconds */
|
|
43
|
+
remainingSeconds: number;
|
|
44
|
+
/** Total estimated speech duration in seconds */
|
|
45
|
+
totalDurationSeconds: number;
|
|
46
|
+
/** Current speech speed in words per minute */
|
|
47
|
+
wpm: number;
|
|
48
|
+
/** Total word count of the script */
|
|
49
|
+
wordCount: number;
|
|
50
|
+
/** Start scrolling */
|
|
51
|
+
play: () => void;
|
|
52
|
+
/** Pause scrolling */
|
|
53
|
+
pause: () => void;
|
|
54
|
+
/** Toggle play/pause state */
|
|
55
|
+
toggle: () => void;
|
|
56
|
+
/** Reset prompter to start (0 offset) */
|
|
57
|
+
reset: () => void;
|
|
58
|
+
/** Change speed in Words Per Minute */
|
|
59
|
+
setWpm: (newWpm: number) => void;
|
|
60
|
+
/** Seek directly to a progress position (0.0 to 1.0) */
|
|
61
|
+
seek: (progressRatio: number) => void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Counts words accurately across multiple languages and whitespace boundaries.
|
|
65
|
+
*/
|
|
66
|
+
export declare function countWords(text: string): number;
|
|
67
|
+
/**
|
|
68
|
+
* Calculates estimated speech duration in seconds based on word count and WPM.
|
|
69
|
+
*/
|
|
70
|
+
export declare function calculateDuration(wordCount: number, wpm?: number): number;
|
|
71
|
+
/**
|
|
72
|
+
* Calculates the Words Per Minute rate from word count and duration.
|
|
73
|
+
*/
|
|
74
|
+
export declare function calculateWpm(wordCount: number, durationSeconds: number): number;
|
|
75
|
+
/**
|
|
76
|
+
* Calculates pixel scroll velocity (pixels per second) needed to finish
|
|
77
|
+
* the entire content within the target duration.
|
|
78
|
+
*/
|
|
79
|
+
export declare function calculateScrollVelocity(contentHeight: number, viewportHeight: number | undefined, durationSeconds: number): number;
|
|
80
|
+
/**
|
|
81
|
+
* Returns React Native compatible transform style for beam-splitter mirror optics.
|
|
82
|
+
* Physical teleprompter glass reflects text reversed, requiring horizontal or vertical inversion.
|
|
83
|
+
*/
|
|
84
|
+
export declare function getMirrorTransform(mode?: MirrorMode): {
|
|
85
|
+
transform: Array<{
|
|
86
|
+
scaleX?: number;
|
|
87
|
+
scaleY?: number;
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Splits a long script into digestible chunks with word counts and time estimates.
|
|
92
|
+
*/
|
|
93
|
+
export declare function chunkScript(text: string, maxWordsPerChunk?: number): ScriptChunk[];
|
|
94
|
+
/**
|
|
95
|
+
* React Hook providing a high-precision, 60fps auto-scroll controller for teleprompters.
|
|
96
|
+
*/
|
|
97
|
+
export declare function useTeleprompter(options: TeleprompterOptions): TeleprompterController;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* react-native-teleflow-prompter
|
|
4
|
+
*
|
|
5
|
+
* High-performance 60fps teleprompter scroll engine, speech timing math,
|
|
6
|
+
* and mirror optics for React Native, Expo, and Web applications.
|
|
7
|
+
*
|
|
8
|
+
* Developed by Muhammet Atmaca (VirelonSoft)
|
|
9
|
+
* Website: https://muhammetatmaca.com.tr
|
|
10
|
+
* Portfolio App: TeleFlow Prompter (https://muhammetatmaca.com.tr/apps/teleflow-prompter)
|
|
11
|
+
* License: MIT
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.countWords = countWords;
|
|
15
|
+
exports.calculateDuration = calculateDuration;
|
|
16
|
+
exports.calculateWpm = calculateWpm;
|
|
17
|
+
exports.calculateScrollVelocity = calculateScrollVelocity;
|
|
18
|
+
exports.getMirrorTransform = getMirrorTransform;
|
|
19
|
+
exports.chunkScript = chunkScript;
|
|
20
|
+
exports.useTeleprompter = useTeleprompter;
|
|
21
|
+
const react_1 = require("react");
|
|
22
|
+
/**
|
|
23
|
+
* Counts words accurately across multiple languages and whitespace boundaries.
|
|
24
|
+
*/
|
|
25
|
+
function countWords(text) {
|
|
26
|
+
if (!text || typeof text !== 'string')
|
|
27
|
+
return 0;
|
|
28
|
+
const trimmed = text.trim();
|
|
29
|
+
if (!trimmed)
|
|
30
|
+
return 0;
|
|
31
|
+
return trimmed.split(/\s+/).filter(Boolean).length;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Calculates estimated speech duration in seconds based on word count and WPM.
|
|
35
|
+
*/
|
|
36
|
+
function calculateDuration(wordCount, wpm = 140) {
|
|
37
|
+
if (wordCount <= 0 || wpm <= 0)
|
|
38
|
+
return 0;
|
|
39
|
+
return Math.round((wordCount / wpm) * 60);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Calculates the Words Per Minute rate from word count and duration.
|
|
43
|
+
*/
|
|
44
|
+
function calculateWpm(wordCount, durationSeconds) {
|
|
45
|
+
if (wordCount <= 0 || durationSeconds <= 0)
|
|
46
|
+
return 0;
|
|
47
|
+
return Math.round((wordCount / durationSeconds) * 60);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Calculates pixel scroll velocity (pixels per second) needed to finish
|
|
51
|
+
* the entire content within the target duration.
|
|
52
|
+
*/
|
|
53
|
+
function calculateScrollVelocity(contentHeight, viewportHeight = 0, durationSeconds) {
|
|
54
|
+
if (durationSeconds <= 0)
|
|
55
|
+
return 0;
|
|
56
|
+
const scrollableDistance = Math.max(0, contentHeight - viewportHeight);
|
|
57
|
+
return scrollableDistance / durationSeconds;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns React Native compatible transform style for beam-splitter mirror optics.
|
|
61
|
+
* Physical teleprompter glass reflects text reversed, requiring horizontal or vertical inversion.
|
|
62
|
+
*/
|
|
63
|
+
function getMirrorTransform(mode = 'none') {
|
|
64
|
+
switch (mode) {
|
|
65
|
+
case 'horizontal':
|
|
66
|
+
return { transform: [{ scaleX: -1 }] };
|
|
67
|
+
case 'vertical':
|
|
68
|
+
return { transform: [{ scaleY: -1 }] };
|
|
69
|
+
case 'both':
|
|
70
|
+
return { transform: [{ scaleX: -1 }, { scaleY: -1 }] };
|
|
71
|
+
case 'none':
|
|
72
|
+
default:
|
|
73
|
+
return { transform: [{ scaleX: 1 }, { scaleY: 1 }] };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Splits a long script into digestible chunks with word counts and time estimates.
|
|
78
|
+
*/
|
|
79
|
+
function chunkScript(text, maxWordsPerChunk = 40) {
|
|
80
|
+
if (!text)
|
|
81
|
+
return [];
|
|
82
|
+
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
83
|
+
const chunks = [];
|
|
84
|
+
let currentWords = [];
|
|
85
|
+
for (let i = 0; i < words.length; i++) {
|
|
86
|
+
currentWords.push(words[i]);
|
|
87
|
+
const isLastWord = i === words.length - 1;
|
|
88
|
+
const reachedLimit = currentWords.length >= maxWordsPerChunk;
|
|
89
|
+
const endsWithPeriod = /[.!?]$/.test(words[i]);
|
|
90
|
+
if (isLastWord || (reachedLimit && endsWithPeriod) || currentWords.length >= maxWordsPerChunk * 1.5) {
|
|
91
|
+
const chunkText = currentWords.join(' ');
|
|
92
|
+
const chunkCount = currentWords.length;
|
|
93
|
+
chunks.push({
|
|
94
|
+
id: chunks.length + 1,
|
|
95
|
+
text: chunkText,
|
|
96
|
+
wordCount: chunkCount,
|
|
97
|
+
estimatedDurationSeconds: calculateDuration(chunkCount, 140),
|
|
98
|
+
});
|
|
99
|
+
currentWords = [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return chunks;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* React Hook providing a high-precision, 60fps auto-scroll controller for teleprompters.
|
|
106
|
+
*/
|
|
107
|
+
function useTeleprompter(options) {
|
|
108
|
+
const { text, initialWpm = 140, contentHeight = 1000, viewportHeight = 400, onComplete, } = options;
|
|
109
|
+
const wordCount = countWords(text);
|
|
110
|
+
const [wpm, setWpmState] = (0, react_1.useState)(initialWpm);
|
|
111
|
+
const [isPlaying, setIsPlaying] = (0, react_1.useState)(false);
|
|
112
|
+
const [scrollOffset, setScrollOffset] = (0, react_1.useState)(0);
|
|
113
|
+
const totalDurationSeconds = calculateDuration(wordCount, wpm);
|
|
114
|
+
const scrollableDistance = Math.max(0, contentHeight - viewportHeight);
|
|
115
|
+
const lastFrameTimeRef = (0, react_1.useRef)(null);
|
|
116
|
+
const rafIdRef = (0, react_1.useRef)(null);
|
|
117
|
+
const onCompleteRef = (0, react_1.useRef)(onComplete);
|
|
118
|
+
onCompleteRef.current = onComplete;
|
|
119
|
+
const play = (0, react_1.useCallback)(() => {
|
|
120
|
+
setIsPlaying(true);
|
|
121
|
+
}, []);
|
|
122
|
+
const pause = (0, react_1.useCallback)(() => {
|
|
123
|
+
setIsPlaying(false);
|
|
124
|
+
lastFrameTimeRef.current = null;
|
|
125
|
+
}, []);
|
|
126
|
+
const toggle = (0, react_1.useCallback)(() => {
|
|
127
|
+
setIsPlaying((prev) => !prev);
|
|
128
|
+
}, []);
|
|
129
|
+
const reset = (0, react_1.useCallback)(() => {
|
|
130
|
+
setIsPlaying(false);
|
|
131
|
+
setScrollOffset(0);
|
|
132
|
+
lastFrameTimeRef.current = null;
|
|
133
|
+
}, []);
|
|
134
|
+
const setWpm = (0, react_1.useCallback)((newWpm) => {
|
|
135
|
+
if (newWpm > 20 && newWpm <= 400) {
|
|
136
|
+
setWpmState(newWpm);
|
|
137
|
+
}
|
|
138
|
+
}, []);
|
|
139
|
+
const seek = (0, react_1.useCallback)((progressRatio) => {
|
|
140
|
+
const clamped = Math.max(0, Math.min(1, progressRatio));
|
|
141
|
+
setScrollOffset(clamped * scrollableDistance);
|
|
142
|
+
}, [scrollableDistance]);
|
|
143
|
+
// 60 FPS animation loop
|
|
144
|
+
(0, react_1.useEffect)(() => {
|
|
145
|
+
if (!isPlaying || scrollableDistance <= 0 || totalDurationSeconds <= 0) {
|
|
146
|
+
if (rafIdRef.current)
|
|
147
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
148
|
+
lastFrameTimeRef.current = null;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const velocityPxPerSec = scrollableDistance / totalDurationSeconds;
|
|
152
|
+
const tick = (now) => {
|
|
153
|
+
if (lastFrameTimeRef.current === null) {
|
|
154
|
+
lastFrameTimeRef.current = now;
|
|
155
|
+
}
|
|
156
|
+
const deltaSeconds = (now - lastFrameTimeRef.current) / 1000;
|
|
157
|
+
lastFrameTimeRef.current = now;
|
|
158
|
+
setScrollOffset((prev) => {
|
|
159
|
+
const next = prev + velocityPxPerSec * deltaSeconds;
|
|
160
|
+
if (next >= scrollableDistance) {
|
|
161
|
+
setIsPlaying(false);
|
|
162
|
+
if (onCompleteRef.current)
|
|
163
|
+
onCompleteRef.current();
|
|
164
|
+
return scrollableDistance;
|
|
165
|
+
}
|
|
166
|
+
return next;
|
|
167
|
+
});
|
|
168
|
+
rafIdRef.current = requestAnimationFrame(tick);
|
|
169
|
+
};
|
|
170
|
+
rafIdRef.current = requestAnimationFrame(tick);
|
|
171
|
+
return () => {
|
|
172
|
+
if (rafIdRef.current)
|
|
173
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
174
|
+
};
|
|
175
|
+
}, [isPlaying, scrollableDistance, totalDurationSeconds]);
|
|
176
|
+
const progress = scrollableDistance > 0 ? Math.min(1, scrollOffset / scrollableDistance) : 0;
|
|
177
|
+
const elapsedSeconds = Math.round(progress * totalDurationSeconds);
|
|
178
|
+
const remainingSeconds = Math.max(0, totalDurationSeconds - elapsedSeconds);
|
|
179
|
+
return {
|
|
180
|
+
isPlaying,
|
|
181
|
+
progress,
|
|
182
|
+
scrollOffset,
|
|
183
|
+
elapsedSeconds,
|
|
184
|
+
remainingSeconds,
|
|
185
|
+
totalDurationSeconds,
|
|
186
|
+
wpm,
|
|
187
|
+
wordCount,
|
|
188
|
+
play,
|
|
189
|
+
pause,
|
|
190
|
+
toggle,
|
|
191
|
+
reset,
|
|
192
|
+
setWpm,
|
|
193
|
+
seek,
|
|
194
|
+
};
|
|
195
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-teleflow-prompter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "High-performance 60fps teleprompter scroll engine, WPM calculation, and mirror optics for React Native, Expo, and Web.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react-native",
|
|
18
|
+
"expo",
|
|
19
|
+
"teleprompter",
|
|
20
|
+
"prompter",
|
|
21
|
+
"teleflow",
|
|
22
|
+
"autoscroll",
|
|
23
|
+
"speech-timer",
|
|
24
|
+
"wpm-calculator",
|
|
25
|
+
"mirror-optics",
|
|
26
|
+
"video-recording",
|
|
27
|
+
"muhammet-atmaca",
|
|
28
|
+
"virelonsoft"
|
|
29
|
+
],
|
|
30
|
+
"author": {
|
|
31
|
+
"name": "Muhammet Atmaca",
|
|
32
|
+
"email": "muhammetatmaca79@gmail.com",
|
|
33
|
+
"url": "https://muhammetatmaca.com.tr"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://muhammetatmaca.com.tr/apps/teleflow-prompter",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/muhammetatmaca/muhammet-atmaca-portfolio.git",
|
|
39
|
+
"directory": "packages/react-native-teleflow-prompter"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://muhammetatmaca.com.tr/#contact"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=16.8.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/react": "^18.2.0 || ^19.0.0",
|
|
55
|
+
"typescript": "~5.9.3"
|
|
56
|
+
}
|
|
57
|
+
}
|