Repository: chanind/hanzi-writer (ts)

Issue:
Final flashing color distinct from stroke hint flashing color
The final flashing color should definitively be separated from the hint as visually they represent 2 conflictual visual communication:
- a stroke hint appears when you were wrong after X times... it is a correction. Common colors that could come to mind is redish colors.
- the final victory flash is a confirmation that you are right. People might want to use green or yellow bright colors. But not refer to the colors used when you actually failed.

Does this make sense ? I can work on that in a few days if this seems okay for you.

Pull request description:
Adding a separate option for highlightCompleteColor
This PR is a completed version of https://github.com/chanind/hanzi-writer/pull/54 that works with the new animation / rendering architecture. This PR adds a new option `highlightCompleteColor` which can be set to control the color that is flashed when a quiz is completed successfully.

closes #49 

Patch:
diff --git a/src/HanziWriter.js b/src/HanziWriter.js
index e9d37ce8..77696e88 100644
--- a/src/HanziWriter.js
+++ b/src/HanziWriter.js
@@ -45,6 +45,7 @@ const defaultOptions = {
   leniency: 1,
   showHintAfterMisses: 3,
   highlightOnComplete: true,
+  highlightCompleteColor: null,
 
   // undocumented obscure options
 
@@ -179,6 +180,10 @@ HanziWriter.prototype._assignOptions = function(options) {
     mergedOptions.strokeHighlightSpeed = 500 / mergedOptions.strokeHighlightDuration;
   }
 
+  if (!options.highlightCompleteColor) {
+    mergedOptions.highlightCompleteColor = mergedOptions.highlightColor;
+  }
+
   return this._fillWidthAndHeight(mergedOptions);
 };
 
diff --git a/src/Quiz.js b/src/Quiz.js
index d46c9126..5e980fc6 100644
--- a/src/Quiz.js
+++ b/src/Quiz.js
@@ -72,7 +72,7 @@ Quiz.prototype.endUserStroke = function() {
     this._handleFailure();
     if (this._numRecentMistakes >= this._options.showHintAfterMisses) {
       this._renderState.run(
-        characterActions.highlightStroke('highlight', currentStroke, this._options.strokeHighlightSpeed),
+        quizActions.highlightStroke(currentStroke, this._options.highlightColor, this._options.strokeHighlightSpeed),
       );
     }
   }
@@ -106,10 +106,11 @@ Quiz.prototype._handleSuccess = function(stroke) {
       totalMistakes: this._totalMistakes,
     });
     if (this._options.highlightOnComplete) {
-      animation = animation
-        .concat(characterActions.hideCharacter('highlight', this._character))
-        .concat(characterActions.showCharacter('highlight', this._character, this._options.strokeHighlightDuration))
-        .concat(characterActions.hideCharacter('highlight', this._character, this._options.strokeHighlightDuration));
+      animation = animation.concat(quizActions.highlightCompleteChar(
+        this._character,
+        this._options.highlightCompleteColor,
+        this._options.strokeHighlightDuration * 2,
+      ));
     }
   }
   this._renderState.run(animation);
diff --git a/src/characterActions.js b/src/characterActions.js
index 13930b5b..cc90fba7 100644
--- a/src/characterActions.js
+++ b/src/characterActions.js
@@ -43,27 +43,6 @@ const animateStroke = (charName, stroke, speed) => {
   ];
 };
 
