Add Contour theme with crisp AA edges, glow, line width controls

main
Warinyourself 3 weeks ago
parent 5ab9be1053
commit e4c32f23e0

@ -0,0 +1,123 @@
#extension GL_OES_standard_derivatives : enable
#ifdef GL_ES
precision highp float;
#endif
uniform float uTime;
uniform vec2 uResolution;
// Zoom/scale of the noise pattern — higher values zoom in, revealing more detail
uniform float uScale;
// Number of stripes/contour lines across the noise field
uniform float uScaleContour;
// Line width: controls how much of the contour field is visible (0 = thin hairlines, 1 = wide bands)
uniform float uWidth;
// Glow brightness: overall intensity of the bloom halo
uniform float uGlow;
// Glow width: 0 = wide diffuse bloom, 1 = tight narrow halo (controls pow exponent)
uniform float uGlowWidth;
// Max line band: caps the upper visible threshold so all lines have similar spatial width
// smaller = more uniform thin lines, larger = allows wide bands
uniform float uMaxLine;
// First gradient color (RGB)
uniform vec3 uColor1;
// Second gradient color (RGB)
uniform vec3 uColor2;
const float M_PI = 3.14159265;
const int NUM_OCTAVES = 2;
// Hash functions — map domain to pseudo-random [0, 1)
float hash11(float t) {
return fract(sin(t * 56789.0) * 56789.0);
}
float hash21(vec2 uv) {
return hash11(hash11(uv[0]) + 2.0 * hash11(uv[1]));
}
// Returns a unit-length gradient vector for a given grid cell
vec2 hashGradient2(vec2 uv) {
float t = hash21(uv);
return vec2(cos(2.0 * M_PI * t), sin(2.0 * M_PI * t));
}
// Bilinear mix helper
float mix2(float f00, float f10, float f01, float f11, vec2 uv) {
return mix(mix(f00, f10, uv[0]), mix(f01, f11, uv[0]), uv[1]);
}
// Rotate a 2D vector by angle r (radians)
vec2 rotate2(vec2 uv, float r) {
mat2 R = mat2(cos(r), sin(r), -sin(r), cos(r));
return R * uv;
}
// Gradient (Perlin) noise with optional gradient rotation for animation
float gradientNoise(vec2 uv, float r) {
vec2 uvi = floor(uv);
vec2 uvf = uv - uvi;
vec2 g00 = rotate2(hashGradient2(uvi + vec2(0.0, 0.0)), r);
vec2 g10 = rotate2(hashGradient2(uvi + vec2(1.0, 0.0)), r);
vec2 g01 = rotate2(hashGradient2(uvi + vec2(0.0, 1.0)), r);
vec2 g11 = rotate2(hashGradient2(uvi + vec2(1.0, 1.0)), r);
float f00 = dot(g00, uvf - vec2(0.0, 0.0));
float f10 = dot(g10, uvf - vec2(1.0, 0.0));
float f01 = dot(g01, uvf - vec2(0.0, 1.0));
float f11 = dot(g11, uvf - vec2(1.0, 1.0));
float t = mix2(f00, f10, f01, f11, smoothstep(vec2(0.0), vec2(1.0), uvf));
// Normalize: theoretical bounds are +-1/sqrt(2) ≈ +-0.7
return (t / 0.7 + 1.0) * 0.5;
}
// Fractional Brownian Motion — stacks octaves for richer noise detail
float noise(vec2 uv, float r) {
float result = 0.0;
for (int i = 0; i < NUM_OCTAVES; i++) {
float p = pow(2.0, float(i));
result += gradientNoise(uv * p, r) / p;
}
// Normalize result back to [0, 1]
result /= (pow(2.0, float(NUM_OCTAVES)) - 1.0) / pow(2.0, float(NUM_OCTAVES - 1));
return result;
}
// Maps noise value to a periodic wave that creates the contour bands
float wave(float t) {
return 0.5 * (1.0 - cos(uScaleContour * M_PI * t));
}
void main() {
vec2 uv = uScale * gl_FragCoord.xy / uResolution.y;
// Animate by rotating noise gradients over time (0.628 ≈ 0.1 * 2π)
float r = 0.628 * uTime;
float noise_fac = noise(uv, r);
float contour_fac = wave(noise_fac);
vec3 color = mix(uColor1, uColor2, noise_fac);
float lo = 1.0 - uWidth;
float hi = lo + uMaxLine;
float fw = fwidth(contour_fac);
// Crisp anti-aliased line: only pixels in the [lo, hi] band are visible,
// bounding maximum spatial width so all lines appear roughly equal-sized
float line = smoothstep(lo - fw, lo + fw, contour_fac)
- smoothstep(hi - fw, hi + fw, contour_fac);
// Glow: tent-shaped halo centered on the band with controlled falloff on both sides
float glowExponent = mix(1.0, 16.0, uGlowWidth);
float glow_t = clamp((contour_fac - lo + 0.1) / (uMaxLine + 0.2), 0.0, 1.0);
float glow_bell = 1.0 - abs(glow_t * 2.0 - 1.0);
float glow = uGlow * pow(glow_bell, glowExponent);
gl_FragColor = vec4(color * max(line, glow), 1.0);
}

