commit 093f67ecfaa4b84a62f4c83701ff868f8eb056d9 Author: Jon Michael Aanes Date: Thu Nov 8 16:27:03 2018 +0100 Separated SpriteSheet2 as own library diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..f680024 --- /dev/null +++ b/init.lua @@ -0,0 +1,3 @@ + +return require ((...) .. '.spritesheet') + diff --git a/spritesheet.lua b/spritesheet.lua new file mode 100644 index 0000000..88f3dd3 --- /dev/null +++ b/spritesheet.lua @@ -0,0 +1,321 @@ + +--local error = require 'errors' 'SpriteSheet2' + +-------------------------------------------------------------------------------- +-- Util + +local function calculate_animation_duration (self, frame_i) + local frame_i = frame_i or math.huge + assert(type(self) == 'table') + assert(type(frame_i) == 'number') + -- Easy if number + if type(self.time) == 'number' then return math.min(#self, frame_i) * self.time end + -- Sum if table + local sum = 0 + for i = 1, math.min(#self, frame_i) do sum = sum + self.time[i] end + return sum +end + +local function get_quad_based_on_time (l, t) + -- TODO: Reimplement as binary search. (Maybe only use the binary search version for very long animations?) + -- + assert(type(l) == 'table') + assert(type(t) == 'number') + -- + local time = l.time + for i = 1, #time do + if t <= time[i] then + return l[i] + end + end + -- + return l[#l] +end + +-------------------------------------------------------------------------------- +---- Sprite + +local Sprite = {} + Sprite.__index = Sprite + +function Sprite.new (quad, imagesheet) + return setmetatable({ quad = quad, imagesheet = imagesheet, is_sprite = true }, Sprite) +end + +function Sprite:generateImage () + local imagesheet, quad = self.imagesheet, self.quad + self.func = function (x, y) + love.graphics.draw(imagesheet.image, quad, math.floor(x), math.floor(y), 0, 1, 1, imagesheet.origin_x, imagesheet.origin_y) + end +end + +function Sprite:getImage () + if not self.func then self:generateImage() end + return self.func +end + +function Sprite:getQuad () + return self.quad +end + +function Sprite:draw(...) + return self:getImage()(...) +end + +setmetatable(Sprite, {__call = function(_, ...) return Sprite.new(...) end}) + +-------------------------------------------------------------------------------- +---- Animation + +local Animation = {} + Animation.__index = Animation + +function Animation.new (self) + assert(type(self) == 'table') + assert(type(self.time) == 'table' or type(self.time) == 'number' and self.time > 0) + assert(#self > 0) + assert(type(self.time) == 'number' or #self == #self.time) + + local self = setmetatable(self, Animation) + self.duration = calculate_animation_duration(self) + self.is_animation = true + -- Contact frame? + if self.contact_frame then + -- why was this minus one? It screwed up the audio. + --self.contact_time = calculate_animation_duration(self, self.contact_frame - 1) + self.contact_time = calculate_animation_duration(self, self.contact_frame) + end + -- + return self +end + +function Animation:generateImage () + self.func = function (x, y, t) + t = t or 0 + assert(type(t) == 'number') + + if self.wrap then t = t % self.duration end + local quad = get_quad_based_on_time(self, t) + if not quad then error.internal('Could not determine quad when drawing animation. Time was %f.', t) end + love.graphics.draw(self.imagesheet.image, quad, x, y, 0, 1, 1, self.imagesheet.origin_x, self.imagesheet.origin_y) + end +end + +function Animation:getImage () + if not self.func then self:generateImage() end + return self.func +end + +function Animation:getQuad (i) + assert(i == nil or type(i) == 'number' and self[i]) + return self[i or 1] +end + +function Animation:getDuration () + return self.duration +end + +function Animation:draw(...) + return self:getImage()(...) +end + +setmetatable(Animation, {__call = function(_, ...) return Animation.new(...) end}) + +-------------------------------------------------------------------------------- +-- Constants + +local SPRITESHEET_ENV = { Anim = Animation } +local SPRITESHEET_DATA_FILETYPES = { ['.lua'] = true, ['.raw'] = true } + +-------------------------------------------------------------------------------- + +local function calculate_frame_times (time_list, nr_frames) + assert(type(time_list) == 'table' or type(time_list) == 'number') + assert(type(nr_frames) == 'number') + + local frame_times = { [0] = -math.huge } + + if type(time_list) == 'number' then + for i = 1, nr_frames do + frame_times[i] = i * time_list + end + else + frame_times[1] = time_list[1] + for i = 2, nr_frames do + frame_times[i] = frame_times[i-1] + time_list[i] + end + end + + return frame_times +end + +local function load_quads (image, quad_data, imagesheet) + assert(image.typeOf and image:typeOf('Image')) + assert(type(quad_data) == 'table') + + local image_width, image_height = image:getDimensions() + local tile_width, tile_height = quad_data.tile_width, quad_data.tile_height + local tiles_pr_row = image_width/tile_width + local max_quad_id = tiles_pr_row * (image_height/tile_height) - 1 + local quad_cache = {} + + local function quad_from_id (id) + -- Error checking + if type(id) ~= 'number' then error('All quad ids must be natural numbers, but one was %s (%s)', id, type(id)) end + if id ~= math.floor(id) then error('All quad ids must be natural numbers, but one was %03.03f (floating point number)', id) end + if not (0 <= id and id <= max_quad_id) then error('All quad ids must - for this spritesheet ("%s") - be natural numbers equal/below %i, but one was %s = 0x%X', imagesheet.filename, max_quad_id, id, id) end + + -- Calculate + if not quad_cache[id] then + quad_cache[id] = love.graphics.newQuad((id%tiles_pr_row)*tile_width, math.floor(id/tiles_pr_row)*tile_height, tile_width, tile_height, image_width, image_height) + end + return quad_cache[id] + end + + local function visit_animation (t) + assert(type(t) == 'table' and t.is_animation) + for i = 1, #t do t[i] = quad_from_id(t[i]) end + t.time = calculate_frame_times(t.time, #t) + t.imagesheet = imagesheet + end + + local function visit_quad (n) + assert(type(n) == 'number') + return Sprite(quad_from_id(n), imagesheet) + end + + local function visit_node (t) + assert(type(t) == 'table') + + for key, val in pairs(t) do + local val_type = type(val) + if val_type == 'number' then + t[key] = visit_quad(val) + elseif val_type == 'table' then + local visit_func = val.is_animation and visit_animation or visit_node + visit_func(val) + end + end + + error.strict_table(t) + end + + assert(type(quad_data) == 'table') + assert(type(quad_data.tile_names) == 'table') + + local visit_func = quad_data.tile_names.is_animation and visit_animation or visit_node + visit_func(quad_data.tile_names) + return quad_data.tile_names +end + +local function load_quad_data (filename) + if type(filename) ~= 'string' then error('Bad argument #1, expected string, got %s (%s)', filename, type(filename)) end + + -- Attempt to load file + for filetype in pairs(SPRITESHEET_DATA_FILETYPES) do + local chunk, error_msg = love.filesystem.load(filename..'.lua') + if chunk then + local data = setfenv(chunk, SPRITESHEET_ENV)() + -- Error check + if type(data) ~= 'table' then error('Bad spritesheet "%s". Must return a table, but returned %s (%s)', filename, data, type(data)) end + if type(data.tile_width) ~= 'number' then error('Bad spritesheet "%s". The root table must contain key "tile_width", with integer value, but it was %s (%s)', filename, data.tile_width, type(data.tile_width)) end + if data.tile_width ~= math.floor(data.tile_width) then error('Bad spritesheet "%s". The root table must contain key "tile_width", with integer value, but it was %f (float)', filename, data.tile_width) end + if type(data.tile_height) ~= 'number' then error('Bad spritesheet "%s". The root table must contain key "tile_height", with integer value, but it was %s (%s)', filename, data.tile_height, type(data.tile_height)) end + if data.tile_height ~= math.floor(data.tile_height) then error('Bad spritesheet "%s". The root table must contain key "tile_height", with integer value, but it was %f (float)', filename, data.tile_height) end + if not (type(data.tile_names) == 'table' or type(data.tile_names) == 'number') then error('Bad spritesheet "%s". The root table must contain key "tile_names", with either a table or a number value, but it was %s (%s)', filename, data.tile_names, type(data.tile_names)) end + if data.tile_origin and type(data.tile_origin) ~= 'table' then error('Bad spritesheet "%s". If the root table contains key "tile_origin", it must be a table value, but it was %s (%s)', filename, data.tile_origin, type(data.tile_origin)) end + -- + return data + end + print(error_msg) + end + + -- Else, give up. + error('Could not find file "%s.lua" or "%s.raw".', filename, filename) +end + +-------------------------------------------------------------------------------- + +local SpriteSheet = {} + SpriteSheet.__index = SpriteSheet + SpriteSheet.is_spritesheet = true + +function SpriteSheet.new (filename) + local quad_data = load_quad_data(filename) + + -- Set info + local self = setmetatable({}, SpriteSheet) + self.filename = filename + self.image = love.graphics.newImage(filename..'.png') + self.origin_x = 0 + self.origin_y = 0 + self.tile_width = quad_data.tile_width + self.tile_height = quad_data.tile_height + + -- Error checking + if self.image:getWidth() % self.tile_width ~= 0 then error('Bad spritesheet "%s". Image size (%i, %i) must be dividable by tile size (%i, %i), but width leaves a remainder of %i.', filename, self.image:getWidth(), self.image:getHeight(), self.tile_width, self.tile_height, self.image:getWidth() % self.tile_width) end + if self.image:getHeight() % self.tile_height ~= 0 then error('Bad spritesheet "%s". Image size (%i, %i) must be dividable by tile size (%i, %i), but height leaves a remainder of %i.', filename, self.image:getWidth(), self.image:getHeight(), self.tile_width, self.tile_height, self.image:getHeight() % self.tile_height) end + + -- Set origin + if quad_data.tile_origin then + self:setOrigin(unpack(quad_data.tile_origin)) + end + + -- Import quads into SpriteSheet + self.quads = load_quads(self.image, quad_data, self) + if rawget(self.quads, 'is_sprite') or rawget(self.quads, 'is_animation') then + self.only_quads = self.quads + else + for key, value in pairs(self.quads) do + assert(not self[key]) + self[key] = value + end + end + + -- Return + return self +end + +function SpriteSheet:setOrigin (ox, oy, mode) + assert(type(ox) == 'number') + assert(type(oy) == 'number') + + if mode == 'absolute' then + self.origin_x = ox + self.origin_y = oy + elseif mode == 'relative' or mode == nil then + self.origin_x = self.tile_width * ox + self.origin_y = self.tile_height * oy + else + error('Unknown origin mode "%s"', mode) + end +end + +function SpriteSheet:getQuadKeys () + local keys = {} + for key in pairs(self.quads) do keys[#keys+1] = key end + return keys +end + +local function assert_self (self_v, is_field) + if type(self_v) ~= 'table' then error('Bad self value, must be indexable, but was %s (%s).', self_v, type(self_v)) end + if is_field and not self_v[is_field] then error('Bad self value, is indexable, but does not possess "%s" field.', is_field) end + return true +end + +-- Create redirections +for _, method_name in pairs{ 'getImage', 'getQuad', 'draw', 'getDuration' } do + SpriteSheet[method_name] = function (self, ...) + assert_self (self, 'is_spritesheet') + if not self.only_quads then error('Attempting to call method "%s" on a SpriteSheet ("%s"). This is only allowed when the Spritesheet contains a single sprite or animation.', method_name, self.filename) end + return self.only_quads[method_name](self.only_quads, ...) + end +end + +setmetatable(SpriteSheet, {__call = function(_, ...) return SpriteSheet.new(...) end}) + +-------------------------------------------------------------------------------- +-- Return + +return SpriteSheet +