zeroed-some/sketch / a903876

Browse files

effects

Authored by espadonne
SHA
a90387695031db870248d0b7f02ddaddf3fe0b87
Parents
57d48f1
Tree
c91719c

3 changed files

StatusFile+-
M css/styles.css 182 55
M index.html 368 6
M js/app.js 808 100
css/styles.cssmodified
@@ -1,56 +1,183 @@
1
+/* Main styles for paralASCII */
2
+
3
+/* CSS Variables */
4
+:root {
5
+    --color-primary: #0f0;
6
+    --color-secondary: #ff0;
7
+    --color-danger: #f00;
8
+    --color-background: #000;
9
+    --color-text: #0f0;
10
+    --font-mono: 'Courier New', Courier, monospace;
11
+    --char-width: 6px;
12
+    --char-height: 12px;
13
+    --control-bg: rgba(0, 0, 0, 0.8);
14
+    --control-border: rgba(0, 255, 0, 0.3);
15
+    
16
+    /* Dynamic variables set by JS */
17
+    --rainbow-speed: 2s;
18
+    --audio-brightness: 1;
19
+}
20
+
21
+/* Reset and base styles */
22
+* {
23
+    margin: 0;
24
+    padding: 0;
25
+    box-sizing: border-box;
26
+}
27
+
28
+html, body {
29
+    width: 100%;
30
+    height: 100%;
31
+    overflow: hidden;
32
+}
33
+
134
 body {
2
-      font-family: sans-serif;
3
-      text-align: center;
35
+    background-color: var(--color-background);
36
+    color: var(--color-text);
37
+    font-family: var(--font-mono);
38
+    font-size: 14px;
39
+    line-height: 1;
40
+}
41
+
42
+/* Visualizer container */
43
+#visualizer {
44
+    position: fixed;
45
+    top: 0;
46
+    left: 0;
47
+    width: 100vw;
48
+    height: 100vh;
49
+    font-size: 10px;
50
+    line-height: 1;
51
+    white-space: pre;
52
+    overflow: hidden;
53
+    letter-spacing: 0;
454
     margin: 0;
5
-      padding: 1rem;
6
-      background: #202124;
7
-      color: #eee
55
+    padding: 0;
56
+    font-family: var(--font-mono);
57
+    z-index: 1;
58
+    text-shadow: 0 0 10px currentColor, 
59
+                 0 0 20px currentColor, 
60
+                 0 0 30px currentColor;
61
+    transition: color 0.1s ease-out;
62
+}
63
+
64
+/* Info display */
65
+.info {
66
+    position: absolute;
67
+    bottom: 20px;
68
+    left: 50%;
69
+    transform: translateX(-50%);
70
+    font-size: 14px;
71
+    opacity: 0.9;
72
+    text-align: center;
73
+    z-index: 100;
74
+    background: var(--control-bg);
75
+    padding: 10px 20px;
76
+    border-radius: 5px;
77
+    text-shadow: 0 0 5px var(--color-primary);
78
+    backdrop-filter: blur(10px);
79
+    border: 1px solid var(--control-border);
80
+    transition: opacity 0.3s ease-out;
81
+}
82
+
83
+.info.hidden {
84
+    opacity: 0;
85
+    pointer-events: none;
86
+}
87
+
88
+/* Error message */
89
+.error-message {
90
+    position: absolute;
91
+    top: 50%;
92
+    left: 50%;
93
+    transform: translate(-50%, -50%);
94
+    background: var(--color-danger);
95
+    color: white;
96
+    padding: 20px 30px;
97
+    border-radius: 10px;
98
+    font-size: 16px;
99
+    box-shadow: 0 0 20px var(--color-danger);
100
+    z-index: 200;
101
+    display: none;
102
+    max-width: 80%;
103
+    text-align: center;
104
+}
105
+
106
+.error-message.visible {
107
+    display: block;
108
+    animation: shake 0.5s ease-out;
109
+}
110
+
111
+@keyframes shake {
112
+    0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
113
+    10% { transform: translate(-51%, -50%) rotate(-1deg); }
114
+    20% { transform: translate(-49%, -50%) rotate(1deg); }
115
+    30% { transform: translate(-51%, -50%) rotate(0deg); }
116
+    40% { transform: translate(-49%, -50%) rotate(1deg); }
117
+    50% { transform: translate(-50%, -50%) rotate(-1deg); }
118
+    60% { transform: translate(-52%, -50%) rotate(0deg); }
119
+    70% { transform: translate(-48%, -50%) rotate(-1deg); }
120
+    80% { transform: translate(-51%, -50%) rotate(1deg); }
121
+    90% { transform: translate(-49%, -50%) rotate(0deg); }
122
+}
123
+
124
+/* Loading state */
125
+.loading {
126
+    position: absolute;
127
+    top: 50%;
128
+    left: 50%;
129
+    transform: translate(-50%, -50%);
130
+    font-size: 24px;
131
+    color: var(--color-primary);
132
+    text-shadow: 0 0 20px var(--color-primary);
133
+    animation: pulse 1s ease-in-out infinite;
134
+}
135
+
136
+@keyframes pulse {
137
+    0%, 100% { opacity: 1; }
138
+    50% { opacity: 0.5; }
8139
 }
9140
 
10
-    h1 {
11
-      margin-top: 0;
12
-      padding-top: 1rem
141
+/* Accessibility */
142
+.visually-hidden {
143
+    position: absolute;
144
+    width: 1px;
145
+    height: 1px;
146
+    padding: 0;
147
+    margin: -1px;
148
+    overflow: hidden;
149
+    clip: rect(0, 0, 0, 0);
150
+    white-space: nowrap;
151
+    border: 0;
13152
 }
14153
 
15
-    canvas {
16
-      border: 1px solid #555;
17
-      background: #111;
18
-      cursor: crosshair
154
+/* Focus styles */
155
+:focus {
156
+    outline: 2px solid var(--color-primary);
157
+    outline-offset: 2px;
19158
 }
20159
 
21
-    .toolbar {
22
-      margin-top: .8rem;
23
-      display: flex;
24
-      justify-content: center;
25
-      align-items: center;
26
-      gap: 1rem;
160
+/* Selection */
161
+::selection {
162
+    background-color: var(--color-primary);
163
+    color: var(--color-background);
27164
 }
28165
 
29
-    button,
30
-    input[type=range] {
31
-      margin: 0 .5rem
166
+/* Scrollbar (if needed) */
167
+::-webkit-scrollbar {
168
+    width: 8px;
169
+    height: 8px;
32170
 }
33171
 
34
-    /* Color wheel styles */
35
-    .color-picker-wrapper {
36
-      position: relative;
37
-      display: inline-block;
172
+::-webkit-scrollbar-track {
173
+    background: var(--color-background);
38174
 }
39175
 
40
-    #colorWheel {
41
-      width: 150px;
42
-      height: 150px;
43
-      border-radius: 50%;
44
-      cursor: crosshair;
45
-      border: 2px solid #444;
176
+::-webkit-scrollbar-thumb {
177
+    background: var(--color-primary);
178
+    border-radius: 4px;
46179
 }
47180
 
48
-    .color-preview {
49
-      width: 30px;
50
-      height: 30px;
51
-      border-radius: 50%;
52
-      border: 2px solid #666;
53
-      display: inline-block;
54
-      vertical-align: middle;
55
-      margin-left: 10px;
181
+::-webkit-scrollbar-thumb:hover {
182
+    background: var(--color-secondary);
56183
 }
index.htmlmodified
@@ -2,15 +2,202 @@
22
 <html lang="en">
33
 <head>
44
   <meta charset="utf-8">
5
-  <title>ASCII Sketch</title>
6
-  <link rel="stylesheet" href="css/styles.css">
5
+  <title>ASCII Sketch</title>
6
+  <style>
7
+    body {
8
+      font-family: sans-serif;
9
+      text-align: center;
10
+      margin: 0;
11
+      padding: 1rem;
12
+      background: #202124;
13
+      color: #eee;
14
+      overflow-x: hidden;
15
+    }
16
+
17
+    h1 {
18
+      margin-top: 0;
19
+      padding-top: 1rem
20
+    }
21
+
22
+    canvas {
23
+      border: 1px solid #555;
24
+      background: #111;
25
+      cursor: crosshair;
26
+    }
27
+    
28
+    #board {
29
+      position: relative;
30
+    }
31
+    
32
+    canvas.erasing {
33
+      cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="8" fill="none" stroke="white" stroke-width="2"/><line x1="6" y1="10" x2="14" y2="10" stroke="white" stroke-width="2"/></svg>') 10 10, auto;
34
+    }
35
+
36
+    .toolbar {
37
+      margin-top: .8rem;
38
+      display: flex;
39
+      justify-content: center;
40
+      align-items: center;
41
+      gap: 1rem;
42
+      flex-wrap: wrap;
43
+    }
44
+
45
+    button {
46
+      padding: 0.5rem 1rem;
47
+      background: #333;
48
+      color: #eee;
49
+      border: 1px solid #555;
50
+      cursor: pointer;
51
+      transition: background 0.2s;
52
+    }
53
+
54
+    button:hover {
55
+      background: #444;
56
+    }
57
+
58
+    button.active {
59
+      background: #0a84ff;
60
+      border-color: #0a84ff;
61
+    }
62
+
63
+    input[type=range] {
64
+      margin: 0 .5rem
65
+    }
66
+
67
+    /* Color wheel styles */
68
+    .color-picker-wrapper {
69
+      position: relative;
70
+      display: inline-block;
71
+    }
72
+
73
+    #colorWheel {
74
+      width: 150px;
75
+      height: 150px;
76
+      border-radius: 50%;
77
+      cursor: crosshair;
78
+      border: 2px solid #444;
79
+    }
80
+
81
+    .color-preview {
82
+      width: 30px;
83
+      height: 30px;
84
+      border-radius: 50%;
85
+      border: 2px solid #666;
86
+      display: inline-block;
87
+      vertical-align: middle;
88
+      margin-left: 10px;
89
+    }
90
+
91
+    .tool-group {
92
+      display: flex;
93
+      gap: 0.5rem;
94
+      align-items: center;
95
+    }
96
+
97
+    /* Effects Panel */
98
+    .effects-panel {
99
+      margin-top: 1rem;
100
+      background: #2a2a2a;
101
+      padding: 1rem;
102
+      border-radius: 8px;
103
+      border: 1px solid #444;
104
+      max-width: 900px;
105
+      margin-left: auto;
106
+      margin-right: auto;
107
+    }
108
+
109
+    .effects-grid {
110
+      display: grid;
111
+      grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
112
+      gap: 0.8rem;
113
+      margin-top: 0.5rem;
114
+    }
115
+
116
+    .effect-category {
117
+      margin-bottom: 1rem;
118
+    }
119
+
120
+    .effect-category h3 {
121
+      margin: 0 0 0.5rem 0;
122
+      color: #888;
123
+      font-size: 0.9rem;
124
+      text-transform: uppercase;
125
+      letter-spacing: 1px;
126
+    }
127
+
128
+    .effect-toggle {
129
+      display: flex;
130
+      align-items: center;
131
+      gap: 0.5rem;
132
+      background: #1a1a1a;
133
+      padding: 0.5rem;
134
+      border-radius: 4px;
135
+      border: 1px solid #333;
136
+      transition: all 0.2s;
137
+    }
138
+
139
+    .effect-toggle:hover {
140
+      background: #222;
141
+      border-color: #555;
142
+    }
143
+
144
+    .effect-toggle.active {
145
+      background: #0a84ff22;
146
+      border-color: #0a84ff;
147
+    }
148
+
149
+    .effect-toggle input[type="checkbox"] {
150
+      width: 16px;
151
+      height: 16px;
152
+      cursor: pointer;
153
+    }
154
+
155
+    .effect-toggle label {
156
+      cursor: pointer;
157
+      user-select: none;
158
+      font-size: 0.9rem;
159
+      flex: 1;
160
+    }
161
+
162
+    /* Canvas container for overlay effects */
163
+    .canvas-container {
164
+      position: relative;
165
+      display: inline-block;
166
+    }
167
+
168
+    #effectsCanvas {
169
+      position: absolute;
170
+      top: 0;
171
+      left: 0;
172
+      pointer-events: none;
173
+      background: none;
174
+    }
175
+
176
+    #effectsCanvas {
177
+      position: absolute;
178
+      top: 0;
179
+      left: 0;
180
+      pointer-events: none;
181
+      z-index: 2;
182
+    }
183
+  </style>
7184
 </head>
8185
 <body>
9
-  <h1>🖋️ sketch 🖋️</h1>
10
-  <p>Draw by dragging — every stroke prints a random ASCII char.</p>
186
+  <h1>🖋️ sketch 🖋️</h1>
187
+  <p>Draw by dragging — every stroke prints a random ASCII char.</p>
188
+  
189
+  <div class="canvas-container">
11190
     <canvas id="board" width="800" height="600"></canvas>
191
+    <canvas id="effectsCanvas" width="800" height="600"></canvas>
192
+  </div>
193
+  
12194
   <div class="toolbar">
13
-    <button id="clear">Clear</button>
195
+    <div class="tool-group">
196
+      <button id="drawTool" class="active">✏️ Draw</button>
197
+      <button id="eraseTool">🧹 Erase</button>
198
+    </div>
199
+    <button id="clear">Clear All</button>
200
+    <button id="download">💾 Save Image</button>
14201
     <label>Size
15202
       <input type="range" id="size" min="12" max="64" value="24">
16203
     </label>
@@ -19,6 +206,181 @@
19206
       <div class="color-preview" id="colorPreview"></div>
20207
     </div>
21208
   </div>
209
+
210
+  <div class="effects-panel">
211
+    <h2>Visual Effects</h2>
212
+    
213
+    <div class="effect-category">
214
+      <h3>Basic Effects</h3>
215
+      <div class="effects-grid">
216
+        <div class="effect-toggle">
217
+          <input type="checkbox" id="glowEffect">
218
+          <label for="glowEffect">✨ Glow</label>
219
+        </div>
220
+        <div class="effect-toggle">
221
+          <input type="checkbox" id="embossEffect">
222
+          <label for="embossEffect">🗿 Emboss</label>
223
+        </div>
224
+        <div class="effect-toggle">
225
+          <input type="checkbox" id="outlineEffect">
226
+          <label for="outlineEffect">⭕ Outline</label>
227
+        </div>
228
+        <div class="effect-toggle">
229
+          <input type="checkbox" id="shadowEffect">
230
+          <label for="shadowEffect">🌑 Shadow</label>
231
+        </div>
232
+      </div>
233
+    </div>
234
+
235
+    <div class="effect-category">
236
+      <h3>Animation Effects</h3>
237
+      <div class="effects-grid">
238
+        <div class="effect-toggle">
239
+          <input type="checkbox" id="pulseEffect">
240
+          <label for="pulseEffect">💗 Pulse</label>
241
+        </div>
242
+        <div class="effect-toggle">
243
+          <input type="checkbox" id="fadeInEffect">
244
+          <label for="fadeInEffect">👻 Fade In</label>
245
+        </div>
246
+        <div class="effect-toggle">
247
+          <input type="checkbox" id="typewriterEffect">
248
+          <label for="typewriterEffect">⌨️ Typewriter</label>
249
+        </div>
250
+        <div class="effect-toggle">
251
+          <input type="checkbox" id="jitterEffect">
252
+          <label for="jitterEffect">📳 Jitter</label>
253
+        </div>
254
+      </div>
255
+    </div>
256
+
257
+    <div class="effect-category">
258
+      <h3>Style Effects</h3>
259
+      <div class="effects-grid">
260
+        <div class="effect-toggle">
261
+          <input type="checkbox" id="rainbowEffect">
262
+          <label for="rainbowEffect">🌈 Rainbow</label>
263
+        </div>
264
+        <div class="effect-toggle">
265
+          <input type="checkbox" id="gradientEffect">
266
+          <label for="gradientEffect">🎨 Gradient</label>
267
+        </div>
268
+        <div class="effect-toggle">
269
+          <input type="checkbox" id="doubleVisionEffect">
270
+          <label for="doubleVisionEffect">👀 Double Vision</label>
271
+        </div>
272
+        <div class="effect-toggle">
273
+          <input type="checkbox" id="mirrorEffect">
274
+          <label for="mirrorEffect">🪞 Mirror</label>
275
+        </div>
276
+      </div>
277
+    </div>
278
+
279
+    <div class="effect-category">
280
+      <h3>Particle Effects</h3>
281
+      <div class="effects-grid">
282
+        <div class="effect-toggle">
283
+          <input type="checkbox" id="sparklesEffect">
284
+          <label for="sparklesEffect">✨ Sparkles</label>
285
+        </div>
286
+        <div class="effect-toggle">
287
+          <input type="checkbox" id="dripEffect">
288
+          <label for="dripEffect">💧 Drip</label>
289
+        </div>
290
+        <div class="effect-toggle">
291
+          <input type="checkbox" id="explodeEffect">
292
+          <label for="explodeEffect">💥 Explode</label>
293
+        </div>
294
+        <div class="effect-toggle">
295
+          <input type="checkbox" id="smokeEffect">
296
+          <label for="smokeEffect">💨 Smoke Trail</label>
297
+        </div>
298
+      </div>
299
+    </div>
300
+
301
+    <div class="effect-category">
302
+      <h3>Transform Effects</h3>
303
+      <div class="effects-grid">
304
+        <div class="effect-toggle">
305
+          <input type="checkbox" id="rotationEffect">
306
+          <label for="rotationEffect">🔄 Rotation</label>
307
+        </div>
308
+        <div class="effect-toggle">
309
+          <input type="checkbox" id="waveEffect">
310
+          <label for="waveEffect">🌊 Wave</label>
311
+        </div>
312
+        <div class="effect-toggle">
313
+          <input type="checkbox" id="scatterEffect">
314
+          <label for="scatterEffect">🎲 Scatter</label>
315
+        </div>
316
+        <div class="effect-toggle">
317
+          <input type="checkbox" id="sizeVarianceEffect">
318
+          <label for="sizeVarianceEffect">📏 Size Variance</label>
319
+        </div>
320
+      </div>
321
+    </div>
322
+
323
+    <div class="effect-category">
324
+      <h3>Special Effects</h3>
325
+      <div class="effects-grid">
326
+        <div class="effect-toggle">
327
+          <input type="checkbox" id="matrixEffect">
328
+          <label for="matrixEffect">💊 Matrix Rain</label>
329
+        </div>
330
+        <div class="effect-toggle">
331
+          <input type="checkbox" id="glitchEffect">
332
+          <label for="glitchEffect">📺 Glitch</label>
333
+        </div>
334
+        <div class="effect-toggle">
335
+          <input type="checkbox" id="neonFlickerEffect">
336
+          <label for="neonFlickerEffect">💡 Neon Flicker</label>
337
+        </div>
338
+        <div class="effect-toggle">
339
+          <input type="checkbox" id="echoEffect">
340
+          <label for="echoEffect">🔊 Echo</label>
341
+        </div>
342
+      </div>
343
+    </div>
344
+
345
+    <div class="effect-category">
346
+      <h3>Motion Effects</h3>
347
+      <div class="effects-grid">
348
+        <div class="effect-toggle">
349
+          <input type="checkbox" id="spiralEffect">
350
+          <label for="spiralEffect">🌪️ Spiral</label>
351
+        </div>
352
+        <div class="effect-toggle">
353
+          <input type="checkbox" id="orbitEffect">
354
+          <label for="orbitEffect">💫 Orbit</label>
355
+        </div>
356
+        <div class="effect-toggle">
357
+          <input type="checkbox" id="magneticEffect">
358
+          <label for="magneticEffect">🎯 Magnetic</label>
359
+        </div>
360
+        <div class="effect-toggle">
361
+          <input type="checkbox" id="vortexEffect">
362
+          <label for="vortexEffect">🌀 Vortex</label>
363
+        </div>
364
+        <div class="effect-toggle">
365
+          <input type="checkbox" id="snowEffect">
366
+          <label for="snowEffect">❄️ Snow</label>
367
+        </div>
368
+        <div class="effect-toggle">
369
+          <input type="checkbox" id="fireworksEffect">
370
+          <label for="fireworksEffect">🎆 Fireworks</label>
371
+        </div>
372
+        <div class="effect-toggle">
373
+          <input type="checkbox" id="lightningEffect">
374
+          <label for="lightningEffect">⚡ Lightning</label>
375
+        </div>
376
+        <div class="effect-toggle">
377
+          <input type="checkbox" id="pixelDissolveEffect">
378
+          <label for="pixelDissolveEffect">👾 Pixel Dissolve</label>
379
+        </div>
380
+      </div>
381
+    </div>
382
+  </div>
383
+
22384
   <script src="js/app.js"></script>
23385
 </body>
24386
 </html>
js/app.jsmodified
@@ -1,12 +1,77 @@
11
 // Main canvas setup
22
 const canvas = document.getElementById('board')
33
 const ctx = canvas.getContext('2d')
4
+const effectsCanvas = document.getElementById('effectsCanvas')
5
+const effectsCtx = effectsCanvas.getContext('2d')
6
+
47
 const chars = [
58
   '@', '#', '$', '%', '&', '*', '+', '=',
69
   '~', '?', 'Ω', '∞', '¶', '§', '★', '☆'
710
 ]
11
+
812
 let drawing = false
913
 let currentColor = '#0f0'
14
+let currentTool = 'draw'
15
+
16
+// Store active characters for animations
17
+const activeChars = []
18
+const particles = []
19
+const drips = []
20
+const smoke = []
21
+
22
+// Tool buttons
23
+const drawBtn = document.getElementById('drawTool')
24
+const eraseBtn = document.getElementById('eraseTool')
25
+
26
+// Effect toggles - organized by category
27
+const effects = {
28
+  // Basic
29
+  glow: document.getElementById('glowEffect'),
30
+  emboss: document.getElementById('embossEffect'),
31
+  outline: document.getElementById('outlineEffect'),
32
+  shadow: document.getElementById('shadowEffect'),
33
+  // Animation
34
+  pulse: document.getElementById('pulseEffect'),
35
+  fadeIn: document.getElementById('fadeInEffect'),
36
+  typewriter: document.getElementById('typewriterEffect'),
37
+  jitter: document.getElementById('jitterEffect'),
38
+  // Style
39
+  rainbow: document.getElementById('rainbowEffect'),
40
+  gradient: document.getElementById('gradientEffect'),
41
+  doubleVision: document.getElementById('doubleVisionEffect'),
42
+  mirror: document.getElementById('mirrorEffect'),
43
+  // Particle
44
+  sparkles: document.getElementById('sparklesEffect'),
45
+  drip: document.getElementById('dripEffect'),
46
+  explode: document.getElementById('explodeEffect'),
47
+  smoke: document.getElementById('smokeEffect'),
48
+  // Transform
49
+  rotation: document.getElementById('rotationEffect'),
50
+  wave: document.getElementById('waveEffect'),
51
+  scatter: document.getElementById('scatterEffect'),
52
+  sizeVariance: document.getElementById('sizeVarianceEffect'),
53
+  // Special
54
+  matrix: document.getElementById('matrixEffect'),
55
+  glitch: document.getElementById('glitchEffect'),
56
+  neonFlicker: document.getElementById('neonFlickerEffect'),
57
+  echo: document.getElementById('echoEffect'),
58
+  // Motion
59
+  spiral: document.getElementById('spiralEffect'),
60
+  orbit: document.getElementById('orbitEffect'),
61
+  magnetic: document.getElementById('magneticEffect'),
62
+  vortex: document.getElementById('vortexEffect'),
63
+  snow: document.getElementById('snowEffect'),
64
+  fireworks: document.getElementById('fireworksEffect'),
65
+  lightning: document.getElementById('lightningEffect'),
66
+  pixelDissolve: document.getElementById('pixelDissolveEffect')
67
+}
68
+
69
+// Add active class toggle for all effects
70
+Object.values(effects).forEach(checkbox => {
71
+  checkbox.addEventListener('change', (e) => {
72
+    e.target.closest('.effect-toggle').classList.toggle('active', e.target.checked)
73
+  })
74
+})
1075
 
1176
 // Color wheel setup
1277
 const colorWheel = document.getElementById('colorWheel')
@@ -15,7 +80,7 @@
1580
 colorWheel.width = 150
1681
 colorWheel.height = 150
1782
 
18
-    // Draw color wheel
83
+// Draw color wheel with white center and black ring
1984
 function drawColorWheel() {
2085
   const centerX = colorWheel.width / 2
2186
   const centerY = colorWheel.height / 2
@@ -33,11 +98,20 @@
3398
     const gradient = colorCtx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius)
3499
     gradient.addColorStop(0, 'white')
35100
     gradient.addColorStop(0.7, `hsl(${angle}, 100%, 50%)`)
36
-        gradient.addColorStop(1,   `hsl(${angle}, 100%, 25%)`)
101
+    gradient.addColorStop(0.95, `hsl(${angle}, 100%, 30%)`)
102
+    gradient.addColorStop(1, 'black')
37103
     
38104
     colorCtx.fillStyle = gradient
39105
     colorCtx.fill()
40106
   }
107
+
108
+  colorCtx.beginPath()
109
+  colorCtx.arc(centerX, centerY, 15, 0, Math.PI * 2)
110
+  colorCtx.fillStyle = 'white'
111
+  colorCtx.fill()
112
+  colorCtx.strokeStyle = '#666'
113
+  colorCtx.lineWidth = 1
114
+  colorCtx.stroke()
41115
 }
