2

This is a duplicate of another question (8778874). However, the accepted answer is not working for me, please help me find my mistake.

I have a javascript class with multiple prototype methods but for some reason I am unable to call one prototype method from another.

var Particles = function() {};

Particles.prototype.physicsStep = function() {
    this.computeForces(); // error in Chrome
    this.integrationStep(); // error in Chrome
}

Particles.prototype.computeForces = function() {
    // do some stuff
}

Particles.prototype.integrationStep = function() {
   // do some stuff
}

Chrome always throws the error "Uncaught TypeError: this.computeForces is not a function". I must be missing the point entirely. Any help would be much appreciated.

particles.js

// define particles class
var Particles = function() {
    // program constants
    this.dt = 0.01;
    this.NUM_CIRCLES = 50;
    this.DRAG_COEFF = 1;
    this.SIMILARITY_COEFF = 0.05;
    this.INTER_CIRCLE_ALPHA = 1000;
    this.INTER_CIRCLE_GAMMA = 1;
    this.BOUNDARY_ALPHA = 100;
    this.BOUNDARY_GAMMA = 2;
    this.MAX_FORCE = 10;
    this.MASS_SCALE = 5;
    this.STATE_LPF_POL = 0.01;
    this.MODEL_UPDATE_INTERVAL_SECONDS = 1;
    // simulation state
    this.t = null;
    this.positions = null;
    this.old_positions = null;
    this.radii = null;
    this.velocities = null;
    this.forces = null;
    this.boundaries = {x: null, y: null};
};



//////////////////////////////////////////////////////////////////////////////////////////////
// Public Interface
//////////////////////////////////////////////////////////////////////////////////////////////

/*
physicsStep()
-------------
step forward the particle simulation.
*/
Particles.prototype.physicsStep = function() {
    this.computeForces();
    this.integrationStep();
}

/*
initState()
-----------
initialize physics state to all zeros.
*/
Particles.prototype.initState = function() {
    this.t = 0;
    this.boundaries = {x: [0, 1], y: [0, 1]};
    this.positions = [];
    this.old_positions = [];
    this.radii = [];
    this.velocities = [];
    this.forces = [];
    for(i = 0; i < this.NUM_CIRCLES; i++) {
        this.positions.push(new THREE.Vector2(0,0));
        this.old_positions.push(new THREE.Vector2(0,0));
        this.radii.push(0);
        this.velocities.push(new THREE.Vector2(0,0));
        this.forces.push(new THREE.Vector2(0,0));
    }
}

/*
initModel()
-----------
initialize model parameters to zeros.
*/
Particles.prototype.initModel = function() {
    // initialize the model
    this.similarities = [];
    for(i = 0; i < this.NUM_CIRCLES; i++) {
        this.similarities.push([]);
        for(j = 0; j < this.NUM_CIRCLES; j++) {
            this.similarities[i].push(0);
        }
    }
}

/*
updateModel()
-------------
get new parameters for the model.
currently implemented with placeholder random update.
*/
Particles.prototype.updateModel = function() {
    for(i = 0; i < this.NUM_CIRCLES; i++) {
        for(j = i+1; j < this.NUM_CIRCLES; j++) {
            // place holder for now
            this.similarities[i][j] = (1 - 2*Math.random());
        }
    }
}

/*
setBoundaries(xlims, ylims)
---------------------------
sets the x and y boundaries for the particle simulation.
xlims is [left, right].
yllims is [bottom, top].
*/
Particles.prototype.setBoundaries = function(xlims, ylims) {
    if(xlims != null) this.boundaries.x = xlims;
    if(ylims != null) this.boundaries.y = ylims;
}

/*
randomizeState()
----------------
randomizes the state of the simulation.
*/
Particles.prototype.randomizeState = function() {
    this.t = 0;
    for(i = 0; i < this.NUM_CIRCLES; i++) {
        var xrange = this.boundaries.x[1] - this.boundaries.x[0];
        var yrange = this.boundaries.y[1] - this.boundaries.y[0];
        this.positions[i].x = this.boundaries.x[0] + xrange * Math.random();
        this.positions[i].y = this.boundaries.y[0] + yrange * Math.random();
        this.old_positions[i].x = this.positions[i].x;
        this.old_positions[i].y = this.positions[i].y;
        this.velocities[i].x = 0;
        this.velocities[i].y = 0;
        this.radii[i] = 0.1 * Math.min(xrange, yrange) * Math.random();
    }
}

//////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
//////////////////////////////////////////////////////////////////////////////////////////////

/*
computeForces()
---------------
gets the forces for the next time step.
*/
Particles.prototype.computeForces = function() {
    // per-particle forces
    var alpha = this.BOUNDARY_ALPHA;
    var gamma = this.BOUNDARY_GAMMA;
    for(i = 0; i < this.NUM_CIRCLES; i++) {
        // start at 0
        this.forces[i].x = 0;
        this.forces[i].y = 0;
        // force exerted by boundaries
        this.forces[i].add(FORCES.boundaryForce(this.positions[i], this.radii[i], this.boundaries.x, this.boundaries.y, alpha, gamma));
        // drag force
        this.forces[i].add(FORCES.dragForce(this.velocities[i], this.DRAG_COEFF));
    }
    // inter-particle forces
    alpha = this.INTER_CIRCLE_ALPHA;
    gamma = this.INTER_CIRCLE_GAMMA;
    for(i = 0; i < this.NUM_CIRCLES; i++) {
        for(j = i+1; j < this.NUM_CIRCLES; j++) {
            // proximity repulsion force
            var repulsion = FORCES.forceBetweenCircles(this.positions[i], this.radii[i], this.positions[j], this.radii[j], alpha, gamma);
            // similarity attraction/repulsion force
            var similarity = this.similarities[i][j] * this.SIMILARITY_COEFF;
            repulsion.add(FORCES.similarityForce(this.positions[i], this.radii[i], this.positions[j], this.radii[j], similarity));
            // add the forces to both particles
            this.forces[i].add(repulsion);
            this.forces[j].add(repulsion.negate());
        }
    }
    // make sure no forces exceed maximum
    for(i=0; i < this.NUM_CIRCLES; i++) {
        if(this.forces[i].length() > this.MAX_FORCE) {
            this.forces[i].normalize().multiplyScalar(this.MAX_FORCE);
        }
    }
}

/*
integrationStep()
-----------------
update based position and velocity based on forces
*/
Particles.prototype.integrationStep = function() {
    for(i = 0; i < this.NUM_CIRCLES; i++) {
        var mass = this.radii[i] * this.MASS_SCALE;
        var a = new THREE.Vector2(this.forces[i].x / mass, this.forces[i].y / mass);
        var pos = new THREE.Vector2(this.positions[i].x, this.positions[i].y);
        // verlet integration
        pos.multiplyScalar(2).sub(this.old_positions[i]).add(a.multiplyScalar(Math.pow(this.dt, 2)));
        // lowpass filter
        pos.addVectors(this.positions[i], pos.sub(this.positions[i]).multiplyScalar(1 - this.STATE_LPF_POLE));
        // update state
        this.velocities[i].subVectors(pos, this.old_positions[i]).divideScalar(2 * this.dt);
        this.old_positions[i] = this.positions[i];
        this.positions[i] = pos;
    }
    this.t += this.dt;
}


//////////////////////////////////////////////////////////////////////////////////////////////

render.js

// opengl variables
var camera, scene, renderer, width, height, res;
// shader variables
var uniforms;
// particles
var particles = new Particles();

var CONSTANTS = {
    PI: Math.PI,
    dt: 0.01,
    NUM_CIRCLES: 50,
    BORDER_PERCENT: 0.1
}

// initialize
init();
// kick off physics
window.setInterval(particles.physicsStep, 1000 * particles.dt);
// kick off model parameter update
//window.setInterval(updateModel, 1000 * CONSTANTS.MODEL_UPDATE_INTERVAL_SECONDS);
animate();

/*
init()
------
*/
function init() {
    particles.initState();
    particles.initModel();
    initCameraAndScene();
    initRenderer();
    particles.randomizeState();
}

/*
animate()
---------
*/
function animate() {
    requestAnimationFrame(animate);
    render();
}

/*
render()
--------
*/
function render() {
    updateUniforms();
    renderer.render(scene, camera);
}


/*
initCameraAndScene()
-----------
setup scene and fullscreen quad.
*/
function initCameraAndScene() {
    // initialize camer
    camera = new THREE.Camera();
    camera.position.z = 1;
    // make a scene...
    scene = new THREE.Scene();
    // fullscreen quad
    var geometry = new THREE.PlaneBufferGeometry(2,2);
    var material = getShaderMaterial();
    var mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
}

/*
initRenderer()
--------------
initialize the opengl renderer element.
*/
function initRenderer() {
    renderer = new THREE.WebGLRenderer();
    renderer.setPixelRatio(window.devicePixelRatio);
    document.body.appendChild(renderer.domElement);
    onWindowResize();
    window.addEventListener('resize', onWindowResize, false);
    window.addEventListener('click', onMouseClick, false);
}

/*
onWindowResize(event)
---------------------
windows resize event handler. updates shader uniforms
as necessary.
*/
function onWindowResize(event) {
    // hack intended to get rid of scrollable area
    width = Math.max(0, window.innerWidth - 20);
    height = Math.max(0, window.innerHeight - 20);
    // end of hack
    renderer.setSize(width, height);
    res = width / height;
    particles.setBoundaries([0, res], null);
}

/*
onMouseClick(event)
-------------------
mouseclick event handler. randomize state.
*/
function onMouseClick(event) {
    particles.updateModel();
}

/*
getShaderMaterial()
---------------
returns a THREE.ShaderMaterial compiled from the
shader strings found in SHADERS.vertexShader and
SHADERS.fragmentShader.
*/
function getShaderMaterial() {
    // this string holds #defined constants
    var constants = "";
    for(var key in CONSTANTS) {
        constants = constants.concat("#define ");
        constants = constants.concat(key + " " + CONSTANTS[key]);
        constants = constants.concat("\n");
    }
    // shader variables
    uniforms = {
        screenwidth: {type: "f", value: window.innerWidth},
        screenheight: {type: "f", value: window.innerHeight},
        t: {type: "f", value: 0},
        centers: {type: "v2v", value: []},
        radii: {type: "fv1", value: []}
    };
    // make the material
    var material = new THREE.ShaderMaterial({
        uniforms: uniforms,
        vertexShader: constants + SHADERS.vertexShader,
        fragmentShader: constants + SHADERS.fragmentShader
    });
    return material;
}

