Robert Carnecky 10 vuotta sitten
vanhempi
commit
b90722cad9
2 muutettua tiedostoa jossa 634 lisäystä ja 1 poistoa
  1. 631
    0
      generator/js/card.ts
  2. 3
    1
      package.json

+ 631
- 0
generator/js/card.ts Näytä tiedosto

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

+ 3
- 1
package.json Näytä tiedosto

@@ -6,7 +6,9 @@
6 6
   "author": "crobi",
7 7
   "dependencies": {
8 8
     "mv": "*",
9
-    "walk": "*"
9
+    "walk": "*",
10
+    "typescript": "*",
11
+    "http-server": "*"
10 12
   },
11 13
   "license": "MIT"
12 14
 }

Loading…
Peruuta
Tallenna