Adding more fonts

This commit is contained in:
Dan 2026-01-22 08:51:17 +00:00
parent f980d65c86
commit 9a73e6c94d
4 changed files with 139 additions and 83 deletions

View file

@ -1,4 +1,4 @@
@import url(https://fonts.bunny.net/css?family=bebas-neue:400|lato:400,400i,700,700i|montserrat:400,400i,700,700i|open-sans:400,400i,700,700i|oswald:400,700|press-start-2p:400|roboto:400,400i,700,700i|roboto-mono:400,400i,700,700i|vt323:400); @import url(https://fonts.bunny.net/css?family=barrio:400|bungee-spice:400|creepster:400|pixelify-sans:400,700|bebas-neue:400|lato:400,400i,700,700i|montserrat:400,400i,700,700i|open-sans:400,400i,700,700i|oswald:400,700|press-start-2p:400|roboto:400,400i,700,700i|roboto-mono:400,400i,700,700i|vt323:400);
#button-generator-app { #button-generator-app {
margin: 2rem 0; margin: 2rem 0;
@ -324,7 +324,6 @@
} }
} }
// Custom file input styling // Custom file input styling
input[type="file"] { input[type="file"] {
width: 100%; width: 100%;

View file

@ -1,6 +1,6 @@
import { ButtonEffect } from './effect-base.js'; import { ButtonEffect } from "./effect-base.js";
import { ColorQuantizer } from './color-quantizer.js'; import { ColorQuantizer } from "./color-quantizer.js";
/** /**
* Animation state class - passed to effects for frame-based rendering * Animation state class - passed to effects for frame-based rendering
@ -30,7 +30,7 @@ export class AnimationState {
export class ButtonGenerator { export class ButtonGenerator {
constructor(canvas, config = {}) { constructor(canvas, config = {}) {
this.canvas = canvas; this.canvas = canvas;
this.ctx = canvas.getContext('2d'); this.ctx = canvas.getContext("2d");
// Animation configuration // Animation configuration
this.animConfig = { this.animConfig = {
@ -38,7 +38,7 @@ export class ButtonGenerator {
duration: config.duration || 2, // seconds duration: config.duration || 2, // seconds
get totalFrames() { get totalFrames() {
return this.fps * this.duration; return this.fps * this.duration;
} },
}; };
// GIF export configuration // GIF export configuration
@ -55,7 +55,7 @@ export class ButtonGenerator {
border: [], border: [],
text: [], text: [],
text2: [], text2: [],
general: [] general: [],
}; };
// Registered effects by ID for quick lookup // Registered effects by ID for quick lookup
@ -69,8 +69,20 @@ export class ButtonGenerator {
// Font list for preloading // Font list for preloading
this.fonts = config.fonts || [ this.fonts = config.fonts || [
'Lato', 'Roboto', 'Open Sans', 'Montserrat', 'Oswald', "Lato",
'Bebas Neue', 'Roboto Mono', 'VT323', 'Press Start 2P', 'DSEG7-Classic' "Roboto",
"Open Sans",
"Montserrat",
"Oswald",
"Bebas Neue",
"Roboto Mono",
"VT323",
"Press Start 2P",
"DSEG7-Classic",
"Pixelify Sans",
"Bungee Spice",
"Creepster",
"Barrio",
]; ];
} }
@ -80,11 +92,13 @@ export class ButtonGenerator {
*/ */
registerEffect(effect) { registerEffect(effect) {
if (!(effect instanceof ButtonEffect)) { if (!(effect instanceof ButtonEffect)) {
throw new Error('Effect must extend ButtonEffect class'); throw new Error("Effect must extend ButtonEffect class");
} }
if (this.effectsById.has(effect.id)) { if (this.effectsById.has(effect.id)) {
console.warn(`Effect with ID "${effect.id}" is already registered. Skipping.`); console.warn(
`Effect with ID "${effect.id}" is already registered. Skipping.`,
);
return; return;
} }
@ -125,14 +139,14 @@ export class ButtonGenerator {
* @returns {Promise} * @returns {Promise}
*/ */
async preloadFonts() { async preloadFonts() {
const fontPromises = this.fonts.flatMap(font => [ const fontPromises = this.fonts.flatMap((font) => [
document.fonts.load(`400 12px "${font}"`), document.fonts.load(`400 12px "${font}"`),
document.fonts.load(`700 12px "${font}"`), document.fonts.load(`700 12px "${font}"`),
document.fonts.load(`italic 400 12px "${font}"`) document.fonts.load(`italic 400 12px "${font}"`),
]); ]);
await Promise.all(fontPromises); await Promise.all(fontPromises);
console.log('All fonts loaded for canvas'); console.log("All fonts loaded for canvas");
} }
/** /**
@ -144,28 +158,28 @@ export class ButtonGenerator {
// Get all registered control IDs from effects // Get all registered control IDs from effects
const allControls = new Set(); const allControls = new Set();
this.getAllEffects().forEach(effect => { this.getAllEffects().forEach((effect) => {
effect.controls.forEach(control => { effect.controls.forEach((control) => {
allControls.add(control.id); allControls.add(control.id);
}); });
}); });
// Read values from DOM // Read values from DOM
allControls.forEach(id => { allControls.forEach((id) => {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
if (element.type === 'checkbox') { if (element.type === "checkbox") {
values[id] = element.checked; values[id] = element.checked;
} else if (element.type === 'range' || element.type === 'number') { } else if (element.type === "range" || element.type === "number") {
values[id] = parseFloat(element.value); values[id] = parseFloat(element.value);
} else if (element.type === 'file') { } else if (element.type === "file") {
// For file inputs, return an object with file metadata and blob URL // For file inputs, return an object with file metadata and blob URL
if (element.dataset.blobUrl) { if (element.dataset.blobUrl) {
values[id] = { values[id] = {
fileName: element.dataset.fileName, fileName: element.dataset.fileName,
blobUrl: element.dataset.blobUrl, blobUrl: element.dataset.blobUrl,
fileSize: parseInt(element.dataset.fileSize), fileSize: parseInt(element.dataset.fileSize),
fileType: element.dataset.fileType fileType: element.dataset.fileType,
}; };
} else { } else {
values[id] = null; values[id] = null;
@ -200,21 +214,29 @@ export class ButtonGenerator {
width: this.canvas.width, width: this.canvas.width,
height: this.canvas.height, height: this.canvas.height,
centerX: this.canvas.width / 2, centerX: this.canvas.width / 2,
centerY: this.canvas.height / 2 centerY: this.canvas.height / 2,
}; };
// Apply effects in order: transform -> background -> background-animation -> text/text2 -> border -> general // Apply effects in order: transform -> background -> background-animation -> text/text2 -> border -> general
const renderOrder = ['transform', 'background', 'background-animation', 'text', 'text2', 'border', 'general']; const renderOrder = [
"transform",
"background",
"background-animation",
"text",
"text2",
"border",
"general",
];
// Save context once before transforms // Save context once before transforms
this.ctx.save(); this.ctx.save();
renderOrder.forEach(type => { renderOrder.forEach((type) => {
this.effects[type]?.forEach(effect => { this.effects[type]?.forEach((effect) => {
if (effect.canApply(controlValues)) { if (effect.canApply(controlValues)) {
// Transform effects should NOT be wrapped in save/restore // Transform effects should NOT be wrapped in save/restore
// They need to persist for all subsequent drawing operations // They need to persist for all subsequent drawing operations
if (type !== 'transform') { if (type !== "transform") {
this.ctx.save(); this.ctx.save();
} }
@ -224,7 +246,7 @@ export class ButtonGenerator {
console.error(`Error applying effect ${effect.id}:`, error); console.error(`Error applying effect ${effect.id}:`, error);
} }
if (type !== 'transform') { if (type !== "transform") {
this.ctx.restore(); this.ctx.restore();
} }
} }
@ -241,9 +263,10 @@ export class ButtonGenerator {
*/ */
hasAnimationsEnabled() { hasAnimationsEnabled() {
const controlValues = this.getControlValues(); const controlValues = this.getControlValues();
return this.getAllEffects().some(effect => return this.getAllEffects().some(
effect.type !== 'background' && // Background effects can be static (effect) =>
effect.isEnabled(controlValues) effect.type !== "background" && // Background effects can be static
effect.isEnabled(controlValues),
); );
} }
@ -264,7 +287,7 @@ export class ButtonGenerator {
const animState = new AnimationState( const animState = new AnimationState(
frameNum, frameNum,
this.animConfig.totalFrames, this.animConfig.totalFrames,
this.animConfig.fps this.animConfig.fps,
); );
this.draw(animState); this.draw(animState);
this.applyPreviewQuantization(); this.applyPreviewQuantization();
@ -308,7 +331,11 @@ export class ButtonGenerator {
applyPreviewQuantization() { applyPreviewQuantization() {
const colorCount = this.gifConfig.colorCount; const colorCount = this.gifConfig.colorCount;
if (colorCount < 256) { if (colorCount < 256) {
const quantizedData = ColorQuantizer.quantize(this.canvas, colorCount, 'floyd-steinberg'); const quantizedData = ColorQuantizer.quantize(
this.canvas,
colorCount,
"floyd-steinberg",
);
this.ctx.putImageData(quantizedData, 0, 0); this.ctx.putImageData(quantizedData, 0, 0);
} }
} }
@ -326,27 +353,34 @@ export class ButtonGenerator {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
// Create temporary canvas for frame generation // Create temporary canvas for frame generation
const frameCanvas = document.createElement('canvas'); const frameCanvas = document.createElement("canvas");
frameCanvas.width = this.canvas.width; frameCanvas.width = this.canvas.width;
frameCanvas.height = this.canvas.height; frameCanvas.height = this.canvas.height;
const frameCtx = frameCanvas.getContext('2d'); const frameCtx = frameCanvas.getContext("2d");
// Merge options with defaults // Merge options with defaults
const quality = options.quality !== undefined ? options.quality : this.gifConfig.quality; const quality =
const gifDither = options.dither !== undefined ? options.dither : this.gifConfig.dither; options.quality !== undefined
const colorCount = options.colorCount !== undefined ? options.colorCount : this.gifConfig.colorCount; ? options.quality
: this.gifConfig.quality;
const gifDither =
options.dither !== undefined ? options.dither : this.gifConfig.dither;
const colorCount =
options.colorCount !== undefined
? options.colorCount
: this.gifConfig.colorCount;
// Determine if we need custom quantization // Determine if we need custom quantization
const useCustomQuantization = colorCount < 256; const useCustomQuantization = colorCount < 256;
const customDither = useCustomQuantization ? 'floyd-steinberg' : false; const customDither = useCustomQuantization ? "floyd-steinberg" : false;
// Initialize gif.js // Initialize gif.js
const gifOptions = { const gifOptions = {
workers: 2, workers: 2,
quality: quality, quality: quality,
workerScript: '/js/gif.worker.js', workerScript: "/js/gif.worker.js",
width: this.canvas.width, width: this.canvas.width,
height: this.canvas.height height: this.canvas.height,
}; };
// Add gif.js dither option if specified (only when not using custom quantization) // Add gif.js dither option if specified (only when not using custom quantization)
@ -361,18 +395,22 @@ export class ButtonGenerator {
const generateFrames = async () => { const generateFrames = async () => {
for (let i = 0; i < totalFrames; i++) { for (let i = 0; i < totalFrames; i++) {
const animState = new AnimationState(i, totalFrames, this.animConfig.fps); const animState = new AnimationState(
i,
totalFrames,
this.animConfig.fps,
);
// Draw to temporary canvas // Draw to temporary canvas
frameCtx.clearRect(0, 0, frameCanvas.width, frameCanvas.height); frameCtx.clearRect(0, 0, frameCanvas.width, frameCanvas.height);
const tempGenerator = new ButtonGenerator(frameCanvas, { const tempGenerator = new ButtonGenerator(frameCanvas, {
fps: this.animConfig.fps, fps: this.animConfig.fps,
duration: this.animConfig.duration, duration: this.animConfig.duration,
fonts: this.fonts fonts: this.fonts,
}); });
// Copy effects // Copy effects
this.getAllEffects().forEach(effect => { this.getAllEffects().forEach((effect) => {
tempGenerator.registerEffect(effect); tempGenerator.registerEffect(effect);
}); });
@ -380,40 +418,43 @@ export class ButtonGenerator {
// Apply custom color quantization if needed // Apply custom color quantization if needed
if (useCustomQuantization) { if (useCustomQuantization) {
const quantizedData = ColorQuantizer.quantize(frameCanvas, colorCount, customDither); const quantizedData = ColorQuantizer.quantize(
frameCanvas,
colorCount,
customDither,
);
frameCtx.putImageData(quantizedData, 0, 0); frameCtx.putImageData(quantizedData, 0, 0);
} }
gif.addFrame(frameCtx, { gif.addFrame(frameCtx, {
delay: 1000 / this.animConfig.fps, delay: 1000 / this.animConfig.fps,
copy: true copy: true,
}); });
if (progressCallback) { if (progressCallback) {
progressCallback(i / totalFrames, 'generating'); progressCallback(i / totalFrames, "generating");
} }
// Yield to browser every 5 frames // Yield to browser every 5 frames
if (i % 5 === 0) { if (i % 5 === 0) {
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
} }
} }
}; };
generateFrames().then(() => { generateFrames().then(() => {
gif.on('finished', (blob) => { gif.on("finished", (blob) => {
resolve(blob); resolve(blob);
}); });
gif.on('progress', (progress) => { gif.on("progress", (progress) => {
if (progressCallback) { if (progressCallback) {
progressCallback(progress, 'encoding'); progressCallback(progress, "encoding");
} }
}); });
gif.render(); gif.render();
}); });
} catch (error) { } catch (error) {
reject(error); reject(error);
} }
@ -425,17 +466,17 @@ export class ButtonGenerator {
*/ */
bindControls() { bindControls() {
const allControls = new Set(); const allControls = new Set();
this.getAllEffects().forEach(effect => { this.getAllEffects().forEach((effect) => {
effect.controls.forEach(control => { effect.controls.forEach((control) => {
allControls.add(control.id); allControls.add(control.id);
}); });
}); });
allControls.forEach(id => { allControls.forEach((id) => {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
element.addEventListener('input', () => this.updatePreview()); element.addEventListener("input", () => this.updatePreview());
element.addEventListener('change', () => this.updatePreview()); element.addEventListener("change", () => this.updatePreview());
} }
}); });
} }

View file

@ -125,6 +125,10 @@ export class StandardTextEffect extends ButtonEffect {
{ value: "VT323", label: "VT323" }, { value: "VT323", label: "VT323" },
{ value: "Press Start 2P", label: "Press Start 2P" }, { value: "Press Start 2P", label: "Press Start 2P" },
{ value: "DSEG7-Classic", label: "DSEG7" }, { value: "DSEG7-Classic", label: "DSEG7" },
{ value: "Pixelify Sans", label: "Pixelify Sans" },
{ value: "Bungee Spice", label: "Bungee Spice" },
{ value: "Creepster", label: "Creepster" },
{ value: "Barrio", label: "Barrio" },
], ],
}, },
{ {
@ -154,7 +158,16 @@ export class StandardTextEffect extends ButtonEffect {
const bounceActive = controlValues[`animate-text-bounce${suffix}`]; const bounceActive = controlValues[`animate-text-bounce${suffix}`];
const glowActive = controlValues[`animate-text-glow${suffix}`]; const glowActive = controlValues[`animate-text-glow${suffix}`];
return text && text.trim() !== "" && !waveActive && !rainbowActive && !spinActive && !tickerActive && !bounceActive && !glowActive; return (
text &&
text.trim() !== "" &&
!waveActive &&
!rainbowActive &&
!spinActive &&
!tickerActive &&
!bounceActive &&
!glowActive
);
} }
apply(context, controlValues, animState, renderData) { apply(context, controlValues, animState, renderData) {

View file

@ -76,7 +76,6 @@ async function setupApp() {
fps: 20, fps: 20,
duration: 2, duration: 2,
fonts: [ fonts: [
"Arial",
"Lato", "Lato",
"Roboto", "Roboto",
"Open Sans", "Open Sans",
@ -87,6 +86,10 @@ async function setupApp() {
"VT323", "VT323",
"Press Start 2P", "Press Start 2P",
"DSEG7-Classic", "DSEG7-Classic",
"Pixelify Sans",
"Bungee Spice",
"Creepster",
"Barrio",
], ],
}); });
@ -169,21 +172,21 @@ async function setupApp() {
* Add GIF export settings controls * Add GIF export settings controls
*/ */
function addGifExportSettings(container, generator) { function addGifExportSettings(container, generator) {
const groupDiv = document.createElement('div'); const groupDiv = document.createElement("div");
groupDiv.className = 'control-group collapsed'; groupDiv.className = "control-group collapsed";
// Header // Header
const header = document.createElement('h3'); const header = document.createElement("h3");
header.className = 'control-group-header'; header.className = "control-group-header";
header.textContent = 'Advanced Settings'; header.textContent = "Advanced Settings";
groupDiv.appendChild(header); groupDiv.appendChild(header);
const controlsDiv = document.createElement('div'); const controlsDiv = document.createElement("div");
controlsDiv.className = 'control-group-controls'; controlsDiv.className = "control-group-controls";
// Color count control // Color count control
const colorsWrapper = document.createElement('div'); const colorsWrapper = document.createElement("div");
colorsWrapper.className = 'control-wrapper'; colorsWrapper.className = "control-wrapper";
colorsWrapper.innerHTML = ` colorsWrapper.innerHTML = `
<div class="info-text">Note: This only affects frame-by-frame settings, i.e. 8 colours would be 8 colours per frame. I am working on a solution for this.</div> <div class="info-text">Note: This only affects frame-by-frame settings, i.e. 8 colours would be 8 colours per frame. I am working on a solution for this.</div>
<label for="gif-colors"> <label for="gif-colors">
@ -198,8 +201,8 @@ function addGifExportSettings(container, generator) {
controlsDiv.appendChild(colorsWrapper); controlsDiv.appendChild(colorsWrapper);
// Quality control // Quality control
const qualityWrapper = document.createElement('div'); const qualityWrapper = document.createElement("div");
qualityWrapper.className = 'control-wrapper'; qualityWrapper.className = "control-wrapper";
qualityWrapper.innerHTML = ` qualityWrapper.innerHTML = `
<label for="gif-quality"> <label for="gif-quality">
Quantization Quality Quantization Quality
@ -213,8 +216,8 @@ function addGifExportSettings(container, generator) {
controlsDiv.appendChild(qualityWrapper); controlsDiv.appendChild(qualityWrapper);
// Dither control // Dither control
const ditherWrapper = document.createElement('div'); const ditherWrapper = document.createElement("div");
ditherWrapper.className = 'control-wrapper'; ditherWrapper.className = "control-wrapper";
ditherWrapper.innerHTML = ` ditherWrapper.innerHTML = `
<label for="gif-dither"> <label for="gif-dither">
Dithering Pattern Dithering Pattern
@ -234,38 +237,38 @@ function addGifExportSettings(container, generator) {
container.appendChild(groupDiv); container.appendChild(groupDiv);
// Setup event listeners for value display // Setup event listeners for value display
const colorsInput = document.getElementById('gif-colors'); const colorsInput = document.getElementById("gif-colors");
const colorsValue = document.getElementById('gif-colors-value'); const colorsValue = document.getElementById("gif-colors-value");
colorsInput.addEventListener('input', () => { colorsInput.addEventListener("input", () => {
colorsValue.textContent = colorsInput.value; colorsValue.textContent = colorsInput.value;
}); });
const qualityInput = document.getElementById('gif-quality'); const qualityInput = document.getElementById("gif-quality");
const qualityValue = document.getElementById('gif-quality-value'); const qualityValue = document.getElementById("gif-quality-value");
qualityInput.addEventListener('input', () => { qualityInput.addEventListener("input", () => {
qualityValue.textContent = qualityInput.value; qualityValue.textContent = qualityInput.value;
}); });
// Update generator config when changed // Update generator config when changed
colorsInput.addEventListener('change', () => { colorsInput.addEventListener("change", () => {
generator.gifConfig.colorCount = parseInt(colorsInput.value); generator.gifConfig.colorCount = parseInt(colorsInput.value);
generator.updatePreview(); // Update preview to show color quantization generator.updatePreview(); // Update preview to show color quantization
}); });
// Also update preview on slider input for real-time feedback // Also update preview on slider input for real-time feedback
colorsInput.addEventListener('input', () => { colorsInput.addEventListener("input", () => {
generator.gifConfig.colorCount = parseInt(colorsInput.value); generator.gifConfig.colorCount = parseInt(colorsInput.value);
generator.updatePreview(); // Real-time preview update generator.updatePreview(); // Real-time preview update
}); });
qualityInput.addEventListener('change', () => { qualityInput.addEventListener("change", () => {
generator.gifConfig.quality = parseInt(qualityInput.value); generator.gifConfig.quality = parseInt(qualityInput.value);
}); });
const ditherSelect = document.getElementById('gif-dither'); const ditherSelect = document.getElementById("gif-dither");
ditherSelect.addEventListener('change', () => { ditherSelect.addEventListener("change", () => {
const value = ditherSelect.value; const value = ditherSelect.value;
generator.gifConfig.dither = value === 'false' ? false : value; generator.gifConfig.dither = value === "false" ? false : value;
}); });
} }