the home site for me: also iteration 3 or 4 of my site

feat: add spherical ray diagram post

Changed files
+764
content
static
blog
spherical-ray-diagrams
tags
fancy
physics
tool
templates
shortcodes
+146
content/blog/2025-02-26_spherical-ray-diagrams.md
···
···
+
+++
+
title = "Determining the properties of a spherical mirror with ray diagrams"
+
date = 2025-02-26
+
slug = "spherical-ray-diagrams"
+
description = "yes i made a tool to help with it :)"
+
+
[taxonomies]
+
tags = ["tool", "fancy", "physics"]
+
+
[extra]
+
has_toc = true
+
+++
+
+
I was recently working through the Geometric Optics section of my physics textbook and was having trouble drawing all the ray diagrams (my wrist is still in a cast though that should come off in a few weeks) so I decided to try and make a tool to make them for me instead! I rather expected this to be a fairly simple process but instead it ended up being one of the most math intensive, most difficult — and also most rewarding — projects I've made recently!
+
+
<!-- more -->
+
+
## the tool (🥁 roll please)
+
+
{{ lensDiagram() }}
+
+
## the math
+
+
I was able to make it a bit simpler by restricting the domain of this tool to spherical mirrors (the only type used in this Module of my physics textbook) but I did tackle both concave and convex mirrors. It generates 3 rays: a horizontal ray, a ray through the focal point, and a ray through the radius of curvature. The first and last are quite easy to generate but the third was a bit more difficult. I ended up using a formula that I don't quite understand to get the point on the mirror where the ray intersects but it does work so 🤷.
+
+
The horizontal ray was dead simple. Draw a line from the top of the arrow to the edge of the mirror and then draw another line from focal point through the intersection point in the mirror. The part of that ray that is behind the mirror is simply the extension of the ray for virtual images but the part in front of the mirror is the actual path of the ray.
+
+
```javascript
+
// Draw the horizontal ray
+
ctx.strokeStyle = "green";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
let intersectionX =
+
Math.sqrt((R * scale) ** 2 - h ** 2) + circleCenterX;
+
ctx.lineTo(intersectionX, objY - h);
+
extendRayToCanvasEdge(
+
intersectionX,
+
objY - h,
+
centerX - F * scale,
+
centerY,
+
);
+
ctx.stroke();
+
```
+
+
The ray through the radius of curvature was also fairly simple but alot more fun to figure out the math for. Since we know that there is a right angle triange between the arrow, center line, and the radius we can use the pythagorean theorem to find the missing side of the intersection height and then we can use the ratio of the radius to the arrow base to find the proper x offset.
+
+
```javascript
+
// Draw the ray through the radius of curvature
+
ctx.strokeStyle = "orange";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
ctx.lineTo(circleCenterX, centerY);
+
const extendedRay3 = findCircleIntersection(
+
R * scale,
+
objX,
+
h,
+
circleCenterX,
+
centerY,
+
circleCenterX,
+
centerY,
+
);
+
ctx.lineTo(extendedRay3[0].x, extendedRay3[0].y);
+
extendRayToCanvasEdge(
+
extendedRay3[0].x,
+
extendedRay3[0].y,
+
centerX - R * scale,
+
centerY,
+
);
+
ctx.stroke();
+
```
+
+
The last ray, the one through the focal point, was the most difficult to figure out. I had to do quite a bit of geometry to find where this ray intersects the mirror. To find this intersection point I used a method that finds where a line intersects with a circle by solving a quadratic equation. This was necessary because the mirror is actually just part of a circle, and by finding where the ray intersects with that circle I can then determine if that intersection point is actually on the mirror's surface.
+
+
```javascript
+
// Draw the ray through the focal point
+
ctx.strokeStyle = "purple";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
ctx.lineTo(centerX - F * scale, centerY);
+
const extendedRay2 = findCircleIntersection(
+
R * scale,
+
objX,
+
h,
+
centerX - F * scale,
+
centerY,
+
circleCenterX,
+
centerY,
+
);
+
ctx.lineTo(extendedRay2[0].x, extendedRay2[0].y);
+
ctx.lineTo(0, extendedRay2[0].y);
+
ctx.stroke();
+
```
+
+
The method works by taking the equation of the line between our arrow tip and focal point (y = mx + b) and the equation of our mirror's circle ((x-h)² + (y-k)² = r²) and substituting one into the other. This gives us a quadratic equation that we can solve to find the x coordinates of the intersection points. Once we have these x values, we can plug them back into our line equation to get the y coordinates.
+
+
Then we just need to check which of these intersection points is actually on the mirror's surface (since a line can intersect a circle in up to two points) and use that for our ray. From there, we can draw the reflected ray just like with the other two methods.
+
+
I will freely admit that I made heavy use of gpt-4o to figure out the inital equations as thats a bit beyond the current scope of my knowledge. The rest of the ray logic was too complex for gemini or claude to figure out so that bit was all me 😎
+
+
```javascript
+
// fancy complex scary math 👻
+
function findCircleIntersection(radius, x1, h, x3, y3, centerX, centerY) {
+
// Check if the input values are valid
+
if (radius <= 0) {
+
throw new Error("Invalid input values.");
+
}
+
+
// Calculate the slope of the line from (x1, h) to (x3, y3)
+
const m = (y3 - (centerY - h)) / (x3 - x1);
+
+
// Define the line equation: y = h + m * (x - x1)
+
// Substitute into circle equation: (x-centerX)^2 + (y-centerY)^2 = radius^2
+
// y = h + m * (x - x1)
+
// (x-centerX)^2 + (h + m*(x-x1) - centerY)^2 = radius^2
+
+
// Coefficients for the quadratic equation
+
const a = 1 + m * m;
+
const b = -2 * centerX + 2 * m * (centerY - h - centerY - m * x1);
+
const c =
+
centerX * centerX +
+
(centerY - h - centerY - m * x1) *
+
(centerY - h - centerY - m * x1) -
+
radius * radius;
+
+
// Calculate the discriminant
+
const discriminant = b * b - 4 * a * c;
+
+
if (discriminant < 0) {
+
throw new Error("No intersection found.");
+
}
+
+
// Calculate the two possible x values
+
const xIntersect1 = (-b + Math.sqrt(discriminant)) / (2 * a);
+
const xIntersect2 = (-b - Math.sqrt(discriminant)) / (2 * a);
+
+
// Calculate the corresponding y values
+
const yIntersect1 = centerY - h + m * (xIntersect1 - x1);
+
const yIntersect2 = centerY - h + m * (xIntersect2 - x1);
+
+
// Return the intersection points
+
return [
+
{ x: xIntersect1, y: yIntersect1 },
+
{ x: xIntersect2, y: yIntersect2 },
+
];
+
}
+
```
static/blog/spherical-ray-diagrams/og.png

