├── .gitignore
├── CHANGELOG.md
├── README.md
├── lerna.json
├── package-lock.json
├── package.json
└── packages
├── caspar-graphics
├── bin
│ └── caspar-graphics.js
├── package.json
├── src
│ ├── client
│ │ ├── Checkbox.jsx
│ │ ├── Input.jsx
│ │ ├── JsonEditor.jsx
│ │ ├── Menu.jsx
│ │ ├── Popover.jsx
│ │ ├── Screen.jsx
│ │ ├── Settings.jsx
│ │ ├── Sidebar.jsx
│ │ ├── Switch.jsx
│ │ ├── TemplatePreview.jsx
│ │ ├── app.jsx
│ │ ├── checkbox.module.css
│ │ ├── favicon.png
│ │ ├── global.css
│ │ ├── index.html
│ │ ├── index.jsx
│ │ ├── index.module.css
│ │ ├── input.module.css
│ │ ├── menu.module.css
│ │ ├── screen.module.css
│ │ ├── settings.module.css
│ │ ├── sidebar.module.css
│ │ ├── switch.module.css
│ │ └── use-persistent-value.js
│ ├── index.js
│ └── node
│ │ ├── build.js
│ │ ├── cli.js
│ │ ├── paths.js
│ │ ├── server.js
│ │ └── utils
│ │ └── gzip.js
├── vite.config.js
└── vite.config.lib.js
├── create-caspar-graphics
├── index.js
├── package.json
├── template-react-framer-motion
│ ├── _gitignore
│ ├── package.json
│ └── templates
│ │ └── example
│ │ ├── index.html
│ │ ├── index.jsx
│ │ ├── manifest.json
│ │ └── style.css
├── template-react-gsap
│ ├── _gitignore
│ ├── package.json
│ └── templates
│ │ └── example
│ │ ├── index.html
│ │ ├── index.jsx
│ │ ├── manifest.json
│ │ └── style.css
├── template-react
│ ├── _gitignore
│ ├── package.json
│ └── templates
│ │ └── example
│ │ ├── index.html
│ │ ├── index.jsx
│ │ └── manifest.json
└── template-vanilla
│ ├── _gitignore
│ ├── package.json
│ └── templates
│ └── example
│ ├── index.html
│ └── manifest.json
├── examples
├── README.md
├── package.json
└── templates
│ ├── custom-font
│ ├── Exo-VariableFont_wght.ttf
│ ├── RobotoCondensed-Bold.ttf
│ ├── RobotoCondensed-Regular.ttf
│ ├── index.html
│ ├── index.jsx
│ ├── manifest.json
│ └── style.css
│ ├── fit-text
│ ├── index.html
│ ├── index.jsx
│ ├── manifest.json
│ └── style.css
│ ├── framer-motion
│ ├── index.html
│ ├── index.jsx
│ ├── manifest.json
│ └── style.css
│ ├── gsap
│ ├── index.html
│ ├── index.jsx
│ ├── manifest.json
│ └── style.css
│ ├── responsive-width
│ ├── index.html
│ ├── index.jsx
│ ├── manifest.json
│ └── style.css
│ ├── split-text-gsap
│ ├── index.html
│ ├── index.jsx
│ ├── manifest.json
│ └── style.css
│ ├── vanilla-gsap
│ ├── index.html
│ └── manifest.json
│ └── webscreen
│ ├── index.html
│ ├── index.jsx
│ ├── manifest.json
│ └── style.css
├── graphics-kit
├── package.json
├── rollup.config.js
└── src
│ ├── Crawl.jsx
│ ├── FitText.jsx
│ ├── FramerMotion.jsx
│ ├── Gsap.jsx
│ ├── Image.jsx
│ ├── SplitText.jsx
│ ├── TemplateProvider.jsx
│ ├── constants.js
│ ├── index.js
│ ├── render.js
│ ├── use-animated-change.js
│ ├── use-caspar.js
│ ├── use-clock.js
│ ├── use-fetch.js
│ ├── use-font.js
│ ├── use-interval.js
│ ├── use-previous.js
│ ├── use-rss-feed.js
│ ├── use-timeout.js
│ └── utils
│ └── parse.js
└── www
├── components
├── app.jsx
└── app.module.css
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
├── _meta.json
├── about.mdx
├── docs
│ ├── _meta.json
│ ├── build-for-caspar.mdx
│ ├── components
│ │ ├── _meta.json
│ │ ├── crawl.mdx
│ │ ├── framer-motion.mdx
│ │ └── gsap-timeline.mdx
│ ├── create-your-first-graphic.mdx
│ ├── create-your-first-graphic
│ │ ├── _meta.json
│ │ ├── create-a-project.mdx
│ │ ├── in-out-animations.mdx
│ │ ├── introducing-state.mdx
│ │ └── sending-data.mdx
│ ├── getting-started.mdx
│ ├── graphics-kit.mdx
│ ├── hooks
│ │ ├── _meta.json
│ │ ├── use-caspar-data.mdx
│ │ ├── use-caspar-state.mdx
│ │ ├── use-caspar.mdx
│ │ ├── use-fetch.mdx
│ │ ├── use-font.mdx
│ │ └── use-image.mdx
│ ├── index.mdx
│ ├── manifest.mdx
│ ├── preview-images.mdx
│ ├── preview-presets.mdx
│ ├── responsive.mdx
│ └── states.mdx
├── examples.mdx
├── index.mdx
└── index.module.css
├── public
├── aspect-ratios.jpg
├── example.jpg
├── favicon.png
├── in-out-animations.mp4
├── initial-launch.jpg
├── lowerthird-1.jpg
├── lowerthird-data-text.jpg
├── lowerthird-hello-world.jpg
├── lowerthird-off.jpg
├── preview-image.jpg
├── react-example-1.jpg
└── thierry-henry.jpg
└── theme.config.tsx
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | .DS_Store
6 |
7 | # Runtime data
8 | pids
9 | *.pid
10 | *.seed
11 |
12 | # Directory for instrumented libs generated by jscoverage/JSCover
13 | lib-cov
14 |
15 | # Coverage directory used by tools like istanbul
16 | coverage
17 |
18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
19 | .grunt
20 |
21 | # node-waf configuration
22 | .lock-wscript
23 |
24 | # Dependency directory
25 | node_modules
26 |
27 | # Optional npm cache directory
28 | .npm
29 |
30 | # Optional REPL history
31 | .node_repl_history
32 |
33 | # Build Output
34 | build
35 | dist
36 | .vscode
37 | .vercel
38 | .next
39 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## v3.0.0
4 |
5 | Added better support for building responsive graphics. See https://gfx.nxtedition.com/docs/responsive for more information.
6 |
7 | **Breaking:**
8 |
9 | All fixed size graphics need to specify their width and height in manifest.json. E.g. if your existing graphics only support being played in 1920x1080 pixels, you should add this to each graphic's manifest.json file:
10 |
11 | ```json
12 | {
13 | "width": 1920,
14 | "height": 1080
15 | }
16 | ```
17 |
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Caspar Graphics
2 |
3 | 
4 |
5 | Caspar Graphics is a tool for building, testing and packaging HTML graphics for CasparCG using code. We created Caspar Graphics to make it easier for ourselves to build and maintain graphic packages. It offers a number of key benefits for people looking to create graphics efficiently and effectively:
6 |
7 | - A simple and intuitive UI makes it easy to interact with your graphics — send data updates, play and stop animations, and view all of your graphics together to make sure they're positioned properly.
8 | - No configuration is needed with out-of-the-box support for TypeScript, JSX, CSS and more.
9 | - Lightning Fast HMR (Hot Module Replacement) enables you to quickly see your changes as they're made.
10 | - Connect to a CasparCG server to easily test your graphics in a production environment.
11 | - Package your graphics into an optimized bundle, ready to be played in CasparCG.
12 |
13 | We've built Caspar Graphics on top of [Vite](https://vitejs.dev/), which offers fast and reliable performance with no need for configuration. This means that you can benefit from most of those features as well.
14 |
15 | [Read the Docs to Learn More.](https://gfx.nxtedition.com)
16 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "independent",
3 | "$schema": "node_modules/lerna/schemas/lerna-schema.json"
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "workspaces": [
4 | "packages/*"
5 | ],
6 | "bugs": "https://github.com/nxtedition/create-caspar-graphics/issues",
7 | "homepage": "https://github.com/nxtedition/create-caspar-graphics#readme",
8 | "devDependencies": {
9 | "husky": "2.1.0",
10 | "lerna": "8.0.1",
11 | "lint-staged": "^4.0.0",
12 | "prettier": "^3.1.1"
13 | },
14 | "scripts": {
15 | "format": "prettier --single-quote --no-semi --write 'packages/*/*.js' 'packages/*/!(node_modules)/**/*.js'",
16 | "examples": "lerna run examples",
17 | "dev": "lerna run --parallel dev"
18 | },
19 | "lint-staged": {
20 | "*.js": [
21 | "prettier --single-quote --no-semi --write",
22 | "git add"
23 | ]
24 | },
25 | "prettier": {
26 | "tabWidth": 2,
27 | "semi": false,
28 | "singleQuote": true
29 | },
30 | "husky": {
31 | "hooks": {
32 | "pre-commit": "lint-staged"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/bin/caspar-graphics.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import('../src/node/cli.js')
4 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@nxtedition/caspar-graphics",
3 | "version": "3.0.3",
4 | "type": "module",
5 | "description": "A UI for building graphics for CasparCG in isolation.",
6 | "repository": "github:nxtedition/create-caspar-graphics",
7 | "author": "nxtedition",
8 | "license": "MIT",
9 | "bin": {
10 | "caspar-graphics": "./bin/caspar-graphics.js"
11 | },
12 | "main": "./dist/caspar-graphics.umd.cjs",
13 | "module": "./dist/caspar-graphics.js",
14 | "exports": {
15 | ".": {
16 | "import": "./dist/caspar-graphics.js",
17 | "require": "./dist/caspar-graphics.umd.cjs"
18 | },
19 | "./dist/*.css": {
20 | "import": "./dist/*.css",
21 | "require": "./dist/*.css"
22 | }
23 | },
24 | "scripts": {
25 | "dev": "vite dev",
26 | "build": "vite build && vite build -c vite.config.lib.js",
27 | "prepublishOnly": "vite build && vite build -c vite.config.lib.js",
28 | "preview": "vite preview"
29 | },
30 | "files": [
31 | "bin",
32 | "src",
33 | "dist"
34 | ],
35 | "keywords": [
36 | "CasparCG",
37 | "graphics",
38 | "HTML graphics"
39 | ],
40 | "prettier": {
41 | "tabWidth": 2,
42 | "semi": false,
43 | "singleQuote": true
44 | },
45 | "dependencies": {
46 | "@monaco-editor/react": "^4.6.0",
47 | "@radix-ui/colors": "^0.1.8",
48 | "@radix-ui/react-checkbox": "^1.0.1",
49 | "@radix-ui/react-collapsible": "^1.0.1",
50 | "@radix-ui/react-dropdown-menu": "^2.0.1",
51 | "@radix-ui/react-popover": "^1.0.2",
52 | "@radix-ui/react-switch": "^1.0.1",
53 | "@radix-ui/react-tabs": "^1.0.1",
54 | "@vitejs/plugin-react": "4.4.1",
55 | "@vitejs/plugin-vue": "5.2.3",
56 | "cac": "^6.7.14",
57 | "chalk": "5.1.2",
58 | "chokidar": "^3.5.3",
59 | "framer-motion": "^7.6.9",
60 | "get-port": "^6.1.2",
61 | "json5": "^2.2.1",
62 | "monaco-editor": "^0.45.0",
63 | "prop-types": "^15.8.1",
64 | "react": "^18.2.0",
65 | "react-colorful": "^5.6.1",
66 | "react-dom": "^18.2.0",
67 | "react-icons": "^4.6.0",
68 | "vite": "5.0.10",
69 | "vite-plugin-singlefile": "^0.13.5",
70 | "ws": "^8.11.0"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/src/client/Checkbox.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as Primitive from '@radix-ui/react-checkbox'
3 | import styles from './checkbox.module.css'
4 | import { MdCheck } from 'react-icons/md'
5 |
6 | export const Checkbox = ({ value, onChange, label, id }) => {
7 | return (
8 |
9 |
15 |
16 |
17 |
18 |
19 | {label != null && (
20 |
21 |
24 | )}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/src/client/Input.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styles from './input.module.css'
3 |
4 | export const Input = ({ type = 'text', id, label, onChange, ...props }) => {
5 | return (
6 |
7 |
8 | {
13 | onChange(evt.target.value)
14 | }}
15 | {...props}
16 | />
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/src/client/JsonEditor.jsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useRef, useState, useImperativeHandle } from 'react'
2 | import Editor from '@monaco-editor/react'
3 | import JSON5 from 'json5'
4 |
5 | export const JsonEditor = forwardRef(function JsonEditor(
6 | { value, onChange },
7 | forwardedRef
8 | ) {
9 | const ref = useRef()
10 | const [editor, setEditor] = useState()
11 | const stringified = JSON.stringify(value, null, 2)
12 |
13 | useImperativeHandle(
14 | forwardedRef,
15 | () => ({
16 | getValue: () => {
17 | try {
18 | editor.updateOptions({ renderValidationDecorations: 'off' })
19 | return JSON5.parse(editor.getValue())
20 | } catch (err) {
21 | console.warn('Invalid JSON', err)
22 | return null
23 | }
24 | }
25 | }),
26 | [editor]
27 | )
28 |
29 | return (
30 |
37 | {
67 | const theme = {
68 | base: 'vs-dark',
69 | inherit: false,
70 | rules: [
71 | { token: '', foreground: 'fafafa', background: '151718' },
72 | { token: 'number', foreground: 'fd9d63', fontStyle: 'bold' },
73 | { token: 'delimiter', foreground: 'fafafa' },
74 | { token: 'string.key.json', foreground: '73daca' },
75 | { token: 'string.value.json', foreground: '96c466' },
76 | { token: 'string', foreground: 'fafafa' },
77 | { token: 'keyword.json', foreground: 'fd9d63', fontStyle: 'bold' }
78 | ],
79 | colors: {
80 | 'editor.background': '#151718',
81 | 'editor.foreground': '#fafafa'
82 | }
83 | }
84 | monaco.editor.defineTheme('custom', theme)
85 | }}
86 | onMount={editor => {
87 | setEditor(editor)
88 | }}
89 | onChange={(value) => {
90 | try {
91 | onChange(JSON5.parse(value))
92 | } catch (err) {
93 | // TODO: show that the state is invalid
94 | }
95 | }}
96 | />
97 |
98 | )
99 | })
100 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/src/client/Menu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as MenuPrimitive from '@radix-ui/react-dropdown-menu'
3 | import styles from './menu.module.css'
4 | import { MdChevronRight } from 'react-icons/md'
5 |
6 | export const Menu = MenuPrimitive.Root
7 | export const MenuTrigger = MenuPrimitive.Trigger
8 |
9 | export const MenuContent = ({ children, ...props }) => {
10 | return (
11 |
12 | {children}
13 |
14 |
15 | )
16 | }
17 |
18 | export const MenuItem = ({ children, ...props }) => {
19 | return (
20 |
21 | {children}
22 |
23 | )
24 | }
25 |
26 | export const MenuRadioGroup = ({ children, ...props }) => {
27 | return (
28 |
29 | {children}
30 |
31 | )
32 | }
33 |
34 | export const MenuRadioItem = ({ children, ...props }) => {
35 | return (
36 |
37 |
38 |
50 |
51 | {children}
52 |
53 | )
54 | }
55 |
56 | export const MenuSub = MenuPrimitive.Sub
57 |
58 | export const MenuSubContent = ({ children, ...props }) => {
59 | return (
60 |
61 | {children}
62 |
63 | )
64 | }
65 |
66 | export const MenuSubTrigger = ({ children }) => {
67 | return (
68 |
69 | {children}
70 |
71 |
72 |
73 |
74 | )
75 | }
76 |
77 | export const MenuLabel = ({ children }) => {
78 | return (
79 |
80 | {children}
81 |
82 | )
83 | }
84 |
85 | export const MenuSeparator = () => {
86 | return
87 | }
88 |
89 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/src/client/Popover.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as PopoverPrimitive from '@radix-ui/react-popover'
3 |
4 | export const Popover = PopoverPrimitive.Root
5 | export const PopoverTrigger = PopoverPrimitive.Trigger
6 | export const PopoverContent = props => {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/src/client/Screen.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useSize } from '@radix-ui/react-use-size'
3 | import styles from './screen.module.css'
4 |
5 | export const Screen = ({ children, background, image, aspectRatio }) => {
6 | const [ref, setRef] = useState()
7 | const containerSize = useSize(ref)
8 |
9 | let width = containerSize?.width
10 | let height = containerSize?.height
11 |
12 | if (aspectRatio && containerSize) {
13 | const containerAspectRatio = containerSize.width / containerSize.height
14 | const [rWidth, rHeight] = aspectRatio.split(':').map(Number)
15 | const ratio = rWidth / rHeight
16 |
17 | if (ratio > containerAspectRatio) {
18 | width = containerSize.width
19 | height = containerSize.width / ratio
20 | } else {
21 | width = containerSize.height * ratio
22 | height = containerSize.height
23 | }
24 | }
25 |
26 | return (
27 |
28 |
36 | {containerSize ? children({ width, height }) : null}
37 | {image ? (
38 |

43 | ) : null}
44 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/src/client/Settings.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import styles from './settings.module.css'
3 | import { MdMenu, MdMoreHoriz, MdMoreVert, MdSettings } from 'react-icons/md'
4 | import { FiSun, FiMoon, FiChevronsLeft } from 'react-icons/fi'
5 | import { IoIosSettings } from 'react-icons/io'
6 | import { Switch } from './Switch'
7 | import { HexColorPicker } from 'react-colorful'
8 | import { Popover, PopoverTrigger, PopoverContent } from './Popover'
9 | import { Input } from './Input'
10 | import {
11 | Menu,
12 | MenuTrigger,
13 | MenuContent,
14 | MenuRadioGroup,
15 | MenuRadioItem
16 | } from './Menu'
17 | import { ASPECT_RATIOS } from './app'
18 |
19 | export const TopSettings = ({ value, onChange }) => {
20 | const {
21 | showSidebar = true,
22 | autoPlay,
23 | showJson,
24 | serverUrl,
25 | serverChannel,
26 | } = value
27 |
28 | if (!showSidebar) {
29 | return (
30 |
31 |
40 |
41 | )
42 | }
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
55 | CasparCG Server
56 | {
60 | onChange((value) => ({ ...value, serverUrl }))
61 | }}
62 | />
63 | {
69 | onChange((value) => ({ ...value, serverChannel }))
70 | }}
71 | />
72 |
73 |
74 |
75 |
76 | {
82 | onChange((value) => ({ ...value, autoPlay }))
83 | }}
84 | />
85 |
86 |
87 |
88 | {
94 | onChange((value) => ({ ...value, showJson }))
95 | }}
96 | />
97 |
98 |
106 |
107 | )
108 | }
109 |
110 | export const BottomSettings = ({
111 | settings,
112 | onSettingsChange,
113 | projectState,
114 | onProjectStateChange,
115 | colorMode,
116 | }) => {
117 | const { colorScheme } = settings
118 | const { background, aspectRatio } = projectState || {}
119 |
120 | return (
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | {
132 | onProjectStateChange((value) => ({ ...value, background }))
133 | }}
134 | />
135 |
136 |
137 |
138 |
155 |
173 |
174 | )
175 | }
176 |
177 | const GuideIcon = () => {
178 | return (
179 |
227 | )
228 | }
229 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/src/client/Switch.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as SwitchPrimitive from '@radix-ui/react-switch'
3 | import styles from './switch.module.css'
4 | import { motion } from 'framer-motion'
5 |
6 | export const Switch = ({ checked, onChange, className, labels, id, ...props }) => {
7 | return (
8 |
16 |
17 |
23 |
24 | {labels !== false && (
25 | <>
26 |
27 | Off
28 |
29 |
30 | On
31 |
32 | >
33 | )}
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/packages/caspar-graphics/src/client/TemplatePreview.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 |
3 | const States = {
4 | load: 0,
5 | loaded: 1,
6 | play: 2,
7 | stop: 3,
8 | }
9 |
10 | export const ServerTemplate = ({ socket, name, layer, show, data }) => {
11 | const [state, setState] = useState(States.load)
12 | const [prevUpdate, setPrevUpdate] = useState()
13 | const source = `/templates/${name}/index.html`
14 | const nextUpdate =
15 | data && JSON.stringify(data) !== JSON.stringify(prevUpdate || {})
16 | ? data
17 | : null
18 |
19 | // Load
20 | useEffect(() => {
21 | if (socket && state === States.load) {
22 | socket.send(JSON.stringify({ type: 'load', source, layer }))
23 | setState(States.loaded)
24 | }
25 | }, [socket, source, state])
26 |
27 | // Data Updates
28 | useEffect(() => {
29 | if (
30 | socket &&
31 | (state === States.loaded || state === States.play) &&
32 | nextUpdate
33 | ) {
34 | socket.send(JSON.stringify({ type: 'update', layer, data: nextUpdate }))
35 | setPrevUpdate(nextUpdate)
36 | }
37 | }, [socket, state, nextUpdate])
38 |
39 | // State Updates
40 | useEffect(() => {
41 | if (!socket) {
42 | return
43 | }
44 |
45 | if (show && state === States.loaded) {
46 | socket.send(JSON.stringify({ type: 'play', layer }))
47 | setState(States.play)
48 | } else if (!show && state === States.play) {
49 | socket.send(JSON.stringify({ type: 'stop', layer }))
50 | setState(States.stop)
51 |
52 | // TODO: wait for window.remove() to be called.
53 | const timeout = window.setTimeout(() => {
54 | setState(States.load)
55 | setPrevUpdate(null)
56 | }, 2000)
57 |
58 | return () => {
59 | window.clearTimeout(timeout)
60 | }
61 | }
62 | }, [socket, show, state])
63 |
64 | return null
65 | }
66 |
67 | export const TemplatePreview = ({
68 | dispatch,
69 | projectSize,
70 | containerSize,
71 | onKeyDown,
72 | manifest,
73 | name,
74 | src = `/templates/${name}/index.html`,
75 | show,
76 | data,
77 | }) => {
78 | const [templateWindow, setTemplateWindow] = useState()
79 | const [didShow, setDidShow] = useState(false)
80 | const ref = useRef()
81 |
82 | // Data Updates
83 | useEffect(() => {
84 | if (templateWindow?.update) {
85 | templateWindow.update(data || {})
86 | }
87 | }, [templateWindow, data])
88 |
89 | // State Updates
90 | useEffect(() => {
91 | if (!templateWindow) {
92 | return
93 | }
94 |
95 | if (show && !didShow) {
96 | templateWindow.play()
97 | setDidShow(true)
98 | } else if (!show && didShow) {
99 | if (templateWindow.stop) {
100 | templateWindow.stop()
101 | } else {
102 | console.warn('No window.stop() found')
103 | }
104 | }
105 | }, [templateWindow, show, didShow])
106 |
107 | // Forward keybindings
108 | useEffect(() => {
109 | if (!templateWindow) {
110 | return
111 | }
112 |
113 | templateWindow.addEventListener('keydown', onKeyDown)
114 |
115 | return () => {
116 | templateWindow.removeEventListener('keydown', onKeyDown)
117 | }
118 | }, [templateWindow, onKeyDown])
119 |
120 | let width = containerSize.width
121 | let height = containerSize.height
122 |
123 | if (manifest.width || manifest.height) {
124 | width = manifest.width
125 | height = manifest.height
126 |
127 | if (!width && height) {
128 | width = (containerSize.width / containerSize.height) * height
129 | } else if (width && !height) {
130 | height = (containerSize.height / containerSize.width) * width
131 | }
132 | }
133 |
134 | return (
135 |