├── .gitignore ├── assets ├── auto-on.svg ├── auto.svg ├── full-close.svg ├── full.svg ├── hammer.svg ├── index.css ├── index.html ├── play.svg ├── save.png └── save.svg ├── display.js ├── editor-glsl.js ├── editor-search.js ├── editor-sublime.js ├── editor.js ├── examples.js ├── index.js ├── package.json ├── server.js ├── shader-share.js ├── shader-string.js ├── shader-tree.js ├── storage-local.js ├── storage-s3.js ├── storage.js └── test ├── index.js └── storage.js /.gitignore: -------------------------------------------------------------------------------- 1 | .glslify 2 | node_modules 3 | dist/ 4 | npm-debug.log 5 | .db 6 | .playground 7 | -------------------------------------------------------------------------------- /assets/auto-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /assets/auto.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /assets/full-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /assets/full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /assets/hammer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /assets/index.css: -------------------------------------------------------------------------------- 1 | html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,img,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:0 0}body{line-height:1}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}nav ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}a{margin:0;padding:0;font-size:100%;vertical-align:baseline;background:0 0}ins{text-decoration:none}ins,mark{background-color:#ff9;color:#000}mark{font-style:italic;font-weight:700}del{text-decoration:line-through}abbr[title],dfn[title]{border-bottom:1px dotted;cursor:help}table{border-collapse:collapse;border-spacing:0}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}input,select{vertical-align:middle} 2 | .CodeMirror{font-family:'Fantasque Sans Mono',monospace;height:300px;color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror div.CodeMirror-cursor{border-left:1px solid #000}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.CodeMirror.cm-fat-cursor div.CodeMirror-cursor{width:auto;border:0;background:#7e7}.CodeMirror.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1)infinite;-moz-animation:blink 1.06s steps(1)infinite;animation:blink 1.06s steps(1)infinite}@-moz-keyframes blink{0%{background:#7e7}50%{background:0 0}100%{background:#7e7}}@-webkit-keyframes blink{0%{background:#7e7}50%{background:0 0}100%{background:#7e7}}@keyframes blink{0%{background:#7e7}50%{background:0 0}100%{background:#7e7}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-error,.cm-invalidchar{color:red}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:none}.CodeMirror-scroll,.CodeMirror-sizer{position:relative;-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-sizer{border-right:30px solid transparent}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;-moz-box-sizing:content-box;box-sizing:content-box;display:inline-block;margin-bottom:-30px;*zoom:1;*display:inline}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;height:100%}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper{-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:none}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-measure pre{position:static}.CodeMirror div.CodeMirror-cursor{position:absolute;border-right:none;width:0}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror ::selection{background:#d7d4f0}.CodeMirror ::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.CodeMirror span{*vertical-align:text-bottom}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0} 3 | 4 | /* 5 | Original theme by: Zeno Rocha 6 | Brackets theme adaptation by: Felipe KM 7 | Statusbar, Toolbar, Sidebar css added by: Rafael Artoo Derolez 8 | */ 9 | 10 | .cm-s-dracula .CodeMirror-scroll{ 11 | margin-right: 5px; 12 | border-color: #202020; 13 | } 14 | .cm-s-dracula .CodeMirror-vscrollbar{ 15 | overflow-y: hidden; 16 | } 17 | .cm-s-dracula ::-webkit-scrollbar { 18 | width: 10px; 19 | } 20 | .cm-s-dracula ::-webkit-scrollbar-track { 21 | background: transparent; 22 | } 23 | .cm-s-dracula ::-webkit-scrollbar-thumb { 24 | background: #383a48; 25 | border-radius: 5px; 26 | } 27 | .cm-s-dracula ::-webkit-scrollbar-thumb:hover{ 28 | background: #444756; 29 | } 30 | .cm-s-dracula ::-webkit-scrollbar-thumb:active{ 31 | background: #5d6073; 32 | } 33 | 34 | .cm-s-dracula.CodeMirror { 35 | background: #34363B; 36 | color: #EEE; 37 | font-size: 14px; 38 | line-height: 1.5em; 39 | } 40 | 41 | .cm-s-dracula div.CodeMirror-cursor { 42 | border-left: 1px solid #f8f8f0; 43 | z-index: 3; 44 | } 45 | 46 | .CodeMirror-selected { 47 | background: #3d3d3d; 48 | opacity: .23; 49 | } 50 | 51 | .cm-s-dracula span.cm-keyword {color: #66C4FF;} 52 | .cm-s-dracula span.cm-atom {color: #FFE169;} 53 | .cm-s-dracula span.cm-number {color: #FFE169;} 54 | .cm-s-dracula span.cm-def {color: #ffb86c;} 55 | .cm-s-dracula span.cm-variable {color: #EEE;} 56 | .cm-s-dracula span.cm-variable-2 {color: #FFE169;} 57 | .cm-s-dracula span.cm-property {color: #8be9fd;} 58 | .cm-s-dracula span.cm-operator {color: #66C4FF;} 59 | .cm-s-dracula span.cm-comment {color: #A9B0C2;} 60 | .cm-s-dracula span.cm-string {color: #FFE169;} 61 | .cm-s-dracula span.cm-string-2 {color: #FFE169;} 62 | .cm-s-dracula span.cm-meta {color: #61FF90;} 63 | .cm-s-dracula span.cm-qualifier, .CodeMirror span.cm-builtin {color: #61FF90;} 64 | .cm-s-dracula span.cm-bracket {color: #f9faf4;} 65 | .cm-s-dracula span.cm-tag {color: #66C4FF !important;} 66 | .cm-s-dracula span.cm-attribute {color: #61FF90;} 67 | .cm-s-dracula span.cm-matchhighlight {background-color: rgba(68,71,90.23);} 68 | 69 | .cm-s-dracula span.CodeMirror-matchingbracket { 70 | color: #61FF90; 71 | font-weight: bold; 72 | } 73 | 74 | .cm-s-dracula span.CodeMirror-matchingtag { 75 | color: #61FF90 !important; 76 | text-decoration:underline; 77 | background:none; 78 | } 79 | 80 | 81 | .cm-s-dracula span.CodeMirror-searching { 82 | background-color: none; 83 | background: none; 84 | box-shadow: 0 0 0 1px #fff; 85 | } 86 | 87 | .cm-s-dracula .CodeMirror-gutters { 88 | background: #34363B; 89 | border-right: 0.5rem solid #34363B; 90 | } 91 | 92 | .cm-s-dracula .CodeMirror-linenumber { 93 | color: #5B6173; 94 | } 95 | 96 | /** 97 | * Editor Styles 98 | */ 99 | .editor { 100 | position: fixed; 101 | top: 0; right: 0; left: 50%; 102 | bottom: 0; 103 | } 104 | 105 | /** 106 | * UI Styles 107 | */ 108 | body { 109 | background: #4A4F5E; 110 | } 111 | 112 | header { 113 | position: absolute; 114 | top: 0; left: 0; right: 50%; 115 | height: 3rem; 116 | background: #4A4F5E; 117 | } 118 | 119 | header h1 { 120 | font-family: 'Roboto'; 121 | line-height: 3rem; 122 | color: #DEE7FF; 123 | margin: 0; 124 | margin-left: 0.75rem; 125 | font-weight: 300; 126 | font-size: 1.25rem; 127 | } 128 | 129 | header h1 a { 130 | text-decoration: none; 131 | color: #DEE7FF; 132 | } 133 | 134 | header h1 .lang { 135 | color: #61FF90; 136 | } 137 | 138 | main { 139 | width: 50vw; 140 | min-height: calc(100vh - 3rem); 141 | background: #4A4F5E; 142 | } 143 | 144 | .canvas-container { 145 | position: relative; 146 | margin-top: 3rem; 147 | width: 50vw; 148 | height: 28vw; 149 | background: #5B6173; 150 | } 151 | 152 | .canvas-inner { 153 | position: absolute; 154 | top: 0; left: 0; right: 0; bottom: 0; 155 | } 156 | 157 | .buttons { 158 | position: absolute; 159 | right: 0.3rem; 160 | top: 0; 161 | } 162 | 163 | .buttons > li { 164 | opacity: 0.5; 165 | display: block; 166 | cursor: pointer; 167 | width: 2rem; 168 | height: 3rem; 169 | float: right; 170 | padding-left: 0.25rem; 171 | background-size: 80%; 172 | background-repeat: no-repeat; 173 | background-position: center; 174 | transition: opacity 0.2s; 175 | } 176 | 177 | .buttons > li:hover { 178 | opacity: 1; 179 | } 180 | 181 | .buttons li.play { background-image: url('/play.svg'); } 182 | .buttons li.auto { background-image: url('/auto.svg'); } 183 | .buttons li.full { background-image: url('/full.svg'); } 184 | .buttons li.save { background-image: url('/save.svg'); } 185 | .buttons li.grow { background-image: url('/hammer.svg'); } 186 | .fullscreen .buttons li.full { background-image: url('/full-close.svg'); } 187 | .instant .buttons li.auto { background-image: url('/auto-on.svg'); } 188 | 189 | .fullscreen .canvas-container { 190 | position: absolute; 191 | top: 3rem; left: 0; right: 0; bottom: 0; 192 | width: auto; height: auto; 193 | margin-top: 0; 194 | } 195 | 196 | .fullscreen .editor { 197 | left: 2rem; right: 2rem; top: 5rem; bottom: 2rem; 198 | } 199 | 200 | .fullscreen .CodeMirror { 201 | background: rgba(52, 54, 59, 0.5); 202 | border-radius: 0.15rem; 203 | opacity: 0; 204 | transition: opacity 0.2s; 205 | text-shadow: 0 1px rgba(52, 54, 59, 0.5); 206 | } 207 | 208 | .fullscreen .CodeMirror:hover { 209 | opacity: 1; 210 | } 211 | 212 | .fullscreen .CodeMirror .CodeMirror-gutters { 213 | background: transparent; 214 | border-right: 0; 215 | padding-right: 0.5rem; 216 | } 217 | 218 | .fullscreen header { 219 | z-index: 99; 220 | width: 100vw; 221 | } 222 | 223 | header select { 224 | margin-right: 0.75rem; 225 | position: relative; 226 | top: 1.5rem; 227 | transform: translate(0,-50%); 228 | } 229 | 230 | article { 231 | padding: 2rem; 232 | font-family: 'Roboto'; 233 | color: #fff; 234 | font-weight: 300; 235 | } 236 | 237 | article a { 238 | color: #66C4FF; 239 | text-decoration: none; 240 | } 241 | 242 | article p { 243 | line-height: 1.5em; 244 | } 245 | 246 | article p + p { 247 | margin-top: 1rem; 248 | } 249 | 250 | .fullscreen article { 251 | display: none; 252 | } 253 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | glslb.in 5 | 6 | 7 | 8 | 9 |
10 |

11 | GLSLbin 12 |

13 | 34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |

43 | glslbin is a fragment shader sandbox similar to 44 | ShaderToy or 45 | GLSL Sandbox. 46 | It's still a work in progress, so expect more to come soon. 47 |

48 |

49 | It adds support for 50 | glslify, 51 | a GLSL module system which allows you to easily pull in shared code 52 | snippets from npm. 53 |

54 |

55 | These modules are all hosted as part of the 56 | stack.gl project: 57 | you can find a full list of packages in the "Shader Components" 58 | section here. 59 | Enjoy! 60 |

61 |
62 |
63 | 64 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /assets/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /assets/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackgl/glslbin/49751a10199070517d11e84785a4f8c43f5dd72c/assets/save.png -------------------------------------------------------------------------------- /assets/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /display.js: -------------------------------------------------------------------------------- 1 | const Context = require('gl-context') 2 | const triangle = require('a-big-triangle') 3 | const Shader = require('gl-shader') 4 | const now = require('right-now') 5 | const glslify = require('glslify') 6 | 7 | module.exports = Display 8 | 9 | const start = now() 10 | const vert = ` 11 | precision mediump float; 12 | 13 | attribute vec2 position; 14 | 15 | void main() { 16 | gl_Position = vec4(position, 1, 1); 17 | } 18 | ` 19 | 20 | const frag = ` 21 | precision mediump float; 22 | 23 | void main() { 24 | gl_FragColor = vec4(0, 0, 0, 0); 25 | } 26 | ` 27 | 28 | function Display(canvas) { 29 | if (!(this instanceof Display)) return new Display(canvas) 30 | 31 | const gl = this.gl = Context(canvas, render) 32 | const shader = this.shader = Shader(gl, vert, frag) 33 | 34 | function render() { 35 | const width = gl.drawingBufferWidth 36 | const height = gl.drawingBufferHeight 37 | 38 | gl.viewport(0, 0, width, height) 39 | 40 | shader.bind() 41 | shader.uniforms.iGlobalTime = (now() - start) / 1000 42 | shader.uniforms.iResolution = [width, height, 1] 43 | triangle(gl) 44 | } 45 | } 46 | 47 | Display.prototype.update = function(source) { 48 | this.shader.update(vert, source) 49 | } 50 | -------------------------------------------------------------------------------- /editor-glsl.js: -------------------------------------------------------------------------------- 1 | module.exports = function(CodeMirror) { 2 | CodeMirror.defineMode("glsl", function(config, parserConfig) { 3 | var indentUnit = config.indentUnit, 4 | keywords = parserConfig.keywords || words(glslKeywords), 5 | builtins = parserConfig.builtins || words(glslBuiltins), 6 | blockKeywords = parserConfig.blockKeywords || words("case do else for if switch while struct"), 7 | atoms = parserConfig.atoms || words("null"), 8 | hooks = parserConfig.hooks || {}, 9 | multiLineStrings = parserConfig.multiLineStrings; 10 | var isOperatorChar = /[+\-*&%=<>!?|\/]/; 11 | 12 | var curPunc; 13 | 14 | function tokenBase(stream, state) { 15 | var ch = stream.next(); 16 | if (hooks[ch]) { 17 | var result = hooks[ch](stream, state); 18 | if (result !== false) return result; 19 | } 20 | if (ch == '"' || ch == "'") { 21 | state.tokenize = tokenString(ch); 22 | return state.tokenize(stream, state); 23 | } 24 | if (/[\[\]{}\(\),;\:\.]/.test(ch)) { 25 | curPunc = ch; 26 | return "bracket"; 27 | } 28 | if (/\d/.test(ch)) { 29 | stream.eatWhile(/[\w\.]/); 30 | return "number"; 31 | } 32 | if (ch == "/") { 33 | if (stream.eat("*")) { 34 | state.tokenize = tokenComment; 35 | return tokenComment(stream, state); 36 | } 37 | if (stream.eat("/")) { 38 | stream.skipToEnd(); 39 | return "comment"; 40 | } 41 | } 42 | if (ch == "#") { 43 | stream.eatWhile(/[\S]+/); 44 | stream.eatWhile(/[\s]+/); 45 | stream.eatWhile(/[\S]+/); 46 | stream.eatWhile(/[\s]+/); 47 | return "comment"; 48 | } 49 | if (isOperatorChar.test(ch)) { 50 | stream.eatWhile(isOperatorChar); 51 | return "operator"; 52 | } 53 | stream.eatWhile(/[\w\$_]/); 54 | var cur = stream.current(); 55 | if (keywords.propertyIsEnumerable(cur)) { 56 | if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; 57 | return "keyword"; 58 | } 59 | if (builtins.propertyIsEnumerable(cur)) { 60 | return "builtin"; 61 | } 62 | if (atoms.propertyIsEnumerable(cur)) return "atom"; 63 | return "word"; 64 | } 65 | 66 | function tokenString(quote) { 67 | return function(stream, state) { 68 | var escaped = false, next, end = false; 69 | while ((next = stream.next()) != null) { 70 | if (next == quote && !escaped) {end = true; break;} 71 | escaped = !escaped && next == "\\"; 72 | } 73 | if (end || !(escaped || multiLineStrings)) 74 | state.tokenize = tokenBase; 75 | return "string"; 76 | }; 77 | } 78 | 79 | function tokenComment(stream, state) { 80 | var maybeEnd = false, ch; 81 | while (ch = stream.next()) { 82 | if (ch == "/" && maybeEnd) { 83 | state.tokenize = tokenBase; 84 | break; 85 | } 86 | maybeEnd = (ch == "*"); 87 | } 88 | return "comment"; 89 | } 90 | 91 | function Context(indented, column, type, align, prev) { 92 | this.indented = indented; 93 | this.column = column; 94 | this.type = type; 95 | this.align = align; 96 | this.prev = prev; 97 | } 98 | function pushContext(state, col, type) { 99 | return state.context = new Context(state.indented, col, type, null, state.context); 100 | } 101 | function popContext(state) { 102 | var t = state.context.type; 103 | if (t == ")" || t == "]" || t == "}") 104 | state.indented = state.context.indented; 105 | return state.context = state.context.prev; 106 | } 107 | 108 | // Interface 109 | 110 | return { 111 | startState: function(basecolumn) { 112 | return { 113 | tokenize: null, 114 | context: new Context((basecolumn || 0) - indentUnit, 0, "top", false), 115 | indented: 0, 116 | startOfLine: true 117 | }; 118 | }, 119 | 120 | token: function(stream, state) { 121 | var ctx = state.context; 122 | if (stream.sol()) { 123 | if (ctx.align == null) ctx.align = false; 124 | state.indented = stream.indentation(); 125 | state.startOfLine = true; 126 | } 127 | if (stream.eatSpace()) return null; 128 | curPunc = null; 129 | var style = (state.tokenize || tokenBase)(stream, state); 130 | if (style == "comment" || style == "meta") return style; 131 | if (ctx.align == null) ctx.align = true; 132 | 133 | if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state); 134 | else if (curPunc == "{") pushContext(state, stream.column(), "}"); 135 | else if (curPunc == "[") pushContext(state, stream.column(), "]"); 136 | else if (curPunc == "(") pushContext(state, stream.column(), ")"); 137 | else if (curPunc == "}") { 138 | while (ctx.type == "statement") ctx = popContext(state); 139 | if (ctx.type == "}") ctx = popContext(state); 140 | while (ctx.type == "statement") ctx = popContext(state); 141 | } 142 | else if (curPunc == ctx.type) popContext(state); 143 | else if (ctx.type == "}" || ctx.type == "top" || (ctx.type == "statement" && curPunc == "newstatement")) 144 | pushContext(state, stream.column(), "statement"); 145 | state.startOfLine = false; 146 | return style; 147 | }, 148 | 149 | indent: function(state, textAfter) { 150 | if (state.tokenize != tokenBase && state.tokenize != null) return 0; 151 | var firstChar = textAfter && textAfter.charAt(0), ctx = state.context, closing = firstChar == ctx.type; 152 | if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : indentUnit); 153 | else if (ctx.align) return ctx.column + (closing ? 0 : 1); 154 | else return ctx.indented + (closing ? 0 : indentUnit); 155 | }, 156 | 157 | electricChars: "{}" 158 | }; 159 | }); 160 | 161 | function words(str) { 162 | var obj = {}, words = str.split(" "); 163 | for (var i = 0; i < words.length; ++i) obj[words[i]] = true; 164 | return obj; 165 | } 166 | var glslKeywords = "attribute const uniform varying break continue " + 167 | "do for while if else in out inout float int void bool true false " + 168 | "lowp mediump highp precision invariant discard return mat2 mat3 " + 169 | "mat4 vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 sampler2D " + 170 | "samplerCube struct gl_FragCoord gl_FragColor"; 171 | var glslBuiltins = "radians degrees sin cos tan asin acos atan pow " + 172 | "exp log exp2 log2 sqrt inversesqrt abs sign floor ceil fract mod " + 173 | "min max clamp mix step smoothstep length distance dot cross " + 174 | "normalize faceforward reflect refract matrixCompMult lessThan " + 175 | "lessThanEqual greaterThan greaterThanEqual equal notEqual any all " + 176 | "not dFdx dFdy fwidth texture2D texture2DProj texture2DLod " + 177 | "texture2DProjLod textureCube textureCubeLod require export"; 178 | 179 | function cppHook(stream, state) { 180 | if (!state.startOfLine) return false; 181 | stream.skipToEnd(); 182 | return "meta"; 183 | } 184 | 185 | ;(function() { 186 | // C#-style strings where "" escapes a quote. 187 | function tokenAtString(stream, state) { 188 | var next; 189 | while ((next = stream.next()) != null) { 190 | if (next == '"' && !stream.eat('"')) { 191 | state.tokenize = null; 192 | break; 193 | } 194 | } 195 | return "string"; 196 | } 197 | 198 | CodeMirror.defineMIME("text/x-glsl", { 199 | name: "glsl", 200 | keywords: words(glslKeywords), 201 | builtins: words(glslBuiltins), 202 | blockKeywords: words("case do else for if switch while struct"), 203 | atoms: words("null"), 204 | hooks: {"#": cppHook} 205 | }); 206 | }()); 207 | } 208 | -------------------------------------------------------------------------------- /editor-search.js: -------------------------------------------------------------------------------- 1 | module.exports = function(CodeMirror) { 2 | "use strict"; 3 | var Pos = CodeMirror.Pos; 4 | 5 | function SearchCursor(doc, query, pos, caseFold) { 6 | this.atOccurrence = false; this.doc = doc; 7 | if (caseFold == null && typeof query == "string") caseFold = false; 8 | 9 | pos = pos ? doc.clipPos(pos) : Pos(0, 0); 10 | this.pos = {from: pos, to: pos}; 11 | 12 | // The matches method is filled in based on the type of query. 13 | // It takes a position and a direction, and returns an object 14 | // describing the next occurrence of the query, or null if no 15 | // more matches were found. 16 | if (typeof query != "string") { // Regexp match 17 | if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g"); 18 | this.matches = function(reverse, pos) { 19 | if (reverse) { 20 | query.lastIndex = 0; 21 | var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start; 22 | for (;;) { 23 | query.lastIndex = cutOff; 24 | var newMatch = query.exec(line); 25 | if (!newMatch) break; 26 | match = newMatch; 27 | start = match.index; 28 | cutOff = match.index + (match[0].length || 1); 29 | if (cutOff == line.length) break; 30 | } 31 | var matchLen = (match && match[0].length) || 0; 32 | if (!matchLen) { 33 | if (start == 0 && line.length == 0) {match = undefined;} 34 | else if (start != doc.getLine(pos.line).length) { 35 | matchLen++; 36 | } 37 | } 38 | } else { 39 | query.lastIndex = pos.ch; 40 | var line = doc.getLine(pos.line), match = query.exec(line); 41 | var matchLen = (match && match[0].length) || 0; 42 | var start = match && match.index; 43 | if (start + matchLen != line.length && !matchLen) matchLen = 1; 44 | } 45 | if (match && matchLen) 46 | return {from: Pos(pos.line, start), 47 | to: Pos(pos.line, start + matchLen), 48 | match: match}; 49 | }; 50 | } else { // String query 51 | var origQuery = query; 52 | if (caseFold) query = query.toLowerCase(); 53 | var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;}; 54 | var target = query.split("\n"); 55 | // Different methods for single-line and multi-line queries 56 | if (target.length == 1) { 57 | if (!query.length) { 58 | // Empty string would match anything and never progress, so 59 | // we define it to match nothing instead. 60 | this.matches = function() {}; 61 | } else { 62 | this.matches = function(reverse, pos) { 63 | if (reverse) { 64 | var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig); 65 | var match = line.lastIndexOf(query); 66 | if (match > -1) { 67 | match = adjustPos(orig, line, match); 68 | return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)}; 69 | } 70 | } else { 71 | var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig); 72 | var match = line.indexOf(query); 73 | if (match > -1) { 74 | match = adjustPos(orig, line, match) + pos.ch; 75 | return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)}; 76 | } 77 | } 78 | }; 79 | } 80 | } else { 81 | var origTarget = origQuery.split("\n"); 82 | this.matches = function(reverse, pos) { 83 | var last = target.length - 1; 84 | if (reverse) { 85 | if (pos.line - (target.length - 1) < doc.firstLine()) return; 86 | if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return; 87 | var to = Pos(pos.line, origTarget[last].length); 88 | for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln) 89 | if (target[i] != fold(doc.getLine(ln))) return; 90 | var line = doc.getLine(ln), cut = line.length - origTarget[0].length; 91 | if (fold(line.slice(cut)) != target[0]) return; 92 | return {from: Pos(ln, cut), to: to}; 93 | } else { 94 | if (pos.line + (target.length - 1) > doc.lastLine()) return; 95 | var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length; 96 | if (fold(line.slice(cut)) != target[0]) return; 97 | var from = Pos(pos.line, cut); 98 | for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln) 99 | if (target[i] != fold(doc.getLine(ln))) return; 100 | if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return; 101 | return {from: from, to: Pos(ln, origTarget[last].length)}; 102 | } 103 | }; 104 | } 105 | } 106 | } 107 | 108 | SearchCursor.prototype = { 109 | findNext: function() {return this.find(false);}, 110 | findPrevious: function() {return this.find(true);}, 111 | 112 | find: function(reverse) { 113 | var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to); 114 | function savePosAndFail(line) { 115 | var pos = Pos(line, 0); 116 | self.pos = {from: pos, to: pos}; 117 | self.atOccurrence = false; 118 | return false; 119 | } 120 | 121 | for (;;) { 122 | if (this.pos = this.matches(reverse, pos)) { 123 | this.atOccurrence = true; 124 | return this.pos.match || true; 125 | } 126 | if (reverse) { 127 | if (!pos.line) return savePosAndFail(0); 128 | pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length); 129 | } 130 | else { 131 | var maxLine = this.doc.lineCount(); 132 | if (pos.line == maxLine - 1) return savePosAndFail(maxLine); 133 | pos = Pos(pos.line + 1, 0); 134 | } 135 | } 136 | }, 137 | 138 | from: function() {if (this.atOccurrence) return this.pos.from;}, 139 | to: function() {if (this.atOccurrence) return this.pos.to;}, 140 | 141 | replace: function(newText, origin) { 142 | if (!this.atOccurrence) return; 143 | var lines = CodeMirror.splitLines(newText); 144 | this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin); 145 | this.pos.to = Pos(this.pos.from.line + lines.length - 1, 146 | lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)); 147 | } 148 | }; 149 | 150 | // Maps a position in a case-folded line back to a position in the original line 151 | // (compensating for codepoints increasing in number during folding) 152 | function adjustPos(orig, folded, pos) { 153 | if (orig.length == folded.length) return pos; 154 | for (var pos1 = Math.min(pos, orig.length);;) { 155 | var len1 = orig.slice(0, pos1).toLowerCase().length; 156 | if (len1 < pos) ++pos1; 157 | else if (len1 > pos) --pos1; 158 | else return pos1; 159 | } 160 | } 161 | 162 | CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { 163 | return new SearchCursor(this.doc, query, pos, caseFold); 164 | }); 165 | CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { 166 | return new SearchCursor(this, query, pos, caseFold); 167 | }); 168 | 169 | CodeMirror.defineExtension("selectMatches", function(query, caseFold) { 170 | var ranges = [], next; 171 | var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold); 172 | while (next = cur.findNext()) { 173 | if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break; 174 | ranges.push({anchor: cur.from(), head: cur.to()}); 175 | } 176 | if (ranges.length) 177 | this.setSelections(ranges, 0); 178 | }); 179 | } 180 | -------------------------------------------------------------------------------- /editor-sublime.js: -------------------------------------------------------------------------------- 1 | module.exports = function(CodeMirror) { 2 | "use strict"; 3 | 4 | var map = CodeMirror.keyMap.sublime = {fallthrough: "default"}; 5 | var cmds = CodeMirror.commands; 6 | var Pos = CodeMirror.Pos; 7 | var mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault; 8 | var ctrl = mac ? "Cmd-" : "Ctrl-"; 9 | 10 | // This is not exactly Sublime's algorithm. I couldn't make heads or tails of that. 11 | function findPosSubword(doc, start, dir) { 12 | if (dir < 0 && start.ch == 0) return doc.clipPos(Pos(start.line - 1)); 13 | var line = doc.getLine(start.line); 14 | if (dir > 0 && start.ch >= line.length) return doc.clipPos(Pos(start.line + 1, 0)); 15 | var state = "start", type; 16 | for (var pos = start.ch, e = dir < 0 ? 0 : line.length, i = 0; pos != e; pos += dir, i++) { 17 | var next = line.charAt(dir < 0 ? pos - 1 : pos); 18 | var cat = next != "_" && CodeMirror.isWordChar(next) ? "w" : "o"; 19 | if (cat == "w" && next.toUpperCase() == next) cat = "W"; 20 | if (state == "start") { 21 | if (cat != "o") { state = "in"; type = cat; } 22 | } else if (state == "in") { 23 | if (type != cat) { 24 | if (type == "w" && cat == "W" && dir < 0) pos--; 25 | if (type == "W" && cat == "w" && dir > 0) { type = "w"; continue; } 26 | break; 27 | } 28 | } 29 | } 30 | return Pos(start.line, pos); 31 | } 32 | 33 | function moveSubword(cm, dir) { 34 | cm.extendSelectionsBy(function(range) { 35 | if (cm.display.shift || cm.doc.extend || range.empty()) 36 | return findPosSubword(cm.doc, range.head, dir); 37 | else 38 | return dir < 0 ? range.from() : range.to(); 39 | }); 40 | } 41 | 42 | // cmds[map["Alt-Left"] = "goSubwordLeft"] = function(cm) { moveSubword(cm, -1); }; 43 | // cmds[map["Alt-Right"] = "goSubwordRight"] = function(cm) { moveSubword(cm, 1); }; 44 | 45 | // cmds[map[ctrl + "Up"] = "scrollLineUp"] = function(cm) { 46 | // var info = cm.getScrollInfo(); 47 | // if (!cm.somethingSelected()) { 48 | // var visibleBottomLine = cm.lineAtHeight(info.top + info.clientHeight, "local"); 49 | // if (cm.getCursor().line >= visibleBottomLine) 50 | // cm.execCommand("goLineUp"); 51 | // } 52 | // cm.scrollTo(null, info.top - cm.defaultTextHeight()); 53 | // }; 54 | // cmds[map[ctrl + "Down"] = "scrollLineDown"] = function(cm) { 55 | // var info = cm.getScrollInfo(); 56 | // if (!cm.somethingSelected()) { 57 | // var visibleTopLine = cm.lineAtHeight(info.top, "local")+1; 58 | // if (cm.getCursor().line <= visibleTopLine) 59 | // cm.execCommand("goLineDown"); 60 | // } 61 | // cm.scrollTo(null, info.top + cm.defaultTextHeight()); 62 | // }; 63 | 64 | cmds[map["Shift-" + ctrl + "L"] = "splitSelectionByLine"] = function(cm) { 65 | var ranges = cm.listSelections(), lineRanges = []; 66 | for (var i = 0; i < ranges.length; i++) { 67 | var from = ranges[i].from(), to = ranges[i].to(); 68 | for (var line = from.line; line <= to.line; ++line) 69 | if (!(to.line > from.line && line == to.line && to.ch == 0)) 70 | lineRanges.push({anchor: line == from.line ? from : Pos(line, 0), 71 | head: line == to.line ? to : Pos(line)}); 72 | } 73 | cm.setSelections(lineRanges, 0); 74 | }; 75 | 76 | map["Shift-Tab"] = "indentLess"; 77 | 78 | cmds[map["Esc"] = "singleSelectionTop"] = function(cm) { 79 | var range = cm.listSelections()[0]; 80 | cm.setSelection(range.anchor, range.head, {scroll: false}); 81 | }; 82 | 83 | cmds[map[ctrl + "L"] = "selectLine"] = function(cm) { 84 | var ranges = cm.listSelections(), extended = []; 85 | for (var i = 0; i < ranges.length; i++) { 86 | var range = ranges[i]; 87 | extended.push({anchor: Pos(range.from().line, 0), 88 | head: Pos(range.to().line + 1, 0)}); 89 | } 90 | cm.setSelections(extended); 91 | }; 92 | 93 | map["Shift-" + ctrl + "K"] = "deleteLine"; 94 | 95 | function insertLine(cm, above) { 96 | cm.operation(function() { 97 | var len = cm.listSelections().length, newSelection = [], last = -1; 98 | for (var i = 0; i < len; i++) { 99 | var head = cm.listSelections()[i].head; 100 | if (head.line <= last) continue; 101 | var at = Pos(head.line + (above ? 0 : 1), 0); 102 | cm.replaceRange("\n", at, null, "+insertLine"); 103 | cm.indentLine(at.line, null, true); 104 | newSelection.push({head: at, anchor: at}); 105 | last = head.line + 1; 106 | } 107 | cm.setSelections(newSelection); 108 | }); 109 | } 110 | 111 | cmds[map[ctrl + "Enter"] = "insertLineAfter"] = function(cm) { insertLine(cm, false); }; 112 | 113 | cmds[map["Shift-" + ctrl + "Enter"] = "insertLineBefore"] = function(cm) { insertLine(cm, true); }; 114 | 115 | function wordAt(cm, pos) { 116 | var start = pos.ch, end = start, line = cm.getLine(pos.line); 117 | while (start && CodeMirror.isWordChar(line.charAt(start - 1))) --start; 118 | while (end < line.length && CodeMirror.isWordChar(line.charAt(end))) ++end; 119 | return {from: Pos(pos.line, start), to: Pos(pos.line, end), word: line.slice(start, end)}; 120 | } 121 | 122 | cmds[map[ctrl + "D"] = "selectNextOccurrence"] = function(cm) { 123 | var from = cm.getCursor("from"), to = cm.getCursor("to"); 124 | var fullWord = cm.state.sublimeFindFullWord == cm.doc.sel; 125 | if (CodeMirror.cmpPos(from, to) == 0) { 126 | var word = wordAt(cm, from); 127 | if (!word.word) return; 128 | cm.setSelection(word.from, word.to); 129 | fullWord = true; 130 | } else { 131 | var text = cm.getRange(from, to); 132 | var query = fullWord ? new RegExp("\\b" + text + "\\b") : text; 133 | var cur = cm.getSearchCursor(query, to); 134 | if (cur.findNext()) { 135 | cm.addSelection(cur.from(), cur.to()); 136 | } else { 137 | cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0)); 138 | if (cur.findNext()) 139 | cm.addSelection(cur.from(), cur.to()); 140 | } 141 | } 142 | if (fullWord) 143 | cm.state.sublimeFindFullWord = cm.doc.sel; 144 | }; 145 | 146 | var mirror = "(){}[]"; 147 | function selectBetweenBrackets(cm) { 148 | var pos = cm.getCursor(), opening = cm.scanForBracket(pos, -1); 149 | if (!opening) return; 150 | for (;;) { 151 | var closing = cm.scanForBracket(pos, 1); 152 | if (!closing) return; 153 | if (closing.ch == mirror.charAt(mirror.indexOf(opening.ch) + 1)) { 154 | cm.setSelection(Pos(opening.pos.line, opening.pos.ch + 1), closing.pos, false); 155 | return true; 156 | } 157 | pos = Pos(closing.pos.line, closing.pos.ch + 1); 158 | } 159 | } 160 | 161 | cmds[map["Shift-" + ctrl + "Space"] = "selectScope"] = function(cm) { 162 | selectBetweenBrackets(cm) || cm.execCommand("selectAll"); 163 | }; 164 | cmds[map["Shift-" + ctrl + "M"] = "selectBetweenBrackets"] = function(cm) { 165 | if (!selectBetweenBrackets(cm)) return CodeMirror.Pass; 166 | }; 167 | 168 | cmds[map[ctrl + "M"] = "goToBracket"] = function(cm) { 169 | cm.extendSelectionsBy(function(range) { 170 | var next = cm.scanForBracket(range.head, 1); 171 | if (next && CodeMirror.cmpPos(next.pos, range.head) != 0) return next.pos; 172 | var prev = cm.scanForBracket(range.head, -1); 173 | return prev && Pos(prev.pos.line, prev.pos.ch + 1) || range.head; 174 | }); 175 | }; 176 | 177 | var swapLineCombo = mac ? "Cmd-Ctrl-" : "Shift-Ctrl-"; 178 | 179 | cmds[map[swapLineCombo + "Up"] = "swapLineUp"] = function(cm) { 180 | var ranges = cm.listSelections(), linesToMove = [], at = cm.firstLine() - 1, newSels = []; 181 | for (var i = 0; i < ranges.length; i++) { 182 | var range = ranges[i], from = range.from().line - 1, to = range.to().line; 183 | newSels.push({anchor: Pos(range.anchor.line - 1, range.anchor.ch), 184 | head: Pos(range.head.line - 1, range.head.ch)}); 185 | if (range.to().ch == 0 && !range.empty()) --to; 186 | if (from > at) linesToMove.push(from, to); 187 | else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to; 188 | at = to; 189 | } 190 | cm.operation(function() { 191 | for (var i = 0; i < linesToMove.length; i += 2) { 192 | var from = linesToMove[i], to = linesToMove[i + 1]; 193 | var line = cm.getLine(from); 194 | cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine"); 195 | if (to > cm.lastLine()) 196 | cm.replaceRange("\n" + line, Pos(cm.lastLine()), null, "+swapLine"); 197 | else 198 | cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine"); 199 | } 200 | cm.setSelections(newSels); 201 | cm.scrollIntoView(); 202 | }); 203 | }; 204 | 205 | cmds[map[swapLineCombo + "Down"] = "swapLineDown"] = function(cm) { 206 | var ranges = cm.listSelections(), linesToMove = [], at = cm.lastLine() + 1; 207 | for (var i = ranges.length - 1; i >= 0; i--) { 208 | var range = ranges[i], from = range.to().line + 1, to = range.from().line; 209 | if (range.to().ch == 0 && !range.empty()) from--; 210 | if (from < at) linesToMove.push(from, to); 211 | else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to; 212 | at = to; 213 | } 214 | cm.operation(function() { 215 | for (var i = linesToMove.length - 2; i >= 0; i -= 2) { 216 | var from = linesToMove[i], to = linesToMove[i + 1]; 217 | var line = cm.getLine(from); 218 | if (from == cm.lastLine()) 219 | cm.replaceRange("", Pos(from - 1), Pos(from), "+swapLine"); 220 | else 221 | cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine"); 222 | cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine"); 223 | } 224 | cm.scrollIntoView(); 225 | }); 226 | }; 227 | 228 | map[ctrl + "/"] = "toggleComment"; 229 | 230 | cmds[map[ctrl + "J"] = "joinLines"] = function(cm) { 231 | var ranges = cm.listSelections(), joined = []; 232 | for (var i = 0; i < ranges.length; i++) { 233 | var range = ranges[i], from = range.from(); 234 | var start = from.line, end = range.to().line; 235 | while (i < ranges.length - 1 && ranges[i + 1].from().line == end) 236 | end = ranges[++i].to().line; 237 | joined.push({start: start, end: end, anchor: !range.empty() && from}); 238 | } 239 | cm.operation(function() { 240 | var offset = 0, ranges = []; 241 | for (var i = 0; i < joined.length; i++) { 242 | var obj = joined[i]; 243 | var anchor = obj.anchor && Pos(obj.anchor.line - offset, obj.anchor.ch), head; 244 | for (var line = obj.start; line <= obj.end; line++) { 245 | var actual = line - offset; 246 | if (line == obj.end) head = Pos(actual, cm.getLine(actual).length + 1); 247 | if (actual < cm.lastLine()) { 248 | cm.replaceRange(" ", Pos(actual), Pos(actual + 1, /^\s*/.exec(cm.getLine(actual + 1))[0].length)); 249 | ++offset; 250 | } 251 | } 252 | ranges.push({anchor: anchor || head, head: head}); 253 | } 254 | cm.setSelections(ranges, 0); 255 | }); 256 | }; 257 | 258 | cmds[map["Shift-" + ctrl + "D"] = "duplicateLine"] = function(cm) { 259 | cm.operation(function() { 260 | var rangeCount = cm.listSelections().length; 261 | for (var i = 0; i < rangeCount; i++) { 262 | var range = cm.listSelections()[i]; 263 | if (range.empty()) 264 | cm.replaceRange(cm.getLine(range.head.line) + "\n", Pos(range.head.line, 0)); 265 | else 266 | cm.replaceRange(cm.getRange(range.from(), range.to()), range.from()); 267 | } 268 | cm.scrollIntoView(); 269 | }); 270 | }; 271 | 272 | map[ctrl + "T"] = "transposeChars"; 273 | 274 | function sortLines(cm, caseSensitive) { 275 | var ranges = cm.listSelections(), toSort = [], selected; 276 | for (var i = 0; i < ranges.length; i++) { 277 | var range = ranges[i]; 278 | if (range.empty()) continue; 279 | var from = range.from().line, to = range.to().line; 280 | while (i < ranges.length - 1 && ranges[i + 1].from().line == to) 281 | to = range[++i].to().line; 282 | toSort.push(from, to); 283 | } 284 | if (toSort.length) selected = true; 285 | else toSort.push(cm.firstLine(), cm.lastLine()); 286 | 287 | cm.operation(function() { 288 | var ranges = []; 289 | for (var i = 0; i < toSort.length; i += 2) { 290 | var from = toSort[i], to = toSort[i + 1]; 291 | var start = Pos(from, 0), end = Pos(to); 292 | var lines = cm.getRange(start, end, false); 293 | if (caseSensitive) 294 | lines.sort(); 295 | else 296 | lines.sort(function(a, b) { 297 | var au = a.toUpperCase(), bu = b.toUpperCase(); 298 | if (au != bu) { a = au; b = bu; } 299 | return a < b ? -1 : a == b ? 0 : 1; 300 | }); 301 | cm.replaceRange(lines, start, end); 302 | if (selected) ranges.push({anchor: start, head: end}); 303 | } 304 | if (selected) cm.setSelections(ranges, 0); 305 | }); 306 | } 307 | 308 | cmds[map["F9"] = "sortLines"] = function(cm) { sortLines(cm, true); }; 309 | cmds[map[ctrl + "F9"] = "sortLinesInsensitive"] = function(cm) { sortLines(cm, false); }; 310 | 311 | cmds[map["F2"] = "nextBookmark"] = function(cm) { 312 | var marks = cm.state.sublimeBookmarks; 313 | if (marks) while (marks.length) { 314 | var current = marks.shift(); 315 | var found = current.find(); 316 | if (found) { 317 | marks.push(current); 318 | return cm.setSelection(found.from, found.to); 319 | } 320 | } 321 | }; 322 | 323 | cmds[map["Shift-F2"] = "prevBookmark"] = function(cm) { 324 | var marks = cm.state.sublimeBookmarks; 325 | if (marks) while (marks.length) { 326 | marks.unshift(marks.pop()); 327 | var found = marks[marks.length - 1].find(); 328 | if (!found) 329 | marks.pop(); 330 | else 331 | return cm.setSelection(found.from, found.to); 332 | } 333 | }; 334 | 335 | cmds[map[ctrl + "F2"] = "toggleBookmark"] = function(cm) { 336 | var ranges = cm.listSelections(); 337 | var marks = cm.state.sublimeBookmarks || (cm.state.sublimeBookmarks = []); 338 | for (var i = 0; i < ranges.length; i++) { 339 | var from = ranges[i].from(), to = ranges[i].to(); 340 | var found = cm.findMarks(from, to); 341 | for (var j = 0; j < found.length; j++) { 342 | if (found[j].sublimeBookmark) { 343 | found[j].clear(); 344 | for (var k = 0; k < marks.length; k++) 345 | if (marks[k] == found[j]) 346 | marks.splice(k--, 1); 347 | break; 348 | } 349 | } 350 | if (j == found.length) 351 | marks.push(cm.markText(from, to, {sublimeBookmark: true, clearWhenEmpty: false})); 352 | } 353 | }; 354 | 355 | cmds[map["Shift-" + ctrl + "F2"] = "clearBookmarks"] = function(cm) { 356 | var marks = cm.state.sublimeBookmarks; 357 | if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear(); 358 | marks.length = 0; 359 | }; 360 | 361 | cmds[map["Alt-F2"] = "selectBookmarks"] = function(cm) { 362 | var marks = cm.state.sublimeBookmarks, ranges = []; 363 | if (marks) for (var i = 0; i < marks.length; i++) { 364 | var found = marks[i].find(); 365 | if (!found) 366 | marks.splice(i--, 0); 367 | else 368 | ranges.push({anchor: found.from, head: found.to}); 369 | } 370 | if (ranges.length) 371 | cm.setSelections(ranges, 0); 372 | }; 373 | 374 | map["Alt-Q"] = "wrapLines"; 375 | 376 | var cK = ctrl + "K "; 377 | 378 | function modifyWordOrSelection(cm, mod) { 379 | cm.operation(function() { 380 | var ranges = cm.listSelections(), indices = [], replacements = []; 381 | for (var i = 0; i < ranges.length; i++) { 382 | var range = ranges[i]; 383 | if (range.empty()) { indices.push(i); replacements.push(""); } 384 | else replacements.push(mod(cm.getRange(range.from(), range.to()))); 385 | } 386 | cm.replaceSelections(replacements, "around", "case"); 387 | for (var i = indices.length - 1, at; i >= 0; i--) { 388 | var range = ranges[indices[i]]; 389 | if (at && CodeMirror.cmpPos(range.head, at) > 0) continue; 390 | var word = wordAt(cm, range.head); 391 | at = word.from; 392 | cm.replaceRange(mod(word.word), word.from, word.to); 393 | } 394 | }); 395 | } 396 | 397 | map[cK + ctrl + "Backspace"] = "delLineLeft"; 398 | 399 | // cmds[map["Backspace"] = "smartBackspace"] = function(cm) { 400 | // if (cm.somethingSelected()) return CodeMirror.Pass; 401 | // 402 | // var cursor = cm.getCursor(); 403 | // var toStartOfLine = cm.getRange({line: cursor.line, ch: 0}, cursor); 404 | // var column = CodeMirror.countColumn(toStartOfLine, null, cm.getOption("tabSize")); 405 | // 406 | // if (!/\S/.test(toStartOfLine) && column % cm.getOption("indentUnit") == 0) 407 | // return cm.indentSelection("subtract"); 408 | // else 409 | // return CodeMirror.Pass; 410 | // }; 411 | 412 | cmds[map[cK + ctrl + "K"] = "delLineRight"] = function(cm) { 413 | cm.operation(function() { 414 | var ranges = cm.listSelections(); 415 | for (var i = ranges.length - 1; i >= 0; i--) 416 | cm.replaceRange("", ranges[i].anchor, Pos(ranges[i].to().line), "+delete"); 417 | cm.scrollIntoView(); 418 | }); 419 | }; 420 | 421 | cmds[map[cK + ctrl + "U"] = "upcaseAtCursor"] = function(cm) { 422 | modifyWordOrSelection(cm, function(str) { return str.toUpperCase(); }); 423 | }; 424 | cmds[map[cK + ctrl + "L"] = "downcaseAtCursor"] = function(cm) { 425 | modifyWordOrSelection(cm, function(str) { return str.toLowerCase(); }); 426 | }; 427 | 428 | cmds[map[cK + ctrl + "Space"] = "setSublimeMark"] = function(cm) { 429 | if (cm.state.sublimeMark) cm.state.sublimeMark.clear(); 430 | cm.state.sublimeMark = cm.setBookmark(cm.getCursor()); 431 | }; 432 | cmds[map[cK + ctrl + "A"] = "selectToSublimeMark"] = function(cm) { 433 | var found = cm.state.sublimeMark && cm.state.sublimeMark.find(); 434 | if (found) cm.setSelection(cm.getCursor(), found); 435 | }; 436 | cmds[map[cK + ctrl + "W"] = "deleteToSublimeMark"] = function(cm) { 437 | var found = cm.state.sublimeMark && cm.state.sublimeMark.find(); 438 | if (found) { 439 | var from = cm.getCursor(), to = found; 440 | if (CodeMirror.cmpPos(from, to) > 0) { var tmp = to; to = from; from = tmp; } 441 | cm.state.sublimeKilled = cm.getRange(from, to); 442 | cm.replaceRange("", from, to); 443 | } 444 | }; 445 | cmds[map[cK + ctrl + "X"] = "swapWithSublimeMark"] = function(cm) { 446 | var found = cm.state.sublimeMark && cm.state.sublimeMark.find(); 447 | if (found) { 448 | cm.state.sublimeMark.clear(); 449 | cm.state.sublimeMark = cm.setBookmark(cm.getCursor()); 450 | cm.setCursor(found); 451 | } 452 | }; 453 | cmds[map[cK + ctrl + "Y"] = "sublimeYank"] = function(cm) { 454 | if (cm.state.sublimeKilled != null) 455 | cm.replaceSelection(cm.state.sublimeKilled, null, "paste"); 456 | }; 457 | 458 | map[cK + ctrl + "G"] = "clearBookmarks"; 459 | cmds[map[cK + ctrl + "C"] = "showInCenter"] = function(cm) { 460 | var pos = cm.cursorCoords(null, "local"); 461 | cm.scrollTo(null, (pos.top + pos.bottom) / 2 - cm.getScrollInfo().clientHeight / 2); 462 | }; 463 | 464 | cmds[map["Shift-Alt-Up"] = "selectLinesUpward"] = function(cm) { 465 | cm.operation(function() { 466 | var ranges = cm.listSelections(); 467 | for (var i = 0; i < ranges.length; i++) { 468 | var range = ranges[i]; 469 | if (range.head.line > cm.firstLine()) 470 | cm.addSelection(Pos(range.head.line - 1, range.head.ch)); 471 | } 472 | }); 473 | }; 474 | cmds[map["Shift-Alt-Down"] = "selectLinesDownward"] = function(cm) { 475 | cm.operation(function() { 476 | var ranges = cm.listSelections(); 477 | for (var i = 0; i < ranges.length; i++) { 478 | var range = ranges[i]; 479 | if (range.head.line < cm.lastLine()) 480 | cm.addSelection(Pos(range.head.line + 1, range.head.ch)); 481 | } 482 | }); 483 | }; 484 | 485 | function getTarget(cm) { 486 | var from = cm.getCursor("from"), to = cm.getCursor("to"); 487 | if (CodeMirror.cmpPos(from, to) == 0) { 488 | var word = wordAt(cm, from); 489 | if (!word.word) return; 490 | from = word.from; 491 | to = word.to; 492 | } 493 | return {from: from, to: to, query: cm.getRange(from, to), word: word}; 494 | } 495 | 496 | function findAndGoTo(cm, forward) { 497 | var target = getTarget(cm); 498 | if (!target) return; 499 | var query = target.query; 500 | var cur = cm.getSearchCursor(query, forward ? target.to : target.from); 501 | 502 | if (forward ? cur.findNext() : cur.findPrevious()) { 503 | cm.setSelection(cur.from(), cur.to()); 504 | } else { 505 | cur = cm.getSearchCursor(query, forward ? Pos(cm.firstLine(), 0) 506 | : cm.clipPos(Pos(cm.lastLine()))); 507 | if (forward ? cur.findNext() : cur.findPrevious()) 508 | cm.setSelection(cur.from(), cur.to()); 509 | else if (target.word) 510 | cm.setSelection(target.from, target.to); 511 | } 512 | }; 513 | cmds[map[ctrl + "F3"] = "findUnder"] = function(cm) { findAndGoTo(cm, true); }; 514 | cmds[map["Shift-" + ctrl + "F3"] = "findUnderPrevious"] = function(cm) { findAndGoTo(cm,false); }; 515 | cmds[map["Alt-F3"] = "findAllUnder"] = function(cm) { 516 | var target = getTarget(cm); 517 | if (!target) return; 518 | var cur = cm.getSearchCursor(target.query); 519 | var matches = []; 520 | var primaryIndex = -1; 521 | while (cur.findNext()) { 522 | matches.push({anchor: cur.from(), head: cur.to()}); 523 | if (cur.from().line <= target.from.line && cur.from().ch <= target.from.ch) 524 | primaryIndex++; 525 | } 526 | cm.setSelections(matches, primaryIndex); 527 | }; 528 | 529 | map["Shift-" + ctrl + "["] = "fold"; 530 | map["Shift-" + ctrl + "]"] = "unfold"; 531 | map[cK + ctrl + "0"] = map[cK + ctrl + "j"] = "unfoldAll"; 532 | 533 | map[ctrl + "I"] = "findIncremental"; 534 | map["Shift-" + ctrl + "I"] = "findIncrementalReverse"; 535 | map[ctrl + "H"] = "replace"; 536 | map["F3"] = "findNext"; 537 | map["Shift-F3"] = "findPrev"; 538 | 539 | CodeMirror.normalizeKeyMap(map); 540 | } 541 | -------------------------------------------------------------------------------- /editor.js: -------------------------------------------------------------------------------- 1 | const mousetrap =(require('mousetrap'), window.Mousetrap) 2 | const frameDebounce = require('frame-debounce') 3 | const Client = require('glslify-client') 4 | const size = require('element-size') 5 | const CodeMirror = require('codemirror') 6 | const inherits = require('inherits') 7 | const debounce = require('debounce') 8 | const Emitter = require('events/') 9 | const xhr = require('xhr') 10 | 11 | module.exports = Editor 12 | 13 | require('./editor-glsl')(CodeMirror) 14 | require('./editor-search')(CodeMirror) 15 | require('./editor-sublime')(CodeMirror) 16 | 17 | inherits(Editor, Emitter) 18 | function Editor(container, src) { 19 | if (!(this instanceof Editor)) return new Editor(container, src) 20 | Emitter.call(this) 21 | 22 | var self = this 23 | 24 | this.el = container.appendChild(document.createElement('div')) 25 | this.el.classList.add('editor') 26 | 27 | this.editor = new CodeMirror(this.el, { 28 | container: this.el, 29 | theme: 'dracula', 30 | mode: 'glsl', 31 | lineNumbers: true, 32 | matchBrackets: true, 33 | indentWithTabs: false, 34 | styleActiveLine: true, 35 | showCursorWhenSelecting: true, 36 | viewportMargin: Infinity, 37 | keyMap: 'sublime', 38 | indentUnit: 2, 39 | tabSize: 2, 40 | value: '' 41 | }) 42 | 43 | this.editor.addKeyMap({ 44 | 'Cmd-Enter': () => this.reload(), 45 | 'Ctrl-Enter': () => this.reload(), 46 | 'Cmd-O': () => this.emit('fullscreen'), 47 | 'Ctrl-O': () => this.emit('fullscreen'), 48 | 'Cmd-;': () => this.instant = !this.instant, 49 | 'Ctrl-;': () => this.instant = !this.instant, 50 | 'Tab': () => this.editor.execCommand('insertSoftTab') 51 | }) 52 | 53 | // Auto-updating disabled for now 54 | this.instant = true 55 | this.editor.on('change', debounce(function() { 56 | if (!self.instant) return 57 | self.update(self.editor.getValue()) 58 | }, 500)) 59 | 60 | this._update = Client(function(source, done) { 61 | xhr({ 62 | uri: '/-/shader', 63 | method: 'POST', 64 | body: source 65 | }, function(err, res, tree) { 66 | if (err) return done(err) 67 | 68 | try { 69 | tree = JSON.parse(tree) 70 | } catch(err) { 71 | return done(err) 72 | } 73 | 74 | done(null, tree) 75 | }) 76 | }) 77 | 78 | setTimeout(function() { 79 | self.editor.focus() 80 | self.resize() 81 | }) 82 | 83 | window.addEventListener('resize', frameDebounce(function() { 84 | self.resize() 85 | }), false) 86 | 87 | if (src) { 88 | this.editor.setValue(src) 89 | this.update(src, function(err) { 90 | if (err) console.error(err) 91 | }) 92 | } 93 | } 94 | 95 | Editor.prototype.resize = function(w, h) { 96 | if (w && h) return this.editor.setSize(w, h) 97 | var sz = size(this.el) 98 | 99 | this.editor.setSize(w || sz[0], h || sz[1]) 100 | } 101 | 102 | Editor.prototype.update = function(src, done) { 103 | var self = this 104 | 105 | this._update(src, function(err, result) { 106 | if (err) return done && done(err) 107 | self.emit('update', result) 108 | done && done(null, result) 109 | }) 110 | } 111 | 112 | Editor.prototype.reload = function() { 113 | this.update(this.editor.getValue()) 114 | } 115 | 116 | Editor.prototype.value = function(value) { 117 | this.editor.setValue(value) 118 | this.reload() 119 | } 120 | 121 | Object.defineProperty(Editor.prototype, 'instant', { 122 | get: function() { 123 | return this._instant 124 | }, 125 | set: function(value) { 126 | if (value === this._instant) return 127 | if (value) { 128 | document.body.classList.add('instant') 129 | } else { 130 | document.body.classList.remove('instant') 131 | } 132 | 133 | return this._instant = value 134 | } 135 | }) 136 | -------------------------------------------------------------------------------- /examples.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | noise: ` 3 | 4 | precision mediump float; 5 | 6 | uniform float iGlobalTime; 7 | uniform vec3 iResolution; 8 | 9 | #pragma glslify: noise = require("glsl-noise/simplex/3d") 10 | 11 | void main() { 12 | float n = noise(vec3(gl_FragCoord.xy * 0.005, iGlobalTime)); 13 | gl_FragColor.rgb = vec3(n); 14 | gl_FragColor.a = 1.0; 15 | } 16 | 17 | `, 18 | 19 | sphere: ` 20 | precision mediump float; 21 | 22 | uniform float iGlobalTime; 23 | uniform vec3 iResolution; 24 | 25 | vec2 doModel(vec3 p); 26 | 27 | #pragma glslify: raytrace = require('glsl-raytrace', map = doModel, steps = 90) 28 | #pragma glslify: normal = require('glsl-sdf-normal', map = doModel) 29 | #pragma glslify: camera = require('glsl-turntable-camera') 30 | 31 | vec2 doModel(vec3 p) { 32 | float id = 0.0; 33 | float d = length(p) - 1.0; 34 | return vec2(d, id); 35 | } 36 | 37 | void main() { 38 | vec3 color = vec3(0.0); 39 | vec3 ro, rd; 40 | 41 | float rotation = iGlobalTime; 42 | float height = 0.0; 43 | float dist = 4.0; 44 | camera(rotation, height, dist, iResolution.xy, ro, rd); 45 | 46 | vec2 t = raytrace(ro, rd); 47 | if (t.x > -0.5) { 48 | vec3 pos = ro + rd * t.x; 49 | vec3 nor = normal(pos); 50 | 51 | color = nor * 0.5 + 0.5; 52 | } 53 | 54 | gl_FragColor.rgb = color; 55 | gl_FragColor.a = 1.0; 56 | } 57 | `, 58 | blob: ` 59 | precision mediump float; 60 | 61 | uniform float iGlobalTime; 62 | uniform vec3 iResolution; 63 | 64 | vec2 doModel(vec3 p); 65 | 66 | #pragma glslify: raytrace = require('glsl-raytrace', map = doModel, steps = 90) 67 | #pragma glslify: normal = require('glsl-sdf-normal', map = doModel) 68 | #pragma glslify: camera = require('glsl-turntable-camera') 69 | #pragma glslify: noise = require('glsl-noise/simplex/4d') 70 | 71 | vec2 doModel(vec3 p) { 72 | float r = 1.0 + noise(vec4(p, iGlobalTime)) * 0.25; 73 | float d = length(p) - r; 74 | float id = 0.0; 75 | 76 | return vec2(d, id); 77 | } 78 | 79 | void main() { 80 | vec3 color = vec3(0.0); 81 | vec3 ro, rd; 82 | 83 | float rotation = iGlobalTime; 84 | float height = 0.0; 85 | float dist = 4.0; 86 | camera(rotation, height, dist, iResolution.xy, ro, rd); 87 | 88 | vec2 t = raytrace(ro, rd); 89 | if (t.x > -0.5) { 90 | vec3 pos = ro + rd * t.x; 91 | vec3 nor = normal(pos); 92 | 93 | color = nor * 0.5 + 0.5; 94 | } 95 | 96 | gl_FragColor.rgb = color; 97 | gl_FragColor.a = 1.0; 98 | } 99 | `, 100 | basicLighting: ` 101 | precision mediump float; 102 | 103 | uniform float iGlobalTime; 104 | uniform vec3 iResolution; 105 | 106 | vec2 doModel(vec3 p); 107 | 108 | #pragma glslify: raytrace = require('glsl-raytrace', map = doModel, steps = 90) 109 | #pragma glslify: normal = require('glsl-sdf-normal', map = doModel) 110 | #pragma glslify: camera = require('glsl-turntable-camera') 111 | #pragma glslify: noise = require('glsl-noise/simplex/4d') 112 | 113 | vec2 doModel(vec3 p) { 114 | float r = 1.0 + noise(vec4(p, iGlobalTime)) * 0.25; 115 | float d = length(p) - r; 116 | float id = 0.0; 117 | 118 | return vec2(d, id); 119 | } 120 | 121 | vec3 lighting(vec3 pos, vec3 nor, vec3 ro, vec3 rd) { 122 | vec3 dir = normalize(vec3(0, 1, 0)); 123 | vec3 col = vec3(0.9, 0.5, 0.3); 124 | vec3 dif = col * max(0.0, dot(dir, nor)); 125 | 126 | vec3 ambient = vec3(0.05); 127 | 128 | return dif + ambient; 129 | } 130 | 131 | void main() { 132 | vec3 color = vec3(0.0); 133 | vec3 ro, rd; 134 | 135 | float rotation = iGlobalTime; 136 | float height = 2.5; 137 | float dist = 4.0; 138 | camera(rotation, height, dist, iResolution.xy, ro, rd); 139 | 140 | vec2 t = raytrace(ro, rd); 141 | if (t.x > -0.5) { 142 | vec3 pos = ro + rd * t.x; 143 | vec3 nor = normal(pos); 144 | 145 | color = lighting(pos, nor, ro, rd); 146 | } 147 | 148 | gl_FragColor.rgb = color; 149 | gl_FragColor.a = 1.0; 150 | } 151 | `, 152 | advancedLighting: ` 153 | precision mediump float; 154 | 155 | uniform float iGlobalTime; 156 | uniform vec3 iResolution; 157 | 158 | vec2 doModel(vec3 p); 159 | 160 | #pragma glslify: raytrace = require('glsl-raytrace', map = doModel, steps = 90) 161 | #pragma glslify: normal = require('glsl-sdf-normal', map = doModel) 162 | #pragma glslify: orenn = require('glsl-diffuse-oren-nayar') 163 | #pragma glslify: gauss = require('glsl-specular-gaussian') 164 | #pragma glslify: camera = require('glsl-turntable-camera') 165 | #pragma glslify: noise = require('glsl-noise/simplex/4d') 166 | 167 | vec2 doModel(vec3 p) { 168 | float r = 1.0 + noise(vec4(p, iGlobalTime)) * 0.25; 169 | float d = length(p) - r; 170 | float id = 0.0; 171 | 172 | return vec2(d, id); 173 | } 174 | 175 | vec3 lighting(vec3 pos, vec3 nor, vec3 ro, vec3 rd) { 176 | vec3 dir1 = normalize(vec3(0, 1, 0)); 177 | vec3 col1 = vec3(3.0, 0.7, 0.4); 178 | vec3 dif1 = col1 * orenn(dir1, -rd, nor, 0.15, 1.0); 179 | vec3 spc1 = col1 * gauss(dir1, -rd, nor, 0.15); 180 | 181 | vec3 dir2 = normalize(vec3(0.4, -1, 0.4)); 182 | vec3 col2 = vec3(0.4, 0.8, 0.9); 183 | vec3 dif2 = col2 * orenn(dir2, -rd, nor, 0.15, 1.0); 184 | vec3 spc2 = col2 * gauss(dir2, -rd, nor, 0.15); 185 | 186 | return dif1 + spc1 + dif2 + spc2; 187 | } 188 | 189 | void main() { 190 | vec3 color = vec3(0.0); 191 | vec3 ro, rd; 192 | 193 | float rotation = iGlobalTime; 194 | float height = 2.5; 195 | float dist = 4.0; 196 | camera(rotation, height, dist, iResolution.xy, ro, rd); 197 | 198 | vec2 t = raytrace(ro, rd); 199 | if (t.x > -0.5) { 200 | vec3 pos = ro + rd * t.x; 201 | vec3 nor = normal(pos); 202 | 203 | color = lighting(pos, nor, ro, rd); 204 | } 205 | 206 | // gamma correction 207 | color = pow(color, vec3(0.5545)); 208 | 209 | gl_FragColor.rgb = color; 210 | gl_FragColor.a = 1.0; 211 | } 212 | ` 213 | } 214 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var examples = require('./examples') 2 | var debounce = require('frame-debounce') 3 | var fit = require('canvas-fit') 4 | var xhr = require('xhr') 5 | var url = require('url') 6 | 7 | var sourceId = String(window.location.pathname).slice(1).split('/') 8 | if (sourceId[0] === 's') { 9 | xhr({ 10 | uri: '/shaders/' + sourceId[1] + '.json', 11 | method: 'GET', 12 | json: true 13 | }, function(err, res, body) { 14 | if (err) throw err 15 | init(body.shader) 16 | }) 17 | } else { 18 | init(examples.blob.trim()) 19 | } 20 | 21 | function init(source) { 22 | var editor = require('./editor')(document.body, source) 23 | 24 | var canvas = document.querySelector('canvas') 25 | var display = require('./display')(canvas) 26 | var fitter = fit(canvas) 27 | var okIKnowWhatImDoing 28 | window.addEventListener('resize', debounce(fitter), false) 29 | 30 | editor.on('update', function(src) { 31 | display.update(src) 32 | }) 33 | 34 | document.querySelector('.buttons .play').addEventListener('click', e => { 35 | editor.reload() 36 | e.preventDefault() 37 | e.stopPropagation() 38 | }, false) 39 | 40 | document.querySelector('.buttons .grow').addEventListener('click', e => { 41 | e.preventDefault() 42 | e.stopPropagation() 43 | 44 | okIKnowWhatImDoing = okIKnowWhatImDoing || confirm('This will bundle all of your dependencies inline, making it harder to edit the code but easy to use it elsewhere. You can still use undo afterwards if you\'re not happy with the results, but otherwise there\'s no nice way to get back to your original code. Keep going?') 45 | if (!okIKnowWhatImDoing) return 46 | 47 | editor.update(editor.editor.getValue(), (err, result) => { 48 | if (err) throw err 49 | editor.value(result.replace('#define GLSLIFY 1', '').trim()) 50 | }) 51 | }) 52 | 53 | document.querySelector('.buttons .full').addEventListener('click', e => { 54 | toggleFullscreen() 55 | e.preventDefault() 56 | e.stopPropagation() 57 | }) 58 | 59 | document.querySelector('.buttons .auto').addEventListener('click', e => { 60 | editor.instant = !editor.instant 61 | e.preventDefault() 62 | e.stopPropagation() 63 | }) 64 | 65 | document.querySelector('.buttons .save').addEventListener('click', e => { 66 | var value = editor.editor.getValue() 67 | 68 | xhr({ 69 | uri: '/-/share', 70 | method: 'POST', 71 | json: { 72 | shader: value 73 | }, 74 | }, function(err, res, body) { 75 | if (err) throw err 76 | window.location = url.parse(body.url).pathname 77 | }) 78 | }) 79 | 80 | editor.on('fullscreen', toggleFullscreen) 81 | function toggleFullscreen() { 82 | document.body.classList.toggle('fullscreen') 83 | fitter(canvas) 84 | editor.resize() 85 | } 86 | 87 | var egSelector = document.querySelector('[name="examples"]') 88 | var szSelector = document.querySelector('[name="scale"]') 89 | var examples = require('./examples') 90 | 91 | egSelector.addEventListener('change', e => { 92 | var name = egSelector.value 93 | if(!name) return 94 | 95 | editor.value(examples[egSelector.value].trim()) 96 | }) 97 | 98 | szSelector.addEventListener('change', e => { 99 | var value = Number(szSelector.value) 100 | if (!value) return 101 | fitter.scale = value 102 | fitter() 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glslbin", 3 | "private": true, 4 | "description": "Online fragment shader editor – with glslify support!", 5 | "version": "1.1.0", 6 | "browserify": { 7 | "transform": [ 8 | "babelify", 9 | "glslify" 10 | ] 11 | }, 12 | "dependencies": { 13 | "a-big-triangle": "^1.0.0", 14 | "babelify": "^5.0.4", 15 | "bl": "^0.9.4", 16 | "browserify": "^9.0.5", 17 | "canvas-fit": "^1.3.0", 18 | "codemirror": "^5.1.0", 19 | "compression": "^1.4.1", 20 | "course": "0.0.1", 21 | "debounce": "^1.0.0", 22 | "element-size": "^1.1.1", 23 | "events": "^1.0.2", 24 | "frame-debounce": "^1.0.1", 25 | "gl-context": "^0.1.1", 26 | "gl-shader": "^4.0.0", 27 | "glslify": "^5.0.0", 28 | "glslify-bundle": "^5.0.0", 29 | "glslify-client": "^2.0.0", 30 | "glslify-deps": "^1.2.5", 31 | "glslify-detective": "^1.0.0", 32 | "glslify-resolve-remote": "^2.1.0", 33 | "inherits": "^2.0.1", 34 | "knox": "^0.9.2", 35 | "level": "^1.4.0", 36 | "mkdirp": "^0.5.0", 37 | "mousetrap": "^1.4.6", 38 | "right-now": "^1.0.0", 39 | "serve-static": "^1.9.1", 40 | "shallow-equals": "0.0.0", 41 | "uglify-js": "^2.4.17", 42 | "watchify": "^2.3.0", 43 | "xhr": "^2.0.1" 44 | }, 45 | "devDependencies": { 46 | "tape": "^3.5.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const gzip = require('compression')() 2 | const st = require('serve-static') 3 | const browserify = require('browserify') 4 | const storage = require('./storage') 5 | const uglify = require('uglify-js') 6 | const router = require('course')() 7 | const watchify = require('watchify') 8 | const mkdirp = require('mkdirp') 9 | const path = require('path') 10 | const http = require('http') 11 | const fs = require('fs') 12 | 13 | mkdirp.sync(path.join(__dirname, 'dist')) 14 | 15 | const PORT = process.env.PORT || 12421 16 | 17 | router.get(st(path.join(__dirname, 'assets'))) 18 | router.get(st(path.join(__dirname, 'dist'))) 19 | router.post('/-/source', require('./shader-string')) 20 | router.post('/-/shader', require('./shader-tree')) 21 | router.post('/-/share', require('./shader-share')) 22 | 23 | router.get('/s/:shader', function(req, res, next) { 24 | req.url = '/' 25 | router(req, res, next) 26 | }) 27 | 28 | router.get('/shaders/:shader', function(req, res, next) { 29 | if (path.extname(req.url) !== '.json') return next() 30 | 31 | this.shader = this.shader.replace(/\.json$/, '') 32 | 33 | storage.get(this.shader, function(err, data) { 34 | if (err) return next(err) 35 | 36 | res.setHeader('content-type', 'application/json') 37 | res.end(JSON.stringify(data)) 38 | }) 39 | }) 40 | 41 | http.createServer(function(req, res) { 42 | gzip(req, res, function(err) { 43 | if (err) return bail(err, req, res) 44 | 45 | router(req, res, function(err) { 46 | if (err) return bail(err, req, res) 47 | 48 | res.statusCode = 404 49 | res.setHeader('content-type', 'text/plain') 50 | res.end('404: ' + req.url) 51 | }) 52 | }) 53 | }).listen(PORT, function(err) { 54 | if (err) throw err 55 | console.log('http://localhost:'+PORT+'/') 56 | }) 57 | 58 | function bail(err, req, res) { 59 | res.statusCode = 500 60 | res.setHeader('content-type', 'text/plain') 61 | res.end([err.message, err.stack].join('\n')) 62 | 63 | console.log(err.message) 64 | console.log(err.stack) 65 | } 66 | 67 | var bundler = browserify({ 68 | entries: [path.join(__dirname, 'index.js')], 69 | packageCache: {}, 70 | cache: {}, 71 | fullPaths: true 72 | }) 73 | 74 | if (process.env.NODE_ENV !== 'production') { 75 | watchify(bundler) 76 | } 77 | 78 | bundler.on('update', update) 79 | update() 80 | 81 | function update() { 82 | console.time('built bundle.js') 83 | bundler.bundle(function(err, src) { 84 | console.timeEnd('built bundle.js') 85 | 86 | src = uglify.minify('' + src, { 87 | fromString: true, 88 | compress: true, 89 | mangle: true 90 | }).code 91 | 92 | if (err) { 93 | console.error(err.message) 94 | console.error(err.stack) 95 | return 96 | } 97 | 98 | var file = path.join(__dirname, 'dist', 'bundle.js') 99 | 100 | fs.writeFile(file, src, function(err) { 101 | if (!err) return 102 | console.error(err.message) 103 | console.error(err.stack) 104 | }) 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /shader-share.js: -------------------------------------------------------------------------------- 1 | const storage = require('./storage') 2 | const crypto = require('crypto') 3 | const bl = require('bl') 4 | 5 | module.exports = function(req, res, next) { 6 | res.setHeader('content-type', 'application/json') 7 | req.pipe(bl(function(err, data) { 8 | if (err) return next(err) 9 | 10 | data = String(data) 11 | 12 | try { 13 | data = JSON.parse(data) 14 | } catch(e) { 15 | return next(e) 16 | } 17 | 18 | var key = crypto.createHash('md5') 19 | .update([Date.now(), Math.random()].join('.')) 20 | .digest('hex') 21 | .slice(0, 8) 22 | 23 | storage.set(key, { 24 | shader: data.shader 25 | }, function(err, data) { 26 | if (err) return next(err) 27 | 28 | res.end(JSON.stringify({ 29 | url: 'http://glslb.in/s/' + key 30 | })) 31 | }) 32 | })) 33 | } 34 | -------------------------------------------------------------------------------- /shader-string.js: -------------------------------------------------------------------------------- 1 | const resolver = require('glslify-resolve-remote')({}) 2 | const bundle = require('glslify-bundle') 3 | const deps = require('glslify-deps') 4 | const bl = require('bl') 5 | 6 | module.exports = shaderString 7 | 8 | function shaderString(req, res, next) { 9 | var depper = deps({ resolve: resolver }) 10 | 11 | res.setHeader('access-control-allow-origin', '*') 12 | res.setHeader('content-type', 'text/plain') 13 | 14 | req.pipe(bl(function(err, input) { 15 | if (err) return next(err) 16 | 17 | depper.inline(input+'', '/', function(err, tree) { 18 | if (err) return next(err) 19 | 20 | try { 21 | var src = bundle(tree) 22 | } catch(e) { 23 | return next(e) 24 | } 25 | 26 | res.end(src) 27 | }) 28 | })) 29 | } 30 | -------------------------------------------------------------------------------- /shader-tree.js: -------------------------------------------------------------------------------- 1 | const resolver = require('glslify-resolve-remote')({}) 2 | const deps = require('glslify-deps') 3 | const bl = require('bl') 4 | 5 | module.exports = shaderTree 6 | 7 | function shaderTree(req, res, next) { 8 | var depper = deps({ resolve: resolver }) 9 | 10 | res.setHeader('access-control-allow-origin', '*') 11 | res.setHeader('content-type', 'application/json') 12 | 13 | req.pipe(bl(function(err, input) { 14 | if (err) return next(err) 15 | 16 | depper.inline(input+'', '/', function(err, tree) { 17 | if (err) return next(err) 18 | 19 | res.end(JSON.stringify(tree)) 20 | }) 21 | })) 22 | } 23 | -------------------------------------------------------------------------------- /storage-local.js: -------------------------------------------------------------------------------- 1 | const level = require('level') 2 | const path = require('path') 3 | 4 | module.exports = LocalStorage 5 | 6 | function LocalStorage() { 7 | if (!(this instanceof LocalStorage)) 8 | return new LocalStorage 9 | 10 | this.client = level(path.join(__dirname, '.db'), { 11 | valueEncoding: 'json' 12 | }) 13 | } 14 | 15 | LocalStorage.prototype.get = function(key, done) { 16 | this.client.get(key, done) 17 | } 18 | 19 | LocalStorage.prototype.set = function(key, data, done) { 20 | this.client.put(key, data, done) 21 | } 22 | 23 | LocalStorage.prototype.del = function(key, done) { 24 | this.client.del(key, done) 25 | } 26 | -------------------------------------------------------------------------------- /storage-s3.js: -------------------------------------------------------------------------------- 1 | const knox = require('knox') 2 | const bl = require('bl') 3 | 4 | module.exports = S3Storage 5 | 6 | function S3Storage() { 7 | if (!(this instanceof S3Storage)) 8 | return new S3Storage 9 | 10 | this.client = new knox({ 11 | key: process.env.AWS_ACCESS_KEY, 12 | secret: process.env.AWS_SECRET_KEY, 13 | bucket: 'glslbin' 14 | }) 15 | } 16 | 17 | S3Storage.prototype.get = function(key, done) { 18 | this.client.getFile(key, function(err, res) { 19 | if (err) return done(err) 20 | 21 | res.pipe(bl(function(err, data) { 22 | if (err) return done(err) 23 | if (res.statusCode !== 200) { 24 | err = new Error('Invalid status code: ' + res.statusCode + '\nOutput: ' + data) 25 | } 26 | 27 | return done(err, !err && JSON.parse(''+data)) 28 | })) 29 | }) 30 | } 31 | 32 | S3Storage.prototype.set = function(key, data, done) { 33 | var value = JSON.stringify(data) 34 | 35 | this.client.put(key, { 36 | 'Content-Length': Buffer.byteLength(value), 37 | 'Content-Type': 'application/json', 38 | 'x-amz-acl': 'public-read' 39 | }).on('response', function(res) { 40 | if (res.statusCode === 200) return done() 41 | 42 | return done(new Error( 43 | res.body || 'Failed: ' + value 44 | )) 45 | }).end(value) 46 | } 47 | 48 | S3Storage.prototype.del = function(key, done) { 49 | this.client.del(key).on('response', function(res) { 50 | if (res.statusCode === 404) return done() 51 | if (res.statusCode === 200) return done() 52 | if (res.statusCode === 204) return done() 53 | 54 | done(new Error('Failed deleting: ' + key)) 55 | }).end() 56 | } 57 | -------------------------------------------------------------------------------- /storage.js: -------------------------------------------------------------------------------- 1 | var useAmazon = !!( 2 | process.env.AWS_SECRET_KEY 3 | && process.env.AWS_ACCESS_KEY 4 | ) 5 | 6 | module.exports = useAmazon 7 | ? require('./storage-s3')() 8 | : require('./storage-local')() 9 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./storage') 2 | -------------------------------------------------------------------------------- /test/storage.js: -------------------------------------------------------------------------------- 1 | const storage = require('../storage') 2 | const test = require('tape') 3 | 4 | test('storage', function(t) { 5 | var key = '__TEST__' 6 | 7 | storage.get(key, function(err, data) { 8 | t.ok(err, 'error should be reported') 9 | 10 | storage.set(key, { hello: 'world' }, function(err) { 11 | if (err) return t.fail(err.message || err) 12 | 13 | storage.get(key, function(err, data) { 14 | if (err) return t.fail(err.message || err) 15 | 16 | t.equal(data.hello, 'world', 'includes JSON property') 17 | 18 | storage.del(key, function(err) { 19 | if (err) return t.fail(err.message || err) 20 | 21 | storage.get(key, function(err) { 22 | t.ok(err, 'file was deleted') 23 | t.end() 24 | }) 25 | }) 26 | }) 27 | }) 28 | }) 29 | }) 30 | --------------------------------------------------------------------------------