前端嘛 Logo
前端嘛
龙骨

龙骨

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>