├── README.md ├── art_neon.css ├── art_neon.js ├── clock.js ├── common.js ├── index.html ├── input.js ├── jquery.js ├── loader.js ├── noise.js └── teaser.jpg /README.md: -------------------------------------------------------------------------------- 1 | # Neonflames 2 | Neon flames is a crazy online HTML5 drawing tool. 3 | 4 | Try the most recent version online at: https://29a.ch/sandbox/2011/neonflames/ 5 | 6 | This is copyright (c) by Jonas Wagner. 7 | -------------------------------------------------------------------------------- /art_neon.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | height: 100%; 4 | overflow: hidden; 5 | font-family: sans-serif; 6 | font-size: 12px; 7 | } 8 | 9 | #colors { 10 | position: absolute; 11 | top: 54px; 12 | left: 8px; 13 | padding: 0; 14 | margin: 0; 15 | width: 200px; 16 | } 17 | 18 | #colors li { 19 | display: inline-block; 20 | opacity: 0.3; 21 | width: 64px; 22 | height: 64px; 23 | margin: 8px; 24 | border-radius: 64px; 25 | border-radius: 50%; 26 | border: 2px solid white; 27 | cursor: pointer; 28 | -webkit-transition: opacity ease-in-out 0.5s; 29 | -moz-transition: opacity ease-in-out 0.5s; 30 | transition: opacity ease-in-out 0.5s; 31 | } 32 | ul#colors li:hover, #colors li.active { 33 | opacity: 1.0; 34 | width: 80px; 35 | height: 80px; 36 | margin: 0; 37 | -webkit-transition: all ease-in-out 0.5s; 38 | -moz-transition: all ease-in-out 0.5s; 39 | transition: all ease-in-out 0.5s; 40 | } 41 | #colors li.active { 42 | opacity: 0.3; 43 | } 44 | 45 | canvas { 46 | clear: left; 47 | } 48 | 49 | #clear { 50 | right: 300px; 51 | } 52 | 53 | #clear:hover { 54 | background: rgb(170,14,0); /* Old browsers */ 55 | background: -moz-linear-gradient(top, rgba(170,14,0,1) 0%, rgba(190,13,0,1) 50%, rgba(148,7,0,1) 51%, rgba(150,8,0,1) 71%, rgba(154,3,0,1) 100%); /* FF3.6+ */ 56 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(170,14,0,1)), color-stop(50%,rgba(190,13,0,1)), color-stop(51%,rgba(148,7,0,1)), color-stop(71%,rgba(150,8,0,1)), color-stop(100%,rgba(154,3,0,1))); /* Chrome,Safari4+ */ 57 | background: -webkit-linear-gradient(top, rgba(170,14,0,1) 0%,rgba(190,13,0,1) 50%,rgba(148,7,0,1) 51%,rgba(150,8,0,1) 71%,rgba(154,3,0,1) 100%); /* Chrome10+,Safari5.1+ */ 58 | background: -o-linear-gradient(top, rgba(170,14,0,1) 0%,rgba(190,13,0,1) 50%,rgba(148,7,0,1) 51%,rgba(150,8,0,1) 71%,rgba(154,3,0,1) 100%); /* Opera11.10+ */ 59 | background: -ms-linear-gradient(top, rgba(170,14,0,1) 0%,rgba(190,13,0,1) 50%,rgba(148,7,0,1) 51%,rgba(150,8,0,1) 71%,rgba(154,3,0,1) 100%); /* IE10+ */ 60 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#aa0e00', endColorstr='#9a0300',GradientType=0 ); /* IE6-9 */ 61 | background: linear-gradient(top, rgba(170,14,0,1) 0%,rgba(190,13,0,1) 50%,rgba(148,7,0,1) 51%,rgba(150,8,0,1) 71%,rgba(154,3,0,1) 100%); /* W3C */ 62 | border: 2px solid #ddd; 63 | } 64 | 65 | #share { 66 | right: 135px; 67 | 68 | } 69 | 70 | #download { 71 | right: 8px; 72 | } 73 | .button { 74 | 75 | -webkit-transition: all ease-in-out 0.5s; 76 | -moz-transition: all ease-in-out 0.5s; 77 | transition: all ease-in-out 0.5s; 78 | box-shadow: 0 0 10px black inset; 79 | position: absolute; 80 | margin: 8px; 81 | top: 4px; 82 | right: 8px; 83 | color: #ddd; 84 | font-weight: bold; 85 | display: block; 86 | text-align: center; 87 | width: 100px; 88 | border-radius: 10px; 89 | padding: 8px; 90 | border: 2px solid #666; 91 | text-decoration: none; 92 | 93 | background: #4c4c4c; /* Old browsers */ 94 | background: -moz-linear-gradient(top, #4c4c4c 0%, #595959 12%, #666666 25%, #474747 39%, #2c2c2c 50%, #000000 51%, #111111 60%, #2b2b2b 76%, #1c1c1c 91%, #131313 100%); /* FF3.6+ */ 95 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(12%,#595959), color-stop(25%,#666666), color-stop(39%,#474747), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(60%,#111111), color-stop(76%,#2b2b2b), color-stop(91%,#1c1c1c), color-stop(100%,#131313)); /* Chrome,Safari4+ */ 96 | background: -webkit-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* Chrome10+,Safari5.1+ */ 97 | background: -o-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* Opera11.10+ */ 98 | background: -ms-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* IE10+ */ 99 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4c4c4c', endColorstr='#131313',GradientType=0 ); /* IE6-9 */ 100 | background: linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* W3C */ 101 | } 102 | .button:hover { 103 | color: #fff; 104 | border-color: #fff; 105 | } 106 | 107 | #social { 108 | position: absolute; 109 | left: 380px; 110 | top: 15px; 111 | height: 10px; 112 | width: 300px; 113 | } 114 | #social > * { 115 | float: left; 116 | opacity: 0.5; 117 | } 118 | #social > *:hover { 119 | opacity: 1.0; 120 | } 121 | 122 | h1, p { 123 | color: #fff; 124 | opacity: 0.5; 125 | position: absolute; 126 | top: -10px; 127 | left: 15px; 128 | font-family: Michroma, sans-serif; 129 | } 130 | 131 | p { 132 | top: 8px; 133 | left: 150px; 134 | } 135 | 136 | h1:hover, p:hover { 137 | opacity: 1.0; 138 | } 139 | h1 a, p a { 140 | color: #fff; 141 | text-decoration: none; 142 | } 143 | noscript { 144 | color: white; 145 | } 146 | -------------------------------------------------------------------------------- /art_neon.js: -------------------------------------------------------------------------------- 1 | var particles = [], 2 | color = 'rgb(12, 2, 2)', 3 | composite = 'lighter', 4 | max_age = 100, 5 | initial_radius = 5, 6 | lineWidth = 1.0, 7 | noiseCanvas = makeOctaveNoise(canvas.width, canvas.height, 8), 8 | noise = noiseCanvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data; 9 | 10 | function clear(){ 11 | _gaq.push(['_trackEvent', 'neonfuzz', 'clear']); 12 | ctx.globalCompositeOperation = 'source-over'; 13 | ctx.fillStyle = 'black'; 14 | ctx.fillRect(0, 0, canvas.width, canvas.height); 15 | } 16 | 17 | function download(){ 18 | _gaq.push(['_trackEvent', 'neonfuzz', 'download']); 19 | window.open(canvas.toDataURL('image/jpeg', 0.9)) 20 | } 21 | 22 | function share(){ 23 | _gaq.push(['_trackEvent', 'neonfuzz', 'share']); 24 | 25 | 26 | try { 27 | var img = canvas.toDataURL('image/jpeg', 0.9).split(',')[1]; 28 | } catch(e) { 29 | var img = canvas.toDataURL().split(',')[1]; 30 | } 31 | var w = window.open(); 32 | w.document.write('Uploading...'); 33 | $.ajax({ 34 | url: 'http://api.imgur.com/2/upload.json', 35 | type: 'POST', 36 | data: { 37 | type: 'base64', 38 | key: '48c16073663cb7d3befd1c2c064dfa0d', 39 | name: 'neon.jpg', 40 | title: 'test title', 41 | caption: 'test caption', 42 | image: img 43 | }, 44 | dataType: 'json' 45 | }).success(function(data) { 46 | w.location.href = data['upload']['links']['imgur_page']; 47 | }).error(function() { 48 | alert('Could not reach api.imgur.com. Sorry :('); 49 | w.close(); 50 | }); 51 | } 52 | 53 | function getNoise(x, y, channel) { 54 | //return fuzzy(0.4); 55 | return noise[(~~x+~~y*canvas.width)*4+channel]/127-1.0; 56 | } 57 | 58 | // base +/- range 59 | function fuzzy(range, base){ 60 | return (base||0) + (Math.random()-0.5)*range*2 61 | } 62 | 63 | timer.ontick = function(td){ 64 | if(input.mouse.down){ 65 | for(var i = 0; i < 10; i++){ 66 | particles.push({ 67 | vx: fuzzy(10.0), 68 | vy: fuzzy(10.0), 69 | x: input.mouse.x, 70 | y: input.mouse.y, 71 | age: 0 72 | }); 73 | } 74 | } 75 | 76 | ctx.lineWidth = lineWidth; 77 | ctx.strokeStyle = color; 78 | ctx.fillStyle = color; 79 | ctx.globalAlpha = 1.0; 80 | ctx.globalCompositeOperation = composite; 81 | var alive = []; 82 | 83 | for(var i = 0; i < particles.length; i++){ 84 | var p = particles[i]; 85 | p.vx = p.vx*0.8 + getNoise(p.x, p.y, 0)*4;//+fuzzy(1.0); 86 | p.vy = p.vy*0.8 + getNoise(p.x, p.y, 1)*4;//+fuzzy(1.0); 87 | p.x += p.vx; 88 | p.y += p.vy; 89 | p.age ++; 90 | 91 | ctx.beginPath(); 92 | ctx.arc(p.x, p.y, 0.5, 0, Math.PI*2, true); 93 | ctx.closePath(); 94 | ctx.fill(); 95 | //ctx.stroke(); 96 | 97 | if(p.age < max_age){ 98 | alive.push(p); 99 | } 100 | } 101 | 102 | particles = alive; 103 | } 104 | 105 | ctx.fillStyle = 'black'; 106 | ctx.fillRect(0, 0, canvas.width, canvas.height); 107 | 108 | $('#colors li').click(function() { 109 | $('#colors li').removeClass('active'); 110 | $(this).addClass('active'); 111 | }); 112 | -------------------------------------------------------------------------------- /clock.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.clock = {}; 3 | window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; 4 | clock.Clock = function () { 5 | this.running = false; 6 | this.interval = null; 7 | this.t0 = new Date(); 8 | } 9 | clock.Clock.prototype = { 10 | tick: function () { 11 | var t1 = new Date(), 12 | td = (t1-this.t0)/1000; 13 | this.t0 = t1; 14 | this.ontick(td); 15 | }, 16 | start: function (element) { 17 | this.running = true; 18 | var self = this, f; 19 | if(window.requestAnimationFrame){ 20 | window.requestAnimationFrame(f = function () { 21 | self.tick(); 22 | if(self.running){ 23 | window.requestAnimationFrame(f, element); 24 | } 25 | }, element); 26 | } 27 | else { 28 | this.interval = window.setInterval(function() { 29 | self.tick(); 30 | }, 1); 31 | } 32 | this.t0 = new Date(); 33 | }, 34 | stop: function() { 35 | if(this.interval){ 36 | window.clearInterval(this.interval); 37 | this.interval = null; 38 | } 39 | this.running = false; 40 | }, 41 | ontick: function() {} 42 | }; 43 | 44 | })(); 45 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var canvas = document.getElementById('c'), 3 | ctx = canvas.getContext('2d'), 4 | input = new inputhandler.InputHandler(canvas), 5 | timer = new clock.Clock(); 6 | 7 | canvas.height = window.innerHeight; 8 | canvas.width = window.innerWidth; 9 | document.body.style.margin = 0; 10 | document.body.style.overflow = 'hidden'; 11 | 12 | timer.start(); 13 | 14 | window.timer = timer; 15 | window.input = input; 16 | window.ctx = ctx; 17 | window.canvas = canvas; 18 | 19 | })(); 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29a.ch - Neonflames 5 | 6 | 7 | 8 | 9 | 10 |

