cleanup+ readme updates
This commit is contained in:
@@ -104,7 +104,6 @@ export function SourceSelector() {
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{/* Removed scroll hint gradient for clean look */}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="windows" className="h-full">
|
||||
@@ -144,7 +143,6 @@ export function SourceSelector() {
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{/* Removed scroll hint gradient for clean look */}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,6 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||
const [initialCrop, setInitialCrop] = useState<CropRegion>(cropRegion);
|
||||
|
||||
// Draw video preview at high quality
|
||||
useEffect(() => {
|
||||
if (!videoElement || !canvasRef.current) return;
|
||||
|
||||
@@ -31,7 +30,6 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
const ctx = canvas.getContext('2d', { alpha: false });
|
||||
if (!ctx) return;
|
||||
|
||||
// Set canvas to actual video dimensions for high quality
|
||||
canvas.width = videoElement.videoWidth || 1920;
|
||||
canvas.height = videoElement.videoHeight || 1080;
|
||||
|
||||
@@ -62,7 +60,6 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
});
|
||||
setInitialCrop(cropRegion);
|
||||
|
||||
// Capture pointer for smooth dragging
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
};
|
||||
|
||||
@@ -79,11 +76,8 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
|
||||
switch (isDragging) {
|
||||
case 'top': {
|
||||
// Calculate new y position
|
||||
const newY = Math.max(0, initialCrop.y + deltaY);
|
||||
// Calculate the bottom edge (which should stay fixed)
|
||||
const bottom = initialCrop.y + initialCrop.height;
|
||||
// Ensure minimum height of 0.1
|
||||
newCrop.y = Math.min(newY, bottom - 0.1);
|
||||
newCrop.height = bottom - newCrop.y;
|
||||
break;
|
||||
@@ -92,11 +86,8 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
newCrop.height = Math.max(0.1, Math.min(initialCrop.height + deltaY, 1 - initialCrop.y));
|
||||
break;
|
||||
case 'left': {
|
||||
// Calculate new x position
|
||||
const newX = Math.max(0, initialCrop.x + deltaX);
|
||||
// Calculate the right edge (which should stay fixed)
|
||||
const right = initialCrop.x + initialCrop.width;
|
||||
// Ensure minimum width of 0.1
|
||||
newCrop.x = Math.min(newX, right - 0.1);
|
||||
newCrop.width = right - newCrop.x;
|
||||
break;
|
||||
@@ -114,7 +105,6 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
try {
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
setIsDragging(null);
|
||||
@@ -140,7 +130,6 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
style={{ imageRendering: 'auto' }}
|
||||
/>
|
||||
|
||||
{/* Dark overlay outside crop */}
|
||||
<div className="absolute inset-0 pointer-events-none" style={{ transition: 'none' }}>
|
||||
<svg width="100%" height="100%" className="absolute inset-0" style={{ transition: 'none' }}>
|
||||
<defs>
|
||||
@@ -167,8 +156,6 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Crop region - 4 straight lines */}
|
||||
{/* Top line */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute h-[3px] cursor-ns-resize z-20 pointer-events-auto bg-green-500"
|
||||
@@ -184,7 +171,6 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
onPointerDown={(e) => handlePointerDown(e, 'top')}
|
||||
/>
|
||||
|
||||
{/* Bottom line */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute h-[3px] cursor-ns-resize z-20 pointer-events-auto bg-green-500"
|
||||
@@ -200,7 +186,6 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
onPointerDown={(e) => handlePointerDown(e, 'bottom')}
|
||||
/>
|
||||
|
||||
{/* Left line */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute w-[3px] cursor-ew-resize z-20 pointer-events-auto bg-green-500"
|
||||
@@ -216,7 +201,6 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
onPointerDown={(e) => handlePointerDown(e, 'left')}
|
||||
/>
|
||||
|
||||
{/* Right line */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute w-[3px] cursor-ew-resize z-20 pointer-events-auto bg-green-500"
|
||||
|
||||
@@ -210,15 +210,13 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
|
||||
{(wallpaperPaths.length > 0 ? wallpaperPaths : WALLPAPER_RELATIVE.map(p => `/${p}`)).map((path, idx) => {
|
||||
const isSelected = (() => {
|
||||
if (!selected) return false;
|
||||
// exact match
|
||||
|
||||
if (selected === path) return true;
|
||||
// file:// vs absolute path mismatch: compare by filename suffix
|
||||
try {
|
||||
const clean = (s: string) => s.replace(/^file:\/\//, '').replace(/^\//, '')
|
||||
if (clean(selected).endsWith(clean(path))) return true;
|
||||
if (clean(path).endsWith(clean(selected))) return true;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
@@ -181,13 +181,11 @@ export default function VideoEditor() {
|
||||
setExportError(null);
|
||||
|
||||
try {
|
||||
// Pause video during export
|
||||
const wasPlaying = isPlaying;
|
||||
if (wasPlaying) {
|
||||
videoPlaybackRef.current?.pause();
|
||||
}
|
||||
|
||||
// Always export at 1920x1080 (16:9)
|
||||
const width = 1920;
|
||||
const height = 1080;
|
||||
|
||||
@@ -212,7 +210,6 @@ export default function VideoEditor() {
|
||||
const result = await exporter.export();
|
||||
|
||||
if (result.success && result.blob) {
|
||||
// Save the blob using Electron
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
const timestamp = Date.now();
|
||||
const fileName = `export-${timestamp}.mp4`;
|
||||
@@ -230,7 +227,6 @@ export default function VideoEditor() {
|
||||
toast.error(result.error || 'Export failed');
|
||||
}
|
||||
|
||||
// Resume playback if it was playing
|
||||
if (wasPlaying) {
|
||||
videoPlaybackRef.current?.play();
|
||||
}
|
||||
|
||||
@@ -165,7 +165,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
}
|
||||
}, [updateOverlayForRegion, cropRegion]);
|
||||
|
||||
// Keep layoutVideoContent ref updated
|
||||
useEffect(() => {
|
||||
layoutVideoContentRef.current = layoutVideoContent;
|
||||
}, [layoutVideoContent]);
|
||||
@@ -262,7 +261,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
try {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
} catch {
|
||||
// ignore release errors when pointer capture is already cleared
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,7 +285,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
isPlayingRef.current = isPlaying;
|
||||
}, [isPlaying]);
|
||||
|
||||
// Reset animation state and transforms when crop changes
|
||||
useEffect(() => {
|
||||
if (!pixiReady || !videoReady) return;
|
||||
|
||||
@@ -306,7 +304,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
video.pause();
|
||||
}
|
||||
|
||||
// Reset animation state so the ticker starts from identity once it resumes
|
||||
animationStateRef.current = {
|
||||
scale: 1,
|
||||
focusX: DEFAULT_FOCUS.cx,
|
||||
@@ -317,7 +314,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
blurFilterRef.current.blur = 0;
|
||||
}
|
||||
|
||||
// Defer layout to the next frame so DOM measurements include the new crop UI state
|
||||
requestAnimationFrame(() => {
|
||||
const container = cameraContainerRef.current;
|
||||
const videoStage = videoContainerRef.current;
|
||||
@@ -327,7 +323,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset all transform hierarchies to identity
|
||||
container.scale.set(1);
|
||||
container.position.set(0, 0);
|
||||
videoStage.scale.set(1);
|
||||
@@ -335,10 +330,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
sprite.scale.set(1);
|
||||
sprite.position.set(0, 0);
|
||||
|
||||
// Now layoutVideoContent will apply the correct transforms for the new crop
|
||||
layoutVideoContent();
|
||||
|
||||
// Apply an explicit identity transform to ensure no residual camera offset
|
||||
applyZoomTransform({
|
||||
cameraContainer: container,
|
||||
blurFilter: blurFilterRef.current,
|
||||
@@ -351,12 +344,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
isPlaying: false,
|
||||
});
|
||||
|
||||
// Restart ticker on a second frame to avoid running mid-layout
|
||||
requestAnimationFrame(() => {
|
||||
const finalApp = appRef.current;
|
||||
if (wasPlaying && video) {
|
||||
video.play().catch(() => {
|
||||
/* ignore */
|
||||
});
|
||||
}
|
||||
if (tickerWasStarted && finalApp?.ticker) {
|
||||
@@ -402,7 +393,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
overlayEl.style.pointerEvents = isPlaying ? 'none' : 'auto';
|
||||
}, [selectedZoom, isPlaying]);
|
||||
|
||||
// Initialize PixiJS application
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
@@ -410,7 +400,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
let mounted = true;
|
||||
let app: PIXI.Application | null = null;
|
||||
|
||||
// Initialize the app
|
||||
(async () => {
|
||||
app = new PIXI.Application();
|
||||
|
||||
@@ -423,7 +412,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
autoDensity: true,
|
||||
});
|
||||
|
||||
// Lock ticker to 60fps for consistent animation speed across all displays
|
||||
app.ticker.maxFPS = 60;
|
||||
|
||||
if (!mounted) {
|
||||
@@ -690,7 +678,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
return
|
||||
}
|
||||
|
||||
// If it's a solid color or CSS gradient, use it directly
|
||||
if (wallpaper.startsWith('#') || wallpaper.startsWith('linear-gradient') || wallpaper.startsWith('radial-gradient')) {
|
||||
if (mounted) setResolvedWallpaper(wallpaper)
|
||||
return
|
||||
@@ -708,8 +695,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
if (mounted) setResolvedWallpaper(wallpaper)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise assume it's a relative path like 'wallpapers/wallpaper1.jpg'
|
||||
const p = await getAssetPath(wallpaper.replace(/^\//, ''))
|
||||
if (mounted) setResolvedWallpaper(p)
|
||||
} catch (err) {
|
||||
|
||||
@@ -151,7 +151,7 @@ function PlaybackCursor({
|
||||
<div
|
||||
className="absolute top-0 bottom-0 pointer-events-none z-50"
|
||||
style={{
|
||||
[sideProperty === "right" ? "marginRight" : "marginLeft"]: `${sidebarWidth - 8}px`, // reduce margin
|
||||
[sideProperty === "right" ? "marginRight" : "marginLeft"]: `${sidebarWidth - 8}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -185,7 +185,6 @@ function PlaybackCursor({
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/* Subtle glow at top */}
|
||||
<div className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-red-500/30 rounded-full blur-sm" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,7 +402,6 @@ export default function TimelineEditor({
|
||||
// Snap if gap is 2ms or less
|
||||
return zoomRegions.some((region) => {
|
||||
if (region.id === excludeId) return false;
|
||||
// If the new span is within 2ms of another region, treat as overlap (snap)
|
||||
const gapBefore = newSpan.start - region.endMs;
|
||||
const gapAfter = region.startMs - newSpan.end;
|
||||
if (gapBefore > 0 && gapBefore <= 2) return true;
|
||||
|
||||
@@ -149,8 +149,6 @@ export default function TimelineWrapper({
|
||||
[clampRange, onRangeChange, totalMs],
|
||||
);
|
||||
|
||||
// To maximize granularity, disable grid snapping by not passing rangeGridSizeDefinition
|
||||
// and allow pixel-level movement for items.
|
||||
return (
|
||||
<TimelineContext
|
||||
range={range}
|
||||
@@ -158,7 +156,6 @@ export default function TimelineWrapper({
|
||||
onResizeEnd={onResizeEnd}
|
||||
onDragEnd={onDragEnd}
|
||||
autoScroll={{ enabled: false }}
|
||||
// Remove rangeGridSizeDefinition to avoid snap effect
|
||||
>
|
||||
{children}
|
||||
</TimelineContext>
|
||||
|
||||
@@ -18,8 +18,7 @@ export function clampFocusToStage(
|
||||
|
||||
const windowWidth = stageSize.width / zoomScale;
|
||||
const windowHeight = stageSize.height / zoomScale;
|
||||
|
||||
// Calculate margins - focus must stay far enough from edges so zoom window stays within stage bounds
|
||||
|
||||
const marginX = windowWidth / (2 * stageSize.width);
|
||||
const marginY = windowHeight / (2 * stageSize.height);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ interface LayoutResult {
|
||||
export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
|
||||
const { container, app, videoSprite, maskGraphics, videoElement, cropRegion, lockedVideoDimensions } = params;
|
||||
|
||||
// Use locked dimensions if available, otherwise use current video dimensions
|
||||
|
||||
const videoWidth = lockedVideoDimensions?.width || videoElement.videoWidth;
|
||||
const videoHeight = lockedVideoDimensions?.height || videoElement.videoHeight;
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) {
|
||||
}
|
||||
|
||||
const handlePlay = () => {
|
||||
// Prevent autoplay during seek operations
|
||||
if (isSeekingRef.current) {
|
||||
video.pause();
|
||||
return;
|
||||
@@ -69,7 +68,6 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) {
|
||||
const handleSeeked = () => {
|
||||
isSeekingRef.current = false;
|
||||
|
||||
// Keep video paused after seek if it wasn't playing
|
||||
if (!isPlayingRef.current && !video.paused) {
|
||||
video.pause();
|
||||
}
|
||||
@@ -79,7 +77,6 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) {
|
||||
const handleSeeking = () => {
|
||||
isSeekingRef.current = true;
|
||||
|
||||
// Prevent autoplay during seek if video was paused
|
||||
if (!isPlayingRef.current && !video.paused) {
|
||||
video.pause();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user