A következő címkéjű bejegyzések mutatása: javascript. Összes bejegyzés megjelenítése
A következő címkéjű bejegyzések mutatása: javascript. Összes bejegyzés megjelenítése

2015. december 23., szerda

avsort

Az elmúlt egy évemet, mióta minden tárgyamat abszolváltam a főiskolán, a szakdolgozatom szövegének megszülésével töltöttem. Ez töltötte ki a szabadidőm legnagyobb részét, még úgy is hogy csak szaros 80 oldal a teljes produktum.

Na de lássuk hogy miről is van szó:

Talán láttátok már a fenti videót, vagy esetleg ennek a lokalizált verzióját:

Az első videón Timo Bingmann sound-of-sorting nevű szoftvere látható működés közben. Ez a program mindenféle számítógépes rendező algoritmus működését mutatja be animációval, és azzal az édes 8-bites fütyüléssel. Az animáció során látható ahogy az algoritmus összehasonlít és megcserél értékeket, és a vizsgált értékek nagyságával arányos frekvenciájú hang is hallható. Az egész játék arról szól, hogy nem csak nézni, hallgatni is érdekes ahogy az értékek növekvő sorrendbe rendeződnek.


avsort

A szakdolgozatom témája ennek a szoftvernek egy javascriptes implementációjáról szól. Természetesen egyáltalán nem titkoltam hogy honnan ered az ötlet, de kifejezetten nem klónt akartam készíteni. A programom, az avsort, elég sok mindenben különbözik a sound-of-sortingtól, elsősorban renderelési lehetőségekben (a sound-of-sorting javára), de egy szerintem egész izgalmas funkcióval sikerült ellátnom, még hozzá hogy nem csak elolvasható a betöltött algoritmus forráskódja, de szerkeszthető is. Tehát a program limitációin belül bárki írhat magának saját algoritmusokat, és mutogathatja őket a csajoknak.

Mivel minden eszköz amit a dolgozat elkészítéséhez felhasználtam nyílt és szabad szoftver, ezért az avsort is egy nyílt alkalmazás, és ezzel együtt a dolgozat teljes szövegét is publikusan elérhető mostantól. (Persze a személyes információkat kigereblyéztem a pdfből)

Akit érdekel a program az a slapec.github.io/avsort címen kipróbálhatja. A projekt forráskódja elérhető githubon. A dolgozat szövegét pedig itt lehet elolvasni.

2015. február 8., vasárnap

Javascript függvény paraméterének kiolvasása PyV8-al

(Ez a bejegyzés egy kicsit módosítva lett 2015. február 9-én)

Rendbe, most hogy ilyen szépen elkészítettük a saját PyV8 példányunkat, ideje lenne kipróbálni valami kóddal, hogy működik-e rendesen.

A saját projektjeim közül hoztam a mostani témát: egy weboldalon egy Javascript Object értékét szeretném megkapni és átalakítani dict-é. A mostani történetben a csavar az, hogy ez az objektum egy függvény paramétere, ami ráadásul a jQuery ready() metódusába van belenyomogatva. Tehát nem holmi pi=3.14 változót kellene kipiszkálni.

(A projektről homályosan: Van egy weboldal, amin egy Google Maps-es térkép látható. Erre a térképre valamilyen saját library-vel markereket tesznek. Én egy adott, sokszög alapú területen levő markereket akarom kigyűjteni. A probléma megoldásának első részével foglalkozik a mai bejegyzés: a weboldal térképén látható összes koordináta összegyűjtésével. Egy későbbi részben majd kitaláljuk, hogy hogyan lehet kiválogatni azokat, amik egy területen belül vannak.)

Lássuk a kódot

Ebben a kis részletben csak a lényeg látszódik. Azzal hogy hogy halásztam ki a DOM-ból most nem foglalkozok (amúgy lxml-el):

$(document).ready(function(){
    new AwesomeLibrary.PutMarkers('#map', {
        data: [{id: '1', lat: '47.160971', lng: '16.4878386'}]
    });
});

