0

I have this svg:

enter image description here

I would like to have a HTML page where the top part is the SVG and the bottom part is a div of dynamic height that continues the gradient

Like this:

<svg>...</svg>
<div id="gradient-box" >Gradient box of dynamic height</div>

enter image description here

I know that one can create gradients with CSS with color, opacity and degree https://cssgradient.io/ - but how can I evaluate the exact properties that I need for the box "#gradient-wrapper"? Is it possible to calculate this somehow?

SVG:

<svg width="1600" viewBox="0 0 1600 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M452.5 85.0981C217.5 29.5982 148.5 -12.9019 0 3.59814V765.598H1600V69.5981C1011 214.098 1118 -101.902 452.5 85.0981Z" fill="url(#paint0_linear_28_1413)"/>
<path d="M1600 165.598C848 -1.90192 667 457.098 0 178.598V1261.6H1H1600V165.598Z" fill="url(#paint1_linear_28_1413)"/>
<defs>
<linearGradient id="paint0_linear_28_1413" x1="832.5" y1="17.5" x2="832.5" y2="684" gradientUnits="userSpaceOnUse">
<stop stop-color="#F7DCDC"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_28_1413" x1="800" y1="127.719" x2="844.5" y2="1230.6" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFDCDC"/>
<stop offset="0.174971" stop-color="#FFBDBD" stop-opacity="0.416667"/>
<stop offset="0.775916" stop-color="#FFA0A0" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>
Adam
  • 25,960
  • 22
  • 158
  • 247

1 Answers1

1

Your question contains the word "dynamic". I am taking this to mean you aim to compute the values needed in the DOM, when rendering the page. Some parts of the computation might be doable in advance from values provided by the static source code. That might simplify the process, but for this answer I will assume everything is dynamic.

I hope I get this right, as there are parts I have never tried out myself. Maybe there is a library out there that implements this. If not, and you write one, do me the favor of mentioning me.

The first part is finding out the coordinates of the start and end points in the coordinate system of the #gradient-wrapper box.

  1. Describe the start and end points as DOMPoint in their own coordinate system. The coordinate values are expressed in <length> values. They can be dimensionless numbers, percentages, pixels, em or inch units. In short, your first task is converting them to numbers.

    const gradient = document.querySelector('#paint1_linear_28_1413');
    const [x1, x2, y1, y2] = ['x1', 'x2', 'y1', 'y2'].map(attr => {
      const val = gradient[attr].baseVal;
      val.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_NUMBER);
      return val.value;
    });
    let p1 = new DOMPoint(x1, y1);
    let p2 = new DOMPoint(x2, y2);
    
  2. The gradient may have an attribute gradientTransform as an additional transformation. Apply it to the points.

    const gradientTransform = new DOMMatrix(gradient.getAttribute('gradientTransform'));
    p1 = p1.matrixTransform(gradientTransform);
    p2 = p2.matrixTransform(gradientTransform);
    
  3. If the gradient has an attribute gradientUnits="objectBoundingBox" or if it is missing, compute the coordinates in user space of the element it is applied to.

    const path = document.querySelector('path[fill="url(#paint1_linear_28_1413)"]');
    const {x, y, width, height} = path.getBBox();
    p1 = new DOMPoint(x + p1.x * width, y + p1.y * height);
    p2 = new DOMPoint(x + p2.x * width, y + p2.y * height);
    
  4. Transform the coordinates to screen coordinates. The transformations applied to the user space of an element might become pretty complex. Fortunately, you can cut through all that with the SVGGraphicsElement.getScreenCTM() method.

    const ctm = path.getScreenCTM();
    p1 = p1.matrixTransform(ctm);
    p2 = p2.matrixTransform(ctm);
    

    If you want to compute this statically, you need to figure out the implicit transformation that the viewBox and preserveAspectRatio attributes on the <svg> element supply. The spec describes the algorithm for this equivalent transform in detail.

  5. Finally, you need to express the coordinates in relation to the target #gradient-wrapper box.

    const box = document.querySelector('#gradient-box').getBoundingClientRect();
    p1 = new DOMPoint(p1.x - box.x, p1.y - box.y);
    p2 = new DOMPoint(p2.x - box.x, p2.y - box.y);
    

The second part is expressing the start and end points in terms used by CSS gradients. This needs a diagram:

