Skip to content

Commit dea5206

Browse files
committed
Enhance DevSourceNoCacheMiddleware and add playground static file support
- Updated `DevSourceNoCacheMiddleware` to also prevent caching for requests to the `/playground/` path. - Added support for serving static files from the `/playground` directory in `factory.py`, enhancing the application's development capabilities.
1 parent 882b599 commit dea5206

5 files changed

Lines changed: 995 additions & 1 deletion

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
- generic [active] [ref=e1]:
2+
- complementary:
3+
- paragraph:
4+
- strong: Sandbox
5+
- text: — edite
6+
- code: playground/globe.js
7+
- paragraph: Arraste o globo para rotacionar
8+
- paragraph:
9+
- generic: R
10+
- text: replay entrada
11+
- generic: "1"
12+
- text: hero
13+
- generic: "2"
14+
- text: thinking
15+
- paragraph:
16+
- generic: D
17+
- text: este painel

app/factory.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class DevSourceNoCacheMiddleware(BaseHTTPMiddleware):
2424

2525
async def dispatch(self, request: Request, call_next):
2626
response = await call_next(request)
27-
if request.url.path.startswith("/src/"):
27+
if request.url.path.startswith("/src/") or request.url.path.startswith("/playground/"):
2828
response.headers["Cache-Control"] = "no-store"
2929
return response
3030

@@ -45,6 +45,7 @@ async def lifespan(app: FastAPI):
4545
app.state.templates = templates
4646

4747
frontend_dir = Path(__file__).resolve().parent.parent / "frontend"
48+
playground_dir = Path(__file__).resolve().parent.parent / "playground"
4849
core_json = Path(__file__).resolve().parent.parent / "core" / "disciplines.json"
4950
assets_dir = frontend_dir / "assets"
5051
src_dir = frontend_dir / "src"
@@ -62,6 +63,8 @@ def favicon() -> FileResponse:
6263
app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
6364
if src_dir.is_dir():
6465
app.mount("/src", StaticFiles(directory=str(src_dir)), name="src")
66+
if playground_dir.is_dir():
67+
app.mount("/playground", StaticFiles(directory=str(playground_dir), html=True), name="playground")
6568

6669
app.include_router(router)
6770

