Search code examples
javascriptmethodsprototype

call javascript prototype method from another


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.


Solution

  • 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