Splitter-Schnitte — Splitterglas-Effekt mit drei Reglern

Splitterglas-Effekt für jedes Foto: Das Skript zerschneidet das Bild in zufällige Polygon-Scherben und schiebt jedes Fragment nach außen, sodass zwischen den Stücken helle Risse aufreißen. Drei Regler steuern den Look — `CUTS` bestimmt die Anzahl der Scherben, `JITTER` wie weit sie auseinanderfliegen, `SEED` reproduziert exakt dasselbe Muster. Ideal für editoriale Collage-Layouts, Plakate im Zerbrochene-Spiegel-Stil, Glitch-Album-Cover oder knackige Social-Media-Header aus einem einzigen Quellfoto.

SRC
SRC — Splitter-Schnitte — Splitterglas-Effekt mit drei Reglern
Shatter cuts
Shatter cuts — Splitter-Schnitte — Splitterglas-Effekt mit drei Reglern
JavaScript
// Shatter / cut-and-jitter — slice the image with N random straight
// cuts (each between a pair of opposite sides), then push every tile
// outward from the centre by JITTER px so the seams open into visible
// gaps.
//
// Generated by Gemini 2.5 Pro via gemini-cli from SKILLS.md as the
// sole reference, then hand-cleaned for whitespace. Per-polygon
// mask/tile allocation cropped to the polygon bbox (Tier-1 optimisation
// over the LLM's first cut, which allocated full W×H buffers per poly).
// Same algorithm; ~7× less transient memory churn at typical CUTS.

//!INPUT: SRC
//!OUTPUT: OUT
//!PARAM: CUTS:integer=2,min=1,max=100
//!PARAM: JITTER:integer=12,min=0,max=64
//!PARAM: SEED:integer=0,min=0,max=1000000

// Seeded LCG for reproducible randomness; SEED=0 picks a random state.
let state = SEED === 0 ? Math.floor(Math.random() * 1000000) : SEED;
const rand = () => {
    state = (state * 1103515245 + 12345) % 2147483648;
    return state / 2147483648;
};

const img = Engine.loadImage(SRC);
const W = img.width;
const H = img.height;
const center = { x: W / 2, y: H / 2 };

// Start with a single polygon covering the entire image.
let polygons = [[{ x: 0, y: 0 }, { x: W, y: 0 }, { x: W, y: H }, { x: 0, y: H }]];

// Iteratively split polygons using random straight lines between opposite sides.
for (let i = 0; i < CUTS; i++) {
    let p1, p2;
    if (rand() < 0.5) {
        // Vertical-ish: top edge -> bottom edge
        p1 = { x: rand() * W, y: 0 };
        p2 = { x: rand() * W, y: H };
    } else {
        // Horizontal-ish: left edge -> right edge
        p1 = { x: 0, y: rand() * H };
        p2 = { x: W, y: rand() * H };
    }

    const nextBatch = [];
    for (const poly of polygons) {
        const partA = [], partB = [];
        for (let j = 0; j < poly.length; j++) {
            const a = poly[j], b = poly[(j + 1) % poly.length];

            // Signed distance of each vertex to the cut line p1->p2
            const distA = (p2.x - p1.x) * (a.y - p1.y) - (p2.y - p1.y) * (a.x - p1.x);
            const distB = (p2.x - p1.x) * (b.y - p1.y) - (p2.y - p1.y) * (b.x - p1.x);

            if (distA >= 0) partA.push(a);
            if (distA <= 0) partB.push(a);

            // If the edge straddles the line, insert the intersection point.
            if (distA * distB < 0) {
                const t = Math.abs(distA) / (Math.abs(distA) + Math.abs(distB));
                const inter = { x: a.x + t * (b.x - a.x), y: a.y + t * (b.y - a.y) };
                partA.push(inter);
                partB.push(inter);
            }
        }
        if (partA.length >= 3) nextBatch.push(partA);
        if (partB.length >= 3) nextBatch.push(partB);
    }
    polygons = nextBatch;
}

const out = Engine.createImage(W, H);

for (const poly of polygons) {
    // Shoelace centroid: (cx, cy) is the area-weighted polygon centre.
    let area2 = 0, cx = 0, cy = 0;
    for (let i = 0; i < poly.length; i++) {
        const a = poly[i], b = poly[(i + 1) % poly.length];
        const f = a.x * b.y - b.x * a.y;
        area2 += f;
        cx += (a.x + b.x) * f;
        cy += (a.y + b.y) * f;
    }

    if (Math.abs(area2) > 0.01) {
        cx /= (3 * area2);
        cy /= (3 * area2);

        // Direction from image centre to polygon centroid, normalised.
        const dx = cx - center.x, dy = cy - center.y;
        const dMag = Math.sqrt(dx * dx + dy * dy);
        const offX = dMag > 0 ? (dx / dMag) * JITTER : 0;
        const offY = dMag > 0 ? (dy / dMag) * JITTER : 0;

        // Polygon bbox (clamped to image rect). Working in bbox-local
        // coords means the mask, the cloned tile, and the rasterised
        // canvas are all only as big as the polygon needs — instead of
        // full W×H for every region. Saves ~36 MB per poly at 1024².
        let minX = W, minY = H, maxX = 0, maxY = 0;
        for (const p of poly) {
            if (p.x < minX) minX = p.x; if (p.y < minY) minY = p.y;
            if (p.x > maxX) maxX = p.x; if (p.y > maxY) maxY = p.y;
        }
        const bx = Math.max(0, Math.floor(minX));
        const by = Math.max(0, Math.floor(minY));
        const bw = Math.min(W, Math.ceil(maxX)) - bx;
        const bh = Math.min(H, Math.ceil(maxY)) - by;
        if (bw < 1 || bh < 1) continue;

        // Rasterise the polygon mask at bbox size (vertices shifted into bbox-local).
        const cv = Engine.createCanvas(bw, bh);
        const path = Engine.createPath();
        path.moveTo(poly[0].x - bx, poly[0].y - by);
        for (let i = 1; i < poly.length; i++) path.lineTo(poly[i].x - bx, poly[i].y - by);
        path.close();

        cv.fill('#fff').drawPath(path);

        // Mask = bbox-sized alpha image; tile = bbox-sized image crop.
        // applyMask zeroes outside the polygon; blendAt places it at the
        // displaced bbox origin.
        const mask = cv.toImage();
        const tile = img.clone().crop(bx, by, bw, bh).applyMask(mask);
        out.blendAt(tile,
                    px(bx + Math.round(offX), by + Math.round(offY)),
                    1.0, Blend.Over);

        tile.free();
        mask.free();
        path.free();
        cv.free();
    }
}

out.save(OUT);
out.free();
img.free();

// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0