MathML/Geometrie mit MathML, SVG und JavaScript
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.
Inhaltsverzeichnis
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.
<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.
<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.
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!
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:
<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.
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.
<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">α</text>
<text class="angle" data-reference="C B A">β</text>
<text class="angle" data-reference="A C B">γ</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.
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.
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
Kapitel 3
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
- MathML/Element - Kurzreferenz für den schnellen Überblick
Weblinks
- ↑ <https://stackoverflow.com/questions/58881441/text-inside-svg-foreignobject-is-not-visible>
- ↑ bl.ocks.org: Drag from HTML, Drop to SVG von Mike Bostok, Drag und Drop mit d3.js
- ↑ Draggable SVG elements (Peter Collingrigde)
sehr ausführliche Erklärung, wie man mit JavaScript ein Element auswählt, zieht und loslässt - ↑ svg-whiz: Drag and Drop (svg-whiz.com)
Beispiel ohne Erklärung