SeamlessClone
a slab of pebbles pasted and cloned onto a photograph of Cozumel at dusk, so the join disappears: the patch keeps its own marks and takes on the color of its surroundings
- Run
swift run --package-path Examples Example-Effects-SeamlessClone- Imports
OllinSamplePhotos- Source
- Examples/Effects/SeamlessClone/Sketch.swift
import Ollin
import OllinSamplePhotos
/// `.seamlessClone` drops one layer into another so the join disappears. The patch
/// keeps every mark you drew into it and gives up its own color and brightness,
/// taking on whatever surrounds it instead.
///
/// The recipe is the one gradient-domain compositing has always used. Around the
/// rim of the patch, measure how far the patch's color sits from the backdrop's.
/// Spread that difference across the inside of the patch as smoothly as anything
/// can be spread. Add it back. On the rim that lands exactly on the backdrop, so
/// there is no join to see, and inside it is the patch nudged by the gentlest
/// correction that reaches it. Only the slow, low part of the patch's color is
/// replaced, which is why the detail survives.
///
/// Here a slab of pebbles drifts across a photograph of Cozumel at dusk. It is
/// the same slab twice: on the left as a plain paste, on the right cloned. Watch
/// the right one take the color it lands in without ever changing its pebbles,
/// while the left keeps a hard rim it never earned. `amount` sweeps from a plain
/// paste to a full clone once a cycle, so the seam closes in front of you.
///
/// The photograph is chosen and framed for one reason: a rim wants quiet ground.
/// The correction is measured around the rim and spread inward, so a rim laid
/// across a hard edge spreads that edge into the patch and the whole thing blows
/// out. This picture keeps its brightness in a narrow band while its color runs
/// from gray-violet to pink, which is what a clone is for, and the frame here is
/// the left square of it, since the sun in the right third is the one hard thing
/// in it. Both slabs stay well above the horizon for the same reason.
///
/// Try it: hold `amount` at 1 and drag `patchHue`, or move a slab down onto the
/// horizon and watch that line smear inward. That is the technique's own limit
/// rather than a bug.
@main
final class SeamlessClone_Example: Sketch {
@Param("Amount", 0...1, icon: "slider.horizontal.3")
var amount = 1.0
@Param("Patch hue", 0...1, icon: "paintpalette")
var patchHue = 0.58
@Param("Sweep the amount", icon: "repeat")
var sweeping = false
private var backdropPicture = Image(width: 1, height: 1)
override func setup() {
seed(7)
// The left square of the photograph. The sun sits in its right third,
// and a rim laid across something that bright spreads it inward, so the
// crop takes the calm end and leaves the glare out of the frame.
let whole = SamplePhoto.boats.load()
backdropPicture = whole.cropped(x: 0, y: 0, width: whole.height, height: whole.height)
}
override func draw() {
background(Color(white: 0.05))
let sweep = sweeping ? (0.5 - 0.5 * cos(time * 0.6)) : amount
let drift = sin(time * 0.35)
// The backdrop is a photograph, and which one matters. The clone keeps
// the patch's own range of tone and only moves where that range sits, so
// a patch dropped somewhere much darker than itself has its shadows
// pushed below black and clips. That is the technique being honest
// rather than failing, and it is why this picture is the one here whose
// brightness stays in a narrow band while its color runs from pink to
// turquoise: the slab travels from sky to water and lands in a different
// color every time without ever landing in the dark.
let backdrop = makeRenderTarget()
withTarget(backdrop) {
drawImage(backdropPicture, in: canvasRectangle, fit: .cover)
}
// The patch, drawn twice at two places. Its own color is deliberately wrong
// for where it lands: cool and flat against a warm, graded backdrop.
func pebbles(at center: Vector2) {
noStroke()
fill(Color(hue: patchHue, saturation: 0.20, brightness: 0.60))
drawCircle(center.x, center.y, 165)
// Real photographic texture is a modest wobble around a mean, not a
// full sweep of tone. Gradient-domain cloning keeps whatever range the
// patch has and only moves its mean, so a high-contrast patch on a dark
// backdrop crushes to black. That is the technique being honest rather
// than failing, and it is why the wobble here is kept small.
for i in 0 ..< 260 {
let a = Double(i) * 2.39996
let r = 162 * sqrt(Double(i) / 260)
let p = center + Vector2(angle: a) * r
fill(Color(hue: patchHue, saturation: 0.24,
brightness: 0.50 + noise(p.x * 0.016, p.y * 0.016) * 0.20))
drawCircle(p.x, p.y, 7 + noise(Double(i) * 0.31) * 8)
}
}
// Both slabs stay in the sky, well above the horizon. The rim is where
// the correction is measured, so a rim laid across a hard edge spreads
// that edge inward and the patch blows out. The sky is the quiet ground
// this picture offers, and it still runs from gray-violet on the left to
// the sun's pink on the right, which is the color the clone has to take.
let leftAt = Vector2(width * 0.27, height * 0.30 + drift * height * 0.10)
let rightAt = Vector2(width * 0.73, height * 0.30 + drift * height * 0.10)
let plain = makeRenderTarget()
withTarget(plain) { pebbles(at: leftAt) }
let clone = makeRenderTarget()
withTarget(clone) { pebbles(at: rightAt) }
// The left slab is pasted with the seam left in, the right one cloned.
let pasted = backdrop.combined(with: plain, .seamlessClone(amount: 0))
let healed = pasted.combined(with: clone, .seamlessClone(amount: sweep))
drawImage(healed.image, 0, 0)
// Labels, and the amount the right slab is being cloned at.
fill(.white)
textSize(30)
textAlign(.center)
drawText("pasted", leftAt.x, height * 0.92)
drawText("cloned at \(String(format: "%.2f", sweep))", rightAt.x, height * 0.92)
drawCaption("Seamless clone. Both slabs are the same; the right one keeps its "
+ "pebbles and takes the color it lands in.")
}
}