Compare commits
5
Commits
2a4b234667
...
af296d674f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af296d674f | ||
|
|
553036b3ac | ||
|
|
aae6c3ac68 | ||
|
|
e4ec9c04a9 | ||
|
|
000dd2d99b |
@@ -0,0 +1,8 @@
|
|||||||
|
.git
|
||||||
|
.idea
|
||||||
|
qa-evidence
|
||||||
|
design-references
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
*.md
|
||||||
Generated
+3
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_22" default="true" project-jdk-name="openjdk-22" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/roncarve-official-web.iml" filepath="$PROJECT_DIR$/.idea/roncarve-official-web.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
email admin@roncarve.com
|
||||||
|
}
|
||||||
|
|
||||||
|
aitrackwalker.roncarve.com {
|
||||||
|
encode zstd gzip
|
||||||
|
reverse_proxy nginx:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
http://62.234.49.39 {
|
||||||
|
encode zstd gzip
|
||||||
|
reverse_proxy nginx:8080 {
|
||||||
|
header_up Host aitrackwalker.roncarve.com
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http://62.234.49.39:8080 {
|
||||||
|
encode zstd gzip
|
||||||
|
reverse_proxy nginx:8080 {
|
||||||
|
header_up Host aitrackwalker.roncarve.com
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
www.roncarve.com {
|
||||||
|
encode zstd gzip
|
||||||
|
reverse_proxy roncarve-web:80
|
||||||
|
}
|
||||||
|
|
||||||
|
roncarve.com {
|
||||||
|
encode zstd gzip
|
||||||
|
redir https://www.roncarve.com{uri} permanent
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY . /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 428 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 999 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 301 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 319 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 369 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
roncarve-web:
|
||||||
|
build: .
|
||||||
|
container_name: roncarve-web
|
||||||
|
restart: unless-stopped
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
|
networks:
|
||||||
|
- gateway
|
||||||
|
|
||||||
|
networks:
|
||||||
|
gateway:
|
||||||
|
external: true
|
||||||
|
name: gateway_gateway
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
roncarve-web:
|
||||||
|
build: .
|
||||||
|
container_name: roncarve-web
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:80"
|
||||||
+1328
File diff suppressed because it is too large
Load Diff
+17
@@ -0,0 +1,17 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:css|js|png|jpg|jpeg|gif|svg|webp|ico|woff2?)$ {
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public, max-age=604800, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
+852
@@ -0,0 +1,852 @@
|
|||||||
|
import * as THREE from './vendor/three.module.min.js';
|
||||||
|
|
||||||
|
const root = document.getElementById('heroWebgl');
|
||||||
|
const canvas = document.getElementById('heroWebglCanvas');
|
||||||
|
|
||||||
|
if (root && canvas) {
|
||||||
|
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||||
|
const lowPower = window.innerWidth < 640 || Boolean(connection && connection.saveData) || (navigator.deviceMemory && navigator.deviceMemory <= 2);
|
||||||
|
const staticMode = reducedMotion || lowPower;
|
||||||
|
let renderer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
renderer = new THREE.WebGLRenderer({
|
||||||
|
canvas,
|
||||||
|
alpha: false,
|
||||||
|
antialias: !lowPower,
|
||||||
|
depth: true,
|
||||||
|
stencil: false,
|
||||||
|
powerPreference: staticMode ? 'low-power' : 'high-performance'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
root.classList.add('has-error');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderer) {
|
||||||
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
renderer.toneMappingExposure = 0.96;
|
||||||
|
renderer.shadowMap.enabled = !lowPower;
|
||||||
|
renderer.shadowMap.type = THREE.PCFShadowMap;
|
||||||
|
renderer.autoClear = false;
|
||||||
|
|
||||||
|
const resolvedTheme = document.documentElement.dataset.theme || 'light';
|
||||||
|
const initialDark = resolvedTheme === 'dark' ? 1 : 0;
|
||||||
|
const uniforms = {
|
||||||
|
uTime: { value: 0 },
|
||||||
|
uDark: { value: initialDark },
|
||||||
|
uResolution: { value: new THREE.Vector2(1, 1) },
|
||||||
|
uMouse: { value: new THREE.Vector2(0.72, 0.4) }
|
||||||
|
};
|
||||||
|
|
||||||
|
const backgroundScene = new THREE.Scene();
|
||||||
|
const backgroundCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||||
|
const backgroundMaterial = new THREE.ShaderMaterial({
|
||||||
|
uniforms,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false,
|
||||||
|
toneMapped: false,
|
||||||
|
vertexShader: `
|
||||||
|
varying vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
gl_Position = vec4(position, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fragmentShader: `
|
||||||
|
precision highp float;
|
||||||
|
varying vec2 vUv;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uDark;
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform vec2 uMouse;
|
||||||
|
|
||||||
|
float hash21(vec2 p) {
|
||||||
|
p = fract(p * vec2(123.34, 456.21));
|
||||||
|
p += dot(p, p + 45.32);
|
||||||
|
return fract(p.x * p.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float segmentDistance(vec2 p, vec2 a, vec2 b) {
|
||||||
|
vec2 pa = p - a;
|
||||||
|
vec2 ba = b - a;
|
||||||
|
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||||
|
return length(pa - ba * h);
|
||||||
|
}
|
||||||
|
|
||||||
|
float beam(vec2 p, vec2 a, vec2 b, float width) {
|
||||||
|
return exp(-segmentDistance(p, a, b) / width);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 uv = vUv;
|
||||||
|
float aspect = uResolution.x / max(uResolution.y, 1.0);
|
||||||
|
vec2 p = uv - 0.5;
|
||||||
|
p.x *= aspect;
|
||||||
|
float t = uTime;
|
||||||
|
|
||||||
|
vec3 lightTop = vec3(0.992, 0.997, 1.0);
|
||||||
|
vec3 lightBottom = vec3(0.88, 0.93, 1.0);
|
||||||
|
vec3 darkTop = vec3(0.016, 0.027, 0.044);
|
||||||
|
vec3 darkBottom = vec3(0.025, 0.055, 0.08);
|
||||||
|
vec3 color = mix(mix(lightBottom, lightTop, uv.y), mix(darkBottom, darkTop, uv.y), uDark);
|
||||||
|
|
||||||
|
vec2 focus = vec2(aspect * 0.18 + (uMouse.x - 0.5) * 0.035, -0.12 + (0.5 - uMouse.y) * 0.025);
|
||||||
|
float halo = exp(-length((p - focus) / vec2(0.48, 0.42)) * 2.2);
|
||||||
|
color += mix(vec3(0.19, 0.52, 1.0), vec3(0.06, 0.42, 0.72), uDark) * halo * mix(0.12, 0.23, uDark);
|
||||||
|
|
||||||
|
float skyWash = exp(-length((p - vec2(aspect * 0.37, 0.39)) / vec2(0.52, 0.34)) * 2.0);
|
||||||
|
color += mix(vec3(0.2, 0.43, 0.78), vec3(0.04, 0.18, 0.36), uDark) * skyWash * mix(0.11, 0.14, uDark);
|
||||||
|
|
||||||
|
vec2 leftSource = vec2(-aspect * 0.58, -0.38);
|
||||||
|
vec2 rightTarget = vec2(aspect * 0.62, -0.19);
|
||||||
|
float cyanBeam = beam(p, leftSource, focus + vec2(-0.06, -0.08), 0.009);
|
||||||
|
float cyanGlow = beam(p, leftSource, focus + vec2(-0.06, -0.08), 0.055);
|
||||||
|
float warmBeam = beam(p, focus + vec2(0.04, 0.02), rightTarget, 0.008);
|
||||||
|
float warmGlow = beam(p, focus + vec2(0.04, 0.02), rightTarget, 0.052);
|
||||||
|
color += vec3(0.18, 0.88, 1.0) * (cyanBeam * 1.48 + cyanGlow * 0.34);
|
||||||
|
color += vec3(1.0, 0.58, 0.18) * (warmBeam * 2.05 + warmGlow * 0.4);
|
||||||
|
|
||||||
|
float spectrumA = beam(p, leftSource + vec2(0.0, 0.025), focus + vec2(0.1, -0.025), 0.0045);
|
||||||
|
float spectrumB = beam(p, focus + vec2(-0.02, -0.03), rightTarget + vec2(0.0, -0.045), 0.0045);
|
||||||
|
color += vec3(0.55, 0.42, 1.0) * spectrumA * 0.24;
|
||||||
|
color += vec3(0.96, 0.35, 0.72) * spectrumB * 0.18;
|
||||||
|
|
||||||
|
float fiberFlow = 0.0;
|
||||||
|
for (int index = 0; index < 7; index++) {
|
||||||
|
float offset = (float(index) - 3.0) * 0.012;
|
||||||
|
fiberFlow += beam(
|
||||||
|
p,
|
||||||
|
leftSource + vec2(0.0, offset),
|
||||||
|
focus + vec2(-0.07, -0.075 + offset * 0.45),
|
||||||
|
0.0022
|
||||||
|
);
|
||||||
|
}
|
||||||
|
color += vec3(0.22, 0.92, 1.0) * fiberFlow * mix(0.12, 0.2, uDark);
|
||||||
|
|
||||||
|
vec2 causticOrigin = focus + vec2(-0.055, -0.16);
|
||||||
|
vec2 causticVector = p - causticOrigin;
|
||||||
|
float causticDistance = length(causticVector);
|
||||||
|
float causticAngle = atan(causticVector.y, causticVector.x);
|
||||||
|
float causticBands = pow(max(0.0, sin(causticAngle * 18.0 + causticDistance * 5.0 - t * 0.18)), 18.0);
|
||||||
|
float causticMask = smoothstep(0.05, 0.22, causticDistance) * exp(-causticDistance * 1.75) * smoothstep(0.1, -0.25, p.y);
|
||||||
|
color += mix(vec3(0.36, 0.78, 1.0), vec3(0.68, 0.34, 1.0), smoothstep(-1.2, 0.5, causticAngle)) * causticBands * causticMask * mix(0.34, 0.44, uDark);
|
||||||
|
|
||||||
|
float floorMask = smoothstep(0.06, -0.28, p.y);
|
||||||
|
vec2 gp = p - vec2(0.0, -0.24);
|
||||||
|
float horizontal = 1.0 - smoothstep(0.0, 0.018, abs(fract((gp.y + 0.55) * 18.0) - 0.5));
|
||||||
|
float perspectiveX = gp.x / max(0.12, abs(gp.y - 0.34));
|
||||||
|
float vertical = 1.0 - smoothstep(0.0, 0.025, abs(fract(perspectiveX * 3.2) - 0.5));
|
||||||
|
float grid = (horizontal * 0.48 + vertical * 0.35) * floorMask;
|
||||||
|
color += mix(vec3(0.12, 0.34, 0.68), vec3(0.08, 0.48, 0.58), uDark) * grid * mix(0.2, 0.22, uDark);
|
||||||
|
|
||||||
|
float dust = step(0.993, hash21(floor(p * 190.0 + t * 0.4))) * floorMask;
|
||||||
|
color += vec3(0.35, 0.9, 1.0) * dust * 0.28;
|
||||||
|
color += (hash21(p * 210.0) - 0.5) * mix(0.008, 0.014, uDark);
|
||||||
|
|
||||||
|
float vignette = smoothstep(1.25, 0.2, length((uv - 0.5) * vec2(0.82, 1.0)));
|
||||||
|
color *= mix(0.95, 1.0, vignette);
|
||||||
|
gl_FragColor = vec4(color, 1.0);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
const backgroundGeometry = new THREE.PlaneGeometry(2, 2);
|
||||||
|
backgroundScene.add(new THREE.Mesh(backgroundGeometry, backgroundMaterial));
|
||||||
|
|
||||||
|
const worldScene = new THREE.Scene();
|
||||||
|
const worldCamera = new THREE.PerspectiveCamera(33, 1, 0.1, 50);
|
||||||
|
worldCamera.position.set(0, 0.12, 9.4);
|
||||||
|
|
||||||
|
const hemisphereLight = new THREE.HemisphereLight(0xe8f8ff, 0x0c1830, 1.8);
|
||||||
|
const keyLight = new THREE.DirectionalLight(0xffffff, 5.2);
|
||||||
|
keyLight.position.set(-3.8, 5.6, 5.8);
|
||||||
|
keyLight.castShadow = !lowPower;
|
||||||
|
keyLight.shadow.mapSize.set(lowPower ? 512 : 1024, lowPower ? 512 : 1024);
|
||||||
|
keyLight.shadow.camera.near = 0.5;
|
||||||
|
keyLight.shadow.camera.far = 20;
|
||||||
|
keyLight.shadow.camera.left = -5;
|
||||||
|
keyLight.shadow.camera.right = 5;
|
||||||
|
keyLight.shadow.camera.top = 5;
|
||||||
|
keyLight.shadow.camera.bottom = -5;
|
||||||
|
|
||||||
|
const cyanLight = new THREE.PointLight(0x62dfff, 22, 15, 2);
|
||||||
|
cyanLight.position.set(-3.2, -0.5, 4.4);
|
||||||
|
const blueLight = new THREE.PointLight(0x4e78ff, 16, 13, 2);
|
||||||
|
blueLight.position.set(3.5, 2.1, 3.2);
|
||||||
|
const warmLight = new THREE.PointLight(0xffb75e, 12, 12, 2);
|
||||||
|
warmLight.position.set(3.2, -1.0, 4.8);
|
||||||
|
worldScene.add(hemisphereLight, keyLight, cyanLight, blueLight, warmLight);
|
||||||
|
|
||||||
|
const disposableGeometries = [];
|
||||||
|
const disposableMaterials = [];
|
||||||
|
const trackGeometry = function (geometry) {
|
||||||
|
disposableGeometries.push(geometry);
|
||||||
|
return geometry;
|
||||||
|
};
|
||||||
|
const trackMaterial = function (material) {
|
||||||
|
disposableMaterials.push(material);
|
||||||
|
return material;
|
||||||
|
};
|
||||||
|
|
||||||
|
const environmentScene = new THREE.Scene();
|
||||||
|
environmentScene.background = new THREE.Color(0x06162d);
|
||||||
|
|
||||||
|
function addEnvironmentPanel(color, position, rotation, scale, intensity) {
|
||||||
|
const panelGeometry = trackGeometry(new THREE.PlaneGeometry(scale[0], scale[1]));
|
||||||
|
const panelMaterial = trackMaterial(new THREE.MeshBasicMaterial({
|
||||||
|
color: new THREE.Color(color).multiplyScalar(intensity || 1),
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
}));
|
||||||
|
const panel = new THREE.Mesh(panelGeometry, panelMaterial);
|
||||||
|
panel.position.set(position[0], position[1], position[2]);
|
||||||
|
panel.rotation.set(rotation[0], rotation[1], rotation[2]);
|
||||||
|
environmentScene.add(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEnvironmentPanel(0xffffff, [0, 5.6, 0], [Math.PI / 2, 0, 0], [14, 14], 1.15);
|
||||||
|
addEnvironmentPanel(0x3ceaff, [-5.4, -0.6, 0], [0, Math.PI / 2, 0], [4.2, 13], 1.55);
|
||||||
|
addEnvironmentPanel(0x1e55cf, [5.2, 0.8, -0.8], [0, -Math.PI / 2, 0], [5, 12], 1.45);
|
||||||
|
addEnvironmentPanel(0xffb663, [2.6, -1.2, 5], [0, 0, 0], [3.2, 9], 1.5);
|
||||||
|
addEnvironmentPanel(0x7b62ff, [-2.2, 0.2, -5], [0, 0, 0], [4, 10], 1.35);
|
||||||
|
addEnvironmentPanel(0xffffff, [-1.8, 1.2, 4.8], [0, 0, 0], [0.8, 9], 1.65);
|
||||||
|
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
||||||
|
pmremGenerator.compileCubemapShader();
|
||||||
|
const environmentTarget = pmremGenerator.fromScene(environmentScene, lowPower ? 0.035 : 0.025, 0.1, 30);
|
||||||
|
pmremGenerator.dispose();
|
||||||
|
worldScene.environment = environmentTarget.texture;
|
||||||
|
|
||||||
|
const crystalShape = new THREE.Shape();
|
||||||
|
crystalShape.moveTo(-1.43, -1.48);
|
||||||
|
crystalShape.lineTo(1.36, -1.48);
|
||||||
|
crystalShape.lineTo(0.06, 2.08);
|
||||||
|
crystalShape.closePath();
|
||||||
|
|
||||||
|
const crystalGeometry = trackGeometry(new THREE.ExtrudeGeometry(crystalShape, {
|
||||||
|
depth: 1.02,
|
||||||
|
bevelEnabled: true,
|
||||||
|
bevelSegments: lowPower ? 1 : 4,
|
||||||
|
bevelSize: 0.065,
|
||||||
|
bevelThickness: 0.1,
|
||||||
|
curveSegments: 1,
|
||||||
|
steps: 1
|
||||||
|
}));
|
||||||
|
crystalGeometry.translate(0, -0.22, -0.51);
|
||||||
|
|
||||||
|
const glassFrontMaterial = trackMaterial(new THREE.MeshPhysicalMaterial({
|
||||||
|
color: 0xc8eaff,
|
||||||
|
roughness: 0.008,
|
||||||
|
metalness: 0,
|
||||||
|
transmission: 0,
|
||||||
|
thickness: 2.8,
|
||||||
|
ior: 1.54,
|
||||||
|
dispersion: lowPower ? 0.06 : 0.19,
|
||||||
|
attenuationColor: new THREE.Color(0x62b8ef),
|
||||||
|
attenuationDistance: 2.6,
|
||||||
|
clearcoat: 1,
|
||||||
|
clearcoatRoughness: 0.006,
|
||||||
|
iridescence: lowPower ? 0.08 : 0.24,
|
||||||
|
iridescenceIOR: 1.34,
|
||||||
|
envMapIntensity: 0.65,
|
||||||
|
specularIntensity: 1,
|
||||||
|
specularColor: new THREE.Color(0xffffff),
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.055,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthWrite: false
|
||||||
|
}));
|
||||||
|
glassFrontMaterial.userData.lightColor = new THREE.Color(0xc8eaff);
|
||||||
|
glassFrontMaterial.userData.darkColor = new THREE.Color(0x8ed2f4);
|
||||||
|
glassFrontMaterial.userData.lightOpacity = 0.055;
|
||||||
|
glassFrontMaterial.userData.darkOpacity = 0.1;
|
||||||
|
glassFrontMaterial.userData.lightEnv = 0.65;
|
||||||
|
glassFrontMaterial.userData.darkEnv = 1.1;
|
||||||
|
|
||||||
|
const glassSideMaterial = trackMaterial(new THREE.MeshPhysicalMaterial({
|
||||||
|
color: 0x2766ad,
|
||||||
|
roughness: 0.015,
|
||||||
|
metalness: 0.06,
|
||||||
|
transmission: 0.4,
|
||||||
|
thickness: 3.2,
|
||||||
|
ior: 1.52,
|
||||||
|
dispersion: lowPower ? 0.04 : 0.13,
|
||||||
|
attenuationColor: new THREE.Color(0x164998),
|
||||||
|
attenuationDistance: 1.45,
|
||||||
|
clearcoat: 1,
|
||||||
|
clearcoatRoughness: 0.008,
|
||||||
|
iridescence: lowPower ? 0.04 : 0.16,
|
||||||
|
iridescenceIOR: 1.3,
|
||||||
|
envMapIntensity: 4.5,
|
||||||
|
emissive: new THREE.Color(0x0d2e78),
|
||||||
|
emissiveIntensity: 0.12,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.94,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthWrite: false
|
||||||
|
}));
|
||||||
|
glassSideMaterial.userData.lightColor = new THREE.Color(0x2766ad);
|
||||||
|
glassSideMaterial.userData.darkColor = new THREE.Color(0x1d57a6);
|
||||||
|
glassSideMaterial.userData.lightOpacity = 0.94;
|
||||||
|
glassSideMaterial.userData.darkOpacity = 0.97;
|
||||||
|
glassSideMaterial.userData.lightEnv = 4.5;
|
||||||
|
glassSideMaterial.userData.darkEnv = 5.3;
|
||||||
|
const crystalMaterials = [glassFrontMaterial, glassSideMaterial];
|
||||||
|
|
||||||
|
const crystalMesh = new THREE.Mesh(crystalGeometry, crystalMaterials);
|
||||||
|
crystalMesh.castShadow = !lowPower;
|
||||||
|
crystalMesh.receiveShadow = true;
|
||||||
|
crystalMesh.renderOrder = 1;
|
||||||
|
|
||||||
|
function createCrystalFacet(points, color, opacity, additive, z) {
|
||||||
|
const shape = new THREE.Shape();
|
||||||
|
shape.moveTo(points[0][0], points[0][1]);
|
||||||
|
points.slice(1).forEach(function (point) { shape.lineTo(point[0], point[1]); });
|
||||||
|
shape.closePath();
|
||||||
|
const geometry = trackGeometry(new THREE.ShapeGeometry(shape));
|
||||||
|
const material = trackMaterial(new THREE.MeshBasicMaterial({
|
||||||
|
color,
|
||||||
|
transparent: true,
|
||||||
|
opacity,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false,
|
||||||
|
toneMapped: false,
|
||||||
|
blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending
|
||||||
|
}));
|
||||||
|
material.userData.lightOpacity = opacity;
|
||||||
|
material.userData.darkOpacity = Math.min(0.68, opacity * 1.28);
|
||||||
|
const facet = new THREE.Mesh(geometry, material);
|
||||||
|
facet.position.z = z || 0.625;
|
||||||
|
facet.renderOrder = additive ? 4 : 3;
|
||||||
|
return { facet, material };
|
||||||
|
}
|
||||||
|
|
||||||
|
const crystalFacets = [
|
||||||
|
createCrystalFacet([[-1.39, -1.65], [1.31, -1.65], [0.05, 1.86]], 0x93d7f1, 0.23, false, 0.615),
|
||||||
|
createCrystalFacet([[-1.36, -1.68], [-0.9, -1.68], [0.05, 1.83], [-0.17, 1.44]], 0x129fd8, 0.42, false),
|
||||||
|
createCrystalFacet([[0.05, 1.83], [0.3, 1.32], [1.29, -1.68], [0.84, -1.68]], 0x082f76, 0.6, false),
|
||||||
|
createCrystalFacet([[-1.36, -1.68], [1.29, -1.68], [0.92, -1.23], [-1.02, -1.23]], 0x2d6fae, 0.38, false),
|
||||||
|
createCrystalFacet([[0.02, 1.68], [0.25, 1.22], [-0.1, 0.84], [-0.25, 1.27]], 0xf8fdff, 0.18, true, 0.64),
|
||||||
|
createCrystalFacet([[-0.25, 1.27], [-1.2, -1.34], [-0.56, -0.61], [-0.1, 0.84]], 0x17a9df, 0.48, false),
|
||||||
|
createCrystalFacet([[-1.23, -1.36], [-0.56, -0.61], [0.18, -1.38]], 0x168bc9, 0.44, false, 0.646),
|
||||||
|
createCrystalFacet([[-0.1, 0.84], [-0.56, -0.61], [0.2, -1.39], [0.46, -0.02]], 0x4ba9dd, 0.46, false),
|
||||||
|
createCrystalFacet([[-0.56, -0.61], [0.46, -0.02], [0.18, -1.38]], 0xbfefff, 0.28, false, 0.648),
|
||||||
|
createCrystalFacet([[-0.72, -0.82], [0.04, 0.74], [0.38, 0.04], [-0.02, -0.72]], 0xd5f5ff, 0.34, false, 0.638),
|
||||||
|
createCrystalFacet([[0.25, 1.22], [0.64, 0.55], [0.46, -0.02], [-0.1, 0.84]], 0xfff8e8, 0.18, true, 0.64),
|
||||||
|
createCrystalFacet([[0.46, -0.02], [0.64, 0.55], [1.12, -1.34], [0.2, -1.39]], 0x0a376e, 0.54, false),
|
||||||
|
createCrystalFacet([[-1.2, -1.34], [0.2, -1.39], [1.12, -1.34], [0.82, -1.04], [-0.86, -1.02]], 0x5acff4, 0.25, true, 0.64),
|
||||||
|
createCrystalFacet([[0.3, 1.32], [0.64, 0.55], [1.12, -1.34], [1.29, -1.68]], 0x06265b, 0.44, false),
|
||||||
|
createCrystalFacet([[0.38, 0.04], [0.72, -0.36], [1.04, -1.2], [0.18, -1.32]], 0xffc982, 0.13, true, 0.644),
|
||||||
|
createCrystalFacet([[-0.17, 1.44], [-0.1, 0.84], [-0.56, -0.61], [-0.9, -1.68]], 0xa7edff, 0.16, true, 0.64),
|
||||||
|
createCrystalFacet([[0.02, 1.68], [0.3, 1.32], [0.25, 1.22]], 0xffffff, 0.25, true, 0.645),
|
||||||
|
createCrystalFacet([[0.02, 1.7], [0.29, 1.31], [0.13, 1.04], [-0.06, 1.35]], 0xff82c8, 0.16, true, 0.65),
|
||||||
|
createCrystalFacet([[0.38, -0.03], [0.83, -0.54], [1.03, -1.16], [0.64, -0.8]], 0xffb867, 0.13, true, 0.651)
|
||||||
|
];
|
||||||
|
const reflectionMaterials = crystalFacets.map(function (entry) { return entry.material; });
|
||||||
|
|
||||||
|
const spectrumGeometry = trackGeometry(new THREE.ShapeGeometry(crystalShape));
|
||||||
|
spectrumGeometry.translate(0, -0.22, 0);
|
||||||
|
const spectrumMaterial = trackMaterial(new THREE.ShaderMaterial({
|
||||||
|
uniforms: {
|
||||||
|
uTime: uniforms.uTime,
|
||||||
|
uDark: uniforms.uDark
|
||||||
|
},
|
||||||
|
transparent: true,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
vertexShader: `
|
||||||
|
varying vec2 vUv;
|
||||||
|
varying vec3 vLocal;
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
vLocal = position;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fragmentShader: `
|
||||||
|
precision highp float;
|
||||||
|
varying vec2 vUv;
|
||||||
|
varying vec3 vLocal;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uDark;
|
||||||
|
|
||||||
|
vec3 spectrum(float t) {
|
||||||
|
return 0.56 + 0.44 * cos(6.28318 * (vec3(0.02, 0.35, 0.68) + t));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float diagonalA = exp(-abs(vLocal.y - (0.37 * vLocal.x - 0.64)) * 24.0);
|
||||||
|
float diagonalB = exp(-abs(vLocal.y - (-0.22 * vLocal.x - 0.22)) * 34.0);
|
||||||
|
float spectralShard = pow(max(0.0, sin(vLocal.x * 8.4 - vLocal.y * 5.7 + uTime * 0.42)), 44.0);
|
||||||
|
float strength = diagonalA * 0.2 + diagonalB * 0.12 + spectralShard * 0.075;
|
||||||
|
vec3 color = spectrum(vLocal.x * 0.16 + vLocal.y * 0.08 + uTime * 0.018);
|
||||||
|
color += vec3(0.76, 0.94, 1.0) * diagonalA * 0.42;
|
||||||
|
gl_FragColor = vec4(color * strength, strength * mix(0.66, 0.9, uDark));
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}));
|
||||||
|
const spectrumMesh = new THREE.Mesh(spectrumGeometry, spectrumMaterial);
|
||||||
|
spectrumMesh.position.z = 0.655;
|
||||||
|
spectrumMesh.renderOrder = 5;
|
||||||
|
spectrumMesh.userData.noReflection = true;
|
||||||
|
|
||||||
|
function createLightStreak(start, end, color, width, opacity, z) {
|
||||||
|
const deltaX = end[0] - start[0];
|
||||||
|
const deltaY = end[1] - start[1];
|
||||||
|
const length = Math.hypot(deltaX, deltaY);
|
||||||
|
const group = new THREE.Group();
|
||||||
|
[
|
||||||
|
{ scale: 7.2, alpha: opacity * 0.12, z: (z || 0.67) - 0.006 },
|
||||||
|
{ scale: 2.8, alpha: opacity * 0.28, z: (z || 0.67) - 0.002 },
|
||||||
|
{ scale: 1, alpha: opacity, z: z || 0.67 }
|
||||||
|
].forEach(function (layer) {
|
||||||
|
const geometry = trackGeometry(new THREE.PlaneGeometry(length, width * layer.scale));
|
||||||
|
const material = trackMaterial(new THREE.MeshBasicMaterial({
|
||||||
|
color,
|
||||||
|
transparent: true,
|
||||||
|
opacity: layer.alpha,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
}));
|
||||||
|
const beam = new THREE.Mesh(geometry, material);
|
||||||
|
beam.position.set((start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, layer.z);
|
||||||
|
beam.rotation.z = Math.atan2(deltaY, deltaX);
|
||||||
|
beam.renderOrder = 6;
|
||||||
|
group.add(beam);
|
||||||
|
});
|
||||||
|
group.userData.noReflection = true;
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cyanStreak = createLightStreak([-1.42, -1.03], [1.32, -0.16], 0x8df6ff, 0.011, 0.92);
|
||||||
|
const warmStreak = createLightStreak([0.24, -0.13], [1.47, -0.4], 0xffc36c, 0.012, 0.82, 0.69);
|
||||||
|
const violetStreak = createLightStreak([-0.38, -0.52], [0.64, -0.75], 0xb478ff, 0.009, 0.68, 0.69);
|
||||||
|
const rainbowStreak = createLightStreak([-0.08, 0.42], [0.62, 0.04], 0xff78d7, 0.007, 0.48, 0.695);
|
||||||
|
|
||||||
|
const sparklePositions = [];
|
||||||
|
const sparkleCount = lowPower ? 26 : 74;
|
||||||
|
for (let index = 0; index < sparkleCount; index += 1) {
|
||||||
|
const seedA = Math.abs(Math.sin(index * 91.73 + 0.47));
|
||||||
|
const seedB = Math.abs(Math.sin(index * 37.19 + 1.21));
|
||||||
|
const y = -1.57 + seedA * 2.28;
|
||||||
|
const widthAtY = Math.max(0.08, 1.18 * (1 - (y + 1.57) / 3.4));
|
||||||
|
sparklePositions.push((seedB * 2 - 1) * widthAtY, y, 0.69 + (index % 5) * 0.001);
|
||||||
|
}
|
||||||
|
const sparkleGeometry = trackGeometry(new THREE.BufferGeometry());
|
||||||
|
sparkleGeometry.setAttribute('position', new THREE.Float32BufferAttribute(sparklePositions, 3));
|
||||||
|
const sparkleMaterial = trackMaterial(new THREE.ShaderMaterial({
|
||||||
|
uniforms: { uTime: uniforms.uTime, uDark: uniforms.uDark },
|
||||||
|
transparent: true,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
vertexShader: `
|
||||||
|
uniform float uTime;
|
||||||
|
varying float vPulse;
|
||||||
|
void main() {
|
||||||
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||||||
|
vPulse = 0.55 + 0.45 * sin(uTime * 1.9 + position.x * 11.0 + position.y * 7.0);
|
||||||
|
gl_PointSize = (0.075 + vPulse * 0.085) * (260.0 / -mvPosition.z);
|
||||||
|
gl_Position = projectionMatrix * mvPosition;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fragmentShader: `
|
||||||
|
precision highp float;
|
||||||
|
varying float vPulse;
|
||||||
|
uniform float uDark;
|
||||||
|
void main() {
|
||||||
|
float d = length(gl_PointCoord - 0.5);
|
||||||
|
float alpha = smoothstep(0.5, 0.06, d) * vPulse * mix(0.34, 0.72, uDark);
|
||||||
|
vec3 color = mix(vec3(0.48, 0.88, 1.0), vec3(1.0, 0.72, 0.48), vPulse * 0.34);
|
||||||
|
gl_FragColor = vec4(color, alpha);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}));
|
||||||
|
const crystalSparkles = new THREE.Points(sparkleGeometry, sparkleMaterial);
|
||||||
|
crystalSparkles.renderOrder = 7;
|
||||||
|
crystalSparkles.userData.noReflection = true;
|
||||||
|
|
||||||
|
const edgeGeometry = trackGeometry(new THREE.EdgesGeometry(crystalGeometry, 16));
|
||||||
|
const edgeMaterial = trackMaterial(new THREE.LineBasicMaterial({
|
||||||
|
color: 0xc5f3ff,
|
||||||
|
transparent: true,
|
||||||
|
depthTest: false,
|
||||||
|
opacity: 0.72
|
||||||
|
}));
|
||||||
|
const crystalEdges = new THREE.LineSegments(edgeGeometry, edgeMaterial);
|
||||||
|
crystalEdges.renderOrder = 7;
|
||||||
|
|
||||||
|
const glintGeometry = trackGeometry(new THREE.PlaneGeometry(1.15, 1.15));
|
||||||
|
const glintMaterial = trackMaterial(new THREE.ShaderMaterial({
|
||||||
|
uniforms: { uTime: uniforms.uTime, uDark: uniforms.uDark },
|
||||||
|
transparent: true,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
vertexShader: `varying vec2 vUv; void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
|
||||||
|
fragmentShader: `
|
||||||
|
precision highp float;
|
||||||
|
varying vec2 vUv;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uDark;
|
||||||
|
void main(){
|
||||||
|
vec2 p=vUv-0.5;
|
||||||
|
float core=exp(-length(p)*24.0);
|
||||||
|
float cross=exp(-abs(p.x)*95.0)*exp(-abs(p.y)*4.5)+exp(-abs(p.y)*95.0)*exp(-abs(p.x)*4.5);
|
||||||
|
float pulse=0.86+0.14*sin(uTime*1.6);
|
||||||
|
vec3 color=mix(vec3(0.42,0.86,1.0),vec3(1.0,0.42,0.92),smoothstep(-0.2,0.18,p.x));
|
||||||
|
gl_FragColor=vec4(color*(core*1.7+cross*0.72)*pulse,(core+cross*0.54)*mix(0.72,1.0,uDark));
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}));
|
||||||
|
const baseGlint = new THREE.Mesh(glintGeometry, glintMaterial);
|
||||||
|
baseGlint.position.set(-0.18, -1.6, 0.86);
|
||||||
|
baseGlint.scale.setScalar(0.94);
|
||||||
|
baseGlint.renderOrder = 8;
|
||||||
|
|
||||||
|
const crystalGroup = new THREE.Group();
|
||||||
|
crystalGroup.add(
|
||||||
|
crystalMesh,
|
||||||
|
...crystalFacets.map(function (entry) { return entry.facet; }),
|
||||||
|
spectrumMesh,
|
||||||
|
cyanStreak,
|
||||||
|
warmStreak,
|
||||||
|
violetStreak,
|
||||||
|
rainbowStreak,
|
||||||
|
crystalSparkles,
|
||||||
|
crystalEdges,
|
||||||
|
baseGlint
|
||||||
|
);
|
||||||
|
crystalGroup.rotation.set(0.045, -0.44, -0.018);
|
||||||
|
|
||||||
|
// Preserve the authored, high-frequency refraction from the approved visual
|
||||||
|
// while keeping it inside the live Three.js composition. The procedural
|
||||||
|
// crystal underneath supplies parallax, glow, reflection and the dark theme.
|
||||||
|
let referenceTexture = null;
|
||||||
|
const referenceGeometry = trackGeometry(new THREE.PlaneGeometry(3.29, 3.7));
|
||||||
|
const referenceMaterial = trackMaterial(new THREE.MeshBasicMaterial({
|
||||||
|
transparent: true,
|
||||||
|
opacity: initialDark ? 0.92 : 0.97,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false,
|
||||||
|
toneMapped: false
|
||||||
|
}));
|
||||||
|
const referenceCrystal = new THREE.Mesh(referenceGeometry, referenceMaterial);
|
||||||
|
referenceCrystal.position.set(-0.29, 0.05, 1.28);
|
||||||
|
referenceCrystal.renderOrder = 24;
|
||||||
|
referenceCrystal.userData.noReflection = true;
|
||||||
|
|
||||||
|
const referenceReflectionMaterial = trackMaterial(new THREE.ShaderMaterial({
|
||||||
|
uniforms: {
|
||||||
|
uMap: { value: null },
|
||||||
|
uOpacity: { value: initialDark ? 0.14 : 0.22 }
|
||||||
|
},
|
||||||
|
transparent: true,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false,
|
||||||
|
toneMapped: false,
|
||||||
|
vertexShader: `
|
||||||
|
varying vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fragmentShader: `
|
||||||
|
precision highp float;
|
||||||
|
varying vec2 vUv;
|
||||||
|
uniform sampler2D uMap;
|
||||||
|
uniform float uOpacity;
|
||||||
|
void main() {
|
||||||
|
vec4 sampleColor = texture2D(uMap, vUv);
|
||||||
|
float fade = pow(clamp(1.0 - vUv.y, 0.0, 1.0), 2.1);
|
||||||
|
vec3 reflected = mix(sampleColor.rgb, vec3(0.56, 0.76, 1.0), 0.38);
|
||||||
|
gl_FragColor = vec4(reflected, sampleColor.a * fade * uOpacity);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}));
|
||||||
|
const referenceReflection = new THREE.Mesh(referenceGeometry, referenceReflectionMaterial);
|
||||||
|
referenceReflection.position.set(-0.29, -3.06, 0.96);
|
||||||
|
referenceReflection.scale.set(1, -0.68, 1);
|
||||||
|
referenceReflection.renderOrder = 2;
|
||||||
|
referenceReflection.visible = false;
|
||||||
|
|
||||||
|
new THREE.TextureLoader().load(
|
||||||
|
'./assets/prism-reference-light.png',
|
||||||
|
function (texture) {
|
||||||
|
referenceTexture = texture;
|
||||||
|
texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||||
|
texture.magFilter = THREE.LinearFilter;
|
||||||
|
referenceMaterial.map = texture;
|
||||||
|
referenceMaterial.needsUpdate = true;
|
||||||
|
referenceReflectionMaterial.uniforms.uMap.value = texture;
|
||||||
|
referenceReflection.visible = true;
|
||||||
|
root.dataset.referenceCrystal = 'loaded';
|
||||||
|
renderFrame(performance.now());
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
function () {
|
||||||
|
referenceCrystal.visible = false;
|
||||||
|
root.dataset.referenceCrystal = 'error';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const crystalReflection = crystalGroup.clone(true);
|
||||||
|
crystalReflection.traverse(function (object) {
|
||||||
|
if (object.userData.noReflection) {
|
||||||
|
object.visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!object.material) return;
|
||||||
|
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
||||||
|
const clonedMaterials = materials.map(function (material) {
|
||||||
|
const clone = trackMaterial(material.clone());
|
||||||
|
clone.transparent = true;
|
||||||
|
clone.depthWrite = false;
|
||||||
|
clone.depthTest = false;
|
||||||
|
clone.opacity = material.isLineBasicMaterial ? 0.08 : Math.min(0.14, (material.opacity || 1) * 0.16);
|
||||||
|
if (clone.isMeshPhysicalMaterial) {
|
||||||
|
clone.transmission = 0;
|
||||||
|
clone.roughness = 0.24;
|
||||||
|
clone.metalness = 0.08;
|
||||||
|
clone.color = new THREE.Color(0x5ba8ff);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
});
|
||||||
|
object.material = Array.isArray(object.material) ? clonedMaterials : clonedMaterials[0];
|
||||||
|
object.renderOrder = 0;
|
||||||
|
});
|
||||||
|
crystalReflection.scale.set(1, -1, 1);
|
||||||
|
crystalReflection.position.y = -3.42;
|
||||||
|
|
||||||
|
const floorEffects = new THREE.Group();
|
||||||
|
function createFloorRay(points, color, opacity) {
|
||||||
|
const geometry = trackGeometry(new THREE.BufferGeometry());
|
||||||
|
const positions = [];
|
||||||
|
points.forEach(function (point) { positions.push(point[0], -1.705, point[1]); });
|
||||||
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||||
|
geometry.computeVertexNormals();
|
||||||
|
const material = trackMaterial(new THREE.MeshBasicMaterial({
|
||||||
|
color,
|
||||||
|
transparent: true,
|
||||||
|
opacity,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthWrite: false,
|
||||||
|
blending: THREE.AdditiveBlending
|
||||||
|
}));
|
||||||
|
material.userData.lightOpacity = opacity;
|
||||||
|
material.userData.darkOpacity = opacity * 1.35;
|
||||||
|
const ray = new THREE.Mesh(geometry, material);
|
||||||
|
floorEffects.add(ray);
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
const floorRayMaterials = [
|
||||||
|
createFloorRay([[-0.95, 0.2], [-5.6, 4.8], [-2.7, 4.8]], 0x6cecff, 0.2),
|
||||||
|
createFloorRay([[-0.2, 0.3], [-2.2, 5.4], [-0.55, 5.4]], 0x7f6dff, 0.14),
|
||||||
|
createFloorRay([[0.54, 0.14], [1.2, 5.5], [3.6, 5.5]], 0xffd58b, 0.16),
|
||||||
|
createFloorRay([[0.72, 0.12], [3.4, 4.6], [5.4, 4.6]], 0x4f83ff, 0.12)
|
||||||
|
];
|
||||||
|
|
||||||
|
const worldRoot = new THREE.Group();
|
||||||
|
worldRoot.add(floorEffects, crystalReflection, referenceReflection, crystalGroup, referenceCrystal);
|
||||||
|
worldScene.add(worldRoot);
|
||||||
|
|
||||||
|
const floorGeometry = trackGeometry(new THREE.PlaneGeometry(10, 10));
|
||||||
|
const floorMaterial = trackMaterial(new THREE.MeshPhysicalMaterial({
|
||||||
|
color: 0xd8e8ff,
|
||||||
|
roughness: 0.2,
|
||||||
|
metalness: 0.08,
|
||||||
|
envMapIntensity: 1.6,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.22,
|
||||||
|
depthWrite: false,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
}));
|
||||||
|
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
|
||||||
|
floor.rotation.x = -Math.PI / 2;
|
||||||
|
floor.position.set(0, -1.71, 0);
|
||||||
|
floor.receiveShadow = !lowPower;
|
||||||
|
worldScene.add(floor);
|
||||||
|
|
||||||
|
const floorShadowMaterial = trackMaterial(new THREE.ShadowMaterial({ color: 0x0b3472, opacity: 0.18 }));
|
||||||
|
const floorShadow = new THREE.Mesh(floorGeometry, floorShadowMaterial);
|
||||||
|
floorShadow.rotation.x = -Math.PI / 2;
|
||||||
|
floorShadow.position.set(0, -1.695, 0);
|
||||||
|
floorShadow.receiveShadow = !lowPower;
|
||||||
|
worldScene.add(floorShadow);
|
||||||
|
|
||||||
|
const restPointer = new THREE.Vector2(0.58, 0.44);
|
||||||
|
const targetMouse = restPointer.clone();
|
||||||
|
const currentMouse = targetMouse.clone();
|
||||||
|
const lightEdgeColor = new THREE.Color(0xbfeeff);
|
||||||
|
const darkEdgeColor = new THREE.Color(0x73e9ff);
|
||||||
|
const lightFloorColor = new THREE.Color(0xd9eaff);
|
||||||
|
const darkFloorColor = new THREE.Color(0x0b2746);
|
||||||
|
const lightReferenceColor = new THREE.Color(0xffffff);
|
||||||
|
const darkReferenceColor = new THREE.Color(0xb7dbff);
|
||||||
|
const currentColor = new THREE.Color();
|
||||||
|
let targetDark = initialDark;
|
||||||
|
let heroVisible = true;
|
||||||
|
let lastFrame = 0;
|
||||||
|
let frameInterval = staticMode ? Infinity : (window.innerWidth < 900 ? 1000 / 30 : 1000 / 45);
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
function updateWorld(elapsed) {
|
||||||
|
const dark = uniforms.uDark.value;
|
||||||
|
const floatOffset = staticMode ? 0 : Math.sin(elapsed * 0.48) * 0.075;
|
||||||
|
crystalGroup.position.y = floatOffset;
|
||||||
|
crystalGroup.rotation.x = 0.02 + Math.sin(elapsed * 0.28) * 0.025 + (currentMouse.y - 0.5) * 0.1;
|
||||||
|
crystalGroup.rotation.y = -0.44 + Math.sin(elapsed * 0.18) * 0.035 + (currentMouse.x - 0.5) * 0.1;
|
||||||
|
crystalGroup.rotation.z = -0.018 + Math.sin(elapsed * 0.22) * 0.012;
|
||||||
|
const pointerTiltX = (0.5 - currentMouse.y) * 0.16;
|
||||||
|
const pointerTiltY = (currentMouse.x - 0.5) * 0.28;
|
||||||
|
const pointerShiftX = (currentMouse.x - 0.5) * 0.11;
|
||||||
|
const pointerShiftY = (0.5 - currentMouse.y) * 0.06;
|
||||||
|
referenceCrystal.position.x = -0.29 + pointerShiftX;
|
||||||
|
referenceCrystal.position.y = 0.05 + floatOffset * 0.72 + pointerShiftY;
|
||||||
|
referenceCrystal.rotation.x = pointerTiltX;
|
||||||
|
referenceCrystal.rotation.y = pointerTiltY;
|
||||||
|
referenceCrystal.rotation.z = staticMode ? 0 : Math.sin(elapsed * 0.22) * 0.0035;
|
||||||
|
referenceReflection.position.x = -0.29 + pointerShiftX * 0.7;
|
||||||
|
referenceReflection.position.y = -3.06 - floatOffset * 0.46;
|
||||||
|
referenceReflection.rotation.x = -pointerTiltX * 0.48;
|
||||||
|
referenceReflection.rotation.y = pointerTiltY * 0.62;
|
||||||
|
referenceReflection.rotation.z = -referenceCrystal.rotation.z * 0.5;
|
||||||
|
referenceMaterial.opacity = THREE.MathUtils.lerp(0.97, 0.92, dark);
|
||||||
|
referenceMaterial.color.copy(currentColor.copy(lightReferenceColor).lerp(darkReferenceColor, dark));
|
||||||
|
referenceCrystal.visible = Boolean(referenceMaterial.map);
|
||||||
|
referenceReflectionMaterial.uniforms.uOpacity.value = THREE.MathUtils.lerp(0.22, 0.14, dark);
|
||||||
|
referenceReflection.visible = Boolean(referenceMaterial.map);
|
||||||
|
crystalGroup.visible = false;
|
||||||
|
crystalReflection.visible = false;
|
||||||
|
crystalReflection.rotation.copy(crystalGroup.rotation);
|
||||||
|
crystalReflection.position.y = -3.42 - floatOffset;
|
||||||
|
worldRoot.rotation.y = (currentMouse.x - 0.5) * 0.03;
|
||||||
|
worldRoot.rotation.x = (0.5 - currentMouse.y) * 0.02;
|
||||||
|
|
||||||
|
crystalMaterials.forEach(function (material) {
|
||||||
|
material.color.copy(currentColor.copy(material.userData.lightColor).lerp(material.userData.darkColor, dark));
|
||||||
|
material.opacity = THREE.MathUtils.lerp(material.userData.lightOpacity, material.userData.darkOpacity, dark);
|
||||||
|
material.envMapIntensity = THREE.MathUtils.lerp(material.userData.lightEnv, material.userData.darkEnv, dark);
|
||||||
|
});
|
||||||
|
glassSideMaterial.emissiveIntensity = THREE.MathUtils.lerp(0.12, 0.2, dark);
|
||||||
|
reflectionMaterials.forEach(function (material) {
|
||||||
|
material.opacity = THREE.MathUtils.lerp(material.userData.lightOpacity, material.userData.darkOpacity, dark);
|
||||||
|
});
|
||||||
|
floorRayMaterials.forEach(function (material) {
|
||||||
|
material.opacity = THREE.MathUtils.lerp(material.userData.lightOpacity, material.userData.darkOpacity, dark);
|
||||||
|
});
|
||||||
|
edgeMaterial.color.copy(currentColor.copy(lightEdgeColor).lerp(darkEdgeColor, dark));
|
||||||
|
edgeMaterial.opacity = THREE.MathUtils.lerp(0.72, 0.9, dark);
|
||||||
|
floorMaterial.color.copy(currentColor.copy(lightFloorColor).lerp(darkFloorColor, dark));
|
||||||
|
floorMaterial.opacity = THREE.MathUtils.lerp(0.22, 0.13, dark);
|
||||||
|
floorMaterial.envMapIntensity = THREE.MathUtils.lerp(1.6, 2.4, dark);
|
||||||
|
floorShadowMaterial.opacity = THREE.MathUtils.lerp(0.16, 0.3, dark);
|
||||||
|
hemisphereLight.intensity = THREE.MathUtils.lerp(1.8, 1.25, dark);
|
||||||
|
keyLight.intensity = THREE.MathUtils.lerp(2.4, 2.6, dark);
|
||||||
|
cyanLight.intensity = THREE.MathUtils.lerp(18, 26, dark);
|
||||||
|
warmLight.intensity = THREE.MathUtils.lerp(13, 20, dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFrame(now) {
|
||||||
|
if (!staticMode && (!heroVisible || document.hidden || now - lastFrame < frameInterval)) return;
|
||||||
|
lastFrame = now;
|
||||||
|
const elapsed = staticMode ? 1.4 : (now - startTime) / 1000;
|
||||||
|
if (staticMode) {
|
||||||
|
uniforms.uDark.value = targetDark;
|
||||||
|
uniforms.uTime.value = 1.4;
|
||||||
|
} else {
|
||||||
|
uniforms.uDark.value += (targetDark - uniforms.uDark.value) * 0.085;
|
||||||
|
currentMouse.lerp(targetMouse, 0.055);
|
||||||
|
uniforms.uMouse.value.copy(currentMouse);
|
||||||
|
uniforms.uTime.value = elapsed;
|
||||||
|
}
|
||||||
|
updateWorld(elapsed);
|
||||||
|
renderer.clear(true, true, true);
|
||||||
|
renderer.render(backgroundScene, backgroundCamera);
|
||||||
|
renderer.clearDepth();
|
||||||
|
renderer.render(worldScene, worldCamera);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
const rect = root.getBoundingClientRect();
|
||||||
|
const width = Math.max(1, Math.round(rect.width));
|
||||||
|
const height = Math.max(1, Math.round(rect.height));
|
||||||
|
const maxDpr = window.innerWidth < 900 ? 1 : 1.5;
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, maxDpr));
|
||||||
|
renderer.setSize(width, height, false);
|
||||||
|
uniforms.uResolution.value.set(width * renderer.getPixelRatio(), height * renderer.getPixelRatio());
|
||||||
|
|
||||||
|
const aspect = width / height;
|
||||||
|
worldCamera.aspect = aspect;
|
||||||
|
worldCamera.fov = aspect < 0.75 ? 38 : 33;
|
||||||
|
worldCamera.updateProjectionMatrix();
|
||||||
|
if (aspect > 1.1) {
|
||||||
|
worldRoot.position.set(Math.min(1.92, aspect * 1.15), -0.02, 0);
|
||||||
|
worldRoot.scale.setScalar(0.9);
|
||||||
|
} else {
|
||||||
|
worldRoot.position.set(0.08, -1.55, 0);
|
||||||
|
worldRoot.scale.setScalar(0.56);
|
||||||
|
}
|
||||||
|
floor.position.set(worldRoot.position.x, worldRoot.position.y - 1.71 * worldRoot.scale.x, 0);
|
||||||
|
floorShadow.position.set(worldRoot.position.x, worldRoot.position.y - 1.695 * worldRoot.scale.x, 0);
|
||||||
|
frameInterval = staticMode ? Infinity : (width < 900 ? 1000 / 30 : 1000 / 45);
|
||||||
|
renderFrame(performance.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(resize);
|
||||||
|
resizeObserver.observe(root);
|
||||||
|
|
||||||
|
const hero = document.getElementById('home');
|
||||||
|
if (hero) {
|
||||||
|
hero.addEventListener('pointermove', function (event) {
|
||||||
|
if (staticMode) return;
|
||||||
|
const rect = hero.getBoundingClientRect();
|
||||||
|
targetMouse.set(
|
||||||
|
THREE.MathUtils.clamp((event.clientX - rect.left) / rect.width, 0, 1),
|
||||||
|
THREE.MathUtils.clamp((event.clientY - rect.top) / rect.height, 0, 1)
|
||||||
|
);
|
||||||
|
}, { passive: true });
|
||||||
|
hero.addEventListener('pointerleave', function () { targetMouse.copy(restPointer); }, { passive: true });
|
||||||
|
|
||||||
|
const visibilityObserver = new IntersectionObserver(function (entries) {
|
||||||
|
heroVisible = entries[0] ? entries[0].isIntersecting : true;
|
||||||
|
if (heroVisible && staticMode) renderFrame(performance.now());
|
||||||
|
}, { rootMargin: '120px 0px' });
|
||||||
|
visibilityObserver.observe(hero);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('roncarve:themechange', function (event) {
|
||||||
|
targetDark = event.detail.resolved === 'dark' ? 1 : 0;
|
||||||
|
if (staticMode) renderFrame(performance.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('webglcontextlost', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
root.classList.remove('is-ready');
|
||||||
|
root.classList.add('has-error');
|
||||||
|
renderer.setAnimationLoop(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
resize();
|
||||||
|
renderFrame(performance.now());
|
||||||
|
root.classList.add('is-ready');
|
||||||
|
if (!staticMode) renderer.setAnimationLoop(renderFrame);
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', function () {
|
||||||
|
renderer.setAnimationLoop(null);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
backgroundGeometry.dispose();
|
||||||
|
backgroundMaterial.dispose();
|
||||||
|
disposableGeometries.forEach(function (geometry) { geometry.dispose(); });
|
||||||
|
disposableMaterials.forEach(function (material) { material.dispose(); });
|
||||||
|
if (referenceTexture) referenceTexture.dispose();
|
||||||
|
environmentTarget.dispose();
|
||||||
|
renderer.dispose();
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+6
File diff suppressed because one or more lines are too long
Vendored
+6
File diff suppressed because one or more lines are too long
+181
@@ -0,0 +1,181 @@
|
|||||||
|
# Roncarve 官网科技感改造研究
|
||||||
|
|
||||||
|
> 研究对象:`index.html` 当前单文件静态官网
|
||||||
|
> 目标:在保持企业可信度和信息可读性的前提下,提升科技感、现代感与品牌辨识度。
|
||||||
|
> 研究日期:2026-08-13
|
||||||
|
|
||||||
|
## 一、结论先行
|
||||||
|
|
||||||
|
当前页面已经具备深色 Hero、蓝青渐变、点阵/网格背景、玻璃拟态导航和基础滚动交互,因此下一步不应继续堆叠发光、渐变和动画,而应建立一套更统一的“数据与 AI 基础设施”视觉叙事:让每个视觉效果都服务于内容、产品能力或用户行动。
|
||||||
|
|
||||||
|
最值得优先改造的方向是:
|
||||||
|
|
||||||
|
1. 将首屏从“宣传口号 + 装饰背景”升级为可理解的 AI / Data 系统可视化场景。
|
||||||
|
2. 用更有辨识度的字体、数字、状态标签、坐标/节点/数据流等设计 token,形成品牌语言。
|
||||||
|
3. 用滚动触发的分层揭示、卡片 hover 状态和轻量粒子/流光表达“系统在运行”,但限制动画在 `transform` 和 `opacity` 上。
|
||||||
|
4. 强化移动端、键盘焦点、对比度和 `prefers-reduced-motion`,否则科技感会以可用性为代价。
|
||||||
|
|
||||||
|
## 二、对当前 `index.html` 的快速诊断
|
||||||
|
|
||||||
|
### 已有优势
|
||||||
|
|
||||||
|
- 深色 Hero 和蓝 / 青 / 紫渐变已经建立了 AI 产品常见的技术语义。
|
||||||
|
- `backdrop-filter`、网格背景、渐变文字和 pulse 动效可作为视觉基础。
|
||||||
|
- 页面结构清晰,包含 Hero、关于、服务、优势、办公室、联系 CTA,适合做叙事化改造。
|
||||||
|
- 单文件结构便于快速试错,不需要先引入复杂框架。
|
||||||
|
|
||||||
|
### 主要问题与风险
|
||||||
|
|
||||||
|
- 当前风格容易落入“通用 SaaS / AI 模板”:深色背景、网格、玻璃卡片和蓝色按钮缺少品牌专属符号。
|
||||||
|
- Hero 右侧视觉如果主要是静态装饰,用户无法快速理解“公司具体解决什么问题”。
|
||||||
|
- 页面中的视觉语言还没有形成完整 token:圆角、边框亮度、光晕强度、字体层级和动效节奏可能各自为政。
|
||||||
|
- `backdrop-filter` 兼容性虽已明显改善,但仍应提供不依赖模糊的降级样式;MDN 将其标记为 Baseline 2024,并提示旧设备可能不支持。[MDN:backdrop-filter](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/backdrop-filter)
|
||||||
|
- 如果继续增加粒子、视差和常驻动画,可能造成性能和眩晕问题。web.dev 建议优先动画 `transform` / `opacity`,避免频繁改变布局和绘制属性。[web.dev:Animations and performance](https://web.dev/articles/animations-and-performance)
|
||||||
|
|
||||||
|
## 三、让官网更具科技感的设计方法
|
||||||
|
|
||||||
|
### 1. 建立“技术叙事”,而不是只做装饰
|
||||||
|
|
||||||
|
科技感的核心是让用户感受到系统、连接、数据和实时状态。建议把抽象能力改成可视化表达:
|
||||||
|
|
||||||
|
- 数据治理:数据节点、校验状态、流向、分层标签。
|
||||||
|
- LLM / AI:输入、检索、推理、输出的链路。
|
||||||
|
- 私有化部署:本地节点、权限边界、加密状态、部署区域。
|
||||||
|
- 企业服务:从业务问题到自动化结果的流程轨迹。
|
||||||
|
|
||||||
|
推荐 Hero 结构:左侧一句明确的价值主张,右侧一个“AI 数据运行台”视觉组件,包含节点连线、实时状态、指标卡和一条可控的流光路径。这样比单纯的发光球体更能证明技术能力。
|
||||||
|
|
||||||
|
### 2. 从“蓝色渐变”升级为可识别的视觉系统
|
||||||
|
|
||||||
|
建议定义一组 CSS variables:
|
||||||
|
|
||||||
|
- 基底:`--ink-950`、`--ink-900`、`--surface-800`,避免所有区域都是同一块黑。
|
||||||
|
- 强调色:保留青蓝作为主色,再加入一个克制的紫色或酸性绿色作为“状态色”,不要平均使用。
|
||||||
|
- 边框:使用低透明度白色边框,重要组件使用单侧高亮或渐变边框。
|
||||||
|
- 光效:将 glow 分成 `ambient`、`interactive`、`status` 三档,避免全页面发光。
|
||||||
|
- 圆角:Hero 主组件、内容卡片、标签、按钮使用不同半径,形成层级。
|
||||||
|
|
||||||
|
视觉原则是“暗底 + 高亮信息 + 大面积留白 + 少量精确光效”,而不是“每个元素都发光”。
|
||||||
|
|
||||||
|
### 3. 增加“工程界面”细节
|
||||||
|
|
||||||
|
可在不影响阅读的前提下加入:
|
||||||
|
|
||||||
|
- `SYSTEM ONLINE`、`MODEL READY`、`PRIVATE DEPLOYMENT` 等状态标签。
|
||||||
|
- 章节编号、坐标标记、细分隔线、数据单位和微型指标。
|
||||||
|
- 卡片顶部的状态灯、右上角的版本号、底部的连接线或进度条。
|
||||||
|
- 以等宽字体展示少量技术元信息;正文继续使用易读的中文无衬线字体。
|
||||||
|
|
||||||
|
这些细节应承担“可信度”和“结构感”,不应伪造真实监控数据。若指标没有数据来源,使用能力标签或示意状态,不要使用容易被理解为真实业务数据的数字。
|
||||||
|
|
||||||
|
### 4. 使用更现代的版式节奏
|
||||||
|
|
||||||
|
- Hero 采用不对称布局:文字约 42%,可视化约 58%,制造“控制台 / 系统入口”的感觉。
|
||||||
|
- 服务区从六张同质卡片改成“1 个主能力 + 5 个辅助能力”,突出核心业务。
|
||||||
|
- 关于区加入一条横向能力链:Data → Model → Deploy → Operate。
|
||||||
|
- 办公室区可采用地图 / 节点式布局,但地图只作为辅助,重点放在服务覆盖和交付能力。
|
||||||
|
- CTA 避免再次重复泛化口号,改成明确动作,例如“预约一次 AI 场景诊断”。
|
||||||
|
|
||||||
|
## 四、推荐的交互与动效方案
|
||||||
|
|
||||||
|
### 优先级 A:低成本、高收益
|
||||||
|
|
||||||
|
- 首屏进入:文字、可视化面板、背景光分别以 80–160ms 的错峰淡入和轻微位移出现。
|
||||||
|
- 服务卡片 hover:边框亮度、顶部状态线、图标或节点路径发生变化。
|
||||||
|
- 滚动进入:章节标题和卡片使用 `IntersectionObserver` 添加一次性 reveal class。
|
||||||
|
- 导航滚动:保留毛玻璃,但增加当前章节的高亮轨迹或底部指示线。
|
||||||
|
- CTA hover:按钮出现短距离光扫或边框流动,不要无限循环。
|
||||||
|
|
||||||
|
### 优先级 B:现代感增强
|
||||||
|
|
||||||
|
- 使用 CSS `scroll-timeline` / `view-timeline` 做轻量滚动进度、节点连线或卡片揭示,并为不支持的浏览器保留普通静态布局。web.dev 明确提示 View Transitions 尚未在所有浏览器中 Baseline,必须准备无动画回退。[web.dev:View transitions for SPAs](https://web.dev/learn/css/view-transitions-spas?hl=en)
|
||||||
|
- 使用鼠标位置驱动 Hero 光晕或网格偏移,但限制在 Hero 内,移动端关闭。
|
||||||
|
- 用 SVG 或 CSS 绘制节点、连线和波形,优先于加载大型视频或 3D 库。
|
||||||
|
|
||||||
|
### 不建议一开始使用
|
||||||
|
|
||||||
|
- 全屏 WebGL / Three.js 粒子场:视觉吸引力强,但开发、移动端性能和可访问性成本高。
|
||||||
|
- 自动播放背景视频:加载成本高,容易掩盖内容,也需要处理静音、暂停和低带宽。
|
||||||
|
- 大量视差、快速闪烁、持续旋转:会分散注意力,且可能触发眩晕。
|
||||||
|
|
||||||
|
## 五、推荐技能与工具
|
||||||
|
|
||||||
|
### 适合本项目的 Codex skills
|
||||||
|
|
||||||
|
1. `product-design:index`:作为产品设计任务路由,帮助明确视觉目标和改造范围。
|
||||||
|
2. `product-design:ideate`:在正式改代码前生成 3 个视觉方向,例如“AI 控制台”“数据流网络”“极简企业科技”。适合比较整体风格,而不是直接凭文字猜方案。
|
||||||
|
3. `product-design:image-to-code`:如果选定了截图或生成的视觉参考,可将其转成响应式 HTML/CSS/JS。
|
||||||
|
4. `product-design:audit`:改造后通过截图检查层级、可读性、交互反馈和无障碍问题。
|
||||||
|
5. `imagegen`:仅用于生成 Hero 的抽象背景、技术纹理或产品概念图;UI 中的节点和状态面板应优先使用 HTML/SVG,确保可读、可控和可访问。
|
||||||
|
6. `browser:control-in-app-browser`:启动本地页面后检查桌面与移动端的真实渲染、滚动行为和交互状态。
|
||||||
|
7. `visualize`:如果要向团队展示数据流、AI 推理链路或部署架构,可制作可交互的说明图,而不是把所有信息塞入官网装饰。
|
||||||
|
|
||||||
|
### 推荐的前端实现工具
|
||||||
|
|
||||||
|
- 原生 CSS variables、CSS Grid、SVG:最适合当前单文件架构,依赖少、性能可控。
|
||||||
|
- `IntersectionObserver`:做滚动入场,不必引入完整动画库。
|
||||||
|
- CSS `@property`、mask、conic-gradient:制作进度环、扫描线和渐变边框。
|
||||||
|
- 需要复杂动画时再考虑 GSAP;需要真实 3D 场景时再考虑 Three.js,二者都应先做移动端性能验证。
|
||||||
|
- Lighthouse / Chrome DevTools Performance:验证动画帧率、LCP、布局抖动和移动端 CPU 压力。
|
||||||
|
|
||||||
|
## 六、性能与无障碍底线
|
||||||
|
|
||||||
|
- 动画优先使用 `transform` 和 `opacity`;避免持续动画 `width`、`height`、`top`、`left`、大面积 `filter`。
|
||||||
|
- 不要滥用 `will-change`,只在确实需要提升的交互元素上使用。
|
||||||
|
- 增加 `@media (prefers-reduced-motion: reduce)`,关闭粒子、视差、扫描线和大幅位移动画。MDN 和 W3C 都将该媒体查询作为减少动画影响的推荐做法。[MDN:prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/%40media/prefers-reduced-motion)、[W3C Technique C39](https://www.w3.org/WAI/WCAG22/Techniques/css/C39)
|
||||||
|
- 文本、按钮和状态标签要保持足够对比度。WCAG 2.2 对普通文本通常要求至少 4.5:1,并新增了更明确的 Focus Appearance 指引。[WCAG 2.2](https://www.w3.org/TR/WCAG22/)
|
||||||
|
- 所有发光按钮都要有清晰的 `:focus-visible` 状态,不能只依赖颜色变化。
|
||||||
|
- 移动端关闭鼠标跟随、降低背景复杂度、减少 `backdrop-filter` 层数,并保证内容在低端设备上仍然可读。
|
||||||
|
- 为 `backdrop-filter` 提供纯色半透明背景回退,例如 `background: rgba(8, 15, 30, .92)`。
|
||||||
|
|
||||||
|
## 七、建议的分阶段实施路线
|
||||||
|
|
||||||
|
### 第一阶段:视觉基础(半天至 1 天)
|
||||||
|
|
||||||
|
- 修正页面中文编码和标题文案。
|
||||||
|
- 重构颜色、间距、圆角、边框和阴影 token。
|
||||||
|
- 统一按钮、标签、卡片和章节标题的组件样式。
|
||||||
|
- 优化 Hero 信息层级和 CTA 文案。
|
||||||
|
|
||||||
|
### 第二阶段:科技叙事(1–2 天)
|
||||||
|
|
||||||
|
- 将 Hero 右侧改为 HTML/SVG 的 AI 数据运行台。
|
||||||
|
- 把服务卡片重组为“核心能力 + 能力链”。
|
||||||
|
- 增加状态标签、章节编号、能力流程和可信的业务信息。
|
||||||
|
|
||||||
|
### 第三阶段:轻量动效(半天至 1 天)
|
||||||
|
|
||||||
|
- 增加 IntersectionObserver reveal。
|
||||||
|
- 增加卡片 hover 状态、节点流光和 CTA 微交互。
|
||||||
|
- 增加 reduced-motion、移动端降级和浏览器兼容回退。
|
||||||
|
|
||||||
|
### 第四阶段:验证与打磨(半天)
|
||||||
|
|
||||||
|
- 以 375px、768px、1440px 三个宽度检查布局。
|
||||||
|
- 用键盘完成导航、按钮和语言切换操作。
|
||||||
|
- 检查 Lighthouse、控制台错误、动画帧率和首屏加载。
|
||||||
|
- 截图进行一次视觉审查,确认效果服务于内容,而不是遮挡内容。
|
||||||
|
|
||||||
|
## 八、推荐的改造方向
|
||||||
|
|
||||||
|
我建议当前官网优先采用“AI Data Control Room / AI 数据控制台”方向:
|
||||||
|
|
||||||
|
- 主色:深靛黑 + 电光青 + 少量紫色状态色。
|
||||||
|
- Hero:左侧价值主张,右侧数据节点 / 模型状态 / 私有部署边界。
|
||||||
|
- 内容:以 Data → Intelligence → Deployment → Operations 作为主叙事。
|
||||||
|
- 动效:节点连接、状态更新、滚动揭示和局部光扫。
|
||||||
|
- 质感:少量玻璃、细边框、等宽技术标签、大留白、明确层级。
|
||||||
|
- 约束:不依赖大型图片或 WebGL;全部视觉核心使用 HTML/CSS/SVG 实现。
|
||||||
|
|
||||||
|
这个方向与当前页面已有的深色、蓝青渐变和 AI 业务内容衔接成本最低,同时比通用网格背景更容易建立品牌记忆点。
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
- [MDN:backdrop-filter](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/backdrop-filter)
|
||||||
|
- [web.dev:Animations and performance](https://web.dev/articles/animations-and-performance)
|
||||||
|
- [web.dev:prefers-reduced-motion](https://web.dev/articles/prefers-reduced-motion?hl=en)
|
||||||
|
- [MDN:prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/%40media/prefers-reduced-motion)
|
||||||
|
- [W3C:Using the CSS prefers-reduced-motion query](https://www.w3.org/WAI/WCAG22/Techniques/css/C39)
|
||||||
|
- [W3C:WCAG 2.2](https://www.w3.org/TR/WCAG22/)
|
||||||
|
- [web.dev:View transitions for single-page applications](https://web.dev/learn/css/view-transitions-spas?hl=en)
|
||||||
|
|
||||||
Reference in New Issue
Block a user