In [ ]:
import json
import pathlib
import logging

from pikov import JSONGraph

In [ ]:
# Helper for displaying images.

# source: http://nbviewer.ipython.org/gist/deeplook/5162445
from io import BytesIO

from IPython import display
from PIL import Image


def display_pil_image(im):
   """Displayhook function for PIL Images, rendered as PNG."""

   b = BytesIO()
   im.save(b, format='png')
   data = b.getvalue()

   ip_img = display.Image(data=data, format='png', embed=True)
   return ip_img._repr_png_()


# register display func with PNG formatter:
png_formatter = get_ipython().display_formatter.formatters['image/png']
dpi = png_formatter.for_type(Image.Image, display_pil_image)

In [ ]:
sample_dir = (pathlib.Path("..") / ".." / "samples").resolve()

with open(sample_dir / "pikov-core.json") as fp:
    core_types = json.load(fp)
    #graph = JSONGraph.load(fp)

In [ ]:
sample_path = sample_dir / "gamekitty.json"

# Merge core types into pikov.json
graph = JSONGraph.load(sample_path)
for key, item in core_types["guidMap"].items():
    graph._guid_map[key] = item

Build names mapping

To make it a little easier to check that I'm using the correct guids, construct a mapping from names back to guid.

Note: this adds a constraint that no two nodes have the same name, which should not be enforced for general semantic graphs.


In [ ]:
names = {}
for node in graph:
    for edge in node:
        if edge.guid == "169a81aefca74e92b45e3fa03c7021df":
            value = node[edge].value
            if value in names:
                raise ValueError('name: "{}" defined twice'.format(value))
            names[value] = node
     
names["ctor"]

In [ ]:
def name_to_guid(name):
    if name not in names:
        return None
    node = names[name]
    if not hasattr(node, "guid"):
        return None
    return node.guid

Pikov Classes

These classes are the core resources used in defining a "Pikov" file.

Note: ideally these classes could be derived from the graph itself, but I don't (yet) encode type or field information in the pikov.json semantic graph.


In [ ]:
from pikov.sprite import Bitmap, Clip, Frame, FrameList, Point, Resource, Sprite, Transition

Gamekitty

Create instances of the Pikov classes to define a concrete Pikov graph, based on my "gamekitty" animations.

Load the spritesheet

In the previous notebook, we chopped the spritesheet into bitmaps. Find those and save them to an array so that they can be indexed as they were in the original PICO-8 gamekitty doodle.


In [ ]:
resource = Resource(graph, guid=name_to_guid("spritesheet"))

spritesheet = []
for row in range(16):
    for column in range(16):
        sprite_number = row * 16 + column
        bitmap_name = "bitmap[{}]".format(sprite_number)
        bitmap = Bitmap(graph, guid=name_to_guid(bitmap_name))
        spritesheet.append(bitmap)

Create frames for each "clip"

Each animation is defined in terms of sprite numbers. Sometimes a clip should loop, but sometimes it's only used as a transition between looping clips.


In [ ]:
def find_nodes(graph, ctor, cls):
    nodes = set()
    # TODO: With graph formats that have indexes, there should be a faster way.
    for node in graph:
        if node[names["ctor"]] == ctor:
            node = cls(graph, guid=node.guid)
            nodes.add(node)
    return nodes


def find_frames(graph):
    return find_nodes(graph, names["frame"], Frame)


def find_transitions(graph):
    return find_nodes(graph, names["transition"], Transition)


def find_absorbing_frames(graph):
    transitions = find_transitions(graph)
    target_frames = set()
    source_frames = set()
    for transition in transitions:
        target_frames.add(transition.target.guid)
        source_frames.add(transition.source.guid)
    return target_frames - source_frames  # In but not out. Dead end!

In [ ]:
MICROS_12_FPS = int(1e6 / 12)  # 12 frames per second
MICROS_24_FPS = int(1e6 / 24)


def connect_frames(graph, transition_name, source, target):
    transition = Transition(graph, guid=name_to_guid(transition_name))
    transition.name = transition_name
    transition.source = source
    transition.target = target
    return transition


def make_clip(graph, name, sprite_numbers, loop=False, duration=MICROS_12_FPS, guid=None):
    clip_name = "clip[{}]".format(name)
    clip_guid = guid or name_to_guid(clip_name)
    clip = Clip(graph, guid=clip_guid)
    clip.name = clip_name

    if clip.frames:
        logging.warning("Clip already has frames")
        return clip

    frame_list_name = "framelist[{}, 0]".format(name)
    end_of_clip = FrameList(graph, guid=name_to_guid(frame_list_name))
    clip.frames = end_of_clip
    clip.frames.name = "framelist[{}, 0]".format(name)
    previous_sprite_name = None
    previous_frame = None
    for sequence, sprite_number in enumerate(sprite_numbers):
        sprite_name = "{}[{}]".format(name, sequence)
        frame_name = "frames[{}]".format(sprite_name)
        frame = Frame(graph, guid=name_to_guid(frame_name))
        frame.name = frame_name
        frame.bitmap = spritesheet[sprite_number]
        frame.duration_microsections = duration
        
        if previous_sprite_name:
            transition_name = "transitions[{}, {}]".format(
                previous_sprite_name,
                sprite_name)
            connect_frames(graph, transition_name, previous_frame, frame)
        
        previous_sprite_name = sprite_name
        previous_frame = frame
        frame_list_name = "framelist[{}, {}]".format(name, sequence + 1)
        end_of_clip = end_of_clip.append(frame, guid=name_to_guid(frame_list_name))
        end_of_clip.name = frame_list_name
    
    if loop:
        transition_name = "transitions[{}, {}]".format(
            previous_sprite_name,
            "{}[0]".format(name))
        connect_frames(graph, transition_name, previous_frame, clip.frames.head)
    
    return clip

In [ ]:
sit = make_clip(graph, "sit", [0], loop=True)
#sit[0].bitmap.image
sit

In [ ]:
sit_to_stand = make_clip(graph, "sit_to_stand", [1,2,3,4])
sit_to_stand

In [ ]:
stand_waggle = make_clip(graph, "stand_waggle", [26,4], loop=True)
stand_waggle

In [ ]:
stand_to_sit = make_clip(graph, "stand_to_sit", [57, 58, 59, 60, 61])
stand_to_sit

Create the root node

gamekitty should be our root node. It's the only object in our scene right now.


In [ ]:
origin = Point(graph, guid=name_to_guid("origin"))
origin.name = "origin"
origin.x = 0
origin.y = 0
origin

In [ ]:
sprite = Sprite(graph, guid=name_to_guid("gamekitty"))
graph._properties["root"] = sprite.guid
sprite.name = "gamekitty"
sprite.position = origin
sprite.frame = sit[0]
sprite

More clips and transitions


In [ ]:
sit_paw = make_clip(graph, "sit_paw", [62, 63, 64, 65])
sit_paw

In [ ]:
sit_to_crouch = make_clip(graph, "sit_to_crouch", [69, 70, 71])
sit_to_crouch

In [ ]:
crouch = make_clip(graph, "crouch", [72])
crouch

In [ ]:
crouch_to_sit = make_clip(graph, "crouch_to_sit", [75, 76, 77])
crouch_to_sit

In [ ]:
find_absorbing_frames(graph)

In [ ]:
graph.save()

In [ ]: