How can I dynamically update SVG viewBox values from Polygon

3.4k views Asked by At

I have created SVG maps of a comic book page panels.

<svg id="svg1413"  class="svg-pg" width="100%" height="100%" version="1.1" viewBox="0 0 178 254" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="transition: .6s ease-out;">
    <image id="image1966" width="178" height="254" clip-path="url('#SvgjsClipPath1413')" xlink:href="https://i.imgur.com/yiZFUK4.jpg" />
    <g id="SvgjsG1413" clip-path="url('#SvgjsClipPath1413')" class="click-panels">
        <polygon fill="transparent" points=" 12,13 88,13 88,49 12,49"></polygon>
        <polygon fill="transparent" points=" 81,44 166,44 166,74 81,74"></polygon>
        <polygon fill="transparent" points=" 12,13 166,13 166,75 12,75"></polygon>
        <polygon fill="transparent" points=" 101,80 166,80 166,118 101,118"></polygon>
        <polygon fill="transparent" points=" 12,80 166,80 166,147 12,147"></polygon>
        <polygon fill="transparent" points=" 12,152 91,152 91,200 12,200"></polygon>
        <polygon fill="transparent" points=" 73,171 155,171 155,209 73,209"></polygon>
        <polygon fill="transparent" points=" 12,198 79,198 79,235 12,235"></polygon>
        <polygon fill="transparent" points=" 12,152 166,152 166,235 12,235"></polygon>
    </g>
    <defs id="SvgjsDefs1413">
        <clipPath id="SvgjsClipPath1413">
            <rect id="SvgjsRect1413" width="100%" height="100%" x="0" y="0">
            </rect>
        </clipPath>
    </defs>     
</svg>
<svg>
    ...svg content
</svg>
<svg>
    ...svg content
</svg>

What I want to achieve is:

  • Loop through all the <svg> tags
  • while on an <svg>, loop through the <polygon> tags contained within the <svg>
  • Each <polygon> contains 4 pairs of coordinates (eg. points=" x1,y1 x2,y2 x3,y3 x4,y4")
  • I want the parent <svg> viewBox values and the <rect> X and Y values to be updated with values from the current <polygon> points in the loop.

I have figured out how to calculate the values, but I don't know how to achieve this in Javascript.

My pseudo-code for calculating <svg> viewBox values

if <svg viewBox=" a b c d ">
a = x1, b = y1, c = x2 - x1, d = y3 - y2

Updating <rect> Values

if <rect x="e" y="f">
e = x1, f = y1

UPDATE: How do I control the transitions with click/swipe events rather than making it automatic?

<div id="controls" class="ctl-btn" style="width: 100%; position: absolute; bottom: 0; margin: 0 -8px; background-color: rgba(6,6,6,0.40);">
    <div style="max-width: 800px; text-align: center; margin: 0 auto;">
        <button class="pg-ctl-bk" style="margin: 8px; padding: 8px 10px;">  Back </button>
        <button class="pg-ctl-nxt" style="margin: 8px; padding: 8px 10px;"> Next  </button>
    </div>
</div>
2

There are 2 answers

3
Kaiido On

I am not sure why you used <polygon> elements here, when all you needed was a javascript Array/JSON, or ultimately <rect> elements which at least have x, y, width and height attribute as requested by viewBox attribute, but well... let's deal with <polygon>s then...

To convert your <polygon>'s point attribute value to a viewBox attribute, the simplest is to call polygonElement.getBBox() which will return an SVGRect with the needed values.

To have the viewBox property animated, the easiest is probably to use SMIL animations, and a polyfill for MS browsers.

You simply need to define an <animate> element targeting the viewBox attribute, and to update its to attribute to the target value, after you updated its from attribute to the current value.

// animate : <animate attributeName="viewBox" ...>
// rect : {SVGRect} result of polygonElement.getBBox();
function animateViewBox(animate, rect) {
  animate.setAttribute('from', animate.getAttribute('to'));
  animate.setAttribute('to', `${rect.x} ${rect.y} ${rect.width} ${rect.height}`);
  animate.beginElement(); // (re)start the animation
}

Once we have this, we only need to set up a function that will iterate over all the <polygon> elements in the svg.

function animateViewBox(animate, rect) {
  animate.setAttribute('from', animate.getAttribute('to'));
  animate.setAttribute('to', `${rect.x} ${rect.y} ${rect.width} ${rect.height}`);
  animate.beginElement(); // (re)start the animation
}

