L'affiage graphique du bras ne fonctionne toujours pas - Follow Up Deployment
Browse files- index.html +561 -470
index.html
CHANGED
@@ -1,524 +1,615 @@
|
|
1 |
<!DOCTYPE html>
|
2 |
-
<html lang="
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
-
<title>
|
7 |
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
-
<
|
9 |
<style>
|
10 |
-
|
11 |
-
|
12 |
-
body {
|
13 |
-
font-family: 'VT323', monospace;
|
14 |
-
background-color: #000;
|
15 |
-
color: #00ff00;
|
16 |
-
overflow: hidden;
|
17 |
-
position: relative;
|
18 |
-
}
|
19 |
-
|
20 |
-
.crt::before {
|
21 |
-
content: " ";
|
22 |
-
display: block;
|
23 |
-
position: absolute;
|
24 |
-
top: 0;
|
25 |
-
left: 0;
|
26 |
-
bottom: 0;
|
27 |
-
right: 0;
|
28 |
-
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%);
|
29 |
-
background-size: 100% 4px;
|
30 |
-
z-index: 100;
|
31 |
-
pointer-events: none;
|
32 |
-
}
|
33 |
-
|
34 |
-
.crt::after {
|
35 |
-
content: " ";
|
36 |
-
display: block;
|
37 |
-
position: absolute;
|
38 |
-
top: 0;
|
39 |
-
left: 0;
|
40 |
-
bottom: 0;
|
41 |
-
right: 0;
|
42 |
-
background: rgba(0, 30, 0, 0.05);
|
43 |
-
z-index: 100;
|
44 |
-
pointer-events: none;
|
45 |
-
}
|
46 |
-
|
47 |
-
.scanlines {
|
48 |
-
position: relative;
|
49 |
-
overflow: hidden;
|
50 |
-
}
|
51 |
-
|
52 |
-
.scanlines::before {
|
53 |
-
content: "";
|
54 |
-
position: absolute;
|
55 |
-
top: 0;
|
56 |
-
left: 0;
|
57 |
-
width: 100%;
|
58 |
-
height: 100%;
|
59 |
-
background: repeating-linear-gradient(
|
60 |
-
to bottom,
|
61 |
-
transparent 0%,
|
62 |
-
rgba(0, 255, 0, 0.05) 0.5%,
|
63 |
-
transparent 1%
|
64 |
-
);
|
65 |
-
animation: scanline 8s linear infinite;
|
66 |
-
z-index: 1001;
|
67 |
-
pointer-events: none;
|
68 |
-
}
|
69 |
-
|
70 |
-
@keyframes scanline {
|
71 |
-
from { transform: translateY(0%); }
|
72 |
-
to { transform: translateY(-100%); }
|
73 |
}
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
|
|
77 |
}
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
border: 1px solid #00ff00;
|
82 |
}
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
}
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
50% { opacity: 0; }
|
91 |
}
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
image-rendering: crisp-edges;
|
97 |
-
}
|
98 |
-
|
99 |
-
.noise {
|
100 |
-
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABOSURBVGhD7c4xEQAgDAAxhHSj+5+2jAgywL0kO7O7AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4zX14KZ3Z3B5QAAAAASUVORK5CYII=");
|
101 |
-
opacity: 0.05;
|
102 |
-
position: absolute;
|
103 |
-
top: 0;
|
104 |
-
left: 0;
|
105 |
-
width: 100%;
|
106 |
-
height: 100%;
|
107 |
-
pointer-events: none;
|
108 |
}
|
109 |
</style>
|
110 |
</head>
|
111 |
-
<body class="
|
112 |
-
<div class="
|
113 |
-
|
114 |
-
|
115 |
-
<div class="container mx-auto h-full p-4 flex flex-col">
|
116 |
-
<!-- Header -->
|
117 |
-
<div class="flex justify-between items-center mb-2">
|
118 |
-
<div class="text-2xl glow">
|
119 |
-
<span class="text-amber-400">FDI</span> TERMINAL v3.1.4
|
120 |
-
</div>
|
121 |
-
<div class="text-sm">
|
122 |
-
<span class="text-amber-400">OPERATOR:</span> EVAN TANNER
|
123 |
-
</div>
|
124 |
-
<div class="text-sm">
|
125 |
-
<span class="text-amber-400">DATE:</span> <span id="current-date">05/12/1991</span>
|
126 |
-
</div>
|
127 |
-
</div>
|
128 |
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
<
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
|
|
149 |
</div>
|
150 |
</div>
|
151 |
|
152 |
-
|
153 |
-
|
154 |
-
<div class="
|
155 |
-
<span class="text-lg glow">TOOLS</span>
|
156 |
-
</div>
|
157 |
-
<div class="grid grid-cols-2 gap-2">
|
158 |
-
<button class="bg-green-900 hover:bg-green-800 p-1 text-xs" onclick="openTool('chemScan')">CHEMSCAN</button>
|
159 |
-
<button class="bg-green-900 hover:bg-green-800 p-1 text-xs" onclick="openTool('dataDecrypt')">DATA DECRYPT</button>
|
160 |
-
<button class="bg-green-900 hover:bg-green-800 p-1 text-xs" onclick="openTool('videoAnalyzer')">VIDEO ANALYZER</button>
|
161 |
-
<button class="bg-green-900 hover:bg-green-800 p-1 text-xs" onclick="openTool('comms')">COMMS LINK</button>
|
162 |
-
<button class="bg-green-900 hover:bg-green-800 p-1 text-xs" onclick="openTool('scada')">SCADA ACCESS</button>
|
163 |
-
<button class="bg-green-900 hover:bg-green-800 p-1 text-xs" onclick="openTool('logs')">SYSTEM LOGS</button>
|
164 |
-
</div>
|
165 |
-
</div>
|
166 |
-
</div>
|
167 |
-
|
168 |
-
<!-- Center Panel -->
|
169 |
-
<div class="w-2/4 flex flex-col gap-4">
|
170 |
-
<!-- Main Terminal -->
|
171 |
-
<div class="window bg-black p-3 flex-1 relative">
|
172 |
-
<div class="border-b border-green-500 pb-1 mb-2">
|
173 |
-
<span class="text-lg glow">MAIN TERMINAL</span>
|
174 |
-
</div>
|
175 |
-
<div id="terminal-output" class="h-5/6 overflow-y-auto text-sm font-mono leading-snug">
|
176 |
-
<p>> INITIALIZING FDI TERMINAL...</p>
|
177 |
-
<p>> LOADING CASE FILES...</p>
|
178 |
-
<p>> ESTABLISHING SECURE CONNECTION...</p>
|
179 |
-
<p class="text-amber-400">> WARNING: UNAUTHORIZED ACCESS DETECTED IN SYSTEM 4B</p>
|
180 |
-
<p>> LAST KNOWN AGENT STATUS: IN COMMUNICATION WITH SUSPECT</p>
|
181 |
-
<p>> SCADA SYSTEMS SHOW ABNORMAL PRESSURE READINGS IN NORTH WING</p>
|
182 |
-
<p class="text-red-400">> ALERT: FIRE SUPPRESSION SYSTEM DISABLED IN SECTOR D</p>
|
183 |
-
<p>> ENTER COMMAND OR SELECT TOOL TO PROCEED<span class="terminal-text">_</span></p>
|
184 |
-
</div>
|
185 |
-
<div class="absolute bottom-3 left-3 right-3">
|
186 |
-
<div class="flex">
|
187 |
-
<span class="mr-2">></span>
|
188 |
-
<input type="text" id="command-input" class="bg-black text-green-500 flex-1 outline-none font-mono" autofocus>
|
189 |
-
</div>
|
190 |
-
</div>
|
191 |
</div>
|
192 |
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
|
|
197 |
</div>
|
198 |
-
<div
|
199 |
-
<
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
<div class="border border-green-500 p-1 flex flex-col items-center" onclick="viewEvidence('chem1')">
|
206 |
-
<div class="w-16 h-16 bg-gray-800 flex items-center justify-center">
|
207 |
-
<i class="fas fa-flask text-green-500"></i>
|
208 |
-
</div>
|
209 |
-
<span class="text-xs mt-1">CHEM_004.dat</span>
|
210 |
-
</div>
|
211 |
-
<div class="border border-green-500 p-1 flex flex-col items-center" onclick="viewEvidence('doc1')">
|
212 |
-
<div class="w-16 h-16 bg-gray-800 flex items-center justify-center">
|
213 |
-
<i class="fas fa-file-alt text-green-500"></i>
|
214 |
-
</div>
|
215 |
-
<span class="text-xs mt-1">MEMO_023.txt</span>
|
216 |
-
</div>
|
217 |
-
<div class="border border-green-500 p-1 flex flex-col items-center" onclick="viewEvidence('audio1')">
|
218 |
-
<div class="w-16 h-16 bg-gray-800 flex items-center justify-center">
|
219 |
-
<i class="fas fa-microphone text-green-500"></i>
|
220 |
-
</div>
|
221 |
-
<span class="text-xs mt-1">AUDIO_007.wav</span>
|
222 |
-
</div>
|
223 |
-
<div class="border border-green-500 p-1 flex flex-col items-center" onclick="viewEvidence('scada1')">
|
224 |
-
<div class="w-16 h-16 bg-gray-800 flex items-center justify-center">
|
225 |
-
<i class="fas fa-chart-line text-green-500"></i>
|
226 |
-
</div>
|
227 |
-
<span class="text-xs mt-1">SCADA_LOG.rdt</span>
|
228 |
-
</div>
|
229 |
</div>
|
230 |
</div>
|
231 |
</div>
|
232 |
|
233 |
-
<!--
|
234 |
-
<div class="
|
235 |
-
|
236 |
-
<div class="window bg-black p-3 flex-1">
|
237 |
-
<div class="border-b border-green-500 pb-1 mb-2">
|
238 |
-
<span class="text-lg glow">COMMS CHANNEL</span>
|
239 |
-
</div>
|
240 |
-
<div id="comms-log" class="h-64 overflow-y-auto text-xs space-y-1">
|
241 |
-
<p class="text-amber-400">[23:47] AGENT KOWALSKI: I'm in position. No visual on suspect yet.</p>
|
242 |
-
<p class="text-amber-400">[23:49] AGENT KOWALSKI: Found something... security logs show unauthorized access.</p>
|
243 |
-
<p class="text-amber-400">[23:52] AGENT KOWALSKI: There's a chemical spill in sector C. Doesn't look accidental.</p>
|
244 |
-
<p class="text-red-400">[23:55] AGENT KOWALSKI: I see someone! They're tampering with the control panel!</p>
|
245 |
-
<p class="text-red-400">[23:56] AGENT KOWALSKI: They're wearing an FDI badge but I don't recognize them.</p>
|
246 |
-
<p class="text-red-400">[23:57] AGENT KOWALSKI: They've spotted me! I'm pursuing on foot!</p>
|
247 |
-
<p class="text-red-400">[23:58] AGENT KOWALSKI: They're heading to the server room!</p>
|
248 |
-
<p class="text-gray-500">[23:59] COMMS LOST...</p>
|
249 |
-
</div>
|
250 |
-
<div class="mt-2 border-t border-green-500 pt-2">
|
251 |
-
<div class="flex">
|
252 |
-
<input type="text" class="bg-black text-green-500 flex-1 outline-none text-xs" placeholder="TYPE MESSAGE...">
|
253 |
-
<button class="bg-green-900 hover:bg-green-800 px-2 text-xs">SEND</button>
|
254 |
-
</div>
|
255 |
-
</div>
|
256 |
-
</div>
|
257 |
|
258 |
-
|
259 |
-
|
260 |
-
<div class="
|
261 |
-
<
|
262 |
-
|
263 |
-
|
264 |
-
<div class="border border-red-500 p-2">
|
265 |
-
<p class="text-xs text-red-400">SCADA ALERT: PRESSURE BUILDING IN NORTH WING</p>
|
266 |
-
<p class="text-xs">Current: 4.7 MPa | Safe limit: 3.2 MPa</p>
|
267 |
</div>
|
268 |
-
<div class="
|
269 |
-
<p class="text-
|
270 |
-
<p class="text-
|
271 |
</div>
|
272 |
-
<div class="
|
273 |
-
<p class="text-
|
|
|
274 |
</div>
|
275 |
</div>
|
276 |
</div>
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
</div>
|
285 |
-
<div>
|
286 |
-
<span class="text-amber-400">FDI HEADQUARTERS</span> - ALL RIGHTS RESERVED 1991
|
287 |
-
</div>
|
288 |
-
</div>
|
289 |
-
</div>
|
290 |
-
|
291 |
-
<!-- Tool Windows (Hidden by default) -->
|
292 |
-
<div id="chemScan-window" class="window bg-black absolute top-1/4 left-1/4 w-1/2 h-1/2 p-3 hidden" style="transform: translate(-50%, -50%); z-index: 1000;">
|
293 |
-
<div class="flex justify-between items-center border-b border-green-500 pb-1 mb-2">
|
294 |
-
<span class="text-lg glow">CHEMSCAN v2.4</span>
|
295 |
-
<button onclick="closeTool('chemScan')" class="text-red-400 hover:text-red-300">X</button>
|
296 |
-
</div>
|
297 |
-
<div class="flex h-5/6">
|
298 |
-
<div class="w-1/3 border-r border-green-500 pr-2">
|
299 |
-
<p class="text-sm mb-2">SELECT SAMPLE:</p>
|
300 |
-
<div class="space-y-1">
|
301 |
-
<button class="w-full text-left p-1 hover:bg-green-900 text-xs">CHEM_001.dat</button>
|
302 |
-
<button class="w-full text-left p-1 hover:bg-green-900 text-xs">CHEM_002.dat</button>
|
303 |
-
<button class="w-full text-left p-1 hover:bg-green-900 text-xs">CHEM_003.dat</button>
|
304 |
-
<button class="w-full text-left p-1 bg-green-900 text-xs">CHEM_004.dat</button>
|
305 |
-
</div>
|
306 |
-
</div>
|
307 |
-
<div class="w-2/3 pl-2">
|
308 |
-
<p class="text-sm mb-2">ANALYSIS RESULTS:</p>
|
309 |
-
<div class="border border-green-500 p-2 h-4/5 overflow-y-auto text-xs">
|
310 |
-
<p>> SAMPLE: CHEM_004.dat</p>
|
311 |
-
<p>> SOURCE: NORTH WING, SECTOR C</p>
|
312 |
-
<p>> COLLECTED: 05/12/1991 23:30</p>
|
313 |
-
<p class="mt-2 text-amber-400">> COMPOSITION ANALYSIS:</p>
|
314 |
-
<p>- 45% TRICHLOROETHYLENE</p>
|
315 |
-
<p>- 30% METHYL ETHYL KETONE</p>
|
316 |
-
<p>- 15% UNKNOWN COMPOUND</p>
|
317 |
-
<p>- 10% WATER</p>
|
318 |
-
<p class="mt-2 text-red-400">> WARNING: UNKNOWN COMPOUND MATCHES EXPERIMENTAL FORMULA X-247</p>
|
319 |
-
<p class="text-red-400">> CAUTION: HIGHLY VOLATILE MIXTURE</p>
|
320 |
-
</div>
|
321 |
-
<button class="mt-2 bg-green-900 hover:bg-green-800 p-1 text-xs w-full">EXPORT REPORT</button>
|
322 |
-
</div>
|
323 |
-
</div>
|
324 |
-
</div>
|
325 |
-
|
326 |
-
<div id="videoAnalyzer-window" class="window bg-black absolute top-1/4 left-1/4 w-1/2 h-1/2 p-3 hidden" style="transform: translate(-50%, -50%); z-index: 1000;">
|
327 |
-
<div class="flex justify-between items-center border-b border-green-500 pb-1 mb-2">
|
328 |
-
<span class="text-lg glow">VIDEO ANALYZER v1.7</span>
|
329 |
-
<button onclick="closeTool('videoAnalyzer')" class="text-red-400 hover:text-red-300">X</button>
|
330 |
-
</div>
|
331 |
-
<div class="flex h-5/6">
|
332 |
-
<div class="w-1/3 border-r border-green-500 pr-2">
|
333 |
-
<p class="text-sm mb-2">SELECT FILE:</p>
|
334 |
-
<div class="space-y-1">
|
335 |
-
<button class="w-full text-left p-1 bg-green-900 text-xs">SURV_001.avi</button>
|
336 |
-
<button class="w-full text-left p-1 hover:bg-green-900 text-xs">SURV_002.avi</button>
|
337 |
-
<button class="w-full text-left p-1 hover:bg-green-900 text-xs">SURV_003.avi</button>
|
338 |
-
</div>
|
339 |
-
<div class="mt-4">
|
340 |
-
<p class="text-sm mb-2">TOOLS:</p>
|
341 |
-
<button class="w-full p-1 hover:bg-green-900 text-xs mb-1">ENHANCE</button>
|
342 |
-
<button class="w-full p-1 hover:bg-green-900 text-xs mb-1">FRAME ANALYSIS</button>
|
343 |
-
<button class="w-full p-1 hover:bg-green-900 text-xs">AUDIO ISOLATION</button>
|
344 |
</div>
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
<div class="
|
349 |
-
<
|
350 |
-
<p
|
351 |
</div>
|
352 |
</div>
|
353 |
-
|
354 |
-
|
355 |
-
<
|
356 |
-
<
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
<p>"...just a few more minutes... systems will be offline... HAL will be pleased..."</p>
|
365 |
</div>
|
366 |
</div>
|
367 |
</div>
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
<p class="
|
378 |
-
<
|
379 |
-
<
|
380 |
-
<
|
381 |
-
<
|
382 |
-
<
|
383 |
-
|
384 |
-
|
385 |
-
</div>
|
386 |
-
<div class="w-2/3 pl-2">
|
387 |
-
<p class="text-sm mb-2">PRESSURE SYSTEMS STATUS:</p>
|
388 |
-
<div class="border border-green-500 p-2 h-4/5 overflow-y-auto text-xs">
|
389 |
-
<div class="mb-4">
|
390 |
-
<p class="text-amber-400">NORTH WING:</p>
|
391 |
-
<div class="w-full bg-gray-800 h-4 mt-1">
|
392 |
-
<div class="bg-red-500 h-full" style="width: 90%;"></div>
|
393 |
-
</div>
|
394 |
-
<p>4.7 MPa / 3.2 MPa MAX</p>
|
395 |
-
<p class="text-red-400">CRITICAL OVERPRESSURE</p>
|
396 |
-
</div>
|
397 |
-
<div class="mb-4">
|
398 |
-
<p class="text-amber-400">SOUTH WING:</p>
|
399 |
-
<div class="w-full bg-gray-800 h-4 mt-1">
|
400 |
-
<div class="bg-green-500 h-full" style="width: 45%;"></div>
|
401 |
-
</div>
|
402 |
-
<p>2.1 MPa / 3.2 MPa MAX</p>
|
403 |
-
<p>NORMAL</p>
|
404 |
-
</div>
|
405 |
-
<div class="mb-4">
|
406 |
-
<p class="text-amber-400">EAST WING:</p>
|
407 |
-
<div class="w-full bg-gray-800 h-4 mt-1">
|
408 |
-
<div class="bg-amber-500 h-full" style="width: 70%;"></div>
|
409 |
-
</div>
|
410 |
-
<p>3.0 MPa / 3.2 MPa MAX</p>
|
411 |
-
<p class="text-amber-400">ELEVATED</p>
|
412 |
-
</div>
|
413 |
-
<div>
|
414 |
-
<p class="text-amber-400">WEST WING:</p>
|
415 |
-
<div class="w-full bg-gray-800 h-4 mt-1">
|
416 |
-
<div class="bg-green-500 h-full" style="width: 30%;"></div>
|
417 |
-
</div>
|
418 |
-
<p>1.4 MPa / 3.2 MPa MAX</p>
|
419 |
-
<p>NORMAL</p>
|
420 |
-
</div>
|
421 |
-
</div>
|
422 |
-
<button class="mt-2 bg-green-900 hover:bg-green-800 p-1 text-xs w-full">OVERRIDE CONTROL</button>
|
423 |
</div>
|
424 |
</div>
|
425 |
</div>
|
426 |
-
|
427 |
<script>
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
element.innerHTML += text.charAt(i);
|
435 |
-
i++;
|
436 |
-
setTimeout(typing, speed);
|
437 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
438 |
}
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
output.innerHTML += `<p>COMMAND NOT RECOGNIZED. TYPE HELP FOR AVAILABLE COMMANDS.</p>`;
|
464 |
}
|
465 |
|
466 |
-
|
467 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
468 |
}
|
469 |
-
});
|
470 |
-
|
471 |
-
// Tool functions
|
472 |
-
function openTool(tool) {
|
473 |
-
document.getElementById(`${tool}-window`).classList.remove('hidden');
|
474 |
-
}
|
475 |
-
|
476 |
-
function closeTool(tool) {
|
477 |
-
document.getElementById(`${tool}-window`).classList.add('hidden');
|
478 |
-
}
|
479 |
-
|
480 |
-
// Evidence viewer
|
481 |
-
function viewEvidence(type) {
|
482 |
-
const output = document.getElementById('terminal-output');
|
483 |
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
497 |
}
|
498 |
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
508 |
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
520 |
}
|
521 |
-
|
|
|
|
|
|
|
|
|
522 |
</script>
|
523 |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MGros/operator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
|
524 |
</html>
|
|
|
1 |
<!DOCTYPE html>
|
2 |
+
<html lang="fr">
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Simulation Cinématique Inverse - Bras Robotique 2D</title>
|
7 |
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
|
9 |
<style>
|
10 |
+
canvas {
|
11 |
+
touch-action: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
}
|
13 |
+
.formula-container {
|
14 |
+
background-color: #f8fafc;
|
15 |
+
border-radius: 0.5rem;
|
16 |
+
padding: 1rem;
|
17 |
+
font-family: 'Courier New', monospace;
|
18 |
}
|
19 |
+
.variable {
|
20 |
+
color: #3b82f6;
|
21 |
+
font-weight: bold;
|
|
|
22 |
}
|
23 |
+
.value {
|
24 |
+
color: #10b981;
|
25 |
+
font-weight: bold;
|
26 |
}
|
27 |
+
.error {
|
28 |
+
color: #ef4444;
|
29 |
+
font-weight: bold;
|
|
|
30 |
}
|
31 |
+
.workspace {
|
32 |
+
fill: rgba(59, 130, 246, 0.1);
|
33 |
+
stroke: #3b82f6;
|
34 |
+
stroke-width: 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
}
|
36 |
</style>
|
37 |
</head>
|
38 |
+
<body class="bg-gray-50 min-h-screen">
|
39 |
+
<div class="container mx-auto px-4 py-8">
|
40 |
+
<h1 class="text-3xl font-bold text-center text-blue-600 mb-6">Simulation de Cinématique Inverse</h1>
|
41 |
+
<p class="text-center text-gray-600 mb-8">Explorez visuellement et mathématiquement la cinématique inverse d'un bras robotique à 2 articulations</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
|
43 |
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
44 |
+
<!-- Canvas et contrôles -->
|
45 |
+
<div class="lg:col-span-2 bg-white rounded-xl shadow-md p-6">
|
46 |
+
<div class="flex flex-col md:flex-row justify-between items-center mb-4">
|
47 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-2 md:mb-0">Représentation du Bras Robotique</h2>
|
48 |
+
<div class="flex space-x-4">
|
49 |
+
<div>
|
50 |
+
<label for="l1" class="block text-sm font-medium text-gray-700">L₁ (cm)</label>
|
51 |
+
<input type="range" id="l1" min="50" max="200" value="100" class="w-24 mt-1">
|
52 |
+
<span id="l1-value" class="ml-2">100</span>
|
53 |
+
</div>
|
54 |
+
<div>
|
55 |
+
<label for="l2" class="block text-sm font-medium text-gray-700">L₂ (cm)</label>
|
56 |
+
<input type="range" id="l2" min="50" max="200" value="100" class="w-24 mt-1">
|
57 |
+
<span id="l2-value" class="ml-2">100</span>
|
58 |
+
</div>
|
59 |
+
<div>
|
60 |
+
<label for="l3" class="block text-sm font-medium text-gray-700">L₃ (cm)</label>
|
61 |
+
<input type="range" id="l3" min="50" max="200" value="100" class="w-24 mt-1">
|
62 |
+
<span id="l3-value" class="ml-2">100</span>
|
63 |
+
</div>
|
64 |
</div>
|
65 |
</div>
|
66 |
|
67 |
+
<div class="relative border border-gray-200 rounded-lg overflow-hidden">
|
68 |
+
<canvas id="robotCanvas" width="600" height="400" class="w-full bg-gray-50"></canvas>
|
69 |
+
<div id="error-message" class="absolute top-4 right-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded hidden"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
</div>
|
71 |
|
72 |
+
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
73 |
+
<div>
|
74 |
+
<button id="toggleTrace" class="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
75 |
+
Activer Trace
|
76 |
+
</button>
|
77 |
</div>
|
78 |
+
<div>
|
79 |
+
<label for="targetX" class="block text-sm font-medium text-gray-700">Position cible X (cm)</label>
|
80 |
+
<input type="number" id="targetX" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
81 |
+
</div>
|
82 |
+
<div>
|
83 |
+
<label for="targetY" class="block text-sm font-medium text-gray-700">Position cible Y (cm)</label>
|
84 |
+
<input type="number" id="targetY" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
</div>
|
86 |
</div>
|
87 |
</div>
|
88 |
|
89 |
+
<!-- Formules et résultats -->
|
90 |
+
<div class="bg-white rounded-xl shadow-md p-6">
|
91 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-4">Calcul des Angles</h2>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
|
93 |
+
<div class="mb-6">
|
94 |
+
<h3 class="font-medium text-gray-700 mb-2">Angles calculés :</h3>
|
95 |
+
<div class="grid grid-cols-2 gap-4">
|
96 |
+
<div class="bg-blue-50 p-3 rounded-lg">
|
97 |
+
<p class="text-sm text-gray-600">θ₁ (épaule)</p>
|
98 |
+
<p id="theta1-value" class="text-xl font-bold text-blue-600">0°</p>
|
|
|
|
|
|
|
99 |
</div>
|
100 |
+
<div class="bg-green-50 p-3 rounded-lg">
|
101 |
+
<p class="text-sm text-gray-600">θ₂ (coude)</p>
|
102 |
+
<p id="theta2-value" class="text-xl font-bold text-green-600">0°</p>
|
103 |
</div>
|
104 |
+
<div class="bg-purple-50 p-3 rounded-lg">
|
105 |
+
<p class="text-sm text-gray-600">θ₃ (poignet)</p>
|
106 |
+
<p id="theta3-value" class="text-xl font-bold text-purple-600">0°</p>
|
107 |
</div>
|
108 |
</div>
|
109 |
</div>
|
110 |
+
|
111 |
+
<div class="mb-6">
|
112 |
+
<h3 class="font-medium text-gray-700 mb-2">Équations directes :</h3>
|
113 |
+
<div class="formula-container">
|
114 |
+
<p>\[ x = L_1 \cos(\theta_1) + L_2 \cos(\theta_1 + \theta_2) + L_3 \cos(\theta_1 + \theta_2 + \theta_3) \]</p>
|
115 |
+
<p>\[ y = L_1 \sin(\theta_1) + L_2 \sin(\theta_1 + \theta_2) + L_3 \sin(\theta_1 + \theta_2 + \theta_3) \]</p>
|
116 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
</div>
|
118 |
+
|
119 |
+
<div class="mb-6">
|
120 |
+
<h3 class="font-medium text-gray-700 mb-2">Cinématique inverse :</h3>
|
121 |
+
<div class="formula-container">
|
122 |
+
<p>\[ \theta_2 = \arccos\left(\frac{x^2 + y^2 - L_1^2 - L_2^2}{2 L_1 L_2}\right) \]</p>
|
123 |
+
<p>\[ \theta_1 = \arctan2(y, x) - \arctan2\left(\frac{L_2 \sin(\theta_2)}{L_1 + L_2 \cos(\theta_2)}\right) \]</p>
|
124 |
</div>
|
125 |
</div>
|
126 |
+
|
127 |
+
<div>
|
128 |
+
<h3 class="font-medium text-gray-700 mb-2">Variables :</h3>
|
129 |
+
<ul class="space-y-2 text-sm">
|
130 |
+
<li><span class="variable">L₁</span> : Longueur du premier segment = <span id="l1-formula" class="value">100</span> cm</li>
|
131 |
+
<li><span class="variable">L₂</span> : Longueur du deuxième segment = <span id="l2-formula" class="value">100</span> cm</li>
|
132 |
+
<li><span class="variable">L₃</span> : Longueur du troisième segment = <span id="l3-formula" class="value">100</span> cm</li>
|
133 |
+
<li><span class="variable">(x, y)</span> : Position cible = (<span id="x-formula" class="value">0</span>, <span id="y-formula" class="value">0</span>) cm</li>
|
134 |
+
<li><span class="variable">θ₁</span> : Angle de l'épaule = <span id="theta1-formula" class="value">0</span>°</li>
|
135 |
+
<li><span class="variable">θ₂</span> : Angle du coude = <span id="theta2-formula" class="value">0</span>°</li>
|
136 |
+
</ul>
|
|
|
137 |
</div>
|
138 |
</div>
|
139 |
</div>
|
140 |
+
|
141 |
+
<div class="mt-8 bg-white rounded-xl shadow-md p-6">
|
142 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-4">Explications</h2>
|
143 |
+
<div class="prose max-w-none">
|
144 |
+
<p>Cette simulation illustre le principe de <strong>cinématique inverse</strong> pour un bras robotique à 2 degrés de liberté :</p>
|
145 |
+
<ul class="list-disc pl-5 space-y-2">
|
146 |
+
<li><strong>Cinématique directe</strong> : Calcul de la position (x,y) de l'effecteur à partir des angles des articulations.</li>
|
147 |
+
<li><strong>Cinématique inverse</strong> : Calcul des angles des articulations nécessaires pour atteindre une position (x,y) donnée.</li>
|
148 |
+
</ul>
|
149 |
+
<p class="mt-4">Pour utiliser la simulation :</p>
|
150 |
+
<ol class="list-decimal pl-5 space-y-2">
|
151 |
+
<li>Déplacez le point d'extrémité (en rouge) avec la souris</li>
|
152 |
+
<li>Ou entrez des coordonnées cible dans les champs X et Y</li>
|
153 |
+
<li>Modifiez les longueurs L₁ et L₂ pour explorer différents cas</li>
|
154 |
+
<li>Observez en temps réel les angles calculés et les formules</li>
|
155 |
+
</ol>
|
156 |
+
<p class="mt-4">Si la position cible est inaccessible (hors de la zone atteignable), un message d'erreur s'affichera.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
157 |
</div>
|
158 |
</div>
|
159 |
</div>
|
160 |
+
|
161 |
<script>
|
162 |
+
document.addEventListener('DOMContentLoaded', function() {
|
163 |
+
// Configuration de MathJax
|
164 |
+
MathJax.Hub.Config({
|
165 |
+
tex2jax: {
|
166 |
+
inlineMath: [['$','$'], ['\\(','\\)']],
|
167 |
+
processEscapes: true
|
|
|
|
|
|
|
168 |
}
|
169 |
+
});
|
170 |
+
|
171 |
+
// Éléments du DOM
|
172 |
+
const canvas = document.getElementById('robotCanvas');
|
173 |
+
const ctx = canvas.getContext('2d');
|
174 |
+
const l1Slider = document.getElementById('l1');
|
175 |
+
const l2Slider = document.getElementById('l2');
|
176 |
+
const l1Value = document.getElementById('l1-value');
|
177 |
+
const l2Value = document.getElementById('l2-value');
|
178 |
+
const targetXInput = document.getElementById('targetX');
|
179 |
+
const targetYInput = document.getElementById('targetY');
|
180 |
+
const theta1Display = document.getElementById('theta1-value');
|
181 |
+
const theta2Display = document.getElementById('theta2-value');
|
182 |
+
const l1Formula = document.getElementById('l1-formula');
|
183 |
+
const l2Formula = document.getElementById('l2-formula');
|
184 |
+
const xFormula = document.getElementById('x-formula');
|
185 |
+
const yFormula = document.getElementById('y-formula');
|
186 |
+
const theta1Formula = document.getElementById('theta1-formula');
|
187 |
+
const theta2Formula = document.getElementById('theta2-formula');
|
188 |
+
const errorMessage = document.getElementById('error-message');
|
189 |
+
|
190 |
+
// Paramètres initiaux
|
191 |
+
let L1 = parseInt(l1Slider.value);
|
192 |
+
let L2 = parseInt(l2Slider.value);
|
193 |
+
let L3 = parseInt(l3Slider.value);
|
194 |
+
let targetX = 150;
|
195 |
+
let targetY = 50;
|
196 |
+
let theta1 = 0;
|
197 |
+
let theta2 = 0;
|
198 |
+
let theta3 = 0;
|
199 |
+
let isDragging = false;
|
200 |
+
let showWorkspace = true;
|
201 |
+
let traceEnabled = false;
|
202 |
+
let tracePoints = [];
|
203 |
+
let drawingHistory = [];
|
204 |
+
let isDrawing = false;
|
205 |
+
|
206 |
+
// Mise à l'échelle pour le canvas
|
207 |
+
const scale = 1.5;
|
208 |
+
const originX = canvas.width / 2;
|
209 |
+
const originY = canvas.height / 2;
|
210 |
+
|
211 |
+
// Fonction pour calculer la cinématique inverse
|
212 |
+
function inverseKinematics(x, y) {
|
213 |
+
const xScaled = x / scale;
|
214 |
+
const yScaled = y / scale;
|
215 |
+
|
216 |
+
// Vérifier si la position est atteignable
|
217 |
+
const distance = Math.sqrt(xScaled*xScaled + yScaled*yScaled);
|
218 |
+
const sumL = L1 + L2 + L3;
|
219 |
+
const diffL = Math.max(0, L1 - L2 - L3, L2 - L1 - L3, L3 - L1 - L2);
|
220 |
+
|
221 |
+
if (distance > sumL || distance < diffL) {
|
222 |
+
return null; // Position inaccessible
|
223 |
+
}
|
224 |
+
|
225 |
+
// Pour 3 articulations, nous utiliserons une approche géométrique
|
226 |
+
// Nous fixons θ3 à 0 pour simplifier (orientation finale fixe)
|
227 |
+
const theta3 = 0;
|
228 |
+
|
229 |
+
// Calcul des coordonnées du "coude virtuel"
|
230 |
+
const targetDistance = Math.sqrt(xScaled*xScaled + yScaled*yScaled);
|
231 |
+
const effectiveDistance = targetDistance - L3; // On retire L3 car θ3=0
|
232 |
+
|
233 |
+
// Vérification à nouveau pour L1 et L2
|
234 |
+
if (effectiveDistance > L1 + L2 || effectiveDistance < Math.abs(L1 - L2)) {
|
235 |
+
return null;
|
236 |
+
}
|
237 |
+
|
238 |
+
// Calcul de θ2 pour le bras à 2 segments (L1 et L2)
|
239 |
+
const cosTheta2 = (effectiveDistance*effectiveDistance - L1*L1 - L2*L2) / (2 * L1 * L2);
|
240 |
+
const clampedCosTheta2 = Math.max(-1, Math.min(1, cosTheta2));
|
241 |
+
const theta2 = Math.acos(clampedCosTheta2);
|
242 |
+
|
243 |
+
// Calcul de θ1
|
244 |
+
const k1 = L1 + L2 * Math.cos(theta2);
|
245 |
+
const k2 = L2 * Math.sin(theta2);
|
246 |
+
const theta1 = Math.atan2(yScaled, xScaled) - Math.atan2(k2, k1);
|
247 |
+
|
248 |
+
return { theta1, theta2, theta3 };
|
249 |
}
|
250 |
+
|
251 |
+
// Fonction pour dessiner le bras robotique
|
252 |
+
function drawRobot() {
|
253 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
254 |
+
|
255 |
+
// Dessiner la zone atteignable (workspace)
|
256 |
+
if (showWorkspace) {
|
257 |
+
ctx.beginPath();
|
258 |
+
ctx.arc(originX, originY, (L1 + L2) * scale, 0, 2 * Math.PI);
|
259 |
+
ctx.fillStyle = 'rgba(59, 130, 246, 0.1)';
|
260 |
+
ctx.fill();
|
261 |
+
ctx.strokeStyle = '#3b82f6';
|
262 |
+
ctx.lineWidth = 1;
|
263 |
+
ctx.stroke();
|
264 |
+
|
265 |
+
if (L1 !== L2) {
|
266 |
+
ctx.beginPath();
|
267 |
+
ctx.arc(originX, originY, Math.abs(L1 - L2) * scale, 0, 2 * Math.PI);
|
268 |
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
269 |
+
ctx.fill();
|
270 |
+
ctx.strokeStyle = '#3b82f6';
|
271 |
+
ctx.lineWidth = 1;
|
272 |
+
ctx.stroke();
|
273 |
+
}
|
|
|
274 |
}
|
275 |
|
276 |
+
// Calculer les positions des articulations
|
277 |
+
const shoulderX = originX;
|
278 |
+
const shoulderY = originY;
|
279 |
+
const elbowX = shoulderX + L1 * Math.cos(theta1) * scale;
|
280 |
+
const elbowY = shoulderY + L1 * Math.sin(theta1) * scale;
|
281 |
+
const wristX = elbowX + L2 * Math.cos(theta1 + theta2) * scale;
|
282 |
+
const wristY = elbowY + L2 * Math.sin(theta1 + theta2) * scale;
|
283 |
+
const handX = wristX + L3 * Math.cos(theta1 + theta2 + theta3) * scale;
|
284 |
+
const handY = wristY + L3 * Math.sin(theta1 + theta2 + theta3) * scale;
|
285 |
+
|
286 |
+
// Dessiner les segments du bras
|
287 |
+
ctx.beginPath();
|
288 |
+
ctx.moveTo(shoulderX, shoulderY);
|
289 |
+
ctx.lineTo(elbowX, elbowY);
|
290 |
+
ctx.lineWidth = 8;
|
291 |
+
ctx.strokeStyle = '#4b5563';
|
292 |
+
ctx.stroke();
|
293 |
+
|
294 |
+
ctx.beginPath();
|
295 |
+
ctx.moveTo(elbowX, elbowY);
|
296 |
+
ctx.lineTo(wristX, wristY);
|
297 |
+
ctx.lineWidth = 8;
|
298 |
+
ctx.strokeStyle = '#4b5563';
|
299 |
+
ctx.stroke();
|
300 |
+
|
301 |
+
ctx.beginPath();
|
302 |
+
ctx.moveTo(wristX, wristY);
|
303 |
+
ctx.lineTo(handX, handY);
|
304 |
+
ctx.lineWidth = 8;
|
305 |
+
ctx.strokeStyle = '#4b5563';
|
306 |
+
ctx.stroke();
|
307 |
+
|
308 |
+
// Dessiner les articulations
|
309 |
+
ctx.beginPath();
|
310 |
+
ctx.arc(shoulderX, shoulderY, 10, 0, 2 * Math.PI);
|
311 |
+
ctx.fillStyle = '#3b82f6';
|
312 |
+
ctx.fill();
|
313 |
+
|
314 |
+
ctx.beginPath();
|
315 |
+
ctx.arc(elbowX, elbowY, 10, 0, 2 * Math.PI);
|
316 |
+
ctx.fillStyle = '#10b981';
|
317 |
+
ctx.fill();
|
318 |
+
|
319 |
+
ctx.beginPath();
|
320 |
+
ctx.arc(wristX, wristY, 10, 0, 2 * Math.PI);
|
321 |
+
ctx.fillStyle = '#8b5cf6';
|
322 |
+
ctx.fill();
|
323 |
+
|
324 |
+
// Dessiner l'effecteur (main)
|
325 |
+
ctx.beginPath();
|
326 |
+
ctx.arc(handX, handY, 12, 0, 2 * Math.PI);
|
327 |
+
ctx.fillStyle = '#ef4444';
|
328 |
+
ctx.fill();
|
329 |
+
|
330 |
+
// Dessiner la trace si activée
|
331 |
+
if (traceEnabled && tracePoints.length > 1) {
|
332 |
+
ctx.beginPath();
|
333 |
+
ctx.moveTo(tracePoints[0].x, tracePoints[0].y);
|
334 |
+
for (let i = 1; i < tracePoints.length; i++) {
|
335 |
+
ctx.lineTo(tracePoints[i].x, tracePoints[i].y);
|
336 |
+
}
|
337 |
+
ctx.strokeStyle = 'rgba(239, 68, 68, 0.5)';
|
338 |
+
ctx.lineWidth = 2;
|
339 |
+
ctx.stroke();
|
340 |
+
}
|
341 |
+
|
342 |
+
// Dessiner l'historique de dessin
|
343 |
+
if (drawingHistory.length > 1) {
|
344 |
+
ctx.beginPath();
|
345 |
+
ctx.moveTo(drawingHistory[0].x, drawingHistory[0].y);
|
346 |
+
for (let i = 1; i < drawingHistory.length; i++) {
|
347 |
+
ctx.lineTo(drawingHistory[i].x, drawingHistory[i].y);
|
348 |
+
}
|
349 |
+
ctx.strokeStyle = 'rgba(59, 130, 246, 0.7)';
|
350 |
+
ctx.lineWidth = 3;
|
351 |
+
ctx.stroke();
|
352 |
+
}
|
353 |
+
|
354 |
+
// Dessiner la cible si elle est différente de la position actuelle
|
355 |
+
const tolerance = 5;
|
356 |
+
if (Math.abs(handX - targetX) > tolerance || Math.abs(handY - targetY) > tolerance) {
|
357 |
+
ctx.beginPath();
|
358 |
+
ctx.arc(targetX, targetY, 8, 0, 2 * Math.PI);
|
359 |
+
ctx.fillStyle = 'rgba(239, 68, 68, 0.5)';
|
360 |
+
ctx.fill();
|
361 |
+
ctx.strokeStyle = '#ef4444';
|
362 |
+
ctx.lineWidth = 2;
|
363 |
+
ctx.stroke();
|
364 |
+
}
|
365 |
+
|
366 |
+
// Mettre à jour les valeurs affichées
|
367 |
+
updateDisplayValues(handX, handY);
|
368 |
+
}
|
369 |
+
|
370 |
+
// Mettre à jour les valeurs affichées
|
371 |
+
function updateDisplayValues(handX, handY) {
|
372 |
+
const x = (handX - originX) / scale;
|
373 |
+
const y = (handY - originY) / scale;
|
374 |
+
|
375 |
+
theta1Display.textContent = `${(theta1 * 180 / Math.PI).toFixed(1)}°`;
|
376 |
+
theta2Display.textContent = `${(theta2 * 180 / Math.PI).toFixed(1)}°`;
|
377 |
+
|
378 |
+
l1Formula.textContent = L1;
|
379 |
+
l2Formula.textContent = L2;
|
380 |
+
xFormula.textContent = x.toFixed(1);
|
381 |
+
yFormula.textContent = y.toFixed(1);
|
382 |
+
theta1Formula.textContent = (theta1 * 180 / Math.PI).toFixed(1);
|
383 |
+
theta2Formula.textContent = (theta2 * 180 / Math.PI).toFixed(1);
|
384 |
+
theta3Formula.textContent = (theta3 * 180 / Math.PI).toFixed(1);
|
385 |
+
|
386 |
+
// Mettre à jour MathJax
|
387 |
+
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
|
388 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
389 |
|
390 |
+
// Mettre à jour la position cible
|
391 |
+
function updateTargetPosition(x, y) {
|
392 |
+
targetX = x;
|
393 |
+
targetY = y;
|
394 |
+
|
395 |
+
// Mettre à jour les champs de saisie
|
396 |
+
targetXInput.value = ((x - originX) / scale).toFixed(1);
|
397 |
+
targetYInput.value = ((y - originY) / scale).toFixed(1);
|
398 |
+
|
399 |
+
// Calculer la cinématique inverse
|
400 |
+
const result = inverseKinematics(x - originX, y - originY);
|
401 |
+
|
402 |
+
if (result) {
|
403 |
+
theta1 = result.theta1;
|
404 |
+
theta2 = result.theta2;
|
405 |
+
errorMessage.classList.add('hidden');
|
406 |
+
} else {
|
407 |
+
errorMessage.textContent = "Position inaccessible !";
|
408 |
+
errorMessage.classList.remove('hidden');
|
409 |
+
}
|
410 |
+
|
411 |
+
drawRobot();
|
412 |
}
|
413 |
|
414 |
+
// Événements pour les sliders
|
415 |
+
l1Slider.addEventListener('input', function() {
|
416 |
+
L1 = parseInt(this.value);
|
417 |
+
l1Value.textContent = L1;
|
418 |
+
l1Formula.textContent = L1;
|
419 |
+
updateTargetPosition(targetX, targetY);
|
420 |
+
});
|
421 |
+
|
422 |
+
l2Slider.addEventListener('input', function() {
|
423 |
+
L2 = parseInt(this.value);
|
424 |
+
l2Value.textContent = L2;
|
425 |
+
l2Formula.textContent = L2;
|
426 |
+
updateTargetPosition(targetX, targetY);
|
427 |
+
});
|
428 |
+
|
429 |
+
l3Slider.addEventListener('input', function() {
|
430 |
+
L3 = parseInt(this.value);
|
431 |
+
l3Value.textContent = L3;
|
432 |
+
l3Formula.textContent = L3;
|
433 |
+
updateTargetPosition(targetX, targetY);
|
434 |
+
});
|
435 |
+
|
436 |
+
// Événements pour les champs de saisie
|
437 |
+
targetXInput.addEventListener('change', function() {
|
438 |
+
const x = parseFloat(this.value) * scale + originX;
|
439 |
+
updateTargetPosition(x, targetY);
|
440 |
+
});
|
441 |
+
|
442 |
+
targetYInput.addEventListener('change', function() {
|
443 |
+
const y = parseFloat(this.value) * scale + originY;
|
444 |
+
updateTargetPosition(targetX, y);
|
445 |
+
});
|
446 |
+
|
447 |
+
// Événements pour le canvas (drag and drop)
|
448 |
+
canvas.addEventListener('mousedown', function(e) {
|
449 |
+
const rect = canvas.getBoundingClientRect();
|
450 |
+
const x = e.clientX - rect.left;
|
451 |
+
const y = e.clientY - rect.top;
|
452 |
+
|
453 |
+
// Vérifier si on clique près de l'effecteur ou de la cible
|
454 |
+
const handX = originX + (L1 * Math.cos(theta1) + L2 * Math.cos(theta1 + theta2)) * scale;
|
455 |
+
const handY = originY + (L1 * Math.sin(theta1) + L2 * Math.sin(theta1 + theta2)) * scale;
|
456 |
+
|
457 |
+
const distToHand = Math.sqrt((x - handX)**2 + (y - handY)**2);
|
458 |
+
const distToTarget = Math.sqrt((x - targetX)**2 + (y - targetY)**2);
|
459 |
+
|
460 |
+
if (distToHand < 20 || distToTarget < 20) {
|
461 |
+
isDragging = true;
|
462 |
+
updateTargetPosition(x, y);
|
463 |
+
}
|
464 |
+
});
|
465 |
+
|
466 |
+
canvas.addEventListener('mousemove', function(e) {
|
467 |
+
if (isDragging) {
|
468 |
+
const rect = canvas.getBoundingClientRect();
|
469 |
+
const x = e.clientX - rect.left;
|
470 |
+
const y = e.clientY - rect.top;
|
471 |
+
updateTargetPosition(x, y);
|
472 |
+
}
|
473 |
+
});
|
474 |
+
|
475 |
+
canvas.addEventListener('mouseup', function() {
|
476 |
+
isDragging = false;
|
477 |
+
});
|
478 |
|
479 |
+
canvas.addEventListener('mouseleave', function() {
|
480 |
+
isDragging = false;
|
481 |
+
});
|
482 |
+
|
483 |
+
// Support tactile
|
484 |
+
canvas.addEventListener('touchstart', function(e) {
|
485 |
+
e.preventDefault();
|
486 |
+
const rect = canvas.getBoundingClientRect();
|
487 |
+
const touch = e.touches[0];
|
488 |
+
const x = touch.clientX - rect.left;
|
489 |
+
const y = touch.clientY - rect.top;
|
490 |
+
|
491 |
+
const handX = originX + (L1 * Math.cos(theta1) + L2 * Math.cos(theta1 + theta2)) * scale;
|
492 |
+
const handY = originY + (L1 * Math.sin(theta1) + L2 * Math.sin(theta1 + theta2)) * scale;
|
493 |
+
|
494 |
+
const distToHand = Math.sqrt((x - handX)**2 + (y - handY)**2);
|
495 |
+
const distToTarget = Math.sqrt((x - targetX)**2 + (y - targetY)**2);
|
496 |
+
|
497 |
+
if (distToHand < 40 || distToTarget < 40) {
|
498 |
+
isDragging = true;
|
499 |
+
updateTargetPosition(x, y);
|
500 |
+
}
|
501 |
+
});
|
502 |
+
|
503 |
+
canvas.addEventListener('touchmove', function(e) {
|
504 |
+
e.preventDefault();
|
505 |
+
if (isDragging && e.touches.length === 1) {
|
506 |
+
const rect = canvas.getBoundingClientRect();
|
507 |
+
const touch = e.touches[0];
|
508 |
+
const x = touch.clientX - rect.left;
|
509 |
+
const y = touch.clientY - rect.top;
|
510 |
+
updateTargetPosition(x, y);
|
511 |
+
}
|
512 |
+
});
|
513 |
+
|
514 |
+
canvas.addEventListener('touchend', function() {
|
515 |
+
isDragging = false;
|
516 |
+
});
|
517 |
+
|
518 |
+
// Gestion du bouton de trace
|
519 |
+
const toggleTraceBtn = document.getElementById('toggleTrace');
|
520 |
+
toggleTraceBtn.addEventListener('click', function() {
|
521 |
+
traceEnabled = !traceEnabled;
|
522 |
+
if (traceEnabled) {
|
523 |
+
this.textContent = 'Désactiver Trace';
|
524 |
+
tracePoints = []; // Réinitialiser la trace
|
525 |
+
drawingHistory = []; // Réinitialiser l'historique
|
526 |
+
} else {
|
527 |
+
this.textContent = 'Activer Trace';
|
528 |
+
}
|
529 |
+
drawRobot();
|
530 |
+
});
|
531 |
+
|
532 |
+
// Activation du dessin au clic sur le canvas
|
533 |
+
canvas.addEventListener('mousedown', function(e) {
|
534 |
+
const rect = canvas.getBoundingClientRect();
|
535 |
+
const x = e.clientX - rect.left;
|
536 |
+
const y = e.clientY - rect.top;
|
537 |
+
|
538 |
+
// Commencer un nouveau dessin
|
539 |
+
isDrawing = true;
|
540 |
+
drawingHistory = [{x, y}];
|
541 |
+
});
|
542 |
+
|
543 |
+
canvas.addEventListener('mousemove', function(e) {
|
544 |
+
if (isDrawing) {
|
545 |
+
const rect = canvas.getBoundingClientRect();
|
546 |
+
const x = e.clientX - rect.left;
|
547 |
+
const y = e.clientY - rect.top;
|
548 |
+
|
549 |
+
// Ajouter le point à l'historique
|
550 |
+
drawingHistory.push({x, y});
|
551 |
+
|
552 |
+
// Limiter la taille de l'historique pour la performance
|
553 |
+
if (drawingHistory.length > 1000) {
|
554 |
+
drawingHistory.shift();
|
555 |
+
}
|
556 |
+
|
557 |
+
// Mettre à jour la position cible
|
558 |
+
updateTargetPosition(x, y);
|
559 |
+
} else if (isDragging) {
|
560 |
+
const rect = canvas.getBoundingClientRect();
|
561 |
+
const x = e.clientX - rect.left;
|
562 |
+
const y = e.clientY - rect.top;
|
563 |
+
updateTargetPosition(x, y);
|
564 |
+
}
|
565 |
+
});
|
566 |
+
|
567 |
+
canvas.addEventListener('mouseup', function() {
|
568 |
+
isDragging = false;
|
569 |
+
isDrawing = false;
|
570 |
+
});
|
571 |
+
|
572 |
+
canvas.addEventListener('mouseleave', function() {
|
573 |
+
isDragging = false;
|
574 |
+
isDrawing = false;
|
575 |
+
});
|
576 |
+
|
577 |
+
// Mettre à jour la position cible avec gestion de la trace
|
578 |
+
function updateTargetPosition(x, y) {
|
579 |
+
targetX = x;
|
580 |
+
targetY = y;
|
581 |
+
|
582 |
+
if (traceEnabled) {
|
583 |
+
tracePoints.push({x, y});
|
584 |
+
// Limiter le nombre de points pour performance
|
585 |
+
if (tracePoints.length > 500) {
|
586 |
+
tracePoints.shift();
|
587 |
+
}
|
588 |
+
}
|
589 |
+
|
590 |
+
// Mettre à jour les champs de saisie
|
591 |
+
targetXInput.value = ((x - originX) / scale).toFixed(1);
|
592 |
+
targetYInput.value = ((y - originY) / scale).toFixed(1);
|
593 |
+
|
594 |
+
// Calculer la cinématique inverse
|
595 |
+
const result = inverseKinematics(x - originX, y - originY);
|
596 |
+
|
597 |
+
if (result) {
|
598 |
+
theta1 = result.theta1;
|
599 |
+
theta2 = result.theta2;
|
600 |
+
errorMessage.classList.add('hidden');
|
601 |
+
} else {
|
602 |
+
errorMessage.textContent = "Position inaccessible !";
|
603 |
+
errorMessage.classList.remove('hidden');
|
604 |
+
}
|
605 |
+
|
606 |
+
drawRobot();
|
607 |
}
|
608 |
+
|
609 |
+
// Initialisation
|
610 |
+
updateTargetPosition(originX + 100 * scale, originY);
|
611 |
+
drawRobot();
|
612 |
+
});
|
613 |
</script>
|
614 |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MGros/operator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
|
615 |
</html>
|