Public
Edited
Apr 17, 2023
1 fork
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { vec2 } from "@esperanc/vec2-utils"
Insert cell
height = Math.trunc((width * 9) / 16)
Insert cell
Insert cell
function trajectory(ray, lenses, tMax = 1e4) {
let legs = [];
function nextLensHit(ray, tMin = 0) {
let hits = lenses
.map((lens) => [lens, lens.hit(ray, tMin)])
.filter(([l, h]) => h);
hits.sort(([r1, [t1, _]], [r2, [t2, __]]) => t1 - t2);
return hits;
}
let t = 0;
do {
let hits = nextLensHit(ray, t + 0.001);
if (hits.length == 0) {
legs.push([ray, tMax]);
break;
} else {
let [lens, [tHit, u]] = hits[0];
let { ray: newRay } = lens.refract(ray);
legs.push([ray, Math.min(tMax, tHit)]);
t = tHit;
ray = newRay;
}
} while (t < tMax);
return legs;
}
Insert cell
// Test code for trajectory
{
const { sourceX, sourceY, focalLen } = demoLensParms;
const lens1 = new Lens([width / 3, height / 2], [1, 0], focalLen, height / 2);
const lens2 = new Lens(
[(2 * width) / 3, height / 2],
[1, 0],
focalLen,
height / 2
);
const angle = 0;
const ray = new Ray([sourceX, sourceY], [Math.cos(angle), Math.sin(angle)]);
return trajectory(ray, [lens1, lens2]);
}
Insert cell
Insert cell
class Lens {
// center -> center point of the lens
// axis -> vector axis of the lens
// focusLen -> focus length of the lens
// size -> size of the lens
// mirror -> true if modeling a mirror rather than a lens
constructor(center, axis, focusLen, size, mirror = false) {
vec2.normalize(axis, axis);
Object.assign(this, { center, axis, focusLen, size });
this.reverse = mirror ? -1 : 1;
this.ray = new Ray(this.center, [-axis[1], axis[0]]); // Ray perpendicular to axis
}

// Returns the ray intersection parameters with the lens, or null if no hit
hit(incomingRay, tMin = 0) {
// Test for intersection with lens
const [t, u] = incomingRay.intersectRay(this.ray);
if (t < tMin || Math.abs(u) > this.size / 2) return false;
return [t, u];
}

// Returns the incoming ray refracted by the lens, parameterized accordingly.
// If the ray misses the lens, returns false
refract(incomingRay) {
let { axis, ray, focusLen, center, size, reverse } = this;

// Make sure to consider axis pointing to the other side of light source
if (vec2.dot(incomingRay.v, axis) * reverse < 0)
axis = vec2.scale([], axis, -1);

// Test for intersection with lens
const tu = this.hit(incomingRay);
if (!tu) return false;
const [t, u] = tu;
const p = incomingRay.at(t); // Intersection point

// Compute slope of incoming ray
const v1 = vec2.orthoProj([], incomingRay.v, axis);
const v2 = vec2.sub([], incomingRay.v, v1);
const sgn = Math.sign(vec2.dot(vec2.sub([], p, center), v2));
const m = vec2.len(v2) / vec2.len(v1);

// Compute slope of outgoing ray
const h = vec2.dist(p, center);
const sgn2 = Math.sign(vec2.dot(v2, ray.v));
const mprime = sgn2 * (m - (sgn * h) / focusLen);
const r = vec2.normalize([], vec2.scaleAndAdd([], axis, ray.v, mprime));

// Parameterize outgoing ray so that source is at same distance as incoming ray
const porig = new Ray(p, r).at(-t);

// Return the outgoing ray and the value of t at the intersection point
return { t, ray: new Ray(porig, r) };
}

// Draw on canvas context ctx
draw(ctx) {
let pts =
this.focusLen < 0
? [
...arrowHead(this.ray.translated(this.size / 2).rotated(Math.PI)),
...arrowHead(
this.ray
.scaled(-1)
.translated(this.size / 2)
.rotated(Math.PI)
)
]
: [
...arrowHead(this.ray.translated(this.size / 2)),
...arrowHead(this.ray.scaled(-1).translated(this.size / 2))
];
ctx.beginPath();
for (let p of pts) ctx.lineTo(...p);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.arc(
...vec2.scaleAndAdd([], this.center, this.axis, this.focusLen),
5,
0,
Math.PI * 2
);
ctx.fill();
}
}
Insert cell
//
// Returns the vertices of an arrowhead at the origin of ray with the given length and width.
//
function arrowHead(ray, length = 8, wid = 5) {
ray = ray.normalized();
return [
ray.p,
ray.rotated(Math.PI / 2).translated(wid / 2).p,
ray.translated(length).p,
ray.rotated(-Math.PI / 2).translated(wid / 2).p,
ray.p
];
}
Insert cell
// test for arrowHead
arrowHead(new Ray([100, 100], [10, 10]).rotated(Math.PI))
Insert cell
Insert cell
class Ray {
// Constructor from a point and a vector
constructor(p, v) {
Object.assign(this, { p, v });
}

// Point at p + t v
at(t) {
return vec2.scaleAndAdd([], this.p, this.v, t);
}

// Returns a copy of this ray with its direction vector normalized
normalized() {
return new Ray(this.p, vec2.normalize([], this.v));
}

// Returns a copy of this ray rotated by angle radians
rotated(angle) {
return new Ray(this.p, vec2.rotate([], this.v, [0, 0], angle));
}

// Returns a copy of this ray translated by its direction multiplied by delta
translated(delta) {
return new Ray(vec2.scaleAndAdd([], this.p, this.v, delta), this.v);
}

// Returns a copy of this ray with its vector scaled by delta
scaled(delta) {
return new Ray(this.p, vec2.scale([], this.v, delta));
}

// Tells whether the two ray-defined lines intersect
intersectsRay(other) {
const { p: p1, v: v1 } = this;
const { p: p2, v: v2 } = other;
const D = v1[0] * v2[1] - v1[1] * v2[0];
return D != 0;
}

// Returns the two parameters [t,u] for the ray intersection with other
// such that if this.intersectsRay(other), then
// this.at(t) == other.at(u) == intersection point
intersectRay(other) {
const { p: p1, v: v1 } = this;
const { p: p2, v: v2 } = other;
const D = v1[0] * v2[1] - v1[1] * v2[0];
const t = (v2[1] * (p2[0] - p1[0]) + p1[1] * v2[0] - p2[1] * v2[0]) / D;
const u = -(v1[1] * (p1[0] - p2[0]) + p2[1] * v1[0] - p1[1] * v1[0]) / D;
return [t, u];
}
}
Insert cell
Insert cell
new Ray([0, 0], [1, 1]).intersectRay(new Ray([1, 0], [-1, 1]))
Insert cell
new Ray([0, 0], [1, 1]).intersectRay(new Ray([1, 0], [1, 1]))
Insert cell
new Ray([0, 0], [1, 1]).intersectsRay(new Ray([1, 0], [-1, 1]))
Insert cell
new Ray([0, 0], [1, 1]).intersectsRay(new Ray([1, 0], [1, 1]))
Insert cell
new Ray([0, 0], [1, 1]).at(0.5)
Insert cell
Insert cell
function smoothControlPoints(pts, edgeRatio = 0.333) {
const n = pts.length;
const edges = pts.map((p, i) => vec2.sub([], pts[(i + 1) % n], p));
const lengths = edges.map((e) => vec2.length(e));
const normEdges = edges.map((e, i) => vec2.scale([], e, 1 / lengths[i]));
const tangents = normEdges.map((e, i) =>
vec2.normalize([], vec2.add([], e, normEdges[(i - 1 + n) % n]))
);
let cp = [];
for (let i = 0; i < n; i++) {
const inext = (i + 1) % n;
cp.push([
vec2.scaleAndAdd([], pts[i], tangents[i], lengths[i] * edgeRatio),
vec2.scaleAndAdd([], pts[inext], tangents[inext], -lengths[i] * edgeRatio)
]);
}
return cp;
}
Insert cell
Insert cell
Insert cell
{
const canvas = htl.html`<canvas width=600 height=600>`;
const ctx = canvas.getContext("2d");
const pts = [
[200, 200],
[200, 350],
[200, 250],
[200, 400],
[400, 400],
[400, 200]
];
const cpts = smoothControlPoints(pts, edgeRatio);
//return cpts;
for (let p of pts) {
ctx.beginPath();
ctx.arc(...p, 5, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = "red";
for (let [p, q] of cpts) {
ctx.beginPath();
ctx.arc(...p, 5, 0, Math.PI * 2);
ctx.arc(...q, 5, 0, Math.PI * 2);
ctx.fill();
}
ctx.beginPath();
let n = pts.length;
ctx.moveTo(...pts[0]);
for (let i = 0; i < n; i++) {
let [cp1, cp2] = cpts[i];
ctx.bezierCurveTo(...cp1, ...cp2, ...pts[(i + 1) % n]);
}
ctx.stroke();
return canvas;
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more