diff options
Diffstat (limited to 'resources/js/blood.js')
| -rw-r--r-- | resources/js/blood.js | 593 |
1 files changed, 593 insertions, 0 deletions
diff --git a/resources/js/blood.js b/resources/js/blood.js new file mode 100644 index 0000000..c45663a --- /dev/null +++ b/resources/js/blood.js @@ -0,0 +1,593 @@ +class BloodEffect { + constructor(canvas) { + this.canvas = canvas; + this.ctx = canvas.getContext('2d'); + this.width = canvas.width; + this.height = canvas.height; + + // Blood parameters + this.gravity = 0.15; + this.viscosity = 0.92; + this.particleCount = 100; + this.particleSize = 3; + this.sprayForce = 10; + this.splatterSize = 5; + this.spread = 0.3; + this.colorVariation = 15; + this.enableDrips = true; + this.enablePooling = true; + + // Animation state + this.paused = false; + this.animationId = null; + + // Particles and splatters + this.particles = []; + this.splatters = []; + this.pools = []; + this.drips = []; + + // Mouse interactions + this.mouseDown = false; + this.mouseX = 0; + this.mouseY = 0; + this.lastMouseX = 0; + this.lastMouseY = 0; + + // Initialize event listeners + this.initEvents(); + } + + initEvents() { + this.canvas.addEventListener('mousedown', (e) => { + this.mouseDown = true; + this.updateMousePosition(e); + this.lastMouseX = this.mouseX; + this.lastMouseY = this.mouseY; + this.createSpray(this.mouseX, this.mouseY, 0, 0); + }); + + this.canvas.addEventListener('mousemove', (e) => { + if (this.mouseDown) { + const lastX = this.mouseX; + const lastY = this.mouseY; + this.updateMousePosition(e); + + // Calculate velocity for direction + const velX = this.mouseX - lastX; + const velY = this.mouseY - lastY; + + // Create spray based on movement + if (Math.abs(velX) > 0.5 || Math.abs(velY) > 0.5) { + this.createSpray(this.mouseX, this.mouseY, velX, velY); + } + } + }); + + this.canvas.addEventListener('mouseup', () => { + this.mouseDown = false; + }); + + this.canvas.addEventListener('mouseleave', () => { + this.mouseDown = false; + }); + } + + updateMousePosition(e) { + const rect = this.canvas.getBoundingClientRect(); + this.mouseX = e.clientX - rect.left; + this.mouseY = e.clientY - rect.top; + } + + createSpray(x, y, velX, velY) { + // Direction influence from velocity + const dirX = velX === 0 ? 0 : Math.sign(velX); + const dirY = velY === 0 ? 0 : Math.sign(velY); + const speed = Math.sqrt(velX * velX + velY * velY); + + for (let i = 0; i < this.particleCount; i++) { + // Calculate random spray direction with influence from movement + const angle = Math.random() * Math.PI * 2; + const force = this.sprayForce * (0.5 + Math.random() * 0.5); + + // Base velocities with randomness + let vx = Math.cos(angle) * force * this.spread; + let vy = Math.sin(angle) * force * this.spread; + + // Add influence from mouse movement + if (speed > 1) { + vx += dirX * force * (1 - this.spread) * Math.random(); + vy += dirY * force * (1 - this.spread) * Math.random(); + } + + // Random size variation + const size = this.particleSize * (0.5 + Math.random()); + + // Random color variation (darker/lighter red) + const r = 120 + Math.floor(Math.random() * this.colorVariation); + const g = 0 + Math.floor(Math.random() * (this.colorVariation * 0.4)); + const b = 0 + Math.floor(Math.random() * (this.colorVariation * 0.2)); + const color = `rgb(${r}, ${g}, ${b})`; + + // Add blood particle + this.particles.push({ + x: x + (Math.random() - 0.5) * 5, + y: y + (Math.random() - 0.5) * 5, + vx: vx, + vy: vy, + size: size, + color: color, + gravity: this.gravity * (0.8 + Math.random() * 0.4), + life: 1.0, // Life percentage (1.0 = full life, 0.0 = dead) + decay: 0.01 + Math.random() * 0.02 + }); + } + } + + createRandomSpray() { + const x = Math.random() * this.width; + const y = Math.random() * (this.height / 2); + const velX = (Math.random() - 0.5) * 20; + const velY = Math.random() * 10; + this.createSpray(x, y, velX, velY); + } + + createBloodBurst() { + const x = this.width / 2 + (Math.random() - 0.5) * 200; + const y = this.height / 2 + (Math.random() - 0.5) * 100; + + // Create a more intense spray for burst + const oldParticleCount = this.particleCount; + const oldSprayForce = this.sprayForce; + + this.particleCount = this.particleCount * 3; + this.sprayForce = this.sprayForce * 1.5; + + this.createSpray(x, y, 0, 0); + + // Reset to original values + this.particleCount = oldParticleCount; + this.sprayForce = oldSprayForce; + } + + createSplatter(x, y, size, color) { + // Create a blood splatter at impact location + const splatterSize = size * this.splatterSize; + + this.splatters.push({ + x: x, + y: y, + size: splatterSize, + color: color, + // Random shapes for splatters + shape: Math.floor(Math.random() * 3), + angle: Math.random() * Math.PI * 2, + // Stretch factor for directional impact + stretchX: 0.5 + Math.random(), + stretchY: 0.5 + Math.random(), + // How much it has dried/darkened (0 = fresh, 1 = dried) + dried: 0 + }); + + // Possibly create a drip + if (this.enableDrips && Math.random() < 0.3) { + this.createDrip(x, y, color); + } + + // Possibly create a pool below the splatter + if (this.enablePooling && y > this.height * 0.7 && Math.random() < 0.5) { + this.createPool(x, y, splatterSize * 1.5, color); + } + } + + createDrip(x, y, color) { + const length = 10 + Math.random() * 30; + const width = 2 + Math.random() * 4; + + this.drips.push({ + x: x, + y: y, + targetY: y + length, + width: width, + color: color, + progress: 0, + speed: 0.005 + Math.random() * 0.01, // How fast it drips down + dried: 0 + }); + } + + createPool(x, y, size, color) { + this.pools.push({ + x: x, + y: y, + currentSize: 0, + targetSize: size * (1 + Math.random()), + growSpeed: 0.1 + Math.random() * 0.2, + color: color, + dried: 0 + }); + } + + updatePhysics() { + // Update particles + for (let i = this.particles.length - 1; i >= 0; i--) { + const particle = this.particles[i]; + + // Apply physics + particle.vy += particle.gravity; + particle.vx *= this.viscosity; + particle.vy *= this.viscosity; + + // Update position + particle.x += particle.vx; + particle.y += particle.vy; + + // Check for collisions with bottom or sides + if (particle.y > this.height - particle.size) { + // Bottom collision - create splatter + this.createSplatter(particle.x, this.height, particle.size, particle.color); + this.particles.splice(i, 1); + } else if (particle.x < 0 || particle.x > this.width) { + // Side collision - create splatter + const x = particle.x < 0 ? 0 : this.width; + this.createSplatter(x, particle.y, particle.size, particle.color); + this.particles.splice(i, 1); + } else { + // Decay life + particle.life -= particle.decay; + if (particle.life <= 0) { + this.particles.splice(i, 1); + } + } + } + + // Update drips + for (let i = this.drips.length - 1; i >= 0; i--) { + const drip = this.drips[i]; + + // Grow drip down + drip.progress += drip.speed; + if (drip.progress >= 1) { + // When drip reaches target, it stops + drip.progress = 1; + // Slowly dry + drip.dried += 0.001; + + // Remove very dried drips + if (drip.dried > 0.7) { + this.drips.splice(i, 1); + } + } + } + + // Update pools + for (let i = 0; i < this.pools.length; i++) { + const pool = this.pools[i]; + + // Grow pool + if (pool.currentSize < pool.targetSize) { + pool.currentSize += pool.growSpeed; + } else { + // Slowly dry + pool.dried += 0.0005; + } + } + + // Update splatters + for (let i = 0; i < this.splatters.length; i++) { + // Slowly dry + this.splatters[i].dried += 0.0002; + } + } + + render() { + // Draw particles + for (const particle of this.particles) { + this.ctx.beginPath(); + this.ctx.fillStyle = particle.color; + this.ctx.globalAlpha = particle.life; + this.ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); + this.ctx.fill(); + } + + // Draw splatters + for (const splatter of this.splatters) { + // Darker color as it dries + const color = this.adjustColorForDrying(splatter.color, splatter.dried); + this.ctx.fillStyle = color; + this.ctx.globalAlpha = 1; + + this.ctx.save(); + this.ctx.translate(splatter.x, splatter.y); + this.ctx.rotate(splatter.angle); + this.ctx.scale(splatter.stretchX, splatter.stretchY); + + // Different splatter shapes + switch (splatter.shape) { + case 0: + // Circular splatter + this.ctx.beginPath(); + this.ctx.arc(0, 0, splatter.size, 0, Math.PI * 2); + this.ctx.fill(); + break; + case 1: + // Star-like splatter + this.ctx.beginPath(); + const points = 5 + Math.floor(Math.random() * 4); + for (let i = 0; i < points * 2; i++) { + const radius = i % 2 === 0 ? splatter.size : splatter.size * 0.5; + const angle = (i * Math.PI) / points; + const x = Math.cos(angle) * radius; + const y = Math.sin(angle) * radius; + if (i === 0) this.ctx.moveTo(x, y); + else this.ctx.lineTo(x, y); + } + this.ctx.closePath(); + this.ctx.fill(); + break; + case 2: + // Irregular blob + this.ctx.beginPath(); + const segments = 8 + Math.floor(Math.random() * 5); + for (let i = 0; i <= segments; i++) { + const angle = (i / segments) * Math.PI * 2; + const radius = splatter.size * (0.7 + Math.random() * 0.6); + const x = Math.cos(angle) * radius; + const y = Math.sin(angle) * radius; + if (i === 0) this.ctx.moveTo(x, y); + else this.ctx.bezierCurveTo( + x - Math.random() * 10, y - Math.random() * 10, + x + Math.random() * 10, y + Math.random() * 10, + x, y + ); + } + this.ctx.closePath(); + this.ctx.fill(); + break; + } + + this.ctx.restore(); + } + + // Draw drips + for (const drip of this.drips) { + const color = this.adjustColorForDrying(drip.color, drip.dried); + this.ctx.fillStyle = color; + this.ctx.globalAlpha = 1; + + // Calculate current height based on progress + const currentY = drip.y + (drip.targetY - drip.y) * drip.progress; + + // Draw drip as elongated teardrop + this.ctx.beginPath(); + this.ctx.moveTo(drip.x - drip.width/2, drip.y); + this.ctx.bezierCurveTo( + drip.x - drip.width, drip.y + (currentY - drip.y) * 0.3, + drip.x - drip.width/3, currentY - drip.width, + drip.x, currentY + ); + this.ctx.bezierCurveTo( + drip.x + drip.width/3, currentY - drip.width, + drip.x + drip.width, drip.y + (currentY - drip.y) * 0.3, + drip.x + drip.width/2, drip.y + ); + this.ctx.closePath(); + this.ctx.fill(); + } + + // Draw pools + for (const pool of this.pools) { + const color = this.adjustColorForDrying(pool.color, pool.dried); + this.ctx.fillStyle = color; + this.ctx.globalAlpha = 1; + + // Draw pool as irregular ellipse + this.ctx.beginPath(); + const segments = 12; + for (let i = 0; i <= segments; i++) { + const angle = (i / segments) * Math.PI * 2; + // Make pool wider than tall + const radiusX = pool.currentSize * (1.2 + Math.sin(angle * 3) * 0.2); + const radiusY = pool.currentSize * (0.6 + Math.cos(angle * 2) * 0.1); + const x = pool.x + Math.cos(angle) * radiusX; + const y = pool.y + Math.sin(angle) * radiusY; + if (i === 0) this.ctx.moveTo(x, y); + else this.ctx.bezierCurveTo( + x - Math.random() * 5, y - Math.random() * 2, + x + Math.random() * 5, y + Math.random() * 2, + x, y + ); + } + this.ctx.closePath(); + this.ctx.fill(); + } + + // Reset alpha + this.ctx.globalAlpha = 1; + } + + adjustColorForDrying(color, driedAmount) { + // Extract RGB components + const rgbMatch = color.match(/\d+/g); + if (!rgbMatch || rgbMatch.length < 3) return color; + + const r = parseInt(rgbMatch[0]); + const g = parseInt(rgbMatch[1]); + const b = parseInt(rgbMatch[2]); + + // Calculate dried color (darker, more brown) + const driedR = Math.max(0, Math.floor(r - (driedAmount * 80))); + const driedG = Math.max(0, Math.floor(g - (driedAmount * 10))); + const driedB = Math.max(0, Math.floor(b - (driedAmount * 10))); + + return `rgb(${driedR}, ${driedG}, ${driedB})`; + } + + animate() { + if (this.paused) return; + + // Update physics + this.updatePhysics(); + + // Render + this.render(); + + // Schedule next frame + this.animationId = requestAnimationFrame(() => this.animate()); + } + + start() { + this.paused = false; + if (!this.animationId) { + this.animate(); + } + } + + pause() { + this.paused = true; + if (this.animationId) { + cancelAnimationFrame(this.animationId); + this.animationId = null; + } + } + + togglePause() { + if (this.paused) { + this.start(); + } else { + this.pause(); + } + } + + clear() { + this.ctx.clearRect(0, 0, this.width, this.height); + this.particles = []; + this.splatters = []; + this.pools = []; + this.drips = []; + } + + // Parameter setters + setGravity(value) { + this.gravity = value; + } + + setViscosity(value) { + this.viscosity = value; + } + + setParticleCount(value) { + this.particleCount = value; + } + + setParticleSize(value) { + this.particleSize = value; + } + + setSprayForce(value) { + this.sprayForce = value; + } + + setSplatterSize(value) { + this.splatterSize = value; + } + + setSpread(value) { + this.spread = value; + } + + setEnableDrips(value) { + this.enableDrips = value; + } + + setEnablePooling(value) { + this.enablePooling = value; + } + + setColorVariation(value) { + this.colorVariation = value; + } + } + + // Initialize the blood effect + const canvas = document.getElementById('sprayCanvas'); + const bloodEffect = new BloodEffect(canvas); + + // Start the animation + bloodEffect.start(); + + // Set up UI controls + const gravitySlider = document.getElementById('gravitySlider'); + const viscositySlider = document.getElementById('viscositySlider'); + const particleCountSlider = document.getElementById('particleCountSlider'); + const particleSizeSlider = document.getElementById('particleSizeSlider'); + const sprayForceSlider = document.getElementById('sprayForceSlider'); + const splatterSizeSlider = document.getElementById('splatterSizeSlider'); + const spreadSlider = document.getElementById('spreadSlider'); + const dripToggle = document.getElementById('dripToggle'); + const poolToggle = document.getElementById('poolToggle'); + const colorVariationSlider = document.getElementById('colorVariationSlider'); + + const clearBtn = document.getElementById('clearBtn'); + const randomSprayBtn = document.getElementById('randomSprayBtn'); + const burstBtn = document.getElementById('burstBtn'); + const pauseBtn = document.getElementById('pauseBtn'); + + // Event listeners for sliders + gravitySlider.addEventListener('input', function() { + bloodEffect.setGravity(parseFloat(this.value)); + }); + + viscositySlider.addEventListener('input', function() { + bloodEffect.setViscosity(parseFloat(this.value)); + }); + + particleCountSlider.addEventListener('input', function() { + bloodEffect.setParticleCount(parseInt(this.value)); + }); + + particleSizeSlider.addEventListener('input', function() { + bloodEffect.setParticleSize(parseInt(this.value)); + }); + + sprayForceSlider.addEventListener('input', function() { + bloodEffect.setSprayForce(parseInt(this.value)); + }); + + splatterSizeSlider.addEventListener('input', function() { + bloodEffect.setSplatterSize(parseInt(this.value)); + }); + + spreadSlider.addEventListener('input', function() { + bloodEffect.setSpread(parseFloat(this.value)); + }); + + dripToggle.addEventListener('change', function() { + bloodEffect.setEnableDrips(this.checked); + }); + + poolToggle.addEventListener('change', function() { + bloodEffect.setEnablePooling(this.checked); + }); + + colorVariationSlider.addEventListener('input', function() { + bloodEffect.setColorVariation(parseInt(this.value)); + }); + + // Button event listeners + clearBtn.addEventListener('click', function() { + bloodEffect.clear(); + }); + + randomSprayBtn.addEventListener('click', function() { + bloodEffect.createRandomSpray(); + }); + + burstBtn.addEventListener('click', function() { + bloodEffect.createBloodBurst(); + }); + + pauseBtn.addEventListener('click', function() { + bloodEffect.togglePause(); + this.textContent = bloodEffect.paused ? 'Resume' : 'Pause'; + });
\ No newline at end of file |
