Webkrauts Logo

Webkrauts Webkrauts Schriftzug

- für mehr Qualität im Web

Zurück zu den Wurzeln

Mathematische Funktionen mit Sass

Zurück zu den Wurzeln

Mit Sass könnt ihr mathematische Funktionen wie Wurzelziehen umsetzen. Wir zeigen euch anhand einer Bildergalerie, wozu das nützlich ist und wie ihr euch geschickt dem Wert für die Wurzel nähert.

Auf einer responsiven Webseite möchten wir eine responsive Bildergalerie einbauen. D.h. die Größen sollen nicht in em oder rem oder gar px angegeben sein, sondern die Bilder sollen ihre Größe dem Viewport anpassen. Die Bilder können unterschiedliche Seitenverhältnisse haben und im Quer- oder Hochformat vorliegen. Wir geben ihnen quadratische Rahmen, die an die Diasammlung unserer Eltern erinnen. Als Markup verwenden wir:

  1. <ul class="gallery">
  2.   <li>
  3.     <div>
  4.       <img src="…" alt="…"/>
  5.     </div>
  6.   </li>
  7.   <li>
  8.     <div>
  9.       <img src="…" alt="…"/>
  10.     </div>
  11.   </li>
  12.   ⋮
  13. </ul>

Ihr werdet fragen: Wozu die div? Dazu gleich mehr.

