Skip to content

Instantly share code, notes, and snippets.

@lord-carlos
Created November 18, 2025 12:19
Show Gist options
  • Select an option

  • Save lord-carlos/4c66b1d38779d9c93d531e2846abe4a9 to your computer and use it in GitHub Desktop.

Select an option

Save lord-carlos/4c66b1d38779d9c93d531e2846abe4a9 to your computer and use it in GitHub Desktop.
<!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