|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fileInput = document.getElementById('fileInput'); |
|
|
const uploadBtn = document.getElementById('uploadBtn'); |
|
|
const inputImage = document.getElementById('inputImage'); |
|
|
const inputPlaceholder = document.getElementById('inputPlaceholder'); |
|
|
const outputImage = document.getElementById('outputImage'); |
|
|
const outputPlaceholder = document.getElementById('outputPlaceholder'); |
|
|
const saveBtn = document.getElementById('saveBtn'); |
|
|
const loadingSpinner = document.getElementById('loadingSpinner'); |
|
|
const cameraBtn = document.getElementById('cameraBtn'); |
|
|
const cameraInput = document.getElementById('cameraInput'); |
|
|
const fullscreenBtn = document.getElementById('fullscreenBtn'); |
|
|
const copyBtn = document.getElementById('copyBtn'); |
|
|
|
|
|
|
|
|
let isShowingOriginal = false; |
|
|
let hasProcessedImage = false; |
|
|
|
|
|
|
|
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || |
|
|
(window.matchMedia && window.matchMedia("(max-width: 768px)").matches); |
|
|
|
|
|
|
|
|
if (cameraBtn) { |
|
|
cameraBtn.style.display = 'inline-block'; |
|
|
} |
|
|
|
|
|
|
|
|
if (uploadBtn && fileInput) { |
|
|
uploadBtn.addEventListener('click', () => fileInput.click()); |
|
|
} |
|
|
|
|
|
|
|
|
const cameraModal = document.getElementById('cameraModal'); |
|
|
const cameraVideo = document.getElementById('cameraVideo'); |
|
|
const cameraCanvas = document.getElementById('cameraCanvas'); |
|
|
const captureBtn = document.getElementById('captureBtn'); |
|
|
const closeCameraBtn = document.getElementById('closeCameraBtn'); |
|
|
|
|
|
|
|
|
if (cameraBtn) { |
|
|
cameraBtn.addEventListener('click', () => { |
|
|
if (isMobile) { |
|
|
|
|
|
if (cameraInput) { |
|
|
cameraInput.click(); |
|
|
showNotification("Camera Opening..."); |
|
|
} |
|
|
} else { |
|
|
|
|
|
openCameraModal(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async function openCameraModal() { |
|
|
try { |
|
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true }); |
|
|
cameraVideo.srcObject = stream; |
|
|
cameraModal.classList.remove('hidden'); |
|
|
cameraModal.style.pointerEvents = 'auto'; |
|
|
cameraModal.style.opacity = '1'; |
|
|
} catch (err) { |
|
|
console.error("Error accessing camera:", err); |
|
|
showNotification("Camera Access Denied"); |
|
|
alert("Could not access camera. Please allow camera permissions."); |
|
|
} |
|
|
} |
|
|
|
|
|
function stopCamera() { |
|
|
const stream = cameraVideo.srcObject; |
|
|
if (stream) { |
|
|
const tracks = stream.getTracks(); |
|
|
tracks.forEach(track => track.stop()); |
|
|
} |
|
|
cameraVideo.srcObject = null; |
|
|
cameraModal.classList.add('hidden'); |
|
|
cameraModal.style.pointerEvents = 'none'; |
|
|
cameraModal.style.opacity = '0'; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const editorModal = document.getElementById('editorModal'); |
|
|
const editorImage = document.getElementById('editorImage'); |
|
|
const cropBtn = document.getElementById('cropBtn'); |
|
|
const rotateSlider = document.getElementById('rotateSlider'); |
|
|
const cancelEditBtn = document.getElementById('cancelEditBtn'); |
|
|
|
|
|
let cropper = null; |
|
|
|
|
|
|
|
|
if (captureBtn) { |
|
|
captureBtn.addEventListener('click', () => { |
|
|
const context = cameraCanvas.getContext('2d'); |
|
|
|
|
|
|
|
|
if (cameraVideo.videoWidth === 0 || cameraVideo.videoHeight === 0) { |
|
|
alert("Error: Video stream not ready yet."); |
|
|
return; |
|
|
} |
|
|
|
|
|
cameraCanvas.width = cameraVideo.videoWidth; |
|
|
cameraCanvas.height = cameraVideo.videoHeight; |
|
|
context.drawImage(cameraVideo, 0, 0, cameraCanvas.width, cameraCanvas.height); |
|
|
|
|
|
|
|
|
cameraCanvas.toBlob(blob => { |
|
|
if (!blob) { |
|
|
alert("Error: Failed to create image blob."); |
|
|
return; |
|
|
} |
|
|
|
|
|
const url = URL.createObjectURL(blob); |
|
|
stopCamera(); |
|
|
openEditor(url); |
|
|
}, 'image/jpeg'); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function openEditor(imageUrl) { |
|
|
|
|
|
if (typeof Cropper === 'undefined') { |
|
|
alert("Error: Cropper.js library is not loaded. Please check your internet connection."); |
|
|
return; |
|
|
} |
|
|
|
|
|
editorImage.src = imageUrl; |
|
|
editorModal.classList.remove('hidden'); |
|
|
|
|
|
|
|
|
editorModal.style.opacity = '1'; |
|
|
editorModal.style.pointerEvents = 'auto'; |
|
|
|
|
|
|
|
|
if (rotateSlider) rotateSlider.value = 0; |
|
|
|
|
|
|
|
|
if (cropper) { |
|
|
cropper.destroy(); |
|
|
} |
|
|
|
|
|
cropper = new Cropper(editorImage, { |
|
|
viewMode: 1, |
|
|
dragMode: 'move', |
|
|
autoCropArea: 0.9, |
|
|
responsive: true, |
|
|
background: false, |
|
|
guides: true, |
|
|
center: true, |
|
|
highlight: false, |
|
|
}); |
|
|
} |
|
|
|
|
|
function closeEditor() { |
|
|
if (cropper) { |
|
|
cropper.destroy(); |
|
|
cropper = null; |
|
|
} |
|
|
editorModal.classList.add('hidden'); |
|
|
editorModal.style.opacity = '0'; |
|
|
editorModal.style.pointerEvents = 'none'; |
|
|
} |
|
|
|
|
|
|
|
|
if (cropBtn) { |
|
|
cropBtn.addEventListener('click', () => { |
|
|
if (!cropper) return; |
|
|
|
|
|
|
|
|
const canvas = cropper.getCroppedCanvas(); |
|
|
|
|
|
canvas.toBlob((blob) => { |
|
|
|
|
|
const file = new File([blob], "edited-capture.jpg", { type: "image/jpeg" }); |
|
|
processImage(file); |
|
|
|
|
|
|
|
|
const url = URL.createObjectURL(blob); |
|
|
inputImage.src = url; |
|
|
inputImage.classList.remove('hidden'); |
|
|
inputPlaceholder.classList.add('hidden'); |
|
|
|
|
|
closeEditor(); |
|
|
}, 'image/jpeg'); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (rotateSlider) { |
|
|
rotateSlider.addEventListener('input', (e) => { |
|
|
if (cropper) { |
|
|
cropper.rotateTo(parseInt(e.target.value)); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
if (cancelEditBtn) { |
|
|
cancelEditBtn.addEventListener('click', closeEditor); |
|
|
} |
|
|
|
|
|
if (closeCameraBtn) { |
|
|
closeCameraBtn.addEventListener('click', stopCamera); |
|
|
} |
|
|
|
|
|
|
|
|
if (cameraInput) { |
|
|
cameraInput.addEventListener('change', (e) => { |
|
|
if (e.target.files && e.target.files[0]) { |
|
|
const file = e.target.files[0]; |
|
|
|
|
|
if (!file.type.match('image.*')) { |
|
|
alert("Error: Please capture a valid image."); |
|
|
return; |
|
|
} |
|
|
|
|
|
const reader = new FileReader(); |
|
|
reader.onload = (event) => { |
|
|
inputImage.src = event.target.result; |
|
|
inputImage.classList.remove('hidden'); |
|
|
inputPlaceholder.classList.add('hidden'); |
|
|
processImage(file); |
|
|
} |
|
|
reader.readAsDataURL(file); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const dropZone = document.getElementById('inputDropZone'); |
|
|
|
|
|
if (dropZone) { |
|
|
|
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
|
|
dropZone.addEventListener(eventName, preventDefaults, false); |
|
|
}); |
|
|
|
|
|
function preventDefaults(e) { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
} |
|
|
|
|
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => { |
|
|
dropZone.addEventListener(eventName, () => dropZone.classList.add('highlight-drop'), false); |
|
|
}); |
|
|
|
|
|
['dragleave', 'drop'].forEach(eventName => { |
|
|
dropZone.addEventListener(eventName, () => dropZone.classList.remove('highlight-drop'), false); |
|
|
}); |
|
|
|
|
|
|
|
|
dropZone.addEventListener('drop', handleDrop, false); |
|
|
|
|
|
|
|
|
dropZone.addEventListener('click', () => fileInput.click()); |
|
|
} |
|
|
|
|
|
function handleDrop(e) { |
|
|
const dt = e.dataTransfer; |
|
|
const files = dt.files; |
|
|
|
|
|
if (files && files[0]) { |
|
|
const file = files[0]; |
|
|
if (!file.type.match('image.*')) { |
|
|
alert("Error: Please select a valid image file."); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const reader = new FileReader(); |
|
|
reader.onload = (event) => { |
|
|
inputImage.src = event.target.result; |
|
|
inputImage.classList.remove('hidden'); |
|
|
inputPlaceholder.classList.add('hidden'); |
|
|
|
|
|
|
|
|
processImage(file); |
|
|
} |
|
|
reader.readAsDataURL(file); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (fileInput) { |
|
|
fileInput.addEventListener('change', (e) => { |
|
|
if (e.target.files && e.target.files[0]) { |
|
|
const file = e.target.files[0]; |
|
|
|
|
|
|
|
|
if (!file.type.match('image.*')) { |
|
|
alert("Error: Please select a valid image file (jpg, png, etc)."); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const reader = new FileReader(); |
|
|
|
|
|
reader.onload = (event) => { |
|
|
|
|
|
inputImage.src = event.target.result; |
|
|
|
|
|
|
|
|
inputImage.classList.remove('hidden'); |
|
|
inputPlaceholder.classList.add('hidden'); |
|
|
|
|
|
|
|
|
processImage(file); |
|
|
} |
|
|
|
|
|
|
|
|
reader.readAsDataURL(file); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const clapperIcon = document.getElementById('clapperIcon'); |
|
|
const movieOverlay = document.getElementById('movieOverlay'); |
|
|
const cinematicTitle = document.querySelector('.cinematic-title'); |
|
|
const cinematicCredits = document.querySelector('.cinematic-credits'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function playMagicalSound() { |
|
|
try { |
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext; |
|
|
if (!AudioContext) return; |
|
|
|
|
|
const ctx = new AudioContext(); |
|
|
const now = ctx.currentTime; |
|
|
const duration = 6.5; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const masterGain = ctx.createGain(); |
|
|
const masterFilter = ctx.createBiquadFilter(); |
|
|
masterFilter.type = 'lowpass'; |
|
|
masterFilter.frequency.value = 4000; |
|
|
masterFilter.Q.value = 0.7; |
|
|
masterGain.connect(masterFilter); |
|
|
masterFilter.connect(ctx.destination); |
|
|
|
|
|
|
|
|
masterGain.gain.setValueAtTime(0, now); |
|
|
masterGain.gain.linearRampToValueAtTime(0.08, now + 1.5); |
|
|
masterGain.gain.linearRampToValueAtTime(0.10, now + 3.0); |
|
|
masterGain.gain.linearRampToValueAtTime(0.08, now + 5.0); |
|
|
masterGain.gain.exponentialRampToValueAtTime(0.001, now + duration); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const bass = ctx.createOscillator(); |
|
|
const bassGain = ctx.createGain(); |
|
|
const bassFilter = ctx.createBiquadFilter(); |
|
|
|
|
|
bass.type = 'triangle'; |
|
|
bass.frequency.value = 82.41; |
|
|
|
|
|
bassFilter.type = 'lowpass'; |
|
|
bassFilter.frequency.value = 200; |
|
|
|
|
|
bassGain.gain.setValueAtTime(0, now); |
|
|
bassGain.gain.linearRampToValueAtTime(0.12, now + 2.0); |
|
|
bassGain.gain.linearRampToValueAtTime(0.10, now + 5.0); |
|
|
bassGain.gain.exponentialRampToValueAtTime(0.001, now + duration); |
|
|
|
|
|
bass.connect(bassFilter); |
|
|
bassFilter.connect(bassGain); |
|
|
bassGain.connect(masterGain); |
|
|
bass.start(now); |
|
|
bass.stop(now + duration); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const padNotes = [164.81, 207.65, 246.94, 311.13]; |
|
|
padNotes.forEach((freq, i) => { |
|
|
const osc = ctx.createOscillator(); |
|
|
const gain = ctx.createGain(); |
|
|
const filter = ctx.createBiquadFilter(); |
|
|
|
|
|
osc.type = 'triangle'; |
|
|
osc.frequency.value = freq; |
|
|
osc.detune.value = (Math.random() - 0.5) * 4; |
|
|
|
|
|
filter.type = 'lowpass'; |
|
|
filter.frequency.value = 2500; |
|
|
filter.Q.value = 0.5; |
|
|
|
|
|
|
|
|
gain.gain.setValueAtTime(0, now + 0.5); |
|
|
gain.gain.linearRampToValueAtTime(0.05, now + 2.0 + (i * 0.15)); |
|
|
gain.gain.linearRampToValueAtTime(0.04, now + 5.5); |
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + duration); |
|
|
|
|
|
osc.connect(filter); |
|
|
filter.connect(gain); |
|
|
gain.connect(masterGain); |
|
|
osc.start(now + 0.5); |
|
|
osc.stop(now + duration); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const melody = [ |
|
|
{ freq: 329.63, time: 1.8, duration: 0.8 }, |
|
|
{ freq: 392.00, time: 2.8, duration: 0.6 }, |
|
|
{ freq: 493.88, time: 3.6, duration: 1.0 }, |
|
|
{ freq: 392.00, time: 4.8, duration: 0.8 } |
|
|
]; |
|
|
|
|
|
melody.forEach(note => { |
|
|
const osc = ctx.createOscillator(); |
|
|
const gain = ctx.createGain(); |
|
|
const filter = ctx.createBiquadFilter(); |
|
|
|
|
|
osc.type = 'sine'; |
|
|
osc.frequency.value = note.freq; |
|
|
|
|
|
filter.type = 'lowpass'; |
|
|
filter.frequency.value = 3000; |
|
|
|
|
|
|
|
|
const start = now + note.time; |
|
|
const end = start + note.duration; |
|
|
|
|
|
gain.gain.setValueAtTime(0, start); |
|
|
gain.gain.linearRampToValueAtTime(0.04, start + 0.05); |
|
|
gain.gain.linearRampToValueAtTime(0.03, start + 0.15); |
|
|
gain.gain.linearRampToValueAtTime(0.025, end - 0.2); |
|
|
gain.gain.exponentialRampToValueAtTime(0.001, end); |
|
|
|
|
|
osc.connect(filter); |
|
|
filter.connect(gain); |
|
|
gain.connect(masterGain); |
|
|
osc.start(start); |
|
|
osc.stop(end); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const shimmer = ctx.createOscillator(); |
|
|
const shimmerGain = ctx.createGain(); |
|
|
const shimmerFilter = ctx.createBiquadFilter(); |
|
|
|
|
|
shimmer.type = 'sine'; |
|
|
shimmer.frequency.value = 987.77; |
|
|
|
|
|
|
|
|
const lfo = ctx.createOscillator(); |
|
|
const lfoGain = ctx.createGain(); |
|
|
lfo.type = 'sine'; |
|
|
lfo.frequency.value = 2.5; |
|
|
lfoGain.gain.value = 4; |
|
|
lfo.connect(lfoGain); |
|
|
lfoGain.connect(shimmer.detune); |
|
|
|
|
|
shimmerFilter.type = 'highpass'; |
|
|
shimmerFilter.frequency.value = 800; |
|
|
|
|
|
shimmerGain.gain.setValueAtTime(0, now + 2.5); |
|
|
shimmerGain.gain.linearRampToValueAtTime(0.015, now + 3.5); |
|
|
shimmerGain.gain.linearRampToValueAtTime(0.012, now + 5.5); |
|
|
shimmerGain.gain.exponentialRampToValueAtTime(0.001, now + duration); |
|
|
|
|
|
shimmer.connect(shimmerFilter); |
|
|
shimmerFilter.connect(shimmerGain); |
|
|
shimmerGain.connect(masterGain); |
|
|
|
|
|
lfo.start(now + 2.5); |
|
|
shimmer.start(now + 2.5); |
|
|
lfo.stop(now + duration); |
|
|
shimmer.stop(now + duration); |
|
|
|
|
|
} catch (e) { |
|
|
console.log("Audio not supported or blocked", e); |
|
|
} |
|
|
} |
|
|
|
|
|
if (clapperIcon && movieOverlay) { |
|
|
clapperIcon.addEventListener('click', triggerCinematicIntro); |
|
|
} |
|
|
|
|
|
function triggerCinematicIntro() { |
|
|
|
|
|
playMagicalSound(); |
|
|
|
|
|
|
|
|
movieOverlay.classList.remove('hidden'); |
|
|
|
|
|
void movieOverlay.offsetWidth; |
|
|
movieOverlay.classList.add('playing'); |
|
|
|
|
|
|
|
|
cinematicTitle.classList.remove('animate-title'); |
|
|
cinematicCredits.classList.remove('animate-credits'); |
|
|
|
|
|
void cinematicTitle.offsetWidth; |
|
|
|
|
|
cinematicTitle.classList.add('animate-title'); |
|
|
cinematicCredits.classList.add('animate-credits'); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
movieOverlay.classList.remove('playing'); |
|
|
setTimeout(() => { |
|
|
movieOverlay.classList.add('hidden'); |
|
|
}, 1000); |
|
|
}, 5500); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function resizeImage(file, maxDimension, quality) { |
|
|
return new Promise((resolve, reject) => { |
|
|
const reader = new FileReader(); |
|
|
reader.onload = (e) => { |
|
|
const img = new Image(); |
|
|
img.onload = () => { |
|
|
let width = img.width; |
|
|
let height = img.height; |
|
|
|
|
|
if (width > maxDimension || height > maxDimension) { |
|
|
if (width > height) { |
|
|
height = Math.round(height * (maxDimension / width)); |
|
|
width = maxDimension; |
|
|
} else { |
|
|
width = Math.round(width * (maxDimension / height)); |
|
|
height = maxDimension; |
|
|
} |
|
|
} |
|
|
|
|
|
const canvas = document.createElement('canvas'); |
|
|
canvas.width = width; |
|
|
canvas.height = height; |
|
|
const ctx = canvas.getContext('2d'); |
|
|
ctx.drawImage(img, 0, 0, width, height); |
|
|
|
|
|
canvas.toBlob((blob) => { |
|
|
resolve(blob); |
|
|
}, 'image/jpeg', quality); |
|
|
}; |
|
|
img.onerror = (err) => reject(new Error("Image load failed")); |
|
|
img.src = e.target.result; |
|
|
}; |
|
|
reader.onerror = (err) => reject(new Error("File read failed")); |
|
|
reader.readAsDataURL(file); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function processImage(file) { |
|
|
|
|
|
|
|
|
outputImage.classList.add('hidden'); |
|
|
outputPlaceholder.classList.add('hidden'); |
|
|
loadingSpinner.classList.remove('hidden'); |
|
|
saveBtn.classList.add('hidden'); |
|
|
if (copyBtn) copyBtn.classList.add('hidden'); |
|
|
hasProcessedImage = false; |
|
|
isShowingOriginal = false; |
|
|
|
|
|
|
|
|
if (clapperIcon) clapperIcon.classList.add('clapper-processing'); |
|
|
|
|
|
try { |
|
|
|
|
|
console.log("Starting image resize..."); |
|
|
const resizedBlob = await resizeImage(file, 1024, 0.85); |
|
|
console.log("Resize complete. Original size:", file.size, "New size:", resizedBlob.size); |
|
|
|
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append('file', resizedBlob, "optimized_image.jpg"); |
|
|
|
|
|
console.log("Sending upload request..."); |
|
|
|
|
|
const response = await fetch('/cartoonize', { |
|
|
method: 'POST', |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
console.log("Response received:", response.status); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`Server error: ${response.statusText}`); |
|
|
} |
|
|
|
|
|
|
|
|
const blob = await response.blob(); |
|
|
console.log("Result blob received size:", blob.size); |
|
|
|
|
|
|
|
|
const imageUrl = URL.createObjectURL(blob); |
|
|
|
|
|
|
|
|
|
|
|
outputImage.src = imageUrl; |
|
|
outputImage.classList.remove('hidden'); |
|
|
hasProcessedImage = true; |
|
|
|
|
|
|
|
|
saveBtn.href = imageUrl; |
|
|
|
|
|
|
|
|
|
|
|
let originalName = file.name; |
|
|
const lastDotIndex = originalName.lastIndexOf('.'); |
|
|
|
|
|
|
|
|
if (lastDotIndex !== -1) { |
|
|
originalName = originalName.substring(0, lastDotIndex); |
|
|
} |
|
|
|
|
|
let downloadName = "Cartoonized_" + originalName + ".jpg"; |
|
|
saveBtn.download = downloadName; |
|
|
|
|
|
saveBtn.classList.remove('hidden'); |
|
|
if (copyBtn) copyBtn.classList.remove('hidden'); |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
console.error("Error in processImage:", error); |
|
|
alert(`Failed: ${error.message}`); |
|
|
|
|
|
outputPlaceholder.classList.remove('hidden'); |
|
|
outputPlaceholder.innerHTML = `<span>Error: ${error.message}</span>`; |
|
|
} finally { |
|
|
|
|
|
loadingSpinner.classList.add('hidden'); |
|
|
|
|
|
const statusText = loadingSpinner.querySelector('p'); |
|
|
if (statusText) statusText.textContent = "Sketching your cartoon..."; |
|
|
|
|
|
|
|
|
if (clapperIcon) clapperIcon.classList.remove('clapper-processing'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function toggleBeforeAfter() { |
|
|
if (!hasProcessedImage) { |
|
|
showNotification("Process an image first!"); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!isShowingOriginal) { |
|
|
|
|
|
const originalSrc = inputImage.src; |
|
|
outputImage.dataset.cartoonSrc = outputImage.src; |
|
|
outputImage.src = originalSrc; |
|
|
isShowingOriginal = true; |
|
|
showNotification("Showing Original"); |
|
|
} else { |
|
|
|
|
|
outputImage.src = outputImage.dataset.cartoonSrc; |
|
|
isShowingOriginal = false; |
|
|
showNotification("Showing Cartoonized"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function resetApp() { |
|
|
inputImage.src = ''; |
|
|
inputImage.classList.add('hidden'); |
|
|
inputPlaceholder.classList.remove('hidden'); |
|
|
|
|
|
outputImage.src = ''; |
|
|
outputImage.classList.add('hidden'); |
|
|
outputPlaceholder.classList.remove('hidden'); |
|
|
|
|
|
saveBtn.classList.add('hidden'); |
|
|
if (copyBtn) copyBtn.classList.add('hidden'); |
|
|
loadingSpinner.classList.add('hidden'); |
|
|
|
|
|
fileInput.value = ''; |
|
|
hasProcessedImage = false; |
|
|
isShowingOriginal = false; |
|
|
|
|
|
showNotification("Reset Complete"); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function showKeyboardHelp() { |
|
|
const helpMessage = ` |
|
|
⌨️ KEYBOARD SHORTCUTS: |
|
|
|
|
|
U - Upload Image |
|
|
P - Take Photo (Camera) |
|
|
S - Save/Download Image |
|
|
SPACE - Toggle Before/After |
|
|
R - Reset Application |
|
|
C - Play Cinematic Intro |
|
|
F - Toggle Fullscreen |
|
|
T - Toggle Theme (Dark/Light) |
|
|
ESC - Close Overlays |
|
|
? or H - Show This Help |
|
|
|
|
|
💡 TIP: Use these shortcuts for faster workflow! |
|
|
`.trim(); |
|
|
|
|
|
alert(helpMessage); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function showNotification(message) { |
|
|
|
|
|
const existing = document.querySelector('.keyboard-notification'); |
|
|
if (existing) { |
|
|
existing.remove(); |
|
|
} |
|
|
|
|
|
const notification = document.createElement('div'); |
|
|
notification.className = 'keyboard-notification'; |
|
|
notification.textContent = message; |
|
|
document.body.appendChild(notification); |
|
|
|
|
|
|
|
|
setTimeout(() => notification.classList.add('show'), 10); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
notification.classList.remove('show'); |
|
|
setTimeout(() => notification.remove(), 300); |
|
|
}, 2000); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
|
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { |
|
|
return; |
|
|
} |
|
|
|
|
|
const key = e.key.toLowerCase(); |
|
|
|
|
|
switch (key) { |
|
|
case 'u': |
|
|
|
|
|
e.preventDefault(); |
|
|
fileInput.click(); |
|
|
showNotification("Upload Image"); |
|
|
break; |
|
|
|
|
|
case 's': |
|
|
|
|
|
e.preventDefault(); |
|
|
if (!saveBtn.classList.contains('hidden')) { |
|
|
saveBtn.click(); |
|
|
showNotification("Downloading..."); |
|
|
} else { |
|
|
showNotification("No image to save yet!"); |
|
|
} |
|
|
break; |
|
|
|
|
|
case ' ': |
|
|
|
|
|
e.preventDefault(); |
|
|
toggleBeforeAfter(); |
|
|
break; |
|
|
|
|
|
case 'r': |
|
|
|
|
|
e.preventDefault(); |
|
|
if (confirm("Reset the application? This will clear all images.")) { |
|
|
resetApp(); |
|
|
} |
|
|
break; |
|
|
|
|
|
case 'c': |
|
|
|
|
|
e.preventDefault(); |
|
|
triggerCinematicIntro(); |
|
|
showNotification("Action!"); |
|
|
break; |
|
|
|
|
|
case 'escape': |
|
|
|
|
|
e.preventDefault(); |
|
|
if (movieOverlay && !movieOverlay.classList.contains('hidden')) { |
|
|
movieOverlay.classList.remove('playing'); |
|
|
setTimeout(() => movieOverlay.classList.add('hidden'), 300); |
|
|
} |
|
|
showNotification("Closed"); |
|
|
break; |
|
|
|
|
|
case '?': |
|
|
|
|
|
e.preventDefault(); |
|
|
showKeyboardHelp(); |
|
|
break; |
|
|
|
|
|
case 'h': |
|
|
|
|
|
e.preventDefault(); |
|
|
showKeyboardHelp(); |
|
|
break; |
|
|
|
|
|
case 'f': |
|
|
|
|
|
e.preventDefault(); |
|
|
toggleFullscreen(); |
|
|
break; |
|
|
|
|
|
case 'p': |
|
|
|
|
|
e.preventDefault(); |
|
|
if (cameraInput) { |
|
|
cameraInput.click(); |
|
|
showNotification("📷 Camera Opening..."); |
|
|
} |
|
|
break; |
|
|
|
|
|
case 't': |
|
|
|
|
|
e.preventDefault(); |
|
|
toggleTheme(); |
|
|
break; |
|
|
|
|
|
default: |
|
|
|
|
|
break; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
window.addEventListener('load', () => { |
|
|
console.log(` |
|
|
╔════════════════════════════════════════╗ |
|
|
║ WHITE-BOX CARTOONIZATION v2.0 ║ |
|
|
║ Press '?' for keyboard shortcuts ║ |
|
|
╚════════════════════════════════════════╝ |
|
|
`); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const themeToggle = document.getElementById('themeToggle'); |
|
|
const THEME_KEY = 'wbc-theme'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function initTheme() { |
|
|
const savedTheme = localStorage.getItem(THEME_KEY); |
|
|
|
|
|
if (savedTheme === 'light') { |
|
|
document.body.classList.add('light-mode'); |
|
|
updateThemeIcon(true); |
|
|
} else { |
|
|
|
|
|
document.body.classList.remove('light-mode'); |
|
|
updateThemeIcon(false); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function toggleTheme() { |
|
|
const isLightMode = document.body.classList.toggle('light-mode'); |
|
|
|
|
|
|
|
|
localStorage.setItem(THEME_KEY, isLightMode ? 'light' : 'dark'); |
|
|
|
|
|
|
|
|
updateThemeIcon(isLightMode); |
|
|
|
|
|
|
|
|
showNotification(isLightMode ? 'Light Mode' : 'Dark Mode'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateThemeIcon(isLightMode) { |
|
|
if (themeToggle) { |
|
|
themeToggle.textContent = isLightMode ? '☀️' : '🌙'; |
|
|
themeToggle.title = isLightMode ? 'Switch to Dark Mode' : 'Switch to Light Mode'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (themeToggle) { |
|
|
themeToggle.addEventListener('click', toggleTheme); |
|
|
} |
|
|
|
|
|
|
|
|
if (copyBtn) { |
|
|
copyBtn.addEventListener('click', async () => { |
|
|
if (!outputImage.src) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!navigator.clipboard) { |
|
|
alert("Clipboard API not available. This feature requires HTTPS.\n\nOn mobile: Long-press the image to copy/save."); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
|
|
|
const canvas = document.createElement('canvas'); |
|
|
canvas.width = outputImage.naturalWidth; |
|
|
canvas.height = outputImage.naturalHeight; |
|
|
const ctx = canvas.getContext('2d'); |
|
|
ctx.drawImage(outputImage, 0, 0); |
|
|
|
|
|
|
|
|
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png')); |
|
|
|
|
|
if (!blob) { |
|
|
showNotification("Copy failed (Blob error)"); |
|
|
return; |
|
|
} |
|
|
|
|
|
await navigator.clipboard.write([ |
|
|
new ClipboardItem({ |
|
|
'image/png': blob |
|
|
}) |
|
|
]); |
|
|
showNotification("Image Copied"); |
|
|
|
|
|
} catch (err) { |
|
|
console.error("Copy failed:", err); |
|
|
|
|
|
alert("Copy failed (Browser restriction).\n\nPlease Long-Press the image to copy."); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
initTheme(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (fullscreenBtn) { |
|
|
fullscreenBtn.addEventListener('click', toggleFullscreen); |
|
|
} |
|
|
|
|
|
function toggleFullscreen() { |
|
|
if (!document.fullscreenElement) { |
|
|
|
|
|
document.documentElement.requestFullscreen().then(() => { |
|
|
fullscreenBtn.textContent = '⛶'; |
|
|
showNotification("Fullscreen Mode"); |
|
|
}).catch(err => { |
|
|
console.error('Error attempting fullscreen:', err); |
|
|
showNotification("Fullscreen not available"); |
|
|
}); |
|
|
} else { |
|
|
|
|
|
document.exitFullscreen().then(() => { |
|
|
fullscreenBtn.textContent = '⛶'; |
|
|
showNotification("Exited Fullscreen"); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('fullscreenchange', () => { |
|
|
if (document.fullscreenElement) { |
|
|
fullscreenBtn.textContent = '↙️'; |
|
|
} else { |
|
|
fullscreenBtn.textContent = '⛶'; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let scale = 1; |
|
|
let lastDistance = 0; |
|
|
let isPinching = false; |
|
|
|
|
|
if (outputImage && isMobile) { |
|
|
let touchStartX = 0; |
|
|
let touchEndX = 0; |
|
|
|
|
|
|
|
|
outputImage.addEventListener('touchstart', (e) => { |
|
|
if (e.touches.length === 2) { |
|
|
isPinching = true; |
|
|
lastDistance = getDistance(e.touches[0], e.touches[1]); |
|
|
e.preventDefault(); |
|
|
} else if (e.touches.length === 1) { |
|
|
touchStartX = e.touches[0].clientX; |
|
|
} |
|
|
}, { passive: false }); |
|
|
|
|
|
outputImage.addEventListener('touchmove', (e) => { |
|
|
if (isPinching && e.touches.length === 2) { |
|
|
const currentDistance = getDistance(e.touches[0], e.touches[1]); |
|
|
const delta = currentDistance - lastDistance; |
|
|
|
|
|
scale += delta * 0.01; |
|
|
scale = Math.min(Math.max(0.5, scale), 3); |
|
|
|
|
|
outputImage.style.transform = `scale(${scale})`; |
|
|
outputImage.style.transition = 'none'; |
|
|
|
|
|
lastDistance = currentDistance; |
|
|
e.preventDefault(); |
|
|
} |
|
|
}, { passive: false }); |
|
|
|
|
|
outputImage.addEventListener('touchend', (e) => { |
|
|
if (isPinching && e.touches.length < 2) { |
|
|
isPinching = false; |
|
|
outputImage.style.transition = 'transform 0.3s ease'; |
|
|
|
|
|
if (scale !== 1) { |
|
|
showNotification(`Zoom: ${Math.round(scale * 100)}%`); |
|
|
} |
|
|
} else if (e.touches.length === 0 && touchStartX > 0) { |
|
|
touchEndX = e.changedTouches[0].clientX; |
|
|
handleSwipe(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
function handleSwipe() { |
|
|
const swipeThreshold = 50; |
|
|
const diff = touchStartX - touchEndX; |
|
|
|
|
|
if (Math.abs(diff) > swipeThreshold && hasProcessedImage) { |
|
|
if (diff > 0) { |
|
|
|
|
|
toggleBeforeAfter(); |
|
|
} else { |
|
|
|
|
|
toggleBeforeAfter(); |
|
|
} |
|
|
} |
|
|
|
|
|
touchStartX = 0; |
|
|
touchEndX = 0; |
|
|
} |
|
|
|
|
|
|
|
|
let lastTap = 0; |
|
|
outputImage.addEventListener('touchend', (e) => { |
|
|
const currentTime = new Date().getTime(); |
|
|
const tapLength = currentTime - lastTap; |
|
|
|
|
|
if (tapLength < 300 && tapLength > 0) { |
|
|
|
|
|
scale = 1; |
|
|
outputImage.style.transform = 'scale(1)'; |
|
|
outputImage.style.transition = 'transform 0.3s ease'; |
|
|
showNotification("Zoom Reset"); |
|
|
e.preventDefault(); |
|
|
} |
|
|
lastTap = currentTime; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function getDistance(touch1, touch2) { |
|
|
const dx = touch1.clientX - touch2.clientX; |
|
|
const dy = touch1.clientY - touch2.clientY; |
|
|
return Math.sqrt(dx * dx + dy * dy); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[inputImage, outputImage].forEach(img => { |
|
|
if (img && isMobile) { |
|
|
img.addEventListener('touchstart', (e) => { |
|
|
if (e.touches.length > 1) { |
|
|
e.preventDefault(); |
|
|
} |
|
|
}, { passive: false }); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isMobile) { |
|
|
const mainContainer = document.querySelector('.main-container'); |
|
|
if (mainContainer) { |
|
|
let startY = 0; |
|
|
|
|
|
mainContainer.addEventListener('touchstart', (e) => { |
|
|
startY = e.touches[0].clientY; |
|
|
}, { passive: true }); |
|
|
|
|
|
mainContainer.addEventListener('touchmove', (e) => { |
|
|
const y = e.touches[0].clientY; |
|
|
|
|
|
if (window.scrollY === 0 && y > startY) { |
|
|
e.preventDefault(); |
|
|
} |
|
|
}, { passive: false }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isMobile) { |
|
|
window.addEventListener('orientationchange', () => { |
|
|
showNotification("Orientation Changed"); |
|
|
|
|
|
if (outputImage) { |
|
|
scale = 1; |
|
|
outputImage.style.transform = 'scale(1)'; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const securityOverlay = document.getElementById('securityOverlay'); |
|
|
const dismissSecurity = document.getElementById('dismissSecurity'); |
|
|
|
|
|
function showSecurityNotice() { |
|
|
if (securityOverlay) { |
|
|
securityOverlay.classList.remove('hidden'); |
|
|
securityOverlay.classList.add('playing'); |
|
|
|
|
|
|
|
|
playMagicalSound(); |
|
|
} |
|
|
} |
|
|
|
|
|
function hideSecurityNotice() { |
|
|
if (securityOverlay) { |
|
|
securityOverlay.classList.remove('playing'); |
|
|
setTimeout(() => securityOverlay.classList.add('hidden'), 500); |
|
|
} |
|
|
} |
|
|
|
|
|
if (dismissSecurity) { |
|
|
dismissSecurity.addEventListener('click', hideSecurityNotice); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('contextmenu', (e) => { |
|
|
e.preventDefault(); |
|
|
showSecurityNotice(); |
|
|
return false; |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
|
|
|
if (e.key === 'F12') { |
|
|
e.preventDefault(); |
|
|
showSecurityNotice(); |
|
|
} |
|
|
|
|
|
|
|
|
const key = e.key.toLowerCase(); |
|
|
if (e.ctrlKey && (key === 'u' || (e.shiftKey && (key === 'i' || key === 'j' || key === 'c')))) { |
|
|
e.preventDefault(); |
|
|
showSecurityNotice(); |
|
|
} |
|
|
|
|
|
|
|
|
if (e.ctrlKey && key === 's') { |
|
|
e.preventDefault(); |
|
|
showNotification("Please use the 'Save' button for cartoon results!"); |
|
|
} |
|
|
|
|
|
|
|
|
if (e.key === 'Escape') { |
|
|
hideSecurityNotice(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('dragstart', (e) => { |
|
|
if (e.target.tagName === 'IMG' && (!e.target.id || !e.target.id.includes('outputImage'))) { |
|
|
e.preventDefault(); |
|
|
} |
|
|
}); |
|
|
|
|
|
console.log("Security Protocols Initialized | Amey Thakur · Hasan Rizvi · Mega Satish"); |
|
|
|
|
|
|
|
|
|