ya2 · news · projects · code · about

fix: building of screenshots
[pmachines.git] / pmachines / scene.py
index 3baf29aed1cac5a8ef6ef1ec3ad8df3df4ad77a7..b2cc3d5960de435e9501d895f4215c661545e8e6 100644 (file)
-from panda3d.core import AmbientLight, DirectionalLight, Point3
+from os.path import exists
+from glob import glob
+from importlib import import_module
+from inspect import isclass
+from panda3d.core import AmbientLight, DirectionalLight, Point3, Texture, \
+    TextPropertiesManager, TextNode, Spotlight, PerspectiveLens, BitMask32
+from panda3d.bullet import BulletPlaneShape, BulletGhostNode
+from direct.gui.OnscreenImage import OnscreenImage
+from direct.gui.OnscreenText import OnscreenText
+from direct.gui.DirectGui import DirectButton, DirectFrame
+from direct.gui.DirectGuiGlobals import FLAT, DISABLED, NORMAL
 from direct.showbase.DirectObject import DirectObject
 from pmachines.items.background import Background
-from pmachines.items.box import Box
+from pmachines.sidepanel import SidePanel
+from lib.engine.gui.cursor import MouseCursor
+from lib.lib.p3d.gfx import P3dGfxMgr
 
 
 class Scene(DirectObject):
 
-    def __init__(self, world):
+    def __init__(self, world, exit_cb, auto_close_instr, dbg_items, reload_cb):
         super().__init__()
         self._world = world
+        self._exit_cb = exit_cb
+        self._dbg_items = dbg_items
+        self._reload_cb = reload_cb
         self._set_camera()
+        self._cursor = MouseCursor(
+            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
+            (.01, .01))
+        self._set_gui()
         self._set_lights()
         self._set_input()
-        Background()
-        self.items = [Box(world)]
+        self._set_mouse_plane()
+        self.items = []
+        self.reset()
+        self._state = 'init'
+        self._paused = False
+        self._item_active = None
+        if auto_close_instr:
+            self.__store_state()
+            self.__restore_state()
+        else:
+            self._set_instructions()
+        self._bg = Background()
+        self._side_panel = SidePanel(world, self._mouse_plane_node, (-5, 4), (-3, 1), 1, self.items)
+        self._scene_tsk = taskMgr.add(self.on_frame, 'on_frame')
+
+    @staticmethod
+    def name():
+        return ''
+
+    def _instr_txt(self):
+        return ''
+
+    def _set_items(self):
+        self.items = []
+
+    def screenshot(self, task=None):
+        tex = Texture('screenshot')
+        buffer = base.win.make_texture_buffer('screenshot', 512, 512, tex, True )
+        cam = base.make_camera(buffer)
+        cam.reparent_to(render)
+        cam.node().get_lens().set_fov(base.camLens.get_fov())
+        cam.set_pos(0, -20, 0)
+        cam.look_at(0, 0, 0)
+        import simplepbr
+        simplepbr.init(
+            window=buffer,
+            camera_node=cam,
+            use_normal_maps=True,
+            use_emission_maps=False,
+            use_occlusion_maps=True,
+            msaa_samples=4,
+            enable_shadows=True)
+        base.graphicsEngine.renderFrame()
+        base.graphicsEngine.renderFrame()
+        fname = self.__class__.__name__
+        if not exists('assets/images/scenes'):
+            makedirs('assets/images/scenes')
+        buffer.save_screenshot('assets/images/scenes/%s.png' % fname)
+        # img = DirectButton(
+        #     frameTexture=buffer.get_texture(), relief=FLAT,
+        #     frameSize=(-.2, .2, -.2, .2))
+        return buffer.get_texture()
+
+    def current_bottom(self):
+        curr_bottom = 1
+        for item in self.items:
+            if item.repos_done:
+                curr_bottom = min(curr_bottom, item.get_bottom())
+        return curr_bottom
+
+    def reset(self):
+        [itm.destroy() for itm in self.items]
+        self._set_items()
+        self._state = 'init'
+        self._commands = []
+        self._command_idx = 0
+        if hasattr(self, '_success_txt'):
+            self._success_txt.destroy()
+            del self._success_txt
+        self.__right_btn['state'] = NORMAL
+
+    def destroy(self):
+        self._unset_gui()
+        self._unset_lights()
+        self._unset_input()
+        self._unset_mouse_plane()
+        [itm.destroy() for itm in self.items]
+        self._bg.destroy()
+        self._side_panel.destroy()
+        self._cursor.destroy()
+        taskMgr.remove(self._scene_tsk)
+        if hasattr(self, '_success_txt'):
+            self._success_txt.destroy()
 
     def _set_camera(self):
         base.camera.set_pos(0, -20, 0)
         base.camera.look_at(0, 0, 0)
 
