Adam
Adam

Reputation: 29119

How to continue gradient from svg with css?

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>

Upvotes: 0

Views: 408

Answers (1)

ccprog
ccprog

Reputation: 21921

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(', ')})`;

Upvotes: 1

Related Questions