- // ==UserScript==
- // @name WaniKani Stroke Order
- // @namespace japanese
- // @version 1.1.22
- // @description Shows a kanji's stroke order on its page and during lessons and reviews.
- // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
- // @match https://www.wanikani.com/*
- // @match https://preview.wanikani.com/*
- // @author Looki, maintained by kind users on the forum
- // @grant GM_xmlhttpRequest
- // @connect jisho.org
- // @connect cloudfront.net
- // @require https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js
- // @require https://gf.qytechs.cn/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1326536
-
- // ==/UserScript==
-
- /*
- * Thanks a lot to ...
- * Wanikani Phonetic-Semantic Composition - Userscript
- * by ruipgpinheiro (LordGravewish)
- * ... for code showing me how to insert sections during kanji reviews.
- * The code heavily borrows from that script!
- * Also thanks to Halo for a loading bug fix!
- */
-
- ;(function () {
- /* global Snap */
-
- /*
- * Helper Functions/Variables
- */
- let wkItemInfo = unsafeWindow.wkItemInfo
-
- /*
- * Global Variables/Objects/Classes
- */
- const JISHO = 'https://jisho.org'
- const strokeOrderCss =
- '.stroke_order_diagram--bounding_box {fill: none; stroke: #ddd; stroke-width: 2; stroke-linecap: square; stroke-linejoin: square;}' +
- '.stroke_order_diagram--bounding_box {fill: none; stroke: #ddd; stroke-width: 2; stroke-linecap: square; stroke-linejoin: square;}' +
- '.stroke_order_diagram--existing_path {fill: none; stroke: #aaa; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;}' +
- '.stroke_order_diagram--current_path {fill: none; stroke: #000; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;}' +
- '.stroke_order_diagram--path_start {fill: rgba(255,0,0,0.7); stroke: none;}' +
- '.stroke_order_diagram--guide_line {fill: none; stroke: #ddd; stroke-width: 2; stroke-linecap: square; stroke-linejoin: square; stroke-dasharray: 5, 5;}'
-
- init()
-
- /*
- * Main
- */
- function init() {
- wkItemInfo.on('lesson').forType('kanji').under('composition').append('Stroke Order', loadDiagram)
- wkItemInfo
- .on('lessonQuiz, review, extraStudy, itemPage')
- .forType('kanji')
- .under('composition')
- .appendAtTop('Stroke Order', loadDiagram)
-
- let style = document.createElement('style')
- style.textContent = strokeOrderCss
- document.head.appendChild(style)
- }
-
- function xmlHttpRequest(urlText) {
- return new Promise((resolve, reject) =>
- GM_xmlhttpRequest({
- method: 'GET',
- url: urlText,
- onload: (xhr) => {
- xhr.status === 200 ? resolve(xhr) : reject(xhr.responseText)
- },
- onerror: (xhr) => {
- reject(xhr.responseText)
- },
- }),
- )
- }
-
- /*
- * Adds the diagram section element to the appropriate location
- */
- async function loadDiagram(injectorState) {
- let xhr = await xmlHttpRequest(JISHO + '/search/' + encodeURI(injectorState.characters) + '%20%23kanji')
-
- let strokeOrderSvg = xhr.responseText.match(/var url = '\/\/(.+)';/)
- if (!strokeOrderSvg) return null
-
- xhr = await xmlHttpRequest('https://' + strokeOrderSvg[1])
-
- let namespace = 'http://www.w3.org/2000/svg'
- let div = document.createElement('div')
- let svg = document.createElementNS(namespace, 'svg')
- svg.id = 'stroke_order'
- div.style = 'width: 100%; overflow: auto hidden;'
- new strokeOrderDiagram(
- svg,
- xhr.responseXML || new DOMParser().parseFromString(xhr.responseText, 'application/xml'),
- )
- div.append(svg)
- return div
- }
-
- /*
- * Lifted from jisho.org, modified to allow multiple rows
- */
- var strokeOrderDiagram = function (element, svgDocument) {
- var s = Snap(element)
- var diagramSize = 200
- var coordRe = '(?:\\d+(?:\\.\\d+)?)'
- var strokeRe = new RegExp('^[LMT]\\s*(' + coordRe + ')[,\\s](' + coordRe + ')', 'i')
- var f = Snap(svgDocument.getElementsByTagName('svg')[0])
- var allPaths = f.selectAll('path')
- var drawnPaths = []
- var framesPerRow = 10
- var rowCount = Math.floor((allPaths.length - 1) / framesPerRow) + 1
- var canvasWidth = (Math.min(framesPerRow, allPaths.length) * diagramSize) / 2
- var frameSize = diagramSize / 2
- var canvasHeight = frameSize * rowCount
- var frameOffsetMatrix = new Snap.Matrix()
- frameOffsetMatrix.translate(-frameSize / 16 + 2, -frameSize / 16 + 2)
-
- // Set drawing area
- s.node.style.width = canvasWidth + 'px'
- s.node.style.height = canvasHeight + 'px'
- s.node.setAttribute('viewBox', '0 0 ' + canvasWidth + ' ' + canvasHeight)
-
- // Draw global guides
- var boundingBoxTop = s.line(1, 1, canvasWidth - 1, 1)
- var boundingBoxLeft = s.line(1, 1, 1, canvasHeight - 1)
- for (var i = 0; i < rowCount; i++) {
- var horizontalY = frameSize / 2 + i * frameSize
- var horizontalGuide = s.line(0, horizontalY, canvasWidth, horizontalY)
- horizontalGuide.attr({ class: 'stroke_order_diagram--guide_line' })
- var boundingBoxBottom = s.line(1, frameSize * (i + 1) - 1, canvasWidth - 1, frameSize * (i + 1) - 1)
- boundingBoxBottom.attr({ class: 'stroke_order_diagram--bounding_box' })
- }
- boundingBoxTop.attr({ class: 'stroke_order_diagram--bounding_box' })
- boundingBoxLeft.attr({ class: 'stroke_order_diagram--bounding_box' })
-
- // Draw strokes
- var pathNumber = 1
- allPaths.forEach(function (currentPath) {
- var effectivePathNumber = ((pathNumber - 1) % framesPerRow) + 1
- var effectiveY = Math.floor((pathNumber - 1) / framesPerRow) * frameSize
- var moveFrameMatrix = new Snap.Matrix()
- moveFrameMatrix.translate(frameSize * (effectivePathNumber - 1) - 4, -4 + effectiveY)
-
- // Draw frame guides
- var verticalGuide = s.line(
- frameSize * effectivePathNumber - frameSize / 2,
- 1,
- frameSize * effectivePathNumber - frameSize / 2,
- canvasHeight - 1,
- )
- var frameBoxRight = s.line(
- frameSize * effectivePathNumber - 1,
- 1,
- frameSize * effectivePathNumber - 1,
- canvasHeight - 1,
- )
- verticalGuide.attr({ class: 'stroke_order_diagram--guide_line' })
- frameBoxRight.attr({ class: 'stroke_order_diagram--bounding_box' })
-
- // Draw previous strokes
- drawnPaths.forEach(function (existingPath) {
- var localPath = existingPath.clone()
- localPath.transform(moveFrameMatrix)
- localPath.attr({ class: 'stroke_order_diagram--existing_path' })
- s.append(localPath)
- })
-
- // Draw current stroke
- currentPath.transform(frameOffsetMatrix)
- currentPath.transform(moveFrameMatrix)
- currentPath.attr({ class: 'stroke_order_diagram--current_path' })
- s.append(currentPath)
-
- // Draw stroke start point
- var match = strokeRe.exec(currentPath.node.getAttribute('d'))
- var pathStartX = match[1]
- var pathStartY = match[2]
- var strokeStart = s.circle(pathStartX, pathStartY, 4)
- strokeStart.attr({ class: 'stroke_order_diagram--path_start' })
- strokeStart.transform(moveFrameMatrix)
-
- pathNumber++
- drawnPaths.push(currentPath.clone())
- })
- }
- })()