///|
/// Affine transforms: an explicit 2x3 matrix type with the usual
/// constructors (rotation, translation, scaling, shear), composition and
/// inversion, plus `Image::affine` which renders via inverse mapping with
/// bilinear resampling. Pixels sampled outside the source are transparent.
///|
/// Row-major 2x3 affine matrix mapping `(x, y)` to
/// `(a*x + b*y + c, d*x + e*y + f)`.
pub(all) struct Affine {
a : Double
b : Double
c : Double
d : Double
e : Double
f : Double
}
///|
const PI : Double = 3.141592653589793
///|
/// Identity transform.
pub fn Affine::identity() -> Affine {
{ a: 1.0, b: 0.0, c: 0.0, d: 0.0, e: 1.0, f: 0.0 }
}
///|
/// Translation by `(dx, dy)`.
pub fn Affine::translation(dx : Double, dy : Double) -> Affine {
{ a: 1.0, b: 0.0, c: dx, d: 0.0, e: 1.0, f: dy }
}
///|
/// Scaling about the origin.
pub fn Affine::scaling(sx : Double, sy : Double) -> Affine {
{ a: sx, b: 0.0, c: 0.0, d: 0.0, e: sy, f: 0.0 }
}
///|
/// Rotation about the origin. Positive angles rotate clockwise on screen
/// (y grows downward in image coordinates), matching `rotate90`.
pub fn Affine::rotation(degrees : Double) -> Affine {
let rad = degrees * PI / 180.0
let cs = @math.cos(rad)
let sn = @math.sin(rad)
{ a: cs, b: -sn, c: 0.0, d: sn, e: cs, f: 0.0 }
}
///|
/// Shear: `x' = x + shx*y`, `y' = shy*x + y`.
pub fn Affine::shear(shx : Double, shy : Double) -> Affine {
{ a: 1.0, b: shx, c: 0.0, d: shy, e: 1.0, f: 0.0 }
}
///|
/// Applies the transform to a point.
pub fn Affine::apply(self : Affine, x : Double, y : Double) -> (Double, Double) {
(self.a * x + self.b * y + self.c, self.d * x + self.e * y + self.f)
}
///|
/// Composition: returns the transform that applies `self` first and `next`
/// second (`next * self` as matrices).
pub fn Affine::then(self : Affine, next : Affine) -> Affine {
{
a: next.a * self.a + next.b * self.d,
b: next.a * self.b + next.b * self.e,
c: next.a * self.c + next.b * self.f + next.c,
d: next.d * self.a + next.e * self.d,
e: next.d * self.b + next.e * self.e,
f: next.d * self.c + next.e * self.f + next.f,
}
}
///|
/// Inverse transform, or `None` when the matrix is singular
/// (determinant close to zero) and cannot be inverted.
pub fn Affine::invert(self : Affine) -> Affine? {
let det = self.a * self.e - self.b * self.d
if det < 1.0e-12 && det > -1.0e-12 {
return None
}
let ia = self.e / det
let ib = -self.b / det
let id = -self.d / det
let ie = self.a / det
Some({
a: ia,
b: ib,
c: -(ia * self.c + ib * self.f),
d: id,
e: ie,
f: -(id * self.c + ie * self.f),
})
}
///|
/// Bilinearly samples the image at a fractional position. Taps that fall
/// outside the image contribute transparent black, so transformed edges
/// fade out instead of smearing.
fn Image::sample_bilinear(
self : Image,
fx : Double,
fy : Double,
) -> (Int, Int, Int, Int) {
let x0 = ifloor(fx)
let y0 = ifloor(fy)
let dx = fx - x0.to_double()
let dy = fy - y0.to_double()
let mut r = 0.0
let mut g = 0.0
let mut b = 0.0
let mut a = 0.0
for j in 0..<2 {
for i in 0..<2 {
let sx = x0 + i
let sy = y0 + j
if sx < 0 || sx >= self.width || sy < 0 || sy >= self.height {
continue
}
let wx = if i == 0 { 1.0 - dx } else { dx }
let wy = if j == 0 { 1.0 - dy } else { dy }
let w = wx * wy
let base = (sy * self.width + sx) * 4
r = r + self.data[base].to_int().to_double() * w
g = g + self.data[base + 1].to_int().to_double() * w
b = b + self.data[base + 2].to_int().to_double() * w
a = a + self.data[base + 3].to_int().to_double() * w
}
}
(
(r + 0.5).to_int(),
(g + 0.5).to_int(),
(b + 0.5).to_int(),
(a + 0.5).to_int(),
)
}
///|
/// Applies an affine transform, keeping the canvas size. Each output pixel
/// is inverse-mapped into the source and bilinearly sampled; areas with no
/// source coverage become transparent. Aborts on singular transforms.
pub fn Image::affine(self : Image, t : Affine) -> Image {
let inv = match t.invert() {
Some(m) => m
None => abort("Image::affine: transform is singular")
}
let out = Image::new(self.width, self.height)
for y in 0.. Image {
let cx = (self.width - 1).to_double() / 2.0
let cy = (self.height - 1).to_double() / 2.0
let t = Affine::translation(-cx, -cy)
.then(Affine::rotation(degrees))
.then(Affine::translation(cx, cy))
self.affine(t)
}
///|
/// Translates by a (possibly fractional) offset on a same-size canvas.
pub fn Image::translate(self : Image, dx : Double, dy : Double) -> Image {
self.affine(Affine::translation(dx, dy))
}