ersatzben

DOUBLE PENDULUM V8UI

Nov 6th, 2024 (edited)
622
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.32 KB | None | 0 0
  1. // JAVASCRIPT FOR DOUBLE PENDULUM - V8UI
  2. // =====================================
  3. //
  4. // Requires Max 9. Paste into a V8UI object. Accepts messages as shown under the anything() function's switch/case.
  5. //
  6. // =====================================
  7.  
  8. autowatch = 1;
  9. outlets = 2;
  10.  
  11. // Initialize physics parameters
  12. const g = 9.81; // gravity
  13. let length1 = 175;
  14. let length2 = 45;
  15. let mass1 = 10;
  16. let mass2 = 13;
  17. let angle1 = Math.PI / 2;
  18. let angle2 = Math.PI / 2;
  19. let velocity1 = 0;
  20. let velocity2 = 0;
  21. let simulationSpeed = 0.75;
  22.  
  23. // Hang point
  24. const viewsize = mgraphics.size;
  25. const width = viewsize[0];
  26. const height = viewsize[1];
  27. const centerX = width * 0.5;
  28. const centerY = height * 0.25;
  29.  
  30. // Trace points for trajectory
  31. let maxTracePoints = 800;
  32. let traceOpacity = 0.35; // Initial opacity value
  33. let tracePoints = [];
  34.  
  35. // Animation control
  36. let isAnimating = true;
  37. const task = new Task(animate, this);
  38.  
  39. // Setup mgraphics
  40. mgraphics.init();
  41. mgraphics.relative_coords = 0;
  42. mgraphics.autofill = 0;
  43.  
  44. function paint() {
  45. // Dark background - commented out to make background transparent
  46. // mgraphics.set_source_rgb(0.1, 0.1, 0.12, 0); // Dark slate color
  47. // mgraphics.rectangle(0, 0, this.box.rect[2], this.box.rect[3]);
  48. // mgraphics.fill();
  49.  
  50. // Calculate positions
  51. const x1 = centerX + length1 * Math.sin(angle1);
  52. const y1 = centerY + length1 * Math.cos(angle1);
  53. const x2 = x1 + length2 * Math.sin(angle2);
  54. const y2 = y1 + length2 * Math.cos(angle2);
  55.  
  56. // Draw trajectory with opacity gradient
  57. if (tracePoints.length) {
  58. mgraphics.set_line_width(2.5);
  59. for (let i = 0; i < tracePoints.length - 1; i++) {
  60. const progress = i / (tracePoints.length - 1);
  61. const segmentOpacity = progress * 0.5;
  62.  
  63. mgraphics.set_source_rgba(0.3, 0.6, 1, segmentOpacity);
  64. mgraphics.move_to(tracePoints[i].x, tracePoints[i].y);
  65. mgraphics.line_to(tracePoints[i + 1].x, tracePoints[i + 1].y);
  66. mgraphics.stroke();
  67. }
  68. }
  69.  
  70. mgraphics.set_line_width(2);
  71.  
  72. // Draw pendulum arms
  73. mgraphics.set_source_rgb(0.7, 0.7, 0.75);
  74. mgraphics.set_line_width(2);
  75. mgraphics.move_to(centerX, centerY);
  76. mgraphics.line_to(x1, y1);
  77. mgraphics.line_to(x2, y2);
  78. mgraphics.stroke();
  79.  
  80. // Draw bobs
  81. mgraphics.set_source_rgb(1, 0.4, 0.4);
  82. mgraphics.ellipse(x1 - mass1 / 2, y1 - mass1 / 2, mass1, mass1);
  83. mgraphics.fill();
  84.  
  85. mgraphics.set_source_rgb(0.4, 0.8, 1);
  86. mgraphics.ellipse(x2 - mass2 / 2, y2 - mass2 / 2, mass2, mass2);
  87. mgraphics.fill();
  88.  
  89. // Normalize and output coordinates
  90. const normalizedX2 = (x2 / this.box.rect[2]);
  91. const normalizedY2 = (y2 / this.box.rect[2]);
  92. outlet(0, normalizedX2, normalizedY2);
  93.  
  94. }
  95.  
  96. // RK4 integration for physics calculations
  97. function rk4Step(state, dt) {
  98. const k1 = calculateDerivatives(state);
  99. const k2 = calculateDerivatives({
  100. a1: state.a1 + k1.da1 * dt/2,
  101. a2: state.a2 + k1.da2 * dt/2,
  102. v1: state.v1 + k1.dv1 * dt/2,
  103. v2: state.v2 + k1.dv2 * dt/2
  104. });
  105. const k3 = calculateDerivatives({
  106. a1: state.a1 + k2.da1 * dt/2,
  107. a2: state.a2 + k2.da2 * dt/2,
  108. v1: state.v1 + k2.dv1 * dt/2,
  109. v2: state.v2 + k2.dv2 * dt/2
  110. });
  111. const k4 = calculateDerivatives({
  112. a1: state.a1 + k3.da1 * dt,
  113. a2: state.a2 + k3.da2 * dt,
  114. v1: state.v1 + k3.dv1 * dt,
  115. v2: state.v2 + k3.dv2 * dt
  116. });
  117.  
  118. return {
  119. da1: (k1.da1 + 2*k2.da1 + 2*k3.da1 + k4.da1) / 6,
  120. da2: (k1.da2 + 2*k2.da2 + 2*k3.da2 + k4.da2) / 6,
  121. dv1: (k1.dv1 + 2*k2.dv1 + 2*k3.dv1 + k4.dv1) / 6,
  122. dv2: (k1.dv2 + 2*k2.dv2 + 2*k3.dv2 + k4.dv2) / 6
  123. };
  124. }
  125.  
  126. function calculateDerivatives(state) {
  127. const { a1, a2, v1, v2 } = state;
  128. const den = 2 * mass1 + mass2 - mass2 * Math.cos(2 * a1 - 2 * a2);
  129.  
  130. const dv1 = (-g * (2 * mass1 + mass2) * Math.sin(a1)
  131. - mass2 * g * Math.sin(a1 - 2 * a2)
  132. - 2 * Math.sin(a1 - a2) * mass2 *
  133. (v2 * v2 * length2 + v1 * v1 * length1 * Math.cos(a1 - a2)))
  134. / (length1 * den);
  135.  
  136. const dv2 = (2 * Math.sin(a1 - a2) *
  137. (v1 * v1 * length1 * (mass1 + mass2)
  138. + g * (mass1 + mass2) * Math.cos(a1)
  139. + v2 * v2 * length2 * mass2 * Math.cos(a1 - a2)))
  140. / (length2 * den);
  141.  
  142. return {
  143. da1: v1,
  144. da2: v2,
  145. dv1: dv1,
  146. dv2: dv2
  147. };
  148. }
  149.  
  150. // Animation function for Task
  151. function animate() {
  152. if (!isAnimating) return;
  153.  
  154. // Physics integration with speed control
  155. const dt = 0.016 * simulationSpeed; // Base dt (60fps) multiplied by speed
  156. const state = { a1: angle1, a2: angle2, v1: velocity1, v2: velocity2 };
  157. const derivatives = rk4Step(state, dt);
  158.  
  159. velocity1 += derivatives.dv1 * dt;
  160. velocity2 += derivatives.dv2 * dt;
  161. angle1 += derivatives.da1 * dt;
  162. angle2 += derivatives.da2 * dt;
  163.  
  164. // Track trajectory
  165. const x2 = centerX + length1 * Math.sin(angle1) + length2 * Math.sin(angle2);
  166. const y2 = centerY + length1 * Math.cos(angle1) + length2 * Math.cos(angle2);
  167. tracePoints.push({ x: x2, y: y2 });
  168. if (tracePoints.length > maxTracePoints) tracePoints.shift();
  169.  
  170.  
  171. // Redraw the pendulum
  172. mgraphics.redraw();
  173. }
  174.  
  175. // Message handlers
  176. function anything() {
  177. var a = arrayfromargs(messagename, arguments);
  178.  
  179. if (a.length == 0) {
  180. post("Error: No command specified\n");
  181. return;
  182. }
  183.  
  184. var command = a[0];
  185. const MIN_LENGTH = 10; // Minimum length in pixels
  186. const MIN_MASS = 5; // Minimum mass
  187.  
  188. switch(command) {
  189. case "length1":
  190. if (a.length >= 2 && !isNaN(a[1])) {
  191. // Clip to minimum length
  192. length1 = Math.max(MIN_LENGTH, parseFloat(a[1]));
  193. if (parseFloat(a[1]) < MIN_LENGTH) {
  194. post("Warning: length1 clipped to minimum value of " + MIN_LENGTH + "\n");
  195. }
  196. mgraphics.redraw();
  197. } else {
  198. post("Error: length1 requires a numeric value\n");
  199. }
  200. break;
  201.  
  202. case "length2":
  203. if (a.length >= 2 && !isNaN(a[1])) {
  204. // Clip to minimum length
  205. length2 = Math.max(MIN_LENGTH, parseFloat(a[1]));
  206. if (parseFloat(a[1]) < MIN_LENGTH) {
  207. post("Warning: length2 clipped to minimum value of " + MIN_LENGTH + "\n");
  208. }
  209. mgraphics.redraw();
  210. } else {
  211. post("Error: length2 requires a numeric value\n");
  212. }
  213. break;
  214.  
  215. case "mass1":
  216. if (a.length >= 2 && !isNaN(a[1])) {
  217. // Clip to minimum mass
  218. mass1 = Math.max(MIN_MASS, parseFloat(a[1]));
  219. if (parseFloat(a[1]) < MIN_MASS) {
  220. post("Warning: mass1 clipped to minimum value of " + MIN_MASS + "\n");
  221. }
  222. mgraphics.redraw();
  223. } else {
  224. post("Error: mass1 requires a numeric value\n");
  225. }
  226. break;
  227.  
  228. case "mass2":
  229. if (a.length >= 2 && !isNaN(a[1])) {
  230. // Clip to minimum mass
  231. mass2 = Math.max(MIN_MASS, parseFloat(a[1]));
  232. if (parseFloat(a[1]) < MIN_MASS) {
  233. post("Warning: mass2 clipped to minimum value of " + MIN_MASS + "\n");
  234. }
  235. mgraphics.redraw();
  236. } else {
  237. post("Error: mass2 requires a numeric value\n");
  238. }
  239. break;
  240.  
  241. case "angle1":
  242. if (a.length >= 2 && !isNaN(a[1])) {
  243. angle1 = parseFloat(a[1]) * Math.PI / 180; // Convert degrees to radians
  244. mgraphics.redraw();
  245. } else {
  246. post("Error: angle1 requires a numeric value (in degrees)\n");
  247. }
  248. break;
  249.  
  250. case "angle2":
  251. if (a.length >= 2 && !isNaN(a[1])) {
  252. angle2 = parseFloat(a[1]) * Math.PI / 180; // Convert degrees to radians
  253. mgraphics.redraw();
  254. } else {
  255. post("Error: angle2 requires a numeric value (in degrees)\n");
  256. }
  257. break;
  258.  
  259. case "velocity1":
  260. if (a.length >= 2 && !isNaN(a[1])) {
  261. velocity1 = parseFloat(a[1]);
  262. mgraphics.redraw();
  263. } else {
  264. post("Error: velocity1 requires a numeric value\n");
  265. }
  266. break;
  267.  
  268. case "velocity2":
  269. if (a.length >= 2 && !isNaN(a[1])) {
  270. velocity2 = parseFloat(a[1]);
  271. mgraphics.redraw();
  272. } else {
  273. post("Error: velocity2 requires a numeric value\n");
  274. }
  275. break;
  276.  
  277. case "gravity":
  278. if (a.length >= 2 && !isNaN(a[1])) {
  279. g = parseFloat(a[1]);
  280. mgraphics.redraw();
  281. } else {
  282. post("Error: gravity requires a numeric value\n");
  283. }
  284. break;
  285.  
  286. case "trace":
  287. if (a.length >= 2 && !isNaN(a[1])) {
  288. maxTracePoints = Math.max(0, parseInt(a[1])); // Ensure non-negative
  289. tracePoints = tracePoints.slice(-maxTracePoints); // Trim existing points if needed
  290. mgraphics.redraw();
  291. } else {
  292. post("Error: trace requires a numeric value\n");
  293. }
  294. break;
  295.  
  296. case "speed":
  297. if (a.length >= 2 && !isNaN(a[1])) {
  298. const newSpeed = parseFloat(a[1]);
  299. if (newSpeed < 0) {
  300. post("Warning: speed cannot be negative. Setting to 0 (frozen)\n");
  301. simulationSpeed = 0;
  302. } else if (newSpeed > 10) {
  303. post("Warning: speed limited to maximum of 10x\n");
  304. simulationSpeed = 10;
  305. } else {
  306. simulationSpeed = newSpeed;
  307. }
  308. } else {
  309. post("Error: speed requires a numeric value (1.0 = normal speed)\n");
  310. }
  311. break;
  312.  
  313. case "reset":
  314. // Reset to initial conditions
  315. velocity1 = 0;
  316. velocity2 = 0;
  317. angle1 = Math.PI / 2;
  318. angle2 = Math.PI / 2;
  319. tracePoints = [];
  320. mgraphics.redraw();
  321. break;
  322.  
  323. case "trace":
  324. if (a.length >= 2 && !isNaN(a[1])) {
  325. const newLength = parseInt(a[1]);
  326. if (newLength < 0) {
  327. post("Warning: trace length cannot be negative. Setting to 0\n");
  328. maxTracePoints = 0;
  329. } else if (newLength > 1000) {
  330. post("Warning: trace length limited to maximum of 1000 points\n");
  331. maxTracePoints = 1000;
  332. } else {
  333. maxTracePoints = newLength;
  334. }
  335. // Trim existing points if needed
  336. tracePoints = tracePoints.slice(-maxTracePoints);
  337. mgraphics.redraw();
  338. post("Trace length set to " + maxTracePoints + " points\n");
  339. } else {
  340. post("Error: trace requires a numeric value (0 to clear trace)\n");
  341. }
  342. break;
  343.  
  344. case "clear":
  345. // Clear just the trajectory
  346. tracePoints = [];
  347. mgraphics.redraw();
  348. break;
  349.  
  350. default:
  351. post("Unknown command: " + command + "\n");
  352. post("Available commands: length1, length2, mass1, mass2, angle1, angle2, velocity1, velocity2, gravity, trace, reset, clear\n");
  353. break;
  354. }
  355. }
  356.  
  357. function bang() {
  358. // Toggle animation
  359. isAnimating = !isAnimating;
  360. if (isAnimating) {
  361. task.interval = 16; // 16ms interval for ~60fps
  362. task.repeat();
  363. } else {
  364. task.cancel();
  365. }
  366. }
  367.  
  368. // Clean up when the script is recompiled
  369. function clean() {
  370. isAnimating = false;
  371. task.cancel();
  372. }
Advertisement
Add Comment
Please, Sign In to add comment