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