Created
November 18, 2025 12:19
-
-
Save lord-carlos/4c66b1d38779d9c93d531e2846abe4a9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>V-Shape Radar Scope</title> | |
| <style> | |
| body { | |
| background-color: #050505; | |
| margin: 0; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100vh; | |
| font-family: 'Consolas', 'Monaco', 'Courier New', monospace; /* Tech font */ | |
| overflow: hidden; | |
| } | |
| .radar-container { | |
| position: relative; | |
| width: 800px; | |
| height: 500px; /* Rectangular container for V shape */ | |
| background: #000; | |
| border-bottom: 2px solid #1a331a; | |
| overflow: hidden; | |
| } | |
| canvas { | |
| display: block; | |
| } | |
| /* CRT Overlay Effect */ | |
| .overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), | |
| linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); | |
| background-size: 100% 3px, 3px 100%; | |
| pointer-events: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="radar-container"> | |
| <canvas id="radarCanvas"></canvas> | |
| <div class="overlay"></div> | |
| </div> | |
| <script> | |
| /* ========================================== | |
| CONFIGURATION VARIABLES | |
| ========================================== */ | |
| const CONFIG = { | |
| // Scanning Speed | |
| SCAN_SPEED: 0.5, | |
| // Fade speed: How fast detected objects disappear | |
| FADE_SPEED: 0.003, | |
| // Generation: Probability of spawning a new invisible target per frame | |
| TARGET_PROBABILITY: 0.05, | |
| // Max active targets (visible or invisible) | |
| MAX_TARGETS: 4, | |
| // Toggle text labels | |
| SHOW_LABELS: true, | |
| // Visuals | |
| COLOR_GRID: '#152b15', | |
| COLOR_SCANLINE: '#33ff00', | |
| COLOR_TARGET: '#ff3333', | |
| COLOR_TEXT: '#33ff00' | |
| }; | |
| /* ========================================== | |
| SETUP & UTILS | |
| ========================================== */ | |
| const canvas = document.getElementById('radarCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Set canvas resolution | |
| const width = 800; | |
| const height = 500; | |
| canvas.width = width; | |
| canvas.height = height; | |
| // Math constants | |
| const ORIGIN_X = width / 2; // Center | |
| const ORIGIN_Y = height - 20; // Bottom (minus padding) | |
| const RADIUS = 450; // Length of the radar range | |
| const ANGLE_START = 225; // Left side of V (In canvas degrees: 0 is right, 270 is up) | |
| const ANGLE_END = 315; // Right side of V | |
| // We map logical 0-90 to the Canvas angles above | |
| // Let's use a simpler "Scan Angle" of 45 to 135 degrees, where 90 is straight up. | |
| // To convert to Canvas Math: radians = -Angle * PI / 180 | |
| let scanAngle = 45; // Start at 45 degrees (right tilt) | |
| let direction = 1; // 1 = moving left, -1 = moving right | |
| let targets = []; | |
| const toRad = (deg) => deg * (Math.PI / 180); | |
| // Generate a random XXX-XX ID | |
| function generateID() { | |
| const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |
| const nums = "0123456789"; | |
| const rC = () => chars[Math.floor(Math.random() * chars.length)]; | |
| const rN = () => nums[Math.floor(Math.random() * nums.length)]; | |
| return `BPDEV-${rN()}${rN()}${rN()}${rN()}`; | |
| } | |
| /* ========================================== | |
| CLASSES | |
| ========================================== */ | |
| class Target { | |
| constructor() { | |
| // Random angle between 45 and 135 | |
| this.angle = 45 + (Math.random() * 90); | |
| // Random distance | |
| this.distance = 100 + (Math.random() * (RADIUS - 120)); | |
| this.id = generateID(); | |
| this.isDetected = false; // Invisible until hit | |
| this.opacity = 0; // Start invisible | |
| this.size = 4; | |
| } | |
| update(currentScanAngle) { | |
| // DETECTION LOGIC: | |
| // If not yet detected, check if scan line is VERY close (collision) | |
| if (!this.isDetected) { | |
| const diff = Math.abs(this.angle - currentScanAngle); | |
| // If the line is within 0.8 degrees of the target | |
| if (diff < 0.8) { | |
| this.isDetected = true; | |
| this.opacity = 1.0; // Flash visible | |
| } | |
| } else { | |
| // If already detected, fade out | |
| this.opacity -= CONFIG.FADE_SPEED; | |
| } | |
| } | |
| draw(ctx) { | |
| // Don't draw if invisible | |
| if (this.opacity <= 0) return; | |
| const rads = toRad(-this.angle); // Negative for Canvas Y-up flip | |
| const x = ORIGIN_X + Math.cos(rads) * this.distance; | |
| const y = ORIGIN_Y + Math.sin(rads) * this.distance; | |
| ctx.globalAlpha = this.opacity; | |
| // Draw Blip | |
| ctx.beginPath(); | |
| ctx.arc(x, y, this.size, 0, Math.PI * 2); | |
| ctx.fillStyle = CONFIG.COLOR_TARGET; | |
| ctx.fill(); | |
| // Draw Ring | |
| ctx.strokeStyle = CONFIG.COLOR_TARGET; | |
| ctx.stroke(); | |
| // Draw Label | |
| if (CONFIG.SHOW_LABELS) { | |
| ctx.font = "12px monospace"; | |
| ctx.fillStyle = CONFIG.COLOR_TEXT; | |
| ctx.fillText(this.id, x + 10, y - 5); | |
| // Optional: connector line | |
| ctx.beginPath(); | |
| ctx.moveTo(x, y); | |
| ctx.lineTo(x + 8, y - 8); | |
| ctx.strokeStyle = CONFIG.COLOR_TEXT; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| } | |
| ctx.globalAlpha = 1.0; // Reset | |
| } | |
| } | |
| /* ========================================== | |
| DRAWING FUNCTIONS | |
| ========================================== */ | |
| function drawGrid() { | |
| ctx.lineWidth = 1; | |
| ctx.strokeStyle = CONFIG.COLOR_GRID; | |
| // 1. Draw Arc Borders | |
| // Create a clipping path or just draw the frame? Let's draw the frame. | |
| ctx.beginPath(); | |
| // Draw V lines | |
| const startRad = toRad(-45); | |
| const endRad = toRad(-135); | |
| // Left Line | |
| ctx.moveTo(ORIGIN_X, ORIGIN_Y); | |
| ctx.lineTo(ORIGIN_X + Math.cos(endRad) * RADIUS, ORIGIN_Y + Math.sin(endRad) * RADIUS); | |
| // Right Line | |
| ctx.moveTo(ORIGIN_X, ORIGIN_Y); | |
| ctx.lineTo(ORIGIN_X + Math.cos(startRad) * RADIUS, ORIGIN_Y + Math.sin(startRad) * RADIUS); | |
| ctx.stroke(); | |
| // 2. Range Rings (Distance markers) | |
| const rings = 4; | |
| for (let i = 1; i <= rings; i++) { | |
| ctx.beginPath(); | |
| const r = (RADIUS / rings) * i; | |
| ctx.arc(ORIGIN_X, ORIGIN_Y, r, toRad(-135), toRad(-45)); | |
| ctx.stroke(); | |
| } | |
| // 3. Angle Lines | |
| const angleStep = 15; | |
| for (let a = 45; a <= 135; a += angleStep) { | |
| if (a === 45 || a === 135) continue; // Skip borders | |
| const rad = toRad(-a); | |
| ctx.beginPath(); | |
| ctx.moveTo(ORIGIN_X, ORIGIN_Y); | |
| ctx.lineTo(ORIGIN_X + Math.cos(rad) * RADIUS, ORIGIN_Y + Math.sin(rad) * RADIUS); | |
| ctx.stroke(); | |
| } | |
| // 4. Degree numbers (Optional polish) | |
| ctx.fillStyle = CONFIG.COLOR_GRID; | |
| ctx.font = "10px monospace"; | |
| ctx.textAlign = "center"; | |
| for (let a = 45; a <= 135; a += 15) { | |
| const rad = toRad(-a); | |
| const textDist = RADIUS + 15; | |
| const tx = ORIGIN_X + Math.cos(rad) * textDist; | |
| const ty = ORIGIN_Y + Math.sin(rad) * textDist; | |
| // Show logic angle (0 is center) | |
| const displayAngle = 90 - a; | |
| ctx.fillText(Math.abs(displayAngle) + "°", tx, ty); | |
| } | |
| } | |
| function drawScanLine() { | |
| const rad = toRad(-scanAngle); | |
| const tipX = ORIGIN_X + Math.cos(rad) * RADIUS; | |
| const tipY = ORIGIN_Y + Math.sin(rad) * RADIUS; | |
| // Draw the main beam | |
| ctx.beginPath(); | |
| ctx.moveTo(ORIGIN_X, ORIGIN_Y); | |
| ctx.lineTo(tipX, tipY); | |
| ctx.strokeStyle = CONFIG.COLOR_SCANLINE; | |
| ctx.lineWidth = 2; | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = CONFIG.COLOR_SCANLINE; | |
| ctx.stroke(); | |
| ctx.shadowBlur = 0; | |
| // Draw the trailing fade (Gradient sector) | |
| // We simulate this by drawing many lines with decreasing opacity behind the main line | |
| const trailLength = 20; // Number of lines in trail | |
| for (let i = 0; i < trailLength; i++) { | |
| // Calculate angle of this trail segment | |
| // If direction is 1 (increasing angle, moving left), trail is behind (smaller angle) | |
| // Actually direction 1 means 45 -> 135. So trail is scanAngle - i | |
| const trailAngle = scanAngle - (i * direction * 0.5); | |
| // Clip trail to V shape bounds | |
| if (trailAngle < 45 || trailAngle > 135) continue; | |
| const tRad = toRad(-trailAngle); | |
| const tx = ORIGIN_X + Math.cos(tRad) * RADIUS; | |
| const ty = ORIGIN_Y + Math.sin(tRad) * RADIUS; | |
| ctx.beginPath(); | |
| ctx.moveTo(ORIGIN_X, ORIGIN_Y); | |
| ctx.lineTo(tx, ty); | |
| ctx.strokeStyle = `rgba(51, 255, 0, ${0.15 - (i * 0.007)})`; | |
| ctx.lineWidth = 2; // Fill gaps | |
| ctx.stroke(); | |
| } | |
| } | |
| /* ========================================== | |
| MAIN LOOP | |
| ========================================== */ | |
| function update() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| drawGrid(); | |
| // 1. Move Scan Line | |
| scanAngle += CONFIG.SCAN_SPEED * direction; | |
| if (scanAngle >= 135) { | |
| scanAngle = 135; | |
| direction = -1; | |
| } else if (scanAngle <= 45) { | |
| scanAngle = 45; | |
| direction = 1; | |
| } | |
| // 2. Manage Targets | |
| // Chance to spawn NEW INVISIBLE target | |
| if (targets.length < CONFIG.MAX_TARGETS && Math.random() < CONFIG.TARGET_PROBABILITY) { | |
| targets.push(new Target()); | |
| } | |
| // Update existing targets | |
| for (let i = targets.length - 1; i >= 0; i--) { | |
| let t = targets[i]; | |
| t.update(scanAngle); // Check for collision or fade | |
| // Draw | |
| t.draw(ctx); | |
| // Cleanup dead targets (only if they were detected and faded out) | |
| // If we remove invisible targets they might never be found, so only remove | |
| // if they faded out, OR if we want to cycle invisible ones (optional complexity) | |
| if (t.isDetected && t.opacity <= 0) { | |
| targets.splice(i, 1); | |
| } | |
| } | |
| drawScanLine(); | |
| requestAnimationFrame(update); | |
| } | |
| // Kickoff | |
| update(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment