Blog
How I made my page doll alive
Because Sophia is really cute like that
I don't really have a name for it, but I heard people call them page dolls, which I find sounds much better than "Live2D", even if it's similar to that, so that's the name I'll use here!
Since making my site I had my cute OC Sophia in the left corner (well technically I had Mizuki there kinda as a placeholder there). And also since the start I have these little animations when you hover things, and I also had Sophia jump on it and have her a different face. And since she's so cute, she also deserves headpats, so I added that as well! (Which isn't even that hard to do, and I also made a Codepen for it here)
And so one time when I headpatted her, I thought "Wouldn't it be nice if she was animated alive?" Like having her follow the cursor and such, kinda like Live2D.
So I looked at Live2D. That's the technology used in games (like many gacha games or a few visual novels) to have the anime character animated and also have them react, like when the player moves their finger around them. And you can also use Live2D on the web! And it would also be free for non-commercial use. But the web SDK would've been more than 2 MB of JavaScript (+ the model), which I didn't really want to add to my site, and I'd also need to learn how to use Live2D Cubism.
So instead I took the harder route and made my own animated doll from scratch! I'll explain a bit about what I did here, but not explain the full code. But you can check out the code here if you want to!
The Model
Like how you'd do it with Live2D, you also need a model first. For this step you just draw and put every movable thing you want to a separate layer. (I also used this chance Sophia's design a bit~)

