bbcmicrobit / micropython

Port of MicroPython for the BBC micro:bit
https://microbit-micropython.readthedocs.io
Other
604 stars 283 forks source link

Create built in image effects. #74

Open ntoll opened 8 years ago

ntoll commented 8 years ago

Pass in a single image and get back a list of images to animate a referenced effect:

Perhaps these functions could live under microbit.display.fx..? E.g.

>>> twinkle_happy = display.fx.sparkle(Image.HAPPY)
>>> twinkle_happy
[ ... a shortish list of Image objects ... ] 
>>> display.animate(twinkle_happy, 50)
dpgeorge commented 8 years ago

Perhaps these functions could live under microbit.display.fx..?

Or just be methods of an Image instance, eg:

>>> twinkle_happy = Image.HAPPY.sparkle()
markshannon commented 8 years ago

I would suggest that each "effect" function should take a single image and return an iterators of images. They could be methods on images, but iterators are not the simplest of concepts, so its best to wrap them in a utility functions. Something like: display.FX.sparkle(img) which would call display.animate(_FX.sparkle(img))

deshipu commented 8 years ago

I think that effects should rather be objects using the iterator protocol and generating the frames on the fly, than functions returning lists (that saves memory).

It would be great if they could accept both a single image (in which case they would use it for the base of each frame), or an iterable of images (in which case the effect would be added to each image from the iterable) -- that would let us compose animations.

The display.animate function duplicates what display.print already does for text, maybe those two could be merged into a single display.show that takes either an image or an iterable of images or characters.

Not sure if that is a good idea, but there could also be a "magical" attribute that display.show looks for, similar to how __repr__ works for print, which would allow making any user object "displayable".

markshannon commented 8 years ago

On 11/11/15 09:46, Radomir Dopieralski wrote:

I think that effects should rather be objects using the iterator protocol and generating the frames on the fly, than functions returning lists (that saves memory). Indeed, I think we are in agreement.

It would be great if they could accept both a single image (in which case they would use it for the base of each frame), or an iterable of images (in which case the effect would be added to each image from the iterable) -- that would let us compose animations. Here we disagree. I think it would make the interface overly complex and potentially confusing. You can already compose iterables and functions: display.show(chain(map(fx.sparkle, list_of_images))) (A more readable form is left as an exercise :) )

The |display.animate| function duplicates what |display.print| already does for text, maybe those two could be merged into a single |display.show| that takes either an image or an iterable of images or characters. Indeed. In fact that is what scroll and animate do already behind the scenes; they create an iterator of images which is then passed to display.print/show

Not sure if that is a good idea, but there could also be a "magical" attribute that |display.show| looks for, similar to how |repr| works for print, which would allow making any user object "displayable". __image__ presumably?

deshipu commented 8 years ago

Here we disagree. I think it would make the interface overly complex and potentially confusing. You can already compose iterables and functions: display.show(chain(map(fx.sparkle, list_of_images))) (A more readable form is left as an exercise :) )

That doesn't do the same thing. It would play an animation of each image sparkling, one after the other. However, if fx.sparkle would accept an iterator and then use each image as the base for each of its frames, you could have something like:

display.show(fx.shake(fx.sparkle(Image.HEART)))

which would display an animation of a heart both shaking and sparkling at the same time. You can't do that in any other way (except for some trivial cases, where you could maybe use the + operator to overlay one list of images on another, but most animations are more than just adding an image on top).

markshannon commented 8 years ago

I don't see how effects could be composable in the way you describe. What would fx.wipe_left(fx.wipe_right(some_image)) do, and how would it work?

deshipu commented 8 years ago

Of course it would work. It would wipe the image from both sides at the same time, narrowing it, so to say.

deshipu commented 8 years ago

For the effects that cannot take a different image for the base of each frame, they could fall back to just using the first frame. But most effects are easily composable by just applying all previous steps to the next base image.

markshannon commented 8 years ago

Here is a simple implementation of wipe_left implementing the Image -> List(Image) interface.

WIPE_LEFT = Image("99999876543210000:"*5)
def wipe_left(img):
     for i in range(12, -1, -1):
          yield WIPE_LEFT.crop(i, 0, 5, 5) * img

How would you implement the List(Image) -> List(Image)?

deshipu commented 8 years ago
WIPE_LEFT = Image("99999876543210000:"*5)
def wipe_left(imgs):
    for i in range(12, -1, -1):
          yield WIPE_LEFT.crop(i, 0, 5, 5) * next(imgs)
    yield from imgs
deshipu commented 8 years ago

Of course you need to have a check if the thing is iterable.

markshannon commented 8 years ago

Presumable you would just convert a single image to a sequence of the same image being repeated? Would all effects need to be infinite sequences?

markshannon commented 8 years ago

being repeated -> repeated for ever

deshipu commented 8 years ago

Well, this is something to be discussed -- how to handle sequences of different lengths. As you see, here I just apply the effect to the first 12 frames, and then return all the rest of frames unchanged. It may make sense to do something else, depending on the effect. For instance, for a fade-out effect, it may make sense to return empty frames.

I don't think returning infinite sequences is wise, since you still need to display it -- and the display function needs to know when to stop.

Note also, that if the imgs iterator at any moment ends, raising StopIteration, that will also end the wipe_left iterator, so the resulting sequence is as most as long as the original sequence. This may not always be true -- you may have effects such as "slow motion" or "fast forward", that actually add or remove frames. For other effects, it would probably make sense to specify with an additional parameter how many frames the effect should take, and maybe even how many frames should be skipped from the beginning?