Wenn man das ImageData beim HMTL5 Canvas ändern möchte.
Das geht nicht:
imageData.data = frame;
Aber das geht:
for(var ifr=0; ifr<frame.length;ifr++){
imageData.data[ifr] = frame[ifr];
}
Nach dem ich den Tag mit der Sound API für Java verbracht habe, hab ich dann für heute noch mal eine Verbesserung für MP4toGIF.com in Angriff genommen. Neben den normalen Farbfiltern wird nun noch ein Vignitierungsfilter als Ergänzung zu den Farbfiltern hinzukommen.
Mit dem Canvas sind solche Effekte sehr einfach umzusetzen und sind dabei noch relativ performant.
function createVignetting(ctx){
var x=Math.round(ctx.canvas.width/2);
var y=Math.round(ctx.canvas.height/2);
var grd=ctx.createRadialGradient(x,y,1,x,y,y-5);
grd.addColorStop(0,'rgba(0,0,0,0)');
grd.addColorStop(1,'rgba(0,0,0,0.8)');
ctx.fillStyle=grd;
ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height);
return ctx;
}
Nachdem längere Zeit bei MP4toGIF.com nichts mehr passiert ist, habe ich heute mich noch mal mit neuen Filtern auseinander gesetzt und mir eine Umgebung zusammen gebaut, in der ich mit neuen Filtern experimentieren kann. Es ist schon interessant, wie man mit dem Ändern weniger Parameter sehr verschiedene Effekte erzielen kann.
Die Test-Umgebung findet man unter http://www.annonyme.de/js/filters/. Da kann jeder gerne mal herum experimentieren und versuchen selbst Einstellungen für einen tollen Effekt zu finden.
In den nächsten Tagen und Wochen, werden dann also wohl noch ein paar Filter mehr für MP4toGIF.com entstehen und dort integriert werden.
Die zentrale Render-Function ist sehr einfach aufgebaut:
function render(src, trg, rm, ra, gm, ga, bm, ba, useGreyScale){
var srcData=src.getImageData(0,0,src.canvas.width,src.canvas.height);
var data=srcData.data;
for(var i=0;i<data.length;i+=4){
if(useGreyScale){
var avg = (data[i+0]*rm) + (data[i+1]*gm) + (data[i+2]*bm);
data[i+0]=avg;
data[i+1]=avg;
data[i+2]=avg;
}
else{
data[i+0]=data[i+0]*rm;
data[i+1]=data[i+1]*gm;
data[i+2]=data[i+2]*bm;
}
data[i+0]=Math.abs(data[i+0]+parseInt(ra));
data[i+1]=Math.abs(data[i+1]+parseInt(ga));
data[i+2]=Math.abs(data[i+2]+parseInt(ba));
}
trg.canvas.width=src.canvas.width;
trg.canvas.height=src.canvas.height;
trg.putImageData(srcData,0,0);
}
Invertieren des Bildes ist zum Beispiel: Multiplicator auf 1 und darauf -255 addieren.

