diff --git a/website/package-lock.json b/website/package-lock.json index c05b7955..91a346ef 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -14,9 +14,11 @@ "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "dotenv": "^17.2.3", + "gsap": "^3.13.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "three": "^0.181.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.9.2", @@ -9733,6 +9735,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/gsap": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.13.0.tgz", + "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -17959,6 +17967,12 @@ "tslib": "^2" } }, + "node_modules/three": { + "version": "0.181.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.181.2.tgz", + "integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==", + "license": "MIT" + }, "node_modules/throttleit": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", diff --git a/website/package.json b/website/package.json index 149485ca..cbe7a51b 100644 --- a/website/package.json +++ b/website/package.json @@ -20,9 +20,11 @@ "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "dotenv": "^17.2.3", + "gsap": "^3.13.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "three": "^0.181.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.9.2", diff --git a/website/src/components/DiamondScene/FacetBadge.js b/website/src/components/DiamondScene/FacetBadge.js new file mode 100644 index 00000000..1e36fb2b --- /dev/null +++ b/website/src/components/DiamondScene/FacetBadge.js @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './facetBadge.module.css'; + +export function FacetBadge({ name, visible }) { + return ( +
+
Active Facet
+
{name || '...'}
+
+
+ ); +} + diff --git a/website/src/components/DiamondScene/facetBadge.module.css b/website/src/components/DiamondScene/facetBadge.module.css new file mode 100644 index 00000000..daaee9cb --- /dev/null +++ b/website/src/components/DiamondScene/facetBadge.module.css @@ -0,0 +1,51 @@ +.badgeContainer { + position: absolute; + top: 70%; /* Position below the diamond roughly */ + right: 15%; /* Align to the right side where diamond floats on desktop */ + transform: translateY(20px); + opacity: 0; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; + z-index: 10; + text-align: center; + min-width: 180px; +} + +.visible { + opacity: 1; + transform: translateY(0); +} + +.badgeLabel { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--ifm-color-primary-light); + margin-bottom: 0.25rem; + font-weight: 600; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); +} + +.badgeValue { + font-size: 1.25rem; + font-weight: 700; + color: #fff; + text-shadow: 0 0 20px rgba(59, 130, 246, 0.6); + background: linear-gradient(to right, #fff, #bfdbfe); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.badgeLine { + width: 40px; + height: 2px; + background: linear-gradient(90deg, transparent, var(--ifm-color-primary), transparent); + margin: 0.5rem auto 0; +} + +/* Mobile Adjustment */ +@media (max-width: 1024px) { + .badgeContainer { + display: none; + } +} diff --git a/website/src/components/DiamondScene/geometry.js b/website/src/components/DiamondScene/geometry.js new file mode 100644 index 00000000..0fed73df --- /dev/null +++ b/website/src/components/DiamondScene/geometry.js @@ -0,0 +1,176 @@ +import * as THREE from 'three'; + +// GEOMETRY: HIGH-FIDELITY ROUND BRILLIANT CUT +// Constructed procedurally to ensure perfect symmetry and sharp facet edges +export function createDiamondGeometry(radius = 1.5) { + const geometry = new THREE.BufferGeometry(); + + const rTable = radius * 0.54; + const rGirdle = radius; + const rMidCrown = radius * 0.82; + const rMidPav = radius * 0.35; + + const hCrown = radius * 0.30; + const hMidCrown = radius * 0.12; + const hGirdle = 0; + const hTip = -radius * 0.75; + const hMidPav = -radius * 0.45; + + const vertices = []; + const indices = []; + + const tableVerts = []; + for (let i = 0; i < 8; i++) { + const theta = (i / 8) * Math.PI * 2; + vertices.push(Math.cos(theta) * rTable, hCrown, Math.sin(theta) * rTable); + tableVerts.push(i); + } + + const midCrownVerts = []; + const midCrownStart = 8; + for (let i = 0; i < 8; i++) { + const theta = ((i + 0.5) / 8) * Math.PI * 2; + vertices.push(Math.cos(theta) * rMidCrown, hMidCrown, Math.sin(theta) * rMidCrown); + midCrownVerts.push(midCrownStart + i); + } + + const girdleVerts = []; + const girdleStart = 16; + for (let i = 0; i < 16; i++) { + const theta = (i / 16) * Math.PI * 2; + vertices.push(Math.cos(theta) * rGirdle, hGirdle, Math.sin(theta) * rGirdle); + girdleVerts.push(girdleStart + i); + } + + const pavMidVerts = []; + const pavMidStart = 32; + for (let i = 0; i < 16; i++) { + const theta = (i / 16) * Math.PI * 2; + vertices.push(Math.cos(theta) * (rGirdle * 0.5), hGirdle + (hTip - hGirdle) * 0.5, Math.sin(theta) * (rGirdle * 0.5)); + pavMidVerts.push(pavMidStart + i); + } + + const tipIdx = 48; + vertices.push(0, hTip, 0); + + const topCenterIdx = 49; + vertices.push(0, hCrown, 0); + + // Table Fan + for (let i = 0; i < 8; i++) { + indices.push(topCenterIdx, tableVerts[i], tableVerts[(i + 1) % 8]); + } + + // Crown + for (let i = 0; i < 8; i++) { + const t1 = tableVerts[i]; + const t2 = tableVerts[(i + 1) % 8]; + const m = midCrownVerts[i]; + + const gLeft = girdleVerts[(i * 2) % 16]; + const gMid = girdleVerts[(i * 2 + 1) % 16]; + const gRight = girdleVerts[(i * 2 + 2) % 16]; + + const nextI = (i + 1) % 8; + const prevI = (i + 7) % 8; + const T_curr = tableVerts[i]; + const T_next = tableVerts[nextI]; + const M_curr = midCrownVerts[i]; + const G_curr = girdleVerts[i * 2]; + const G_mid = girdleVerts[i * 2 + 1]; + const G_next = girdleVerts[(i * 2 + 2) % 16]; + + indices.push(T_curr, M_curr, T_next); // Star + indices.push(M_curr, G_curr, G_mid); // Upper Girdle 1 + indices.push(M_curr, G_mid, G_next); // Upper Girdle 2 + + const M_prev = midCrownVerts[prevI]; + indices.push(T_curr, M_prev, G_curr); // Bezel 1 + indices.push(T_curr, G_curr, M_curr); // Bezel 2 + } + + // Pavilion + for (let i = 0; i < 16; i++) { + const G_curr = girdleVerts[i]; + const G_next = girdleVerts[(i + 1) % 16]; + const P_curr = pavMidVerts[i]; + const P_next = pavMidVerts[(i + 1) % 16]; + + indices.push(G_curr, P_curr, G_next); + indices.push(G_next, P_curr, P_next); + indices.push(P_curr, tipIdx, P_next); + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + geometry.setIndex(indices); + geometry.computeVertexNormals(); + + return geometry; +} + +// Helper to map triangles to logical Facet IDs for interaction +export function addFacetIds(nonIndexedGeometry) { + const positionAttribute = nonIndexedGeometry.getAttribute('position'); + const vertexCount = positionAttribute.count; + // nonIndexedGeometry has unique vertices for each triangle, so count is multiple of 3 + + const facetIds = new Float32Array(vertexCount); + + let triIndex = 0; + + // MUST MATCH THE ORDER OF INDICES PUSHED IN createDiamondGeometry + + // 1. Table: 8 triangles (Fan) + // ID 1: Table + for (let i = 0; i < 8; i++) { + const id = 1; + for (let v = 0; v < 3; v++) facetIds[triIndex * 3 + v] = id; + triIndex++; + } + + // 2. Crown: 8 sections * 5 triangles + for (let i = 0; i < 8; i++) { + // Star: 1 triangle + const starId = 100 + i; + for (let v = 0; v < 3; v++) facetIds[triIndex * 3 + v] = starId; + triIndex++; + + // Upper Girdle 1: 1 triangle + const upGirdle1Id = 200 + i * 2; + for (let v = 0; v < 3; v++) facetIds[triIndex * 3 + v] = upGirdle1Id; + triIndex++; + + // Upper Girdle 2: 1 triangle + const upGirdle2Id = 200 + i * 2 + 1; + for (let v = 0; v < 3; v++) facetIds[triIndex * 3 + v] = upGirdle2Id; + triIndex++; + + // Bezel: 2 triangles (Kite) -> Same ID + const bezelId = 300 + i; + for (let v = 0; v < 3; v++) facetIds[triIndex * 3 + v] = bezelId; + triIndex++; + for (let v = 0; v < 3; v++) facetIds[triIndex * 3 + v] = bezelId; + triIndex++; + } + + // 3. Pavilion: 16 sections * 3 triangles + for (let i = 0; i < 16; i++) { + // Lower Girdle / Upper Pav 1 + const lowGirdle1Id = 400 + i; + for (let v = 0; v < 3; v++) facetIds[triIndex * 3 + v] = lowGirdle1Id; + triIndex++; + + // Lower Girdle / Upper Pav 2 + const lowGirdle2Id = 500 + i; + for (let v = 0; v < 3; v++) facetIds[triIndex * 3 + v] = lowGirdle2Id; + triIndex++; + + // Pavilion Main (Tip) + const pavMainId = 600 + i; + for (let v = 0; v < 3; v++) facetIds[triIndex * 3 + v] = pavMainId; + triIndex++; + } + + nonIndexedGeometry.setAttribute('aFacetId', new THREE.BufferAttribute(facetIds, 1)); + return nonIndexedGeometry; +} diff --git a/website/src/components/DiamondScene/index.js b/website/src/components/DiamondScene/index.js new file mode 100644 index 00000000..07b35f73 --- /dev/null +++ b/website/src/components/DiamondScene/index.js @@ -0,0 +1,273 @@ +import React, { useEffect, useRef } from 'react'; +import * as THREE from 'three'; +import gsap from 'gsap'; +import { DiamondShader, ParticleShader, FacetHighlightShader } from './shaders'; +import { createDiamondGeometry, addFacetIds } from './geometry'; + +export default function DiamondScene({ className, onHoverChange }) { + const canvasContainerRef = useRef(null); + + useEffect(() => { + if (!canvasContainerRef.current) return; + + const container = canvasContainerRef.current; + + // Scene setup + const scene = new THREE.Scene(); + + // Camera + const camera = new THREE.PerspectiveCamera(22, container.clientWidth / container.clientHeight, 0.1, 1000); + camera.position.z = 12; + + // Renderer + const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); // Alpha true for transparency + renderer.setSize(container.clientWidth, container.clientHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + container.appendChild(renderer.domElement); + + // GEOMETRY + // Use slightly smaller radius to fit composition + const diamondGeometry = createDiamondGeometry(1.7); + + // Flat Normals for Faceted Look (Critical for diamond shader) + let flatGeometry = diamondGeometry.toNonIndexed(); + flatGeometry.computeVertexNormals(); + + // ADD FACET IDs for Interaction + flatGeometry = addFacetIds(flatGeometry); + + // SHADER MATERIAL (Main Body) + const material = new THREE.ShaderMaterial({ + uniforms: THREE.UniformsUtils.clone(DiamondShader.uniforms), + vertexShader: DiamondShader.vertexShader, + fragmentShader: DiamondShader.fragmentShader, + side: THREE.DoubleSide, + transparent: true, + extensions: { derivatives: true } + }); + + const mesh = new THREE.Mesh(flatGeometry, material); + + // HIGHLIGHT MESH (Overlay for EIP-2535 Facets) + const highlightUniforms = THREE.UniformsUtils.clone(FacetHighlightShader.uniforms); + const highlightMaterial = new THREE.ShaderMaterial({ + uniforms: highlightUniforms, + vertexShader: FacetHighlightShader.vertexShader, + fragmentShader: FacetHighlightShader.fragmentShader, + side: THREE.DoubleSide, + transparent: true, + depthTest: true, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + + // Create a slightly scaled up mesh or use polygon offset to avoid z-fighting + const highlightMesh = new THREE.Mesh(flatGeometry, highlightMaterial); + highlightMesh.scale.setScalar(1.001); // Tiny scale up to sit on top + + // WIREFRAME (Subtle Structure) + // Angle threshold reduced to 15 to catch the top table edges (which are shallow) + const edges = new THREE.EdgesGeometry(diamondGeometry, 15); + const lineMat = new THREE.LineBasicMaterial({ + color: 0xffffff, + transparent: true, + opacity: 0.3, + blending: THREE.AdditiveBlending + }); + const wireframe = new THREE.LineSegments(edges, lineMat); + + const diamondGroup = new THREE.Group(); + diamondGroup.add(mesh); + diamondGroup.add(highlightMesh); + diamondGroup.add(wireframe); + + // PARTICLES: FLOATING WAVE (PRO GRADE - DENSE & UNORDERED) + const particlesGeometry = new THREE.BufferGeometry(); + const countX = 200; + const countZ = 100; + const particlesCount = countX * countZ; + const posArray = new Float32Array(particlesCount * 3); + + let i = 0; + const separation = 0.5; + const offsetX = (countX * separation) / 2; + const offsetZ = (countZ * separation) / 2; + + for(let x = 0; x < countX; x++) { + for(let z = 0; z < countZ; z++) { + posArray[i] = (x * separation) - offsetX + (Math.random() - 0.5) * separation * 0.8; + posArray[i+1] = 0; + posArray[i+2] = (z * separation) - offsetZ + (Math.random() - 0.5) * separation * 0.8; + i += 3; + } + } + + particlesGeometry.setAttribute('position', new THREE.BufferAttribute(posArray, 3)); + + const particlesMaterial = new THREE.ShaderMaterial({ + uniforms: { + ...ParticleShader.uniforms, + uWidth: { value: countX * separation }, + uDepth: { value: countZ * separation } + }, + vertexShader: ParticleShader.vertexShader, + fragmentShader: ParticleShader.fragmentShader, + transparent: true, + blending: THREE.AdditiveBlending, + depthWrite: false + }); + + const particlesMesh = new THREE.Points(particlesGeometry, particlesMaterial); + + const particlesGroup = new THREE.Group(); + particlesGroup.add(particlesMesh); + + particlesGroup.position.y = -1; + particlesGroup.position.z = -1; + particlesGroup.rotation.x = 0.05; + + scene.add(diamondGroup); + scene.add(particlesGroup); + + // INITIAL POSITION + diamondGroup.position.x = window.innerWidth > 1024 ? 2.2 : 0; + diamondGroup.position.y = 0.1; + diamondGroup.rotation.x = 0.25; + + // ANIMATION + // Rotation + gsap.to(diamondGroup.rotation, { + y: Math.PI * 2, + duration: 40, + repeat: -1, + ease: "none" + }); + + // Float + gsap.to(diamondGroup.position, { + y: 0.4, + duration: 4, + yoyo: true, + repeat: -1, + ease: "sine.inOut" + }); + + // RAYCASTER SETUP + const raycaster = new THREE.Raycaster(); + const mouse = new THREE.Vector2(-100, -100); // Start off-screen + + const onMouseMove = (event) => { + // Disable interaction on mobile + if (window.innerWidth <= 1024) { + mouse.x = -100; + mouse.y = -100; + return; + } + + const rect = renderer.domElement.getBoundingClientRect(); + mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + }; + + window.addEventListener('mousemove', onMouseMove); + + // Time Uniform & Loop + const clock = new THREE.Clock(); + let animationId; + let currentHoverId = -1; + + const animate = () => { + animationId = requestAnimationFrame(animate); + const elapsedTime = clock.getElapsedTime(); + + // Update Uniforms + mesh.material.uniforms.uTime.value = elapsedTime; + highlightMesh.material.uniforms.uTime.value = elapsedTime; + particlesMaterial.uniforms.uTime.value = elapsedTime; + + // Rotate wave slightly + particlesGroup.rotation.y = Math.sin(elapsedTime * 0.1) * 0.1; + + // Raycasting logic + raycaster.setFromCamera(mouse, camera); + + // Intersect with the highlight mesh (which has the same geometry as the diamond) + const intersects = raycaster.intersectObject(highlightMesh); + + if (intersects.length > 0) { + const intersect = intersects[0]; + const faceIndex = intersect.faceIndex; + + // Retrieve Facet ID from geometry attribute + if (flatGeometry.attributes.aFacetId) { + const facetId = flatGeometry.attributes.aFacetId.getX(faceIndex * 3); + + if (highlightMesh.material.uniforms.uHoverFacetId.value !== facetId) { + highlightMesh.material.uniforms.uHoverFacetId.value = facetId; + + // Trigger callback only if changed + if (currentHoverId !== facetId) { + currentHoverId = facetId; + if (onHoverChange) onHoverChange(facetId); + } + } + } + } else { + // Reset if no intersection + if (highlightMesh.material.uniforms.uHoverFacetId.value !== -1.0) { + highlightMesh.material.uniforms.uHoverFacetId.value = -1.0; + + if (currentHoverId !== -1) { + currentHoverId = -1; + if (onHoverChange) onHoverChange(-1); + } + } + } + + renderer.render(scene, camera); + }; + animate(); + + // RESIZE + const handleResize = () => { + if (!container) return; + const width = container.clientWidth; + const height = container.clientHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + renderer.setSize(width, height); + + const isDesktop = window.innerWidth > 1024; + + if (isDesktop) { + gsap.to(diamondGroup.position, { x: 2.2, duration: 0.5 }); + mesh.material.opacity = 1.0; + wireframe.material.opacity = 0.2; + } else { + gsap.to(diamondGroup.position, { x: 0, duration: 0.5 }); + mesh.material.opacity = 0.1; + wireframe.material.opacity = 0.05; + } + }; + + handleResize(); + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + window.removeEventListener('mousemove', onMouseMove); + cancelAnimationFrame(animationId); + if (container && renderer.domElement && container.contains(renderer.domElement)) { + container.removeChild(renderer.domElement); + } + diamondGeometry.dispose(); + flatGeometry.dispose(); + edges.dispose(); + particlesGeometry.dispose(); + renderer.dispose(); + }; + }, [onHoverChange]); // Depend on callback + + return
; +} diff --git a/website/src/components/DiamondScene/shaders.js b/website/src/components/DiamondScene/shaders.js new file mode 100644 index 00000000..13db4279 --- /dev/null +++ b/website/src/components/DiamondScene/shaders.js @@ -0,0 +1,358 @@ +import * as THREE from 'three'; + +// ULTRA-REALISTIC DIAMOND SHADER +// Implements physical dispersion (chromatic aberration), internal reflection simulation, +// and a crystalline normal map perturbation for that "crushed ice" look. +export const DiamondShader = { + uniforms: { + uTime: { value: 0 }, + // Color Palette: Deep rich blues for shadow, bright electric blues for highlights + uColor1: { value: new THREE.Color('#020617') }, // Almost black navy (Depth) + uColor2: { value: new THREE.Color('#1d4ed8') }, // Rich Blue (Body) + uColor3: { value: new THREE.Color('#bfdbfe') }, // Ice White (Sparkle) + uEnvRotation: { value: 0 }, + uPixelSize: { value: 2.0 } // Tighter dithering for definition + }, + vertexShader: ` + varying vec2 vUv; + varying vec3 vNormal; + varying vec3 vPosition; + varying vec3 vViewPosition; + varying vec3 vWorldPosition; + varying vec3 vReflect; + + void main() { + vUv = uv; + vNormal = normalize(normalMatrix * normal); + vPosition = position; + + vec4 worldPosition = modelMatrix * vec4(position, 1.0); + vWorldPosition = worldPosition.xyz; + + vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); + vViewPosition = -mvPosition.xyz; + + // Calculate reflection vector in view space for env mapping + vReflect = reflect(-normalize(vViewPosition), vNormal); + + gl_Position = projectionMatrix * mvPosition; + } + `, + fragmentShader: ` + uniform float uTime; + uniform vec3 uColor1; + uniform vec3 uColor2; + uniform vec3 uColor3; + uniform float uPixelSize; + + varying vec2 vUv; + varying vec3 vNormal; + varying vec3 vViewPosition; + varying vec3 vReflect; + + // Ordered dithering matrix 4x4 + float dither4x4(vec2 position, float brightness) { + int x = int(mod(position.x, 4.0)); + int y = int(mod(position.y, 4.0)); + int index = x + y * 4; + float limit = 0.0; + + if (x < 8) { + if (index == 0) limit = 0.0625; + if (index == 1) limit = 0.5625; + if (index == 2) limit = 0.1875; + if (index == 3) limit = 0.6875; + if (index == 4) limit = 0.8125; + if (index == 5) limit = 0.3125; + if (index == 6) limit = 0.9375; + if (index == 7) limit = 0.4375; + if (index == 8) limit = 0.25; + if (index == 9) limit = 0.75; + if (index == 10) limit = 0.125; + if (index == 11) limit = 0.625; + if (index == 12) limit = 1.0; + if (index == 13) limit = 0.5; + if (index == 14) limit = 0.875; + if (index == 15) limit = 0.375; + } + return brightness < limit ? 0.0 : 1.0; + } + + // Fast pseudo-random + float rand(vec2 co){ + return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); + } + + // Cheap environment map simulation (Studio Lights) + float getEnvLight(vec3 dir) { + float light = 0.0; + // Key Light (Top Right) - Boosted for brightness + light += pow(max(0.0, dot(dir, normalize(vec3(1.0, 1.5, 1.0)))), 32.0) * 3.5; + // Top Light (Direct Overhead) - Added for extra brilliance + light += pow(max(0.0, dot(dir, vec3(0.0, 0.95, 0.1))), 20.0) * 2.0; + // Fill Light (Left) + light += pow(max(0.0, dot(dir, normalize(vec3(-1.0, 0.5, 0.2)))), 16.0) * 1.2; + // Rim Light (Back/Bottom) - Reduced intensity to darken bottom + light += pow(max(0.0, dot(dir, normalize(vec3(0.0, -1.0, -1.0)))), 8.0) * 0.2; + + return light; + } + + void main() { + vec3 viewDir = normalize(vViewPosition); + vec3 normal = normalize(vNormal); + + // 1. Fresnel (The "Glass" Edge) + float fresnel = pow(1.0 - max(0.0, dot(viewDir, normal)), 4.0); + + // 2. Reflection (External bounce) + vec3 refDir = reflect(-viewDir, normal); + + // 3. Refraction/Internal (The "Fire") + // We simulate internal bounces by distorting the reflection vector + // This creates the "scrambled" look of a diamond interior + vec3 internalDir = refDir; + internalDir.x += sin(uTime * 0.5 + vViewPosition.y * 10.0) * 0.1; + internalDir.y += cos(uTime * 0.3 + vViewPosition.x * 10.0) * 0.1; + internalDir = normalize(internalDir); + + // 4. Dispersion (Chromatic Aberration - The Rainbow) + // We sample the environment light at slightly offset angles for R, G, B + float rLight = getEnvLight(normalize(internalDir + vec3(0.02, 0.0, 0.0))); + float gLight = getEnvLight(normalize(internalDir)); + float bLight = getEnvLight(normalize(internalDir - vec3(0.02, 0.0, 0.0))); + + vec3 sparkles = vec3(rLight, gLight, bLight); + + // 5. Edge Definition (Facet Cuts) + // Use derivatives to find sharp geometric edges + vec3 dNx = dFdx(vNormal); + vec3 dNy = dFdy(vNormal); + float edgeStrength = length(dNx) + length(dNy); + // Lower threshold to catch fainter edges (like the top table angles) + float edge = smoothstep(0.01, 0.06, edgeStrength); + + // COMPOSITING + + // Base Body: Deep blue to mid blue gradient based on view angle + float bodyTerm = dot(normal, vec3(0.0, 1.0, 0.0)) * 0.5 + 0.5; + // Brighter top mix: 0.8 influence instead of 0.6 + vec3 bodyColor = mix(uColor1, uColor2, bodyTerm * 0.8 + fresnel * 0.4); + + // Add Sparkles (Dispersion) + // Sparkles are masked by the body density to feel "internal" + vec3 finalColor = bodyColor + (sparkles * uColor3 * 1.5); + + // Add extra top face brightness (Table highlight) + float topGlow = smoothstep(0.8, 1.0, normal.y); // Widen the range to catch more top angles + finalColor += uColor3 * topGlow * 0.25; // Boosted intensity + + // Add crisp white edges - Changed to Light Blue for definition + vec3 edgeColor = mix(uColor3, uColor2, 0.2); // Whiter blue + finalColor += edgeColor * edge * 2.0; // Stronger edge definition + + // DITHERING (The Retro-Tech Style) + // We dither based on luminance to create texture + float luminance = dot(finalColor, vec3(0.299, 0.587, 0.114)); + + // Boost dither input at edges and highlights - Increased weight to fill body + float ditherInput = luminance * 1.2 + edge * 0.4 + max(rLight, max(gLight, bLight)) * 0.2; + + vec2 pixelCoord = gl_FragCoord.xy / uPixelSize; + float dither = dither4x4(pixelCoord, ditherInput); + + // Final Mix: + // Balanced mix: Use calculated color for both states to preserve form + // Shadow state: Dimmer but colored (0.6) + // Light state: Boosted (1.4) + vec3 pixelColor = mix(finalColor * 0.6, finalColor * 1.4, dither); + + // Add pure white sparkle post-dither for "blinding" hits + float superHighlight = step(0.95, max(rLight, max(gLight, bLight))); + pixelColor = mix(pixelColor, vec3(1.0), superHighlight * 0.8); + + // MOBILE VISIBILITY FIX: + // On small screens, the diamond often sits BEHIND the text. + // We need to fade it out or darken it significantly when it might interfere with text readability. + + gl_FragColor = vec4(pixelColor, 0.9); // Slight transparency for blending + } +` +}; + +// PARTICLE SHADER - Floating diamond dust (Pro Wave with Flow) +export const ParticleShader = { + uniforms: { + uTime: { value: 0 }, + uColor: { value: new THREE.Color('#3b82f6') }, // Blue-500 + uPixelSize: { value: 2.0 }, + uWidth: { value: 100.0 } // Width of the field for wrapping + }, + vertexShader: ` + uniform float uTime; + uniform float uWidth; + varying vec3 vPos; + varying float vDist; + + void main() { + vPos = position; + vec3 pos = position; + + // CONTINUOUS FLOW: + // Move particles to the right over time + float speed = 0.5; + pos.x += uTime * speed; + + // Infinite Scroll Logic: + // If pos.x goes beyond half width, wrap it back to start + // Assumes initial grid is centered at 0 + // uWidth is total width. Range is [-uWidth/2, uWidth/2] + float halfWidth = uWidth * 0.5; + + // Modulo math for GLSL to wrap around [-halfWidth, halfWidth] + // We add halfWidth first to shift to [0, uWidth], mod it, then shift back + pos.x = mod(pos.x + halfWidth, uWidth) - halfWidth; + + // WAVE MOVEMENT (Vertical): + // Use the new wrapped X for wave calculation so the wave stays cohesive + float time = uTime * 0.3; + + // Layered sine waves for "water" look + float wave1 = sin(pos.x * 0.4 + time) * cos(pos.z * 0.3 + time) * 0.6; + float wave2 = sin(pos.x * 0.8 - time * 1.2) * 0.2; + float wave3 = cos((pos.x + pos.z) * 0.2) * 0.3; + + pos.y += wave1 + wave2 + wave3; + + vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); + gl_Position = projectionMatrix * mvPosition; + + // Calculate distance for depth fade + vDist = length(mvPosition.xyz); + + // Size attenuation - Dust Size + gl_PointSize = (4.0 * 12.0) / -mvPosition.z; + } + `, + fragmentShader: ` + uniform vec3 uColor; + uniform float uPixelSize; + varying float vDist; + + // Reusing dither logic for consistency + float dither4x4(vec2 position, float brightness) { + int x = int(mod(position.x, 4.0)); + int y = int(mod(position.y, 4.0)); + int index = x + y * 4; + float limit = 0.0; + if (x < 8) { + if (index == 0) limit = 0.0625; + if (index == 1) limit = 0.5625; + if (index == 2) limit = 0.1875; + if (index == 3) limit = 0.6875; + if (index == 4) limit = 0.8125; + if (index == 5) limit = 0.3125; + if (index == 6) limit = 0.9375; + if (index == 7) limit = 0.4375; + if (index == 8) limit = 0.25; + if (index == 9) limit = 0.75; + if (index == 10) limit = 0.125; + if (index == 11) limit = 0.625; + if (index == 12) limit = 1.0; + if (index == 13) limit = 0.5; + if (index == 14) limit = 0.875; + if (index == 15) limit = 0.375; + } + return brightness < limit ? 0.0 : 1.0; + } + + void main() { + vec2 center = gl_PointCoord - 0.5; + float dist = length(center); + if (dist > 0.5) discard; + + // Softer, more diffuse edge for "dust" look + float alpha = 1.0 - smoothstep(0.4, 0.8, dist); + + // Depth Fade + float depthFade = 1.0 - smoothstep(8.0, 22.0, vDist); + alpha *= depthFade; + + vec2 pixelCoord = gl_FragCoord.xy / uPixelSize; + float dither = dither4x4(pixelCoord, alpha * 1.5); + + if (dither < 0.1) discard; + + // Slightly more varied color for dust (mostly blue with faint white) + vec3 finalColor = mix(uColor, vec3(1.0), 0.1); + + gl_FragColor = vec4(finalColor, alpha * 0.6); + } + ` +}; + +// HIGHLIGHT SHADER - For highlighting specific facets (Diamond Standard / EIP-2535 Visualization) +export const FacetHighlightShader = { + uniforms: { + uTime: { value: 0 }, + uColor: { value: new THREE.Color('#488FF8') }, // Blue-400 (Bright Blue) + uActiveFacetId: { value: -1.0 }, // ID of the facet group to highlight (-1 = none) + uHoverFacetId: { value: -1.0 } // ID of the facet currently hovered (optional) + }, + vertexShader: ` + attribute float aFacetId; + varying float vFacetId; + varying vec3 vNormal; + + void main() { + vFacetId = aFacetId; + vNormal = normalize(normalMatrix * normal); + vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); + gl_Position = projectionMatrix * mvPosition; + } + `, + fragmentShader: ` + uniform float uTime; + uniform vec3 uColor; + uniform float uActiveFacetId; + uniform float uHoverFacetId; + + varying float vFacetId; + varying vec3 vNormal; + varying vec3 vPosition; // Added for rim calculation + + void main() { + // Check if this fragment belongs to the active facet group + float isActive = 1.0 - step(0.1, abs(vFacetId - uActiveFacetId)); + float isHover = 1.0 - step(0.1, abs(vFacetId - uHoverFacetId)); + + float totalActive = max(isActive, isHover); + + if (totalActive < 0.1) discard; + + // Clean, steady glow instead of frantic pulsing + // Small subtle breathe for life + float breathe = sin(uTime * 3.0) * 0.1 + 0.95; // Faster, brighter base + + // Add Rim Light for definition (Fresnel-like) + // View vector is roughly along Z in local space for this simple rim + float rim = 1.5 - abs(dot(normalize(vNormal), vec3(0.0, 0.0, 1.0))); + rim = pow(rim, 3.0); + + // Combine + vec3 finalColor = uColor * 1.4; // Boost base color brightness + + // Mix solid fill with extra bright rim + finalColor += vec3(0.6) * rim; // Brighter rim + + // Base alpha: steady and clean + // Increased opacity significantly for brighter appearance + float alpha = totalActive * breathe * 0.85; + + // Boost alpha at rim for "glassy" edge + alpha += rim * 0.4; + + gl_FragColor = vec4(finalColor, alpha); + } + ` +}; diff --git a/website/src/components/DiamondScene/useFacetBadges.js b/website/src/components/DiamondScene/useFacetBadges.js new file mode 100644 index 00000000..b1049498 --- /dev/null +++ b/website/src/components/DiamondScene/useFacetBadges.js @@ -0,0 +1,42 @@ +import { useState, useCallback } from 'react'; + +// A list of realistic facet names related to EIP-2535 Diamond Standard +const FACET_NAMES = [ + "DiamondCutFacet", + "DiamondLoupeFacet", + "OwnerFacet", + "AccessControlFacet", + "ERC20Facet", + "ERC721Facet", + "ERC721EnumerableFacet", + "ERC1155Facet", + "RoyaltyFacet", + "ERC165Facet", + "AccessControlPausableFacet", + "AccessControlTemporalFacet" +]; + +export function useFacetBadges() { + const [activeFacetName, setActiveFacetName] = useState(null); + + // Map random names to IDs to keep them consistent during a session if we wanted, + // but for now we just pick a random one on hover entry if it's not already set. + const [facetMap] = useState(() => new Map()); + + const handleHover = useCallback((facetId) => { + if (facetId === -1) { + setActiveFacetName(null); + return; + } + + // If we haven't assigned a name to this ID yet, pick one randomly + if (!facetMap.has(facetId)) { + const randomName = FACET_NAMES[Math.floor(Math.random() * FACET_NAMES.length)]; + facetMap.set(facetId, randomName); + } + + setActiveFacetName(facetMap.get(facetId)); + }, [facetMap]); + + return { activeFacetName, handleHover }; +} diff --git a/website/src/pages/home/HomepageHeader.js b/website/src/pages/home/HomepageHeader.js index 71add6ee..d1482b22 100644 --- a/website/src/pages/home/HomepageHeader.js +++ b/website/src/pages/home/HomepageHeader.js @@ -1,19 +1,29 @@ +import React from 'react'; import clsx from 'clsx'; import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Heading from '@theme/Heading'; -import GitHubStarButton from '@site/src/components/navigation/GitHubStarButton'; import Icon from '../../components/ui/Icon'; import styles from './homepageHeader.module.css'; +import DiamondScene from '../../components/DiamondScene'; +import { useFacetBadges } from '../../components/DiamondScene/useFacetBadges'; +import { FacetBadge } from '../../components/DiamondScene/FacetBadge'; export default function HomepageHeader() { - const {siteConfig} = useDocusaurusContext(); + const { activeFacetName, handleHover } = useFacetBadges(); + return (
+ + + {/* Floating Badge for Diamond Interaction */} + +
+
@@ -25,36 +35,30 @@ export default function HomepageHeader() { Build the future of
Smart Contracts -

- {siteConfig.tagline} -

-

- A smart contract library for building diamond-based systems with an onchain - standard library of facets. Write code that's designed to be understood, - maintained, and scaled. -

+
+

+ Compose is a smart contract library for building diamond-based systems with an onchain + standard library of facets. +

+

+ Write code that's designed to be understood, maintained, and scaled. +

+
- + Get Started - + Learn Core Concepts - +
GitHub - + Join Discord @@ -65,6 +69,7 @@ export default function HomepageHeader() {
+
@@ -73,5 +78,3 @@ export default function HomepageHeader() {
); } - - diff --git a/website/src/pages/home/homepageHeader.module.css b/website/src/pages/home/homepageHeader.module.css index 38b47121..afcd2846 100644 --- a/website/src/pages/home/homepageHeader.module.css +++ b/website/src/pages/home/homepageHeader.module.css @@ -14,6 +14,7 @@ position: absolute; inset: 0; z-index: 0; + pointer-events: none; } .heroGradient { @@ -35,21 +36,39 @@ background-size: 64px 64px; } -.heroContainer { position: relative; z-index: 1; } +.heroContainer { + position: relative; + z-index: 1; + /* Removed grid to allow full overlay */ + width: 100%; +} + +/* Canvas container is now absolute and full width to allow particles everywhere */ +.canvasContainer { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; /* Pass clicks through to buttons */ + z-index: 0; /* Behind content */ +} .heroContent { - max-width: 880px; - margin: 0 auto; - text-align: center; + position: relative; + z-index: 1; + text-align: left; + max-width: 640px; + /* margin-left: 0; */ /* Default is fine inside container */ + margin-right: auto; /* Pushes content to the left side if container is flex, but block is fine */ } -.badgeWrapper { margin-bottom: 1.5rem; animation: fadeInDown var(--motion-duration-normal) var(--motion-ease-standard); } +.badgeWrapper { margin-bottom: 2rem; animation: fadeInDown var(--motion-duration-normal) var(--motion-ease-standard); } .badge { position: relative; display: inline-flex; align-items: center; - gap: 0.5rem; + gap: 0.625rem; padding: 0.75rem 1.5rem; background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(16px); @@ -63,58 +82,60 @@ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); } -.badgePulse { - position: absolute; - left: 1.25rem; - width: 8px; - height: 8px; - background: #ef4444; - border-radius: 50%; - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.5; transform: scale(1.2); } -} - .heroTitle { - font-size: clamp(3rem, 8vw, 5rem); + font-size: clamp(2.5rem, 5vw, 4rem); /* Reduced max size from 5rem to 4rem */ font-weight: 900; - line-height: 1.1; + line-height: 1.2; letter-spacing: -0.04em; color: white; - margin-bottom: 1rem; + margin-bottom: 1.5rem; animation: fadeInUp var(--motion-duration-normal) var(--motion-ease-standard) 0.2s both; + text-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); } .heroTitleGradient { - background: linear-gradient(135deg, #60a5fa 0%, var(--compose-primary-500) 50%, var(--compose-primary-600) 100%); + background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #2563eb 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; display: inline-block; + filter: drop-shadow(0 0 20px rgba(6, 37, 88, 0.6)); +} + +.heroDescriptionWrapper { + position: relative; + margin-bottom: 3rem; + max-width: 640px; + animation: fadeInUp var(--motion-duration-normal) var(--motion-ease-standard) 0.3s both; } .heroSubtitle { - font-size: clamp(1.125rem, 2.5vw, 1.5rem); - color: rgba(255, 255, 255, 0.85); - font-weight: 500; + font-size: clamp(1rem, 4vw, 1.3rem); + color: #ffffff; + font-weight: 700; line-height: 1.6; - margin-bottom: 1rem; - animation: fadeInUp var(--motion-duration-normal) var(--motion-ease-standard) 0.3s both; + margin-bottom: 0.75rem; } .heroDescription { - font-size: clamp(1rem, 2vw, 1.125rem); - color: rgba(255, 255, 255, 0.7); - line-height: 1.8; - max-width: 720px; - margin: 0 auto 2rem; - animation: fadeInUp var(--motion-duration-normal) var(--motion-ease-standard) 0.4s both; + font-size: clamp(0.9375rem, 1.5vw, 1.0625rem); + color: rgba(255, 255, 255, 0.9); + font-weight: 500; + line-height: 1.6; + max-width: 100%; + margin: 0; + padding: 0; } -.heroCta { display: flex; align-items: center; justify-content: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 2rem; animation: fadeInUp var(--motion-duration-normal) var(--motion-ease-standard) 0.5s both; } +.heroCta { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 1.25rem; + flex-wrap: wrap; + margin-bottom: 1.5rem; + animation: fadeInUp var(--motion-duration-normal) var(--motion-ease-standard) 0.5s both; +} .ctaButton { display: inline-flex; @@ -128,6 +149,7 @@ transition: all var(--motion-duration-normal) var(--motion-ease-standard); border: 2px solid transparent; white-space: nowrap; + box-shadow: 0 4px 6px rgba(0,0,0,0.2); } .ctaPrimary { @@ -139,7 +161,7 @@ } .ctaPrimary:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(59,130,246,0.5), 0 4px 8px rgba(0,0,0,0.12); color: white; } -.ctaSecondary { background: rgba(255,255,255,0.05); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: white; border: 2px solid rgba(255,255,255,0.15); } +.ctaSecondary { background: rgba(15, 23, 42, 0.6); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: white; border: 2px solid rgba(255,255,255,0.15); } .ctaSecondary:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3); transform: translateY(-2px); color: white; } .ctaButton:focus-visible, @@ -149,26 +171,31 @@ border-radius: 8px; } -.ctaButtonDisabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; } -.ctaButtonDisabled:hover { transform: none; background: inherit; border-color: inherit; } - -.heroLinks { display: flex; align-items: center; justify-content: center; gap: 1rem; flex-wrap: wrap; animation: fadeInUp var(--motion-duration-normal) var(--motion-ease-standard) 0.6s both; } +.heroLinks { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 1rem; + flex-wrap: wrap; + animation: fadeInUp var(--motion-duration-normal) var(--motion-ease-standard) 0.6s both; +} .heroLink { display: inline-flex; align-items: center; - gap: 0.5rem; + gap: 0.625rem; padding: 0.75rem 1.25rem; - background: rgba(255, 255, 255, 0.03); + background: rgba(15, 23, 42, 0.6); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 0.625rem; - color: rgba(255, 255, 255, 0.85); + color: rgba(255, 255, 255, 0.9); font-size: 0.9375rem; font-weight: 500; text-decoration: none; transition: all 0.25s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); } .heroLink:hover { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.15); color: white; transform: translateY(-1px); } .heroLink svg { flex-shrink: 0; opacity: 0.9; } @@ -195,12 +222,54 @@ /* Responsive */ @media screen and (max-width: 1024px) { .heroBanner { padding: 8rem 0 10rem; min-height: 80vh; } + + /* Reset canvas container on mobile */ + .canvasContainer { + /* Still absolute, but maybe we center diamond via JS */ + } + + .heroContent { + text-align: center; /* Center text on mobile */ + margin: 0 auto; + max-width: 100%; + } + + .heroDescriptionWrapper { + margin: 0 auto 1.5rem; + text-align: center; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); + } + + .heroSubtitle, .heroDescription { + margin-left: auto; + margin-right: auto; + color: #ffffff; + } + + .heroDescription { + /* Remove previous mobile-only styles since wrapper handles it */ + background: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + padding: 0; + border: none; + } + + .heroCta { justify-content: center; } + .heroLinks { justify-content: center; } } + @media screen and (max-width: 768px) { .heroBanner { padding: 6rem 0 8rem; min-height: auto; } .heroTitle { font-size: 2.5rem; margin-bottom: 1rem; } .heroSubtitle { font-size: 1.125rem; margin-bottom: 1rem; } - .heroDescription { font-size: 1rem; margin-bottom: 2rem; } + .heroDescription { font-size: 1rem;} .heroCta { flex-direction: column; gap: 0.75rem; margin-bottom: 2rem; } .ctaButton { width: 100%; max-width: 320px; justify-content: center; } .heroLinks { flex-direction: column; gap: 0.75rem; } @@ -213,9 +282,7 @@ .badge { font-size: 0.8125rem; padding: 0.625rem 1.25rem; } } -/* Theme-specific wave colors to match section below */ +/* Theme-specific wave colors */ [data-theme='dark'] .heroWave { color: var(--compose-bg-900); } - -