Merge pull request #138 from Utkarsh-Singhal-26/feat/grainient

[ FEAT ] : 🎉 New <Grainient /> Background
This commit is contained in:
David
2026-02-24 23:12:06 +02:00
committed by GitHub
11 changed files with 601 additions and 11 deletions
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -19,10 +19,10 @@
/>
<div class="hero-main-content">
<router-link to="/backgrounds/pixel-snow" class="hero-new-badge-container">
<span class="hero-new-badge">Christmas Special 🎁</span>
<router-link to="/backgrounds/grainient" class="hero-new-badge-container">
<span class="hero-new-badge">New 🎉</span>
<div class="hero-new-badge-text">
<span>Pixel Snow</span>
<span>Grainient</span>
<i class="pi-arrow-right pi" style="font-size: 0.8rem"></i>
</div>
</router-link>
@@ -83,7 +83,7 @@ const ResponsiveSplitText = defineComponent({
isMobile: { type: Boolean, required: true },
text: { type: String, required: true },
className: { type: String, default: '' },
splitType: { type: String as () => 'chars' | 'words' | 'lines' | 'words, chars', default: 'chars' },
splitType: { type: String as () => 'chars' | 'words' | 'lines', default: 'chars' },
delay: { type: Number, default: 100 },
duration: { type: Number, default: 0.6 },
ease: { type: String, default: 'power3.out' },
+4 -6
View File
@@ -1,15 +1,12 @@
// Highlighted sidebar items
export const NEW = [
'Antigravity',
'Color Bends',
'Floating Lines',
'Ghost Cursor',
'Grid Scan',
'Laser Flow',
'Light Pillar',
'Liquid Ether',
'Pixel Blast',
'Antigravity',
'Reflective Card',
'Pixel Snow',
'Grainient',
];
export const UPDATED = ['Metallic Paint'];
@@ -133,6 +130,7 @@ export const CATEGORIES = [
'Floating Lines',
'Galaxy',
'Gradient Blinds',
'Grainient',
'Grid Distortion',
'Grid Motion',
'Grid Scan',
+1
View File
@@ -127,6 +127,7 @@ const backgrounds = {
'light-pillar': () => import('../demo/Backgrounds/LightPillarDemo.vue'),
'pixel-snow': () => import('../demo/Backgrounds/PixelSnowDemo.vue'),
'grid-scan': () => import('../demo/Backgrounds/GridScanDemo.vue'),
'grainient': () => import('../demo/Backgrounds/GrainientDemo.vue'),
};
export const componentMap = {
+8
View File
@@ -765,6 +765,14 @@ export const componentMetadata: ComponentMetadata = {
docsUrl: 'https://vue-bits.dev/backgrounds/gradient-blinds',
tags: []
},
'Backgrounds/Grainient': {
videoUrl: '/assets/videos/grainient.webm',
description: 'Grainy gradient swirls with soft wave distortion.',
category: 'Backgrounds',
name: 'Grainient',
docsUrl: 'https://vue-bits.dev/backgrounds/grainient',
tags: []
},
'Backgrounds/GridDistortion': {
videoUrl: '/assets/videos/griddistortion.webm',
description: 'Warped grid mesh distorts smoothly reacting to cursor.',
@@ -0,0 +1,38 @@
import code from '@content/Backgrounds/Grainient/Grainient.vue?raw';
import { createCodeObject } from '../../../types/code';
export const grainient = createCodeObject(code, 'Backgrounds/Grainient', {
installation: `npm install ogl`,
usage: `<template>
<div style="width: 100%; height: 600px; position: relative;">
<Grainient
color1="#5227FF"
color2="#FF9FFC"
color3="#B19EEF"
:time-speed="0.25"
:color-balance="0.0"
:warp-strength="1.0"
:warp-frequency="5.0"
:warp-speed="2.0"
:warp-amplitude="50.0"
:blend-angle="0.0"
:blend-softness="0.05"
:rotation-amount="500.0"
:noise-scale="2.0"
:grain-amount="0.1"
:grain-scale="2.0"
:grain-animated="false"
:contrast="1.5"
:gamma="1.0"
:saturation="1.0"
:center-x="0.0"
:center-y="0.0"
:zoom="0.9"
/>
</div>
</template>
<script setup lang="ts">
import Grainient from "./Grainient.vue";
</script>`
});
@@ -0,0 +1,285 @@
<script setup lang="ts">
import { Mesh, Program, Renderer, Triangle } from 'ogl';
import { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue';
interface GrainientProps {
timeSpeed?: number;
colorBalance?: number;
warpStrength?: number;
warpFrequency?: number;
warpSpeed?: number;
warpAmplitude?: number;
blendAngle?: number;
blendSoftness?: number;
rotationAmount?: number;
noiseScale?: number;
grainAmount?: number;
grainScale?: number;
grainAnimated?: boolean;
contrast?: number;
gamma?: number;
saturation?: number;
centerX?: number;
centerY?: number;
zoom?: number;
color1?: string;
color2?: string;
color3?: string;
className?: string;
}
const hexToRgb = (hex: string): [number, number, number] => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) return [1, 1, 1];
return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];
};
const vertex = `#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragment = `#version 300 es
precision highp float;
uniform vec2 iResolution;
uniform float iTime;
uniform float uTimeSpeed;
uniform float uColorBalance;
uniform float uWarpStrength;
uniform float uWarpFrequency;
uniform float uWarpSpeed;
uniform float uWarpAmplitude;
uniform float uBlendAngle;
uniform float uBlendSoftness;
uniform float uRotationAmount;
uniform float uNoiseScale;
uniform float uGrainAmount;
uniform float uGrainScale;
uniform float uGrainAnimated;
uniform float uContrast;
uniform float uGamma;
uniform float uSaturation;
uniform vec2 uCenterOffset;
uniform float uZoom;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
out vec4 fragColor;
#define S(a,b,t) smoothstep(a,b,t)
mat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);}
vec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);}
float noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}
void mainImage(out vec4 o, vec2 C){
float t=iTime*uTimeSpeed;
vec2 uv=C/iResolution.xy;
float ratio=iResolution.x/iResolution.y;
vec2 tuv=uv-0.5+uCenterOffset;
tuv/=max(uZoom,0.001);
float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);
tuv.y*=1.0/ratio;
tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));
tuv.y*=ratio;
float frequency=uWarpFrequency;
float ws=max(uWarpStrength,0.001);
float amplitude=uWarpAmplitude/ws;
float warpTime=t*uWarpSpeed;
tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;
tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);
vec3 colLav=uColor1;
vec3 colOrg=uColor2;
vec3 colDark=uColor3;
float b=uColorBalance;
float s=max(uBlendSoftness,0.0);
mat2 blendRot=Rot(radians(uBlendAngle));
float blendX=(tuv*blendRot).x;
float edge0=-0.3-b-s;
float edge1=0.2-b+s;
float v0=0.5-b+s;
float v1=-0.3-b-s;
vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));
vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));
vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));
vec2 grainUv=uv*max(uGrainScale,0.001);
if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);}
float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);
col+=(grain-0.5)*uGrainAmount;
col=(col-0.5)*uContrast+0.5;
float luma=dot(col,vec3(0.2126,0.7152,0.0722));
col=mix(vec3(luma),col,uSaturation);
col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));
col=clamp(col,0.0,1.0);
o=vec4(col,1.0);
}
void main(){
vec4 o=vec4(0.0);
mainImage(o,gl_FragCoord.xy);
fragColor=o;
}
`;
const props = withDefaults(defineProps<GrainientProps>(), {
timeSpeed: 0.25,
colorBalance: 0.0,
warpStrength: 1.0,
warpFrequency: 5.0,
warpSpeed: 2.0,
warpAmplitude: 50.0,
blendAngle: 0.0,
blendSoftness: 0.05,
rotationAmount: 500.0,
noiseScale: 2.0,
grainAmount: 0.1,
grainScale: 2.0,
grainAnimated: false,
contrast: 1.5,
gamma: 1.0,
saturation: 1.0,
centerX: 0.0,
centerY: 0.0,
zoom: 0.9,
color1: '#FF9FFC',
color2: '#5227FF',
color3: '#B19EEF',
className: ''
});
const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
let cleanup: (() => void) | null = null;
const setup = () => {
if (!containerRef.value) return;
const renderer = new Renderer({
webgl: 2,
alpha: true,
antialias: false,
dpr: Math.min(window.devicePixelRatio || 1, 2)
});
const gl = renderer.gl;
const canvas = gl.canvas as HTMLCanvasElement;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
const container = containerRef.value;
container.appendChild(canvas);
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex,
fragment,
uniforms: {
iTime: { value: 0 },
iResolution: { value: new Float32Array([1, 1]) },
uTimeSpeed: { value: props.timeSpeed },
uColorBalance: { value: props.colorBalance },
uWarpStrength: { value: props.warpStrength },
uWarpFrequency: { value: props.warpFrequency },
uWarpSpeed: { value: props.warpSpeed },
uWarpAmplitude: { value: props.warpAmplitude },
uBlendAngle: { value: props.blendAngle },
uBlendSoftness: { value: props.blendSoftness },
uRotationAmount: { value: props.rotationAmount },
uNoiseScale: { value: props.noiseScale },
uGrainAmount: { value: props.grainAmount },
uGrainScale: { value: props.grainScale },
uGrainAnimated: { value: props.grainAnimated ? 1.0 : 0.0 },
uContrast: { value: props.contrast },
uGamma: { value: props.gamma },
uSaturation: { value: props.saturation },
uCenterOffset: { value: new Float32Array([props.centerX, props.centerY]) },
uZoom: { value: props.zoom },
uColor1: { value: new Float32Array(hexToRgb(props.color1)) },
uColor2: { value: new Float32Array(hexToRgb(props.color2)) },
uColor3: { value: new Float32Array(hexToRgb(props.color3)) }
}
});
const mesh = new Mesh(gl, { geometry, program });
const setSize = () => {
const rect = container.getBoundingClientRect();
const width = Math.max(1, Math.floor(rect.width));
const height = Math.max(1, Math.floor(rect.height));
renderer.setSize(width, height);
const res = (program.uniforms.iResolution as { value: Float32Array }).value;
res[0] = gl.drawingBufferWidth;
res[1] = gl.drawingBufferHeight;
};
const ro = new ResizeObserver(setSize);
ro.observe(container);
setSize();
let raf = 0;
const t0 = performance.now();
const loop = (t: number) => {
(program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;
renderer.render({ scene: mesh });
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
cleanup = () => {
cancelAnimationFrame(raf);
ro.disconnect();
try {
container.removeChild(canvas);
} catch {
// Ignore
}
};
};
onMounted(setup);
onBeforeUnmount(() => {
cleanup?.();
});
watch(
() => [
props.timeSpeed,
props.colorBalance,
props.warpStrength,
props.warpFrequency,
props.warpSpeed,
props.warpAmplitude,
props.blendAngle,
props.blendSoftness,
props.rotationAmount,
props.noiseScale,
props.grainAmount,
props.grainScale,
props.grainAnimated,
props.contrast,
props.gamma,
props.saturation,
props.centerX,
props.centerY,
props.zoom,
props.color1,
props.color2,
props.color3
],
() => {
cleanup?.();
setup();
},
{
deep: true
}
);
</script>
<template>
<div ref="containerRef" :class="['relative h-full w-full overflow-hidden', className]" />
</template>
+259
View File
@@ -0,0 +1,259 @@
<template>
<TabbedLayout>
<template #preview>
<div class="relative p-0 h-[600px] overflow-hidden demo-container">
<Grainient
:key="rerenderKey"
:color1="color1"
:color2="color2"
:color3="color3"
:time-speed="timeSpeed"
:color-balance="colorBalance"
:warp-strength="warpStrength"
:warp-frequency="warpFrequency"
:warp-speed="warpSpeed"
:warp-amplitude="warpAmplitude"
:blend-angle="blendAngle"
:blend-softness="blendSoftness"
:rotation-amount="rotationAmount"
:noise-scale="noiseScale"
:grain-amount="grainAmount"
:grain-scale="grainScale"
:grain-animated="grainAnimated"
:contrast="contrast"
:gamma="gamma"
:saturation="saturation"
:centerX="centerX"
:centerY="centerY"
:zoom="zoom"
/>
<BackgroundContent pill-text="New Background" headline="Grainy gradient colors with soft motion." />
</div>
<Customize>
<div class="gap-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mb-4">
<PreviewColor title="Color 1" v-model="color1" />
<PreviewColor title="Color 2" v-model="color2" />
<PreviewColor title="Color 3" v-model="color3" />
</div>
<div class="gap-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<PreviewSlider title="Time Speed" :min="0" :max="5" :step="0.05" v-model="timeSpeed" />
<PreviewSlider title="Color Balance" :min="-1" :max="1" :step="0.01" v-model="colorBalance" />
<PreviewSlider title="Warp Strength" :min="0" :max="4" :step="0.05" v-model="warpStrength" />
<PreviewSlider title="Warp Frequency" :min="0" :max="12" :step="0.1" v-model="warpFrequency" />
<PreviewSlider title="Warp Speed" :min="0" :max="6" :step="0.1" v-model="warpSpeed" />
<PreviewSlider title="Warp Amplitude" :min="5" :max="80" :step="1" v-model="warpAmplitude" />
<PreviewSlider title="Blend Angle" :min="-180" :max="180" :step="1" v-model="blendAngle" />
<PreviewSlider title="Blend Softness" :min="0" :max="1" :step="0.01" v-model="blendSoftness" />
<PreviewSlider title="Rotation Amount" :min="0" :max="1440" :step="10" v-model="rotationAmount" />
<PreviewSlider title="Noise Scale" :min="0" :max="4" :step="0.05" v-model="noiseScale" />
<PreviewSlider title="Grain Amount" :min="0" :max="0.4" :step="0.01" v-model="grainAmount" />
<PreviewSlider title="Grain Scale" :min="0.2" :max="8" :step="0.1" v-model="grainScale" />
<PreviewSwitch title="Grain Animated" v-model="grainAnimated" />
<PreviewSlider title="Contrast" :min="0" :max="2.5" :step="0.05" v-model="contrast" />
<PreviewSlider title="Gamma" :min="0.4" :max="2.5" :step="0.05" v-model="gamma" />
<PreviewSlider title="Saturation" :min="0" :max="2.5" :step="0.05" v-model="saturation" />
<PreviewSlider title="Center Offset X" :min="-1" :max="1" :step="0.01" v-model="centerX" />
<PreviewSlider title="Center Offset Y" :min="-1" :max="1" :step="0.01" v-model="centerY" />
<PreviewSlider title="Zoom" :min="0.3" :max="3" :step="0.05" v-model="zoom" />
</div>
</Customize>
<PropTable :data="propData" />
<Dependencies :dependency-list="['ogl']" />
</template>
<template #code>
<CodeExample :code-object="grainient" />
</template>
<template #cli>
<CliInstallation :command="grainient.cli" />
</template>
</TabbedLayout>
</template>
<script setup lang="ts">
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 PreviewColor from '@/components/common/PreviewColor.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 { useForceRerender } from '@/composables/useForceRerender';
import { grainient } from '@/constants/code/Backgrounds/grainientCode';
import Grainient from '@/content/Backgrounds/Grainient/Grainient.vue';
import { ref } from 'vue';
const { rerenderKey } = useForceRerender();
const color1 = ref('#5227FF');
const color2 = ref('#FF9FFC');
const color3 = ref('#B19EEF');
const timeSpeed = ref(0.25);
const colorBalance = ref(0.0);
const warpStrength = ref(1.0);
const warpFrequency = ref(5.0);
const warpSpeed = ref(2.0);
const warpAmplitude = ref(50.0);
const blendAngle = ref(0.0);
const blendSoftness = ref(0.05);
const rotationAmount = ref(500.0);
const noiseScale = ref(2.0);
const grainAmount = ref(0.1);
const grainScale = ref(2.0);
const grainAnimated = ref(false);
const contrast = ref(1.5);
const gamma = ref(1.0);
const saturation = ref(1.0);
const centerX = ref(0.0);
const centerY = ref(0.0);
const zoom = ref(0.9);
const propData = [
{
name: 'color1',
type: 'string',
default: "'#48FF28'",
description: 'Primary light color used in the gradient blend.'
},
{
name: 'color2',
type: 'string',
default: "'#A2FFC6'",
description: 'Secondary accent color used in the gradient blend.'
},
{
name: 'color3',
type: 'string',
default: "'#9EF19E'",
description: 'Deep base color used in the gradient blend.'
},
{
name: 'timeSpeed',
type: 'number',
default: '0.25',
description: 'Animation speed multiplier for the gradient motion.'
},
{
name: 'colorBalance',
type: 'number',
default: '0.0',
description: 'Shifts the palette balance toward dark or lighter tones.'
},
{
name: 'warpStrength',
type: 'number',
default: '1.0',
description: 'Strength of the wave warp distortion (0 = none).'
},
{
name: 'warpFrequency',
type: 'number',
default: '5.0',
description: 'Frequency of the wave warp.'
},
{
name: 'warpSpeed',
type: 'number',
default: '2.0',
description: 'Speed multiplier for the warp animation.'
},
{
name: 'warpAmplitude',
type: 'number',
default: '50.0',
description: 'Base amplitude for the warp distortion.'
},
{
name: 'blendAngle',
type: 'number',
default: '0.0',
description: 'Rotation angle for the color blend axis (degrees).'
},
{
name: 'blendSoftness',
type: 'number',
default: '0.05',
description: 'Softens the blend edges between color layers.'
},
{
name: 'rotationAmount',
type: 'number',
default: '500.0',
description: 'Rotation amount driven by noise.'
},
{
name: 'noiseScale',
type: 'number',
default: '2.0',
description: 'Scales the noise frequency that drives rotation.'
},
{
name: 'grainAmount',
type: 'number',
default: '0.1',
description: 'Amount of film grain applied to the gradient.'
},
{
name: 'grainScale',
type: 'number',
default: '2.0',
description: 'Scale of the grain pattern.'
},
{
name: 'grainAnimated',
type: 'boolean',
default: 'false',
description: 'Animate grain over time.'
},
{
name: 'contrast',
type: 'number',
default: '1.5',
description: 'Overall contrast applied to the final color.'
},
{
name: 'gamma',
type: 'number',
default: '1.0',
description: 'Gamma correction for the final color.'
},
{
name: 'saturation',
type: 'number',
default: '1.0',
description: 'Saturation amount for the final color.'
},
{
name: 'centerX',
type: 'number',
default: '0.0',
description: 'Horizontal offset of the gradient center.'
},
{
name: 'centerY',
type: 'number',
default: '0.0',
description: 'Vertical offset of the gradient center.'
},
{
name: 'zoom',
type: 'number',
default: '0.9',
description: 'Zoom level for the gradient field.'
},
{
name: 'className',
type: 'string',
default: "''",
description: 'Additional CSS classes applied to the container.'
}
];
</script>