@@ -1,120 +1,828 @@ |
| 1 | // Main canvas setup | 1 | // Main canvas setup |
| 2 | - const canvas = document.getElementById('board') | 2 | +const canvas = document.getElementById('board') |
| 3 | - const ctx = canvas.getContext('2d') | 3 | +const ctx = canvas.getContext('2d') |
| 4 | - const chars = [ | 4 | +const effectsCanvas = document.getElementById('effectsCanvas') |
| 5 | - '@', '#', '$', '%', '&', '*', '+', '=', | 5 | +const effectsCtx = effectsCanvas.getContext('2d') |
| 6 | - '~', '?', 'Ω', '∞', '¶', '§', '★', '☆' | 6 | + |
| 7 | - ] | 7 | +const chars = [ |
| 8 | - let drawing = false | 8 | + '@', '#', '$', '%', '&', '*', '+', '=', |
| 9 | - let currentColor = '#0f0' | 9 | + '~', '?', 'Ω', '∞', '¶', '§', '★', '☆' |
| 10 | - | 10 | +] |
| 11 | - // Color wheel setup | 11 | + |
| 12 | - const colorWheel = document.getElementById('colorWheel') | 12 | +let drawing = false |
| 13 | - const colorCtx = colorWheel.getContext('2d') | 13 | +let currentColor = '#0f0' |
| 14 | - const colorPreview = document.getElementById('colorPreview') | 14 | +let currentTool = 'draw' |
| 15 | - colorWheel.width = 150 | 15 | + |
| 16 | - colorWheel.height = 150 | 16 | +// Store active characters for animations |
| 17 | - | 17 | +const activeChars = [] |
| 18 | - // Draw color wheel | 18 | +const particles = [] |
| 19 | - function drawColorWheel() { | 19 | +const drips = [] |
| 20 | - const centerX = colorWheel.width / 2 | 20 | +const smoke = [] |
| 21 | - const centerY = colorWheel.height / 2 | 21 | + |
| 22 | - const radius = Math.min(centerX, centerY) - 2 | 22 | +// Tool buttons |
| 23 | - | 23 | +const drawBtn = document.getElementById('drawTool') |
| 24 | - for (let angle = 0; angle < 360; angle++) { | 24 | +const eraseBtn = document.getElementById('eraseTool') |
| 25 | - const startAngle = (angle - 1) * Math.PI / 180 | 25 | + |
| 26 | - const endAngle = angle * Math.PI / 180 | 26 | +// Effect toggles - organized by category |
| 27 | - | 27 | +const effects = { |
| 28 | - colorCtx.beginPath() | 28 | + // Basic |
| 29 | - colorCtx.moveTo(centerX, centerY) | 29 | + glow: document.getElementById('glowEffect'), |
| 30 | - colorCtx.arc(centerX, centerY, radius, startAngle, endAngle) | 30 | + emboss: document.getElementById('embossEffect'), |
| 31 | - colorCtx.closePath() | 31 | + outline: document.getElementById('outlineEffect'), |
| 32 | - | 32 | + shadow: document.getElementById('shadowEffect'), |
| 33 | - const gradient = colorCtx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius) | 33 | + // Animation |
| 34 | - gradient.addColorStop(0, 'white') | 34 | + pulse: document.getElementById('pulseEffect'), |
| 35 | - gradient.addColorStop(0.7, `hsl(${angle}, 100%, 50%)`) | 35 | + fadeIn: document.getElementById('fadeInEffect'), |
| 36 | - gradient.addColorStop(1, `hsl(${angle}, 100%, 25%)`) | 36 | + typewriter: document.getElementById('typewriterEffect'), |
| 37 | - | 37 | + jitter: document.getElementById('jitterEffect'), |
| 38 | - colorCtx.fillStyle = gradient | 38 | + // Style |
| 39 | - colorCtx.fill() | 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 | +}) |
| | 75 | + |
| | 76 | +// Color wheel setup |
| | 77 | +const colorWheel = document.getElementById('colorWheel') |
| | 78 | +const colorCtx = colorWheel.getContext('2d') |
| | 79 | +const colorPreview = document.getElementById('colorPreview') |
| | 80 | +colorWheel.width = 150 |
| | 81 | +colorWheel.height = 150 |
| | 82 | + |
| | 83 | +// Draw color wheel with white center and black ring |
| | 84 | +function drawColorWheel() { |
| | 85 | + const centerX = colorWheel.width / 2 |
| | 86 | + const centerY = colorWheel.height / 2 |
| | 87 | + const radius = Math.min(centerX, centerY) - 2 |
| | 88 | + |
| | 89 | + for (let angle = 0; angle < 360; angle++) { |
| | 90 | + const startAngle = (angle - 1) * Math.PI / 180 |
| | 91 | + const endAngle = angle * Math.PI / 180 |
| | 92 | + |
| | 93 | + colorCtx.beginPath() |
| | 94 | + colorCtx.moveTo(centerX, centerY) |
| | 95 | + colorCtx.arc(centerX, centerY, radius, startAngle, endAngle) |
| | 96 | + colorCtx.closePath() |
| | 97 | + |
| | 98 | + const gradient = colorCtx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius) |
| | 99 | + gradient.addColorStop(0, 'white') |
| | 100 | + gradient.addColorStop(0.7, `hsl(${angle}, 100%, 50%)`) |
| | 101 | + gradient.addColorStop(0.95, `hsl(${angle}, 100%, 30%)`) |
| | 102 | + gradient.addColorStop(1, 'black') |
| | 103 | + |
| | 104 | + colorCtx.fillStyle = gradient |
| | 105 | + colorCtx.fill() |
| | 106 | + } |
| | 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() |
| | 115 | +} |
| | 116 | + |
| | 117 | +// Get color from wheel position |
| | 118 | +function getColorFromWheel(x, y) { |
| | 119 | + const centerX = colorWheel.width / 2 |
| | 120 | + const centerY = colorWheel.height / 2 |
| | 121 | + const dx = x - centerX |
| | 122 | + const dy = y - centerY |
| | 123 | + const distance = Math.sqrt(dx * dx + dy * dy) |
| | 124 | + const maxRadius = Math.min(centerX, centerY) - 2 |
| | 125 | + |
| | 126 | + if (distance > maxRadius) return null |
| | 127 | + if (distance < 15) return 'white' |
| | 128 | + if (distance > maxRadius * 0.95) return 'black' |
| | 129 | + |
| | 130 | + let angle = Math.atan2(dy, dx) * 180 / Math.PI |
| | 131 | + angle = (angle + 360) % 360 |
| | 132 | + |
| | 133 | + const normalizedDistance = (distance - 15) / (maxRadius * 0.95 - 15) |
| | 134 | + const saturation = Math.min(100, normalizedDistance * 100) |
| | 135 | + const lightness = 50 - (normalizedDistance * 20) |
| | 136 | + |
| | 137 | + return `hsl(${angle}, ${saturation}%, ${lightness}%)` |
| | 138 | +} |
| | 139 | + |
| | 140 | +// Color wheel interaction |
| | 141 | +colorWheel.addEventListener('click', (e) => { |
| | 142 | + const rect = colorWheel.getBoundingClientRect() |
| | 143 | + const x = e.clientX - rect.left |
| | 144 | + const y = e.clientY - rect.top |
| | 145 | + const color = getColorFromWheel(x, y) |
| | 146 | + |
| | 147 | + if (color) { |
| | 148 | + currentColor = color |
| | 149 | + colorPreview.style.backgroundColor = color |
| | 150 | + } |
| | 151 | +}) |
| | 152 | + |
| | 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 | + |
| | 170 | +// Drawing functions |
| | 171 | +function randomChar() { |
| | 172 | + return chars[Math.floor(Math.random() * chars.length)] |
| | 173 | +} |
| | 174 | + |
| | 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 |
| 40 | } | 266 | } |
| 41 | } | 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 | + } |
| 42 | | 380 | |
| 43 | - // Get color from wheel position | 381 | + update() { |
| 44 | - function getColorFromWheel(x, y) { | 382 | + this.x += this.vx |
| 45 | - const centerX = colorWheel.width / 2 | 383 | + this.y += this.vy |
| 46 | - const centerY = colorWheel.height / 2 | 384 | + this.life -= 0.02 |
| 47 | - const dx = x - centerX | 385 | + |
| 48 | - const dy = y - centerY | 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 |
| | 471 | + } |
| | 472 | + |
| | 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 |
| | 496 | +function draw(e) { |
| | 497 | + if (!drawing) return |
| | 498 | + const rect = canvas.getBoundingClientRect() |
| | 499 | + const x = e.clientX - rect.left |
| | 500 | + const y = e.clientY - rect.top |
| | 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 |
| 49 | const distance = Math.sqrt(dx * dx + dy * dy) | 516 | const distance = Math.sqrt(dx * dx + dy * dy) |
| 50 | - const maxRadius = Math.min(centerX, centerY) - 2 | 517 | + if (distance <= eraseRadius) { |
| 51 | - | 518 | + activeChars.splice(i, 1) |
| 52 | - if (distance > maxRadius) return null | 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) |
| 53 | | 579 | |
| 54 | - let angle = Math.atan2(dy, dx) * 180 / Math.PI | 580 | + // Double vision (only if not animated) |
| 55 | - angle = (angle + 360) % 360 | 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 | + } |
| 56 | | 586 | |
| 57 | - const saturation = Math.min(100, (distance / maxRadius) * 100) | 587 | + // Mirror (only if not animated) |
| 58 | - const lightness = 50 - (distance / maxRadius) * 25 | 588 | + if (effects.mirror.checked) { |
| | 589 | + const mirrorX = canvas.width - drawX |
| | 590 | + ctx.fillStyle = color |
| | 591 | + ctx.fillText(char, mirrorX, drawY) |
| | 592 | + } |
| 59 | | 593 | |
| 60 | - return `hsl(${angle}, ${saturation}%, ${lightness}%)` | 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 | + } |
| 61 | } | 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 | +} |
| 62 | | 648 | |
| 63 | - // Color wheel interaction | 649 | +// Animation loop |
| 64 | - colorWheel.addEventListener('click', (e) => { | 650 | +function animate() { |
| 65 | - const rect = colorWheel.getBoundingClientRect() | 651 | + effectsCtx.clearRect(0, 0, effectsCanvas.width, effectsCanvas.height) |
| 66 | - const x = e.clientX - rect.left | 652 | + |
| 67 | - const y = e.clientY - rect.top | 653 | + // Update and draw animated characters on the effects canvas |
| 68 | - const color = getColorFromWheel(x, y) | 654 | + activeChars.forEach((char, index) => { |
| 69 | - | 655 | + char.update() |
| 70 | - if (color) { | 656 | + |
| 71 | - currentColor = color | 657 | + // Matrix rain movement |
| 72 | - colorPreview.style.backgroundColor = color | 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 |
| 73 | } | 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 | + }) |
| 74 | }) | 712 | }) |
| 75 | - | 713 | + effectsCtx.globalAlpha = 1 |
| 76 | - // Drawing functions | 714 | + } |
| 77 | - function randomChar() { | 715 | + |
| 78 | - return chars[Math.floor(Math.random() * chars.length)] | 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) |
| 79 | } | 722 | } |
| 80 | - | 723 | + }) |
| 81 | - function getFont() { | 724 | + |
| 82 | - return `${sizeInput.value}px monospace` | 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) |
| 83 | } | 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 | +} |
| 84 | | 743 | |
| 85 | - function draw(e) { | 744 | +// Helper functions |
| 86 | - if (!drawing) return | 745 | +function adjustBrightness(color, amount) { |
| 87 | - const rect = canvas.getBoundingClientRect() | 746 | + // Simple brightness adjustment for hex colors |
| 88 | - const x = e.clientX - rect.left | 747 | + if (color.startsWith('#')) { |
| 89 | - const y = e.clientY - rect.top | 748 | + const num = parseInt(color.slice(1), 16) |
| 90 | - ctx.font = getFont() | 749 | + const r = Math.max(0, Math.min(255, (num >> 16) + amount)) |
| 91 | - ctx.fillStyle = currentColor | 750 | + const g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount)) |
| 92 | - ctx.fillText(randomChar(), x, y) | 751 | + const b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount)) |
| 93 | - } | 752 | + return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}` |
| | 753 | + } |
| | 754 | + return color |
| | 755 | +} |
| 94 | | 756 | |
| 95 | - // Mouse events | 757 | +// Mouse tracking for magnetic effect |
| 96 | - canvas.addEventListener('mousedown', () => (drawing = true)) | 758 | +window.mouseX = 0 |
| 97 | - canvas.addEventListener('mouseup', () => (drawing = false)) | 759 | +window.mouseY = 0 |
| 98 | - canvas.addEventListener('mouseleave', () => (drawing = false)) | 760 | +canvas.addEventListener('mousemove', (e) => { |
| 99 | - canvas.addEventListener('mousemove', draw) | 761 | + const rect = canvas.getBoundingClientRect() |
| | 762 | + window.mouseX = e.clientX - rect.left |
| | 763 | + window.mouseY = e.clientY - rect.top |
| | 764 | +}) |
| 100 | | 765 | |
| 101 | - // Touch support | 766 | +// Mouse events |
| 102 | - canvas.addEventListener('touchstart', e => { | 767 | +canvas.addEventListener('mousedown', () => (drawing = true)) |
| 103 | - drawing = true | 768 | +canvas.addEventListener('mouseup', () => (drawing = false)) |
| 104 | - draw(e.touches[0]) | 769 | +canvas.addEventListener('mouseleave', () => { |
| 105 | - }) | 770 | + drawing = false |
| 106 | - canvas.addEventListener('touchmove', e => { | 771 | + window.mouseX = null |
| 107 | - draw(e.touches[0]) | 772 | + window.mouseY = null |
| 108 | - e.preventDefault() | 773 | +}) |
| 109 | - }) | 774 | +canvas.addEventListener('mousemove', draw) |
| 110 | - canvas.addEventListener('touchend', () => (drawing = false)) | 775 | + |
| | 776 | +// Touch support |
| | 777 | +canvas.addEventListener('touchstart', e => { |
| | 778 | + drawing = true |
| | 779 | + draw(e.touches[0]) |
| | 780 | +}) |
| | 781 | +canvas.addEventListener('touchmove', e => { |
| | 782 | + draw(e.touches[0]) |
| | 783 | + e.preventDefault() |
| | 784 | +}) |
| | 785 | +canvas.addEventListener('touchend', () => (drawing = false)) |
| | 786 | + |
| | 787 | +// Controls |
| | 788 | +const clearBtn = document.getElementById('clear') |
| | 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 | +} |
| 111 | | 797 | |
| 112 | - // Controls | 798 | +const downloadBtn = document.getElementById('download') |
| 113 | - const clearBtn = document.getElementById('clear') | 799 | +downloadBtn.onclick = () => { |
| 114 | - clearBtn.onclick = () => ctx.clearRect(0, 0, canvas.width, canvas.height) | 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 | +} |
| 115 | | 822 | |
| 116 | - const sizeInput = document.getElementById('size') | 823 | +const sizeInput = document.getElementById('size') |
| 117 | | 824 | |
| 118 | - // Initialize | 825 | +// Initialize |
| 119 | - drawColorWheel() | 826 | +drawColorWheel() |
| 120 | - colorPreview.style.backgroundColor = currentColor | 827 | +colorPreview.style.backgroundColor = currentColor |
| | 828 | +animate() |