与鼠标进行交互

TIP

Canvas是一种非保留性的绘图界面,即不会记录过去执行的绘图操作,而是保持最终结果(构成图像的彩色像素)。 如果想让Canvas变得具有交互性,比如用户可以选择、拖动画布上的图形。那么我们必须记录绘制的每一个对象,才能在将来灵活的修改并重绘它们,实现交互。

需求:

  1. 点击“添加圆圈”按钮可以在画布上增加位置、大小、颜色都是随机的圆圈。
  2. 点击“清空画布”按钮可以清除画布上所有圆圈。
  3. 鼠标点击任意圆圈,该圆圈会出现黑色边框,表示选中。
  4. 如果鼠标点击不在任何圆圈中,则以当前点击位置为圆心绘制随机圆圈。
  5. 鼠标选择圆圈可以进行拖动,拖动的时候显示其颜色的提示框。

代码如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <style>
    canvas {
      cursor: pointer;
      border: 1px solid black;
    }
  </style>
</head>
<body>
  <canvas id="canvas" width="400" height="300">
  </canvas>
  <div>
    <button onclick="addRandomCircle()">添加圆圈</button>
    <button onclick="clearCanvas()">清空画布</button>
  </div>
  <script>
    // 这个方法用来储存每个圆圈对象
    function Circle(x, y, radius, color) {
      this.x = x;
      this.y = y;
      this.radius = radius;
      this.color = color;
      // 存储当前圆是否被选中
      this.isSelected = false;
      // 存储当前圆是否可以拖动
      this.isDragging = false;
    }
    var canvas;
    var context;
    // 保存画布上所有的圆圈
    var circles = [];
    // 记录上一个被选中的圆
    var previousSelectedCircle;
    window.onload = function () {
      canvas = document.getElementById("canvas");
      context = canvas.getContext("2d");
      canvas.onmousedown = canvasClick;
      canvas.onmouseup = stopDragging;
      canvas.onmouseout = stopDragging;
      canvas.onmousemove = mouseMove;
    };
    // 添加一个随机圆,圆心为(x, y)
    function addRandomCircle(x, y) {
      // 为圆圈计算一个随机大小和位置
      var radius = randomFromTo(10, 60);
      var x = x || randomFromTo(0, canvas.width);
      var y = y || randomFromTo(0, canvas.height);
      // 为圆圈计算一个随机颜色
      var colors = ["green", "blue", "red", "yellow", "magenta", "orange", "brown", "purple", "pink"];
      var color = colors[randomFromTo(0, 8)];
      // 创建一个新圆圈
      var circle = new Circle(x, y, radius, color);
      // 把它保存在数组中
      circles.push(circle);
      // 重新绘制画布
      drawCircles();
    }
    // 清空画布
    function clearCanvas() {
      // 去除所有圆圈
      circles = [];
      // 重新绘制画布.
      drawCircles();
    }
    // 重绘画布
    function drawCircles() {
      // 清除画布,准备绘制
      context.clearRect(0, 0, canvas.width, canvas.height);
      // 遍历所有圆圈
      for (var i = 0; i < circles.length; i++) {
        var circle = circles[i];
        // 绘制圆圈
        context.globalAlpha = 0.85;
        context.beginPath();
        context.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
        context.fillStyle = circle.color;
        context.strokeStyle = "black";
        if (circle.isSelected) {
          context.lineWidth = 5;
        }
        else {
          context.lineWidth = 1;
        }
        context.fill();
        context.stroke();
      }
    }
    // 在某个范围内生成随机数
    function randomFromTo(from, to) {
      return Math.floor(Math.random() * (to - from + 1) + from);
    }
    // 点击选择/添加圆圈
    function canvasClick(e) {
      // 取得画布上被单击的点
      var clickX = e.pageX - canvas.offsetLeft;
      var clickY = e.pageY - canvas.offsetTop;
      // 查找被单击的圆圈,由于是逆序循环,所以总能先找到最上层的圆圈
      for (var i = circles.length - 1; i >= 0; i--) {
        var circle = circles[i];
        // 使用勾股定理计算这个点与圆心之间的距离
        var distanceFromCenter = Math.sqrt(Math.pow(circle.x - clickX, 2)
          + Math.pow(circle.y - clickY, 2))
        // 判断这个点是否在圆圈中
        if (distanceFromCenter <= circle.radius) {
          // 清除之前选择的圆圈
          if (previousSelectedCircle != null) {
            previousSelectedCircle.isSelected = false;
            previousSelectedCircle.isDragging = false;
          }
          // 更新上一个被选中的圆为当前圆
          previousSelectedCircle = circle;
          // 选择新圆圈
          circle.isSelected = true;
          // 使圆圈允许拖拽
          circle.isDragging = true;
          // 更新显示
          drawCircles();
          // 停止搜索
          return;
        }
      }
      // 如果当前位置没有圆,则添加一个随机圆
      addRandomCircle(clickX, clickY)
    }
    // 绘制tooltip提示文字
    function drawToolTip(txtLoc, x, y) {
      context.save();
      var padding = 3;
      var font = "16px arial";
      context.font = font;
      context.textBaseline = 'bottom';
      context.fillStyle = 'yellow';
      //绘制ToolTip背景
      var width = context.measureText(txtLoc).width;
      var height = parseInt(font, 10);
      context.fillRect(x, y-height, width+padding*2, height+padding*2);
      //绘制ToolTip文字
      context.fillStyle = '#000';
      context.fillText(txtLoc, x+padding, y+padding);
      context.restore();
    }
    // 停止拖动
    function stopDragging () {
      if (previousSelectedCircle) {
        previousSelectedCircle.isDragging = false;
      }
    }
    // 拖动圆圈
    function mouseMove (e) {
      // 判断拖拽对象是否存在、圆圈是否开始拖拽
      if (previousSelectedCircle && previousSelectedCircle.isDragging == true) {
        // 取得鼠标位置
        var x = e.pageX - canvas.offsetLeft;
        var y = e.pageY - canvas.offsetTop;
        // 将圆圈移动到鼠标位置
        previousSelectedCircle.x = x;
        previousSelectedCircle.y = y;
        // 更新画布
        drawCircles();
        // 如果当前鼠标位置有圆圈,还要显示tip
        drawToolTip("颜色:" + previousSelectedCircle.color, x, y);
      }
    }
  </script>
</body>
</html>

代码说明:

  1. 为了能够将圆圈对象保存起来,我们定义了一个叫 Circle() 的函数类创建自定义对象。同时要让这个对象能够保持数据,要使用关键字 this 来创建属性。

  2. drawCircles() 函数用来根据当前圆圈的集合来填充画布。每次程序刷新画布时,会先使用 clearRect() 方法清除画布上的所有内容。但不用当心这样会造成画布闪烁,即画布上的圆圈一下子全部消失,然后一下子又重新出现。因为Canvas针对这个问题进行了优化,会在所有绘图逻辑执行完毕后才清除或绘制所有内容,保证最终结果的流畅。

  3. 要实现鼠标选中某个图像,就要用到碰撞检测。即计算鼠标点击的那个点是否落在某个形状里。对于圆圈而言,只要计算单击点与圆心的直线距离即可。

MIT Licensed | Copyright © 2018-present 滇ICP备16006294号

Design by Quanzaiyu | Power by VuePress