Maga a forráskód nagyon egyértelmű, a PutMarkers() második paraméterének az értékére van szükség ({data:[{id: 1, lat: '47.160971', lng: '16.4878386'}]})

Megoldás

A jó multkor egyszerűbbnek találtam, ha először egy fake Javascript kódot eval()-olok a contextben, amivel módosítottam a később ténylegesen meghívott metódusok működését. Ahhoz hogy ez most is működjön kézzel deklarálnom kell a $, document (mivel itt nincs DOM), ready(), AwesomeLibrary, PutMarkers értékeit, hogy ne kapjak sehol se undefined is not a function errort. Így szépen végig fut az egész kóceráj, de a sajnos a PutMarkers visszatérési értékét az eredeti függvény nem teszi ki globális változónak, és maga az anonim függvény se tér vissza semmivel se (főleg nem a koordinátákkal). Jobb ötlet híján a saját PutMarkers implementációmban kiraktam globális változóba a kapott paramétereket, de aztán inkább úgy döntöttem, hogy kipróbálok valami mást, és minden hiányzó függvényt egy Python osztályból rakosgatok a contextbe.

class Globals(PyV8.JSClass):
    def __init__(self):
        super(Globals, self).__init__()
        self.coords = []

    def PutMarkers(self, selector, coords):
        coords = PyV8.convert(coords)

        for coord in coords['data']:
            self.coords.append({
                'lat': float(coord['lat']),
                'lng': float(coord['lng']),
                'id': int(coord['id'])
            })

    def ready(self, function):
        function()

    def __call__(self, *args, **kwargs):
        return self

    def __getattr__(self, *args):
        return self

Az osztály legfontosabb metódusa a __getattr__(). Ez minden olyan esetben meghívódik, ha a Python nem találja az objektum egy property-jét. Mivel ez a metódus a globális névtérbe került, ezért minden olyan esetbe, amikor a V8 a globális névtérből keres egy változót, ez a Python metódus fog lefutni. Ide kerül minden olyan logika ami azokat az eseteket kezeli le, amikor valami deklarálatlan értéket keres a V8, de egyáltalán nem fontos, hogy mi annak az értéke. Másik fontos metódus a __call__(). Ennek segítségével a Globals példányunk függvényekhez hasonlóan tud viselkedni (meg lehet hívni).

A két magic method kombinációjával meg lehet oldani, hogy a számunkra nem érdekes változókat és metódusokat ne kelljen kézzel deklarálgatni. Amikor a V8 egy property-t keres, akkor szépen megkapja a objektumunkat a __getattr__()-ből. Ha abban a property-ben még egy propertyt keres, akkor megint csak megkapja az objektumot, de ha a proprty értékét callable-nek tekinti az is működni fog, mivel az objektum függvényként is tud viselkedni.

Magyarul tehát a Javascript kódban bármikor is egy deklarálatlan változót talál a V8, akkor azt mindig a Globals objektum attribútumai között keresi, és bármikor is talál egy deklarálatlan függvényt, meg fogja tudni hívni, mert a Globals példánya callable. Gyakorlatilag ez egy lánc (method chaining), így mindig minden keresett érték deklarált lesz. (Nem kerülünk rekurzióba, mivel a property-k kibontogatása véges).

Ha ez megvan, akkor jöhetnek az “érdekes metódusok”, szóval azok, amik ténylegesen végeznek valami munkát. A Javascript forráskódban ezek a ready() és PutMarkers().

A $ mindegy hogy mit csinál, csak függvény legyen, és valami olyan objektummal térjen vissza, amiben létezik a ready() metódus. A __getattr__() visszatér egy objektummal, ami függvényként viselkedik, és függvény által visszakapott objektumban létezik a ready() metódus (lásd kicsit lejjebb).
A document, AwesomeLibrary értékei hasonlóan. Egy csomót spóroltunk!

A ready() metódus a Globals osztályban magkapja az anonim függvény, egy JSFunction Python objektumként. Ezt ugyan úgy meg lehet hívni mint bármelyik sima függvényt, de természetesen a kódot a V8 futtatja, a kontextusban.

