Simone Seagle

Simone Seagle

Independent Web and Educational Software Developer

Read More



The Mansion of the Plates

Mouse-over or touch/drag to interact with The Mansion of the Plates Or, view fullscreen.

About the Art

Katsushika Hokusai is well known for his Great Wave, but he had a flair for the creepy as well. He began a project to create prints of 100 Japanese ghost stories, but only finished five of them. I found this print, the Mansion of the Plates, in a Public Domain Review collection of those. It's a shame he didn't complete the rest of the 100 ghosts.

This image depicts a ghost story by the same name - the woman you see coming out of the well is a maid named Okiku who lived in the house of a cruel samurai Aoyama Tessan who repeatedly tried to seduce her which she consistently refused. He finally resorted to trickery, hiding one of ten incredibly expensive plates. He told her to go bring them, and after trying and trying, was only able to find nine plates. She went to him in tears confessing that she could not find the final plate. The result of this mistake should have been death, so the samurai offered to let it slide if she would be his lover. Again she refused, and in his rage Aoyama threw Okiku down a well to her death.

She came back as a vengeful spirit - counting to the nine and shrieking horribly at the loss of the tenth plate.

About the Programming

The whole backbone (literally!) of this animation is one cubic Bezier curve. I use that curve to make it look like the maid's head is following the mouse (or touch pad, etc); There is a formula for getting a point along a Bezier curve that I found here, and once I was able to move the curve in a satisfying way, I was able to move all the plates and warp the hair just as I wanted. The original question was expressing frustration because different parts of Bezier curves have different rates of change, but that was irrelevant for my application.

function getBezierPoint(t) {
    let pt = {x: 0, y: 0};
    pt.x = ((1 - t) * (1 - t) * (1 - t)) * bezier.a2.x
        + 3 * ((1 - t) * (1 - t)) * t * bezier.c2.x
        + 3 * (1 - t) * (t * t) * bezier.c1.x
        + (t * t * t) * bezier.a1.x;

    pt.y = ((1 - t) * (1 - t) * (1 - t)) * bezier.a2.y
        + 3 * ((1 - t) * (1 - t)) * t * bezier.c2.y
        + 3 * (1 - t) * (t * t) * bezier.c1.y
        + (t * t * t) * bezier.a1.y;

    return pt;

I did a few different tricks to mostly keep the body within the well, follow the mouse, tween it slowly, etc. I'm not going to put the whole function here, but a couple of lines are illustrative of the general idea.

let easing = 0.04;
// Get the angle between the watch point and the head.  The head is at bezier.a1, or the top anchor point.
// I know sometimes anchor and control points are used differently, but I'm using the terms as they're used in Photoshop
let a = Math.atan2(watchPt.y - bezier.a1.y, watchPt.x - bezier.a1.x);

// Top target x point - flip the head if the point is close
// to the left of the image
let tx = watchPt.x - 400 * scale;
if (watchPt.x < 420 * scale)  tx = watchPt.x + 400 * scale;
bezier.a1.x += (tx - bezier.a1.x) * easing;

// Top target y point - Don't bob the head to low or too high
let ty = Math.max(176 * scale, Math.min(watchPt.y, 717 * scale));
bezier.a1.y += (ty -bezier.a1.y) * easing;

// The target of the handle point
let ht = {x: bezier.a1.x + 430 * scale * Math.cos(a + Math.PI),
        y: bezier.a1.y + 430 * scale * Math.sin(a + Math.PI)}
bezier.h1.x += (ht.x - bezier.h1.x) * easing;
bezier.h1.y += (ht.y - bezier.h1.y) * easing;

In honor of Okiku's story, I have made her body out of nine plates. The t above represents a fraction along the line, so I just had to take 1/9 to get the location of each plate along the backbone. The hair is a warped rope mesh, but I've used those many times and discussed them there. The only new thing here is that I used a clever trick to get the offset so all the strips of hair aren't on top of one another. At first I saw the formula above and felt instantly tired, thinking that I would need to take a derivative of that function in order to get the tangent. But you don't! Do it the lazy way.

// You're welcome.
function getNormalToBezier(t) {
    let p1 = getBezierPoint(t + 0.02);
    let p2 = getBezierPoint(t - 0.02);

    return Math.atan2(p2.y - p1.y, p2.x - p1.x) - Math.PI/2;

Happy Halloween!