<canvas id="canvas"></canvas>
<h1 class="ttl">let's drawing.</h1>
body {
  background: #e3e3e3;
  overflow: hidden;
}

.ttl {
  position: absolute;
  top: 50%;
  margin-top: -25px;
  width: 100%;
  color: #d3d3d3;
  font: 50px AvenirNext-Heavy;
  text-align: center;
  text-shadow: 0 0 2px rgba(0, 0, 0, .1);
  user-select: none;
  pointer-events: none;
}
'use strict';

// NameSpace
((win) => {
  win.App = win.App || {};
})(this);

// Util
((win) => {
  win.App = win.App || {};
  win.App.Util = win.App.Util || {};

  function buildGetAve(opt_maxLength) {
    const array = [];
    const maxLength = opt_maxLength || 10;
    let index = 0;

    function _ave() {
      const length = array.length;
      let sum = 0;

      for (let i = 0; i < length; ++i) {
        sum += array[i];
      }

      return sum / length;
    }

    function getAve(val) {
      array[index] = val;
      index = (index + 1) % maxLength;

      return _ave();
    }

    return getAve;
  }

  win.App.Util = {
    buildGetAve
  };
})(this);

// EventDispatcher
((win, doc, ns) => {
  function EventDispatcher() {
    this._events = {};
  }

  EventDispatcher.prototype.hasEventListener = function(eventName) {
    return !!this._events[eventName];
  };

  EventDispatcher.prototype.addEventListener = function(eventName, callback) {
    console.log(this);
    if (this.hasEventListener(eventName)) {
      const events = this._events[eventName];
      const length = events.length;

      for (let i = 0; i < length; i++) {
        if (events[i] === callback) {
          return;
        }
      }

      events.push(callback);
    } else {
      this._events[eventName] = [callback];
    }

    return this;
  };

  EventDispatcher.prototype.removeEventListener = function(eventName, callback) {
    if (!this.hasEventListener(eventName)) {
      return;
    } else {
      const events = this._events[eventName];
      let i = events.length;
      let index;

      while (i--) {
        if (events[i] === callback) {
          index = i;
        }
      }

      if (typeof index === 'number') {
        events.splice(index, 1);
      }
    }

    return this;
  };

  EventDispatcher.prototype.fireEvent = function(eventName, opt_this, opt_arg) {
    if (!this.hasEventListener(eventName)) {
      return;
    } else {
      const events = this._events[eventName];
      const copyEvents = [ ...events ];
      const arg = [ ...arguments ];
      const length = events.length;

      // eventNameとopt_thisを削除
      arg.splice(0, 2);

      for (let i = 0; i < length; i++) {
        copyEvents[i].apply(opt_this || this, arg);
      }
    }
  };

  ns.EventDispatcher = EventDispatcher;
})(this, document, App);

// Throttle
((win, doc, ns) => {
  function Throttle(opt_interval, opt_callback) {
    this._timer = null;
    this._lastEventTime = 0;
    this._interval = opt_interval || 500;
    this._callback = opt_callback || function() {};
  }

  Throttle.prototype.setInterval = function(ms) {
    this._interval = ms;
  };

  Throttle.prototype.addEvent = function(fn) {
    this._callback = fn;
  };

  Throttle.prototype.fireEvent = function(opt_arg) {
    const _this = this;
    const currentTime = Date.now();
    const timerInterval = this._interval / 10;

    clearTimeout(this.timer);

    if (currentTime - _this._lastEventTime > _this._interval) {
      _fire();
    } else {
      _this.timer = setTimeout(_fire, timerInterval);
    }

    function _fire() {
      _this._callback.call(_this, opt_arg || null);
      _this._lastEventTime = currentTime;
    }
  };

  ns.Throttle = Throttle;
})(this, document, App);

// Point
((win, doc, ns) => {
  function Point(x, y, opt_size) {
    const _this = this;

    _init();

    function _init() {
      ns.EventDispatcher.call(_this);
    }

    _this.x = x;
    _this.y = y;
    _this.size = opt_size || 1;
  }

  Point.prototype = new ns.EventDispatcher();
  Point.prototype.constructor = Point;

  Point.prototype.setSize = function(size) {
    this.size = size;
  };

  Point.prototype.draw = function(ctx, opt_size) {
    const size = opt_size || this.size || 1;

    ctx.save();
      ctx.beginPath();
      ctx.arc(this.x, this.y, size, 0, Math.PI * 2, false);
      ctx.fill();
      ctx.moveTo(this.x, this.y);
    ctx.restore();

    return this;
  };

  Point.getDistance = function(pointA, pointB) {
    return Math.sqrt(Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2));
  };

  ns.Point = Point;
})(this, document, App);