-    def _set_directional_light(self, name, hpr, color):
-        light = DirectionalLight(name)
+    def __load_img_btn(self, path, col):
+        img = OnscreenImage('assets/images/buttons/%s.dds' % path)
+        img.set_transparency(True)
+        img.set_color(col)
+        img.detach_node()
+        return img
+
+    def _set_gui(self):
+        def load_images_btn(path, col):
+            colors = {
+                'gray': [
+                    (.6, .6, .6, 1),  # ready
+                    (1, 1, 1, 1), # press
+                    (.8, .8, .8, 1), # rollover
+                    (.4, .4, .4, .4)],
+                'green': [
+                    (.1, .68, .1, 1),
+                    (.1, 1, .1, 1),
+                    (.1, .84, .1, 1),
+                    (.4, .1, .1, .4)]}[col]
+            return [self.__load_img_btn(path, col) for col in colors]
+        abl, abr = base.a2dBottomLeft, base.a2dBottomRight
+        btn_info = [
+            ('home', self.on_home, NORMAL, abl, 'gray'),
+            ('information', self._set_instructions, NORMAL, abl, 'gray'),
+            ('right', self.on_play, NORMAL, abr, 'green'),
+            ('next', self.on_next, DISABLED, abr, 'gray'),
+            ('previous', self.on_prev, DISABLED, abr, 'gray'),
+            ('rewind', self.reset, NORMAL, abr, 'gray')]
+        num_l = num_r = 0
+        btns = []
+        for binfo in btn_info:
+            imgs = load_images_btn(binfo[0], binfo[4])
+            if binfo[3] == base.a2dBottomLeft:
+                sign, num = 1, num_l
+                num_l += 1
+            else:
+                sign, num = -1, num_r
+                num_r += 1
+            fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+            btn = DirectButton(
+                image=imgs, scale=.05, pos=(sign * (.06 + .11 * num), 1, .06),
+                parent=binfo[3], command=binfo[1], state=binfo[2], relief=FLAT,
+                frameColor=fcols[0] if binfo[2] == NORMAL else fcols[1],
+                rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+                clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+            btn.set_transparency(True)
+            btns += [btn]
+        self.__home_btn, self.__info_btn, self.__right_btn, self.__next_btn, \
+            self.__prev_btn, self.__rewind_btn = btns
+        if self._dbg_items:
+            self._info_txt = OnscreenText(
+                '', parent=base.a2dTopRight, scale=0.04,
+                pos=(-.03, -.06), fg=(.9, .9, .9, 1), align=TextNode.A_right)
+
+    def _unset_gui(self):
+        btns = [
+            self.__home_btn, self.__info_btn, self.__right_btn,
+            self.__next_btn, self.__prev_btn, self.__rewind_btn]
+        [btn.destroy() for btn in btns]
+        if self._dbg_items:
+            self._info_txt.destroy()
+
+    def _set_spotlight(self, name, pos, look_at, color, shadows=False):
+        light = Spotlight(name)
+        if shadows:
+            light.setLens(PerspectiveLens())
         light_np = render.attach_new_node(light)
-        light_np.set_hpr(*hpr)
+        light_np.set_pos(pos)
+        light_np.look_at(look_at)
         light.set_color(color)
         render.set_light(light_np)
+        return light_np
 
     def _set_lights(self):
         alight = AmbientLight('alight')  # for ao
-        alight.set_color((.4, .4, .4, 1))
-        alnp = render.attach_new_node(alight)
-        render.set_light(alnp)
-        self._set_directional_light('key light', (315, -60, 0),
-                                    (3.6, 3.6, 3.6, 1))
-        self._set_directional_light('fill light', (195, -30, 0),
-                                    (.4, .4, .4, 1))
-        self._set_directional_light('rim light', (75, -30, 0), (.3, .3, .3, 1))
+        alight.set_color((.15, .15, .15, 1))
+        self._alnp = render.attach_new_node(alight)
+        render.set_light(self._alnp)
+        self._key_light = self._set_spotlight(
+            'key light', (-5, -80, 5), (0, 0, 0), (2.8, 2.8, 2.8, 1))
+        self._shadow_light = self._set_spotlight(
+            'key light', (-5, -80, 5), (0, 0, 0), (.58, .58, .58, 1), True)
+        self._shadow_light.node().set_shadow_caster(True, 2048, 2048)
+        self._shadow_light.node().get_lens().set_film_size(2048, 2048)
+        self._shadow_light.node().get_lens().set_near_far(1, 256)
+        self._shadow_light.node().set_camera_mask(BitMask32(0x01))
+
+    def _unset_lights(self):
+        for light in [self._alnp, self._key_light, self._shadow_light]:
+            render.clear_light(light)
+            light.remove_node()
 
     def _set_input(self):
-        self.accept('mouse1-up', self.on_click)
-
-    def on_click(self):
-        if not base.mouseWatcherNode.has_mouse(): return
-        p_from = Point3()  # in camera coordinates
-        p_to = Point3()    # in camera coordinates
-        base.camLens.extrude(base.mouseWatcherNode.get_mouse(), p_from, p_to)
-        p_from = render.get_relative_point(base.cam, p_from)  # global coords
-        p_to = render.get_relative_point(base.cam, p_to)  # global coords
-        for hit in self._world.ray_test_all(p_from, p_to).get_hits():
-            for item in [i for i in self.items if hit.get_node() == i.node]:
-                item.on_click(hit)
+        self.accept('mouse1', self.on_click_l)
+        self.accept('mouse1-up', self.on_release)
+        self.accept('mouse3', self.on_click_r)
+        self.accept('mouse3-up', self.on_release)
+
+    def _unset_input(self):
+        for evt in ['mouse1', 'mouse1-up', 'mouse3', 'mouse3-up']:
+            self.ignore(evt)
+
+    def _set_mouse_plane(self):
+        shape = BulletPlaneShape((0, -1, 0), 0)
+        #self._mouse_plane_node = BulletRigidBodyNode('mouse plane')
+        self._mouse_plane_node = BulletGhostNode('mouse plane')
+        self._mouse_plane_node.addShape(shape)
+        #np = render.attachNewNode(self._mouse_plane_node)
+        #self._world.attachRigidBody(self._mouse_plane_node)
+        self._world.attach_ghost(self._mouse_plane_node)
+
+    def _unset_mouse_plane(self):
+        self._world.remove_ghost(self._mouse_plane_node)
+
+    def _get_hits(self):
+        if not base.mouseWatcherNode.has_mouse(): return []
+        p_from, p_to = P3dGfxMgr.world_from_to(base.mouseWatcherNode.get_mouse())
+        return self._world.ray_test_all(p_from, p_to).get_hits()
+
+    def _update_info(self, item):
+        txt = ''
+        if item:
+            txt = '%.3f %.3f\n%.3f°' % (
+                item._np.get_x(), item._np.get_z(), item._np.get_r())
+        self._info_txt['text'] = txt
+
+    def _on_click(self, method):
+        if self._paused:
+            return
+        for hit in self._get_hits():
+            if hit.get_node() == self._mouse_plane_node:
+                pos = hit.get_hit_pos()
+        for hit in self._get_hits():
+            for item in [i for i in self.items if hit.get_node() == i.node and i.interactable]:
+                if not self._item_active:
+                    self._item_active = item
+                getattr(item, method)(pos)
+                img = 'move' if method == 'on_click_l' else 'rotate'
+                if not (img == 'rotate' and not item._instantiated):
+                    self._cursor.set_image('assets/images/buttons/%s.dds' % img)
+
+    def on_click_l(self):
+        self._on_click('on_click_l')
+
+    def on_click_r(self):
+        self._on_click('on_click_r')
+
+    def on_release(self):
+        if self._item_active and not self._item_active._first_command:
+            self._commands = self._commands[:self._command_idx]
+            self._commands += [self._item_active]
+            self._command_idx += 1
+            self.__prev_btn['state'] = NORMAL
+            fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+            self.__prev_btn['frameColor'] = fcols[0]
+            if self._item_active._command_idx == len(self._item_active._commands) - 1:
+                self.__next_btn['state'] = DISABLED
+                self.__next_btn['frameColor'] = fcols[1]
+        self._item_active = None
+        [item.on_release() for item in self.items]
+        self._cursor.set_image('assets/images/buttons/arrowUpLeft.dds')
+
+    def repos(self):
+        for item in self.items:
+            item.repos_done = False
+        self.items = sorted(self.items, key=lambda itm: itm.__class__.__name__)
+        [item.on_aspect_ratio_changed() for item in self.items]
+        self._side_panel.update(self.items)
+        max_x = -float('inf')
+        for item in self.items:
+            if not item._instantiated:
+                max_x = max(item._np.get_x(), max_x)
+        for item in self.items:
+            if not item._instantiated:
+                item.repos_x(max_x)
+
+    def on_aspect_ratio_changed(self):
+        self.repos()
+
+    def _win_condition(self):
+        pass
+
+    def _fail_condition(self):
+        return all(itm.fail_condition() for itm in self.items) and not self._paused and self._state == 'playing'
+
+    def on_frame(self, task):
+        hits = self._get_hits()
+        pos = None
+        for hit in self._get_hits():
+            if hit.get_node() == self._mouse_plane_node:
+                pos = hit.get_hit_pos()
+        hit_nodes = [hit.get_node() for hit in hits]
+        if self._item_active:
+            items_hit = [self._item_active]
+        else:
+            items_hit = [itm for itm in self.items if itm.node in hit_nodes]
+        items_no_hit = [itm for itm in self.items if itm not in items_hit]
+        [itm.on_mouse_on() for itm in items_hit]
+        [itm.on_mouse_off() for itm in items_no_hit]
+        if pos and self._item_active:
+            self._item_active.on_mouse_move(pos)
+        if self._dbg_items:
+            self._update_info(items_hit[0] if items_hit else None)
+        if self._win_condition():
+            self._set_win()
+        elif self._state == 'playing' and self._fail_condition():
+            self._set_fail()
+        if any(itm._overlapping for itm in self.items):
+            self._cursor.cursor_img.img.set_color(.9, .1, .1, 1)
+        else:
+            self._cursor.cursor_img.img.set_color(.9, .9, .9, 1)
+        return task.cont
+
+    def cb_inst(self, item):
+        self.items += [item]
+
+    def on_play(self):
+        self._state = 'playing'
+        self.__prev_btn['state'] = DISABLED
+        self.__next_btn['state'] = DISABLED
+        self.__right_btn['state'] = DISABLED
+        [itm.play() for itm in self.items]
+
+    def on_next(self):
+        self._commands[self._command_idx].redo()
+        self._command_idx += 1
+        fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        self.__prev_btn['state'] = NORMAL
+        self.__prev_btn['frameColor'] = fcols[0]
+        more_commands = self._command_idx < len(self._commands)
+        self.__next_btn['state'] = NORMAL if more_commands else DISABLED
+        self.__next_btn['frameColor'] = fcols[0] if more_commands else fcols[1]
+
+    def on_prev(self):
+        self._command_idx -= 1
+        self._commands[self._command_idx].undo()
+        fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+        self.__next_btn['state'] = NORMAL
+        self.__next_btn['frameColor'] = fcols[0]
+        self.__prev_btn['state'] = NORMAL if self._command_idx else DISABLED
+        self.__prev_btn['frameColor'] = fcols[0] if self._command_idx else fcols[1]
+
+    def on_home(self):
+        self._exit_cb()
+
+    def _set_instructions(self):
+        self._paused = True
+        self.__store_state()
+        mgr = TextPropertiesManager.get_global_ptr()
+        for name in ['mouse_l', 'mouse_r']:
+            graphic = OnscreenImage('assets/images/buttons/%s.dds' % name)
+            graphic.set_scale(.5)
+            graphic.get_texture().set_minfilter(Texture.FTLinearMipmapLinear)
+            graphic.get_texture().set_anisotropic_degree(2)
+            mgr.set_graphic(name, graphic)
+            graphic.set_z(-.2)
+            graphic.set_transparency(True)
+            graphic.detach_node()
+        frm = DirectFrame(frameColor=(.4, .4, .4, .06),
+                          frameSize=(-.6, .6, -.3, .3))
+        font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
+        font.clear()
+        font.set_pixels_per_unit(60)
+        font.set_minfilter(Texture.FTLinearMipmapLinear)
+        font.set_outline((0, 0, 0, 1), .8, .2)
+        self._txt = OnscreenText(
+            self._instr_txt(), parent=frm, font=font, scale=0.06,
+            fg=(.9, .9, .9, 1), align=TextNode.A_left)
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
+        btn_scale = .05
+        mar = .06  # margin
+        z = h / 2 - font.get_line_height() * self._txt['scale'][1]
+        z += (btn_scale + 2 * mar) / 2
+        self._txt['pos'] = -w / 2, z
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
+        fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
+        frm['frameSize'] = fsz
+        colors = [
+            (.6, .6, .6, 1),  # ready
+            (1, 1, 1, 1), # press
+            (.8, .8, .8, 1), # rollover
+            (.4, .4, .4, .4)]
+        imgs = [self.__load_img_btn('exitRight', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(l_r[0] - btn_scale, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self.__on_close_instructions, extraArgs=[frm],
+            relief=FLAT, frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.set_transparency(True)
+
+    def _set_win(self):
+        loader.load_sfx('assets/audio/sfx/success.ogg').play()
+        self._paused = True
+        self.__store_state()
+        frm = DirectFrame(frameColor=(.4, .4, .4, .06),
+                          frameSize=(-.6, .6, -.3, .3))
+        font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
+        font.clear()
+        font.set_pixels_per_unit(60)
+        font.set_minfilter(Texture.FTLinearMipmapLinear)
+        font.set_outline((0, 0, 0, 1), .8, .2)
+        self._txt = OnscreenText(
+            _('You win!'),
+            parent=frm,
+            font=font, scale=0.2,
+            fg=(.9, .9, .9, 1))
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
+        btn_scale = .05
+        mar = .06  # margin
+        z = h / 2 - font.get_line_height() * self._txt['scale'][1]
+        z += (btn_scale + 2 * mar) / 2
+        self._txt['pos'] = 0, z
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
+        fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
+        frm['frameSize'] = fsz
+        colors = [
+            (.6, .6, .6, 1),  # ready
+            (1, 1, 1, 1), # press
+            (.8, .8, .8, 1), # rollover
+            (.4, .4, .4, .4)]
+        imgs = [self.__load_img_btn('home', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(-2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self._on_end_home, extraArgs=[frm],
+            relief=FLAT, frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.set_transparency(True)
+        imgs = [self.__load_img_btn('rewind', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(0, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self._on_restart, extraArgs=[frm],
+            relief=FLAT, frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.set_transparency(True)
+        scenes = []
+        for _file in glob('pmachines/scenes/*.py'):
+            _fn = _file.replace('.py', '').replace('/', '.')
+            for member in import_module(_fn).__dict__.values():
+                if isclass(member) and issubclass(member, Scene) and \
+                        member != Scene:
+                    scenes += [member]
+        scenes = sorted(scenes, key=lambda elm: elm.sorting)
+        enabled = scenes.index(self.__class__) < len(scenes) - 1
+        if enabled:
+            next_scene = scenes[scenes.index(self.__class__) + 1]
+        else:
+            next_scene = None
+        imgs = [self.__load_img_btn('right', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self._on_next_scene,
+            extraArgs=[frm, next_scene], relief=FLAT,
+            frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn['state'] = NORMAL if enabled else DISABLED
+        btn.set_transparency(True)
+
+    def _set_fail(self):
+        loader.load_sfx('assets/audio/sfx/success.ogg').play()
+        self._paused = True
+        self.__store_state()
+        frm = DirectFrame(frameColor=(.4, .4, .4, .06),
+                          frameSize=(-.6, .6, -.3, .3))
+        font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
+        font.clear()
+        font.set_pixels_per_unit(60)
+        font.set_minfilter(Texture.FTLinearMipmapLinear)
+        font.set_outline((0, 0, 0, 1), .8, .2)
+        self._txt = OnscreenText(
+            _('You have failed!'),
+            parent=frm,
+            font=font, scale=0.2,
+            fg=(.9, .9, .9, 1))
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
+        btn_scale = .05
+        mar = .06  # margin
+        z = h / 2 - font.get_line_height() * self._txt['scale'][1]
+        z += (btn_scale + 2 * mar) / 2
+        self._txt['pos'] = 0, z
+        u_l = self._txt.textNode.get_upper_left_3d()
+        l_r = self._txt.textNode.get_lower_right_3d()
+        c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
+        fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
+        frm['frameSize'] = fsz
+        colors = [
+            (.6, .6, .6, 1),  # ready
+            (1, 1, 1, 1), # press
+            (.8, .8, .8, 1), # rollover
+            (.4, .4, .4, .4)]
+        imgs = [self.__load_img_btn('home', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(-2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self._on_end_home, extraArgs=[frm],
+            relief=FLAT, frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.set_transparency(True)
+        imgs = [self.__load_img_btn('rewind', col) for col in colors]
+        btn = DirectButton(
+            image=imgs, scale=btn_scale,
+            pos=(0, 1, l_r[2] - mar - btn_scale),
+            parent=frm, command=self._on_restart, extraArgs=[frm],
+            relief=FLAT, frameColor=(.6, .6, .6, .08),
+            rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+            clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+        btn.set_transparency(True)
+
+    def _on_restart(self, frm):
+        self.__on_close_instructions(frm)
+        self.reset()
+
+    def _on_end_home(self, frm):
+        self.__on_close_instructions(frm)
+        self.on_home()
+
+    def _on_next_scene(self, frm, scene):
+        self.__on_close_instructions(frm)
+        self._reload_cb(scene)
+
+    def __store_state(self):
+        btns = [
+            self.__home_btn, self.__info_btn, self.__right_btn,
+            self.__next_btn, self.__prev_btn, self.__rewind_btn]
+        self.__btn_state = [btn['state'] for btn in btns]
+        for btn in btns:
+            btn['state'] = DISABLED
+        [itm.store_state() for itm in self.items]
+
+    def __restore_state(self):
+        btns = [
+            self.__home_btn, self.__info_btn, self.__right_btn,
+            self.__next_btn, self.__prev_btn, self.__rewind_btn]
+        for btn, state in zip(btns, self.__btn_state):
+            btn['state'] = state
+        [itm.restore_state() for itm in self.items]
+        self._paused = False
+
+    def __on_close_instructions(self, frm):
+        frm.remove_node()
+        self.__restore_state()