<!-- Canvas-based Neural ODE demo: vector field + numerical integration paths -->
<div class="controls">
<label for="field-select">Field:</label>
<select id="field-select">
<option value="spiral">Spiral sink</option>
<option value="saddle">Saddle</option>
<option value="vortex">Vortex</option>
</select>
<label for="method-select">Integrator:</label>
<select id="method-select">
<option value="euler">Euler</option>
<option value="rk4">Euler vs RK4</option>
</select>
<button id="btn-reset" type="button">Reset</button>
</div>
<canvas id="canvas" width="420" height="300"></canvas>
<div id="info" class="info">Click anywhere on the canvas to place a starting point.</div>
/* Styles for the Neural ODE vector field demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: #fff; }
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; margin-bottom: .5rem; font-size: .85rem; }
label { font-weight: 600; }
select { padding: .25rem .4rem; border: 1px solid #aaa; border-radius: 6px; font-size: .85rem; }
button { padding: .3rem .7rem; font: 600 .85rem system-ui; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button:hover { background: #14253e; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair; max-width: 100%; }
.info { margin-top: .4rem; font-size: .85rem; color: #555; min-height: 1.3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Neural ODE demo: draw a vector field and integrate a trajectory through it
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// World coordinate domain and canvas mapping helpers
const XMIN = -2, XMAX = 2, YMIN = -1.5, YMAX = 1.5;
const toCanvasX = x => (x - XMIN) / (XMAX - XMIN) * W;
const toCanvasY = y => H - (y - YMIN) / (YMAX - YMIN) * H;
const fromCanvas = (px, py) => [
XMIN + px / W * (XMAX - XMIN),
YMIN + (H - py) / H * (YMAX - YMIN)
];
// Three preset vector fields — each is a simple 2D ODE right-hand side
const FIELDS = {
spiral: (x, y) => [-x - y, x - y],
saddle: (x, y) => [x, -y],
vortex: (x, y) => [-y, x]
};
// Euler method: constant step size, first-order accuracy
function eulerSteps(f, x0, y0, t1, dt) {
const path = [[x0, y0]];
let x = x0, y = y0;
for (let t = 0; t < t1; t += dt) {
const [dx, dy] = f(x, y);
x += dx * dt;
y += dy * dt;
path.push([x, y]);
// stop if trajectory leaves the visible domain
if (Math.abs(x) > 5 || Math.abs(y) > 5) break;
}
return path;
}
// Runge-Kutta 4: 4 evaluations per step, fourth-order accuracy
function rk4Steps(f, x0, y0, t1, dt) {
const path = [[x0, y0]];
let x = x0, y = y0;
for (let t = 0; t < t1; t += dt) {
const [k1x, k1y] = f(x, y);
const [k2x, k2y] = f(x + k1x * dt / 2, y + k1y * dt / 2);
const [k3x, k3y] = f(x + k2x * dt / 2, y + k2y * dt / 2);
const [k4x, k4y] = f(x + k3x * dt, y + k3y * dt);
x += (k1x + 2 * k2x + 2 * k3x + k4x) * dt / 6;
y += (k1y + 2 * k2y + 2 * k3y + k4y) * dt / 6;
path.push([x, y]);
if (Math.abs(x) > 5 || Math.abs(y) > 5) break;
}
return path;
}
// Draw the vector field as a grid of arrows
function drawArrows(f) {
const COLS = 18, ROWS = 13;
const stepX = W / COLS, stepY = H / ROWS;
ctx.strokeStyle = '#aab8c8';
ctx.lineWidth = 0.8;
for (let i = 0; i < COLS; i++) {
for (let j = 0; j < ROWS; j++) {
const px = (i + 0.5) * stepX, py = (j + 0.5) * stepY;
const [wx, wy] = fromCanvas(px, py);
const [dvx, dvy] = f(wx, wy);
const mag = Math.sqrt(dvx * dvx + dvy * dvy) + 1e-9;
const scale = Math.min(stepX, stepY) * 0.38 / Math.sqrt(mag + 0.3);
const ex = px + dvx * scale, ey = py - dvy * scale;
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(ex, ey);
// small arrowhead lines
const angle = Math.atan2(ey - py, ex - px);
ctx.lineTo(ex - 5 * Math.cos(angle - 0.4), ey - 5 * Math.sin(angle - 0.4));
ctx.moveTo(ex, ey);
ctx.lineTo(ex - 5 * Math.cos(angle + 0.4), ey - 5 * Math.sin(angle + 0.4));
ctx.stroke();
}
}
}
// Draw an integrated trajectory path on the canvas
function drawPath(path, color, lineWidth) {
if (path.length < 2) return;
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(toCanvasX(path[0][0]), toCanvasY(path[0][1]));
for (let i = 1; i < path.length; i++) {
ctx.lineTo(toCanvasX(path[i][0]), toCanvasY(path[i][1]));
}
ctx.stroke();
}
// Draw a filled circle (starting point marker)
function drawDot(x, y, color, r) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(toCanvasX(x), toCanvasY(y), r, 0, 2 * Math.PI);
ctx.fill();
}
let startPt = null;
function getField() {
return FIELDS[document.getElementById('field-select').value];
}
function getMethod() {
return document.getElementById('method-select').value;
}
function redraw() {
ctx.clearRect(0, 0, W, H);
const f = getField();
drawArrows(f);
if (!startPt) return;
const [x0, y0] = startPt;
const T = 4, DT_EULER = 0.05, DT_RK4 = 0.1;
const method = getMethod();
if (method === 'euler') {
// single Euler path in orange
const path = eulerSteps(f, x0, y0, T, DT_EULER);
drawPath(path, '#e07b39', 2.5);
// report how many Euler steps were taken
const steps = path.length - 1;
document.getElementById('info').textContent =
fmt('Euler: %steps% steps to integrate T=4.', { steps: steps });
} else {
// show Euler (faded) and RK4 (bright) side by side
const pathE = eulerSteps(f, x0, y0, T, DT_EULER);
const pathR = rk4Steps(f, x0, y0, T, DT_RK4);
drawPath(pathE, '#e0a039', 1.5);
drawPath(pathR, '#c8a000', 2.5);
const stepsE = pathE.length - 1;
const stepsR = pathR.length - 1;
document.getElementById('info').textContent =
fmt('RK4: %stepsR% steps (gold) vs Euler: %stepsE% steps (faded). Same field, better accuracy per step.', { stepsR: stepsR, stepsE: stepsE });
}
// red dot marks the starting point
drawDot(x0, y0, '#e63946', 5);
}
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const px = (e.clientX - rect.left) * (W / rect.width);
const py = (e.clientY - rect.top) * (H / rect.height);
startPt = fromCanvas(px, py);
redraw();
});
document.getElementById('field-select').addEventListener('change', redraw);
document.getElementById('method-select').addEventListener('change', redraw);
document.getElementById('btn-reset').addEventListener('click', () => {
startPt = null;
redraw();
document.getElementById('info').textContent = 'Click anywhere on the canvas to place a starting point.';
});
// Draw the initial field on load (no trajectory yet)
redraw();
<!-- Canvas-based Neural ODE demo: vector field + numerical integration paths -->
<div class="controls">
<label for="field-select">Field:</label>
<select id="field-select">
<option value="spiral">Spiral sink</option>
<option value="saddle">Saddle</option>
<option value="vortex">Vortex</option>
</select>
<label for="method-select">Integrator:</label>
<select id="method-select">
<option value="euler">Euler</option>
<option value="rk4">Euler vs RK4</option>
</select>
<button id="btn-reset" type="button">Reset</button>
</div>
<canvas id="canvas" width="420" height="300"></canvas>
<div id="info" class="info">Click anywhere on the canvas to place a starting point.</div>
/* Styles for the Neural ODE vector field demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: #fff; }
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; margin-bottom: .5rem; font-size: .85rem; }
label { font-weight: 600; }
select { padding: .25rem .4rem; border: 1px solid #aaa; border-radius: 6px; font-size: .85rem; }
button { padding: .3rem .7rem; font: 600 .85rem system-ui; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button:hover { background: #14253e; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair; max-width: 100%; }
.info { margin-top: .4rem; font-size: .85rem; color: #555; min-height: 1.3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Neural ODE demo: draw a vector field and integrate a trajectory through it
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// World coordinate domain and canvas mapping helpers
const XMIN = -2, XMAX = 2, YMIN = -1.5, YMAX = 1.5;
const toCanvasX = x => (x - XMIN) / (XMAX - XMIN) * W;
const toCanvasY = y => H - (y - YMIN) / (YMAX - YMIN) * H;
const fromCanvas = (px, py) => [
XMIN + px / W * (XMAX - XMIN),
YMIN + (H - py) / H * (YMAX - YMIN)
];
// Three preset vector fields — each is a simple 2D ODE right-hand side
const FIELDS = {
spiral: (x, y) => [-x - y, x - y],
saddle: (x, y) => [x, -y],
vortex: (x, y) => [-y, x]
};
// Euler method: constant step size, first-order accuracy
function eulerSteps(f, x0, y0, t1, dt) {
const path = [[x0, y0]];
let x = x0, y = y0;
for (let t = 0; t < t1; t += dt) {
const [dx, dy] = f(x, y);
x += dx * dt;
y += dy * dt;
path.push([x, y]);
// stop if trajectory leaves the visible domain
if (Math.abs(x) > 5 || Math.abs(y) > 5) break;
}
return path;
}
// Runge-Kutta 4: 4 evaluations per step, fourth-order accuracy
function rk4Steps(f, x0, y0, t1, dt) {
const path = [[x0, y0]];
let x = x0, y = y0;
for (let t = 0; t < t1; t += dt) {
const [k1x, k1y] = f(x, y);
const [k2x, k2y] = f(x + k1x * dt / 2, y + k1y * dt / 2);
const [k3x, k3y] = f(x + k2x * dt / 2, y + k2y * dt / 2);
const [k4x, k4y] = f(x + k3x * dt, y + k3y * dt);
x += (k1x + 2 * k2x + 2 * k3x + k4x) * dt / 6;
y += (k1y + 2 * k2y + 2 * k3y + k4y) * dt / 6;
path.push([x, y]);
if (Math.abs(x) > 5 || Math.abs(y) > 5) break;
}
return path;
}
// Draw the vector field as a grid of arrows
function drawArrows(f) {
const COLS = 18, ROWS = 13;
const stepX = W / COLS, stepY = H / ROWS;
ctx.strokeStyle = '#aab8c8';
ctx.lineWidth = 0.8;
for (let i = 0; i < COLS; i++) {
for (let j = 0; j < ROWS; j++) {
const px = (i + 0.5) * stepX, py = (j + 0.5) * stepY;
const [wx, wy] = fromCanvas(px, py);
const [dvx, dvy] = f(wx, wy);
const mag = Math.sqrt(dvx * dvx + dvy * dvy) + 1e-9;
const scale = Math.min(stepX, stepY) * 0.38 / Math.sqrt(mag + 0.3);
const ex = px + dvx * scale, ey = py - dvy * scale;
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(ex, ey);
// small arrowhead lines
const angle = Math.atan2(ey - py, ex - px);
ctx.lineTo(ex - 5 * Math.cos(angle - 0.4), ey - 5 * Math.sin(angle - 0.4));
ctx.moveTo(ex, ey);
ctx.lineTo(ex - 5 * Math.cos(angle + 0.4), ey - 5 * Math.sin(angle + 0.4));
ctx.stroke();
}
}
}
// Draw an integrated trajectory path on the canvas
function drawPath(path, color, lineWidth) {
if (path.length < 2) return;
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(toCanvasX(path[0][0]), toCanvasY(path[0][1]));
for (let i = 1; i < path.length; i++) {
ctx.lineTo(toCanvasX(path[i][0]), toCanvasY(path[i][1]));
}
ctx.stroke();
}
// Draw a filled circle (starting point marker)
function drawDot(x, y, color, r) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(toCanvasX(x), toCanvasY(y), r, 0, 2 * Math.PI);
ctx.fill();
}
let startPt = null;
function getField() {
return FIELDS[document.getElementById('field-select').value];
}
function getMethod() {
return document.getElementById('method-select').value;
}
function redraw() {
ctx.clearRect(0, 0, W, H);
const f = getField();
drawArrows(f);
if (!startPt) return;
const [x0, y0] = startPt;
const T = 4, DT_EULER = 0.05, DT_RK4 = 0.1;
const method = getMethod();
if (method === 'euler') {
// single Euler path in orange
const path = eulerSteps(f, x0, y0, T, DT_EULER);
drawPath(path, '#e07b39', 2.5);
// report how many Euler steps were taken
const steps = path.length - 1;
document.getElementById('info').textContent =
fmt('Euler: %steps% steps to integrate T=4.', { steps: steps });
} else {
// show Euler (faded) and RK4 (bright) side by side
const pathE = eulerSteps(f, x0, y0, T, DT_EULER);
const pathR = rk4Steps(f, x0, y0, T, DT_RK4);
drawPath(pathE, '#e0a039', 1.5);
drawPath(pathR, '#c8a000', 2.5);
const stepsE = pathE.length - 1;
const stepsR = pathR.length - 1;
document.getElementById('info').textContent =
fmt('RK4: %stepsR% steps (gold) vs Euler: %stepsE% steps (faded). Same field, better accuracy per step.', { stepsR: stepsR, stepsE: stepsE });
}
// red dot marks the starting point
drawDot(x0, y0, '#e63946', 5);
}
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const px = (e.clientX - rect.left) * (W / rect.width);
const py = (e.clientY - rect.top) * (H / rect.height);
startPt = fromCanvas(px, py);
redraw();
});
document.getElementById('field-select').addEventListener('change', redraw);
document.getElementById('method-select').addEventListener('change', redraw);
document.getElementById('btn-reset').addEventListener('click', () => {
startPt = null;
redraw();
document.getElementById('info').textContent = 'Click anywhere on the canvas to place a starting point.';
});
// Draw the initial field on load (no trajectory yet)
redraw();
Comments
Loading comments...