Creating a circle in SVG that cannot be scaled using Javascript

I'm working on a map project using JavaScript and SVG for drawing the lines.

One feature I'd like to implement is the ability to search for a specific road, and if found, display a circle on the map.

I understand how to draw a circle in SVG, but my challenge lies in ensuring that the size of the circle remains consistent regardless of the zoom level. The roads on my map maintain this consistency by simply adding

vector-effect="non-scaling-stroke"

to the line attributes.

A sample line element looks like this:

<line vector-effect="non-scaling-stroke" stroke-width="3" id='line1' x1='0' y1='0' x2='0' y2='0' style='stroke:rgb(255,215,0);'/> 

And a sample circle element looks like this:

<circle id="pointCircle" cx="0" cy="0" r="10" stroke="red" stroke-width="1" fill="red"/>

Is there a way to define the circle as "non-scaling" to maintain its size?

Answer №1

I spent some time figuring it out, but I finally cracked the math puzzle. This method involves three key steps:

  1. Include this particular script on your webpage (in conjunction with the SVGPan.js script), for example,
    <script xlink:href="SVGPanUnscale.js"></script>
  2. Identify the elements you do not want to scale (e.g., place them in a group with a distinct class or ID, or assign a specific class to each element) and instruct the script how to locate those items, for instance,
    unscaleEach("g.non-scaling > *, circle.non-scaling");
  3. Utilize transform="translate(…,…)" to position each element on the diagram, as opposed to cx="…" cy="…".

By following these steps alone, scaling and panning with SVGPan will have no impact on the scale (or rotation, or skew) of designated elements.

Demo:

Library

// Copyright 2012 © Gavin Kistner, <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="bf9effcfd7cdd0d8c591d1dacb">[email protected]</a>
// License: http://phrogz.net/JS/_ReuseLicense.txt

// Cancel out the scaling for selected elements inside an SVGPan viewport
function unscaleEach(selector){
  if (!selector) selector = "g.non-scaling > *";
  window.addEventListener('mousewheel',     unzoom, false);
  window.addEventListener('DOMMouseScroll', unzoom, false);
  function unzoom(evt){
    // getRoot is a global function exposed by SVGPan
    var r = getRoot(evt.target.ownerDocument);
    [].forEach.call(r.querySelectorAll(selector), unscale);
  }
}

// Counteract all transforms applied above an element.
// Apply a translation to the element so it stays at a local position
function unscale(el){
  var svg = el.ownerSVGElement;
  var xf = el.scaleIndependentXForm;
  if (!xf){
    // Keep a single transform matrix in the stack for fighting transformations
    // Be sure to apply this transform after existing transforms (translate)
    xf = el.scaleIndependentXForm = svg.createSVGTransform();
    el.transform.baseVal.appendItem(xf);
  }
  var m = svg.getTransformToElement(el.parentNode);
  m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
  xf.setMatrix(m);
}

Demo Code

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <title>Scale-Independent Elements</title>
  <style>
    polyline { fill:none; stroke:#000; vector-effect:non-scaling-stroke; }
    circle, polygon { fill:#ff9; stroke:#f00; opacity:0.5 }
  </style>
  <g id="viewport" transform="translate(500,300)">
    <polyline points="-100,-50 50,75 100,50" />
    <g class="non-scaling">
      <circle  transform="translate(-100,-50)" r="10" />
      <polygon transform="translate(100,50)" points="0,-10 10,0 0,10 -10,0" />
    </g>
    <circle class="non-scaling" transform="translate(50,75)" r="10" />
  </g>
  <script xlink:href="SVGPan.js"></script>
  <script xlink:href="SVGPanUnscale.js"></script>
  <script>
    unscaleEach("g.non-scaling > *, circle.non-scaling");
  </script>
</svg>

Answer №2

If you're interested in a completely static approach to achieve this, one option could be combining non-scaling-stroke with markers. This way, the markers can adjust relative to the stroke-width.

To explain further, you could enclose the circles within a <marker> element and then utilize those markers as needed.

<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 2000">
    <marker id="Triangle"
      viewBox="0 0 10 10" refX="0" refY="5" 
      markerUnits="strokeWidth"
      markerWidth="4" markerHeight="3"
      orient="auto">
      <path d="M 0 0 L 10 5 L 0 10 z" />
    </marker>
        <path d="M 100 100 l 200 0" vector-effect="non-scaling-stroke"
        fill="none" stroke="black" stroke-width="10" 
        marker-end="url(#Triangle)"  />
        <path d="M 100 200 l 200 0" 
        fill="none" stroke="black" stroke-width="10" 
        marker-end="url(#Triangle)"  />
</svg>

You can also try making adjustments and viewing it here. It's worth noting that the svg specifications are not entirely clear on how this scenario should behave (as markers are not included in SVG Tiny 1.2, and vector-effect is absent in SVG 1.1). My assumption is that it should impact the marker size, although current viewers may not support this feature (try using a viewer like Opera or Chrome).

Answer №3

It appears that some adjustments were made in webkit (potentially linked to this issue: 320635) and the new transform doesn't persist when simply added in that manner.

transform.baseVal.appendItem

This approach seems to yield better results, even functioning correctly in IE 10.

UPDATE: Revised the code to cater to a more general scenario involving multiple translate transformations upfront and possible additional transformations afterwards. The first matrix transformation following all translates should be kept for unscale purposes.

translate(1718.07 839.711) translate(0 0) matrix(0.287175 0 0 0.287175 0 0) rotate(45 100 100)

function unscale()
{
    var xf = this.ownerSVGElement.createSVGTransform();
    var m = this.ownerSVGElement.getTransformToElement(this.parentNode);
    m.e = m.f = 0; // Disregard any translations carried out up to this point
    xf.setMatrix(m);

    // Maintain a single transform matrix in the stack for combating transformations
    // Make sure to apply this transform after existing transforms (translate)
    var SVG_TRANSFORM_MATRIX = 1;
    var SVG_TRANSFORM_TRANSLATE = 2;
    var baseVal = this.transform.baseVal;
    if(baseVal.numberOfItems == 0)
        baseVal.appendItem(xf);
    else
    {
        for(var i = 0; i < baseVal.numberOfItems; ++i)
        {
            if(baseVal.getItem(i).type == SVG_TRANSFORM_TRANSLATE && i == baseVal.numberOfItems - 1)
        {
                baseVal.appendItem(xf);
            }

            if(baseVal.getItem(i).type != SVG_TRANSFORM_TRANSLATE)
            {
                if(baseVal.getItem(i).type == SVG_TRANSFORM_MATRIX)
                    baseVal.replaceItem(xf, i);
                else
                    baseVal.insertItemBefore(xf, i);
                break;
            }
        }
    }
}

UPDATE 2: Chrome removed getTransformToElement inexplicably, hence the need to manually retrieve the matrix:

var m = this.parentNode.getScreenCTM().inverse().multiply(this.ownerSVGElement.getScreenCTM());

Answer №4

The issue is addressed here and also discussed in detail here

Current browser behavior may not align with expectations, which requires the application of an inverse transform to adjust the zoom (scale) on <marker> contents. For instance, using transform: scaleX(5) on a <marker> user should be accompanied by

transform: translate(...) scaleX(0.2)
within <pattern>, taking into account possible values for x, y, width, height, and transform-origin within the pattern if necessary.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What could be causing the "Error - Only secure origins are permitted" message to appear for my service worker?

Whenever I attempt to implement a service worker on my progressive web application page, why does the browser console display this specific error message? ERROR "Uncaught (in promise) DOMException: Only secure origins are allowed JavaScript Code: ...

Learn how to integrate Bootstrap with Vue.js TreeView in this tutorial

If you're looking to create a treeview using Vue.js, the code structure would resemble something like this: HTML: <!-- item template --> <script type="text/x-template" id="item-template"> <li> <div ...

Communication between the content script and background page in a chrome extension is not functioning correctly as intended

Displayed below is the code I posted: manifest.json { "manifest_version": 2, "name": "Demo", "description": "all_frames test", "version": "1.0", "background": { "scripts": ["background.js"] }, "content_scripts": [{ "matches": ...

Encounter issues with v-for functionality when using Internet Explorer 11

I am facing an issue with the rendering of a table in a simple Vue instance. The document displays correctly on Firefox and Chrome, however, I encounter an error in IE11: [Vue warn]: Error when rendering root instance. I assumed that Vue is compatible ...

Is there a more efficient approach to displaying a list of elements and sharing state in React with TypeScript?

Check out this code sample I'm attempting to display a list with multiple elements and incorporate a counter on the main element that updates every time one of the buttons is clicked. I'm uncertain if this approach is optimal, as I am transition ...

Whenever I try to retrieve data from MongoDB using Node.js, I consistently encounter a timeout error

Currently, I am in the process of developing a website using React.js for the front end, Node.js for the back end, and MongoDB as the database. Dummy data has been inserted into the database which can be viewed . The database has been created in Atlas, the ...

Executing PHP Functions with Script Tags

I am trying to use PHP to output the <script></script> tag. Here is the code I am using: <?php echo "test"; echo "<br>"; echo '<script src="http://mywwebiste./mycode.js" type="text/javascript& ...

Inserting information into an array: Access the final index consistently with TypeScript

I am interested in dynamically creating a table by allocating it, with the variable nbrBoules: boules:Boule :[] boule:Boule; Boule{id,poids} Method(){ for (var _i = 0; _i < this.nbrBoules; _i++) { this.boule.id = _i; alert(_i); this ...

Enhancing user experience with AngularJS: Harnessing ng-Click for seamless task management on display pages

I'm struggling with my TodoList in AngularJS. I need help creating the ngClick function for the "addTodo" button. My goal is to send the entire form data to another page where I can display the tasks. Can someone guide me on what needs to be added to ...

What is the best way to send a JavaScript array to a Perl script using AJAX?

Is it possible to convert a JavaScript array passed via AJAX into a Perl array? Accessing in Perl: @searchType = $cgi->param('searchType'); print @searchType[0]; Result: employee,admin,users,accounts It appears that the first value in the ...

The Ocelot API Gateway is a powerful tool for managing

I encountered an issue while working on my API gateway project. I initially installed the latest version of Ocelot (16.0.1), but it did not function correctly. The problem was resolved by reverting back to Ocelot version 15.0.6, while keeping my .NET Core ...

Switch the Ionic Slide Box to the "does-continue" setting

Currently in the process of constructing a slide-box containing numerous items. I've borrowed the code from the example provided here for an infinite number of items, and it's performing well. However, I have a considerable yet finite amount of ...

Preventing a webpage's CSS from affecting an iframe loading an HTML document

Whenever I load an iframe, it seems to change a bit. Do I need to apply some kind of reset or adjustment? How can I ensure that the content within the iframe looks consistent no matter what page it's loaded into? ...

$injector encountered a problem resolving the required dependency

Currently, I am attempting to adopt the LIFT protocol (Locate, Identify, Flat, Try(Dry)) for organizing my Angular projects. However, I am encountering difficulties in handling dependencies from other files. Here is the factory I have: (function () { ...

What is the best way to implement a sub-menu using jQuery?

After successfully implementing a hover effect on my menu item using CSS, I am now struggling to make the sub-menu appear below the menu item upon hovering. Despite my efforts to search for jQuery solutions online, I have not been successful. Are there a ...

What is the best way to hide the next/previous tabs once the jQuery dataTable has been set up using jSON data

Setting up a jQuery table using JSON data. Despite knowing that there will only be one row, the next/previous tabs are still displayed after the table. Is there a way to remove them? Here is the code for the table: table = $("#retrievedTable").dataTabl ...

Using Javascript Timers in an ASP.NET AJAX application with the pageLoad() function

function initiatePageLoad() { clearTimeout("MessagesTimer"); clearTimeout("NotificationsTimer"); var MessagesTimer = setTimeout("CheckMessages()", 15000); var NotificationsTimer = setTimeout("CheckNotifications()", 15000); } I've be ...

The essence of responsiveness within personalized widgets

To create a vertical slider input, I had to find an alternative option since the built-in sliderInput function does not support it. After exploring this thread, I learned that there are two possible solutions: (I) rotating the sliderInput widget using CSS ...

How to Handle Tab Navigation on a Mobile Website without Javascript?

My website primarily targets mobile devices, with tabs in the top navigation. Currently, these tabs are hard coded instead of utilizing Javascript. We want to ensure our site is accessible on all mobile devices, including those without Javascript support. ...

Increasing and decreasing the display of content using JQuery based on height rather than character count

I'm attempting to create a show more/show less link with a set height of 200px that, when clicked, will reveal the rest of the content. Anything exceeding 200px will be hidden, and there will be a "show more" link to display the remaining text. I&apos ...