Adding manual image background

This commit is contained in:
Dan 2026-01-09 17:51:53 +00:00
parent f8d45f957a
commit 0d09907da1
7 changed files with 233 additions and 1328 deletions

View file

@ -37,12 +37,13 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
defineControls() {
return [
{
id: 'bg-image-url',
type: 'text',
label: 'Image URL',
id: 'bg-image-file',
type: 'file',
label: 'Image File',
defaultValue: '',
accept: 'image/*',
showWhen: 'bg-type',
description: 'URL of the image to use as background'
description: 'Select an image file from your computer'
},
{
id: 'bg-image-fit',
@ -53,11 +54,45 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
{ value: 'cover', label: 'Cover (fill, crop if needed)' },
{ value: 'contain', label: 'Contain (fit inside)' },
{ value: 'stretch', label: 'Stretch (may distort)' },
{ value: 'center', label: 'Center (actual size)' }
{ value: 'center', label: 'Center (actual size)' },
{ value: 'manual', label: 'Manual (custom zoom & position)' }
],
showWhen: 'bg-type',
description: 'How the image should fit in the canvas'
},
{
id: 'bg-image-zoom',
type: 'range',
label: 'Image Zoom',
defaultValue: 100,
min: 10,
max: 500,
step: 5,
showWhen: 'bg-image-fit',
description: 'Zoom level for manual positioning (percentage)'
},
{
id: 'bg-image-offset-x',
type: 'range',
label: 'Horizontal Position',
defaultValue: 50,
min: 0,
max: 100,
step: 1,
showWhen: 'bg-image-fit',
description: 'Horizontal position of the image (percentage)'
},
{
id: 'bg-image-offset-y',
type: 'range',
label: 'Vertical Position',
defaultValue: 50,
min: 0,
max: 100,
step: 1,
showWhen: 'bg-image-fit',
description: 'Vertical position of the image (percentage)'
},
{
id: 'bg-image-opacity',
type: 'range',
@ -77,24 +112,24 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
}
/**
* Start loading an image from a URL with caching
* Start loading an image from a blob URL with caching
* Triggers async loading but returns immediately
* @param {string} url - Image URL
* @param {string} blobUrl - Blob URL created from uploaded file
*/
startLoadingImage(url) {
startLoadingImage(blobUrl) {
// Skip if already cached or loading
if (this.imageCache.has(url) || this.loadingImages.has(url)) {
if (this.imageCache.has(blobUrl) || this.loadingImages.has(blobUrl)) {
return;
}
// Mark as loading to prevent duplicate requests
this.loadingImages.set(url, true);
this.loadingImages.set(blobUrl, true);
const img = new Image();
img.onload = () => {
this.imageCache.set(url, img);
this.loadingImages.delete(url);
this.imageCache.set(blobUrl, img);
this.loadingImages.delete(blobUrl);
// Trigger a redraw when image loads
const event = new CustomEvent('imageLoaded');
window.dispatchEvent(event);
@ -102,29 +137,29 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
img.onerror = () => {
// Cache the error state to prevent retry spam
this.imageCache.set(url, 'ERROR');
this.loadingImages.delete(url);
this.imageCache.set(blobUrl, 'ERROR');
this.loadingImages.delete(blobUrl);
// Trigger a redraw to show error state
const event = new CustomEvent('imageLoaded');
window.dispatchEvent(event);
};
img.src = url;
img.src = blobUrl;
}
/**
* Get cached image if available
* @param {string} url - Image URL
* @param {string} blobUrl - Blob URL
* @returns {HTMLImageElement|string|null} Image element, 'ERROR', or null
*/
getCachedImage(url) {
return this.imageCache.get(url) || null;
getCachedImage(blobUrl) {
return this.imageCache.get(blobUrl) || null;
}
/**
* Draw image with the specified fit mode
*/
drawImage(context, img, fitMode, opacity, width, height) {
drawImage(context, img, fitMode, opacity, width, height, zoom, offsetX, offsetY) {
context.globalAlpha = opacity;
const imgRatio = img.width / img.height;
@ -174,6 +209,31 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
drawY = (height - drawHeight) / 2;
break;
case 'manual':
// Manual positioning with zoom and offset controls
const zoomFactor = zoom / 100;
// Start with the larger dimension to ensure coverage
if (imgRatio > canvasRatio) {
// Image is wider relative to canvas
drawHeight = height * zoomFactor;
drawWidth = drawHeight * imgRatio;
} else {
// Image is taller relative to canvas
drawWidth = width * zoomFactor;
drawHeight = drawWidth / imgRatio;
}
// Calculate the range of movement (how far can we move the image)
const maxOffsetX = drawWidth - width;
const maxOffsetY = drawHeight - height;
// Convert percentage (0-100) to actual position
// 0% = image fully left/top, 100% = image fully right/bottom
drawX = -(maxOffsetX * offsetX / 100);
drawY = -(maxOffsetY * offsetY / 100);
break;
case 'stretch':
// Stretch to fill (default values already set)
break;
@ -184,12 +244,15 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
}
apply(context, controlValues, animState, renderData) {
const url = controlValues['bg-image-url'] || '';
const file = controlValues['bg-image-file'];
const fitMode = controlValues['bg-image-fit'] || 'cover';
const opacity = controlValues['bg-image-opacity'] ?? 1;
const zoom = controlValues['bg-image-zoom'] ?? 100;
const offsetX = controlValues['bg-image-offset-x'] ?? 50;
const offsetY = controlValues['bg-image-offset-y'] ?? 50;
// If no URL provided, fill with a placeholder color
if (!url.trim()) {
// If no file selected, fill with a placeholder color
if (!file || !file.blobUrl) {
context.fillStyle = '#cccccc';
context.fillRect(0, 0, renderData.width, renderData.height);
@ -198,16 +261,16 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
context.font = '8px Arial';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText('Enter image URL', renderData.centerX, renderData.centerY);
context.fillText('Select an image', renderData.centerX, renderData.centerY);
return;
}
// Start loading if not already cached or loading
const cachedImage = this.getCachedImage(url);
const cachedImage = this.getCachedImage(file.blobUrl);
if (!cachedImage) {
// Not cached yet - start loading
this.startLoadingImage(url);
this.startLoadingImage(file.blobUrl);
// Draw loading state
context.fillStyle = '#3498db';
@ -236,7 +299,7 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
}
// Image is loaded - draw it
this.drawImage(context, cachedImage, fitMode, opacity, renderData.width, renderData.height);
this.drawImage(context, cachedImage, fitMode, opacity, renderData.width, renderData.height, zoom, offsetX, offsetY);
}
}

View file

@ -26,7 +26,7 @@ export class SolidBackgroundEffect extends ButtonEffect {
{ value: "gradient", label: "Gradient" },
{ value: "texture", label: "Texture" },
{ value: "emoji-wallpaper", label: "Emoji Wallpaper" },
// { value: 'external-image', label: 'External Image' }
{ value: 'external-image', label: 'Image Upload' }
],
},
{