Customizable Health/Energy bar [2D Canvas] (Not a progress bar)
This is an energy or health bar or whatever that doesn’t use the ‘progress bar’ slider concept. Instead, it reads more like testing a signal, and viewing it on a crt. Optional numbers, custom colors for everything…
I tried to make it adaptable to any project you want to throw at it.
CC0, free forever. Use it however you want. I hope that it can help you in your project.
Shader code
shader_type canvas_item;
render_mode blend_mix;
// ============================================================================
// OSCILLOSCOPE HEALTH / ENERGY BAR
//
// A bar drawn as if you were probing a signal on a CRT screen.
// Value reads as signal STABILITY, not just amplitude:
// - High value: clean harmonic waveform with rich overtones
// - Low value: jittery, noise-injected, monochrome chaos
//
// The waveform amplitude IS the bar. The gimmick is that
// "health or energy is the integrity of the signal you're listening to".
//
// Optional 7-segment-style numeric readout (0-100) with the same trace
// glow, scanline modulation, and low-value scintillation.
//
// Godot 4.7+ CanvasItem shader
// CC0 - Public Domain
// Free forever with no limitations...
// Hopefully customizable enough to fit any vibe you need.
// Just trying to give back to the indy dev community.
// - Jon Berg [The Recurrent Neural Nitwit]
// ============================================================================
// --- VALUE (only required input) ---
uniform float value : hint_range(0.0, 1.0) = 1.0;
// --- COLOR THEME ---
uniform vec3 healthy_color : source_color = vec3(0.3, 1.0, 0.6);
uniform vec3 warning_color : source_color = vec3(1.0, 0.7, 0.25);
uniform vec3 critical_color : source_color = vec3(1.0, 0.25, 0.1);
uniform vec3 background_color : source_color = vec3(0.0, 0.04, 0.02);
uniform vec3 grid_color : source_color = vec3(0.05, 0.2, 0.1);
uniform vec3 frame_color : source_color = vec3(0.2, 0.5, 0.3);
// --- WAVEFORM ---
uniform float base_frequency : hint_range(1.0, 20.0) = 2.5;
uniform float wave_speed : hint_range(0.0, 5.0) = 1.5;
uniform float max_amplitude : hint_range(0.05, 0.45) = 0.4;
// --- SIGNAL DEGRADATION ---
uniform float jitter_amount : hint_range(0.0, 0.2) = 0.04;
uniform float noise_amount : hint_range(0.0, 1.0) = 0.6;
uniform float fade_to_critical : hint_range(0.0, 0.5) = 0.3;
// --- TRACE LOOK ---
uniform float core_size : hint_range(0.0005, 0.01) = 0.002;
uniform float glow_radius : hint_range(0.001, 0.05) = 0.012;
uniform float glow_intensity : hint_range(0.0, 4.0) = 1.5;
// --- CRT OVERLAY ---
uniform float scanline_freq : hint_range(20.0, 500.0) = 100.0;
uniform float scanline_strength : hint_range(0.0, 0.4) = 0.12;
uniform vec2 grid_divisions = vec2(16.0, 8.0);
uniform float grid_thickness : hint_range(0.001, 0.02) = 0.005;
uniform float grid_strength : hint_range(0.0, 1.0) = 0.4;
// --- FRAME ---
uniform vec2 frame_inset = vec2(0.02, 0.05);
uniform float frame_corner_radius : hint_range(0.0, 0.1) = 0.02;
uniform float frame_thickness : hint_range(0.005, 0.03) = 0.012;
// --- NUMBER READOUT ---
uniform bool show_number = false;
uniform bool show_percent = true;
uniform float number_scale : hint_range(0.04, 0.40) = 0.15;
uniform vec2 number_offset = vec2(0.85, 0.85);
uniform vec3 number_color : source_color = vec3(0.6, 1.0, 0.7);
// ============================================================================
// HELPERS
// ============================================================================
float hash21(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float vnoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash21(i), hash21(i + vec2(1.0, 0.0)), f.x),
mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), f.x),
f.y);
}
float sd_rounded_box(vec2 p, vec2 b, float r) {
vec2 q = abs(p) - b + r;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
vec3 health_color(float v, vec3 crit, vec3 warn, vec3 heal, float fade) {
if (v > 1.0 - fade) {
return mix(warn, heal, smoothstep(1.0 - fade, 1.0, v));
} else if (v > fade) {
return mix(crit, warn, smoothstep(fade, 1.0 - fade, v));
}
return crit;
}
// Capsule SDF between endpoints a and b with radius w
float segment_sdf(vec2 p, vec2 a, vec2 b, float w) {
vec2 pa = p - a;
vec2 ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h) - w;
}
// 7-segment bitmask for digit d (0-9).
// Bit layout: 0=A(top), 1=B(top-rt), 2=C(bot-rt), 3=D(bot),
// 4=E(bot-lt), 5=F(top-lt), 6=G(mid).
int digit_mask(int d) {
if (d == 1) return 6; // 0x06
if (d == 2) return 91; // 0x5B
if (d == 3) return 79; // 0x4F
if (d == 4) return 102; // 0x66
if (d == 5) return 109; // 0x6D
if (d == 6) return 125; // 0x7D
if (d == 7) return 7; // 0x07
if (d == 8) return 127; // 0x7F
if (d == 9) return 111; // 0x6F
return 63; // 0 and default
}
// Signed distance to the nearest active segment of a digit centered at origin.
// Digit height = size, width = size * 0.5.
float digit_7seg_dist(vec2 p, int mask, float size) {
float w = size * 0.11;
float hw = size * 0.25;
float h = -size * 0.8;
float d = 1e6;
if ((mask & 1) != 0) d = min(d, segment_sdf(p, vec2(-hw, h), vec2( hw, h), w)); // A
if ((mask & 2) != 0) d = min(d, segment_sdf(p, vec2( hw, h), vec2( hw, 0.0), w)); // B
if ((mask & 4) != 0) d = min(d, segment_sdf(p, vec2( hw, 0.0), vec2( hw,-h), w)); // C
if ((mask & 8) != 0) d = min(d, segment_sdf(p, vec2(-hw,-h), vec2( hw,-h), w)); // D
if ((mask & 16) != 0) d = min(d, segment_sdf(p, vec2(-hw, 0.0), vec2(-hw,-h), w)); // E
if ((mask & 32) != 0) d = min(d, segment_sdf(p, vec2(-hw, h), vec2(-hw, 0.0), w)); // F
if ((mask & 64) != 0) d = min(d, segment_sdf(p, vec2(-hw, 0.0), vec2( hw, 0.0), w)); // G
return d;
}
// Signed distance to the % glyph: two circles + a diagonal slash, all in one frame.
float percent_dist(vec2 p, float size) {
float r = size * 0.16;
float w = size * 0.12;
float d1 = length(p - vec2( size * 0.30, size * 0.30)) - r;
float d2 = length(p - vec2(-size * 0.30, -size * 0.30)) - r;
vec2 pa = p - vec2(-size * 0.40, size * 0.40);
vec2 ba = vec2( size * 0.80, -size * 0.80);
float ht = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
float d3 = length(pa - ba * ht) - w;
return min(min(d1, d2), d3);
}
// ============================================================================
// MAIN
// ============================================================================
void fragment() {
vec2 uv = UV;
vec2 c = uv - 0.5;
// --- FRAME SDFs ---
vec2 outer_bo = vec2(0.5 - frame_inset.x, 0.5 - frame_inset.y);
vec2 inner_bo = outer_bo - frame_thickness;
float outer_sdf = sd_rounded_box(c, outer_bo, frame_corner_radius);
float inner_sdf = sd_rounded_box(c, inner_bo, frame_corner_radius);
if (outer_sdf > 0.0) {
COLOR = vec4(0.0);
discard;
}
// --- INNER COORDINATES ---
vec2 inner_uv = clamp((uv - frame_inset) / (1.0 - 2.0 * frame_inset), 0.0, 1.0);
float t = TIME * wave_speed;
float amp = max_amplitude * value;
// --- WAVEFORM (cleaner as value rises) ---
float wave_x = inner_uv.x * base_frequency;
float wave = 0.0;
wave += sin(wave_x - t) * 0.6;
wave += sin(wave_x * 2.0 + t * 0.7) * 0.3;
wave += sin(wave_x * 3.0 - t * 1.5) * value * 0.15;
wave += sin(wave_x * 5.0 - t * 2.0) * value * 0.07;
wave += sin(wave_x * 7.0 - t * 2.5) * value * 0.04;
wave /= (0.6 + 0.3 + 0.15 + 0.07 + 0.04);
wave *= amp;
float jitter = (vnoise(vec2(t * 5.0, 0.0)) - 0.5) * jitter_amount * (1.0 - value);
wave += jitter;
float n = vnoise(inner_uv * 30.0 + vec2(t * 2.0, 0.0)) - 0.5;
wave += n * noise_amount * (1.0 - value) * max_amplitude * 0.5;
float waveform_y = 0.5 + wave;
float dist_to_wave = abs(inner_uv.y - waveform_y);
// --- TRACE ---
float core = 1.0 - smoothstep(0.0, core_size, dist_to_wave);
float glow = exp(-dist_to_wave / glow_radius) * glow_intensity;
vec3 line_color = health_color(value, critical_color, warning_color,
healthy_color, fade_to_critical);
// --- CRT OVERLAY ---
float scanline = 1.0 - scanline_strength * (0.5 + 0.5 * sin(inner_uv.y * scanline_freq * 3.14159));
vec2 gd = abs(fract(inner_uv * grid_divisions) - 0.5);
float grid = (1.0 - smoothstep(0.0, grid_thickness, min(gd.x, gd.y))) * grid_strength;
// --- COMPOSITE INNER ---
vec3 col = background_color;
col += grid * grid_color * 0.5;
col *= mix(1.0, scanline, 0.7);
col += (core + glow) * line_color;
// --- FRAME RING ---
if (inner_sdf > 0.0) {
col = frame_color * mix(1.0, scanline, 0.3);
}
// --- NUMBER READOUT ---
// Lives inside the inner box only. Multiple character SDFs reduced to a
// single distance, then re-amplified through the same trace formula so
// the digits glow / scanline with the rest of the bar instead of
// looking like a clean label thrown in place.
if (show_number && inner_sdf <= 0.0) {
int v100 = clamp(int(round(value * 100.0)), 0, 100);
int d0 = v100 / 100;
int d1 = (v100 / 10) % 10;
int d2 = v100 % 10;
float cw = number_scale * 0.9;
vec2 c0 = number_offset - vec2(cw * 3.0, 0.0); // hundreds
vec2 c1 = number_offset - vec2(cw * 2.0, 0.0); // tens
vec2 c2 = number_offset - vec2(cw, 0.0); // ones
vec2 c3 = number_offset; // %
float nd = 1e6;
if (v100 >= 100) {
nd = min(nd, digit_7seg_dist(inner_uv - c0, digit_mask(d0), number_scale));
}
nd = min(nd, digit_7seg_dist(inner_uv - c1, digit_mask(d1), number_scale));
nd = min(nd, digit_7seg_dist(inner_uv - c2, digit_mask(d2), number_scale));
if (show_percent) {
nd = min(nd, percent_dist(inner_uv - c3, number_scale));
}
float stripe_w = core_size * 8.0; // wider than waveform for legibility
float num_core = 1.0 - smoothstep(0.0, stripe_w, abs(nd));
float num_glow = exp(-max(nd, 0.0) / glow_radius) * glow_intensity * 0.25;
float num_lit = num_core + num_glow;
float scint = 1.0 + 0.08 * (vnoise(vec2(inner_uv.x * 60.0, t * 4.0)) - 0.5) * (1.0 - value) * 4.0;
col += num_lit * number_color * scint * scanline;
}
COLOR = vec4(col, 1.0);
}

