Repository: chanind/hanzi-writer
Pull request #62 (2018-02-19 23:58:07)

Clip path render + performance improvements
This PR switches rendering to use `clipPath` instead of `mask`. This should improve performance since `clipPath` is a simpler operation, and removes the need for a special IE-specific rendering option. This is made possible by adding rounded stroke caps to v2.0.0 of hanzi-writer-data, so there's no paper-thin gaps between strokes without adding stroke width.

This PR also adds some performance improvements by adding `display: none` to characters with 0 opacity, and canceling rendering early for characters with no changes in the current render cycle.


Patch:
diff --git a/demo/index.html b/demo/index.html
index 03aebbae..5be07513 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -28,7 +28,7 @@
 		</div>
 
 		<!-- <script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script> -->
-		<script type="application/javascript" src="../dist/hanzi-writer.js"></script>
+		<script type="application/javascript" src="../dist/hanzi-writer.min.js"></script>
 		<script type="application/javascript" src="test.js"></script>
 		
 	</body>
diff --git a/src/HanziWriter.js b/src/HanziWriter.js
index f52730bf..ce7e13ab 100644
--- a/src/HanziWriter.js
+++ b/src/HanziWriter.js
@@ -8,7 +8,7 @@ const svg = require('./svg');
 const defaultCharDataLoader = require('./defaultCharDataLoader');
 const LoadingManager = require('./LoadingManager');
 const characterActions = require('./characterActions');
