Skip to main content

Zeichnungstool

Viele Benutzer des eLP wünschten sich für Tablet eine Umgebung in der sie auch mit dem Stift zeichnen können. Deswegen haben wir den CKEditor 5 um ein Zeichnungstool erweitert. Da das Speichern vieler Zeichnungen zu viel Speicherplatz benötigt, soll das Zeichnungstool nur in der geplanten Desktop-Offline Variante des eLP zur Verfügung stehen. Unser Zeichentool basiert auf dem HTML-Canvas-Element. Über die vorhandenen Befehle lassen sich beliebige Zeichnungen programmieren. Durch das Zwischenspeichern der Mausclicks oder Stiftberührungen des Tabletnutzers lassen sich beliebige Zeichnungen realisieren. Diese zwei Elemente bilden die Grundlage des Zeichentools.

Aufbau des Tools

img

Der Nutzer/Die Nutzerin hat die Möglichkeit verschiedene Farben auszuwählen, die Stiftstärke zu bestimmen, zwischen Stift und Radiergummi zu wählen, eine Seite zu löschen und das Tool zo schließen. Über die verschiedenen Seitenzahlen hat er/sie die Möglichkeit zwischen den verschiedenen Seiten zu wechseln oder über das "+"-Symbol neue zu erstellen. Auf der weißen Fläche kann der Nutzer/die Nutzerin zeichnen.

Dateistruktur

Bei der Dateistrutur haben wir uns an ein Beispiel in der CKEditor 5 Dokumentation gehalten, welches du hier findest.
In der Datei ckeditor5/src.js wird das Plugin eingebunden:

import NewCanvas from '../customPlugins/NewCanvas/newcanvas.js';

ckeditor5/customPlugins/NewCanvas/newcanvas.js: Definiert die new NewCanvas-Klasse, die quasi leer ist und nur andere Klassen (NewCanvasEditing, NewCanvasUI) einbindet.
ckeditor5/customPlugins/NewCanvas/canvasediting.js: Registriert folgenden Befehl im Editor:

 this.editor.commands.add( 'insertNewCanvas', new InsertNewCanvasCommand( this.editor ) );

ckeditor5/customPlugins/NewCanvas/canvasui.js: Definiert die UI des Tools, die in den Editor integriert werden soll. Besteht nur aus einem Button, über den der Befehl 'insertNewCanvas' aufgerufen wird. Die restliche UI ist außerhalb des Editors realisiert worden. To-Do: Neue Version erstellen, die überflüssigen Code des alten UI-Konzepts entfernt und einen aktuellen Namen für den Knopf finden & diesen dann hier auch ändern!

this.listenTo( dropdownView, 'execute', () => editor.execute( 'insertNewCanvas' ) );

ckeditor5/customPlugins/NewCanvas/insertnewcanvascommand.js: Beinhaltet unter anderem den Code, durch den der Canvas erstellt wird, die Buttons hinzugefügt werden und die Toolbar beschrieben werden weiter zum zugehörigen Abschnitt.

ckeditor5/customPlugins/NewCanvas/canvasDummy.js: Unbekannte HTML-Elemente können nicht einfach im Model gespeichert und aus diesem geladen werden. Das Element muss registriert werden und der Upcast und Downcast definiert werden. Das beinhaltet diese Datei für das HTML-Element CanvasDummy über welches wir die Bild-Daten speichern. Wie registrier ich ein Element?

ckeditor5/customPlugins/NewCanvas/canvasInfo.js Beinhaltet die Conversion für das Element CanvasInfo, welches wir benötigen um die Höhe und die Breite des Canvas zu speichern.

Zeichnungsfunktion

Der Algorithmus der Zeichnungsfunktion befindet sich im Verzeichnis planer/Resources/packages/ckeditor5/customPlugins/NewCanvas/Draw. Dieser ist objektorientiert realisisert. Grundlage ist das Objekt mit dem Namen obj.

img