Wir verwenden Flexbox dazu, mehrere Bilder nebeneinander zu platzieren. Da aber alle Bilderrahmen gleich groß sein sollen, unabhängig davon, wie viele davon in einer Zeile erscheinen (in der letzten können es ja weniger sein als in den anderen), erlauben wir den Flexitems nicht zu wachsen oder zu schrumpfen. Wir geben deren Breite als Prozentwert der Breite des Containers abzüglich ihres Außenabstandes an. Für breitere Viewports lassen wir per Media-Queries mehr als 2 Bilderrahmen in einer Zeile Platz finden. Sass-Code (SCSS):

  1. .gallery
  2. {
  3.   li
  4.   {
  5.     $margin: 1vw;
  6.     margin: $margin;
  7.     flex-basis: calc(#{percentage(1/2)} - 2 * #{$margin});
  8.  
  9.     @media (min-width: 36em)
  10.     {
  11.       flex-basis: calc(#{percentage(1/3)} - 2 * #{$margin});
  12.     }
  13.     ⋮
  14.   }
  15. }

Für die quadratischen Rahmen können wir nicht einfach deren Höhe angeben, da sich Prozentangaben auf Höhen von Containern beziehen, wir aber eine Beziehung zur Breite benötigen. Wir können aber ausnutzen, dass dies bei Padding anders ist und sich Prozentangaben auch in der Vertikalen auf die Breite des umschließenden Blocks beziehen:

  1. .gallery
  2. {
  3.   div
  4.   {
  5.     height: 0;
  6.     padding-top: 100%;
  7.     position: relative;
  8.   }
  9. }

Jetzt wird deutlich, wozu dieses zusätzliche div im Markup dient: Damit sich die 100% auf die Breite des Bilderrahmens beziehen können. (Wer eine Lösung kennt, die ebenso flexibel ist und ohne zusätzliche div auskommt, hinterlasse diese bitte in den Kommentaren.)

Mittels max-width und max-height passen wir die Bilder in die Rahmen ein. Damit die Bilder nicht direkt am Rand kleben, sollen sie maximal 90% der Breite bzw. Höhe einnehmen. Sie werden innerhalb des divs zentriert, indem sie absolut bei 50% positioniert werden (diese Prozentangabe bezieht sich auf die Größe des Rahmens; zentriert also den linken oberen Eckpunkt) und dann um −50% verschoben werden (diese Prozentangabe bezieht sich auf die Größe des Bildes; zentriert damit das Bild).

  1. .gallery
  2. {
  3.   img
  4.   {
  5.     position: absolute;
  6.     left: 50%;
  7.     top: 50%;
  8.     transform: translate(-50%, -50%);
  9.     max-width: 90%;
  10.     max-height: 90%;
  11.   }
  12. }

Das sieht dann so aus:

See the Pen LGNgmr by Gunnar Bittersmann (@gunnarbittersmann) on CodePen.

Wenn alle Bilder in der Galerie dasselbe Seitenverhältnis haben – kein Problem. Was aber, wenn das nicht der Fall ist? Wenn die Bilder auf dieselbe (Maximal-)Breite skaliert werden, ist ein quadratisches Bild größer als ein rechteckiges im Querformat; wenn die Bilder auf dieselbe (Maximal-)Höhe skaliert werden, ist ein quadratisches Bild größer als ein rechteckiges im Hochformat. (Vor einem ähnlichem Problem steht ihr, wenn ihr mehrere Logos nebeneinander platziert, bspw. die Logos der Sponsoren einer Veranstaltung.)

Eine Idee, die Größe der Bilder einander anzupassen, ist sie so zu skalieren, dass alle dieselbe Fläche einnehmen. Das möchten wir (für eine Reihe vorgegebener Seitenverhältnisse) im Stylesheet umsetzen. Die Seitenverhältnisse der einzelnen Bilder sind dabei im Markup jeweils in data-Attributen angegeben, z.B. data-aspect-ratio="3x2", was wir uns im Stylesheet mittels Attributselektoren zunutze machen werden. Wir könnten die Information auch in class-Attributen unterbringen, nutzen hier aber aus, dass es außer Klassen durchaus noch andere Möglichkeiten zum Stylen gibt.

Für jedes Seitenverhältnis sind nun die Maße der jeweiligen Bilder (in Prozent) zu bestimmen. Für die gewünschte Fläche A und das Seitenverhältnis r ergeben sich Breite b und Höhe h aus dem Gleichungssystem

b × h = A
b : h = r

Nach b und h umgestellt ergibt das:

b = √(A × r)
h = √(A : r)

Multiplizieren und Dividieren können wir in Sass, sogar in CSS mit der calc()-Funktion – aber Wurzelziehen? Wir könnten nun den Taschenrechner bemühen und die jeweiligen Werte berechnen – aber sollten wir? Wir haben doch schon ein Gerät vor uns, das rechnen kann! Die Implementierung der Wurzelberechnung hat den Vorteil, dass wir damit eine universelle Funktion zur Ermittlung die Bildmaße erhalten und wir uns auch bei zukünftig hinzukommenden Seitenverhältnissen nicht noch einmal Gedanken darüber machen müssen.

Dazu erinnern wir uns daran, wie schon die Babylonier vor Tausenden von Jahren Wurzeln berechneten: die wiederholte Berechnung von x ← ½(x + a/x) ergibt √a mit beliebiger Genauigkeit. Als Startwert wählen wir x = 1, und 4 Schritte sollen uns genügen. Die Implementierung in Sass sieht dann so aus:

  1. @function sqrt($a, $iterations: 4)
  2. {
  3.   $x: 1;
  4.    
  5.   @for $i from 1 through $iterations
  6.   {
  7.     $x: ($x + $a / $x) / 2;
  8.   }
  9.    
  10.   @return $x;
  11. }

Damit können wir die Breiten der Bilder in Abhängigkeit von den Seitenverhältnissen nach oben genannter Formel b = √(A × r) berechnen; die Höhen ergeben sich automatisch, da die Bilder so skaliert werden, dass das Seitenverhältnis erhalten bleibt. Den Wert für A können wir bspw. so wählen, dass die Bilder 50% der Fläche des quadratischen Rahmens ausfüllen. Bei einem Seitenverhältnis von 2:1 würde die Breite dann 100% und die Höhe 50% betragen. Da wir 90% als Maximalbreite und -höhe angegeben haben, wird ein 2:1-Bild aber etwas kleiner dargestellt. Bei 90% Breite ergibt sich 45% Höhe; also 90% × 45% = 40,5% ausgefüllte Fläche. Wir wählen folglich 0,4 als passenden Wert für A.

  1. @function calcWidth($aspectratio, $area: 0.4)
  2. {
  3.   @return percentage(sqrt($area * $aspectratio));
  4. }

Hinweise: Die Funktionsdefinition ist nicht unbedingt der beste Platz für den Defaultwert; den bringt ihr besser in einem Konfigurationsblock unter.

Breitere Panoramabilder bzw. schmalere Handtuchformate würden wegen der Beschränkung der Maximalbreite und -höhe weniger als den angegebenen Teil der Fläche des Rahmens ausfüllen.

Mithilfe dieser Funktion geben wir die Breiten für die benötigten Seitenverhältnisse an, bspw.:

  1. .gallery
  2. {
  3.   [data-aspect-ratio="2x1"] { width: calcWidth(2/1); }
  4.   [data-aspect-ratio="3x2"] { width: calcWidth(3/2); }
  5.   [data-aspect-ratio="4x3"] { width: calcWidth(4/3); }
  6.   [data-aspect-ratio="1x1"] { width: calcWidth(1/1); }
  7.   [data-aspect-ratio="3x4"] { width: calcWidth(3/4); }
  8.   [data-aspect-ratio="2x3"] { width: calcWidth(2/3); }
  9.   [data-aspect-ratio="1x2"] { width: calcWidth(1/2); }
  10. }

Da wir beim Schreiben von Code (im positiven Sinn) faul sind und das auch sein sollten – wir nennen das »Don’t Repeat Yourself« – automatisieren wir diesen Schritt. Wir legen uns eine Liste mit den Seitenverhältnissen an und iterieren darüber:

  1. .gallery
  2. {
  3.   $aspectratios: ((2,1), (3,2), (4,3), (1,1));
  4.  
  5.   @each $aspectratio in $aspectratios
  6.   {
  7.     $w: nth($aspectratio, 1);
  8.     $h: nth($aspectratio, 2);
  9.            
  10.     [data-aspect-ratio="#{$w}x#{$h}"]
  11.     {
  12.       width: calcWidth($w / $h);
  13.     }
  14.            
  15.     @if $w != $h
  16.     {
  17.       [data-aspect-ratio="#{$h}x#{$w}"]
  18.       {
  19.         width: calcWidth($h / $w);
  20.       }
  21.     }
  22.   }
  23. }

Und so sieht’s dann mit auf gleiche Fläche skalierten Bildern aus:

See the Pen adNQpQ by Gunnar Bittersmann (@gunnarbittersmann) on CodePen.

Neben Wurzeln könntet ihr auch trigonometrische Funktionen zum Stylen benötigen. In diesem Vortrag (19 min; Folien dazu) könnt ihr sehen, wie ihr bspw. sin, cos und arctan mit Taylorreihen in Sass implementieren könnt. (Wobei Taylorreihen nicht unbedingt die am schnellsten konvergierenden Reihen dafür sind; ihr benötigt schon etliche Iterationsschritte, wenn ihr einen genauen Wert benötigt.)

Wurzel- und trigonometrische Funktionen sind übrigens in Compass schon drin. Wer Interesse hat kann ja mal nachsehen, wie sie dort implementiert sind.

Kommentare

Tom
am 23.12.2015 - 10:49

Danke für den Ansatz. Ich habe neulich genau an dem Problem rumgekaut, wäre aber nicht auf die Idee gekommen, die Fläche heranzuziehen. :)
Wäre es nicht universeller, direkt die Breite und Höhe der Bilder zu benutzen, und diese per data-img-w data-img-h in der sass funktion zu nutzen, statt über festgelegte aspect ratios zu iterieren?
Das würde den Einsatz auch in cms-/redaktionserzeugten workflows und templates vereinfachen :-).
(Ich mache mal ein codepen und probiere das)
Danke!

