draw(x, y, previousX, previousY) {
    // If the user clicks (or double clicks) without moving the mouse,
    // previousX/Y will be 0. In this case, we don't want to draw a line from (0,0) to (x,y),
    // but a "point" from (x,y) to (x,y).
    const prevX = previousX || x;
    const prevY = previousY || y;
    // get distance from the previous point
    // and use it to calculate the smoothed coordinates
    const smoothingFactor = this.getSmoothingFactor(lineDistance(x, y, prevX, prevY));
    const procX = x - (x - prevX) * smoothingFactor;
    const procY = y - (y - prevY) * smoothingFactor;

    // recalculate distance from previous point, this time relative to the smoothed coords
    const dist = lineDistance(procX, procY, prevX, prevY);

    // Adaptive stroke allows an effect where thickness changes
    // over the course of the stroke. This simulates the variation in
    // ink discharge of a physical pen.
    if (this.adaptiveStroke) {
      // Thickness range is inversely proportional to pressure,
      // because with higher pressure, the effect of distance
      // on the thickness ratio should be greater.
      const range = LINE_THICKNESS_RANGE * (1 - this.#pressure);
      const ratio = (dist - MIN_LINE_THICKNESS) / range;
      const targetThickness = ratio * (this.#maxWeight - this.#weight) + this.#weight;
      // approach the target gradually
      if (this.#thickness > targetThickness) {
        this.#thickness -= THICKNESS_INCREMENT;
      } else if (this.#thickness < targetThickness) {
        this.#thickness += THICKNESS_INCREMENT;
      }
    } else {
      this.#thickness = this.#weight;
    }

    this.#context.lineWidth = this.#thickness;

    // Draw the segment using quad interpolation.
    this.#context.beginPath();
    this.#context.moveTo(prevX, prevY);
    this.#context.quadraticCurveTo(prevX, prevY, procX, procY);
    this.#context.closePath();
    this.#context.stroke();

    if (this.recordStrokes) {
      this.#strokeMemory.push({
        point: new Point(x, y),
        time: performance.now() - this.strokeTimestamp,
      });

      this.dispatchEvent('segmentdrawn', { stroke: this.currentStroke });
    }

    // At this point, we can be certain the canvas has some drawing on it,
    // so we can toggle the "dirty" state. Checking it here ensures that
    // the state is also updated during programmatic drawing.
    if (!this.#dirty && this.#mode === MODE_DRAW) {
      this.#dirty = true;
      this.dispatchEvent('dirty');
    }

    return { x: procX, y: procY };
  }
Draws the next stroke segment as a smooth quadratic curve
with adaptive stroke thickness between two points.

@param {number} x current X coordinate
@param {number} y current Y coordinate
@param {number} previousX previous X coordinate
@param {number} previousY previous Y coordinate