I am trying to make a simple simulation of pendulum using Runge–Kutta fourth-order method. I am using p5.js. Usually it calculates the angles properly but sometimes it just starts spinning randomly etc. I have no idea how to resolve this issue it seems to be a problem with my implementation of said algorithm I think?
I used code from pang tao's introduction to Computational Physics as an inspiration and it seems quite similar
First part of said code in Fortran
Second part
let screenWidth = 1300;
let screenHight = 1970;
let angleChangeDifference;
let gSlider;
let lSlider;
let aFrequencySlider;
let dumpingSlider;
let startAngleSlider;
let timestepSlider;
let timeMaxSlider;
let dForceSlider;
let initForceSlider;
let startAngle = 0;
let currentAngle = 0;
let circleX = 0;
let circleY = 200;
let circleRWidth = 100;
let circleRHeight = 100;
let lineXStart = 0;
let lineYStart = 0;
let lineXEnd = circleX;
let lineYEnd = (circleY - circleRHeight / 2);
function setup() {
createCanvas(screenWidth, screenHight);
changeScreenDeafultStartingPoint(screenWidth / 2, 100);
frameRate(60)
createSliders();
setInterval(showSliderValue, 100);
startButton.mouseClicked(start);
restartButton.mouseClicked(restart);
chartButton.mouseClicked(enableChart);
background(200);
}
let angleSign = '\u00B0';
let omegaSign = '\u03C9';
let chartOn = false;
let step = 0;
function draw() {
startupConfiguration()
showSliderValue()
if (step >= 1) {
rotatePendulum();
if (step == 1) {}
if (step == 2)
startButton.remove();
chartButton.position(20, 340);
if (chartOn == true) {
createChart(0, 1, 'czas (s)', 'kat (' + angleSign + ')', degreesArr);
createChart(0, 320, 'czas (s)', omegaSign + ' (' + angleSign + '/s)', omegaArr);
scale(2);
createPhaseChart(290, 80, 'kat (' + angleSign + ')', omegaSign + ' (' + angleSign + '/s)', degreesArr);
scale(0.5);
}
}
line(lineXStart, lineYStart, lineXEnd, lineYEnd + (20 * lSlider.value()));
fill(200, 76, 43)
ellipse(circleX, circleY + (20 * lSlider.value()), circleRWidth, circleRHeight);
}
function createSliders() {
gSlider = createSlider(0.05, 20, 9.81, 0.01);
gSlider.position(1100, -90);
lSlider = createSlider(0.5, 10, 5, 0.5);
lSlider.position(1100, -50);
initForceSlider = createSlider(-5, 5, 0, 0.5);
initForceSlider.position(1100, 50);
dForceSlider = createSlider(-2, 2, 0.9, 0.05)
dForceSlider.position(1100, 90);
aFrequencySlider = createSlider(-2, 2, 2 / 3, 1 / 3);
aFrequencySlider.position(1100, 130);
dumpingSlider = createSlider(0.00, 1.5, 0.5, 0.05);
dumpingSlider.position(1100, 170);
startAngleSlider = createSlider(-Math.PI / 2, Math.PI / 2, 0, Math.PI / 32);
startAngleSlider.position(1100, 210);
timestepSlider = createSlider(0, 1000, 100, 10);
timestepSlider.position(1100, 250);
timeMaxSlider = createSlider(10, 10000, 1000, 10);
timeMaxSlider.position(1100, 290);
startButton = createButton('ZATWIERDZ', false);
startButton.position(100, 310)
restartButton = createButton('RESTART', false);
restartButton.position(20, 310)
chartButton = createButton('WYKRES', false);
chartButton.position(-200, 340);
}
function showSliderValue() {
background(200);
fill(0, 0, 0)
text('sila poczatkowa', 440, -60)
text(initForceSlider.value(), 400, -42)
text('sila sprawcza', 440, -20)
text(dForceSlider.value(), 400, -2)
text('czestosc katowa', 440, 20)
text(aFrequencySlider.value(), 400, 42)
text('tlumienie', 440, 60)
text(dumpingSlider.value(), 400, 82)
text('kat poczatkowy', 440, 100)
text(int(degrees(startAngleSlider.value())), 400, 122)
text('krok czasowy (N1)', 440, 140)
text(timestepSlider.value(), 400, 162)
text('dlugosc symulacji (N2)', 440, 180)
text(timeMaxSlider.value(), 400, 202)
}
function start() {
angleIndex = 0;
step++;
startAngle = startAngleSlider.value();
currentAngle = startAngle;
angleChangeDifference = simulate();
rotatePendulum()
startButton.html("START")
}
function restart() {
window.location.reload();
}
function enableChart() {
chartOn = true;
}
function createChart(moveByX, moveByY, xName, yName, table) {
rotate(-currentAngle);
scale(1.1);
translate(moveByX, moveByY);
strokeWeight(1);
line(-500, 500, 530, 500);
line(-500, 700, 530, 700);
line(-500, 500, -500, 700);
line(530, 500, 530, 700);
strokeWeight(1);
let counter = 0;
for (i = 510; i < 700; i += 10) {
if (counter < 9 && counter % 2 == 0)
text(90 - 10 * counter, -520, i + 5)
else if (counter == 9 && counter % 2 == 0)
text(90 - 10 * counter, -515, i + 5)
else if (counter > 9 && counter % 2 == 0)
text(90 - 10 * counter, -525, i + 5)
line(-505, i, 530, i);
counter++;
}
textSize(25);
text(xName, -20, 750)
textSize(12);
counter = 0;
for (i = -490; i < 535; i += 25) {
line(i, 500, i, 705);
if (counter % 4 == 0) {
line(i, 500, i, 705);
text(counter * 2.5, i - 5, 715);
}
counter++;
}
rotate(-90);
textSize(25);
text(yName, -670, -550)
textSize(12);
rotate(90);
fillChartByTableValues(table);
translate(-moveByX, -moveByY);
scale(0.91);
rotate(currentAngle);
}
function fillChartByTableValues(table) {
strokeWeight(2);
stroke(0, 0, 255);
for (i = 0; i < timeArr.length - 1; i++) {
FirstPointX = -490 + timeArr[i] * 10;
FirstPointY = 600 + table[i] * (-1);
SecondPointX = -490 + timeArr[i + 1] * 10;
SecondPointY = 600 + table[i + 1] * (-1);
line(FirstPointX, FirstPointY, SecondPointX, SecondPointY);
}
stroke(0, 0, 0);
strokeWeight(0.1);
}
function createPhaseChart(moveByX, moveByY, xName, yName, table) {
rotate(-currentAngle);
scale(1.1);
translate(moveByX, moveByY);
strokeWeight(1);
line(-500, 500, -300, 500);
line(-500, 700, -300, 700);
line(-500, 500, -500, 700);
line(-300, 500, -300, 700);
strokeWeight(1);
let counter = 0;
textSize(8);
for (i = 510; i < 700; i += 10) {
if (counter < 9 && counter % 2 == 0)
text(90 - 10 * counter, -517, i + 3)
else if (counter == 9 && counter % 2 == 0)
text(90 - 10 * counter, -512, i + 3)
else if (counter > 9 && counter % 2 == 0)
text(90 - 10 * counter, -520, i + 3)
line(-505, i, -300, i);
counter++;
}
textSize(12);
textSize(15);
text(xName, -430, 735)
textSize(12);
counter = 0;
textSize(8);
for (i = -490; i < -300; i += 10) {
line(i, 500, i, 705);
if (counter < 9 && counter % 2 == 0)
text(-90 + 10 * counter, i - 7, 715);
else if (counter == 9 && counter % 2 == 0)
text(-90 + 10 * counter, i - 2, 715);
else if (counter > 9 && counter % 2 == 0)
text(-90 + 10 * counter, i - 4, 715);
counter++;
}
textSize(12);
rotate(-90);
textSize(15);
text(yName, -620, -528)
textSize(12);
rotate(90);
fillPhaseChartByTableValues(degreesArr, omegaArr)
translate(-moveByX, -moveByY);
scale(0.91);
rotate(currentAngle);
}
function fillPhaseChartByTableValues(tableX, tableY) {
translate(-400, 600);
strokeWeight(1);
stroke(0, 0, 255);
for (i = 0; i < tableX.length; i++) {
ellipse(tableX[i], tableY[i], 0.5, 0.5);
}
translate(400, -600);
stroke(0, 0, 0);
strokeWeight(0.1);
}
function startupConfiguration() {
background(200);
angleMode(DEGREES);
changeScreenDeafultStartingPoint(screenWidth / 2, 100);
}
function changeScreenDeafultStartingPoint(x, y) {
translate(x, y);
}
let angleIndex = 0;
function rotatePendulum() {
currentAngle = angleChangeDifference[angleIndex] * (180 / PI);
rotate(currentAngle);
if (step > 1) {
angleIndex++
}
}
function calculateIntegral(t, q, dt, f) {
let k1 = f(t, q).map(val => val * dt);
let temp = k1.map(val => val * 0.5);
temp = temp.map((val, index) => val + q[index])
let k2 = f(t + 0.5 * dt, temp).map(val => val * dt);
temp = k2.map(val => val * 0.5);
temp = temp.map((val, index) => val + q[index])
let k3 = f(t + 0.5 * dt, temp).map(val => val * dt);
temp = q.map((val, index) => val + k3[index]);
let k4 = f(t + dt, temp).map(val => val * dt);
temp = k2.map((val, index) => val + k3[index])
temp = temp.map(val => val * 2)
temp = temp.map((val, index) => (val + k1[index] + k4[index]) / 6)
temp = temp.map((val, index) => val + q[index])
return [t + dt, temp];
}
function modelPendulum(t, q) {
let c = dumpingSlider.value();
let fw = dForceSlider.value();
let w = aFrequencySlider.value();
let x1 = q[0];
let x2 = q[1];
return [x2, -(Math.sin(x1)) - (c * x2) + (fw * Math.cos(w * t))];
}
let degreesArr, timeArr;
function simulate() {
let t = 0.0;
let dt = (3 * Math.PI) / timestepSlider.value()
let tf = timeMaxSlider.value() * dt
let q = [startAngle, initForceSlider.value()];
let Nt = int(Math.round((tf - t) / dt)) + 1;
let solution = new Array(q.length + 1);
for (i = 0; i < q.length + 1; i++) {
solution[i] = new Array(Nt).fill(0);
}
solution[0][0] = t;
solution[1][0] = q[0];
solution[2][0] = q[1];
k = 1;
while (t <= tf) {
let temporaryResult = [];
temporaryResult = calculateIntegral(t, q, dt, modelPendulum);
t = temporaryResult[0];
q = temporaryResult[1];
solution[0][k] = t;
solution[1][k] = q[0];
solution[2][k] = q[1];
k = k + 1
}
timeArr = solution[0];
degreesArr = solution[1];
omegaArr = solution[2];
let counter = 0;
let ifChaos = false;
while (counter != degreesArr.length - 1 && ifChaos != true) {
if (degreesArr[counter] > 13.5 || degreesArr[counter] < -13.5) {
ifChaos = true;
}
counter++;
}
if (ifChaos == true) {
degreesArr = degreesArr.map(val => val * 5.32);
omegaArr = omegaArr.map(val => val * 23.32);
} else {
degreesArr = degreesArr.map(val => val * 35.32);
omegaArr = omegaArr.map(val => val * 35.32);
}
return solution[1]
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.1.9/lib/p5.js"></script>
Am not familiar with P5, so here's a solution using ThreeJS leveraging the Runge-Kutta algorithm from the Mathematics Stack Exchange.
For convenience, I have wrapped the Runge-Kutta algorithm in a class, with the constructor taking the initial parameters of:
the gravity acceleration constant g (for earth, 9.81 meters/sec/sec),
the pendulum length (in meters),
the initial angle of the pendulum (where 0 is straight down),
the initial angular velocity (in meters/sec), and
the max time increment. (Since the Runge-Kutta is employed to solve a second order differential equation using time as the variable, it appears based on this author's experimentation that one cannot overextend the delta time increment and still retain accuracy of the resulting pendulum position and velocity. This parameter simply limits the maximum t value passed in the updatePosition method, with a default of 0.1s.)
To assist in the use of the Runta-Kutta algorithm, the code below simulates two 1 meter pendulums:
The first having an initial position of -PI/2 (-90 deg) with no starting angular velocity.
The second having an initial position of PI (180 deg) with a very small starting angular velocity.
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three#0.115.0/build/three.module.js';
class RungeKutta {
constructor( g, pendulumLength, initialAngle, angularVelocity, maxTimeDelta ) {
this.g = g;
this.pendulumLength = pendulumLength;
this.theta = initialAngle;
this.omega = angularVelocity;
this.maxTimeDelta = maxTimeDelta || 0.1;
}
updatePosition( t ) {
let self = this;
function omegaDot( theta ){
return -( self.g / self.pendulumLength ) * Math.sin( theta );
}
function thetaDot( omega ){
return omega;
}
// If the browser tab becomes inactive, then there will be a large
// time delta, which will disrupt the RungeKutta algorithm. If more
// than max allowed seconds has lapsed, then reset the timer.
if ( self.maxTimeDelta < t ) {
t = self.maxTimeDelta;
}
let aomega = omegaDot( self.theta );
let atheta = thetaDot( self.omega );
let bomega = omegaDot( self.theta + 0.5 * t * atheta );
let btheta = thetaDot( self.omega + 0.5 * t * aomega );
let comega = omegaDot( self.theta + 0.5 * t * btheta );
let ctheta = thetaDot( self.omega + 0.5 * t * bomega );
let domega = omegaDot( self.theta + t * ctheta );
let dtheta = thetaDot( self.omega + t * comega );
self.omega = self.omega + ( t / 6 ) * ( aomega + 2 * bomega + 2 * comega + domega );
self.theta = self.theta + ( t / 6 ) * ( atheta + 2 * btheta + 2 * ctheta + dtheta );
return self;
}
}
//
// Set up the ThreeJS environment.
//
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
var camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 );
camera.position.set( 0, 0, 100 );
camera.lookAt( 0, 0, 0 );
var scene = new THREE.Scene();
//
// Create the pendulum mesh.
//
var length = 30, width = 1;
var shape = new THREE.Shape();
shape.moveTo( -width / 2, 0 );
shape.lineTo( +width / 2, 0 );
shape.lineTo( +width / 2, -length );
shape.lineTo( -width / 2, -length );
shape.lineTo( -width / 2, 0 );
var extrudeSettings = {
steps: 2,
depth: 2,
bevelEnabled: true,
bevelThickness: .25,
bevelSize: .25,
bevelOffset: 0,
bevelSegments: 1
};
var geometry = new THREE.ExtrudeBufferGeometry( shape, extrudeSettings );
var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
var mesh0 = new THREE.Mesh( geometry, material );
mesh0.position.x = -17;
scene.add( mesh0 );
var mesh1 = mesh0.clone();
mesh1.position.x = +17;
scene.add( mesh1 );
//
// And now animate the pendulum using RungeKutta.
//
let pendulumState0 = new RungeKutta( 9.81, 1, -Math.PI / 2, 0, 0.1 );
let pendulumState1 = new RungeKutta( 9.81, 1, Math.PI, 0.01, 0.1 );
let now = performance.now();
let lastTimer = now;
var animate = function () {
requestAnimationFrame( animate );
now = performance.now();
pendulumState0.updatePosition( ( now - lastTimer ) / 1000 );
pendulumState1.updatePosition( ( now - lastTimer ) / 1000 );
lastTimer = now;
mesh0.rotation.z = pendulumState0.theta;
mesh1.rotation.z = pendulumState1.theta;
renderer.render( scene, camera );
};
animate();
</script>
Hopefully this will assist with your P5 implementation.
This is an example of a pendulum with p5, using Leapfrog method, but it can be easily adapted to use Runge-Kutta 4 (Leapfrog has the advantage that it is symplectic, at variance with RK).
For a simple pendulum you have to solve the second order differential (Newton or Euler-Lagrange) equation
d^2 O / dt^2 = -(g/l) sin(O).
where O is the angle from the vertical.
To apply numerical methods is convenient first to translate it into an equivalent system of two first order differential equations
dw/dt = -(g/l) sin(O)
dO/dt = w
you can solve this system for instance with Runge-Kutta4 or other methods. A very simple one to start with is the simple 2nd order Leapfrog method. You discretize times in steps dt so
w_n == w((n-1/2)dt)
O_n == O(n dt)
and then do the following iteratively in a loop
w_{n+1} = w_n -(g/l) sin(O_n) dt
O_{n+1} = O_n + w_{n+1} dt
This method is implemented in a pendulum class in this code, together with its parameters, a very simple render() function for visualization in p5, and a method for calculating energy for instance (can be used to check conservation of energy if not forced). It is very simple and you can test it. Of course, small time-steps dt should be used in order to have good accuracy.
If you wish to use Runge-Kutta4 or other high order method it is convenient to write functions Fw(t,w,O) and FO(t,w,O) such that the equations read
dw/dt = Fw(t,w,O) (== -(g/l) sin(O) + F0 cos(W t))
dO/dt = FO(t,w,O) (== w )
where I have now included a forcing F0 cos(W t) to show, just in case, how to add any time dependent force.
You can then discretize in time (no leapfrog steps)
t_n == n dt
w_n == w(n dt)
O_n == O(n dt)
and calculate the quantities k1, k2, k3, k4 of RK4 each time step n.
Note that for this case the ki are two dimensional vectors {kiw,kiO}, and the function f(t,w,O) =={Fw(t,w,O),FO(t,w,O)} is also a 2d vector.
Taking this into account, and defining separately the functions Fw and FO you can easily replace the Leapfrog method of the p5 example 1 by a RK4 method, and add forcing if desired.
If you see very strange behaviour, you should check signs and a proper setup of the RK4 method.
I hope this can help.
let P1;
function setup() {
createCanvas(720, 400);
P1=new PenduloSimple(3.14,0.0);
}
function draw() {
background(220);
P1.render();
P1.leapFrog();
}
class PenduloSimple {
constructor(ang, velang) {
this.ang = ang;
this.velang = velang;
this.g = 9.8;
this.dt = 0.01;
this.l = 1.0;
this.m = 0.1;
this.E0 = this.Energy();
}
Energy() {
let E=
this.m * this.l * this.l * this.velang * this.velang * 0.5 - this.m * this.g * this.l * cos(this.ang);
return E;
}
leapFrog() {
// Método de Leapfrog
this.velang =
this.velang + this.dt*(-this.g/this.l)*sin(this.ang);
this.ang = this.ang + this.dt*this.velang;
}
render(){
var x0 = width / 2;
var y0 = height / 2;
var mult0 = 100;
var x = this.l * sin(this.ang);
var y = this.l * cos(this.ang);
var xplot = x0 + x * mult0;
var yplot = y0 + y * mult0;
// Draw a circle
stroke(50);
fill(100);
ellipse(xplot, yplot, 24, 24);
// La cuerda
stroke(50);
line(x0, y0, xplot, yplot);
let E = this.Energy();
stroke(0);
fill(255, 0, 0);
rect(10, height * 0.5, 20, -E * height * 0.45 / this.E0);
fill(0, 255, 0);
rect(30, height * 0.5, 20, -this.ang * height * 0.4 / 6.28);
fill(0, 0, 255);
rect(50, height * 0.5, 20, -this.velang * mult0 * 0.1);
fill(1);
stroke(255);
fill(255, 0, 0);
text("E: " + E, width * 0.1, 20);
fill(50, 200, 50);
text("ang: " + this.ang, width * 0.1, 30);
fill(0, 0, 255);
text("velang: " + this.velang, width * 0.1, 40);
}
}
I need to build canvas animation like design requires. I spend almost 3 days but I'm not able to do anything like in design. Here a REQUESTED design!. And here - what I've got for now: current implementation which definitely not what requested from design .I need only animation of planet from particles at background (also whole process of animation changes in time, it starts from few particles but then amount growing and movings directions of particles changes)
here my current code:
export class CanvasComponent implements OnInit {
sphereRad = 280;
radius_sp = 1;
distance = 600;
particle_size = 0.7;
constructor() { }
ngOnInit() {
this.canvasApp();
}
canvasApp () {
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let displayWidth;
let displayHeight;
let wait;
let count;
let numToAddEachFrame;
let particleList;
let recycleBin;
let particleAlpha;
let r, g, b;
let fLen;
let m;
let projCenterX;
let projCenterY;
let zMax;
let turnAngle;
let turnSpeed;
let sphereCenterX, sphereCenterY, sphereCenterZ;
let particleRad;
let zeroAlphaDepth;
let randAccelX, randAccelY, randAccelZ;
let gravity;
let rgbString;
// we are defining a lot of letiables used in the screen update functions globally so that they don't have to be redefined every frame.
let p;
let outsideTest;
let nextParticle;
let sinAngle;
let cosAngle;
let rotX, rotZ;
let depthAlphaFactor;
let i;
let theta, phi;
let x0, y0, z0;
// INITIALLI
const init = () => {
wait = 1;
count = wait - 1;
numToAddEachFrame = 30;
// particle color
r = 255;
g = 255;
b = 255;
rgbString = 'rgba(' + r + ',' + g + ',' + b + ','; // partial string for color which will be completed by appending alpha value.
particleAlpha = 1; // maximum alpha
displayWidth = canvas.width;
displayHeight = canvas.height;
fLen = this.distance; // represents the distance from the viewer to z=0 depth.
// projection center coordinates sets location of origin
projCenterX = displayWidth / 2;
projCenterY = displayHeight / 2;
// we will not draw coordinates if they have too large of a z-coordinate (which means they are very close to the observer).
zMax = fLen - 2;
particleList = {};
recycleBin = {};
// random acceleration factors - causes some random motion
randAccelX = 0.1;
randAccelY = 0.1;
randAccelZ = 0.1;
gravity = -0; // try changing to a positive number (not too large, for example 0.3), or negative for floating upwards.
particleRad = this.particle_size;
sphereCenterX = 0;
sphereCenterY = 0;
sphereCenterZ = -3 - this.sphereRad;
// alpha values will lessen as particles move further back, causing depth-based darkening:
zeroAlphaDepth = 0;
turnSpeed = 2 * Math.PI / 1200; // the sphere will rotate at this speed (one complete rotation every 1600 frames).
turnAngle = 0; // initial angle
// timer = setInterval(onTimer, 10 / 24);
onTimer();
}
const onTimer = () => {
// if enough time has elapsed, we will add new particles.
count++;
if (count >= wait) {
count = 0;
for (i = 0; i < numToAddEachFrame; i++) {
theta = Math.random() * 2 * Math.PI;
phi = Math.acos(Math.random() * 2 - 1);
x0 = this.sphereRad * Math.sin(phi) * Math.cos(theta);
y0 = this.sphereRad * Math.sin(phi) * Math.sin(theta);
z0 = this.sphereRad * Math.cos(phi);
// We use the addParticle function to add a new particle. The parameters set the position and velocity components.
// Note that the velocity parameters will cause the particle to initially fly outwards away from the sphere center (after
// it becomes unstuck).
const p = addParticle(x0, sphereCenterY + y0, sphereCenterZ + z0, 0.002 * x0, 0.002 * y0, 0.002 * z0);
// we set some 'envelope' parameters which will control the evolving alpha of the particles.
p.attack = 50;
p.hold = 50;
p.decay = 100;
p.initValue = 0;
p.holdValue = particleAlpha;
p.lastValue = 0;
// the particle will be stuck in one place until this time has elapsed:
p.stuckTime = 90 + Math.random() * 20;
p.accelX = 0;
p.accelY = gravity;
p.accelZ = 0;
}
}
// update viewing angle
turnAngle = (turnAngle + turnSpeed) % (2 * Math.PI);
sinAngle = Math.sin(turnAngle);
cosAngle = Math.cos(turnAngle);
// background fill
context.fillStyle = '#000000';
context.fillRect(0, 0, displayWidth, displayHeight);
// update and draw particles
p = particleList.first;
while (p != null) {
// before list is altered record next particle
nextParticle = p.next;
// update age
p.age++;
// if the particle is past its 'stuck' time, it will begin to move.
if (p.age > p.stuckTime) {
p.velX += p.accelX + randAccelX * (Math.random() * 2 - 1);
p.velY += p.accelY + randAccelY * (Math.random() * 2 - 1);
p.velZ += p.accelZ + randAccelZ * (Math.random() * 2 - 1);
p.x += p.velX;
p.y += p.velY;
p.z += p.velZ;
}
/*
We are doing two things here to calculate display coordinates.
The whole display is being rotated around a vertical axis, so we first calculate rotated coordinates for
x and z (but the y coordinate will not change).
Then, we take the new coordinates (rotX, y, rotZ), and project these onto the 2D view plane.
*/
rotX = cosAngle * p.x + sinAngle * (p.z - sphereCenterZ);
rotZ = -sinAngle * p.x + cosAngle * (p.z - sphereCenterZ) + sphereCenterZ;
// m = this.radius_sp * fLen / (fLen - rotZ);
m = this.radius_sp;
p.projX = rotX * m + projCenterX;
p.projY = p.y * m + projCenterY;
p.projZ = rotZ * m + projCenterX;
// update alpha according to envelope parameters.
if (p.age < p.attack + p.hold + p.decay) {
if (p.age < p.attack) {
p.alpha = (p.holdValue - p.initValue) / p.attack * p.age + p.initValue;
} else if (p.age < p.attack + p.hold) {
p.alpha = p.holdValue;
} else if (p.age < p.attack + p.hold + p.decay) {
p.alpha = (p.lastValue - p.holdValue) / p.decay * (p.age - p.attack - p.hold) + p.holdValue;
}
} else {
p.dead = true;
}
// see if the particle is still within the viewable range.
if ((p.projX > displayWidth) || (p.projX < 0) || (p.projY < 0) || (p.projY > displayHeight) || (rotZ > zMax)) {
outsideTest = true;
} else {
outsideTest = false;
}
if (outsideTest || p.dead ||
(p.projX > displayWidth / (2 + (1 - Math.random())) && p.projZ + displayWidth * 0.1 > displayWidth / 2) ||
(p.projX < displayWidth / (2 - (1 - Math.random())) && p.projZ + displayWidth * 0.25 < displayWidth / 2)
) {
recycle(p);
} else {
// depth-dependent darkening
// console.log(turnAngle, rotZ)
depthAlphaFactor = 1;
// depthAlphaFactor = (1 - (1.5 + rotZ / 100));
depthAlphaFactor = (depthAlphaFactor > 1) ? 1 : ((depthAlphaFactor < 0) ? 0 : depthAlphaFactor);
context.fillStyle = rgbString + depthAlphaFactor * p.alpha + ')';
// draw
context.beginPath();
context.arc(p.projX, p.projY, m * particleRad, 0, 2 * Math.PI, false);
context.closePath();
context.fill();
}
p = nextParticle;
}
window.requestAnimationFrame(onTimer);
}
const addParticle = (x0, y0, z0, vx0, vy0, vz0) => {
let newParticle;
// const color;
// check recycle bin for available drop:
if (recycleBin.first != null) {
newParticle = recycleBin.first;
// remove from bin
if (newParticle.next != null) {
recycleBin.first = newParticle.next;
newParticle.next.prev = null;
} else {
recycleBin.first = null;
}
} else {
newParticle = {};
}
// if the recycle bin is empty, create a new particle (a new empty object):
// add to beginning of particle list
if (particleList.first == null) {
particleList.first = newParticle;
newParticle.prev = null;
newParticle.next = null;
} else {
newParticle.next = particleList.first;
particleList.first.prev = newParticle;
particleList.first = newParticle;
newParticle.prev = null;
}
// initialize
newParticle.x = x0;
newParticle.y = y0;
newParticle.z = z0;
newParticle.velX = vx0;
newParticle.velY = vy0;
newParticle.velZ = vz0;
newParticle.age = 0;
newParticle.dead = false;
if (Math.random() < 0.5) {
newParticle.right = true;
} else {
newParticle.right = false;
}
return newParticle;
}
const recycle = (p) => {
// remove from particleList
if (particleList.first === p) {
if (p.next != null) {
p.next.prev = null;
particleList.first = p.next;
} else {
particleList.first = null;
}
} else {
if (p.next == null) {
p.prev.next = null;
} else {
p.prev.next = p.next;
p.next.prev = p.prev;
}
}
// add to recycle bin
if (recycleBin.first == null) {
recycleBin.first = p;
p.prev = null;
p.next = null;
} else {
p.next = recycleBin.first;
recycleBin.first.prev = p;
recycleBin.first = p;
p.prev = null;
}
};
init();
}
}
So I will be happy with any help also REWARD(for full implementation) is possible (ETH, BTC any currency you wish).
var stars = function() {
this.x = Math.floor(Math.random()* 1000) ;
this.y = Math.floor(Math.random()* 900) ;
this.radius = 2 ;
this.starColour = "gold";
}
var starNum = 20;
var starry = new Array(starNum);
for(var s = 0 ; s < 100 ; s++){
starry[s] = new stars()
}
var starDraw = function() {
var starCanvas = document.getElementById("stars");
var starCtx = starCanvas.getContext("2d");
starCtx.clearRect(0, 0, 1000, 900);
for(i = 0; i < 100 ; i++){
var star = starry[i];
starCtx.fillStyle= "white";
starCtx.shadowBlur = 5;
starCtx.shadowColor = "white";
starCtx.beginPath();
// draw it
starCtx.arc(star.x, star.y, star.radius, Math.PI * 2, false);
starCtx.stroke();
starCtx.fill();
}
}
function starLoop(){
starDraw();
requestAnimationFrame(starLoop);
}
requestAnimationFrame(starLoop);
So I am trying to create a twinkling effect for the stars using only javascript and I can't figure out how to do it.
I have searched around and found no real answers up to now so I would appreciate if I could get an answer here. I am very new to coding so please take it easy on me.
A random star field. A little exaggerated, but easy to tone down (or up) if needed.
The important part is to avoid direct random values as most things in nature are not random but tend to fall close to a fixed point. This is call a gaussian distribution. There are several ways to generate such random values.
// gRandom is far more likely to be near 0.5 than 1 or zero
var gRandom = (Math.random()+Math.random()+Math.random()+Math.random()) / 4;
// or
// gRandom is more likely to be near zero than near 1
var gRandom = Math.random() * Math.random();
I use these method to set the sizes of stars (far more small stars than big) and create the colour and movement.
To try and get a more realistic effect I also move the stars by less than a pixel. This has the effect of changing the brightness but not look like movement.
Code has plenty of comments
const ctx = canvas.getContext("2d");
// function calls a callback count times. Saves typing out for loops all the time
const doFor = (count, callback) => {
var i = 0;
while (i < count) {
callback(i++)
}
};
// creates a random integer between min and max. If min only given the between 0 and the value
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
// same as above but as floats.
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
// creates a 2d point at x,y. If only x is a point than set to that point
const point = (x = 0, y) => {
if (x.x && y === undefined) {return { x: x.x,y: x.y} }
return {x,y: y === undefined ? 0 : y }
};
function ease (time, amount = 2) { return Math.pow(time % 1,amount) };
const clamp = (v, min = 1,max = min + (min = 0)) => v < min ? min : v > max ? max : v;
// stuff for stars
const skyColour = [10,30,50];
const density = 1000; // number of star per every density pixels
const colourChangeRate = 16; // Time in frames to change a colour
const stars = [];
const star = { // define a star
draw() {
this.count += 1; // integer counter used to triger color change every 16 frames
if (this.count % colourChangeRate === 0) { // change colour ?
// colour is a gaussian distrabution (NOT random) centered at #888
var c = (Math.random() + Math.random() + Math.random() + Math.random()) * 4;
var str = "#";
str += Math.floor(c * this.red).toString(16); // change color
str += Math.floor(c * this.green).toString(16); // change color
str += Math.floor(c * this.blue).toString(16); // change color
this.col = str;
}
ctx.fillStyle = this.col;
// move star around a pixel. Again its not random
// but a gaussian distrabution. The movement is sub pixel and will only
// make the stars brightness vary not look like its moving
var ox = (Math.random() + Math.random() + Math.random() + Math.random()) / 4;
var oy = (Math.random() + Math.random() + Math.random() + Math.random()) / 4;
ctx.fillRect(this.pos.x + ox, this.pos.y + oy, this.size, this.size);
}
}
// create a random star
// the size is caculated to produce many more smaller stars than big
function createStar(pos) {
stars.push(Object.assign({}, star, {
pos,
col: "#ccc",
count: randI(colourChangeRate),
size: rand(1) * rand(1) * 2 + 0.5,
red: 1-(rand(1) * rand(1) *rand(1)), // reduces colour channels
green: 1-(rand(1) * rand(1) *rand(1)), // but only by a very small amount
blue: 1-(rand(1) * rand(1) *rand(1)), // most of the time but occasional
// star will have a distinct colour
}));
}
var starCount;
var skyGrad;
// render the stars
function mainLoop(time) {
// resize canva if page size changes
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
canvas.width = innerWidth;
canvas.height = innerHeight;
// create a new set of stars
stars.length = 0;
// density is number of pixels one the canvas that has one star
starCount = Math.floor((canvas.width * canvas.height) / density);
// create the random stars;
doFor(starCount, () => createStar(point(randI(canvas.width), randI(canvas.height))));
skyGrad = ctx.createLinearGradient(0,0,0,canvas.height);
skyGrad.addColorStop(0,"black");
doFor(100,(i)=>{
var pos = clamp(i/100,0,1);
var col = ease(pos);
skyGrad.addColorStop(
pos,
"rgb(" +
Math.floor(skyColour[0] * col) + "," +
Math.floor(skyColour[1] * col) + "," +
Math.floor(skyColour[2] * col) + ")"
);
});
// floating point error can cause problems if we dont set the top
// at 1
skyGrad.addColorStop(1,"rgb("+skyColour[0]+","+skyColour[1]+","+skyColour[2]+")");
}
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
doFor(starCount, (i) => stars[i].draw());
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>