diff --git a/LICENSE b/LICENSE index 2071b23..00d0965 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) +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: diff --git a/README.md b/README.md index c1d86f6..102d71b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,20 @@ # neko -Rough Javascript implementation of the infamous Neko cat \ No newline at end of file +Rough Javascript implementation of the infamous Neko Cat + +## Usage + +```html + +``` + +## 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 + diff --git a/neko.js b/neko.js new file mode 100644 index 0000000..eeeb40e --- /dev/null +++ b/neko.js @@ -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(); diff --git a/res/alert.gif b/res/alert.gif new file mode 100644 index 0000000..a135fc3 Binary files /dev/null and b/res/alert.gif differ diff --git a/res/dead.gif b/res/dead.gif new file mode 100644 index 0000000..343052d Binary files /dev/null and b/res/dead.gif differ diff --git a/res/dying1.gif b/res/dying1.gif new file mode 100644 index 0000000..e0691b4 Binary files /dev/null and b/res/dying1.gif differ diff --git a/res/dying2.gif b/res/dying2.gif new file mode 100644 index 0000000..169714b Binary files /dev/null and b/res/dying2.gif differ diff --git a/res/dying3.gif b/res/dying3.gif new file mode 100644 index 0000000..131c7eb Binary files /dev/null and b/res/dying3.gif differ diff --git a/res/dying4.gif b/res/dying4.gif new file mode 100644 index 0000000..1536b03 Binary files /dev/null and b/res/dying4.gif differ diff --git a/res/dying5.gif b/res/dying5.gif new file mode 100644 index 0000000..445111c Binary files /dev/null and b/res/dying5.gif differ diff --git a/res/dying6.gif b/res/dying6.gif new file mode 100644 index 0000000..b528408 Binary files /dev/null and b/res/dying6.gif differ diff --git a/res/erun1.gif b/res/erun1.gif new file mode 100644 index 0000000..195d1b2 Binary files /dev/null and b/res/erun1.gif differ diff --git a/res/erun2.gif b/res/erun2.gif new file mode 100644 index 0000000..bb871b9 Binary files /dev/null and b/res/erun2.gif differ diff --git a/res/escratch1.gif b/res/escratch1.gif new file mode 100644 index 0000000..ce892b2 Binary files /dev/null and b/res/escratch1.gif differ diff --git a/res/escratch2.gif b/res/escratch2.gif new file mode 100644 index 0000000..cf92cfd Binary files /dev/null and b/res/escratch2.gif differ diff --git a/res/itch1.gif b/res/itch1.gif new file mode 100644 index 0000000..071fc16 Binary files /dev/null and b/res/itch1.gif differ diff --git a/res/itch2.gif b/res/itch2.gif new file mode 100644 index 0000000..c92541f Binary files /dev/null and b/res/itch2.gif differ diff --git a/res/nerun1.gif b/res/nerun1.gif new file mode 100644 index 0000000..4f53c4b Binary files /dev/null and b/res/nerun1.gif differ diff --git a/res/nerun2.gif b/res/nerun2.gif new file mode 100644 index 0000000..1288849 Binary files /dev/null and b/res/nerun2.gif differ diff --git a/res/nrun1.gif b/res/nrun1.gif new file mode 100644 index 0000000..4d2751f Binary files /dev/null and b/res/nrun1.gif differ diff --git a/res/nrun2.gif b/res/nrun2.gif new file mode 100644 index 0000000..8ac0b30 Binary files /dev/null and b/res/nrun2.gif differ diff --git a/res/nscratch1.gif b/res/nscratch1.gif new file mode 100644 index 0000000..2ce13f1 Binary files /dev/null and b/res/nscratch1.gif differ diff --git a/res/nscratch2.gif b/res/nscratch2.gif new file mode 100644 index 0000000..9c00ed7 Binary files /dev/null and b/res/nscratch2.gif differ diff --git a/res/nwrun1.gif b/res/nwrun1.gif new file mode 100644 index 0000000..6ed2ec6 Binary files /dev/null and b/res/nwrun1.gif differ diff --git a/res/nwrun2.gif b/res/nwrun2.gif new file mode 100644 index 0000000..5627f99 Binary files /dev/null and b/res/nwrun2.gif differ diff --git a/res/serun1.gif b/res/serun1.gif new file mode 100644 index 0000000..5d3aee2 Binary files /dev/null and b/res/serun1.gif differ diff --git a/res/serun2.gif b/res/serun2.gif new file mode 100644 index 0000000..7a62e64 Binary files /dev/null and b/res/serun2.gif differ diff --git a/res/sleep1.gif b/res/sleep1.gif new file mode 100644 index 0000000..b431095 Binary files /dev/null and b/res/sleep1.gif differ diff --git a/res/sleep2.gif b/res/sleep2.gif new file mode 100644 index 0000000..d9f525a Binary files /dev/null and b/res/sleep2.gif differ diff --git a/res/srun1.gif b/res/srun1.gif new file mode 100644 index 0000000..6fa22ca Binary files /dev/null and b/res/srun1.gif differ diff --git a/res/srun2.gif b/res/srun2.gif new file mode 100644 index 0000000..46d71de Binary files /dev/null and b/res/srun2.gif differ diff --git a/res/sscratch1.gif b/res/sscratch1.gif new file mode 100644 index 0000000..485ecec Binary files /dev/null and b/res/sscratch1.gif differ diff --git a/res/sscratch2.gif b/res/sscratch2.gif new file mode 100644 index 0000000..2e7c08e Binary files /dev/null and b/res/sscratch2.gif differ diff --git a/res/still.gif b/res/still.gif new file mode 100644 index 0000000..e534a2a Binary files /dev/null and b/res/still.gif differ diff --git a/res/swrun1.gif b/res/swrun1.gif new file mode 100644 index 0000000..68f09a4 Binary files /dev/null and b/res/swrun1.gif differ diff --git a/res/swrun2.gif b/res/swrun2.gif new file mode 100644 index 0000000..3614ea8 Binary files /dev/null and b/res/swrun2.gif differ diff --git a/res/wrun1.gif b/res/wrun1.gif new file mode 100644 index 0000000..576c0f7 Binary files /dev/null and b/res/wrun1.gif differ diff --git a/res/wrun2.gif b/res/wrun2.gif new file mode 100644 index 0000000..b0fe2c2 Binary files /dev/null and b/res/wrun2.gif differ diff --git a/res/wscratch1.gif b/res/wscratch1.gif new file mode 100644 index 0000000..1ec736d Binary files /dev/null and b/res/wscratch1.gif differ diff --git a/res/wscratch2.gif b/res/wscratch2.gif new file mode 100644 index 0000000..7931068 Binary files /dev/null and b/res/wscratch2.gif differ diff --git a/res/yawn.gif b/res/yawn.gif new file mode 100644 index 0000000..4b3e564 Binary files /dev/null and b/res/yawn.gif differ