// container
const svg = document.getElementById('svg1413');
// all the <polygons> coordinates (would be better as JSON...)
const polygons = svg.querySelectorAll('polygon');
// <animate> element
const animator = svg.querySelector('.viewBoxAnimator');

// our iterator, we could call it on click
let i = 0;

function iterate() {
  if (i < polygons.length) {
    animateViewBox(animator, polygons[i++].getBBox());
    return true;
  }
}

// but we'll automate it
(async() => {
  while (iterate()) {
    await wait(1500);
  }
})();


function wait(time) {
  return new Promise(res => setTimeout(res, time));
}
svg {
  width: 100%;
  height: 100%;
  max-width: 100vw;
  max-height: 100vh;
  transition: all .6s;
}

html {
  background: black;
}
<!-- SMIL for IE -->
<script src="https://cdn.jsdelivr.net/gh/Kaiido/FakeSmile@1e50d675df616a8e784e0e6e931b3f0d595367d4/smil.user.js"></script>

<svg id="svg1413" class="svg-pg" width="154" height="83" version="1.1" viewBox="0 0 178 254" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <animate class="viewBoxAnimator" attributeType="XML" attributeName="viewBox" from="0 0 178 254" to="0 0 178 254" dur="0.6s" fill="freeze"/>
  <image id="image1966" width="178" height="254" xlink:href="https://i.imgur.com/yiZFUK4.jpg" />
  <g id="SvgjsG1413" class="click-panels">
    <polygon fill="transparent" points=" 12,13 88,13 88,49 12,49"></polygon>
    <polygon fill="transparent" points=" 81,44 166,44 166,74 81,74"></polygon>
    <polygon fill="transparent" points=" 12,13 166,13 166,75 12,75"></polygon>
    <polygon fill="transparent" points=" 101,80 166,80 166,118 101,118"></polygon>
    <polygon fill="transparent" points=" 12,80 166,80 166,147 12,147"></polygon>
    <polygon fill="transparent" points=" 12,152 91,152 91,200 12,200"></polygon>
    <polygon fill="transparent" points=" 73,171 155,171 155,209 73,209"></polygon>
    <polygon fill="transparent" points=" 12,198 79,198 79,235 12,235"></polygon>
    <polygon fill="transparent" points=" 12,152 166,152 166,235 12,235"></polygon>
  </g>
</svg>

2
Fuzzyma On

You used the svg.js tag, so you get an svg.js answer:

// Reference to svg
const canvas = SVG('#svg1413')

// List of all polygons
const polygons = canvas.find('#SvgjsG1413 polygon')

// List of all bboxes
const boxes = polygons.bbox()

const nextImage = function (index) {
  // Animate viewbox over 1s to new box
  canvas.animate(1000).viewbox(boxes[index])

  // Next image in 2s
  setTimeout(() => nextImage(++index), 2000)
}

nextImage(0)
svg {
  width: 100%;
  height: 100%;
  max-width: 100vw;
  max-height: 100vh;
}

html {
  background: black;
}
<script src="https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js@latest/dist/svg.min.js"></script>


<svg id="svg1413"  class="svg-pg" width="154" height="254" version="1.1" viewBox="0 0 178 254" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="transition: .6s ease-out;">
    <image id="image1966" width="178" height="254" clip-path="url('#SvgjsClipPath1413')" xlink:href="https://i.imgur.com/yiZFUK4.jpg" />
    <g id="SvgjsG1413" clip-path="url('#SvgjsClipPath1413')" class="click-panels">
        <polygon fill="transparent" points=" 12,13 88,13 88,49 12,49"></polygon>
        <polygon fill="transparent" points=" 81,44 166,44 166,74 81,74"></polygon>
        <polygon fill="transparent" points=" 12,13 166,13 166,75 12,75"></polygon>
        <polygon fill="transparent" points=" 101,80 166,80 166,118 101,118"></polygon>
        <polygon fill="transparent" points=" 12,80 166,80 166,147 12,147"></polygon>
        <polygon fill="transparent" points=" 12,152 91,152 91,200 12,200"></polygon>
        <polygon fill="transparent" points=" 73,171 155,171 155,209 73,209"></polygon>
        <polygon fill="transparent" points=" 12,198 79,198 79,235 12,235"></polygon>
        <polygon fill="transparent" points=" 12,152 166,152 166,235 12,235"></polygon>
    </g>  
</svg>