3 Porträts entlang einer kubischen Bézier-Kurve platziert, geschnitten
Eine fortgeschrittene Version der Bézier-Platzierungs-Demo, bei der jedes Bild in vertikale Streifen geschnitten und diese einzeln verzerrt werden. Dies ermöglicht es den Bildern, sich flüssiger entlang des Kurvenpfades zu biegen.
INPUT
OUTPUT
JavaScript
// 3 portraits placed along a cubic Bézier curve, sliced
// demo_bezier_placer.js
//!OUTPUT: OUTPUT
// demo_bezier_placer_sliced_final.js
// Crops strips from the image, then warps each strip individually to follow the curve.
const CANVAS_W = 1200;
const CANVAS_H = 600;
const NUM_IMAGES = 3; // How many complete images to place
const SLICES_PER_IMAGE = 15; // Cut each image into this many strips
const TILE_W = 250; // Image width on canvas
const TILE_H = 166; // Image height on canvas
const BLEND_OVER = BlendMode.Over;
// Cubic Bézier control points
const P0 = { x: 80, y: 300 };
const P1 = { x: 380, y: 30 };
const P2 = { x: 820, y: 570 };
const P3 = { x: 1120, y: 300 };
// ── Bézier math ──────────────────────────────────────────────────────────────
function bezierPoint(t) {
const mt = 1 - t, mt2 = mt * mt, mt3 = mt2 * mt;
const t2 = t * t, t3 = t2 * t;
return {
x: mt3*P0.x + 3*mt2*t*P1.x + 3*mt*t2*P2.x + t3*P3.x,
y: mt3*P0.y + 3*mt2*t*P1.y + 3*mt*t2*P2.y + t3*P3.y,
};
}
function bezierTangent(t) {
const mt = 1 - t;
const dx = 3*mt*mt*(P1.x-P0.x) + 6*mt*t*(P2.x-P1.x) + 3*t*t*(P3.x-P2.x);
const dy = 3*mt*mt*(P1.y-P0.y) + 6*mt*t*(P2.y-P1.y) + 3*t*t*(P3.y-P2.y);
const len = Math.sqrt(dx*dx + dy*dy);
return len < 1e-6 ? { x: 1, y: 0 } : { x: dx/len, y: dy/len };
}
// ── Arc-length LUT ───────────────────────────────────────────────────────────
const LUT_STEPS = 400;
const arcLUT = [{ t: 0, dist: 0 }];
let totalLen = 0;
{
let prev = bezierPoint(0);
for (let i = 1; i <= LUT_STEPS; i++) {
const t = i / LUT_STEPS;
const pt = bezierPoint(t);
const dx = pt.x - prev.x, dy = pt.y - prev.y;
totalLen += Math.sqrt(dx*dx + dy*dy);
arcLUT.push({ t, dist: totalLen });
prev = pt;
}
}
function getT(d) {
if (d <= 0) { return 0; }
if (d >= totalLen) { return 1; }
let lo = 0, hi = arcLUT.length - 1;
while (hi - lo > 1) {
const mid = (lo + hi) >> 1;
if (arcLUT[mid].dist < d) { lo = mid; } else { hi = mid; }
}
const s = (d - arcLUT[lo].dist) / (arcLUT[hi].dist - arcLUT[lo].dist);
return arcLUT[lo].t + s * (arcLUT[hi].t - arcLUT[lo].t);
}
// ── Homography math ──────────────────────────────────────────────────────────
function gaussElim(A, b) {
const n = 8;
const M = A.map((row, i) => [...row, b[i]]);
for (let col = 0; col < n; col++) {
let maxRow = col;
for (let row = col + 1; row < n; row++) {
if (Math.abs(M[row][col]) > Math.abs(M[maxRow][col])) { maxRow = row; }
}
const tmp = M[col]; M[col] = M[maxRow]; M[maxRow] = tmp;
for (let row = 0; row < n; row++) {
if (row === col) { continue; }
const f = M[row][col] / M[col][col];
for (let j = col; j <= n; j++) { M[row][j] -= f * M[col][j]; }
}
}
return M.map((row, i) => row[n] / row[i]);
}
function mat3mul(A, B) {
const [a0,a1,a2, a3,a4,a5, a6,a7,a8] = A;
const [b0,b1,b2, b3,b4,b5, b6,b7,b8] = B;
return [
a0*b0+a1*b3+a2*b6, a0*b1+a1*b4+a2*b7, a0*b2+a1*b5+a2*b8,
a3*b0+a4*b3+a5*b6, a3*b1+a4*b4+a5*b7, a3*b2+a4*b5+a5*b8,
a6*b0+a7*b3+a8*b6, a6*b1+a7*b4+a8*b7, a6*b2+a7*b5+a8*b8,
];
}
function computeH_fwd(srcPts, dstPts, canvasW, canvasH) {
const sw = srcPts[1].x;
const sh = srcPts[2].y;
const sn = srcPts.map(p => ({ x: p.x / sw, y: p.y / sh }));
const dn = dstPts.map(p => ({ x: p.x / canvasW, y: p.y / canvasH }));
const A = [], b = [];
for (let i = 0; i < 4; i++) {
const { x: xs, y: ys } = sn[i];
const { x: xd, y: yd } = dn[i];
A.push([xs, ys, 1, 0, 0, 0, -xd*xs, -xd*ys]); b.push(xd);
A.push([0, 0, 0, xs, ys, 1, -yd*xs, -yd*ys]); b.push(yd);
}
const h = gaussElim(A, b);
const H_n = [h[0],h[1],h[2], h[3],h[4],h[5], h[6],h[7], 1.0];
const normSrc = [1/sw, 0, 0, 0, 1/sh, 0, 0, 0, 1];
const denormDst = [canvasW, 0, 0, 0, canvasH, 0, 0, 0, 1];
return mat3mul(denormDst, mat3mul(H_n, normSrc));
}
// ── Render strips along curve ────────────────────────────────────────────────
const src = Engine.loadImage(INPUT);
const srcW = src.width;
const srcH = src.height;
const sliceWidthPx = srcW / SLICES_PER_IMAGE; // Width of each strip in source
const sliceWidthCanvas = TILE_W / SLICES_PER_IMAGE; // Width on canvas
// White canvas background
const canvas = Engine.createImage(1, 1);
canvas.setPixel(px(0, 0), new Pixel(1, 1, 1, 1));
canvas.resize(CANVAS_W, CANVAS_H);
// Curve range where we place images
const startDist = 0.10 * totalLen;
const endDist = 0.90 * totalLen;
const curveLengthPerImage = (endDist - startDist) / NUM_IMAGES;
const stepSize = curveLengthPerImage / SLICES_PER_IMAGE; // Arc-length per strip
for (let imgIdx = 0; imgIdx < NUM_IMAGES; imgIdx++) {
const imageStartDist = startDist + (imgIdx * curveLengthPerImage);
for (let slice = 0; slice < SLICES_PER_IMAGE; slice++) {
// Arc-length distance for this strip's position on the curve
const sliceDist = imageStartDist + (slice * stepSize);
// Get curve position and tangent at this point
const t = getT(sliceDist);
const pos = bezierPoint(t);
const tang = bezierTangent(t);
const N = { x: tang.y, y: -tang.x }; // Normal (perpendicular)
// Crop the strip from the original image
const stripX = slice * sliceWidthPx;
const strip = src.clone().crop(stripX, 0, sliceWidthPx, srcH);
// Source corners for this strip (0 to strip width, 0 to strip height)
const stripW = strip.width;
const stripH = strip.height;
const srcPts = [
{ x: 0, y: 0 }, // TL
{ x: stripW-1, y: 0 }, // TR
{ x: stripW-1, y: stripH-1}, // BR
{ x: 0, y: stripH-1}, // BL
];
// Destination quad on canvas for this strip
// The strip is centered on the curve point
const halfSliceWidth = sliceWidthCanvas / 2;
const halfHeight = TILE_H / 2;
const dstPts = [
{ x: pos.x - halfSliceWidth * tang.x + halfHeight * N.x, // TL
y: pos.y - halfSliceWidth * tang.y + halfHeight * N.y },
{ x: pos.x + halfSliceWidth * tang.x + halfHeight * N.x, // TR
y: pos.y + halfSliceWidth * tang.y + halfHeight * N.y },
{ x: pos.x + halfSliceWidth * tang.x - halfHeight * N.x, // BR
y: pos.y + halfSliceWidth * tang.y - halfHeight * N.y },
{ x: pos.x - halfSliceWidth * tang.x - halfHeight * N.x, // BL
y: pos.y - halfSliceWidth * tang.y - halfHeight * N.y },
];
// Compute homography for this strip (strip source → canvas destination)
const H_fwd = computeH_fwd(srcPts, dstPts, CANVAS_W, CANVAS_H);
// Warp the strip onto the canvas
const warpedStrip = strip.warpPerspective(CANVAS_W, CANVAS_H, H_fwd);
canvas.blendAt(warpedStrip, px(0, 0), 1.0, BLEND_OVER);
// Cleanup
warpedStrip.free();
strip.free();
}
}
// ── Overlay: Bézier curve ────────────────────────────────────────────────────
const ov = Engine.createCanvas(CANVAS_W, CANVAS_H);
function circleMarker(cv, cx, cy, r, color, strokeW) {
const p = Engine.createPath().circle(cx, cy, r);
cv.fill(color).drawPath(p);
if (strokeW > 0) { cv.pen("#ffffffff", strokeW).drawPath(p, true); }
p.free();
}
// The curve
ov.pen(new Pixel(0.10, 0.10, 0.10, 0.85), 3.0);
ov.drawPathStr(`M ${P0.x},${P0.y} C ${P1.x},${P1.y} ${P2.x},${P2.y} ${P3.x},${P3.y}`, true);
// Control lines
ov.pen(new Pixel(0.92, 0.42, 0.12, 0.75), 1.5);
ov.drawPathStr(`M ${P0.x},${P0.y} L ${P1.x},${P1.y}`, true);
ov.drawPathStr(`M ${P3.x},${P3.y} L ${P2.x},${P2.y}`, true);
// Control points
for (const p of [P0, P3]) { circleMarker(ov, p.x, p.y, 7, new Pixel(0.18, 0.40, 0.92, 1.0), 1.5); }
for (const p of [P1, P2]) { circleMarker(ov, p.x, p.y, 5, new Pixel(0.92, 0.42, 0.12, 0.90), 1.5); }
const ovImg = ov.toImage();
canvas.blendAt(ovImg, px(0, 0), 1.0, BLEND_OVER);
ovImg.free();
ov.free();
canvas.save(OUTPUT);
// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0