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
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
// 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);
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user