MathML/Geometrie mit MathML, SVG und JavaScript

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

MathML ist wie SVG ein XML-Dialekt, der problemlos in HTML eingebunden werden kann. Allerdings können Sie auch SVG durch andere XML-Formate, wie XHTML und MathML, erweitern.

Dieses Tutorial zeigt, wie Sie Webseiten mit MathML und SVG zugänglich gestalten. Solche Formeln und Grafiken sind beliebig skalierbar und immer gestochen scharf, können aber auch durch Screenreader vorgelesen werden.

foreignObject

Leider ist es nicht möglich HTML-Elemente direkt in SVG einzubinden. Mit HTML5 wurde es zwar möglich, dass HTML-Browser auch SVG parsen – bei HTML innerhalb von SVG ist dies aber (noch) nicht der Fall.

So wäre es am einfachsten:
<rect x="10" y="10" width="200" height="150" fill="#5a9905">
  <h2>Überschrift</h2>
  <p>Dies ist ein Textabsatz mit einem <strong>fetten</strong> Wort und automatischem Zeilenumbruch.</p>
</rect>

HTML in SVG

Mit dem foreignObject-Element können Sie SVG durch andere XML-Formate, wie XHTML und MathML, erweitern.

Einbindung von HTML in SVG ansehen …
<svg>
  <rect x="10" y="10" width="200" height="150" fill="skyblue"/>
  <foreignObject x="20" y="10" width="180" height="150">
    <body xmlns="http://www.w3.org/1999/xhtml">
      <h2>mehrzeiliger Text</h2>
      <p>Dies ist ein Textabsatz mit einem <strong>fetten</strong> Wort und automatischem Zeilenumbruch.</p>
    </body>
  </foreignobject>
 
    <circle cx="300" cy="200" r="100" fill="gold"/>
    <foreignobject x="230" y="120" width="180" height="180">
      <h2>Liste</h2>
      <ol>
        <li>Listenelement</li>
        <li>Listenelement</li>
        <li>Listenelement</li>
      </ol>
    </foreignobject>
  </svg>

Innerhalb des foreignObject-Elements befindet sich ein HTML-body-Element mit weiterem HTML.

Beachten Sie, dass die Höhe des hellblauen SVG-rect durch eine Magic Number festgelegt wurde. Wenn sich der Umfang des HTML-Texts ändert, muss die Höhe manuell angepasst werden!

In Webseiten mit HTML5-Doctype und in SVG2-Standalone-Dokumenten benötigen Sie keine Namensraumdeklaration für die SVG- und die foreignObject-Elemente mehr. Auch eine in alten Beispielen noch verwendete Angabe des requiredExtensions-Attribut ist heute nicht mehr nötig - im Gegenteil verhindert sie ein Rendern im Chrome.[1]

Achtung!

Bitte öffnen Sie diese Beispiele mit einem Klick auf "Vorschau" in einem neuen Tab. Im Frickl überschreibt der Parser den body des HTML-Dokuments mit dem body des foreignObjects, sodass das (vorhergehende) SVG und die SVG-Elemente nicht gerendert werden. --Matthias Scharwies (Diskussion) 15:27, 24. Jun. 2020 (CEST)
Empfehlung: Validieren Sie das SVG-Dokument und den einzubindenden Inhalt getrennt, da Validatoren Dokumente mit gemischten Namensräumen nicht überprüfen können. Erst wenn alle Teile valide sind, werden sie zusammengefügt.

MathML in SVG

Dies geht auch bei SVGs. Leider gibt es bei SVG im SELF-Wiki (und auch anderen Wikis) oft Probleme mit der automatischen Erzeugung der png-Vorschau.

Deshalb wurden Formeln so oft als Rastergrafiken oder bestenfalls als SVG erstellt. Dabei wurde aber Text oft nicht mit SVG-Text ausgezeichnet, sondern über die „in-Pfad-umwandeln“-Funktion von Inkscape oder Illustrator in nicht zugängliche Grafik-Objekten umgewandelt.

