dmak

Draw Me a Kanji

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/11842/68906/dmak.js

  1. /*
  2. * Draw Me A Kanji - v0.3.1
  3. * A funny drawer for your Japanese writings
  4. * http://drawmeakanji.com
  5. *
  6. * Made by Matthieu Bilbille
  7. * Under MIT License
  8. */
  9. ;(function () {
  10.  
  11. "use strict";
  12.  
  13. // Create a safe reference to the DrawMeAKanji object for use below.
  14. var Dmak = function (text, options) {
  15. this.text = text;
  16. this.options = extend(Dmak.options, options);
  17. this.strokes = [];
  18. this.papers = [];
  19. this.pointer = 0;
  20. this.timeouts = {
  21. play : [],
  22. erasing : [],
  23. drawing : []
  24. };
  25.  
  26. if (!this.options.skipLoad) {
  27. var loader = new DmakLoader(this.options.uri),
  28. self = this;
  29.  
  30. loader.load(text, function (data) {
  31. self.prepare(data);
  32.  
  33. // Execute custom callback "loaded" here
  34. self.options.loaded(self.kanjis);
  35.  
  36. if (self.options.autoplay) {
  37. self.render();
  38. }
  39. });
  40. }
  41. };
  42.  
  43. // Current version.
  44. Dmak.VERSION = "0.2.0";
  45.  
  46. Dmak.options = {
  47. uri: "",
  48. skipLoad: false,
  49. autoplay: true,
  50. height: 109,
  51. width: 109,
  52. viewBox: {
  53. x: 0,
  54. y: 0,
  55. w: 109,
  56. h: 109
  57. },
  58. step: 0.03,
  59. element: "draw",
  60. stroke: {
  61. animated : {
  62. drawing : true,
  63. erasing : true
  64. },
  65. order: {
  66. visible: false,
  67. attr: {
  68. "font-size": "8",
  69. "fill": "#999999"
  70. }
  71. },
  72. attr: {
  73. "active": "#BF0000",
  74. // may use the keyword "random" here for random color
  75. "stroke": "#2C2C2C",
  76. "stroke-width": 4,
  77. "stroke-linecap": "round",
  78. "stroke-linejoin": "round"
  79. }
  80. },
  81. grid: {
  82. show: true,
  83. attr: {
  84. "stroke": "#CCCCCC",
  85. "stroke-width": 0.5,
  86. "stroke-dasharray": "--"
  87. }
  88. },
  89. loaded: function () {
  90. },
  91. erased: function () {
  92. },
  93. drew: function () {
  94. }
  95. };
  96.  
  97. Dmak.fn = Dmak.prototype = {
  98.  
  99. /**
  100. * Prepare kanjis and papers for rendering.
  101. */
  102. prepare: function (data) {
  103. this.kanjis = preprocessStrokes(data);
  104. this.papers = giveBirthToRaphael(data.length);
  105. if (this.options.grid.show) {
  106. showGrid(this.papers);
  107. }
  108. },
  109.  
  110. /**
  111. * Clean all strokes on papers.
  112. */
  113. erase: function (end) {
  114. // Cannot have two rendering process for the same draw. Keep it cool.
  115. if (this.timeouts.play.length) {
  116. return false;
  117. }
  118.  
  119. // Don't go behind the beginning.
  120. if (this.pointer <= 0) {
  121. return false;
  122. }
  123.  
  124. if (typeof end === "undefined") {
  125. end = 0;
  126. }
  127.  
  128. do {
  129. this.pointer--;
  130. eraseStroke(this.kanjis[this.pointer], this.timeouts.erasing);
  131.  
  132. // Execute custom callback "erased" here
  133. this.options.erased(this.pointer);
  134. }
  135. while (this.pointer > end);
  136. },
  137.  
  138. /**
  139. * All the magic happens here.
  140. */
  141. render: function (end) {
  142.  
  143. // Cannot have two rendering process for
  144. // the same draw. Keep it cool.
  145. if (this.timeouts.play.length) {
  146. return false;
  147. }
  148.  
  149. if (typeof end === "undefined") {
  150. end = this.kanjis.length;
  151. } else if (end > this.kanjis.length) {
  152. return false;
  153. }
  154.  
  155. var cb = function (that) {
  156. drawStroke(that.papers[that.kanjis[that.pointer].char], that.kanjis[that.pointer], that.timeouts.drawing);
  157.  
  158. // Execute custom callback "drew" here
  159. that.options.drew(that.pointer);
  160.  
  161. that.pointer++;
  162. that.timeouts.play.shift();
  163. },
  164. delay = 0,
  165. i;
  166.  
  167. // Before drawing clear any remaining erasing timeouts
  168. for (i = 0; i < this.timeouts.erasing.length; i++) {
  169. window.clearTimeout(this.timeouts.erasing[i]);
  170. this.timeouts.erasing = [];
  171. }
  172.  
  173. for (i = this.pointer; i < end; i++) {
  174. if (!Dmak.options.stroke.animated.drawing || delay <= 0) {
  175. cb(this);
  176. } else {
  177. this.timeouts.play.push(setTimeout(cb, delay, this));
  178. }
  179. delay += this.kanjis[i].duration;
  180. }
  181. },
  182.  
  183. /**
  184. * Pause rendering
  185. */
  186. pause: function () {
  187. for (var i = 0; i < this.timeouts.play.length; i++) {
  188. window.clearTimeout(this.timeouts.play[i]);
  189. }
  190. this.timeouts.play = [];
  191. },
  192.  
  193. /**
  194. * Wrap the erase function to remove the x last strokes.
  195. */
  196. eraseLastStrokes: function (nbStrokes) {
  197. this.erase(this.pointer - nbStrokes);
  198. },
  199.  
  200. /**
  201. * Wrap the render function to render the x next strokes.
  202. */
  203. renderNextStrokes: function (nbStrokes) {
  204. this.render(this.pointer + nbStrokes);
  205. }
  206.  
  207. };
  208.  
  209. // HELPERS
  210.  
  211. /**
  212. * Flattens the array of strokes ; 3D > 2D and does some preprocessing while
  213. * looping through all the strokes:
  214. * - Maps to a character index
  215. * - Calculates path length
  216. */
  217. function preprocessStrokes(data) {
  218. var strokes = [],
  219. stroke,
  220. length,
  221. i,
  222. j;
  223.  
  224. for (i = 0; i < data.length; i++) {
  225. for (j = 0; j < data[i].length; j++) {
  226. length = Raphael.getTotalLength(data[i][j].path);
  227. stroke = {
  228. "char": i,
  229. "length": length,
  230. "duration": length * Dmak.options.step * 1000,
  231. "path": data[i][j].path,
  232. "groups" : data[i][j].groups,
  233. "text": data[i][j].text,
  234. "object": {
  235. "path" : null,
  236. "text": null
  237. }
  238. };
  239. strokes.push(stroke);
  240. }
  241. }
  242.  
  243. return strokes;
  244. }
  245.  
  246. /**
  247. * Init Raphael paper objects
  248. */
  249. function giveBirthToRaphael(nbChar) {
  250. var papers = [],
  251. paper,
  252. i;
  253.  
  254. for (i = 0; i < nbChar; i++) {
  255. paper = new Raphael(Dmak.options.element, Dmak.options.width + "px", Dmak.options.height + "px");
  256. paper.setViewBox(Dmak.options.viewBox.x, Dmak.options.viewBox.y, Dmak.options.viewBox.w, Dmak.options.viewBox.h);
  257. paper.canvas.setAttribute("class", "dmak-svg");
  258. papers.push(paper);
  259. }
  260. return papers.reverse();
  261. }
  262.  
  263. /**
  264. * Draw the background grid
  265. */
  266. function showGrid(papers) {
  267. var i;
  268.  
  269. for (i = 0; i < papers.length; i++) {
  270. 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);
  271. papers[i].path("M0," + (Dmak.options.viewBox.h / 2) + " L" + Dmak.options.viewBox.w + "," + (Dmak.options.viewBox.h / 2)).attr(Dmak.options.grid.attr);
  272. }
  273. }
  274.  
  275. /**
  276. * Remove a single stroke ; deletion can be animated if set as so.
  277. */
  278. function eraseStroke(stroke, timeouts) {
  279. // In some cases the text object may be null:
  280. // - Stroke order display disabled
  281. // - Stroke already deleted
  282. if (stroke.object.text !== null) {
  283. stroke.object.text.remove();
  284. }
  285.  
  286. var cb = function() {
  287. stroke.object.path.remove();
  288.  
  289. // Finally properly prepare the object variable
  290. stroke.object = {
  291. "path" : null,
  292. "text" : null
  293. };
  294.  
  295. timeouts.shift();
  296. };
  297.  
  298. if (Dmak.options.stroke.animated.erasing) {
  299. stroke.object.path.node.style.stroke = Dmak.options.stroke.attr.active;
  300. timeouts.push(animateStroke(stroke, -1, cb));
  301. }
  302. else {
  303. cb();
  304. }
  305. }
  306.  
  307. /**
  308. * Draw a single stroke ; drawing can be animated if set as so.
  309. */
  310. function drawStroke(paper, stroke, timeouts) {
  311. var cb = function() {
  312.  
  313. // The stroke object may have been already erased when we reach this timeout
  314. if (stroke.object.path === null) {
  315. return;
  316. }
  317.  
  318. var color = Dmak.options.stroke.attr.stroke;
  319. if(Dmak.options.stroke.attr.stroke === "random") {
  320. color = Raphael.getColor();
  321. }
  322.  
  323. // Revert back to the default color.
  324. stroke.object.path.node.style.stroke = color;
  325. stroke.object.path.node.style.transition = stroke.object.path.node.style.WebkitTransition = "stroke 400ms ease";
  326.  
  327. timeouts.shift();
  328. };
  329.  
  330. stroke.object.path = paper.path(stroke.path);
  331. stroke.object.path.attr(Dmak.options.stroke.attr);
  332.  
  333. if (Dmak.options.stroke.order.visible) {
  334. showStrokeOrder(paper, stroke);
  335. }
  336.  
  337. if (Dmak.options.stroke.animated.drawing) {
  338. animateStroke(stroke, 1, cb);
  339. }
  340. else {
  341. cb();
  342. }
  343. }
  344.  
  345. /**
  346. * Draw a single next to
  347. */
  348. function showStrokeOrder(paper, stroke) {
  349. stroke.object.text = paper.text(stroke.text.x, stroke.text.y, stroke.text.value);
  350. stroke.object.text.attr(Dmak.options.stroke.order.attr);
  351. }
  352.  
  353. /**
  354. * Animate stroke drawing.
  355. * Based on the great article wrote by Jake Archibald
  356. * http://jakearchibald.com/2013/animated-line-drawing-svg/
  357. */
  358. function animateStroke(stroke, direction, callback) {
  359. stroke.object.path.attr({"stroke": Dmak.options.stroke.attr.active});
  360. stroke.object.path.node.style.transition = stroke.object.path.node.style.WebkitTransition = "none";
  361.  
  362. // Set up the starting positions
  363. stroke.object.path.node.style.strokeDasharray = stroke.length + " " + stroke.length;
  364. stroke.object.path.node.style.strokeDashoffset = (direction > 0) ? stroke.length : 0;
  365.  
  366. // Trigger a layout so styles are calculated & the browser
  367. // picks up the starting position before animating
  368. stroke.object.path.node.getBoundingClientRect();
  369. stroke.object.path.node.style.transition = stroke.object.path.node.style.WebkitTransition = "stroke-dashoffset " + stroke.duration + "ms ease";
  370.  
  371. // Go!
  372. stroke.object.path.node.style.strokeDashoffset = (direction > 0) ? "0" : stroke.length;
  373.  
  374. // Execute the callback once the animation is done
  375. // and return the timeout id.
  376. return setTimeout(callback, stroke.duration);
  377. }
  378.  
  379. /**
  380. * Simplistic helper function for extending objects
  381. */
  382. function extend(defaults, replacement) {
  383. var result = defaults,
  384. key;
  385.  
  386. if (arguments.length !== 2) {
  387. throw new Error("Missing arguments in extend function");
  388. }
  389.  
  390. for (key in replacement) {
  391. if (typeof result[key] === "object") {
  392. result[key] = extend(result[key], replacement[key]);
  393. } else if (result.hasOwnProperty(key)) {
  394. result[key] = replacement[key];
  395. }
  396. }
  397. return result;
  398. }
  399.  
  400. window.Dmak = Dmak;
  401. }());
  402.  
  403. ;(function () {
  404.  
  405. "use strict";
  406.  
  407. // Create a safe reference to the DrawMeAKanji object for use below.
  408. var DmakLoader = function (uri) {
  409. this.uri = uri;
  410. };
  411.  
  412. /**
  413. * Gather SVG data information for a given set of characters.
  414. * By default this action is done while instanciating the Word
  415. * object, but it can be skipped, see above
  416. */
  417. DmakLoader.prototype.load = function (text, callback) {
  418. var paths = [],
  419. nbChar = text.length,
  420. done = 0,
  421. i,
  422. callbacks = {
  423. done: function (index, data) {
  424. paths[index] = data;
  425. done++;
  426. if (done === nbChar) {
  427. callback(paths);
  428. }
  429. },
  430. error: function (msg) {
  431. console.log("Error", msg);
  432. }
  433. };
  434.  
  435. for (i = 0; i < nbChar; i++) {
  436. loadSvg(this.uri, i, text.charCodeAt(i).toString(16), callbacks);
  437. }
  438. };
  439.  
  440. /**
  441. * Try to load a SVG file matching the given char code.
  442. * @thanks to the incredible work made by KanjiVG
  443. * @see: http://kanjivg.tagaini.net
  444. */
  445. function loadSvg(uri, index, charCode, callbacks) {
  446. var xhr = new XMLHttpRequest(),
  447. code = ("00000" + charCode).slice(-5);
  448.  
  449. // Skip space character
  450. if(code === "00020" || code === "03000") {
  451. callbacks.done(index, {
  452. paths: [],
  453. texts: []
  454. });
  455. return;
  456. }
  457.  
  458. xhr.open("GET", uri + code + ".svg", true);
  459. xhr.onreadystatechange = function () {
  460. if (xhr.readyState === 4) {
  461. if (xhr.status === 200) {
  462. callbacks.done(index, parseResponse(xhr.response, code));
  463. } else {
  464. callbacks.error(xhr.statusText);
  465. }
  466. }
  467. };
  468. xhr.send();
  469. }
  470.  
  471. /**
  472. * Simple parser to extract paths and texts data.
  473. */
  474. function parseResponse(response, code) {
  475. var data = [],
  476. dom = new DOMParser().parseFromString(response, "application/xml"),
  477. texts = dom.querySelectorAll("text"),
  478. groups = [],
  479. i;
  480. // Private recursive function to parse DOM content
  481. function __parse(element) {
  482. var children = element.childNodes,
  483. i;
  484.  
  485. for(i = 0; i < children.length; i++) {
  486. if(children[i].tagName === "g") {
  487. groups.push(children[i].getAttribute("id"));
  488. __parse(children[i]);
  489. groups.splice(groups.indexOf(children[i].getAttribute("id")), 1);
  490. }
  491. else if(children[i].tagName === "path") {
  492. data.push({
  493. "path" : children[i].getAttribute("d"),
  494. "groups" : groups.slice(0)
  495. });
  496. }
  497. }
  498. }
  499.  
  500. // Start parsing
  501. __parse(dom.getElementById("kvg:" + code));
  502.  
  503. // And finally add order mark information
  504. for (i = 0; i < texts.length; i++) {
  505. data[i].text = {
  506. "value" : texts[i].textContent,
  507. "x" : texts[i].getAttribute("transform").split(" ")[4],
  508. "y" : texts[i].getAttribute("transform").split(" ")[5].replace(")", "")
  509. };
  510. }
  511. return data;
  512. }
  513.  
  514. window.DmakLoader = DmakLoader;
  515. }());