// Snap a cube-coordinate position to the nearest valid hex cell.
// Cube coordinates are handy for hex grids because x + y + z = 0.
vec3 cubeRound(vec3 cube) {
vec3 rounded = floor(cube + 0.5);
vec3 diff = abs(rounded - cube);
// Repair the axis with the largest rounding error
// so the cube-coordinate constraint still holds.
if (diff.x > diff.y && diff.x > diff.z) {
rounded.x = -rounded.y - rounded.z;
} else if (diff.y > diff.z) {
rounded.y = -rounded.x - rounded.z;
} else {
rounded.z = -rounded.x - rounded.y;
}
return rounded;
}
// Gold noise uses the golden ratio to generate pseudo-random variation.
// It tends to give a more consistent result across different hardware types,
// which makes it a nice fit for subtle per-cell color variation.
float gold_noise(vec2 uv, float seed) {
float phi = 1.61803398874989484820459;
return fract(tan(distance(uv * phi, uv) * seed) * uv.x);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
const float sqrt3 = 1.7320508;
// Border and cell palette.
vec3 borderPurple = vec3(0.176, 0.04, 1.0);
vec3 cyanA = vec3(0.14, 0.9, 0.96);
vec3 cyanB = vec3(0.44, 0.98, 1.0);
// Center the UV space and normalize it so the pattern does not stretch
// with aspect ratio changes.
vec2 p = (fragCoord - 0.5 * u_resolution.xy) / min(u_resolution.x, u_resolution.y);
// Slider-driven scale controls how large or dense the honeycomb appears.
p *= u_patternScale;
// Base hex radius in this local coordinate space.
float hexSize = 0.42;
// Convert from 2D screen space into pointy-top axial hex coordinates.
vec2 axial = vec2(
(sqrt3 / 3.0 * p.x - 1.0 / 3.0 * p.y) / hexSize,
(2.0 / 3.0 * p.y) / hexSize
);
// Promote axial coordinates to cube coordinates,
// then snap to the nearest hex cell.
vec3 cube = vec3(axial.x, -axial.x - axial.y, axial.y);
vec3 rounded = cubeRound(cube);
// Convert the snapped hex coordinate back to a 2D center point.
vec2 center = hexSize * vec2(
sqrt3 * (rounded.x + 0.5 * rounded.z),
1.5 * rounded.z
);
// Measure the pixel inside the chosen cell.
vec2 local = p - center;
vec2 q = abs(local);
// Signed-distance-style hex shape for a pointy-top hexagon.
float baseHex = max(q.y * 0.8660254 + q.x * 0.5, q.x);
// Anti-alias width scales with pattern density.
float aa = 1.4 * u_patternScale / min(u_resolution.x, u_resolution.y);
// Create a left-to-right pulse by shifting the phase using each cell center.
float pulse = 0.5 + 0.5 * sin(center.x * u_horizontalPhase - u_time * 1.6);
// The purple inner hex shrinks and expands, but never fully fills the border.
float maxRadius = hexSize * 0.76;
float minRadius = maxRadius - hexSize * u_shrinkAmount;
float innerRadius = mix(minRadius, maxRadius, pulse);
float innerHex = baseHex - innerRadius;
// Convert the signed distance into a soft mask for the inner cyan hex.
float cellInterior = 1.0 - smoothstep(-aa, aa, innerHex);
// Use gold noise on the snapped cell id so each hex gets a stable,
// slightly different cyan tint.
float cellNoise = gold_noise(vec2(rounded.x, rounded.z), 4.7);
vec3 hexColor = mix(cyanA, cyanB, cellNoise);
// Blend between the purple border and cyan cell interior.
vec3 color = mix(borderPurple, hexColor, cellInterior);
fragColor = vec4(color, 1.0);
}