+@* GPU Canvas Demo
+ Demonstrates SKGLView with EnableRenderLoop for continuous animation,
+ SKRuntimeEffect.BuildShader() for compiling SkSL shaders, and
+ touch interaction via uniforms. The entire visual is rendered in SkSL —
+ no C# draw calls. C# only handles touch input and passes uniforms. *@
-
The canvas below is using WebGL. See the great FPS!
+
GPU Canvas
-
-
-
+
+ Pure SkSL lava lamp — all rendering in the shader.
+ Tap or drag to add a blob.
+
-
+
-
+
+
+
+ @($"{displayFps:F2}") fps
+
@code {
+
+ // SkSL shader source: a "lava lamp" metaball effect.
+ // 6 colored blobs orbit on Lissajous curves. When the user touches,
+ // an extra white-hot blob appears at the touch position and merges
+ // with the others. All rendering is per-pixel in the shader.
+ const string sksl = @"
+ // Uniforms passed from C# each frame
+ uniform float iTime; // elapsed seconds
+ uniform float2 iResolution; // canvas size in pixels
+ uniform float2 iTouchPos; // touch position normalized 0..1
+ uniform float iTouchActive; // 1.0 when touching, 0.0 otherwise
+
+ half4 main(float2 fragCoord) {
+ // Convert pixel coords to normalized UV and aspect-corrected coords
+ float2 uv = fragCoord / iResolution;
+ float aspect = iResolution.x / iResolution.y;
+ float2 st = float2(uv.x * aspect, uv.y);
+ float t = iTime;
+
+ // Metaball field: accumulate 1/r² contributions from each blob.
+ // 'weighted' tracks color contribution weighted by field strength.
+ float field = 0.0;
+ float3 weighted = float3(0.0);
+
+ // Color palette for the 6 orbiting blobs
+ float3 colors[6];
+ colors[0] = float3(1.0, 0.3, 0.4); // hot pink
+ colors[1] = float3(0.3, 0.7, 1.0); // sky blue
+ colors[2] = float3(1.0, 0.6, 0.1); // orange
+ colors[3] = float3(0.4, 1.0, 0.7); // mint
+ colors[4] = float3(0.7, 0.3, 1.0); // purple
+ colors[5] = float3(1.0, 0.9, 0.2); // yellow
+
+ // Each blob orbits on a Lissajous curve with unique phase and speed
+ for (int i = 0; i < 6; i++) {
+ float fi = float(i);
+ float phase = fi * 1.047; // evenly spaced: 2*pi/6
+ float speed = 0.3 + fi * 0.07;
+
+ // Lissajous orbit center
+ float2 center = float2(
+ aspect * 0.5 + 0.4 * sin(t * speed + phase) * cos(t * speed * 0.6 + fi),
+ 0.5 + 0.4 * cos(t * speed * 0.8 + phase * 1.3) * sin(t * speed * 0.4 + fi * 0.7)
+ );
+
+ // Metaball field: strength falls off as 1/r², creating smooth merging
+ float2 d = st - center;
+ float r = length(d);
+ float strength = 0.030 / (r * r + 0.002);
+ field += strength;
+ weighted += colors[i] * strength;
+ }
+
+ // Touch interaction: add an extra, larger blob at the touch position
+ if (iTouchActive > 0.5) {
+ float2 touchSt = float2(iTouchPos.x * aspect, iTouchPos.y);
+ float2 d = st - touchSt;
+ float r = length(d);
+ float strength = 0.050 / (r * r + 0.002);
+ field += strength;
+ // white-hot touch blob
+ weighted += float3(1.0, 0.95, 0.9) * strength;
+ }
+
+ // Normalize accumulated color by field strength for smooth blending
+ float3 blobColor = weighted / max(field, 0.001);
+
+ // Threshold the field into blob edges with smooth falloff
+ float edge = smoothstep(5.0, 8.0, field);
+ float innerGlow = smoothstep(8.0, 20.0, field) * 0.3;
+
+ // Subtly animated dark background
+ float3 bg = float3(0.03, 0.02, 0.08);
+ bg += float3(0.02, 0.01, 0.03) * sin(t * 0.2 + uv.y * 3.0);
+
+ // Outer glow: visible between the halo and solid edge
+ float halo = smoothstep(3.0, 5.0, field) * (1.0 - edge);
+
+ // Composite: background + halo + solid blobs with inner glow
+ float3 result = bg;
+ result += blobColor * halo * 0.4;
+ result = mix(result, blobColor * (1.0 + innerGlow), edge);
+
+ // Vignette: darken corners for depth
+ float2 vc = uv - 0.5;
+ float vignette = 1.0 - dot(vc, vc) * 0.8;
+ result *= vignette;
+
+ return half4(clamp(result, 0.0, 1.0), 1.0);
+ }
+ ";
+
+ // SKRuntimeShaderBuilder: compiled once via BuildShader(), then reused.
+ // Each frame we update uniforms and call Build() to get a new SKShader.
+ readonly SKRuntimeShaderBuilder builder = SKRuntimeEffect.BuildShader(sksl);
+ readonly long startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+
+ // The paint that is reused for drawing the shader quad.
+ readonly SKPaint shaderPaint = new();
+
+ // Touch state passed to the shader as uniforms.
+ float touchX = -1f, touchY = -1f;
+ float touchActive = 0f;
+ // FPS display value, updated ~4Hz.
+ double displayFps;
+ long lastUiUpdate;
+
+ // Rolling average FPS calculation.
int tickIndex = 0;
long tickSum = 0;
long[] tickList = new long[100];
long lastTick = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ // Handle pointer down: start tracking touch position.
+ void OnPointerDown(PointerEventArgs e)
+ {
+ if (!e.IsPrimary)
+ return;
+ touchX = (float)e.OffsetX;
+ touchY = (float)e.OffsetY;
+ touchActive = 1f;
+ }
+
+ // Handle pointer move: update touch position while pointer is pressed.
+ void OnPointerMove(PointerEventArgs e)
+ {
+ if (!e.IsPrimary || e.Buttons == 0)
+ return;
+ touchX = (float)e.OffsetX;
+ touchY = (float)e.OffsetY;
+ }
+
+ // Handle pointer up: clear touch state.
+ void OnPointerUp(PointerEventArgs e)
+ {
+ if (!e.IsPrimary)
+ return;
+ touchActive = 0f;
+ }
+
+ // Called every frame by SKGLView's render loop.
void OnPaintSurface(SKPaintGLSurfaceEventArgs e)
{
- // the the canvas and properties
var canvas = e.Surface.Canvas;
-
- // make sure the canvas is blank
- canvas.Clear(SKColors.White);
-
- using var paint = new SKPaint
- {
- IsAntialias = true,
- StrokeWidth = 5f,
- StrokeCap = SKStrokeCap.Round
- };
- using var font = new SKFont
- {
- Size = 24
- };
-
- var surfaceSize = e.Info.Size;
- var clockSize = Math.Min(surfaceSize.Width, surfaceSize.Height) * 0.4f;
- var center = new SKPoint(surfaceSize.Width / 2f, surfaceSize.Height / 2f);
- var now = DateTime.Now;
- var fps = GetCurrentFPS();
-
- // draw the fps counter
- canvas.DrawText($"{fps:0.00}fps", surfaceSize.Width / 2, surfaceSize.Height - 10f, SKTextAlign.Center, font, paint);
-
- // draw the clock
- canvas.RotateDegrees(-90f, center.X, center.Y);
-
- // hours
- paint.StrokeWidth = 3f;
- canvas.Save();
- canvas.Translate(center);
- canvas.RotateDegrees(360f * (now.Hour / 12f));
- canvas.DrawLine(0, 0, clockSize * 0.4f, 0, paint);
- canvas.Restore();
-
- // minutes
- paint.StrokeWidth = 2f;
- canvas.Save();
- canvas.Translate(center);
- canvas.RotateDegrees(360f * (now.Minute / 60f));
- canvas.DrawLine(0, 0, clockSize * 0.6f, 0, paint);
- canvas.Restore();
-
- // seconds
- paint.StrokeWidth = 1f;
- canvas.Save();
- canvas.Translate(center);
- canvas.RotateDegrees(360f * ((now.Second * 1000f + now.Millisecond) / 1000f / 60f));
- canvas.DrawLine(0, 0, clockSize * 0.8f, 0, paint);
- canvas.Restore();
-
- // center
- canvas.DrawCircle(center, 10f, paint);
-
- // border
- paint.Style = SKPaintStyle.Stroke;
- canvas.DrawCircle(center, clockSize * 0.9f, paint);
+ var width = e.Info.Width;
+ var height = e.Info.Height;
+
+ // Update uniforms — no allocations, reuses the builder's internal storage.
+ var elapsed = (float)(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - startTime) / 1000f;
+ builder.Uniforms["iTime"] = elapsed;
+ builder.Uniforms["iResolution"] = new float[] { width, height };
+ builder.Uniforms["iTouchPos"] = new float[] {
+ touchActive > 0 ? touchX / width : -1f,
+ touchActive > 0 ? touchY / height : -1f
+ };
+ builder.Uniforms["iTouchActive"] = touchActive;
+
+ // Build() creates a shader from cached effect + current uniforms.
+ // Draw a full-screen quad to run the shader on every pixel.
+ using var shader = builder.Build();
+ shaderPaint.Shader = shader;
+ canvas.DrawRect(0, 0, width, height, shaderPaint);
+
+ displayFps = GetCurrentFPS();
+
+ // Throttle Blazor UI updates to ~4Hz for the FPS counter.
+ var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ if (now - lastUiUpdate > 250)
+ {
+ lastUiUpdate = now;
+ _ = InvokeAsync(StateHasChanged);
+ }
}
double GetCurrentFPS()
@@ -88,14 +205,11 @@
var newTick = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var delta = newTick - lastTick;
lastTick = newTick;
-
tickSum -= tickList[tickIndex];
tickSum += delta;
tickList[tickIndex] = delta;
-
if (++tickIndex == tickList.Length)
tickIndex = 0;
-
return 1000.0 / ((double)tickSum / tickList.Length);
}
}
diff --git a/samples/Basic/BlazorWebAssembly/SkiaSharpSample/Pages/GPU.razor.css b/samples/Basic/BlazorWebAssembly/SkiaSharpSample/Pages/GPU.razor.css
new file mode 100644
index 0000000000..ad7aa7b51e
--- /dev/null
+++ b/samples/Basic/BlazorWebAssembly/SkiaSharpSample/Pages/GPU.razor.css
@@ -0,0 +1,19 @@
+.canvas-container {
+ position: relative;
+}
+
+.overlay-stats {
+ position: absolute;
+ top: 12px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 1;
+ background: rgba(255, 255, 255, 0.5);
+ border: 1px solid rgba(255, 255, 255, 0.6);
+ border-radius: 8px;
+ padding: 6px 16px;
+ backdrop-filter: blur(4px);
+ color: black;
+ font-size: 0.85rem;
+ font-weight: 500;
+}
diff --git a/samples/Basic/BlazorWebAssembly/SkiaSharpSample/Pages/Home.razor b/samples/Basic/BlazorWebAssembly/SkiaSharpSample/Pages/Home.razor
index 2011a936c5..41caa361cd 100644
--- a/samples/Basic/BlazorWebAssembly/SkiaSharpSample/Pages/Home.razor
+++ b/samples/Basic/BlazorWebAssembly/SkiaSharpSample/Pages/Home.razor
@@ -1,44 +1,106 @@
-@page "/"
+@page "/"
+
+@* Home Page
+ Demonstrates basic SKCanvasView usage with gradients, shapes, and text.
+ Shows SKShader.CreateRadialGradient, DrawCircle, DrawText, and SKFont. *@
SkiaSharp
-
SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.
+
+ SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on
+ Google's Skia Graphics Library, providing comprehensive 2D rendering
+ across mobile, server, and desktop.
+
-
-
-
+
-
+
-
-
@code {
+ // The paint that is reused for drawing circles.
+ readonly SKPaint circlePaint = new()
+ {
+ IsAntialias = true,
+ Style = SKPaintStyle.Fill
+ };
+
+ // The circles with normalized positions/sizes and colors.
+ readonly (float xf, float yf, float rf, SKColor color)[] circles =
+ {
+ (0.2f, 0.3f, 0.10f, new SKColor(0xFF, 0x4D, 0x66, 0xCC)), // hot pink
+ (0.75f, 0.25f, 0.08f, new SKColor(0x4D, 0xB3, 0xFF, 0xCC)), // sky blue
+ (0.15f, 0.7f, 0.07f, new SKColor(0xFF, 0x99, 0x1A, 0xCC)), // orange
+ (0.8f, 0.7f, 0.12f, new SKColor(0x66, 0xFF, 0xB3, 0xCC)), // mint
+ (0.5f, 0.15f, 0.06f, new SKColor(0xB3, 0x4D, 0xFF, 0xCC)), // purple
+ (0.4f, 0.8f, 0.09f, new SKColor(0xFF, 0xE6, 0x33, 0xCC)), // yellow
+ };
+
+ // The paint for the background gradient, reused each frame.
+ readonly SKPaint bgPaint = new()
+ {
+ IsAntialias = true
+ };
+
+ // The paint for drawing the centered text.
+ readonly SKPaint textPaint = new()
+ {
+ Color = SKColors.White,
+ IsAntialias = true,
+ Style = SKPaintStyle.Fill
+ };
+
+ // The font for drawing the centered text.
+ readonly SKFont textFont = new()
+ {
+ Size = 48
+ };
+
void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
- // the the canvas and properties
var canvas = e.Surface.Canvas;
+ var width = e.Info.Width;
+ var height = e.Info.Height;
+ var center = new SKPoint(width / 2f, height / 2f);
+ var radius = Math.Max(width, height) / 2f;
- // make sure the canvas is blank
+ // Clear to white before drawing the background gradient and circles.
canvas.Clear(SKColors.White);
- // decide what the text looks like
- using var paint = new SKPaint
- {
- Color = SKColors.Black,
- IsAntialias = true,
- Style = SKPaintStyle.Fill
- };
- using var font = new SKFont
- {
- Size = 24
- };
-
- // draw some text
- var coord = new SKPoint(e.Info.Width / 2, (e.Info.Height + font.Size) / 2);
- canvas.DrawText("SkiaSharp", coord, SKTextAlign.Center, font, paint);
- }
+ // Background radial gradient centered on the canvas, fading from blue to purple.
+ using var bgShader = SKShader.CreateRadialGradient(
+ center,
+ radius,
+ [
+ new SKColor(0x44, 0x88, 0xFF),
+ new SKColor(0x88, 0x33, 0xCC)
+ ],
+ SKShaderTileMode.Clamp);
+ bgPaint.Shader = bgShader;
+ // Draw a full-screen rectangle with the background gradient shader.
+ canvas.DrawRect(0, 0, width, height, bgPaint);
+
+ // Draw the circles with their specified colors, positions, and sizes.
+ foreach (var (xf, yf, rf, color) in circles)
+ {
+ circlePaint.Color = color;
+ canvas.DrawCircle(
+ xf * width,
+ yf * height,
+ rf * Math.Min(width, height),
+ circlePaint);
+ }
+
+ // Draw centered "SkiaSharp" text
+ canvas.DrawText(
+ "SkiaSharp",
+ center.X,
+ center.Y + textFont.Size / 3f,
+ SKTextAlign.Center,
+ textFont,
+ textPaint);
+ }
}
diff --git a/samples/Basic/BlazorWebAssembly/SkiaSharpSample/wwwroot/css/app.css b/samples/Basic/BlazorWebAssembly/SkiaSharpSample/wwwroot/css/app.css
index e9c2cb2d06..877f844090 100644
--- a/samples/Basic/BlazorWebAssembly/SkiaSharpSample/wwwroot/css/app.css
+++ b/samples/Basic/BlazorWebAssembly/SkiaSharpSample/wwwroot/css/app.css
@@ -1,5 +1,12 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+}
+
+#app {
+ height: 100%;
}
h1:focus {
@@ -22,6 +29,12 @@ a, .btn-link {
.content {
padding-top: 1.1rem;
+ padding-bottom: 1rem;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ overflow: hidden;
}
.valid.modified:not([type=checkbox]) {
@@ -105,9 +118,11 @@ code {
.canvas-container {
line-height: 1;
+ flex: 1;
+ min-height: 0;
}
.canvas-container canvas {
width: 100%;
- height: 300px;
+ height: 100%;
}
diff --git a/samples/Basic/BlazorWebAssembly/SkiaSharpSample/wwwroot/index.html b/samples/Basic/BlazorWebAssembly/SkiaSharpSample/wwwroot/index.html
index 6394d03cf1..c576997971 100644
--- a/samples/Basic/BlazorWebAssembly/SkiaSharpSample/wwwroot/index.html
+++ b/samples/Basic/BlazorWebAssembly/SkiaSharpSample/wwwroot/index.html
@@ -5,8 +5,8 @@
SkiaSharpSample
-
-
+
+