Compare commits
10
Commits
f75c8b9d3c
...
dad5eef9aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad5eef9aa | ||
|
|
425919e9a3 | ||
|
|
31a8f85e54 | ||
|
|
5b3094bd4f | ||
|
|
eaca56c07f | ||
|
|
c4cfd59613 | ||
|
|
63a7fbac9b | ||
|
|
358ed9ad5b | ||
|
|
cf536e18d6 | ||
|
|
64d0d29a7c |
@@ -0,0 +1 @@
|
||||
TODO
|
||||
@@ -1,3 +1,3 @@
|
||||
# Basic Aura Highlighting
|
||||
|
||||
A small, FoundryVTT Pathfinder 2e/Starfinder 2e module that places a highlight around tokens in another token's aura.
|
||||
A module that places a highlight around tokens in another token's aura.
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
"name": "Refresh During Movement",
|
||||
"hint": "If disabled, aura highlights will only refresh when a token stops moving or its settings are updated. This can improve performance."
|
||||
},
|
||||
"thickness": {
|
||||
"name": "Highlight Thickness",
|
||||
"hint": "The thickness of the aura highlights in pixels (1-100)."
|
||||
},
|
||||
"name": {
|
||||
"name": "Aura Name"
|
||||
},
|
||||
@@ -26,6 +30,13 @@
|
||||
"to": "To",
|
||||
"edge": "Edge",
|
||||
"center": "Center"
|
||||
},
|
||||
"walls": {
|
||||
"name": "Restricted by Walls",
|
||||
"hint": "If enabled, the aura will be blocked by walls (Line of Sight). GM Only."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-13
@@ -1,28 +1,19 @@
|
||||
{
|
||||
"id": "basic_aura_highlighting",
|
||||
"title": "Basic Aura Highlighting",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"verified": "13",
|
||||
"maximum": "14"
|
||||
},
|
||||
"description": "<p>A module specifically for Pathfinder 2e that places a highlight around tokens in another token's aura. This aura can be measured from the center of the tokens or edge to edge. See the token or prototype token menus for configuration.</p>",
|
||||
"relationships": {
|
||||
"systems": [
|
||||
{
|
||||
"id": "pf2e",
|
||||
"manifest": "https://github.com/foundryvtt/pf2e/releases/latest/download/system.json",
|
||||
"compatibility": {
|
||||
"minimum": "6.0.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "<p>A module that places a highlight around tokens in another token's aura. This aura can be measured from the center of the tokens or edge to edge. See the token or prototype token menus for configuration.</p>",
|
||||
"flags": {
|
||||
"canUpload": true
|
||||
},
|
||||
"url": "https://codeberg.org/Grimsace/basic_aura_highlighting",
|
||||
"manifest": "https://codeberg.org/Grimsace/basic_aura_highlighting/raw/branch/main/module.json",
|
||||
"download": "https://codeberg.org/Grimsace/basic_aura_highlighting/archive/main.zip",
|
||||
"esmodules": ["scripts/module.js"],
|
||||
"styles": ["styles/module.css"],
|
||||
"languages": [
|
||||
|
||||
+242
-81
@@ -1,12 +1,17 @@
|
||||
// Basic Aura Highlighting
|
||||
// A module for Foundry VTT that highlights tokens within specified aura ranges.
|
||||
|
||||
const MODULE_ID = "basic_aura_highlighting";
|
||||
const AURAS_TAB = "basic-aura-highlighting-auras";
|
||||
const AURAS_FLAG_PATH = `flags.${MODULE_ID}.auras`;
|
||||
const HIGHLIGHT_CONTAINER_NAME = `${MODULE_ID}.highlights`;
|
||||
const HIGHLIGHT_LINE_WIDTH = 4;
|
||||
const HIGHLIGHT_RING_SPACING = 5;
|
||||
const HIGHLIGHT_ALPHA_THRESHOLD = 0.2;
|
||||
const ALPHA_SAMPLE_SIZE = 48;
|
||||
const HIGHLIGHT_FILTER_MARKER = Symbol(MODULE_ID);
|
||||
const THICKNESS_SETTING = "thickness";
|
||||
|
||||
// Shader for drawing an outline around the token's non-transparent pixels
|
||||
const ALPHA_OUTLINE_FRAGMENT_SHADER = `
|
||||
varying vec2 vTextureCoord;
|
||||
uniform sampler2D uSampler;
|
||||
@@ -48,6 +53,7 @@ const alphaShapeCache = new Map();
|
||||
|
||||
const REFRESH_DURING_MOVE_SETTING = "refreshDuringMove";
|
||||
|
||||
// Initialization: Register settings and hooks
|
||||
Hooks.once("init", () => {
|
||||
game.settings.register(MODULE_ID, REFRESH_DURING_MOVE_SETTING, {
|
||||
name: localize("settings.refreshDuringMove.name"),
|
||||
@@ -58,16 +64,35 @@ Hooks.once("init", () => {
|
||||
default: true,
|
||||
onChange: scheduleAuraHighlightRefresh,
|
||||
});
|
||||
|
||||
game.settings.register(MODULE_ID, THICKNESS_SETTING, {
|
||||
name: localize("settings.thickness.name"),
|
||||
hint: localize("settings.thickness.hint"),
|
||||
scope: "client",
|
||||
config: true,
|
||||
type: Number,
|
||||
range: {
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
},
|
||||
default: 4,
|
||||
onChange: scheduleAuraHighlightRefresh,
|
||||
});
|
||||
});
|
||||
|
||||
Hooks.on("renderTokenConfig", injectAurasTab);
|
||||
Hooks.on("renderPrototypeTokenConfig", injectAurasTab);
|
||||
|
||||
// Data parsing hooks
|
||||
Hooks.on("preUpdateToken", (_token, changes) =>
|
||||
parseAurasInChanges(changes, AURAS_FLAG_PATH),
|
||||
);
|
||||
Hooks.on("preUpdateActor", (_actor, changes) =>
|
||||
parseAurasInChanges(changes, `prototypeToken.${AURAS_FLAG_PATH}`),
|
||||
);
|
||||
|
||||
// Refresh hooks
|
||||
Hooks.on("canvasReady", scheduleAuraHighlightRefresh);
|
||||
Hooks.on("canvasTearDown", clearAuraHighlights);
|
||||
Hooks.on("controlToken", scheduleAuraHighlightRefresh);
|
||||
@@ -89,6 +114,7 @@ Hooks.on("refreshToken", (token) => {
|
||||
Hooks.on("createToken", scheduleAuraHighlightRefresh);
|
||||
Hooks.on("deleteToken", scheduleAuraHighlightRefresh);
|
||||
|
||||
// UI Injection: Add the "Auras" tab to Token Configuration
|
||||
function injectAurasTab(app, element) {
|
||||
const root = getRootElement(element);
|
||||
if (!root || root.querySelector(`[data-tab="${AURAS_TAB}"]`)) return;
|
||||
@@ -167,6 +193,7 @@ function createAurasTabPanel(group, auras) {
|
||||
return panel;
|
||||
}
|
||||
|
||||
// Event handling for the Auras configuration tab
|
||||
function activateAurasControls(root, group, panel) {
|
||||
const form = root.querySelector("form");
|
||||
const aurasTabButton = root.querySelector(
|
||||
@@ -224,14 +251,23 @@ function renderAuras(panel, auras) {
|
||||
function renderAura(aura, index) {
|
||||
const id = escapeHtml(aura.id);
|
||||
const nameLabel = localize("settings.name.name");
|
||||
const rangeLabel = localize("settings.radius.name");
|
||||
const rangeLabel = `${localize("settings.radius.name")} (Scene Units)`;
|
||||
const colorLabel = localize("settings.color.name");
|
||||
const fromLabel = localize("settings.measurement.from");
|
||||
const toLabel = localize("settings.measurement.to");
|
||||
const wallsLabel = localize("settings.walls.name");
|
||||
const enabledLabel = localize("settings.enabled.name");
|
||||
const isGM = game.user.isGM;
|
||||
|
||||
return `
|
||||
<fieldset class="basic-aura-highlighting__aura" data-aura-id="${id}">
|
||||
<legend>${escapeHtml(aura.name || defaultAuraName(index))}</legend>
|
||||
<div class="form-group">
|
||||
<label for="${id}-enabled">${enabledLabel}</label>
|
||||
<div class="form-fields">
|
||||
<input id="${id}-enabled" type="checkbox" ${aura.enabled ? "checked" : ""} data-aura-field="enabled">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="${id}-name">${nameLabel}</label>
|
||||
<div class="form-fields">
|
||||
@@ -243,7 +279,6 @@ function renderAura(aura, index) {
|
||||
<div class="form-fields">
|
||||
<input id="${id}-range" type="number" min="0" step="0.5" value="${aura.range}" data-aura-field="range">
|
||||
</div>
|
||||
<p class="hint">${localize("settings.radius.hint")}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="${id}-color">${colorLabel}</label>
|
||||
@@ -269,6 +304,12 @@ function renderAura(aura, index) {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="${id}-walls">${wallsLabel}</label>
|
||||
<div class="form-fields">
|
||||
<input id="${id}-walls" type="checkbox" ${aura.walls ? "checked" : ""} data-aura-field="walls" ${isGM ? "" : "disabled"}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="button" class="basic-aura-highlighting__delete" data-action="delete-aura">
|
||||
<i class="fa-solid fa-trash"></i> ${localize("tabs.delete")}
|
||||
@@ -278,6 +319,7 @@ function renderAura(aura, index) {
|
||||
`;
|
||||
}
|
||||
|
||||
// Data Management: Parsing and normalizing aura flags
|
||||
function readAuras(panel) {
|
||||
return Array.from(panel.querySelectorAll("[data-aura-id]")).map((item) => ({
|
||||
id: item.dataset.auraId,
|
||||
@@ -290,6 +332,8 @@ function readAuras(panel) {
|
||||
to: normalizeMeasurementMode(
|
||||
item.querySelector("[data-aura-field='to']")?.value,
|
||||
),
|
||||
walls: !!item.querySelector("[data-aura-field='walls']")?.checked,
|
||||
enabled: !!item.querySelector("[data-aura-field='enabled']")?.checked,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -307,6 +351,8 @@ function normalizeAura(aura = {}) {
|
||||
color: typeof aura.color === "string" ? aura.color : "#ff0000",
|
||||
from: normalizeMeasurementMode(aura.from ?? legacyMeasurement),
|
||||
to: normalizeMeasurementMode(aura.to ?? legacyMeasurement),
|
||||
walls: aura.walls ?? true,
|
||||
enabled: aura.enabled ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -351,6 +397,9 @@ function parseAuras(value) {
|
||||
}
|
||||
}
|
||||
|
||||
// Rendering Logic: Highlights and calculations
|
||||
|
||||
// Debounce highlight refreshes to avoid excessive calculations
|
||||
function scheduleAuraHighlightRefresh() {
|
||||
if (pendingHighlightRefresh !== null) return;
|
||||
|
||||
@@ -422,131 +471,228 @@ function clearHighlightContainer(container) {
|
||||
}
|
||||
}
|
||||
|
||||
// Distance Calculation Logic
|
||||
|
||||
// Find which tokens are within range of which auras
|
||||
function collectAuraHighlights(sourceTokens) {
|
||||
const highlights = new Map();
|
||||
const tokens = globalThis.canvas.tokens.placeables;
|
||||
const pixelsPerUnit = getPixelsPerSceneUnit();
|
||||
|
||||
// Cache token-specific data for the duration of this refresh
|
||||
const tokenCache = new Map();
|
||||
|
||||
for (const source of sourceTokens) {
|
||||
const auras = getTokenAuras(source);
|
||||
for (const target of globalThis.canvas.tokens.placeables) {
|
||||
if (auras.length === 0) continue;
|
||||
|
||||
const sourceData = getCachedTokenData(source, tokenCache, pixelsPerUnit);
|
||||
|
||||
for (const target of tokens) {
|
||||
if (target === source || target.document.hidden) continue;
|
||||
|
||||
for (const aura of auras) {
|
||||
if (!isTokenInAura(source, target, aura)) continue;
|
||||
const targetData = getCachedTokenData(target, tokenCache, pixelsPerUnit);
|
||||
const dz = Math.abs(sourceData.elevation - targetData.elevation);
|
||||
const dz2 = dz * dz;
|
||||
|
||||
for (const aura of auras) {
|
||||
if (!aura.enabled) continue;
|
||||
|
||||
const rangePixels = aura.range * pixelsPerUnit;
|
||||
const rangePixels2 = rangePixels * rangePixels;
|
||||
|
||||
// Broad phase: check if elevation difference alone exceeds range
|
||||
if (dz2 > rangePixels2) continue;
|
||||
|
||||
if (
|
||||
isTokenInAuraCached(
|
||||
source,
|
||||
target,
|
||||
aura,
|
||||
sourceData,
|
||||
targetData,
|
||||
dz2,
|
||||
rangePixels2,
|
||||
)
|
||||
) {
|
||||
const targetHighlights = highlights.get(target.id) ?? [];
|
||||
targetHighlights.push(aura);
|
||||
highlights.set(target.id, targetHighlights);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return highlights;
|
||||
}
|
||||
|
||||
function getCachedTokenData(token, cache, pixelsPerUnit) {
|
||||
let data = cache.get(token.id);
|
||||
if (data) return data;
|
||||
|
||||
data = {
|
||||
center: token.center,
|
||||
bounds: token.bounds,
|
||||
elevation: (token.document?.elevation ?? 0) * pixelsPerUnit,
|
||||
alphaPoints: null, // Lazy loaded
|
||||
};
|
||||
cache.set(token.id, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
function getTokenAuras(token) {
|
||||
return normalizeAuras(
|
||||
parseAuras(token.document?.getFlag?.(MODULE_ID, "auras")),
|
||||
);
|
||||
}
|
||||
|
||||
function isTokenInAura(source, target, aura) {
|
||||
const rangePixels = aura.range * getPixelsPerSceneUnit();
|
||||
return measureAuraDistance(source, target, aura) <= rangePixels;
|
||||
}
|
||||
function isTokenInAuraCached(
|
||||
source,
|
||||
target,
|
||||
aura,
|
||||
sourceData,
|
||||
targetData,
|
||||
dz2,
|
||||
rangePixels2,
|
||||
) {
|
||||
// Broad phase: Bounding box check
|
||||
const sourceBounds = sourceData.bounds;
|
||||
const targetBounds = targetData.bounds;
|
||||
|
||||
function measureCenterDistance(source, target) {
|
||||
const dx = source.center.x - target.center.x;
|
||||
const dy = source.center.y - target.center.y;
|
||||
const dz = getElevationOffset(source, target);
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
function measureAuraDistance(source, target, aura) {
|
||||
const sourcePoints = getMeasurementPoints(source, aura.from);
|
||||
const targetPoints = getMeasurementPoints(target, aura.to);
|
||||
if (sourcePoints.length === 0 || targetPoints.length === 0)
|
||||
return measureBoundsDistance(source, target, aura);
|
||||
|
||||
return measurePointSetDistance(sourcePoints, targetPoints, source, target);
|
||||
}
|
||||
|
||||
function measureBoundsDistance(source, target, aura) {
|
||||
const dz = getElevationOffset(source, target);
|
||||
let d2d = 0;
|
||||
if (aura.from === "center" && aura.to === "center")
|
||||
return measureCenterDistance(source, target);
|
||||
if (aura.from === "center")
|
||||
d2d = measurePointToBoundsDistance(source.center, target.bounds);
|
||||
else if (aura.to === "center")
|
||||
d2d = measurePointToBoundsDistance(target.center, source.bounds);
|
||||
else d2d = measureBoundsEdgeDistance(source, target);
|
||||
|
||||
return Math.sqrt(d2d * d2d + dz * dz);
|
||||
}
|
||||
|
||||
function measureBoundsEdgeDistance(source, target) {
|
||||
const alphaDistance = measureAlphaEdgeDistance(source, target);
|
||||
if (Number.isFinite(alphaDistance)) return alphaDistance;
|
||||
|
||||
const sourceBounds = source.bounds;
|
||||
const targetBounds = target.bounds;
|
||||
const dx = Math.max(
|
||||
const dxBox = Math.max(
|
||||
sourceBounds.x - targetBounds.right,
|
||||
targetBounds.x - sourceBounds.right,
|
||||
0,
|
||||
);
|
||||
const dy = Math.max(
|
||||
const dyBox = Math.max(
|
||||
sourceBounds.y - targetBounds.bottom,
|
||||
targetBounds.y - sourceBounds.bottom,
|
||||
0,
|
||||
);
|
||||
return Math.hypot(dx, dy);
|
||||
|
||||
if (dxBox * dxBox + dyBox * dyBox + dz2 > rangePixels2) return false;
|
||||
|
||||
// Narrow phase: Distance calculation
|
||||
const d2d2 = measureAuraDistanceSquared(
|
||||
source,
|
||||
target,
|
||||
aura,
|
||||
sourceData,
|
||||
targetData,
|
||||
);
|
||||
if (d2d2 + dz2 > rangePixels2) return false;
|
||||
|
||||
// Wall restriction check
|
||||
if (aura.walls) {
|
||||
const isBlocked = CONFIG.Canvas.polygonBackends.sight.testCollision(
|
||||
sourceData.center,
|
||||
targetData.center,
|
||||
{ mode: "any", type: "sight" },
|
||||
);
|
||||
if (isBlocked) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function measurePointToBoundsDistance(point, bounds) {
|
||||
// Distance from center to center (squared)
|
||||
function measureCenterDistance2(sourceData, targetData) {
|
||||
const dx = sourceData.center.x - targetData.center.x;
|
||||
const dy = sourceData.center.y - targetData.center.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
// Main distance entry point (squared)
|
||||
function measureAuraDistanceSquared(
|
||||
source,
|
||||
target,
|
||||
aura,
|
||||
sourceData,
|
||||
targetData,
|
||||
) {
|
||||
if (aura.from === "center" && aura.to === "center") {
|
||||
return measureCenterDistance2(sourceData, targetData);
|
||||
}
|
||||
|
||||
const sourcePoints = getMeasurementPointsCached(
|
||||
source,
|
||||
aura.from,
|
||||
sourceData,
|
||||
);
|
||||
const targetPoints = getMeasurementPointsCached(target, aura.to, targetData);
|
||||
|
||||
if (sourcePoints.length === 0 || targetPoints.length === 0) {
|
||||
return measureBoundsDistance2(sourceData, targetData, aura);
|
||||
}
|
||||
|
||||
return measurePointSetDistance2(sourcePoints, targetPoints);
|
||||
}
|
||||
|
||||
// Fallback distance calculation using token bounds (squared)
|
||||
function measureBoundsDistance2(sourceData, targetData, aura) {
|
||||
if (aura.from === "center")
|
||||
return measurePointToBoundsDistance2(sourceData.center, targetData.bounds);
|
||||
if (aura.to === "center")
|
||||
return measurePointToBoundsDistance2(targetData.center, sourceData.bounds);
|
||||
|
||||
const dx = Math.max(
|
||||
sourceData.bounds.x - targetData.bounds.right,
|
||||
targetData.bounds.x - sourceData.bounds.right,
|
||||
0,
|
||||
);
|
||||
const dy = Math.max(
|
||||
sourceData.bounds.y - targetData.bounds.bottom,
|
||||
targetData.bounds.y - sourceData.bounds.bottom,
|
||||
0,
|
||||
);
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
function measurePointToBoundsDistance2(point, bounds) {
|
||||
const dx = Math.max(bounds.x - point.x, point.x - bounds.right, 0);
|
||||
const dy = Math.max(bounds.y - point.y, point.y - bounds.bottom, 0);
|
||||
return Math.hypot(dx, dy);
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
function measureAlphaEdgeDistance(source, target) {
|
||||
const sourcePoints = getTokenAlphaEdgePoints(source);
|
||||
const targetPoints = getTokenAlphaEdgePoints(target);
|
||||
if (sourcePoints.length === 0 || targetPoints.length === 0) return Number.NaN;
|
||||
function getMeasurementPointsCached(token, mode, tokenData) {
|
||||
if (mode === "center") return [tokenData.center];
|
||||
|
||||
return measurePointSetDistance(sourcePoints, targetPoints, source, target);
|
||||
if (tokenData.alphaPoints === null) {
|
||||
tokenData.alphaPoints = getTokenAlphaEdgePoints(token);
|
||||
}
|
||||
return tokenData.alphaPoints;
|
||||
}
|
||||
|
||||
function getMeasurementPoints(token, mode) {
|
||||
if (mode === "center") return [token.center];
|
||||
|
||||
const alphaPoints = getTokenAlphaEdgePoints(token);
|
||||
if (alphaPoints.length > 0) return alphaPoints;
|
||||
return [];
|
||||
}
|
||||
|
||||
function measurePointSetDistance(sourcePoints, targetPoints, source, target) {
|
||||
// Find the minimum distance squared between two sets of points
|
||||
function measurePointSetDistance2(sourcePoints, targetPoints) {
|
||||
let minDistanceSquared = Infinity;
|
||||
for (const sourcePoint of sourcePoints) {
|
||||
for (const targetPoint of targetPoints) {
|
||||
const dx = sourcePoint.x - targetPoint.x;
|
||||
const dy = sourcePoint.y - targetPoint.y;
|
||||
const distanceSquared = dx * dx + dy * dy;
|
||||
if (distanceSquared < minDistanceSquared)
|
||||
minDistanceSquared = distanceSquared;
|
||||
for (let i = 0, lenS = sourcePoints.length; i < lenS; i++) {
|
||||
const sP = sourcePoints[i];
|
||||
for (let j = 0, lenT = targetPoints.length; j < lenT; j++) {
|
||||
const tP = targetPoints[j];
|
||||
const dx = sP.x - tP.x;
|
||||
const dy = sP.y - tP.y;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 < minDistanceSquared) {
|
||||
minDistanceSquared = d2;
|
||||
// Optimization: Distance can't be less than 0
|
||||
if (minDistanceSquared === 0) return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const dz = getElevationOffset(source, target);
|
||||
return Math.sqrt(minDistanceSquared + dz * dz);
|
||||
}
|
||||
return minDistanceSquared;
|
||||
}
|
||||
|
||||
// Convert elevation difference to pixel distance
|
||||
function getElevationOffset(source, target) {
|
||||
const e1 = source.document?.elevation ?? 0;
|
||||
const e2 = target.document?.elevation ?? 0;
|
||||
return Math.abs(e1 - e2) * getPixelsPerSceneUnit();
|
||||
}
|
||||
|
||||
// Alpha Sampling: Pixel-perfect edge detection
|
||||
|
||||
// Get points along the visual edge of the token's texture
|
||||
function getTokenAlphaEdgePoints(token) {
|
||||
const displayObject = getTokenFilterTarget(token);
|
||||
const normalizedPoints = getTextureAlphaEdgePoints(displayObject);
|
||||
@@ -556,6 +702,7 @@ function getTokenAlphaEdgePoints(token) {
|
||||
return normalizedPoints.map(mapper);
|
||||
}
|
||||
|
||||
// Normalize texture-space points to canvas-space coordinates
|
||||
function getAlphaPointMapper(displayObject, token) {
|
||||
const localBounds = displayObject?.getLocalBounds?.();
|
||||
const transform = displayObject?.worldTransform;
|
||||
@@ -567,6 +714,7 @@ function getAlphaPointMapper(displayObject, token) {
|
||||
x: localBounds.x + point.x * localBounds.width,
|
||||
y: localBounds.y + point.y * localBounds.height,
|
||||
};
|
||||
// Map from local mesh space to world (screen) space, then back to canvas space
|
||||
const worldPoint = transform.apply(localPoint);
|
||||
return stageTransform.applyInverse(worldPoint);
|
||||
};
|
||||
@@ -579,6 +727,7 @@ function getAlphaPointMapper(displayObject, token) {
|
||||
});
|
||||
}
|
||||
|
||||
// Extract and cache points representing the non-transparent outline of a texture
|
||||
function getTextureAlphaEdgePoints(displayObject) {
|
||||
const texture = displayObject?.texture;
|
||||
const source = getTextureSource(texture);
|
||||
@@ -649,6 +798,7 @@ function getAlphaShapeCacheKey(texture, frame) {
|
||||
return `${textureId}:${frame.x}:${frame.y}:${frame.width}:${frame.height}:${ALPHA_SAMPLE_SIZE}:${HIGHLIGHT_ALPHA_THRESHOLD}`;
|
||||
}
|
||||
|
||||
// Draw the texture to a small canvas to scan its alpha channel
|
||||
function sampleTextureAlphaEdges(source, frame) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = ALPHA_SAMPLE_SIZE;
|
||||
@@ -682,6 +832,7 @@ function sampleTextureAlphaEdges(source, frame) {
|
||||
}
|
||||
}
|
||||
|
||||
// Scan image data for pixels that transition from opaque to transparent
|
||||
function getAlphaEdgeSamplePoints(imageData) {
|
||||
const alphaThreshold = HIGHLIGHT_ALPHA_THRESHOLD * 255;
|
||||
const { data, width, height } = imageData;
|
||||
@@ -735,6 +886,8 @@ function isAlphaEdgePixel(opaquePixels, width, height, x, y) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter Management: Drawing the highlights
|
||||
|
||||
function highlightToken(token, highlights) {
|
||||
const target = getTokenFilterTarget(token);
|
||||
const filters = highlights
|
||||
@@ -777,11 +930,12 @@ function removeHighlightFilters(target) {
|
||||
target.filters = remainingFilters.length > 0 ? remainingFilters : null;
|
||||
}
|
||||
|
||||
// Determine which outline filter to use based on available system/module filters
|
||||
function createOutlineFilter(color) {
|
||||
const colorNumber = colorToNumber(color);
|
||||
const pixiOutlineFilter = globalThis.PIXI?.filters?.OutlineFilter;
|
||||
if (pixiOutlineFilter)
|
||||
return new pixiOutlineFilter(HIGHLIGHT_LINE_WIDTH, colorNumber, 0.2);
|
||||
return new pixiOutlineFilter(getHighlightThickness(), colorNumber, 0.2);
|
||||
|
||||
const outlineOverlayFilter =
|
||||
globalThis.OutlineOverlayFilter ??
|
||||
@@ -789,7 +943,7 @@ function createOutlineFilter(color) {
|
||||
if (outlineOverlayFilter?.create) {
|
||||
const filter = outlineOverlayFilter.create({
|
||||
outlineColor: colorToRgbArray(colorNumber),
|
||||
thickness: [HIGHLIGHT_LINE_WIDTH, HIGHLIGHT_LINE_WIDTH],
|
||||
thickness: [getHighlightThickness(), getHighlightThickness()],
|
||||
alphaThreshold: HIGHLIGHT_ALPHA_THRESHOLD,
|
||||
knockout: false,
|
||||
wave: false,
|
||||
@@ -808,7 +962,7 @@ function createOutlineFilter(color) {
|
||||
if (glowOverlayFilter?.create) {
|
||||
const filter = glowOverlayFilter.create({
|
||||
glowColor: colorToRgbArray(colorNumber),
|
||||
distance: HIGHLIGHT_LINE_WIDTH,
|
||||
distance: getHighlightThickness(),
|
||||
outerStrength: 3,
|
||||
innerStrength: 0,
|
||||
alpha: 1,
|
||||
@@ -824,7 +978,7 @@ function createOutlineFilter(color) {
|
||||
function createAlphaOutlineFilter(color) {
|
||||
const uniforms = {
|
||||
outlineColor: colorToRgbArray(color),
|
||||
thickness: HIGHLIGHT_LINE_WIDTH,
|
||||
thickness: getHighlightThickness(),
|
||||
alphaThreshold: HIGHLIGHT_ALPHA_THRESHOLD,
|
||||
};
|
||||
const baseFilter =
|
||||
@@ -838,19 +992,20 @@ function createAlphaOutlineFilter(color) {
|
||||
ALPHA_OUTLINE_FRAGMENT_SHADER,
|
||||
uniforms,
|
||||
);
|
||||
filter.padding = HIGHLIGHT_LINE_WIDTH;
|
||||
filter.padding = getHighlightThickness();
|
||||
return filter;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback graphics when filters are not supported
|
||||
function drawFallbackTokenHighlights(container, token, highlights) {
|
||||
highlights.forEach((aura, index) => {
|
||||
const bounds = token.bounds.clone().pad(index * HIGHLIGHT_RING_SPACING);
|
||||
const graphic = new PIXI.Graphics();
|
||||
|
||||
graphic.lineStyle(HIGHLIGHT_LINE_WIDTH, colorToNumber(aura.color), 0.9);
|
||||
graphic.lineStyle(getHighlightThickness(), colorToNumber(aura.color), 0.9);
|
||||
graphic.drawRoundedRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
@@ -862,6 +1017,8 @@ function drawFallbackTokenHighlights(container, token, highlights) {
|
||||
});
|
||||
}
|
||||
|
||||
// Utility Functions
|
||||
|
||||
function getPixelsPerSceneUnit() {
|
||||
const activeCanvas = globalThis.canvas;
|
||||
const gridSize =
|
||||
@@ -904,6 +1061,10 @@ function activateAurasTab(root, group) {
|
||||
}
|
||||
}
|
||||
|
||||
function getHighlightThickness() {
|
||||
return game.settings.get(MODULE_ID, THICKNESS_SETTING) ?? 4;
|
||||
}
|
||||
|
||||
function localize(path) {
|
||||
return game.i18n.localize(`BASIC_AURA_HIGHLIGHTING.${path}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user