362 lines
No EOL
9.8 KiB
Text
362 lines
No EOL
9.8 KiB
Text
import "luxe: assets" for Assets
|
|
import "luxe: color" for Color
|
|
import "luxe: string" for Str
|
|
import "luxe: render" for Geometry
|
|
import "luxe: draw" for Draw, PathStyle, LineJoin, LineCap
|
|
import "luxe: math" for Math
|
|
import "luxe: lx" for LX
|
|
import "luxe: id" for ID
|
|
|
|
|
|
class Spine{
|
|
bones_by_index{_bones_by_index}
|
|
bones_by_name{_bones_by_name}
|
|
|
|
static parse(id: String) : Spine{
|
|
var asset = LX.read(id + ".spine.lx")["spine"]
|
|
var skeleton = LX.read(asset["skeleton"]+".json")
|
|
var atlas = LX.read(asset["atlas"]+".atlas")
|
|
|
|
var spine_asset = Spine.from_data(skeleton)
|
|
return spine_asset
|
|
}
|
|
|
|
construct from_data(jsonDict: Map){
|
|
init()
|
|
load_skeleton(jsonDict["skeleton"])
|
|
load_bones(jsonDict["bones"])
|
|
load_slots(jsonDict["slots"])
|
|
load_skins(jsonDict["skins"])
|
|
load_animations(jsonDict["animations"])
|
|
//todo: constraints
|
|
}
|
|
|
|
init(){
|
|
_pos = [0, 0]
|
|
_size = [0, 0]
|
|
_fps = 30
|
|
|
|
_bones_by_name = {}
|
|
_bones_by_index = []
|
|
_slots_by_name = {}
|
|
_slots_by_index = []
|
|
_skins = {}
|
|
_animations = {}
|
|
_active_skins = ["default"]
|
|
_active_attachments = []
|
|
_active_animation = "" //todo: tie this into Anim system
|
|
}
|
|
|
|
prototype_skeleton():Map{
|
|
var elements = {}
|
|
|
|
for(bone in _bones_by_index){
|
|
var transform = {"type": "luxe: modifier/transform"}
|
|
if(!Util.approximately_vec(bone.position, [0, 0])) transform["pos"] = [bone.position.x, bone.position.y, 0]
|
|
if(!Util.approximately_num(bone.rotation, 0)) transform["rotation"] = [0, 0, bone.rotation]
|
|
if(!Util.approximately_vec(bone.scale, [1, 1])) transform["scale"] = [bone.scale.x, bone.scale.y, 1]
|
|
if(bone.parent != null) transform["link"] = bone.parent.uuid
|
|
var modifiers = {"transform": transform}
|
|
var entity = {"modifiers": modifiers, "uuid": bone.uuid}
|
|
elements[bone.name] = entity
|
|
}
|
|
|
|
return {"elements":elements}
|
|
}
|
|
|
|
load_skeleton(skeleton_data: Map){
|
|
_pos = [skeleton_data["x"], skeleton_data["y"]]
|
|
_size = [skeleton_data["width"], skeleton_data["height"]]
|
|
_fps = skeleton_data["fps"] || 30
|
|
}
|
|
|
|
load_bones(bones_data: List){
|
|
_bones_by_index.clear()
|
|
for(bone_data in bones_data){
|
|
var bone = SpineBone.from_data(bone_data, _bones_by_name)
|
|
_bones_by_name[bone.name] = bone
|
|
_bones_by_index.add(bone)
|
|
}
|
|
}
|
|
|
|
load_slots(slots_data: List){
|
|
if(slots_data == null) return
|
|
_slots_by_index.clear()
|
|
for(slot_data in slots_data){
|
|
var slot = SpineSlot.from_data(slot_data, _bones_by_name)
|
|
_slots_by_name[slot.name] = slot
|
|
_slots_by_index.add(slot)
|
|
}
|
|
}
|
|
|
|
load_skins(skins_data: List){
|
|
if(skins_data == null) return
|
|
for(skin_data in skins_data){
|
|
var skin = SpineSkin.from_data(skin_data, this)
|
|
_skins[skin.name] = skin
|
|
}
|
|
}
|
|
|
|
load_animations(animation_data: Map){
|
|
//todo
|
|
}
|
|
|
|
|
|
draw_bones(context){draw_bones(context, [1, 0, 0, 1])}
|
|
draw_bones(context, color){
|
|
for(bone in _bones_by_index){
|
|
var pos = bone.position()
|
|
pos = [pos.x+200, pos.y+200]
|
|
var length = bone.length
|
|
var rotation = Math.radians(bone.rotation())
|
|
|
|
var direction = [rotation.cos, rotation.sin] //todo: use matrices - this approach might be lossy with nonuniform scaling
|
|
var target = [pos.x + direction.x * length, pos.y + direction.y * length]
|
|
|
|
var style = PathStyle.new()
|
|
style.color = color
|
|
Draw.line(context, pos.x, pos.y, target.x, target.y, 0, style)
|
|
}
|
|
}
|
|
|
|
draw_outlines(context){draw_outlines(context, [0, 1, 0, 1])}
|
|
draw_outlines(context, color){
|
|
for(skin_id in _active_skins){ //todo: only draw "uppermost" attachments
|
|
_skins[skin_id].draw_outlines(context, color)
|
|
}
|
|
}
|
|
}
|
|
|
|
class Util{
|
|
static approximately_vec(vec1:List, vec2:List):Boolean{
|
|
if(vec1.count != vec2.count) return false
|
|
for(i in 0...vec1.count){
|
|
if(!approximately_num(vec1[i], vec2[i])) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
static approximately_num(value1:Num, value2:Num):Boolean{
|
|
return (value1 - value2).abs < 0.00001
|
|
}
|
|
}
|
|
|
|
class SpineSkin{
|
|
name{_name}
|
|
|
|
construct from_data(skin_data: Map, spine){
|
|
_name = skin_data["name"]
|
|
|
|
_slots = {}
|
|
for(slot in skin_data["attachments"]){
|
|
var attachments = []
|
|
for(attachment_data in slot.value){
|
|
var attachment = SpineAttachment.from_data(attachment_data.value, attachment_data.key, spine)
|
|
if(attachment) attachments.add(attachment)
|
|
}
|
|
_slots[slot.key] = attachments
|
|
}
|
|
|
|
//todo: skins that are not the default skin can have bones/constraints/paths - handle that (http://esotericsoftware.com/spine-json-format/#Attachments)
|
|
}
|
|
|
|
draw_outlines(context, color){
|
|
for(slot in _slots.values){
|
|
for(attachment in slot){
|
|
attachment.draw_outlines(context, color)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class SpineAttachment{
|
|
type{_type}
|
|
type=(value){_type=value}
|
|
|
|
name{_name}
|
|
name=(value){_name=value}
|
|
|
|
static from_data(attachment_data: Map, map_name: String, spine): SpineAttachment{
|
|
var type = attachment_data["type"] || "region"
|
|
var name = attachment_data["name"] || map_name
|
|
|
|
var attachment
|
|
if(type == "mesh"){
|
|
var weighted = attachment_data["vertices"].count > attachment_data["uvs"].count
|
|
if(weighted){
|
|
attachment = SpineSkinnedMeshAttachment.from_data(attachment_data, spine)
|
|
} else {
|
|
attachment = SpineMeshAttachment.from_data(attachment_data)
|
|
}
|
|
} else {
|
|
System.print("Unknown attachment type \"%(type)\"")
|
|
return null
|
|
}
|
|
attachment.name = name
|
|
attachment.type = type
|
|
|
|
return attachment
|
|
}
|
|
}
|
|
|
|
class SpineMeshAttachment is SpineAttachment{
|
|
path{(_path != null) ? _path : name} //use path when set, otherwise fall back to name
|
|
|
|
construct from_data(attachment_data: Map){
|
|
_hull = attachment_data["hull"] //amount of vertices that form the hull - always the first n vertices in the list
|
|
_path = attachment_data["path"]
|
|
//todo: parse tint (RGBA hex)
|
|
|
|
var triangles = attachment_data["triangles"]
|
|
var vertices = attachment_data["vertices"]
|
|
var uvs = attachment_data["uvs"]
|
|
|
|
_vertices = vertices
|
|
_triangles = triangles
|
|
_uvs = uvs
|
|
}
|
|
}
|
|
|
|
class SpineSkinnedMeshAttachment is SpineAttachment{
|
|
path{(_path != null) ? _path : name} //use path when set, otherwise fall back to name
|
|
|
|
construct from_data(attachment_data: Map, spine: Spine){
|
|
_hull = attachment_data["hull"] //amount of vertices that form the hull - always the first n vertices in the list
|
|
_path = attachment_data["path"]
|
|
//todo: parse tint (RGBA hex)
|
|
|
|
var triangles = attachment_data["triangles"]
|
|
var vertices = attachment_data["vertices"]
|
|
var uvs = attachment_data["uvs"]
|
|
|
|
_vertices = []
|
|
var vert_index = 0
|
|
//var uv_index = 0
|
|
while(vert_index<vertices.count){
|
|
var bone_count = vertices[vert_index]
|
|
var bones = []
|
|
vert_index = vert_index+1
|
|
for(ii in 0...bone_count){
|
|
var bone_index = vertices[vert_index]
|
|
var bone = spine.bones_by_index[bone_index]
|
|
var bind_x = vertices[vert_index+1]
|
|
var bind_y = vertices[vert_index+2]
|
|
var weight = vertices[vert_index+3]
|
|
bones.add(SpineBoneWeight.new(bone, [bind_x, bind_y], weight))
|
|
vert_index = vert_index+4
|
|
}
|
|
//var uv = [uvs[uv_index], uvs[uv_index+1]]
|
|
//uv_index = uv_index+2
|
|
_vertices.add(SpineSkinnedVertex.new(null, bones))
|
|
}
|
|
|
|
_uvs = uvs
|
|
_triangles = triangles
|
|
}
|
|
|
|
|
|
draw_outlines(context, color){
|
|
var style = PathStyle.new()
|
|
style.color = color
|
|
var points = (0..._hull).map{|i| _vertices[i].position()}
|
|
.map{|pos| [pos.x + 200, pos.y + 200]}.toList
|
|
points.add(points[0])
|
|
Draw.path(context, points, style, true)
|
|
}
|
|
}
|
|
|
|
class SpineSkinnedVertex{
|
|
bone_weights{_bone_weights}
|
|
//uv{_uv}
|
|
|
|
construct new(uv: List, bones:List){
|
|
//_uv = uv
|
|
_bone_weights = bones
|
|
}
|
|
|
|
position(){
|
|
return _bone_weights
|
|
.map{|weight| [weight.bone.transform(weight.bind_position), weight.weight]}
|
|
.map{|args| [args[0].x * args[1], args[0].y * args[1]]}
|
|
.reduce([0, 0]){|acc, item| [acc.x+item.x, acc.y+item.y]}
|
|
}
|
|
}
|
|
|
|
class SpineBoneWeight{
|
|
bind_position{_bind_position}
|
|
weight{_weight}
|
|
bone{_bone}
|
|
|
|
construct new(bone: SpineBone, bind_position: List, weight: Num){
|
|
_bone = bone
|
|
_bind_position = bind_position
|
|
_weight = weight
|
|
}
|
|
}
|
|
|
|
class SpineBone{
|
|
name{_name}
|
|
|
|
rotation{_rotation}
|
|
position{_position}
|
|
scale{_scale}
|
|
parent{_parent}
|
|
length{_length}
|
|
uuid{_uuid || (_uuid = ID.uuid())} //assign on first access
|
|
|
|
//todo: get good information out of bones
|
|
|
|
construct from_data(bone_data, existing_bones){
|
|
_name = bone_data["name"]
|
|
_parent = bone_data["parent"]
|
|
_parent = _parent && existing_bones[_parent] //we can just take a reference to the parent because parent bones are guaranteed to be before their children
|
|
_length = bone_data["length"] || 0
|
|
_transform = bone_data["transform"] || "normal" //todo: use enum
|
|
_skin = bone_data["skin"] || false
|
|
//all transformation are relative to the parent
|
|
_position = [bone_data["x"] || 0, bone_data["y"] || 0]
|
|
_rotation = bone_data["rotation"] || 0
|
|
_scale = [bone_data["scaleX"] || 1, bone_data["scaleY"] || 1]
|
|
_shear = [bone_data["shearX"] || 0, bone_data["shearY"] || 0]
|
|
//todo: tint of the bone
|
|
}
|
|
|
|
transform(pos){
|
|
var bone = this
|
|
while(bone != null){
|
|
pos = [pos.x * bone.scale.x, pos.y * bone.scale.y]
|
|
Math.rotate(pos, 0, 0, bone.rotation)
|
|
pos = [pos.x + bone.position.x, pos.y + bone.position.y]
|
|
bone = bone.parent
|
|
}
|
|
return pos
|
|
}
|
|
|
|
position(){
|
|
return transform([0, 0])
|
|
}
|
|
|
|
rotation(){
|
|
var bone = this
|
|
var rot = 0
|
|
while(bone != null){
|
|
rot = rot + bone.rotation
|
|
bone = bone.parent
|
|
}
|
|
return rot
|
|
}
|
|
}
|
|
|
|
class SpineSlot{
|
|
name{_name}
|
|
|
|
construct from_data(slot_data, bone_dict){
|
|
_name = slot_data["name"]
|
|
_bone = slot_data["bone"]
|
|
_bone = _bone && bone_dict[_bone]
|
|
//todo: parse color
|
|
//todo: parse dark color
|
|
//todo: consider looking at and understanding how "attachment" works
|
|
_blend = slot_data["blend"] || "normal" //todo: enum
|
|
}
|
|
} |