Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Canvas & SVG renderers will no longer will invert characters #239

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dist/*",
"build/*",
"examples/*",
"demo/*",
"babel.config.js",
"rollup.config.js",
"jest-setup.js"
Expand Down
14 changes: 9 additions & 5 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
</head>
<body>

<h1 class="title">Hanzi Writer</h1>
<form class="js-char-form char-form">
<label>
Character
<input type="text" class="js-char char-input" size="1" maxlength="1" value="" />
<input type="text" class="js-char char-input" size="1" maxlength="1" value="" />
</label>
<button type="submit">Update</button>
</form>
Expand All @@ -24,11 +23,16 @@ <h1 class="title">Hanzi Writer</h1>
<button class="js-quiz">Quiz yourself</button>
</div>

<div id="target">

<div id="target"></div>
<div class="actions">
<select class="char-selection" style="align-self: flex-end;">
<option selected value="hanzi-writer-data">hanzi-writer-data (v2.0.1)</option>
<option value="hanzi-writer-data-fixed">hanzi-writer-data, fixed set</option>
<option value="hanzi-writer-data-jp">hanzi-writer-data-jp (v0.0.2)</option>
<option value="hanzi-writer-data-jp-fixed">hanzi-writer-data-jp, fixed set</option>
</select>
</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="test.js"></script>

Expand Down
11 changes: 4 additions & 7 deletions demo/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,12 @@ button, #target {
.actions {
width: 400px;
margin: 0 auto;
display: flex;
gap: 4px;
margin: 4px auto;
justify-content: center;
}

.actions button {
display: block;
width: 97px;
float: left;
margin-right: 4px;
margin-bottom: 4px;
}
.actions button:last-child {
margin-right: 0;
}
Expand Down
61 changes: 59 additions & 2 deletions demo/test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
var writer;
var isCharVisible;
var isOutlineVisible;
var processCharData;
var charDataLoader;

function printStrokePoints(data) {
var pointStrs = data.drawnPath.points.map((point) => `{x: ${point.x}, y: ${point.y}}`);
Expand All @@ -12,15 +14,22 @@ function updateCharacter() {

var character = document.querySelector('.js-char').value;
window.location.hash = character;
writer = HanziWriter.create('target', character, {

const options = {
width: 400,
height: 400,
renderer: 'svg',
radicalColor: '#166E16',
onCorrectStroke: printStrokePoints,
onMistake: printStrokePoints,
showCharacter: false,
});
processCharData,
};
if (charDataLoader) {
options.charDataLoader = charDataLoader;
}

writer = HanziWriter.create('target', character, options);
isCharVisible = true;
isOutlineVisible = true;
window.writer = writer;
Expand Down Expand Up @@ -55,4 +64,52 @@ window.onload = function () {
showOutline: true,
});
});

document.querySelector('.char-selection').addEventListener('input', (ev) => {
const value = ev.target.value; // "hanzi-writer-data" | "hanzi-writer-data-fixed" | "hanzi-writer-data-jp"
const char = document.querySelector('.js-char').value;

switch (value) {
case 'hanzi-writer-data':
charDataLoader = undefined;
processCharData = undefined;
break;
case 'hanzi-writer-data-fixed': {
charDataLoader = createCharDataLoader(
(char) =>
`https://raw.githubusercontent.com/jamsch/hanzi-writer-data/fixed-set/data/${char}.json`,
);
processCharData = null; // We don't need to process the character data
break;
}
case 'hanzi-writer-data-jp': {
charDataLoader = createCharDataLoader(
(char) =>
`https://raw.githubusercontent.com/jamsch/hanzi-writer-data-jp/master/data/${char}.json`,
);
processCharData = undefined; // We need to process the character data
break;
}
case 'hanzi-writer-data-jp-fixed': {
charDataLoader = createCharDataLoader(
(char) =>
`https://raw.githubusercontent.com/jamsch/hanzi-writer-data-jp/fixed-set/data/${char}.json`,
);
processCharData = null; // We don't need to process the character data
break;
}
default:
break;
}

updateCharacter();
});
};

const createCharDataLoader = (getUrl) => {
return (char, onLoad, onError) =>
fetch(getUrl(char))
.then((res) => res.json())
.then(onLoad)
.catch(onError);
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"test": "jest",
"lint-test": "eslint -c .eslintrc src",
"lint-fix": "eslint --fix --ext .tsx,.ts .",
"build": "rm -rf dist && rollup -c",
"build": "rollup -c",
"prepublishOnly": "rollup -c",
"semantic-release": "semantic-release",
"prettier": "prettier -w src",
Expand Down
26 changes: 14 additions & 12 deletions src/HanziWriter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import RenderState from './RenderState';
import parseCharData from './parseCharData';
import Positioner from './Positioner';
import Quiz from './Quiz';
import svgRenderer from './renderers/svg';
import canvasRenderer from './renderers/canvas';
import defaultOptions from './defaultOptions';
import LoadingManager from './LoadingManager';
import * as characterActions from './characterActions';
import { trim, colorStringToVals, selectIndex, fixIndex } from './utils';
import { colorStringToVals, selectIndex, fixIndex } from './utils';
import Character from './models/Character';
import HanziWriterRendererBase, {
HanziWriterRendererConstructor,
Expand Down Expand Up @@ -90,14 +89,12 @@ export default class HanziWriter {

static getScalingTransform(width: number, height: number, padding = 0) {
const positioner = new Positioner({ width, height, padding });
const { xOffset: x, yOffset: y, scale } = positioner;
return {
x: positioner.xOffset,
y: positioner.yOffset,
scale: positioner.scale,
transform: trim(`
translate(${positioner.xOffset}, ${positioner.height - positioner.yOffset})
scale(${positioner.scale}, ${-1 * positioner.scale})
`).replace(/\s+/g, ' '),
x,
y,
scale,
transform: `translate(${x}, ${y}) scale(${scale})`,
};
}

Expand Down Expand Up @@ -424,13 +421,18 @@ export default class HanziWriter {
this._hanziWriterRenderer = null;
this._withDataPromise = this._loadingManager
.loadCharData(char)
.then((pathStrings) => {
.then((characterJson) => {
// if "pathStrings" isn't set, ".catch()"" was probably called and loading likely failed
if (!pathStrings || this._loadingManager.loadingFailed) {
if (!characterJson || this._loadingManager.loadingFailed) {
return;
}

this._character = parseCharData(char, pathStrings);
this._character = Character.fromObject(
char,
characterJson,
this._options.processCharData,
);

this._renderState = new RenderState(this._character, this._options, (nextState) =>
hanziWriterRenderer.render(nextState),
);
Expand Down
15 changes: 7 additions & 8 deletions src/Mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Delay implements GenericMutation {
_duration: number;
_startTime: number | null;
_paused: boolean;
_timeout!: NodeJS.Timeout;
_timeout!: ReturnType<typeof setTimeout>;
_resolve: (() => void) | undefined;

constructor(duration: number) {
Expand All @@ -43,7 +43,6 @@ class Delay implements GenericMutation {
this._startTime = performanceNow();
this._runningPromise = new Promise((resolve) => {
this._resolve = resolve;
// @ts-ignore return type of "setTimeout" in builds is parsed as `number` instead of `Timeout`
this._timeout = setTimeout(() => this.cancel(), this._duration);
}) as Promise<void>;
return this._runningPromise;
Expand All @@ -61,13 +60,12 @@ class Delay implements GenericMutation {
resume() {
if (!this._paused) return;
this._startTime = performance.now();
// @ts-ignore return type of "setTimeout" in builds is parsed as `number` instead of `Timeout`
this._timeout = setTimeout(() => this.cancel(), this._duration);
this._paused = false;
}

cancel() {
clearTimeout(this._timeout!);
clearTimeout(this._timeout);
if (this._resolve) {
this._resolve();
}
Expand Down Expand Up @@ -140,10 +138,11 @@ export default class Mutation<
}

private _inflateValues(renderState: TRenderStateClass) {
let values = this._valuesOrCallable;
if (typeof this._valuesOrCallable === 'function') {
values = this._valuesOrCallable(renderState.state);
}
const values =
typeof this._valuesOrCallable === 'function'
? this._valuesOrCallable(renderState.state)
: this._valuesOrCallable;

this._values = inflate(this.scope, values);
}

Expand Down
22 changes: 5 additions & 17 deletions src/Positioner.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { Point } from './typings/types';

// All makemeahanzi characters have the same bounding box
const CHARACTER_BOUNDS = [
{ x: 0, y: -124 },
{ x: 1024, y: 900 },
];
const [from, to] = CHARACTER_BOUNDS;
const preScaledWidth = to.x - from.x;
const preScaledHeight = to.y - from.y;
const BOUNDING_BOX = 1024;

export type PositionerOptions = {
/** Default: 0 */
Expand All @@ -34,22 +28,16 @@ export default class Positioner {

const effectiveWidth = width - 2 * padding;
const effectiveHeight = height - 2 * padding;
const scaleX = effectiveWidth / preScaledWidth;
const scaleY = effectiveHeight / preScaledHeight;

this.scale = Math.min(scaleX, scaleY);
this.scale = Math.min(effectiveWidth / BOUNDING_BOX, effectiveHeight / BOUNDING_BOX);

const xCenteringBuffer = padding + (effectiveWidth - this.scale * preScaledWidth) / 2;
const yCenteringBuffer =
padding + (effectiveHeight - this.scale * preScaledHeight) / 2;

this.xOffset = -1 * from.x * this.scale + xCenteringBuffer;
this.yOffset = -1 * from.y * this.scale + yCenteringBuffer;
this.xOffset = padding + (effectiveWidth - this.scale * BOUNDING_BOX) / 2;
this.yOffset = padding + (effectiveHeight - this.scale * BOUNDING_BOX) / 2;
}