A történet vége a PutMarkers() metódus. Ez 2 paramétert vár, egy css selectort, amivel nem csinálunk semmit, és magát az értékes adatot, szóval a koordinátákat. Innen már nagyon egyszerű a dolog, a PyV8.convert() átalakítja dict-é az Javascript Object-et (így kikerül a context-ből az adat), majd egy sima iterációval bepakolgatjuk az összes koordinátát a self.coords tömbbe.

script_content = """
$(document).ready(function(){
    new AwesomeLibrary.PutMarkers('#map', {
        data: [{id: '1', lat: '47.160971', lng: '16.4878386'}]
    });
});"""

context_globals = Globals()
with PyV8.JSContext(context_globals) as ctx:
    ctx.eval(script_content)

print(context_globals.coords)

Példányosítjuk a Globals osztályt, majd nyitunk egy JSContext-et. Paraméternek átadjuk a csinos objektumunkat. A contextben pedig futtatjuk a Javascript kódot (script_content).

A kontextuson kívül a context_globals.coords propertyben már ott is vannak a koordináták

>>> print(context_globals.coords)
[{'lat': 47.160971, 'id': 1, 'lng': 16.4878386}]

Zárás

Azt hiszem jól látszódik, hogy kivállóan együtt tudnak működni a Python-os osztályok és a V8. Lényegesen elegánsabb ez a megoldás, mint kézzel kikapuzni egy másik Javascript fájlban az összes előforduló de haszontalan változót és aztán betölteni azt a context elején.

A teljes kód megtekinthető ezen a Gist-en.

A következő részben kitaláljuk, hogy hogy lehet kiválogatni az egy területen levő koordinátákat.

-slp

2013. december 26., csütörtök

Javascript futtatása Python kódból, PyV8 fordítása

Kellemes Ünnepeket mindenkinek!
Természetesen azért, mert megint nem írtam már hetek hónapok óta, még nem állt meg az élet. Mostanában web scraping-el foglalkozok, ami nagyon komplex egy feladat, persze csak ha az ember normálisan akarja megcsinálni.

Egy olyan problémába ütköztem, ami manapság egyre hétköznapibb, hogy a weboldal nagy részét Javascript kódból építik fel. Használhatnék amúgy erre egy valódi böngészőt is, ami futtatja a kapott kódot, és Python alól birizgálom, pl Selenium-al, de nekem csak a Javascript kódban tárolt adatokra van szükségem. Párperces keresgélés után nem találtam normális parsert a nyelvhez, így aztán úgy döntöttem, hogy felrakom a Google V8 engine-jét, a hozzá tartozó Python-os bindinggel, a PyV8-al, betápolom neki a kódot, és dolgozok a visszakapott értékekkel.

Szerencsére a kis Odroid-X2-n szépen -lassan- lefordul maga az engine, és a binding is. A dokumentációban nem nagyon van részletes leírás a fordítás menetéről, így ezt magamnak kellett kitalálnom. Szerencsére apt-ből mindent össze lehet szedni. Lássuk a parancsokat!
sudo apt-get install libboost-python-dev libboost-thread-dev libboost-system-dev
Ennyi az összes függőség a fordításhoz. Ha nincs svn kliens feltelepítve, akkor erre még szükség lehet:
sudo apt-get install subversion
Ezek után egyszerűen pip-el telepítjük
sudo pip install pyv8

Rövid példácska

Rendbe, ha már ilyen szépen feltelepült a lib, akkor azt is leírom hogy mire kellett.
Tehát, a feldolgozandó kódban, egyes HTML tag-ek onmouseover eventjén van az alábbihoz hasonló kód:
jQuery("#1337").html("Leet");
Azaz rámutatáskor ad 1337 id-ű tag tartalma legyen a Leet string. Ilyen egyszerű.

