- /*
- * Draw Me A Kanji - v0.3.1
- * A funny drawer for your Japanese writings
- * http://drawmeakanji.com
- *
- * Made by Matthieu Bilbille
- * Under MIT License
- */
- ;(function () {
-
- "use strict";
-
- // Create a safe reference to the DrawMeAKanji object for use below.
- var Dmak = function (text, options) {
- this.text = text;
- this.options = extend(Dmak.options, options);
- this.strokes = [];
- this.papers = [];
- this.pointer = 0;
- this.timeouts = {
- play : [],
- erasing : [],
- drawing : []
- };
-
- if (!this.options.skipLoad) {
- var loader = new DmakLoader(this.options.uri),
- self = this;
-
- loader.load(text, function (data) {
- self.prepare(data);
-
- // Execute custom callback "loaded" here
- self.options.loaded(self.kanjis);
-
- if (self.options.autoplay) {
- self.render();
- }
- });
- }
- };
-
- // Current version.
- Dmak.VERSION = "0.2.0";
-
- Dmak.options = {
- uri: "",
- skipLoad: false,
- autoplay: true,
- height: 109,
- width: 109,
- viewBox: {
- x: 0,
- y: 0,
- w: 109,
- h: 109
- },
- step: 0.03,
- element: "draw",
- stroke: {
- animated : {
- drawing : true,
- erasing : true
- },
- order: {
- visible: false,
- attr: {
- "font-size": "8",
- "fill": "#999999"
- }
- },
- attr: {
- "active": "#BF0000",
- // may use the keyword "random" here for random color
- "stroke": "#2C2C2C",
- "stroke-width": 4,
- "stroke-linecap": "round",
- "stroke-linejoin": "round"
- }
- },
- grid: {
- show: true,
- attr: {
- "stroke": "#CCCCCC",
- "stroke-width": 0.5,
- "stroke-dasharray": "--"
- }
- },
- loaded: function () {
- },
- erased: function () {
- },
- drew: function () {
- }
- };
-
- Dmak.fn = Dmak.prototype = {
-
- /**
- * Prepare kanjis and papers for rendering.
- */
- prepare: function (data) {
- this.kanjis = preprocessStrokes(data);
- this.papers = giveBirthToRaphael(data.length);
- if (this.options.grid.show) {
- showGrid(this.papers);
- }
- },
-
- /**
- * Clean all strokes on papers.
- */
- erase: function (end) {
- // Cannot have two rendering process for the same draw. Keep it cool.
- if (this.timeouts.play.length) {
- return false;
- }
-
- // Don't go behind the beginning.
- if (this.pointer <= 0) {
- return false;
- }
-
- if (typeof end === "undefined") {
- end = 0;
- }
-
- do {
- this.pointer--;
- eraseStroke(this.kanjis[this.pointer], this.timeouts.erasing);
-
- // Execute custom callback "erased" here
- this.options.erased(this.pointer);
- }
- while (this.pointer > end);
- },
-
- /**
- * All the magic happens here.
- */
- render: function (end) {
-
- // Cannot have two rendering process for
- // the same draw. Keep it cool.
- if (this.timeouts.play.length) {
- return false;
- }
-
- if (typeof end === "undefined") {
- end = this.kanjis.length;
- } else if (end > this.kanjis.length) {
- return false;
- }
-
- var cb = function (that) {
- drawStroke(that.papers[that.kanjis[that.pointer].char], that.kanjis[that.pointer], that.timeouts.drawing);
-
- // Execute custom callback "drew" here
- that.options.drew(that.pointer);
-
- that.pointer++;
- that.timeouts.play.shift();
- },
- delay = 0,
- i;
-
- // Before drawing clear any remaining erasing timeouts
- for (i = 0; i < this.timeouts.erasing.length; i++) {
- window.clearTimeout(this.timeouts.erasing[i]);
- this.timeouts.erasing = [];
- }
-
- for (i = this.pointer; i < end; i++) {
- if (!Dmak.options.stroke.animated.drawing || delay <= 0) {
- cb(this);
- } else {
- this.timeouts.play.push(setTimeout(cb, delay, this));
- }
- delay += this.kanjis[i].duration;
- }
- },
-
- /**
- * Pause rendering
- */
- pause: function () {
- for (var i = 0; i < this.timeouts.play.length; i++) {
- window.clearTimeout(this.timeouts.play[i]);
- }
- this.timeouts.play = [];
- },
-
- /**
- * Wrap the erase function to remove the x last strokes.
- */
- eraseLastStrokes: function (nbStrokes) {
- this.erase(this.pointer - nbStrokes);
- },
-
- /**
- * Wrap the render function to render the x next strokes.
- */
- renderNextStrokes: function (nbStrokes) {
- this.render(this.pointer + nbStrokes);
- }
-
- };
-
- // HELPERS
-
- /**
- * Flattens the array of strokes ; 3D > 2D and does some preprocessing while
- * looping through all the strokes:
- * - Maps to a character index
- * - Calculates path length
- */
- function preprocessStrokes(data) {
- var strokes = [],
- stroke,
- length,
- i,
- j;
-
- for (i = 0; i < data.length; i++) {
- for (j = 0; j < data[i].length; j++) {
- length = Raphael.getTotalLength(data[i][j].path);
- stroke = {
- "char": i,
- "length": length,
- "duration": length * Dmak.options.step * 1000,
- "path": data[i][j].path,
- "groups" : data[i][j].groups,
- "text": data[i][j].text,
- "object": {
- "path" : null,
- "text": null
- }
- };
- strokes.push(stroke);
- }
- }
-
- return strokes;
- }
-
- /**
- * Init Raphael paper objects
- */
- function giveBirthToRaphael(nbChar) {
- var papers = [],
- paper,
- i;
-
- for (i = 0; i < nbChar; i++) {
- paper = new Raphael(Dmak.options.element, Dmak.options.width + "px", Dmak.options.height + "px");
- paper.setViewBox(Dmak.options.viewBox.x, Dmak.options.viewBox.y, Dmak.options.viewBox.w, Dmak.options.viewBox.h);
- paper.canvas.setAttribute("class", "dmak-svg");
- papers.push(paper);
- }
- return papers.reverse();
- }
-
- /**
- * Draw the background grid
- */
- function showGrid(papers) {
- var i;
-
- for (i = 0; i < papers.length; i++) {
- papers[i].path("M" + (Dmak.options.viewBox.w / 2) + ",0 L" + (Dmak.options.viewBox.w / 2) + "," + Dmak.options.viewBox.h).attr(Dmak.options.grid.attr);
- papers[i].path("M0," + (Dmak.options.viewBox.h / 2) + " L" + Dmak.options.viewBox.w + "," + (Dmak.options.viewBox.h / 2)).attr(Dmak.options.grid.attr);
- }
- }
-
- /**
- * Remove a single stroke ; deletion can be animated if set as so.
- */
- function eraseStroke(stroke, timeouts) {
- // In some cases the text object may be null:
- // - Stroke order display disabled
- // - Stroke already deleted
- if (stroke.object.text !== null) {
- stroke.object.text.remove();
- }
-
- var cb = function() {
- stroke.object.path.remove();
-
- // Finally properly prepare the object variable
- stroke.object = {
- "path" : null,
- "text" : null
- };
-
- timeouts.shift();
- };
-
- if (Dmak.options.stroke.animated.erasing) {
- stroke.object.path.node.style.stroke = Dmak.options.stroke.attr.active;
- timeouts.push(animateStroke(stroke, -1, cb));
- }
- else {
- cb();
- }
- }
-
- /**
- * Draw a single stroke ; drawing can be animated if set as so.
- */
- function drawStroke(paper, stroke, timeouts) {
- var cb = function() {
-
- // The stroke object may have been already erased when we reach this timeout
- if (stroke.object.path === null) {
- return;
- }
-
- var color = Dmak.options.stroke.attr.stroke;
- if(Dmak.options.stroke.attr.stroke === "random") {
- color = Raphael.getColor();
- }
-
- // Revert back to the default color.
- stroke.object.path.node.style.stroke = color;
- stroke.object.path.node.style.transition = stroke.object.path.node.style.WebkitTransition = "stroke 400ms ease";
-
- timeouts.shift();
- };
-
- stroke.object.path = paper.path(stroke.path);
- stroke.object.path.attr(Dmak.options.stroke.attr);
-
- if (Dmak.options.stroke.order.visible) {
- showStrokeOrder(paper, stroke);
- }
-
- if (Dmak.options.stroke.animated.drawing) {
- animateStroke(stroke, 1, cb);
- }
- else {
- cb();
- }
- }
-
- /**
- * Draw a single next to
- */
- function showStrokeOrder(paper, stroke) {
- stroke.object.text = paper.text(stroke.text.x, stroke.text.y, stroke.text.value);
- stroke.object.text.attr(Dmak.options.stroke.order.attr);
- }
-
- /**
- * Animate stroke drawing.
- * Based on the great article wrote by Jake Archibald
- * http://jakearchibald.com/2013/animated-line-drawing-svg/
- */
- function animateStroke(stroke, direction, callback) {
- stroke.object.path.attr({"stroke": Dmak.options.stroke.attr.active});
- stroke.object.path.node.style.transition = stroke.object.path.node.style.WebkitTransition = "none";
-
- // Set up the starting positions
- stroke.object.path.node.style.strokeDasharray = stroke.length + " " + stroke.length;
- stroke.object.path.node.style.strokeDashoffset = (direction > 0) ? stroke.length : 0;
-
- // Trigger a layout so styles are calculated & the browser
- // picks up the starting position before animating
- stroke.object.path.node.getBoundingClientRect();
- stroke.object.path.node.style.transition = stroke.object.path.node.style.WebkitTransition = "stroke-dashoffset " + stroke.duration + "ms ease";
-
- // Go!
- stroke.object.path.node.style.strokeDashoffset = (direction > 0) ? "0" : stroke.length;
-
- // Execute the callback once the animation is done
- // and return the timeout id.
- return setTimeout(callback, stroke.duration);
- }
-
- /**
- * Simplistic helper function for extending objects
- */
- function extend(defaults, replacement) {
- var result = defaults,
- key;
-
- if (arguments.length !== 2) {
- throw new Error("Missing arguments in extend function");
- }
-
- for (key in replacement) {
- if (typeof result[key] === "object") {
- result[key] = extend(result[key], replacement[key]);
- } else if (result.hasOwnProperty(key)) {
- result[key] = replacement[key];
- }
- }
- return result;
- }
-
- window.Dmak = Dmak;
- }());
-
- ;(function () {
-
- "use strict";
-
- // Create a safe reference to the DrawMeAKanji object for use below.
- var DmakLoader = function (uri) {
- this.uri = uri;
- };
-
- /**
- * Gather SVG data information for a given set of characters.
- * By default this action is done while instanciating the Word
- * object, but it can be skipped, see above
- */
- DmakLoader.prototype.load = function (text, callback) {
- var paths = [],
- nbChar = text.length,
- done = 0,
- i,
- callbacks = {
- done: function (index, data) {
- paths[index] = data;
- done++;
- if (done === nbChar) {
- callback(paths);
- }
- },
- error: function (msg) {
- console.log("Error", msg);
- }
- };
-
- for (i = 0; i < nbChar; i++) {
- loadSvg(this.uri, i, text.charCodeAt(i).toString(16), callbacks);
- }
- };
-
- /**
- * Try to load a SVG file matching the given char code.
- * @thanks to the incredible work made by KanjiVG
- * @see: http://kanjivg.tagaini.net
- */
- function loadSvg(uri, index, charCode, callbacks) {
- var xhr = new XMLHttpRequest(),
- code = ("00000" + charCode).slice(-5);
-
- // Skip space character
- if(code === "00020" || code === "03000") {
- callbacks.done(index, {
- paths: [],
- texts: []
- });
- return;
- }
-
- xhr.open("GET", uri + code + ".svg", true);
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- callbacks.done(index, parseResponse(xhr.response, code));
- } else {
- callbacks.error(xhr.statusText);
- }
- }
- };
- xhr.send();
- }
-
- /**
- * Simple parser to extract paths and texts data.
- */
- function parseResponse(response, code) {
- var data = [],
- dom = new DOMParser().parseFromString(response, "application/xml"),
- texts = dom.querySelectorAll("text"),
- groups = [],
- i;
-
- // Private recursive function to parse DOM content
- function __parse(element) {
- var children = element.childNodes,
- i;
-
- for(i = 0; i < children.length; i++) {
- if(children[i].tagName === "g") {
- groups.push(children[i].getAttribute("id"));
- __parse(children[i]);
- groups.splice(groups.indexOf(children[i].getAttribute("id")), 1);
- }
- else if(children[i].tagName === "path") {
- data.push({
- "path" : children[i].getAttribute("d"),
- "groups" : groups.slice(0)
- });
- }
- }
- }
-
- // Start parsing
- __parse(dom.getElementById("kvg:" + code));
-
- // And finally add order mark information
- for (i = 0; i < texts.length; i++) {
- data[i].text = {
- "value" : texts[i].textContent,
- "x" : texts[i].getAttribute("transform").split(" ")[4],
- "y" : texts[i].getAttribute("transform").split(" ")[5].replace(")", "")
- };
- }
-
- return data;
- }
-
- window.DmakLoader = DmakLoader;
- }());