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);
}
Live Preview
Tags
energy, health, indicator, Status
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

guest

0 Comments
Oldest
Newest Most Voted