lineWidht: Legt die Stiftstärke fest.
color : Legt die Stiftfarbe fest.
mode : Legt den Zeichnungsmodus fest (Zeichnen, Radieren).
ctx : Variable zum Speichern des canvas.CanvasRenderingContext2D
height : Variable zum Speichern der Höhe des Canvas-Elements.
width : Variable zum Speichern der Weite des Canvas-Elements.
draw(pMyPics): Methode zum Zeichnen per Maus. Über den Übergabeparameter pMyPics soll das Canvas-Element, auf dem gezeichnet wird, übergeben werden. Innerhalb der Funktion werden weitere Funktionen aufgerufen, z. B. das Zeichnen per Touchscreen. Diese Funktion und alle weiteren Funktionen sind im Code Stück für Stück genauer durch Kommentare beschrieben. Daher dient Folgendes der Übersicht.

weitere Funktionen

  • function drawLine(context, x1, y1 ,x2 ,y2): Über den Context des Canvas wird eine Linie gezeichnet. Falls Sie die verwendeten Befehle nicht kennen, finden Sie hier eine gut verständliche Beschreibung und Beispiel. Weiterhin könnte zum Verständnis diese Seite hilfreich sein.
  • function getImageData(myPics, thistmp, evt, callback): Um die Daten des Canvas-Context zu bekommen, um sie z. B. zu speichern, gibt es standardmäßig die Funktion CanvasRenderingContext2D.getImageData(). Warum braucht es dann eine eigene Funktion? In der Anwendung stellen sich folgende Probleme:
  • Der Canvas ist noch nicht initialisiert.
  • Die größe des Canvas des Nutzers kann sich ändern(Verwendung eines anderen Geräts, Veränderung der Größe des Browserfensters). Erstes Problem lässt sich leicht mit einer if-Abfrage lösen. Das zweite Problem ist komplitzierter. Wenn das neue Bild größer ist als das alte, ist das unproblematisch. Anders herum wird es schwierig. Wenn ich die Daten einfach speichere, dann habe ich die Teile des Bildes, die ursprünglich größer waren, gelöscht. Daher verwenden wir für den letzten Fall die Funktion drawImage um das neue Bild in das alte einzufügen. Die Daten werden dann im Model des CK-Editor 5 gespeichert und werden darüber letztendlich im User-FileLink zur Beschreibung einfügen gespeichert.
  • function startup(pMyPics) Funktion zum Zeichnen per Touchscreen. Fügt Event-Listener den TouchEvents hinzu. Beim Zeichnen per Touchscreen werden andere Events ausgelöst als beim Zeichnen mit der Maus. Diese Seite könnte für Ihr Verständnis hilfreich sein.
  • handleStart, handleEnd, handleCancel, handleEnd, handleMove ... sind Funktionen, die das jeweilige TouchEvent abhandeln.
  • getAcutalModel, getActualModelDataURL Der Index des aktuellen Canvas-Elements wird übergeben. Sucht den entsprechenden CavnasDummy im Model und gibt alle bereits gespeicherten Daten oder nur die bereits gespeicherte DataURL zurück.

Funktionen zum Aufbau der Elemente des Tools

Der Code der Funktion in ckeditor5/customPlugins/NewCanvas/insertnewcanvascommand.js beschreibt unter anderem den Aufbau der Elemente die dem User angezeigt werden, wie der Canvas, die Werkzeugleiste und die Seitenauswahl.

Es wird eine neue Klasse erstellt, die nur eine Methode enthält. Diese ist im Editor registriert wird vom Benuter über die UI aufgerufen.

export default class InsertNewCanvasCommand extends Command {
execute() {
...
}}

Im folgenden wird nicht der komplette Code, sondern zum grundlegenden Verständnis des Aufbaus elementare Elemente beschrieben.

Methode execute()

img


Zuerst wird überprüft, ob sich im HTML im Bereich der Editor Instanz schon Canvas-Elemente befinden.
if(this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0].getElementsByTagName('canvas').length == 0)

Es wird überprüft, ob sich im "model" des Editors schon CanvasDummy(Link zur Beschreibung einfügen) Elemente befinden, d. h. der Benutzer hat schonmal etwas gezeichnet.

let containsCanvasDummy = editorContainsCanvas(this.editor.model.document.getRoot());

(Link zur Beschreibung einfügen)

Falls es noch keinen CanvasDummy im "model" gibt, füge zwei neue Elemente dem "model" hinzu: 1. einen CanvasDummy und 2. CanvasInfo zum Speichern der Canvas-Dimensionen. Diese können je nach Größe des Browserfensters unterschiedlich sein.

this.editor.model.change( writer => {
...
thistmp.editor.model.insertContent(writer.createElement('canvasDummy', object), writer.createPositionAt( root, 'end' ));
if(getCanvasInfoHeight(root) == false)
thistmp.editor.model.insertContent(writer.createElement('canvasInfo', object2), writer.createPositionAt( root, 'end' ));
...
}

Falls schon ein Canvas-Dummy existiert, speichere Information über die Größe des Canvas.

if(containsCanvasDummy){
this.obj.height = getCanvasInfoHeight(this.editor.model.document.getRoot());
this.obj.width = getCanvasInfoWidth(this.editor.model.document.getRoot());
}

Falls das Canvas und die Bedienelemente minimiert sind.

else if(editorContainsCanvas(this.editor.model.document.getRoot()) && this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0].classList.contains('canvas-minimized')){            
maximizeCanvas.bind(this)();
this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0].classList.remove('canvas-minimized');
this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0].classList.add('canvas-maximized');
this.editor.sourceElement.nextElementSibling.classList.add('newCanvas');
this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__top ck-reset_all")[0].classList.add('hide');
maximizeButton.click();
}

Falls das Canvas und die Bedienelemente maximiert sind.

else if(editorContainsCanvas(this.editor.model.document.getRoot()) && this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0].classList.contains('canvas-maximized')){
minimizeCanvas.bind(this)()
this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0].classList.remove('canvas-maximized');
this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0].classList.add('canvas-minimized');
this.editor.sourceElement.nextElementSibling.classList.remove('newCanvas');
}

function newCanvas()

Erstellt ein Canvas-Element und fügt es ins DOM ein.

var parent = this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0];

Erstellt falls nicht vorhanden einen FocusTracker. Immer wenn der Benutzer außerhalb des Editors klickt, soll die das Canvas gespeichert werden.

if (!onChangeFocusTracker) onChangeFocusTracker =  function onChangeFocusTracker (evt, data, isFocused, old, caller)
...
getImageData(canvasTmp, thistmp, evt)
...

function applyImageData()

Lädt gespeicherte Bild-Dateien vom Model in das zugehörige Canvas.

ar imgdataFromModel = getActualModelImageData(this.editor.model.document.getRoot(), this.editor.sourceElement.nextElementSibling.getElementsByClassName('buttonbar-canvas')[0].getAttribute('selectedCanvas'));
if(imgdataFromModel != 0){
var image = new Image()
image.onload = function(){
context.drawImage(image, 0, 0)
};
image.src = imgdataFromModel;
this.imgdata = imgdata;
}

function minimizeCanvas()

Minimiert den Canvas und seine Bedienelemente.

function minimizeCanvas(){
this.editor.sourceElement.nextElementSibling.getElementsByTagName("canvas")[0].classList.add('hide');
minimizeButtonBar.bind(this)();
minimizeToolbar.bind(this)();
}

function maximizeCanvas()

Maximiert den Canvas und seine Bedienelemente.

function maximizeCanvas(){
this.editor.sourceElement.nextElementSibling.getElementsByTagName("canvas")[0].classList.remove('hide');
maximizeButtonBar.bind(this)();
maximizeToolbar.bind(this)();
}

function resizeCanvas()

Wenn die Größe des Browserfensters geändert wird, müssen die im Editor-model gespeicherten Bild-Daten neu ins Canvas geladen werden. Dies hat den Grund, dass Canvas-Elemente beim Zoomen verzerren.

 function resizeCanvas(){
var textfield = this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0];
let myPics = textfield.getElementsByTagName('canvas')[0];
myPics.setAttribute("height", textfield.offsetHeight);
myPics.setAttribute("width", textfield.offsetWidth);
applyImageData.bind(this)();
}

function createToolBar()

var toolbar = document.createElement('div');
toolbar.setAttribute('class', 'toolbar');
...

Eine Toolbar mit Bedienelementen wird erstellt.
img

Ein Dropdown-Menü wird erstellt.

let dropdown = document.createElement('div');
toolbar.appendChild(dropdown);
dropdown.setAttribute('class', 'dropdown');

