Skip to content

Instantly share code, notes, and snippets.

@miere43
Created February 12, 2017 15:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save miere43/6eca9b24765230adbd79e21696a404a5 to your computer and use it in GitHub Desktop.
Save miere43/6eca9b24765230adbd79e21696a404a5 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Wanikani Critical Items Export
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Export critical items from Wanikani in form of pictures
// @author Vladislav <miere> Vorobiev
// @match https://www.wanikani.com/critical-items
// @grant none
// ==/UserScript==
(function() {
'use strict';
var options = {
// set to false to export all pictures
'testMode': true,
// your wanikani api key
'apiKey': '',
'imageWidth': 800,
'imageHeight': 600,
// maximum font size for character rendering
'common': {
'fontFamily': 'Arial', // 'NotoSansCJK',
// font size for drawing text.
// for 'character' section, font size will be reduced to fit to the image, text wrapping is enabled for other sections.
'fontSize': 28,
'textColor': '#FFFFFF',
// line height for 'vocabulary' and 'radical' sections.
'lineHeight': 20,
// maximum text width in percents (1.0 = 100%, 0.0 = 0%)
'maxWidth': 1.0,
// horizontal text offset in percents relative to image center
'offsetX': 0.5,
// vertical text offset
'offsetY': 0.5
},
'kanji': {
'backgroundColor': '#f100a1',
// if some property is not set, it will be inherited from 'common' section.
// works for 'character', 'reading' and 'meaning' sections.
'character': {
// 'character' section doesn't care about 'lineHeight' from 'common' section.
'maxWidth': 0.6,
'fontSize': 240,
'offsetY': 0.5
},
'reading': {
'offsetY': 0.8
},
'meaning': {
'offsetY': 0.9
}
},
'vocabulary': {
'backgroundColor': '#a100f1',
'character': {
'maxWidth': 0.6,
'fontSize': 240,
'offsetY': 0.5
},
'reading': {
'offsetY': 0.8
},
'meaning': {
'offsetY': 0.9
}
},
'radical': {
'backgroundColor': '#00a1f1',
'character': {
'maxWidth': 0.5,
'fontSize': 240,
'offsetY': 0.5
},
'meaning': {
'offsetY': 0.8,
'fontSize': 40,
}
},
};
var measureTextSize = (function() {
var measure = document.createElement('div');
measure.style.position = 'absolute';
measure.style.top = '-9999px';
measure.style.left = '-9999px';
//measure.id = 'test123';
document.body.appendChild(measure);
return function(text, fontSizeInPixels, fontFamily) {
// @TODO add font weight?
measure.style.fontFamily = fontFamily;
measure.style.fontSize = fontSizeInPixels + 'px';
// measure.style.fontWeight = ...;
measure.innerText = text;
return { 'width': measure.clientWidth, 'height': measure.clientHeight };
};
})();
function getCriticalItems(apiKey, successCallback, errorCallback)
{
var url = 'https://www.wanikani.com/api/user/' + apiKey + '/critical-items';
var xhr = new XMLHttpRequest();
var criticalItems = null;
xhr.addEventListener('load', function() {
var status = this.status;
if (status == 200) {
var response = JSON.parse(this.response);
if (response.error !== undefined) {
errorCallback(response.error);
} else {
criticalItems = response.requested_information;
successCallback(criticalItems);
}
} else {
errorCallback({ 'message': 'Wanikani request error', 'code': status });
}
});
xhr.respondType = 'json';
xhr.open('GET', url, true);
xhr.send();
}
var saveCanvasToDisk = (function() {
var saveElement = document.createElement('a');
saveElement.style = 'display: none';
document.body.appendChild(saveElement);
return function(canvas, fileName) {
var canvasData = canvas.toDataURL();
saveElement.href = canvasData;
saveElement.download = fileName;
saveElement.click();
window.URL.revokeObjectURL(canvasData);
};
})();
var canvas = document.createElement("canvas");
canvas.width = options.imageWidth || 240;
canvas.height = options.imageHeight || 240;
var context = canvas.getContext('2d');
if (!context) {
alert('Unable to get canvas context.');
}
context.textAlign = 'center';
context.textBaseline = 'middle';
var criticalItemsSection = document.getElementsByClassName("low-percentage kotoba-table-list dashboard-sub-section")[0];
var exportButton = document.createElement("input");
exportButton.type = "button";
exportButton.value = "Export as pictures";
if ((options.testMode || false) === true) {
exportButton.value += " (test mode, only 1 picture will be exported)";
}
function fitTextToSize(text, fontSize, fontFamily, maxWidth, maxHeight)
{
// @Speed: this stuff is not efficient at all
while (fontSize > 1) {
var measure = measureTextSize(text, fontSize, fontFamily);
if (measure.width > maxWidth/* || measure.height <= maxHeight*/) {
fontSize -= 1;
continue;
} else {
break;
}
}
return fontSize;
}
function getContextSettingsForType(type) {
return options[type];
}
function getReading(item) {
if (item.type == 'vocabulary') {
return item.kana;
} else if (item.type == 'kanji') {
return item[item.important_reading];
} else {
return null;
}
}
function getMeaning(item) {
return item.meaning;
}
function fillTextWrapped(context, text, maxWidth, lineHeight, x, y)
{
var words = text.split(" ");
var currentLine = words[0];
var currentLineHeight = 0;
for (var i = 1; i < words.length; i++) {
var word = words[i];
var width = context.measureText(currentLine + " " + word).width;
if (width < maxWidth) {
currentLine += " " + word;
} else {
context.fillText(currentLine, x, y + currentLineHeight);
currentLineHeight += lineHeight;
currentLine = word;
}
}
context.fillText(currentLine, x, y + currentLineHeight);
}
function getContextProperty(contextOptions, property, defaultValue) {
var value = contextOptions[property];
if (value === undefined) {
value = options.common[property];
if (value === undefined) {
return defaultValue;
}
}
return value;
}
exportButton.addEventListener('click', function() {
exportButton.setAttribute('disabled', true);
getCriticalItems(options.apiKey, function(criticalItems) {
var length = (options.testMode || false) === false ? criticalItems.length : 1;
for (var i = 0; i < length; ++i) {
var criticalItem = criticalItems[i];
var contextOptions = null;
if (criticalItem.type === 'kanji' || criticalItem.type === 'vocabulary' || criticalItem.type === 'radical') {
contextOptions = getContextSettingsForType(criticalItem.type);
if (!contextOptions) {
alert('No options found for critical item type "' + criticalItem.type + '"');
}
} else {
alert('Unknown critical item type: "' + criticalItem.type + '" at position ' + i);
continue;
}
// Background
context.fillStyle = contextOptions.backgroundColor || '#FFFFFF';
context.fillRect(0, 0, canvas.width, canvas.height);
// Character
{
context.fillStyle = getContextProperty(contextOptions.character, 'textColor', '#000000');
var fontFamily = getContextProperty(contextOptions.character, 'fontFamily', 'Arial');
var fontSize = fitTextToSize(criticalItem.character,
getContextProperty(contextOptions.character, 'fontSize', 24),
fontFamily,
(contextOptions.character.maxWidth || 1.0) * canvas.width,
canvas.height);
context.font = fontSize + 'px ' + fontFamily;
context.fillText(criticalItem.character,
canvas.width * getContextProperty(contextOptions.character, 'offsetX', 0.5),
canvas.height * getContextProperty(contextOptions.character, 'offsetY', 0.5));
}
// Meaning
context.fillStyle = getContextProperty(contextOptions.meaning, 'textColor', '#000000');
context.font = getContextProperty(contextOptions.meaning, 'fontSize', 10) + 'px ' + getContextProperty(contextOptions.meaning, 'fontFamily', 'Arial');
fillTextWrapped(context,
getMeaning(criticalItem),
getContextProperty(contextOptions.meaning, 'maxWidth', 1.0) * canvas.width,
getContextProperty(contextOptions.meaning, 'lineHeight', 20),
canvas.width * getContextProperty(contextOptions.meaning, 'offsetX', 0.5),
canvas.height * getContextProperty(contextOptions.meaning, 'offsetY', 0.6));
// Reading
if (criticalItem.type !== 'radical') {
context.fillStyle = getContextProperty(contextOptions.reading, 'textColor', '#000000');
context.font = getContextProperty(contextOptions.reading, 'fontSize', 10) + 'px ' + getContextProperty(contextOptions.reading, 'fontFamily', 'Arial');
fillTextWrapped(context,
getReading(criticalItem),
getContextProperty(contextOptions.reading, 'maxWidth', 1.0) * canvas.width,
getContextProperty(contextOptions.reading, 'lineHeight', 20),
canvas.width * getContextProperty(contextOptions.reading, 'offsetX', 0.5),
canvas.height * getContextProperty(contextOptions.reading, 'offsetY', 0.7));
}
saveCanvasToDisk(canvas, criticalItem.character + '.png');
}
}, function(error) {
alert('Error rethieving critical items: ' + error.message + ' (' + error.code + ')');
});
});
criticalItemsSection.appendChild(exportButton);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment