Skip to content
C.W.K.
Stream
Lesson 02 of 05 · published

Clone Through Coverage

~12 min · libmypaint, clone, compositing, copy-on-write, architecture

Level 0Blank Canvas
0 XP0/37 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

A Tool the Engine Does Not Have

Clone is the tool where any brush paints with the picture itself, taken from a source circle that keeps its distance from the brush. Drag the circle onto a patch of sky, paint over a power line, and the sky arrives under the brush with its texture intact. libmypaint has nothing like it.

The nearest thing it has is smudge, and it is worth seeing exactly why smudge is not a clone tool. When a libmypaint brush smudges, update_smudge_color calls get_color over a radius around the dab. That returns one averaged color, which is mixed into a running smudge color, and the dab is then painted in that single color. Smudge carries a color along the stroke. It never carries the picture: a dab is one color, so the grain of the sky, an edge, the highlight in an eye cannot ride on it. No setting changes that, because it is the shape of the computation.

Split Where From What

Ariandel's answer is to divide the job. The brush decides where and how strongly. The picture decides what. libmypaint is excellent at the first half and is asked to do nothing else.

When a Clone stroke begins, beginStroke paints the clone brush in plain white, with eraser, smudge and alpha lock all at 0 (first code block). It also builds a CloneStroke, which owns a scratch TileStore of its own and a BrushSurface over that store. libmypaint paints into the scratch store as it would paint anywhere: pressure, tilt, jitter, a shaped tip, symmetry, all of it. White means only the alpha matters. What lands in the scratch store is pure coverage, a map of where and how much.

Laying the Picture Through It

After each batch, lay(_:over:of:) (second code block) visits every tile the coverage reached in that batch and does four things:

  1. Reads the source. It takes the tile's own rectangle, moves it by offset, and reads that rectangle from original in fix15. The shifted rectangle usually straddles up to four tiles; fix15(in:) assembles it.
  2. Finds what lies under. original[key], the same tile as the stroke found it.
  3. Merges through the coverage. ard_fix15_clone_merge computes under + (source − under) · c / ONE per channel, where c is the coverage's alpha at that pixel.
  4. Respects alpha lock. On a locked layer, ard_fix15_keep_alpha restores the original alpha, and a tile that was empty before stays empty.

Notice what the merge is written over: under, the layer as the stroke found it, not the live tile. The coverage keeps accumulating for the whole stroke, so each batch recomputes the tile from scratch as "the original, moved toward the source by everything the brush has laid so far." The result depends only on total coverage, never on how many batches it took to get there. Laying it again is harmless.

When an engine can answer "where" but not "what", let it paint a mask and supply the "what" yourself. The engine keeps every dynamic it knows; you keep the meaning. The same split carries the dual brush in the next lesson.

The Offset

Painter.cloneOffset is a SIMD2<Int> in whole canvas pixels: source = brush + offset. The app sets it from the circle. When Clone opens with no circle yet, the circle appears left of center (35% across, 45% down the view). The first stroke after the circle moves fixes the offset as source − start, rounded to whole pixels; later strokes keep that distance, and while Dad paints, the circle follows the brush at it. That is an aligned clone: lift, paint somewhere else, and the source moves with you.

Whole pixels are a choice, not an accident. A fractional offset would mean resampling the source on every batch, and every resample softens the picture a little. A whole-pixel offset makes reading the source a straight copy, so what arrives under the brush is what was there.

Never Its Own Fresh Paint

original is taken once, when the stroke begins: layer.tiles.frozenTiles(), every live tile frozen. Thanks to copy-on-write that costs no pixel copies at all; the layer copies a tile only when the stroke first writes it (Track 3).

Why not read the source from the live layer, which would be simpler? Because the brush travels. Paint so the source trails behind the brush along its own path, and the source rectangle soon lands on pixels this very stroke just cloned. Read from the live layer, and you clone your clone, then clone that, and a few pixels repeat across the stroke. The third code block runs both versions on one row: from original, the row reads ABCDEFABCDEFGHIJKLMNOPQR, a true copy; from the live layer, ABCDEFABCDEFABCDEFABCDEF, the first six pixels stamped over and over. The engine suite pins the real thing as aStrokeNeverClonesItsOwnFreshPaint: a red square, an offset of ten pixels back along the stroke, red just past the square, and empty canvas further on.

