Add click-to-jump feature for speakers and fix start offset logic
- Add findNextPhraseForSpeaker() to find next segment for a speaker - Add findSpeakerAtPosition() for hit detection on speaker circles - Click on speaker circle jumps to their next phrase (with wrap-around) - Change cursor to pointer when hovering over speakers - Simplify audio start offset logic in loadedmetadata handler - Add SPEAKER_22 alias mapping to SPEAKER_21
This commit is contained in:
78
app.js
78
app.js
@@ -286,6 +286,53 @@ function findActiveSpeaker(time) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findNextPhraseForSpeaker(speakerId, afterTime) {
|
||||
// Find the next segment where this speaker talks after the given time
|
||||
for (const segment of state.segments) {
|
||||
if (resolveSpeaker(segment.speaker) === speakerId && segment.start > afterTime) {
|
||||
return segment.start;
|
||||
}
|
||||
}
|
||||
// Wrap around: if no phrase found after current time, find first phrase from beginning
|
||||
for (const segment of state.segments) {
|
||||
if (resolveSpeaker(segment.speaker) === speakerId) {
|
||||
return segment.start;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findSpeakerAtPosition(clientX, clientY) {
|
||||
const rect = spaceCanvas.getBoundingClientRect();
|
||||
const x = clientX - rect.left;
|
||||
const y = clientY - rect.top;
|
||||
|
||||
// Check each speaker, starting from the one with largest hit area (active speaker)
|
||||
// Sort by whether they're active and by size to check larger ones first
|
||||
const sortedSpeakers = [...state.speakers].sort((a, b) => {
|
||||
const aActive = a.id === state.activeSpeaker;
|
||||
const bActive = b.id === state.activeSpeaker;
|
||||
if (aActive !== bActive) return bActive - aActive;
|
||||
return b.baseSize - a.baseSize;
|
||||
});
|
||||
|
||||
for (const speaker of sortedSpeakers) {
|
||||
const isActive = speaker.id === state.activeSpeaker;
|
||||
const size = speaker.baseSize * (isActive ? 1.4 : 1);
|
||||
// Use a slightly larger hit area for better UX
|
||||
const hitRadius = size * 1.5;
|
||||
|
||||
const dx = x - speaker.x;
|
||||
const dy = y - speaker.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance <= hitRadius) {
|
||||
return speaker;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resizeCanvas(canvas, ctx) {
|
||||
const { width, height } = canvas.getBoundingClientRect();
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
@@ -671,6 +718,28 @@ async function init() {
|
||||
attachScrubHandlers();
|
||||
attachEmbeddedScrubHandlers();
|
||||
|
||||
// Click on speaker to jump to their next phrase
|
||||
spaceCanvas.addEventListener("click", (event) => {
|
||||
const speaker = findSpeakerAtPosition(event.clientX, event.clientY);
|
||||
if (speaker) {
|
||||
const nextTime = findNextPhraseForSpeaker(speaker.id, audio.currentTime);
|
||||
if (nextTime !== null) {
|
||||
audio.currentTime = nextTime;
|
||||
// Start playing if not already
|
||||
if (audio.paused) {
|
||||
audio.play().catch(() => {});
|
||||
playPause.textContent = "Pause";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Change cursor when hovering over a speaker
|
||||
spaceCanvas.addEventListener("mousemove", (event) => {
|
||||
const speaker = findSpeakerAtPosition(event.clientX, event.clientY);
|
||||
spaceCanvas.style.cursor = speaker ? "pointer" : "default";
|
||||
});
|
||||
|
||||
// Load waveform data immediately on page load
|
||||
loadWaveformData();
|
||||
|
||||
@@ -717,12 +786,9 @@ async function init() {
|
||||
playPause.textContent = "Play";
|
||||
});
|
||||
audio.addEventListener("loadedmetadata", () => {
|
||||
// Start at minute 10 by default
|
||||
if (audio.currentTime < START_OFFSET_SECONDS && audio.duration >= START_OFFSET_SECONDS) {
|
||||
audio.currentTime = START_OFFSET_SECONDS;
|
||||
}
|
||||
if (Number.isFinite(audio.duration) && audio.duration > 0) {
|
||||
audio.currentTime = Math.min(START_OFFSET_SECONDS, Math.max(0, audio.duration - 0.01));
|
||||
// Start at configured offset, but don't exceed audio duration
|
||||
if (Number.isFinite(audio.duration) && audio.duration > 0 && START_OFFSET_SECONDS > 0) {
|
||||
audio.currentTime = Math.min(START_OFFSET_SECONDS, audio.duration - 1);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user