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
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:
Generated
+2
-2
@@ -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
@@ -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": {
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user