暂无描述
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

card.ts 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. module rpgcards {
  2. function normalizeTag(tag: string): string {
  3. return tag.trim().toLowerCase();
  4. }
  5. function splitParams(value: string): string[] {
  6. return value.split("|").map(function (str) { return str.trim(); });
  7. }
  8. export class Options {
  9. foreground_color: string;
  10. background_color: string;
  11. empty_color: string;
  12. default_color: string;
  13. default_icon: string;
  14. default_title_size: string;
  15. page_size: string;
  16. page_rows: number;
  17. page_columns: number;
  18. card_arrangement: string;
  19. card_size: string;
  20. card_count: number;
  21. icon_inline: boolean;
  22. constructor() {
  23. this.foreground_color = "white";
  24. this.background_color = "white";
  25. this.empty_color = "white";
  26. this.default_color = "black";
  27. this.default_icon = "ace";
  28. this.default_title_size = "13pt";
  29. this.page_size = "A4";
  30. this.page_rows = 3;
  31. this.page_columns = 3;
  32. this.card_arrangement = "doublesided";
  33. this.card_size = "25x35";
  34. this.card_count = null;
  35. this.icon_inline = true;
  36. }
  37. }
  38. export class Card {
  39. count: number;
  40. title: string;
  41. title_size: string;
  42. color: string;
  43. color_front: string;
  44. color_back: string;
  45. icon: string;
  46. icon_front: string;
  47. icon_back: string;
  48. contents: string[];
  49. tags: string[];
  50. constructor() {
  51. this.count = 1;
  52. this.title = "New card";
  53. this.title_size = null;
  54. this.color = null;
  55. this.color_front = null;
  56. this.color_back = null;
  57. this.icon = null;
  58. this.icon_front = null;
  59. this.icon_back = null;
  60. this.contents = [];
  61. this.tags = [];
  62. }
  63. static fromJSON(json: any): Card {
  64. var result = new Card;
  65. result.count = json.count || 1;
  66. result.title = json.title || "";
  67. result.title_size = json.title_size || null;
  68. result.color = json.color || null;
  69. result.color_front = json.color_front || null;
  70. result.color_back = json.color_back || null;
  71. result.icon = json.icon || null;
  72. result.icon_front = json.icon_front || null;
  73. result.icon_back = json.icon_back || null;
  74. result.contents = json.contents || [];
  75. result.tags = json.tags || [];
  76. return result;
  77. }
  78. public toJSON(): any {
  79. return {
  80. count: this.count,
  81. title: this.title,
  82. title_size: this.title_size,
  83. color: this.color,
  84. color_front: this.color_front,
  85. color_back: this.color_back,
  86. icon: this.icon,
  87. icon_front: this.icon_front,
  88. icon_back: this.icon_back,
  89. contents: this.contents.slice(),
  90. tags: this.tags.slice()
  91. }
  92. }
  93. public duplicate(): Card {
  94. return Card.fromJSON(this.toJSON());
  95. }
  96. public hasTag(tag: string): boolean {
  97. var index = this.tags.indexOf(normalizeTag(tag));
  98. return index > -1;
  99. }
  100. public addTag(tag: string): void {
  101. if (!this.hasTag(tag)) {
  102. this.tags.push(normalizeTag(tag));
  103. }
  104. }
  105. public removeTag(tag: string): void {
  106. var ntag = normalizeTag(tag);
  107. this.tags = this.tags.filter(function (t) {
  108. return ntag != t;
  109. });
  110. }
  111. public getTitle(options: Options): string {
  112. return this.title || "";
  113. }
  114. public getTitleSize(options: Options): string {
  115. return this.title_size || options.default_title_size || "13pt";
  116. }
  117. public getColorFront(options: Options): string {
  118. return this.color_front || this.color || options.default_color || "black";
  119. }
  120. public getColorBack(options: Options): string {
  121. return this.color_back || this.color || options.default_color || "black";
  122. }
  123. public getIconFront(options: Options): string {
  124. return this.icon_front || this.icon || options.default_icon || "ace";
  125. }
  126. public getIconBack(options: Options): string {
  127. return this.icon_back || this.icon || options.default_icon || "ace";
  128. }
  129. };
  130. type ContentGeneratorFunction = (params: string[], card: Card, options: Options, ind: string, ind0: string) => string;
  131. type CardGeneratorFunction = (card: Card, options: Options, ind: string, ind0: string) => string;
  132. export class CardHtmlGenerator {
  133. constructor() {
  134. }
  135. private _icon(src: string, ind: string, ind0: string): string {
  136. return ind + '<card-icon src="/icons/' + src + '.svg"></card-icon>\n';
  137. }
  138. private _subtitle(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  139. var text = params[0] || "";
  140. return ind + '<card-subtitle>' + text + '</card-subtitle>\n';
  141. }
  142. private _ruler(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  143. return ind + '<card-rule></card-rule>\n';
  144. }
  145. private _boxes(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  146. var count = params[0] || 1;
  147. var size = params[1] || 3;
  148. return ind + '<card-boxes size="' + size + '" count="' + count + '"></card-boxes>\n';
  149. }
  150. private _property(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  151. var header = params[0] || "";
  152. var text = params[1] || "";
  153. var result = "";
  154. result += ind + '<card-property>\n';
  155. result += ind + ind0 + '<h4>' + header + '</h4>\n';
  156. result += ind + ind0 + '<p>' + text + '</p>\n';
  157. result += ind + '</card-property>\n';
  158. return result;
  159. }
  160. private _description(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  161. var header = params[0] || "";
  162. var text = params[1] || "";
  163. var result = "";
  164. result += ind + '<card-description>\n';
  165. result += ind + ind0 + '<h4>' + header + '</h4>\n';
  166. result += ind + ind0 + '<p>' + text + '</p>\n';
  167. result += ind + '</card-description>\n';
  168. return result;
  169. }
  170. private _text(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  171. var text = params[0] || "";
  172. var result = "";
  173. result += ind + '<card-description>\n';
  174. result += ind + ind0 + '<p>' + text + '</p>\n';
  175. result += ind + '</card-description>\n';
  176. return result;
  177. }
  178. private _dndstats(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  179. var stats = ["str", "dex", "con", "int", "wis", "cha"];
  180. var result = "";
  181. result += ind + '<card-dndstats';
  182. for (var i = 0; i < stats.length; ++i) {
  183. var value = parseInt(params[0], 10) || "";
  184. var stat = stats[i];
  185. result += ' ' + stat + '="' + value + '"';
  186. }
  187. result += '></card-dndstats>\n';
  188. return result;
  189. }
  190. private _bullet(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  191. var text = params[0] || "";
  192. return ind + '<card-bullet>' + text + '</card-bullet>\n';
  193. }
  194. private _section(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  195. var text = params[0] || "";
  196. return ind + '<card-section>' + text + '</card-section>\n';
  197. }
  198. private _fill(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  199. var size = params[0] || "1";
  200. return ind + '<card-fill size="' + size + '"></card-fill>\n';
  201. }
  202. private _unknown(params: string[], card: Card, options: Options, ind: string, ind0: string): string {
  203. var text = params.join(' | ');
  204. return ind + '<card-description><p>' + text + '</p></card-description>\n';
  205. }
  206. private _empty(params: string[], card: Card, options: Options, ind: string, ind0: string) {
  207. return '';
  208. }
  209. private _contents(contents: string[], card: Card, options: Options, ind: string, ind0: string): string {
  210. var result = "";
  211. result += ind + '<card-contents>\n';
  212. result += contents.map(function (value) {
  213. var parts = splitParams(value);
  214. var name = parts[0];
  215. var params = parts.splice(1);
  216. var generator: ContentGeneratorFunction = null;
  217. switch (name) {
  218. case "subtitle": generator = this._subtitle; break;
  219. case "property": generator = this._property; break;
  220. case "rule": generator = this._ruler; break;
  221. case "ruler": generator = this._ruler; break;
  222. case "boxes": generator = this._boxes; break;
  223. case "description": generator = this._description; break;
  224. case "dndstats": generator = this._dndstats; break;
  225. case "text": generator = this._text; break;
  226. case "bullet": generator = this._bullet; break;
  227. case "fill": generator = this._fill; break;
  228. case "section": generator = this._section; break;
  229. case "disabled": generator = this._empty; break;
  230. case "": generator = this._empty; break;
  231. default: return this._unknown(parts, card, options, ind, ind0);
  232. }
  233. return generator(params, card, options, ind + ind0, ind);
  234. }).join("\n");
  235. result += ind + '</card-contents>\n';
  236. return result;
  237. }
  238. private _title(card: Card, options: Options, ind: string, ind0: string): string {
  239. var title = card.getTitle(options);
  240. var title_size = card.getTitleSize(options)
  241. var icon = card.getIconFront(options);
  242. var result = "";
  243. result += ind + '<card-title size="' + title_size + '">\n';
  244. result += ind + ind0 + '<h1>' + title + '</h1>\n';
  245. result += ind + ind0 + '<h2>' + "" + '</h2>\n';
  246. result += this._icon(icon, ind + ind0, ind0);
  247. result += ind + '</card-title>\n';
  248. return result;
  249. }
  250. private _card_front(card: Card, options: Options, ind: string, ind0: string): string {
  251. var result = "";
  252. result += this._title(card, options, ind + ind0, ind0);
  253. result += this._contents(card.contents, card, options, ind + ind0, ind0);
  254. return result;
  255. }
  256. private _card_back(card: Card, options: Options, ind: string, ind0: string): string {
  257. var icon = card.getIconBack(options);
  258. var result = "";
  259. result += ind + '<card-back>\n';
  260. result += this._icon(icon, ind + ind0, ind);
  261. result += ind + '</card-back>\n';
  262. return result;
  263. }
  264. private _card_empty(options: Options, ind: string, ind0: string): string {
  265. return '';
  266. }
  267. private _card(options: Options, ind: string, ind0: string, content: string, color: string): string {
  268. var size = options.card_size || "25x35";
  269. var result = "";
  270. result += ind + '<rpg-card color="' + color + '" size="' + size + '">\n';
  271. result += content;
  272. result += ind + '</rpg-card>\n';
  273. return result;
  274. }
  275. /** Generates HTML for the front side of the given card */
  276. public card_front(card: Card, options: Options, indent: string): string {
  277. var content = this._card_front(card, options, "", indent);
  278. return this._card(options, "", indent, content, card.getColorFront(options));
  279. }
  280. /** Generates HTML for the back side of the given card */
  281. public card_back(card: Card, options: Options, indent: string): string {
  282. var content = this._card_back(card, options, "", indent);
  283. return this._card(options, "", indent, content, card.getColorBack(options));
  284. }
  285. /** Generates HTML for an empty given card */
  286. public card_empty(options: Options, indent: string): string {
  287. var content = this._card_empty(options, "", indent);
  288. return this._card(options, "", indent, content, options.empty_color);
  289. }
  290. }
  291. class CardPage<T> {
  292. rows: number;
  293. cols: number;
  294. cards: T[];
  295. constructor(rows: number, cols: number) {
  296. this.rows = rows;
  297. this.cols = cols;
  298. this.cards = [];
  299. }
  300. /** Returns an empty page with the same dimensions */
  301. public newPage(): CardPage<T> {
  302. return new CardPage<T>(this.rows, this.cols);
  303. }
  304. private _posToIndex(row: number, col: number): number {
  305. return row * this.cols + col;
  306. }
  307. /** Adds one card to the page */
  308. public addCard(card: T): void {
  309. if (this.capacity() === 0) {
  310. throw new Error("This page is full.");
  311. }
  312. this.cards.push(card);
  313. }
  314. /**
  315. Adds several copies of a card to the page.
  316. Returns the number of copies that did not fit on the page.
  317. */
  318. public addCards(card: T, count: number): number {
  319. while (this.capacity() > 0 && count > 0) {
  320. this.addCard(card);
  321. --count;
  322. }
  323. return count;
  324. }
  325. /** Fills all remaining slots on the current row with the given card */
  326. public fillRow(card: T): void {
  327. while (this.capacityRow() > 0) {
  328. this.addCard(card);
  329. }
  330. }
  331. /** Fills all remaining slots on the page with empty cards */
  332. public fillPage(card: T): void {
  333. while (this.capacity() > 0) {
  334. this.addCard(card);
  335. }
  336. }
  337. /** Empty slots on the page */
  338. public capacity(): number {
  339. return this.cards.length - this.rows * this.cols;
  340. }
  341. /** Empty slots on the current line */
  342. public capacityRow(): number {
  343. return this.capacity() % this.cols;
  344. }
  345. /** Flip card slots horizontally */
  346. public flipH() {
  347. if (this.capacity() > 0) {
  348. throw new Error("Cannot perform this operation while the page is not full");
  349. }
  350. for (var r = 0; r < Math.floor(this.rows / 2); ++r) {
  351. for (var c = 0; c < this.cols; ++c) {
  352. var indexL = this._posToIndex(r, c);
  353. var indexR = this._posToIndex(this.rows - r - 1, c);
  354. var cardL = this.cards[indexL];
  355. var cardR = this.cards[indexR];
  356. this.cards[indexL] = cardR;
  357. this.cards[indexR] = cardL;
  358. }
  359. }
  360. }
  361. }
  362. class CardPageSet<T> {
  363. rows: number;
  364. cols: number;
  365. pages: CardPage<T>[];
  366. constructor(rows: number, cols: number) {
  367. this.rows = rows;
  368. this.cols = cols;
  369. this.pages = [];
  370. }
  371. public lastPage(): CardPage<T> {
  372. if (this.pages.length === 0) {
  373. return null;
  374. } else {
  375. return this.pages[this.pages.length - 1];
  376. }
  377. }
  378. public addPage(): CardPage<T> {
  379. var newPage = new CardPage<T>(this.rows, this.cols);
  380. this.pages.push(newPage);
  381. return newPage;
  382. }
  383. /**
  384. Adds one card to the last page.
  385. Adds a new pages if necessary.
  386. */
  387. public addCard(card: T): void {
  388. var page = this.lastPage();
  389. if (page === null || page.capacity() === 0) {
  390. page = this.addPage();
  391. }
  392. page.addCard(card);
  393. }
  394. /**
  395. Adds several copies of a card to the last page.
  396. Adds new pages if necessary.
  397. */
  398. public addCards(card: T, count: number): void {
  399. for (let i = 0; i < count; ++i) {
  400. this.addCard(card);
  401. }
  402. }
  403. public forEach(fn: (page: CardPage<T>)=>void) {
  404. this.pages.forEach(fn);
  405. }
  406. public merge(other: CardPageSet<T>): CardPageSet<T> {
  407. if (this.pages.length !== other.pages.length) {
  408. throw new Error("This function is only for merging two equally sized page sets");
  409. }
  410. var result = new CardPageSet<T>(this.rows, this.cols);
  411. for (var i = 0; i < this.pages.length; ++i) {
  412. result.pages.push(this.pages[i]);
  413. result.pages.push(other.pages[i]);
  414. }
  415. return result;
  416. }
  417. }
  418. export class PageHtmlGenerator {
  419. indent: string;
  420. constructor() {
  421. this.indent = " ";
  422. }
  423. private _pageColor(page: number, options: Options): string {
  424. if ((options.card_arrangement == "doublesided") && (page % 2 == 1)) {
  425. return options.background_color;
  426. } else {
  427. return options.foreground_color;
  428. }
  429. }
  430. private _wrap(pageSet: CardPageSet<string>, options: Options) {
  431. var size = options.page_size || "A4";
  432. var result = "";
  433. for (var i = 0; i < pageSet.pages.length; ++i) {
  434. var page = pageSet.pages[i];
  435. var style = 'style="background-color:' + this._pageColor(i, options) + '"';
  436. result += '<page class="page page-preview" size="' + size + '" ' + style + '>\n';
  437. result += page.cards.join("");
  438. result += '</page>\n';
  439. }
  440. return result;
  441. }
  442. private _generatePagesDoublesided(cards: Card[], options: Options, rows: number, cols: number, generator: CardHtmlGenerator): CardPageSet<string> {
  443. var front_pages: CardPageSet<string> = new CardPageSet<string>(rows, cols);
  444. var back_pages: CardPageSet<string> = new CardPageSet<string>(rows, cols);
  445. var empty = generator.card_empty(options, this.indent);
  446. // Fill pages with cards
  447. for (let i = 0; i < cards.length; ++i) {
  448. var card = cards[i];
  449. var front = generator.card_front(card, options, this.indent);
  450. var back = generator.card_back(card, options, this.indent);
  451. front_pages.addCards(front, card.count);
  452. back_pages.addCards(back, card.count);
  453. }
  454. // Fill empty slots
  455. front_pages.forEach((page) => page.fillPage(empty));
  456. back_pages.forEach((page) => page.fillPage(empty));
  457. // Shuffle back cards so that they line up with their corresponding front cards
  458. back_pages.forEach((page) => page.flipH());
  459. // Interleave front and back pages so that we can print double-sided
  460. return front_pages.merge(back_pages);
  461. }
  462. private _generatePagesFrontOnly(cards: Card[], options: Options, rows: number, cols: number, generator: CardHtmlGenerator): CardPageSet<string> {
  463. var pages: CardPageSet<string> = new CardPageSet<string>(rows, cols);
  464. var empty = generator.card_empty(options, this.indent);
  465. // Fill pages with cards
  466. for (let i = 0; i < cards.length; ++i) {
  467. var card = cards[i];
  468. var front = generator.card_front(card, options, this.indent);
  469. pages.addCards(front, card.count);
  470. }
  471. return pages;
  472. }
  473. private _generatePagesSideBySide(cards: Card[], options: Options, rows: number, cols: number, generator: CardHtmlGenerator): CardPageSet<string> {
  474. if (cols < 2) {
  475. throw new Error("Need at least two columns for side-by-side");
  476. }
  477. var pages: CardPageSet<string> = new CardPageSet<string>(rows, cols);
  478. var empty = generator.card_empty(options, this.indent);
  479. // Fill pages with cards (two at a time)
  480. for (let i = 0; i < cards.length; ++i) {
  481. var card = cards[i];
  482. var front = generator.card_front(card, options, this.indent);
  483. var back = generator.card_back(card, options, this.indent);
  484. if (pages.lastPage().capacityRow() < 2) {
  485. pages.lastPage().fillRow(empty);
  486. }
  487. pages.addCards(front, card.count);
  488. pages.addCards(back, card.count);
  489. }
  490. return pages;
  491. }
  492. private _generatePages(cards: Card[], options: Options, rows: number, cols: number, generator: CardHtmlGenerator): CardPageSet<string> {
  493. switch (options.card_arrangement) {
  494. case "doublesided": return this._generatePagesDoublesided(cards, options, rows, cols, generator);
  495. case "front_only": return this._generatePagesFrontOnly(cards, options, rows, cols, generator);
  496. case "side_by_side": return this._generatePagesSideBySide(cards, options, rows, cols, generator);
  497. default: throw new Error("Unknown card arrangement");
  498. }
  499. }
  500. private _generateStyle(options) {
  501. var size = "a4";
  502. switch (options.page_size) {
  503. case "A3": size = "A3 portrait"; break;
  504. case "A4": size = "210mm 297mm"; break;
  505. case "A5": size = "A5 portrait"; break;
  506. case "Letter": size = "letter portrait"; break;
  507. case "25x35": size = "2.5in 3.5in"; break;
  508. default: size = "auto";
  509. }
  510. var result = "";
  511. result += "<style>\n";
  512. result += "@page {\n";
  513. result += " margin: 0;\n";
  514. result += " size:" + size + ";\n";
  515. result += " -webkit-print-color-adjust: exact;\n";
  516. result += "}\n";
  517. result += "</style>\n";
  518. return result;
  519. }
  520. public generateHtml(cards: Card[], options: Options) {
  521. options = options || new Options();
  522. var rows = options.page_rows || 3;
  523. var cols = options.page_columns || 3;
  524. // Generate the HTML for each card
  525. var generator = new CardHtmlGenerator();
  526. var pages: CardPageSet<string> = this._generatePages(cards, options, rows, cols, generator);
  527. // Wrap all pages in a <page> element
  528. var document = this._wrap(pages, options);
  529. // Generate the HTML for the page layout
  530. var style = this._generateStyle(options);
  531. // Wrap all pages in a <page> element and add CSS for the page size
  532. var result = "";
  533. result += style
  534. result += document;
  535. return result;
  536. }
  537. public insertInto(cards: Card[], options: Options, container: HTMLElement) {
  538. // Clear the previous content of the document
  539. while (container.hasChildNodes()) {
  540. container.removeChild(container.lastChild);
  541. }
  542. // Insert the HTML
  543. var html = this.generateHtml(cards, options);
  544. container.innerHTML = html;
  545. }
  546. }
  547. }