PixelRef.cs
namespace PixelPusher;
/// <summary>
/// Temporary tracing overlay. Never written to saves, drafts, or the Library.
/// </summary>
public static class PixelRef
{
static readonly string[] Hosts =
{
"files.catbox.moe",
"litterbox.catbox.moe",
"files.litterbox.catbox.moe",
"i.imgur.com",
"imgur.com",
"i.ibb.co",
"ibb.co",
"i.postimg.cc",
"postimg.cc",
"raw.githubusercontent.com",
"github.com",
"i.gyazo.com",
"media.discordapp.net",
"cdn.discordapp.com",
"i.redd.it",
"preview.redd.it",
"pomf.lain.la",
"0x0.st",
"tmpfiles.org"
};
public static bool Visible { get; private set; }
public static bool Fit { get; set; } = true;
public static int Opacity { get; private set; } = 40;
public static int Version { get; private set; }
public static string Status { get; private set; } = "no trace";
public static bool Busy { get; private set; }
static Color32[] _px;
static int _w;
static int _h;
public static bool HasImage => _px is { Length: > 0 } && _w > 0 && _h > 0;
public static void Drop()
{
_px = null;
_w = 0;
_h = 0;
Visible = false;
Status = "no trace";
Version++;
GameFlow.ShowToast( "trace dropped" );
GameFlow.Bump();
}
public static void Toggle()
{
if ( !HasImage )
return;
Visible = !Visible;
Version++;
GameFlow.Bump();
}
public static void ToggleFit()
{
Fit = !Fit;
Version++;
GameFlow.Bump();
}
public static void NudgeOpacity( int dir )
{
Opacity = Math.Clamp( Opacity + dir, 10, 80 );
Version++;
GameFlow.Bump();
}
public static void Paste()
{
var url = ReadClipboard();
Load( url );
}
public static async void Load( string url )
{
url = (url ?? "").Trim().Trim( '"', '\'', '<', '>' );
if ( url.Length < 12 )
{
GameFlow.ShowToast( "copy an image URL first" );
return;
}
if ( !Uri.TryCreate( url, UriKind.Absolute, out var uri ) )
{
GameFlow.ShowToast( "not a URL" );
return;
}
if ( !HostOk( uri.Host ) || !UrlAllowed( uri ) )
{
GameFlow.ShowToast( "host not allowed" );
return;
}
if ( LooksLikePage( uri ) )
{
GameFlow.ShowToast( "need a direct image link" );
return;
}
Busy = true;
Status = "loading";
Version++;
GameFlow.Bump();
try
{
var tex = await Texture.LoadAsync( uri.ToString() );
if ( !tex.IsValid() )
throw new Exception( "bad image" );
var w = tex.Width;
var h = tex.Height;
if ( w < 2 || h < 2 )
throw new Exception( "too small" );
if ( w > 2048 || h > 2048 )
throw new Exception( "too large" );
var px = ReadPixels( tex, w, h );
if ( px is null || px.Length < w * h )
throw new Exception( "could not read pixels" );
_px = px;
_w = w;
_h = h;
Opacity = 40;
Visible = true;
Status = $"{w}x{h} trace";
Version++;
GameFlow.ShowToast( "trace on · not saved" );
}
catch ( Exception e )
{
Status = "failed";
GameFlow.ShowToast( $"trace failed {e.Message}" );
Log.Warning( $"[PIXEL PUSHER] trace: {e.Message}" );
}
Busy = false;
Version++;
GameFlow.Bump();
}
public static Color32 Sample( int cx, int cy, int canvasW, int canvasH )
{
if ( !Visible || !HasImage || canvasW <= 0 || canvasH <= 0 )
return default;
if ( (uint)cx >= (uint)canvasW || (uint)cy >= (uint)canvasH )
return default;
int rx;
int ry;
if ( Fit )
{
rx = cx * _w / canvasW;
ry = cy * _h / canvasH;
}
else
{
rx = cx;
ry = cy;
}
if ( (uint)rx >= (uint)_w || (uint)ry >= (uint)_h )
return default;
var src = _px[ry * _w + rx];
Color c = src;
if ( c.a < 0.04f )
return default;
var a = (byte)Math.Clamp( Opacity * 255 / 100, 0, 200 );
return new Color32(
(byte)MathF.Round( c.r * 255f ),
(byte)MathF.Round( c.g * 255f ),
(byte)MathF.Round( c.b * 255f ),
a );
}
static bool HostOk( string host )
{
host = (host ?? "").ToLowerInvariant();
if ( host.StartsWith( "www." ) )
host = host[4..];
foreach ( var h in Hosts )
{
if ( host == h || host.EndsWith( "." + h ) )
return true;
}
return false;
}
static bool LooksLikePage( Uri uri )
{
var path = uri.AbsolutePath ?? "";
if ( path.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) ) return false;
if ( path.EndsWith( ".jpg", StringComparison.OrdinalIgnoreCase ) ) return false;
if ( path.EndsWith( ".jpeg", StringComparison.OrdinalIgnoreCase ) ) return false;
if ( path.EndsWith( ".gif", StringComparison.OrdinalIgnoreCase ) ) return false;
if ( path.EndsWith( ".webp", StringComparison.OrdinalIgnoreCase ) ) return false;
if ( path.EndsWith( ".bmp", StringComparison.OrdinalIgnoreCase ) ) return false;
if ( hostIsCatboxFile( uri ) ) return false;
return path.Length < 2 || path.EndsWith( "/" ) || !path.Contains( '.' );
}
static bool hostIsCatboxFile( Uri uri )
{
var host = uri.Host.ToLowerInvariant();
return host.Contains( "catbox.moe" ) && uri.AbsolutePath.Length > 2;
}
static bool UrlAllowed( Uri uri )
{
try
{
return Http.IsAllowed( uri );
}
catch
{
return uri.Scheme == "https" || uri.Scheme == "http";
}
}
static string ReadClipboard()
{
return "";
}
static Color32[] ReadPixels( Texture tex, int w, int h )
{
try
{
var data = tex.GetPixels();
if ( data is { Length: > 0 } )
return data;
}
catch
{
// try per-pixel
}
var px = new Color32[w * h];
try
{
for ( var y = 0; y < h; y++ )
for ( var x = 0; x < w; x++ )
px[y * w + x] = tex.GetPixel( x, y );
return px;
}
catch
{
return null;
}
}
}