spine-module/spine.wren
2021-07-10 19:43:40 +02:00

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
}
}