Add custom cursor packs and harden native postinstall builds

This commit is contained in:
insomniachooman
2026-03-26 22:39:18 +06:00
parent 1826cfec3a
commit 157af5f083
22 changed files with 635 additions and 352 deletions
+15 -11
View File
@@ -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
+1 -1
View File
@@ -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 .",
+45 -19
View File
@@ -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",
+45 -8
View File
@@ -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',
+53
View File
@@ -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);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

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,
+203 -208
View File
@@ -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),
+6 -1
View File
@@ -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",
+6 -1
View File
@@ -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",
+6 -1
View File
@@ -22,7 +22,12 @@
"tahoe": "Tahoe",
"dot": "圆点",
"figma": "Minimal",
"mono": "反相"
"mono": "反相",
"lavender": "Lavender",
"parched": "Parched",
"chooper": "Chooper",
"amongus": "Among Us",
"turtle": "Turtle"
},
"backgroundBlur": "背景模糊",
"zoomMotionBlur": "缩放运动模糊",