enter image description here

  1. Compute the slope and the angle of the gradient. Note that in the coordinate system 0deg points to the right, while 0deg in CSS points upwards. Both angle values run clockwise.

    const slope = (p2.y - p1.y) / (p2.x - p1.x)
    const angle = (Math.atan2(p2.y - p1.y, p2.x - p1.x) / Math.PI * 180 + 270) % 360;
    
  2. Compute the positions of 0% and 100% as defined in CSS as coordinates. As seen in the diagram, these points are selected in such a way that the opposing corners get a color value that exactly matches those at these two values. The line connecting the two points goes through the center of the box. I'll leave out the trigonometry involved and just give the results for the points q1 and q2.

    if (angle == 0) {
      q1.x = width / 2
      q1.y = height
      q2.x = width / 2
      q2.y = 0
    } else if (angle == 180) {
      q1.x = width / 2
      q1.y = 0
      q2.x = width / 2
      q2.y = height
    } else {
      q1.x = (width * slope - height) / (slope + 1/slope) / 2
      q1.y = -(width - height / slope) / (slope + 1/slope) / 2
      q2.x = width - (width * slope - height) / (slope + 1/slope) / 2
      q2.y = height + (width - height / slope) / (slope + 1/slope) / 2
    }
    
  3. Compute the two points pp1 and pp2 on the line that are nearest to p1 and p2. This is a bit more of math, instead of spelling it out, refer to other questions like this one.

    The percentage values belonging to pp1 and pp2 can be found as a proportion of the distance between the two CSS end points.

    const f1 = (pp1.x - q1.x) / (q2.x - q1.x) || (pp1.y - q1.y) / (q2.y - q1.y);
    const f2 = (pp2.x - q1.x) / (q2.x - q1.x) || (pp2.y - q1.y) / (q2.y - q1.y);
    
  4. Compute the relative position of each color stop between f1 and f2.

    const stops = [...gradient.querySelectorAll('stop')].map(stop => {
      const color = stop.getAttribute('stop-color');
      const opacity = stop.getAttribute('stop-opacity');
      const offset = parseFloat(stop.getAttribute('offset'));
      const fraction = offset * (f2 - f1) + f1
      return {color, opacity, fraction};
    });
    

Now, you can finally express the CSS function. The last hurdle to overcome is to convert the RGB color string plus the opacity value to rgba() notation. You can find apropriate libraries for that. I am going to assume color-string:

const stopStrs = stops.map(({color, opacity, fraction}) => {
  const rgbValue = colorString.get(color);
  const opacityValue = parseFloat(opacity);
  const rgba = colorString.to.rgb(rgbValue, opacityValue);
  return `${fraction * 100}% ${rgba}`
};
const gradientFunction = `linearGradient(${angle}deg, ${stopStrs.join(', ')})`;
ccprog
  • 20,308
  • 4
  • 27
  • 44
  • Wow what an answer. Thank you so much for it. I am not planning to write a library. I am actually in the situation, that I have exactly the provided image and two things are dynamic: 1) The width of the SVG is 100%, so it may vary on browser width 2) the height of "gradient-box", so basically q2 will vary. I have not understood the entire answer yet, but my hope is that I can do the calculation manually and in the end its just a specific hardcoded linear-gradient css with some angle and stops . I let you know once I got the chance to figure this all out. – Adam Oct 30 '22 at 20:33
  • Then it is as I suspected. If the svg width varies you'll have to at least compute the transformation in step 4 dynamically. Whether that may be easier done via the `.getSreeenCTM()` method, or via computing the equivalent transform of the `` (which probably reduces to a simple uniform `scale(element width / viewBox width)`) may be a matter of taste. – ccprog Oct 30 '22 at 20:51
  • Okay, so I tried to follow the instructions. In my case, I don't need to convert to convertToSpecifiedUnits in step 1), and also step 2 & 3 can be skipped. Step 4 works and points coords change depending on browser width. – Adam Oct 31 '22 at 03:10
  • In step 5, I actually don't understand what we the goal is here. As the boxes have same x-values (in my example as well as in your diagram) the new points p1 & p2 are essentially just shifted down and lie on a parallel line. I guess you want that to compute new points p1 & p2 that are still on the line but inside the green box, right? If thats the goal, you would need to shift the x coordinates not by (box2.x - box1.x) but by ((p2.x - p1.x)((p2.y - p1.y))* (box2.y - box1.y) – Adam Oct 31 '22 at 03:30
  • So I had to read the docs again for css linear-gradient, and now I understand why you want to compute q1/q2 https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#composition_of_a_linear_gradient But I think there is a logic flaw here. This only works, if the line spanned by p1/p2 from svg actually intersects with the center of the gradient-box div. This is unlikly to happen, especially as the gradient-div's heigt is not fixed. And in this case, the gradients of svg and div won't match. You would clearly be able to see the border line. – Adam Oct 31 '22 at 03:38
  • You are right. I've updated the answer accordingly: q1 and q2 can be computed from the angle/slope alone, and you need points pp1 and pp1 as orthogonal projections of the original start and end onto the line defining the CSS gradient. I hope that one is better. – ccprog Oct 31 '22 at 16:45
  • Regarding step 5, I now express all points p1, p2, q1 and q2 in a coordinate system originating at top left of the green box. That makes things a lot easier. – ccprog Oct 31 '22 at 17:12