adproperties now has transforms list; assembler can make transformed skeleton

new fun: LinearSearch(By)
This commit is contained in:
minjaesong
2019-01-06 22:43:50 +09:00
parent 7978d8b8e0
commit d6075b4334
8 changed files with 145 additions and 40 deletions

View File

@@ -1,5 +1,6 @@
package net.torvald.spriteassembler package net.torvald.spriteassembler
import net.torvald.terrarum.linearSearchBy
import java.io.InputStream import java.io.InputStream
import java.io.Reader import java.io.Reader
import java.util.* import java.util.*
@@ -18,19 +19,27 @@ data class Animation(val name: String, val delay: Float, val row: Int, val frame
override fun toString() = "$name delay: $delay, row: $row, frames: $frames, skeleton: ${skeleton.name}" override fun toString() = "$name delay: $delay, row: $row, frames: $frames, skeleton: ${skeleton.name}"
} }
/** Later the 'translate' can be changed so that it represents affine transformation (Matrix2d) */
data class Transform(val joint: Joint, val translate: ADPropertyObject.Vector2i) {
override fun toString() = "$joint transform: $translate"
fun getTransformVector() = joint.position + translate
}
class ADProperties { class ADProperties {
private val javaProp = Properties() private val javaProp = Properties()
/** Every key is CAPITALISED */ /** Every key is CAPITALISED */
private val propTable = HashMap<String, List<ADPropertyObject>>() private val propTable = HashMap<String, List<ADPropertyObject>>()
/** list of bodyparts used by all the skeletons */ /** list of bodyparts used by all the skeletons (HEAD, UPPER_TORSO, LOWER_TORSO) */
lateinit var bodyparts: List<String>; private set lateinit var bodyparts: List<String>; private set
lateinit var bodypartFiles: List<String>; private set lateinit var bodypartFiles: List<String>; private set
/** properties that are being used as skeletons */ /** properties that are being used as skeletons (SKELETON_STAND) */
lateinit var skeletons: HashMap<String, Skeleton>; private set lateinit var skeletons: HashMap<String, Skeleton>; private set
/** properties that are recognised as animations */ /** properties that are recognised as animations (ANIM_RUN, ANIM)IDLE) */
lateinit var animations: HashMap<String, Animation>; private set lateinit var animations: HashMap<String, Animation>; private set
/** an "animation frame" property (ANIM_RUN_1, ANIM_RUN_2) */
lateinit var transforms: HashMap<String, List<Transform>>; private set
private val reservedProps = listOf("SPRITESHEET", "EXTENSION") private val reservedProps = listOf("SPRITESHEET", "EXTENSION")
private val animMustContain = listOf("DELAY", "ROW", "SKELETON") private val animMustContain = listOf("DELAY", "ROW", "SKELETON")
@@ -40,6 +49,8 @@ class ADProperties {
private val animFrameSuffixRegex = Regex("""_[0-9]+""") private val animFrameSuffixRegex = Regex("""_[0-9]+""")
private val ALL_JOINT = Joint("ALL", ADPropertyObject.Vector2i(0, 0))
constructor(reader: Reader) { constructor(reader: Reader) {
javaProp.load(reader) javaProp.load(reader)
continueLoad() continueLoad()
@@ -59,13 +70,14 @@ class ADProperties {
} }
// set reserved values for the animation: filename, extension // set reserved values for the animation: filename, extension
baseFilename = get("SPRITESHEET")!![0].variable baseFilename = get("SPRITESHEET")[0].variable
extension = get("EXTENSION")!![0].variable extension = get("EXTENSION")[0].variable
val bodyparts = HashSet<String>() val bodyparts = HashSet<String>()
val skeletons = HashMap<String, Skeleton>() val skeletons = HashMap<String, Skeleton>()
val animations = HashMap<String, Animation>() val animations = HashMap<String, Animation>()
val animFrames = HashMap<String, Int>() val animFrames = HashMap<String, Int>()
val transforms = HashMap<String, List<Transform>>()
// scan every props, write down anim frames for later use // scan every props, write down anim frames for later use
propTable.keys.forEach { propTable.keys.forEach {
if (animFrameSuffixRegex.containsMatchIn(it)) { if (animFrameSuffixRegex.containsMatchIn(it)) {
@@ -94,16 +106,16 @@ class ADProperties {
// if it is indeed anim, populate animations list // if it is indeed anim, populate animations list
if (propsHashMap.containsKey("SKELETON")) { if (propsHashMap.containsKey("SKELETON")) {
val skeletonName = propsHashMap["SKELETON"] as String val skeletonName = propsHashMap["SKELETON"] as String
val skeletonDef = get(skeletonName) ?: throw Error("Skeleton definition for $skeletonName not found") val skeletonDef = get(skeletonName)
skeletons.put(skeletonName, Skeleton(skeletonName, skeletonDef.toJoints())) skeletons[skeletonName] = Skeleton(skeletonName, skeletonDef.toJoints())
animations.put(s, Animation( animations[s] = Animation(
s, s,
propsHashMap["DELAY"] as Float, propsHashMap["DELAY"] as Float,
(propsHashMap["ROW"] as Float).toInt(), (propsHashMap["ROW"] as Float).toInt(),
animFrames[s]!!, animFrames[s]!!,
Skeleton(skeletonName, skeletonDef.toJoints()) Skeleton(skeletonName, skeletonDef.toJoints())
)) )
} }
} }
@@ -114,13 +126,44 @@ class ADProperties {
} }
} }
// populate transforms
animations.forEach { t, u ->
for (fc in 1..u.frames) {
val frameName = "${t}_$fc"
val prop = get(frameName)
var emptyList = prop.size == 1 && prop[0].variable.isEmpty()
val transformList = if (!emptyList) {
List(prop.size) { index ->
val jointNameToSearch = prop[index].variable.toUpperCase()
val joint = if (jointNameToSearch == "ALL")
ALL_JOINT
else
u.skeleton.joints.linearSearchBy { it.name == jointNameToSearch }
?: throw NullPointerException("No such joint: $jointNameToSearch")
val translate = prop[index].input as ADPropertyObject.Vector2i
Transform(joint, translate)
}
}
else {
// to make real empty list
List(0) { Transform(ALL_JOINT, ADPropertyObject.Vector2i(0, 0)) }
}
transforms[frameName] = transformList
}
}
this.bodyparts = bodyparts.toList().sorted() this.bodyparts = bodyparts.toList().sorted()
this.skeletons = skeletons this.skeletons = skeletons
this.animations = animations this.animations = animations
this.bodypartFiles = this.bodyparts.map { toFilename(it) } this.bodypartFiles = this.bodyparts.map { toFilename(it) }
this.transforms = transforms
} }
operator fun get(identifier: String) = propTable[identifier.toUpperCase()] operator fun get(identifier: String) = propTable[identifier.toUpperCase()]!!
val keys val keys
get() = propTable.keys get() = propTable.keys
fun containsKey(key: String) = propTable.containsKey(key) fun containsKey(key: String) = propTable.containsKey(key)
@@ -129,14 +172,14 @@ class ADProperties {
fun toFilename(partName: String) = fun toFilename(partName: String) =
"${this.baseFilename}${partName.toLowerCase()}${this.extension}" "${this.baseFilename}${partName.toLowerCase()}${this.extension}"
fun getAnimByFrameName(frameName: String): Animation { fun getAnimByFrameName(frameName: String) = animations[getAnimNameFromFrame(frameName)]!!
return animations[getAnimNameFromFrame(frameName)]!! fun getSkeleton(name: String) = skeletons[name]!!
} fun getTransform(name: String) = transforms[name]!!
private fun getAnimNameFromFrame(s: String) = s.substring(0 until s.lastIndexOf('_')) private fun getAnimNameFromFrame(s: String) = s.substring(0 until s.lastIndexOf('_'))
private fun List<ADPropertyObject>.toJoints() = List(this.size) { private fun List<ADPropertyObject>.toJoints() = List(this.size) {
Joint(this[it].variable, this[it].input!! as ADPropertyObject.Vector2i) Joint(this[it].variable.toUpperCase(), this[it].input!! as ADPropertyObject.Vector2i)
} }
} }
@@ -167,7 +210,7 @@ class ADPropertyObject(propertyRaw: String) {
if (isADvariable(propertyRaw)) { if (isADvariable(propertyRaw)) {
variable = propPair[0] variable = propPair[0]
val inputStr = propPair[1]!! val inputStr = propPair[1]
if (isADivec2(inputStr)) { if (isADivec2(inputStr)) {
type = ADPropertyType.IVEC2 type = ADPropertyType.IVEC2
@@ -211,7 +254,9 @@ class ADPropertyObject(propertyRaw: String) {
} }
data class Vector2i(var x: Int, var y: Int) { data class Vector2i(var x: Int, var y: Int) {
override fun toString() = "$x, $y" override fun toString() = "($x, $y)"
operator fun plus(other: Vector2i) = Vector2i(this.x + other.x, this.y + other.y)
} }
enum class ADPropertyType { enum class ADPropertyType {

View File

@@ -97,6 +97,14 @@ Remember that 'variables' are contained within 'properties'
|ROW|variable: float|which row the animation goes in the spritesheet| |ROW|variable: float|which row the animation goes in the spritesheet|
|SKELETON|variable: string_pair|Which skeleton this animation uses |SKELETON|variable: string_pair|Which skeleton this animation uses
#### Transforms
Things like ```LEG_RIGHT -1,0``` within ```ANIM_RUN_3``` are called 'Transform'
|Name|Type|Meaning|
|---|---|---|
|ALL|variable: ivec2|Shifts (translates) everything by set value|
### Notes ### Notes
* All indices are one-based * All indices are one-based
@@ -104,6 +112,7 @@ Remember that 'variables' are contained within 'properties'
## Operation ## Operation
* Each line describes transformation * Each line describes transformation
* Transformation are applied sequentially from left to right. In other words, their order matters. Be wary of the clipping that may occur! * Transformation are applied sequentially from left to right. In other words, their order matters. Be wary of the clipping that may occur! (really?)
* The Field is an identifier the game code -- sprite assembler -- recognises. * The Field is an identifier the game code -- sprite assembler -- recognises.
* The Field of animation's name is the name the game code looks for. Example: ```this.setAnim("ANIM_RUN")``` * The Field of animation's name is the name the game code looks for. Example: ```this.setAnim("ANIM_RUN")```
* Coord system is Y-Up, meaning Y=0 is the bottommost position.

View File

@@ -1,6 +1,6 @@
package net.torvald.spriteassembler package net.torvald.spriteassembler
import net.torvald.terrarum.AppLoader.printdbg import net.torvald.terrarum.linearSearch
import java.awt.Image import java.awt.Image
import java.awt.Toolkit import java.awt.Toolkit
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
@@ -16,6 +16,7 @@ object AssembleFrameAWT {
operator fun invoke(properties: ADProperties, frameName: String, assembleConfig: AssembleConfig = AssembleConfig()) { operator fun invoke(properties: ADProperties, frameName: String, assembleConfig: AssembleConfig = AssembleConfig()) {
val theAnim = properties.getAnimByFrameName(frameName) val theAnim = properties.getAnimByFrameName(frameName)
val skeleton = theAnim.skeleton.joints.reversed() val skeleton = theAnim.skeleton.joints.reversed()
val transforms = properties.getTransform(frameName)
val bodyparts = Array<Image?>(skeleton.size) { val bodyparts = Array<Image?>(skeleton.size) {
// if file does not exist, null it // if file does not exist, null it
val file = File("assets/" + properties.toFilename(skeleton[it].name)) val file = File("assets/" + properties.toFilename(skeleton[it].name))
@@ -33,9 +34,13 @@ object AssembleFrameAWT {
val canvas = BufferedImage(assembleConfig.fw, assembleConfig.fh, BufferedImage.TYPE_4BYTE_ABGR) val canvas = BufferedImage(assembleConfig.fw, assembleConfig.fh, BufferedImage.TYPE_4BYTE_ABGR)
//printdbg(this, "==============================") println("Frame name: $frameName")
transforms.forEach { println(it) }
properties[frameName].forEach { printdbg(this, it) } println("==========================")
println("Transformed skeleton:")
AssembleFrameBase.makeTransformList(skeleton, transforms).forEach { (name, transform) ->
println("$name transformedOut: $transform")
}
} }
} }
@@ -46,4 +51,28 @@ object AssembleFrameAWT {
* @param ox Origin-X, leftmost point being zero * @param ox Origin-X, leftmost point being zero
* @param oy Origin-Y, bottommost point being zero * @param oy Origin-Y, bottommost point being zero
*/ */
data class AssembleConfig(val fw: Int = 48, val fh: Int = 56, val ox: Int = 29, val oy: Int = 0) data class AssembleConfig(val fw: Int = 48, val fh: Int = 56, val ox: Int = 29, val oy: Int = 0)
object AssembleFrameBase {
/**
* Returns joints list with tranform applied.
* @param skeleton list of joints
* @param transform ordered list of transforms should be applied. First come first serve.
* @return List of pairs that contains joint name on left, final transform value on right
*/
fun makeTransformList(joints: List<Joint>, transforms: List<Transform>): List<Pair<String, ADPropertyObject.Vector2i>> {
// make our mutable list
val transformOutput = ArrayList<Pair<String, ADPropertyObject.Vector2i>>()
joints.forEach {
transformOutput.add(it.name to it.position)
}
// process transform queue
transforms.forEach { transform ->
val jointToMoveIndex = transformOutput.linearSearch { it.first == transform.joint.name }!!
transformOutput[jointToMoveIndex] = transformOutput[jointToMoveIndex].first to transform.getTransformVector()
}
return transformOutput.toList()
}
}

View File

@@ -26,10 +26,11 @@ class SpriteAssemblerApp : JFrame() {
private val panelBodypartsList = JList<String>() private val panelBodypartsList = JList<String>()
private val panelImageFilesList = JList<String>() private val panelImageFilesList = JList<String>()
private val panelSkeletonsList = JList<String>() private val panelSkeletonsList = JList<String>()
private val panelTransformsList = JList<String>()
private val panelCode = JTextPane() private val panelCode = JTextPane()
private val statBar = JTextArea("Null.") private val statBar = JTextArea("Null.")
private var adProperties: ADProperties? = null private lateinit var adProperties: ADProperties
private val props = Properties() private val props = Properties()
private val lang = Properties() private val lang = Properties()
@@ -93,12 +94,14 @@ class SpriteAssemblerApp : JFrame() {
panelBodypartsList.model = DefaultListModel() panelBodypartsList.model = DefaultListModel()
panelImageFilesList.model = DefaultListModel() panelImageFilesList.model = DefaultListModel()
panelSkeletonsList.model = DefaultListModel() panelSkeletonsList.model = DefaultListModel()
panelTransformsList.model = DefaultListModel()
val panelPartsList = JTabbedPane(JTabbedPane.TOP) val panelPartsList = JTabbedPane(JTabbedPane.TOP)
panelPartsList.add("Animations", JScrollPane(panelAnimationsList)) panelPartsList.add("Animations", JScrollPane(panelAnimationsList))
panelPartsList.add("Bodyparts", JScrollPane(panelBodypartsList)) panelPartsList.add("Bodyparts", JScrollPane(panelBodypartsList))
panelPartsList.add("Images", JScrollPane(panelImageFilesList)) panelPartsList.add("Images", JScrollPane(panelImageFilesList))
panelPartsList.add("Skeletons", JScrollPane(panelSkeletonsList)) panelPartsList.add("Skeletons", JScrollPane(panelSkeletonsList))
panelPartsList.add("Transforms", JScrollPane(panelTransformsList))
val panelDataView = JSplitPane(JSplitPane.VERTICAL_SPLIT, JScrollPane(panelProperties), panelPartsList) val panelDataView = JSplitPane(JSplitPane.VERTICAL_SPLIT, JScrollPane(panelProperties), panelPartsList)
@@ -116,7 +119,7 @@ class SpriteAssemblerApp : JFrame() {
val propRoot = DefaultMutableTreeNode("Properties") val propRoot = DefaultMutableTreeNode("Properties")
adProperties?.forEach { s, list -> adProperties.forEach { s, list ->
// build tree node for the properties display // build tree node for the properties display
val propNode = DefaultMutableTreeNode(s) val propNode = DefaultMutableTreeNode(s)
propRoot.add(propNode) propRoot.add(propNode)
@@ -133,21 +136,25 @@ class SpriteAssemblerApp : JFrame() {
panelSkeletonsList.model = DefaultListModel() panelSkeletonsList.model = DefaultListModel()
// populate animations view // populate animations view
adProperties!!.animations.forEach { adProperties.animations.forEach {
(panelAnimationsList.model as DefaultListModel).addElement("${it.value}") (panelAnimationsList.model as DefaultListModel).addElement("${it.value}")
} }
// populate bodyparts view // populate bodyparts view
adProperties!!.bodyparts.forEach { partName -> adProperties.bodyparts.forEach { partName ->
(panelBodypartsList.model as DefaultListModel).addElement(partName) (panelBodypartsList.model as DefaultListModel).addElement(partName)
} }
// populate image file list view // populate image file list view
adProperties!!.bodypartFiles.forEach { partName -> adProperties.bodypartFiles.forEach { partName ->
(panelImageFilesList.model as DefaultListModel).addElement(partName) (panelImageFilesList.model as DefaultListModel).addElement(partName)
} }
// populate skeletons view // populate skeletons view
adProperties!!.skeletons.forEach { adProperties.skeletons.forEach {
(panelSkeletonsList.model as DefaultListModel).addElement("${it.value}") (panelSkeletonsList.model as DefaultListModel).addElement("${it.value}")
} }
// populate transforms view
adProperties.transforms.forEach {
(panelTransformsList.model as DefaultListModel).addElement("$it")
}
} }
catch (fehler: Throwable) { catch (fehler: Throwable) {
displayError("ERROR_PARSE_FAIL", fehler) displayError("ERROR_PARSE_FAIL", fehler)

View File

@@ -21,12 +21,12 @@ SKELETON_STAND=HEADGEAR 0,32;HAIR_FORE 0,32;\
# skeleton_stand is used for testing purpose # skeleton_stand is used for testing purpose
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=LEG_RIGHT 1,-1;LEG_LEFT -1,0 ANIM_RUN_1=LEG_RIGHT 1,1;FOOT_RIGHT 1,1;LEG_LEFT -1,0;FOOT_LEFT -1,0
ANIM_RUN_2=ALL 0,-1;LEG_RIGHT 0,1;LEG_LEFT 0,-1 ANIM_RUN_2=ALL 0,1;LEG_RIGHT 0,-1;FOOT_RIGHT 0,-1;LEG_LEFT 0,1;FOOT_LEFT 0,1
ANIM_RUN_3=LEG_RIGHT -1,0;LEG_LEFT 1,-1 ANIM_RUN_3=LEG_RIGHT -1,0;FOOT_RIGHT -1,0;LEG_LEFT 1,1;FOOT_LEFT 1,1
ANIM_RUN_4=ALL 0,-1;LEG_RIGHT 0,-1;LEG_LEFT 0,1 ANIM_RUN_4=ALL 0,1;LEG_RIGHT 0,1;LEG_LEFT 0,-1
ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND
ANIM_IDLE_1= ANIM_IDLE_1=
! ANIM_IDLE_1 will not make any transformation ! ANIM_IDLE_1 will not make any transformation
ANIM_IDLE_2=UPPER_TORSO 0,-1 ANIM_IDLE_2=UPPER_TORSO 0,-1;HEAD 0,-1;ARM_REST_LEFT 0,-1;HAND_REST_LEFT 0,-1;ARM_REST_RIGHT 0,-1;HAND_REST_RIGHT 0,-1

View File

@@ -724,3 +724,18 @@ fun interpolateLinear(scale: Double, startValue: Double, endValue: Double): Doub
} }
return (1.0 - scale) * startValue + scale * endValue return (1.0 - scale) * startValue + scale * endValue
} }
fun <T> List<T>.linearSearch(selector: (T) -> Boolean): Int? {
this.forEachIndexed { index, it ->
if (selector.invoke(it)) return index
}
return null
}
fun <T> List<T>.linearSearchBy(selector: (T) -> Boolean): T? {
this.forEach {
if (selector.invoke(it)) return it
}
return null
}

View File

@@ -32,15 +32,15 @@ class ADLParsingTest {
# skeleton_stand is used for testing purpose # skeleton_stand is used for testing purpose
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=LEG_RIGHT 1,-1;LEG_LEFT -1,0 ANIM_RUN_1=LEG_RIGHT 1,1;FOOT_RIGHT 1,1;LEG_LEFT -1,0;FOOT_LEFT -1,0
ANIM_RUN_2=ALL 0,-1;LEG_RIGHT 0,1;LEG_LEFT 0,-1 ANIM_RUN_2=ALL 0,1;LEG_RIGHT 0,-1;FOOT_RIGHT 0,-1;LEG_LEFT 0,1;FOOT_LEFT 0,1
ANIM_RUN_3=LEG_RIGHT -1,0;LEG_LEFT 1,-1 ANIM_RUN_3=LEG_RIGHT -1,0;FOOT_RIGHT -1,0;LEG_LEFT 1,1;FOOT_LEFT 1,1
ANIM_RUN_4=ALL 0,-1;LEG_RIGHT 0,-1;LEG_LEFT 0,1 ANIM_RUN_4=ALL 0,1;LEG_RIGHT 0,1;LEG_LEFT 0,-1
ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND
ANIM_IDLE_1= ANIM_IDLE_1=
! ANIM_IDLE_1 will not make any transformation ! ANIM_IDLE_1 will not make any transformation
ANIM_IDLE_2=UPPER_TORSO 0,-1 ANIM_IDLE_2=UPPER_TORSO 0,-1;HEAD 0,-1;ARM_REST_LEFT 0,-1;HAND_REST_LEFT 0,-1;ARM_REST_RIGHT 0,-1;HAND_REST_RIGHT 0,-1
""".trimIndent() """.trimIndent()
operator fun invoke() { operator fun invoke() {

View File

@@ -1,7 +1,7 @@
package net.torvald.terrarum.tests package net.torvald.terrarum.tests
import net.torvald.spriteassembler.ADProperties import net.torvald.spriteassembler.ADProperties
import net.torvald.spriteassembler.AssembleFrameGdxPixmap import net.torvald.spriteassembler.AssembleFrameAWT
import java.io.StringReader import java.io.StringReader
/** /**
@@ -11,7 +11,7 @@ class SpriteAssemblerTest {
operator fun invoke() { operator fun invoke() {
val properties = ADProperties(StringReader(ADLParsingTest().TEST_STR)) val properties = ADProperties(StringReader(ADLParsingTest().TEST_STR))
AssembleFrameGdxPixmap.invoke(properties, "ANIM_RUN_1") AssembleFrameAWT.invoke(properties, "ANIM_RUN_1")
} }
} }