hoel/js/entities.js
2026-02-02 23:27:35 +01:00

666 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Entity classes for game objects
function Star(canvas) {
this.canvas = canvas
this.reset();
this.baseBrightness = 0.6 + Math.random() * 0.4;
this.brightness = this.baseBrightness;
this.twinkleSpeed = Math.random() * 0.003 + 0.001;
this.twinkleAmount = 0.1 + Math.random() * 0.1;
var colorRand = Math.random();
for (var i = 0; i < CONFIG.STAR_COLORS.length; i++) {
if (colorRand < CONFIG.STAR_COLORS[i].threshold) {
this.color = CONFIG.STAR_COLORS[i].rgb;
break;
}
}
}
Star.prototype.reset = function() {
this.x = Math.random() * this.canvas.width;
this.y = Math.random() * this.canvas.height;
this.size = Math.random() * 1;
};
Star.prototype.update = function() {
this.brightness += this.twinkleSpeed;
if (this.brightness > this.baseBrightness + this.twinkleAmount ||
this.brightness < this.baseBrightness - this.twinkleAmount) {
this.twinkleSpeed = -this.twinkleSpeed;
}
};
Star.prototype.draw = function(ctx) {
ctx.fillStyle = 'rgba(' + this.color + ', ' + this.brightness + ')';
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
};
function Asteroid(type, blackHole, canvas) {
this.canvas = canvas
if (!type) type = 'small';
this.type = type;
var angle = Math.random() * Math.PI * 2;
var distance = Math.max(canvas.width, canvas.height) * 0.7;
this.x = this.canvas.width / 2 + Math.cos(angle) * distance;
this.y = this.canvas.height / 2 + Math.sin(angle) * distance;
this.angle = Math.atan2(this.y - blackHole.y, this.x - blackHole.x);
this.distance = Math.sqrt(
Math.pow(this.x - blackHole.x, 2) +
Math.pow(this.y - blackHole.y, 2)
);
this.orbitSpeed = 0.005 + Math.random() * 0.003;
this.decayRate = 0.08 + Math.random() * 0.05;
this.blackHole = blackHole;
this.initializeTypeSpecificProperties();
this.rotation = Math.random() * Math.PI * 2;
this.rotationSpeed = (Math.random() - 0.5) * 0.1;
}
Asteroid.prototype.initializeTypeSpecificProperties = function() {
// Visual size (unchanged)
if (this.type === 'comet') {
this.size = 3 + Math.random() * 3;
} else if (this.type === 'mtype') {
this.size = 15 + Math.random() * 3;
this.orbitSpeed *= 0.2;
this.decayRate *= 0.3;
this.planetColors = CONFIG.MTYPE_COLORS[Math.floor(Math.random() * CONFIG.MTYPE_COLORS.length)];
// Generate spots (dark brownish surface features)
this.spots = [];
var spotCount = 0 + Math.floor(Math.random() * 9); // 0-9 spots
for (var i = 0; i < spotCount; i++) {
var angle = Math.random() * Math.PI * 2;
var dist = Math.random() * this.size * 0.7;
this.spots.push({
x: Math.cos(angle) * dist,
y: Math.sin(angle) * dist,
radius: 1 + Math.random() * (this.size * 0.3),
color: CONFIG.MTYPE_SPOT_COLORS[Math.floor(Math.random() * CONFIG.MTYPE_SPOT_COLORS.length)]
});
}
// Generate sparkles
this.sparkles = [];
for (var s = 0; s < 20; s++) { // more sparkles if you want
var angle = Math.random() * Math.PI * 2;
var radius = Math.random() * this.size * 0.9;
this.sparkles.push({
x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius,
phase: Math.random() * Math.PI * 2 // random phase for variety
});
}
} else if (this.type === 'giant') {
this.size = 10 + Math.random() * 5;
this.orbitSpeed *= 0.3;
this.decayRate *= 0.4;
this.planetColors = CONFIG.GIANT_COLORS[Math.floor(Math.random() * CONFIG.GIANT_COLORS.length)];
// Initialize rings
this.rings = [];
var ringCount = Math.floor(Math.random() * 4) + 1; // 14 rings
for (var i = 0; i < ringCount; i++) {
this.rings.push({
color: CONFIG.GIANT_RING_COLORS[Math.floor(Math.random() * CONFIG.GIANT_RING_COLORS.length)],
thickness: 1 + Math.random() * 9, // random thickness
radiusOffset: 10 + i * 5 + Math.random() * 5, // spread rings outward
alpha: 0.3 + Math.random() * 0.4 // stored permanently
});
}
} else if (this.type === 'planet') {
this.size = 6 + Math.random() * 4;
this.orbitSpeed *= 0.5;
this.decayRate *= 0.6;
this.planetColors = CONFIG.PLANET_COLORS[Math.floor(Math.random() * CONFIG.PLANET_COLORS.length)];
} else if (this.type === 'large') {
this.size = 3 + Math.random() * 1;
} else if (this.type === 'medium') {
this.size = 2 + Math.random() * 1;
} else {
this.size = 1 + Math.random() * 1;
}
// Physical mass (kg)
var range = CONFIG.ASTEROID_MASS_RANGES[this.type] || CONFIG.ASTEROID_MASS_RANGES.small;
this.massKg = range[0] + Math.random() * (range[1] - range[0]);
};
Asteroid.prototype.update = function() {
this.angle += this.orbitSpeed;
this.distance -= this.decayRate;
this.x = this.blackHole.x + Math.cos(this.angle) * this.distance;
this.y = this.blackHole.y + Math.sin(this.angle) * this.distance;
this.rotation += this.rotationSpeed;
};
Asteroid.prototype.draw = function(ctx) {
if (this.type === 'comet') {
this.drawComet(ctx);
} else if (this.type === 'mtype') {
this.drawMType(ctx, performance.now() * 0.002);
} else if (this.type === 'giant') {
// Initialize per-giant tilt if not already done
if (this._ringTiltBase === undefined) {
this._ringTiltBase = (Math.random() - 0.5) * 0.3; // ±0.15 rad
this._tiltOscillationSpeed = 0.001 + Math.random() * 0.002;
this._tiltScale = 0.1 + Math.random() * 0.69; // vertical squash
}
const tilt = this._ringTiltBase + Math.sin(Date.now() * this._tiltOscillationSpeed) * 0.05;
if (this.rings && this.rings.length > 0) {
// Draw back halves of rings
this.rings.forEach((ring) => {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(tilt);
ctx.scale(1, this._tiltScale);
ctx.strokeStyle = ring.color.replace(/;?\s*$/, '') // just in case
ctx.strokeStyle = ring.color.startsWith('rgba')
? ring.color.replace(/rgba\(([^)]+),[^)]+\)/, `rgba($1,${ring.alpha})`)
: `rgba(${parseInt(ring.color.slice(1,3),16)},${parseInt(ring.color.slice(3,5),16)},${parseInt(ring.color.slice(5,7),16)},${ring.alpha})`;
ctx.lineWidth = ring.thickness;
ctx.beginPath();
ctx.arc(0, 0, this.size + ring.radiusOffset, Math.PI, 2 * Math.PI); // back half
ctx.stroke();
ctx.restore();
});
}
// Draw the giant planet
this.drawGiant(ctx);
if (this.rings && this.rings.length > 0) {
// Draw front halves of rings
this.rings.forEach((ring) => {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(tilt);
ctx.scale(1, this._tiltScale);
ctx.strokeStyle = ring.color.startsWith('rgba')
? ring.color.replace(/rgba\(([^)]+),[^)]+\)/, `rgba($1,${rings.alpha})`)
: `rgba(${parseInt(ring.color.slice(1,3),16)},${parseInt(ring.color.slice(3,5),16)},${parseInt(ring.color.slice(5,7),16)},${ring.alpha})`;
ctx.lineWidth = ring.thickness;
ctx.beginPath();
ctx.arc(0, 0, this.size + ring.radiusOffset, 0, Math.PI); // front half
ctx.stroke();
ctx.restore();
});
}
} else if (this.type === 'planet') {
this.drawPlanet(ctx);
} else {
this.drawAsteroid(ctx);
}
};
Asteroid.prototype.drawComet = function(ctx) {
var orbitTangentAngle = this.angle + Math.PI / 2;
var tailLength = this.size * 5;
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(orbitTangentAngle + Math.PI);
var gradient = ctx.createLinearGradient(0, 0, tailLength, 0);
gradient.addColorStop(0, 'rgba(80, 140, 200, 0.5)');
gradient.addColorStop(0.5, 'rgba(60, 100, 150, 0.25)');
gradient.addColorStop(1, 'rgba(40, 80, 120, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, -this.size * 0.4, tailLength, this.size * 0.8);
ctx.restore();
var nucleusSize = this.size * 0.5;
ctx.save();
ctx.translate(this.x, this.y);
ctx.fillStyle = '#5a7a8a';
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.arc(0, 0, nucleusSize, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = 'rgba(80, 120, 140, 0.2)';
ctx.beginPath();
ctx.arc(0, 0, nucleusSize * 1.3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
};
Asteroid.prototype.drawMType = function(ctx, time) {
var massRange = CONFIG.ASTEROID_MASS_RANGES.mtype;
var massNormalized = (this.massKg - massRange[0]) / (massRange[1] - massRange[0]); // 0-1
// Pulsing multiplier for glow
var pulse = 0.2 * Math.sin(time * 2 + this.massKg) + 1;
// Glow
var glowIntensity = (0.5 + massNormalized * 0.8) * pulse;
var glowSize = this.size * (1.5 + massNormalized * 0.8) * pulse;
ctx.save();
ctx.translate(this.x, this.y);
// Outer glow (soft)
var outerGlow = ctx.createRadialGradient(0, 0, this.size * 0.5, 0, 0, glowSize);
outerGlow.addColorStop(0, 'rgba(255, 200, 150,' + (glowIntensity * 0.4) + ')');
outerGlow.addColorStop(0.5, 'rgba(200, 120, 80,' + (glowIntensity * 0.2) + ')');
outerGlow.addColorStop(1, 'rgba(140, 80, 50, 0)');
ctx.fillStyle = outerGlow;
ctx.beginPath();
ctx.arc(0, 0, glowSize, 0, Math.PI * 2);
ctx.fill();
// Main body
var bodyGradient = ctx.createRadialGradient(
-this.size * 0.1, -this.size * 0.1, 0,
0, 0, this.size
);
bodyGradient.addColorStop(0, this.planetColors.light);
bodyGradient.addColorStop(0.5, this.planetColors.mid);
bodyGradient.addColorStop(1, this.planetColors.dark);
ctx.fillStyle = bodyGradient;
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
ctx.fill();
// Gritty surface texture (tiny semi-random dots)
for (var i = 0; i < this.size * 4; i++) {
ctx.fillStyle = 'rgba(0,0,0,' + (Math.random() * 0.05) + ')';
var rx = (Math.random() - 0.5) * this.size * 2;
var ry = (Math.random() - 0.5) * this.size * 2;
if (rx*rx + ry*ry <= this.size*this.size) ctx.fillRect(rx, ry, 1, 1);
}
// Swirling bands (subtle, irregular)
ctx.strokeStyle = 'rgba(0,0,0,0.06)';
ctx.lineWidth = 1;
var amplitude = this.size * 0.06;
var frequency = 0.25;
for (var band = -this.size * 0.7; band < this.size * 0.7; band += this.size * 0.15) {
ctx.beginPath();
for (var x = -this.size; x <= this.size; x += 1) {
var y = band + Math.sin(x * frequency + band + time) * amplitude
+ (Math.random()-0.5) * 1.5; // gritty randomness
if (x === -this.size) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
}
// Spots
for (var i = 0; i < this.spots.length; i++) {
var spot = this.spots[i];
ctx.save();
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
ctx.clip();
ctx.fillStyle = spot.color;
ctx.beginPath();
ctx.arc(spot.x, spot.y, spot.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Inner core glow
var coreGlow = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size * 0.5 * pulse);
coreGlow.addColorStop(0, 'rgba(255, 230, 160,' + (glowIntensity * 0.5) + ')');
coreGlow.addColorStop(1, 'rgba(255, 150, 80, 0)');
ctx.fillStyle = coreGlow;
ctx.beginPath();
ctx.arc(0, 0, this.size * 0.5 * pulse, 0, Math.PI * 2);
ctx.fill();
// Subtle edge glow instead of hard stroke
var edgeGlow = ctx.createRadialGradient(0, 0, this.size * 0.9, 0, 0, this.size);
edgeGlow.addColorStop(0, 'rgba(255,200,120,0.15)');
edgeGlow.addColorStop(1, 'rgba(255,200,120,0)');
ctx.fillStyle = edgeGlow;
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
ctx.fill();
// Sparkles (tiny random twinkles)
for (var i = 0; i < this.sparkles.length; i++) {
var s = this.sparkles[i];
var sparkleFrequency = 0.1;
var sparkleIntensity = 0.4 + 0.4 * Math.sin(time * sparkleFrequency + s.phase);
ctx.fillStyle = 'rgba(255,255,200,' + sparkleIntensity + ')';
ctx.fillRect(s.x, s.y, 1.5, 1.5);
}
ctx.restore();
};
Asteroid.prototype.drawGiant = function(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
var gradient = ctx.createRadialGradient(-this.size * 0.25, -this.size * 0.25, 0, 0, 0, this.size);
gradient.addColorStop(0, this.planetColors.light);
gradient.addColorStop(0.5, this.planetColors.mid);
gradient.addColorStop(1, this.planetColors.dark);
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
ctx.lineWidth = 1;
for (var band = -this.size * 0.6; band < this.size * 0.6; band += this.size * 0.15) {
ctx.beginPath();
var bandWidth = Math.sqrt(this.size * this.size - band * band);
ctx.moveTo(-bandWidth, band);
ctx.lineTo(bandWidth, band);
ctx.stroke();
}
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
};
Asteroid.prototype.drawPlanet = function(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
var gradient = ctx.createRadialGradient(-this.size * 0.2, -this.size * 0.2, 0, 0, 0, this.size);
gradient.addColorStop(0, this.planetColors.light);
gradient.addColorStop(0.5, this.planetColors.mid);
gradient.addColorStop(1, this.planetColors.dark);
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 0.5;
ctx.stroke();
ctx.restore();
};
Asteroid.prototype.drawAsteroid = function(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
var asteroidColor = CONFIG.ASTEROID_COLORS[this.type] || CONFIG.ASTEROID_COLORS.small;
ctx.fillStyle = asteroidColor;
ctx.beginPath();
ctx.moveTo(0, -this.size);
ctx.lineTo(this.size * 0.8, this.size * 0.6);
ctx.lineTo(-this.size * 0.8, this.size * 0.6);
ctx.closePath();
ctx.fill();
ctx.restore();
};
Asteroid.prototype.isDestroyed = function() {
return this.distance < this.blackHole.radius;
};
function BlackHole(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.pulse = 0;
this.pulseColor = '#000000';
};
BlackHole.prototype.draw = function(ctx, consumptionRate) {
// Smooth transition for jet intensity
if (!this.jetIntensity) this.jetIntensity = 0;
if (!this.targetJetIntensity) this.targetJetIntensity = 0;
var transitionSpeed = 0.05; // Fade speed
if (this.targetJetIntensity > this.jetIntensity) {
// Fade in
this.jetIntensity = Math.min(this.jetIntensity + transitionSpeed, this.targetJetIntensity);
} else if (this.targetJetIntensity < this.jetIntensity) {
// Fade out
this.jetIntensity = Math.max(this.jetIntensity - transitionSpeed, this.targetJetIntensity);
}
// Decay target intensity over time (jets fade after consumption)
if (this.targetJetIntensity > 0) {
this.targetJetIntensity = Math.max(0, this.targetJetIntensity - 0.01);
}
// Draw jets first (behind the black hole)
if (this.jetIntensity > 0) {
this.drawJets(ctx, this.jetIntensity);
}
// Existing black hole drawing code
if (this.pulse > 0) {
this.pulse -= 0.02;
if (this.pulse < 0) this.pulse = 0;
}
if (this.pulse === 0) {
this.pulseColor = '#000000';
}
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
var visualPulse = Math.min(this.pulse, 1);
var glowIntensity = 0.2 + (visualPulse * 0.3);
var glowRadius = this.radius * (1.01 + visualPulse * 0.1);
var gradient = ctx.createRadialGradient(
this.x, this.y, this.radius * 0.97,
this.x, this.y, glowRadius
);
var r, g, b;
if (this.pulseColor.startsWith('#')) {
var hex = this.pulseColor;
r = parseInt(hex.substr(1, 2), 16);
g = parseInt(hex.substr(3, 2), 16);
b = parseInt(hex.substr(5, 2), 16);
} else {
r = 60; g = 60; b = 80;
}
// Increase saturation by 3x when pulsing
var saturated = saturateColor(r, g, b, 3);
r = saturated.r;
g = saturated.g;
b = saturated.b;
gradient.addColorStop(0, 'rgba(' + r + ', ' + g + ', ' + b + ', 0)');
gradient.addColorStop(1, 'rgba(' + r + ', ' + g + ', ' + b + ', ' + glowIntensity + ')');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(this.x, this.y, glowRadius, 0, Math.PI * 2);
ctx.fill();
};
BlackHole.prototype.consumeAsteroid = function(asteroid) {
// Calculate pulse contribution (same as before)
var normalized = (Math.log10(asteroid.massKg) - 3) / 6;
var pulseContribution = Math.max(0.1, normalized);
this.pulse = Math.min(this.pulse + pulseContribution, 9);
// Determine new pulse color based on asteroid type/size
var newColor;
if (asteroid.planetColors) {
newColor = asteroid.planetColors.mid;
} else if (asteroid.type === 'comet') {
newColor = '#5a7a8a';
} else if (asteroid.size === 'large') {
newColor = '#f8f8f8';
} else if (asteroid.size === 'medium') {
newColor = '#888888';
} else {
newColor = '#444444';
}
// Blend with existing pulseColor instead of replacing
if (!this.pulseColor) this.pulseColor = newColor;
else this.pulseColor = this.blendColors(this.pulseColor, newColor, 0.3);
var jetMin = 1e7; // Start triggering at 10 Mt (Comets)
var jetMax = 1e29; // Max intensity (M-Type range)
if (asteroid.massKg >= jetMin) {
var logMin = Math.log10(jetMin);
var logMax = Math.log10(jetMax);
var logMass = Math.log10(asteroid.massKg);
// Log-scaled 0..1
var targetIntensity = Math.max(0, Math.min(
(logMass - logMin) / (logMax - logMin),
1
));
// Perceptual tweaks
targetIntensity = Math.pow(targetIntensity, 0.35); // boost low end
targetIntensity = 0.1 + 0.9 * targetIntensity; // 10%-100% range
// Set jet properties
this.targetJetIntensity = targetIntensity;
this.jetColor = newColor; // Match the object's color
}
};
// Helper function to blend two hex colors by weight (01)
BlackHole.prototype.blendColors = function(c1, c2, weight) {
var d2h = d => d.toString(16).padStart(2, '0');
var h2d = h => parseInt(h, 16);
// Remove #
c1 = c1.replace('#','');
c2 = c2.replace('#','');
var r = Math.round(h2d(c1.substring(0,2)) * (1-weight) + h2d(c2.substring(0,2)) * weight);
var g = Math.round(h2d(c1.substring(2,4)) * (1-weight) + h2d(c2.substring(2,4)) * weight);
var b = Math.round(h2d(c1.substring(4,6)) * (1-weight) + h2d(c2.substring(4,6)) * weight);
return '#' + d2h(r) + d2h(g) + d2h(b);
};
BlackHole.prototype.drawJets = function(ctx, intensity) {
// Get jet color from consumed object (default to blue if not set)
var baseColor = this.jetColor || '#6496ff';
// Convert hex to RGB
var r = parseInt(baseColor.substr(1, 2), 16);
var g = parseInt(baseColor.substr(3, 2), 16);
var b = parseInt(baseColor.substr(5, 2), 16);
// Lighten the color for the core
var coreR = Math.min(255, r + 50);
var coreG = Math.min(255, g + 50);
var coreB = Math.min(255, b + 50);
// Jet configuration (easily adjustable)
var config = {
color: 'rgba(' + r + ', ' + g + ', ' + b + ', 0.8)',
coreColor: 'rgba(' + coreR + ', ' + coreG + ', ' + coreB + ', 0.9)',
length: this.radius * 8, // Jet length (8x black hole radius)
width: this.radius * 0.2, // Jet width at base
coreWidth: this.radius * 0.05, // Bright core width
particleCount: 15, // Number of particles per jet
particleSize: 2, // Size of individual particles
rotation: 0, // Rotation angle (for spinning jets)
flickerSpeed: 0.1 // How fast jets flicker
};
// Intensity affects opacity and size (0-1 scale)
var alpha = Math.min(intensity, 1);
var sizeMultiplier = 0.5 + (intensity * 0.5); // 50% to 100% size based on intensity
// Draw both jets (top and bottom)
for (var direction = -1; direction <= 1; direction += 2) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(config.rotation);
// Outer jet cone (gradient)
var jetGradient = ctx.createLinearGradient(
0, 0,
0, direction * config.length * sizeMultiplier
);
var jetColor = config.color.replace(/[\d.]+\)/, (alpha * 0.6) + ')');
jetGradient.addColorStop(0, jetColor);
jetGradient.addColorStop(0.3, jetColor.replace(/[\d.]+\)/, (alpha * 0.4) + ')'));
jetGradient.addColorStop(1, 'rgba(' + r + ', ' + g + ', ' + b + ', 0)');
ctx.fillStyle = jetGradient;
ctx.beginPath();
ctx.moveTo(-config.width * sizeMultiplier, 0);
ctx.lineTo(0, direction * config.length * sizeMultiplier);
ctx.lineTo(config.width * sizeMultiplier, 0);
ctx.closePath();
ctx.fill();
// Inner bright core
var coreGradient = ctx.createLinearGradient(
0, 0,
0, direction * config.length * sizeMultiplier * 0.7
);
var coreColor = config.coreColor.replace(/[\d.]+\)/, alpha + ')');
coreGradient.addColorStop(0, coreColor);
coreGradient.addColorStop(0.5, coreColor.replace(/[\d.]+\)/, (alpha * 0.5) + ')'));
coreGradient.addColorStop(1, 'rgba(' + coreR + ', ' + coreG + ', ' + coreB + ', 0)');
ctx.fillStyle = coreGradient;
ctx.beginPath();
ctx.moveTo(-config.coreWidth * sizeMultiplier, 0);
ctx.lineTo(0, direction * config.length * sizeMultiplier * 0.7);
ctx.lineTo(config.coreWidth * sizeMultiplier, 0);
ctx.closePath();
ctx.fill();
// Particles along the jet (for dynamic effect)
ctx.fillStyle = config.coreColor.replace(/[\d.]+\)/, alpha + ')');
for (var i = 0; i < config.particleCount; i++) {
var particlePos = (i / config.particleCount) * config.length * sizeMultiplier;
var particleOffset = (Math.sin(Date.now() * 0.01 + i) * config.width * 0.3) * sizeMultiplier;
var particleSize = config.particleSize * (1 - i / config.particleCount); // Smaller as they go out
ctx.beginPath();
ctx.arc(
particleOffset,
direction * particlePos,
particleSize * alpha,
0,
Math.PI * 2
);
ctx.fill();
}
ctx.restore();
}
};