Nem akartam az egész jQuery-t betölteni a memóriába, ezért inkább írtam egy kis kamu függvényt, amivel a fenti sor futtatható. Így néz ki:
var jQuery = function (unused){
    html = function(data){
        return data
    }
    return this;
}
A jQuery függvény visszatér a this-el, ami úgy mellékesen a window. Maga a függvény nem csinál semmit se a paraméterével. A függvény törzsében definiálunk egy html nevű függvényt. Ez globális lesz, így a window.html-en keresztül is elérhető.
Tehát amikor a jQuery visszatér a this-el, ott lesz már a window.html is, így működni fog method chaining (igazából nem fog működni, ahhoz megint csak this-el kellett volna visszatérni, de nekem most így is megfelel a kód).

Rakjuk össze az egészet és próbáljuk ki:

import PyV8
ctx = PyV8.JSContext()
ctx.enter()
ctx.eval('var jQuery=function(unused){html=function(data){return data};return this;}')
ctx.eval("jQuery('#1337').html('Leet')")
#>>>'Leet'

Először létrehozunk egy új contextet és bele is lépünk. Ez után betöltjük a függvényt, aztán kipróbáljuk hogy visszakapjuk-e a html-nek adott stringet, természetesen a Python interpreterben.
Az utolsó sorban látszik hogy ez megtörtént, úgyhogy nagy az öröm!

-slp

2013. március 3., vasárnap

Gamma korrekció Javascript canvason

TL;DR Live demo elérhető itt: http://jsfiddle.net/slapec/m8RN9/6/
Jó, rögtön a 7. bejegyzésem (ezen a blogon) egyből nem a pythonnal foglalkozik.

A korábbi anaglif kép előállító scriptem írása közben vettem csak észre, hogy a weboldalon ahol különböző módszereket felsorolják, az optimized anaglyph eljárásnál a kész kép piros színcsatornájára gamma korrekciót alkalmaznak. Ekkor derült csak ki, hogy a PIL-ben nincs erre dedikált metódus, szóval magamnak kellett leprogramozni. Ezzel véget is érhetett volna a móka, de valójából a pythonos kódom egy javascriptes projektből származik, amit akkor mondjuk nem mondtam, de mivel kiderült hogy a javascript canvas se ismeri ezt a funkciót, ezért most felfedhettem az eredeti szándékomat.

Jó pár órával nappal ezelőtt ültem le megírni a kódot amit itt most be szeretnék mutatni. Ez a sok idő elég volt arra, hogy kicsit elkanyarodjak a probléma "magjától", szóval 2 tonna csicsa került a gyakorlatilag 3 sornyi hasznos kód köré, de nem baj, imádom a csicsát :3. Lássuk hogy mit akartam:
Képszerkesztő programokban szokott lenni külön ablak, ahol gamma korrekciót lehet állítani, csinos kis csúszkákkal, és még a függvény képe is látszik 0 ≤ x ≤ 1 között, illetve azonnal lehet látni a végeredményt is.

Arról hogy mi a gamma korrekció az első bekezdésben levő linkben lehet olvasni. Az ottani képleten egy kicsit kell kalapálni, és ezt kapjuk a végén:

Image

Arról van szó, hogy a kapott értéket (Vin) le kell osztani 255-el, hogy 0 ... 1 közé essen. A kapott értéket hatványozzuk a gammával, ami nálam valójából a gamma reciproka, de csak azért, hogy a gamma < 1 legyen általában (ez a gamma compression) majd a hatványozott értéket vissza szorozzuk 255-el, hogy újra 0 ... 255 közé essen, hogy aztán vissza lehessen rakni a helyére.

A HTML

Annyira nem túl izgalmas, de mivel van 1-2 HTML5-ös element a kódomban, ezért akkor kicsit részletezném azt is. Ide nem másoltam be a teljes kódot, azt meg lehet nézni a bejegyzés legelső sorában levő linkjén, meg majd alul is!
<canvas id='canvas' width='512' height='512'></canvas>
<div id='controlls' style='display: inline-block'>
    <form id='gammaForm'>
        <label for='red'>Piros</label>
        <input type='range' value='1.0' min='0.2' max='5.0' step='0.1'
        id='red' name='red'></input>
        <output for='red' name='red_out'>1</output>
        <br>
        <label for='green'>Zöld</label>
        <input type='range' value='1.0' min='0.2' max='5.0' step='0.1'
        id='green' name='green'></input>
        <output for='green' name='green_out'>1</output>
        <br>
        <label for='blue'>Kék</label>
        <input type='range' value='1.0' min='0.2' max='5.0' step='0.1'
        id='blue' name='blue'></input>
        <output for='blue' name='blue_out'>1</output>
    </form>
    <br>
    <canvas id='plot' width='150' height='150'></canvas>
