Files
bookhoard/web/src/reader/comic/chapter-markers.ts
T
john-okeefe c36d8e6b57 Add comic panel detection system with multi-tier fallback
- panel-detection.service.ts: Main orchestration with OpenCV → ML → Grid fallback chain
- panel-detection.opencv.ts: Edge detection using OpenCV for 80% of comics
- panel-detection.ml.ts: COCO-SSD object detection for irregular layouts
- panel-detector.ts: Unified detector interface
- panel-editor.ts: Manual panel editor UI for user corrections
- panel-ml-detector.ts: TensorFlow.js integration for ML detection
- page-cache.ts: Efficient page caching for large comics
- background-color.ts: Auto-detect comic background color
- chapter-markers.ts: Chapter detection and navigation
- page-scrubber.ts: Fast page scrubbing/thumbnails
- page-order.ts: RTL/LTR page ordering support
- panel-gap.ts: Panel gap detection
2026-04-04 01:00:43 -04:00

148 lines
3.6 KiB
TypeScript

// Chapter markers for manga/comics
// Visual indicators for chapter boundaries
// Procedural implementation (no OOP)
interface ChapterInfo {
chapterNumber: number;
pageStart: number;
pageEnd: number;
title?: string;
}
interface ChapterMarkerState {
chapters: ChapterInfo[];
currentChapter: number;
showMarkers: boolean;
}
function createChapterMarkerState(
chapters: ChapterInfo[],
currentPage: number,
): ChapterMarkerState {
const currentChapter =
chapters.find((c) => currentPage >= c.pageStart && currentPage <= c.pageEnd)
?.chapterNumber || 1;
return {
chapters,
currentChapter,
showMarkers: true,
};
}
function renderChapterMarkers(
container: HTMLElement,
state: ChapterMarkerState,
): void {
if (!state.showMarkers) return;
const markersContainer = document.createElement("div");
markersContainer.className =
"chapter-markers absolute left-0 right-0 pointer-events-none z-10";
state.chapters.forEach((chapter) => {
const marker = document.createElement("div");
marker.className =
"chapter-marker flex items-center gap-2 text-sm text-gray-400";
const isCurrentChapter = chapter.chapterNumber === state.currentChapter;
marker.style.position = "absolute";
marker.style.top = `${((chapter.pageStart - 1) / 100) * 100}%`;
marker.style.left = "10px";
marker.innerHTML = `
<span class="chapter-number ${isCurrentChapter ? "text-blue-400 font-bold" : ""}">
${chapter.title || `Chapter ${chapter.chapterNumber}`}
</span>
<span class="page-number text-xs">p.${chapter.pageStart}</span>
${isCurrentChapter ? '<span class="current-indicator">←</span>' : ""}
`;
markersContainer.appendChild(marker);
});
const existing = container.querySelector(".chapter-markers");
existing?.remove();
container.appendChild(markersContainer);
}
function updateCurrentChapter(
state: ChapterMarkerState,
currentPage: number,
): ChapterMarkerState {
const currentChapter =
state.chapters.find(
(c) => currentPage >= c.pageStart && currentPage <= c.pageEnd,
)?.chapterNumber || state.currentChapter;
if (currentChapter !== state.currentChapter) {
const newState = { ...state, currentChapter };
const markers = document.querySelector(".chapter-markers");
if (markers) {
renderChapterMarkers(markers.parentElement!, newState);
}
return newState;
}
return state;
}
function toggleChapterMarkers(state: ChapterMarkerState): ChapterMarkerState {
const newState = { ...state, showMarkers: !state.showMarkers };
const markers = document.querySelector(".chapter-markers");
if (markers) {
markers.classList.toggle("hidden", !newState.showMarkers);
}
return newState;
}
function scrollToChapter(
state: ChapterMarkerState,
chapterNumber: number,
): void {
const chapter = state.chapters.find((c) => c.chapterNumber === chapterNumber);
if (chapter) {
window.dispatchEvent(
new CustomEvent("navigate-to-page", {
detail: { page: chapter.pageStart },
}),
);
}
}
const chapterMarkerCSS = `
.chapter-marker {
padding: 4px 8px;
margin-left: -18px;
opacity: 0.7;
transition: opacity 0.2s;
}
.chapter-marker:hover {
opacity: 1;
}
.chapter-marker .current-indicator {
color: #3b82f6;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.chapter-marker-line {
position: absolute;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(to right, rgba(255,255,255,0.1), transparent);
}
`;