├── .gitignore
├── README.md
├── docs
├── earring.jpg
├── examples
│ ├── boxblur.png
│ ├── greyscale.png
│ ├── histogramequalization.png
│ ├── invert.png
│ ├── medianfilter.png
│ ├── seamcarvedynamicprogramming.png
│ ├── seamcarving.png
│ ├── sharpen.png
│ ├── skformula.png
│ ├── threshold.png
│ └── warmfilter.png
├── index.html
├── index.js
├── low-contrast.png
├── noisy.png
├── simple.png
├── styles.css
├── supernoisy.jpg
└── tower.jpg
├── images
├── earring.jpg
├── low-contrast.png
├── noisy.png
├── simple.png
├── supernoisy.jpg
└── tower.jpg
├── package-lock.json
├── package.json
└── src
├── Editor.js
├── Image.js
├── effects
├── boxblur.js
├── greyscale.js
├── histogramequalization.js
├── invert.js
├── medianfilter.js
├── seamcarving.js
├── sharpen.js
├── threshold.js
└── warmfilter.js
├── index.html
├── index.js
└── styles.css
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | *.iml
3 | node_modules/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Image workshop
2 |
3 | Velkommen til AlgPip's konkurrent til Photoshop!
4 |
5 | I denne workshopen skal vi implementere forskjellige bildebehandlingsalgoritmer.
6 |
7 |
8 | # Oppsett
9 |
10 | ```
11 | npm install
12 | npm run dev
13 | ```
14 |
15 | Åpne http://localhost:9966/ i nettleseren. Når du gjør endringer i koden vil browseren
16 | refreshe.
17 |
18 | ## Koden
19 |
20 | I `src/effects/`-mappa ligger filene til de forskjellige effektene. En effekt får typisk inn bildet det skal jobbes på, og eventuelt noen parametre.
21 | Så itererer man over pikslene i bildet og lager et nytt bilde som man returnerer.
22 |
23 | APIet er ikke så stort, det består bare av funksjoner for å lese eller skrive verdier til et bilde.
24 |
25 | ```javascript
26 | // Lage et nytt bilde:
27 | var newImage = Image.empty(width, height); // Lager et blankt bilde med en viss størrelse
28 | var newImage2 = Image.clone(image); // Kopi av et annet bilde
29 |
30 | // Lese ut størrelser
31 | var width = image.width;
32 | var height = image.height;
33 |
34 | // Lese ut verdier
35 | // venstre topp er (0, 0). y=0 er altså øverste rad, y=1 neste rad osv.
36 | var red = image.getR(x, y); // Få rødkomponenten av fargen på piksel x,y
37 | var green = image.getG(x, y);
38 | var blue = image.getB(x, y);
39 |
40 | // Setter verdier/farger tilsvarende
41 | // Fra og med 0 til og med 255
42 | newImage.setR(x, y, 255);
43 | newImage.setG(x, y, 0);
44 | newImage.setB(x, y, 128);
45 |
46 | // Kan evt sette alle i ett
47 | newImage.setRGB(x, y, [255, 0, 128]);
48 | ```
49 |
50 | En typisk effekt ser ut som noe ala det her:
51 |
52 | ```javascript
53 | function effect(image) {
54 | // Lag et nytt bilde av samme størrelse
55 | const newImage = Image.empty(image.width, image.height);
56 |
57 | // Iterer over hver pixel, rad for rad (merk vi itererer over y ytterst)
58 | for (let y = 0; y < image.height; y++) {
59 | for (let x = 0; x < image.width; x++) {
60 |
61 | // Les ut aktuelle verdier fra originalbildet
62 | var r = image.getR(x, y);
63 | // ...
64 |
65 | // Regn ut nye verdier
66 | var nyR = r + 25;
67 | // ...
68 |
69 | // Sett verdi i det nye bildet
70 | newImage.setR(x, y, [nyR, nyG, nyB]);
71 | }
72 | }
73 | return newImage;
74 | }
75 |
76 |
77 | ```
78 |
79 | # Oppgaver
80 |
81 | Anbefaler at man starter med Greyscale for å få en følelse av hvordan ting fungerer,
82 | deretter er det bare å plukke det man synes virker mest spennende! Merk at vanskelighetsgraden
83 | varierer mellom de forskjellige.
84 |
85 | Kan se fasit (/få hjelp) ved å sjekke ut branchen `solution`, evt klikke [her](https://github.com/Matsemann/image-workshop/tree/solution/src/effects)
86 |
87 | [Greyscale](#Greyscale) (liten)
88 | [Threshold](#Threshold) (liten)
89 | [Invert](#Invert) (liten)
90 | [Warmfilter](#Warmfilter) (liten)
91 | [Blur](#Boxblur) (middels)
92 | [Median filter](#Median-filter) (middels)
93 | [Sharpen](#Sharpen) (liten, men avhengig av èn av de to foregående)
94 | [Histogram equalization](#Histogram-equalization) (middels)
95 | [Seam carving](#Seam-carving) (stor)
96 |
97 |
98 | ## Greyscale
99 |
100 | 
101 |
102 | Ofte kalt svart/hvitt, men vi har jo mange "shades of grey". For hver piksel, regner man ut "intensiteten"
103 | den har, basert på R,G,B verdiene. Det letteste er bare å ta snittet av RGB-verdiene på en piksel i originalbildet og sette
104 | pikselen i det nye bildet til den verdien. Men for å et greyscale som bedre tilsvarer det et menneskeøye opplever intensiteten er,
105 | kan man heller bruke formelen `intensity = 0.34 * r + 0.5 * g + 0.16 * b`.
106 |
107 | Endre på fila `src/effects/greyscale.js`. Gjør tilsvarende som i eksempelet over:
108 | Iterer over alle pikslene i bildet. Les ut verdiene fra originalbildet og beregn intensitetsverdien/gråverdien
109 | pikselen skal ha og sett den i det nye bildet. For å sette verdien som grå, setter man RGB til samme verdi. F. eks. om man
110 | regnet ut at intensiteten skal være 203, setter man både R, G og B til 203.
111 |
112 |
113 | ## Threshold
114 |
115 | 
116 |
117 | Den egentlige "svart/hvitt". Ligner veldig på greyscale, men i stedet for å sette RGB til intensiteten,
118 | setter man enten fargen til svart (0,0,0) eller hvitt (255,255,255) basert på om intensiteten er over en viss grense/threshold.
119 |
120 | Endre på fila `src/effects/threshold.js`. I tillegg til bildet effekten skal legges på, får du inn grenseverdien for om hver piksel skal bli hvit eller svart.
121 |
122 | ## Invert
123 |
124 | 
125 |
126 | Rett og slett invertering av fargene, 255 - c, der c er intensiteten per pixel per farge. Brukes hyppig i flashes i skrekkfilmer.
127 |
128 | Endre på fila `src/effects/invert.js`.
129 |
130 | ## Warmfilter
131 |
132 | 
133 |
134 | Gjør bildet varmere ved å legge til litt rød og trekke fra litt blå. Endre på fila `src/effects/warmfilter.js`.
135 | Lag et nytt blankt bilde på størrelse med bildet du får inn, og iterer over alle pikslene. For hver piksel,
136 | les ut RGB verdiene fra bildet du fikk inn. Men i stedet for å kopiere verdiene direkte over til det nye bildet, legger du til litt (f eks +25) på rødverdien,
137 | og trekker til litt på blåverdien (feks -25).
138 |
139 | ## Boxblur
140 |
141 | 
142 |
143 | Dette er gode gamle blur som mange har minner fra i photoshop. Det finnes mange måter å implementere blur på, men vi har
144 | valgt det aller enkleste tilfellet, nemlig å sette hver pixel til å være gjennomsnittet av pixlene rundt. Vi sier ofte
145 | at vi beregner verdien basert på et *omegn* av hver verdi. Denne måten å behandle bilder på kalles *spatial filtering*,
146 | og box blur er et såkalt *lavpassfilter**. I vår implemetasjon sender man med hvor stor radius rundt hver pixel som skal
147 | være beregningsgrunnlaget. Dvs. at sender vi med 1, så er omegnet et 3x3-grid, sender vi med 2, er omegnet et 5x5-grid.
148 |
149 | Endre på fila `src/effects/boxblur.js`. Iterer over alle pikslene, og for hver piksel må du finne pikslene i en radius rundt og summere opp RGB-verdiene for å regne ut snittet for hver farge.
150 | Sett deretter fargen på pikselen til å være disse snittene av RGB. For å iterere over alle naboene, kan du bruke en dobbel for-løkke, der man går `(fra x-radius til og med x+radius)` og tilsvarende for y.
151 |
152 | ## Median filter
153 |
154 | 
155 |
156 | Medianfilteret er en veldig god støyreduserer, og har veldig lik implementasjon som blur, bare at man setter hver pixel
157 | til å være medianen av omegnet i stedet for gjennomsnittet.
158 |
159 | Endre på fila `src/effects/medianfilter.js`. Gjør som for boxblur, men i stedet for å finne snittet putter du alle verdiene i en liste, sorterer den og plukker ut den
160 | midterste verdien og bruker den som verdi på pikselen du ser på.
161 |
162 | ## Sharpen
163 |
164 | 
165 |
166 | Krever at man har implementert enten blur eller medianfilter (eller et annet lavpassfilter!).
167 |
168 | En enkel sharpen-funksjon er å ta differansen mellom et bilde og den lavpassfiltrerte versjonen av bildet, for så å
169 | legge til differansen på orignalbildet igjen.
170 |
171 | `diff = originalbilde - lavpass(originalbilde)`,
172 | `sharpened_originalbilde = orignalbilde + diff`.
173 |
174 | Hvorfor fungerer dette? Når vi trekker et utjevnet bilde fra originalen, står vi igjen med høye verdier i de pixlene som
175 | har blitt utjevnet mye, altså de pixlene som skiller seg ut fra nabopixlene. Når vi så legger dette på originalbildet igjen,
176 | vil de homogene områdene forbli de samme (fordi differansen er ~0 der), mens vi får utslag der det er kanter e.l.
177 |
178 | Endre på fila `src/effects/sharpen.js`.
179 |
180 | ## Histogram equalization
181 |
182 | 
183 |
184 | Histogram equalization dreier seg om å utjevne spekteret av intensitetsverdier man bruker i et bilde. Enklere forklart,
185 | dersom man har et bilde som er lite kontrastfylt, f. eks et veldig lyst bilde, så gir man bildet mer kontrast.
186 |
187 | Det som skjer er at vi for hver pixel og farge gjør en mapping til en ny verdi. Lang historie kort, formelen er på
188 | følgende format. Hver intensitetsverdi `k` skal mappes til verdien `ny_k` (0 til 255). `ny_k` er på
189 | formen (se bildet):
190 |
191 | 
192 |
193 | Der `L-1 = 255`, `MN = image.height*image.width` og `n_j`er antallet pixler i bildet som har intensitetsverdi `j` (mellom 0 og `k`).
194 |
195 | Endre på fila `src/effects/histogramequalization.js`.
196 |
197 | **Eksempel**
198 |
199 | La oss si vi har et bilde som er `3x3`, der rødverdiene er `5, 4, 4, 5, 3, 1, 0, 4, 1`.
200 | Vi teller antall av hver verdi `k` og får linjen `nr` i tabellen under.
201 | Deretter summerer vi hvor mange verdier som er til og med hver `k`. F. eks. er `sr[4] = 7`, fordi det er summen av `nr[0]+nr[1]+nr[2]+nr[3]+nr[4]`.
202 | Så regner vi `ny_kr` for hver `k`, altså verdien den skal mappes til. For bårt 3x3 bilde blir det da `(255 / (3*3)) * sr[k]`, rundet til hele tall.
203 | Når det er gjort itererer vi over alle pikslene i vårt originale bilde, og bytter ut verdiene i henhold til tabellen. F. eks. skal alle `4` bli byttet med `198`, da får vi spredd
204 | verdiene utover hele spekteret og får bedre kontrast.
205 |
206 | ||||||||||
207 | |---|---|---|---|---|---|---|---|---|
208 | |**k**|**0**|**1**|**2**|**3**|**4**|**5**|**6**|...|
209 | |**nr**|1|2|0|1|3|2|0|...|
210 | |**sr**|1|3|3|4|7|9|9|...|
211 | |**ny_kr**|28|85|85|113|198|255|255|...
212 |
213 | ## Seam carving
214 |
215 | 
216 |
217 | Når man skalerer bilder ned i én akse, får man et problem med at ting blir skvist/strukket ut av sine egentlige proposjoner.
218 | Seam carving løser dette ved at i stedet for å skalere ned hele bildet jevnt, finner man heller de
219 | uviktige delene av bildet og forkaster dette.
220 |
221 | Algoritmen består av 3 deler:
222 | 1. Finn energien til hver piksel, altså hvor viktig den er i bildet
223 | 2. Beregn forskjellige stier fra topp til bunn og regn ut deres totale energi
224 | 3. Fjern pikslene i den stien som var minst viktig (hadde lavest energi)
225 |
226 | Dette gjøres om og om igjen til bildet har den bredden man ønsker.
227 |
228 | I `src/effects/seamcarving.js` skal du implementere del 1 og 2. Del 3 er ikke så spennende og mest knot, så det får du av oss.
229 |
230 | **imageEnergy(..)**
231 | Bildet vi skal returnere skal ikke ha RGB, men bare én verdi per piksel, så i stedet for å lage et nytt bilde kan vi heller lage et
232 | energyimage i riktig størrelse, ala `var energyImage = Image.createEnergyImage(image.width, image.height);`.
233 | Et energyimage er det samme som et vanlig image, men kan bare ha én verdi per piksel. Den kan vi sette ved å gjøre `energyImage.setValue(x, y, 1000);`.
234 |
235 | Vi må beregne energinivået til hver piksel. For å beregne det for en piksel, ser vi på pikslene rundt.
236 | For piksel `(x, y)`, er det pikselen over `(x, y-1)`, under `(x, y+1)`, venstre `(x-1, y)` og til høyre `(x+1, y)` vi må se på.
237 | Vi regner ut differansen i rød, grønn og blå mellom pikselen til høyre og den til venstre, aka
238 | ```
239 | diffRx = (rHøyre - rVenstre)^2
240 | diffGx = (gHøyre - gVenstre)^2
241 | diffBx = (bHøyre - bVenstre)^2
242 | ```
243 | I kode ville det tilsvart `diffRx = Math.pow(image.getR(x+1, y) - image.getR(x-1, y), 2)`. Deretter gjør man det samme, men for pikselen under minus den over.
244 | Når man har gjort det summerer man sammen alle 6 verdiene og tar roten av dem, ala `Math.sqrt(diffRx + diffGx + diffBx + diffRy + diffGy + diffBy)`.
245 |
246 | **Eksempel**
247 | Om det var litt forvirrende, har vi heldigvis et eksempel her. Vi skal beregne energien til pikselen i midten.
248 |
249 | | | | |
250 | |---|---|---|
251 | |(0,0,0)|(180,190,50)|(0,0,0)|
252 | |(100,101,75)|(55,55,55)|(255,125,50)|
253 | |(0,0,0)|(199,200,10)|(0,0,0)|
254 |
255 | ```
256 | diffRx = (255 - 100)^2 = 24025
257 | diffGx = (125 - 101)^2 = 576
258 | diffBx = (50 - 75)^2 = 625
259 | diffRy = (199 - 180)^2 = 361
260 | diffGy = (200 - 190)^2 = 100
261 | diffBy = (10 - 50)^2 = 1600
262 | energi = sqrt(24025 + 576 + 625 + 361 + 100 + 1600) = 165.18
263 | ```
264 |
265 | Piksler som ligger på kanten vil ikke dette fungere på, så du kan bruke `isBorderPixel(..)` funksjonen for å sjekke dette, og da heller sette
266 | energien for den pikselen til en fast verdi. `300` f. eks.
267 |
268 | **calculateSeams(..)**
269 |
270 | Her bruker vi dynamisk programmering for å finne den veien fra topp til bunn av bildet som har minst energi.
271 | Vi får inn energibildet vi akkurat regnet ut, og skal nå summere verdiene nedover for å beregne totalenergien for de forskjellige veiene man kan velge.
272 |
273 | En path/vei er sammenhengende, og kan enten komme fra pikselen rett over, den skrått oppover til venstre, eller den skrått oppover til høyre.
274 |
275 | **Eksempel**:
276 | 
277 |
278 | Verdiene i EnergyImage til venstre er de vi beregnet i forrige steg, og får som input til vår funksjon. Etter at funksjonen vår har kjørt,
279 | skal vi returnere et nytt bilde med verdier tilsvarende det i høyre bilde. Algoritmen er som følger:
280 | For første rad, kopier over verdiene som energibildet hadde. For de andre radene, finner vi billigste path til den pikselen vi ser på ovenifra, og legger til verdien til pikselen fra energibildet.
281 | For eksempel om vi ser på ruten med pilene: For den pikselen ser vi på de 3 pikslene ovenfor. Av 4, 3 og 5 er 3 lavest. Så ser vi på energibildet, der har den pikselen verdien 5. Vi summerer da 3 og 5 og får verdien 8.
282 | Vi gjør dette for alle piksler på rad to, før vi så gjør det samme for rad 3. Da finner vi den laveste av de over på rad 2.
283 | Merk at på kanten har vi bare to over oss. F. eks. nederst i venstre hjørne er det det laveste av 4 og 3, plusset på 5, som blir verdien.
284 |
285 | Algoritmen er litt forenklet for hver piksel (man må ta hensyn til om man er på kanten):
286 | ```javascript
287 | seamValue = Math.min(
288 | seamImage.getValue(x-1, y-1),
289 | seamImage.getValue(x, y-1),
290 | seamImage.getValue(x+1, y-1)
291 | )
292 | seamImage.setValue(x, y, seamValue + valueOfEnergyImage);
293 | ```
294 | Dette gjøres da først for alle pikslene på en rad, så raden under osv.
295 |
296 |
297 | (* Lavpassfilter er et type filter som jevner ut forskjeller. Motsetningen er høypassfilter, som fremhever forskjeller.
298 | Både blur og median filter er lavpassfiltre. Et filter som beregner den andrederiverte er et eksempel på et høypassfilter.)
299 |
300 |
301 |
--------------------------------------------------------------------------------
/docs/earring.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/earring.jpg
--------------------------------------------------------------------------------
/docs/examples/boxblur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/boxblur.png
--------------------------------------------------------------------------------
/docs/examples/greyscale.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/greyscale.png
--------------------------------------------------------------------------------
/docs/examples/histogramequalization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/histogramequalization.png
--------------------------------------------------------------------------------
/docs/examples/invert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/invert.png
--------------------------------------------------------------------------------
/docs/examples/medianfilter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/medianfilter.png
--------------------------------------------------------------------------------
/docs/examples/seamcarvedynamicprogramming.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/seamcarvedynamicprogramming.png
--------------------------------------------------------------------------------
/docs/examples/seamcarving.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/seamcarving.png
--------------------------------------------------------------------------------
/docs/examples/sharpen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/sharpen.png
--------------------------------------------------------------------------------
/docs/examples/skformula.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/skformula.png
--------------------------------------------------------------------------------
/docs/examples/threshold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/threshold.png
--------------------------------------------------------------------------------
/docs/examples/warmfilter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/examples/warmfilter.png
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AlgPip's Super Image Editor
6 |
7 |
8 |
9 |
10 | AlgPip > Photoshop
11 | Se oppgavene / implementasjonen her
12 |
13 |
14 |
15 |
16 |
17 | tower.jpg
18 | earring.jpg
19 | simple.png
20 | low-contrast.png
21 | noisy.png
22 | supernoisy.jpg
23 |
24 | Reset image
25 | Undo effect
26 |
27 |
28 |
29 |
30 | Greyscale
31 |
32 | Threshold
33 |
34 |
35 | Invert
36 | Warm
37 |
38 |
39 |
40 |
41 | Boxblur
42 |
43 |
44 | Sharpen
45 |
46 | Median filter
47 |
48 |
49 | Histogram equalization
50 |
51 |
52 |
53 |
54 | Show energy
55 | Show seam
56 | Run seam carving
57 | Stop
58 |
59 |
60 |
61 |
62 |
63 | Edited:
64 |
65 |
66 |
67 |
68 | Original:
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/docs/index.js:
--------------------------------------------------------------------------------
1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i {
15 | const imageData = this.getImageData(loader);
16 | this.setNewImage(imageData);
17 | };
18 | loader.src = imageName;
19 | }
20 |
21 | getImageData(loader) {
22 | const loaderCanvas = document.querySelector("#loaderCanvas");
23 | const loaderContext = loaderCanvas.getContext('2d');
24 |
25 | // Draw scaled image to invisible canvas and read back the pixel data
26 | const [width, height] = scaleSize(loader.width, loader.height);
27 | loaderCanvas.width = width;
28 | loaderCanvas.height = height;
29 |
30 | loaderContext.drawImage(loader, 0, 0, width, height);
31 | const data = loaderContext.getImageData(0, 0, width, height);
32 |
33 | return new ImageData.Image(data);
34 | }
35 |
36 | setNewImage(image) {
37 | this.original = image;
38 | this.current = ImageData.clone(image);
39 | this.olds = [this.current];
40 | this.render(this.original, "original");
41 | this.render(this.current, "edited");
42 | }
43 |
44 | render(image, canvasName) {
45 | const canvas = document.querySelector("#" + canvasName);
46 | const context = canvas.getContext('2d');
47 |
48 | canvas.width = image.width;
49 | canvas.height = image.height;
50 |
51 | context.putImageData(image.imageData, 0, 0);
52 | }
53 |
54 | applyEffect(effect, ...args) {
55 | this.current = effect(ImageData.clone(this.current), ...args);
56 | this.olds.push(this.current);
57 | this.render(this.current, "edited");
58 | }
59 |
60 | setCurrent(image) {
61 | this.current = image;
62 | this.olds.push(this.current);
63 | this.render(this.current, "edited");
64 | }
65 |
66 | undoEffect() {
67 | if (this.olds.length > 1) {
68 | this.olds.pop();
69 | this.current = this.olds[this.olds.length - 1];
70 | this.render(this.current, "edited");
71 | }
72 | }
73 |
74 |
75 |
76 | }
77 |
78 | function scaleSize(width, height) {
79 | if (width > MAX_WIDTH) {
80 | height = height / (width / MAX_WIDTH);
81 | width = MAX_WIDTH;
82 | }
83 | return [width, height];
84 | }
85 |
86 | module.exports = Editor;
87 | },{"./Image":2}],2:[function(require,module,exports){
88 | class Image {
89 |
90 | constructor(imageData) {
91 | this.imageData = imageData;
92 | this.data = imageData.data;
93 | this.width = imageData.width;
94 | this.height = imageData.height;
95 | }
96 |
97 | /**
98 | * Data is a single-dimensional array, with 4 values (rgba) per px
99 | */
100 | getIndex(x, y) {
101 | if (x < 0 || x >= this.width) {
102 | throw new Error("x " + x + " is not between 0 and " + this.width);
103 | }else if (y < 0 || y >= this.height) {
104 | throw new Error("y " + y + " is not between 0 and " + this.height);
105 | }
106 | return y * (this.width * 4) + (x * 4);
107 | }
108 |
109 | getR(x, y) {
110 | return this.data[this.getIndex(x, y)];
111 | }
112 |
113 | getG(x, y) {
114 | return this.data[this.getIndex(x, y) + 1];
115 | }
116 |
117 | getB(x, y) {
118 | return this.data[this.getIndex(x, y) + 2];
119 | }
120 |
121 | getRGB(x, y) {
122 | const index = this.getIndex(x, y);
123 | return [this.data[index], this.data[index + 1], this.data[index + 2]];
124 | }
125 |
126 | setR(x, y, rValue) {
127 | this.data[this.getIndex(x, y)] = rValue;
128 | }
129 |
130 | setG(x, y, gValue) {
131 | this.data[this.getIndex(x, y) + 1] = gValue;
132 | }
133 |
134 | setB(x, y, bValue) {
135 | this.data[this.getIndex(x, y) + 2] = bValue;
136 | }
137 |
138 | setRGB(x, y, [r, g, b]) {
139 | const index = this.getIndex(x, y);
140 | this.data[index] = r;
141 | this.data[index + 1] = g;
142 | this.data[index + 2] = b;
143 | }
144 |
145 | }
146 |
147 | /**
148 | *
149 | * @param width number
150 | * @param height number
151 | * @returns {Image}
152 | */
153 | function empty(width, height) {
154 | const buffer = new ArrayBuffer(width * height * 4);
155 | const data = new Uint8ClampedArray(buffer);
156 | const imageData = new ImageData(data, width, height);
157 |
158 | // Set alpha to 255
159 | for (let i = 3; i < (width * height * 4); i += 4) {
160 | data[i] = 255;
161 | }
162 |
163 | return new Image(imageData);
164 | }
165 |
166 | /**
167 | * @param image {Image}
168 | * @returns {Image}
169 | */
170 | function clone(image) {
171 | const buffer = new ArrayBuffer(image.width * image.height * 4);
172 | const data = new Uint8ClampedArray(buffer);
173 | const imageData = new ImageData(data, image.width, image.height);
174 |
175 | for (let i = 0; i < data.length; i++) {
176 | data[i] = image.data[i];
177 | }
178 |
179 | return new Image(imageData);
180 |
181 | }
182 |
183 |
184 | class EnergyImage {
185 | constructor(data32bit, width, height) {
186 | this.data32bit = data32bit;
187 | this.width = width;
188 | this.height = height;
189 | }
190 |
191 | setValue(x, y, value) {
192 | this.data32bit[this.getIndex(x, y)] = value;
193 | }
194 |
195 | getValue(x, y) {
196 | return this.data32bit[this.getIndex(x, y)];
197 | }
198 |
199 | getIndex(x, y) {
200 | if (x < 0 || x >= this.width) {
201 | throw new Error("x " + x + " is not between 0 and " + this.width);
202 | }else if (y < 0 || y >= this.height) {
203 | throw new Error("y " + y + " is not between 0 and " + this.height);
204 | }
205 | return y * this.width + x;
206 | }
207 | }
208 |
209 | function createEnergyImage(width, height) {
210 | const buffer = new ArrayBuffer(width * height * 4);
211 | const data32Bit = new Uint32Array(buffer);
212 |
213 | return new EnergyImage(data32Bit, width, height);
214 | }
215 | /*
216 | function cloneEnergyImage(energyImage) {
217 |
218 | const buffer = new ArrayBuffer(energyImage.width * energyImage.height * 4);
219 | const data32Bit = new Uint32Array(buffer);
220 |
221 | for (let i = 0; i < data32Bit.length; i++) {
222 | data32Bit[i] = energyImage.data32bit[i];
223 | }
224 |
225 | return new EnergyImage(data32Bit, energyImage.width, energyImage.height);
226 | }*/
227 |
228 | module.exports = {
229 | Image,
230 | empty,
231 | clone,
232 | EnergyImage,
233 | createEnergyImage,
234 | // cloneEnergyImage
235 | };
236 |
237 | },{}],3:[function(require,module,exports){
238 | const Image = require('../Image');
239 |
240 | /**
241 | * @param image {Image}
242 | * @param radius Number
243 | * @returns {Image}
244 | */
245 | function boxblur(image, radius) {
246 | radius = Number(radius);
247 | const newImage = Image.empty(image.width, image.height);
248 |
249 | for (let x = radius; x < image.width-radius; x++) {
250 | for (let y = radius; y < image.height-radius; y++) {
251 | let sumR = 0;
252 | let sumG = 0;
253 | let sumB = 0;
254 |
255 | for (let i = x-radius; i <= x+radius; i++) {
256 | for (let j = y-radius; j <= y+radius; j++) {
257 | sumR = sumR + image.getR(i, j);
258 | sumG = sumG + image.getG(i, j);
259 | sumB = sumB + image.getB(i, j);
260 | }
261 | }
262 |
263 | newImage.setR(x, y, sumR/Math.pow(radius*2+1,2));
264 | newImage.setG(x, y, sumG/Math.pow(radius*2+1,2));
265 | newImage.setB(x, y, sumB/Math.pow(radius*2+1,2));
266 | }
267 | }
268 | return newImage;
269 | }
270 |
271 | module.exports = boxblur;
272 |
273 | },{"../Image":2}],4:[function(require,module,exports){
274 | const Image = require('../Image');
275 |
276 | /**
277 | * Takes an image with RGB values, and converts it to greyscale
278 | * by calculating intensity at each pixel
279 | *
280 | * @param image {Image}
281 | * @returns {Image}
282 | */
283 | function greyscale(image) {
284 | const newImage = Image.empty(image.width, image.height);
285 |
286 | for (let y = 0; y < image.height; y++) {
287 | for (let x = 0; x < image.width; x++) {
288 |
289 | const r = image.getR(x, y);
290 | const g = image.getG(x, y);
291 | const b = image.getB(x, y);
292 |
293 | const intensity = 0.34 * r + 0.5 * g + 0.16 * b;
294 |
295 | newImage.setR(x, y, intensity);
296 | newImage.setG(x, y, intensity);
297 | newImage.setB(x, y, intensity);
298 |
299 | /* evt
300 | const [r, g, b] = image.getRGB(x, y);
301 | const c = 0.34 * r + 0.5 * g + 0.16 * b;
302 | newImage.setRGB(x, y, [c, c, c]);*/
303 | }
304 | }
305 | return newImage;
306 | }
307 |
308 | module.exports = greyscale;
309 | },{"../Image":2}],5:[function(require,module,exports){
310 | const Image = require('../Image');
311 |
312 | /**
313 | * @param image {Image}
314 | * @returns {Image}
315 | */
316 | function histogramequalization(image) {
317 | const newImage = Image.empty(image.width, image.height);
318 |
319 | const nr = Array(256).fill(0);
320 | const ng = Array(256).fill(0);
321 | const nb = Array(256).fill(0);
322 |
323 | const sr = Array(256).fill(0);
324 | const sg = Array(256).fill(0);
325 | const sb = Array(256).fill(0);
326 |
327 | // Counting number of pixels with intensity from 0 to 255
328 | // nr[0] is the number of pixels with R-value that has an intensity of 0, nr[1] is the number of pixels with R-value with intensity 1 etc.
329 | // Similiar for ng and nb
330 | for (let x = 0; x < image.width; x++) {
331 | for (let y = 0; y < image.height; y++) {
332 | nr[image.getR(x, y)]++;
333 | ng[image.getG(x, y)]++;
334 | nb[image.getB(x, y)]++;
335 | }
336 | }
337 |
338 | // sr[k] is the new intensity for all the pixels with R-value equal to intensity k
339 | for (let k = 0; k < 256; k++) {
340 | for (let j = 0; j <= k; j++) {
341 | sr[k] = sr[k] + nr[j];
342 | sg[k] = sg[k] + ng[j];
343 | sb[k] = sb[k] + nb[j];
344 | }
345 | sr[k] = Math.round(sr[k]*255/(image.height*image.width));
346 | sg[k] = Math.round(sg[k]*255/(image.height*image.width));
347 | sb[k] = Math.round(sb[k]*255/(image.height*image.width));
348 | }
349 |
350 | // Tranforming all pixels and their RGB-values to their new histogramequalized values.
351 | for (let x = 0; x < image.width; x++) {
352 | for (let y = 0; y < image.height; y++) {
353 | newImage.setR(x, y, sr[image.getR(x, y)]);
354 | newImage.setG(x, y, sg[image.getG(x, y)]);
355 | newImage.setB(x, y, sb[image.getB(x, y)]);
356 | }
357 | }
358 |
359 | return newImage;
360 | }
361 |
362 | module.exports = histogramequalization;
363 |
364 | },{"../Image":2}],6:[function(require,module,exports){
365 | const Image = require('../Image');
366 |
367 | /**
368 | * Takes an image with RGB values, and inverts the value of each pixel, i.e 255 - pixel value.
369 | *
370 | * @param image {Image}
371 | * @returns {Image}
372 | */
373 | function invert(image) {
374 | const newImage = Image.empty(image.width, image.height);
375 |
376 | for (let x = 0; x < image.width; x++) {
377 | for (let y = 0; y < image.height; y++) {
378 |
379 | const r = 255 - image.getR(x, y);
380 | const g = 255 - image.getG(x, y);
381 | const b = 255 - image.getB(x, y);
382 |
383 | newImage.setRGB(x, y, [r, g, b]);
384 | }
385 | }
386 | return newImage;
387 | }
388 |
389 | module.exports = invert;
390 |
391 | },{"../Image":2}],7:[function(require,module,exports){
392 | const Image = require('../Image');
393 |
394 | /**
395 | * @param image {Image}
396 | * @param radius Number
397 | * @returns {Image}
398 | */
399 | function medianfilter(image, radius) {
400 | radius = Number(radius);
401 | const median = Math.trunc(Math.pow(2*radius+1, 2)/2);
402 | const newImage = Image.empty(image.width, image.height);
403 |
404 | const medianr = Array(Math.pow(2*radius+1, 2)).fill(0);
405 | const mediang = Array(Math.pow(2*radius+1, 2)).fill(0);
406 | const medianb = Array(Math.pow(2*radius+1, 2)).fill(0);
407 |
408 | for (let x = radius; x < image.width-radius; x++) {
409 | for (let y = radius; y < image.height-radius; y++) {
410 |
411 | let count = 0;
412 | for (let i = x-radius; i <= x+radius; i++) {
413 | for (let j = y-radius; j <= y+radius; j++) {
414 | medianr[count] = image.getR(i, j);
415 | mediang[count] = image.getG(i, j);
416 | medianb[count] = image.getB(i, j);
417 | count++;
418 | }
419 | }
420 |
421 | medianr.sort((a, b) => a - b);
422 | mediang.sort((a, b) => a - b);
423 | medianb.sort((a, b) => a - b);
424 |
425 | newImage.setR(x, y, medianr[median]);
426 | newImage.setG(x, y, mediang[median]);
427 | newImage.setB(x, y, medianb[median]);
428 | }
429 | }
430 | return newImage;
431 | }
432 |
433 | module.exports = medianfilter;
434 |
435 | },{"../Image":2}],8:[function(require,module,exports){
436 | const Image = require('../Image');
437 |
438 | /*
439 | * IMPLEMENT imageEnergy(..) and calculateSeams(..) functions
440 | */
441 |
442 | /**
443 | * Takes an RGB image, returns a new image with energylevels per pixel
444 | * @param image {Image}
445 | * @returns {EnergyImage}
446 | */
447 | function imageEnergy(image) {
448 | const energyImage = Image.createEnergyImage(image.width, image.height);
449 |
450 | for (let y = 0; y < image.height; y++) {
451 | for (let x = 0; x < image.width; x++) {
452 | let energy;
453 | if (isBorderPixel(x, y, image.width, image.height)) {
454 | energy = 300;
455 | } else {
456 | energy = Math.sqrt(
457 | Math.pow(image.getR(x + 1, y) - image.getR(x - 1, y), 2) +
458 | Math.pow(image.getG(x + 1, y) - image.getG(x - 1, y), 2) +
459 | Math.pow(image.getB(x + 1, y) - image.getB(x - 1, y), 2) +
460 |
461 | Math.pow(image.getR(x, y + 1) - image.getR(x, y - 1), 2) +
462 | Math.pow(image.getG(x, y + 1) - image.getG(x, y - 1), 2) +
463 | Math.pow(image.getB(x, y + 1) - image.getB(x, y - 1), 2)
464 | );
465 |
466 | }
467 |
468 | energyImage.setValue(x, y, energy);
469 | }
470 | }
471 | return energyImage;
472 | }
473 |
474 | function isBorderPixel(x, y, imageWidth, imageHeight) {
475 | return x === 0 || x === imageWidth - 1 || y === 0 || y === imageHeight - 1;
476 | }
477 |
478 |
479 | /**
480 | * Takes an energyImage with energylevels per pixel, and uses dynamic programming
481 | * to find paths from top to bottom with the least energy
482 | * @param energyImage {EnergyImage}
483 | */
484 | function calculateSeams(energyImage) {
485 | const seamImage = Image.createEnergyImage(energyImage.width, energyImage.height);
486 |
487 | for (let y = 0; y < seamImage.height; y++) {
488 | for (let x = 0; x < seamImage.width; x++) {
489 | const energyAtPx = energyImage.getValue(x, y);
490 |
491 | if (y === 0) {
492 | seamImage.setValue(x, y, energyAtPx);
493 | continue;
494 | }
495 |
496 | const minParent = Math.min(x - 1 >= 0 ? seamImage.getValue(x - 1, y - 1) : 99999, seamImage.getValue(x, y - 1), x + 1 < seamImage.width ? seamImage.getValue(x + 1, y - 1) : 99999);
497 | seamImage.setValue(x, y, energyAtPx + minParent);
498 | }
499 | }
500 | return seamImage;
501 | }
502 |
503 |
504 | /*
505 | *
506 | * STUFF below already implemented for you :)
507 | *
508 | */
509 |
510 |
511 | /**
512 | * After all the paths are calculated, find the lowest one on the last row
513 | * and move back up, keeping track of the path
514 | */
515 | function findMinSeam(seams) {
516 | const positions = [];
517 |
518 | // find lowest pos
519 | let lowest = 99999, lowestIndex = 0;
520 | for (let x = 0; x < seams.width; x++) {
521 | const value = seams.getValue(x, seams.height - 1);
522 | if (value < lowest) {
523 | lowest = value;
524 | lowestIndex = x;
525 | }
526 | }
527 | positions[seams.height - 1] = lowestIndex;
528 |
529 | // iterate upwards
530 | for (let y = seams.height - 2; y >= 0; y--) {
531 | let lowestParent = 999999, lowestParentIndex = 0;
532 | if (lowestIndex - 1 >= 0) {
533 | lowestParent = seams.getValue(lowestIndex - 1, y);
534 | lowestParentIndex = lowestIndex - 1;
535 | }
536 |
537 | if (seams.getValue(lowestIndex, y) < lowestParent) {
538 | lowestParent = seams.getValue(lowestIndex, y);
539 | lowestParentIndex = lowestIndex;
540 | }
541 |
542 | if (lowestIndex + 1 < seams.width && seams.getValue(lowestIndex + 1, y) < lowestParent) {
543 | lowestParent = seams.getValue(lowestIndex + 1, y);
544 | lowestParentIndex = lowestIndex + 1;
545 | }
546 |
547 | positions[y] = lowestParentIndex;
548 | lowestIndex = lowestParentIndex;
549 | }
550 |
551 | return positions;
552 | }
553 |
554 |
555 | /**
556 | * Creates a new image, one pixel smaller, that contains everything
557 | * from the original image, except the pixel on each line found from seam carving to remove
558 | * @param image {Image}
559 | * @param seamPos {Number[]} x value for pixel to remove on each row
560 | * @returns {Image}
561 | */
562 | function removeSeam(image, seamPos) {
563 | const newImage = Image.empty(image.width - 1, image.height);
564 |
565 |
566 | for (let y = 0; y < newImage.height; y++) {
567 | const xToRemove = seamPos[y];
568 | for (let x = 0; x < newImage.width; x++) {
569 | if (x < xToRemove) {
570 | newImage.setRGB(x, y, image.getRGB(x, y));
571 | } else {
572 | newImage.setRGB(x, y, image.getRGB(x + 1, y));
573 | }
574 |
575 | }
576 | }
577 | return newImage;
578 | }
579 |
580 | /**
581 | * Marks the found seam as a red path on the image
582 | * Not part of the algorithm, just used to show in the UI / debugging
583 | */
584 | function showSeam(image, seamPos) {
585 | const newImage = Image.clone(image);
586 |
587 | for (let y = 0; y < image.height; y++) {
588 | newImage.setRGB(seamPos[y], y, [255, 0, 0]);
589 | }
590 |
591 | return newImage;
592 | }
593 |
594 | /**
595 | * Showing the energy as greyscale image for debugging
596 | * Not part of the algorithm, just to show in the UI / debugging
597 | */
598 | function showEnergyImage(energy) {
599 | const image = Image.empty(energy.width, energy.height);
600 |
601 | for (let x = 0; x < image.width; x++) {
602 | for (let y = 0; y < image.height; y++) {
603 | const c = Math.ceil((energy.getValue(x, y) / 300) * 255);
604 |
605 | image.setRGB(x, y, [c, c, c]);
606 | }
607 | }
608 |
609 | return image;
610 | }
611 |
612 | module.exports = {
613 | imageEnergy,
614 | showEnergyImage,
615 | calculateSeams,
616 | findMinSeam,
617 | showSeam,
618 | removeSeam
619 | };
620 | },{"../Image":2}],9:[function(require,module,exports){
621 | const Image = require('../Image');
622 | const boxblur = require('./boxblur');
623 |
624 | /**
625 | * @param image {Image}
626 | * @returns {Image}
627 | */
628 | function sharpen(image) {
629 |
630 | const blurredImage = boxblur(image, 3);
631 | const newImage = Image.empty(image.width, image.height);
632 |
633 | for (let x = 0; x < image.width; x++) {
634 | for (let y = 0; y < image.height; y++) {
635 |
636 | const r = image.getR(x, y) + 3*(image.getR(x,y) - blurredImage.getR(x, y));
637 | const g = image.getG(x, y) + 3*(image.getG(x,y) - blurredImage.getG(x, y));
638 | const b = image.getB(x, y) + 3*(image.getB(x,y) - blurredImage.getB(x, y));
639 |
640 | newImage.setRGB(x, y, [r, g, b]);
641 | }
642 | }
643 |
644 | return newImage;
645 | }
646 |
647 | module.exports = sharpen;
648 |
649 | },{"../Image":2,"./boxblur":3}],10:[function(require,module,exports){
650 | const Image = require('../Image');
651 |
652 | /**
653 | * Takes an image with RGB values, and turns the pixels either white (255,255,255) or black (0,0,0)
654 | * depending on if the intensity is above or below the threshold
655 | *
656 | * @param image {Image}
657 | * @param threshold Number
658 | * @returns {Image}
659 | */
660 | function threshold(image, threshold) {
661 | const newImage = Image.empty(image.width, image.height);
662 |
663 | for (let x = 0; x < image.width; x++) {
664 | for (let y = 0; y < image.height; y++) {
665 |
666 | const r = image.getR(x, y);
667 | const g = image.getG(x, y);
668 | const b = image.getB(x, y);
669 |
670 | let c = 0;
671 | if ((r + g + b) / 3 > threshold) {
672 | c = 255;
673 | }
674 | newImage.setRGB(x, y, [c, c, c]);
675 | }
676 | }
677 | return newImage;
678 | }
679 |
680 | module.exports = threshold;
681 | },{"../Image":2}],11:[function(require,module,exports){
682 | const Image = require('../Image');
683 |
684 | /**
685 | * Adds some more red and removes some blue from the image's pixels
686 | * @param image {Image}
687 | * @returns {Image}
688 | */
689 | function warmfilter(image) {
690 | const newImage = Image.empty(image.width, image.height);
691 |
692 | for (let x = 0; x < image.width; x++) {
693 | for (let y = 0; y < image.height; y++) {
694 |
695 | const r = image.getR(x, y);
696 | const g = image.getG(x, y);
697 | const b = image.getB(x, y);
698 |
699 | newImage.setR(x, y, r + 25);
700 | newImage.setG(x, y, g);
701 | newImage.setB(x, y, b - 25);
702 | }
703 | }
704 | return newImage;
705 | }
706 |
707 | module.exports = warmfilter;
708 |
709 | },{"../Image":2}],12:[function(require,module,exports){
710 | const Editor = require('./Editor');
711 | const greyscale = require('./effects/greyscale');
712 | const colorfilter = require('./effects/warmfilter');
713 | const threshold = require('./effects/threshold');
714 | const boxblur = require('./effects/boxblur');
715 | const invert = require('./effects/invert');
716 | const histogramequalization = require('./effects/histogramequalization');
717 | const medianfilter = require('./effects/medianfilter');
718 | const sharpen = require('./effects/sharpen');
719 | const seam = require('./effects/seamcarving');
720 |
721 |
722 | const editor = new Editor();
723 | editor.loadImage('tower.jpg');
724 |
725 | document.querySelector("#images").addEventListener('change', () => {
726 | const selectedImage = document.querySelector("#images").value;
727 | editor.loadImage(selectedImage);
728 | });
729 |
730 | document.querySelector("#load").addEventListener('click', () => {
731 | const selectedImage = document.querySelector("#images").value;
732 | editor.loadImage(selectedImage);
733 | });
734 |
735 | document.querySelector("#undo").addEventListener('click', () => {
736 | editor.undoEffect();
737 | });
738 |
739 | document.querySelector("#threshold").addEventListener('click', () => {
740 | editor.applyEffect(threshold, document.querySelector("#thresholdvalue").value);
741 | });
742 |
743 | document.querySelector("#greyscale").addEventListener('click', () => {
744 | editor.applyEffect(greyscale);
745 | });
746 |
747 | document.querySelector("#warmfilter").addEventListener('click', () => {
748 | editor.applyEffect(colorfilter);
749 | });
750 |
751 | document.querySelector("#boxblur").addEventListener('click', () => {
752 | editor.applyEffect(boxblur, document.querySelector("#blurradius").value);
753 | });
754 |
755 | document.querySelector("#medianfilter").addEventListener('click', () => {
756 | editor.applyEffect(medianfilter, document.querySelector("#radius").value);
757 | });
758 |
759 | document.querySelector("#invert").addEventListener('click', () => {
760 | editor.applyEffect(invert);
761 | });
762 |
763 | document.querySelector("#sharpen").addEventListener('click', () => {
764 | editor.applyEffect(sharpen);
765 | });
766 |
767 | document.querySelector("#histogramequalization").addEventListener('click', () => {
768 | editor.applyEffect(histogramequalization);
769 | });
770 |
771 | document.querySelector("#energy").addEventListener('click', () => {
772 | editor.applyEffect((image) => {
773 | return seam.showEnergyImage(seam.imageEnergy(image));
774 | });
775 | });
776 | document.querySelector("#findSeam").addEventListener('click', () => {
777 | editor.applyEffect((image) => {
778 | const energy = seam.imageEnergy(image);
779 | const seams = seam.calculateSeams(energy);
780 | const minSeam = seam.findMinSeam(seams);
781 | const showSeam = seam.showSeam(image, minSeam);
782 | return showSeam;
783 | });
784 | });
785 |
786 |
787 | let isRunning = false;
788 | document.querySelector("#runSeamcarver").addEventListener('click', () => {
789 | isRunning = true;
790 | document.querySelector("#runSeamcarver").disabled = true;
791 | document.querySelector("#stopSeamcarver").disabled = false;
792 |
793 | let image = editor.current;
794 |
795 | function seamCarve() {
796 |
797 | const energy = seam.imageEnergy(image);
798 | const seams = seam.calculateSeams(energy);
799 | const minSeam = seam.findMinSeam(seams);
800 | const showSeam = seam.showSeam(image, minSeam);
801 |
802 | editor.render(showSeam, "edited");
803 |
804 | image = seam.removeSeam(image, minSeam);
805 |
806 | if (isRunning) {
807 | setTimeout(seamCarve, 10);
808 | } else {
809 | editor.setCurrent(image);
810 | }
811 | }
812 |
813 | seamCarve();
814 | });
815 |
816 | document.querySelector("#stopSeamcarver").addEventListener('click', () => {
817 | isRunning = false;
818 | document.querySelector("#runSeamcarver").disabled = false;
819 | document.querySelector("#stopSeamcarver").disabled = true;
820 | });
821 |
822 |
823 |
824 | },{"./Editor":1,"./effects/boxblur":3,"./effects/greyscale":4,"./effects/histogramequalization":5,"./effects/invert":6,"./effects/medianfilter":7,"./effects/seamcarving":8,"./effects/sharpen":9,"./effects/threshold":10,"./effects/warmfilter":11}]},{},[12])
825 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCJzcmMvRWRpdG9yLmpzIiwic3JjL0ltYWdlLmpzIiwic3JjL2VmZmVjdHMvYm94Ymx1ci5qcyIsInNyYy9lZmZlY3RzL2dyZXlzY2FsZS5qcyIsInNyYy9lZmZlY3RzL2hpc3RvZ3JhbWVxdWFsaXphdGlvbi5qcyIsInNyYy9lZmZlY3RzL2ludmVydC5qcyIsInNyYy9lZmZlY3RzL21lZGlhbmZpbHRlci5qcyIsInNyYy9lZmZlY3RzL3NlYW1jYXJ2aW5nLmpzIiwic3JjL2VmZmVjdHMvc2hhcnBlbi5qcyIsInNyYy9lZmZlY3RzL3RocmVzaG9sZC5qcyIsInNyYy9lZmZlY3RzL3dhcm1maWx0ZXIuanMiLCJzcmMvaW5kZXguanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUNBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNwRkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNwSkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNsQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNsQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3JEQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3pCQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUMxQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDdkxBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQzNCQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUM5QkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQzFCQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJmaWxlIjoiZ2VuZXJhdGVkLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXNDb250ZW50IjpbIihmdW5jdGlvbigpe2Z1bmN0aW9uIHIoZSxuLHQpe2Z1bmN0aW9uIG8oaSxmKXtpZighbltpXSl7aWYoIWVbaV0pe3ZhciBjPVwiZnVuY3Rpb25cIj09dHlwZW9mIHJlcXVpcmUmJnJlcXVpcmU7aWYoIWYmJmMpcmV0dXJuIGMoaSwhMCk7aWYodSlyZXR1cm4gdShpLCEwKTt2YXIgYT1uZXcgRXJyb3IoXCJDYW5ub3QgZmluZCBtb2R1bGUgJ1wiK2krXCInXCIpO3Rocm93IGEuY29kZT1cIk1PRFVMRV9OT1RfRk9VTkRcIixhfXZhciBwPW5baV09e2V4cG9ydHM6e319O2VbaV1bMF0uY2FsbChwLmV4cG9ydHMsZnVuY3Rpb24ocil7dmFyIG49ZVtpXVsxXVtyXTtyZXR1cm4gbyhufHxyKX0scCxwLmV4cG9ydHMscixlLG4sdCl9cmV0dXJuIG5baV0uZXhwb3J0c31mb3IodmFyIHU9XCJmdW5jdGlvblwiPT10eXBlb2YgcmVxdWlyZSYmcmVxdWlyZSxpPTA7aTx0Lmxlbmd0aDtpKyspbyh0W2ldKTtyZXR1cm4gb31yZXR1cm4gcn0pKCkiLCJjb25zdCBJbWFnZURhdGEgPSByZXF1aXJlKCcuL0ltYWdlJyk7XHJcblxyXG5jb25zdCBNQVhfV0lEVEggPSAxMjAwO1xyXG5cclxuY2xhc3MgRWRpdG9yIHtcclxuXHJcblxyXG4gICAgY29uc3RydWN0b3IoKSB7XHJcbiAgICB9XHJcblxyXG4gICAgbG9hZEltYWdlKGltYWdlTmFtZSkge1xyXG4gICAgICAgIGNvbnN0IGxvYWRlciA9IG5ldyBJbWFnZSgpO1xyXG4gICAgICAgIGxvYWRlci5vbmxvYWQgPSAoKSA9PiB7XHJcbiAgICAgICAgICAgIGNvbnN0IGltYWdlRGF0YSA9IHRoaXMuZ2V0SW1hZ2VEYXRhKGxvYWRlcik7XHJcbiAgICAgICAgICAgIHRoaXMuc2V0TmV3SW1hZ2UoaW1hZ2VEYXRhKTtcclxuICAgICAgICB9O1xyXG4gICAgICAgIGxvYWRlci5zcmMgPSBpbWFnZU5hbWU7XHJcbiAgICB9XHJcblxyXG4gICAgZ2V0SW1hZ2VEYXRhKGxvYWRlcikge1xyXG4gICAgICAgIGNvbnN0IGxvYWRlckNhbnZhcyA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXCIjbG9hZGVyQ2FudmFzXCIpO1xyXG4gICAgICAgIGNvbnN0IGxvYWRlckNvbnRleHQgPSBsb2FkZXJDYW52YXMuZ2V0Q29udGV4dCgnMmQnKTtcclxuXHJcbiAgICAgICAgLy8gRHJhdyBzY2FsZWQgaW1hZ2UgdG8gaW52aXNpYmxlIGNhbnZhcyBhbmQgcmVhZCBiYWNrIHRoZSBwaXhlbCBkYXRhXHJcbiAgICAgICAgY29uc3QgW3dpZHRoLCBoZWlnaHRdID0gc2NhbGVTaXplKGxvYWRlci53aWR0aCwgbG9hZGVyLmhlaWdodCk7XHJcbiAgICAgICAgbG9hZGVyQ2FudmFzLndpZHRoID0gd2lkdGg7XHJcbiAgICAgICAgbG9hZGVyQ2FudmFzLmhlaWdodCA9IGhlaWdodDtcclxuXHJcbiAgICAgICAgbG9hZGVyQ29udGV4dC5kcmF3SW1hZ2UobG9hZGVyLCAwLCAwLCB3aWR0aCwgaGVpZ2h0KTtcclxuICAgICAgICBjb25zdCBkYXRhID0gbG9hZGVyQ29udGV4dC5nZXRJbWFnZURhdGEoMCwgMCwgd2lkdGgsIGhlaWdodCk7XHJcblxyXG4gICAgICAgIHJldHVybiBuZXcgSW1hZ2VEYXRhLkltYWdlKGRhdGEpO1xyXG4gICAgfVxyXG5cclxuICAgIHNldE5ld0ltYWdlKGltYWdlKSB7XHJcbiAgICAgICAgdGhpcy5vcmlnaW5hbCA9IGltYWdlO1xyXG4gICAgICAgIHRoaXMuY3VycmVudCA9IEltYWdlRGF0YS5jbG9uZShpbWFnZSk7XHJcbiAgICAgICAgdGhpcy5vbGRzID0gW3RoaXMuY3VycmVudF07XHJcbiAgICAgICAgdGhpcy5yZW5kZXIodGhpcy5vcmlnaW5hbCwgXCJvcmlnaW5hbFwiKTtcclxuICAgICAgICB0aGlzLnJlbmRlcih0aGlzLmN1cnJlbnQsIFwiZWRpdGVkXCIpO1xyXG4gICAgfVxyXG5cclxuICAgIHJlbmRlcihpbWFnZSwgY2FudmFzTmFtZSkge1xyXG4gICAgICAgIGNvbnN0IGNhbnZhcyA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXCIjXCIgKyBjYW52YXNOYW1lKTtcclxuICAgICAgICBjb25zdCBjb250ZXh0ID0gY2FudmFzLmdldENvbnRleHQoJzJkJyk7XHJcblxyXG4gICAgICAgIGNhbnZhcy53aWR0aCA9IGltYWdlLndpZHRoO1xyXG4gICAgICAgIGNhbnZhcy5oZWlnaHQgPSBpbWFnZS5oZWlnaHQ7XHJcblxyXG4gICAgICAgIGNvbnRleHQucHV0SW1hZ2VEYXRhKGltYWdlLmltYWdlRGF0YSwgMCwgMCk7XHJcbiAgICB9XHJcblxyXG4gICAgYXBwbHlFZmZlY3QoZWZmZWN0LCAuLi5hcmdzKSB7XHJcbiAgICAgICAgdGhpcy5jdXJyZW50ID0gZWZmZWN0KEltYWdlRGF0YS5jbG9uZSh0aGlzLmN1cnJlbnQpLCAuLi5hcmdzKTtcclxuICAgICAgICB0aGlzLm9sZHMucHVzaCh0aGlzLmN1cnJlbnQpO1xyXG4gICAgICAgIHRoaXMucmVuZGVyKHRoaXMuY3VycmVudCwgXCJlZGl0ZWRcIik7XHJcbiAgICB9XHJcblxyXG4gICAgc2V0Q3VycmVudChpbWFnZSkge1xyXG4gICAgICAgIHRoaXMuY3VycmVudCA9IGltYWdlO1xyXG4gICAgICAgIHRoaXMub2xkcy5wdXNoKHRoaXMuY3VycmVudCk7XHJcbiAgICAgICAgdGhpcy5yZW5kZXIodGhpcy5jdXJyZW50LCBcImVkaXRlZFwiKTtcclxuICAgIH1cclxuXHJcbiAgICB1bmRvRWZmZWN0KCkge1xyXG4gICAgICAgIGlmICh0aGlzLm9sZHMubGVuZ3RoID4gMSkge1xyXG4gICAgICAgICAgICB0aGlzLm9sZHMucG9wKCk7XHJcbiAgICAgICAgICAgIHRoaXMuY3VycmVudCA9IHRoaXMub2xkc1t0aGlzLm9sZHMubGVuZ3RoIC0gMV07XHJcbiAgICAgICAgICAgIHRoaXMucmVuZGVyKHRoaXMuY3VycmVudCwgXCJlZGl0ZWRcIik7XHJcbiAgICAgICAgfVxyXG4gICAgfVxyXG5cclxuXHJcblxyXG59XHJcblxyXG5mdW5jdGlvbiBzY2FsZVNpemUod2lkdGgsIGhlaWdodCkge1xyXG4gICAgaWYgKHdpZHRoID4gTUFYX1dJRFRIKSB7XHJcbiAgICAgICAgaGVpZ2h0ID0gaGVpZ2h0IC8gKHdpZHRoIC8gTUFYX1dJRFRIKTtcclxuICAgICAgICB3aWR0aCA9IE1BWF9XSURUSDtcclxuICAgIH1cclxuICAgIHJldHVybiBbd2lkdGgsIGhlaWdodF07XHJcbn1cclxuXHJcbm1vZHVsZS5leHBvcnRzID0gRWRpdG9yOyIsImNsYXNzIEltYWdlIHtcclxuXHJcbiAgICBjb25zdHJ1Y3RvcihpbWFnZURhdGEpIHtcclxuICAgICAgICB0aGlzLmltYWdlRGF0YSA9IGltYWdlRGF0YTtcclxuICAgICAgICB0aGlzLmRhdGEgPSBpbWFnZURhdGEuZGF0YTtcclxuICAgICAgICB0aGlzLndpZHRoID0gaW1hZ2VEYXRhLndpZHRoO1xyXG4gICAgICAgIHRoaXMuaGVpZ2h0ID0gaW1hZ2VEYXRhLmhlaWdodDtcclxuICAgIH1cclxuXHJcbiAgICAvKipcclxuICAgICAqIERhdGEgaXMgYSBzaW5nbGUtZGltZW5zaW9uYWwgYXJyYXksIHdpdGggNCB2YWx1ZXMgKHJnYmEpIHBlciBweFxyXG4gICAgICovXHJcbiAgICBnZXRJbmRleCh4LCB5KSB7XHJcbiAgICAgICAgaWYgKHggPCAwIHx8IHggPj0gdGhpcy53aWR0aCkge1xyXG4gICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoXCJ4IFwiICsgeCArIFwiIGlzIG5vdCBiZXR3ZWVuIDAgYW5kIFwiICsgdGhpcy53aWR0aCk7XHJcbiAgICAgICAgfWVsc2UgaWYgKHkgPCAwIHx8IHkgPj0gdGhpcy5oZWlnaHQpIHtcclxuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKFwieSBcIiArIHkgKyBcIiBpcyBub3QgYmV0d2VlbiAwIGFuZCBcIiArIHRoaXMuaGVpZ2h0KTtcclxuICAgICAgICB9XHJcbiAgICAgICAgcmV0dXJuIHkgKiAodGhpcy53aWR0aCAqIDQpICsgKHggKiA0KTtcclxuICAgIH1cclxuXHJcbiAgICBnZXRSKHgsIHkpIHtcclxuICAgICAgICByZXR1cm4gdGhpcy5kYXRhW3RoaXMuZ2V0SW5kZXgoeCwgeSldO1xyXG4gICAgfVxyXG5cclxuICAgIGdldEcoeCwgeSkge1xyXG4gICAgICAgIHJldHVybiB0aGlzLmRhdGFbdGhpcy5nZXRJbmRleCh4LCB5KSArIDFdO1xyXG4gICAgfVxyXG5cclxuICAgIGdldEIoeCwgeSkge1xyXG4gICAgICAgIHJldHVybiB0aGlzLmRhdGFbdGhpcy5nZXRJbmRleCh4LCB5KSArIDJdO1xyXG4gICAgfVxyXG5cclxuICAgIGdldFJHQih4LCB5KSB7XHJcbiAgICAgICAgY29uc3QgaW5kZXggPSB0aGlzLmdldEluZGV4KHgsIHkpO1xyXG4gICAgICAgIHJldHVybiBbdGhpcy5kYXRhW2luZGV4XSwgdGhpcy5kYXRhW2luZGV4ICsgMV0sIHRoaXMuZGF0YVtpbmRleCArIDJdXTtcclxuICAgIH1cclxuXHJcbiAgICBzZXRSKHgsIHksIHJWYWx1ZSkge1xyXG4gICAgICAgIHRoaXMuZGF0YVt0aGlzLmdldEluZGV4KHgsIHkpXSA9IHJWYWx1ZTtcclxuICAgIH1cclxuXHJcbiAgICBzZXRHKHgsIHksIGdWYWx1ZSkge1xyXG4gICAgICAgIHRoaXMuZGF0YVt0aGlzLmdldEluZGV4KHgsIHkpICsgMV0gPSBnVmFsdWU7XHJcbiAgICB9XHJcblxyXG4gICAgc2V0Qih4LCB5LCBiVmFsdWUpIHtcclxuICAgICAgICB0aGlzLmRhdGFbdGhpcy5nZXRJbmRleCh4LCB5KSArIDJdID0gYlZhbHVlO1xyXG4gICAgfVxyXG5cclxuICAgIHNldFJHQih4LCB5LCBbciwgZywgYl0pIHtcclxuICAgICAgICBjb25zdCBpbmRleCA9IHRoaXMuZ2V0SW5kZXgoeCwgeSk7XHJcbiAgICAgICAgdGhpcy5kYXRhW2luZGV4XSA9IHI7XHJcbiAgICAgICAgdGhpcy5kYXRhW2luZGV4ICsgMV0gPSBnO1xyXG4gICAgICAgIHRoaXMuZGF0YVtpbmRleCArIDJdID0gYjtcclxuICAgIH1cclxuXHJcbn1cclxuXHJcbi8qKlxyXG4gKlxyXG4gKiBAcGFyYW0gd2lkdGggbnVtYmVyXHJcbiAqIEBwYXJhbSBoZWlnaHQgbnVtYmVyXHJcbiAqIEByZXR1cm5zIHtJbWFnZX1cclxuICovXHJcbmZ1bmN0aW9uIGVtcHR5KHdpZHRoLCBoZWlnaHQpIHtcclxuICAgIGNvbnN0IGJ1ZmZlciA9IG5ldyBBcnJheUJ1ZmZlcih3aWR0aCAqIGhlaWdodCAqIDQpO1xyXG4gICAgY29uc3QgZGF0YSA9IG5ldyBVaW50OENsYW1wZWRBcnJheShidWZmZXIpO1xyXG4gICAgY29uc3QgaW1hZ2VEYXRhID0gbmV3IEltYWdlRGF0YShkYXRhLCB3aWR0aCwgaGVpZ2h0KTtcclxuXHJcbiAgICAvLyBTZXQgYWxwaGEgdG8gMjU1XHJcbiAgICBmb3IgKGxldCBpID0gMzsgaSA8ICh3aWR0aCAqIGhlaWdodCAqIDQpOyBpICs9IDQpIHtcclxuICAgICAgICBkYXRhW2ldID0gMjU1O1xyXG4gICAgfVxyXG5cclxuICAgIHJldHVybiBuZXcgSW1hZ2UoaW1hZ2VEYXRhKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIEBwYXJhbSBpbWFnZSB7SW1hZ2V9XHJcbiAqIEByZXR1cm5zIHtJbWFnZX1cclxuICovXHJcbmZ1bmN0aW9uIGNsb25lKGltYWdlKSB7XHJcbiAgICBjb25zdCBidWZmZXIgPSBuZXcgQXJyYXlCdWZmZXIoaW1hZ2Uud2lkdGggKiBpbWFnZS5oZWlnaHQgKiA0KTtcclxuICAgIGNvbnN0IGRhdGEgPSBuZXcgVWludDhDbGFtcGVkQXJyYXkoYnVmZmVyKTtcclxuICAgIGNvbnN0IGltYWdlRGF0YSA9IG5ldyBJbWFnZURhdGEoZGF0YSwgaW1hZ2Uud2lkdGgsIGltYWdlLmhlaWdodCk7XHJcblxyXG4gICAgZm9yIChsZXQgaSA9IDA7IGkgPCBkYXRhLmxlbmd0aDsgaSsrKSB7XHJcbiAgICAgICAgZGF0YVtpXSA9IGltYWdlLmRhdGFbaV07XHJcbiAgICB9XHJcblxyXG4gICAgcmV0dXJuIG5ldyBJbWFnZShpbWFnZURhdGEpO1xyXG5cclxufVxyXG5cclxuXHJcbmNsYXNzIEVuZXJneUltYWdlIHtcclxuICAgIGNvbnN0cnVjdG9yKGRhdGEzMmJpdCwgd2lkdGgsIGhlaWdodCkge1xyXG4gICAgICAgIHRoaXMuZGF0YTMyYml0ID0gZGF0YTMyYml0O1xyXG4gICAgICAgIHRoaXMud2lkdGggPSB3aWR0aDtcclxuICAgICAgICB0aGlzLmhlaWdodCA9IGhlaWdodDtcclxuICAgIH1cclxuXHJcbiAgICBzZXRWYWx1ZSh4LCB5LCB2YWx1ZSkge1xyXG4gICAgICAgIHRoaXMuZGF0YTMyYml0W3RoaXMuZ2V0SW5kZXgoeCwgeSldID0gdmFsdWU7XHJcbiAgICB9XHJcblxyXG4gICAgZ2V0VmFsdWUoeCwgeSkge1xyXG4gICAgICAgIHJldHVybiB0aGlzLmRhdGEzMmJpdFt0aGlzLmdldEluZGV4KHgsIHkpXTtcclxuICAgIH1cclxuXHJcbiAgICBnZXRJbmRleCh4LCB5KSB7XHJcbiAgICAgICAgaWYgKHggPCAwIHx8IHggPj0gdGhpcy53aWR0aCkge1xyXG4gICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoXCJ4IFwiICsgeCArIFwiIGlzIG5vdCBiZXR3ZWVuIDAgYW5kIFwiICsgdGhpcy53aWR0aCk7XHJcbiAgICAgICAgfWVsc2UgaWYgKHkgPCAwIHx8IHkgPj0gdGhpcy5oZWlnaHQpIHtcclxuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKFwieSBcIiArIHkgKyBcIiBpcyBub3QgYmV0d2VlbiAwIGFuZCBcIiArIHRoaXMuaGVpZ2h0KTtcclxuICAgICAgICB9XHJcbiAgICAgICAgcmV0dXJuIHkgKiB0aGlzLndpZHRoICsgeDtcclxuICAgIH1cclxufVxyXG5cclxuZnVuY3Rpb24gY3JlYXRlRW5lcmd5SW1hZ2Uod2lkdGgsIGhlaWdodCkge1xyXG4gICAgY29uc3QgYnVmZmVyID0gbmV3IEFycmF5QnVmZmVyKHdpZHRoICogaGVpZ2h0ICogNCk7XHJcbiAgICBjb25zdCBkYXRhMzJCaXQgPSBuZXcgVWludDMyQXJyYXkoYnVmZmVyKTtcclxuXHJcbiAgICByZXR1cm4gbmV3IEVuZXJneUltYWdlKGRhdGEzMkJpdCwgd2lkdGgsIGhlaWdodCk7XHJcbn1cclxuLypcclxuZnVuY3Rpb24gY2xvbmVFbmVyZ3lJbWFnZShlbmVyZ3lJbWFnZSkge1xyXG5cclxuICAgIGNvbnN0IGJ1ZmZlciA9IG5ldyBBcnJheUJ1ZmZlcihlbmVyZ3lJbWFnZS53aWR0aCAqIGVuZXJneUltYWdlLmhlaWdodCAqIDQpO1xyXG4gICAgY29uc3QgZGF0YTMyQml0ID0gbmV3IFVpbnQzMkFycmF5KGJ1ZmZlcik7XHJcblxyXG4gICAgZm9yIChsZXQgaSA9IDA7IGkgPCBkYXRhMzJCaXQubGVuZ3RoOyBpKyspIHtcclxuICAgICAgICBkYXRhMzJCaXRbaV0gPSBlbmVyZ3lJbWFnZS5kYXRhMzJiaXRbaV07XHJcbiAgICB9XHJcblxyXG4gICAgcmV0dXJuIG5ldyBFbmVyZ3lJbWFnZShkYXRhMzJCaXQsIGVuZXJneUltYWdlLndpZHRoLCBlbmVyZ3lJbWFnZS5oZWlnaHQpO1xyXG59Ki9cclxuXHJcbm1vZHVsZS5leHBvcnRzID0ge1xyXG4gICAgSW1hZ2UsXHJcbiAgICBlbXB0eSxcclxuICAgIGNsb25lLFxyXG4gICAgRW5lcmd5SW1hZ2UsXHJcbiAgICBjcmVhdGVFbmVyZ3lJbWFnZSxcclxuICAgIC8vIGNsb25lRW5lcmd5SW1hZ2VcclxufTtcclxuIiwiY29uc3QgSW1hZ2UgPSByZXF1aXJlKCcuLi9JbWFnZScpO1xyXG5cclxuLyoqXHJcbiAqIEBwYXJhbSBpbWFnZSB7SW1hZ2V9XHJcbiAqIEBwYXJhbSByYWRpdXMgTnVtYmVyXHJcbiAqIEByZXR1cm5zIHtJbWFnZX1cclxuICovXHJcbmZ1bmN0aW9uIGJveGJsdXIoaW1hZ2UsIHJhZGl1cykge1xyXG4gICAgcmFkaXVzID0gTnVtYmVyKHJhZGl1cyk7XHJcbiAgICBjb25zdCBuZXdJbWFnZSA9IEltYWdlLmVtcHR5KGltYWdlLndpZHRoLCBpbWFnZS5oZWlnaHQpO1xyXG5cclxuICAgIGZvciAobGV0IHggPSByYWRpdXM7IHggPCBpbWFnZS53aWR0aC1yYWRpdXM7IHgrKykge1xyXG4gICAgICAgIGZvciAobGV0IHkgPSByYWRpdXM7IHkgPCBpbWFnZS5oZWlnaHQtcmFkaXVzOyB5KyspIHtcclxuICAgICAgICAgICAgbGV0IHN1bVIgPSAwO1xyXG4gICAgICAgICAgICBsZXQgc3VtRyA9IDA7XHJcbiAgICAgICAgICAgIGxldCBzdW1CID0gMDtcclxuXHJcbiAgICAgICAgICAgIGZvciAobGV0IGkgPSB4LXJhZGl1czsgaSA8PSB4K3JhZGl1czsgaSsrKSB7XHJcbiAgICAgICAgICAgICAgICBmb3IgKGxldCBqID0geS1yYWRpdXM7IGogPD0geStyYWRpdXM7IGorKykge1xyXG4gICAgICAgICAgICAgICAgICAgIHN1bVIgPSBzdW1SICsgaW1hZ2UuZ2V0UihpLCBqKTtcclxuICAgICAgICAgICAgICAgICAgICBzdW1HID0gc3VtRyArIGltYWdlLmdldEcoaSwgaik7XHJcbiAgICAgICAgICAgICAgICAgICAgc3VtQiA9IHN1bUIgKyBpbWFnZS5nZXRCKGksIGopO1xyXG4gICAgICAgICAgICAgICAgfVxyXG4gICAgICAgICAgICB9XHJcblxyXG4gICAgICAgICAgICBuZXdJbWFnZS5zZXRSKHgsIHksIHN1bVIvTWF0aC5wb3cocmFkaXVzKjIrMSwyKSk7XHJcbiAgICAgICAgICAgIG5ld0ltYWdlLnNldEcoeCwgeSwgc3VtRy9NYXRoLnBvdyhyYWRpdXMqMisxLDIpKTtcclxuICAgICAgICAgICAgbmV3SW1hZ2Uuc2V0Qih4LCB5LCBzdW1CL01hdGgucG93KHJhZGl1cyoyKzEsMikpO1xyXG4gICAgICAgIH1cclxuICAgIH1cclxuICAgIHJldHVybiBuZXdJbWFnZTtcclxufVxyXG5cclxubW9kdWxlLmV4cG9ydHMgPSBib3hibHVyO1xyXG4iLCJjb25zdCBJbWFnZSA9IHJlcXVpcmUoJy4uL0ltYWdlJyk7XHJcblxyXG4vKipcclxuICogVGFrZXMgYW4gaW1hZ2Ugd2l0aCBSR0IgdmFsdWVzLCBhbmQgY29udmVydHMgaXQgdG8gZ3JleXNjYWxlXHJcbiAqIGJ5IGNhbGN1bGF0aW5nIGludGVuc2l0eSBhdCBlYWNoIHBpeGVsXHJcbiAqXHJcbiAqIEBwYXJhbSBpbWFnZSB7SW1hZ2V9XHJcbiAqIEByZXR1cm5zIHtJbWFnZX1cclxuICovXHJcbmZ1bmN0aW9uIGdyZXlzY2FsZShpbWFnZSkge1xyXG4gICAgY29uc3QgbmV3SW1hZ2UgPSBJbWFnZS5lbXB0eShpbWFnZS53aWR0aCwgaW1hZ2UuaGVpZ2h0KTtcclxuXHJcbiAgICBmb3IgKGxldCB5ID0gMDsgeSA8IGltYWdlLmhlaWdodDsgeSsrKSB7XHJcbiAgICAgICAgZm9yIChsZXQgeCA9IDA7IHggPCBpbWFnZS53aWR0aDsgeCsrKSB7XHJcblxyXG4gICAgICAgICAgICBjb25zdCByID0gaW1hZ2UuZ2V0Uih4LCB5KTtcclxuICAgICAgICAgICAgY29uc3QgZyA9IGltYWdlLmdldEcoeCwgeSk7XHJcbiAgICAgICAgICAgIGNvbnN0IGIgPSBpbWFnZS5nZXRCKHgsIHkpO1xyXG5cclxuICAgICAgICAgICAgY29uc3QgaW50ZW5zaXR5ID0gMC4zNCAqIHIgKyAwLjUgKiBnICsgMC4xNiAqIGI7XHJcblxyXG4gICAgICAgICAgICBuZXdJbWFnZS5zZXRSKHgsIHksIGludGVuc2l0eSk7XHJcbiAgICAgICAgICAgIG5ld0ltYWdlLnNldEcoeCwgeSwgaW50ZW5zaXR5KTtcclxuICAgICAgICAgICAgbmV3SW1hZ2Uuc2V0Qih4LCB5LCBpbnRlbnNpdHkpO1xyXG5cclxuICAgICAgICAgICAgLyogZXZ0XHJcbiAgICAgICAgICAgIGNvbnN0IFtyLCBnLCBiXSA9IGltYWdlLmdldFJHQih4LCB5KTtcclxuICAgICAgICAgICAgY29uc3QgYyA9IDAuMzQgKiByICsgMC41ICogZyArIDAuMTYgKiBiO1xyXG4gICAgICAgICAgICBuZXdJbWFnZS5zZXRSR0IoeCwgeSwgW2MsIGMsIGNdKTsqL1xyXG4gICAgICAgIH1cclxuICAgIH1cclxuICAgIHJldHVybiBuZXdJbWFnZTtcclxufVxyXG5cclxubW9kdWxlLmV4cG9ydHMgPSBncmV5c2NhbGU7IiwiY29uc3QgSW1hZ2UgPSByZXF1aXJlKCcuLi9JbWFnZScpO1xyXG5cclxuLyoqXHJcbiAqIEBwYXJhbSBpbWFnZSB7SW1hZ2V9XHJcbiAqIEByZXR1cm5zIHtJbWFnZX1cclxuICovXHJcbmZ1bmN0aW9uIGhpc3RvZ3JhbWVxdWFsaXphdGlvbihpbWFnZSkge1xyXG4gICAgY29uc3QgbmV3SW1hZ2UgPSBJbWFnZS5lbXB0eShpbWFnZS53aWR0aCwgaW1hZ2UuaGVpZ2h0KTtcclxuXHJcbiAgICBjb25zdCBuciA9IEFycmF5KDI1NikuZmlsbCgwKTtcclxuICAgIGNvbnN0IG5nID0gQXJyYXkoMjU2KS5maWxsKDApO1xyXG4gICAgY29uc3QgbmIgPSBBcnJheSgyNTYpLmZpbGwoMCk7XHJcblxyXG4gICAgY29uc3Qgc3IgPSBBcnJheSgyNTYpLmZpbGwoMCk7XHJcbiAgICBjb25zdCBzZyA9IEFycmF5KDI1NikuZmlsbCgwKTtcclxuICAgIGNvbnN0IHNiID0gQXJyYXkoMjU2KS5maWxsKDApO1xyXG5cclxuICAgIC8vIENvdW50aW5nIG51bWJlciBvZiBwaXhlbHMgd2l0aCBpbnRlbnNpdHkgZnJvbSAwIHRvIDI1NVxyXG4gICAgLy8gbnJbMF0gaXMgdGhlIG51bWJlciBvZiBwaXhlbHMgd2l0aCBSLXZhbHVlIHRoYXQgaGFzIGFuIGludGVuc2l0eSBvZiAwLCBuclsxXSBpcyB0aGUgbnVtYmVyIG9mIHBpeGVscyB3aXRoIFItdmFsdWUgd2l0aCBpbnRlbnNpdHkgMSBldGMuXHJcbiAgICAvLyBTaW1pbGlhciBmb3IgbmcgYW5kIG5iXHJcbiAgICBmb3IgKGxldCB4ID0gMDsgeCA8IGltYWdlLndpZHRoOyB4KyspIHtcclxuICAgICAgICBmb3IgKGxldCB5ID0gMDsgeSA8IGltYWdlLmhlaWdodDsgeSsrKSB7XHJcbiAgICAgICAgICAgIG5yW2ltYWdlLmdldFIoeCwgeSldKys7XHJcbiAgICAgICAgICAgIG5nW2ltYWdlLmdldEcoeCwgeSldKys7XHJcbiAgICAgICAgICAgIG5iW2ltYWdlLmdldEIoeCwgeSldKys7XHJcbiAgICAgICAgfVxyXG4gICAgfVxyXG5cclxuICAgIC8vIHNyW2tdIGlzIHRoZSBuZXcgaW50ZW5zaXR5IGZvciBhbGwgdGhlIHBpeGVscyB3aXRoIFItdmFsdWUgZXF1YWwgdG8gaW50ZW5zaXR5IGtcclxuICAgIGZvciAobGV0IGsgPSAwOyBrIDwgMjU2OyBrKyspIHtcclxuICAgICAgICBmb3IgKGxldCBqID0gMDsgaiA8PSBrOyBqKyspIHtcclxuICAgICAgICAgICAgc3Jba10gPSBzcltrXSArIG5yW2pdO1xyXG4gICAgICAgICAgICBzZ1trXSA9IHNnW2tdICsgbmdbal07XHJcbiAgICAgICAgICAgIHNiW2tdID0gc2Jba10gKyBuYltqXTtcclxuICAgICAgICB9XHJcbiAgICAgICAgc3Jba10gPSBNYXRoLnJvdW5kKHNyW2tdKjI1NS8oaW1hZ2UuaGVpZ2h0KmltYWdlLndpZHRoKSk7XHJcbiAgICAgICAgc2dba10gPSBNYXRoLnJvdW5kKHNnW2tdKjI1NS8oaW1hZ2UuaGVpZ2h0KmltYWdlLndpZHRoKSk7XHJcbiAgICAgICAgc2Jba10gPSBNYXRoLnJvdW5kKHNiW2tdKjI1NS8oaW1hZ2UuaGVpZ2h0KmltYWdlLndpZHRoKSk7XHJcbiAgICB9XHJcblxyXG4gICAgLy8gVHJhbmZvcm1pbmcgYWxsIHBpeGVscyBhbmQgdGhlaXIgUkdCLXZhbHVlcyB0byB0aGVpciBuZXcgaGlzdG9ncmFtZXF1YWxpemVkIHZhbHVlcy5cclxuICAgIGZvciAobGV0IHggPSAwOyB4IDwgaW1hZ2Uud2lkdGg7IHgrKykge1xyXG4gICAgICAgIGZvciAobGV0IHkgPSAwOyB5IDwgaW1hZ2UuaGVpZ2h0OyB5KyspIHtcclxuICAgICAgICAgICAgbmV3SW1hZ2Uuc2V0Uih4LCB5LCBzcltpbWFnZS5nZXRSKHgsIHkpXSk7XHJcbiAgICAgICAgICAgIG5ld0ltYWdlLnNldEcoeCwgeSwgc2dbaW1hZ2UuZ2V0Ryh4LCB5KV0pO1xyXG4gICAgICAgICAgICBuZXdJbWFnZS5zZXRCKHgsIHksIHNiW2ltYWdlLmdldEIoeCwgeSldKTtcclxuICAgICAgICB9XHJcbiAgICB9XHJcblxyXG4gICAgcmV0dXJuIG5ld0ltYWdlO1xyXG59XHJcblxyXG5tb2R1bGUuZXhwb3J0cyA9IGhpc3RvZ3JhbWVxdWFsaXphdGlvbjtcclxuIiwiY29uc3QgSW1hZ2UgPSByZXF1aXJlKCcuLi9JbWFnZScpO1xyXG5cclxuLyoqXHJcbiAqIFRha2VzIGFuIGltYWdlIHdpdGggUkdCIHZhbHVlcywgYW5kIGludmVydHMgdGhlIHZhbHVlIG9mIGVhY2ggcGl4ZWwsIGkuZSAyNTUgLSBwaXhlbCB2YWx1ZS5cclxuICpcclxuICogQHBhcmFtIGltYWdlIHtJbWFnZX1cclxuICogQHJldHVybnMge0ltYWdlfVxyXG4gKi9cclxuZnVuY3Rpb24gaW52ZXJ0KGltYWdlKSB7XHJcbiAgICBjb25zdCBuZXdJbWFnZSA9IEltYWdlLmVtcHR5KGltYWdlLndpZHRoLCBpbWFnZS5oZWlnaHQpO1xyXG5cclxuICAgIGZvciAobGV0IHggPSAwOyB4IDwgaW1hZ2Uud2lkdGg7IHgrKykge1xyXG4gICAgICAgIGZvciAobGV0IHkgPSAwOyB5IDwgaW1hZ2UuaGVpZ2h0OyB5KyspIHtcclxuXHJcbiAgICAgICAgICAgIGNvbnN0IHIgPSAyNTUgLSBpbWFnZS5nZXRSKHgsIHkpO1xyXG4gICAgICAgICAgICBjb25zdCBnID0gMjU1IC0gaW1hZ2UuZ2V0Ryh4LCB5KTtcclxuICAgICAgICAgICAgY29uc3QgYiA9IDI1NSAtIGltYWdlLmdldEIoeCwgeSk7XHJcblxyXG4gICAgICAgICAgICBuZXdJbWFnZS5zZXRSR0IoeCwgeSwgW3IsIGcsIGJdKTtcclxuICAgICAgICB9XHJcbiAgICB9XHJcbiAgICByZXR1cm4gbmV3SW1hZ2U7XHJcbn1cclxuXHJcbm1vZHVsZS5leHBvcnRzID0gaW52ZXJ0O1xyXG4iLCJjb25zdCBJbWFnZSA9IHJlcXVpcmUoJy4uL0ltYWdlJyk7XHJcblxyXG4vKipcclxuICogQHBhcmFtIGltYWdlIHtJbWFnZX1cclxuICogQHBhcmFtIHJhZGl1cyBOdW1iZXJcclxuICogQHJldHVybnMge0ltYWdlfVxyXG4gKi9cclxuZnVuY3Rpb24gbWVkaWFuZmlsdGVyKGltYWdlLCByYWRpdXMpIHtcclxuICAgIHJhZGl1cyA9IE51bWJlcihyYWRpdXMpO1xyXG4gICAgY29uc3QgbWVkaWFuID0gTWF0aC50cnVuYyhNYXRoLnBvdygyKnJhZGl1cysxLCAyKS8yKTtcclxuICAgIGNvbnN0IG5ld0ltYWdlID0gSW1hZ2UuZW1wdHkoaW1hZ2Uud2lkdGgsIGltYWdlLmhlaWdodCk7XHJcblxyXG4gICAgY29uc3QgbWVkaWFuciA9IEFycmF5KE1hdGgucG93KDIqcmFkaXVzKzEsIDIpKS5maWxsKDApO1xyXG4gICAgY29uc3QgbWVkaWFuZyA9IEFycmF5KE1hdGgucG93KDIqcmFkaXVzKzEsIDIpKS5maWxsKDApO1xyXG4gICAgY29uc3QgbWVkaWFuYiA9IEFycmF5KE1hdGgucG93KDIqcmFkaXVzKzEsIDIpKS5maWxsKDApO1xyXG5cclxuICAgIGZvciAobGV0IHggPSByYWRpdXM7IHggPCBpbWFnZS53aWR0aC1yYWRpdXM7IHgrKykge1xyXG4gICAgICAgIGZvciAobGV0IHkgPSByYWRpdXM7IHkgPCBpbWFnZS5oZWlnaHQtcmFkaXVzOyB5KyspIHtcclxuXHJcbiAgICAgICAgICAgIGxldCBjb3VudCA9IDA7XHJcbiAgICAgICAgICAgIGZvciAobGV0IGkgPSB4LXJhZGl1czsgaSA8PSB4K3JhZGl1czsgaSsrKSB7XHJcbiAgICAgICAgICAgICAgICBmb3IgKGxldCBqID0geS1yYWRpdXM7IGogPD0geStyYWRpdXM7IGorKykge1xyXG4gICAgICAgICAgICAgICAgICAgIG1lZGlhbnJbY291bnRdID0gaW1hZ2UuZ2V0UihpLCBqKTtcclxuICAgICAgICAgICAgICAgICAgICBtZWRpYW5nW2NvdW50XSA9IGltYWdlLmdldEcoaSwgaik7XHJcbiAgICAgICAgICAgICAgICAgICAgbWVkaWFuYltjb3VudF0gPSBpbWFnZS5nZXRCKGksIGopO1xyXG4gICAgICAgICAgICAgICAgICAgIGNvdW50Kys7XHJcbiAgICAgICAgICAgICAgICB9XHJcbiAgICAgICAgICAgIH1cclxuXHJcbiAgICAgICAgICAgIG1lZGlhbnIuc29ydCgoYSwgYikgPT4gYSAtIGIpO1xyXG4gICAgICAgICAgICBtZWRpYW5nLnNvcnQoKGEsIGIpID0+IGEgLSBiKTtcclxuICAgICAgICAgICAgbWVkaWFuYi5zb3J0KChhLCBiKSA9PiBhIC0gYik7XHJcblxyXG4gICAgICAgICAgICBuZXdJbWFnZS5zZXRSKHgsIHksIG1lZGlhbnJbbWVkaWFuXSk7XHJcbiAgICAgICAgICAgIG5ld0ltYWdlLnNldEcoeCwgeSwgbWVkaWFuZ1ttZWRpYW5dKTtcclxuICAgICAgICAgICAgbmV3SW1hZ2Uuc2V0Qih4LCB5LCBtZWRpYW5iW21lZGlhbl0pO1xyXG4gICAgICAgIH1cclxuICAgIH1cclxuICAgIHJldHVybiBuZXdJbWFnZTtcclxufVxyXG5cclxubW9kdWxlLmV4cG9ydHMgPSBtZWRpYW5maWx0ZXI7XHJcbiIsImNvbnN0IEltYWdlID0gcmVxdWlyZSgnLi4vSW1hZ2UnKTtcclxuXHJcbi8qXHJcbiAqIElNUExFTUVOVCBpbWFnZUVuZXJneSguLikgYW5kIGNhbGN1bGF0ZVNlYW1zKC4uKSBmdW5jdGlvbnNcclxuICovXHJcblxyXG4vKipcclxuICogVGFrZXMgYW4gUkdCIGltYWdlLCByZXR1cm5zIGEgbmV3IGltYWdlIHdpdGggZW5lcmd5bGV2ZWxzIHBlciBwaXhlbFxyXG4gKiBAcGFyYW0gaW1hZ2Uge0ltYWdlfVxyXG4gKiBAcmV0dXJucyB7RW5lcmd5SW1hZ2V9XHJcbiAqL1xyXG5mdW5jdGlvbiBpbWFnZUVuZXJneShpbWFnZSkge1xyXG4gICAgY29uc3QgZW5lcmd5SW1hZ2UgPSBJbWFnZS5jcmVhdGVFbmVyZ3lJbWFnZShpbWFnZS53aWR0aCwgaW1hZ2UuaGVpZ2h0KTtcclxuXHJcbiAgICBmb3IgKGxldCB5ID0gMDsgeSA8IGltYWdlLmhlaWdodDsgeSsrKSB7XHJcbiAgICAgICAgZm9yIChsZXQgeCA9IDA7IHggPCBpbWFnZS53aWR0aDsgeCsrKSB7XHJcbiAgICAgICAgICAgIGxldCBlbmVyZ3k7XHJcbiAgICAgICAgICAgIGlmIChpc0JvcmRlclBpeGVsKHgsIHksIGltYWdlLndpZHRoLCBpbWFnZS5oZWlnaHQpKSB7XHJcbiAgICAgICAgICAgICAgICBlbmVyZ3kgPSAzMDA7XHJcbiAgICAgICAgICAgIH0gZWxzZSB7XHJcbiAgICAgICAgICAgICAgICBlbmVyZ3kgPSBNYXRoLnNxcnQoXHJcbiAgICAgICAgICAgICAgICAgICAgTWF0aC5wb3coaW1hZ2UuZ2V0Uih4ICsgMSwgeSkgLSBpbWFnZS5nZXRSKHggLSAxLCB5KSwgMikgK1xyXG4gICAgICAgICAgICAgICAgICAgIE1hdGgucG93KGltYWdlLmdldEcoeCArIDEsIHkpIC0gaW1hZ2UuZ2V0Ryh4IC0gMSwgeSksIDIpICtcclxuICAgICAgICAgICAgICAgICAgICBNYXRoLnBvdyhpbWFnZS5nZXRCKHggKyAxLCB5KSAtIGltYWdlLmdldEIoeCAtIDEsIHkpLCAyKSArXHJcblxyXG4gICAgICAgICAgICAgICAgICAgIE1hdGgucG93KGltYWdlLmdldFIoeCwgeSArIDEpIC0gaW1hZ2UuZ2V0Uih4LCB5IC0gMSksIDIpICtcclxuICAgICAgICAgICAgICAgICAgICBNYXRoLnBvdyhpbWFnZS5nZXRHKHgsIHkgKyAxKSAtIGltYWdlLmdldEcoeCwgeSAtIDEpLCAyKSArXHJcbiAgICAgICAgICAgICAgICAgICAgTWF0aC5wb3coaW1hZ2UuZ2V0Qih4LCB5ICsgMSkgLSBpbWFnZS5nZXRCKHgsIHkgLSAxKSwgMilcclxuICAgICAgICAgICAgICAgICk7XHJcblxyXG4gICAgICAgICAgICB9XHJcblxyXG4gICAgICAgICAgICBlbmVyZ3lJbWFnZS5zZXRWYWx1ZSh4LCB5LCBlbmVyZ3kpO1xyXG4gICAgICAgIH1cclxuICAgIH1cclxuICAgIHJldHVybiBlbmVyZ3lJbWFnZTtcclxufVxyXG5cclxuZnVuY3Rpb24gaXNCb3JkZXJQaXhlbCh4LCB5LCBpbWFnZVdpZHRoLCBpbWFnZUhlaWdodCkge1xyXG4gICAgcmV0dXJuIHggPT09IDAgfHwgeCA9PT0gaW1hZ2VXaWR0aCAtIDEgfHwgeSA9PT0gMCB8fCB5ID09PSBpbWFnZUhlaWdodCAtIDE7XHJcbn1cclxuXHJcblxyXG4vKipcclxuICogVGFrZXMgYW4gZW5lcmd5SW1hZ2Ugd2l0aCBlbmVyZ3lsZXZlbHMgcGVyIHBpeGVsLCBhbmQgdXNlcyBkeW5hbWljIHByb2dyYW1taW5nXHJcbiAqIHRvIGZpbmQgcGF0aHMgZnJvbSB0b3AgdG8gYm90dG9tIHdpdGggdGhlIGxlYXN0IGVuZXJneVxyXG4gKiBAcGFyYW0gZW5lcmd5SW1hZ2Uge0VuZXJneUltYWdlfVxyXG4gKi9cclxuZnVuY3Rpb24gY2FsY3VsYXRlU2VhbXMoZW5lcmd5SW1hZ2UpIHtcclxuICAgIGNvbnN0IHNlYW1JbWFnZSA9IEltYWdlLmNyZWF0ZUVuZXJneUltYWdlKGVuZXJneUltYWdlLndpZHRoLCBlbmVyZ3lJbWFnZS5oZWlnaHQpO1xyXG5cclxuICAgIGZvciAobGV0IHkgPSAwOyB5IDwgc2VhbUltYWdlLmhlaWdodDsgeSsrKSB7XHJcbiAgICAgICAgZm9yIChsZXQgeCA9IDA7IHggPCBzZWFtSW1hZ2Uud2lkdGg7IHgrKykge1xyXG4gICAgICAgICAgICBjb25zdCBlbmVyZ3lBdFB4ID0gZW5lcmd5SW1hZ2UuZ2V0VmFsdWUoeCwgeSk7XHJcblxyXG4gICAgICAgICAgICBpZiAoeSA9PT0gMCkge1xyXG4gICAgICAgICAgICAgICAgc2VhbUltYWdlLnNldFZhbHVlKHgsIHksIGVuZXJneUF0UHgpO1xyXG4gICAgICAgICAgICAgICAgY29udGludWU7XHJcbiAgICAgICAgICAgIH1cclxuXHJcbiAgICAgICAgICAgIGNvbnN0IG1pblBhcmVudCA9IE1hdGgubWluKHggLSAxID49IDAgPyBzZWFtSW1hZ2UuZ2V0VmFsdWUoeCAtIDEsIHkgLSAxKSA6IDk5OTk5LCBzZWFtSW1hZ2UuZ2V0VmFsdWUoeCwgeSAtIDEpLCB4ICsgMSA8IHNlYW1JbWFnZS53aWR0aCA/IHNlYW1JbWFnZS5nZXRWYWx1ZSh4ICsgMSwgeSAtIDEpIDogOTk5OTkpO1xyXG4gICAgICAgICAgICBzZWFtSW1hZ2Uuc2V0VmFsdWUoeCwgeSwgZW5lcmd5QXRQeCArIG1pblBhcmVudCk7XHJcbiAgICAgICAgfVxyXG4gICAgfVxyXG4gICAgcmV0dXJuIHNlYW1JbWFnZTtcclxufVxyXG5cclxuXHJcbi8qXHJcbiAqXHJcbiAqIFNUVUZGIGJlbG93IGFscmVhZHkgaW1wbGVtZW50ZWQgZm9yIHlvdSA6KVxyXG4gKlxyXG4gKi9cclxuXHJcblxyXG4vKipcclxuICogQWZ0ZXIgYWxsIHRoZSBwYXRocyBhcmUgY2FsY3VsYXRlZCwgZmluZCB0aGUgbG93ZXN0IG9uZSBvbiB0aGUgbGFzdCByb3dcclxuICogYW5kIG1vdmUgYmFjayB1cCwga2VlcGluZyB0cmFjayBvZiB0aGUgcGF0aFxyXG4gKi9cclxuZnVuY3Rpb24gZmluZE1pblNlYW0oc2VhbXMpIHtcclxuICAgIGNvbnN0IHBvc2l0aW9ucyA9IFtdO1xyXG5cclxuICAgIC8vIGZpbmQgbG93ZXN0IHBvc1xyXG4gICAgbGV0IGxvd2VzdCA9IDk5OTk5LCBsb3dlc3RJbmRleCA9IDA7XHJcbiAgICBmb3IgKGxldCB4ID0gMDsgeCA8IHNlYW1zLndpZHRoOyB4KyspIHtcclxuICAgICAgICBjb25zdCB2YWx1ZSA9IHNlYW1zLmdldFZhbHVlKHgsIHNlYW1zLmhlaWdodCAtIDEpO1xyXG4gICAgICAgIGlmICh2YWx1ZSA8IGxvd2VzdCkge1xyXG4gICAgICAgICAgICBsb3dlc3QgPSB2YWx1ZTtcclxuICAgICAgICAgICAgbG93ZXN0SW5kZXggPSB4O1xyXG4gICAgICAgIH1cclxuICAgIH1cclxuICAgIHBvc2l0aW9uc1tzZWFtcy5oZWlnaHQgLSAxXSA9IGxvd2VzdEluZGV4O1xyXG5cclxuICAgIC8vIGl0ZXJhdGUgdXB3YXJkc1xyXG4gICAgZm9yIChsZXQgeSA9IHNlYW1zLmhlaWdodCAtIDI7IHkgPj0gMDsgeS0tKSB7XHJcbiAgICAgICAgbGV0IGxvd2VzdFBhcmVudCA9IDk5OTk5OSwgbG93ZXN0UGFyZW50SW5kZXggPSAwO1xyXG4gICAgICAgIGlmIChsb3dlc3RJbmRleCAtIDEgPj0gMCkge1xyXG4gICAgICAgICAgICBsb3dlc3RQYXJlbnQgPSBzZWFtcy5nZXRWYWx1ZShsb3dlc3RJbmRleCAtIDEsIHkpO1xyXG4gICAgICAgICAgICBsb3dlc3RQYXJlbnRJbmRleCA9IGxvd2VzdEluZGV4IC0gMTtcclxuICAgICAgICB9XHJcblxyXG4gICAgICAgIGlmIChzZWFtcy5nZXRWYWx1ZShsb3dlc3RJbmRleCwgeSkgPCBsb3dlc3RQYXJlbnQpIHtcclxuICAgICAgICAgICAgbG93ZXN0UGFyZW50ID0gc2VhbXMuZ2V0VmFsdWUobG93ZXN0SW5kZXgsIHkpO1xyXG4gICAgICAgICAgICBsb3dlc3RQYXJlbnRJbmRleCA9IGxvd2VzdEluZGV4O1xyXG4gICAgICAgIH1cclxuXHJcbiAgICAgICAgaWYgKGxvd2VzdEluZGV4ICsgMSA8IHNlYW1zLndpZHRoICYmIHNlYW1zLmdldFZhbHVlKGxvd2VzdEluZGV4ICsgMSwgeSkgPCBsb3dlc3RQYXJlbnQpIHtcclxuICAgICAgICAgICAgbG93ZXN0UGFyZW50ID0gc2VhbXMuZ2V0VmFsdWUobG93ZXN0SW5kZXggKyAxLCB5KTtcclxuICAgICAgICAgICAgbG93ZXN0UGFyZW50SW5kZXggPSBsb3dlc3RJbmRleCArIDE7XHJcbiAgICAgICAgfVxyXG5cclxuICAgICAgICBwb3NpdGlvbnNbeV0gPSBsb3dlc3RQYXJlbnRJbmRleDtcclxuICAgICAgICBsb3dlc3RJbmRleCA9IGxvd2VzdFBhcmVudEluZGV4O1xyXG4gICAgfVxyXG5cclxuICAgIHJldHVybiBwb3NpdGlvbnM7XHJcbn1cclxuXHJcblxyXG4vKipcclxuICogQ3JlYXRlcyBhIG5ldyBpbWFnZSwgb25lIHBpeGVsIHNtYWxsZXIsIHRoYXQgY29udGFpbnMgZXZlcnl0aGluZ1xyXG4gKiBmcm9tIHRoZSBvcmlnaW5hbCBpbWFnZSwgZXhjZXB0IHRoZSBwaXhlbCBvbiBlYWNoIGxpbmUgZm91bmQgZnJvbSBzZWFtIGNhcnZpbmcgdG8gcmVtb3ZlXHJcbiAqIEBwYXJhbSBpbWFnZSB7SW1hZ2V9XHJcbiAqIEBwYXJhbSBzZWFtUG9zIHtOdW1iZXJbXX0geCB2YWx1ZSBmb3IgcGl4ZWwgdG8gcmVtb3ZlIG9uIGVhY2ggcm93XHJcbiAqIEByZXR1cm5zIHtJbWFnZX1cclxuICovXHJcbmZ1bmN0aW9uIHJlbW92ZVNlYW0oaW1hZ2UsIHNlYW1Qb3MpIHtcclxuICAgIGNvbnN0IG5ld0ltYWdlID0gSW1hZ2UuZW1wdHkoaW1hZ2Uud2lkdGggLSAxLCBpbWFnZS5oZWlnaHQpO1xyXG5cclxuXHJcbiAgICBmb3IgKGxldCB5ID0gMDsgeSA8IG5ld0ltYWdlLmhlaWdodDsgeSsrKSB7XHJcbiAgICAgICAgY29uc3QgeFRvUmVtb3ZlID0gc2VhbVBvc1t5XTtcclxuICAgICAgICBmb3IgKGxldCB4ID0gMDsgeCA8IG5ld0ltYWdlLndpZHRoOyB4KyspIHtcclxuICAgICAgICAgICAgaWYgKHggPCB4VG9SZW1vdmUpIHtcclxuICAgICAgICAgICAgICAgIG5ld0ltYWdlLnNldFJHQih4LCB5LCBpbWFnZS5nZXRSR0IoeCwgeSkpO1xyXG4gICAgICAgICAgICB9IGVsc2Uge1xyXG4gICAgICAgICAgICAgICAgbmV3SW1hZ2Uuc2V0UkdCKHgsIHksIGltYWdlLmdldFJHQih4ICsgMSwgeSkpO1xyXG4gICAgICAgICAgICB9XHJcblxyXG4gICAgICAgIH1cclxuICAgIH1cclxuICAgIHJldHVybiBuZXdJbWFnZTtcclxufVxyXG5cclxuLyoqXHJcbiAqIE1hcmtzIHRoZSBmb3VuZCBzZWFtIGFzIGEgcmVkIHBhdGggb24gdGhlIGltYWdlXHJcbiAqIE5vdCBwYXJ0IG9mIHRoZSBhbGdvcml0aG0sIGp1c3QgdXNlZCB0byBzaG93IGluIHRoZSBVSSAvIGRlYnVnZ2luZ1xyXG4gKi9cclxuZnVuY3Rpb24gc2hvd1NlYW0oaW1hZ2UsIHNlYW1Qb3MpIHtcclxuICAgIGNvbnN0IG5ld0ltYWdlID0gSW1hZ2UuY2xvbmUoaW1hZ2UpO1xyXG5cclxuICAgIGZvciAobGV0IHkgPSAwOyB5IDwgaW1hZ2UuaGVpZ2h0OyB5KyspIHtcclxuICAgICAgICBuZXdJbWFnZS5zZXRSR0Ioc2VhbVBvc1t5XSwgeSwgWzI1NSwgMCwgMF0pO1xyXG4gICAgfVxyXG5cclxuICAgIHJldHVybiBuZXdJbWFnZTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFNob3dpbmcgdGhlIGVuZXJneSBhcyBncmV5c2NhbGUgaW1hZ2UgZm9yIGRlYnVnZ2luZ1xyXG4gKiBOb3QgcGFydCBvZiB0aGUgYWxnb3JpdGhtLCBqdXN0IHRvIHNob3cgaW4gdGhlIFVJIC8gZGVidWdnaW5nXHJcbiAqL1xyXG5mdW5jdGlvbiBzaG93RW5lcmd5SW1hZ2UoZW5lcmd5KSB7XHJcbiAgICBjb25zdCBpbWFnZSA9IEltYWdlLmVtcHR5KGVuZXJneS53aWR0aCwgZW5lcmd5LmhlaWdodCk7XHJcblxyXG4gICAgZm9yIChsZXQgeCA9IDA7IHggPCBpbWFnZS53aWR0aDsgeCsrKSB7XHJcbiAgICAgICAgZm9yIChsZXQgeSA9IDA7IHkgPCBpbWFnZS5oZWlnaHQ7IHkrKykge1xyXG4gICAgICAgICAgICBjb25zdCBjID0gTWF0aC5jZWlsKChlbmVyZ3kuZ2V0VmFsdWUoeCwgeSkgLyAzMDApICogMjU1KTtcclxuXHJcbiAgICAgICAgICAgIGltYWdlLnNldFJHQih4LCB5LCBbYywgYywgY10pO1xyXG4gICAgICAgIH1cclxuICAgIH1cclxuXHJcbiAgICByZXR1cm4gaW1hZ2U7XHJcbn1cclxuXHJcbm1vZHVsZS5leHBvcnRzID0ge1xyXG4gICAgaW1hZ2VFbmVyZ3ksXHJcbiAgICBzaG93RW5lcmd5SW1hZ2UsXHJcbiAgICBjYWxjdWxhdGVTZWFtcyxcclxuICAgIGZpbmRNaW5TZWFtLFxyXG4gICAgc2hvd1NlYW0sXHJcbiAgICByZW1vdmVTZWFtXHJcbn07IiwiY29uc3QgSW1hZ2UgPSByZXF1aXJlKCcuLi9JbWFnZScpO1xyXG5jb25zdCBib3hibHVyID0gcmVxdWlyZSgnLi9ib3hibHVyJyk7XHJcblxyXG4vKipcclxuICogQHBhcmFtIGltYWdlIHtJbWFnZX1cclxuICogQHJldHVybnMge0ltYWdlfVxyXG4gKi9cclxuZnVuY3Rpb24gc2hhcnBlbihpbWFnZSkge1xyXG5cclxuICAgIGNvbnN0IGJsdXJyZWRJbWFnZSA9IGJveGJsdXIoaW1hZ2UsIDMpO1xyXG4gICAgY29uc3QgbmV3SW1hZ2UgPSBJbWFnZS5lbXB0eShpbWFnZS53aWR0aCwgaW1hZ2UuaGVpZ2h0KTtcclxuXHJcbiAgICBmb3IgKGxldCB4ID0gMDsgeCA8IGltYWdlLndpZHRoOyB4KyspIHtcclxuICAgICAgICBmb3IgKGxldCB5ID0gMDsgeSA8IGltYWdlLmhlaWdodDsgeSsrKSB7XHJcblxyXG4gICAgICAgICAgICBjb25zdCByID0gaW1hZ2UuZ2V0Uih4LCB5KSArIDMqKGltYWdlLmdldFIoeCx5KSAtIGJsdXJyZWRJbWFnZS5nZXRSKHgsIHkpKTtcclxuICAgICAgICAgICAgY29uc3QgZyA9IGltYWdlLmdldEcoeCwgeSkgKyAzKihpbWFnZS5nZXRHKHgseSkgLSBibHVycmVkSW1hZ2UuZ2V0Ryh4LCB5KSk7XHJcbiAgICAgICAgICAgIGNvbnN0IGIgPSBpbWFnZS5nZXRCKHgsIHkpICsgMyooaW1hZ2UuZ2V0Qih4LHkpIC0gYmx1cnJlZEltYWdlLmdldEIoeCwgeSkpO1xyXG5cclxuICAgICAgICAgICAgbmV3SW1hZ2Uuc2V0UkdCKHgsIHksIFtyLCBnLCBiXSk7XHJcbiAgICAgICAgfVxyXG4gICAgfVxyXG5cclxuICAgIHJldHVybiBuZXdJbWFnZTtcclxufVxyXG5cclxubW9kdWxlLmV4cG9ydHMgPSBzaGFycGVuO1xyXG4iLCJjb25zdCBJbWFnZSA9IHJlcXVpcmUoJy4uL0ltYWdlJyk7XHJcblxyXG4vKipcclxuICogVGFrZXMgYW4gaW1hZ2Ugd2l0aCBSR0IgdmFsdWVzLCBhbmQgdHVybnMgdGhlIHBpeGVscyBlaXRoZXIgd2hpdGUgKDI1NSwyNTUsMjU1KSBvciBibGFjayAoMCwwLDApXHJcbiAqIGRlcGVuZGluZyBvbiBpZiB0aGUgaW50ZW5zaXR5IGlzIGFib3ZlIG9yIGJlbG93IHRoZSB0aHJlc2hvbGRcclxuICpcclxuICogQHBhcmFtIGltYWdlIHtJbWFnZX1cclxuICogQHBhcmFtIHRocmVzaG9sZCBOdW1iZXJcclxuICogQHJldHVybnMge0ltYWdlfVxyXG4gKi9cclxuZnVuY3Rpb24gdGhyZXNob2xkKGltYWdlLCB0aHJlc2hvbGQpIHtcclxuICAgIGNvbnN0IG5ld0ltYWdlID0gSW1hZ2UuZW1wdHkoaW1hZ2Uud2lkdGgsIGltYWdlLmhlaWdodCk7XHJcblxyXG4gICAgZm9yIChsZXQgeCA9IDA7IHggPCBpbWFnZS53aWR0aDsgeCsrKSB7XHJcbiAgICAgICAgZm9yIChsZXQgeSA9IDA7IHkgPCBpbWFnZS5oZWlnaHQ7IHkrKykge1xyXG5cclxuICAgICAgICAgICAgY29uc3QgciA9IGltYWdlLmdldFIoeCwgeSk7XHJcbiAgICAgICAgICAgIGNvbnN0IGcgPSBpbWFnZS5nZXRHKHgsIHkpO1xyXG4gICAgICAgICAgICBjb25zdCBiID0gaW1hZ2UuZ2V0Qih4LCB5KTtcclxuXHJcbiAgICAgICAgICAgIGxldCBjID0gMDtcclxuICAgICAgICAgICAgaWYgKChyICsgZyArIGIpIC8gMyA+IHRocmVzaG9sZCkge1xyXG4gICAgICAgICAgICAgICAgYyA9IDI1NTtcclxuICAgICAgICAgICAgfVxyXG4gICAgICAgICAgICBuZXdJbWFnZS5zZXRSR0IoeCwgeSwgW2MsIGMsIGNdKTtcclxuICAgICAgICB9XHJcbiAgICB9XHJcbiAgICByZXR1cm4gbmV3SW1hZ2U7XHJcbn1cclxuXHJcbm1vZHVsZS5leHBvcnRzID0gdGhyZXNob2xkOyIsImNvbnN0IEltYWdlID0gcmVxdWlyZSgnLi4vSW1hZ2UnKTtcclxuXHJcbi8qKlxyXG4gKiBBZGRzIHNvbWUgbW9yZSByZWQgYW5kIHJlbW92ZXMgc29tZSBibHVlIGZyb20gdGhlIGltYWdlJ3MgcGl4ZWxzXHJcbiAqIEBwYXJhbSBpbWFnZSB7SW1hZ2V9XHJcbiAqIEByZXR1cm5zIHtJbWFnZX1cclxuICovXHJcbmZ1bmN0aW9uIHdhcm1maWx0ZXIoaW1hZ2UpIHtcclxuICAgIGNvbnN0IG5ld0ltYWdlID0gSW1hZ2UuZW1wdHkoaW1hZ2Uud2lkdGgsIGltYWdlLmhlaWdodCk7XHJcblxyXG4gICAgZm9yIChsZXQgeCA9IDA7IHggPCBpbWFnZS53aWR0aDsgeCsrKSB7XHJcbiAgICAgICAgZm9yIChsZXQgeSA9IDA7IHkgPCBpbWFnZS5oZWlnaHQ7IHkrKykge1xyXG5cclxuICAgICAgICAgICAgY29uc3QgciA9IGltYWdlLmdldFIoeCwgeSk7XHJcbiAgICAgICAgICAgIGNvbnN0IGcgPSBpbWFnZS5nZXRHKHgsIHkpO1xyXG4gICAgICAgICAgICBjb25zdCBiID0gaW1hZ2UuZ2V0Qih4LCB5KTtcclxuXHJcbiAgICAgICAgICAgIG5ld0ltYWdlLnNldFIoeCwgeSwgciArIDI1KTtcclxuICAgICAgICAgICAgbmV3SW1hZ2Uuc2V0Ryh4LCB5LCBnKTtcclxuICAgICAgICAgICAgbmV3SW1hZ2Uuc2V0Qih4LCB5LCBiIC0gMjUpO1xyXG4gICAgICAgIH1cclxuICAgIH1cclxuICAgIHJldHVybiBuZXdJbWFnZTtcclxufVxyXG5cclxubW9kdWxlLmV4cG9ydHMgPSB3YXJtZmlsdGVyO1xyXG4iLCJjb25zdCBFZGl0b3IgPSByZXF1aXJlKCcuL0VkaXRvcicpO1xyXG5jb25zdCBncmV5c2NhbGUgPSByZXF1aXJlKCcuL2VmZmVjdHMvZ3JleXNjYWxlJyk7XHJcbmNvbnN0IGNvbG9yZmlsdGVyID0gcmVxdWlyZSgnLi9lZmZlY3RzL3dhcm1maWx0ZXInKTtcclxuY29uc3QgdGhyZXNob2xkID0gcmVxdWlyZSgnLi9lZmZlY3RzL3RocmVzaG9sZCcpO1xyXG5jb25zdCBib3hibHVyID0gcmVxdWlyZSgnLi9lZmZlY3RzL2JveGJsdXInKTtcclxuY29uc3QgaW52ZXJ0ID0gcmVxdWlyZSgnLi9lZmZlY3RzL2ludmVydCcpO1xyXG5jb25zdCBoaXN0b2dyYW1lcXVhbGl6YXRpb24gPSByZXF1aXJlKCcuL2VmZmVjdHMvaGlzdG9ncmFtZXF1YWxpemF0aW9uJyk7XHJcbmNvbnN0IG1lZGlhbmZpbHRlciA9IHJlcXVpcmUoJy4vZWZmZWN0cy9tZWRpYW5maWx0ZXInKTtcclxuY29uc3Qgc2hhcnBlbiA9IHJlcXVpcmUoJy4vZWZmZWN0cy9zaGFycGVuJyk7XHJcbmNvbnN0IHNlYW0gPSByZXF1aXJlKCcuL2VmZmVjdHMvc2VhbWNhcnZpbmcnKTtcclxuXHJcblxyXG5jb25zdCBlZGl0b3IgPSBuZXcgRWRpdG9yKCk7XHJcbmVkaXRvci5sb2FkSW1hZ2UoJ3Rvd2VyLmpwZycpO1xyXG5cclxuZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcIiNpbWFnZXNcIikuYWRkRXZlbnRMaXN0ZW5lcignY2hhbmdlJywgKCkgPT4ge1xyXG4gICAgY29uc3Qgc2VsZWN0ZWRJbWFnZSA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXCIjaW1hZ2VzXCIpLnZhbHVlO1xyXG4gICAgZWRpdG9yLmxvYWRJbWFnZShzZWxlY3RlZEltYWdlKTtcclxufSk7XHJcblxyXG5kb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiI2xvYWRcIikuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcbiAgICBjb25zdCBzZWxlY3RlZEltYWdlID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcIiNpbWFnZXNcIikudmFsdWU7XHJcbiAgICBlZGl0b3IubG9hZEltYWdlKHNlbGVjdGVkSW1hZ2UpO1xyXG59KTtcclxuXHJcbmRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXCIjdW5kb1wiKS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHtcclxuICAgIGVkaXRvci51bmRvRWZmZWN0KCk7XHJcbn0pO1xyXG5cclxuZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcIiN0aHJlc2hvbGRcIikuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcbiAgICBlZGl0b3IuYXBwbHlFZmZlY3QodGhyZXNob2xkLCBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiI3RocmVzaG9sZHZhbHVlXCIpLnZhbHVlKTtcclxufSk7XHJcblxyXG5kb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiI2dyZXlzY2FsZVwiKS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHtcclxuICAgIGVkaXRvci5hcHBseUVmZmVjdChncmV5c2NhbGUpO1xyXG59KTtcclxuXHJcbmRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXCIjd2FybWZpbHRlclwiKS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHtcclxuICAgIGVkaXRvci5hcHBseUVmZmVjdChjb2xvcmZpbHRlcik7XHJcbn0pO1xyXG5cclxuZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcIiNib3hibHVyXCIpLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4ge1xyXG4gICAgZWRpdG9yLmFwcGx5RWZmZWN0KGJveGJsdXIsIGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXCIjYmx1cnJhZGl1c1wiKS52YWx1ZSk7XHJcbn0pO1xyXG5cclxuZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcIiNtZWRpYW5maWx0ZXJcIikuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcbiAgICBlZGl0b3IuYXBwbHlFZmZlY3QobWVkaWFuZmlsdGVyLCBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiI3JhZGl1c1wiKS52YWx1ZSk7XHJcbn0pO1xyXG5cclxuZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcIiNpbnZlcnRcIikuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcbiAgICBlZGl0b3IuYXBwbHlFZmZlY3QoaW52ZXJ0KTtcclxufSk7XHJcblxyXG5kb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiI3NoYXJwZW5cIikuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcbiAgICBlZGl0b3IuYXBwbHlFZmZlY3Qoc2hhcnBlbik7XHJcbn0pO1xyXG5cclxuZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcIiNoaXN0b2dyYW1lcXVhbGl6YXRpb25cIikuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcbiAgICBlZGl0b3IuYXBwbHlFZmZlY3QoaGlzdG9ncmFtZXF1YWxpemF0aW9uKTtcclxufSk7XHJcblxyXG5kb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiI2VuZXJneVwiKS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHtcclxuICAgIGVkaXRvci5hcHBseUVmZmVjdCgoaW1hZ2UpID0+IHtcclxuICAgICAgICByZXR1cm4gc2VhbS5zaG93RW5lcmd5SW1hZ2Uoc2VhbS5pbWFnZUVuZXJneShpbWFnZSkpO1xyXG4gICAgfSk7XHJcbn0pO1xyXG5kb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiI2ZpbmRTZWFtXCIpLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4ge1xyXG4gICAgZWRpdG9yLmFwcGx5RWZmZWN0KChpbWFnZSkgPT4ge1xyXG4gICAgICAgIGNvbnN0IGVuZXJneSA9IHNlYW0uaW1hZ2VFbmVyZ3koaW1hZ2UpO1xyXG4gICAgICAgIGNvbnN0IHNlYW1zID0gc2VhbS5jYWxjdWxhdGVTZWFtcyhlbmVyZ3kpO1xyXG4gICAgICAgIGNvbnN0IG1pblNlYW0gPSBzZWFtLmZpbmRNaW5TZWFtKHNlYW1zKTtcclxuICAgICAgICBjb25zdCBzaG93U2VhbSA9IHNlYW0uc2hvd1NlYW0oaW1hZ2UsIG1pblNlYW0pO1xyXG4gICAgICAgIHJldHVybiBzaG93U2VhbTtcclxuICAgIH0pO1xyXG59KTtcclxuXHJcblxyXG5sZXQgaXNSdW5uaW5nID0gZmFsc2U7XHJcbmRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXCIjcnVuU2VhbWNhcnZlclwiKS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHtcclxuICAgIGlzUnVubmluZyA9IHRydWU7XHJcbiAgICBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiI3J1blNlYW1jYXJ2ZXJcIikuZGlzYWJsZWQgPSB0cnVlO1xyXG4gICAgZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcIiNzdG9wU2VhbWNhcnZlclwiKS5kaXNhYmxlZCA9IGZhbHNlO1xyXG5cclxuICAgIGxldCBpbWFnZSA9IGVkaXRvci5jdXJyZW50O1xyXG5cclxuICAgIGZ1bmN0aW9uIHNlYW1DYXJ2ZSgpIHtcclxuXHJcbiAgICAgICAgY29uc3QgZW5lcmd5ID0gc2VhbS5pbWFnZUVuZXJneShpbWFnZSk7XHJcbiAgICAgICAgY29uc3Qgc2VhbXMgPSBzZWFtLmNhbGN1bGF0ZVNlYW1zKGVuZXJneSk7XHJcbiAgICAgICAgY29uc3QgbWluU2VhbSA9IHNlYW0uZmluZE1pblNlYW0oc2VhbXMpO1xyXG4gICAgICAgIGNvbnN0IHNob3dTZWFtID0gc2VhbS5zaG93U2VhbShpbWFnZSwgbWluU2VhbSk7XHJcblxyXG4gICAgICAgIGVkaXRvci5yZW5kZXIoc2hvd1NlYW0sIFwiZWRpdGVkXCIpO1xyXG5cclxuICAgICAgICBpbWFnZSA9IHNlYW0ucmVtb3ZlU2VhbShpbWFnZSwgbWluU2VhbSk7XHJcblxyXG4gICAgICAgIGlmIChpc1J1bm5pbmcpIHtcclxuICAgICAgICAgICAgc2V0VGltZW91dChzZWFtQ2FydmUsIDEwKTtcclxuICAgICAgICB9IGVsc2Uge1xyXG4gICAgICAgICAgICBlZGl0b3Iuc2V0Q3VycmVudChpbWFnZSk7XHJcbiAgICAgICAgfVxyXG4gICAgfVxyXG5cclxuICAgIHNlYW1DYXJ2ZSgpO1xyXG59KTtcclxuXHJcbmRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXCIjc3RvcFNlYW1jYXJ2ZXJcIikuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcbiAgICBpc1J1bm5pbmcgPSBmYWxzZTtcclxuICAgIGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXCIjcnVuU2VhbWNhcnZlclwiKS5kaXNhYmxlZCA9IGZhbHNlO1xyXG4gICAgZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcIiNzdG9wU2VhbWNhcnZlclwiKS5kaXNhYmxlZCA9IHRydWU7XHJcbn0pO1xyXG5cclxuXHJcbiJdfQ==
--------------------------------------------------------------------------------
/docs/low-contrast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/low-contrast.png
--------------------------------------------------------------------------------
/docs/noisy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/noisy.png
--------------------------------------------------------------------------------
/docs/simple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/simple.png
--------------------------------------------------------------------------------
/docs/styles.css:
--------------------------------------------------------------------------------
1 | .ui {
2 | margin: 20px 0;
3 | }
4 |
5 | .ui .group {
6 | display: inline-block;
7 | padding: 0px 0px;
8 | margin-right: 10px;
9 | /*border: 1px solid #ccc;*/
10 | }
11 |
12 | .notgroup {
13 | margin-right: 10px;
14 | }
15 |
16 | .ui .uirow {
17 | margin-top: 15px;
18 | }
--------------------------------------------------------------------------------
/docs/supernoisy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/supernoisy.jpg
--------------------------------------------------------------------------------
/docs/tower.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/docs/tower.jpg
--------------------------------------------------------------------------------
/images/earring.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/images/earring.jpg
--------------------------------------------------------------------------------
/images/low-contrast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/images/low-contrast.png
--------------------------------------------------------------------------------
/images/noisy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/images/noisy.png
--------------------------------------------------------------------------------
/images/simple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/images/simple.png
--------------------------------------------------------------------------------
/images/supernoisy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/images/supernoisy.jpg
--------------------------------------------------------------------------------
/images/tower.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matsemann/image-workshop/97354a0dc67ce51d3f15e866e7a669c6494e1e57/images/tower.jpg
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "image-workshop",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "dev": "budo src/index.js --dir src --dir images --live"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/Matsemann/image-workshop.git"
13 | },
14 | "author": "",
15 | "license": "ISC",
16 | "bugs": {
17 | "url": "https://github.com/Matsemann/image-workshop/issues"
18 | },
19 | "homepage": "https://github.com/Matsemann/image-workshop#readme",
20 | "devDependencies": {
21 | "budo": "11.5.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Editor.js:
--------------------------------------------------------------------------------
1 | const ImageData = require('./Image');
2 |
3 | const MAX_WIDTH = 1200;
4 |
5 | class Editor {
6 |
7 |
8 | constructor() {
9 | }
10 |
11 | loadImage(imageName) {
12 | const loader = new Image();
13 | loader.onload = () => {
14 | const imageData = this.getImageData(loader);
15 | this.setNewImage(imageData);
16 | };
17 | loader.src = imageName;
18 | }
19 |
20 | getImageData(loader) {
21 | const loaderCanvas = document.querySelector("#loaderCanvas");
22 | const loaderContext = loaderCanvas.getContext('2d');
23 |
24 | // Draw scaled image to invisible canvas and read back the pixel data
25 | const [width, height] = scaleSize(loader.width, loader.height);
26 | loaderCanvas.width = width;
27 | loaderCanvas.height = height;
28 |
29 | loaderContext.drawImage(loader, 0, 0, width, height);
30 | const data = loaderContext.getImageData(0, 0, width, height);
31 |
32 | return new ImageData.Image(data);
33 | }
34 |
35 | setNewImage(image) {
36 | this.original = image;
37 | this.current = ImageData.clone(image);
38 | this.olds = [this.current];
39 | this.render(this.original, "original");
40 | this.render(this.current, "edited");
41 | }
42 |
43 | render(image, canvasName) {
44 | const canvas = document.querySelector("#" + canvasName);
45 | const context = canvas.getContext('2d');
46 |
47 | canvas.width = image.width;
48 | canvas.height = image.height;
49 |
50 | context.putImageData(image.imageData, 0, 0);
51 | }
52 |
53 | applyEffect(effect, ...args) {
54 | this.current = effect(ImageData.clone(this.current), ...args);
55 | this.olds.push(this.current);
56 | this.render(this.current, "edited");
57 | }
58 |
59 | setCurrent(image) {
60 | this.current = image;
61 | this.olds.push(this.current);
62 | this.render(this.current, "edited");
63 | }
64 |
65 | undoEffect() {
66 | if (this.olds.length > 1) {
67 | this.olds.pop();
68 | this.current = this.olds[this.olds.length - 1];
69 | this.render(this.current, "edited");
70 | }
71 | }
72 |
73 |
74 |
75 | }
76 |
77 | function scaleSize(width, height) {
78 | if (width > MAX_WIDTH) {
79 | height = height / (width / MAX_WIDTH);
80 | width = MAX_WIDTH;
81 | }
82 | return [width, height];
83 | }
84 |
85 | module.exports = Editor;
--------------------------------------------------------------------------------
/src/Image.js:
--------------------------------------------------------------------------------
1 | class Image {
2 |
3 | constructor(imageData) {
4 | this.imageData = imageData;
5 | this.data = imageData.data;
6 | this.width = imageData.width;
7 | this.height = imageData.height;
8 | }
9 |
10 | /**
11 | * Data is a single-dimensional array, with 4 values (rgba) per px
12 | */
13 | getIndex(x, y) {
14 | if (x < 0 || x >= this.width) {
15 | throw new Error("x " + x + " is not between 0 and " + this.width);
16 | }else if (y < 0 || y >= this.height) {
17 | throw new Error("y " + y + " is not between 0 and " + this.height);
18 | }
19 | return y * (this.width * 4) + (x * 4);
20 | }
21 |
22 | getR(x, y) {
23 | return this.data[this.getIndex(x, y)];
24 | }
25 |
26 | getG(x, y) {
27 | return this.data[this.getIndex(x, y) + 1];
28 | }
29 |
30 | getB(x, y) {
31 | return this.data[this.getIndex(x, y) + 2];
32 | }
33 |
34 | getRGB(x, y) {
35 | const index = this.getIndex(x, y);
36 | return [this.data[index], this.data[index + 1], this.data[index + 2]];
37 | }
38 |
39 | setR(x, y, rValue) {
40 | this.data[this.getIndex(x, y)] = rValue;
41 | }
42 |
43 | setG(x, y, gValue) {
44 | this.data[this.getIndex(x, y) + 1] = gValue;
45 | }
46 |
47 | setB(x, y, bValue) {
48 | this.data[this.getIndex(x, y) + 2] = bValue;
49 | }
50 |
51 | setRGB(x, y, [r, g, b]) {
52 | const index = this.getIndex(x, y);
53 | this.data[index] = r;
54 | this.data[index + 1] = g;
55 | this.data[index + 2] = b;
56 | }
57 |
58 | }
59 |
60 | /**
61 | *
62 | * @param width number
63 | * @param height number
64 | * @returns {Image}
65 | */
66 | function empty(width, height) {
67 | const buffer = new ArrayBuffer(width * height * 4);
68 | const data = new Uint8ClampedArray(buffer);
69 | const imageData = new ImageData(data, width, height);
70 |
71 | // Set alpha to 255
72 | for (let i = 3; i < (width * height * 4); i += 4) {
73 | data[i] = 255;
74 | }
75 |
76 | return new Image(imageData);
77 | }
78 |
79 | /**
80 | * @param image {Image}
81 | * @returns {Image}
82 | */
83 | function clone(image) {
84 | const buffer = new ArrayBuffer(image.width * image.height * 4);
85 | const data = new Uint8ClampedArray(buffer);
86 | const imageData = new ImageData(data, image.width, image.height);
87 |
88 | for (let i = 0; i < data.length; i++) {
89 | data[i] = image.data[i];
90 | }
91 |
92 | return new Image(imageData);
93 |
94 | }
95 |
96 |
97 | class EnergyImage {
98 | constructor(data32bit, width, height) {
99 | this.data32bit = data32bit;
100 | this.width = width;
101 | this.height = height;
102 | }
103 |
104 | setValue(x, y, value) {
105 | this.data32bit[this.getIndex(x, y)] = value;
106 | }
107 |
108 | getValue(x, y) {
109 | return this.data32bit[this.getIndex(x, y)];
110 | }
111 |
112 | getIndex(x, y) {
113 | if (x < 0 || x >= this.width) {
114 | throw new Error("x " + x + " is not between 0 and " + this.width);
115 | }else if (y < 0 || y >= this.height) {
116 | throw new Error("y " + y + " is not between 0 and " + this.height);
117 | }
118 | return y * this.width + x;
119 | }
120 | }
121 |
122 | function createEnergyImage(width, height) {
123 | const buffer = new ArrayBuffer(width * height * 4);
124 | const data32Bit = new Uint32Array(buffer);
125 |
126 | return new EnergyImage(data32Bit, width, height);
127 | }
128 | /*
129 | function cloneEnergyImage(energyImage) {
130 |
131 | const buffer = new ArrayBuffer(energyImage.width * energyImage.height * 4);
132 | const data32Bit = new Uint32Array(buffer);
133 |
134 | for (let i = 0; i < data32Bit.length; i++) {
135 | data32Bit[i] = energyImage.data32bit[i];
136 | }
137 |
138 | return new EnergyImage(data32Bit, energyImage.width, energyImage.height);
139 | }*/
140 |
141 | module.exports = {
142 | Image,
143 | empty,
144 | clone,
145 | EnergyImage,
146 | createEnergyImage,
147 | // cloneEnergyImage
148 | };
149 |
--------------------------------------------------------------------------------
/src/effects/boxblur.js:
--------------------------------------------------------------------------------
1 | const Image = require('../Image');
2 |
3 | /**
4 | * @param image {Image}
5 | * @param radius Number
6 | * @returns {Image}
7 | */
8 | function boxblur(image, radius) {
9 | radius = Number(radius);
10 | const newImage = Image.empty(image.width, image.height);
11 |
12 | for (let x = radius; x < image.width-radius; x++) {
13 | for (let y = radius; y < image.height-radius; y++) {
14 |
15 | // Create a localized spatial filter, averaging each pixel
16 |
17 | }
18 | }
19 | return newImage;
20 | }
21 |
22 | module.exports = boxblur;
23 |
--------------------------------------------------------------------------------
/src/effects/greyscale.js:
--------------------------------------------------------------------------------
1 | const Image = require('../Image');
2 |
3 | /**
4 | * Takes an image with RGB values, and converts it to greyscale
5 | * by calculating intensity at each pixel
6 | *
7 | * @param image {Image}
8 | * @returns {Image}
9 | */
10 | function greyscale(image) {
11 | const newImage = Image.empty(image.width, image.height);
12 |
13 | // Iterate over all pixels and calculate the intensity
14 |
15 | return newImage;
16 | }
17 |
18 | module.exports = greyscale;
--------------------------------------------------------------------------------
/src/effects/histogramequalization.js:
--------------------------------------------------------------------------------
1 | const Image = require('../Image');
2 |
3 | /**
4 | * @param image {Image}
5 | * @returns {Image}
6 | */
7 | function histogramequalization(image) {
8 | const newImage = Image.empty(image.width, image.height);
9 |
10 | // Step 1. Count the number of pixels with intensity from 0 to 255 per color.
11 |
12 | // Step 2. Calculate the new intensity for all the pixels with intensity equal to k.
13 |
14 | // Step 3. Tranform all pixels and their RGB-values to their new histogramequalized values.
15 |
16 | return newImage;
17 | }
18 |
19 | module.exports = histogramequalization;
20 |
--------------------------------------------------------------------------------
/src/effects/invert.js:
--------------------------------------------------------------------------------
1 | const Image = require('../Image');
2 |
3 | /**
4 | * Takes an image with RGB values, and inverts the value of each pixel, i.e 255 - pixel value.
5 | *
6 | * @param image {Image}
7 | * @returns {Image}
8 | */
9 | function invert(image) {
10 | const newImage = Image.empty(image.width, image.height);
11 |
12 | // Invert the value of each pixel
13 |
14 | return newImage;
15 | }
16 |
17 | module.exports = invert;
18 |
--------------------------------------------------------------------------------
/src/effects/medianfilter.js:
--------------------------------------------------------------------------------
1 | const Image = require('../Image');
2 |
3 | /**
4 | * @param image {Image}
5 | * @param radius Number
6 | * @returns {Image}
7 | */
8 | function medianfilter(image, radius) {
9 | radius = Number(radius);
10 | const newImage = Image.empty(image.width, image.height);
11 |
12 | for (let x = radius; x < image.width-radius; x++) {
13 | for (let y = radius; y < image.height-radius; y++) {
14 |
15 | // Create a localized spatial filter, choosing the median value for each pixel
16 |
17 | }
18 | }
19 | return newImage;
20 | }
21 |
22 | module.exports = medianfilter;
23 |
--------------------------------------------------------------------------------
/src/effects/seamcarving.js:
--------------------------------------------------------------------------------
1 | const Image = require('../Image');
2 |
3 | /*
4 | * IMPLEMENT imageEnergy(..) and calculateSeams(..) functions
5 | */
6 |
7 | /**
8 | * Takes an RGB image, returns a new image with energylevels per pixel
9 | * @param image {Image}
10 | * @returns {EnergyImage}
11 | */
12 | function imageEnergy(image) {
13 | const energyImage = Image.createEnergyImage(image.width, image.height);
14 |
15 | for (let y = 0; y < image.height; y++) {
16 | for (let x = 0; x < image.width; x++) {
17 | let energy;
18 |
19 | // Calculate energy for the pixel
20 |
21 | energyImage.setValue(x, y, energy);
22 | }
23 | }
24 | return energyImage;
25 | }
26 |
27 | function isBorderPixel(x, y, imageWidth, imageHeight) {
28 | return x === 0 || x === imageWidth - 1 || y === 0 || y === imageHeight - 1;
29 | }
30 |
31 |
32 | /**
33 | * Takes an energyImage with energylevels per pixel, and uses dynamic programming
34 | * to find paths from top to bottom with the least energy
35 | * @param energyImage {EnergyImage}
36 | */
37 | function calculateSeams(energyImage) {
38 | const seamImage = Image.createEnergyImage(energyImage.width, energyImage.height);
39 |
40 | // iterate over all rows and columns and calculate the cheapest way to get to the current pixel
41 |
42 | return seamImage;
43 | }
44 |
45 |
46 | /*
47 | *
48 | * STUFF below already implemented for you :)
49 | *
50 | */
51 |
52 |
53 | /**
54 | * After all the paths are calculated, find the lowest one on the last row
55 | * and move back up, keeping track of the path
56 | */
57 | function findMinSeam(seams) {
58 | const positions = [];
59 |
60 | // find lowest pos
61 | let lowest = 99999, lowestIndex = 0;
62 | for (let x = 0; x < seams.width; x++) {
63 | const value = seams.getValue(x, seams.height - 1);
64 | if (value < lowest) {
65 | lowest = value;
66 | lowestIndex = x;
67 | }
68 | }
69 | positions[seams.height - 1] = lowestIndex;
70 |
71 | // iterate upwards
72 | for (let y = seams.height - 2; y >= 0; y--) {
73 | let lowestParent = 999999, lowestParentIndex = 0;
74 | if (lowestIndex - 1 >= 0) {
75 | lowestParent = seams.getValue(lowestIndex - 1, y);
76 | lowestParentIndex = lowestIndex - 1;
77 | }
78 |
79 | if (seams.getValue(lowestIndex, y) < lowestParent) {
80 | lowestParent = seams.getValue(lowestIndex, y);
81 | lowestParentIndex = lowestIndex;
82 | }
83 |
84 | if (lowestIndex + 1 < seams.width && seams.getValue(lowestIndex + 1, y) < lowestParent) {
85 | lowestParent = seams.getValue(lowestIndex + 1, y);
86 | lowestParentIndex = lowestIndex + 1;
87 | }
88 |
89 | positions[y] = lowestParentIndex;
90 | lowestIndex = lowestParentIndex;
91 | }
92 |
93 | return positions;
94 | }
95 |
96 |
97 | /**
98 | * Creates a new image, one pixel smaller, that contains everything
99 | * from the original image, except the pixel on each line found from seam carving to remove
100 | * @param image {Image}
101 | * @param seamPos {Number[]} x value for pixel to remove on each row
102 | * @returns {Image}
103 | */
104 | function removeSeam(image, seamPos) {
105 | const newImage = Image.empty(image.width - 1, image.height);
106 |
107 |
108 | for (let y = 0; y < newImage.height; y++) {
109 | const xToRemove = seamPos[y];
110 | for (let x = 0; x < newImage.width; x++) {
111 | if (x < xToRemove) {
112 | newImage.setRGB(x, y, image.getRGB(x, y));
113 | } else {
114 | newImage.setRGB(x, y, image.getRGB(x + 1, y));
115 | }
116 |
117 | }
118 | }
119 | return newImage;
120 | }
121 |
122 | /**
123 | * Marks the found seam as a red path on the image
124 | * Not part of the algorithm, just used to show in the UI / debugging
125 | */
126 | function showSeam(image, seamPos) {
127 | const newImage = Image.clone(image);
128 |
129 | for (let y = 0; y < image.height; y++) {
130 | newImage.setRGB(seamPos[y], y, [255, 0, 0]);
131 | }
132 |
133 | return newImage;
134 | }
135 |
136 | /**
137 | * Showing the energy as greyscale image for debugging
138 | * Not part of the algorithm, just to show in the UI / debugging
139 | */
140 | function showEnergyImage(energy) {
141 | const image = Image.empty(energy.width, energy.height);
142 |
143 | for (let x = 0; x < image.width; x++) {
144 | for (let y = 0; y < image.height; y++) {
145 | const c = Math.ceil((energy.getValue(x, y) / 300) * 255);
146 |
147 | image.setRGB(x, y, [c, c, c]);
148 | }
149 | }
150 |
151 | return image;
152 | }
153 |
154 | module.exports = {
155 | imageEnergy,
156 | showEnergyImage,
157 | calculateSeams,
158 | findMinSeam,
159 | showSeam,
160 | removeSeam
161 | };
--------------------------------------------------------------------------------
/src/effects/sharpen.js:
--------------------------------------------------------------------------------
1 | const Image = require('../Image');
2 | const boxblur = require('./boxblur');
3 |
4 | /**
5 | * @param image {Image}
6 | * @returns {Image}
7 | */
8 | function sharpen(image) {
9 |
10 | const newImage = Image.empty(image.width, image.height);
11 |
12 | //1. Blur original image
13 |
14 | //2. Find difference between orignal image and blurred version
15 |
16 | //3. Add difference to original image
17 |
18 | return newImage;
19 | }
20 |
21 | module.exports = sharpen;
22 |
--------------------------------------------------------------------------------
/src/effects/threshold.js:
--------------------------------------------------------------------------------
1 | const Image = require('../Image');
2 |
3 | /**
4 | * Takes an image with RGB values, and turns the pixels either white (255,255,255) or black (0,0,0)
5 | * depending on if the intensity is above or below the threshold
6 | *
7 | * @param image {Image}
8 | * @param threshold Number
9 | * @returns {Image}
10 | */
11 | function threshold(image, threshold) {
12 | const newImage = Image.empty(image.width, image.height);
13 |
14 | // Iterate over all pixels and set the pixel to black or white depending on if the intensity is
15 | // above or below the threshold
16 |
17 | return newImage;
18 | }
19 |
20 | module.exports = threshold;
--------------------------------------------------------------------------------
/src/effects/warmfilter.js:
--------------------------------------------------------------------------------
1 | const Image = require('../Image');
2 |
3 | /**
4 | * Adds some more red and removes some blue from the image's pixels
5 | * @param image {Image}
6 | * @returns {Image}
7 | */
8 | function warmfilter(image) {
9 | const newImage = Image.empty(image.width, image.height);
10 |
11 | // Iterate over all pixels and tweak the color values
12 |
13 | return newImage;
14 | }
15 |
16 | module.exports = warmfilter;
17 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AlgPip's Super Image Editor
6 |
7 |
8 |
9 |
10 | AlgPip > Photoshop
11 | Se oppgavene / implementasjonen her
12 |
13 |
14 |
15 |
16 |
17 | tower.jpg
18 | earring.jpg
19 | simple.png
20 | low-contrast.png
21 | noisy.png
22 | supernoisy.jpg
23 |
24 | Reset image
25 | Undo effect
26 |
27 |
28 |
29 |
30 | Greyscale
31 |
32 | Threshold
33 |
34 |
35 | Invert
36 | Warm
37 |
38 |
39 |
40 |
41 | Boxblur
42 |
43 |
44 | Sharpen
45 |
46 | Median filter
47 |
48 |
49 | Histogram equalization
50 |
51 |
52 |
53 |
54 | Show energy
55 | Show seam
56 | Run seam carving
57 | Stop
58 |
59 |
60 |
61 |
62 |
63 | Edited:
64 |
65 |
66 |
67 |
68 | Original:
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const Editor = require('./Editor');
2 | const greyscale = require('./effects/greyscale');
3 | const colorfilter = require('./effects/warmfilter');
4 | const threshold = require('./effects/threshold');
5 | const boxblur = require('./effects/boxblur');
6 | const invert = require('./effects/invert');
7 | const histogramequalization = require('./effects/histogramequalization');
8 | const medianfilter = require('./effects/medianfilter');
9 | const sharpen = require('./effects/sharpen');
10 | const seam = require('./effects/seamcarving');
11 |
12 |
13 | const editor = new Editor();
14 | editor.loadImage('tower.jpg');
15 |
16 | document.querySelector("#images").addEventListener('change', () => {
17 | const selectedImage = document.querySelector("#images").value;
18 | editor.loadImage(selectedImage);
19 | });
20 |
21 | document.querySelector("#load").addEventListener('click', () => {
22 | const selectedImage = document.querySelector("#images").value;
23 | editor.loadImage(selectedImage);
24 | });
25 |
26 | document.querySelector("#undo").addEventListener('click', () => {
27 | editor.undoEffect();
28 | });
29 |
30 | document.querySelector("#threshold").addEventListener('click', () => {
31 | editor.applyEffect(threshold, document.querySelector("#thresholdvalue").value);
32 | });
33 |
34 | document.querySelector("#greyscale").addEventListener('click', () => {
35 | editor.applyEffect(greyscale);
36 | });
37 |
38 | document.querySelector("#warmfilter").addEventListener('click', () => {
39 | editor.applyEffect(colorfilter);
40 | });
41 |
42 | document.querySelector("#boxblur").addEventListener('click', () => {
43 | editor.applyEffect(boxblur, document.querySelector("#blurradius").value);
44 | });
45 |
46 | document.querySelector("#medianfilter").addEventListener('click', () => {
47 | editor.applyEffect(medianfilter, document.querySelector("#radius").value);
48 | });
49 |
50 | document.querySelector("#invert").addEventListener('click', () => {
51 | editor.applyEffect(invert);
52 | });
53 |
54 | document.querySelector("#sharpen").addEventListener('click', () => {
55 | editor.applyEffect(sharpen);
56 | });
57 |
58 | document.querySelector("#histogramequalization").addEventListener('click', () => {
59 | editor.applyEffect(histogramequalization);
60 | });
61 |
62 | document.querySelector("#energy").addEventListener('click', () => {
63 | editor.applyEffect((image) => {
64 | return seam.showEnergyImage(seam.imageEnergy(image));
65 | });
66 | });
67 | document.querySelector("#findSeam").addEventListener('click', () => {
68 | editor.applyEffect((image) => {
69 | const energy = seam.imageEnergy(image);
70 | const seams = seam.calculateSeams(energy);
71 | const minSeam = seam.findMinSeam(seams);
72 | const showSeam = seam.showSeam(image, minSeam);
73 | return showSeam;
74 | });
75 | });
76 |
77 |
78 | let isRunning = false;
79 | document.querySelector("#runSeamcarver").addEventListener('click', () => {
80 | isRunning = true;
81 | document.querySelector("#runSeamcarver").disabled = true;
82 | document.querySelector("#stopSeamcarver").disabled = false;
83 |
84 | let image = editor.current;
85 |
86 | function seamCarve() {
87 |
88 | const energy = seam.imageEnergy(image);
89 | const seams = seam.calculateSeams(energy);
90 | const minSeam = seam.findMinSeam(seams);
91 | const showSeam = seam.showSeam(image, minSeam);
92 |
93 | editor.render(showSeam, "edited");
94 |
95 | image = seam.removeSeam(image, minSeam);
96 |
97 | if (isRunning) {
98 | setTimeout(seamCarve, 10);
99 | } else {
100 | editor.setCurrent(image);
101 | }
102 | }
103 |
104 | seamCarve();
105 | });
106 |
107 | document.querySelector("#stopSeamcarver").addEventListener('click', () => {
108 | isRunning = false;
109 | document.querySelector("#runSeamcarver").disabled = false;
110 | document.querySelector("#stopSeamcarver").disabled = true;
111 | });
112 |
113 |
114 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | .ui {
2 | margin: 20px 0;
3 | }
4 |
5 | .ui .group {
6 | display: inline-block;
7 | padding: 0px 0px;
8 | margin-right: 10px;
9 | /*border: 1px solid #ccc;*/
10 | }
11 |
12 | .notgroup {
13 | margin-right: 10px;
14 | }
15 |
16 | .ui .uirow {
17 | margin-top: 15px;
18 | }
--------------------------------------------------------------------------------