Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  shapes.js   Sprache: JAVA

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


"use strict";

const {
  CanvasFrameAnonymousContentHelper,
  getComputedStyle,
} = require("resource://devtools/server/actors/highlighters/utils/markup.js");
const {
  setIgnoreLayoutChanges,
  getCurrentZoom,
  getAdjustedQuads,
  getFrameOffsets,
} = require("resource://devtools/shared/layout/utils.js");
const {
  AutoRefreshHighlighter,
} = require("resource://devtools/server/actors/highlighters/auto-refresh.js");
const {
  getDistance,
  clickedOnEllipseEdge,
  distanceToLine,
  projection,
  clickedOnPoint,
} = require("resource://devtools/server/actors/utils/shapes-utils.js");
const {
  identity,
  apply,
  translate,
  multiply,
  scale,
  rotate,
  changeMatrixBase,
  getBasis,
} = require("resource://devtools/shared/layout/dom-matrix-2d.js");
const EventEmitter = require("resource://devtools/shared/event-emitter.js");
const {
  getMatchingCSSRules,
} = require("resource://devtools/shared/inspector/css-logic.js");

const BASE_MARKER_SIZE = 5;
// the width of the area around highlighter lines that can be clicked, in px
const LINE_CLICK_WIDTH = 5;
const ROTATE_LINE_LENGTH = 50;
const DOM_EVENTS = ["mousedown""mousemove""mouseup""dblclick"];
const _dragging = Symbol("shapes/dragging");

/**
 * The ShapesHighlighter draws an outline shapes in the page.
 * The idea is to have something that is able to wrap complex shapes for css properties
 * such as shape-outside/inside, clip-path but also SVG elements.
 *
 * Notes on shape transformation:
 *
 * When using transform mode to translate, scale, and rotate shapes, a transformation
 * matrix keeps track of the transformations done to the original shape. When the
 * highlighter is toggled on/off or between transform mode and point editing mode,
 * the transformations applied to the shape become permanent.
 *
 * While transformations are being performed on a shape, there is an "original" and
 * a "transformed" coordinate system. This is used when scaling or rotating a rotated
 * shape.
 *
 * The "original" coordinate system is the one where (0,0) is at the top left corner
 * of the page, the x axis is horizontal, and the y axis is vertical.
 *
 * The "transformed" coordinate system is the one where (0,0) is at the top left
 * corner of the current shape. The x axis follows the north edge of the shape
 * (from the northwest corner to the northeast corner) and the y axis follows
 * the west edge of the shape (from the northwest corner to the southwest corner).
 *
 * Because of rotation, the "north" and "west" edges might not actually be at the
 * top and left of the transformed shape. Imagine that the compass directions are
 * also rotated along with the shape.
 *
 * A refresher for coordinates and change of basis that may be helpful:
 * https://www.math.ubc.ca/~behrend/math221/Coords.pdf
 *
 * @param {String} options.hoverPoint
 *        The point to highlight.
 * @param {Boolean} options.transformMode
 *        Whether to show the highlighter in transforms mode.
 * @param {} options.mode
 */

class ShapesHighlighter extends AutoRefreshHighlighter {
  constructor(highlighterEnv) {
    super(highlighterEnv);
    EventEmitter.decorate(this);

    this.ID_CLASS_PREFIX = "shapes-";

    this.referenceBox = "border";
    this.useStrokeBox = false;
    this.geometryBox = "";
    this.hoveredPoint = null;
    this.fillRule = "";
    this.numInsetPoints = 0;
    this.transformMode = false;
    this.viewport = {};

    this.markup = new CanvasFrameAnonymousContentHelper(
      this.highlighterEnv,
      this._buildMarkup.bind(this)
    );
    this.isReady = this.markup.initialize();
    this.onPageHide = this.onPageHide.bind(this);

    const { pageListenerTarget } = this.highlighterEnv;
    DOM_EVENTS.forEach(event =>
      pageListenerTarget.addEventListener(event, this)
    );
    pageListenerTarget.addEventListener("pagehide"this.onPageHide);
  }

