Edit: created a new Codepen that shows the terminator as a gradient, which eliminates the hard line.

Rigorous:
For those not wanting to settle for an approximation, rendering a photorealistic, properly shaded (with mountains and craters having shadows) image is pretty easy with existing tools like Blender, and NASA's CGI Moon Kit. Since that's a solved problem, and this question is specifically looking for an approximation, I'll focus on that. But an example using ThreeJS is here, and a tutorial using Blender is here.
Approximate:
An approximate method is to imagine a plane intersecting the center of the Moon, perpendicular to the direction of the Sun. As the phase changes, the plane is rotated about the Y axis. To keep things simple, we can ignore any perspective distortion (e.g. ignore the Z coordinate), the Y coordinate won't change, leaving only the X coordinate to be computed, and the rotation matrix simplifies to:
$$
x= radius * \cos(phase)
$$
Where $phase$ is the phase from 0° (new moon) to 360°, and $radius$ is the radius of the full sphere.
The JavaScript code below expects HTML with a canvas with an ID of "canvas", and a moon image with ID of "moon". Here is a demo in CodePen. It generates the polygon necessary to fill in the dark side of a full moon image, and renders the moon as it would be seen from the Northern hemisphere.
This generates a pretty harsh edge. Since the Sun has an apparent size of about 0.5°, it will produce a gradient of that angular size on the Moon. So it would produce a better result to compute two arcs, one +0.25° of the current phase, and one -0.25° and fill a linear gradient in between.
Here is an example produced by the code below:

const rad=Math.PI/180;
const canvas = document.getElementById("canvas");
const w=canvas.width;
const h=canvas.height;
const ctx = canvas.getContext("2d");
const r=w/2*.91;
function drawPhase(phase){
//A rotation matrix, ignoring the Z result, and with Z=0 initially simplifies to cos(phase*360)
const f=Math.cos(phase*rad);
ctx.lineWidth=1;
ctx.beginPath();
let x=f*r*Math.cos(0)+w/2;
let y=r*Math.sin(0)+h/2;
ctx.moveTo(x,y);
if(phase<=180){
for(let i=0;i<=360;i++){
const cosi=Math.cos(i*rad);
if(cosi>0){
x=f*r*Math.cos(i*rad)+w/2;
} else {
x=r*Math.cos(i*rad)+w/2;
}
y=r*Math.sin(i*rad)+h/2;
ctx.lineTo(x,y+1);
}
} else {
for(let i=0;i<=360;i++){
const cosi=Math.cos(i*rad);
if(cosi<0){
x=f*r*Math.cos(i*rad)+w/2;
} else {
x=r*Math.cos(i*rad)+w/2;
}
y=r*Math.sin(i*rad)+h/2;
ctx.lineTo(x,y+1);
}
}
ctx.closePath();
ctx.fill();
}
function display(){
ctx.fillStyle="#000000cc";
ctx.drawImage(document.getElementById("moon"), 0, 0);
phase=(phase+1)%360;
drawPhase(phase);
window.setTimeout(()=>{
display();
},10);
}
let phase=0;
display();