@ -0,0 +1,82 @@
import { defineComponent, ref, computed, onMounted, onBeforeUnmount } from 'vue'
import GL from '@/utils/gl'
import { useThemeStore } from '@/store/theme'
import { hexToRgb } from '@/utils/color'
import fragmentShader from './fragment.glsl'
import vertexShader from './vertex.glsl'
let render: GL
export default defineComponent({
name: 'ContourTheme',
setup() {
const themeStore = useThemeStore()
const canvasRef = ref<HTMLCanvasElement | null>(null)
const animationSpeed = computed(() => themeStore.getThemeInput('animation-speed')?.value as number || 5)
const pxratio = computed(() => themeStore.getThemeInput('pxratio')?.value as number || 0.8)
const scale = computed(() => themeStore.getThemeInput('scale')?.value as number || 3.0)
const contour = computed(() => themeStore.getThemeInput('contour')?.value as number || 32.0)
const width = computed(() => themeStore.getThemeInput('width')?.value as number || 0.15)
const glow = computed(() => themeStore.getThemeInput('glow')?.value as number || 0.15)
const glowWidth = computed(() => themeStore.getThemeInput('glow-width')?.value as number || 0.5)
const maxLine = computed(() => themeStore.getThemeInput('max-line')?.value as number || 0.3)
const color1 = computed(() => themeStore.getThemeInput('color-active')?.value as string || '#FF00FF')
const color2 = computed(() => themeStore.getThemeInput('color-second')?.value as string || '#00FFFF')
onMounted(() => {
render = new GL(
canvasRef.value!,
vertexShader,
fragmentShader,
window.innerWidth,
window.innerHeight,
{
extensions: ['OES_standard_derivatives'],
renderOptions: { externalTimeUse: true },
renderHook() {
const gl = this as unknown as GL
if (!gl.programInfo.uniforms.scale) {
gl.programInfo.uniforms.scale = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uScale')
gl.programInfo.uniforms.contour = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uScaleContour')
gl.programInfo.uniforms.width = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uWidth')
gl.programInfo.uniforms.glow = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uGlow')
gl.programInfo.uniforms.glowWidth = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uGlowWidth')
gl.programInfo.uniforms.maxLine = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uMaxLine')
gl.programInfo.uniforms.color1 = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uColor1')
gl.programInfo.uniforms.color2 = gl.ctx.getUniformLocation(gl.program as WebGLProgram, 'uColor2')
}
gl.pxratio = pxratio.value
gl.time += animationSpeed.value / 500
const c1 = hexToRgb(color1.value)
const c2 = hexToRgb(color2.value)
gl.ctx.uniform1f(gl.programInfo.uniforms.time, gl.time)
gl.ctx.uniform1f(gl.programInfo.uniforms.scale, scale.value)
gl.ctx.uniform1f(gl.programInfo.uniforms.contour, contour.value)
gl.ctx.uniform1f(gl.programInfo.uniforms.width, width.value)
gl.ctx.uniform1f(gl.programInfo.uniforms.glow, glow.value)
gl.ctx.uniform1f(gl.programInfo.uniforms.glowWidth, glowWidth.value)
gl.ctx.uniform1f(gl.programInfo.uniforms.maxLine, maxLine.value)
gl.ctx.uniform3f(gl.programInfo.uniforms.color1, c1[0] / 255, c1[1] / 255, c1[2] / 255)
gl.ctx.uniform3f(gl.programInfo.uniforms.color2, c2[0] / 255, c2[1] / 255, c2[2] / 255)
}
}
)
render.running = true
})
onBeforeUnmount(() => {
render.running = false
})
return () => (
<div>
<canvas ref={canvasRef} />
</div>
)
}
})

@ -0,0 +1,5 @@
attribute vec2 aVertexPosition;
void main() {
gl_Position = vec4(aVertexPosition, 0.0, 1.0);
}

@ -15,6 +15,7 @@ interface GlOption {
renderHook: () => void;
renderOptions?: GLRenderOptions;
windowListener?: WindowListenerCallback[];
extensions?: string[];
}
export default class GL {
@ -82,6 +83,8 @@ export default class GL {
this.ctx = ctx
options?.extensions?.forEach((ext) => ctx.getExtension(ext))
// Create the shaders
this.vertexShader = this.createShaderOfType(this.ctx, this.ctx.VERTEX_SHADER, vertexShaderSource) as WebGLShader
this.fragmentShader = this.createShaderOfType(this.ctx, this.ctx.FRAGMENT_SHADER, fragmentShaderSource) as WebGLShader

Loading…
Cancel
Save