From 5069354df3e3775dac12a76ae2e1270fe49e01c7 Mon Sep 17 00:00:00 2001 From: huanld Date: Fri, 5 Jun 2026 20:39:26 +0700 Subject: [PATCH] Adjust guide video annotation timing --- package-lock.json | 4 +- package.json | 2 +- src/guide/videoAnnotations.test.ts | 11 +++- src/guide/videoAnnotations.ts | 82 +++++++++++++++++++++--------- 4 files changed, 70 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a99ccc..b9feec1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscreen", - "version": "1.4.11", + "version": "1.4.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscreen", - "version": "1.4.11", + "version": "1.4.12", "dependencies": { "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", diff --git a/package.json b/package.json index e6e72db..b6b0b69 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openscreen", "private": true, - "version": "1.4.11", + "version": "1.4.12", "type": "module", "packageManager": "npm@10.9.4", "engines": { diff --git a/src/guide/videoAnnotations.test.ts b/src/guide/videoAnnotations.test.ts index 034a197..2ca09fb 100644 --- a/src/guide/videoAnnotations.test.ts +++ b/src/guide/videoAnnotations.test.ts @@ -65,7 +65,11 @@ describe("buildGuideVideoAnnotations", () => { startMs: 1200, content: "1. Click Settings.", }); + expect(annotations[0]?.endMs).toBe(3200); expect(annotations[0]?.position.x).toBeGreaterThan(20); + expect(annotations[1]?.endMs).toBe(3200); + expect(annotations[1]?.position.x).toBeGreaterThan((annotations[0]?.position.x ?? 0) + 34); + expect(annotations[1]?.position.y).toBeCloseTo(30.5); expect(annotations[1]).toMatchObject({ id: "guide-video-2", type: "magnifier", @@ -79,10 +83,13 @@ describe("buildGuideVideoAnnotations", () => { expect(annotations[2]).toMatchObject({ id: "guide-video-3", type: "figure", + endMs: 3200, figureData: { + arrowDirection: "left", color: "#34B27B", }, }); + expect(annotations[2]?.position.x).toBeGreaterThan(20); }); it("returns an empty list when no draft exists", () => { @@ -97,7 +104,7 @@ describe("buildGuideVideoAnnotations", () => { expect(annotations).toEqual([]); }); - it("creates 0.3x speed regions for one second at each guide point", () => { + it("creates 0.3x speed regions for two seconds at each guide point", () => { let id = 1; const speedRegions = buildGuideVideoSpeedRegions(createSession(), { nextId: () => `guide-speed-${id++}`, @@ -107,7 +114,7 @@ describe("buildGuideVideoAnnotations", () => { { id: "guide-speed-1", startMs: 1200, - endMs: 2200, + endMs: 3200, speed: 0.3, }, ]); diff --git a/src/guide/videoAnnotations.ts b/src/guide/videoAnnotations.ts index a16218d..62ea49b 100644 --- a/src/guide/videoAnnotations.ts +++ b/src/guide/videoAnnotations.ts @@ -14,13 +14,14 @@ export interface BuildGuideVideoAnnotationsOptions { defaultDurationMs?: number; } -const DEFAULT_STEP_DURATION_MS = 3200; -const DEFAULT_STEP_SLOW_MOTION_DURATION_MS = 1000; +const DEFAULT_STEP_DURATION_MS = 2000; +const DEFAULT_STEP_SLOW_MOTION_DURATION_MS = 2000; const DEFAULT_STEP_SLOW_MOTION_SPEED = 0.3; const CAPTION_WIDTH = 34; const CAPTION_HEIGHT = 13; const MAGNIFIER_SIZE = 18; const ARROW_SIZE = 10; +const ANNOTATION_GAP = 2; function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); @@ -58,15 +59,19 @@ function getCaptionPosition(candidate: GuideStepCandidate | undefined) { function getArrowDirection( candidate: GuideStepCandidate | undefined, - captionPosition: { x: number; y: number }, + originPosition: { x: number; y: number }, + originSize: { width: number; height: number } = { + width: CAPTION_WIDTH, + height: CAPTION_HEIGHT, + }, ): ArrowDirection { const target = candidate?.position; if (!target) return "right"; - const captionCenterX = captionPosition.x + CAPTION_WIDTH / 2; - const captionCenterY = captionPosition.y + CAPTION_HEIGHT / 2; - const dx = target.normalizedX * 100 - captionCenterX; - const dy = target.normalizedY * 100 - captionCenterY; + const originCenterX = originPosition.x + originSize.width / 2; + const originCenterY = originPosition.y + originSize.height / 2; + const dx = target.normalizedX * 100 - originCenterX; + const dy = target.normalizedY * 100 - originCenterY; const horizontal = dx > 8 ? "right" : dx < -8 ? "left" : ""; const vertical = dy > 8 ? "down" : dy < -8 ? "up" : ""; @@ -74,6 +79,40 @@ function getArrowDirection( return (horizontal || vertical || "right") as ArrowDirection; } +function getMagnifierPosition(captionPosition: { x: number; y: number }) { + const canPlaceRight = captionPosition.x + CAPTION_WIDTH + ANNOTATION_GAP + MAGNIFIER_SIZE <= 98; + const x = canPlaceRight + ? captionPosition.x + CAPTION_WIDTH + ANNOTATION_GAP + : captionPosition.x - MAGNIFIER_SIZE - ANNOTATION_GAP; + const y = captionPosition.y + (CAPTION_HEIGHT - MAGNIFIER_SIZE) / 2; + + return { + x: clamp(x, 2, 100 - MAGNIFIER_SIZE - 2), + y: clamp(y, 2, 100 - MAGNIFIER_SIZE - 2), + }; +} + +function getArrowPosition( + position: NonNullable, + originPosition: { x: number; y: number }, + originSize: { width: number; height: number }, +) { + const targetX = position.normalizedX * 100; + const targetY = position.normalizedY * 100; + const originCenterX = originPosition.x + originSize.width / 2; + const originCenterY = originPosition.y + originSize.height / 2; + const distance = Math.hypot(targetX - originCenterX, targetY - originCenterY); + const targetOffset = Math.min(18, Math.max(10, distance * 0.35)); + const ratio = distance > 0 ? Math.max(0, (distance - targetOffset) / distance) : 0; + const arrowCenterX = originCenterX + (targetX - originCenterX) * ratio; + const arrowCenterY = originCenterY + (targetY - originCenterY) * ratio; + + return { + x: clamp(arrowCenterX - ARROW_SIZE / 2, 0, 100 - ARROW_SIZE), + y: clamp(arrowCenterY - ARROW_SIZE / 2, 0, 100 - ARROW_SIZE), + }; +} + function buildCaption(step: GeneratedGuideStep) { const instruction = step.instruction.trim(); const title = step.title.trim(); @@ -101,7 +140,6 @@ export function buildGuideVideoAnnotations( const startMs = Math.max(0, Math.round(candidate?.timeMs ?? index * durationMs)); const endMs = Math.max(startMs + 750, startMs + durationMs); const captionPosition = getCaptionPosition(candidate); - const arrowDirection = getArrowDirection(candidate, captionPosition); annotations.push({ id: options.nextId(), @@ -124,24 +162,23 @@ export function buildGuideVideoAnnotations( }); if (candidate?.position) { + const magnifierPosition = getMagnifierPosition(captionPosition); + const arrowPosition = getArrowPosition(candidate.position, magnifierPosition, { + width: MAGNIFIER_SIZE, + height: MAGNIFIER_SIZE, + }); + const arrowDirection = getArrowDirection(candidate, arrowPosition, { + width: ARROW_SIZE, + height: ARROW_SIZE, + }); + annotations.push({ id: options.nextId(), startMs, endMs, type: "magnifier", content: buildCaption(step), - position: { - x: clamp( - candidate.position.normalizedX * 100 - MAGNIFIER_SIZE / 2, - 0, - 100 - MAGNIFIER_SIZE, - ), - y: clamp( - candidate.position.normalizedY * 100 - MAGNIFIER_SIZE / 2, - 0, - 100 - MAGNIFIER_SIZE, - ), - }, + position: magnifierPosition, size: { width: MAGNIFIER_SIZE, height: MAGNIFIER_SIZE }, style: { ...DEFAULT_ANNOTATION_STYLE }, zIndex: options.nextZIndex(), @@ -160,10 +197,7 @@ export function buildGuideVideoAnnotations( endMs, type: "figure", content: "", - position: { - x: clamp(candidate.position.normalizedX * 100 - ARROW_SIZE / 2, 0, 100 - ARROW_SIZE), - y: clamp(candidate.position.normalizedY * 100 - ARROW_SIZE / 2, 0, 100 - ARROW_SIZE), - }, + position: arrowPosition, size: { width: ARROW_SIZE, height: ARROW_SIZE }, style: { ...DEFAULT_ANNOTATION_STYLE }, zIndex: options.nextZIndex(),