mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 23:35:43 +00:00
fix(playback): improve cursor rendering and layout calculations
- Refine cursor telemetry loop with extension coordinate support - Improve DPI-aware cursor rendering pipeline - Extend layout utilities for extension-driven scene transforms
This commit is contained in:
@@ -174,12 +174,15 @@ export function buildLoopedCursorTelemetry(
|
||||
const sourceStartMs = firstSample.timeMs;
|
||||
const sourceEndMs = Math.max(sourceStartMs, findLastMovingSampleTime(boundedSamples));
|
||||
const sourceDurationMs = Math.max(1, sourceEndMs - sourceStartMs);
|
||||
const playbackWindowMs = Math.max(1, motionEndMs);
|
||||
// Keep remapped times in source-time coordinates so cursor lookups
|
||||
// (which always use video.currentTime, i.e. source time) stay correct
|
||||
// even when the visible timeline window doesn't start at 0.
|
||||
const motionDurationMs = Math.max(1, motionEndMs - timelineStartClampedMs);
|
||||
const startingCursorType = findFirstStableCursorType(boundedSamples);
|
||||
const loopedSamples: CursorTelemetryPoint[] = [
|
||||
{
|
||||
...firstSample,
|
||||
timeMs: 0,
|
||||
timeMs: timelineStartClampedMs,
|
||||
interactionType: undefined,
|
||||
cursorType: startingCursorType,
|
||||
},
|
||||
@@ -187,7 +190,7 @@ export function buildLoopedCursorTelemetry(
|
||||
|
||||
for (const sample of boundedSamples) {
|
||||
const progress = clamp((sample.timeMs - sourceStartMs) / sourceDurationMs, 0, 1);
|
||||
const mappedTimeMs = Math.round(playbackWindowMs * progress);
|
||||
const mappedTimeMs = Math.round(timelineStartClampedMs + motionDurationMs * progress);
|
||||
|
||||
if (mappedTimeMs <= loopedSamples[loopedSamples.length - 1].timeMs) {
|
||||
loopedSamples[loopedSamples.length - 1] = {
|
||||
|
||||
@@ -91,7 +91,7 @@ export const DEFAULT_CURSOR_CONFIG: CursorRenderConfig = {
|
||||
|
||||
const REFERENCE_WIDTH = 1920;
|
||||
const MIN_CURSOR_VIEWPORT_SCALE = 0.55;
|
||||
const CLICK_RING_FADE_MS = 240;
|
||||
const CLICK_RING_FADE_MS = 600;
|
||||
const CURSOR_MOTION_BLUR_BASE_MULTIPLIER = 0.08;
|
||||
const CURSOR_TIME_DISCONTINUITY_MS = 100;
|
||||
const CURSOR_SWAY_SMOOTHING_MULTIPLIER = 0.7;
|
||||
@@ -852,13 +852,6 @@ export class SmoothedCursorState {
|
||||
}
|
||||
}
|
||||
|
||||
function drawClickRing(graphics: Graphics, px: number, py: number, h: number, progress: number) {
|
||||
void graphics;
|
||||
void px;
|
||||
void py;
|
||||
void h;
|
||||
void progress;
|
||||
}
|
||||
|
||||
export class PixiCursorOverlay {
|
||||
public readonly container: Container;
|
||||
@@ -1000,6 +993,22 @@ export class PixiCursorOverlay {
|
||||
this.customCursorSprite.anchor.set(asset.anchorX, asset.anchorY);
|
||||
}
|
||||
|
||||
getSmoothedCursorSnapshot(): {
|
||||
cx: number;
|
||||
cy: number;
|
||||
trail: Array<{ cx: number; cy: number }>;
|
||||
} | null {
|
||||
if (!this.container.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
cx: this.state.x,
|
||||
cy: this.state.y,
|
||||
trail: this.state.trail.map((point) => ({ cx: point.x, cy: point.y })),
|
||||
};
|
||||
}
|
||||
|
||||
update(
|
||||
samples: CursorTelemetryPoint[],
|
||||
timeMs: number,
|
||||
@@ -1053,7 +1062,7 @@ export class PixiCursorOverlay {
|
||||
const px = viewport.x + this.state.x * viewport.width;
|
||||
const py = viewport.y + this.state.y * viewport.height;
|
||||
const h = this.config.dotRadius * getCursorViewportScale(viewport);
|
||||
const { cursorType, clickBounceProgress, clickProgress } = getCursorVisualState(
|
||||
const { cursorType, clickBounceProgress } = getCursorVisualState(
|
||||
samples,
|
||||
timeMs,
|
||||
this.config.clickBounceDuration,
|
||||
@@ -1066,7 +1075,6 @@ export class PixiCursorOverlay {
|
||||
const swayRotation = this.updateCursorSway(px, py, timeMs, shouldFreezeCursorMotion);
|
||||
|
||||
this.clickRingGraphics.clear();
|
||||
drawClickRing(this.clickRingGraphics, px, py, h, clickProgress);
|
||||
|
||||
const spriteKey = (cursorType in this.cursorSprites ? cursorType : "arrow") as CursorAssetKey;
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ interface LayoutParams {
|
||||
lockedVideoDimensions?: { width: number; height: number } | null;
|
||||
borderRadius?: number;
|
||||
padding?: number;
|
||||
/** Screen insets from the active device frame, used to scale/center the full frame */
|
||||
frameInsets?: { top: number; right: number; bottom: number; left: number } | null;
|
||||
}
|
||||
|
||||
interface LayoutResult {
|
||||
@@ -30,7 +32,7 @@ interface LayoutResult {
|
||||
}
|
||||
|
||||
export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
|
||||
const { container, app, videoSprite, maskGraphics, videoElement, cropRegion, lockedVideoDimensions, borderRadius = 0, padding = 0 } = params;
|
||||
const { container, app, videoSprite, maskGraphics, videoElement, cropRegion, lockedVideoDimensions, borderRadius = 0, padding = 0, frameInsets } = params;
|
||||
|
||||
const videoWidth = lockedVideoDimensions?.width || videoElement.videoWidth;
|
||||
const videoHeight = lockedVideoDimensions?.height || videoElement.videoHeight;
|
||||
@@ -68,9 +70,20 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
|
||||
const maxDisplayWidth = width * paddingScale;
|
||||
const maxDisplayHeight = height * paddingScale;
|
||||
|
||||
// When a device frame is active, the frame extends beyond the video area.
|
||||
// We need to scale so the ENTIRE frame (video + bezels) fits in the viewport,
|
||||
// then center the full frame, not just the video content.
|
||||
const insets = frameInsets;
|
||||
// Fraction of the full frame occupied by the screen area
|
||||
const screenFracW = insets ? (1 - insets.left - insets.right) : 1;
|
||||
const screenFracH = insets ? (1 - insets.top - insets.bottom) : 1;
|
||||
// Full frame dimensions in video pixels (the frame image is this large relative to the screen)
|
||||
const fullFrameVideoW = croppedVideoWidth / screenFracW;
|
||||
const fullFrameVideoH = croppedVideoHeight / screenFracH;
|
||||
|
||||
const scale = Math.min(
|
||||
maxDisplayWidth / croppedVideoWidth,
|
||||
maxDisplayHeight / croppedVideoHeight,
|
||||
maxDisplayWidth / fullFrameVideoW,
|
||||
maxDisplayHeight / fullFrameVideoH,
|
||||
);
|
||||
|
||||
videoSprite.scale.set(scale);
|
||||
@@ -83,9 +96,20 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
|
||||
const croppedDisplayWidth = croppedVideoWidth * scale;
|
||||
const croppedDisplayHeight = croppedVideoHeight * scale;
|
||||
|
||||
// Center the cropped region in the container
|
||||
const centerOffsetX = (width - croppedDisplayWidth) / 2;
|
||||
const centerOffsetY = (height - croppedDisplayHeight) / 2;
|
||||
// Center the full frame (or just the video if no frame) in the container
|
||||
// Full frame display dimensions
|
||||
const fullFrameDisplayW = fullFrameVideoW * scale;
|
||||
const fullFrameDisplayH = fullFrameVideoH * scale;
|
||||
// The full frame's top-left, centered in the viewport
|
||||
const frameCenterX = (width - fullFrameDisplayW) / 2;
|
||||
const frameCenterY = (height - fullFrameDisplayH) / 2;
|
||||
// The screen area starts at frameCenterX + insets.left * fullFrameDisplayW
|
||||
const centerOffsetX = insets
|
||||
? frameCenterX + insets.left * fullFrameDisplayW
|
||||
: (width - croppedDisplayWidth) / 2;
|
||||
const centerOffsetY = insets
|
||||
? frameCenterY + insets.top * fullFrameDisplayH
|
||||
: (height - croppedDisplayHeight) / 2;
|
||||
|
||||
// Position the full video sprite so that when we apply the mask,
|
||||
// the cropped region appears centered
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type React from 'react';
|
||||
import type { TrimRegion, SpeedRegion } from '../types';
|
||||
import { extensionHost } from '@/lib/extensions';
|
||||
|
||||
interface VideoEventHandlersParams {
|
||||
video: HTMLVideoElement;
|
||||
@@ -31,6 +32,7 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) {
|
||||
const emitTime = (timeValue: number) => {
|
||||
currentTimeRef.current = timeValue * 1000;
|
||||
onTimeUpdate(timeValue);
|
||||
extensionHost.emitEvent({ type: 'playback:timeupdate', timeMs: timeValue * 1000 });
|
||||
};
|
||||
|
||||
// Helper function to check if current time is within a trim region
|
||||
|
||||
Reference in New Issue
Block a user