Skip to content

Instantly share code, notes, and snippets.

@ZedDevStuff
Created July 5, 2025 17:03
Show Gist options
  • Save ZedDevStuff/3d3e15fac1f29915679aa45c8b3e19c8 to your computer and use it in GitHub Desktop.
Save ZedDevStuff/3d3e15fac1f29915679aa45c8b3e19c8 to your computer and use it in GitHub Desktop.
Working SFML.Net ICanvasRenderer for Prowl.Paper
using Prowl.PaperUI;
using Prowl.Quill;
using Prowl.Vector;
using SFML.Graphics;
using SFML.System;
using System;
using System.Collections.Generic;
using System.Drawing;
using SFMLVertex = SFML.Graphics.Vertex;
using QuillVertex = Prowl.Quill.Vertex;
using ProwlIntRect = Prowl.Vector.IntRect;
using SFML.Graphics.Glsl;
using System.IO;
using System.Text;
namespace PaperTest
{
public class SFMLCanvasRenderer : ICanvasRenderer, IDisposable
{
private readonly RenderWindow _window;
private readonly Texture _whiteTexture;
private RenderStates _renderStates = RenderStates.Default;
private const string Stroke_FragmentShaderString = """
varying vec2 fragTexCoord;
varying vec4 fragColor;
varying vec2 fragPos;
uniform sampler2D texture;
uniform mat4 scissorMat;
uniform vec2 scissorExt;
uniform mat4 brushMat;
uniform int brushType; // 0=none, 1=linear, 2=radial, 3=box
uniform vec4 brushColor1; // Start color
uniform vec4 brushColor2; // End color
uniform vec4 brushParams; // x,y = start point, z,w = end point (or center+radius for radial)
uniform vec2 brushParams2; // x = Box radius, y = Box Feather
float calculateBrushFactor() {
// No brush
if (brushType == 0) return 0.0;
vec2 transformedPoint = (brushMat * vec4(fragPos, 0.0, 1.0)).xy;
// Linear brush - projects position onto the line between start and end
if (brushType == 1) {
vec2 startPoint = brushParams.xy;
vec2 endPoint = brushParams.zw;
vec2 line = endPoint - startPoint;
float lineLength = length(line);
if (lineLength < 0.001) return 0.0;
vec2 posToStart = transformedPoint - startPoint;
float projection = dot(posToStart, line) / (lineLength * lineLength);
return clamp(projection, 0.0, 1.0);
}
// Radial brush - based on distance from center
if (brushType == 2) {
vec2 center = brushParams.xy;
float innerRadius = brushParams.z;
float outerRadius = brushParams.w;
if (outerRadius < 0.001) return 0.0;
float distance = smoothstep(innerRadius, outerRadius, length(transformedPoint - center));
return clamp(distance, 0.0, 1.0);
}
// Box brush - like radial but uses max distance in x or y direction
if (brushType == 3) {
vec2 center = brushParams.xy;
vec2 halfSize = brushParams.zw;
float radius = brushParams2.x;
float feather = brushParams2.y;
if (halfSize.x < 0.001 || halfSize.y < 0.001) return 0.0;
// Calculate distance from center (normalized by half-size)
vec2 q = abs(transformedPoint - center) - (halfSize - vec2(radius));
// Distance field calculation for rounded rectangle
//float dist = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
float dist = min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radius;
return clamp((dist + feather * 0.5) / feather, 0.0, 1.0);
}
return 0.0;
}
float scissorMask(vec2 p) {
// Early exit if scissoring is disabled (when scissorExt.x is negative or zero)
if(scissorExt.x <= 0.0) return 1.0;
// Transform point to scissor space
vec2 transformedPoint = (scissorMat * vec4(p, 0.0, 1.0)).xy;
// Calculate signed distance from scissor edges (negative inside, positive outside)
vec2 distanceFromEdges = abs(transformedPoint) - scissorExt;
// Apply offset for smooth edge transition (0.5 creates half-pixel anti-aliased edges)
vec2 smoothEdges = vec2(0.5, 0.5) - distanceFromEdges;
// Clamp each component and multiply to get final mask value
// Result is 1.0 inside, 0.0 outside, with smooth transition at edges
return clamp(smoothEdges.x, 0.0, 1.0) * clamp(smoothEdges.y, 0.0, 1.0);
}
vec4 textureNice(sampler2D sam, vec2 uv)
{
float textureResolution = float(textureSize(sam,0).x);
uv = uv * textureResolution + 0.5;
vec2 iuv = floor(uv);
vec2 fuv = fract(uv);
uv = iuv + fuv * fuv * (3.0 - 2.0 * fuv);
uv = (uv - 0.5) / textureResolution;
return texture2D(sam, uv);
}
void main()
{
vec2 pixelSize = fwidth(fragTexCoord);
vec2 edgeDistance = min(fragTexCoord, 1.0 - fragTexCoord);
float edgeAlpha = smoothstep(0.0, pixelSize.x, edgeDistance.x) * smoothstep(0.0, pixelSize.y, edgeDistance.y);
edgeAlpha = clamp(edgeAlpha, 0.0, 1.0);
float mask = scissorMask(fragPos);
vec4 color = fragColor;
// Apply brush if active
if (brushType > 0) {
float factor = calculateBrushFactor();
color = mix(brushColor1, brushColor2, factor);
}
vec4 textureColor = textureNice(texture, fragTexCoord);
color *= textureColor.xyzw;
color *= edgeAlpha * mask;
gl_FragColor = color;
}
""";
private const string Vertex_VertexShaderString = """
uniform mat4 mvp;
varying vec2 fragTexCoord;
varying vec4 fragColor;
varying vec2 fragPos;
void main()
{
fragTexCoord = gl_MultiTexCoord0;
fragColor = gl_Color;
fragPos = gl_Vertex.xy;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
""";
private Shader? _shader;
private Mat4 _scissorMat
{
set => _shader?.SetUniform("scissorMat", value);
}
private Vector2f _scissorExt
{
set => _shader?.SetUniform("scissorExt", value);
}
private int _brushType
{
set => _shader?.SetUniform("brushType", value);
}
private Mat4 _brushMat
{
set => _shader?.SetUniform("brushMat", value);
}
private Vec4 _brushColor1
{
set => _shader?.SetUniform("brushColor1", value);
}
private Vec4 _brushColor2
{
set => _shader?.SetUniform("brushColor2", value);
}
private Vec4 _brushParams
{
set => _shader?.SetUniform("brushParams", value);
}
private Vec2 _brushParams2
{
set => _shader?.SetUniform("brushParams2", value);
}
private List<VertexArray> _triangles = new List<VertexArray>();
public SFMLCanvasRenderer(RenderWindow window)
{
_window = window ?? throw new ArgumentNullException(nameof(window));
_shader = new Shader(
new MemoryStream(Encoding.UTF8.GetBytes(Vertex_VertexShaderString)),
null,
new MemoryStream(Encoding.UTF8.GetBytes(Stroke_FragmentShaderString)));
var whiteImage = new Image(1024, 1024, SFML.Graphics.Color.White);
_whiteTexture = new Texture(whiteImage);
}
public object CreateTexture(uint width, uint height)
{
var img = new Image(width, height, SFML.Graphics.Color.Transparent);
var tex = new Texture(img)
{
Smooth = true
};
return tex;
}
public Vector2Int GetTextureSize(object texture)
{
if (texture is not Texture tex)
throw new ArgumentException("Invalid texture type", nameof(texture));
return new Vector2Int((int)tex.Size.X, (int)tex.Size.Y);
}
public void SetTextureData(object texture, ProwlIntRect bounds, byte[] data)
{
if (texture is not Texture tex)
throw new ArgumentException("Invalid texture type", nameof(texture));
if (data == null || data.Length == 0)
throw new ArgumentNullException(nameof(data));
tex.Update(data, (uint)bounds.width, (uint)bounds.height, (uint)bounds.x, (uint)bounds.y);
}
private void SetUniforms(DrawCall drawCall)
{
Texture textureToUse = _whiteTexture;
if (drawCall.Texture is Texture tex)
textureToUse = tex;
_renderStates.Texture = textureToUse;
_renderStates.Shader = _shader;
drawCall.GetScissor(out Matrix4x4 scissor, out Vector2 scissorExt);
scissor = Matrix4x4.Transpose(scissor);
_scissorMat = ToFloat(scissor);
_scissorExt = new Vector2f((float)scissorExt.x, (float)scissorExt.y);
_brushType = (int)drawCall.Brush.Type;
if (drawCall.Brush.Type != BrushType.None)
{
var brushMat = Matrix4x4.Transpose(drawCall.Brush.BrushMatrix);
_shader?.SetUniform("brushMat", ToFloat(brushMat));
_brushMat = ToFloat(brushMat);
_brushColor1 = new Vec4(drawCall.Brush.Color1.R, drawCall.Brush.Color1.G, drawCall.Brush.Color1.B, drawCall.Brush.Color1.A);
_brushColor2 = new Vec4(drawCall.Brush.Color2.R, drawCall.Brush.Color2.G, drawCall.Brush.Color2.B, drawCall.Brush.Color2.A);
_brushParams = new Vec4((float)drawCall.Brush.Point1.x, (float)drawCall.Brush.Point1.y, (float)drawCall.Brush.Point2.x, (float)drawCall.Brush.Point2.y);
_brushParams2 = new Vec2((float)drawCall.Brush.CornerRadii, (float)drawCall.Brush.Feather);
}
}
public void RenderCalls(Canvas canvas, IReadOnlyList<DrawCall> drawCalls)
{
int index = 0;
foreach (var drawCall in drawCalls)
{
SetUniforms(drawCall);
_renderStates.BlendMode = new BlendMode(
BlendMode.Factor.SrcAlpha,
BlendMode.Factor.OneMinusSrcAlpha,
BlendMode.Equation.Add);
// Create vertex array with the correct size for all triangles in this draw call
var vertices = new VertexArray(PrimitiveType.Triangles, (uint)drawCall.ElementCount);
uint vertexIndex = 0;
for (int i = 0; i < drawCall.ElementCount; i += 3)
{
var a = canvas.Vertices[(int)canvas.Indices[index]];
var b = canvas.Vertices[(int)canvas.Indices[index + 1]];
var c = canvas.Vertices[(int)canvas.Indices[index + 2]];
vertices[vertexIndex++] = new SFMLVertex(
new Vector2f(a.x, a.y),
new SFML.Graphics.Color(a.r, a.g, a.b, a.a),
new Vector2f(a.u, a.v));
vertices[vertexIndex++] = new SFMLVertex(
new Vector2f(b.x, b.y),
new SFML.Graphics.Color(b.r, b.g, b.b, b.a),
new Vector2f(b.u, b.v));
vertices[vertexIndex++] = new SFMLVertex(
new Vector2f(c.x, c.y),
new SFML.Graphics.Color(c.r, c.g, c.b, c.a),
new Vector2f(c.u, c.v));
index += 3;
}
_window.Draw(vertices, _renderStates);
}
}
public void Dispose()
{
_shader?.Dispose();
_whiteTexture?.Dispose();
}
private static Mat4 ToFloat(Matrix4x4 matrix)
{
return new Mat4(
(float)matrix.M11, (float)matrix.M12, (float)matrix.M13, (float)matrix.M14,
(float)matrix.M21, (float)matrix.M22, (float)matrix.M23, (float)matrix.M24,
(float)matrix.M31, (float)matrix.M32, (float)matrix.M33, (float)matrix.M34,
(float)matrix.M41, (float)matrix.M42, (float)matrix.M43, (float)matrix.M44
);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment