· 5 years ago · Oct 20, 2020, 07:22 PM
1<!DOCTYPE html>
2<meta charset="utf-8">
3<meta name="referrer" content="no-referrer">
4<meta name="viewport" content="width=device-width, initial-scale=1.0">
5<script src="https://cdn.jsdelivr.net/combine/npm/replaceall@0.1.6,npm/jszip@3/dist/jszip.min.js,npm/jquery@3,npm/jimp@0.16.1/browser/lib/jimp.js"></script>
6<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
7<link rel="stylesheet" href="https://use.typekit.net/dkw1lrx.css">
8<link href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AkREwo3+IbxEwAAAjZJREFUeNrtmz9LJDEchh/vLD28ZgsbBZtDUBBbLRVld7lDRc/mvoQfxUqwEbXwHwoLesVZXnuchTaChVaid3hy2glaGGEY3Ux2J+Pml+SFsGSSTPI+bDKTTAJRUUGrLSN9FhgDSgZ5XdMD8Ac4ANYbLTwA3Kib+BD+A4Om5vs9Mp4OgyZd4B/Q6WmXvwU+JC+8S2WY9tg8QAcwpwMwGsDAP6oDUAoAQEkHILhHf4gAiAAigAhAFIDfwApwVMTN2x03PwXsJuLfFIwg/gHVlHmAVeBrCAAqwF6dtE01TfcWwAywn5FnS+XzEsCdYb5tNSZ4A+BE/e4Dw4Zl1oBjHwBUgU/Aoor/bADCL5sN2ebtV2nKqTYsJdJMIBw2WF/NJQDlOu1YTuQZ0bR3sok6nQFQNujfOgjjTdbrBICKYZfcqANhIkfdLQdQaXBc2kmVnc5Zf0sBVJocnGsW21Br1WOwqnm9zdJn4EzydDiP+ec+3yMVgA3z36UuiDhtvmgAzpsvEoAI80UBEGO+CACizNsGIM68TQAizdsCMC/VvC0Ah1LN2wJwLtW8LQBnUs3bAnAv1bwNABeSzdsAcC7Z/FsCGHfRPOT/PD5U5/p7oFstYgwACziqvAB6gVOevs50q9CFINnYINGrgkjFPUIRQAQQAYSmBx2AqwAAXOoA/AgAwEEy8tqRmWvgo6fmM4/MgH5HhnQZe+sD/uLPabFrnk7DvVDWYcgvaiYn9eDklRrXdomKinpNj+Ks7+3FrzuXAAAAAElFTkSuQmCC" rel="shortcut icon">
9<title>Ghetto PCIO Editor v14</title>
10
11<!--
12 BUTTONS
13 The buttons are defined in "var buttons".
14 The key is an icon from https://material.io/resources/icons/?style=baseline .
15
16 There are three different buttons: If there is a "ONE_SHOT" after the button tooltip (for example folder_open) it's a ONE_SHOT function.
17 The defined function gets called once when the button is pressed.
18
19 If there is a "TOGGLE_TOOL" after the button tooltip (for example notes) it toggles the tool that comes afterwards.
20
21 If there isn't any number, then there is a "function(allSelected, i, widget)" (for example align_vertical_top).
22 The given function gets called once for every selected widget.
23 The parameters:
24 allSelected - an array of all selected widgets (the same for every call of the function)
25 i - a simple counter incremented each function call
26 widget - one of the selected widgets.
27
28 Some buttons are a bit hacky. They are of the latter type but only do their stuff once (by doing i === 0).
29 For example mediation. I do that if I want to work with the selected widgets but a normal iteration doesn't quite work.
30
31 TOOLS
32 Some buttons need further settings and open a tool. The <div id="tools"> contains all those tools as separate divs
33 that are hidden by default and get toggled by the button function.
34
35 Then just add buttons or whatever to that div and add a normal jQuery click handler somewhere at the bottom where the others are:
36 $("#editImages [type=button]").on("click", function() {
37 ...
38 });
39 For multiple buttons it's probably a good idea to give them IDs instead.
40-->
41
42<div class="primary toolbar"></div>
43<div class="secondary toolbarContainer"></div>
44<div id="room"></div>
45<div id="tools">
46 <input type="file" id="file">
47 <input type="file" id="fileStripped">
48 <input type="file" id="fileImportWidgets">
49 <input type="file" id="fileCustomBoard">
50 <div id="toolBox">
51 <h2>Standard widgets creation</h2>
52 <h3>Cards</h3>
53 <input type=button id=WCardHolder value="Empty Card Holder"><br>
54 <input type=button id=WdeckStandard value="Standard 52 Card Deck">
55 <input type=button id=WdeckExtended value="Extended Card Deck (1-15)">
56 <input type=button id=WdeckPiquet value="Piquet Pack (7-A)">
57 <input type=button id=WdeckEuchre value="Euchre Deck (9-A)">
58 <input type=button id=WdeckSpanish value="Spanish Deck"><br>
59 <input type=button id=WdeckCustom value="Custom Card Deck">
60
61 <h3>Pieces</h3>
62 <input type=button id=WSpinner value="Random Spinner">
63 <input type=button id=WCounter value="Counter"><br>
64 <input type=button class=WPiece data-type=classic value="Game Piece">
65 <input type=button class=WPiece data-type=checkers value="Checkers Piece">
66 <input type=button class=WPiece data-type=pin value="Pin Piece"><br>
67 <input type=button id=WAutomationButton value="Automation Button">
68
69 <h3>Game Boards</h3>
70 <input type=button id=WBoardChess value="Chess Board">
71 <input type=button id=WBoardBackgammon value="Backgammon Board">
72 <input type=button id=WBoardCribbage value="Cribbage Board"><br>
73 <input type=button id=WBoardCustom value="Custom Board">
74
75 <h2>Ghetto widgets creation</h2>
76 <h3>Cards</h3>
77 <input type=button id=WdeckStandardOld value="Old 52 Card Deck">
78 <input type=button id=WdeckPiquetOld value="Old Piquet Pack (7-A)">
79 <input type=button id=WdeckEuchreOld value="Old Euchre Deck (9-A)"><br>
80 <input type=button id=WdeckCustomOld value="Old Custom Card Deck">
81
82 <h3>Pieces</h3>
83 <input type=button id=WMainHand value="Main Hand">
84 <input type=button id=WExtraHand value="Extra Hand"><br>
85 <input type=button id=WGhettoSpinner value="Custom Spinner"><br>
86 <h5>Game Pieces</h5>
87 <select name="Color" id="GPColor">
88 <option value="red">Red</option>
89 <option value="orange">Orange</option>
90 <option value="yellow">Yellow</option>
91 <option value="green">Green</option>
92 <option value="blue">Blue</option>
93 <option value="purple">Purple</option>
94 <option value="black">Black</option>
95 <option value="white">White pin or dark black classic/checkers</option>
96 </select>
97 <input type=button class=WColorPiece value="Classic">
98 <input type=button class=WColorPiece value="Checkers">
99 <input type=button class=WColorPiece value="Pin"><br>
100 <h5>Chess pieces</h5>
101 <select name="Color" id="ChessColor">
102 <option value="white">White</option>
103 <option value="black">Black</option>
104 </select>
105 <input type=button class=WChess value="Pawn">
106 <input type=button class=WChess value="Rook">
107 <input type=button class=WChess value="Knight">
108 <input type=button class=WChess value="Bishop">
109 <input type=button class=WChess value="Queen">
110 <input type=button class=WChess value="King"><br>
111
112 <h3>Game Boards</h3>
113 <input type=button id=WBoardFull value="1600*1000 Board">
114 </div>
115 <div id="tabletopImporter">
116 <h2>Tabletop Simulator importer</h2>
117 <ol>
118 <li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS</a> prevents downloading images. You have to install (and enable for the editor) an <a href="https://addons.mozilla.org/en-US/firefox/addon/cors-everywhere/?utm_source=addons.mozilla.org&utm_medium=referral&utm_content=search">extension for Firefox</a> or <a href="https://stackoverflow.com/questions/3102819/disable-same-origin-policy-in-chrome">start Chrome in a special way</a> to disable it. <b>I'm looking for a way around that</b>.</li>
119 <li>Search for a mod in <a href="https://steamcommunity.com/app/286160/workshop/">the steam workshop</a>.</li>
120 <li>Use <a href="https://steamworkshopdownloader.io/">the steam workshop downloader</a> to download it.</li>
121 <li>Select the file here: <input type="file" id="fileImportTabletop"></li>
122 <li>Wait. Seriously.</li>
123 </ol>
124 <div id="tabletopPreview" class="fullWidth">
125 </div>
126 </div>
127 <div id="typeFilter">
128 <h2>Filter widget types</h2>
129 <input type="checkbox" id="automationButton" checked><label for="automationButton" class="type-automationButton"> Automation Buttons</label><span class="hotkey"> <Shift>+A</span><br>
130 <input type="checkbox" id="board" checked><label for="board" class="type-board"> Boards</label><span class="hotkey"> <Shift>+B</span><br>
131 <input type="checkbox" id="card"><label for="card" class="type-card"> Cards</label><span class="hotkey"> <Shift>+C</span><br>
132 <input type="checkbox" id="cardDeck" checked><label for="cardDeck" class="type-cardDeck"> Card Decks</label><span class="hotkey"> <Shift>+D</span><br>
133 <input type="checkbox" id="cardPile" checked><label for="cardPile" class="type-cardPile"> Card Piles</label><span class="hotkey"> <Shift>+P</span><br>
134 <input type="checkbox" id="counter" checked><label for="counter" class="type-counter"> Counters</label><span class="hotkey"> <Shift>+T</span><br>
135 <input type="checkbox" id="gamePiece" checked><label for="gamePiece" class="type-gamePiece"> Game Pieces</label><span class="hotkey"> <Shift>+G</span><br>
136 <input type="checkbox" id="hand" checked><label for="hand" class="type-hand"> Hands</label><span class="hotkey"> <Shift>+N</span><br>
137 <input type="checkbox" id="spinner" checked><label for="spinner" class="type-spinner"> Spinners</label><span class="hotkey"> <Shift>+I</span><br>
138 <input type="checkbox" id="label"><label for="label" class="type-label"> Label</label> (virtual widget type for attached labels)<span class="hotkey"> <Shift>+L</span>
139 </div>
140 <div id="jsonEdit">
141 <h2>Direct JSON editing</h2>
142 <p><input type="checkbox" id="compactAB"><label for="compactAB"> Compact Automation Button clickRoutine</label></p>
143 <div class="hideWithMulti">
144 <textarea id="json"></textarea><br><input type="button" value="Set" class="set"><br>
145 Default size<span id="defaultSize"></span>
146 </div>
147 <p>(only visible if exactly one widget is selected)</p>
148 <p>See <a href="https://www.reddit.com/user/RaphaelAlvez/">u/RaphaelAlvez'</a> <a href="https://www.notion.so/Playingcards-io-s-documentation-1ae08cee1f6043f599e92229ae122cd5">unoffical documentation</a>.</p>
149 </div>
150 <div id="macros">
151 <h2>Macros</h2>
152 Preset: <select></select><br>
153 <textarea id="generateDefinition"></textarea><br><input type="button" value="Go">
154 <h3>Some notes</h3>
155 <ul>
156 <li>This shit is pretty powerful but requires some coding knowledge. Ask for help in <a href="https://www.reddit.com/r/PlayingCardsIO/comments/inuocb/pcio_editor/">my reddit post</a>. Also please share your code there.</li>
157 <li>The current representation of <tt>widgets.json</tt> is available as the variable <tt>widgets</tt>.</li>
158 <li>A card pile is 8px bigger than the cards in each dimension.</li>
159 </ul>
160 </div>
161 <div id="cloudUpload">
162 <h2>Replace packaged assets by links</h2>
163 <p>The packaged assets (images inside the pcio file) will be replaced by the online version (firebasestorage.googleapis.com links) the game created.</p>
164 <ol>
165 <li>Start Firefox (this does not work on Chrome).</li>
166 <li>Open a <a href="https://playingcards.io/room/new/generic">new playingcards.io room</a>.</li>
167 <li>Press <F12> to open the dev tools and select the "Network" tab.</li>
168 <li>Import your PCIO file into the room (and wait for it to finish).</li>
169 <li>Right-click somewhere in the list of network requests.</li>
170 <li>Select "Copy" and "Copy All As HAR".</li>
171 <li>Paste it into the text box below (takes a while) and press "Replace".</li>
172 <li>Save the PCIO as an "online" version. There's no way to undo this so save the original PCIO as well.</li>
173 </ol>
174 <p><textarea id="firefoxHAR"></textarea></p>
175 <p><input type="button" id="checkHAR" value="Replace"></p>
176 </div>
177 <div id="editCardDecks">
178 <h2>Edit card decks</h2>
179 <h3>Card deck settings</h3>
180 <input type="checkbox" id="cardOverlapH" checked><label for="cardOverlapH"> Overlap in hand</label><br>
181 <input type="checkbox" id="onRemoveFromHand" checked><label for="onRemoveFromHand"> Flip out of hand</label><br>
182 <input type="checkbox" id="confirmRecall"><label for="confirmRecall"> Recall confirmation</label><br>
183 <input type="checkbox" id="confirmRecallAll" checked><label for="confirmRecallAll"> Recall all confirmation</label><br>
184 <input type="checkbox" id="enlarge"><label for="enlarge"> Enlarge</label><br>
185 <input type="button" class="set" value="Set">
186 <h3>Edit cards</h3>
187 <p>Here you can merge card types, move/copy card types to a new deck and add simulated card rotation.</p>
188 <input type="button" class="preview" value="Preview">
189 <div id="editCardDecksPreview" class="fullWidth">
190 </div>
191 </div>
192 <div id="editImages">
193 <h2>Edit images</h2>
194 <p>This operates on the selected card decks and boards.</p>
195 <p>It uses JIMP which has <a href="https://github.com/oliver-moran/jimp/blob/master/packages/jimp/README.md#Methods">many editing functions</a>. Tell me if you need one not implemented here.</p>
196 <h3>Compress images</h3>
197 <p>Note: all other actions keep the dimensions and convert to PNG. Use compression to turn images back to JPG.</p>
198 Type: <select id="compressType"><option value="image/jpeg">JPG</option><option value="image/png">PNG</option></select> - Quality (JPG only): <input id="compressQuality" value="0.7" class="short"><br>
199 Width: <input id="compressWidth" class="short" value="200"><br>
200 <input type="button" class="compress" value="Preview">
201 <h3>Rotate images</h3>
202 Degrees: <input id="rotateDegrees" class="short" value="90"><br>
203 <input type="checkbox" id="rotateResizeDeck" checked><label for="rotateResizeDeck"> Resize the deck/board</label><br>
204 <input type="button" class="rotate" value="Preview">
205 <h3>Crop images</h3>
206 X: <input id="cropX" class="short" value="10"> Y: <input id="cropY" class="short" value="10"><br>
207 Width: <input id="cropWidth" class="short" value="100"> Height: <input id="cropHeight" class="short" value="100"><br>
208 <input type="checkbox" id="cropResizeDeck" checked><label for="cropResizeDeck"> Resize the deck/board</label><br>
209 <input type="button" class="crop" value="Preview">
210 <div id="editPreview" class="fullWidth">
211 </div>
212 </div>
213 <div id="help">
214 <h2>Hotkeys</h2>
215 <ul>
216 <li>tooltips for the toolbar buttons show hotkeys (capital letters means press <b>Shift</b>)</li>
217 <li>see "filter widget types" for filter hotkeys</li>
218 <li><b>Ctrl</b>: hold while resizing to keep aspect ratio</li>
219 <li><b>Shift+Arrows</b>: move widgets by 1px</li>
220 <li><b>Ctrl+Shift+Arrows</b>: move widgets by 5px</li>
221 <li><b>Escape</b>: close all tools</li>
222 </ul>
223 <h2>Changelog</h2>
224 <h3>Updates</h3>
225 <p>For updates check <a href="https://www.reddit.com/r/PlayingCardsIO/comments/inuocb/pcio_editor/">my reddit post</a>.</p>
226 <!--
227 TODO LIST
228 "ruler"
229 fix "bla-copy01-copy01-copy02"
230 improve rotation for Bears vs Babies
231 add hotkeys for toolbar inputs
232 set coordinates and ID with ENTER
233 change "new" button to an init function
234 check uniqueID("gpe-board") - that shouldn't work
235 check https://imgur.com/a/1iIJvpc
236 paste into prompt only works if nothing was copied before
237 move/copy merge duplicate assets to unlink?
238 status bar is too small on narrow views (remove the headers?)
239
240 RaphaelAlvez:
241 - make widgets creation look better
242
243 KNOWN BUGS
244
245 WISH LIST
246 only add toolbar linebreaks between icon groups
247 It would be better to have copy and paste buttons and to make it possible to move all selected widgets with the mouse of course.
248 RaphaelAlvez:
249 -"maybe you should consider changing the merge cards into deck edditor. It only lacks changing the number of cards and renaming card types. but it's not really necessary"
250 -reset button on deck eddit. (the option don't match the deck's current properties)
251 -As a request, I saw that merging cards keep the cards with the same back face.
252 -I think you should get rid of the vertical menu for square scales
253 -->
254 <h3>v14 - 2020-10-20</h3>
255 <ul>
256 <li>Tabletop Simulator importer: added support for card decks with unique back images.</li>
257 <li>Tabletop Simulator importer: only importing the selected cards works now.</li>
258 <li>Tabletop Simulator importer: selected cards of different decks can now be imported into the same deck.</li>
259 <li>New secondary toolbars. Most features were there already but you can directly change widget IDs now.</li>
260 <li>Copy and paste.</li>
261 <li>"Direct coordinate editing" can now be set to use right/bottom as a reference.</li>
262 <li>Added a new button that replaces packaged images by links.</li>
263 <li>New macro to create a movement loop between piles.</li>
264 <li>Made the realistic preview more realistic.</li>
265 </ul>
266 <h3>v13 - 2020-10-07</h3>
267 <ul>
268 <li>Standard widgets can be added like in the vanilla editor thanks to <a href="https://www.reddit.com/user/RaphaelAlvez/">u/RaphaelAlvez</a>.</li>
269 <li>"Edit images" can also crop images now. And the resize checkboxes work.</li>
270 <li>"Merge card types" and "edit card deck settings" are now "edit card decks". It can also copy and split decks now and it can create card decks with rotated images.</li>
271 <li>Added <Shift>+<Arrows> as hotkeys for moving selected widgets by 1px (5px with <Shift>+<Ctrl>).</li>
272 <li>Added hotkeys for filtering several widget types.</li>
273 <li>Pressing <Ctrl> while resizing keeps the aspect ratio.</li>
274 <li>"Direct coordinate editing" can now also edit width and height.</li>
275 <li>The changelog button is now a help button and shows hotkeys.</li>
276 </ul>
277 <h3>v12 - 2020-09-28</h3>
278 <ul>
279 <li>"Compress images" is now "edit images". It can also rotate images now.</li>
280 <li>Tabletop Simulator importer: all import buttons should do something now.</li>
281 <li>Tabletop Simulator importer: back images work for simple cases.</li>
282 <li>Tabletop Simulator importer: custom mesh textures can be imported (probably useless).</li>
283 <li>Tabletop Simulator importer: you can set the desired height before importing.</li>
284 </ul>
285 <h3>v11 - 2020-09-27</h3>
286 <ul>
287 <li>Integrated Tabletop Simulator importer (still WIP).</li>
288 <li>Widgets can now also be resized with the mouse after toggling a button.</li>
289 <li>Added hotkeys to buttons.</li>
290 <li>Labels and recall buttons can now have a visual representation thanks to <a href="https://www.reddit.com/user/RaphaelAlvez/">u/RaphaelAlvez</a>.</li>
291 <li>JSON edit can now show a compressed version of the automation button clickRoutine.</li>
292 <li>Added a button that reselects previous widgets.</li>
293 <li>Added a button that toggles a pretty realistic preview (most parts thanks to <a href="https://www.reddit.com/user/RaphaelAlvez/">u/RaphaelAlvez</a>).</li>
294 </ul>
295 <h3>v10 - 2020-09-21</h3>
296 <ul>
297 <li>Renamed "generate widgets" to "macros" and added a lot of presets by <a href="https://www.reddit.com/user/RaphaelAlvez/">u/RaphaelAlvez</a>.</li>
298 <li>Active tools are now highlighted (and there's a button to close them all).</li>
299 <li>The filename can now be changed when saving.</li>
300 <li>Added favicon.</li>
301 </ul>
302 <h3>v9 - 2020-09-16</h3>
303 <ul>
304 <li>Turn all HTTP links into packaged assets.</li>
305 <li>Merge multiple card types with the same image into one.</li>
306 <li>Status bar with coordinates and status of some tools.</li>
307 <li>Board images can now also be compressed.</li>
308 <li>Widgets can now be moved with the mouse after toggling the button.</li>
309 <li>Saving works now without loading anything prior.</li>
310 <li>More colors for widget types (also in the filter selection).</li>
311 <li>Several widget generation presets by <a href="https://www.reddit.com/user/RaphaelAlvez/">u/RaphaelAlvez</a>.</li>
312 </ul>
313 <h3>v8 - 2020-09-15</h3>
314 <ul>
315 <li>Procedurally generate new widgets.</li>
316 </ul>
317 <h3>v7 - 2020-09-14</h3>
318 <ul>
319 <li>Import widgets from another PCIO file.</li>
320 </ul>
321 <h3>v6 - 2020-09-14</h3>
322 <ul>
323 <li>Undo.</li>
324 </ul>
325 <h3>v5 - 2020-09-13</h3>
326 <ul>
327 <li>Mass compress card images.</li>
328 <li>Remove unused card type variables and unused images.</li>
329 <li>Set card deck options.</li>
330 </ul>
331 <h3>v4 - 2020-09-12</h3>
332 <ul>
333 <li>Added "stripped" save and import buttons for quickly testing and changing things in the official editor without loading hundreds of card images.</li>
334 <li>Mass set X or Y spacing.</li>
335 <li>Direct coordinate editing now supports formulas.</li>
336 <li>Delete widgets.</li>
337 <li>Filter what widget types are being displayed.</li>
338 <li>Made the editor layout responsive.</li>
339 </ul>
340 <h3>v3 - 2020-09-08</h3>
341 <ul>
342 <li>Merge card decks.</li>
343 <li>Cleaned it up a bit.</li>
344 <li>Added a few descriptions and links.</li>
345 </ul>
346 <h3>v2 - 2020-09-07</h3>
347 <ul>
348 <li>Mass offset X or Y coordinates.</li>
349 <li>Alignment buttons.</li>
350 <li>Direct JSON editing.</li>
351 <li>Added a pretty awkward selection rectangle.</li>
352 <li>Board images are now being displayed.</li>
353 </ul>
354 <h3>v1 - 2020-09-06</h3>
355 <ul>
356 <li>Loading and saving PCIO files.</li>
357 <li>Simple rendering and selecting of widgets.</li>
358 <li>Mass set X or Y coordinates.</li>
359 </ul>
360 </div>
361</div>
362
363<div id="statusbar">
364 <span>Coordinates: <span id="statusCoordinates">0, 0</span></span>
365 <span>Widget: <span id="statusWidget">-</span></span>
366 <span>Tool status: <span id="statusTool">-</span></span>
367</div>
368<div id="selectionDiv"></div>
369
370<style>
371 body, #editCardDecks {
372 padding-bottom: calc(8px + 3vw);
373 -webkit-user-select: none;
374 -moz-user-select: none;
375 user-select: none;
376 }
377 body {
378 min-height: 1200px;
379 }
380 body, .tooltip {
381 font-family: ubuntu, sans;
382 }
383 #x, #y , #height, #width {
384 width: 50px;
385 }
386 textarea {
387 width: 95%;
388 height: 400px;
389 }
390 .error {
391 color: red;
392 font-weight: bold;
393 font-size: 200%;
394 }
395 .short {
396 width: 50px;
397 }
398 #macros select {
399 width: 95%;
400 }
401 #tools {
402 margin-right: 1610px;
403 }
404 .toolbar {
405 margin-bottom: 0;
406 }
407 .toolbar > span {
408 display: none;
409 }
410 .toolbar span + button {
411 margin-left: 16px;
412 }
413 button.material-icons {
414 padding: 1px;
415 border: 1px solid black;
416 margin: 2px;
417 position: relative;
418 }
419 button.material-icons.activated {
420 background: #888;
421 }
422 button.material-icons.toolbarButton.activated, .toolbarContainer .toolbar {
423 background: #aaa;
424 }
425 .toolbarContainer .toolbar {
426 border: 1px solid black;
427 }
428 button.material-icons.toolbarButton.activated {
429 border-bottom: 0;
430 top: 4px;
431 }
432 .toolbarContainer button.material-icons.toolbarButton.activated {
433 top: 5px;
434 }
435 .tooltip {
436 display: none;
437 font-size: 12px;
438 position: absolute;
439 text-align: left;
440 background: #ccc;
441 border: 1px solid black;
442 padding: 4px;
443 top: 31px;
444 left: -1px;
445 width: 200px;
446 white-space: normal;
447 z-index: 999999;
448 }
449 .tooltip ul + br {
450 display: none;
451 }
452 button:nth-child(n+14) .tooltip {
453 left: -183px;
454 }
455 button:hover > .tooltip {
456 display: block;
457 }
458 button.toolbarButton.activated:hover > .tooltip {
459 display: none;
460 }
461 .toolbar label:nth-child(1) {
462 margin-left: 6px;
463 }
464 .toolbar label {
465 vertical-align: super;
466 }
467 .toolbar input {
468 height: 26px;
469 padding: 0 6px;
470 text-align: right;
471 margin: 2px;
472 border: 1px solid black;
473 width: 50px;
474 vertical-align: top;
475 }
476 .toolbar input.text {
477 width: 200px;
478 text-align: left;
479 }
480 input.inactive {
481 color: #ccc;
482 }
483 #room {
484 position: absolute;
485 margin-top: 8px;
486 right: 8px;
487 width: 1600px;
488 height: 1000px;
489 border: 1px solid black;
490 }
491 #statusbar {
492 color: white;
493 background: black;
494 position: fixed;
495 right: 0;
496 bottom: 0;
497 padding: 4px;
498 left: 0;
499 font-size: 1vw;
500 }
501 #statusbar > * {
502 display: inline-block;
503 }
504 #statusbar > span:nth-child(1) {
505 width: 15%;
506 }
507 #statusbar > span:nth-child(2) {
508 width: 30%;
509 }
510 #statusbar > span:nth-child(3) {
511 width: 50%;
512 }
513 .fullWidth {
514 margin-top: 1000px;
515 margin-right: -1600px;
516 }
517 .hotkey {
518 color: #ccc;
519 }
520 .hotkey:hover {
521 color: initial;
522 }
523 .cardPreview {
524 display: inline-block;
525 position: relative;
526 vertical-align: middle;
527 border: 5px solid #ccc;
528 font-size: 0;
529 margin: 0 !important;
530 }
531 .cardPreview > img {
532 width: 200px;
533 }
534 .cardPreview > img.back {
535 width: 100px;
536 }
537 .cardPreview > div {
538 position: absolute;
539 left: 0;
540 right: 0;
541 top: 40px;
542 font-size: 60px;
543 display: inline;
544 text-align: center;
545 color: white;
546 text-shadow: -2px 2px 0 #000, 2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000;
547 }
548 .cardPreview.cardSelected {
549 border: 5px solid red;
550 }
551 #tabletopPreview div {
552 margin-left: 20px;
553 }
554 #tabletopPreview .empty > h4, #tabletopPreview .empty > button {
555 display: inline;
556 font-size: 8px;
557 padding: 0;
558 margin: 0 4px;
559 border: 0;
560 opacity: 0.5;
561 }
562 .targetHeight {
563 width: 50px;
564 }
565 #selectionDiv, .hideWithMulti, #tools > * {
566 display: none;
567 }
568 #selectionDiv {
569 border: 1px dotted #000;
570 position: fixed;
571 z-index: -1;
572 }
573 .widget, .widgetLabel {
574 position: absolute;
575 background: black;
576 opacity: 0.5;
577 background-size: cover;
578 font-size: 0;
579 }
580 .selected.widget {
581 opacity: 1;
582 }
583 .type-board {
584 background-color: #ccc;
585 z-index: 0 !important;
586 }
587 .type-automationButton { background: red; color: white; }
588 .type-card { background: teal; }
589 .type-cardDeck { background: blue; color: white; }
590 .type-cardPile { background: black; color: white; }
591 .type-counter { background: green; }
592 .type-gamePiece { background: orange; }
593 .type-hand { background: sienna; }
594 .type-spinner { background: purple; color: white; }
595 .type-label, .widgetLabel { background: cyan; }
596
597
598 .realistic {background :#f0f0f0;}
599 .realistic .widget, .realistic .widgetLabel { opacity: 1; box-sizing: border-box; border-width: 0; font-size: initial; }
600
601 .realistic .type-cardDeck { display: none; }
602 .realistic .type-hand.disabled { display: none; }
603
604 .realistic .type-cardPile { background:white; border-top: 1px solid #d8d8d8; border-left: 1px solid #d8d8d8; border-bottom: 1px solid #ccc; border-right: 1px solid #ccc; background: #fff; border-radius: 8px; }
605 .realistic .type-hand { background: #fff; border-top: 1px solid #d8d8d8; border-left: 1px solid #d8d8d8; border-bottom: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 0; border-top-left-radius: 6px; border-top-right-radius: 6px; bottom: 0;}
606 .realistic .widgetLabel {
607 background:white;
608 text-align: center;
609 font-size: 14px;
610 font-weight: normal;
611 letter-spacing: 0.1px;
612 line-height: 1em;
613 padding: 0;
614 background: linear-gradient(to bottom, #ffffff 0%,#f7f7f7 100%);
615 border-top: 1px solid #d8d8d8;
616 border-left: 1px solid #d8d8d8;
617 border-bottom: 1px solid #ccc;
618 border-right: 1px solid #ccc;
619 border-radius: 4px;
620 box-shadow: 1px 1px 1px #dedede;
621 color: #8e8e8e;
622 font-family: gill-sans-nova, sans-serif;
623 text-transform: uppercase;
624 }
625
626 .realistic .type-counter { background: url(https://s1.gifyu.com/images/counter.png); }
627 .realistic .type-card { background: url(https://playingcards.io/img/cardback-red.svg) #A23B2A; background-size: contain; background-repeat: no-repeat; background-position: center; border-radius: 8px; }
628
629 .realistic .type-spinner { border-radius: 800px; background:url(https://svgshare.com/i/Pzs.svg;) #e1e1e1; background-size:cover;}
630 .realistic .type-automationButton { border-radius: 800px; background: radial-gradient(circle, #a9e9e2 0%, #a9e9e2 64%, #93d0c9 64%); color: #6d6d6d; display:flex;justify-content:center;align-items:center;}
631
632 .realistic .type-gamePiece.subtype-pin.color-black { background: url(https://playingcards.io/img/pieces/pin-black.svg); }
633 .realistic .type-gamePiece.subtype-pin.color-blue { background: url(https://playingcards.io/img/pieces/pin-blue.svg); }
634 .realistic .type-gamePiece.subtype-pin.color-purple { background: url(https://playingcards.io/img/pieces/pin-purple.svg); }
635 .realistic .type-gamePiece.subtype-pin.color-red { background: url(https://playingcards.io/img/pieces/pin-red.svg); }
636 .realistic .type-gamePiece.subtype-pin.color-yellow { background: url(https://playingcards.io/img/pieces/pin-yellow.svg); }
637 .realistic .type-gamePiece.subtype-pin.color-green { background: url(https://playingcards.io/img/pieces/pin-green.svg); }
638 .realistic .type-gamePiece.subtype-pin.color-orange { background: url(https://playingcards.io/img/pieces/pin-orange.svg); }
639 .realistic .type-gamePiece.subtype-pin.color-white { background: url(https://playingcards.io/img/pieces/pin-white.svg); }
640
641 .realistic .type-gamePiece.subtype-pin { width:35.85px !important; height:43.83px !important;}
642 .realistic .type-gamePiece.subtype-classic { background:url(https://svgshare.com/i/PzL.svg); width:90px !important; height:90px !important;}
643 .realistic .type-gamePiece.subtype-checkers { border-radius: 800px; width:90px !important; height:90px !important;}
644
645
646 .realistic .type-gamePiece.subtype-classic.color-black { background: url(https://svgshare.com/i/Q1P.svg); }
647 .realistic .type-gamePiece.subtype-classic.color-blue { background: url(https://svgshare.com/i/PzK.svg); }
648 .realistic .type-gamePiece.subtype-classic.color-purple { background: url(https://svgshare.com/i/Q0v.svg); }
649 .realistic .type-gamePiece.subtype-classic.color-red { background: url(https://svgshare.com/i/Pzi.svg); }
650 .realistic .type-gamePiece.subtype-classic.color-yellow { background: url(https://svgshare.com/i/Pzg.svg); }
651 .realistic .type-gamePiece.subtype-classic.color-green { background: url(https://svgshare.com/i/Q0h.svg); }
652 .realistic .type-gamePiece.subtype-classic.color-orange { background: url(https://svgshare.com/i/Q0i.svg); }
653
654 .realistic .type-gamePiece.subtype-checkers { background: radial-gradient(circle, #000000 0%, #000000 33%, #525252 33%, #525252 46%, #000000 46%, #000000 58%, rgba(0, 0, 0, 0) 58%); }
655 .realistic .type-gamePiece.subtype-checkers.color-black { background: radial-gradient(circle, #4a4a4a 0%, #4a4a4a 33%, #848484 33%, #848484 46%, #4a4a4a 46%, #4a4a4a 58%, rgba(0, 0, 0, 0) 58%); }
656 .realistic .type-gamePiece.subtype-checkers.color-blue { background: radial-gradient(circle, #4c5fea 0%, #4c5fea 33%, #8693f1 33%, #8693f1 46%, #4c5fea 46%, #4c5fea 58%, rgba(0, 0, 0, 0) 58%); }
657 .realistic .type-gamePiece.subtype-checkers.color-purple { background: radial-gradient(circle, #bc5bee 0%, #bc5bee 33%, #d290f4 33%, #d290f4 46%, #bc5bee 46%, #bc5bee 58%, rgba(0, 0, 0, 0) 58%); }
658 .realistic .type-gamePiece.subtype-checkers.color-red { background: radial-gradient(circle, #e84242 0%, #e84242 33%, #f07f7f 33%, #f07f7f 46%, #e84242 46%, #e84242 58%, rgba(0, 0, 0, 0) 58%); }
659 .realistic .type-gamePiece.subtype-checkers.color-yellow { background: radial-gradient(circle, #e0cb0b 0%, #e0cb0b 33%, #eadc59 33%, #eadc59 46%, #e0cb0b 46%, #e0cb0b 58%, rgba(0, 0, 0, 0) 58%); }
660 .realistic .type-gamePiece.subtype-checkers.color-green { background: radial-gradient(circle, #23ca5b 0%, #23ca5b 33%, #6adb90 33%, #6adb90 46%, #23ca5b 46%, #23ca5b 58%, rgba(0, 0, 0, 0) 58%); }
661 .realistic .type-gamePiece.subtype-checkers.color-orange { background: radial-gradient(circle, #e2a633 0%, #e2a633 33%, #ecc375 33%, #ecc375 46%, #e2a633 46%, #e2a633 58%, rgba(0, 0, 0, 0) 58%); }
662
663
664 @media (max-width: 2048px) {
665 body {
666 min-height: 600px;
667 }
668 #room {
669 border: 2px solid black;
670 transform: scale(0.5, 0.5);
671 transform-origin: top right;
672 }
673 .fullWidth {
674 margin-top: 500px;
675 margin-right: -800px;
676 }
677 #tools {
678 margin-right: 810px;
679 }
680 }
681
682 @media (max-width: 801px) { /* adjust for button count (32 per button) */
683 button.material-icons {
684 font-size: 20px;
685 padding: 0;
686 margin: 2px 1px 2px 0;
687 }
688 .toolbarContainer button:nth-child(1) {
689 margin-left: 2px;
690 }
691 .toolbar span + button {
692 margin-left: 5px;
693 }
694 .toolbar input {
695 height: 20px;
696 font-size: 14px;
697 padding: 0 2px;
698 width: 32px;
699 }
700 button.material-icons.toolbarButton.activated {
701 top: 5px;
702 }
703 .tooltip {
704 top: 22px;
705 }
706 button:nth-child(n+14) .tooltip {
707 left: -189px;
708 }
709 }
710
711 @media (max-width: 563px) { /* adjust for button count (23 per button) */
712 button.material-icons.toolbarButton.activated {
713 top: 0;
714 border: 1px solid black;
715 }
716 .tooltip {
717 position: fixed;
718 left: 0 !important;
719 right: 0;
720 top: unset;
721 margin: auto;
722 margin-top: 2px;
723 }
724 }
725
726 @media (max-width: 1248px) {
727 #tools {
728 margin-right: 0;
729 margin-top: 516px;
730 }
731 .fullWidth {
732 margin-top: initial;
733 margin-right: initial;
734 }
735 }
736
737 @media (max-width: 828px) {
738 body {
739 min-height: 300px;
740 }
741 #room {
742 border: 4px solid black;
743 transform: scale(0.25, 0.25);
744 transform-origin: top right;
745 }
746 #tools {
747 margin-top: 266px;
748 }
749 }
750
751 @media (max-width: 900px) {
752 body, #editCardDecks, #tools {
753 padding-bottom: calc(8px + 6vw);
754 }
755 #statusbar {
756 font-size: 2vw;
757 }
758 #statusbar > * {
759 display: inline-block;
760 }
761 #statusbar > span:nth-child(1) {
762 width: 30%;
763 }
764 #statusbar > span:nth-child(2) {
765 width: 65%;
766 }
767 #statusbar > span:nth-child(3) {
768 width: 95%;
769 }
770 }
771
772 @media (max-width: 460px) {
773 body {
774 min-height: 150px;
775 }
776 #room {
777 border: 8px solid black;
778 transform: scale(0.125, 0.125);
779 transform-origin: top right;
780 }
781 #tools {
782 margin-top: 142px;
783 }
784 }
785
786 @media (max-width: 218px) {
787 body * {
788 display: none;
789 }
790 body:before {
791 content: "UNREASONABLE";
792 }
793 }
794
795 @media (max-width: 160px) {
796 body:before {
797 content: "JUST NO!";
798 }
799 }
800
801 @media (max-width: 60px) {
802 body {
803 padding: 0;
804 margin: 0;
805 }
806 body:before {
807 content: "FU!";
808 font-size: 60vw;
809 }
810 }
811</style>
812
813<script>
814 var zipFile = dataURLtoFile("data:application/zip;base64,UEsDBAoAAAAAAGRkMFHZQGDUQgAAAEIAAAAMAAAAd2lkZ2V0cy5qc29uW3siaWQiOiJoYW5kIiwidHlwZSI6ImhhbmQiLCJ4Ijo1MCwieSI6ODIwLCJ6IjoxLCJkcmFnZ2luZyI6bnVsbH1dUEsBAhQACgAAAAAAZGQwUdlAYNRCAAAAQgAAAAwAAAAAAAAAAAAAAAAAAAAAAHdpZGdldHMuanNvblBLBQYAAAAAAQABADoAAABsAAAAAAA=", "unnamed.pcio");
815 var widgets = [{"id":"hand","type":"hand","x":50,"y":820,"z":1,"dragging":null}];
816
817 var moveWidgets = false;
818 var resizeWidgets = false;
819 var selectionOrder = 1;
820 var previouslySelected = [];
821
822 var lastFilename = null;
823
824 var imageCache = {};
825 var boardImageCache = {};
826 var compressedImageCache = {};
827 var removeFromZip = {};
828
829 var defaultSizes = {
830 automationButton: [ 80, 80 ],
831 card: [ 103, 160 ],
832 cardDeck: [ 86, 86 ],
833 cardPile: [ 111, 168 ],
834 counter: [ 140, 44 ],
835 gamePiece: {
836 checkers: [ 90, 90 ],
837 classic: [ 90, 90 ],
838 pin: [ 35.85, 43.83 ],
839 pawn: [ 46.6536, 80.6544 ],
840 rook: [ 46.6536, 87.4008 ],
841 knight: [ 54.4344, 98.2632 ],
842 bishop: [ 48.2304, 108.3336 ],
843 queen: [ 52.3200, 124.8000 ],
844 king: [ 52.3512, 132.396 ]
845 },
846 hand: [ 1500, 180 ],
847 spinner: [ 110, 110 ]
848 };
849 var generateWidgetPresets = {
850 "Widgets Creation": {
851 "Grid of card piles": `
852 number: 10,
853 widget: function(i) {
854 var x = 20;
855 var y = 40;
856
857 var dx = 120;
858 var dy = 170;
859
860 var perRow = 5;
861
862 if(i == 0)
863 idPrefix = uniqueID("grid", true);
864
865 return {
866 "id": idPrefix + "-" + (Math.floor(i/perRow)+1) + "-" + (i%perRow+1),
867 "x": x + (i%perRow)*dx,
868 "y": y + Math.floor(i/perRow)*dy,
869 "z": 400 + i,
870 "type": "cardPile",
871 "dragging": null,
872 "label": "",
873 "hasShuffleButton": false,
874 "width": 111,
875 "height": 168
876 };
877 }
878 `,
879 "Simple Card Pile Creation": `
880 number: 1,
881 widget: function(i) {
882 return {
883 "id": uniqueID("pile"),
884 "x": 20,
885 "y": 40,
886 "z": 400,
887 "type": "cardPile",
888 "width": 111,
889 "height": 168
890 };
891 }
892 `,
893
894 "Spinner with N options":`
895 // contributed by u/RaphaelAlvez
896
897 number: 1, /* ONLY change this if you want to create a grid */
898 widget: function(i) {
899 var x = 20;
900 var y = 40;
901
902 var numberOfOptions = 6;
903
904 /* ONLY change this if you want to create a grid */
905 var dx = 0;
906 var dy = 0;
907 var perRow = 1;
908
909 return {
910 "id": uniqueID("spinner"),
911 "x": x + (i%perRow)*dx,
912 "y": y + Math.floor(i/perRow)*dy,
913 "z": i,
914 "type": "spinner",
915 "options": Array.from({length: numberOfOptions}, (_, i) => i + 1),
916 "rotation": 0,
917 "value": "",
918 "dragging": null
919 };
920 }
921 `,
922 "Normal Game Piece":`
923 // contributed by u/RaphaelAlvez
924
925 number: 1, /* ONLY change this if you want to create a grid */
926 widget: function(i) {
927 var x = 20;
928 var y = 40;
929
930 /* ONLY change this if you want to create a grid */
931 var dx = 0;
932 var dy = 0;
933 var perRow = 1;
934
935 return {
936 "id": uniqueID("gamePiece"),
937 "x": x + (i%perRow)*dx,
938 "y": y + Math.floor(i/perRow)*dy,
939 "z": i,
940 "type":"gamePiece",
941 "pieceType":"checkers", /* can be "classic", "checkers" or "pin" */
942 "color":"red", /* can be "red", "orange", "yellow", "green", "blue", "purple" or "black" */
943 "dragging":null,
944 "kinged":true /* can be true or false */
945 };
946 }
947 `,
948 "Normal Counter":`
949 // contributed by u/RaphaelAlvez
950
951 number: 1, /* ONLY change this if you want to create a grid */
952 widget: function(i) {
953 var x = 20;
954 var y = 40;
955
956 /* ONLY change this if you want to create a grid */
957 var dx = 0;
958 var dy = 0;
959 var perRow = 1;
960
961 return {
962 "id": uniqueID("counter"),
963 "x": x + (i%perRow)*dx,
964 "y": y + Math.floor(i/perRow)*dy,
965 "z": i,
966 "type":"counter",
967 "counterValue":0,
968 "dragging":null,
969 "label":""
970 };
971 }
972 `
973 },
974 "Widgets Duplication": {
975 "Duplicate the selected widgets": `
976 number: $(".selected").length,
977 widget: function(i) {
978 w = widgets[$(".selected").eq(i).data("index")];
979 newW = JSON.parse(JSON.stringify(w)); // deep clone
980 newW.id = uniqueID(w.id + "-copy");
981 newW.x += 50;
982 newW.y += 50;
983 return newW;
984 }
985 `,
986 "Duplicate a selected automation button 5x adding 1 to its label and quantities": `
987 number: 5,
988 widget: function(i) {
989 w = widgets[$(".selected.type-automationButton").eq(0).data("index")];
990 newW = JSON.parse(JSON.stringify(w)); // deep clone
991 newW.id = uniqueID(w.id + "-copy");
992 newW.x += 85 * (i+1);
993 newW.label = +newW.label + i+1;
994 for(var r in newW.clickRoutine)
995 if(newW.clickRoutine[r].args.quantity != undefined)
996 newW.clickRoutine[r].args.quantity.value += i+1;
997 return newW;
998 }
999 `
1000 },
1001 "Automation Button (AB) Routines": {
1002 "Create Clean AB": `
1003 // contributed by u/RaphaelAlvez
1004
1005 number: 1, /* ONLY change this if you want to create a grid */
1006 widget: function(i) {
1007 var x = 20;
1008 var y = 40;
1009
1010 /* ONLY change this if you want to create a grid */
1011 var dx = 0;
1012 var dy = 0;
1013 var perRow = 1;
1014
1015 return {
1016 "id": uniqueID("button"),
1017 "x": x + (i%perRow)*dx,
1018 "y": y + Math.floor(i/perRow)*dy,
1019 "z": i,
1020 "type": "automationButton",
1021 "label": "Button "+i,
1022 "clickRoutine": [],
1023 "dragging": null
1024 };
1025 }
1026 `,
1027 "Clean ALL routines of selected ABs": `
1028 // contributed by u/RaphaelAlvez
1029
1030 number: $(".selected").length,
1031 widget: function(i) {
1032 w = widgets[$(".selected").eq(i).data("index")];
1033 if (w.clickRoutine!=null){w.clickRoutine=[]};
1034 }
1035 `,
1036 "Move Routine": `
1037 // contributed by u/RaphaelAlvez
1038
1039 number: 1,
1040 widget: function(i) {
1041
1042 /* You should first select the source card piles then the automation button and finally the destination card piles. Do not select any other kind of widgets because it will get introduced in the routine */
1043
1044 /* Choose here the number of cards that should be moved */
1045 var Quantity=1 ;
1046 var selected=$(".selected").get().sort((a, b) => $(a).data("selectionOrder") - $(b).data("selectionOrder"));
1047
1048 /* finds the AB */
1049 var abIndex = selected.map((o) => widgets[$(o).data("index")].type).indexOf("automationButton");
1050
1051 /* gets the ids of the widgets selected before the AB */
1052 From=selected.slice(0, abIndex).map((o)=>widgets[$(o).data("index")].id);
1053
1054 /* gets the ids of the widgets selected after the AB */
1055 To=selected.slice(abIndex+1).map((o)=>widgets[$(o).data("index")].id);
1056
1057 /* Checks if you did something wrong */
1058 if(abIndex==-1||From.length==0||To.length==0){return};
1059
1060 /* Add routine */
1061 widgets[$(selected[abIndex]).data("index")].clickRoutine.push({
1062 "func": "MOVE_CARDS_BETWEEN_HOLDERS",
1063 "args": {
1064 "from": {
1065 "type": "literal",
1066 "value": From
1067 },
1068 "to": {
1069 "type": "literal",
1070 "value": To
1071 },
1072 "quantity": {
1073 "type": "literal",
1074 "value": Quantity
1075 } ,
1076 "moveFlip": {
1077 "type": "literal",
1078 "value": "none" /* Can be "none","faceUp" or "faceDown" */
1079 }
1080 }
1081 });
1082 }
1083 `,
1084 "Shuffle Routine": `
1085 // contributed by u/RaphaelAlvez
1086 /*Add Shuffle to selected piles from selected AB*/
1087
1088 number: 1,
1089 widget: function(i) {
1090 shuffle=[];
1091 for (var n = 1; n <= $(".selected").length; n++) {
1092 a=widgets[$(".selected").eq(n-1).data("index")];
1093 if(a.type=="cardPile"){
1094 shuffle.push(a.id)}
1095 else{if (a.type="automationButton"){
1096 b=a}
1097 }
1098 };
1099 b.clickRoutine.push({
1100 "func": "SHUFFLE_CARDS",
1101 "args": {
1102 "holders": {
1103 "type": "literal",
1104 "value": shuffle
1105 }
1106 }
1107 });
1108 return ;
1109 }
1110 `,
1111 "Flip Routines": `
1112 // contributed by u/RaphaelAlvez
1113 /*Flip cards of selected piles from selected AB*/
1114
1115 number: 1,
1116 widget: function(i) {
1117 Flip=[];
1118 for (var n = 1; n <= $(".selected").length; n++) {
1119 a=widgets[$(".selected").eq(n-1).data("index")];
1120 if(a.type=="cardPile"){
1121 Flip.push(a.id)}
1122 else{if (a.type="automationButton"){
1123 b=a}
1124 }
1125 };
1126 b.clickRoutine.push({
1127 "func": "FLIP_CARDS",
1128 "args": {
1129 "flipMode": {
1130 "type": "literal",
1131 "value": "pile" /* can be "pile" or "top" */
1132 },
1133 "holders": {
1134 "type": "literal",
1135 "value": Flip
1136 }
1137 }
1138 });
1139 return ;
1140 }
1141 `,
1142 "Set Flip Routines": `
1143 // contributed by u/RaphaelAlvez
1144 /* Set flip of cards of selected piles from selected AB */
1145
1146 number: 1,
1147 widget: function(i) {
1148 FlipSet=[];
1149 for (var n = 1; n <= $(".selected").length; n++) {
1150 a=widgets[$(".selected").eq(n-1).data("index")];
1151 if(a.type=="cardPile"){
1152 FlipSet.push(a.id)}
1153 else{if (a.type="automationButton"){
1154 b=a}
1155 }
1156 };
1157 b.clickRoutine.push({
1158 "func": "FLIP_CARDS",
1159 "args": {
1160 "flipFace": {
1161 "type": "literal",
1162 "value": "faceUp" /* can be "faceUp" or "faceDown" */
1163 },
1164 "flipMode": {
1165 "type": "literal",
1166 "value": "pile" /* can be "pile" or "top" */
1167 },
1168 "holders": {
1169 "type": "literal",
1170 "value": FlipSet
1171 }
1172 }
1173 });
1174 return ;
1175 }
1176 `,
1177 "Counters Routines": `
1178 // contributed by u/RaphaelAlvez
1179 /* Add automation to selected counters from selected AB */
1180
1181 number: 1,
1182 widget: function(i) {
1183 Counters=[];
1184 for (var n = 1; n <= $(".selected").length; n++) {
1185 a=widgets[$(".selected").eq(n-1).data("index")];
1186 if(a.type=="counter"){
1187 Counters.push(a.id)}
1188 else{if (a.type="automationButton"){
1189 b=a}
1190 }
1191 };
1192 b.clickRoutine.push({
1193 "func": "CHANGE_COUNTER",
1194 "args": {
1195 "counters": {
1196 "type": "literal",
1197 "value": Counters
1198 },
1199 "changeMode": {
1200 "type": "literal",
1201 "value": "set" /* can be "set", "inc" or "dec"*/
1202 },
1203 "changeNumber": {
1204 "type": "literal",
1205 "value": 10
1206 }
1207 }
1208 });
1209 return ;
1210 }
1211 `,
1212 "Create a movement loop between piles":`
1213 // contributed by u/RaphaelAlvez
1214
1215 number: 1,
1216 widget: function(i) {
1217
1218 var Quantity=1 ;
1219 var selected=$(".selected").get().sort((a, b) => $(a).data("selectionOrder") - $(b).data("selectionOrder"));
1220 loop=[];
1221 for (var n = 1; n <= $(".selected").length; n++) {
1222 a=widgets[$(".selected").eq(n-1).data("index")];
1223 if(a.type=="cardPile"){
1224 loop.push(a.id)}
1225 else if (a.type=="automationButton"){
1226 b=a};
1227 };
1228
1229 /* finds the AB */
1230 var abIndex = selected.map((o) => widgets[$(o).data("index")].type).indexOf("automationButton");
1231
1232 /* Checks if you did something wrong */
1233 if(abIndex==-1||loop.length==0){return};
1234
1235 buffer=uniqueID("buffer")
1236
1237 addWidget({
1238 "id": buffer,
1239 "x": 1650,
1240 "y": 0,
1241 "type": "cardPile",
1242 "width": loop[0].width,
1243 "height": loop[0].height
1244 })
1245
1246 widgets[$(selected[abIndex]).data("index")].clickRoutine.push({
1247 "func": "MOVE_CARDS_BETWEEN_HOLDERS",
1248 "args": {
1249 "from": {
1250 "type": "literal",
1251 "value": loop[0]
1252 },
1253 "to": {
1254 "type": "literal",
1255 "value": [buffer]
1256 },
1257 "quantity": {
1258 "type": "literal",
1259 "value": Quantity
1260 } ,
1261 "moveFlip": {
1262 "type": "literal",
1263 "value": "none" /* Can be "none","faceUp" or "faceDown" */
1264 }
1265 }
1266 });
1267
1268
1269 for (var n = 1; n <= loop.length; n++) {
1270
1271 /* Add routine */
1272 widgets[$(selected[abIndex]).data("index")].clickRoutine.push({
1273 "func": "MOVE_CARDS_BETWEEN_HOLDERS",
1274 "args": {
1275 "from": {
1276 "type": "literal",
1277 "value": [loop[n]]
1278 },
1279 "to": {
1280 "type": "literal",
1281 "value": [loop[n-1]]
1282 },
1283 "quantity": {
1284 "type": "literal",
1285 "value": Quantity
1286 } ,
1287 "moveFlip": {
1288 "type": "literal",
1289 "value": "none" /* Can be "none","faceUp" or "faceDown" */
1290 }
1291 }
1292 });
1293 };
1294
1295 widgets[$(selected[abIndex]).data("index")].clickRoutine.push({
1296 "func": "MOVE_CARDS_BETWEEN_HOLDERS",
1297 "args": {
1298 "from": {
1299 "type": "literal",
1300 "value": [buffer]
1301 },
1302 "to": {
1303 "type": "literal",
1304 "value": [loop[loop.length-1]]
1305 },
1306 "quantity": {
1307 "type": "literal",
1308 "value": Quantity
1309 } ,
1310 "moveFlip": {
1311 "type": "literal",
1312 "value": "none" /* Can be "none","faceUp" or "faceDown" */
1313 }
1314 }
1315 });
1316 }
1317 `
1318 },
1319 "Other": {
1320 "Smart Rename":`
1321 // contributed by u/RaphaelAlvez
1322 // please note that you can edit the ID of widgets directly now (hotkey e)
1323
1324 /* Changes the Id of the seleted Widget (just one) and makes the same change wherever the selected widget is cited */
1325
1326 number: $(".selected").length,
1327 widget: function(i) {
1328
1329 var newId ='"'+prompt("New ID?", widgets[$(".selected").eq(i).data("index")].id)+'"'
1330
1331 widgets=JSON.parse(replaceall(JSON.stringify(widgets[$(".selected").eq(i).data("index")].id), newId, JSON.stringify(widgets)));
1332 }
1333 `,
1334 "Smart resize card deck":`
1335 /* select a card deck and this will resize its templates, cards and piles */
1336
1337 number: $(".selected.type-cardDeck").length,
1338 widget: function(i) {
1339
1340 var newHeight = 160;
1341
1342 var w = widgets[$(".selected.type-cardDeck").eq(i).data("index")];
1343
1344 var newWidth = newHeight/w.cardHeight*w.cardWidth;
1345
1346 // this uses a function I needed somewhere else
1347 resizeCardDeck(w, newWidth, newHeight);
1348 }
1349 `,
1350 "Set pile as parents":`
1351 // contributed by u/RaphaelAlvez
1352
1353 number: 1,
1354 widget: function(i) {
1355 Cards=[];
1356 Decks = [];
1357 b = null;
1358
1359 for (var n = 1; n <= $(".selected").length; n++) {
1360 a=widgets[$(".selected").eq(n-1).data("index")];
1361 if(a.type=="card"){
1362 Cards.push(a)};
1363 if (a.type=="cardPile"){
1364 b=a};
1365 if (a.type=="cardDeck"){
1366 Decks.push(a)};
1367 };
1368 b.z=0;
1369
1370 for (var n = 1; n <= Decks.length; n++) {
1371 Decks[n-1].parent=b.id;
1372 Decks[n-1].x= 13+(b.x);
1373 Decks[n-1].y= 44+(b.y);
1374 Decks[n-1].z= n;
1375 };
1376
1377 for (var n = 1; n <= Cards.length; n++) {
1378 Cards[n-1].parent=b.id;
1379 Cards[n-1].x= 4+(b.x);
1380 Cards[n-1].y= 4+(b.y);
1381 Cards[n-1].z= n+(Decks.length);
1382 };
1383 return ;
1384 }
1385 `,
1386 "Reset height and width":`
1387 // contributed by u/RaphaelAlvez
1388
1389 number: $(".selected").length,
1390 widget: function(i) {
1391 w = widgets[$(".selected").eq(i).data("index")];
1392 if (w.type=="gamePiece"){[w.width,w.height]=defaultSizes["gamePiece"][w.pieceType]}
1393 else {[w.width,w.height]=defaultSizes[w.type]}
1394 }
1395 `,
1396 "Human-Readable Widget IDs":`
1397 // contributed by u/RaphaelAlvez
1398
1399 /* Changes the Id of all Widgets to something like "spinner001" and makes the same change wherever the widgets' id is cited. */
1400
1401 number: widgets.length,
1402 widget: function(i) {
1403
1404 if(widgets[i].id!="hand") {
1405 var newId ='"'+uniqueID(widgets[i].type)+'"'
1406 /* The uniqueID() function can be used to create an unique ID using a string as input. Here I used the widget's type as input. */
1407
1408 widgets=JSON.parse(replaceall(JSON.stringify(widgets[i].id), newId, JSON.stringify(widgets)));
1409 }
1410 }
1411 `,
1412 "Empty for tests": `
1413 // contributed by u/RaphaelAlvez
1414
1415 number: 1,
1416 widget: function(i) {
1417 return ;
1418 }
1419 `
1420 }
1421 };
1422 var buttons = {
1423 folder_open: [ "open a PCIO file", "o", "ONE_SHOT", function() {
1424 $("#file").click();
1425 } ],
1426 save_alt: [ "save as PCIO file (triggers browser download)<br><br>this takes around 30s for a 50MiB file", "s", "ONE_SHOT", function() {
1427 lastFilename = prompt("Name?", lastFilename || zipFile.name);
1428 if(!lastFilename)
1429 return;
1430
1431 JSZip.loadAsync(zipFile).then(function(zip) {
1432 zip.file("widgets.json", JSON.stringify(widgets));
1433 for(var name in imageCache)
1434 if(name != "widgets.json")
1435 zip.file(name, imageCache[name], { base64: true });
1436 for(var name in removeFromZip)
1437 zip.remove(name);
1438
1439 zip.generateAsync({type:"base64"}).then(function (base64) {
1440 downloadURI("data:application/zip;base64," + base64, lastFilename);
1441 }, function (e) {
1442 console.log("error saving zip file", e);
1443 });
1444 });
1445 } ],
1446 import_export: [ "advanced file operations<ul><li>import from second PCIO</li><li>import from Tabletop Simulator</li><li>quick testing in game</li><li>turn links into assets and vice versa</li></ul>", "X", {
1447 insert_drive_file: [ "start from scratch", "0", "ONE_SHOT", function() {
1448 clearUndoStack();
1449 zipFile = dataURLtoFile("data:application/zip;base64,UEsDBAoAAAAAAGRkMFHZQGDUQgAAAEIAAAAMAAAAd2lkZ2V0cy5qc29uW3siaWQiOiJoYW5kIiwidHlwZSI6ImhhbmQiLCJ4Ijo1MCwieSI6ODIwLCJ6IjoxLCJkcmFnZ2luZyI6bnVsbH1dUEsBAhQACgAAAAAAZGQwUdlAYNRCAAAAQgAAAAwAAAAAAAAAAAAAAAAAAAAAAHdpZGdldHMuanNvblBLBQYAAAAAAQABADoAAABsAAAAAAA=", "unnamed.pcio");
1450 widgets = [{"id":"hand","type":"hand","x":50,"y":820,"z":1,"dragging":null}];
1451 lastFilename = "unnamed";
1452 imageCache = {};
1453 boardImageCache = {};
1454 compressedImageCache = {};
1455 removeFromZip = {};
1456 $("#editPreview").empty();
1457 $(".selected").removeClass("selected");
1458 drawWidgets(widgets);
1459 } ],
1460 sep1: 1,
1461 exit_to_app: [ "add all widgets from another PCIO file<br><br>delete everything you don't want from it first", "O", "ONE_SHOT", function() {
1462 $("#fileImportWidgets").click();
1463 } ],
1464 sep2: 1,
1465 casino: [ "import Tabletop Simulator mod", "t", "TOGGLE_TOOL", "#tabletopImporter" ],
1466 sep3: 1,
1467 layers_clear: [ "save as stripped PCIO file<br><br>no card images for fast testing of everything else<br><br>don't touch the contents of card decks with the vanilla editor", "S", "ONE_SHOT", function() {
1468 lastFilename = prompt("Name?", lastFilename || zipFile.name);
1469 if(!lastFilename)
1470 return;
1471
1472 var zip = new JSZip();
1473 zip.file("widgets.json", JSON.stringify(widgets));
1474 for(var name in boardImageCache)
1475 zip.file(name, imageCache[name], { base64: true });
1476
1477 zip.generateAsync({type:"base64"}).then(function (base64) {
1478 downloadURI("data:application/zip;base64," + base64, lastFilename);
1479 }, function (e) {
1480 console.log("error saving zip file", e);
1481 });
1482 } ],
1483 assignment_returned: [ "import changes from a re-exported stripped PCIO file", "i", "ONE_SHOT", function() {
1484 $("#fileStripped").click();
1485 } ],
1486 sep4: 1,
1487 cloud_download: [ "turn all http links into packaged assets", "p", "ONE_SHOT", function() {
1488 var urls = JSON.stringify(widgets).match(/https?:\/\/[^"]+/g);
1489 if(urls)
1490 $("#statusTool").text("0/" + urls.length + " downloaded");
1491 else
1492 $("#statusTool").text("no HTTP links found");
1493 var downloaded = 0;
1494 $.each(urls, function(i, url) {
1495 var filename = "userassets/gpe-dl-" + hashFnv32a(url, true);
1496 toDataURL(url, function(data) {
1497 imageCache[filename] = data.replace(/data.*?,/, '');
1498 widgets = JSON.parse(JSON.stringify(widgets).replace(url, "package://" + filename));
1499 ++downloaded;
1500 $("#statusTool").text(downloaded + "/" + urls.length + " downloaded");
1501 drawWidgets(widgets);
1502 });
1503 });
1504 } ],
1505 cloud_upload: [ "turn all packaged assets into http links", "W", "TOGGLE_TOOL", "#cloudUpload" ]
1506 } ],
1507 sep1: 1,
1508 undo: [ "undo<br><br>only widgets.json is supported - changes to images will remain/break", "z", "ONE_SHOT", function() {
1509 popUndoStack();
1510 } ],
1511 content_copy: [ "copy", "c", "ONE_SHOT", function() {
1512 copy=[];
1513 $(".selected").each(function() {
1514 copy.push(JSON.parse(JSON.stringify(widgets[$(this).data("index")])));
1515 });
1516 copyTextToClipboard(JSON.stringify(copy));
1517 } ],
1518 content_paste: [ "paste", "v", "ONE_SHOT", function(isHotkey) {
1519 pushUndoStack();
1520 if(typeof copy !== "undefined" && copy.length)
1521 paste=JSON.parse(JSON.stringify(copy));
1522 else
1523 paste=JSON.parse(prompt("Paste here:"));
1524 var b = $("#room").get(0).getBoundingClientRect();
1525 var scale = 1600 / (b.width - 2);
1526 var x = Math.floor((x2 - b.x) * scale);
1527 var y = Math.floor((y2 - b.y) * scale);
1528 if(!isHotkey) {
1529 x = 100;
1530 y = 100;
1531 }
1532 var minX = 1600;
1533 var minY = 1000;
1534
1535 paste.forEach(function(w){
1536 if (minX>w.x){minX=w.x};
1537 if (minY>w.y){minY=w.y};
1538 })
1539
1540 var idsToSelect = [];
1541 paste.forEach(function(w){
1542 w.x += x-minX;
1543 w.y += y-minY;
1544 delete w.z;
1545 var newID = uniqueID(w.id + "-copy");
1546 renameID(paste, w.id, newID);
1547 addWidget(w);
1548 idsToSelect.push(newID);
1549 });
1550 drawWidgets(widgets);
1551
1552 $(".widget").each(function() {
1553 if(idsToSelect.indexOf(widgets[$(this).data("index")].id) != -1)
1554 $(this).click();
1555 });
1556 } ],
1557 delete: [ "delete<br><br>it's a good idea to use the button 'remove unused card type variables,[...]' afterwards", "Delete", "ONE_SHOT", function() {
1558 pushUndoStack();
1559 var selected = $.map($(".selected"), (s) => widgets[$(s).data("index")].id);
1560 widgets = widgets.filter((w) => selected.indexOf(w.id) == -1);
1561 drawWidgets(widgets);
1562 } ],
1563 select_all: [ "select the previously selected widgets", "-", "ONE_SHOT", function() {
1564 $(".widget").each(function() {
1565 $(this).toggleClass("selected", previouslySelected.indexOf(widgets[$(this).data("index")].id) != -1);
1566 $("#statusTool").text($(".selected").length + " widgets selected");
1567 });
1568 } ],
1569 sep2: 1,
1570 business_center: [ "widgets<br><br>add widgets similarly to the standard editor", "w", "TOGGLE_TOOL", "#toolBox" ],
1571 edit: [ "edit widgets", "e", {
1572 html1: "<label for='quickSetID'>ID </label><input id='quickSetID' class='text'>",
1573 fingerprint: [ "set ID", "none", function(allSelected, i, widget) {
1574 if(widgets.map((w) => w.id).indexOf($("#quickSetID").val()) == -1)
1575 renameID(widgets, widget.id, $("#quickSetID").val());
1576 else
1577 alert("That ID already exists.");
1578 } ],
1579 sep1: 1,
1580 notes: [ "edit the JSON representation of a widget directly", "j", "TOGGLE_TOOL", "#jsonEdit" ],
1581 sep2: 1,
1582 mediation: [ "merge selected card decks into one<br><br>this doesn't do magic<br>they should be similar", "+", "ONE_SHOT", function() {
1583 pushUndoStack();
1584
1585 var allSelected = $.map($(".selected"), (s) => widgets[$(s).data("index")]);
1586 var widget = allSelected.shift();
1587
1588 allSelected.forEach(function(w) {
1589 Object.assign(widget.cardTypes, w.cardTypes);
1590 widgets = widgets.filter((v) => v.id != w.id);
1591 for(var wi in widgets)
1592 if(widgets[wi].deck !== undefined)
1593 widgets[wi].deck = widgets[wi].deck == w.id ? widget.id : widgets[wi].deck;
1594 });
1595 drawWidgets(widgets);
1596 } ],
1597 settings_applications: [ "edit card decks<ul><li>edit settings</li><li>merge card types</li><li>split decks</li><li>add rotation</li></ul>", "d", "TOGGLE_TOOL", "#editCardDecks" ]
1598 } ],
1599 architecture: [ "align the selected widgets<br><br>manually set or offset the coordinates of the selected widgets<br><br>the input fields support formulas like 12+32", ".", {
1600 html1: `
1601 <label for='quickSetX'>X </label><input id='quickSetX' class='inactive quickSet x'>
1602 <label for='quickSetY'>Y </label><input id='quickSetY' class='inactive quickSet y'>
1603 <label for='quickSetW'>W </label><input id='quickSetW' class='inactive quickSet width'>
1604 <label for='quickSetH'>H </label><input id='quickSetH' class='inactive quickSet height'>
1605 `,
1606 highlight_alt: [ "align widgets to the right/bottom", "b", "ONE_SHOT", function() {
1607 $(".highlight_alt").toggleClass("activated");
1608 fillDetails($(".selected").get(0));
1609 } ],
1610 sep1: 1,
1611 check: [ "set values directly to the widgets", "none", function(allSelected, i, widget) {
1612 [ "x", "y", "width", "height" ].forEach(function(field) {
1613 if(!$(".quickSet." + field).is(".inactive")) {
1614 var value = +eval($(".quickSet." + field).val());
1615 if($(".highlight_alt").is(".activated") && [ "x", "y" ].indexOf(field) > -1)
1616 widget[field] = (field == "x" ? 1600 : 1000) - allSelected[i][field == "x" ? "width" : "height"] - value;
1617 else
1618 widget[field] = value;
1619 if($(".highlight_alt").is(".activated") && field == "width")
1620 widget.x -= widget.width - allSelected[i].width;
1621 if($(".highlight_alt").is(".activated") && field == "height")
1622 widget.y -= widget.height - allSelected[i].height;
1623 }
1624 });
1625 if(i == allSelected.length - 1)
1626 $(".quickSet").addClass("inactive");
1627 } ],
1628 double_arrow: [ "add values to current values of the widgets", "none", function(allSelected, i, widget) {
1629 [ "x", "y", "width", "height" ].forEach(function(field) {
1630 if(!$(".quickSet." + field).is(".inactive")) {
1631 var value = +eval($(".quickSet." + field).val());
1632 if($(".highlight_alt").is(".activated") && [ "x", "y" ].indexOf(field) > -1)
1633 widget[field] = allSelected[i][field] - value;
1634 else
1635 widget[field] = allSelected[i][field] + value;
1636 if($(".highlight_alt").is(".activated") && field == "width")
1637 widget.x -= value;
1638 if($(".highlight_alt").is(".activated") && field == "height")
1639 widget.y -= value;
1640 }
1641 });
1642 if(i == allSelected.length - 1)
1643 $(".quickSet").addClass("inactive");
1644 } ],
1645 settings_ethernet: [ "set the space between the selected widgets to the entered value", "none", "ONE_SHOT", function() {
1646 pushUndoStack();
1647 var allSelected = [];
1648 $(".selected").each(function() {
1649 allSelected.push(addDimensions(widgets[$(this).data("index")]));
1650 });
1651
1652 [ "x", "y" ].forEach(function(dir) {
1653 if($(".quickSet." + dir).is(".inactive"))
1654 return;
1655
1656 var dim = dir == "x" ? "width" : "height";
1657 var minCoord = Math.min(...allSelected.map((w) => w[dir]));
1658 var maxCoord = Math.max(...allSelected.map((w) => w[dir]+w[dim]));
1659 var spacing = +eval($(".quickSet." + dir).val());
1660
1661 if($(".highlight_alt").is(".activated"))
1662 allSelected.sort((a, b) => a[dir]+a[dim] > b[dir]+b[dim] ? 1 : a[dir]+a[dim] < b[dir]+b[dim] ? -1 : 0);
1663 else
1664 allSelected.sort((a, b) => a[dir] > b[dir] ? 1 : a[dir] < b[dir] ? -1 : 0);
1665
1666 $(".selected").each(function() {
1667 var i = allSelected.findIndex((w) => w.id == widgets[$(this).data("index")].id);
1668 var w = widgets[$(this).data("index")];
1669
1670 if($(".highlight_alt").is(".activated")) {
1671 var after = allSelected.slice(i);
1672 w[dir] = maxCoord - after.map((w) => w[dim] + spacing).reduce((a, b) => a+b, 0) + spacing;
1673 } else {
1674 var before = allSelected.slice(0, i);
1675 w[dir] = minCoord + before.map((w) => w[dim] + spacing).reduce((a, b) => a+b, 0);
1676 }
1677 });
1678 });
1679
1680 $(".quickSet").addClass("inactive");
1681 drawWidgets(widgets);
1682 } ],
1683 sep2: 1,
1684 align_vertical_top: [ "align elements to the highest top edge", "ArrowUp", function(allSelected, i, widget) {
1685 widget.y = Math.min.apply(Math, allSelected.map((w) => w.y));
1686 } ],
1687 align_vertical_center: [ "align element centers to the middle between the highest top edge and lowest bottom edge", "V", function(allSelected, i, widget, wd) {
1688 var minY = Math.min.apply(Math, allSelected.map((w) => w.y))
1689 var maxY = Math.max.apply(Math, allSelected.map((w) => w.y + w.height))
1690 widget.y = minY + (maxY-minY)/2 - wd.height/2;
1691 } ],
1692 align_vertical_bottom: [ "align elements to the lowest bottom edge", "ArrowDown", function(allSelected, i, widget, wd) {
1693 widget.y = Math.max.apply(Math, allSelected.map((w) => w.y + w.height)) - wd.height;
1694 } ],
1695 vertical_distribute: [ "equalize the spacing between all the elements vertically", "k", function(allSelected, i, widget, wd) {
1696 var minY = Math.min.apply(Math, allSelected.map((w) => w.y))
1697 var maxY = Math.max.apply(Math, allSelected.map((w) => w.y + w.height))
1698 var heights = allSelected.map((w) => w.height).reduce((a, b) => a+b, 0)
1699 var spacing = (maxY-minY-heights)/(allSelected.length-1);
1700 allSelected.sort((a, b) => a.y > b.y ? 1 : a.y < b.y ? -1 : 0);
1701 var before = allSelected.slice(0, allSelected.findIndex((w) => w.id == widget.id));
1702 widget.y = minY + before.map((w) => w.height + spacing).reduce((a, b) => a+b, 0);
1703 } ],
1704 align_horizontal_left: [ "align elements to the leftmost left edge", "ArrowLeft", function(allSelected, i, widget) {
1705 widget.x = Math.min.apply(Math, allSelected.map((w) => w.x));
1706 } ],
1707 align_horizontal_center: [ "align element centers to the middle between the leftmost left edge and rightmost right edge", "H", function(allSelected, i, widget, wd) {
1708 var minX = Math.min.apply(Math, allSelected.map((w) => w.x))
1709 var maxX = Math.max.apply(Math, allSelected.map((w) => w.x + w.width))
1710 widget.x = minX + (maxX-minX)/2 - wd.width/2;
1711 } ],
1712 align_horizontal_right: [ "align elements to the rightmost right edge", "ArrowRight", function(allSelected, i, widget, wd) {
1713 widget.x = Math.max.apply(Math, allSelected.map((w) => w.x + w.width)) - wd.width;
1714 } ],
1715 horizontal_distribute: [ "equalize the spacing between all the elements horizontally", "h", function(allSelected, i, widget) {
1716 var minX = Math.min.apply(Math, allSelected.map((w) => w.x));
1717 var maxX = Math.max.apply(Math, allSelected.map((w) => w.x + w.width));
1718 var widths = allSelected.map((w) => w.width).reduce((a, b) => a+b, 0);
1719 var spacing = (maxX-minX-widths)/(allSelected.length-1);
1720 allSelected.sort((a, b) => a.x > b.x ? 1 : a.x < b.x ? -1 : 0);
1721 var before = allSelected.slice(0, allSelected.findIndex((w) => w.id == widget.id));
1722 widget.x = minX + before.map((w) => w.width + spacing).reduce((a, b) => a+b, 0);
1723 } ],
1724 } ],
1725 memory: [ "macros<br><br>write custom Javascript or use presets as powerful editing tools", "#", "TOGGLE_TOOL", "#macros" ],
1726 sep3: 1,
1727 library_add_check: [ "align all card decks and cards with their parent pile", "a", "ONE_SHOT", function() {
1728 pushUndoStack();
1729 for(var w in widgets) {
1730 if(typeof widgets[w].parent === "string") {
1731 var p = widgets.filter((v) => v.id == widgets[w].parent)[0];
1732 if(widgets[w].type == "cardDeck") {
1733 if(p) {
1734 widgets[w].x = p.x + 13;
1735 widgets[w].y = p.y + 44;
1736 } else {
1737 widgets[w].parent = null;
1738 }
1739 }
1740 if(widgets[w].type == "card" && widgets[w].parent) {
1741 p = widgets.filter((v) => v.id == widgets[w].parent)[0];
1742 if(p) {
1743 widgets[w].x = p.x + 4;
1744 widgets[w].y = p.y + 4;
1745 } else {
1746 widgets[w].parent = null;
1747 }
1748 }
1749 }
1750 }
1751 drawWidgets(widgets);
1752 } ],
1753 photo_size_select_large: [ "edit images<ul><li>resize and compress</li><li>rotate</li><li>crop</li></ul>", "x", "TOGGLE_TOOL", "#editImages" ],
1754 link_off: [ "remove unused card type variables, cards without a deck and unreferenced images", "u", "ONE_SHOT", function() {
1755 var keysDeleted = 0;
1756 var imagesDeleted = 0;
1757 var widgetIDsToRemove = [];
1758 for(var w in widgets) {
1759 if(widgets[w].type === "cardDeck") {
1760 var validVariables = [ "label" ];
1761 for(var field in { faceTemplate: 1, backTemplate: 1 }) {
1762 validVariables = validVariables.concat(widgets[w][field].objects.map((v) => v.valueType == "dynamic" && v.value).filter((v) => v !== false));
1763 }
1764 for(var c in widgets[w].cardTypes) {
1765 for(var key in widgets[w].cardTypes[c]) {
1766 if(validVariables.indexOf(key) == -1) {
1767 ++keysDeleted;
1768 delete widgets[w].cardTypes[c][key];
1769 }
1770 }
1771 }
1772 }
1773 if(widgets[w].type === "card")
1774 if(!widgets.filter((v) => v.id == widgets[w].deck).length)
1775 widgetIDsToRemove.push(widgets[w].id);
1776 }
1777 widgets = widgets.filter((w) => widgetIDsToRemove.indexOf(w.id) == -1);
1778 var imagesFound = JSON.stringify(widgets).match(/"package:\/\/[^"]+/g);
1779 imagesFound = imagesFound && imagesFound.map((x) => x.substr(11)) || [];
1780 $("#statusTool").text("deleted " + widgetIDsToRemove.length + " cards, " + keysDeleted + " variables and 0 images");
1781 JSZip.loadAsync(zipFile).then(function(zip) {
1782 for(var file in zip.files) {
1783 if(file != "widgets.json" && file != "userassets/" && imagesFound.indexOf(file) == -1 && removeFromZip[file] == undefined) {
1784 console.log("Deleting image " + file);
1785 ++imagesDeleted;
1786 removeFromZip[file] = true;
1787 delete imageCache[file];
1788 $("#statusTool").text("deleted " + widgetIDsToRemove.length + " cards, " + keysDeleted + " variables and " + imagesDeleted + " images");
1789 }
1790 }
1791 });
1792 for(var file in imageCache) {
1793 if(file != "widgets.json" && file != "userassets/" && imagesFound.indexOf(file) == -1 && removeFromZip[file] == undefined) {
1794 console.log("Deleting image " + file);
1795 ++imagesDeleted;
1796 removeFromZip[file] = true;
1797 delete imageCache[file];
1798 $("#statusTool").text("deleted " + widgetIDsToRemove.length + " cards, " + keysDeleted + " variables and " + imagesDeleted + " images");
1799 }
1800 }
1801 drawWidgets(widgets);
1802 } ],
1803 sep4: 1,
1804 open_with: [ "move widgets with the mouse instead of selecting them", "m", "ONE_SHOT", function() {
1805 if(resizeWidgets)
1806 $(".settings_overscan").click();
1807 moveWidgets = !moveWidgets;
1808 $(".open_with").toggleClass("activated", moveWidgets);
1809 } ],
1810 settings_overscan: [ "resize widgets with the mouse instead of selecting them", "r", "ONE_SHOT", function() {
1811 if(moveWidgets)
1812 $(".open_with").click();
1813 resizeWidgets = !resizeWidgets;
1814 $(".settings_overscan").toggleClass("activated", resizeWidgets);
1815 } ],
1816 filter_alt: [ "filter widget types", "f", "TOGGLE_TOOL", "#typeFilter" ],
1817 filter_b_and_w: [ "toggle realistic visualization<br><br>more or less", "R", "ONE_SHOT", function() {
1818 $("#room").toggleClass("realistic");
1819 $(".filter_b_and_w").toggleClass("activated", $("#room").is(".realistic"));
1820 drawWidgets(widgets);
1821 } ],
1822 sep5: 1,
1823 build_circle: [ "start a new custom playingcards.io room", "none", "ONE_SHOT", function() {
1824 window.open("https://playingcards.io/room/new/generic", "_blank");
1825 } ],
1826 brush: [ "open Photopea", "none", "ONE_SHOT", function() {
1827 window.open("https://www.photopea.com/", "_blank");
1828 } ],
1829 help: [ "hotkeys & changelog", "?", "TOGGLE_TOOL", "#help" ]
1830 };
1831 var hotkeys = {
1832 A: $("#automationButton"),
1833 B: $("#board"),
1834 C: $("#card"),
1835 D: $("#cardDeck"),
1836 G: $("#gamePiece"),
1837 I: $("#spinner"),
1838 L: $("#label"),
1839 N: $("#hand"),
1840 P: $("#cardPile"),
1841 T: $("#counter")
1842 };
1843 function buildToolbar(buttons, toolbar) {
1844 for(var b in buttons) {
1845 if(b.match(/^sep[0-9]+$/)) {
1846 $("<span class='separator'> | </span>").appendTo(toolbar);
1847 continue;
1848 }
1849
1850 if(b.match(/^html[0-9]+$/)) {
1851 $(buttons[b]).appendTo(toolbar);
1852 continue;
1853 }
1854
1855 var toolInfo = "<br><br><i>Hotkey: " + buttons[b][1] + "</i>";
1856 if(buttons[b][2] === "TOGGLE_TOOL")
1857 toolInfo += "<br><br><i>this button opens a tool</i>";
1858
1859 if(hotkeys[buttons[b][1]] && buttons[b][1] != "none")
1860 console.log("HOTKEY CONFLICT for " + buttons[b][1]);
1861
1862 if(typeof buttons[b][2] === "object") {
1863 var subBar = $("<div class='toolbar " + b + "'></div>").appendTo(".secondary.toolbarContainer").hide();
1864 buildToolbar(buttons[b][2], subBar);
1865 toolInfo += "<br><br><i>this button opens a secondary toolbar</i>";
1866 }
1867
1868 hotkeys[buttons[b][1]] = $("<button class='material-icons " + b + "'>" + b + "<span class='tooltip'>" + buttons[b][0] + toolInfo + "</span></button>").on("click", function() {
1869 var button = this;
1870
1871 if(buttons[button.firstChild.nodeValue][2] === "ONE_SHOT") {
1872 buttons[button.firstChild.nodeValue][3](window.triggeredByHotkey || false);
1873 return;
1874 }
1875
1876 if(buttons[button.firstChild.nodeValue][2] === "TOGGLE_TOOL") {
1877 $(this).toggleClass("activated");
1878 $(buttons[button.firstChild.nodeValue][3]).toggle($(this).is(".activated"));
1879 return;
1880 }
1881
1882 if(typeof buttons[button.firstChild.nodeValue][2] === "object") {
1883 if($(this).is(".toolbarButton:not(.activated)"))
1884 $(this).siblings(".activated.toolbarButton").click();
1885 else
1886 $(".toolbar." + button.firstChild.nodeValue + " .activated.toolbarButton").click();
1887 $(this).toggleClass("activated").blur();
1888 $(".toolbar." + button.firstChild.nodeValue).toggle($(this).is(".activated"));
1889 return;
1890 }
1891
1892 var allSelected = [];
1893 $(".selected").each(function() {
1894 allSelected.push(addDimensions(widgets[$(this).data("index")]));
1895 });
1896 if(allSelected.length > 0)
1897 pushUndoStack();
1898 var i = 0;
1899 $(".selected").each(function() {
1900 buttons[button.firstChild.nodeValue][2](allSelected, i++, widgets[$(this).data("index")], addDimensions(widgets[$(this).data("index")]));
1901 });
1902 drawWidgets(widgets);
1903 }).appendTo(toolbar);
1904
1905 if(typeof buttons[b][2] === "object")
1906 hotkeys[buttons[b][1]].addClass("toolbarButton");
1907 }
1908 }
1909 buildToolbar(buttons, $(".primary.toolbar"));
1910
1911 $("#file").on("change", function(evt) {
1912 clearUndoStack();
1913 zipFile = evt.target.files[0];
1914 lastFilename = zipFile.name
1915 imageCache = {};
1916 boardImageCache = {};
1917 compressedImageCache = {};
1918 removeFromZip = {};
1919 $("#editPreview").empty();
1920 JSZip.loadAsync(zipFile).then(function(zip) {
1921 return zip.files["widgets.json"].async("text");
1922 }).then(function(content) {
1923 widgets = JSON.parse(content);
1924 $(".selected").removeClass("selected");
1925 drawWidgets(widgets);
1926 }, function (e) {
1927 console.log("error loading zip file", e);
1928 });
1929 });
1930 $("#fileImportWidgets").on("change", function(evt) {
1931 pushUndoStack();
1932 loadAllImages(evt.target.files[0], function() {
1933 JSZip.loadAsync(evt.target.files[0]).then(function(zip) {
1934 return zip.files["widgets.json"].async("text");
1935 }).then(function(content) {
1936 widgets = widgets.concat(JSON.parse(content));
1937 drawWidgets(widgets);
1938 }, function (e) {
1939 console.log("error loading zip file", e);
1940 });
1941 });
1942 });
1943 $("#fileImportTabletop").on("change", function(evt) {
1944 tabletop.showImport(evt.target.files[0]);
1945 $("#tabletopImporter").show();
1946 });
1947 $("#checkHAR").on("click", function() {
1948 var count = 0;
1949 var errors = 0;
1950
1951 loadAllImages(zipFile, function() {
1952 var fileNames = {};
1953 var duplicates = 0;
1954 for(var i in imageCache) {
1955 if(fileNames[imageCache[i]] === undefined) {
1956 fileNames[imageCache[i]] = i;
1957 } else {
1958 console.log("replacing duplicate " + i + " by " + fileNames[imageCache[i]]);
1959 widgets = JSON.parse(replaceall(JSON.stringify("package://" + i), JSON.stringify("package://" + fileNames[imageCache[i]]), JSON.stringify(widgets)));
1960 delete imageCache[i];
1961 removeFromZip[i] = true;
1962 ++duplicates;
1963 }
1964 }
1965
1966 var multiParts = {};
1967 JSON.parse($("#firefoxHAR").val()).log.entries.forEach(function(entry) {
1968 if(entry.request.method == "POST") {
1969 var request = entry.request.postData;
1970 var image = request.text.replace(/^(.|\r|\n)*?Content-Type: image\/.*\r\n\r\n/, "").replace(/\r\n.*$/, "");
1971 var response = null;
1972 try {
1973 response = JSON.parse(entry.response.content.text);
1974 } catch(e) {
1975 var name = "";
1976 entry.request.queryString.forEach(function(x) { if(x.name == "name") name = x.value; });
1977 for(var i in entry.request.headers) {
1978 var h = entry.request.headers[i];
1979 if(h.name == "X-Goog-Upload-Command" && h.value == "start")
1980 multiParts[name] = "";
1981 if(h.name == "X-Goog-Upload-Command" && h.value == "upload")
1982 multiParts[name] += image;
1983 }
1984 return;
1985 }
1986 var url = "https://firebasestorage.googleapis.com/v0/b/playingcardsio.appspot.com/o/" + response.name + "?alt=media&token=" + response.downloadTokens;
1987 //$("body").append("<img src='" + url + "'>");
1988 //$("body").append("<img src='data:" + imageType(btoa(image)) + ";base64," + btoa(image) + "'>");
1989 var packageURL = fileNames[btoa((multiParts[response.name] || "") + image)];
1990 if(packageURL) {
1991 if(!removeFromZip[packageURL]) {
1992 widgets = JSON.parse(replaceall(JSON.stringify("package://" + packageURL), JSON.stringify(url), JSON.stringify(widgets)));
1993 removeFromZip[packageURL] = true;
1994 delete imageCache[packageURL];
1995 ++count;
1996 }
1997 } else {
1998 console.log("PACKAGED ASSET NOT FOUND", entry, url, btoa((multiParts[response.name] || "") + image));
1999 ++errors;
2000 }
2001
2002 $("#statusTool").text("replaced " + count + " assets (" + errors + " assets not found - " + duplicates + " duplicates merged)");
2003 }
2004 });
2005 });
2006 });
2007 $("#typeFilter input").on("change", function() {
2008 drawWidgets(widgets);
2009 });
2010 $(function() {
2011 for(var header in generateWidgetPresets) {
2012 var optGroup = $("<optgroup label='" + header +"'>");
2013 for(var name in generateWidgetPresets[header])
2014 $("<option data-group='" + header + "'>" + name + "</option>").appendTo(optGroup);
2015 optGroup.appendTo($("#macros select"));
2016 }
2017 $("#macros select").change();
2018 });
2019 $("#macros select").on("change", function(evt) {
2020 $("#macros textarea").val(generateWidgetPresets[$("option:selected", this).data("group")][$(this).val()].replace(/^ /gm, '').trim());
2021 });
2022 $("#macros [type=button]").on("click", function(evt) {
2023 pushUndoStack();
2024 eval("obj = { " + $("#macros textarea").val() + "}");
2025 for(var i=0; i<(obj.number || 1); ++i) {
2026 var newWidget = obj.widget(i);
2027 if(typeof newWidget === "object" && newWidget.id !== undefined)
2028 addWidget(newWidget);
2029 else
2030 $("#statusTool").text("The generated widget was not added. It did not have an ID.");
2031 }
2032 drawWidgets(widgets);
2033 });
2034 $("#fileStripped").on("change", function(evt) {
2035 pushUndoStack();
2036 var tempZipFile = evt.target.files[0];
2037 JSZip.loadAsync(tempZipFile).then(function(zip) {
2038 return zip.files["widgets.json"].async("text");
2039 }).then(function(content) {
2040 newWidgets = JSON.parse(content);
2041 for(var i in newWidgets) {
2042 if(newWidgets[i].type == "board") {
2043 var oldWidget = widgets.filter((v) => v.id == newWidgets[i].id)[0];
2044 var newName = newWidgets[i].boardImage.replace("package://", "");
2045 var oldName = oldWidget.boardImage.replace("package://", "");
2046 if(newName != oldName) {
2047 if(oldWidget.boardImage.match(/^package/)) {
2048 delete imageCache[oldName];
2049 removeFromZip[oldName] = true;
2050 }
2051 if(newWidgets[i].boardImage.match(/^package/))
2052 JSZip.loadAsync(tempZipFile).then((zip) => zip.files[newName].async("base64").then(function(b) { imageCache[newName] = b; drawWidgets(widgets); }));
2053 }
2054 }
2055 }
2056 widgets = newWidgets;
2057 drawWidgets(widgets);
2058 }, function (e) {
2059 console.log("error loading zip file", e);
2060 });
2061 });
2062 $("#room").on("click", ".widget", function() {
2063 $(this).toggleClass("selected").data("selectionOrder", selectionOrder++);
2064 $("#statusTool").text($(".selected").length + " widgets selected");
2065 fillDetails(this);
2066 })
2067
2068 $("#jsonEdit .set").on("click", function() {
2069 pushUndoStack();
2070 widgets[$(".selected").data("index")] = json_postProcess(JSON.parse($("#json").val()));
2071 drawWidgets(widgets);
2072 });
2073
2074 $("#editCardDecks input.set").on("click", function() {
2075 pushUndoStack();
2076 $(".selected").each(function() {
2077 var widget = widgets[$(this).data("index")];
2078
2079 if($("#cardOverlapH").get(0).checked)
2080 widget.cardOverlapH = null;
2081 else
2082 widget.cardOverlapH = 0;
2083
2084 if($("#onRemoveFromHand").get(0).checked)
2085 delete widget.onRemoveFromHand;
2086 else
2087 widget.onRemoveFromHand = null;
2088
2089 if($("#confirmRecall").get(0).checked)
2090 widget.confirmRecall = true;
2091 else
2092 delete widget.confirmRecall;
2093
2094 if($("#confirmRecallAll").get(0).checked)
2095 delete widget.confirmRecallAll;
2096 else
2097 widget.confirmRecallAll = false;
2098
2099 if($("#enlarge").get(0).checked)
2100 widget.enlarge = true;
2101 else
2102 delete widget.enlarge;
2103 });
2104 drawWidgets(widgets);
2105 });
2106
2107 var ctrlIsPressed = false;
2108 var shiftIsPressed = false;
2109 var altIsPressed = false;
2110 document.onkeydown = ({key}) => {
2111 if(key == "Control")
2112 ctrlIsPressed = true;
2113 if(key == "Alt")
2114 altIsPressed = true;
2115 if(key == "Shift")
2116 shiftIsPressed = true;
2117
2118 if($("input:focus, textarea:focus").filter(":not([type])").length > 0)
2119 return true;
2120
2121 if(key == "Escape") {
2122 $(".activated").click();
2123 return false;
2124 }
2125
2126 //console.log(key, ctrlIsPressed, altIsPressed, shiftIsPressed);
2127 if(shiftIsPressed && key.match(/^Arrow/)) {
2128 $(".selected").each(function() {
2129 var left = key == "ArrowLeft" ? -1 : (key == "ArrowRight" ? 1 : 0);
2130 var top = key == "ArrowUp" ? -1 : (key == "ArrowDown" ? 1 : 0);
2131 if(ctrlIsPressed) {
2132 left *= 5;
2133 top *= 5;
2134 }
2135
2136 $(this).offset({ left: $(this).offset().left+left, top: $(this).offset().top+top });
2137 widgets[$(this).data("index")].x += left;
2138 widgets[$(this).data("index")].y += top;
2139 var w = addDimensions(widgets[$(this).data("index")]);
2140 $("#statusWidget").text(w.type + " " + Math.floor(w.width*10)/10 + "x" + Math.floor(w.height*10)/10 + "+" + Math.floor(w.x*10)/10 + "+" + Math.floor(w.y*10)/10);
2141 });
2142 return false;
2143 }
2144
2145 if(key == "B") {
2146 $("#board").click();
2147 return false;
2148 }
2149 if(key == "C") {
2150 $("#card").click();
2151 return false;
2152 }
2153 if(key == "P") {
2154 $("#cardPile").click();
2155 return false;
2156 }
2157
2158 if(hotkeys[key]) {
2159 triggeredByHotkey = true;
2160 hotkeys[key].click();
2161 triggeredByHotkey = false;
2162 return false;
2163 }
2164 };
2165 document.onkeyup = ({key}) => {
2166 if(key == "Control")
2167 ctrlIsPressed = false;
2168 if(key == "Alt")
2169 altIsPressed = false;
2170 if(key == "Shift")
2171 shiftIsPressed = false;
2172 };
2173
2174 function loadAllImages(zipfileToLoad, callback) {
2175 var toLoad = 0;
2176 var loaded = 0;
2177 JSZip.loadAsync(zipfileToLoad).then(function(zip) {
2178 zip.forEach(() => ++toLoad);
2179 zip.forEach(function(filename, file) {
2180 if(imageCache[filename] === undefined) {
2181 file.async("base64").then(function(content) {
2182 imageCache[filename] = content;
2183 ++loaded;
2184 if(loaded == toLoad)
2185 callback();
2186 });
2187 } else {
2188 ++loaded;
2189 if(loaded == toLoad)
2190 callback();
2191 }
2192 });
2193 });
2194 }
2195
2196 function addResultToEditPreview(dataURL, zipPath, target) {
2197 compressedImageCache[zipPath] = dataURL.replace(/data.*?,/, '');
2198 target.children("td:eq(2)").append("<img src=\"" + dataURL + "\">");
2199 target.children("td:eq(3)").append(Math.round(atob(compressedImageCache[zipPath]).length/1024) + " KiB");
2200 }
2201
2202 function addImageToEditPreview(width, height, path, $table, button) {
2203 if(!path.match(/^package/))
2204 return;
2205
2206 path = path.replace("package://", "");
2207 var $tr = $("<tr><td>Loading...</td><td></td><td></td><td></td></tr>").appendTo($table);
2208 loadImage(path, [ $tr, path ], function(image, type, vars) {
2209 var target = vars[0];
2210 var zipPath = vars[1];
2211
2212 target.children("td:eq(0)").empty().append("<img src=\"data:" + type + ";base64," + image + "\">");
2213 target.children("td:eq(1)").append(Math.round(atob(image).length/1024) + " KiB");
2214 if($(button).is(".compress"))
2215 compress("data:" + type + ";base64," + image, width, height, $("#compressType").val(), +$("#compressQuality").val(), (dataURL) => addResultToEditPreview(dataURL, zipPath, target));
2216 if($(button).is(".rotate"))
2217 rotate("data:" + type + ";base64," + image, +$("#rotateDegrees").val()).then((dataURL) => addResultToEditPreview(dataURL, zipPath, target));
2218 if($(button).is(".crop"))
2219 crop("data:" + type + ";base64," + image, +$("#cropX").val(), +$("#cropY").val(), +$("#cropWidth").val(), +$("#cropHeight").val(), (dataURL) => addResultToEditPreview(dataURL, zipPath, target));
2220 });
2221 }
2222
2223 $("#editImages [type=button]").on("click", function() {
2224 var button = this;
2225 $("#editPreview").empty().text("Loading...");
2226 compressedImageCache = {};
2227 loadAllImages(zipFile, function() {
2228 $("#editPreview").empty();
2229 $(".selected").each(function() {
2230 var widget = widgets[$(this).data("index")];
2231 if(widget.type == "board" || widget.type == "cardDeck") {
2232 widget = addDimensions(widget);
2233 $("#editPreview").append("<h2>" + widget.type + ": " + widget.id + "</h2>");
2234 var $table = $("<table><tr><th>Original</th><th>Size</th><th>Resized</th><th>Size</th></tr></table>").appendTo("#editPreview");
2235 $table.data("widgetIndex", $(this).data("index"));
2236 }
2237 if(widget.type == "board") {
2238 var width = +$("#compressWidth").val();
2239 var height = width*widget.height/widget.width;
2240 addImageToEditPreview(width, height, widget.boardImage, $table, button);
2241 }
2242 if(widget.type == "cardDeck") {
2243 var width = +$("#compressWidth").val();
2244 var height = width*widget.cardHeight/widget.cardWidth;
2245 for(var field in { faceTemplate: 1, backTemplate: 1 })
2246 for(var obj in widget[field].objects)
2247 if(widget[field].objects[obj].type == "image" && widget[field].objects[obj].valueType == "static")
2248 addImageToEditPreview(width, height, widget[field].objects[obj].value, $table, button);
2249 for(var t in widget.cardTypes)
2250 for(var key in widget.cardTypes[t])
2251 addImageToEditPreview(width, height, widget.cardTypes[t][key], $table, button);
2252 }
2253 });
2254 $("<input type=\"button\" value=\"Set\">").appendTo("#editPreview").on("click", function() {
2255 Object.assign(imageCache, compressedImageCache);
2256 if($(button).is(".rotate") && $("#rotateResizeDeck").prop("checked") || $(button).is(".crop") && $("#cropResizeDeck").prop("checked")) {
2257 $("#editPreview table").each(function() {
2258 var w = widgets[$(this).data("widgetIndex")];
2259 var widthFactor = $("img", this).get(1).width /$("img", this).get(0).width;
2260 var heightFactor = $("img", this).get(1).height/$("img", this).get(0).height;
2261
2262 if(w.type == "board") {
2263 w.width = addDimensions(w).width *widthFactor;
2264 w.height = addDimensions(w).height*heightFactor;
2265 }
2266
2267 if(w.type == "cardDeck")
2268 resizeCardDeck(w, addDimensions(w).cardWidth*widthFactor, addDimensions(w).cardHeight*heightFactor);
2269 });
2270 }
2271 drawWidgets(widgets);
2272 });
2273 $("#editPreview").get(0).scrollIntoView();
2274 });
2275 });
2276
2277 $("#editCardDecks input.preview").on("click", function() {
2278 if($(".selected.type-cardDeck").length == 0) {
2279 $("#editCardDecksPreview").empty().text("Please select at least one card deck!");
2280 return;
2281 }
2282
2283 $("#editCardDecksPreview").empty().text("Loading...");
2284 loadAllImages(zipFile, function() {
2285 $("#editCardDecksPreview").empty();
2286 $(".selected").each(function() {
2287 var widget = addDimensions(widgets[$(this).data("index")]);
2288 if(widget.type == "cardDeck") {
2289 $("#editCardDecksPreview").append("<h2>" + widget.type + ": " + widget.id + "</h2>");
2290 $("<input type='button' value='Toggle all'>").appendTo("#editCardDecksPreview").on("click", function() {
2291 $(this).next().children().click();
2292 });
2293 var $div = $("<div data-widgetid=\"" + widget.id + "\"></div>").appendTo("#editCardDecksPreview");
2294 for(var t in widget.cardTypes) {
2295 for(var key in widget.cardTypes[t]) {
2296 var value = widget.cardTypes[t][key];
2297 if(value.match(/^package/)) {
2298 loadImage(value.replace("package://", ""), [ $div, t, key, widget ], function(image, type, vars) {
2299 var count = widgets.filter((w) => w.type == "card" && w.deck == vars[3].id && w.cardType == vars[1]).length;
2300 vars[0].append("<div class='cardPreview'><img src=\"data:" + type + ";base64," + image + "\" data-type=\"" + vars[1] + "\" data-key=\"" + vars[2] + "\"><div>" + count + "x</div></div>");
2301 });
2302 break;
2303 } else if(value.match(/^(http|\/img)/)) {
2304 var url = value.match(/^\/img/) ? "https://playingcards.io" + value : value;
2305 var count = widgets.filter((w) => w.type == "card" && w.deck == widget.id && w.cardType == t).length;
2306 $div.append("<div class='cardPreview'><img src=\"" + url + "\" data-type=\"" + t + "\" data-key=\"" + key + "\"><div>" + count + "x</div></div>");
2307 break;
2308 }
2309 }
2310 }
2311 }
2312 });
2313 $("<input type='button' class='mergeTypes first' value='Merge to first' >").appendTo("#editCardDecksPreview");
2314 $("<input type='button' class='mergeTypes last' value='Merge to last' >").appendTo("#editCardDecksPreview");
2315 $("<input type='button' class='splitDeck move' value='Move to new deck'>").appendTo("#editCardDecksPreview");
2316 $("<input type='button' class='splitDeck copy' value='Copy to new deck'>").appendTo("#editCardDecksPreview");
2317 $(`<fieldset>
2318 <legend>Card rotation</legend>
2319 <input type='checkbox' id='addRotationRandomizedPile'><label for='addRotationRandomizedPile'> Add a draw pile with randomized rotations</label><br>
2320 <input type='checkbox' id='addRotationTray'><label for='addRotationTray'> Add card piles with all possible cards and a rotate button</label>
2321 - cards per type
2322 min <select id="rotTypeMin"><option selected>1</option><option>2</option><option>5</option><option>10</option></select>
2323 , target <select id="rotTypeTarget"><option>0.5n</option><option selected>1n</option><option>2n</option><option>5n</option><option>10n</option></select>
2324 , max <select id="rotTypeMax"><option>1</option><option>2</option><option>5</option><option selected>10</option><option>20</option><option>50</option></select>
2325 (n is the number on the card above - don't overdo it!)<br>
2326 <input type='button' class='addRotation' value='Add rotation'>
2327 </fieldset>
2328 `).appendTo("#editCardDecksPreview");
2329 $("#editCardDecksPreview").get(0).scrollIntoView();
2330 });
2331 });
2332
2333 function addCardsToRotatedPile(i, cardCount, clone, pileID, type, deg) {
2334 for(var cc=0; cc<cardCount; ++cc) {
2335 addWidget({
2336 id: pileID + "-" + deg + "-" + cc,
2337 type: "card",
2338 deck: clone.id,
2339 parent: pileID,
2340 cardType: type,
2341 x: 4 + i*(clone.cardWidth+10),
2342 y: 1104
2343 });
2344 }
2345 }
2346
2347 function addRotateTrayPile(clone, i, type, deg, cardCount, typesCount) {
2348 var trayPileID = clone.id + "-" + i + "-" + deg;
2349 var dy = 1000/typesCount;
2350 addWidget({
2351 id: trayPileID,
2352 type: "cardPile",
2353 x: deg == 0 ? 1600-clone.cardWidth-4 : 1700,
2354 y: -4 + i*dy,
2355 width: clone.cardWidth + 8,
2356 height: clone.cardHeight + 8
2357 });
2358 for(var cc=0; cc<cardCount; ++cc) {
2359 addWidget({
2360 id: trayPileID + "-" + cc,
2361 type: "card",
2362 deck: clone.id,
2363 parent: trayPileID,
2364 cardType: type,
2365 faceup: true,
2366 x: deg == 0 ? 1600-clone.cardWidth : 1704,
2367 y: 0 + i*dy
2368 });
2369 }
2370 }
2371
2372 async function addRotations(button) {
2373 pushUndoStack();
2374 var totalTodo = $("#editCardDecksPreview .cardSelected").length * 3;
2375 var done = 0;
2376 var decks = $("#editCardDecksPreview > div");
2377 for(var d=0; d<decks.length; ++d) {
2378 var deck = decks.eq(d);
2379 var widget = widgets.filter((w) => w.id == deck.data("widgetid"))[0];
2380 var widget = widgets.filter((w) => w.id == deck.data("widgetid"))[0];
2381 var clone = cloneCardDeck(widget, uniqueID("rotDeck"));
2382 clone.cardTypes = {};
2383 widgets = widgets.filter((w) => !(w.type == "card" && w.deck == clone.id));
2384 var images = $(".cardSelected > img", deck);
2385 var key = images.data("key");
2386
2387 if($("#addRotationRandomizedPile").prop("checked")) {
2388 var rotatedPileIDs = [];
2389 var rotatedDrawPileID = clone.id + "-permdraw";
2390 var automationButton = {
2391 id: clone.id + "-permbutton",
2392 type: "automationButton",
2393 label: "Start",
2394 x: 120,
2395 y: 120,
2396 clickRoutine: []
2397 };
2398 addWidget({
2399 id: rotatedDrawPileID,
2400 type: "cardPile",
2401 width: clone.cardWidth + 8,
2402 height: clone.cardHeight + 8
2403 });
2404 }
2405
2406 if($("#addRotationTray").prop("checked")) {
2407 var rotateButton = {
2408 id: clone.id + "-traybutton",
2409 type: "automationButton",
2410 label: "↻",
2411 x: 1600-50-clone.cardWidth,
2412 y: 50,
2413 clickRoutine: [],
2414 height: 40,
2415 width: 40
2416 };
2417 addWidget(rotateButton);
2418 }
2419
2420 for(var i=0; i<images.length; ++i) {
2421 var img = images.eq(i);
2422 var type = img.data("type");
2423 var cardCount = +img.next().text().replace(/x/, "");
2424
2425 clone.cardTypes[type] = { label: type };
2426 clone.cardTypes[type][key] = "package://userassets/" + type;
2427
2428 if($("#addRotationRandomizedPile").prop("checked")) {
2429 var pileID = clone.id + "-permdraw-" + i;
2430 rotatedPileIDs.push(pileID);
2431 addWidget({
2432 id: pileID,
2433 type: "cardPile",
2434 x: 0 + i*(clone.cardWidth+10),
2435 y: 1100,
2436 width: clone.cardWidth + 8,
2437 height: clone.cardHeight + 8
2438 });
2439 addCardsToRotatedPile(i, cardCount, clone, pileID, type, 0);
2440 automationButton.clickRoutine.push({
2441 "func": "MOVE_CARDS_BETWEEN_HOLDERS",
2442 "args": {
2443 "from": {
2444 "type": "literal",
2445 "value": [ pileID ]
2446 },
2447 "to": {
2448 "type": "literal",
2449 "value": [ rotatedDrawPileID ]
2450 },
2451 "quantity": {
2452 "type": "literal",
2453 "value": cardCount
2454 }
2455 }
2456 });
2457 }
2458
2459 if($("#addRotationTray").prop("checked")) {
2460 var trayCardCount = Math.max($("#rotTypeMin").val(), Math.min($("#rotTypeMax").val(), $("#rotTypeTarget").val().replace(/n/, "") * cardCount));
2461
2462 addRotateTrayPile(clone, i, type, "empty", 0, images.length);
2463 addRotateTrayPile(clone, i, type, 0, trayCardCount, images.length);
2464 [ [ "0", "empty" ], [ "90", "0" ], [ "180", "90" ], [ "270", "180" ], [ "empty", "270" ] ].forEach(function(v) {
2465 rotateButton.clickRoutine.push({
2466 "func": "MOVE_CARDS_BETWEEN_HOLDERS",
2467 "args": {
2468 "from": {
2469 "type": "literal",
2470 "value": [ clone.id + "-" + i + "-" + v[0] ]
2471 },
2472 "to": {
2473 "type": "literal",
2474 "value": [ clone.id + "-" + i + "-" + v[1] ]
2475 },
2476 "quantity": {
2477 "type": "literal",
2478 "value": 1000
2479 }
2480 }
2481 });
2482 });
2483 }
2484
2485 for(var deg=90; deg<=270; deg+=90) {
2486 var dataURL = await rotate(img.get(0).src, deg);
2487 var rotType = type + "-rot" + deg;
2488 clone.cardTypes[rotType] = { label: rotType };
2489 clone.cardTypes[rotType][key] = "package://userassets/" + rotType;
2490 imageCache["userassets/" + rotType] = dataURL.replace(/data.*?,/, '');
2491
2492 if($("#addRotationRandomizedPile").prop("checked")) {
2493 addCardsToRotatedPile(i, cardCount, clone, pileID, rotType, deg);
2494 }
2495
2496 if($("#addRotationTray").prop("checked")) {
2497 addRotateTrayPile(clone, i, rotType, deg, trayCardCount, images.length);
2498 }
2499
2500 ++done;
2501 $("#statusTool").text("Rotated " + done + "/" + totalTodo + " images.");
2502 }
2503 }
2504
2505 if($("#addRotationRandomizedPile").prop("checked")) {
2506 automationButton.clickRoutine.unshift({
2507 "func": "SHUFFLE_CARDS",
2508 "args": {
2509 "holders": {
2510 "type": "literal",
2511 "value": rotatedPileIDs
2512 }
2513 }
2514 });
2515 automationButton.clickRoutine.push({
2516 "func": "SHUFFLE_CARDS",
2517 "args": {
2518 "holders": {
2519 "type": "literal",
2520 "value": [ rotatedDrawPileID ]
2521 }
2522 }
2523 });
2524 addWidget(automationButton);
2525 }
2526 }
2527 $("#statusTool").text("Rotated deck added.");
2528 drawWidgets(widgets);
2529 }
2530
2531 $("#editCardDecksPreview").on("click", ".addRotation", function() {
2532 addRotations(this);
2533 });
2534
2535 function uniqueID(prefix="widget", cutSuffixes=false) {
2536 var currentIDs = widgets.map((w) => cutSuffixes ? w.id.replace(/-.*/, "") : w.id);
2537 var id;
2538
2539 for(var i=1; i<9999; ++i) {
2540 id = prefix+String(i).padStart(3, "0");
2541 if(currentIDs.indexOf(id) == -1)
2542 break;
2543 }
2544
2545 return id;
2546 }
2547
2548 function renameIDinClickRoutine(cr, funcName, argName, oldID, newID) {
2549 if(cr.func == funcName)
2550 cr.args[argName].value = cr.args[argName].value.map((x) => x == oldID ? newID : oldID);
2551 }
2552
2553 function renameID(widgets, oldID, newID) {
2554 widgets.forEach(function(w) {
2555 [ "id", "deck", "parent" ].forEach(function(f) {
2556 if(w[f] == oldID)
2557 w[f] = newID;
2558 });
2559 if(w.clickRoutine !== undefined) {
2560 w.clickRoutine.forEach(function(cr) {
2561 renameIDinClickRoutine(cr, "CHANGE_COUNTER", "counters", oldID, newID);
2562 renameIDinClickRoutine(cr, "FLIP_CARDS", "holders", oldID, newID);
2563 renameIDinClickRoutine(cr, "MOVE_CARDS_BETWEEN_HOLDERS", "from", oldID, newID);
2564 renameIDinClickRoutine(cr, "MOVE_CARDS_BETWEEN_HOLDERS", "to", oldID, newID);
2565 renameIDinClickRoutine(cr, "SHUFFLE_CARDS", "holders", oldID, newID);
2566 });
2567 }
2568 });
2569 }
2570
2571 $("#tools").on("click", ".cardPreview", function() {
2572 $(this).toggleClass("cardSelected");
2573 });
2574
2575 $("#editCardDecksPreview").on("click", ".mergeTypes", function() {
2576 var button = this;
2577 $("#editCardDecksPreview > div").each(function() {
2578 var widget = widgets.filter((w) => w.id == $(this).data("widgetid"))[0];
2579 var target = $(".cardSelected > img", this).eq($(button).is(".first") ? 0 : -1).data();
2580 $(".cardSelected > img", this).each(function() {
2581 var d = $(this).data();
2582 if(d.type != target.type) {
2583 var cards = widgets.filter((w) => w.type == "card" && w.deck == widget.id && w.cardType == d.type);
2584 delete widget.cardTypes[d.type];
2585 $.each(cards, (_, c) => c.cardType = target.type);
2586 }
2587 });
2588 });
2589 drawWidgets(widgets);
2590 $(".select_all").click();
2591 $("#editCardDecks input.preview").click();
2592 });
2593
2594 $("#editCardDecksPreview").on("click", ".splitDeck", function() {
2595 pushUndoStack();
2596 var button = this;
2597 $("#editCardDecksPreview > div").each(function() {
2598 var widget = widgets.filter((w) => w.id == $(this).data("widgetid"))[0];
2599 var clone = cloneCardDeck(widget);
2600
2601 $(":not(.cardSelected) > img", this).each(function() {
2602 var d = $(this).data();
2603 widgets = widgets.filter((w) => !(w.type == "card" && w.deck == clone.id && w.cardType == d.type));
2604 delete clone.cardTypes[d.type];
2605 });
2606
2607 if($(button).is(".move")) {
2608 $(".cardSelected > img", this).each(function() {
2609 var d = $(this).data();
2610 widgets = widgets.filter((w) => !(w.type == "card" && w.deck == widget.id && w.cardType == d.type));
2611 delete widget.cardTypes[d.type];
2612 });
2613 }
2614 });
2615 drawWidgets(widgets);
2616 });
2617
2618 var undoStack = [];
2619 function pushUndoStack() {
2620 undoStack.push(JSON.parse(JSON.stringify(widgets)));
2621 }
2622
2623 function popUndoStack() {
2624 if(undoStack.length)
2625 widgets = undoStack.pop();
2626 drawWidgets(widgets);
2627 }
2628
2629 function clearUndoStack() {
2630 undoStack = [];
2631 }
2632
2633 function selectByRectangle(x1, y1, x2, y2) {
2634 $(".widget").each(function() {
2635 var b = $(this).get(0).getBoundingClientRect();
2636 $(this).toggleClass("selected", b.left >= x1 && b.left+b.width <= x2 && b.top >= y1 && b.top+b.height <= y2).data("selectionOrder", selectionOrder++);
2637 fillDetails(this);
2638 });
2639 $("#statusTool").text($(".selected").length + " widgets selected");
2640 }
2641
2642 function addDimensions(widget) {
2643 widget = JSON.parse(JSON.stringify(widget));
2644 var fields = [ "width", "height" ];
2645 if(widget.type == "cardDeck") {
2646 var d = widgets.filter((v) => v.id == widget.deck)[0];
2647 widget.cardWidth = widget.cardWidth || defaultSizes["card"][0];
2648 widget.cardHeight = widget.cardHeight || defaultSizes["card"][1];
2649 }
2650 if(widget.type == "card") {
2651 var d = widgets.filter((v) => v.id == widget.deck)[0];
2652 if(widget.width == undefined)
2653 widget.width = d && d.cardWidth || defaultSizes["card"][0];
2654 if(widget.height == undefined)
2655 widget.height = d && d.cardHeight || defaultSizes["card"][1];
2656 }
2657 for(var i in fields) {
2658 if(widget[fields[i]] === undefined && widget.pieceType !== undefined)
2659 widget[fields[i]] = defaultSizes[widget.type][widget.pieceType][i];
2660 else if(widget[fields[i]] === undefined)
2661 widget[fields[i]] = defaultSizes[widget.type] && defaultSizes[widget.type][i] || 40;
2662 }
2663 return widget;
2664 }
2665
2666 function imageType(content) {
2667 // https://stackoverflow.com/a/58158656
2668 var signatures = {
2669 R0lGOD: "image/gif",
2670 iVBORw0KGgo: "image/png",
2671 PHN: "image/svg+xml"
2672 };
2673
2674 var type = "image/jpeg";
2675 for(var s in signatures)
2676 if(content.indexOf(s) === 0)
2677 type = signatures[s];
2678
2679 return type;
2680 }
2681
2682 function loadImage(image, target, callback) {
2683 if(imageCache[image] !== undefined) {
2684 callback(imageCache[image], imageType(imageCache[image]), target);
2685 return;
2686 }
2687 JSZip.loadAsync(zipFile).then(function(zip) {
2688 return zip.files[image].async("base64");
2689 }).then(function(content) {
2690 imageCache[image] = content;
2691 callback(content, imageType(content), target);
2692 }, function (e) {
2693 console.log("error loading image from zip file", e);
2694 });
2695 }
2696
2697 function drawWidgets(ws) {
2698 previouslySelected = $.map($(".selected"), (s) => (widgets[$(s).data("index")] || {id:null}).id);
2699 var typesToDisplay = {};
2700 $('#typeFilter input').get().map((o) => typesToDisplay[o.id] = o.checked);
2701
2702 $(".hideWithMulti").hide();
2703 $("#room").empty();
2704 for(var i in ws) {
2705 if(typesToDisplay[ws[i].type]) {
2706 var w = addDimensions(ws[i]);
2707 var $w = $("<div data-index='" + i + "' class='widget type-" + w.type + "' style='top: " + w.y + "px; left: " + w.x + "px; width: " + w.width + "px; height: " + w.height + "px; z-index: " + w.z + "'>"+(w.type == "automationButton" ? w.label : "")+"</div>").appendTo("#room");
2708
2709 if(w.type == "cardPile" && w.hasShuffleButton === true && typesToDisplay["label"]) {
2710 var labelY = w.y + 1.02*(w.height || 168 - 2) - 1;
2711 $("<div class='widgetLabel' style='top: " + labelY + "px; left: " + (w.x+1) + "px; width: " + (w.width-2) + "px; height: 32px; z-index: " + w.z + "'>Recall & Shuffle</div>").appendTo("#room");
2712 }
2713
2714 if((w.type == "cardPile" || w.type == "counter") && w.label && typesToDisplay["label"])
2715 $("<div class='widgetLabel' style='top: " + (w.y - 18) + "px; left: " + w.x + "px; width: " + w.width + "px; height: 18px; z-index: " + w.z + "'>" + w.label + "</div>").appendTo("#room");
2716
2717 if(w.type == "hand" && w.enabled === false)
2718 $w.addClass("disabled");
2719 if(w.type == "gamePiece" && !w.enabled)
2720 $w.addClass("subtype-" + w.pieceType).addClass("color-" + w.color);
2721 if(w.type == "board" && w.boardImage != undefined) {
2722 if(w.boardImage.match(/^package/)) {
2723 var imageName = w.boardImage.replace("package://", "");
2724 loadImage(imageName, [ $w, imageName ], function(image, type, target) {
2725 target[0].css("background-image", "url(data:" + type + ";base64," + image + ")");
2726 boardImageCache[target[1]] = image;
2727 });
2728 } else if(w.boardImage.match(/^\//)) {
2729 $w.css("background-image", "url(http://playingcards.io" + w.boardImage + ")");
2730 } else {
2731 $w.css("background-image", "url(" + w.boardImage + ")");
2732 }
2733 }
2734 }
2735 }
2736 }
2737 $(function() { drawWidgets(widgets); });
2738
2739 function json_preProcess(w) {
2740 if($("#compactAB").prop("checked") && w.type == "automationButton") {
2741 for(var i in w.clickRoutine) {
2742 if(w.clickRoutine[i].func == "MOVE_CARDS_BETWEEN_HOLDERS") {
2743 var moveFlip = w.clickRoutine[i].args.moveFlip && w.clickRoutine[i].args.moveFlip.value;
2744 w.clickRoutine[i] = [ "MOVE", w.clickRoutine[i].args.from.value, (w.clickRoutine[i].args.quantity || { value: 1 }).value, w.clickRoutine[i].args.to.value ];
2745 if(w.clickRoutine[i][1].length == 1)
2746 w.clickRoutine[i][1] = w.clickRoutine[i][1][0];
2747 if(w.clickRoutine[i][3].length == 1)
2748 w.clickRoutine[i][3] = w.clickRoutine[i][3][0];
2749 }
2750 if(w.clickRoutine[i].func == "SHUFFLE_CARDS") {
2751 w.clickRoutine[i] = [ "SHUFFLE", w.clickRoutine[i].args.holders.value ];
2752 if(w.clickRoutine[i][1].length == 1)
2753 w.clickRoutine[i][1] = w.clickRoutine[i][1][0];
2754 }
2755 if(w.clickRoutine[i].func == "FLIP_CARDS") {
2756 w.clickRoutine[i] = [ "FLIP", w.clickRoutine[i].args.flipMode.value, w.clickRoutine[i].args.holders.value ];
2757 if(w.clickRoutine[i][2].length == 1)
2758 w.clickRoutine[i][2] = w.clickRoutine[i][2][0];
2759 }
2760 if(w.clickRoutine[i].func == "CHANGE_COUNTER") {
2761 w.clickRoutine[i] = [ "COUNTER", w.clickRoutine[i].args.counters.value, w.clickRoutine[i].args.changeMode.value, w.clickRoutine[i].args.changeNumber.value ];
2762 if(w.clickRoutine[i][1].length == 1)
2763 w.clickRoutine[i][1] = w.clickRoutine[i][1][0];
2764 }
2765 if(moveFlip && moveFlip != "none")
2766 w.clickRoutine[i][4] = moveFlip;
2767 }
2768 }
2769 return w;
2770 }
2771
2772 function json_postProcess(w) {
2773 if($("#compactAB").prop("checked") && w.type == "automationButton") {
2774 for(var i in w.clickRoutine) {
2775 if(Array.isArray(w.clickRoutine[i]) && w.clickRoutine[i][0] == "MOVE") {
2776 if(typeof w.clickRoutine[i][1] === "string")
2777 w.clickRoutine[i][1] = [ w.clickRoutine[i][1] ];
2778 if(typeof w.clickRoutine[i][3] === "string")
2779 w.clickRoutine[i][3] = [ w.clickRoutine[i][3] ];
2780 w.clickRoutine[i] = {
2781 "func": "MOVE_CARDS_BETWEEN_HOLDERS",
2782 "args": {
2783 "from": {
2784 "type": "literal",
2785 "value": w.clickRoutine[i][1]
2786 },
2787 "to": {
2788 "type": "literal",
2789 "value": w.clickRoutine[i][3]
2790 },
2791 "quantity": {
2792 "type": "literal",
2793 "value": w.clickRoutine[i][2]
2794 },
2795 "moveFlip": {
2796 "type": "literal",
2797 "value": w.clickRoutine[i][4] || "none"
2798 }
2799 }
2800 };
2801 }
2802 if(Array.isArray(w.clickRoutine[i]) && w.clickRoutine[i][0] == "SHUFFLE") {
2803 if(typeof w.clickRoutine[i][1] === "string")
2804 w.clickRoutine[i][1] = [ w.clickRoutine[i][1] ];
2805 w.clickRoutine[i] = {
2806 "func": "SHUFFLE_CARDS",
2807 "args": {
2808 "holders": {
2809 "type": "literal",
2810 "value": w.clickRoutine[i][1]
2811 }
2812 }
2813 };
2814 }
2815 if(Array.isArray(w.clickRoutine[i]) && w.clickRoutine[i][0] == "FLIP") {
2816 if(typeof w.clickRoutine[i][2] === "string")
2817 w.clickRoutine[i][2] = [ w.clickRoutine[i][2] ];
2818 w.clickRoutine[i] = {
2819 "func": "FLIP_CARDS",
2820 "args": {
2821 "flipMode": {
2822 "type": "literal",
2823 "value": w.clickRoutine[i][1]
2824 },
2825 "holders": {
2826 "type": "literal",
2827 "value": w.clickRoutine[i][2]
2828 }
2829 }
2830 };
2831 }
2832 if(Array.isArray(w.clickRoutine[i]) && w.clickRoutine[i][0] == "COUNTER") {
2833 if(typeof w.clickRoutine[i][1] === "string")
2834 w.clickRoutine[i][1] = [ w.clickRoutine[i][1] ];
2835 w.clickRoutine[i] = {
2836 "func": "CHANGE_COUNTER",
2837 "args": {
2838 "changeMode": {
2839 "type": "literal",
2840 "value": w.clickRoutine[i][2]
2841 },
2842 "changeNumber": {
2843 "type": "literal",
2844 "value": w.clickRoutine[i][3]
2845 },
2846 "counters": {
2847 "type": "literal",
2848 "value": w.clickRoutine[i][1]
2849 }
2850 }
2851 };
2852 }
2853 }
2854 }
2855 return w;
2856 }
2857
2858 function json_postProcessString(str) {
2859 return str.replace(/\[\n +"(MOVE|SHUFFLE|FLIP|COUNTER)",\n[^\[\]]*(\[[^\[\]]*\][^\[\]]*)*\]/g, function(match) {
2860 return match.replace(/\n */g, " ");
2861 }).replace(/"options": \[\n[^\[\]]*\]/g, function(match) {
2862 return match.replace(/\n */g, " ");
2863 });
2864 }
2865
2866 $("#jsonEdit [type=checkbox]").on("click", () => fillDetails($(".selected").get(0)));
2867
2868 function fillDetails(element) {
2869 var w = addDimensions(widgets[$(element).data("index")]);
2870
2871 $(".hideWithMulti").toggle($(".selected").length == 1);
2872
2873 if($(".selected").length == 0) {
2874 $("#x, #y, #height, #width, #quickSetX.inactive, #quickSetY.inactive, #quickSetW.inactive, #quickSetH.inactive, #quickSetID").val("");
2875 return;
2876 } else if($(".selected").length == 1) {
2877 $("#x").val(w.x);
2878 $("#y").val(w.y);
2879
2880 $("#width" ).val(w.width);
2881 $("#height").val(w.height);
2882
2883 $("#quickSetX.inactive").val($(".highlight_alt").is(".activated") ? 1600 - w.width - w.x : w.x);
2884 $("#quickSetY.inactive").val($(".highlight_alt").is(".activated") ? 1000 - w.height - w.y : w.y);
2885
2886 $("#quickSetW.inactive").val(w.width);
2887 $("#quickSetH.inactive").val(w.height);
2888
2889 $("#quickSetID").val(w.id);
2890 } else {
2891 return;
2892 }
2893
2894 w = widgets[$(".selected:eq(0)").data("index")];
2895 $("#json").val(json_postProcessString(JSON.stringify(json_preProcess(JSON.parse(JSON.stringify(w))), null, " ")));
2896 if(w.pieceType !== undefined)
2897 $("#defaultSize").text(" for " + w.type + ": " + defaultSizes[w.type][w.pieceType][0] + "*" + defaultSizes[w.type][w.pieceType][1]);
2898 else if(defaultSizes[w.type] !== undefined)
2899 $("#defaultSize").text(" for " + w.type + ": " + defaultSizes[w.type][0] + "*" + defaultSizes[w.type][1]);
2900 else
2901 $("#defaultSize").text(" for " + w.type + " is unavailable");
2902 }
2903
2904 $(".toolbar").on("focus", "input.inactive", function() {
2905 $(this).removeClass("inactive");
2906 });
2907 $(".toolbar").on("blur", "input:not(inactive)", function() {
2908 if($(this).val() == "") {
2909 $(this).addClass("inactive");
2910 fillDetails($(".selected").get(0));
2911 }
2912 });
2913 function resizeCardDeck(w, width, height) {
2914 var dim = addDimensions(w);
2915 var widthFactor = width/dim.cardWidth;
2916 var heightFactor = height/dim.cardHeight;
2917
2918 w.cardWidth *= widthFactor;
2919 w.cardHeight *= heightFactor;
2920 var templateScale = function(o) {
2921 o.x *= widthFactor;
2922 o.y *= heightFactor;
2923 o.w *= widthFactor;
2924 o.h *= heightFactor;
2925 };
2926 w.backTemplate.objects.forEach(templateScale);
2927 w.faceTemplate.objects.forEach(templateScale);
2928
2929 var pileIDs = widgets.map((wi) => wi.type == "card" && wi.deck == w.id && wi.parent || null);
2930 widgets.forEach(function (wi) {
2931 if(wi.type == "cardPile" && pileIDs.indexOf(wi.id) > -1) {
2932 wi.width = (wi.width - 8)*widthFactor + 8;
2933 wi.height = (wi.height - 8)*heightFactor + 8;
2934 }
2935 });
2936 }
2937
2938 function cloneCardDeck(w, id) {
2939 var clone = JSON.parse(JSON.stringify(w));
2940 var newID = id || uniqueID(clone.id + "-copy");
2941
2942 clone.id = newID;
2943 clone.parent = null;
2944 clone.x = 200;
2945 clone.y = 200;
2946
2947 addWidget(clone);
2948
2949 widgets.forEach(function (wi) {
2950 if(wi.type == "card" && wi.deck == w.id) {
2951 var cardClone = JSON.parse(JSON.stringify(wi));
2952
2953 delete cardClone.id;
2954 cardClone.x = 200;
2955 cardClone.y = 200;
2956 cardClone.deck = newID;
2957 cardClone.parent = null;
2958
2959 addWidget(cardClone);
2960 }
2961 });
2962
2963 return clone;
2964 }
2965
2966 function downloadURI(uri, name) {
2967 // https://stackoverflow.com/a/15832662
2968 var link = document.createElement("a");
2969 link.download = name;
2970 link.href = uri;
2971 document.body.appendChild(link);
2972 link.click();
2973 document.body.removeChild(link);
2974 delete link;
2975 }
2976
2977 function compress(url, width, height, type, compression, callback) {
2978 Jimp.read(url).then(image => {
2979 return image.background(0xFFFFFFFF).contain(width, height, Jimp.RESIZE_BICUBIC).quality(compression*100).getBase64Async(type == "image/jpeg" ? Jimp.MIME_JPEG : Jimp.MIME_PNG);
2980 }).then(base64 => callback(base64)).catch(console.log);
2981 }
2982
2983 function rotate(url, degrees) {
2984 return Jimp.read(url).then(image => {
2985 return image.rotate(-degrees, Jimp.RESIZE_BICUBIC).getBase64Async(Jimp.MIME_PNG);
2986 }).catch(console.log);
2987 }
2988
2989 function crop(url, x, y, width, height, callback) {
2990 Jimp.read(url).then(image => {
2991 return image.crop(x, y, width, height).getBase64Async(Jimp.MIME_PNG);
2992 }).then(base64 => callback(base64)).catch(console.log);
2993 }
2994
2995 function toDataURL(url, callback) {
2996 // https://stackoverflow.com/a/20285053
2997 var xhr = new XMLHttpRequest();
2998 xhr.onload = function() {
2999 var reader = new FileReader();
3000 reader.onloadend = function() {
3001 callback(reader.result);
3002 }
3003 reader.readAsDataURL(xhr.response);
3004 };
3005
3006 if(window.location.protocol == "https:")
3007 url = url.replace(/^http:/, "https:");
3008
3009 xhr.open('GET', url);
3010 xhr.responseType = 'blob';
3011 xhr.send();
3012 }
3013
3014 function hashFnv32a(str, asString, seed) {
3015 // https://stackoverflow.com/a/22429679
3016 var i, l,
3017 hval = (seed === undefined) ? 0x811c9dc5 : seed;
3018
3019 for (i = 0, l = str.length; i < l; i++) {
3020 hval ^= str.charCodeAt(i);
3021 hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
3022 }
3023 if( asString ){
3024 // Convert to 8 digit hex string
3025 return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
3026 }
3027 return hval >>> 0;
3028 }
3029
3030 function dataURLtoFile(dataurl, filename) {
3031 // https://stackoverflow.com/a/38935990
3032 var arr = dataurl.split(','),
3033 mime = arr[0].match(/:(.*?);/)[1],
3034 bstr = atob(arr[1]),
3035 n = bstr.length,
3036 u8arr = new Uint8Array(n);
3037
3038 while(n--)
3039 u8arr[n] = bstr.charCodeAt(n);
3040
3041 return new File([u8arr], filename, {type:mime});
3042 }
3043
3044 function copyTextToClipboard(text) {
3045 // https://stackoverflow.com/a/30810322
3046 var textArea = document.createElement("textarea");
3047 textArea.value = text;
3048
3049 // Avoid scrolling to bottom
3050 textArea.style.top = "0";
3051 textArea.style.left = "0";
3052 textArea.style.position = "fixed";
3053
3054 document.body.appendChild(textArea);
3055 textArea.focus();
3056 textArea.select();
3057
3058 try {
3059 var successful = document.execCommand('copy');
3060 var msg = successful ? 'successful' : 'unsuccessful';
3061 console.log('Fallback: Copying text command was ' + msg);
3062 } catch (err) {
3063 console.error('Fallback: Oops, unable to copy', err);
3064 }
3065
3066 document.body.removeChild(textArea);
3067 }
3068
3069 window.onmouseover = function(e) {
3070 var i = $(e.target).data("index");
3071 if(i !== undefined) {
3072 var w = addDimensions(widgets[i]);
3073 $("#statusWidget").text(w.type + " " + Math.floor(w.width*10)/10 + "x" + Math.floor(w.height*10)/10 + "+" + Math.floor(w.x*10)/10 + "+" + Math.floor(w.y*10)/10);
3074 } else {
3075 $("#statusWidget").text("-");
3076 }
3077 };
3078
3079 $('div').on('dragstart', function(event) { event.preventDefault(); });
3080 // https://stackoverflow.com/a/23284608
3081 var div = document.getElementById('selectionDiv'), x1 = 0, y1 = 0, x2 = 0, y2 = 0;
3082 function reCalc() { //This will restyle the div
3083 var x3 = Math.min(x1,x2); //Smaller X
3084 var x4 = Math.max(x1,x2); //Larger X
3085 var y3 = Math.min(y1,y2); //Smaller Y
3086 var y4 = Math.max(y1,y2); //Larger Y
3087 div.style.left = x3 + 'px';
3088 div.style.top = y3 + 'px';
3089 div.style.width = x4 - x3 + 'px';
3090 div.style.height = y4 - y3 + 'px';
3091 }
3092 var $movingWidgets = $([]);
3093 var movingWidgetAspect = 1;
3094 $(document).on("mousedown", function(e) {
3095 if($("input:hover, textarea:hover, button:hover, select:hover").length > 0)
3096 return true;
3097
3098 if(!$(e.target).is("#room") && (moveWidgets || resizeWidgets)) {
3099 pushUndoStack();
3100 $movingWidgets = $(".selected");
3101 if(!$movingWidgets.length)
3102 $movingWidgets = $(e.target);
3103 $movingWidgets.each(function() {
3104 var rect = $(this).get(0).getBoundingClientRect();
3105 $(this).data("moveStartClientRect", rect).data("resizeAspect", rect.width/rect.height);
3106 });
3107 } else {
3108 $(div).show(); //Unhide the div
3109 }
3110 x1 = x2 = e.clientX; //Set the initial X
3111 y1 = y2 = e.clientY; //Set the initial Y
3112 reCalc();
3113 });
3114 $(document).on("mousemove", function(e) {
3115 x2 = e.clientX; //Update the current position X
3116 y2 = e.clientY; //Update the current position Y
3117
3118 var b = $("#room").get(0).getBoundingClientRect();
3119 var scale = 1600 / (b.width - 2);
3120 var x = Math.floor((x2 - b.x) * scale);
3121 var y = Math.floor((y2 - b.y) * scale);
3122 $("#statusCoordinates").text(x + ", " + y);
3123 reCalc();
3124 if(moveWidgets && $movingWidgets.length) {
3125 $movingWidgets.each(function() {
3126 var newX = Math.floor(x + ($(this).data("moveStartClientRect").left - x1) * scale);
3127 var newY = Math.floor(y + ($(this).data("moveStartClientRect").top - y1) * scale);
3128 $(this).css({ left: newX+"px", top: newY+"px" });
3129 widgets[$(this).data("index")].x = newX;
3130 widgets[$(this).data("index")].y = newY;
3131 });
3132 $("#statusTool").text("Δx " + ((x2-x1) * scale) + ", Δy " + ((y2-y1) * scale));
3133 }
3134 if(resizeWidgets && $movingWidgets.length) {
3135 $movingWidgets.each(function() {
3136 var w = Math.floor(x-$(this).position().left * scale);
3137 var h = Math.floor(y-$(this).position().top * scale);
3138 if(ctrlIsPressed)
3139 h = Math.floor(w / $(this).data("resizeAspect"));
3140 $movingWidgets.css({ width: w+"px", height: h+"px" });
3141 widgets[$(this).data("index")].width = w;
3142 widgets[$(this).data("index")].height = h;
3143 });
3144 $("#statusTool").text(w + "x" + h);
3145 }
3146 }).on("mouseup", function(e) {
3147 if(moveWidgets || resizeWidgets) {
3148 $movingWidgets = $([]);
3149 drawWidgets(widgets);
3150 $(".select_all").click();
3151 }
3152 if(!$(div).is(":hidden")) {
3153 $(div).hide(); //Hide the div
3154 if(Math.abs(x1-x2) > 2 && Math.abs(y1-y2) > 2)
3155 selectByRectangle(Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2));
3156 }
3157 });
3158
3159
3160
3161 tabletop = {
3162 images: {},
3163 crops: {},
3164
3165 addCardDecks: function(o, fieldName, target) {
3166 var container = tabletop.addFieldHeaderAndContainer(fieldName, target);
3167 (o.DeckIDs || [ o.CardID ]).forEach(function(id) {
3168 var deckID = Math.floor(id/100);
3169 tabletop.addCardToDeck(o.CustomDeck[deckID], id%100, container);
3170 });
3171 var importContainer = $("<div class='import'></div>").appendTo(container);
3172 $("<div class='loading'>Loading...</div>").appendTo(container);
3173 $("<label>Target height: </label>" ).appendTo(importContainer);
3174 var targetHeight = $("<input class='targetHeight' class='short' value='160'>" ).appendTo(importContainer);
3175 $("<button>Import as new deck</button>").appendTo(importContainer).on("click", function() {
3176 var cards = $(".cardSelected.cardPreview", container);
3177 if(!cards.length)
3178 cards = $(".cardPreview", container);
3179 tabletop.importAsNewDeck(cards, +targetHeight.val());
3180 });
3181 $("<input type='button' value='Toggle all'>").appendTo(importContainer).on("click", function() {
3182 $(".cardPreview", container).click();
3183 });
3184 },
3185 addCardImage: function(frontURL, backURL, target) {
3186 return $("<div class='cardPreview'><img class='front' src='" + frontURL + "'><img class='back' src='" + backURL + "'></div>").appendTo(target);
3187 },
3188 addCardToDeck: function(deck, id, target) {
3189 var w = deck.NumWidth || 10;
3190 var h = deck.NumHeight || 7;
3191
3192 if(deck.BackURL.match(/\{Unique\}$/)) {
3193 deck.UniqueBack = true;
3194 deck.BackURL = deck.BackURL.replace(/\{Unique\}$/, "");
3195 }
3196
3197 tabletop.cropCard(deck.FaceURL, h, w, id, 0, function(url) {
3198 if(deck.UniqueBack) {
3199 tabletop.cropCard(deck.BackURL, h, w, id, 0, function(backURL) {
3200 $(".loading", target).remove();
3201 tabletop.addCardImage(url, backURL, target);
3202 });
3203 } else {
3204 $(".loading", target).remove();
3205 tabletop.addCardImage(url, deck.BackURL, target);
3206 }
3207 }, function() {
3208 $("<div class='error'><p>Loading and cropping failed! Did you disable CORS (see step 1 above)?</p><p>If you didn't, do it and reload with <CTRL>+<F5> to clear the cache.</p><p>If you did, the image is probably offline.</p></div>").appendTo(target);
3209 });
3210 },
3211 addFieldHeaderAndContainer: function(fieldName, target) {
3212 $("<h5>" + fieldName + "</h5>").appendTo(target);
3213 return $("<div></div>").appendTo(target);
3214 },
3215 addImage: function(url, fieldName, target, useParent) {
3216 var container = tabletop.addFieldHeaderAndContainer(fieldName, target);
3217 var importContainer = $("<div class='import'></div>").appendTo(container);
3218 $("<label>Target height: </label>" ).appendTo(importContainer);
3219 var targetHeight = $("<input class='targetHeight' class='short' value='160'>" ).appendTo(importContainer);
3220 $("<button>Import as board</button>").appendTo(importContainer).on("click", ()=>tabletop.importAsBoard(container, url));
3221 $("<button>Import as card</button>" ).appendTo(importContainer).on("click", ()=>tabletop.importAsNewDeck($(".cardPreview", useParent ? container.parent() : container), +targetHeight.val()) );
3222 $("<div class='cardPreview'><img src='" + url + "'></div>").appendTo(container);
3223 },
3224 cropCard: function(url, rows, perRow, i, rotation, callback, errorCallback) {
3225 if(!(url in tabletop.images)) {
3226 tabletop.crops[url] = {};
3227 tabletop.crops[url].meta = { rows: rows, perRow: perRow, rotation: rotation };
3228 tabletop.images[url] = new Image();
3229 tabletop.images[url].crossOrigin = "anonymous";
3230 tabletop.images[url].onload = tabletop.imageLoaded;
3231 tabletop.images[url].onerror = errorCallback;
3232 }
3233
3234 if(!(i in tabletop.crops[url]))
3235 tabletop.crops[url][i] = [];
3236
3237 if(typeof tabletop.crops[url][i] == "string")
3238 callback(tabletop.crops[url][i]);
3239 else
3240 tabletop.crops[url][i].push(callback);
3241 //img.src = "https://cors-anywhere.herokuapp.com/" + url;
3242 tabletop.images[url].src = url;
3243 },
3244 imageLoaded: function() {
3245 var m = tabletop.crops[this.src].meta;
3246 const canvas = document.createElement('canvas');
3247 canvas.width = this.width/m.perRow;
3248 canvas.height = this.height/m.rows;
3249 const ctx = canvas.getContext('2d');
3250 //console.log(i, canvas.width, canvas.width*(i%perRow), canvas.height, canvas.height*Math.floor(i/perRow));
3251 for(var i in tabletop.crops[this.src]) {
3252 if(i != "meta") {
3253 ctx.drawImage(this, canvas.width*(i%m.perRow), canvas.height*Math.floor(i/m.perRow), this.width/m.perRow, this.height/m.rows, 0, 0, this.width/m.perRow, this.height/m.rows);
3254 var dataURL = ctx.canvas.toDataURL("image/jpeg", 0.9);
3255 tabletop.crops[this.src][i].forEach(function(c) { c(dataURL); });
3256 tabletop.crops[this.src][i] = dataURL;
3257 }
3258 }
3259 },
3260 importAsBoard: function(container, url) {
3261 pushUndoStack();
3262 var height = +$(".targetHeight", container).val();
3263 addWidget({
3264 id: uniqueID("ttboard"),
3265 type: "board",
3266 height: height,
3267 width: height*$("img", container).width()/$("img", container).height(),
3268 boardImage: url,
3269 dragging: null
3270 });
3271 drawWidgets(widgets);
3272 },
3273 importAsNewDeck: function(cards, targetHeight) {
3274 pushUndoStack();
3275 var deck = uniqueID("ttdeck");
3276 var pile = uniqueID("ttpile");
3277
3278 var x = 24;
3279 var y = 24;
3280
3281 var width = 160;
3282 var height = 160;
3283 var types = {};
3284
3285 var typeI = 0;
3286
3287 cards.each(function() {
3288 var front = $(this).children("img:eq(0)");
3289 var back = $(this).children("img.back");
3290 if(back.length == 0)
3291 back = front;
3292
3293 width = front.width();
3294 height = front.height();
3295
3296 var frontURL = front.get(0).src;
3297 var backURL = back.get(0).src;
3298
3299 var type = deck + "-" + (++typeI);
3300 if(frontURL.match(/^data/)) {
3301 imageCache["userassets/" + type] = frontURL.replace(/data.*?,/, '');
3302 types[type] = {
3303 label: type,
3304 front: "package://userassets/" + type
3305 };
3306 } else {
3307 types[type] = {
3308 label: type,
3309 front: frontURL
3310 };
3311 }
3312 if(backURL.match(/^data/)) {
3313 imageCache["userassets/" + type + "-back"] = backURL.replace(/data.*?,/, '');
3314 types[type].back = "package://userassets/" + type + "-back";
3315 } else {
3316 types[type].back = backURL;
3317 }
3318 });
3319
3320 addWidget({
3321 id: pile,
3322 x: x,
3323 y: y,
3324 type: "cardPile",
3325 dragging: null,
3326 hasShuffleButton: true,
3327 width: targetHeight*width/height + 8,
3328 height: targetHeight + 8
3329 });
3330 addWidget({
3331 id: deck,
3332 type: "cardDeck",
3333 faceTemplate: {
3334 includeBorder: false,
3335 includeRadius: true,
3336 objects: [
3337 {
3338 type: "image",
3339 x: 0,
3340 y: 0,
3341 w: targetHeight*width/height,
3342 h: targetHeight,
3343 color: "white",
3344 valueType: "dynamic",
3345 value: "front"
3346 }
3347 ]
3348 },
3349 backTemplate: {
3350 includeBorder: false,
3351 includeRadius: true,
3352 objects: [
3353 {
3354 type: "image",
3355 x: 0,
3356 y: 0,
3357 w: targetHeight*width/height,
3358 h: targetHeight,
3359 color: "white",
3360 valueType: "dynamic",
3361 value: "back"
3362 }
3363 ]
3364 },
3365 cardTypes: types,
3366 dragging: null,
3367 x: x+13,
3368 y: y+44,
3369 parent: pile,
3370 cardWidth: targetHeight*width/height,
3371 cardHeight: targetHeight,
3372 cardOverlapH: 0,
3373 enlarge: true,
3374 });
3375 for(var t in types) {
3376 addWidget({
3377 id: uniqueID("ttcard"),
3378 type: "card",
3379 cardType: t,
3380 deck: deck,
3381 parent: pile,
3382 x: x+4,
3383 y: y+4,
3384 faceup: false,
3385 dragging: null,
3386 owner: null
3387 });
3388 }
3389 drawWidgets(widgets);
3390 },
3391 listObjects: function(objects, target) {
3392 objects.forEach(function(o) {
3393 if([ "FogOfWarTrigger" ].indexOf(o.Name) > -1)
3394 return;
3395
3396 var title = o.Nickname ? o.Name + " (" + o.Nickname + ")" : o.Name;
3397 var container = $("<div></div>").appendTo(target);
3398 $("<h4>" + title + "</h4>").appendTo(container);
3399 tabletop.showJSONbutton(o, container);
3400 $("<button>Remove</button>").appendTo(container).on("click", function() { $(this).parent().remove(); });
3401
3402 if("CustomImage" in o && o.CustomImage && "ImageURL" in o.CustomImage && o.CustomImage.ImageURL)
3403 tabletop.addImage(o.CustomImage.ImageURL, "CustomImage.ImageURL", container, "USE_PARENT");
3404 if("CustomImage" in o && o.CustomImage && "ImageSecondaryURL" in o.CustomImage && o.CustomImage.ImageSecondaryURL) {
3405 var backContainer = $("<div class='deckBack'>").appendTo(container);
3406 tabletop.addImage(o.CustomImage.ImageSecondaryURL, "CustomImage.ImageSecondaryURL", backContainer);
3407 }
3408 if("CustomMesh" in o && o.CustomMesh && "DiffuseURL" in o.CustomMesh && o.CustomMesh.DiffuseURL)
3409 tabletop.addImage(o.CustomMesh.DiffuseURL, "CustomMesh.DiffuseURL", container);
3410 if("CustomDeck" in o && o.CustomDeck)
3411 tabletop.addCardDecks(o, "CustomDeck", container);
3412 if(o.Name == "Custom_PDF")
3413 $("<p>Not importable: <a href='" + o.CustomPDF.PDFUrl + "'>PDF</a></p>").appendTo(container);
3414
3415 if(o.ContainedObjects && o.Name != "DeckCustom" && o.Name != "Deck")
3416 tabletop.listObjects(o["ContainedObjects"], $("<div></div>").appendTo(container));
3417 if($("img,p", container).length == 0)
3418 container.addClass("empty");
3419 });
3420 },
3421 readJSON: function(json) {
3422 tabletop.images = {};
3423 tabletop.crops = {};
3424 $("#tabletopPreview").empty();
3425
3426 var importContainer = $("<div class='import'></div>").appendTo("#tabletopPreview");
3427 $("<label>Target height: </label>" ).appendTo(importContainer);
3428 var targetHeight = $("<input class='targetHeight' class='short' value='160'>" ).appendTo(importContainer);
3429 $("<button>Import all selected cards as a single new deck</button>").appendTo(importContainer).on("click", function() {
3430 var cards = $("#tabletopPreview .cardSelected.cardPreview");
3431 if(cards.length)
3432 tabletop.importAsNewDeck(cards, +targetHeight.val());
3433 });
3434 $("<input type='button' value='Toggle all'>").appendTo(importContainer).on("click", function() {
3435 $("#tabletopPreview .cardPreview").click();
3436 });
3437
3438 $("#tabletopPreview").append("<h3>" + json.SaveName + "</h3>");
3439
3440 var target = $("<div id='tabletopMain'></div>").appendTo("#tabletopPreview");
3441 tabletop.showJSONbutton(json, target);
3442 if(json.Table == "Table_Custom")
3443 tabletop.addImage(json.TableURL, "Table_Custom", target);
3444 tabletop.listObjects(json["ObjectStates"], target);
3445 $("#tabletopPreview").get(0).scrollIntoView();
3446 },
3447 showImport: function(file) {
3448 if(file.type == "application/json" || file.type == "") {
3449 const reader = new FileReader();
3450 reader.addEventListener('load', (event) => tabletop.readJSON(JSON.parse(event.target.result.replace(/\+Infinity/g, 0))));
3451 reader.readAsText(file);
3452 } else if(file.type == "application/zip") {
3453 JSZip.loadAsync(file).then(function(zip) {
3454 for(var file in zip.files)
3455 if(file.match(/\.json$/))
3456 return zip.files[file].async("blob");
3457 }).then(function(content) {
3458 tabletop.showImport(new File([content], "tabletop.json"));
3459 }, function (e) {
3460 console.log("error loading zip file", e);
3461 });
3462 } else {
3463 console.log(file);
3464 }
3465 },
3466 showJSONbutton: function(json, target) {
3467 $("<button>Show JSON</button>").appendTo(target).on("click", function() {
3468 $(this).parent().removeClass("empty");
3469 $(this).replaceWith("<textarea>" + JSON.stringify(json, null, " ") + "</textarea>");
3470 });
3471 }
3472 };
3473
3474
3475 function addWidget(w, offsetX, offsetY) {
3476 w.id = w.id || uniqueID(w.type);
3477 if(typeof w.x === "undefined")
3478 w.x = 200 + (offsetX || 0);
3479 if(typeof w.y === "undefined")
3480 w.y = 200 + (offsetY || 0);
3481 if(typeof w.z === "undefined")
3482 w.z = Math.max(...widgets.map((wi)=>wi.z || 0).concat(0))+1;
3483 widgets.push(w);
3484 return w.id;
3485 }
3486
3487 function addCardDeck(o) {
3488 o = Object.assign({
3489 pathPrefix: "/img/cards",
3490 backImage: "/img/cardback-red.svg",
3491 jokerColors: [ "Black", "Red", "Blue" ],
3492 addJokers: false,
3493 suits: [ "Diamonds", "Hearts", "Spades", "Clubs" ],
3494 numbers: [ "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K" ],
3495 cardsToAdd: [ "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K" ],
3496 addPile: true
3497 }, o || {});
3498
3499 var pileID = o.addPile ? addWidget({"type":"cardPile","hasShuffleButton":true}) : null;
3500 var types = {};
3501 var cards = [];
3502 o.jokerColors.forEach(function(c) {
3503 types["joker-" + c.toLowerCase()] = {"label":c+" Joker","image":o.pathPrefix+"/joker-"+c.toLowerCase()+".svg"};
3504 if(o.addJokers)
3505 cards.push({"parent":pileID,"type":"card","cardType":"joker-" + c.toLowerCase(),"faceup":false});
3506 });
3507 o.suits.forEach(function(s) {
3508 o.numbers.forEach(function(n) {
3509 types[s.toLowerCase() + "-" + n.toLowerCase()] = {"label":n+" of "+s,"image":o.pathPrefix+"/"+s.toLowerCase()+"-"+n.toLowerCase()+".svg"};
3510 if(o.cardsToAdd.indexOf(n) > -1)
3511 cards.push({"parent":pileID,"type":"card","cardType":s.toLowerCase() + "-" + n.toLowerCase(),"faceup":false});
3512 });
3513 });
3514 var deckID = addWidget({
3515 "type":"cardDeck",
3516 "parent":pileID,
3517 "cardTypes":types,
3518 "faceTemplate":{
3519 "includeBorder":true,
3520 "includeRadius":true,
3521 "objects":[{"type":"image","x":0,"y":0,"w":103,"h":160,"color":"white","valueType":"dynamic","value":"image"}]
3522 },
3523 "backTemplate":{
3524 "includeBorder":false,
3525 "includeRadius":true,
3526 "objects":[{"type":"image","x":0,"y":0,"w":103,"h":160,"color":"#a23b2a","valueType":"static","value":o.backImage}]
3527 }
3528 }, 13, 44);
3529 cards.forEach(function(c) {
3530 c.deck = deckID;
3531 addWidget(c, 4, 4);
3532 });
3533 drawWidgets(widgets);
3534 }
3535
3536 $("#WCardHolder").on("click", function() {
3537 addWidget({
3538 type: "cardPile"
3539 });
3540 drawWidgets(widgets);
3541 });
3542
3543 $(".WPiece").on("click", function() {
3544 addWidget({
3545 type: "gamePiece",
3546 pieceType: $(this).data("type"),
3547 color: "red"
3548 });
3549 drawWidgets(widgets);
3550 });
3551
3552 $("#WAutomationButton").on("click", function() {
3553 addWidget({
3554 type: "automationButton",
3555 label: "DEAL",
3556 clickRoutine: [
3557 {
3558 "func": "MOVE_CARDS_BETWEEN_HOLDERS",
3559 "args": {
3560 "from": {
3561 "type": "literal",
3562 "value": []
3563 },
3564 "to": {
3565 "type": "literal",
3566 "value": []
3567 },
3568 "quantity": {
3569 "type": "literal",
3570 "value": 1
3571 }
3572 }
3573 }
3574 ]
3575 });
3576 drawWidgets(widgets);
3577 });
3578
3579 $("#WCounter").on("click", function() {
3580 addWidget({
3581 type: "counter",
3582 counterValue: 0
3583 });
3584 drawWidgets(widgets);
3585 });
3586
3587 $("#WSpinner").on("click", function() {
3588 addWidget({
3589 type: "spinner",
3590 options: [ 1, 2, 3, 4, 5, 6 ]
3591 });
3592 drawWidgets(widgets);
3593 });
3594
3595 $("#WBoardCustom").on("click", function() {
3596 addWidget({
3597 x: 300,
3598 y: 0,
3599 type: "board",
3600 width: 1000,
3601 height: 1000
3602 });
3603 drawWidgets(widgets);
3604 });
3605
3606 $("#WBoardBackgammon").on("click", function() {
3607 addWidget({
3608 x: 205,
3609 y: 0,
3610 type: "board",
3611 boardImage: "/img/boards/backgammon.svg",
3612 width: 1190,
3613 height: 1000
3614 });
3615 drawWidgets(widgets);
3616 });
3617
3618 $("#WBoardChess").on("click", function() {
3619 addWidget({
3620 x: 300,
3621 y: 0,
3622 type: "board",
3623 boardImage: "/img/boards/chess.svg",
3624 height: 1000,
3625 width: 1000
3626 });
3627 drawWidgets(widgets);
3628 });
3629
3630 $("#WBoardCribbage").on("click", function() {
3631 addWidget({
3632 x: 34,
3633 type: "board",
3634 boardImage: "/img/boards/cribbage.svg",
3635 width: 1533,
3636 height: 414
3637 });
3638 drawWidgets(widgets);
3639 });
3640
3641
3642 $("#WdeckStandard").on("click", function() {
3643 addCardDeck({ pathPrefix: "/img/cards-french", cardsToAdd: [ "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K" ] });
3644 });
3645
3646 $("#WdeckExtended").on("click", function() {
3647 addCardDeck({
3648 pathPrefix: "/img/cards-french",
3649 numbers: [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15" ],
3650 cardsToAdd: [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15" ]
3651 });
3652 });
3653
3654 $("#WdeckPiquet").on("click", function() {
3655 addCardDeck({ pathPrefix: "/img/cards-french", cardsToAdd: [ "A", "7", "8", "9", "10", "J", "Q", "K" ] });
3656 });
3657
3658 $("#WdeckEuchre").on("click", function() {
3659 addCardDeck({ pathPrefix: "/img/cards-french", cardsToAdd: [ "A", "9", "10", "J", "Q", "K" ] });
3660 });
3661
3662 $("#WdeckSpanish").on("click", function() {
3663 addCardDeck({
3664 pathPrefix: "/img/cards-spanish",
3665 backImage: "/img/cardback-crosshatch-noborder.svg",
3666 jokerColors: [],
3667 suits: [ 'Clubs', 'Coins', 'Cups', 'Swords' ],
3668 numbers: [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" ],
3669 cardsToAdd: [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" ]
3670 });
3671 });
3672
3673 $("#WdeckCustom").on("click", function() {
3674 addCardDeck({ pathPrefix: "/img/cards-french", addPile: false, cardsToAdd: [] });
3675 });
3676
3677 $("#WMainHand").on("click", function() {
3678 addWidget({
3679 id: "hand",
3680 type: "hand",
3681 x: 50,
3682 y: 820,
3683 z: 1,
3684 dragging: null
3685 });
3686 drawWidgets(widgets);
3687 });
3688
3689 $("#WExtraHand").on("click", function() {
3690 addWidget({
3691 type: "hand",
3692 x: 50,
3693 y: 820,
3694 dragging: null
3695 });
3696 drawWidgets(widgets);
3697 });
3698
3699 $("#WGhettoSpinner").on("click", function() {
3700 var options=[1,2,3,4,5,6];
3701 var input = prompt("Enter a number or options separated by commas:", 6);
3702 var a="";
3703 try {a= JSON.parse(input)}catch(err) {};
3704 if (typeof(a)=="number"){
3705 var options=Array.from({length: input}, (_, i) => i + 1);
3706 }else if (input.split(",").length!=1){options=JSON.parse("["+input+"]")}
3707 else if(input == null || input == "") {
3708 var options=[1,2,3,4,5,6];
3709 };
3710 addWidget({
3711 type: "spinner",
3712 options: options
3713 });
3714 drawWidgets(widgets);
3715 });
3716
3717 $("#WdeckStandardOld").on("click", function() {
3718 addCardDeck({ jokerColors: [ "Black", "Red" ] });
3719 });
3720
3721 $("#WdeckPiquetOld").on("click", function() {
3722 addCardDeck({ jokerColors: [ "Black", "Red" ], cardsToAdd: [ "A", "7", "8", "9", "10", "J", "Q", "K" ] });
3723 });
3724
3725 $("#WdeckEuchreOld").on("click", function() {
3726 addCardDeck({ jokerColors: [ "Black", "Red" ], cardsToAdd: [ "A", "9", "10", "J", "Q", "K" ] });
3727 });
3728
3729 $("#WdeckCustomOld").on("click", function() {
3730 addCardDeck({ jokerColors: [ "Black", "Red" ], addPile: false, cardsToAdd: [] });
3731 });
3732
3733 $(".WColorPiece").on("click", function() {
3734 addWidget({
3735 type: "gamePiece",
3736 pieceType: $(this).val().toLowerCase(),
3737 color: $("#GPColor").val().toLowerCase()
3738 });
3739 drawWidgets(widgets);
3740 });
3741
3742 $(".WChess").on("click", function() {
3743 var type = $(this).val().toLowerCase();
3744 addWidget({
3745 type: "gamePiece",
3746 pieceType: type,
3747 color: $("#ChessColor").val().toLowerCase(),
3748 width: defaultSizes.gamePiece[type][0],
3749 height: defaultSizes.gamePiece[type][1],
3750 dragging: null
3751 });
3752 drawWidgets(widgets);
3753 });
3754
3755 $("#WBoardFull").on("click", function() {
3756 $("#fileCustomBoard").click();
3757 });
3758
3759 $("#fileCustomBoard").on("change", function(evt) {
3760 const reader = new FileReader();
3761
3762 reader.addEventListener("load", function () {
3763 var name = uniqueID("gpe-board");
3764 imageCache["userassets/" + name] = reader.result.replace(/data.*?,/, '');
3765 addWidget({
3766 x: 0,
3767 y: 0,
3768 type: "board",
3769 boardImage: "package://userassets/" + name,
3770 width: 1600,
3771 height: 1000
3772 });
3773 drawWidgets(widgets);
3774 }, false);
3775
3776 if(evt.target.files[0])
3777 reader.readAsDataURL(evt.target.files[0]);
3778 });
3779
3780</script>
3781