Add custom cursor packs and harden native postinstall builds
@@ -1,8 +1,8 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
@@ -29,9 +29,13 @@ release/**
|
||||
.tmp/
|
||||
.history/
|
||||
*.tsbuildinfo
|
||||
vite.config.js
|
||||
vite.config.d.ts
|
||||
|
||||
# Native capture build artifacts
|
||||
electron/native/wgc-capture/build/
|
||||
electron/native/cursor-monitor/build/
|
||||
vite.config.js
|
||||
vite.config.d.ts
|
||||
|
||||
# Native capture build artifacts
|
||||
electron/native/wgc-capture/build/
|
||||
electron/native/cursor-monitor/build/
|
||||
|
||||
# Local debug helpers
|
||||
tmp-*.ps1
|
||||
.tmp-*.ps1
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"postinstall": "npm run rebuild:native && npm run build:platform-native-helpers",
|
||||
"postinstall": "node scripts/postinstall.mjs",
|
||||
"build": "npm run build:platform-native-helpers && tsc && vite build && electron-builder",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --write .",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const projectRoot = process.cwd();
|
||||
@@ -25,29 +25,46 @@ function findCmake() {
|
||||
// not on PATH
|
||||
}
|
||||
|
||||
// VS 2022 bundled CMake
|
||||
const vsEditions = ["Community", "Professional", "Enterprise", "BuildTools"];
|
||||
for (const edition of vsEditions) {
|
||||
const cmakePath = path.join(
|
||||
"C:",
|
||||
"Program Files",
|
||||
"Microsoft Visual Studio",
|
||||
"2022",
|
||||
edition,
|
||||
"Common7",
|
||||
"IDE",
|
||||
"CommonExtensions",
|
||||
"Microsoft",
|
||||
"CMake",
|
||||
"CMake",
|
||||
"bin",
|
||||
"cmake.exe",
|
||||
);
|
||||
const standaloneCmakePaths = [
|
||||
path.join("C:", "Program Files", "CMake", "bin", "cmake.exe"),
|
||||
path.join("C:", "Program Files (x86)", "CMake", "bin", "cmake.exe"),
|
||||
];
|
||||
for (const cmakePath of standaloneCmakePaths) {
|
||||
if (existsSync(cmakePath)) {
|
||||
return `"${cmakePath}"`;
|
||||
}
|
||||
}
|
||||
|
||||
// VS 2022 bundled CMake
|
||||
const vsRoots = [
|
||||
path.join("C:", "Program Files", "Microsoft Visual Studio"),
|
||||
path.join("C:", "Program Files (x86)", "Microsoft Visual Studio"),
|
||||
];
|
||||
const vsEditions = ["Community", "Professional", "Enterprise", "BuildTools"];
|
||||
const vsVersions = ["2022", "2019"];
|
||||
for (const root of vsRoots) {
|
||||
for (const version of vsVersions) {
|
||||
for (const edition of vsEditions) {
|
||||
const cmakePath = path.join(
|
||||
root,
|
||||
version,
|
||||
edition,
|
||||
"Common7",
|
||||
"IDE",
|
||||
"CommonExtensions",
|
||||
"Microsoft",
|
||||
"CMake",
|
||||
"CMake",
|
||||
"bin",
|
||||
"cmake.exe",
|
||||
);
|
||||
if (existsSync(cmakePath)) {
|
||||
return `"${cmakePath}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -60,9 +77,17 @@ if (!cmake) {
|
||||
}
|
||||
|
||||
mkdirSync(buildDir, { recursive: true });
|
||||
const cacheFile = path.join(buildDir, "CMakeCache.txt");
|
||||
const cacheDir = path.join(buildDir, "CMakeFiles");
|
||||
|
||||
function clearCmakeCache() {
|
||||
rmSync(cacheFile, { force: true });
|
||||
rmSync(cacheDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log("[build-cursor-monitor] Configuring CMake...");
|
||||
try {
|
||||
clearCmakeCache();
|
||||
execSync(`${cmake} .. -G "Visual Studio 17 2022" -A x64`, {
|
||||
cwd: buildDir,
|
||||
stdio: "inherit",
|
||||
@@ -71,6 +96,7 @@ try {
|
||||
} catch {
|
||||
console.log("[build-cursor-monitor] VS 2022 generator not found, trying VS 2019...");
|
||||
try {
|
||||
clearCmakeCache();
|
||||
execSync(`${cmake} .. -G "Visual Studio 16 2019" -A x64`, {
|
||||
cwd: buildDir,
|
||||
stdio: "inherit",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import { mkdirSync, existsSync } from 'node:fs';
|
||||
import { mkdirSync, existsSync, rmSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const projectRoot = process.cwd();
|
||||
@@ -25,18 +25,46 @@ function findCmake() {
|
||||
// not on PATH
|
||||
}
|
||||
|
||||
// VS 2022 bundled CMake
|
||||
const vsEditions = ['Community', 'Professional', 'Enterprise', 'BuildTools'];
|
||||
for (const edition of vsEditions) {
|
||||
const cmakePath = path.join(
|
||||
'C:', 'Program Files', 'Microsoft Visual Studio', '2022', edition,
|
||||
'Common7', 'IDE', 'CommonExtensions', 'Microsoft', 'CMake', 'CMake', 'bin', 'cmake.exe'
|
||||
);
|
||||
const standaloneCmakePaths = [
|
||||
path.join('C:', 'Program Files', 'CMake', 'bin', 'cmake.exe'),
|
||||
path.join('C:', 'Program Files (x86)', 'CMake', 'bin', 'cmake.exe'),
|
||||
];
|
||||
for (const cmakePath of standaloneCmakePaths) {
|
||||
if (existsSync(cmakePath)) {
|
||||
return `"${cmakePath}"`;
|
||||
}
|
||||
}
|
||||
|
||||
// VS 2022 bundled CMake
|
||||
const vsRoots = [
|
||||
path.join('C:', 'Program Files', 'Microsoft Visual Studio'),
|
||||
path.join('C:', 'Program Files (x86)', 'Microsoft Visual Studio'),
|
||||
];
|
||||
const vsEditions = ['Community', 'Professional', 'Enterprise', 'BuildTools'];
|
||||
const vsVersions = ['2022', '2019'];
|
||||
for (const root of vsRoots) {
|
||||
for (const version of vsVersions) {
|
||||
for (const edition of vsEditions) {
|
||||
const cmakePath = path.join(
|
||||
root,
|
||||
version,
|
||||
edition,
|
||||
'Common7',
|
||||
'IDE',
|
||||
'CommonExtensions',
|
||||
'Microsoft',
|
||||
'CMake',
|
||||
'CMake',
|
||||
'bin',
|
||||
'cmake.exe'
|
||||
);
|
||||
if (existsSync(cmakePath)) {
|
||||
return `"${cmakePath}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -47,9 +75,17 @@ if (!cmake) {
|
||||
}
|
||||
|
||||
mkdirSync(buildDir, { recursive: true });
|
||||
const cacheFile = path.join(buildDir, 'CMakeCache.txt');
|
||||
const cacheDir = path.join(buildDir, 'CMakeFiles');
|
||||
|
||||
function clearCmakeCache() {
|
||||
rmSync(cacheFile, { force: true });
|
||||
rmSync(cacheDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log('[build-windows-capture] Configuring CMake...');
|
||||
try {
|
||||
clearCmakeCache();
|
||||
execSync(`${cmake} .. -G "Visual Studio 17 2022" -A x64`, {
|
||||
cwd: buildDir,
|
||||
stdio: 'inherit',
|
||||
@@ -58,6 +94,7 @@ try {
|
||||
} catch {
|
||||
console.log('[build-windows-capture] VS 2022 generator not found, trying VS 2019...');
|
||||
try {
|
||||
clearCmakeCache();
|
||||
execSync(`${cmake} .. -G "Visual Studio 16 2019" -A x64`, {
|
||||
cwd: buildDir,
|
||||
stdio: 'inherit',
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const npmExecPath = process.env.npm_execpath;
|
||||
const hasNpmExecPath = typeof npmExecPath === "string" && npmExecPath.length > 0;
|
||||
const npmInvoker = hasNpmExecPath
|
||||
? {
|
||||
command: process.execPath,
|
||||
argsPrefix: [npmExecPath],
|
||||
shell: false,
|
||||
}
|
||||
: {
|
||||
command: process.platform === "win32" ? "npm.cmd" : "npm",
|
||||
argsPrefix: [],
|
||||
shell: process.platform === "win32",
|
||||
};
|
||||
|
||||
function runScript(scriptName) {
|
||||
console.log(`[postinstall] Running npm script: ${scriptName}`);
|
||||
const result = spawnSync(npmInvoker.command, [...npmInvoker.argsPrefix, "run", scriptName], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: npmInvoker.shell,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(
|
||||
`[postinstall] Failed to start "${scriptName}" (${result.error.message}).`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.signal) {
|
||||
console.error(`[postinstall] "${scriptName}" was terminated by signal ${result.signal}.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error(
|
||||
`[postinstall] "${scriptName}" exited with code ${result.status}.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!runScript("rebuild:native")) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!runScript("build:platform-native-helpers")) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -18,7 +18,12 @@ import type { BuiltInWallpaper } from "@/lib/wallpapers";
|
||||
import { BUILT_IN_WALLPAPERS, getAvailableWallpapers } from "@/lib/wallpapers";
|
||||
import { type AspectRatio } from "@/utils/aspectRatioUtils";
|
||||
import minimalCursorUrl from "../../../Minimal Cursor.svg";
|
||||
import amongusCursorUrl from "../../assets/cursors/amongus/default.png";
|
||||
import tahoeCursorUrl from "../../assets/cursors/Cursor=Default.svg";
|
||||
import chooperCursorUrl from "../../assets/cursors/chooper/default.png";
|
||||
import lavenderCursorUrl from "../../assets/cursors/lavender/default.png";
|
||||
import parchedCursorUrl from "../../assets/cursors/parched/default.png";
|
||||
import turtleCursorUrl from "../../assets/cursors/turtle/default.png";
|
||||
import { useI18n, useScopedT } from "../../contexts/I18nContext";
|
||||
import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel";
|
||||
import { loadEditorPreferences, saveEditorPreferences } from "./editorPreferences";
|
||||
@@ -255,6 +260,11 @@ const CURSOR_STYLE_OPTIONS: Array<{ value: CursorStyle; label: string }> = [
|
||||
{ value: "dot", label: "Dot" },
|
||||
{ value: "figma", label: "Minimal" },
|
||||
{ value: "mono", label: "Inverted" },
|
||||
{ value: "lavender", label: "Lavender" },
|
||||
{ value: "parched", label: "Parched" },
|
||||
{ value: "chooper", label: "Chooper" },
|
||||
{ value: "amongus", label: "Among Us" },
|
||||
{ value: "turtle", label: "Turtle" },
|
||||
];
|
||||
|
||||
const CAPTION_LANGUAGE_OPTIONS = [
|
||||
@@ -442,6 +452,36 @@ function CursorStylePreview({
|
||||
);
|
||||
}
|
||||
|
||||
if (style === "lavender") {
|
||||
return (
|
||||
<img src={lavenderCursorUrl} alt="" className="h-7 w-7 object-contain" draggable={false} />
|
||||
);
|
||||
}
|
||||
|
||||
if (style === "parched") {
|
||||
return (
|
||||
<img src={parchedCursorUrl} alt="" className="h-7 w-7 object-contain" draggable={false} />
|
||||
);
|
||||
}
|
||||
|
||||
if (style === "chooper") {
|
||||
return (
|
||||
<img src={chooperCursorUrl} alt="" className="h-7 w-7 object-contain" draggable={false} />
|
||||
);
|
||||
}
|
||||
|
||||
if (style === "amongus") {
|
||||
return (
|
||||
<img src={amongusCursorUrl} alt="" className="h-7 w-7 object-contain" draggable={false} />
|
||||
);
|
||||
}
|
||||
|
||||
if (style === "turtle") {
|
||||
return (
|
||||
<img src={turtleCursorUrl} alt="" className="h-7 w-7 object-contain" draggable={false} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={previewUrls.mono ?? tahoeCursorUrl}
|
||||
@@ -1620,6 +1660,8 @@ export function SettingsPanel({
|
||||
<ToggleGroupItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
title={option.label}
|
||||
aria-label={option.label}
|
||||
className={cn(
|
||||
"group aspect-square h-auto min-w-0 rounded-[10px] border border-white/10 bg-white/[0.03] p-3 text-left text-slate-200 shadow-none transition-all hover:border-white/20 hover:bg-white/[0.06]",
|
||||
"data-[state=on]:border-[#2563EB]/70 data-[state=on]:bg-[#2563EB]/12 data-[state=on]:text-white",
|
||||
|
||||
@@ -2,30 +2,31 @@ import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@
|
||||
import { DEFAULT_WALLPAPER_PATH } from "@/lib/wallpapers";
|
||||
import { ASPECT_RATIOS, type AspectRatio, isCustomAspectRatio } from "@/utils/aspectRatioUtils";
|
||||
import {
|
||||
type AnnotationRegion,
|
||||
type AudioRegion,
|
||||
type AutoCaptionAnimation,
|
||||
type AutoCaptionSettings,
|
||||
type CaptionCue,
|
||||
type CaptionCueWord,
|
||||
type AnnotationRegion,
|
||||
type AudioRegion,
|
||||
type CropRegion,
|
||||
type CursorStyle,
|
||||
DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
getDefaultCaptionFontFamily,
|
||||
DEFAULT_ANNOTATION_POSITION,
|
||||
DEFAULT_ANNOTATION_SIZE,
|
||||
DEFAULT_ANNOTATION_STYLE,
|
||||
DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
DEFAULT_CONNECTED_ZOOM_DURATION_MS,
|
||||
DEFAULT_CONNECTED_ZOOM_EASING,
|
||||
DEFAULT_CONNECTED_ZOOM_GAP_MS,
|
||||
DEFAULT_CROP_REGION,
|
||||
DEFAULT_CURSOR_CLICK_BOUNCE,
|
||||
DEFAULT_CURSOR_CLICK_BOUNCE_DURATION,
|
||||
DEFAULT_CURSOR_MOTION_BLUR,
|
||||
DEFAULT_CURSOR_SIZE,
|
||||
DEFAULT_CURSOR_STYLE,
|
||||
DEFAULT_CURSOR_SMOOTHING,
|
||||
DEFAULT_CURSOR_STYLE,
|
||||
DEFAULT_CURSOR_SWAY,
|
||||
DEFAULT_CONNECTED_ZOOM_DURATION_MS,
|
||||
DEFAULT_CONNECTED_ZOOM_EASING,
|
||||
DEFAULT_CONNECTED_ZOOM_GAP_MS,
|
||||
DEFAULT_FIGURE_DATA,
|
||||
DEFAULT_PLAYBACK_SPEED,
|
||||
DEFAULT_WEBCAM_CORNER_RADIUS,
|
||||
DEFAULT_WEBCAM_MARGIN,
|
||||
DEFAULT_WEBCAM_OVERLAY,
|
||||
@@ -36,8 +37,6 @@ import {
|
||||
DEFAULT_WEBCAM_SHADOW,
|
||||
DEFAULT_WEBCAM_SIZE,
|
||||
DEFAULT_WEBCAM_TIME_OFFSET_MS,
|
||||
DEFAULT_FIGURE_DATA,
|
||||
DEFAULT_PLAYBACK_SPEED,
|
||||
DEFAULT_ZOOM_DEPTH,
|
||||
DEFAULT_ZOOM_IN_DURATION_MS,
|
||||
DEFAULT_ZOOM_IN_EASING,
|
||||
@@ -45,11 +44,12 @@ import {
|
||||
DEFAULT_ZOOM_MOTION_BLUR,
|
||||
DEFAULT_ZOOM_OUT_DURATION_MS,
|
||||
DEFAULT_ZOOM_OUT_EASING,
|
||||
getDefaultCaptionFontFamily,
|
||||
type SpeedRegion,
|
||||
type TrimRegion,
|
||||
type WebcamOverlaySettings,
|
||||
type ZoomTransitionEasing,
|
||||
type ZoomRegion,
|
||||
type ZoomTransitionEasing,
|
||||
} from "./types";
|
||||
|
||||
export const PROJECT_VERSION = 1;
|
||||
@@ -420,12 +420,16 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
const endMs = Math.max(startMs + 1, rawEnd);
|
||||
const words: CaptionCueWord[] | undefined = Array.isArray(cue.words)
|
||||
? cue.words
|
||||
.filter(
|
||||
(word): word is CaptionCueWord => Boolean(word && typeof word.text === "string"),
|
||||
.filter((word): word is CaptionCueWord =>
|
||||
Boolean(word && typeof word.text === "string"),
|
||||
)
|
||||
.map((word) => {
|
||||
const rawWordStart = isFiniteNumber(word.startMs) ? Math.round(word.startMs) : startMs;
|
||||
const rawWordEnd = isFiniteNumber(word.endMs) ? Math.round(word.endMs) : rawWordStart + 1;
|
||||
const rawWordStart = isFiniteNumber(word.startMs)
|
||||
? Math.round(word.startMs)
|
||||
: startMs;
|
||||
const rawWordEnd = isFiniteNumber(word.endMs)
|
||||
? Math.round(word.endMs)
|
||||
: rawWordStart + 1;
|
||||
const normalizedWordStart = clamp(rawWordStart, startMs, endMs - 1);
|
||||
const normalizedWordEnd = clamp(rawWordEnd, normalizedWordStart + 1, endMs);
|
||||
|
||||
@@ -463,8 +467,7 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
typeof rawAutoCaptionSettings.language === "string" && rawAutoCaptionSettings.language.trim()
|
||||
? rawAutoCaptionSettings.language.trim()
|
||||
: DEFAULT_AUTO_CAPTION_SETTINGS.language,
|
||||
fontFamily:
|
||||
getDefaultCaptionFontFamily(),
|
||||
fontFamily: getDefaultCaptionFontFamily(),
|
||||
fontSize: isFiniteNumber(rawAutoCaptionSettings.fontSize)
|
||||
? clamp(rawAutoCaptionSettings.fontSize, 16, 72)
|
||||
: DEFAULT_AUTO_CAPTION_SETTINGS.fontSize,
|
||||
@@ -485,11 +488,13 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
? clamp(rawAutoCaptionSettings.boxRadius, 0, 40)
|
||||
: DEFAULT_AUTO_CAPTION_SETTINGS.boxRadius,
|
||||
textColor:
|
||||
typeof rawAutoCaptionSettings.textColor === "string" && rawAutoCaptionSettings.textColor.trim()
|
||||
typeof rawAutoCaptionSettings.textColor === "string" &&
|
||||
rawAutoCaptionSettings.textColor.trim()
|
||||
? rawAutoCaptionSettings.textColor
|
||||
: DEFAULT_AUTO_CAPTION_SETTINGS.textColor,
|
||||
inactiveTextColor:
|
||||
typeof rawAutoCaptionSettings.inactiveTextColor === "string" && rawAutoCaptionSettings.inactiveTextColor.trim()
|
||||
typeof rawAutoCaptionSettings.inactiveTextColor === "string" &&
|
||||
rawAutoCaptionSettings.inactiveTextColor.trim()
|
||||
? rawAutoCaptionSettings.inactiveTextColor
|
||||
: DEFAULT_AUTO_CAPTION_SETTINGS.inactiveTextColor,
|
||||
backgroundOpacity: isFiniteNumber(rawAutoCaptionSettings.backgroundOpacity)
|
||||
@@ -518,10 +523,11 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
const webcam: Partial<WebcamOverlaySettings> =
|
||||
editor.webcam && typeof editor.webcam === "object" ? editor.webcam : {};
|
||||
const webcamSourcePath = typeof webcam.sourcePath === "string" ? webcam.sourcePath : null;
|
||||
const legacyZoomScaleEffect =
|
||||
isFiniteNumber((webcam as Partial<{ zoomScaleEffect: number }>).zoomScaleEffect)
|
||||
? (webcam as Partial<{ zoomScaleEffect: number }>).zoomScaleEffect
|
||||
: null;
|
||||
const legacyZoomScaleEffect = isFiniteNumber(
|
||||
(webcam as Partial<{ zoomScaleEffect: number }>).zoomScaleEffect,
|
||||
)
|
||||
? (webcam as Partial<{ zoomScaleEffect: number }>).zoomScaleEffect
|
||||
: null;
|
||||
|
||||
return {
|
||||
wallpaper: typeof editor.wallpaper === "string" ? editor.wallpaper : DEFAULT_WALLPAPER_PATH,
|
||||
@@ -546,7 +552,12 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
editor.cursorStyle === "dot" ||
|
||||
editor.cursorStyle === "figma" ||
|
||||
editor.cursorStyle === "mono" ||
|
||||
editor.cursorStyle === "tahoe"
|
||||
editor.cursorStyle === "tahoe" ||
|
||||
editor.cursorStyle === "lavender" ||
|
||||
editor.cursorStyle === "parched" ||
|
||||
editor.cursorStyle === "chooper" ||
|
||||
editor.cursorStyle === "amongus" ||
|
||||
editor.cursorStyle === "turtle"
|
||||
? editor.cursorStyle
|
||||
: DEFAULT_CURSOR_STYLE,
|
||||
cursorSize: isFiniteNumber(editor.cursorSize)
|
||||
@@ -561,7 +572,9 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
cursorClickBounce: isFiniteNumber((editor as Partial<ProjectEditorState>).cursorClickBounce)
|
||||
? clamp((editor as Partial<ProjectEditorState>).cursorClickBounce as number, 0, 5)
|
||||
: DEFAULT_CURSOR_CLICK_BOUNCE,
|
||||
cursorClickBounceDuration: isFiniteNumber((editor as Partial<ProjectEditorState>).cursorClickBounceDuration)
|
||||
cursorClickBounceDuration: isFiniteNumber(
|
||||
(editor as Partial<ProjectEditorState>).cursorClickBounceDuration,
|
||||
)
|
||||
? clamp((editor as Partial<ProjectEditorState>).cursorClickBounceDuration as number, 60, 500)
|
||||
: DEFAULT_CURSOR_CLICK_BOUNCE_DURATION,
|
||||
cursorSway: isFiniteNumber((editor as Partial<ProjectEditorState>).cursorSway)
|
||||
@@ -603,11 +616,11 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
webcam.positionPreset === "custom"
|
||||
? webcam.positionPreset
|
||||
: webcam.corner === "top-left" ||
|
||||
webcam.corner === "top-right" ||
|
||||
webcam.corner === "bottom-left" ||
|
||||
webcam.corner === "bottom-right"
|
||||
? webcam.corner
|
||||
: DEFAULT_WEBCAM_POSITION_PRESET,
|
||||
webcam.corner === "top-right" ||
|
||||
webcam.corner === "bottom-left" ||
|
||||
webcam.corner === "bottom-right"
|
||||
? webcam.corner
|
||||
: DEFAULT_WEBCAM_POSITION_PRESET,
|
||||
positionX: isFiniteNumber(webcam.positionX)
|
||||
? clamp(webcam.positionX, 0, 1)
|
||||
: DEFAULT_WEBCAM_POSITION_X,
|
||||
|
||||
@@ -1,85 +1,83 @@
|
||||
export type ZoomDepth = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
export interface ZoomFocus {
|
||||
cx: number; // normalized horizontal center (0-1)
|
||||
cy: number; // normalized vertical center (0-1)
|
||||
cx: number; // normalized horizontal center (0-1)
|
||||
cy: number; // normalized vertical center (0-1)
|
||||
}
|
||||
|
||||
export interface ZoomRegion {
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
depth: ZoomDepth;
|
||||
focus: ZoomFocus;
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
depth: ZoomDepth;
|
||||
focus: ZoomFocus;
|
||||
}
|
||||
|
||||
export interface CursorTelemetryPoint {
|
||||
timeMs: number;
|
||||
cx: number;
|
||||
cy: number;
|
||||
interactionType?:
|
||||
| "move"
|
||||
| "click"
|
||||
| "double-click"
|
||||
| "right-click"
|
||||
| "middle-click"
|
||||
| "mouseup";
|
||||
cursorType?:
|
||||
| "arrow"
|
||||
| "text"
|
||||
| "pointer"
|
||||
| "crosshair"
|
||||
| "open-hand"
|
||||
| "closed-hand"
|
||||
| "resize-ew"
|
||||
| "resize-ns"
|
||||
| "not-allowed";
|
||||
timeMs: number;
|
||||
cx: number;
|
||||
cy: number;
|
||||
interactionType?: "move" | "click" | "double-click" | "right-click" | "middle-click" | "mouseup";
|
||||
cursorType?:
|
||||
| "arrow"
|
||||
| "text"
|
||||
| "pointer"
|
||||
| "crosshair"
|
||||
| "open-hand"
|
||||
| "closed-hand"
|
||||
| "resize-ew"
|
||||
| "resize-ns"
|
||||
| "not-allowed";
|
||||
}
|
||||
|
||||
export interface CursorVisualSettings {
|
||||
size: number;
|
||||
smoothing: number;
|
||||
motionBlur: number;
|
||||
clickBounce: number;
|
||||
clickBounceDuration: number;
|
||||
sway: number;
|
||||
style: CursorStyle;
|
||||
size: number;
|
||||
smoothing: number;
|
||||
motionBlur: number;
|
||||
clickBounce: number;
|
||||
clickBounceDuration: number;
|
||||
sway: number;
|
||||
style: CursorStyle;
|
||||
}
|
||||
|
||||
export type CursorStyle = "tahoe" | "dot" | "figma" | "mono";
|
||||
export type CursorStyle =
|
||||
| "tahoe"
|
||||
| "dot"
|
||||
| "figma"
|
||||
| "mono"
|
||||
| "lavender"
|
||||
| "parched"
|
||||
| "chooper"
|
||||
| "amongus"
|
||||
| "turtle";
|
||||
export const DEFAULT_CURSOR_STYLE: CursorStyle = "tahoe";
|
||||
|
||||
export type ZoomTransitionEasing =
|
||||
| "recordly"
|
||||
| "glide"
|
||||
| "smooth"
|
||||
| "snappy"
|
||||
| "linear";
|
||||
export type ZoomTransitionEasing = "recordly" | "glide" | "smooth" | "snappy" | "linear";
|
||||
|
||||
export type WebcamCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
||||
export type WebcamPositionPreset =
|
||||
| WebcamCorner
|
||||
| "top-center"
|
||||
| "center-left"
|
||||
| "center"
|
||||
| "center-right"
|
||||
| "bottom-center"
|
||||
| "custom";
|
||||
| WebcamCorner
|
||||
| "top-center"
|
||||
| "center-left"
|
||||
| "center"
|
||||
| "center-right"
|
||||
| "bottom-center"
|
||||
| "custom";
|
||||
|
||||
export interface WebcamOverlaySettings {
|
||||
enabled: boolean;
|
||||
sourcePath: string | null;
|
||||
timeOffsetMs: number;
|
||||
mirror: boolean;
|
||||
corner: WebcamCorner;
|
||||
positionPreset: WebcamPositionPreset;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
size: number;
|
||||
reactToZoom: boolean;
|
||||
cornerRadius: number;
|
||||
shadow: number;
|
||||
margin: number;
|
||||
enabled: boolean;
|
||||
sourcePath: string | null;
|
||||
timeOffsetMs: number;
|
||||
mirror: boolean;
|
||||
corner: WebcamCorner;
|
||||
positionPreset: WebcamPositionPreset;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
size: number;
|
||||
reactToZoom: boolean;
|
||||
cornerRadius: number;
|
||||
shadow: number;
|
||||
margin: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_CURSOR_SIZE = 3.0;
|
||||
@@ -108,236 +106,233 @@ export const DEFAULT_WEBCAM_POSITION_Y = 1;
|
||||
export const DEFAULT_WEBCAM_TIME_OFFSET_MS = 0;
|
||||
|
||||
export const DEFAULT_WEBCAM_OVERLAY: WebcamOverlaySettings = {
|
||||
enabled: false,
|
||||
sourcePath: null,
|
||||
timeOffsetMs: DEFAULT_WEBCAM_TIME_OFFSET_MS,
|
||||
mirror: true,
|
||||
corner: "bottom-right",
|
||||
positionPreset: DEFAULT_WEBCAM_POSITION_PRESET,
|
||||
positionX: DEFAULT_WEBCAM_POSITION_X,
|
||||
positionY: DEFAULT_WEBCAM_POSITION_Y,
|
||||
size: DEFAULT_WEBCAM_SIZE,
|
||||
reactToZoom: DEFAULT_WEBCAM_REACT_TO_ZOOM,
|
||||
cornerRadius: DEFAULT_WEBCAM_CORNER_RADIUS,
|
||||
shadow: DEFAULT_WEBCAM_SHADOW,
|
||||
margin: DEFAULT_WEBCAM_MARGIN,
|
||||
enabled: false,
|
||||
sourcePath: null,
|
||||
timeOffsetMs: DEFAULT_WEBCAM_TIME_OFFSET_MS,
|
||||
mirror: true,
|
||||
corner: "bottom-right",
|
||||
positionPreset: DEFAULT_WEBCAM_POSITION_PRESET,
|
||||
positionX: DEFAULT_WEBCAM_POSITION_X,
|
||||
positionY: DEFAULT_WEBCAM_POSITION_Y,
|
||||
size: DEFAULT_WEBCAM_SIZE,
|
||||
reactToZoom: DEFAULT_WEBCAM_REACT_TO_ZOOM,
|
||||
cornerRadius: DEFAULT_WEBCAM_CORNER_RADIUS,
|
||||
shadow: DEFAULT_WEBCAM_SHADOW,
|
||||
margin: DEFAULT_WEBCAM_MARGIN,
|
||||
};
|
||||
|
||||
export interface TrimRegion {
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
}
|
||||
|
||||
export type AnnotationType = "text" | "image" | "figure";
|
||||
|
||||
export type ArrowDirection =
|
||||
| "up"
|
||||
| "down"
|
||||
| "left"
|
||||
| "right"
|
||||
| "up-right"
|
||||
| "up-left"
|
||||
| "down-right"
|
||||
| "down-left";
|
||||
| "up"
|
||||
| "down"
|
||||
| "left"
|
||||
| "right"
|
||||
| "up-right"
|
||||
| "up-left"
|
||||
| "down-right"
|
||||
| "down-left";
|
||||
|
||||
export interface FigureData {
|
||||
arrowDirection: ArrowDirection;
|
||||
color: string;
|
||||
strokeWidth: number;
|
||||
arrowDirection: ArrowDirection;
|
||||
color: string;
|
||||
strokeWidth: number;
|
||||
}
|
||||
|
||||
export interface AnnotationPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface AnnotationSize {
|
||||
width: number;
|
||||
height: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface AnnotationTextStyle {
|
||||
color: string;
|
||||
backgroundColor: string;
|
||||
fontSize: number; // pixels
|
||||
fontFamily: string;
|
||||
fontWeight: "normal" | "bold";
|
||||
fontStyle: "normal" | "italic";
|
||||
textDecoration: "none" | "underline";
|
||||
textAlign: "left" | "center" | "right";
|
||||
color: string;
|
||||
backgroundColor: string;
|
||||
fontSize: number; // pixels
|
||||
fontFamily: string;
|
||||
fontWeight: "normal" | "bold";
|
||||
fontStyle: "normal" | "italic";
|
||||
textDecoration: "none" | "underline";
|
||||
textAlign: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
function getDefaultAnnotationFontFamily() {
|
||||
if (typeof navigator !== "undefined" && /mac/i.test(navigator.platform)) {
|
||||
return '"SF Pro Display", "SF Pro Text", -apple-system, BlinkMacSystemFont, sans-serif';
|
||||
}
|
||||
if (typeof navigator !== "undefined" && /mac/i.test(navigator.platform)) {
|
||||
return '"SF Pro Display", "SF Pro Text", -apple-system, BlinkMacSystemFont, sans-serif';
|
||||
}
|
||||
|
||||
return "Inter, system-ui, sans-serif";
|
||||
return "Inter, system-ui, sans-serif";
|
||||
}
|
||||
|
||||
export function getDefaultCaptionFontFamily() {
|
||||
if (typeof navigator !== "undefined" && /mac/i.test(navigator.platform)) {
|
||||
return '"SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif';
|
||||
}
|
||||
if (typeof navigator !== "undefined" && /mac/i.test(navigator.platform)) {
|
||||
return '"SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif';
|
||||
}
|
||||
|
||||
return '"Helvetica Neue", Helvetica, Arial, sans-serif';
|
||||
return '"Helvetica Neue", Helvetica, Arial, sans-serif';
|
||||
}
|
||||
|
||||
export interface AnnotationRegion {
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
type: AnnotationType;
|
||||
content: string; // Legacy - still used for current type
|
||||
textContent?: string; // Separate storage for text
|
||||
imageContent?: string; // Separate storage for image data URL
|
||||
position: AnnotationPosition;
|
||||
size: AnnotationSize;
|
||||
style: AnnotationTextStyle;
|
||||
zIndex: number;
|
||||
figureData?: FigureData;
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
type: AnnotationType;
|
||||
content: string; // Legacy - still used for current type
|
||||
textContent?: string; // Separate storage for text
|
||||
imageContent?: string; // Separate storage for image data URL
|
||||
position: AnnotationPosition;
|
||||
size: AnnotationSize;
|
||||
style: AnnotationTextStyle;
|
||||
zIndex: number;
|
||||
figureData?: FigureData;
|
||||
}
|
||||
|
||||
export const DEFAULT_ANNOTATION_POSITION: AnnotationPosition = {
|
||||
x: 50,
|
||||
y: 50,
|
||||
x: 50,
|
||||
y: 50,
|
||||
};
|
||||
|
||||
export const DEFAULT_ANNOTATION_SIZE: AnnotationSize = {
|
||||
width: 30,
|
||||
height: 20,
|
||||
width: 30,
|
||||
height: 20,
|
||||
};
|
||||
|
||||
export const DEFAULT_ANNOTATION_STYLE: AnnotationTextStyle = {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "transparent",
|
||||
fontSize: 32,
|
||||
fontFamily: getDefaultAnnotationFontFamily(),
|
||||
fontWeight: "bold",
|
||||
fontStyle: "normal",
|
||||
textDecoration: "none",
|
||||
textAlign: "center",
|
||||
color: "#ffffff",
|
||||
backgroundColor: "transparent",
|
||||
fontSize: 32,
|
||||
fontFamily: getDefaultAnnotationFontFamily(),
|
||||
fontWeight: "bold",
|
||||
fontStyle: "normal",
|
||||
textDecoration: "none",
|
||||
textAlign: "center",
|
||||
};
|
||||
|
||||
export const DEFAULT_FIGURE_DATA: FigureData = {
|
||||
arrowDirection: "right",
|
||||
color: "#2563EB",
|
||||
strokeWidth: 4,
|
||||
arrowDirection: "right",
|
||||
color: "#2563EB",
|
||||
strokeWidth: 4,
|
||||
};
|
||||
|
||||
export interface CropRegion {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_CROP_REGION: CropRegion = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
export interface AudioRegion {
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
audioPath: string;
|
||||
volume: number;
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
audioPath: string;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export interface CaptionCue {
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
words?: CaptionCueWord[];
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
words?: CaptionCueWord[];
|
||||
}
|
||||
|
||||
export interface CaptionCueWord {
|
||||
text: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
leadingSpace?: boolean;
|
||||
text: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
leadingSpace?: boolean;
|
||||
}
|
||||
|
||||
export type AutoCaptionAnimation = "none" | "fade" | "rise" | "pop";
|
||||
|
||||
export interface AutoCaptionSettings {
|
||||
enabled: boolean;
|
||||
language: string;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
bottomOffset: number;
|
||||
maxWidth: number;
|
||||
maxRows: number;
|
||||
animationStyle: AutoCaptionAnimation;
|
||||
boxRadius: number;
|
||||
textColor: string;
|
||||
inactiveTextColor: string;
|
||||
backgroundOpacity: number;
|
||||
enabled: boolean;
|
||||
language: string;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
bottomOffset: number;
|
||||
maxWidth: number;
|
||||
maxRows: number;
|
||||
animationStyle: AutoCaptionAnimation;
|
||||
boxRadius: number;
|
||||
textColor: string;
|
||||
inactiveTextColor: string;
|
||||
backgroundOpacity: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_AUTO_CAPTION_SETTINGS: AutoCaptionSettings = {
|
||||
enabled: false,
|
||||
language: "auto",
|
||||
fontFamily: getDefaultCaptionFontFamily(),
|
||||
fontSize: 30,
|
||||
bottomOffset: 3,
|
||||
maxWidth: 62,
|
||||
maxRows: 1,
|
||||
animationStyle: "fade",
|
||||
boxRadius: 17.5,
|
||||
textColor: "#FFFFFF",
|
||||
inactiveTextColor: "#A3A3A3",
|
||||
backgroundOpacity: 0.9,
|
||||
enabled: false,
|
||||
language: "auto",
|
||||
fontFamily: getDefaultCaptionFontFamily(),
|
||||
fontSize: 30,
|
||||
bottomOffset: 3,
|
||||
maxWidth: 62,
|
||||
maxRows: 1,
|
||||
animationStyle: "fade",
|
||||
boxRadius: 17.5,
|
||||
textColor: "#FFFFFF",
|
||||
inactiveTextColor: "#A3A3A3",
|
||||
backgroundOpacity: 0.9,
|
||||
};
|
||||
|
||||
export type PlaybackSpeed = 0.25 | 0.5 | 0.75 | 1.25 | 1.5 | 1.75 | 2;
|
||||
|
||||
export interface SpeedRegion {
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
speed: PlaybackSpeed;
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
speed: PlaybackSpeed;
|
||||
}
|
||||
|
||||
export const SPEED_OPTIONS: Array<{ speed: PlaybackSpeed; label: string }> = [
|
||||
{ speed: 0.25, label: "0.25×" },
|
||||
{ speed: 0.5, label: "0.5×" },
|
||||
{ speed: 0.75, label: "0.75×" },
|
||||
{ speed: 1.25, label: "1.25×" },
|
||||
{ speed: 1.5, label: "1.5×" },
|
||||
{ speed: 1.75, label: "1.75×" },
|
||||
{ speed: 2, label: "2×" },
|
||||
{ speed: 0.25, label: "0.25×" },
|
||||
{ speed: 0.5, label: "0.5×" },
|
||||
{ speed: 0.75, label: "0.75×" },
|
||||
{ speed: 1.25, label: "1.25×" },
|
||||
{ speed: 1.5, label: "1.5×" },
|
||||
{ speed: 1.75, label: "1.75×" },
|
||||
{ speed: 2, label: "2×" },
|
||||
];
|
||||
|
||||
export const DEFAULT_PLAYBACK_SPEED: PlaybackSpeed = 1.5;
|
||||
|
||||
export const ZOOM_DEPTH_SCALES: Record<ZoomDepth, number> = {
|
||||
1: 1.25,
|
||||
2: 1.5,
|
||||
3: 1.8,
|
||||
4: 2.2,
|
||||
5: 3.5,
|
||||
6: 5.0,
|
||||
1: 1.25,
|
||||
2: 1.5,
|
||||
3: 1.8,
|
||||
4: 2.2,
|
||||
5: 3.5,
|
||||
6: 5.0,
|
||||
};
|
||||
|
||||
export const DEFAULT_ZOOM_DEPTH: ZoomDepth = 3;
|
||||
|
||||
export function clampFocusToDepth(
|
||||
focus: ZoomFocus,
|
||||
_depth: ZoomDepth,
|
||||
): ZoomFocus {
|
||||
return {
|
||||
cx: clamp(focus.cx, 0, 1),
|
||||
cy: clamp(focus.cy, 0, 1),
|
||||
};
|
||||
export function clampFocusToDepth(focus: ZoomFocus, _depth: ZoomDepth): ZoomFocus {
|
||||
return {
|
||||
cx: clamp(focus.cx, 0, 1),
|
||||
cy: clamp(focus.cy, 0, 1),
|
||||
};
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
if (Number.isNaN(value)) return (min + max) / 2;
|
||||
return Math.min(max, Math.max(min, value));
|
||||
if (Number.isNaN(value)) return (min + max) / 2;
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Assets, BlurFilter, Container, Graphics, Sprite, Texture } from "pixi.js";
|
||||
import { MotionBlurFilter } from "pixi-filters/motion-blur";
|
||||
import minimalCursorUrl from "../../../../Minimal Cursor.svg";
|
||||
import amongusDefaultCursorUrl from "../../../assets/cursors/amongus/default.png";
|
||||
import amongusPointerCursorUrl from "../../../assets/cursors/amongus/pointer.png";
|
||||
import chooperDefaultCursorUrl from "../../../assets/cursors/chooper/default.png";
|
||||
import chooperPointerCursorUrl from "../../../assets/cursors/chooper/pointer.png";
|
||||
import lavenderDefaultCursorUrl from "../../../assets/cursors/lavender/default.png";
|
||||
import lavenderPointerCursorUrl from "../../../assets/cursors/lavender/pointer.png";
|
||||
import parchedDefaultCursorUrl from "../../../assets/cursors/parched/default.png";
|
||||
import parchedPointerCursorUrl from "../../../assets/cursors/parched/pointer.png";
|
||||
import turtleDefaultCursorUrl from "../../../assets/cursors/turtle/default.png";
|
||||
import turtlePointerCursorUrl from "../../../assets/cursors/turtle/pointer.png";
|
||||
import {
|
||||
type CursorStyle,
|
||||
type CursorTelemetryPoint,
|
||||
@@ -18,6 +28,10 @@ import {
|
||||
import { UPLOADED_CURSOR_SAMPLE_SIZE, uploadedCursorAssets } from "./uploadedCursorAssets";
|
||||
|
||||
type CursorAssetKey = NonNullable<CursorTelemetryPoint["cursorType"]>;
|
||||
type StatefulCursorStyle = Extract<CursorStyle, "tahoe" | "mono">;
|
||||
type SingleCursorStyle = Extract<CursorStyle, "dot" | "figma">;
|
||||
type CursorPackStyle = Exclude<CursorStyle, StatefulCursorStyle | SingleCursorStyle>;
|
||||
type CursorPackVariant = "default" | "pointer";
|
||||
|
||||
type LoadedCursorAsset = {
|
||||
texture: Texture;
|
||||
@@ -27,6 +41,15 @@ type LoadedCursorAsset = {
|
||||
anchorY: number;
|
||||
};
|
||||
|
||||
type LoadedCursorPackAssets = Record<CursorPackVariant, LoadedCursorAsset>;
|
||||
|
||||
type CursorPackSource = {
|
||||
defaultUrl: string;
|
||||
pointerUrl: string;
|
||||
defaultAnchor: { x: number; y: number };
|
||||
pointerAnchor: { x: number; y: number };
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration for cursor rendering.
|
||||
*/
|
||||
@@ -84,7 +107,9 @@ const CURSOR_SHADOW_PADDING = 12;
|
||||
let cursorAssetsPromise: Promise<void> | null = null;
|
||||
let loadedCursorAssets: Partial<Record<CursorAssetKey, LoadedCursorAsset>> = {};
|
||||
let loadedInvertedCursorAssets: Partial<Record<CursorAssetKey, LoadedCursorAsset>> = {};
|
||||
let loadedCursorStyleAssets: Partial<Record<Exclude<CursorStyle, "tahoe">, LoadedCursorAsset>> = {};
|
||||
let loadedCursorStyleAssets: Partial<Record<SingleCursorStyle, LoadedCursorAsset>> = {};
|
||||
let loadedCursorPackAssets: Partial<Record<CursorPackStyle, LoadedCursorPackAssets>> = {};
|
||||
const warnedMissingCursorPackStyles = new Set<CursorPackStyle>();
|
||||
const SUPPORTED_CURSOR_KEYS: CursorAssetKey[] = [
|
||||
"arrow",
|
||||
"text",
|
||||
@@ -97,26 +122,56 @@ const SUPPORTED_CURSOR_KEYS: CursorAssetKey[] = [
|
||||
"not-allowed",
|
||||
];
|
||||
|
||||
const CUSTOM_CURSOR_ARROW_WIDTH = 150;
|
||||
const CUSTOM_CURSOR_ARROW_HEIGHT = 214;
|
||||
const CUSTOM_CURSOR_ARROW_TIP_X = 14;
|
||||
const CUSTOM_CURSOR_ARROW_TIP_Y = 12;
|
||||
const DEFAULT_CURSOR_PACK_ANCHOR = { x: 0.08, y: 0.08 } as const;
|
||||
const POINTER_CURSOR_PACK_ANCHOR = { x: 0.48, y: 0.1 } as const;
|
||||
const CENTERED_CURSOR_PACK_ANCHOR = { x: 0.5, y: 0.5 } as const;
|
||||
const CURSOR_PACK_POINTER_TYPES = new Set<CursorAssetKey>(["pointer", "open-hand", "closed-hand"]);
|
||||
const CURSOR_PACK_SOURCES: Record<CursorPackStyle, CursorPackSource> = {
|
||||
lavender: {
|
||||
defaultUrl: lavenderDefaultCursorUrl,
|
||||
pointerUrl: lavenderPointerCursorUrl,
|
||||
defaultAnchor: DEFAULT_CURSOR_PACK_ANCHOR,
|
||||
pointerAnchor: POINTER_CURSOR_PACK_ANCHOR,
|
||||
},
|
||||
parched: {
|
||||
defaultUrl: parchedDefaultCursorUrl,
|
||||
pointerUrl: parchedPointerCursorUrl,
|
||||
defaultAnchor: DEFAULT_CURSOR_PACK_ANCHOR,
|
||||
pointerAnchor: POINTER_CURSOR_PACK_ANCHOR,
|
||||
},
|
||||
chooper: {
|
||||
defaultUrl: chooperDefaultCursorUrl,
|
||||
pointerUrl: chooperPointerCursorUrl,
|
||||
defaultAnchor: DEFAULT_CURSOR_PACK_ANCHOR,
|
||||
pointerAnchor: POINTER_CURSOR_PACK_ANCHOR,
|
||||
},
|
||||
amongus: {
|
||||
defaultUrl: amongusDefaultCursorUrl,
|
||||
pointerUrl: amongusPointerCursorUrl,
|
||||
defaultAnchor: CENTERED_CURSOR_PACK_ANCHOR,
|
||||
pointerAnchor: CENTERED_CURSOR_PACK_ANCHOR,
|
||||
},
|
||||
turtle: {
|
||||
defaultUrl: turtleDefaultCursorUrl,
|
||||
pointerUrl: turtlePointerCursorUrl,
|
||||
defaultAnchor: CENTERED_CURSOR_PACK_ANCHOR,
|
||||
pointerAnchor: CENTERED_CURSOR_PACK_ANCHOR,
|
||||
},
|
||||
};
|
||||
|
||||
function drawArrowCursorPath(ctx: CanvasRenderingContext2D, width: number, height: number) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(width * 0.093, height * 0.056);
|
||||
ctx.lineTo(width * 0.136, height * 0.78);
|
||||
ctx.lineTo(width * 0.34, height * 0.618);
|
||||
ctx.lineTo(width * 0.453, height * 0.967);
|
||||
ctx.lineTo(width * 0.62, height * 0.906);
|
||||
ctx.lineTo(width * 0.501, height * 0.57);
|
||||
ctx.lineTo(width * 0.933, height * 0.57);
|
||||
ctx.closePath();
|
||||
function isStatefulCursorStyle(style: CursorStyle): style is StatefulCursorStyle {
|
||||
return style === "tahoe" || style === "mono";
|
||||
}
|
||||
|
||||
async function createCursorStyleAsset(
|
||||
style: Exclude<CursorStyle, "tahoe">,
|
||||
): Promise<LoadedCursorAsset> {
|
||||
function isSingleCursorStyle(style: CursorStyle): style is SingleCursorStyle {
|
||||
return style === "dot" || style === "figma";
|
||||
}
|
||||
|
||||
function resolveCursorPackVariant(cursorType: CursorAssetKey): CursorPackVariant {
|
||||
return CURSOR_PACK_POINTER_TYPES.has(cursorType) ? "pointer" : "default";
|
||||
}
|
||||
|
||||
async function createCursorStyleAsset(style: SingleCursorStyle): Promise<LoadedCursorAsset> {
|
||||
if (style === "figma") {
|
||||
const image = await loadImage(minimalCursorUrl);
|
||||
const sourceCanvas = document.createElement("canvas");
|
||||
@@ -139,40 +194,19 @@ async function createCursorStyleAsset(
|
||||
}
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
let anchorX = 0.5;
|
||||
let anchorY = 0.5;
|
||||
|
||||
if (style === "dot") {
|
||||
canvas.width = 112;
|
||||
canvas.height = 112;
|
||||
anchorX = 0.5;
|
||||
anchorY = 0.5;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const cx = canvas.width / 2;
|
||||
const cy = canvas.height / 2;
|
||||
const radius = 26;
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.strokeStyle = "rgba(15, 23, 42, 0.88)";
|
||||
ctx.lineWidth = 10;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
} else {
|
||||
canvas.width = CUSTOM_CURSOR_ARROW_WIDTH;
|
||||
canvas.height = CUSTOM_CURSOR_ARROW_HEIGHT;
|
||||
anchorX = CUSTOM_CURSOR_ARROW_TIP_X / CUSTOM_CURSOR_ARROW_WIDTH;
|
||||
anchorY = CUSTOM_CURSOR_ARROW_TIP_Y / CUSTOM_CURSOR_ARROW_HEIGHT;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.strokeStyle = "#111111";
|
||||
ctx.lineWidth = 11;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.lineCap = "round";
|
||||
drawArrowCursorPath(ctx, canvas.width, canvas.height);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
canvas.width = 112;
|
||||
canvas.height = 112;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const cx = canvas.width / 2;
|
||||
const cy = canvas.height / 2;
|
||||
const radius = 26;
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.strokeStyle = "rgba(15, 23, 42, 0.88)";
|
||||
ctx.lineWidth = 10;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
const dataUrl = canvas.toDataURL("image/png");
|
||||
await Assets.load(dataUrl);
|
||||
@@ -183,8 +217,25 @@ async function createCursorStyleAsset(
|
||||
texture,
|
||||
image,
|
||||
aspectRatio: canvas.height > 0 ? canvas.width / canvas.height : 1,
|
||||
anchorX,
|
||||
anchorY,
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
async function createCursorPackAsset(
|
||||
url: string,
|
||||
anchor: { x: number; y: number },
|
||||
): Promise<LoadedCursorAsset> {
|
||||
await Assets.load(url);
|
||||
const image = await loadImage(url);
|
||||
const texture = Texture.from(url);
|
||||
|
||||
return {
|
||||
texture,
|
||||
image,
|
||||
aspectRatio: image.naturalHeight > 0 ? image.naturalWidth / image.naturalHeight : 1,
|
||||
anchorX: clamp(anchor.x, 0, 1),
|
||||
anchorY: clamp(anchor.y, 0, 1),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -373,7 +424,7 @@ function getAvailableCursorKeys(): CursorAssetKey[] {
|
||||
return loadedKeys.length > 0 ? loadedKeys : ["arrow"];
|
||||
}
|
||||
|
||||
function getCursorStyleAsset(style: Exclude<CursorStyle, "tahoe">) {
|
||||
function getCursorStyleAsset(style: SingleCursorStyle) {
|
||||
const asset = loadedCursorStyleAssets[style];
|
||||
if (!asset) {
|
||||
throw new Error(`Missing cursor style asset for ${style}`);
|
||||
@@ -382,10 +433,23 @@ function getCursorStyleAsset(style: Exclude<CursorStyle, "tahoe">) {
|
||||
return asset;
|
||||
}
|
||||
|
||||
function getStatefulCursorAsset(
|
||||
style: Extract<CursorStyle, "tahoe" | "mono">,
|
||||
key: CursorAssetKey,
|
||||
) {
|
||||
function getCursorPackStyleAsset(style: CursorPackStyle, key: CursorAssetKey) {
|
||||
const styleAssets = loadedCursorPackAssets[style];
|
||||
if (!styleAssets) {
|
||||
if (!warnedMissingCursorPackStyles.has(style)) {
|
||||
warnedMissingCursorPackStyles.add(style);
|
||||
console.warn(
|
||||
`[CursorRenderer] Missing cursor pack assets for ${style}; falling back to Tahoe cursors.`,
|
||||
);
|
||||
}
|
||||
return getStatefulCursorAsset("tahoe", key);
|
||||
}
|
||||
|
||||
const variant = resolveCursorPackVariant(key);
|
||||
return styleAssets[variant] ?? styleAssets.default;
|
||||
}
|
||||
|
||||
function getStatefulCursorAsset(style: StatefulCursorStyle, key: CursorAssetKey) {
|
||||
const assetMap = style === "mono" ? loadedInvertedCursorAssets : loadedCursorAssets;
|
||||
const asset = assetMap[key] ?? assetMap.arrow;
|
||||
if (!asset) {
|
||||
@@ -499,9 +563,33 @@ export async function preloadCursorAssets() {
|
||||
);
|
||||
|
||||
loadedCursorStyleAssets = Object.fromEntries(customStyleEntries) as Partial<
|
||||
Record<Exclude<CursorStyle, "tahoe">, LoadedCursorAsset>
|
||||
Record<SingleCursorStyle, LoadedCursorAsset>
|
||||
>;
|
||||
|
||||
const cursorPackEntries = await Promise.all(
|
||||
(Object.entries(CURSOR_PACK_SOURCES) as Array<[CursorPackStyle, CursorPackSource]>).map(
|
||||
async ([style, source]) => {
|
||||
try {
|
||||
const [defaultAsset, pointerAsset] = await Promise.all([
|
||||
createCursorPackAsset(source.defaultUrl, source.defaultAnchor),
|
||||
createCursorPackAsset(source.pointerUrl, source.pointerAnchor),
|
||||
]);
|
||||
return [style, { default: defaultAsset, pointer: pointerAsset }] as const;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[CursorRenderer] Failed to load cursor pack style for: ${style}`,
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
loadedCursorPackAssets = Object.fromEntries(
|
||||
cursorPackEntries.filter(Boolean).map((entry) => entry!),
|
||||
) as Partial<Record<CursorPackStyle, LoadedCursorPackAssets>>;
|
||||
|
||||
if (!loadedCursorAssets.arrow) {
|
||||
throw new Error("Failed to initialize the fallback arrow cursor asset");
|
||||
}
|
||||
@@ -886,7 +974,7 @@ export class PixiCursorOverlay {
|
||||
|
||||
setStyle(style: CursorStyle) {
|
||||
this.config.style = style;
|
||||
if (style === "tahoe" || style === "mono") {
|
||||
if (isStatefulCursorStyle(style)) {
|
||||
for (const key of getAvailableCursorKeys()) {
|
||||
const asset = getStatefulCursorAsset(style, key);
|
||||
const shadowSprite = this.cursorShadowSprites[key];
|
||||
@@ -903,7 +991,9 @@ export class PixiCursorOverlay {
|
||||
return;
|
||||
}
|
||||
|
||||
const asset = getCursorStyleAsset(style);
|
||||
const asset = isSingleCursorStyle(style)
|
||||
? getCursorStyleAsset(style)
|
||||
: getCursorPackStyleAsset(style, "arrow");
|
||||
this.customCursorShadowSprite.texture = asset.texture;
|
||||
this.customCursorShadowSprite.anchor.set(asset.anchorX, asset.anchorY);
|
||||
this.customCursorSprite.texture = asset.texture;
|
||||
@@ -978,11 +1068,12 @@ export class PixiCursorOverlay {
|
||||
this.clickRingGraphics.clear();
|
||||
drawClickRing(this.clickRingGraphics, px, py, h, clickProgress);
|
||||
|
||||
if (this.config.style === "tahoe" || this.config.style === "mono") {
|
||||
const spriteKey = (cursorType in this.cursorSprites ? cursorType : "arrow") as CursorAssetKey;
|
||||
|
||||
if (isStatefulCursorStyle(this.config.style)) {
|
||||
this.customCursorShadowSprite.visible = false;
|
||||
this.customCursorSprite.visible = false;
|
||||
|
||||
const spriteKey = (cursorType in this.cursorSprites ? cursorType : "arrow") as CursorAssetKey;
|
||||
const asset = getStatefulCursorAsset(this.config.style, spriteKey);
|
||||
const shadowSprite = this.cursorShadowSprites[spriteKey] ?? this.cursorShadowSprites.arrow!;
|
||||
const sprite = this.cursorSprites[spriteKey] ?? this.cursorSprites.arrow!;
|
||||
@@ -1022,8 +1113,12 @@ export class PixiCursorOverlay {
|
||||
currentSprite.visible = false;
|
||||
}
|
||||
|
||||
const asset = getCursorStyleAsset(this.config.style);
|
||||
const asset = isSingleCursorStyle(this.config.style)
|
||||
? getCursorStyleAsset(this.config.style)
|
||||
: getCursorPackStyleAsset(this.config.style, spriteKey);
|
||||
const showSeparateShadow = this.config.style !== "figma";
|
||||
this.customCursorShadowSprite.texture = asset.texture;
|
||||
this.customCursorShadowSprite.anchor.set(asset.anchorX, asset.anchorY);
|
||||
this.customCursorShadowSprite.visible = showSeparateShadow;
|
||||
if (showSeparateShadow) {
|
||||
this.customCursorShadowSprite.height = scaledH * bounceScale;
|
||||
@@ -1035,6 +1130,8 @@ export class PixiCursorOverlay {
|
||||
this.customCursorShadowSprite.rotation = swayRotation;
|
||||
}
|
||||
|
||||
this.customCursorSprite.texture = asset.texture;
|
||||
this.customCursorSprite.anchor.set(asset.anchorX, asset.anchorY);
|
||||
this.customCursorSprite.visible = true;
|
||||
this.customCursorSprite.alpha = this.config.dotAlpha;
|
||||
this.customCursorSprite.height = scaledH * bounceScale;
|
||||
@@ -1168,13 +1265,14 @@ export function drawCursorOnCanvas(
|
||||
timeMs,
|
||||
config.clickBounceDuration,
|
||||
);
|
||||
const asset =
|
||||
config.style === "tahoe" || config.style === "mono"
|
||||
? getStatefulCursorAsset(
|
||||
config.style,
|
||||
(cursorType && loadedCursorAssets[cursorType] ? cursorType : "arrow") as CursorAssetKey,
|
||||
)
|
||||
: getCursorStyleAsset(config.style);
|
||||
const spriteKey = (
|
||||
cursorType && loadedCursorAssets[cursorType] ? cursorType : "arrow"
|
||||
) as CursorAssetKey;
|
||||
const asset = isStatefulCursorStyle(config.style)
|
||||
? getStatefulCursorAsset(config.style, spriteKey)
|
||||
: isSingleCursorStyle(config.style)
|
||||
? getCursorStyleAsset(config.style)
|
||||
: getCursorPackStyleAsset(config.style, spriteKey);
|
||||
const bounceScale = Math.max(
|
||||
0.72,
|
||||
1 - Math.sin(clickBounceProgress * Math.PI) * (0.08 * config.clickBounce),
|
||||
|
||||
@@ -22,7 +22,12 @@
|
||||
"tahoe": "Tahoe",
|
||||
"dot": "Dot",
|
||||
"figma": "Minimal",
|
||||
"mono": "Inverted"
|
||||
"mono": "Inverted",
|
||||
"lavender": "Lavender",
|
||||
"parched": "Parched",
|
||||
"chooper": "Chooper",
|
||||
"amongus": "Among Us",
|
||||
"turtle": "Turtle"
|
||||
},
|
||||
"backgroundBlur": "Background Blur",
|
||||
"zoomMotionBlur": "Zoom Motion Blur",
|
||||
|
||||
@@ -22,7 +22,12 @@
|
||||
"tahoe": "Tahoe",
|
||||
"dot": "Punto",
|
||||
"figma": "Minimal",
|
||||
"mono": "Invertido"
|
||||
"mono": "Invertido",
|
||||
"lavender": "Lavender",
|
||||
"parched": "Parched",
|
||||
"chooper": "Chooper",
|
||||
"amongus": "Among Us",
|
||||
"turtle": "Turtle"
|
||||
},
|
||||
"backgroundBlur": "Desenfoque de fondo",
|
||||
"zoomMotionBlur": "Desenfoque de movimiento del zoom",
|
||||
|
||||
@@ -22,7 +22,12 @@
|
||||
"tahoe": "Tahoe",
|
||||
"dot": "圆点",
|
||||
"figma": "Minimal",
|
||||
"mono": "反相"
|
||||
"mono": "反相",
|
||||
"lavender": "Lavender",
|
||||
"parched": "Parched",
|
||||
"chooper": "Chooper",
|
||||
"amongus": "Among Us",
|
||||
"turtle": "Turtle"
|
||||
},
|
||||
"backgroundBlur": "背景模糊",
|
||||
"zoomMotionBlur": "缩放运动模糊",
|
||||
|
||||