adding advanced settings
This commit is contained in:
parent
5a004981a9
commit
b27f9cfb39
3 changed files with 423 additions and 5 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
import { ButtonEffect } from './effect-base.js';
|
import { ButtonEffect } from './effect-base.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
|
||||||
*/
|
*/
|
||||||
|
|
@ -7,7 +9,7 @@ export class AnimationState {
|
||||||
constructor(frameNumber = 0, totalFrames = 40, fps = 20) {
|
constructor(frameNumber = 0, totalFrames = 40, fps = 20) {
|
||||||
this.frame = frameNumber;
|
this.frame = frameNumber;
|
||||||
this.totalFrames = totalFrames;
|
this.totalFrames = totalFrames;
|
||||||
this.progress = frameNumber / totalFrames; // 0 to 1
|
this.progress = totalFrames > 1 ? frameNumber / (totalFrames - 1) : 0; // 0 to 1, inclusive of last frame
|
||||||
this.fps = fps;
|
this.fps = fps;
|
||||||
this.time = (frameNumber / fps) * 1000; // milliseconds
|
this.time = (frameNumber / fps) * 1000; // milliseconds
|
||||||
}
|
}
|
||||||
|
|
@ -39,6 +41,13 @@ export class ButtonGenerator {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// GIF export configuration
|
||||||
|
this.gifConfig = {
|
||||||
|
quality: config.gifQuality || 1, // 1-30, lower is better quality quantization
|
||||||
|
dither: config.gifDither || false, // false, 'FloydSteinberg', 'FalseFloydSteinberg', 'Stucki', 'Atkinson'
|
||||||
|
colorCount: config.gifColorCount || 256, // 2-256, number of colors to reduce to (custom quantization)
|
||||||
|
};
|
||||||
|
|
||||||
// Effect registry organized by type
|
// Effect registry organized by type
|
||||||
this.effects = {
|
this.effects = {
|
||||||
transform: [],
|
transform: [],
|
||||||
|
|
@ -250,6 +259,7 @@ export class ButtonGenerator {
|
||||||
this.animConfig.fps
|
this.animConfig.fps
|
||||||
);
|
);
|
||||||
this.draw(animState);
|
this.draw(animState);
|
||||||
|
this.applyPreviewQuantization();
|
||||||
|
|
||||||
frameNum = (frameNum + 1) % this.animConfig.totalFrames;
|
frameNum = (frameNum + 1) % this.animConfig.totalFrames;
|
||||||
lastFrameTime = currentTime - (elapsed % frameDelay);
|
lastFrameTime = currentTime - (elapsed % frameDelay);
|
||||||
|
|
@ -280,15 +290,31 @@ export class ButtonGenerator {
|
||||||
} else {
|
} else {
|
||||||
this.stopAnimatedPreview();
|
this.stopAnimatedPreview();
|
||||||
this.draw();
|
this.draw();
|
||||||
|
this.applyPreviewQuantization();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply color quantization to preview if enabled
|
||||||
|
*/
|
||||||
|
applyPreviewQuantization() {
|
||||||
|
const colorCount = this.gifConfig.colorCount;
|
||||||
|
if (colorCount < 256) {
|
||||||
|
const quantizedData = ColorQuantizer.quantize(this.canvas, colorCount, 'floyd-steinberg');
|
||||||
|
this.ctx.putImageData(quantizedData, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export as animated GIF
|
* Export as animated GIF
|
||||||
* @param {Function} progressCallback - Called with progress (0-1)
|
* @param {Function} progressCallback - Called with progress (0-1)
|
||||||
|
* @param {Object} options - Export options
|
||||||
|
* @param {number} options.quality - Quality (1-30, lower is better, default: 10)
|
||||||
|
* @param {boolean|string} options.dither - Dithering algorithm for gif.js
|
||||||
|
* @param {number} options.colorCount - Number of colors (2-256, default: 256) - uses custom quantization
|
||||||
* @returns {Promise<Blob>}
|
* @returns {Promise<Blob>}
|
||||||
*/
|
*/
|
||||||
async exportAsGif(progressCallback = null) {
|
async exportAsGif(progressCallback = null, options = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
// Create temporary canvas for frame generation
|
// Create temporary canvas for frame generation
|
||||||
|
|
@ -297,14 +323,30 @@ export class ButtonGenerator {
|
||||||
frameCanvas.height = this.canvas.height;
|
frameCanvas.height = this.canvas.height;
|
||||||
const frameCtx = frameCanvas.getContext('2d');
|
const frameCtx = frameCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Merge options with defaults
|
||||||
|
const quality = options.quality !== undefined ? 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
|
||||||
|
const useCustomQuantization = colorCount < 256;
|
||||||
|
const customDither = useCustomQuantization ? 'floyd-steinberg' : false;
|
||||||
|
|
||||||
// Initialize gif.js
|
// Initialize gif.js
|
||||||
const gif = new GIF({
|
const gifOptions = {
|
||||||
workers: 2,
|
workers: 2,
|
||||||
quality: 10,
|
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)
|
||||||
|
if (!useCustomQuantization && gifDither !== false) {
|
||||||
|
gifOptions.dither = gifDither;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gif = new GIF(gifOptions);
|
||||||
|
|
||||||
// Generate frames
|
// Generate frames
|
||||||
const totalFrames = this.animConfig.totalFrames;
|
const totalFrames = this.animConfig.totalFrames;
|
||||||
|
|
@ -328,6 +370,12 @@ export class ButtonGenerator {
|
||||||
|
|
||||||
tempGenerator.draw(animState);
|
tempGenerator.draw(animState);
|
||||||
|
|
||||||
|
// Apply custom color quantization if needed
|
||||||
|
if (useCustomQuantization) {
|
||||||
|
const quantizedData = ColorQuantizer.quantize(frameCanvas, colorCount, customDither);
|
||||||
|
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
|
||||||
|
|
|
||||||
263
static/js/button-generator/color-quantizer.js
Normal file
263
static/js/button-generator/color-quantizer.js
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
/**
|
||||||
|
* Color Quantizer - Custom color reduction with median cut algorithm
|
||||||
|
* Reduces canvas colors with optional dithering for retro aesthetic
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ColorQuantizer {
|
||||||
|
/**
|
||||||
|
* Reduce colors in a canvas using median cut algorithm
|
||||||
|
* @param {HTMLCanvasElement} canvas - Canvas to quantize
|
||||||
|
* @param {number} colorCount - Target number of colors (2-256)
|
||||||
|
* @param {string|boolean} dither - Dithering algorithm ('floyd-steinberg', false)
|
||||||
|
* @returns {ImageData} Quantized image data
|
||||||
|
*/
|
||||||
|
static quantize(canvas, colorCount = 256, dither = false) {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const pixels = imageData.data;
|
||||||
|
|
||||||
|
if (colorCount >= 256) {
|
||||||
|
return imageData; // No quantization needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build palette using median cut
|
||||||
|
const palette = this.buildPalette(pixels, colorCount);
|
||||||
|
|
||||||
|
// Apply palette to image
|
||||||
|
if (dither === 'floyd-steinberg') {
|
||||||
|
this.applyPaletteWithDithering(pixels, palette, canvas.width, canvas.height);
|
||||||
|
} else {
|
||||||
|
this.applyPalette(pixels, palette);
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build color palette using median cut algorithm
|
||||||
|
*/
|
||||||
|
static buildPalette(pixels, colorCount) {
|
||||||
|
// Collect unique colors
|
||||||
|
const colorMap = new Map();
|
||||||
|
for (let i = 0; i < pixels.length; i += 4) {
|
||||||
|
const r = pixels[i];
|
||||||
|
const g = pixels[i + 1];
|
||||||
|
const b = pixels[i + 2];
|
||||||
|
const a = pixels[i + 3];
|
||||||
|
|
||||||
|
if (a === 0) continue; // Skip transparent pixels
|
||||||
|
|
||||||
|
const key = (r << 16) | (g << 8) | b;
|
||||||
|
colorMap.set(key, (colorMap.get(key) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to array of color objects with counts
|
||||||
|
const colors = Array.from(colorMap.entries()).map(([key, count]) => ({
|
||||||
|
r: (key >> 16) & 0xFF,
|
||||||
|
g: (key >> 8) & 0xFF,
|
||||||
|
b: key & 0xFF,
|
||||||
|
count: count
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If we have fewer colors than target, return as-is
|
||||||
|
if (colors.length <= colorCount) {
|
||||||
|
return colors.map(c => [c.r, c.g, c.b]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with all colors in one bucket
|
||||||
|
let buckets = [colors];
|
||||||
|
|
||||||
|
// Split buckets until we have desired number
|
||||||
|
while (buckets.length < colorCount) {
|
||||||
|
// Find bucket with largest range
|
||||||
|
let maxRange = -1;
|
||||||
|
let maxBucketIdx = 0;
|
||||||
|
let maxChannel = 'r';
|
||||||
|
|
||||||
|
buckets.forEach((bucket, idx) => {
|
||||||
|
if (bucket.length <= 1) return;
|
||||||
|
|
||||||
|
const ranges = this.getColorRanges(bucket);
|
||||||
|
const range = Math.max(ranges.r, ranges.g, ranges.b);
|
||||||
|
|
||||||
|
if (range > maxRange) {
|
||||||
|
maxRange = range;
|
||||||
|
maxBucketIdx = idx;
|
||||||
|
if (ranges.r >= ranges.g && ranges.r >= ranges.b) maxChannel = 'r';
|
||||||
|
else if (ranges.g >= ranges.b) maxChannel = 'g';
|
||||||
|
else maxChannel = 'b';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (maxRange === -1) break; // Can't split further
|
||||||
|
|
||||||
|
// Split the bucket
|
||||||
|
const bucket = buckets[maxBucketIdx];
|
||||||
|
bucket.sort((a, b) => a[maxChannel] - b[maxChannel]);
|
||||||
|
|
||||||
|
const mid = Math.floor(bucket.length / 2);
|
||||||
|
const bucket1 = bucket.slice(0, mid);
|
||||||
|
const bucket2 = bucket.slice(mid);
|
||||||
|
|
||||||
|
buckets.splice(maxBucketIdx, 1, bucket1, bucket2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Average colors in each bucket to create palette
|
||||||
|
return buckets.map(bucket => {
|
||||||
|
let totalWeight = 0;
|
||||||
|
let sumR = 0, sumG = 0, sumB = 0;
|
||||||
|
|
||||||
|
bucket.forEach(color => {
|
||||||
|
const weight = color.count;
|
||||||
|
totalWeight += weight;
|
||||||
|
sumR += color.r * weight;
|
||||||
|
sumG += color.g * weight;
|
||||||
|
sumB += color.b * weight;
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
Math.round(sumR / totalWeight),
|
||||||
|
Math.round(sumG / totalWeight),
|
||||||
|
Math.round(sumB / totalWeight)
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color ranges in a bucket
|
||||||
|
*/
|
||||||
|
static getColorRanges(bucket) {
|
||||||
|
let minR = 255, maxR = 0;
|
||||||
|
let minG = 255, maxG = 0;
|
||||||
|
let minB = 255, maxB = 0;
|
||||||
|
|
||||||
|
bucket.forEach(color => {
|
||||||
|
minR = Math.min(minR, color.r);
|
||||||
|
maxR = Math.max(maxR, color.r);
|
||||||
|
minG = Math.min(minG, color.g);
|
||||||
|
maxG = Math.max(maxG, color.g);
|
||||||
|
minB = Math.min(minB, color.b);
|
||||||
|
maxB = Math.max(maxB, color.b);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
r: maxR - minR,
|
||||||
|
g: maxG - minG,
|
||||||
|
b: maxB - minB
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find nearest color in palette
|
||||||
|
*/
|
||||||
|
static findNearestColor(r, g, b, palette) {
|
||||||
|
let minDist = Infinity;
|
||||||
|
let nearest = palette[0];
|
||||||
|
|
||||||
|
for (const color of palette) {
|
||||||
|
const dr = r - color[0];
|
||||||
|
const dg = g - color[1];
|
||||||
|
const db = b - color[2];
|
||||||
|
const dist = dr * dr + dg * dg + db * db;
|
||||||
|
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
nearest = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply palette without dithering
|
||||||
|
*/
|
||||||
|
static applyPalette(pixels, palette) {
|
||||||
|
for (let i = 0; i < pixels.length; i += 4) {
|
||||||
|
const r = pixels[i];
|
||||||
|
const g = pixels[i + 1];
|
||||||
|
const b = pixels[i + 2];
|
||||||
|
const a = pixels[i + 3];
|
||||||
|
|
||||||
|
if (a === 0) continue;
|
||||||
|
|
||||||
|
const nearest = this.findNearestColor(r, g, b, palette);
|
||||||
|
pixels[i] = nearest[0];
|
||||||
|
pixels[i + 1] = nearest[1];
|
||||||
|
pixels[i + 2] = nearest[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply palette with Floyd-Steinberg dithering
|
||||||
|
*/
|
||||||
|
static applyPaletteWithDithering(pixels, palette, width, height) {
|
||||||
|
// Create error buffer
|
||||||
|
const errors = new Float32Array(width * height * 3);
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const idx = (y * width + x) * 4;
|
||||||
|
const errIdx = (y * width + x) * 3;
|
||||||
|
|
||||||
|
const r = pixels[idx];
|
||||||
|
const g = pixels[idx + 1];
|
||||||
|
const b = pixels[idx + 2];
|
||||||
|
const a = pixels[idx + 3];
|
||||||
|
|
||||||
|
if (a === 0) continue;
|
||||||
|
|
||||||
|
// Add accumulated error
|
||||||
|
const newR = Math.max(0, Math.min(255, r + errors[errIdx]));
|
||||||
|
const newG = Math.max(0, Math.min(255, g + errors[errIdx + 1]));
|
||||||
|
const newB = Math.max(0, Math.min(255, b + errors[errIdx + 2]));
|
||||||
|
|
||||||
|
// Find nearest palette color
|
||||||
|
const nearest = this.findNearestColor(newR, newG, newB, palette);
|
||||||
|
|
||||||
|
// Set pixel to nearest color
|
||||||
|
pixels[idx] = nearest[0];
|
||||||
|
pixels[idx + 1] = nearest[1];
|
||||||
|
pixels[idx + 2] = nearest[2];
|
||||||
|
|
||||||
|
// Calculate error
|
||||||
|
const errR = newR - nearest[0];
|
||||||
|
const errG = newG - nearest[1];
|
||||||
|
const errB = newB - nearest[2];
|
||||||
|
|
||||||
|
// Distribute error to neighboring pixels (Floyd-Steinberg)
|
||||||
|
// Right pixel (x+1, y): 7/16
|
||||||
|
if (x + 1 < width) {
|
||||||
|
const rightIdx = (y * width + (x + 1)) * 3;
|
||||||
|
errors[rightIdx] += errR * 7 / 16;
|
||||||
|
errors[rightIdx + 1] += errG * 7 / 16;
|
||||||
|
errors[rightIdx + 2] += errB * 7 / 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom-left pixel (x-1, y+1): 3/16
|
||||||
|
if (y + 1 < height && x > 0) {
|
||||||
|
const blIdx = ((y + 1) * width + (x - 1)) * 3;
|
||||||
|
errors[blIdx] += errR * 3 / 16;
|
||||||
|
errors[blIdx + 1] += errG * 3 / 16;
|
||||||
|
errors[blIdx + 2] += errB * 3 / 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom pixel (x, y+1): 5/16
|
||||||
|
if (y + 1 < height) {
|
||||||
|
const bottomIdx = ((y + 1) * width + x) * 3;
|
||||||
|
errors[bottomIdx] += errR * 5 / 16;
|
||||||
|
errors[bottomIdx + 1] += errG * 5 / 16;
|
||||||
|
errors[bottomIdx + 2] += errB * 5 / 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom-right pixel (x+1, y+1): 1/16
|
||||||
|
if (y + 1 < height && x + 1 < width) {
|
||||||
|
const brIdx = ((y + 1) * width + (x + 1)) * 3;
|
||||||
|
errors[brIdx] += errR * 1 / 16;
|
||||||
|
errors[brIdx + 1] += errG * 1 / 16;
|
||||||
|
errors[brIdx + 2] += errB * 1 / 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -126,6 +126,9 @@ async function setupApp() {
|
||||||
uiBuilder.buildUI(generator.getAllEffects());
|
uiBuilder.buildUI(generator.getAllEffects());
|
||||||
uiBuilder.setupConditionalVisibility();
|
uiBuilder.setupConditionalVisibility();
|
||||||
|
|
||||||
|
// Add GIF export settings at the bottom
|
||||||
|
addGifExportSettings(controlsContainer, generator);
|
||||||
|
|
||||||
// Preload fonts
|
// Preload fonts
|
||||||
console.log("Loading fonts...");
|
console.log("Loading fonts...");
|
||||||
await generator.preloadFonts();
|
await generator.preloadFonts();
|
||||||
|
|
@ -148,6 +151,110 @@ async function setupApp() {
|
||||||
console.log("Button Generator ready!");
|
console.log("Button Generator ready!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add GIF export settings controls
|
||||||
|
*/
|
||||||
|
function addGifExportSettings(container, generator) {
|
||||||
|
const groupDiv = document.createElement('div');
|
||||||
|
groupDiv.className = 'control-group collapsed';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const header = document.createElement('h3');
|
||||||
|
header.className = 'control-group-header';
|
||||||
|
header.textContent = 'Advanced Settings';
|
||||||
|
groupDiv.appendChild(header);
|
||||||
|
|
||||||
|
const controlsDiv = document.createElement('div');
|
||||||
|
controlsDiv.className = 'control-group-controls';
|
||||||
|
|
||||||
|
// Color count control
|
||||||
|
const colorsWrapper = document.createElement('div');
|
||||||
|
colorsWrapper.className = 'control-wrapper';
|
||||||
|
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>
|
||||||
|
<label for="gif-colors">
|
||||||
|
Color Count
|
||||||
|
<span class="control-description">(16-32 for the 90s experience)</span>
|
||||||
|
</label>
|
||||||
|
<div class="range-container">
|
||||||
|
<input type="range" id="gif-colors" min="2" max="256" step="1" value="256">
|
||||||
|
<span id="gif-colors-value" class="range-value">256</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
controlsDiv.appendChild(colorsWrapper);
|
||||||
|
|
||||||
|
// Quality control
|
||||||
|
const qualityWrapper = document.createElement('div');
|
||||||
|
qualityWrapper.className = 'control-wrapper';
|
||||||
|
qualityWrapper.innerHTML = `
|
||||||
|
<label for="gif-quality">
|
||||||
|
Quantization Quality
|
||||||
|
<span class="control-description">(lower = better but slower export)</span>
|
||||||
|
</label>
|
||||||
|
<div class="range-container">
|
||||||
|
<input type="range" id="gif-quality" min="1" max="30" step="1" value="1">
|
||||||
|
<span id="gif-quality-value" class="range-value">1</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
controlsDiv.appendChild(qualityWrapper);
|
||||||
|
|
||||||
|
// Dither control
|
||||||
|
const ditherWrapper = document.createElement('div');
|
||||||
|
ditherWrapper.className = 'control-wrapper';
|
||||||
|
ditherWrapper.innerHTML = `
|
||||||
|
<label for="gif-dither">
|
||||||
|
Dithering Pattern
|
||||||
|
<span class="control-description"></span>
|
||||||
|
</label>
|
||||||
|
<select id="gif-dither">
|
||||||
|
<option value="false">None (Smooth)</option>
|
||||||
|
<option value="FloydSteinberg">Floyd-Steinberg (Classic)</option>
|
||||||
|
<option value="FalseFloydSteinberg">False Floyd-Steinberg (Fast)</option>
|
||||||
|
<option value="Stucki">Stucki (Distributed)</option>
|
||||||
|
<option value="Atkinson">Atkinson (Retro Mac)</option>
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
controlsDiv.appendChild(ditherWrapper);
|
||||||
|
|
||||||
|
groupDiv.appendChild(controlsDiv);
|
||||||
|
container.appendChild(groupDiv);
|
||||||
|
|
||||||
|
// Setup event listeners for value display
|
||||||
|
const colorsInput = document.getElementById('gif-colors');
|
||||||
|
const colorsValue = document.getElementById('gif-colors-value');
|
||||||
|
colorsInput.addEventListener('input', () => {
|
||||||
|
colorsValue.textContent = colorsInput.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const qualityInput = document.getElementById('gif-quality');
|
||||||
|
const qualityValue = document.getElementById('gif-quality-value');
|
||||||
|
qualityInput.addEventListener('input', () => {
|
||||||
|
qualityValue.textContent = qualityInput.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update generator config when changed
|
||||||
|
colorsInput.addEventListener('change', () => {
|
||||||
|
generator.gifConfig.colorCount = parseInt(colorsInput.value);
|
||||||
|
generator.updatePreview(); // Update preview to show color quantization
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also update preview on slider input for real-time feedback
|
||||||
|
colorsInput.addEventListener('input', () => {
|
||||||
|
generator.gifConfig.colorCount = parseInt(colorsInput.value);
|
||||||
|
generator.updatePreview(); // Real-time preview update
|
||||||
|
});
|
||||||
|
|
||||||
|
qualityInput.addEventListener('change', () => {
|
||||||
|
generator.gifConfig.quality = parseInt(qualityInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ditherSelect = document.getElementById('gif-dither');
|
||||||
|
ditherSelect.addEventListener('change', () => {
|
||||||
|
const value = ditherSelect.value;
|
||||||
|
generator.gifConfig.dither = value === 'false' ? false : value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup collapsible section functionality
|
* Setup collapsible section functionality
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue