1+ import * as THREE from 'three' ;
2+
3+ const canvas = document . querySelector ( 'canvas.webgl' ) ;
4+ const scene = new THREE . Scene ( ) ;
5+ const geometry = new THREE . PlaneGeometry ( 2 , 2 ) ;
6+
7+ const uniforms = {
8+ iTime : { value : 0 } ,
9+ iResolution : { value : new THREE . Vector3 ( ) } ,
10+ uMode : { value : 0 } , // 0: Loxodromic, 1: Elliptic, 2: Hyperbolic, 3: Parabolic
11+ uShowSphere : { value : 1.0 } , // 1.0: On, 0.0: Off
12+ // Camera uniforms
13+ uCameraPos : { value : new THREE . Vector3 ( ) } ,
14+ uCameraTarget : { value : new THREE . Vector3 ( ) } ,
15+ uCameraForward : { value : new THREE . Vector3 ( ) } ,
16+ uCameraRight : { value : new THREE . Vector3 ( ) } ,
17+ uCameraUp : { value : new THREE . Vector3 ( ) }
18+ } ;
19+
20+ const fragmentShader = `
21+ uniform float iTime;
22+ uniform vec3 iResolution;
23+ uniform int uMode;
24+ uniform float uShowSphere;
25+ uniform vec3 uCameraPos;
26+ uniform vec3 uCameraTarget;
27+ uniform vec3 uCameraForward;
28+ uniform vec3 uCameraRight;
29+ uniform vec3 uCameraUp;
30+
31+ #define PI 3.14159265359
32+
33+ // --- Complex Math ---
34+ vec2 cmul(vec2 z, vec2 w) { return vec2(z.x * w.x - z.y * w.y, z.x * w.y + z.y * w.x); }
35+ vec2 cdiv(vec2 z, vec2 w) { return vec2(z.x * w.x + z.y * w.y, -z.x * w.y + z.y * w.x) / dot(w, w); }
36+
37+ // The fixed Mobius base warp
38+ vec2 applyMobius(vec2 z) {
39+ vec2 A = vec2(-1, 0), B = vec2(1, 0), C = vec2(-1, 0), D = vec2(-1, 0);
40+ return cdiv(cmul(A, z) + B, cmul(C, z) + D);
41+ }
42+
43+ float map(vec3 p) {
44+ float d = p.y;
45+ if (uShowSphere > 0.5) d = min(d, length(p - vec3(0, 1, 0)) - 1.0);
46+ return d;
47+ }
48+
49+ void main() {
50+ vec2 uv = (gl_FragCoord.xy - iResolution.xy * 0.5) / iResolution.y;
51+
52+ // Use uniforms for camera instead of hardcoded values
53+ vec3 ro = uCameraPos;
54+ vec3 look = uCameraTarget;
55+ vec3 f = uCameraForward;
56+ vec3 r = uCameraRight;
57+ vec3 u = uCameraUp;
58+
59+ vec3 rd = normalize(uv.x * r + uv.y * u + 2.0 * f);
60+
61+ float t = 0.0;
62+ for(int i = 0; i < 120; i++) {
63+ float d = map(ro + rd * t);
64+ if(d < 0.001 || t > 40.0) break;
65+ t += d;
66+ }
67+
68+ vec3 col = vec3(0.05, 0.05, 0.08);
69+
70+ if(t < 50.0) {
71+ vec3 pos = ro + rd * t;
72+ bool isSphere = (uShowSphere > 0.5 && length(pos - vec3(0, 1, 0)) < 1.1);
73+
74+ // 1. Map to Complex Plane
75+ vec2 z = isSphere ? vec2(pos.x, pos.z) / (2.001 - pos.y) : pos.xz * 0.5;
76+
77+ // Apply Mobius base
78+ z = applyMobius(z);
79+
80+ vec2 gridUV;
81+
82+ // 2. Apply Mode (State Machine)
83+ float speed = 0.09;
84+ if (uMode == 0) { // Loxodromic: Seamless & Large Tiles
85+ float logR = log(length(z) + 0.0001) - iTime * speed;
86+ float theta = atan(z.y, z.x) * (2.0 / PI) + iTime * speed; // Period is 4.0
87+
88+ // slope = 0.5 means for every full rotation (4 units),
89+ // we shift log-radius by 2 units.
90+ // Since 2 is an even integer, the checkerboard color stays the same
91+ // across the jump, hiding the branch cut.
92+ float slope = 0.5;
93+
94+ gridUV.x = logR + slope * theta;
95+ gridUV.y = logR - (1.0 / slope) * theta;
96+
97+ // Smaller density: 1.0 makes tiles large.
98+ // Use integers (1.0, 2.0, etc.) to maintain the seamless branch cut.
99+ gridUV *= 0.75;
100+ }
101+ else if (uMode == 1) { // Elliptic: Pure Rotation
102+ gridUV = vec2(log(length(z) + 0.0001), atan(z.y, z.x) * (2.0 / PI) + iTime * speed * 2.2);
103+ }
104+ else if (uMode == 2) { // Hyperbolic: Pure Scaling
105+ gridUV = vec2(log(length(z) + 0.0001) - iTime * speed * 2.4, atan(z.y, z.x) * (2.0 / PI));
106+ }
107+ else if (uMode == 3) { // Parabolic: Pure Translation
108+ gridUV = (z + vec2(iTime * speed * 2.2, 0.0)) * 1.0;
109+ }
110+
111+ // 3. Render Checkerboard
112+ vec2 check = floor(mod(gridUV * 4.0, 2.0));
113+ float mask = abs(check.x - check.y);
114+
115+ // Define light position/direction here
116+ vec3 lightPos = vec3(-1.0, 1.0, -0.5);
117+ vec3 lightDir = normalize(lightPos); // Or just use a fixed direction like normalize(vec3(1, 2, 3))
118+
119+ vec3 nor = isSphere ? normalize(pos - vec3(0, 1, 0)) : vec3(0, 1, 0);
120+ // Diffuse lighting
121+ float diff = clamp(dot(nor, lightDir), 0.2, 1.0);
122+ col = mix(vec3(0.01), vec3(0.9), mask) * diff;
123+
124+ if(isSphere) col += pow(clamp(dot(reflect(rd, nor), lightDir), 0.0, 1.0), 32.0) * 0.4;
125+ }
126+
127+ gl_FragColor = vec4(pow(col, vec3(0.4545)), 1.0);
128+ }
129+ ` ;
130+
131+ const material = new THREE . ShaderMaterial ( {
132+ uniforms : uniforms ,
133+ vertexShader : `void main() { gl_Position = vec4(position, 1.0); }` ,
134+ fragmentShader : fragmentShader
135+ } ) ;
136+
137+ const mesh = new THREE . Mesh ( geometry , material ) ;
138+ scene . add ( mesh ) ;
139+
140+ const renderer = new THREE . WebGLRenderer ( { canvas, antialias : true } ) ;
141+ renderer . setSize ( window . innerWidth , window . innerHeight ) ;
142+ renderer . setPixelRatio ( Math . min ( window . devicePixelRatio , 2 ) ) ;
143+
144+ // Initial uniform set with pixel ratio
145+ const canvasWidth = window . innerWidth * renderer . getPixelRatio ( ) ;
146+ const canvasHeight = window . innerHeight * renderer . getPixelRatio ( ) ;
147+ uniforms . iResolution . value . set ( canvasWidth , canvasHeight , 1 ) ;
148+
149+ const modeText = document . getElementById ( 'mode-text' ) ;
150+ const modes = {
151+ '1' : 'Loxodromic' ,
152+ '2' : 'Elliptic' ,
153+ '3' : 'Hyperbolic' ,
154+ '4' : 'Parabolic'
155+ } ;
156+
157+ // ========== VIEW CONTROLS - MODIFY THESE VALUES TO CHANGE THE VIEW ==========
158+
159+ // 1. CHANGE WHERE THE CAMERA LOOKS AT
160+ const cameraTarget = new THREE . Vector3 ( 0 , 0.6 , 0 ) ;
161+
162+ // 2. CHANGE THE CAMERA DISTANCE FROM THE TARGET
163+ const cameraDistance = 8.0 ;
164+
165+ // 3. CHANGE THE VERTICAL OFFSET OF THE CAMERA ORBIT
166+ const yOffset = 1.5 ;
167+
168+ // 4. CHANGE MOUSE SENSITIVITY
169+ const mouseSensitivityX = 0.01 ; // Horizontal sensitivity
170+ const mouseSensitivityY = 0.007 ; // Vertical sensitivity
171+
172+ // 5. CHANGE ROTATION LIMITS
173+ const maxVerticalTilt = 1.5 ; // Max up/down angle (radians)
174+ const minVerticalTilt = 0.3 ; // Min up/down angle (radians)
175+
176+ // 6. CHANGE SMOOTHING SPEED
177+ const smoothingSpeed = 0.1 ;
178+
179+ // ========== END OF VIEW CONTROLS ==========
180+
181+ // Mouse interaction variables
182+ let targetRotationX = 0.3 ; // Initial vertical tilt (radians)
183+ let targetRotationY = - 0.5 ; // Initial horizontal rotation (radians)
184+ let currentRotationX = 0.3 ; // Match the target
185+ let currentRotationY = - 0.5 ; // Match the target
186+ let isMouseDown = false ;
187+ let lastMouseX = 0 ;
188+ let lastMouseY = 0 ;
189+
190+ // Mouse event handlers
191+ canvas . addEventListener ( 'mousedown' , ( e ) => {
192+ isMouseDown = true ;
193+ lastMouseX = e . clientX ;
194+ lastMouseY = e . clientY ;
195+ canvas . style . cursor = 'grabbing' ;
196+ } ) ;
197+
198+ window . addEventListener ( 'mousemove' , ( e ) => {
199+ if ( isMouseDown ) {
200+ const deltaX = e . clientX - lastMouseX ;
201+ const deltaY = e . clientY - lastMouseY ;
202+
203+ // Apply sensitivity settings
204+ targetRotationY += deltaX * mouseSensitivityX ;
205+ targetRotationX += deltaY * mouseSensitivityY ;
206+
207+ // Apply rotation limits
208+ targetRotationX = Math . max ( minVerticalTilt , Math . min ( maxVerticalTilt , targetRotationX ) ) ;
209+
210+ lastMouseX = e . clientX ;
211+ lastMouseY = e . clientY ;
212+ }
213+ } ) ;
214+
215+ window . addEventListener ( 'mouseup' , ( ) => {
216+ isMouseDown = false ;
217+ canvas . style . cursor = 'grab' ;
218+ } ) ;
219+
220+ // Initial cursor style
221+ canvas . style . cursor = 'grab' ;
222+
223+ // Smooth rotation and camera update
224+ function updateCamera ( ) {
225+ // Smooth interpolation
226+ currentRotationX += ( targetRotationX - currentRotationX ) * smoothingSpeed ;
227+ currentRotationY += ( targetRotationY - currentRotationY ) * smoothingSpeed ;
228+
229+ // Calculate camera position based on spherical coordinates
230+ const radius = cameraDistance ;
231+ const theta = currentRotationY ;
232+ const phi = currentRotationX ;
233+
234+ // Calculate camera position relative to target
235+ const x = radius * Math . sin ( theta ) * Math . cos ( phi ) ;
236+ const y = radius * Math . sin ( phi ) + yOffset ;
237+ const z = radius * Math . cos ( theta ) * Math . cos ( phi ) ;
238+
239+ const cameraPos = new THREE . Vector3 ( x , y , z ) ;
240+ const lookAt = cameraTarget ;
241+
242+ // Calculate the direction vectors for the shader
243+ const forward = lookAt . clone ( ) . sub ( cameraPos ) . normalize ( ) ;
244+ const right = new THREE . Vector3 ( 0 , 1 , 0 ) . cross ( forward ) . normalize ( ) ;
245+ const up = forward . clone ( ) . cross ( right ) . normalize ( ) ;
246+
247+ // Update shader uniforms with camera parameters
248+ uniforms . uCameraPos . value = cameraPos ;
249+ uniforms . uCameraTarget . value = lookAt ;
250+ uniforms . uCameraForward . value = forward ;
251+ uniforms . uCameraRight . value = right ;
252+ uniforms . uCameraUp . value = up ;
253+ }
254+
255+ // Keyboard controls for mode switching
256+ window . addEventListener ( 'keydown' , ( e ) => {
257+ // Handle Mode Switching (1-4)
258+ if ( modes [ e . key ] ) {
259+ uniforms . uMode . value = parseInt ( e . key ) - 1 ;
260+
261+ if ( modeText ) {
262+ modeText . innerText = modes [ e . key ] ;
263+ }
264+ }
265+
266+ // Handle Sphere Toggle (5)
267+ if ( e . key === '5' ) {
268+ uniforms . uShowSphere . value = uniforms . uShowSphere . value === 1.0 ? 0.0 : 1.0 ;
269+ console . log ( "Sphere toggled to:" , uniforms . uShowSphere . value ) ;
270+ }
271+ } ) ;
272+
273+ window . addEventListener ( 'resize' , ( ) => {
274+ const width = window . innerWidth ;
275+ const height = window . innerHeight ;
276+
277+ renderer . setSize ( width , height ) ;
278+
279+ const pixelRatio = Math . min ( window . devicePixelRatio , 2 ) ;
280+ renderer . setPixelRatio ( pixelRatio ) ;
281+
282+ uniforms . iResolution . value . set ( width * pixelRatio , height * pixelRatio , 1 ) ;
283+ } ) ;
284+
285+ // Initialize camera uniforms
286+ updateCamera ( ) ;
287+
288+ const tick = ( ) => {
289+ uniforms . iTime . value = performance . now ( ) / 1000 ;
290+ updateCamera ( ) ; // Update camera position based on mouse input
291+ renderer . render ( scene , new THREE . Camera ( ) ) ;
292+ window . requestAnimationFrame ( tick ) ;
293+ } ;
294+
295+ tick ( ) ;
0 commit comments