42116
 
43117
 // Get color from wheel position
@@ -50,12 +124,15 @@
50124
   const maxRadius = Math.min(centerX, centerY) - 2
51125
   
52126
   if (distance > maxRadius) return null
127
+  if (distance < 15) return 'white'
128
+  if (distance > maxRadius * 0.95) return 'black'
53129
   
54130
   let angle = Math.atan2(dy, dx) * 180 / Math.PI
55131
   angle = (angle + 360) % 360
56132
   
57
-      const saturation = Math.min(100, (distance / maxRadius) * 100)
58
-      const lightness = 50 - (distance / maxRadius) * 25
133
+  const normalizedDistance = (distance - 15) / (maxRadius * 0.95 - 15)
134
+  const saturation = Math.min(100, normalizedDistance * 100)
135
+  const lightness = 50 - (normalizedDistance * 20)
59136
   
60137
   return `hsl(${angle}, ${saturation}%, ${lightness}%)`
61138
 }
@@ -73,29 +150,627 @@
73150
   }
74151
 })
75152
 
153
+// Tool switching
154
+function setTool(tool) {
155
+  currentTool = tool
156
+  if (tool === 'draw') {
157
+    drawBtn.classList.add('active')
158
+    eraseBtn.classList.remove('active')
159
+    canvas.classList.remove('erasing')
160
+  } else {
161
+    eraseBtn.classList.add('active')
162
+    drawBtn.classList.remove('active')
163
+    canvas.classList.add('erasing')
164
+  }
165
+}
166
+
167
+drawBtn.addEventListener('click', () => setTool('draw'))
168
+eraseBtn.addEventListener('click', () => setTool('erase'))
169
+
76170
 // Drawing functions
