Add Dither background component

This commit is contained in:
onmax
2025-07-19 12:22:28 +02:00
parent 8cc0e53874
commit 8cf5425515
5 changed files with 569 additions and 0 deletions

View File

@@ -80,6 +80,7 @@ export const CATEGORIES = [
subcategories: [
'Aurora',
'Beams',
'Dither',
'Dot Grid',
'Hyperspeed',
'Ripple Grid',

View File

@@ -75,6 +75,7 @@ const backgrounds = {
'threads': () => import('../demo/Backgrounds/ThreadsDemo.vue'),
'aurora': () => import('../demo/Backgrounds/AuroraDemo.vue'),
'beams': () => import('../demo/Backgrounds/BeamsDemo.vue'),
'dither': () => import('../demo/Backgrounds/DitherDemo.vue'),
'grid-motion': () => import('../demo/Backgrounds/GridMotionDemo.vue'),
'hyperspeed': () => import('../demo/Backgrounds/HyperspeedDemo.vue'),
'balatro': () => import('../demo/Backgrounds/BalatroDemo.vue'),

View File

@@ -0,0 +1,27 @@
import code from '@content/Backgrounds/Dither/Dither.vue?raw';
import type { CodeObject } from '../../../types/code';
export const dither: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Dither`,
installation: `npm install ogl`,
usage: `<template>
<div class="relative w-full h-[500px] overflow-hidden">
<Dither
:wave-speed="0.05"
:wave-frequency="3"
:wave-amplitude="0.3"
:wave-color="[0.5, 0.5, 0.5]"
:color-num="4"
:pixel-size="2"
:disable-animation="false"
:enable-mouse-interaction="true"
:mouse-radius="1"
/>
</div>
</template>
<script setup lang="ts">
import Dither from "./Dither.vue";
</script>`,
code
};

View File

@@ -0,0 +1,390 @@
<template>
<div ref="containerRef" class="w-full h-full absolute top-0 left-0" />
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, watch, useTemplateRef } from 'vue';
import { Renderer, Program, Mesh, Triangle, Color } from 'ogl';
import type { OGLRenderingContext } from 'ogl';
interface DitherProps {
waveSpeed?: number;
waveFrequency?: number;
waveAmplitude?: number;
waveColor?: [number, number, number];
colorNum?: number;
pixelSize?: number;
disableAnimation?: boolean;
enableMouseInteraction?: boolean;
mouseRadius?: number;
}
const props = withDefaults(defineProps<DitherProps>(), {
waveSpeed: 0.05,
waveFrequency: 3,
waveAmplitude: 0.3,
waveColor: () => [0.5, 0.5, 0.5] as [number, number, number],
colorNum: 4,
pixelSize: 2,
disableAnimation: false,
enableMouseInteraction: true,
mouseRadius: 1
});
const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
let renderer: Renderer | null = null;
let gl: OGLRenderingContext | null = null;
let program: Program | null = null;
let mesh: Mesh | null = null;
let animationId: number | null = null;
let currentMouse = [0, 0];
let targetMouse = [0, 0];
const vertexShader = `
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragmentShader = `
precision highp float;
uniform float time;
uniform vec2 resolution;
uniform float waveSpeed;
uniform float waveFrequency;
uniform float waveAmplitude;
uniform vec3 waveColor;
uniform vec2 mousePos;
uniform int enableMouseInteraction;
uniform float mouseRadius;
uniform float colorNum;
uniform float pixelSize;
varying vec2 vUv;
vec4 mod289(vec4 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
vec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }
float cnoise(vec2 P) {
vec4 Pi = floor(P.xyxy) + vec4(0.0,0.0,1.0,1.0);
vec4 Pf = fract(P.xyxy) - vec4(0.0,0.0,1.0,1.0);
Pi = mod289(Pi);
vec4 ix = Pi.xzxz;
vec4 iy = Pi.yyww;
vec4 fx = Pf.xzxz;
vec4 fy = Pf.yyww;
vec4 i = permute(permute(ix) + iy);
vec4 gx = fract(i * (1.0/41.0)) * 2.0 - 1.0;
vec4 gy = abs(gx) - 0.5;
vec4 tx = floor(gx + 0.5);
gx = gx - tx;
vec2 g00 = vec2(gx.x, gy.x);
vec2 g10 = vec2(gx.y, gy.y);
vec2 g01 = vec2(gx.z, gy.z);
vec2 g11 = vec2(gx.w, gy.w);
vec4 norm = taylorInvSqrt(vec4(dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11)));
g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;
float n00 = dot(g00, vec2(fx.x, fy.x));
float n10 = dot(g10, vec2(fx.y, fy.y));
float n01 = dot(g01, vec2(fx.z, fy.z));
float n11 = dot(g11, vec2(fx.w, fy.w));
vec2 fade_xy = fade(Pf.xy);
vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);
}
const int OCTAVES = 8;
float fbm(vec2 p) {
float value = 0.0;
float amp = 1.0;
float freq = waveFrequency;
for (int i = 0; i < OCTAVES; i++) {
value += amp * abs(cnoise(p));
p *= freq;
amp *= waveAmplitude;
}
return value;
}
float pattern(vec2 p) {
vec2 p2 = p - time * waveSpeed;
return fbm(p - fbm(p + fbm(p2)));
}
float getBayerValue(int x, int y) {
if (y == 0) {
if (x == 0) return 0.0/64.0;
if (x == 1) return 48.0/64.0;
if (x == 2) return 12.0/64.0;
if (x == 3) return 60.0/64.0;
if (x == 4) return 3.0/64.0;
if (x == 5) return 51.0/64.0;
if (x == 6) return 15.0/64.0;
if (x == 7) return 63.0/64.0;
} else if (y == 1) {
if (x == 0) return 32.0/64.0;
if (x == 1) return 16.0/64.0;
if (x == 2) return 44.0/64.0;
if (x == 3) return 28.0/64.0;
if (x == 4) return 35.0/64.0;
if (x == 5) return 19.0/64.0;
if (x == 6) return 47.0/64.0;
if (x == 7) return 31.0/64.0;
} else if (y == 2) {
if (x == 0) return 8.0/64.0;
if (x == 1) return 56.0/64.0;
if (x == 2) return 4.0/64.0;
if (x == 3) return 52.0/64.0;
if (x == 4) return 11.0/64.0;
if (x == 5) return 59.0/64.0;
if (x == 6) return 7.0/64.0;
if (x == 7) return 55.0/64.0;
} else if (y == 3) {
if (x == 0) return 40.0/64.0;
if (x == 1) return 24.0/64.0;
if (x == 2) return 36.0/64.0;
if (x == 3) return 20.0/64.0;
if (x == 4) return 43.0/64.0;
if (x == 5) return 27.0/64.0;
if (x == 6) return 39.0/64.0;
if (x == 7) return 23.0/64.0;
} else if (y == 4) {
if (x == 0) return 2.0/64.0;
if (x == 1) return 50.0/64.0;
if (x == 2) return 14.0/64.0;
if (x == 3) return 62.0/64.0;
if (x == 4) return 1.0/64.0;
if (x == 5) return 49.0/64.0;
if (x == 6) return 13.0/64.0;
if (x == 7) return 61.0/64.0;
} else if (y == 5) {
if (x == 0) return 34.0/64.0;
if (x == 1) return 18.0/64.0;
if (x == 2) return 46.0/64.0;
if (x == 3) return 30.0/64.0;
if (x == 4) return 33.0/64.0;
if (x == 5) return 17.0/64.0;
if (x == 6) return 45.0/64.0;
if (x == 7) return 29.0/64.0;
} else if (y == 6) {
if (x == 0) return 10.0/64.0;
if (x == 1) return 58.0/64.0;
if (x == 2) return 6.0/64.0;
if (x == 3) return 54.0/64.0;
if (x == 4) return 9.0/64.0;
if (x == 5) return 57.0/64.0;
if (x == 6) return 5.0/64.0;
if (x == 7) return 53.0/64.0;
} else if (y == 7) {
if (x == 0) return 42.0/64.0;
if (x == 1) return 26.0/64.0;
if (x == 2) return 38.0/64.0;
if (x == 3) return 22.0/64.0;
if (x == 4) return 41.0/64.0;
if (x == 5) return 25.0/64.0;
if (x == 6) return 37.0/64.0;
if (x == 7) return 21.0/64.0;
}
return 0.0;
}
vec3 dither(vec2 uv, vec3 color) {
vec2 scaledCoord = floor(uv * resolution / pixelSize);
int x = int(mod(scaledCoord.x, 8.0));
int y = int(mod(scaledCoord.y, 8.0));
float threshold = getBayerValue(x, y) - 0.25;
float step = 1.0 / (colorNum - 1.0);
color += threshold * step;
float bias = 0.2;
color = clamp(color - bias, 0.0, 1.0);
return floor(color * (colorNum - 1.0) + 0.5) / (colorNum - 1.0);
}
void main() {
vec2 uv = gl_FragCoord.xy / resolution.xy;
vec2 centeredUv = uv - 0.5;
centeredUv.x *= resolution.x / resolution.y;
float f = pattern(centeredUv);
if (enableMouseInteraction == 1) {
vec2 mouseNDC = (mousePos / resolution - 0.5) * vec2(1.0, -1.0);
mouseNDC.x *= resolution.x / resolution.y;
float dist = length(centeredUv - mouseNDC);
float effect = 1.0 - smoothstep(0.0, mouseRadius, dist);
f -= 0.5 * effect;
}
vec3 col = mix(vec3(0.0), waveColor, f);
col = dither(uv, col);
gl_FragColor = vec4(col, 1.0);
}
`;
const resize = () => {
if (!containerRef.value || !renderer || !program) return;
const container = containerRef.value;
const { clientWidth, clientHeight } = container;
renderer.setSize(clientWidth, clientHeight);
program.uniforms.resolution.value[0] = clientWidth;
program.uniforms.resolution.value[1] = clientHeight;
};
const handleMouseMove = (e: MouseEvent) => {
if (!containerRef.value || !gl) return;
const rect = containerRef.value.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const x = (e.clientX - rect.left) * dpr;
const y = (e.clientY - rect.top) * dpr;
targetMouse = [x, y];
};
const handleMouseLeave = () => {
if (!gl) return;
const dpr = window.devicePixelRatio || 1;
targetMouse = [gl.canvas.width / (2 * dpr), gl.canvas.height / (2 * dpr)];
};
const update = (t: number) => {
if (!program || !renderer || !mesh) return;
if (props.enableMouseInteraction) {
const smoothing = 0.05;
currentMouse[0] += smoothing * (targetMouse[0] - currentMouse[0]);
currentMouse[1] += smoothing * (targetMouse[1] - currentMouse[1]);
program.uniforms.mousePos.value[0] = currentMouse[0];
program.uniforms.mousePos.value[1] = currentMouse[1];
} else {
if (gl) {
const dpr = window.devicePixelRatio || 1;
program.uniforms.mousePos.value[0] = gl.canvas.width / (2 * dpr);
program.uniforms.mousePos.value[1] = gl.canvas.height / (2 * dpr);
}
}
if (!props.disableAnimation) {
program.uniforms.time.value = t * 0.001;
}
program.uniforms.waveSpeed.value = props.waveSpeed;
program.uniforms.waveFrequency.value = props.waveFrequency;
program.uniforms.waveAmplitude.value = props.waveAmplitude;
program.uniforms.waveColor.value.r = props.waveColor[0];
program.uniforms.waveColor.value.g = props.waveColor[1];
program.uniforms.waveColor.value.b = props.waveColor[2];
program.uniforms.enableMouseInteraction.value = props.enableMouseInteraction ? 1 : 0;
program.uniforms.mouseRadius.value = props.mouseRadius;
program.uniforms.colorNum.value = props.colorNum;
program.uniforms.pixelSize.value = props.pixelSize;
renderer.render({ scene: mesh });
animationId = requestAnimationFrame(update);
};
const initializeScene = () => {
if (!containerRef.value) return;
cleanup();
const container = containerRef.value;
renderer = new Renderer({ alpha: true });
gl = renderer.gl;
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
const geometry = new Triangle(gl);
program = new Program(gl, {
vertex: vertexShader,
fragment: fragmentShader,
uniforms: {
time: { value: 0 },
resolution: { value: new Float32Array([gl.canvas.width, gl.canvas.height]) },
waveSpeed: { value: props.waveSpeed },
waveFrequency: { value: props.waveFrequency },
waveAmplitude: { value: props.waveAmplitude },
waveColor: { value: new Color(...props.waveColor) },
mousePos: { value: new Float32Array([gl.canvas.width / 2, gl.canvas.height / 2]) },
enableMouseInteraction: { value: props.enableMouseInteraction ? 1 : 0 },
mouseRadius: { value: props.mouseRadius },
colorNum: { value: props.colorNum },
pixelSize: { value: props.pixelSize }
}
});
mesh = new Mesh(gl, { geometry, program });
const canvas = gl.canvas as HTMLCanvasElement;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
container.appendChild(canvas);
window.addEventListener('resize', resize);
if (props.enableMouseInteraction) {
container.addEventListener('mousemove', handleMouseMove);
container.addEventListener('mouseleave', handleMouseLeave);
}
resize();
animationId = requestAnimationFrame(update);
};
const cleanup = () => {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
window.removeEventListener('resize', resize);
if (containerRef.value) {
containerRef.value.removeEventListener('mousemove', handleMouseMove);
containerRef.value.removeEventListener('mouseleave', handleMouseLeave);
const canvas = containerRef.value.querySelector('canvas');
if (canvas) {
containerRef.value.removeChild(canvas);
}
}
if (gl) {
gl.getExtension('WEBGL_lose_context')?.loseContext();
}
renderer = null;
gl = null;
program = null;
mesh = null;
currentMouse = [0, 0];
targetMouse = [0, 0];
};
onMounted(() => {
initializeScene();
});
onUnmounted(() => {
cleanup();
});
watch(
() => props,
() => {
initializeScene();
},
{ deep: true }
);
</script>

View File

@@ -0,0 +1,150 @@
<template>
<TabbedLayout>
<template #preview>
<div class="relative h-[600px] overflow-hidden demo-container">
<Dither
:key="rerenderKey"
:wave-speed="waveSpeed"
:wave-frequency="waveFrequency"
:wave-amplitude="waveAmplitude"
:wave-color="waveColor"
:color-num="colorNum"
:pixel-size="pixelSize"
:disable-animation="disableAnimation"
:enable-mouse-interaction="enableMouseInteraction"
:mouse-radius="mouseRadius"
/>
<BackgroundContent pillText="Retro Background" headline="Dithered waves with vintage charm" />
</div>
<Customize>
<PreviewSwitch title="Mouse Interaction" v-model="enableMouseInteraction" />
<PreviewSwitch title="Disable Animation" v-model="disableAnimation" />
<PreviewSlider title="Wave Speed" :min="0.01" :max="0.2" :step="0.01" v-model="waveSpeed" />
<PreviewSlider title="Wave Frequency" :min="1" :max="8" :step="0.5" v-model="waveFrequency" />
<PreviewSlider title="Wave Amplitude" :min="0.1" :max="0.8" :step="0.1" v-model="waveAmplitude" />
<PreviewSlider title="Color Count" :min="2" :max="16" :step="1" v-model="colorNum" />
<PreviewSlider title="Pixel Size" :min="1" :max="8" :step="1" v-model="pixelSize" />
<PreviewSlider title="Mouse Radius" :min="0.1" :max="2" :step="0.1" v-model="mouseRadius" />
<PreviewSlider title="Wave Color R" v-model="waveColor[0]" :min="0" :max="1" :step="0.1" />
<PreviewSlider title="Wave Color G" v-model="waveColor[1]" :min="0" :max="1" :step="0.1" />
<PreviewSlider title="Wave Color B" v-model="waveColor[2]" :min="0" :max="1" :step="0.1" />
</Customize>
<PropTable :data="propData" />
<Dependencies :dependency-list="['ogl']" />
</template>
<template #code>
<CodeExample :code-object="dither" />
</template>
<template #cli>
<CliInstallation :command="dither.cli" />
</template>
</TabbedLayout>
</template>
<script setup lang="ts">
import { useForceRerender } from '@/composables/useForceRerender';
import { ref, watch } from 'vue';
import CliInstallation from '../../components/code/CliInstallation.vue';
import CodeExample from '../../components/code/CodeExample.vue';
import Dependencies from '../../components/code/Dependencies.vue';
import BackgroundContent from '../../components/common/BackgroundContent.vue';
import Customize from '../../components/common/Customize.vue';
import PreviewSlider from '../../components/common/PreviewSlider.vue';
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
import PropTable from '../../components/common/PropTable.vue';
import TabbedLayout from '../../components/common/TabbedLayout.vue';
import { dither } from '@/constants/code/Backgrounds/ditherCode';
import Dither from '../../content/Backgrounds/Dither/Dither.vue';
const { rerenderKey, forceRerender } = useForceRerender();
const waveSpeed = ref(0.05);
const waveFrequency = ref(3);
const waveAmplitude = ref(0.3);
const waveColor = ref<[number, number, number]>([0.5, 0.5, 0.5]);
const colorNum = ref(4);
const pixelSize = ref(2);
const disableAnimation = ref(false);
const enableMouseInteraction = ref(true);
const mouseRadius = ref(1);
watch(
[waveSpeed, waveFrequency, waveAmplitude, waveColor, colorNum, pixelSize, disableAnimation, enableMouseInteraction, mouseRadius],
() => {
forceRerender();
},
{ deep: true }
);
const propData = [
{
name: 'waveSpeed',
type: 'number',
default: '0.05',
description: 'Controls the speed of the wave animation.'
},
{
name: 'waveFrequency',
type: 'number',
default: '3',
description: 'Sets the frequency of the wave pattern.'
},
{
name: 'waveAmplitude',
type: 'number',
default: '0.3',
description: 'Controls the amplitude of the wave pattern.'
},
{
name: 'waveColor',
type: 'array',
default: '[0.5, 0.5, 0.5]',
description: 'RGB color values for the wave pattern (0-1 range).'
},
{
name: 'colorNum',
type: 'number',
default: '4',
description: 'Number of colors in the dithering palette.'
},
{
name: 'pixelSize',
type: 'number',
default: '2',
description: 'Size of the dithering pixels for the retro effect.'
},
{
name: 'disableAnimation',
type: 'boolean',
default: 'false',
description: 'Disables the wave animation when set to true.'
},
{
name: 'enableMouseInteraction',
type: 'boolean',
default: 'true',
description: 'Enables mouse interaction with the wave pattern.'
},
{
name: 'mouseRadius',
type: 'number',
default: '1',
description: 'Radius of the mouse interaction effect.'
}
];
</script>