├── .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 | Animated Remotion Logo 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 |     
40 |
41 |
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 | --------------------------------------------------------------------------------