Fix ORA editor layer visibility and mask preview
- Layer visibility toggles now show/hide individual layer images on canvas - Added tint red checkbox per layer for manual mask verification - Mask preview modal uses CSS mask-image to show red overlay only on selected areas (white pixels become semi-transparent red, black pixels remain transparent) - Added spinner icons to Open and Extract Mask buttons during long operations
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: #374151; }
|
||||
::-webkit-scrollbar-thumb { background: #6B7280; border-radius: 4px; }
|
||||
.polygon-point { position: absolute; width: 8px; height: 8px; background: #0F0; border-radius: 50%; transform: translate(-50%, -50%); pointer-events: none; }
|
||||
.polygon-point { position: absolute; width: 20px; height: 20px; background: #FFFFFF; border: 3px solid #FF0000; border-radius: 50%; transform: translate(-50%, -50%); pointer-events: none; z-index: 100; }
|
||||
</style>
|
||||
|
||||
<!-- Alpine data component - MUST be defined before x-data directive -->
|
||||
@@ -26,15 +26,21 @@
|
||||
imageWidth: 800,
|
||||
imageHeight: 600,
|
||||
|
||||
// Display scale (percentage, default 25%)
|
||||
scale: 25,
|
||||
|
||||
// Selection state
|
||||
selectedLayer: null,
|
||||
|
||||
|
||||
// Polygon state
|
||||
isDrawing: false,
|
||||
polygonPoints: [],
|
||||
polygonColor: '#FF0000',
|
||||
polygonWidth: 2,
|
||||
polygonPreviewUrl: null,
|
||||
clickTimeout: null,
|
||||
pendingPoint: null,
|
||||
|
||||
// Mask extraction state
|
||||
maskSubject: '',
|
||||
@@ -184,13 +190,27 @@
|
||||
});
|
||||
},
|
||||
|
||||
startDrawing() {
|
||||
startDrawing() {
|
||||
console.log('[ORA EDITOR] Starting polygon drawing mode');
|
||||
this.isDrawing = true;
|
||||
this.polygonPoints = [];
|
||||
|
||||
// Reset click/double-click state for new drawing session
|
||||
canvasDoubleClickPending = false;
|
||||
|
||||
// Clear the preview image from previous drawings (hide old overlay)
|
||||
this.polygonPreviewUrl = null;
|
||||
|
||||
// Note: We don't clear backend storage here - let updatePolygonPreview() overwrite it later
|
||||
|
||||
// Setup canvas after it's created
|
||||
setTimeout(() => this.setupCanvas(), 50);
|
||||
setTimeout(() => {
|
||||
this.setupCanvas();
|
||||
const canvas = document.getElementById('polygonCanvas');
|
||||
if (canvas) {
|
||||
canvas.style.display = 'block';
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
|
||||
setupCanvas() {
|
||||
@@ -220,18 +240,74 @@
|
||||
y = Math.max(0, Math.min(1, y));
|
||||
|
||||
this.polygonPoints.push({ x, y });
|
||||
this.updatePolygonPreview();
|
||||
this.drawPolygonOnCanvas();
|
||||
},
|
||||
|
||||
async updatePolygonPreview() {
|
||||
drawPolygonOnCanvas() {
|
||||
const canvas = document.getElementById('polygonCanvas');
|
||||
if (!canvas) return;
|
||||
if (this.polygonPoints.length < 2) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (this.polygonPoints.length < 2) return;
|
||||
|
||||
// Draw line segments connecting points
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = this.polygonColor;
|
||||
ctx.lineWidth = this.polygonWidth;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
const startPoint = this.polygonPoints[0];
|
||||
ctx.moveTo(startPoint.x * canvas.width, startPoint.y * canvas.height);
|
||||
|
||||
for (let i = 1; i < this.polygonPoints.length; i++) {
|
||||
const point = this.polygonPoints[i];
|
||||
ctx.lineTo(point.x * canvas.width, point.y * canvas.height);
|
||||
}
|
||||
|
||||
// Close the polygon if we have 3+ points
|
||||
if (this.polygonPoints.length >= 3) {
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// Draw point markers
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
for (const point of this.polygonPoints) {
|
||||
const px = point.x * canvas.width;
|
||||
const py = point.y * canvas.height;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = this.polygonColor;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.arc(px, py, 6, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
},
|
||||
|
||||
async updatePolygonPreview() {
|
||||
console.log('[UPDATE] Updating preview with', this.polygonPoints.length, 'points');
|
||||
if (!this.oraPath || this.polygonPoints.length < 3) return;
|
||||
|
||||
const pointsToSend = [...this.polygonPoints]; // Make a copy
|
||||
console.log('[UPDATE] Sending points:', JSON.stringify(pointsToSend));
|
||||
|
||||
const response = await fetch('/api/polygon', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ora_path: this.oraPath,
|
||||
points: this.polygonPoints,
|
||||
points: pointsToSend,
|
||||
color: this.polygonColor,
|
||||
width: this.polygonWidth
|
||||
})
|
||||
@@ -239,24 +315,47 @@
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.polygonPreviewUrl = data.overlay_url;
|
||||
console.log('[UPDATE] Got preview URL:', data.overlay_url);
|
||||
this.polygonPreviewUrl = data.overlay_url + '&ts=' + Date.now(); // Bust cache
|
||||
}
|
||||
},
|
||||
|
||||
finishDrawing() {
|
||||
this.isDrawing = false;
|
||||
async finishDrawing() {
|
||||
console.log('[FINISH] finishDrawing called with', this.polygonPoints.length, 'points');
|
||||
|
||||
// Don't change isDrawing until after we've updated the preview!
|
||||
if (this.polygonPoints.length >= 3) {
|
||||
// Keep polygon visible
|
||||
await this.updatePolygonPreview();
|
||||
console.log('[FINISH] Preview URL set to:', this.polygonPreviewUrl);
|
||||
|
||||
// Now exit drawing mode - this will show the img with new URL
|
||||
this.isDrawing = false;
|
||||
|
||||
// Hide canvas
|
||||
const canvas = document.getElementById('polygonCanvas');
|
||||
if (canvas) {
|
||||
canvas.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
this.clearPolygon();
|
||||
}
|
||||
},
|
||||
|
||||
clearPolygon() {
|
||||
clearPolygon() {
|
||||
this.isDrawing = false;
|
||||
this.polygonPoints = [];
|
||||
this.polygonPreviewUrl = null;
|
||||
|
||||
// Hide canvas
|
||||
const canvas = document.getElementById('polygonCanvas');
|
||||
if (canvas) {
|
||||
canvas.style.display = 'none';
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.oraPath) {
|
||||
fetch('/api/polygon/clear', {
|
||||
method: 'POST',
|
||||
@@ -299,8 +398,7 @@
|
||||
this.showMaskModal = true;
|
||||
} catch (e) {
|
||||
console.error('[ORA EDITOR] Error extracting mask:', e);
|
||||
document.getElementById('maskError').innerHTML = e.message;
|
||||
document.getElementById('maskError').style.display = 'block';
|
||||
this.lastError = e.message;
|
||||
} finally {
|
||||
this.isExtracting = false;
|
||||
}
|
||||
@@ -383,8 +481,11 @@
|
||||
<button
|
||||
@click="openFile()"
|
||||
:disabled="!filePath || isLoading"
|
||||
class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-4 py-2 rounded transition"
|
||||
>Open</button>
|
||||
class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-4 py-2 rounded transition flex items-center justify-center gap-2"
|
||||
>
|
||||
<span x-show="isLoading" class="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-white"></span>
|
||||
<span x-text="isLoading ? 'Opening...' : 'Open'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="openInKrita()"
|
||||
@@ -401,6 +502,18 @@
|
||||
:disabled="!oraPath || isLoading"
|
||||
class="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-4 py-2 rounded transition"
|
||||
>Save</button>
|
||||
<div class="flex items-center gap-2 bg-gray-800 rounded px-3 py-2 border border-gray-700">
|
||||
<span class="text-sm text-gray-300">Scale:</span>
|
||||
<input
|
||||
type="range"
|
||||
x-model.number="scale"
|
||||
min="10"
|
||||
max="100"
|
||||
step="5"
|
||||
class="w-32 accent-blue-500"
|
||||
>
|
||||
<span class="text-sm text-gray-300 w-12" x-text="scale + '%'"></span>
|
||||
</div>
|
||||
<button
|
||||
@click="showSettings = true"
|
||||
class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded transition"
|
||||
@@ -430,8 +543,16 @@
|
||||
:checked="layer.visible"
|
||||
@change="toggleVisibility(layer.name, $event.target.checked)"
|
||||
class="w-4 h-4 rounded"
|
||||
title="Toggle visibility"
|
||||
>
|
||||
<span class="flex-1 text-sm truncate" x-text="layer.name"></span>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="layer.tintRed || false"
|
||||
@change="layer.tintRed = $event.target.checked"
|
||||
class="w-4 h-4 rounded"
|
||||
title="Tint red for preview"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -496,8 +617,9 @@
|
||||
<button
|
||||
@click="extractMask()"
|
||||
:disabled="!maskSubject.trim() || isExtracting || !oraPath"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-600 px-4 py-2 rounded transition"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-600 px-4 py-2 rounded transition flex items-center justify-center gap-2"
|
||||
>
|
||||
<span x-show="isExtracting" class="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-white"></span>
|
||||
<span x-show="!isExtracting">Extract Mask</span>
|
||||
<span x-show="isExtracting">Extracting...</span>
|
||||
</button>
|
||||
@@ -510,18 +632,25 @@
|
||||
<!-- Main canvas area -->
|
||||
<main class="flex-1 bg-gray-800 rounded-lg border border-gray-700 p-4 relative overflow-auto" style="min-height: 600px;">
|
||||
|
||||
<div id="imageContainer" x-show="!isLoading && !error && layers.length > 0" class="relative inline-block"
|
||||
:style="`width: ${imageWidth}px; height: ${imageHeight}px; position: relative;`">
|
||||
<div id="imageContainer" x-show="!isLoading && !error && layers.length > 0" class="relative inline-block origin-top-left"
|
||||
:style="`width: ${imageWidth}px; height: ${imageHeight}px; transform: scale(${scale / 100});`">
|
||||
|
||||
<!-- Layer display -->
|
||||
<div class="relative w-full h-full">
|
||||
<!-- Base image -->
|
||||
<img
|
||||
:src="'/api/image/base?ora_path=' + encodeURIComponent(oraPath)"
|
||||
class="absolute inset-0 w-full h-full object-contain"
|
||||
@load="initCanvas"
|
||||
id="baseImage"
|
||||
>
|
||||
<!-- Individual layer images (rendered in order) -->
|
||||
<template x-for="layer in layers" :key="'layer-' + layer.name">
|
||||
<img
|
||||
x-show="layer.visible"
|
||||
:src="'/api/image/layer/' + encodeURIComponent(layer.name) + '?ora_path=' + encodeURIComponent(oraPath)"
|
||||
class="absolute inset-0 w-full h-full object-contain"
|
||||
>
|
||||
<!-- Red tint overlay when enabled -->
|
||||
<div
|
||||
x-show="layer.visible && layer.tintRed"
|
||||
class="absolute inset-0 bg-red-500 mix-blend-multiply"
|
||||
style="opacity: 0.5;"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- Polygon points markers when drawing -->
|
||||
<template x-for="(point, idx) in polygonPoints" :key="idx">
|
||||
@@ -545,8 +674,8 @@
|
||||
:width="imageWidth"
|
||||
:height="imageHeight"
|
||||
class="absolute inset-0 cursor-crosshair pointer-events-auto border-2 border-dashed border-blue-500 opacity-90"
|
||||
@click="handleCanvasClick"
|
||||
@dblclick="handleCanvasDoubleClick"
|
||||
@click="handleCanvasClick($event)"
|
||||
@dblclick="handleCanvasDoubleClick($event)"
|
||||
></canvas>
|
||||
|
||||
</div>
|
||||
@@ -578,13 +707,21 @@
|
||||
<div class="bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4 border border-gray-600" style="max-height: 90vh; display: flex; flex-direction: column;">
|
||||
<h2 class="text-xl font-bold mb-4">Extracted Mask</h2>
|
||||
|
||||
<div class="flex-1 overflow-auto bg-gray-700 rounded mb-4 relative" :style="`min-height: 300px; max-height: 50vh;`">
|
||||
<!-- Base image -->
|
||||
<img :src="'/api/image/base?ora_path=' + encodeURIComponent(oraPath)" class="relative w-full h-auto object-contain">
|
||||
|
||||
<!-- Mask overlay with tint -->
|
||||
<div x-show="tempMaskUrl" class="absolute inset-0 bg-green-500 opacity-30 pointer-events-none">
|
||||
<img :src="tempMaskUrl" class="w-full h-auto object-contain mix-blend-screen">
|
||||
<div class="flex-1 overflow-auto bg-gray-700 rounded mb-4 flex items-center justify-center p-4" :style="`min-height: 300px; max-height: 50vh;`">
|
||||
<!-- Container for properly aligned base + mask -->
|
||||
<div class="relative inline-block">
|
||||
<!-- Base image -->
|
||||
<img
|
||||
:src="'/api/image/base?ora_path=' + encodeURIComponent(oraPath)"
|
||||
class="border border-gray-600"
|
||||
style="max-width: 100%; max-height: calc(50vh - 2rem);"
|
||||
>
|
||||
|
||||
<!-- Semi-transparent red overlay with mask applied as alpha channel -->
|
||||
<div x-show="tempMaskUrl"
|
||||
class="absolute inset-0 pointer-events-none border border-gray-600"
|
||||
:style="`background-color: rgba(255, 80, 80, 0.6); -webkit-mask-image: url(${tempMaskUrl}); mask-image: url(${tempMaskUrl}); -webkit-mask-size: cover; mask-size: cover; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;`">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -621,8 +758,10 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Minimal JS for canvas polygon drawing -->
|
||||
<!-- Canvas click handler - works with Alpine -->
|
||||
<script>
|
||||
let canvasDoubleClickPending = false;
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = document.getElementById('polygonCanvas');
|
||||
if (!canvas) return;
|
||||
@@ -632,39 +771,13 @@ function initCanvas() {
|
||||
canvas.style.height = canvas.parentElement.offsetHeight + 'px';
|
||||
}
|
||||
|
||||
// Access Alpine store for canvas click
|
||||
function handleCanvasClick(e) {
|
||||
const container = e.target.closest('[x-data]');
|
||||
if (!container) return;
|
||||
|
||||
// Get click position
|
||||
const rect = container.getBoundingClientRect();
|
||||
let x = (e.clientX - rect.left) / rect.width;
|
||||
let y = (e.clientY - rect.top) / rect.height;
|
||||
|
||||
// Normalize to 0-1 range
|
||||
x = Math.max(0, Math.min(1, x));
|
||||
y = Math.max(0, Math.min(1, y));
|
||||
|
||||
// Get Alpine component and add point
|
||||
const alpineComponent = $data(container);
|
||||
if (alpineComponent) {
|
||||
alpineComponent.addPolygonPoint(x, y);
|
||||
// Skip click if it was part of a double-click
|
||||
if (canvasDoubleClickPending) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCanvasDoubleClick(e) {
|
||||
e.preventDefault();
|
||||
const alpineComponent = $data(e.target.closest('[x-data]'));
|
||||
if (alpineComponent) {
|
||||
alpineComponent.finishDrawing();
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Canvas click handler - works with Alpine -->
|
||||
<script>
|
||||
function handleCanvasClick(e) {
|
||||
const container = e.target.closest('#imageContainer');
|
||||
|
||||
const container = document.getElementById('imageContainer');
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
@@ -679,8 +792,8 @@ function handleCanvasClick(e) {
|
||||
const alpineEl = container.closest('[x-data]');
|
||||
if (!alpineEl) return;
|
||||
|
||||
// Get the Alpine component instance using $data
|
||||
const store = $data(alpineEl);
|
||||
// Get Alpine component instance using Alpine.$data
|
||||
const store = Alpine.$data(alpineEl);
|
||||
if (!store) return;
|
||||
|
||||
store.addPolygonPoint(x, y);
|
||||
@@ -688,13 +801,16 @@ function handleCanvasClick(e) {
|
||||
|
||||
function handleCanvasDoubleClick(e) {
|
||||
e.preventDefault();
|
||||
const container = e.target.closest('#imageContainer');
|
||||
// Mark that a double-click happened so we skip the subsequent clicks
|
||||
canvasDoubleClickPending = true;
|
||||
|
||||
const container = document.getElementById('imageContainer');
|
||||
if (!container) return;
|
||||
|
||||
const alpineEl = container.closest('[x-data]');
|
||||
if (!alpineEl) return;
|
||||
|
||||
const store = $data(alpineEl);
|
||||
const store = Alpine.$data(alpineEl);
|
||||
if (!store) return;
|
||||
|
||||
if (store.isDrawing && store.polygonPoints.length >= 3) {
|
||||
@@ -702,16 +818,7 @@ function handleCanvasDoubleClick(e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for Alpine to initialize, then setup canvas
|
||||
document.addEventListener('alpine:init', () => {
|
||||
setTimeout(() => {
|
||||
const canvas = document.getElementById('polygonCanvas');
|
||||
if (canvas) {
|
||||
canvas.addEventListener('click', handleCanvasClick);
|
||||
canvas.addEventListener('dblclick', handleCanvasDoubleClick);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user