// 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 = 5 + Math.random() * 3; } else if (this.type === 'giant') { this.size = 15 + Math.random() * 10; 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); // 0-3 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 = 10 + Math.random() * 5; 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 = 5 + Math.random() * 2; } else if (this.type === 'medium') { this.size = 3 + Math.random() * 2; } else { this.size = 1 + Math.random() * 2; } // 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 === '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.draw = function(ctx) { // if (this.type === 'comet') { // this.drawComet(ctx); // } else if (this.type === 'giant') { // // Draw the giant at its absolute position // this.drawGiant(ctx); // // // Draw rings if present // if (this.rings && this.rings.length > 0) { // this.rings.forEach((ring, index) => { // ctx.save(); // // // Move origin to giant's center // ctx.translate(this.x, this.y); // // // Independent tilt + oscillation per ring // if (ring._tiltBase === undefined) { // ring._tiltBase = (Math.random() - 0.5) * 0.4; // ±0.2 rad // ring._oscillationSpeed = 0.001 + Math.random() * 0.002; // } // const tilt = ring._tiltBase + Math.sin(Date.now() * ring._oscillationSpeed) * 0.05; // // ctx.rotate(tilt); // // ctx.strokeStyle = ring.color; // ctx.lineWidth = ring.thickness; // ctx.beginPath(); // ctx.arc(0, 0, this.size + ring.radiusOffset, 0, Math.PI * 2); // ctx.stroke(); // // ctx.restore(); // }); // } // } else if (this.type === 'planet') { // this.drawPlanet(ctx); // } else { // this.drawAsteroid(ctx); // } //}; //Asteroid.prototype.draw = function(ctx) { // if (this.type === 'comet') { // this.drawComet(ctx); // } else if (this.type === 'giant') { // this.drawGiant(ctx); // if (this.rings && this.rings.length > 0) { // this.rings.forEach(function(ring) { // ctx.strokeStyle = ring.color; // ctx.lineWidth = ring.thickness; // ctx.beginPath(); // ctx.arc(0, 0, this.size + ring.radiusOffset, 0, Math.PI * 2); // ctx.stroke(); // }, this); // } // } 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.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) { 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); }; // Helper function to blend two hex colors by weight (0–1) 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.consumeAsteroid = function(asteroid) { // // Calculate pulse contribution based on mass // // Using log scale: small asteroids (1e3 kg) give ~0.1, giants (5e14 kg) give ~2.0 // var normalized = (Math.log10(asteroid.massKg) - 3) / 6; // Range: 0 to ~2 // var pulseContribution = Math.max(0.1, normalized); // Minimum 0.05 for visibility // // // Add to existing pulse (cumulative effect) // this.pulse = Math.min(this.pulse + pulseContribution, 9); // Cap at 9 for very large accumulations // // // Update color - blend with existing color if already glowing // if (asteroid.planetColors) { // this.pulseColor = asteroid.planetColors.mid; // } else if (asteroid.type === 'comet') { // this.pulseColor = '#5a7a8a'; // } else if (asteroid.size === 'large') { // this.pulseColor = '#ffffff'; // } else if (asteroid.size === 'medium') { // this.pulseColor = '#888888'; // } else { // this.pulseColor = '#444444'; // } //};