Handcrafted Mascot On My Blog Like Its 2007 (feat. VanillaJS)
You know those cute little mascots that follow your cursor like needy pets? I wanted one. So I built it. No frameworks, no libraries, no npm, no bs.
Just pure VanillaJS and some emotional support from chatgpt (read as: hair scratching because I think I would’ve done it faster by taking support from stackoverflow instead).
The [cyb]org to my oxal <3 :
Step 1: Create a Sprite
I asked chatgpt to create a cute cyborg for me, then I put it in piskel editor and edited it heavily, changed the hands, legs, clothes, eyes, animated it. Basically broke it apart and fixed it.
const cyborgImgSprite = "/static/img/cyborg-sprite.png"
const SPRITE_CONFIG = {
frames: 9,
frameWidth: 64,
frameHeight: 64,
animationSpeed: 100/1000,
spriteSheet: cyborgImgSprite
};
Nine frames. Because he’s inspired by the way of the nine tales!
Step 2: Some state to remember where our buddy is
let mascotX = 0, mascotY = 0;
let targetX = window.innerWidth / 3;
let targetY = window.innerHeight / 3;
let currentMouseX = 0, currentMouseY = 0;
let currentFrame = 0;
let isFlipped = false;
We track position, target, mouse, animation frame, and whether he’s moonwalking. Globals are not evil if you use them like this.
Step 3: Follow the mouse and jump on click
function setupEventListeners(mascot) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('click', handleClick);
}
function handleMouseMove(e) {
currentMouseX = e.clientX;
currentMouseY = e.clientY;
}
function handleClick(e) {
targetX = e.clientX;
targetY = e.clientY;
}
Mouse moves? He looks. Click? He lunges.
Step 4: Sometimes he just needs to move
Go listen to dancin krono remix. N>o If left alone, he shuffles side to side every few seconds like he’s trying to get your attention. CSS left and top. You’ve done this in 2007, you can do it again. Frame changes = walking illusion. Your cyborg is alive. ALIVE. The game loop. Every sufficiently large program ends up reimplementing either ECS or a game loop. The DOM! Result? You’ve got an animated cyborg mascot that follows clicks, moves on its own, and flips direction like a moody anime character. All in ~100 lines of JavaScript. No react components were harmed in the making of this blog post. I promise.function moveEvery5Seconds(mascot, dt) {
lastMoveDt += dt;
if (lastMoveDt >= MOVE_INTERVAL) {
lastMoveDt = 0;
if (Math.abs(lastMoveX - mascotX) > 10 || Math.abs(lastMoveY - mascotY) > 10) {
const directionX = Math.random() > 0.5 ? 1 : -1;
targetX = mascotX + 70 * directionX;
isFlipped = directionX < 0;
mascot.style.transform = isFlipped ? 'translate(-50%, -50%) scaleX(-1)' : 'translate(-50%, -50%) scaleX(1)';
}
lastMoveX = mascotX;
lastMoveY = mascotY;
}
}
Step 5: Glide toward the target like a smooooooooth criminal
function updateMascotPosition(mascot, dt) {
mascotX += (targetX - mascotX) * EASING * dt;
mascotY += (targetY - mascotY) * EASING * dt;
if (mascotX > window.innerWidth || mascotX < 0) targetX = window.innerWidth / 2;
if (mascotY > window.innerHeight || mascotY < 0) targetY = window.innerHeight / 2;
}
function drawMascot(mascot, dt) {
mascot.style.left = mascotX + 'px';
mascot.style.top = mascotY + 'px';
}
Step 999: Animate the sprite so it’s not a cardboard cutout
function updateSpriteAnimation(mascot, dt) {
lastAnimationDt += dt;
if (lastAnimationDt >= SPRITE_CONFIG.animationSpeed) {
lastAnimationDt = 0;
currentFrame = (currentFrame + 1) % SPRITE_CONFIG.frames;
const bgPositionX = currentFrame * SPRITE_CONFIG.frameWidth;
mascot.style.backgroundPosition = `-${bgPositionX}px 0`;
}
}
Step 9000: Tick tock tick tock
function ticker(mascot) {
const currentTime = Date.now();
const dt = (currentTime - prevFrameTime) / 1000;
prevFrameTime = currentTime;
updateMascotPosition(mascot, dt);
moveEvery5Seconds(mascot, dt);
updateSpriteAnimation(mascot, dt);
drawMascot(mascot, dt);
requestAnimationFrame(() => ticker(mascot));
}
Step -1: And finally
function createMascotElement(spriteConfig) {
const mascot = document.createElement('div');
mascot.id = 'cursor-mascot';
const style = document.createElement('style');
style.textContent = `
#cursor-mascot {
position: fixed;
width: ${spriteConfig.frameWidth}px;
height: ${spriteConfig.frameHeight}px;
background-image: url('${spriteConfig.spriteSheet}');
background-repeat: no-repeat;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: transform 0.1s ease;
}
`;
document.head.appendChild(style);
return mascot;
}
Step 0: Init!
function initMascot() {
const mascot = createMascotElement(SPRITE_CONFIG);
document.body.appendChild(mascot);
setupEventListeners(mascot);
ticker(mascot);
}
document.addEventListener('DOMContentLoaded', initMascot);