@@ -18,11 +18,19 @@ const imageCanvas = document.querySelector('#image-canvas');
1818const imgInput = document . querySelector ( '#image-input' ) ;
1919const resetButton = document . querySelector ( '#reset-btn' ) ;
2020const downloadButton = document . querySelector ( '#download-btn' ) ;
21+ const animButton = document . querySelector ( '#anim-btn' ) ;
22+ const recordButton = document . querySelector ( '#record-btn' ) ;
2123const dropZone = document . querySelector ( '.bottom' ) ;
2224const canvasCtx = imageCanvas . getContext ( '2d' ) ;
2325let file = null ;
2426let image = null ;
2527
28+ let isAnimating = false ;
29+ let animationId = null ;
30+ let mediaRecorder = null ;
31+ let recordedChunks = [ ] ;
32+ let isRecording = false ;
33+
2634function createFilterElement ( name , unit = '%' , value , min , max ) {
2735 const div = document . createElement ( 'div' ) ;
2836 div . classList . add ( 'filter' ) ;
@@ -52,7 +60,7 @@ function createFilterElement(name, unit = '%', value, min, max) {
5260 input . addEventListener ( 'input' , ( event ) => {
5361 filters [ name ] . value = input . value ;
5462 valDisplay . innerText = `${ input . value } ${ unit } ` ;
55- applyFilters ( ) ;
63+ if ( ! isAnimating ) applyFilters ( ) ;
5664 } ) ;
5765
5866 return div ;
@@ -124,6 +132,7 @@ function loadImage(file) {
124132 imageCanvas . width = img . width ;
125133 imageCanvas . height = img . height ;
126134 applyFilters ( ) ;
135+ if ( recordButton ) recordButton . disabled = false ;
127136 } ;
128137}
129138
@@ -132,10 +141,8 @@ imgInput.addEventListener('change', () => {
132141 if ( file ) loadImage ( file ) ;
133142} ) ;
134143
135- function applyFilters ( ) {
136- if ( ! image ) return ;
137- canvasCtx . clearRect ( 0 , 0 , imageCanvas . width , imageCanvas . height ) ;
138- canvasCtx . filter = `
144+ function getFilterString ( ) {
145+ return `
139146 brightness(${ filters . brightness . value } ${ filters . brightness . unit } )
140147 contrast(${ filters . contrast . value } ${ filters . contrast . unit } )
141148 saturate(${ filters . saturation . value } ${ filters . saturation . unit } )
@@ -146,11 +153,123 @@ function applyFilters() {
146153 opacity(${ filters . opacity . value } ${ filters . opacity . unit } )
147154 invert(${ filters . invert . value } ${ filters . invert . unit } )
148155 ` . trim ( ) ;
156+ }
157+
158+ function applyFilters ( ) {
159+ if ( ! image ) return ;
160+ canvasCtx . clearRect ( 0 , 0 , imageCanvas . width , imageCanvas . height ) ;
161+ canvasCtx . filter = getFilterString ( ) ;
149162 canvasCtx . drawImage ( image , 0 , 0 ) ;
150163}
151164
165+ // --- Animation Logic ---
166+
167+ function toggleAnimation ( ) {
168+ if ( isAnimating ) {
169+ isAnimating = false ;
170+ cancelAnimationFrame ( animationId ) ;
171+ animButton . innerHTML = `<i class="ri-movie-line"></i> Animate` ;
172+ applyFilters ( ) ; // Reset to static
173+ } else {
174+ if ( ! image ) return ;
175+ isAnimating = true ;
176+ animButton . innerHTML = `<i class="ri-pause-line"></i> Stop` ;
177+ animateGlitch ( ) ;
178+ }
179+ }
180+
181+ animButton . addEventListener ( 'click' , toggleAnimation ) ;
182+
183+ function animateGlitch ( ) {
184+ if ( ! isAnimating ) return ;
185+
186+ // 1. Draw base filtered image
187+ canvasCtx . clearRect ( 0 , 0 , imageCanvas . width , imageCanvas . height ) ;
188+ canvasCtx . filter = getFilterString ( ) ;
189+ canvasCtx . drawImage ( image , 0 , 0 ) ;
190+
191+ // 2. Add Glitch Effects
192+ if ( Math . random ( ) > 0.8 ) {
193+ const sliceHeight = Math . random ( ) * 50 + 5 ;
194+ const sliceY = Math . random ( ) * imageCanvas . height ;
195+ const offset = ( Math . random ( ) - 0.5 ) * 40 ;
196+ canvasCtx . drawImage (
197+ imageCanvas ,
198+ 0 , sliceY , imageCanvas . width , sliceHeight , // Source
199+ offset , sliceY , imageCanvas . width , sliceHeight // Dest
200+ ) ;
201+ }
202+
203+ // RGB Split / Color Shift
204+ if ( Math . random ( ) > 0.9 ) {
205+ const offset = ( Math . random ( ) - 0.5 ) * 10 ;
206+ canvasCtx . globalCompositeOperation = 'screen' ;
207+ canvasCtx . filter = getFilterString ( ) + ` hue-rotate(90deg) opacity(0.5)` ;
208+ canvasCtx . drawImage ( image , offset , 0 ) ;
209+ canvasCtx . globalCompositeOperation = 'source-over' ;
210+ }
211+
212+ animationId = requestAnimationFrame ( animateGlitch ) ;
213+ }
214+
215+ // --- Recording Logic ---
216+
217+ recordButton . addEventListener ( 'click' , ( ) => {
218+ if ( isRecording ) {
219+ stopRecording ( ) ;
220+ } else {
221+ startRecording ( ) ;
222+ }
223+ } ) ;
224+
225+ function startRecording ( ) {
226+ if ( ! image ) return ;
227+
228+ // Force animation if not active
229+ if ( ! isAnimating ) toggleAnimation ( ) ;
230+
231+ const stream = imageCanvas . captureStream ( 30 ) ;
232+ mediaRecorder = new MediaRecorder ( stream , { mimeType : 'video/webm;codecs=vp9' } ) ;
233+
234+ recordedChunks = [ ] ;
235+ isRecording = true ;
236+ recordButton . innerHTML = `<i class="ri-stop-circle-line"></i> Stop Rec` ;
237+ recordButton . classList . add ( 'recording-active' ) ;
238+
239+ mediaRecorder . ondataavailable = ( e ) => {
240+ if ( e . data . size > 0 ) recordedChunks . push ( e . data ) ;
241+ } ;
242+
243+ mediaRecorder . onstop = exportVideo ;
244+ mediaRecorder . start ( ) ;
245+ }
246+
247+ function stopRecording ( ) {
248+ isRecording = false ;
249+ recordButton . innerHTML = `<i class="ri-record-circle-line"></i> Record` ;
250+ recordButton . classList . remove ( 'recording-active' ) ;
251+ mediaRecorder . stop ( ) ;
252+ if ( isAnimating ) toggleAnimation ( ) ;
253+ }
254+
255+ function exportVideo ( ) {
256+ const blob = new Blob ( recordedChunks , { type : 'video/webm' } ) ;
257+ const url = URL . createObjectURL ( blob ) ;
258+ const a = document . createElement ( 'a' ) ;
259+ a . style . display = 'none' ;
260+ a . href = url ;
261+ a . download = `glitch-animation-${ Date . now ( ) } .webm` ;
262+ document . body . appendChild ( a ) ;
263+ a . click ( ) ;
264+ setTimeout ( ( ) => {
265+ document . body . removeChild ( a ) ;
266+ window . URL . revokeObjectURL ( url ) ;
267+ } , 100 ) ;
268+ }
269+
152270resetButton . addEventListener ( 'click' , ( ) => {
153271 filters = JSON . parse ( JSON . stringify ( defaultFilters ) ) ;
272+ if ( isAnimating ) toggleAnimation ( ) ;
154273 applyFilters ( ) ;
155274 createFilters ( ) ;
156275} ) ;
0 commit comments