// Sim.js (C) 2017; Walter Bislin // // Licence: Feel free to use and change this file in any way you want for your projects. // Do not sell this file as it is to make profit of not your own work please. // // This class controls real time animations and provides some statistics. // For documentation see http://walter.bislins.ch/doc/sim function Sim( params ) { this.TimeStep = xDefNum( params.TimeStep, 0.001 ); // s this.TimeSpeed = xDefNum( params.TimeSpeed, 1 ); this.TargetFps = xDefNum( params.TargetFps, 25 ); this.SimObj = xDefObj( params.SimObj, null ); this.DeltaTime = 0; // [s] computed from TimeStep and current delta frame time, limited by TargetFps this.TimeStepFuncs = new xCallbackChain(); this.FrameFuncs = new xCallbackChain(); this.ResetFuncs = new xCallbackChain(); this.RunFuncs = new xCallbackChain(); this.StopFuncs = new xCallbackChain(); this.SimulTime = 0; // s this.RealTime = 0; // s this.Running = false; // statistics this.EnableStatistics = xDefBool( params.EnableStatistics, false ); this.NAvgSamples = xDefNum( params.NAvgSamples, 25 ); this.SampleTime = xDefNum( params.SampleTime, 500 ); // ms this.CpuLoadSim = 0; // 0..1 this.CpuLoadSimMax = 0; // 0..1 this.CpuLoadSimAvg = 0; // 0..1 this.CpuLoadDraw = 0; // 0..1 this.CpuLoadDrawMax = 0; // 0..1 this.CpuLoadDrawAvg = 0; // 0..1 this.CpuLoadFrame = 0; // 0..1 this.CpuLoadFrameMax = 0; // 0..1 this.CpuLoadFrameAvg = 0; // 0..1 this.Fps = 0; // average FPS over NSvgSamples this.FpsMin = 0; this.FpsMax = 0; this.NFrames = 0; // private properties this.StartTime = 0; // ms this.LastFrameTime = 0; // ms this.LastSampleTime = 0; // ms this.IsInAnimationFrame = false; this.AnimationFrame = null; this.CurrCpuLoadSimMax = 0; this.CurrCpuLoadFrameMax = 0; this.CurrFpsMax = 0; this.CurrFpsMin = 1e9; if (params.TimeStepFuncs) this.AddTimeStepFunc( params.TimeStepFuncs ); if (params.FrameFuncs) this.AddFrameFunc( params.FrameFuncs ); if (params.ResetFuncs) this.AddResetFunc( params.ResetFuncs ); if (params.RunFuncs) this.AddRunFunc( params.RunFuncs ); if (params.StopFuncs) this.AddStopFunc( params.StopFuncs ); } Sim.prototype.AddTimeStepFunc = function( funcs ) { if (xArray(funcs)) { xArrForEach( funcs, function CB_AddTimeStepFunc(func){ this.AddTimeStepFunc(func); }, this ); } else { this.TimeStepFuncs.Add( funcs ); } } Sim.prototype.AddFrameFunc = function( funcs ) { if (xArray(funcs)) { xArrForEach( funcs, function CB_AddFrameFunc(func){ this.AddFrameFunc(func); }, this ); } else { this.FrameFuncs.Add( funcs ); } } Sim.prototype.AddResetFunc = function( funcs ) { if (xArray(funcs)) { xArrForEach( funcs, function CB_AddResetFunc(func){ this.AddResetFunc(func); }, this ); } else { this.ResetFuncs.Add( funcs ); } } Sim.prototype.AddRunFunc = function( funcs ) { if (xArray(funcs)) { xArrForEach( funcs, function CB_AddRunFunc(func){ this.AddRunFunc(func); }, this ); } else { this.FrameRun.Add( funcs ); } } Sim.prototype.AddStopFunc = function( funcs ) { if (xArray(funcs)) { xArrForEach( funcs, function CB_AddStopFunc(func){ this.AddStopFunc(func); }, this ); } else { this.StopFuncs.Add( funcs ); } } Sim.prototype.Reset = function() { if (this.Running) { this.Stop(); } this.RealTime = 0; this.SimulTime = 0; this.StartTime = 0; this.LastFrameTime = 0; this.LastSampleTime = 0; this.NFrames = 0; if (this.EnableStatistics) { this.CpuLoadSim = 0; this.CpuLoadSimMax = 0; this.CpuLoadSimAvg = 0; this.CpuLoadDraw = 0; this.CpuLoadDrawMax = 0; this.CpuLoadDrawAvg = 0; this.CpuLoadFrame = 0; this.CpuLoadFrameMax = 0; this.CpuLoadFrameAvg = 0; this.Fps = 0; this.FpsMax = 0; this.FpsMin = 0; this.CurrCpuLoadSimMax = 0; this.CurrCpuLoadFrameMax = 0; this.CurrFpsMax = 0; this.CurrFpsMin = 1e9; } this.ResetFuncs.Call( this ); } Sim.prototype.Stop = function( reset ) { this.Running = false; if (this.AnimationFrame) { cancelAnimationFrame( this.AnimationFrame ); this.AnimationFrame = null; } this.StopFuncs.Call( this ); if (reset) { this.Reset(); } } Sim.prototype.Run = function( reset ) { if (reset) { if (this.Running) { this.Stop(); } this.Reset(); } this.RunFuncs.Call( this ); if (this.Running) return; this.StartTime = xTimeMS(); this.LastFrameTime = this.StartTime; this.LastSampleTime = this.StartTime; this.Running = true; this.NFrames = 0; var me = this; this.AnimationFrame = requestAnimationFrame( function CB_OnAnimationFrame() { me.OnAnimationFrame(); } ); } Sim.prototype.Pause = function( reset ) { if (this.Running) { this.Stop(); } else { this.Run( reset ); } } Sim.prototype.InitStates = function( states, dim, size ) { // states: State = { OldAccel, Accel, Speed, Pos } or array of State // OldAccel, Accel, Speed, Pos: Number or array of Number // dim: integer; dimension of State properties; 0 -> meaning they are Numbers, else they are arrays // size: integer; size of states array; 0 -> meaning states is a State object, not an array (vector) // // initializes states.OldAccel from states.Accel // call this function before using CompNewStates() the first time if (size > 0) { for (var i = 0; i < size; i++) { this.InitStates( states, dim, 0 ); } return; } // assert size = 0 and states is a State object if (dim > 0) { for (var i = 0; i < dim; i++) { states.OldAccel[i] = states.Accel[i]; } } else { states.OldAccel = states.Accel; } } Sim.prototype.CompNewStates = function( states, dim, size ) { // states: State = { OldAccel, Accel, Speed, Pos } or array of State // OldAccel, Accel, Speed, Pos: Number or array of Number // dim: integer; dimension of State properties; 0 -> meaning they are Numbers, else they are arrays // size: integer; size of states array; 0 -> meaning states is a State object, not an array (vector) // // you have to provide current calculated acceleration (eg from sum of forces) in states.Accel // from that states.Speed and states.Pos is computed by numerical integration // OldAccel stores current Accel for next step of integration for more accuracy // numerical integration uses this.DeltaTime as the time step // requires that states.OldAccel is valid, by calling InitStates() if (size > 0) { for (var i = 0; i < size; i++) { this.CompNewStates( states[i], dim, 0 ); } return; } // assert size = 0 and states is a State object if (dim > 0) { for (var i = 0; i < dim; i++) { var deltaAccel = states.Accel[i] - states.OldAccel[i]; states.OldAccel[i] = states.Accel[i]; var deltaSpeed = ((0.5 * deltaAccel) + states.Accel[i]) * this.DeltaTime; states.Speed[i] += deltaSpeed; var deltaPos = ((0.5 * deltaSpeed) + states.Speed[i]) * this.DeltaTime; states.Pos[i] += deltaPos; } } else { var deltaAccel = states.Accel - states.OldAccel; states.OldAccel = states.Accel; var deltaSpeed = ((0.5 * deltaAccel) + states.Accel) * this.DeltaTime; states.Speed += deltaSpeed; var deltaPos = ((0.5 * deltaSpeed) + states.Speed) * this.DeltaTime; states.Pos += deltaPos; } } Sim.prototype.OnAnimationFrame = function() { if (!this.Running || this.IsInAnimationFrame) return; if (this.AnimationFrame) { cancelAnimationFrame( this.AnimationFrame ); this.AnimationFrame = null; } this.NFrames++; this.IsInAnimationFrame = true; var frameStartTime_ms = xTimeMS(); var frameRealTime_s = (frameStartTime_ms - this.LastFrameTime) / 1000; var estimatedFps = 1 / frameRealTime_s; var maxFrameRealTime_s = 1 / this.TargetFps; if (maxFrameRealTime_s > frameRealTime_s) maxFrameRealTime_s = frameRealTime_s; // assert( maxFrameRealTime_s(TargetFps) <= frameRealTime_s(Fps) ) var maxFrameSimulTime_s = maxFrameRealTime_s * this.TimeSpeed; var nTimeSteps = Math.floor( maxFrameSimulTime_s / this.TimeStep ) + 1; this.DeltaTime = maxFrameSimulTime_s / nTimeSteps; var maxFrameCpuTime_ms = maxFrameRealTime_s * 1000; var frameSimulTime_s = 0; var frameCpuTime_ms = 0; maxFrameSimulTime_s -= this.DeltaTime / 2; while ( frameSimulTime_s < maxFrameSimulTime_s && frameCpuTime_ms <= maxFrameCpuTime_ms ) { frameSimulTime_s += this.DeltaTime; this.SimulTime += this.DeltaTime; this.TimeStepFuncs.Call( this ); // -----------> time step callback frameCpuTime_ms = xTimeMS() - frameStartTime_ms; } // simulation statistics if (this.EnableStatistics && this.NFrames > 1) { // current simulation cpu load if (frameCpuTime_ms >= maxFrameCpuTime_ms) { this.CpuLoadSim = 1; } else { this.CpuLoadSim = frameCpuTime_ms / maxFrameCpuTime_ms; } // average simulation cpu load and Fps var n = this.NAvgSamples; if (n <= 0) n = 2; this.CpuLoadSimAvg += (this.CpuLoadSim - this.CpuLoadSimAvg) / n; this.Fps += (estimatedFps - this.Fps) / n; // simulation max cpu load and min/max Fps var currSampleTime_ms = frameStartTime_ms - this.LastSampleTime; if (currSampleTime_ms > this.SampleTime) { this.CpuLoadSimMax = this.CurrCpuLoadSimMax; this.CurrCpuLoadSimMax = 0; this.FpsMax = this.CurrFpsMax; this.CurrFpsMax = 0; this.FpsMin = this.CurrFpsMin; this.CurrFpsMin = 1e9; } if (this.CpuLoadSim > this.CurrCpuLoadSimMax) this.CurrCpuLoadSimMax = this.CpuLoadSim; if (this.Fps > this.CurrFpsMax) this.CurrFpsMax = this.Fps; if (this.Fps < this.CurrFpsMin) this.CurrFpsMin = this.Fps; } this.LastFrameTime = frameStartTime_ms; this.RealTime = (frameStartTime_ms - this.StartTime) / 1000; this.FrameFuncs.Call( this ); // -----------> frame callback this.IsInAnimationFrame = false; // frame statistics if (this.EnableStatistics && this.NFrames > 1) { // current frame cpu load frameCpuTime_ms = xTimeMS() - frameStartTime_ms; if (frameCpuTime_ms >= maxFrameCpuTime_ms) { this.CpuLoadFrame = 1; } else { this.CpuLoadFrame = frameCpuTime_ms / maxFrameCpuTime_ms; } // average frame cpu laod var n = this.NAvgSamples; if (n <= 0) n = 2; this.CpuLoadFrameAvg += this.CpuLoadFrame / n - this.CpuLoadFrameAvg / n; // frame max cpu load if (frameStartTime_ms - this.LastSampleTime > this.SampleTime) { this.LastSampleTime = frameStartTime_ms; this.CpuLoadFrameMax = this.CurrCpuLoadFrameMax; this.CurrCpuLoadFrameMax = 0; } if (this.CpuLoadFrame > this.CurrCpuLoadFrameMax) this.CurrCpuLoadFrameMax = this.CpuLoadFrame; // draw cpu load as diff of frame cpu load and sim cpu load this.CpuLoadDraw = this.CpuLoadFrame - this.CpuLoadSim; if (this.CpuLoadDraw < 0) this.CpuLoadDraw = 0; this.CpuLoadDrawMax = this.CpuLoadFrameMax - this.CpuLoadSimMax; if (this.CpuLoadDrawMax < 0) this.CpuLoadDrawMax = 0; this.CpuLoadDrawAvg = this.CpuLoadFrameAvg - this.CpuLoadSimAvg; if (this.CpuLoadDrawAvg < 0) this.CpuLoadDrawAvg = 0; } var me = this; this.AnimationFrame = requestAnimationFrame( function CB_OnAnimationFrame() { me.OnAnimationFrame(); } ); }