2026-07-24 10:33:45 +02:00
|
|
|
|
internal import CPlaydate
|
|
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
extension Graphics {
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// A drawable image and drawing target. Wraps `LCDBitmap`. Bitmaps borrowed from
|
2026-09-18 12:50:47 +02:00
|
|
|
|
/// tables, fonts, video players, or the system live only as long as their owner.
|
2026-07-24 10:33:45 +02:00
|
|
|
|
public final class Bitmap {
|
|
|
|
|
|
let pointer: OpaquePointer
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// Whether deinit frees the `LCDBitmap`.
|
2026-07-24 10:33:45 +02:00
|
|
|
|
let isOwned: Bool
|
2026-09-18 12:50:47 +02:00
|
|
|
|
/// Kept alive because this bitmap shares its pixels.
|
|
|
|
|
|
private let owner: Bitmap?
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 12:50:47 +02:00
|
|
|
|
init(pointer: OpaquePointer, isOwned: Bool, owner: Bitmap? = nil) {
|
2026-07-24 10:33:45 +02:00
|
|
|
|
self.pointer = pointer
|
|
|
|
|
|
self.isOwned = isOwned
|
2026-09-18 12:50:47 +02:00
|
|
|
|
self.owner = owner
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public convenience init(width: Int, height: Int, backgroundColor: Color = .clear) {
|
|
|
|
|
|
let pointer = backgroundColor.withLCDColor {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
gfx.pointee.newBitmap.unsafelyUnwrapped(Int32(width), Int32(height), $0)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
self.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// `path` is in the game's pdx or Data directory.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public convenience init(path: String) throws(PlaydateError) {
|
2026-07-24 10:33:45 +02:00
|
|
|
|
var error: UnsafePointer<CChar>?
|
2026-09-18 11:58:57 +02:00
|
|
|
|
let pointer = path.withCString { gfx.pointee.loadBitmap.unsafelyUnwrapped($0, &error) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
guard let pointer else { throw PlaydateError(cString: error) }
|
2026-07-24 10:33:45 +02:00
|
|
|
|
self.init(pointer: pointer, isOwned: true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
deinit {
|
|
|
|
|
|
if isOwned {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
gfx.pointee.freeBitmap.unsafelyUnwrapped(pointer)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
// MARK: Size and pixel data
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
|
|
|
|
|
public var data: Data {
|
2026-09-18 12:06:21 +02:00
|
|
|
|
let raw = rawData()
|
|
|
|
|
|
return Data(width: raw.width, height: raw.height, rowBytes: raw.rowBytes,
|
|
|
|
|
|
hasMask: raw.mask != nil)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// 1 bit per pixel, MSB first, `height` rows of `rowBytes` bytes. The span is valid
|
|
|
|
|
|
/// only inside `body`, and empty if the bitmap has no data.
|
2026-09-18 12:06:21 +02:00
|
|
|
|
public func withPixelData<Result, Failure: Error>(
|
|
|
|
|
|
_ body: (inout MutableSpan<UInt8>) throws(Failure) -> Result
|
|
|
|
|
|
) throws(Failure) -> Result {
|
|
|
|
|
|
let raw = rawData()
|
|
|
|
|
|
var span = UnsafeMutableBufferPointer(
|
|
|
|
|
|
start: raw.data, count: raw.data == nil ? 0 : raw.height * raw.rowBytes).mutableSpan
|
|
|
|
|
|
return try body(&span)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// Laid out like the pixel data; valid only inside `body`. Returns `nil` without
|
|
|
|
|
|
/// calling `body` if the bitmap has no mask.
|
2026-09-18 12:06:21 +02:00
|
|
|
|
public func withMaskData<Result, Failure: Error>(
|
|
|
|
|
|
_ body: (inout MutableSpan<UInt8>) throws(Failure) -> Result
|
|
|
|
|
|
) throws(Failure) -> Result? {
|
|
|
|
|
|
let raw = rawData()
|
|
|
|
|
|
guard let mask = raw.mask else { return nil }
|
|
|
|
|
|
var span = UnsafeMutableBufferPointer(start: mask, count: raw.height * raw.rowBytes).mutableSpan
|
|
|
|
|
|
return try body(&span)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func rawData() -> (width: Int, height: Int, rowBytes: Int,
|
|
|
|
|
|
mask: UnsafeMutablePointer<UInt8>?, data: UnsafeMutablePointer<UInt8>?) {
|
2026-07-24 10:33:45 +02:00
|
|
|
|
var width: Int32 = 0, height: Int32 = 0, rowBytes: Int32 = 0
|
|
|
|
|
|
var mask: UnsafeMutablePointer<UInt8>?
|
|
|
|
|
|
var data: UnsafeMutablePointer<UInt8>?
|
2026-07-25 07:23:19 +02:00
|
|
|
|
gfx.pointee.getBitmapData.unsafelyUnwrapped(pointer, &width, &height, &rowBytes, &mask, &data)
|
2026-09-18 12:06:21 +02:00
|
|
|
|
return (Int(width), Int(height), Int(rowBytes), mask, data)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// Saves a `getBitmapData` call per access. Reset by `load(path:)`, the only resizer.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
private var cachedSize: (width: Int, height: Int)?
|
|
|
|
|
|
|
|
|
|
|
|
private var size: (width: Int, height: Int) {
|
|
|
|
|
|
if let cachedSize { return cachedSize }
|
2026-09-18 12:06:21 +02:00
|
|
|
|
let raw = rawData()
|
|
|
|
|
|
let size = (raw.width, raw.height)
|
2026-07-25 10:43:32 +02:00
|
|
|
|
cachedSize = size
|
|
|
|
|
|
return size
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// Width, in pixels.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
public var width: Int { size.width }
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// Height, in pixels.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
public var height: Int { size.height }
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// `.black` or `.white`, or `.clear` if out of bounds or masked out.
|
2026-07-24 10:33:45 +02:00
|
|
|
|
public func pixel(x: Int, y: Int) -> SolidColor {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
SolidColor(gfx.pointee.getBitmapPixel.unsafelyUnwrapped(pointer, Int32(x), Int32(y)))
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: Operations
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// Replaces the contents, and possibly the size, with the image at `path`.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func load(path: String) throws(PlaydateError) {
|
2026-07-24 10:33:45 +02:00
|
|
|
|
var error: UnsafePointer<CChar>?
|
2026-09-18 11:58:57 +02:00
|
|
|
|
path.withCString { gfx.pointee.loadIntoBitmap.unsafelyUnwrapped($0, pointer, &error) }
|
2026-07-25 10:43:32 +02:00
|
|
|
|
cachedSize = nil
|
2026-07-24 11:10:25 +02:00
|
|
|
|
if let error { throw PlaydateError(cString: error) }
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public func clear(color: Color) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
color.withLCDColor { gfx.pointee.clearBitmap.unsafelyUnwrapped(pointer, $0) }
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public func copy() -> Bitmap {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
Bitmap(pointer: gfx.pointee.copyBitmap.unsafelyUnwrapped(pointer).unsafelyUnwrapped, isOwned: true)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// `degrees` is clockwise. Returns `nil` on failure.
|
2026-07-24 10:33:45 +02:00
|
|
|
|
public func rotated(by degrees: Float, xScale: Float = 1, yScale: Float = 1) -> Bitmap? {
|
|
|
|
|
|
var allocatedSize: Int32 = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
guard let rotated = gfx.pointee.rotatedBitmap.unsafelyUnwrapped(
|
2026-07-24 10:33:45 +02:00
|
|
|
|
pointer, degrees, xScale, yScale, &allocatedSize) else { return nil }
|
|
|
|
|
|
return Bitmap(pointer: rotated, isOwned: true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// Returns `false` if `mask` is `nil` or a different size.
|
2026-07-24 10:33:45 +02:00
|
|
|
|
@discardableResult
|
|
|
|
|
|
public func setMask(_ mask: Bitmap?) -> Bool {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
gfx.pointee.setBitmapMask.unsafelyUnwrapped(pointer, mask?.pointer) != 0
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:50:47 +02:00
|
|
|
|
/// Shares this bitmap's mask data, and keeps this bitmap alive.
|
2026-07-24 10:33:45 +02:00
|
|
|
|
public var mask: Bitmap? {
|
2026-09-18 12:50:47 +02:00
|
|
|
|
// Owned by the caller; pixels are shared with `self`.
|
2026-07-25 07:23:19 +02:00
|
|
|
|
guard let mask = gfx.pointee.getBitmapMask.unsafelyUnwrapped(pointer) else { return nil }
|
2026-09-18 12:50:47 +02:00
|
|
|
|
return Bitmap(pointer: mask, isOwned: true, owner: self)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// Whether opaque pixels of both bitmaps overlap within the non-empty `rect`.
|
|
|
|
|
|
/// `false` if either bitmap lies entirely outside `rect`.
|
2026-07-24 10:33:45 +02:00
|
|
|
|
public func checkMaskCollision(x: Int, y: Int, flip: BitmapFlip = .unflipped,
|
|
|
|
|
|
other: Bitmap, otherX: Int, otherY: Int,
|
|
|
|
|
|
otherFlip: BitmapFlip = .unflipped,
|
|
|
|
|
|
in rect: Rect) -> Bool {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
gfx.pointee.checkMaskCollision.unsafelyUnwrapped(
|
2026-07-24 10:33:45 +02:00
|
|
|
|
pointer, Int32(x), Int32(y), flip.cValue,
|
|
|
|
|
|
other.pointer, Int32(otherX), Int32(otherY), otherFlip.cValue,
|
|
|
|
|
|
rect.cValue) != 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: Drawing
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// (x, y) is the upper-left corner.
|
2026-07-24 10:33:45 +02:00
|
|
|
|
public func draw(x: Int, y: Int, flip: BitmapFlip = .unflipped) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
gfx.pointee.drawBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y), flip.cValue)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// (x, y) is the upper-left corner. Negative scales flip the bitmap.
|
2026-07-24 10:33:45 +02:00
|
|
|
|
public func drawScaled(x: Int, y: Int, xScale: Float, yScale: Float) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
gfx.pointee.drawScaledBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y), xScale, yScale)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// Scales, then rotates, placing the anchor (`centerX`, `centerY`) at (x, y). Anchors
|
|
|
|
|
|
/// are proportional: (0.5, 0.5) is the center, (0, 0) the unrotated upper-left.
|
2026-07-24 10:33:45 +02:00
|
|
|
|
public func drawRotated(x: Int, y: Int, degrees: Float,
|
|
|
|
|
|
centerX: Float = 0.5, centerY: Float = 0.5,
|
|
|
|
|
|
xScale: Float = 1, yScale: Float = 1) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
gfx.pointee.drawRotatedBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y), degrees,
|
2026-07-24 10:33:45 +02:00
|
|
|
|
centerX, centerY, xScale, yScale)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:45:39 +02:00
|
|
|
|
/// Tiles the `width` × `height` rect whose upper-left corner is (x, y).
|
2026-07-24 10:33:45 +02:00
|
|
|
|
public func tile(x: Int, y: Int, width: Int, height: Int, flip: BitmapFlip = .unflipped) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
gfx.pointee.tileBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y),
|
2026-07-24 10:33:45 +02:00
|
|
|
|
Int32(width), Int32(height), flip.cValue)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|