Mittlerweile können Sie dies viel einfacher erreichen:

Goldener Schnitt - MathML in SVG ansehen …
<foreignObject x="290" y="240" width="100" height="100" requiredExtensions="http://www.w3.org/1998/Math/MathML">
  <math xmlns="http://www.w3.org/1998/Math/MathML">
	<mfrac>
		<mn>1</mn>
		<mn>2</mn>
	</mfrac>
	<mo>+</mo>
	<mfrac>
		<msqrt><mn>5</mn></msqrt>
		<mn>2</mn>
	</mfrac>	
  </math>						
</foreignObject>

Dieses Beispiel ist eine normale Webseite mit HTML5-Doctype.

Links wird MathML in einem math-Element notiert.

Rechts wird SVG in einem svg-Element notiert. Die Infografik enthält …

  • Einzelne Zahlen und Variablen, die mit SVG-Text (schwarze Textfarbe) und …
  • Formeln, die innerhalb eines foreignObject-Elements in MathML (grau) notiert wurden.

Die farbliche Kennzeichnung dient nur zur Unterscheidung. Um beide Arten von Text möglichst gleich aussehen zu lassen, erhält das math-Element eine Schriftgröße von 1.5em. Dies sieht sowohl im Chrome als auch im Firefox annährend gleichgroß aus.

So ist die Grafik zugänglich und kann auch von Screenreadern vorgelesen werden.

Dreieck-Generator

Als erstes „richtiges“ Projekt soll ein Dreieck-Generator ermöglichen, die Punkte eines Dreiecks frei zu verschieben und dann mit JavaScript Seitenlängen und Innenwinkel zu berechnen.

Die Grafik soll mit SVG, die Formel mit MathML und die Webseite mit HTML realisiert werden.

Drag & Drop in SVG

Sowohl die HTML5 Drag & Drop-API als auch die Umsetzung von jQuery funktionieren nicht mit SVG-Elementen. Es gibt jedoch einige Frameworks wie d3.js die diese Funktionalität nachrüsten.[2]

Dieses Beispiel arbeitet mit einigen Zeilen Vanilla JS:[3][4]

Sie können die makeDraggable-Funktion beispielsweise aus einem DOMContentLoaded Handler heraus aufrufen, worin Sie das gewünschte SVG Element aus dem DOM heraussuchen und an makeDraggable übergeben.

