Skip to content

Commit debd7db

Browse files
committed
ok
1 parent facb81c commit debd7db

6 files changed

Lines changed: 870 additions & 0 deletions

File tree

applets/mobius/index.html

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
5+
<head>
6+
<meta charset="UTF-8">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8+
<title>Möbius transformations</title>
9+
<link rel="stylesheet" href="./style.css">
10+
11+
</head>
12+
13+
<body>
14+
<canvas class="webgl"></canvas>
15+
16+
<footer class="footer">
17+
Möbius transformations <br/>Inspired by <a href="https://www.shadertoy.com/view/4scfR2" target="_blank">neozhaoliang</a>
18+
<br/>
19+
<div style="margin-top: 10px;">Type: <b id="mode-text">Loxodromic</b></div>
20+
</footer>
21+
22+
<div class="instructions">
23+
24+
<p>Press <b>1, 2, 3, 4</b> to switch transformations</p>
25+
<p>Press <b>5</b> to toggle the sphere</p>
26+
</div>
27+
28+
<script type="importmap">
29+
{
30+
"imports": {
31+
"three": "https://cdn.jsdelivr.net/npm/three@v0.183.2/build/three.module.min.js",
32+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@v0.183.2/examples/jsm/"
33+
}
34+
}
35+
</script>
36+
<script type="module" src="./script.js"></script>
37+
38+
39+
</body>
40+
41+
</html>

applets/mobius/script.js

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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

Comments
 (0)