Below is a small plane with two fixed polynomials, f(x,y)=x2+y2−4 (a circle) and g(x,y)=y−x2 (a parabola). Press Decompose and watch the grid get shaded by the sign pattern of (f,g) at each point: four colors for the four sign combinations, plus thin curves where a polynomial is exactly zero.
<p class="hint">Two fixed polynomials live on this plane: <b>f(x,y) = x² + y² - 4</b> (a circle) and <b>g(x,y) = y - x²</b> (a parabola). Press <b>Decompose</b> to shade every point by the sign pattern of (f, g).</p>
<div class="stage">
<canvas id="board" width="360" height="360"></canvas>
<div class="legend">
<div class="row"><span class="swatch pp"></span>f > 0, g > 0</div>
<div class="row"><span class="swatch pm"></span>f > 0, g < 0</div>
<div class="row"><span class="swatch mp"></span>f < 0, g > 0</div>
<div class="row"><span class="swatch mm"></span>f < 0, g < 0</div>
<div class="row"><span class="swatch curve"></span>f = 0 or g = 0 (cell boundary)</div>
</div>
</div>
<div class="status" id="status">Press Decompose to build the sign-invariant map.</div>
<div class="btns">
<button id="decompose" type="button">Decompose</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.stage { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; }
canvas { border-radius: 8px; border: 1px solid #cdd9e3; cursor: crosshair; background: #fff; }
.legend { display: flex; flex-direction: column; gap: .35rem; font-size: .82rem; min-width: 170px; }
.row { display: flex; align-items: center; gap: .5rem; }
.swatch { width: 16px; height: 16px; border-radius: 4px; display: inline-block; border: 1px solid rgba(0,0,0,.15); }
.swatch.pp { background: #cdeccd; }
.swatch.pm { background: #f6d6a6; }
.swatch.mp { background: #a9d3f2; }
.swatch.mm { background: #e3b8d6; }
.swatch.curve { background: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Cylindrical algebraic decomposition demo: shade the plane by the sign pattern of two fixed polynomials.
// f(x,y) = x^2 + y^2 - 4 (circle of radius 2), g(x,y) = y - x^2 (parabola).
const W = 360, H = 360, N = 90;
const RANGE = 3.2; // half-width of the world-coordinate window shown on screen
// Convert a pixel coordinate on the canvas into world (x,y) coordinates.
function toWorld(px, py) {
return [ (px / W) * (2 * RANGE) - RANGE, RANGE - (py / H) * (2 * RANGE) ];
}
// f(x,y): negative inside the circle of radius 2, positive outside, zero on it.
function f(x, y) { return x * x + y * y - 4; }
// g(x,y): negative below the parabola y = x^2, positive above, zero on it.
function g(x, y) { return y - x * x; }
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
let decomposed = false;
// One color per sign combination of (f, g); curves (sign = 0) get their own color.
const COLORS = { '1,1': '#cdeccd', '1,-1': '#f6d6a6', '-1,1': '#a9d3f2', '-1,-1': '#e3b8d6' };
const CURVE = '#1d3557';
const EPS = 0.05;
function sign(v) { return Math.abs(v) < EPS ? 0 : (v > 0 ? 1 : -1); }
function drawBlank() {
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = '#e3e8ee';
for (let i = 0; i <= 8; i++) {
const p = (i / 8) * W;
ctx.beginPath(); ctx.moveTo(p, 0); ctx.lineTo(p, H); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, p); ctx.lineTo(W, p); ctx.stroke();
}
}
// Scan a fine grid and paint each sample by its sign pattern — approximates the true CAD cells.
function decompose() {
const cell = W / N;
for (let i = 0; i < N; i++) {
for (let j = 0; j < N; j++) {
const px = (i + 0.5) * cell, py = (j + 0.5) * cell;
const [x, y] = toWorld(px, py);
const sf = sign(f(x, y)), sg = sign(g(x, y));
if (sf === 0 || sg === 0) {
ctx.fillStyle = CURVE;
} else {
ctx.fillStyle = COLORS[sf + ',' + sg];
}
ctx.fillRect(i * cell, j * cell, cell + 0.6, cell + 0.6);
}
}
decomposed = true;
setStatus('Decomposed. Click any cell to inspect its sign pattern.');
}
function setStatus(t) { document.getElementById('status').textContent = t; }
canvas.addEventListener('click', (ev) => {
if (!decomposed) return;
const rect = canvas.getBoundingClientRect();
const px = (ev.clientX - rect.left) * (W / rect.width);
const py = (ev.clientY - rect.top) * (H / rect.height);
const [x, y] = toWorld(px, py);
const sf = sign(f(x, y)), sg = sign(g(x, y));
// Report the sign pattern of the polynomials at the clicked point.
setStatus(i18nFmt('At (%x%, %y%): sign(f) = %fs%, sign(g) = %gs% — constant throughout this cell.', { fs: sf, gs: sg, x: x.toFixed(2), y: y.toFixed(2) }));
console.log('Sign pattern at clicked point:', 'x=', x.toFixed(2), 'y=', y.toFixed(2), 'sign(f)=', sf, 'sign(g)=', sg);
});
document.getElementById('decompose').addEventListener('click', decompose);
document.getElementById('reset').addEventListener('click', () => {
decomposed = false;
drawBlank();
setStatus('Press Decompose to build the sign-invariant map.');
});
drawBlank();
<p class="hint">Two fixed polynomials live on this plane: <b>f(x,y) = x² + y² - 4</b> (a circle) and <b>g(x,y) = y - x²</b> (a parabola). Press <b>Decompose</b> to shade every point by the sign pattern of (f, g).</p>
<div class="stage">
<canvas id="board" width="360" height="360"></canvas>
<div class="legend">
<div class="row"><span class="swatch pp"></span>f > 0, g > 0</div>
<div class="row"><span class="swatch pm"></span>f > 0, g < 0</div>
<div class="row"><span class="swatch mp"></span>f < 0, g > 0</div>
<div class="row"><span class="swatch mm"></span>f < 0, g < 0</div>
<div class="row"><span class="swatch curve"></span>f = 0 or g = 0 (cell boundary)</div>
</div>
</div>
<div class="status" id="status">Press Decompose to build the sign-invariant map.</div>
<div class="btns">
<button id="decompose" type="button">Decompose</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.stage { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; }
canvas { border-radius: 8px; border: 1px solid #cdd9e3; cursor: crosshair; background: #fff; }
.legend { display: flex; flex-direction: column; gap: .35rem; font-size: .82rem; min-width: 170px; }
.row { display: flex; align-items: center; gap: .5rem; }
.swatch { width: 16px; height: 16px; border-radius: 4px; display: inline-block; border: 1px solid rgba(0,0,0,.15); }
.swatch.pp { background: #cdeccd; }
.swatch.pm { background: #f6d6a6; }
.swatch.mp { background: #a9d3f2; }
.swatch.mm { background: #e3b8d6; }
.swatch.curve { background: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Cylindrical algebraic decomposition demo: shade the plane by the sign pattern of two fixed polynomials.
// f(x,y) = x^2 + y^2 - 4 (circle of radius 2), g(x,y) = y - x^2 (parabola).
const W = 360, H = 360, N = 90;
const RANGE = 3.2; // half-width of the world-coordinate window shown on screen
// Convert a pixel coordinate on the canvas into world (x,y) coordinates.
function toWorld(px, py) {
return [ (px / W) * (2 * RANGE) - RANGE, RANGE - (py / H) * (2 * RANGE) ];
}
// f(x,y): negative inside the circle of radius 2, positive outside, zero on it.
function f(x, y) { return x * x + y * y - 4; }
// g(x,y): negative below the parabola y = x^2, positive above, zero on it.
function g(x, y) { return y - x * x; }
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
let decomposed = false;
// One color per sign combination of (f, g); curves (sign = 0) get their own color.
const COLORS = { '1,1': '#cdeccd', '1,-1': '#f6d6a6', '-1,1': '#a9d3f2', '-1,-1': '#e3b8d6' };
const CURVE = '#1d3557';
const EPS = 0.05;
function sign(v) { return Math.abs(v) < EPS ? 0 : (v > 0 ? 1 : -1); }
function drawBlank() {
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = '#e3e8ee';
for (let i = 0; i <= 8; i++) {
const p = (i / 8) * W;
ctx.beginPath(); ctx.moveTo(p, 0); ctx.lineTo(p, H); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, p); ctx.lineTo(W, p); ctx.stroke();
}
}
// Scan a fine grid and paint each sample by its sign pattern — approximates the true CAD cells.
function decompose() {
const cell = W / N;
for (let i = 0; i < N; i++) {
for (let j = 0; j < N; j++) {
const px = (i + 0.5) * cell, py = (j + 0.5) * cell;
const [x, y] = toWorld(px, py);
const sf = sign(f(x, y)), sg = sign(g(x, y));
if (sf === 0 || sg === 0) {
ctx.fillStyle = CURVE;
} else {
ctx.fillStyle = COLORS[sf + ',' + sg];
}
ctx.fillRect(i * cell, j * cell, cell + 0.6, cell + 0.6);
}
}
decomposed = true;
setStatus('Decomposed. Click any cell to inspect its sign pattern.');
}
function setStatus(t) { document.getElementById('status').textContent = t; }
canvas.addEventListener('click', (ev) => {
if (!decomposed) return;
const rect = canvas.getBoundingClientRect();
const px = (ev.clientX - rect.left) * (W / rect.width);
const py = (ev.clientY - rect.top) * (H / rect.height);
const [x, y] = toWorld(px, py);
const sf = sign(f(x, y)), sg = sign(g(x, y));
// Report the sign pattern of the polynomials at the clicked point.
setStatus(i18nFmt('At (%x%, %y%): sign(f) = %fs%, sign(g) = %gs% — constant throughout this cell.', { fs: sf, gs: sg, x: x.toFixed(2), y: y.toFixed(2) }));
console.log('Sign pattern at clicked point:', 'x=', x.toFixed(2), 'y=', y.toFixed(2), 'sign(f)=', sf, 'sign(g)=', sg);
});
document.getElementById('decompose').addEventListener('click', decompose);
document.getElementById('reset').addEventListener('click', () => {
decomposed = false;
drawBlank();
setStatus('Press Decompose to build the sign-invariant map.');
});
drawBlank();
Click any cell to see its sign pattern printed out. Notice that every point inside one shaded region gives the exact same pattern — that constancy is the entire point of CAD. A real algorithm would find the curves first (algebraically, not by scanning pixels) and then describe each resulting cell exactly; our demo scans a grid to make the same regions visible.
Comments
Loading comments...