Drag & Drop in SVG ansehen …
	function makeDraggable(svg) {
		svg.addEventListener('pointerdown', startDrag, false);
		svg.addEventListener('pointermove', drag, false);
		svg.addEventListener('pointerup', endDrag, false);
		svg.addEventListener('pointerleave', endDrag);

		function getMousePosition(evt) {
			const CTM = svg.getScreenCTM();
			return {
				x: (evt.clientX - CTM.e) / CTM.a,
				y: (evt.clientY - CTM.f) / CTM.d
			};
		}

		var selectedElement, offset, transform;

		function startDrag(evt) {
			if (evt.target.draggable = 'true') {
				selectedElement = evt.target;
				offset = getMousePosition(evt);

			// Make sure the first transform on the element is a translate transform
			const transforms = selectedElement.transform.baseVal;

			if (transforms.length === 0 || transforms.getItem(0).type !== SVGTransform.SVG_TRANSFORM_TRANSLATE) {
			// Create a transform that translates by (0, 0)
				const translate = svg.createSVGTransform();
				translate.setTranslate(0, 0);
				selectedElement.transform.baseVal.insertItemBefore(translate, 0);
			}

			// Get initial translation
			transform = transforms.getItem(0);
			offset.x -= transform.matrix.e;
			offset.y -= transform.matrix.f;
			}
		}

		function drag(evt) {
			if (selectedElement) {
				evt.preventDefault();
				const coord = getMousePosition(evt);
				transform.setTranslate(coord.x - offset.x, coord.y - offset.y);
			}
		}

		function endDrag(evt) {
			selectedElement = false;
		}

Über das boolesche Attribut draggable können Sie nun SVG-Objekte auszeichnen, die mit der Maus (oder einem anderen digitalen Zeiger) aufgenommen und an einen anderen Punkt verschoben werden können. Die Koordinaten dieses Punkts werden ermittelt und in einer transform="translate()"-Funktion eingesetzt.

Dies hat den Vorteil, dass alle SVG-Objekte gleichermaßen verschoben werden können; während man nicht in jedem Fall ein x- oder y-Attribut verwenden könnte (circle und ellipse haben cx und cy; Pfade beginnen an den in MoveTo festgelegten Koordinaten).

Ein Dreieck zeichnen

Die drei Punkte sollen nun zu einem Dreieck verbunden werden. Dafür werden zwischen den Punkten Linien gezogen und sowohl Punkte als auch Linien beschriftet.

Jeder der drei Punkte hat die Koordinaten x, y

let  = {
  A: {x: 20, y: 100},
  B: {x: 60, y: 120},
  C: {x: 80, y: 40}
};

Man könnte diese Koordinaten nun als Werte in ein line-Element einfügen - also x1, y1 x2, y2. Genauso möglich wäre es einen Pfad vom Ausgangspunkt Ax, Ay zu Bx, By und Cx, Cy und diesen dann mit z zu schließen.

Am einfachsten ist ein Polygon mit drei Punkten, das automatisch geschlossen wird. Gegenüber den drei Linien hat es den Vorteil, dass das Dreieck auch mit einer Farbe gefüllt werden kann.

Polygon entlang von Punkten zeichnen ansehen …
<g id="dreieck" class="draggablePolygon">
	<polygon />

	<circle style="fill:#c82f04;" cx="20" cy="100"><title>A</title></circle>
	<circle style="fill:#337599;" cx="60" cy="120"><title>B</title></circle>
	<circle style="fill:#dfac20;" cx="80" cy="70"><title>C</title></circle>

	<text class="angle" data-reference="B A C">&alpha;</text>
	<text class="angle" data-reference="C B A">&beta;</text>
	<text class="angle" data-reference="A C B">&gamma;</text>
</g>

Im SVG findet sich nun unser polygon-Element - allerdings noch völlig leer! Das JavaScript wird die Koordinaten der Punkte auslesen und dann ein points-Attribut erstellen.

Jeder Punkt erhält ein title-Element, das bei :hover (und dem Ziehen) sichtbar wird.

Daneben gibt es für jeden Punkt, bzw die Ecke eine Beschriftung für den Winkel. Dieses text-Element ist immer sichtbar.

Dieses Beispiel nutzt die Vorlage eines Koordinatensystems aus dem Tutorial Diagramme mit Koordinatensystem.

Winkel und Maße berechnen

Für die Berechnung der Winkel und der Seitenlängen können die in A, B und C gespeicherten Koordinaten verwendet werden.

Winkel berechnen ansehen …
document.addEventListener('DOMContentLoaded', function () {
	const degreeFormatter = Intl.NumberFormat(
					"de-DE",
    		  { style: "unit", unit: "degree",
		      	minimumFractionDigits: 2, maximumFractionDigits: 2 });
        
	let svg = document.querySelector('svg');

	const dreieck_steuerung = [ 
  	{ output: "#alpha", angleAt: 'A' },
  	{ output: "#beta", angleAt: 'B' },
  	{ output: "#gamma", angleAt: 'C' },
  ];
  
	draggablePolygon(svg, svg.querySelector("#dreieck"), zeigeWinkel);
	draggablePolygon(svg, svg.querySelector("#viereck"));

	function zeigeWinkel(points, angles) {
  	for (let steuer of dreieck_steuerung) {
    	let winkel = angles.find(angle => angle.point.name == steuer.angleAt);
      let output = document.querySelector(steuer.output);
      if (output) {
      	if (winkel)
      		output.textContent = degreeFormatter.format(winkel.value * 180 / Math.PI);
      	else
        	output.textContent = '?';
      }
    }
  }
   
 	function draggablePolygon(svg, polygon, onupdate) {
		let selectedCorner, offset;

		svg.addEventListener('pointerdown', startDrag);
		svg.addEventListener('pointermove', drag);
 		svg.addEventListener('pointerup', endDrag);
		svg.addEventListener('pointerleave', endDrag);

    aktualisiereAnsicht();

		function getMousePosition(mouseEvent) {
    	return Point2D.fromClientPos(mouseEvent)
      			 .matrixTransform(svg.getScreenCTM().inverse());
		}

		function startDrag(evt) {
    	const elem = evt.target;
      if (elem instanceof SVGCircleElement && polygon.contains(elem)) {
      	selectedCorner = elem;
				offset = getMousePosition(evt).vectorTo(Point2D.fromCircle(elem));
			}
		}

		function drag(evt) {
			if (selectedCorner) {
				evt.preventDefault();
				let newPos = offset.vectorTo(getMousePosition(evt));
        selectedCorner.setAttribute("cx", newPos.x);
        selectedCorner.setAttribute("cy", newPos.y);
				aktualisiereAnsicht();
			}
		}

		function endDrag(evt) {
			selectedCorner = null;
		}

		function aktualisiereAnsicht() {
    	const points = [];
      const angles = [];
      for (let corner of polygon.querySelectorAll("circle")) {
      	points.push(Point2D.fromCircle(corner));
			}
			polygon.querySelector('polygon').setAttribute('points', points.join(' '));
      
      // Magic Code: Winkelnamen an der richtigen Position einzeichnen. Klappt
      // für konvexe Objekte gut...
      for (let angleText of polygon.querySelectorAll("text.angle")) {
				let ref = angleText.dataset.reference;
        if (ref) {
        	// Namen der Bezugspunkte aus dem data-reference Attribut holen
          // Mathetypisch sind die Punkte im Uhrzeigersinn anzugeben und der
          // mittlere Punkt ist der Scheitelpunkt.
        	let refPoints = ref.split(/\s+/).map(rp => points.find(p => p.name == rp));
          // Es müssen 3 Punkte sein und alle drei Referenzen müssen existieren
          if (refPoints.length == 3 && !refPoints.includes(null)) {
          	// Vektor vom Scheitel zum ersten Punkt - auf Länge 1 normalisiert
          	let v1 = refPoints[1].vectorTo(refPoints[0]).normalized();
          	// Vektor vom Scheitel zum zweiten Punkt - auf Länge 1 normalisiert
          	let v2 = refPoints[1].vectorTo(refPoints[2]).normalized();
            // Summe der beiden liegt dann auf der Winkelhalbierenden. Länge auf
            // 8 normalisieren, das wird der Abstand zum Punkt.
            let offs = v1.addTo(v2).normalized(8);
            // Skalarprodukt zweier normalisierter Vektoren ist der cos des
            // eingeschlossenen Winkels!
            let sp =v1.scalarProduct(v2);
            // Daten in Winkelliste ablegen
            angles.push({
            	point: refPoints[1],
              offset: offs,
              value: Math.acos(sp)
            });
            // Textelement mit Winkeltext relativ zum Scheitel platzieren
            // Todo: Konkave Objekt erfordern eine Prüfung, auf welcher Seite des
            // Punktes "innen" ist und müssen ggf. offs subtrahieren statt addieren
            let d = refPoints[1].addTo(offs);
            angleText.setAttribute("x", d.x);
            angleText.setAttribute("y", d.y+2);
          }
          
        }
      }

			if (onupdate) onupdate(points, angles);
    }
	}
});

Das Script ist um einiges komplexer geworden als geplant - dafür aber auch mächtiger:

Mathe-Funktionen:

  • Das Quadrat einer Zahl kann man mit a**2 ermitteln. Der doppelte Stern ist viel kürzer als das klassische Math.pow(a,2).
  • Die Länge eines Vektors ermittelt man modernerweise mit Math.hypot(x,y) und nicht mit dem umständlicheren Math.sqrt(xx + yy).

eine Klasse mit vielen Funktionen

Man kann statt { x, y } Objekten die Klasse DOMPointReadOnly verwenden. Ein solcher Punkt lässt sich als Punkt oder als Ursprungsvektor auffassen.

Winkel berechnen ansehen …
class Point2D extends DOMPointReadOnly {
	static fromCircle(svgCircle) {
    const p = new Point2D(svgCircle.cx.baseVal.value, svgCircle.cy.baseVal.value);
    let title = svgCircle.querySelector("title");
    if (title)
    	p.name = title.textContent;
    return p;
  }
  static fromClientPos(mouseEvent) {
  	return new Point2D(event.clientX, event.clientY);
  }

	constructor(x,y) {
  	super(x,y, 0, 1);
  }
  get length() {
  	return Math.hypot(this.x, this.y);
  }
  toString() {
  	return `${this.x.toFixed(3)} ${this.y.toFixed(3)}`;
  }
  vectorTo(p) {
  	return new Point2D(p.x - this.x, p.y - this.y);
  }
  addTo(p) {
  	return new Point2D(p.x + this.x, p.y + this.y);
  }
  scalarProduct(p) {
    return p.x * this.x + p.y * this.y;
  }
	normalized(len = 1) {
  	let scale = len / this.length;
    return new Point2D(this.x * scale, this.y * scale);
  }
 	// matrixTransform liefert einen DOMPoint, der muss wieder zum Point2D gemacht werden
  matrixTransform(matrix) {
    const transformed = super.matrixTransform(matrix);
    return new Point2D(transformed.x, transformed.y);
  }
}

Diese Klasse kann man noch aufbohren. Ich habe Point2D davon abgeleitet und einiges an Vektorfunktionen hineingepackt:

  • length
  • toString() - liefert das Wertepaar für polygon.points
  • vectorTo(p) - liefert den Vektor von this nach p (oder die Differenz p - this, je nach Sichtweise)
  • addTo(p) - liefert die Vektorsumme von this und p
  • scalarProduct(p) - liefert Skalarprodukt von this und p, praktisch für den Kosinus des eingeschlossenen Winkels!
  • normalized(len) - Liefert normalisierten Vektor mit Länge len
  • matrixTransform(m) - erzeugt den mit der Matrix transformierten Point2D, praktisch für die Umrechnung von Maus in SVG Koordinaten
  • Dazu dann noch Factorymethodeh fromCircle und fromClientPos, um einen Point2D aus einem SVG Circle oder aus der Mausposition abzuleigen.

Kapitel 3

ToDo (weitere ToDos)

Dieser Artikel sollte weiter ausgebaut werden.

Bitte schicken Sie Ideen, gute Beispiele und best-practice-Tipps an projekt@selfhtml.org.

Noch besser wäre es, wenn Sie in unserem Wiki mitmachen würden.

--Matthias Scharwies (Diskussion) 23:03, 23. Mär. 2023 (CET)

Siehe auch


Weblinks

  1. <https://stackoverflow.com/questions/58881441/text-inside-svg-foreignobject-is-not-visible>
  2. bl.ocks.org: Drag from HTML, Drop to SVG von Mike Bostok, Drag und Drop mit d3.js
  3. Draggable SVG elements (Peter Collingrigde)
    sehr ausführliche Erklärung, wie man mit JavaScript ein Element auswählt, zieht und loslässt
  4. svg-whiz: Drag and Drop (svg-whiz.com)
    Beispiel ohne Erklärung