All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.pawelkrol.Afterimage.Mode.HiRes.scala Maven / Gradle / Ivy

The newest version!
package com.github.pawelkrol.Afterimage
package Mode

import Mode.Data.{Bitmap,Screen}
import Mode.Data.Row.{HiResRow,Row}
import Util.Util

/** HiRes image data abstraction providing convenient access to the bitmap and screen colours.
  *
  * @constructor create a new `HiRes` image data
  * @param bitmap plain hi-resolution bitmap data of an image
  * @param screen optional screen colours data of an image
  * @param border optional single byte of image border colour
  */
case class HiRes(
  val bitmap: Bitmap,
  val screen: Option[Screen],
  val border: Option[Byte]
) extends CBM {

  /** An actual pixel width of this HiRes image. */
  val width = HiRes.maxWidth

  /** An actual pixel height of this HiRes image. */
  val height = HiRes.maxHeight

  /** Image pixel width rounded up to the right margin of an 8x8 character. */
  val widthRounded = Util.roundSizeToChar(width)

  /** Image pixel height rounded up to the bottom margin of an 8x8 character. */
  val heightRounded = Util.roundSizeToChar(height)

  /** An actual char width of this HiRes image. */
  val numCharCols = widthRounded / 8

  /** An actual char height of this HiRes image. */
  val numCharRows = heightRounded / 8

  validate()

  /** Validates consistency of an object instance data. */
  def validate(): Unit = {
    require(bitmap.get().length == HiRes.size("bitmap"), "Invalid %s image data".format(imageMode))

    screen match {
      case Some(scr) =>
        require(scr.get().length == HiRes.size("screen"), "Invalid %s image data".format(imageMode))
      case None =>
    }
  }

  private def colour(x: Int, y: Int, isPixelSet: Boolean): Byte = {
    val colour = screen match {
      case Some(data) =>
        if (isPixelSet) (data(x, y) & 0xf0) >> 4 else data(x, y) & 0x0f
      case None =>
        if (isPixelSet) HiRes.defaultColour else HiRes.defaultBackground
    }
    colour.toByte
  }

  /** Returns the C64 colour of the pixel at [x,y].
    *
    * @param x X coordinate of a requested pixel
    * @param y Y coordinate of a requested pixel
    */
  def pixel(x: Int, y: Int) = {
    validatePixelCoordinates(x, y)

    val isPixelSet: Boolean = bitmap.isPixelSet(x, y)

    val (screenX, screenY) = pixelCoordinatesToScreen(x, y)
    colour(screenX, screenY, isPixelSet)
  }

  /** Returns a new HiRes image with all pixels of bitmap data bits inverted. */
  def invert() = {
    val inverted = bitmap.get().map(byte => (byte ^ 0xff).toByte)

    new HiRes(Bitmap(inverted, numCharCols, numCharRows), screen, border)
  }

  private def sliceOffsetsToScreen(x: Int, y: Int, width: Int, height: Int): Tuple4[Int, Int, Int, Int] = {

    val screenX = (x & 0xfff8) >> 3
    val screenY = (y & 0xf8) >> 3
    val screenWidth = (width & 0xfff8) >> 3
    val screenHeight = (height & 0xfff8) >> 3

    (screenX, screenY, screenWidth, screenHeight)
  }

  /** Returns a new HiResSlice instance with truncated contents of the image.
   *
   * @param x X coordinate of the top-left corner of a rectangular selection area
   * @param y Y coordinate of the top-left corner of a rectangular selection area
   * @param newWidth total width of a rectangular selection area in number of pixels
   * @param newHeight total height of a rectangular selection area in number of pixels
   */
  def slice(x: Int, y: Int, newWidth: Int, newHeight: Int) = {

    // Check that top-left corner of a slice is within an image data area:
    validatePixelCoordinates(x, y)

    // Check that bottom-right corner of a slice is within an image data area:
    validatePixelCoordinates(x + newWidth - 1, y + newHeight - 1)

    screen match {
      case Some(scr) => {
        // Validate start and end position alignment to char boundaries:
        require(
          x % 8 == 0 && y % 8 == 0,
          "Invalid slice coordinates requested: got %s, but expected %s".format(
            "[%d,%d]".format(x, y),
            "values aligned with an 8x8 char boundaries"
          )
        )

        // Validate newWidth and newHeight values:
        require(
          x + newWidth <= width && y + newHeight <= height,
          "Invalid slice dimensions requested: got %s, but at this position maximum allowed size is %s".format(
            "[%d,%d]".format(newWidth, newHeight),
            "[%d,%d]".format(width, height)
          )
        )

        val newBitmap = bitmap.slice(x, y, newWidth, newHeight)
        val newScreen = screen match {
          case Some(data) => {
            val (scrX, scrY, scrNewWidth, scrNewHeight) = sliceOffsetsToScreen(x, y, newWidth, newHeight)
            Some(data.slice(scrX, scrY, scrNewWidth, scrNewHeight))
          }
          case None => None
        }

        HiResSlice(newBitmap, newScreen, border, newWidth, newHeight)
      }
      case None => {
        val newBitmap = bitmap.slice(x, y, newWidth, newHeight)

        HiResSlice(
          newBitmap,
          None,
          border,
          Util.roundSizeToChar(newWidth),
          Util.roundSizeToChar(newHeight)
        )
      }
    }
  }

  /** A fallback method returning default screen colour values (1000 bytes filled with 0xbc). */
  val emptyScreen = Array.fill(numCharRows * numCharCols){HiRes.defaultScreenValue}

  /** Returns image data as an array of hires rows ([[com.github.pawelkrol.Afterimage.Mode.Data.Row.HiResRow]] objects).
    *
    * Note that fetching rows data from an image slice will also always return an array of full rows with 40 columns length each!
    */
  def rows = {
    val screenData = screen match {
      case Some(scr) => scr.get()
      case None => emptyScreen
    }

    (0 to numCharRows - 1).map(row =>
      HiResRow(
        Row.getBitmapRow(row, bitmap.get(), widthRounded),
        Row.getScreenRow(row, screenData, numCharCols)
      )
    ).toArray
  }

  def canEqual(that: Any) = that.isInstanceOf[HiRes]

  override def equals(other: Any) = other match {
    case that: HiRes =>
      (that canEqual this) && (this.bitmap == that.bitmap) && (this.screen == that.screen) && (this.border == that.border)
    case _ =>
      false
  }
}