29a.ch

11 |

More Experiments & Toys

12 |
13 | Tweet 17 | 20 |
22 |
23 | 24 | 42 | Clear 43 | Share Image 44 | Download 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /input.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | window.inputhandler = {}; 3 | 4 | function clamp(a, b, c) { 5 | return a < b ? b : (a > c ? c : a); 6 | } 7 | 8 | // mapping keycodes to names 9 | var keyname = { 10 | 32: 'SPACE', 11 | 13: 'ENTER', 12 | 9: 'TAB', 13 | 8: 'BACKSPACE', 14 | 16: 'SHIFT', 15 | 17: 'CTRL', 16 | 18: 'ALT', 17 | 20: 'CAPS_LOCK', 18 | 144: 'NUM_LOCK', 19 | 145: 'SCROLL_LOCK', 20 | 37: 'LEFT', 21 | 38: 'UP', 22 | 39: 'RIGHT', 23 | 40: 'DOWN', 24 | 33: 'PAGE_UP', 25 | 34: 'PAGE_DOWN', 26 | 36: 'HOME', 27 | 35: 'END', 28 | 45: 'INSERT', 29 | 46: 'DELETE', 30 | 27: 'ESCAPE', 31 | 19: 'PAUSE' 32 | }; 33 | 34 | 35 | /* User input handler using jQuery */ 36 | inputhandler.InputHandler = function InputHandler(element) { 37 | this.bind(element); 38 | this.reset(); 39 | } 40 | inputhandler.InputHandler.prototype = { 41 | offset: {x: 0, y: 0}, 42 | onClick: null, 43 | onKeyUp: null, 44 | onKeyDown: null, 45 | hasFocus: true, 46 | bind: function(element) { 47 | var self = this, 48 | offset = $(element).offset(); 49 | this.offset = {x:offset.left, y:offset.top}; 50 | $(document).keydown(function(e){ 51 | return !self.keyDown(e.keyCode); 52 | }); 53 | $(document).keyup(function(e){ 54 | return !self.keyUp(e.keyCode); 55 | }); 56 | $(window).click(function(e){ 57 | if(e.originalEvent.target != element){ 58 | self.blur(); 59 | } 60 | else { 61 | self.focus(); 62 | } 63 | }); 64 | $(window).blur(function(){ 65 | self.blur(); 66 | }); 67 | /* 68 | too much overhead, using the dom directly for mouse tracking 69 | $(window).mousemove(function(e){ 70 | self.mouseMove(e.pageX, e.pageY); 71 | }); 72 | */ 73 | $(element).bind('touchstart', function () { 74 | return self.mouseDown(); 75 | }); 76 | element.ontouchmove = function(e){ 77 | self.mouseMove(e.touches[0].pageX, e.touches[0].pageY); 78 | return false; 79 | } 80 | $(element).bind('touchend', function () { 81 | return self.mouseUp(); 82 | }); 83 | document.onmousemove = function(e) { 84 | self.mouseMove(e.pageX, e.pageY); 85 | } 86 | $(element).mousedown(function(e){ 87 | return self.mouseDown(); 88 | }); 89 | $(element).mouseup(function(e){ 90 | self.mouseUp(); 91 | }); 92 | // prevent text selection in browsers that support it 93 | document.onselectstart = function(){return false;} 94 | }, 95 | blur: function() { 96 | this.hasFocus = false; 97 | this.reset(); 98 | }, 99 | focus: function() { 100 | if(!this.hasFocus) { 101 | this.hasFocus = true; 102 | this.reset(); 103 | } 104 | }, 105 | reset: function() { 106 | this.keys = {}; 107 | for(var i = 65; i < 128; i++) { 108 | this.keys[String.fromCharCode(i)] = false; 109 | } 110 | for(i in keyname){ 111 | this.keys[keyname[i]] = false; 112 | } 113 | this.mouse = {x: 0, y: 0, down: false}; 114 | }, 115 | keyDown: function(key) { 116 | var name = this._getKeyName(key), 117 | wasDown = this.keys[name]; 118 | this.keys[name] = true; 119 | if(this.onKeyDown && !wasDown) { 120 | this.onKeyDown(name); 121 | } 122 | return this.hasFocus; 123 | }, 124 | keyUp: function(key) { 125 | var name = this._getKeyName(key); 126 | this.keys[name] = false; 127 | if(this.onKeyUp) { 128 | this.onKeyUp(name); 129 | } 130 | }, 131 | mouseDown: function() { 132 | this.mouse.down = true; 133 | return this.hasFocus; 134 | }, 135 | mouseUp: function() { 136 | this.mouse.down = false; 137 | if(this.hasFocus && this.onClick) { 138 | this.onClick(this.mouse.x, this.mouse.y); 139 | } 140 | }, 141 | mouseMove: function(x, y){ 142 | this.mouse.x = clamp(x-this.offset.x, 0, this.width); 143 | this.mouse.y = clamp(y-this.offset.y, 0, this.height); 144 | }, 145 | _getKeyName: function(key) { 146 | if(key in keyname) { 147 | return keyname[key]; 148 | } 149 | return String.fromCharCode(key); 150 | } 151 | 152 | }; 153 | })(); 154 | -------------------------------------------------------------------------------- /loader.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | window.loader = {}; 3 | 4 | loader.Loader = function Loader(root){ 5 | this.root = root || ''; 6 | this.pending = 0; 7 | this.total = 0; 8 | this.failed = 0; 9 | this.resources = {}; 10 | } 11 | loader.Loader.prototype = { 12 | load: function(resources) { 13 | for(var i = 0; i < resources.length; i++) { 14 | var resource = resources[i]; 15 | // allows loading in multiple stages 16 | if(resource in this.resources){ 17 | continue; 18 | } 19 | this.pending ++; 20 | this.total ++; 21 | if(/\.(jpe?g|gif|png)$/.test(resource)){ 22 | this._loadImage(resource); 23 | } 24 | else if(/\.(og(g|a)|mp3)$/.test(resource)){ 25 | this._loadAudio(resource); 26 | } 27 | else if(/\.json$/.test(resource)){ 28 | this._loadJSON(resource); 29 | } 30 | else { 31 | this._loadData(resource); 32 | } 33 | } 34 | 35 | if(this.pending === 0 && this.onready){ 36 | var self = this; 37 | // always call AFTER the mainloop 38 | // multiple load calls can result in 39 | // multiple onready() calls! 40 | window.setTimeout(function () { 41 | if(self.onready){ 42 | self.onready(); 43 | } 44 | }, 1); 45 | } 46 | }, 47 | onready: null, 48 | _loadImage: function(src) { 49 | var self = this; 50 | var img = document.createElement('img'); 51 | img.onload = function() { 52 | self._success(src, img); 53 | } 54 | img.onerror = function (e) { 55 | self._error(src, e); 56 | } 57 | img.src = this.root + src; 58 | }, 59 | _loadJSON: function(src){ 60 | var self = this; 61 | $.getJSON(this.root + src) 62 | .success(function(data) { self._success(src, data); }) 63 | .error(function(error) { self._error(src, error); }); 64 | }, 65 | _loadData: function(src){ 66 | var self = this; 67 | $.get(this.root + src) 68 | .success(function(data) { self._success(src, data); }) 69 | .error(function(error) { self._error(src, error); }); 70 | }, 71 | _success: function(src, data) { 72 | this.resources[src] = data; 73 | this.pending --; 74 | if(this.pending === 0 && this.onready){ 75 | this.onready(); 76 | } 77 | }, 78 | _error: function(src, error) { 79 | this.pending --; 80 | this.failed ++; 81 | this.resources[src] = null; 82 | error.src = src; 83 | throw error; 84 | } 85 | }; 86 | 87 | })(); 88 | -------------------------------------------------------------------------------- /noise.js: -------------------------------------------------------------------------------- 1 | function makeNoise(width, height){ 2 | var canvas = document.createElement('canvas'), 3 | ctx = canvas.getContext('2d'); 4 | 5 | canvas.width = width; 6 | canvas.height = height; 7 | 8 | var imgData = ctx.getImageData(0, 0, width, height), 9 | data = imgData.data, 10 | pixels = data.length; 11 | 12 | for(var i = 0; i < pixels; i+=4){ 13 | data[i] = Math.random()*255; 14 | data[i+1] = Math.random()*255; 15 | data[i+2] = Math.random()*255; 16 | // data[i+1] = data[i]; 17 | // data[i+2] = data[i]; 18 | data[i+3] = 255; 19 | } 20 | ctx.putImageData(imgData, 0, 0); 21 | 22 | return canvas; 23 | } 24 | 25 | function makeOctaveNoise(width, height, octaves){ 26 | var canvas = document.createElement('canvas'), 27 | ctx = canvas.getContext('2d'); 28 | 29 | canvas.width = width; 30 | canvas.height = height; 31 | 32 | ctx.fillStyle = 'black'; 33 | ctx.fillRect(0,0,width,height); 34 | 35 | ctx.globalAlpha = 1/octaves; 36 | ctx.globalCompositeOperation = 'lighter'; 37 | 38 | for(var i = 0; i < octaves; i++){ 39 | var octave = makeNoise(width>>i, height>>i); 40 | ctx.drawImage(octave, 0, 0, width, height); 41 | } 42 | 43 | return canvas; 44 | } 45 | -------------------------------------------------------------------------------- /teaser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwagner/Neonflames/2d4407230ffe0db6e9223e24b72ec0f863c38f88/teaser.jpg --------------------------------------------------------------------------------