This is a binary file and will not be displayed.

static/tags/fancy/og.png

This is a binary file and will not be displayed.

static/tags/physics/og.png

This is a binary file and will not be displayed.

static/tags/tool/og.png

This is a binary file and will not be displayed.

+618
templates/shortcodes/lensDiagram.html
···
···
+
<div
+
id="rayTracer"
+
style="display: flex; flex-direction: column; min-height: 30rem"
+
>
+
<div class="controls" style="display: flex; flex-direction: column">
+
<div style="display: flex; gap: 20px; align-items: center">
+
<div>
+
<label>Mirror Type:</label>
+
<select id="mirrorType">
+
<option value="concave">Concave Mirror</option>
+
<option value="convex">Convex Mirror</option>
+
</select>
+
</div>
+
<div>
+
<label>Radius of Curvature:</label>
+
<input type="number" id="radius" value="20" min="1" />
+
</div>
+
<div>
+
<label>Object Distance:</label>
+
<input type="number" id="objectDist" value="30" min="1" />
+
</div>
+
</div>
+
<div>
+
<label>Zoom:</label>
+
<input
+
type="range"
+
id="zoom"
+
min="0.01"
+
max="8"
+
step="0.01"
+
value="1"
+
style="width: 100%"
+
/>
+
</div>
+
</div>
+
<canvas id="canvas" style="flex: 1; cursor: move"></canvas>
+
</div>
+
+
<style>
+
#rayTracer {
+
padding: 20px;
+
}
+
.controls {
+
margin-bottom: 20px;
+
}
+
.controls div {
+
margin: 0.2rem 0;
+
}
+
#canvas {
+
border: 1px solid #ccc;
+
width: 100%;
+
}
+
</style>
+
+
<script>
+
const canvas = document.getElementById("canvas");
+
const ctx = canvas.getContext("2d");
+
const mirrorType = document.getElementById("mirrorType");
+
const radiusInput = document.getElementById("radius");
+
const objectDistInput = document.getElementById("objectDist");
+
const zoomInput = document.getElementById("zoom");
+
+
let offsetX = 0;
+
let offsetY = 0;
+
let isDragging = false;
+
let lastX = 0;
+
let lastY = 0;
+
+
canvas.addEventListener("mousedown", (e) => {
+
isDragging = true;
+
lastX = e.clientX;
+
lastY = e.clientY;
+
});
+
+
canvas.addEventListener("mousemove", (e) => {
+
if (isDragging) {
+
offsetX += e.clientX - lastX;
+
offsetY += e.clientY - lastY;
+
lastX = e.clientX;
+
lastY = e.clientY;
+
update();
+
}
+
});
+
+
canvas.addEventListener("mouseup", () => {
+
isDragging = false;
+
});
+
+
canvas.addEventListener("mouseleave", () => {
+
isDragging = false;
+
});
+
+
canvas.addEventListener("wheel", (e) => {
+
e.preventDefault();
+
const zoomSpeed = 0.001;
+
const newZoom = parseFloat(zoomInput.value) - e.deltaY * zoomSpeed;
+
zoomInput.value = Math.min(Math.max(newZoom, 0.01), 8);
+
update();
+
});
+
+
function calculateReflectedRay(
+
startX,
+
startY,
+
incidentX,
+
incidentY,
+
centerX,
+
centerY,
+
radius,
+
) {
+
// Calculate normal vector at intersection point
+
const nx = (incidentX - centerX) / radius;
+
const ny = (incidentY - centerY) / radius;
+
+
// Calculate incident vector
+
const ix = incidentX - startX;
+
const iy = incidentY - startY;
+
const iLen = Math.sqrt(ix * ix + iy * iy);
+
const dirX = ix / iLen;
+
const dirY = iy / iLen;
+
+
// Calculate reflection using r = i - 2(i·n)n
+
const dot = dirX * nx + dirY * ny;
+
const reflectX = dirX - 2 * dot * nx;
+
const reflectY = dirY - 2 * dot * ny;
+
+
// Extend reflected ray to edge of canvas
+
const t = Math.max(
+
Math.abs((0 - incidentX) / reflectX),
+
Math.abs((canvas.width - incidentX) / reflectX),
+
Math.abs((0 - incidentY) / reflectY),
+
Math.abs((canvas.height - incidentY) / reflectY),
+
);
+
+
return {
+
x: incidentX + reflectX * t,
+
y: incidentY + reflectY * t,
+
};
+
}
+
+
function drawMirror(isConcave, R) {
+
const scale = (canvas.width / (R * 6)) * parseFloat(zoomInput.value);
+
const centerX = canvas.width / 2 + R * scale * isConcave + offsetX;
+
const centerY = canvas.height / 2 + offsetY;
+
+
ctx.beginPath();
+
ctx.strokeStyle = "black";
+
if (isConcave) {
+
ctx.arc(
+
centerX - R * scale,
+
centerY,
+
R * scale,
+
-Math.PI / 3,
+
Math.PI / 3,
+
);
+
} else {
+
ctx.arc(
+
centerX + R * scale,
+
centerY,
+
R * scale,
+
(2 * Math.PI) / 3,
+
(4 * Math.PI) / 3,
+
);
+
}
+
ctx.stroke();
+
}
+
+
function drawArrow(x, y, height) {
+
const arrowHeadSize = height * 0.1; // Scale arrow head with height
+
ctx.lineWidth = 1;
+
ctx.beginPath();
+
+
// Draw the main shaft
+
ctx.moveTo(x, y);
+
ctx.lineTo(x, y - height * 0.9);
+
+
// Draw the arrow head
+
ctx.moveTo(x, y - height);
+
ctx.lineTo(x - arrowHeadSize, y - height + arrowHeadSize);
+
ctx.moveTo(x, y - height);
+
ctx.lineTo(x + arrowHeadSize, y - height + arrowHeadSize);
+
ctx.moveTo(x - arrowHeadSize, y - height + arrowHeadSize);
+
ctx.lineTo(x + arrowHeadSize, y - height + arrowHeadSize);
+
+
ctx.stroke();
+
}
+
+
function extendRayToCanvasEdge(x1, y1, x2, y2) {
+
const rayDirX = x2 - x1;
+
const rayDirY = y2 - y1;
+
const t = Math.max(
+
Math.abs((0 - x2) / rayDirX),
+
Math.abs((canvas.width - x2) / rayDirX),
+
Math.abs((0 - y2) / rayDirY),
+
Math.abs((canvas.height - y2) / rayDirY),
+
);
+
ctx.lineTo(x2 + rayDirX * t, y2 + rayDirY * t);
+
}
+
+
function findCircleIntersection(radius, x1, h, x3, y3, centerX, centerY) {
+
// Check if the input values are valid
+
if (radius <= 0) {
+
throw new Error("Invalid input values.");
+
}
+
+
// Calculate the slope of the line from (x1, h) to (x3, y3)
+
const m = (y3 - (centerY - h)) / (x3 - x1);
+
+
// Define the line equation: y = h + m * (x - x1)
+
// Substitute into circle equation: (x-centerX)^2 + (y-centerY)^2 = radius^2
+
// y = h + m * (x - x1)
+
// (x-centerX)^2 + (h + m*(x-x1) - centerY)^2 = radius^2
+
+
// Coefficients for the quadratic equation
+
const a = 1 + m * m;
+
const b = -2 * centerX + 2 * m * (centerY - h - centerY - m * x1);
+
const c =
+
centerX * centerX +
+
(centerY - h - centerY - m * x1) *
+
(centerY - h - centerY - m * x1) -
+
radius * radius;
+
+
// Calculate the discriminant
+
const discriminant = b * b - 4 * a * c;
+
+
if (discriminant < 0) {
+
throw new Error("No intersection found.");
+
}
+
+
// Calculate the two possible x values
+
const xIntersect1 = (-b + Math.sqrt(discriminant)) / (2 * a);
+
const xIntersect2 = (-b - Math.sqrt(discriminant)) / (2 * a);
+
+
// Calculate the corresponding y values
+
const yIntersect1 = centerY - h + m * (xIntersect1 - x1);
+
const yIntersect2 = centerY - h + m * (xIntersect2 - x1);
+
+
// Return the intersection points
+
return [
+
{ x: xIntersect1, y: yIntersect1 },
+
{ x: xIntersect2, y: yIntersect2 },
+
];
+
}
+
+
function drawRays(isConcave, R, objDist) {
+
const scale = (canvas.width / (R * 6)) * parseFloat(zoomInput.value);
+
const F = R / 2;
+
const h = (R * scale) / 3;
+
const centerX = canvas.width / 2 + R * scale + offsetX;
+
const centerY = canvas.height / 2 + offsetY;
+
const objX =
+
centerX +
+
objDist * scale * (isConcave ? -1 : -1) -
+
R * scale * !isConcave;
+
const objY = centerY;
+
+
drawArrow(objX, objY, h);
+
+
ctx.beginPath();
+
ctx.moveTo(0, centerY);
+
ctx.lineTo(canvas.width, centerY);
+
ctx.stroke();
+
+
ctx.fillStyle = "red";
+
ctx.beginPath();
+
ctx.arc(centerX - F * scale, centerY, 3, 0, 2 * Math.PI);
+
ctx.fill();
+
+
ctx.fillStyle = "blue";
+
ctx.beginPath();
+
ctx.arc(centerX - R * scale * isConcave, centerY, 3, 0, 2 * Math.PI);
+
ctx.fill();
+
+
const circleCenterX = isConcave
+
? centerX - R * scale
+
: centerX - R * scale;
+
+
if (isConcave) {
+
// ray that travels from the top of the object towards the mirror and then calculating the bounce angle it goes in that direction
+
ctx.strokeStyle = "green";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
let intersectionX =
+
Math.sqrt((R * scale) ** 2 - h ** 2) + circleCenterX;
+
ctx.lineTo(intersectionX, objY - h);
+
extendRayToCanvasEdge(
+
intersectionX,
+
objY - h,
+
centerX - F * scale,
+
centerY,
+
);
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(0, 128, 0, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(intersectionX, objY - h);
+
extendRayToCanvasEdge(
+
centerX - F * scale,
+
centerY,
+
intersectionX,
+
objY - h,
+
);
+
ctx.stroke();
+
+
// draw a point at the intersection of the ray and the mirror
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(intersectionX, objY - h, 3, 0, 2 * Math.PI);
+
ctx.fill();
+
+
// draw a ray that travels from the top of the object towards the focal point of the mirror and through the focal point till it reaches the mirror
+
ctx.strokeStyle = "purple";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
ctx.lineTo(centerX - F * scale, centerY);
+
const extendedRay2 = findCircleIntersection(
+
R * scale,
+
objX,
+
h,
+
centerX - F * scale,
+
centerY,
+
circleCenterX,
+
centerY,
+
);
+
ctx.lineTo(extendedRay2[0].x, extendedRay2[0].y);
+
ctx.lineTo(0, extendedRay2[0].y);
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(128, 0, 128, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(extendedRay2[0].x, extendedRay2[0].y);
+
ctx.lineTo(canvas.width, extendedRay2[0].y);
+
ctx.stroke();
+
+
// draw a point at the intersection of the ray and the mirror
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(extendedRay2[0].x, extendedRay2[0].y, 3, 0, 2 * Math.PI);
+
ctx.fill();
+
+
// draw a ray that travels from the top of the object through the radius of curvature of the mirror
+
ctx.strokeStyle = "orange";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
ctx.lineTo(circleCenterX, centerY);
+
const extendedRay3 = findCircleIntersection(
+
R * scale,
+
objX,
+
h,
+
circleCenterX,
+
centerY,
+
circleCenterX,
+
centerY,
+
);
+
ctx.lineTo(extendedRay3[0].x, extendedRay3[0].y);
+
extendRayToCanvasEdge(
+
extendedRay3[0].x,
+
extendedRay3[0].y,
+
centerX - R * scale,
+
centerY,
+
);
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(255, 165, 0, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(extendedRay3[0].x, extendedRay3[0].y);
+
extendRayToCanvasEdge(
+
centerX - R * scale,
+
centerY,
+
extendedRay3[0].x,
+
extendedRay3[0].y,
+
);
+
ctx.stroke();
+
+
// draw a point at the intersection of the ray and the mirror
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(extendedRay3[0].x, extendedRay3[0].y, 3, 0, 2 * Math.PI);
+
ctx.fill();
+
} else {
+
// draw a ray that travels from the top of the object horizontally towards the mirror
+
ctx.strokeStyle = "green";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
ctx.lineTo(
+
centerX - Math.sqrt((R * scale) ** 2 - h ** 2),
+
objY - h,
+
);
+
extendRayToCanvasEdge(
+
centerX - F * scale,
+
centerY,
+
centerX - Math.sqrt((R * scale) ** 2 - h ** 2),
+
objY - h,
+
);
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(0, 128, 0, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(
+
centerX - Math.sqrt((R * scale) ** 2 - h ** 2),
+
objY - h,
+
);
+
ctx.lineTo(centerX - F * scale, centerY);
+
ctx.stroke();
+
+
// draw a point at the intersection of the ray and the mirror
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(
+
centerX - Math.sqrt((R * scale) ** 2 - h ** 2),
+
objY - h,
+
3,
+
0,
+
2 * Math.PI,
+
);
+
ctx.fill();
+
+
// draw a ray that travels from the top of the object towards the focal point of the mirror and through the focal point till it reaches the mirror
+
ctx.strokeStyle = "purple";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
const extendedRay2 = findCircleIntersection(
+
R * scale,
+
objX,
+
h,
+
centerX - F * scale,
+
centerY,
+
circleCenterX,
+
centerY,
+
);
+
const extendedRay2Y = centerY - (extendedRay2[0].y - centerY);
+
ctx.lineTo(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (centerY - extendedRay2Y) ** 2,
+
),
+
centerY - (extendedRay2[0].y - centerY),
+
);
+
ctx.lineTo(0, centerY - (extendedRay2[0].y - centerY));
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(128, 0, 128, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (centerY - extendedRay2Y) ** 2,
+
),
+
centerY - (extendedRay2[0].y - centerY),
+
);
+
ctx.lineTo(canvas.width, centerY - (extendedRay2[0].y - centerY));
+
ctx.stroke();
+
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (centerY - extendedRay2Y) ** 2,
+
),
+
centerY - (extendedRay2[0].y - centerY),
+
3,
+
0,
+
2 * Math.PI,
+
);
+
ctx.fill();
+
+
// draw a ray that travels from the top of the object through the radius of curvature of the mirror
+
ctx.strokeStyle = "orange";
+
ctx.beginPath();
+
ctx.lineTo(objX, objY - h);
+
// ctx.lineTo(centerX, centerY);
+
const extendedRay3ScaleFactor =
+
(R * scale) / Math.abs(objX - centerX);
+
ctx.lineTo(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2,
+
),
+
centerY - h * extendedRay3ScaleFactor,
+
);
+
extendRayToCanvasEdge(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2,
+
),
+
centerY - h * extendedRay3ScaleFactor,
+
objX,
+
objY - h,
+
);
+
ctx.stroke();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(255, 165, 0, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2,
+
),
+
centerY - h * extendedRay3ScaleFactor,
+
);
+
extendRayToCanvasEdge(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2,
+
),
+
centerY - h * extendedRay3ScaleFactor,
+
centerX,
+
centerY,
+
);
+
ctx.stroke();
+
+
// draw a point at the intersection of the ray and the mirror
+
+
ctx.fillStyle = "black";
+
ctx.beginPath();
+
ctx.arc(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2,
+
),
+
centerY - h * extendedRay3ScaleFactor,
+
3,
+
0,
+
2 * Math.PI,
+
);
+
ctx.fill();
+
+
// draw an extension of the ray through the mirror in a slightly opacified color
+
ctx.strokeStyle = "rgba(255, 165, 0, 0.5)";
+
ctx.beginPath();
+
ctx.lineTo(
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (centerY - extendedRay3Y) ** 2,
+
),
+
centerY - (extendedRay3[0].y - centerY),
+
);
+
extendRayToCanvasEdge(
+
centerX - R * scale,
+
centerY,
+
centerX -
+
Math.sqrt(
+
(R * scale) ** 2 - (centerY - extendedRay3Y) ** 2,
+
),
+
centerY - (extendedRay3[0].y - centerY),
+
);
+
ctx.stroke();
+
}
+
}
+
+
function update() {
+
canvas.width = canvas.offsetWidth;
+
canvas.height = canvas.offsetHeight * 0.8;
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
+
ctx.fillStyle = "#f0f0f0";
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
+
const isConcave = mirrorType.value === "concave";
+
const R = parseFloat(radiusInput.value);
+
const objDist = parseFloat(objectDistInput.value);
+
+
drawMirror(isConcave, R);
+
drawRays(isConcave, R, objDist);
+
}
+
+
mirrorType.addEventListener("change", update);
+
radiusInput.addEventListener("input", update);
+
objectDistInput.addEventListener("input", update);
+
zoomInput.addEventListener("input", update);
+
window.addEventListener("resize", update);
+
+
update();
+
+
let isCanvasHovered = false;
+
+
canvas.addEventListener("mouseenter", () => {
+
isCanvasHovered = true;
+
});
+
+
canvas.addEventListener("mouseleave", () => {
+
isCanvasHovered = false;
+
});
+
+
document.addEventListener("keydown", (e) => {
+
if (!isCanvasHovered) return;
+
if (e.key === "+" || e.key === "=") {
+
zoomInput.value = Math.min(parseFloat(zoomInput.value) + 0.1, 8);
+
update();
+
}
+
if (e.key === "-" || e.key === "_") {
+
zoomInput.value = Math.max(parseFloat(zoomInput.value) - 0.1, 0.1);
+
update();
+
}
+
+
// translate the canvas
+
if (e.key === "ArrowUp") {
+
offsetY -= 25;
+
update();
+
}
+
if (e.key === "ArrowDown") {
+
offsetY += 25;
+
update();
+
}
+
if (e.key === "ArrowLeft") {
+
offsetX -= 25;
+
update();
+
}
+
if (e.key === "ArrowRight") {
+
offsetX += 25;
+
update();
+
}
+
});
+
</script>