diff --git a/assets/sprites_transparent.png b/assets/sprites_transparent.png new file mode 100644 index 0000000..88d674d Binary files /dev/null and b/assets/sprites_transparent.png differ diff --git a/index.html b/index.html index 8087ad8..e48bc53 100644 --- a/index.html +++ b/index.html @@ -97,12 +97,20 @@ document.addEventListener('DOMContentLoaded', function() { let form = document.getElementById("userForm"); let stats = document.getElementById("statsDisplay"); + let filterBar = document.getElementById("fname"); + if (!!filterBar) filterBar.value=""; form.style.display = "none"; stats.style.display = "none"; document.addEventListener('keydown', function(event) { if ((event.key === 'f' || event.key === 'F') && document.activeElement.id !== 'fname') { form.style.display = form.style.display === 'none' ? 'block' : 'none'; statsDisplay.style.display = statsDisplay.style.display === 'none' ? 'block' : 'none'; + }else if ((event.key === 'd' || event.key === 'D') && document.activeElement.id !== 'fname') { + toggleSpriteDirections(); + }else if ((event.key === 's' || event.key === 'S') && document.activeElement.id !== 'fname') { + toggleAgentsPathStacking(); + }else if ((event.key === 'w' || event.key === 'W') && document.activeElement.id !== 'fname') { + wipeAgents(); } }); }); diff --git a/visualizer_live.js b/visualizer_live.js index 07080e7..40d6ef3 100644 --- a/visualizer_live.js +++ b/visualizer_live.js @@ -16,6 +16,9 @@ let socket = null; let lastFrameTime = Date.now(); let curStats = {envs: 0, viewers: 0}; +let allowSpriteDirections = false; +let allowAgentsPathStacking = true; + let backgroundSharp = null; let backgroundSmooth = null; @@ -34,7 +37,6 @@ container.pivot.x = container.width / 2; container.pivot.y = container.height / 2; container.scale.set(0.1, 0.1); - app.stage.addChild(container); // add the view that Pixi created for you to the DOM @@ -47,15 +49,64 @@ function smoothstep(min, max, value) { return x*x*(3 - 2*x); } +const createLock = () => { + let lockStatus = false; + const release = () => { + lockStatus = false; + } + const acquire = () => { + if (lockStatus == true) + return false; + lockStatus = true; + return true; + } + return { + lockStatus: lockStatus, + acquire: acquire, + release: release, + }; +} + let userFilter = new RegExp(""); -let activeSprites = []; -function setUserFilter(value) { - userFilter = new RegExp(value); - activeSprites.forEach(obj => { - container.removeChild(obj.subContainer); // Remove sprite from the scene - obj.subContainer.destroy({ children: true }); // Optional: frees up memory used by the sprite +let activeAgents = {}; +let agentsLock = createLock(); + +function updateAgents(time){ + Object.entries(activeAgents).forEach(([phash, agent]) => { + agent.updatePath(time); }); - activeSprites = [] +} + +function wipeAgents(time){ + agentsLock.acquire(); + if(time!==undefined){ + activeAgents = Object.keys(activeAgents).reduce(function (filtered, phash) { + if (!activeAgents[phash].waitingDelete && activeAgents[phash].getRelativeTime(time) < animationDuration) filtered[phash] = activeAgents[phash]; + else activeAgents[phash].setPendingDelete(); + return filtered; + }, {}); + }else{ + activeAgents = Object.keys(activeAgents).reduce(function (filtered, phash) { + activeAgents[phash].setPendingDelete(); + return filtered; + }, {}); + } + agentsLock.release(); +} + +function setUserFilter(value) { + userFilter = new RegExp(value); + wipeAgents(); +} + +function toggleSpriteDirections(){ + allowSpriteDirections = !allowSpriteDirections; + wipeAgents(); +} + +function toggleAgentsPathStacking(){ + allowAgentsPathStacking = !allowAgentsPathStacking; + wipeAgents(); } app.view.addEventListener('wheel', (e) => { @@ -70,7 +121,7 @@ app.view.addEventListener('wheel', (e) => { // Calculate the point to scale around const point = new PIXI.Point(x, y); const localPoint = container.toLocal(point); - + // Scale the container container.scale.x *= scaleFactor; container.scale.y *= scaleFactor; @@ -157,7 +208,7 @@ app.view.addEventListener('touchmove', (e) => { }, { passive: false }); app.view.addEventListener('touchend', () => { - lastTouchDistance = null; + lastTouchDistance = null; }); // panning @@ -186,7 +237,7 @@ app.view.addEventListener('touchend', () => { }); -let coordConversionFunc = (coords) => [0,0]; +let coordConversionFunc = (coords) => [0,0,4]; fetch('assets/map_data.json') .then(response => response.json()) @@ -199,10 +250,12 @@ fetch('assets/map_data.json') if (MAP_DATA[coords[2]] !== undefined) { const mapX = MAP_DATA[coords[2]].coordinates[0]; const mapY = MAP_DATA[coords[2]].coordinates[1];//-vec2(217.5,221.5) - return [coords[0] + mapX - 217.5, coords[1] + mapY - 221.5]; + const maxMapX = Math.trunc(MAP_DATA[coords[2]].tileSize[0],16); + const maxMapY = Math.trunc(MAP_DATA[coords[2]].tileSize[1],16); + return [Math.max(0,Math.min(maxMapX,coords[0])) + mapX - 217.5, Math.max(0,Math.min(maxMapY,coords[1])) + mapY - 221.5, coords[3] || 4]; } else { console.warn(`No map coordiate location for id: ${coords[2]}`); - return [0,0]; + return [0,0,0]; } }; }) @@ -217,14 +270,29 @@ function getSpriteByCoords(x, y, baseTex) { return new PIXI.Texture(baseTex, new PIXI.Rectangle(sx, sy, width, height)); } +function getDirectionalSpritesById(id, sprite_x_count, baseTex) { + const sx = 0; + const sy = 16 * id; + const width = 16 * (sprite_x_count || 1); + const height = 16; + + return new PIXI.Texture(baseTex, new PIXI.Rectangle(sx, sy, width, height)); +} + +function getAgentHash(user,stackID){ + return user + "@" + stackID; +} + // "kanto_big_done1.png", - // "characters_transparent.png", + // "sprites_transparent.png", + // "characters_transparent.png", // OLD SPRITES, REMOVED // "characters_front.png" PIXI.Assets.load([ "assets/kanto_big_done1.png", - "assets/characters_transparent.png", - "assets/characters_front.png" + "assets/sprites_transparent.png", +// "assets/characters_transparent.png", +// "assets/characters_front.png" ]).then(() => { let baseTextureSmooth = new PIXI.BaseTexture("assets/kanto_big_done1.png", { @@ -245,7 +313,7 @@ PIXI.Assets.load([ container.addChild(backgroundSmooth); container.addChild(backgroundSharp); - // Function to initialize WebSocket connection + // Function to initialize WebSocket connection function initializeWebSocket(url) { const ws = new WebSocket(url); ws.onmessage = function(event) { @@ -257,7 +325,7 @@ PIXI.Assets.load([ } else { const path = data["coords"]; const meta = data["metadata"]; - console.log(meta); + ///console.log(meta); if (Date.now() - lastFrameTime < 2 * animationDuration) { startAnimationForPath(path, meta); } @@ -279,79 +347,167 @@ PIXI.Assets.load([ // Refresh WebSocket connection every 2 minutes (120000 milliseconds) setInterval(refreshWS, 120000); - let baseTextureChar = new PIXI.BaseTexture("assets/characters_transparent.png", { + let baseTextureChar = new PIXI.BaseTexture("assets/sprites_transparent.png", { scaleMode: PIXI.SCALE_MODES.NEAREST, }); - const charOffset = 1; // 1 index here gets sprite direction index - let textureChar = getSpriteByCoords(charOffset, 0, baseTextureChar); - - - function startAnimationForPath(path, meta) { + let texturesChars = []; + let texturesCharsDirectional = []; + + for(let i = 0; i < 73; ++i){ + texturesCharsDirectional.push(getDirectionalSpritesById(i, 4, baseTextureChar)); + texturesChars.push(getDirectionalSpritesById(i, 1, baseTextureChar)); + } - // Check if meta is defined and has a 'user' key - if (meta && meta.user !== undefined && typeof(meta.user) === "string") { - // Create a text label - const envID = meta.env_id !== undefined ? `-${meta.env_id}` : ""; - const extraInfo = meta.extra !== undefined ? ` ${meta.extra}` : ""; - const color = (meta.color && CSS.supports('color', meta.color)) ? meta.color : "0x000000"; - - const labelText = meta.user + envID + extraInfo; - if (userFilter.exec(labelText) !== null) { - const sprite = new PIXI.Sprite(textureChar); - //sprite.x = charOffset * 40; - sprite.anchor.set(0.5); - //sprite.scale.set(0.5); // Adjust scale as needed - const subContainer = new PIXI.Container(); - - subContainer.addChild(sprite); - const label = new PIXI.Text( - labelText, - { - fontFamily: 'Arial', - fontSize: 14, - fill: color, - align: 'center', - }); - label.x = sprite.x + sprite.width * 0.5; // Position the label next to the sprite - label.y -= sprite.height; // Adjust the label position as needed - subContainer.addChild(label); - container.addChild(subContainer); - - activeSprites.push({ subContainer, path, startTime: null }); + class Agent{ + constructor(user, envID, stackID, extraInfo, color, spriteID, path) { + this.user = user; + this.envID = Math.abs(parseInt(envID) || 0); + const curstackID = Math.abs(parseInt(stackID)); + this.stackID = isNaN(curstackID)? Math.floor(Math.random()*2048) : curstackID; + this.spriteID = 0; + this.dataBatchIdx = -1; + this.dataBatches = []; + this.waitingDelete = false; + this.animationDuration = animationDuration; + this.usingSpriteDirections = true; + this.sprite = null; + this.changeSprite(spriteID); + this.sprite.anchor.set(0.5); + this.subContainer = new PIXI.Container(); + this.subContainer.addChild(this.sprite); + this.label = new PIXI.Text(this.formatText(extraInfo), {fontFamily: 'Arial', fontSize: 14, fill: color, align: 'center'}); + this.label.x = this.sprite.x + this.sprite.width * 0.5; // Position the label next to the sprite + this.label.y -= this.sprite.height; // Adjust the label position as needed + this.subContainer.addChild(this.label); + container.addChild(this.subContainer); + this.appendBatch(path, null, null); + } + formatText(extraInfo){ + return this.user + "|" + this.envID + extraInfo; + } + changeText(extraInfo, color){ + if (this.waitingDelete) return + if (extraInfo) this.label.text = this.formatText(extraInfo); + if (color) this.label.style.fill = color; + } + allocateSprite(spriteID){ + this.usingSpriteDirections = allowSpriteDirections; + return this.usingSpriteDirections ? new PIXI.TilingSprite(texturesCharsDirectional[spriteID], 16, 16) : new PIXI.Sprite(texturesChars[spriteID]); + } + changeSprite(spriteID){ + this.spriteID = Math.abs(parseInt(spriteID) || 0) % texturesCharsDirectional.length; + this.sprite = this.allocateSprite(this.spriteID); + } + updateAnimationTime(){ + if (this.waitingDelete || !allowAgentsPathStacking || this.dataBatches[this.dataBatchIdx] === undefined){ + this.animationDuration = animationDuration; + }else{ + let batchSteps = Math.max(1, this.dataBatches[this.dataBatchIdx].path.length - 1); + let nextBatchesCount = Math.max(0,this.dataBatches.length - this.dataBatchIdx - 1); + let totalSteps = batchSteps + nextBatchesCount; + for(let i = this.dataBatchIdx+1; i 3) log2Steps += 1; + else if (nextBatchesCount > 7) log2Steps += 3; + if (log2Steps < 5) log2Steps = 1; + else if (log2Steps < 7) log2Steps = 2; + this.animationDuration = 400 * batchSteps / log2Steps; } } - - } - - function animate(time) { - activeSprites.forEach(obj => { - if (!obj.startTime) obj.startTime = time; - const timeDelta = time - obj.startTime; - const progress = Math.min(timeDelta / animationDuration, 1); - + appendBatch(path, extraInfo, color){ + if (!this.waitingDelete && path !== undefined){ + if (this.dataBatchIdx < 0) this.dataBatchIdx = 0; + if ((this.dataBatches.length - this.dataBatchIdx) < 10){ + if (path.length < 2048) path=path.slice(0, 2048); + this.dataBatches.push({path, extraInfo, color, startTime: null}); + this.updateAnimationTime(); + return true; + } + } + return false; + } + getRelativeTime(time){ + return this.dataBatches[this.dataBatchIdx] !== undefined ? (time - (this.dataBatches[this.dataBatchIdx].startTime || time)) || 0 : this.animationDuration + 1; + } + setPendingDelete(){ + container.removeChild(this.subContainer); // Remove sprite from the scene + this.subContainer.destroy({ children: true }); // Optional: frees up memory used by the sprite + this.waitingDelete = true; + } + updatePath(time){ + if (this.waitingDelete) return + if (!this.dataBatches[this.dataBatchIdx].startTime) this.dataBatches[this.dataBatchIdx].startTime = time; + const timeDelta = time - this.dataBatches[this.dataBatchIdx].startTime; + const progress = Math.min(timeDelta / this.animationDuration, 1); // Calculate the current position - const currentIndex = Math.floor(progress * (obj.path.length - 1)); - const nextIndex = Math.min(currentIndex + 1, obj.path.length - 1); - const pointProgress = (progress * (obj.path.length - 1)) - currentIndex; - - const currentPoint = coordConversionFunc(obj.path[currentIndex]); - const nextPoint = coordConversionFunc(obj.path[nextIndex]); - obj.subContainer.x = 16*(currentPoint[0] + (nextPoint[0] - currentPoint[0]) * pointProgress); - obj.subContainer.y = 16*(currentPoint[1] + (nextPoint[1] - currentPoint[1]) * pointProgress); - + const currentIndex = Math.floor(progress * (this.dataBatches[this.dataBatchIdx].path.length - 1)); + const nextIndex = Math.min(currentIndex + 1, this.dataBatches[this.dataBatchIdx].path.length - 1); + const pointProgress = (progress * (this.dataBatches[this.dataBatchIdx].path.length - 1)) - currentIndex; + + const currentPoint = coordConversionFunc(this.dataBatches[this.dataBatchIdx].path[currentIndex]); + const nextPoint = coordConversionFunc(this.dataBatches[this.dataBatchIdx].path[nextIndex]); + const deltaPoints = [nextPoint[0] - currentPoint[0], nextPoint[1] - currentPoint[1]]; + const absDeltaPoints = [Math.abs(deltaPoints[0]), Math.abs(deltaPoints[1])]; + // Hide subContainer when warping, to prevent fast and noisy movements + const visible = Math.max(absDeltaPoints[0], absDeltaPoints[1]) < 1.5; + this.subContainer.visible = visible; + if (visible){ + if (this.usingSpriteDirections&&this.subContainer.children[0].tilePosition!==undefined){ + let direction = Math.abs(parseInt(nextPoint[2]) || 0); + if (direction == 4) { + direction = progress >= 1 ? 0 : (absDeltaPoints[1] > 0.5 || absDeltaPoints[0] < 0.5 ? (deltaPoints[1] < -0.5 ? 1 : 0) : (deltaPoints[0] > 0.5 ? 3 : 2)); + } + console.log(this.usingSpriteDirections); + this.subContainer.children[0].tilePosition.x = -16 * (direction % 4); + } + this.subContainer.x = 16 * (currentPoint[0] + deltaPoints[0] * pointProgress); + this.subContainer.y = 16 * (currentPoint[1] + deltaPoints[1] * pointProgress); + } + if (progress >= 1) { - container.removeChild(obj.subContainer); // Remove sprite from the scene - obj.subContainer.destroy({ children: true }); // Optional: frees up memory used by the sprite + this.dataBatchIdx+=1; + if (this.dataBatches[this.dataBatchIdx] === undefined){ + this.setPendingDelete(); + }else{ + this.dataBatches[this.dataBatchIdx].path.unshift(this.dataBatches[this.dataBatchIdx - 1].path.slice(-1)[0]); + this.changeText(this.dataBatches[this.dataBatchIdx].extraInfo, this.dataBatches[this.dataBatchIdx].color); + } + delete this.dataBatches[this.dataBatchIdx-1]; + this.updateAnimationTime(); } + } + } - }); + function startAnimationForPath(path, meta) { + // Check if meta is defined and has ['user', 'env_id'] keys + if (meta && meta.user !== undefined && typeof(meta.user) === "string" && meta.user.length > 1){ + const envID = meta.env_id !== undefined ? Math.abs(parseInt(meta.env_id)) : NaN; + const invalidEnvID = isNaN(envID); + const stackID = !invalidEnvID && allowAgentsPathStacking ? envID : Math.floor(Math.random()*2048); + const phash = getAgentHash(meta.user, stackID); + if (userFilter.exec(phash) !== null) { + console.log(meta); + const extraInfo = meta.extra !== undefined ? ` ${meta.extra}` : ""; + const color = (meta.color && CSS.supports('color', meta.color)) ? meta.color : "0x000000"; + agentsLock.acquire(); + if (activeAgents[phash] === undefined || activeAgents[phash].waitingDelete) { + const spriteID = meta && meta.sprite_id !== undefined && meta.sprite_id >= 0 && meta.sprite_id < 73 ? parseInt(meta.sprite_id) : 0; + let agent = new Agent(meta.user, invalidEnvID ? 0 : envID, stackID, extraInfo, color, spriteID, path); + activeAgents[phash]=agent; + } else activeAgents[phash].appendBatch(path, extraInfo, color); + agentsLock.release(); + } + }; + } - // Remove sprites that have completed their animation - activeSprites = activeSprites.filter(obj => (time - obj.startTime) < animationDuration); + function animate(time) { + updateAgents(time); + wipeAgents(time); lastFrameTime = Date.now(); requestAnimationFrame(animate); } - requestAnimationFrame(animate); });