I didn't make it as detailed as you'd typically do it with Live2D, since I didn't need that many moving parts, but also to make my life easier when I later programmed it.
And then I just exported each layer as a separate image!
The Code
To make my life a bit easier, I created a class for the body parts, so that I could also configure each part easier. My idea was to write down each coordinate for each part to where they'd get attached to, similar to how you do movable paper dolls, where you make holes at specific locations and put something in it so that it can move.
class BodyPart {
constructor(filename, left, top, anchor_left, anchor_top, z_index, children = {}) {
this.filename = filename;
this.left = left;
this.anchor_left = anchor_left;
this.anchor_top = anchor_top;
this.children = children;
this.el = document.createElement("div");
this.el.style.left = left - anchor_left + "px";
this.el.style.top = top - anchor_top + "px";
this.el.style.transformOrigin = `${anchor_left}px ${anchor_top}px`;
this.el.style.transform = "rotate(0deg)";
this.img = document.createElement("img");
this.img.src =
filename == "" ? "" : `/assets/images/sophia-model/${filename}`;
this.el.style.zIndex = z_index + defaultZIndex;
this.img.style.zIndex = z_index + defaultZIndex;
this.img.draggable = false;
Object.entries(children).forEach(([key, part]) => {
part.el.id = key;
this.el.appendChild(part.el);
this[key] = part;
parts[key] = part;
});
this.el.appendChild(this.img);
if (this.filename == "") return;
toLoad++;
this.img.addEventListener("load", () => {
toLoad--;
if (toLoad == 0)
document.getElementById("characterBox").style.opacity = 1;
});
}
// ...
}
After setting their positions and setting some other stuff, I copy the children directly to the object, so that it's easier to access them (so instead of writing body.children.tail you can directly write body.tail), and the last part is to ensure that the doll only gets shown once all images have loaded.
With this class set I could just assign each part easily like this:
const parts = {};
parts.character = new BodyPart("", 0, 0, 0, 0, 0, {
body: new BodyPart("Body2.webp", 620, 550, 219, 0, 0, {
tail: new BodyPart("Tail.webp", 260, 450, 15, 150),
hairBack: new BodyPart("HairBack.webp", 228, -342, 322, 0, -10, {
ears: new Ears("", 0, 0, 0, 0, -20, {
lEar: new BodyPart("LEar.webp", 290 + 150, 0, 105, 150),
rEar: new BodyPart("REar.webp", 47 + 150, 0, 105, 150),
}),
}),
// ...
});
Oh right, I also made specific classes for the ears and hair (which are just inherited from the BodyPart class), but more on that later.
The Moving Magic
What's the secret ingredient used to make everything movable? CSS Transforms! They're very powerful to use, but using multiple at once on different levels is kinda finicky, and I also had many issues with it. But in the end I somehow managed to make it work.
I applied the transforms based on where the cursor is. For that, I also used a bit of (scary) math: Vectors! I know some people are scared of vectors, but they're not that hard, and quite useful for this! I won't explain the math here, but basically what I did here was to calculate a vector from the middle of the eyes to the cursor, and also cap that (with normalizing the vector when it was too long).
// function move(event)
const rectSophia = parts.eyeballs.el.getBoundingClientRect();
const sophiaX = rectSophia.left + rectSophia.width / 2;
const sophiaY = rectSophia.top + rectSophia.height / 2;
// ...
let vector = new Vector(
(event.clientX - sophiaX) / vectorRange,
(event.clientY - sophiaY) / vectorRange,
);
if (vector.Length() > 1) {
vector = vector.Normalize();
}
So now I had a vector that had a length between 0 and 1 (0 ≤ x ≤ 1), and just apply that with a multiplier to the different parts! (The multipliers were just picked based on feeling)
// function move(event)
let multiplier = 1.75;
const yDeg = vector.x * 6 * multiplier;
const xDeg = vector.y * -6 * multiplier;
const rotate = vector.x * 2 * multiplier;
const faceMoveX = vector.x * 2 * multiplier;
const faceMoveY = vector.y * 5 * multiplier;
// setTransform is just a small helper function I wrote
parts.head.setTransform({
translateX: faceMoveX + "px",
translateY: faceMoveY + "px",
rotateX: xDeg + "deg",
rotateY: yDeg + "deg",
rotate: rotate + "deg",
});
parts.hair.setTransform({
translateX: faceMoveX * (2 / 3) + "px",
translateY: Math.max(-2, faceMoveY * (2 / 3)) + "px",
rotate: rotate * (1 / 3) + "deg",
});
// ...
For the animations, like breathing or moving the arms lightly up and down, I just use CSS animations defined in JS:
let animations = [];
function startAnimations() {
animations.push(
parts.lArm.el.animate(
[{
transform: "rotate(0deg)"
}, {
transform: "rotate(-4deg)"
}], {
duration: 6000,
iterations: Infinity,
direction: "alternate",
easing: "ease-in-out",
},
),
parts.rArm.el.animate(
[{
transform: "rotate(0deg)"
}, {
transform: "rotate(4deg)"
}], {
duration: 6000,
iterations: Infinity,
direction: "alternate",
easing: "ease-in-out",
},
),
// ...
)
}
I could've also done the animations in CSS directly, but this way I can easily stop every animation for things like putting her to sleep:
function stopAnimations() {
animations.forEach((a) => {
a.cancel();
});
animations = [];
}
The eyes, ears, and front hair are a bit special though, which is why they're also in special classes (which are inherited from the BodyPart class) to make it easier to trigger things like headpats. (If you don't know what inheritance is, in a nutshell all inherited classes get the same stuff the parent class has, and can add other stuff as well. In this case I can treat the ears like a normal body part, but also start and stop the animation there.)
Eyes and Ears
Both eyes and ears run on a timer. For the eyes this means every 4 second they blink, with a chance of them blinking twice (because it looks cute). And the ears twitch every 11-21 seconds, and the ear gets randomly picked.
Blinking was done with having the eyes, eyelids and pupils separated in seperate images, and when blinking I just move the eyelid down while cropping both the eye holes and pupil at the same time, and also flipping the eyelid upside down so that it looks better (which is more important for the sleep mode). All of that is done with animations defined in JS.
blinkAnimation(iterations = null, direction = "alternate") {
const blinkTimes = iterations ?? (Math.random() < 0.25 ? 2 : 1) * 2;
this.children.eyeholes.animation = this.children.eyeholes.el.animate(
[{
clipPath: `polygon(0% 10%, 20% 0%, 80% 0%, 100% 10%, 100% 100%, 0% 100%)`,
transform: "translateY(0px)",
},
{
clipPath: `polygon(0% 130%, 20% 120%, 80% 130%, 100% 120%, 100% 100%, 0% 100%)`,
transform: "translateY(-32px)",
},
], {
duration: 100,
iterations: blinkTimes,
direction: direction,
},
);
this.children.eyelids.animation = this.children.eyelids.el.animate(
[{
transform: "translateY(0px)",
},
{
transform: "translateY(80px)",
},
], {
duration: 100,
iterations: blinkTimes,
direction: direction,
},
);
}
Front Hair
I wanted to have the front hair a bit of movement and physics when you move your cursor. But I don't know that much about physics (only a little when I once made a smol game). So I tried a few things out, and ended up with this: I'm using the (appropriately named) variable movementX from the mousemove event, and use that to add velocity to the ears. And then I use CSS transitions and rotate the hair to a specific angle, and once it reaches that position, I apply the velocity again. I was aiming for a pendulum-like animation, and I've tried to tweak it a bit, but this is what I ended up with.
class FrontHair extends BodyPart {
constructor(...) {
super(...);
this.velocity = 0;
this.children.hairLBang.el.addEventListener("transitionend", () =>
this.move(),
);
}
addVelocity(velocity) {
if (this.velocity == 0) {
this.velocity += velocity;
this.move();
} else {
this.velocity += velocity;
}
}
// ...
move() {
this.velocity *= -0.3;
if (Math.abs(this.velocity) < 0.1) this.velocity = 0;
this.setBangsTransform({
rotate: this.velocity + "deg",
});
}
}
Other Modes
I already had headpats implemented before, so I just basically used that script and adapted it to here. Having separated the Eyes and Ears in seperate classes with methods to control them also made it easier to implement. The same is true for the sleep mode, which I've added if for some reason you find a cute purple creature cutely looking at what you do a bit distracting, which just stops all animations and gives her sweet dreams~ (at least I hope so)
Actually Putting this on My Site
That was surprisingly hard to do. Since I've used transforms for everything in the model, and since the model is bigger than her current position, I had to get creative with placing her. So I had to resort to also use a transform for it, and just play with the values until it fitted perfectly. But I don't want to touch that part any time soon...
Conclusion
It may not be the prettiest code, and most of this has just been me trying things out and see what sticks. But I'm still very happy to have her be real on my website. Yes it would've been much easier to just a library like Live2D (to my knowledge that's the only library available for this kind of thing), but I think it was still better to just make this myself. At least I learned new things, and I think it also looks more elegant. And oh my gosh, she's way too cute this way.
If you want to look at the code, like if you want to put something similar on your page, you can check it out here!