How to proxy SDK graphics.image

I was looking for a way to use animated images with very little hassle and little work. That is, I wanted to be able to swap a static playdate.graphics.image for an animated one and have it just work. No calls to update a timer, etc.

Thought I'd share an interesting approach which isn't quite a subclass, but more of a proxy. That is, it is a standalone object that forwards undefined methods to the current frame image object. So you can use any function supported by playdate.graphics.image, and when you call any image draw methods, the correct frame of your animation will be drawn.

It works by catching called to undefined methods and then wrapping that call in a method and storing that method on the animated image object. It's more or less a class that builds its methods out lazily. So it will only proxy methods from playdate.graphics.image that you use.

Now, in this example, I'm using my own timer class, but you get the idea. If there's interest, I may pull the appropriate bit of code from my timer class so this can stand alone, but you could swap it out for the SDK's animation object easily enough.

local graphics <const> = playdate.graphics

local function string_has_prefix(str, prefix)
	return (string.sub(str, 1, string.len(prefix)) == prefix)
end

AnimatedImage = {}

function AnimatedImage.new(path, options)
	options = options or {}
	
	local animated_image = {}
	setmetatable(animated_image, AnimatedImage)
	
	animated_image.imagetable = graphics.imagetable.new(path)
	animated_image.timer = Chrono()
	animated_image.last_time = 0
	
	-- Set default options.
	local animation_options = {
		bounce = options.bounce,
		count = options.count,
		after = options.after,
		decay = options.decay,
		start_frame = options.start_frame
	}
	animated_image.timer:animation(options.delay or 0.1, animated_image.imagetable, options, "animation")
	
	return animated_image
end

AnimatedImage.__index = function(animated_image, key)
	local proxy_value = animated_image.imagetable:getImage(animated_image.timer:getAnimationFrame("animation"))[key]
	if type(proxy_value) == "function" then
		local is_draw_call = string_has_prefix(key, "draw")
		rawset(animated_image, key, function(ai, ...)
			if is_draw_call then
				local last_time = ai.last_time
				if last_time == 0 then
					ai.last_time = playdate.getCurrentTimeMilliseconds()
				else
					local cur_time = playdate.getCurrentTimeMilliseconds()
					ai.timer:update((cur_time - last_time) / 1000.0)
					ai.last_time = cur_time
				end
			end
			local img = ai.imagetable:getImage(ai.timer:getAnimationFrame("animation"))
			return img[key](img, ...)
		end)
		return animated_image[key]
	end
	return proxy_value
end

1 Like

Have you checked that my AnimatedSprite library is not suitable for your purposes?
Yet it's sprite class inheritor, not image, but it has build in timer that you don't need to worry about~

Need to get my head around this!

Runable sample would be great.