@@ -1,12 +1,77 @@ |
| 1 | 1 | // Main canvas setup |
| 2 | 2 | const canvas = document.getElementById('board') |
| 3 | 3 | const ctx = canvas.getContext('2d') |
| 4 | +const effectsCanvas = document.getElementById('effectsCanvas') |
| 5 | +const effectsCtx = effectsCanvas.getContext('2d') |
| 6 | + |
| 4 | 7 | const chars = [ |
| 5 | 8 | '@', '#', '$', '%', '&', '*', '+', '=', |
| 6 | 9 | '~', '?', 'Ω', '∞', '¶', '§', '★', '☆' |
| 7 | 10 | ] |
| 11 | + |
| 8 | 12 | let drawing = false |
| 9 | 13 | 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 | +}) |
| 10 | 75 | |
| 11 | 76 | // Color wheel setup |
| 12 | 77 | const colorWheel = document.getElementById('colorWheel') |
@@ -15,7 +80,7 @@ |
| 15 | 80 | colorWheel.width = 150 |
| 16 | 81 | colorWheel.height = 150 |
| 17 | 82 | |
| 18 | | - // Draw color wheel |
| 83 | +// Draw color wheel with white center and black ring |
| 19 | 84 | function drawColorWheel() { |
| 20 | 85 | const centerX = colorWheel.width / 2 |
| 21 | 86 | const centerY = colorWheel.height / 2 |
@@ -33,11 +98,20 @@ |
| 33 | 98 | const gradient = colorCtx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius) |
| 34 | 99 | gradient.addColorStop(0, 'white') |
| 35 | 100 | 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') |
| 37 | 103 | |
| 38 | 104 | colorCtx.fillStyle = gradient |
| 39 | 105 | colorCtx.fill() |
| 40 | 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() |
| 41 | 115 | } |
| 42 | 116 | |
| 43 | 117 | // Get color from wheel position |
@@ -50,12 +124,15 @@ |
| 50 | 124 | const maxRadius = Math.min(centerX, centerY) - 2 |
| 51 | 125 | |
| 52 | 126 | if (distance > maxRadius) return null |
| 127 | + if (distance < 15) return 'white' |
| 128 | + if (distance > maxRadius * 0.95) return 'black' |
| 53 | 129 | |
| 54 | 130 | let angle = Math.atan2(dy, dx) * 180 / Math.PI |
| 55 | 131 | angle = (angle + 360) % 360 |
| 56 | 132 | |
| 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) |
| 59 | 136 | |
| 60 | 137 | return `hsl(${angle}, ${saturation}%, ${lightness}%)` |
| 61 | 138 | } |
@@ -73,29 +150,627 @@ |
| 73 | 150 | } |
| 74 | 151 | }) |
| 75 | 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 | + |
| 76 | 170 | // Drawing functions |
| 77 | 171 | function randomChar() { |
| 78 | 172 | return chars[Math.floor(Math.random() * chars.length)] |
| 79 | 173 | } |
| 80 | 174 | |
| 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 |
| 83 | 471 | } |
| 84 | 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 |
| 85 | 496 | function draw(e) { |
| 86 | 497 | if (!drawing) return |
| 87 | 498 | const rect = canvas.getBoundingClientRect() |
| 88 | 499 | const x = e.clientX - rect.left |
| 89 | 500 | 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 |
| 93 | 714 | } |
| 94 | 715 | |
| 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 | + |
| 95 | 766 | // Mouse events |
| 96 | 767 | canvas.addEventListener('mousedown', () => (drawing = true)) |
| 97 | 768 | 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 | +}) |
| 99 | 774 | canvas.addEventListener('mousemove', draw) |
| 100 | 775 | |
| 101 | 776 | // Touch support |
@@ -111,10 +786,43 @@ |
| 111 | 786 | |
| 112 | 787 | // Controls |
| 113 | 788 | 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 | +} |
| 115 | 822 | |
| 116 | 823 | const sizeInput = document.getElementById('size') |
| 117 | 824 | |
| 118 | 825 | // Initialize |
| 119 | 826 | drawColorWheel() |
| 120 | 827 | colorPreview.style.backgroundColor = currentColor |
| 828 | +animate() |