Repository: chanind/hanzi-writer (ts)

Issue:
Acute angle stroke in characters like 去 and 么 is too strict; curved stroke in 都 is too lenient.
There is a particular stroke that my users have complained about being too difficult to write. It is the acute angle found in characters like 么 去 公 能 台 and so on.

Here is a screenshot of a rejected stroke from the [demo page](https://chanind.github.io/hanzi-writer/demo.html):

![strictangle](https://user-images.githubusercontent.com/920512/49417011-b962e080-f741-11e8-8cd7-a4581ecdc2ce.png)

The line is almost entirely inside the outline, so it seems excessively picky to reject it while some other strokes are very lenient, such as the right component of 都. Here is a screenshot of a stroke that was not rejected:

![lenientstroke](https://user-images.githubusercontent.com/920512/49417235-9553cf00-f742-11e8-9914-4e8e767cbf37.png)


Pull request:
Improving shape matching
This PR improves curve shape matching by improving how curves are normalized during the frechet distance calculation. Previously, curves were scaled and positioned only based on their start and end points rather than based on the full shape of the curve. As a result, if the start and end points were off but the rest of the curve was drawn correctly, it would be scaled and positioned incorrectly and the frechet distance would be much larger than it should be have.

Fixes #94

Patch:
diff --git a/src/geometry.js b/src/geometry.js
index a814b952..2962fd86 100644
--- a/src/geometry.js
+++ b/src/geometry.js
@@ -93,12 +93,41 @@ const subdivideCurve = (curve, maxLen = 0.05) => {
   return newCurve;
 };
 
+// redraw the curve using numPoints equally spaced out along the length of the curve
+const outlineCurve = (curve, numPoints = 30) => {
+  const curveLen = length(curve);
+  const segmentLen = curveLen / (numPoints - 1);
+  const outlinePoints = [curve[0]];
+  const endPoint = arrLast(curve);
+  const remainingCurvePoints = curve.slice(1);
+  for (let i = 0; i < numPoints - 2; i++) {
+    let lastPoint = arrLast(outlinePoints);
+    let remainingDist = segmentLen;
+    let outlinePointFound = false;
+    while (!outlinePointFound) {
+      const nextPointDist = distance(lastPoint, remainingCurvePoints[0]);
+      if (nextPointDist < remainingDist) {
+        remainingDist -= nextPointDist;
+        lastPoint = remainingCurvePoints.shift();
+      } else {
+        const nextPoint = extendPointOnLine(lastPoint, remainingCurvePoints[0], remainingDist - nextPointDist);
+        outlinePoints.push(nextPoint);
+        outlinePointFound = true;
+      }
+    }
+
+  }
+  outlinePoints.push(endPoint);
+  return outlinePoints;
+};
+
 // translate and scale from https://en.wikipedia.org/wiki/Procrustes_analysis
 const normalizeCurve = (curve) => {
-  const meanX = average([curve[0].x, arrLast(curve).x]);
-  const meanY = average([curve[0].y, arrLast(curve).y]);
+  const outlinedCurve = outlineCurve(curve);
+  const meanX = average(outlinedCurve.map(point => point.x));
+  const meanY = average(outlinedCurve.map(point => point.y));
   const mean = { x: meanX, y: meanY };
-  const translatedCurve = curve.map(point => subtract(point, mean));
+  const translatedCurve = outlinedCurve.map(point => subtract(point, mean));
   const scale = Math.sqrt(average([
     Math.pow(translatedCurve[0].x, 2) + Math.pow(translatedCurve[0].y, 2),
     Math.pow(arrLast(translatedCurve).x, 2) + Math.pow(arrLast(translatedCurve).y, 2),
@@ -142,6 +171,7 @@ module.exports = {
   rotate,
   subtract,
   cosineSimilarity,
+  outlineCurve,
   extendPointOnLine,
   filterParallelPoints,
   subdivideCurve,
diff --git a/src/strokeMatches.js b/src/strokeMatches.js
index 10393acd..a1677c09 100644
--- a/src/strokeMatches.js
+++ b/src/strokeMatches.js
@@ -13,7 +13,7 @@ const {
 const AVG_DIST_THRESHOLD = 350; // bigger = more lenient
 const COSINE_SIMILARITY_THRESHOLD = 0; // -1 to 1, smaller = more lenient
 const START_AND_END_DIST_THRESHOLD = 250; // bigger = more lenient
-const FRECHET_THRESHOLD = 0.75; // bigger = more lenient
+const FRECHET_THRESHOLD = 0.40; // bigger = more lenient
 const MIN_LEN_THRESHOLD = 0.35; // smalled = more lenient
 
 const startAndEndMatches = function(points, closestStroke, leniency) {


Test patch:
diff --git a/demo/test.js b/demo/test.js
index 747aae04..44ad977b 100644
--- a/demo/test.js
+++ b/demo/test.js
@@ -2,14 +2,22 @@ var writer;
 var isCharVisible;
 var isOutlineVisible;
 
+function printStrokePoints(data) {
+	var pointStrs = data.drawnPath.points.map(point => `{x: ${point.x}, y: ${point.y}}`);
+	console.log(`[${pointStrs.join(', ')}]`);
+}
+
 function updateCharacter() {
 	document.querySelector('#target').innerHTML = '';
 
-	var character = document.querySelector('.js-char').value
-	writer = new HanziWriter('target', character, {
+	var character = document.querySelector('.js-char').value;
+	window.location.hash = character;
+	writer = HanziWriter.create('target', character, {
 		width: 400,
 		height: 400,
 		radicalColor: '#166E16',
+		onCorrectStroke: printStrokePoints,
+		onMistake: printStrokePoints,
 	});
 	isCharVisible = true;
 	isOutlineVisible = true;
@@ -17,6 +25,11 @@ function updateCharacter() {
 }
 
 window.onload = function() {
+	var char = decodeURIComponent(window.location.hash.slice(1));
+	if (char) {
+		document.querySelector('.js-char').value = char;
+	}
+
 	updateCharacter();
 
 	document.querySelector('.js-char-form').addEventListener('submit', function(evt) {
@@ -40,4 +53,4 @@ window.onload = function() {
 			showOutline: true
 		});
 	});
-}
\ No newline at end of file
+}
diff --git a/src/__tests__/geometry-test.js b/src/__tests__/geometry-test.js
index b730ab0d..fa3bce75 100644
--- a/src/__tests__/geometry-test.js
+++ b/src/__tests__/geometry-test.js
@@ -83,6 +83,21 @@ describe('geometry', () => {
     });
   });
 
+  describe('outlineCurve', () => {
+    it('divides a curve into equally spaced segments', () => {
+      const curve1 = [{x: 0, y: 0}, {x: 4, y: 6}];
+      expect(geometry.outlineCurve(curve1, 3)).toEqual([{x: 0, y: 0}, {x: 2, y: 3}, {x: 4, y: 6}]);
+
+      const curve2 = [{x: 0, y: 0}, {x: 9, y: 12}, {x: 0, y: 24}];
+      expect(geometry.outlineCurve(curve2, 4)).toEqual([
+        {x: 0, y: 0},
+        {x: 6, y: 8},
+        {x: 6, y: 16},
+        {x: 0, y: 24},
+      ]);
+    });
+  });
+
   describe('filterParallelPoints', () => {
     it('removes internal points that are on the line connecting the points on either side', () => {
       const points = [
diff --git a/src/__tests__/strokeMatches-test.js b/src/__tests__/strokeMatches-test.js
index 1bccd735..d3dbf2f6 100644
--- a/src/__tests__/strokeMatches-test.js
+++ b/src/__tests__/strokeMatches-test.js
@@ -162,7 +162,7 @@ describe('strokeMatches', () => {
   });
 
   it('matches using real data 15', () => {
-    const points = [{x: 368.2, y: 684.4}, {x: 371.6, y: 684.4}, {x: 378.4, y: 684.4}, {x: 395.5, y: 684.4}, {x: 412.5, y: 684.4}, {x: 446.7, y: 684.4}, {x: 477.4, y: 684.4}, {x: 518.3, y: 684.4}, {x: 562.7, y: 684.4}, {x: 593.4, y: 684.4}, {x: 613.9, y: 687.8}, {x: 627.6, y: 698.1}, {x: 631, y: 701.5}, {x: 631, y: 698.1}, {x: 631, y: 694.7}, {x: 631, y: 687.8}, {x: 620.7, y: 670.8}, {x: 610.5, y: 653.7}, {x: 603.7, y: 636.6}, {x: 586.6, y: 605.9}, {x: 573, y: 582}, {x: 569.5, y: 558.1}, {x: 569.5, y: 534.2}, {x: 569.5, y: 517.2}, {x: 569.5, y: 493.3}, {x: 569.5, y: 486.5}, {x: 573, y: 472.8}, {x: 579.8, y: 462.6}, {x: 590, y: 448.9}, {x: 603.7, y: 442.1}, {x: 620.7, y: 431.8}, {x: 637.8, y: 421.6}, {x: 648.1, y: 411.4}, {x: 651.5, y: 397.7}, {x: 654.9, y: 384.1}, {x: 654.9, y: 367}, {x: 654.9, y: 353.3}, {x: 654.9, y: 346.5}, {x: 654.9, y: 336.3}, {x: 651.5, y: 329.4}, {x: 641.2, y: 329.4}, {x: 610.5, y: 336.3}];
+    const points = [{x: 455.1, y: 681}, {x: 455.1, y: 683.8}, {x: 458, y: 683.8}, {x: 463.6, y: 683.8}, {x: 483.6, y: 683.8}, {x: 503.5, y: 683.8}, {x: 531.9, y: 686.7}, {x: 563.2, y: 686.7}, {x: 594.5, y: 689.5}, {x: 620.1, y: 692.4}, {x: 637.2, y: 692.4}, {x: 648.5, y: 692.4}, {x: 657.1, y: 692.4}, {x: 662.8, y: 692.4}, {x: 662.8, y: 692.4}, {x: 665.6, y: 692.4}, {x: 665.6, y: 689.5}, {x: 665.6, y: 683.8}, {x: 665.6, y: 678.1}, {x: 665.6, y: 669.6}, {x: 665.6, y: 661.1}, {x: 662.8, y: 644}, {x: 659.9, y: 626.9}, {x: 648.5, y: 590}, {x: 640, y: 570}, {x: 634.3, y: 550.1}, {x: 628.6, y: 535.9}, {x: 622.9, y: 527.4}, {x: 620.1, y: 521.7}, {x: 617.2, y: 516}, {x: 617.2, y: 513.2}, {x: 617.2, y: 510.3}, {x: 617.2, y: 510.3}, {x: 622.9, y: 507.5}, {x: 628.6, y: 501.8}, {x: 637.2, y: 498.9}, {x: 642.8, y: 493.2}, {x: 645.7, y: 487.6}, {x: 648.5, y: 484.7}, {x: 651.4, y: 476.2}, {x: 651.4, y: 464.8}, {x: 654.2, y: 450.6}, {x: 654.2, y: 430.7}, {x: 654.2, y: 410.8}, {x: 648.5, y: 393.7}, {x: 640, y: 379.5}, {x: 625.8, y: 365.2}, {x: 597.3, y: 342.5}, {x: 571.7, y: 328.3}, {x: 551.8, y: 322.6}, {x: 531.9, y: 316.9}, {x: 526.2, y: 314}];
     assertMatches('阝', 0, points);
     assertNotMatches('阝', 1, points);
   });
@@ -215,4 +215,36 @@ describe('strokeMatches', () => {
     assertMatches('得', 5, points, { leniency: 2 });
     assertMatches('得', 6, points);
   });
+
+  it('matches using real data 23', () => {
+    const points = [{x: 500.6, y: 624.1}, {x: 500.6, y: 624.1}, {x: 500.6, y: 612.7}, {x: 492.1, y: 598.5}, {x: 480.7, y: 578.6}, {x: 466.5, y: 555.8}, {x: 452.3, y: 530.2}, {x: 429.5, y: 493.2}, {x: 415.3, y: 470.5}, {x: 403.9, y: 444.9}, {x: 389.7, y: 425}, {x: 378.3, y: 405.1}, {x: 369.8, y: 390.8}, {x: 361.2, y: 373.8}, {x: 349.9, y: 359.6}, {x: 341.3, y: 342.5}, {x: 332.8, y: 325.4}, {x: 324.3, y: 314}, {x: 318.6, y: 302.7}, {x: 310, y: 288.4}, {x: 301.5, y: 277.1}, {x: 293, y: 265.7}, {x: 287.3, y: 251.5}, {x: 281.6, y: 240.1}, {x: 273.1, y: 231.6}, {x: 270.2, y: 223}, {x: 264.5, y: 214.5}, {x: 261.7, y: 208.8}, {x: 258.8, y: 203.1}, {x: 256, y: 197.4}, {x: 253.2, y: 194.6}, {x: 253.2, y: 191.7}, {x: 253.2, y: 191.7}, {x: 253.2, y: 191.7}, {x: 253.2, y: 191.7}, {x: 253.2, y: 191.7}, {x: 253.2, y: 191.7}, {x: 258.8, y: 194.6}, {x: 264.5, y: 194.6}, {x: 273.1, y: 194.6}, {x: 287.3, y: 194.6}, {x: 301.5, y: 194.6}, {x: 315.7, y: 197.4}, {x: 332.8, y: 197.4}, {x: 349.9, y: 197.4}, {x: 364.1, y: 200.3}, {x: 378.3, y: 200.3}, {x: 392.5, y: 203.1}, {x: 403.9, y: 206}, {x: 415.3, y: 208.8}, {x: 426.7, y: 211.6}, {x: 435.2, y: 211.6}, {x: 449.4, y: 214.5}, {x: 460.8, y: 217.3}, {x: 472.2, y: 217.3}, {x: 489.2, y: 223}, {x: 497.8, y: 225.9}, {x: 506.3, y: 228.7}, {x: 514.8, y: 228.7}, {x: 523.4, y: 231.6}, {x: 531.9, y: 234.4}, {x: 540.4, y: 237.2}, {x: 549, y: 240.1}, {x: 563.2, y: 248.6}, {x: 571.7, y: 254.3}, {x: 583.1, y: 260}, {x: 591.6, y: 262.8}, {x: 600.2, y: 268.5}, {x: 611.6, y: 277.1}, {x: 620.1, y: 279.9}, {x: 628.6, y: 285.6}, {x: 637.2, y: 294.1}, {x: 645.7, y: 299.8}, {x: 654.2, y: 305.5}, {x: 659.9, y: 314}, {x: 665.6, y: 319.7}, {x: 674.1, y: 328.3}, {x: 679.8, y: 331.1}, {x: 682.7, y: 336.8}, {x: 688.4, y: 342.5}, {x: 691.2, y: 345.3}, {x: 694, y: 348.2}, {x: 696.9, y: 351}, {x: 699.7, y: 353.9}, {x: 702.6, y: 353.9}, {x: 702.6, y: 353.9}];
+    assertMatches('厶', 0, points);
+  });
+
+  it('matches using real data 24', () => {
+    const points = [{x: 708.3, y: 749.2}, {x: 711.1, y: 749.2}, {x: 714, y: 749.2}, {x: 716.8, y: 749.2}, {x: 733.9, y: 749.2}, {x: 745.2, y: 749.2}, {x: 779.4, y: 749.2}, {x: 807.8, y: 752.1}, {x: 836.3, y: 754.9}, {x: 861.9, y: 757.8}, {x: 887.5, y: 757.8}, {x: 913.1, y: 760.6}, {x: 927.3, y: 760.6}, {x: 944.4, y: 760.6}, {x: 958.6, y: 760.6}, {x: 981.3, y: 763.5}, {x: 995.6, y: 763.5}, {x: 1006.9, y: 763.5}, {x: 1012.6, y: 763.5}, {x: 1018.3, y: 763.5}, {x: 1021.2, y: 763.5}, {x: 1021.2, y: 763.5}, {x: 1024, y: 760.6}, {x: 1024, y: 760.6}, {x: 1024, y: 757.8}, {x: 1024, y: 754.9}, {x: 1024, y: 749.2}, {x: 1024, y: 746.4}, {x: 1021.2, y: 737.9}, {x: 1015.5, y: 729.3}, {x: 1006.9, y: 709.4}, {x: 992.7, y: 681}, {x: 975.6, y: 652.5}, {x: 961.4, y: 621.2}, {x: 944.4, y: 592.8}, {x: 924.4, y: 550.1}, {x: 915.9, y: 535.9}, {x: 901.7, y: 504.6}, {x: 896, y: 487.6}, {x: 887.5, y: 470.5}, {x: 881.8, y: 459.1}, {x: 876.1, y: 444.9}, {x: 870.4, y: 436.4}, {x: 864.7, y: 427.8}, {x: 861.9, y: 422.1}, {x: 861.9, y: 419.3}, {x: 859, y: 416.4}, {x: 859, y: 413.6}, {x: 859, y: 413.6}, {x: 859, y: 410.8}, {x: 859, y: 410.8}, {x: 859, y: 405.1}, {x: 859, y: 396.5}, {x: 859, y: 390.8}, {x: 859, y: 382.3}, {x: 859, y: 379.5}, {x: 859, y: 376.6}, {x: 859, y: 373.8}, {x: 859, y: 373.8}, {x: 859, y: 370.9}, {x: 856.2, y: 370.9}];
+    assertNotMatches('那', 4, points);
+  });
+
+  it('matches using real data 25', () => {
+    const points = [{x: 662.8, y: 726.5}, {x: 662.8, y: 726.5}, {x: 668.4, y: 726.5}, {x: 699.7, y: 729.3}, {x: 708.3, y: 729.3}, {x: 731, y: 732.2}, {x: 762.3, y: 737.9}, {x: 787.9, y: 740.7}, {x: 816.4, y: 743.6}, {x: 839.1, y: 746.4}, {x: 856.2, y: 749.2}, {x: 876.1, y: 749.2}, {x: 890.3, y: 749.2}, {x: 904.5, y: 749.2}, {x: 910.2, y: 749.2}, {x: 913.1, y: 749.2}, {x: 915.9, y: 749.2}, {x: 915.9, y: 749.2}, {x: 918.8, y: 749.2}, {x: 918.8, y: 746.4}, {x: 918.8, y: 746.4}, {x: 921.6, y: 746.4}, {x: 921.6, y: 746.4}, {x: 924.4, y: 746.4}, {x: 924.4, y: 746.4}, {x: 927.3, y: 746.4}, {x: 927.3, y: 743.6}, {x: 930.1, y: 743.6}, {x: 930.1, y: 737.9}, {x: 930.1, y: 735}, {x: 930.1, y: 729.3}, {x: 927.3, y: 726.5}, {x: 924.4, y: 720.8}, {x: 918.8, y: 715.1}, {x: 907.4, y: 703.7}, {x: 896, y: 689.5}, {x: 881.8, y: 675.3}, {x: 864.7, y: 661.1}, {x: 853.3, y: 649.7}, {x: 839.1, y: 635.5}, {x: 824.9, y: 624.1}, {x: 816.4, y: 615.6}, {x: 807.8, y: 607}, {x: 802.1, y: 601.3}, {x: 793.6, y: 592.8}, {x: 790.8, y: 592.8}, {x: 787.9, y: 590}, {x: 785.1, y: 587.1}, {x: 785.1, y: 587.1}, {x: 785.1, y: 587.1}, {x: 785.1, y: 587.1}, {x: 785.1, y: 587.1}, {x: 787.9, y: 587.1}, {x: 787.9, y: 587.1}, {x: 793.6, y: 587.1}, {x: 796.4, y: 587.1}, {x: 807.8, y: 584.3}, {x: 819.2, y: 575.7}, {x: 836.3, y: 564.4}, {x: 853.3, y: 553}, {x: 864.7, y: 541.6}, {x: 884.6, y: 527.4}, {x: 893.2, y: 521.7}, {x: 898.8, y: 513.2}, {x: 904.5, y: 507.5}, {x: 910.2, y: 501.8}, {x: 915.9, y: 496.1}, {x: 915.9, y: 490.4}, {x: 918.8, y: 484.7}, {x: 921.6, y: 481.9}, {x: 921.6, y: 479}, {x: 921.6, y: 476.2}, {x: 921.6, y: 473.3}, {x: 921.6, y: 467.6}, {x: 921.6, y: 462}, {x: 921.6, y: 456.3}, {x: 918.8, y: 444.9}, {x: 915.9, y: 430.7}, {x: 910.2, y: 413.6}, {x: 907.4, y: 393.7}, {x: 898.8, y: 376.6}, {x: 893.2, y: 356.7}, {x: 887.5, y: 342.5}, {x: 881.8, y: 325.4}, {x: 876.1, y: 314}, {x: 870.4, y: 305.5}, {x: 867.6, y: 299.8}, {x: 864.7, y: 294.1}, {x: 861.9, y: 288.4}, {x: 859, y: 285.6}, {x: 856.2, y: 279.9}, {x: 853.3, y: 271.4}, {x: 850.5, y: 268.5}, {x: 850.5, y: 268.5}, {x: 850.5, y: 265.7}, {x: 847.6, y: 265.7}, {x: 847.6, y: 265.7}, {x: 847.6, y: 265.7}, {x: 844.8, y: 265.7}, {x: 836.3, y: 265.7}, {x: 822, y: 268.5}, {x: 796.4, y: 274.2}, {x: 782.2, y: 277.1}, {x: 773.7, y: 279.9}, {x: 768, y: 282.8}, {x: 765.2, y: 285.6}, {x: 765.2, y: 288.4}];
+    assertMatches('那', 4, points);
+  });
+
+  it('matches using real data 26', () => {
+    const points = [{x: 674.1, y: 749.2}, {x: 674.1, y: 749.2}, {x: 677, y: 749.2}, {x: 685.5, y: 749.2}, {x: 699.7, y: 749.2}, {x: 722.5, y: 749.2}, {x: 753.8, y: 749.2}, {x: 785.1, y: 749.2}, {x: 810.7, y: 749.2}, {x: 836.3, y: 746.4}, {x: 847.6, y: 740.7}, {x: 861.9, y: 735}, {x: 876.1, y: 723.6}, {x: 887.5, y: 715.1}, {x: 896, y: 703.7}, {x: 907.4, y: 692.4}, {x: 913.1, y: 683.8}, {x: 927.3, y: 663.9}, {x: 935.8, y: 649.7}, {x: 941.5, y: 632.6}, {x: 950, y: 615.6}, {x: 958.6, y: 598.5}, {x: 961.4, y: 584.3}, {x: 967.1, y: 564.4}, {x: 970, y: 538.8}, {x: 972.8, y: 518.8}, {x: 972.8, y: 496.1}, {x: 964.3, y: 459.1}, {x: 952.9, y: 433.5}, {x: 944.4, y: 407.9}, {x: 933, y: 382.3}, {x: 918.8, y: 356.7}, {x: 910.2, y: 334}, {x: 901.7, y: 316.9}, {x: 890.3, y: 297}, {x: 884.6, y: 282.8}, {x: 876.1, y: 268.5}, {x: 873.2, y: 260}, {x: 861.9, y: 251.5}, {x: 850.5, y: 248.6}];
+    assertNotMatches('那', 4, points);
+  });
+
+  it('matches using real data 27', () => {
+    const points = [{x: 455.1, y: 681}, {x: 455.1, y: 683.8}, {x: 458, y: 683.8}, {x: 463.6, y: 683.8}, {x: 483.6, y: 683.8}, {x: 503.5, y: 683.8}, {x: 531.9, y: 686.7}, {x: 563.2, y: 686.7}, {x: 594.5, y: 689.5}, {x: 620.1, y: 692.4}, {x: 637.2, y: 692.4}, {x: 648.5, y: 692.4}, {x: 657.1, y: 692.4}, {x: 662.8, y: 692.4}, {x: 662.8, y: 692.4}, {x: 665.6, y: 692.4}, {x: 665.6, y: 689.5}, {x: 665.6, y: 683.8}, {x: 665.6, y: 678.1}, {x: 665.6, y: 669.6}, {x: 665.6, y: 661.1}, {x: 662.8, y: 644}, {x: 659.9, y: 626.9}, {x: 648.5, y: 590}, {x: 640, y: 570}, {x: 634.3, y: 550.1}, {x: 628.6, y: 535.9}, {x: 622.9, y: 527.4}, {x: 620.1, y: 521.7}, {x: 617.2, y: 516}, {x: 617.2, y: 513.2}, {x: 617.2, y: 510.3}, {x: 617.2, y: 510.3}, {x: 622.9, y: 507.5}, {x: 628.6, y: 501.8}, {x: 637.2, y: 498.9}, {x: 642.8, y: 493.2}, {x: 645.7, y: 487.6}, {x: 648.5, y: 484.7}, {x: 651.4, y: 476.2}, {x: 651.4, y: 464.8}, {x: 654.2, y: 450.6}, {x: 654.2, y: 430.7}, {x: 654.2, y: 410.8}, {x: 648.5, y: 393.7}, {x: 640, y: 379.5}, {x: 625.8, y: 365.2}, {x: 597.3, y: 342.5}, {x: 571.7, y: 328.3}, {x: 551.8, y: 322.6}, {x: 531.9, y: 316.9}, {x: 526.2, y: 314}];
+    assertMatches('阝', 0, points);
+    assertNotMatches('阝', 1, points);
+  });
+
+  it('matches using real data 28', () => {
+    const points = [{x: 494.9, y: 726.5}, {x: 497.8, y: 726.5}, {x: 509.2, y: 726.5}, {x: 537.6, y: 726.5}, {x: 583.1, y: 726.5}, {x: 620.1, y: 726.5}, {x: 654.2, y: 720.8}, {x: 671.3, y: 715.1}, {x: 682.7, y: 706.6}, {x: 688.4, y: 695.2}, {x: 688.4, y: 686.7}, {x: 671.3, y: 661.1}, {x: 654.2, y: 641.2}, {x: 628.6, y: 612.7}, {x: 617.2, y: 595.6}, {x: 605.9, y: 584.3}, {x: 600.2, y: 578.6}, {x: 600.2, y: 578.6}, {x: 600.2, y: 578.6}, {x: 605.9, y: 575.7}, {x: 617.2, y: 570}, {x: 625.8, y: 561.5}, {x: 637.2, y: 547.3}, {x: 648.5, y: 530.2}, {x: 657.1, y: 516}, {x: 665.6, y: 496.1}, {x: 668.4, y: 470.5}, {x: 668.4, y: 439.2}, {x: 648.5, y: 402.2}, {x: 617.2, y: 368.1}, {x: 583.1, y: 342.5}, {x: 546.1, y: 319.7}, {x: 526.2, y: 311.2}];
+    assertMatches('阝', 0, points);
+    assertNotMatches('阝', 1, points);
+  });
 });