├── README.md ├── animation.gif ├── conf.lua ├── fancymodel.lua ├── gui.lua ├── helper.lua ├── main.lua ├── model.lua ├── models └── turri.model └── screenshot.png /README.md: -------------------------------------------------------------------------------- 1 | # IK 2 | 3 | A character animation editor. The model is made up of a list of polygons. 4 | 5 | The character and its animations for my game [VS](https://github.com/2bt/vs) were created with this tool. 6 | 7 | ![image](animation.gif) 8 | 9 | ![image](screenshot.png) 10 | 11 | 12 | ## Documentation 13 | 14 | I'm gonna mostly describe things here that aren't obvious from the GUI. 15 | 16 | When starting the application, you should provide a name of the model file you want to edit. 17 | Loading and saving will use this name for file operations. 18 | 19 | love . models/turri.model # this will open the example model 20 | 21 | Use the mouse wheel to zoom in and out. 22 | Drag the mouse while pressing the mouse wheel to scroll. 23 | Press `tab` to switch between bone mode and mesh mode. 24 | 25 | 26 | ### Bone mode 27 | 28 | There's always exactly one bone selected at all times. 29 | Right-click to select bones. 30 | 31 | Left-click while holding `C` to create a new bone. 32 | The new bone will become a child of the selected bone. 33 | Press `X` to delete the selected bone. 34 | All child bones will also be deleted. 35 | 36 | Drag the mouse while holding `G` to move the selected bone. 37 | Drag the mouse while holding `R` to rotate the selected bone. 38 | Additionally holding `shift` will give you more fine-grained control. 39 | 40 | Drag the mouse while left-clicking to move the bone around. 41 | This will actually only change the angels of parent bones to achieve IK. 42 | 43 | 44 | ### Mesh mode 45 | 46 | Mesh mode let's you add, remove, and edit polygons. 47 | You can only edit one polygon at the time. 48 | Right-click on a polygon to select it. 49 | This will also select all its vertices. 50 | Right-click someplace where there's no polygon to deselect a polygon. 51 | While there's no polygon selected, left-click while holding `C` to create a new polygon. 52 | 53 | Press `A` to deselect all vertices. 54 | Press `A` again to select all vertices again. 55 | Right-click a vertex to select it. 56 | Drag the mouse while right-clicking to box-select vertices. 57 | Doing this while holding `shift` will add the vertices to the selection. 58 | 59 | Left-click while holding `C` to add a vertex to the polygon. 60 | Press `X` to delete all selected vertices. 61 | 62 | Drag the mouse while holding `G` to move the selected vertices. 63 | Drag the mouse while holding `R` to rotate the selected vertices. 64 | Drag the mouse while holding `S` to scale the selected vertices. 65 | Additionally holding `shift` will give you more fine-grained control. 66 | 67 | Click the *assign* button to assign the selected polygon to the selected bone. 68 | When the bone moves, assigned polygons will move with it. 69 | 70 | -------------------------------------------------------------------------------- /animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bt/ik/27488f0ef92ac2fac98973fe3b4777a60fc6f244/animation.gif -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.window.title = "ik" 3 | t.window.resizable = true 4 | t.window.msaa = 4 5 | end 6 | -------------------------------------------------------------------------------- /fancymodel.lua: -------------------------------------------------------------------------------- 1 | require("model") 2 | 3 | 4 | local keyframe_buffer = {} 5 | 6 | function Model:add_bone(b) 7 | table.insert(self.bones, b) 8 | b.i = #self.bones 9 | for _, k in ipairs(b.kids) do self:add_bone(k) end 10 | end 11 | local function transform_to_global_space(points, bone) 12 | local p = {} 13 | local si = math.sin(bone.global_a) 14 | local co = math.cos(bone.global_a) 15 | for i = 1, #points, 2 do 16 | local x = points[i ] 17 | local y = points[i + 1] 18 | p[i ] = bone.global_x + x * co - y * si 19 | p[i + 1] = bone.global_y + y * co + x * si 20 | end 21 | return p 22 | end 23 | function Model:delete_bone(b) 24 | keyframe_buffer = {} 25 | for _, p in ipairs(self.polys) do 26 | if p.bone == b then 27 | p.bone = nil 28 | p.data = transform_to_global_space(p.data, b) 29 | end 30 | end 31 | for i, k in ipairs(b.parent.kids) do 32 | if k == b then 33 | table.remove(b.parent.kids, i) 34 | break 35 | end 36 | end 37 | for i, p in ipairs(self.bones) do 38 | if p == b then 39 | table.remove(self.bones, i) 40 | for _, k in ipairs(b.kids) do 41 | self:delete_bone(k) 42 | end 43 | break 44 | end 45 | end 46 | for i, b in ipairs(self.bones) do 47 | b.i = i 48 | end 49 | end 50 | function Model:save(name) 51 | local order = {} 52 | for i, b in ipairs(self.bones) do order[b] = i end 53 | local data = { 54 | bones = {}, 55 | polys = {}, 56 | anims = self.anims, 57 | } 58 | for i, p in ipairs(self.polys) do 59 | data.polys[i] = { 60 | data = p.data, 61 | color = p.color, 62 | shade = p.shade, 63 | bone = order[p.bone], 64 | } 65 | end 66 | for i, b in ipairs(self.bones) do 67 | local d = { 68 | x = b.x, 69 | y = b.y, 70 | a = b.a, 71 | parent = order[b.parent], 72 | } 73 | if #b.keyframes > 0 then d.keyframes = b.keyframes end 74 | data.bones[i] = d 75 | end 76 | local file = io.open(name, "w") 77 | file:write(table.tostring(data) .. "\n") 78 | file:close() 79 | end 80 | 81 | -- keyframe stuff 82 | function Bone:insert_keyframe(frame) 83 | local kf 84 | for i, k in ipairs(self.keyframes) do 85 | if k[1] == frame then 86 | kf = k 87 | break 88 | end 89 | if k[1] > frame then 90 | kf = { frame } 91 | table.insert(self.keyframes, i, kf) 92 | break 93 | end 94 | end 95 | if not kf then 96 | kf = { frame } 97 | table.insert(self.keyframes, kf) 98 | end 99 | kf[2] = self.x 100 | kf[3] = self.y 101 | kf[4] = self.a 102 | end 103 | function Model:insert_keyframe(frame) 104 | for _, b in ipairs(self.bones) do 105 | b:insert_keyframe(frame) 106 | end 107 | end 108 | function Bone:delete_keyframe(frame) 109 | for i, k in ipairs(self.keyframes) do 110 | if k[1] == frame then 111 | table.remove(self.keyframes, i) 112 | break 113 | end 114 | end 115 | end 116 | function Model:delete_keyframe(frame) 117 | for _, b in ipairs(self.bones) do 118 | b:delete_keyframe(frame) 119 | end 120 | end 121 | function Model:copy_keyframe(frame) 122 | keyframe_buffer = {} 123 | for _, b in ipairs(self.bones) do 124 | for _, k in ipairs(b.keyframes) do 125 | if k[1] == frame then 126 | table.insert(keyframe_buffer, { k[2], k[3], k[4] }) 127 | break 128 | end 129 | end 130 | end 131 | end 132 | function Model:paste_keyframe(frame) 133 | for i, b in ipairs(self.bones) do 134 | local q = keyframe_buffer[i] 135 | if not q then break end 136 | local kf 137 | for j, k in ipairs(b.keyframes) do 138 | if k[1] == frame then 139 | kf = k 140 | break 141 | end 142 | if k[1] > frame then 143 | kf = { frame } 144 | table.insert(b.keyframes, j, kf) 145 | break 146 | end 147 | end 148 | if not kf then 149 | kf = { frame } 150 | table.insert(b.keyframes, kf) 151 | end 152 | kf[2] = q[1] 153 | kf[3] = q[2] 154 | kf[4] = q[3] 155 | end 156 | self:set_frame(frame) 157 | end 158 | 159 | local keyframe_buffer_x = {} 160 | function Model:copy_keyframe_x(frame, bone) 161 | keyframe_buffer_x = {} 162 | local function cp(bone) 163 | for _, k in ipairs(bone.keyframes) do 164 | if k[1] == frame then 165 | table.insert(keyframe_buffer_x, { k[2], k[3], k[4] }) 166 | break 167 | end 168 | end 169 | for _, b in ipairs(bone.kids) do cp(b) end 170 | end 171 | cp(bone) 172 | end 173 | function Model:paste_keyframe_x(frame, bone) 174 | local index = 1 175 | local function pst(bone) 176 | local q = keyframe_buffer_x[index] 177 | if not q then 178 | print("WARNING: not enough bones copied") 179 | return 180 | end 181 | index = index + 1 182 | local kf 183 | for j, k in ipairs(bone.keyframes) do 184 | if k[1] == frame then 185 | kf = k 186 | break 187 | end 188 | if k[1] > frame then 189 | kf = { frame } 190 | table.insert(bone.keyframes, j, kf) 191 | break 192 | end 193 | end 194 | if not kf then 195 | kf = { frame } 196 | table.insert(bone.keyframes, kf) 197 | end 198 | kf[2] = q[1] 199 | kf[3] = q[2] 200 | kf[4] = q[3] 201 | 202 | for _, b in ipairs(bone.kids) do pst(b) end 203 | end 204 | pst(bone) 205 | self:set_frame(frame) 206 | end 207 | -------------------------------------------------------------------------------- /gui.lua: -------------------------------------------------------------------------------- 1 | local G = love.graphics 2 | 3 | local PADDING = 5 4 | 5 | local colors = { 6 | text = { 1, 1, 1 }, 7 | window = { 0.2, 0.2, 0.2, 0.8 }, 8 | separator = { 0.4, 0.4, 0.4, 0.4 }, 9 | 10 | active = { 0.8, 0.4, 0.4, 0.8 }, 11 | hover = { 0.6, 0.4, 0.4, 0.8 }, 12 | normal = { 0.4, 0.4, 0.4, 0.8 }, 13 | check = { 1, 1, 1, 0.8 }, 14 | 15 | drag_active = { 0.6, 0.4, 0.4, 0.4 }, 16 | drag_hover = { 0.6, 0.4, 0.4, 0.4 }, 17 | drag_normal = { 0.4, 0.4, 0.4, 0.4 }, 18 | drag_handle = { 0.8, 0.4, 0.4, 0.8 }, 19 | } 20 | local function set_color(c) 21 | G.setColor(unpack(colors[c])) 22 | end 23 | 24 | 25 | gui = { 26 | mx = 0, 27 | my = 0, 28 | iw = 0, 29 | ih = 0, 30 | 31 | wheel = 0, 32 | is_mouse_down = false, 33 | was_mouse_clicked = false, 34 | was_key_pressed = {}, 35 | hover_item = nil, 36 | active_item = nil, 37 | windows = { {}, {}, {} }, 38 | } 39 | for _, win in ipairs(gui.windows) do 40 | win.columns = { 41 | { 42 | min_x = 0, 43 | max_x = 0, 44 | min_y = 0, 45 | max_y = 0, 46 | } 47 | } 48 | end 49 | 50 | function gui:get_id(label) 51 | return self.id_prefix .. label 52 | end 53 | function gui:has_focus() 54 | for _, win in ipairs(self.windows) do 55 | local c = win.columns[#win.columns] 56 | local box = { 57 | x = c.min_x, 58 | y = c.min_y, 59 | w = c.max_x - c.min_x + PADDING, 60 | h = c.max_y - c.min_y + PADDING, 61 | } 62 | if self:mouse_in_box(box) then return true end 63 | end 64 | return self.active_item ~= nil 65 | end 66 | function gui:keypressed(k) 67 | self.was_key_pressed[k] = true 68 | end 69 | function gui:wheelmoved(y) 70 | self.wheel = y 71 | return self:has_focus() 72 | end 73 | function gui:mousemoved(x, y, dx, dy) 74 | self.mx = x 75 | self.my = y 76 | return self:has_focus() 77 | end 78 | function gui:select_win(nr) 79 | self.current_window = self.windows[nr] 80 | self.id_prefix = tostring(nr) 81 | end 82 | function gui:item_min_size(w, h) 83 | self.iw = w 84 | self.ih = h 85 | end 86 | function gui:item_box(w, h, pad) 87 | w = math.max(w, self.iw) 88 | h = math.max(h, self.ih) 89 | self.iw = 0 90 | self.ih = 0 91 | 92 | pad = pad or PADDING 93 | local win = self.current_window 94 | local box = {} 95 | if win.same_line then 96 | win.same_line = false 97 | box.x = win.max_cx + pad 98 | box.y = win.min_cy + pad 99 | if win.max_cy - win.min_cy - pad > h then 100 | box.y = box.y + (win.max_cy - win.min_cy - pad - h) / 2 101 | end 102 | win.max_cx = math.max(win.max_cx, box.x + w) 103 | win.max_cy = math.max(win.max_cy, box.y + h) 104 | else 105 | box.x = win.min_cx + pad 106 | box.y = win.max_cy + pad 107 | win.min_cy = win.max_cy 108 | win.max_cx = box.x + w 109 | win.max_cy = box.y + h 110 | end 111 | 112 | local c = win.columns[#win.columns] 113 | c.max_x = math.max(c.max_x, win.max_cx) 114 | c.max_y = math.max(c.max_y, win.max_cy) 115 | box.w = w 116 | box.h = h 117 | return box 118 | end 119 | function gui:mouse_in_box(box) 120 | return self.mx >= box.x and self.mx <= box.x + box.w 121 | and self.my >= box.y and self.my <= box.y + box.h 122 | end 123 | 124 | 125 | -- public functions 126 | function gui:same_line() 127 | self.current_window.same_line = true 128 | end 129 | function gui:begin_frame() 130 | 131 | -- input 132 | local p = self.is_mouse_down 133 | self.is_mouse_down = love.mouse.isDown(1) 134 | self.was_mouse_clicked = self.is_mouse_down and not p 135 | if not self.is_mouse_down then 136 | self.active_item = nil 137 | end 138 | self.hover_item = nil 139 | 140 | 141 | -- draw windows 142 | for _, win in ipairs(self.windows) do 143 | if win.columns then 144 | local c = win.columns[1] 145 | set_color("window") 146 | G.rectangle("fill", c.min_x, c.min_y, c.max_x - c.min_x + PADDING, c.max_y - c.min_y + PADDING, 147 | PADDING) 148 | end 149 | end 150 | 151 | do 152 | -- custom window size and position policies 153 | 154 | -- left window 155 | -- shrink height 156 | self.windows[1].columns[1].max_y = 0 157 | 158 | -- right window 159 | local c = self.windows[2].columns[1] 160 | if c.min_x == 0 then 161 | c.min_x = G.getWidth() 162 | else 163 | c.min_x = G.getWidth() - (c.max_x - c.min_x) - PADDING 164 | end 165 | c.max_x = G.getWidth() - PADDING 166 | c.min_y = 0 167 | c.max_y = 0 168 | 169 | 170 | -- bottom window 171 | local c = self.windows[3].columns[1] 172 | if c.min_y == 0 then 173 | c.min_y = G.getHeight() 174 | else 175 | c.min_y = G.getHeight() - (c.max_y - c.min_y) - PADDING 176 | end 177 | c.max_y = c.min_y 178 | c.max_x = G.getWidth() - PADDING 179 | end 180 | 181 | 182 | 183 | for _, win in ipairs(self.windows) do 184 | local c = win.columns[1] 185 | win.min_cx = c.min_x 186 | win.max_cx = c.min_x 187 | win.min_cy = c.min_y 188 | win.max_cy = c.min_y 189 | end 190 | 191 | self:select_win(1) 192 | end 193 | function gui:end_frame() 194 | self.was_key_pressed = {} 195 | self.wheel = 0 196 | end 197 | function gui:begin_column() 198 | local win = self.current_window 199 | local c = {} 200 | if win.same_line then 201 | c.min_x = win.max_cx 202 | c.min_y = win.min_cy 203 | else 204 | c.min_x = win.min_cx 205 | c.min_y = win.max_cy 206 | end 207 | c.max_x = c.min_x 208 | c.max_y = c.min_y 209 | table.insert(win.columns, c) 210 | win.min_cx = c.min_x 211 | win.max_cx = c.min_x 212 | win.min_cy = c.min_y 213 | win.max_cy = c.min_y 214 | end 215 | function gui:end_column() 216 | local win = self.current_window 217 | local c = table.remove(win.columns) 218 | win.min_cx = c.min_x 219 | win.min_cy = c.min_y 220 | win.max_cx = c.max_x 221 | win.max_cy = c.max_y 222 | local c = win.columns[#win.columns] 223 | c.max_x = math.max(c.max_x, win.max_cx) 224 | c.max_y = math.max(c.max_y, win.max_cy) 225 | end 226 | function gui:separator() 227 | local win = self.current_window 228 | set_color("separator") 229 | if win.same_line then 230 | local box = self:item_box(4, win.max_cy - win.min_cy - PADDING) 231 | G.rectangle("fill", box.x, box.y - PADDING, box.w, box.h + PADDING * 2) 232 | win.same_line = true 233 | else 234 | local c = win.columns[#win.columns] 235 | local box = self:item_box(c.max_x - c.min_x - PADDING, 4) 236 | G.rectangle("fill", box.x - PADDING, box.y, box.w + PADDING * 2, box.h) 237 | end 238 | end 239 | function gui:text(fmt, ...) 240 | local str = fmt:format(...) 241 | local w = G.getFont():getWidth(str) 242 | local box = self:item_box(w, 14) 243 | set_color("text") 244 | G.print(str, box.x, box.y + box.h / 2 - 8) 245 | end 246 | function gui:button(label) 247 | local id = self:get_id(label) 248 | local w = G.getFont():getWidth(label) + 10 249 | local box = self:item_box(w, 20) 250 | 251 | local hover = self:mouse_in_box(box) 252 | if hover then 253 | self.hover_item = id 254 | if self.was_mouse_clicked then 255 | self.active_item = id 256 | end 257 | end 258 | 259 | if id == self.active_item then 260 | set_color("active") 261 | elseif hover then 262 | set_color("hover") 263 | else 264 | set_color("normal") 265 | end 266 | G.rectangle("fill", box.x, box.y, box.w, box.h, PADDING) 267 | 268 | set_color("text") 269 | G.printf(label, box.x, box.y + box.h / 2 - 8, box.w, "center") 270 | 271 | return hover and self.was_mouse_clicked 272 | end 273 | function gui:checkbox(label, t, n) 274 | local id = self:get_id(label) 275 | local w = G.getFont():getWidth(label) + 20 + PADDING 276 | local box = self:item_box(w, 20) 277 | 278 | local hover = self:mouse_in_box(box) 279 | if hover then 280 | self.hover_item = id 281 | if self.was_mouse_clicked then 282 | self.active_item = id 283 | t[n] = not t[n] 284 | end 285 | end 286 | 287 | if id == self.active_item then 288 | set_color("active") 289 | elseif hover then 290 | set_color("hover") 291 | else 292 | set_color("normal") 293 | end 294 | G.rectangle("fill", box.x, box.y, box.h, box.h, PADDING) 295 | 296 | if t[n] then 297 | set_color("check") 298 | G.rectangle("fill", box.x + 5, box.y + 5, box.h - 10, box.h - 10) 299 | end 300 | 301 | set_color("text") 302 | G.print(label, box.x + box.h + PADDING, box.y + box.h / 2 - 8) 303 | 304 | return hover and self.was_mouse_clicked 305 | end 306 | function gui:radio_button(label, v, t) 307 | local id = self:get_id(label) 308 | local w = G.getFont():getWidth(label) + 10 309 | local box = self:item_box(w, 20) 310 | 311 | local hover = self:mouse_in_box(box) 312 | if hover then 313 | self.hover_item = id 314 | if self.was_mouse_clicked then 315 | self.active_item = id 316 | t[1] = v 317 | end 318 | end 319 | 320 | if t[1] == v or id == self.active_item then 321 | set_color("active") 322 | elseif hover then 323 | set_color("hover") 324 | else 325 | set_color("normal") 326 | end 327 | G.rectangle("fill", box.x, box.y, box.w, box.h, PADDING) 328 | 329 | set_color("text") 330 | G.printf(label, box.x, box.y + box.h / 2 - 8, box.w, "center") 331 | 332 | return hover and self.was_mouse_clicked 333 | end 334 | function gui:drag_value(label, t, n, step, min, max, fmt) 335 | local id = self:get_id(label) 336 | local v = t[n] 337 | local text = label .. " " .. fmt:format(v) 338 | local w = G.getFont():getWidth(text) + 10 339 | local box = self:item_box(w, 20) 340 | 341 | local hover = self:mouse_in_box(box) 342 | if hover then 343 | self.hover_item = id 344 | if self.was_mouse_clicked then 345 | self.active_item = id 346 | end 347 | end 348 | 349 | local handle_w = math.max(4, box.w / (1 + (max - min) / step)) 350 | local handle_x = (v - min) / (max - min) * (box.w - handle_w) 351 | 352 | if id == self.active_item then 353 | local x = (self.mx - box.x - handle_w * 0.5) / (box.w - handle_w) 354 | x = min + math.floor(x * (max - min) / step + 0.5) * step 355 | t[n] = clamp(x, min, max) 356 | set_color("drag_active") 357 | elseif hover then 358 | t[n] = clamp(v + step * self.wheel, min, max) 359 | set_color("drag_hover") 360 | else 361 | set_color("drag_normal") 362 | end 363 | G.rectangle("fill", box.x, box.y, box.w, box.h) 364 | 365 | 366 | set_color("drag_handle") 367 | G.rectangle("fill", box.x + handle_x, box.y, handle_w, box.h) 368 | 369 | set_color("text") 370 | G.printf(text, box.x, box.y + box.h / 2 - 8, box.w, "center") 371 | 372 | return v ~= t[n] 373 | end 374 | -------------------------------------------------------------------------------- /helper.lua: -------------------------------------------------------------------------------- 1 | Object = {} 2 | function Object:new(o) 3 | o = o or {} 4 | setmetatable(o, self) 5 | local m = getmetatable(self) 6 | self.__index = self 7 | self.__call = m.__call 8 | self.super = m.__index and m.__index.init 9 | return o 10 | end 11 | setmetatable(Object, { __call = function(self, ...) 12 | local o = self:new() 13 | if o.init then o:init(...) end 14 | return o 15 | end }) 16 | 17 | 18 | function clamp(v, min, max) 19 | return math.max(min, math.min(max, v)) 20 | end 21 | 22 | function length(dx, dy) 23 | return (dx * dx + dy * dy) ^ 0.5 24 | end 25 | 26 | 27 | function distance(ax, ay, bx, by) 28 | return length(bx - ax, by - ay) 29 | end 30 | 31 | 32 | function table.tostring(t) 33 | local buf = {} 34 | local function w(o, s, p) 35 | p = p or s 36 | local t = type(o) 37 | if t == "table" then 38 | buf[#buf + 1] = p .. "{" 39 | if not next(o) 40 | or (o[1] and type(o[1]) == "number") then 41 | for i, a in ipairs(o) do 42 | if i > 1 then buf[#buf+1] = "," end 43 | w(a, "") 44 | end 45 | buf[#buf + 1] = "}" 46 | else 47 | buf[#buf+1] = "\n" 48 | if o[1] then 49 | for i, a in ipairs(o) do 50 | w(a, s .. " ") 51 | buf[#buf+1] = ",\n" 52 | end 53 | else 54 | -- sort keys for cleaner diffs 55 | local keys = {} 56 | for k in pairs(o) do table.insert(keys, k) end 57 | table.sort(keys) 58 | for _, k in ipairs(keys) do 59 | buf[#buf+1] = s .. " " .. k .. "=" 60 | w(o[k], s .. " ", "") 61 | buf[#buf+1] = ",\n" 62 | end 63 | end 64 | if buf[#buf] == "," 65 | or buf[#buf] == "\n" 66 | or buf[#buf] == ",\n" then buf[#buf] = nil end 67 | buf[#buf+1] = "\n" .. s .. "}" 68 | end 69 | elseif t == "string" then 70 | buf[#buf+1] = p .. ("%q"):format(o) 71 | elseif t == "number" then 72 | buf[#buf+1] = p .. ("%g"):format(o) 73 | else 74 | buf[#buf+1] = p .. tostring(o) 75 | end 76 | end 77 | w(t, "") 78 | return table.concat(buf) 79 | end 80 | local function clone(t) 81 | if type(t) ~= "table" then 82 | return t 83 | end 84 | local c = {} 85 | for k, v in pairs(t) do 86 | c[clone(k)] = clone(v) 87 | end 88 | return c 89 | end 90 | table.clone = clone 91 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | require("helper") 2 | require("fancymodel") 3 | require("gui") 4 | 5 | local G = love.graphics 6 | love.keyboard.setKeyRepeat(true) 7 | 8 | -- C64 color palette 9 | local colors = { 10 | { 0, 0, 0 }, 11 | { 1, 1, 1 }, 12 | { 0.41, 0.22, 0.17 }, 13 | { 0.44, 0.64, 0.7 }, 14 | { 0.44, 0.24, 0.53 }, 15 | { 0.35, 0.55, 0.26 }, 16 | { 0.21, 0.16, 0.47 }, 17 | { 0.72, 0.78, 0.44 }, 18 | { 0.44, 0.31, 0.15 }, 19 | { 0.26, 0.22, 0 }, 20 | { 0.6, 0.4, 0.35 }, 21 | { 0.27, 0.27, 0.27 }, 22 | { 0.42, 0.42, 0.42 }, 23 | { 0.6, 0.82, 0.52 }, 24 | { 0.42, 0.37, 0.71 }, 25 | { 0.58, 0.58, 0.58 }, 26 | } 27 | 28 | 29 | local cam = { 30 | x = 0, 31 | y = -100, 32 | zoom = 1, 33 | } 34 | local edit = { 35 | file_name = arg[2] or "save.model", 36 | 37 | -- view options 38 | show_fill = true, 39 | show_grid = true, 40 | show_joints = true, 41 | show_bones = true, 42 | 43 | -- animation 44 | current_anim = nil, 45 | is_playing = false, 46 | speed = 0.5, 47 | frame = 0, 48 | 49 | -- mouse 50 | mx = 0, 51 | my = 0, 52 | 53 | modes = {}, 54 | } 55 | 56 | 57 | local model = Model(edit.file_name) 58 | 59 | 60 | function edit:find_current_anim() 61 | self.current_anim = nil 62 | for _, a in ipairs(model.anims) do 63 | if self.frame >= a.start 64 | and self.frame < a.stop then 65 | self.current_anim = a 66 | break 67 | end 68 | end 69 | end 70 | function edit:set_frame(f) 71 | edit:set_mode("bone") 72 | self.frame = math.max(0, f) 73 | model:set_frame(self.frame) 74 | self:find_current_anim() 75 | end 76 | function edit:update_frame() 77 | if not self.is_playing then return end 78 | local f = self.frame + self.speed 79 | if self.current_anim then 80 | f = self.frame + self.current_anim.speed 81 | if f >= self.current_anim.stop then 82 | if self.current_anim.loop then 83 | f = self.current_anim.start + f - self.current_anim.stop 84 | else 85 | f = self.current_anim.start 86 | self.is_playing = false 87 | end 88 | end 89 | end 90 | self:set_frame(f) 91 | end 92 | function edit:set_playing(p) 93 | self.is_playing = p 94 | if not p then 95 | self:set_frame(math.floor(self.frame + 0.5)) 96 | end 97 | end 98 | function edit:set_mode(m) 99 | if self.mode then 100 | self.mode:exit() 101 | end 102 | self.modes[m]:enter() 103 | end 104 | 105 | 106 | edit.modes.bone = { 107 | ik_length = 2, 108 | selected_bone = model.root, 109 | bone_buffer = nil, -- for copying 110 | } 111 | function edit.modes.bone:enter() 112 | edit.mode = self 113 | end 114 | function edit.modes.bone:exit() 115 | end 116 | function edit.modes.bone:keypressed(k) 117 | end 118 | function edit.modes.bone:mousepressed(x, y, button) 119 | if button == 1 and love.keyboard.isDown("c") then 120 | -- add new bone 121 | local b = self.selected_bone 122 | local si = math.sin(b.global_a) 123 | local co = math.cos(b.global_a) 124 | local dx = edit.mx - b.global_x 125 | local dy = edit.my - b.global_y 126 | local k = Bone(dx * co + dy * si, dy * co - dx * si) 127 | model:add_bone(k) 128 | b:add_kid(k) 129 | k:update() 130 | self.selected_bone = k 131 | elseif button == 2 then 132 | -- select bone 133 | local dist = 10 134 | for _, b in ipairs(model.bones) do 135 | local d = math.max( 136 | math.abs(b.global_x - edit.mx), 137 | math.abs(b.global_y - edit.my)) / cam.zoom 138 | if d < dist then 139 | dist = d 140 | self.selected_bone = b 141 | end 142 | end 143 | end 144 | end 145 | function edit.modes.bone:mousereleased(x, y, button) 146 | end 147 | function edit.modes.bone:mousemoved(x, y, dx, dy) 148 | local function move(dx, dy) 149 | local b = self.selected_bone 150 | local si = math.sin(b.global_a - b.a) 151 | local co = math.cos(b.global_a - b.a) 152 | b.x = b.x + dx * co + dy * si 153 | b.y = b.y + dy * co - dx * si 154 | b:update() 155 | end 156 | 157 | if love.keyboard.isDown("g") then 158 | -- move 159 | move(dx, dy) 160 | 161 | elseif love.keyboard.isDown("r") then 162 | -- rotate 163 | local b = self.selected_bone 164 | local bx = edit.mx - b.global_x 165 | local by = edit.my - b.global_y 166 | local a = math.atan2(bx - dx, by - dy) - math.atan2(bx, by) 167 | if a < -math.pi then a = a + 2 * math.pi end 168 | if a > math.pi then a = a - 2 * math.pi end 169 | b.a = b.a + a 170 | b:update() 171 | 172 | elseif love.mouse.isDown(1) then 173 | -- ik 174 | if not self.selected_bone.parent then 175 | move(dx, dy) 176 | return 177 | end 178 | 179 | local tx = self.selected_bone.global_x + dx 180 | local ty = self.selected_bone.global_y + dy 181 | 182 | local function calc_error() 183 | return distance(self.selected_bone.global_x, self.selected_bone.global_y, tx, ty) 184 | end 185 | 186 | for _ = 1, 200 do 187 | local delta = 0.0005 188 | 189 | local improve = false 190 | local b = self.selected_bone 191 | for _ = 1, self.ik_length do 192 | b = b.parent 193 | if not b then break end 194 | 195 | local e = calc_error() 196 | b.a = b.a + delta 197 | b:update() 198 | if calc_error() > e then 199 | b.a = b.a - delta * 2 200 | b:update() 201 | if calc_error() > e then 202 | b.a = b.a + delta 203 | b:update() 204 | else 205 | improve = true 206 | end 207 | else 208 | improve = true 209 | end 210 | 211 | -- give parents a smaller weight 212 | delta = delta * 1.0 213 | end 214 | if not improve then break end 215 | end 216 | 217 | end 218 | end 219 | function edit.modes.bone:do_gui() 220 | gui:select_win(1) 221 | 222 | gui:separator() 223 | gui:item_min_size(125, 0) 224 | gui:drag_value("IK", self, "ik_length", 1, 1, 5, "%d") 225 | 226 | -- duplicate bone 227 | local function duplicate(bone, parent, src_polys, dst_polys) 228 | local k = Bone(bone.x, bone.y, bone.a) 229 | if parent then parent:add_kid(k) end 230 | for _, poly in ipairs(src_polys) do 231 | if poly.bone == bone then 232 | table.insert(dst_polys, { 233 | data = table.clone(poly.data), 234 | shade = poly.shade, 235 | color = poly.color, 236 | bone = k, 237 | }) 238 | end 239 | end 240 | for i, l in ipairs(bone.kids) do 241 | duplicate(l, k, src_polys, dst_polys) 242 | end 243 | return k 244 | end 245 | gui:item_min_size(60, 0) 246 | if gui:button("copy") then 247 | if self.selected_bone ~= model.root then 248 | self.poly_buffer = {} 249 | self.bone_buffer = duplicate(self.selected_bone, 250 | nil, 251 | model.polys, 252 | self.poly_buffer) 253 | end 254 | end 255 | gui:same_line() 256 | gui:item_min_size(60, 0) 257 | if gui:button("paste") and self.bone_buffer then 258 | self.selected_bone = duplicate(self.bone_buffer, 259 | self.selected_bone, 260 | self.poly_buffer, 261 | model.polys) 262 | model:add_bone(self.selected_bone) 263 | self.selected_bone:update() 264 | end 265 | 266 | gui:item_min_size(60, 0) 267 | if gui:button("delete") 268 | or gui.was_key_pressed["x"] then 269 | -- delete bone 270 | if self.selected_bone.parent then 271 | local k = self.selected_bone 272 | self.selected_bone = k.parent 273 | model:delete_bone(k) 274 | end 275 | end 276 | 277 | 278 | local b = self.selected_bone 279 | gui:text("nr %d", b.i or 0) 280 | gui:text("x %.2f", b.x) 281 | gui:text("y %.2f", b.y) 282 | gui:text("a %.2f°", b.a * 180 / math.pi) 283 | gui:text("X %.2f", b.global_x) 284 | gui:text("Y %.2f", b.global_y) 285 | end 286 | 287 | 288 | local function transform_to_global_space(points, bone) 289 | local p = {} 290 | local si = math.sin(bone.global_a) 291 | local co = math.cos(bone.global_a) 292 | for i = 1, #points, 2 do 293 | local x = points[i ] 294 | local y = points[i + 1] 295 | p[i ] = bone.global_x + x * co - y * si 296 | p[i + 1] = bone.global_y + y * co + x * si 297 | end 298 | return p 299 | end 300 | local function transform_to_local_space(points, bone) 301 | local si = math.sin(bone.global_a) 302 | local co = math.cos(bone.global_a) 303 | local p = {} 304 | for i = 1, #points, 2 do 305 | local dx = points[i ] - bone.global_x 306 | local dy = points[i + 1] - bone.global_y 307 | p[i ] = dx * co + dy * si 308 | p[i + 1] = dy * co - dx * si 309 | end 310 | return p 311 | end 312 | 313 | 314 | edit.modes.mesh = { 315 | poly_index = 0, 316 | selected_vertices = {}, 317 | } 318 | function edit.modes.mesh:enter() 319 | if edit.is_playing then 320 | edit:set_playing(false) 321 | end 322 | edit.mode = self 323 | local poly = model.polys[self.poly_index] 324 | if poly and poly.bone then 325 | poly.data = transform_to_global_space(poly.data, poly.bone) 326 | end 327 | end 328 | function edit.modes.mesh:exit() 329 | local poly = model.polys[self.poly_index] 330 | if poly and poly.bone then 331 | poly.data = transform_to_local_space(poly.data, poly.bone) 332 | end 333 | end 334 | function edit.modes.mesh:select_poly() 335 | local index = self.poly_index 336 | self:exit() 337 | self.poly_index = 0 338 | for i = 1, #model.polys do 339 | if index > 0 then 340 | i = (i + index - 1) % #model.polys + 1 341 | end 342 | local poly = model.polys[i] 343 | local click = false 344 | local data = poly.data 345 | if poly.bone then 346 | data = transform_to_global_space(poly.data, poly.bone) 347 | end 348 | local x1 = data[#data - 1] 349 | local y1 = data[#data] 350 | for j = 1, #data, 2 do 351 | local x2 = data[j] 352 | local y2 = data[j + 1] 353 | local dx = x2 - x1 354 | local dy = y2 - y1 355 | local ex = edit.mx - x1 356 | local ey = edit.my - y1 357 | if (y1 <= edit.my) == (y2 > edit.my) 358 | and ex < dx * ey / dy then 359 | click = not click 360 | end 361 | x1 = x2 362 | y1 = y2 363 | end 364 | 365 | if click then 366 | self.poly_index = i 367 | -- select all vertices 368 | self.selected_vertices = {} 369 | poly.data = data 370 | for i = 1, #poly.data, 2 do 371 | table.insert(self.selected_vertices, i) 372 | end 373 | break 374 | end 375 | end 376 | end 377 | function edit.modes.mesh:calc_selection_center() 378 | local poly = model.polys[self.poly_index] 379 | local cx = 0 380 | local cy = 0 381 | for _, i in ipairs(self.selected_vertices) do 382 | cx = cx + poly.data[i ] 383 | cy = cy + poly.data[i + 1] 384 | end 385 | cx = cx / #self.selected_vertices 386 | cy = cy / #self.selected_vertices 387 | return cx, cy 388 | end 389 | function edit.modes.mesh:keypressed(k) 390 | local poly = model.polys[self.poly_index] 391 | if poly then 392 | 393 | if k == "x" then 394 | -- delete selected vertice 395 | for j = #self.selected_vertices, 1, -1 do 396 | local i = self.selected_vertices[j] 397 | table.remove(poly.data, i) 398 | table.remove(poly.data, i) 399 | end 400 | self.selected_vertices = {} 401 | 402 | -- remove polygon if less than 3 three vertices are left 403 | if #poly.data < 6 then 404 | table.remove(model.polys, self.poly_index) 405 | self.poly_index = 0 406 | end 407 | 408 | elseif k == "a" then 409 | -- toggle select 410 | local v = {} 411 | if #self.selected_vertices == 0 then 412 | for i = 1, #poly.data, 2 do 413 | v[#v + 1] = i 414 | end 415 | end 416 | self.selected_vertices = v 417 | end 418 | end 419 | end 420 | function edit.modes.mesh:mousepressed(x, y, button) 421 | local poly = model.polys[self.poly_index] 422 | if poly then 423 | if button == 1 and love.keyboard.isDown("c") then 424 | -- add new vertex 425 | local index = 1 426 | local min_l = nil 427 | for i = 1, #poly.data, 2 do 428 | local ax = poly.data[i] 429 | local ay = poly.data[i + 1] 430 | local bx = poly.data[(i + 2) % #poly.data] 431 | local by = poly.data[(i + 2) % #poly.data + 1] 432 | local d0 = distance(ax, ay, bx, by) 433 | local d1 = distance(ax, ay, edit.mx, edit.my) 434 | local d2 = distance(bx, by, edit.mx, edit.my) 435 | l = (d1 + d2) / d0 436 | if not min_l or l < min_l then 437 | min_l = l 438 | index = i + 2 439 | end 440 | end 441 | 442 | table.insert(poly.data, index, edit.mx) 443 | table.insert(poly.data, index + 1, edit.my) 444 | self.selected_vertices = { index } 445 | 446 | elseif button == 2 then 447 | -- vertex selection rect 448 | self.sx = edit.mx 449 | self.sy = edit.my 450 | end 451 | 452 | else 453 | if button == 1 and love.keyboard.isDown("c") then 454 | -- create new poly 455 | local s = cam.zoom * 20 456 | table.insert(model.polys, { 457 | data = { 458 | edit.mx - s, edit.my - s, edit.mx + s, edit.my - s, 459 | edit.mx + s, edit.my + s, edit.mx - s, edit.my + s 460 | }, 461 | color = 11, 462 | shade = 1, 463 | bone = nil, 464 | }) 465 | self.poly_index = #model.polys 466 | self.selected_vertices = { 1, 3, 5, 7 } 467 | end 468 | end 469 | end 470 | function edit.modes.mesh:mousereleased(x, y, button) 471 | local poly = model.polys[self.poly_index] 472 | if poly then 473 | if button == 2 then 474 | -- select vertices 475 | local shift = love.keyboard.isDown("lshift", "rshift") 476 | if not shift then 477 | self.selected_vertices = {} 478 | end 479 | if edit.mx == self.sx and edit.my == self.sy then 480 | local dist = 10 481 | local vertex = nil 482 | for i = 1, #poly.data, 2 do 483 | local d = math.max( 484 | math.abs(poly.data[i ] - edit.mx), 485 | math.abs(poly.data[i + 1] - edit.my)) / cam.zoom 486 | if d < dist then 487 | dist = d 488 | vertex = i 489 | end 490 | end 491 | if vertex then 492 | table.insert(self.selected_vertices, vertex) 493 | end 494 | if not shift and #self.selected_vertices == 0 then 495 | self:select_poly() 496 | end 497 | else 498 | local min_x = math.min(edit.mx, self.sx) 499 | local min_y = math.min(edit.my, self.sy) 500 | local max_x = math.max(edit.mx, self.sx) 501 | local max_y = math.max(edit.my, self.sy) 502 | for i = 1, #poly.data, 2 do 503 | local x = poly.data[i] 504 | local y = poly.data[i + 1] 505 | local s = x >= min_x and x <= max_x and y >= min_y and y <= max_y 506 | if s then 507 | table.insert(self.selected_vertices, i) 508 | end 509 | end 510 | end 511 | self.sx = nil 512 | self.sy = nil 513 | end 514 | else 515 | if button == 2 then 516 | -- select poly 517 | self:select_poly() 518 | end 519 | end 520 | end 521 | function edit.modes.mesh:mousemoved(x, y, dx, dy) 522 | local poly = model.polys[self.poly_index] 523 | 524 | if poly then 525 | if love.mouse.isDown(1) or love.keyboard.isDown("g") then 526 | -- move 527 | for _, i in ipairs(self.selected_vertices) do 528 | poly.data[i ] = poly.data[i ] + dx 529 | poly.data[i + 1] = poly.data[i + 1] + dy 530 | end 531 | 532 | elseif love.keyboard.isDown("s") then 533 | -- scale 534 | local cx, cy = self:calc_selection_center() 535 | local l1 = distance(edit.mx, edit.my, cx + dx, cy + dy) 536 | local l2 = distance(edit.mx, edit.my, cx, cy) 537 | local s = l2 / l1 538 | 539 | for _, i in ipairs(self.selected_vertices) do 540 | poly.data[i ] = cx + (poly.data[i ] - cx) * s 541 | poly.data[i + 1] = cy + (poly.data[i + 1] - cy) * s 542 | end 543 | 544 | elseif love.keyboard.isDown("r") then 545 | -- rotate 546 | local cx, cy = self:calc_selection_center() 547 | 548 | local bx = edit.mx - cx 549 | local by = edit.my - cy 550 | local a = math.atan2(bx - dx, by - dy)- math.atan2(bx, by) 551 | if a < -math.pi then a = a + 2 * math.pi end 552 | if a > math.pi then a = a - 2 * math.pi end 553 | local si = math.sin(a) 554 | local co = math.cos(a) 555 | 556 | for _, i in ipairs(self.selected_vertices) do 557 | local dx = poly.data[i ] - cx 558 | local dy = poly.data[i + 1] - cy 559 | poly.data[i ] = cx + dx * co - dy * si 560 | poly.data[i + 1] = cy + dy * co + dx * si 561 | end 562 | end 563 | end 564 | end 565 | function edit.modes.mesh:do_gui() 566 | gui:select_win(1) 567 | 568 | local poly = model.polys[self.poly_index] 569 | if poly then 570 | gui:separator() 571 | 572 | gui:text("vertices %d", #poly.data / 2) 573 | 574 | gui:item_min_size(75, 20) 575 | gui:text("index %d", self.poly_index) 576 | gui:same_line() 577 | gui:item_min_size(20, 0) 578 | if gui:button("<") then 579 | if self.poly_index > 1 then 580 | model.polys[self.poly_index], model.polys[self.poly_index - 1] = 581 | model.polys[self.poly_index - 1], model.polys[self.poly_index] 582 | self.poly_index = self.poly_index - 1 583 | end 584 | end 585 | gui:same_line() 586 | gui:item_min_size(20, 0) 587 | if gui:button(">") then 588 | if self.poly_index < #model.polys then 589 | model.polys[self.poly_index], model.polys[self.poly_index + 1] = 590 | model.polys[self.poly_index + 1], model.polys[self.poly_index] 591 | self.poly_index = self.poly_index + 1 592 | end 593 | end 594 | 595 | gui:item_min_size(125, 0) 596 | gui:drag_value("color", poly, "color", 1, 1, 16, "%d") 597 | gui:item_min_size(125, 0) 598 | gui:drag_value("shade", poly, "shade", 0.05, 0.3, 1.3, "%.2f") 599 | 600 | 601 | gui:item_min_size(60, 0) 602 | if gui:button("flip H") then 603 | local cx, cy = self:calc_selection_center() 604 | for _, i in ipairs(self.selected_vertices) do 605 | poly.data[i] = 2 * cx - poly.data[i] 606 | end 607 | end 608 | gui:same_line() 609 | gui:item_min_size(60, 0) 610 | if gui:button("flip V") then 611 | local cx, cy = self:calc_selection_center() 612 | for _, i in ipairs(self.selected_vertices) do 613 | poly.data[i+1] = 2 * cy - poly.data[i+1] 614 | end 615 | end 616 | 617 | gui:item_min_size(60, 0) 618 | if gui:button("clone") then 619 | if #self.selected_vertices >= 3 then 620 | local p = { 621 | data = {}, 622 | color = poly.color, 623 | shade = poly.shade, 624 | bone = poly.bone, 625 | } 626 | table.insert(model.polys, p) 627 | for i, j in ipairs(self.selected_vertices) do 628 | p.data[#p.data + 1] = poly.data[j ] + cam.zoom * 10 629 | p.data[#p.data + 1] = poly.data[j + 1] + cam.zoom * 10 630 | self.selected_vertices[i] = i * 2 - 1 631 | end 632 | self:exit() 633 | self.poly_index = #model.polys 634 | end 635 | end 636 | 637 | gui:same_line() 638 | gui:item_min_size(60, 0) 639 | if gui:button("snap") then 640 | for _, i in ipairs(self.selected_vertices) do 641 | poly.data[i] = math.floor(poly.data[i] / 10 + 0.5) * 10 642 | poly.data[i+1] = math.floor(poly.data[i+1] / 10 + 0.5) * 10 643 | end 644 | end 645 | 646 | gui:item_min_size(60, 0) 647 | if gui:button("assign") then 648 | poly.bone = edit.modes.bone.selected_bone 649 | end 650 | gui:same_line() 651 | gui:item_min_size(60, 0) 652 | if gui:button("orphan") then 653 | poly.bone = nil 654 | end 655 | 656 | 657 | end 658 | end 659 | 660 | 661 | edit:set_mode("bone") 662 | 663 | 664 | function love.keypressed(k) 665 | gui:keypressed(k) 666 | edit.mode:keypressed(k) 667 | end 668 | function love.mousepressed(x, y, button) 669 | edit.mode:mousepressed(x, y, button) 670 | end 671 | function love.mousereleased(x, y, button) 672 | edit.mode:mousereleased(x, y, button) 673 | end 674 | function love.mousemoved(x, y, dx, dy) 675 | if gui:mousemoved(x, y, dx, dy) then return end 676 | 677 | -- update mouse pos 678 | edit.mx = cam.x + (x - G.getWidth() / 2) * cam.zoom 679 | edit.my = cam.y + (y - G.getHeight() / 2) * cam.zoom 680 | 681 | -- scale movement 682 | dx = dx * cam.zoom 683 | dy = dy * cam.zoom 684 | if love.keyboard.isDown("lshift", "rshift") then 685 | dx = dx * 0.1 686 | dy = dy * 0.1 687 | end 688 | 689 | -- move camera 690 | if love.mouse.isDown(3) then 691 | cam.x = cam.x - dx 692 | cam.y = cam.y - dy 693 | return 694 | end 695 | 696 | edit.mode:mousemoved(x, y, dx, dy) 697 | end 698 | function love.wheelmoved(_, y) 699 | if gui:wheelmoved(y) then return end 700 | cam.zoom = cam.zoom * (0.9 ^ y) 701 | 702 | -- update mouse pos 703 | local x, y = love.mouse.getPosition() 704 | edit.mx = cam.x + (x - G.getWidth() / 2) * cam.zoom 705 | edit.my = cam.y + (y - G.getHeight() / 2) * cam.zoom 706 | end 707 | function love.update() 708 | edit:update_frame() 709 | end 710 | 711 | 712 | local function do_gui() 713 | G.origin() 714 | G.setLineWidth(1) 715 | gui:begin_frame() 716 | 717 | do 718 | gui:select_win(1) 719 | 720 | gui:item_min_size(60, 0) 721 | gui:text("mx %.2f", edit.mx) 722 | gui:text("my %.2f", edit.my) 723 | gui:separator() 724 | 725 | 726 | gui:item_min_size(60, 0) 727 | gui:checkbox("fill", edit, "show_fill") 728 | gui:same_line() 729 | gui:checkbox("joint", edit, "show_joints") 730 | gui:item_min_size(60, 0) 731 | local p = gui.id_prefix 732 | gui.id_prefix = p .."#" -- becaue we use "bone" somewhere else 733 | gui:checkbox("bone", edit, "show_bones") 734 | gui.id_prefix = p 735 | gui:same_line() 736 | gui:checkbox("grid", edit, "show_grid") 737 | if gui.was_key_pressed["#"] then 738 | local v = not edit.show_grid 739 | edit.show_grid = v 740 | edit.show_bones = v 741 | end 742 | 743 | 744 | -- if gui.was_key_pressed["b"] then 745 | -- bg.enabled = not bg.enabled 746 | -- end 747 | -- gui:checkbox("image", bg, "enabled") 748 | 749 | 750 | gui:item_min_size(125, 0) 751 | gui:separator() 752 | 753 | local m = edit.mode == edit.modes.bone and "bone" or "mesh" 754 | local t = { m } 755 | gui:item_min_size(60, 0) 756 | gui:radio_button("bone", "bone", t) 757 | gui:same_line() 758 | gui:item_min_size(60, 0) 759 | gui:radio_button("mesh", "mesh", t) 760 | if m ~= t[1] 761 | or gui.was_key_pressed["tab"] then 762 | m = m == "bone" and "mesh" or "bone" 763 | edit:set_mode(m) 764 | end 765 | end 766 | 767 | 768 | local ctrl = love.keyboard.isDown("lctrl", "rctrl") 769 | local shift = love.keyboard.isDown("lshift", "rshift") 770 | 771 | do 772 | gui:select_win(2) 773 | 774 | if gui:button("new") 775 | or (gui.was_key_pressed["n"] and ctrl) then 776 | model:reset() 777 | edit.modes.mesh.selected_vertices = {} 778 | edit.modes.mesh.poly_index = 0 779 | edit.modes.bone.selected_bone = model.root 780 | edit:find_current_anim() 781 | end 782 | gui:same_line() 783 | if gui:button("load") 784 | or (gui.was_key_pressed["l"] and ctrl) then 785 | if model:load(edit.file_name) then 786 | print("model loaded") 787 | else 788 | print("error loading model") 789 | end 790 | edit.modes.mesh.selected_vertices = {} 791 | edit.modes.mesh.poly_index = 0 792 | edit.modes.bone.selected_bone = model.root 793 | edit:find_current_anim() 794 | end 795 | gui:same_line() 796 | if gui:button("save") 797 | or (gui.was_key_pressed["s"] and ctrl) then 798 | edit.mode:exit() 799 | model:save(edit.file_name) 800 | edit.mode:enter() 801 | print("model saved") 802 | end 803 | gui:same_line() 804 | 805 | if gui:button("quit") then 806 | love.event.quit() 807 | end 808 | end 809 | 810 | do 811 | gui:select_win(3) 812 | 813 | -- timeline 814 | local w = gui.current_window.columns[1].max_x - gui.current_window.max_cx - 5 815 | local box = gui:item_box(w, 55) 816 | 817 | -- change frame 818 | if gui.was_key_pressed["backspace"] then 819 | if edit.current_anim then 820 | edit:set_frame(edit.current_anim.start) 821 | else 822 | edit:set_frame(0) 823 | end 824 | end 825 | local dx = (gui.was_key_pressed["right"] and 1 or 0) 826 | - (gui.was_key_pressed["left"] and 1 or 0) 827 | if (not gui.active_item or gui.active_item == "timeline") and gui:mouse_in_box(box) then 828 | dx = dx + gui.wheel 829 | if gui.is_mouse_down then 830 | gui.active_item = "timeline" 831 | edit:set_frame(math.floor((gui.mx - box.x - 5) / 10 + 0.5)) 832 | end 833 | end 834 | if dx ~= 0 then 835 | if shift then dx = dx * 10 end 836 | local f = edit.frame + dx 837 | if ctrl and edit.current_anim then 838 | local a = edit.current_anim 839 | f = a.start + (f - a.start) % (a.stop - a.start) 840 | end 841 | edit:set_frame(f) 842 | end 843 | 844 | G.setScissor(box.x, box.y, box.w, box.h) 845 | G.push() 846 | G.translate(box.x, box.y) 847 | 848 | 849 | G.setColor(0.4, 0.4, 0.4, 0.8) 850 | G.rectangle("fill", 0, 0, box.w, box.h) 851 | 852 | -- current frame 853 | G.setColor(0, 1, 0) 854 | local x = 5 + edit.frame * 10 855 | G.line(x, 0, x, 55) 856 | 857 | -- animations 858 | G.setColor(0, 1, 0, 0.3) 859 | for _, a in ipairs(model.anims) do 860 | local x1 = 5 + a.start * 10 861 | local x2 = 5 + a.stop * 10 862 | G.rectangle("fill", x1, 5, x2 - x1, 20, 8) 863 | end 864 | 865 | -- fill keyframe mask 866 | local is_keyframe = {} 867 | for _, b in ipairs(model.bones) do 868 | for _, k in ipairs(b.keyframes) do 869 | is_keyframe[k[1]] = true 870 | end 871 | end 872 | -- lines 873 | local i = 0 874 | for x = 5, box.w, 10 do 875 | G.setColor(1, 1, 1) 876 | if i % 10 == 0 then 877 | G.line(x, 45, x, 55) 878 | G.printf(i, x - 50, 28, 100, "center") 879 | else 880 | G.line(x, 50, x, 55) 881 | end 882 | 883 | -- keyframe 884 | if is_keyframe[i] then 885 | G.setColor(1, 0.8, 0.4) 886 | G.circle("fill", x, 15, 5, 4) 887 | end 888 | i = i + 1 889 | end 890 | 891 | G.pop() 892 | G.setScissor() 893 | 894 | -- keyframe buttons 895 | gui:item_min_size(0, 20) 896 | gui:text("keyframe") 897 | gui:same_line() 898 | local bone = edit.modes.bone.selected_bone 899 | if gui:button("insert") or gui.was_key_pressed["i"] then 900 | if ctrl then 901 | bone:insert_keyframe(edit.frame) 902 | else 903 | model:insert_keyframe(edit.frame) 904 | end 905 | end 906 | gui:same_line() 907 | if gui:button("copy") then 908 | if ctrl then 909 | model:copy_keyframe_x(edit.frame, bone) 910 | else 911 | model:copy_keyframe(edit.frame) 912 | end 913 | end 914 | gui:same_line() 915 | if gui:button("paste") then 916 | if ctrl then 917 | model:paste_keyframe_x(edit.frame, bone) 918 | else 919 | model:paste_keyframe(edit.frame) 920 | end 921 | end 922 | gui:same_line() 923 | if gui:button("delete") or gui.was_key_pressed["k"] then 924 | if ctrl then 925 | bone:delete_keyframe(edit.frame) 926 | else 927 | model:delete_keyframe(edit.frame) 928 | end 929 | end 930 | gui:same_line() 931 | if gui:button("animate other limb") and edit.current_anim and bone ~= model.root then 932 | -- find limb start bone 933 | while bone.parent and #bone.parent.kids <= 1 do 934 | bone = bone.parent 935 | end 936 | -- find other limb 937 | local len = math.sqrt(bone.x^2 + bone.y^2) 938 | local other_bone = nil 939 | local other_len = nil 940 | for _, b in ipairs(bone.parent.kids) do 941 | if b ~= bone then 942 | local l = math.sqrt(b.x^2 + b.y^2) 943 | if not other_bone or math.abs(l - len) < math.abs(other_len - len) then 944 | other_bone = b 945 | other_len = l 946 | end 947 | end 948 | end 949 | if not other_bone then 950 | print("ERROR: couldn't find other limb") 951 | else 952 | -- copy keyframes 953 | local anim = edit.current_anim 954 | local k = math.floor((anim.start + anim.stop) / 2) 955 | for i = anim.start, anim.stop - 1 do 956 | model:copy_keyframe_x(i, bone) 957 | model:paste_keyframe_x(k, other_bone) 958 | if k >= anim.stop then 959 | k = anim.start 960 | model:copy_keyframe_x(i, bone) 961 | model:paste_keyframe_x(k, other_bone) 962 | end 963 | k = k + 1 964 | end 965 | model:set_frame(edit.frame) 966 | end 967 | end 968 | 969 | gui:same_line() 970 | gui:separator() 971 | 972 | -- animation 973 | gui:text("animation") 974 | gui:same_line() 975 | 976 | -- play 977 | local t = { edit.is_playing } 978 | gui:radio_button("stop", false, t) 979 | gui:same_line() 980 | gui:radio_button("play", true, t) 981 | if edit.is_playing ~= t[1] 982 | or gui.was_key_pressed["space"] then 983 | edit:set_playing(not edit.is_playing) 984 | end 985 | gui:same_line() 986 | 987 | gui.id_prefix = "anim" -- hacky :) 988 | local t = edit.current_anim or edit 989 | gui:item_min_size(200, 0) 990 | gui:drag_value("speed", t, "speed", 0.01, 0.0, 1, "%.2f") 991 | gui:same_line() 992 | if edit.current_anim then 993 | local t = { len = edit.current_anim.stop - edit.current_anim.start } 994 | gui:item_min_size(200, 0) 995 | if gui:drag_value("length", t, "len", 1, 1, 50, "%d") then 996 | edit.current_anim.stop = edit.current_anim.start + t.len 997 | end 998 | 999 | gui:same_line() 1000 | gui:checkbox("loop", edit.current_anim, "loop") 1001 | gui:same_line() 1002 | if gui:button("delete") then 1003 | for i, a in ipairs(model.anims) do 1004 | if a == edit.current_anim then 1005 | table.remove(model.anims, i) 1006 | break 1007 | end 1008 | end 1009 | edit:find_current_anim() 1010 | end 1011 | else 1012 | if gui:button("insert") then 1013 | edit:set_playing(false) 1014 | local index = #model.anims + 1 1015 | for i, a in ipairs(model.anims) do 1016 | if a.start > edit.frame then 1017 | index = i 1018 | break 1019 | end 1020 | end 1021 | edit.current_anim = { 1022 | start = edit.frame, 1023 | stop = edit.frame + 10, 1024 | loop = false, 1025 | speed = edit.speed, 1026 | } 1027 | table.insert(model.anims, index, edit.current_anim) 1028 | end 1029 | end 1030 | 1031 | end 1032 | 1033 | 1034 | -- -- background 1035 | -- gui:select_win(2) 1036 | -- gui:item_min_size(600, 0) 1037 | -- gui:drag_value("x", bg, "x", 10, -8000, 0, "%d") 1038 | -- gui:item_min_size(600, 0) 1039 | -- gui:drag_value("y", bg, "y", 10, -4000, 0, "%d") 1040 | -- gui:item_min_size(600, 0) 1041 | -- gui:drag_value("scale", bg, "scale", 1, 1, 10, "%d") 1042 | 1043 | edit.mode:do_gui() 1044 | 1045 | gui:end_frame() 1046 | end 1047 | 1048 | --bg = { 1049 | -- enabled = false, 1050 | -- img = G.newImage("super_turri_2.png"), 1051 | -- x = -140, 1052 | -- y = -392, 1053 | -- scale = 8, 1054 | --} 1055 | --bg.img:setFilter("nearest", "nearest") 1056 | 1057 | 1058 | local function draw_concav_poly(p) 1059 | if #p < 6 then return end 1060 | local status, err = pcall(function() 1061 | local tris = love.math.triangulate(p) 1062 | for _, t in ipairs(tris) do G.polygon("fill", t) end 1063 | end) 1064 | if not status then 1065 | print(err) 1066 | end 1067 | end 1068 | function love.draw() 1069 | G.translate(G.getWidth() / 2, G.getHeight() / 2) 1070 | G.scale(1 / cam.zoom) 1071 | G.translate(-cam.x, -cam.y) 1072 | G.setLineWidth(cam.zoom) 1073 | 1074 | -- axis and grid 1075 | do 1076 | G.setColor(1, 1, 1, 0.2) 1077 | G.line(-1000, 0, 1000, 0) 1078 | G.line(0, -1000, 0, 1000) 1079 | 1080 | if edit.show_grid then 1081 | for x = -1000, 1000, 100 do 1082 | G.line(x, -1000, x, 1000) 1083 | end 1084 | for y = -1000, 1000, 100 do 1085 | G.line(-1000, y, 1000, y) 1086 | end 1087 | 1088 | -- fine grid 1089 | if cam.zoom < 1 then 1090 | local d = 10 1091 | local x1 = math.floor((cam.x - cam.zoom * G.getWidth() / 2) / d) * d 1092 | local y1 = math.floor((cam.y - cam.zoom * G.getHeight() / 2) / d) * d 1093 | local x2 = cam.x + cam.zoom * G.getWidth() / 2 1094 | local y2 = cam.y + cam.zoom * G.getHeight() / 2 1095 | 1096 | for x = x1, x2, d do 1097 | G.line(x, y1, x, y2) 1098 | end 1099 | for y = y1, y2, d do 1100 | G.line(x1, y, x2, y) 1101 | end 1102 | end 1103 | end 1104 | end 1105 | 1106 | 1107 | -- polys 1108 | for i, p in ipairs(model.polys) do 1109 | G.push() 1110 | if p.bone and (edit.mode ~= edit.modes.mesh or i ~= edit.modes.mesh.poly_index) then 1111 | G.translate(p.bone.global_x, p.bone.global_y) 1112 | G.rotate(p.bone.global_a) 1113 | end 1114 | local c = colors[p.color] 1115 | local s = p.shade 1116 | G.setColor(c[1] * s, c[2] * s, c[3] * s) 1117 | if edit.show_fill then 1118 | draw_concav_poly(p.data) 1119 | else 1120 | G.polygon("line", p.data) 1121 | end 1122 | G.pop() 1123 | end 1124 | 1125 | -- bone 1126 | if edit.show_bones then 1127 | for _, b in ipairs(model.bones) do 1128 | if b.parent then 1129 | local dx = b.global_x - b.parent.global_x 1130 | local dy = b.global_y - b.parent.global_y 1131 | local l = length(dx, dy) * 0.1 / cam.zoom 1132 | G.setColor(0.4, 0.6, 0.8, 0.6) 1133 | G.polygon("fill", 1134 | b.parent.global_x + dy / l, 1135 | b.parent.global_y - dx / l, 1136 | b.parent.global_x - dy / l, 1137 | b.parent.global_y + dx / l, 1138 | b.global_x, 1139 | b.global_y) 1140 | end 1141 | end 1142 | end 1143 | 1144 | -- joint 1145 | if edit.show_joints then 1146 | for _, b in ipairs(model.bones) do 1147 | G.setColor(1, 1, 1, 0.6) 1148 | G.circle("fill", b.global_x, b.global_y, 5 * cam.zoom) 1149 | end 1150 | -- selected 1151 | local b = edit.modes.bone.selected_bone 1152 | G.setColor(1, 1, 0, 0.6) 1153 | G.circle("fill", b.global_x, b.global_y, 10 * cam.zoom) 1154 | end 1155 | 1156 | local m = edit.mode 1157 | if m == edit.modes.mesh then 1158 | 1159 | local poly = model.polys[m.poly_index] 1160 | if poly then 1161 | 1162 | -- selected poly 1163 | G.setColor(1, 1, 1, 0.75) 1164 | G.polygon("line", poly.data) 1165 | 1166 | G.setColor(1, 1, 1) 1167 | G.setPointSize(5) 1168 | G.points(poly.data) 1169 | 1170 | -- selected vertices 1171 | local s = {} 1172 | for _, i in ipairs(m.selected_vertices) do 1173 | s[#s + 1] = poly.data[i] 1174 | s[#s + 1] = poly.data[i + 1] 1175 | end 1176 | G.setColor(1, 1, 0) 1177 | G.setPointSize(7) 1178 | G.points(s) 1179 | 1180 | -- selection box 1181 | if m.sx then 1182 | G.setColor(0.7, 0.7, 0.7) 1183 | G.rectangle("line", m.sx, m.sy, edit.mx - m.sx, edit.my - m.sy) 1184 | end 1185 | 1186 | -- parent bone 1187 | if poly.bone then 1188 | G.setColor(1, 1, 0) 1189 | G.setLineWidth(cam.zoom * 2) 1190 | G.circle("line", poly.bone.global_x, poly.bone.global_y, 13 * cam.zoom) 1191 | end 1192 | end 1193 | end 1194 | 1195 | 1196 | -- if bg.enabled then 1197 | -- G.setColor(1, 1, 1, 0.25) 1198 | -- G.draw(bg.img, bg.x, bg.y, 0, bg.scale) 1199 | -- end 1200 | 1201 | do_gui() 1202 | end 1203 | -------------------------------------------------------------------------------- /model.lua: -------------------------------------------------------------------------------- 1 | Bone = Object:new() 2 | function Bone:init(x, y, a, keyframes) 3 | self.x = x or 0 4 | self.y = y or 0 5 | self.a = a or 0 6 | self.keyframes = keyframes or {} 7 | self.kids = {} 8 | end 9 | function Bone:add_kid(k) 10 | table.insert(self.kids, k) 11 | k.parent = self 12 | end 13 | function Bone:update() 14 | local p = self.parent or { 15 | global_x = 0, 16 | global_y = 0, 17 | global_a = 0, 18 | } 19 | local si = math.sin(p.global_a) 20 | local co = math.cos(p.global_a) 21 | self.global_x = p.global_x + self.x * co - self.y * si 22 | self.global_y = p.global_y + self.y * co + self.x * si 23 | self.global_a = p.global_a + self.a 24 | for _, k in ipairs(self.kids) do k:update() end 25 | end 26 | 27 | Model = Object:new() 28 | function Model:init(file_name) 29 | if file_name then 30 | self:load(file_name) 31 | else 32 | self:reset() 33 | end 34 | end 35 | function Model:reset() 36 | self.root = Bone() 37 | self.root:update() 38 | self.bones = { self.root } 39 | self.polys = {} 40 | self.anims = {} 41 | end 42 | function Model:set_frame(frame) 43 | for _, b in ipairs(self.bones) do 44 | local k1, k2 45 | for i, k in ipairs(b.keyframes) do 46 | if k[1] < frame then 47 | k1 = k 48 | end 49 | if k[1] >= frame then 50 | k2 = k 51 | break 52 | end 53 | end 54 | if k1 and k2 then 55 | local l = (frame - k1[1]) / (k2[1] - k1[1]) 56 | local function lerp(i) return k1[i] * (1 - l) + k2[i] * l end 57 | b.x = lerp(2) 58 | b.y = lerp(3) 59 | b.a = lerp(4) 60 | elseif k1 or k2 then 61 | local k = k1 or k2 62 | b.x = k[2] 63 | b.y = k[3] 64 | b.a = k[4] 65 | end 66 | end 67 | self.root:update() 68 | end 69 | function Model:load(name) 70 | self:reset() 71 | local f = io.open(name) 72 | if not f then return false end 73 | local str = f:read("*a") 74 | f:close() 75 | local data = loadstring("return " .. str)() 76 | self.anims = data.anims 77 | self.polys = data.polys 78 | self.bones = {} 79 | for i, d in ipairs(data.bones) do 80 | local b = Bone(d.x, d.y, d.a, d.keyframes) 81 | b.i = i 82 | table.insert(self.bones, b) 83 | end 84 | for i, d in ipairs(data.bones) do 85 | local b = self.bones[i] 86 | if d.parent then 87 | self.bones[d.parent]:add_kid(b) 88 | else 89 | self.root = b 90 | end 91 | end 92 | for _, p in ipairs(self.polys) do 93 | p.bone = self.bones[p.bone] 94 | end 95 | self.root:update() 96 | return true 97 | end 98 | -------------------------------------------------------------------------------- /models/turri.model: -------------------------------------------------------------------------------- 1 | { 2 | anims={ 3 | { 4 | loop=true, 5 | speed=0.5, 6 | start=10, 7 | stop=26 8 | }, 9 | { 10 | loop=true, 11 | speed=0.15, 12 | start=30, 13 | stop=40 14 | }, 15 | { 16 | loop=false, 17 | speed=0.1, 18 | start=50, 19 | stop=60 20 | }, 21 | { 22 | loop=false, 23 | speed=0.1, 24 | start=70, 25 | stop=80 26 | }, 27 | { 28 | loop=false, 29 | speed=0, 30 | start=110, 31 | stop=118 32 | }, 33 | { 34 | loop=false, 35 | speed=0.72, 36 | start=90, 37 | stop=100 38 | } 39 | }, 40 | bones={ 41 | { 42 | a=0.368, 43 | keyframes={ 44 | {0,44.6759,-20.6945,0.368}, 45 | {10,44.6759,-20.6945,0.368}, 46 | {12,44.6759,-20.6945,0.368}, 47 | {14,44.6759,-20.6945,0.368}, 48 | {16,44.6759,-20.6945,0.368}, 49 | {17,44.6759,-20.6945,0.368}, 50 | {18,44.6759,-20.6945,0.368}, 51 | {20,44.6759,-20.6945,0.368}, 52 | {22,44.6759,-20.6945,0.368}, 53 | {24,44.6759,-20.6945,0.368}, 54 | {25,44.6759,-20.6945,0.368}, 55 | {26,44.6759,-20.6945,0.368}, 56 | {30,44.6759,-20.6945,0.368}, 57 | {31,44.6759,-20.6945,0.368}, 58 | {34,44.6759,-20.6945,0.368}, 59 | {35,44.6759,-20.6945,0.368}, 60 | {36,44.6759,-20.6945,0.368}, 61 | {39,44.6759,-20.6945,0.368}, 62 | {40,44.6759,-20.6945,0.368}, 63 | {50,44.6759,-20.6945,0.368}, 64 | {55,44.6759,-20.6945,0.368}, 65 | {60,44.6759,-20.6945,0.368}, 66 | {70,44.6759,-20.6945,0.368}, 67 | {80,44.6759,-20.6945,0.368}, 68 | {90,44.6759,-20.6945,0.368}, 69 | {91,44.6759,-20.6945,0.5555}, 70 | {95,44.6759,-20.6945,0.532062}, 71 | {100,44.6759,-20.6945,0.368}, 72 | {110,44.6759,-20.6945,0.03}, 73 | {114,44.6759,-20.6945,0.368}, 74 | {118,44.6759,-20.6945,-1.906} 75 | }, 76 | parent=8, 77 | x=44.6759, 78 | y=-20.6945 79 | }, 80 | { 81 | a=-0.31, 82 | keyframes={ 83 | {0,8.13583,71.6728,-0.31}, 84 | {10,8.13583,71.6728,-0.31}, 85 | {12,8.13583,71.6728,-0.31}, 86 | {14,8.13583,71.6728,-0.31}, 87 | {16,8.13583,71.6728,-0.31}, 88 | {17,8.13583,71.6728,-0.31}, 89 | {18,8.13583,71.6728,-0.31}, 90 | {20,8.13583,71.6728,-0.31}, 91 | {22,8.13583,71.6728,-0.31}, 92 | {24,8.13583,71.6728,-0.31}, 93 | {25,8.13583,71.6728,-0.31}, 94 | {26,8.13583,71.6728,-0.31}, 95 | {30,8.13583,71.6728,-0.31}, 96 | {31,8.13583,71.6728,-0.31}, 97 | {34,8.13583,71.6728,-0.31}, 98 | {35,8.13583,71.6728,-0.31}, 99 | {36,8.13583,71.6728,-0.31}, 100 | {39,8.13583,71.6728,-0.31}, 101 | {40,8.13583,71.6728,-0.31}, 102 | {50,8.13583,71.6728,-0.31}, 103 | {55,8.13583,71.6728,-0.31}, 104 | {60,8.13583,71.6728,-0.31}, 105 | {70,8.13583,71.6728,-0.31}, 106 | {80,8.13583,71.6728,-0.31}, 107 | {90,8.13583,71.6728,-0.31}, 108 | {91,8.13583,71.6728,-0.384}, 109 | {95,8.13583,71.6728,-0.37475}, 110 | {100,8.13583,71.6728,-0.31}, 111 | {110,8.13583,71.6728,1.3895}, 112 | {114,8.13583,71.6728,-0.31}, 113 | {118,8.13583,71.6728,-0.0885} 114 | }, 115 | parent=1, 116 | x=8.13583, 117 | y=71.6728 118 | }, 119 | { 120 | a=0.2245, 121 | keyframes={ 122 | {0,17.5864,-7.55627,0.2245}, 123 | {10,17.5864,-7.55627,0.517}, 124 | {12,12.4208,-4.97347,1.12312}, 125 | {14,14.5731,-6.26487,1.80725}, 126 | {16,9.83799,-2.39066,1.59687}, 127 | {17,13.7122,-4.97347,1.24394}, 128 | {18,17.5864,-7.55627,0.891}, 129 | {20,17.5864,-7.55627,-0.022625}, 130 | {22,17.5864,-7.55627,-0.52375}, 131 | {24,17.5864,-7.55627,-0.459375}, 132 | {25,17.5864,-7.55627,-0.0844375}, 133 | {26,17.5864,-7.55627,0.517}, 134 | {30,17.5864,-7.55627,0.2245}, 135 | {31,17.5864,-7.55627,0.20292}, 136 | {34,17.5864,-7.55627,0.04605}, 137 | {35,17.5864,-7.55627,0.017}, 138 | {36,17.5864,-7.55627,0.03858}, 139 | {39,17.5864,-7.55627,0.19545}, 140 | {40,17.5864,-7.55627,0.2245}, 141 | {50,17.5864,-7.55627,-0.4255}, 142 | {55,17.5864,-7.55627,-0.4855}, 143 | {60,17.5864,-7.55627,0.0755}, 144 | {70,17.5864,-7.55627,0.2245}, 145 | {80,17.5864,-7.55627,-0.4105}, 146 | {90,17.5864,-7.55627,0.2245}, 147 | {91,17.5864,-7.55627,0.2245}, 148 | {95,17.5864,-7.55627,0.2245}, 149 | {100,17.5864,-7.55627,0.2245}, 150 | {110,17.5864,-7.55627,0.081}, 151 | {114,17.5864,-7.55627,0.2245}, 152 | {118,17.5864,-7.55627,0.111} 153 | }, 154 | parent=4, 155 | x=17.5864, 156 | y=-7.55627 157 | }, 158 | { 159 | a=0, 160 | keyframes={ 161 | {0,0.38742,-126.299,0}, 162 | {10,2.30061,-116.255,0}, 163 | {12,2.73108,-120.129,0}, 164 | {14,1.87014,-119.268,0}, 165 | {16,2.17147,-110.186,0}, 166 | {17,2.23604,-113.221,0}, 167 | {18,2.30061,-116.255,0}, 168 | {20,1.87014,-120.689,0}, 169 | {22,2.30061,-114.533,0}, 170 | {24,1.91319,-111.219,0}, 171 | {25,2.1069,-113.737,0}, 172 | {26,2.30061,-116.255,0}, 173 | {30,0.38742,-126.299,0}, 174 | {31,0.38742,-125.413,0}, 175 | {34,0.38742,-118.969,0}, 176 | {35,0.38742,-117.776,0}, 177 | {36,0.38742,-118.662,0}, 178 | {39,0.38742,-125.106,0}, 179 | {40,0.38742,-126.299,0}, 180 | {50,0.38742,-119.395,0}, 181 | {55,0.38742,-119.395,0}, 182 | {60,0.38742,-119.395,0}, 183 | {70,0.38742,-126.299,0}, 184 | {80,-0.459869,-96.6439,0}, 185 | {90,0.38742,-126.299,0}, 186 | {91,0.38742,-126.299,0}, 187 | {95,0.38742,-126.299,0}, 188 | {100,0.38742,-126.299,0}, 189 | {110,-14.3748,-123.347,0.0799902}, 190 | {114,0.38742,-126.299,0}, 191 | {118,16.3307,-118.623,0} 192 | }, 193 | x=0.38742, 194 | y=-126.299 195 | }, 196 | { 197 | a=0, 198 | keyframes={ 199 | {0,0.38742,-43.3911,0}, 200 | {10,0.38742,-43.3911,0}, 201 | {12,0.38742,-43.3911,0}, 202 | {14,0.38742,-43.3911,0}, 203 | {16,0.38742,-43.3911,0}, 204 | {17,0.38742,-43.3911,0}, 205 | {18,0.38742,-43.3911,0}, 206 | {20,0.38742,-43.3911,0}, 207 | {22,0.38742,-43.3911,0}, 208 | {24,0.38742,-43.3911,0}, 209 | {25,0.38742,-43.3911,0}, 210 | {26,0.38742,-43.3911,0}, 211 | {30,0.38742,-43.3911,0}, 212 | {31,0.38742,-43.3911,0}, 213 | {34,0.38742,-43.3911,0}, 214 | {35,0.38742,-43.3911,0}, 215 | {36,0.38742,-43.3911,0}, 216 | {39,0.38742,-43.3911,0}, 217 | {40,0.38742,-43.3911,0}, 218 | {50,0.38742,-43.3911,0}, 219 | {55,0.38742,-43.3911,0}, 220 | {60,0.38742,-43.3911,0}, 221 | {70,0.38742,-43.3911,0}, 222 | {80,0.38742,-43.3911,0}, 223 | {90,0.38742,-43.3911,0}, 224 | {91,0.38742,-43.3911,0}, 225 | {95,0.38742,-43.3911,0}, 226 | {100,0.38742,-43.3911,0}, 227 | {110,8.65428,-44.5721,0.456972}, 228 | {114,0.38742,-43.3911,0}, 229 | {118,9.86442,-51.4101,-0.525569} 230 | }, 231 | parent=4, 232 | x=0.38742, 233 | y=-43.3911 234 | }, 235 | { 236 | a=-0.2405, 237 | keyframes={ 238 | {0,42.4492,32.7901,-0.2405}, 239 | {10,42.4492,32.7901,0.1935}, 240 | {12,42.4492,32.7901,0.11975}, 241 | {14,42.4492,32.7901,-0.732}, 242 | {16,42.4492,32.7901,0.25925}, 243 | {17,42.4492,32.7901,0.513375}, 244 | {18,42.4492,32.7901,0.7675}, 245 | {20,42.4492,32.7901,1.379}, 246 | {22,42.4492,32.7901,1.297}, 247 | {24,42.4492,32.7901,-0.1185}, 248 | {25,42.4492,32.7901,0.2775}, 249 | {26,42.4492,32.7901,0.1935}, 250 | {30,42.4492,32.7901,-0.2405}, 251 | {31,42.4492,32.7901,-0.206544}, 252 | {34,42.4492,32.7901,0.04029}, 253 | {35,42.4492,32.7901,0.086}, 254 | {36,42.4492,32.7901,0.052044}, 255 | {39,42.4492,32.7901,-0.19479}, 256 | {40,42.4492,32.7901,-0.2405}, 257 | {50,42.4492,32.7901,1.2155}, 258 | {55,42.4492,32.7901,0.99175}, 259 | {60,42.4492,32.7901,-0.171}, 260 | {70,42.4492,32.7901,-0.2405}, 261 | {80,42.4492,32.7901,0.6075}, 262 | {90,42.4492,32.7901,-0.2405}, 263 | {91,42.4492,32.7901,-0.2405}, 264 | {95,42.4492,32.7901,-0.2405}, 265 | {100,42.4492,32.7901,-0.2405}, 266 | {110,42.4492,32.7901,-0.343}, 267 | {114,42.4492,32.7901,-0.2405}, 268 | {118,42.4492,32.7901,0.163} 269 | }, 270 | parent=3, 271 | x=42.4492, 272 | y=32.7901 273 | }, 274 | { 275 | a=0.0386758, 276 | keyframes={ 277 | {0,0.254187,74.4767,0.0386758}, 278 | {10,0.254187,74.4767,-0.719314}, 279 | {12,0.254187,74.4767,-0.321802}, 280 | {14,0.254187,74.4767,0.259029}, 281 | {16,0.254187,74.4767,0.280533}, 282 | {17,0.254187,74.4767,0.123376}, 283 | {18,0.254187,74.4767,-0.0337818}, 284 | {20,0.254187,74.4767,0.305651}, 285 | {22,0.254187,74.4767,0.0929993}, 286 | {24,0.254187,74.4767,-0.230544}, 287 | {25,0.254187,74.4767,-0.345617}, 288 | {26,0.254187,74.4767,-0.719314}, 289 | {30,0.254187,74.4767,0.0386758}, 290 | {31,0.254187,74.4767,0.0242118}, 291 | {34,0.254187,74.4767,-0.0809302}, 292 | {35,0.254187,74.4767,-0.100401}, 293 | {36,0.254187,74.4767,-0.085937}, 294 | {39,0.254187,74.4767,0.019205}, 295 | {40,0.254187,74.4767,0.0386758}, 296 | {50,0.254187,74.4767,0.18143}, 297 | {55,0.254187,74.4767,0.308618}, 298 | {60,0.254187,74.4767,0.435806}, 299 | {70,0.254187,74.4767,0.0386758}, 300 | {80,0.254187,74.4767,-0.201157}, 301 | {90,0.254187,74.4767,0.0386758}, 302 | {91,0.254187,74.4767,0.0386758}, 303 | {95,0.254187,74.4767,0.0386758}, 304 | {100,0.254187,74.4767,0.0386758}, 305 | {110,0.254187,74.4767,0.168752}, 306 | {114,0.254187,74.4767,0.0386758}, 307 | {118,0.254187,74.4767,-0.262325} 308 | }, 309 | parent=6, 310 | x=0.254187, 311 | y=74.4767 312 | }, 313 | { 314 | a=0, 315 | keyframes={ 316 | {0,-2.32452,-63.9244,0}, 317 | {10,-2.32452,-63.9244,0}, 318 | {12,-2.32452,-63.9244,0}, 319 | {14,-2.32452,-63.9244,0}, 320 | {16,-2.32452,-63.9244,0}, 321 | {17,-2.32452,-63.9244,0}, 322 | {18,-2.32452,-63.9244,0}, 323 | {20,-2.32452,-63.9244,0}, 324 | {22,-2.32452,-63.9244,0}, 325 | {24,-2.32452,-63.9244,0}, 326 | {25,-2.32452,-63.9244,0}, 327 | {26,-2.32452,-63.9244,0}, 328 | {30,-2.32452,-63.9244,0}, 329 | {31,-2.32452,-63.9244,0}, 330 | {34,-2.32452,-63.9244,0}, 331 | {35,-2.32452,-63.9244,0}, 332 | {36,-2.32452,-63.9244,0}, 333 | {39,-2.32452,-63.9244,0}, 334 | {40,-2.32452,-63.9244,0}, 335 | {50,-2.32452,-63.9244,0}, 336 | {55,-2.32452,-63.9244,0}, 337 | {60,-2.32452,-63.9244,0}, 338 | {70,-2.32452,-63.9244,0}, 339 | {80,-2.32452,-63.9244,0}, 340 | {90,-2.32452,-63.9244,0}, 341 | {91,-2.32452,-63.9244,0}, 342 | {95,-2.32452,-63.9244,0}, 343 | {100,-2.32452,-63.9244,0}, 344 | {110,-2.32452,-63.9244,0}, 345 | {114,-2.32452,-63.9244,0}, 346 | {118,-2.32452,-63.9244,0.008} 347 | }, 348 | parent=5, 349 | x=-2.32452, 350 | y=-63.9244 351 | }, 352 | { 353 | a=1.181, 354 | keyframes={ 355 | {0,-22.3036,-6.71131,1.181}, 356 | {10,-22.3036,-6.71131,0.115}, 357 | {12,-13.6943,-4.12851,-0.38925}, 358 | {14,-13.6943,-6.28084,-0.668}, 359 | {16,-17.6546,-5.16163,-0.35275}, 360 | {17,-19.9791,-5.93647,-0.115125}, 361 | {18,-22.3036,-6.71131,0.3395}, 362 | {20,-22.3036,-6.71131,1.098}, 363 | {22,-22.3036,-6.71131,1.7775}, 364 | {24,-22.3036,-6.71131,1.1695}, 365 | {25,-22.3036,-6.71131,0.64225}, 366 | {26,-22.3036,-6.71131,0.115}, 367 | {30,-22.3036,-6.71131,1.181}, 368 | {31,-22.3036,-6.71131,1.15354}, 369 | {34,-22.3036,-6.71131,0.95396}, 370 | {35,-22.3036,-6.71131,0.917}, 371 | {36,-22.3036,-6.71131,0.944456}, 372 | {39,-22.3036,-6.71131,1.14404}, 373 | {40,-22.3036,-6.71131,1.181}, 374 | {50,-22.3036,-6.71131,0.8515}, 375 | {55,-22.3036,-6.71131,0.14525}, 376 | {60,-22.3036,-6.71131,-0.158}, 377 | {70,-22.3036,-6.71131,1.181}, 378 | {80,-22.3036,-6.71131,0.6385}, 379 | {90,-22.3036,-6.71131,1.181}, 380 | {91,-22.3036,-6.71131,1.181}, 381 | {95,-22.3036,-6.71131,1.181}, 382 | {100,-22.3036,-6.71131,1.181}, 383 | {110,-22.3036,-6.71131,0.736}, 384 | {114,-22.3036,-6.71131,1.181}, 385 | {118,-22.3036,-6.71131,1.2685} 386 | }, 387 | parent=4, 388 | x=-22.3036, 389 | y=-6.71131 390 | }, 391 | { 392 | a=-0.641, 393 | keyframes={ 394 | {0,42.4492,32.7901,-0.641}, 395 | {10,42.4492,32.7901,1.391}, 396 | {12,42.4492,32.7901,1.53025}, 397 | {14,42.4492,32.7901,1.1595}, 398 | {16,42.4492,32.7901,-0.36025}, 399 | {17,42.4492,32.7901,0.133875}, 400 | {18,42.4492,32.7901,0.311}, 401 | {20,42.4492,32.7901,-0.02325}, 402 | {22,42.4492,32.7901,-0.7615}, 403 | {24,42.4492,32.7901,0.72625}, 404 | {25,42.4492,32.7901,1.05862}, 405 | {26,42.4492,32.7901,1.391}, 406 | {30,42.4492,32.7901,-0.641}, 407 | {31,42.4492,32.7901,-0.588688}, 408 | {34,42.4492,32.7901,-0.20842}, 409 | {35,42.4492,32.7901,-0.138}, 410 | {36,42.4492,32.7901,-0.190312}, 411 | {39,42.4492,32.7901,-0.57058}, 412 | {40,42.4492,32.7901,-0.641}, 413 | {50,42.4492,32.7901,0.092}, 414 | {55,42.4492,32.7901,0.80025}, 415 | {60,42.4492,32.7901,0.7875}, 416 | {70,42.4492,32.7901,-0.641}, 417 | {80,42.4492,32.7901,0.4665}, 418 | {90,42.4492,32.7901,-0.641}, 419 | {91,42.4492,32.7901,-0.641}, 420 | {95,42.4492,32.7901,-0.641}, 421 | {100,42.4492,32.7901,-0.641}, 422 | {110,42.4492,32.7901,-0.202}, 423 | {114,42.4492,32.7901,-0.641}, 424 | {118,42.4492,32.7901,-0.579} 425 | }, 426 | parent=9, 427 | x=42.4492, 428 | y=32.7901 429 | }, 430 | { 431 | a=-0.534686, 432 | keyframes={ 433 | {0,0.254187,74.4767,-0.534686}, 434 | {10,0.254187,74.4767,0.188927}, 435 | {12,0.254187,74.4767,0.860019}, 436 | {14,0.254187,74.4767,0.164288}, 437 | {16,0.254187,74.4767,0.240598}, 438 | {17,0.254187,74.4767,-0.0806672}, 439 | {18,0.254187,74.4767,-0.645043}, 440 | {20,0.254187,74.4767,-0.36125}, 441 | {22,0.254187,74.4767,0.471558}, 442 | {24,0.254187,74.4767,0.532678}, 443 | {25,0.254187,74.4767,0.360803}, 444 | {26,0.254187,74.4767,0.188927}, 445 | {30,0.254187,74.4767,-0.534686}, 446 | {31,0.254187,74.4767,-0.559996}, 447 | {34,0.254187,74.4767,-0.743976}, 448 | {35,0.254187,74.4767,-0.778047}, 449 | {36,0.254187,74.4767,-0.752737}, 450 | {39,0.254187,74.4767,-0.568757}, 451 | {40,0.254187,74.4767,-0.534686}, 452 | {50,0.254187,74.4767,0.247437}, 453 | {55,0.254187,74.4767,0.244704}, 454 | {60,0.254187,74.4767,0.241972}, 455 | {70,0.254187,74.4767,-0.534686}, 456 | {80,0.254187,74.4767,-1.10779}, 457 | {90,0.254187,74.4767,-0.534686}, 458 | {91,0.254187,74.4767,-0.534686}, 459 | {95,0.254187,74.4767,-0.534686}, 460 | {100,0.254187,74.4767,-0.534686}, 461 | {110,0.254187,74.4767,-0.623368}, 462 | {114,0.254187,74.4767,-0.534686}, 463 | {118,0.254187,74.4767,-0.690729} 464 | }, 465 | parent=10, 466 | x=0.254187, 467 | y=74.4767 468 | }, 469 | { 470 | a=0, 471 | keyframes={ 472 | {0,-2.64099,-25.447,0}, 473 | {10,-2.64099,-25.447,0}, 474 | {12,-2.64099,-25.447,0}, 475 | {14,-2.64099,-25.447,0}, 476 | {16,-2.64099,-25.447,0}, 477 | {17,-2.64099,-25.447,0}, 478 | {18,-2.64099,-25.447,0}, 479 | {20,-2.64099,-25.447,0}, 480 | {22,-2.64099,-25.447,0}, 481 | {24,-2.64099,-25.447,0}, 482 | {25,-2.64099,-25.447,0}, 483 | {26,-2.64099,-25.447,0}, 484 | {30,-2.64099,-25.447,0}, 485 | {31,-2.64099,-25.447,0}, 486 | {34,-2.64099,-25.447,0}, 487 | {35,-2.64099,-25.447,0}, 488 | {36,-2.64099,-25.447,0}, 489 | {39,-2.64099,-25.447,0}, 490 | {40,-2.64099,-25.447,0}, 491 | {50,-2.64099,-25.447,0}, 492 | {55,-2.64099,-25.447,0}, 493 | {60,-2.64099,-25.447,0}, 494 | {70,-2.64099,-25.447,0}, 495 | {80,-2.64099,-25.447,0}, 496 | {90,-2.64099,-25.447,0}, 497 | {91,-2.64099,-25.447,0}, 498 | {95,-2.64099,-25.447,0}, 499 | {100,-2.64099,-25.447,0}, 500 | {110,-3.8373,-30.9197,0.171354}, 501 | {114,-2.64099,-25.447,0}, 502 | {118,-12.5607,-31.8828,-0.258397} 503 | }, 504 | parent=8, 505 | x=-2.64099, 506 | y=-25.447 507 | }, 508 | { 509 | a=-0.032, 510 | keyframes={ 511 | {0,-71.2144,-11.4998,-0.032}, 512 | {10,-71.2144,-11.4998,-0.032}, 513 | {12,-71.2144,-11.4998,-0.032}, 514 | {14,-71.2144,-11.4998,-0.032}, 515 | {16,-71.2144,-11.4998,-0.032}, 516 | {17,-71.2144,-11.4998,-0.032}, 517 | {18,-71.2144,-11.4998,-0.032}, 518 | {20,-71.2144,-11.4998,-0.032}, 519 | {22,-71.2144,-11.4998,-0.032}, 520 | {24,-71.2144,-11.4998,-0.032}, 521 | {25,-71.2144,-11.4998,-0.032}, 522 | {26,-71.2144,-11.4998,-0.032}, 523 | {30,-71.2144,-11.4998,-0.032}, 524 | {31,-71.2144,-11.4998,-0.032}, 525 | {34,-71.2144,-11.4998,-0.032}, 526 | {35,-71.2144,-11.4998,-0.032}, 527 | {36,-71.2144,-11.4998,-0.032}, 528 | {39,-71.2144,-11.4998,-0.032}, 529 | {40,-71.2144,-11.4998,-0.032}, 530 | {50,-71.2144,-11.4998,-0.032}, 531 | {55,-71.2144,-11.4998,-0.032}, 532 | {60,-71.2144,-11.4998,-0.032}, 533 | {70,-71.2144,-11.4998,-0.032}, 534 | {80,-71.2144,-11.4998,-0.032}, 535 | {90,-71.2144,-11.4998,-0.032}, 536 | {91,-71.2144,-11.4998,0.192}, 537 | {95,-71.2144,-11.4998,0.164}, 538 | {100,-71.2144,-11.4998,-0.032}, 539 | {110,-73.0148,-17.9872,-0.225}, 540 | {114,-71.2144,-11.4998,-0.032}, 541 | {118,-71.2144,-11.4998,-0.856} 542 | }, 543 | parent=8, 544 | x=-71.2144, 545 | y=-11.4998 546 | }, 547 | { 548 | a=0, 549 | keyframes={ 550 | {0,23.2264,-11.8698,0}, 551 | {10,23.2264,-11.8698,0}, 552 | {12,23.2264,-11.8698,0}, 553 | {14,23.2264,-11.8698,0}, 554 | {16,23.2264,-11.8698,0}, 555 | {17,23.2264,-11.8698,0}, 556 | {18,23.2264,-11.8698,0}, 557 | {20,23.2264,-11.8698,0}, 558 | {22,23.2264,-11.8698,0}, 559 | {24,23.2264,-11.8698,0}, 560 | {25,23.2264,-11.8698,0}, 561 | {26,23.2264,-11.8698,0}, 562 | {30,23.2264,-11.8698,0}, 563 | {31,23.2264,-11.8698,0}, 564 | {34,23.2264,-11.8698,0}, 565 | {35,23.2264,-11.8698,0}, 566 | {36,23.2264,-11.8698,0}, 567 | {39,23.2264,-11.8698,0}, 568 | {40,23.2264,-11.8698,0}, 569 | {50,23.2264,-11.8698,0}, 570 | {55,23.2264,-11.8698,0}, 571 | {60,23.2264,-11.8698,0}, 572 | {70,23.2264,-11.8698,0}, 573 | {80,23.2264,-11.8698,0}, 574 | {90,23.2264,-11.8698,0}, 575 | {91,23.2264,-11.8698,0}, 576 | {95,23.2264,-11.8698,0}, 577 | {100,23.2264,-11.8698,0}, 578 | {110,23.2264,-11.8698,0.0310643}, 579 | {114,23.2264,-11.8698,0}, 580 | {118,21.5177,-18.7466,-0.199659} 581 | }, 582 | parent=17, 583 | x=23.2264, 584 | y=-11.8698 585 | }, 586 | { 587 | a=-0.545351, 588 | keyframes={ 589 | {0,38.4239,6.80061,-0.545351}, 590 | {10,38.4239,6.80061,-0.545351}, 591 | {12,38.4239,6.80061,-0.545351}, 592 | {14,38.4239,6.80061,-0.545351}, 593 | {16,38.4239,6.80061,-0.545351}, 594 | {17,38.4239,6.80061,-0.545351}, 595 | {18,38.4239,6.80061,-0.545351}, 596 | {20,38.4239,6.80061,-0.545351}, 597 | {22,38.4239,6.80061,-0.545351}, 598 | {24,38.4239,6.80061,-0.545351}, 599 | {25,38.4239,6.80061,-0.545351}, 600 | {26,38.4239,6.80061,-0.545351}, 601 | {30,38.4239,6.80061,-0.545351}, 602 | {31,38.4239,6.80061,-0.545351}, 603 | {34,38.4239,6.80061,-0.545351}, 604 | {35,38.4239,6.80061,-0.545351}, 605 | {36,38.4239,6.80061,-0.545351}, 606 | {39,38.4239,6.80061,-0.545351}, 607 | {40,38.4239,6.80061,-0.545351}, 608 | {50,38.4239,6.80061,-0.545351}, 609 | {55,38.4239,6.80061,-0.545351}, 610 | {60,38.4239,6.80061,-0.545351}, 611 | {70,38.4239,6.80061,-0.545351}, 612 | {80,38.4239,6.80061,-0.545351}, 613 | {90,38.4239,6.80061,-0.545351}, 614 | {91,38.4239,6.80061,-0.545351}, 615 | {95,38.4239,6.80061,-0.545351}, 616 | {100,38.4239,6.80061,-0.545351}, 617 | {110,38.4239,6.80061,-0.545351}, 618 | {114,38.4239,6.80061,-0.545351}, 619 | {118,38.4239,6.80061,-0.545351} 620 | }, 621 | parent=2, 622 | x=38.4239, 623 | y=6.80061 624 | }, 625 | { 626 | a=-0.064, 627 | keyframes={ 628 | {0,8.13583,71.6728,-0.064}, 629 | {10,8.13583,71.6728,-0.064}, 630 | {12,8.13583,71.6728,-0.064}, 631 | {14,8.13583,71.6728,-0.064}, 632 | {16,8.13583,71.6728,-0.064}, 633 | {17,8.13583,71.6728,-0.064}, 634 | {18,8.13583,71.6728,-0.064}, 635 | {20,8.13583,71.6728,-0.064}, 636 | {22,8.13583,71.6728,-0.064}, 637 | {24,8.13583,71.6728,-0.064}, 638 | {25,8.13583,71.6728,-0.064}, 639 | {26,8.13583,71.6728,-0.064}, 640 | {30,8.13583,71.6728,-0.064}, 641 | {31,8.13583,71.6728,-0.064}, 642 | {34,8.13583,71.6728,-0.064}, 643 | {35,8.13583,71.6728,-0.064}, 644 | {36,8.13583,71.6728,-0.064}, 645 | {39,8.13583,71.6728,-0.064}, 646 | {40,8.13583,71.6728,-0.064}, 647 | {50,8.13583,71.6728,-0.064}, 648 | {55,8.13583,71.6728,-0.064}, 649 | {60,8.13583,71.6728,-0.064}, 650 | {70,8.13583,71.6728,-0.064}, 651 | {80,8.13583,71.6728,-0.064}, 652 | {90,8.13583,71.6728,-0.064}, 653 | {91,8.13583,71.6728,-0.284}, 654 | {95,8.13583,71.6728,-0.2565}, 655 | {100,8.13583,71.6728,-0.064}, 656 | {110,8.13583,71.6728,0.994}, 657 | {114,8.13583,71.6728,-0.064}, 658 | {118,8.13583,71.6728,0.295326} 659 | }, 660 | parent=13, 661 | x=8.13583, 662 | y=71.6728 663 | }, 664 | { 665 | a=0, 666 | keyframes={ 667 | {0,62.9771,5.19517,0}, 668 | {10,62.9771,5.19517,0}, 669 | {12,62.9771,5.19517,0}, 670 | {14,62.9771,5.19517,0}, 671 | {16,62.9771,5.19517,0}, 672 | {17,62.9771,5.19517,0}, 673 | {18,62.9771,5.19517,0}, 674 | {20,62.9771,5.19517,0}, 675 | {22,62.9771,5.19517,0}, 676 | {24,62.9771,5.19517,0}, 677 | {25,62.9771,5.19517,0}, 678 | {26,62.9771,5.19517,0}, 679 | {30,62.9771,5.19517,0}, 680 | {31,62.9771,5.19517,0}, 681 | {34,62.9771,5.19517,0}, 682 | {35,62.9771,5.19517,0}, 683 | {36,62.9771,5.19517,0}, 684 | {39,62.9771,5.19517,0}, 685 | {40,62.9771,5.19517,0}, 686 | {50,62.9771,5.19517,0}, 687 | {55,62.9771,5.19517,0}, 688 | {60,62.9771,5.19517,0}, 689 | {70,62.9771,5.19517,0}, 690 | {80,62.9771,5.19517,0}, 691 | {90,62.9771,5.19517,0}, 692 | {91,62.9771,5.19517,0}, 693 | {95,62.9771,5.19517,0}, 694 | {100,62.9771,5.19517,0}, 695 | {110,62.9771,5.19517,0.118579}, 696 | {114,62.9771,5.19517,0}, 697 | {118,62.9771,5.19517,-0.380871} 698 | }, 699 | parent=16, 700 | x=62.9771, 701 | y=5.19517 702 | } 703 | }, 704 | polys={ 705 | { 706 | bone=1, 707 | color=11, 708 | data={16.962,-6.98775,22.5157,69.925,-5.58835,74.3152,-17.7269,0.954311,-2.22934,-14.7755}, 709 | shade=0.7 710 | }, 711 | { 712 | bone=2, 713 | color=11, 714 | data={-14.2865,-10.0109,12.3975,-13.9471,41.3563,-7.00951,47.7144,4.39177,40.7695,20.4306,-9.68551,21.3081,-17.0398,7.54535}, 715 | shade=0.8 716 | }, 717 | { 718 | bone=3, 719 | color=11, 720 | data={13.8789,-17.3241,50.122,17.9744,49.3558,35.7019,30.1163,51.9567,-13.4318,20.8546,-18.4207,-1.94706,-4.15108,-18.4276}, 721 | shade=0.75 722 | }, 723 | { 724 | bone=4, 725 | color=11, 726 | data={26.2601,-18.5267,19.2916,1.86687,6.945,15.5619,-9.2362,16.1861,-25.1326,6.51592,-36.2567,-16.5122,-40.811,-58.6248,28.421,-58.0824}, 727 | shade=0.85 728 | }, 729 | { 730 | bone=5, 731 | color=11, 732 | data={31.149,-1.17399,-39.6988,-1.14036,-103.822,-67.0573,-98.5267,-102.272,-52.1821,-97.9301,-2.96307,-74.3604,23.0972,-101.248,57.1159,-101.608,63.1159,-79.1344}, 733 | shade=0.7 734 | }, 735 | { 736 | bone=6, 737 | color=11, 738 | data={15.634,-22.5631,22.6243,74.0154,0.301377,88.0054,-22.3552,77.5022,-17.125,-3.73992,7.61029,-23.8891}, 739 | shade=0.7 740 | }, 741 | { 742 | bone=7, 743 | color=11, 744 | data={46.429,17.67,-25.3988,17.67,-21.9774,-14.0309,18.186,-9.52694,45.0343,12.0911}, 745 | shade=0.7 746 | }, 747 | { 748 | bone=9, 749 | color=11, 750 | data={16.941,-17.8149,51.7148,18.6751,49.3558,35.7019,30.1163,51.9567,-14.1497,21.6702,-18.7795,-0.302659,-8.55575,-16.404}, 751 | shade=1 752 | }, 753 | { 754 | bone=10, 755 | color=11, 756 | data={15.8994,-21.94,22.6243,74.0154,2.65538,85.9591,-21.7582,74.9797,-17.125,-3.73992,7.09745,-24.5154}, 757 | shade=0.95 758 | }, 759 | { 760 | bone=11, 761 | color=11, 762 | data={46.429,17.67,-25.3988,17.67,-21.0716,-15.0012,18.186,-9.52694,45.0343,12.0911}, 763 | shade=0.95 764 | }, 765 | { 766 | bone=12, 767 | color=11, 768 | data={24.7949,-2.32449,13.1723,21.6955,-5.81131,22.083,-35.2552,-3.87419,-22.4704,-41.0666,11.2352,-41.454,23.2452,-31.3811}, 769 | shade=1 770 | }, 771 | { 772 | bone=13, 773 | color=11, 774 | data={18.303,-6.66849,24.2002,65.0713,10.2286,82.1651,-7.72954,71.5104,-19.4299,-0.347192,-2.85318,-15.8447}, 775 | shade=0.9 776 | }, 777 | { 778 | bone=15, 779 | color=11, 780 | data={2.76025,-7.38406,18.7765,-13.3631,35.9174,0.812058,29.0968,13.569,16.2749,22.0205,-6.21633,12.2032}, 781 | shade=0.95 782 | }, 783 | { 784 | bone=14, 785 | color=13, 786 | data={9.54089,38.4172,-65.5128,31.69,-61.4656,-9.8719,-10.2144,-35.0507,127.225,-21.9584,123.154,16.3271,34.374,8.44476}, 787 | shade=0.95 788 | }, 789 | { 790 | bone=16, 791 | color=11, 792 | data={-24.7392,-10.8986,12.3975,-13.9471,65.766,-3.81672,71.3493,8.37343,65.1792,23.6234,-3.07169,21.3776,-22.0202,0.982116}, 793 | shade=1 794 | }, 795 | { 796 | bone=17, 797 | color=11, 798 | data={-0.029135,-8.15209,24.196,-10.984,38.7293,-9.02076,36.3041,5.55672,26.2876,20.8179,-2.19665,18.1609}, 799 | shade=0.95 800 | } 801 | } 802 | } 803 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bt/ik/27488f0ef92ac2fac98973fe3b4777a60fc6f244/screenshot.png --------------------------------------------------------------------------------