Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // JAVASCRIPT FOR DOUBLE PENDULUM - V8UI
- // =====================================
- //
- // Requires Max 9. Paste into a V8UI object. Accepts messages as shown under the anything() function's switch/case.
- //
- // =====================================
- autowatch = 1;
- outlets = 2;
- // Initialize physics parameters
- const g = 9.81; // gravity
- let length1 = 175;
- let length2 = 45;
- let mass1 = 10;
- let mass2 = 13;
- let angle1 = Math.PI / 2;
- let angle2 = Math.PI / 2;
- let velocity1 = 0;
- let velocity2 = 0;
- let simulationSpeed = 0.75;
- // Hang point
- const viewsize = mgraphics.size;
- const width = viewsize[0];
- const height = viewsize[1];
- const centerX = width * 0.5;
- const centerY = height * 0.25;
- // Trace points for trajectory
- let maxTracePoints = 800;
- let traceOpacity = 0.35; // Initial opacity value
- let tracePoints = [];
- // Animation control
- let isAnimating = true;
- const task = new Task(animate, this);
- // Setup mgraphics
- mgraphics.init();
- mgraphics.relative_coords = 0;
- mgraphics.autofill = 0;
- function paint() {
- // Dark background - commented out to make background transparent
- // mgraphics.set_source_rgb(0.1, 0.1, 0.12, 0); // Dark slate color
- // mgraphics.rectangle(0, 0, this.box.rect[2], this.box.rect[3]);
- // mgraphics.fill();
- // Calculate positions
- const x1 = centerX + length1 * Math.sin(angle1);
- const y1 = centerY + length1 * Math.cos(angle1);
- const x2 = x1 + length2 * Math.sin(angle2);
- const y2 = y1 + length2 * Math.cos(angle2);
- // Draw trajectory with opacity gradient
- if (tracePoints.length) {
- mgraphics.set_line_width(2.5);
- for (let i = 0; i < tracePoints.length - 1; i++) {
- const progress = i / (tracePoints.length - 1);
- const segmentOpacity = progress * 0.5;
- mgraphics.set_source_rgba(0.3, 0.6, 1, segmentOpacity);
- mgraphics.move_to(tracePoints[i].x, tracePoints[i].y);
- mgraphics.line_to(tracePoints[i + 1].x, tracePoints[i + 1].y);
- mgraphics.stroke();
- }
- }
- mgraphics.set_line_width(2);
- // Draw pendulum arms
- mgraphics.set_source_rgb(0.7, 0.7, 0.75);
- mgraphics.set_line_width(2);
- mgraphics.move_to(centerX, centerY);
- mgraphics.line_to(x1, y1);
- mgraphics.line_to(x2, y2);
- mgraphics.stroke();
- // Draw bobs
- mgraphics.set_source_rgb(1, 0.4, 0.4);
- mgraphics.ellipse(x1 - mass1 / 2, y1 - mass1 / 2, mass1, mass1);
- mgraphics.fill();
- mgraphics.set_source_rgb(0.4, 0.8, 1);
- mgraphics.ellipse(x2 - mass2 / 2, y2 - mass2 / 2, mass2, mass2);
- mgraphics.fill();
- // Normalize and output coordinates
- const normalizedX2 = (x2 / this.box.rect[2]);
- const normalizedY2 = (y2 / this.box.rect[2]);
- outlet(0, normalizedX2, normalizedY2);
- }
- // RK4 integration for physics calculations
- function rk4Step(state, dt) {
- const k1 = calculateDerivatives(state);
- const k2 = calculateDerivatives({
- a1: state.a1 + k1.da1 * dt/2,
- a2: state.a2 + k1.da2 * dt/2,
- v1: state.v1 + k1.dv1 * dt/2,
- v2: state.v2 + k1.dv2 * dt/2
- });
- const k3 = calculateDerivatives({
- a1: state.a1 + k2.da1 * dt/2,
- a2: state.a2 + k2.da2 * dt/2,
- v1: state.v1 + k2.dv1 * dt/2,
- v2: state.v2 + k2.dv2 * dt/2
- });
- const k4 = calculateDerivatives({
- a1: state.a1 + k3.da1 * dt,
- a2: state.a2 + k3.da2 * dt,
- v1: state.v1 + k3.dv1 * dt,
- v2: state.v2 + k3.dv2 * dt
- });
- return {
- da1: (k1.da1 + 2*k2.da1 + 2*k3.da1 + k4.da1) / 6,
- da2: (k1.da2 + 2*k2.da2 + 2*k3.da2 + k4.da2) / 6,
- dv1: (k1.dv1 + 2*k2.dv1 + 2*k3.dv1 + k4.dv1) / 6,
- dv2: (k1.dv2 + 2*k2.dv2 + 2*k3.dv2 + k4.dv2) / 6
- };
- }
- function calculateDerivatives(state) {
- const { a1, a2, v1, v2 } = state;
- const den = 2 * mass1 + mass2 - mass2 * Math.cos(2 * a1 - 2 * a2);
- const dv1 = (-g * (2 * mass1 + mass2) * Math.sin(a1)
- - mass2 * g * Math.sin(a1 - 2 * a2)
- - 2 * Math.sin(a1 - a2) * mass2 *
- (v2 * v2 * length2 + v1 * v1 * length1 * Math.cos(a1 - a2)))
- / (length1 * den);
- const dv2 = (2 * Math.sin(a1 - a2) *
- (v1 * v1 * length1 * (mass1 + mass2)
- + g * (mass1 + mass2) * Math.cos(a1)
- + v2 * v2 * length2 * mass2 * Math.cos(a1 - a2)))
- / (length2 * den);
- return {
- da1: v1,
- da2: v2,
- dv1: dv1,
- dv2: dv2
- };
- }
- // Animation function for Task
- function animate() {
- if (!isAnimating) return;
- // Physics integration with speed control
- const dt = 0.016 * simulationSpeed; // Base dt (60fps) multiplied by speed
- const state = { a1: angle1, a2: angle2, v1: velocity1, v2: velocity2 };
- const derivatives = rk4Step(state, dt);
- velocity1 += derivatives.dv1 * dt;
- velocity2 += derivatives.dv2 * dt;
- angle1 += derivatives.da1 * dt;
- angle2 += derivatives.da2 * dt;
- // Track trajectory
- const x2 = centerX + length1 * Math.sin(angle1) + length2 * Math.sin(angle2);
- const y2 = centerY + length1 * Math.cos(angle1) + length2 * Math.cos(angle2);
- tracePoints.push({ x: x2, y: y2 });
- if (tracePoints.length > maxTracePoints) tracePoints.shift();
- // Redraw the pendulum
- mgraphics.redraw();
- }
- // Message handlers
- function anything() {
- var a = arrayfromargs(messagename, arguments);
- if (a.length == 0) {
- post("Error: No command specified\n");
- return;
- }
- var command = a[0];
- const MIN_LENGTH = 10; // Minimum length in pixels
- const MIN_MASS = 5; // Minimum mass
- switch(command) {
- case "length1":
- if (a.length >= 2 && !isNaN(a[1])) {
- // Clip to minimum length
- length1 = Math.max(MIN_LENGTH, parseFloat(a[1]));
- if (parseFloat(a[1]) < MIN_LENGTH) {
- post("Warning: length1 clipped to minimum value of " + MIN_LENGTH + "\n");
- }
- mgraphics.redraw();
- } else {
- post("Error: length1 requires a numeric value\n");
- }
- break;
- case "length2":
- if (a.length >= 2 && !isNaN(a[1])) {
- // Clip to minimum length
- length2 = Math.max(MIN_LENGTH, parseFloat(a[1]));
- if (parseFloat(a[1]) < MIN_LENGTH) {
- post("Warning: length2 clipped to minimum value of " + MIN_LENGTH + "\n");
- }
- mgraphics.redraw();
- } else {
- post("Error: length2 requires a numeric value\n");
- }
- break;
- case "mass1":
- if (a.length >= 2 && !isNaN(a[1])) {
- // Clip to minimum mass
- mass1 = Math.max(MIN_MASS, parseFloat(a[1]));
- if (parseFloat(a[1]) < MIN_MASS) {
- post("Warning: mass1 clipped to minimum value of " + MIN_MASS + "\n");
- }
- mgraphics.redraw();
- } else {
- post("Error: mass1 requires a numeric value\n");
- }
- break;
- case "mass2":
- if (a.length >= 2 && !isNaN(a[1])) {
- // Clip to minimum mass
- mass2 = Math.max(MIN_MASS, parseFloat(a[1]));
- if (parseFloat(a[1]) < MIN_MASS) {
- post("Warning: mass2 clipped to minimum value of " + MIN_MASS + "\n");
- }
- mgraphics.redraw();
- } else {
- post("Error: mass2 requires a numeric value\n");
- }
- break;
- case "angle1":
- if (a.length >= 2 && !isNaN(a[1])) {
- angle1 = parseFloat(a[1]) * Math.PI / 180; // Convert degrees to radians
- mgraphics.redraw();
- } else {
- post("Error: angle1 requires a numeric value (in degrees)\n");
- }
- break;
- case "angle2":
- if (a.length >= 2 && !isNaN(a[1])) {
- angle2 = parseFloat(a[1]) * Math.PI / 180; // Convert degrees to radians
- mgraphics.redraw();
- } else {
- post("Error: angle2 requires a numeric value (in degrees)\n");
- }
- break;
- case "velocity1":
- if (a.length >= 2 && !isNaN(a[1])) {
- velocity1 = parseFloat(a[1]);
- mgraphics.redraw();
- } else {
- post("Error: velocity1 requires a numeric value\n");
- }
- break;
- case "velocity2":
- if (a.length >= 2 && !isNaN(a[1])) {
- velocity2 = parseFloat(a[1]);
- mgraphics.redraw();
- } else {
- post("Error: velocity2 requires a numeric value\n");
- }
- break;
- case "gravity":
- if (a.length >= 2 && !isNaN(a[1])) {
- g = parseFloat(a[1]);
- mgraphics.redraw();
- } else {
- post("Error: gravity requires a numeric value\n");
- }
- break;
- case "trace":
- if (a.length >= 2 && !isNaN(a[1])) {
- maxTracePoints = Math.max(0, parseInt(a[1])); // Ensure non-negative
- tracePoints = tracePoints.slice(-maxTracePoints); // Trim existing points if needed
- mgraphics.redraw();
- } else {
- post("Error: trace requires a numeric value\n");
- }
- break;
- case "speed":
- if (a.length >= 2 && !isNaN(a[1])) {
- const newSpeed = parseFloat(a[1]);
- if (newSpeed < 0) {
- post("Warning: speed cannot be negative. Setting to 0 (frozen)\n");
- simulationSpeed = 0;
- } else if (newSpeed > 10) {
- post("Warning: speed limited to maximum of 10x\n");
- simulationSpeed = 10;
- } else {
- simulationSpeed = newSpeed;
- }
- } else {
- post("Error: speed requires a numeric value (1.0 = normal speed)\n");
- }
- break;
- case "reset":
- // Reset to initial conditions
- velocity1 = 0;
- velocity2 = 0;
- angle1 = Math.PI / 2;
- angle2 = Math.PI / 2;
- tracePoints = [];
- mgraphics.redraw();
- break;
- case "trace":
- if (a.length >= 2 && !isNaN(a[1])) {
- const newLength = parseInt(a[1]);
- if (newLength < 0) {
- post("Warning: trace length cannot be negative. Setting to 0\n");
- maxTracePoints = 0;
- } else if (newLength > 1000) {
- post("Warning: trace length limited to maximum of 1000 points\n");
- maxTracePoints = 1000;
- } else {
- maxTracePoints = newLength;
- }
- // Trim existing points if needed
- tracePoints = tracePoints.slice(-maxTracePoints);
- mgraphics.redraw();
- post("Trace length set to " + maxTracePoints + " points\n");
- } else {
- post("Error: trace requires a numeric value (0 to clear trace)\n");
- }
- break;
- case "clear":
- // Clear just the trajectory
- tracePoints = [];
- mgraphics.redraw();
- break;
- default:
- post("Unknown command: " + command + "\n");
- post("Available commands: length1, length2, mass1, mass2, angle1, angle2, velocity1, velocity2, gravity, trace, reset, clear\n");
- break;
- }
- }
- function bang() {
- // Toggle animation
- isAnimating = !isAnimating;
- if (isAnimating) {
- task.interval = 16; // 16ms interval for ~60fps
- task.repeat();
- } else {
- task.cancel();
- }
- }
- // Clean up when the script is recompiled
- function clean() {
- isAnimating = false;
- task.cancel();
- }
Advertisement
Add Comment
Please, Sign In to add comment