Function bodies 189 total
flipHorizontal function · javascript · L214-L220 (7 LOC)src/js/photoeditor.js
function flipHorizontal() {
if (!originalImage) return;
saveState();
flipH = !flipH;
drawImage();
Accessibility.announce(flipH ? 'Flipped horizontally' : 'Horizontal flip removed');
}flipVertical function · javascript · L223-L229 (7 LOC)src/js/photoeditor.js
function flipVertical() {
if (!originalImage) return;
saveState();
flipV = !flipV;
drawImage();
Accessibility.announce(flipV ? 'Flipped vertically' : 'Vertical flip removed');
}resize function · javascript · L232-L239 (8 LOC)src/js/photoeditor.js
function resize(newWidth, newHeight) {
if (!originalImage) return;
saveState();
canvas.width = newWidth;
canvas.height = newHeight;
drawImage();
Accessibility.announce(`Image resized to ${newWidth} by ${newHeight} pixels`);
}setAdjustment function · javascript · L242-L247 (6 LOC)src/js/photoeditor.js
function setAdjustment(name, value) {
if (name in adjustments) {
adjustments[name] = parseInt(value);
drawImage();
}
}applyPreset function · javascript · L250-L281 (32 LOC)src/js/photoeditor.js
function applyPreset(presetName) {
saveState();
switch (presetName) {
case 'none':
resetAdjustments();
break;
case 'grayscale':
adjustments = { brightness: 0, contrast: 10, saturation: -100, sharpness: 0 };
break;
case 'sepia':
adjustments = { brightness: 10, contrast: -10, saturation: -60, sharpness: 0 };
break;
case 'vintage':
adjustments = { brightness: 15, contrast: -15, saturation: -30, sharpness: 0 };
break;
case 'warm':
adjustments = { brightness: 10, contrast: 5, saturation: 15, sharpness: 0 };
break;
case 'cool':
adjustments = { brightness: 0, contrast: 5, saturation: -10, sharpness: 0 };
break;
case 'vivid':
adjustments = { brightness: 5, contrast: 20, saturation: 50, sharpness: 10 };
break;
case 'noir':
adjustments = { brightness: -5, contrast: 40, saturation: -100, sharpness: 5 };
break;
resetAdjustments function · javascript · L284-L290 (7 LOC)src/js/photoeditor.js
function resetAdjustments() {
adjustments = { brightness: 0, contrast: 0, saturation: 0, sharpness: 0 };
rotation = 0;
flipH = false;
flipV = false;
updateAdjustmentSliders();
}updateAdjustmentSliders function · javascript · L293-L303 (11 LOC)src/js/photoeditor.js
function updateAdjustmentSliders() {
for (const [name, value] of Object.entries(adjustments)) {
const slider = document.getElementById(`photo-${name}`);
const display = document.getElementById(`photo-${name}-val`);
if (slider) {
slider.value = value;
slider.setAttribute('aria-valuetext', String(value));
}
if (display) display.textContent = value;
}
}Powered by Repobility — scan your code at https://repobility.com
saveImage function · javascript · L306-L326 (21 LOC)src/js/photoeditor.js
async function saveImage(filePath, format = 'png') {
if (!canvas) return;
const mimeTypes = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
webp: 'image/webp',
bmp: 'image/bmp',
};
const mime = mimeTypes[format] || 'image/png';
return new Promise((resolve) => {
canvas.toBlob((blob) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsArrayBuffer(blob);
}, mime, 0.95);
});
}init function · javascript · L329-L341 (13 LOC)src/js/photoeditor.js
function init() {
['brightness', 'contrast', 'saturation', 'sharpness'].forEach(name => {
const slider = document.getElementById(`photo-${name}`);
if (slider) {
slider.addEventListener('input', () => {
setAdjustment(name, slider.value);
const display = document.getElementById(`photo-${name}-val`);
if (display) display.textContent = slider.value;
slider.setAttribute('aria-valuetext', slider.value);
});
}
});
}detectBackgroundColors function · javascript · L348-L421 (74 LOC)src/js/photoeditor.js
function detectBackgroundColors() {
if (!originalImage) return null;
const tempCanvas = document.createElement('canvas');
tempCanvas.width = originalImage.width;
tempCanvas.height = originalImage.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImage, 0, 0);
const w = tempCanvas.width;
const h = tempCanvas.height;
// Get ALL pixel data at once (fast), then sample edges from the array
const allData = tempCtx.getImageData(0, 0, w, h).data;
const edgePixels = [];
const edgeSize = Math.max(3, Math.floor(Math.min(w, h) * 0.02));
// Top and bottom edges
for (let x = 0; x < w; x += 2) {
for (let y = 0; y < edgeSize; y++) {
const i = (y * w + x) * 4;
edgePixels.push({ r: allData[i], g: allData[i + 1], b: allData[i + 2] });
}
for (let y = h - edgeSize; y < h; y++) {
const i = (y * w + x) * 4;
edgePixels.push({ r: allData[i], g: allData[i + 1], b: allData[idetectBackgroundColor function · javascript · L424-L427 (4 LOC)src/js/photoeditor.js
function detectBackgroundColor() {
const colors = detectBackgroundColors();
return colors && colors.length > 0 ? colors[0] : null;
}isBackgroundMulti function · javascript · L430-L437 (8 LOC)src/js/photoeditor.js
function isBackgroundMulti(r, g, b, bgColors, tol) {
const tolSq = tol * tol;
for (const bg of bgColors) {
const dr = r - bg.r, dg = g - bg.g, db = b - bg.b;
if (dr * dr + dg * dg + db * db < tolSq) return true;
}
return false;
}isBackground function · javascript · L440-L443 (4 LOC)src/js/photoeditor.js
function isBackground(r, g, b, bg, tol) {
const dr = r - bg.r, dg = g - bg.g, db = b - bg.b;
return dr * dr + dg * dg + db * db < tol * tol;
}removeBackground function · javascript · L446-L549 (104 LOC)src/js/photoeditor.js
async function removeBackground(tolerance = 70) {
if (!originalImage || !ctx) {
Accessibility.announce('No image loaded');
return 'No image loaded.';
}
saveState();
// Draw raw image (no adjustments) so we work on clean pixels
const w = originalImage.width;
const h = originalImage.height;
canvas.width = w;
canvas.height = h;
drawImageRaw();
const bgColors = detectBackgroundColors();
if (!bgColors || bgColors.length === 0) return 'Could not detect background color.';
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
let removed = 0;
const total = data.length / 4;
// First pass: identify background pixels using multi-color matching
const isBg = new Uint8Array(w * h);
for (let i = 0; i < total; i++) {
const idx = i * 4;
if (isBackgroundMulti(data[idx], data[idx + 1], data[idx + 2], bgColors, tolerance)) {
isBg[i] = 1;
}
}
// Flood fill froboxBlur function · javascript · L552-L598 (47 LOC)src/js/photoeditor.js
function boxBlur(srcData, w, h, radius) {
const dst = new Uint8ClampedArray(srcData.length);
const size = radius * 2 + 1;
const area = size * size;
// Horizontal pass
const temp = new Uint8ClampedArray(srcData.length);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let rSum = 0, gSum = 0, bSum = 0, aSum = 0;
for (let dx = -radius; dx <= radius; dx++) {
const sx = Math.min(w - 1, Math.max(0, x + dx));
const idx = (y * w + sx) * 4;
rSum += srcData[idx];
gSum += srcData[idx + 1];
bSum += srcData[idx + 2];
aSum += srcData[idx + 3];
}
const idx = (y * w + x) * 4;
temp[idx] = rSum / size;
temp[idx + 1] = gSum / size;
temp[idx + 2] = bSum / size;
temp[idx + 3] = aSum / size;
}
}
// Vertical pass
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let rSum = 0, gSum = 0, bSum = 0, aSum Source: Repobility analyzer · https://repobility.com
blurBackground function · javascript · L601-L689 (89 LOC)src/js/photoeditor.js
async function blurBackground(tolerance = 70, blurRadius = 30) {
if (!originalImage || !ctx) {
Accessibility.announce('No image loaded');
return 'No image loaded.';
}
saveState();
const w = originalImage.width;
const h = originalImage.height;
canvas.width = w;
canvas.height = h;
drawImageRaw();
// For large images, use CSS blur on a temp canvas (fast, hardware-accelerated)
const sharpData = ctx.getImageData(0, 0, w, h);
const bgColors = detectBackgroundColors();
if (!bgColors || bgColors.length === 0) return 'Could not detect background color.';
// Build background mask via flood fill from edges
const mask = new Uint8Array(w * h);
for (let i = 0; i < w * h; i++) {
const idx = i * 4;
mask[i] = isBackgroundMulti(sharpData.data[idx], sharpData.data[idx + 1], sharpData.data[idx + 2], bgColors, tolerance) ? 1 : 0;
}
const bgMask = new Uint8Array(w * h);
const queue = [];
for (let x = 0;parseRegion function · javascript · L696-L716 (21 LOC)src/js/photoeditor.js
function parseRegion(description) {
const d = description.toLowerCase();
let x = 0.25, y = 0.2, w = 0.5, h = 0.6;
if (d.includes('left')) { x = 0; w = 0.4; }
else if (d.includes('right')) { x = 0.6; w = 0.4; }
else if (d.includes('center') || d.includes('middle')) { x = 0.2; w = 0.6; }
if (d.includes('top')) { y = 0; h = 0.4; }
else if (d.includes('bottom')) { y = 0.6; h = 0.4; }
if (d.includes('top') && d.includes('left')) { x = 0; y = 0; w = 0.4; h = 0.4; }
if (d.includes('top') && d.includes('right')) { x = 0.6; y = 0; w = 0.4; h = 0.4; }
if (d.includes('bottom') && d.includes('left')) { x = 0; y = 0.6; w = 0.4; h = 0.4; }
if (d.includes('bottom') && d.includes('right')) { x = 0.6; y = 0.6; w = 0.4; h = 0.4; }
if (d.includes('small') || d.includes('tiny')) { w *= 0.5; h *= 0.5; x += w * 0.25; y += h * 0.25; }
else if (d.includes('large') || d.includes('big')) { w = Math.min(w * 1.4, 0.9); h = Math.min(h * 1.4, 0.9); }
reremoveRegion function · javascript · L719-L858 (140 LOC)src/js/photoeditor.js
async function removeRegion(description) {
if (!originalImage || !ctx) {
Accessibility.announce('No image loaded');
return 'No image loaded. Open an image first.';
}
saveState();
canvas.width = originalImage.width;
canvas.height = originalImage.height;
drawImageRaw();
const region = parseRegion(description);
const cw = canvas.width;
const ch = canvas.height;
const rx = Math.round(region.x * cw);
const ry = Math.round(region.y * ch);
const rw = Math.round(region.w * cw);
const rh = Math.round(region.h * ch);
const imageData = ctx.getImageData(0, 0, cw, ch);
const data = imageData.data;
// Build a border sampling band (pixels just outside the region)
const bandSize = Math.max(8, Math.floor(Math.min(rw, rh) * 0.15));
// Helper to get a pixel safely
const getPixel = (px, py) => {
const cx2 = Math.max(0, Math.min(cw - 1, px));
const cy2 = Math.max(0, Math.min(ch - 1, py));
const smartRemove function · javascript · L868-L1092 (225 LOC)src/js/photoeditor.js
async function smartRemove(description, coords) {
if (!originalImage || !ctx) {
Accessibility.announce('No image loaded');
return 'No image loaded.';
}
saveState();
const w = canvas.width;
const h = canvas.height;
drawImageRaw();
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
// Use precise coordinates if provided (percentages 0-100), otherwise fall back to text parsing
let region;
const hasPreciseCoords = coords && coords.x !== undefined && coords.y !== undefined;
if (hasPreciseCoords) {
region = {
x: (coords.x || 0) / 100,
y: (coords.y || 0) / 100,
w: (coords.w || 20) / 100,
h: (coords.h || 20) / 100,
};
} else {
region = parseRegion(description);
}
const rx = Math.max(0, Math.round(region.x * w));
const ry = Math.max(0, Math.round(region.y * h));
const rw = Math.min(w - rx, Math.round(region.w * w));
const rh = Math.insertImage function · javascript · L1105-L1149 (45 LOC)src/js/photoeditor.js
async function insertImage(imagePath, position = 'center', scale = 0.3, opacity = 100) {
if (!originalImage || !ctx) {
Accessibility.announce('No image loaded');
return 'No image loaded. Open a base image first.';
}
saveState();
return new Promise((resolve) => {
const overlay = new Image();
overlay.onload = () => {
const cw = canvas.width;
const ch = canvas.height;
// Calculate overlay size
let ow = Math.round(overlay.width * scale);
let oh = Math.round(overlay.height * scale);
// Clamp to canvas size
if (ow > cw * 0.95) { const r = (cw * 0.95) / ow; ow = Math.round(ow * r); oh = Math.round(oh * r); }
if (oh > ch * 0.95) { const r = (ch * 0.95) / oh; ow = Math.round(ow * r); oh = Math.round(oh * r); }
// Calculate position
const pos = (position || 'center').toLowerCase();
let x = (cw - ow) / 2, y = (ch - oh) / 2; // default center
if (pos.includesinsertImageFromPicker function · javascript · L1154-L1166 (13 LOC)src/js/photoeditor.js
async function insertImageFromPicker(position = 'center', scale = 0.3) {
if (!window.api) {
Accessibility.announce('Insert image requires the desktop application');
return;
}
const result = await window.api.showOpenDialog({
title: 'Select Image to Insert',
filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'] }],
properties: ['openFile'],
});
if (result.canceled || result.filePaths.length === 0) return;
return insertImage(result.filePaths[0], position, scale, 100);
}drawRect function · javascript · L1180-L1213 (34 LOC)src/js/photoeditor.js
async function drawRect(position, widthPct, heightPct, color, borderRadius = 0) {
if (!originalImage || !ctx) return 'No image loaded.';
saveState();
drawImage(); // make sure canvas is current
const cw = canvas.width, ch = canvas.height;
const rw = Math.round((widthPct / 100) * cw);
const rh = Math.round((heightPct / 100) * ch);
const region = parseRegion(position || 'center');
const rx = Math.round((region.x + region.w / 2) * cw - rw / 2);
const ry = Math.round((region.y + region.h / 2) * ch - rh / 2);
ctx.fillStyle = color || '#FFD700';
if (borderRadius > 0) {
ctx.beginPath();
ctx.moveTo(rx + borderRadius, ry);
ctx.lineTo(rx + rw - borderRadius, ry);
ctx.quadraticCurveTo(rx + rw, ry, rx + rw, ry + borderRadius);
ctx.lineTo(rx + rw, ry + rh - borderRadius);
ctx.quadraticCurveTo(rx + rw, ry + rh, rx + rw - borderRadius, ry + rh);
ctx.lineTo(rx + borderRadius, ry + rh);
ctx.quadraticCurveTodrawTextOnPhoto function · javascript · L1224-L1265 (42 LOC)src/js/photoeditor.js
async function drawTextOnPhoto(text, position = 'center', fontSize = 5, color = '#ffffff', bgColor = '', font = 'Arial') {
if (!originalImage || !ctx) return 'No image loaded.';
saveState();
drawImage();
const cw = canvas.width, ch = canvas.height;
const fontPx = Math.round((fontSize / 100) * ch);
ctx.font = `bold ${fontPx}px ${font}`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
const region = parseRegion(position || 'center');
const tx = Math.round((region.x + region.w / 2) * cw);
const ty = Math.round((region.y + region.h / 2) * ch);
// Measure text
const metrics = ctx.measureText(text);
const textW = metrics.width;
const textH = fontPx;
const padding = Math.round(fontPx * 0.4);
// Draw background behind text if specified
if (bgColor) {
ctx.fillStyle = bgColor;
ctx.fillRect(tx - textW / 2 - padding, ty - textH / 2 - padding, textW + padding * 2, textH + padding * 2);
}
// DraMethodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
fillRegion function · javascript · L1270-L1287 (18 LOC)src/js/photoeditor.js
async function fillRegion(position, color) {
if (!originalImage || !ctx) return 'No image loaded.';
saveState();
drawImage();
const cw = canvas.width, ch = canvas.height;
const region = parseRegion(position || 'center');
const rx = Math.round(region.x * cw);
const ry = Math.round(region.y * ch);
const rw = Math.round(region.w * cw);
const rh = Math.round(region.h * ch);
ctx.fillStyle = color || '#FFD700';
ctx.fillRect(rx, ry, rw, rh);
await bakeCanvas();
return `Filled region "${position}" with ${color} (${rw}x${rh}px).`;
}getImageDataURL function · javascript · L1290-L1298 (9 LOC)src/js/photoeditor.js
function getImageDataURL() {
if (!canvas) return null;
try {
return canvas.toDataURL('image/png');
} catch (e) {
console.error('getImageDataURL failed:', e);
return null;
}
}syncAudioClips function · javascript · L26-L71 (46 LOC)src/js/player.js
function syncAudioClips(currentTime) {
const allClips = Timeline.getClips();
const audioClips = allClips.filter(c => c.type === 'audio');
// Start audio clips that should be playing
for (const clip of audioClips) {
const clipEnd = clip.startTime + clip.duration;
const shouldPlay = isPlaying && currentTime >= clip.startTime && currentTime < clipEnd;
if (shouldPlay) {
if (!activeAudioElements.has(clip.id)) {
// Create and start a new audio element for this clip
const audio = new Audio(clip.filePath);
audio.volume = isMuted ? 0 : (clip.volume ?? 100) / 100 * (video.volume || 1);
audio.currentTime = currentTime - clip.startTime;
audio.play().catch(() => {}); // ignore autoplay errors
activeAudioElements.set(clip.id, audio);
} else {
// Already playing - check if we need to correct drift
const audio = activeAudioElements.get(clip.id);
const expectedstopAllAudio function · javascript · L74-L80 (7 LOC)src/js/player.js
function stopAllAudio() {
for (const [clipId, audio] of activeAudioElements) {
audio.pause();
audio.src = '';
}
activeAudioElements.clear();
}loadVideo function · javascript · L83-L104 (22 LOC)src/js/player.js
function loadVideo(filePath) {
currentSource = filePath;
video.src = filePath;
video.style.display = 'block';
if (noVideoMsg) noVideoMsg.style.display = 'none';
video.addEventListener('loadedmetadata', () => {
seekSlider.max = Math.floor(video.duration * 10) / 10;
updateTimeDisplay();
detectFrameRate();
// Generate filmstrip thumbnails
filmstripGenerated = false;
setTimeout(() => generateFilmstrip(), 300);
Accessibility.announce(`Video loaded. Duration: ${Accessibility.formatTime(video.duration)}`);
Accessibility.setStatus('Video loaded: ' + filePath.split(/[\\/]/).pop());
}, { once: true });
video.addEventListener('error', () => {
Accessibility.announce('Error loading video file');
Accessibility.setStatus('Error: Could not load video');
}, { once: true });
}play function · javascript · L107-L130 (24 LOC)src/js/player.js
function play() {
const hasVideo = video.src && video.src !== '';
const hasAudioClips = Timeline.getClips().some(c => c.type === 'audio');
if (!hasVideo && !hasAudioClips) {
Accessibility.announce('No media loaded. Import media first.');
return;
}
if (hasVideo) {
video.play();
}
isPlaying = true;
syncAudioClips(getCurrentTime());
// Start audio sync loop if we have audio clips
if (hasAudioClips) {
startAudioSyncLoop();
}
playBtn.textContent = 'Pause';
playBtn.setAttribute('aria-label', 'Pause');
Accessibility.announceStatus('Playing');
}pause function · javascript · L133-L141 (9 LOC)src/js/player.js
function pause() {
video.pause();
isPlaying = false;
stopAllAudio();
stopAudioSyncLoop();
playBtn.textContent = 'Play';
playBtn.setAttribute('aria-label', 'Play');
Accessibility.announceStatus('Paused');
}togglePlay function · javascript · L144-L147 (4 LOC)src/js/player.js
function togglePlay() {
if (isPlaying) pause();
else play();
}Repobility — same analyzer, your code, free for public repos · /scan/
stop function · javascript · L150-L162 (13 LOC)src/js/player.js
function stop() {
video.pause();
video.currentTime = 0;
isPlaying = false;
stopAllAudio();
stopAudioSyncLoop();
audioOnlyTime = 0;
playBtn.textContent = 'Play';
playBtn.setAttribute('aria-label', 'Play');
updateTimeDisplay();
Timeline.setPlayheadPosition(0);
Accessibility.announceStatus('Stopped');
}startAudioSyncLoop function · javascript · L169-L186 (18 LOC)src/js/player.js
function startAudioSyncLoop() {
if (audioSyncInterval) return;
lastRealTime = performance.now();
audioSyncInterval = setInterval(() => {
if (!isPlaying) return;
const now = performance.now();
const delta = (now - lastRealTime) / 1000;
lastRealTime = now;
const hasVideo = video.src && video.src !== '' && video.duration;
if (!hasVideo) {
// Audio-only mode: advance our own clock
audioOnlyTime += delta;
updateTimeDisplay();
}
syncAudioClips(getCurrentTime());
}, 50);
}stopAudioSyncLoop function · javascript · L188-L193 (6 LOC)src/js/player.js
function stopAudioSyncLoop() {
if (audioSyncInterval) {
clearInterval(audioSyncInterval);
audioSyncInterval = null;
}
}skipForward function · javascript · L196-L200 (5 LOC)src/js/player.js
function skipForward(seconds = 5) {
video.currentTime = Math.min(video.currentTime + seconds, video.duration || 0);
updateTimeDisplay();
Accessibility.announceStatus(`Skipped forward to ${Accessibility.formatTime(video.currentTime)}`);
}skipBack function · javascript · L203-L207 (5 LOC)src/js/player.js
function skipBack(seconds = 5) {
video.currentTime = Math.max(video.currentTime - seconds, 0);
updateTimeDisplay();
Accessibility.announceStatus(`Skipped back to ${Accessibility.formatTime(video.currentTime)}`);
}detectFrameRate function · javascript · L212-L235 (24 LOC)src/js/player.js
function detectFrameRate() {
// Try to detect FPS using requestVideoFrameCallback
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
let lastTime = null;
let samples = [];
const detect = (now, metadata) => {
if (lastTime !== null) {
const delta = metadata.mediaTime - lastTime;
if (delta > 0 && delta < 0.2) {
samples.push(1 / delta);
}
}
lastTime = metadata.mediaTime;
if (samples.length < 10) {
video.requestVideoFrameCallback(detect);
} else {
const avgFps = samples.reduce((a, b) => a + b, 0) / samples.length;
detectedFPS = Math.round(avgFps);
if (detectedFPS < 10 || detectedFPS > 120) detectedFPS = 30;
}
};
video.requestVideoFrameCallback(detect);
}
}frameForward function · javascript · L241-L267 (27 LOC)src/js/player.js
function frameForward() {
if (!video.src || !video.duration) {
Accessibility.announce('No video loaded');
return;
}
const frameTime = 1 / detectedFPS;
const targetTime = Math.min(video.currentTime + frameTime, video.duration);
// Unmute so you hear the audio while stepping
video.muted = false;
video.playbackRate = 0.5; // slow so the audio blip is audible
video.play();
// Stop after one frame's worth of real time
setTimeout(() => {
video.pause();
video.playbackRate = 1;
video.currentTime = targetTime; // snap to exact frame
isPlaying = false;
playBtn.textContent = 'Play';
playBtn.setAttribute('aria-label', 'Play video');
updateTimeDisplay();
const frameNum = Math.round(video.currentTime * detectedFPS);
Accessibility.announceStatus(`Frame ${frameNum}, ${Accessibility.formatTimeDisplay(video.currentTime)}`);
}, Math.round(frameTime * 1000 * 2)); // 2x because playbackRate isframeBack function · javascript · L273-L301 (29 LOC)src/js/player.js
function frameBack() {
if (!video.src || !video.duration) {
Accessibility.announce('No video loaded');
return;
}
const frameTime = 1 / detectedFPS;
const targetTime = Math.max(video.currentTime - frameTime, 0);
// Seek to the target frame
video.currentTime = targetTime;
// Play a tiny blip so user hears where they are
video.muted = false;
video.playbackRate = 0.5;
video.play();
setTimeout(() => {
video.pause();
video.playbackRate = 1;
video.currentTime = targetTime; // snap back to exact frame
isPlaying = false;
playBtn.textContent = 'Play';
playBtn.setAttribute('aria-label', 'Play video');
updateTimeDisplay();
const frameNum = Math.round(video.currentTime * detectedFPS);
Accessibility.announceStatus(`Frame ${frameNum}, ${Accessibility.formatTimeDisplay(video.currentTime)}`);
}, Math.round(frameTime * 1000 * 2));
}Powered by Repobility — scan your code at https://repobility.com
seekTo function · javascript · L304-L318 (15 LOC)src/js/player.js
function seekTo(time) {
const maxTime = getDuration();
const seekTime = Math.max(0, Math.min(time, maxTime));
const hasVideo = video.src && video.src !== '' && video.duration;
if (hasVideo) {
video.currentTime = Math.min(seekTime, video.duration);
}
audioOnlyTime = seekTime;
// Re-sync audio clips to new position
stopAllAudio();
if (isPlaying) {
syncAudioClips(seekTime);
}
updateTimeDisplay();
}setVolume function · javascript · L321-L335 (15 LOC)src/js/player.js
function setVolume(val) {
video.volume = val / 100;
volumeSlider.value = val;
volumeSlider.setAttribute('aria-valuetext', `${val} percent`);
volumeSlider.setAttribute('aria-label', `Volume, currently ${val} percent`);
if (val === 0) {
isMuted = true;
muteBtn.textContent = 'Unmute';
muteBtn.setAttribute('aria-label', 'Unmute audio');
} else {
isMuted = false;
muteBtn.textContent = 'Mute';
muteBtn.setAttribute('aria-label', 'Mute audio');
}
}toggleMute function · javascript · L338-L360 (23 LOC)src/js/player.js
function toggleMute() {
if (isMuted) {
video.muted = false;
isMuted = false;
// Unmute all active audio elements
for (const [, audio] of activeAudioElements) {
audio.volume = video.volume || 1;
}
muteBtn.textContent = 'Mute';
muteBtn.setAttribute('aria-label', 'Mute audio');
Accessibility.announceStatus('Audio unmuted');
} else {
video.muted = true;
isMuted = true;
// Mute all active audio elements
for (const [, audio] of activeAudioElements) {
audio.volume = 0;
}
muteBtn.textContent = 'Unmute';
muteBtn.setAttribute('aria-label', 'Unmute audio');
Accessibility.announceStatus('Audio muted');
}
}updateTimeDisplay function · javascript · L366-L388 (23 LOC)src/js/player.js
function updateTimeDisplay() {
const current = getCurrentTime();
const duration = getDuration();
const currentStr = Accessibility.formatTimeDisplay(current);
const durationStr = Accessibility.formatTimeDisplay(duration);
timeDisplay.textContent = `${currentStr} / ${durationStr}`;
seekSlider.value = current;
seekSlider.setAttribute('aria-valuetext',
`${Accessibility.formatTime(current)} of ${Accessibility.formatTime(duration)}`);
// Update filmstrip playhead position
if (filmstripPlayhead && filmstripContainer && duration > 0) {
const pct = current / duration;
const containerWidth = filmstripContainer.offsetWidth;
filmstripPlayhead.style.left = (pct * containerWidth) + 'px';
}
// Update the playhead info text (visible + read by screen reader on demand)
updatePlayheadInfo(current);
// Update timeline playhead
Timeline.setPlayheadPosition(current);
}updatePlayheadInfo function · javascript · L391-L415 (25 LOC)src/js/player.js
function updatePlayheadInfo(time) {
const infoEl = document.getElementById('playhead-position-text');
if (!infoEl) return;
const currentSecond = Math.floor(time);
// Only update text once per second to avoid excessive DOM writes
if (currentSecond === lastAnnouncedSecond) return;
lastAnnouncedSecond = currentSecond;
const clipsHere = Timeline.getClipsAtTime(time);
const timeStr = Accessibility.formatTime(time);
let info = `Playhead at ${timeStr}.`;
if (clipsHere.length === 0) {
info += ' No clips at this position.';
} else {
const descriptions = clipsHere.map(c => {
const remaining = (c.startTime + c.duration) - time;
return `${c.type} clip "${c.name}" (${Accessibility.formatTime(remaining)} remaining)`;
});
info += ' At this position: ' + descriptions.join(', ') + '.';
}
infoEl.textContent = info;
}announceWhereAmI function · javascript · L418-L462 (45 LOC)src/js/player.js
function announceWhereAmI() {
const current = getCurrentTime();
const duration = getDuration();
const clipsHere = Timeline.getClipsAtTime(current);
const timeStr = Accessibility.formatTime(current);
const durationStr = Accessibility.formatTime(duration);
let msg = `Playhead is at ${timeStr} out of ${durationStr} total.`;
if (clipsHere.length === 0) {
msg += ' There are no clips at this position. ';
// Find the nearest clip
const allClips = Timeline.getClips();
if (allClips.length > 0) {
const sorted = allClips.slice().sort((a, b) => {
const distA = Math.min(Math.abs(a.startTime - current), Math.abs(a.startTime + a.duration - current));
const distB = Math.min(Math.abs(b.startTime - current), Math.abs(b.startTime + b.duration - current));
return distA - distB;
});
const nearest = sorted[0];
if (current < nearest.startTime) {
msg += `The next clip is "${nearest.ngenerateFilmstrip function · javascript · L465-L536 (72 LOC)src/js/player.js
function generateFilmstrip() {
if (!filmstripCanvas || !video.duration || filmstripGenerated) return;
const ctx = filmstripCanvas.getContext('2d');
if (!ctx) return;
const container = filmstripContainer;
const width = container.offsetWidth;
const height = container.offsetHeight;
// Set canvas resolution
filmstripCanvas.width = width * 2; // 2x for sharpness
filmstripCanvas.height = height * 2;
filmstripCanvas.style.width = width + 'px';
filmstripCanvas.style.height = height + 'px';
ctx.scale(2, 2);
const duration = video.duration;
const thumbWidth = Math.max(40, height * (video.videoWidth / video.videoHeight || 16/9));
const numThumbs = Math.ceil(width / thumbWidth) + 1;
const timeStep = duration / numThumbs;
// Use an offscreen video to grab frames without interrupting playback
const offscreen = document.createElement('video');
offscreen.src = video.src;
offscreen.muted = true;
offscreen.preloagrabNextFrame function · javascript · L499-L535 (37 LOC)src/js/player.js
function grabNextFrame() {
if (thumbIndex >= numThumbs) {
filmstripGenerated = true;
offscreen.remove();
// Draw subtle frame borders
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
ctx.lineWidth = 0.5;
for (let i = 1; i < numThumbs; i++) {
const x = i * thumbWidth;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
return;
}
const seekTime = thumbIndex * timeStep;
offscreen.currentTime = seekTime;
offscreen.addEventListener('seeked', function onSeeked() {
offscreen.removeEventListener('seeked', onSeeked);
const x = thumbIndex * thumbWidth;
try {
ctx.drawImage(offscreen, x, 0, thumbWidth, height);
} catch (e) {
// If cross-origin or error, draw a placeholder
ctx.fillStyle = `hsl(${(thumbIndex * 30) % 360}, 30%, 25%)`;
ctx.fillRect(x, 0, thumbWidth, Source: Repobility analyzer · https://repobility.com
initFilmstripClick function · javascript · L539-L587 (49 LOC)src/js/player.js
function initFilmstripClick() {
if (!filmstripContainer) return;
let isDragging = false;
function seekFromMouse(e) {
const rect = filmstripContainer.getBoundingClientRect();
const x = e.clientX - rect.left;
const pct = Math.max(0, Math.min(1, x / rect.width));
const time = pct * (video.duration || 0);
seekTo(time);
}
filmstripContainer.addEventListener('mousedown', (e) => {
isDragging = true;
seekFromMouse(e);
});
document.addEventListener('mousemove', (e) => {
if (isDragging) seekFromMouse(e);
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
// Touch support
filmstripContainer.addEventListener('touchstart', (e) => {
isDragging = true;
const touch = e.touches[0];
const rect = filmstripContainer.getBoundingClientRect();
const x = touch.clientX - rect.left;
const pct = Math.max(0, Math.min(1, x / rect.width));
seekTo(pctseekFromMouse function · javascript · L544-L550 (7 LOC)src/js/player.js
function seekFromMouse(e) {
const rect = filmstripContainer.getBoundingClientRect();
const x = e.clientX - rect.left;
const pct = Math.max(0, Math.min(1, x / rect.width));
const time = pct * (video.duration || 0);
seekTo(time);
}getCurrentTime function · javascript · L590-L594 (5 LOC)src/js/player.js
function getCurrentTime() {
const hasVideo = video.src && video.src !== '' && video.duration;
if (hasVideo) return video.currentTime || 0;
return audioOnlyTime;
}