</div>
<img style='display: none;' id='lena' src='data:image/png;base64,'>

Az 1. sorban létrehozunk egy 512x512-es canvast.
A 2. sorban egy nagy div lesz, ebbe lesznek a kezelő szervek, pontosabban a divben levő formban. Itt a csúszkák melletti szöveg label elemek közé vannak téve, ahogy illik.
Az 5. sorban egy range típusú inputot definiáltam, amit amúgy a Firefox még nem támogat, szóval ott nem fog látszódni.
Ez egy vízszintes csúszka lesz. Az alap értéke 1.0, 0.2-ig mehet le, 5.0-ig mehet fel 0.1-es lépésekkel. Az id-je és a name-je megegyezik, mert a label-ben a for-hoz name-et kell írni... legalábbis azt hiszem ezért csináltam így.
A 7. sorban egy output element van, ebbe lesz majd beleírni a csúszka aktuális értéke. A label és az output element amúgy a weboldalon 1-1 sima szövegként fog megjelenni igazából, a különbség majd a JS oldalán lesz, illetve a böngésző pontosan tudja majd hogy mik a szándékaink.
A 23. sorig bezárólag copypasta van, de ott megjelenik még egy 150x150-es canvas, amibe majd a gamma függvény képét rajzoljuk csatornánként.
A 25. sorba egy img van, ide kerül a kép, az id-je 'lena'.
Én a src attribútumba a képet inline módon, data URI-val bemásoltam. Így a HTML fájl vagy 30kb lett, de így nem akadok bele a JS same origin policy-jába, meg a példának megteszi ez is.
Ez a kép CSS-el el lett rejtve, mert igazából nincs rá szükség, szerintem. Összehasonlítás miatt mondjuk egymás mellé lehetne tenni, de csak ennyi ötletem van.

A Javascript

Végre elérkeztem az izgalmas részhez. Először szedjük szét a feladatot részekre:

  1. Kell egy függvény ami ami benyeli a csatornánkénti (R, G, B) gammát, és módosítja a képet
  2. Kell egy függvény ami megrajzolja a gamma függvény képét
  3. Kell egy függvény ami frissíti az output elemet
(Amúgy ami a Javascriptes programozási stílusomat illeti, ez így a C-s és Pythonos élményeim egyvelege, valódi JS szakértelmet csak nyomokban tartalmaz. Na ettől még annyira nem katasztrófa, csak nem olyan elegáns)

Kép módosítása

El se hiszem, végre itt a bejegyzésem magja. Ne is várakozzunk tovább, itt a kód:
function gammaApply(gR, gG, gB) {
    var canvas, ctx, lena;
    var i, len, px, imgArray, imgData;

    var lut_r = [],
        lut_g = [],
        lut_b = [];

    lena = document.getElementById('lena');

    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');
    ctx.drawImage(lena, 0, 0);

    imgData = ctx.getImageData(0, 0, 512, 512);
    imgArray = imgData.data;
    len = imgData.width * imgData.height * 4;

    for (i = 0; i < 256; i++) {
        lut_r[i] = gammaCalc(i, gR);
        lut_g[i] = gammaCalc(i, gG);
        lut_b[i] = gammaCalc(i, gB);
    }

    for (i = 0; i < len; i += 4) {
        imgArray[i] = lut_r[imgArray[i]];
        imgArray[i + 1] = lut_g[imgArray[i + 1]];
        imgArray[i + 2] = lut_b[imgArray[i + 2]];
    }

    ctx.putImageData(imgData, 0, 0);
}