  _buildMarkup() {
    const container = this.markup.createNode({
      attributes: {
        class"highlighter-container",
      },
    });

    // The root wrapper is used to unzoom the highlighter when needed.
    const rootWrapper = this.markup.createNode({
      parent: container,
      attributes: {
        id: "root",
        class"root",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    const mainSvg = this.markup.createSVGNode({
      nodeType: "svg",
      parent: rootWrapper,
      attributes: {
        id: "shape-container",
        class"shape-container",
        viewBox: "0 0 100 100",
        preserveAspectRatio: "none",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    // This clipPath and its children make sure the element quad outline
    // is only shown when the shape extends past the element quads.
    const clipSvg = this.markup.createSVGNode({
      nodeType: "clipPath",
      parent: mainSvg,
      attributes: {
        id: "clip-path",
        class"clip-path",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    this.markup.createSVGNode({
      nodeType: "polygon",
      parent: clipSvg,
      attributes: {
        id: "clip-polygon",
        class"clip-polygon",
        hidden: "true",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    this.markup.createSVGNode({
      nodeType: "ellipse",
      parent: clipSvg,
      attributes: {
        id: "clip-ellipse",
        class"clip-ellipse",
        hidden: true,
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    this.markup.createSVGNode({
      nodeType: "rect",
      parent: clipSvg,
      attributes: {
        id: "clip-rect",
        class"clip-rect",
        hidden: true,
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    // Rectangle that displays the element quads. Only shown for shape-outside.
    // Only the parts of the rectangle's outline that overlap with the shape is shown.
    this.markup.createSVGNode({
      nodeType: "rect",
      parent: mainSvg,
      attributes: {
        id: "quad",
        class"quad",
        hidden: "true",
        "clip-path""url(#shapes-clip-path)",
        x: 0,
        y: 0,
        width: 100,
        height: 100,
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    // clipPath that corresponds to the element's quads. Only applied for shape-outside.
    // This ensures only the parts of the shape that are within the element's quads are
    // outlined by a solid line.
    const shapeClipSvg = this.markup.createSVGNode({
      nodeType: "clipPath",
      parent: mainSvg,
      attributes: {
        id: "quad-clip-path",
        class"quad-clip-path",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    this.markup.createSVGNode({
      nodeType: "rect",
      parent: shapeClipSvg,
      attributes: {
        id: "quad-clip",
        class"quad-clip",
        x: -1,
        y: -1,
        width: 102,
        height: 102,
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    const mainGroup = this.markup.createSVGNode({
      nodeType: "g",
      parent: mainSvg,
      attributes: {
        id: "group",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    // Append a polygon for polygon shapes.
    this.markup.createSVGNode({
      nodeType: "polygon",
      parent: mainGroup,
      attributes: {
        id: "polygon",
        class"polygon",
        hidden: "true",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    // Append an ellipse for circle/ellipse shapes.
    this.markup.createSVGNode({
      nodeType: "ellipse",
      parent: mainGroup,
      attributes: {
        id: "ellipse",
        class"ellipse",
        hidden: true,
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    // Append a rect for inset().
    this.markup.createSVGNode({
      nodeType: "rect",
      parent: mainGroup,
      attributes: {
        id: "rect",
        class"rect",
        hidden: true,
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    // Dashed versions of each shape. Only shown for the parts of the shape
    // that extends past the element's quads.
    this.markup.createSVGNode({
      nodeType: "polygon",
      parent: mainGroup,
      attributes: {
        id: "dashed-polygon",
        class"polygon",
        hidden: "true",
        "stroke-dasharray""5, 5",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    this.markup.createSVGNode({
      nodeType: "ellipse",
      parent: mainGroup,
      attributes: {
        id: "dashed-ellipse",
        class"ellipse",
        hidden: "true",
        "stroke-dasharray""5, 5",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    this.markup.createSVGNode({
      nodeType: "rect",
      parent: mainGroup,
      attributes: {
        id: "dashed-rect",
        class"rect",
        hidden: "true",
        "stroke-dasharray""5, 5",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    this.markup.createSVGNode({
      nodeType: "path",
      parent: mainGroup,
      attributes: {
        id: "bounding-box",
        class"bounding-box",
        "stroke-dasharray""5, 5",
        hidden: true,
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    this.markup.createSVGNode({
      nodeType: "path",
      parent: mainGroup,
      attributes: {
        id: "rotate-line",
        class"rotate-line",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    // Append a path to display the markers for the shape.
    this.markup.createSVGNode({
      nodeType: "path",
      parent: mainGroup,
      attributes: {
        id: "markers-outline",
        class"markers-outline",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    this.markup.createSVGNode({
      nodeType: "path",
      parent: mainGroup,
      attributes: {
        id: "markers",
        class"markers",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    this.markup.createSVGNode({
      nodeType: "path",
      parent: mainGroup,
      attributes: {
        id: "marker-hover",
        class"marker-hover",
        hidden: true,
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    return container;
  }

  get currentDimensions() {
    let dims = this.currentQuads[this.referenceBox][0].bounds;
    const zoom = getCurrentZoom(this.win);

    // If an SVG element has a stroke, currentQuads will return the stroke bounding box.
    // However, clip-path always uses the object bounding box unless "stroke-box" is
    // specified. So, we must calculate the object bounding box if there is a stroke
    // and "stroke-box" is not specified. stroke only applies to SVG elements, so use
    // getBBox, which only exists for SVG, to check if currentNode is an SVG element.
    if (
      this.drawingNode.getBBox &&
      getComputedStyle(this.drawingNode).stroke !== "none" &&
      !this.useStrokeBox
    ) {
      dims = getObjectBoundingBox(
        dims.top,
        dims.left,
        dims.width,
        dims.height,
        this.drawingNode
      );
    }

    return {
      top: dims.top / zoom,
      left: dims.left / zoom,
      width: dims.width / zoom,
      height: dims.height / zoom,
    };
  }

  get frameDimensions() {
    // In an iframe, we get the node's quads relative to the frame, instead of the parent
    // document.
    let dims =
      this.highlighterEnv.window.document === this.drawingNode.ownerDocument
        ? this.currentQuads[this.referenceBox][0].bounds
        : getAdjustedQuads(
            this.drawingNode.ownerGlobal,
            this.drawingNode,
            this.referenceBox
          )[0].bounds;
    const zoom = getCurrentZoom(this.win);

    // If an SVG element has a stroke, currentQuads will return the stroke bounding box.
    // However, clip-path always uses the object bounding box unless "stroke-box" is
    // specified. So, we must calculate the object bounding box if there is a stroke
    // and "stroke-box" is not specified. stroke only applies to SVG elements, so use
    // getBBox, which only exists for SVG, to check if currentNode is an SVG element.
    if (
      this.drawingNode.getBBox &&
      getComputedStyle(this.drawingNode).stroke !== "none" &&
      !this.useStrokeBox
    ) {
      dims = getObjectBoundingBox(
        dims.top,
        dims.left,
        dims.width,
        dims.height,
        this.drawingNode
      );
    }

    return {
      top: dims.top / zoom,
      left: dims.left / zoom,
      width: dims.width / zoom,
      height: dims.height / zoom,
    };
  }

  /**
   * Changes the appearance of the mouse cursor on the highlighter.
   *
   * Because we can't attach event handlers to individual elements in the
   * highlighter, we determine if the mouse is hovering over a point by seeing if
   * it's within 5 pixels of it. This creates a square hitbox that doesn't match
   * perfectly with the circular markers. So if we were to use the :hover
   * pseudo-class to apply changes to the mouse cursor, the cursor change would not
   * always accurately reflect whether you can interact with the point. This is
   * also the reason we have the hidden marker-hover element instead of using CSS
   * to fill in the marker.
   *
   * In addition, the cursor CSS property is applied to .shapes-root because if
   * it were attached to .shapes-marker, the cursor change no longer applies if
   * you are for example resizing the shape and your mouse goes off the point.
   * Also, if you are dragging a polygon point, the marker plays catch up to your
   * mouse position, resulting in an undesirable visual effect where the cursor
   * rapidly flickers between "grab" and "auto".
   *
   * @param {String} cursorType the name of the cursor to display
   */

  setCursor(cursorType) {
    const container = this.getElement("root");
    let style = container.getAttribute("style");
    // remove existing cursor definitions in the style
    style = style.replace(/cursor:.*?;/g, "");
    style = style.replace(/pointer-events:.*?;/g, "");
    const pointerEvents = cursorType === "auto" ? "none" : "auto";
    container.setAttribute(
      "style",
      `${style}pointer-events:${pointerEvents};cursor:${cursorType};`
    );
  }

  /**
   * Set the absolute pixel offsets which define the current viewport in relation to
   * the full page size.
   *
   * If a padding value is given, inset the viewport by this value. This is used to define
   * a virtual viewport which ensures some element remains visible even when at the edges
   * of the actual viewport.
   *
   * @param {Number} padding
   *        Optional. Amount by which to inset the viewport in all directions.
   */

  setViewport(padding = 0) {
    let xOffset = 0;
    let yOffset = 0;

    // If the node exists within an iframe, get offsets for the virtual viewport so that
    // points can be dragged to the extent of the global window, outside of the iframe
    // window.
    if (this.currentNode.ownerGlobal !== this.win) {
      const win = this.win;
      const nodeWin = this.currentNode.ownerGlobal;
      // Get bounding box of iframe document relative to global document.
      const bounds = nodeWin.document
        .getBoxQuads({
          relativeTo: win.document,
          createFramesForSuppressedWhitespace: false,
        })[0]
        .getBounds();
      xOffset = bounds.left - nodeWin.scrollX + win.scrollX;
      yOffset = bounds.top - nodeWin.scrollY + win.scrollY;
    }

    const { pageXOffset, pageYOffset } = this.win;
    const { clientHeight, clientWidth } = this.win.document.documentElement;
    const left = pageXOffset + padding - xOffset;
    const right = clientWidth + pageXOffset - padding - xOffset;
    const top = pageYOffset + padding - yOffset;
    const bottom = clientHeight + pageYOffset - padding - yOffset;
    this.viewport = { left, right, top, bottom, padding };
  }

  // eslint-disable-next-line complexity
  handleEvent(event) {
    // No event handling if the highlighter is hidden
    if (this.areShapesHidden()) {
      return;
    }

    let { target, type, pageX, pageY } = event;

    // For events on highlighted nodes in an iframe, when the event takes place
    // outside the iframe. Check if event target belongs to the iframe. If it doesn't,
    // adjust pageX/pageY to be relative to the iframe rather than the parent.
    const nodeDocument = this.currentNode.ownerDocument;
    if (target !== nodeDocument && target.ownerDocument !== nodeDocument) {
      const [xOffset, yOffset] = getFrameOffsets(
        target.ownerGlobal,
        this.currentNode
      );
      const zoom = getCurrentZoom(this.win);
      // xOffset/yOffset are relative to the viewport, so first find the top/left
      // edges of the viewport relative to the page.
      const viewportLeft = pageX - event.clientX;
      const viewportTop = pageY - event.clientY;
      // Also adjust for scrolling in the iframe.
      const { scrollTop, scrollLeft } = nodeDocument.documentElement;
      pageX -= viewportLeft + xOffset / zoom - scrollLeft;
      pageY -= viewportTop + yOffset / zoom - scrollTop;
    }

    switch (type) {
      case "pagehide":
        // If a page hide event is triggered for current window's highlighter, hide the
        // highlighter.
        if (target.defaultView === this.win) {
          this.destroy();
        }

        break;
      case "mousedown":
        if (this.transformMode) {
          this._handleTransformClick(pageX, pageY);
        } else if (this.shapeType === "polygon") {
          this._handlePolygonClick(pageX, pageY);
        } else if (this.shapeType === "circle") {
          this._handleCircleClick(pageX, pageY);
        } else if (this.shapeType === "ellipse") {
          this._handleEllipseClick(pageX, pageY);
        } else if (this.shapeType === "inset") {
          this._handleInsetClick(pageX, pageY);
        }
        event.stopPropagation();
        event.preventDefault();

        // Calculate constraints for a virtual viewport which ensures that a dragged
        // marker remains visible even at the edges of the actual viewport.
        this.setViewport(BASE_MARKER_SIZE);
        break;
      case "mouseup":
        if (this[_dragging]) {
          this[_dragging] = null;
          this._handleMarkerHover(this.hoveredPoint);
        }
        break;
      case "mousemove":
        if (!this[_dragging]) {
          this._handleMouseMoveNotDragging(pageX, pageY);
          return;
        }
        event.stopPropagation();
        event.preventDefault();

        // Set constraints for mouse position to ensure dragged marker stays in viewport.
        const { left, right, top, bottom } = this.viewport;
        pageX = Math.min(Math.max(left, pageX), right);
        pageY = Math.min(Math.max(top, pageY), bottom);

        const { point } = this[_dragging];
        if (this.transformMode) {
          this._handleTransformMove(pageX, pageY);
        } else if (this.shapeType === "polygon") {
          this._handlePolygonMove(pageX, pageY);
        } else if (this.shapeType === "circle") {
          this._handleCircleMove(point, pageX, pageY);
        } else if (this.shapeType === "ellipse") {
          this._handleEllipseMove(point, pageX, pageY);
        } else if (this.shapeType === "inset") {
          this._handleInsetMove(point, pageX, pageY);
        }
        break;
      case "dblclick":
        if (this.shapeType === "polygon" && !this.transformMode) {
          const { percentX, percentY } = this.convertPageCoordsToPercent(
            pageX,
            pageY
          );
          const index = this.getPolygonPointAt(percentX, percentY);
          if (index === -1) {
            this.getPolygonClickedLine(percentX, percentY);
            return;
          }

          this._deletePolygonPoint(index);
        }
        break;
    }
  }

  /**
   * Handle a mouse click in transform mode.
   * @param {Number} pageX the x coordinate of the mouse
   * @param {Number} pageY the y coordinate of the mouse
   */

  _handleTransformClick(pageX, pageY) {
    const { percentX, percentY } = this.convertPageCoordsToPercent(
      pageX,
      pageY
    );
    const type = this.getTransformPointAt(percentX, percentY);
    if (!type) {
      return;
    }

    if (this.shapeType === "polygon") {
      this._handlePolygonTransformClick(pageX, pageY, type);
    } else if (this.shapeType === "circle") {
      this._handleCircleTransformClick(pageX, pageY, type);
    } else if (this.shapeType === "ellipse") {
      this._handleEllipseTransformClick(pageX, pageY, type);
    } else if (this.shapeType === "inset") {
      this._handleInsetTransformClick(pageX, pageY, type);
    }
  }

  /**
   * Handle a click in transform mode while highlighting a polygon.
   * @param {Number} pageX the x coordinate of the mouse.
   * @param {Number} pageY the y coordinate of the mouse.
   * @param {String} type the type of transform handle that was clicked.
   */

  _handlePolygonTransformClick(pageX, pageY, type) {
    const { width, height } = this.currentDimensions;
    const pointsInfo = this.origCoordUnits.map(([x, y], i) => {
      const xComputed = (this.origCoordinates[i][0] / 100) * width;
      const yComputed = (this.origCoordinates[i][1] / 100) * height;
      const unitX = getUnit(x);
      const unitY = getUnit(y);
      const valueX = isUnitless(x) ? xComputed : parseFloat(x);
      const valueY = isUnitless(y) ? yComputed : parseFloat(y);

      const ratioX = this.getUnitToPixelRatio(unitX, width);
      const ratioY = this.getUnitToPixelRatio(unitY, height);
      return { unitX, unitY, valueX, valueY, ratioX, ratioY };
    });
    this[_dragging] = {
      type,
      pointsInfo,
      x: pageX,
      y: pageY,
      bb: this.boundingBox,
      matrix: this.transformMatrix,
      transformedBB: this.transformedBoundingBox,
    };
    this._handleMarkerHover(this.hoveredPoint);
  }

  /**
   * Handle a click in transform mode while highlighting a circle.
   * @param {Number} pageX the x coordinate of the mouse.
   * @param {Number} pageY the y coordinate of the mouse.
   * @param {String} type the type of transform handle that was clicked.
   */

  _handleCircleTransformClick(pageX, pageY, type) {
    const { width, height } = this.currentDimensions;
    const { cx, cy } = this.origCoordUnits;
    const cxComputed = (this.origCoordinates.cx / 100) * width;
    const cyComputed = (this.origCoordinates.cy / 100) * height;
    const unitX = getUnit(cx);
    const unitY = getUnit(cy);
    const valueX = isUnitless(cx) ? cxComputed : parseFloat(cx);
    const valueY = isUnitless(cy) ? cyComputed : parseFloat(cy);

    const ratioX = this.getUnitToPixelRatio(unitX, width);
    const ratioY = this.getUnitToPixelRatio(unitY, height);

    let { radius } = this.origCoordinates;
    const computedSize = Math.sqrt(width ** 2 + height ** 2) / Math.sqrt(2);
    radius = (radius / 100) * computedSize;
    let valueRad = this.origCoordUnits.radius;
    const unitRad = getUnit(valueRad);
    valueRad = isUnitless(valueRad) ? radius : parseFloat(valueRad);
    const ratioRad = this.getUnitToPixelRatio(unitRad, computedSize);

    this[_dragging] = {
      type,
      unitX,
      unitY,
      unitRad,
      valueX,
      valueY,
      ratioX,
      ratioY,
      ratioRad,
      x: pageX,
      y: pageY,
      bb: this.boundingBox,
      matrix: this.transformMatrix,
      transformedBB: this.transformedBoundingBox,
    };
  }

  /**
   * Handle a click in transform mode while highlighting an ellipse.
   * @param {Number} pageX the x coordinate of the mouse.
   * @param {Number} pageY the y coordinate of the mouse.
   * @param {String} type the type of transform handle that was clicked.
   */

  _handleEllipseTransformClick(pageX, pageY, type) {
    const { width, height } = this.currentDimensions;
    const { cx, cy } = this.origCoordUnits;
    const cxComputed = (this.origCoordinates.cx / 100) * width;
    const cyComputed = (this.origCoordinates.cy / 100) * height;
    const unitX = getUnit(cx);
    const unitY = getUnit(cy);
    const valueX = isUnitless(cx) ? cxComputed : parseFloat(cx);
    const valueY = isUnitless(cy) ? cyComputed : parseFloat(cy);

    const ratioX = this.getUnitToPixelRatio(unitX, width);
    const ratioY = this.getUnitToPixelRatio(unitY, height);

    let { rx, ry } = this.origCoordinates;
    rx = (rx / 100) * width;
    let valueRX = this.origCoordUnits.rx;
    const unitRX = getUnit(valueRX);
    valueRX = isUnitless(valueRX) ? rx : parseFloat(valueRX);
    const ratioRX = valueRX / rx || 1;
    ry = (ry / 100) * height;
    let valueRY = this.origCoordUnits.ry;
    const unitRY = getUnit(valueRY);
    valueRY = isUnitless(valueRY) ? ry : parseFloat(valueRY);
    const ratioRY = valueRY / ry || 1;

    this[_dragging] = {
      type,
      unitX,
      unitY,
      unitRX,
      unitRY,
      valueX,
      valueY,
      ratioX,
      ratioY,
      ratioRX,
      ratioRY,
      x: pageX,
      y: pageY,
      bb: this.boundingBox,
      matrix: this.transformMatrix,
      transformedBB: this.transformedBoundingBox,
    };
  }

  /**
   * Handle a click in transform mode while highlighting an inset.
   * @param {Number} pageX the x coordinate of the mouse.
   * @param {Number} pageY the y coordinate of the mouse.
   * @param {String} type the type of transform handle that was clicked.
   */

  _handleInsetTransformClick(pageX, pageY, type) {
    const { width, height } = this.currentDimensions;
    const pointsInfo = {};
    ["top""right""bottom""left"].forEach(point => {
      let value = this.origCoordUnits[point];
      const size = point === "left" || point === "right" ? width : height;
      const computedValue = (this.origCoordinates[point] / 100) * size;
      const unit = getUnit(value);
      value = isUnitless(value) ? computedValue : parseFloat(value);
      const ratio = this.getUnitToPixelRatio(unit, size);

      pointsInfo[point] = { value, unit, ratio };
    });
    this[_dragging] = {
      type,
      pointsInfo,
      x: pageX,
      y: pageY,
      bb: this.boundingBox,
      matrix: this.transformMatrix,
      transformedBB: this.transformedBoundingBox,
    };
  }

  /**
   * Handle mouse movement after a click on a handle in transform mode.
   * @param {Number} pageX the x coordinate of the mouse
   * @param {Number} pageY the y coordinate of the mouse
   */

  _handleTransformMove(pageX, pageY) {
    const { type } = this[_dragging];
    if (type === "translate") {
      this._translateShape(pageX, pageY);
    } else if (type.includes("scale")) {
      this._scaleShape(pageX, pageY);
    } else if (type === "rotate" && this.shapeType === "polygon") {
      this._rotateShape(pageX, pageY);
    }

    this.transformedBoundingBox = this.calculateTransformedBoundingBox();
  }

  /**
   * Translates a shape based on the current mouse position.
   * @param {Number} pageX the x coordinate of the mouse.
   * @param {Number} pageY the y coordinate of the mouse.
   */

  _translateShape(pageX, pageY) {
    const { x, y, matrix } = this[_dragging];
    const deltaX = pageX - x;
    const deltaY = pageY - y;
    this.transformMatrix = multiply(translate(deltaX, deltaY), matrix);

    if (this.shapeType === "polygon") {
      this._transformPolygon();
    } else if (this.shapeType === "circle") {
      this._transformCircle();
    } else if (this.shapeType === "ellipse") {
      this._transformEllipse();
    } else if (this.shapeType === "inset") {
      this._transformInset();
    }
  }

  /**
   * Scales a shape according to the current mouse position.
   * @param {Number} pageX the x coordinate of the mouse.
   * @param {Number} pageY the y coordinate of the mouse.
   */

  _scaleShape(pageX, pageY) {
    /**
     * To scale a shape:
     * 1) Get the change of basis matrix corresponding to the current transformation
     *    matrix of the shape.
     * 2) Convert the mouse x/y deltas to the "transformed" coordinate system, using
     *    the change of base matrix.
     * 3) Calculate the proportion to which the shape should be scaled to, using the
     *    mouse x/y deltas and the width/height of the transformed shape.
     * 4) Translate the shape such that the anchor (the point opposite to the one
     *    being dragged) is at the top left of the element.
     * 5) Scale each point by multiplying by the scaling proportion.
     * 6) Translate the shape back such that the anchor is in its original position.
     */

    const { type, x, y, matrix } = this[_dragging];
    const { width, height } = this.currentDimensions;
    // The point opposite to the one being dragged
    const anchor = getAnchorPoint(type);

    const { ne, nw, sw } = this[_dragging].transformedBB;
    // u/v are the basis vectors of the transformed coordinate system.
    const u = [
      ((ne[0] - nw[0]) / 100) * width,
      ((ne[1] - nw[1]) / 100) * height,
    ];
    const v = [
      ((sw[0] - nw[0]) / 100) * width,
      ((sw[1] - nw[1]) / 100) * height,
    ];
    // uLength/vLength represent the width/height of the shape in the
    // transformed coordinate system.
    const { basis, invertedBasis, uLength, vLength } = getBasis(u, v);

    // How much points on each axis should be translated before scaling
    const transX = (this[_dragging].transformedBB[anchor][0] / 100) * width;
    const transY = (this[_dragging].transformedBB[anchor][1] / 100) * height;

    // Distance from original click to current mouse position
    const distanceX = pageX - x;
    const distanceY = pageY - y;
    // Convert from original coordinate system to transformed coordinate system
    const tDistanceX =
      invertedBasis[0] * distanceX + invertedBasis[1] * distanceY;
    const tDistanceY =
      invertedBasis[3] * distanceX + invertedBasis[4] * distanceY;

    // Proportion of distance to bounding box width/height of shape
    const proportionX = tDistanceX / uLength;
    const proportionY = tDistanceY / vLength;
    // proportionX is positive for size reductions dragging on w/nw/sw,
    // negative for e/ne/se.
    const scaleX = type.includes("w") ? 1 - proportionX : 1 + proportionX;
    // proportionT is positive for size reductions dragging on n/nw/ne,
    // negative for s/sw/se.
    const scaleY = type.includes("n") ? 1 - proportionY : 1 + proportionY;
    // Take the average of scaleX/scaleY for scaling on two axes
    const scaleXY = (scaleX + scaleY) / 2;

    const translateMatrix = translate(-transX, -transY);
    let scaleMatrix = identity();
    // The scale matrices are in the transformed coordinate system. We must convert
    // them to the original coordinate system before applying it to the transformation
    // matrix.
    if (type === "scale-e" || type === "scale-w") {
      scaleMatrix = changeMatrixBase(scale(scaleX, 1), invertedBasis, basis);
    } else if (type === "scale-n" || type === "scale-s") {
      scaleMatrix = changeMatrixBase(scale(1, scaleY), invertedBasis, basis);
    } else {
      scaleMatrix = changeMatrixBase(
        scale(scaleXY, scaleXY),
        invertedBasis,
        basis
      );
    }
    const translateBackMatrix = translate(transX, transY);
    this.transformMatrix = multiply(
      translateBackMatrix,
      multiply(scaleMatrix, multiply(translateMatrix, matrix))
    );

    if (this.shapeType === "polygon") {
      this._transformPolygon();
    } else if (this.shapeType === "circle") {
      this._transformCircle(transX);
    } else if (this.shapeType === "ellipse") {
      this._transformEllipse(transX, transY);
    } else if (this.shapeType === "inset") {
      this._transformInset();
    }
  }

  /**
   * Rotates a polygon based on the current mouse position.
   * @param {Number} pageX the x coordinate of the mouse.
   * @param {Number} pageY the y coordinate of the mouse.
   */

  _rotateShape(pageX, pageY) {
    const { matrix } = this[_dragging];
    const { center, ne, nw, sw } = this[_dragging].transformedBB;
    const { width, height } = this.currentDimensions;
    const centerX = (center[0] / 100) * width;
    const centerY = (center[1] / 100) * height;
    const { x: pageCenterX, y: pageCenterY } = this.convertPercentToPageCoords(
      ...center
    );

    const dx = pageCenterX - pageX;
    const dy = pageCenterY - pageY;

    const u = [
      ((ne[0] - nw[0]) / 100) * width,
      ((ne[1] - nw[1]) / 100) * height,
    ];
    const v = [
      ((sw[0] - nw[0]) / 100) * width,
      ((sw[1] - nw[1]) / 100) * height,
    ];
    const { invertedBasis } = getBasis(u, v);

    const tdx = invertedBasis[0] * dx + invertedBasis[1] * dy;
    const tdy = invertedBasis[3] * dx + invertedBasis[4] * dy;
    const angle = Math.atan2(tdx, tdy);
    const translateMatrix = translate(-centerX, -centerY);
    const rotateMatrix = rotate(angle);
    const translateBackMatrix = translate(centerX, centerY);
    this.transformMatrix = multiply(
      translateBackMatrix,
      multiply(rotateMatrix, multiply(translateMatrix, matrix))
    );

    this._transformPolygon();
  }

  /**
   * Transform a polygon depending on the current transformation matrix.
   */

  _transformPolygon() {
    const { pointsInfo } = this[_dragging];

    let polygonDef = this.fillRule ? `${this.fillRule}, ` : "";
    polygonDef += pointsInfo
      .map(point => {
        const { unitX, unitY, valueX, valueY, ratioX, ratioY } = point;
        const vector = [valueX / ratioX, valueY / ratioY];
        let [newX, newY] = apply(this.transformMatrix, vector);
        newX = round(newX * ratioX, unitX);
        newY = round(newY * ratioY, unitY);

        return `${newX}${unitX} ${newY}${unitY}`;
      })
      .join(", ");
    polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();

    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
  }

  /**
   * Transform a circle depending on the current transformation matrix.
   * @param {Number} transX the number of pixels the shape is translated on the x axis
   *                 before scaling
   */

  _transformCircle(transX = null) {
    const { unitX, unitY, unitRad, valueX, valueY, ratioX, ratioY, ratioRad } =
      this[_dragging];
    let { radius } = this.coordUnits;

    let [newCx, newCy] = apply(this.transformMatrix, [
      valueX / ratioX,
      valueY / ratioY,
    ]);
    if (transX !== null) {
      // As part of scaling, the shape is translated to be tangent to the line y=0.
      // To get the new radius, we translate the new cx back to that point and get
      // the distance to the line y=0.
      radius = round(Math.abs((newCx - transX) * ratioRad), unitRad);
      radius = `${radius}${unitRad}`;
    }

    newCx = round(newCx * ratioX, unitX);
    newCy = round(newCy * ratioY, unitY);
    const circleDef =
      `circle(${radius} at ${newCx}${unitX} ${newCy}${unitY})` +
      ` ${this.geometryBox}`.trim();
    this.emit("highlighter-event", { type: "shape-change", value: circleDef });
  }

  /**
   * Transform an ellipse depending on the current transformation matrix.
   * @param {Number} transX the number of pixels the shape is translated on the x axis
   *                 before scaling
   * @param {Number} transY the number of pixels the shape is translated on the y axis
   *                 before scaling
   */

  _transformEllipse(transX = null, transY = null) {
    const {
      unitX,
      unitY,
      unitRX,
      unitRY,
      valueX,
      valueY,
      ratioX,
      ratioY,
      ratioRX,
      ratioRY,
    } = this[_dragging];
    let { rx, ry } = this.coordUnits;

    let [newCx, newCy] = apply(this.transformMatrix, [
      valueX / ratioX,
      valueY / ratioY,
    ]);
    if (transX !== null && transY !== null) {
      // As part of scaling, the shape is translated to be tangent to the lines y=0 & x=0.
      // To get the new radii, we translate the new center back to that point and get the
      // distances to the line x=0 and y=0.
      rx = round(Math.abs((newCx - transX) * ratioRX), unitRX);
      rx = `${rx}${unitRX}`;
      ry = round(Math.abs((newCy - transY) * ratioRY), unitRY);
      ry = `${ry}${unitRY}`;
    }

    newCx = round(newCx * ratioX, unitX);
    newCy = round(newCy * ratioY, unitY);

    const centerStr = `${newCx}${unitX} ${newCy}${unitY}`;
    const ellipseDef =
      `ellipse(${rx} ${ry} at ${centerStr}) ${this.geometryBox}`.trim();
    this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
  }

  /**
   * Transform an inset depending on the current transformation matrix.
   */

  _transformInset() {
    const { top, left, right, bottom } = this[_dragging].pointsInfo;
    const { width, height } = this.currentDimensions;

    const topLeft = [left.value / left.ratio, top.value / top.ratio];
    let [newLeft, newTop] = apply(this.transformMatrix, topLeft);
    newLeft = round(newLeft * left.ratio, left.unit);
    newLeft = `${newLeft}${left.unit}`;
    newTop = round(newTop * top.ratio, top.unit);
    newTop = `${newTop}${top.unit}`;

    // Right and bottom values are relative to the right and bottom edges of the
    // element, so convert to the value relative to the left/top edges before scaling
    // and convert back.
    const bottomRight = [
      width - right.value / right.ratio,
      height - bottom.value / bottom.ratio,
    ];
    let [newRight, newBottom] = apply(this.transformMatrix, bottomRight);
    newRight = round((width - newRight) * right.ratio, right.unit);
    newRight = `${newRight}${right.unit}`;
    newBottom = round((height - newBottom) * bottom.ratio, bottom.unit);
    newBottom = `${newBottom}${bottom.unit}`;

    let insetDef = this.insetRound
      ? `inset(${newTop} ${newRight} ${newBottom} ${newLeft} round ${this.insetRound})`
      : `inset(${newTop} ${newRight} ${newBottom} ${newLeft})`;
    insetDef += this.geometryBox ? this.geometryBox : "";

    this.emit("highlighter-event", { type: "shape-change", value: insetDef });
  }

  /**
   * Handle a click when highlighting a polygon.
   * @param {Number} pageX the x coordinate of the click
   * @param {Number} pageY the y coordinate of the click
   */

  _handlePolygonClick(pageX, pageY) {
    const { width, height } = this.currentDimensions;
    const { percentX, percentY } = this.convertPageCoordsToPercent(
      pageX,
      pageY
    );
    const point = this.getPolygonPointAt(percentX, percentY);
    if (point === -1) {
      return;
    }

    const [x, y] = this.coordUnits[point];
    const xComputed = (this.coordinates[point][0] / 100) * width;
    const yComputed = (this.coordinates[point][1] / 100) * height;
    const unitX = getUnit(x);
    const unitY = getUnit(y);
    const valueX = isUnitless(x) ? xComputed : parseFloat(x);
    const valueY = isUnitless(y) ? yComputed : parseFloat(y);

    const ratioX = this.getUnitToPixelRatio(unitX, width);
    const ratioY = this.getUnitToPixelRatio(unitY, height);

    this.setCursor("grabbing");
    this[_dragging] = {
      point,
      unitX,
      unitY,
      valueX,
      valueY,
      ratioX,
      ratioY,
      x: pageX,
      y: pageY,
    };
  }

  /**
   * Update the dragged polygon point with the given x/y coords and update
   * the element style.
   * @param {Number} pageX the new x coordinate of the point
   * @param {Number} pageY the new y coordinate of the point
   */

  _handlePolygonMove(pageX, pageY) {
    const { point, unitX, unitY, valueX, valueY, ratioX, ratioY, x, y } =
      this[_dragging];
    const deltaX = (pageX - x) * ratioX;
    const deltaY = (pageY - y) * ratioY;
    const newX = round(valueX + deltaX, unitX);
    const newY = round(valueY + deltaY, unitY);

    let polygonDef = this.fillRule ? `${this.fillRule}, ` : "";
    polygonDef += this.coordUnits
      .map((coords, i) => {
        return i === point
          ? `${newX}${unitX} ${newY}${unitY}`
          : `${coords[0]} ${coords[1]}`;
      })
      .join(", ");
    polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();

    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
  }

  /**
   * Add new point to the polygon defintion and update element style.
   * TODO: Bug 1436054 - Do not default to percentage unit when inserting new point.
   * https://bugzilla.mozilla.org/show_bug.cgi?id=1436054
   *
   * @param {Number} after the index of the point that the new point should be added after
   * @param {Number} x the x coordinate of the new point
   * @param {Number} y the y coordinate of the new point
   */

  _addPolygonPoint(after, x, y) {
    let polygonDef = this.fillRule ? `${this.fillRule}, ` : "";
    polygonDef += this.coordUnits
      .map((coords, i) => {
        return i === after
          ? `${coords[0]} ${coords[1]}, ${x}% ${y}%`
          : `${coords[0]} ${coords[1]}`;
      })
      .join(", ");
    polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();

    this.hoveredPoint = after + 1;
    this._emitHoverEvent(this.hoveredPoint);
    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
  }

  /**
   * Remove point from polygon defintion and update the element style.
   * @param {Number} point the index of the point to delete
   */

  _deletePolygonPoint(point) {
    const coordinates = this.coordUnits.slice();
    coordinates.splice(point, 1);
    let polygonDef = this.fillRule ? `${this.fillRule}, ` : "";
    polygonDef += coordinates
      .map(coords => {
        return `${coords[0]} ${coords[1]}`;
      })
      .join(", ");
    polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();

    this.hoveredPoint = null;
    this._emitHoverEvent(this.hoveredPoint);
    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
  }
  /**
   * Handle a click when highlighting a circle.
   * @param {Number} pageX the x coordinate of the click
   * @param {Number} pageY the y coordinate of the click
   */

  _handleCircleClick(pageX, pageY) {
    const { width, height } = this.currentDimensions;
    const { percentX, percentY } = this.convertPageCoordsToPercent(
      pageX,
      pageY
    );
    const point = this.getCirclePointAt(percentX, percentY);
    if (!point) {
      return;
    }

    this.setCursor("grabbing");
    if (point === "center") {
      const { cx, cy } = this.coordUnits;
      const cxComputed = (this.coordinates.cx / 100) * width;
      const cyComputed = (this.coordinates.cy / 100) * height;
      const unitX = getUnit(cx);
      const unitY = getUnit(cy);
      const valueX = isUnitless(cx) ? cxComputed : parseFloat(cx);
      const valueY = isUnitless(cy) ? cyComputed : parseFloat(cy);

      const ratioX = this.getUnitToPixelRatio(unitX, width);
      const ratioY = this.getUnitToPixelRatio(unitY, height);

      this[_dragging] = {
        point,
        unitX,
        unitY,
        valueX,
        valueY,
        ratioX,
        ratioY,
        x: pageX,
        y: pageY,
      };
    } else if (point === "radius") {
      let { radius } = this.coordinates;
      const computedSize = Math.sqrt(width ** 2 + height ** 2) / Math.sqrt(2);
      radius = (radius / 100) * computedSize;
      let value = this.coordUnits.radius;
      const unit = getUnit(value);
      value = isUnitless(value) ? radius : parseFloat(value);
      const ratio = this.getUnitToPixelRatio(unit, computedSize);

      this[_dragging] = { point, value, origRadius: radius, unit, ratio };
    }
  }

  /**
   * Set the center/radius of the circle according to the mouse position and
   * update the element style.
   * @param {String} point either "center" or "radius"
   * @param {Number} pageX the x coordinate of the mouse position, in terms of %
   *        relative to the element
   * @param {Number} pageY the y coordinate of the mouse position, in terms of %
   *        relative to the element
   */

  _handleCircleMove(point, pageX, pageY) {
    const { radius, cx, cy } = this.coordUnits;

    if (point === "center") {
      const { unitX, unitY, valueX, valueY, ratioX, ratioY, x, y } =
        this[_dragging];
      const deltaX = (pageX - x) * ratioX;
      const deltaY = (pageY - y) * ratioY;
      const newCx = `${round(valueX + deltaX, unitX)}${unitX}`;
      const newCy = `${round(valueY + deltaY, unitY)}${unitY}`;
      // if not defined by the user, geometryBox will be an empty string; trim() cleans up
      const circleDef =
        `circle(${radius} at ${newCx} ${newCy}) ${this.geometryBox}`.trim();

      this.emit("highlighter-event", {
        type: "shape-change",
        value: circleDef,
      });
    } else if (point === "radius") {
      const { value, unit, origRadius, ratio } = this[_dragging];
      // convert center point to px, then get distance between center and mouse.
      const { x: pageCx, y: pageCy } = this.convertPercentToPageCoords(
        this.coordinates.cx,
        this.coordinates.cy
      );
      const newRadiusPx = getDistance(pageCx, pageCy, pageX, pageY);

      const delta = (newRadiusPx - origRadius) * ratio;
      const newRadius = `${round(value + delta, unit)}${unit}`;

      const position = cx !== "" ? ` at ${cx} ${cy}` : "";
      const circleDef =
        `circle(${newRadius}${position}) ${this.geometryBox}`.trim();

      this.emit("highlighter-event", {
        type: "shape-change",
        value: circleDef,
      });
    }
  }

  /**
   * Handle a click when highlighting an ellipse.
   * @param {Number} pageX the x coordinate of the click
   * @param {Number} pageY the y coordinate of the click
   */

  _handleEllipseClick(pageX, pageY) {
    const { width, height } = this.currentDimensions;
    const { percentX, percentY } = this.convertPageCoordsToPercent(
      pageX,
      pageY
    );
    const point = this.getEllipsePointAt(percentX, percentY);
    if (!point) {
      return;
    }

    this.setCursor("grabbing");
    if (point === "center") {
      const { cx, cy } = this.coordUnits;
      const cxComputed = (this.coordinates.cx / 100) * width;
      const cyComputed = (this.coordinates.cy / 100) * height;
      const unitX = getUnit(cx);
      const unitY = getUnit(cy);
      const valueX = isUnitless(cx) ? cxComputed : parseFloat(cx);
      const valueY = isUnitless(cy) ? cyComputed : parseFloat(cy);

      const ratioX = this.getUnitToPixelRatio(unitX, width);
      const ratioY = this.getUnitToPixelRatio(unitY, height);

      this[_dragging] = {
        point,
        unitX,
        unitY,
        valueX,
        valueY,
        ratioX,
        ratioY,
        x: pageX,
        y: pageY,
      };
    } else if (point === "rx") {
      let { rx } = this.coordinates;
      rx = (rx / 100) * width;
      let value = this.coordUnits.rx;
      const unit = getUnit(value);
      value = isUnitless(value) ? rx : parseFloat(value);
      const ratio = this.getUnitToPixelRatio(unit, width);

      this[_dragging] = { point, value, origRadius: rx, unit, ratio };
    } else if (point === "ry") {
      let { ry } = this.coordinates;
      ry = (ry / 100) * height;
      let value = this.coordUnits.ry;
      const unit = getUnit(value);
      value = isUnitless(value) ? ry : parseFloat(value);
      const ratio = this.getUnitToPixelRatio(unit, height);

      this[_dragging] = { point, value, origRadius: ry, unit, ratio };
    }
  }

  /**
   * Set center/rx/ry of the ellispe according to the mouse position and update the
   * element style.
   * @param {String} point "center", "rx", or "ry"
   * @param {Number} pageX the x coordinate of the mouse position, in terms of %
   *        relative to the element
   * @param {Number} pageY the y coordinate of the mouse position, in terms of %
   *        relative to the element
   */

  _handleEllipseMove(point, pageX, pageY) {
    const { percentX, percentY } = this.convertPageCoordsToPercent(
      pageX,
      pageY
    );
    const { rx, ry, cx, cy } = this.coordUnits;
    const position = cx !== "" ? ` at ${cx} ${cy}` : "";

    if (point === "center") {
      const { unitX, unitY, valueX, valueY, ratioX, ratioY, x, y } =
        this[_dragging];
      const deltaX = (pageX - x) * ratioX;
      const deltaY = (pageY - y) * ratioY;
      const newCx = `${round(valueX + deltaX, unitX)}${unitX}`;
      const newCy = `${round(valueY + deltaY, unitY)}${unitY}`;
      const ellipseDef =
        `ellipse(${rx} ${ry} at ${newCx} ${newCy}) ${this.geometryBox}`.trim();

      this.emit("highlighter-event", {
        type: "shape-change",
        value: ellipseDef,
      });
    } else if (point === "rx") {
      const { value, unit, origRadius, ratio } = this[_dragging];
      const newRadiusPercent = Math.abs(percentX - this.coordinates.cx);
      const { width } = this.currentDimensions;
      const delta = ((newRadiusPercent / 100) * width - origRadius) * ratio;
      const newRadius = `${round(value + delta, unit)}${unit}`;

      const ellipseDef =
        `ellipse(${newRadius} ${ry}${position}) ${this.geometryBox}`.trim();

      this.emit("highlighter-event", {
        type: "shape-change",
        value: ellipseDef,
      });
    } else if (point === "ry") {
      const { value, unit, origRadius, ratio } = this[_dragging];
      const newRadiusPercent = Math.abs(percentY - this.coordinates.cy);
      const { height } = this.currentDimensions;
      const delta = ((newRadiusPercent / 100) * height - origRadius) * ratio;
      const newRadius = `${round(value + delta, unit)}${unit}`;

      const ellipseDef =
        `ellipse(${rx} ${newRadius}${position}) ${this.geometryBox}`.trim();

      this.emit("highlighter-event", {
        type: "shape-change",
        value: ellipseDef,
      });
    }
  }

  /**
   * Handle a click when highlighting an inset.
   * @param {Number} pageX the x coordinate of the click
   * @param {Number} pageY the y coordinate of the click
   */

  _handleInsetClick(pageX, pageY) {
    const { width, height } = this.currentDimensions;
    const { percentX, percentY } = this.convertPageCoordsToPercent(
      pageX,
      pageY
    );
    const point = this.getInsetPointAt(percentX, percentY);
    if (!point) {
      return;
    }

    this.setCursor("grabbing");
    let value = this.coordUnits[point];
    const size = point === "left" || point === "right" ? width : height;
    const computedValue = (this.coordinates[point] / 100) * size;
    const unit = getUnit(value);
    value = isUnitless(value) ? computedValue : parseFloat(value);
    const ratio = this.getUnitToPixelRatio(unit, size);
    const origValue = point === "left" || point === "right" ? pageX : pageY;

    this[_dragging] = { point, value, origValue, unit, ratio };
  }

  /**
   * Set the top/left/right/bottom of the inset shape according to the mouse position
   * and update the element style.
   * @param {String} point "top", "left", "right", or "bottom"
   * @param {Number} pageX the x coordinate of the mouse position, in terms of %
   *        relative to the element
   * @param {Number} pageY the y coordinate of the mouse position, in terms of %
   *        relative to the element
   * @memberof ShapesHighlighter
   */

  _handleInsetMove(point, pageX, pageY) {
    let { top, left, right, bottom } = this.coordUnits;
    const { value, origValue, unit, ratio } = this[_dragging];

    if (point === "left") {
      const delta = (pageX - origValue) * ratio;
      left = `${round(value + delta, unit)}${unit}`;
    } else if (point === "right") {
      const delta = (pageX - origValue) * ratio;
      right = `${round(value - delta, unit)}${unit}`;
    } else if (point === "top") {
      const delta = (pageY - origValue) * ratio;
      top = `${round(value + delta, unit)}${unit}`;
    } else if (point === "bottom") {
      const delta = (pageY - origValue) * ratio;
      bottom = `${round(value - delta, unit)}${unit}`;
    }

    let insetDef = this.insetRound
      ? `inset(${top} ${right} ${bottom} ${left} round ${this.insetRound})`
      : `inset(${top} ${right} ${bottom} ${left})`;

    insetDef += this.geometryBox ? this.geometryBox : "";

    this.emit("highlighter-event", { type: "shape-change", value: insetDef });
  }

  _handleMouseMoveNotDragging(pageX, pageY) {
    const { percentX, percentY } = this.convertPageCoordsToPercent(
      pageX,
      pageY
    );
    if (this.transformMode) {
      const point = this.getTransformPointAt(percentX, percentY);
      this.hoveredPoint = point;
      this._handleMarkerHover(point);
    } else if (this.shapeType === "polygon") {
      const point = this.getPolygonPointAt(percentX, percentY);
      const oldHoveredPoint = this.hoveredPoint;
      this.hoveredPoint = point !== -1 ? point : null;
      if (this.hoveredPoint !== oldHoveredPoint) {
        this._emitHoverEvent(this.hoveredPoint);
      }
      this._handleMarkerHover(point);
    } else if (this.shapeType === "circle") {
      const point = this.getCirclePointAt(percentX, percentY);
      const oldHoveredPoint = this.hoveredPoint;
      this.hoveredPoint = point ? point : null;
      if (this.hoveredPoint !== oldHoveredPoint) {
        this._emitHoverEvent(this.hoveredPoint);
      }
      this._handleMarkerHover(point);
    } else if (this.shapeType === "ellipse") {
      const point = this.getEllipsePointAt(percentX, percentY);
      const oldHoveredPoint = this.hoveredPoint;
      this.hoveredPoint = point ? point : null;
      if (this.hoveredPoint !== oldHoveredPoint) {
        this._emitHoverEvent(this.hoveredPoint);
      }
      this._handleMarkerHover(point);
    } else if (this.shapeType === "inset") {
      const point = this.getInsetPointAt(percentX, percentY);
      const oldHoveredPoint = this.hoveredPoint;
      this.hoveredPoint = point ? point : null;
      if (this.hoveredPoint !== oldHoveredPoint) {
        this._emitHoverEvent(this.hoveredPoint);
      }
      this._handleMarkerHover(point);
    }
  }

  /**
   * Change the appearance of the given marker when the mouse hovers over it.
   * @param {String|Number} point if the shape is a polygon, the integer index of the
   *        point being hovered. Otherwise, a string identifying the point being hovered.
   *        Integers < 0 and falsey values excluding 0 indicate no point is being hovered.
   */

  _handleMarkerHover(point) {
    // Hide hover marker for now, will be shown if point is a valid hover target
    this.getElement("marker-hover").setAttribute("hidden"true);
    // Catch all falsey values except when point === 0, as that's a valid point
    if (!point && point !== 0) {
      this.setCursor("auto");
      return;
    }
    const hoverCursor = this[_dragging] ? "grabbing" : "grab";

    if (this.transformMode) {
      if (!point) {
        this.setCursor("auto");
        return;
      }
      const { nw, ne, sw, se, n, w, s, e, rotatePoint, center } =
        this.transformedBoundingBox;

      const points = [
        {
          pointName: "translate",
          x: center[0],
          y: center[1],
          cursor: hoverCursor,
        },
        { pointName: "scale-se", x: se[0], y: se[1], anchor: "nw" },
        { pointName: "scale-ne", x: ne[0], y: ne[1], anchor: "sw" },
        { pointName: "scale-sw", x: sw[0], y: sw[1], anchor: "ne" },
        { pointName: "scale-nw", x: nw[0], y: nw[1], anchor: "se" },
        { pointName: "scale-n", x: n[0], y: n[1], anchor: "s" },
        { pointName: "scale-s", x: s[0], y: s[1], anchor: "n" },
        { pointName: "scale-e", x: e[0], y: e[1], anchor: "w" },
        { pointName: "scale-w", x: w[0], y: w[1], anchor: "e" },
        {
          pointName: "rotate",
          x: rotatePoint[0],
          y: rotatePoint[1],
          cursor: hoverCursor,
        },
      ];

      for (const { pointName, x, y, cursor, anchor } of points) {
        if (point === pointName) {
          this._drawHoverMarker([[x, y]]);

          // If the point is a scale handle, we will need to determine the direction
          // of the resize cursor based on the position of the handle relative to its
          // "anchor" (the handle opposite to it).
          if (pointName.includes("scale")) {
            const direction = this.getRoughDirection(pointName, anchor);
            this.setCursor(`${direction}-resize`);
          } else {
            this.setCursor(cursor);
          }
        }
      }
    } else if (this.shapeType === "polygon") {
      if (point === -1) {
        this.setCursor("auto");
        return;
      }
      this.setCursor(hoverCursor);
      this._drawHoverMarker([this.coordinates[point]]);
    } else if (this.shapeType === "circle") {
      this.setCursor(hoverCursor);

      const { cx, cy, rx } = this.coordinates;
      if (point === "radius") {
        this._drawHoverMarker([[cx + rx, cy]]);
      } else if (point === "center") {
        this._drawHoverMarker([[cx, cy]]);
      }
    } else if (this.shapeType === "ellipse") {
      this.setCursor(hoverCursor);

      if (point === "center") {
        const { cx, cy } = this.coordinates;
        this._drawHoverMarker([[cx, cy]]);
      } else if (point === "rx") {
        const { cx, cy, rx } = this.coordinates;
        this._drawHoverMarker([[cx + rx, cy]]);
      } else if (point === "ry") {
        const { cx, cy, ry } = this.coordinates;
        this._drawHoverMarker([[cx, cy + ry]]);
      }
    } else if (this.shapeType === "inset") {
      this.setCursor(hoverCursor);

      const { top, right, bottom, left } = this.coordinates;
      const centerX = (left + (100 - right)) / 2;
      const centerY = (top + (100 - bottom)) / 2;
      const points = point.split(",");
      const coords = points.map(side => {
        if (side === "top") {
          return [centerX, top];
        } else if (side === "right") {
          return [100 - right, centerY];
        } else if (side === "bottom") {
          return [centerX, 100 - bottom];
        } else if (side === "left") {
          return [left, centerY];
        }
        return null;
      });

      this._drawHoverMarker(coords);
    }
  }

  _drawHoverMarker(points) {
    const { width, height } = this.currentDimensions;
    const zoom = getCurrentZoom(this.win);
    const path = points
      .map(([x, y]) => {
        return getCirclePath(BASE_MARKER_SIZE, x, y, width, height, zoom);
      })
      .join(" ");

    const markerHover = this.getElement("marker-hover");
    markerHover.setAttribute("d", path);
    markerHover.removeAttribute("hidden");
  }

  _emitHoverEvent(point) {
    if (point === null || point === undefined) {
      this.emit("highlighter-event", {
        type: "shape-hover-off",
      });
    } else {
      this.emit("highlighter-event", {
        type: "shape-hover-on",
        point: point.toString(),
      });
    }
  }

  /**
   * Convert the given coordinates on the page to percentages relative to the current
   * element.
   * @param {Number} pageX the x coordinate on the page
   * @param {Number} pageY the y coordinate on the page
   * @returns {Object} object of form {percentX, percentY}, which are the x/y coords
   *          in percentages relative to the element.
   */

  convertPageCoordsToPercent(pageX, pageY) {
    // If the current node is in an iframe, we get dimensions relative to the frame.
    const dims = this.frameDimensions;
    const { top, left, width, height } = dims;
    pageX -= left;
    pageY -= top;
    const percentX = (pageX * 100) / width;
    const percentY = (pageY * 100) / height;
    return { percentX, percentY };
  }

  /**
   * Convert the given x/y coordinates, in percentages relative to the current element,
   * to pixel coordinates relative to the page
   * @param {Number} x the x coordinate
   * @param {Number} y the y coordinate
   * @returns {Object} object of form {x, y}, which are the x/y coords in pixels
   *          relative to the page
   *
   * @memberof ShapesHighlighter
   */

  convertPercentToPageCoords(x, y) {
    const dims = this.frameDimensions;
    const { top, left, width, height } = dims;
    x = (x * width) / 100;
    y = (y * height) / 100;
    x += left;
    y += top;
    return { x, y };
  }

  /**
   * Get which transformation should be applied based on the mouse position.
   * @param {Number} pageX the x coordinate of the mouse.
   * @param {Number} pageY the y coordinate of the mouse.
   * @returns {String} a string describing the transformation that should be applied
   *          to the shape.
   */

  getTransformPointAt(pageX, pageY) {
    const { nw, ne, sw, se, n, w, s, e, rotatePoint, center } =
      this.transformedBoundingBox;
    const { width, height } = this.currentDimensions;
    const zoom = getCurrentZoom(this.win);
    const clickRadiusX = ((BASE_MARKER_SIZE / zoom) * 100) / width;
    const clickRadiusY = ((BASE_MARKER_SIZE / zoom) * 100) / height;

    const points = [
      { pointName: "translate", x: center[0], y: center[1] },
      { pointName: "scale-se", x: se[0], y: se[1] },
      { pointName: "scale-ne", x: ne[0], y: ne[1] },
      { pointName: "scale-sw", x: sw[0], y: sw[1] },
      { pointName: "scale-nw", x: nw[0], y: nw[1] },
    ];

    if (this.shapeType === "polygon" || this.shapeType === "ellipse") {
      points.push(
        { pointName: "scale-n", x: n[0], y: n[1] },
        { pointName: "scale-s", x: s[0], y: s[1] },
        { pointName: "scale-e", x: e[0], y: e[1] },
        { pointName: "scale-w", x: w[0], y: w[1] }
      );
    }

    if (this.shapeType === "polygon") {
      const x = rotatePoint[0];
      const y = rotatePoint[1];
      if (
        pageX >= x - clickRadiusX &&
        pageX <= x + clickRadiusX &&
        pageY >= y - clickRadiusY &&
        pageY <= y + clickRadiusY
      ) {
        return "rotate";
      }
    }

    for (const { pointName, x, y } of points) {
      if (
        pageX >= x - clickRadiusX &&
        pageX <= x + clickRadiusX &&
        pageY >= y - clickRadiusY &&
        pageY <= y + clickRadiusY
      ) {
        return pointName;
      }
    }

    return "";
  }

  /**
   * Get the id of the point on the polygon highlighter at the given coordinate.
   * @param {Number} pageX the x coordinate on the page, in % relative to the element
   * @param {Number} pageY the y coordinate on the page, in % relative to the element
   * @returns {Number} the index of the point that was clicked on in this.coordinates,
   *          or -1 if none of the points were clicked on.
   */

  getPolygonPointAt(pageX, pageY) {
    const { coordinates } = this;
    const { width, height } = this.currentDimensions;
    const zoom = getCurrentZoom(this.win);
    const clickRadiusX = ((BASE_MARKER_SIZE / zoom) * 100) / width;
    const clickRadiusY = ((BASE_MARKER_SIZE / zoom) * 100) / height;

    for (const [index, coord] of coordinates.entries()) {
      const [x, y] = coord;
      if (
        pageX >= x - clickRadiusX &&
        pageX <= x + clickRadiusX &&
        pageY >= y - clickRadiusY &&
        pageY <= y + clickRadiusY
      ) {
        return index;
      }
    }

    return -1;
  }

  /**
   * Check if the mouse clicked on a line of the polygon, and if so, add a point near
   * the click.
   * @param {Number} pageX the x coordinate on the page, in % relative to the element
   * @param {Number} pageY the y coordinate on the page, in % relative to the element
   */

  getPolygonClickedLine(pageX, pageY) {
    const { coordinates } = this;
    const { width } = this.currentDimensions;
    const clickWidth = (LINE_CLICK_WIDTH * 100) / width;

    for (let i = 0; i < coordinates.length; i++) {
      const [x1, y1] = coordinates[i];
      const [x2, y2] =
        i === coordinates.length - 1 ? coordinates[0] : coordinates[i + 1];
      // Get the distance between clicked point and line drawn between points 1 and 2
      // to check if the click was on the line between those two points.
      const distance = distanceToLine(x1, y1, x2, y2, pageX, pageY);
      if (
        distance <= clickWidth &&
        Math.min(x1, x2) - clickWidth <= pageX &&
        pageX <= Math.max(x1, x2) + clickWidth &&
        Math.min(y1, y2) - clickWidth <= pageY &&
        pageY <= Math.max(y1, y2) + clickWidth
      ) {
        // Get the point on the line closest to the clicked point.
        const [newX, newY] = projection(x1, y1, x2, y2, pageX, pageY);
        // Default unit for new points is percentages
        this._addPolygonPoint(i, round(newX, "%"), round(newY, "%"));
        return;
      }
    }
  }

  /**
   * Check if the center point or radius of the circle highlighter is at given coords
   * @param {Number} pageX the x coordinate on the page, in % relative to the element
   * @param {Number} pageY the y coordinate on the page, in % relative to the element
   * @returns {String} "center" if the center point was clicked, "radius" if the radius
   *          was clicked, "" if neither was clicked.
   */

  getCirclePointAt(pageX, pageY) {
    const { cx, cy, rx, ry } = this.coordinates;
    const { width, height } = this.currentDimensions;
    const zoom = getCurrentZoom(this.win);
    const clickRadiusX = ((BASE_MARKER_SIZE / zoom) * 100) / width;
    const clickRadiusY = ((BASE_MARKER_SIZE / zoom) * 100) / height;

    if (clickedOnPoint(pageX, pageY, cx, cy, clickRadiusX, clickRadiusY)) {
      return "center";
    }

    const clickWidthX = (LINE_CLICK_WIDTH * 100) / width;
    const clickWidthY = (LINE_CLICK_WIDTH * 100) / height;
    if (
      clickedOnEllipseEdge(
        pageX,
        pageY,
--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=94 H=99 G=96

¤ Dauer der Verarbeitung: 0.10 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge