The canvas below shows a background gradient (simulating a real photo) and a circular patch from a different "source" image (a brighter radial gradient). Press Hard paste to copy the patch with no blending — the hard ring is the seam. Press Poisson blend to solve the discrete Poisson equation over the patch pixels: the interior colors shift so that gradients match the source while boundary values match the destination.
<!-- Canvas + controls for the Poisson blending demo -->
<p class="hint">A background gradient simulates a target photo. The warm circular patch is from a different "source" image. <b>Hard paste</b> copies it directly — see the seam. <b>Poisson blend</b> solves a sparse linear system to match source gradients inside while meeting destination colors on the boundary.</p>
<canvas id="canvas" width="340" height="260" title="Poisson image editing demo canvas"></canvas>
<div class="status" id="status">Choose an action to begin.</div>
<div class="btns">
<button id="btn-hard" type="button">Hard paste</button>
<button id="btn-poisson" type="button">Poisson blend</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Styles for the Poisson demo */
* { 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; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; max-width: 100%; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { 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] ?? ''));
// Poisson Image Editing demo — compare hard copy-paste with seamless Poisson blending.
// Canvas dimensions and patch center/radius.
const CW = 340, CH = 260;
const CX = 170, CY = 130, CR = 55; // patch center (CX,CY) and radius CR
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status');
function setStatus(t, cls) { statusEl.textContent = t; statusEl.className = 'status ' + cls; }
// Background color at pixel (x,y) — a smooth blue-green gradient simulating a real photo.
function bgColor(x, y) {
const r = Math.round(40 + (x / CW) * 120);
const g = Math.round(60 + (y / CH) * 100);
const b = Math.round(180 - (x / CW) * 60);
return [r, g, b];
}
// Source patch color — a warm radial gradient brighter near the center.
function srcColor(x, y) {
const dx = x - CX, dy = y - CY;
const dist = Math.sqrt(dx * dx + dy * dy);
const t = Math.max(0, 1 - dist / CR);
const r = Math.round(220 + t * 30);
const g = Math.round(180 + t * 50);
const b = Math.round(80 + t * 40);
return [r, g, b];
}
// Render the background into an ImageData object.
function buildBackground() {
const img = ctx.createImageData(CW, CH);
for (let y = 0; y < CH; y++) {
for (let x = 0; x < CW; x++) {
const [r, g, b] = bgColor(x, y);
const i = (y * CW + x) * 4;
img.data[i] = r; img.data[i+1] = g; img.data[i+2] = b; img.data[i+3] = 255;
}
}
return img;
}
// Returns true if pixel (x,y) is inside the circular patch.
function inPatch(x, y) {
const dx = x - CX, dy = y - CY;
return dx * dx + dy * dy <= CR * CR;
}
// Hard paste — copy source colors directly into the patch, creates a visible seam.
function hardPaste() {
const img = buildBackground();
for (let y = 0; y < CH; y++) {
for (let x = 0; x < CW; x++) {
if (inPatch(x, y)) {
const [r, g, b] = srcColor(x, y);
const i = (y * CW + x) * 4;
img.data[i] = r; img.data[i+1] = g; img.data[i+2] = b;
}
}
}
ctx.putImageData(img, 0, 0);
setStatus('Hard paste done — see the visible seam ring.', 'bad');
drawSeamHint(true);
}
// Poisson blend using Gauss-Seidel iteration.
// Each interior pixel satisfies: Laplacian(f) = div(gradient of source).
function poissonBlend() {
setStatus('Solving Poisson system…', 'info');
// Collect interior patch pixels and build an index map.
const pixels = [];
const pixIdx = {};
for (let y = 1; y < CH - 1; y++) {
for (let x = 1; x < CW - 1; x++) {
if (inPatch(x, y)) {
pixIdx[y * CW + x] = pixels.length;
pixels.push([x, y]);
}
}
}
const N = pixels.length;
const DIRS = [[0,-1],[0,1],[-1,0],[1,0]]; // 4-neighborhood directions
// Solve independently for each color channel (R, G, B).
const result = new Array(N * 3).fill(0);
for (let ch = 0; ch < 3; ch++) {
// Gauss-Seidel iteration for the discrete Poisson equation.
const f = new Float32Array(N);
// Initialize f with destination (background) colors.
for (let i = 0; i < N; i++) {
const [x, y] = pixels[i];
const bg = bgColor(x, y);
f[i] = bg[ch];
}
// Compute right-hand side: Laplacian of the source image.
const rhs = new Float32Array(N);
for (let i = 0; i < N; i++) {
const [x, y] = pixels[i];
let lap = 0;
for (const [dx, dy] of DIRS) {
const nx = x + dx, ny = y + dy;
const src_p = srcColor(x, y)[ch];
const src_n = srcColor(nx, ny)[ch];
lap += src_n - src_p; // accumulate source Laplacian
}
rhs[i] = lap;
}
// Iterate until convergence (120 passes is sufficient for this patch size).
for (let iter = 0; iter < 120; iter++) {
for (let i = 0; i < N; i++) {
const [x, y] = pixels[i];
let sum = rhs[i];
let cnt = 0;
for (const [dx, dy] of DIRS) {
const nx = x + dx, ny = y + dy;
const ni = pixIdx[ny * CW + nx];
if (ni !== undefined) {
sum += f[ni]; cnt++;
} else {
// Boundary pixel — clamp to destination color.
const bg = bgColor(nx, ny);
sum += bg[ch]; cnt++;
}
}
f[i] = cnt > 0 ? sum / cnt : f[i];
}
}
for (let i = 0; i < N; i++) result[i * 3 + ch] = Math.max(0, Math.min(255, Math.round(f[i])));
}
// Write solved pixel values back to the ImageData.
const img = buildBackground();
for (let i = 0; i < N; i++) {
const [x, y] = pixels[i];
const idx = (y * CW + x) * 4;
img.data[idx] = result[i * 3];
img.data[idx+1] = result[i * 3 + 1];
img.data[idx+2] = result[i * 3 + 2];
}
ctx.putImageData(img, 0, 0);
setStatus(i18nFmt('Poisson blend done — %n% pixels solved, seam invisible.', { n: N }), 'ok');
drawSeamHint(false);
}
// Draw a circle to highlight (red) or confirm (green) the seam.
function drawSeamHint(visible) {
ctx.save();
ctx.beginPath();
ctx.arc(CX, CY, CR, 0, 2 * Math.PI);
ctx.strokeStyle = visible ? 'rgba(220,50,50,0.7)' : 'rgba(0,180,80,0.5)';
ctx.lineWidth = visible ? 2.5 : 1.5;
ctx.setLineDash(visible ? [] : [5, 4]);
ctx.stroke();
ctx.restore();
}
function reset() {
const img = buildBackground();
ctx.putImageData(img, 0, 0);
setStatus('Choose an action to begin.', '');
}
document.getElementById('btn-hard').addEventListener('click', hardPaste);
document.getElementById('btn-poisson').addEventListener('click', poissonBlend);
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
<!-- Canvas + controls for the Poisson blending demo -->
<p class="hint">A background gradient simulates a target photo. The warm circular patch is from a different "source" image. <b>Hard paste</b> copies it directly — see the seam. <b>Poisson blend</b> solves a sparse linear system to match source gradients inside while meeting destination colors on the boundary.</p>
<canvas id="canvas" width="340" height="260" title="Poisson image editing demo canvas"></canvas>
<div class="status" id="status">Choose an action to begin.</div>
<div class="btns">
<button id="btn-hard" type="button">Hard paste</button>
<button id="btn-poisson" type="button">Poisson blend</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Styles for the Poisson demo */
* { 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; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; max-width: 100%; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { 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] ?? ''));
// Poisson Image Editing demo — compare hard copy-paste with seamless Poisson blending.
// Canvas dimensions and patch center/radius.
const CW = 340, CH = 260;
const CX = 170, CY = 130, CR = 55; // patch center (CX,CY) and radius CR
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status');
function setStatus(t, cls) { statusEl.textContent = t; statusEl.className = 'status ' + cls; }
// Background color at pixel (x,y) — a smooth blue-green gradient simulating a real photo.
function bgColor(x, y) {
const r = Math.round(40 + (x / CW) * 120);
const g = Math.round(60 + (y / CH) * 100);
const b = Math.round(180 - (x / CW) * 60);
return [r, g, b];
}
// Source patch color — a warm radial gradient brighter near the center.
function srcColor(x, y) {
const dx = x - CX, dy = y - CY;
const dist = Math.sqrt(dx * dx + dy * dy);
const t = Math.max(0, 1 - dist / CR);
const r = Math.round(220 + t * 30);
const g = Math.round(180 + t * 50);
const b = Math.round(80 + t * 40);
return [r, g, b];
}
// Render the background into an ImageData object.
function buildBackground() {
const img = ctx.createImageData(CW, CH);
for (let y = 0; y < CH; y++) {
for (let x = 0; x < CW; x++) {
const [r, g, b] = bgColor(x, y);
const i = (y * CW + x) * 4;
img.data[i] = r; img.data[i+1] = g; img.data[i+2] = b; img.data[i+3] = 255;
}
}
return img;
}
// Returns true if pixel (x,y) is inside the circular patch.
function inPatch(x, y) {
const dx = x - CX, dy = y - CY;
return dx * dx + dy * dy <= CR * CR;
}
// Hard paste — copy source colors directly into the patch, creates a visible seam.
function hardPaste() {
const img = buildBackground();
for (let y = 0; y < CH; y++) {
for (let x = 0; x < CW; x++) {
if (inPatch(x, y)) {
const [r, g, b] = srcColor(x, y);
const i = (y * CW + x) * 4;
img.data[i] = r; img.data[i+1] = g; img.data[i+2] = b;
}
}
}
ctx.putImageData(img, 0, 0);
setStatus('Hard paste done — see the visible seam ring.', 'bad');
drawSeamHint(true);
}
// Poisson blend using Gauss-Seidel iteration.
// Each interior pixel satisfies: Laplacian(f) = div(gradient of source).
function poissonBlend() {
setStatus('Solving Poisson system…', 'info');
// Collect interior patch pixels and build an index map.
const pixels = [];
const pixIdx = {};
for (let y = 1; y < CH - 1; y++) {
for (let x = 1; x < CW - 1; x++) {
if (inPatch(x, y)) {
pixIdx[y * CW + x] = pixels.length;
pixels.push([x, y]);
}
}
}
const N = pixels.length;
const DIRS = [[0,-1],[0,1],[-1,0],[1,0]]; // 4-neighborhood directions
// Solve independently for each color channel (R, G, B).
const result = new Array(N * 3).fill(0);
for (let ch = 0; ch < 3; ch++) {
// Gauss-Seidel iteration for the discrete Poisson equation.
const f = new Float32Array(N);
// Initialize f with destination (background) colors.
for (let i = 0; i < N; i++) {
const [x, y] = pixels[i];
const bg = bgColor(x, y);
f[i] = bg[ch];
}
// Compute right-hand side: Laplacian of the source image.
const rhs = new Float32Array(N);
for (let i = 0; i < N; i++) {
const [x, y] = pixels[i];
let lap = 0;
for (const [dx, dy] of DIRS) {
const nx = x + dx, ny = y + dy;
const src_p = srcColor(x, y)[ch];
const src_n = srcColor(nx, ny)[ch];
lap += src_n - src_p; // accumulate source Laplacian
}
rhs[i] = lap;
}
// Iterate until convergence (120 passes is sufficient for this patch size).
for (let iter = 0; iter < 120; iter++) {
for (let i = 0; i < N; i++) {
const [x, y] = pixels[i];
let sum = rhs[i];
let cnt = 0;
for (const [dx, dy] of DIRS) {
const nx = x + dx, ny = y + dy;
const ni = pixIdx[ny * CW + nx];
if (ni !== undefined) {
sum += f[ni]; cnt++;
} else {
// Boundary pixel — clamp to destination color.
const bg = bgColor(nx, ny);
sum += bg[ch]; cnt++;
}
}
f[i] = cnt > 0 ? sum / cnt : f[i];
}
}
for (let i = 0; i < N; i++) result[i * 3 + ch] = Math.max(0, Math.min(255, Math.round(f[i])));
}
// Write solved pixel values back to the ImageData.
const img = buildBackground();
for (let i = 0; i < N; i++) {
const [x, y] = pixels[i];
const idx = (y * CW + x) * 4;
img.data[idx] = result[i * 3];
img.data[idx+1] = result[i * 3 + 1];
img.data[idx+2] = result[i * 3 + 2];
}
ctx.putImageData(img, 0, 0);
setStatus(i18nFmt('Poisson blend done — %n% pixels solved, seam invisible.', { n: N }), 'ok');
drawSeamHint(false);
}
// Draw a circle to highlight (red) or confirm (green) the seam.
function drawSeamHint(visible) {
ctx.save();
ctx.beginPath();
ctx.arc(CX, CY, CR, 0, 2 * Math.PI);
ctx.strokeStyle = visible ? 'rgba(220,50,50,0.7)' : 'rgba(0,180,80,0.5)';
ctx.lineWidth = visible ? 2.5 : 1.5;
ctx.setLineDash(visible ? [] : [5, 4]);
ctx.stroke();
ctx.restore();
}
function reset() {
const img = buildBackground();
ctx.putImageData(img, 0, 0);
setStatus('Choose an action to begin.', '');
}
document.getElementById('btn-hard').addEventListener('click', hardPaste);
document.getElementById('btn-poisson').addEventListener('click', poissonBlend);
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
Notice that Poisson blending does not simply average the two images. The interior can end up darker or lighter than either source or destination, because the solver is imposing gradient constraints, not color averages. The seam disappears because the rate of change is continuous, even though the colors are not copied literally.
Comments
Loading comments...