Skip to content

Commit aa6fdfd

Browse files
committed
Added ColorBlindnessNG plugin
Comparison mode in ColorBlindnessNG plugin, custom mode with custom LUT for specific color replacement.
1 parent 4b99f78 commit aa6fdfd

3 files changed

Lines changed: 256 additions & 34 deletions

File tree

plugins/MouseEffects.Effects.ColorBlindnessNG/ColorBlindnessNGEffect.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ public sealed class ColorBlindnessNGEffect : EffectBase
9595
private float _splitPositionV = 0.5f;
9696
private bool _comparisonMode = false;
9797

98+
// Mouse position for virtual cursor in comparison mode
99+
private Vector2 _mousePosition;
100+
98101
// Per-zone settings (4 zones max)
99102
private readonly ZoneSettings[] _zones = new ZoneSettings[MaxZones];
100103

@@ -388,7 +391,7 @@ private static bool ColorsEqual(Vector3 a, Vector3 b)
388391

389392
protected override void OnUpdate(GameTime gameTime, MouseState mouseState)
390393
{
391-
// No per-frame updates needed
394+
_mousePosition = mouseState.Position;
392395
}
393396

394397
protected override void OnRender(IRenderContext context)
@@ -448,6 +451,7 @@ private ColorBlindnessNGParams BuildConstantBuffer(Vector2 viewportSize)
448451
{
449452
var cbParams = new ColorBlindnessNGParams
450453
{
454+
MousePosition = _mousePosition,
451455
ViewportSize = viewportSize,
452456
SplitModeValue = (float)_splitMode,
453457
SplitPosition = _splitPosition,
@@ -567,20 +571,18 @@ private struct ZoneParams
567571

568572
/// <summary>
569573
/// Full constant buffer for shader.
570-
/// Size: 16 (global) + 4 * 64 (zones) = 272 bytes, padded to 288 for 16-byte alignment
574+
/// Size: 32 (global) + 4 * 64 (zones) = 288 bytes
571575
/// </summary>
572576
[StructLayout(LayoutKind.Sequential, Size = 288)]
573577
private struct ColorBlindnessNGParams
574578
{
575-
// Global parameters (16 bytes)
579+
// Global parameters (32 bytes)
580+
public Vector2 MousePosition; // 8 bytes
576581
public Vector2 ViewportSize; // 8 bytes
577582
public float SplitModeValue; // 4 bytes
578583
public float SplitPosition; // 4 bytes
579-
580584
public float SplitPositionV; // 4 bytes
581585
public float ComparisonMode; // 4 bytes
582-
public float GlobalPadding1; // 4 bytes
583-
public float GlobalPadding2; // 4 bytes
584586

585587
// Per-zone parameters (64 bytes each × 4 = 256 bytes)
586588
public ZoneParams Zone0;

plugins/MouseEffects.Effects.ColorBlindnessNG/Shaders/ColorBlindnessNG.hlsl

Lines changed: 247 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,12 @@ struct ZoneParams
4646
cbuffer ColorBlindnessNGParams : register(b0)
4747
{
4848
// Global parameters (32 bytes)
49-
float2 ViewportSize;
49+
float2 MousePosition; // Mouse position in screen pixels
50+
float2 ViewportSize; // Viewport size in pixels
5051
float SplitModeValue; // 0=Full, 1=SplitV, 2=SplitH, 3=Quad
5152
float SplitPosition; // Horizontal split (0-1)
52-
5353
float SplitPositionV; // Vertical split (0-1)
54-
float ComparisonMode; // 0=off, 1=on (zone 0 shows original)
55-
float _globalPad1;
56-
float _globalPad2;
54+
float ComparisonMode; // 0=off, 1=on (screen duplication mode)
5755

5856
// Per-zone parameters (64 bytes each × 4 = 256 bytes)
5957
ZoneParams Zone0;
@@ -669,6 +667,176 @@ float GetSeparatorAlpha(float2 screenPos)
669667
return 0.0;
670668
}
671669

670+
// ============================================================================
671+
// Comparison Mode - Screen Duplication
672+
// ============================================================================
673+
674+
// Get zone bounds in screen coordinates
675+
void GetZoneBounds(int zoneIndex, out float2 zoneMin, out float2 zoneMax)
676+
{
677+
float splitX = ViewportSize.x * SplitPosition;
678+
float splitY = ViewportSize.y * SplitPositionV;
679+
680+
if (SplitModeValue > 2.5) // Quadrants (3)
681+
{
682+
if (zoneIndex == 0) // Top-Left
683+
{
684+
zoneMin = float2(0, 0);
685+
zoneMax = float2(splitX, splitY);
686+
}
687+
else if (zoneIndex == 1) // Top-Right
688+
{
689+
zoneMin = float2(splitX, 0);
690+
zoneMax = float2(ViewportSize.x, splitY);
691+
}
692+
else if (zoneIndex == 2) // Bottom-Left
693+
{
694+
zoneMin = float2(0, splitY);
695+
zoneMax = float2(splitX, ViewportSize.y);
696+
}
697+
else // Bottom-Right (3)
698+
{
699+
zoneMin = float2(splitX, splitY);
700+
zoneMax = ViewportSize;
701+
}
702+
}
703+
else if (SplitModeValue > 1.5) // Split Horizontal (2)
704+
{
705+
if (zoneIndex == 0) // Top
706+
{
707+
zoneMin = float2(0, 0);
708+
zoneMax = float2(ViewportSize.x, splitY);
709+
}
710+
else // Bottom
711+
{
712+
zoneMin = float2(0, splitY);
713+
zoneMax = ViewportSize;
714+
}
715+
}
716+
else // Split Vertical (1)
717+
{
718+
if (zoneIndex == 0) // Left
719+
{
720+
zoneMin = float2(0, 0);
721+
zoneMax = float2(splitX, ViewportSize.y);
722+
}
723+
else // Right
724+
{
725+
zoneMin = float2(splitX, 0);
726+
zoneMax = ViewportSize;
727+
}
728+
}
729+
}
730+
731+
// Get UV coordinates for comparison mode with aspect ratio preservation
732+
// Returns (-1, -1) if pixel is in letterbox/pillarbox area (black bars)
733+
float2 GetComparisonUV(float2 screenPos, int zoneIndex)
734+
{
735+
float2 zoneMin, zoneMax;
736+
GetZoneBounds(zoneIndex, zoneMin, zoneMax);
737+
float2 zoneSize = zoneMax - zoneMin;
738+
float2 zoneCenter = (zoneMin + zoneMax) * 0.5;
739+
740+
if (SplitModeValue > 2.5) // Quadrants - stretch to fit
741+
{
742+
return (screenPos - zoneMin) / zoneSize;
743+
}
744+
745+
// Split modes - preserve aspect ratio with letterboxing/pillarboxing
746+
float screenAspect = ViewportSize.x / ViewportSize.y;
747+
float zoneAspect = zoneSize.x / zoneSize.y;
748+
749+
float2 scaledSize;
750+
if (screenAspect > zoneAspect)
751+
{
752+
// Screen is wider than zone - fit to zone width, letterbox
753+
scaledSize.x = zoneSize.x;
754+
scaledSize.y = zoneSize.x / screenAspect;
755+
}
756+
else
757+
{
758+
// Screen is taller than zone - fit to zone height, pillarbox
759+
scaledSize.y = zoneSize.y;
760+
scaledSize.x = zoneSize.y * screenAspect;
761+
}
762+
763+
float2 contentMin = zoneCenter - scaledSize * 0.5;
764+
float2 contentMax = zoneCenter + scaledSize * 0.5;
765+
766+
// Check if pixel is outside content area (in black bars)
767+
if (screenPos.x < contentMin.x || screenPos.x > contentMax.x ||
768+
screenPos.y < contentMin.y || screenPos.y > contentMax.y)
769+
{
770+
return float2(-1, -1); // Signal to render black
771+
}
772+
773+
return (screenPos - contentMin) / scaledSize;
774+
}
775+
776+
// Get the transformed mouse position within a zone for comparison mode
777+
float2 GetTransformedMousePos(int zoneIndex)
778+
{
779+
float2 zoneMin, zoneMax;
780+
GetZoneBounds(zoneIndex, zoneMin, zoneMax);
781+
float2 zoneSize = zoneMax - zoneMin;
782+
float2 zoneCenter = (zoneMin + zoneMax) * 0.5;
783+
784+
// Normalize mouse position to 0-1
785+
float2 normalizedMouse = MousePosition / ViewportSize;
786+
787+
if (SplitModeValue > 2.5) // Quadrants - stretch to fit
788+
{
789+
return zoneMin + normalizedMouse * zoneSize;
790+
}
791+
792+
// Split modes - preserve aspect ratio
793+
float screenAspect = ViewportSize.x / ViewportSize.y;
794+
float zoneAspect = zoneSize.x / zoneSize.y;
795+
796+
float2 scaledSize;
797+
if (screenAspect > zoneAspect)
798+
{
799+
scaledSize.x = zoneSize.x;
800+
scaledSize.y = zoneSize.x / screenAspect;
801+
}
802+
else
803+
{
804+
scaledSize.y = zoneSize.y;
805+
scaledSize.x = zoneSize.y * screenAspect;
806+
}
807+
808+
float2 contentMin = zoneCenter - scaledSize * 0.5;
809+
return contentMin + normalizedMouse * scaledSize;
810+
}
811+
812+
// Draw a crosshair cursor indicator
813+
float DrawCursor(float2 screenPos, float2 cursorPos, float size)
814+
{
815+
float2 delta = screenPos - cursorPos;
816+
float dist = length(delta);
817+
818+
// Crosshair parameters
819+
float lineWidth = 2.0;
820+
float innerRadius = 4.0;
821+
float outerRadius = size;
822+
823+
// Horizontal line
824+
bool onHLine = abs(delta.y) < lineWidth && abs(delta.x) > innerRadius && abs(delta.x) < outerRadius;
825+
// Vertical line
826+
bool onVLine = abs(delta.x) < lineWidth && abs(delta.y) > innerRadius && abs(delta.y) < outerRadius;
827+
// Center dot
828+
bool onCenter = dist < 3.0;
829+
// Outer circle (thin)
830+
bool onCircle = abs(dist - outerRadius) < 1.5;
831+
832+
if (onHLine || onVLine || onCenter || onCircle)
833+
{
834+
return 1.0;
835+
}
836+
837+
return 0.0;
838+
}
839+
672840
// ============================================================================
673841
// Pixel Shader
674842
// ============================================================================
@@ -678,36 +846,88 @@ float4 PSMain(PSInput input) : SV_TARGET
678846
float2 uv = input.TexCoord;
679847
float2 screenPos = uv * ViewportSize;
680848

681-
// Sample original screen color
682-
float4 screenColor = ScreenTexture.Sample(LinearSampler, uv);
683-
float3 color = screenColor.rgb;
684-
685849
// Get zone index
686850
int zoneIndex = GetZoneIndex(screenPos);
687851

688-
float3 finalColor;
689-
690-
// In comparison mode, zone 0 always shows original
691-
bool forceOriginal = (ComparisonMode > 0.5 && zoneIndex == 0);
852+
// Handle comparison mode - full screen duplication in each zone
853+
float2 sampleUV = uv;
854+
bool isBlackBar = false;
692855

693-
if (forceOriginal)
856+
if (ComparisonMode > 0.5 && SplitModeValue > 0.5)
694857
{
695-
finalColor = color;
858+
// In comparison mode, duplicate the full screen into each zone
859+
float2 comparisonUV = GetComparisonUV(screenPos, zoneIndex);
860+
if (comparisonUV.x < 0)
861+
{
862+
isBlackBar = true;
863+
}
864+
else
865+
{
866+
sampleUV = comparisonUV;
867+
}
696868
}
697-
else
869+
870+
// Return black for letterbox/pillarbox areas
871+
if (isBlackBar)
698872
{
699-
// Get zone parameters and process
700-
ZoneParams zone;
701-
if (zoneIndex == 0)
702-
zone = Zone0;
703-
else if (zoneIndex == 1)
704-
zone = Zone1;
705-
else if (zoneIndex == 2)
706-
zone = Zone2;
707-
else
708-
zone = Zone3;
873+
return float4(0.0, 0.0, 0.0, 1.0);
874+
}
709875

710-
finalColor = ProcessZone(color, zone, zoneIndex);
876+
// Sample original screen color at the (possibly remapped) UV
877+
float4 screenColor = ScreenTexture.Sample(LinearSampler, sampleUV);
878+
float3 color = screenColor.rgb;
879+
880+
float3 finalColor;
881+
882+
// Get zone parameters and process
883+
ZoneParams zone;
884+
if (zoneIndex == 0)
885+
zone = Zone0;
886+
else if (zoneIndex == 1)
887+
zone = Zone1;
888+
else if (zoneIndex == 2)
889+
zone = Zone2;
890+
else
891+
zone = Zone3;
892+
893+
finalColor = ProcessZone(color, zone, zoneIndex);
894+
895+
// Draw virtual cursor in comparison mode
896+
if (ComparisonMode > 0.5 && SplitModeValue > 0.5)
897+
{
898+
float2 transformedMouse = GetTransformedMousePos(zoneIndex);
899+
float cursorSize = 15.0;
900+
901+
// Scale cursor size based on zone size (smaller for quadrants)
902+
if (SplitModeValue > 2.5) // Quadrants
903+
{
904+
cursorSize = 12.0;
905+
}
906+
907+
float cursorAlpha = DrawCursor(screenPos, transformedMouse, cursorSize);
908+
909+
if (cursorAlpha > 0.5)
910+
{
911+
// Draw cursor with contrasting color (inverted luminance)
912+
float lum = dot(finalColor, float3(0.299, 0.587, 0.114));
913+
float3 cursorColor = lum > 0.5 ? float3(0.0, 0.0, 0.0) : float3(1.0, 1.0, 1.0);
914+
915+
// Add colored accent for better visibility
916+
float2 delta = screenPos - transformedMouse;
917+
float dist = length(delta);
918+
919+
// Center dot and outer ring - use accent color (cyan/magenta based on zone)
920+
if (dist < 3.0 || abs(dist - cursorSize) < 1.5)
921+
{
922+
float3 accentColor = (zoneIndex % 2 == 0) ? float3(0.0, 1.0, 1.0) : float3(1.0, 0.0, 1.0);
923+
finalColor = lerp(finalColor, accentColor, 0.9);
924+
}
925+
else
926+
{
927+
// Crosshair lines - use contrasting color
928+
finalColor = lerp(finalColor, cursorColor, 0.95);
929+
}
930+
}
711931
}
712932

713933
// Draw separator line

src/MouseEffects.App/MouseEffects.App.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<Platforms>x64</Platforms>
1616
<EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition>
1717
<!-- Version for Velopack (AssemblyVersion and FileVersion are derived automatically) -->
18-
<Version>1.0.24</Version>
18+
<Version>1.0.25</Version>
1919
</PropertyGroup>
2020

2121
<!-- MSIX Packaging Configuration -->

0 commit comments

Comments
 (0)