initial version

This commit is contained in:
Jeff Clement 2023-02-25 16:38:20 -07:00
parent 75038ff666
commit 7258453761
Signed by: jclement
GPG Key ID: 3BCB43A3F0E1D7DA
41 changed files with 254 additions and 2 deletions

View File

@ -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:

View File

@ -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

235
neko.js Normal file
View File

@ -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();

BIN
res/alert.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

BIN
res/dead.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

BIN
res/dying1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

BIN
res/dying2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

BIN
res/dying3.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

BIN
res/dying4.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

BIN
res/dying5.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

BIN
res/dying6.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

BIN
res/erun1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

BIN
res/erun2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

BIN
res/escratch1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

BIN
res/escratch2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

BIN
res/itch1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

BIN
res/itch2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

BIN
res/nerun1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

BIN
res/nerun2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

BIN
res/nrun1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

BIN
res/nrun2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

BIN
res/nscratch1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

BIN
res/nscratch2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

BIN
res/nwrun1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

BIN
res/nwrun2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

BIN
res/serun1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

BIN
res/serun2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

BIN
res/sleep1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

BIN
res/sleep2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

BIN
res/srun1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

BIN
res/srun2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

BIN
res/sscratch1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

BIN
res/sscratch2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

BIN
res/still.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

BIN
res/swrun1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

BIN
res/swrun2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

BIN
res/wrun1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

BIN
res/wrun2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

BIN
res/wscratch1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

BIN
res/wscratch2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

BIN
res/yawn.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B