Refactor TerritoryLayer and TerritoryWebGLRenderer for enhanced transition management

- Updated transition handling in TerritoryLayer, replacing epoch and progress tracking with a more efficient system using arrays for start times and active masks.
- Introduced new methods to manage transition states and ensure proper initialization of transition data.
- Modified TerritoryWebGLRenderer to align with the new transition management, removing obsolete properties and updating uniform handling for transitions.
- Improved rendering logic to support smoother visual updates during tile transitions.
This commit is contained in:
scamiv
2026-01-09 21:46:21 +01:00
parent 7acf863c1a
commit afd5607934
2 changed files with 352 additions and 170 deletions
+92 -32
View File
@@ -42,10 +42,10 @@ export class TerritoryLayer implements Layer {
private lastMyPlayerSmallId: number | null = null;
private lastPaletteSignature: string | null = null;
private transitionActive = false;
private transitionEpoch = 1;
private transitionProgress = 1;
private transitionStartTime = 0;
private transitionDurationMs = 100;
private transitionDurationMs = 500;
private transitionTiles: TileRef[] = [];
private transitionStartTimes: Uint16Array | null = null;
private transitionActiveMask: Uint8Array | null = null;
private lastGameTick = 0;
private lastTickTime = 0;
private lastTickDurationMs = 100;
@@ -353,6 +353,10 @@ export class TerritoryLayer implements Layer {
this.cachedTerritoryPatternsEnabled = this.userSettings.territoryPatterns();
this.configureRenderers();
this.transitionActive = false;
this.transitionTiles = [];
this.ensureTransitionScratch();
this.transitionStartTimes?.fill(0);
this.transitionActiveMask?.fill(0);
// Add a second canvas for highlights
this.highlightCanvas = document.createElement("canvas");
@@ -413,7 +417,7 @@ export class TerritoryLayer implements Layer {
return;
}
const now = this.nowMs();
this.updateTransitionProgress(now);
this.updateTransitionState(now);
const renderTerritoryStart = FrameProfiler.start();
this.territoryRenderer.render();
@@ -505,6 +509,17 @@ export class TerritoryLayer implements Layer {
return typeof performance !== "undefined" ? performance.now() : Date.now();
}
private ensureTransitionScratch() {
const size = this.game.width() * this.game.height();
if (
!this.transitionStartTimes ||
this.transitionStartTimes.length !== size
) {
this.transitionStartTimes = new Uint16Array(size);
this.transitionActiveMask = new Uint8Array(size);
}
}
private updateTickTiming(now: number) {
const currentTick = this.game.ticks();
if (currentTick === this.lastGameTick) {
@@ -524,49 +539,94 @@ export class TerritoryLayer implements Layer {
changes: Array<{ tile: TileRef; previousOwner: number; newOwner: number }>,
now: number,
) {
if (!this.territoryRenderer) {
return;
}
this.ensureTransitionScratch();
const startTimes = this.transitionStartTimes!;
const activeMask = this.transitionActiveMask!;
const renderer = this.territoryRenderer;
if (changes.length === 0) {
return;
}
this.transitionEpoch = (this.transitionEpoch + 1) & 0xff;
if (this.transitionEpoch === 0) {
this.transitionEpoch = 1;
}
this.transitionStartTime = now;
this.transitionDurationMs = this.lastTickDurationMs;
this.transitionProgress = 0;
this.transitionActive = false;
const nowPacked = this.packTransitionTime(now);
const startPacked = nowPacked | 0x8000;
for (const change of changes) {
if (change.newOwner === change.previousOwner) {
continue;
}
if (this.territoryRenderer) {
this.territoryRenderer.setTransitionEpoch(
change.tile,
this.transitionEpoch,
);
this.transitionActive = true;
const tile = change.tile;
if (activeMask[tile] === 0) {
activeMask[tile] = 1;
this.transitionTiles.push(tile);
}
startTimes[tile] = nowPacked;
renderer.setTransitionTile(tile, change.previousOwner, startPacked);
}
this.transitionActive = this.transitionTiles.length > 0;
}
private updateTransitionProgress(now: number) {
if (!this.territoryRenderer || !this.transitionActive) {
private updateTransitionState(now: number) {
if (!this.territoryRenderer) {
return;
}
const elapsed = now - this.transitionStartTime;
const duration =
this.transitionDurationMs > 0 ? this.transitionDurationMs : 1;
const progress = Math.max(0, Math.min(1, elapsed / duration));
const eased = progress * progress * (3 - 2 * progress);
this.transitionProgress = eased;
this.territoryRenderer.setTransitionProgress(
this.transitionProgress,
this.transitionEpoch,
this.ensureTransitionScratch();
const nowPacked = this.packTransitionTime(now);
this.territoryRenderer.setTransitionTime(
nowPacked,
this.transitionDurationMs,
);
if (progress >= 1) {
this.transitionActive = false;
if (!this.transitionActive || this.transitionTiles.length === 0) {
return;
}
const startTimes = this.transitionStartTimes!;
const activeMask = this.transitionActiveMask!;
const tiles = this.transitionTiles;
const duration = this.transitionDurationMs;
let writeIndex = 0;
for (let i = 0; i < tiles.length; i++) {
const tile = tiles[i];
const start = startTimes[tile];
if (start === 0) {
activeMask[tile] = 0;
this.territoryRenderer.clearTransitionTile(
tile,
this.game.ownerID(tile),
);
continue;
}
const elapsed = this.transitionElapsed(nowPacked, start);
if (elapsed >= duration) {
activeMask[tile] = 0;
startTimes[tile] = 0;
this.territoryRenderer.clearTransitionTile(
tile,
this.game.ownerID(tile),
);
} else {
tiles[writeIndex++] = tile;
}
}
tiles.length = writeIndex;
this.transitionActive = tiles.length > 0;
}
private packTransitionTime(now: number): number {
const wrap = 32768;
return Math.floor(now) % wrap | 0;
}
private transitionElapsed(nowPacked: number, startPacked: number): number {
const wrap = 32768;
if (nowPacked >= startPacked) {
return nowPacked - startPacked;
}
return wrap - startPacked + nowPacked;
}
private computePaletteSignature(): string {
@@ -43,10 +43,8 @@ export class TerritoryWebGLRenderer {
relations: WebGLUniformLocation | null;
patterns: WebGLUniformLocation | null;
transitions: WebGLUniformLocation | null;
transitionEpoch: WebGLUniformLocation | null;
transitionProgress: WebGLUniformLocation | null;
transitionHintColor: WebGLUniformLocation | null;
transitionHintStrength: WebGLUniformLocation | null;
transitionNow: WebGLUniformLocation | null;
transitionDuration: WebGLUniformLocation | null;
patternStride: WebGLUniformLocation | null;
patternRows: WebGLUniformLocation | null;
fallout: WebGLUniformLocation | null;
@@ -66,7 +64,7 @@ export class TerritoryWebGLRenderer {
};
private readonly state: Uint16Array;
private readonly transitionEpochState: Uint8Array;
private readonly transitionState: Uint16Array;
private readonly dirtyRows: Map<number, DirtySpan> = new Map();
private readonly transitionDirtyRows: Map<number, DirtySpan> = new Map();
private needsFullUpload = true;
@@ -79,10 +77,8 @@ export class TerritoryWebGLRenderer {
private hoverPulseSpeed = Math.PI * 2;
private hoveredPlayerId = -1;
private animationStartTime = Date.now();
private transitionEpoch = 1;
private transitionProgress = 1;
private transitionHintColor: [number, number, number] = [1, 1, 1];
private transitionHintStrength = 0.25;
private transitionNow = 0;
private transitionDurationMs = 500;
private readonly userSettings = new UserSettings();
private readonly patternBytesCache = new Map<string, Uint8Array>();
@@ -96,7 +92,11 @@ export class TerritoryWebGLRenderer {
this.canvas.height = game.height();
this.state = state;
this.transitionEpochState = new Uint8Array(state.length);
this.transitionState = new Uint16Array(state.length * 2);
for (let i = 0; i < state.length; i++) {
this.transitionState[i * 2] = state[i] & 0x0fff;
this.transitionState[i * 2 + 1] = 0;
}
this.gl = this.canvas.getContext("webgl2", {
premultipliedAlpha: true,
@@ -120,10 +120,8 @@ export class TerritoryWebGLRenderer {
relations: null,
patterns: null,
transitions: null,
transitionEpoch: null,
transitionProgress: null,
transitionHintColor: null,
transitionHintStrength: null,
transitionNow: null,
transitionDuration: null,
patternStride: null,
patternRows: null,
fallout: null,
@@ -161,10 +159,8 @@ export class TerritoryWebGLRenderer {
relations: null,
patterns: null,
transitions: null,
transitionEpoch: null,
transitionProgress: null,
transitionHintColor: null,
transitionHintStrength: null,
transitionNow: null,
transitionDuration: null,
patternStride: null,
patternRows: null,
fallout: null,
@@ -192,18 +188,10 @@ export class TerritoryWebGLRenderer {
relations: gl.getUniformLocation(this.program, "u_relations"),
patterns: gl.getUniformLocation(this.program, "u_patterns"),
transitions: gl.getUniformLocation(this.program, "u_transitions"),
transitionEpoch: gl.getUniformLocation(this.program, "u_transitionEpoch"),
transitionProgress: gl.getUniformLocation(
transitionNow: gl.getUniformLocation(this.program, "u_transitionNow"),
transitionDuration: gl.getUniformLocation(
this.program,
"u_transitionProgress",
),
transitionHintColor: gl.getUniformLocation(
this.program,
"u_transitionHintColor",
),
transitionHintStrength: gl.getUniformLocation(
this.program,
"u_transitionHintStrength",
"u_transitionDurationMs",
),
patternStride: gl.getUniformLocation(this.program, "u_patternStride"),
patternRows: gl.getUniformLocation(this.program, "u_patternRows"),
@@ -296,13 +284,13 @@ export class TerritoryWebGLRenderer {
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.R8UI,
gl.RG16UI,
this.canvas.width,
this.canvas.height,
0,
gl.RED_INTEGER,
gl.UNSIGNED_BYTE,
this.transitionEpochState,
gl.RG_INTEGER,
gl.UNSIGNED_SHORT,
this.transitionState,
);
gl.useProgram(this.program);
@@ -398,26 +386,11 @@ export class TerritoryWebGLRenderer {
if (this.uniforms.hoverPulseSpeed) {
gl.uniform1f(this.uniforms.hoverPulseSpeed, this.hoverPulseSpeed);
}
if (this.uniforms.transitionEpoch) {
gl.uniform1i(this.uniforms.transitionEpoch, this.transitionEpoch);
if (this.uniforms.transitionNow) {
gl.uniform1i(this.uniforms.transitionNow, this.transitionNow);
}
if (this.uniforms.transitionProgress) {
gl.uniform1f(this.uniforms.transitionProgress, this.transitionProgress);
}
if (this.uniforms.transitionHintColor) {
this.transitionHintColor = [1, 1, 1];
gl.uniform3f(
this.uniforms.transitionHintColor,
this.transitionHintColor[0],
this.transitionHintColor[1],
this.transitionHintColor[2],
);
}
if (this.uniforms.transitionHintStrength) {
gl.uniform1f(
this.uniforms.transitionHintStrength,
this.transitionHintStrength,
);
if (this.uniforms.transitionDuration) {
gl.uniform1f(this.uniforms.transitionDuration, this.transitionDurationMs);
}
gl.enable(gl.BLEND);
@@ -492,12 +465,18 @@ export class TerritoryWebGLRenderer {
}
}
setTransitionEpoch(tile: TileRef, epoch: number) {
const value = epoch & 0xff;
if (this.transitionEpochState[tile] === value) {
setTransitionTile(tile: TileRef, previousOwner: number, startPacked: number) {
const offset = tile * 2;
const ownerValue = previousOwner & 0xffff;
const startValue = startPacked & 0xffff;
if (
this.transitionState[offset] === ownerValue &&
this.transitionState[offset + 1] === startValue
) {
return;
}
this.transitionEpochState[tile] = value;
this.transitionState[offset] = ownerValue;
this.transitionState[offset + 1] = startValue;
if (this.needsTransitionFullUpload) {
return;
}
@@ -512,9 +491,13 @@ export class TerritoryWebGLRenderer {
}
}
setTransitionProgress(progress: number, epoch: number) {
this.transitionEpoch = epoch & 0xff;
this.transitionProgress = Math.max(0, Math.min(1, progress));
clearTransitionTile(tile: TileRef, currentOwner: number) {
this.setTransitionTile(tile, currentOwner, 0);
}
setTransitionTime(nowPacked: number, durationMs: number) {
this.transitionNow = nowPacked | 0;
this.transitionDurationMs = Math.max(1, durationMs);
}
markAllDirty() {
@@ -582,11 +565,11 @@ export class TerritoryWebGLRenderer {
const viewerId = this.game.myPlayer()?.smallID() ?? 0;
gl.uniform1i(this.uniforms.viewerId, viewerId);
}
if (this.uniforms.transitionEpoch) {
gl.uniform1i(this.uniforms.transitionEpoch, this.transitionEpoch);
if (this.uniforms.transitionNow) {
gl.uniform1i(this.uniforms.transitionNow, this.transitionNow);
}
if (this.uniforms.transitionProgress) {
gl.uniform1f(this.uniforms.transitionProgress, this.transitionProgress);
if (this.uniforms.transitionDuration) {
gl.uniform1f(this.uniforms.transitionDuration, this.transitionDurationMs);
}
gl.clearColor(0, 0, 0, 0);
@@ -657,7 +640,7 @@ export class TerritoryWebGLRenderer {
gl.activeTexture(gl.TEXTURE4);
gl.bindTexture(gl.TEXTURE_2D, this.transitionTexture);
const bytesPerPixel = Uint8Array.BYTES_PER_ELEMENT;
const bytesPerPixel = Uint16Array.BYTES_PER_ELEMENT * 2;
let rowsUploaded = 0;
let bytesUploaded = 0;
@@ -665,13 +648,13 @@ export class TerritoryWebGLRenderer {
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.R8UI,
gl.RG16UI,
this.canvas.width,
this.canvas.height,
0,
gl.RED_INTEGER,
gl.UNSIGNED_BYTE,
this.transitionEpochState,
gl.RG_INTEGER,
gl.UNSIGNED_SHORT,
this.transitionState,
);
this.needsTransitionFullUpload = false;
this.transitionDirtyRows.clear();
@@ -686,10 +669,10 @@ export class TerritoryWebGLRenderer {
for (const [y, span] of this.transitionDirtyRows) {
const width = span.maxX - span.minX + 1;
const offset = y * this.canvas.width + span.minX;
const rowSlice = this.transitionEpochState.subarray(
const offset = (y * this.canvas.width + span.minX) * 2;
const rowSlice = this.transitionState.subarray(
offset,
offset + width,
offset + width * 2,
);
gl.texSubImage2D(
gl.TEXTURE_2D,
@@ -698,8 +681,8 @@ export class TerritoryWebGLRenderer {
y,
width,
1,
gl.RED_INTEGER,
gl.UNSIGNED_BYTE,
gl.RG_INTEGER,
gl.UNSIGNED_SHORT,
rowSlice,
);
rowsUploaded++;
@@ -897,10 +880,8 @@ export class TerritoryWebGLRenderer {
uniform usampler2D u_relations;
uniform usampler2D u_patterns;
uniform usampler2D u_transitions;
uniform int u_transitionEpoch;
uniform float u_transitionProgress;
uniform vec3 u_transitionHintColor;
uniform float u_transitionHintStrength;
uniform int u_transitionNow;
uniform float u_transitionDurationMs;
uniform int u_patternStride;
uniform int u_patternRows;
uniform int u_viewerId;
@@ -930,6 +911,24 @@ export class TerritoryWebGLRenderer {
return texelFetch(u_state, clamped, 0).r & 0xFFFu;
}
uint prevOwnerAtTex(ivec2 texCoord) {
ivec2 clamped = clamp(
texCoord,
ivec2(0, 0),
ivec2(int(u_resolution.x) - 1, int(u_resolution.y) - 1)
);
return texelFetch(u_transitions, clamped, 0).r & 0xFFFu;
}
uint transitionPackedAtTex(ivec2 texCoord) {
ivec2 clamped = clamp(
texCoord,
ivec2(0, 0),
ivec2(int(u_resolution.x) - 1, int(u_resolution.y) - 1)
);
return texelFetch(u_transitions, clamped, 0).g;
}
uint relationCode(uint owner, uint other) {
if (owner == 0u || other == 0u) {
return 0u;
@@ -985,26 +984,26 @@ export class TerritoryWebGLRenderer {
ivec2 texCoord = ivec2(fragCoord.x, int(u_resolution.y) - 1 - fragCoord.y);
uint state = texelFetch(u_state, texCoord, 0).r;
uint transitionEpoch = texelFetch(u_transitions, texCoord, 0).r;
bool inTransition =
(u_transitionEpoch != 0) && (transitionEpoch == uint(u_transitionEpoch));
float transition = inTransition ? u_transitionProgress : 1.0;
float hintMix = inTransition ? u_transitionHintStrength * (1.0 - transition) : 0.0;
uint owner = state & 0xFFFu;
uint prevOwner = prevOwnerAtTex(texCoord);
uint startPacked = transitionPackedAtTex(texCoord);
const uint TRANSITION_FLAG = 0x8000u;
const uint TRANSITION_TIME_MASK = 0x7FFFu;
const uint TRANSITION_WRAP = 32768u;
bool hasTransition = (startPacked & TRANSITION_FLAG) != 0u;
float t = 1.0;
if (hasTransition) {
uint start = startPacked & TRANSITION_TIME_MASK;
uint nowTime = uint(u_transitionNow);
uint elapsed = nowTime >= start
? (nowTime - start)
: (TRANSITION_WRAP - start + nowTime);
t = clamp(float(elapsed) / u_transitionDurationMs, 0.0, 1.0);
}
bool doTransition = hasTransition && t < 1.0 && prevOwner != owner;
bool hasFallout = (state & 0x2000u) != 0u;
bool isDefended = (state & 0x1000u) != 0u;
if (owner == 0u) {
if (hasFallout) {
vec3 color = u_fallout.rgb;
float a = u_alpha;
outColor = vec4(color * a, a);
} else {
outColor = vec4(0.0);
}
return;
}
bool isBorder = false;
bool hasFriendlyRelation = false;
bool hasEmbargoRelation = false;
@@ -1037,18 +1036,79 @@ export class TerritoryWebGLRenderer {
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
}
if (u_alternativeView) {
uint relationAlt = relationCode(owner, uint(u_viewerId));
vec4 altColor = u_altNeutral;
if (isSelf(relationAlt)) {
altColor = u_altSelf;
} else if (isFriendly(relationAlt)) {
altColor = u_altAlly;
} else if (isEmbargo(relationAlt)) {
altColor = u_altEnemy;
bool oldIsBorder = false;
bool oldFriendlyRelation = false;
bool oldEmbargoRelation = false;
if (doTransition && prevOwner != 0u) {
uint prevNeighbor = prevOwnerAtTex(texCoord + ivec2(1, 0));
oldIsBorder = oldIsBorder || (prevNeighbor != prevOwner);
if (prevNeighbor != prevOwner && prevNeighbor != 0u) {
uint rel = relationCode(prevOwner, prevNeighbor);
oldEmbargoRelation = oldEmbargoRelation || isEmbargo(rel);
oldFriendlyRelation = oldFriendlyRelation || isFriendly(rel);
}
float a = (isBorder ? 1.0 : 0.0) * transition;
vec3 color = altColor.rgb;
prevNeighbor = prevOwnerAtTex(texCoord + ivec2(-1, 0));
oldIsBorder = oldIsBorder || (prevNeighbor != prevOwner);
if (prevNeighbor != prevOwner && prevNeighbor != 0u) {
uint rel = relationCode(prevOwner, prevNeighbor);
oldEmbargoRelation = oldEmbargoRelation || isEmbargo(rel);
oldFriendlyRelation = oldFriendlyRelation || isFriendly(rel);
}
prevNeighbor = prevOwnerAtTex(texCoord + ivec2(0, 1));
oldIsBorder = oldIsBorder || (prevNeighbor != prevOwner);
if (prevNeighbor != prevOwner && prevNeighbor != 0u) {
uint rel = relationCode(prevOwner, prevNeighbor);
oldEmbargoRelation = oldEmbargoRelation || isEmbargo(rel);
oldFriendlyRelation = oldFriendlyRelation || isFriendly(rel);
}
prevNeighbor = prevOwnerAtTex(texCoord + ivec2(0, -1));
oldIsBorder = oldIsBorder || (prevNeighbor != prevOwner);
if (prevNeighbor != prevOwner && prevNeighbor != 0u) {
uint rel = relationCode(prevOwner, prevNeighbor);
oldEmbargoRelation = oldEmbargoRelation || isEmbargo(rel);
oldFriendlyRelation = oldFriendlyRelation || isFriendly(rel);
}
}
if (u_alternativeView) {
vec3 newColor = vec3(0.0);
float newAlpha = 0.0;
if (owner != 0u) {
uint relationAlt = relationCode(owner, uint(u_viewerId));
vec4 altColor = u_altNeutral;
if (isSelf(relationAlt)) {
altColor = u_altSelf;
} else if (isFriendly(relationAlt)) {
altColor = u_altAlly;
} else if (isEmbargo(relationAlt)) {
altColor = u_altEnemy;
}
newColor = altColor.rgb;
newAlpha = isBorder ? 1.0 : 0.0;
}
vec3 color = newColor;
float a = newAlpha;
if (doTransition) {
vec3 oldColor = vec3(0.0);
float oldAlpha = 0.0;
if (prevOwner != 0u) {
uint relationAltOld = relationCode(prevOwner, uint(u_viewerId));
vec4 altColorOld = u_altNeutral;
if (isSelf(relationAltOld)) {
altColorOld = u_altSelf;
} else if (isFriendly(relationAltOld)) {
altColorOld = u_altAlly;
} else if (isEmbargo(relationAltOld)) {
altColorOld = u_altEnemy;
}
oldColor = altColorOld.rgb;
oldAlpha = oldIsBorder ? 1.0 : 0.0;
}
color = mix(oldColor, newColor, t);
a = mix(oldAlpha, newAlpha, t);
}
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
float pulse = u_hoverPulseStrength > 0.0
? (1.0 - u_hoverPulseStrength) +
@@ -1060,44 +1120,106 @@ export class TerritoryWebGLRenderer {
return;
}
vec4 base = texelFetch(u_palette, ivec2(int(owner) * 2, 0), 0);
vec4 baseBorder = texelFetch(u_palette, ivec2(int(owner) * 2 + 1, 0), 0);
vec3 color = base.rgb;
float a = u_alpha;
if (isBorder) {
vec3 borderColor = baseBorder.rgb;
const float BORDER_TINT_RATIO = 0.35;
const vec3 FRIENDLY_TINT_TARGET = vec3(0.0, 1.0, 0.0);
const vec3 EMBARGO_TINT_TARGET = vec3(1.0, 0.0, 0.0);
if (hasFriendlyRelation) {
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO;
vec3 newColor = vec3(0.0);
float newAlpha = 0.0;
if (owner == 0u) {
if (hasFallout) {
newColor = u_fallout.rgb;
newAlpha = u_alpha;
}
if (hasEmbargoRelation) {
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
EMBARGO_TINT_TARGET * BORDER_TINT_RATIO;
}
if (isDefended) {
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
const float LIGHT_FACTOR = 1.2;
const float DARK_FACTOR = 0.8;
borderColor *= isLightTile ? LIGHT_FACTOR : DARK_FACTOR;
}
color = borderColor;
a = baseBorder.a * transition;
} else {
bool isPrimary = patternIsPrimary(owner, texCoord);
color = isPrimary ? base.rgb : baseBorder.rgb;
a = u_alpha;
vec4 base = texelFetch(u_palette, ivec2(int(owner) * 2, 0), 0);
vec4 baseBorder = texelFetch(
u_palette,
ivec2(int(owner) * 2 + 1, 0),
0
);
if (isBorder) {
vec3 borderColor = baseBorder.rgb;
const float BORDER_TINT_RATIO = 0.35;
const vec3 FRIENDLY_TINT_TARGET = vec3(0.0, 1.0, 0.0);
const vec3 EMBARGO_TINT_TARGET = vec3(1.0, 0.0, 0.0);
if (hasFriendlyRelation) {
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO;
}
if (hasEmbargoRelation) {
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
EMBARGO_TINT_TARGET * BORDER_TINT_RATIO;
}
if (isDefended) {
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
const float LIGHT_FACTOR = 1.2;
const float DARK_FACTOR = 0.8;
borderColor *= isLightTile ? LIGHT_FACTOR : DARK_FACTOR;
}
newColor = borderColor;
newAlpha = baseBorder.a;
} else {
bool isPrimary = patternIsPrimary(owner, texCoord);
newColor = isPrimary ? base.rgb : baseBorder.rgb;
newAlpha = u_alpha;
}
}
if (hintMix > 0.0) {
color = mix(color, u_transitionHintColor, hintMix);
vec3 color = newColor;
float a = newAlpha;
if (doTransition) {
vec3 oldColor = vec3(0.0);
float oldAlpha = 0.0;
if (prevOwner == 0u) {
if (hasFallout) {
oldColor = u_fallout.rgb;
oldAlpha = u_alpha;
}
} else {
vec4 oldBase = texelFetch(
u_palette,
ivec2(int(prevOwner) * 2, 0),
0
);
vec4 oldBorder = texelFetch(
u_palette,
ivec2(int(prevOwner) * 2 + 1, 0),
0
);
if (oldIsBorder) {
vec3 borderColor = oldBorder.rgb;
const float BORDER_TINT_RATIO = 0.35;
const vec3 FRIENDLY_TINT_TARGET = vec3(0.0, 1.0, 0.0);
const vec3 EMBARGO_TINT_TARGET = vec3(1.0, 0.0, 0.0);
if (oldFriendlyRelation) {
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO;
}
if (oldEmbargoRelation) {
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
EMBARGO_TINT_TARGET * BORDER_TINT_RATIO;
}
if (isDefended) {
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
const float LIGHT_FACTOR = 1.2;
const float DARK_FACTOR = 0.8;
borderColor *= isLightTile ? LIGHT_FACTOR : DARK_FACTOR;
}
oldColor = borderColor;
oldAlpha = oldBorder.a;
} else {
bool isPrimary = patternIsPrimary(prevOwner, texCoord);
oldColor = isPrimary ? oldBase.rgb : oldBorder.rgb;
oldAlpha = u_alpha;
}
}
color = mix(oldColor, newColor, t);
a = mix(oldAlpha, newAlpha, t);
}
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {