The triangle below has a red vertex, a green vertex, and a blue vertex. Click or drag anywhere inside to place a point. The three barycentric weights λ1â, λ2â, λ3â are computed and used to mix the vertex colors â giving the point exactly the blend it deserves.
<!-- Barycentric coordinate color-blend demo -->
<div class="hint" id="hint">Click or drag inside the triangle. The three barycentric weights (λâ, λâ, λâ) determine the point's color as a blend of vertex A (red), B (green), and C (blue).</div>
<div class="canvas-wrap">
<canvas id="cvs" width="360" height="300"></canvas>
</div>
<div class="coords" id="coords">Click inside the triangle to place a point.</div>
<div class="weights" id="weights"></div>
<button id="reset" type="button" class="ghost">Reset</button>
/* Layout and visual styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.canvas-wrap { display: flex; justify-content: center; }
canvas { border-radius: 10px; cursor: crosshair; touch-action: none; display: block; max-width: 100%; }
.coords { font-size: .9rem; color: #555; margin: .5rem 0 .2rem; min-height: 1.3em; }
.weights { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; min-height: 1.6em; }
.w-chip { font-size: .85rem; font-weight: 600; padding: .2rem .55rem; border-radius: 999px; color: #fff; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// Barycentric coordinates: any interior point P = λ1·A + λ2·B + λ3·C
// where λ1 + λ2 + λ3 = 1 and all λi >= 0.
// The weights are computed via signed triangle areas (2D cross products).
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const cvs = document.getElementById('cvs');
const ctx = cvs.getContext('2d');
const W = cvs.width, H = cvs.height;
// Triangle vertices with RGB colors (red, green, blue corners)
const V = [
{ x: W * 0.5, y: H * 0.06, r: 230, g: 57, b: 70 }, // red vertex (top)
{ x: W * 0.05, y: H * 0.93, r: 29, g: 185, b: 84 }, // green vertex (bottom-left)
{ x: W * 0.95, y: H * 0.93, r: 69, g: 123, b: 210 } // blue vertex (bottom-right)
];
// 2D cross product: area of parallelogram spanned by (a) and (b)
function cross2d(ax, ay, bx, by) { return ax * by - ay * bx; }
// Compute barycentric coordinates for point (px, py)
function bary(px, py) {
const [A, B, C] = V;
const denom = cross2d(B.x - A.x, B.y - A.y, C.x - A.x, C.y - A.y);
const l1 = cross2d(B.x - px, B.y - py, C.x - px, C.y - py) / denom;
const l2 = cross2d(C.x - px, C.y - py, A.x - px, A.y - py) / denom;
const l3 = 1 - l1 - l2;
return [l1, l2, l3];
}
// Point is inside the triangle iff all weights are non-negative
function inside(l1, l2, l3) { return l1 >= 0 && l2 >= 0 && l3 >= 0; }
// Fill the triangle pixel-by-pixel using barycentric interpolation
function drawTriangle() {
// For each pixel, compute barycentric coords and blend vertex colors
const imgData = ctx.createImageData(W, H);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const [l1, l2, l3] = bary(x + 0.5, y + 0.5);
if (!inside(l1, l2, l3)) continue;
const i = (y * W + x) * 4;
imgData.data[i] = Math.round(l1 * V[0].r + l2 * V[1].r + l3 * V[2].r);
imgData.data[i + 1] = Math.round(l1 * V[0].g + l2 * V[1].g + l3 * V[2].g);
imgData.data[i + 2] = Math.round(l1 * V[0].b + l2 * V[1].b + l3 * V[2].b);
imgData.data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
// Draw labeled vertex dots on top
V.forEach((v, i) => {
ctx.beginPath();
ctx.arc(v.x, v.y, 8, 0, Math.PI * 2);
ctx.fillStyle = `rgb(${v.r},${v.g},${v.b})`;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
const label = ['A', 'B', 'C'][i];
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label, v.x, v.y);
});
}
let dot = null; // last clicked/dragged point
// Draw the user's chosen point
function drawDot() {
if (!dot) return;
ctx.beginPath();
ctx.arc(dot.x, dot.y, 6, 0, Math.PI * 2);
ctx.fillStyle = dot.color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
function render() {
ctx.clearRect(0, 0, W, H);
drawTriangle();
drawDot();
}
// Compute and display weights for the chosen point
function update(px, py) {
const [l1, l2, l3] = bary(px, py);
if (!inside(l1, l2, l3)) return;
const r = Math.round(l1 * V[0].r + l2 * V[1].r + l3 * V[2].r);
const g = Math.round(l1 * V[0].g + l2 * V[1].g + l3 * V[2].g);
const b = Math.round(l1 * V[0].b + l2 * V[1].b + l3 * V[2].b);
const color = `rgb(${r},${g},${b})`;
dot = { x: px, y: py, color };
render();
document.getElementById('coords').textContent =
fmt('Point at (%x%, %y%)', { x: Math.round(px), y: Math.round(py) });
const labels = ['λâ (A)', 'λâ (B)', 'λâ (C)'];
const vals = [l1, l2, l3];
const chips = ['#e63946', '#1db954', '#457bd9'];
document.getElementById('weights').innerHTML = vals.map((v, i) =>
`<span class="w-chip" style="background:${chips[i]}">${labels[i]} = ${v.toFixed(3)}</span>`
).join('');
}
// Mouse and touch event handlers
function getPos(e) {
const rect = cvs.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const src = e.touches ? e.touches[0] : e;
return { x: (src.clientX - rect.left) * scaleX, y: (src.clientY - rect.top) * scaleY };
}
let dragging = false;
cvs.addEventListener('mousedown', e => { dragging = true; const p = getPos(e); update(p.x, p.y); });
cvs.addEventListener('mousemove', e => { if (dragging) { const p = getPos(e); update(p.x, p.y); } });
cvs.addEventListener('mouseup', () => { dragging = false; });
cvs.addEventListener('touchstart', e => { e.preventDefault(); const p = getPos(e); update(p.x, p.y); }, { passive: false });
cvs.addEventListener('touchmove', e => { e.preventDefault(); const p = getPos(e); update(p.x, p.y); }, { passive: false });
document.getElementById('reset').addEventListener('click', () => {
dot = null;
render();
document.getElementById('coords').textContent = 'Click inside the triangle to place a point.';
document.getElementById('weights').innerHTML = '';
});
render();
<!-- Barycentric coordinate color-blend demo -->
<div class="hint" id="hint">Click or drag inside the triangle. The three barycentric weights (λâ, λâ, λâ) determine the point's color as a blend of vertex A (red), B (green), and C (blue).</div>
<div class="canvas-wrap">
<canvas id="cvs" width="360" height="300"></canvas>
</div>
<div class="coords" id="coords">Click inside the triangle to place a point.</div>
<div class="weights" id="weights"></div>
<button id="reset" type="button" class="ghost">Reset</button>
/* Layout and visual styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.canvas-wrap { display: flex; justify-content: center; }
canvas { border-radius: 10px; cursor: crosshair; touch-action: none; display: block; max-width: 100%; }
.coords { font-size: .9rem; color: #555; margin: .5rem 0 .2rem; min-height: 1.3em; }
.weights { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; min-height: 1.6em; }
.w-chip { font-size: .85rem; font-weight: 600; padding: .2rem .55rem; border-radius: 999px; color: #fff; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// Barycentric coordinates: any interior point P = λ1·A + λ2·B + λ3·C
// where λ1 + λ2 + λ3 = 1 and all λi >= 0.
// The weights are computed via signed triangle areas (2D cross products).
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const cvs = document.getElementById('cvs');
const ctx = cvs.getContext('2d');
const W = cvs.width, H = cvs.height;
// Triangle vertices with RGB colors (red, green, blue corners)
const V = [
{ x: W * 0.5, y: H * 0.06, r: 230, g: 57, b: 70 }, // red vertex (top)
{ x: W * 0.05, y: H * 0.93, r: 29, g: 185, b: 84 }, // green vertex (bottom-left)
{ x: W * 0.95, y: H * 0.93, r: 69, g: 123, b: 210 } // blue vertex (bottom-right)
];
// 2D cross product: area of parallelogram spanned by (a) and (b)
function cross2d(ax, ay, bx, by) { return ax * by - ay * bx; }
// Compute barycentric coordinates for point (px, py)
function bary(px, py) {
const [A, B, C] = V;
const denom = cross2d(B.x - A.x, B.y - A.y, C.x - A.x, C.y - A.y);
const l1 = cross2d(B.x - px, B.y - py, C.x - px, C.y - py) / denom;
const l2 = cross2d(C.x - px, C.y - py, A.x - px, A.y - py) / denom;
const l3 = 1 - l1 - l2;
return [l1, l2, l3];
}
// Point is inside the triangle iff all weights are non-negative
function inside(l1, l2, l3) { return l1 >= 0 && l2 >= 0 && l3 >= 0; }
// Fill the triangle pixel-by-pixel using barycentric interpolation
function drawTriangle() {
// For each pixel, compute barycentric coords and blend vertex colors
const imgData = ctx.createImageData(W, H);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const [l1, l2, l3] = bary(x + 0.5, y + 0.5);
if (!inside(l1, l2, l3)) continue;
const i = (y * W + x) * 4;
imgData.data[i] = Math.round(l1 * V[0].r + l2 * V[1].r + l3 * V[2].r);
imgData.data[i + 1] = Math.round(l1 * V[0].g + l2 * V[1].g + l3 * V[2].g);
imgData.data[i + 2] = Math.round(l1 * V[0].b + l2 * V[1].b + l3 * V[2].b);
imgData.data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
// Draw labeled vertex dots on top
V.forEach((v, i) => {
ctx.beginPath();
ctx.arc(v.x, v.y, 8, 0, Math.PI * 2);
ctx.fillStyle = `rgb(${v.r},${v.g},${v.b})`;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
const label = ['A', 'B', 'C'][i];
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label, v.x, v.y);
});
}
let dot = null; // last clicked/dragged point
// Draw the user's chosen point
function drawDot() {
if (!dot) return;
ctx.beginPath();
ctx.arc(dot.x, dot.y, 6, 0, Math.PI * 2);
ctx.fillStyle = dot.color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
function render() {
ctx.clearRect(0, 0, W, H);
drawTriangle();
drawDot();
}
// Compute and display weights for the chosen point
function update(px, py) {
const [l1, l2, l3] = bary(px, py);
if (!inside(l1, l2, l3)) return;
const r = Math.round(l1 * V[0].r + l2 * V[1].r + l3 * V[2].r);
const g = Math.round(l1 * V[0].g + l2 * V[1].g + l3 * V[2].g);
const b = Math.round(l1 * V[0].b + l2 * V[1].b + l3 * V[2].b);
const color = `rgb(${r},${g},${b})`;
dot = { x: px, y: py, color };
render();
document.getElementById('coords').textContent =
fmt('Point at (%x%, %y%)', { x: Math.round(px), y: Math.round(py) });
const labels = ['λâ (A)', 'λâ (B)', 'λâ (C)'];
const vals = [l1, l2, l3];
const chips = ['#e63946', '#1db954', '#457bd9'];
document.getElementById('weights').innerHTML = vals.map((v, i) =>
`<span class="w-chip" style="background:${chips[i]}">${labels[i]} = ${v.toFixed(3)}</span>`
).join('');
}
// Mouse and touch event handlers
function getPos(e) {
const rect = cvs.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const src = e.touches ? e.touches[0] : e;
return { x: (src.clientX - rect.left) * scaleX, y: (src.clientY - rect.top) * scaleY };
}
let dragging = false;
cvs.addEventListener('mousedown', e => { dragging = true; const p = getPos(e); update(p.x, p.y); });
cvs.addEventListener('mousemove', e => { if (dragging) { const p = getPos(e); update(p.x, p.y); } });
cvs.addEventListener('mouseup', () => { dragging = false; });
cvs.addEventListener('touchstart', e => { e.preventDefault(); const p = getPos(e); update(p.x, p.y); }, { passive: false });
cvs.addEventListener('touchmove', e => { e.preventDefault(); const p = getPos(e); update(p.x, p.y); }, { passive: false });
document.getElementById('reset').addEventListener('click', () => {
dot = null;
render();
document.getElementById('coords').textContent = 'Click inside the triangle to place a point.';
document.getElementById('weights').innerHTML = '';
});
render();
Notice that points near a corner look almost like that corner's color. Points near the center mix all three equally. The weights always sum to 1, so the blend is always a proper convex combination â it can never produce a color outside the triangle's palette.
Comments
Loading comments...