/** Factory for [[com.github.pawelkrol.Afterimage.Mode.HiRes]] instances. */
object HiRes {

  /** Maximum possible pixel width of a HiRes image. */
  val maxWidth = 320

  /** Maximum possible pixel height of a HiRes image. */
  val maxHeight = 200

  /** Default size of raw byte arrays comprising a HiRes image. */
  val size = Map[String,Int](
    "bitmap" -> 0x1f40,
    "screen" -> 0x03e8
  )

  /** Default colour of a drawn pixel. */
  val defaultColour = 0x0b

  /** Default colour of a blank pixel. */
  val defaultBackground = 0x0c

  /** Default byte value of a HiRes screen data. */
  val defaultScreenValue = ((defaultColour << 4) | defaultBackground).toByte

  /** Creates an empty HiRes image. */
  def apply(): HiRes =
    HiRes(
      bitmap = Array.fill(HiRes.size("bitmap")){0x00}
    )

  /** Creates a new HiRes image with a given bitmap data.
    *
    * @param bitmap array of 8000 raw bytes with image bitmap data
    */
  def apply(bitmap: Array[Byte]): HiRes =
    HiRes(
      Bitmap(bitmap, Bitmap.maxCols, Bitmap.maxRows),
      None,
      None
    )

  /** Creates a new HiRes image with a given bitmap data and border colour.
    *
    * @param bitmap array of 8000 raw bytes with image bitmap data
    * @param border single byte of image border colour
    */
  def apply(bitmap: Array[Byte], border: Byte): HiRes =
    HiRes(
      Bitmap(bitmap, Bitmap.maxCols, Bitmap.maxRows),
      None,
      Some(border)
    )

  /** Creates a new HiRes image with a given bitmap data and screen and border colours.
    *
    * @param bitmap array of 8000 raw bytes with image bitmap data
    * @param screen array of 1000 raw bytes with image screen data
    * @param border single byte of image border colour
    */
  def apply(bitmap: Array[Byte], screen: Array[Byte], border: Byte): HiRes =
    HiRes(
      Bitmap(bitmap, Bitmap.maxCols, Bitmap.maxRows),
      Some(Screen(screen, Screen.maxCols, Screen.maxRows)),
      Some(border)
    )

  /** Creates a new HiRes image with a given bitmap data and screen colours.
    *
    * @param bitmap array of 8000 raw bytes with image bitmap data
    * @param screen array of 1000 raw bytes with image screen data
    */
  def apply(bitmap: Array[Byte], screen: Array[Byte]): HiRes =
    HiRes(
      Bitmap(bitmap, Bitmap.maxCols, Bitmap.maxRows),
      Some(Screen(screen, Screen.maxCols, Screen.maxRows)),
      None
    )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy