1 /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40; -*- */
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
10 var gConversionBuffer
= new ArrayBuffer(4);
11 var gFloatConversion
= new WebGLFloatArray(gConversionBuffer
);
12 var gIntConversion
= new WebGLIntArray(gConversionBuffer
);
15 gIntConversion
[0] = i
;
16 return gFloatConversion
[0];
20 gFloatConversion
[0] = f
;
21 return gIntConversion
[0];
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
;
31 // force integer part into lower bits of mantissa
32 var i
= ReinterpretFloatAsInt(f
+ kMagicFloatToInt
);
33 // return lower bits of mantissa
37 function FastLog2(x
) {
38 return (AsInt(x
) - kOneAsInt
) * kScaleDown
;
41 function FastPower(x
, p
) {
42 return AsFloat(p
* AsInt(x
) + (1.0 - p
) * kOneAsInt
);
45 var LOG2_HALF
= FastLog2(0.5);
47 function FastBias(b
, x
) {
48 return FastPower(x
, FastLog2(b
) / LOG2_HALF
);
53 function FastLog2(x
) {
54 return Math
.log(x
) / Math
.LN2
;
57 var LOG2_HALF
= FastLog2(0.5);
59 function FastBias(b
, x
) {
60 return Math
.pow(x
, FastLog2(b
) / LOG2_HALF
);
65 function FastGain(g
, x
) {
67 FastBias(1.0 - g
, 2.0 * x
) * 0.5 :
68 1.0 - FastBias(1.0 - g
, 2.0 - 2.0 * x
) * 0.5;
72 return (x
< 0.0) ? 0.0 : ((x
> 1.0) ? 1.0 : x
);
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
;
88 var brightness_a
, brightness_b
;
89 var oo255
= 1.0 / 255.0;
91 // do some adjustments
93 brightness
= (brightness
- 1.0) * 0.75 + 1.0;
94 if (brightness
< 1.0) {
95 brightness_a
= brightness
;
98 brightness_b
= brightness
- 1.0;
99 brightness_a
= 1.0 - brightness_b
;
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);
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
;
124 y
= (1.0 + blackPoint
) * y
- blackPoint
;
126 y
= y
* brightness_a
+ brightness_b
;
127 y
= FastGain(contrast
, Clamp(y
));
129 if (y
< splitPoint
) {
130 q
= q
+ (shadowsHue
* shadowsSaturation
) * (splitPoint
- y
);
132 i
= i
+ (highlightsHue
* highlightsSaturation
) * (y
- splitPoint
);
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
;
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;
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];
163 // If true, apply image correction to the original
164 // source image before scaling down; if false,
166 var gCorrectBefore
= false;
169 var gIgnoreChanges
= true;
171 function OnSliderChanged() {
179 // The values will come in as 0.0 .. 1.0; some params want
180 // a different range.
182 "saturation": [0, 2],
184 "brightness": [0, 2],
185 "temperature": [-2000, 2000],
186 "splitPoint": [-1, 1]
189 $(".slider").each(function(index
, e
) {
190 var val
= Math
.floor($(e
).slider("value")) / 1000.0;
191 var id
= e
.getAttribute("id");
193 val
= val
* (ranges
[id
][1] - ranges
[id
][0]) + ranges
[id
][0];
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
);
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];
213 function Redisplay() {
218 (gParams
.angle
*2.0 - 1.0) * 90.0 +
219 (gParams
.fineangle
*2.0 - 1.0) * 2.0;
221 angle
= Math
.max(-90, Math
.min(90, angle
));
222 angle
= (angle
* Math
.PI
) / 180.0;
225 var processWidth
, processHeight
;
227 var t0
= (new Date()).getTime();
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
234 if ((gCorrectBefore
&& gDirty
) ||
238 gFullContext
.translate(Math
.floor(gFullImage
.width
/ 2), Math
.floor(gFullImage
.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();
247 function FullToDisplay() {
248 gDisplayContext
.save();
250 var pt
= ClampZoomPointToTranslation();
252 gDisplayContext
.translate(-pt
[0], -pt
[1]);
254 gDisplayContext
.translate(0, 0);
255 var ratio
= gDisplaySize
[0] / gFullCanvas
.width
;
256 gDisplayContext
.scale(ratio
, ratio
);
259 gDisplayContext
.globalCompositeOperation
= "copy";
260 gDisplayContext
.drawImage(gFullCanvas
, 0, 0);
261 gDisplayContext
.restore();
264 function ProcessCanvas(cx
, canvas
) {
265 var ts
= (new Date()).getTime();
267 var data
= cx
.getImageData(0, 0, canvas
.width
, canvas
.height
);
268 ProcessImageData(data
, gParams
);
269 cx
.putImageData(data
, 0, 0);
271 processWidth
= canvas
.width
;
272 processHeight
= canvas
.height
;
274 processTime
= (new Date()).getTime() - ts
;
277 if (gCorrectBefore
) {
279 ProcessCanvas(gFullContext
, gFullCanvas
);
287 ProcessCanvas(gDisplayContext
, gDisplayCanvas
);
290 var t3
= (new Date()).getTime();
292 if (processTime
!= -1) {
293 $("#log")[0].innerHTML
= "<p>" +
294 "Size: " + processWidth
+ "x" + processHeight
+ " (" + (processWidth
*processHeight
) + " 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>" +
300 $("#log")[0].innerHTML
= "<p>(No stats when zoomed and no processing done)</p>";
304 function ZoomToPoint(x
, y
) {
305 if (gZoomSize
[0] > gFullImage
.width
||
306 gZoomSize
[1] > gFullImage
.height
)
309 var r
= gDisplaySize
[0] / gFullCanvas
.width
;
311 gDisplayCanvas
.width
= gZoomSize
[0];
312 gDisplayCanvas
.height
= gZoomSize
[1];
313 gZoomPoint
= [x
/r, y/r
];
314 $("#canvas").removeClass("canzoomin").addClass("cangrab");
318 function ZoomReset() {
319 gDisplayCanvas
.width
= gDisplaySize
[0];
320 gDisplayCanvas
.height
= gDisplaySize
[1];
322 $("#canvas").removeClass("canzoomout cangrab isgrabbing").addClass("canzoomin");
326 function LoadImage(url
) {
328 gFullCanvas
= document
.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
330 gDisplayCanvas
= $("#canvas")[0];
332 var img
= new Image();
333 img
.onload = function() {
339 gFullCanvas
.width
= w
;
340 gFullCanvas
.height
= h
;
341 gFullContext
= gFullCanvas
.getContext("2d");
343 // XXX use the actual size of the visible region, so that
344 // we rescale along with the window
346 if (Math
.max(w
,h
) > dim
) {
347 var scale
= dim
/ Math
.max(w
,h
);
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");
357 $("#canvas").removeClass("canzoomin canzoomout cangrab isgrabbing");
359 if (gZoomSize
[0] <= gFullImage
.width
&&
360 gZoomSize
[1] <= gFullImage
.height
)
362 $("#canvas").addClass("canzoomin");
367 //img.src = "foo.jpg";
368 //img.src = "Nina6.jpg";
369 img
.src
= url
? url
: "sunspots.jpg";
372 function SetupDnD() {
373 $("#imagedisplay").bind({
374 dragenter: function(e
) {
375 $("#imagedisplay").addClass("indrag");
379 dragover: function(e
) {
383 dragleave: function(e
) {
384 $("#imagedisplay").removeClass("indrag");
390 var dt
= e
.dataTransfer
;
391 var files
= dt
.files
;
393 if (files
.length
> 0) {
395 var reader
= new FileReader();
396 reader
.onload = function(e
) { LoadImage(e
.target
.result
); };
397 reader
.readAsDataURL(file
);
400 $("#imagedisplay").removeClass("indrag");
406 function SetupZoomClick() {
412 var bounds
= $("#canvas")[0].getBoundingClientRect();
413 var x
= e
.clientX
- bounds
.left
;
414 var y
= e
.clientY
- bounds
.top
;
420 mousedown: function(e
) {
424 $("#canvas").addClass("isgrabbing");
426 gMouseOrig
[0] = gZoomPoint
[0];
427 gMouseOrig
[1] = gZoomPoint
[1];
428 gMouseStart
= [ e
.clientX
, e
.clientY
];
433 mouseup: function(e
) {
434 if (!gZoomPoint
|| !gMouseStart
)
436 $("#canvas").removeClass("isgrabbing");
438 gZoomPoint
= ClampZoomPointToTranslation();
440 gZoomPoint
[0] += gZoomSize
[0]/2;
441 gZoomPoint
[1] += gZoomSize
[1]/2;
447 mousemove: function(e
) {
448 if (!gZoomPoint
|| !gMouseStart
)
451 gZoomPoint
[0] = gMouseOrig
[0] + (gMouseStart
[0] - e
.clientX
);
452 gZoomPoint
[1] = gMouseOrig
[1] + (gMouseStart
[1] - e
.clientY
);
461 function CheckboxToggled(skipRedisplay
) {
462 gCorrectBefore
= $("#correct_before")[0].checked
? true : false;
468 function ResetSliders() {
469 gIgnoreChanges
= true;
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);
477 gIgnoreChanges
= false;
486 function DoRedisplay() {
490 // Speed test: run 10 processings, report in thousands-of-pixels-per-second
491 function Benchmark() {
494 var width
= gFullCanvas
.width
;
495 var height
= gFullCanvas
.height
;
497 $("#benchmark-status")[0].innerHTML
= "Resetting...";
501 setTimeout(RunOneTiming
, 0);
503 function RunOneTiming() {
505 $("#benchmark-status")[0].innerHTML
= "Running... " + (times
.length
+ 1);
507 // reset to original image
509 gFullContext
.translate(Math
.floor(gFullImage
.width
/ 2), Math
.floor(gFullImage
.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();
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
);
524 if (times
.length
< 5) {
525 setTimeout(RunOneTiming
, 0);
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
;
541 function SetBackground(n
) {
542 $("body").removeClass("blackbg whitebg graybg");
546 $("body").addClass("blackbg");
549 $("body").addClass("graybg");
552 $("body").addClass("whitebg");
558 $(".slider").slider({
559 orientation
: 'horizontal',
563 slide
: OnSliderChanged
,
564 change
: OnSliderChanged
569 CheckboxToggled(true);