Hívjuk ezt a függvényt a továbbiakban gammaApply()-nak.
A 2-7. sorban deklarálok pár változót.

  • A canvas nevű mutat majd a weboldal 'canvas' nevű canvasára (ej, de szar neveket adtam, most jövök rá). 
  • A ctx-ben a contextünk lesz ezen a canvason. 
  • A lena-ba a kép lesz majd, ami a HTML-be kézzel bele lett írva.
  • Az i majd a ciklusváltozó lesz
  • A len-be a kapott kép mérete lesz
  • Az imgArray mutat majd a canvasról kihalászott kép tömbjére
  • Az imgData-ba lesz majd a canvasról kihalászott kép maga
Az 5. sorban 3 db lookup table-t is deklaráltam, minden csatornának egyet-egyet. Erre azért lesz szükség, mert nagyon sokat tud gyorsítani majd a korrekció alkalmazásánál.

A 9. sorban a lena változót ráállítjuk a képre.
A 11. sorban a canvas változót ráállítjuk a canvasra amire majd a korrigált képet rajzoljuk.
A 12. és 13. sorban először kérünk egy 2D contextet, majd rárajzoljuk a lena változóban levő képet a canvasra.
Erre a lépésre azért volt szükség, mert JS-el csak így tudjuk majd kikérni a kép bináris adatát. Arra, hogy az adatot kikérjük egy átmeneti canvast is felhasználhatnánk, de az csak plusz idő és memória. Most tehát rárajzoljuk a képet a canvasra, kikérjük az adatot, azt átírjuk majd visszaírjuk az adatot a canvasra és BUM kész is.
A 15. sorban kikérjük tehát a kép adatát, ez az imgData változóban lesz.
A 16. sorban ráállítjuk az imgArray változót az imgData .data propertijére, így egy kicsivel gyorsabb lesz az adat elérése.
A 17. sorban kiszámoljuk hogy mekkora lesz az a tömb amiben az RGB értékek benne vannak, tehát az imgArray mérete. Ez amúgy benne van az imgArray-ben, viszont így hogy ki van írva el tudom magyarázni, hogy hogy jön ki a méret.
A szélességet szorozzuk a magassággal, ez idáig rendbe van, megkapjuk hány pixel a kép összesen. Egy pixelt viszont JS-ben 4 érték ír le, a pixel R G B és A csatornájának értékei (az A az alpha), ezért kerül a 4-el szorzás a sor végére.
A 19. sorban jön az első ciklus, ez viszont még csak a LUT-ot számolja ki. A gondolat a következő:
Az 512x512-es kép mind a három színcsatornájára alkalmazunk a korrekciót, az 786,432 függvényhívás minimum, ami sok, de minden 0 ... 255 értékhez ugyan az a korrigált érték fog tartozni, tehát sokkal gyorsabb kiszámolni a 3 csatornára előre az összes korrigált értéket, hisz az csak 3 * 255 = 765 függvényhívás, és utána kikeresni a tömbből hogy majd melyik pixelhez milyen korrigált érték társul. A tömbből a kikeresés se lesz nehéz: A 0-hoz tartozó érték a tömb 0. indexén van, az 1 az 1-en ... a 255 a 255-ön.
A ciklusban elszámolunk 0-tól 255-ig, meglesz minden lehetséges érték amit egy színcsatorna felvehet. A gammaCalc() függvény megkapja a pixel értékét és a gammát és visszaadja a korrigált értéket. Ebben a ciklusban most az i változó reprezentálja a pixelt magát. Aki nem értené:
Van 1 pixelünk, ami mondjuk RGB(76, 149, 29) színű. A gammaCalc() megkapja a 76-ot és a gammát, az legyen most 1.5, kihányja hogy 113, azaz ennyi a korrigált értéke. A LUT-ban a lut_r 76. indexén a 113-as érték fog majd lenni, azaz elég lesz majd a pixel R csatornájának értékével megindexelni a lut_r-t, és készen is vagyunk.
A 25. sorban végigugrálunk négyesével a kép tömbjén, az imgArray-en. 
Így az imgArray[i] az R lesz, az i+1 a G, i+2 a B i+3 az A. A cikluson belül, ahogy feljebb írtam, megindexeljük a csatornák LUTjait a csatornák értékeivel, és amit kapunk azt állítjuk be a csatorna új értékének, szóval ez a korrigált érték kikeresése.
A 31. sorban visszarakjuk a canvas-ra az imageData objektumot, amit az előbb módosítottunk az imgArray-en keresztül, hiszen az imgArray az imageData.data-jára mutatott.

