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

indigo.shared.formats.Aseprite.scala Maven / Gradle / Ivy

The newest version!
package indigo.shared.formats

import indigo.shared.IndigoLogger
import indigo.shared.animation.Animation
import indigo.shared.animation.AnimationKey
import indigo.shared.animation.Cycle
import indigo.shared.animation.CycleLabel
import indigo.shared.animation.Frame
import indigo.shared.assets.AssetName
import indigo.shared.collections.Batch
import indigo.shared.collections.NonEmptyList
import indigo.shared.datatypes.BindingKey
import indigo.shared.datatypes.Depth
import indigo.shared.datatypes.Flip
import indigo.shared.datatypes.Point
import indigo.shared.datatypes.Radians
import indigo.shared.datatypes.Rectangle
import indigo.shared.datatypes.Size
import indigo.shared.datatypes.Vector2
import indigo.shared.dice.Dice
import indigo.shared.events.GlobalEvent
import indigo.shared.materials.Material
import indigo.shared.scenegraph.Clip
import indigo.shared.scenegraph.ClipPlayMode
import indigo.shared.scenegraph.ClipSheet
import indigo.shared.scenegraph.ClipSheetArrangement
import indigo.shared.scenegraph.Sprite
import indigo.shared.time.Millis
import indigo.shared.time.Seconds

final case class Aseprite(frames: List[AsepriteFrame], meta: AsepriteMeta) derives CanEqual:
  def toSpriteAndAnimations(dice: Dice, assetName: AssetName): Option[SpriteAndAnimations] =
    Aseprite.toSpriteAndAnimations(this, dice, assetName)

  def toClips(assetName: AssetName): Option[Map[CycleLabel, Clip[Material.Bitmap]]] =
    Aseprite.toClips(this, assetName)

final case class AsepriteFrame(
    filename: String,
    frame: AsepriteRectangle,
    rotated: Boolean,
    trimmed: Boolean,
    spriteSourceSize: AsepriteRectangle,
    sourceSize: AsepriteSize,
    duration: Int
) derives CanEqual

final case class AsepriteRectangle(x: Int, y: Int, w: Int, h: Int) derives CanEqual:
  def position: Point = Point(x, y)
  def size: Size      = Size(w, h)

final case class AsepriteMeta(
    app: String,
    version: String,
    format: String,
    size: AsepriteSize,
    scale: String,
    frameTags: List[AsepriteFrameTag]
) derives CanEqual

final case class AsepriteSize(w: Int, h: Int) derives CanEqual:
  def toSize: Size = Size(w, h)

final case class AsepriteFrameTag(name: String, from: Int, to: Int, direction: String) derives CanEqual

final case class SpriteAndAnimations(sprite: Sprite[Material.Bitmap], animations: Animation) derives CanEqual:
  def modifySprite(alter: Sprite[Material.Bitmap] => Sprite[Material.Bitmap]): SpriteAndAnimations =
    this.copy(sprite = alter(sprite))

object Aseprite:

  def toSpriteAndAnimations(aseprite: Aseprite, dice: Dice, assetName: AssetName): Option[SpriteAndAnimations] =
    extractCycles(aseprite) match {
      case Nil =>
        IndigoLogger.info("No animation frames found in Aseprite")
        None
      case x :: xs =>
        val animations: Animation =
          Animation(
            animationKey = AnimationKey.fromDice(dice),
            currentCycleLabel = x.label,
            cycles = NonEmptyList.pure(x, xs)
          )
        Option(
          SpriteAndAnimations(
            Sprite(
              bindingKey = BindingKey.fromDice(dice),
              material = Material.Bitmap(assetName),
              animationKey = animations.animationKey,
              animationActions = Batch.empty,
              eventHandlerEnabled = false,
              eventHandler = Function.const(None),
              position = Point(0, 0),
              rotation = Radians.zero,
              scale = Vector2.one,
              depth = Depth.zero,
              ref = Point(0, 0),
              flip = Flip.default
            ),
            animations
          )
        )
    }

  def toClips(aseprite: Aseprite, assetName: AssetName): Option[Map[CycleLabel, Clip[Material.Bitmap]]] =
    extractClipData(aseprite)
      .map(
        _.map { clipData =>
          clipData.label ->
            Clip(
              size = clipData.size,
              sheet = clipData.sheet,
              playMode = ClipPlayMode.default,
              material = Material.Bitmap(assetName),
              eventHandlerEnabled = false,
              eventHandler = Function.const(None),
              position = Point.zero,
              rotation = Radians.zero,
              scale = Vector2.one,
              depth = Depth.zero,
              ref = Point.zero,
              flip = Flip.default
            )
        }
      )
      .map(_.toMap)

  private def extractCycles(aseprite: Aseprite): List[Cycle] =
    aseprite.meta.frameTags
      .map { frameTag =>
        extractFrames(frameTag, aseprite.frames) match {
          case Nil =>
            IndigoLogger.info(s"Failed to extract cycle with frameTag: ${frameTag.toString()}")
            None
          case x :: xs =>
            Option(
              Cycle.create(frameTag.name, NonEmptyList.pure(x, xs))
            )
        }
      }
      .collect { case Some(s) => s }

  private def extractFrames(frameTag: AsepriteFrameTag, asepriteFrames: List[AsepriteFrame]): List[Frame] =
    asepriteFrames.slice(frameTag.from, frameTag.to + 1).map { aseFrame =>
      Frame(
        crop = Rectangle(
          position = Point(aseFrame.frame.x, aseFrame.frame.y),
          size = Size(aseFrame.frame.w, aseFrame.frame.h)
        ),
        duration = Millis(aseFrame.duration.toLong)
      )
    }

  private def extractClipData(aseprite: Aseprite): Option[List[ClipData]] =
    aseprite.frames match
      case f :: Nil =>
        Option(
          aseprite.meta.frameTags
            .map { frameTag =>
              val sheet =
                ClipSheet(
                  frameCount = (frameTag.to - frameTag.from) + 1,
                  frameDuration = Millis(f.duration.toLong).toSeconds,
                  wrapAt = 1,
                  arrangement = ClipSheetArrangement.Horizontal,
                  startOffset = frameTag.from
                )

              ClipData(CycleLabel(frameTag.name), f.frame.size, sheet)
            }
        )

      case f1 :: f2 :: _ =>
        val arrangement: ClipSheetArrangement =
          if f2.frame.x > f1.frame.x then ClipSheetArrangement.Horizontal
          else ClipSheetArrangement.Vertical

        val wrapAt: Int =
          arrangement match
            case ClipSheetArrangement.Horizontal =>
              aseprite.meta.size.toSize.width / f1.frame.w

            case ClipSheetArrangement.Vertical =>
              aseprite.meta.size.toSize.height / f1.frame.h

        Option(
          aseprite.meta.frameTags
            .map { frameTag =>
              val sheet =
                ClipSheet(
                  frameCount = (frameTag.to - frameTag.from) + 1,
                  frameDuration = Millis(f1.duration.toLong).toSeconds,
                  wrapAt = wrapAt,
                  arrangement = arrangement,
                  startOffset = frameTag.from
                )

              ClipData(CycleLabel(frameTag.name), f1.frame.size, sheet)
            }
        )

      case Nil =>
        IndigoLogger.info(s"No frames were found during Aseprite converstion to Clips")
        None

final case class ClipData(label: CycleLabel, size: Size, sheet: ClipSheet)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy