Note:
This component enables you to zoom by pressing shift while mouse wheel up (zoom out) or mouse wheel down (zoom in) and move the image by pressing mouse button 1 down while moving. (it's more panning than dragging it)
Restriction in Blazor: (at the time of writing this)
- The biggest issue at the moment is not having access to the mouse
OffsetX and OffsetY within the html element as described here and also here, so the moving of the image has to be done using CSS only.
- The reason I used Shift for scrolling is because scrolling is not being blocked or disabled as described here even with
@onscroll:stopPropagation, @onwheel:stopPropagation, @onmousewheel:stopPropagation and/or @onscroll:preventDefault, @onwheel:preventDefault, @onmousewheel:preventDefault set on the parent mainImageContainer element. The screen will still scroll left and right if the content is wider than the viewable page.
Solution:
The Zooming part is pretty straight forward, all you need to do is set the transform:scale(n); property in the @onmousewheel event.
The moving of the image is a bit more complex because there is no reference to where the mouse pointer is in relation to the image or element boundaries. (OffsetX and OffsetY)
The only thing we can determine is if a mouse button is pressed and then calculate what direction the mouse is moving in left, right, up or down.
The idea is then to move the position of element with the image by setting the top and left CSS values as percentages.
This component code:
@using System.Text;
<div id="mainImageContainer" style="display: block;width:@($"{ImageWidthInPx}px");height:@($"{ImageHeightInPx}px");overflow: hidden;">
<div id="imageMover"
@onmousewheel="MouseWheelZooming"
style="@MoveImageStyle">
<div id="imageContainer"
@onmousemove="MouseMoving"
style="@ZoomImageStyle">
@*this div is used just for moving around when zoomed*@
</div>
</div>
</div>
@if (ShowResetButton)
{
<div style="display:block">
<button @onclick="ResetImgage">Reset</button>
</div>
}
@code{
/// <summary>
/// The path or url of the image
/// </summary>
[Parameter]
public string ImageUrlPath { get; set; }
/// <summary>
/// The width of the image
/// </summary>
[Parameter]
public int ImageWidthInPx { get; set; }
/// <summary>
/// The height of the image
/// </summary>
[Parameter]
public int ImageHeightInPx { get; set; }
/// <summary>
/// Set to true to show the reset button
/// </summary>
[Parameter]
public bool ShowResetButton { get; set; }
/// <summary>
/// Set the amount the image is scaled by, default is 0.1f
/// </summary>
[Parameter]
public double DefaultScaleBy { get; set; } = 0.1f;
/// <summary>
/// The Maximum the image can scale to, default = 5f
/// </summary>
[Parameter]
public double ScaleToMaximum { get; set; } = 5f;
/// <summary>
/// Set the speed at which the image is moved by, default 2.
/// 2 or 3 seems to work best.
/// </summary>
[Parameter]
public double DefaultMoveBy { get; set; } = 2;
//defaults
double _CurrentScale = 1.0f;
double _PositionLeft = 0;
double _PositionTop = 0;
double _OldClientX = 0;
double _OldClientY = 0;
double _DefaultMinPosition = 0;//to the top and left
double _DefaultMaxPosition = 0;//to the right and down
//the default settings used to display the image in the child div
private Dictionary<string, string> _ImageContainerStyles;
Dictionary<string, string> ImageContainerStyles
{
get
{
if (_ImageContainerStyles == null)
{
_ImageContainerStyles = new Dictionary<string, string>();
_ImageContainerStyles.Add("width", "100%");
_ImageContainerStyles.Add("height", "100%");
_ImageContainerStyles.Add("position", "relative");
_ImageContainerStyles.Add("background-size", "contain");
_ImageContainerStyles.Add("background-repeat", "no-repeat");
_ImageContainerStyles.Add("background-position", "50% 50%");
_ImageContainerStyles.Add("background-image", $"URL({ImageUrlPath})");
}
return _ImageContainerStyles;
}
}
private Dictionary<string, string> _MovingContainerStyles;
Dictionary<string, string> MovingContainerStyles
{
get
{
if (_MovingContainerStyles == null)
{
InvokeAsync(ResetImgage);
}
return _MovingContainerStyles;
}
}
protected async Task ResetImgage()
{
_PositionLeft = 0;
_PositionTop = 0;
_DefaultMinPosition = 0;
_DefaultMaxPosition = 0;
_CurrentScale = 1.0f;
_MovingContainerStyles = new Dictionary<string, string>();
_MovingContainerStyles.Add("width", "100%");
_MovingContainerStyles.Add("height", "100%");
_MovingContainerStyles.Add("position", "relative");
_MovingContainerStyles.Add("left", $"{_PositionLeft}%");
_MovingContainerStyles.TryAdd("top", $"{_PositionTop}%");
await InvokeAsync(StateHasChanged);
}
string ZoomImageStyle { get => DictionaryToCss(ImageContainerStyles); }
string MoveImageStyle { get => DictionaryToCss(MovingContainerStyles); }
private string DictionaryToCss(Dictionary<string, string> styleDictionary)
{
StringBuilder sb = new StringBuilder();
foreach (var kvp in styleDictionary.AsEnumerable())
{
sb.AppendFormat("{0}:{1};", kvp.Key, kvp.Value);
}
return sb.ToString();
}
protected async void MouseMoving(MouseEventArgs e)
{
//if the mouse button 1 is not down exit the function
if (e.Buttons != 1)
{
_OldClientX = e.ClientX;
_OldClientY = e.ClientY;
return;
}
//get the % of the current scale to move by at least the default move speed plus any scaled changes
//basically the bigger the image the faster it moves..
double scaleFrac = (_CurrentScale / ScaleToMaximum);
double scaleMove = (DefaultMoveBy * (DefaultMoveBy * scaleFrac));
//moving mouse right
if (_OldClientX < e.ClientX)
{
if ((_PositionLeft - DefaultMoveBy) <= _DefaultMaxPosition)
{
_PositionLeft += scaleMove;
}
}
//moving mouse left
if (_OldClientX > e.ClientX)
{
//if (_DefaultMinPosition < (_PositionLeft - DefaultMoveBy))
if ((_PositionLeft + DefaultMoveBy) >= _DefaultMinPosition)
{
_PositionLeft -= scaleMove;
}
}
//moving mouse down
if (_OldClientY < e.ClientY)
{
//if ((_PositionTop + DefaultMoveBy) <= _DefaultMaxPosition)
if ((_PositionTop - DefaultMoveBy) <= _DefaultMaxPosition)
{
_PositionTop += scaleMove;
}
}
//moving mouse up
if (_OldClientY > e.ClientY)
{
//if ((_PositionTop - DefaultMoveBy) > _DefaultMinPosition)
if ((_PositionTop + DefaultMoveBy) >= _DefaultMinPosition)
{
_PositionTop -= scaleMove;
}
}
_OldClientX = e.ClientX;
_OldClientY = e.ClientY;
await UpdateScaleAndPosition();
}
async Task<double> IncreaseScale()
{
return await Task.Run(() =>
{
//increase the scale first then calculate the max and min positions
_CurrentScale += DefaultScaleBy;
double scaleFrac = (_CurrentScale / ScaleToMaximum);
double scaleDiff = (DefaultMoveBy + (DefaultMoveBy * scaleFrac));
double scaleChange = DefaultMoveBy + scaleDiff;
_DefaultMaxPosition += scaleChange;
_DefaultMinPosition -= scaleChange;
return _CurrentScale;
});
}
async Task<double> DecreaseScale()
{
return await Task.Run(() =>
{
_CurrentScale -= DefaultScaleBy;
double scaleFrac = (_CurrentScale / ScaleToMaximum);
double scaleDiff = (DefaultMoveBy + (DefaultMoveBy * scaleFrac));
double scaleChange = DefaultMoveBy + scaleDiff;
_DefaultMaxPosition -= scaleChange;
_DefaultMinPosition += scaleChange;//DefaultMoveBy;
//fix descaling, move the image back into view when descaling (zoomin out)
if (_CurrentScale <= 1)
{
_PositionLeft = 0;
_PositionTop = 0;
}
else
{
//left can not be more than max position
_PositionLeft = (_DefaultMaxPosition < _PositionLeft) ? _DefaultMaxPosition : _PositionLeft;
//top can not be more than max position
_PositionTop = (_DefaultMaxPosition < _PositionTop) ? _DefaultMaxPosition : _PositionTop;
//left can not be less than min position
_PositionLeft = (_DefaultMinPosition > _PositionLeft) ? _DefaultMinPosition : _PositionLeft;
//top can not be less than min position
_PositionTop = (_DefaultMinPosition > _PositionTop) ? _DefaultMinPosition : _PositionTop;
}
return _CurrentScale;
});
}
protected async void MouseWheelZooming(WheelEventArgs e)
{
//holding shift stops the page from scrolling
if (e.ShiftKey == true)
{
if (e.DeltaY > 0)
{
_CurrentScale = ((_CurrentScale + DefaultScaleBy) >= 5) ? _CurrentScale = 5f : await IncreaseScale();
}
if (e.DeltaY < 0)
{
_CurrentScale = ((_CurrentScale - DefaultScaleBy) <= 0) ? _CurrentScale = DefaultScaleBy : await DecreaseScale();
}
await UpdateScaleAndPosition();
}
}
/// <summary>
/// Refresh the values in the moving style dictionary that is used to position the image.
/// </summary>
async Task UpdateScaleAndPosition()
{
await Task.Run(() =>
{
if (!MovingContainerStyles.TryAdd("transform", $"scale({_CurrentScale})"))
{
MovingContainerStyles["transform"] = $"scale({_CurrentScale})";
}
if (!MovingContainerStyles.TryAdd("left", $"{_PositionLeft}%"))
{
MovingContainerStyles["left"] = $"{_PositionLeft}%";
}
if (!MovingContainerStyles.TryAdd("top", $"{_PositionTop}%"))
{
MovingContainerStyles["top"] = $"{_PositionTop}%";
}
});
}
}
This is the usage:
@page "/"
@using BlazorWasmApp.Components
Welcome to your new app.
<ZoomableImageComponent ImageUrlPath="images/Capricorn.png"
ImageWidthInPx=400
ImageHeightInPx=300
ShowResetButton=true
DefaultScaleBy=0.1f />
and this is the result:

I only tested this in chrome on a desktop computer without touch input.