@effing/create 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.
package/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ O'Saasy License
2
+
3
+ Copyright © 2026, Trackuity BV.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ 1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ 2. No licensee or downstream recipient may use the Software (including any modified or derivative versions) to directly compete with the original Licensor by offering it to third parties as a hosted, managed, or Software-as-a-Service (SaaS) product or cloud service where the primary value of the service is the functionality of the Software itself.
10
+
11
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @effing/create
2
+
3
+ **Scaffold a new Effing project with the starter template.**
4
+
5
+ > Part of the [**Effing**](../../README.md) family — programmatic video creation with TypeScript.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ # Using npm
11
+ npm create @effing my-app
12
+
13
+ # Using pnpm
14
+ pnpm create @effing my-app
15
+
16
+ # Using yarn
17
+ yarn create @effing my-app
18
+
19
+ # Using npx directly
20
+ npx @effing/create my-app
21
+ ```
22
+
23
+ Then:
24
+
25
+ ```bash
26
+ cd my-app
27
+ npm install
28
+ npm run dev
29
+ ```
30
+
31
+ Open [http://localhost:3839](http://localhost:3839) to see your project.
32
+
33
+ ## What's included
34
+
35
+ The starter template includes:
36
+
37
+ - **React Router** for routing and nifty preview pages
38
+ - **Annies** — Frame-based animations streamed as TAR archives
39
+ - **Effies** — Video compositions combining animations, images, and audio
40
+ - **FFS integration** — A “Render it FFS” button that can POST your Effie JSON to an `@effing/ffs` server (which you can run locally or remotely)
41
+ - Example annies and effies to get you started
42
+
43
+ ## Development
44
+
45
+ This package is part of the Effing monorepo.
46
+
47
+ ### Testing locally
48
+
49
+ ```bash
50
+ # From the monorepo root
51
+ cd packages/create
52
+ pnpm install
53
+ pnpm build
54
+
55
+ # Test the CLI (creates a project outside the monorepo)
56
+ node dist/index.js /tmp/test-effing-app
57
+ ```
58
+
59
+ ### Updating the template
60
+
61
+ 1. Make changes in `demos/starter`
62
+ 2. Run `pnpm build` in this package
63
+ 3. The `prebuild` script automatically copies `demos/starter` → `template/`, renaming dotfiles and replacing `workspace:*` versions
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs from "fs/promises";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ async function copyDir(src, dest) {
9
+ await fs.mkdir(dest, { recursive: true });
10
+ const entries = await fs.readdir(src, { withFileTypes: true });
11
+ for (const entry of entries) {
12
+ const srcPath = path.join(src, entry.name);
13
+ const destName = entry.name.startsWith("_") ? "." + entry.name.slice(1) : entry.name;
14
+ const destPath = path.join(dest, destName);
15
+ if (entry.isDirectory()) {
16
+ await copyDir(srcPath, destPath);
17
+ } else {
18
+ await fs.copyFile(srcPath, destPath);
19
+ }
20
+ }
21
+ }
22
+ function printUsage() {
23
+ console.log(`
24
+ Usage: npm create @effing <project-name>
25
+
26
+ Creates a new Effing project with the starter template.
27
+
28
+ Examples:
29
+ npm create @effing my-app
30
+ pnpm create @effing my-app
31
+ yarn create @effing my-app
32
+ `);
33
+ }
34
+ async function main() {
35
+ const args = process.argv.slice(2);
36
+ if (args.includes("--help") || args.includes("-h")) {
37
+ printUsage();
38
+ process.exit(0);
39
+ }
40
+ const projectName = args[0];
41
+ if (!projectName) {
42
+ console.error("Error: Please specify a project name.\n");
43
+ printUsage();
44
+ process.exit(1);
45
+ }
46
+ const root = path.resolve(projectName);
47
+ const templateDir = path.resolve(__dirname, "../template");
48
+ try {
49
+ await fs.access(root);
50
+ console.error(`Error: Directory "${projectName}" already exists.`);
51
+ process.exit(1);
52
+ } catch {
53
+ }
54
+ console.log(`
55
+ Creating a new Effing project in ${root}...
56
+ `);
57
+ await copyDir(templateDir, root);
58
+ const pkgPath = path.join(root, "package.json");
59
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
60
+ pkg.name = path.basename(root);
61
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
62
+ console.log("Done! To get started:\n");
63
+ console.log(` cd ${projectName}`);
64
+ console.log(" npm install");
65
+ console.log(" npm run dev\n");
66
+ console.log("Then open http://localhost:3839 to see your project.\n");
67
+ }
68
+ main().catch((err) => {
69
+ console.error("Error:", err.message);
70
+ process.exit(1);
71
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@effing/create",
3
+ "version": "0.1.0",
4
+ "description": "Create a new Effing project",
5
+ "type": "module",
6
+ "bin": "./dist/index.js",
7
+ "files": [
8
+ "dist",
9
+ "template"
10
+ ],
11
+ "devDependencies": {
12
+ "tsup": "^8.0.0",
13
+ "typescript": "^5.9.3"
14
+ },
15
+ "engines": {
16
+ "node": ">=22"
17
+ },
18
+ "keywords": [
19
+ "effing",
20
+ "create",
21
+ "scaffold",
22
+ "cli"
23
+ ],
24
+ "license": "O'Saasy",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "prebuild": "node scripts/copy-template.js",
30
+ "build": "tsup",
31
+ "typecheck": "tsc --noEmit"
32
+ }
33
+ }
@@ -0,0 +1,245 @@
1
+ # Effing Starter Demo
2
+
3
+ A complete example application demonstrating how to build Annies (animations) and Effies (video compositions) using the `@effing/*` packages.
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ cd demos/starter
9
+ pnpm install
10
+ pnpm dev
11
+ ```
12
+
13
+ Open [http://localhost:3839](http://localhost:3839) to see the demo.
14
+
15
+ ## Project Structure
16
+
17
+ ```
18
+ demos/starter/
19
+ ├── app/
20
+ │ ├── annies/ # Annie animation definitions
21
+ │ │ ├── index.ts # Annie registry and types
22
+ │ │ └── *.annie.tsx # Annies
23
+ │ ├── effies/ # Effie composition definitions
24
+ │ │ ├── index.ts # Effie registry and types
25
+ │ │ └── *.effie.tsx # Effies
26
+ │ ├── routes/
27
+ │ │ ├── _index.tsx # Homepage listing all annies/effies
28
+ │ │ ├── an.$segment.tsx # Annie TAR streaming endpoint
29
+ │ │ ├── ff.$segment.tsx # Effie JSON endpoint
30
+ │ │ ├── pan.$annieId.tsx # Annie preview page
31
+ │ │ └── pff.$effieId.tsx # Effie preview page
32
+ │ ├── fonts.server.ts # Font definitions and loading utils
33
+ │ └── urls.server.ts # URL generation helpers
34
+ └── vite.config.ts
35
+ ```
36
+
37
+ ## Creating Annies
38
+
39
+ Annies are frame-based animations. Create a file at `app/annies/*.annie.tsx`:
40
+
41
+ ```typescript
42
+ // app/annies/my-animation.annie.tsx
43
+ import { z } from "zod";
44
+ import { pngFromSatori } from "@effing/satori";
45
+ import { tween, easeOutQuad } from "@effing/tween";
46
+ import type { AnnieRendererArgs } from ".";
47
+
48
+ // 1. Define props schema
49
+ export const propsSchema = z.object({
50
+ text: z.string(),
51
+ frameCount: z.number().int().min(1).optional(),
52
+ });
53
+
54
+ type MyAnimationProps = z.infer<typeof propsSchema>;
55
+
56
+ // 2. Define preview props (used by preview page)
57
+ export const previewProps: MyAnimationProps = {
58
+ text: "Hello!",
59
+ frameCount: 60,
60
+ };
61
+
62
+ // 3. Export async generator that yields PNG frames
63
+ export async function* renderer({
64
+ props: { text, frameCount = 60 },
65
+ width,
66
+ height,
67
+ }: AnnieRendererArgs<MyAnimationProps>): AsyncGenerator<Buffer> {
68
+ const fonts = await loadFonts([myFont]);
69
+
70
+ yield* tween(frameCount, async ({ lower: progress }) => {
71
+ const scale = 1 + 0.3 * easeOutQuad(progress);
72
+
73
+ return pngFromSatori(
74
+ <div style={{
75
+ width, height,
76
+ display: "flex",
77
+ alignItems: "center",
78
+ justifyContent: "center",
79
+ fontSize: 72,
80
+ transform: `scale(${scale})`,
81
+ }}>
82
+ {text}
83
+ </div>,
84
+ { width, height, fonts }
85
+ );
86
+ });
87
+ }
88
+ ```
89
+
90
+ The Annie will automatically appear on the homepage and be accessible at:
91
+
92
+ - **Preview:** `/pan/my-animation`
93
+ - **TAR stream:** `/an/{signed-segment}?w=1080&h=1080`
94
+
95
+ ## Creating Effies
96
+
97
+ Effies are video compositions that combine Annies, images, audio, and effects. Create a file at `app/effies/*.effie.tsx`:
98
+
99
+ ```typescript
100
+ // app/effies/my-video.effie.tsx
101
+ import { z } from "zod";
102
+ import { effieData, effieSegment } from "@effing/effie";
103
+ import { annieUrl } from "~/urls.server";
104
+ import type { EffieRendererArgs } from ".";
105
+
106
+ // 1. Define props schema
107
+ export const propsSchema = z.object({
108
+ title: z.string(),
109
+ imageUrl: z.string().url(),
110
+ });
111
+
112
+ type MyVideoProps = z.infer<typeof propsSchema>;
113
+
114
+ // 2. Define preview props
115
+ export const previewProps: MyVideoProps = {
116
+ title: "My Video",
117
+ imageUrl: "https://picsum.photos/1080/1920",
118
+ };
119
+
120
+ // 3. Export async function that returns EffieData
121
+ export async function renderer({
122
+ props: { title, imageUrl },
123
+ width,
124
+ height,
125
+ }: EffieRendererArgs<MyVideoProps>) {
126
+ return effieData({
127
+ width,
128
+ height,
129
+ fps: 30,
130
+ cover: imageUrl,
131
+ background: { type: "color", color: "black" },
132
+ segments: [
133
+ effieSegment({
134
+ duration: 5,
135
+ layers: [
136
+ {
137
+ type: "animation",
138
+ source: await annieUrl({
139
+ annieId: "text-typewriter",
140
+ props: { text: title, fontSize: 72 },
141
+ width,
142
+ height,
143
+ }),
144
+ },
145
+ ],
146
+ }),
147
+ ],
148
+ });
149
+ }
150
+ ```
151
+
152
+ The Effie will automatically appear on the homepage and be accessible at:
153
+
154
+ - **Preview:** `/pff/my-video`
155
+ - **JSON:** `/ff/{signed-segment}?ratio=1:1`
156
+
157
+ ## Routes
158
+
159
+ ### Homepage (`/`)
160
+
161
+ Lists all available Annies and Effies with links to their preview pages.
162
+
163
+ ### Annie Preview (`/pan/:annieId`)
164
+
165
+ Displays an interactive preview of an Annie using `@effing/annie-player`. Shows:
166
+
167
+ - Playable animation with load/play/pause controls
168
+ - Direct URL to the TAR stream
169
+
170
+ ### Effie Preview (`/pff/:effieId`)
171
+
172
+ Comprehensive preview of an Effie composition using `@effing/effie-preview`. Shows:
173
+
174
+ - Cover image (or rendered video after clicking "Render it FFS")
175
+ - Background preview
176
+ - All segments with their layers
177
+ - Render button to generate video via FFS
178
+
179
+ ### Annie Stream (`/an/:segment`)
180
+
181
+ Serves Annie TAR archives. The segment is a signed payload containing:
182
+
183
+ - `annieId` — Which Annie to render
184
+ - Props — Animation parameters
185
+
186
+ ### Effie JSON (`/ff/:segment`)
187
+
188
+ Serves Effie JSON. The segment is a signed payload containing:
189
+
190
+ - `effieId` — Which Effie to render
191
+ - Props — Composition parameters
192
+
193
+ ## CDN Caching
194
+
195
+ Both the `/an/:segment` and `/ff/:segment` routes can easily be placed behind a CDN in production. Since the segment contains signed, deterministic parameters, the same URL always produces the same output, making them ideal cache keys.
196
+
197
+ Note also that CDN timeouts are not a concern for the annies, even though they might take a while to generate in practice, because they are streamed frame by frame. The CDN receives data continuously and won't time out waiting for the first byte.
198
+
199
+ ## Environment Variables
200
+
201
+ ```bash
202
+ # Required: Base URL for the application
203
+ BASE_URL=http://localhost:3839
204
+ # Required: Secret for signing URL segments
205
+ SECRET_KEY=your-secret-key
206
+
207
+ # Optional: FFS rendering service
208
+ FFS_BASE_URL=http://localhost:2000
209
+ FFS_API_KEY=your-ffs-api-key
210
+ ```
211
+
212
+ ## URL Generation
213
+
214
+ The `urls.server.ts` module provides helpers for generating URLs to be used in effies:
215
+
216
+ ```typescript
217
+ import { annieUrl, pngUrlFromSatori } from "~/urls.server";
218
+
219
+ // Generate signed Annie URL
220
+ const url = await annieUrl({
221
+ annieId: "text-typewriter",
222
+ props: { text: "Hello", fontSize: 72 },
223
+ width: 1080,
224
+ height: 1920,
225
+ });
226
+
227
+ // Generate data URL from JSX
228
+ const coverUrl = await pngUrlFromSatori(
229
+ <div>Cover Image</div>,
230
+ { width: 1080, height: 1920, fonts }
231
+ );
232
+ ```
233
+
234
+ ## Rendering Videos
235
+
236
+ The preview page can render videos if FFS is configured:
237
+
238
+ 1. Set `FFS_BASE_URL` and `FFS_API_KEY` environment variables
239
+ 2. Open an Effie preview page (`/pff/:effieId`)
240
+ 3. Click "Render it FFS" to send the composition to the rendering service
241
+ 4. The rendered video appears in place of the cover image
242
+
243
+ You can also render at different scales (33%, 67%, 100%, 200%) for faster previews or higher quality output.
244
+
245
+ In production, you'd use an FFS server directly to render effies. Point FFS at your `/ff/:segment` endpoint and it will fetch the effie JSON, resolve all annie URLs, and produce the final video.
@@ -0,0 +1,5 @@
1
+ BASE_URL=http://localhost:3839
2
+ SECRET_KEY=s3cr3t-k3y-goes-here
3
+
4
+ FFS_BASE_URL=http://localhost:2000
5
+ FFS_API_KEY=ffs-4p1-k3y-goes-here
@@ -0,0 +1,45 @@
1
+ import invariant from "tiny-invariant";
2
+ import type { z } from "zod";
3
+
4
+ /**
5
+ * Arguments passed to annie renderer functions
6
+ */
7
+ export type AnnieRendererArgs<PropsType> = {
8
+ props: PropsType;
9
+ width: number;
10
+ height: number;
11
+ };
12
+
13
+ type AnnieModule = {
14
+ propsSchema: z.ZodSchema<unknown>;
15
+ previewProps: object;
16
+ renderer: (args: AnnieRendererArgs<unknown>) => AsyncGenerator<Buffer>;
17
+ };
18
+
19
+ // Dynamically load all annie modules
20
+ const modules = Object.fromEntries(
21
+ Object.entries(import.meta.glob("./*.annie.tsx")).map(([key, value]) => {
22
+ const id = key.split("/").slice(-1)[0].replace(".annie.tsx", "");
23
+ return [id, value];
24
+ }),
25
+ );
26
+
27
+ export type AnnieId = string;
28
+
29
+ export function isAnnieId(annieId: string): annieId is AnnieId {
30
+ return annieId in modules;
31
+ }
32
+
33
+ export function getAnnieIds(): string[] {
34
+ return Object.keys(modules);
35
+ }
36
+
37
+ export async function getAnnie(annieId: string): Promise<AnnieModule> {
38
+ invariant(isAnnieId(annieId), `no annie found for annieId '${annieId}'`);
39
+ const module = (await modules[annieId]()) as AnnieModule;
40
+ return {
41
+ renderer: module.renderer,
42
+ previewProps: module.previewProps,
43
+ propsSchema: module.propsSchema,
44
+ };
45
+ }
@@ -0,0 +1,56 @@
1
+ import sharp from "sharp";
2
+ import { z } from "zod";
3
+ import { tween, easeOutQuad } from "@effing/tween";
4
+ import type { AnnieRendererArgs } from ".";
5
+
6
+ export const propsSchema = z.object({
7
+ imageUrl: z.string().url(),
8
+ frameCount: z.number().int().min(1).optional(),
9
+ zoomLevel: z.number().optional(),
10
+ });
11
+
12
+ export type PhotoZoomProps = z.infer<typeof propsSchema>;
13
+
14
+ export const previewProps: PhotoZoomProps = {
15
+ imageUrl: "https://picsum.photos/1200/1200",
16
+ frameCount: 120,
17
+ zoomLevel: 0.2,
18
+ };
19
+
20
+ export async function* renderer({
21
+ props: { imageUrl, frameCount = 90, zoomLevel = 0.2 },
22
+ width,
23
+ height,
24
+ }: AnnieRendererArgs<PhotoZoomProps>): AsyncGenerator<Buffer> {
25
+ // Fetch and decode the source image
26
+ const image = await fetch(imageUrl);
27
+ const imageBuffer = await image.arrayBuffer();
28
+ const { data: originalImageData, info: originalImageInfo } = await sharp(
29
+ Buffer.from(imageBuffer),
30
+ )
31
+ .raw()
32
+ .toBuffer({ resolveWithObject: true });
33
+
34
+ const imageSharp = sharp(originalImageData, {
35
+ raw: {
36
+ width: originalImageInfo.width,
37
+ height: originalImageInfo.height,
38
+ channels: originalImageInfo.channels,
39
+ },
40
+ });
41
+
42
+ // Generate frames with zoom effect
43
+ yield* tween(frameCount, async ({ lower: p }) => {
44
+ const zoomValue = 1 + zoomLevel * easeOutQuad(p);
45
+ const ow = Math.round(width * zoomValue);
46
+ const oh = Math.round(height * zoomValue);
47
+ const left = Math.floor((ow - width) / 2);
48
+ const top = Math.floor((oh - height) / 2);
49
+ return imageSharp
50
+ .clone()
51
+ .resize(ow, oh, { fit: "cover" })
52
+ .extract({ left, top, width, height })
53
+ .jpeg()
54
+ .toBuffer();
55
+ });
56
+ }
@@ -0,0 +1,151 @@
1
+ import { z } from "zod";
2
+ import { interBold, loadFonts } from "~/fonts.server";
3
+ import { pngFromSatori } from "@effing/satori";
4
+ import { tween } from "@effing/tween";
5
+ import type { AnnieRendererArgs } from ".";
6
+
7
+ export const propsSchema = z.object({
8
+ text: z.string(),
9
+ fontSize: z.number().int().min(1),
10
+ fontFamily: z.enum(["Inter", "Roboto", "Open Sans"]).optional(),
11
+ fontColor: z.string().optional(),
12
+ typingFrameCount: z.number().int().min(10).optional(),
13
+ blinkingFrameCount: z.number().int().min(0).optional(),
14
+ horizontalAlignment: z.enum(["left", "center", "right"]).optional(),
15
+ verticalAlignment: z.enum(["top", "center", "bottom"]).optional(),
16
+ });
17
+
18
+ export type TextTypewriterProps = z.infer<typeof propsSchema>;
19
+
20
+ export const previewProps: TextTypewriterProps = {
21
+ text: "Hello World!",
22
+ fontSize: 72,
23
+ fontFamily: "Inter",
24
+ fontColor: "#ffffff",
25
+ horizontalAlignment: "center",
26
+ verticalAlignment: "center",
27
+ };
28
+
29
+ export async function* renderer({
30
+ props: {
31
+ text,
32
+ fontSize,
33
+ fontFamily = "Inter",
34
+ fontColor = "#ffffff",
35
+ typingFrameCount,
36
+ blinkingFrameCount = 60,
37
+ horizontalAlignment = "center",
38
+ verticalAlignment = "center",
39
+ },
40
+ width,
41
+ height,
42
+ }: AnnieRendererArgs<TextTypewriterProps>): AsyncGenerator<Buffer> {
43
+ const fonts = await loadFonts([interBold]);
44
+
45
+ if (!typingFrameCount) {
46
+ typingFrameCount = text.length * 3;
47
+ }
48
+
49
+ // Typing phase
50
+ yield* tween(typingFrameCount, async ({ lower: p }) => {
51
+ const charsShown = Math.floor(p * text.length);
52
+ const textToShow = text.slice(0, charsShown);
53
+ return pngFromSatori(
54
+ <TextTypewriterOverlay
55
+ text={textToShow}
56
+ fontSize={fontSize}
57
+ fontFamily={fontFamily}
58
+ fontColor={fontColor}
59
+ horizontalAlignment={horizontalAlignment}
60
+ verticalAlignment={verticalAlignment}
61
+ cursorShown={true}
62
+ />,
63
+ { width, height, fonts },
64
+ );
65
+ });
66
+
67
+ // Blinking cursor phase
68
+ yield* tween(blinkingFrameCount, async ({ lower: p }) => {
69
+ const cursorShown = Math.floor(p * 5) % 2 === 1;
70
+ return pngFromSatori(
71
+ <TextTypewriterOverlay
72
+ text={text}
73
+ fontSize={fontSize}
74
+ fontFamily={fontFamily}
75
+ fontColor={fontColor}
76
+ horizontalAlignment={horizontalAlignment}
77
+ verticalAlignment={verticalAlignment}
78
+ cursorShown={cursorShown}
79
+ />,
80
+ { width, height, fonts },
81
+ );
82
+ });
83
+ }
84
+
85
+ export function TextTypewriterOverlay({
86
+ text,
87
+ fontFamily = "Inter",
88
+ fontSize,
89
+ fontColor = "#ffffff",
90
+ horizontalAlignment = "center",
91
+ verticalAlignment = "center",
92
+ cursorShown,
93
+ }: {
94
+ text: string;
95
+ fontFamily?: string;
96
+ fontSize: number;
97
+ fontColor?: string;
98
+ horizontalAlignment?: "left" | "center" | "right";
99
+ verticalAlignment?: "top" | "center" | "bottom";
100
+ cursorShown: boolean;
101
+ }) {
102
+ return (
103
+ <div
104
+ style={{
105
+ display: "flex",
106
+ position: "absolute",
107
+ top: 0,
108
+ left: 0,
109
+ right: 0,
110
+ bottom: 0,
111
+ alignItems: { top: "flex-start", center: "center", bottom: "flex-end" }[
112
+ verticalAlignment
113
+ ],
114
+ justifyContent: {
115
+ left: "flex-start",
116
+ center: "center",
117
+ right: "flex-end",
118
+ }[horizontalAlignment],
119
+ padding: 40,
120
+ }}
121
+ >
122
+ <div
123
+ style={{
124
+ fontFamily,
125
+ fontSize: fontSize,
126
+ fontWeight: "bold",
127
+ color: fontColor,
128
+ display: "flex",
129
+ flexDirection: "row",
130
+ alignItems: "center",
131
+ justifyContent: "center",
132
+ textAlign: horizontalAlignment,
133
+ whiteSpace: "nowrap",
134
+ }}
135
+ >
136
+ {text}
137
+ <span
138
+ style={{
139
+ height: fontSize,
140
+ width: 3,
141
+ display: "block",
142
+ backgroundColor: fontColor,
143
+ marginLeft: 4,
144
+ verticalAlign: "middle",
145
+ opacity: Number(cursorShown),
146
+ }}
147
+ />
148
+ </div>
149
+ </div>
150
+ );
151
+ }