benchmarks/assorted-dom/explanations/darkroom.js (424 lines of code) (raw):

/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40; -*- */ // The if (0) block of function definitions here tries to use // faster math primitives, based on being able to reinterpret // floats as ints and vice versa. We do that using the // WebGL arrays. if (0) { var gConversionBuffer = new ArrayBuffer(4); var gFloatConversion = new WebGLFloatArray(gConversionBuffer); var gIntConversion = new WebGLIntArray(gConversionBuffer); function AsFloat(i) { gIntConversion[0] = i; return gFloatConversion[0]; } function AsInt(f) { gFloatConversion[0] = f; return gIntConversion[0]; } // magic constants used for various floating point manipulations var kMagicFloatToInt = (1 << 23); var kOneAsInt = 0x3F800000; var kScaleUp = AsFloat(0x00800000); var kScaleDown = 1.0 / kScaleUp; function ToInt(f) { // force integer part into lower bits of mantissa var i = ReinterpretFloatAsInt(f + kMagicFloatToInt); // return lower bits of mantissa return i & 0x3FFFFF; } function FastLog2(x) { return (AsInt(x) - kOneAsInt) * kScaleDown; } function FastPower(x, p) { return AsFloat(p * AsInt(x) + (1.0 - p) * kOneAsInt); } var LOG2_HALF = FastLog2(0.5); function FastBias(b, x) { return FastPower(x, FastLog2(b) / LOG2_HALF); } } else { function FastLog2(x) { return Math.log(x) / Math.LN2; } var LOG2_HALF = FastLog2(0.5); function FastBias(b, x) { return Math.pow(x, FastLog2(b) / LOG2_HALF); } } function FastGain(g, x) { return (x < 0.5) ? FastBias(1.0 - g, 2.0 * x) * 0.5 : 1.0 - FastBias(1.0 - g, 2.0 - 2.0 * x) * 0.5; } function Clamp(x) { return (x < 0.0) ? 0.0 : ((x > 1.0) ? 1.0 : x); } function ProcessImageData(imageData, params) { var saturation = params.saturation; var contrast = params.contrast; var brightness = params.brightness; var blackPoint = params.blackPoint; var fill = params.fill; var temperature = params.temperature; var shadowsHue = params.shadowsHue; var shadowsSaturation = params.shadowsSaturation; var highlightsHue = params.highlightsHue; var highlightsSaturation = params.highlightsSaturation; var splitPoint = params.splitPoint; var brightness_a, brightness_b; var oo255 = 1.0 / 255.0; // do some adjustments fill *= 0.2; brightness = (brightness - 1.0) * 0.75 + 1.0; if (brightness < 1.0) { brightness_a = brightness; brightness_b = 0.0; } else { brightness_b = brightness - 1.0; brightness_a = 1.0 - brightness_b; } contrast = contrast * 0.5; contrast = (contrast - 0.5) * 0.75 + 0.5; temperature = (temperature / 2000.0) * 0.1; if (temperature > 0.0) temperature *= 2.0; splitPoint = ((splitPoint + 1.0) * 0.5); // apply to pixels var sz = imageData.width * imageData.height; var data = imageData.data; for (var j = 0; j < sz; j++) { var r = data[j*4+0] * oo255; var g = data[j*4+1] * oo255; var b = data[j*4+2] * oo255; // convert RGB to YIQ // this is a less than ideal colorspace; // HSL would probably be better, but more expensive var y = 0.299 * r + 0.587 * g + 0.114 * b; var i = 0.596 * r - 0.275 * g - 0.321 * b; var q = 0.212 * r - 0.523 * g + 0.311 * b; i = i + temperature; q = q - temperature; i = i * saturation; q = q * saturation; y = (1.0 + blackPoint) * y - blackPoint; y = y + fill; y = y * brightness_a + brightness_b; y = FastGain(contrast, Clamp(y)); if (y < splitPoint) { q = q + (shadowsHue * shadowsSaturation) * (splitPoint - y); } else { i = i + (highlightsHue * highlightsSaturation) * (y - splitPoint); } // convert back to RGB for display r = y + 0.956 * i + 0.621 * q; g = y - 0.272 * i - 0.647 * q; b = y - 1.105 * i + 1.702 * q; // clamping is "free" as part of the ImageData object data[j*4+0] = r * 255.0; data[j*4+1] = g * 255.0; data[j*4+2] = b * 255.0; } } // // UI code // var gFullCanvas = null; var gFullContext = null; var gFullImage = null; var gDisplayCanvas = null; var gDisplayContext = null; var gZoomPoint = null; var gDisplaySize = null; var gZoomSize = [600, 600]; var gMouseStart = null; var gMouseOrig = [0, 0]; var gDirty = true; // If true, apply image correction to the original // source image before scaling down; if false, // scale down first. var gCorrectBefore = false; var gParams = null; var gIgnoreChanges = true; function OnSliderChanged() { if (gIgnoreChanges) return; gDirty = true; gParams = {}; // The values will come in as 0.0 .. 1.0; some params want // a different range. var ranges = { "saturation": [0, 2], "contrast": [0, 2], "brightness": [0, 2], "temperature": [-2000, 2000], "splitPoint": [-1, 1] }; $(".slider").each(function(index, e) { var val = Math.floor($(e).slider("value")) / 1000.0; var id = e.getAttribute("id"); if (id in ranges) val = val * (ranges[id][1] - ranges[id][0]) + ranges[id][0]; gParams[id] = val; }); Redisplay(); } function ClampZoomPointToTranslation() { var tx = gZoomPoint[0] - gZoomSize[0]/2; var ty = gZoomPoint[1] - gZoomSize[1]/2; tx = Math.max(0, tx); ty = Math.max(0, ty); if (tx + gZoomSize[0] > gFullImage.width) tx = gFullImage.width - gZoomSize[0]; if (ty + gZoomSize[1] > gFullImage.height) ty = gFullImage.height - gZoomSize[1]; return [tx, ty]; } function Redisplay() { if (!gParams) return; var angle = (gParams.angle*2.0 - 1.0) * 90.0 + (gParams.fineangle*2.0 - 1.0) * 2.0; angle = Math.max(-90, Math.min(90, angle)); angle = (angle * Math.PI) / 180.0; var processTime; var processWidth, processHeight; var t0 = (new Date()).getTime(); // Render the image with rotation; we only need to render // if we're either correcting just the portion that's visible, // or if we're correcting the full thing and the sliders have been // changed. Otherwise, what's in the full canvas is already corrected // and correct. if ((gCorrectBefore && gDirty) || !gCorrectBefore) { gFullContext.save(); gFullContext.translate(Math.floor(gFullImage.width / 2), Math.floor(gFullImage.height / 2)); gFullContext.rotate(angle); gFullContext.globalCompositeOperation = "copy"; gFullContext.drawImage(gFullImage, -Math.floor(gFullImage.width / 2), -Math.floor(gFullImage.height / 2)); gFullContext.restore(); } function FullToDisplay() { gDisplayContext.save(); if (gZoomPoint) { var pt = ClampZoomPointToTranslation(); gDisplayContext.translate(-pt[0], -pt[1]); } else { gDisplayContext.translate(0, 0); var ratio = gDisplaySize[0] / gFullCanvas.width; gDisplayContext.scale(ratio, ratio); } gDisplayContext.globalCompositeOperation = "copy"; gDisplayContext.drawImage(gFullCanvas, 0, 0); gDisplayContext.restore(); } function ProcessCanvas(cx, canvas) { var ts = (new Date()).getTime(); var data = cx.getImageData(0, 0, canvas.width, canvas.height); ProcessImageData(data, gParams); cx.putImageData(data, 0, 0); processWidth = canvas.width; processHeight = canvas.height; processTime = (new Date()).getTime() - ts; } if (gCorrectBefore) { if (gDirty) { ProcessCanvas(gFullContext, gFullCanvas); } else { processTime = -1; } gDirty = false; FullToDisplay(); } else { FullToDisplay(); ProcessCanvas(gDisplayContext, gDisplayCanvas); } var t3 = (new Date()).getTime(); if (processTime != -1) { $("#log")[0].innerHTML = "<p>" + "Size: " + processWidth + "x" + processHeight + " (" + (processWidth*processHeight) + " pixels)<br>" + "Process: " + processTime + "ms" + " Total: " + (t3-t0) + "ms<br>" + "Throughput: " + Math.floor((processWidth*processHeight) / (processTime / 1000.0)) + " pixels per second<br>" + "FPS: " + (Math.floor((1000.0 / (t3-t0)) * 100) / 100) + "<br>" + "</p>"; } else { $("#log")[0].innerHTML = "<p>(No stats when zoomed and no processing done)</p>"; } } function ZoomToPoint(x, y) { if (gZoomSize[0] > gFullImage.width || gZoomSize[1] > gFullImage.height) return; var r = gDisplaySize[0] / gFullCanvas.width; gDisplayCanvas.width = gZoomSize[0]; gDisplayCanvas.height = gZoomSize[1]; gZoomPoint = [x/r, y/r]; $("#canvas").removeClass("canzoomin").addClass("cangrab"); Redisplay(); } function ZoomReset() { gDisplayCanvas.width = gDisplaySize[0]; gDisplayCanvas.height = gDisplaySize[1]; gZoomPoint = null; $("#canvas").removeClass("canzoomout cangrab isgrabbing").addClass("canzoomin"); Redisplay(); } function LoadImage(url) { if (!gFullCanvas) gFullCanvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); if (!gDisplayCanvas) gDisplayCanvas = $("#canvas")[0]; var img = new Image(); img.onload = function() { var w = img.width; var h = img.height; gFullImage = img; gFullCanvas.width = w; gFullCanvas.height = h; gFullContext = gFullCanvas.getContext("2d"); // XXX use the actual size of the visible region, so that // we rescale along with the window var dim = 600; if (Math.max(w,h) > dim) { var scale = dim / Math.max(w,h); w *= scale; h *= scale; } gDisplayCanvas.width = Math.floor(w); gDisplayCanvas.height = Math.floor(h); gDisplaySize = [ Math.floor(w), Math.floor(h) ]; gDisplayContext = gDisplayCanvas.getContext("2d"); $("#canvas").removeClass("canzoomin canzoomout cangrab isgrabbing"); if (gZoomSize[0] <= gFullImage.width && gZoomSize[1] <= gFullImage.height) { $("#canvas").addClass("canzoomin"); } OnSliderChanged(); }; //img.src = "foo.jpg"; //img.src = "Nina6.jpg"; img.src = url ? url : "sunspots.jpg"; } function SetupDnD() { $("#imagedisplay").bind({ dragenter: function(e) { $("#imagedisplay").addClass("indrag"); return false; }, dragover: function(e) { return false; }, dragleave: function(e) { $("#imagedisplay").removeClass("indrag"); return false; }, drop: function(e) { e = e.originalEvent; var dt = e.dataTransfer; var files = dt.files; if (files.length > 0) { var file = files[0]; var reader = new FileReader(); reader.onload = function(e) { LoadImage(e.target.result); }; reader.readAsDataURL(file); } $("#imagedisplay").removeClass("indrag"); return false; } }); } function SetupZoomClick() { $("#canvas").bind({ click: function(e) { if (gZoomPoint) return true; var bounds = $("#canvas")[0].getBoundingClientRect(); var x = e.clientX - bounds.left; var y = e.clientY - bounds.top; ZoomToPoint(x, y); return false; }, mousedown: function(e) { if (!gZoomPoint) return true; $("#canvas").addClass("isgrabbing"); gMouseOrig[0] = gZoomPoint[0]; gMouseOrig[1] = gZoomPoint[1]; gMouseStart = [ e.clientX, e.clientY ]; return false; }, mouseup: function(e) { if (!gZoomPoint || !gMouseStart) return true; $("#canvas").removeClass("isgrabbing"); gZoomPoint = ClampZoomPointToTranslation(); gZoomPoint[0] += gZoomSize[0]/2; gZoomPoint[1] += gZoomSize[1]/2; gMouseStart = null; return false; }, mousemove: function(e) { if (!gZoomPoint || !gMouseStart) return true; gZoomPoint[0] = gMouseOrig[0] + (gMouseStart[0] - e.clientX); gZoomPoint[1] = gMouseOrig[1] + (gMouseStart[1] - e.clientY); Redisplay(); return false; } }); } function CheckboxToggled(skipRedisplay) { gCorrectBefore = $("#correct_before")[0].checked ? true : false; if (!skipRedisplay) Redisplay(); } function ResetSliders() { gIgnoreChanges = true; $(".slider").each(function(index, e) { $(e).slider("value", 500); }); $("#blackPoint").slider("value", 0); $("#fill").slider("value", 0); $("#shadowsSaturation").slider("value", 0); $("#highlightsSaturation").slider("value", 0); gIgnoreChanges = false; } function DoReset() { ResetSliders(); ZoomReset(); OnSliderChanged(); } function DoRedisplay() { Redisplay(); } // Speed test: run 10 processings, report in thousands-of-pixels-per-second function Benchmark() { var times = []; var width = gFullCanvas.width; var height = gFullCanvas.height; $("#benchmark-status")[0].innerHTML = "Resetting..."; ResetSliders(); setTimeout(RunOneTiming, 0); function RunOneTiming() { $("#benchmark-status")[0].innerHTML = "Running... " + (times.length + 1); // reset to original image gFullContext.save(); gFullContext.translate(Math.floor(gFullImage.width / 2), Math.floor(gFullImage.height / 2)); gFullContext.globalCompositeOperation = "copy"; gFullContext.drawImage(gFullImage, -Math.floor(gFullImage.width / 2), -Math.floor(gFullImage.height / 2)); gFullContext.restore(); // time the processing var start = (new Date()).getTime(); var data = gFullContext.getImageData(0, 0, width, height); ProcessImageData(data, gParams); gFullContext.putImageData(data, 0, 0); var end = (new Date()).getTime(); times.push(end - start); if (times.length < 5) { setTimeout(RunOneTiming, 0); } else { displayResults(); } } function displayResults() { var totalTime = times.reduce(function(p, c) { return p + c; }); var totalPixels = height * width * times.length; var MPixelsPerSec = totalPixels / totalTime / 1000; $("#benchmark-status")[0].innerHTML = "Complete: " + MPixelsPerSec.toFixed(2) + " megapixels/sec"; $("#benchmark-ua")[0].innerHTML = navigator.userAgent; } } function SetBackground(n) { $("body").removeClass("blackbg whitebg graybg"); switch (n) { case 0: // black $("body").addClass("blackbg"); break; case 1: // gray $("body").addClass("graybg"); break; case 2: // white $("body").addClass("whitebg"); break; } } $(function() { $(".slider").slider({ orientation: 'horizontal', range: "min", max: 1000, value: 500, slide: OnSliderChanged, change: OnSliderChanged }); ResetSliders(); SetupDnD(); SetupZoomClick(); CheckboxToggled(true); LoadImage(); });