initial commit

Georg Fischer 2013-06-27 14:52:28 +02:00
commit cca32d920e
19 changed files with 4103 additions and 0 deletions

25
.gitignore vendored Normal file

@ -0,0 +1,25 @@
#Mac OS files
.DS_Store
.AppleDouble
.LSOverride
Icon
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# SublimeText project files
*.sublime-workspace

25
README.md Normal file

@ -0,0 +1,25 @@
image triangulation experiment
===
this is an experiment for the web browser. it uses the [delaunay triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation) algorithm to alter an image.
[![triangulation experiment screen shot](http://dl.dropboxusercontent.com/u/1098704/Screenshots/github-triangulation.png)](http://snorpey.github.io/triangulation/)
[online demo](http://snorpey.github.io/triangulation/)
this experiment is very much based on the [triangulation image generator](http://jsdo.it/akm2/xoYx) script. it includes several speed enhancements. it is my goal to make it fast enough for use with real time streaming input, e.g. from a [web cam](https://github.com/snorpey/photobooth).
you can fine another experiment that applies the the triangulation to text input here: [http://snorpey.github.io/text-triangulation/](http://snorpey.github.io/text-triangulation/)
third party code used in this experiment
---
* some parts of the code code from [triangulation image generator](http://jsdo.it/akm2/xoYx) by [akm2](http://codepen.io/akm2), MIT license
* [delaunay js](https://github.com/ironwallaby/delaunay) by [ironwallaby](https://github.com/ironwallaby), public domain
* [html5slider](http://frankyan.com/labs/html5slider/) by [fryn](https://github.com/fryn), MIT license
* [js signals](http://millermedeiros.github.io/js-signals/) by [millermedeiros](https://github.com/millermedeiros), MIT license
* [superfast boxblur for canvas](http://quasimondo.com/BoxBlurForCanvas/FastBlurDemo.html) by [quasimondo](https://github.com/quasimondo), MIT license
* [require js](http://requirejs.org/), by [jrburke](jrburke), BSD & MIT license
license
---
[MIT License](http://www.opensource.org/licenses/mit-license.php)

38
index.html Normal file

@ -0,0 +1,38 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>image glitch experiment</title>
<link rel="stylesheet" href="styles/global.css" />
</head>
<body>
<article class="content">
<h1 class="headline">glitch images</h1>
<p>drop an image in the browser.</p>
</article>
<div class="content" id="controls">
<div class="control-wrapper">
<label class="control-label" for="seed">seed</label>
<input class="control-input" type="range" id="seed" min="0" max="100" value="50" />
</div>
<div class="control-wrapper">
<label class="control-label" for="quality">quality</label>
<input class="control-input" type="range" id="quality" min="0" max="100" value="50" />
</div>
<div class="control-wrapper">
<label class="control-label" for="offset">px offset</label>
<input class="control-input" type="range" id="offset" min="0" max="100" value="50" />
</div>
<div class="control-wrapper">
<label class="control-label" for="iterations">iterations</label>
<input class="control-input" type="range" id="iterations" min="1" max="20" value="5" />
</div>
</div>
<div class="export-wrapper">
<button id="save-button" class="button">export image</button>
<a id="png-button" download="triangulated-image.png" target="_blank" class="download-link">download bitmap file<span> (.png)</span></a>
</div>
<canvas id="canvas"></canvas>
<script src="scripts/lib/require-2.1.4.js" data-main="scripts/src/main"></script>
</body>
</html>

BIN
lincoln.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

@ -0,0 +1,48 @@
/*global define*/
define(
function()
{
var tests = {
'canvas': { required: true, test: function(){ return !! document.createElement('canvas').getContext; } },
'query-selector-all': { required: false, test: function(){ return !! document.querySelectorAll; } },
'drag-drop': { required: false, test: function(){ return 'draggable' in document.createElement('span'); } },
'file-api': { required: false, test: function(){ return typeof FileReader !== 'undefined'; } }
};
function test( success, error )
{
var required_supported = true;
var results = { };
var required_features_missing = [ ];
for ( var key in tests )
{
var result = tests[key].test();
if ( ! result )
{
if ( tests[key].required )
{
required_supported = false;
required_features_missing.push( key );
}
}
results[key] = result;
}
if ( required_supported )
{
success( results );
}
else
{
error( required_features_missing, results );
}
}
return test;
}
);

206
scripts/aux/glitch.js Normal file

@ -0,0 +1,206 @@
/*global define*/
define(
function()
{
var canvas = document.createElement( 'canvas' );
var ctx = canvas.getContext( '2d' );
var tmp_canvas = document.createElement( 'canvas' );
var tmp_ctx = tmp_canvas.getContext( '2d' );
var canvas_size = { width: 10, height: 10 };
var base64_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var base64_map = base64_chars.split( '' );
var reverse_base64_map = { };
var iterations;
var quality;
var seed;
var offset;
var base64;
var byte_array;
var jpg_header_length;
var img;
var new_image_data;
var i;
var len;
base64_map.forEach( function( val, key ) { reverse_base64_map[val] = key; } );
function getGlitchedImageSrc( image_data, input, callback )
{
seed = input.seed / 100;
quality = input.quality / 100;
offset = input.offset / 100;
iterations = input.iterations;
updateCanvasSize( image_data.width, image_data.height );
base64 = getBase64FromImageData( image_data, quality );
byte_array = base64ToByteArray( base64 );
jpg_header_length = getJpegHeaderSize( byte_array );
for ( i = 0; i < iterations; i++ )
{
glitchJpegBytes( byte_array, jpg_header_length, seed, offset, i );
}
img = new Image();
img.onload = function() {
ctx.drawImage( img, 0, 0 );
new_image_data = ctx.getImageData( 0, 0, image_data.width, image_data.height );
callback( new_image_data );
};
img.src = byteArrayToBase64( byte_array );
}
function updateCanvasSize( width, height )
{
var updated = false;
if ( canvas_size.width !== width ) { canvas_size.width = width; updated = true; }
if ( canvas_size.height !== height ) { canvas_size.height = height; updated = true; }
if ( updated )
{
resizeCanvas( tmp_canvas, canvas_size );
resizeCanvas( canvas, canvas_size );
}
}
function resizeCanvas( canvas, img )
{
canvas.width = img.width;
canvas.height = img.height;
}
function glitchJpegBytes( byte_array, jpg_header_length, seed, offset, iteration )
{
var it = ( iteration + 1 ) / 10;
var max_index = byte_array.length - jpg_header_length - 4;
var px_index = it * max_index + offset * 10;
if ( px_index > max_index )
{
px_index = px_index;
}
var index = Math.floor( jpg_header_length + px_index );
byte_array[index] = Math.floor( seed * 256 );
}
function getBase64FromImageData( image_data, quality )
{
var q = typeof quality === 'number' && quality < 1 && quality > 0 ? quality : 0.1;
tmp_ctx.putImageData( image_data, 0, 0 );
return tmp_canvas.toDataURL( 'image/jpeg', q );
}
function getJpegHeaderSize( data )
{
var result = 417;
for ( var i = 0, l = data.length; i < l; i++ )
{
if ( data[i] === 0xFF && data[i + 1] === 0xDA )
{
result = i + 2;
break;
}
}
return result;
}
// https://github.com/mutaphysis/smackmyglitchupjs/blob/master/glitch.html
// base64 is 2^6, byte is 2^8, every 4 base64 values create three bytes
function base64ToByteArray( str )
{
var result = [ ];
var digit_num;
var cur;
var prev;
for ( var i = 23, l = str.length; i < l; i++ )
{
cur = reverse_base64_map[ str.charAt( i ) ];
digit_num = ( i - 23 ) % 4;
switch ( digit_num )
{
// case 0: first digit - do nothing, not enough info to work with
case 1: // second digit
result.push( prev << 2 | cur >> 4 );
break;
case 2: // third digit
result.push( ( prev & 0x0f ) << 4 | cur >> 2 );
break;
case 3: // fourth digit
result.push( ( prev & 3 ) << 6 | cur );
break;
}
prev = cur;
}
return result;
}
function byteArrayToBase64( arr )
{
var result = [ 'data:image/jpeg;base64,' ];
var byte_num;
var cur;
var prev;
var i;
for ( var i = 0, l = arr.length; i < l; i++ )
{
cur = arr[i];
byte_num = i % 3;
switch ( byte_num )
{
case 0: // first byte
result.push( base64_map[ cur >> 2 ] );
break;
case 1: // second byte
result.push( base64_map[( prev & 3 ) << 4 | ( cur >> 4 )] );
break;
case 2: // third byte
result.push( base64_map[( prev & 0x0f ) << 2 | ( cur >> 6 )] );
result.push( base64_map[cur & 0x3f] );
break;
}
prev = cur;
}
if ( byte_num === 0 )
{
result.push( base64_map[( prev & 3 ) << 4] );
result.push( '==' );
}
else if ( byte_num === 1 )
{
result.push( base64_map[( prev & 0x0f ) << 2] );
result.push( '=' );
}
return result.join( '' );
}
function getImageDataCopy( image_data )
{
var copy = tmp_ctx.createImageData( image_data.width, image_data.height );
copy.data.set( image_data.data );
return copy;
}
return getGlitchedImageSrc;
}
);

182
scripts/lib/delaunay.js Normal file

@ -0,0 +1,182 @@
// https://github.com/ironwallaby/delaunay/blob/master/delaunay.js
function Triangle(a, b, c) {
this.a = a
this.b = b
this.c = c
var A = b.x - a.x,
B = b.y - a.y,
C = c.x - a.x,
D = c.y - a.y,
E = A * (a.x + b.x) + B * (a.y + b.y),
F = C * (a.x + c.x) + D * (a.y + c.y),
G = 2 * (A * (c.y - b.y) - B * (c.x - b.x)),
minx, miny, dx, dy
/* If the points of the triangle are collinear, then just find the
* extremes and use the midpoint as the center of the circumcircle. */
if(Math.abs(G) < 0.000001) {
minx = Math.min(a.x, b.x, c.x)
miny = Math.min(a.y, b.y, c.y)
dx = (Math.max(a.x, b.x, c.x) - minx) * 0.5
dy = (Math.max(a.y, b.y, c.y) - miny) * 0.5
this.x = minx + dx
this.y = miny + dy
this.r = dx * dx + dy * dy
}
else {
this.x = (D*E - B*F) / G
this.y = (A*F - C*E) / G
dx = this.x - a.x
dy = this.y - a.y
this.r = dx * dx + dy * dy
}
}
Triangle.prototype.draw = function(ctx) {
ctx.beginPath()
ctx.moveTo(this.a.x, this.a.y)
ctx.lineTo(this.b.x, this.b.y)
ctx.lineTo(this.c.x, this.c.y)
ctx.closePath()
ctx.stroke()
}
function byX(a, b) {
return b.x - a.x
}
function dedup(edges) {
var j = edges.length,
a, b, i, m, n
outer: while(j) {
b = edges[--j]
a = edges[--j]
i = j
while(i) {
n = edges[--i]
m = edges[--i]
if((a === m && b === n) || (a === n && b === m)) {
edges.splice(j, 2)
edges.splice(i, 2)
j -= 2
continue outer
}
}
}
}
function triangulate(vertices) {
/* Bail if there aren't enough vertices to form any triangles. */
if(vertices.length < 3)
return []
/* Ensure the vertex array is in order of descending X coordinate
* (which is needed to ensure a subquadratic runtime), and then find
* the bounding box around the points. */
vertices.sort(byX)
var i = vertices.length - 1,
xmin = vertices[i].x,
xmax = vertices[0].x,
ymin = vertices[i].y,
ymax = ymin
while(i--) {
if(vertices[i].y < ymin) ymin = vertices[i].y
if(vertices[i].y > ymax) ymax = vertices[i].y
}
/* Find a supertriangle, which is a triangle that surrounds all the
* vertices. This is used like something of a sentinel value to remove
* cases in the main algorithm, and is removed before we return any
* results.
*
* Once found, put it in the "open" list. (The "open" list is for
* triangles who may still need to be considered; the "closed" list is
* for triangles which do not.) */
var dx = xmax - xmin,
dy = ymax - ymin,
dmax = (dx > dy) ? dx : dy,
xmid = (xmax + xmin) * 0.5,
ymid = (ymax + ymin) * 0.5,
open = [
new Triangle(
{x: xmid - 20 * dmax, y: ymid - dmax, __sentinel: true},
{x: xmid , y: ymid + 20 * dmax, __sentinel: true},
{x: xmid + 20 * dmax, y: ymid - dmax, __sentinel: true}
)
],
closed = [],
edges = [],
j, a, b
/* Incrementally add each vertex to the mesh. */
i = vertices.length
while(i--) {
/* For each open triangle, check to see if the current point is
* inside it's circumcircle. If it is, remove the triangle and add
* it's edges to an edge list. */
edges.length = 0
j = open.length
while(j--) {
/* If this point is to the right of this triangle's circumcircle,
* then this triangle should never get checked again. Remove it
* from the open list, add it to the closed list, and skip. */
dx = vertices[i].x - open[j].x
if(dx > 0 && dx * dx > open[j].r) {
closed.push(open[j])
open.splice(j, 1)
continue
}
/* If not, skip this triangle. */
dy = vertices[i].y - open[j].y
if(dx * dx + dy * dy > open[j].r)
continue
/* Remove the triangle and add it's edges to the edge list. */
edges.push(
open[j].a, open[j].b,
open[j].b, open[j].c,
open[j].c, open[j].a
)
open.splice(j, 1)
}
/* Remove any doubled edges. */
dedup(edges)
/* Add a new triangle for each edge. */
j = edges.length
while(j) {
b = edges[--j]
a = edges[--j]
open.push(new Triangle(a, b, vertices[i]))
}
}
/* Copy any remaining open triangles to the closed list, and then
* remove any triangles that share a vertex with the supertriangle. */
Array.prototype.push.apply(closed, open)
i = closed.length
while(i--)
if(closed[i].a.__sentinel ||
closed[i].b.__sentinel ||
closed[i].c.__sentinel)
closed.splice(i, 1)
/* Yay, we're done! */
return closed
}
if (typeof module !== 'undefined') {
module.exports = {
Triangle: Triangle,
triangulate: triangulate
}
}

285
scripts/lib/html5slider.js Normal file

@ -0,0 +1,285 @@
/*
html5slider - a JS implementation of <input type=range> for Firefox 16 and up
https://github.com/fryn/html5slider
Copyright (c) 2010-2013 Frank Yan, <http://frankyan.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
(function() {
// test for native support
var test = document.createElement('input');
try {
test.type = 'range';
if (test.type == 'range')
return;
} catch (e) {
return;
}
// test for required property support
test.style.background = 'linear-gradient(red, red)';
if (!test.style.backgroundImage || !('MozAppearance' in test.style) ||
!document.mozSetImageElement || !this.MutationObserver)
return;
var scale;
var isMac = navigator.platform == 'MacIntel';
var thumb = {
radius: isMac ? 9 : 6,
width: isMac ? 22 : 12,
height: isMac ? 16 : 20
};
var track = 'linear-gradient(transparent ' + (isMac ?
'6px, #999 6px, #999 7px, #ccc 8px, #bbb 9px, #bbb 10px, transparent 10px' :
'9px, #999 9px, #bbb 10px, #fff 11px, transparent 11px') +
', transparent)';
var styles = {
'min-width': thumb.width + 'px',
'min-height': thumb.height + 'px',
'max-height': thumb.height + 'px',
padding: '0 0 ' + (isMac ? '2px' : '1px'),
border: 0,
'border-radius': 0,
cursor: 'default',
'text-indent': '-999999px' // -moz-user-select: none; breaks mouse capture
};
var options = {
attributes: true,
attributeFilter: ['min', 'max', 'step', 'value']
};
var forEach = Array.prototype.forEach;
var onInput = document.createEvent('HTMLEvents');
onInput.initEvent('input', true, false);
var onChange = document.createEvent('HTMLEvents');
onChange.initEvent('change', true, false);
if (document.readyState == 'loading')
document.addEventListener('DOMContentLoaded', initialize, true);
else
initialize();
function initialize() {
// create initial sliders
forEach.call(document.querySelectorAll('input[type=range]'), transform);
// create sliders on-the-fly
new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes)
forEach.call(mutation.addedNodes, function(node) {
check(node);
if (node.childElementCount)
forEach.call(node.querySelectorAll('input'), check);
});
});
}).observe(document, { childList: true, subtree: true });
}
function check(input) {
if (input.localName == 'input' && input.type != 'range' &&
input.getAttribute('type') == 'range')
transform(input);
}
function transform(slider) {
var isValueSet, areAttrsSet, isChanged, isClick, prevValue, rawValue, prevX;
var min, max, step, range, value = slider.value;
// lazily create shared slider affordance
if (!scale) {
scale = document.body.appendChild(document.createElement('hr'));
style(scale, {
'-moz-appearance': isMac ? 'scale-horizontal' : 'scalethumb-horizontal',
display: 'block',
visibility: 'visible',
opacity: 1,
position: 'fixed',
top: '-999999px'
});
document.mozSetImageElement('__sliderthumb__', scale);
}
// reimplement value and type properties
var getValue = function() { return '' + value; };
var setValue = function setValue(val) {
value = '' + val;
isValueSet = true;
draw();
delete slider.value;
slider.value = value;
Object.defineProperty(slider, 'value', {
get: getValue,
set: setValue
});
};
Object.defineProperty(slider, 'value', {
get: getValue,
set: setValue
});
Object.defineProperty(slider, 'type', {
get: function() { return 'range'; }
});
// sync properties with attributes
['min', 'max', 'step'].forEach(function(prop) {
if (slider.hasAttribute(prop))
areAttrsSet = true;
Object.defineProperty(slider, prop, {
get: function() { return this.hasAttribute(prop) ? this.getAttribute(prop) : ''; },
set: function(val) { val === null ? this.removeAttribute(prop) : this.setAttribute(prop, val); }
});
});
// initialize slider
slider.readOnly = true;
style(slider, styles);
update();
new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName != 'value') {
update();
areAttrsSet = true;
}
// note that value attribute only sets initial value
else if (!isValueSet) {
value = slider.getAttribute('value');
draw();
}
});
}).observe(slider, options);
slider.addEventListener('mousedown', onDragStart, true);
slider.addEventListener('keydown', onKeyDown, true);
slider.addEventListener('focus', onFocus, true);
slider.addEventListener('blur', onBlur, true);
function onDragStart(e) {
isClick = true;
setTimeout(function() { isClick = false; }, 0);
if (e.button || !range)
return;
var width = parseFloat(getComputedStyle(this, 0).width);
var multiplier = (width - thumb.width) / range;
if (!multiplier)
return;
// distance between click and center of thumb
var dev = e.clientX - this.getBoundingClientRect().left - thumb.width / 2 -
(value - min) * multiplier;
// if click was not on thumb, move thumb to click location
if (Math.abs(dev) > thumb.radius) {
isChanged = true;
this.value -= -dev / multiplier;
}
rawValue = value;
prevX = e.clientX;
this.addEventListener('mousemove', onDrag, true);
this.addEventListener('mouseup', onDragEnd, true);
}
function onDrag(e) {
var width = parseFloat(getComputedStyle(this, 0).width);
var multiplier = (width - thumb.width) / range;
if (!multiplier)
return;
rawValue += (e.clientX - prevX) / multiplier;
prevX = e.clientX;
isChanged = true;
this.value = rawValue;
}
function onDragEnd() {
this.removeEventListener('mousemove', onDrag, true);
this.removeEventListener('mouseup', onDragEnd, true);
slider.dispatchEvent(onChange);
}
function onKeyDown(e) {
if (e.keyCode > 36 && e.keyCode < 41) { // 37-40: left, up, right, down
onFocus.call(this);
isChanged = true;
this.value = value + (e.keyCode == 38 || e.keyCode == 39 ? step : -step);
}
}
function onFocus() {
if (!isClick)
this.style.boxShadow = !isMac ? '0 0 0 2px #fb0' :
'inset 0 0 20px rgba(0,127,255,.1), 0 0 1px rgba(0,127,255,.4)';
}
function onBlur() {
this.style.boxShadow = '';
}
// determines whether value is valid number in attribute form
function isAttrNum(value) {
return !isNaN(value) && +value == parseFloat(value);
}
// validates min, max, and step attributes and redraws
function update() {
min = isAttrNum(slider.min) ? +slider.min : 0;
max = isAttrNum(slider.max) ? +slider.max : 100;
if (max < min)
max = min > 100 ? min : 100;
step = isAttrNum(slider.step) && slider.step > 0 ? +slider.step : 1;
range = max - min;
draw(true);
}
// recalculates value property
function calc() {
if (!isValueSet && !areAttrsSet)
value = slider.getAttribute('value');
if (!isAttrNum(value))
value = (min + max) / 2;;
// snap to step intervals (WebKit sometimes does not - bug?)
value = Math.round((value - min) / step) * step + min;
if (value < min)
value = min;
else if (value > max)
value = min + ~~(range / step) * step;
}
// renders slider using CSS background ;)
function draw(attrsModified) {
calc();
if (isChanged && value != prevValue)
slider.dispatchEvent(onInput);
isChanged = false;
if (!attrsModified && value == prevValue)
return;
prevValue = value;
var position = range ? (value - min) / range * 100 : 0;
var bg = '-moz-element(#__sliderthumb__) ' + position + '% no-repeat, ';
style(slider, { background: bg + track });
}
}
function style(element, styles) {
for (var prop in styles)
element.style.setProperty(prop, styles[prop], 'important');
}
})();

2000
scripts/lib/require-2.1.4.js Normal file

File diff suppressed because it is too large Load Diff

445
scripts/lib/signals-1.0.0.js Executable file

@ -0,0 +1,445 @@
/*jslint onevar:true, undef:true, newcap:true, regexp:true, bitwise:true, maxerr:50, indent:4, white:false, nomen:false, plusplus:false */
/*global define:false, require:false, exports:false, module:false, signals:false */
/** @license
* JS Signals <http://millermedeiros.github.com/js-signals/>
* Released under the MIT license
* Author: Miller Medeiros
* Version: 1.0.0 - Build: 268 (2012/11/29 05:48 PM)
*/
(function(global){
// SignalBinding -------------------------------------------------
//================================================================
/**
* Object that represents a binding between a Signal and a listener function.
* <br />- <strong>This is an internal constructor and shouldn't be called by regular users.</strong>
* <br />- inspired by Joa Ebert AS3 SignalBinding and Robert Penner's Slot classes.
* @author Miller Medeiros
* @constructor
* @internal
* @name SignalBinding
* @param {Signal} signal Reference to Signal object that listener is currently bound to.
* @param {Function} listener Handler function bound to the signal.
* @param {boolean} isOnce If binding should be executed just once.
* @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function).
* @param {Number} [priority] The priority level of the event listener. (default = 0).
*/
function SignalBinding(signal, listener, isOnce, listenerContext, priority) {
/**
* Handler function bound to the signal.
* @type Function
* @private
*/
this._listener = listener;
/**
* If binding should be executed just once.
* @type boolean
* @private
*/
this._isOnce = isOnce;
/**
* Context on which listener will be executed (object that should represent the `this` variable inside listener function).
* @memberOf SignalBinding.prototype
* @name context
* @type Object|undefined|null
*/
this.context = listenerContext;
/**
* Reference to Signal object that listener is currently bound to.
* @type Signal
* @private
*/
this._signal = signal;
/**
* Listener priority
* @type Number
* @private
*/
this._priority = priority || 0;
}
SignalBinding.prototype = {
/**
* If binding is active and should be executed.
* @type boolean
*/
active : true,
/**
* Default parameters passed to listener during `Signal.dispatch` and `SignalBinding.execute`. (curried parameters)
* @type Array|null
*/
params : null,
/**
* Call listener passing arbitrary parameters.
* <p>If binding was added using `Signal.addOnce()` it will be automatically removed from signal dispatch queue, this method is used internally for the signal dispatch.</p>
* @param {Array} [paramsArr] Array of parameters that should be passed to the listener
* @return {*} Value returned by the listener.
*/
execute : function (paramsArr) {
var handlerReturn, params;
if (this.active && !!this._listener) {
params = this.params? this.params.concat(paramsArr) : paramsArr;
handlerReturn = this._listener.apply(this.context, params);
if (this._isOnce) {
this.detach();
}
}
return handlerReturn;
},
/**
* Detach binding from signal.
* - alias to: mySignal.remove(myBinding.getListener());
* @return {Function|null} Handler function bound to the signal or `null` if binding was previously detached.
*/
detach : function () {
return this.isBound()? this._signal.remove(this._listener, this.context) : null;
},
/**
* @return {Boolean} `true` if binding is still bound to the signal and have a listener.
*/
isBound : function () {
return (!!this._signal && !!this._listener);
},
/**
* @return {boolean} If SignalBinding will only be executed once.
*/
isOnce : function () {
return this._isOnce;
},
/**
* @return {Function} Handler function bound to the signal.
*/
getListener : function () {
return this._listener;
},
/**
* @return {Signal} Signal that listener is currently bound to.
*/
getSignal : function () {
return this._signal;
},
/**
* Delete instance properties
* @private
*/
_destroy : function () {
delete this._signal;
delete this._listener;
delete this.context;
},
/**
* @return {string} String representation of the object.
*/
toString : function () {
return '[SignalBinding isOnce:' + this._isOnce +', isBound:'+ this.isBound() +', active:' + this.active + ']';
}
};
/*global SignalBinding:false*/
// Signal --------------------------------------------------------
//================================================================
function validateListener(listener, fnName) {
if (typeof listener !== 'function') {
throw new Error( 'listener is a required param of {fn}() and should be a Function.'.replace('{fn}', fnName) );
}
}
/**
* Custom event broadcaster
* <br />- inspired by Robert Penner's AS3 Signals.
* @name Signal
* @author Miller Medeiros
* @constructor
*/
function Signal() {
/**
* @type Array.<SignalBinding>
* @private
*/
this._bindings = [];
this._prevParams = null;
// enforce dispatch to aways work on same context (#47)
var self = this;
this.dispatch = function(){
Signal.prototype.dispatch.apply(self, arguments);
};
}
Signal.prototype = {
/**
* Signals Version Number
* @type String
* @const
*/
VERSION : '1.0.0',
/**
* If Signal should keep record of previously dispatched parameters and
* automatically execute listener during `add()`/`addOnce()` if Signal was
* already dispatched before.
* @type boolean
*/
memorize : false,
/**
* @type boolean
* @private
*/
_shouldPropagate : true,
/**
* If Signal is active and should broadcast events.
* <p><strong>IMPORTANT:</strong> Setting this property during a dispatch will only affect the next dispatch, if you want to stop the propagation of a signal use `halt()` instead.</p>
* @type boolean
*/
active : true,
/**
* @param {Function} listener
* @param {boolean} isOnce
* @param {Object} [listenerContext]
* @param {Number} [priority]
* @return {SignalBinding}
* @private
*/
_registerListener : function (listener, isOnce, listenerContext, priority) {
var prevIndex = this._indexOfListener(listener, listenerContext),
binding;
if (prevIndex !== -1) {
binding = this._bindings[prevIndex];
if (binding.isOnce() !== isOnce) {
throw new Error('You cannot add'+ (isOnce? '' : 'Once') +'() then add'+ (!isOnce? '' : 'Once') +'() the same listener without removing the relationship first.');
}
} else {
binding = new SignalBinding(this, listener, isOnce, listenerContext, priority);
this._addBinding(binding);
}
if(this.memorize && this._prevParams){
binding.execute(this._prevParams);
}
return binding;
},
/**
* @param {SignalBinding} binding
* @private
*/
_addBinding : function (binding) {
//simplified insertion sort
var n = this._bindings.length;
do { --n; } while (this._bindings[n] && binding._priority <= this._bindings[n]._priority);
this._bindings.splice(n + 1, 0, binding);
},
/**
* @param {Function} listener
* @return {number}
* @private
*/
_indexOfListener : function (listener, context) {
var n = this._bindings.length,
cur;
while (n--) {
cur = this._bindings[n];
if (cur._listener === listener && cur.context === context) {
return n;
}
}
return -1;
},
/**
* Check if listener was attached to Signal.
* @param {Function} listener
* @param {Object} [context]
* @return {boolean} if Signal has the specified listener.
*/
has : function (listener, context) {
return this._indexOfListener(listener, context) !== -1;
},
/**
* Add a listener to the signal.
* @param {Function} listener Signal handler function.
* @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function).
* @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0)
* @return {SignalBinding} An Object representing the binding between the Signal and listener.
*/
add : function (listener, listenerContext, priority) {
validateListener(listener, 'add');
return this._registerListener(listener, false, listenerContext, priority);
},
/**
* Add listener to the signal that should be removed after first execution (will be executed only once).
* @param {Function} listener Signal handler function.
* @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function).
* @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0)
* @return {SignalBinding} An Object representing the binding between the Signal and listener.
*/
addOnce : function (listener, listenerContext, priority) {
validateListener(listener, 'addOnce');
return this._registerListener(listener, true, listenerContext, priority);
},
/**
* Remove a single listener from the dispatch queue.
* @param {Function} listener Handler function that should be removed.
* @param {Object} [context] Execution context (since you can add the same handler multiple times if executing in a different context).
* @return {Function} Listener handler function.
*/
remove : function (listener, context) {
validateListener(listener, 'remove');
var i = this._indexOfListener(listener, context);
if (i !== -1) {
this._bindings[i]._destroy(); //no reason to a SignalBinding exist if it isn't attached to a signal
this._bindings.splice(i, 1);
}
return listener;
},
/**
* Remove all listeners from the Signal.
*/
removeAll : function () {
var n = this._bindings.length;
while (n--) {
this._bindings[n]._destroy();
}
this._bindings.length = 0;
},
/**
* @return {number} Number of listeners attached to the Signal.
*/
getNumListeners : function () {
return this._bindings.length;
},
/**
* Stop propagation of the event, blocking the dispatch to next listeners on the queue.
* <p><strong>IMPORTANT:</strong> should be called only during signal dispatch, calling it before/after dispatch won't affect signal broadcast.</p>
* @see Signal.prototype.disable
*/
halt : function () {
this._shouldPropagate = false;
},
/**
* Dispatch/Broadcast Signal to all listeners added to the queue.
* @param {...*} [params] Parameters that should be passed to each handler.
*/
dispatch : function (params) {
if (! this.active) {
return;
}
var paramsArr = Array.prototype.slice.call(arguments),
n = this._bindings.length,
bindings;
if (this.memorize) {
this._prevParams = paramsArr;
}
if (! n) {
//should come after memorize
return;
}
bindings = this._bindings.slice(); //clone array in case add/remove items during dispatch
this._shouldPropagate = true; //in case `halt` was called before dispatch or during the previous dispatch.
//execute all callbacks until end of the list or until a callback returns `false` or stops propagation
//reverse loop since listeners with higher priority will be added at the end of the list
do { n--; } while (bindings[n] && this._shouldPropagate && bindings[n].execute(paramsArr) !== false);
},
/**
* Forget memorized arguments.
* @see Signal.memorize
*/
forget : function(){
this._prevParams = null;
},
/**
* Remove all bindings from signal and destroy any reference to external objects (destroy Signal object).
* <p><strong>IMPORTANT:</strong> calling any method on the signal instance after calling dispose will throw errors.</p>
*/
dispose : function () {
this.removeAll();
delete this._bindings;
delete this._prevParams;
},
/**
* @return {string} String representation of the object.
*/
toString : function () {
return '[Signal active:'+ this.active +' numListeners:'+ this.getNumListeners() +']';
}
};
// Namespace -----------------------------------------------------
//================================================================
/**
* Signals namespace
* @namespace
* @name signals
*/
var signals = Signal;
/**
* Custom event broadcaster
* @see Signal
*/
// alias for backwards compatibility (see #gh-44)
signals.Signal = Signal;
//exports to multiple environments
if(typeof define === 'function' && define.amd){ //AMD
define(function () { return signals; });
} else if (typeof module !== 'undefined' && module.exports){ //node
module.exports = signals;
} else { //browser
//use string because of Google closure compiler ADVANCED_MODE
/*jslint sub:true */
global['signals'] = signals;
}
}(this));

@ -0,0 +1,338 @@
/*global define*/
/*
Superfast Blur - a fast Box Blur For Canvas
Version: 0.5
Author: Mario Klingemann
Contact: mario@quasimondo.com
Website: http://www.quasimondo.com/BoxBlurForCanvas
Twitter: @quasimondo
In case you find this class useful - especially in commercial projects -
I am not totally unhappy for a small donation to my PayPal account
mario@quasimondo.de
Or support me on flattr:
https://flattr.com/thing/140066/Superfast-Blur-a-pretty-fast-Box-Blur-Effect-for-CanvasJavascript
Copyright (c) 2011 Mario Klingemann
Note by Georg Fischer (snorpey@gmail.com / @snorpey):
While much of the original algorithm is still the same,
I modified some parts of the script to fit my needs:
- removed the iterations argument
- modified the functions to accept an imageData object
instead of element ids to remove dependency on the
document object.
- added AMD / requirejs wrapper
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
define(
function()
{
var mul_table = [
1,57,41,21,203,34,97,73,227,91,149,62,105,45,39,137,241,107,3,173,39,71,65,238,219,101,
187,87,81,151,141,133,249,117,221,209,197,187,177,169,5,153,73,139,133,127,243,233,223,
107,103,99,191,23,177,171,165,159,77,149,9,139,135,131,253,245,119,231,224,109,211,103,
25,195,189,23,45,175,171,83,81,79,155,151,147,9,141,137,67,131,129,251,123,30,235,115,
113,221,217,53,13,51,50,49,193,189,185,91,179,175,43,169,83,163,5,79,155,19,75,147,145,
143,35,69,17,67,33,65,255,251,247,243,239,59,29,229,113,111,219,27,213,105,207,51,201,
199,49,193,191,47,93,183,181,179,11,87,43,85,167,165,163,161,159,157,155,77,19,75,37,
73,145,143,141,35,138,137,135,67,33,131,129,255,63,250,247,61,121,239,237,117,29,229,
227,225,111,55,109,216,213,211,209,207,205,203,201,199,197,195,193,48,190,47,93,185,
183,181,179,178,176,175,173,171,85,21,167,165,41,163,161,5,79,157,78,154,153,19,75,
149,74,147,73,144,143,71,141,140,139,137,17,135,134,133,66,131,65,129,1
];
var shg_table = [
0,9,10,10,14,12,14,14,16,15,16,15,16,15,15,17,18,17,12,18,16,17,17,19,19,18,19,18,18,
19,19,19,20,19,20,20,20,20,20,20,15,20,19,20,20,20,21,21,21,20,20,20,21,18,21,21,21,
21,20,21,17,21,21,21,22,22,21,22,22,21,22,21,19,22,22,19,20,22,22,21,21,21,22,22,22,
18,22,22,21,22,22,23,22,20,23,22,22,23,23,21,19,21,21,21,23,23,23,22,23,23,21,23,22,
23,18,22,23,20,22,23,23,23,21,22,20,22,21,22,24,24,24,24,24,22,21,24,23,23,24,21,24,
23,24,22,24,24,22,24,24,22,23,24,24,24,20,23,22,23,24,24,24,24,24,24,24,23,21,23,22,
23,24,24,24,22,24,24,24,23,22,24,24,25,23,25,25,23,24,25,25,24,22,25,25,25,24,23,24,
25,25,25,25,25,25,25,25,25,25,25,25,23,25,23,24,25,25,25,25,25,25,25,25,25,24,22,25,
25,23,25,25,20,24,25,24,25,25,22,24,25,24,25,24,25,25,24,25,25,25,25,22,25,25,25,24,
25,24,25,18
];
function boxBlurCanvas( image_data, radius, blur_alpha_channel )
{
var result = image_data;
if ( ! ( isNaN( radius ) || radius < 1 ) )
{
if ( blur_alpha_channel )
{
result = boxBlurCanvasRGBA( image_data, radius );
}
else
{
result = boxBlurCanvasRGB( image_data, radius );
}
}
return result;
}
function boxBlurCanvasRGBA( image_data, radius )
{
radius |= 0;
var pixels = image_data.data;
var width = image_data.width;
var height = image_data.height;
var rsum, gsum, bsum, asum, x, y, i, p, p1, p2, yp, yi, yw, idx, pa;
var wm = width - 1;
var hm = height - 1;
var wh = width * height;
var rad1 = radius + 1;
var mul_sum = mul_table[radius];
var shg_sum = shg_table[radius];
var r = [ ];
var g = [ ];
var b = [ ];
var a = [ ];
var vmin = [ ];
var vmax = [ ];
yw = yi = 0;
for ( y = 0; y < height; y++ )
{
rsum = pixels[yw] * rad1;
gsum = pixels[yw + 1] * rad1;
bsum = pixels[yw + 2] * rad1;
asum = pixels[yw + 3] * rad1;
for ( i = 1; i <= radius; i++ )
{
p = yw + ( ( ( i > wm ? wm : i ) ) << 2 );
rsum += pixels[p++];
gsum += pixels[p++];
bsum += pixels[p++];
asum += pixels[p];
}
for ( x = 0; x < width; x++ )
{
r[yi] = rsum;
g[yi] = gsum;
b[yi] = bsum;
a[yi] = asum;
if ( y === 0 )
{
vmin[x] = ( ( p = x + rad1) < wm ? p : wm ) << 2;
vmax[x] = ( ( p = x - radius) > 0 ? p << 2 : 0 );
}
p1 = yw + vmin[x];
p2 = yw + vmax[x];
rsum += pixels[p1++] - pixels[p2++];
gsum += pixels[p1++] - pixels[p2++];
bsum += pixels[p1++] - pixels[p2++];
asum += pixels[p1] - pixels[p2];
yi++;
}
yw += ( width << 2 );
}
for ( x = 0; x < width; x++ )
{
yp = x;
rsum = r[yp] * rad1;
gsum = g[yp] * rad1;
bsum = b[yp] * rad1;
asum = a[yp] * rad1;
for ( i = 1; i <= radius; i++ )
{
yp += ( i > hm ? 0 : width );
rsum += r[yp];
gsum += g[yp];
bsum += b[yp];
asum += a[yp];
}
yi = x << 2;
for ( y = 0; y < height; y++ )
{
pixels[yi + 3] = pa = ( asum * mul_sum ) >>> shg_sum;
if ( pa > 0 )
{
pa = 255 / pa;
pixels[yi] = ( ( rsum * mul_sum ) >>> shg_sum ) * pa;
pixels[yi+1] = ( ( gsum * mul_sum ) >>> shg_sum ) * pa;
pixels[yi+2] = ( ( bsum * mul_sum ) >>> shg_sum ) * pa;
}
else
{
pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0;
}
if ( x === 0 )
{
vmin[y] = ( ( p = y + rad1) < hm ? p : hm ) * width;
vmax[y] = ( ( p = y - radius) > 0 ? p * width : 0 );
}
p1 = x + vmin[y];
p2 = x + vmax[y];
rsum += r[p1] - r[p2];
gsum += g[p1] - g[p2];
bsum += b[p1] - b[p2];
asum += a[p1] - a[p2];
yi += width << 2;
}
}
return image_data;
}
function boxBlurCanvasRGB( image_data, radius )
{
radius |= 0;
var pixels = image_data.data;
var width = image_data.width;
var height = image_data.height;
var rsum, gsum, bsum, asum, x, y, i, p, p1, p2, yp, yi, yw, idx;
var wm = width - 1;
var hm = height - 1;
var wh = width * height;
var rad1 = radius + 1;
var r = [ ];
var g = [ ];
var b = [ ];
var mul_sum = mul_table[radius];
var shg_sum = shg_table[radius];
var vmin = [ ];
var vmax = [ ];
yw = yi = 0;
for ( y = 0; y < height; y++ )
{
rsum = pixels[yw] * rad1;
gsum = pixels[yw + 1] * rad1;
bsum = pixels[yw + 2] * rad1;
for ( i = 1; i <= radius; i++ )
{
p = yw + ( ( ( i > wm ? wm : i ) ) << 2 );
rsum += pixels[p++];
gsum += pixels[p++];
bsum += pixels[p++];
}
for ( x = 0; x < width; x++ )
{
r[yi] = rsum;
g[yi] = gsum;
b[yi] = bsum;
if ( y === 0 )
{
vmin[x] = ( ( p = x + rad1) < wm ? p : wm ) << 2;
vmax[x] = ( ( p = x - radius) > 0 ? p << 2 : 0 );
}
p1 = yw + vmin[x];
p2 = yw + vmax[x];
rsum += pixels[p1++] - pixels[p2++];
gsum += pixels[p1++] - pixels[p2++];
bsum += pixels[p1++] - pixels[p2++];
yi++;
}
yw += ( width << 2 );
}
for ( x = 0; x < width; x++ )
{
yp = x;
rsum = r[yp] * rad1;
gsum = g[yp] * rad1;
bsum = b[yp] * rad1;
for ( i = 1; i <= radius; i++ )
{
yp += ( i > hm ? 0 : width );
rsum += r[yp];
gsum += g[yp];
bsum += b[yp];
}
yi = x << 2;
for ( y = 0; y < height; y++ )
{
pixels[yi] = (rsum * mul_sum) >>> shg_sum;
pixels[yi+1] = (gsum * mul_sum) >>> shg_sum;
pixels[yi+2] = (bsum * mul_sum) >>> shg_sum;
if ( x === 0 )
{
vmin[y] = ( ( p = y + rad1) < hm ? p : hm ) * width;
vmax[y] = ( ( p = y - radius) > 0 ? p * width : 0 );
}
p1 = x + vmin[y];
p2 = x + vmax[y];
rsum += r[p1] - r[p2];
gsum += g[p1] - g[p2];
bsum += b[p1] - b[p2];
yi += width << 2;
}
}
return image_data;
}
return boxBlurCanvas;
}
);

53
scripts/src/controls.js vendored Normal file

@ -0,0 +1,53 @@
/*global define*/
define(
function()
{
var values = { };
var is_initialized = false;
var signals;
function init( shared )
{
signals = shared.signals;
if ( shared.feature['query-selector-all'] )
{
var wrapper = document.getElementById( 'controls' );
var controls = document.querySelectorAll( '.control-input' );
wrapper.className += ' is-active';
for ( var i = 0; i < controls.length; i++ )
{
var control = controls[i];
control.addEventListener( 'change', controlUpdated, false );
updateValue( control.id, control.value );
}
is_initialized = true;
signals['control-updated'].dispatch( values );
}
}
function controlUpdated( event )
{
var target = event.target;
updateValue( target.id, target.value );
}
function updateValue( key, value )
{
values[key] = value;
if ( is_initialized )
{
signals['control-updated'].dispatch( values );
}
}
return { init: init };
}
);

43
scripts/src/dragdrop.js vendored Normal file

@ -0,0 +1,43 @@
/*global define*/
define(
function()
{
var signals;
var reader;
var feature;
function init( shared )
{
feature = shared.feature;
signals = shared.signals;
if ( feature['drag-drop' ] && feature['file-api' ] )
{
document.addEventListener( 'drop', dropped, false );
document.addEventListener( 'dragover', preventDefault, false );
document.addEventListener( 'dragleave', preventDefault, false );
reader = new FileReader();
reader.addEventListener( 'load', fileLoaded, false );
}
}
function preventDefault( event )
{
event.preventDefault();
}
function dropped( event )
{
event.preventDefault();
reader.readAsDataURL( event.dataTransfer.files[0] );
}
function fileLoaded( event )
{
signals['set-new-src'].dispatch( event.target.result );
}
return { init: init };
}
);

31
scripts/src/export-png.js Normal file

@ -0,0 +1,31 @@
/*global define*/
define(
function()
{
var signals;
var png_button;
function init( shared )
{
signals = shared.signals;
png_button = document.getElementById( 'png-button' );
signals['export-png'].add( generatePNG );
signals['control-updated'].add( hideLink );
png_button.addEventListener( 'click', hideLink, false );
}
function generatePNG( data_url )
{
png_button.href = data_url;
png_button.classList.add( 'is-active' );
}
function hideLink()
{
png_button.classList.remove( 'is-active' );
}
return { init: init };
}
);

45
scripts/src/image.js Normal file

@ -0,0 +1,45 @@
/*global define*/
define(
function()
{
var signals;
var image;
var initialized = false;
function init( shared )
{
signals = shared.signals;
image = new Image();
signals['set-new-src'].add( setSrc );
image.addEventListener( 'load', imageLoaded, false );
// the image "Abraham Lincoln November 1863" is public domain:
// https://en.wikipedia.org/wiki/File:Abraham_Lincoln_November_1863.jpg
setSrc( 'lincoln.jpg' );
}
function imageLoaded()
{
signals['image-loaded'].dispatch( image );
initialized = true;
}
function setSrc( src )
{
image.src = src;
if (
initialized &&
image.naturalWidth !== undefined &&
image.naturalWidth !== 0
)
{
signals['image-loaded'].dispatch( image );
}
}
return { init: init };
}
);

75
scripts/src/main.js Normal file

@ -0,0 +1,75 @@
/*global require, requirejs, define, Modernizr, _basepath_ */
// http://requirejs.org/docs/api.html#config
var path = typeof _basepath_ === 'string' ? _basepath_ + '/' : '';
requirejs.config(
{
baseUrl: path + 'scripts/',
waitSeconds: 5,
urlArgs: 'bust=' + ( new Date() ).getTime(),
shim: {
'lib/delaunay': { exports: 'triangulate' }
}
}
);
require(
[
'src/process',
'src/image',
'src/dragdrop',
'src/controls',
'src/export-png',
'src/save-button',
'aux/feature-test',
'lib/signals-1.0.0',
'lib/html5slider'
],
function(
process,
image,
dragdrop,
controls,
png,
save_button,
testFeatures,
Signal
)
{
testFeatures( init, showError );
function init( supported_features )
{
var shared = {
feature: supported_features,
signals: {
'image-loaded' : new Signal(),
'set-new-src' : new Signal(),
'control-updated' : new Signal(),
'export-png' : new Signal(),
'saved' : new Signal()
}
};
process.init( shared );
dragdrop.init( shared );
controls.init( shared );
png.init( shared );
save_button.init( shared );
image.init( shared );
}
function showError( required_features )
{
var message = document.createElement( 'div' );
var message_text = 'sorry. it looks like your browser is missing some of the features ';
message_text += '(' + required_features.join( ', ' ) + ') that are required to run this ';
message_text += 'experiment.';
message.innerText = message_text;
message.className = 'missing-feature';
document.getElementsByTagName( 'body' )[0].appendChild( message );
}
}
);

109
scripts/src/process.js Normal file

@ -0,0 +1,109 @@
/*global define*/
define(
[ 'aux/glitch' ],
function( glitch )
{
var tmp_canvas = document.createElement( 'canvas' );
var tmp_ctx = tmp_canvas.getContext( '2d' );
var canvas = document.getElementById( 'canvas' );
var ctx = canvas.getContext( '2d' );
var is_processing = false;
var values;
var image;
var signals;
var image_data;
function init( shared )
{
signals = shared.signals;
signals['image-loaded'].add( generate );
signals['control-updated'].add( controlsUpdated );
signals['saved'].add( exportData );
}
function controlsUpdated( new_values )
{
values = getAdjustedValues( new_values );
update();
}
function generate( img )
{
if ( ! is_processing )
{
image = img;
processImage( image );
}
}
function update()
{
if ( ! is_processing && image )
{
processImage( image );
}
}
function processImage( img )
{
is_processing = true;
clearCanvas( tmp_canvas, tmp_ctx );
clearCanvas( canvas, ctx );
resizeCanvas( tmp_canvas, img );
resizeCanvas( canvas, img );
tmp_ctx.drawImage( img, 0, 0 );
image_data = tmp_ctx.getImageData( 0, 0, tmp_canvas.width, tmp_canvas.height );
glitch( image_data, values, draw );
}
function draw( image_data )
{
resizeCanvas( canvas, image_data );
ctx.putImageData( image_data, 0, 0 );
is_processing = false;
image_data = null;
}
function resizeCanvas( canvas, img )
{
canvas.width = img.width;
canvas.height = img.height;
}
function clearCanvas( canvas, ctx )
{
ctx.clearRect( ctx, 0, 0, canvas.width, canvas.height );
}
function exportData()
{
signals['export-png'].dispatch( canvas.toDataURL( 'image/png' ) );
}
function getAdjustedValues( new_values )
{
var result = { };
for ( var key in new_values )
{
result[key] = parseInt( new_values[key], 10 );
}
key = null;
return result;
}
return { init: init };
}
);

@ -0,0 +1,25 @@
/*global define*/
define(
function()
{
var signals;
var save_button;
function init( shared )
{
signals = shared.signals;
save_button = document.getElementById( 'save-button' );
save_button.addEventListener( 'click', buttonClicked, false );
}
function buttonClicked( event )
{
event.preventDefault();
signals['saved'].dispatch();
}
return { init: init };
}
);

130
styles/global.css Normal file

@ -0,0 +1,130 @@
*
{
margin: 0;
padding: 0;
}
body
{
padding: 50px;
font-family: sans-serif;
color: #666;
line-height: 18px;
font-size: 12px;
}
a
{
color: #06f;
text-decoration: none;
}
a:hover
{
text-decoration: underline;
}
.button
{
background-color: #eaeaea;
padding: 5px 9px;
display: inline-block;
color: #06f;
font-weight: normal;
border-radius: 2px;
cursor: pointer;
border: none;
font-family: sans-serif;
font-size: 12px;
text-decoration: none;
}
.button:hover
{
background-color: #06f;
color: #fff;
}
.headline
{
font-size: 12px;
color: #333;
margin-bottom: 10px;
}
.content,
.missing-feature
{
max-width: 650px;
}
#controls
{
float: left;
margin-top: 30px;
display: none;
}
#controls.is-active
{
display: block;
}
.control-wrapper
{
float: left;
width: 100px;
margin-right: 20px;
}
.control-label
{
display: block;
color: #666;
}
.control-input
{
display: block;
width: 100px;
}
#canvas
{
clear: both;
float: left;
margin-top: 30px;
display: block;
}
.export-wrapper
{
clear: both;
float: left;
margin-top: 30px;
}
.download-link
{
display: none;
}
.download-link.is-active
{
display: inline-block;
margin-left: 15px;
}
.download-link span
{
color: #999;
display: inline-block;
text-decoration: none;
margin-left: 4px;
}
.missing-feature
{
clear: both;
}