├── LICENSE ├── README.md ├── chart.js ├── curve.js ├── filterer.js ├── filtering.css ├── images ├── desert.jpg ├── fields.jpg ├── milky.jpg └── valley.jpg ├── index.html ├── main.js └── shared.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 David Li (http://david.li) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Fourier Image Filtering 2 | 3 | [http://david.li/filtering](http://david.li/filtering) 4 | 5 | Interactive Fourier transform image filtering. FFT implemented on the GPU via WebGL. -------------------------------------------------------------------------------- /chart.js: -------------------------------------------------------------------------------- 1 | var Chart = function (canvas) { 2 | var context = canvas.getContext('2d'); 3 | var data = []; 4 | 5 | this.setData = function (powersByFrequency, curve) { 6 | for (var i = 0; i < END_EDIT_FREQUENCY; i += 1) { 7 | data[i] = []; 8 | } 9 | 10 | for (var frequency in powersByFrequency) { 11 | if (frequency < END_EDIT_FREQUENCY) { 12 | for (var i = 0; i < powersByFrequency[frequency].length; ++i) { 13 | data[Math.floor(frequency)].push(powersByFrequency[frequency][i]); 14 | } 15 | } 16 | } 17 | 18 | for (var i = 0; i < data.length; i += 1) { 19 | data[i] = averageArray(data[i]) * CHART_SCALE; 20 | } 21 | 22 | this.draw(curve); 23 | }; 24 | 25 | this.draw = function (curve) { 26 | context.fillStyle = CHART_BACKGROUND_COLOR; 27 | context.fillRect(0, 0, canvas.width, canvas.height); 28 | 29 | context.fillStyle = CHART_SECONDARY_COLOR; 30 | 31 | context.beginPath(); 32 | context.moveTo(0, canvas.height); 33 | 34 | for (var i = 0; i < data.length; i += 1) { 35 | var x = (i / END_EDIT_FREQUENCY) * canvas.width; 36 | 37 | var power = data[i]; 38 | context.lineTo(x + 0.5, canvas.height - power + 0.5); //left side of bar (also right side of previous bar) 39 | context.lineTo(x + canvas.width / END_EDIT_FREQUENCY + 0.5, canvas.height - power + 0.5); 40 | } 41 | 42 | context.lineTo(canvas.width, canvas.height); 43 | 44 | context.closePath(); 45 | context.fill(); 46 | 47 | 48 | context.fillStyle = CHART_PRIMARY_COLOR; 49 | 50 | context.beginPath(); 51 | context.moveTo(0, canvas.height); 52 | 53 | for (var i = 0; i < data.length; i += 1) { 54 | var x = (i / END_EDIT_FREQUENCY) * canvas.width; 55 | 56 | var scale = (curve.evaluate(CURVE_CANVAS_WIDTH * i / END_EDIT_FREQUENCY) / CURVE_CANVAS_HEIGHT) * CURVE_SCALE; 57 | var power = data[i] * scale; 58 | 59 | context.lineTo(x + 0.5, canvas.height - power + 0.5); //left side of bar (also right side of previous bar) 60 | context.lineTo(x + canvas.width / END_EDIT_FREQUENCY + 0.5, canvas.height - power + 0.5); 61 | } 62 | 63 | context.lineTo(canvas.width, canvas.height); 64 | 65 | context.closePath(); 66 | context.fill(); 67 | 68 | 69 | context.beginPath(); 70 | context.moveTo(0, canvas.height); 71 | 72 | for (var i = 0; i < data.length; i += 1) { 73 | var x = (i / END_EDIT_FREQUENCY) * canvas.width; 74 | 75 | var power = data[i]; 76 | context.lineTo(x + 0.5, canvas.height - power + 0.5); //left side of bar (also right side of previous bar) 77 | context.lineTo(x + canvas.width / END_EDIT_FREQUENCY + 0.5, canvas.height - power + 0.5); 78 | } 79 | 80 | context.strokeStyle = CHART_SECONDARY_COLOR; 81 | context.stroke(); 82 | 83 | }; 84 | }; -------------------------------------------------------------------------------- /curve.js: -------------------------------------------------------------------------------- 1 | var Curve = function (canvas, options) { 2 | var canvas = canvas, 3 | context = canvas.getContext('2d'); 4 | 5 | var width = canvas.width, 6 | height = canvas.height; 7 | 8 | var activePoint = null, 9 | draggingPoint = null; 10 | 11 | var pointColor = options.pointColor, 12 | pointSize = options.pointSize, 13 | resolution = options.resolution, 14 | curveWidth = options.curveWidth, 15 | curveColor = options.curveColor, 16 | selectionRadius = options.selectionRadius, 17 | addDistance = options.addDistance, 18 | backgroundColor = options.backgroundColor, 19 | 20 | overPointCursor = options.overPointCursor, 21 | overCurveCursor = options.overCurveCursor, 22 | defaultCursor = options.defaultCursor, 23 | 24 | minSpacing = options.minSpacing; 25 | 26 | var head = null, 27 | tail = null, 28 | length = 0; 29 | 30 | var changeListeners = []; 31 | 32 | //recomputes correct second derivatives for all points 33 | var recompute = function () { 34 | var n = length; 35 | 36 | var x = [], 37 | y = []; 38 | iteratePoints(function (point) { 39 | x.push(point.x); 40 | y.push(point.y); 41 | }); 42 | 43 | var a = [], 44 | b = [], 45 | c = [], 46 | d = []; 47 | 48 | //second derivatives of endpoints = 0 49 | a[0] = 0.0; 50 | b[0] = 1.0; 51 | c[0] = 0.0; 52 | d[0] = 0.0; 53 | a[n - 1] = 0.0; 54 | b[n - 1] = 1.0; 55 | c[n - 1] = 0.0; 56 | d[n - 1] = 0.0; 57 | 58 | for (var i = 1; i < n - 1; i += 1) { 59 | a[i] = x[i] - x[i - 1]; 60 | b[i] = 2.0 * (x[i + 1] - x[i - 1]); 61 | c[i] = x[i + 1] - x[i]; 62 | d[i] = 6.0 * ((y[i + 1] - y[i]) / (x[i + 1] - x[i]) - (y[i] - y[i - 1]) / (x[i] - x[i - 1])); 63 | } 64 | 65 | c[0] = c[0] / b[0]; 66 | d[0] = d[0] / b[0]; 67 | for (var i = 1; i < n; i += 1) { 68 | var m = 1.0 / (b[i] - a[i] * c[i - 1]); 69 | c[i] *= m; 70 | d[i] = (d[i] - a[i] * d[i - 1]) * m; 71 | } 72 | 73 | for (var i = n - 2; i >= 0; i -= 1) { 74 | d[i] = d[i] - c[i] * d[i + 1]; 75 | } 76 | 77 | d[0] = 0.0; 78 | d[n - 1] = 0.0; 79 | 80 | var i = 0; 81 | iteratePoints(function (point) { 82 | point.secondDerivative = d[i]; 83 | i += 1; 84 | }); 85 | }; 86 | 87 | var evaluateBetween = function (x, left, right) { 88 | var A = (right.x - x) / (right.x - left.x), 89 | B = 1 - A, 90 | C = ((A * A * A - A) * (right.x - left.x) * (right.x - left.x)) / 6.0, 91 | D = ((B * B * B - B) * (right.x - left.x) * (right.x - left.x)) / 6.0; 92 | 93 | return clamp(A * left.y + B * right.y + C * left.secondDerivative + D * right.secondDerivative, 1, canvas.height); 94 | }; 95 | 96 | var createPoint = function (x_, y_, next_, previous_) { 97 | return { 98 | x: x_, 99 | y: y_, 100 | next: next_, 101 | previous: previous_ 102 | }; 103 | }; 104 | 105 | this.add = function (x, y) { 106 | var point; 107 | if (head === null) { //empty curve 108 | point = createPoint(x, y, null, null); 109 | head = point; 110 | tail = point; 111 | } else if (x < head.x) { 112 | point = createPoint(x, y, head, null); 113 | head.previous = point; 114 | head = point; 115 | } else if (x > tail.x) { 116 | point = createPoint(x, y, null, tail); 117 | tail.next = point; 118 | tail = point; 119 | } else { 120 | var current = head.next, 121 | previous = head; 122 | while (x > current.x) { 123 | current = current.next; 124 | previous = previous.next; 125 | } 126 | var point = createPoint(x, y, current, previous); 127 | previous.next = point; 128 | current.previous = point; 129 | } 130 | length += 1; 131 | 132 | this.triggerChangeListeners(); 133 | 134 | return point; 135 | }; 136 | 137 | this.setPoints = function (points) { 138 | head = null; 139 | tail = null; 140 | length = 0; 141 | 142 | for (var i = 0; i < points.length; ++i) { 143 | this.add(points[i][0], points[i][1]); 144 | } 145 | }; 146 | 147 | this.remove = function (point) { 148 | if (head === point) { 149 | head = point.next; 150 | point.next.previous = null; 151 | } else if (tail === point) { 152 | tail = point.previous; 153 | point.previous.next = null; 154 | } else { 155 | point.previous.next = point.next; 156 | point.next.previous = point.previous; 157 | } 158 | length -= 1; 159 | 160 | this.triggerChangeListeners(); 161 | }; 162 | 163 | this.evaluate = function (x) { 164 | if (head === tail) { //only one point in curve 165 | return head.y; 166 | } 167 | 168 | if (x <= head.x) { 169 | var derivative = (head.next.y - head.y) / (head.next.x - head.x) - (1 / 6) * (head.next.x - head.x) * head.next.secondDerivative; 170 | return clamp(head.y + derivative * (x - head.x), 1, canvas.height); 171 | } else if (x >= tail.x) { 172 | var derivative = (tail.y - tail.previous.y) / (tail.x - tail.previous.x) + (1 / 6) * (tail.x - tail.previous.x) * tail.previous.secondDerivative; 173 | return clamp(tail.y + derivative * (x - tail.x), 1, canvas.height); 174 | } 175 | var current = head.next, 176 | previous = head; 177 | while (x > current.x) { 178 | current = current.next; 179 | previous = previous.next; 180 | } 181 | var left = previous; 182 | var right = current; 183 | 184 | return evaluateBetween(x, left, right); 185 | }; 186 | 187 | this.iterate = function (start, end, step, callback) { 188 | var left = head, 189 | right = head.next; 190 | 191 | var i = 0; 192 | 193 | for (var x = start; x <= end; x += step) { 194 | if (head === tail) { //only one point 195 | callback(x, head.y); 196 | continue; 197 | } 198 | 199 | if (x <= head.x) { 200 | var derivative = (head.next.y - head.y) / (head.next.x - head.x) - (1 / 6) * (head.next.x - head.x) * head.next.secondDerivative; 201 | callback(x, clamp(head.y + derivative * (x - head.x), 1, canvas.height)); 202 | continue; 203 | } else if (x >= tail.x) { 204 | var derivative = (tail.y - tail.previous.y) / (tail.x - tail.previous.x) + (1 / 6) * (tail.x - tail.previous.x) * tail.previous.secondDerivative; 205 | callback(x, clamp(tail.y + derivative * (x - tail.x), 1, canvas.height)); 206 | continue; 207 | } 208 | 209 | if (x > right.x) { 210 | right = right.next; 211 | left = left.next; 212 | } 213 | 214 | i += 1; 215 | 216 | callback(x, evaluateBetween(x, left, right), i); 217 | } 218 | }; 219 | 220 | var iteratePoints = function (callback) { 221 | var current = head; 222 | while (current !== null) { 223 | callback(current); 224 | current = current.next; 225 | } 226 | }; 227 | 228 | this.addChangeListener = function (callback) { 229 | changeListeners.push(callback); 230 | }; 231 | 232 | this.triggerChangeListeners = function () { 233 | for (var i = 0; i < changeListeners.length; i += 1) { 234 | changeListeners[i](); 235 | } 236 | }; 237 | 238 | this.addChangeListener(recompute); 239 | 240 | var that = this; 241 | canvas.addEventListener('mousedown', function (event) { 242 | event.preventDefault(); 243 | 244 | var mousePosition = getMousePosition(event, canvas), 245 | size = selectionRadius; 246 | 247 | mousePosition.y = height - mousePosition.y; 248 | 249 | iteratePoints(function (point) { 250 | if (mousePosition.x > point.x - size * 0.5 && mousePosition.x < point.x + size * 0.5 && 251 | mousePosition.y > point.y - size * 0.5 && mousePosition.y < point.y + size * 0.5) { 252 | 253 | //activate point 254 | activePoint = point; 255 | draggingPoint = point; 256 | redraw(); 257 | } 258 | }); 259 | 260 | if (draggingPoint === null) { 261 | if (length === 0 || Math.abs(mousePosition.y - that.evaluate(mousePosition.x)) < addDistance) { 262 | //add a point 263 | var point = that.add(mousePosition.x, mousePosition.y); 264 | 265 | activePoint = point; 266 | draggingPoint = point; 267 | } else { //deselect points 268 | activePoint = null; 269 | redraw(); 270 | } 271 | } 272 | }); 273 | 274 | document.addEventListener('mousemove', function (event) { 275 | event.preventDefault(); 276 | 277 | var mousePosition = getMousePosition(event, canvas); 278 | mousePosition.y = height - mousePosition.y; 279 | 280 | if (draggingPoint !== null) { //move point being dragged 281 | 282 | var x = clamp(mousePosition.x, 0, canvas.width); 283 | var y = clamp(mousePosition.y, 0, canvas.height); 284 | 285 | if (draggingPoint.previous === null && draggingPoint.next === null) { 286 | draggingPoint.x = x; 287 | } else if (draggingPoint.previous === null) { 288 | draggingPoint.x = Math.min(x, draggingPoint.next.x - minSpacing); 289 | } else if (draggingPoint.next === null) { 290 | draggingPoint.x = Math.max(x, draggingPoint.previous.x + minSpacing); 291 | } else { 292 | draggingPoint.x = clamp(x, draggingPoint.previous.x + minSpacing, draggingPoint.next.x - minSpacing); 293 | } 294 | draggingPoint.y = y; 295 | 296 | that.triggerChangeListeners(); 297 | 298 | } else { //set appropriate cursor 299 | var size = selectionRadius; 300 | 301 | var overCurve = false; 302 | if (Math.abs(mousePosition.y - that.evaluate(mousePosition.x)) < addDistance) { 303 | overCurve = true; 304 | } 305 | 306 | var overPoint = false; 307 | iteratePoints(function (point) { 308 | if (mousePosition.x > point.x - size * 0.5 && mousePosition.x < point.x + size * 0.5 && 309 | mousePosition.y > point.y - size * 0.5 && mousePosition.y < point.y + size * 0.5) { 310 | overPoint = true; 311 | } 312 | }); 313 | 314 | if (overPoint) { 315 | canvas.style.cursor = overPointCursor; 316 | } else if (overCurve) { 317 | canvas.style.cursor = overCurveCursor; 318 | } else { 319 | canvas.style.cursor = defaultCursor; 320 | } 321 | } 322 | }); 323 | 324 | document.addEventListener('mouseup', function (event) { 325 | event.preventDefault(); 326 | 327 | //trigger event for curve change end 328 | 329 | if (draggingPoint !== null) { 330 | draggingPoint = null; 331 | } 332 | }); 333 | 334 | document.addEventListener('keydown', function (event) { 335 | if (event.keyCode === 46 && activePoint !== null && length > 1) { 336 | that.remove(activePoint); 337 | } 338 | }); 339 | 340 | var clearCanvas = function () { 341 | context.fillStyle = backgroundColor; 342 | context.fillRect(0, 0, canvas.width, canvas.height); 343 | }; 344 | 345 | var drawCurve = function () { 346 | context.strokeStyle = curveColor; 347 | 348 | context.moveTo(0, head.y); 349 | context.beginPath(); 350 | 351 | that.iterate(0, width, 1, function (x, y) { 352 | context.lineTo(x + 0.5, height - y + 0.5); 353 | }); 354 | 355 | context.stroke(); 356 | }; 357 | 358 | var drawPoints = function () { 359 | context.lineWidth = 1; 360 | 361 | context.fillStyle = pointColor; 362 | 363 | iteratePoints(function (point) { 364 | if (point === activePoint) { 365 | context.fillRect(point.x - pointSize * 0.5, height - point.y - pointSize * 0.5, pointSize, pointSize); 366 | context.strokeRect(point.x - pointSize * 0.5 + 0.5, height - point.y - pointSize * 0.5 + 0.5, pointSize, pointSize); 367 | } else { 368 | context.strokeRect(point.x - pointSize * 0.5 + 0.5, height - point.y - pointSize * 0.5 + 0.5, pointSize, pointSize); 369 | } 370 | 371 | }); 372 | }; 373 | 374 | var redraw = function () { 375 | clearCanvas(); 376 | 377 | context.strokeStyle = 'rgb(100, 100, 100);'; 378 | context.beginPath(); 379 | context.moveTo(0 + 0.5, canvas.height / 2 + 0.5); 380 | context.lineTo(canvas.width + 0.5, canvas.height / 2 + 0.5); 381 | context.stroke(); 382 | 383 | drawPoints(); 384 | drawCurve(); 385 | }; 386 | 387 | this.addChangeListener(redraw); 388 | }; -------------------------------------------------------------------------------- /filterer.js: -------------------------------------------------------------------------------- 1 | var FULLSCREEN_VERTEX_SOURCE = [ 2 | 'attribute vec2 a_position;', 3 | 'varying vec2 v_coordinates;', //this might be phased out soon (no pun intended) 4 | 'void main (void) {', 5 | 'v_coordinates = a_position * 0.5 + 0.5;', 6 | 'gl_Position = vec4(a_position, 0.0, 1.0);', 7 | '}' 8 | ].join('\n'); 9 | 10 | var SUBTRANSFORM_FRAGMENT_SOURCE = [ 11 | 'precision highp float;', 12 | 13 | 'const float PI = 3.14159265;', 14 | 15 | 'uniform sampler2D u_input;', 16 | 17 | 'uniform float u_resolution;', 18 | 'uniform float u_subtransformSize;', 19 | 20 | 'uniform bool u_horizontal;', 21 | 'uniform bool u_forward;', 22 | 'uniform bool u_normalize;', 23 | 24 | 'vec2 multiplyComplex (vec2 a, vec2 b) {', 25 | 'return vec2(a[0] * b[0] - a[1] * b[1], a[1] * b[0] + a[0] * b[1]);', 26 | '}', 27 | 28 | 'void main (void) {', 29 | 30 | 'float index = 0.0;', 31 | 'if (u_horizontal) {', 32 | 'index = gl_FragCoord.x - 0.5;', 33 | '} else {', 34 | 'index = gl_FragCoord.y - 0.5;', 35 | '}', 36 | 37 | 'float evenIndex = floor(index / u_subtransformSize) * (u_subtransformSize / 2.0) + mod(index, u_subtransformSize / 2.0);', 38 | 39 | 'vec4 even = vec4(0.0), odd = vec4(0.0);', 40 | 41 | 'if (u_horizontal) {', 42 | 'even = texture2D(u_input, vec2(evenIndex + 0.5, gl_FragCoord.y) / u_resolution);', 43 | 'odd = texture2D(u_input, vec2(evenIndex + u_resolution * 0.5 + 0.5, gl_FragCoord.y) / u_resolution);', 44 | '} else {', 45 | 'even = texture2D(u_input, vec2(gl_FragCoord.x, evenIndex + 0.5) / u_resolution);', 46 | 'odd = texture2D(u_input, vec2(gl_FragCoord.x, evenIndex + u_resolution * 0.5 + 0.5) / u_resolution);', 47 | '}', 48 | 49 | //normalisation 50 | 'if (u_normalize) {', 51 | 'even /= u_resolution * u_resolution;', 52 | 'odd /= u_resolution * u_resolution;', 53 | '}', 54 | 55 | 'float twiddleArgument = 0.0;', 56 | 'if (u_forward) {', 57 | 'twiddleArgument = 2.0 * PI * (index / u_subtransformSize);', 58 | '} else {', 59 | 'twiddleArgument = -2.0 * PI * (index / u_subtransformSize);', 60 | '}', 61 | 'vec2 twiddle = vec2(cos(twiddleArgument), sin(twiddleArgument));', 62 | 63 | 'vec2 outputA = even.rg + multiplyComplex(twiddle, odd.rg);', 64 | 'vec2 outputB = even.ba + multiplyComplex(twiddle, odd.ba);', 65 | 66 | 'gl_FragColor = vec4(outputA, outputB);', 67 | 68 | '}' 69 | ].join('\n'); 70 | 71 | var FILTER_FRAGMENT_SOURCE = [ 72 | 'precision highp float;', 73 | 74 | 'uniform sampler2D u_input;', 75 | 'uniform float u_resolution;', 76 | 77 | 'uniform float u_maxEditFrequency;', 78 | 79 | 'uniform sampler2D u_filter;', 80 | 81 | 'void main (void) {', 82 | 'vec2 coordinates = gl_FragCoord.xy - 0.5;', 83 | 'float xFrequency = (coordinates.x < u_resolution * 0.5) ? coordinates.x : coordinates.x - u_resolution;', 84 | 'float yFrequency = (coordinates.y < u_resolution * 0.5) ? coordinates.y : coordinates.y - u_resolution;', 85 | 86 | 'float frequency = sqrt(xFrequency * xFrequency + yFrequency * yFrequency);', 87 | 88 | 'float gain = texture2D(u_filter, vec2(frequency / u_maxEditFrequency, 0.5)).a;', 89 | 'vec4 originalPower = texture2D(u_input, gl_FragCoord.xy / u_resolution);', 90 | 91 | 'gl_FragColor = originalPower * gain;', 92 | 93 | '}', 94 | ].join('\n'); 95 | 96 | var POWER_FRAGMENT_SOURCE = [ 97 | 'precision highp float;', 98 | 99 | 'varying vec2 v_coordinates;', 100 | 101 | 'uniform sampler2D u_spectrum;', 102 | 'uniform float u_resolution;', 103 | 104 | 'vec2 multiplyByI (vec2 z) {', 105 | 'return vec2(-z[1], z[0]);', 106 | '}', 107 | 108 | 'vec2 conjugate (vec2 z) {', 109 | 'return vec2(z[0], -z[1]);', 110 | '}', 111 | 112 | 'vec4 encodeFloat (float v) {', //hack because WebGL cannot read back floats 113 | 'vec4 enc = vec4(1.0, 255.0, 65025.0, 160581375.0) * v;', 114 | 'enc = fract(enc);', 115 | 'enc -= enc.yzww * vec4(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 0.0);', 116 | 'return enc;', 117 | '}', 118 | 119 | 'void main (void) {', 120 | 'vec2 coordinates = v_coordinates - 0.5;', 121 | 122 | 'vec4 z = texture2D(u_spectrum, coordinates);', 123 | 'vec4 zStar = texture2D(u_spectrum, 1.0 - coordinates + 1.0 / u_resolution);', 124 | 'zStar = vec4(conjugate(zStar.xy), conjugate(zStar.zw));', 125 | 126 | 'vec2 r = 0.5 * (z.xy + zStar.xy);', 127 | 'vec2 g = -0.5 * multiplyByI(z.xy - zStar.xy);', 128 | 'vec2 b = z.zw;', 129 | 130 | 'float rPower = length(r);', 131 | 'float gPower = length(g);', 132 | 'float bPower = length(b);', 133 | 134 | 'float averagePower = (rPower + gPower + bPower) / 3.0;', 135 | 'gl_FragColor = encodeFloat(averagePower / (u_resolution * u_resolution));', 136 | '}', 137 | ].join('\n'); 138 | 139 | var IMAGE_FRAGMENT_SOURCE = [ 140 | 'precision highp float;', 141 | 142 | 'varying vec2 v_coordinates;', 143 | 144 | 'uniform float u_resolution;', 145 | 146 | 'uniform sampler2D u_texture;', 147 | 'uniform sampler2D u_spectrum;', 148 | 149 | 'void main (void) {', 150 | 'vec3 image = texture2D(u_texture, v_coordinates).rgb;', 151 | 152 | 'gl_FragColor = vec4(image, 1.0);', 153 | '}', 154 | ].join('\n'); 155 | 156 | var Filterer = function (canvas) { 157 | var canvas = canvas; 158 | var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 159 | 160 | canvas.width = RESOLUTION; 161 | canvas.height = RESOLUTION; 162 | 163 | gl.getExtension('OES_texture_float'); 164 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 165 | 166 | var imageTexture, 167 | pingTexture = buildTexture(gl, PING_TEXTURE_UNIT, gl.RGBA, gl.FLOAT, RESOLUTION, RESOLUTION, null, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE, gl.NEAREST, gl.NEAREST), 168 | pongTexture = buildTexture(gl, PONG_TEXTURE_UNIT, gl.RGBA, gl.FLOAT, RESOLUTION, RESOLUTION, null, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE, gl.NEAREST, gl.NEAREST), 169 | filterTexture = buildTexture(gl, FILTER_TEXTURE_UNIT, gl.RGBA, gl.FLOAT, RESOLUTION, 1, null, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE, gl.NEAREST, gl.NEAREST), 170 | originalSpectrumTexture = buildTexture(gl, ORIGINAL_SPECTRUM_TEXTURE_UNIT, gl.RGBA, gl.FLOAT, RESOLUTION, RESOLUTION, null, gl.REPEAT, gl.REPEAT, gl.NEAREST, gl.NEAREST), 171 | filteredSpectrumTexture = buildTexture(gl, FILTERED_SPECTRUM_TEXTURE_UNIT, gl.RGBA, gl.FLOAT, RESOLUTION, RESOLUTION, null, gl.REPEAT, gl.REPEAT, gl.NEAREST, gl.NEAREST), 172 | filteredImageTexture = buildTexture(gl, FILTERED_IMAGE_TEXTURE_UNIT, gl.RGBA, gl.FLOAT, RESOLUTION, RESOLUTION, null, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE, gl.NEAREST, gl.NEAREST), 173 | readoutTexture = buildTexture(gl, READOUT_TEXTURE_UNIT, gl.RGBA, gl.UNSIGNED_BYTE, RESOLUTION, RESOLUTION, null, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE, gl.NEAREST, gl.NEAREST); 174 | 175 | var pingFramebuffer = buildFramebuffer(gl, pingTexture), 176 | pongFramebuffer = buildFramebuffer(gl, pongTexture), 177 | originalSpectrumFramebuffer = buildFramebuffer(gl, originalSpectrumTexture), 178 | filteredSpectrumFramebuffer = buildFramebuffer(gl, filteredSpectrumTexture), 179 | filteredImageFramebuffer = buildFramebuffer(gl, filteredImageTexture), 180 | readoutFramebuffer = buildFramebuffer(gl, readoutTexture); 181 | 182 | var fullscreenVertexShader = buildShader(gl, gl.VERTEX_SHADER, FULLSCREEN_VERTEX_SOURCE); 183 | 184 | var subtransformProgramWrapper = buildProgramWrapper(gl, 185 | fullscreenVertexShader, 186 | buildShader(gl, gl.FRAGMENT_SHADER, SUBTRANSFORM_FRAGMENT_SOURCE), { 187 | 'a_position': 0 188 | }); 189 | gl.useProgram(subtransformProgramWrapper.program); 190 | gl.uniform1f(subtransformProgramWrapper.uniformLocations['u_resolution'], RESOLUTION); 191 | 192 | var readoutProgram = buildProgramWrapper(gl, 193 | fullscreenVertexShader, 194 | buildShader(gl, gl.FRAGMENT_SHADER, POWER_FRAGMENT_SOURCE), { 195 | 'a_position': 0 196 | }); 197 | gl.useProgram(readoutProgram.program); 198 | gl.uniform1i(readoutProgram.uniformLocations['u_spectrum'], ORIGINAL_SPECTRUM_TEXTURE_UNIT); 199 | gl.uniform1f(readoutProgram.uniformLocations['u_resolution'], RESOLUTION); 200 | 201 | var imageProgram = buildProgramWrapper(gl, 202 | fullscreenVertexShader, 203 | buildShader(gl, gl.FRAGMENT_SHADER, IMAGE_FRAGMENT_SOURCE), { 204 | 'a_position': 0 205 | }); 206 | gl.useProgram(imageProgram.program); 207 | gl.uniform1i(imageProgram.uniformLocations['u_texture'], FILTERED_IMAGE_TEXTURE_UNIT); 208 | 209 | var filterProgram = buildProgramWrapper(gl, 210 | fullscreenVertexShader, 211 | buildShader(gl, gl.FRAGMENT_SHADER, FILTER_FRAGMENT_SOURCE), { 212 | 'a_position': 0 213 | }); 214 | gl.useProgram(filterProgram.program); 215 | gl.uniform1i(filterProgram.uniformLocations['u_input'], ORIGINAL_SPECTRUM_TEXTURE_UNIT); 216 | gl.uniform1i(filterProgram.uniformLocations['u_filter'], FILTER_TEXTURE_UNIT); 217 | gl.uniform1f(filterProgram.uniformLocations['u_resolution'], RESOLUTION); 218 | gl.uniform1f(filterProgram.uniformLocations['u_maxEditFrequency'], END_EDIT_FREQUENCY); 219 | 220 | var fullscreenVertexBuffer = gl.createBuffer(); 221 | gl.bindBuffer(gl.ARRAY_BUFFER, fullscreenVertexBuffer); 222 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]), gl.STATIC_DRAW); 223 | gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); 224 | gl.enableVertexAttribArray(0); 225 | 226 | var iterations = log2(RESOLUTION) * 2; 227 | 228 | var fft = function (inputTextureUnit, outputFramebuffer, width, height, direction) { 229 | gl.useProgram(subtransformProgramWrapper.program); 230 | gl.viewport(0, 0, RESOLUTION, RESOLUTION); 231 | gl.uniform1i(subtransformProgramWrapper.uniformLocations['u_horizontal'], 1); 232 | gl.uniform1i(subtransformProgramWrapper.uniformLocations['u_forward'], direction === FORWARD ? 1 : 0); 233 | for (var i = 0; i < iterations; i += 1) { 234 | if (i === 0) { 235 | gl.bindFramebuffer(gl.FRAMEBUFFER, pingFramebuffer); 236 | gl.uniform1i(subtransformProgramWrapper.uniformLocations['u_input'], inputTextureUnit); 237 | } else if (i === iterations - 1) { 238 | gl.bindFramebuffer(gl.FRAMEBUFFER, outputFramebuffer); 239 | gl.uniform1i(subtransformProgramWrapper.uniformLocations['u_input'], PING_TEXTURE_UNIT); 240 | } else if (i % 2 === 1) { 241 | gl.bindFramebuffer(gl.FRAMEBUFFER, pongFramebuffer); 242 | gl.uniform1i(subtransformProgramWrapper.uniformLocations['u_input'], PING_TEXTURE_UNIT); 243 | } else { 244 | gl.bindFramebuffer(gl.FRAMEBUFFER, pingFramebuffer); 245 | gl.uniform1i(subtransformProgramWrapper.uniformLocations['u_input'], PONG_TEXTURE_UNIT); 246 | } 247 | 248 | if (direction === INVERSE && i === 0) { 249 | gl.uniform1i(subtransformProgramWrapper.uniformLocations['u_normalize'], 1); 250 | } else if (direction === INVERSE && i === 1) { 251 | gl.uniform1i(subtransformProgramWrapper.uniformLocations['u_normalize'], 0); 252 | } 253 | 254 | if (i === iterations / 2) { 255 | gl.uniform1i(subtransformProgramWrapper.uniformLocations['u_horizontal'], 0); 256 | } 257 | 258 | gl.uniform1f(subtransformProgramWrapper.uniformLocations['u_subtransformSize'], Math.pow(2, (i % (iterations / 2)) + 1)); 259 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 260 | } 261 | }; 262 | 263 | this.setImage = function (image) { 264 | gl.activeTexture(gl.TEXTURE0 + IMAGE_TEXTURE_UNIT); 265 | imageTexture = gl.createTexture(); 266 | gl.bindTexture(gl.TEXTURE_2D, imageTexture); 267 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); 268 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 269 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 270 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 271 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 272 | 273 | gl.activeTexture(gl.TEXTURE0 + ORIGINAL_SPECTRUM_TEXTURE_UNIT); 274 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, RESOLUTION, RESOLUTION, 0, gl.RGBA, gl.FLOAT, null); 275 | 276 | fft(IMAGE_TEXTURE_UNIT, originalSpectrumFramebuffer, RESOLUTION, RESOLUTION, FORWARD); 277 | 278 | output(); 279 | 280 | gl.viewport(0, 0, RESOLUTION, RESOLUTION); 281 | 282 | gl.bindFramebuffer(gl.FRAMEBUFFER, readoutFramebuffer); 283 | gl.useProgram(readoutProgram.program); 284 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 285 | 286 | var pixels = new Uint8Array(RESOLUTION * RESOLUTION * 4); 287 | gl.readPixels(0, 0, RESOLUTION, RESOLUTION, gl.RGBA, gl.UNSIGNED_BYTE, pixels); 288 | 289 | var powersByFrequency = {}; 290 | 291 | var pixelIndex = 0; 292 | for (var yIndex = 0; yIndex < RESOLUTION; yIndex += 1) { 293 | for (var xIndex = 0; xIndex < RESOLUTION; xIndex += 1) { 294 | var x = xIndex - RESOLUTION / 2, 295 | y = yIndex - RESOLUTION / 2; 296 | 297 | var r = pixels[pixelIndex] / 255, 298 | g = pixels[pixelIndex + 1] / 255, 299 | b = pixels[pixelIndex + 2] / 255, 300 | a = pixels[pixelIndex + 3] / 255; 301 | 302 | var average = r + g / 255 + b / 65025 + a / 160581375; //unpack float from rgb 303 | 304 | var frequency = Math.sqrt(x * x + y * y); 305 | 306 | if (powersByFrequency[frequency] === undefined) { 307 | powersByFrequency[frequency] = []; 308 | } 309 | powersByFrequency[frequency].push(average); 310 | 311 | pixelIndex += 4; 312 | } 313 | } 314 | 315 | return powersByFrequency; 316 | }; 317 | 318 | this.filter = function (filterArray) { 319 | gl.activeTexture(gl.TEXTURE0 + FILTER_TEXTURE_UNIT); 320 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, filterArray.length, 1, 0, gl.ALPHA, gl.FLOAT, filterArray); 321 | 322 | gl.useProgram(filterProgram.program); 323 | 324 | gl.bindFramebuffer(gl.FRAMEBUFFER, filteredSpectrumFramebuffer); 325 | gl.viewport(0, 0, RESOLUTION, RESOLUTION); 326 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 327 | 328 | fft(FILTERED_SPECTRUM_TEXTURE_UNIT, filteredImageFramebuffer, RESOLUTION, RESOLUTION, INVERSE); 329 | 330 | output(); 331 | }; 332 | 333 | var output = function () { 334 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 335 | 336 | gl.viewport(0, 0, RESOLUTION, RESOLUTION); 337 | gl.useProgram(imageProgram.program); 338 | gl.uniform1f(imageProgram.uniformLocations['u_resolution'], RESOLUTION); 339 | gl.uniform1i(imageProgram.uniformLocations['u_texture'], FILTERED_IMAGE_TEXTURE_UNIT); 340 | gl.uniform1i(imageProgram.uniformLocations['u_spectrum'], FILTERED_SPECTRUM_TEXTURE_UNIT); 341 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 342 | }; 343 | }; -------------------------------------------------------------------------------- /filtering.css: -------------------------------------------------------------------------------- 1 | /* < 1280 width overall */ 2 | /* 20px margins */ 3 | 4 | body { 5 | background: #222222; 6 | width: 100%; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | canvas { 11 | display: block; 12 | } 13 | 14 | #title { 15 | display: inline-block; 16 | color: white; 17 | font-family: 'PT Sans', Arial, Helvetica, sans-serif; 18 | font-size: 28px; 19 | font-weight: bold; 20 | 21 | padding: 10px 20px; 22 | 23 | margin-top: 20px; 24 | 25 | background-color: rgb(50, 50, 50); 26 | } 27 | 28 | #wrapper { 29 | width: 1260px; 30 | position: relative; 31 | margin: 20px auto 0 auto; 32 | } 33 | 34 | #columns { 35 | margin-top: 20px; 36 | } 37 | 38 | #left { 39 | float: left; 40 | } 41 | 42 | #right { 43 | float: left; 44 | margin-left: 20px; 45 | } 46 | 47 | #filterer { 48 | left: 0; 49 | z-index: 0; 50 | } 51 | 52 | #image-toggle { 53 | color: white; 54 | font-family: 'PT Sans', Arial, Helvetica, sans-serif; 55 | font-size: 13px; 56 | 57 | background-color: rgb(50, 50, 50); 58 | 59 | padding: 10px 15px; 60 | 61 | display: inline-block; 62 | 63 | margin-top: 20px; 64 | } 65 | 66 | #image-toggle span { 67 | color: white; 68 | text-decoration: underline; 69 | 70 | cursor: pointer; 71 | } 72 | 73 | #editor-wrapper { 74 | z-index: 0; 75 | position: relative; 76 | margin-top: 20px; 77 | } 78 | 79 | #instructions { 80 | position: absolute; 81 | top: 5px; 82 | right: 5px; 83 | text-align: right; 84 | 85 | font-family: 'PT Sans', Arial, Helvetica, sans-serif; 86 | color: #cccccc; 87 | font-size: 12px; 88 | 89 | pointer-events: none; 90 | } 91 | 92 | #editor { 93 | } 94 | 95 | #chart-wrapper { 96 | z-index: 0; 97 | position: relative; 98 | } 99 | 100 | #chart { 101 | } 102 | 103 | .label { 104 | font-family: 'PT Sans', Arial, Helvetica, sans-serif; 105 | font-size: 13px; 106 | color: white; 107 | 108 | position: absolute; 109 | 110 | z-index: 5; 111 | } 112 | 113 | #frequency { 114 | right: 5px; 115 | bottom: 5px; 116 | } 117 | 118 | #frequency2 { 119 | right: 5px; 120 | bottom: 5px; 121 | } 122 | 123 | #power { 124 | left: 5px; 125 | top: 5px; 126 | } 127 | 128 | #gain { 129 | left: 5px; 130 | top: 5px; 131 | } 132 | 133 | #info { 134 | font-size: 13px; 135 | font-family: Arial; 136 | color: white; 137 | 138 | line-height: 20px; 139 | 140 | background-color: rgb(50, 50, 50); 141 | padding: 15px 20px; 142 | 143 | margin-top: 20px; 144 | 145 | display: inline-block; 146 | } 147 | 148 | #info span { 149 | text-decoration: underline; 150 | 151 | cursor: pointer; 152 | } 153 | 154 | #unsupported { 155 | font-size: 13px; 156 | font-family: 'PT Sans', Arial, Helvetica, sans-serif; 157 | color: white; 158 | 159 | line-height: 20px; 160 | 161 | background-color: rgb(50, 50, 50); 162 | padding: 15px 20px; 163 | 164 | margin-top: 20px; 165 | 166 | display: none; 167 | } 168 | 169 | #unsupported a{ 170 | color: white; 171 | } 172 | 173 | #footer { 174 | text-align: right; 175 | 176 | font-size: 13px; 177 | font-family: 'PT Sans', Arial, Helvetica, sans-serif; 178 | color: white; 179 | 180 | margin-top: 20px; 181 | } 182 | 183 | #footer a { 184 | color: white; 185 | } -------------------------------------------------------------------------------- /images/desert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dli/filtering/071005418896fe26104f22a34a67d3add54eaec2/images/desert.jpg -------------------------------------------------------------------------------- /images/fields.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dli/filtering/071005418896fe26104f22a34a67d3add54eaec2/images/fields.jpg -------------------------------------------------------------------------------- /images/milky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dli/filtering/071005418896fe26104f22a34a67d3add54eaec2/images/milky.jpg -------------------------------------------------------------------------------- /images/valley.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dli/filtering/071005418896fe26104f22a34a67d3add54eaec2/images/valley.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fourier Image Filtering 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
Fourier Image Filtering
15 | 16 |
17 | 18 |
19 | 20 |
Previous Image | Next Image
21 |
22 | 23 | 54 | 55 |
56 | 57 |
58 |
59 | Unfortunately, your browser does not appear to support the required features. Back to David.Li 60 |
61 | 62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var main = function () { 2 | var currentImageIndex = 0; 3 | var images = []; 4 | 5 | var curve = new Curve(document.getElementById('editor'), { 6 | pointSize: 6, 7 | pointColor: 'white', 8 | selectionRadius: 20, 9 | addDistance: 20, 10 | overPointCursor: 'move', 11 | overCurveCursor: 'crosshair', 12 | defaultCursor: 'default', 13 | curveColor: 'white', 14 | backgroundColor: 'rgb(50, 50, 50)', 15 | minSpacing: 5 16 | }); 17 | 18 | curve.add(200, 75); 19 | 20 | var chart = new Chart(document.getElementById('chart')); 21 | 22 | var filterer = new Filterer(document.getElementById('filterer')); 23 | 24 | curve.addChangeListener(function () { 25 | var filterArray = new Float32Array(CURVE_CANVAS_WIDTH); 26 | 27 | curve.iterate(0, CURVE_CANVAS_WIDTH - 1, 1, function (x, y) { 28 | filterArray[x] = Math.max(0.0, y / CURVE_CANVAS_HEIGHT * CURVE_SCALE); 29 | }); 30 | 31 | chart.draw(curve); 32 | 33 | filterer.filter(filterArray); 34 | }); 35 | curve.triggerChangeListeners(); 36 | 37 | var imagesLoaded = 0; 38 | for (var i = 0; i < IMAGE_URLS.length; ++i) { 39 | var image = new Image(); 40 | image.onload = function () { 41 | imagesLoaded += 1; 42 | if (imagesLoaded === IMAGE_URLS.length) { //all images have been loaded 43 | var powersByFrequency = filterer.setImage(images[0]); 44 | chart.setData(powersByFrequency, curve); 45 | curve.triggerChangeListeners(); 46 | } 47 | }; 48 | image.src = IMAGE_URLS[i]; 49 | 50 | images[i] = image; 51 | } 52 | 53 | document.getElementById('next').onclick = function () { 54 | currentImageIndex = mod(currentImageIndex + 1, images.length); 55 | var powersByFrequency = filterer.setImage(images[currentImageIndex]); 56 | chart.setData(powersByFrequency, curve); 57 | curve.triggerChangeListeners(); 58 | }; 59 | 60 | document.getElementById('previous').onclick = function () { 61 | currentImageIndex = mod(currentImageIndex - 1, images.length); 62 | var powersByFrequency = filterer.setImage(images[currentImageIndex]); 63 | chart.setData(powersByFrequency, curve); 64 | curve.triggerChangeListeners(); 65 | }; 66 | 67 | document.getElementById('gaussian').onclick = function () { 68 | curve.setPoints([ 69 | [11, 76], 70 | [56, 73], 71 | [187, 17], 72 | [314, 0] 73 | ]); 74 | }; 75 | 76 | document.getElementById('sharpen').onclick = function () { 77 | curve.setPoints([ 78 | [173, 74], 79 | [252, 79], 80 | [416, 134], 81 | [521, 142] 82 | ]); 83 | }; 84 | 85 | document.getElementById('edges').onclick = function () { 86 | curve.setPoints([ 87 | [9, 0], 88 | [41, 2], 89 | [411, 122], 90 | [675, 150] 91 | ]); 92 | }; 93 | 94 | }; 95 | 96 | if (hasWebGLSupportWithExtensions(['OES_texture_float'])) { 97 | main(); 98 | } else { 99 | document.getElementById('columns').style.display = 'none'; 100 | document.getElementById('unsupported').style.display = 'inline-block'; 101 | } -------------------------------------------------------------------------------- /shared.js: -------------------------------------------------------------------------------- 1 | //all images by Trey Ratcliff 2 | var IMAGE_URLS = [ 3 | 'images/fields.jpg', 4 | 'images/desert.jpg', 5 | 'images/milky.jpg', 6 | 'images/valley.jpg', 7 | ]; 8 | 9 | var PING_TEXTURE_UNIT = 0, 10 | PONG_TEXTURE_UNIT = 1, 11 | FILTER_TEXTURE_UNIT = 2, 12 | ORIGINAL_SPECTRUM_TEXTURE_UNIT = 3, 13 | FILTERED_SPECTRUM_TEXTURE_UNIT = 4, 14 | IMAGE_TEXTURE_UNIT = 5, 15 | FILTERED_IMAGE_TEXTURE_UNIT = 6, 16 | READOUT_TEXTURE_UNIT = 7; 17 | 18 | var RESOLUTION = 512; 19 | 20 | var FORWARD = 0, 21 | INVERSE = 1; 22 | 23 | var END_EDIT_FREQUENCY = 150.0; 24 | 25 | var CHART_SCALE = 100000; 26 | 27 | var CURVE_CANVAS_WIDTH = 720; 28 | var CURVE_CANVAS_HEIGHT = 150.0; 29 | var CURVE_SCALE = 2.0; 30 | 31 | var CHART_BACKGROUND_COLOR = 'rgb(50, 50, 50)'; 32 | 33 | var CHART_PRIMARY_COLOR = 'rgb(150, 150, 150)'; 34 | var CHART_SECONDARY_COLOR = 'rgb(75, 75, 75)'; 35 | 36 | var log2 = function (x) { 37 | return Math.log(x) / Math.log(2); 38 | }; 39 | 40 | var buildShader = function (gl, type, source) { 41 | var shader = gl.createShader(type); 42 | gl.shaderSource(shader, source); 43 | gl.compileShader(shader); 44 | console.log(gl.getShaderInfoLog(shader)); 45 | return shader; 46 | }; 47 | 48 | var buildProgramWrapper = function (gl, vertexShader, fragmentShader, attributeLocations) { 49 | var programWrapper = {}; 50 | 51 | var program = gl.createProgram(); 52 | gl.attachShader(program, vertexShader); 53 | gl.attachShader(program, fragmentShader); 54 | for (var attributeName in attributeLocations) { 55 | gl.bindAttribLocation(program, attributeLocations[attributeName], attributeName); 56 | } 57 | gl.linkProgram(program); 58 | var uniformLocations = {}; 59 | var numberOfUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); 60 | for (var i = 0; i < numberOfUniforms; i += 1) { 61 | var activeUniform = gl.getActiveUniform(program, i), 62 | uniformLocation = gl.getUniformLocation(program, activeUniform.name); 63 | uniformLocations[activeUniform.name] = uniformLocation; 64 | } 65 | 66 | programWrapper.program = program; 67 | programWrapper.uniformLocations = uniformLocations; 68 | 69 | return programWrapper; 70 | }; 71 | 72 | var buildTexture = function (gl, unit, format, type, width, height, data, wrapS, wrapT, minFilter, magFilter) { 73 | var texture = gl.createTexture(); 74 | gl.activeTexture(gl.TEXTURE0 + unit); 75 | gl.bindTexture(gl.TEXTURE_2D, texture); 76 | gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, type, data); 77 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS); 78 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT); 79 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter); 80 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter); 81 | return texture; 82 | }; 83 | 84 | var buildFramebuffer = function (gl, attachment) { 85 | var framebuffer = gl.createFramebuffer(); 86 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 87 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, attachment, 0); 88 | return framebuffer; 89 | }; 90 | 91 | var getMousePosition = function (event, element) { 92 | var boundingRect = element.getBoundingClientRect(); 93 | return { 94 | x: event.clientX - boundingRect.left, 95 | y: event.clientY - boundingRect.top 96 | }; 97 | }; 98 | 99 | var clamp = function (x, min, max) { 100 | return Math.min(Math.max(x, min), max); 101 | }; 102 | 103 | var averageArray = function (array) { 104 | var sum = 0; 105 | for (var i = 0; i < array.length; i += 1) { 106 | sum += array[i]; 107 | } 108 | return sum / array.length; 109 | }; 110 | 111 | var hasWebGLSupportWithExtensions = function (extensions) { 112 | var canvas = document.createElement('canvas'); 113 | var gl = null; 114 | try { 115 | gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 116 | } catch (e) { 117 | return false; 118 | } 119 | if (gl === null) { 120 | return false; 121 | } 122 | 123 | for (var i = 0; i < extensions.length; ++i) { 124 | if (gl.getExtension(extensions[i]) === null) { 125 | return false 126 | } 127 | } 128 | 129 | return true; 130 | }; 131 | 132 | var mod = function (x, n) { //positive modulo 133 | var m = x % n; 134 | return m < 0 ? m + n : m; 135 | }; --------------------------------------------------------------------------------