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);