/*
updateUniforms()
----------------
sets the shader uniforms based on the current simulation state.
*/
function updateUniforms() {
    uniforms.t.value = particles.t;
    uniforms.screenwidth.value = width;
    uniforms.screenheight.value = height;
    uniforms.centers.value = particles.positions;
    uniforms.radii.value = particles.radii;
}

there are some other files as well, but i think the error must be in one of these two. thank you for your help.

Community
  • 1
  • 1
applecollider
  • 23
  • 1
  • 4
  • 2
    How do you create an instance, and how do you call the methods? *this* is set by the call. – RobG Sep 14 '15 at 01:54
  • in another file i use 'particles = new Particles();' and then 'particles.physicsStep()' – applecollider Sep 14 '15 at 01:59
  • 2
    I don't get an error [doing this](http://jsfiddle.net/6t5r6o5u/). EDIT: Post your actual code (relevant snippets), including in what files `Particles` is declared and called. – Jared Farrish Sep 14 '15 at 01:59
  • @JaredFarrish even better would be a Plunker example where OP could show how the separate files are included (with Plunker supporting multiple source files) – Phil Sep 14 '15 at 02:03
  • 1
    It's [`window.setInterval(particles.physicsStep, 1000 * particles.dt);`](http://jsfiddle.net/6t5r6o5u/3/). where you're passing the function method reference, not the object itself. Change it to [`window.setInterval(function(){particles.physicsStep()}, 1000 * particles.dt);`](http://jsfiddle.net/6t5r6o5u/2/) and it will work (or at least you won't get that error anymore, whether the whole thing works is a different question). – Jared Farrish Sep 14 '15 at 02:11
  • thank you, that did the trick. can you explain to me how the `this` parameter is passed around differently in the correct answer? – applecollider Sep 14 '15 at 02:14
  • Which correct answer are you referring? The one you linked to in the question content? – Jared Farrish Sep 14 '15 at 02:18
  • I suspect the [`Function.prototype.bind()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind) documentation will explain to you what you're missing. Basically, you passed a function reference to `window.setInterval()` that didn't have the object context you expected (it saw `window` instead of `particles`). `window` didn't have the method you specified, hence the error. – Jared Farrish Sep 14 '15 at 02:21

2 Answers2

4

With your current code, you can just do this:

var p = new Particles();
p.physicsStep();

And, then inside of physicsStep(), it will appropriately execute this.computeForces() and this.integrationStep() and this will be a pointer to the p object that was created in the first line of code above.

The value of this is set by how a method/function is called as described here so if you are having problems with the value of this, then the issue is likely not in the method itself, but in how it is being called.

If you want help with that part of your code, then can add that code to your question and let us take a look.

Working example: http://jsfiddle.net/jfriend00/yuxppyyf/


Yes, you are correct one problem does have to do with how you are using setInterval.

You can change this:

window.setInterval(particles.physicsStep, 1000 * particles.dt);

to this:

window.setInterval(particles.physicsStep.bind(particles), 1000 * particles.dt);

When you pass particles.physicsStep as a function reference to another function, the particles part of that gets lost. All that is passed is a reference to the physicsStep method so when setInterval then calls it, it is called as an ordinary function, not as a method of your object.

This is a common mistake in Javascript and there are a couple ways to deal with that issue. I showed using .bind() above. You can also make your own little stub function (which is essentially what .bind() does for you) like this:

window.setInterval(function() {
    particles.physicsStep();
}, 1000 * particles.dt);

This ensures that physicStep() is called on the right object.

FYI, similar problem and answer here: How to get callback to work with "this" in class scope

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • thank you. after reading that link and a few others, I assume that the issue is something to do with the way the event handler for `window.setInterval` is setting up `this`, but I don't understand the mechanisms well enough to see a solution. I really appreciate everybody's help. – applecollider Sep 14 '15 at 02:12
  • @applecollider - yes, you are correct about `setInterval()`. See what I've added to my answer now that you've disclosed that part of your code. – jfriend00 Sep 14 '15 at 02:20
1

Change this line:

window.setInterval(particles.physicsStep, 1000 * particles.dt);

http://jsfiddle.net/6t5r6o5u/3/

Where you're passing the function method reference, not the object itself, to:

window.setInterval(function(){particles.physicsStep()}, 1000 * particles.dt);

http://jsfiddle.net/6t5r6o5u/2/

Or this (which binds the particle object to the context when passed as a reference):

window.setInterval(particles.physicsStep.bind(p), 1000);

http://jsfiddle.net/6t5r6o5u/4/

And it will work (or at least you won't get that error anymore, whether the whole thing works is a different question).

Jared Farrish
  • 48,585
  • 17
  • 95
  • 104