Es wird der Content des Dropdwon-Menüs erstellt. Der obere Bereich enthält vier Farben. Der Bereich wird "aufgeklappt" wenn auf eine der vier Farben geklickt wird. Der User sieht dann eine Farbpalette. Wird eine Farbe ausgewählt, ersetzt sie die bisher ausgewählte Farbe in der Toolbar. Es können bis zu vier Farben ausgewählt und in der Toolbar "abgelegt" werden. Die Farbe in der geschrieben wird, wird mit einem grünen Kreis umrandet. Weiterhin kann die Stiftstärke eingestellt werden.
img

var colors =['black', 'red', 'blue', 'green', 'silver', 'maroon', 'navy', 'yellow', 'grey', 'purple', 'teal', 'lime', 'white', 'fuchsia', 'aqua', 'olive'];
...
// Die oberen vier Farben werden erstellt.
selectedButtonsColor.push(document.createElement('button'));
selectedButtonsColor[i].setAttribute("class", 'colorPicker');
...
selectedButtonsColor[i].addEventListener('click', (function(color, i){
if(selectedButtonsColor[i].getAttribute('status') == 'unselected'){
for(let j = 0; j<4; j+=1){
selectedButtonsColor[j].setAttribute('status', 'unselected');
selectedButtonsColor[j].classList.remove('selected');
}
...
selectedButtonsColor[i].setAttribute('status', 'selected');
...
// Die unteren vier Farben werden erstellt.
for(var i = 0; i<colors.length; i+=1){
buttonsColor.push(document.createElement('div'));
buttonsColor[i].setAttribute("class", 'colorPicker');
buttonsColor[i].classList.add(colors[i]);
buttonsColor[i].classList.add('dropwdown');
buttonsColor[i].addEventListener('click', (function(color){
changeColor.bind(thistmp)(undefined, color);
dropdownContent.selectedButton.setAttribute("class", 'colorPicker '+ color + ' selected');
dropdownContent.style.display = 'none';
}).bind(this, colors[i]));
dropdownContent.appendChild(buttonsColor[i]);
}
}}
...
//Der Stiftstärke-Slider
let sliderHeading = document.createElement('h6');
sliderHeading.setAttribute('class', 'sliderHeader');
...

Um auszuwählen, ob man zeichnen/schreiben oder radieren möchte, wird ein Draw-Mode-Dropdown erstellt.

let modeDropdownContent = document.createElement('div');
modeDropdownContent.setAttribute('class', 'drawModeDropdownContent');
...
let penButton = document.createElement('button');
modeDropdownContent.appendChild(penButton);
...
let rubberButton = document.createElement('button');
modeDropdownContent.appendChild(rubberButton);
...

img

Um eine einzelne Seite löschen zu können, wird ein Lösch-Button erstellt. Wenn die letzte Seite gelöscht wird, wird das Zeichnen im Canvas-beendet.

let bin = document.createElement('div');
toolbar.appendChild(bin);
bin.setAttribute('class', 'bin fa fa-trash fa-2x');
bin.addEventListener('click', deleteCanvas.bind(this));

img

Um das Zeichnen im Canvas beenden zu können, wird ein Schließen-Button erstellt. (buggy)

et closeButton = document.createElement('div')
toolbar.appendChild(closeButton);
let maximizeButton = this.editor.sourceElement.nextElementSibling.getElementsByClassName('fa-arrows-alt')[0];
maximizeButton.click();
minimizeCanvas.bind(this)();
console.log(this);
this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0].classList.remove('canvas-maximized');
this.editor.sourceElement.nextElementSibling.getElementsByClassName("ck ck-editor__main")[0].classList.add('canvas-minimized');
this.editor.sourceElement.nextElementSibling.classList.remove('newCanvas');
editorToolbar.classList.remove('hide');

img

function createButtonBar()

var divTmp  = document.createElement('div');
divTmp.setAttribute('selectedCanvas', 1);
divTmp.setAttribute('class', 'buttonbar-canvas');
...

Eine "Button-Bar" ermöglicht es dem Benutzer mehrere Canvas zu erstellen und zwischen diesen zu wechseln.

img

function initialiseButtons()

Es müssen verschiedene Event-Listener je Button erstellt werden, damit das richtige Canvas geöffnet wird oder ein neues erstellt wird.

...
//Von Button 2 bis Button X
for(var i = 2; i<=dummyCount; i++){
buttonTmp = document.createElement('button');
buttonTmp.setAttribute('class', 'button page-number');
pButtonBar.appendChild(buttonTmp);
buttonTmp.appendChild(separator.cloneNode(true));
node = document.createTextNode(i);
buttonTmp.appendChild(node)
//Event-Listener hinzufügen
buttonTmp.addEventListener("click", (function(j){
selectCanvasPage.bind(thistmp)(pButtonBar, j);
}).bind(this, i));
}
//+Button
var plusTmp = document.createElement('button');
plusTmp.setAttribute('class', 'button page-number');
var nodeplus = document.createTextNode("+");
plusTmp.appendChild(nodeplus);
pButtonBar.appendChild(plusTmp);
plusTmp.appendChild(separator.cloneNode(true));
pButtonBar.setAttribute('buttonCount', dummyCount+1);
plusTmp.addEventListener("click", (function(){addNewCanvasPage.bind(this)(pButtonBar);}).bind(this));
}

