Files
2025-12-06 22:46:19 +00:00

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);
}
}
};
}