77171
 function randomChar() {
78172
   return chars[Math.floor(Math.random() * chars.length)]
79173
 }
80174
 
81
-    function getFont() {
82
-      return `${sizeInput.value}px monospace`
175
+function getFont(size = null) {
176
+  const baseSize = size || sizeInput.value
177
+  return `${baseSize}px monospace`
178
+}
179
+
180
+// Character class for animations
181
+class Character {
182
+  constructor(x, y, char, color, size) {
183
+    this.x = x
184
+    this.y = y
185
+    this.char = char
186
+    this.color = color
187
+    this.size = size
188
+    this.originalSize = size
189
+    this.opacity = effects.fadeIn.checked ? 0 : 1
190
+    this.rotation = 0
191
+    this.offset = { x: 0, y: 0 }
192
+    this.time = Date.now()
193
+    this.delay = effects.typewriter.checked ? activeChars.length * 50 : 0
194
+    this.hue = 0
195
+    this.vy = 0  // Add this for matrix rain
196
+  }
197
+
198
+  update() {
199
+    const elapsed = Date.now() - this.time
200
+    
201
+    // Fade in
202
+    if (effects.fadeIn.checked && this.opacity < 1) {
203
+      this.opacity = Math.min(1, (elapsed - this.delay) / 500)
204
+    }
205
+    
206
+    // Pulse
207
+    if (effects.pulse.checked) {
208
+      this.size = this.originalSize + Math.sin(elapsed / 300) * 3
209
+    }
210
+    
211
+    // Jitter
212
+    if (effects.jitter.checked) {
213
+      this.offset.x = (Math.random() - 0.5) * 2
214
+      this.offset.y = (Math.random() - 0.5) * 2
215
+    } else {
216
+      this.offset.x = 0
217
+      this.offset.y = 0
218
+    }
219
+    
220
+    // Rainbow
221
+    if (effects.rainbow.checked) {
222
+      this.hue = (this.hue + 2) % 360
223
+      this.color = `hsl(${this.hue}, 100%, 50%)`
224
+    }
225
+    
226
+    // Rotation
227
+    if (effects.rotation.checked) {
228
+      this.rotation += 0.05
229
+    }
230
+    
231
+    // Wave
232
+    if (effects.wave.checked) {
233
+      this.offset.y = Math.sin((elapsed + this.x * 10) / 200) * 5
234
+    }
235
+    
236
+    // Neon flicker
237
+    if (effects.neonFlicker.checked) {
238
+      this.opacity = Math.random() > 0.1 ? 1 : 0.3
239
+    }
240
+    
241
+    // Spiral
242
+    if (effects.spiral.checked) {
243
+      const angle = elapsed / 500
244
+      const radius = elapsed / 50
245
+      this.offset.x = Math.cos(angle) * radius
246
+      this.offset.y = Math.sin(angle) * radius
247
+    }
248
+    
249
+    // Orbit
250
+    if (effects.orbit.checked) {
251
+      const angle = elapsed / 300
252
+      const radius = 30
253
+      this.offset.x = Math.cos(angle) * radius
254
+      this.offset.y = Math.sin(angle) * radius
255
+    }
256
+    
257
+    // Magnetic (attract to mouse)
258
+    if (effects.magnetic.checked && window.mouseX && window.mouseY) {
259
+      const dx = window.mouseX - this.x
260
+      const dy = window.mouseY - this.y
261
+      const distance = Math.sqrt(dx * dx + dy * dy)
262
+      if (distance < 200) {
263
+        const force = (200 - distance) / 200
264
+        this.offset.x += (dx / distance) * force * 2
265
+        this.offset.y += (dy / distance) * force * 2
266
+      }
267
+    }
268
+    
269
+    // Vortex
270
+    if (effects.vortex.checked) {
271
+      const centerX = canvas.width / 2
272
+      const centerY = canvas.height / 2
273
+      const dx = this.x - centerX
274
+      const dy = this.y - centerY
275
+      const angle = Math.atan2(dy, dx) + elapsed / 1000
276
+      const distance = Math.sqrt(dx * dx + dy * dy)
277
+      this.x = centerX + Math.cos(angle) * distance
278
+      this.y = centerY + Math.sin(angle) * distance
279
+    }
280
+    
281
+    // Snow fall
282
+    if (effects.snow.checked) {
283
+      this.y += 1
284
+      this.x += Math.sin(elapsed / 300) * 0.5
285
+      if (this.y > canvas.height) {
286
+        this.y = 0
287
+      }
288
+    }
289
+    
290
+    // Fireworks
291
+    if (effects.fireworks.checked) {
292
+      if (!this.fireworkPhase) {
293
+        this.fireworkPhase = 'rise'
294
+        this.vy = -10
295
+      }
296
+      if (this.fireworkPhase === 'rise') {
297
+        this.y += this.vy
298
+        this.vy += 0.3
299
+        if (this.vy > 0) {
300
+          this.fireworkPhase = 'explode'
301
+          // Create explosion particles
302
+          for (let i = 0; i < 10; i++) {
303
+            particles.push(new Particle(this.x, this.y, this.color, 'explode'))
304
+          }
305
+          this.opacity = 0
306
+        }
307
+      }
308
+    }
309
+    
310
+    // Pixel dissolve
311
+    if (effects.pixelDissolve.checked) {
312
+      const dissolveTime = elapsed - 1000
313
+      if (dissolveTime > 0) {
314
+        this.opacity = Math.max(0, 1 - dissolveTime / 1000)
315
+        this.offset.x = (Math.random() - 0.5) * dissolveTime / 50
316
+        this.offset.y = (Math.random() - 0.5) * dissolveTime / 50
317
+      }
318
+    }
319
+  }
320
+
321
+  draw(context) {
322
+    if (effects.typewriter.checked && Date.now() - this.time < this.delay) return
323
+    
324
+    context.save()
325
+    context.globalAlpha = this.opacity
326
+    
327
+    // Apply basic effects for animated characters
328
+    if (effects.glow.checked) {
329
+      context.shadowColor = this.color
330
+      context.shadowBlur = 10
331
+      context.shadowOffsetX = 0
332
+      context.shadowOffsetY = 0
333
+    }
334
+    
335
+    if (effects.emboss.checked) {
336
+      context.shadowColor = 'rgba(0, 0, 0, 0.8)'
337
+      context.shadowBlur = 2
338
+      context.shadowOffsetX = 2
339
+      context.shadowOffsetY = 2
340
+    }
341
+    
342
+    if (effects.shadow.checked) {
343
+      context.shadowColor = 'rgba(0, 0, 0, 0.5)'
344
+      context.shadowBlur = 5
345
+      context.shadowOffsetX = 3
346
+      context.shadowOffsetY = 3
347
+    }
348
+    
349
+    if (this.rotation !== 0) {
350
+      context.translate(this.x + this.offset.x, this.y + this.offset.y)
351
+      context.rotate(this.rotation)
352
+      context.font = getFont(this.size)
353
+      context.fillStyle = this.color
354
+      context.textAlign = 'center'
355
+      context.textBaseline = 'middle'
356
+      context.fillText(this.char, 0, 0)
357
+    } else {
358
+      // For non-rotating text, use the same positioning as main draw
359
+      context.font = getFont(this.size)
360
+      context.fillStyle = this.color
361
+      context.fillText(this.char, this.x + this.offset.x, this.y + this.offset.y)
362
+    }
363
+    
364
+    context.restore()
365
+  }
366
+}
367
+
368
+// Particle classes
369
+class Particle {
370
+  constructor(x, y, color, type = 'sparkle') {
371
+    this.x = x
372
+    this.y = y
373
+    this.vx = (Math.random() - 0.5) * 4
374
+    this.vy = (Math.random() - 0.5) * 4
375
+    this.color = color
376
+    this.life = 1
377
+    this.type = type
378
+    this.size = type === 'sparkle' ? Math.random() * 3 + 1 : Math.random() * 5 + 2
379
+  }
380
+
381
+  update() {
382
+    this.x += this.vx
383
+    this.y += this.vy
384
+    this.life -= 0.02
385
+    
386
+    if (this.type === 'explode') {
387
+      this.vy += 0.2 // gravity
388
+    }
389
+  }
390
+
391
+  draw(context) {
392
+    context.save()
393
+    context.globalAlpha = this.life
394
+    context.fillStyle = this.color
395
+    
396
+    if (this.type === 'sparkle') {
397
+      context.beginPath()
398
+      context.arc(this.x, this.y, this.size, 0, Math.PI * 2)
399
+      context.fill()
400
+    } else {
401
+      context.font = '12px monospace'
402
+      context.fillText(['*', '+', '•'][Math.floor(Math.random() * 3)], this.x, this.y)
403
+    }
404
+    
405
+    context.restore()
406
+  }
407
+}
408
+
409
+class Drip {
410
+  constructor(x, y, char, color, size) {
411
+    this.x = x
412
+    this.y = y
413
+    this.char = char
414
+    this.color = color
415
+    this.size = size
416
+    this.vy = 0
417
+    this.life = 1
418
+  }
419
+
420
+  update() {
421
+    this.vy += 0.3
422
+    this.y += this.vy
423
+    this.life -= 0.005
424
+  }
425
+
426
+  draw(context) {
427
+    context.save()
428
+    context.globalAlpha = this.life
429
+    context.font = getFont(this.size)
430
+    context.fillStyle = this.color
431
+    context.fillText(this.char, this.x, this.y)
432
+    context.restore()
433
+  }
434
+}
435
+
436
+class SmokeParticle {
437
+  constructor(x, y) {
438
+    this.x = x
439
+    this.y = y
440
+    this.vx = (Math.random() - 0.5) * 2
441
+    this.vy = -Math.random() * 2 - 1
442
+    this.size = Math.random() * 20 + 10
443
+    this.life = 1
444
+  }
445
+
446
+  update() {
447
+    this.x += this.vx
448
+    this.y += this.vy
449
+    this.size += 0.5
450
+    this.life -= 0.02
451
+  }
452
+
453
+  draw(context) {
454
+    context.save()
455
+    context.globalAlpha = this.life * 0.2
456
+    context.fillStyle = 'white'
457
+    context.beginPath()
458
+    context.arc(this.x, this.y, this.size, 0, Math.PI * 2)
459
+    context.fill()
460
+    context.restore()
461
+  }
462
+}
463
+
464
+// Apply basic effects
465
+function applyBasicEffects() {
466
+  if (effects.glow.checked) {
467
+    ctx.shadowColor = currentColor
468
+    ctx.shadowBlur = 10
469
+    ctx.shadowOffsetX = 0
470
+    ctx.shadowOffsetY = 0
83471
   }
84472
   
473
+  if (effects.emboss.checked) {
474
+    ctx.shadowColor = 'rgba(0, 0, 0, 0.8)'
475
+    ctx.shadowBlur = 2
476
+    ctx.shadowOffsetX = 2
477
+    ctx.shadowOffsetY = 2
478
+  }
479
+  
480
+  if (effects.shadow.checked) {
481
+    ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
482
+    ctx.shadowBlur = 5
483
+    ctx.shadowOffsetX = 3
484
+    ctx.shadowOffsetY = 3
485
+  }
486
+  
487
+  if (effects.glow.checked && effects.emboss.checked) {
488
+    ctx.shadowColor = currentColor
489
+    ctx.shadowBlur = 8
490
+    ctx.shadowOffsetX = 1
491
+    ctx.shadowOffsetY = 1
492
+  }
493
+}
494
+
495
+// Main draw function
85496
 function draw(e) {
86497
   if (!drawing) return
87498
   const rect = canvas.getBoundingClientRect()
88499
   const x = e.clientX - rect.left
89500
   const y = e.clientY - rect.top
90
-      ctx.font = getFont()
91
-      ctx.fillStyle = currentColor
92
-      ctx.fillText(randomChar(), x, y)
501
+  
502
+  if (currentTool === 'erase') {
503
+    const eraseRadius = parseInt(sizeInput.value) / 2
504
+    ctx.save()
505
+    ctx.globalCompositeOperation = 'destination-out'
506
+    ctx.beginPath()
507
+    ctx.arc(x, y, eraseRadius, 0, Math.PI * 2)
508
+    ctx.fill()
509
+    ctx.restore()
510
+    
511
+    // Also remove any animated characters in the erase area
512
+    for (let i = activeChars.length - 1; i >= 0; i--) {
513
+      const char = activeChars[i]
514
+      const dx = char.x - x
515
+      const dy = char.y - y
516
+      const distance = Math.sqrt(dx * dx + dy * dy)
517
+      if (distance <= eraseRadius) {
518
+        activeChars.splice(i, 1)
519
+      }
520
+    }
521
+  } else {
522
+    // Get character and apply effects
523
+    const char = randomChar()
524
+    let size = parseInt(sizeInput.value)
525
+    let drawX = x
526
+    let drawY = y
527
+    let color = currentColor
528
+    
529
+    // Size variance
530
+    if (effects.sizeVariance.checked) {
531
+      size = size * (0.5 + Math.random())
532
+    }
533
+    
534
+    // Scatter
535
+    if (effects.scatter.checked) {
536
+      drawX += (Math.random() - 0.5) * 20
537
+      drawY += (Math.random() - 0.5) * 20
538
+    }
539
+    
540
+    // Gradient effect
541
+    if (effects.gradient.checked) {
542
+      const gradient = ctx.createLinearGradient(drawX - size/2, drawY - size/2, drawX + size/2, drawY + size/2)
543
+      gradient.addColorStop(0, color)
544
+      gradient.addColorStop(1, adjustBrightness(color, -30))
545
+      color = gradient
546
+    }
547
+    
548
+    // Apply basic effects
549
+    ctx.save()
550
+    applyBasicEffects()
551
+    
552
+    // Draw outline if enabled
553
+    if (effects.outline.checked) {
554
+      ctx.font = getFont(size)
555
+      ctx.strokeStyle = adjustBrightness(currentColor, -50)
556
+      ctx.lineWidth = 2
557
+      ctx.strokeText(char, drawX, drawY)
558
+    }
559
+    
560
+    // Draw emboss highlight
561
+    if (effects.emboss.checked && !effects.glow.checked) {
562
+      ctx.font = getFont(size)
563
+      ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'
564
+      ctx.fillText(char, drawX - 1, drawY - 1)
565
+    }
566
+    
567
+    // Draw main character (only if not using animation effects)
568
+    const hasAnimationEffects = effects.pulse.checked || effects.fadeIn.checked || 
569
+        effects.typewriter.checked || effects.jitter.checked || effects.rainbow.checked || 
570
+        effects.rotation.checked || effects.wave.checked || effects.neonFlicker.checked ||
571
+        effects.spiral.checked || effects.orbit.checked || effects.magnetic.checked ||
572
+        effects.vortex.checked || effects.snow.checked || effects.fireworks.checked ||
573
+        effects.pixelDissolve.checked
574
+    
575
+    if (!hasAnimationEffects) {
576
+      ctx.font = getFont(size)
577
+      ctx.fillStyle = color
578
+      ctx.fillText(char, drawX, drawY)
579
+      
580
+      // Double vision (only if not animated)
581
+      if (effects.doubleVision.checked) {
582
+        ctx.globalAlpha = 0.5
583
+        ctx.fillStyle = adjustBrightness(color, 20)
584
+        ctx.fillText(char, drawX + 5, drawY + 5)
585
+      }
586
+      
587
+      // Mirror (only if not animated)
588
+      if (effects.mirror.checked) {
589
+        const mirrorX = canvas.width - drawX
590
+        ctx.fillStyle = color
591
+        ctx.fillText(char, mirrorX, drawY)
592
+      }
593
+      
594
+      // Echo effect (only if not animated)
595
+      if (effects.echo.checked) {
596
+        for (let i = 1; i <= 3; i++) {
597
+          ctx.globalAlpha = 0.3 / i
598
+          ctx.fillText(char, drawX - i * 5, drawY - i * 5)
599
+        }
600
+      }
601
+    }
602
+    
603
+    ctx.restore()
604
+    
605
+    // Add to active chars for animation
606
+    if (hasAnimationEffects) {
607
+      activeChars.push(new Character(drawX, drawY, char, color instanceof CanvasGradient ? currentColor : color, size))
608
+    }
609
+    
610
+    // Particle effects
611
+    if (effects.sparkles.checked) {
612
+      for (let i = 0; i < 5; i++) {
613
+        particles.push(new Particle(drawX + Math.random() * 20 - 10, drawY + Math.random() * 20 - 10, color instanceof CanvasGradient ? currentColor : color, 'sparkle'))
614
+      }
615
+    }
616
+    
617
+    if (effects.explode.checked) {
618
+      for (let i = 0; i < 8; i++) {
619
+        particles.push(new Particle(drawX, drawY, color instanceof CanvasGradient ? currentColor : color, 'explode'))
620
+      }
621
+    }
622
+    
623
+    if (effects.drip.checked) {
624
+      drips.push(new Drip(drawX, drawY, char, color instanceof CanvasGradient ? currentColor : color, size))
625
+    }
626
+    
627
+    if (effects.smoke.checked) {
628
+      smoke.push(new SmokeParticle(drawX, drawY))
629
+    }
630
+    
631
+    // Matrix rain
632
+    if (effects.matrix.checked) {
633
+      const matrixChar = new Character(drawX, 0, char, '#0f0', size)
634
+      matrixChar.vy = Math.random() * 3 + 2
635
+      activeChars.push(matrixChar)
636
+    }
637
+    
638
+    // Glitch effect
639
+    if (effects.glitch.checked && Math.random() < 0.1) {
640
+      ctx.save()
641
+      ctx.globalCompositeOperation = 'difference'
642
+      ctx.fillStyle = '#ff00ff'
643
+      ctx.fillRect(drawX - size/2, drawY - size/2, size, size)
644
+      ctx.restore()
645
+    }
646
+  }
647
+}
648
+
649
+// Animation loop
650
+function animate() {
651
+  effectsCtx.clearRect(0, 0, effectsCanvas.width, effectsCanvas.height)
652
+  
653
+  // Update and draw animated characters on the effects canvas
654
+  activeChars.forEach((char, index) => {
655
+    char.update()
656
+    
657
+    // Matrix rain movement
658
+    if (effects.matrix.checked && char.vy) {
659
+      char.y += char.vy
660
+      if (char.y > canvas.height) {
661
+        activeChars.splice(index, 1)
662
+        return
663
+      }
664
+    }
665
+    
666
+    // Draw on effects canvas
667
+    if (char.opacity > 0) {
668
+      char.draw(effectsCtx)
669
+    }
670
+  })
671
+  
672
+  // Update and draw particles
673
+  particles.forEach((particle, index) => {
674
+    particle.update()
675
+    particle.draw(effectsCtx)
676
+    if (particle.life <= 0) {
677
+      particles.splice(index, 1)
678
+    }
679
+  })
680
+  
681
+  // Draw lightning between nearby characters
682
+  if (effects.lightning.checked && activeChars.length > 1) {
683
+    effectsCtx.strokeStyle = 'cyan'
684
+    effectsCtx.lineWidth = 1
685
+    effectsCtx.globalAlpha = 0.5
686
+    
687
+    activeChars.forEach((char1, i) => {
688
+      activeChars.forEach((char2, j) => {
689
+        if (i < j) {
690
+          const dx = char2.x - char1.x
691
+          const dy = char2.y - char1.y
692
+          const distance = Math.sqrt(dx * dx + dy * dy)
693
+          
694
+          if (distance < 100 && Math.random() > 0.9) {
695
+            effectsCtx.beginPath()
696
+            effectsCtx.moveTo(char1.x + char1.offset.x, char1.y + char1.offset.y)
697
+            
698
+            // Create lightning path
699
+            const steps = 5
700
+            for (let k = 1; k < steps; k++) {
701
+              const t = k / steps
702
+              const x = char1.x + dx * t + (Math.random() - 0.5) * 10
703
+              const y = char1.y + dy * t + (Math.random() - 0.5) * 10
704
+              effectsCtx.lineTo(x, y)
705
+            }
706
+            
707
+            effectsCtx.lineTo(char2.x + char2.offset.x, char2.y + char2.offset.y)
708
+            effectsCtx.stroke()
709
+          }
710
+        }
711
+      })
712
+    })
713
+    effectsCtx.globalAlpha = 1
93714
   }
94715
   
716
+  // Update and draw drips
717
+  drips.forEach((drip, index) => {
718
+    drip.update()
719
+    drip.draw(effectsCtx)
720
+    if (drip.life <= 0 || drip.y > canvas.height) {
721
+      drips.splice(index, 1)
722
+    }
723
+  })
724
+  
725
+  // Update and draw smoke
726
+  smoke.forEach((s, index) => {
727
+    s.update()
728
+    s.draw(effectsCtx)
729
+    if (s.life <= 0) {
730
+      smoke.splice(index, 1)
731
+    }
732
+  })
733
+  
734
+  // Debug: show particle count
735
+  if (particles.length > 0 || activeChars.length > 0) {
736
+    effectsCtx.fillStyle = 'white'
737
+    effectsCtx.font = '12px monospace'
738
+    effectsCtx.fillText(`Particles: ${particles.length}, Chars: ${activeChars.length}`, 10, 20)
739
+  }
740
+  
741
+  requestAnimationFrame(animate)
742
+}
743
+
744
+// Helper functions
745
+function adjustBrightness(color, amount) {
746
+  // Simple brightness adjustment for hex colors
747
+  if (color.startsWith('#')) {
748
+    const num = parseInt(color.slice(1), 16)
749
+    const r = Math.max(0, Math.min(255, (num >> 16) + amount))
750
+    const g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount))
751
+    const b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount))
752
+    return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`
753
+  }
754
+  return color
755
+}
756
+
757
+// Mouse tracking for magnetic effect
758
+window.mouseX = 0
759
+window.mouseY = 0
760
+canvas.addEventListener('mousemove', (e) => {
761
+  const rect = canvas.getBoundingClientRect()
762
+  window.mouseX = e.clientX - rect.left
763
+  window.mouseY = e.clientY - rect.top
764
+})
765
+
95766
 // Mouse events
96767
 canvas.addEventListener('mousedown', () => (drawing = true))
97768
 canvas.addEventListener('mouseup', () => (drawing = false))
98
-    canvas.addEventListener('mouseleave', () => (drawing = false))
769
+canvas.addEventListener('mouseleave', () => {
770
+  drawing = false
771
+  window.mouseX = null
772
+  window.mouseY = null
773
+})
99774
 canvas.addEventListener('mousemove', draw)
100775
 
101776
 // Touch support
@@ -111,10 +786,43 @@
111786
 
112787
 // Controls
113788
 const clearBtn = document.getElementById('clear')
114
-    clearBtn.onclick = () => ctx.clearRect(0, 0, canvas.width, canvas.height)
789
+clearBtn.onclick = () => {
790
+  ctx.clearRect(0, 0, canvas.width, canvas.height)
791
+  effectsCtx.clearRect(0, 0, effectsCanvas.width, effectsCanvas.height)
792
+  activeChars.length = 0
793
+  particles.length = 0
794
+  drips.length = 0
795
+  smoke.length = 0
796
+}
797
+
798
+const downloadBtn = document.getElementById('download')
799
+downloadBtn.onclick = () => {
800
+  // Create temporary canvas to merge both layers
801
+  const tempCanvas = document.createElement('canvas')
802
+  tempCanvas.width = canvas.width
803
+  tempCanvas.height = canvas.height
804
+  const tempCtx = tempCanvas.getContext('2d')
805
+  
806
+  // Draw background
807
+  tempCtx.fillStyle = '#111'
808
+  tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height)
809
+  
810
+  // Draw main canvas
811
+  tempCtx.drawImage(canvas, 0, 0)
812
+  
813
+  // Draw effects canvas
814
+  tempCtx.drawImage(effectsCanvas, 0, 0)
815
+  
816
+  // Create download link
817
+  const link = document.createElement('a')
818
+  link.download = `ascii-art-${Date.now()}.png`
819
+  link.href = tempCanvas.toDataURL()
820
+  link.click()
821
+}
115822
 
116823
 const sizeInput = document.getElementById('size')
117824
 
118825
 // Initialize
119826
 drawColorWheel()
120827
 colorPreview.style.backgroundColor = currentColor
828
+animate()