Skip to content

Instantly share code, notes, and snippets.

@netux
Last active January 24, 2026 18:50
Show Gist options
  • Select an option

  • Save netux/bb51d659aaca5cd823244f4dfc7095bc to your computer and use it in GitHub Desktop.

Select an option

Save netux/bb51d659aaca5cd823244f4dfc7095bc to your computer and use it in GitHub Desktop.
How to add Oneko to WebTiles
/**
* Port of https://sleepie.dev/oneko for https://webtiles.kicya.net/ !
* See https://gist.github.com/netux/bb51d659aaca5cd823244f4dfc7095bc for instructions
*/
function initTileOneko(options) {
var nekoEl = document.querySelector('.oneko');
var nekoStyleEl = nekoEl.querySelector('style');
var NEKO_BACKGROUND_POSITION_CLASS_PREFIX = 'oneko--bg-pos_';
var neko = {
posX: 32,
posY: 32,
speed: 10
};
var nekoZIndex = 2147483647;
if (options != null) {
if (options.nekoPosX != null) {
neko.posX = options.nekoPosX;
}
if (options.nekoPosY != null) {
neko.posY = options.nekoPosY;
}
if (options.nekoSpeed != null) {
neko.speed = options.nekoSpeed;
}
if (options.nekoZIndex != null) {
nekoZIndex = options.nekoZIndex;
}
}
var mousePosX = neko.posX;
var mousePosY = neko.posY;
var frameCount = 0;
var idleTime = 0;
var idleAnimation = null;
var idleAnimationFrame = 0;
var spriteSets = {
idle: [[-3, -3]],
alert: [[-7, -3]],
scratchSelf: [
[-5, 0],
[-6, 0],
[-7, 0],
],
scratchWallN: [
[0, 0],
[0, -1],
],
scratchWallS: [
[-7, -1],
[-6, -2],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
N: [
[-1, -2],
[-1, -3],
],
NE: [
[0, -2],
[0, -3],
],
E: [
[-3, 0],
[-3, -1],
],
SE: [
[-5, -1],
[-5, -2],
],
S: [
[-6, -3],
[-7, -2],
],
SW: [
[-5, -3],
[-6, -1],
],
W: [
[-4, -2],
[-4, -3],
],
NW: [
[-1, 0],
[-1, -1],
],
};
function arrayFindIndex(arr, cb) {
for (var i = 0; i < arr.length; i++) {
if (cb(arr[i], i, arr)) {
return i;
}
}
return -1;
}
function requestAnimationFrame(cb) {
setTimeout(function () {
cb(Date.now());
}, 1000 / 120);
}
function init() {
nekoEl.ariaHidden = true;
nekoEl.style.width = '32px';
nekoEl.style.height = '32px';
nekoEl.style.position = 'absolute';
nekoEl.style.imageRendering = 'pixelated';
nekoEl.style.left = (neko.posX - 16).toString() + 'px';
nekoEl.style.top = (neko.posY - 16).toString() + 'px';
nekoEl.style.zIndex = nekoZIndex;
nekoEl.style.display = 'initial';
for (var x = -8; x < 8; x++) {
for (var y = -4; y < 4; y++) {
var selector = '.' + NEKO_BACKGROUND_POSITION_CLASS_PREFIX + x.toString() + '_' + y.toString();
nekoStyleEl.textContent += '\n' + selector + ' { background-position: ' + (x * 32).toString() + 'px ' + (y * 32).toString() + 'px; }';
}
}
nekoEl.appendChild(nekoStyleEl);
nekoEl.parentElement.addEventListener('mousemove', function(event) {
mousePosX = event.offsetX;
mousePosY = event.offsetY;
});
setSprite('idle', 0);
requestAnimationFrame(onAnimationFrame);
}
var lastFrameTimestamp;
function onAnimationFrame(timestamp) {
if (!lastFrameTimestamp) {
lastFrameTimestamp = timestamp;
}
if (timestamp - lastFrameTimestamp > 100) {
lastFrameTimestamp = timestamp
frame()
}
if (options != null && options.onAnimationFrame != null) {
options.onAnimationFrame.apply(neko, arguments);
}
requestAnimationFrame(onAnimationFrame);
}
function setSprite(name, frame) {
var sprite = spriteSets[name][frame % spriteSets[name].length];
var nekoClasses = nekoEl.className.split(' ');
var backgroundPosClassIdx = arrayFindIndex(nekoClasses, function(className) {
return className.indexOf(NEKO_BACKGROUND_POSITION_CLASS_PREFIX) === 0;
});
if (backgroundPosClassIdx >= 0) {
nekoClasses.splice(backgroundPosClassIdx, 1);
}
nekoClasses.push(NEKO_BACKGROUND_POSITION_CLASS_PREFIX + sprite[0].toString() + '_' + sprite[1].toString());
nekoEl.className = nekoClasses.join(' ');
}
function resetIdleAnimation() {
idleAnimation = null;
idleAnimationFrame = 0;
}
function idle() {
idleTime += 1;
// every ~ 20 seconds
if (
idleTime > 10 &&
Math.floor(Math.random() * 200) == 0 &&
idleAnimation == null
) {
var avalibleIdleAnimations = ['sleeping', 'scratchSelf'];
if (neko.posX < 32) {
avalibleIdleAnimations.push('scratchWallW');
}
if (neko.posY < 32) {
avalibleIdleAnimations.push('scratchWallN');
}
if (neko.posX > nekoEl.parentElement.clientWidth - 32) {
avalibleIdleAnimations.push('scratchWallE');
}
if (neko.posY > nekoEl.parentElement.clientHeight - 32) {
avalibleIdleAnimations.push('scratchWallS');
}
idleAnimation =
avalibleIdleAnimations[
Math.floor(Math.random() * avalibleIdleAnimations.length)
];
}
switch (idleAnimation) {
case 'sleeping':
if (idleAnimationFrame < 8) {
setSprite('tired', 0);
break;
}
setSprite('sleeping', Math.floor(idleAnimationFrame / 4));
if (idleAnimationFrame > 192) {
resetIdleAnimation();
}
break;
case 'scratchWallN':
case 'scratchWallS':
case 'scratchWallE':
case 'scratchWallW':
case 'scratchSelf':
setSprite(idleAnimation, idleAnimationFrame);
if (idleAnimationFrame > 9) {
resetIdleAnimation();
}
break;
default:
setSprite('idle', 0);
return;
}
idleAnimationFrame += 1;
}
function frame() {
frameCount += 1;
var diffX = neko.posX - mousePosX;
var diffY = neko.posY - mousePosY;
var distance = Math.sqrt(Math.pow(diffX, 2) + Math.pow(diffY, 2));
if (distance < neko.speed || distance < 48) {
idle();
return;
}
idleAnimation = null;
idleAnimationFrame = 0;
if (idleTime > 1) {
setSprite('alert', 0);
idleTime = Math.min(idleTime, 7);
idleTime -= 1;
return;
}
var direction;
direction = diffY / distance > 0.5 ? 'N' : '';
direction += diffY / distance < -0.5 ? 'S' : '';
direction += diffX / distance > 0.5 ? 'W' : '';
direction += diffX / distance < -0.5 ? 'E' : '';
setSprite(direction, frameCount);
neko.posX -= (diffX / distance) * neko.speed;
neko.posY -= (diffY / distance) * neko.speed;
neko.posX = Math.min(Math.max(16, neko.posX), nekoEl.parentElement.clientWidth - 16);
neko.posY = Math.min(Math.max(16, neko.posY), nekoEl.parentElement.clientHeight - 16);
nekoEl.style.left = (neko.posX - 16).toString() + 'px';
nekoEl.style.top = (neko.posY - 16).toString() + 'px';
}
init();
return neko;
}
<div id="oneko-container">
<a class="oneko" href="https://sleepie.dev/oneko" target="_blank">
<style>
.oneko {
background-image: url('./oneko.gif');
cursor: default;
display: none;
}
</style>
<!-- #region Make oneko sleep when the tile is inactive -->
<div class="oneko__inactive oneko__inactive--frame-1"></div>
<div class="oneko__inactive oneko__inactive--frame-2"></div>
<div class="oneko__inactive oneko__inactive--sleeping-loop"></div>
<style>
.oneko {
overflow-y: hidden;
& .oneko__inactive {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
opacity: 1;
}
}
body:not(.active) .oneko {
background-image: none !important;
opacity: 0.5;
& .oneko__inactive {
background-image: url('./oneko.gif');
left: 0;
/* WebTiles pauses all scripts and animations in inactive tiles, so we have to improvise. */
&.oneko__inactive--frame-1 {
background-position: calc(32px * -3) calc(32px * -3); /* idle */
transition: opacity 0s linear 2s;
opacity: 0;
}
&.oneko__inactive--frame-2 {
background-position: calc(32px * -3) calc(32px * -2); /* tired */
transition: left 0s linear 2s, opacity 0s linear 3s;
opacity: 0;
}
&.oneko__inactive--sleeping-loop {
background-image: url('./oneko-sleeping.gif');
transition: left 0s linear 3s;
}
}
}
</style>
<!-- #endregion Make oneko sleep when the tile is inactive -->
<script src="/oneko.js"></script>
</a>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment