Simulate AFM Cantilever Deflection in Matter.js
This article explains how to simulate Atomic Force Microscope (AFM) cantilever tip deflection across atomic surfaces using the Matter.js 2D physics engine. You will learn how to model a flexible cantilever using spring constraints, create a periodic atomic substrate, apply non-linear interatomic forces (such as the Lennard-Jones potential) via custom engine update loops, and track the vertical deflection of the probe tip in real time during a lateral surface scan.
Conceptual Overview
An Atomic Force Microscope operates by scanning an extremely sharp tip attached to a flexible cantilever across a specimen. As the tip approaches surface atoms, interatomic forces cause the cantilever to bend. In Matter.js, this setup requires three core components:
- The Cantilever Assembly: A translating base attached to a flexible beam or elastic constraint with a defined spring constant (\(k\)).
- The Atomic Substrate: A periodic grid of static circular bodies representing atoms on a surface.
- The Interaction Potential: A custom force calculation (typically a Lennard-Jones 12-6 potential) applied between the tip and the surface atoms on every engine tick.
Step 1: Initialize the Engine and World
Set up a standard Matter.js environment, disabling global gravity to isolate the mechanical and interatomic forces.
const { Engine, Render, Runner, Bodies, Composite, Constraint, Vector, Body, Events } = Matter;
const engine = Engine.create({
gravity: { x: 0, y: 0 } // Microscopic simulation ignores macro gravity
});
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 400,
wireframes: false
}
});
Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);Step 2: Build the Atomic Substrate
Create a row of static circular bodies to act as individual atoms arranged in a 1D lattice.
const surfaceAtoms = [];
const atomRadius = 15;
const spacing = 35;
const surfaceY = 320;
for (let x = 100; x < 700; x += spacing) {
const atom = Bodies.circle(x, surfaceY, atomRadius, {
isStatic: true,
render: { fillStyle: '#4a90e2' }
});
surfaceAtoms.push(atom);
}
Composite.add(engine.world, surfaceAtoms);Step 3: Model the Cantilever and Probe Tip
Model the cantilever using a rigid base that moves horizontally, connected to a dynamic probe tip through an elastic constraint representing the cantilever's spring constant (\(k\)).
// Scanner stage that drives horizontal movement
const stage = Bodies.rectangle(100, 150, 40, 20, {
isStatic: true,
render: { fillStyle: '#333' }
});
// AFM Tip
const tip = Bodies.circle(100, 220, 8, {
density: 0.001,
frictionAir: 0.05,
render: { fillStyle: '#e74c3c' }
});
// Elastic Cantilever (Spring)
const cantileverSpring = Constraint.create({
bodyA: stage,
pointA: { x: 0, y: 0 },
bodyB: tip,
pointB: { x: 0, y: 0 },
stiffness: 0.08, // Models the spring constant (k)
damping: 0.02,
render: {
strokeStyle: '#7f8c8d',
lineWidth: 3
}
});
Composite.add(engine.world, [stage, tip, cantileverSpring]);Step 4: Implement the Lennard-Jones Potential Force
To simulate atomic interaction, calculate the Lennard-Jones force between the tip and nearby substrate atoms before each physics step:
\[F(r) = 24\epsilon \left[ 2\left(\frac{\sigma}{r}\right)^{13} - \left(\frac{\sigma}{r}\right)^7 \right] \frac{\mathbf{r}}{r}\]
Where:
- \(\epsilon\) is the potential well depth (interaction strength).
- \(\sigma\) is the distance at which the inter-particle potential is zero.
- \(r\) is the Euclidean distance between the tip and a surface atom.
const epsilon = 1.2;
const sigma = 45;
const cutoffDistance = 90; // Ignore atoms outside this radius for performance
Events.on(engine, 'beforeUpdate', () => {
// 1. Move the stage horizontally to perform the raster scan
Body.setPosition(stage, { x: stage.position.x + 0.5, y: stage.position.y });
// 2. Compute interaction forces between the tip and nearby atoms
for (let i = 0; i < surfaceAtoms.length; i++) {
const atom = surfaceAtoms[i];
const delta = Vector.sub(tip.position, atom.position);
const distance = Vector.magnitude(delta);
if (distance > 0 && distance < cutoffDistance) {
const normal = Vector.normalise(delta);
const termRepulsive = 2 * Math.pow(sigma / distance, 13);
const termAttractive = Math.pow(sigma / distance, 7);
const forceMagnitude = (24 * epsilon / distance) * (termRepulsive - termAttractive);
// Apply calculated force to the cantilever tip
Body.applyForce(tip, tip.position, {
x: normal.x * forceMagnitude,
y: normal.y * forceMagnitude
});
}
}
});Step 5: Measure and Log Tip Deflection
Cantilever deflection is defined as the deviation of the tip from its rest position relative to the stage. Calculate and output the deflection on each render frame:
Events.on(render, 'afterRender', () => {
const equilibriumY = stage.position.y + cantileverSpring.length;
const deflection = tip.position.y - equilibriumY;
// Optional console logging or canvas plotting
const context = render.context;
context.fillStyle = '#000';
context.font = '14px monospace';
context.fillText(`Deflection: ${deflection.toFixed(3)} px`, 20, 30);
context.fillText(`Scan Position X: ${stage.position.x.toFixed(1)} px`, 20, 50);
});By tuning the stiffness of the constraint and the
epsilon and sigma parameters of the potential,
you can accurately reproduce both contact-mode repulsion and non-contact
attractive regime dynamics in your browser.