hanzi-writer.js 82 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760
  1. /**
  2. * Hanzi Writer v3.6.0 | https://chanind.github.io/hanzi-writer
  3. */
  4. var HanziWriter = (function () {
  5. 'use strict';
  6. var _globalObj$navigator; // hacky way to get around rollup not properly setting `global` to `window` in browser
  7. const globalObj = typeof window === 'undefined' ? global : window;
  8. const performanceNow = globalObj.performance && (() => globalObj.performance.now()) || (() => Date.now());
  9. const requestAnimationFrame = globalObj.requestAnimationFrame || (callback => setTimeout(() => callback(performanceNow()), 1000 / 60));
  10. const cancelAnimationFrame = globalObj.cancelAnimationFrame || clearTimeout; // Object.assign polyfill, because IE :/
  11. function arrLast(arr) {
  12. return arr[arr.length - 1];
  13. }
  14. const fixIndex = (index, length) => {
  15. // helper to handle negative indexes in array indices
  16. if (index < 0) {
  17. return length + index;
  18. }
  19. return index;
  20. };
  21. const selectIndex = (arr, index) => {
  22. // helper to select item from array at index, supporting negative indexes
  23. return arr[fixIndex(index, arr.length)];
  24. };
  25. function copyAndMergeDeep(base, override) {
  26. const output = { ...base
  27. };
  28. for (const key in override) {
  29. const baseVal = base[key];
  30. const overrideVal = override[key];
  31. if (baseVal === overrideVal) {
  32. continue;
  33. }
  34. if (baseVal && overrideVal && typeof baseVal === 'object' && typeof overrideVal === 'object' && !Array.isArray(overrideVal)) {
  35. output[key] = copyAndMergeDeep(baseVal, overrideVal);
  36. } else {
  37. // @ts-ignore
  38. output[key] = overrideVal;
  39. }
  40. }
  41. return output;
  42. }
  43. /** basically a simplified version of lodash.get, selects a key out of an object like 'a.b' from {a: {b: 7}} */
  44. function inflate(scope, obj) {
  45. const parts = scope.split('.');
  46. const final = {};
  47. let current = final;
  48. for (let i = 0; i < parts.length; i++) {
  49. const cap = i === parts.length - 1 ? obj : {};
  50. current[parts[i]] = cap;
  51. current = cap;
  52. }
  53. return final;
  54. }
  55. let count = 0;
  56. function counter() {
  57. count++;
  58. return count;
  59. }
  60. function average(arr) {
  61. const sum = arr.reduce((acc, val) => val + acc, 0);
  62. return sum / arr.length;
  63. }
  64. function colorStringToVals(colorString) {
  65. const normalizedColor = colorString.toUpperCase().trim(); // based on https://stackoverflow.com/a/21648508
  66. if (/^#([A-F0-9]{3}){1,2}$/.test(normalizedColor)) {
  67. let hexParts = normalizedColor.substring(1).split('');
  68. if (hexParts.length === 3) {
  69. hexParts = [hexParts[0], hexParts[0], hexParts[1], hexParts[1], hexParts[2], hexParts[2]];
  70. }
  71. const hexStr = `${hexParts.join('')}`;
  72. return {
  73. r: parseInt(hexStr.slice(0, 2), 16),
  74. g: parseInt(hexStr.slice(2, 4), 16),
  75. b: parseInt(hexStr.slice(4, 6), 16),
  76. a: 1
  77. };
  78. }
  79. const rgbMatch = normalizedColor.match(/^RGBA?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*(\d*\.?\d+))?\)$/);
  80. if (rgbMatch) {
  81. return {
  82. r: parseInt(rgbMatch[1], 10),
  83. g: parseInt(rgbMatch[2], 10),
  84. b: parseInt(rgbMatch[3], 10),
  85. // @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 2.
  86. a: parseFloat(rgbMatch[4] || 1, 10)
  87. };
  88. }
  89. throw new Error(`Invalid color: ${colorString}`);
  90. }
  91. const trim = string => string.replace(/^\s+/, '').replace(/\s+$/, ''); // return a new array-like object with int keys where each key is item
  92. // ex: objRepeat({x: 8}, 3) === {0: {x: 8}, 1: {x: 8}, 2: {x: 8}}
  93. function objRepeat(item, times) {
  94. const obj = {};
  95. for (let i = 0; i < times; i++) {
  96. obj[i] = item;
  97. }
  98. return obj;
  99. } // similar to objRepeat, but takes in a callback which is called for each index in the object
  100. function objRepeatCb(times, cb) {
  101. const obj = {};
  102. for (let i = 0; i < times; i++) {
  103. obj[i] = cb(i);
  104. }
  105. return obj;
  106. }
  107. const ua = ((_globalObj$navigator = globalObj.navigator) === null || _globalObj$navigator === void 0 ? void 0 : _globalObj$navigator.userAgent) || '';
  108. const isMsBrowser = ua.indexOf('MSIE ') > 0 || ua.indexOf('Trident/') > 0 || ua.indexOf('Edge/') > 0; // eslint-disable-next-line @typescript-eslint/no-empty-function
  109. const noop = () => {};
  110. class RenderState {
  111. constructor(character, options, onStateChange = noop) {
  112. this._mutationChains = [];
  113. this._onStateChange = onStateChange;
  114. this.state = {
  115. options: {
  116. drawingFadeDuration: options.drawingFadeDuration,
  117. drawingWidth: options.drawingWidth,
  118. drawingColor: colorStringToVals(options.drawingColor),
  119. strokeColor: colorStringToVals(options.strokeColor),
  120. outlineColor: colorStringToVals(options.outlineColor),
  121. radicalColor: colorStringToVals(options.radicalColor || options.strokeColor),
  122. highlightColor: colorStringToVals(options.highlightColor)
  123. },
  124. character: {
  125. main: {
  126. opacity: options.showCharacter ? 1 : 0,
  127. strokes: {}
  128. },
  129. outline: {
  130. opacity: options.showOutline ? 1 : 0,
  131. strokes: {}
  132. },
  133. highlight: {
  134. opacity: 1,
  135. strokes: {}
  136. }
  137. },
  138. userStrokes: null
  139. };
  140. for (let i = 0; i < character.strokes.length; i++) {
  141. this.state.character.main.strokes[i] = {
  142. opacity: 1,
  143. displayPortion: 1
  144. };
  145. this.state.character.outline.strokes[i] = {
  146. opacity: 1,
  147. displayPortion: 1
  148. };
  149. this.state.character.highlight.strokes[i] = {
  150. opacity: 0,
  151. displayPortion: 1
  152. };
  153. }
  154. }
  155. overwriteOnStateChange(onStateChange) {
  156. this._onStateChange = onStateChange;
  157. }
  158. updateState(stateChanges) {
  159. const nextState = copyAndMergeDeep(this.state, stateChanges);
  160. this._onStateChange(nextState, this.state);
  161. this.state = nextState;
  162. }
  163. run(mutations, options = {}) {
  164. const scopes = mutations.map(mut => mut.scope);
  165. this.cancelMutations(scopes);
  166. return new Promise(resolve => {
  167. const mutationChain = {
  168. _isActive: true,
  169. _index: 0,
  170. _resolve: resolve,
  171. _mutations: mutations,
  172. _loop: options.loop,
  173. _scopes: scopes
  174. };
  175. this._mutationChains.push(mutationChain);
  176. this._run(mutationChain);
  177. });
  178. }
  179. _run(mutationChain) {
  180. if (!mutationChain._isActive) {
  181. return;
  182. }
  183. const mutations = mutationChain._mutations;
  184. if (mutationChain._index >= mutations.length) {
  185. if (mutationChain._loop) {
  186. mutationChain._index = 0; // eslint-disable-line no-param-reassign
  187. } else {
  188. mutationChain._isActive = false; // eslint-disable-line no-param-reassign
  189. this._mutationChains = this._mutationChains.filter(chain => chain !== mutationChain); // The chain is done - resolve the promise to signal it finished successfully
  190. mutationChain._resolve({
  191. canceled: false
  192. });
  193. return;
  194. }
  195. }
  196. const activeMutation = mutationChain._mutations[mutationChain._index];
  197. activeMutation.run(this).then(() => {
  198. if (mutationChain._isActive) {
  199. mutationChain._index++; // eslint-disable-line no-param-reassign
  200. this._run(mutationChain);
  201. }
  202. });
  203. }
  204. _getActiveMutations() {
  205. return this._mutationChains.map(chain => chain._mutations[chain._index]);
  206. }
  207. pauseAll() {
  208. this._getActiveMutations().forEach(mutation => mutation.pause());
  209. }
  210. resumeAll() {
  211. this._getActiveMutations().forEach(mutation => mutation.resume());
  212. }
  213. cancelMutations(scopesToCancel) {
  214. for (const chain of this._mutationChains) {
  215. for (const chainId of chain._scopes) {
  216. for (const scopeToCancel of scopesToCancel) {
  217. if (chainId.startsWith(scopeToCancel) || scopeToCancel.startsWith(chainId)) {
  218. this._cancelMutationChain(chain);
  219. }
  220. }
  221. }
  222. }
  223. }
  224. cancelAll() {
  225. this.cancelMutations(['']);
  226. }
  227. _cancelMutationChain(mutationChain) {
  228. var _mutationChain$_resol;
  229. mutationChain._isActive = false;
  230. for (let i = mutationChain._index; i < mutationChain._mutations.length; i++) {
  231. mutationChain._mutations[i].cancel(this);
  232. }
  233. (_mutationChain$_resol = mutationChain._resolve) === null || _mutationChain$_resol === void 0 ? void 0 : _mutationChain$_resol.call(mutationChain, {
  234. canceled: true
  235. });
  236. this._mutationChains = this._mutationChains.filter(chain => chain !== mutationChain);
  237. }
  238. }
  239. const subtract = (p1, p2) => ({
  240. x: p1.x - p2.x,
  241. y: p1.y - p2.y
  242. });
  243. const magnitude = point => Math.sqrt(Math.pow(point.x, 2) + Math.pow(point.y, 2));
  244. const distance = (point1, point2) => magnitude(subtract(point1, point2));
  245. const equals = (point1, point2) => point1.x === point2.x && point1.y === point2.y;
  246. const round = (point, precision = 1) => {
  247. const multiplier = precision * 10;
  248. return {
  249. x: Math.round(multiplier * point.x) / multiplier,
  250. y: Math.round(multiplier * point.y) / multiplier
  251. };
  252. };
  253. const length = points => {
  254. let lastPoint = points[0];
  255. const pointsSansFirst = points.slice(1);
  256. return pointsSansFirst.reduce((acc, point) => {
  257. const dist = distance(point, lastPoint);
  258. lastPoint = point;
  259. return acc + dist;
  260. }, 0);
  261. };
  262. const cosineSimilarity = (point1, point2) => {
  263. const rawDotProduct = point1.x * point2.x + point1.y * point2.y;
  264. return rawDotProduct / magnitude(point1) / magnitude(point2);
  265. };
  266. /**
  267. * return a new point, p3, which is on the same line as p1 and p2, but distance away
  268. * from p2. p1, p2, p3 will always lie on the line in that order
  269. */
  270. const _extendPointOnLine = (p1, p2, dist) => {
  271. const vect = subtract(p2, p1);
  272. const norm = dist / magnitude(vect);
  273. return {
  274. x: p2.x + norm * vect.x,
  275. y: p2.y + norm * vect.y
  276. };
  277. };
  278. /** based on http://www.kr.tuwien.ac.at/staff/eiter/et-archive/cdtr9464.pdf */
  279. const frechetDist = (curve1, curve2) => {
  280. const longCurve = curve1.length >= curve2.length ? curve1 : curve2;
  281. const shortCurve = curve1.length >= curve2.length ? curve2 : curve1;
  282. const calcVal = (i, j, prevResultsCol, curResultsCol) => {
  283. if (i === 0 && j === 0) {
  284. return distance(longCurve[0], shortCurve[0]);
  285. }
  286. if (i > 0 && j === 0) {
  287. return Math.max(prevResultsCol[0], distance(longCurve[i], shortCurve[0]));
  288. }
  289. const lastResult = curResultsCol[curResultsCol.length - 1];
  290. if (i === 0 && j > 0) {
  291. return Math.max(lastResult, distance(longCurve[0], shortCurve[j]));
  292. }
  293. return Math.max(Math.min(prevResultsCol[j], prevResultsCol[j - 1], lastResult), distance(longCurve[i], shortCurve[j]));
  294. };
  295. let prevResultsCol = [];
  296. for (let i = 0; i < longCurve.length; i++) {
  297. const curResultsCol = [];
  298. for (let j = 0; j < shortCurve.length; j++) {
  299. // we only need the results from i - 1 and j - 1 to continue the calculation
  300. // so we only need to hold onto the last column of calculated results
  301. // prevResultsCol is results[i-1][:] in the original algorithm
  302. // curResultsCol is results[i][:j-1] in the original algorithm
  303. curResultsCol.push(calcVal(i, j, prevResultsCol, curResultsCol));
  304. }
  305. prevResultsCol = curResultsCol;
  306. }
  307. return prevResultsCol[shortCurve.length - 1];
  308. };
  309. /** break up long segments in the curve into smaller segments of len maxLen or smaller */
  310. const subdivideCurve = (curve, maxLen = 0.05) => {
  311. const newCurve = curve.slice(0, 1);
  312. for (const point of curve.slice(1)) {
  313. const prevPoint = newCurve[newCurve.length - 1];
  314. const segLen = distance(point, prevPoint);
  315. if (segLen > maxLen) {
  316. const numNewPoints = Math.ceil(segLen / maxLen);
  317. const newSegLen = segLen / numNewPoints;
  318. for (let i = 0; i < numNewPoints; i++) {
  319. newCurve.push(_extendPointOnLine(point, prevPoint, -1 * newSegLen * (i + 1)));
  320. }
  321. } else {
  322. newCurve.push(point);
  323. }
  324. }
  325. return newCurve;
  326. };
  327. /** redraw the curve using numPoints equally spaced out along the length of the curve */
  328. const outlineCurve = (curve, numPoints = 30) => {
  329. const curveLen = length(curve);
  330. const segmentLen = curveLen / (numPoints - 1);
  331. const outlinePoints = [curve[0]];
  332. const endPoint = arrLast(curve);
  333. const remainingCurvePoints = curve.slice(1);
  334. for (let i = 0; i < numPoints - 2; i++) {
  335. let lastPoint = arrLast(outlinePoints);
  336. let remainingDist = segmentLen;
  337. let outlinePointFound = false;
  338. while (!outlinePointFound) {
  339. const nextPointDist = distance(lastPoint, remainingCurvePoints[0]);
  340. if (nextPointDist < remainingDist) {
  341. remainingDist -= nextPointDist;
  342. lastPoint = remainingCurvePoints.shift();
  343. } else {
  344. const nextPoint = _extendPointOnLine(lastPoint, remainingCurvePoints[0], remainingDist - nextPointDist);
  345. outlinePoints.push(nextPoint);
  346. outlinePointFound = true;
  347. }
  348. }
  349. }
  350. outlinePoints.push(endPoint);
  351. return outlinePoints;
  352. };
  353. /** translate and scale from https://en.wikipedia.org/wiki/Procrustes_analysis */
  354. const normalizeCurve = curve => {
  355. const outlinedCurve = outlineCurve(curve);
  356. const meanX = average(outlinedCurve.map(point => point.x));
  357. const meanY = average(outlinedCurve.map(point => point.y));
  358. const mean = {
  359. x: meanX,
  360. y: meanY
  361. };
  362. const translatedCurve = outlinedCurve.map(point => subtract(point, mean));
  363. 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)]));
  364. const scaledCurve = translatedCurve.map(point => ({
  365. x: point.x / scale,
  366. y: point.y / scale
  367. }));
  368. return subdivideCurve(scaledCurve);
  369. }; // rotate around the origin
  370. const rotate = (curve, theta) => {
  371. return curve.map(point => ({
  372. x: Math.cos(theta) * point.x - Math.sin(theta) * point.y,
  373. y: Math.sin(theta) * point.x + Math.cos(theta) * point.y
  374. }));
  375. }; // remove intermediate points that are on the same line as the points to either side
  376. const _filterParallelPoints = points => {
  377. if (points.length < 3) return points;
  378. const filteredPoints = [points[0], points[1]];
  379. points.slice(2).forEach(point => {
  380. const numFilteredPoints = filteredPoints.length;
  381. const curVect = subtract(point, filteredPoints[numFilteredPoints - 1]);
  382. const prevVect = subtract(filteredPoints[numFilteredPoints - 1], filteredPoints[numFilteredPoints - 2]); // this is the z coord of the cross-product. If this is 0 then they're parallel
  383. const isParallel = curVect.y * prevVect.x - curVect.x * prevVect.y === 0;
  384. if (isParallel) {
  385. filteredPoints.pop();
  386. }
  387. filteredPoints.push(point);
  388. });
  389. return filteredPoints;
  390. };
  391. function getPathString(points, close = false) {
  392. const start = round(points[0]);
  393. const remainingPoints = points.slice(1);
  394. let pathString = `M ${start.x} ${start.y}`;
  395. remainingPoints.forEach(point => {
  396. const roundedPoint = round(point);
  397. pathString += ` L ${roundedPoint.x} ${roundedPoint.y}`;
  398. });
  399. if (close) {
  400. pathString += 'Z';
  401. }
  402. return pathString;
  403. }
  404. /** take points on a path and move their start point backwards by distance */
  405. const extendStart = (points, dist) => {
  406. const filteredPoints = _filterParallelPoints(points);
  407. if (filteredPoints.length < 2) return filteredPoints;
  408. const p1 = filteredPoints[1];
  409. const p2 = filteredPoints[0];
  410. const newStart = _extendPointOnLine(p1, p2, dist);
  411. const extendedPoints = filteredPoints.slice(1);
  412. extendedPoints.unshift(newStart);
  413. return extendedPoints;
  414. };
  415. class Stroke {
  416. constructor(path, points, strokeNum, isInRadical = false) {
  417. this.path = path;
  418. this.points = points;
  419. this.strokeNum = strokeNum;
  420. this.isInRadical = isInRadical;
  421. }
  422. getStartingPoint() {
  423. return this.points[0];
  424. }
  425. getEndingPoint() {
  426. return this.points[this.points.length - 1];
  427. }
  428. getLength() {
  429. return length(this.points);
  430. }
  431. getVectors() {
  432. let lastPoint = this.points[0];
  433. const pointsSansFirst = this.points.slice(1);
  434. return pointsSansFirst.map(point => {
  435. const vector = subtract(point, lastPoint);
  436. lastPoint = point;
  437. return vector;
  438. });
  439. }
  440. getDistance(point) {
  441. const distances = this.points.map(strokePoint => distance(strokePoint, point));
  442. return Math.min(...distances);
  443. }
  444. getAverageDistance(points) {
  445. const totalDist = points.reduce((acc, point) => acc + this.getDistance(point), 0);
  446. return totalDist / points.length;
  447. }
  448. }
  449. class Character {
  450. constructor(symbol, strokes) {
  451. this.symbol = symbol;
  452. this.strokes = strokes;
  453. }
  454. }
  455. function generateStrokes({
  456. radStrokes,
  457. strokes,
  458. medians
  459. }) {
  460. const isInRadical = strokeNum => {
  461. var _radStrokes$indexOf;
  462. return ((_radStrokes$indexOf = radStrokes === null || radStrokes === void 0 ? void 0 : radStrokes.indexOf(strokeNum)) !== null && _radStrokes$indexOf !== void 0 ? _radStrokes$indexOf : -1) >= 0;
  463. };
  464. return strokes.map((path, index) => {
  465. const points = medians[index].map(pointData => {
  466. const [x, y] = pointData;
  467. return {
  468. x,
  469. y
  470. };
  471. });
  472. return new Stroke(path, points, index, isInRadical(index));
  473. });
  474. }
  475. function parseCharData(symbol, charJson) {
  476. const strokes = generateStrokes(charJson);
  477. return new Character(symbol, strokes);
  478. }
  479. // All makemeahanzi characters have the same bounding box
  480. const CHARACTER_BOUNDS = [{
  481. x: 0,
  482. y: -124
  483. }, {
  484. x: 1024,
  485. y: 900
  486. }];
  487. const [from, to] = CHARACTER_BOUNDS;
  488. const preScaledWidth = to.x - from.x;
  489. const preScaledHeight = to.y - from.y;
  490. class Positioner {
  491. constructor(options) {
  492. const {
  493. padding,
  494. width,
  495. height
  496. } = options;
  497. this.padding = padding;
  498. this.width = width;
  499. this.height = height;
  500. const effectiveWidth = width - 2 * padding;
  501. const effectiveHeight = height - 2 * padding;
  502. const scaleX = effectiveWidth / preScaledWidth;
  503. const scaleY = effectiveHeight / preScaledHeight;
  504. this.scale = Math.min(scaleX, scaleY);
  505. const xCenteringBuffer = padding + (effectiveWidth - this.scale * preScaledWidth) / 2;
  506. const yCenteringBuffer = padding + (effectiveHeight - this.scale * preScaledHeight) / 2;
  507. this.xOffset = -1 * from.x * this.scale + xCenteringBuffer;
  508. this.yOffset = -1 * from.y * this.scale + yCenteringBuffer;
  509. }
  510. convertExternalPoint(point) {
  511. const x = (point.x - this.xOffset) / this.scale;
  512. const y = (this.height - this.yOffset - point.y) / this.scale;
  513. return {
  514. x,
  515. y
  516. };
  517. }
  518. }
  519. const COSINE_SIMILARITY_THRESHOLD = 0; // -1 to 1, smaller = more lenient
  520. const START_AND_END_DIST_THRESHOLD = 250; // bigger = more lenient
  521. const FRECHET_THRESHOLD = 0.4; // bigger = more lenient
  522. const MIN_LEN_THRESHOLD = 0.35; // smaller = more lenient
  523. function strokeMatches(userStroke, character, strokeNum, options = {}) {
  524. const strokes = character.strokes;
  525. const points = stripDuplicates(userStroke.points);
  526. if (points.length < 2) {
  527. return {
  528. isMatch: false,
  529. meta: {
  530. isStrokeBackwards: false
  531. }
  532. };
  533. }
  534. const {
  535. isMatch,
  536. meta,
  537. avgDist
  538. } = getMatchData(points, strokes[strokeNum], options);
  539. if (!isMatch) {
  540. return {
  541. isMatch,
  542. meta
  543. };
  544. } // if there is a better match among strokes the user hasn't drawn yet, the user probably drew the wrong stroke
  545. const laterStrokes = strokes.slice(strokeNum + 1);
  546. let closestMatchDist = avgDist;
  547. for (let i = 0; i < laterStrokes.length; i++) {
  548. const {
  549. isMatch,
  550. avgDist
  551. } = getMatchData(points, laterStrokes[i], { ...options,
  552. checkBackwards: false
  553. });
  554. if (isMatch && avgDist < closestMatchDist) {
  555. closestMatchDist = avgDist;
  556. }
  557. } // if there's a better match, rather that returning false automatically, try reducing leniency instead
  558. // if leniency is already really high we can allow some similar strokes to pass
  559. if (closestMatchDist < avgDist) {
  560. // adjust leniency between 0.3 and 0.6 depending on how much of a better match the new match is
  561. const leniencyAdjustment = 0.6 * (closestMatchDist + avgDist) / (2 * avgDist);
  562. const {
  563. isMatch,
  564. meta
  565. } = getMatchData(points, strokes[strokeNum], { ...options,
  566. leniency: (options.leniency || 1) * leniencyAdjustment
  567. });
  568. return {
  569. isMatch,
  570. meta
  571. };
  572. }
  573. return {
  574. isMatch,
  575. meta
  576. };
  577. }
  578. const startAndEndMatches = (points, closestStroke, leniency) => {
  579. const startingDist = distance(closestStroke.getStartingPoint(), points[0]);
  580. const endingDist = distance(closestStroke.getEndingPoint(), points[points.length - 1]);
  581. return startingDist <= START_AND_END_DIST_THRESHOLD * leniency && endingDist <= START_AND_END_DIST_THRESHOLD * leniency;
  582. }; // returns a list of the direction of all segments in the line connecting the points
  583. const getEdgeVectors = points => {
  584. const vectors = [];
  585. let lastPoint = points[0];
  586. points.slice(1).forEach(point => {
  587. vectors.push(subtract(point, lastPoint));
  588. lastPoint = point;
  589. });
  590. return vectors;
  591. };
  592. const directionMatches = (points, stroke) => {
  593. const edgeVectors = getEdgeVectors(points);
  594. const strokeVectors = stroke.getVectors();
  595. const similarities = edgeVectors.map(edgeVector => {
  596. const strokeSimilarities = strokeVectors.map(strokeVector => cosineSimilarity(strokeVector, edgeVector));
  597. return Math.max(...strokeSimilarities);
  598. });
  599. const avgSimilarity = average(similarities);
  600. return avgSimilarity > COSINE_SIMILARITY_THRESHOLD;
  601. };
  602. const lengthMatches = (points, stroke, leniency) => {
  603. return leniency * (length(points) + 25) / (stroke.getLength() + 25) >= MIN_LEN_THRESHOLD;
  604. };
  605. const stripDuplicates = points => {
  606. if (points.length < 2) return points;
  607. const [firstPoint, ...rest] = points;
  608. const dedupedPoints = [firstPoint];
  609. for (const point of rest) {
  610. if (!equals(point, dedupedPoints[dedupedPoints.length - 1])) {
  611. dedupedPoints.push(point);
  612. }
  613. }
  614. return dedupedPoints;
  615. };
  616. const SHAPE_FIT_ROTATIONS = [Math.PI / 16, Math.PI / 32, 0, -1 * Math.PI / 32, -1 * Math.PI / 16];
  617. const shapeFit = (curve1, curve2, leniency) => {
  618. const normCurve1 = normalizeCurve(curve1);
  619. const normCurve2 = normalizeCurve(curve2);
  620. let minDist = Infinity;
  621. SHAPE_FIT_ROTATIONS.forEach(theta => {
  622. const dist = frechetDist(normCurve1, rotate(normCurve2, theta));
  623. if (dist < minDist) {
  624. minDist = dist;
  625. }
  626. });
  627. return minDist <= FRECHET_THRESHOLD * leniency;
  628. };
  629. const getMatchData = (points, stroke, options) => {
  630. const {
  631. leniency = 1,
  632. isOutlineVisible = false,
  633. checkBackwards = true,
  634. averageDistanceThreshold = 350
  635. } = options;
  636. const avgDist = stroke.getAverageDistance(points);
  637. const distMod = isOutlineVisible || stroke.strokeNum > 0 ? 0.5 : 1;
  638. const withinDistThresh = avgDist <= averageDistanceThreshold * distMod * leniency; // short circuit for faster matching
  639. if (!withinDistThresh) {
  640. return {
  641. isMatch: false,
  642. avgDist,
  643. meta: {
  644. isStrokeBackwards: false
  645. }
  646. };
  647. }
  648. const startAndEndMatch = startAndEndMatches(points, stroke, leniency);
  649. const directionMatch = directionMatches(points, stroke);
  650. const shapeMatch = shapeFit(points, stroke.points, leniency);
  651. const lengthMatch = lengthMatches(points, stroke, leniency);
  652. const isMatch = withinDistThresh && startAndEndMatch && directionMatch && shapeMatch && lengthMatch;
  653. if (checkBackwards && !isMatch) {
  654. const backwardsMatchData = getMatchData([...points].reverse(), stroke, { ...options,
  655. checkBackwards: false
  656. });
  657. if (backwardsMatchData.isMatch) {
  658. return {
  659. isMatch,
  660. avgDist,
  661. meta: {
  662. isStrokeBackwards: true
  663. }
  664. };
  665. }
  666. }
  667. return {
  668. isMatch,
  669. avgDist,
  670. meta: {
  671. isStrokeBackwards: false
  672. }
  673. };
  674. };
  675. class UserStroke {
  676. constructor(id, startingPoint, startingExternalPoint) {
  677. this.id = id;
  678. this.points = [startingPoint];
  679. this.externalPoints = [startingExternalPoint];
  680. }
  681. appendPoint(point, externalPoint) {
  682. this.points.push(point);
  683. this.externalPoints.push(externalPoint);
  684. }
  685. }
  686. class Delay {
  687. constructor(duration) {
  688. this._duration = duration;
  689. this._startTime = null;
  690. this._paused = false;
  691. this.scope = `delay.${duration}`;
  692. }
  693. run() {
  694. this._startTime = performanceNow();
  695. this._runningPromise = new Promise(resolve => {
  696. this._resolve = resolve; // @ts-ignore return type of "setTimeout" in builds is parsed as `number` instead of `Timeout`
  697. this._timeout = setTimeout(() => this.cancel(), this._duration);
  698. });
  699. return this._runningPromise;
  700. }
  701. pause() {
  702. if (this._paused) return; // to pause, clear the timeout and rewrite this._duration with whatever time is remaining
  703. const elapsedDelay = performance.now() - (this._startTime || 0);
  704. this._duration = Math.max(0, this._duration - elapsedDelay);
  705. clearTimeout(this._timeout);
  706. this._paused = true;
  707. }
  708. resume() {
  709. if (!this._paused) return;
  710. this._startTime = performance.now(); // @ts-ignore return type of "setTimeout" in builds is parsed as `number` instead of `Timeout`
  711. this._timeout = setTimeout(() => this.cancel(), this._duration);
  712. this._paused = false;
  713. }
  714. cancel() {
  715. clearTimeout(this._timeout);
  716. if (this._resolve) {
  717. this._resolve();
  718. }
  719. this._resolve = undefined;
  720. }
  721. }
  722. class Mutation {
  723. /**
  724. *
  725. * @param scope a string representation of what fields this mutation affects from the state. This is used to cancel conflicting mutations
  726. * @param valuesOrCallable a thunk containing the value to set, or a callback which will return those values
  727. */
  728. constructor(scope, valuesOrCallable, options = {}) {
  729. this._tick = timing => {
  730. if (this._startPauseTime !== null) {
  731. return;
  732. }
  733. const progress = Math.min(1, (timing - this._startTime - this._pausedDuration) / this._duration);
  734. if (progress === 1) {
  735. this._renderState.updateState(this._values);
  736. this._frameHandle = undefined;
  737. this.cancel(this._renderState);
  738. } else {
  739. const easedProgress = ease(progress);
  740. const stateChanges = getPartialValues(this._startState, this._values, easedProgress);
  741. this._renderState.updateState(stateChanges);
  742. this._frameHandle = requestAnimationFrame(this._tick);
  743. }
  744. };
  745. this.scope = scope;
  746. this._valuesOrCallable = valuesOrCallable;
  747. this._duration = options.duration || 0;
  748. this._force = options.force;
  749. this._pausedDuration = 0;
  750. this._startPauseTime = null;
  751. }
  752. run(renderState) {
  753. if (!this._values) this._inflateValues(renderState);
  754. if (this._duration === 0) renderState.updateState(this._values);
  755. if (this._duration === 0 || isAlreadyAtEnd(renderState.state, this._values)) {
  756. return Promise.resolve();
  757. }
  758. this._renderState = renderState;
  759. this._startState = renderState.state;
  760. this._startTime = performance.now();
  761. this._frameHandle = requestAnimationFrame(this._tick);
  762. return new Promise(resolve => {
  763. this._resolve = resolve;
  764. });
  765. }
  766. _inflateValues(renderState) {
  767. let values = this._valuesOrCallable;
  768. if (typeof this._valuesOrCallable === 'function') {
  769. values = this._valuesOrCallable(renderState.state);
  770. }
  771. this._values = inflate(this.scope, values);
  772. }
  773. pause() {
  774. if (this._startPauseTime !== null) {
  775. return;
  776. }
  777. if (this._frameHandle) {
  778. cancelAnimationFrame(this._frameHandle);
  779. }
  780. this._startPauseTime = performance.now();
  781. }
  782. resume() {
  783. if (this._startPauseTime === null) {
  784. return;
  785. }
  786. this._frameHandle = requestAnimationFrame(this._tick);
  787. this._pausedDuration += performance.now() - this._startPauseTime;
  788. this._startPauseTime = null;
  789. }
  790. cancel(renderState) {
  791. var _this$_resolve;
  792. (_this$_resolve = this._resolve) === null || _this$_resolve === void 0 ? void 0 : _this$_resolve.call(this);
  793. this._resolve = undefined;
  794. cancelAnimationFrame(this._frameHandle || -1);
  795. this._frameHandle = undefined;
  796. if (this._force) {
  797. if (!this._values) this._inflateValues(renderState);
  798. renderState.updateState(this._values);
  799. }
  800. }
  801. }
  802. Mutation.Delay = Delay;
  803. function getPartialValues(startValues, endValues, progress) {
  804. const target = {};
  805. for (const key in endValues) {
  806. const endValue = endValues[key];
  807. const startValue = startValues === null || startValues === void 0 ? void 0 : startValues[key];
  808. if (typeof startValue === 'number' && typeof endValue === 'number' && endValue >= 0) {
  809. target[key] = progress * (endValue - startValue) + startValue;
  810. } else {
  811. target[key] = getPartialValues(startValue, endValue, progress);
  812. }
  813. }
  814. return target;
  815. }
  816. function isAlreadyAtEnd(startValues, endValues) {
  817. for (const key in endValues) {
  818. const endValue = endValues[key];
  819. const startValue = startValues === null || startValues === void 0 ? void 0 : startValues[key];
  820. if (endValue >= 0) {
  821. if (endValue !== startValue) {
  822. return false;
  823. }
  824. } else if (!isAlreadyAtEnd(startValue, endValue)) {
  825. return false;
  826. }
  827. }
  828. return true;
  829. } // from https://github.com/maxwellito/vivus
  830. const ease = x => -Math.cos(x * Math.PI) / 2 + 0.5;
  831. const showStrokes = (charName, character, duration) => {
  832. return [new Mutation(`character.${charName}.strokes`, objRepeat({
  833. opacity: 1,
  834. displayPortion: 1
  835. }, character.strokes.length), {
  836. duration,
  837. force: true
  838. })];
  839. };
  840. const showCharacter = (charName, character, duration) => {
  841. return [new Mutation(`character.${charName}`, {
  842. opacity: 1,
  843. strokes: objRepeat({
  844. opacity: 1,
  845. displayPortion: 1
  846. }, character.strokes.length)
  847. }, {
  848. duration,
  849. force: true
  850. })];
  851. };
  852. const hideCharacter = (charName, character, duration) => {
  853. return [new Mutation(`character.${charName}.opacity`, 0, {
  854. duration,
  855. force: true
  856. }), ...showStrokes(charName, character, 0)];
  857. };
  858. const updateColor = (colorName, colorVal, duration) => {
  859. return [new Mutation(`options.${colorName}`, colorVal, {
  860. duration
  861. })];
  862. };
  863. const highlightStroke = (stroke, color, speed) => {
  864. const strokeNum = stroke.strokeNum;
  865. const duration = (stroke.getLength() + 600) / (3 * speed);
  866. return [new Mutation('options.highlightColor', color), new Mutation('character.highlight', {
  867. opacity: 1,
  868. strokes: {
  869. [strokeNum]: {
  870. displayPortion: 0,
  871. opacity: 0
  872. }
  873. }
  874. }), new Mutation(`character.highlight.strokes.${strokeNum}`, {
  875. displayPortion: 1,
  876. opacity: 1
  877. }, {
  878. duration
  879. }), new Mutation(`character.highlight.strokes.${strokeNum}.opacity`, 0, {
  880. duration,
  881. force: true
  882. })];
  883. };
  884. const animateStroke = (charName, stroke, speed) => {
  885. const strokeNum = stroke.strokeNum;
  886. const duration = (stroke.getLength() + 600) / (3 * speed);
  887. return [new Mutation(`character.${charName}`, {
  888. opacity: 1,
  889. strokes: {
  890. [strokeNum]: {
  891. displayPortion: 0,
  892. opacity: 1
  893. }
  894. }
  895. }), new Mutation(`character.${charName}.strokes.${strokeNum}.displayPortion`, 1, {
  896. duration
  897. })];
  898. };
  899. const animateSingleStroke = (charName, character, strokeNum, speed) => {
  900. const mutationStateFunc = state => {
  901. const curCharState = state.character[charName];
  902. const mutationState = {
  903. opacity: 1,
  904. strokes: {}
  905. };
  906. for (let i = 0; i < character.strokes.length; i++) {
  907. mutationState.strokes[i] = {
  908. opacity: curCharState.opacity * curCharState.strokes[i].opacity
  909. };
  910. }
  911. return mutationState;
  912. };
  913. const stroke = character.strokes[strokeNum];
  914. return [new Mutation(`character.${charName}`, mutationStateFunc), ...animateStroke(charName, stroke, speed)];
  915. };
  916. const showStroke = (charName, strokeNum, duration) => {
  917. return [new Mutation(`character.${charName}.strokes.${strokeNum}`, {
  918. displayPortion: 1,
  919. opacity: 1
  920. }, {
  921. duration,
  922. force: true
  923. })];
  924. };
  925. const animateCharacter = (charName, character, fadeDuration, speed, delayBetweenStrokes) => {
  926. let mutations = hideCharacter(charName, character, fadeDuration);
  927. mutations = mutations.concat(showStrokes(charName, character, 0));
  928. mutations.push(new Mutation(`character.${charName}`, {
  929. opacity: 1,
  930. strokes: objRepeat({
  931. opacity: 0
  932. }, character.strokes.length)
  933. }, {
  934. force: true
  935. }));
  936. character.strokes.forEach((stroke, i) => {
  937. if (i > 0) mutations.push(new Mutation.Delay(delayBetweenStrokes));
  938. mutations = mutations.concat(animateStroke(charName, stroke, speed));
  939. });
  940. return mutations;
  941. };
  942. const animateCharacterLoop = (charName, character, fadeDuration, speed, delayBetweenStrokes, delayBetweenLoops) => {
  943. const mutations = animateCharacter(charName, character, fadeDuration, speed, delayBetweenStrokes);
  944. mutations.push(new Mutation.Delay(delayBetweenLoops));
  945. return mutations;
  946. };
  947. const startQuiz = (character, fadeDuration, startStrokeNum) => {
  948. return [...hideCharacter('main', character, fadeDuration), new Mutation('character.highlight', {
  949. opacity: 1,
  950. strokes: objRepeat({
  951. opacity: 0
  952. }, character.strokes.length)
  953. }, {
  954. force: true
  955. }), new Mutation('character.main', {
  956. opacity: 1,
  957. strokes: objRepeatCb(character.strokes.length, i => ({
  958. opacity: i < startStrokeNum ? 1 : 0
  959. }))
  960. }, {
  961. force: true
  962. })];
  963. };
  964. const startUserStroke = (id, point) => {
  965. return [new Mutation('quiz.activeUserStrokeId', id, {
  966. force: true
  967. }), new Mutation(`userStrokes.${id}`, {
  968. points: [point],
  969. opacity: 1
  970. }, {
  971. force: true
  972. })];
  973. };
  974. const updateUserStroke = (userStrokeId, points) => {
  975. return [new Mutation(`userStrokes.${userStrokeId}.points`, points, {
  976. force: true
  977. })];
  978. };
  979. const removeUserStroke = (userStrokeId, duration) => {
  980. return [new Mutation(`userStrokes.${userStrokeId}.opacity`, 0, {
  981. duration
  982. }), new Mutation(`userStrokes.${userStrokeId}`, null, {
  983. force: true
  984. })];
  985. };
  986. const highlightCompleteChar = (character, color, duration) => {
  987. return [new Mutation('options.highlightColor', color), ...hideCharacter('highlight', character), ...showCharacter('highlight', character, duration / 2), ...hideCharacter('highlight', character, duration / 2)];
  988. };
  989. const getDrawnPath = userStroke => ({
  990. pathString: getPathString(userStroke.externalPoints),
  991. points: userStroke.points.map(point => round(point))
  992. });
  993. class Quiz {
  994. constructor(character, renderState, positioner) {
  995. this._currentStrokeIndex = 0;
  996. this._mistakesOnStroke = 0;
  997. this._totalMistakes = 0;
  998. this._character = character;
  999. this._renderState = renderState;
  1000. this._isActive = false;
  1001. this._positioner = positioner;
  1002. }
  1003. startQuiz(options) {
  1004. this._isActive = true;
  1005. this._options = options;
  1006. const startIndex = fixIndex(options.quizStartStrokeNum, this._character.strokes.length);
  1007. this._currentStrokeIndex = Math.min(startIndex, this._character.strokes.length - 1);
  1008. this._mistakesOnStroke = 0;
  1009. this._totalMistakes = 0;
  1010. return this._renderState.run(startQuiz(this._character, options.strokeFadeDuration, this._currentStrokeIndex));
  1011. }
  1012. startUserStroke(externalPoint) {
  1013. if (!this._isActive) {
  1014. return null;
  1015. }
  1016. if (this._userStroke) {
  1017. return this.endUserStroke();
  1018. }
  1019. const point = this._positioner.convertExternalPoint(externalPoint);
  1020. const strokeId = counter();
  1021. this._userStroke = new UserStroke(strokeId, point, externalPoint);
  1022. return this._renderState.run(startUserStroke(strokeId, point));
  1023. }
  1024. continueUserStroke(externalPoint) {
  1025. if (!this._userStroke) {
  1026. return Promise.resolve();
  1027. }
  1028. const point = this._positioner.convertExternalPoint(externalPoint);
  1029. this._userStroke.appendPoint(point, externalPoint);
  1030. const nextPoints = this._userStroke.points.slice(0);
  1031. return this._renderState.run(updateUserStroke(this._userStroke.id, nextPoints));
  1032. }
  1033. setPositioner(positioner) {
  1034. this._positioner = positioner;
  1035. }
  1036. endUserStroke() {
  1037. var _this$_options$drawin;
  1038. if (!this._userStroke) return;
  1039. this._renderState.run(removeUserStroke(this._userStroke.id, (_this$_options$drawin = this._options.drawingFadeDuration) !== null && _this$_options$drawin !== void 0 ? _this$_options$drawin : 300)); // skip single-point strokes
  1040. if (this._userStroke.points.length === 1) {
  1041. this._userStroke = undefined;
  1042. return;
  1043. }
  1044. const {
  1045. acceptBackwardsStrokes,
  1046. markStrokeCorrectAfterMisses
  1047. } = this._options;
  1048. const currentStroke = this._getCurrentStroke();
  1049. const {
  1050. isMatch,
  1051. meta
  1052. } = strokeMatches(this._userStroke, this._character, this._currentStrokeIndex, {
  1053. isOutlineVisible: this._renderState.state.character.outline.opacity > 0,
  1054. leniency: this._options.leniency,
  1055. averageDistanceThreshold: this._options.averageDistanceThreshold
  1056. }); // if markStrokeCorrectAfterMisses is passed, just force the stroke to count as correct after n tries
  1057. const isForceAccepted = markStrokeCorrectAfterMisses && this._mistakesOnStroke + 1 >= markStrokeCorrectAfterMisses;
  1058. const isAccepted = isMatch || isForceAccepted || meta.isStrokeBackwards && acceptBackwardsStrokes;
  1059. if (isAccepted) {
  1060. this._handleSuccess(meta);
  1061. } else {
  1062. this._handleFailure(meta);
  1063. const {
  1064. showHintAfterMisses,
  1065. highlightColor,
  1066. strokeHighlightSpeed
  1067. } = this._options;
  1068. if (showHintAfterMisses !== false && this._mistakesOnStroke >= showHintAfterMisses) {
  1069. this._renderState.run(highlightStroke(currentStroke, colorStringToVals(highlightColor), strokeHighlightSpeed));
  1070. }
  1071. }
  1072. this._userStroke = undefined;
  1073. }
  1074. cancel() {
  1075. this._isActive = false;
  1076. if (this._userStroke) {
  1077. this._renderState.run(removeUserStroke(this._userStroke.id, this._options.drawingFadeDuration));
  1078. }
  1079. }
  1080. _getStrokeData({
  1081. isCorrect,
  1082. meta
  1083. }) {
  1084. return {
  1085. character: this._character.symbol,
  1086. strokeNum: this._currentStrokeIndex,
  1087. mistakesOnStroke: this._mistakesOnStroke,
  1088. totalMistakes: this._totalMistakes,
  1089. strokesRemaining: this._character.strokes.length - this._currentStrokeIndex - (isCorrect ? 1 : 0),
  1090. drawnPath: getDrawnPath(this._userStroke),
  1091. isBackwards: meta.isStrokeBackwards
  1092. };
  1093. }
  1094. _handleSuccess(meta) {
  1095. if (!this._options) return;
  1096. const {
  1097. strokes,
  1098. symbol
  1099. } = this._character;
  1100. const {
  1101. onCorrectStroke,
  1102. onComplete,
  1103. highlightOnComplete,
  1104. strokeFadeDuration,
  1105. highlightCompleteColor,
  1106. highlightColor,
  1107. strokeHighlightDuration
  1108. } = this._options;
  1109. onCorrectStroke === null || onCorrectStroke === void 0 ? void 0 : onCorrectStroke({ ...this._getStrokeData({
  1110. isCorrect: true,
  1111. meta
  1112. })
  1113. });
  1114. let animation = showStroke('main', this._currentStrokeIndex, strokeFadeDuration);
  1115. this._mistakesOnStroke = 0;
  1116. this._currentStrokeIndex += 1;
  1117. const isComplete = this._currentStrokeIndex === strokes.length;
  1118. if (isComplete) {
  1119. this._isActive = false;
  1120. onComplete === null || onComplete === void 0 ? void 0 : onComplete({
  1121. character: symbol,
  1122. totalMistakes: this._totalMistakes
  1123. });
  1124. if (highlightOnComplete) {
  1125. animation = animation.concat(highlightCompleteChar(this._character, colorStringToVals(highlightCompleteColor || highlightColor), (strokeHighlightDuration || 0) * 2));
  1126. }
  1127. }
  1128. this._renderState.run(animation);
  1129. }
  1130. _handleFailure(meta) {
  1131. var _this$_options$onMist, _this$_options;
  1132. this._mistakesOnStroke += 1;
  1133. this._totalMistakes += 1;
  1134. (_this$_options$onMist = (_this$_options = this._options).onMistake) === null || _this$_options$onMist === void 0 ? void 0 : _this$_options$onMist.call(_this$_options, this._getStrokeData({
  1135. isCorrect: false,
  1136. meta
  1137. }));
  1138. }
  1139. _getCurrentStroke() {
  1140. return this._character.strokes[this._currentStrokeIndex];
  1141. }
  1142. }
  1143. function createElm(elmType) {
  1144. return document.createElementNS('http://www.w3.org/2000/svg', elmType);
  1145. }
  1146. function attr(elm, name, value) {
  1147. elm.setAttributeNS(null, name, value);
  1148. }
  1149. function attrs(elm, attrsMap) {
  1150. Object.keys(attrsMap).forEach(attrName => attr(elm, attrName, attrsMap[attrName]));
  1151. } // inspired by https://talk.observablehq.com/t/hanzi-writer-renders-incorrectly-inside-an-observable-notebook-on-a-mobile-browser/1898
  1152. function urlIdRef(id) {
  1153. let prefix = '';
  1154. if (window.location && window.location.href) {
  1155. prefix = window.location.href.replace(/#[^#]*$/, '').replace(/"/gi, '%22');
  1156. }
  1157. return `url("${prefix}#${id}")`;
  1158. }
  1159. function removeElm(elm) {
  1160. var _elm$parentNode;
  1161. elm === null || elm === void 0 ? void 0 : (_elm$parentNode = elm.parentNode) === null || _elm$parentNode === void 0 ? void 0 : _elm$parentNode.removeChild(elm);
  1162. }
  1163. class StrokeRendererBase {
  1164. constructor(stroke) {
  1165. this.stroke = stroke;
  1166. this._pathLength = stroke.getLength() + StrokeRendererBase.STROKE_WIDTH / 2;
  1167. }
  1168. _getStrokeDashoffset(displayPortion) {
  1169. return this._pathLength * 0.999 * (1 - displayPortion);
  1170. }
  1171. _getColor({
  1172. strokeColor,
  1173. radicalColor
  1174. }) {
  1175. return radicalColor && this.stroke.isInRadical ? radicalColor : strokeColor;
  1176. }
  1177. }
  1178. StrokeRendererBase.STROKE_WIDTH = 200;
  1179. const STROKE_WIDTH = 200;
  1180. /** This is a stroke composed of several stroke parts **/
  1181. class StrokeRenderer extends StrokeRendererBase {
  1182. constructor(stroke) {
  1183. super(stroke);
  1184. this._oldProps = undefined;
  1185. }
  1186. mount(target) {
  1187. this._animationPath = createElm('path');
  1188. this._clip = createElm('clipPath');
  1189. this._strokePath = createElm('path');
  1190. const maskId = `mask-${counter()}`;
  1191. attr(this._clip, 'id', maskId);
  1192. attr(this._strokePath, 'd', this.stroke.path);
  1193. this._animationPath.style.opacity = '0';
  1194. attr(this._animationPath, 'clip-path', urlIdRef(maskId));
  1195. const extendedMaskPoints = extendStart(this.stroke.points, STROKE_WIDTH / 2);
  1196. attr(this._animationPath, 'd', getPathString(extendedMaskPoints));
  1197. attrs(this._animationPath, {
  1198. stroke: '#FFFFFF',
  1199. 'stroke-width': STROKE_WIDTH.toString(),
  1200. fill: 'none',
  1201. 'stroke-linecap': 'round',
  1202. 'stroke-linejoin': 'miter',
  1203. 'stroke-dasharray': `${this._pathLength},${this._pathLength}`
  1204. });
  1205. this._clip.appendChild(this._strokePath);
  1206. target.defs.appendChild(this._clip);
  1207. target.svg.appendChild(this._animationPath);
  1208. return this;
  1209. }
  1210. render(props) {
  1211. var _this$_oldProps, _this$_oldProps2;
  1212. if (props === this._oldProps || !this._animationPath) {
  1213. return;
  1214. }
  1215. if (props.displayPortion !== ((_this$_oldProps = this._oldProps) === null || _this$_oldProps === void 0 ? void 0 : _this$_oldProps.displayPortion)) {
  1216. this._animationPath.style.strokeDashoffset = this._getStrokeDashoffset(props.displayPortion).toString();
  1217. }
  1218. const color = this._getColor(props);
  1219. if (!this._oldProps || color !== this._getColor(this._oldProps)) {
  1220. const {
  1221. r,
  1222. g,
  1223. b,
  1224. a
  1225. } = color;
  1226. attrs(this._animationPath, {
  1227. stroke: `rgba(${r},${g},${b},${a})`
  1228. });
  1229. }
  1230. if (props.opacity !== ((_this$_oldProps2 = this._oldProps) === null || _this$_oldProps2 === void 0 ? void 0 : _this$_oldProps2.opacity)) {
  1231. this._animationPath.style.opacity = props.opacity.toString();
  1232. }
  1233. this._oldProps = props;
  1234. }
  1235. }
  1236. class CharacterRenderer {
  1237. constructor(character) {
  1238. this._oldProps = undefined;
  1239. this._strokeRenderers = character.strokes.map(stroke => new StrokeRenderer(stroke));
  1240. }
  1241. mount(target) {
  1242. const subTarget = target.createSubRenderTarget();
  1243. this._group = subTarget.svg;
  1244. this._strokeRenderers.forEach(strokeRenderer => {
  1245. strokeRenderer.mount(subTarget);
  1246. });
  1247. }
  1248. render(props) {
  1249. var _this$_oldProps, _this$_oldProps3;
  1250. if (props === this._oldProps || !this._group) {
  1251. return;
  1252. }
  1253. const {
  1254. opacity,
  1255. strokes,
  1256. strokeColor,
  1257. radicalColor = null
  1258. } = props;
  1259. if (opacity !== ((_this$_oldProps = this._oldProps) === null || _this$_oldProps === void 0 ? void 0 : _this$_oldProps.opacity)) {
  1260. this._group.style.opacity = opacity.toString(); // MS browsers seem to have a bug where if SVG is set to display:none, it sometimes breaks.
  1261. // More info: https://github.com/chanind/hanzi-writer/issues/164
  1262. // this is just a perf improvement, so disable for MS browsers
  1263. if (!isMsBrowser) {
  1264. var _this$_oldProps2;
  1265. if (opacity === 0) {
  1266. this._group.style.display = 'none';
  1267. } else if (((_this$_oldProps2 = this._oldProps) === null || _this$_oldProps2 === void 0 ? void 0 : _this$_oldProps2.opacity) === 0) {
  1268. this._group.style.removeProperty('display');
  1269. }
  1270. }
  1271. }
  1272. const colorsChanged = !this._oldProps || strokeColor !== this._oldProps.strokeColor || radicalColor !== this._oldProps.radicalColor;
  1273. if (colorsChanged || strokes !== ((_this$_oldProps3 = this._oldProps) === null || _this$_oldProps3 === void 0 ? void 0 : _this$_oldProps3.strokes)) {
  1274. for (let i = 0; i < this._strokeRenderers.length; i++) {
  1275. var _this$_oldProps4;
  1276. if (!colorsChanged && (_this$_oldProps4 = this._oldProps) !== null && _this$_oldProps4 !== void 0 && _this$_oldProps4.strokes && strokes[i] === this._oldProps.strokes[i]) {
  1277. continue;
  1278. }
  1279. this._strokeRenderers[i].render({
  1280. strokeColor,
  1281. radicalColor,
  1282. opacity: strokes[i].opacity,
  1283. displayPortion: strokes[i].displayPortion
  1284. });
  1285. }
  1286. }
  1287. this._oldProps = props;
  1288. }
  1289. }
  1290. class UserStrokeRenderer {
  1291. constructor() {
  1292. this._oldProps = undefined;
  1293. }
  1294. mount(target) {
  1295. this._path = createElm('path');
  1296. target.svg.appendChild(this._path);
  1297. }
  1298. render(props) {
  1299. var _this$_oldProps, _this$_oldProps2, _this$_oldProps3, _this$_oldProps4;
  1300. if (!this._path || props === this._oldProps) {
  1301. return;
  1302. }
  1303. if (props.strokeColor !== ((_this$_oldProps = this._oldProps) === null || _this$_oldProps === void 0 ? void 0 : _this$_oldProps.strokeColor) || props.strokeWidth !== ((_this$_oldProps2 = this._oldProps) === null || _this$_oldProps2 === void 0 ? void 0 : _this$_oldProps2.strokeWidth)) {
  1304. const {
  1305. r,
  1306. g,
  1307. b,
  1308. a
  1309. } = props.strokeColor;
  1310. attrs(this._path, {
  1311. fill: 'none',
  1312. stroke: `rgba(${r},${g},${b},${a})`,
  1313. 'stroke-width': props.strokeWidth.toString(),
  1314. 'stroke-linecap': 'round',
  1315. 'stroke-linejoin': 'round'
  1316. });
  1317. }
  1318. if (props.opacity !== ((_this$_oldProps3 = this._oldProps) === null || _this$_oldProps3 === void 0 ? void 0 : _this$_oldProps3.opacity)) {
  1319. attr(this._path, 'opacity', props.opacity.toString());
  1320. }
  1321. if (props.points !== ((_this$_oldProps4 = this._oldProps) === null || _this$_oldProps4 === void 0 ? void 0 : _this$_oldProps4.points)) {
  1322. attr(this._path, 'd', getPathString(props.points));
  1323. }
  1324. this._oldProps = props;
  1325. }
  1326. destroy() {
  1327. removeElm(this._path);
  1328. }
  1329. }
  1330. class HanziWriterRenderer {
  1331. constructor(character, positioner) {
  1332. this._character = character;
  1333. this._positioner = positioner;
  1334. this._mainCharRenderer = new CharacterRenderer(character);
  1335. this._outlineCharRenderer = new CharacterRenderer(character);
  1336. this._highlightCharRenderer = new CharacterRenderer(character);
  1337. this._userStrokeRenderers = {};
  1338. }
  1339. mount(target) {
  1340. const positionedTarget = target.createSubRenderTarget();
  1341. const group = positionedTarget.svg;
  1342. const {
  1343. xOffset,
  1344. yOffset,
  1345. height,
  1346. scale
  1347. } = this._positioner;
  1348. attr(group, 'transform', `translate(${xOffset}, ${height - yOffset}) scale(${scale}, ${-1 * scale})`);
  1349. this._outlineCharRenderer.mount(positionedTarget);
  1350. this._mainCharRenderer.mount(positionedTarget);
  1351. this._highlightCharRenderer.mount(positionedTarget);
  1352. this._positionedTarget = positionedTarget;
  1353. }
  1354. render(props) {
  1355. const {
  1356. main,
  1357. outline,
  1358. highlight
  1359. } = props.character;
  1360. const {
  1361. outlineColor,
  1362. radicalColor,
  1363. highlightColor,
  1364. strokeColor,
  1365. drawingWidth,
  1366. drawingColor
  1367. } = props.options;
  1368. this._outlineCharRenderer.render({
  1369. opacity: outline.opacity,
  1370. strokes: outline.strokes,
  1371. strokeColor: outlineColor
  1372. });
  1373. this._mainCharRenderer.render({
  1374. opacity: main.opacity,
  1375. strokes: main.strokes,
  1376. strokeColor,
  1377. radicalColor: radicalColor
  1378. });
  1379. this._highlightCharRenderer.render({
  1380. opacity: highlight.opacity,
  1381. strokes: highlight.strokes,
  1382. strokeColor: highlightColor
  1383. });
  1384. const userStrokes = props.userStrokes || {};
  1385. for (const userStrokeId in this._userStrokeRenderers) {
  1386. if (!userStrokes[userStrokeId]) {
  1387. var _this$_userStrokeRend;
  1388. (_this$_userStrokeRend = this._userStrokeRenderers[userStrokeId]) === null || _this$_userStrokeRend === void 0 ? void 0 : _this$_userStrokeRend.destroy();
  1389. delete this._userStrokeRenderers[userStrokeId];
  1390. }
  1391. }
  1392. for (const userStrokeId in userStrokes) {
  1393. const stroke = userStrokes[userStrokeId];
  1394. if (!stroke) {
  1395. continue;
  1396. }
  1397. const userStrokeProps = {
  1398. strokeWidth: drawingWidth,
  1399. strokeColor: drawingColor,
  1400. ...stroke
  1401. };
  1402. const strokeRenderer = (() => {
  1403. if (this._userStrokeRenderers[userStrokeId]) {
  1404. return this._userStrokeRenderers[userStrokeId];
  1405. }
  1406. const newStrokeRenderer = new UserStrokeRenderer();
  1407. newStrokeRenderer.mount(this._positionedTarget);
  1408. this._userStrokeRenderers[userStrokeId] = newStrokeRenderer;
  1409. return newStrokeRenderer;
  1410. })();
  1411. strokeRenderer.render(userStrokeProps);
  1412. }
  1413. }
  1414. destroy() {
  1415. removeElm(this._positionedTarget.svg);
  1416. this._positionedTarget.defs.innerHTML = '';
  1417. }
  1418. }
  1419. /** Generic render target */
  1420. class RenderTargetBase {
  1421. constructor(node) {
  1422. this.node = node;
  1423. }
  1424. addPointerStartListener(callback) {
  1425. this.node.addEventListener('mousedown', evt => {
  1426. callback(this._eventify(evt, this._getMousePoint));
  1427. });
  1428. this.node.addEventListener('touchstart', evt => {
  1429. callback(this._eventify(evt, this._getTouchPoint));
  1430. });
  1431. }
  1432. addPointerMoveListener(callback) {
  1433. this.node.addEventListener('mousemove', evt => {
  1434. callback(this._eventify(evt, this._getMousePoint));
  1435. });
  1436. this.node.addEventListener('touchmove', evt => {
  1437. callback(this._eventify(evt, this._getTouchPoint));
  1438. });
  1439. }
  1440. addPointerEndListener(callback) {
  1441. // TODO: find a way to not need global listeners
  1442. document.addEventListener('mouseup', callback);
  1443. document.addEventListener('touchend', callback);
  1444. }
  1445. getBoundingClientRect() {
  1446. return this.node.getBoundingClientRect();
  1447. }
  1448. updateDimensions(width, height) {
  1449. this.node.setAttribute('width', `${width}`);
  1450. this.node.setAttribute('height', `${height}`);
  1451. }
  1452. _eventify(evt, pointFunc) {
  1453. return {
  1454. getPoint: () => pointFunc.call(this, evt),
  1455. preventDefault: () => evt.preventDefault()
  1456. };
  1457. }
  1458. _getMousePoint(evt) {
  1459. const {
  1460. left,
  1461. top
  1462. } = this.getBoundingClientRect();
  1463. const x = evt.clientX - left;
  1464. const y = evt.clientY - top;
  1465. return {
  1466. x,
  1467. y
  1468. };
  1469. }
  1470. _getTouchPoint(evt) {
  1471. const {
  1472. left,
  1473. top
  1474. } = this.getBoundingClientRect();
  1475. const x = evt.touches[0].clientX - left;
  1476. const y = evt.touches[0].clientY - top;
  1477. return {
  1478. x,
  1479. y
  1480. };
  1481. }
  1482. }
  1483. class RenderTarget extends RenderTargetBase {
  1484. constructor(svg, defs) {
  1485. super(svg);
  1486. this.svg = svg;
  1487. this.defs = defs;
  1488. if ('createSVGPoint' in svg) {
  1489. this._pt = svg.createSVGPoint();
  1490. }
  1491. }
  1492. static init(elmOrId, width = '100%', height = '100%') {
  1493. const element = (() => {
  1494. if (typeof elmOrId === 'string') {
  1495. return document.getElementById(elmOrId);
  1496. }
  1497. return elmOrId;
  1498. })();
  1499. if (!element) {
  1500. throw new Error(`HanziWriter target element not found: ${elmOrId}`);
  1501. }
  1502. const nodeType = element.nodeName.toUpperCase();
  1503. const svg = (() => {
  1504. if (nodeType === 'SVG' || nodeType === 'G') {
  1505. return element;
  1506. } else {
  1507. const svg = createElm('svg');
  1508. element.appendChild(svg);
  1509. return svg;
  1510. }
  1511. })();
  1512. attrs(svg, {
  1513. width,
  1514. height
  1515. });
  1516. const defs = createElm('defs');
  1517. svg.appendChild(defs);
  1518. return new RenderTarget(svg, defs);
  1519. }
  1520. createSubRenderTarget() {
  1521. const group = createElm('g');
  1522. this.svg.appendChild(group);
  1523. return new RenderTarget(group, this.defs);
  1524. }
  1525. _getMousePoint(evt) {
  1526. if (this._pt) {
  1527. this._pt.x = evt.clientX;
  1528. this._pt.y = evt.clientY;
  1529. if ('getScreenCTM' in this.node) {
  1530. var _this$node$getScreenC;
  1531. const localPt = this._pt.matrixTransform((_this$node$getScreenC = this.node.getScreenCTM()) === null || _this$node$getScreenC === void 0 ? void 0 : _this$node$getScreenC.inverse());
  1532. return {
  1533. x: localPt.x,
  1534. y: localPt.y
  1535. };
  1536. }
  1537. }
  1538. return super._getMousePoint.call(this, evt);
  1539. }
  1540. _getTouchPoint(evt) {
  1541. if (this._pt) {
  1542. this._pt.x = evt.touches[0].clientX;
  1543. this._pt.y = evt.touches[0].clientY;
  1544. if ('getScreenCTM' in this.node) {
  1545. var _this$node$getScreenC2;
  1546. const localPt = this._pt.matrixTransform((_this$node$getScreenC2 = this.node.getScreenCTM()) === null || _this$node$getScreenC2 === void 0 ? void 0 : _this$node$getScreenC2.inverse());
  1547. return {
  1548. x: localPt.x,
  1549. y: localPt.y
  1550. };
  1551. }
  1552. }
  1553. return super._getTouchPoint(evt);
  1554. }
  1555. }
  1556. var svgRenderer = {
  1557. HanziWriterRenderer,
  1558. createRenderTarget: RenderTarget.init
  1559. };
  1560. const drawPath = (ctx, points) => {
  1561. ctx.beginPath();
  1562. const start = points[0];
  1563. const remainingPoints = points.slice(1);
  1564. ctx.moveTo(start.x, start.y);
  1565. for (const point of remainingPoints) {
  1566. ctx.lineTo(point.x, point.y);
  1567. }
  1568. ctx.stroke();
  1569. };
  1570. /**
  1571. * Break a path string into a series of canvas path commands
  1572. *
  1573. * Note: only works with the subset of SVG paths used by MakeMeAHanzi data
  1574. * @param pathString
  1575. */
  1576. const pathStringToCanvas = pathString => {
  1577. const pathParts = pathString.split(/(^|\s+)(?=[A-Z])/).filter(part => part !== ' ');
  1578. const commands = [ctx => ctx.beginPath()];
  1579. for (const part of pathParts) {
  1580. const [cmd, ...rawParams] = part.split(/\s+/);
  1581. const params = rawParams.map(param => parseFloat(param));
  1582. if (cmd === 'M') {
  1583. commands.push(ctx => ctx.moveTo(...params));
  1584. } else if (cmd === 'L') {
  1585. commands.push(ctx => ctx.lineTo(...params));
  1586. } else if (cmd === 'C') {
  1587. commands.push(ctx => ctx.bezierCurveTo(...params));
  1588. } else if (cmd === 'Q') {
  1589. commands.push(ctx => ctx.quadraticCurveTo(...params));
  1590. } else ;
  1591. }
  1592. return ctx => commands.forEach(cmd => cmd(ctx));
  1593. };
  1594. /** this is a stroke composed of several stroke parts */
  1595. class StrokeRenderer$1 extends StrokeRendererBase {
  1596. constructor(stroke, usePath2D = true) {
  1597. super(stroke);
  1598. if (usePath2D && Path2D) {
  1599. this._path2D = new Path2D(this.stroke.path);
  1600. } else {
  1601. this._pathCmd = pathStringToCanvas(this.stroke.path);
  1602. }
  1603. this._extendedMaskPoints = extendStart(this.stroke.points, StrokeRendererBase.STROKE_WIDTH / 2);
  1604. }
  1605. render(ctx, props) {
  1606. if (props.opacity < 0.05) {
  1607. return;
  1608. }
  1609. ctx.save();
  1610. if (this._path2D) {
  1611. ctx.clip(this._path2D);
  1612. } else {
  1613. var _this$_pathCmd;
  1614. (_this$_pathCmd = this._pathCmd) === null || _this$_pathCmd === void 0 ? void 0 : _this$_pathCmd.call(this, ctx); // wechat bugs out if the clip path isn't stroked or filled
  1615. ctx.globalAlpha = 0;
  1616. ctx.stroke();
  1617. ctx.clip();
  1618. }
  1619. const {
  1620. r,
  1621. g,
  1622. b,
  1623. a
  1624. } = this._getColor(props);
  1625. const color = a === 1 ? `rgb(${r},${g},${b})` : `rgb(${r},${g},${b},${a})`;
  1626. const dashOffset = this._getStrokeDashoffset(props.displayPortion);
  1627. ctx.globalAlpha = props.opacity;
  1628. ctx.strokeStyle = color;
  1629. ctx.fillStyle = color;
  1630. ctx.lineWidth = StrokeRendererBase.STROKE_WIDTH;
  1631. ctx.lineCap = 'round';
  1632. ctx.lineJoin = 'round'; // wechat sets dashOffset as a second param here. Should be harmless for browsers to add here too
  1633. // @ts-ignore
  1634. ctx.setLineDash([this._pathLength, this._pathLength], dashOffset);
  1635. ctx.lineDashOffset = dashOffset;
  1636. drawPath(ctx, this._extendedMaskPoints);
  1637. ctx.restore();
  1638. }
  1639. }
  1640. class CharacterRenderer$1 {
  1641. constructor(character) {
  1642. this._strokeRenderers = character.strokes.map(stroke => new StrokeRenderer$1(stroke));
  1643. }
  1644. render(ctx, props) {
  1645. if (props.opacity < 0.05) return;
  1646. const {
  1647. opacity,
  1648. strokeColor,
  1649. radicalColor,
  1650. strokes
  1651. } = props;
  1652. for (let i = 0; i < this._strokeRenderers.length; i++) {
  1653. this._strokeRenderers[i].render(ctx, {
  1654. strokeColor,
  1655. radicalColor,
  1656. opacity: strokes[i].opacity * opacity,
  1657. displayPortion: strokes[i].displayPortion || 0
  1658. });
  1659. }
  1660. }
  1661. }
  1662. function renderUserStroke(ctx, props) {
  1663. if (props.opacity < 0.05) {
  1664. return;
  1665. }
  1666. const {
  1667. opacity,
  1668. strokeWidth,
  1669. strokeColor,
  1670. points
  1671. } = props;
  1672. const {
  1673. r,
  1674. g,
  1675. b,
  1676. a
  1677. } = strokeColor;
  1678. ctx.save();
  1679. ctx.globalAlpha = opacity;
  1680. ctx.lineWidth = strokeWidth;
  1681. ctx.strokeStyle = `rgba(${r},${g},${b},${a})`;
  1682. ctx.lineCap = 'round';
  1683. ctx.lineJoin = 'round';
  1684. drawPath(ctx, points);
  1685. ctx.restore();
  1686. }
  1687. class HanziWriterRenderer$1 {
  1688. constructor(character, positioner) {
  1689. this.destroy = noop;
  1690. this._character = character;
  1691. this._positioner = positioner;
  1692. this._mainCharRenderer = new CharacterRenderer$1(character);
  1693. this._outlineCharRenderer = new CharacterRenderer$1(character);
  1694. this._highlightCharRenderer = new CharacterRenderer$1(character);
  1695. }
  1696. mount(target) {
  1697. this._target = target;
  1698. }
  1699. _animationFrame(cb) {
  1700. const {
  1701. width,
  1702. height,
  1703. scale,
  1704. xOffset,
  1705. yOffset
  1706. } = this._positioner;
  1707. const ctx = this._target.getContext();
  1708. ctx.clearRect(0, 0, width, height);
  1709. ctx.save();
  1710. ctx.translate(xOffset, height - yOffset);
  1711. ctx.transform(1, 0, 0, -1, 0, 0);
  1712. ctx.scale(scale, scale);
  1713. cb(ctx);
  1714. ctx.restore(); // @ts-expect-error Verify if this is still needed for the "wechat miniprogram".
  1715. if (ctx.draw) {
  1716. // @ts-expect-error
  1717. ctx.draw();
  1718. }
  1719. }
  1720. render(props) {
  1721. const {
  1722. outline,
  1723. main,
  1724. highlight
  1725. } = props.character;
  1726. const {
  1727. outlineColor,
  1728. strokeColor,
  1729. radicalColor,
  1730. highlightColor,
  1731. drawingColor,
  1732. drawingWidth
  1733. } = props.options;
  1734. this._animationFrame(ctx => {
  1735. this._outlineCharRenderer.render(ctx, {
  1736. opacity: outline.opacity,
  1737. strokes: outline.strokes,
  1738. strokeColor: outlineColor
  1739. });
  1740. this._mainCharRenderer.render(ctx, {
  1741. opacity: main.opacity,
  1742. strokes: main.strokes,
  1743. strokeColor: strokeColor,
  1744. radicalColor: radicalColor
  1745. });
  1746. this._highlightCharRenderer.render(ctx, {
  1747. opacity: highlight.opacity,
  1748. strokes: highlight.strokes,
  1749. strokeColor: highlightColor
  1750. });
  1751. const userStrokes = props.userStrokes || {};
  1752. for (const userStrokeId in userStrokes) {
  1753. const userStroke = userStrokes[userStrokeId];
  1754. if (userStroke) {
  1755. const userStrokeProps = {
  1756. strokeWidth: drawingWidth,
  1757. strokeColor: drawingColor,
  1758. ...userStroke
  1759. };
  1760. renderUserStroke(ctx, userStrokeProps);
  1761. }
  1762. }
  1763. });
  1764. }
  1765. }
  1766. class RenderTarget$1 extends RenderTargetBase {
  1767. constructor(canvas) {
  1768. super(canvas);
  1769. }
  1770. static init(elmOrId, width = '100%', height = '100%') {
  1771. const element = (() => {
  1772. if (typeof elmOrId === 'string') {
  1773. return document.getElementById(elmOrId);
  1774. }
  1775. return elmOrId;
  1776. })();
  1777. if (!element) {
  1778. throw new Error(`HanziWriter target element not found: ${elmOrId}`);
  1779. }
  1780. const nodeType = element.nodeName.toUpperCase();
  1781. const canvas = (() => {
  1782. if (nodeType === 'CANVAS') {
  1783. return element;
  1784. }
  1785. const canvas = document.createElement('canvas');
  1786. element.appendChild(canvas);
  1787. return canvas;
  1788. })();
  1789. canvas.setAttribute('width', width);
  1790. canvas.setAttribute('height', height);
  1791. return new RenderTarget$1(canvas);
  1792. }
  1793. getContext() {
  1794. return this.node.getContext('2d');
  1795. }
  1796. }
  1797. var canvasRenderer = {
  1798. HanziWriterRenderer: HanziWriterRenderer$1,
  1799. createRenderTarget: RenderTarget$1.init
  1800. };
  1801. const VERSION = '2.0';
  1802. const getCharDataUrl = char => `https://cdn.jsdelivr.net/npm/hanzi-writer-data@${VERSION}/${char}.json`;
  1803. const defaultCharDataLoader = (char, onLoad, onError) => {
  1804. // load char data from hanziwriter cdn (currently hosted on jsdelivr)
  1805. const xhr = new XMLHttpRequest();
  1806. if (xhr.overrideMimeType) {
  1807. // IE 9 and 10 don't seem to support this...
  1808. xhr.overrideMimeType('application/json');
  1809. }
  1810. xhr.open('GET', getCharDataUrl(char), true);
  1811. xhr.onerror = event => {
  1812. onError(xhr, event);
  1813. };
  1814. xhr.onreadystatechange = () => {
  1815. // TODO: error handling
  1816. if (xhr.readyState !== 4) return;
  1817. if (xhr.status === 200) {
  1818. onLoad(JSON.parse(xhr.responseText));
  1819. } else if (xhr.status !== 0 && onError) {
  1820. onError(xhr);
  1821. }
  1822. };
  1823. xhr.send(null);
  1824. };
  1825. const defaultOptions = {
  1826. charDataLoader: defaultCharDataLoader,
  1827. onLoadCharDataError: null,
  1828. onLoadCharDataSuccess: null,
  1829. showOutline: true,
  1830. showCharacter: true,
  1831. renderer: 'svg',
  1832. // positioning options
  1833. width: 0,
  1834. height: 0,
  1835. padding: 20,
  1836. // animation options
  1837. strokeAnimationSpeed: 1,
  1838. strokeFadeDuration: 400,
  1839. strokeHighlightDuration: 200,
  1840. strokeHighlightSpeed: 2,
  1841. delayBetweenStrokes: 1000,
  1842. delayBetweenLoops: 2000,
  1843. // colors
  1844. strokeColor: '#555',
  1845. radicalColor: null,
  1846. highlightColor: '#AAF',
  1847. outlineColor: '#DDD',
  1848. drawingColor: '#333',
  1849. // quiz options
  1850. leniency: 1,
  1851. showHintAfterMisses: 3,
  1852. highlightOnComplete: true,
  1853. highlightCompleteColor: null,
  1854. markStrokeCorrectAfterMisses: false,
  1855. acceptBackwardsStrokes: false,
  1856. quizStartStrokeNum: 0,
  1857. averageDistanceThreshold: 350,
  1858. // undocumented obscure options
  1859. drawingFadeDuration: 300,
  1860. drawingWidth: 4,
  1861. strokeWidth: 2,
  1862. outlineWidth: 2,
  1863. rendererOverride: {}
  1864. };
  1865. class LoadingManager {
  1866. constructor(options) {
  1867. this._loadCounter = 0;
  1868. this._isLoading = false;
  1869. /** use this to attribute to determine if there was a problem with loading */
  1870. this.loadingFailed = false;
  1871. this._options = options;
  1872. }
  1873. _debouncedLoad(char, count) {
  1874. // these wrappers ignore all responses except the most recent.
  1875. const wrappedResolve = data => {
  1876. if (count === this._loadCounter) {
  1877. var _this$_resolve;
  1878. (_this$_resolve = this._resolve) === null || _this$_resolve === void 0 ? void 0 : _this$_resolve.call(this, data);
  1879. }
  1880. };
  1881. const wrappedReject = reason => {
  1882. if (count === this._loadCounter) {
  1883. var _this$_reject;
  1884. (_this$_reject = this._reject) === null || _this$_reject === void 0 ? void 0 : _this$_reject.call(this, reason);
  1885. }
  1886. };
  1887. const returnedData = this._options.charDataLoader(char, wrappedResolve, wrappedReject);
  1888. if (returnedData) {
  1889. if ('then' in returnedData) {
  1890. returnedData.then(wrappedResolve).catch(wrappedReject);
  1891. } else {
  1892. wrappedResolve(returnedData);
  1893. }
  1894. }
  1895. }
  1896. _setupLoadingPromise() {
  1897. return new Promise((resolve, reject) => {
  1898. this._resolve = resolve;
  1899. this._reject = reject;
  1900. }).then(data => {
  1901. var _this$_options$onLoad, _this$_options;
  1902. this._isLoading = false;
  1903. (_this$_options$onLoad = (_this$_options = this._options).onLoadCharDataSuccess) === null || _this$_options$onLoad === void 0 ? void 0 : _this$_options$onLoad.call(_this$_options, data);
  1904. return data;
  1905. }).catch(reason => {
  1906. this._isLoading = false;
  1907. this.loadingFailed = true; // If the user has provided an "onLoadCharDataError", call this function
  1908. // Otherwise, throw the promise
  1909. if (this._options.onLoadCharDataError) {
  1910. this._options.onLoadCharDataError(reason);
  1911. return;
  1912. } // If error callback wasn't provided, throw an error so the developer will be aware something went wrong
  1913. if (reason instanceof Error) {
  1914. throw reason;
  1915. }
  1916. const err = new Error(`Failed to load char data for ${this._loadingChar}`);
  1917. err.reason = reason;
  1918. throw err;
  1919. });
  1920. }
  1921. loadCharData(char) {
  1922. this._loadingChar = char;
  1923. const promise = this._setupLoadingPromise();
  1924. this.loadingFailed = false;
  1925. this._isLoading = true;
  1926. this._loadCounter++;
  1927. this._debouncedLoad(char, this._loadCounter);
  1928. return promise;
  1929. }
  1930. }
  1931. class HanziWriter {
  1932. constructor(element, options = {}) {
  1933. const {
  1934. HanziWriterRenderer,
  1935. createRenderTarget
  1936. } = options.renderer === 'canvas' ? canvasRenderer : svgRenderer;
  1937. const rendererOverride = options.rendererOverride || {};
  1938. this._renderer = {
  1939. HanziWriterRenderer: rendererOverride.HanziWriterRenderer || HanziWriterRenderer,
  1940. createRenderTarget: rendererOverride.createRenderTarget || createRenderTarget
  1941. }; // wechat miniprogram component needs direct access to the render target, so this is public
  1942. this.target = this._renderer.createRenderTarget(element, options.width, options.height);
  1943. this._options = this._assignOptions(options);
  1944. this._loadingManager = new LoadingManager(this._options);
  1945. this._setupListeners();
  1946. }
  1947. /** Main entry point */
  1948. static create(element, character, options) {
  1949. const writer = new HanziWriter(element, options);
  1950. writer.setCharacter(character);
  1951. return writer;
  1952. }
  1953. static loadCharacterData(character, options = {}) {
  1954. const loadingManager = (() => {
  1955. const {
  1956. _loadingManager,
  1957. _loadingOptions
  1958. } = HanziWriter;
  1959. if ((_loadingManager === null || _loadingManager === void 0 ? void 0 : _loadingManager._loadingChar) === character && _loadingOptions === options) {
  1960. return _loadingManager;
  1961. }
  1962. return new LoadingManager({ ...defaultOptions,
  1963. ...options
  1964. });
  1965. })();
  1966. HanziWriter._loadingManager = loadingManager;
  1967. HanziWriter._loadingOptions = options;
  1968. return loadingManager.loadCharData(character);
  1969. }
  1970. static getScalingTransform(width, height, padding = 0) {
  1971. const positioner = new Positioner({
  1972. width,
  1973. height,
  1974. padding
  1975. });
  1976. return {
  1977. x: positioner.xOffset,
  1978. y: positioner.yOffset,
  1979. scale: positioner.scale,
  1980. transform: trim(`
  1981. translate(${positioner.xOffset}, ${positioner.height - positioner.yOffset})
  1982. scale(${positioner.scale}, ${-1 * positioner.scale})
  1983. `).replace(/\s+/g, ' ')
  1984. };
  1985. }
  1986. showCharacter(options = {}) {
  1987. this._options.showCharacter = true;
  1988. return this._withData(() => {
  1989. var _this$_renderState;
  1990. return (_this$_renderState = this._renderState) === null || _this$_renderState === void 0 ? void 0 : _this$_renderState.run(showCharacter('main', this._character, typeof options.duration === 'number' ? options.duration : this._options.strokeFadeDuration)).then(res => {
  1991. var _options$onComplete;
  1992. (_options$onComplete = options.onComplete) === null || _options$onComplete === void 0 ? void 0 : _options$onComplete.call(options, res);
  1993. return res;
  1994. });
  1995. });
  1996. }
  1997. hideCharacter(options = {}) {
  1998. this._options.showCharacter = false;
  1999. return this._withData(() => {
  2000. var _this$_renderState2;
  2001. return (_this$_renderState2 = this._renderState) === null || _this$_renderState2 === void 0 ? void 0 : _this$_renderState2.run(hideCharacter('main', this._character, typeof options.duration === 'number' ? options.duration : this._options.strokeFadeDuration)).then(res => {
  2002. var _options$onComplete2;
  2003. (_options$onComplete2 = options.onComplete) === null || _options$onComplete2 === void 0 ? void 0 : _options$onComplete2.call(options, res);
  2004. return res;
  2005. });
  2006. });
  2007. }
  2008. animateCharacter(options = {}) {
  2009. this.cancelQuiz();
  2010. return this._withData(() => {
  2011. var _this$_renderState3;
  2012. return (_this$_renderState3 = this._renderState) === null || _this$_renderState3 === void 0 ? void 0 : _this$_renderState3.run(animateCharacter('main', this._character, this._options.strokeFadeDuration, this._options.strokeAnimationSpeed, this._options.delayBetweenStrokes)).then(res => {
  2013. var _options$onComplete3;
  2014. (_options$onComplete3 = options.onComplete) === null || _options$onComplete3 === void 0 ? void 0 : _options$onComplete3.call(options, res);
  2015. return res;
  2016. });
  2017. });
  2018. }
  2019. animateStroke(strokeNum, options = {}) {
  2020. this.cancelQuiz();
  2021. return this._withData(() => {
  2022. var _this$_renderState4;
  2023. return (_this$_renderState4 = this._renderState) === null || _this$_renderState4 === void 0 ? void 0 : _this$_renderState4.run(animateSingleStroke('main', this._character, fixIndex(strokeNum, this._character.strokes.length), this._options.strokeAnimationSpeed)).then(res => {
  2024. var _options$onComplete4;
  2025. (_options$onComplete4 = options.onComplete) === null || _options$onComplete4 === void 0 ? void 0 : _options$onComplete4.call(options, res);
  2026. return res;
  2027. });
  2028. });
  2029. }
  2030. highlightStroke(strokeNum, options = {}) {
  2031. const promise = () => {
  2032. if (!this._character || !this._renderState) {
  2033. return;
  2034. }
  2035. return this._renderState.run(highlightStroke(selectIndex(this._character.strokes, strokeNum), colorStringToVals(this._options.highlightColor), this._options.strokeHighlightSpeed)).then(res => {
  2036. var _options$onComplete5;
  2037. (_options$onComplete5 = options.onComplete) === null || _options$onComplete5 === void 0 ? void 0 : _options$onComplete5.call(options, res);
  2038. return res;
  2039. });
  2040. };
  2041. return this._withData(promise);
  2042. }
  2043. async loopCharacterAnimation() {
  2044. this.cancelQuiz();
  2045. return this._withData(() => this._renderState.run(animateCharacterLoop('main', this._character, this._options.strokeFadeDuration, this._options.strokeAnimationSpeed, this._options.delayBetweenStrokes, this._options.delayBetweenLoops), {
  2046. loop: true
  2047. }));
  2048. }
  2049. pauseAnimation() {
  2050. return this._withData(() => {
  2051. var _this$_renderState5;
  2052. return (_this$_renderState5 = this._renderState) === null || _this$_renderState5 === void 0 ? void 0 : _this$_renderState5.pauseAll();
  2053. });
  2054. }
  2055. resumeAnimation() {
  2056. return this._withData(() => {
  2057. var _this$_renderState6;
  2058. return (_this$_renderState6 = this._renderState) === null || _this$_renderState6 === void 0 ? void 0 : _this$_renderState6.resumeAll();
  2059. });
  2060. }
  2061. showOutline(options = {}) {
  2062. this._options.showOutline = true;
  2063. return this._withData(() => {
  2064. var _this$_renderState7;
  2065. return (_this$_renderState7 = this._renderState) === null || _this$_renderState7 === void 0 ? void 0 : _this$_renderState7.run(showCharacter('outline', this._character, typeof options.duration === 'number' ? options.duration : this._options.strokeFadeDuration)).then(res => {
  2066. var _options$onComplete6;
  2067. (_options$onComplete6 = options.onComplete) === null || _options$onComplete6 === void 0 ? void 0 : _options$onComplete6.call(options, res);
  2068. return res;
  2069. });
  2070. });
  2071. }
  2072. hideOutline(options = {}) {
  2073. this._options.showOutline = false;
  2074. return this._withData(() => {
  2075. var _this$_renderState8;
  2076. return (_this$_renderState8 = this._renderState) === null || _this$_renderState8 === void 0 ? void 0 : _this$_renderState8.run(hideCharacter('outline', this._character, typeof options.duration === 'number' ? options.duration : this._options.strokeFadeDuration)).then(res => {
  2077. var _options$onComplete7;
  2078. (_options$onComplete7 = options.onComplete) === null || _options$onComplete7 === void 0 ? void 0 : _options$onComplete7.call(options, res);
  2079. return res;
  2080. });
  2081. });
  2082. }
  2083. /** Updates the size of the writer instance without resetting render state */
  2084. updateDimensions({
  2085. width,
  2086. height,
  2087. padding
  2088. }) {
  2089. if (width !== undefined) this._options.width = width;
  2090. if (height !== undefined) this._options.height = height;
  2091. if (padding !== undefined) this._options.padding = padding;
  2092. this.target.updateDimensions(this._options.width, this._options.height); // if there's already a character drawn, destroy and recreate the renderer in the same state
  2093. if (this._character && this._renderState && this._hanziWriterRenderer && this._positioner) {
  2094. this._hanziWriterRenderer.destroy();
  2095. const hanziWriterRenderer = this._initAndMountHanziWriterRenderer(this._character); // TODO: this should probably implement EventEmitter instead of manually tracking updates like this
  2096. this._renderState.overwriteOnStateChange(nextState => hanziWriterRenderer.render(nextState));
  2097. hanziWriterRenderer.render(this._renderState.state); // update the current quiz as well, if one is active
  2098. if (this._quiz) {
  2099. this._quiz.setPositioner(this._positioner);
  2100. }
  2101. }
  2102. }
  2103. updateColor(colorName, colorVal, options = {}) {
  2104. var _options$duration;
  2105. let mutations = [];
  2106. const fixedColorVal = (() => {
  2107. // If we're removing radical color, tween it to the stroke color
  2108. if (colorName === 'radicalColor' && !colorVal) {
  2109. return this._options.strokeColor;
  2110. }
  2111. return colorVal;
  2112. })();
  2113. const mappedColor = colorStringToVals(fixedColorVal);
  2114. this._options[colorName] = colorVal;
  2115. const duration = (_options$duration = options.duration) !== null && _options$duration !== void 0 ? _options$duration : this._options.strokeFadeDuration;
  2116. mutations = mutations.concat(updateColor(colorName, mappedColor, duration)); // make sure to set radicalColor back to null after the transition finishes if val == null
  2117. if (colorName === 'radicalColor' && !colorVal) {
  2118. mutations = mutations.concat(updateColor(colorName, null, 0));
  2119. }
  2120. return this._withData(() => {
  2121. var _this$_renderState9;
  2122. return (_this$_renderState9 = this._renderState) === null || _this$_renderState9 === void 0 ? void 0 : _this$_renderState9.run(mutations).then(res => {
  2123. var _options$onComplete8;
  2124. (_options$onComplete8 = options.onComplete) === null || _options$onComplete8 === void 0 ? void 0 : _options$onComplete8.call(options, res);
  2125. return res;
  2126. });
  2127. });
  2128. }
  2129. quiz(quizOptions = {}) {
  2130. return this._withData(async () => {
  2131. if (this._character && this._renderState && this._positioner) {
  2132. this.cancelQuiz();
  2133. this._quiz = new Quiz(this._character, this._renderState, this._positioner);
  2134. this._options = { ...this._options,
  2135. ...quizOptions
  2136. };
  2137. this._quiz.startQuiz(this._options);
  2138. }
  2139. });
  2140. }
  2141. cancelQuiz() {
  2142. if (this._quiz) {
  2143. this._quiz.cancel();
  2144. this._quiz = undefined;
  2145. }
  2146. }
  2147. setCharacter(char) {
  2148. this.cancelQuiz();
  2149. this._char = char;
  2150. if (this._hanziWriterRenderer) {
  2151. this._hanziWriterRenderer.destroy();
  2152. }
  2153. if (this._renderState) {
  2154. this._renderState.cancelAll();
  2155. }
  2156. this._hanziWriterRenderer = null;
  2157. this._withDataPromise = this._loadingManager.loadCharData(char).then(pathStrings => {
  2158. // if "pathStrings" isn't set, ".catch()"" was probably called and loading likely failed
  2159. if (!pathStrings || this._loadingManager.loadingFailed) {
  2160. return;
  2161. }
  2162. this._character = parseCharData(char, pathStrings);
  2163. this._renderState = new RenderState(this._character, this._options, nextState => hanziWriterRenderer.render(nextState));
  2164. const hanziWriterRenderer = this._initAndMountHanziWriterRenderer(this._character);
  2165. hanziWriterRenderer.render(this._renderState.state);
  2166. });
  2167. return this._withDataPromise;
  2168. }
  2169. _initAndMountHanziWriterRenderer(character) {
  2170. const {
  2171. width,
  2172. height,
  2173. padding
  2174. } = this._options;
  2175. this._positioner = new Positioner({
  2176. width,
  2177. height,
  2178. padding
  2179. });
  2180. const hanziWriterRenderer = new this._renderer.HanziWriterRenderer(character, this._positioner);
  2181. hanziWriterRenderer.mount(this.target);
  2182. this._hanziWriterRenderer = hanziWriterRenderer;
  2183. return hanziWriterRenderer;
  2184. }
  2185. async getCharacterData() {
  2186. if (!this._char) {
  2187. throw new Error('setCharacter() must be called before calling getCharacterData()');
  2188. }
  2189. const character = await this._withData(() => this._character);
  2190. return character;
  2191. }
  2192. _assignOptions(options) {
  2193. const mergedOptions = { ...defaultOptions,
  2194. ...options
  2195. }; // backfill strokeAnimationSpeed if deprecated strokeAnimationDuration is provided instead
  2196. if (options.strokeAnimationDuration && !options.strokeAnimationSpeed) {
  2197. mergedOptions.strokeAnimationSpeed = 500 / options.strokeAnimationDuration;
  2198. }
  2199. if (options.strokeHighlightDuration && !options.strokeHighlightSpeed) {
  2200. mergedOptions.strokeHighlightSpeed = 500 / mergedOptions.strokeHighlightDuration;
  2201. }
  2202. if (!options.highlightCompleteColor) {
  2203. mergedOptions.highlightCompleteColor = mergedOptions.highlightColor;
  2204. }
  2205. return this._fillWidthAndHeight(mergedOptions);
  2206. }
  2207. /** returns a new options object with width and height filled in if missing */
  2208. _fillWidthAndHeight(options) {
  2209. const filledOpts = { ...options
  2210. };
  2211. if (filledOpts.width && !filledOpts.height) {
  2212. filledOpts.height = filledOpts.width;
  2213. } else if (filledOpts.height && !filledOpts.width) {
  2214. filledOpts.width = filledOpts.height;
  2215. } else if (!filledOpts.width && !filledOpts.height) {
  2216. const {
  2217. width,
  2218. height
  2219. } = this.target.getBoundingClientRect();
  2220. const minDim = Math.min(width, height);
  2221. filledOpts.width = minDim;
  2222. filledOpts.height = minDim;
  2223. }
  2224. return filledOpts;
  2225. }
  2226. _withData(func) {
  2227. // if this._loadingManager.loadingFailed, then loading failed before this method was called
  2228. if (this._loadingManager.loadingFailed) {
  2229. throw Error('Failed to load character data. Call setCharacter and try again.');
  2230. }
  2231. if (this._withDataPromise) {
  2232. return this._withDataPromise.then(() => {
  2233. if (!this._loadingManager.loadingFailed) {
  2234. return func();
  2235. }
  2236. });
  2237. }
  2238. return Promise.resolve().then(func);
  2239. }
  2240. _setupListeners() {
  2241. this.target.addPointerStartListener(evt => {
  2242. if (this._quiz) {
  2243. evt.preventDefault();
  2244. this._quiz.startUserStroke(evt.getPoint());
  2245. }
  2246. });
  2247. this.target.addPointerMoveListener(evt => {
  2248. if (this._quiz) {
  2249. evt.preventDefault();
  2250. this._quiz.continueUserStroke(evt.getPoint());
  2251. }
  2252. });
  2253. this.target.addPointerEndListener(() => {
  2254. var _this$_quiz;
  2255. (_this$_quiz = this._quiz) === null || _this$_quiz === void 0 ? void 0 : _this$_quiz.endUserStroke();
  2256. });
  2257. }
  2258. }
  2259. /** Singleton instance of LoadingManager. Only set in `loadCharacterData` */
  2260. HanziWriter._loadingManager = null;
  2261. /** Singleton loading options. Only set in `loadCharacterData` */
  2262. HanziWriter._loadingOptions = null;
  2263. return HanziWriter;
  2264. }());