@subvo/renderer 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.
@@ -0,0 +1,66 @@
1
+ type SchemaVersion = "1.0";
2
+ type RenderJob = {
3
+ schemaVersion: SchemaVersion;
4
+ rendererVersion: string;
5
+ fps: number;
6
+ durationMs: number;
7
+ playRes: {
8
+ width: number;
9
+ height: number;
10
+ };
11
+ style: RenderStyle;
12
+ lines: RenderLine[];
13
+ layoutHints?: RenderLayoutHints;
14
+ metadata?: RenderMetadata;
15
+ };
16
+ type RenderLine = {
17
+ id: string;
18
+ startMs: number;
19
+ endMs: number;
20
+ words: RenderWord[];
21
+ };
22
+ type RenderWord = {
23
+ id: string;
24
+ text: string;
25
+ startMs: number;
26
+ endMs: number;
27
+ };
28
+ type RenderLayoutHints = {
29
+ maxWordsPerLine?: number;
30
+ };
31
+ type RenderMetadata = {
32
+ jobId?: string;
33
+ source?: string;
34
+ createdAt?: string;
35
+ tags?: string[];
36
+ };
37
+ type RenderPosition = "bottom_safe" | "center";
38
+ type RenderPreset = "fire" | "clean" | "luxury" | "pop" | "ghost";
39
+ type RenderStyle = {
40
+ fontFamily: string;
41
+ fontSize: number;
42
+ fontWeight: number;
43
+ lineSpacing: number;
44
+ baseColor: string;
45
+ highlightColor: string;
46
+ outlineWidth: number;
47
+ outlineColor: string;
48
+ shadow: boolean;
49
+ shadowDepth: number;
50
+ glowIntensity: number;
51
+ highlightStrength: number;
52
+ pulse: number;
53
+ karaokeScale: number;
54
+ maxWordsPerLine: number;
55
+ position: RenderPosition;
56
+ offsetY: number;
57
+ fadeInMs: number;
58
+ fadeOutMs: number;
59
+ preset: RenderPreset;
60
+ };
61
+
62
+ declare function renderFrame(ctx: CanvasRenderingContext2D, renderJob: RenderJob, timeMs: number): void;
63
+
64
+ declare const VERSION: string;
65
+
66
+ export { type RenderJob, type RenderLayoutHints, type RenderLine, type RenderMetadata, type RenderPosition, type RenderPreset, type RenderStyle, type RenderWord, type SchemaVersion, VERSION, renderFrame };
@@ -0,0 +1,146 @@
1
+ // src/layout.ts
2
+ function computeLayout(ctx, renderJob) {
3
+ const { style } = renderJob;
4
+ ctx.font = `${style.fontWeight} ${style.fontSize}px ${style.fontFamily}`;
5
+ ctx.textBaseline = "alphabetic";
6
+ return renderJob.lines.map((line) => {
7
+ const rows = chunkWords(line.words, style.maxWordsPerLine);
8
+ return {
9
+ line,
10
+ rows: rows.map((words) => {
11
+ let rowWidth = 0;
12
+ const measuredWords = words.map((word, index) => {
13
+ const text = index < words.length - 1 ? `${word.text} ` : word.text;
14
+ const metrics = ctx.measureText(text);
15
+ const width = metrics.width;
16
+ rowWidth += width;
17
+ return {
18
+ word,
19
+ text,
20
+ width
21
+ };
22
+ });
23
+ return {
24
+ words: measuredWords,
25
+ width: rowWidth
26
+ };
27
+ })
28
+ };
29
+ });
30
+ }
31
+ function chunkWords(words, maxPerLine) {
32
+ if (maxPerLine <= 0) return [words];
33
+ const rows = [];
34
+ let current = [];
35
+ for (const word of words) {
36
+ current.push(word);
37
+ if (current.length >= maxPerLine) {
38
+ rows.push(current);
39
+ current = [];
40
+ }
41
+ }
42
+ if (current.length > 0) {
43
+ rows.push(current);
44
+ }
45
+ return rows;
46
+ }
47
+
48
+ // src/effects.ts
49
+ function clamp(value, min = 0, max = 1) {
50
+ return Math.min(max, Math.max(min, value));
51
+ }
52
+ function computeFadeAlpha(style, line, timeMs) {
53
+ let alpha = 1;
54
+ if (style.fadeInMs > 0) {
55
+ alpha = Math.min(alpha, (timeMs - line.startMs) / style.fadeInMs);
56
+ }
57
+ if (style.fadeOutMs > 0) {
58
+ alpha = Math.min(alpha, (line.endMs - timeMs) / style.fadeOutMs);
59
+ }
60
+ return clamp(alpha);
61
+ }
62
+ function computeWordProgress(word, timeMs) {
63
+ const duration = word.endMs - word.startMs;
64
+ if (duration <= 0) return 0;
65
+ return clamp((timeMs - word.startMs) / duration);
66
+ }
67
+ function computePulseScale(style, progress) {
68
+ return 1 + (style.pulse - 1) * progress * style.highlightStrength;
69
+ }
70
+
71
+ // src/drawText.ts
72
+ function drawWord(ctx, text, x, y, style, scaleFactor, ascent, fillColor) {
73
+ const invScale = 1 / scaleFactor;
74
+ ctx.save();
75
+ ctx.translate(x, y);
76
+ ctx.translate(0, -ascent);
77
+ ctx.scale(scaleFactor, scaleFactor);
78
+ ctx.translate(0, ascent);
79
+ ctx.fillStyle = fillColor;
80
+ if (style.outlineWidth > 0) {
81
+ ctx.lineWidth = style.outlineWidth * invScale;
82
+ ctx.strokeStyle = style.outlineColor;
83
+ ctx.strokeText(text, 0, 0);
84
+ }
85
+ if (style.shadow || style.glowIntensity > 0) {
86
+ ctx.shadowColor = style.outlineColor;
87
+ ctx.shadowOffsetX = style.shadow ? style.shadowDepth * invScale : 0;
88
+ ctx.shadowOffsetY = style.shadow ? style.shadowDepth * invScale : 0;
89
+ ctx.shadowBlur = style.glowIntensity > 0 ? style.fontSize * 0.3 * invScale : 0;
90
+ } else {
91
+ ctx.shadowColor = "transparent";
92
+ ctx.shadowBlur = 0;
93
+ ctx.shadowOffsetX = 0;
94
+ ctx.shadowOffsetY = 0;
95
+ }
96
+ ctx.fillText(text, 0, 0);
97
+ ctx.restore();
98
+ }
99
+
100
+ // src/renderFrame.ts
101
+ function renderFrame(ctx, renderJob, timeMs) {
102
+ const { playRes, style } = renderJob;
103
+ const layout = computeLayout(ctx, renderJob);
104
+ const active = layout.find(
105
+ (line) => timeMs >= line.line.startMs && timeMs <= line.line.endMs
106
+ );
107
+ if (!active) return;
108
+ const transform = ctx.getTransform();
109
+ const canvasWidth = ctx.canvas.width / (transform.a || 1);
110
+ const canvasHeight = ctx.canvas.height / (transform.d || 1);
111
+ const scale = Math.max(canvasWidth / playRes.width, canvasHeight / playRes.height);
112
+ const offsetX = (canvasWidth - playRes.width * scale) / 2;
113
+ const offsetY = (canvasHeight - playRes.height * scale) / 2;
114
+ const alpha = computeFadeAlpha(style, active.line, timeMs);
115
+ ctx.save();
116
+ ctx.translate(offsetX, offsetY);
117
+ ctx.scale(scale, scale);
118
+ ctx.globalAlpha = alpha;
119
+ ctx.font = `${style.fontWeight} ${style.fontSize}px ${style.fontFamily}`;
120
+ ctx.textBaseline = "alphabetic";
121
+ const metrics = ctx.measureText("Mg");
122
+ const ascent = metrics.actualBoundingBoxAscent;
123
+ const centerX = playRes.width / 2;
124
+ let y = style.position === "center" ? playRes.height / 2 + style.offsetY : playRes.height * 0.92 + style.offsetY;
125
+ for (const row of active.rows) {
126
+ let x = centerX - row.width / 2;
127
+ for (const { word, text, width } of row.words) {
128
+ const activeWord = timeMs >= word.startMs && timeMs <= word.endMs;
129
+ const progress = activeWord ? computeWordProgress(word, timeMs) : 0;
130
+ const scaleFactor = computePulseScale(style, progress);
131
+ const fillColor = activeWord ? style.highlightColor : style.baseColor;
132
+ drawWord(ctx, text, x, y, style, scaleFactor, ascent, fillColor);
133
+ x += width;
134
+ }
135
+ y += style.fontSize * style.lineSpacing;
136
+ }
137
+ ctx.restore();
138
+ }
139
+
140
+ // src/index.ts
141
+ var injectedVersion = "1.0.0".length > 0 ? "1.0.0" : "dev";
142
+ var VERSION = injectedVersion;
143
+ export {
144
+ VERSION,
145
+ renderFrame
146
+ };
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var SubvoRenderer = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ VERSION: () => VERSION,
25
+ renderFrame: () => renderFrame
26
+ });
27
+
28
+ // src/layout.ts
29
+ function computeLayout(ctx, renderJob) {
30
+ const { style } = renderJob;
31
+ ctx.font = `${style.fontWeight} ${style.fontSize}px ${style.fontFamily}`;
32
+ ctx.textBaseline = "alphabetic";
33
+ return renderJob.lines.map((line) => {
34
+ const rows = chunkWords(line.words, style.maxWordsPerLine);
35
+ return {
36
+ line,
37
+ rows: rows.map((words) => {
38
+ let rowWidth = 0;
39
+ const measuredWords = words.map((word, index) => {
40
+ const text = index < words.length - 1 ? `${word.text} ` : word.text;
41
+ const metrics = ctx.measureText(text);
42
+ const width = metrics.width;
43
+ rowWidth += width;
44
+ return {
45
+ word,
46
+ text,
47
+ width
48
+ };
49
+ });
50
+ return {
51
+ words: measuredWords,
52
+ width: rowWidth
53
+ };
54
+ })
55
+ };
56
+ });
57
+ }
58
+ function chunkWords(words, maxPerLine) {
59
+ if (maxPerLine <= 0) return [words];
60
+ const rows = [];
61
+ let current = [];
62
+ for (const word of words) {
63
+ current.push(word);
64
+ if (current.length >= maxPerLine) {
65
+ rows.push(current);
66
+ current = [];
67
+ }
68
+ }
69
+ if (current.length > 0) {
70
+ rows.push(current);
71
+ }
72
+ return rows;
73
+ }
74
+
75
+ // src/effects.ts
76
+ function clamp(value, min = 0, max = 1) {
77
+ return Math.min(max, Math.max(min, value));
78
+ }
79
+ function computeFadeAlpha(style, line, timeMs) {
80
+ let alpha = 1;
81
+ if (style.fadeInMs > 0) {
82
+ alpha = Math.min(alpha, (timeMs - line.startMs) / style.fadeInMs);
83
+ }
84
+ if (style.fadeOutMs > 0) {
85
+ alpha = Math.min(alpha, (line.endMs - timeMs) / style.fadeOutMs);
86
+ }
87
+ return clamp(alpha);
88
+ }
89
+ function computeWordProgress(word, timeMs) {
90
+ const duration = word.endMs - word.startMs;
91
+ if (duration <= 0) return 0;
92
+ return clamp((timeMs - word.startMs) / duration);
93
+ }
94
+ function computePulseScale(style, progress) {
95
+ return 1 + (style.pulse - 1) * progress * style.highlightStrength;
96
+ }
97
+
98
+ // src/drawText.ts
99
+ function drawWord(ctx, text, x, y, style, scaleFactor, ascent, fillColor) {
100
+ const invScale = 1 / scaleFactor;
101
+ ctx.save();
102
+ ctx.translate(x, y);
103
+ ctx.translate(0, -ascent);
104
+ ctx.scale(scaleFactor, scaleFactor);
105
+ ctx.translate(0, ascent);
106
+ ctx.fillStyle = fillColor;
107
+ if (style.outlineWidth > 0) {
108
+ ctx.lineWidth = style.outlineWidth * invScale;
109
+ ctx.strokeStyle = style.outlineColor;
110
+ ctx.strokeText(text, 0, 0);
111
+ }
112
+ if (style.shadow || style.glowIntensity > 0) {
113
+ ctx.shadowColor = style.outlineColor;
114
+ ctx.shadowOffsetX = style.shadow ? style.shadowDepth * invScale : 0;
115
+ ctx.shadowOffsetY = style.shadow ? style.shadowDepth * invScale : 0;
116
+ ctx.shadowBlur = style.glowIntensity > 0 ? style.fontSize * 0.3 * invScale : 0;
117
+ } else {
118
+ ctx.shadowColor = "transparent";
119
+ ctx.shadowBlur = 0;
120
+ ctx.shadowOffsetX = 0;
121
+ ctx.shadowOffsetY = 0;
122
+ }
123
+ ctx.fillText(text, 0, 0);
124
+ ctx.restore();
125
+ }
126
+
127
+ // src/renderFrame.ts
128
+ function renderFrame(ctx, renderJob, timeMs) {
129
+ const { playRes, style } = renderJob;
130
+ const layout = computeLayout(ctx, renderJob);
131
+ const active = layout.find(
132
+ (line) => timeMs >= line.line.startMs && timeMs <= line.line.endMs
133
+ );
134
+ if (!active) return;
135
+ const transform = ctx.getTransform();
136
+ const canvasWidth = ctx.canvas.width / (transform.a || 1);
137
+ const canvasHeight = ctx.canvas.height / (transform.d || 1);
138
+ const scale = Math.max(canvasWidth / playRes.width, canvasHeight / playRes.height);
139
+ const offsetX = (canvasWidth - playRes.width * scale) / 2;
140
+ const offsetY = (canvasHeight - playRes.height * scale) / 2;
141
+ const alpha = computeFadeAlpha(style, active.line, timeMs);
142
+ ctx.save();
143
+ ctx.translate(offsetX, offsetY);
144
+ ctx.scale(scale, scale);
145
+ ctx.globalAlpha = alpha;
146
+ ctx.font = `${style.fontWeight} ${style.fontSize}px ${style.fontFamily}`;
147
+ ctx.textBaseline = "alphabetic";
148
+ const metrics = ctx.measureText("Mg");
149
+ const ascent = metrics.actualBoundingBoxAscent;
150
+ const centerX = playRes.width / 2;
151
+ let y = style.position === "center" ? playRes.height / 2 + style.offsetY : playRes.height * 0.92 + style.offsetY;
152
+ for (const row of active.rows) {
153
+ let x = centerX - row.width / 2;
154
+ for (const { word, text, width } of row.words) {
155
+ const activeWord = timeMs >= word.startMs && timeMs <= word.endMs;
156
+ const progress = activeWord ? computeWordProgress(word, timeMs) : 0;
157
+ const scaleFactor = computePulseScale(style, progress);
158
+ const fillColor = activeWord ? style.highlightColor : style.baseColor;
159
+ drawWord(ctx, text, x, y, style, scaleFactor, ascent, fillColor);
160
+ x += width;
161
+ }
162
+ y += style.fontSize * style.lineSpacing;
163
+ }
164
+ ctx.restore();
165
+ }
166
+
167
+ // src/index.ts
168
+ var injectedVersion = "1.0.0".length > 0 ? "1.0.0" : "dev";
169
+ var VERSION = injectedVersion;
170
+ return __toCommonJS(src_exports);
171
+ })();
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@subvo/renderer",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/renderer.esm.js",
6
+ "module": "./dist/renderer.esm.js",
7
+ "types": "./dist/renderer.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/renderer.d.ts",
11
+ "import": "./dist/renderer.esm.js"
12
+ },
13
+ "./iife": "./dist/renderer.iife.js"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "devDependencies": {
19
+ "tsup": "^8.0.0",
20
+ "typescript": "^5.7.0"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup"
24
+ }
25
+ }