Wer solche Diagramme mit dem Canvas Element und etwas Javascript zeichnen möchte, kann gerne diese von mir geschriebene Klasse verwenden:
function BarChartFactory(canvasElement,unitName,plotEvery){
this.canvas=canvasElement;
this.maxValue=0;
this.categoryTitleWidth=50;
this.valueTitleWidth=50;
this.bottomScalaHeight=10;
this.padding=5;
this.linesSeperateValues=true;
this.barStart=0;
this.barEnd=0;
this.barTranslationFactor=1;
this.barHeight=20;
this.barPadding=4; //top + bottom
this.xPlotEvery=plotEvery;
this.unitName=unitName;
this.categoryValueSize=2; //if more.. ignore.. if less.. paint empty
this.categories=[];
this.ctx=null;
this.width=200;
this.height=0;
this.addValue=function(categoryTitle,valueTitle,value,valueColor){
var cat=null;
for(var ii=0;ii<this.categories.length && cat==null;ii++){
if(this.categories[ii].title==categoryTitle){
cat=this.categories[ii];
console.log("found existing category: "+categoryTitle);
}
}
if(cat==null){
cat={title:"",values:[]};
cat.title=categoryTitle;
this.categories[this.categories.length]=cat;
}
cat.values[cat.values.length]={value:value,title:valueTitle,color:valueColor};
};
this.init=function(ctx,width){
this.barStart=this.categoryTitleWidth+this.valueTitleWidth;
this.barEnd=width;
for(var ii=0;ii<this.categories.length;ii++){
var cat=this.categories[ii];
for(var j=0;j<cat.values.length;j++){
if(cat.values[j].value>this.maxValue){
this.maxValue=cat.values[j].value;
}
}
if(cat.values.length>this.categoryValueSize){
this.categoryValueSize=cat.values.length;
}
}
this.maxValue=this.maxValue+(this.maxValue/10);
this.barTranslationFactor=(this.barEnd-this.barStart)/this.maxValue;
this.ctx=ctx;
this.ctx.canvas.width=width+(this.padding*2);
this.ctx.canvas.height=(this.categories.length*this.categoryValueSize*this.barHeight)+this.bottomScalaHeight+(this.padding*2);
this.width=this.ctx.canvas.width;
this.height=this.ctx.canvas.height;
//TODO clearRect
};
this.paintStructure=function(){
//paint main structure
this.ctx.strokeStyle="#AAAAAA";
this.ctx.beginPath();
this.ctx.moveTo(this.categoryTitleWidth+this.valueTitleWidth,0+this.padding);
this.ctx.lineTo(this.categoryTitleWidth+this.valueTitleWidth,this.height-this.barHeight+this.barPadding);
this.ctx.stroke();
if(this.xPlotEvery>0){
console.log("paint structure ("+this.maxValue+")");
var xStart=this.categoryTitleWidth+this.valueTitleWidth;
for(var i=this.xPlotEvery;i<this.maxValue;i=i+this.xPlotEvery){
this.ctx.beginPath();
this.ctx.moveTo(xStart+(i*this.barTranslationFactor),0+this.padding);
this.ctx.lineTo(xStart+(i*this.barTranslationFactor),this.height-this.barHeight+this.barPadding);
this.ctx.stroke();
this.ctx.fillStyle="#000000";
this.ctx.fillText(""+Math.round(i)+this.unitName,xStart+(i*this.barTranslationFactor), this.height-this.barHeight+this.barPadding+12);
}
}
};
this.paint=function(width){
this.init(this.canvas.getContext("2d"),width);
this.ctx.lineWidth=1;
this.ctx.strokeStyle="#000000";
this.paintStructure();
this.ctx.strokeStyle="#000000";
for(var iC=0;iC<this.categories.length;iC++){
var cat=this.categories[iC];
this.ctx.fillStyle="#000000";
this.ctx.fillText(cat.title,this.padding,(((iC)*this.categoryValueSize)+Math.round(this.categoryValueSize/2))*this.barHeight+this.padding);
for(var i=0;i<this.categoryValueSize;i++){
var value={value:0,title:"",color:"#FF0000"};
if(cat.values){
value=cat.values;
}
this.ctx.fillStyle="#000000";
this.ctx.fillText(value.title,this.categoryTitleWidth,((iC*this.categoryValueSize)+i+1)*this.barHeight);
//line under value title
this.ctx.beginPath();
if(i==this.categoryValueSize-1){
this.ctx.moveTo(this.padding,((iC*this.categoryValueSize)+i+1)*this.barHeight+this.padding);
}
else{
this.ctx.moveTo(this.categoryTitleWidth,((iC*this.categoryValueSize)+i+1)*this.barHeight+this.padding);
}
if(this.linesSeperateValues){
this.ctx.lineTo(this.width-this.padding,((iC*this.categoryValueSize)+i+1)*this.barHeight+this.padding);
}
else{
this.ctx.lineTo(this.categoryTitleWidth+this.valueTitleWidth,((iC*this.categoryValueSize)+i+1)*this.barHeight+this.padding);
}
this.ctx.stroke();
//paint real value
this.ctx.beginPath();
this.ctx.rect(this.categoryTitleWidth+this.valueTitleWidth,((iC*this.categoryValueSize)+i)*this.barHeight+this.padding+this.barPadding,value.value*this.barTranslationFactor,this.barHeight-(this.barPadding*2));
this.ctx.stroke();
this.ctx.fillStyle=value.color;
this.ctx.fill();
}
}
};
}
Das Beispiel zum Bild sieht dann so aus:
<html>
<head>
<title>2dbar test</title>
<script type="text/javascript" src="./BarChartFactory.js"></script>
</head>
<body>
<canvas id="can" style="border:1px solid #000000;border-radius:4px;">
</canvas>
<script type="text/javascript">
var bcf=new BarChartFactory(document.getElementById("can"),"fps",10);
bcf.addValue("386SX","16 Mhz",20,"#70B4B6");
bcf.addValue("386SX","25 Mhz",25,"#0000FF");
bcf.addValue("386DX","25 Mhz",40,"#70B4B6");
bcf.addValue("386DX","33 Mhz",60,"#70B4B6");
bcf.paint(400);
</script>
</body>
</html>
Irgendwann kommt der Zeitpunkt, da ist eine Anwendung langsam. Es liegt nicht am Datenbankserver oder der Netzwerkanbindung oder der Auslastung des Servers. Es liegt einfach ganz allein daran, dass die Anwendung langsam ist.
Oft findet man einige Dinge von selbst heraus. Aber oft ist man einfach überfragt in welchen Teilen der Anwendung die Zeit verloren geht. Was braucht lange? Werden einige Dinge unnötig oft aufgerufen? Zu viele Dateisystem-Zugriffe?
Hier hilft dann nur noch ein Profiling der Anwendung. Profiling ist einfach die Anwendung eine Zeit lang zu überwachen und zu protokollieren, wie viel Zeit in der Zeit auf welche Methoden oder Funktionen verwendet wird.
Das alleine sagt natürlich erstmal nicht wo Probleme vorhanden sind. Deswegen halte ich die Idee ein separate Team solche Performance-TEst durch zu führen und zu analysieren für nicht ganz so zielführend. Denn manchmal brauchen einige Methoden viel Zeit. Da man Zeit sowie so meistens nur in Verhältnis der Methoden zu einander betrachtet muss man wissen was schnell sein soll und was langsam sein sollte oder darf.
Ich hatte mal bei Bouncy Dolphin das Problem, dass alles an sich ganz schnell lief, aber beim Profiling auf eine Methode fast 40% der Zeit ging, die nur den aktuellen Punktestand auf das Canvas zeichnete. Nach viel hin und her Probieren kopierte ich den Inhalt eines Canvas mit dem Punktestand auf das Haupt-Canvas. Das Canvas mit dem Punktestand wurde nur neu gezeichnet wenn sich der Punktestand auch änderte. Danach verbrauchte die Methode nur nach 15%. Also war es schneller das gesamte Canvas zu kopieren als eine oder zwei Ziffern zeichnen zu lassen.
document.getElementById["aaaaa"].value=score verursacht z.B. auch extrem hohe Kosten. Also immer alle wichtigen Elemente in Variablen halten und nicht jedesmal neu im Document suchen!
Wärend man in Java extrem mächtige Tools wie VisualVM hat und der Profilier des Firefox oder Chrome einem bei JavaScript Problem sehr gut hilft, ist die Situation bei PHP etwas umständlicher. Zwar kann man so gut wie immer XDebug verwenden, aber so einfaches Remote-Profiling wie mit VisualVM ist da nicht zu machen.
Aber da man meistens sowie so lokal auf dem eigenen PC entwickelt und testet, reicht es die Daten in eine Datei schreiben zu lassen und diese dann mit Hilfe eines Programms zu analysieren.
xdebug.profiler_enable=1
xdebug.profiler_output_dir=C:/test/profile
PHPStorm bringt ein entsprechendes Tool mit.
Aber ich habe bis jetzt WinCacheGrind verwendet. Damit ließen sich nach etwas Einarbeitung dann schnell heraus finden, wo die Zeit verloren ging und welche Methoden wie oft aufgerufen wurden.
Der Class-Loader durchsuchte das System-Verzeichnis zu oft, weil an der Stelle nicht richtig geprüft wurde, ob die Klasse schon bekannt war. So konnte ich die Ladezeit einer Seite in meinem Framework am Ende nach vielen solcher Probleme von 160ms auf ungefähr 80ms senken. Viel Caching kam auch noch dazu und das Vermeiden von Zugriffen auf das Dateisystem.
Aber es gibt noch andere Profiler als XDebug für PHP. Hier findet man eine gute Übersicht:
PHP Profiler im Vergleich
Ich hab schön öfters gehört, dass solche Test und das Profiling ans Ende der Entwicklung gehören und man so etwas nur macht wenn man keine andere Wahl hat. Aber am Ende findet man viele Fehler dabei und ich halte es für falsch nicht schon am Anfang zu testen ob eine Anwendung auch später mit vielen produktiven Daten noch performant laufen wird. Denn am Ende sind grundlegende Fehler in der Architektur schwerer und auf wendiger zu beheben als am Anfang oder in der Mitte der Entwicklung.
Nachträglich an einzelnen Stellen Caching einzubauen ist auch nicht so gut wie von Anfang an ein allgemeinen Caching-Mechanismus zu entwerfen, der an allen relevanten Stellen automatisch greift.
Deswegen sollte man auch schon ganz am Anfang immer mal einen Profiler mitlaufen lassen und gucken, ob alles so läuft wie man es sich dachte.
Ein einfaches Code-Beispiel um mit JavaScript ein Bild in ein schwarz-weiß Bild umzuwandeln. Läuft schnell und ist relativ unkompliziert einzubauen.
if(this.color=="bw"){
var idata=ctx.getImageData(0,0,ctx.canvas.width,ctx.canvas.height);
var data=idata.data;
for(var i=0;i<data.length;i+=4){
var grayValue=data*0.3+data[i+1]*0.59+data[i+2]*0.11;
data=grayValue;
data[i+1]=grayValue;
data[i+2]=grayValue;
//dont change alpha value [i+4]
}
ctx.putImageData(idata,0,0);
}
Vor dem Upload ist es oft wünschenwerts ein Bild drehen/rotieren zu können.
Ich hatte mich damals doch sehr damit rumgeärgert, um den richtigen Mittelpunkt bei Rotationen heraus zu bekommen.
Hier ist meine Lösung für Rotationen um 90,180,270 Grad (select oder durch Auf- und Abrunden kann man das sicher stellen,
dass keine anderen Werte eingegeben werden).
var rotate=parseInt(this.rotate);
if(rotate>0){
if(rotate!=180){
ctx.canvas.width=height;
ctx.canvas.height=width;
}
else{
ctx.canvas.width=width;
ctx.canvas.height=height;
}
ctx.save();
ctx.translate(ctx.canvas.width/2,ctx.canvas.height/2);
ctx.rotate(((Math.PI / 180)*rotate));
ctx.drawImage(this.img.image,0-(width/2),0-(height/2),width,height);
ctx.restore();
}
else{
ctx.canvas.width=width;
ctx.canvas.height=height;
ctx.drawImage(this.img.image,0,0,width,height);
}
Wenn man nicht das tollste Smartphone mit dem schnellsten Internet hat, hat man öfters das Problem, dass man ein Foto macht und es dann gerne irgendwo hin hochladen möchte. Dann dauert es ewig, bricht manchmal dann mittendrin ab und am Ende hat es viel Volumen verbraucht. Nur damit die doch einiger Massen gute Kamera des Smartphones ein belangloses Foto gemacht hat, dass die volle Auflösung nicht gebraucht hätte und am Ende auf Serverseite sowie so noch mal kaputt komprimiert wird. Es wird aber selten die Auflösung noch angepasst als eher die JPEG-Qualität runter gedreht (auf etwas mal man klassisch so bei 70% oder weniger schätzen würde).
Hoch aufgelöste Bilder wo alle Details durch die Kompression kaputt gemacht wurden. Wenn es nicht in der vollen Auflösung angezeigt wird merkt man es weniger (Super-Sampling.. auch gut um Rauschen in Bildern zu entfernen). Man könnte natürlich auch das Foto schon auf dem Client/Smartphone schon so weit verkleinern, dass es nicht unnötig Volumen und Bandbreite verbraucht. Aber auf welche Auflösung sollte man das Bild runter rechnen?
Wenn der Benutzer super Qualität mit vielen Details und guten Farben am Ende erwartet.. also im Grunde genau das was er hochgeladen hat und mit etwas Glück aus einer modernen DSLR stammt.. ja.. dann ist 6MP schon Minimum. Mit minimalen Vergrößern sollte es sogar auf 4K noch hinnehmbar aussehen. Sonst muss es leider schon 10MP sein.
Aber im normal Fall wird man eine Website/Anwendung haben, die eine Reihe kleinerer Bilder anzeigt und dann bei einem Klick darauf das Bild vergrößert anzeigt. Entweder auf Vollbild, wobei oft Ränder noch da sind, sowie Bereich für Titel, Beschreibung und Datum. In vielen Fällen auch noch Platz für Kommentare.
Kaum jemand hat eine Auflösung über 1920x1200 oder 1920x1080. Also die Breite auf max 1920 oder die Höhe auf max 1080 zu skalieren wird für normale Anwendungen, die nur zum Betrachten da sind und nicht um die Bilder nochmal runter zu laden und zu bearbeiten, vollkommen reichen.
Seiten die viele Bilder halten im Zusammenhang mit Texten (Foren und Imageboards) brauchen noch sehr viel geringere Auflösungen. Selbst Seiten wie 9gag haben bis auf wenige Ausnahmen Bilder in hohen Auflösungen. Wir reden hier nur von Bildern und nicht von GIFs.. das wäre nochmal ein Thema für sich.
Außer bei diesen sehr langgezogenen Bildern wäre eine Breite über 1000 Pixeln kaum nötig. Also 1MP reicht für die meisten Zwecke. Die Vorschaubilder sind noch bedeutend geringer aufgelöst. Und auch bei Facebook reichen Bilder mit 1000px in der Breite eigentlich immer aus, wenn man nicht detaillierte Landschaften zeigen möchte.
1000x1000 Pixel sind schon mal eine ganz andere Größe. Wir gelangen da von 2MB auf gut einige 100KB. Das macht auch beim Upload extreme Unterschiede.
Ideal wäre eine Check-Box, die man setzen kann, wenn es ein Bild mit vielen Details ist. Das Problem wären normale Benutzer, die Abends in der Cocktailbar ihr Essen im Kerzenschein fotografieren müssen und keine ruhige Hand haben und nun glauben, es wäre ein hoch detailreiches fotografisches Kunst entstanden. Wenn man genug solche Benutzer hat, würde die Checkbox immer angeklickt werden, weil kein Wissen darüber besteht, wann was von Vorteil ist und dann lieber die "bessre" Qualität gewählt wird.
Am Ende bleibt die Frage, wie kann ich Bilder vor dem Upload verkleinern? Früher wurde das doch immer auf dem Server erledigt.
Mit dem Canvas-Element auf HTML5 geht es extrem einfach. Man braucht nur das Bild am img-Element vorliegen.
Beispiel (wie man an $scope sieht ist es AngularJS-Code). $scope.longestSide gibt die max Breite hier an.
var canvas=document.createElement("canvas");
var factor=1;
if($scope.newPostPreviewImage.width>$scope.longestSide){
factor=($scope.newPostPreviewImage.width/$scope.longestSide);
}
canvas.width=$scope.newPostPreviewImage.width/factor;
canvas.height=$scope.newPostPreviewImage.height/factor;
var ctx=canvas.getContext("2d");
ctx.drawImage($scope.newPostPreviewImage,0,0,canvas.width,canvas.height);
blob=dataURItoBlob(canvas.toDataURL("image/jpeg",0.7));
Wie man hier sieht ist eines der großen Geheimnise, wie man die DataURL vom Canvas wieder in
ein Binär-Object zurück wandelt, so dass es wieder wie ein normal File-Upload gehandhabt werden kann.
Den Code haben ich nach langer Suche im Internet gefunden und er funktioniert!
function dataURItoBlob(dataURI) {
// convert base64 to raw binary data held in a string
var byteString = atob(dataURI.split(',')[1]);
// separate out the mime component
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
// write the bytes of the string to an ArrayBuffer
var arrayBuffer = new ArrayBuffer(byteString.length);
var _ia = new Uint8Array(arrayBuffer);
for (var i = 0; i < byteString.length; i++) {
_ia = byteString.charCodeAt(i);
}
var dataView = new DataView(arrayBuffer);
var blob = new Blob([dataView], { type: mimeString });
return blob;
}
Damit haben wir dann alles um es hochladen zu können. Wie man etwas hochlädt erkläre ich später vielleicht nochmal, aber da gibt es sonst genug Erklärungen auf anderen Seiten, die einfach und gut verständlich sind.
Aber hier kommt nochmal auf die schnelle der Code um ein Image aus einem Input des Types "file" heraus zu bekommen. Die Methode wird über das onchange-Event des Input aufgerufen.
$scope.openFile=function(event){
var files=event.target.files;
if(files.length>0 && (files[0].type.match('image.*') || files[0].type.match('image/*'))){
console.log("load file: "+files[0].name);
$scope.newPostFile=files[0];
$scope.newPostPreviewImage=document.createElement("img");
var URL = window.URL || window.webkitURL;
var imgURL = URL.createObjectURL($scope.newPostFile);
$scope.newPostPreviewImage.src=imgURL;
URL.revokeObjectURL(imgURL);
var reader = new FileReader();
reader.onload=function(e){
$scope.newPostPreviewURL=e.target.result;
console.log("add preview image");
try{
$scope.$apply();
}
catch(e){
}
};
reader.readAsDataURL($scope.newPostFile);
}
};
Hier wird die Datei in eine Object-URL umgewandelt und einmal ein img-Element erzeugt und diese Object-URL als src gesetzt. Zusätzlich wird nochmal eine DataURL von der Bild-Datei erzeugt, um ein kleines Vorschau-Bild anzeigen zu können. Die DataURL wird, wenn sie fertig
geladen ist, bei einem bestimmten img-Element als src gesetzt. Da wird über CSS skaliert. Alle Verkleinerungsoperationen werden aber auf dem internen separat gehaltenen img-Element ausgeführt.
<img ng-src=""/>