convertExternalPoint(point: Point) {
const x = (point.x - this.xOffset) / this.scale;
const y = (this.height - this.yOffset - point.y) / this.scale;
const y = (point.y - this.yOffset) / this.scale;
return { x, y };
}
}
10 changes: 4 additions & 6 deletions src/Quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as quizActions from './quizActions';
import * as geometry from './geometry';
import * as characterActions from './characterActions';
import Character from './models/Character';
import Stroke from './models/Stroke';
import { ParsedHanziWriterOptions, Point, StrokeData } from './typings/types';
import RenderState from './RenderState';
import { MutationChain } from './Mutation';
Expand Down Expand Up @@ -120,11 +121,8 @@ export default class Quiz {
} else {
this._handleFailure(meta);

const {
showHintAfterMisses,
highlightColor,
strokeHighlightSpeed,
} = this._options!;
const { showHintAfterMisses, highlightColor, strokeHighlightSpeed } =
this._options!;

if (
showHintAfterMisses !== false &&
Expand Down Expand Up @@ -227,7 +225,7 @@ export default class Quiz {
this._options!.onMistake?.(this._getStrokeData({ isCorrect: false, meta }));
}

_getCurrentStroke() {
_getCurrentStroke(): Stroke {
return this._character.strokes[this._currentStrokeIndex];
}
}
52 changes: 52 additions & 0 deletions src/__tests__/Character-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import ta from 'hanzi-writer-data/他.json';
import { CharacterJson } from 'typings/types';
import Character from '../models/Character';

describe('Character', () => {
describe('fromObject', () => {
it("does not transform stroke paths if 'null' is provided in the 'processCharacter' param", () => {
const character = Character.fromObject('他', ta, null);
for (let i = 0; i < ta.strokes.length; i++) {
expect(character.strokes[i].path).toEqual(ta.strokes[i]);
}
});
it('correctly parses a given object', () => {
const res = Character.fromObject('他', ta);
expect(res.strokes).toHaveLength(5);
expect(res.strokes[0].isInRadical).toBe(true);
expect(res.strokes[1].isInRadical).toBe(true);
expect(res.strokes[2].isInRadical).toBe(false);
expect(res.strokes[3].isInRadical).toBe(false);
expect(res.strokes[4].isInRadical).toBe(false);
});
it("applies 'scaleX(-1)' transform to path strings by default", () => {
const strokeTransforms = [
{
before: 'L 400 500 C 100 200 100 300 100 400 Q 100 200 100 300 Z',
after: `L 400 400 C 100 700 100 600 100 500 Q 100 700 100 600 Z`,
},
{
before: 'L 1 150 M 1 250 C 1 350 3 450 5 650 Q 1 750 3 850 Z',
after: 'L 1 750 M 1 650 C 1 550 3 450 5 250 Q 1 150 3 50 Z',
},
// Verify paths with commas also get correctly scaled
{
before: 'L 1,150 M 1,250 C 1,350,3,450,5,650 Q 1,750,3,850 Z',
after: 'L 1 750 M 1 650 C 1 550 3 450 5 250 Q 1 150 3 50 Z',
},
];

const basicCharJson: CharacterJson = {
strokes: strokeTransforms.map((transform) => transform.before),
medians: [[], [], []],
radStrokes: undefined,
};

const res = Character.fromObject('他', basicCharJson);

for (let i = 0; i < strokeTransforms.length; i++) {
expect(res.strokes[i].path).toEqual(strokeTransforms[i].after);
}
});
});
});
Loading