├── src ├── core │ ├── index.ts │ ├── .gitignore │ ├── types │ │ ├── asset │ │ │ ├── Asset.ts │ │ │ ├── AudioAsset.ts │ │ │ ├── FontAsset.ts │ │ │ ├── ImageAsset.ts │ │ │ ├── VideoAsset.ts │ │ │ ├── index.ts │ │ │ └── ScriptAsset.ts │ │ ├── effects │ │ │ ├── Effect.ts │ │ │ ├── KeyFrame.ts │ │ │ ├── index.ts │ │ │ ├── AudioEffect.ts │ │ │ ├── ScriptEffect.ts │ │ │ ├── ImageEffect.ts │ │ │ ├── VideoEffect.ts │ │ │ └── TextEffect.ts │ │ ├── index.ts │ │ ├── Strip.ts │ │ ├── utils │ │ │ ├── isAudioEffect.ts │ │ │ ├── isImageEffect.ts │ │ │ ├── isVideoEffect.ts │ │ │ ├── index.ts │ │ │ ├── hasKeyFrame.ts │ │ │ ├── isTextEffect.ts │ │ │ └── calculateKeyFrameValue.ts │ │ ├── hooks │ │ │ └── index.ts │ │ ├── AppContext.ts │ │ └── easing.ts │ ├── package.json │ ├── tsconfig.json │ └── build.js ├── tsconfig.json ├── public │ ├── icon.png │ ├── screenshot.png │ └── icon.html ├── interfaces │ ├── UserData.ts │ ├── strips │ │ ├── checkOverlap.ts │ │ ├── canMove.tsx │ │ ├── moveStrip.tsx │ │ └── snap.tsx │ └── plugins │ │ └── CustomEffect.ts ├── components │ ├── Row.tsx │ ├── PropertyName.tsx │ ├── Card.tsx │ ├── asset_details_panel │ │ ├── VideoAssetDetailsPanel.tsx │ │ ├── TextAssetDetailsPanel.tsx │ │ ├── AudioAssetDetailsPanel.tsx │ │ ├── ImageAssetDetailsPanel.tsx │ │ ├── ScriptAssetDetailsPanel.tsx │ │ ├── EditableView.tsx │ │ └── AssetDetailsPanel.tsx │ ├── timeline_panel │ │ ├── TextEffectStripUI.tsx │ │ ├── VideoEffectStripUI.tsx │ │ ├── ImageEffectStripUI.tsx │ │ ├── AudioEffectStripUI.tsx │ │ ├── StripUI.tsx │ │ └── AddStripButton.tsx │ ├── strip_panel │ │ ├── effects │ │ │ ├── audio │ │ │ │ ├── audioEffectOptions.tsx │ │ │ │ └── AudioEffectView.tsx │ │ │ ├── text │ │ │ │ ├── textEffectConfig.ts │ │ │ │ └── TextEffectView.tsx │ │ │ ├── image │ │ │ │ └── imageEffectOptions.ts │ │ │ └── video │ │ │ │ ├── videoEffectOptions.tsx │ │ │ │ └── VideoEffectView.tsx │ │ ├── ScriptEffectView.tsx │ │ ├── KeyframeButton.tsx │ │ ├── StripPanel.tsx │ │ ├── Effects.tsx │ │ └── AddEffectButton.tsx │ ├── preview_panel │ │ ├── CurrentTime.tsx │ │ ├── utils │ │ │ ├── makeNewKeyframes.tsx │ │ │ └── textEffectToRect.tsx │ │ ├── createResizeHandler.tsx │ │ ├── useHandleSelectStrip.tsx │ │ └── Gizmo.tsx │ ├── Flex.tsx │ ├── keyframes_panel │ │ ├── useClickOutside.tsx │ │ ├── MakeSVG.tsx │ │ └── ChangeEaseButton.tsx │ ├── SplashModal.tsx │ ├── RecordMenuButton.tsx │ ├── SettingsMenuButton.tsx │ ├── MenuButton.tsx │ └── assets_panel │ │ └── AssetPanel.tsx ├── types │ └── PickProperties.ts ├── hooks │ ├── useFps.ts │ ├── useCurrentTime.ts │ ├── useStripTime.ts │ ├── useSelector.ts │ ├── useFirstRender.tsx │ ├── useSelectedStrip.ts │ ├── useAssetOptions.tsx │ ├── useAnimationedValue.tsx │ ├── useGetAppContext.tsx │ └── useUpdateEffect.tsx ├── utils │ ├── jsonCompare.ts │ ├── checkFocus.ts │ ├── roundToFrame.ts │ ├── hasKeyFrame.ts │ ├── exactKeyFrame.ts │ ├── readRecentFiles.ts │ ├── registerGlobalVar.ts │ ├── compareScene.ts │ ├── download.ts │ ├── formatForSave.ts │ ├── restrictStartEnd.ts │ ├── updateEffectByResolutions.ts │ ├── filePick.ts │ └── initGlobalEvent.ts ├── electron-src │ ├── ipc │ │ ├── const.ts │ │ ├── writeFile.ts │ │ ├── readFile.ts │ │ ├── readFileUserDataDir.ts │ │ └── writeFileUserDataDir.ts │ ├── electron-next.d.ts │ ├── preload.ts │ ├── tsconfig.json │ └── index.ts ├── pages │ ├── mobile │ │ ├── TextEffectView.tsx │ │ ├── StripDetails.tsx │ │ ├── utils │ │ │ └── getTouchDragHandler.tsx │ │ ├── index.page.tsx │ │ ├── Timeline.tsx │ │ ├── FAB.tsx │ │ └── MobileStrip.tsx │ ├── _document.page.tsx │ └── index.page.tsx ├── store.ts ├── rendering │ ├── stripIsVisible.tsx │ ├── updateScriptEffect.ts │ ├── updateImageEffect.tsx │ ├── recorder.ts │ ├── updateAudioEffect.tsx │ ├── updateTextEffect.tsx │ └── updateVideoEffect.tsx ├── ipc │ ├── readFile.ts │ ├── writeFile.ts │ ├── writeFileUserDataDir.ts │ └── readFileUserDataDir.ts ├── store │ ├── app.ts │ └── scene.ts ├── global.d.ts └── UndoManager.ts ├── .yarnrc.yml ├── .gitmodules ├── script └── after_build_renderer.sh ├── README.md ├── next.config.js ├── .eslintrc.json ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ └── release.yaml ├── package.json └── LICENSE /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /src/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshusai/Vega/HEAD/src/public/icon.png -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.4.1.cjs 4 | -------------------------------------------------------------------------------- /src/interfaces/UserData.ts: -------------------------------------------------------------------------------- 1 | export type UserData = { 2 | recentUsedProjects: string[]; 3 | }; 4 | -------------------------------------------------------------------------------- /src/public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshusai/Vega/HEAD/src/public/screenshot.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/app-ui"] 2 | path = src/app-ui 3 | url = git@github.com:toshusai/app-ui.git 4 | -------------------------------------------------------------------------------- /src/components/Row.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Row = styled.div` 4 | display: flex; 5 | `; 6 | -------------------------------------------------------------------------------- /src/core/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .pnp.* 3 | .yarn/* 4 | !.yarn/patches 5 | !.yarn/plugins 6 | !.yarn/releases 7 | !.yarn/sdks 8 | !.yarn/versions -------------------------------------------------------------------------------- /src/core/types/asset/Asset.ts: -------------------------------------------------------------------------------- 1 | export type Asset = { 2 | id: string; 3 | name: string; 4 | type: string; 5 | path: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/types/PickProperties.ts: -------------------------------------------------------------------------------- 1 | 2 | export type PickProperties = { 3 | [K in keyof T as T[K] extends TFilter ? K : never]: T[K]; 4 | }; 5 | -------------------------------------------------------------------------------- /src/components/PropertyName.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | 4 | export const PropertyName = styled.div` 5 | margin-right: auto; 6 | `; 7 | -------------------------------------------------------------------------------- /src/core/types/asset/AudioAsset.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from "./Asset"; 2 | 3 | export type AudioAsset = Asset & { 4 | type: "audio"; 5 | path: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/core/types/asset/FontAsset.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from "./Asset"; 2 | 3 | export type FontAsset = Asset & { 4 | type: "font"; 5 | path: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/core/types/asset/ImageAsset.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from "./Asset"; 2 | 3 | export type ImageAsset = Asset & { 4 | type: "image"; 5 | path: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/core/types/asset/VideoAsset.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from "./Asset"; 2 | 3 | export type VideoAsset = Asset & { 4 | type: "video"; 5 | path: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/hooks/useFps.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from "./useSelector"; 2 | 3 | export function useFps() { 4 | return useSelector((state) => state.scene.fps); 5 | } 6 | -------------------------------------------------------------------------------- /src/core/types/effects/Effect.ts: -------------------------------------------------------------------------------- 1 | import { KeyFrame } from "./KeyFrame"; 2 | 3 | export type Effect = { 4 | id: string; 5 | type: string; 6 | keyframes: KeyFrame[]; 7 | }; 8 | -------------------------------------------------------------------------------- /src/hooks/useCurrentTime.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from "./useSelector"; 2 | 3 | export function useCurrentTime() { 4 | return useSelector((state) => state.scene.currentTime); 5 | } 6 | -------------------------------------------------------------------------------- /script/after_build_renderer.sh: -------------------------------------------------------------------------------- 1 | ## replace all src="/ with src=" for electron to work 2 | sed -i '' -e 's/src="\//src="/g' out/index.html 3 | ## copy public folder 4 | cp -r src/public out/public 5 | -------------------------------------------------------------------------------- /src/core/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Strip"; 2 | export * from "./easing"; 3 | export * from "./asset"; 4 | export * from "./effects"; 5 | export * from "./utils"; 6 | export * from "./AppContext"; 7 | -------------------------------------------------------------------------------- /src/utils/jsonCompare.ts: -------------------------------------------------------------------------------- 1 | import { sortStringify } from "./formatForSave"; 2 | 3 | export const jsonCompare = (a: any, b: any) => { 4 | return sortStringify(a, undefined) === sortStringify(b, undefined); 5 | }; 6 | -------------------------------------------------------------------------------- /src/core/types/Strip.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from "./effects/Effect"; 2 | 3 | export type Strip = { 4 | id: string; 5 | start: number; 6 | length: number; 7 | effects: Effect[]; 8 | layer: number; 9 | }; 10 | -------------------------------------------------------------------------------- /src/core/types/asset/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Asset"; 2 | export * from "./AudioAsset"; 3 | export * from "./FontAsset"; 4 | export * from "./ImageAsset"; 5 | export * from "./ScriptAsset"; 6 | export * from "./VideoAsset"; 7 | -------------------------------------------------------------------------------- /src/core/types/effects/KeyFrame.ts: -------------------------------------------------------------------------------- 1 | import { Ease } from "../easing"; 2 | 3 | export type KeyFrame = { 4 | id: string; 5 | time: number; 6 | value: number; 7 | property: string; 8 | ease: Ease; 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/checkFocus.ts: -------------------------------------------------------------------------------- 1 | export function checkFocus() { 2 | const el = document.activeElement; 3 | if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA")) { 4 | return true; 5 | } 6 | return false; 7 | } 8 | -------------------------------------------------------------------------------- /src/electron-src/ipc/const.ts: -------------------------------------------------------------------------------- 1 | export const WRITE_FILE_USER_DATA_DIR = "writeFileUserDataDir"; 2 | export const READ_FILE_USER_DATA_DIR = "readFileUserDataDir"; 3 | export const WRITE_FILE = "writeFile"; 4 | export const READ_FILE = "readFile"; -------------------------------------------------------------------------------- /src/core/types/utils/isAudioEffect.ts: -------------------------------------------------------------------------------- 1 | import { AudioEffect } from "../effects"; 2 | import { Effect } from "../effects"; 3 | 4 | export function isAudioEffect(effect: Effect): effect is AudioEffect { 5 | return effect.type === "audio"; 6 | } 7 | -------------------------------------------------------------------------------- /src/core/types/utils/isImageEffect.ts: -------------------------------------------------------------------------------- 1 | import { ImageEffect } from "../effects"; 2 | import { Effect } from "../effects"; 3 | 4 | export function isImageEffect(effect: Effect): effect is ImageEffect { 5 | return effect.type === "image"; 6 | } 7 | -------------------------------------------------------------------------------- /src/core/types/utils/isVideoEffect.ts: -------------------------------------------------------------------------------- 1 | import { VideoEffect } from "../effects"; 2 | import { Effect } from "../effects"; 3 | 4 | export function isVideoEffect(effect: Effect): effect is VideoEffect { 5 | return effect.type === "video"; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/roundToFrame.ts: -------------------------------------------------------------------------------- 1 | export function roundToFrame(time: number, fps: number) { 2 | return Math.round(time * fps) / fps; 3 | } 4 | 5 | export function floorFrame(time: number, fps: number) { 6 | return Math.round(time * fps); 7 | } 8 | -------------------------------------------------------------------------------- /src/core/types/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./hasKeyFrame"; 2 | export * from "./isAudioEffect"; 3 | export * from "./isImageEffect"; 4 | export * from "./isTextEffect"; 5 | export * from "./isVideoEffect"; 6 | export * from "./calculateKeyFrameValue"; 7 | -------------------------------------------------------------------------------- /src/hooks/useStripTime.ts: -------------------------------------------------------------------------------- 1 | import { Strip } from "@/core/types"; 2 | 3 | import { useSelector } from "./useSelector"; 4 | 5 | export function useStripTime(strip: Strip) { 6 | return useSelector((state) => state.scene.currentTime - strip.start); 7 | } 8 | -------------------------------------------------------------------------------- /src/core/types/effects/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AudioEffect"; 2 | export * from "./Effect"; 3 | export * from "./ImageEffect"; 4 | export * from "./KeyFrame"; 5 | export * from "./ScriptEffect"; 6 | export * from "./TextEffect"; 7 | export * from "./VideoEffect"; 8 | -------------------------------------------------------------------------------- /src/hooks/useSelector.ts: -------------------------------------------------------------------------------- 1 | import { useSelector as useSelectorRR } from "react-redux"; 2 | 3 | import { SelectorType } from "@/store"; 4 | 5 | export function useSelector(f: (state: SelectorType) => T): T { 6 | return useSelectorRR(f); 7 | } 8 | -------------------------------------------------------------------------------- /src/core/types/utils/hasKeyFrame.ts: -------------------------------------------------------------------------------- 1 | import { KeyFrame } from "../effects/KeyFrame"; 2 | 3 | export function hasKeyFrameProperty(object: any): object is { keyframes: KeyFrame[] } { 4 | return object.keyframes !== undefined && Array.isArray(object.keyframes); 5 | } 6 | -------------------------------------------------------------------------------- /src/core/types/effects/AudioEffect.ts: -------------------------------------------------------------------------------- 1 | import { KeyFrame } from "./KeyFrame"; 2 | 3 | export type AudioEffect = { 4 | id: string; 5 | type: "audio"; 6 | audioAssetId: string; 7 | volume: number; 8 | offset: number; 9 | keyframes: KeyFrame[]; 10 | }; 11 | -------------------------------------------------------------------------------- /src/hooks/useFirstRender.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function useFirstRender() { 4 | const [firstRender, setFirstRender] = React.useState(true); 5 | React.useEffect(() => { 6 | setFirstRender(false); 7 | }, []); 8 | return firstRender; 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/useSelectedStrip.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from "./useSelector"; 2 | 3 | export function useSelectedStrip() { 4 | return useSelector((state) => 5 | state.scene.strips.filter((s) => 6 | state.scene.selectedStripIds.includes(s.id) 7 | ) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/hasKeyFrame.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from "@/core/types"; 2 | 3 | export function hasKeyFrame( 4 | effect: T, 5 | key: keyof T 6 | ) { 7 | if (!effect.keyframes) return false; 8 | return effect.keyframes.some((k) => k.property === key); 9 | } 10 | -------------------------------------------------------------------------------- /src/electron-src/electron-next.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'electron-next' { 2 | interface Directories { 3 | production: string 4 | development: string 5 | } 6 | 7 | export default function ( 8 | directories: Directories | string, 9 | port?: number 10 | ): Promise 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import { COLOR_BACKGROUND_NAME } from "@/app-ui/src"; 4 | 5 | export const Card = styled.div` 6 | display: flex; 7 | width: 100%; 8 | height: 100%; 9 | 10 | background-color: var(${COLOR_BACKGROUND_NAME}); 11 | `; 12 | -------------------------------------------------------------------------------- /src/core/types/utils/isTextEffect.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from "../effects"; 2 | import { TextEffect } from "../effects/TextEffect"; 3 | import { hasKeyFrameProperty } from "./hasKeyFrame"; 4 | 5 | export function isTextEffect(effect: Effect): effect is TextEffect { 6 | return effect.type === "text" && hasKeyFrameProperty(effect); 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vega 2 | 3 | Vega is a video editing software. 4 | 5 | ![Vega](src/public/screenshot.png) 6 | 7 | ## Features 8 | 9 | - :desktop_computer:️ Cross platform 10 | - :star2: Video, Audio, Image, Text 11 | - :key: Keyframe animations 12 | - :scissors: Copy, Cut, Paste 13 | - :family_man_woman_girl_boy: Multi select 14 | - :keyboard: Keyboards shortcuts 15 | - :package: Plugin System 16 | -------------------------------------------------------------------------------- /src/core/types/effects/ScriptEffect.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from "./Effect"; 2 | import { KeyFrame } from "./KeyFrame"; 3 | 4 | 5 | export type ScriptEffect = { 6 | id: string; 7 | type: "script"; 8 | scriptAssetId: string; 9 | keyframes: KeyFrame[]; 10 | }; 11 | 12 | export function isScriptEffect(effect: Effect): effect is ScriptEffect { 13 | return effect.type === "script"; 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/mobile/TextEffectView.tsx: -------------------------------------------------------------------------------- 1 | import { VNumberInput } from "@/app-ui/src"; 2 | import { TextEffect } from "@/core"; 3 | 4 | export function TextEffectView(props: { effect: TextEffect }) { 5 | return ( 6 |
7 |
Text
8 |
9 |
size
10 | 11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/exactKeyFrame.ts: -------------------------------------------------------------------------------- 1 | import { KeyFrame } from "@/core/types"; 2 | 3 | export function exactKeyFrame< 4 | T extends { 5 | keyframes: KeyFrame[]; 6 | } 7 | >(effect: T, key: keyof T, time: number) { 8 | if (!effect.keyframes) { 9 | return false; 10 | } 11 | return effect.keyframes.find( 12 | (k) => k.property === key && Math.abs(k.time - time) < 1 / 60 / 2 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/electron-src/preload.ts: -------------------------------------------------------------------------------- 1 | import { IpcRenderer,ipcRenderer } from "electron"; 2 | 3 | declare global { 4 | namespace NodeJS { 5 | interface Global { 6 | ipcRenderer: IpcRenderer; 7 | } 8 | } 9 | } 10 | 11 | // Since we disabled nodeIntegration we can reintroduce 12 | // needed node functionality here 13 | process.once("loaded", () => { 14 | global.ipcRenderer = ipcRenderer; 15 | }); 16 | -------------------------------------------------------------------------------- /src/utils/readRecentFiles.ts: -------------------------------------------------------------------------------- 1 | import { readFileUserDataDir } from "@/ipc/readFileUserDataDir"; 2 | 3 | export function readRecentFiles() { 4 | let fileJson = readFileUserDataDir("recentFiles.json"); 5 | if (fileJson === false) { 6 | // throw new Error("Could not read recent files"); 7 | fileJson = "[]"; 8 | } 9 | const recentFiles = JSON.parse(fileJson); 10 | return recentFiles; 11 | } 12 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | 3 | import { appReducer, AppState } from "./store/app"; 4 | import { sceneReducer, SceneState } from "./store/scene"; 5 | 6 | export type SelectorType = { 7 | scene: SceneState; 8 | app: AppState; 9 | }; 10 | 11 | export default configureStore({ 12 | reducer: { 13 | scene: sceneReducer, 14 | app: appReducer, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/electron-src/ipc/writeFile.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from "electron"; 2 | import { writeFileSync } from "fs"; 3 | 4 | import { WRITE_FILE } from "./const"; 5 | 6 | export const initWriteFile = () => { 7 | ipcMain.on(WRITE_FILE, (e, path, data) => { 8 | try { 9 | writeFileSync(path, data); 10 | e.returnValue = true; 11 | } catch { 12 | e.returnValue = false; 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/asset_details_panel/VideoAssetDetailsPanel.tsx: -------------------------------------------------------------------------------- 1 | import { VideoAsset } from "@/core/types"; 2 | 3 | export function VideoAssetDetailsPanel(props: { asset: VideoAsset; }) { 4 | return ( 5 |
6 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/core/types/asset/ScriptAsset.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from "./Asset"; 2 | 3 | export type ScriptAsset = Asset & { 4 | type: "script"; 5 | path: string; 6 | }; 7 | 8 | export type ScriptMeta = { 9 | id: string; 10 | name: string; 11 | description: string; 12 | version: string; 13 | }; 14 | 15 | export function isScriptAsset(asset: Asset): asset is ScriptAsset { 16 | return asset.type === "script"; 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/registerGlobalVar.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import * as RiAppUi from "@/app-ui/src"; 5 | import * as Core from "@/core"; 6 | 7 | export function registerGlobalVar() { 8 | if (typeof window !== "undefined") { 9 | window.React = React; 10 | window.styled = styled; 11 | window.Core = Core; 12 | window.RiAppUi = RiAppUi; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/electron-src/ipc/readFile.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from "electron"; 2 | import { readFileSync } from "fs"; 3 | 4 | import { READ_FILE } from "./const"; 5 | 6 | export const initReadFile = () => { 7 | ipcMain.on(READ_FILE, (e, path) => { 8 | try { 9 | const data = readFileSync(path).toString(); 10 | e.returnValue = data; 11 | } catch { 12 | e.returnValue = false; 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/compareScene.ts: -------------------------------------------------------------------------------- 1 | import { SceneState } from "@/store/scene"; 2 | 3 | import { jsonCompare } from "./jsonCompare"; 4 | 5 | export const compareScene = (scene: SceneState, old: SceneState) => { 6 | return ( 7 | jsonCompare(scene.assets, old.assets) && 8 | jsonCompare(scene.strips, old.strips) && 9 | scene.canvasHeight === old.canvasHeight && 10 | scene.canvasWidth === old.canvasWidth && 11 | scene.fps === old.fps 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/rendering/stripIsVisible.tsx: -------------------------------------------------------------------------------- 1 | import { Strip } from "@/core/types"; 2 | import { floorFrame } from "@/utils/roundToFrame"; 3 | 4 | 5 | export function stripIsVisible(strip: Strip, currentTime: number, fps: number) { 6 | const currentFrame = floorFrame(currentTime, fps); 7 | const startFrame = floorFrame(strip.start, fps); 8 | const endFrame = floorFrame(strip.start + strip.length, fps); 9 | return currentFrame >= startFrame && currentFrame < endFrame; 10 | } 11 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | /** 3 | * @type {import('next').NextConfig} 4 | */ 5 | const nextConfig = { 6 | /* config options here */ 7 | compiler: { 8 | styledComponents: true, 9 | }, 10 | experimental: { 11 | appDir: false, 12 | }, 13 | pageExtensions: ["page.tsx"], 14 | webpack(config) { 15 | config.resolve.alias["@"] = path.join(__dirname, "src"); 16 | return config; 17 | }, 18 | }; 19 | 20 | module.exports = nextConfig; 21 | -------------------------------------------------------------------------------- /src/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vega-types", 3 | "version": "1.0.0", 4 | "description": "", 5 | "module": "dist/esm/index.js", 6 | "types": "dist/core/index.d.ts", 7 | "scripts": { 8 | "build": "tsc --declaration --emitDeclarationOnly --declarationDir './dist' && node build.js" 9 | }, 10 | "author": "tosuhsai.net@gmail.com", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "esbuild": "^0.17.7", 14 | "typescript": "^4.9.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/core/types/effects/ImageEffect.ts: -------------------------------------------------------------------------------- 1 | import { KeyFrame } from "./KeyFrame"; 2 | 3 | export type ImageEffect = { 4 | id: string; 5 | type: "image"; 6 | imageAssetId: string; 7 | x: number; 8 | y: number; 9 | /** 10 | * @deprecated use width instead 11 | */ 12 | scaleX?: number; 13 | /** 14 | * @deprecated use height instead 15 | */ 16 | scaleY?: number; 17 | width?: number; 18 | height?: number; 19 | opacity?: number; 20 | keyframes: KeyFrame[]; 21 | }; 22 | -------------------------------------------------------------------------------- /src/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "incremental": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "outDir": "./dist" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ipc/readFile.ts: -------------------------------------------------------------------------------- 1 | import { READ_FILE } from "@/electron-src/ipc/const"; 2 | 3 | import { isElectron } from "./readFileUserDataDir"; 4 | 5 | /** 6 | * read file from disk 7 | * 8 | * @param url file url should be started with file:// 9 | * @returns 10 | */ 11 | export function readFile(url: string) { 12 | if (!isElectron()) { 13 | return localStorage.getItem(url) || false; 14 | } 15 | const res = ipcRenderer.sendSync(READ_FILE, url.replace("file://", "")); 16 | return res; 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/download.ts: -------------------------------------------------------------------------------- 1 | 2 | export function download(blob: Blob | string, name: string) { 3 | const link = document.createElement("a"); 4 | if (link.href) { 5 | URL.revokeObjectURL(link.href); 6 | } 7 | if (typeof blob === "string") { 8 | link.href = "data:text/json;charset=utf-8," + encodeURIComponent(blob); 9 | } else { 10 | link.href = URL.createObjectURL(blob); 11 | } 12 | link.download = name; 13 | link.dispatchEvent(new MouseEvent("click")); 14 | link.remove(); 15 | } 16 | -------------------------------------------------------------------------------- /src/core/types/effects/VideoEffect.ts: -------------------------------------------------------------------------------- 1 | import { KeyFrame } from "./KeyFrame"; 2 | 3 | export type VideoEffect = { 4 | id: string; 5 | type: "video"; 6 | videoAssetId: string; 7 | x: number; 8 | y: number; 9 | 10 | /** 11 | * @deprecated use width instead 12 | */ 13 | scaleX?: number; 14 | /** 15 | * @deprecated use height instead 16 | */ 17 | scaleY?: number; 18 | 19 | width?: number; 20 | height?: number; 21 | 22 | keyframes: KeyFrame[]; 23 | 24 | playbackRate?: number; 25 | 26 | offset?: number; 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/timeline_panel/TextEffectStripUI.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import { isTextEffect, Strip } from "@/core/types"; 4 | 5 | export function TextEffectStripUI(props: { strip: Strip }) { 6 | const textEffect = props.strip.effects.find(isTextEffect); 7 | if (!textEffect) return null; 8 | return
{textEffect.text}
; 9 | } 10 | 11 | const Div = styled.div` 12 | margin: 0 12px; 13 | max-width: calc(100% - 24px); 14 | overflow: hidden; 15 | white-space: nowrap; 16 | align-self: center; 17 | user-select: none; 18 | `; 19 | -------------------------------------------------------------------------------- /src/electron-src/ipc/readFileUserDataDir.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from "electron"; 2 | import { readFileSync } from "fs"; 3 | import { join } from "path"; 4 | 5 | import { READ_FILE_USER_DATA_DIR } from "./const"; 6 | 7 | export const initReadFileUserDataDir = () => { 8 | ipcMain.on(READ_FILE_USER_DATA_DIR, (e, path: string) => { 9 | try { 10 | const dir = app.getPath("userData"); 11 | const data = readFileSync(join(dir, path)).toString(); 12 | e.returnValue = data; 13 | } catch { 14 | e.returnValue = false; 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/electron-src/ipc/writeFileUserDataDir.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from "electron"; 2 | import { writeFileSync } from "fs"; 3 | import { join } from "path"; 4 | 5 | import { WRITE_FILE_USER_DATA_DIR } from "./const"; 6 | 7 | export const initWriteFileUserDataDir = () => { 8 | ipcMain.on(WRITE_FILE_USER_DATA_DIR, (e, path: string, data: string) => { 9 | try { 10 | const dir = app.getPath("userData"); 11 | writeFileSync(join(dir, path), data); 12 | e.returnValue = true; 13 | } catch { 14 | e.returnValue = false; 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/interfaces/strips/checkOverlap.ts: -------------------------------------------------------------------------------- 1 | import { Strip } from "@/core/types"; 2 | 3 | const DIFF = 1 / 60 / 2; 4 | export function checkOverlap(strips: Strip[], strip: Strip): Strip | null { 5 | const isOverlapped = strips.findIndex((s) => { 6 | if (s.id === strip.id) { 7 | return false; 8 | } 9 | const isOverlapped = 10 | s.layer === strip.layer && 11 | s.start < strip.start + strip.length - DIFF && 12 | strip.start + DIFF < s.start + s.length; 13 | return isOverlapped; 14 | }); 15 | return isOverlapped >= 0 ? strips[isOverlapped] : null; 16 | } 17 | -------------------------------------------------------------------------------- /src/ipc/writeFile.ts: -------------------------------------------------------------------------------- 1 | import { WRITE_FILE } from "@/electron-src/ipc/const"; 2 | 3 | import { isElectron } from "./readFileUserDataDir"; 4 | 5 | /** 6 | * Writes data to a file 7 | * @param url file url should be started with file:// 8 | * @param data data to write 9 | * @returns 10 | */ 11 | export function writeFile(url: string, data: string) { 12 | if (!isElectron()) { 13 | localStorage.setItem(url, data); 14 | return true; 15 | } 16 | const res = ipcRenderer.sendSync( 17 | WRITE_FILE, 18 | url.replace("file://", ""), 19 | data 20 | ); 21 | return res; 22 | } 23 | -------------------------------------------------------------------------------- /src/core/types/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { PickProperties } from "../../../types/PickProperties"; 2 | import { Effect } from "../effects"; 3 | import { Strip } from "../Strip"; 4 | 5 | export type UseUpdateEffect = ( 6 | effect: T, 7 | strip: Strip 8 | ) => { 9 | emit: (partial: Partial) => void; 10 | addKeyFrame: (key: keyof T) => void; 11 | }; 12 | 13 | export type UseStripTime = (strip: Strip) => number; 14 | 15 | export type UseAnimationedValue = ( 16 | effect: T, 17 | strip: Strip 18 | ) => (key: keyof PickProperties) => number; 19 | -------------------------------------------------------------------------------- /src/hooks/useAssetOptions.tsx: -------------------------------------------------------------------------------- 1 | import { Item } from "@/app-ui/src"; 2 | import { useSelector } from "@/hooks/useSelector"; 3 | 4 | 5 | export function useAssetOptions(type: string) { 6 | const assets = useSelector((state) => state.scene.assets); 7 | const filteredAssets = assets.filter((a) => a.type === type); 8 | 9 | const filteredAssetItems: Item[] = filteredAssets.map((a) => ({ 10 | value: a.id, 11 | label: a.name, 12 | })); 13 | 14 | filteredAssetItems.unshift({ 15 | value: "", 16 | label: "No asset", 17 | disabled: true, 18 | }); 19 | return filteredAssetItems; 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/strips/canMove.tsx: -------------------------------------------------------------------------------- 1 | import { Strip } from "@/core/types"; 2 | 3 | import { checkOverlap } from "./checkOverlap"; 4 | 5 | export const MAX_LAYER = 32; 6 | 7 | export function canMove( 8 | strip: Strip, 9 | withoutSelectedStrips: Strip[], 10 | timelineLength: number 11 | ) { 12 | const isOverlap = checkOverlap(withoutSelectedStrips, strip); 13 | if ( 14 | isOverlap || 15 | strip.start < 0 || 16 | strip.start + strip.length > timelineLength || 17 | strip.layer < 0 || 18 | strip.layer > MAX_LAYER - 1 19 | ) { 20 | return false; 21 | } 22 | return true; 23 | } 24 | -------------------------------------------------------------------------------- /src/core/types/AppContext.ts: -------------------------------------------------------------------------------- 1 | import { UseAnimationedValue, UseStripTime, UseUpdateEffect } from "./hooks"; 2 | 3 | export interface AppContext { 4 | dispatch: any; 5 | actions: any; 6 | fs: { 7 | writeFile: (path: string, data: any) => Promise; 8 | readFile: (path: string) => Promise; 9 | writeFileUserDataDir: (path: string, data: any) => Promise; 10 | readFileUserDataDir: (path: string) => string | false; 11 | }; 12 | hooks: { 13 | useUpdateEffect: UseUpdateEffect; 14 | useStripTime: UseStripTime; 15 | useAnimationedValue: UseAnimationedValue; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/core/types/effects/TextEffect.ts: -------------------------------------------------------------------------------- 1 | import { KeyFrame } from "./KeyFrame"; 2 | 3 | export type TextEffect = { 4 | id: string; 5 | type: "text"; 6 | text: string; 7 | x: number; 8 | y: number; 9 | fontAssetId: string; 10 | fontSize: number; 11 | fontStyle?: string; 12 | color?: string; 13 | shadowColor?: string; 14 | shadowBlur?: number; 15 | outlineColor?: string; 16 | outlineWidth?: number; 17 | characterSpacing?: number; 18 | align?: TextAlign; 19 | keyframes: KeyFrame[]; 20 | }; 21 | 22 | export enum TextAlign { 23 | left = "left", 24 | center = "center", 25 | right = "right", 26 | } 27 | -------------------------------------------------------------------------------- /src/components/strip_panel/effects/audio/audioEffectOptions.tsx: -------------------------------------------------------------------------------- 1 | import { AudioEffect } from "@/core/types"; 2 | 3 | const numberKeys: (keyof AudioEffect)[] = ["volume", "offset"]; 4 | 5 | const keyframesKeys: (keyof AudioEffect)[] = ["volume"]; 6 | 7 | const scaleKeysMap: { 8 | [key in keyof AudioEffect]?: number; 9 | } = { 10 | volume: 0.01, 11 | offset: 0.01, 12 | }; 13 | 14 | const minMaxKeysMap: { 15 | [key in keyof AudioEffect]?: [number, number]; 16 | } = { 17 | volume: [0, 1], 18 | }; 19 | 20 | export const audioEffectOptions = { 21 | numberKeys, 22 | keyframesKeys, 23 | scaleKeysMap, 24 | minMaxKeysMap, 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/preview_panel/CurrentTime.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import { useSelector } from "@/hooks/useSelector"; 4 | import { floorFrame } from "@/utils/roundToFrame"; 5 | 6 | export function CurrentTime() { 7 | const currentTime = useSelector((state) => state.scene.currentTime); 8 | const fps = useSelector((state) => state.scene.fps); 9 | const curerntFrame = floorFrame(currentTime, fps); 10 | return ( 11 | 12 | {curerntFrame} / {currentTime.toFixed(2)} 13 | 14 | ); 15 | } 16 | 17 | const StyledDiv = styled.div` 18 | position: absolute; 19 | left: 8px; 20 | `; 21 | -------------------------------------------------------------------------------- /src/components/asset_details_panel/TextAssetDetailsPanel.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { FontAsset } from "@/core/types"; 3 | import { loadFont } from "@/rendering/updateTextEffect"; 4 | 5 | export function TextAssetDetailsPanel(props: { asset: FontAsset }) { 6 | loadFont(props.asset); 7 | return ( 8 |
9 |
16 | {sampleText} 17 |
18 |
19 | ); 20 | } 21 | const sampleText = `あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、うつくしい森で飾られたモリーオ市、郊外のぎらぎらひかる草の波。`; 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "plugin:import/recommended"], 3 | "plugins": ["unused-imports", "simple-import-sort", "@typescript-eslint"], 4 | "parser": "@typescript-eslint/parser", 5 | "ignorePatterns": ["src/core/*", "src/app-ui/*"], 6 | "rules": { 7 | "no-restricted-imports": ["error", { "patterns": ["../"] }], 8 | "react-hooks/exhaustive-deps": "error", 9 | "simple-import-sort/imports": "error", 10 | "simple-import-sort/exports": "error", 11 | "@typescript-eslint/no-unused-vars": "error", 12 | "@next/next/no-img-element": "off", 13 | "no-unused-vars": "off", 14 | "unused-imports/no-unused-imports": "error" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ipc/writeFileUserDataDir.ts: -------------------------------------------------------------------------------- 1 | import { WRITE_FILE_USER_DATA_DIR } from "@/electron-src/ipc/const"; 2 | 3 | import { isElectron, toUserDataLocalPath } from "./readFileUserDataDir"; 4 | /** 5 | * Writes data to a file in the userData directory 6 | * @param path relative path from userData directory e.g. "recent-projects.json" 7 | * @param data data to write 8 | * @returns 9 | */ 10 | 11 | export function writeFileUserDataDir(path: string, data: string) { 12 | if (!isElectron()) { 13 | localStorage.setItem(toUserDataLocalPath(path), data); 14 | return true; 15 | } 16 | const res = ipcRenderer.sendSync(WRITE_FILE_USER_DATA_DIR, path, data); 17 | return res; 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | main 39 | .next 40 | dist 41 | 42 | .pnp.* 43 | .yarn/* 44 | !.yarn/patches 45 | !.yarn/plugins 46 | !.yarn/releases 47 | !.yarn/sdks 48 | !.yarn/versions -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "ESNext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "ESNext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "outDir": "./main", 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/formatForSave.ts: -------------------------------------------------------------------------------- 1 | import { SceneState } from "@/store/scene"; 2 | 3 | export function sortStringify(data: SceneState, space: number | undefined = 2) { 4 | const json = JSON.stringify( 5 | data, 6 | (_, value) => { 7 | if (Array.isArray(value) && value instanceof Array) { 8 | return [...value].sort(); 9 | } 10 | if (value instanceof Object) { 11 | const ordered: { [key: string]: any } = {}; 12 | Object.keys(value) 13 | .sort() 14 | .forEach((key) => { 15 | ordered[key] = value[key]; 16 | }); 17 | 18 | return ordered; 19 | } 20 | 21 | return value; 22 | }, 23 | space 24 | ); 25 | return json; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/strip_panel/ScriptEffectView.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { ScriptEffect, Strip } from "@/core/types"; 4 | import { useGetAppContext } from "@/hooks/useGetAppContext"; 5 | import { userScriptMap } from "@/rendering/updateScriptEffect"; 6 | 7 | 8 | export const ScriptEffectView: FC<{ 9 | scriptEffect: ScriptEffect; 10 | strip: Strip; 11 | }> = (props) => { 12 | const userScript = userScriptMap.get(props.scriptEffect.scriptAssetId); 13 | const Component = userScript?.Component; 14 | 15 | const appCtx = useGetAppContext(); 16 | if (!Component) return
Script not found
; 17 | return ( 18 |
19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/interfaces/plugins/CustomEffect.ts: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { 4 | AppContext, 5 | Effect, 6 | ScriptEffect, 7 | ScriptMeta, 8 | Strip, 9 | } from "@/core/types"; 10 | import { SceneState } from "@/store/scene"; 11 | 12 | export type UpdateHandler = ( 13 | ctx: CanvasRenderingContext2D, 14 | effect: T, 15 | strip: Strip, 16 | scene: SceneState, 17 | appCtx: any 18 | ) => void; 19 | 20 | export type EffectPlugin = { 21 | pkg?: ScriptMeta; 22 | Component?: FC<{ scriptEffect: Effect; strip: Strip; appCtx: AppContext }>; 23 | AssetPanel?: FC<{ appCtx: AppContext }>; 24 | update?: UpdateHandler; 25 | beforeRender?: UpdateHandler; 26 | defaultEffect?: Effect; 27 | }; 28 | -------------------------------------------------------------------------------- /src/store/app.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export type AppState = { 4 | currentPath: string; 5 | readedDataJsonString: string; 6 | }; 7 | 8 | export const sceneSlice = createSlice({ 9 | name: "app", 10 | initialState: { 11 | currentPath: "", 12 | readedDataJsonString: "{}", 13 | } as AppState, 14 | reducers: { 15 | setCurrentPath: (state, action: { payload: string }) => { 16 | state.currentPath = action.payload; 17 | }, 18 | setReadedDataJsonString: (state, action: { payload: string }) => { 19 | state.readedDataJsonString = action.payload; 20 | }, 21 | }, 22 | }); 23 | 24 | export const appAction = sceneSlice.actions; 25 | 26 | export const appReducer = sceneSlice.reducer; 27 | -------------------------------------------------------------------------------- /src/ipc/readFileUserDataDir.ts: -------------------------------------------------------------------------------- 1 | import { READ_FILE_USER_DATA_DIR } from "@/electron-src/ipc/const"; 2 | 3 | export function isElectron() { 4 | return navigator.userAgent.toLowerCase().indexOf(" electron/") > -1; 5 | } 6 | 7 | export function toUserDataLocalPath(path: string) { 8 | return `userData_${path}`; 9 | } 10 | 11 | /** 12 | * Reads a file in the userData directory 13 | * @param path relative path from userData directory e.g. "recent-projects.json" 14 | * @returns file data 15 | */ 16 | export function readFileUserDataDir(path: string): string | false { 17 | if (!isElectron()) { 18 | return localStorage.getItem(toUserDataLocalPath(path)) || false; 19 | } 20 | const res = ipcRenderer.sendSync(READ_FILE_USER_DATA_DIR, path); 21 | return res; 22 | } 23 | -------------------------------------------------------------------------------- /src/core/build.js: -------------------------------------------------------------------------------- 1 | const { build } = require("esbuild"); 2 | const pkg = require("./package.json"); 3 | 4 | // const dependencies = Object.keys(pkg.dependencies ?? {}); 5 | const peerDependencies = Object.keys(pkg.peerDependencies ?? {}); 6 | 7 | const external = [...peerDependencies]; 8 | 9 | const entryFile = "index.ts"; 10 | const shared = { 11 | bundle: true, 12 | entryPoints: [entryFile], 13 | external, 14 | allowOverwrite: true, 15 | logLevel: "info", 16 | minify: true, 17 | sourcemap: false, 18 | }; 19 | 20 | build({ 21 | ...shared, 22 | format: "esm", 23 | outfile: pkg.module, 24 | jsx: "transform", 25 | target: ["ES6"], 26 | }); 27 | 28 | build({ 29 | ...shared, 30 | format: "cjs", 31 | outfile: pkg.main, 32 | target: ["ES6"], 33 | }); 34 | -------------------------------------------------------------------------------- /src/electron-src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": ["dom", "es2017"], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noEmit": false, 13 | "noFallthroughCasesInSwitch": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "target": "esnext", 20 | "outDir": "../../main" 21 | }, 22 | "exclude": ["node_modules"], 23 | "include": ["**/*.ts", "**/*.tsx", "**/*.js"] 24 | } -------------------------------------------------------------------------------- /src/components/strip_panel/KeyframeButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconKey } from "@tabler/icons-react"; 2 | import { FC } from "react"; 3 | 4 | import { IconButton, iconProps, ToolTip } from "@/app-ui/src"; 5 | 6 | export const KeyframeButton: FC<{ 7 | onClick: () => void; 8 | highlight: boolean; 9 | active: boolean; 10 | }> = (props) => { 11 | return ( 12 | 13 | 14 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/timeline_panel/VideoEffectStripUI.tsx: -------------------------------------------------------------------------------- 1 | import { isVideoEffect, Strip } from "@/core/types"; 2 | import { useSelector } from "@/hooks/useSelector"; 3 | 4 | export function VideoEffectStripUI(props: { strip: Strip }) { 5 | const assets = useSelector((state) => state.scene.assets); 6 | const effect = props.strip.effects.find(isVideoEffect); 7 | if (!effect) return null; 8 | const asset = assets.find((a) => a.id === effect.videoAssetId); 9 | if (!asset) return null; 10 | return ( 11 |