// Line
((win, doc, ns) => {
  let Point = ns.Point;

  function Line(opt_point) {
    const _this = this;
    const pointList = [];

    _init();

    function _init() {
      ns.EventDispatcher.call(_this);
      _this.pointList = pointList;

      if (opt_point) {
        _this.push(opt_point);
      }
    }
  }

  Line.prototype = new ns.EventDispatcher();
  Line.prototype.constructor = Line;

  Line.prototype.push = function(pointModel) {
    this.pointList.push(pointModel);
  };

  Line.prototype.drawLine = function(ctx) {
    const pointList = this.pointList;
    const length = pointList.length;

    ctx.save();
      ctx.lineCap  = 'round';
      ctx.lineJoin = 'round';

      if (length > 1) {
        for (let i = 1; i < length; ++i) {
          ctx.beginPath();
            ctx.moveTo(pointList[i - 1].x, pointList[i - 1].y);
            ctx.lineTo(pointList[i].x, pointList[i].y);
            ctx.lineWidth = pointList[i].size;
          ctx.stroke();
        }
      } else {
        pointList[0].draw(ctx);
      }

      ctx.restore();
  };

  Line.prototype.drawQuadraticCurve = function(ctx) {
    const pointList = this.pointList;
    const length = pointList.length;
    const quadraticPointList = [];
    let lastIndex = 0;

    ctx.save();
      ctx.lineCap  = 'round';
      ctx.lineJoin = 'round';

      if (length > 1) {
        quadraticPointList[lastIndex] = pointList[0];

        ctx.beginPath();
          ctx.moveTo(quadraticPointList[0].x, quadraticPointList[0].y);

          for (let i = 1; i < length; ++i) {
            quadraticPointList[++lastIndex] = new Point(
              (quadraticPointList[lastIndex - 1].x + pointList[i].x) / 2,
              (quadraticPointList[lastIndex - 1].y + pointList[i].y) / 2
            );
            quadraticPointList[++lastIndex] = (pointList[i]);
            ctx.quadraticCurveTo(
              quadraticPointList[i * 2 - 2].x,
              quadraticPointList[i * 2 - 2].y,
              quadraticPointList[i * 2 - 1].x,
              quadraticPointList[i * 2 - 1].y
            );
            ctx.lineWidth = pointList[i].size;
            ctx.stroke();
            ctx.beginPath();
            ctx.moveTo(quadraticPointList[i * 2 - 1].x, quadraticPointList[i * 2 - 1].y);
          }

          ctx.lineTo(quadraticPointList[lastIndex].x, quadraticPointList[lastIndex].y);
        ctx.stroke();
      } else {
        pointList[lastIndex].draw(ctx);
      }
    ctx.restore();
  };

  ns.Line = Line;
})(this, document, App);

// LineManager
((win, doc, ns) => {
  let instance;

  function getInstance() {
    if (!instance) {
      instance = new LineManager();
    }

    return instance;
  }

  function LineManager() {
    const _this = this;
    const lineList = [];

    _init();

    function _init() {
      ns.EventDispatcher.call(_this);
    }

    _this.lineList = lineList;
  }

  LineManager.prototype = new ns.EventDispatcher();
  LineManager.prototype.constructor = LineManager;

  LineManager.prototype.push = function(lineModel) {
    this.lineList.push(lineModel);
  };

  LineManager.prototype.addPoint = function(pointModel, opt_index) {
    const index = opt_index || this.lineList.length - 1;

    this.lineList[index].push(pointModel);
  };

  LineManager.prototype.drawQuadraticCurve = function(ctx) {
    const lineList = this.lineList;
    const length = lineList.length;

    if (length) {
      lineList[length - 1].drawQuadraticCurve(ctx);
    }
  };

  LineManager.prototype.clear = function() {
    let _this = this;

    _this.lineList = [];
  };

  ns.LineManager = {
    getInstance: getInstance
  };
})(this, document, App);

// main
((win, doc, ns) => {
  const SIZE = 10;
  const _getAve = win.App.Util.buildGetAve(10);

  function _getSize(l) {
    const MAX_DISTANCE = 100;
    let width;

    if (l > MAX_DISTANCE) {
      width = 1;
    }

    width = (MAX_DISTANCE - l) * SIZE / MAX_DISTANCE;

    return _getAve(width);
  }

  const START_EVENT = 'mousedown';
  const MOVE_EVENT = 'mousemove';
  const END_EVENT = 'mouseup';

  const lineManager = ns.LineManager.getInstance();
  const canvas = doc.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const sub = doc.createElement('canvas');
  const subCtx  = sub.getContext('2d');

  setup();

  async function setup() {
    addSketchEventForCanvas(canvas);

    canvas.width = sub.width = win.innerWidth;
    canvas.height = sub.height = win.innerHeight;

    tick();
  }

  function tick() {
    draw();
    requestAnimationFrame(tick);
  }

  function draw() {
    canvas.width = win.innerWidth;
    canvas.height = win.innerHeight;

    ctx.drawImage(sub, 0, 0);

    ctx.save();
      ctx.fillStyle = ctx.strokeStyle = 'rgba(50, 50, 50, 1)';
      lineManager.drawQuadraticCurve(ctx);
    ctx.restore();
  }

  function addSketchEventForCanvas(elm) {
    const throttle = new ns.Throttle(10, handleThrottleMoveEvent);
    let lastPoint = null;

    elm.addEventListener(START_EVENT, handleStartEvent, false);
    doc.addEventListener(END_EVENT, handleEndEvent, false);

    function handleThrottleMoveEvent(evt) {
      let currentPoint = new ns.Point(evt.offsetX, evt.offsetY);

      currentPoint.setSize(_getSize(ns.Point.getDistance(lastPoint, currentPoint)));
      lastPoint = currentPoint;
      lineManager.addPoint(currentPoint);
    }

    function handleStartEvent(evt) {
      lastPoint = new ns.Point(evt.offsetX, evt.offsetY, SIZE / 2);

      lineManager.push(new ns.Line(lastPoint));
      elm.addEventListener(MOVE_EVENT, handleMoveEvent, false);
    }

    function handleMoveEvent(evt) {
      throttle.fireEvent(evt);
    }

    function handleEndEvent(evt) {
      sub.width = win.innerWidth;
      sub.height = win.innerHeight;
      subCtx.drawImage(canvas, 0, 0);
      elm.removeEventListener(MOVE_EVENT, handleMoveEvent, false);
      lastPoint = null;
    }
  }
})(this, document, App);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.