Skip to content

Instantly share code, notes, and snippets.

@nabbynz
Last active March 16, 2025 21:29
Show Gist options
  • Save nabbynz/9ca59e661ea4577a6087835a93cd184d to your computer and use it in GitHub Desktop.
Save nabbynz/9ca59e661ea4577a6087835a93cd184d to your computer and use it in GitHub Desktop.
TagPro Telestrator
// ==UserScript==
// @name TagPro Telestrator
// @version 2.3.0
// @description Use a telestrator while spectating TagPro!
// @include *.koalabeast.com/game
// @include *.koalabeast.com/game?*
// @updateURL https://gist.github.com/nabbynz/9ca59e661ea4577a6087835a93cd184d/TagPro Telestrator.user.js
// @downloadURL https://gist.github.com/nabbynz/9ca59e661ea4577a6087835a93cd184d/TagPro Telestrator.user.js
// @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @grant none
// @author BBQchicken
// ==/UserScript==
console.log('START: ' + GM_info.script.name + ' (v' + GM_info.script.version + ' by ' + GM_info.script.author + ')');
/* globals tagpro, tagproConfig, PIXI */
/* eslint-disable no-multi-spaces */
/* eslint-disable dot-notation */
tagpro.ready(function init() {
if (!tagpro.playerId || !tagpro.renderer.layers.background || (tagpro.state !== 1 && tagpro.state !== 5)) {
return setTimeout(init, 200);
}
if (!tagpro.spectator) {
return false;
}
var traceLayer = tagpro.renderer.layers.midground;
var drawingLayer = tagpro.renderer.layers.foreground;
var redBallColor = 0xFF0000;
var blueBallColor = 0x0000FF;
// ---------- OPTIONS ---------- \\
var kickClick = false;
var traceLength = Infinity;
var blinkTime = 250;
var circleColor = 0xFF6600;
var pathColor = 0xFFFF00;
var arrowColor = 0xC800FA;
var autoTrace = true;
var traceDelay = 2000;
var matchBallColors = true;
// ---------- PATH -------------- \\
var dashOn = true;
function Path(start, color, alpha, dashed, layer) {
this.points = [start, start, start];
this.color = color;
this.alpha = alpha || 0.6;
this.dashed = dashed;
this.graphics = new PIXI.Graphics();
this.layer = layer || drawingLayer;
this.layer.addChild(this.graphics);
}
Path.prototype.update = function(point) {
var points = this.points;
this.graphics.lineStyle(6, this.color, this.alpha);
var from = points.shift();
points.push(point);
this.graphics.moveTo(from.x, from.y);
// this.graphics.quadraticCurveTo(points[0].x, points[0].y, points[1].x, points[1].y);
// this.graphics.bezierCurveTo(points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y);
(!this.dashed || dashOn) && this.graphics.lineTo(points[0].x, points[0].y);
}
Path.prototype.clear = function(delayMS) {
var delay = delayMS || 0;
var graphics = this.graphics;
var layer = this.layer;
setTimeout(function() {
graphics.clear();
layer.removeChild(graphics);
}, delayMS);
}
// ---------- TRACE -------------- \\
function Trace(player, auto) {
this.player = player;
this.path = new Path({x: player.x + 20, y: player.y + 20}, player.team === 1 ? redBallColor : blueBallColor, 0.4, true, traceLayer);
this.active = true;
this.flaccid = auto;
var thisThing = this; //because javascript scope rules
this.flaccid && setTimeout(function() { thisThing.flaccid = false; }, 3000);
}
Trace.prototype.update = function() {
this.active && this.path.update({x: this.player.x + 20, y: this.player.y + 20});
};
Trace.prototype.clear = function(delayMS) {
this.path.clear(this.flaccid ? 0 : delayMS);
};
Trace.prototype.stop = function() {
this.active = false;
}
// ---------- CIRCLE -------------- \\
function Circle(center, color) {
this.radius = 0;
this.color = color || 0;
this.center = center;
this.graphics = new PIXI.Graphics();
drawingLayer.addChild(this.graphics);
}
Circle.prototype.update = function(point) {
this.radius = Math.sqrt(Math.pow((point.x - this.center.x), 2) + Math.pow(point.y - this.center.y, 2));
this.graphics.clear();
this.graphics.lineStyle(10, this.color, 0.6);
this.graphics.drawCircle(this.center.x, this.center.y, this.radius);
}
Circle.prototype.clear = function() {
this.graphics.clear();
drawingLayer.removeChild(this.graphics);
}
// ---------- ARROW -------------- \\
function Arrow(start, color) {
console.log("Arrow constructor start");
this.start = new PIXI.Point(start.x, start.y);
this.wingLength = 45;
this.headAngle = .4;
this.leftWing = this.start.clone();
this.rightWing = this.start.clone();
this.angle = 0;
this.color = color || 0;
console.log("Arrow Graphics creation");
this.graphics = new PIXI.Graphics();
drawingLayer.addChild(this.graphics);
console.log("Arrow constructor end");
}
Arrow.prototype.rotateHead = function(end) {
console.log("Arrow rotate right wing");
var phiRight = this.angle + this.headAngle;
this.rightWing.x = end.x - this.wingLength * Math.cos(phiRight);
this.rightWing.y = end.y - this.wingLength * Math.sin(phiRight);
console.log("Arrow rotate left wing");
var phiLeft = this.angle - this.headAngle;
this.leftWing.x = end.x - this.wingLength * Math.cos(phiLeft);
this.leftWing.y = end.y - this.wingLength * Math.sin(phiLeft);
}
Arrow.prototype.draw = function(end) {
console.log("Arrow draw begin");
this.graphics.clear();
this.graphics.lineStyle(10, this.color, .6);
this.graphics.moveTo(this.start.x, this.start.y);
this.graphics.lineTo(end.x, end.y);
this.graphics.moveTo(this.rightWing.x, this.rightWing.y);
this.graphics.lineTo(end.x, end.y);
this.graphics.lineTo(this.leftWing.x, this.leftWing.y);
console.log("Arrow draw end");
}
Arrow.prototype.update = function(end) {
console.log("Arrow update begin");
//this.end = end;
this.angle = Math.atan2(end.y - this.start.y, end.x - this.start.x);
this.rotateHead(end);
this.draw(end);
console.log("Arrow update end");
}
Arrow.prototype.clear = function() {
this.graphics.clear();
drawingLayer.removeChild(this.graphics);
}
// ---------- LOGIC -------------- \\
var current = null, drawing = false, drawings = [], traces = {};
var shift = false, alt = false;
var stage = tagpro.renderer.stage;
function addUniqueTrace(player, auto) {
console.log('trace construction attempted');
if (!traces[player.id]) {
traces[player.id] = new Trace(player, auto || false);
console.log('trace construction succeeded')
}
}
function removeTrace(id) {
if (!traces[id]) {
return false;
}
traces[id].stop();
traces[id].clear(traceDelay);
setTimeout(function() { delete traces[id]; }, traceDelay);
}
tagpro.socket.on('p', function (event) {
var events = (tagproConfig.serverHost.search("map") !== -1) ? event.u : event;
for (var idx in events) {
var e = events[idx];
if ((typeof(e.flag) !== 'undefined') && e.flag) {
autoTrace && addUniqueTrace(tagpro.players[e.id], true);
} else if((typeof(e['s-pops']) !== 'undefined') || (typeof(e['s-captures']) !== 'undefined')) {
removeTrace(e.id);
}
}
});
tpKick = tagpro.kick.player;
tagpro.kick.player = function (player) {
console.log("kick.player called");
var shiftAlt = (alt && shift);
if (kickClick || !(tagpro.spectator || shiftAlt)) {
tpKick(player);
}
if (shiftAlt) {
console.log("trace added");
addUniqueTrace(player, false);
}
}
$(document).on("keydown keyup", function (event) {
shift = event.shiftKey;
alt = event.altKey;
});
$(document).dblclick(function(event) {
window.getSelection().removeAllRanges();
for (var i in drawings) {
drawings[i].clear();
}
drawings = [];
if (!event.shiftKey) {
return false;
}
for (var i in traces) {
traces[i] && traces[i].clear();
}
traces = {};
});
//thanks to ProfessorTag
function canvasMousePosition(e) {
var tr = tagpro.renderer;
var resizeScaleFactor = tr.options.disableViewportScaling ? 1 : (tr.vpHeight / tr.canvas_height).toFixed(2),
scale = (tagpro.zoom / resizeScaleFactor),
x1 = tr.gameContainer.x * scale,
x2 = x1 - tr.vpWidth * scale,
x = - Math.round((x1 + x2) / 2 - (e.x - innerWidth / 2) * scale),
y1 = tr.gameContainer.y * scale,
y2 = y1 - tr.vpHeight * scale,
y = - Math.round((y1 + y2) / 2 - (e.y - innerHeight / 2) * scale);
return {x: x, y: y};
}
if (matchBallColors) {
let canvas = new OffscreenCanvas(80, 40);
let ctx = canvas.getContext("2d");
ctx.drawImage(tagpro.tiles.image, 560,0,80,40, 0,0,80,40);
let rgbToHex = function(r, g, b) {
return "0x" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
};
let getAverageColor = function(data, asHex=false, ignoreGrey=false, pixelInterval=1) {
let rgb = { r:null, g:null, b:null };
let length = data.length;
let count = 0;
let i = -4;
while ((i += pixelInterval * 4) < length) {
//let b = data[i] * 0.299 + data[i+1] * 0.587 + data[i+2] * 0.114;
let isGrey = ignoreGrey && (Math.abs(data[i] - data[i+1]) + Math.abs(data[i+1] - data[i+2]) < 10);
if (!isGrey && data[i+3]) {
rgb.r += data[i];
rgb.g += data[i+1];
rgb.b += data[i+2];
count++;
}
}
if (count === 0) return null; //transparent or all grey
rgb.r = Math.floor(rgb.r / count);
rgb.g = Math.floor(rgb.g / count);
rgb.b = Math.floor(rgb.b / count);
return rgbToHex(rgb.r, rgb.g, rgb.b).slice(0, 8);
};
let ballPixelData = ctx.getImageData(5, 5, 30, 30).data;
redBallColor = getAverageColor(ballPixelData, true, true) || redBallColor;
ballPixelData = ctx.getImageData(45, 5, 30, 30).data;
blueBallColor = getAverageColor(ballPixelData, true, true) || blueBallColor;
}
window.onmousedown = function (click) {
drawing = true;
}
window.onmousemove = function(click) {
if (!drawing) {
return false;
} else if (current) {
current.update(canvasMousePosition(click));
} else {
if (shift && alt) {
drawing = false;
} else if (shift) {
current = new Arrow(canvasMousePosition(click), arrowColor);
} else if (alt) {
current = new Circle(canvasMousePosition(click), circleColor);
} else {
current = new Path(canvasMousePosition(click), pathColor);
}
}
}
window.onmouseup = function(click) {
console.log("mouseup");
drawing = false;
current && drawings.push(current);
current = null;
}
function drawAll() {
for (var idx in traces) {
trace = traces[idx];
trace && trace.update();
}
requestAnimationFrame(drawAll);
}
requestAnimationFrame(drawAll);
setInterval(function() {
dashOn = !dashOn;
}, blinkTime);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment