2018-11-08 15:27:03 +00:00
2019-12-04 17:11:11 +00:00
local error_orig = error
local error , error_internal do
error , error_internal = error_orig , error_orig
error_orig = nil
local success , errorlib = pcall ( require , ' errors ' )
if success then
error = errorlib ' spritesheet '
error_internal = error.internal
end
end
2018-11-08 15:27:03 +00:00
--------------------------------------------------------------------------------
-- 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
2019-02-26 21:45:31 +00:00
function Sprite : getSheetDimensions ( )
return self.imagesheet . image : getDimensions ( )
end
2018-11-08 15:27:03 +00:00
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 )
2018-11-10 14:03:21 +00:00
assert ( self.wrap == nil or self.wrap == true or self.wrap == false )
2018-11-08 15:27:03 +00:00
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 )
2019-12-04 17:11:11 +00:00
if not quad then error_internal ( ' Could not determine quad when drawing animation. Time was %f. ' , t ) end
2018-11-08 15:27:03 +00:00
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
2019-02-26 21:45:31 +00:00
function Animation : getSheetDimensions ( )
return self.imagesheet . image : getDimensions ( )
end
2018-11-08 15:27:03 +00:00
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
2019-12-04 17:11:11 +00:00
local tiles_per_row = image_width / tile_width
local max_quad_id = tiles_per_row * ( image_height / tile_height ) - 1
2018-11-08 15:27:03 +00:00
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
2019-12-04 17:11:11 +00:00
quad_cache [ id ] = love.graphics . newQuad ( ( id % tiles_per_row ) * tile_width , math.floor ( id / tiles_per_row ) * tile_height , tile_width , tile_height , image_width , image_height )
2018-11-08 15:27:03 +00:00
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 ) ( )
2019-12-04 17:11:11 +00:00
2018-11-08 15:27:03 +00:00
-- Error check
2019-12-04 17:11:11 +00:00
if type ( data ) ~= ' table ' then
error ( ' Bad spritesheet "%s". Must return a table, but returned %s (%s) ' , filename , data , type ( data ) )
end
local l = { ' Bad spritesheet " ' .. filename .. ' " ' }
if data.tiles_per_row ~= nil and data.tile_width ~= nil then
l [ # l + 1 ] = ' Root table must not contain both keys "tiles_per_row" and "tile_width" '
elseif data.tiles_per_row == nil and data.tile_width == nil then
l [ # l + 1 ] = ' Root table must contain either keys "tiles_per_row" or "tile_width" '
end
if data.tiles_per_column ~= nil and data.tile_height ~= nil then
l [ # l + 1 ] = ' Root table must not contain both keys "tiles_per_column" and "tile_height" '
elseif data.tiles_per_column == nil and data.tile_height == nil then
l [ # l + 1 ] = ' Root table must contain either keys "tiles_per_column" or "tile_height" '
end
local INTEGER_TILESET_KEYS = { ' tile_width ' , ' tile_height ' , ' tiles_per_row ' , ' tiles_per_column ' }
for _ , integer_key in ipairs ( INTEGER_TILESET_KEYS ) do
local v = data [ integer_key ]
if v and ( type ( v ) ~= ' number ' or v % 1 ~= 0 ) then
l [ # l + 1 ] = string.format ( ' Key "%s" in root table must map to integer value, but it was %s (%s) ' , integer_key , v , type ( v ) )
end
end
if not ( type ( data.tile_names ) == ' table ' or type ( data.tile_names ) == ' number ' ) then
l [ # l + 1 ] = string.format ( ' Root table must contain key "tile_names", with either a table or a number value, but it was %s (%s) ' , data.tile_names , type ( data.tile_names ) )
end
if data.tile_origin and type ( data.tile_origin ) ~= ' table ' then
l [ # l + 1 ] = string.format ( ' Key "%s" in root table must map to a table value, but it was %s (%s) ' , ' tile_origin ' , data.tile_origin , type ( data.tile_origin ) )
end
-- Throw error or return
if # l > 1 then
error ( table.concat ( l , ' \n ' ) )
end
2018-11-08 15:27:03 +00:00
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 )
2019-12-04 17:11:11 +00:00
-- NOTE: `force_uneven_tile_size` in quad_data can be used to
-- ignore the image size-tile size divisibility check. Edit the
-- spriteimage itself if you can, as it will silently ignore
-- several errors.
2018-11-08 15:27:03 +00:00
-- Set info
local self = setmetatable ( { } , SpriteSheet )
self.filename = filename
self.image = love.graphics . newImage ( filename .. ' .png ' )
self.origin_x = 0
self.origin_y = 0
2019-12-04 17:11:11 +00:00
-- TODO: Give warning/error due to rounding down.
quad_data.tile_width = quad_data.tile_width or math.floor ( self.image : getWidth ( ) / quad_data.tiles_per_row )
quad_data.tile_height = quad_data.tile_height or math.floor ( self.image : getHeight ( ) / quad_data.tiles_per_column )
print ( self.tile_width , self.tile_height )
self.tile_width = quad_data.tile_width
self.tile_height = quad_data.tile_height
2018-11-08 15:27:03 +00:00
-- Error checking
2019-12-04 17:11:11 +00:00
do
local rem_width = self.image : getWidth ( ) % self.tile_width
local rem_height = self.image : getHeight ( ) % self.tile_height
if not quad_data.force_uneven_tile_size and ( rem_width ~= 0 or rem_height ~= 0 ) then
local s = ( ' Bad spritesheet "%s". Image size (%i, %i) must be dividable by tile size (%i, %i) ' )
: format ( filename , self.image : getWidth ( ) , self.image : getHeight ( ) , self.tile_width , self.tile_height )
if rem_width ~= 0 then s = s .. ( ' \n Width leaves a remainder of %i. ' ) : format ( rem_width ) end
if rem_height ~= 0 then s = s .. ( ' \n Height leaves a remainder of %i. ' ) : format ( rem_height ) end
error ( s )
end
end
2018-11-08 15:27:03 +00:00
-- 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
2019-06-10 17:41:59 +00:00
error ( ' Unknown origin mode %s (%s) ' , mode , type ( mode ) )
2018-11-08 15:27:03 +00:00
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