// Chaotic Pendulum simulation // Copyright 2020, J. Donald Tillman. All rights reserved. import React, { useState, useEffect, useRef } from 'react'; import { add, atan2, sin, cos, concat, dotMultiply, lusolve } from 'mathjs'; import styled from 'styled-components'; export const PendulumDisplay = (props) => { const width = 630; const height = 630; const x0 = Math.round(0.5 * width); const y0 = Math.round(0.5 * height); const [pstate, setPstate] = useState(initialState); const hand = useRef(null); // for test and debug // const pe0 = potentialEnergy(initialState); // const pe = 0.01 * (potentialEnergy(pstate) - pe0); // const ke = 0.01 * kineticEnergy(pstate); const animationStep = () => { setPstate(s => incrementState(s, hand.current)); window.requestAnimationFrame(animationStep); }; // auto start useEffect(animationStep, []); const handleHand = (v) => { if (v) { if (hand.current) { // mouse dragged, update const h1 = hand.current && hand.current[1]; hand.current = [v + h1, h1]; } else { // mouse down, new hand.current = ([pstate[0][0], pstate[0][0] - v]); } } else { hand.current = null; } }; const [[x1, y1], [x2, y2], [x3, y3]] = tbarPoints(lengths[0], pstate[0][0], x0, y0); return (); }; // return the tbar's endpoints, 3 graphic x,y pairs const tbarPoints = (length: number, angle: number, x0: number, y0: number) => [[x0 - length * cos(angle), y0 + length * sin(angle)], [x0 + length * sin(angle), y0 + length * cos(angle)], [x0 + length * cos(angle), y0 - length * sin(angle)]]; const TBar = (props) => { const { length, angle, x, y } = props; const [[x1, y1], [x2, y2], [x3, y3]] = tbarPoints(length, angle, x, y); return (<> ); }; const SmallPendulum = (props) => { const { x, y, length, angle } = props; const x1 = x + length * sin(angle); const y1 = y + length * cos(angle); return (<> ); }; const Bar1 = styled.line` stroke: darkolivegreen; stroke-width: 12px; stroke-linecap: round; `; const Bar2 = styled.line` stroke: beige; stroke-width: 3px; stroke-linecap: round; `; const Pivot = styled.circle` fill: beige; stroke: darkolivegreen; `; const Mass = styled.circle` fill: url('#myGradient'); strokeN: darkolivegreen; stroke-widthN: 4px; `; // Grab the pendulum with the mouse. const MouseTrack = (props) => { const { x, y, r, onChange } = props; const valueRef = useRef(null); let a, e0; const handleEvent = (e) => { e.preventDefault(); e.stopPropagation(); switch (e.type) { case 'mouseup': case 'mouseout': case 'touchend': onChange(null); valueRef.current = null; return; case 'mousedown': a = mouseAngle(e.target, e.clientX, e.clientY) valueRef.current = a; onChange(a); break; case 'mousemove': if (null === valueRef.current) { return; } onChange(mouseAngle(e.target, e.clientX, e.clientY)) break; case 'touchstart': e0 = e.targetTouches[0]; a = mouseAngle(e0.target, e0.clientX, e0.clientY) valueRef.current = a; onChange(a); break; case 'touchmove': e0 = e.targetTouches[0]; a = mouseAngle(e0.target, e0.clientX, e0.clientY) onChange(a); break; } }; const mouseAngle = (target, clientX, clientY): number => { const bb = target.parentElement.getBoundingClientRect(); const xx = clientX - bb.left - x; const yy = -(clientY - bb.top - y); return atan2(yy, xx) as number; }; return (); }; const MouseTrackCircle = styled.circle` stroke: #00000000; fill: #00000000; &:hover { stroke: #88888822; fill: #eeeeee33; } `; // Math section: // array of masses, array of lengths var masses = [10, 4, 4, 4]; var lengths = [180, 120, 120, 120]; // angle and angle-dot for each body const initialState = [[0.03, 0], [0.04, 0], [-0.02, 0], [0.03, 0]]; // system parameters var delta = .375; var overclock = 30; var friction = 0.03; var g = 9.8; // the main simulation // hand is the mouse angle to start it going const incrementState = (startState: number[][], hand: null | number[]): number[][] => { let state = dotMultiply(1, startState) as number[][]; if (hand) { state[0][0] = hand[0]; state[0][1] = 0; } for (let i = 0; i < overclock; i++) { // lagrange calculated accelerations const accels = lagrangeAccels(state); // velocities const vs = column(state, 1); const f = -friction * ((hand) ? 1000 : 1) / overclock; let frictionAccels = dotMultiply(f, vs) as number[][]; const velsAccels = concat(vs, add(accels, frictionAccels) as number[][]); state = add(state, dotMultiply(delta / overclock, velsAccels)) as number[][]; } return state; }; // Solves with LaGrange, and returns [tdd0, tdd1, tdd2, tdd3]. const lagrangeAccels = (state: number[][]): number[][] => { const [[t0, td0], [t1, td1], [t2, td2], [t3, td3]] = state; const [m0, m1, m2, m3] = masses; const [l0, l1, l2, l3] = lengths; const sint0t1 = sin(t0 - t1); const cost0t1 = cos(t0 - t1); const sint0t2 = sin(t0 - t2); const cost0t2 = cos(t0 - t2); const sint0t3 = sin(t0 - t3); const cost0t3 = cos(t0 - t3); const left = [[(m0 + m1 + m2 + m3) * l0, m1 * l1 * sint0t1, m2 * l2 * cost0t2, - m3 * l3 * sint0t3], [l0 * sint0t1, l1, 0, 0], [l0 * cost0t2, 0, l2, 0], [-l0 * sint0t3, 0, 0, l3]]; const l0td02 = l0 * td0 * td0; const right = [m1 * l1 * td1 * td1 * cost0t1 - m2 * l2 * td2 * td2 * sint0t2 - m3 * l3 * td3 * td3 * cost0t3 - g * ((m0 + m2) * sin(t0) + (m1 - m3) * cos(t0)), - l0td02 * cost0t1 - g * sin(t1), l0td02 * sint0t2 - g * sin(t2), l0td02 * cost0t3 - g * sin(t3)]; const sol = lusolve(left, right) as number[][]; return sol; }; // math.column() doesn't seem to exist. const column = (value: number[][], column: number): number[][] => value.map(x => [x[column]]); // Below is for testing and debugging, plotting curves const potentialEnergy = (state: number[][]) => { const [[t0, _td0], [t1, _td1], [t2, _td2], [t3, _td3]] = state; const [m0, m1, m2, m3] = masses; const [l0, l1, l2, l3] = lengths; return -g * (l0 * ((m0 + m2) * cos(t0) + (m1 - m3) * sin(t0)) + m1 * l1 * cos(t1) + m2 * l2 * cos(t2) + m3 * l3 * cos(t3)); } const kineticEnergy = (state: number[][]) => { const [[t0, td0], [t1, td1], [t2, td2], [t3, td3]] = state; const [m0, m1, m2, m3] = masses; const [l0, l1, l2, l3] = lengths; return .5 * (m0 + m1 + m2 + m3) * l0 * l0 * td0 * td0 + 0.5 * m1 * l1 * l1 * td1 * td1 + m1 * l0 * l1 * td0 * td1 * sin(t0 - t1) + 0.5 * m2 * l2 * l2 * td2 * td2 + m2 * l0 * l2 * td0 * td2 * cos(t0 - t2) + 0.5 * m3 * l3 * l3 * td3 * td3 - m3 * l0 * l3 * td0 * td3 * sin(t0 - t3); }; const Plot = (props) => { const { y, width, val, color } = props; const [history, setHistory] = useState([0]); const [lineVal, setLineVal] = useState('0,0'); useEffect(() => { const newHistory = history.slice(Math.max(0, history.length - width)).concat([-val]) setHistory(newHistory); setLineVal(valsToPoly(0, y, newHistory)); }, [val]); return (); } // formatting points for svg polyline const valsToPoly = (x: number, y: number, vs: number[]) => vs.map((v, i) => `\${x + i},\${y + Math.round(v)}`).join(' ');