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 |