How to make image zoom in & out with mouse wheel in Blazor?

6.3k views Asked by At

I want to zoom in & out an image in blazor on asp.net.

As I use for Google Maps, I want to move the image position by zooming and dragging the image with the mouse wheel.(I want to use an image file, not a Google map.)

Example of use

Is there a way to zoom in, zoom out and drag specific images in blazor?

1

There are 1 answers

3
CobyC On

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:

enter image description here

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