├── .gitignore ├── LICENSE ├── README.md ├── biome.json ├── build.mjs ├── package.json ├── packages ├── captioning │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── transcription.ts │ │ └── webvtt.ts │ └── tsconfig.json ├── cli │ ├── liqvid-cli.mjs │ ├── package.json │ ├── src │ │ ├── index.mts │ │ └── tasks │ │ │ ├── audio.mts │ │ │ ├── build.mts │ │ │ ├── config.mts │ │ │ ├── index.mts │ │ │ ├── load-sync.cts │ │ │ ├── render.mts │ │ │ ├── serve.mts │ │ │ └── thumbs.mts │ └── tsconfig.json ├── diff │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── apply.ts │ │ ├── builders.ts │ │ ├── compute.ts │ │ ├── index.ts │ │ ├── merge.ts │ │ ├── runes.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tests │ │ └── suite.test.ts │ └── tsconfig.json ├── gsap │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── yarn.lock ├── host │ ├── README.md │ ├── lv-host.js │ └── package.json ├── katex │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── RenderGroup.ts │ │ ├── fancy.tsx │ │ ├── index.tsx │ │ ├── loading.ts │ │ └── plain.tsx │ └── tsconfig.json ├── keymap │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── mixedCaseVals.ts │ │ └── react.ts │ ├── tests │ │ └── keymap.test.ts │ └── tsconfig.json ├── magic │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── default-assets.ts │ │ ├── index.ts │ │ └── types.ts │ ├── tests │ │ └── magic.test.ts │ └── tsconfig.json ├── main │ ├── .env.example │ ├── CHANGELOG.md │ ├── DEVELOPMENT.md │ ├── README.md │ ├── e2e │ │ ├── app │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ └── index.tsx │ │ │ ├── static │ │ │ │ ├── index.html │ │ │ │ ├── liqvid.min.css │ │ │ │ └── style.css │ │ │ ├── tsconfig.json │ │ │ └── webpack.config.js │ │ └── tests │ │ │ └── Media.spec.tsx │ ├── jest.config.js │ ├── package.json │ ├── playwright.config.ts │ ├── pnpm-lock.yaml │ ├── rollup.config.js │ ├── src │ │ ├── Audio.tsx │ │ ├── CaptionsDisplay.tsx │ │ ├── Controls.tsx │ │ ├── IdMap.tsx │ │ ├── Media.ts │ │ ├── Player.tsx │ │ ├── Video.tsx │ │ ├── controls │ │ │ ├── Captions.tsx │ │ │ ├── FullScreen.tsx │ │ │ ├── PlayPause.tsx │ │ │ ├── ScrubberBar.tsx │ │ │ ├── Settings.tsx │ │ │ ├── ThumbnailBox.tsx │ │ │ ├── TimeDisplay.tsx │ │ │ └── Volume.tsx │ │ ├── fake-fullscreen.ts │ │ ├── hooks.ts │ │ ├── i18n.ts │ │ ├── index.ts │ │ ├── playback.ts │ │ ├── polyfills.ts │ │ ├── script.ts │ │ ├── utils.ts │ │ └── utils │ │ │ ├── authoring.ts │ │ │ ├── dom.ts │ │ │ ├── interactivity.ts │ │ │ ├── media.ts │ │ │ ├── mobile.ts │ │ │ └── rsc.ts │ ├── styl │ │ ├── controls │ │ │ ├── captions.styl │ │ │ ├── scrubber.styl │ │ │ ├── settings.styl │ │ │ ├── thumbs.styl │ │ │ ├── time.styl │ │ │ └── volume.styl │ │ ├── liqvid.styl │ │ └── mobile.styl │ ├── tests │ │ ├── DocumentTimeline.mock │ │ ├── IdMap.test.tsx │ │ ├── Player.test.tsx │ │ ├── controls │ │ │ ├── PlayPause.test.tsx │ │ │ ├── ScrubberBar.test.tsx │ │ │ ├── TimeDisplay.test.tsx │ │ │ ├── Volume.test.tsx │ │ │ └── __snapshots__ │ │ │ │ ├── PlayPause.test.tsx.snap │ │ │ │ └── Volume.test.tsx.snap │ │ ├── hooks.test.tsx │ │ ├── matchMedia.mock │ │ └── script.test.ts │ └── tsconfig.json ├── mathjax │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── RenderGroup.ts │ │ ├── fancy.tsx │ │ ├── index.ts │ │ ├── loading.ts │ │ └── plain.tsx │ ├── test │ │ └── index.js │ └── tsconfig.json ├── playback │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── animation.ts │ │ ├── core.ts │ │ ├── index.ts │ │ └── react.ts │ ├── tests │ │ └── core.test.ts │ └── tsconfig.json ├── player │ └── package.json ├── polyfills │ ├── package.json │ ├── src │ │ ├── polyfills.ts │ │ └── waapi.js │ └── yarn.lock ├── prompt │ ├── README.md │ ├── package.json │ ├── src │ │ ├── Cue.tsx │ │ ├── Prompt.tsx │ │ └── index.ts │ ├── style.css │ ├── style.styl │ └── tsconfig.json ├── react-three │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.tsx │ └── tsconfig.json ├── react │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── three.tsx │ ├── tsconfig.json │ └── yarn.lock ├── recording │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── Control.tsx │ │ ├── RecordingManager.ts │ │ ├── RecordingRow.tsx │ │ ├── index.ts │ │ ├── recorder.ts │ │ ├── recorders │ │ │ ├── audio-recording.tsx │ │ │ ├── marker-recording.tsx │ │ │ ├── replay-data-recorder.ts │ │ │ └── video-recording.tsx │ │ └── types.ts │ ├── styl │ │ └── style.styl │ ├── tsconfig.json │ └── video.svg ├── renderer │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.mts │ │ ├── tasks │ │ │ ├── convert.mts │ │ │ ├── join.mts │ │ │ ├── solidify.mts │ │ │ └── thumbs.mts │ │ ├── types.ts │ │ └── utils │ │ │ ├── binaries.mts │ │ │ ├── capture.mts │ │ │ ├── concurrency.mts │ │ │ ├── connect.mts │ │ │ ├── pool.mts │ │ │ └── stitch.mts │ └── tsconfig.json ├── server │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── yarn.lock ├── utils │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── animation.ts │ │ ├── interaction.ts │ │ ├── interactivity.ts │ │ ├── json.ts │ │ ├── misc.ts │ │ ├── react.ts │ │ ├── replay-data.ts │ │ ├── ssr.ts │ │ ├── svg.ts │ │ ├── time.ts │ │ └── types.ts │ ├── tests │ │ ├── animation.test.ts │ │ ├── json.test.ts │ │ ├── misc.test.ts │ │ ├── react.test.tsx │ │ └── time.test.ts │ └── tsconfig.json └── xyjax │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ └── index.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | packages/**/LICENSE 2 | 3 | # Node 4 | node_modules 5 | dist 6 | coverage 7 | tsconfig.tsbuildinfo 8 | *.log 9 | 10 | # Configuration 11 | .env 12 | 13 | # Editors 14 | *.code-* 15 | *.sublime-* 16 | 17 | # Generated 18 | bundle.js 19 | 20 | # Operating system 21 | .DS_Store 22 | 23 | # static files 24 | *.mp4 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Yuri Sulyma 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 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Liqvid 2 | 3 | [Liqvid](https://liqvidjs.org/) is a library for creating **interactive** videos in React. 4 | 5 | ## Links 6 | 7 | [Documentation](https://liqvidjs.org/docs/) 8 | 9 | [Discord](https://discord.gg/u8Qab99zHx) 10 | 11 | ## Repository structure 12 | 13 | This is a monorepo. Here is what the various packages do: 14 | 15 | ### Frontend Core 16 | 17 | * `main` 18 | Provides the main `liqvid` package. 19 | 20 | * `host` 21 | Script for pages hosting Liqvid videos; currently just handles [fake fullscreen](https://liqvidjs.org/docs/guide/mobile#fake-fullscreen) 22 | 23 | * `keymap` 24 | Provides the [`Keymap`](https://liqvidjs.org/docs/reference/Keymap) class 25 | 26 | * `playback` 27 | Provides the [`Playback`](https://liqvidjs.org/docs/reference/Playback) class 28 | 29 | * `polyfills` 30 | Polyfills for Liqvid videos; currently just handles [Web Animations](https://liqvidjs.org/docs/guide/mobile/#web-animations) 31 | 32 | * `utils` 33 | Provides the various helper functions in [`Utils`](https://liqvidjs.org/docs/reference/Utils/animation) 34 | 35 | ### Backend Tools 36 | 37 | * `cli` 38 | The Liqvid [CLI tool](https://liqvidjs.org/docs/cli/tool) 39 | 40 | * `magic` 41 | Provides wacky[resource macro](https://liqvidjs.org/docs/cli/macros) syntax 42 | 43 | * `renderer` 44 | Handles the [`audio`](https://liqvidjs.org/docs/cli/audio), [`build`](https://liqvidjs.org/docs/cli/build), [`render`](https://liqvidjs.org/docs/cli/render), and [`thumbs`](https://liqvidjs.org/docs/cli/thumbs) CLI commands 45 | 46 | * `serve` 47 | Development server; provides the [`serve`](https://liqvidjs.org/docs/cli/tool) CLI command 48 | 49 | ### Integrations 50 | 51 | * `katex` 52 | Provides [KaTeX integration](https://liqvidjs.org/docs/integrations/katex) 53 | 54 | * `react-three` 55 | Provides [React Three Fiber](https://liqvidjs.org/docs/integrations/three) integration 56 | 57 | ### In-development 58 | 59 | * `captioning` 60 | Captions editor 61 | 62 | * `gsap` 63 | [GSAP](https://greensock.com/gsap/) integration (maybe already works???) 64 | 65 | * `i18n` 66 | Internationalization utilities 67 | 68 | * `player` 69 | New Web Components-based `` 70 | 71 | * `mathjax` 72 | [MathJax](https://www.mathjax.org/) integration 73 | 74 | * `react` 75 | Probably for when Liqvid goes to Web Components (v3) 76 | 77 | * `xyjax` 78 | [XyJax](https://github.com/sonoisa/XyJax-v3/) integration 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": ["packages/*"], 5 | "devDependencies": { 6 | "@babel/core": "^7.17.10", 7 | "@babel/plugin-transform-modules-umd": "^7.16.7", 8 | "@babel/preset-env": "^7.17.10", 9 | "@biomejs/biome": "1.9.4", 10 | "@playwright/experimental-ct-react": "^1.27.1", 11 | "@playwright/test": "^1.27.1", 12 | "@rollup/plugin-babel": "^5.3.1", 13 | "@rollup/plugin-commonjs": "^22.0.0", 14 | "@rollup/plugin-node-resolve": "^13.3.0", 15 | "@testing-library/dom": "^8.13.0", 16 | "@testing-library/react": "^13.2.0", 17 | "@testing-library/user-event": "^14.2.0", 18 | "@types/jest": "^27.5.1", 19 | "@types/node": "^22.10.10", 20 | "@types/react": "^18.0.9", 21 | "@types/react-dom": "^18.0.4", 22 | "concurrently": "^7.5.0", 23 | "dotenv": "^16.0.3", 24 | "jest": "^28.1.0", 25 | "jest-environment-jsdom": "^28.1.0", 26 | "playwright": "^1.27.1", 27 | "react": "^18.1.0", 28 | "react-dom": "^18.1.0", 29 | "rollup": "^2.73.0", 30 | "rollup-plugin-dts": "^4.2.1", 31 | "rollup-plugin-terser": "^7.0.2", 32 | "serve": "^14.1.1", 33 | "ts-jest": "^28.0.2", 34 | "ts-node": "^10.7.0", 35 | "typescript": "^5.7.3" 36 | }, 37 | "overrides": { 38 | "@types/node": "^22.10.10", 39 | "yargs": "^17.7.2", 40 | "yargs-parser": "^21.1.1" 41 | }, 42 | "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0" 43 | } 44 | -------------------------------------------------------------------------------- /packages/captioning/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Yuri Sulyma 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 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /packages/captioning/README.md: -------------------------------------------------------------------------------- 1 | # @liqvid/captioning 2 | 3 | This package provides audio transcription and captioning utilities for [Liqvid](https://liqvidjs.org). It is used internally by [@liqvid/cli](../cli). 4 | -------------------------------------------------------------------------------- /packages/captioning/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@liqvid/captioning", 3 | "version": "1.0.0", 4 | "description": "Audio transcription and captioning for Liqvid", 5 | "files": ["dist/*"], 6 | "main": "dist/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/liqvidjs/liqvid.git" 10 | }, 11 | "author": "Yuri Sulyma ", 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/liqvidjs/liqvid/issues" 15 | }, 16 | "homepage": "https://github.com/liqvidjs/liqvid/tree/main/packages/renderer#readme", 17 | "devDependencies": { 18 | "ibm-watson": "^6.2.1" 19 | }, 20 | "peerDependencies": { 21 | "ibm-watson": "^6.2.1" 22 | }, 23 | "peerDependenciesMeta": { 24 | "ibm-watson": { 25 | "optional": true 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/captioning/src/index.ts: -------------------------------------------------------------------------------- 1 | export {transcribe} from "./transcription"; 2 | export {toWebVTT} from "./webvtt"; 3 | -------------------------------------------------------------------------------- /packages/captioning/src/transcription.ts: -------------------------------------------------------------------------------- 1 | import fs, {promises as fsp} from "fs"; 2 | import {IamAuthenticator} from "ibm-watson/auth"; 3 | import SpeechToTextV1 from "ibm-watson/speech-to-text/v1"; 4 | import path from "path"; 5 | import {toWebVTT} from "./webvtt"; 6 | 7 | /** 8 | * Transcript with per-word timings 9 | */ 10 | export type Transcript = [string, number, number][][]; 11 | 12 | /** 13 | * Transcribe audio file 14 | */ 15 | export async function transcribe(args: { 16 | /** Path to audio file */ 17 | input: string; 18 | 19 | /** Path for WebVTT captions */ 20 | captions: string; 21 | 22 | /** Params to pass to IBM Watson. */ 23 | params: Partial[0]>; 24 | 25 | /** Path for rich transcript */ 26 | transcript: string; 27 | 28 | /** IBM Cloud API key */ 29 | apiKey: string; 30 | 31 | /** IBM Watson endpoint URL */ 32 | apiUrl: string; 33 | }) { 34 | const filename = path.resolve(process.cwd(), args.input); 35 | const output = path.resolve(process.cwd(), args.transcript); 36 | 37 | const extn = path.extname(filename); 38 | 39 | // SpeechToText instance 40 | const speechToText = new SpeechToTextV1({ 41 | authenticator: new IamAuthenticator({ 42 | apikey: args.apiKey, 43 | }), 44 | serviceUrl: args.apiUrl, 45 | }); 46 | 47 | const params = Object.assign( 48 | { 49 | audio: fs.createReadStream(filename), 50 | contentType: `audio/${extn.slice(1)}`, 51 | 52 | objectMode: true, 53 | model: "en-US_BroadbandModel", 54 | profanityFilter: false, 55 | smartFormatting: true, 56 | timestamps: true, 57 | }, 58 | args.params, 59 | ); 60 | 61 | // transcribe 62 | const {result: json} = await speechToText.recognize(params); 63 | await fsp.writeFile(args.transcript, JSON.stringify(json, null, 2)); 64 | 65 | // format 66 | const blockSize = 8; 67 | const words = json.results 68 | .map((_) => _.alternatives[0].timestamps) 69 | .reduce((a, b) => a.concat(b), [] as [string, number, number][]) 70 | .map( 71 | ([word, t1, t2]: [string, number, number]) => 72 | [word, Math.floor(t1 * 1000), Math.floor(t2 * 1000)] as [ 73 | string, 74 | number, 75 | number, 76 | ], 77 | ); 78 | 79 | const blocks: Transcript = []; 80 | 81 | for (let i = 0; i < words.length; i += blockSize) { 82 | blocks.push(words.slice(i, i + blockSize)); 83 | } 84 | 85 | // save new version 86 | let str = JSON.stringify(blocks, null, 2); 87 | str = str.replace(/(? " + 18 | formatTimeMs(line[line.length - 1][2]), 19 | ); 20 | captions.push(line.map((_) => _[0]).join(" ")); 21 | captions.push(""); 22 | } 23 | 24 | return captions.join("\n"); 25 | } 26 | 27 | /* WebVTT requires mm:ss whereas @liqvid/utils/time produces [m]m:ss */ 28 | function formatTime(time: number): string { 29 | if (time < 0) { 30 | return "-" + formatTime(-time); 31 | } 32 | const minutes = Math.floor(time / 60 / 1000), 33 | seconds = Math.floor((time / 1000) % 60); 34 | 35 | return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; 36 | } 37 | 38 | function formatTimeMs(time: number): string { 39 | if (time < 0) { 40 | return "-" + formatTimeMs(-time); 41 | } 42 | const milliseconds = Math.floor(time % 1000); 43 | 44 | return `${formatTime(time)}.${milliseconds.toString().padStart(3, "0")}`; 45 | } 46 | -------------------------------------------------------------------------------- /packages/captioning/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "./dist", 6 | "rootDir": "./src" 7 | }, 8 | "include": ["./src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/cli/liqvid-cli.mjs: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import * as pkg from "./dist/index.mjs"; 3 | 4 | pkg 5 | .main() 6 | .then(() => process.exit(0)) 7 | .catch((err) => { 8 | // eslint-disable-next-line no-console 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@liqvid/cli", 3 | "version": "1.0.5", 4 | "description": "Liqvid command line utility", 5 | "main": "dist/index.js", 6 | "bin": { 7 | "liqvid": "liqvid-cli.mjs" 8 | }, 9 | "files": ["dist/*", "liqvid-cli.mjs"], 10 | "sideEffects": false, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/liqvidjs/liqvid.git" 14 | }, 15 | "author": "Yuri Sulyma ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/liqvidjs/liqvid/issues" 19 | }, 20 | "homepage": "https://github.com/liqvidjs/liqvid#readme", 21 | "scripts": { 22 | "lint": "eslint --ext mts,ts --fix src/" 23 | }, 24 | "devDependencies": { 25 | "@types/cli-progress": "^3.9.2", 26 | "@types/yargs": "^17.0.10" 27 | }, 28 | "dependencies": { 29 | "@liqvid/captioning": "workspace:^", 30 | "@liqvid/magic": "workspace:^", 31 | "@liqvid/renderer": "workspace:^", 32 | "@liqvid/server": "workspace:^", 33 | "@liqvid/utils": "workspace:^", 34 | "@types/node": "^17.0.23", 35 | "cli-progress": "^3.10.0", 36 | "execa": "^6.1.0", 37 | "ts-node": "^10.7.0", 38 | "webpack": "^5.70.0", 39 | "yargs": "^17.4.1", 40 | "yargs-parser": "^21.0.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/cli/src/index.mts: -------------------------------------------------------------------------------- 1 | import {readFile} from "fs/promises"; 2 | import * as path from "path"; 3 | import {fileURLToPath} from "url"; 4 | import yargs from "yargs"; 5 | import {hideBin} from "yargs/helpers"; 6 | 7 | // shared options 8 | 9 | import {audio} from "./tasks/audio.mjs"; 10 | import {build} from "./tasks/build.mjs"; 11 | import {serve} from "./tasks/serve.mjs"; 12 | import {render} from "./tasks/render.mjs"; 13 | import {thumbs} from "./tasks/thumbs.mjs"; 14 | 15 | // entry 16 | export async function main() { 17 | let config = // WTF 18 | yargs(hideBin(process.argv)) 19 | .scriptName("liqvid") 20 | .strict() 21 | .usage("$0 [args]") 22 | .demandCommand(1, "Must specify a command"); 23 | 24 | config = audio(config); 25 | config = build(config); 26 | config = serve(config); 27 | config = render(config); 28 | config = thumbs(config); 29 | 30 | // version 31 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 32 | const {version} = JSON.parse( 33 | await readFile(path.join(__dirname, "..", "package.json"), "utf8"), 34 | ); 35 | config.version(version); 36 | 37 | return config.help().argv; 38 | } 39 | 40 | import type {createServer} from "@liqvid/server"; 41 | import type {solidify, thumbs as captureThumbs} from "@liqvid/renderer"; 42 | import type {buildProject} from "./tasks/build.mjs"; 43 | import type {transcribe} from "@liqvid/captioning"; 44 | 45 | /** 46 | * Configuration object 47 | */ 48 | export interface LiqvidConfig { 49 | audio?: { 50 | transcribe: Partial[0]>; 51 | }; 52 | build?: Partial[0]>; 53 | render?: Partial[0]>; 54 | serve?: Partial[0]>; 55 | thumbs?: Partial[0]>; 56 | } 57 | -------------------------------------------------------------------------------- /packages/cli/src/tasks/config.mts: -------------------------------------------------------------------------------- 1 | import "ts-node/register/transpile-only"; 2 | import os from "os"; 3 | import path from "path"; 4 | // @ts-expect-error TypeScript complains about this not being a module 5 | import loadSync from "./load-sync.cjs"; 6 | 7 | export const DEFAULT_LIST = [ 8 | "liqvid.config.ts", 9 | "liqvid.config.js", 10 | "liqvid.config.json", 11 | ]; 12 | export const DEFAULT_CONFIG = DEFAULT_LIST[0]; 13 | 14 | export function parseConfig(...keys: string[]) { 15 | return (configPath: string) => { 16 | try { 17 | return access(loadSync(configPath), keys); 18 | } catch (e) { 19 | if (e.code === "MODULE_NOT_FOUND") { 20 | // default value => assume not specified 21 | if (path.join(process.cwd(), DEFAULT_CONFIG) === configPath) { 22 | return {}; 23 | } 24 | throw e; 25 | } else { 26 | throw e; 27 | } 28 | } 29 | }; 30 | } 31 | 32 | // function require(filename: string) { 33 | // return JSON.parse(readFileSync(path.resolve(process.cwd(), filename), "utf8")); 34 | // } 35 | 36 | function access(o: any, keys: string[]): any { 37 | if (keys.length === 0) return o; 38 | const key = keys.shift(); 39 | if (!o[key]) return {}; 40 | return access(o[key], keys); 41 | } 42 | 43 | export const BROWSER_EXECUTABLE = { 44 | alias: "x", 45 | desc: "Path to a Chrome/ium executable. If not specified and a suitable executable cannot be found, one will be downloaded during rendering.", 46 | normalize: true, 47 | type: "string", 48 | } as const; 49 | 50 | export const CONCURRENCY = { 51 | alias: "n", 52 | default: Math.floor(os.cpus().length / 2), 53 | desc: "How many threads to use", 54 | type: "number", 55 | } as const; 56 | -------------------------------------------------------------------------------- /packages/cli/src/tasks/index.mts: -------------------------------------------------------------------------------- 1 | import {audio} from "./audio.mjs"; 2 | import {build} from "./build.mjs"; 3 | import {serve} from "./serve.mjs"; 4 | import {render} from "./render.mjs"; 5 | import {thumbs} from "./thumbs.mjs"; 6 | 7 | export const commands = [audio, build, render, serve, thumbs]; 8 | -------------------------------------------------------------------------------- /packages/cli/src/tasks/load-sync.cts: -------------------------------------------------------------------------------- 1 | module.exports = function loadSync(path: string) { 2 | return require(path); 3 | }; 4 | -------------------------------------------------------------------------------- /packages/cli/src/tasks/serve.mts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import type Yargs from "yargs"; 3 | import {DEFAULT_CONFIG, parseConfig} from "./config.mjs"; 4 | 5 | /** 6 | * Run preview server 7 | */ 8 | export const serve = (yargs: typeof Yargs) => 9 | yargs.command( 10 | "serve", 11 | "Run preview server", 12 | (yargs) => 13 | yargs 14 | .config("config", parseConfig("serve")) 15 | .default("config", DEFAULT_CONFIG) 16 | .option("build", { 17 | alias: "b", 18 | coerce: path.resolve, 19 | desc: "Build directory", 20 | default: "./dist", 21 | }) 22 | .option("livereload-port", { 23 | alias: "L", 24 | desc: "Port to run LiveReload on", 25 | default: 0, 26 | }) 27 | .option("port", { 28 | alias: "p", 29 | desc: "Port to run on", 30 | default: 3000, 31 | }) 32 | .option("static", { 33 | alias: "s", 34 | coerce: path.resolve, 35 | desc: "Static directory", 36 | default: "./static", 37 | }) 38 | .option("scripts", { 39 | desc: "Script aliases", 40 | default: {}, 41 | }) 42 | .option("styles", { 43 | desc: "Style aliases", 44 | default: {}, 45 | }), 46 | async (argv) => { 47 | const {createServer} = await import("@liqvid/server"); 48 | // await so script doesn't close 49 | await new Promise(() => { 50 | createServer(argv); 51 | }); 52 | }, 53 | ); 54 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "esModuleInterop": true, 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "module": "esnext", 9 | "target": "esnext" 10 | }, 11 | "include": ["./src"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/diff/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.0 (April 15, 2024) 2 | 3 | Add generics 4 | 5 | ## 1.0.0 (April 14, 2024) 6 | 7 | Initial release 8 | -------------------------------------------------------------------------------- /packages/diff/README.md: -------------------------------------------------------------------------------- 1 | # @liqvid/diff 2 | 3 | This package provides functions to diff Javascript objects and arrays. It is used internally by recording plugins, and as such aims to produce very compact output. 4 | -------------------------------------------------------------------------------- /packages/diff/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testPathIgnorePatterns: ["dist"], 4 | transform: {}, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/diff/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@liqvid/diff", 3 | "version": "1.1.0", 4 | "description": "Object-diffing utility", 5 | "exports": { 6 | ".": { 7 | "import": "./dist/esm/index.mjs", 8 | "require": "./dist/cjs/index.cjs", 9 | "types": "./dist/types/index.d.ts" 10 | } 11 | }, 12 | "typesVersions": { 13 | "*": { 14 | "*": ["./dist/types/*.d.ts"] 15 | } 16 | }, 17 | "files": ["dist/*"], 18 | "scripts": { 19 | "build": "pnpm build:clean; pnpm build:js; pnpm build:postclean", 20 | "build:clean": "rm -rf dist", 21 | "build:js": "tsc --outDir dist/esm --module esnext; tsc --outDir dist/cjs --module commonjs; node ../../build.mjs", 22 | "build:postclean": "rm dist/tsconfig.tsbuildinfo", 23 | "lint": "eslint --ext ts,tsx --fix src && eslint --ext ts,tsx --fix tests", 24 | "test": "eslint src --ext ts,tsx && jest --coverage" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/liqvidjs/liqvid.git" 29 | }, 30 | "author": "Yuri Sulyma ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/liqvidjs/liqvid/issues" 34 | }, 35 | "homepage": "https://github.com/liqvidjs/liqvid/tree/main/packages/diff#readme", 36 | "dependencies": { 37 | "@liqvid/utils": "workspace:^" 38 | }, 39 | "sideEffects": false 40 | } 41 | -------------------------------------------------------------------------------- /packages/diff/src/apply.ts: -------------------------------------------------------------------------------- 1 | import type {ArrayDiff, ObjectDiff} from "./types"; 2 | import {matchItemDiff, matchRunes, objectKeys} from "./utils"; 3 | 4 | /** 5 | * Apply a diff to an object. 6 | * @param a - The object to apply the diff to. 7 | * @param b - The diff to apply. 8 | * @returns A new object with the diff applied. 9 | */ 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export function applyDiff(a: T, b: ObjectDiff): T { 12 | const copy = structuredClone(a); 13 | 14 | for (const rkey of objectKeys(b)) { 15 | matchRunes(b, rkey, { 16 | create(key, item) { 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | copy[key] = item as any; 19 | }, 20 | delete(key) { 21 | delete copy[key]; 22 | }, 23 | array(key, item) { 24 | const target = copy[key]; 25 | 26 | if (!Array.isArray(target)) { 27 | throw new TypeError("Expected array"); 28 | } 29 | 30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 31 | copy[key] = applyArrayDiff(target, item) as any; 32 | }, 33 | object(key, item) { 34 | const target = copy[key]; 35 | 36 | if (typeof target !== "object" || target === null) { 37 | throw new TypeError("Expected object"); 38 | } 39 | 40 | copy[key] = applyDiff(target, item); 41 | }, 42 | change(key, item) { 43 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 44 | copy[key] = item as any; 45 | }, 46 | }); 47 | } 48 | 49 | return copy; 50 | } 51 | 52 | /** 53 | * Apply a diff to an array. 54 | * @param arr - The array to apply the diff to. 55 | * @param diff - The diff to apply. 56 | * @returns A new array with the diff applied. 57 | */ 58 | export function applyArrayDiff(arr: T[], diff: ArrayDiff): T[] { 59 | const [delta, itemDiffs = [], ...appends] = diff; 60 | const copy = arr.slice(); 61 | 62 | for (const diff of itemDiffs) { 63 | matchItemDiff(diff, { 64 | set(offset, item) { 65 | copy[copy.length - offset] = item as T; 66 | }, 67 | array(offset, item) { 68 | copy[copy.length - offset] = applyArrayDiff( 69 | copy[copy.length - offset] as unknown[], 70 | item, 71 | ) as T; 72 | }, 73 | object(offset, item) { 74 | copy[copy.length - offset] = applyDiff( 75 | copy[copy.length - offset], 76 | item, 77 | ) as T; 78 | }, 79 | }); 80 | } 81 | 82 | if (delta < 0) { 83 | copy.splice(copy.length + delta, -delta); 84 | } else { 85 | for (const append of appends) { 86 | copy.push(append as T); 87 | } 88 | } 89 | 90 | return copy; 91 | } 92 | -------------------------------------------------------------------------------- /packages/diff/src/builders.ts: -------------------------------------------------------------------------------- 1 | import {deletePlaceholder, runes} from "./runes"; 2 | import type { 3 | ArrayDiff, 4 | ArrayItemDiff, 5 | ChangeItemDiff, 6 | DeletePlaceholder, 7 | ObjectDiff, 8 | RunedKey, 9 | } from "./types"; 10 | 11 | /** 12 | * Make a diff to create a value. 13 | * @param key Key to use. 14 | * @param value Value to create. 15 | */ 16 | export function creationDiff(key: string, value: V) { 17 | return {[`${runes.create}${key}`]: value} as Record, V>; 18 | } 19 | 20 | /** 21 | * Make a diff to delete a value. 22 | * @param key Key to use. 23 | * @returns Diff to delete the value. 24 | */ 25 | export function deletionDiff(key: K) { 26 | return {[`${runes.delete}${key}`]: deletePlaceholder} as Record< 27 | RunedKey<"delete", K>, 28 | DeletePlaceholder 29 | >; 30 | } 31 | 32 | /** 33 | * Make a diff to update an array. 34 | * @param key Key to use. 35 | * @param diff Array diff to apply. 36 | */ 37 | export function arrayDiff>( 38 | key: K, 39 | diff: D, 40 | ) { 41 | return {[`${runes.array}${key}`]: diff} as Record, D>; 42 | } 43 | 44 | /** 45 | * Make a diff to update an object. 46 | * @param key Key to use. 47 | * @param diff Object diff to apply. 48 | */ 49 | export function objectDiff>( 50 | key: K, 51 | diff: D, 52 | ) { 53 | return {[`${runes.object}${key}`]: diff} as Record, D>; 54 | } 55 | 56 | /** 57 | * Make a diff to set a value. 58 | * @param key Key to use. 59 | * @value Value to set. 60 | */ 61 | export function changeDiff(key: string, value: V) { 62 | return {[`${runes.change}${key}`]: value} as Record, V>; 63 | } 64 | 65 | // item diffs 66 | 67 | /** 68 | * Make an item diff to change a value. 69 | * @param offset Offset from the end to change. 70 | * @param value Value to change to. 71 | */ 72 | export function changeItemDiff(offset: number, value: T): ChangeItemDiff { 73 | return [offset, value]; 74 | } 75 | 76 | /** 77 | * Make an item diff to change an array item. 78 | * @param offset Offset from the end to change. 79 | * @param diff Array diff to apply. 80 | */ 81 | export function arrayItemDiff( 82 | offset: number, 83 | diff: ArrayDiff, 84 | ): ArrayItemDiff { 85 | return [`${runes.array}${offset}`, diff]; 86 | } 87 | 88 | /** 89 | * Make an item diff to change an object item. 90 | * @param index Index to change. 91 | * @param diff Object diff to apply. 92 | */ 93 | export function objectItemDiff>( 94 | offset: number, 95 | diff: D, 96 | ) { 97 | return [`${runes.object}${offset}`, diff] as [RunedKey<"object">, D]; 98 | } 99 | -------------------------------------------------------------------------------- /packages/diff/src/index.ts: -------------------------------------------------------------------------------- 1 | export {applyArrayDiff, applyDiff} from "./apply"; 2 | export { 3 | arrayDiff, 4 | arrayItemDiff, 5 | changeDiff, 6 | changeItemDiff, 7 | creationDiff, 8 | deletionDiff, 9 | objectDiff, 10 | objectItemDiff, 11 | } from "./builders"; 12 | export {diffArrays, diffObjects as diffObjects} from "./compute"; 13 | export {mergeArrayDiffs, mergeDiffs} from "./merge"; 14 | export type { 15 | ArrayDiff, 16 | ArrayItemDiff, 17 | ChangeItemDiff, 18 | DeletePlaceholder, 19 | ItemDiff, 20 | ObjectDiff, 21 | ObjectItemDiff, 22 | Rune, 23 | RuneName, 24 | RunedKey, 25 | } from "./types"; 26 | export {cmp, invertDiff, matchItemDiff, matchRunes} from "./utils"; 27 | -------------------------------------------------------------------------------- /packages/diff/src/runes.ts: -------------------------------------------------------------------------------- 1 | export const runes = { 2 | array: "#", 3 | change: "=", 4 | create: "+", 5 | delete: "-", 6 | object: "@", 7 | } as const; 8 | 9 | export const deletePlaceholder = 0; 10 | -------------------------------------------------------------------------------- /packages/diff/src/types.ts: -------------------------------------------------------------------------------- 1 | import type {deletePlaceholder, runes} from "./runes"; 2 | 3 | // runes 4 | export type RuneName = keyof typeof runes; 5 | export type Rune = (typeof runes)[RuneName]; 6 | export type RunedKey< 7 | K extends RuneName, 8 | Name extends string = string, 9 | > = `${(typeof runes)[K]}${Name}`; 10 | 11 | // array diffs 12 | export type ChangeItemDiff = [offset: number, value: T]; 13 | export type ObjectItemDiff = [ 14 | offset: RunedKey<"object">, 15 | diff: ObjectDiff, 16 | ]; 17 | export type ArrayItemDiff = [offset: RunedKey<"array">, diff: ArrayDiff]; 18 | 19 | /** 20 | * Note that offsets are relative to the **end** of the array. 21 | */ 22 | export type ItemDiff = 23 | | ArrayItemDiff 24 | | ChangeItemDiff 25 | | ObjectItemDiff; 26 | 27 | /** 28 | * A record describing how to make changes to an array. 29 | */ 30 | export type ArrayDiff = [ 31 | delta: number, 32 | itemDiffs?: ItemDiff[], 33 | ...tail: unknown[], 34 | ]; 35 | 36 | // delete placeholder 37 | export type DeletePlaceholder = typeof deletePlaceholder; 38 | 39 | /** 40 | * A record describing how to make changes to an object. 41 | */ 42 | export type ObjectDiff = { 43 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 44 | [key: RunedKey<"array">]: ArrayDiff; 45 | [key: RunedKey<"change">]: unknown; 46 | [key: RunedKey<"create">]: unknown; 47 | [key: RunedKey<"delete">]: DeletePlaceholder; 48 | [key: RunedKey<"object">]: ObjectDiff; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/diff/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declarationDir": "./dist/types", 6 | "outDir": "./dist", 7 | "rootDir": "./src" 8 | }, 9 | "include": ["./src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/gsap/README.md: -------------------------------------------------------------------------------- 1 | # @liqvid/gsap 2 | 3 | This module provides [GSAP](https://greensock.com/gsap/) integration for Liqvid. 4 | 5 | ## Installation 6 | 7 | $ npm install @liqvid/gsap 8 | 9 | ## Usage 10 | 11 | See the [GSAP docs](https://greensock.com/docs/) and especially the [React section](https://greensock.com/react). 12 | 13 | ```tsx 14 | import {useTimeline} from "@liqvid/gsap"; 15 | import {useEffect} from "react"; 16 | 17 | export function Demo() { 18 | const tl = useTimeline(); 19 | 20 | useEffect(() => { 21 | tl.to(".box", {duration: 3, x: 800}); 22 | tl.to(".box", {duration: 3, rotation: 360, y: 500}); 23 | tl.to(".box", {duration: 3, x: 0}); 24 | }, []); 25 | 26 | return ( 27 |
28 |
29 |
30 |
31 |
32 | ); 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /packages/gsap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@liqvid/gsap", 3 | "version": "1.0.1", 4 | "description": "GSAP bindings for Liqvid", 5 | "keywords": ["animation", "GSAP", "Liqvid"], 6 | "main": "./dist/index.js", 7 | "typings": "./dist/index.d.ts", 8 | "files": ["dist/*"], 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/liqvidjs/liqvid.git" 15 | }, 16 | "author": "Yuri Sulyma ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/liqvidjs/liqvid/issues" 20 | }, 21 | "homepage": "https://github.com/liqvidjs/liqvid#readme", 22 | "peerDependencies": { 23 | "gsap": "^3.9.0", 24 | "liqvid": "workspace:^" 25 | }, 26 | "sideEffects": false, 27 | "devDependencies": { 28 | "gsap": "^3.9.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/gsap/src/index.ts: -------------------------------------------------------------------------------- 1 | import gsap from "gsap"; 2 | import {Playback, usePlayer} from "liqvid"; 3 | 4 | const sym = Symbol(); 5 | 6 | declare module "liqvid" { 7 | interface Playback { 8 | [sym]: gsap.core.Timeline; 9 | } 10 | } 11 | 12 | /** 13 | * Get a GSAP timeline synced with Liqvid playback. 14 | */ 15 | export function useTimeline() { 16 | const {playback} = usePlayer(); 17 | if (!playback[sym]) { 18 | playback[sym] = syncTimeline(playback); 19 | } 20 | return playback[sym] as gsap.core.Timeline; 21 | } 22 | 23 | /** 24 | * Create a GSAP timeline and sync it with Liqvid playback. 25 | */ 26 | function syncTimeline(playback: Playback) { 27 | const tl = gsap.timeline({paused: true}); 28 | 29 | playback.hub.on("play", () => tl.resume()); 30 | playback.hub.on("pause", () => tl.pause()); 31 | playback.hub.on("ratechange", () => tl.timeScale(playback.playbackRate)); 32 | playback.hub.on("seek", () => tl.seek(playback.currentTime / 1000)); 33 | 34 | return tl; 35 | } 36 | -------------------------------------------------------------------------------- /packages/gsap/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "module": "commonjs" 8 | }, 9 | "include": ["./src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/gsap/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@liqvid/playback@^0.0.1": 6 | version "0.0.1" 7 | resolved "https://registry.yarnpkg.com/@liqvid/playback/-/playback-0.0.1.tgz#3cc1a1258330b962a38a2f4297947c153b33bb8a" 8 | integrity sha512-XqmY90VeRmAnsZfPpNWF7jSFUnWeRG9kUy55GLShuoL+87l93n1IClRK2JhPFbZqaf4+PVflRgcsFw1Qrz6rGg== 9 | dependencies: 10 | "@liqvid/utils" "*" 11 | "@types/events" "^3.0.0" 12 | events "^3.3.0" 13 | strict-event-emitter-types "^2.0.0" 14 | 15 | "@liqvid/utils@*": 16 | version "0.0.1" 17 | resolved "https://registry.yarnpkg.com/@liqvid/utils/-/utils-0.0.1.tgz#696cc14f3e630e6dfea0171cda24855667cdd9f8" 18 | integrity sha512-HsuUQ2cKV3rPt6cOqWehS+NjN1Yqsz4zvPe+UqcrWywKzE+oM0br1ocjnJvpmPgo5A317okhLlByv/J5m9MPIQ== 19 | dependencies: 20 | bezier-easing "^2.1.0" 21 | 22 | "@types/events@^3.0.0": 23 | version "3.0.0" 24 | resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" 25 | integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== 26 | 27 | bezier-easing@^2.1.0: 28 | version "2.1.0" 29 | resolved "https://registry.yarnpkg.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86" 30 | integrity sha1-wE3+i5JtbsrKGBPWn/F5t8ICXYY= 31 | 32 | events@^3.3.0: 33 | version "3.3.0" 34 | resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" 35 | integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== 36 | 37 | gsap@^3.9.0: 38 | version "3.9.0" 39 | resolved "https://registry.yarnpkg.com/gsap/-/gsap-3.9.0.tgz#c5cf85b686ccfe530bb9f309f1b42efde6cf9841" 40 | integrity sha512-YfIBNHJu4UHES1Vj780+sXtQuiD78QQwgJqktaXE9PO9OuXz5l4ETz05pnhxUfJcxJy4SUINXJxT9ZZhuYwU2g== 41 | 42 | liqvid@^2.0.10: 43 | version "2.0.10" 44 | resolved "https://registry.yarnpkg.com/liqvid/-/liqvid-2.0.10.tgz#680dbc0040b2a3fde6f7bdedc785df40ba8fdbcd" 45 | integrity sha512-B5pj/P6RYQdVFAqUplp6c0lW8IdqEeITylmSIj2h5wB9/ff5njhcSgoZ3RjpiNAqbxnOKdna4DqGQVPDtLG9eQ== 46 | 47 | strict-event-emitter-types@^2.0.0: 48 | version "2.0.0" 49 | resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f" 50 | integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA== 51 | -------------------------------------------------------------------------------- /packages/host/README.md: -------------------------------------------------------------------------------- 1 | # lv-host 2 | 3 | This package provides a script which should be included in pages hosting [Liqvid](https://liqvidjs.org) videos. Currently, all it does is shim fullscreen behavior in iOS. 4 | -------------------------------------------------------------------------------- /packages/host/lv-host.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | (() => { 4 | const setDims = () => { 5 | document.body.style.setProperty("--vh", `${window.innerHeight}px`); 6 | document.body.style.setProperty("--vw", `${window.innerWidth}px`); 7 | document.body.style.setProperty("--scroll-y", `${window.scrollY || 0}px`); 8 | }; 9 | 10 | document.addEventListener("DOMContentLoaded", () => { 11 | // add CSS 12 | { 13 | const style = document.createElement("style"); 14 | style.setAttribute("type", "text/css"); 15 | style.textContent = ` 16 | iframe.fake-fullscreen { 17 | position: fixed; 18 | top: 0;/*var(--scroll-y);*/ 19 | left: 0; 20 | height: var(--vh); 21 | width: var(--vw); 22 | z-index: 10000; 23 | } 24 | 25 | @media (orientation: portrait) { 26 | iframe.fake-fullscreen { 27 | transform: rotate(-90deg); 28 | transform-origin: top left; 29 | left: 0; 30 | top: 100%; 31 | width: var(--vh); 32 | height: var(--vw); 33 | } 34 | }`; 35 | document.head.appendChild(style); 36 | } 37 | 38 | // resize listener 39 | window.addEventListener("resize", setDims); 40 | setDims(); 41 | 42 | // live collection of iframes 43 | const iframes = document.getElementsByTagName("iframe"); 44 | 45 | const listener = (e) => { 46 | for (let i = 0; i < iframes.length; ++i) { 47 | const iframe = iframes.item(i); 48 | if ( 49 | iframe.allowFullscreen && 50 | !document.fullscreenEnabled && 51 | iframe.contentWindow === e.source 52 | ) { 53 | // handle the resize event 54 | if ("type" in e.data && e.data.type === "fake-fullscreen") { 55 | // resize event doesn't work reliably in iOS... 56 | setDims(); 57 | iframe.classList.toggle("fake-fullscreen", e.data.value); 58 | } 59 | return; 60 | } 61 | } 62 | }; 63 | 64 | // communicate with children 65 | window.addEventListener("message", listener); 66 | }); 67 | })(); 68 | -------------------------------------------------------------------------------- /packages/host/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@liqvid/host", 3 | "version": "1.1.0", 4 | "description": "Liqvid host page script", 5 | "files": ["lv-host.js"], 6 | "main": "lv-host.js", 7 | "keywords": ["Liqvid", "React", "Javascript"], 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/liqvidjs/liqvid.git" 11 | }, 12 | "author": "Yuri Sulyma ", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/liqvidjs/liqvid/issues" 16 | }, 17 | "homepage": "https://github.com/liqvidjs/liqvid#readme" 18 | } 19 | -------------------------------------------------------------------------------- /packages/katex/README.md: -------------------------------------------------------------------------------- 1 | # @liqvid/katex 2 | 3 | [KaTeX](https://katex.org/) integration for [Liqvid](https://liqvidjs.org). See https://liqvidjs.org/docs/integrations/katex/ for documentation. 4 | -------------------------------------------------------------------------------- /packages/katex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@liqvid/katex", 3 | "version": "0.1.0", 4 | "description": "KaTeX integration for Liqvid", 5 | "files": ["dist/*"], 6 | "exports": { 7 | ".": { 8 | "import": "./dist/esm/index.mjs", 9 | "require": "./dist/cjs/index.cjs" 10 | }, 11 | "./plain": { 12 | "import": "./dist/esm/plain.mjs", 13 | "require": "./dist/cjs/plain.cjs" 14 | } 15 | }, 16 | "typesVersions": { 17 | "*": { 18 | "*": ["./dist/types/*.d.ts"] 19 | } 20 | }, 21 | "author": "Yuri Sulyma ", 22 | "keywords": ["liqvid", "katex"], 23 | "scripts": { 24 | "build": "pnpm build:clean && pnpm build:js && pnpm build:postclean", 25 | "build:clean": "rm -rf dist", 26 | "build:js": "tsc --module esnext --outDir dist/esm; tsc --module commonjs --outDir dist/cjs; node ../../build.mjs", 27 | "build:postclean": "rm dist/tsconfig.tsbuildinfo", 28 | "lint": "eslint --ext ts,tsx --fix src" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/liqvidjs/liqvid.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/liqvidjs/liqvid/issues" 36 | }, 37 | "homepage": "https://github.com/liqvidjs/liqvid/tree/main/packages/katex", 38 | "license": "MIT", 39 | "peerDependencies": { 40 | "@types/katex": ">=0.14.0", 41 | "@types/react": ">=18.0.0", 42 | "liqvid": "workspace:^", 43 | "react": ">=18.1.0" 44 | }, 45 | "peerDependenciesMeta": { 46 | "liqvid": { 47 | "optional": true 48 | } 49 | }, 50 | "devDependencies": { 51 | "liqvid": "workspace:^" 52 | }, 53 | "dependencies": { 54 | "@liqvid/utils": "workspace:^" 55 | }, 56 | "sideEffects": false, 57 | "type": "module" 58 | } 59 | -------------------------------------------------------------------------------- /packages/katex/rollup.config.js: -------------------------------------------------------------------------------- 1 | import dts from "rollup-plugin-dts"; 2 | 3 | const external = ["@liqvid/utils/react", "react", "react/jsx-runtime.js"]; 4 | 5 | export default [ 6 | // index 7 | { 8 | external: [...external, "liqvid"], 9 | input: "dist/esm/index.mjs", 10 | 11 | output: [ 12 | // ESM 13 | {file: "./dist/index.mjs", format: "esm"}, 14 | // CJS 15 | {file: "./dist/index.cjs", format: "cjs"}, 16 | ], 17 | }, 18 | // plain 19 | { 20 | external, 21 | input: "dist/esm/plain.mjs", 22 | 23 | output: [ 24 | // ESM 25 | {file: "./dist/plain.mjs", format: "esm"}, 26 | // CJS 27 | {file: "./dist/plain.cjs", format: "cjs"}, 28 | ], 29 | }, 30 | // index types 31 | { 32 | input: "dist/types/index.d.ts", 33 | plugins: [dts()], 34 | output: { 35 | file: "dist/index.d.ts", 36 | format: "es", 37 | }, 38 | }, 39 | // plain types 40 | { 41 | input: "dist/types/plain.d.ts", 42 | plugins: [dts()], 43 | output: { 44 | file: "dist/plain.d.ts", 45 | format: "es", 46 | }, 47 | }, 48 | ]; 49 | -------------------------------------------------------------------------------- /packages/katex/src/fancy.tsx: -------------------------------------------------------------------------------- 1 | import {combineRefs} from "@liqvid/utils/react"; 2 | import {usePlayer} from "liqvid"; 3 | import {forwardRef, useEffect, useRef} from "react"; 4 | import {Handle, KTX as KTXPlain} from "./plain"; 5 | 6 | interface Props extends React.ComponentProps { 7 | /** 8 | * Player events to obstruct 9 | * @default "canplay canplaythrough" 10 | */ 11 | obstruct?: string; 12 | 13 | /** 14 | * Whether to reparse descendants for `during()` and `from()` 15 | * @default false 16 | */ 17 | reparse?: boolean; 18 | } 19 | 20 | /** Component for KaTeX code */ 21 | export const KTX = forwardRef(function KTX(props, ref) { 22 | const { 23 | obstruct = "canplay canplaythrough", 24 | reparse = false, 25 | ...attrs 26 | } = props; 27 | 28 | const plain = useRef(); 29 | const combined = combineRefs(plain, ref); 30 | 31 | const player = usePlayer(); 32 | 33 | useEffect(() => { 34 | // obstruction 35 | if (obstruct.match(/\bcanplay\b/)) { 36 | player.obstruct("canplay", plain.current.ready); 37 | } 38 | if (obstruct.match("canplaythrough")) { 39 | player.obstruct("canplaythrough", plain.current.ready); 40 | } 41 | 42 | // reparsing 43 | if (reparse) { 44 | plain.current.ready.then(() => 45 | player.reparseTree(plain.current.domElement), 46 | ); 47 | } 48 | }, []); 49 | 50 | return ; 51 | }); 52 | -------------------------------------------------------------------------------- /packages/katex/src/index.tsx: -------------------------------------------------------------------------------- 1 | export {KTX} from "./fancy"; 2 | export {KaTeXReady} from "./loading"; 3 | export {Handle} from "./plain"; 4 | export {RenderGroup} from "./RenderGroup"; 5 | 6 | declare global { 7 | const katex: typeof katex; 8 | } 9 | -------------------------------------------------------------------------------- /packages/katex/src/loading.ts: -------------------------------------------------------------------------------- 1 | // option of loading KaTeX asynchronously 2 | const KaTeXLoad = new Promise((resolve) => { 3 | const script = document.querySelector( 4 | 'script[src*="katex.js"], script[src*="katex.min.js"]', 5 | ); 6 | if (!script) return; 7 | 8 | if (window.hasOwnProperty("katex")) { 9 | resolve(katex); 10 | } else { 11 | script.addEventListener("load", () => resolve(katex)); 12 | } 13 | }); 14 | 15 | // load macros from 16 | const KaTeXMacros = new Promise<{[key: string]: string}>((resolve) => { 17 | const macros: {[key: string]: string} = {}; 18 | const scripts: HTMLScriptElement[] = Array.from( 19 | document.querySelectorAll("head > script[type='math/tex']"), 20 | ); 21 | return Promise.all( 22 | scripts.map((script) => 23 | fetch(script.src) 24 | .then((res) => { 25 | if (res.ok) return res.text(); 26 | throw new Error(`${res.status} ${res.statusText}: ${script.src}`); 27 | }) 28 | .then((tex) => { 29 | Object.assign(macros, parseMacros(tex)); 30 | }), 31 | ), 32 | ).then(() => resolve(macros)); 33 | }); 34 | 35 | /** 36 | * Ready Promise 37 | */ 38 | export const KaTeXReady = Promise.all([KaTeXLoad, KaTeXMacros]); 39 | 40 | /** 41 | * Parse \newcommand macros in a file. 42 | * Also supports \ktxnewcommand (for use in conjunction with MathJax). 43 | * @param file TeX file to parse 44 | */ 45 | function parseMacros(file: string) { 46 | const macros: Record = {}; 47 | const rgx = /\\(?:ktx)?newcommand\{(.+?)\}(?:\[\d+\])?\{/g; 48 | let match: RegExpExecArray; 49 | 50 | while ((match = rgx.exec(file))) { 51 | let body = ""; 52 | 53 | const macro = match[1]; 54 | let braceCount = 1; 55 | 56 | for ( 57 | let i = match.index + match[0].length; 58 | braceCount > 0 && i < file.length; 59 | ++i 60 | ) { 61 | const char = file[i]; 62 | if (char === "{") { 63 | braceCount++; 64 | } else if (char === "}") { 65 | braceCount--; 66 | if (braceCount === 0) break; 67 | } else if (char === "\\") { 68 | body += file.slice(i, i + 2); 69 | ++i; 70 | continue; 71 | } 72 | body += char; 73 | } 74 | macros[macro] = body; 75 | } 76 | return macros; 77 | } 78 | -------------------------------------------------------------------------------- /packages/katex/src/plain.tsx: -------------------------------------------------------------------------------- 1 | import {usePromise} from "@liqvid/utils/react"; 2 | import {forwardRef, useEffect, useImperativeHandle, useRef} from "react"; 3 | import {KaTeXReady} from "./loading"; 4 | 5 | /** 6 | * KTX element API 7 | */ 8 | export interface Handle { 9 | /** The underlying element */ 10 | domElement: HTMLSpanElement; 11 | 12 | /** Promise that resolves once typesetting is finished */ 13 | ready: Promise; 14 | } 15 | 16 | interface Props extends React.HTMLAttributes { 17 | /** 18 | * Whether to render in display style 19 | * @default false 20 | */ 21 | display?: boolean; 22 | } 23 | 24 | /** Component for KaTeX code */ 25 | export const KTX = forwardRef(function KTX(props, ref) { 26 | const spanRef = useRef(); 27 | const {children, display = false, ...attrs} = props; 28 | const [ready, resolve] = usePromise(); 29 | 30 | // handle 31 | useImperativeHandle(ref, () => ({ 32 | domElement: spanRef.current, 33 | ready, 34 | })); 35 | 36 | useEffect(() => { 37 | KaTeXReady.then(([katex, macros]) => { 38 | katex.render(children.toString(), spanRef.current, { 39 | displayMode: !!display, 40 | macros, 41 | strict: "ignore", 42 | throwOnError: false, 43 | trust: true, 44 | }); 45 | 46 | /* move katex into placeholder element */ 47 | const child = spanRef.current.firstElementChild as HTMLSpanElement; 48 | 49 | // copy classes 50 | for (let i = 0, len = child.classList.length; i < len; ++i) { 51 | spanRef.current.classList.add(child.classList.item(i)); 52 | } 53 | 54 | // move children 55 | while (child.childNodes.length > 0) { 56 | spanRef.current.appendChild(child.firstChild); 57 | } 58 | 59 | // delete child 60 | child.remove(); 61 | 62 | // resolve promise 63 | resolve(); 64 | }); 65 | }, [children]); 66 | 67 | // Google Chrome fails without this 68 | if (display) { 69 | if (!attrs.style) attrs.style = {}; 70 | attrs.style.display = "block"; 71 | } 72 | 73 | return ; 74 | }); 75 | -------------------------------------------------------------------------------- /packages/katex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declarationDir": "./dist/types", 6 | "module": "esnext", 7 | "outDir": "./dist/esm", 8 | "rootDir": "./src", 9 | "target": "esnext" 10 | }, 11 | "include": ["./src"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/keymap/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.1 (January 20, 2024) 2 | 3 | - include `"use client"` in `@liqvid/keymap/react` 4 | 5 | ## 1.2.0 (September 13, 2023) 6 | 7 | - add `useKeyboardShortcut()` 8 | 9 | ## 1.1.4 (November 13, 2022) 10 | 11 | - don't throw when unbinding callback that hasn't been bound 12 | -------------------------------------------------------------------------------- /packages/keymap/README.md: -------------------------------------------------------------------------------- 1 | # @liqvid/keymap 2 | 3 | This package provides key bindings for [Liqvid](https://liqvidjs.org). See https://liqvidjs.org/docs/reference/KeyMap for documentation. 4 | -------------------------------------------------------------------------------- /packages/keymap/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | testPathIgnorePatterns: ["dist"], 5 | coverageReporters: ["json-summary"], 6 | transform: {}, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/keymap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@liqvid/keymap", 3 | "version": "1.2.2", 4 | "description": "Key binding for Liqvid", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/liqvidjs/liqvid.git" 8 | }, 9 | "exports": { 10 | ".": { 11 | "import": "./dist/esm/index.mjs", 12 | "require": "./dist/cjs/index.cjs", 13 | "types": "./dist/types/index.d.ts" 14 | }, 15 | "./react": { 16 | "import": "./dist/esm/react.mjs", 17 | "require": "./dist/cjs/react.cjs", 18 | "types": "./dist/types/react.d.ts" 19 | } 20 | }, 21 | "typesVersions": { 22 | "*": { 23 | "*": ["./dist/types/*"] 24 | } 25 | }, 26 | "files": ["dist/*"], 27 | "scripts": { 28 | "build": "pnpm build:clean && pnpm build:js && pnpm build:postclean", 29 | "build:clean": "rm -rf dist", 30 | "build:js": "tsc --module esnext --outDir dist/esm; tsc --module commonjs --outDir dist/cjs; node ../../build.mjs", 31 | "build:postclean": "find ./dist -name tsconfig.tsbuildinfo -delete", 32 | "lint": "eslint --ext ts,tsx --fix src && eslint --ext ts,tsx --fix tests", 33 | "test": "jest" 34 | }, 35 | "author": "Yuri Sulyma ", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/liqvidjs/liqvid/issues" 39 | }, 40 | "homepage": "https://github.com/liqvidjs/liqvid/tree/main/packages/keymap#readme", 41 | "sideEffects": false, 42 | "peerDependencies": { 43 | "@types/react": ">=17.0.0", 44 | "react": ">=17.0.0" 45 | }, 46 | "peerDependenciesMeta": { 47 | "react": { 48 | "optional": true 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/keymap/src/react.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {createContext, useContext, useEffect} from "react"; 4 | import type {Keymap} from "."; 5 | 6 | const symbol = Symbol.for("@lqv/keymap"); 7 | 8 | type GlobalThis = { 9 | [symbol]: React.Context; 10 | }; 11 | 12 | if (!(symbol in globalThis)) { 13 | (globalThis as unknown as GlobalThis)[symbol] = createContext(null); 14 | } 15 | 16 | /** 17 | * {@link React.Context} used to access ambient Keymap 18 | */ 19 | export const KeymapContext = (globalThis as unknown as GlobalThis)[symbol]; 20 | KeymapContext.displayName = "Keymap"; 21 | 22 | /** Access the ambient {@link Keymap} */ 23 | export function useKeymap() { 24 | return useContext(KeymapContext); 25 | } 26 | 27 | /** Register a keyboard shortcut for the duration of the component. */ 28 | export function useKeyboardShortcut( 29 | /** Keyboard sequence to bind to */ 30 | seq: string, 31 | 32 | /** Callback to handle the shortcut */ 33 | callback: (e: KeyboardEvent) => unknown, 34 | ) { 35 | const keymap = useKeymap(); 36 | 37 | useEffect(() => { 38 | keymap.bind(seq, callback); 39 | 40 | return () => keymap.unbind(seq, callback); 41 | }, [callback, keymap, seq]); 42 | } 43 | -------------------------------------------------------------------------------- /packages/keymap/tests/keymap.test.ts: -------------------------------------------------------------------------------- 1 | import {Keymap} from "../src/index"; 2 | 3 | /* Modifier keys cannot be tested in Keymap::identify and Keymap.handle 4 | due to a bug in jsdom: https://github.com/jsdom/jsdom/issues/3126 5 | */ 6 | 7 | test("Keymap::identify", () => { 8 | const e = new KeyboardEvent("keyup", {key: "a", code: "KeyA"}); 9 | expect(Keymap.identify(e)).toBe("A"); 10 | }); 11 | 12 | test("Keymap::normalize", () => { 13 | expect(Keymap.normalize("A+Shift+Ctrl")).toBe("Ctrl+Shift+A"); 14 | expect(Keymap.normalize("q+alt+ctrl")).toBe("Ctrl+Alt+Q"); 15 | }); 16 | 17 | describe("Keymap bind handling", () => { 18 | const keymap = new Keymap(); 19 | 20 | const cb = jest.fn(); 21 | const cb2 = jest.fn(); 22 | 23 | keymap.bind("A", cb); 24 | keymap.bind("B", cb2); 25 | 26 | test("getHandlers", () => { 27 | expect(keymap.getHandlers("A")).toEqual([cb]); 28 | expect(keymap.getHandlers("B")).toEqual([cb2]); 29 | }); 30 | 31 | test("getKeys", () => { 32 | expect(keymap.getKeys()).toEqual(["A", "B"]); 33 | }); 34 | 35 | test("unbind", () => { 36 | expect(() => keymap.unbind("C", cb)).not.toThrow(); 37 | expect(() => keymap.unbind("B", cb)).not.toThrow(); 38 | keymap.unbind("A", cb); 39 | expect(keymap.getHandlers("A")).toEqual([]); 40 | }); 41 | 42 | test("handle", () => { 43 | const e = new KeyboardEvent("keyup", {key: "B", code: "KeyB"}); 44 | keymap.handle(e); 45 | expect(cb2).toHaveBeenCalledTimes(1); 46 | expect(cb2).toHaveBeenCalledWith(e); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/keymap/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declarationDir": "./dist/types", 6 | "outDir": "./dist", 7 | "rootDir": "./src" 8 | }, 9 | "include": ["./src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/magic/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Yuri Sulyma 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 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /packages/magic/README.md: -------------------------------------------------------------------------------- 1 | # @liqvid/magic 2 | 3 | This package provides template macros for [Liqvid](https://liqvidjs.org). See https://liqvidjs.org/docs/cli/macros/ for documentation. 4 | -------------------------------------------------------------------------------- /packages/magic/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | testPathIgnorePatterns: ["dist"], 5 | coverageReporters: ["json-summary"], 6 | transform: {}, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/magic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@liqvid/magic", 3 | "version": "1.1.2", 4 | "description": "Templating functions for Liqvid", 5 | "main": "./dist/index.js", 6 | "typings": "./dist/index.d.ts", 7 | "files": ["dist/*"], 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/liqvidjs/liqvid.git" 11 | }, 12 | "author": "Yuri Sulyma ", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/liqvidjs/liqvid/issues" 16 | }, 17 | "scripts": { 18 | "build": "tsc --build --force", 19 | "lint": "eslint --ext ts,tsx --fix src && eslint --ext ts,tsx --fix tests", 20 | "test": "jest" 21 | }, 22 | "homepage": "https://github.com/liqvidjs/liqvid/tree/main/packages/magic#readme", 23 | "sideEffects": false 24 | } 25 | -------------------------------------------------------------------------------- /packages/magic/src/default-assets.ts: -------------------------------------------------------------------------------- 1 | import type {ScriptData, StyleData} from "./types"; 2 | 3 | export const scripts: Record = { 4 | host: "https://unpkg.com/@liqvid/host/lv-host.js", 5 | liqvid: { 6 | crossorigin: true, 7 | development: "https://unpkg.com/liqvid@2.1.4/dist/liqvid.js", 8 | production: "https://unpkg.com/liqvid@2.1.4/dist/liqvid.min.js", 9 | integrity: 10 | "sha384-o8Svf9aNpbI8MzaCkJ0rPo5OxnnZ9Zf86Z18azwsy6rPuenc22zYvNwyv49wIgWa", 11 | }, 12 | livereload: {}, 13 | polyfills: "https://unpkg.com/@liqvid/polyfills/dist/waapi.js", 14 | rangetouch: { 15 | crossorigin: true, 16 | development: "https://cdn.rangetouch.com/2.0.1/rangetouch.js", 17 | integrity: 18 | "sha384-ImWMbbJ1rSn1mn+2vsKm/wN6Vc7hPNB2VKN0lX3FAzGK+c7M2mD6ZZcwknuKlP7K", 19 | production: "https://cdn.rangetouch.com/2.0.1/rangetouch.js", 20 | }, 21 | react: { 22 | crossorigin: true, 23 | development: "https://unpkg.com/react@17.0.2/umd/react.development.js", 24 | production: "https://unpkg.com/react@17.0.2/umd/react.production.min.js", 25 | integrity: 26 | "sha384-7Er69WnAl0+tY5MWEvnQzWHeDFjgHSnlQfDDeWUvv8qlRXtzaF/pNo18Q2aoZNiO", 27 | }, 28 | "react-dom": { 29 | crossorigin: true, 30 | development: 31 | "https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js", 32 | production: 33 | "https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js", 34 | integrity: 35 | "sha384-vj2XpC1SOa8PHrb0YlBqKN7CQzJYO72jz4CkDQ+ePL1pwOV4+dn05rPrbLGUuvCv", 36 | }, 37 | recording: { 38 | crossorigin: true, 39 | development: "https://unpkg.com/rp-recording@2.1.1/dist/rp-recording.js", 40 | }, 41 | }; 42 | 43 | export const styles: Record = { 44 | liqvid: { 45 | development: "https://unpkg.com/liqvid@2.1.4/dist/liqvid.css", 46 | production: "https://unpkg.com/liqvid@2.1.4/dist/liqvid.min.css", 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /packages/magic/src/types.ts: -------------------------------------------------------------------------------- 1 | export type ScriptData = 2 | | { 3 | /** 4 | * Whether script is crossorigin. 5 | * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-crossorigin 6 | */ 7 | crossorigin?: boolean | string; 8 | 9 | /** 10 | * Whether to apply the defer attribute 11 | * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer 12 | */ 13 | defer?: boolean; 14 | 15 | /** 16 | * Development src. 17 | */ 18 | development?: string | (() => string); 19 | 20 | /** 21 | * Integrity attribute for production. 22 | */ 23 | integrity?: string; 24 | 25 | /** 26 | * Production src. 27 | */ 28 | production?: string | (() => string); 29 | } 30 | | string; 31 | 32 | export type StyleData = 33 | | { 34 | /** 35 | * Development href. 36 | */ 37 | development?: string; 38 | 39 | /** 40 | * Production href. 41 | */ 42 | production?: string; 43 | } 44 | | string; 45 | -------------------------------------------------------------------------------- /packages/magic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "module": "none" 8 | }, 9 | "include": ["./src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/main/.env.example: -------------------------------------------------------------------------------- 1 | PLAYWRIGHT_EXECUTABLE_PATH=/snap/bin/chromium 2 | PLAYWRIGHT_HOST=http://localhost:41728 3 | PLAYWRIGHT_TEST_VIDEO=https://d2og9lpzrymesl.cloudfront.net/v/train_1920.mp4 4 | -------------------------------------------------------------------------------- /packages/main/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ## Testing 2 | 3 | In order for media codecs to work in the e2e tests, Playwright may need your system Chromium instead of its bundled one. To configure this, rename `.env.example` to `.env` and adjust `PLAYWRIGHT_EXECUTABLE_PATH` as necessary. 4 | -------------------------------------------------------------------------------- /packages/main/README.md: -------------------------------------------------------------------------------- 1 | # liqvid 2 | 3 | This is a library for making **interactive** videos in React. 4 | 5 | For example, here's an interactive coding demo inside a video: 6 | 7 | 8 | 9 | Here's an interactive graph: 10 | 11 | 12 | 13 | To get started, go to https://liqvidjs.org/docs/ 14 | 15 | For inspiration, see https://epiplexis.xyz/ 16 | -------------------------------------------------------------------------------- /packages/main/e2e/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "E2E tests for Liqvid", 4 | "main": "index.js", 5 | "scripts": { 6 | "build": "webpack", 7 | "dev": "concurrently \"pnpm watch\" \"pnpm serve\"", 8 | "serve": "serve -p 41728 -S -s static", 9 | "watch": "webpack --watch" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@liqvid/cli": "workspace:^", 16 | "@liqvid/recording": "workspace:^", 17 | "liqvid": "workspace:^" 18 | }, 19 | "devDependencies": { 20 | "ts-loader": "^9.3.1", 21 | "typescript": "^4.8.4", 22 | "webpack": "^5.74.0", 23 | "webpack-cli": "^4.10.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/main/e2e/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import {createRoot} from "react-dom/client"; 2 | 3 | import * as Liqvid from "../../../src/index"; 4 | import {Playback, Player, Video} from "../../../src/index"; 5 | 6 | // simplifies testing for now 7 | window.Liqvid = Liqvid; 8 | 9 | const playback = new Playback({duration: 60000}); 10 | 11 | function Lesson() { 12 | return ( 13 | 14 | 17 | 18 | ); 19 | } 20 | 21 | createRoot(document.querySelector("main")).render(); 22 | -------------------------------------------------------------------------------- /packages/main/e2e/app/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/main/e2e/app/static/liqvid.min.css: -------------------------------------------------------------------------------- 1 | ../../../dist/liqvid.min.css -------------------------------------------------------------------------------- /packages/main/e2e/app/static/style.css: -------------------------------------------------------------------------------- 1 | video { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /packages/main/e2e/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "incremental": true, 6 | "jsx": "react-jsx", 7 | "lib": ["es2015", "es2016", "es2017", "dom"], 8 | "moduleResolution": "node", 9 | "pretty": true, 10 | "removeComments": true, 11 | "target": "es2017", 12 | 13 | "paths": { 14 | "@env/*": ["./src/@development/*", "./src/@production/*"] 15 | } 16 | }, 17 | "files": ["./src/index.tsx"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/main/e2e/app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const TerserPlugin = require("terser-webpack-plugin"); 2 | const path = require("path"); 3 | const env = process.env.NODE_ENV || "development"; 4 | require("dotenv").config({path: "../../.env"}); 5 | const webpack = require("webpack"); 6 | 7 | module.exports = { 8 | entry: `./src/index.tsx`, 9 | output: { 10 | filename: "bundle.js", 11 | path: path.join(__dirname, "static"), 12 | }, 13 | 14 | mode: env, 15 | 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.[jt]sx?$/, 20 | loader: "ts-loader", 21 | }, 22 | ], 23 | }, 24 | 25 | plugins: [new webpack.EnvironmentPlugin(["PLAYWRIGHT_TEST_VIDEO"])], 26 | 27 | // necessary due to bug in old versions of mobile Safari 28 | devtool: false, 29 | 30 | optimization: { 31 | minimizer: [ 32 | new TerserPlugin({ 33 | parallel: true, 34 | terserOptions: { 35 | safari10: true, 36 | }, 37 | }), 38 | ], 39 | emitOnErrors: true, 40 | }, 41 | 42 | resolve: { 43 | extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], 44 | alias: { 45 | "@env": path.join(__dirname, "src", "@" + env), 46 | }, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /packages/main/e2e/tests/Media.spec.tsx: -------------------------------------------------------------------------------- 1 | import {ElementHandle, expect, JSHandle, test} from "@playwright/test"; 2 | import type {Playback, Player} from "../../src/index"; 3 | 4 | test.describe("Media", () => { 5 | let playback: JSHandle; 6 | let player: JSHandle; 7 | let video: ElementHandle; 8 | 9 | test.beforeEach(async ({page}) => { 10 | await page.goto("/"); 11 | 12 | // globals 13 | player = await page.evaluateHandle(() => { 14 | return (document.querySelector(".lv-player") as HTMLDivElement)[ 15 | window.Liqvid.Player.symbol 16 | ] as Player; 17 | }); 18 | playback = await player.evaluateHandle((player) => player.playback); 19 | 20 | // load video 21 | const locator = page.locator("video"); 22 | await locator.waitFor(); 23 | await locator.evaluate((video) => 24 | window.Liqvid.Utils.media.awaitMediaCanPlay(video), 25 | ); 26 | 27 | // create handle 28 | video = (await locator.elementHandle()) as ElementHandle; 29 | }); 30 | 31 | test("seeking past video.duration should seek to video end", async () => { 32 | await playback.evaluate((p) => p.seek(p.duration)); 33 | expect(await video.evaluate((v) => v.currentTime === v.duration)).toBe( 34 | true, 35 | ); 36 | }); 37 | 38 | test("restarting playback should restart video", async () => { 39 | await playback.evaluate((p) => { 40 | p.seek(p.duration); 41 | }); 42 | expect(await video.evaluate((v) => v.currentTime === v.duration)).toBe( 43 | true, 44 | ); 45 | // don't batch with the previous evaluate or else video won't have time to update 46 | await playback.evaluate((p) => p.play()); 47 | expect(await video.evaluate((v) => v.currentTime)).toBe(0); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/main/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | testPathIgnorePatterns: ["dist", "e2e"], 5 | transform: {}, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liqvid", 3 | "version": "2.1.19", 4 | "description": "Library for playing interactive videos using HTML/CSS/Javascript", 5 | "files": ["dist/*"], 6 | "main": "./dist/liqvid.js", 7 | "module": "./dist/liqvid.mjs", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/esm/index.mjs", 11 | "require": "./dist/cjs/index.cjs", 12 | "types": "./dist/types/index.d.ts" 13 | }, 14 | "./dist/liqvid.css": "./dist/liqvid.css", 15 | "./dist/liqvid.min.css": "./dist/liqvid.min.css", 16 | "./liqvid.css": "./dist/liqvid.css", 17 | "./liqvid.min.css": "./dist/liqvid.min.css" 18 | }, 19 | "scripts": { 20 | "build": "pnpm build:clean && pnpm build:css && pnpm build:js", 21 | "build:clean": "rm -rf dist", 22 | "build:css": "stylus -o dist/liqvid.css styl/liqvid.styl; stylus -c -o dist/liqvid.min.css styl/liqvid.styl", 23 | "build:js": "pnpm build:js:bundle; pnpm build:js:cjs; pnpm build:js:esm; pnpm build:js:fix", 24 | "build:js:bundle": "tsc && rollup -c && rm -rf dist/esm", 25 | "build:js:cjs": "tsc --module commonjs --outDir dist/cjs", 26 | "build:js:esm": "tsc --module esnext --outDir dist/esm", 27 | "build:js:fix": "node ../../build.mjs", 28 | "lint": "eslint --ext ts,tsx --fix e2e src tests", 29 | "stylus": "stylus -o dist/liqvid.css -w styl/liqvid.styl", 30 | "stylus:prod": "stylus -c -o dist/liqvid.min.css -w styl/liqvid.styl", 31 | "test": "pnpm test:jest && pnpm test:build-e2e && pnpm test:playwright", 32 | "test:build-e2e": "cd e2e/app && pnpm build && cd ../..", 33 | "test:jest": "jest", 34 | "test:playwright": "npx playwright test" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/liqvidjs/liqvid.git" 39 | }, 40 | "author": "Yuri Sulyma ", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/liqvidjs/liqvid/issues" 44 | }, 45 | "homepage": "https://github.com/liqvidjs/liqvid/tree/master/packages/main#readme", 46 | "devDependencies": { 47 | "nib": "^1.1.2", 48 | "stylus": "^0.57.0", 49 | "tslib": "^2.4.0", 50 | "typedoc": "^0.22.15", 51 | "typedoc-plugin-markdown": "^3.12.1" 52 | }, 53 | "dependencies": { 54 | "@liqvid/keymap": "workspace:^", 55 | "@liqvid/playback": "workspace:^", 56 | "@liqvid/utils": "workspace:^", 57 | "@types/events": "^3.0.0", 58 | "@types/node": "^22.10.10", 59 | "events": "^3.3.0", 60 | "strict-event-emitter-types": "^2.0.0" 61 | }, 62 | "peerDependencies": { 63 | "@types/react": ">=17", 64 | "@types/react-dom": ">=17", 65 | "react": ">=17", 66 | "react-dom": ">=17" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/main/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | 4 | import type {PlaywrightTestConfig} from "@playwright/test"; 5 | const config: PlaywrightTestConfig = { 6 | testDir: "e2e/tests", 7 | use: { 8 | baseURL: process.env.PLAYWRIGHT_HOST, 9 | headless: true, 10 | launchOptions: { 11 | executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH, 12 | }, 13 | viewport: {width: 1280, height: 720}, 14 | ignoreHTTPSErrors: true, 15 | video: "off", 16 | }, 17 | webServer: { 18 | command: "cd e2e/app && pnpm serve", 19 | url: process.env.PLAYWRIGHT_HOST, 20 | reuseExistingServer: !process.env.CI, 21 | }, 22 | }; 23 | export default config; 24 | -------------------------------------------------------------------------------- /packages/main/rollup.config.js: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import {getBabelOutputPlugin} from "@rollup/plugin-babel"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import {nodeResolve} from "@rollup/plugin-node-resolve"; 5 | import dts from "rollup-plugin-dts"; 6 | import {terser} from "rollup-plugin-terser"; 7 | 8 | // banner 9 | const licenseComment = "/*!" + fs.readFileSync("./LICENSE", "utf8") + "*/"; 10 | const useClientDirective = '"use client";'; 11 | const banner = `${licenseComment}\n${useClientDirective}`; 12 | 13 | /* shared UMD config --- don't put plugins here bc array will get copied by reference */ 14 | const umdConfig = { 15 | banner, 16 | format: "esm", 17 | globals: { 18 | react: "React", 19 | "react-dom": "ReactDOM", 20 | }, 21 | }; 22 | 23 | // babel config 24 | const babelConfig = () => 25 | getBabelOutputPlugin({ 26 | plugins: [ 27 | [ 28 | "@babel/plugin-transform-modules-umd", 29 | { 30 | globals: { 31 | react: "React", 32 | "react-dom": "ReactDOM", 33 | }, 34 | moduleId: "Liqvid", 35 | moduleRoot: "Liqvid", 36 | }, 37 | ], 38 | ], 39 | presets: [["@babel/env", {targets: {ios: "12"}}]], 40 | }); 41 | 42 | export default [ 43 | { 44 | external: ["react", "react-dom"], 45 | input: "dist/esm/index.js", 46 | plugins: [nodeResolve({preferBuiltins: false}), commonjs()], 47 | 48 | output: [ 49 | // ESM 50 | { 51 | banner, 52 | file: "./dist/liqvid.mjs", 53 | format: "esm", 54 | }, 55 | // UMD development 56 | { 57 | ...umdConfig, 58 | file: "./dist/liqvid.js", 59 | plugins: [babelConfig()], 60 | }, 61 | // UMD production 62 | { 63 | ...umdConfig, 64 | file: "./dist/liqvid.min.js", 65 | plugins: [babelConfig(), terser({module: false, safari10: true})], 66 | }, 67 | ], 68 | }, 69 | // types 70 | { 71 | input: "dist/types/index.d.ts", 72 | plugins: [dts()], 73 | output: { 74 | file: "dist/liqvid.d.ts", 75 | format: "es", 76 | }, 77 | }, 78 | ]; 79 | -------------------------------------------------------------------------------- /packages/main/src/Audio.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Media} from "./Media"; 3 | 4 | import {fragmentFromHTML} from "./utils/dom"; 5 | 6 | /** Liqvid equivalent of {@link HTMLAudioElement `