WaniKani Stroke Order

Shows a kanji's stroke order on its page and during lessons and reviews.

  1. // ==UserScript==
  2. // @name WaniKani Stroke Order
  3. // @namespace japanese
  4. // @version 1.1.22
  5. // @description Shows a kanji's stroke order on its page and during lessons and reviews.
  6. // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
  7. // @match https://www.wanikani.com/*
  8. // @match https://preview.wanikani.com/*
  9. // @author Looki, maintained by kind users on the forum
  10. // @grant GM_xmlhttpRequest
  11. // @connect jisho.org
  12. // @connect cloudfront.net
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js
  14. // @require https://gf.qytechs.cn/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1326536
  15.  
  16. // ==/UserScript==
  17.  
  18. /*
  19. * Thanks a lot to ...
  20. * Wanikani Phonetic-Semantic Composition - Userscript
  21. * by ruipgpinheiro (LordGravewish)
  22. * ... for code showing me how to insert sections during kanji reviews.
  23. * The code heavily borrows from that script!
  24. * Also thanks to Halo for a loading bug fix!
  25. */
  26.  
  27. ;(function () {
  28. /* global Snap */
  29.  
  30. /*
  31. * Helper Functions/Variables
  32. */
  33. let wkItemInfo = unsafeWindow.wkItemInfo
  34.  
  35. /*
  36. * Global Variables/Objects/Classes
  37. */
  38. const JISHO = 'https://jisho.org'
  39. const strokeOrderCss =
  40. '.stroke_order_diagram--bounding_box {fill: none; stroke: #ddd; stroke-width: 2; stroke-linecap: square; stroke-linejoin: square;}' +
  41. '.stroke_order_diagram--bounding_box {fill: none; stroke: #ddd; stroke-width: 2; stroke-linecap: square; stroke-linejoin: square;}' +
  42. '.stroke_order_diagram--existing_path {fill: none; stroke: #aaa; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;}' +
  43. '.stroke_order_diagram--current_path {fill: none; stroke: #000; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;}' +
  44. '.stroke_order_diagram--path_start {fill: rgba(255,0,0,0.7); stroke: none;}' +
  45. '.stroke_order_diagram--guide_line {fill: none; stroke: #ddd; stroke-width: 2; stroke-linecap: square; stroke-linejoin: square; stroke-dasharray: 5, 5;}'
  46.  
  47. init()
  48.  
  49. /*
  50. * Main
  51. */
  52. function init() {
  53. wkItemInfo.on('lesson').forType('kanji').under('composition').append('Stroke Order', loadDiagram)
  54. wkItemInfo
  55. .on('lessonQuiz, review, extraStudy, itemPage')
  56. .forType('kanji')
  57. .under('composition')
  58. .appendAtTop('Stroke Order', loadDiagram)
  59.  
  60. let style = document.createElement('style')
  61. style.textContent = strokeOrderCss
  62. document.head.appendChild(style)
  63. }
  64.  
  65. function xmlHttpRequest(urlText) {
  66. return new Promise((resolve, reject) =>
  67. GM_xmlhttpRequest({
  68. method: 'GET',
  69. url: urlText,
  70. onload: (xhr) => {
  71. xhr.status === 200 ? resolve(xhr) : reject(xhr.responseText)
  72. },
  73. onerror: (xhr) => {
  74. reject(xhr.responseText)
  75. },
  76. }),
  77. )
  78. }
  79.  
  80. /*
  81. * Adds the diagram section element to the appropriate location
  82. */
  83. async function loadDiagram(injectorState) {
  84. let xhr = await xmlHttpRequest(JISHO + '/search/' + encodeURI(injectorState.characters) + '%20%23kanji')
  85.  
  86. let strokeOrderSvg = xhr.responseText.match(/var url = '\/\/(.+)';/)
  87. if (!strokeOrderSvg) return null
  88.  
  89. xhr = await xmlHttpRequest('https://' + strokeOrderSvg[1])
  90.  
  91. let namespace = 'http://www.w3.org/2000/svg'
  92. let div = document.createElement('div')
  93. let svg = document.createElementNS(namespace, 'svg')
  94. svg.id = 'stroke_order'
  95. div.style = 'width: 100%; overflow: auto hidden;'
  96. new strokeOrderDiagram(
  97. svg,
  98. xhr.responseXML || new DOMParser().parseFromString(xhr.responseText, 'application/xml'),
  99. )
  100. div.append(svg)
  101. return div
  102. }
  103.  
  104. /*
  105. * Lifted from jisho.org, modified to allow multiple rows
  106. */
  107. var strokeOrderDiagram = function (element, svgDocument) {
  108. var s = Snap(element)
  109. var diagramSize = 200
  110. var coordRe = '(?:\\d+(?:\\.\\d+)?)'
  111. var strokeRe = new RegExp('^[LMT]\\s*(' + coordRe + ')[,\\s](' + coordRe + ')', 'i')
  112. var f = Snap(svgDocument.getElementsByTagName('svg')[0])
  113. var allPaths = f.selectAll('path')
  114. var drawnPaths = []
  115. var framesPerRow = 10
  116. var rowCount = Math.floor((allPaths.length - 1) / framesPerRow) + 1
  117. var canvasWidth = (Math.min(framesPerRow, allPaths.length) * diagramSize) / 2
  118. var frameSize = diagramSize / 2
  119. var canvasHeight = frameSize * rowCount
  120. var frameOffsetMatrix = new Snap.Matrix()
  121. frameOffsetMatrix.translate(-frameSize / 16 + 2, -frameSize / 16 + 2)
  122.  
  123. // Set drawing area
  124. s.node.style.width = canvasWidth + 'px'
  125. s.node.style.height = canvasHeight + 'px'
  126. s.node.setAttribute('viewBox', '0 0 ' + canvasWidth + ' ' + canvasHeight)
  127.  
  128. // Draw global guides
  129. var boundingBoxTop = s.line(1, 1, canvasWidth - 1, 1)
  130. var boundingBoxLeft = s.line(1, 1, 1, canvasHeight - 1)
  131. for (var i = 0; i < rowCount; i++) {
  132. var horizontalY = frameSize / 2 + i * frameSize
  133. var horizontalGuide = s.line(0, horizontalY, canvasWidth, horizontalY)
  134. horizontalGuide.attr({ class: 'stroke_order_diagram--guide_line' })
  135. var boundingBoxBottom = s.line(1, frameSize * (i + 1) - 1, canvasWidth - 1, frameSize * (i + 1) - 1)
  136. boundingBoxBottom.attr({ class: 'stroke_order_diagram--bounding_box' })
  137. }
  138. boundingBoxTop.attr({ class: 'stroke_order_diagram--bounding_box' })
  139. boundingBoxLeft.attr({ class: 'stroke_order_diagram--bounding_box' })
  140.  
  141. // Draw strokes
  142. var pathNumber = 1
  143. allPaths.forEach(function (currentPath) {
  144. var effectivePathNumber = ((pathNumber - 1) % framesPerRow) + 1
  145. var effectiveY = Math.floor((pathNumber - 1) / framesPerRow) * frameSize
  146. var moveFrameMatrix = new Snap.Matrix()
  147. moveFrameMatrix.translate(frameSize * (effectivePathNumber - 1) - 4, -4 + effectiveY)
  148.  
  149. // Draw frame guides
  150. var verticalGuide = s.line(
  151. frameSize * effectivePathNumber - frameSize / 2,
  152. 1,
  153. frameSize * effectivePathNumber - frameSize / 2,
  154. canvasHeight - 1,
  155. )
  156. var frameBoxRight = s.line(
  157. frameSize * effectivePathNumber - 1,
  158. 1,
  159. frameSize * effectivePathNumber - 1,
  160. canvasHeight - 1,
  161. )
  162. verticalGuide.attr({ class: 'stroke_order_diagram--guide_line' })
  163. frameBoxRight.attr({ class: 'stroke_order_diagram--bounding_box' })
  164.  
  165. // Draw previous strokes
  166. drawnPaths.forEach(function (existingPath) {
  167. var localPath = existingPath.clone()
  168. localPath.transform(moveFrameMatrix)
  169. localPath.attr({ class: 'stroke_order_diagram--existing_path' })
  170. s.append(localPath)
  171. })
  172.  
  173. // Draw current stroke
  174. currentPath.transform(frameOffsetMatrix)
  175. currentPath.transform(moveFrameMatrix)
  176. currentPath.attr({ class: 'stroke_order_diagram--current_path' })
  177. s.append(currentPath)
  178.  
  179. // Draw stroke start point
  180. var match = strokeRe.exec(currentPath.node.getAttribute('d'))
  181. var pathStartX = match[1]
  182. var pathStartY = match[2]
  183. var strokeStart = s.circle(pathStartX, pathStartY, 4)
  184. strokeStart.attr({ class: 'stroke_order_diagram--path_start' })
  185. strokeStart.transform(moveFrameMatrix)
  186.  
  187. pathNumber++
  188. drawnPaths.push(currentPath.clone())
  189. })
  190. }
  191. })()

QingJ © 2025

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