summaryrefslogtreecommitdiff
path: root/resources/js/blood.js
diff options
context:
space:
mode:
Diffstat (limited to 'resources/js/blood.js')
-rw-r--r--resources/js/blood.js593
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