playground/globe-interaction.js

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
/* =============================================================
2+
Drag + inertia + handoff for the low-poly globe (playground).
3+
Preserves external auto-spin (GSAP or internal spin) at rest.
4+
============================================================= */
5+
6+
const SENS_X = 0.0052;
7+
const SENS_Y = 0.0031;
8+
const FRICTION = 3.8;
9+
const VELOCITY_STOP = 0.018;
10+
const HANDOFF_MS = 1600;
11+
const INERTIA_MAX_MS = 2400;
12+
const TILT_MIN = -0.88;
13+
const TILT_MAX = 0.18;
14+
15+
/**
16+
* @param {HTMLCanvasElement} canvas
17+
* @param {ReturnType<import("./globe.js").createGlobe>} globe
18+
* @param {{
19+
* stopAutoSpin: () => void,
20+
* startAutoSpin: () => void,
21+
* getAutoSpinOmega?: () => number,
22+
* }} options
23+
*/
24+
export function attachGlobeInteraction(canvas, globe, options) {
25+
const { state, hitTest, getDefaultTilt } = globe;
26+
27+
let enabled = false;
28+
let ready = false;
29+
let mode = "auto"; // auto | drag | inertia | handoff
30+
let pointerId = null;
31+
let lastX = 0;
32+
let lastY = 0;
33+
let lastT = 0;
34+
let velRotY = 0;
35+
let velTilt = 0;
36+
let motionRaf = 0;
37+
let handoffStart = 0;
38+
let handoffFromVel = 0;
39+
let handoffFromTilt = 0;
40+
let handoffLastT = 0;
41+
let inertiaStart = 0;
42+
43+
const prefersReducedMotion =
44+
typeof window !== "undefined" &&
45+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
46+
47+
function autoOmega() {
48+
const internal = globe.getAutoSpinOmega?.() || 0;
49+
if (internal > 0) return internal;
50+
return options.getAutoSpinOmega?.() || 0;
51+
}
52+
53+
function clampTilt(value) {
54+
return Math.min(TILT_MAX, Math.max(TILT_MIN, value));
55+
}
56+
57+
function setCursor(kind) {
58+
canvas.classList.remove("is-grabbable", "is-grabbing");
59+
if (kind === "grab") canvas.classList.add("is-grabbable");
60+
if (kind === "grabbing") canvas.classList.add("is-grabbing");
61+
}
62+
63+
function cancelMotionLoop() {
64+
if (motionRaf) cancelAnimationFrame(motionRaf);
65+
motionRaf = 0;
66+
}
67+
68+
function stopAutoSpin() {
69+
options.stopAutoSpin();
70+
globe.setSpinPaused?.(true);
71+
}
72+
73+
function startAutoSpin() {
74+
globe.setSpinPaused?.(false);
75+
options.startAutoSpin();
76+
mode = "auto";
77+
}
78+
79+
function finishHandoff(now) {
80+
cancelMotionLoop();
81+
startAutoSpin();
82+
handoffLastT = now;
83+
}
84+
85+
function handoffStep(now) {
86+
if (mode !== "handoff") return;
87+
88+
const dt = Math.min(0.032, Math.max(0.001, (now - handoffLastT) / 1000));
89+
handoffLastT = now;
90+
const t = Math.min(1, (now - handoffStart) / HANDOFF_MS);
91+
const ease = 1 - Math.pow(1 - t, 3);
92+
const targetOmega = autoOmega();
93+
const omega = handoffFromVel + (targetOmega - handoffFromVel) * ease;
94+
const tiltTarget = getDefaultTilt();
95+
96+
state.rotY += omega * dt;
97+
state.tilt = handoffFromTilt + (tiltTarget - handoffFromTilt) * ease;
98+
99+
if (t >= 1) {
100+
finishHandoff(now);
101+
return;
102+
}
103+
104+
motionRaf = requestAnimationFrame(handoffStep);
105+
}
106+
107+
function beginHandoff() {
108+
mode = "handoff";
109+
handoffStart = performance.now();
110+
handoffLastT = handoffStart;
111+
handoffFromVel = velRotY;
112+
handoffFromTilt = state.tilt;
113+
cancelMotionLoop();
114+
motionRaf = requestAnimationFrame(handoffStep);
115+
}
116+
117+
function inertiaStep(now) {
118+
if (mode !== "inertia") return;
119+
120+
const dt = Math.min(0.032, Math.max(0.001, (now - lastT) / 1000));
121+
lastT = now;
122+
123+
state.rotY += velRotY * dt;
124+
state.tilt = clampTilt(state.tilt + velTilt * dt);
125+
126+
const decay = Math.exp(-FRICTION * dt);
127+
velRotY *= decay;
128+
velTilt *= decay;
129+
130+
const tiltTarget = getDefaultTilt();
131+
state.tilt += (tiltTarget - state.tilt) * Math.min(1, dt * 2.8);
132+
133+
if (
134+
Math.abs(velRotY) < VELOCITY_STOP ||
135+
now - inertiaStart > INERTIA_MAX_MS
136+
) {
137+
velRotY = 0;
138+
velTilt = 0;
139+
beginHandoff();
140+
return;
141+
}
142+
143+
motionRaf = requestAnimationFrame(inertiaStep);
144+
}
145+
146+
function updateHoverCursor(clientX, clientY) {
147+
if (!ready || mode !== "auto" || pointerId !== null) return;
148+
setCursor(hitTest(clientX, clientY) ? "grab" : null);
149+
}
150+
151+
function beginInertia() {
152+
if (prefersReducedMotion) {
153+
velRotY = 0;
154+
velTilt = 0;
155+
beginHandoff();
156+
return;
157+
}
158+
159+
mode = "inertia";
160+
lastT = performance.now();
161+
inertiaStart = lastT;
162+
cancelMotionLoop();
163+
motionRaf = requestAnimationFrame(inertiaStep);
164+
}
165+
166+
function onPointerDown(e) {
167+
if (!ready || e.button !== 0) return;
168+
if (!hitTest(e.clientX, e.clientY)) return;
169+
170+
e.preventDefault();
171+
cancelMotionLoop();
172+
stopAutoSpin();
173+
174+
pointerId = e.pointerId;
175+
mode = "drag";
176+
lastX = e.clientX;
177+
lastY = e.clientY;
178+
lastT = performance.now();
179+
velRotY = 0;
180+
velTilt = 0;
181+
182+
canvas.setPointerCapture(e.pointerId);
183+
setCursor("grabbing");
184+
}
185+
186+
function onPointerMove(e) {
187+
if (mode === "drag" && e.pointerId === pointerId) {
188+
e.preventDefault();
189+
const now = performance.now();
190+
const dt = Math.max(0.001, (now - lastT) / 1000);
191+
const dx = e.clientX - lastX;
192+
const dy = e.clientY - lastY;
193+
194+
const dRotY = dx * SENS_X;
195+
const dTilt = dy * SENS_Y;
196+
197+
state.rotY += dRotY;
198+
state.tilt = clampTilt(state.tilt + dTilt);
199+
200+
const instantRotY = dRotY / dt;
201+
const instantTilt = dTilt / dt;
202+
velRotY = velRotY * 0.35 + instantRotY * 0.65;
203+
velTilt = velTilt * 0.35 + instantTilt * 0.65;
204+
205+
lastX = e.clientX;
206+
lastY = e.clientY;
207+
lastT = now;
208+
return;
209+
}
210+
211+
updateHoverCursor(e.clientX, e.clientY);
212+
}
213+
214+
function onMouseMove(e) {
215+
updateHoverCursor(e.clientX, e.clientY);
216+
}
217+
218+
function endDrag(e) {
219+
if (e.pointerId !== pointerId) return;
220+
221+
if (canvas.hasPointerCapture(e.pointerId)) {
222+
canvas.releasePointerCapture(e.pointerId);
223+
}
224+
pointerId = null;
225+
226+
if (mode !== "drag") return;
227+
228+
const speed = Math.hypot(velRotY, velTilt);
229+
if (speed < VELOCITY_STOP * 0.5) {
230+
velRotY = 0;
231+
velTilt = 0;
232+
beginHandoff();
233+
} else {
234+
beginInertia();
235+
}
236+
237+
setCursor(hitTest(e.clientX, e.clientY) ? "grab" : null);
238+
}
239+
240+
function onPointerUp(e) {
241+
endDrag(e);
242+
}
243+
244+
function onPointerCancel(e) {
245+
if (e.pointerId !== pointerId) return;
246+
pointerId = null;
247+
mode = "auto";
248+
cancelMotionLoop();
249+
setCursor(null);
250+
startAutoSpin();
251+
}
252+
253+
function onLostCapture() {
254+
if (pointerId === null || mode !== "drag") return;
255+
pointerId = null;
256+
beginInertia();
257+
setCursor(null);
258+
}
259+
260+
canvas.addEventListener("pointerdown", onPointerDown);
261+
canvas.addEventListener("pointermove", onPointerMove);
262+
canvas.addEventListener("pointerup", onPointerUp);
263+
canvas.addEventListener("pointercancel", onPointerCancel);
264+
canvas.addEventListener("lostpointercapture", onLostCapture);
265+
canvas.addEventListener("mousemove", onMouseMove);
266+
267+
return {
268+
getMode: () => mode,
269+
isReady: () => ready,
270+
setEnabled(value) {
271+
enabled = Boolean(value);
272+
ready = enabled;
273+
if (!enabled) {
274+
ready = false;
275+
cancelMotionLoop();
276+
pointerId = null;
277+
mode = "auto";
278+
setCursor(null);
279+
}
280+
},
281+
destroy() {
282+
cancelMotionLoop();
283+
canvas.removeEventListener("pointerdown", onPointerDown);
284+
canvas.removeEventListener("pointermove", onPointerMove);
285+
canvas.removeEventListener("pointerup", onPointerUp);
286+
canvas.removeEventListener("pointercancel", onPointerCancel);
287+
canvas.removeEventListener("lostpointercapture", onLostCapture);
288+
canvas.removeEventListener("mousemove", onMouseMove);
289+
setCursor(null);
290+
},
291+
};
292+
}

0 commit comments

Comments
 (0)