adding circuitpython neko cat
This commit is contained in:
parent
99a7e2d70a
commit
e3ad67fbf3
3 changed files with 389 additions and 0 deletions
386
CircuitPython_Neko_Cat/code.py
Normal file
386
CircuitPython_Neko_Cat/code.py
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import board
|
||||||
|
import displayio
|
||||||
|
import adafruit_imageload
|
||||||
|
|
||||||
|
# display background color
|
||||||
|
BACKGROUND_COLOR = 0x00AEF0
|
||||||
|
|
||||||
|
# how long to wait between animation frames in seconds
|
||||||
|
ANIMATION_TIME = 0.3
|
||||||
|
|
||||||
|
|
||||||
|
class NekoAnimatedSprite(displayio.TileGrid):
|
||||||
|
# how many pixels the cat will move for each step
|
||||||
|
CONFIG_STEP_SIZE = 10
|
||||||
|
|
||||||
|
# how likely the cat is to stop moving to clean or sleep.
|
||||||
|
# lower number means more likely to happen
|
||||||
|
CONFIG_STOP_CHANCE_FACTOR = 30
|
||||||
|
|
||||||
|
# how likely the cat is to start moving after scratching a wall.
|
||||||
|
# lower number means mroe likely to heppen
|
||||||
|
CONFIG_START_CHANCE_FACTOR = 10
|
||||||
|
|
||||||
|
# Minimum time to stop and scratch in seconds. larger time means scratch for longer
|
||||||
|
CONFIG_MIN_SCRATCH_TIME = 2
|
||||||
|
|
||||||
|
# State object indexes
|
||||||
|
_ID = 0
|
||||||
|
_ANIMATION_LIST = 1
|
||||||
|
_MOVEMENT_STEP = 2
|
||||||
|
|
||||||
|
# last time an animation occurred
|
||||||
|
LAST_ANIMATION_TIME = -1
|
||||||
|
|
||||||
|
# index of the sprite within the current animation that is currently showing
|
||||||
|
CURRENT_ANIMATION_INDEX = 0
|
||||||
|
|
||||||
|
# last time the cat changed states
|
||||||
|
# used to enforce minimum scratch time
|
||||||
|
LAST_STATE_CHANGE_TIME = -1
|
||||||
|
|
||||||
|
# State objects
|
||||||
|
# (ID, (Animation List), (Step Sizes))
|
||||||
|
STATE_SITTING = (0, (0,), (0, 0))
|
||||||
|
|
||||||
|
STATE_MOVING_LEFT = (1, (20, 21), (-CONFIG_STEP_SIZE, 0))
|
||||||
|
STATE_MOVING_UP = (2, (16, 17), (0, -CONFIG_STEP_SIZE))
|
||||||
|
STATE_MOVING_RIGHT = (3, (12, 13), (CONFIG_STEP_SIZE, 0))
|
||||||
|
STATE_MOVING_DOWN = (4, (8, 9), (0, CONFIG_STEP_SIZE))
|
||||||
|
STATE_MOVING_UP_RIGHT = (
|
||||||
|
5,
|
||||||
|
(14, 15),
|
||||||
|
(CONFIG_STEP_SIZE // 2, -CONFIG_STEP_SIZE // 2),
|
||||||
|
)
|
||||||
|
STATE_MOVING_UP_LEFT = (
|
||||||
|
6,
|
||||||
|
(18, 19),
|
||||||
|
(-CONFIG_STEP_SIZE // 2, -CONFIG_STEP_SIZE // 2),
|
||||||
|
)
|
||||||
|
STATE_MOVING_DOWN_LEFT = (
|
||||||
|
7,
|
||||||
|
(22, 23),
|
||||||
|
(-CONFIG_STEP_SIZE // 2, CONFIG_STEP_SIZE // 2),
|
||||||
|
)
|
||||||
|
STATE_MOVING_DOWN_RIGHT = (
|
||||||
|
8,
|
||||||
|
(10, 11),
|
||||||
|
(CONFIG_STEP_SIZE // 2, CONFIG_STEP_SIZE // 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
STATE_SCRATCHING_LEFT = (9, (30, 31), (0, 0))
|
||||||
|
STATE_SCRATCHING_RIGHT = (10, (26, 27), (0, 0))
|
||||||
|
STATE_SCRATCHING_DOWN = (11, (24, 25), (0, 0))
|
||||||
|
STATE_SCRATCHING_UP = (12, (28, 29), (0, 0))
|
||||||
|
|
||||||
|
STATE_CLEANING = (13, (0, 0, 1, 1, 2, 3, 2, 3, 1, 1, 2, 3, 2, 3, 0, 0, 0), (0, 0))
|
||||||
|
STATE_SLEEPING = (
|
||||||
|
14,
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
(0, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
# these states count as "moving"
|
||||||
|
MOVING_STATES = (
|
||||||
|
STATE_MOVING_UP,
|
||||||
|
STATE_MOVING_DOWN,
|
||||||
|
STATE_MOVING_LEFT,
|
||||||
|
STATE_MOVING_RIGHT,
|
||||||
|
STATE_MOVING_UP_LEFT,
|
||||||
|
STATE_MOVING_UP_RIGHT,
|
||||||
|
STATE_MOVING_DOWN_LEFT,
|
||||||
|
STATE_MOVING_DOWN_RIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# current state private field
|
||||||
|
_CURRENT_STATE = STATE_SITTING
|
||||||
|
|
||||||
|
# current animation list
|
||||||
|
CURRENT_ANIMATION = _CURRENT_STATE[_ANIMATION_LIST]
|
||||||
|
|
||||||
|
"""
|
||||||
|
Neko Animated Cat Sprite. Extends displayio.TileGrid manages changing the visible
|
||||||
|
sprite image to animate the cat in it's various states.
|
||||||
|
|
||||||
|
:param float animation_time: How long to wait in-between animation frames. Unit is seconds.
|
||||||
|
default is 0.3 seconds
|
||||||
|
:param tuple display_size: Tuple containing width and height of display.
|
||||||
|
Defaults to values from board.DISPLAY. Used to determine with we are at the edge
|
||||||
|
so we know to start scratching.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, animation_time=0.3, display_size=None):
|
||||||
|
if not display_size:
|
||||||
|
# if display_size was not passed, try to use defaults from board
|
||||||
|
if "DISPLAY" in dir(board):
|
||||||
|
self._display_size = (board.DISPLAY.width, board.DISPLAY.height)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Must pass display_size argument if not using built-in display."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# use the display_size that was passed in
|
||||||
|
self._display_size = display_size
|
||||||
|
|
||||||
|
# Load the sprite sheet bitmap and palette
|
||||||
|
sprite_sheet, palette = adafruit_imageload.load(
|
||||||
|
"/neko_cat_spritesheet.bmp",
|
||||||
|
bitmap=displayio.Bitmap,
|
||||||
|
palette=displayio.Palette,
|
||||||
|
)
|
||||||
|
|
||||||
|
# make the first color transparent
|
||||||
|
palette.make_transparent(0)
|
||||||
|
|
||||||
|
# Create a sprite tilegrid as self
|
||||||
|
super().__init__(
|
||||||
|
sprite_sheet,
|
||||||
|
pixel_shader=palette,
|
||||||
|
width=1,
|
||||||
|
height=1,
|
||||||
|
tile_width=32,
|
||||||
|
tile_height=32,
|
||||||
|
)
|
||||||
|
|
||||||
|
# set the animation time into a private field
|
||||||
|
self._animation_time = animation_time
|
||||||
|
|
||||||
|
def _advance_animation_index(self):
|
||||||
|
"""
|
||||||
|
Helper function to increment the animation index, and wrap it back around to
|
||||||
|
0 after it reaches the final animation in the list.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.CURRENT_ANIMATION_INDEX += 1
|
||||||
|
if self.CURRENT_ANIMATION_INDEX >= len(self.CURRENT_ANIMATION):
|
||||||
|
self.CURRENT_ANIMATION_INDEX = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def animation_time(self):
|
||||||
|
"""
|
||||||
|
How long to wait in-between animation frames. Unit is seconds.
|
||||||
|
|
||||||
|
:return: animation_time
|
||||||
|
"""
|
||||||
|
return self._animation_time
|
||||||
|
|
||||||
|
@animation_time.setter
|
||||||
|
def animation_time(self, new_time):
|
||||||
|
self._animation_time = new_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_state(self):
|
||||||
|
"""
|
||||||
|
The current state object.
|
||||||
|
(ID, (Animation List), (Step Sizes))
|
||||||
|
|
||||||
|
:return tuple: current state object
|
||||||
|
"""
|
||||||
|
return self._CURRENT_STATE
|
||||||
|
|
||||||
|
@current_state.setter
|
||||||
|
def current_state(self, new_state):
|
||||||
|
# update the current state object
|
||||||
|
self._CURRENT_STATE = new_state
|
||||||
|
# update the current animation list
|
||||||
|
self.CURRENT_ANIMATION = new_state[self._ANIMATION_LIST]
|
||||||
|
# reset current animation index to 0
|
||||||
|
self.CURRENT_ANIMATION_INDEX = 0
|
||||||
|
# show the first sprite in the animation
|
||||||
|
self[0] = self.CURRENT_ANIMATION[self.CURRENT_ANIMATION_INDEX]
|
||||||
|
# update the last state change time
|
||||||
|
self.LAST_STATE_CHANGE_TIME = time.monotonic()
|
||||||
|
|
||||||
|
def animate(self):
|
||||||
|
"""
|
||||||
|
If enough time has passed since the previous animation then
|
||||||
|
execute the next animation step by changing the currently visible sprite and
|
||||||
|
advancing the animation index.
|
||||||
|
|
||||||
|
:return bool: True if an animation frame occured. False if it's not time yet
|
||||||
|
for an animation frame.
|
||||||
|
"""
|
||||||
|
_now = time.monotonic()
|
||||||
|
# is it time to do an animation step?
|
||||||
|
if _now > self.LAST_ANIMATION_TIME + self.animation_time:
|
||||||
|
# update the visible sprite
|
||||||
|
self[0] = self.CURRENT_ANIMATION[self.CURRENT_ANIMATION_INDEX]
|
||||||
|
# advance the animation index
|
||||||
|
self._advance_animation_index()
|
||||||
|
# update the last animation time
|
||||||
|
self.LAST_ANIMATION_TIME = _now
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Not time for animation step yet
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_moving(self):
|
||||||
|
"""
|
||||||
|
Is the cat currently moving or not.
|
||||||
|
|
||||||
|
:return bool: True if cat is in a moving state. False otherwise.
|
||||||
|
"""
|
||||||
|
return self.current_state in self.MOVING_STATES
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
|
"""
|
||||||
|
Attempt to do animation step. Move if in a moving state. Change states if needed.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
_now = time.monotonic()
|
||||||
|
# attempt animation
|
||||||
|
did_animate = self.animate()
|
||||||
|
|
||||||
|
# if we did do an animation step
|
||||||
|
if did_animate:
|
||||||
|
# if cat is in a moving state
|
||||||
|
if self.is_moving:
|
||||||
|
# random chance to start sleeping or cleaning
|
||||||
|
_roll = random.randint(0, self.CONFIG_STOP_CHANCE_FACTOR - 1)
|
||||||
|
if _roll == 0:
|
||||||
|
# change to new state: sleeping or cleaning
|
||||||
|
_chosen_state = random.choice(
|
||||||
|
(self.STATE_CLEANING, self.STATE_SLEEPING)
|
||||||
|
)
|
||||||
|
self.current_state = _chosen_state
|
||||||
|
else: # cat is not moving
|
||||||
|
|
||||||
|
# if we are currently in a scratching state
|
||||||
|
if len(self.current_state[self._ANIMATION_LIST]) <= 2:
|
||||||
|
|
||||||
|
# check if we have scratched the minimum time
|
||||||
|
if (
|
||||||
|
_now
|
||||||
|
>= self.LAST_STATE_CHANGE_TIME + self.CONFIG_MIN_SCRATCH_TIME
|
||||||
|
):
|
||||||
|
# minimum scratch time has elapsed
|
||||||
|
|
||||||
|
# random chance to start moving
|
||||||
|
_roll = random.randint(0, self.CONFIG_START_CHANCE_FACTOR - 1)
|
||||||
|
if _roll == 0:
|
||||||
|
# start moving in a random direction
|
||||||
|
_chosen_state = random.choice(self.MOVING_STATES)
|
||||||
|
self.current_state = _chosen_state
|
||||||
|
|
||||||
|
else: # if we are sleeping or cleaning or another complex animation state
|
||||||
|
|
||||||
|
# if we have done every step of the animation
|
||||||
|
if self.CURRENT_ANIMATION_INDEX == 0:
|
||||||
|
# change to a random moving state
|
||||||
|
_chosen_state = random.choice(self.MOVING_STATES)
|
||||||
|
self.current_state = _chosen_state
|
||||||
|
|
||||||
|
# If we are far enough away from side walls to step in the current moving direction
|
||||||
|
if (
|
||||||
|
0
|
||||||
|
<= (self.x + self.current_state[self._MOVEMENT_STEP][0])
|
||||||
|
< (self._display_size[0] - self.tile_width)
|
||||||
|
):
|
||||||
|
|
||||||
|
# move the cat horizontally by current state step size x
|
||||||
|
self.x += self.current_state[self._MOVEMENT_STEP][0]
|
||||||
|
|
||||||
|
else: # we ran into a side wall
|
||||||
|
if self.x > self.CONFIG_STEP_SIZE:
|
||||||
|
# ran into right wall
|
||||||
|
self.x = self._display_size[0] - self.tile_width - 1
|
||||||
|
# change state to scratching right
|
||||||
|
self.current_state = self.STATE_SCRATCHING_RIGHT
|
||||||
|
else:
|
||||||
|
# ran into left wall
|
||||||
|
self.x = 1
|
||||||
|
# change state to scratching left
|
||||||
|
self.current_state = self.STATE_SCRATCHING_LEFT
|
||||||
|
|
||||||
|
# If we are far enough away from top and bottom walls
|
||||||
|
# to step in the current moving direction
|
||||||
|
if (
|
||||||
|
0
|
||||||
|
<= (self.y + self.current_state[self._MOVEMENT_STEP][1])
|
||||||
|
< (self._display_size[1] - self.tile_height)
|
||||||
|
):
|
||||||
|
|
||||||
|
# move the cat vertically by current state step size y
|
||||||
|
self.y += self.current_state[self._MOVEMENT_STEP][1]
|
||||||
|
|
||||||
|
else: # ran into top or bottom wall
|
||||||
|
if self.y > self.CONFIG_STEP_SIZE:
|
||||||
|
# ran into bottom wall
|
||||||
|
self.y = self._display_size[1] - self.tile_height - 1
|
||||||
|
# change state to scratching down
|
||||||
|
self.current_state = self.STATE_SCRATCHING_DOWN
|
||||||
|
else:
|
||||||
|
# ran into top wall
|
||||||
|
self.y = 1
|
||||||
|
# change state to scratching up
|
||||||
|
self.current_state = self.STATE_SCRATCHING_UP
|
||||||
|
|
||||||
|
|
||||||
|
# default to built-in display
|
||||||
|
display = board.DISPLAY
|
||||||
|
|
||||||
|
# create displayio Group
|
||||||
|
main_group = displayio.Group()
|
||||||
|
|
||||||
|
# create background group
|
||||||
|
background_group = displayio.Group(scale=16)
|
||||||
|
background_bitmap = displayio.Bitmap(20, 15, 1)
|
||||||
|
background_palette = displayio.Palette(1)
|
||||||
|
background_palette[0] = BACKGROUND_COLOR
|
||||||
|
background_tilegrid = displayio.TileGrid(
|
||||||
|
background_bitmap, pixel_shader=background_palette
|
||||||
|
)
|
||||||
|
background_group.append(background_tilegrid)
|
||||||
|
|
||||||
|
# add background to main_group
|
||||||
|
main_group.append(background_group)
|
||||||
|
|
||||||
|
# create Neko
|
||||||
|
neko = NekoAnimatedSprite(animation_time=ANIMATION_TIME)
|
||||||
|
|
||||||
|
# put Neko in center of display
|
||||||
|
neko.x = display.width // 2 - neko.tile_width // 2
|
||||||
|
neko.y = display.height // 2 - neko.tile_height // 2
|
||||||
|
|
||||||
|
# add neko to main_group
|
||||||
|
main_group.append(neko)
|
||||||
|
|
||||||
|
# show main_group on the display
|
||||||
|
display.show(main_group)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# update Neko to do animations and movements
|
||||||
|
neko.update()
|
||||||
BIN
CircuitPython_Neko_Cat/neko_cat_spritesheet.bmp
Normal file
BIN
CircuitPython_Neko_Cat/neko_cat_spritesheet.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
3
CircuitPython_Neko_Cat/neko_cat_spritesheet.bmp.license
Normal file
3
CircuitPython_Neko_Cat/neko_cat_spritesheet.bmp.license
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
SPDX-FileCopyrightText: GoodClover
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Public Domain
|
||||||
Loading…
Reference in a new issue