function addNewCanvasPage(pButtonBar)

Wenn auf den "PlusKnopf" gedrückt wird, um ein neuen Canvas zu erstellen, wird diese Funktion aufgerufen. In dieser werden ein neuer Button für die neue Seite erstellt und ein neuer Canvas-Dummy im Model erstellt und "applyImageData" ausgeführt, damit der Inhalt des vorherigen Canvas verschwindet.

var buttonTmp  = document.createElement('button');
buttonTmp.setAttribute('class', 'button page-number');
...
this.editor.model.change( writer => {
...
thistmp.obj.modelCanvas = thistmp.editor.model.insertContent(writer.createElement('canvasDummy', object), writer.createPositionAt( root, 'end' ));
...
} );
...
applyImageData.bind(this)();
...

function selectCanvasPage(pButtonBar, number, del)

Wenn der Benutzer eine andere Seite als die bisherige auswählt, wird eine neuer Button "gehighlighted" und "applyImageData" ausgeführt, damit der Inhalt des gewünschten Canvas geladen wird und für den Benutzer sichtbar ist.

...
stopHighlightButton(pButtonBar, pButtonBar.getAttribute('selectedCanvas'))
pButtonBar.setAttribute('selectedCanvas', number);
highlightButton(pButtonBar.childNodes[number - 1])
applyImageData.bind(this)();
...

function deleteCanvas()

Funktion wird aufgerufen, wenn der Benutzer auf den "Lösch-Button" klickt. Löscht den aktuell ausgewählten Canvas. Wenn es der letzte Canvas war, wird der Zeichnungs-Modus beendet.

...
buttonbar.removeChild(buttonbar.childNodes[selectedCanvas-1]);
...

function editorContainsCanvas(pRoot)

Überprüft ob sich mindestens ein Canvas im Model befindet.

function deleteCanvasInModel(pRoot, pSelectedCanvas)

Löscht ein CanvasDummy im Model anhand des übergebenen Index.

function getActualModelImageData(pRoot, pSelectedCanvas)

Übergibt die Bild-Daten als DataURL eines Canvas anhand des übergebenen Index.

function getCanvasDummyCount(pRoot)

Gibt die Anzahl der gespeicherten CanvasDummys zurück.

function getCanvasInfoHeight(pRoot)

Übergibt die Höhe des zuletzt gespeicherten Canvas. Wenn das ELP geschlossen wird und neu geladen wird, kann das Canvas somit einfacher geladen werden.

function getCanvasInfoWidth

Übergibt die Breite des zuletzt gespeicherten Canvas. Wenn das ELP geschlossen wird und neu geladen wird, kann das Canvas somit einfacher geladen werden.

function changeLineWidth(event, pLineWidth)

Die Linienstärke wird geändert. Wird im Objekt gespeichert und wird so von obj.draw. automatisch verwendet.

this.obj.lineWidth = pLineWidth;

Äquivalent verhalten sich die Funktionen function changeColor(event, pColor) und function setMode(event, pMode) für die Farbe und den Schreibmodus.