fix table reconnect on lag; keep scroll on token select and master list when refreshing; add token copy; v0.2.0

This commit is contained in:
Iris Lightshard 2023-07-16 21:29:53 -06:00
parent 196294b5b8
commit f1c56f63f1
Signed by: nilix
GPG key ID: 3B7FBC22144E6398
8 changed files with 86 additions and 56 deletions

View file

@ -38,9 +38,11 @@ Joining a table also grants the admin access to the `tokens` and `sprites` panel
After hitting the `New Token` button, the list of tokens is replaced with a form. The sprite can be selected from a dropdown (which sets the other fields to resonable defaults), and the name, width, height, and coordinate origin of the token can be adjusted. There is a toggle for holding the aspect ratio of the token constant, and as changes are made a live preview similar to the one in the `token select` panel is updated (with the addition of a small pink cross indicating the coordinate origin).
In the `tokens` list, a token name can be clicked to live preview or deleted with the button to the right of the name.
*Scene-building note* - The coordinate origin determintes the layering of the tokens - tokens are ordered on the z-axis based on their vertical coordinate - eg scenic tokens which should be underneath characters can have their coordinate origin at the top of the sprite, and tokens which should be rendered above others in the scene can have their coordinate origin at or below the bottom edge.
Creating or destroying tokens as well as changing the map causes updates to be sent to all clients at the same table in the same manner as (de)activating and moving tokens.
In the `tokens` list, a token name can be clicked to live preview, or you can use the buttons at right to copy (opens the form with the token's data - you can, eg, change the name and create the copy quickly) or destroy the token.
Creating or destroying tokens as well as changing the map causes updates to be sent to all clients at the same table in the same manner as (de)activating and moving tokens, updating status, or rolling dice.
## build, run, deploy

View file

@ -63,7 +63,6 @@ async function rebindUi(name, pass) {
let tokenListHTML = "<input id='token_img_upload' type='file'/><button onclick='uploadTokenImg()'>Upload Sprite</button><br/>";
if (tokenImgs.ok) {
tokenListHTML += "<label>Available Sprites</label>";
const tokens = (await tokenImgs.json()).sort();
tokenListHTML += "<ul class='single_btn_list'>";
for (const t of tokens) {
@ -129,6 +128,11 @@ function scaleSpritePreview(source) {
const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth;
const keepAspect = tokenAspect.checked;
const img = previewZone.children[0];
tokenHeight.value = Math.floor(Number(tokenHeight.value))
tokenWidth.value = Math.floor(Number(tokenWidth.value))
if (img) {
if (!keepAspect || !source) {
img.width = Number(tokenWidth.value) * scaleFactor;
@ -140,12 +144,12 @@ function scaleSpritePreview(source) {
case "token_width":
img.width = Number(tokenWidth.value) * scaleFactor;
img.height = (img.clientWidth / img.naturalWidth) * img.naturalHeight;
tokenHeight.value = Number(tokenWidth.value)/currentAspect;
tokenHeight.value = Math.floor(Number(tokenWidth.value)/currentAspect);
break;
case "token_height":
img.height = Number(tokenHeight.value) * scaleFactor;
img.width = (img.clientHeight / img.naturalHeight) * img.naturalWidth;
tokenWidth.value = currentAspect * Number(tokenHeight.value);
tokenWidth.value = Math.floor(currentAspect * Number(tokenHeight.value));
break;
}
}
@ -159,6 +163,10 @@ function scaleSpritePreview(source) {
function drawTokenOrigin() {
if (tokenSpriteDropdown.selectedIndex >= 0) {
tokenCX.value = Math.floor(Number(tokenCX.value))
tokenCY.value = Math.floor(Number(tokenCY.value))
const img = previewZone.children[0];
const x = Number(tokenWidth.value) / Number(tokenCX.value);
const y = Number(tokenHeight.value) / Number(tokenCY.value);
@ -257,14 +265,20 @@ function previewExistingToken(id) {
}
}
function copyToken(id) {
setTokenCreateFormVisible(true);
previewExistingToken(id);
}
function renderTokenMasterList() {
if (tokenZone) {
let tokenMasterListHTML = "<label>Available Tokens</label><br/><ul class='single_btn_list'>";
let tokenMasterListHTML = "";
const scroll = tokenZone.scrollTop;
for (const t of tokens) {
tokenMasterListHTML += `<li><a href="#" onclick="previewExistingToken('${t.t.id}');return false">${t.t.name}</a><button onclick="destroyToken('${t.t.id}')">Destroy</button></li>\n`;
tokenMasterListHTML += `<li><a href="#" onclick="previewExistingToken('${t.t.id}');return false">${t.t.name}</a><button onclick="copyToken('${t.t.id}')">Copy</button><button onclick="destroyToken('${t.t.id}')">Destroy</button></li>\n`;
}
tokenMasterListHTML += "</ul>";
tokenZone.innerHTML = tokenMasterListHTML;
tokenZone.scrollTop = scroll;
}
}
@ -409,9 +423,10 @@ async function getTables() {
adminZone.innerHTML = tableListHTML;
tokenWrapper.style.display = "none";
} else {
// fail silently here
// fail silently
}
} catch {
// fail silently
}
}
@ -484,7 +499,7 @@ function setTokenCreateFormVisible(v) {
tokenSpriteDropdown.selectedIndex = 0;
}
createTokenForm.style.display = v ? "block" : "none";
tokenZone.style.display = v ? "none" : "inline";
tokenZone.style.display = v ? "none" : "block";
reinitializeSpritePreview();
}
}

View file

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width" />
<link rel="shortcut icon" href="./favicon.png"/>
<link href="./leaflet.css?v=1.9.4" rel="stylesheet" />
<link href="./style.css?v=0.1.0" rel="stylesheet" />
<link href="./style.css?v=0.2.0" rel="stylesheet" />
</head>
<body>
<noscript><div id="noscript_container">
@ -77,7 +77,7 @@
<button id="tokenPreview_alt_clear" onclick="dismissPreview()" style="display: none;">Dismiss Preview</button>
<div id="tokenPreview_alt"></div>
<input hidden id="tokenPreview_alt_id"/>
<div id="token_select"></div>
<ul id="token_select" class="single_btn_list"></ul>
</details><br/>
</div>
@ -103,6 +103,7 @@
</form>
<div id="adminZone"></div>
</details><br/>
<div id="adminWrapper_tokens">
<details id="admin_token_win" class="ui_win admin_win"><summary>tokens</summary>
<button onclick="setTokenCreateFormVisible(true)">New Token</button>
@ -110,20 +111,22 @@
<label>Sprite<select id="token_combobox" onchange="previewSprite(this)"></select></label><br/>
<label>Name<input id="token_name"/></label><br/>
<label>Width<input type="number" id="token_width" min="1" max="9999" onchange="previewSprite(this)"/></label><label id="aspectLockLabel" for="tokenKeepAspect">&#128274;</label><input type="checkbox" checked id="tokenKeepAspect" onchange="toggleAspectLock()"/><br/>
<label>Height<input type="number" id="token_height" min="1" max="9999" onchange="previewSprite(this)"/></label><br/>
<label>cX<input type="number" id="token_cx" min="0" max="9999" onchange="previewSprite(this)"/></label><br/>
<label>cY<input type="number" id="token_cy" min="0" max="9999" onchange="previewSprite(this)"/></label><br/>
<label>Width<input type="number" id="token_width" min="1" max="9999" step="1" onchange="previewSprite(this)"/></label><label id="aspectLockLabel" for="tokenKeepAspect">&#128274;</label><input type="checkbox" checked id="tokenKeepAspect" onchange="toggleAspectLock()"/><br/>
<label>Height<input type="number" id="token_height" min="1" step="1" max="9999" onchange="previewSprite(this)"/></label><br/>
<label>cX<input type="number" id="token_cx" min="0" max="9999" step="1" onchange="previewSprite(this)"/></label><br/>
<label>cY<input type="number" id="token_cy" min="0" max="9999" step="1" onchange="previewSprite(this)"/></label><br/>
<button type="submit" onclick="createToken()">Create</button>
<button onclick="setTokenCreateFormVisible(false)">Cancel</button>
</form>
<div id="tokenPreview_zone"></div>
<div id="tokenZone"></div>
<ul id="tokenZone" class="two_btn_list"></ul>
</details><br/>
<details id="admin_sprite_win" class="ui_win admin_win"><summary>sprites</summary>
<div id="spriteZone"></div>
</details>
</div>
</div>
</section>
@ -143,13 +146,13 @@
</form>
</details>
<div id="lag" style="display:none;">lag...</div>
<div class="ui_win" id="felt_info"><a href="https://hacklab.nilfm.cc/felt">felt v0.1.0</a> (<a href="https://hacklab.nilfm.cc/felt/raw/main/LICENSE">license</a>) | built with <a href="https://leafletjs.com">leaflet</a> (<a href="https://hacklab.nilfm.cc/felt/raw/main/LEAFLET_LICENSE">license</a>) </div>
<div class="ui_win" id="felt_info"><a href="https://hacklab.nilfm.cc/felt">felt v0.2.0</a> (<a href="https://hacklab.nilfm.cc/felt/raw/main/LICENSE">license</a>) | built with <a href="https://leafletjs.com">leaflet</a> (<a href="https://hacklab.nilfm.cc/felt/raw/main/LEAFLET_LICENSE">license</a>) </div>
</nav>
</body>
<script src="./leaflet.js?v=1.9.4" type="text/javascript"></script>
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
<script src="./map.js?v=0.1.0" type="text/javascript"></script>
<script src="./socket.js?v=0.1.0" type="text/javascript"></script>
<script src="./dice.js?v=0.1.0" type="text/javascript"></script>
<script src="./admin.js?v=0.1.0" type="text/javascript"></script>
<script src="./util.js?v=0.2.0" type="text/javascript"></script>
<script src="./map.js?v=0.2.0" type="text/javascript"></script>
<script src="./socket.js?v=0.2.0" type="text/javascript"></script>
<script src="./dice.js?v=0.2.0" type="text/javascript"></script>
<script src="./admin.js?v=0.2.0" type="text/javascript"></script>
</html>

View file

@ -1,6 +1,6 @@
let map = null;
let mapImg = null;
let tokens = [];
const tokens = [];
const worldBounds = [[180, -180], [-180, 180]];
const cameraBounds = [[270, -270], [-270, 270]];
@ -26,10 +26,11 @@ function initializeMap(mapImgUrl) {
function resizeMarkers() {
// for newly created tokens, the icon may not be loaded and thus the resize will not properly complete
// we need a way to queue the resize for when the icon is finished loading
// TODO: we need a way to queue the resize for when the icon is finished loading
tokens.forEach(t=>{
const icon = t.m.options.icon;
const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth;
icon.options.iconSize = [scaleFactor * t.t.w, scaleFactor * t.t.h];

View file

@ -2,7 +2,6 @@ let tableKey = {
name: "",
passcode: ""
}
let table = null;
let conn = null;
let offline = false;
@ -77,12 +76,13 @@ function updateTokens(tokens) {
function renderTokenSelect() {
const tokenSelect = document.getElementById("token_select");
let tokenSelectHTML = "<ul class='single_btn_list'>";
let tokenSelectHTML = ""
const scroll = tokenSelect.scrollTop;
for (const t of tokens) {
tokenSelectHTML += `<li><a href="#" onclick="initSpritePreviewById('${t.t.id}')">${t.t.name}</a><button onclick="toggleActive('${t.t.id}');return false">${(t.t.active ? "Deactivate" : "Activate")}</button></li>\n`;
}
tokenSelectHTML += "</ul>";
tokenSelect.innerHTML = tokenSelectHTML;
tokenSelect.scrollTop = scroll;
}
// the following few functions aren't socket related but they directly relate to the previous function
@ -123,26 +123,29 @@ function scaleAltSpritePreview() {
}
}
function makeUpToDate(table) {
if (table) {
function makeUpToDate(msg) {
if (msg) {
// map image has to be set before tokens can be handled!
if (table.mapImg) {
setMapImg(table.mapImg);
}
if (table.auxMsg) {
setAuxMsg(table.auxMsg);
}
if (table.diceRolls) {
logDice(table.diceRolls);
} else if (table.diceRoll) {
logDice(table.diceRoll);
}
if (table.tokens) {
updateTokens(table.tokens);
} else if (table.token) {
updateTokens([table.token]);
if (msg.mapImg) {
setMapImg(msg.mapImg);
}
if (msg.auxMsg) {
setAuxMsg(msg.auxMsg);
}
if (msg.diceRolls) {
logDice(msg.diceRolls);
} else if (msg.diceRoll) {
logDice(msg.diceRoll);
}
if (msg.tokens) {
updateTokens(msg.tokens);
} else if (msg.token) {
updateTokens([msg.token]);
}
}
}
@ -176,33 +179,40 @@ function dial() {
}
const wsProto = location.protocol == "https:" ? "wss" : "ws";
conn = new WebSocket(`${wsProto}://${location.host}/subscribe`, `${tableKey.name}.${tableKey.passcode}`);
conn.addEventListener("close", e => {
offline = true;
if (e.code == 1006 && e.wasClean) {
setErr("Table not found - check the name and passcode are correct");
} else if (e.code > 1001) {
lagDiv.style.display = "block";
setTimeout(dial, 1000)
} else {
tblNameInput.readOnly = false;
tblPassInput.readOnly = false;
joinTblBtn.style.display = adminToken ? "none" : "inline";
leaveTblBtn.style.display = "none";
tabletop = document.getElementById("tabletop");
if (tabletop) {
tabletop.style.display = "none";
}
table = null;
while (tokens.some(t=>t)) {
tokens[0].m.removeFrom(map);
tokens.shift();
}
if (mapImg) {
mapImg.removeFrom(map);
mapImg = null;
}
}
});
conn.addEventListener("open", e => {
offline = false;
tblNameInput.readOnly = true;
@ -227,15 +237,13 @@ function dial() {
conn.addEventListener("message", e => {
const data = JSON.parse(e.data);
if (table == null) {
table = data;
}
if (data.diceRolls) {
// dicerolls are treated as a byte array when marshalling to json, so we have to decode them
data.diceRolls.forEach(r=>{
r.roll = Uint8Array.from(atob(r.roll), c => c.charCodeAt(0))
})
makeUpToDate(table);
makeUpToDate(data);
} else {
if (data.diceRoll) {
data.diceRoll.roll = Uint8Array.from(atob(data.diceRoll.roll), c => c.charCodeAt(0));
@ -245,6 +253,7 @@ function dial() {
console.log(data);
});
} else {
setErr("Table name and passcode can only be alphanumeric and underscores");
}

View file

@ -6,7 +6,7 @@
<title>Felt &mdash; Error</title>
<meta name="viewport" content="width=device-width" />
<link rel="shortcut icon" href="/table/favicon.png"/>
<link href="/table/style.css?v=0.1.0" rel="stylesheet" />
<link href="/table/style.css?v=0.2.0" rel="stylesheet" />
</head>
<body>
<main id="registration">
@ -15,6 +15,6 @@
<p><a href="/table">Get back to gaming...</a></p>
</main>
</body>
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
<script src="./util.js?v=0.2.0" type="text/javascript"></script>
</html>
<html>

View file

@ -7,7 +7,7 @@
<title>Felt &mdash; Admin Registration</title>
<meta name="viewport" content="width=device-width" />
<link rel="shortcut icon" href="/table/favicon.png"/>
<link href="/table/style.css?v=0.1.0" rel="stylesheet" />
<link href="/table/style.css?v=0.2.0" rel="stylesheet" />
</head>
<body>
<main id="registration">
@ -23,5 +23,5 @@
{{end}}
</main>
</body>
<script src="/table/util.js?v=0.1.0" type="text/javascript"></script>
<script src="/table/util.js?v=0.2.0" type="text/javascript"></script>
</html>

View file

@ -6,7 +6,7 @@
<title>Felt &mdash; Registration Complete</title>
<meta name="viewport" content="width=device-width" />
<link rel="shortcut icon" href="/table/favicon.png"/>
<link href="/table/style.css?v=0.1.0" rel="stylesheet" />
<link href="/table/style.css?v=0.2.0" rel="stylesheet" />
</head>
<body>
<main id="registration">
@ -19,5 +19,5 @@
{{end}}
</main>
</body>
<script src="/table/util.js?v=0.1.0" type="text/javascript"></script>
<script src="/table/util.js?v=0.2.0" type="text/javascript"></script>
</html>