You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Applies a color gradient to an image and generates an audio "sonification"
* based on the original image's brightness, controlled by a play button.
* The output is a self-contained HTML element with the processed image and audio player.
*
* @param {Image} originalImg The original JavaScript Image object.
* @param {string} gradientColors A comma-separated string of CSS colors for the gradient (e.g., "red,yellow,#0000FF").
* @param {string} scanDirection The direction to scan for sonification ('horizontal' or 'vertical').
* @param {number} duration The duration of the generated audio in seconds.
* @param {number} minFreq The minimum frequency of the audio in Hz.
* @param {number} maxFreq The maximum frequency of the audio in Hz.
* @returns {Promise<HTMLDivElement>} A promise that resolves to a DIV element containing the canvas and a play button.
*/
async function processImage(originalImg, gradientColors = 'black,white', scanDirection = 'horizontal', duration = 5, minFreq = 220, maxFreq = 1200) {
// --- Helper Functions ---
/**
* Parses a CSS color string into an [R, G, B] array.
* @param {string} colorStr - e.g., 'red', '#FF0000', 'rgb(255,0,0)'
* @returns {number[]} - An array [r, g, b]
*/
const parseColor = (colorStr) => {
const ctx = document.createElement('canvas').getContext('2d');
ctx.fillStyle = colorStr.trim();
const hex = ctx.fillStyle; // Will be in #rrggbb format
return [
parseInt(hex.slice(1, 3), 16),
parseInt(hex.slice(3, 5), 16),
parseInt(hex.slice(5, 7), 16),
];
};
/**
* Linearly interpolates between two numbers.
* @param {number} a - Start value.
* @param {number} b - End value.
* @param {number} t - Interpolation factor (0-1).
* @returns {number} The interpolated value.
*/
const lerp = (a, b, t) => a * (1 - t) + b * t;
// --- Main Setup ---
// 1. Create a container for the canvas and button
const container = document.createElement('div');
container.style.position = 'relative';
container.style.display = 'inline-block';
container.style.lineHeight = '0'; // Removes extra space below canvas
// 2. Setup canvas and draw the gradient-mapped image
const canvas = document.createElement('canvas');
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0);
// --- Gradient Mapping ---
// 3. Create a 256-color gradient lookup table from the input string
const colors = gradientColors.split(',').map(parseColor);
const gradientMap = [];
if (colors.length === 1) {
for (let i = 0; i < 256; i++) gradientMap.push(colors[0]);
} else {
for (let i = 0; i < 256; i++) {
const t = i / 255;
const segment = (colors.length - 1) * t;
const startIndex = Math.floor(segment);
const endIndex = Math.min(startIndex + 1, colors.length - 1);
const segmentT = segment - startIndex;
const startColor = colors[startIndex];
const endColor = colors[endIndex];
const r = lerp(startColor[0], endColor[0], segmentT);
const g = lerp(startColor[1], endColor[1], segmentT);
const b = lerp(startColor[2], endColor[2], segmentT);
gradientMap.push([r, g, b]);
}
}
// 4. Apply the gradient map to the image on the canvas
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const brightness = Math.floor((data[i] + data[i + 1] + data[i + 2]) / 3);
const newColor = gradientMap[brightness];
data[i] = newColor[0];
data[i + 1] = newColor[1];
data[i + 2] = newColor[2];
}
ctx.putImageData(imageData, 0, 0);
container.appendChild(canvas);
// --- Audio Generation & UI ---
// 5. Create the play button
const playButton = document.createElement('button');
playButton.textContent = '▶ Play Sonification';
Object.assign(playButton.style, {
position: 'absolute',
bottom: '10px',
left: '50%',
transform: 'translateX(-50%)',
padding: '10px 20px',
border: '1px solid white',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
color: 'white',
borderRadius: '5px',
cursor: 'pointer',
fontSize: '1em',
fontFamily: 'sans-serif',
textShadow: '1px 1px 2px black',
backdropFilter: 'blur(2px)'
});
let audioContext = null;
playButton.onclick = () => {
// If audio is currently playing, stop it.
if (audioContext && audioContext.state === 'running') {
audioContext.close();
audioContext = null;
playButton.textContent = '▶ Play Sonification';
return;
}
// Create a new AudioContext (must be done on user interaction)
audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
const now = audioContext.currentTime;
// Sonify based on the *original* image's brightness, not the new gradient
const originalCtx = document.createElement('canvas').getContext('2d');
originalCtx.canvas.width = originalImg.width;
originalCtx.canvas.height = originalImg.height;
originalCtx.drawImage(originalImg, 0, 0);
const originalData = originalCtx.getImageData(0, 0, originalImg.width, originalImg.height).data;
const scanLength = (scanDirection === 'horizontal') ? originalImg.width : originalImg.height;
const stepDuration = duration / scanLength;
// Schedule frequency changes based on pixel brightness
for (let i = 0; i < scanLength; i++) {
let totalBrightness = 0;
let pixelCount = 0;
if (scanDirection === 'horizontal') {
for (let y = 0; y < originalImg.height; y++) {
const idx = (y * originalImg.width + i) * 4;
totalBrightness += (originalData[idx] + originalData[idx + 1] + originalData[idx + 2]) / 3;
pixelCount++;
}
} else { // vertical
for (let x = 0; x < originalImg.width; x++) {
const idx = (i * originalImg.width + x) * 4;
totalBrightness += (originalData[idx] + originalData[idx + 1] + originalData[idx + 2]) / 3;
pixelCount++;
}
}
const avgBrightness = pixelCount > 0 ? totalBrightness / pixelCount : 0;
const freq = lerp(minFreq, maxFreq, avgBrightness / 255);
oscillator.frequency.setValueAtTime(freq, now + i * stepDuration);
}
// Add a small fade-in/fade-out to prevent clicks
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(0.5, now + 0.1);
gainNode.gain.setValueAtTime(0.5, now + duration - 0.1);
gainNode.gain.linearRampToValueAtTime(0, now + duration);
oscillator.start(now);
oscillator.stop(now + duration);
playButton.textContent = '■ Stop';
oscillator.onended = () => {
if (audioContext) {
audioContext.close();
audioContext = null;
}
playButton.textContent = '▶ Play Sonification';
};
};
container.appendChild(playButton);
// 6. Return the final element
return container;
}
Apply Changes