The same original survives a redraw. When QuickShape snaps a stroke or a taper redraws it at the lift, startAgain builds a fresh CloneStroke with an empty coverage and the same original, so the redrawn clone still copies the layer as the stroke first found it.

Code

A clone stroke paints plain coverage (AriandelEngine/Painter.swift, beginStroke, verbatim)·swift
case .clone:
    // Only the brush's coverage counts: plain white, never erasing or smearing.
    brush.color = RGBAColor.white.hsv
    brush.setBaseValue(.eraser, 0)
    brush.setBaseValue(.smudge, 0)
    clone = CloneStroke(layer: layer, offset: cloneOffset, symmetry: symmetry)
}
if layer.isAlphaLocked, tool != .clone {
    brush.setBaseValue(.lockAlpha, 1)
}
if tool == .clone {
    brush.setBaseValue(.lockAlpha, 0)
}
Laying the offset original through the coverage (AriandelEngine/Painter.swift, verbatim)·swift
/// Clone: for each tile the batch's coverage reached, the picture from
/// `offset` away laid over the layer as it was, through the coverage so far.
private func lay(_ clone: CloneStroke, over keys: Dictionary<TileKey, TileBuffer?>.Keys, of store: TileStore) {
    for key in keys {
        guard let coverage = clone.coverage.tile(at: key) else { continue }
        let rect = PixelRect(tile: key)
        let source = clone.original.fix15(in: PixelRect(x: rect.x + clone.offset.x, y: rect.y + clone.offset.y,
                                                        width: rect.width, height: rect.height))
        let under = clone.original[key]
        let tile = store.writableTile(at: key)
        source.withUnsafeBufferPointer { source in
            ard_fix15_clone_merge(tile.pixels, under.map { UnsafePointer($0.pixels) }, source.baseAddress!,
                                  coverage.pixels, Tile.pixelCount)
        }
        if clone.alphaLocked {
            if let under {
                ard_fix15_keep_alpha(tile.pixels, under.pixels, Tile.pixelCount)
            } else {
                store.setTile(nil, at: key)
            }
        }
    }
}
A minimal sketch of the offset logic on one row (coverage of 0 or 1, no tiles; compiled and run with swiftc in Swift 6 mode)·swift
// A minimal sketch of Clone's offset logic on one row of pixels: the brush
// moves right, one pixel a batch, with the source 6 pixels to its left.
// Painter.lay(_:over:of:) reads the source from `original`, the layer as the
// stroke found it. Reading from the live layer instead clones fresh paint.

let original = Array("ABCDEFGHIJKLMNOPQRSTUVWX")
let offset = -6  // source = brush + offset, in whole pixels

func clone(readingFromLive: Bool) -> String {
    var live = original
    var coverage = [Double](repeating: 0, count: original.count)
    for brush in 6..<original.count {
        coverage[brush] = 1  // the white scratch stroke reaches this pixel
        // After the batch: every pixel the coverage reached gets the source laid
        // through it (under + (source - under) * c; c is 0 or 1 here).
        let before = live
        for x in 0..<original.count where coverage[x] > 0 {
            let from = readingFromLive ? before : original
            live[x] = coverage[x] >= 1 ? from[x + offset] : live[x]
        }
    }
    return String(live)
}

print("layer before:      ", String(original))
print("cloned (original): ", clone(readingFromLive: false))
print("cloned (live):     ", clone(readingFromLive: true))

// Output:
// layer before:       ABCDEFGHIJKLMNOPQRSTUVWX
// cloned (original):  ABCDEFABCDEFGHIJKLMNOPQR
// cloned (live):      ABCDEFABCDEFABCDEFABCDEF

External links

Exercise

Build a clone tool in the smallest setting you can: an HTML canvas, a NumPy array, anything with pixels. Keep three buffers: the image as the stroke found it, a coverage buffer the brush paints white into, and the live image. After each mouse move, rebuild every covered pixel as original + (source − original) · coverage, with source taken from a whole-pixel offset. Then switch the source to the live image and drag so the source follows the brush's own path. Describe what you see, and explain it in one sentence.
Hint
Drag so the source trails behind the brush over ground the stroke has already covered, for example an offset of (−20, 0) and a stroke going right. Past the first twenty pixels, the live version starts copying pixels it already wrote.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.