1

<svg width="450" height="250">
  <defs>
    <pattern id="image" patternUnits="userSpaceOnUse" height="100" width="100">
      <image x="0" y="0" width="100" xlink:href="https://storage.googleapis.com/cbb-image-files/PlayerHeadshots/103536-934915.png"></image>
    </pattern>
  </defs>
  <circle id='top' cx="50" cy="50" r="49" stroke="black" fill="url(#image)"/>
  
  <defs>
    <pattern id="image2" patternUnits="userSpaceOnUse" height="100" width="100">
      <image x="0" y="0" width="100" xlink:href="https://storage.googleapis.com/cbb-image-files/PlayerHeadshots/104201-933650.png"></image>
    </pattern>
  </defs>
  <circle id='top2' cx="175" cy="50" r="49" stroke="black" fill="url(#image2)"/>
  
    <defs>
    <pattern id="image3" patternUnits="userSpaceOnUse" height="100" width="100">
      <image x="0" y="0" width="100" xlink:href="https://storage.googleapis.com/cbb-image-files/PlayerHeadshots/104041-1917730.png"></image>
    </pattern>
  </defs>
  <circle id='top3' cx="300" cy="50" r="49" stroke="black" fill="url(#image3)"/>
</svg>

We are very close to the desired output here, which is player images fitted snugly as the background images of SVG circles. We are running into 2 last issues:

  • seemingly cannot change the cx values. For the 2nd and 3rd circles here, if we update the cx values to 150 (rather than 175) and 250 (rather than 300), then the images are no longer cutoff. But, we want this space between the images (in general we want to be able to position all circles as desired using cx and cy. Changing the x, y values on the image elements is not helping. How can we avoid this cutoff while keeping the 175, 300 values for cx?

  • we've intentionally removed the height element from the images, setting width equal to the circle diameter to get this clean result. However, in the 3rd image, the images width is less than its height, leading to an issue of white-space at the bottom. It seems in this case we should set height to 100 and leave the width out, but we do not know the image dimensions beforehand. How can we cleanly have all images fit in circles despite not knowing if height > width or vice versa?

Canovice
  • 9,012
  • 22
  • 93
  • 211

3 Answers3

2

Change your patternUnits to objectBoundingBox and use preserveAspectRatio to position your images correctly. preserveAspectRatio's slice option will crop out any whitespace around your shorter dimension if your source images are different aspect ratios. However, you might find that you need to adjust the relative position (yMin or yMid or yMax) if the heads are not in the same relative position.

<svg width="450" height="250">
  <defs>
    <pattern id="image" patternUnits="userSpaceOnUse" height="100" width="100">
      <image x="0" y="0" width="100" xlink:href="https://storage.googleapis.com/cbb-image-files/PlayerHeadshots/103536-934915.png" preserveAspectRatio="xMidYMax slice"></image>
    </pattern>
  </defs>
  <circle id='top' cx="50" cy="50" r="49" stroke="black" fill="url(#image)"/>
  
  <defs>
    <pattern id="image2" patternUnits="objectBoundingBox" height="100%" width="100%">
      <image x="0" y="0" width="100" height="100" xlink:href="https://storage.googleapis.com/cbb-image-files/PlayerHeadshots/104201-933650.png" preserveAspectRatio="xMaxYMin slice"></image>
    </pattern>
  </defs>
  <circle id='top2' cx="175" cy="50" r="49" stroke="black" fill="url(#image2)"/>
  
    <defs>
    <pattern id="image3" patternUnits="objectBoundingBox" height="100%" width="100%">
      <image x="0" y="0" width="100" height="100" xlink:href="https://storage.googleapis.com/cbb-image-files/PlayerHeadshots/104041-1917730.png" preserveAspectRatio="xMidYMin slice"></image>
    </pattern>
  </defs>
  <circle id='top3' cx="300" cy="50" r="49" stroke="black" fill="url(#image3)"/>
</svg>
Michael Mullany
  • 30,283
  • 6
  • 81
  • 105
  • This is okay but we have thousands of these images, and our web app dynamically fetches them. We'd need to be able to programatically assess what the correct `patternUnits` and `preserveAspectRatio` is... – Canovice Mar 08 '23 at 18:00
  • For the example, these values can be hardcoded `patternUnits="userSpaceOnUse"` or `patternUnits="objectBoundingBox"` and `preserveAspectRatio="xMidYMax slice"` or `preserveAspectRatio="xMaxYMin slice"`. However, we cannot hard code these in our web app. – Canovice Mar 08 '23 at 22:07
  • 1
    I would see if patternUnits="objectBoundingBox" and xMidYMin slice works on your image base - my guess is they will? – Michael Mullany Mar 09 '23 at 09:45
  • If we default to these 2 values for all images, the 2nd and 3rd images look good but the 1st image becomes entirely blurry – Canovice Mar 09 '23 at 11:52
  • 1
    You have to change the height and width to 100% (if you leave it at 100 - it thinks you mean 10000% - which overallocates memory and downscales resolution) – Michael Mullany Mar 09 '23 at 12:35
  • 1
    `width` and `height` to 100 on the `image`, and `width` and `height` to '100%' on the `pattern`, with `objectBoundingBox` and `xMidYMin` looks good for all images! – Canovice Mar 09 '23 at 13:09
1

If your image elements have width/height=100 then they will look centered when they are within a circle with cx and cy both equal to 50.

Given that, you just need to translate them to wherever you want them to be.

You can use a viewBox to fix the third image but you'll need javascript if you want to fit unknown content i.e. get the image dimensions then create an appropriate viewBox if you need to.

As an aside, specifying x="0" and y="0" on SVG image elements is redundant as that is the default value. I've removed those attributes.

<svg width="450" height="250">
  <defs>
    <pattern id="image" patternUnits="userSpaceOnUse" height="100" width="100">
      <image width="100" xlink:href="https://storage.googleapis.com/cbb-image-files/PlayerHeadshots/103536-934915.png"></image>
    </pattern>
  </defs>
  <circle id='top' cx="50" cy="50" r="49" stroke="black" fill="url(#image)"/>
  
  <defs>
    <pattern id="image2" patternUnits="userSpaceOnUse" height="100" width="100">
      <image width="100" xlink:href="https://storage.googleapis.com/cbb-image-files/PlayerHeadshots/104201-933650.png"></image>
    </pattern>
  </defs>
  <circle id='top2' transform="translate(125, 0)" cx="50" cy="50" r="49" stroke="black" fill="url(#image2)"/>
  
    <defs>
    <pattern id="image3" patternUnits="userSpaceOnUse" height="100" width="100" viewBox="5 0 90 90">
      <image width="100" xlink:href="https://storage.googleapis.com/cbb-image-files/PlayerHeadshots/104041-1917730.png"></image>
    </pattern>
  </defs>
  <circle id='top3'  transform="translate(250, 0)" cx="50" cy="50" r="49" stroke="black" fill="url(#image3)"/>
</svg>
Robert Longson
  • 118,664
  • 26
  • 252
  • 242
  • `Looks like your image patterns are designed to fit inside a circle with cx and cy both equal to 50.` seems like this is because the images are set to x=0, y=0 and width=100? Transform(translate) seems to resolve this per your solution, but is there another, perhaps better way to do this that involves removing the x=0, y=0 from the `image` elements? – Canovice Mar 08 '23 at 14:00
  • Also, any recommendations on how to get the image dimensions using JS? Much appreciated on the answer in general, even without the image dimensions the translate is very helpful! – Canovice Mar 08 '23 at 14:03
  • 1
    https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/naturalWidth – Robert Longson Mar 08 '23 at 14:11
1

And to not fall into the SVG-wants-unique-id-values trap, you can create a Web Component:

customElements.define("svg-avatar", class extends HTMLElement {
  connectedCallback() {
    this.style.display = "inline-block";
    let id = Math.floor(Math.random() * 1e6);
    let file = this.getAttribute("file");
    let href = `https://storage.googleapis.com/cbb-image-files/PlayerHeadshots/${file}.png`;
    let ratio = this.getAttribute("ratio") || "xMidYMin";
    this.innerHTML = `<svg viewBox="0 0 100 100">
      <defs>
        <pattern id="im${id}" patternUnits="objectBoundingBox" height="100%" width="100%">
          <image width="100" height="100" href="${href}" preserveAspectRatio="${ratio} slice"/>
        </pattern>
      </defs>
      <circle cx="50" cy="50" r="49" stroke="black" fill="url(#im${id})"/>`;
  }
});
svg-avatar {
  width: 180px;
  background: rebeccapurple;
}
<svg-avatar file="103536-934915"></svg-avatar>
<svg-avatar file="104201-933650"></svg-avatar>
<svg-avatar file="104041-1917730"></svg-avatar>
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
  • this seems like a really good solution. grabs the image, gets the image's ratio, and uses the ratio to render the images correctly. I am working on a way to implement this in react in a component that handles the data fetching and returning of the `svg` – Canovice Mar 08 '23 at 22:06
  • You can do that in [7 lines of native JavaScript](https://dev.to/dannyengelman/load-file-web-component-add-external-content-to-the-dom-1nd) if you don't want to load a 43KB React dependency – Danny '365CSI' Engelman Mar 09 '23 at 08:21
  • This seems like it wouldn't play well with our react app because of the way it directly interacts with the HTML / with the DOM? – Canovice Mar 09 '23 at 11:54
  • Yes, React sucks in supporting **native** Web Components. All other Frameworks are 100% compatible. For React you basically have to write a wrapper around all DOM, Event functionality. – Danny '365CSI' Engelman Mar 09 '23 at 12:02
  • I will try to map your solution into something that uses react hooks, useEffect and useState, to fetch the image and save the image ratio into a variable. – Canovice Mar 09 '23 at 12:05