Files
playdate-kit/Sources/PlayDate/GraphicsBitmap.swift
T
javierandClaude Fable 5 6485997edc Promote subsystem namespaces to the top level of the module
System, Display, Graphics, Sprite, Sound, File, JSON, Lua, Scoreboards,
Network, Rect, SystemEvent, and AccessReply now live at module scope
instead of being nested in the Playdate enum; consumers write
System.log(...) instead of Playdate.System.log(...), and can qualify with
the module name (PlayDate.System) on collision. The Playdate enum remains
only as the raw C API bootstrap (initialize(with:), api, apiPointer), and
Playdate.Error is renamed PlaydateError so no top-level type shadows
Swift.Error.

Breaking change for any existing consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:10:25 +02:00

208 lines
8.6 KiB
Swift

//
// GraphicsBitmap.swift
// Bitmap and BitmapTable wrappers around LCDBitmap / LCDBitmapTable.
//
internal import CPlaydate
extension Graphics {
/// An image that can be drawn to the screen or used as a drawing target.
/// Wraps `LCDBitmap`.
public final class Bitmap {
let pointer: OpaquePointer
/// Whether this wrapper owns the underlying `LCDBitmap` and frees it
/// on deinit. Bitmaps vended by tables or the system are not owned;
/// keep their owner alive while using them.
let isOwned: Bool
init(pointer: OpaquePointer, isOwned: Bool) {
self.pointer = pointer
self.isOwned = isOwned
}
/// Allocates a new bitmap filled with `backgroundColor`.
public convenience init(width: Int, height: Int, backgroundColor: Color = .clear) {
let pointer = backgroundColor.withLCDColor {
gfx.newBitmap.unsafelyUnwrapped(Int32(width), Int32(height), $0)
}
self.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
}
/// Loads a bitmap from a file in the game's pdx or Data directory.
public convenience init(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
let pointer = path.withPlaydateCString { gfx.loadBitmap.unsafelyUnwrapped($0, &error) }
guard let pointer else { throw PlaydateError(cString: error) }
self.init(pointer: pointer, isOwned: true)
}
deinit {
if isOwned {
gfx.freeBitmap.unsafelyUnwrapped(pointer)
}
}
// MARK: Properties
/// The bitmap's dimensions, row stride, and raw pixel/mask storage.
/// The pointers are owned by the bitmap.
public struct Data {
public let width: Int
public let height: Int
public let rowBytes: Int
public let mask: UnsafeMutablePointer<UInt8>?
public let data: UnsafeMutablePointer<UInt8>?
}
public var data: Data {
var width: Int32 = 0, height: Int32 = 0, rowBytes: Int32 = 0
var mask: UnsafeMutablePointer<UInt8>?
var data: UnsafeMutablePointer<UInt8>?
gfx.getBitmapData.unsafelyUnwrapped(pointer, &width, &height, &rowBytes, &mask, &data)
return Data(width: Int(width), height: Int(height), rowBytes: Int(rowBytes),
mask: mask, data: data)
}
public var width: Int { data.width }
public var height: Int { data.height }
/// The color of the pixel at (x, y).
public func pixel(x: Int, y: Int) -> SolidColor {
SolidColor(gfx.getBitmapPixel.unsafelyUnwrapped(pointer, Int32(x), Int32(y)))
}
// MARK: Operations
/// Replaces the bitmap's contents with the image at `path`.
public func load(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
path.withPlaydateCString { gfx.loadIntoBitmap.unsafelyUnwrapped($0, pointer, &error) }
if let error { throw PlaydateError(cString: error) }
}
/// Fills the bitmap with `color`.
public func clear(color: Color) {
color.withLCDColor { gfx.clearBitmap.unsafelyUnwrapped(pointer, $0) }
}
public func copy() -> Bitmap {
Bitmap(pointer: gfx.copyBitmap.unsafelyUnwrapped(pointer).unsafelyUnwrapped, isOwned: true)
}
/// Returns a new bitmap rotated by `degrees` (clockwise) and scaled.
public func rotated(by degrees: Float, xScale: Float = 1, yScale: Float = 1) -> Bitmap? {
var allocatedSize: Int32 = 0
guard let rotated = gfx.rotatedBitmap.unsafelyUnwrapped(
pointer, degrees, xScale, yScale, &allocatedSize) else { return nil }
return Bitmap(pointer: rotated, isOwned: true)
}
/// Sets a mask image. The mask must match the bitmap's dimensions.
@discardableResult
public func setMask(_ mask: Bitmap?) -> Bool {
gfx.setBitmapMask.unsafelyUnwrapped(pointer, mask?.pointer) != 0
}
/// The bitmap's mask, if any. The returned bitmap references storage
/// owned by this bitmap.
public var mask: Bitmap? {
guard let mask = gfx.getBitmapMask.unsafelyUnwrapped(pointer) else { return nil }
return Bitmap(pointer: mask, isOwned: false)
}
/// Tests whether the opaque pixels of two bitmaps overlap within
/// `rect`, given each bitmap's position and flip.
public func checkMaskCollision(x: Int, y: Int, flip: BitmapFlip = .unflipped,
other: Bitmap, otherX: Int, otherY: Int,
otherFlip: BitmapFlip = .unflipped,
in rect: Rect) -> Bool {
gfx.checkMaskCollision.unsafelyUnwrapped(
pointer, Int32(x), Int32(y), flip.cValue,
other.pointer, Int32(otherX), Int32(otherY), otherFlip.cValue,
rect.cValue) != 0
}
// MARK: Drawing
/// Draws the bitmap with its upper-left corner at (x, y).
public func draw(x: Int, y: Int, flip: BitmapFlip = .unflipped) {
gfx.drawBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y), flip.cValue)
}
/// Draws the bitmap scaled by (xScale, yScale) with its upper-left
/// corner at (x, y).
public func drawScaled(x: Int, y: Int, xScale: Float, yScale: Float) {
gfx.drawScaledBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y), xScale, yScale)
}
/// Draws the bitmap rotated by `degrees` around its anchor point,
/// where (0.5, 0.5) is the center.
public func drawRotated(x: Int, y: Int, degrees: Float,
centerX: Float = 0.5, centerY: Float = 0.5,
xScale: Float = 1, yScale: Float = 1) {
gfx.drawRotatedBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y), degrees,
centerX, centerY, xScale, yScale)
}
/// Tiles the bitmap over the given area.
public func tile(x: Int, y: Int, width: Int, height: Int, flip: BitmapFlip = .unflipped) {
gfx.tileBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y),
Int32(width), Int32(height), flip.cValue)
}
}
/// A collection of bitmaps loaded from an image table. Wraps `LCDBitmapTable`.
public final class BitmapTable {
let pointer: OpaquePointer
init(pointer: OpaquePointer) {
self.pointer = pointer
}
/// Allocates a table with room for `count` bitmaps of the given size.
public convenience init(count: Int, width: Int, height: Int) {
let pointer = gfx.newBitmapTable.unsafelyUnwrapped(Int32(count), Int32(width), Int32(height))
self.init(pointer: pointer.unsafelyUnwrapped)
}
/// Loads an image table from a file.
public convenience init(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
let pointer = path.withPlaydateCString { gfx.loadBitmapTable.unsafelyUnwrapped($0, &error) }
guard let pointer else { throw PlaydateError(cString: error) }
self.init(pointer: pointer)
}
deinit {
gfx.freeBitmapTable.unsafelyUnwrapped(pointer)
}
/// Replaces the table's contents with the image table at `path`.
public func load(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
path.withPlaydateCString { gfx.loadIntoBitmapTable.unsafelyUnwrapped($0, pointer, &error) }
if let error { throw PlaydateError(cString: error) }
}
/// The bitmap at `index`, or `nil` if out of range. The bitmap
/// references storage owned by the table; keep the table alive while
/// using it.
public func bitmap(at index: Int) -> Bitmap? {
guard let bitmap = gfx.getTableBitmap.unsafelyUnwrapped(pointer, Int32(index)) else {
return nil
}
return Bitmap(pointer: bitmap, isOwned: false)
}
/// The number of bitmaps in the table and the number of cells per row
/// of the source image.
public var info: (count: Int, cellsWide: Int) {
var count: Int32 = 0, width: Int32 = 0
gfx.getBitmapTableInfo.unsafelyUnwrapped(pointer, &count, &width)
return (Int(count), Int(width))
}
public var count: Int { info.count }
}
}