Merge version_2 into main #2
47
src/app/demo.tsx
Normal file
47
src/app/demo.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Demo file for WebGLShader and LiquidButton components
|
||||
* This file demonstrates the import and usage of advanced UI components
|
||||
* Dependencies: three, @radix-ui/react-slot, class-variance-authority
|
||||
*/
|
||||
|
||||
// Import WebGLShader component
|
||||
// Note: Update the path based on actual component location
|
||||
// import WebGLShader from "@/components/webgl/WebGLShader";
|
||||
|
||||
// Import LiquidButton component
|
||||
// Note: Update the path based on actual component location
|
||||
// import LiquidButton from "@/components/button/LiquidButton";
|
||||
|
||||
// Example usage (commented out as components may not exist yet):
|
||||
/*
|
||||
export default function Demo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 p-8">
|
||||
<h1 className="text-3xl font-bold">Component Demo</h1>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-4">WebGLShader Component</h2>
|
||||
<WebGLShader />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-4">LiquidButton Component</h2>
|
||||
<LiquidButton>Click Me</LiquidButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
// Placeholder demo component
|
||||
export default function Demo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 p-8">
|
||||
<h1 className="text-3xl font-bold">Demo Component</h1>
|
||||
<p>WebGLShader and LiquidButton components are ready to be imported once they are created.</p>
|
||||
<p>Dependencies installed: three, @radix-ui/react-slot, class-variance-authority</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +1,41 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Halant } from "next/font/google";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Open_Sans } from "next/font/google";
|
||||
import { Lexend } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ServiceWrapper } from "@/components/ServiceWrapper";
|
||||
import Tag from "@/tag/Tag";
|
||||
import ServiceWrapper from "@/providers/serviceWrapper/ServiceWrapper";
|
||||
import Tag from "@/components/tag/Tag";
|
||||
|
||||
const halant = Halant({
|
||||
variable: "--font-halant", subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter", subsets: ["latin"],
|
||||
});
|
||||
|
||||
const openSans = Open_Sans({
|
||||
variable: "--font-open-sans", subsets: ["latin"],
|
||||
const lexend = Lexend({
|
||||
variable: "--font-lexend", subsets: ["latin"],
|
||||
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Premium Creative & AI Solutions | Blackroom Collective", description: "High-end video production, photography, web design with custom animations, and AI-powered automation for brands, events, and films.", keywords: "video production, photography, web design, AI automation, creative agency, custom animations, AI bots, digital solutions", metadataBase: new URL("https://blackroomcollective.com"),
|
||||
alternates: {
|
||||
canonical: "https://blackroomcollective.com"
|
||||
},
|
||||
openGraph: {
|
||||
title: "Premium Creative & AI Solutions | Blackroom Collective", description: "Elevate your brand with high-end video, photography, web design, and AI-powered automation.", siteName: "Blackroom Collective", type: "website", images: [
|
||||
{
|
||||
url: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AXdmfJndyWjHwLYE0hN6oDnn2n/a-luxurious-creative-studio-workspace-sh-1772975511907-b40ff6f4.png", alt: "Premium Creative Studio"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image", title: "Premium Creative & AI Solutions | Blackroom Collective", description: "High-end creative services for brands and events.", images: ["https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AXdmfJndyWjHwLYE0hN6oDnn2n/a-luxurious-creative-studio-workspace-sh-1772975511907-b40ff6f4.png"]
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true
|
||||
}
|
||||
};
|
||||
title: "Blackroom Collective - Premium Creative Solutions", description: "High-end video production, professional photography, custom web design, and AI-powered automation solutions for luxury brands and premium events."};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<ServiceWrapper>
|
||||
<body
|
||||
className={`${halant.variable} ${inter.variable} ${openSans.variable} antialiased`}
|
||||
>
|
||||
<body className={`${lexend.variable}`}>
|
||||
<ServiceWrapper>
|
||||
<Tag />
|
||||
{children}
|
||||
|
||||
</ServiceWrapper>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@@ -1421,7 +1403,6 @@ export default function RootLayout({
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</ServiceWrapper>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
154
src/components/ui/liquid-glass-button.tsx
Normal file
154
src/components/ui/liquid-glass-button.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
|
||||
export interface LiquidButtonProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const LiquidButton: React.FC<LiquidButtonProps> = ({
|
||||
text,
|
||||
onClick,
|
||||
className = '',
|
||||
disabled = false,
|
||||
children,
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={`relative px-6 py-3 font-medium transition-all duration-300 ${
|
||||
isHovered ? 'scale-105' : 'scale-100'
|
||||
} ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${className}`}
|
||||
>
|
||||
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 opacity-75 blur-md group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<div className="relative z-10 flex items-center justify-center gap-2">
|
||||
{children || text}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ButtonProps extends LiquidButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'outline';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
text,
|
||||
onClick,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
className = '',
|
||||
disabled = false,
|
||||
children,
|
||||
}) => {
|
||||
const baseClass =
|
||||
'relative font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
const sizeClasses = {
|
||||
sm: 'px-3 py-2 text-sm',
|
||||
md: 'px-6 py-3 text-base',
|
||||
lg: 'px-8 py-4 text-lg',
|
||||
};
|
||||
const variantClasses = {
|
||||
primary: 'bg-blue-600 text-white hover:bg-blue-700 disabled:bg-gray-400',
|
||||
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300 disabled:bg-gray-100',
|
||||
outline: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50 disabled:border-gray-400 disabled:text-gray-400',
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`${baseClass} ${sizeClasses[size]} ${variantClasses[variant]} ${className}`}
|
||||
>
|
||||
{children || text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export interface GlassFilterProps {
|
||||
children: React.ReactNode;
|
||||
intensity?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const GlassFilter: React.FC<GlassFilterProps> = ({
|
||||
children,
|
||||
intensity = 0.1,
|
||||
className = '',
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`backdrop-blur-md bg-white/10 rounded-xl border border-white/20 ${className}`}
|
||||
style={{ backdropFilter: `blur(${intensity * 20}px)` }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface MetalButtonProps extends ButtonProps {
|
||||
shine?: boolean;
|
||||
}
|
||||
|
||||
export const MetalButton: React.FC<MetalButtonProps> = ({
|
||||
text,
|
||||
onClick,
|
||||
className = '',
|
||||
disabled = false,
|
||||
shine = true,
|
||||
children,
|
||||
}) => {
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!buttonRef.current) return;
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setMousePosition({
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
onMouseMove={handleMouseMove}
|
||||
className={`relative px-6 py-3 font-medium rounded-lg transition-all duration-200 overflow-hidden
|
||||
bg-gradient-to-b from-gray-100 to-gray-300 text-gray-900 hover:from-gray-50 hover:to-gray-200
|
||||
border border-gray-400 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${className}`}
|
||||
>
|
||||
{shine && (
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
background: `radial-gradient(circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(255,255,255,0.3), transparent 80%)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-10 flex items-center justify-center gap-2">
|
||||
{children || text}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
LiquidButton,
|
||||
Button,
|
||||
GlassFilter,
|
||||
MetalButton,
|
||||
};
|
||||
148
src/components/ui/web-gl-shader.tsx
Normal file
148
src/components/ui/web-gl-shader.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
export interface WebGLShaderProps {
|
||||
fragmentShader: string;
|
||||
vertexShader?: string;
|
||||
uniforms?: Record<string, { type: string; value: any }>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultVertexShader = `
|
||||
attribute vec4 position;
|
||||
void main() {
|
||||
gl_Position = position;
|
||||
}
|
||||
`;
|
||||
|
||||
export const WebGLShader: React.FC<WebGLShaderProps> = ({
|
||||
fragmentShader,
|
||||
vertexShader = defaultVertexShader,
|
||||
uniforms = {},
|
||||
className = '',
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const glRef = useRef<WebGLRenderingContext | null>(null);
|
||||
const programRef = useRef<WebGLProgram | null>(null);
|
||||
const uniformLocationsRef = useRef<Record<string, WebGLUniformLocation>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||||
if (!gl) {
|
||||
console.error('WebGL not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
glRef.current = gl as WebGLRenderingContext;
|
||||
|
||||
// Compile shaders
|
||||
const compileShader = (source: string, type: number): WebGLShader | null => {
|
||||
const shader = gl.createShader(type);
|
||||
if (!shader) return null;
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
console.error('Shader compilation error:', gl.getShaderInfoLog(shader));
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
return shader;
|
||||
};
|
||||
|
||||
const vShader = compileShader(vertexShader, gl.VERTEX_SHADER);
|
||||
const fShader = compileShader(fragmentShader, gl.FRAGMENT_SHADER);
|
||||
|
||||
if (!vShader || !fShader) return;
|
||||
|
||||
// Link program
|
||||
const program = gl.createProgram();
|
||||
if (!program) return;
|
||||
gl.attachShader(program, vShader);
|
||||
gl.attachShader(program, fShader);
|
||||
gl.linkProgram(program);
|
||||
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
console.error('Program link error:', gl.getProgramInfoLog(program));
|
||||
gl.deleteProgram(program);
|
||||
return;
|
||||
}
|
||||
|
||||
programRef.current = program;
|
||||
gl.useProgram(program);
|
||||
|
||||
// Store uniform locations
|
||||
const locations: Record<string, WebGLUniformLocation> = {};
|
||||
Object.keys(uniforms).forEach((name) => {
|
||||
const location = gl.getUniformLocation(program, name);
|
||||
if (location !== null) {
|
||||
locations[name] = location;
|
||||
}
|
||||
});
|
||||
uniformLocationsRef.current = locations;
|
||||
|
||||
// Set canvas size
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
// Set up quad vertices
|
||||
const vertices = new Float32Array([
|
||||
-1, -1, 1, -1, -1, 1, 1, 1,
|
||||
]);
|
||||
|
||||
const vertexBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
||||
|
||||
const positionLocation = gl.getAttribLocation(program, 'position');
|
||||
gl.enableVertexAttribArray(positionLocation);
|
||||
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Render loop
|
||||
const render = () => {
|
||||
// Update uniforms
|
||||
Object.entries(uniforms).forEach(([name, { type, value }]) => {
|
||||
const location = uniformLocationsRef.current[name];
|
||||
if (!location) return;
|
||||
|
||||
if (type === '1f') gl.uniform1f(location, value);
|
||||
else if (type === '2f') gl.uniform2f(location, value[0], value[1]);
|
||||
else if (type === '3f') gl.uniform3f(location, value[0], value[1], value[2]);
|
||||
else if (type === '4f') gl.uniform4f(location, value[0], value[1], value[2], value[3]);
|
||||
else if (type === '1i') gl.uniform1i(location, value);
|
||||
else if (type === 'matrix4fv') gl.uniformMatrix4fv(location, false, value);
|
||||
});
|
||||
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeCanvas);
|
||||
gl.deleteProgram(program);
|
||||
gl.deleteShader(vShader);
|
||||
gl.deleteShader(fShader);
|
||||
};
|
||||
}, [fragmentShader, vertexShader, uniforms]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={`block w-full h-full ${className}`}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebGLShader;
|
||||
Reference in New Issue
Block a user