Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Request: TurnMarker animation update

Now that roll20 supports animations natively, how much effort would it be to create a version of TurnMarker that uses an animated png, rather than using JS client-side to animate? And while I'm asking for the moon: Is TurnMarker2 with multi-page support still in the works?
1549424393

Edited 1549424411
The Aaron
Roll20 Production Team
API Scripter
That's a great idea!  As it happens, I've started on a full rewrite of TurnMarker recently.  Multi page support and switching out the image it uses are both things I am working on.  Now you've upped the bar! Anyone have an animated image that would make a suitable marker?  Or maybe a couple to choose from?
Personally I actually really like the existing one.  The contrast shows up on every map I've used it on thus far and I'm colorblind.  If that rotation could be turned into an integrated animation, I'd be very happy.
1549468664
Spren
Sheet Author
I use Tracker Jacker but I might switch if this becomes a thing. This is honestly a really good idea.
1549470304
The Aaron
Roll20 Production Team
API Scripter
Yeah, it’s brilliant and I wish I’d thought of it. =D Michael, I’ll definitely reach out to you with new images for feedback. I originally wrote TurnMarker and it’s animations because one of my players and long term friends is nearly blind and needed the high contrast and highlighting to be able to pick out the token. 
1549504128

Edited 1549504148
The Aaron
Roll20 Production Team
API Scripter
Tried it out and it's AWESOME! If you want to try it out, here is a version with the original image as an animation: // Github: <a href="https://github.com/shdwjk/Roll20API/blob/master/TurnMarker1/TurnMarker1.js" rel="nofollow">https://github.com/shdwjk/Roll20API/blob/master/TurnMarker1/TurnMarker1.js</a> // By: The Aaron, Arcane Scriptomancer // Contact: <a href="https://app.roll20.net/users/104025/the-aaron" rel="nofollow">https://app.roll20.net/users/104025/the-aaron</a> /* global GroupInitiative:false Mark:false */ /* ############################################################### */ /* TurnMarker */ /* ############################################################### */ var TurnMarker = TurnMarker || (function(){ "use strict"; var version = '1.3.9-animation', lastUpdate = 1500408657, schemaVersion = 1.18, active = false, threadSync = 1, autoPullOptions = { 'none' : 'None', 'npcs' : 'NPCs', 'all' : 'All' }, fixedSendPing = (function(){ var last={}; return function(left,top,pageid,playerid,pull){ if( last.left === left &amp;&amp; last.top === top &amp;&amp; last.pageid === pageid ) { sendPing(-100,-100,pageid,null,false); } sendPing(left,top,pageid,playerid,pull); last.left=left; last.top=top; last.pageid=pageid; }; }()), checkInstall = function() { log('-=&gt; TurnMarker v'+version+' &lt;=- ['+(new Date(lastUpdate*1000))+']'); if( ! state.hasOwnProperty('TurnMarker') || state.TurnMarker.version !== schemaVersion) { log(' &gt; Updating Schema to v'+schemaVersion+' &lt;'); switch(state.TurnMarker &amp;&amp; state.TurnMarker.version) { case 1.16: state.TurnMarker.autoPull = 'none'; /* falls through */ case 'UpdateSchemaVersion': state.TurnMarker.version = schemaVersion; break; default: state.TurnMarker = { version: schemaVersion, announceRounds: true, announceTurnChange: true, announcePlayerInTurnAnnounce: true, announcePlayerInTurnAnnounceSize: '100%', autoPull: 'none', autoskipHidden: true, tokenName: 'Round', tokenURL: '<a href="https://s3.amazonaws.com/files.d20.io/images/73253126/AW44rzefhcZtWozC4b9myg/thumb.webm?1549503561" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/73253126/AW44rzefhcZtWozC4b9myg/thumb.webm?1549503561</a>', playAnimations: false, rotation: false, animationSpeed: 5, scale: 1.7, aura1: { pulse: false, size: 5, color: '#ff00ff' }, aura2: { pulse: false, size: 5, color: '#00ff00' } }; break; } } if(Campaign().get('turnorder') ==='') { Campaign().set('turnorder','[]'); } if('undefined' !== typeof GroupInitiative &amp;&amp; GroupInitiative.ObserveTurnOrderChange){ GroupInitiative.ObserveTurnOrderChange(handleExternalTurnOrderChange); } }, showHelp = function(who) { var marker = getMarker(); var rounds =parseInt(marker.get('bar2_value'),10); sendChat('', '/w "'+who+'" '+ '&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;'+ '&lt;div style="font-weight: bold; border-bottom: 1px solid black;font-size: 130%;"&gt;'+ 'TurnMarker v'+version+ '&lt;/div&gt;'+ '&lt;b&gt;Commands&lt;/b&gt;'+ '&lt;div style="padding-left:10px;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;!tm&lt;/span&gt;&lt;/b&gt;'+ '&lt;div style="padding-left: 10px;padding-right:20px"&gt;'+ 'The following arguments may be supplied in order to change the configuration. All changes are persisted between script restarts.'+ '&lt;ul&gt;'+ '&lt;div style="float:right;width:40px;border:1px solid black;background-color:#ffc;text-align:center;"&gt;&lt;span style="color: blue; font-weight:bold; padding: 0px 4px;"&gt;'+rounds+'&lt;/span&gt;&lt;/div&gt;'+ '&lt;li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;reset &amp;lbrack;#&amp;rbrack;&lt;/span&gt;&lt;/b&gt; -- Sets the round counter back to 0 or the supplied #.&lt;/li&gt; '+ '&lt;div style="float:right;width:40px;border:1px solid black;background-color:#ffc;text-align:center;"&gt;&lt;span style="color: blue; font-weight:bold; padding: 0px 4px;"&gt;'+autoPullOptions[state.TurnMarker.autoPull]+'&lt;/span&gt;&lt;/div&gt;'+ '&lt;li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;autopull &amp;lt;mode&amp;gt;&lt;/span&gt;&lt;/b&gt; -- Sets auto pulling to the token whose turn it is. Modes: '+_.keys(autoPullOptions)+'&lt;/li&gt; '+ '&lt;div style="float:right;width:40px;border:1px solid black;background-color:#ffc;text-align:center;"&gt;'+( state.TurnMarker.announceRounds ? '&lt;span style="color: red; font-weight:bold; padding: 0px 4px;"&gt;ON&lt;/span&gt;' : '&lt;span style="color: #999999; font-weight:bold; padding: 0px 4px;"&gt;OFF&lt;/span&gt;' )+'&lt;/div&gt;'+ '&lt;li style="border-bottom: 1px solid #ccc;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;toggle-announce&lt;/span&gt;&lt;/b&gt; -- When on, each round will be announced to chat.&lt;/li&gt;'+ '&lt;div style="float:right;width:40px;border:1px solid black;background-color:#ffc;text-align:center;"&gt;'+( state.TurnMarker.announceTurnChange ? '&lt;span style="color: red; font-weight:bold; padding: 0px 4px;"&gt;ON&lt;/span&gt;' : '&lt;span style="color: #999999; font-weight:bold; padding: 0px 4px;"&gt;OFF&lt;/span&gt;' )+'&lt;/div&gt;'+ '&lt;li style="border-bottom: 1px solid #ccc;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;toggle-announce-turn&lt;/span&gt;&lt;/b&gt; -- When on, the transition between visible turns will be announced.&lt;/li&gt; '+ '&lt;div style="float:right;width:40px;border:1px solid black;background-color:#ffc;text-align:center;"&gt;'+( state.TurnMarker.announcePlayerInTurnAnnounce ? '&lt;span style="color: red; font-weight:bold; padding: 0px 4px;"&gt;ON&lt;/span&gt;' : '&lt;span style="color: #999999; font-weight:bold; padding: 0px 4px;"&gt;OFF&lt;/span&gt;' )+'&lt;/div&gt;'+ '&lt;li style="border-bottom: 1px solid #ccc;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;toggle-announce-player&lt;/span&gt;&lt;/b&gt; -- When on, the player(s) controlling the current turn are included in the turn announcement.&lt;/li&gt; '+ '&lt;div style="float:right;width:40px;border:1px solid black;background-color:#ffc;text-align:center;"&gt;'+( state.TurnMarker.autoskipHidden ? '&lt;span style="color: red; font-weight:bold; padding: 0px 4px;"&gt;ON&lt;/span&gt;' : '&lt;span style="color: #999999; font-weight:bold; padding: 0px 4px;"&gt;OFF&lt;/span&gt;' )+'&lt;/div&gt;'+ '&lt;li style="border-bottom: 1px solid #ccc;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;toggle-skip-hidden&lt;/span&gt;&lt;/b&gt; -- When on, turn order will automatically be advanced past any hidden turns.&lt;/li&gt; '+ '&lt;div style="float:right;width:40px;border:1px solid black;background-color:#ffc;text-align:center;"&gt;'+( state.TurnMarker.playAnimations ? '&lt;span style="color: red; font-weight:bold; padding: 0px 4px;"&gt;ON&lt;/span&gt;' : '&lt;span style="color: #999999; font-weight:bold; padding: 0px 4px;"&gt;OFF&lt;/span&gt;' )+'&lt;/div&gt;'+ '&lt;li style="border-bottom: 1px solid #ccc;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;toggle-animations&lt;/span&gt;&lt;/b&gt; -- Turns on turn marker animations. [Experimental!]&lt;/li&gt; '+ '&lt;div style="float:right;width:40px;border:1px solid black;background-color:#ffc;text-align:center;"&gt;'+( state.TurnMarker.rotation ? '&lt;span style="color: red; font-weight:bold; padding: 0px 4px;"&gt;ON&lt;/span&gt;' : '&lt;span style="color: #999999; font-weight:bold; padding: 0px 4px;"&gt;OFF&lt;/span&gt;' )+'&lt;/div&gt;'+ '&lt;li style="border-bottom: 1px solid #ccc;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;toggle-rotate&lt;/span&gt;&lt;/b&gt; -- When on, the turn marker will rotate slowly clockwise. [Animation]&lt;/li&gt; '+ '&lt;div style="float:right;width:40px;border:1px solid black;background-color:#ffc;text-align:center;"&gt;'+( state.TurnMarker.aura1.pulse ? '&lt;span style="color: red; font-weight:bold; padding: 0px 4px;"&gt;ON&lt;/span&gt;' : '&lt;span style="color: #999999; font-weight:bold; padding: 0px 4px;"&gt;OFF&lt;/span&gt;' )+'&lt;/div&gt;'+ '&lt;li style="border-bottom: 1px solid #ccc;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;toggle-aura-1&lt;/span&gt;&lt;/b&gt; -- When on, aura 2 will pulse in and out. [Animation]&lt;/li&gt; '+ '&lt;div style="float:right;width:40px;border:1px solid black;background-color:#ffc;text-align:center;"&gt;'+( state.TurnMarker.aura2.pulse ? '&lt;span style="color: red; font-weight:bold; padding: 0px 4px;"&gt;ON&lt;/span&gt;' : '&lt;span style="color: #999999; font-weight:bold; padding: 0px 4px;"&gt;OFF&lt;/span&gt;' )+'&lt;/div&gt;'+ '&lt;li style="border-bottom: 1px solid #ccc;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;toggle-aura-2&lt;/span&gt;&lt;/b&gt; -- When on, aura 2 will pulse in and out. [Animation]&lt;/li&gt; '+ '&lt;/ul&gt;'+ '&lt;/div&gt;'+ '&lt;/div&gt;'+ '&lt;div style="padding-left:10px;"&gt;&lt;b&gt;&lt;span style="font-family: serif;"&gt;!eot&lt;/span&gt;&lt;/b&gt;'+ '&lt;div style="padding-left: 10px;padding-right:20px;"&gt;'+ 'Players may execute this command to advance the initiative to the next turn. This only succeeds if the current token is one that the caller controls or if it is executed by a GM.'+ '&lt;/div&gt;'+ '&lt;/div&gt;'+ '&lt;/div&gt;' ); }, handleInput = function(msg){ var who, tokenized, command; if (msg.type !== "api") { return; } who=(getObj('player',msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); tokenized = msg.content.split(/\s+/); command = tokenized[0]; switch(command) { case "!tm": case "!turnmarker": { if(!playerIsGM(msg.playerid)){ return; } let tokens=_.rest(tokenized),marker,value; switch (tokens[0]) { case 'reset': marker = getMarker(); value = parseInt(tokens[1],10)||0; marker.set({ name: state.TurnMarker.tokenName+' '+value, bar2_value: value }); sendChat('','/w "'+who+'" &lt;b&gt;Round&lt;/b&gt; count is reset to &lt;b&gt;'+value+'&lt;/b&gt;.'); break; case 'ping-target': var obj=getObj('graphic',tokens[1]); if(obj){ fixedSendPing(obj.get('left'),obj.get('top'),obj.get('pageid'),null,true); } break; case 'autopull': if(_.contains(_.keys(autoPullOptions), tokens[1])){ state.TurnMarker.autoPull=tokens[1]; sendChat('','/w "'+who+'" &lt;b&gt;AutoPull&lt;/b&gt; is now &lt;b&gt;'+(autoPullOptions[state.TurnMarker.autoPull])+'&lt;/b&gt;.'); } else { sendChat('','/w "'+who+'" "'+tokens[1]+'" is not a valid &lt;b&gt;AutoPull&lt;/b&gt; options. Please specify one of: '+_.keys(autoPullOptions).join(', ')+'&lt;/b&gt;.'); } break; case 'toggle-announce': state.TurnMarker.announceRounds=!state.TurnMarker.announceRounds; sendChat('','/w "'+who+'" &lt;b&gt;Announce Rounds&lt;/b&gt; is now &lt;b&gt;'+(state.TurnMarker.announceRounds ? 'ON':'OFF' )+'&lt;/b&gt;.'); break; case 'toggle-announce-turn': state.TurnMarker.announceTurnChange=!state.TurnMarker.announceTurnChange; sendChat('','/w "'+who+'" &lt;b&gt;Announce Turn Changes&lt;/b&gt; is now &lt;b&gt;'+(state.TurnMarker.announceTurnChange ? 'ON':'OFF' )+'&lt;/b&gt;.'); break; case 'toggle-announce-player': state.TurnMarker.announcePlayerInTurnAnnounce=!state.TurnMarker.announcePlayerInTurnAnnounce; sendChat('','/w "'+who+'" &lt;b&gt;Player Name in Announce&lt;/b&gt; is now &lt;b&gt;'+(state.TurnMarker.announcePlayerInTurnAnnounce ? 'ON':'OFF' )+'&lt;/b&gt;.'); break; case 'toggle-skip-hidden': state.TurnMarker.autoskipHidden=!state.TurnMarker.autoskipHidden; sendChat('','/w "'+who+'" &lt;b&gt;Auto-skip Hidden&lt;/b&gt; is now &lt;b&gt;'+(state.TurnMarker.autoskipHidden ? 'ON':'OFF' )+'&lt;/b&gt;.'); break; case 'toggle-animations': state.TurnMarker.playAnimations=!state.TurnMarker.playAnimations; if(state.TurnMarker.playAnimations) { stepAnimation(threadSync); } else { marker = getMarker(); marker.set({ aura1_radius: '', aura2_radius: '' }); } sendChat('','/w "'+who+'" &lt;b&gt;Animations&lt;/b&gt; are now &lt;b&gt;'+(state.TurnMarker.playAnimations ? 'ON':'OFF' )+'&lt;/b&gt;.'); break; case 'toggle-rotate': state.TurnMarker.rotation=!state.TurnMarker.rotation; sendChat('','/w "'+who+'" &lt;b&gt;Rotation&lt;/b&gt; is now &lt;b&gt;'+(state.TurnMarker.rotation ? 'ON':'OFF' )+'&lt;/b&gt;.'); break; case 'toggle-aura-1': state.TurnMarker.aura1.pulse=!state.TurnMarker.aura1.pulse; sendChat('','/w "'+who+'" &lt;b&gt;Aura 1&lt;/b&gt; is now &lt;b&gt;'+(state.TurnMarker.aura1.pulse ? 'ON':'OFF' )+'&lt;/b&gt;.'); break; case 'toggle-aura-2': state.TurnMarker.aura2.pulse=!state.TurnMarker.aura2.pulse; sendChat('','/w "'+who+'" &lt;b&gt;Aura 2&lt;/b&gt; is now &lt;b&gt;'+(state.TurnMarker.aura2.pulse ? 'ON':'OFF' )+'&lt;/b&gt;.'); break; default: case 'help': showHelp(who); break; } } break; case "!eot": requestTurnAdvancement(msg.playerid); break; } }, getMarker = function(){ var marker = findObjs({ imgsrc: state.TurnMarker.tokenURL, pageid: Campaign().get("playerpageid") })[0]; if (marker === undefined) { marker = createObj('graphic', { name: state.TurnMarker.tokenName+' 0', pageid: Campaign().get("playerpageid"), layer: 'gmlayer', imgsrc: state.TurnMarker.tokenURL, left: 0, top: 0, height: 70, width: 70, bar2_value: 0, showplayers_name: true, showplayers_aura1: true, showplayers_aura2: true }); } if(!TurnOrder.HasTurn(marker.id)) { TurnOrder.AddTurn({ id: marker.id, pr: -1, custom: "", pageid: marker.get('pageid') }); } return marker; }, stepAnimation = function( sync ){ if (!state.TurnMarker.playAnimations || sync !== threadSync) { return; } var marker=getMarker(); if(active === true) { var rotation=(marker.get('bar1_value')+state.TurnMarker.animationSpeed)%360; marker.set('bar1_value', rotation ); if(state.TurnMarker.rotation) { marker.set( 'rotation', rotation ); } if( state.TurnMarker.aura1.pulse ) { marker.set('aura1_radius', Math.abs(Math.sin(rotation * (Math.PI/180))) * state.TurnMarker.aura1.size ); } else { marker.set('aura1_radius',''); } if( state.TurnMarker.aura2.pulse ) { marker.set('aura2_radius', Math.abs(Math.cos(rotation * (Math.PI/180))) * state.TurnMarker.aura2.size ); } else { marker.set('aura2_radius',''); } setTimeout(_.bind(stepAnimation,this,sync), 100); } }, checkForTokenMove = function(obj){ var turnOrder, current, marker; if(active) { turnOrder = TurnOrder.Get(); current = _.first(turnOrder); if( obj &amp;&amp; current &amp;&amp; current.id === obj.id) { threadSync++; marker = getMarker(); marker.set({ "layer": obj.get("layer"), "top": obj.get("top"), "left": obj.get("left") }); setTimeout(_.bind(stepAnimation,this,threadSync), 300); } } }, requestTurnAdvancement = function(playerid){ if(active) { let turnOrder = TurnOrder.Get(), current = getObj('graphic',_.first(turnOrder).id), character = getObj('character',(current &amp;&amp; current.get('represents'))); if(playerIsGM(playerid) || ( current &amp;&amp; ( _.contains(current.get('controlledby').split(','),playerid) || _.contains(current.get('controlledby').split(','),'all') ) ) || ( character &amp;&amp; ( _.contains(character.get('controlledby').split(','),playerid) || _.contains(character.get('controlledby').split(','),'all') ) ) ) { TurnOrder.Next(); turnOrderChange(true); } } }, announceRound = function(round){ if(state.TurnMarker.announceRounds) { sendChat( '', "/direct "+ "&lt;div style='"+ 'background-color: #4B0082;'+ 'border: 3px solid #808080;'+ 'font-size: 20px;'+ 'text-align:center;'+ 'vertical-align: top;'+ 'color: white;'+ 'font-weight:bold;'+ 'padding: 5px 5px;'+ "'&gt;"+ "&lt;img src='"+state.TurnMarker.tokenURL+"' style='width:20px; height:20px; padding: 0px 5px;' /&gt;"+ "Round "+ round + "&lt;img src='"+state.TurnMarker.tokenURL+"' style='width:20px; height:20px; padding: 0px 5px;' /&gt;"+ "&lt;/div&gt;"+ '&lt;a style="position:relative;z-index:10000; top:-1em; float: right;font-size: .6em; color: white; border: 1px solid #cccccc; border-radius: 1em; margin: 0 .1em; font-weight: bold; padding: .1em .4em;" href="!tm reset ?{Round number|0}"&gt;Reset &amp;'+'#x21ba;&lt;/a&gt;' ); } }, turnOrderChange = function(FirstTurnChanged){ var marker = getMarker(); if( !Campaign().get('initiativepage') ) { return; } var turnOrder = TurnOrder.Get(); if (!turnOrder.length) { return; } var current = _.first(turnOrder); if(state.TurnMarker.playAnimations) { threadSync++; setTimeout(_.bind(stepAnimation,this,threadSync), 300); } if (current.id === "-1") { return; } handleMarkerTurn(); if(state.TurnMarker.autoskipHidden) { TurnOrder.NextVisible(); handleMarkerTurn(); } turnOrder=TurnOrder.Get(); if(turnOrder[0].id === marker.id) { return; } current = _.first(TurnOrder.Get()); var currentToken = getObj("graphic", turnOrder[0].id), currentChar = getObj('character', (currentToken||{get:_.noop}).get('represents')); if(currentToken) { if(FirstTurnChanged) { handleAnnounceTurnChange(); } var size = Math.max(currentToken.get("height"),currentToken.get("width")) * state.TurnMarker.scale; if (marker.get("layer") === "gmlayer" &amp;&amp; currentToken.get("layer") !== "gmlayer") { marker.set({ "top": currentToken.get("top"), "left": currentToken.get("left"), "height": size, "width": size }); setTimeout(function() { marker.set({ "layer": currentToken.get("layer") }); }, 500); } else { marker.set({ "layer": currentToken.get("layer"), "top": currentToken.get("top"), "left": currentToken.get("left"), "height": size, "width": size }); } toFront(currentToken); if( 'all' === state.TurnMarker.autoPull || ('npcs' === state.TurnMarker.autoPull &amp;&amp; ( '' === currentToken.get('controlledby') &amp;&amp; ( !currentChar || '' === currentChar.get('controlledby')) )) ){ fixedSendPing(currentToken.get('left'),currentToken.get('top'),currentToken.get('pageid'),null,true); } } }, handleDestroyGraphic = function(obj){ if(TurnOrder.HasTurn(obj.id)){ let prev=JSON.parse(JSON.stringify(Campaign())); TurnOrder.RemoveTurn(obj.id); handleTurnOrderChange(Campaign(),prev); } }, handleTurnOrderChange = function(obj, prev) { var prevOrder=JSON.parse(prev.turnorder); var objOrder=JSON.parse(obj.get('turnorder')); if( _.isArray(prevOrder) &amp;&amp; _.isArray(objOrder) &amp;&amp; prevOrder.length &amp;&amp; objOrder.length &amp;&amp; objOrder[0].id !== prevOrder[0].id ) { turnOrderChange(true); } }, handleExternalTurnOrderChange = function() { var marker = getMarker(), turnorder = Campaign().get('turnorder'), markerTurn; turnorder = ('' === turnorder) ? [] : JSON.parse(turnorder); markerTurn = _.filter(turnorder, function(i){ return marker.id === i.id; })[0]; if(markerTurn.pr !== -1){ markerTurn.pr = -1; turnorder =_.union([markerTurn], _.reject(turnorder, function(i){ return marker.id === i.id || (getObj('graphic',i.id)||{get:_.noop}).get('imgsrc')===state.TurnMarker.tokenURL; })); Campaign().set('turnorder',JSON.stringify(turnorder)); } _.defer(dispatchInitiativePage); }, handleMarkerTurn = function(){ var marker = getMarker(), turnOrder = TurnOrder.Get(), round; if(turnOrder[0].id === marker.id) { round=parseInt(marker.get('bar2_value'))+1; marker.set({ name: state.TurnMarker.tokenName+' '+round, bar2_value: round }); announceRound(round); TurnOrder.Next(); } }, handleAnnounceTurnChange = function(){ if(state.TurnMarker.announceTurnChange ) { var marker = getMarker(); var turnOrder = TurnOrder.Get(); var currentToken = getObj("graphic", turnOrder[0].id); if('gmlayer' === currentToken.get('layer')) { return; } var previousTurn=_.last(_.filter(turnOrder,function(element){ var token=getObj("graphic", element.id); return token &amp;&amp; token.get('layer') !== 'gmlayer' &amp;&amp; element.id !== marker.id; })); /* find previous token. */ var previousToken = getObj("graphic", previousTurn.id); var pImage=previousToken.get('imgsrc'); var cImage=currentToken.get('imgsrc'); var pRatio=previousToken.get('width')/previousToken.get('height'); var cRatio=currentToken.get('width')/currentToken.get('height'); var pNameString="The Previous turn is done."; if(previousToken &amp;&amp; previousToken.get('showplayers_name')) { pNameString='&lt;span style=\''+ 'font-family: Baskerville, "Baskerville Old Face", "Goudy Old Style", Garamond, "Times New Roman", serif;'+ 'text-decoration: underline;'+ 'font-size: 130%;' + '\'&gt;'+ previousToken.get('name')+ '&lt;/span&gt;\'s turn is done.'; } var cNameString='The next turn has begun!'; if(currentToken &amp;&amp; currentToken.get('showplayers_name')) { cNameString='&lt;span style=\''+ 'font-family: Baskerville, "Baskerville Old Face", "Goudy Old Style", Garamond, "Times New Roman", serif;'+ 'text-decoration: underline;'+ 'font-size: 130%;'+ '\'&gt;'+ currentToken.get('name')+ '&lt;/span&gt;, it\'s now your turn!'; } var PlayerAnnounceExtra='&lt;a style="position:relative;z-index:10000; top:-1em;float: right;font-size: .6em; color: white; border: 1px solid #cccccc; border-radius: 1em; margin: 0 .1em; font-weight: bold; padding: .1em .4em;" href="!eot"&gt;EOT &amp;'+'#x21e8;&lt;/a&gt;'; if(state.TurnMarker.announcePlayerInTurnAnnounce) { var Char=currentToken.get('represents'); if(Char) { Char=getObj('character',Char); if(Char &amp;&amp; _.isFunction(Char.get)) { var Controllers=Char.get('controlledby').split(','); _.each(Controllers,function(c){ switch(c) { case 'all': PlayerAnnounceExtra+='&lt;div style="'+ 'padding: 0px 5px;'+ 'font-weight: bold;'+ 'text-align: center;'+ 'font-size: '+state.TurnMarker.announcePlayerInTurnAnnounceSize+';'+ 'border: 5px solid black;'+ 'background-color: white;'+ 'color: black;'+ 'letter-spacing: 3px;'+ 'line-height: 130%;'+ '"&gt;'+ 'All'+ '&lt;/div&gt;'; break; default: var player=getObj('player',c); if(player) { var PlayerColor=player.get('color'); var PlayerName=player.get('displayname'); PlayerAnnounceExtra+='&lt;div style="'+ 'padding: 5px;'+ 'text-align: center;'+ 'font-size: '+state.TurnMarker.announcePlayerInTurnAnnounceSize+';'+ 'background-color: '+PlayerColor+';'+ 'text-shadow: '+ '-1px -1px 1px #000,'+ ' 1px -1px 1px #000,'+ '-1px 1px 1px #000,'+ ' 1px 1px 1px #000;'+ 'letter-spacing: 3px;'+ 'line-height: 130%;'+ '"&gt;'+ PlayerName+ '&lt;/div&gt;'; } break; } }); } } } var tokenSize=70; sendChat( '', "/direct "+ "&lt;div style='border: 3px solid #808080; background-color: #4B0082; color: white; padding: 1px 1px;'&gt;"+ '&lt;div style="text-align: left; margin: 5px 5px;"&gt;'+ '&lt;a style="position:relative;z-index:1000;float:left; background-color:transparent;border:0;padding:0;margin:0;display:block;" href="!tm ping-target '+previousToken.id+'"&gt;'+ "&lt;img src='"+pImage+"' style='width:"+Math.round(tokenSize*pRatio)+"px; height:"+tokenSize+"px; padding: 0px 2px;' /&gt;"+ '&lt;/a&gt;'+ pNameString+ '&lt;/div&gt;'+ '&lt;div style="text-align: right; margin: 5px 5px; position: relative; vertical-align: text-bottom;"&gt;'+ '&lt;a style="position:relative;z-index:1000;float:right; background-color:transparent;border:0;padding:0;margin:0;display:block;" href="!tm ping-target '+currentToken.id+'"&gt;'+ "&lt;img src='"+cImage+"' style='width:"+Math.round(tokenSize*cRatio)+"px; height:"+tokenSize+"px; padding: 0px 2px;' /&gt;"+ '&lt;/a&gt;'+ '&lt;span style="position:absolute; bottom: 0;right:'+Math.round((tokenSize*cRatio)+6)+'px;"&gt;'+ cNameString+ '&lt;/span&gt;'+ '&lt;div style="clear:both;"&gt;&lt;/div&gt;'+ '&lt;/div&gt;'+ PlayerAnnounceExtra+ '&lt;div style="clear:both;"&gt;&lt;/div&gt;'+ "&lt;/div&gt;" ); } }, resetMarker = function() { active=false; threadSync++; var marker = getMarker(); marker.set({ layer: "gmlayer", aura1_radius: '', aura2_radius: '', left: 35, top: 35, height: 70, width: 70, rotation: 0, bar1_value: 0 }); }, startMarker = function() { var marker = getMarker(); if(state.TurnMarker.playAnimations &amp;&amp; state.TurnMarker.aura1.pulse) { marker.set({ aura1_radius: state.TurnMarker.aura1.size, aura1_color: state.TurnMarker.aura1.color }); } if(state.TurnMarker.playAnimations &amp;&amp; state.TurnMarker.aura2.pulse) { marker.set({ aura2_radius: state.TurnMarker.aura2.size, aura2_color: state.TurnMarker.aura2.color }); } active=true; stepAnimation(threadSync); turnOrderChange(true); }, dispatchInitiativePage = function(){ if( !Campaign().get('initiativepage') ) { resetMarker(); } else { startMarker(); } }, registerEventHandlers = function(){ on("change:campaign:initiativepage", dispatchInitiativePage ); on("change:campaign:turnorder", handleTurnOrderChange ); on("change:graphic:lastmove", checkForTokenMove ); on("destroy:graphic", handleDestroyGraphic ); on("chat:message", handleInput ); dispatchInitiativePage(); } ; return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers, TurnOrderChange: handleExternalTurnOrderChange }; }()); on("ready",function(){ 'use strict'; TurnMarker.CheckInstall(); TurnMarker.RegisterEventHandlers(); }); var TurnOrder = TurnOrder || (function() { "use strict"; return { Get: function(){ var to=Campaign().get("turnorder"); to=(''===to ? '[]' : to); return JSON.parse(to); }, Set: function(turnOrder){ Campaign().set({turnorder: JSON.stringify(turnOrder)}); }, Next: function(){ this.Set(TurnOrder.Get().rotate(1)); if("undefined" !== typeof Mark &amp;&amp; _.has(Mark,'Reset') &amp;&amp; _.isFunction(Mark.Reset)) { Mark.Reset(); } }, NextVisible: function(){ var turns=this.Get(); var context={skip: 0}; var found=_.find(turns,function(element){ var token=getObj("graphic", element.id); if( (undefined !== token) &amp;&amp; (token.get('layer')!=='gmlayer') ) { return true; } else { this.skip++; } },context); if(undefined !== found &amp;&amp; context.skip&gt;0) { this.Set(turns.rotate(context.skip)); } }, HasTurn: function(id){ return (_.filter(this.Get(),function(turn){ return id === turn.id; }).length !== 0); }, AddTurn: function(entry){ var turnorder = this.Get(); turnorder.push(entry); this.Set(turnorder); }, RemoveTurn: function(id){ this.Set(_.reject(this.Get(),(o)=&gt;o.id===id)); } }; }()); Object.defineProperty(Array.prototype, 'rotate', { enumerable: false, writable: true }); Array.prototype.rotate = (function() { "use strict"; var unshift = Array.prototype.unshift, splice = Array.prototype.splice; return function(count) { var len = this.length &gt;&gt;&gt; 0; count = count &gt;&gt; 0; unshift.apply(this, splice.call(this, count % len, len)); return this; }; }());
That is smashing! Gorgeous! Buttery-smooth, no hitching, no stutter - I love it!
1549507434
The Aaron
Roll20 Production Team
API Scripter
Right?&nbsp; It was a great idea.&nbsp; Now I just need to finish the rewrite and make an official version.
1549518843
Loren the GM
Pro
Marketplace Creator
That looks great The Aaron! Can't wait to see your rewrite!
Looks great, thanks!
The round counter seems to have lost its graphic in that change. It used to be a small version of the token marker. Maybe animated png doesn't work for things inside the turn order window, or the chat window?
1549845309
The Aaron
Roll20 Production Team
API Scripter
Yeah, It's that webm is not a valid image format and it was using the same image in both. the token and the chat.&nbsp; I'll fix it before I release it.
When I try using this one, it causes a lot of flickering of the map's surrounding grid lines, particularly when on larger tokens.&nbsp; I've had to revert to the old edition to spare my eyes the lightshow.
1551145476
The Aaron
Roll20 Production Team
API Scripter
That’s a bummer. I’ll have to see if I can duplicate that. What OS and Browser?
1551154960

Edited 1551154989
GM Michael
API Scripter
Windows and Chrome :/&nbsp; Several of my players witnessed the same behavior.&nbsp; Strangely, the flickering continued even after deleting the token.&nbsp; I had to force a reload of the page to actually get rid of it.&nbsp; It might just be a generic roll20 bug for all I know.
First off, thanks so much for doing this Aaron! The second they announced animations I knew turn markers would be an awesome application, and you totally delivered! I'm really considering switching over from Tracker Jacker, but it sounds like custom markers aren't supported yet and changing the token URL doesn't seem to work either. Am I being silly here and missing a step, or is this something we're gonna have to wait a little longer on?
1552258398
The Aaron
Roll20 Production Team
API Scripter
I have a new version in the works, but it doesn’t have custom statuses. I do have some ideas for that, but I’ve not had the time to devote to it that I did in the past.&nbsp;