Created
September 2, 2025 20:17
-
-
Save gszauer/1d86a1df16a34ac94c24d1e895dcdaad to your computer and use it in GitHub Desktop.
rainbowroad.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Rainbow Road</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| font-family: Arial, sans-serif; | |
| } | |
| canvas { | |
| display: block; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script> | |
| // Scene setup | |
| const scene = new THREE.Scene(); | |
| scene.fog = new THREE.Fog(0x000033, 10, 100); | |
| const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 2, 5); | |
| camera.lookAt(0, 0, -5); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| document.body.appendChild(renderer.domElement); | |
| // Rainbow Road Shader | |
| const vertexShader = ` | |
| varying vec2 vUv; | |
| void main() { | |
| vUv = uv; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| `; | |
| const fragmentShader = ` | |
| varying vec2 vUv; | |
| uniform float time; | |
| uniform vec2 resolution; | |
| vec3 hsv2rgb(vec3 c) { | |
| vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); | |
| vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); | |
| return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); | |
| } | |
| // Mario Kart 8 specific rainbow colors | |
| vec3 getRainbowColor(float t) { | |
| float segment = mod(t * 6.0, 6.0); | |
| vec3 colors[6]; | |
| colors[0] = vec3(1.0, 0.4, 0.7); // Light pink | |
| colors[1] = vec3(1.0, 0.95, 0.3); // Yellow | |
| colors[2] = vec3(0.3, 1.0, 0.6); // Turquoise | |
| colors[3] = vec3(0.3, 0.8, 1.0); // Cyan | |
| colors[4] = vec3(0.4, 0.4, 1.0); // Blue | |
| colors[5] = vec3(0.8, 0.3, 1.0); // Purple | |
| int idx = int(segment); | |
| float frac = fract(segment); | |
| vec3 col1 = colors[idx]; | |
| vec3 col2 = colors[int(mod(float(idx + 1), 6.0))]; | |
| return mix(col1, col2, smoothstep(0.0, 1.0, frac)); | |
| } | |
| const float MotionSpeed = 2.0; | |
| void main() { | |
| vec2 fragCoord = vUv * resolution; | |
| vec2 uv = (fragCoord - 0.5 * resolution) / resolution.y; | |
| vec3 ro = vec3(0.0, 1.8, time * MotionSpeed); | |
| vec3 rd = normalize(vec3(uv, 1.0)); | |
| vec3 col = vec3(0.0); | |
| float d = 0.0; | |
| float mask = 0.0; | |
| for (int i = 0; i < 120; i++) { | |
| vec3 p = ro + rd * d; | |
| float h = max(abs(p.y), abs(p.x) - 6.0); | |
| if (h < 0.01) { | |
| float tileSizeX = 1.0; // Twice as wide | |
| float tileSizeZ = 0.25; // Half as tall | |
| float row = floor(p.z / tileSizeZ); | |
| float rowOffset = (fract(sin(row * 47.823) * 34567.8) - 0.5) * 0.2; // -0.1 to 0.1 | |
| // Grid coordinates for solar panels with row offset | |
| float tileX = floor((p.x + rowOffset) / tileSizeX); | |
| float tileZ = row; | |
| float rainbowFlow = p.z * 0.15 + time * 0.3; | |
| vec3 baseColor = getRainbowColor(rainbowFlow); | |
| // Dynamic color shifting within spectrum | |
| float colorShift = sin(time * 2.0 + p.x * 0.5) * 0.1; | |
| vec3 panelColor = getRainbowColor(rainbowFlow + colorShift); | |
| // Solar panel grid lines (with row offset applied) | |
| float localX = fract((p.x + rowOffset) / tileSizeX); | |
| float localZ = fract(p.z / tileSizeZ); | |
| // Subtle parallax effect | |
| float gridWidth = 0.02; // Very thin gaps | |
| // Create smooth height map for tiles | |
| float tileHeight = smoothstep(gridWidth, gridWidth * 4.0, localX) * | |
| smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localX) * | |
| smoothstep(gridWidth, gridWidth * 4.0, localZ) * | |
| smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localZ); | |
| // Add per-tile variation | |
| float tileVariation = fract(sin(tileX * 12.9898 + tileZ * 78.233) * 43758.5453); | |
| tileHeight *= 0.8 + tileVariation * 0.2; | |
| // Calculate surface normal from height gradient | |
| float eps = 0.01; | |
| float heightX1 = smoothstep(gridWidth, gridWidth * 4.0, localX + eps) * smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localX - eps); | |
| float heightX2 = smoothstep(gridWidth, gridWidth * 4.0, localX - eps) * smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localX + eps); | |
| float heightZ1 = smoothstep(gridWidth, gridWidth * 4.0, localZ + eps) * smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localZ - eps); | |
| float heightZ2 = smoothstep(gridWidth, gridWidth * 4.0, localZ - eps) * smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localZ + eps); | |
| vec2 gradient = vec2(heightX1 - heightX2, heightZ1 - heightZ2) * 5.0; | |
| vec3 normal = normalize(vec3(-gradient.x, 1.0, -gradient.y)); | |
| // Randomize light direction per tile | |
| float lightAngle = fract(sin(tileX * 45.23 + tileZ * 98.54) * 65432.1) * 6.28318; | |
| float lightTilt = fract(sin(tileX * 23.45 + tileZ * 67.89) * 54321.9) * 0.3 + 0.7; | |
| vec3 lightDir = normalize(vec3(cos(lightAngle) * 0.4, lightTilt, sin(lightAngle) * 0.4)); | |
| vec3 viewDir = normalize(-rd); | |
| vec3 halfDir = normalize(lightDir + viewDir); | |
| float NdotL = max(dot(normal, lightDir), 0.0); | |
| float NdotH = max(dot(normal, halfDir), 0.0); | |
| // Ambient + diffuse lighting | |
| float lighting = 0.6 + NdotL * 0.4; | |
| // per-tile intensity variation for visual breakup | |
| float tileIntensity = 0.7 + tileVariation * 0.4; // 70% to 110% brightness per tile | |
| col = panelColor * lighting * tileIntensity; | |
| // Sharp specular highlight only on raised portions | |
| float spec = pow(NdotH, 128.0) * tileHeight; | |
| col += vec3(1.0) * spec * 0.5; | |
| // Edge highlight - tiles catch light on their beveled edges | |
| float edgeFactor = 1.0 - tileHeight; | |
| float edgeSpec = pow(NdotH, 32.0) * edgeFactor * 0.3; | |
| col += panelColor * edgeSpec; | |
| // rim lighting to define tile shape | |
| float rim = 1.0 - max(0.0, dot(viewDir, normal)); | |
| rim = pow(rim, 2.0) * tileHeight; | |
| col += panelColor * rim * 0.2; | |
| // Clear shadows in the gaps between tiles (back to original) | |
| float gap = 1.0 - tileHeight; | |
| col *= 1.0 - gap * 0.4; | |
| // Create a rotated/shifted pattern for the underlayer | |
| float underlayX = p.x * 0.7 - p.z * 0.3; | |
| float underlayZ = p.x * 0.3 + p.z * 0.7; | |
| float underlayFlow = underlayZ * 0.2 + time * 0.15; | |
| // Different color offset for contrast | |
| vec3 underlayColor = getRainbowColor(underlayFlow + underlayX * 0.3 + 2.0); | |
| // Make underlayer more visible through semi-transparent tiles | |
| // Stronger in tile centers, weaker at edges | |
| float transparency = tileHeight * 0.4; | |
| col = col * (1.0 - transparency) + underlayColor * transparency; | |
| // Add shimmer where layers interact | |
| float layerInteraction = sin(underlayX * 5.0) * sin(p.z * 5.0) * 0.1; | |
| col += underlayColor * layerInteraction * tileHeight; | |
| // Edge glow for rainbow road effect | |
| float tileEdgeGlow = pow(1.0 - abs(localX - 0.5) * 2.0, 3.0) + | |
| pow(1.0 - abs(localZ - 0.5) * 2.0, 3.0); | |
| col += panelColor * tileEdgeGlow * 0.1; | |
| // Boost colors for vibrancy | |
| col *= 1.4; | |
| // Add bloom-like glow for bright areas | |
| float brightness = dot(col, vec3(0.299, 0.587, 0.114)); | |
| vec3 bloom = panelColor * smoothstep(0.5, 1.0, brightness) * 0.5; | |
| col += bloom; | |
| // Distance fade | |
| col /= max(d * 0.025, 1.0); | |
| // Edge transparency and glow | |
| float edgeDist = abs(p.x) / 6.0; | |
| mask = 1.0 - smoothstep(0.8, 1.0, edgeDist); | |
| // Edge rainbow glow | |
| float edgeGlow = exp(-edgeDist * 2.0) * 0.5; | |
| col += getRainbowColor(p.z * 0.1 + time) * edgeGlow * (1.0 - mask); | |
| break; | |
| } | |
| d += h * 0.4; | |
| if (d > 60.0) break; | |
| } | |
| // Space glow effect | |
| vec3 spaceGlow = getRainbowColor(uv.x * 2.0 + time * 0.2); | |
| col += spaceGlow * 0.1 * (1.0 - mask); | |
| col *= mask; | |
| // Tonemapping | |
| col = pow(col, vec3(1.2)); | |
| col = col / (1.0 + col); | |
| col = pow(col, vec3(1.0 / 1.5)); | |
| // Color enhancement | |
| col = mix(col, col * col * (3.0 - 2.0 * col), vec3(0.5)); | |
| gl_FragColor = vec4(col, 1.0); | |
| } | |
| `; | |
| // Create road material | |
| const roadMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| time: { value: 0 }, | |
| resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) } | |
| }, | |
| vertexShader: vertexShader, | |
| fragmentShader: fragmentShader, | |
| transparent: true, | |
| side: THREE.DoubleSide, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| // Create road geometry | |
| const roadGeometry = new THREE.PlaneGeometry(8, 200, 32, 128); | |
| const road = new THREE.Mesh(roadGeometry, roadMaterial); | |
| road.rotation.x = -Math.PI / 2; | |
| road.position.y = -1; | |
| road.position.z = -50; | |
| scene.add(road); | |
| // Add stars in background | |
| const starsGeometry = new THREE.BufferGeometry(); | |
| const starsCount = 5000; | |
| const positions = new Float32Array(starsCount * 3); | |
| for (let i = 0; i < starsCount * 3; i += 3) { | |
| positions[i] = (Math.random() - 0.5) * 200; | |
| positions[i + 1] = Math.random() * 50 - 10; | |
| positions[i + 2] = (Math.random() - 0.5) * 200; | |
| } | |
| starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| const starsMaterial = new THREE.PointsMaterial({ | |
| color: 0xffffff, | |
| size: 0.5, | |
| transparent: true, | |
| opacity: 0.8 | |
| }); | |
| const stars = new THREE.Points(starsGeometry, starsMaterial); | |
| scene.add(stars); | |
| // Add ambient light | |
| const ambientLight = new THREE.AmbientLight(0x222244, 0.5); | |
| scene.add(ambientLight); | |
| // Animation | |
| const clock = new THREE.Clock(); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const elapsedTime = clock.getElapsedTime(); | |
| // Update shader uniforms | |
| roadMaterial.uniforms.time.value = elapsedTime; | |
| // Rotate stars slowly | |
| stars.rotation.y = elapsedTime * 0.02; | |
| // Slight camera movement for dynamic feel | |
| camera.position.x = Math.sin(elapsedTime * 0.5) * 0.5; | |
| camera.position.y = 2 + Math.sin(elapsedTime * 0.3) * 0.2; | |
| renderer.render(scene, camera); | |
| } | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| roadMaterial.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); | |
| }); | |
| animate(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment