OLD | NEW |
| (Empty) |
1 /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40; -*-
*/ | |
2 | |
3 // The if (0) block of function definitions here tries to use | |
4 // faster math primitives, based on being able to reinterpret | |
5 // floats as ints and vice versa. We do that using the | |
6 // WebGL arrays. | |
7 | |
8 if (0) { | |
9 | |
10 var gConversionBuffer = new ArrayBuffer(4); | |
11 var gFloatConversion = new WebGLFloatArray(gConversionBuffer); | |
12 var gIntConversion = new WebGLIntArray(gConversionBuffer); | |
13 | |
14 function AsFloat(i) { | |
15 gIntConversion[0] = i; | |
16 return gFloatConversion[0]; | |
17 } | |
18 | |
19 function AsInt(f) { | |
20 gFloatConversion[0] = f; | |
21 return gIntConversion[0]; | |
22 } | |
23 | |
24 // magic constants used for various floating point manipulations | |
25 var kMagicFloatToInt = (1 << 23); | |
26 var kOneAsInt = 0x3F800000; | |
27 var kScaleUp = AsFloat(0x00800000); | |
28 var kScaleDown = 1.0 / kScaleUp; | |
29 | |
30 function ToInt(f) { | |
31 // force integer part into lower bits of mantissa | |
32 var i = ReinterpretFloatAsInt(f + kMagicFloatToInt); | |
33 // return lower bits of mantissa | |
34 return i & 0x3FFFFF; | |
35 } | |
36 | |
37 function FastLog2(x) { | |
38 return (AsInt(x) - kOneAsInt) * kScaleDown; | |
39 } | |
40 | |
41 function FastPower(x, p) { | |
42 return AsFloat(p * AsInt(x) + (1.0 - p) * kOneAsInt); | |
43 } | |
44 | |
45 var LOG2_HALF = FastLog2(0.5); | |
46 | |
47 function FastBias(b, x) { | |
48 return FastPower(x, FastLog2(b) / LOG2_HALF); | |
49 } | |
50 | |
51 } else { | |
52 | |
53 function FastLog2(x) { | |
54 return Math.log(x) / Math.LN2; | |
55 } | |
56 | |
57 var LOG2_HALF = FastLog2(0.5); | |
58 | |
59 function FastBias(b, x) { | |
60 return Math.pow(x, FastLog2(b) / LOG2_HALF); | |
61 } | |
62 | |
63 } | |
64 | |
65 function FastGain(g, x) { | |
66 return (x < 0.5) ? | |
67 FastBias(1.0 - g, 2.0 * x) * 0.5 : | |
68 1.0 - FastBias(1.0 - g, 2.0 - 2.0 * x) * 0.5; | |
69 } | |
70 | |
71 function Clamp(x) { | |
72 return (x < 0.0) ? 0.0 : ((x > 1.0) ? 1.0 : x); | |
73 } | |
74 | |
75 function ProcessImageData(imageData, params) { | |
76 var saturation = params.saturation; | |
77 var contrast = params.contrast; | |
78 var brightness = params.brightness; | |
79 var blackPoint = params.blackPoint; | |
80 var fill = params.fill; | |
81 var temperature = params.temperature; | |
82 var shadowsHue = params.shadowsHue; | |
83 var shadowsSaturation = params.shadowsSaturation; | |
84 var highlightsHue = params.highlightsHue; | |
85 var highlightsSaturation = params.highlightsSaturation; | |
86 var splitPoint = params.splitPoint; | |
87 | |
88 var brightness_a, brightness_b; | |
89 var oo255 = 1.0 / 255.0; | |
90 | |
91 // do some adjustments | |
92 fill *= 0.2; | |
93 brightness = (brightness - 1.0) * 0.75 + 1.0; | |
94 if (brightness < 1.0) { | |
95 brightness_a = brightness; | |
96 brightness_b = 0.0; | |
97 } else { | |
98 brightness_b = brightness - 1.0; | |
99 brightness_a = 1.0 - brightness_b; | |
100 } | |
101 contrast = contrast * 0.5; | |
102 contrast = (contrast - 0.5) * 0.75 + 0.5; | |
103 temperature = (temperature / 2000.0) * 0.1; | |
104 if (temperature > 0.0) temperature *= 2.0; | |
105 splitPoint = ((splitPoint + 1.0) * 0.5); | |
106 | |
107 // apply to pixels | |
108 var sz = imageData.width * imageData.height; | |
109 var data = imageData.data; | |
110 for (var j = 0; j < sz; j++) { | |
111 var r = data[j*4+0] * oo255; | |
112 var g = data[j*4+1] * oo255; | |
113 var b = data[j*4+2] * oo255; | |
114 // convert RGB to YIQ | |
115 // this is a less than ideal colorspace; | |
116 // HSL would probably be better, but more expensive | |
117 var y = 0.299 * r + 0.587 * g + 0.114 * b; | |
118 var i = 0.596 * r - 0.275 * g - 0.321 * b; | |
119 var q = 0.212 * r - 0.523 * g + 0.311 * b; | |
120 i = i + temperature; | |
121 q = q - temperature; | |
122 i = i * saturation; | |
123 q = q * saturation; | |
124 y = (1.0 + blackPoint) * y - blackPoint; | |
125 y = y + fill; | |
126 y = y * brightness_a + brightness_b; | |
127 y = FastGain(contrast, Clamp(y)); | |
128 | |
129 if (y < splitPoint) { | |
130 q = q + (shadowsHue * shadowsSaturation) * (splitPoint - y); | |
131 } else { | |
132 i = i + (highlightsHue * highlightsSaturation) * (y - splitPoint); | |
133 } | |
134 | |
135 // convert back to RGB for display | |
136 r = y + 0.956 * i + 0.621 * q; | |
137 g = y - 0.272 * i - 0.647 * q; | |
138 b = y - 1.105 * i + 1.702 * q; | |
139 | |
140 // clamping is "free" as part of the ImageData object | |
141 data[j*4+0] = r * 255.0; | |
142 data[j*4+1] = g * 255.0; | |
143 data[j*4+2] = b * 255.0; | |
144 } | |
145 } | |
146 | |
147 // | |
148 // UI code | |
149 // | |
150 | |
151 var gFullCanvas = null; | |
152 var gFullContext = null; | |
153 var gFullImage = null; | |
154 var gDisplayCanvas = null; | |
155 var gDisplayContext = null; | |
156 var gZoomPoint = null; | |
157 var gDisplaySize = null; | |
158 var gZoomSize = [600, 600]; | |
159 var gMouseStart = null; | |
160 var gMouseOrig = [0, 0]; | |
161 var gDirty = true; | |
162 | |
163 // If true, apply image correction to the original | |
164 // source image before scaling down; if false, | |
165 // scale down first. | |
166 var gCorrectBefore = false; | |
167 | |
168 var gParams = null; | |
169 var gIgnoreChanges = true; | |
170 | |
171 function OnSliderChanged() { | |
172 if (gIgnoreChanges) | |
173 return; | |
174 | |
175 gDirty = true; | |
176 | |
177 gParams = {}; | |
178 | |
179 // The values will come in as 0.0 .. 1.0; some params want | |
180 // a different range. | |
181 var ranges = { | |
182 "saturation": [0, 2], | |
183 "contrast": [0, 2], | |
184 "brightness": [0, 2], | |
185 "temperature": [-2000, 2000], | |
186 "splitPoint": [-1, 1] | |
187 }; | |
188 | |
189 $(".slider").each(function(index, e) { | |
190 var val = Math.floor($(e).slider("value")) / 1000.0; | |
191 var id = e.getAttribute("id"); | |
192 if (id in ranges) | |
193 val = val * (ranges[id][1] - ranges[id][0]) + ranges[id]
[0]; | |
194 gParams[id] = val; | |
195 }); | |
196 | |
197 Redisplay(); | |
198 } | |
199 | |
200 function ClampZoomPointToTranslation() { | |
201 var tx = gZoomPoint[0] - gZoomSize[0]/2; | |
202 var ty = gZoomPoint[1] - gZoomSize[1]/2; | |
203 tx = Math.max(0, tx); | |
204 ty = Math.max(0, ty); | |
205 | |
206 if (tx + gZoomSize[0] > gFullImage.width) | |
207 tx = gFullImage.width - gZoomSize[0]; | |
208 if (ty + gZoomSize[1] > gFullImage.height) | |
209 ty = gFullImage.height - gZoomSize[1]; | |
210 return [tx, ty]; | |
211 } | |
212 | |
213 function Redisplay() { | |
214 if (!gParams) | |
215 return; | |
216 | |
217 var angle = | |
218 (gParams.angle*2.0 - 1.0) * 90.0 + | |
219 (gParams.fineangle*2.0 - 1.0) * 2.0; | |
220 | |
221 angle = Math.max(-90, Math.min(90, angle)); | |
222 angle = (angle * Math.PI) / 180.0; | |
223 | |
224 var processTime; | |
225 var processWidth, processHeight; | |
226 | |
227 var t0 = (new Date()).getTime(); | |
228 | |
229 // Render the image with rotation; we only need to render | |
230 // if we're either correcting just the portion that's visible, | |
231 // or if we're correcting the full thing and the sliders have been | |
232 // changed. Otherwise, what's in the full canvas is already corrected | |
233 // and correct. | |
234 if ((gCorrectBefore && gDirty) || | |
235 !gCorrectBefore) | |
236 { | |
237 gFullContext.save(); | |
238 gFullContext.translate(Math.floor(gFullImage.width / 2), Math.floor(gFullIma
ge.height / 2)); | |
239 gFullContext.rotate(angle); | |
240 gFullContext.globalCompositeOperation = "copy"; | |
241 gFullContext.drawImage(gFullImage, | |
242 -Math.floor(gFullImage.width / 2), | |
243 -Math.floor(gFullImage.height / 2)); | |
244 gFullContext.restore(); | |
245 } | |
246 | |
247 function FullToDisplay() { | |
248 gDisplayContext.save(); | |
249 if (gZoomPoint) { | |
250 var pt = ClampZoomPointToTranslation(); | |
251 | |
252 gDisplayContext.translate(-pt[0], -pt[1]); | |
253 } else { | |
254 gDisplayContext.translate(0, 0); | |
255 var ratio = gDisplaySize[0] / gFullCanvas.width; | |
256 gDisplayContext.scale(ratio, ratio); | |
257 } | |
258 | |
259 gDisplayContext.globalCompositeOperation = "copy"; | |
260 gDisplayContext.drawImage(gFullCanvas, 0, 0); | |
261 gDisplayContext.restore(); | |
262 } | |
263 | |
264 function ProcessCanvas(cx, canvas) { | |
265 var ts = (new Date()).getTime(); | |
266 | |
267 var data = cx.getImageData(0, 0, canvas.width, canvas.height); | |
268 ProcessImageData(data, gParams); | |
269 cx.putImageData(data, 0, 0); | |
270 | |
271 processWidth = canvas.width; | |
272 processHeight = canvas.height; | |
273 | |
274 processTime = (new Date()).getTime() - ts; | |
275 } | |
276 | |
277 if (gCorrectBefore) { | |
278 if (gDirty) { | |
279 ProcessCanvas(gFullContext, gFullCanvas); | |
280 } else { | |
281 processTime = -1; | |
282 } | |
283 gDirty = false; | |
284 FullToDisplay(); | |
285 } else { | |
286 FullToDisplay(); | |
287 ProcessCanvas(gDisplayContext, gDisplayCanvas); | |
288 } | |
289 | |
290 var t3 = (new Date()).getTime(); | |
291 | |
292 if (processTime != -1) { | |
293 $("#log")[0].innerHTML = "<p>" + | |
294 "Size: " + processWidth + "x" + processHeight + " (" + (processWidth*proce
ssHeight) + " pixels)<br>" + | |
295 "Process: " + processTime + "ms" + " Total: " + (t3-t0) + "ms<br>" + | |
296 "Throughput: " + Math.floor((processWidth*processHeight) / (processTime /
1000.0)) + " pixels per second<br>" + | |
297 "FPS: " + (Math.floor((1000.0 / (t3-t0)) * 100) / 100) + "<br>" + | |
298 "</p>"; | |
299 } else { | |
300 $("#log")[0].innerHTML = "<p>(No stats when zoomed and no processing done)</
p>"; | |
301 } | |
302 } | |
303 | |
304 function ZoomToPoint(x, y) { | |
305 if (gZoomSize[0] > gFullImage.width || | |
306 gZoomSize[1] > gFullImage.height) | |
307 return; | |
308 | |
309 var r = gDisplaySize[0] / gFullCanvas.width; | |
310 | |
311 gDisplayCanvas.width = gZoomSize[0]; | |
312 gDisplayCanvas.height = gZoomSize[1]; | |
313 gZoomPoint = [x/r, y/r]; | |
314 $("#canvas").removeClass("canzoomin").addClass("cangrab"); | |
315 Redisplay(); | |
316 } | |
317 | |
318 function ZoomReset() { | |
319 gDisplayCanvas.width = gDisplaySize[0]; | |
320 gDisplayCanvas.height = gDisplaySize[1]; | |
321 gZoomPoint = null; | |
322 $("#canvas").removeClass("canzoomout cangrab isgrabbing").addClass("canzoomin"
); | |
323 Redisplay(); | |
324 } | |
325 | |
326 function LoadImage(url) { | |
327 if (!gFullCanvas) | |
328 gFullCanvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canv
as"); | |
329 if (!gDisplayCanvas) | |
330 gDisplayCanvas = $("#canvas")[0]; | |
331 | |
332 var img = new Image(); | |
333 img.onload = function() { | |
334 var w = img.width; | |
335 var h = img.height; | |
336 | |
337 gFullImage = img; | |
338 | |
339 gFullCanvas.width = w; | |
340 gFullCanvas.height = h; | |
341 gFullContext = gFullCanvas.getContext("2d"); | |
342 | |
343 // XXX use the actual size of the visible region, so that | |
344 // we rescale along with the window | |
345 var dim = 600; | |
346 if (Math.max(w,h) > dim) { | |
347 var scale = dim / Math.max(w,h); | |
348 w *= scale; | |
349 h *= scale; | |
350 } | |
351 | |
352 gDisplayCanvas.width = Math.floor(w); | |
353 gDisplayCanvas.height = Math.floor(h); | |
354 gDisplaySize = [ Math.floor(w), Math.floor(h) ]; | |
355 gDisplayContext = gDisplayCanvas.getContext("2d"); | |
356 | |
357 $("#canvas").removeClass("canzoomin canzoomout cangrab isgrabbing"); | |
358 | |
359 if (gZoomSize[0] <= gFullImage.width && | |
360 gZoomSize[1] <= gFullImage.height) | |
361 { | |
362 $("#canvas").addClass("canzoomin"); | |
363 } | |
364 | |
365 OnSliderChanged(); | |
366 }; | |
367 //img.src = "foo.jpg"; | |
368 //img.src = "Nina6.jpg"; | |
369 img.src = url ? url : "sunspots.jpg"; | |
370 } | |
371 | |
372 function SetupDnD() { | |
373 $("#imagedisplay").bind({ | |
374 dragenter: function(e) { | |
375 $("#imagedisplay").addClass("indrag"); | |
376 return false; | |
377 }, | |
378 | |
379 dragover: function(e) { | |
380 return false; | |
381 }, | |
382 | |
383 dragleave: function(e) { | |
384 $("#imagedisplay").removeClass("indrag"); | |
385 return false; | |
386 }, | |
387 | |
388 drop: function(e) { | |
389 e = e.originalEvent; | |
390 var dt = e.dataTransfer; | |
391 var files = dt.files; | |
392 | |
393 if (files.length > 0) { | |
394 var file = files[0]; | |
395 var reader = new FileReader(); | |
396 reader.onload = function(e) { LoadImage(e.target
.result); }; | |
397 reader.readAsDataURL(file); | |
398 } | |
399 | |
400 $("#imagedisplay").removeClass("indrag"); | |
401 return false; | |
402 } | |
403 }); | |
404 } | |
405 | |
406 function SetupZoomClick() { | |
407 $("#canvas").bind({ | |
408 click: function(e) { | |
409 if (gZoomPoint) | |
410 return true; | |
411 | |
412 var bounds = $("#canvas")[0].getBoundingClientRect(); | |
413 var x = e.clientX - bounds.left; | |
414 var y = e.clientY - bounds.top; | |
415 | |
416 ZoomToPoint(x, y); | |
417 return false; | |
418 }, | |
419 | |
420 mousedown: function(e) { | |
421 if (!gZoomPoint) | |
422 return true; | |
423 | |
424 $("#canvas").addClass("isgrabbing"); | |
425 | |
426 gMouseOrig[0] = gZoomPoint[0]; | |
427 gMouseOrig[1] = gZoomPoint[1]; | |
428 gMouseStart = [ e.clientX, e.clientY ]; | |
429 | |
430 return false; | |
431 }, | |
432 | |
433 mouseup: function(e) { | |
434 if (!gZoomPoint || !gMouseStart) | |
435 return true; | |
436 $("#canvas").removeClass("isgrabbing"); | |
437 | |
438 gZoomPoint = ClampZoomPointToTranslation(); | |
439 | |
440 gZoomPoint[0] += gZoomSize[0]/2; | |
441 gZoomPoint[1] += gZoomSize[1]/2; | |
442 | |
443 gMouseStart = null; | |
444 return false; | |
445 }, | |
446 | |
447 mousemove: function(e) { | |
448 if (!gZoomPoint || !gMouseStart) | |
449 return true; | |
450 | |
451 gZoomPoint[0] = gMouseOrig[0] + (gMouseStart[0] - e.clie
ntX); | |
452 gZoomPoint[1] = gMouseOrig[1] + (gMouseStart[1] - e.clie
ntY); | |
453 Redisplay(); | |
454 | |
455 return false; | |
456 } | |
457 }); | |
458 | |
459 } | |
460 | |
461 function CheckboxToggled(skipRedisplay) { | |
462 gCorrectBefore = $("#correct_before")[0].checked ? true : false; | |
463 | |
464 if (!skipRedisplay) | |
465 Redisplay(); | |
466 } | |
467 | |
468 function ResetSliders() { | |
469 gIgnoreChanges = true; | |
470 | |
471 $(".slider").each(function(index, e) { $(e).slider("value", 500); }); | |
472 $("#blackPoint").slider("value", 0); | |
473 $("#fill").slider("value", 0); | |
474 $("#shadowsSaturation").slider("value", 0); | |
475 $("#highlightsSaturation").slider("value", 0); | |
476 | |
477 gIgnoreChanges = false; | |
478 } | |
479 | |
480 function DoReset() { | |
481 ResetSliders(); | |
482 ZoomReset(); | |
483 OnSliderChanged(); | |
484 } | |
485 | |
486 function DoRedisplay() { | |
487 Redisplay(); | |
488 } | |
489 | |
490 // Speed test: run 10 processings, report in thousands-of-pixels-per-second | |
491 function Benchmark() { | |
492 var times = []; | |
493 | |
494 var width = gFullCanvas.width; | |
495 var height = gFullCanvas.height; | |
496 | |
497 $("#benchmark-status")[0].innerHTML = "Resetting..."; | |
498 | |
499 ResetSliders(); | |
500 | |
501 setTimeout(RunOneTiming, 0); | |
502 | |
503 function RunOneTiming() { | |
504 | |
505 $("#benchmark-status")[0].innerHTML = "Running... " + (times.length + 1); | |
506 | |
507 // reset to original image | |
508 gFullContext.save(); | |
509 gFullContext.translate(Math.floor(gFullImage.width / 2), Math.floor(gFullIma
ge.height / 2)); | |
510 gFullContext.globalCompositeOperation = "copy"; | |
511 gFullContext.drawImage(gFullImage, | |
512 -Math.floor(gFullImage.width / 2), | |
513 -Math.floor(gFullImage.height / 2)); | |
514 gFullContext.restore(); | |
515 | |
516 // time the processing | |
517 var start = (new Date()).getTime(); | |
518 var data = gFullContext.getImageData(0, 0, width, height); | |
519 ProcessImageData(data, gParams); | |
520 gFullContext.putImageData(data, 0, 0); | |
521 var end = (new Date()).getTime(); | |
522 times.push(end - start); | |
523 | |
524 if (times.length < 5) { | |
525 setTimeout(RunOneTiming, 0); | |
526 } else { | |
527 displayResults(); | |
528 } | |
529 | |
530 } | |
531 | |
532 function displayResults() { | |
533 var totalTime = times.reduce(function(p, c) { return p + c; }); | |
534 var totalPixels = height * width * times.length; | |
535 var MPixelsPerSec = totalPixels / totalTime / 1000; | |
536 $("#benchmark-status")[0].innerHTML = "Complete: " + MPixelsPerSec.toFixed(2
) + " megapixels/sec"; | |
537 $("#benchmark-ua")[0].innerHTML = navigator.userAgent; | |
538 } | |
539 } | |
540 | |
541 function SetBackground(n) { | |
542 $("body").removeClass("blackbg whitebg graybg"); | |
543 | |
544 switch (n) { | |
545 case 0: // black | |
546 $("body").addClass("blackbg"); | |
547 break; | |
548 case 1: // gray | |
549 $("body").addClass("graybg"); | |
550 break; | |
551 case 2: // white | |
552 $("body").addClass("whitebg"); | |
553 break; | |
554 } | |
555 } | |
556 | |
557 $(function() { | |
558 $(".slider").slider({ | |
559 orientation: 'horizontal', | |
560 range: "min", | |
561 max: 1000, | |
562 value: 500, | |
563 slide: OnSliderChanged, | |
564 change: OnSliderChanged | |
565 }); | |
566 ResetSliders(); | |
567 SetupDnD(); | |
568 SetupZoomClick(); | |
569 CheckboxToggled(true); | |
570 LoadImage(); | |
571 }); | |
OLD | NEW |