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