initial version
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
Copyright (c) 2023, Jeff Clement
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
19
README.md
|
@ -1,3 +1,20 @@
|
|||
# neko
|
||||
|
||||
Rough Javascript implementation of the infamous Neko cat
|
||||
Rough Javascript implementation of the infamous Neko Cat
|
||||
|
||||
## Usage
|
||||
|
||||
```html
|
||||
<script src="/neko/neko.js"></script>
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
Thanks to Evert Pot's post for some inspiration on the structure of
|
||||
Neko's state machine.
|
||||
https://evertpot.com/neko/
|
||||
|
||||
The images (which I believe are public domain at this point) were
|
||||
taken from here:
|
||||
https://webneko.net/?white
|
||||
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
// Description: Neko
|
||||
// Author: Jeff Clement
|
||||
// License: MIT
|
||||
|
||||
// Size of the neko, in pixels
|
||||
const nekoSize = 32;
|
||||
|
||||
// Speed of running animation
|
||||
const runSpeed = 0.2;
|
||||
|
||||
// The neko statemachine
|
||||
const stateMachine = {
|
||||
sleep: {
|
||||
images: ['sleep1', 'sleep2'],
|
||||
imageInterval: 1,
|
||||
click: 'awake',
|
||||
},
|
||||
yawn: {
|
||||
images: ['yawn'],
|
||||
nextState: ['sleep'],
|
||||
nextStateDelay: 1,
|
||||
},
|
||||
awake: {
|
||||
images: ['alert'],
|
||||
nextState: ['normal'],
|
||||
nextStateDelay: 1,
|
||||
},
|
||||
itch: {
|
||||
images: ['itch1','itch2'],
|
||||
imageInterval: 0.5,
|
||||
nextState: ['normal'],
|
||||
nextStateDelay: 2,
|
||||
click: 'dying', // OMG. Don't click an itchin' neko!
|
||||
},
|
||||
normal: {
|
||||
awake: true,
|
||||
images: ['still'],
|
||||
nextState: ['normal','normal','normal','itch', 'yawn'],
|
||||
nextStateDelay: 1,
|
||||
},
|
||||
|
||||
nrun: {
|
||||
awake: true,
|
||||
imageInterval: runSpeed,
|
||||
images: ['nrun1','nrun2'],
|
||||
},
|
||||
nerun: {
|
||||
awake: true,
|
||||
imageInterval: runSpeed,
|
||||
images: ['nerun1','nerun2'],
|
||||
},
|
||||
erun: {
|
||||
awake: true,
|
||||
imageInterval: runSpeed,
|
||||
images: ['erun1','erun2'],
|
||||
},
|
||||
serun: {
|
||||
awake: true,
|
||||
imageInterval: runSpeed,
|
||||
images: ['serun1','serun2'],
|
||||
},
|
||||
srun: {
|
||||
awake: true,
|
||||
imageInterval: runSpeed,
|
||||
images: ['srun1','srun2'],
|
||||
},
|
||||
swrun: {
|
||||
awake: true,
|
||||
imageInterval: runSpeed,
|
||||
images: ['swrun1','swrun2'],
|
||||
},
|
||||
wrun: {
|
||||
awake: true,
|
||||
imageInterval: runSpeed,
|
||||
images: ['wrun1','wrun2'],
|
||||
},
|
||||
nwrun: {
|
||||
awake: true,
|
||||
imageInterval: runSpeed,
|
||||
images: ['nwrun1','nwrun2'],
|
||||
},
|
||||
|
||||
|
||||
dying: {
|
||||
images: ['alert','dying1','dying2','dying3','dying4','dying5'],
|
||||
imageInterval: 0.5,
|
||||
nextState: ['dead'],
|
||||
nextStateDelay: 3,
|
||||
},
|
||||
dead: {
|
||||
images: ['dying6'],
|
||||
imageInterval: 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
// Preload all the images
|
||||
var images={};
|
||||
for(let state in stateMachine) {
|
||||
stateMachine[state].images.forEach(function(x) {
|
||||
let img = new Image(nekoSize, nekoSize);
|
||||
img.src = 'res/'+x+'.gif';
|
||||
images[x] = img;
|
||||
});
|
||||
}
|
||||
|
||||
class Neko {
|
||||
// Neko's current state
|
||||
state = null;
|
||||
|
||||
// Neko's current animation frame and timer to flip to the next one
|
||||
animationInterval = null;
|
||||
animationIndex = 0;
|
||||
|
||||
// Timer to switch to the next state
|
||||
nextStateDelay = null;
|
||||
|
||||
// where is Neko heading (current mouse position, or last press)
|
||||
targetX = -1;
|
||||
targetY = -1;
|
||||
|
||||
constructor(x = window.innerWidth - nekoSize,y = window.innerHeight - nekoSize) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
// build up the placeholder for the neko in the DOM
|
||||
this.image = new Image(nekoSize, nekoSize);
|
||||
this.image.style.position = 'fixed';
|
||||
this.image.onclick = this.handleClick.bind(this);
|
||||
document.body.appendChild(this.image);
|
||||
|
||||
// start the movement loop
|
||||
setInterval(this.update.bind(this), 100);
|
||||
|
||||
// hook into event handlers
|
||||
window.addEventListener('resize', this.handleResize.bind(this));
|
||||
window.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
||||
window.addEventListener('touchstart', this.handleTouch.bind(this));
|
||||
|
||||
// start the neko in the sleep state
|
||||
this.setState('sleep');
|
||||
|
||||
// force a resize
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
clearInterval(this.animationInterval);
|
||||
clearTimeout(this.nextStateDelay);
|
||||
|
||||
this.state = state;
|
||||
this.animationIndex = 0;
|
||||
|
||||
if (stateMachine[state].images.length > 1) {
|
||||
this.animationInterval = setInterval(this.nextFrame.bind(this), (stateMachine[state].imageInterval || 1) * 1000);
|
||||
}
|
||||
|
||||
if (stateMachine[state].nextState && stateMachine[state].nextState.length > 0) {
|
||||
this.nextStateDelay = setTimeout(() => {
|
||||
this.setState(stateMachine[state].nextState[Math.floor(Math.random() * stateMachine[state].nextState.length)]);
|
||||
}, (stateMachine[state].nextStateDelay || 1) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
nextFrame() {
|
||||
this.animationIndex = (this.animationIndex + 1) % stateMachine[this.state].images.length;
|
||||
}
|
||||
|
||||
handleTouch(event) {
|
||||
this.targetX = event.touches[0].clientX - nekoSize/2;
|
||||
this.targetY = event.touches[0].clientY - nekoSize/2;
|
||||
}
|
||||
|
||||
handleMouseMove(event) {
|
||||
this.targetX = event.clientX - nekoSize/2;
|
||||
this.targetY = event.clientY - nekoSize/2;
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
if (stateMachine[this.state].click) {
|
||||
this.setState(stateMachine[this.state].click);
|
||||
}
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
// adjust Neko's X and Y speed based on window size
|
||||
this.speedX = document.body.clientWidth / 100 * nekoSize/32.0;
|
||||
this.speedY = document.body.clientHeight / 100 * nekoSize/32.0;
|
||||
|
||||
// keep Neko on the screen
|
||||
if (this.x > document.body.clientWidth - nekoSize) {
|
||||
this.x = document.body.clientWidth - nekoSize;
|
||||
}
|
||||
if (this.y > document.body.clientHeight - nekoSize) {
|
||||
this.y = document.body.clientHeight - nekoSize;
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
const state = stateMachine[this.state];
|
||||
if (state.awake) {
|
||||
|
||||
const distanceX = this.targetX - this.x;
|
||||
const distanceY = this.targetY - this.y;
|
||||
|
||||
// If we're close enough to the target, stop moving
|
||||
const dx = Math.abs(distanceX) < this.speedX ? 0 : Math.sign(distanceX);
|
||||
const dy = Math.abs(distanceY) < this.speedY ? 0 : Math.sign(distanceY);
|
||||
|
||||
// determine our new state
|
||||
var newState = 'normal';
|
||||
if (dx == 1 && dy == 0) newState = 'erun';
|
||||
if (dx == 1 && dy == 1) newState = 'serun';
|
||||
if (dx == 0 && dy == 1) newState = 'srun';
|
||||
if (dx == -1 && dy == 1) newState = 'swrun';
|
||||
if (dx == -1 && dy == 0) newState = 'wrun';
|
||||
if (dx == -1 && dy == -1) newState = 'nwrun';
|
||||
if (dx == 0 && dy == -1) newState = 'nrun';
|
||||
if (dx == 1 && dy == -1) newState = 'nerun';
|
||||
if (newState != this.state) {
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
// move Neko, if required
|
||||
this.x += dx * this.speedX;
|
||||
this.y += dy * this.speedY;
|
||||
}
|
||||
|
||||
// Draw the neko
|
||||
this.image.src = images[state.images[this.animationIndex]].src;
|
||||
this.image.style.top = this.y + 'px';
|
||||
this.image.style.left = this.x + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
const neko = new Neko();
|
After Width: | Height: | Size: 206 B |
After Width: | Height: | Size: 235 B |
After Width: | Height: | Size: 174 B |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 187 B |
After Width: | Height: | Size: 195 B |
After Width: | Height: | Size: 205 B |
After Width: | Height: | Size: 209 B |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 176 B |
After Width: | Height: | Size: 179 B |
After Width: | Height: | Size: 172 B |
After Width: | Height: | Size: 175 B |
After Width: | Height: | Size: 175 B |
After Width: | Height: | Size: 173 B |
After Width: | Height: | Size: 183 B |
After Width: | Height: | Size: 166 B |
After Width: | Height: | Size: 186 B |
After Width: | Height: | Size: 187 B |
After Width: | Height: | Size: 182 B |
After Width: | Height: | Size: 178 B |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 183 B |
After Width: | Height: | Size: 184 B |
After Width: | Height: | Size: 156 B |
After Width: | Height: | Size: 159 B |
After Width: | Height: | Size: 171 B |
After Width: | Height: | Size: 190 B |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 183 B |
After Width: | Height: | Size: 170 B |
After Width: | Height: | Size: 178 B |
After Width: | Height: | Size: 181 B |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 174 B |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 174 B |
After Width: | Height: | Size: 193 B |