You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Creates a video player-like experience for a static image using the Ken Burns effect (pan and zoom).
*
* @param {Image} originalImg The original javascript Image object.
* @param {number} duration The duration of the "video" clip in seconds.
* @param {string} zoom A string representing the start and end zoom levels, separated by a hyphen. E.g., '1.0-1.5' zooms in by 50%. '1.5-1.0' zooms out.
* @param {string} pan A string representing the start and end pan points, separated by a hyphen. Each point is a normalized 'x,y' coordinate (0,0 is top-left, 1,1 is bottom-right, 0.5,0.5 is center). E.g., '0,0-1,1' pans from top-left to bottom-right.
* @returns {HTMLElement} A div element containing the canvas and player controls, which can be appended to the document.
*/
async function processImage(originalImg, duration = 10, zoom = '1.0-1.2', pan = '0.5,0.5-0.5,0.5') {
// --- Parameter Parsing & Validation ---
const zoomParts = String(zoom).split('-').map(parseFloat);
if (zoomParts.length !== 2 || isNaN(zoomParts[0]) || isNaN(zoomParts[1]) || zoomParts[0] <= 0 || zoomParts[1] <= 0) {
console.error("Invalid zoom parameter. Expected format 'start-end' with positive numbers, e.g., '1.0-1.2'. Defaulting to '1.0-1.2'.");
zoomParts = [1.0, 1.2];
}
const [zoomStart, zoomEnd] = zoomParts;
const panParts = String(pan).split('-');
if (panParts.length !== 2) {
console.error("Invalid pan parameter. Expected format 'x1,y1-x2,y2', e.g., '0,0-1,1'. Defaulting to '0.5,0.5-0.5,0.5'.");
panParts = ['0.5,0.5', '0.5,0.5'];
}
const [panStartCoords, panEndCoords] = panParts.map(p => {
const coords = p.split(',').map(parseFloat);
if (coords.length !== 2 || isNaN(coords[0]) || isNaN(coords[1])) return [0.5, 0.5];
// Clamp coordinates between 0 and 1
return [Math.max(0, Math.min(1, coords[0])), Math.max(0, Math.min(1, coords[1]))];
});
const [[panStartX, panStartY], [panEndX, panEndY]] = [panStartCoords, panEndCoords];
// --- Element Creation ---
const container = document.createElement('div');
const canvas = document.createElement('canvas');
const controls = document.createElement('div');
const playButton = document.createElement('button');
const timeDisplay = document.createElement('span');
const progressBarContainer = document.createElement('div');
const progressBar = document.createElement('div');
const ctx = canvas.getContext('2d');
// --- Styling ---
Object.assign(container.style, {
position: 'relative',
display: 'inline-block',
background: '#000',
lineHeight: '0',
maxWidth: '100%',
overflow: 'hidden',
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
color: '#fff',
});
Object.assign(canvas.style, {
display: 'block',
width: '100%',
height: 'auto',
});
Object.assign(controls.style, {
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '40px',
background: 'linear-gradient(to top, rgba(0,0,0,0.7), transparent)',
display: 'flex',
alignItems: 'center',
padding: '0 10px',
boxSizing: 'border-box',
opacity: '0',
transition: 'opacity 0.3s ease-in-out'
});
container.onmouseenter = () => { controls.style.opacity = '1'; };
container.onmouseleave = () => { if (isPlaying) controls.style.opacity = '0'; };
Object.assign(playButton.style, {
background: 'none',
border: 'none',
color: 'white',
fontSize: '24px',
cursor: 'pointer',
padding: '0 10px 0 0',
lineHeight: '1',
});
Object.assign(timeDisplay.style, {
fontSize: '12px',
userSelect: 'none',
minWidth: '75px',
});
Object.assign(progressBarContainer.style, {
flexGrow: '1',
height: '5px',
background: 'rgba(255,255,255,0.3)',
marginLeft: '10px',
position: 'relative',
});
Object.assign(progressBar.style, {
height: '100%',
width: '0%',
background: 'white',
transition: isPlaying ? 'width 0.1s linear' : 'none',
});
// --- Setup ---
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
container.style.aspectRatio = `${canvas.width} / ${canvas.height}`;
// --- Animation State ---
let animationId = null;
let startTime = 0;
let isPlaying = false;
let elapsedTimeBeforePause = 0; // in milliseconds
// --- Render Logic ---
const formatTime = (seconds) => {
const min = Math.floor(seconds / 60);
const sec = Math.floor(seconds % 60);
return `${min}:${sec.toString().padStart(2, '0')}`;
};
const render = (currentTime) => {
if (!startTime) startTime = currentTime;
const currentElapsedTime = elapsedTimeBeforePause + (currentTime - startTime);
let progress = currentElapsedTime / (duration * 1000);
progress = Math.min(progress, 1);
const currentZoom = zoomStart + (zoomEnd - zoomStart) * progress;
const currentPanX = panStartX + (panEndX - panStartX) * progress;
const currentPanY = panStartY + (panEndY - panStartY) * progress;
const sWidth = originalImg.naturalWidth / currentZoom;
const sHeight = originalImg.naturalHeight / currentZoom;
const sX = (originalImg.naturalWidth - sWidth) * currentPanX;
const sY = (originalImg.naturalHeight - sHeight) * currentPanY;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(originalImg, sX, sY, sWidth, sHeight, 0, 0, canvas.width, canvas.height);
progressBar.style.width = `${progress * 100}%`;
timeDisplay.textContent = `${formatTime(currentElapsedTime / 1000)} / ${formatTime(duration)}`;
if (progress < 1 && isPlaying) {
animationId = requestAnimationFrame(render);
} else if (progress >= 1) {
isPlaying = false;
playButton.innerHTML = '⟳'; // Replay icon
controls.style.opacity = '1';
}
};
// --- Control Logic ---
const play = () => {
if (isPlaying) return;
const currentProgress = elapsedTimeBeforePause / (duration * 1000);
if (currentProgress >= 1) elapsedTimeBeforePause = 0; // Reset if at the end
isPlaying = true;
playButton.innerHTML = '❚❚'; // Pause icon
startTime = performance.now();
animationId = requestAnimationFrame(render);
};
const pause = () => {
if (!isPlaying) return;
isPlaying = false;
playButton.innerHTML = '▶'; // Play icon
cancelAnimationFrame(animationId);
elapsedTimeBeforePause += performance.now() - startTime;
};
playButton.onclick = () => isPlaying ? pause() : play();
// --- Assembly ---
container.append(canvas, controls);
controls.append(playButton, timeDisplay, progressBarContainer);
progressBarContainer.append(progressBar);
// --- Initial State ---
render(0); // Draw the first frame at progress 0
playButton.innerHTML = '▶'; // Play icon
// Wait for the image to be fully loaded before starting
if (originalImg.complete) {
render(0);
} else {
originalImg.onload = () => render(0);
}
return container;
}
Apply Changes