@inject IJSRuntime JS @using Excubo.Blazor.Canvas.Contexts
Zoom: @($"{ZoomScale:F2}x") | Mouse: (@($"{MouseX:F0}"), @($"{MouseY:F0}")) | World: (@($"{WorldMouseX:F2}m"), @($"{WorldMouseY:F2}m")) | Translate: (@($"{CanvasTranslateX:F2}"), @($"{CanvasTranslateY:F2}"))
@code { private ElementReference CanvasRef; private ElementReference ViewContainerRef; private double ZoomScale = 1.0; private const double MIN_ZOOM = 0.1; private const double MAX_ZOOM = 5.0; private const double BASE_PIXELS_PER_METER = 50.0; private bool IsMouseInCanvas = false; private double MouseX; private double MouseY; private double OriginX = 0; private double OriginY = 0; private double WorldMouseX; private double WorldMouseY; private double CanvasWidth; private double CanvasHeight; private double CanvasTranslateX = 0; private double CanvasTranslateY = 0; protected override async Task OnAfterRenderAsync(bool first_render) { await base.OnAfterRenderAsync(first_render); if (!first_render) return; var parentSize = await JS.InvokeAsync("getElementSize", ViewContainerRef); CanvasWidth = parentSize.Width; CanvasHeight = parentSize.Height; await JS.InvokeVoidAsync("setCanvasSize", CanvasRef, CanvasWidth, CanvasHeight); CanvasTranslateX = CanvasWidth / 2; CanvasTranslateY = CanvasHeight / 2; await DrawCanvas(); } private async Task DrawCanvas() { await using var ctx = await JS.GetContext2DAsync(CanvasRef); await ctx.ClearRectAsync(0, 0, CanvasWidth, CanvasHeight); await ctx.SaveAsync(); await ctx.TranslateAsync(CanvasTranslateX, CanvasTranslateY); await ctx.ScaleAsync(ZoomScale, ZoomScale); await DrawGrid(ctx); await DrawAxes(ctx); await ctx.RestoreAsync(); } private async Task DrawAxes(Context2D ctx) { double originCanvasX = OriginX * BASE_PIXELS_PER_METER; double originCanvasY = OriginY * BASE_PIXELS_PER_METER; await ctx.FillStyleAsync("red"); await ctx.BeginPathAsync(); await ctx.ArcAsync(originCanvasX, originCanvasY, 10 / ZoomScale, 0, Math.PI * 2); await ctx.FillAsync(FillRule.NonZero); await ctx.LineWidthAsync(2 / ZoomScale); await ctx.StrokeStyleAsync("blue"); await ctx.BeginPathAsync(); await ctx.MoveToAsync(-CanvasWidth / ZoomScale, originCanvasY); await ctx.LineToAsync(CanvasWidth / ZoomScale, originCanvasY); await ctx.StrokeAsync(); await ctx.StrokeStyleAsync("red"); await ctx.BeginPathAsync(); await ctx.MoveToAsync(originCanvasX, -CanvasHeight / ZoomScale); await ctx.LineToAsync(originCanvasX, CanvasHeight / ZoomScale); await ctx.StrokeAsync(); double gridSpacingMeters = GetGridSpacingMeters(); double arrowLength = gridSpacingMeters * BASE_PIXELS_PER_METER; double arrowHeadSize = 16 / ZoomScale; await ctx.FillStyleAsync("blue"); await ctx.StrokeStyleAsync("blue"); await ctx.LineWidthAsync(4 / ZoomScale); await ctx.BeginPathAsync(); await ctx.MoveToAsync(originCanvasX, originCanvasY); await ctx.LineToAsync(originCanvasX + arrowLength, originCanvasY); await ctx.StrokeAsync(); await ctx.BeginPathAsync(); await ctx.MoveToAsync(originCanvasX + arrowLength, originCanvasY); await ctx.LineToAsync(originCanvasX + arrowLength - arrowHeadSize, originCanvasY - arrowHeadSize / 2); await ctx.LineToAsync(originCanvasX + arrowLength - arrowHeadSize, originCanvasY + arrowHeadSize / 2); await ctx.ClosePathAsync(); await ctx.FillAsync(FillRule.NonZero); await ctx.FillStyleAsync("red"); await ctx.StrokeStyleAsync("red"); await ctx.LineWidthAsync(4 / ZoomScale); await ctx.BeginPathAsync(); await ctx.MoveToAsync(originCanvasX, originCanvasY); await ctx.LineToAsync(originCanvasX, originCanvasY + arrowLength); await ctx.StrokeAsync(); await ctx.BeginPathAsync(); await ctx.MoveToAsync(originCanvasX, originCanvasY + arrowLength); await ctx.LineToAsync(originCanvasX - arrowHeadSize / 2, originCanvasY + arrowLength - arrowHeadSize); await ctx.LineToAsync(originCanvasX + arrowHeadSize / 2, originCanvasY + arrowLength - arrowHeadSize); await ctx.ClosePathAsync(); await ctx.FillAsync(FillRule.NonZero); } private async Task DrawGrid(Context2D ctx) { await ctx.StrokeStyleAsync("rgba(200, 200, 200, 0.4)"); await ctx.LineWidthAsync(1 / ZoomScale); await ctx.SetLineDashAsync(new double[] { 5 / ZoomScale, 5 / ZoomScale }); double gridSpacingMeters = GetGridSpacingMeters(); double gridSpacingPixels = gridSpacingMeters * BASE_PIXELS_PER_METER; double visibleLeft = -CanvasTranslateX / ZoomScale; double visibleRight = (CanvasWidth - CanvasTranslateX) / ZoomScale; double visibleTop = -CanvasTranslateY / ZoomScale; double visibleBottom = (CanvasHeight - CanvasTranslateY) / ZoomScale; double startX = Math.Floor(visibleLeft / gridSpacingPixels) * gridSpacingPixels; double startY = Math.Floor(visibleTop / gridSpacingPixels) * gridSpacingPixels; for (double x = startX; x <= visibleRight; x += gridSpacingPixels) { await ctx.BeginPathAsync(); await ctx.MoveToAsync(x, visibleTop); await ctx.LineToAsync(x, visibleBottom); await ctx.StrokeAsync(); } for (double y = startY; y <= visibleBottom; y += gridSpacingPixels) { await ctx.BeginPathAsync(); await ctx.MoveToAsync(visibleLeft, y); await ctx.LineToAsync(visibleRight, y); await ctx.StrokeAsync(); } await ctx.SetLineDashAsync(new double[] { }); } private double GetGridSpacingMeters() { if (BASE_PIXELS_PER_METER * ZoomScale >= 300) return 0.2; else if (BASE_PIXELS_PER_METER * ZoomScale >= 150) return 0.5; else if (BASE_PIXELS_PER_METER * ZoomScale >= 75) return 1.0; else if (BASE_PIXELS_PER_METER * ZoomScale >= 40) return 2.0; else if (BASE_PIXELS_PER_METER * ZoomScale >= 20) return 5.0; else if (BASE_PIXELS_PER_METER * ZoomScale >= 10) return 10.0; else return 20.0; } private double CanvasToWorldX(double canvasX) { return (canvasX - CanvasTranslateX) / ZoomScale / BASE_PIXELS_PER_METER - OriginX; } private double CanvasToWorldY(double canvasY) { return (canvasY - CanvasTranslateY) / ZoomScale / BASE_PIXELS_PER_METER - OriginY; } private double WorldToCanvasX(double worldX) { return (worldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale + CanvasTranslateX; } private double WorldToCanvasY(double worldY) { return (worldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale + CanvasTranslateY; } private async Task HandleMouseMove(MouseEventArgs e) { MouseX = e.OffsetX; MouseY = e.OffsetY; IsMouseInCanvas = true; WorldMouseX = CanvasToWorldX(MouseX); WorldMouseY = CanvasToWorldY(MouseY); StateHasChanged(); if (e.Buttons == 4) { CanvasTranslateX += e.MovementX; CanvasTranslateY -= e.MovementY; await DrawCanvas(); } } private async Task HandleMouseLeave(MouseEventArgs e) { IsMouseInCanvas = false; MouseX = 0; MouseY = 0; StateHasChanged(); await DrawCanvas(); } private async Task HandleWheel(WheelEventArgs e) { if (e.Buttons == 4) return; const double zoomFactor = 0.1; double oldZoom = ZoomScale; if (e.DeltaY < 0) ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor)); else ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * (1 - zoomFactor)); if (Math.Abs(ZoomScale - oldZoom) < 0.001) return; MouseX = e.OffsetX; MouseY = e.OffsetY; WorldMouseX = CanvasToWorldX(MouseX); WorldMouseY = CanvasToWorldY(MouseY); double zoomPointWorldX = (MouseX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX; double zoomPointWorldY = (MouseY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY; double newZoomPointCanvasX = (zoomPointWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale; double newZoomPointCanvasY = (zoomPointWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale; CanvasTranslateX = MouseX - newZoomPointCanvasX; CanvasTranslateY = MouseY - newZoomPointCanvasY; StateHasChanged(); await DrawCanvas(); } private async Task ResetView() { CanvasTranslateX = CanvasWidth / 2; CanvasTranslateY = CanvasHeight / 2; ZoomScale = 1.0; StateHasChanged(); await DrawCanvas(); } public class DomRect { public double Width { get; set; } public double Height { get; set; } } }