├── .gitignore
├── .prettierrc
├── README.md
├── eslint.config.mjs
├── package.json
├── public
├── code1.tsx
├── code2.tsx
├── code3.tsx
└── code4.swift
├── remotion.config.ts
├── src
├── CodeTransition.tsx
├── Main.tsx
├── ProgressBar.tsx
├── ReloadOnCodeChange.tsx
├── Root.tsx
├── annotations
│ ├── Callout.tsx
│ ├── Error.tsx
│ └── InlineToken.tsx
├── calculate-metadata
│ ├── calculate-metadata.tsx
│ ├── get-files.ts
│ ├── process-snippet.ts
│ ├── schema.ts
│ └── theme.tsx
├── font.ts
├── index.ts
└── utils.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .DS_Store
4 | .env
5 |
6 | # Ignore the output video from Git but not videos you import into src/.
7 | out
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "bracketSpacing": true,
4 | "tabWidth": 2
5 | }
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Remotion video
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Welcome to your Remotion project!
13 |
14 | ## Commands
15 |
16 | **Install Dependencies**
17 |
18 | ```console
19 | npm i
20 | ```
21 |
22 | **Start Preview**
23 |
24 | ```console
25 | npm run dev
26 | ```
27 |
28 | **Change code snippets**
29 |
30 | The snippets are located in the `public` folder.
31 | Change the code or create new files in there.
32 |
33 | **Render video**
34 |
35 | ```console
36 | npx remotion render
37 | ```
38 |
39 | **Upgrade Remotion**
40 |
41 | ```console
42 | npx remotion upgrade
43 | ```
44 |
45 | ## More examples
46 |
47 | Visit the [Code Hike examples](https://github.com/code-hike/examples/tree/main/with-remotion) for more variants of code animations.
48 |
49 | ## Docs
50 |
51 | Get started with Remotion by reading the [fundamentals page](https://www.remotion.dev/docs/the-fundamentals).
52 |
53 | ## Help
54 |
55 | We provide help on our [Discord server](https://discord.gg/6VzzNDwUwV).
56 |
57 | ## Issues
58 |
59 | Found an issue with Remotion? [File an issue here](https://github.com/remotion-dev/remotion/issues/new).
60 |
61 | ## License
62 |
63 | Note that for some entities a company license is needed. [Read the terms here](https://github.com/remotion-dev/remotion/blob/main/LICENSE.md).
64 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { config } from "@remotion/eslint-config-flat";
2 |
3 | export default config;
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "template-code-hike",
3 | "version": "1.0.0",
4 | "description": "My Remotion video",
5 | "repository": {},
6 | "license": "UNLICENSED",
7 | "type": "module",
8 | "dependencies": {
9 | "@code-hike/lighter": "1.0.3",
10 | "@remotion/cli": "^4.0.0",
11 | "@remotion/google-fonts": "^4.0.0",
12 | "@remotion/studio": "^4.0.0",
13 | "@remotion/layout-utils": "^4.0.0",
14 | "codehike": "1.0.4",
15 | "react": "19.0.0",
16 | "react-dom": "19.0.0",
17 | "remotion": "^4.0.0",
18 | "twoslash-cdn": "0.3.1",
19 | "polished": "4.3.1",
20 | "zod": "3.22.3"
21 | },
22 | "devDependencies": {
23 | "@remotion/eslint-config-flat": "^4.0.0",
24 | "@types/react": "19.0.0",
25 | "@types/web": "0.0.166",
26 | "eslint": "9.19.0",
27 | "prettier": "3.3.3",
28 | "typescript": "5.8.2"
29 | },
30 | "scripts": {
31 | "dev": "remotion studio",
32 | "build": "remotion bundle",
33 | "upgrade": "remotion upgrade",
34 | "lint": "tsc && eslint src"
35 | },
36 | "private": true
37 | }
38 |
--------------------------------------------------------------------------------
/public/code1.tsx:
--------------------------------------------------------------------------------
1 | const user = {
2 | name: 'Lorem',
3 | age: 26,
4 | };
5 |
6 | console.log(user);
7 | // ^?
8 |
--------------------------------------------------------------------------------
/public/code2.tsx:
--------------------------------------------------------------------------------
1 | const user = {
2 | name: 'Lorem',
3 | age: 26,
4 | };
5 | // @errors: 2339
6 | console.log(user.location);
7 |
--------------------------------------------------------------------------------
/public/code3.tsx:
--------------------------------------------------------------------------------
1 | const user = {
2 | name: 'Lorem',
3 | age: 26,
4 | location: 'Ipsum',
5 | };
6 |
7 | console.log(user.location);
8 | // ^?
9 |
--------------------------------------------------------------------------------
/public/code4.swift:
--------------------------------------------------------------------------------
1 | class Person {
2 | var name: String
3 | var age: Int
4 |
5 | init(name: String, age: Int) {
6 | self.name = name
7 | self.age = age
8 | }
9 | }
10 |
11 | let user = Person(name: "Lorem", age: 26)
12 |
13 | print(user.location)
14 |
15 |
16 |
--------------------------------------------------------------------------------
/remotion.config.ts:
--------------------------------------------------------------------------------
1 | import {Config} from '@remotion/cli/config';
2 |
3 | Config.setVideoImageFormat('jpeg');
4 | Config.setOverwriteOutput(true);
5 |
--------------------------------------------------------------------------------
/src/CodeTransition.tsx:
--------------------------------------------------------------------------------
1 | import { Easing, interpolate } from "remotion";
2 | import { continueRender, delayRender, useCurrentFrame } from "remotion";
3 | import { Pre, HighlightedCode, AnnotationHandler } from "codehike/code";
4 | import React, { useEffect, useLayoutEffect, useMemo, useState } from "react";
5 |
6 | import {
7 | calculateTransitions,
8 | getStartingSnapshot,
9 | TokenTransitionsSnapshot,
10 | } from "codehike/utils/token-transitions";
11 | import { applyStyle } from "./utils";
12 | import { callout } from "./annotations/Callout";
13 |
14 | import { tokenTransitions } from "./annotations/InlineToken";
15 | import { errorInline, errorMessage } from "./annotations/Error";
16 | import { fontFamily, fontSize, tabSize } from "./font";
17 |
18 | export function CodeTransition({
19 | oldCode,
20 | newCode,
21 | durationInFrames = 30,
22 | }: {
23 | readonly oldCode: HighlightedCode | null;
24 | readonly newCode: HighlightedCode;
25 | readonly durationInFrames?: number;
26 | }) {
27 | const frame = useCurrentFrame();
28 |
29 | const ref = React.useRef(null);
30 | const [oldSnapshot, setOldSnapshot] =
31 | useState(null);
32 | const [handle] = React.useState(() => delayRender());
33 |
34 | const prevCode: HighlightedCode = useMemo(() => {
35 | return oldCode || { ...newCode, tokens: [], annotations: [] };
36 | }, [newCode, oldCode]);
37 |
38 | const code = useMemo(() => {
39 | return oldSnapshot ? newCode : prevCode;
40 | }, [newCode, prevCode, oldSnapshot]);
41 |
42 | useEffect(() => {
43 | if (!oldSnapshot) {
44 | setOldSnapshot(getStartingSnapshot(ref.current!));
45 | }
46 | }, [oldSnapshot]);
47 |
48 | // eslint-disable-next-line react-hooks/exhaustive-deps
49 | useLayoutEffect(() => {
50 | if (!oldSnapshot) {
51 | setOldSnapshot(getStartingSnapshot(ref.current!));
52 | return;
53 | }
54 | const transitions = calculateTransitions(ref.current!, oldSnapshot);
55 | transitions.forEach(({ element, keyframes, options }) => {
56 | const delay = durationInFrames * options.delay;
57 | const duration = durationInFrames * options.duration;
58 | const linearProgress = interpolate(
59 | frame,
60 | [delay, delay + duration],
61 | [0, 1],
62 | {
63 | extrapolateLeft: "clamp",
64 | extrapolateRight: "clamp",
65 | },
66 | );
67 | const progress = interpolate(linearProgress, [0, 1], [0, 1], {
68 | easing: Easing.bezier(0.17, 0.67, 0.76, 0.91),
69 | });
70 |
71 | applyStyle({
72 | element,
73 | keyframes,
74 | progress,
75 | linearProgress,
76 | });
77 | });
78 | continueRender(handle);
79 | });
80 |
81 | const handlers: AnnotationHandler[] = useMemo(() => {
82 | return [tokenTransitions, callout, errorInline, errorMessage];
83 | }, []);
84 |
85 | const style: React.CSSProperties = useMemo(() => {
86 | return {
87 | position: "relative",
88 | fontSize,
89 | lineHeight: 1.5,
90 | fontFamily,
91 | tabSize,
92 | };
93 | }, []);
94 |
95 | return ;
96 | }
97 |
--------------------------------------------------------------------------------
/src/Main.tsx:
--------------------------------------------------------------------------------
1 | import { AbsoluteFill, Series, useVideoConfig } from "remotion";
2 | import { ProgressBar } from "./ProgressBar";
3 | import { CodeTransition } from "./CodeTransition";
4 | import { HighlightedCode } from "codehike/code";
5 | import { ThemeColors, ThemeProvider } from "./calculate-metadata/theme";
6 | import { useMemo } from "react";
7 | import { RefreshOnCodeChange } from "./ReloadOnCodeChange";
8 | import { verticalPadding } from "./font";
9 |
10 | export type Props = {
11 | steps: HighlightedCode[] | null;
12 | themeColors: ThemeColors | null;
13 | codeWidth: number | null;
14 | };
15 |
16 | export const Main: React.FC = ({ steps, themeColors, codeWidth }) => {
17 | if (!steps) {
18 | throw new Error("Steps are not defined");
19 | }
20 |
21 | const { durationInFrames } = useVideoConfig();
22 | const stepDuration = durationInFrames / steps.length;
23 | const transitionDuration = 30;
24 |
25 | if (!themeColors) {
26 | throw new Error("Theme colors are not defined");
27 | }
28 |
29 | const outerStyle: React.CSSProperties = useMemo(() => {
30 | return {
31 | backgroundColor: themeColors.background,
32 | };
33 | }, [themeColors]);
34 |
35 | const style: React.CSSProperties = useMemo(() => {
36 | return {
37 | padding: `${verticalPadding}px 0px`,
38 | };
39 | }, []);
40 |
41 | return (
42 |
43 |
44 |
50 |
51 |
52 |
53 | {steps.map((step, index) => (
54 |
60 |
65 |
66 | ))}
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/src/ProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { useCurrentFrame, useVideoConfig } from "remotion";
3 | import { useThemeColors } from "./calculate-metadata/theme";
4 | import React from "react";
5 |
6 | const Step: React.FC<{
7 | readonly index: number;
8 | readonly currentStep: number;
9 | readonly currentStepProgress: number;
10 | }> = ({ index, currentStep, currentStepProgress }) => {
11 | const themeColors = useThemeColors();
12 |
13 | const outer: React.CSSProperties = useMemo(() => {
14 | return {
15 | backgroundColor:
16 | themeColors.editor.lineHighlightBackground ??
17 | themeColors.editor.rangeHighlightBackground,
18 | borderRadius: 6,
19 | overflow: "hidden",
20 | height: "100%",
21 | flex: 1,
22 | };
23 | }, [themeColors]);
24 |
25 | const inner: React.CSSProperties = useMemo(() => {
26 | return {
27 | height: "100%",
28 | backgroundColor: themeColors.icon.foreground,
29 | width:
30 | index > currentStep
31 | ? 0
32 | : index === currentStep
33 | ? currentStepProgress * 100 + "%"
34 | : "100%",
35 | };
36 | }, [themeColors.icon.foreground, index, currentStep, currentStepProgress]);
37 |
38 | return (
39 |
42 | );
43 | };
44 |
45 | export function ProgressBar({ steps }: { readonly steps: unknown[] }) {
46 | const frame = useCurrentFrame();
47 | const { durationInFrames } = useVideoConfig();
48 |
49 | const stepDuration = durationInFrames / steps.length;
50 | const currentStep = Math.floor(frame / stepDuration);
51 | const currentStepProgress = (frame % stepDuration) / stepDuration;
52 |
53 | const container: React.CSSProperties = useMemo(() => {
54 | return {
55 | position: "absolute",
56 | top: 48,
57 | height: 6,
58 | left: 0,
59 | right: 0,
60 | display: "flex",
61 | gap: 12,
62 | };
63 | }, []);
64 |
65 | return (
66 |
67 | {steps.map((_, index) => (
68 |
74 | ))}
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/src/ReloadOnCodeChange.tsx:
--------------------------------------------------------------------------------
1 | import { getStaticFiles, reevaluateComposition } from "@remotion/studio";
2 | import { useState } from "react";
3 | import React, { useEffect } from "react";
4 | import { watchPublicFolder } from "@remotion/studio";
5 | import { getRemotionEnvironment } from "remotion";
6 |
7 | const getCurrentHash = () => {
8 | const files = getStaticFiles();
9 | const codeFiles = files.filter((file) => file.name.startsWith("code"));
10 | const contents = codeFiles.map((file) => file.src + file.lastModified);
11 | return contents.join("");
12 | };
13 |
14 | export const RefreshOnCodeChange: React.FC = () => {
15 | const [files, setFiles] = useState(getCurrentHash());
16 |
17 | useEffect(() => {
18 | if (getRemotionEnvironment().isReadOnlyStudio) {
19 | return;
20 | }
21 |
22 | const { cancel } = watchPublicFolder(() => {
23 | const hash = getCurrentHash();
24 | if (hash !== files) {
25 | setFiles(hash);
26 | reevaluateComposition();
27 | }
28 | });
29 |
30 | return () => {
31 | cancel();
32 | };
33 | }, [files]);
34 |
35 | return null;
36 | };
37 |
--------------------------------------------------------------------------------
/src/Root.tsx:
--------------------------------------------------------------------------------
1 | import { Composition } from "remotion";
2 | import { Main } from "./Main";
3 |
4 | import { calculateMetadata } from "./calculate-metadata/calculate-metadata";
5 | import { schema } from "./calculate-metadata/schema";
6 |
7 | export const RemotionRoot = () => {
8 | return (
9 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/annotations/Callout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | InlineAnnotation,
3 | AnnotationHandler,
4 | InnerLine,
5 | Pre,
6 | } from "codehike/code";
7 | import { interpolate, useCurrentFrame } from "remotion";
8 | import { useThemeColors } from "../calculate-metadata/theme";
9 | import { mix, readableColor } from "polished";
10 |
11 | export const callout: AnnotationHandler = {
12 | name: "callout",
13 | transform: (annotation: InlineAnnotation) => {
14 | const { name, query, lineNumber, fromColumn, toColumn, data } = annotation;
15 | return {
16 | name,
17 | query,
18 | fromLineNumber: lineNumber,
19 | toLineNumber: lineNumber,
20 | data: { ...data, column: (fromColumn + toColumn) / 2 },
21 | };
22 | },
23 | AnnotatedLine: ({ annotation, ...props }) => {
24 | const { column, codeblock } = annotation.data;
25 | const { indentation } = props;
26 | const frame = useCurrentFrame();
27 |
28 | const opacity = interpolate(frame, [25, 35], [0, 1], {
29 | extrapolateLeft: "clamp",
30 | extrapolateRight: "clamp",
31 | });
32 |
33 | const themeColors = useThemeColors();
34 |
35 | const color = readableColor(themeColors.background);
36 | const calloutColor = mix(0.08, color, themeColors.background);
37 |
38 | return (
39 | <>
40 |
41 |
55 |
66 | {codeblock ? (
67 |
71 | ) : (
72 | annotation.data.children || annotation.query
73 | )}
74 |
75 | >
76 | );
77 | },
78 | };
79 |
--------------------------------------------------------------------------------
/src/annotations/Error.tsx:
--------------------------------------------------------------------------------
1 | import { InlineAnnotation, AnnotationHandler, InnerToken } from "codehike/code";
2 | import { interpolate, useCurrentFrame } from "remotion";
3 | import { useThemeColors } from "../calculate-metadata/theme";
4 | import { mix, readableColor } from "polished";
5 |
6 | export const errorInline: AnnotationHandler = {
7 | name: "error",
8 | transform: (annotation: InlineAnnotation) => {
9 | const { query, lineNumber, data } = annotation;
10 | return [
11 | annotation,
12 | {
13 | name: "error-message",
14 | query,
15 | fromLineNumber: lineNumber,
16 | toLineNumber: lineNumber,
17 | data,
18 | },
19 | ];
20 | },
21 | Inline: ({ children }) => (
22 |
28 | {children}
29 |
30 | ),
31 | Token: (props) => {
32 | return (
33 |
39 | );
40 | },
41 | };
42 |
43 | export const errorMessage: AnnotationHandler = {
44 | name: "error-message",
45 | Block: ({ annotation, children }) => {
46 | const frame = useCurrentFrame();
47 | const opacity = interpolate(frame, [25, 35], [0, 1], {
48 | extrapolateLeft: "clamp",
49 | extrapolateRight: "clamp",
50 | });
51 | const themeColors = useThemeColors();
52 |
53 | const color = readableColor(themeColors.background);
54 | const calloutColor = mix(0.08, color, themeColors.background);
55 |
56 | return (
57 | <>
58 | {children}
59 |
71 | {annotation.data.children || annotation.query}
72 |
73 | >
74 | );
75 | },
76 | };
77 |
--------------------------------------------------------------------------------
/src/annotations/InlineToken.tsx:
--------------------------------------------------------------------------------
1 | import { AnnotationHandler, InnerToken } from "codehike/code";
2 |
3 | export const tokenTransitions: AnnotationHandler = {
4 | name: "token-transitions",
5 | Token: ({ ...props }) => (
6 |
7 | ),
8 | };
9 |
--------------------------------------------------------------------------------
/src/calculate-metadata/calculate-metadata.tsx:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { CalculateMetadataFunction } from "remotion";
3 | import { getThemeColors } from "@code-hike/lighter";
4 | import { Props } from "../Main";
5 | import { schema } from "./schema";
6 | import { processSnippet } from "./process-snippet";
7 | import { getFiles } from "./get-files";
8 | import { measureText } from "@remotion/layout-utils";
9 | import {
10 | fontFamily,
11 | fontSize,
12 | horizontalPadding,
13 | tabSize,
14 | waitUntilDone,
15 | } from "../font";
16 | import { HighlightedCode } from "codehike/code";
17 |
18 | export const calculateMetadata: CalculateMetadataFunction<
19 | Props & z.infer
20 | > = async ({ props }) => {
21 | const contents = await getFiles();
22 |
23 | await waitUntilDone();
24 | const widthPerCharacter = measureText({
25 | text: "A",
26 | fontFamily,
27 | fontSize,
28 | validateFontIsLoaded: true,
29 | }).width;
30 |
31 | const maxCharacters = Math.max(
32 | ...contents
33 | .map(({ value }) => value.split("\n"))
34 | .flat()
35 | .map((value) => value.replaceAll("\t", " ".repeat(tabSize)).length)
36 | .flat(),
37 | );
38 | const codeWidth = widthPerCharacter * maxCharacters;
39 |
40 | const defaultStepDuration = 90;
41 |
42 | const themeColors = await getThemeColors(props.theme);
43 |
44 | const twoSlashedCode: HighlightedCode[] = [];
45 | for (const snippet of contents) {
46 | twoSlashedCode.push(await processSnippet(snippet, props.theme));
47 | }
48 |
49 | const naturalWidth = codeWidth + horizontalPadding * 2;
50 | const divisibleByTwo = Math.ceil(naturalWidth / 2) * 2; // MP4 requires an even width
51 |
52 | const minimumWidth = props.width.type === "fixed" ? 0 : 1080;
53 | const minimumWidthApplied = Math.max(minimumWidth, divisibleByTwo);
54 |
55 | return {
56 | durationInFrames: contents.length * defaultStepDuration,
57 | width:
58 | props.width.type === "fixed"
59 | ? Math.max(minimumWidthApplied, props.width.value)
60 | : minimumWidthApplied,
61 | props: {
62 | theme: props.theme,
63 | width: props.width,
64 | steps: twoSlashedCode,
65 | themeColors,
66 | codeWidth,
67 | },
68 | };
69 | };
70 |
--------------------------------------------------------------------------------
/src/calculate-metadata/get-files.ts:
--------------------------------------------------------------------------------
1 | import { getStaticFiles } from "@remotion/studio";
2 |
3 | export type PublicFolderFile = {
4 | filename: string;
5 | value: string;
6 | };
7 |
8 | export const getFiles = async () => {
9 | const files = getStaticFiles();
10 | const codeFiles = files.filter((file) => file.name.startsWith("code"));
11 |
12 | const contents = codeFiles.map(async (file): Promise => {
13 | const contents = await fetch(file.src);
14 | const text = await contents.text();
15 |
16 | return { filename: file.name, value: text };
17 | });
18 |
19 | return Promise.all(contents);
20 | };
21 |
--------------------------------------------------------------------------------
/src/calculate-metadata/process-snippet.ts:
--------------------------------------------------------------------------------
1 | import { highlight } from "codehike/code";
2 | import { createTwoslashFromCDN } from "twoslash-cdn";
3 | import { PublicFolderFile } from "./get-files";
4 | import { Theme } from "./theme";
5 | import { CompilerOptions, JsxEmit, ModuleKind, ScriptTarget } from "typescript";
6 |
7 | const compilerOptions: CompilerOptions = {
8 | lib: ["dom", "es2023"],
9 | jsx: JsxEmit.ReactJSX,
10 | target: ScriptTarget.ES2023,
11 | module: ModuleKind.ESNext,
12 | };
13 |
14 | const twoslash = createTwoslashFromCDN({
15 | compilerOptions,
16 | });
17 |
18 | export const processSnippet = async (step: PublicFolderFile, theme: Theme) => {
19 | const splitted = step.filename.split(".");
20 | const extension = splitted[splitted.length - 1];
21 |
22 | const twoslashResult =
23 | extension === "ts" || extension === "tsx"
24 | ? await twoslash.run(step.value, extension, {
25 | compilerOptions,
26 | })
27 | : null;
28 |
29 | const highlighted = await highlight(
30 | {
31 | lang: extension,
32 | meta: "",
33 | value: twoslashResult ? twoslashResult.code : step.value,
34 | },
35 | theme,
36 | );
37 |
38 | if (!twoslashResult) {
39 | return highlighted;
40 | }
41 |
42 | // If it is TypeScript code, let's also generate callouts (^?) and errors
43 | for (const { text, line, character, length } of twoslashResult.queries) {
44 | const codeblock = await highlight(
45 | { value: text, lang: "ts", meta: "callout" },
46 | theme,
47 | );
48 | highlighted.annotations.push({
49 | name: "callout",
50 | query: text,
51 | lineNumber: line + 1,
52 | data: {
53 | character,
54 | codeblock,
55 | },
56 | fromColumn: character,
57 | toColumn: character + length,
58 | });
59 | }
60 |
61 | for (const { text, line, character, length } of twoslashResult.errors) {
62 | highlighted.annotations.push({
63 | name: "error",
64 | query: text,
65 | lineNumber: line + 1,
66 | data: { character },
67 | fromColumn: character,
68 | toColumn: character + length,
69 | });
70 | }
71 |
72 | return highlighted;
73 | };
74 |
--------------------------------------------------------------------------------
/src/calculate-metadata/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { themeSchema } from "./theme";
3 |
4 | export const width = z.discriminatedUnion("type", [
5 | z.object({
6 | type: z.literal("auto"),
7 | }),
8 | z.object({
9 | type: z.literal("fixed"),
10 | value: z.number().step(1),
11 | }),
12 | ]);
13 |
14 | export const schema = z.object({
15 | theme: themeSchema,
16 | width,
17 | });
18 |
--------------------------------------------------------------------------------
/src/calculate-metadata/theme.tsx:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { getThemeColors } from "@code-hike/lighter";
3 | import React from "react";
4 |
5 | export type ThemeColors = Awaited>;
6 |
7 | export const themeSchema = z.enum([
8 | "dark-plus",
9 | "dracula-soft",
10 | "dracula",
11 | "github-dark",
12 | "github-dark-dimmed",
13 | "github-light",
14 | "light-plus",
15 | "material-darker",
16 | "material-default",
17 | "material-lighter",
18 | "material-ocean",
19 | "material-palenight",
20 | "min-dark",
21 | "min-light",
22 | "monokai",
23 | "nord",
24 | "one-dark-pro",
25 | "poimandres",
26 | "slack-dark",
27 | "slack-ochin",
28 | "solarized-dark",
29 | "solarized-light",
30 | ]);
31 |
32 | export type Theme = z.infer;
33 |
34 | export const ThemeColorsContext = React.createContext(null);
35 |
36 | export const useThemeColors = () => {
37 | const themeColors = React.useContext(ThemeColorsContext);
38 | if (!themeColors) {
39 | throw new Error("ThemeColorsContext not found");
40 | }
41 |
42 | return themeColors;
43 | };
44 |
45 | export const ThemeProvider = ({
46 | children,
47 | themeColors,
48 | }: {
49 | readonly children: React.ReactNode;
50 | readonly themeColors: ThemeColors;
51 | }) => {
52 | return (
53 |
54 | {children}
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/src/font.ts:
--------------------------------------------------------------------------------
1 | import { loadFont } from "@remotion/google-fonts/RobotoMono";
2 |
3 | export const { fontFamily, waitUntilDone } = loadFont("normal", {
4 | subsets: ["latin"],
5 | weights: ["400", "700"],
6 | });
7 | export const fontSize = 40;
8 | export const tabSize = 3;
9 | export const horizontalPadding = 60;
10 | export const verticalPadding = 84;
11 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { registerRoot } from "remotion";
2 | import { RemotionRoot } from "./Root";
3 |
4 | registerRoot(RemotionRoot);
5 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { TokenTransition } from "codehike/utils/token-transitions";
2 | import { interpolate, interpolateColors } from "remotion";
3 |
4 | export function applyStyle({
5 | element,
6 | keyframes,
7 | progress,
8 | linearProgress,
9 | }: {
10 | element: HTMLElement;
11 | keyframes: TokenTransition["keyframes"];
12 | progress: number;
13 | linearProgress: number;
14 | }) {
15 | const { translateX, translateY, color, opacity } = keyframes;
16 |
17 | if (opacity) {
18 | element.style.opacity = linearProgress.toString();
19 | }
20 | if (color) {
21 | element.style.color = interpolateColors(progress, [0, 1], color);
22 | }
23 | const x = translateX ? interpolate(progress, [0, 1], translateX) : 0;
24 | const y = translateY ? interpolate(progress, [0, 1], translateY) : 0;
25 | element.style.translate = `${x}px ${y}px`;
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2021",
4 | "module": "Preserve",
5 | "moduleResolution": "Bundler",
6 | "jsx": "react-jsx",
7 | "strict": true,
8 | "noEmit": true,
9 | "lib": ["ES2021"],
10 | "esModuleInterop": true,
11 | "skipLibCheck": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noUnusedLocals": true
14 | },
15 | "exclude": ["public", "remotion.config.ts"]
16 | }
17 |
--------------------------------------------------------------------------------