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