Added ability to load in external image
This commit is contained in:
parent
c82cfa1d23
commit
39e72f56f9
4 changed files with 259 additions and 2 deletions
248
static/js/button-generator/effects/background-external-image.js
Normal file
248
static/js/button-generator/effects/background-external-image.js
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import { ButtonEffect } from '../effect-base.js';
|
||||
|
||||
/**
|
||||
* External Image Background Effect
|
||||
* Loads an external image from a URL and displays it as the background
|
||||
*/
|
||||
export class ExternalImageBackgroundEffect extends ButtonEffect {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'bg-external-image',
|
||||
name: 'External Image Background',
|
||||
type: 'background',
|
||||
category: 'Background',
|
||||
renderOrder: 1
|
||||
});
|
||||
|
||||
// Cache for loaded images
|
||||
this.imageCache = new Map();
|
||||
this.loadingImages = new Map();
|
||||
this.generator = null;
|
||||
|
||||
// Set up event listener for image loads
|
||||
this.boundImageLoadHandler = this.handleImageLoad.bind(this);
|
||||
window.addEventListener('imageLoaded', this.boundImageLoadHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle image load event
|
||||
*/
|
||||
handleImageLoad() {
|
||||
// Trigger a redraw if we have a generator reference
|
||||
if (this.generator) {
|
||||
this.generator.updatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
defineControls() {
|
||||
return [
|
||||
{
|
||||
id: 'bg-image-url',
|
||||
type: 'text',
|
||||
label: 'Image URL',
|
||||
defaultValue: '',
|
||||
showWhen: 'bg-type',
|
||||
description: 'URL of the image to use as background'
|
||||
},
|
||||
{
|
||||
id: 'bg-image-fit',
|
||||
type: 'select',
|
||||
label: 'Image Fit',
|
||||
defaultValue: 'cover',
|
||||
options: [
|
||||
{ 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)' }
|
||||
],
|
||||
showWhen: 'bg-type',
|
||||
description: 'How the image should fit in the canvas'
|
||||
},
|
||||
{
|
||||
id: 'bg-image-opacity',
|
||||
type: 'range',
|
||||
label: 'Image Opacity',
|
||||
defaultValue: 1,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
showWhen: 'bg-type',
|
||||
description: 'Transparency of the background image'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
isEnabled(controlValues) {
|
||||
return controlValues['bg-type'] === 'external-image';
|
||||
}
|
||||
|
||||
/**
|
||||
* Start loading an image from a URL with caching
|
||||
* Triggers async loading but returns immediately
|
||||
* @param {string} url - Image URL
|
||||
*/
|
||||
startLoadingImage(url) {
|
||||
// Skip if already cached or loading
|
||||
if (this.imageCache.has(url) || this.loadingImages.has(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as loading to prevent duplicate requests
|
||||
this.loadingImages.set(url, true);
|
||||
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
this.imageCache.set(url, img);
|
||||
this.loadingImages.delete(url);
|
||||
// Trigger a redraw when image loads
|
||||
const event = new CustomEvent('imageLoaded');
|
||||
window.dispatchEvent(event);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
// Cache the error state to prevent retry spam
|
||||
this.imageCache.set(url, 'ERROR');
|
||||
this.loadingImages.delete(url);
|
||||
// Trigger a redraw to show error state
|
||||
const event = new CustomEvent('imageLoaded');
|
||||
window.dispatchEvent(event);
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached image if available
|
||||
* @param {string} url - Image URL
|
||||
* @returns {HTMLImageElement|string|null} Image element, 'ERROR', or null
|
||||
*/
|
||||
getCachedImage(url) {
|
||||
return this.imageCache.get(url) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw image with the specified fit mode
|
||||
*/
|
||||
drawImage(context, img, fitMode, opacity, width, height) {
|
||||
context.globalAlpha = opacity;
|
||||
|
||||
const imgRatio = img.width / img.height;
|
||||
const canvasRatio = width / height;
|
||||
|
||||
let drawX = 0;
|
||||
let drawY = 0;
|
||||
let drawWidth = width;
|
||||
let drawHeight = height;
|
||||
|
||||
switch (fitMode) {
|
||||
case 'cover':
|
||||
// Fill the canvas, cropping if necessary
|
||||
if (imgRatio > canvasRatio) {
|
||||
// Image is wider, fit height
|
||||
drawHeight = height;
|
||||
drawWidth = height * imgRatio;
|
||||
drawX = (width - drawWidth) / 2;
|
||||
} else {
|
||||
// Image is taller, fit width
|
||||
drawWidth = width;
|
||||
drawHeight = width / imgRatio;
|
||||
drawY = (height - drawHeight) / 2;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'contain':
|
||||
// Fit inside the canvas, showing all of the image
|
||||
if (imgRatio > canvasRatio) {
|
||||
// Image is wider, fit width
|
||||
drawWidth = width;
|
||||
drawHeight = width / imgRatio;
|
||||
drawY = (height - drawHeight) / 2;
|
||||
} else {
|
||||
// Image is taller, fit height
|
||||
drawHeight = height;
|
||||
drawWidth = height * imgRatio;
|
||||
drawX = (width - drawWidth) / 2;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'center':
|
||||
// Center the image at actual size
|
||||
drawWidth = img.width;
|
||||
drawHeight = img.height;
|
||||
drawX = (width - drawWidth) / 2;
|
||||
drawY = (height - drawHeight) / 2;
|
||||
break;
|
||||
|
||||
case 'stretch':
|
||||
// Stretch to fill (default values already set)
|
||||
break;
|
||||
}
|
||||
|
||||
context.drawImage(img, drawX, drawY, drawWidth, drawHeight);
|
||||
context.globalAlpha = 1; // Reset alpha
|
||||
}
|
||||
|
||||
apply(context, controlValues, animState, renderData) {
|
||||
const url = controlValues['bg-image-url'] || '';
|
||||
const fitMode = controlValues['bg-image-fit'] || 'cover';
|
||||
const opacity = controlValues['bg-image-opacity'] ?? 1;
|
||||
|
||||
// If no URL provided, fill with a placeholder color
|
||||
if (!url.trim()) {
|
||||
context.fillStyle = '#cccccc';
|
||||
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||
|
||||
// Draw placeholder text
|
||||
context.fillStyle = '#666666';
|
||||
context.font = '8px Arial';
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
context.fillText('Enter image URL', renderData.centerX, renderData.centerY);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start loading if not already cached or loading
|
||||
const cachedImage = this.getCachedImage(url);
|
||||
|
||||
if (!cachedImage) {
|
||||
// Not cached yet - start loading
|
||||
this.startLoadingImage(url);
|
||||
|
||||
// Draw loading state
|
||||
context.fillStyle = '#3498db';
|
||||
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||
|
||||
context.fillStyle = '#ffffff';
|
||||
context.font = '6px Arial';
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
context.fillText('Loading...', renderData.centerX, renderData.centerY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cachedImage === 'ERROR') {
|
||||
// Failed to load
|
||||
context.fillStyle = '#ff6b6b';
|
||||
context.fillRect(0, 0, renderData.width, renderData.height);
|
||||
|
||||
context.fillStyle = '#ffffff';
|
||||
context.font = '6px Arial';
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
context.fillText('Failed to load', renderData.centerX, renderData.centerY - 4);
|
||||
context.fillText('image', renderData.centerX, renderData.centerY + 4);
|
||||
return;
|
||||
}
|
||||
|
||||
// Image is loaded - draw it
|
||||
this.drawImage(context, cachedImage, fitMode, opacity, renderData.width, renderData.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-register effect
|
||||
export function register(generator) {
|
||||
const effect = new ExternalImageBackgroundEffect();
|
||||
effect.generator = generator; // Store reference for redraws
|
||||
generator.registerEffect(effect);
|
||||
}
|
||||
|
|
@ -25,7 +25,8 @@ export class SolidBackgroundEffect extends ButtonEffect {
|
|||
{ value: 'solid', label: 'Solid Color' },
|
||||
{ value: 'gradient', label: 'Gradient' },
|
||||
{ value: 'texture', label: 'Texture' },
|
||||
{ value: 'emoji-wallpaper', label: 'Emoji Wallpaper' }
|
||||
{ value: 'emoji-wallpaper', label: 'Emoji Wallpaper' },
|
||||
{ value: 'external-image', label: 'External Image' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import * as solidBg from "./effects/background-solid.js";
|
|||
import * as gradientBg from "./effects/background-gradient.js";
|
||||
import * as textureBg from "./effects/background-texture.js";
|
||||
import * as emojiWallpaper from "./effects/background-emoji-wallpaper.js";
|
||||
import * as externalImage from "./effects/background-external-image.js";
|
||||
import * as rainbowBg from "./effects/background-rainbow.js";
|
||||
import * as rain from "./effects/background-rain.js";
|
||||
import * as starfield from "./effects/background-starfield.js";
|
||||
|
|
@ -88,6 +89,7 @@ async function setupApp() {
|
|||
gradientBg.register(generator);
|
||||
textureBg.register(generator);
|
||||
emojiWallpaper.register(generator);
|
||||
externalImage.register(generator);
|
||||
rainbowBg.register(generator);
|
||||
rain.register(generator);
|
||||
starfield.register(generator);
|
||||
|
|
|
|||
|
|
@ -418,7 +418,11 @@ export class UIBuilder {
|
|||
input.type = 'text';
|
||||
input.id = id;
|
||||
input.value = defaultValue || '';
|
||||
|
||||
// Only set maxLength for text inputs that aren't URLs
|
||||
if (id !== 'bg-image-url') {
|
||||
input.maxLength = 20;
|
||||
}
|
||||
|
||||
container.appendChild(labelEl);
|
||||
container.appendChild(input);
|
||||
|
|
@ -464,6 +468,8 @@ export class UIBuilder {
|
|||
control.style.display = triggerControl.value === 'texture' ? 'block' : 'none';
|
||||
} else if (controlId && (controlId.startsWith('emoji-') || controlId === 'emoji-text')) {
|
||||
control.style.display = triggerControl.value === 'emoji-wallpaper' ? 'block' : 'none';
|
||||
} else if (controlId && (controlId.startsWith('bg-image-') || controlId === 'bg-image-url' || controlId === 'bg-image-fit' || controlId === 'bg-image-opacity')) {
|
||||
control.style.display = triggerControl.value === 'external-image' ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
// For text color controls
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue