Adjust guide video annotation timing
CI / Lint (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Test (push) Waiting to run
CI / Build (push) Waiting to run
Bump Nix package on release / bump (release) Waiting to run
Update Homebrew Cask / update-cask (release) Waiting to run

This commit is contained in:
huanld
2026-06-05 20:39:26 +07:00
parent ee69df9222
commit 5069354df3
4 changed files with 70 additions and 29 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "openscreen", "name": "openscreen",
"version": "1.4.11", "version": "1.4.12",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "openscreen", "name": "openscreen",
"version": "1.4.11", "version": "1.4.12",
"dependencies": { "dependencies": {
"@fix-webm-duration/fix": "^1.0.1", "@fix-webm-duration/fix": "^1.0.1",
"@pixi/filter-drop-shadow": "^5.2.0", "@pixi/filter-drop-shadow": "^5.2.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "openscreen", "name": "openscreen",
"private": true, "private": true,
"version": "1.4.11", "version": "1.4.12",
"type": "module", "type": "module",
"packageManager": "npm@10.9.4", "packageManager": "npm@10.9.4",
"engines": { "engines": {
+9 -2
View File
@@ -65,7 +65,11 @@ describe("buildGuideVideoAnnotations", () => {
startMs: 1200, startMs: 1200,
content: "1. Click Settings.", content: "1. Click Settings.",
}); });
expect(annotations[0]?.endMs).toBe(3200);
expect(annotations[0]?.position.x).toBeGreaterThan(20); 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({ expect(annotations[1]).toMatchObject({
id: "guide-video-2", id: "guide-video-2",
type: "magnifier", type: "magnifier",
@@ -79,10 +83,13 @@ describe("buildGuideVideoAnnotations", () => {
expect(annotations[2]).toMatchObject({ expect(annotations[2]).toMatchObject({
id: "guide-video-3", id: "guide-video-3",
type: "figure", type: "figure",
endMs: 3200,
figureData: { figureData: {
arrowDirection: "left",
color: "#34B27B", color: "#34B27B",
}, },
}); });
expect(annotations[2]?.position.x).toBeGreaterThan(20);
}); });
it("returns an empty list when no draft exists", () => { it("returns an empty list when no draft exists", () => {
@@ -97,7 +104,7 @@ describe("buildGuideVideoAnnotations", () => {
expect(annotations).toEqual([]); 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; let id = 1;
const speedRegions = buildGuideVideoSpeedRegions(createSession(), { const speedRegions = buildGuideVideoSpeedRegions(createSession(), {
nextId: () => `guide-speed-${id++}`, nextId: () => `guide-speed-${id++}`,
@@ -107,7 +114,7 @@ describe("buildGuideVideoAnnotations", () => {
{ {
id: "guide-speed-1", id: "guide-speed-1",
startMs: 1200, startMs: 1200,
endMs: 2200, endMs: 3200,
speed: 0.3, speed: 0.3,
}, },
]); ]);
+58 -24
View File
@@ -14,13 +14,14 @@ export interface BuildGuideVideoAnnotationsOptions {
defaultDurationMs?: number; defaultDurationMs?: number;
} }
const DEFAULT_STEP_DURATION_MS = 3200; const DEFAULT_STEP_DURATION_MS = 2000;
const DEFAULT_STEP_SLOW_MOTION_DURATION_MS = 1000; const DEFAULT_STEP_SLOW_MOTION_DURATION_MS = 2000;
const DEFAULT_STEP_SLOW_MOTION_SPEED = 0.3; const DEFAULT_STEP_SLOW_MOTION_SPEED = 0.3;
const CAPTION_WIDTH = 34; const CAPTION_WIDTH = 34;
const CAPTION_HEIGHT = 13; const CAPTION_HEIGHT = 13;
const MAGNIFIER_SIZE = 18; const MAGNIFIER_SIZE = 18;
const ARROW_SIZE = 10; const ARROW_SIZE = 10;
const ANNOTATION_GAP = 2;
function clamp(value: number, min: number, max: number) { function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value)); return Math.min(max, Math.max(min, value));
@@ -58,15 +59,19 @@ function getCaptionPosition(candidate: GuideStepCandidate | undefined) {
function getArrowDirection( function getArrowDirection(
candidate: GuideStepCandidate | undefined, 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 { ): ArrowDirection {
const target = candidate?.position; const target = candidate?.position;
if (!target) return "right"; if (!target) return "right";
const captionCenterX = captionPosition.x + CAPTION_WIDTH / 2; const originCenterX = originPosition.x + originSize.width / 2;
const captionCenterY = captionPosition.y + CAPTION_HEIGHT / 2; const originCenterY = originPosition.y + originSize.height / 2;
const dx = target.normalizedX * 100 - captionCenterX; const dx = target.normalizedX * 100 - originCenterX;
const dy = target.normalizedY * 100 - captionCenterY; const dy = target.normalizedY * 100 - originCenterY;
const horizontal = dx > 8 ? "right" : dx < -8 ? "left" : ""; const horizontal = dx > 8 ? "right" : dx < -8 ? "left" : "";
const vertical = dy > 8 ? "down" : dy < -8 ? "up" : ""; const vertical = dy > 8 ? "down" : dy < -8 ? "up" : "";
@@ -74,6 +79,40 @@ function getArrowDirection(
return (horizontal || vertical || "right") as ArrowDirection; 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<GuideStepCandidate["position"]>,
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) { function buildCaption(step: GeneratedGuideStep) {
const instruction = step.instruction.trim(); const instruction = step.instruction.trim();
const title = step.title.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 startMs = Math.max(0, Math.round(candidate?.timeMs ?? index * durationMs));
const endMs = Math.max(startMs + 750, startMs + durationMs); const endMs = Math.max(startMs + 750, startMs + durationMs);
const captionPosition = getCaptionPosition(candidate); const captionPosition = getCaptionPosition(candidate);
const arrowDirection = getArrowDirection(candidate, captionPosition);
annotations.push({ annotations.push({
id: options.nextId(), id: options.nextId(),
@@ -124,24 +162,23 @@ export function buildGuideVideoAnnotations(
}); });
if (candidate?.position) { 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({ annotations.push({
id: options.nextId(), id: options.nextId(),
startMs, startMs,
endMs, endMs,
type: "magnifier", type: "magnifier",
content: buildCaption(step), content: buildCaption(step),
position: { position: magnifierPosition,
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,
),
},
size: { width: MAGNIFIER_SIZE, height: MAGNIFIER_SIZE }, size: { width: MAGNIFIER_SIZE, height: MAGNIFIER_SIZE },
style: { ...DEFAULT_ANNOTATION_STYLE }, style: { ...DEFAULT_ANNOTATION_STYLE },
zIndex: options.nextZIndex(), zIndex: options.nextZIndex(),
@@ -160,10 +197,7 @@ export function buildGuideVideoAnnotations(
endMs, endMs,
type: "figure", type: "figure",
content: "", content: "",
position: { position: arrowPosition,
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),
},
size: { width: ARROW_SIZE, height: ARROW_SIZE }, size: { width: ARROW_SIZE, height: ARROW_SIZE },
style: { ...DEFAULT_ANNOTATION_STYLE }, style: { ...DEFAULT_ANNOTATION_STYLE },
zIndex: options.nextZIndex(), zIndex: options.nextZIndex(),