Guest User

Ghetto PCIO Editor v12

a guest
Sep 28th, 2020
335
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 5 89.08 KB | None | 0 0
  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/[email protected],npm/jszip@3/dist/jszip.min.js,npm/jquery@3"></script>
  6. <script src="https://unpkg.com/[email protected]/browser/lib/jimp.js"></script>
  7. <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  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 v12</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. <!--<body class="screenshot">-->
  43. <div id="toolbar"></div>
  44. <div id="tools">
  45.   <input type="file" id="file">
  46.   <input type="file" id="fileStripped">
  47.   <input type="file" id="fileImportWidgets">
  48.   <div id="tabletopImporter">
  49.     <h2>Tabletop Simulator importer</h2>
  50.     <ol>
  51.       <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>
  52.       <li>Search for a mod in <a href="https://steamcommunity.com/app/286160/workshop/">the steam workshop</a>.</li>
  53.       <li>Use <a href="https://steamworkshopdownloader.io/">the steam workshop downloader</a> to download it.</li>
  54.       <li>For now extract the JSON file from the archive.</li>
  55.       <li>Select the file here: <input type="file" id="fileImportTabletop"></li>
  56.       <li>Wait. Seriously.</li>
  57.     </ol>
  58.     <div id="tabletopPreview">
  59.     </div>
  60.   </div>
  61.   <div id="typeFilter">
  62.     <h2>Filter widget types</h2>
  63.     <input type="checkbox" id="automationButton" checked><label for="automationButton" class="type-automationButton"> Automation Buttons</label><br>
  64.     <input type="checkbox" id="board" checked><label for="board" class="type-board"> Boards</label><br>
  65.     <input type="checkbox" id="card"><label for="card" class="type-card"> Cards</label><br>
  66.     <input type="checkbox" id="cardDeck" checked><label for="cardDeck" class="type-cardDeck"> Card Decks</label><br>
  67.     <input type="checkbox" id="cardPile" checked><label for="cardPile" class="type-cardPile"> Card Piles</label><br>
  68.     <input type="checkbox" id="counter" checked><label for="counter" class="type-counter"> Counters</label><br>
  69.     <input type="checkbox" id="gamePiece" checked><label for="gamePiece" class="type-gamePiece"> Game Pieces</label><br>
  70.     <input type="checkbox" id="hand" checked><label for="hand" class="type-hand"> Hands</label><br>
  71.     <input type="checkbox" id="spinner" checked><label for="spinner" class="type-spinner"> Spinners</label><br>
  72.     <input type="checkbox" id="label" checked><label for="label" class="type-label"> Label</label>
  73.   </div>
  74.   <div id="coordinates">
  75.     <h2>Direct coordinate editing</h2>
  76.     <p>"Set" sets the entered value to all selected widgets.</p>
  77.     <p>"Offset" adds the entered value to each selected widgets (use negative numbers to offset left/up).</p>
  78.     <p>"Spacing" makes the leftmost/topmost widget stay where it is and moves the rest so the spacing between the widgets equals the entered value.</p>
  79.     <p>You can also use formulas like "12+52".</p>
  80.     X: <input id="x"><input type="button" value="Set" class="set" data-target="x"/><input type="button" value="Offset" class="offset" data-target="x"/><input type="button" value="Spacing" class="spacing" data-target="x"/><br>
  81.     Y: <input id="y"><input type="button" value="Set" class="set" data-target="y"/><input type="button" value="Offset" class="offset" data-target="y"/><input type="button" value="Spacing" class="spacing" data-target="y"/><br>
  82.     <input type="button" value="Set both" id="setboth"/><input type="button" value="Offset both" id="offsetboth"/>
  83.   </div>
  84.   <div id="jsonEdit">
  85.     <h2>Direct JSON editing</h2>
  86.     <p>(only visible if exactly one widget is selected)</p>
  87.     <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>
  88.     <p><input type="checkbox" id="compactAB"><label for="compactAB"> Compact Automation Button clickRoutine (only MOVE and SHUFFLE right now)</label></p>
  89.     <div class="hideWithMulti">
  90.       <textarea id="json"></textarea><br><input type="button" value="Set" class="set" data-target="json"><br>
  91.       Default size<span id="defaultSize"></span>
  92.     </div>
  93.   </div>
  94.   <div id="macros">
  95.     <h2>Macros</h2>
  96.     Preset: <select></select><br>
  97.     <textarea id="generateDefinition"></textarea><br><input type="button" value="Go">
  98.     <h3>Some notes</h3>
  99.     <ul>
  100.       <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>
  101.       <li>The current representation of <tt>widgets.json</tt> is available as the variable <tt>widgets</tt>.</li>
  102.       <li>A card pile is 8px bigger than the cards in each dimension.</li>
  103.     </ul>
  104.   </div>
  105.   <div id="cardDeckSettings">
  106.     <h2>Card deck settings</h2>
  107.     <input type="checkbox" id="cardOverlapH" checked><label for="cardOverlapH"> Overlap in hand</label><br>
  108.     <input type="checkbox" id="onRemoveFromHand" checked><label for="onRemoveFromHand"> Flip out of hand</label><br>
  109.     <input type="checkbox" id="confirmRecall"><label for="confirmRecall"> Recall confirmation</label><br>
  110.     <input type="checkbox" id="confirmRecallAll" checked><label for="confirmRecallAll"> Recall all confirmation</label><br>
  111.     <input type="checkbox" id="enlarge"><label for="enlarge"> Enlarge</label><br>
  112.     <input type="button" value="Set">
  113.   </div>
  114.   <div id="editImages">
  115.     <h2>Edit images</h2>
  116.     <h3>Compress images</h3>
  117.     <p>Note: all other actions keep the size and convert to PNG. Use compression to turn images back to JPG.</p>
  118.     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"><br>
  119.     Width:   <input id="compressWidth" value="200"><br>
  120.     <input type="button" class="compress" value="Preview">
  121.     <h3>Rotate images</h3>
  122.     Degrees: <input id="rotateDegrees" value="90"><br>
  123.     <input type="checkbox" id="rotateResizeDeck" checked><label for="rotateResizeDeck"> Resize the deck</label><br>
  124.     <input type="button" class="rotate" value="Preview">
  125.     <div id="editPreview">
  126.     </div>
  127.   </div>
  128.   <div id="changelog">
  129.     <h2>Changelog</h2>
  130.     <h3>Updates</h3>
  131.     <p>For updates check <a href="https://www.reddit.com/r/PlayingCardsIO/comments/inuocb/pcio_editor/">my reddit post</a>.</p>
  132.     <!--
  133.      TODO LIST
  134.        aspect ratio resize for boards
  135.        hotkeys for filtering widget types
  136.        hotkeys for moving 1px
  137.        "ruler"
  138.        simpler AB routine for the other ones
  139.        add rotation stuff automatically
  140.  
  141.      KNOWN BUGS
  142.        text gets selected (when selecting from room to outside)
  143.        importer overlaps everything
  144.  
  145.      WISH LIST
  146.        RaphaelAlvez:
  147.          -"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"
  148.          -repository of standard and custom widgets so we can save widgets or little setups and share them
  149.          -svg support in deck preview
  150.          -reset button on deck eddit. (the option don't match the deck's current properties)
  151.          -maybe add another button as recal all?
  152.    -->
  153.     <h3>v12 - 2020-09-28</h3>
  154.     <ul>
  155.       <li>"Compress images" is now "edit images". It can also rotate images now.</li>
  156.       <li>Tabletop Simulator importer: all import buttons should do something now.</li>
  157.       <li>Tabletop Simulator importer: back images work for simple cases.</li>
  158.       <li>Tabletop Simulator importer: custom mesh textures can be imported (probably useless).</li>
  159.       <li>Tabletop Simulator importer: you can set the desired height before importing.</li>
  160.     </ul>
  161.     <h3>v11 - 2020-09-27</h3>
  162.     <ul>
  163.       <li>Integrated Tabletop Simulator importer (still WIP).</li>
  164.       <li>Widgets can now also be resized with the mouse after toggling a button.</li>
  165.       <li>Added hotkeys to buttons.</li>
  166.       <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>
  167.       <li>JSON edit can now show a compressed version of the automation button clickRoutine.</li>
  168.       <li>Added a button that reselects previous widgets.</li>
  169.       <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>
  170.     </ul>
  171.     <h3>v10 - 2020-09-21</h3>
  172.     <ul>
  173.       <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>
  174.       <li>Active tools are now highlighted (and there's a button to close them all).</li>
  175.       <li>The filename can now be changed when saving.</li>
  176.       <li>Added favicon.</li>
  177.     </ul>
  178.     <h3>v9 - 2020-09-16</h3>
  179.     <ul>
  180.       <li>Turn all HTTP links into packaged assets.</li>
  181.       <li>Merge multiple card types with the same image into one.</li>
  182.       <li>Status bar with coordinates and status of some tools.</li>
  183.       <li>Board images can now also be compressed.</li>
  184.       <li>Widgets can now be moved with the mouse after toggling the button.</li>
  185.       <li>Saving works now without loading anything prior.</li>
  186.       <li>More colors for widget types (also in the filter selection).</li>
  187.       <li>Several widget generation presets by <a href="https://www.reddit.com/user/RaphaelAlvez/">u/RaphaelAlvez</a>.</li>
  188.     </ul>
  189.     <h3>v8 - 2020-09-15</h3>
  190.     <ul>
  191.       <li>Procedurally generate new widgets.</li>
  192.     </ul>
  193.     <h3>v7 - 2020-09-14</h3>
  194.     <ul>
  195.       <li>Import widgets from another PCIO file.</li>
  196.     </ul>
  197.     <h3>v6 - 2020-09-14</h3>
  198.     <ul>
  199.       <li>Undo.</li>
  200.     </ul>
  201.     <h3>v5 - 2020-09-13</h3>
  202.     <ul>
  203.       <li>Mass compress card images.</li>
  204.       <li>Remove unused card type variables and unused images.</li>
  205.       <li>Set card deck options.</li>
  206.     </ul>
  207.     <h3>v4 - 2020-09-12</h3>
  208.     <ul>
  209.       <li>Added "stripped" save and import buttons for quickly testing and changing things in the official editor without loading hundreds of card images.</li>
  210.       <li>Mass set X or Y spacing.</li>
  211.       <li>Direct coordinate editing now supports formulas.</li>
  212.       <li>Delete widgets.</li>
  213.       <li>Filter what widget types are being displayed.</li>
  214.       <li>Made the editor layout responsive.</li>
  215.     </ul>
  216.     <h3>v3 - 2020-09-08</h3>
  217.     <ul>
  218.       <li>Merge card decks.</li>
  219.       <li>Cleaned it up a bit.</li>
  220.       <li>Added a few descriptions and links.</li>
  221.     </ul>
  222.     <h3>v2 - 2020-09-07</h3>
  223.     <ul>
  224.       <li>Mass offset X or Y coordinates.</li>
  225.       <li>Alignment buttons.</li>
  226.       <li>Direct JSON editing.</li>
  227.       <li>Added a pretty awkward selection rectangle.</li>
  228.       <li>Board images are now being displayed.</li>
  229.     </ul>
  230.     <h3>v1 - 2020-09-06</h3>
  231.     <ul>
  232.       <li>Loading and saving PCIO files.</li>
  233.       <li>Simple rendering and selecting of widgets.</li>
  234.       <li>Mass set X or Y coordinates.</li>
  235.     </ul>
  236.   </div>
  237.   <div id="mergeCardTypes"> <!-- this has to be last - don't ask! -->
  238.     <h2>Merge card types</h2>
  239.     <input type="button" value="Preview">
  240.     <div id="mergePreview">
  241.     </div>
  242.   </div>
  243. </div>
  244.  
  245. <div id="room"></div>
  246. <div id="statusbar">
  247.   <span>Coordinates: <span id="statusCoordinates">0, 0</span></span>
  248.   <span>Widget: <span id="statusWidget">-</span></span>
  249.   <span>Tool status: <span id="statusTool">-</span></span>
  250. </div>
  251. <div id="selectionDiv"></div>
  252.  
  253. <style>
  254.   body, #mergeCardTypes {
  255.     padding-bottom: calc(8px + 3vw);
  256.   }
  257.   #x, #y {
  258.     width: 50px;
  259.   }
  260.   textarea {
  261.     width: 95%;
  262.     height: 400px;
  263.   }
  264.   #macros select {
  265.     width: 95%;
  266.   }
  267.   #tools {
  268.     margin-right: 1610px;
  269.   }
  270.   #toolbar {
  271.     margin-bottom: 8px;
  272.   }
  273.   #toolbar > span {
  274.     display: none;
  275.   }
  276.   #toolbar span + button {
  277.     margin-left: 16px;
  278.   }
  279.   #room {
  280.     position: absolute;
  281.     top: 48px;
  282.     right: 8px;
  283.     width: 1600px;
  284.     height: 1000px;
  285.     border: 1px solid black;
  286.   }
  287.   #statusbar {
  288.     color: white;
  289.     background: black;
  290.     position: fixed;
  291.     right: 0;
  292.     bottom: 0;
  293.     padding: 4px;
  294.     left: 0;
  295.     font-size: 1vw;
  296.   }
  297.   #statusbar > * {
  298.     display: inline-block;
  299.   }
  300.   #statusbar > span:nth-child(1) {
  301.     width: 15%;
  302.   }
  303.   #statusbar > span:nth-child(2) {
  304.     width: 30%;
  305.   }
  306.   #statusbar > span:nth-child(3) {
  307.     width: 50%;
  308.   }
  309.   #editPreview > h2:nth-of-type(1), #mergePreview > h2:nth-of-type(1), #tabletopPreview > h2:nth-of-type(1) {
  310.     margin-top: 1000px;
  311.   }
  312.   #mergeCardTypes, #tabletopImporter {
  313.     position: absolute;
  314.     left: 20px;
  315.     right: 20px;
  316.   }
  317.   .cardPreview {
  318.     display: inline-block;
  319.     position: relative;
  320.     vertical-align: middle;
  321.     border: 5px solid #ccc;
  322.   }
  323.   .cardPreview > img {
  324.     width: 200px;
  325.   }
  326.   .cardPreview > div {
  327.     position: absolute;
  328.     left: 0;
  329.     right: 0;
  330.     top: 40px;
  331.     font-size: 60px;
  332.     display: inline;
  333.     text-align: center;
  334.     color: white;
  335.     text-shadow: -2px 2px 0 #000, 2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000;
  336.   }
  337.   .cardPreview.cardSelected {
  338.     border: 5px solid red;
  339.   }
  340.   #tabletopPreview div {
  341.     margin-left: 20px;
  342.   }
  343.   #tabletopPreview .empty > h4, #tabletopPreview .empty > button {
  344.     display: inline;
  345.     font-size: 8px;
  346.     padding: 0;
  347.     margin: 0 4px;
  348.     border: 0;
  349.     opacity: 0.5;
  350.   }
  351.   .targetHeight {
  352.     width: 50px;
  353.   }
  354.   #selectionDiv, .hideWithMulti, #tools > * {
  355.     display: none;
  356.   }
  357.   #selectionDiv {
  358.     border: 1px dotted #000;
  359.     position: fixed;
  360.     z-index: -1;
  361.   }
  362.   button.material-icons {
  363.     padding: 1px;
  364.     border: 1px solid black;
  365.     margin: 2px;
  366.     position: relative;
  367.   }
  368.   button.material-icons.activated {
  369.     background: #888;
  370.   }
  371.   .tooltip {
  372.     display: none;
  373.     font-family: sans;
  374.     font-size: 12px;
  375.     position: absolute;
  376.     text-align: left;
  377.     background: #ccc;
  378.     border: 1px solid black;
  379.     padding: 4px;
  380.     top: 31px;
  381.     left: -1px;
  382.     width: 200px;
  383.     white-space: normal;
  384.     z-index: 999999;
  385.   }
  386.   button:nth-child(n+14) .tooltip {
  387.     left: -183px;
  388.   }
  389.   button:hover > .tooltip {
  390.     display: block;
  391.   }
  392.   body.screenshot #toolbar {
  393.     width: 10px;
  394.   }
  395.   body.screenshot #toolbar span + button {
  396.     margin-top: 16px;
  397.     margin-left: 2px;
  398.   }
  399.   body.screenshot .tooltip {
  400.     display: inline-block !important;
  401.     position: unset;
  402.   }
  403.   .widget {
  404.     position: absolute;
  405.     background: black;
  406.     opacity: 0.5;
  407.     background-size: cover;
  408.    }
  409.   .selected.widget {
  410.     opacity: 1;
  411.   }
  412.   .type-board {
  413.     background-color: #ccc;
  414.     z-index: 0 !important;
  415.   }
  416.   .type-automationButton { background: red;    color: white; }
  417.   .type-card             { background: teal;   }
  418.   .type-cardDeck         { background: blue;   color: white; }
  419.   .type-cardPile         { background: black;  color: white; }
  420.   .type-counter          { background: green;  }
  421.   .type-gamePiece        { background: orange; }
  422.   .type-hand             { background: sienna; }
  423.   .type-spinner          { background: purple; color: white; }
  424.   .type-label            { background: cyan;   }
  425.  
  426.  
  427.   .realistic {background :#f0f0f0;}
  428.   .realistic .widget                { opacity: 1; border-width: 0;    }
  429.   .realistic .type-cardDeck         { display: none; }
  430.   .realistic .type-hand.disabled    { display: none; }
  431.  
  432.   .realistic .type-cardPile         { outline: 1px solid black;  outline-offset: -1px; background:white;}
  433.   .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;}
  434.   .realistic .type-label            { outline: 2px solid #d8d8d8;  outline-offset: -2px; background:white; outline-style:outset; text-align: center;}
  435.  
  436.   .realistic .type-counter          { background: url(https://s1.gifyu.com/images/counter.png); }
  437.   .realistic .type-card             { background: url(https://playingcards.io/img/cardback-red.svg) #A23B2A; background-size: contain; background-repeat: no-repeat; background-position: center; }
  438.  
  439.   .realistic .type-spinner          { border-radius: 800px; background:url(https://svgshare.com/i/Pzs.svg;) #e1e1e1; background-size:cover;}
  440.   .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;}
  441.  
  442.   .realistic .type-gamePiece.subtype-pin.color-black  { background: url(https://playingcards.io/img/pieces/pin-black.svg);  }
  443.   .realistic .type-gamePiece.subtype-pin.color-blue   { background: url(https://playingcards.io/img/pieces/pin-blue.svg);   }
  444.   .realistic .type-gamePiece.subtype-pin.color-purple { background: url(https://playingcards.io/img/pieces/pin-purple.svg); }
  445.   .realistic .type-gamePiece.subtype-pin.color-red    { background: url(https://playingcards.io/img/pieces/pin-red.svg);    }
  446.   .realistic .type-gamePiece.subtype-pin.color-yellow { background: url(https://playingcards.io/img/pieces/pin-yellow.svg); }
  447.   .realistic .type-gamePiece.subtype-pin.color-green  { background: url(https://playingcards.io/img/pieces/pin-green.svg);  }
  448.   .realistic .type-gamePiece.subtype-pin.color-orange { background: url(https://playingcards.io/img/pieces/pin-orange.svg); }
  449.  
  450.   .realistic .type-gamePiece.subtype-pin      { width:35.85px !important; height:43.83px !important;}
  451.   .realistic .type-gamePiece.subtype-classic  { background:url(https://svgshare.com/i/PzL.svg); width:90px !important; height:90px !important;}
  452.   .realistic .type-gamePiece.subtype-checkers { border-radius: 800px; width:90px !important; height:90px !important;}
  453.  
  454.  
  455.   .realistic .type-gamePiece.subtype-classic.color-black   { background: url(https://svgshare.com/i/Q1P.svg); }
  456.   .realistic .type-gamePiece.subtype-classic.color-blue    { background: url(https://svgshare.com/i/PzK.svg); }
  457.   .realistic .type-gamePiece.subtype-classic.color-purple  { background: url(https://svgshare.com/i/Q0v.svg); }
  458.   .realistic .type-gamePiece.subtype-classic.color-red     { background: url(https://svgshare.com/i/Pzi.svg); }
  459.   .realistic .type-gamePiece.subtype-classic.color-yellow  { background: url(https://svgshare.com/i/Pzg.svg); }
  460.   .realistic .type-gamePiece.subtype-classic.color-green   { background: url(https://svgshare.com/i/Q0h.svg); }
  461.   .realistic .type-gamePiece.subtype-classic.color-orange  { background: url(https://svgshare.com/i/Q0i.svg); }
  462.  
  463.   .realistic .type-gamePiece.subtype-checkers.color-black  { background: radial-gradient(circle, #4a4a4a 0%, #4a4a4a 44%, #848484 44%, #848484 58%, #4a4a4a 58%); }
  464.   .realistic .type-gamePiece.subtype-checkers.color-blue   { background: radial-gradient(circle, #4c5fea 0%, #4c5fea 44%, #8693f1 44%, #8693f1 58%, #4c5fea 58%); }
  465.   .realistic .type-gamePiece.subtype-checkers.color-purple { background: radial-gradient(circle, #bc5bee 0%, #bc5bee 44%, #d290f4 44%, #d290f4 58%, #bc5bee 58%); }
  466.   .realistic .type-gamePiece.subtype-checkers.color-red    { background: radial-gradient(circle, #e84242 0%, #e84242 44%, #f07f7f 44%, #f07f7f 58%, #e84242 58%); }
  467.   .realistic .type-gamePiece.subtype-checkers.color-yellow { background: radial-gradient(circle, #e0cb0b 0%, #e0cb0b 44%, #eadc59 44%, #eadc59 58%, #e0cb0b 58%); }
  468.   .realistic .type-gamePiece.subtype-checkers.color-green  { background: radial-gradient(circle, #23ca5b 0%, #23ca5b 44%, #6adb90 44%, #6adb90 58%, #23ca5b 58%); }
  469.   .realistic .type-gamePiece.subtype-checkers.color-orange { background: radial-gradient(circle, #e2a633 0%, #e2a633 44%, #ecc375 44%, #ecc375 58%, #e2a633 58%); }
  470.  
  471.  
  472.   @media (max-width: 2048px) {
  473.     #room {
  474.       border: 2px solid black;
  475.       transform: scale(0.5, 0.5);
  476.       transform-origin: top right;
  477.     }
  478.     #editPreview > h2:nth-of-type(1), #mergePreview > h2:nth-of-type(1) {
  479.       margin-top: 500px;
  480.     }
  481.     #tools {
  482.       margin-right: 810px;
  483.     }
  484.     .realistic .type-cardPile { outline: 2px solid black; outline-offset: -2px; }
  485.   }
  486.  
  487.   @media (max-width: 1260px) {
  488.     button.material-icons {
  489.       font-size: 12px;
  490.     }
  491.     #room {
  492.       top: 36px;
  493.     }
  494.     #toolbar span + button {
  495.       margin-left: 6px;
  496.     }
  497.     .tooltip {
  498.       top: 19px;
  499.     }
  500.     button:nth-child(n+14) .tooltip {
  501.       left: -195px;
  502.     }
  503.   }
  504.  
  505.   @media (max-width: 1248px) {
  506.     #tools {
  507.       margin-right: 0;
  508.       margin-top: 516px;
  509.     }
  510.     #editPreview > h2:nth-of-type(1), #mergePreview > h2:nth-of-type(1) {
  511.       margin-top: initial;
  512.     }
  513.   }
  514.  
  515.   @media (max-width: 828px) {
  516.     #room {
  517.       border: 4px solid black;
  518.       transform: scale(0.25, 0.25);
  519.       transform-origin: top right;
  520.     }
  521.     #tools {
  522.       margin-top: 266px;
  523.     }
  524.     .realistic .type-cardPile { outline: 4px solid black; outline-offset: -4px; }
  525.   }
  526.  
  527.   @media (max-width: 790px) {
  528.     body, #mergeCardTypes, #tools {
  529.       padding-bottom: calc(8px + 6vw);
  530.     }
  531.     #room {
  532.       top: 8px;
  533.     }
  534.     #toolbar {
  535.       width: 20px;
  536.     }
  537.     #toolbar span + button {
  538.       margin-top: 6px;
  539.       margin-left: 2px;
  540.     }
  541.     .tooltip {
  542.       left: 19px;
  543.       top: -1px;
  544.     }
  545.     button:nth-child(n+14) .tooltip {
  546.       left: 19px;
  547.     }
  548.     #statusbar {
  549.       font-size: 2vw;
  550.     }
  551.     #statusbar > * {
  552.       display: inline-block;
  553.     }
  554.     #statusbar > span:nth-child(1) {
  555.       width: 30%;
  556.     }
  557.     #statusbar > span:nth-child(2) {
  558.       width: 65%;
  559.     }
  560.     #statusbar > span:nth-child(3) {
  561.       width: 95%;
  562.     }
  563.     #tools {
  564.       position: absolute;
  565.       top: 266px;
  566.       left: 32px;
  567.       right: 8px;
  568.       margin-top: 0;
  569.     }
  570.   }
  571.  
  572.   @media (max-width: 460px) {
  573.     #room {
  574.       border: 8px solid black;
  575.       transform: scale(0.125, 0.125);
  576.       transform-origin: top right;
  577.     }
  578.     #tools {
  579.       top: 142px;
  580.     }
  581.     .realistic .type-cardPile { outline: 8px solid black; outline-offset: -8px; }
  582.   }
  583. </style>
  584.  
  585. <script>
  586.   var zipFile = dataURLtoFile("data:application/zip;base64,UEsDBAoAAAAAAGRkMFHZQGDUQgAAAEIAAAAMAAAAd2lkZ2V0cy5qc29uW3siaWQiOiJoYW5kIiwidHlwZSI6ImhhbmQiLCJ4Ijo1MCwieSI6ODIwLCJ6IjoxLCJkcmFnZ2luZyI6bnVsbH1dUEsBAhQACgAAAAAAZGQwUdlAYNRCAAAAQgAAAAwAAAAAAAAAAAAAAAAAAAAAAHdpZGdldHMuanNvblBLBQYAAAAAAQABADoAAABsAAAAAAA=", "unnamed.pcio");
  587.   var widgets = [{"id":"hand","type":"hand","x":50,"y":820,"z":1,"dragging":null}];
  588.  
  589.   var moveWidgets = false;
  590.   var resizeWidgets = false;
  591.   var selectionOrder = 1;
  592.   var previouslySelected = [];
  593.  
  594.   var lastFilename = null;
  595.  
  596.   var imageCache = {};
  597.   var boardImageCache = {};
  598.   var compressedImageCache = {};
  599.   var removeFromZip = {};
  600.  
  601.   var defaultSizes = {
  602.     automationButton: [ 80, 80 ],
  603.     card: [ 103, 160 ],
  604.     cardDeck: [ 86, 86 ],
  605.     cardPile: [ 111, 168 ],
  606.     counter: [ 140, 44 ],
  607.     gamePiece: { checkers: [ 90, 90 ], classic: [ 90, 90 ], pin: [ 35.85, 43.83 ] },
  608.     hand: [ 1500, 180 ],
  609.     spinner: [ 110, 110 ],
  610.     label: [111,18]
  611.   };
  612.   var generateWidgetPresets = {
  613.     "Widgets Creation": {
  614.       "Grid of card piles": `
  615.         number: 10,
  616.         widget: function(i) {
  617.           var x = 20;
  618.           var y = 40;
  619.  
  620.           var dx = 120;
  621.           var dy = 170;
  622.  
  623.           var perRow = 5;
  624.  
  625.           return {
  626.             "id": "generatedPile-" + Math.random(),
  627.             "x": x + (i%perRow)*dx,
  628.             "y": y + Math.floor(i/perRow)*dy,
  629.             "z": 400 + i,
  630.             "type": "cardPile",
  631.             "dragging": null,
  632.             "label": "",
  633.             "hasShuffleButton": false,
  634.             "width": 111,
  635.             "height": 168
  636.           };
  637.         }
  638.       `,
  639.       "Simple Card Pile Creation": `
  640.         number: 1,
  641.         widget: function(i) {
  642.           return {
  643.             "id": "generatedPile-" + Math.random(),
  644.             "x": 20,
  645.             "y": 40,
  646.             "z": 400,
  647.             "type": "cardPile",
  648.             "width": 111,
  649.             "height": 168
  650.           };
  651.         }
  652.       `,
  653.  
  654.       "Spinner with N options":`
  655.         // contributed by u/RaphaelAlvez
  656.  
  657.         number: 1, /* ONLY change this if you want to create a grid */
  658.         widget: function(i) {
  659.           var x = 20;
  660.           var y = 40;
  661.  
  662.           var numberOfOptions = 7;
  663.  
  664.           /* ONLY change this if you want to create a grid */
  665.           var dx = 0;
  666.           var dy = 0;
  667.           var perRow = 1;
  668.  
  669.           return {
  670.             "id": "generatedSpinner-" + (numberOfOptions+Math.random()),
  671.             "x": x + (i%perRow)*dx,
  672.             "y": y + Math.floor(i/perRow)*dy,
  673.             "z": i,
  674.             "type": "spinner",
  675.             "options": Array.from({length: numberOfOptions}, (_, i) => i + 1),
  676.             "rotation": 0,
  677.             "value": "",
  678.             "dragging": null
  679.           };
  680.         }
  681.       `,
  682.       "Normal Game Piece":`
  683.         // contributed by u/RaphaelAlvez
  684.  
  685.         number: 1, /* ONLY change this if you want to create a grid */
  686.         widget: function(i) {
  687.           var x = 20;
  688.           var y = 40;
  689.  
  690.           /* ONLY change this if you want to create a grid */
  691.           var dx = 0;
  692.           var dy = 0;
  693.           var perRow = 1;
  694.  
  695.           return {
  696.             "id": "generatedGamePiece-" + Math.random(),
  697.             "x": x + (i%perRow)*dx,
  698.             "y": y + Math.floor(i/perRow)*dy,
  699.             "z": i,
  700.             "type":"gamePiece",
  701.             "pieceType":"checkers", /* can be "classic", "checkers" or "pin" */
  702.             "color":"red", /* can be "red", "orange", "yellow", "green", "blue", "purple" or "black" */
  703.             "dragging":null,
  704.             "kinged":true /* can be true or false */
  705.           };
  706.         }
  707.       `,
  708.       "Normal Counter":`
  709.         // contributed by u/RaphaelAlvez
  710.  
  711.         number: 1, /* ONLY change this if you want to create a grid */
  712.         widget: function(i) {
  713.           var x = 20;
  714.           var y = 40;
  715.  
  716.           /* ONLY change this if you want to create a grid */
  717.           var dx = 0;
  718.           var dy = 0;
  719.           var perRow = 1;
  720.  
  721.           return {
  722.             "id": "generatedCounter-" + Math.random(),
  723.             "x": x + (i%perRow)*dx,
  724.             "y": y + Math.floor(i/perRow)*dy,
  725.             "z": i,
  726.             "type":"counter",
  727.             "counterValue":0,
  728.             "dragging":null,
  729.             "label":""
  730.           };
  731.         }
  732.       `
  733.     },
  734.     "Widgets Duplication": {
  735.       "Duplicate the selected widgets": `
  736.         number: $(".selected").length,
  737.         widget: function(i) {
  738.           w = widgets[$(".selected").eq(i).data("index")];
  739.           newW = JSON.parse(JSON.stringify(w)); // deep clone
  740.           newW.id = w.id + "duplicatedPile-" + Math.random();
  741.           newW.x += 50;
  742.           newW.y += 50;
  743.           return newW;
  744.         }
  745.       `,
  746.       "Duplicate a selected automation button 5x adding 1 to its label and quantities": `
  747.         number: 5,
  748.         widget: function(i) {
  749.           w = widgets[$(".selected.type-automationButton").eq(0).data("index")];
  750.           newW = JSON.parse(JSON.stringify(w)); // deep clone
  751.           newW.id = w.id + "duplicatedPile-" + Math.random();
  752.           newW.x += 85 * (i+1);
  753.           newW.label = +newW.label + i+1;
  754.           for(var r in newW.clickRoutine)
  755.             if(newW.clickRoutine[r].args.quantity != undefined)
  756.               newW.clickRoutine[r].args.quantity.value += i+1;
  757.           return newW;
  758.         }
  759.       `
  760.     },
  761.     "Automation Button (AB) Routines": {
  762.       "Create Clean AB": `
  763.         // contributed by u/RaphaelAlvez
  764.  
  765.         number: 1, /* ONLY change this if you want to create a grid */
  766.         widget: function(i) {
  767.           var x = 20;
  768.           var y = 40;
  769.  
  770.           /* ONLY change this if you want to create a grid */
  771.           var dx = 0;
  772.           var dy = 0;
  773.           var perRow = 1;
  774.  
  775.           return {
  776.             "id": "generatedButton-" + Math.random(),
  777.             "x": x + (i%perRow)*dx,
  778.             "y": y + Math.floor(i/perRow)*dy,
  779.             "z": i,
  780.             "type": "automationButton",
  781.             "label": "Button "+i,
  782.             "clickRoutine": [],
  783.             "dragging": null
  784.           };
  785.         }
  786.       `,
  787.       "Clean ALL routines of selected ABs": `
  788.         // contributed by u/RaphaelAlvez
  789.  
  790.         number: $(".selected").length,
  791.         widget: function(i) {
  792.           w = widgets[$(".selected").eq(i).data("index")];
  793.           if (w.clickRoutine!=null){w.clickRoutine=[]};
  794.         }
  795.       `,
  796.       "Move Routine": `
  797.         // contributed by u/RaphaelAlvez
  798.  
  799.         number: 1,
  800.         widget: function(i) {
  801.  
  802.           /* 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 */
  803.  
  804.           /* Choose here the number of cards that should be moved */
  805.           var Quantity=1 ;
  806.           var selected=$(".selected").get().sort((a, b) => $(a).data("selectionOrder") - $(b).data("selectionOrder"));
  807.  
  808.           /* finds the AB */
  809.           var abIndex = selected.map((o) => widgets[$(o).data("index")].type).indexOf("automationButton");
  810.  
  811.           /* gets the ids of the widgets selected before the AB */
  812.           From=selected.slice(0, abIndex).map((o)=>widgets[$(o).data("index")].id);
  813.  
  814.           /* gets the ids of the widgets selected after the AB */
  815.           To=selected.slice(abIndex+1).map((o)=>widgets[$(o).data("index")].id);
  816.  
  817.           /* Checks if you did something wrong */
  818.           if(abIndex==-1||From.length==0||To.length==0){return};
  819.  
  820.           /* Add routine */
  821.           widgets[$(selected[abIndex]).data("index")].clickRoutine.push({
  822.             "func": "MOVE_CARDS_BETWEEN_HOLDERS",
  823.             "args": {
  824.               "from": {
  825.                 "type": "literal",
  826.                 "value": From
  827.               },
  828.               "to": {
  829.                 "type": "literal",
  830.                 "value": To
  831.               },
  832.               "quantity": {
  833.                 "type": "literal",
  834.                 "value": Quantity
  835.               } ,
  836.               "moveFlip": {
  837.                 "type": "literal",
  838.                 "value": "none" /* Can be "none","faceUp" or "faceDown" */
  839.               }
  840.             }
  841.           });
  842.         }
  843.       `,
  844.       "Shuffle Routine": `
  845.         // contributed by u/RaphaelAlvez
  846.         /*Add Shuffle to selected piles from selected AB*/
  847.  
  848.         number: 1,
  849.         widget: function(i) {
  850.           shuffle=[];
  851.           for (var n = 1; n <= $(".selected").length; n++) {
  852.            a=widgets[$(".selected").eq(n-1).data("index")];
  853.            if(a.type=="cardPile"){
  854.              shuffle.push(a.id)}
  855.            else{if (a.type="automationButton"){
  856.                b=a}
  857.            }
  858.          };
  859.          b.clickRoutine.push({
  860.            "func": "SHUFFLE_CARDS",
  861.            "args": {
  862.              "holders": {
  863.                "type": "literal",
  864.                "value": shuffle
  865.              }
  866.            }
  867.          });
  868.          return ;
  869.        }
  870.      `,
  871.      "Flip Routines": `
  872.        // contributed by u/RaphaelAlvez
  873.        /*Flip cards of selected piles from selected AB*/
  874.  
  875.        number: 1,
  876.        widget: function(i) {
  877.          Flip=[];
  878.          for (var n = 1; n <= $(".selected").length; n++) {
  879.            a=widgets[$(".selected").eq(n-1).data("index")];
  880.            if(a.type=="cardPile"){
  881.              Flip.push(a.id)}
  882.            else{if (a.type="automationButton"){
  883.                b=a}
  884.            }
  885.          };
  886.          b.clickRoutine.push({
  887.            "func": "FLIP_CARDS",
  888.            "args": {
  889.              "flipMode": {
  890.                "type": "literal",
  891.                "value": "pile" /* can be "pile" or "top" */
  892.              },
  893.              "holders": {
  894.                "type": "literal",
  895.                "value": Flip
  896.              }
  897.            }
  898.          });
  899.          return ;
  900.        }
  901.      `,
  902.      "Set Flip Routines": `
  903.        // contributed by u/RaphaelAlvez
  904.        /* Set flip of cards of selected piles from selected AB */
  905.  
  906.        number: 1,
  907.        widget: function(i) {
  908.          FlipSet=[];
  909.          for (var n = 1; n <= $(".selected").length; n++) {
  910.            a=widgets[$(".selected").eq(n-1).data("index")];
  911.            if(a.type=="cardPile"){
  912.              FlipSet.push(a.id)}
  913.            else{if (a.type="automationButton"){
  914.                b=a}
  915.            }
  916.          };
  917.          b.clickRoutine.push({
  918.            "func": "FLIP_CARDS",
  919.            "args": {
  920.              "flipFace": {
  921.                "type": "literal",
  922.                "value": "faceUp" /* can be "faceUp" or "faceDown" */
  923.              },
  924.              "flipMode": {
  925.                "type": "literal",
  926.                "value": "pile" /* can be "pile" or "top" */
  927.              },
  928.              "holders": {
  929.                "type": "literal",
  930.                "value": FlipSet
  931.              }
  932.            }
  933.          });
  934.          return ;
  935.        }
  936.      `,
  937.      "Counters Routines": `
  938.        // contributed by u/RaphaelAlvez
  939.        /* Add automation to selected counters from selected AB */
  940.  
  941.        number: 1,
  942.        widget: function(i) {
  943.          Counters=[];
  944.          for (var n = 1; n <= $(".selected").length; n++) {
  945.            a=widgets[$(".selected").eq(n-1).data("index")];
  946.            if(a.type=="counter"){
  947.              Counters.push(a.id)}
  948.            else{if (a.type="automationButton"){
  949.                b=a}
  950.            }
  951.          };
  952.          b.clickRoutine.push({
  953.            "func": "CHANGE_COUNTER",
  954.            "args": {
  955.              "counters": {
  956.                "type": "literal",
  957.                "value": [
  958.                  "614d416e-050f-440a-a91a-0d5b285b2d7c"
  959.                ]
  960.              },
  961.              "changeMode": {
  962.                "type": "literal",
  963.                "value": "set" /* can be "set", "inc" or "dec"*/
  964.              },
  965.              "changeNumber": {
  966.                "type": "literal",
  967.                "value": 10
  968.              }
  969.            }
  970.          });
  971.          return ;
  972.        }
  973.      `
  974.    },
  975.    "Other": {
  976.      "Smart Rename":`
  977.        // contributed by u/RaphaelAlvez
  978.  
  979.        /* Changes the Id of the seleted Widget (just one) and makes the same change wherever the selected widget is cited */
  980.  
  981.        number: $(".selected").length,
  982.        widget: function(i) {
  983.  
  984.          var newId ='"'+prompt("New ID?", widgets[$(".selected").eq(i).data("index")].id)+'"'
  985.  
  986.          widgets=JSON.parse(replaceall(JSON.stringify(widgets[$(".selected").eq(i).data("index")].id), newId, JSON.stringify(widgets)));
  987.        }
  988.      `,
  989.      "Reset height and width":`
  990.        // contributed by u/RaphaelAlvez
  991.  
  992.        number: $(".selected").length,
  993.        widget: function(i) {
  994.          w = widgets[$(".selected").eq(i).data("index")];
  995.          if (w.type=="gamePiece"){[w.width,w.height]=defaultSizes["gamePiece"][w.pieceType]}
  996.          else {[w.width,w.height]=defaultSizes[w.type]}
  997.        }
  998.      `,
  999.      "Empty for tests": `
  1000.        // contributed by u/RaphaelAlvez
  1001.  
  1002.        number: 1,
  1003.        widget: function(i) {
  1004.          return ;
  1005.        }
  1006.      `
  1007.    }
  1008.  };
  1009.  var buttons = {
  1010.    folder_open: [ "open a PCIO file", "o", "ONE_SHOT", function() {
  1011.      $("#file").click();
  1012.    } ],
  1013.    exit_to_app: [ "add all widgets from another PCIO file<br><br>delete everything else from it first", "O", "ONE_SHOT", function() {
  1014.       $("#fileImportWidgets").click();
  1015.     } ],
  1016.     casino: [ "import Tabletop Simulator mod", "t", "TOGGLE_TOOL", "#tabletopImporter" ],
  1017.     save_alt: [ "save as PCIO file (triggers browser download)<br><br>this takes like 30s for a 50MiB file", "s", "ONE_SHOT", function() {
  1018.       lastFilename = prompt("Name?", lastFilename || zipFile.name);
  1019.       if(!lastFilename)
  1020.         return;
  1021.  
  1022.       while (widgets.map(function(item) { return item.type; }).indexOf("label")!=-1){widgets.splice(widgets.map(function(item) { return item.type; }).indexOf("label"),1)};
  1023.       JSZip.loadAsync(zipFile).then(function(zip) {
  1024.         zip.file("widgets.json", JSON.stringify(widgets));
  1025.         for(var name in imageCache)
  1026.           if(name != "widgets.json")
  1027.             zip.file(name, imageCache[name], { base64: true });
  1028.         for(var name in removeFromZip)
  1029.           zip.remove(name);
  1030.  
  1031.         zip.generateAsync({type:"base64"}).then(function (base64) {
  1032.           downloadURI("data:application/zip;base64," + base64, lastFilename);
  1033.         }, function (e) {
  1034.           console.log("error saving zip file", e);
  1035.         });
  1036.       });
  1037.     } ],
  1038.     sep1: 1,
  1039.     layers_clear: [ "save as stripped PCIO file<br><br>no card images for fast testing of everything else<br><br>don't change the contents of card decks", "1", "ONE_SHOT", function() {
  1040.       lastFilename = prompt("Name?", lastFilename || zipFile.name);
  1041.       if(!lastFilename)
  1042.         return;
  1043.  
  1044.       while (widgets.map(function(item) { return item.type; }).indexOf("label")!=-1){widgets.splice(widgets.map(function(item) { return item.type; }).indexOf("label"),1)};
  1045.       var zip = new JSZip();
  1046.       zip.file("widgets.json", JSON.stringify(widgets));
  1047.       for(var name in boardImageCache)
  1048.         zip.file(name, imageCache[name], { base64: true });
  1049.  
  1050.       zip.generateAsync({type:"base64"}).then(function (base64) {
  1051.         downloadURI("data:application/zip;base64," + base64, lastFilename);
  1052.       }, function (e) {
  1053.         console.log("error saving zip file", e);
  1054.       });
  1055.     } ],
  1056.     assignment_returned: [ "import changes from a re-exported stripped PCIO file", "2", "ONE_SHOT", function() {
  1057.       $("#fileStripped").click();
  1058.     } ],
  1059.     build_circle: [ "start a new custom playingcards.io room", "3", "ONE_SHOT", function() {
  1060.       window.open("https://playingcards.io/room/new/generic", "_blank");
  1061.     } ],
  1062.     brush: [ "open Photopea", "4", "ONE_SHOT", function() {
  1063.       window.open("https://www.photopea.com/", "_blank");
  1064.     } ],
  1065.     sep2: 1,
  1066.     undo: [ "undo<br><br>only widgets.json is supported - changes to images will remain/break", "z", "ONE_SHOT", function() {
  1067.       popUndoStack();
  1068.     } ],
  1069.     local_offer: [ "Set/align labels and recall button<br><br>Works for recall buttons and labels of card piles and counters<br>Only visual aid for a better notion of final board", "l", "ONE_SHOT", function() {
  1070.       while(widgets.map((item)=>item.type).indexOf("label")!=-1) {
  1071.         widgets.splice(widgets.map((item)=>item.type).indexOf("label"),1)
  1072.       }
  1073.       widgets.map((item)=>item.type).forEach(setLabel);
  1074.       drawWidgets(widgets);
  1075.     } ],
  1076.     open_with: [ "move widgets with the mouse instead of selecting them", "m", "ONE_SHOT", function() {
  1077.       if(resizeWidgets)
  1078.         $(".settings_overscan").click();
  1079.       moveWidgets = !moveWidgets;
  1080.       $(".open_with").toggleClass("activated", moveWidgets);
  1081.     } ],
  1082.     settings_overscan: [ "resize widgets with the mouse instead of selecting them", "r", "ONE_SHOT", function() {
  1083.       if(moveWidgets)
  1084.         $(".open_with").click();
  1085.       resizeWidgets = !resizeWidgets;
  1086.       $(".settings_overscan").toggleClass("activated", resizeWidgets);
  1087.     } ],
  1088.     filter_alt: [ "select what widget types to display", "f", "TOGGLE_TOOL", "#typeFilter" ],
  1089.     filter_b_and_w: [ "toggle realistic visualization<br><br>more or less", "R", "ONE_SHOT", function() {
  1090.       $("#room").toggleClass("realistic");
  1091.       $(".filter_b_and_w").toggleClass("activated", $("#room").is(".realistic"));
  1092.       drawWidgets(widgets);
  1093.     } ],
  1094.     notes: [ "edit the JSON representation of a widget directly", "j", "TOGGLE_TOOL", "#jsonEdit" ],
  1095.     memory: [ "macros<br><br>write custom Javascript or use presets as powerful editing tools", "#", "TOGGLE_TOOL", "#macros" ],
  1096.     cloud_download: [ "turn all http links into packaged assets", "c", "ONE_SHOT", function() {
  1097.       var urls = JSON.stringify(widgets).match(/https?:\/\/[^"]+/g);
  1098.       if(urls)
  1099.         $("#statusTool").text("0/" + urls.length + " downloaded");
  1100.       else
  1101.         $("#statusTool").text("no HTTP links found");
  1102.       var downloaded = 0;
  1103.       $.each(urls, function(i, url) {
  1104.         var filename = "userassets/gpe-dl-" + hashFnv32a(url, true);
  1105.         toDataURL(url, function(data) {
  1106.           imageCache[filename] = data.replace(/data.*?,/, '');
  1107.           widgets = JSON.parse(JSON.stringify(widgets).replace(url, "package://" + filename));
  1108.           ++downloaded;
  1109.           $("#statusTool").text(downloaded + "/" + urls.length + " downloaded");
  1110.           drawWidgets(widgets);
  1111.         });
  1112.       });
  1113.     } ],
  1114.     select_all: [ "select the previously selected widgets", "-", "ONE_SHOT", function() {
  1115.       $(".widget").each(function() {
  1116.         $(this).toggleClass("selected", previouslySelected.indexOf(widgets[$(this).data("index")].id) != -1);
  1117.       });
  1118.     } ],
  1119.     delete: [ "delete<br><br>you shouldn't delete anything with children (decks with cards / piles with decks) unless you delete the children too<br><br>it's a good idea to use the button 'remove unused card type variables,[...]' afterwards", "Delete", "ONE_SHOT", function() {
  1120.       pushUndoStack();
  1121.       var selected = $.map($(".selected"), (s) => widgets[$(s).data("index")].id);
  1122.       widgets = widgets.filter((w) => selected.indexOf(w.id) == -1);
  1123.       $(".selected").remove();
  1124.       drawWidgets(widgets);
  1125.     } ],
  1126.     sep3: 1,
  1127.     architecture: [ "manually set or offset the coordinates of the selected widgets", ".", "TOGGLE_TOOL", "#coordinates" ],
  1128.     align_vertical_top: [ "align elements to the highest top edge", "ArrowUp", function(allSelected, i, widget) {
  1129.       widget.y = Math.min.apply(Math, allSelected.map((w) => w.y));
  1130.     } ],
  1131.     align_vertical_center: [ "align element centers to the middle between the highest top edge and lowest bottom edge", "V", function(allSelected, i, widget, wd) {
  1132.       var minY = Math.min.apply(Math, allSelected.map((w) => w.y))
  1133.       var maxY = Math.max.apply(Math, allSelected.map((w) => w.y + w.height))
  1134.       widget.y = minY + (maxY-minY)/2 - wd.height/2;
  1135.     } ],
  1136.     align_vertical_bottom: [ "align elements to the lowest bottom edge", "ArrowDown", function(allSelected, i, widget, wd) {
  1137.       widget.y = Math.max.apply(Math, allSelected.map((w) => w.y + w.height)) - wd.height;
  1138.     } ],
  1139.     vertical_distribute: [ "equalize the spacing between all the elements vertically", "v", function(allSelected, i, widget, wd) {
  1140.       var minY = Math.min.apply(Math, allSelected.map((w) => w.y))
  1141.       var maxY = Math.max.apply(Math, allSelected.map((w) => w.y + w.height))
  1142.       var heights = allSelected.map((w) => w.height).reduce((a, b) => a+b, 0)
  1143.       var spacing = (maxY-minY-heights)/(allSelected.length-1);
  1144.       allSelected.sort((a, b) => a.y > b.y ? 1 : a.y < b.y ? -1 : 0);
  1145.      var before = allSelected.slice(0, allSelected.findIndex((w) => w.id == widget.id));
  1146.       widget.y = minY + before.map((w) => w.height + spacing).reduce((a, b) => a+b, 0);
  1147.     } ],
  1148.     align_horizontal_left: [ "align elements to the leftmost left edge", "ArrowLeft", function(allSelected, i, widget) {
  1149.       widget.x = Math.min.apply(Math, allSelected.map((w) => w.x));
  1150.     } ],
  1151.     align_horizontal_center: [ "align element centers to the middle between the leftmost left edge and rightmost right edge", "H", function(allSelected, i, widget, wd) {
  1152.       var minX = Math.min.apply(Math, allSelected.map((w) => w.x))
  1153.       var maxX = Math.max.apply(Math, allSelected.map((w) => w.x + w.width))
  1154.       widget.x = minX + (maxX-minX)/2 - wd.width/2;
  1155.     } ],
  1156.     align_horizontal_right: [ "align elements to the rightmost right edge", "ArrowRight", function(allSelected, i, widget, wd) {
  1157.       widget.x = Math.max.apply(Math, allSelected.map((w) => w.x + w.width)) - wd.width;
  1158.     } ],
  1159.     horizontal_distribute: [ "equalize the spacing between all the elements horizontally", "h", function(allSelected, i, widget) {
  1160.       var minX = Math.min.apply(Math, allSelected.map((w) => w.x));
  1161.       var maxX = Math.max.apply(Math, allSelected.map((w) => w.x + w.width));
  1162.       var widths = allSelected.map((w) => w.width).reduce((a, b) => a+b, 0);
  1163.       var spacing = (maxX-minX-widths)/(allSelected.length-1);
  1164.       allSelected.sort((a, b) => a.x > b.x ? 1 : a.x < b.x ? -1 : 0);
  1165.      var before = allSelected.slice(0, allSelected.findIndex((w) => w.id == widget.id));
  1166.       widget.x = minX + before.map((w) => w.width + spacing).reduce((a, b) => a+b, 0);
  1167.     } ],
  1168.     sep4: 1,
  1169.     mediation: [ "merge selected card decks into one<br><br>this doesn't do magic<br>they should be similar", "+", function(allSelected, i, widget) {
  1170.       if(i === 0) {
  1171.         for(var x=1; x<allSelected.length; ++x) {
  1172.          Object.assign(widget.cardTypes, allSelected[x].cardTypes);
  1173.          widgets = widgets.filter((v) => v.id != allSelected[x].id);
  1174.           for(var wi in widgets)
  1175.             if(widgets[wi].deck !== undefined)
  1176.               widgets[wi].deck = widgets[wi].deck == allSelected[x].id ? allSelected[0].id : widgets[wi].deck;
  1177.         }
  1178.       }
  1179.     } ],
  1180.     merge_type: [ "manually merge duplicate card types", "5", "TOGGLE_TOOL", "#mergeCardTypes" ],
  1181.     library_add_check: [ "align all card decks and cards with their parent pile", "6", "ONE_SHOT", function() {
  1182.       pushUndoStack();
  1183.       for(var w in widgets) {
  1184.         if(typeof widgets[w].parent === "string") {
  1185.           var p = widgets.filter((v) => v.id == widgets[w].parent)[0];
  1186.           if(widgets[w].type == "cardDeck") {
  1187.             widgets[w].x = p.x + 13;
  1188.             widgets[w].y = p.y + 44;
  1189.           }
  1190.           if(widgets[w].type == "card" && widgets[w].parent) {
  1191.            p = widgets.filter((v) => v.id == widgets[w].parent)[0];
  1192.             widgets[w].x = p.x + 4;
  1193.             widgets[w].y = p.y + 4;
  1194.           }
  1195.         }
  1196.       }
  1197.       drawWidgets(widgets);
  1198.     } ],
  1199.     settings_applications: [ "edit card deck settings", "d", "TOGGLE_TOOL", "#cardDeckSettings" ],
  1200.     photo_size_select_large: [ "edit images", "x", "TOGGLE_TOOL", "#editImages" ],
  1201.     link_off: [ "remove unused card type variables, cards without a deck and unreferenced images", "-", "ONE_SHOT", function() {
  1202.       var keysDeleted = 0;
  1203.       var imagesDeleted = 0;
  1204.       var widgetIDsToRemove = [];
  1205.       for(var w in widgets) {
  1206.         if(widgets[w].type === "cardDeck") {
  1207.           var validVariables = [ "label" ];
  1208.           for(var field in { faceTemplate: 1, backTemplate: 1 }) {
  1209.             validVariables = validVariables.concat(widgets[w][field].objects.map((v) => v.valueType == "dynamic" && v.value).filter((v) => v !== false));
  1210.           }
  1211.           for(var c in widgets[w].cardTypes) {
  1212.             for(var key in widgets[w].cardTypes[c]) {
  1213.               if(validVariables.indexOf(key) == -1) {
  1214.                 ++keysDeleted;
  1215.                 delete widgets[w].cardTypes[c][key];
  1216.               }
  1217.             }
  1218.           }
  1219.         }
  1220.         if(widgets[w].type === "card")
  1221.           if(!widgets.filter((v) => v.id == widgets[w].deck).length)
  1222.             widgetIDsToRemove.push(widgets[w].id);
  1223.       }
  1224.       widgets = widgets.filter((w) => widgetIDsToRemove.indexOf(w.id) == -1);
  1225.       var imagesFound = JSON.stringify(widgets).match(/"package:\/\/[^"]+/g);
  1226.       imagesFound = imagesFound && imagesFound.map((x) => x.substr(11)) || [];
  1227.       $("#statusTool").text("deleted " + widgetIDsToRemove.length + " cards, " + keysDeleted + " variables and 0 images");
  1228.       JSZip.loadAsync(zipFile).then(function(zip) {
  1229.         for(var file in zip.files) {
  1230.           if(file != "widgets.json" && file != "userassets/" && imagesFound.indexOf(file) == -1 && removeFromZip[file] == undefined) {
  1231.            ++imagesDeleted;
  1232.             removeFromZip[file] = true;
  1233.             $("#statusTool").text("deleted " + widgetIDsToRemove.length + " cards, " + keysDeleted + " variables and " + imagesDeleted + " images");
  1234.           }
  1235.         }
  1236.       });
  1237.       drawWidgets(widgets);
  1238.     } ],
  1239.     sep5: 1,
  1240.     chrome_reader_mode: [ "changelog", "L", "TOGGLE_TOOL", "#changelog" ],
  1241.     close: [ "close all tools", "Escape", "ONE_SHOT", () => $(".activated").click() ]
  1242.   };
  1243.   for(var b in buttons) {
  1244.     if(b.match(/^sep[0-9]+$/)) {
  1245.       $("<span> | </span>").appendTo("#toolbar");
  1246.       continue;
  1247.     }
  1248.  
  1249.     var toolInfo = "<br><br><i>Hotkey: " + buttons[b][1] + "</i>";
  1250.     if(buttons[b][2] === "TOGGLE_TOOL")
  1251.       toolInfo += "<br><br><i>this button opens a tool</i>";
  1252.  
  1253.     $("<button class='material-icons " + b + "'>" + b + "<span class='tooltip'>" + buttons[b][0] + toolInfo + "</span></button>").on("click", function() {
  1254.       var button = this;
  1255.  
  1256.       if(buttons[button.firstChild.nodeValue][2] === "ONE_SHOT") {
  1257.         buttons[button.firstChild.nodeValue][3]();
  1258.         return;
  1259.       }
  1260.  
  1261.       if(buttons[button.firstChild.nodeValue][2] === "TOGGLE_TOOL") {
  1262.         $(this).toggleClass("activated");
  1263.         $(buttons[button.firstChild.nodeValue][3]).toggle($(this).is(".activated"));
  1264.         return;
  1265.       }
  1266.  
  1267.       var allSelected = [];
  1268.       $(".selected").each(function() {
  1269.         allSelected.push(addDimensions(widgets[$(this).data("index")]));
  1270.       });
  1271.       if(allSelected.length > 0)
  1272.         pushUndoStack();
  1273.       var i = 0;
  1274.       $(".selected").each(function() {
  1275.         buttons[button.firstChild.nodeValue][2](allSelected, i++, widgets[$(this).data("index")], addDimensions(widgets[$(this).data("index")]));
  1276.       });
  1277.       drawWidgets(widgets);
  1278.     }).appendTo("#toolbar");
  1279.   }
  1280.   $("#file").on("change", function(evt) {
  1281.     clearUndoStack();
  1282.     zipFile = evt.target.files[0];
  1283.     imageCache = {};
  1284.     boardImageCache = {};
  1285.     compressedImageCache = {};
  1286.     removeFromZip = {};
  1287.     $("#editPreview").empty();
  1288.     JSZip.loadAsync(zipFile).then(function(zip) {
  1289.       return zip.files["widgets.json"].async("text");
  1290.     }).then(function(content) {
  1291.       widgets = JSON.parse(content);
  1292.       $(".selected").removeClass("selected");
  1293.       drawWidgets(widgets);
  1294.     }, function (e) {
  1295.       console.log("error loading zip file", e);
  1296.     });
  1297.   });
  1298.   $("#fileImportWidgets").on("change", function(evt) {
  1299.     pushUndoStack();
  1300.     loadAllImages(evt.target.files[0], function() {
  1301.       JSZip.loadAsync(evt.target.files[0]).then(function(zip) {
  1302.         return zip.files["widgets.json"].async("text");
  1303.       }).then(function(content) {
  1304.         widgets = widgets.concat(JSON.parse(content));
  1305.         drawWidgets(widgets);
  1306.       }, function (e) {
  1307.         console.log("error loading zip file", e);
  1308.       });
  1309.     });
  1310.   });
  1311.   $("#fileImportTabletop").on("change", function(evt) {
  1312.     tabletop.showImport(evt.target.files[0]);
  1313.     $("#tabletopImporter").show();
  1314.   });
  1315.   $("#typeFilter input").on("change", function(evt) {
  1316.     drawWidgets(widgets);
  1317.   });
  1318.   $(function() {
  1319.     for(var header in generateWidgetPresets) {
  1320.       var optGroup = $("<optgroup label='" + header +"'>");
  1321.       for(var name in generateWidgetPresets[header])
  1322.         $("<option data-group='" + header + "'>" + name + "</option>").appendTo(optGroup);
  1323.       optGroup.appendTo($("#macros select"));
  1324.     }
  1325.     $("#macros select").change();
  1326.   });
  1327.   $("#macros select").on("change", function(evt) {
  1328.     $("#macros textarea").val(generateWidgetPresets[$("option:selected", this).data("group")][$(this).val()].replace(/^        /gm, '').trim());
  1329.   });
  1330.   $("#macros [type=button]").on("click", function(evt) {
  1331.     pushUndoStack();
  1332.     eval("obj = { " + $("#macros textarea").val() + "}");
  1333.     for(var i=0; i<(obj.number || 1); ++i) {
  1334.      var newWidget = obj.widget(i);
  1335.      if(typeof newWidget === "object" && newWidget.id !== undefined)
  1336.        widgets.push(newWidget);
  1337.      else
  1338.        $("#statusTool").text("The generated widget was not added. It did not have an ID.");
  1339.    }
  1340.    drawWidgets(widgets);
  1341.  });
  1342.  $("#fileStripped").on("change", function(evt) {
  1343.    pushUndoStack();
  1344.    var tempZipFile = evt.target.files[0];
  1345.    JSZip.loadAsync(tempZipFile).then(function(zip) {
  1346.      return zip.files["widgets.json"].async("text");
  1347.    }).then(function(content) {
  1348.      newWidgets = JSON.parse(content);
  1349.      for(var i in newWidgets) {
  1350.        if(newWidgets[i].type == "board") {
  1351.          var oldWidget = widgets.filter((v) => v.id == newWidgets[i].id)[0];
  1352.           var newName   = newWidgets[i].boardImage.replace("package://", "");
  1353.           var oldName   = oldWidget.boardImage.replace("package://", "");
  1354.           if(newName != oldName) {
  1355.             if(oldWidget.boardImage.match(/^package/)) {
  1356.               delete imageCache[oldName];
  1357.               removeFromZip[oldName] = true;
  1358.             }
  1359.             if(newWidgets[i].boardImage.match(/^package/))
  1360.               JSZip.loadAsync(tempZipFile).then((zip) => zip.files[newName].async("base64").then(function(b) { imageCache[newName] = b; drawWidgets(widgets); }));
  1361.           }
  1362.         }
  1363.       }
  1364.       widgets = newWidgets;
  1365.       drawWidgets(widgets);
  1366.     }, function (e) {
  1367.       console.log("error loading zip file", e);
  1368.     });
  1369.   });
  1370.   $("#room").on("click", ".widget", function() {
  1371.     $(this).toggleClass("selected").data("selectionOrder", selectionOrder++);
  1372.     fillDetails(this);
  1373.   })
  1374.  
  1375.   $(".set").on("click", function() {
  1376.     pushUndoStack();
  1377.     var field = $(this).data("target");
  1378.     $(".selected").each(function() {
  1379.       if(field == "json")
  1380.         widgets[$(this).data("index")] = json_postProcess(JSON.parse($("#" + field).val()));
  1381.       else
  1382.         widgets[$(this).data("index")][field] = +eval($("#" + field).val());
  1383.     });
  1384.     drawWidgets(widgets);
  1385.   });
  1386.  
  1387.   $(".offset").on("click", function() {
  1388.     pushUndoStack();
  1389.     var field = $(this).data("target");
  1390.     $(".selected").each(function() {
  1391.       widgets[$(this).data("index")][field] += +eval($("#" + field).val());
  1392.     });
  1393.     drawWidgets(widgets);
  1394.   });
  1395.  
  1396.   $(".spacing").on("click", function() {
  1397.     pushUndoStack();
  1398.     var allSelected = [];
  1399.     $(".selected").each(function() {
  1400.       allSelected.push(addDimensions(widgets[$(this).data("index")]));
  1401.     });
  1402.  
  1403.     var dir = $(this).data("target");
  1404.     var dim = dir == "x" ? "width" : "height";
  1405.     var minCoord = Math.min.apply(Math, allSelected.map((w) => w[dir]))
  1406.     var spacing = +eval($("#" + dir).val());
  1407.     allSelected.sort((a, b) => a[dir] > b[dir] ? 1 : a[dir] < b[dir] ? -1 : 0);
  1408.  
  1409.    $(".selected").each(function() {
  1410.      var before = allSelected.slice(0, allSelected.findIndex((w) => w.id == widgets[$(this).data("index")].id));
  1411.       widgets[$(this).data("index")][dir] = minCoord + before.map((w) => w[dim] + spacing).reduce((a, b) => a+b, 0);
  1412.     });
  1413.  
  1414.     drawWidgets(widgets);
  1415.   });
  1416.  
  1417.   $("#setboth").on("click", function() {
  1418.     $("#coordinates .set[data-target=x]").click();
  1419.     $(".select_all").click();
  1420.     $("#coordinates .set[data-target=y]").click();
  1421.   });
  1422.  
  1423.   $("#offsetboth").on("click", function() {
  1424.     $("#coordinates .offset[data-target=x]").click();
  1425.     $(".select_all").click();
  1426.     $("#coordinates .offset[data-target=y]").click();
  1427.   });
  1428.  
  1429.   $("#cardDeckSettings [type=button]").on("click", function() {
  1430.     pushUndoStack();
  1431.     $(".selected").each(function() {
  1432.       var widget = widgets[$(this).data("index")];
  1433.  
  1434.       if($("#cardOverlapH").get(0).checked)
  1435.         widget.cardOverlapH = null;
  1436.       else
  1437.         widget.cardOverlapH = 0;
  1438.  
  1439.       if($("#onRemoveFromHand").get(0).checked)
  1440.         delete widget.onRemoveFromHand;
  1441.       else
  1442.         widget.onRemoveFromHand = null;
  1443.  
  1444.       if($("#confirmRecall").get(0).checked)
  1445.         widget.confirmRecall = true;
  1446.       else
  1447.         delete widget.confirmRecall;
  1448.  
  1449.       if($("#confirmRecallAll").get(0).checked)
  1450.         delete widget.confirmRecallAll;
  1451.       else
  1452.         widget.confirmRecallAll = false;
  1453.  
  1454.       if($("#enlarge").get(0).checked)
  1455.         widget.enlarge = true;
  1456.       else
  1457.         delete widget.enlarge;
  1458.     });
  1459.     drawWidgets(widgets);
  1460.   });
  1461.  
  1462.   document.addEventListener('keyup', ({key}) => {
  1463.     //console.log(key);
  1464.     if($("input:focus, textarea:focus").filter(":not([type])").length == 0) {
  1465.       for(var button in buttons) {
  1466.         if(buttons[button] !== 1 && key == buttons[button][1]) {
  1467.          $("." + button).click();
  1468.           return false;
  1469.         }
  1470.       }
  1471.     }
  1472.   }, false);
  1473.  
  1474.   function loadAllImages(zipfileToLoad, callback) {
  1475.     var toLoad = 0;
  1476.     var loaded = 0;
  1477.     JSZip.loadAsync(zipfileToLoad).then(function(zip) {
  1478.       zip.forEach(() => ++toLoad);
  1479.       zip.forEach(function(filename, file) {
  1480.         if(imageCache[filename] === undefined) {
  1481.           file.async("base64").then(function(content) {
  1482.             imageCache[filename] = content;
  1483.             ++loaded;
  1484.             if(loaded == toLoad)
  1485.               callback();
  1486.           });
  1487.         } else {
  1488.           ++loaded;
  1489.           if(loaded == toLoad)
  1490.             callback();
  1491.         }
  1492.       });
  1493.     });
  1494.   }
  1495.  
  1496.   function addResultToEditPreview(dataURL, zipPath, target) {
  1497.     compressedImageCache[zipPath] = dataURL.replace(/data.*?,/, '');
  1498.     target.children("td:eq(2)").append("<img src=\"" + dataURL + "\">");
  1499.     target.children("td:eq(3)").append(Math.round(atob(compressedImageCache[zipPath]).length/1024) + " KiB");
  1500.   }
  1501.  
  1502.   function addImageToEditPreview(width, height, path, $table, button) {
  1503.     if(!path.match(/^package/))
  1504.       return;
  1505.  
  1506.     path = path.replace("package://", "");
  1507.     var $tr = $("<tr><td>Loading...</td><td></td><td></td><td></td></tr>").appendTo($table);
  1508.     loadImage(path, [ $tr, path ], function(image, vars) {
  1509.       var target = vars[0];
  1510.       var zipPath = vars[1];
  1511.  
  1512.       target.children("td:eq(0)").empty().append("<img src=\"data:image/jpeg;base64," + image + "\">");
  1513.       target.children("td:eq(1)").append(Math.round(atob(image).length/1024) + " KiB");
  1514.       if($(button).is(".compress"))
  1515.         compress("data:image/jpeg;base64," + image, width, height, $("#compressType").val(), +$("#compressQuality").val(), (dataURL) => addResultToEditPreview(dataURL, zipPath, target));
  1516.       if($(button).is(".rotate"))
  1517.         rotate("data:image/jpeg;base64," + image, +$("#rotateDegrees").val(), (dataURL) => addResultToEditPreview(dataURL, zipPath, target));
  1518.     });
  1519.   }
  1520.  
  1521.   $("#editImages [type=button]").on("click", function() {
  1522.     var button = this;
  1523.     $("#editPreview").empty().text("Loading...");
  1524.     compressedImageCache = {};
  1525.     loadAllImages(zipFile, function() {
  1526.       $("#editPreview").empty().text("You might have to scroll down for the preview.");
  1527.       $(".selected").each(function() {
  1528.         var widget = widgets[$(this).data("index")];
  1529.         if(widget.type == "board" || widget.type == "cardDeck") {
  1530.           widget = addDimensions(widget);
  1531.           $("#editPreview").append("<h2>" + widget.type + ": " + widget.id + "</h2>");
  1532.           var $table = $("<table><tr><th>Original</th><th>Size</th><th>Resized</th><th>Size</th></tr></table>").appendTo("#editPreview");
  1533.         }
  1534.         if(widget.type == "board") {
  1535.           var width = +$("#compressWidth").val();
  1536.           var height = width*widget.height/widget.width;
  1537.           addImageToEditPreview(width, height, widget.boardImage, $table, button);
  1538.         }
  1539.         if(widget.type == "cardDeck") {
  1540.           var width = +$("#compressWidth").val();
  1541.           var height = width*widget.cardHeight/widget.cardWidth;
  1542.           for(var field in { faceTemplate: 1, backTemplate: 1 })
  1543.             for(var obj in widget[field].objects)
  1544.               if(widget[field].objects[obj].type == "image" && widget[field].objects[obj].valueType == "static")
  1545.                addImageToEditPreview(width, height, widget[field].objects[obj].value, $table, button);
  1546.           for(var t in widget.cardTypes)
  1547.             for(var key in widget.cardTypes[t])
  1548.               addImageToEditPreview(width, height, widget.cardTypes[t][key], $table, button);
  1549.         }
  1550.       });
  1551.       $("<input type=\"button\" value=\"Set\">").appendTo("#editPreview").on("click", () => Object.assign(imageCache, compressedImageCache));
  1552.     });
  1553.   });
  1554.  
  1555.   $("#mergeCardTypes [type=button]").on("click", function() {
  1556.     if($(".selected.type-cardDeck").length == 0) {
  1557.       $("#mergePreview").empty().text("Please select at least one card deck!");
  1558.       return;
  1559.     }
  1560.  
  1561.     $("#mergePreview").empty().text("Loading...");
  1562.     loadAllImages(zipFile, function() {
  1563.       $("#mergePreview").empty().text("You might have to scroll down for the preview.");
  1564.       $(".selected").each(function() {
  1565.         var widget = addDimensions(widgets[$(this).data("index")]);
  1566.         if(widget.type == "cardDeck") {
  1567.           $("#mergePreview").append("<h2>" + widget.type + ": " + widget.id + "</h2>");
  1568.           var $div = $("<div data-widgetid=\"" + widget.id + "\"></div>").appendTo("#mergePreview");
  1569.           for(var t in widget.cardTypes) {
  1570.             for(var key in widget.cardTypes[t]) {
  1571.               if(widget.cardTypes[t][key].match(/^package/)) {
  1572.                 loadImage(widget.cardTypes[t][key].replace("package://", ""), [ $div, t, key, widget ], function(image, vars) {
  1573.                   var count = widgets.filter((w) => w.type == "card" && w.deck == vars[3].id && w.cardType == vars[1]).length;
  1574.                   vars[0].append("<div class='cardPreview'><img src=\"data:image/jpeg;base64," + image + "\" data-type=\"" + vars[1] + "\" data-key=\"" + vars[2] + "\"><div>" + count + "x</div></div>");
  1575.                 });
  1576.                 break;
  1577.               }
  1578.             }
  1579.           }
  1580.         }
  1581.       });
  1582.       $("<input type=\"button\" value=\"Merge to first\">").appendTo("#mergePreview");
  1583.       $("<input type=\"button\" value=\"Merge to last\">").appendTo("#mergePreview");
  1584.     });
  1585.   });
  1586.  
  1587.   $("#tools").on("click", ".cardPreview", function() {
  1588.     $(this).toggleClass("cardSelected");
  1589.   });
  1590.  
  1591.   $("#mergePreview").on("click", "[type=button]", function() {
  1592.     var button = this;
  1593.     $("#mergePreview > div").each(function() {
  1594.       var widget = widgets.filter((w) => w.id == $(this).data("widgetid"))[0];
  1595.       var target = $(".cardSelected > img", this).eq($(button).val() == "Merge to last" ? -1 : 0).data();
  1596.       $(".cardSelected > img", this).each(function() {
  1597.         var d = $(this).data();
  1598.         if(d.type != target.type) {
  1599.           var cards = widgets.filter((w) => w.type == "card" && w.deck == widget.id && w.cardType == d.type);
  1600.           delete widget.cardTypes[d.type];
  1601.           $.each(cards, (_, c) => c.cardType = target.type);
  1602.         }
  1603.       });
  1604.     });
  1605.     $("#mergeCardTypes [type=button]").click();
  1606.   });
  1607.  
  1608.   var undoStack = [];
  1609.   function pushUndoStack() {
  1610.     undoStack.push(JSON.parse(JSON.stringify(widgets)));
  1611.   }
  1612.  
  1613.   function popUndoStack() {
  1614.     if(undoStack.length)
  1615.       widgets = undoStack.pop();
  1616.     drawWidgets(widgets);
  1617.   }
  1618.  
  1619.   function clearUndoStack() {
  1620.     undoStack = [];
  1621.   }
  1622.  
  1623.   function selectByRectangle(x1, y1, x2, y2) {
  1624.     $(".widget").each(function() {
  1625.       var b = $(this).get(0).getBoundingClientRect();
  1626.       $(this).toggleClass("selected", b.left >= x1 && b.left+b.width <= x2 && b.top >= y1 && b.top+b.height <= y2).data("selectionOrder", selectionOrder++);
  1627.       fillDetails(this);
  1628.     });
  1629.   }
  1630.  
  1631.   function addDimensions(widget) {
  1632.     widget = JSON.parse(JSON.stringify(widget));
  1633.     var fields = [ "width", "height" ];
  1634.     if(widget.type == "cardDeck") {
  1635.       var d = widgets.filter((v) => v.id == widget.deck)[0];
  1636.       widget.cardWidth  = widget.cardWidth  || defaultSizes["card"][0];
  1637.       widget.cardHeight = widget.cardHeight || defaultSizes["card"][1];
  1638.     }
  1639.     if(widget.type == "card") {
  1640.       var d = widgets.filter((v) => v.id == widget.deck)[0];
  1641.       if(widget.width == undefined)
  1642.         widget.width  = d && d.cardWidth  || defaultSizes["card"][0];
  1643.       if(widget.height == undefined)
  1644.         widget.height = d && d.cardHeight || defaultSizes["card"][1];
  1645.     }
  1646.     for(var i in fields) {
  1647.       if(widget[fields[i]] === undefined && widget.pieceType !== undefined)
  1648.        widget[fields[i]] = defaultSizes[widget.type][widget.pieceType][i];
  1649.       else if(widget[fields[i]] === undefined)
  1650.         widget[fields[i]] = defaultSizes[widget.type] && defaultSizes[widget.type][i] || 40;
  1651.     }
  1652.     return widget;
  1653.   }
  1654.  
  1655.   function loadImage(image, target, callback) {
  1656.     if(imageCache[image] !== undefined) {
  1657.       callback(imageCache[image], target);
  1658.       return;
  1659.     }
  1660.     JSZip.loadAsync(zipFile).then(function(zip) {
  1661.       return zip.files[image].async("base64");
  1662.     }).then(function(content) {
  1663.       imageCache[image] = content;
  1664.       callback(content, target);
  1665.     }, function (e) {
  1666.       console.log("error loading image from zip file", e);
  1667.     });
  1668.   }
  1669.   function setLabel (item, index){
  1670.     if (item=="cardPile"){if (widgets[index].hasShuffleButton===true){if (widgets[index].height===null){widgets.push({"id":"label of "+widgets[index].id,"type":"label","label":"RECALL & SHUFFLE","x":widgets[index].x,"y":(widgets[index].y+168),"z":widgets[index].z,"width":widgets[index].width,"height":34})}else{widgets.push({"id":"label of "+widgets[index].id,"type":"label","label":"RECALL & SHUFFLE","x":widgets[index].x,"y":(widgets[index].y+widgets[index].height),"z":widgets[index].z,"width":widgets[index].width,"height":34})}}};
  1671.     if (item=="cardPile"||item=="counter"){if (!(widgets[index].label==""||widgets[index].label==null)){widgets.push({"id":"label of "+widgets[index].id,"type":"label","label":widgets[index].label,"x":widgets[index].x,"y":(widgets[index].y-18),"z":widgets[index].z,"width":widgets[index].width||defaultSizes[item][0],"height":defaultSizes["label"][1]})}};
  1672.   }
  1673.  
  1674.   function drawWidgets(ws) {
  1675.     previouslySelected = $.map($(".selected"), (s) => widgets[$(s).data("index")].id);
  1676.     var typesToDisplay = {};
  1677.     $('#typeFilter input').get().map((o) => typesToDisplay[o.id] = o.checked);
  1678.  
  1679.     $(".hideWithMulti").hide();
  1680.     $("#room").empty();
  1681.     for(var i in ws) {
  1682.       if(typesToDisplay[ws[i].type]) {
  1683.         var w = addDimensions(ws[i]);
  1684.         if((w.type == "automationButton"||w.type == "label")&& $("#room").is(".realistic")){
  1685.          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.label+"</div>").appendTo("#room");
  1686.         }else{
  1687.           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 + "'></div>").appendTo("#room");
  1688.         }
  1689.         if(w.type == "hand" && w.enabled === false)
  1690.          $w.addClass("disabled");
  1691.         if(w.type == "gamePiece" && !w.enabled)
  1692.          $w.addClass("subtype-" + w.pieceType).addClass("color-" + w.color);
  1693.         if(w.type == "board" && w.boardImage != undefined) {
  1694.          if(w.boardImage.match(/^package/)) {
  1695.            var imageName = w.boardImage.replace("package://", "");
  1696.             loadImage(imageName, [ $w, imageName ], function(image, target) {
  1697.               target[0].css("background-image", "url(data:image/jpeg;base64," + image + ")");
  1698.               boardImageCache[target[1]] = image;
  1699.             });
  1700.           } else if(w.boardImage.match(/^\//)) {
  1701.             $w.css("background-image", "url(http://playingcards.io" + w.boardImage + ")");
  1702.           } else {
  1703.             $w.css("background-image", "url(" + w.boardImage + ")");
  1704.           }
  1705.         }
  1706.       }
  1707.     }
  1708.   }
  1709.   $(function() { drawWidgets(widgets); });
  1710.  
  1711.   function json_preProcess(w) {
  1712.     if($("#compactAB").prop("checked") && w.type == "automationButton") {
  1713.      for(var i in w.clickRoutine) {
  1714.        if(w.clickRoutine[i].func == "MOVE_CARDS_BETWEEN_HOLDERS") {
  1715.          var moveFlip = w.clickRoutine[i].args.moveFlip && w.clickRoutine[i].args.moveFlip.value;
  1716.           w.clickRoutine[i] = [ "MOVE", w.clickRoutine[i].args.from.value, (w.clickRoutine[i].args.quantity || { value: 1 }).value, w.clickRoutine[i].args.to.value ];
  1717.           if(w.clickRoutine[i][1].length == 1)
  1718.             w.clickRoutine[i][1] = w.clickRoutine[i][1][0];
  1719.           if(w.clickRoutine[i][3].length == 1)
  1720.             w.clickRoutine[i][3] = w.clickRoutine[i][3][0];
  1721.         }
  1722.         if(w.clickRoutine[i].func == "SHUFFLE_CARDS") {
  1723.           w.clickRoutine[i] = [ "SHUFFLE", w.clickRoutine[i].args.holders.value ];
  1724.           if(w.clickRoutine[i][1].length == 1)
  1725.             w.clickRoutine[i][1] = w.clickRoutine[i][1][0];
  1726.         }
  1727.         if(moveFlip && moveFlip != "none")
  1728.          w.clickRoutine[i][4] = moveFlip;
  1729.       }
  1730.     }
  1731.     return w;
  1732.   }
  1733.  
  1734.   function json_postProcess(w) {
  1735.     if($("#compactAB").prop("checked") && w.type == "automationButton") {
  1736.      for(var i in w.clickRoutine) {
  1737.        if(Array.isArray(w.clickRoutine[i]) && w.clickRoutine[i][0] == "MOVE") {
  1738.          if(typeof w.clickRoutine[i][1] === "string")
  1739.            w.clickRoutine[i][1] = [ w.clickRoutine[i][1] ];
  1740.           if(typeof w.clickRoutine[i][3] === "string")
  1741.             w.clickRoutine[i][3] = [ w.clickRoutine[i][3] ];
  1742.           w.clickRoutine[i] = {
  1743.             "func": "MOVE_CARDS_BETWEEN_HOLDERS",
  1744.             "args": {
  1745.               "from": {
  1746.                 "type": "literal",
  1747.                 "value": w.clickRoutine[i][1]
  1748.               },
  1749.               "to": {
  1750.                 "type": "literal",
  1751.                 "value": w.clickRoutine[i][3]
  1752.               },
  1753.               "quantity": {
  1754.                 "type": "literal",
  1755.                 "value": w.clickRoutine[i][2]
  1756.               },
  1757.               "moveFlip": {
  1758.                 "type": "literal",
  1759.                 "value": w.clickRoutine[i][4] || "none"
  1760.               }
  1761.             }
  1762.           };
  1763.         }
  1764.         if(Array.isArray(w.clickRoutine[i]) && w.clickRoutine[i][0] == "SHUFFLE") {
  1765.          if(typeof w.clickRoutine[i][1] === "string")
  1766.            w.clickRoutine[i][1] = [ w.clickRoutine[i][1] ];
  1767.           w.clickRoutine[i] = {
  1768.             "func": "SHUFFLE_CARDS",
  1769.             "args": {
  1770.               "holders": {
  1771.                 "type": "literal",
  1772.                 "value": w.clickRoutine[i][1]
  1773.               }
  1774.             }
  1775.           };
  1776.         }
  1777.       }
  1778.     }
  1779.     return w;
  1780.   }
  1781.  
  1782.   function json_postProcessString(str) {
  1783.     return str.replace(/\[\n +"(MOVE|SHUFFLE)",\n[^\[\]]*(\[[^\[\]]*\][^\[\]]*)*\]/g, function(match) {
  1784.       return match.replace(/\n */g, " ");
  1785.     }).replace(/"options": \[\n[^\[\]]*\]/g, function(match) {
  1786.       return match.replace(/\n */g, " ");
  1787.     });
  1788.   }
  1789.  
  1790.   $("#jsonEdit [type=checkbox]").on("click", () => fillDetails($(".selected").get(0)));
  1791.  
  1792.   function fillDetails(element) {
  1793.     var w = widgets[$(element).data("index")];
  1794.  
  1795.     $(".hideWithMulti").toggle($(".selected").length == 1);
  1796.  
  1797.     if($(".selected").length == 0) {
  1798.       $("#x").val("");
  1799.       $("#y").val("");
  1800.       return;
  1801.     } else if($(".selected").length == 1) {
  1802.       $("#x").val(w.x);
  1803.       $("#y").val(w.y);
  1804.     } else {
  1805.       return;
  1806.     }
  1807.  
  1808.     w = widgets[$(".selected:eq(0)").data("index")];
  1809.     $("#json").val(json_postProcessString(JSON.stringify(json_preProcess(JSON.parse(JSON.stringify(w))), null, "  ")));
  1810.     if(w.pieceType !== undefined)
  1811.       $("#defaultSize").text(" for " + w.type + ": " + defaultSizes[w.type][w.pieceType][0] + "*" + defaultSizes[w.type][w.pieceType][1]);
  1812.     else if(defaultSizes[w.type] !== undefined)
  1813.       $("#defaultSize").text(" for " + w.type + ": " + defaultSizes[w.type][0] + "*" + defaultSizes[w.type][1]);
  1814.     else
  1815.       $("#defaultSize").text(" for " + w.type + " is unavailable");
  1816.   }
  1817.  
  1818.   function downloadURI(uri, name) {
  1819.     // https://stackoverflow.com/a/15832662
  1820.     var link = document.createElement("a");
  1821.     link.download = name;
  1822.     link.href = uri;
  1823.     document.body.appendChild(link);
  1824.     link.click();
  1825.     document.body.removeChild(link);
  1826.     delete link;
  1827.   }
  1828.  
  1829.   // https://github.com/oliver-moran/jimp/blob/master/packages/jimp/README.md
  1830.   function compress(url, width, height, type, compression, callback) {
  1831.     Jimp.read(url).then(image => {
  1832.       return image.contain(width, height, Jimp.RESIZE_BICUBIC).quality(compression*100).getBase64Async(type == "image/jpeg" ? Jimp.MIME_JPEG : Jimp.MIME_PNG);
  1833.     }).then(base64 => callback(base64)).catch(console.log);
  1834.   }
  1835.  
  1836.   function rotate(url, degrees, callback) {
  1837.     Jimp.read(url).then(image => {
  1838.       return image.rotate(-degrees, Jimp.RESIZE_BICUBIC).getBase64Async(Jimp.MIME_PNG);
  1839.     }).then(base64 => callback(base64)).catch(console.log);
  1840.   }
  1841.  
  1842.   function toDataURL(url, callback) {
  1843.     // https://stackoverflow.com/a/20285053
  1844.     var xhr = new XMLHttpRequest();
  1845.     xhr.onload = function() {
  1846.       var reader = new FileReader();
  1847.       reader.onloadend = function() {
  1848.         callback(reader.result);
  1849.       }
  1850.       reader.readAsDataURL(xhr.response);
  1851.     };
  1852.     xhr.open('GET', url);
  1853.     xhr.responseType = 'blob';
  1854.     xhr.send();
  1855.   }
  1856.  
  1857.   function hashFnv32a(str, asString, seed) {
  1858.     // https://stackoverflow.com/a/22429679
  1859.     var i, l,
  1860.     hval = (seed === undefined) ? 0x811c9dc5 : seed;
  1861.  
  1862.     for (i = 0, l = str.length; i < l; i++) {
  1863.      hval ^= str.charCodeAt(i);
  1864.      hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
  1865.    }
  1866.    if( asString ){
  1867.      // Convert to 8 digit hex string
  1868.      return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
  1869.     }
  1870.     return hval >>> 0;
  1871.   }
  1872.  
  1873.   function dataURLtoFile(dataurl, filename) {
  1874.     // https://stackoverflow.com/a/38935990
  1875.     var arr = dataurl.split(','),
  1876.         mime = arr[0].match(/:(.*?);/)[1],
  1877.         bstr = atob(arr[1]),
  1878.         n = bstr.length,
  1879.         u8arr = new Uint8Array(n);
  1880.  
  1881.     while(n--)
  1882.       u8arr[n] = bstr.charCodeAt(n);
  1883.  
  1884.     return new File([u8arr], filename, {type:mime});
  1885.   }
  1886.  
  1887.   window.onmouseover = function(e) {
  1888.     var i = $(e.target).data("index");
  1889.     if(i !== undefined) {
  1890.       var w = addDimensions(widgets[i]);
  1891.       $("#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);
  1892.     } else {
  1893.       $("#statusWidget").text("-");
  1894.     }
  1895.   };
  1896.  
  1897.   $('div').on('dragstart', function(event) { event.preventDefault(); });
  1898.   // https://stackoverflow.com/a/23284608
  1899.   var div = document.getElementById('selectionDiv'), x1 = 0, y1 = 0, x2 = 0, y2 = 0;
  1900.   function reCalc() { //This will restyle the div
  1901.     var x3 = Math.min(x1,x2); //Smaller X
  1902.     var x4 = Math.max(x1,x2); //Larger X
  1903.     var y3 = Math.min(y1,y2); //Smaller Y
  1904.     var y4 = Math.max(y1,y2); //Larger Y
  1905.     div.style.left = x3 + 'px';
  1906.     div.style.top = y3 + 'px';
  1907.     div.style.width = x4 - x3 + 'px';
  1908.     div.style.height = y4 - y3 + 'px';
  1909.   }
  1910.   var $movingWidget = $("#room");
  1911.   $("#room").on("mousedown", function(e) {
  1912.     if(moveWidgets || resizeWidgets) {
  1913.       pushUndoStack();
  1914.       $movingWidget = $(e.target);
  1915.     } else {
  1916.       $(div).show(); //Unhide the div
  1917.     }
  1918.     x1 = e.clientX; //Set the initial X
  1919.     y1 = e.clientY; //Set the initial Y
  1920.     reCalc();
  1921.   });
  1922.   $(document).on("mousemove", function(e) {
  1923.     x2 = e.clientX; //Update the current position X
  1924.     y2 = e.clientY; //Update the current position Y
  1925.  
  1926.     var b = $("#room").get(0).getBoundingClientRect();
  1927.     var x = Math.floor((x2 - b.x) / b.width  * 1600);
  1928.     var y = Math.floor((y2 - b.y) / b.height * 1000);
  1929.     $("#statusCoordinates").text(x + ", " + y);
  1930.     reCalc();
  1931.     if(moveWidgets && $movingWidget.is(".widget")) {
  1932.      $movingWidget.css({ left: x+"px", top: y+"px" });
  1933.       widgets[$movingWidget.data("index")].x = x;
  1934.       widgets[$movingWidget.data("index")].y = y;
  1935.       $("#statusTool").text(x + ", " + y);
  1936.     }
  1937.     if(resizeWidgets && $movingWidget.is(".widget")) {
  1938.      var w = Math.floor(x-$movingWidget.position().left / b.width  * 1600);
  1939.       var h = Math.floor(y-$movingWidget.position().top  / b.height * 1000);
  1940.       $movingWidget.css({ width: w+"px", height: h+"px" });
  1941.       widgets[$movingWidget.data("index")].width = w;
  1942.       widgets[$movingWidget.data("index")].height = h;
  1943.       $("#statusTool").text(w + "x" + h);
  1944.     }
  1945.   }).on("mouseup", function(e) {
  1946.     if(moveWidgets || resizeWidgets) {
  1947.       $movingWidget = $("#room");
  1948.       drawWidgets(widgets);
  1949.     }
  1950.     if(!$(div).is(":hidden")) {
  1951.       $(div).hide(); //Hide the div
  1952.       if(Math.abs(x1-x2) > 2 && Math.abs(y1-y2) > 2)
  1953.        selectByRectangle(Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2));
  1954.     }
  1955.   });
  1956.  
  1957.  
  1958.  
  1959.   tabletop = {
  1960.     images: {},
  1961.     crops: {},
  1962.  
  1963.     addCardDecks: function(o, fieldName, target) {
  1964.       var foundBack = false;
  1965.       var container = tabletop.addFieldHeaderAndContainer(fieldName, target);
  1966.       (o.DeckIDs || [ o.CardID ]).forEach(function(id) {
  1967.         var deckID = Math.floor(id/100);
  1968.         //tabletop.addImage(o.CustomDeck[deckID].FaceURL, "CustomDeck." + deckID + ".FaceURL", container);
  1969.         if(!foundBack) {
  1970.           var backContainer = $("<div class='deckBack'>").appendTo(container);
  1971.           tabletop.addImage(o.CustomDeck[deckID].BackURL, "CustomDeck." + deckID + ".BackURL", backContainer);
  1972.           foundBack = true;
  1973.         }
  1974.         tabletop.cropCard(o.CustomDeck[deckID].FaceURL, o.CustomDeck[deckID].NumHeight || 7, o.CustomDeck[deckID].NumWidth || 10, id%100, 0, function(url) {
  1975.           tabletop.addCardImage(url, "Card Image " + id, container);
  1976.         });
  1977.       });
  1978.       $("<label>Target height: </label><input class='targetHeight' value='160'>" ).appendTo(container);
  1979.       $("<button>Import as new deck</button>" ).appendTo(container).on("click", ()=>tabletop.importAsNewDeck(container) );
  1980.     },
  1981.     addCardImage: function(url, fieldName, target) {
  1982.       $("<div class='cardPreview'><img src='" + url + "'></div>").appendTo(target);
  1983.     },
  1984.     addFieldHeaderAndContainer: function(fieldName, target) {
  1985.       $("<h5>" + fieldName + "</h5>").appendTo(target);
  1986.       return $("<div></div>").appendTo(target);
  1987.     },
  1988.     addImage: function(url, fieldName, target, useParent) {
  1989.       var container = tabletop.addFieldHeaderAndContainer(fieldName, target);
  1990.       $("<div class='cardPreview'><img src='" + url + "'></div>").appendTo(container);
  1991.       $("<label>Target height: </label><input class='targetHeight' value='160'>" ).appendTo(container);
  1992.       $("<button>Import as board</button>").appendTo(container).on("click", ()=>tabletop.importAsBoard(container, url));
  1993.       $("<button>Import as card</button>" ).appendTo(container).on("click", ()=>tabletop.importAsNewDeck(useParent ? container.parent() : container) );
  1994.     },
  1995.     addXYZwidth: function(widget, container) {
  1996.       widget.x = 0;
  1997.       widget.y = 0;
  1998.       widget.z = 100;
  1999.       widget.width = tabletop.getWidth(container, widget.height);
  2000.       return widget;
  2001.     },
  2002.     cropCard: function(url, rows, perRow, i, rotation, callback) {
  2003.       if(!(url in tabletop.images)) {
  2004.         tabletop.crops[url] = {};
  2005.         tabletop.crops[url].meta = { rows: rows, perRow: perRow, rotation: rotation };
  2006.         tabletop.images[url] = new Image();
  2007.         tabletop.images[url].crossOrigin = "anonymous";
  2008.         tabletop.images[url].onload = tabletop.imageLoaded;
  2009.       }
  2010.  
  2011.       if(!(i in tabletop.crops[url]))
  2012.         tabletop.crops[url][i] = [];
  2013.  
  2014.       if(typeof tabletop.crops[url][i] == "string")
  2015.         callback(tabletop.crops[url][i]);
  2016.       else
  2017.         tabletop.crops[url][i].push(callback);
  2018.       //img.src = "https://cors-anywhere.herokuapp.com/" + url;
  2019.       tabletop.images[url].src = url;
  2020.     },
  2021.     getWidth: function(container, height) {
  2022.       return height*$("img", container).width()/$("img", container).height();
  2023.     },
  2024.     imageLoaded: function() {
  2025.       var m = tabletop.crops[this.src].meta;
  2026.       const canvas = document.createElement('canvas');
  2027.       canvas.width = this.width/m.perRow;
  2028.       canvas.height = this.height/m.rows;
  2029.       const ctx = canvas.getContext('2d');
  2030.       //console.log(i, canvas.width, canvas.width*(i%perRow), canvas.height, canvas.height*Math.floor(i/perRow));
  2031.       for(var i in tabletop.crops[this.src]) {
  2032.         if(i != "meta") {
  2033.           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);
  2034.           var dataURL = ctx.canvas.toDataURL("image/jpeg", 0.9);
  2035.           tabletop.crops[this.src][i].forEach(function(c) { c(dataURL); });
  2036.           tabletop.crops[this.src][i] = dataURL;
  2037.         }
  2038.       }
  2039.     },
  2040.     importAsBoard: function(container, url) {
  2041.       pushUndoStack();
  2042.       widgets.push(tabletop.addXYZwidth({
  2043.         id: "ttboard-" + Math.random(),
  2044.         type: "board",
  2045.         height: +$(".targetHeight", container).val(),
  2046.         boardImage: url,
  2047.         dragging: null
  2048.       }, container));
  2049.       drawWidgets(widgets);
  2050.     },
  2051.     importAsNewDeck: function(container) {
  2052.       pushUndoStack();
  2053.       var deck = "ttdeck-" + Math.random();
  2054.       var pile = "ttpile-" + Math.random();
  2055.  
  2056.       if($(container).is(".deckBack > div"))
  2057.         var targetHeight = +$(".targetHeight", container).val();
  2058.       else
  2059.         var targetHeight = +$(".targetHeight:not(.deckBack input)", container).val();
  2060.       var x = 24;
  2061.       var y = 24;
  2062.  
  2063.       var width = 160;
  2064.       var height = 160;
  2065.       var types = {};
  2066.  
  2067.       var back = $(".deckBack img", container);
  2068.  
  2069.       $("img", container).each(function() {
  2070.         if($(this).is(".deckBack img") && !$(container).is(".deckBack > div"))
  2071.          return;
  2072.         width = $(this).width();
  2073.         height = $(this).height();
  2074.  
  2075.         var type = ("tttype-" + Math.random()).replace(/\./, '');
  2076.         var card = "ttcard-" + type;
  2077.         if(this.src.match(/^data/)) {
  2078.           imageCache["userassets/" + type] = this.src.replace(/data.*?,/, '');
  2079.           types[type] = {
  2080.             label: type,
  2081.             image: "package://userassets/" + type
  2082.           };
  2083.         } else {
  2084.           types[type] = {
  2085.             label: type,
  2086.             image: this.src
  2087.           };
  2088.         }
  2089.         widgets.push({
  2090.           id: card,
  2091.           type: "card",
  2092.           cardType: type,
  2093.           deck: deck,
  2094.           parent: pile,
  2095.           x: x+4,
  2096.           y: y+4,
  2097.           z: 543,
  2098.           faceup: false,
  2099.           dragging: null,
  2100.           owner: null
  2101.         });
  2102.       });
  2103.  
  2104.       widgets.push({
  2105.         id: pile,
  2106.         x: 24,
  2107.         y: 24,
  2108.         type: "cardPile",
  2109.         dragging: null,
  2110.         z: 418,
  2111.         label: "",
  2112.         hasShuffleButton: true,
  2113.         width: targetHeight*width/height + 8,
  2114.         height: targetHeight + 8
  2115.       });
  2116.       widgets.push({
  2117.         id: deck,
  2118.         type: "cardDeck",
  2119.         faceTemplate: {
  2120.           includeBorder: false,
  2121.           includeRadius: true,
  2122.           objects: [
  2123.             {
  2124.               type: "image",
  2125.               x: 0,
  2126.               y: 0,
  2127.               w: targetHeight*width/height,
  2128.               h: targetHeight,
  2129.               color: "transparent",
  2130.               valueType: "dynamic",
  2131.               value: "image"
  2132.             }
  2133.           ]
  2134.         },
  2135.         backTemplate: {
  2136.           includeBorder: false,
  2137.           includeRadius: true,
  2138.           objects: [
  2139.             {
  2140.               type: "image",
  2141.               x: 0,
  2142.               y: 0,
  2143.               w: targetHeight*width/height,
  2144.               h: targetHeight,
  2145.               valueType: back.length ? "static" : "dynamic",
  2146.               value: back.length ? back.get(0).src : "image"
  2147.             }
  2148.           ]
  2149.         },
  2150.         cardTypes: types,
  2151.         dragging: null,
  2152.         x: x+13,
  2153.         y: y+44,
  2154.         z: 508,
  2155.         parent: pile,
  2156.         cardWidth: targetHeight*width/height,
  2157.         cardHeight: targetHeight,
  2158.         cardOverlapH: 0,
  2159.         enlarge: true,
  2160.       }, container);
  2161.       drawWidgets(widgets);
  2162.     },
  2163.     listObjects: function(objects, target) {
  2164.       objects.forEach(function(o) {
  2165.         if([ "FogOfWarTrigger" ].indexOf(o.Name) > -1)
  2166.           return;
  2167.  
  2168.         var title = o.Nickname ? o.Name + " (" + o.Nickname + ")" : o.Name;
  2169.         var container = $("<div></div>").appendTo(target);
  2170.         $("<h4>" + title + "</h4>").appendTo(container);
  2171.         tabletop.showJSONbutton(o, container);
  2172.         $("<button>Remove</button>").appendTo(container).on("click", function() { $(this).parent().remove(); });
  2173.  
  2174.         if("CustomImage" in o && o.CustomImage && "ImageURL" in o.CustomImage && o.CustomImage.ImageURL)
  2175.          tabletop.addImage(o.CustomImage.ImageURL, "CustomImage.ImageURL", container, "USE_PARENT");
  2176.         if("CustomImage" in o && o.CustomImage && "ImageSecondaryURL" in o.CustomImage && o.CustomImage.ImageSecondaryURL) {
  2177.          var backContainer = $("<div class='deckBack'>").appendTo(container);
  2178.           tabletop.addImage(o.CustomImage.ImageSecondaryURL, "CustomImage.ImageSecondaryURL", backContainer);
  2179.         }
  2180.         if("CustomMesh" in o && o.CustomMesh && "DiffuseURL" in o.CustomMesh && o.CustomMesh.DiffuseURL)
  2181.          tabletop.addImage(o.CustomMesh.DiffuseURL, "CustomMesh.DiffuseURL", container);
  2182.         if("CustomDeck" in o && o.CustomDeck)
  2183.          tabletop.addCardDecks(o, "CustomDeck", container);
  2184.         if(o.Name == "Custom_PDF")
  2185.           $("<p>Not importable: <a href='" + o.CustomPDF.PDFUrl + "'>PDF</a></p>").appendTo(container);
  2186.  
  2187.         if(o.ContainedObjects && o.Name != "DeckCustom" && o.Name != "Deck")
  2188.          tabletop.listObjects(o["ContainedObjects"], $("<div></div>").appendTo(container));
  2189.         if($("img,p", container).length == 0)
  2190.           container.addClass("empty");
  2191.       });
  2192.     },
  2193.     readJSON: function(json) {
  2194.       $("#tabletopPreview").empty().append("<h3>" + json.SaveName + "</h3>");
  2195.       var target = $("<div id='tabletopMain'></div>").appendTo("#tabletopPreview");
  2196.       tabletop.showJSONbutton(json, target);
  2197.       if(json.Table == "Table_Custom")
  2198.         tabletop.addImage(json.TableURL, "Table_Custom", target);
  2199.       tabletop.listObjects(json["ObjectStates"], target);
  2200.     },
  2201.     showImport: function(file) {
  2202.       if(file.type == "application/json") {
  2203.         const reader = new FileReader();
  2204.         reader.addEventListener('load', (event) => tabletop.readJSON(JSON.parse(event.target.result.replace(/\+Infinity/g, 0))));
  2205.         reader.readAsText(file);
  2206.       } else {
  2207.         console.log(file);
  2208.       }
  2209.     },
  2210.     showJSONbutton: function(json, target) {
  2211.       $("<button>Show JSON</button>").appendTo(target).on("click", function() {
  2212.         $(this).parent().removeClass("empty");
  2213.         $(this).replaceWith("<textarea>" + JSON.stringify(json, null, "  ") + "</textarea>");
  2214.       });
  2215.     }
  2216.   };
  2217. </script>
  2218.  
Add Comment
Please, Sign In to add comment