Add Contour theme with crisp AA edges, glow, line width controls
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);
|
||||
}
|
||||
Loading…
Reference in New Issue