-const highlightStroke = (charName, stroke, speed) => {
-  const strokeNum = stroke.strokeNum;
-  const duration = (stroke.getLength() + 600) / (3 * speed);
-  return [
-    new Mutation(`character.${charName}`, {
-      opacity: 1,
-      strokes: {
-        [strokeNum]: {
-          displayPortion: 0,
-          opacity: 0,
-        },
-      },
-    }),
-    new Mutation(`character.${charName}.strokes.${strokeNum}`, {
-      displayPortion: 1,
-      opacity: 1,
-    }, { duration }),
-    new Mutation(`character.${charName}.strokes.${strokeNum}.opacity`, 0, { duration }),
-  ];
-};
-
 const showStroke = (charName, strokeNum, duration) => {
   return [
     new Mutation(`character.${charName}.strokes.${strokeNum}`, {
@@ -100,6 +79,5 @@ module.exports = {
   animateCharacter,
   animateCharacterLoop,
   animateStroke,
-  highlightStroke,
   showStroke,
 };
diff --git a/src/quizActions.js b/src/quizActions.js
index 74a155e0..9befae2e 100644
--- a/src/quizActions.js
+++ b/src/quizActions.js
@@ -39,7 +39,40 @@ const removeUserStroke = (userStrokeId, duration) => {
   ];
 };
 
+const highlightStroke = (stroke, color, speed) => {
+  const strokeNum = stroke.strokeNum;
+  const duration = (stroke.getLength() + 600) / (3 * speed);
+  return [
+    new Mutation('character.highlight.strokeColor', color),
+    new Mutation('character.highlight', {
+      opacity: 1,
+      strokes: {
+        [strokeNum]: {
+          displayPortion: 0,
+          opacity: 0,
+        },
+      },
+    }),
+    new Mutation(`character.highlight.strokes.${strokeNum}`, {
+      displayPortion: 1,
+      opacity: 1,
+    }, { duration }),
+    new Mutation(`character.highlight.strokes.${strokeNum}.opacity`, 0, { duration }),
+  ];
+};
+
+const highlightCompleteChar = (character, color, duration) => {
+  return [
+    new Mutation('character.highlight.strokeColor', color),
+  ]
+    .concat(characterActions.hideCharacter('highlight', character))
+    .concat(characterActions.showCharacter('highlight', character, duration / 2))
+    .concat(characterActions.hideCharacter('highlight', character, duration / 2));
+};
+
 module.exports = {
+  highlightCompleteChar,
+  highlightStroke,
   startQuiz,
   startUserStroke,
   updateUserStroke,


Test patch:
diff --git a/src/__tests__/HanziWriter-test.js b/src/__tests__/HanziWriter-test.js
index 1f4d155b..e20bdf53 100644
--- a/src/__tests__/HanziWriter-test.js
+++ b/src/__tests__/HanziWriter-test.js
@@ -482,5 +482,15 @@ describe('HanziWriter', () => {
       expect(writer._options.strokeAnimationSpeed).toBe(0.5);
       expect(writer._options.strokeHighlightSpeed).toBe(2);
     });
+
+    it('sets highlightCompleteColor to highlightColor if not explicitly set', () => {
+      const writer = new HanziWriter('target', '人', { highlightColor: '#ABC' });
+      expect(writer._options.highlightCompleteColor).toBe('#ABC');
+    });
+
+    it('sets highlightCompleteColor to the default highilghtColor if none is passed', () => {
+      const writer = new HanziWriter('target', '人');
+      expect(writer._options.highlightCompleteColor).toBe('#AAF');
+    });
   });
 });
diff --git a/src/__tests__/Quiz-test.js b/src/__tests__/Quiz-test.js
index 0a3119b1..e6539c45 100644
--- a/src/__tests__/Quiz-test.js
+++ b/src/__tests__/Quiz-test.js
@@ -55,6 +55,7 @@ const opts = {
 
   showHintAfterMisses: 3,
   highlightOnComplete: true,
+  highlightCompleteColor: null,
 
   // undocumented obscure options
 
@@ -64,7 +65,10 @@ const opts = {
   outlineWidth: 2,
 };
 
-const createRenderState = () => new RenderState(char, opts, () => {});
+const createRenderState = (optOverrides = {}) => {
+  const options = Object.assign({}, opts, optOverrides);
+  return new RenderState(char, options, () => {});
+};
 
 
 describe('Quiz', () => {
@@ -373,6 +377,8 @@ describe('Quiz', () => {
       strokeMatches.mockImplementation(() => false);
 
       const renderState = createRenderState();
+      // should reset this color before highlighting
+      renderState.state.character.highlight.strokeColor = '#00F';
       const quiz = new Quiz(char, renderState, new Positioner());
       const onCorrectStroke = jest.fn();
       const onMistake = jest.fn();
@@ -421,6 +427,7 @@ describe('Quiz', () => {
       expect(renderState.state.character.main.strokes[1].opacity).toBe(0);
 
       // should highlight the stroke now
+      expect(renderState.state.character.highlight.strokeColor).toBe('#AAF');
       expect(renderState.state.character.highlight.strokes[0].opacity).toBe(1);
       clock.tick(1000);
       await resolvePromises();
@@ -435,7 +442,13 @@ describe('Quiz', () => {
       const onCorrectStroke = jest.fn();
       const onMistake = jest.fn();
       const onComplete = jest.fn();
-      quiz.startQuiz(Object.assign({}, opts, { onCorrectStroke, onComplete, onMistake, highlightOnComplete: true }));
+      quiz.startQuiz(Object.assign({}, opts, {
+        onCorrectStroke,
+        onComplete,
+        onMistake,
+        highlightOnComplete: true,
+        highlightCompleteColor: '#0F0',
+      }));
       clock.tick(1000);
       await resolvePromises();
 
@@ -483,6 +496,7 @@ describe('Quiz', () => {
       await resolvePromises();
 
       // highlights the character at the end
+      expect(renderState.state.character.highlight.strokeColor).toBe('#0F0');
       expect(renderState.state.character.highlight.opacity).toBe(1);
       clock.tick(1000);
       await resolvePromises();