Ezzel készen is van a függvény, a lényeg készen van.

Korrekció

function gammaCalc(px, g) {
    return parseInt(Math.pow(px / 255, 1 / g) * 255, 10);
}

Ez az egyszerű kis függvény számolja ki a korrigált értékeket. A bejegyzés elején levő képlet javascriptes verziója. Annyi megjegyzés csak, hogy az egész kifejezés egy parseInt() függvénybe van téve, ami int-é castolja a korrekció eredményét. A biztonság kedvéért kapott ez a függvény egy második paramétert, ami azt jelzi, hogy milyen számrendszerű az első paramétere. Most így belegondolva, ennek csak akkor lenne haszna, ha 0-val kezdődő számokat kéne feldolgozni (az jelzi a 8-as számrendszert), de ilyen itt nem fordulhat elő. Sebaj, ez már így marad.

Függvény rajzolás

A függvény kirajzolásának nagy izgalommal estem neki, aztán végül is egy kis buta egyszerű algoritmus lett a vége. gammaPlot()-nak neveztem el, és a rajzolás maga eléggé egyszerű benne:
Vesz egy canvast, elindul vízszintesen pixelenként, és az épp aktuális pozíciójának az x koordinátáját betolja a korrekciót számoló képletbe, megkapjuk az y koordinátát, szóval mint egy sima kis függvény az általános iskolából.
function gammaPlot(r, g, b) {
    var canvas, ctx;
    var w, h, i;

    canvas = document.getElementById('plot');
    w = canvas.width;
    h = canvas.height;

    ctx = canvas.getContext('2d');

    ctx.fillStyle = '#DBDBDB';
    ctx.lineWidth = 2;
    ctx.fillRect(0, 0, w, h);

    //red
    ctx.beginPath();
    ctx.strokeStyle = '#F00';
    ctx.moveTo(0, h);
    for (i = 0; i < w; i++) {
        ctx.lineTo(i, w - (Math.pow(i / w, 1 / r) * w));
    }
    ctx.stroke();
    ctx.closePath();

    //green
    ctx.beginPath();
    ctx.strokeStyle = '#0F0';
    ctx.moveTo(0, h);
    for (i = 0; i < w; i++) {
        ctx.lineTo(i, w - (Math.pow(i / w, 1 / g) * w));
    }
    ctx.stroke();
    ctx.closePath();

    //blue
    ctx.beginPath();
    ctx.strokeStyle = '#00F';
    ctx.moveTo(0, h);
    for (i = 0; i < w; i++) {
        ctx.lineTo(i, w - (Math.pow(i / w, 1 / b) * w));
    }
    ctx.stroke();
    ctx.closePath();
}
A 2. és 3. sorban deklarálok megint pár változót, a canvasnak, a contextnek, szélességnek/magasságnak meg ciklusváltozónak.
5. sorban rámutat a kód a 'plot' canvasra
A 6. & 7. sorban lekérjük a canvas méreteit, szóval így dinamikus lesz kicsit a kód. A 9. sorban beállítjuk még a contextet.
A 11. sorban beállítom az aktuális kitöltő színt és a vonalvastagságot, majd az egész canvasra húzok egy négyzetet, így lesz egy kis háttérszín is.
A 16. sorban kezdődik az érdemi munka. A beginPath()-al kezdi rögzíteni a JS hogy majd milyen útvonalon húzza végig a tollat.
A 17. sorban beállítom a toll színét, ez most #F00
A 18. sorban lekerül a toll a bal-alsó sarokba
A 19. sorban történik a kirajzolás.
Elindul egy ciklus, ami végigszámol egyesével a vízszintes tengely mentén. A cikluson belül a lineTo()-ba állítjuk be hogy hova menjen majd a toll. Az első paramétere maga a ciklusváltozó, azaz vízszintesen mindig 1-el megy majd odébb a toll, ez az X tengely. A függőleges irányt a bejegyzés elején levő képlet alapján számoljuk, annyi a különbség, hogy nem 255-el van leosztva az i, hanem a szélességgel, hisz a lényeg az, hogy a tört 0 ... 1 közé essen. Jó, 255-el osztásnál is oda esne ha a canvas szélessége <256, de így éppen pontosan keresztbe teljesen végig ér a vonal.
A 22. sorban kirajzolódik a vonal, és a 23. sorban lezárjuk a toll mozgásának rögzítését.
Ezek után pontosan ugyan ez a kód van még kétszer lemásolva, a különbség egyedül az, hogy a különböző csatornák számolásánál más-más gammákkal fut a kirajzolás. Ezeket a gammákat a függvény paraméterül kapta, de ez látszik.

