372 lines
12 KiB
JavaScript
372 lines
12 KiB
JavaScript
/**
|
|
* Background Image Manager Module
|
|
* Handles background image updates, overlays, and display management
|
|
*/
|
|
|
|
import {
|
|
darkenImage,
|
|
scaleImageToRefDimensions,
|
|
processBgImage,
|
|
createImageOverlayForConfigure
|
|
} from '../image_overlay.js';
|
|
import { clearBackgroundVideo, updateVideoBrightness, hasBackgroundVideo } from '../canvas/canvas_video_background.js';
|
|
import {
|
|
loadImageAsBase64,
|
|
getCachedRefImage,
|
|
safeSetSessionItem,
|
|
blobToBase64,
|
|
getImageDimensions,
|
|
loadImageAndExtractBase64
|
|
} from './image-cache.js';
|
|
|
|
/**
|
|
* Create background image manager instance for a node
|
|
* @param {Object} node - The PowerSplineEditor node instance
|
|
* @returns {Object} - Manager with methods for handling background images
|
|
*/
|
|
export function createBackgroundImageManager(node) {
|
|
return {
|
|
/**
|
|
* Update background image with current opacity
|
|
*/
|
|
async updateBackgroundImage() {
|
|
console.log('[updateBackgroundImage] Called');
|
|
|
|
// Get opacity value
|
|
const bgOpacityWidget = node.widgets?.find(w => w.name === "bg_opacity");
|
|
const opacity = bgOpacityWidget ? bgOpacityWidget.value : 1.0;
|
|
|
|
// Check if we have a video background instead of an image
|
|
if (node.editor && hasBackgroundVideo(node.editor)) {
|
|
console.log('[updateBackgroundImage] Video detected, updating video brightness');
|
|
updateVideoBrightness(node.editor, opacity);
|
|
return; // Don't process image when video is active
|
|
}
|
|
|
|
// No video, proceed with image processing
|
|
// Load reference image with opacity applied
|
|
await this._loadReferenceImageWithOpacity(opacity);
|
|
},
|
|
|
|
/**
|
|
* Load reference image and apply opacity (darkening)
|
|
*/
|
|
async _loadReferenceImageWithOpacity(opacity) {
|
|
try {
|
|
// Try to get the reference image from various sources
|
|
let refImageData = null;
|
|
|
|
if (node.originalRefImageData?.base64) {
|
|
// Use original ref image if available (from refresh button)
|
|
const imgType = node.originalRefImageData.type || 'image/jpeg';
|
|
refImageData = {
|
|
base64: node.originalRefImageData.base64,
|
|
type: imgType
|
|
};
|
|
} else {
|
|
// Try to load cached ref image
|
|
const cachedImageUrl = await this._loadCorrectCachedRefImage();
|
|
if (cachedImageUrl) {
|
|
// Convert URL to base64
|
|
refImageData = await this._urlToBase64(cachedImageUrl);
|
|
}
|
|
}
|
|
|
|
if (refImageData) {
|
|
// Apply opacity to the reference image
|
|
await this._applyOpacityToImage(refImageData.base64, refImageData.type, opacity);
|
|
} else {
|
|
console.warn('[_loadReferenceImageWithOpacity] No reference image found');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading reference image with opacity:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Apply opacity to base64 image (0.0 = black, 1.0 = full brightness)
|
|
*/
|
|
async _applyOpacityToImage(base64Data, imageType, opacity) {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Draw the image with the specified opacity
|
|
ctx.globalAlpha = opacity;
|
|
ctx.drawImage(img, 0, 0);
|
|
ctx.globalAlpha = 1.0; // Reset
|
|
|
|
const processedDataUrl = canvas.toDataURL('image/jpeg');
|
|
node.imgData = {
|
|
name: 'bg_image.png',
|
|
base64: processedDataUrl.split(',')[1],
|
|
type: 'image/jpeg'
|
|
};
|
|
|
|
node.editor?.refreshBackgroundImage?.();
|
|
resolve();
|
|
};
|
|
img.onerror = () => {
|
|
console.error('Error loading image for opacity adjustment');
|
|
reject(new Error('Failed to load image'));
|
|
};
|
|
img.src = `data:${imageType};base64,${base64Data}`;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Convert image URL to base64
|
|
*/
|
|
async _urlToBase64(imageUrl) {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
const response = await fetch(imageUrl);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch image: ${response.status}`);
|
|
}
|
|
const blob = await response.blob();
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
const dataUrl = reader.result;
|
|
const base64 = dataUrl.split(',')[1];
|
|
const type = blob.type || 'image/jpeg';
|
|
resolve({ base64, type });
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Load the correct cached ref image based on PrepareRefs connection
|
|
*/
|
|
async _loadCorrectCachedRefImage() {
|
|
const isConnectedToPrepareRefs =
|
|
node.checkIfConnectedToPrepareRefs?.() || false;
|
|
|
|
if (isConnectedToPrepareRefs) {
|
|
// Load bg_image_cl.png from ref folder
|
|
try {
|
|
const timestamp = Date.now();
|
|
const refImageUrl = new URL(
|
|
`../ref/bg_image_cl.png?t=${timestamp}`,
|
|
import.meta.url
|
|
).href;
|
|
const response = await fetch(refImageUrl);
|
|
if (response.ok) {
|
|
const blob = await response.blob();
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => resolve(reader.result);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load bg_image_cl.png from ref folder:', e);
|
|
}
|
|
return null;
|
|
} else {
|
|
// Load bg_image.png from bg folder (standard behavior)
|
|
return await this._loadCachedRefImageAsBase64();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load cached ref_image as base64 (local implementation)
|
|
*/
|
|
async _loadCachedRefImageAsBase64() {
|
|
try {
|
|
// First try to load from actual file in bg folder
|
|
const timestamp = Date.now();
|
|
const refImageUrl = new URL(
|
|
`../bg/bg_image.png?t=${timestamp}`,
|
|
import.meta.url
|
|
).href;
|
|
const response = await fetch(refImageUrl);
|
|
if (response.ok) {
|
|
const blob = await response.blob();
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => resolve(reader.result);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
}
|
|
|
|
// If file doesn't exist, try to load from sessionStorage cache
|
|
const cachedData = await getCachedRefImage();
|
|
if (cachedData) {
|
|
return `data:${cachedData.type};base64,${cachedData.base64}`;
|
|
}
|
|
|
|
return null;
|
|
} catch (e) {
|
|
console.error('Failed to load cached ref image as base64:', e);
|
|
try {
|
|
const cachedData = await getCachedRefImage();
|
|
return cachedData
|
|
? `data:${cachedData.type};base64,${cachedData.base64}`
|
|
: null;
|
|
} catch (fallbackError) {
|
|
console.error('Fallback cache also failed:', fallbackError);
|
|
return null;
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* Load background image from URL with optional scaling
|
|
*/
|
|
loadBackgroundImageFromUrl(imageUrl, imageName, targetWidth, targetHeight) {
|
|
loadImageAsBase64(imageUrl).then(dataUrl => {
|
|
if (!dataUrl) {
|
|
console.error(`Failed to load image from ${imageUrl}`);
|
|
return;
|
|
}
|
|
|
|
// Get current opacity from widget
|
|
const bgOpacityWidget = node.widgets?.find(w => w.name === "bg_opacity");
|
|
const opacity = bgOpacityWidget ? bgOpacityWidget.value : 1.0;
|
|
|
|
// If we have target dimensions, load and scale the image
|
|
if (targetWidth && targetHeight) {
|
|
// First, create an image to get the original dimensions
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
// Scale the image to match the target dimensions
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = targetWidth;
|
|
canvas.height = targetHeight;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Apply opacity while drawing
|
|
ctx.globalAlpha = opacity;
|
|
// Draw the original image scaled to the target dimensions
|
|
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
|
ctx.globalAlpha = 1.0; // Reset
|
|
|
|
// Convert back to base64
|
|
const scaledDataUrl = canvas.toDataURL('image/jpeg');
|
|
node.imgData = {
|
|
name: imageName,
|
|
base64: scaledDataUrl.split(',')[1],
|
|
type: 'image/jpeg'
|
|
};
|
|
node.editor?.refreshBackgroundImage?.();
|
|
|
|
// Update the widget values to match the new dimensions
|
|
// unless user has their own dims
|
|
const widthWidget = node.widgets?.find(
|
|
w => w.name === 'mask_width'
|
|
);
|
|
const heightWidget = node.widgets?.find(
|
|
w => w.name === 'mask_height'
|
|
);
|
|
const userDimsJson = node.uuid
|
|
? sessionStorage.getItem(`spline-editor-user-dims-${node.uuid}`)
|
|
: null;
|
|
const hasUserDims =
|
|
node.properties?.userAdjustedDims || !!userDimsJson;
|
|
|
|
if (!hasUserDims) {
|
|
if (widthWidget) widthWidget.value = targetWidth;
|
|
if (heightWidget) heightWidget.value = targetHeight;
|
|
}
|
|
|
|
// Update editor dimensions (respect user dims if set)
|
|
if (node.editor) {
|
|
const newW = hasUserDims && widthWidget
|
|
? Number(widthWidget.value)
|
|
: targetWidth;
|
|
const newH = hasUserDims && heightWidget
|
|
? Number(heightWidget.value)
|
|
: targetHeight;
|
|
|
|
node.editor.width = newW;
|
|
node.editor.height = newH;
|
|
|
|
if (node.editor.vis) {
|
|
node.editor.vis.width(newW);
|
|
node.editor.vis.height(newH);
|
|
node.editor.vis.render();
|
|
}
|
|
|
|
// Refresh the active layer to ensure proper rendering
|
|
if (node.layerManager?.activeWidget) {
|
|
node.editor.onActiveLayerChanged?.();
|
|
}
|
|
|
|
// Trigger a full refresh of the editor
|
|
if (node.editor.layerRenderer) {
|
|
node.editor.layerRenderer.render();
|
|
}
|
|
}
|
|
};
|
|
img.onerror = () => {
|
|
// If scaling fails, apply opacity to the original image
|
|
const img2 = new Image();
|
|
img2.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = img2.width;
|
|
canvas.height = img2.height;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.globalAlpha = opacity;
|
|
ctx.drawImage(img2, 0, 0);
|
|
ctx.globalAlpha = 1.0;
|
|
const processedDataUrl = canvas.toDataURL('image/jpeg');
|
|
node.imgData = {
|
|
name: imageName,
|
|
base64: processedDataUrl.split(',')[1],
|
|
type: 'image/jpeg'
|
|
};
|
|
node.editor?.refreshBackgroundImage?.();
|
|
};
|
|
img2.src = dataUrl;
|
|
};
|
|
img.src = dataUrl;
|
|
} else {
|
|
// No target dimensions, apply opacity to the original image
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.globalAlpha = opacity;
|
|
ctx.drawImage(img, 0, 0);
|
|
ctx.globalAlpha = 1.0;
|
|
const processedDataUrl = canvas.toDataURL('image/jpeg');
|
|
node.imgData = {
|
|
name: imageName,
|
|
base64: processedDataUrl.split(',')[1],
|
|
type: 'image/jpeg'
|
|
};
|
|
node.editor?.refreshBackgroundImage?.();
|
|
};
|
|
img.src = dataUrl;
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Initialize background image on first load
|
|
*/
|
|
async initializeBackgroundImage() {
|
|
try {
|
|
// Load initial image with default opacity
|
|
await this.updateBackgroundImage();
|
|
} catch (error) {
|
|
console.error('Error initializing background image:', error);
|
|
}
|
|
}
|
|
};
|
|
} |