WaniKani Nippongrammar Extension

Svg Canvas used to draw Kanji on reviews and lessons, using website code.

  1. // ==UserScript==
  2. // @name WaniKani Nippongrammar Extension
  3. // @namespace WK-nippongrammar
  4. // @version 0.11
  5. // @website http://nippongrammar.appspot.com/
  6. // @description Svg Canvas used to draw Kanji on reviews and lessons, using website code.
  7. // @author Code by Aaron Drew, Wanikani adaption by Ethan McCoy
  8. // @include *.wanikani.com/review/session*
  9. // @include *.wanikani.com/lesson/session*
  10. // @grant none
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14.  
  15.  
  16.  
  17. /**
  18. * Returns a function that animates a linear curve on a canvas and calls its first argument when complete.
  19. * @param {HTMLElement} canvas
  20. * @param {Array.<Number>} p0 - Initial coordinates
  21. * @param {Array.<Number>} p1 - Final coordinates
  22. * @param {Number} w0 - Initial stroke width
  23. * @param {Number} w1 - Final stroke width
  24. * @param {Number} milliseconds - time taken to draw the stroke in milliseconds
  25. */
  26. function linearCurve(canvas, p0, p1, w0, w1, milliseconds)
  27. {
  28. return function(success) {
  29. var t = 0;
  30. //canvas.beginPath();
  31. canvas.lineWidth = w0;
  32. canvas.moveTo(p0[0],p0[1]);
  33. var interval = setInterval(function() {
  34. // If there's less than 5ms left
  35. t += 5 / milliseconds;
  36. if(t > 1.0)
  37. {
  38. //finish drawing line and call success function
  39. canvas.lineWidth = w1;
  40. canvas.lineTo(p1[0],p1[1]);
  41. canvas.stroke();
  42. clearInterval(interval);
  43. success();
  44. }
  45. else
  46. // more than 5ms left. t is the portion of time left that equals 5ms
  47. {
  48.  
  49. var x = (1-t)*p0[0] + t*p1[0];
  50. var y = (1-t)*p0[1] + t*p1[1];
  51. canvas.lineWidth = w0 * (1 - t) + w1 * t;
  52. //Draw the line to the percentage along the line and percentage of widths
  53. canvas.lineTo(x,y);
  54. canvas.strokeStyle = "white";
  55. canvas.stroke();
  56. }
  57. }, 5);
  58. };
  59. }
  60.  
  61. /**
  62. * Returns a function that animates a bezier curve on a canvas and calls its first argument when complete.
  63. */
  64. function bezierCurve(canvas, p0, p1, p2, w0, w1, milliseconds)
  65. {
  66. return function(success) {
  67. var t = 0;
  68. //canvas.beginPath();
  69. canvas.lineWidth = w0;
  70. canvas.moveTo(p0[0],p0[1]);
  71.  
  72. var interval = setInterval(function() {
  73. t += 1 / milliseconds;
  74. if(t > 1.0)
  75. {
  76. canvas.lineWidth = w1;
  77. canvas.lineTo(p2[0],p2[1]);
  78. canvas.stroke();
  79. clearInterval(interval);
  80. success();
  81.  
  82. }
  83. else
  84. {
  85. var x = (1-t)*(1-t)*p0[0] + 2*(1-t)*t*p1[0] + t*t*p2[0];
  86. var y = (1-t)*(1-t)*p0[1] + 2*(1-t)*t*p1[1] + t*t*p2[1];
  87.  
  88. canvas.lineWidth = w0 * (1 - t) + w1 * t;
  89. canvas.lineTo(x,y);
  90. canvas.strokeStyle = "white";
  91. canvas.stroke();
  92. }
  93. }, 1);
  94. };
  95. }
  96.  
  97. /**
  98. * Returns a function that animates a quadratic curve on a canvas and calls its first argument when complete.
  99. */
  100. function quadraticCurve(canvas, p0, p1, p2, p3, w0, w1, milliseconds)
  101. {
  102. return function(success) {
  103. var t = 0;
  104. //canvas.beginPath();
  105. canvas.lineWidth = w0;
  106. canvas.moveTo(p0[0],p0[1]);
  107. var interval = setInterval(function() {
  108. t += 1 / milliseconds;
  109. if(t > 1.0)
  110. {
  111. canvas.lineWidth = w1;
  112. canvas.lineTo(p3[0],p3[1]);
  113. canvas.stroke();
  114. clearInterval(interval);
  115. success();
  116. }
  117. else
  118. {
  119. var x = (1-t)*(1-t)*(1-t)*p0[0] + 3*(1-t)*(1-t)*t*p1[0] + 3*(1-t)*t*t*p2[0] + t*t*t*p3[0];
  120. var y = (1-t)*(1-t)*(1-t)*p0[1] + 3*(1-t)*(1-t)*t*p1[1] + 3*(1-t)*t*t*p2[1] + t*t*t*p3[1];
  121. canvas.lineWidth = w0 * (1 - t) + w1 * t;
  122. canvas.lineTo(x,y);
  123. canvas.strokeStyle = "white";
  124. canvas.stroke();
  125. }
  126. }, 1);
  127. };
  128. }
  129.  
  130. function renderPath(canvas, path, millisecondsPerStroke, callback)
  131. {
  132. console.log ("rendering path: " + path);
  133. var upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  134. var lower = "abcdefghijklmnopqrstuvwxyz";
  135. var last = null;
  136. var strokes = [];
  137. // Parse the path string
  138. while(path.length) {
  139. var cmd = path.charAt(0);
  140. var i = 1;
  141. while(i < path.length) {
  142. var c = path.charAt(i);
  143. if((c>='A'&&c<='Z')||(c>='a'&&c<='z'))
  144. break;
  145. i++;
  146. }
  147. var data = path.substring(1, i);
  148. path = path.slice(i);
  149.  
  150. data = data.replace(/(\d)-(\d)/g, "$1,-$2").split(",");
  151. for(i=0;i<data.length;i++)
  152. data[i] = parseFloat(data[i]);
  153.  
  154. // convert from relative to absolute positions as needed.
  155. if(lower.indexOf(cmd) != -1)
  156. {
  157. for(i=0;i<data.length;i++)
  158. data[i] += last[i%2];
  159. cmd = upper.charAt(lower.indexOf(cmd));
  160. }
  161.  
  162. if(cmd == "L")
  163. strokes.push(linearCurve(canvas, last, data, 9, 2, millisecondsPerStroke));
  164. if(cmd == "S")
  165. strokes.push(bezierCurve(canvas, last, [data[0],data[1]], [data[2], data[3]], 9, 1, millisecondsPerStroke));
  166. if(cmd == "C")
  167. strokes.push(quadraticCurve(canvas, last, [data[0],data[1]], [data[2], data[3]], [data[4],data[5]], 9, 1, millisecondsPerStroke));
  168.  
  169. if(cmd == "M") {
  170. last = data;
  171. } else if(cmd == "L") {
  172. last = data;
  173. } else if(cmd == "S") {
  174. last = [data[2],data[3]];
  175. } else if(cmd == "C") {
  176. last = [data[4],data[5]];
  177. }
  178. }
  179.  
  180. function nextStroke() {
  181. if(strokes.length)
  182. {
  183. strokes.shift()(nextStroke);
  184. }
  185. else
  186. {
  187. if(typeof callback != "undefined")
  188. callback();
  189. }
  190. };
  191. nextStroke();
  192. };
  193.  
  194. function renderMoji(canvas, moji, millisecondsPerStroke)
  195. {
  196. return function(success, error) {
  197. kanjiSvg.getSvg(moji, function(moji, svg) {
  198. renderPath(canvas, svg, millisecondsPerStroke, success);
  199. }, function(k) {
  200. if(typeof(error) != "undefined") error();
  201. });
  202. };
  203. }
  204.  
  205. function renderMojiOrSpan(canvasElem, moji, millisecondsPerStroke)
  206. {
  207. return function(success) {
  208. renderMoji(canvasElem.getContext("2d"), moji, millisecondsPerStroke)(function() {
  209. success();
  210. }, function() {
  211. canvasElem.outerHTML = "<span>"+moji+"</span>";
  212. success();
  213. });
  214. };
  215. }
  216.  
  217. function animateWriting(txt, div_id, millisecondsPerStroke)
  218. {
  219. if (typeof div_id === 'string'){
  220. document.getElementById(div_id).innerHTML = "";
  221. }else{
  222. div_id.innerHTML = "";
  223. }
  224. var renderQueue = [];
  225.  
  226. function renderNext() {
  227. if(renderQueue.length)
  228. renderQueue.shift()();
  229. }
  230.  
  231. for(var i=0;i<txt.length;i++) {
  232. var ch = txt.charAt(i);
  233. var canvas = document.createElement("canvas");
  234. canvas.width = 110;
  235. canvas.height = 110;
  236. canvas.style.width = "1em";
  237. canvas.style.height = "1em";
  238.  
  239.  
  240. if (typeof div_id === 'string'){
  241. document.getElementById(div_id).appendChild(canvas);
  242. }else{
  243. div_id.appendChild(canvas);;
  244. }
  245.  
  246. renderQueue.push(renderMojiOrSpan(canvas, ch, millisecondsPerStroke));
  247. }
  248.  
  249. function nextStroke() {
  250. if(renderQueue.length)
  251. {
  252. renderQueue.shift()(nextStroke, function() {
  253. nextStroke();
  254. });
  255. }
  256. };
  257. nextStroke();
  258. };
  259.  
  260.  
  261.  
  262. //"use strict";
  263. //Load resources KanjiSVG and renderer.
  264. $("head").prepend('<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/kanjisvg@0.2.1/kanjiSVG.js"></script>');
  265.  
  266. // Don't need to load this anymore, just pasted the code above.
  267. //$("head").prepend('<script type="text/javascript" src="https://gf.qytechs.cn/scripts/11951-kanjirender/code/kanjirender.js"></script>');
  268.  
  269. //NipponGrammar class object
  270. var NG = {
  271. //Flag to keep track of state, whether canvas is shown or not.
  272. strokesShown: true,
  273. //html div that holds the canvases, each character has its own canvas
  274. strokeDiv: document.getElementById('strokeChar'),
  275.  
  276. //function to show the canvas kanji
  277. showStrokes : function(){
  278. //set the flag if false, setting it to true if true does nothing. We want to be sure it is true.
  279. this.strokesShown = true;
  280. this.strokeDiv && (this.strokeDiv.style.display = "block");
  281. this.charDiv && (this.charDiv.style.display = "none");
  282. },
  283. //function to hide the canvas and show the original kanji
  284. showOriginal: function(){
  285. this.strokesShown = false;
  286. this.strokeDiv && (this.strokeDiv.style.display = "none");
  287. this.charDiv && (this.charDiv.style.display = "block");
  288. },
  289.  
  290. showImage: function(){
  291. //as above but don't change strokesShown flag.
  292. //This is used for radicals that are images, there are no canvases to show, but we do not want to change the default behaviour of whether we use them or not for other items
  293. this.strokeDiv && (this.strokeDiv.style.display = "none");
  294. this.charDiv && (this.charDiv.style.display = "block");
  295. },
  296.  
  297. animateStrokes: function(text){
  298. //If the document doesn't have a div with the id 'strokeChar', make one and put it in the appropriate spot.
  299. if (this.strokeDiv === null){
  300. this.strokeDiv = document.createElement('div');
  301. //this.strokeDiv.style = "padding: 20px 20px 0; height: 110px; width: 110px",
  302. this.strokeDiv.id = "strokeChar";
  303.  
  304. //charDiv is set when a new quiz is loaded. Lessons and reviews are slightly different layout, so it retrieves it differently for each
  305. this.charDiv && this.charDiv.parentNode.insertBefore(this.strokeDiv, this.charDiv);
  306. }
  307.  
  308. //I forgot if there's a reason the border is set here
  309. this.strokeDiv.style.border = "1px";
  310.  
  311. //add replay functionality by removing and reapplying click listeners
  312. this.strokeDiv.removeEventListener("click", handlers.onDivClick);
  313. this.strokeDiv.addEventListener("click", handlers.onDivClick);
  314.  
  315. //function from kanjirender.js. text is the string, strokeChar is id of the div containing the canvases and 10 is the speed to animate the strokes
  316. animateWriting(text,'strokeChar',10);
  317.  
  318. //Default is to show the strokes. run the function initially if flag set. Function will again set the flag because it needs to when called elsewhere.
  319. if (this.strokesShown){
  320. console.debug("showing strokes");
  321. this.showStrokes();
  322. }
  323. },
  324. };
  325.  
  326. //Handler functions for mouse events, new quizItem events, and hotkeys
  327. var handlers = {
  328. switchViews: function(e){
  329. //Shift and Left Arrow
  330. e.shiftKey && e.keyCode === 37 && NG.showStrokes();
  331. e.shiftKey && e.keyCode === 39 && NG.showOriginal();
  332. },
  333.  
  334. onDivClick: function(){
  335. NG.animateStrokes(NG.text||"");
  336. },
  337.  
  338. handleKeyChange: function(key, action){
  339.  
  340. console.groupCollapsed("animate strokes userscript");
  341.  
  342. switch (key){
  343. case "l/currentLesson":
  344. case "l/currentQuizItem":
  345. //for lessons and their following quizzes
  346. NG.charDiv = document.getElementById("character");
  347. break;
  348. case "currentItem":
  349. //for reviews
  350. var spanArr = document.getElementById("character").getElementsByTagName("span");
  351. NG.charDiv = spanArr[spanArr.length-1]; //animate strokes may insert spans for chars it doesn't know.
  352. break;
  353. }
  354. var cur = $.jStorage.get(key);
  355. NG.text = cur.voc || cur.kan || cur.rad || "";
  356. if (NG.text.indexOf(".png") === -1) { //weed out picture radicals for now, extend svg library later
  357. NG.animateStrokes(NG.text);
  358. //introduce hotkey switching
  359. document.addEventListener("keyup", handlers.switchViews);
  360. }else{
  361. //remove hotkey switching
  362. document.removeEventListener("keyup", handlers.switchViews);
  363. NG.showImage();
  364. }
  365. console.groupEnd();
  366. }
  367. };
  368.  
  369. var handleLevelsPage = function(){
  370. };
  371.  
  372.  
  373.  
  374.  
  375. function main() {
  376.  
  377. if (document.URL.match(/.wanikani.com\/level\//)){
  378. false&&handleLevelsPage();
  379. }else{
  380. $.jStorage.listenKeyChange("currentItem", handlers.handleKeyChange);
  381. $.jStorage.listenKeyChange("l/currentQuizItem", handlers.handleKeyChange);
  382. $.jStorage.listenKeyChange("l/currentLesson", handlers.handleKeyChange);
  383. }
  384.  
  385.  
  386. }
  387.  
  388. function animateWriting(txt, div_id, millisecondsPerStroke)
  389. {
  390. if (typeof div_id === 'string'){
  391. document.getElementById(div_id).innerHTML = "";
  392. }else{
  393. div_id.innerHTML = "";
  394. }
  395. var renderQueue = [];
  396.  
  397. function renderNext() {
  398. if(renderQueue.length)
  399. renderQueue.shift()();
  400. }
  401.  
  402. for(var i=0;i<txt.length;i++) {
  403. var ch = txt.charAt(i);
  404. var canvas = document.createElement("canvas");
  405. canvas.width = 110;
  406. canvas.height = 110;
  407. canvas.style.width = "110";
  408. canvas.style.height = "110";
  409.  
  410. if (typeof div_id === 'string'){
  411. document.getElementById(div_id).appendChild(canvas);
  412. }else{
  413. div_id.appendChild(canvas);;
  414. }
  415.  
  416. renderQueue.push(renderMojiOrSpan(canvas, ch, millisecondsPerStroke));
  417. }
  418.  
  419. function nextStroke() {
  420. if(renderQueue.length)
  421. {
  422. renderQueue.shift()(nextStroke, function() {
  423. nextStroke();
  424. });
  425. }
  426. };
  427. nextStroke();
  428. };
  429.  
  430. if (document.readyState === 'complete')
  431. main();
  432. else
  433. window.addEventListener("load", main, false);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址