-const { assign, isMSBrowser, callIfExists } = require('./utils');
+const { assign, callIfExists } = require('./utils');
 
 
 const defaultOptions = {
@@ -52,8 +52,6 @@ const defaultOptions = {
   drawingWidth: 4,
   strokeWidth: 2,
   outlineWidth: 2,
-  // MS browsers are terrible and can't handle masks using paths with stroke
-  usePolygonMasks: isMSBrowser(),
 };
 
 function HanziWriter(element, character, options = {}) {
diff --git a/src/RenderState.js b/src/RenderState.js
index 5f288fbb..f6009a40 100644
--- a/src/RenderState.js
+++ b/src/RenderState.js
@@ -5,11 +5,9 @@ function RenderState(character, options, onStateChange) {
   this._mutationChains = [];
   this.state = {
     options: {
-      usePolygonMasks: options.usePolygonMasks,
       drawingFadeDuration: options.drawingFadeDuration,
       drawingWidth: options.drawingWidth,
       drawingColor: options.drawingColor,
-      strokeWidth: options.strokeWidth,
     },
     character: {
       main: {
diff --git a/src/geometry.js b/src/geometry.js
index 8f1b9375..3be252a1 100644
--- a/src/geometry.js
+++ b/src/geometry.js
@@ -8,65 +8,6 @@ const extendPointOnLine = (p1, p2, distance) => {
   return new Point(p2.x + norm * vect.x, p2.y + norm * vect.y);
 };
 
-// return 2 points distance from targetPoint on line perpendicular to the line between
-// targetPoint and refPoint
-const getPerpendicularPointsAtDist = (targetPoint, refPoint, distance) => {
-  const vect = targetPoint.subtract(refPoint);
-  const norm = distance / vect.getMagnitude();
-  // simulate taking a cross-product with the vector (0, 0, 1) to get the new perpendicular vect
-  const perpVect = new Point(norm * vect.y, -1 * norm * vect.x);
-  return [
-    targetPoint.add(perpVect),
-    targetPoint.subtract(perpVect),
-  ];
-};
-
-// get the intersection point of 2 lines defined by 2 points each
-// from https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
-const getLinesIntersectPoint = (l1p1, l1p2, l2p1, l2p2) => {
-  const x1 = l1p1.x;
-  const x2 = l1p2.x;
-  const x3 = l2p1.x;
-  const x4 = l2p2.x;
-  const y1 = l1p1.y;
-  const y2 = l1p2.y;
-  const y3 = l2p1.y;
-  const y4 = l2p2.y;
-  const xNumerator = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4);
-  const yNumerator = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4);
-  const denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
-  return new Point(xNumerator / denominator, yNumerator / denominator);
-};
-
-const getLineSegmentsLength = (points) => {
-  let totalDist = 0;
-  for (let i = 1; i < points.length; i++) {
-    totalDist += Point.getDistance(points[i], points[i - 1]);
-  }
-  return totalDist;
-};
-
-const getLineSegmentsPortion = (points, portion) => {
-  if (points.length < 2 || portion >= 1) return points;
-  if (portion === 0) return [points[0]];
-  const totalDist = getLineSegmentsLength(points);
-  const portionedPoints = [points[0]];
-  const portionedDist = totalDist * portion;
-  let cumuativeDist = 0;
-  for (let i = 1; i < points.length; i++) {
-    const lastPoint = points[i - 1];
-    const segmentLength = Point.getDistance(points[i], lastPoint);
-    if (cumuativeDist + segmentLength >= portionedDist) {
-      const vect = points[i].subtract(lastPoint);
-      const norm = (portionedDist - cumuativeDist) / segmentLength;
-      portionedPoints.push(new Point(lastPoint.x + norm * vect.x, lastPoint.y + norm * vect.y));
-      return portionedPoints;
-    }
-    cumuativeDist += segmentLength;
-    portionedPoints.push(points[i]);
-  }
-  return portionedPoints;
-};
 
 // remove intermediate points that are on the same line as the points to either side
 const filterParallelPoints = (points) => {
@@ -86,71 +27,7 @@ const filterParallelPoints = (points) => {
   return filteredPoints;
 };
 
-// given the points of a polyline, return the points outlining a polygon that's that polyline stroked with thickness
-const linesToPolygon = (points, thickness) => {
-  if (points.length < 2) return points;
-  const dist = thickness / 2;
-  const topSegments = [];
-  const bottomSegments = [];
-  for (let i = 1; i < points.length; i++) {
-    const startPoints = getPerpendicularPointsAtDist(points[i - 1], points[i], dist);
-    const endPoints = getPerpendicularPointsAtDist(points[i], points[i - 1], dist);
-    topSegments.push({ start: startPoints[0], end: endPoints[1] });
-    bottomSegments.push({ start: startPoints[1], end: endPoints[0] });
-  }
-  const topPoints = [topSegments[0].start];
-  const bottomPoints = [bottomSegments[0].start];
-  for (let i = 1; i < topSegments.length; i++) {
-    const topIntersect = getLinesIntersectPoint(
-      topSegments[i - 1].start,
-      topSegments[i - 1].end,
-      topSegments[i].start,
-      topSegments[i].end,
-    );
-    const bottomIntersect = getLinesIntersectPoint(
-      bottomSegments[i - 1].start,
-      bottomSegments[i - 1].end,
-      bottomSegments[i].start,
-      bottomSegments[i].end,
-    );
-    topPoints.push(topIntersect);
-    bottomPoints.push(bottomIntersect);
-  }
-
-  let topEndPoint = topSegments[topSegments.length - 1].end;
-  let bottomEndPoint = bottomSegments[bottomSegments.length - 1].end;
-
-  const endOverlapIntersect = getLinesIntersectPoint(
-    topPoints[topPoints.length - 1],
-    bottomPoints[bottomPoints.length - 1],
-    topEndPoint,
-    bottomEndPoint,
-  );
-
-  // correct for case where there's a hard corner and we're overlapping an area we already drew
-  if (Point.getDistance(endOverlapIntersect, points[points.length - 1]) < dist) {
-    const topVect = topEndPoint.subtract(points[points.length - 1]);
-    const overlapVect = endOverlapIntersect.subtract(points[points.length - 1]);
-    // figure out if the top point is overlapping of the bottom point is overlapping by using dot-product
-    const isTopOverlapping = ((topVect.x * overlapVect.x + topVect.y * overlapVect.y) > 0);
-    if (isTopOverlapping) {
-      topEndPoint = endOverlapIntersect;
-    } else {
-      bottomEndPoint = endOverlapIntersect;
-    }
-  }
-
-  topPoints.push(topEndPoint);
-  bottomPoints.push(bottomEndPoint);
-  bottomPoints.reverse();
-  return topPoints.concat(bottomPoints);
-};
-
 module.exports = {
   extendPointOnLine,
   filterParallelPoints,
-  getLineSegmentsPortion,
-  getLinesIntersectPoint,
-  getPerpendicularPointsAtDist,
-  linesToPolygon,
 };
diff --git a/src/renderers/CharacterRenderer.js b/src/renderers/CharacterRenderer.js
index cecd1799..7d63ea1f 100644
--- a/src/renderers/CharacterRenderer.js
+++ b/src/renderers/CharacterRenderer.js
@@ -1,56 +1,40 @@
-const Renderer = require('./Renderer');
 const StrokeRenderer = require('./StrokeRenderer');
-const { inherits } = require('../utils');
-const svg = require('../svg');
-
-
-const exractStrokeProps = (strokeNum, props) => {
-  if (!props.strokes) return props;
-  return {
-    usePolygonMasks: props.usePolygonMasks,
-    strokeColor: props.strokeColor,
-    radicalColor: props.radicalColor,
-    strokeWidth: props.strokeWidth,
-    opacity: props.strokes[strokeNum].opacity,
-    displayPortion: props.strokes[strokeNum].displayPortion,
-  };
-};
 
 
 function CharacterRenderer(character) {
-  CharacterRenderer.super_.call(this);
   this._oldProps = {};
   this.character = character;
-  this.strokeRenderers = this.character.strokes.map((stroke) => {
-    return this.registerChild(new StrokeRenderer(stroke));
-  });
+  this.strokeRenderers = this.character.strokes.map((stroke) => new StrokeRenderer(stroke));
 }
 
-inherits(CharacterRenderer, Renderer);
-
-CharacterRenderer.prototype.mount = function(canvas, props) {
+CharacterRenderer.prototype.mount = function(canvas) {
   const subCanvas = canvas.createSubCanvas();
   this._group = subCanvas.svg;
   this.strokeRenderers.forEach((strokeRenderer, i) => {
-    strokeRenderer.mount(subCanvas, exractStrokeProps(i, props));
+    strokeRenderer.mount(subCanvas);
   });
 };
 
 CharacterRenderer.prototype.render = function(props) {
+  if (props === this._oldProps) return;
   if (props.opacity !== this._oldProps.opacity) {
     this._group.style.opacity = props.opacity;
+    if (props.opacity === 0) {
+      this._group.style.display = 'none';
+    } else if (this._oldProps.opacity === 0) {
+      this._group.style.display = 'initial';
+    }
   }
   for (let i = 0; i < this.strokeRenderers.length; i++) {
-    this.strokeRenderers[i].render(exractStrokeProps(i, props));
+    this.strokeRenderers[i].render({
+      strokeColor: props.strokeColor,
+      radicalColor: props.radicalColor,
+      opacity: props.strokes[i].opacity,
+      displayPortion: props.strokes[i].displayPortion,
+    });
   }
   this._oldProps = props;
 };
 
 
-CharacterRenderer.prototype.destroy = function() {
-  CharacterRenderer.super_.prototype.destroy.call(this);
-  svg.removeElm(this._group);
-};
-
-
 module.exports = CharacterRenderer;
diff --git a/src/renderers/HanziWriterRenderer.js b/src/renderers/HanziWriterRenderer.js
index afc9741f..608057da 100644
--- a/src/renderers/HanziWriterRenderer.js
+++ b/src/renderers/HanziWriterRenderer.js
@@ -1,47 +1,34 @@
-const Renderer = require('./Renderer');
 const CharacterRenderer = require('./CharacterRenderer');
 const UserStrokeRenderer = require('./UserStrokeRenderer');
-const {inherits, assign} = require('../utils');
+const {assign} = require('../utils');
 const svg = require('../svg');
 
-
-const extractCharProps = (props, charName) => {
-  if (!props.character) return props;
-  return assign({
-    usePolygonMasks: props.options.usePolygonMasks,
-  }, props.character[charName]);
-};
-
-
 function HanziWriterRenderer(character, positioner) {
-  HanziWriterRenderer.super_.call(this);
   this._character = character;
   this._positioner = positioner;
-  this._mainCharRenderer = this.registerChild(new CharacterRenderer(character));
-  this._outlineCharRenderer = this.registerChild(new CharacterRenderer(character));
-  this._highlightCharRenderer = this.registerChild(new CharacterRenderer(character));
+  this._mainCharRenderer = new CharacterRenderer(character);
+  this._outlineCharRenderer = new CharacterRenderer(character);
+  this._highlightCharRenderer = new CharacterRenderer(character);
   this._userStrokeRenderers = {};
 }
 
-inherits(HanziWriterRenderer, Renderer);
-
-HanziWriterRenderer.prototype.mount = function(canvas, props) {
+HanziWriterRenderer.prototype.mount = function(canvas) {
   const positionedCanvas = canvas.createSubCanvas();
   const group = positionedCanvas.svg;
   svg.attr(group, 'transform', `
     translate(${this._positioner.getXOffset()}, ${this._positioner.getHeight() - this._positioner.getYOffset()})
     scale(${this._positioner.getScale()}, ${-1 * this._positioner.getScale()})
   `);
-  this._outlineCharRenderer.mount(positionedCanvas, extractCharProps(props, 'outline'));
-  this._mainCharRenderer.mount(positionedCanvas, extractCharProps(props, 'main'));
-  this._highlightCharRenderer.mount(positionedCanvas, extractCharProps(props, 'highlight'));
+  this._outlineCharRenderer.mount(positionedCanvas);
+  this._mainCharRenderer.mount(positionedCanvas);
+  this._highlightCharRenderer.mount(positionedCanvas);
   this._positionedCanvas = positionedCanvas;
 };
 
 HanziWriterRenderer.prototype.render = function(props) {
-  this._outlineCharRenderer.render(extractCharProps(props, 'outline'));
-  this._mainCharRenderer.render(extractCharProps(props, 'main'));
-  this._highlightCharRenderer.render(extractCharProps(props, 'highlight'));
+  this._outlineCharRenderer.render(props.character.outline);
+  this._mainCharRenderer.render(props.character.main);
+  this._highlightCharRenderer.render(props.character.highlight);
 
   const userStrokes = props.userStrokes || {};
   Object.keys(this._userStrokeRenderers).forEach(userStrokeId => {
@@ -68,8 +55,8 @@ HanziWriterRenderer.prototype.render = function(props) {
 };
 
 HanziWriterRenderer.prototype.destroy = function() {
-  HanziWriterRenderer.super_.prototype.destroy.call(this);
   svg.removeElm(this._positionedCanvas.svg);
+  this._positionedCanvas.defs.innerHTML = '';
 };
 
 module.exports = HanziWriterRenderer;
diff --git a/src/renderers/Renderer.js b/src/renderers/Renderer.js
deleted file mode 100644
index 0c620043..00000000
--- a/src/renderers/Renderer.js
+++ /dev/null
@@ -1,25 +0,0 @@
-function Renderer() {
-  this._childRenderers = [];
-}
-
-// implement in children
-Renderer.prototype.mount = function(canvas, props) {
-  return this;
-};
-
-// implement in children
-Renderer.prototype.render = function(props) {
-  return this;
-};
-
-Renderer.prototype.registerChild = function(child) {
-  this._childRenderers.push(child);
-  return child;
-};
-
-// extend this in children with extra behavior
-Renderer.prototype.destroy = function() {
-  this._childRenderers.forEach(child => child.destroy());
-};
-
-module.exports = Renderer;
diff --git a/src/renderers/StrokeRenderer.js b/src/renderers/StrokeRenderer.js
index e8a023f5..45320ae3 100644
--- a/src/renderers/StrokeRenderer.js
+++ b/src/renderers/StrokeRenderer.js
@@ -1,11 +1,8 @@
-const Renderer = require('./Renderer');
-const {counter, inherits } = require('../utils');
+const {counter} = require('../utils');
 const svg = require('../svg');
 const {
   extendPointOnLine,
-  getLineSegmentsPortion,
   filterParallelPoints,
-  linesToPolygon,
 } = require('../geometry');
 
 const STROKE_WIDTH = 200;
@@ -21,117 +18,64 @@ const extendStart = (points, distance) => {
   return extendedPoints;
 };
 
-// take points on a path and move their end point backwards by distance
-const extendEnd = (points, distance) => {
-  if (points.length < 2) return points;
-  const p1 = points[points.length - 2];
-  const p2 = points[points.length - 1];
-  const newEnd = extendPointOnLine(p1, p2, distance);
-  const extendedPoints = points.slice(0, points.length - 1);
-  extendedPoints.push(newEnd);
-  return extendedPoints;
-};
-
 // this is a stroke composed of several stroke parts
 function StrokeRenderer(stroke) {
-  StrokeRenderer.super_.call(this);
   this._oldProps = {};
   this._stroke = stroke;
-  this._maskPathLength = stroke.getLength() + (STROKE_WIDTH / 2);
+  this._pathLength = stroke.getLength() + (STROKE_WIDTH / 2);
 }
-inherits(StrokeRenderer, Renderer);
 
-StrokeRenderer.prototype.mount = function(canvas, props) {
-  const { usePolygonMasks } = props;
-  this._path = svg.createElm('path');
-  const maskType = usePolygonMasks ? 'clipPath' : 'mask';
-  this._mask = svg.createElm(maskType);
-  this._maskPath = svg.createElm('path');
+StrokeRenderer.prototype.mount = function(canvas) {
+  this._animationPath = svg.createElm('path');
+  this._clip = svg.createElm('clipPath');
+  this._strokePath = svg.createElm('path');
   const maskId = `mask-${counter()}`;
-  svg.attr(this._mask, 'id', maskId);
-
-  svg.attr(this._path, 'd', this._stroke.path);
-  this._path.style.opacity = 0;
-  const maskAttr = usePolygonMasks ? 'clip-path' : 'mask';
-  svg.attr(this._path, maskAttr, `url(#${maskId})`);
-
-  this.extendedMaskPoints = extendStart(filterParallelPoints(this._stroke.points), STROKE_WIDTH / 2);
-  if (usePolygonMasks) {
-    this.extendedMaskPoints = extendEnd(this.extendedMaskPoints, STROKE_WIDTH / 2);
-    this._polyMaskTip = svg.createElm('circle');
-    // need to add this to the mask before the maskPath or else weird things happen. Not sure why
-    this._mask.appendChild(this._polyMaskTip);
-    svg.attr(this._polyMaskTip, 'r', STROKE_WIDTH / 2);
-    this._setPolyMaskPortion(1);
-  } else {
-    svg.attr(this._maskPath, 'd', svg.getPathString(this.extendedMaskPoints));
-    svg.attrs(this._maskPath, {
-      stroke: '#FFFFFF',
-      'stroke-width': STROKE_WIDTH,
-      fill: 'none',
-      'stroke-linecap': 'round',
-      'stroke-linejoin': 'miter',
-      'stroke-dasharray': `${this._maskPathLength},${this._maskPathLength}`,
-    });
-  }
-
-  this._mask.appendChild(this._maskPath);
-  canvas.defs.appendChild(this._mask);
-  canvas.svg.appendChild(this._path);
+  svg.attr(this._clip, 'id', maskId);
+
+  svg.attr(this._strokePath, 'd', this._stroke.path);
+  this._animationPath.style.opacity = 0;
+  svg.attr(this._animationPath, 'clip-path', `url(#${maskId})`);
+
+  const extendedMaskPoints = extendStart(filterParallelPoints(this._stroke.points), STROKE_WIDTH / 2);
+  svg.attr(this._animationPath, 'd', svg.getPathString(extendedMaskPoints));
+  svg.attrs(this._animationPath, {
+    stroke: '#FFFFFF',
+    'stroke-width': STROKE_WIDTH,
+    fill: 'none',
+    'stroke-linecap': 'round',
+    'stroke-linejoin': 'miter',
+    'stroke-dasharray': `${this._pathLength},${this._pathLength}`,
+  });
+
+  this._clip.appendChild(this._strokePath);
+  canvas.defs.appendChild(this._clip);
+  canvas.svg.appendChild(this._animationPath);
   return this;
 };
 
 StrokeRenderer.prototype.render = function(props) {
-  const { usePolygonMasks } = props;
+  if (props === this._oldProps) return;
   if (props.displayPortion !== this._oldProps.displayPortion) {
-    if (usePolygonMasks) {
-      this._setPolyMaskPortion(props.displayPortion);
-    } else {
-      this._maskPath.style.strokeDashoffset = this._getStrokeDashoffset(props.displayPortion);
-    }
+    this._animationPath.style.strokeDashoffset = this._getStrokeDashoffset(props.displayPortion);
   }
 
   const color = this._getColor(props);
   if (color !== this._getColor(this._oldProps)) {
-    svg.attrs(this._path, {
-      fill: color,
-      stroke: color,
-    });
-  }
-
-  if (props.strokeWidth !== this._oldProps.strokeWidth) {
-    svg.attrs(this._path, { strokeWidth: props.strokeWidth });
+    svg.attrs(this._animationPath, { stroke: color });
   }
 
   if (props.opacity !== this._oldProps.opacity) {
-    this._path.style.opacity = props.opacity;
+    this._animationPath.style.opacity = props.opacity;
   }
   this._oldProps = props;
 };
 
-StrokeRenderer.prototype._setPolyMaskPortion = function(portion) {
-  const strokePointsPortion = getLineSegmentsPortion(this.extendedMaskPoints, portion);
-  const pathString = svg.getPathString(linesToPolygon(strokePointsPortion, STROKE_WIDTH), true);
-  const endPoint = strokePointsPortion[strokePointsPortion.length - 1];
-  svg.attr(this._maskPath, 'd', pathString);
-  svg.attr(this._polyMaskTip, 'cx', endPoint.x);
-  svg.attr(this._polyMaskTip, 'cy', endPoint.y);
-};
-
 StrokeRenderer.prototype._getStrokeDashoffset = function(displayPortion) {
-  return this._maskPathLength * 0.999 * (1 - displayPortion);
+  return this._pathLength * 0.999 * (1 - displayPortion);
 };
 
 StrokeRenderer.prototype._getColor = function({ strokeColor, radicalColor }) {
   return radicalColor && this._stroke.isInRadical ? radicalColor : strokeColor;
 };
 
-StrokeRenderer.prototype.destroy = function() {
-  StrokeRenderer.super_.prototype.destroy.call(this);
-  svg.removeElm(this._maskPath);
-  svg.removeElm(this._polyMaskTip);
-  svg.removeElm(this._path);
-  svg.removeElm(this._mask);
-};
-
 module.exports = StrokeRenderer;
diff --git a/src/renderers/UserStrokeRenderer.js b/src/renderers/UserStrokeRenderer.js
index 58f0fc31..ae12bf03 100644
--- a/src/renderers/UserStrokeRenderer.js
+++ b/src/renderers/UserStrokeRenderer.js
@@ -1,20 +1,17 @@
-const Renderer = require('./Renderer');
-const { inherits } = require('../utils');
 const svg = require('../svg');
 
 
 function UserStrokeRenderer() {
-  UserStrokeRenderer.super_.call(this);
   this._oldProps = {};
 }
-inherits(UserStrokeRenderer, Renderer);
 
-UserStrokeRenderer.prototype.mount = function(canvas, props) {
+UserStrokeRenderer.prototype.mount = function(canvas) {
   this._path = svg.createElm('path');
   canvas.svg.appendChild(this._path);
 };
 
 UserStrokeRenderer.prototype.render = function(props) {
+  if (props === this._oldProps) return;
   if (props.strokeColor !== this._oldProps.strokeColor || props.strokeWidth !== this._oldProps.strokeWidth) {
     svg.attrs(this._path, {
       fill: 'none',
@@ -34,8 +31,7 @@ UserStrokeRenderer.prototype.render = function(props) {
 };
 
 UserStrokeRenderer.prototype.destroy = function() {
-  UserStrokeRenderer.super_.prototype.destroy.call(this);
-  this._path.parentNode.removeChild(this._path);
+  svg.removeElm(this._path);
 };
 
 module.exports = UserStrokeRenderer;
diff --git a/src/utils.js b/src/utils.js
index 190089c2..36e0a183 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -54,19 +54,6 @@ function inflate(scope, obj) {
   return final;
 }
 
-// utils for classes without es6, sigh...
-// from: https://github.com/nodejs/node-v0.x-archive/blob/546ae2ee/lib/util.js#L552-L575
-function inherits(ctor, superCtor) {
-  ctor.super_ = superCtor; // eslint-disable-line no-param-reassign
-  ctor.prototype = Object.create(superCtor.prototype, { // eslint-disable-line no-param-reassign
-    constructor: {
-      value: ctor,
-      enumerable: false,
-      writable: true,
-      configurable: true,
-    },
-  });
-}
 
 function arrayMax(numArray) {
   return Math.max.apply(null, numArray);
@@ -104,13 +91,6 @@ function timeout(duration = 0) {
   });
 }
 
-function isMSBrowser() {
-  return global.navigator && global.navigator.userAgent && (
-    global.navigator.userAgent.indexOf('Edge') >= 0
-    || global.navigator.userAgent.indexOf('MSIE') >= 0
-  );
-}
-
 // return a new array-like object with int keys where each key is item
 // ex: objRepeat({x: 8}, 3) === {0: {x: 8}, 1: {x: 8}, 2: {x: 8}}
 const objRepeat = (item, times) => {
@@ -133,8 +113,6 @@ module.exports = {
   emptyFunc,
   getExtremes,
   inflate,
-  inherits,
-  isMSBrowser,
   objRepeat,
   performanceNow,
   requestAnimationFrame,


Test patch:
diff --git a/src/__tests__/RenderState-test.js b/src/__tests__/RenderState-test.js
index fceaf8cf..08ecc3db 100644
--- a/src/__tests__/RenderState-test.js
+++ b/src/__tests__/RenderState-test.js
@@ -13,11 +13,9 @@ const opts = {
   drawingColor: '#333',
   drawingFadeDuration: 300,
   drawingWidth: 4,
-  strokeWidth: 2,
   outlineWidth: 2,
   showCharacter: true,
   showOutline: false,
-  usePolygonMasks: false,
 };
 
 
@@ -27,11 +25,9 @@ describe('RenderState', () => {
 
     expect(renderState.state).toEqual({
       options: {
-        usePolygonMasks: false,
         drawingFadeDuration: 300,
         drawingWidth: 4,
         drawingColor: '#333',
-        strokeWidth: 2,
       },
       character: {
         main: {
diff --git a/src/__tests__/geometry-test.js b/src/__tests__/geometry-test.js
index 634cbcb3..2150c8a4 100644
--- a/src/__tests__/geometry-test.js
+++ b/src/__tests__/geometry-test.js
@@ -28,103 +28,6 @@ describe('geometry', () => {
     });
   });
 
-  describe('getPerpendicularPointsAtDist', () => {
-    it('returns points at distance from target perpendicular to the line between target and ref', () => {
-      const target = new Point(1, 1);
-      const ref = new Point(0, 0);
-      expect(geometry.getPerpendicularPointsAtDist(target, ref, 2 * Math.sqrt(2))).toEqual([
-        new Point(3, -1),
-        new Point(-1, 3),
-      ]);
-    });
-  });
-
-  describe('getLinesIntersectPoint', () => {
-    it('returns the point where lines defined by 2 points each intersect', () => {
-      const line1point1 = new Point(0, 0);
-      const line1point2 = new Point(1, 1);
-      const line2point1 = new Point(4, 1);
-      const line2point2 = new Point(5, 0);
-      const intersection = geometry.getLinesIntersectPoint(
-        line1point1,
-        line1point2,
-        line2point1,
-        line2point2,
-      );
-      expect(intersection).toEqual(new Point(2.5, 2.5));
-    });
-
-    it('returns the same result even if the order of points and lines are flipped', () => {
-      const line2point2 = new Point(0, 0);
-      const line2point1 = new Point(1, 1);
-      const line1point2 = new Point(4, 1);
-      const line1point1 = new Point(5, 0);
-      const intersection = geometry.getLinesIntersectPoint(
-        line1point1,
-        line1point2,
-        line2point1,
-        line2point2,
-      );
-      expect(intersection).toEqual(new Point(2.5, 2.5));
-    });
-
-    it('works with vertical and horizontal lines', () => {
-      const line1point1 = new Point(0, 4);
-      const line1point2 = new Point(1, 4);
-      const line2point1 = new Point(3, 0);
-      const line2point2 = new Point(3, -1);
-      const intersection = geometry.getLinesIntersectPoint(
-        line1point1,
-        line1point2,
-        line2point1,
-        line2point2,
-      );
-      expect(intersection).toEqual(new Point(3, 4));
-    });
-  });
-
-  describe('linesToPolygon', () => {
-    it("returns the outline of the polygon that's the stroked path of the points passed in", () => {
-      const points = [
-        new Point(0, 0),
-        new Point(5, 0),
-        new Point(5, 2),
-      ];
-      expect(geometry.linesToPolygon(points, 2)).toEqual([
-        new Point(0, 1),
-        new Point(4, 1),
-        new Point(4, 2),
-        new Point(6, 2),
-        new Point(6, -1),
-        new Point(0, -1),
-      ]);
-    });
-  });
-
-  describe('getLineSegmentsPortion', () => {
-    it('retuns a new series of points that is portion of the passed in segment', () => {
-      // total length 10
-      const points = [
-        new Point(0, 0),
-        new Point(5, 0),
-        new Point(9, 3),
-      ];
-      expect(geometry.getLineSegmentsPortion(points, 0)).toEqual([
-        new Point(0, 0),
-      ]);
-      expect(geometry.getLineSegmentsPortion(points, 0.2)).toEqual([
-        new Point(0, 0),
-        new Point(2, 0),
-      ]);
-      expect(geometry.getLineSegmentsPortion(points, 0.75)).toEqual([
-        new Point(0, 0),
-        new Point(5, 0),
-        new Point(7, 1.5),
-      ]);
-      expect(geometry.getLineSegmentsPortion(points, 1)).toEqual(points);
-    });
-  });
-
   describe('filterParallelPoints', () => {
     it('removes internal points that are on the line connecting the points on either side', () => {
       const points = [
diff --git a/src/renderers/__tests__/CharacterRenderer-test.js b/src/renderers/__tests__/CharacterRenderer-test.js
index c9c90285..22ec587f 100644
--- a/src/renderers/__tests__/CharacterRenderer-test.js
+++ b/src/renderers/__tests__/CharacterRenderer-test.js
@@ -18,7 +18,6 @@ describe('CharacterRenderer', () => {
 
   it('renders a g element and puts strokes inside', () => {
     const props = {
-      usePolygonMasks: false,
       strokeColor: '#123',
       radicalColor: null,
       strokeWidth: 2,
@@ -46,13 +45,12 @@ describe('CharacterRenderer', () => {
     expect(subCanvas.childNodes.length).toBe(2);
     subCanvas.childNodes.forEach(node => {
       expect(node.nodeName).toBe('path');
-      expect(node.getAttribute('fill')).toBe('#123');
+      expect(node.getAttribute('stroke')).toBe('#123');
     });
   });
 
   it('updates opacity and updates passed-through props', () => {
     const props1 = {
-      usePolygonMasks: false,
       strokeColor: '#123',
       radicalColor: null,
       strokeWidth: 2,
@@ -87,7 +85,7 @@ describe('CharacterRenderer', () => {
     expect(subCanvas.childNodes.length).toBe(2);
     subCanvas.childNodes.forEach(node => {
       expect(node.nodeName).toBe('path');
-      expect(node.getAttribute('fill')).toBe('#456');
+      expect(node.getAttribute('stroke')).toBe('#456');
     });
   });
 
diff --git a/src/renderers/__tests__/StrokeRenderer-test.js b/src/renderers/__tests__/StrokeRenderer-test.js
index 0e5ac97e..2731a662 100644
--- a/src/renderers/__tests__/StrokeRenderer-test.js
+++ b/src/renderers/__tests__/StrokeRenderer-test.js
@@ -15,9 +15,8 @@ describe('StrokeRenderer', () => {
     canvas = svg.Canvas.init('target');
   });
 
-  it('renders a path and mask', () => {
+  it('renders a path and clipPath', () => {
     const props = {
-      usePolygonMasks: false,
       strokeColor: '#123',
       radicalColor: null,
       strokeWidth: 2,
@@ -30,45 +29,16 @@ describe('StrokeRenderer', () => {
     renderer.render(props);
 
     expect(canvas.defs.childNodes.length).toBe(1);
-    expect(canvas.defs.childNodes[0].nodeName).toBe('mask');
+    expect(canvas.defs.childNodes[0].nodeName).toBe('clipPath');
     const maskPath = canvas.defs.childNodes[0].childNodes[0];
     expect(maskPath.nodeName).toBe('path');
     const maskId = canvas.defs.childNodes[0].getAttribute('id');
     expect(canvas.svg.childNodes.length).toBe(2); // defs and path
     expect(canvas.svg.childNodes[1].nodeName).toBe('path');
-    expect(canvas.svg.childNodes[1].getAttribute('fill')).toBe('#123');
-    expect(canvas.svg.childNodes[1].getAttribute('mask')).toBe(`url(#${maskId})`);
-
-    expect(maskPath).toMatchSnapshot();
-  });
-
-  it('can render using clip-mask', () => {
-    const props = {
-      usePolygonMasks: true,
-      strokeColor: '#123',
-      radicalColor: null,
-      strokeWidth: 2,
-      opacity: 0.7,
-      displayPortion: 0.4,
-    };
-
-    const renderer = new StrokeRenderer(char.strokes[0]);
-    renderer.mount(canvas, props);
-    renderer.render(props);
-
-    expect(canvas.defs.childNodes.length).toBe(1);
-    const clipPath = canvas.defs.childNodes[0];
-    expect(clipPath.nodeName).toBe('clipPath');
-    expect(clipPath.childNodes.length).toBe(2);
-    expect(clipPath.childNodes[0].nodeName).toBe('circle');
-    expect(clipPath.childNodes[1].nodeName).toBe('path');
-    const maskId = canvas.defs.childNodes[0].getAttribute('id');
-    expect(canvas.svg.childNodes.length).toBe(2); // defs and path
-    expect(canvas.svg.childNodes[1].nodeName).toBe('path');
-    expect(canvas.svg.childNodes[1].getAttribute('fill')).toBe('#123');
+    expect(canvas.svg.childNodes[1].getAttribute('stroke')).toBe('#123');
     expect(canvas.svg.childNodes[1].getAttribute('clip-path')).toBe(`url(#${maskId})`);
 
-    expect(clipPath).toMatchSnapshot();
+    expect(maskPath).toMatchSnapshot();
   });
 
 });
diff --git a/src/renderers/__tests__/__snapshots__/StrokeRenderer-test.js.snap b/src/renderers/__tests__/__snapshots__/StrokeRenderer-test.js.snap
index e60f4e9c..60b9658d 100644
--- a/src/renderers/__tests__/__snapshots__/StrokeRenderer-test.js.snap
+++ b/src/renderers/__tests__/__snapshots__/StrokeRenderer-test.js.snap
@@ -1,29 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`StrokeRenderer can render using clip-mask 1`] = `
-<clippath
-  id="mask-2"
->
-  <circle
-    cx="420.3610543060645"
-    cy="402.2623261897416"
-    r="100"
-  />
-  <path
-    d="M 53 517 L 200.69230769230745 473.92307692307696 L 406.46418793868423 501.48181088464526 L 412.5798403425982 501.95913009666106 L 428.1422682695308 302.56552228282214 L 427.5358120612777 302.5181891153496 L 185.30769230769243 270.07692307692304 L -3 325Z"
-  />
-</clippath>
-`;
-
-exports[`StrokeRenderer renders a path and mask 1`] = `
+exports[`StrokeRenderer renders a path and clipPath 1`] = `
 <path
-  d="M 25 421 L 193 372 L 417 402 L 827 434 L 920 401"
-  fill="none"
-  stroke="#FFFFFF"
-  stroke-dasharray="910.9281897070704,910.9281897070704"
-  stroke-linecap="round"
-  stroke-linejoin="miter"
-  stroke-width="200"
-  style="stroke-dashoffset: 546.010356910418;"
+  d="M 518 382 Q 572 385 623 389 Q 758 399 900 383 Q 928 379 935 390 Q 944 405 930 419 Q 896 452 845 475 Q 829 482 798 473 Q 723 460 480 434 Q 180 409 137 408 Q 130 408 124 408 Q 108 408 106 395 Q 105 380 127 363 Q 146 348 183 334 Q 195 330 216 338 Q 232 344 306 354 Q 400 373 518 382 Z"
 />
 `;