Spaces:
Running
Running
| // TiltShift Studio - Main JavaScript | |
| class TiltShiftEditor { | |
| constructor() { | |
| this.canvas = document.getElementById('previewCanvas'); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.originalImage = null; | |
| this.currentImage = null; | |
| this.filters = { | |
| focusPosition: 50, | |
| blurAmount: 10, | |
| focusSize: 30, | |
| effectType: 'linear' | |
| }; | |
| this.initializeEventListeners(); | |
| this.setupDragAndDrop(); | |
| } | |
| initializeEventListeners() { | |
| // File input | |
| document.getElementById('fileInput').addEventListener('change', (e) => { | |
| this.handleFileSelect(e); | |
| }); | |
| // Control sliders | |
| document.getElementById('focusPosition').addEventListener('input', (e) => { | |
| this.filters.focusPosition = e.target.value; | |
| document.getElementById('focusPositionValue').textContent = `${e.target.value}%`; | |
| this.applyTiltShift(); | |
| }); | |
| document.getElementById('blurAmount').addEventListener('input', (e) => { | |
| this.filters.blurAmount = e.target.value; | |
| document.getElementById('blurAmountValue').textContent = `${e.target.value}px`; | |
| this.applyTiltShift(); | |
| }); | |
| document.getElementById('focusSize').addEventListener('input', (e) => { | |
| this.filters.focusSize = e.target.value; | |
| document.getElementById('focusSizeValue').textContent = `${e.target.value}%`; | |
| this.applyTiltShift(); | |
| }); | |
| document.getElementById('effectType').addEventListener('change', (e) => { | |
| this.filters.effectType = e.target.value; | |
| this.applyTiltShift(); | |
| }); | |
| // Reset button | |
| document.getElementById('resetBtn').addEventListener('click', () => { | |
| this.resetFilters(); | |
| }); | |
| // Download button | |
| document.getElementById('downloadBtn').addEventListener('click', () => { | |
| this.downloadImage(); | |
| }); | |
| } | |
| setupDragAndDrop() { | |
| const dropZone = document.getElementById('dropZone'); | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, this.preventDefaults, false); | |
| }); | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, () => { | |
| dropZone.classList.add('drag-over'); | |
| }, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, () => { | |
| dropZone.classList.remove('drag-over'); | |
| }, false); | |
| }); | |
| dropZone.addEventListener('drop', (e) => { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| this.handleFiles(files); | |
| }, false); | |
| } | |
| preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| handleFileSelect(e) { | |
| const files = e.target.files; | |
| this.handleFiles(files); | |
| } | |
| handleFiles(files) { | |
| if (files.length > 0) { | |
| const file = files[0]; | |
| if (file.type.match('image.*')) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| this.loadImage(e.target.result); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| } | |
| } | |
| loadImage(src) { | |
| const img = new Image(); | |
| img.onload = () => { | |
| this.originalImage = img; | |
| this.currentImage = img; | |
| this.setupCanvas(); | |
| this.applyTiltShift(); | |
| document.getElementById('editorSection').classList.remove('hidden'); | |
| // Scroll to editor section | |
| document.getElementById('editorSection').scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| }; | |
| img.src = src; | |
| } | |
| setupCanvas() { | |
| const container = this.canvas.parentElement; | |
| const maxWidth = container.clientWidth - 48; // Account for padding | |
| const maxHeight = 384; // Max height from design | |
| let width = this.originalImage.width; | |
| let height = this.originalImage.height; | |
| // Maintain aspect ratio while fitting within constraints | |
| if (width > maxWidth) { | |
| height = (maxWidth / width) * height; | |
| width = maxWidth; | |
| } | |
| if (height > maxHeight) { | |
| width = (maxHeight / height) * width; | |
| height = maxHeight; | |
| } | |
| this.canvas.width = width; | |
| this.canvas.height = height; | |
| } | |
| applyTiltShift() { | |
| if (!this.originalImage) return; | |
| // Clear canvas | |
| this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |
| // Draw original image | |
| this.ctx.drawImage(this.originalImage, 0, 0, this.canvas.width, this.canvas.height); | |
| // Apply blur effect based on tilt-shift parameters | |
| this.applyBlurEffect(); | |
| this.updateFocusIndicator(); | |
| } | |
| applyBlurEffect() { | |
| const { focusPosition, blurAmount, focusSize, effectType } = this.filters; | |
| const canvas = this.canvas; | |
| const ctx = this.ctx; | |
| // Create a temporary canvas for blur operations | |
| const tempCanvas = document.createElement('canvas'); | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| // Copy current image to temp canvas | |
| tempCtx.drawImage(canvas, 0, 0); | |
| // Apply blur to temp canvas | |
| this.applyGaussianBlur(tempCanvas, tempCtx, blurAmount); | |
| // Calculate focus area | |
| const focusStart = (focusPosition / 100) * canvas.height - (focusSize / 200) * canvas.height; | |
| const focusEnd = (focusPosition / 100) * canvas.height + (focusSize / 200) * canvas.height; | |
| if (effectType === 'linear') { | |
| // Linear tilt-shift: sharp in the middle, blurred at top and bottom | |
| this.applyLinearGradientMask(canvas, ctx, tempCanvas, focusStart, focusEnd); | |
| } else { | |
| // Radial tilt-shift: sharp in center, blurred towards edges | |
| this.applyRadialGradientMask(canvas, ctx, tempCanvas, focusPosition); | |
| } | |
| } | |
| applyGaussianBlur(canvas, ctx, radius) { | |
| // Simple box blur approximation | |
| ctx.filter = `blur(${radius}px)`; | |
| ctx.drawImage(canvas, 0, 0); | |
| ctx.filter = 'none'; | |
| } | |
| applyLinearGradientMask(canvas, ctx, blurredCanvas, focusStart, focusEnd) { | |
| // Create gradient mask for linear tilt-shift | |
| const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); | |
| // Add color stops for the gradient | |
| gradient.addColorStop(0, 'rgba(0,0,0,0)'); | |
| gradient.addColorStop(Math.max(0, focusStart / canvas.height - 0.1), 'rgba(0,0,0,1)'); | |
| gradient.addColorStop(focusStart / canvas.height, 'rgba(0,0,0,0)'); | |
| gradient.addColorStop(focusEnd / canvas.height, 'rgba(0,0,0,0)'); | |
| gradient.addColorStop(Math.min(1, focusEnd / canvas.height + 0.1), 'rgba(0,0,0,1)'); | |
| gradient.addColorStop(1, 'rgba(0,0,0,1)'); | |
| // Draw blurred image with gradient mask | |
| ctx.save(); | |
| ctx.globalCompositeOperation = 'source-in'; | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.restore(); | |
| // Draw the sharp focus area | |
| ctx.drawImage( | |
| this.originalImage, | |
| 0, focusStart, canvas.width, focusEnd - focusStart, | |
| 0, focusStart, canvas.width, focusEnd - focusStart | |
| ); | |
| } | |
| applyRadialGradientMask(canvas, ctx, blurredCanvas, focusPosition) { | |
| // Create radial gradient mask | |
| const centerX = canvas.width / 2; | |
| const centerY = (focusPosition / 100) * canvas.height; | |
| const radius = Math.min(canvas.width, canvas.height) * 0.3; | |
| const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius); | |
| gradient.addColorStop(0, 'rgba(0,0,0,0)'); | |
| gradient.addColorStop(0.8, 'rgba(0,0,0,0)'); | |
| gradient.addColorStop(1, 'rgba(0,0,0,1)'); | |
| // Apply gradient mask | |
| ctx.save(); | |
| ctx.globalCompositeOperation = 'source-in'; | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.restore(); | |
| // Draw the sharp focus area in the center | |
| const focusSize = radius * 0.8; | |
| ctx.drawImage( | |
| this.originalImage, | |
| centerX - focusSize, centerY - focusSize, focusSize * 2, focusSize * 2, | |
| centerX - focusSize, centerY - focusSize, focusSize * 2, focusSize * 2 | |
| ); | |
| } | |
| updateFocusIndicator() { | |
| const indicator = document.getElementById('focusIndicator'); | |
| if (!this.originalImage || this.filters.effectType !== 'linear') { | |
| indicator.classList.add('hidden'); | |
| return; | |
| } | |
| const canvas = this.canvas; | |
| const focusStart = (this.filters.focusPosition / 100) * canvas.height - (this.filters.focusSize / 200) * canvas.height; | |
| const focusEnd = (this.filters.focusPosition / 100) * canvas.height + (this.filters.focusSize / 200) * canvas.height; | |
| indicator.style.top = `${focusStart}px`; | |
| indicator.style.left = '0px'; | |
| indicator.style.width = `${canvas.width}px`; | |
| indicator.style.height = `${focusEnd - focusStart}px`; | |
| indicator.classList.remove('hidden'); | |
| } | |
| resetFilters() { | |
| this.filters = { | |
| focusPosition: 50, | |
| blurAmount: 10, | |
| focusSize: 30, | |
| effectType: 'linear' | |
| }; | |
| // Update UI elements | |
| document.getElementById('focusPosition').value = 50; | |
| document.getElementById('blurAmount').value = 10; | |
| document.getElementById('focusSize').value = 30; | |
| document.getElementById('effectType').value = 'linear'; | |
| document.getElementById('focusPositionValue').textContent = '50%'; | |
| document.getElementById('blurAmountValue').textContent = '10px'; | |
| document.getElementById('focusSizeValue').textContent = '30%'; | |
| this.applyTiltShift(); | |
| } | |
| downloadImage() { | |
| if (!this.canvas) return; | |
| const link = document.createElement('a'); | |
| link.download = 'tiltshift-image.png'; | |
| link.href = this.canvas.toDataURL(); | |
| link.click(); | |
| } | |
| } | |
| // Initialize the editor when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new TiltShiftEditor(); | |
| }); | |
| // Feather icons replacement | |
| document.addEventListener('DOMContentLoaded', () => { | |
| if (typeof feather !== 'undefined') { | |
| feather.replace(); | |
| } | |
| }); |