pmptr 0.1.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.
@@ -0,0 +1,260 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="color-scheme" content="dark" />
6
+ <title>pmptr - control</title>
7
+ <link rel="stylesheet" href="control.css" />
8
+ </head>
9
+ <body>
10
+ <header class="topbar">
11
+ <div class="brand">pmptr</div>
12
+ <div class="sub">virtual teleprompter</div>
13
+ </header>
14
+
15
+ <main class="layout">
16
+ <section class="card">
17
+ <div class="card-head">
18
+ <h2>Script</h2>
19
+ <div class="row tight">
20
+ <button id="loadSample" type="button" class="ghost">
21
+ Load sample
22
+ </button>
23
+ <button id="clearScript" type="button" class="ghost">
24
+ Clear
25
+ </button>
26
+ </div>
27
+ </div>
28
+ <textarea
29
+ id="script"
30
+ spellcheck="false"
31
+ placeholder="Paste your script here. Blank lines become paragraph breaks."
32
+ ></textarea>
33
+ <div class="hint" id="scriptMeta">0 words · 0 chars</div>
34
+ </section>
35
+
36
+ <section class="card">
37
+ <h2>Reading</h2>
38
+ <div class="grid">
39
+ <label class="field">
40
+ <span class="lbl"
41
+ >Speed <output id="speedOut">40</output>
42
+ <span class="unit">px/s</span></span
43
+ >
44
+ <input id="speed" type="range" min="5" max="300" step="1" value="40" />
45
+ </label>
46
+ <label class="field">
47
+ <span class="lbl"
48
+ >Text size <output id="fontOut">44</output>
49
+ <span class="unit">px</span></span
50
+ >
51
+ <input id="font" type="range" min="16" max="120" step="1" value="44" />
52
+ </label>
53
+ <label class="field">
54
+ <span class="lbl"
55
+ >Line height
56
+ <output id="lhOut">1.45</output></span
57
+ >
58
+ <input
59
+ id="lh"
60
+ type="range"
61
+ min="1"
62
+ max="2.4"
63
+ step="0.05"
64
+ value="1.45"
65
+ />
66
+ </label>
67
+ <label class="field">
68
+ <span class="lbl"
69
+ >Letter spacing
70
+ <output id="lsOut">0</output>
71
+ <span class="unit">px</span></span
72
+ >
73
+ <input
74
+ id="ls"
75
+ type="range"
76
+ min="-2"
77
+ max="10"
78
+ step="0.5"
79
+ value="0"
80
+ />
81
+ </label>
82
+ <label class="field">
83
+ <span class="lbl"
84
+ >Margin <output id="marginOut">0</output>
85
+ <span class="unit">px</span></span
86
+ >
87
+ <input
88
+ id="margin"
89
+ type="range"
90
+ min="0"
91
+ max="200"
92
+ step="2"
93
+ value="0"
94
+ />
95
+ </label>
96
+ </div>
97
+ </section>
98
+
99
+ <section class="card">
100
+ <h2>Appearance</h2>
101
+ <div class="grid">
102
+ <label class="field">
103
+ <span class="lbl">Text color</span>
104
+ <input id="color" type="color" value="#ffffff" />
105
+ </label>
106
+ <label class="field">
107
+ <span class="lbl">Highlight color</span>
108
+ <input id="hl" type="color" value="#ffd84d" />
109
+ </label>
110
+ <label class="field">
111
+ <span class="lbl"
112
+ >Background opacity
113
+ <output id="bgOut">35</output>
114
+ <span class="unit">%</span></span
115
+ >
116
+ <input
117
+ id="bg"
118
+ type="range"
119
+ min="0"
120
+ max="100"
121
+ step="1"
122
+ value="35"
123
+ />
124
+ </label>
125
+ <label class="field">
126
+ <span class="lbl"
127
+ >Background dim
128
+ <output id="dimOut">0</output>
129
+ <span class="unit">%</span></span
130
+ >
131
+ <input
132
+ id="dim"
133
+ type="range"
134
+ min="0"
135
+ max="100"
136
+ step="1"
137
+ value="0"
138
+ />
139
+ </label>
140
+ <label class="field wide">
141
+ <span class="lbl"
142
+ >Text outline
143
+ <output id="strokeOut">0</output>
144
+ <span class="unit">px</span></span
145
+ >
146
+ <input
147
+ id="stroke"
148
+ type="range"
149
+ min="0"
150
+ max="6"
151
+ step="0.5"
152
+ value="0"
153
+ />
154
+ </label>
155
+ </div>
156
+ <div class="row tight">
157
+ <label class="check">
158
+ <input id="mirror" type="checkbox" />
159
+ <span>Mirror (for beam-splitter glass)</span>
160
+ </label>
161
+ <label class="check">
162
+ <input id="bold" type="checkbox" />
163
+ <span>Bold</span>
164
+ </label>
165
+ <label class="check">
166
+ <input id="uppercase" type="checkbox" />
167
+ <span>UPPERCASE</span>
168
+ </label>
169
+ <label class="check">
170
+ <input id="showReadingLine" type="checkbox" checked />
171
+ <span>Reading line</span>
172
+ </label>
173
+ </div>
174
+ </section>
175
+
176
+ <section class="card">
177
+ <h2>Window</h2>
178
+ <div class="grid">
179
+ <label class="field">
180
+ <span class="lbl"
181
+ >Width <output id="winWidthOut">900</output>
182
+ <span class="unit">px</span></span
183
+ >
184
+ <input
185
+ id="winWidth"
186
+ type="range"
187
+ min="240"
188
+ max="2400"
189
+ step="10"
190
+ value="900"
191
+ />
192
+ </label>
193
+ <label class="field">
194
+ <span class="lbl"
195
+ >Height <output id="winHeightOut">280</output>
196
+ <span class="unit">px</span></span
197
+ >
198
+ <input
199
+ id="winHeight"
200
+ type="range"
201
+ min="80"
202
+ max="900"
203
+ step="10"
204
+ value="280"
205
+ />
206
+ </label>
207
+ <label class="field">
208
+ <span class="lbl">Position preset</span>
209
+ <select id="position">
210
+ <option value="top-center" selected>Top center</option>
211
+ <option value="top-left">Top left</option>
212
+ <option value="top-right">Top right</option>
213
+ <option value="bottom-center">Bottom center</option>
214
+ </select>
215
+ </label>
216
+ </div>
217
+ <div class="row tight">
218
+ <label class="check">
219
+ <input id="alwaysOnTop" type="checkbox" checked />
220
+ <span>Always on top</span>
221
+ </label>
222
+ <label class="check">
223
+ <input id="clickThrough" type="checkbox" />
224
+ <span>Click-through (lock)</span>
225
+ </label>
226
+ </div>
227
+ </section>
228
+
229
+ <section class="card actions">
230
+ <button id="openBtn" class="primary" type="button">
231
+ Open floating prompter
232
+ </button>
233
+ <button id="closeBtn" type="button" disabled>Close prompter</button>
234
+ <span class="state" id="state">prompter closed</span>
235
+ </section>
236
+
237
+ <details class="card hint-card">
238
+ <summary>Shortcuts &amp; tips</summary>
239
+ <ul>
240
+ <li>
241
+ <b>Space</b> - play / pause. <b>R</b> - reset to top.
242
+ <b>↑ / ↓</b> - speed ± 5. <b>L</b> - toggle click-through.
243
+ <b>Esc</b> - close prompter.
244
+ </li>
245
+ <li>
246
+ When the prompter is <b>locked</b> (click-through on), the HUD
247
+ still works because we forward mouse events to interactive
248
+ children. The reading area itself is fully click-through.
249
+ </li>
250
+ <li>
251
+ Drag the bottom-right corner of the floating window to resize
252
+ live. Position preset recenters it.
253
+ </li>
254
+ </ul>
255
+ </details>
256
+ </main>
257
+
258
+ <script src="control.js"></script>
259
+ </body>
260
+ </html>
@@ -0,0 +1,313 @@
1
+ const api = window.pmptr;
2
+
3
+ const DEFAULTS = {
4
+ script: "",
5
+ speed: 40,
6
+ font: 44,
7
+ lh: 1.45,
8
+ ls: 0,
9
+ margin: 0,
10
+ color: "#ffffff",
11
+ hl: "#ffd84d",
12
+ bg: 35,
13
+ dim: 0,
14
+ stroke: 0,
15
+ mirror: false,
16
+ bold: false,
17
+ uppercase: false,
18
+ showReadingLine: true,
19
+ alwaysOnTop: true,
20
+ clickThrough: false,
21
+ winWidth: 900,
22
+ winHeight: 280,
23
+ position: "top-center",
24
+ };
25
+
26
+ const SAMPLE = `Welcome to pmptr.
27
+
28
+ This is a minimal virtual teleprompter that floats above your work as a transparent, always-on-top window.
29
+
30
+ Paste your own script on the left, adjust the speed, colors, and opacity, then open the floating prompter.
31
+
32
+ A few tips:
33
+
34
+ * The "lock" toggle makes the prompter click-through, so you can keep working with your mouse on whatever is behind it.
35
+ * Drag the bottom-right corner of the floating window to resize it.
36
+ * The reading line stays fixed in the middle; the text scrolls past it.
37
+
38
+ Good luck on stage.`;
39
+
40
+ const $ = (id) => document.getElementById(id);
41
+
42
+ const els = {
43
+ script: $("script"),
44
+ scriptMeta: $("scriptMeta"),
45
+ loadSample: $("loadSample"),
46
+ clearScript: $("clearScript"),
47
+ speed: $("speed"),
48
+ speedOut: $("speedOut"),
49
+ font: $("font"),
50
+ fontOut: $("fontOut"),
51
+ lh: $("lh"),
52
+ lhOut: $("lhOut"),
53
+ ls: $("ls"),
54
+ lsOut: $("lsOut"),
55
+ margin: $("margin"),
56
+ marginOut: $("marginOut"),
57
+ color: $("color"),
58
+ hl: $("hl"),
59
+ bg: $("bg"),
60
+ bgOut: $("bgOut"),
61
+ dim: $("dim"),
62
+ dimOut: $("dimOut"),
63
+ stroke: $("stroke"),
64
+ strokeOut: $("strokeOut"),
65
+ mirror: $("mirror"),
66
+ bold: $("bold"),
67
+ uppercase: $("uppercase"),
68
+ showReadingLine: $("showReadingLine"),
69
+ alwaysOnTop: $("alwaysOnTop"),
70
+ clickThrough: $("clickThrough"),
71
+ winWidth: $("winWidth"),
72
+ winWidthOut: $("winWidthOut"),
73
+ winHeight: $("winHeight"),
74
+ winHeightOut: $("winHeightOut"),
75
+ position: $("position"),
76
+ openBtn: $("openBtn"),
77
+ closeBtn: $("closeBtn"),
78
+ state: $("state"),
79
+ };
80
+
81
+ let state = { ...DEFAULTS };
82
+ let saveTimer = null;
83
+ let prompterOpen = false;
84
+
85
+ function snapshot() {
86
+ state = {
87
+ ...state,
88
+ script: els.script.value,
89
+ speed: +els.speed.value,
90
+ font: +els.font.value,
91
+ lh: +els.lh.value,
92
+ ls: +els.ls.value,
93
+ margin: +els.margin.value,
94
+ color: els.color.value,
95
+ hl: els.hl.value,
96
+ bg: +els.bg.value,
97
+ dim: +els.dim.value,
98
+ stroke: +els.stroke.value,
99
+ mirror: els.mirror.checked,
100
+ bold: els.bold.checked,
101
+ uppercase: els.uppercase.checked,
102
+ showReadingLine: els.showReadingLine.checked,
103
+ alwaysOnTop: els.alwaysOnTop.checked,
104
+ clickThrough: els.clickThrough.checked,
105
+ winWidth: +els.winWidth.value,
106
+ winHeight: +els.winHeight.value,
107
+ position: els.position.value,
108
+ };
109
+ }
110
+
111
+ function renderOutputs() {
112
+ els.speedOut.value = els.speed.value;
113
+ els.fontOut.value = els.font.value;
114
+ els.lhOut.value = (+els.lh.value).toFixed(2);
115
+ els.lsOut.value = (+els.ls.value).toFixed(1);
116
+ els.marginOut.value = els.margin.value;
117
+ els.bgOut.value = els.bg.value;
118
+ els.dimOut.value = els.dim.value;
119
+ els.strokeOut.value = (+els.stroke.value).toFixed(1);
120
+ els.winWidthOut.value = els.winWidth.value;
121
+ els.winHeightOut.value = els.winHeight.value;
122
+ updateMeta();
123
+ }
124
+
125
+ function updateMeta() {
126
+ const t = els.script.value || "";
127
+ const words = t.trim() ? t.trim().split(/\s+/).length : 0;
128
+ const minutes = state.speed > 0 ? Math.max(0.1, words / (state.speed * 1.4)) : 0;
129
+ els.scriptMeta.textContent = `${words} word${words === 1 ? "" : "s"} · ${t.length} chars · ≈ ${minutes.toFixed(1)} min @ current size`;
130
+ }
131
+
132
+ function fillForm() {
133
+ els.script.value = state.script || "";
134
+ els.speed.value = state.speed;
135
+ els.font.value = state.font;
136
+ els.lh.value = state.lh;
137
+ els.ls.value = state.ls;
138
+ els.margin.value = state.margin;
139
+ els.color.value = state.color;
140
+ els.hl.value = state.hl;
141
+ els.bg.value = state.bg;
142
+ els.dim.value = state.dim;
143
+ els.stroke.value = state.stroke;
144
+ els.mirror.checked = state.mirror;
145
+ els.bold.checked = state.bold;
146
+ els.uppercase.checked = state.uppercase;
147
+ els.showReadingLine.checked = state.showReadingLine;
148
+ els.alwaysOnTop.checked = state.alwaysOnTop;
149
+ els.clickThrough.checked = state.clickThrough;
150
+ els.winWidth.value = state.winWidth;
151
+ els.winHeight.value = state.winHeight;
152
+ els.position.value = state.position;
153
+ renderOutputs();
154
+ }
155
+
156
+ function queueSave() {
157
+ clearTimeout(saveTimer);
158
+ saveTimer = setTimeout(() => api.saveSettings(state), 250);
159
+ }
160
+
161
+ async function pushToPrompter() {
162
+ if (!prompterOpen) return;
163
+ await api.sendSettings(state);
164
+ }
165
+
166
+ function applyClickThrough(enabled) {
167
+ if (!prompterOpen) return;
168
+ api.setClickThrough(enabled);
169
+ }
170
+
171
+ function applyAlwaysOnTop(enabled) {
172
+ if (!prompterOpen) return;
173
+ api.setAlwaysOnTop(enabled);
174
+ }
175
+
176
+ function applyPosition() {
177
+ if (!prompterOpen) return;
178
+ api.setBounds({ width: state.winWidth, height: state.winHeight });
179
+ api.sendCommand({ type: "position", value: state.position });
180
+ }
181
+
182
+ async function openPrompter() {
183
+ snapshot();
184
+ await api.openPrompter({
185
+ windowWidth: state.winWidth,
186
+ windowHeight: state.winHeight,
187
+ position: state.position,
188
+ alwaysOnTop: state.alwaysOnTop,
189
+ });
190
+ prompterOpen = true;
191
+ els.openBtn.disabled = true;
192
+ els.closeBtn.disabled = false;
193
+ setState("prompter open", "ok");
194
+ await pushToPrompter();
195
+ if (state.clickThrough) applyClickThrough(true);
196
+ }
197
+
198
+ async function closePrompter() {
199
+ await api.closePrompter();
200
+ }
201
+
202
+ function setState(text, kind) {
203
+ els.state.textContent = text;
204
+ els.state.classList.remove("ok", "warn");
205
+ if (kind) els.state.classList.add(kind);
206
+ }
207
+
208
+ function wire() {
209
+ const inputs = [
210
+ "speed",
211
+ "font",
212
+ "lh",
213
+ "ls",
214
+ "margin",
215
+ "color",
216
+ "hl",
217
+ "bg",
218
+ "dim",
219
+ "stroke",
220
+ "winWidth",
221
+ "winHeight",
222
+ "position",
223
+ ];
224
+ for (const k of inputs) {
225
+ els[k].addEventListener("input", () => {
226
+ snapshot();
227
+ renderOutputs();
228
+ queueSave();
229
+ pushToPrompter();
230
+ if (k === "winWidth" || k === "winHeight" || k === "position") {
231
+ applyPosition();
232
+ }
233
+ });
234
+ }
235
+ const toggles = [
236
+ "mirror",
237
+ "bold",
238
+ "uppercase",
239
+ "showReadingLine",
240
+ "alwaysOnTop",
241
+ "clickThrough",
242
+ ];
243
+ for (const k of toggles) {
244
+ els[k].addEventListener("change", () => {
245
+ snapshot();
246
+ queueSave();
247
+ pushToPrompter();
248
+ if (k === "clickThrough") applyClickThrough(els.clickThrough.checked);
249
+ if (k === "alwaysOnTop") applyAlwaysOnTop(els.alwaysOnTop.checked);
250
+ });
251
+ }
252
+ els.script.addEventListener("input", () => {
253
+ snapshot();
254
+ renderOutputs();
255
+ queueSave();
256
+ pushToPrompter();
257
+ });
258
+
259
+ els.openBtn.addEventListener("click", openPrompter);
260
+ els.closeBtn.addEventListener("click", closePrompter);
261
+
262
+ els.loadSample.addEventListener("click", () => {
263
+ els.script.value = SAMPLE;
264
+ snapshot();
265
+ renderOutputs();
266
+ queueSave();
267
+ pushToPrompter();
268
+ });
269
+ els.clearScript.addEventListener("click", () => {
270
+ els.script.value = "";
271
+ snapshot();
272
+ renderOutputs();
273
+ queueSave();
274
+ pushToPrompter();
275
+ els.script.focus();
276
+ });
277
+
278
+ api.onPrompterClosed(() => {
279
+ prompterOpen = false;
280
+ els.openBtn.disabled = false;
281
+ els.closeBtn.disabled = true;
282
+ setState("prompter closed");
283
+ });
284
+
285
+ api.onPrompterState((s) => {
286
+ if (!s || typeof s !== "object") return;
287
+ let changed = false;
288
+ if (s.locked !== undefined && s.locked !== els.clickThrough.checked) {
289
+ els.clickThrough.checked = !!s.locked;
290
+ state.clickThrough = !!s.locked;
291
+ changed = true;
292
+ }
293
+ if (typeof s.speed === "number" && s.speed !== +els.speed.value) {
294
+ els.speed.value = s.speed;
295
+ state.speed = s.speed;
296
+ els.speedOut.value = s.speed;
297
+ changed = true;
298
+ }
299
+ if (changed) {
300
+ queueSave();
301
+ }
302
+ });
303
+ }
304
+
305
+ (async function init() {
306
+ const saved = await api.loadSettings();
307
+ if (saved && typeof saved === "object") {
308
+ state = { ...DEFAULTS, ...saved };
309
+ }
310
+ fillForm();
311
+ wire();
312
+ setState("ready");
313
+ })();