龙骨
2026-07-31
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Dragon Kinematics</title>
</head>
<body>
<script>
var Input = {
keys: [],
mouse: {
left: false,
right: false,
middle: false,
x: 0,
y: 0,
},
};
for (var i = 0; i < 230; i++) {
Input.keys.push(false);
}
document.addEventListener("keydown", function (event) {
Input.keys[event.keyCode] = true;
});
document.addEventListener("keyup", function (event) {
Input.keys[event.keyCode] = false;
});
document.addEventListener("mousedown", function (event) {
if ((event.button = 0)) {
Input.mouse.left = true;
}
if ((event.button = 1)) {
Input.mouse.middle = true;
}
if ((event.button = 2)) {
Input.mouse.right = true;
}
});
document.addEventListener("mouseup", function (event) {
if ((event.button = 0)) {
Input.mouse.left = false;
}
if ((event.button = 1)) {
Input.mouse.middle = false;
}
if ((event.button = 2)) {
Input.mouse.right = false;
}
});
document.addEventListener("mousemove", function (event) {
Input.mouse.x = event.clientX;
Input.mouse.y = event.clientY;
});
/* Sets up canvas */
var canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.width = Math.max(window.innerWidth, window.innerWidth);
canvas.height = Math.max(window.innerWidth, window.innerWidth);
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
document.body.style.overflow = "hidden";
var ctx = canvas.getContext("2d");
/* Necessary classes */
var segmentCount = 0;
class Segment {
constructor(parent, size, angle, range, stiffness) {
segmentCount++;
this.isSegment = true;
this.parent = parent; /* Segment which this one is connected to */
if (typeof parent.children == "object") {
parent.children.push(this);
}
this.children = []; /* Segments connected to this segment */
this.size = size; /* Distance from parent */
this.relAngle = angle; /* Angle relative to parent */
this.defAngle = angle; /* Default angle relative to parent */
this.absAngle =
parent.absAngle + angle; /* Angle relative to x-axis */
this.range =
range; /* Difference between maximum and minimum angles */
this.stiffness =
stiffness; /* How closely it conforms to default angle */
this.updateRelative(false, true);
}
updateRelative(iter, flex) {
this.relAngle =
this.relAngle -
2 *
Math.PI *
Math.floor((this.relAngle - this.defAngle) / 2 / Math.PI + 1 / 2);
if (flex) {
/* this.relAngle=this.range/ */
/* (1+Math.exp(-4*(this.relAngle-this.defAngle)/ */
/* (this.stiffness*this.range))) */
/* -this.range/2+this.defAngle; */
this.relAngle = Math.min(
this.defAngle + this.range / 2,
Math.max(
this.defAngle - this.range / 2,
(this.relAngle - this.defAngle) / this.stiffness +
this.defAngle,
),
);
}
this.absAngle = this.parent.absAngle + this.relAngle;
this.x =
this.parent.x + Math.cos(this.absAngle) * this.size; /* Position */
this.y =
this.parent.y + Math.sin(this.absAngle) * this.size; /* Position */
if (iter) {
for (var i = 0; i < this.children.length; i++) {
this.children[i].updateRelative(iter, flex);
}
}
}
draw(iter) {
ctx.beginPath();
ctx.moveTo(this.parent.x, this.parent.y);
ctx.lineTo(this.x, this.y);
ctx.stroke();
if (iter) {
for (var i = 0; i < this.children.length; i++) {
this.children[i].draw(true);
}
}
}
follow(iter) {
var x = this.parent.x;
var y = this.parent.y;
var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5;
this.x = x + (this.size * (this.x - x)) / dist;
this.y = y + (this.size * (this.y - y)) / dist;
this.absAngle = Math.atan2(this.y - y, this.x - x);
this.relAngle = this.absAngle - this.parent.absAngle;
this.updateRelative(false, true);
/* this.draw(); */
if (iter) {
for (var i = 0; i < this.children.length; i++) {
this.children[i].follow(true);
}
}
}
}
class LimbSystem {
constructor(end, length, speed, creature) {
this.end = end;
this.length = Math.max(1, length);
this.creature = creature;
this.speed = speed;
creature.systems.push(this);
this.nodes = [];
var node = end;
for (var i = 0; i < length; i++) {
this.nodes.unshift(node);
/* node.stiffness=1; */
node = node.parent;
if (!node.isSegment) {
this.length = i + 1;
break;
}
}
this.hip = this.nodes[0].parent;
}
moveTo(x, y) {
this.nodes[0].updateRelative(true, true);
var dist = ((x - this.end.x) ** 2 + (y - this.end.y) ** 2) ** 0.5;
var len = Math.max(0, dist - this.speed);
for (var i = this.nodes.length - 1; i >= 0; i--) {
var node = this.nodes[i];
var ang = Math.atan2(node.y - y, node.x - x);
node.x = x + len * Math.cos(ang);
node.y = y + len * Math.sin(ang);
x = node.x;
y = node.y;
len = node.size;
}
for (var i = 0; i < this.nodes.length; i++) {
var node = this.nodes[i];
node.absAngle = Math.atan2(
node.y - node.parent.y,
node.x - node.parent.x,
);
node.relAngle = node.absAngle - node.parent.absAngle;
for (var ii = 0; ii < node.children.length; ii++) {
var childNode = node.children[ii];
if (!this.nodes.includes(childNode)) {
childNode.updateRelative(true, false);
}
}
}
/* this.nodes[0].updateRelative(true,false) */
}
update() {
this.moveTo(Input.mouse.x, Input.mouse.y);
}
}
class LegSystem extends LimbSystem {
constructor(end, length, speed, creature) {
super(end, length, speed, creature);
this.goalX = end.x;
this.goalY = end.y;
this.step = 0; /* 0 stand still, 1 move forward,2 move towards foothold */
this.forwardness = 0;
/* For foot goal placement */
this.reach =
0.9 *
((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) **
0.5;
var relAngle =
this.creature.absAngle -
Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x);
relAngle -= 2 * Math.PI * Math.floor(relAngle / 2 / Math.PI + 1 / 2);
this.swing = -relAngle + ((2 * (relAngle < 0) - 1) * Math.PI) / 2;
this.swingOffset = this.creature.absAngle - this.hip.absAngle;
/* this.swing*=(2*(relAngle>0)-1); */
}
update(x, y) {
this.moveTo(this.goalX, this.goalY);
/* this.nodes[0].follow(true,true) */
if (this.step == 0) {
var dist =
((this.end.x - this.goalX) ** 2 +
(this.end.y - this.goalY) ** 2) **
0.5;
if (dist > 1) {
this.step = 1;
/* this.goalX=x; */
/* this.goalY=y; */
this.goalX =
this.hip.x +
this.reach *
Math.cos(this.swing + this.hip.absAngle + this.swingOffset) +
((2 * Math.random() - 1) * this.reach) / 2;
this.goalY =
this.hip.y +
this.reach *
Math.sin(this.swing + this.hip.absAngle + this.swingOffset) +
((2 * Math.random() - 1) * this.reach) / 2;
}
} else if (this.step == 1) {
var theta =
Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x) -
this.hip.absAngle;
var dist =
((this.end.x - this.hip.x) ** 2 +
(this.end.y - this.hip.y) ** 2) **
0.5;
var forwardness2 = dist * Math.cos(theta);
var dF = this.forwardness - forwardness2;
this.forwardness = forwardness2;
if (dF * dF < 1) {
this.step = 0;
this.goalX = this.hip.x + (this.end.x - this.hip.x);
this.goalY = this.hip.y + (this.end.y - this.hip.y);
}
}
/* ctx.strokeStyle='blue'; */
/* ctx.beginPath(); */
/* ctx.moveTo(this.end.x,this.end.y); */
/* ctx.lineTo(this.hip.x+this.reach*Math.cos(this.swing+this.hip.absAngle+this.swingOffset), */
/* this.hip.y+this.reach*Math.sin(this.swing+this.hip.absAngle+this.swingOffset)); */
/* ctx.stroke(); */
/* ctx.strokeStyle='black'; */
}
}
class Creature {
constructor(
x,
y,
angle,
fAccel,
fFric,
fRes,
fThresh,
rAccel,
rFric,
rRes,
rThresh,
) {
this.x = x; /* Starting position */
this.y = y;
this.absAngle = angle; /* Staring angle */
this.fSpeed = 0; /* Forward speed */
this.fAccel = fAccel; /* Force when moving forward */
this.fFric = fFric; /* Friction against forward motion */
this.fRes = fRes; /* Resistance to motion */
this.fThresh =
fThresh; /* minimum distance to target to keep moving forward */
this.rSpeed = 0; /* Rotational speed */
this.rAccel = rAccel; /* Force when rotating */
this.rFric = rFric; /* Friction against rotation */
this.rRes = rRes; /* Resistance to rotation */
this.rThresh = rThresh; /* Maximum angle difference before rotation */
this.children = [];
this.systems = [];
}
follow(x, y) {
var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5;
var angle = Math.atan2(y - this.y, x - this.x);
/* Update forward */
var accel = this.fAccel;
if (this.systems.length > 0) {
var sum = 0;
for (var i = 0; i < this.systems.length; i++) {
sum += this.systems[i].step == 0;
}
accel *= sum / this.systems.length;
}
this.fSpeed += accel * (dist > this.fThresh);
this.fSpeed *= 1 - this.fRes;
this.speed = Math.max(0, this.fSpeed - this.fFric);
/* Update rotation */
var dif = this.absAngle - angle;
dif -= 2 * Math.PI * Math.floor(dif / (2 * Math.PI) + 1 / 2);
if (Math.abs(dif) > this.rThresh && dist > this.fThresh) {
this.rSpeed -= this.rAccel * (2 * (dif > 0) - 1);
}
this.rSpeed *= 1 - this.rRes;
if (Math.abs(this.rSpeed) > this.rFric) {
this.rSpeed -= this.rFric * (2 * (this.rSpeed > 0) - 1);
} else {
this.rSpeed = 0;
}
/* Update position */
this.absAngle += this.rSpeed;
this.absAngle -=
2 * Math.PI * Math.floor(this.absAngle / (2 * Math.PI) + 1 / 2);
this.x += this.speed * Math.cos(this.absAngle);
this.y += this.speed * Math.sin(this.absAngle);
this.absAngle += Math.PI;
for (var i = 0; i < this.children.length; i++) {
this.children[i].follow(true, true);
}
for (var i = 0; i < this.systems.length; i++) {
this.systems[i].update(x, y);
}
this.absAngle -= Math.PI;
this.draw(true);
}
draw(iter) {
if (this.hasDragonHead) {
this.drawDragonHead(this.headSize || 4);
} else {
var r = 4;
ctx.beginPath();
ctx.arc(
this.x,
this.y,
r,
Math.PI / 4 + this.absAngle,
(7 * Math.PI) / 4 + this.absAngle,
);
ctx.moveTo(
this.x + r * Math.cos((7 * Math.PI) / 4 + this.absAngle),
this.y + r * Math.sin((7 * Math.PI) / 4 + this.absAngle),
);
ctx.lineTo(
this.x + r * Math.cos(this.absAngle) * 2 ** 0.5,
this.y + r * Math.sin(this.absAngle) * 2 ** 0.5,
);
ctx.lineTo(
this.x + r * Math.cos(Math.PI / 4 + this.absAngle),
this.y + r * Math.sin(Math.PI / 4 + this.absAngle),
);
ctx.stroke();
}
if (iter) {
for (var i = 0; i < this.children.length; i++) {
this.children[i].draw(true);
}
}
}
drawDragonHead(r) {
/* Top-down dragon head. Local coords: f = forward, l = lateral, */
/* origin at the back of the skull so the neck attaches cleanly. */
var cos = Math.cos(this.absAngle);
var sin = Math.sin(this.absAngle);
var ox = this.x;
var oy = this.y;
function px(f, l) {
return ox + (f * cos - l * sin) * r;
}
function py(f, l) {
return oy + (f * sin + l * cos) * r;
}
function moveTo(f, l) {
ctx.moveTo(px(f, l), py(f, l));
}
function lineTo(f, l) {
ctx.lineTo(px(f, l), py(f, l));
}
function curveTo(cf, cl, f, l) {
ctx.quadraticCurveTo(px(cf, cl), py(cf, cl), px(f, l), py(f, l));
}
/* Skull, cheeks and snout */
ctx.beginPath();
for (var side = -1; side <= 1; side += 2) {
moveTo(-0.1, 0);
curveTo(-0.1, 0.38 * side, 0.3, 0.65 * side); /* Occiput */
curveTo(0.65, 1.0 * side, 1.05, 0.95 * side); /* Cheek */
curveTo(1.7, 0.95 * side, 2.05, 0.52 * side); /* Behind the snout */
curveTo(2.45, 0.44 * side, 2.75, 0.44 * side); /* Snout */
curveTo(3.25, 0.38 * side, 3.3, 0); /* Blunt tip */
}
ctx.stroke();
/* Horns, jaw spikes and fangs */
ctx.beginPath();
for (var side = -1; side <= 1; side += 2) {
moveTo(0.8, 0.88 * side); /* Swept-back horn */
curveTo(0.1, 1.35 * side, -0.9, 1.55 * side);
curveTo(-0.35, 0.95 * side, 0.35, 0.68 * side);
moveTo(1.5, 0.82 * side); /* Jaw spike */
curveTo(1.2, 1.25 * side, 0.85, 1.5 * side);
curveTo(1.05, 1.0 * side, 1.2, 0.93 * side);
moveTo(2.78, 0.43 * side); /* Fang */
lineTo(3.02, 0.68 * side);
}
ctx.stroke();
/* Mouth seam */
ctx.beginPath();
moveTo(3.25, 0);
lineTo(1.95, 0);
ctx.stroke();
/* Eyes and nostrils */
var fill = ctx.fillStyle;
ctx.fillStyle = ctx.strokeStyle;
for (var side = -1; side <= 1; side += 2) {
ctx.beginPath();
ctx.arc(
px(1.35, 0.6 * side),
py(1.35, 0.6 * side),
r * 0.2,
0,
6.283,
);
ctx.stroke();
ctx.beginPath();
ctx.arc(
px(1.35, 0.6 * side),
py(1.35, 0.6 * side),
r * 0.08,
0,
6.283,
);
ctx.fill();
ctx.beginPath();
ctx.arc(
px(2.95, 0.2 * side),
py(2.95, 0.2 * side),
r * 0.07,
0,
6.283,
);
ctx.fill();
}
ctx.fillStyle = fill;
}
}
/* Initializes and animates */
var dragon;
function setupSimple() {
/* (x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) */
var dragon = new Creature(
window.innerWidth / 2,
window.innerHeight / 2,
0,
12,
1,
0.5,
16,
0.5,
0.085,
0.5,
0.3,
);
var node = dragon;
/* (parent,size,angle,range,stiffness) */
for (var i = 0; i < 128; i++) {
var node = new Segment(node, 8, 0, 3.14159 / 2, 1);
}
setInterval(function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
dragon.follow(Input.mouse.x, Input.mouse.y);
}, 33);
}
function setupTentacle() {
/* (x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) */
dragon = new Creature(
window.innerWidth / 2,
window.innerHeight / 2,
0,
12,
1,
0.5,
16,
0.5,
0.085,
0.5,
0.3,
);
var node = dragon;
/* (parent,size,angle,range,stiffness) */
for (var i = 0; i < 32; i++) {
var node = new Segment(node, 8, 0, 2, 1);
}
/* (end,length,speed,creature) */
var tentacle = new LimbSystem(node, 32, 8, dragon);
setInterval(function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
dragon.follow(canvas.width / 2, canvas.height / 2);
ctx.beginPath();
ctx.arc(Input.mouse.x, Input.mouse.y, 2, 0, 6.283);
ctx.fill();
}, 33);
}
function setupArm() {
/* (x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) */
var dragon = new Creature(
window.innerWidth / 2,
window.innerHeight / 2,
0,
12,
1,
0.5,
16,
0.5,
0.085,
0.5,
0.3,
);
var node = dragon;
/* (parent,size,angle,range,stiffness) */
for (var i = 0; i < 3; i++) {
var node = new Segment(node, 80, 0, 3.1416, 1);
}
var tentacle = new LimbSystem(node, 3, dragon);
setInterval(function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
dragon.follow(canvas.width / 2, canvas.height / 2);
}, 33);
ctx.beginPath();
ctx.arc(Input.mouse.x, Input.mouse.y, 2, 0, 6.283);
ctx.fill();
}
function setupTestSquid(size, legs) {
/* (x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) */
dragon = new Creature(
window.innerWidth / 2,
window.innerHeight / 2,
0,
size * 10,
size * 3,
0.5,
16,
0.5,
0.085,
0.5,
0.3,
);
var legNum = legs;
var jointNum = 32;
for (var i = 0; i < legNum; i++) {
var node = dragon;
var ang = (Math.PI / 2) * (i / (legNum - 1) - 0.5);
for (var ii = 0; ii < jointNum; ii++) {
var node = new Segment(
node,
(size * 64) / jointNum,
ang * (ii == 0),
3.1416,
1.2,
);
}
/* (end,length,speed,creature,dist) */
var leg = new LegSystem(node, jointNum, size * 30, dragon);
}
setInterval(function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
dragon.follow(Input.mouse.x, Input.mouse.y);
}, 33);
}
function setupDragon(size, legs, tail) {
var s = size;
/* (x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) */
dragon = new Creature(
window.innerWidth / 2,
window.innerHeight / 2,
0,
s * 10,
s * 2,
0.5,
16,
0.5,
0.085,
0.5,
0.3,
);
dragon.hasDragonHead = true;
dragon.headSize = s * 3;
var spinal = dragon;
/* (parent,size,angle,range,stiffness) */
/* Neck */
for (var i = 0; i < 6; i++) {
spinal = new Segment(spinal, s * 4, 0, (3.1415 * 2) / 3, 1.1);
for (var ii = -1; ii <= 1; ii += 2) {
var node = new Segment(spinal, s * 3, ii, 0.1, 2);
for (var iii = 0; iii < 3; iii++) {
node = new Segment(node, s * 0.1, -ii * 0.1, 0.1, 2);
}
}
}
/* Torso and legs */
for (var i = 0; i < legs; i++) {
if (i > 0) {
/* Vertebrae and ribs */
for (var ii = 0; ii < 6; ii++) {
spinal = new Segment(spinal, s * 4, 0, 1.571, 1.5);
for (var iii = -1; iii <= 1; iii += 2) {
var node = new Segment(spinal, s * 3, iii * 1.571, 0.1, 1.5);
for (var iv = 0; iv < 3; iv++) {
node = new Segment(node, s * 3, -iii * 0.3, 0.1, 2);
}
}
}
}
/* Legs and shoulders (only the second and rear-most pairs) */
if (i == 1 || i == legs - 1) {
for (var ii = -1; ii <= 1; ii += 2) {
var node = new Segment(
spinal,
s * 12,
ii * 0.785,
0,
8,
); /* Hip */
node = new Segment(
node,
s * 16,
-ii * 0.785,
6.28,
1,
); /* Humerus */
node = new Segment(
node,
s * 16,
ii * 1.571,
3.1415,
2,
); /* Forearm */
for (var iii = 0; iii < 4; iii++ /* fingers */) {
new Segment(node, s * 4, (iii / 3 - 0.5) * 1.571, 0.1, 4);
}
new LegSystem(node, 3, s * 12, dragon, 4);
}
}
}
/* Tail */
for (var i = 0; i < tail; i++) {
spinal = new Segment(spinal, s * 4, 0, (3.1415 * 2) / 3, 1.1);
for (var ii = -1; ii <= 1; ii += 2) {
var node = new Segment(spinal, s * 3, ii, 0.1, 2);
for (var iii = 0; iii < 3; iii++) {
node = new Segment(
node,
(s * 3 * (tail - i)) / tail,
-ii * 0.1,
0.1,
2,
);
}
}
}
setInterval(function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
dragon.follow(Input.mouse.x, Input.mouse.y);
}, 33);
}
canvas.style.backgroundColor = "black";
ctx.strokeStyle = "white";
/* setupSimple(); */ /* Just the very basic string */
/* setupTentacle(); */ /* Tentacle that reaches for mouse */
/* setupDragon(.5,100,128); */ /* Huge serpentine dragon */
/* setupSquid(2,8); */ /* Spidery thing */
/* Fixed dragon: same body on every refresh */
/* (size,legs,tail) - legs is the number of torso slots; only the */
/* second and the last one actually grow a pair of legs */
setupDragon(4.5, 5, 30);
</script>
</body>
</html>