Most hogy elkészült a képet módosító függvény, és a függvényt kirajzoló függvény, már csak össze kell fogni a dolgokat.

Csúszkák kezelése

Az alábbi függvény mindig meghívódik, amikor valami változik a weboldal formjában:
function updateOutputs(form) {
    var gR = form.red.valueAsNumber,
        gG = form.green.valueAsNumber,
        gB = form.blue.valueAsNumber;

    form.red_out.value = gR;
    form.green_out.value = gG;
    form.blue_out.value = gB;

    return {
        'red': gR,
        'green': gG,
        'blue': gB
    };
}
A függvény megkapja a formot, így ebbe nem lesz getElementById..

A 2-4. sorban kiszedegetjük a csúszkák értékeit. Szerencsére van olyan properyjük, amivel azonnal számként kapjuk az értékeiket, szóval nem kell nekem castolni.
A 6-8. sorban az előbb kiszedett értékeket beállítom az outputoknak, így látszódik majd hogy melyik csúszka hogy áll
A függvény a végén visszatér a beállított értékekkel, azaz a gammákkal. Arra gondoltam hogy ha már ez a függvény úgyis mindig meghívódik, és tudja hogy ki és mit állított be, akkor vissza is adhatná, és nem kell más függvénynek magának összeszednie.

window.onload

Már csak annyi van hátra, hogy megírjuk hogy mi kerül a window.onload-ba, azaz mi fusson ha betöltődött teljesen a weboldal.
gammaApply(1, 1, 1);
gammaPlot(1, 1, 1);

form = document.getElementById('gammaForm');
form.oninput = function () {
    var g = updateOutputs(this);
    gammaApply(g.red, g.green, g.blue);
    gammaPlot(g.red, g.green, g.blue);
};

Kézzel 1,1,1-es gammákkal meghíjuk a gammaAply-t és plot-ot, így megjeleni a kép, és a függvény is kirajzolódik.
A 4. sorban kikérjük a DOM-ból a gammaForm nevű elementet, szóval a formot, na.
Az 5. sorban a form oninput eventjére beállítunk egy függvényt. Az oninput mindig lefut majd, amikor valami változik a form területén.
A 6. sorban meghívjuk az updateOutputs()-ot, ami ugye átírja majd az output elementeket és vissza is adja hogy miket állított be.
A 7. sorban a kapott gammákkal meghívjuk a gammaApply-t ami kirajzolja a képet, és a 8. sorban a gammaPlot pedig kirajzolja a függvény képét

Végeredmény

Itt látható mindenestől: http://jsfiddle.net/slapec/m8RN9/6/
Akit nem érdekel a kód: http://jsfiddle.net/slapec/m8RN9/embedded/result/

Zárás

Te jó ég! Szerintem kicsit túl sokat írok, az biztos. Ennyi elég is volt egy időre :)

-slp