Permanenter Link

Tom
am 23.12.2015 - 12:19

Ok, Denkfehler meinerseits. Das SASS kennt ja zum Zeitpunkt der Kompilierung des CSS nichts von den dann vergebenen data-img-w/h Attributen, bzw deren Werten - mööp. :-)

Permanenter Link
Gunnar Bittersmann

Gunnar Bittersmann (Autor)
am 23.12.2015 - 12:35

Man kann mit Sass zwar zaubern, aber zaubern kann man damit auch nicht. ;-) Letztendlich generiert man CSS; man kann mit Sass nicht mehr machen als mit reinem CSS, genausowenig wie man mit jQuery mehr machen kann als mit reinem JavaScript.

Mit Sass kommt man nicht an data-img-w und data-img-h ran; es sei denn, du hattest im Sinn, solches CSS zu generieren:

  1. .gallery [data-img-w="2134"][data-img-h="3999"] { width: …% }
  2. .gallery [data-img-w="2134"][data-img-h="4000"] { width: …% }
  3. .gallery [data-img-w="2135"][data-img-h="20"] { width: …% }
  4. .gallery [data-img-w="2135"][data-img-h="21"] { width: …% }

Das wäre mit Sass zwar möglich, aber eher nicht sinnvoll. Der Ansatz ist wie gesagt nur „für eine Reihe vorgegebener Seitenverhältnisse“ tauglich. Er lässt sich noch auf weitere Seitenverhältnisse erweitern (deshalb hatte ich ja ganz zum Schluss die Liste mit den Seitenverhältnissen angelegt und eine Schleife drüber laufen lassen), aber nicht auf viele.

Wenn man wirklich nicht nur Bilder vorgegebener Klassen hat (Jetzt hat er doch „Klassen“ gesagt! ;-)), sondern Bilder mit beliebig unterschiedlichen Seitenverhältnissen, müsste man für jedes Bild die Breite berechnen. Kann man serverseitig tun und im style-Attribut festhalten (Jetzt hat er doch „Inline-Style“ gesagt! ;-)) oder per unobtrusive JavaScript. Wenn man nicht gerade SSI dafür einsetzt, kann man dann auch die sqrt-Funktion seiner Programmiersprache verwenden. ;-)

Permanenter Link

wortwart
am 23.12.2015 - 11:58

Die quadratische Form kriegst du auch ohne div mit einem li::before hin (display: block und content: "", klar). Dann noch ein position: relative auf die li, damit das position: absolute in den imgs sich darauf beziehen kann.

Permanenter Link
Gunnar Bittersmann

Gunnar Bittersmann (Autor)
am 23.12.2015 - 14:58

Sehr schön, danke. Hab ich gleich mal so umgesetzt.

Permanenter Link

Webdesign Basel
am 17.01.2016 - 10:30

Vielen Dank für diesen tollen Artikel, habe so diverse Probleme bei einer Gallerie meines Kunden beheben können.

Grüsse aus der Schweiz

Permanenter Link

Gunnar Bittersmann
am 21.01.2016 - 02:00

Schön, dass ich mich mal als Artikel-Schreiber nützlich machen kann, nicht nur als Kommentar-Schreiber … (Wie hieß doch gleich das Wort? Ⓖ) ;-)

Permanenter Link

Die Kommentare sind geschlossen.