├── .gitignore ├── .DS_Store ├── favicon.ico ├── img └── cam.png ├── index.js ├── webpack.config.js ├── present.html ├── package.json ├── README.md ├── index.html ├── defaultShaders.js ├── edit.html ├── camera.js ├── lint.css ├── style.css ├── fragmentShader.js copy └── editor.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.swp 3 | .env 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharStiles/shaderplace/HEAD/.DS_Store -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharStiles/shaderplace/HEAD/favicon.ico -------------------------------------------------------------------------------- /img/cam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharStiles/shaderplace/HEAD/img/cam.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | window.onload = (event) => { 2 | var editButton = document.getElementById("editButton"); 3 | editButton.onclick = sendToEditor; 4 | var presentButton = document.getElementById("presentButton"); 5 | presentButton.onclick = sendToPresenter; 6 | } 7 | 8 | function sendToEditor() { 9 | var room = document.getElementById("room").value; 10 | window.location.href = "./edit.html?room=" + room; 11 | } 12 | 13 | function sendToPresenter() { 14 | var room = document.getElementById("room").value; 15 | window.location.href = "./present.html?room=" + room; 16 | } 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | devtool: 'source-map', 6 | entry: { 7 | codemirror: './editor.js', 8 | index: './index.js' 9 | }, 10 | output: { 11 | globalObject: 'self', 12 | path: path.resolve(__dirname, './dist/'), 13 | filename: '[name].bundle.js', 14 | publicPath: '/editor/dist/' 15 | }, 16 | devServer: { 17 | contentBase: path.join(__dirname), 18 | compress: true, 19 | inline:true, 20 | disableHostCheck: true, 21 | watchContentBase: true, 22 | publicPath: '/dist/', 23 | historyApiFallback: { 24 | index: 'index.html' 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /present.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shader.Place 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | Oh no! Your browser doesn't support canvas! 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shader.place", 3 | "version": "0.0.2", 4 | "description": "❤ a place for shader collaboration ❤", 5 | "scripts": { 6 | "dist": "webpack --mode=production --watch", 7 | "start": "webpack-dev-server --open-page index.html" 8 | }, 9 | "author": "Char Stiles ", 10 | "license": "Anti-Capitalist Software License", 11 | "dependencies": { 12 | "@codemirror/lint": "^0.18.2", 13 | "@opentok/client": "^2.19.3", 14 | "agora-rtc-sdk": "^3.5.2", 15 | "codemirror": "^5.59.1", 16 | "dotenv-webpack": "^8.0.1", 17 | "opentok": "^2.11.0", 18 | "serve": "^14.2.1", 19 | "three": "^0.126.1", 20 | "webpack": "^5.89.0", 21 | "webpack-cli": "^3.3.12", 22 | "webpack-dev-server": "^3.11.1", 23 | "y-codemirror": "^2.0.9", 24 | "y-webrtc": "^10.3.0", 25 | "yjs": "^13.4.12" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shader Place 2 | A realtime collaborative livecode GLSL editor 3 | 4 | ## Requirements 5 | - node.js 6 | 7 | ## Quickstart! 8 | ``` 9 | $ git clone https://github.com/CharStiles/shaderplace.git 10 | $ cd shaderplace 11 | $ npm i --legacy-peer-deps 12 | $ npm start 13 | ``` 14 | go to http://localhost:8080/ 15 | 16 | ## TODO: 17 | - add backend for password, more permanent rooms 18 | - WebRTC implementation 19 | 20 | **High Priority** 21 | - overall profile and analyse efficiency 22 | - undo manager 23 | - carot color 24 | - usernames to be above carot 25 | - add backbuffer texture input 26 | - add webcam texture input (for multiple users) 27 | - add bins to audio input 28 | - menu for info on globals, hot keys and meta info 29 | - hot keys for hiding code 30 | - organize code 31 | 32 | **Nice To have** 33 | - webcam mice?? 34 | - input images 35 | 36 | This has been made possible by the STUDIO for Creative Inquiry's Spring 2021 OSSTA Residencies (Open-Source Software Toolkits for the Arts) 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shader.Place 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

Welcome to Shader.Place

15 |
16 | 17 |

A real-time collaborative GLSL livecode editor

18 |

19 | 20 | 21 | 22 | 23 |

24 |

25 | To make a suggestion, report an issue, contribute or fork go to: https://github.com/CharStiles/shaderplace 26 |
27 | or email contact at charstiles.com 28 |

29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /defaultShaders.js: -------------------------------------------------------------------------------- 1 | export const _fragmentShaderC = ` 2 | #ifdef GL_ES 3 | precision mediump float; 4 | #endif 5 | 6 | uniform vec2 u_resolution; 7 | uniform float u_time; 8 | uniform float u_vol; 9 | 10 | // main is a reserved function that is going to be called first 11 | void main(void) 12 | { 13 | vec2 normCoord = gl_FragCoord.xy/u_resolution; 14 | 15 | float time = u_time/5.0; //slow down time 16 | 17 | vec2 uv = -1. + 2. * normCoord; 18 | float r = sin(time + uv.x); 19 | // x is left to right, why we see red moving from right to left think about us as a camera moving around 20 | // sin returns a number from -1 to 1, and colors are from 0 to 1, so it clips to no red half the time 21 | 22 | float g = sin(-time + uv.y * 20.); // higher frequency green stripes 23 | 24 | float b = mod(uv.x / uv.y,1.0); 25 | // when x is eual to y the colors will be brighter, mod repeats the space 26 | // mod is like a sawtooth function 27 | 28 | vec4 color = vec4(r,g,b,1); 29 | gl_FragColor = color; 30 | } 31 | 32 | `; 33 | 34 | export const _vertexShaderC = ` 35 | attribute vec2 aVertexPosition; 36 | 37 | void main() { 38 | gl_Position = vec4(aVertexPosition, 0.0, 1.0); 39 | } 40 | 41 | `; 42 | 43 | -------------------------------------------------------------------------------- /edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shader.Place 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | Oh no! Your browser doesn't support canvas! 23 | 24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /camera.js: -------------------------------------------------------------------------------- 1 | // this script is from cut-ruby.glitch.me 2 | 3 | // so good 4 | var FFT_SIZE = 512; 5 | var vol; 6 | 7 | if (window.isProduction && window.location.protocol !== "https:") { 8 | window.location = "https://" + window.location.hostname; 9 | } 10 | 11 | class Camera { 12 | constructor() { 13 | this.video = document.createElement("video"); 14 | this.video.setAttribute("muted", true); 15 | this.video.setAttribute("playsinline", false); 16 | 17 | this.selfie = false; 18 | 19 | this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 20 | } 21 | 22 | _startCapture() { 23 | return navigator.mediaDevices 24 | .getUserMedia({ 25 | audio: true, 26 | video: false, 27 | }) 28 | .then((stream) => { 29 | this.stream = stream; 30 | var source = this.audioCtx.createMediaStreamSource(stream); 31 | 32 | this.analyser = this.audioCtx.createAnalyser(); 33 | this.analyser.smoothingTimeConstant = 0.2; 34 | this.analyser.fftSize = FFT_SIZE; 35 | source.connect(this.analyser); 36 | }); 37 | } 38 | 39 | _startCaptureVid() { 40 | return navigator.mediaDevices 41 | .getUserMedia({ 42 | audio: false, 43 | video: { 44 | facingMode: this.selfie ? "user" : "environment", 45 | muted: true, 46 | }, 47 | }) 48 | .then((stream) => { 49 | this.stream = stream; 50 | this.video.srcObject = stream; 51 | 52 | this.video.play(); 53 | }); 54 | } 55 | 56 | init() { 57 | this._startCapture(); 58 | return this._startCaptureVid(); 59 | } 60 | flip() { 61 | this.selfie = !this.selfie; 62 | this._startCapture(); 63 | } 64 | } 65 | 66 | let button = document.querySelector("button"); 67 | let camera = new Camera(); 68 | document.querySelector(".vidholder").appendChild(camera.video); 69 | 70 | button.addEventListener("click", function (e) { 71 | camera 72 | .init() 73 | .then(start) 74 | .catch((e) => console.error(e)); 75 | }); 76 | 77 | (function () { 78 | camera 79 | .init() 80 | .then(start) 81 | .catch((e) => console.error(e)); 82 | })(); 83 | 84 | function start() {} -------------------------------------------------------------------------------- /lint.css: -------------------------------------------------------------------------------- 1 | /* CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | Distributed under an MIT license: https://codemirror.net/LICENSE */ 3 | 4 | /* The lint marker gutter */ 5 | .CodeMirror-lint-markers { 6 | width: 16px; 7 | } 8 | 9 | .CodeMirror-lint-tooltip { 10 | background-color: #ffd; 11 | border: 1px solid black; 12 | border-radius: 4px 4px 4px 4px; 13 | color: black; 14 | font-family: monospace; 15 | font-size: 10pt; 16 | overflow: hidden; 17 | padding: 2px 5px; 18 | position: fixed; 19 | white-space: pre; 20 | white-space: pre-wrap; 21 | z-index: 100; 22 | max-width: 600px; 23 | opacity: 0; 24 | transition: opacity .4s; 25 | -moz-transition: opacity .4s; 26 | -webkit-transition: opacity .4s; 27 | -o-transition: opacity .4s; 28 | -ms-transition: opacity .4s; 29 | } 30 | 31 | .CodeMirror-lint-mark { 32 | background-position: left bottom; 33 | background-repeat: repeat-x; 34 | } 35 | 36 | .CodeMirror-lint-mark-warning { 37 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); 38 | } 39 | 40 | .CodeMirror-lint-mark-error { 41 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg=="); 42 | } 43 | 44 | .CodeMirror-lint-marker { 45 | background-position: center center; 46 | background-repeat: no-repeat; 47 | cursor: pointer; 48 | display: inline-block; 49 | height: 16px; 50 | width: 16px; 51 | vertical-align: middle; 52 | position: relative; 53 | } 54 | 55 | .CodeMirror-lint-message { 56 | padding-left: 18px; 57 | background-position: top left; 58 | background-repeat: no-repeat; 59 | } 60 | 61 | .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { 62 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); 63 | } 64 | 65 | .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { 66 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); 67 | } 68 | 69 | .CodeMirror-lint-marker-multiple { 70 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); 71 | background-repeat: no-repeat; 72 | background-position: right bottom; 73 | width: 100%; height: 100%; 74 | } 75 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* ==================================== 2 | GLOBAL STYLES 3 | ======================================= */ 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | font-family: "Benton Sans", "Helvetica Neue", helvetica, arial, sans-serif; 11 | background-color : black; 12 | overflow: hidden; 13 | caret-color: red; 14 | } 15 | 16 | h1, 17 | h2, 18 | h3 { 19 | font-style: italic; 20 | } 21 | 22 | h1 { 23 | color: #d0ff37; 24 | } 25 | 26 | h2 { 27 | color: #dbfa6b; 28 | margin: auto; 29 | width: 50%; 30 | } 31 | 32 | h3 { 33 | color: #d9fdaa; 34 | margin: auto; 35 | width: 50%; 36 | } 37 | 38 | a { 39 | overflow-wrap: break-word; 40 | } 41 | 42 | input { 43 | width: 30%; 44 | display: block; 45 | margin: 0 auto 10px; 46 | padding: 5px; 47 | border: 1px solid lightgrey; 48 | border-radius: 3px; 49 | font-size: 16px; 50 | } 51 | 52 | button { 53 | font-size: 16px; 54 | border-radius: 3px; 55 | background-color: lightgrey; 56 | border: 1px solid grey; 57 | box-shadow: 2px 2px rgb(255, 145, 0); 58 | cursor: pointer; 59 | } 60 | 61 | button:hover { 62 | background-color: yellow; 63 | } 64 | 65 | button:active { 66 | box-shadow: none; 67 | } 68 | 69 | /* ==================================== 70 | SCROLLBAR STYLES 71 | ======================================= 72 | Only for webkit based browsers. 73 | For mozilla styles go to selector: 74 | .CodeMirror 75 | ======================================= */ 76 | 77 | ::-webkit-scrollbar { 78 | width: 11px; 79 | height: 11px; 80 | } 81 | 82 | ::-webkit-scrollbar-button { 83 | width: 0; 84 | height: 0; 85 | } 86 | 87 | ::-webkit-scrollbar-thumb { 88 | border: 1px solid #7e7e7e; 89 | border-radius: 50px; 90 | } 91 | 92 | ::-webkit-scrollbar-track { 93 | border: 0 none #000000; 94 | border-radius: 53px; 95 | } 96 | 97 | ::-webkit-scrollbar-thumb, 98 | ::-webkit-scrollbar-track, 99 | ::-webkit-scrollbar-corner { 100 | background: transparent; 101 | } 102 | 103 | /* ==================================== 104 | UTILITY/HELPER CLASSES 105 | ======================================= */ 106 | 107 | .u-txtcenter { 108 | text-align: center; 109 | } 110 | 111 | .u-displaywrapper { 112 | height: 100vh; 113 | width: 100%; 114 | } 115 | 116 | /* ==================================== 117 | COMPONENTS CLASSES 118 | ======================================= */ 119 | 120 | .wrapper { 121 | width: 100%; 122 | height: 100%; 123 | display: flex; 124 | } 125 | 126 | .editor-mode { 127 | width: 85%; 128 | max-height: 100vh; 129 | overflow-y: scroll; 130 | } 131 | 132 | /* ==================================== 133 | CodeMirror CUSTOM STYLES 134 | ======================================= */ 135 | 136 | .CodeMirror { 137 | width: 100%; 138 | height: 100%; 139 | scrollbar-color: rgba(0.5,0.5,0.5,1.0) rgba(0.5,0.5,0.5,.0); /* Mozilla styles */ 140 | } 141 | 142 | .CodeMirror-present { 143 | background-color: rgba(255, 255, 255, 0.0) !important; 144 | text-shadow: 1px 1px #555555; 145 | caret-color: red !important; 146 | position: absolute; 147 | top: 0; 148 | left: 0; 149 | } 150 | 151 | .CodeMirror-editor { 152 | /* ToDo: Change bg color in CodeMirror directly or leave it here? */ 153 | background-color: #c5c5c5; 154 | } 155 | 156 | .remote-caret { 157 | position: relative; 158 | border-left: 1px solid black; 159 | border-right: 1px solid black; 160 | margin-left: -1px; 161 | margin-right: -1px; 162 | box-sizing: border-box; 163 | } 164 | 165 | .remote-caret > div { 166 | position: absolute; 167 | top: -1.05em; 168 | left: -1px; 169 | font-size: .6em; 170 | background-color: rgb(250, 129, 0); 171 | font-family: serif; 172 | font-style: normal; 173 | font-weight: normal; 174 | line-height: normal; 175 | user-select: none; 176 | color: white; 177 | padding-left: 2px; 178 | padding-right: 2px; 179 | z-index: 3; 180 | transition: opacity .3s ease-in-out; 181 | } 182 | 183 | .remote-caret.hide-name > div { 184 | transition-delay: .7s; 185 | opacity: 0.5; 186 | } 187 | 188 | .remote-caret:hover > div { 189 | opacity: 1; 190 | transition-delay: 0s; 191 | } 192 | 193 | .cm-searching { 194 | background-color: transparent; 195 | } 196 | 197 | .CodeMirror-gutters { 198 | border-right: 1px solid #000; 199 | background-color: #000000; 200 | } 201 | 202 | .CodeMirror-present.cm-s-default .cm-comment { 203 | color: rgb(255, 255, 255); 204 | mix-blend-mode: difference; 205 | text-shadow: 0 0 5px black, 0 0 15px black; 206 | } 207 | 208 | .CodeMirror-present span[role=presentation] { 209 | background-color: rgba(255, 255, 255, 0.69); 210 | } 211 | 212 | span.cm-tab[role=presentation] { 213 | background-color: transparent; 214 | } -------------------------------------------------------------------------------- /fragmentShader.js copy: -------------------------------------------------------------------------------- 1 | 2 | var _fragmentShader = ` 3 | 4 | 5 | #ifdef GL_ES 6 | precision mediump float; 7 | #endif 8 | 9 | uniform vec2 u_resolution; 10 | uniform vec2 u_mouse; 11 | uniform float u_time; 12 | uniform vec4 u_camRot; 13 | uniform vec4 u_camQuat; 14 | uniform vec3 u_camPos; 15 | uniform float u_vol; 16 | uniform sampler2D u_feed; 17 | 18 | #define PI 3.14159265 19 | #define TAU (2*PI) 20 | #define PHI (sqrt(5)*0.5 + 0.5) 21 | // Define some constants 22 | const int steps = 128; // This is the maximum amount a ray can march. 23 | const float smallNumber = 0.001; 24 | const float maxDist = 30.; // This is the maximum distance a ray can travel. 25 | 26 | float smin( float a, float b, float k ) 27 | { 28 | float h = clamp( 0.5+0.5*(b-a)/k, 0.0, 1.0 ); 29 | return mix( b, a, h ) - k*h*(1.0-h); 30 | } 31 | vec3 rotateQuat( vec4 quat, vec3 vec ) 32 | { 33 | return vec + 2.0 * cross( cross( vec, quat.xyz ) + quat.w * vec, quat.xyz ); 34 | } 35 | float random (vec2 st) { 36 | return fract(sin(dot(st.xy, 37 | vec2(12.9898,78.233)))* 38 | 43758.5453123); 39 | } 40 | 41 | vec3 lookAt(vec2 uv, vec3 camOrigin, vec3 camTarget){ 42 | vec3 zAxis = normalize(camTarget - camOrigin); 43 | vec3 up = vec3(0,1,0); 44 | vec3 xAxis = normalize(cross(up, zAxis)); 45 | vec3 yAxis = normalize(cross(zAxis, xAxis)); 46 | 47 | float fov = 2.; 48 | 49 | vec3 dir = (normalize(uv.x * xAxis + uv.y * yAxis + zAxis * fov)); 50 | 51 | return dir; 52 | } 53 | 54 | 55 | 56 | float sphere(vec3 p) { 57 | float l = length(p) ; 58 | return l - 1.5 ; 59 | } 60 | 61 | float scene(vec3 position){ 62 | float time = u_time; 63 | float ground = position.y - sin(position.x*10.)/10. - sin((cos(time/30.) + position.z*2. -time)) / 1. - (0.+ (pow(length(position),.10)*20.0 )- 20.0) + 6.; 64 | 65 | float b = sphere(vec3( 66 | position.x, 67 | position.y, 68 | position.z - 10.) 69 | ); 70 | 71 | 72 | return smin(b,ground,0.2); 73 | } 74 | 75 | vec3 estimateNormal(vec3 p) { 76 | float smallNumber = 0.002; 77 | vec3 n = vec3( 78 | scene(vec3(p.x + smallNumber, p.yz)) - 79 | scene(vec3(p.x - smallNumber, p.yz)), 80 | scene(vec3(p.x, p.y + smallNumber, p.z)) - 81 | scene(vec3(p.x, p.y - smallNumber, p.z)), 82 | scene(vec3(p.xy, p.z + smallNumber)) - 83 | scene(vec3(p.xy, p.z - smallNumber)) 84 | ); 85 | // poke around the point to get the line perpandicular 86 | // to the surface at p, a point in space. 87 | return normalize(n); 88 | } 89 | vec3 hsv2rgb(vec3 c) { 90 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 91 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 92 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 93 | } 94 | 95 | vec4 lighting2(vec3 pos, vec3 viewDir){ 96 | vec3 lightPos = vec3(cos(u_time)*0.23,0.3,sin(u_time)*0.2); 97 | // light moves left to right 98 | vec3 normal = estimateNormal(pos); 99 | vec3 reflectDir = reflect(-lightPos, normal); 100 | 101 | float specularStrength =5500.; 102 | vec3 specColor = hsv2rgb( vec3((((pos.y)*tan(pos.x)*10.)/normal.z )/700.,normal.x,.041)); 103 | float spec = pow( max(dot(viewDir, reflectDir), -0.80), 6.); 104 | vec3 specular = specularStrength * spec * specColor; 105 | return vec4(specular,1.); 106 | 107 | } 108 | vec4 trace (vec3 origin, vec3 direction){ 109 | 110 | float dist = 0.; 111 | float totalDistance = 0.; 112 | vec3 positionOnRay = origin; 113 | 114 | for(int i = 0 ; i < steps; i++){ 115 | 116 | dist = scene(positionOnRay); 117 | 118 | // Advance along the ray trajectory the amount that we know the ray 119 | // can travel without going through an object. 120 | positionOnRay += dist * direction + random(positionOnRay.xy)*0.1; 121 | 122 | // Total distance is keeping track of how much the ray has traveled 123 | // thus far. 124 | totalDistance += dist; 125 | 126 | // If we hit an object or are close enough to an object, 127 | if (dist < smallNumber){ 128 | // return the distance the ray had to travel normalized so be white 129 | // at the front and black in the back. 130 | return lighting2(positionOnRay, direction);//1. - (vec4(totalDistance) / maxDist); 131 | 132 | } 133 | 134 | if (totalDistance > maxDist){ 135 | 136 | return texture2D(u_feed, gl_FragCoord.xy/u_resolution); // Background color. 137 | } 138 | } 139 | 140 | return texture2D(u_feed, gl_FragCoord.xy/u_resolution); 141 | } 142 | 143 | // main is a reserved function that is going to be called first 144 | void main(void) 145 | { 146 | vec2 normCoord = gl_FragCoord.xy/u_resolution; 147 | 148 | vec2 uv = -1. + 2. * normCoord; 149 | // Unfortunately our screens are not square so we must account for that. 150 | uv.x *= (u_resolution.x / u_resolution.y); 151 | 152 | vec3 rayOrigin = vec3(uv, 0.); 153 | vec3 camOrigin = u_camPos; //vec3(0., 0., -1.); 154 | 155 | 156 | 157 | vec3 zAxis = vec3(0,0,1); 158 | vec3 up = vec3(0,1,0); 159 | vec3 xAxis = normalize(cross(up, zAxis)); 160 | vec3 yAxis = normalize(cross(zAxis, xAxis)); 161 | 162 | // we need to apply rotate 3 times each with rotation on the relative object, 163 | // then we can get the lookat direction that we need. SO lets start with looking at forward 164 | 165 | vec3 dirToLook = zAxis;//normalize(camOrigin + rayOrigin); 166 | dirToLook = rotateQuat(u_camQuat,dirToLook); 167 | 168 | 169 | // according to 3js docs Default order is 'XYZ' 170 | 171 | vec3 dir = lookAt(uv, camOrigin, dirToLook); 172 | 173 | 174 | gl_FragColor = max(trace(camOrigin, dir),0.2); 175 | } 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | `; 209 | -------------------------------------------------------------------------------- /editor.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | // @ts-ignore 4 | import CodeMirror from "codemirror"; 5 | import * as Y from "yjs"; 6 | import { WebsocketProvider } from "y-websocket"; 7 | //import { WebrtcProvider } from 'y-webrtc' 8 | import { CodemirrorBinding } from "y-codemirror"; 9 | import "codemirror/mode/clike/clike.js"; 10 | import 'codemirror/addon/lint/lint'; 11 | import {_fragmentShaderC, _vertexShaderC} from "./defaultShaders.js"; 12 | 13 | // Element storage 14 | var gl; 15 | var editor; 16 | let glCanvas = null; 17 | let _fragmentShader = _fragmentShaderC; 18 | 19 | // Current state storage 20 | var isDirty = false; 21 | let shaderProgram; 22 | 23 | // Aspect ratio and coordinate system 24 | // details 25 | let aspectRatio; 26 | let resolution; 27 | 28 | // Vertex information 29 | let vertexArray; 30 | let vertexBuffer; 31 | let vertexNumComponents; 32 | let vertexCount; 33 | 34 | // Rendering data shared with the 35 | // scalers. 36 | let uResolution; 37 | let uTime; 38 | let uVol; 39 | let aVertexPosition; 40 | 41 | // Animation timing 42 | let previousTime = 0.0; 43 | // this script is from cut-ruby.glitch.me 44 | 45 | // so good 46 | var FFT_SIZE = 512; 47 | var vol; 48 | 49 | if (window.isProduction && window.location.protocol !== "https:") { 50 | window.location = "https://" + window.location.hostname; 51 | } 52 | 53 | class Camera { 54 | constructor() { 55 | this.video = document.createElement("video"); 56 | this.video.setAttribute("muted", true); 57 | this.video.setAttribute("playsinline", false); 58 | 59 | this.selfie = false; 60 | 61 | this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 62 | } 63 | 64 | _startCapture() { 65 | return navigator.mediaDevices 66 | .getUserMedia({ 67 | audio: true, 68 | video: false, 69 | }) 70 | .then((stream) => { 71 | this.stream = stream; 72 | var source = this.audioCtx.createMediaStreamSource(stream); 73 | 74 | this.analyser = this.audioCtx.createAnalyser(); 75 | this.analyser.smoothingTimeConstant = 0.9; 76 | this.analyser.fftSize = FFT_SIZE; 77 | source.connect(this.analyser); 78 | }); 79 | } 80 | init() { 81 | this._startCapture(); 82 | return this._startCapture(); 83 | } 84 | flip() { 85 | this.selfie = !this.selfie; 86 | this._startCapture(); 87 | } 88 | } 89 | 90 | let button = document.querySelector("button"); 91 | let camera;// new Camera(); 92 | //document.querySelector("body").appendChild(camera.video); 93 | 94 | // document.addEventListener("click", function (e) { 95 | // camera 96 | // .init() 97 | // .then(start) 98 | // .catch((e) => console.error(e)); 99 | // }); 100 | 101 | // (function () { 102 | // camera 103 | // .init() 104 | // .then(start) 105 | // .catch((e) => console.error(e)); 106 | // })(); 107 | 108 | function start() {} 109 | 110 | 111 | 112 | // from here https://hackernoon.com/creative-coding-using-the-microphone-to-make-sound-reactive-art-part1-164fd3d972f3 113 | // A more accurate way to get overall volume 114 | function getRMS(spectrum) { 115 | var rms = 0; 116 | for (var i = 0; i < spectrum.length; i++) { 117 | rms += spectrum[i] * spectrum[i]; 118 | } 119 | rms /= spectrum.length; 120 | rms = Math.sqrt(rms); 121 | let norm = rms / 128; 122 | return (norm - 0.99) * 100; 123 | } 124 | 125 | function isInPresentationMode() { 126 | if (window.location.pathname.split('/').pop() == 'present.html') { 127 | return true; 128 | } 129 | return false; 130 | } 131 | 132 | function isLockedPresent(){ 133 | const queryString = window.location.search; 134 | console.log(queryString); 135 | const urlParams = new URLSearchParams(queryString); 136 | const pw = urlParams.get('pw') 137 | const room = urlParams.get('room') 138 | console.log(room) 139 | // if you hack into my classroom I will cry in front of everyone :,( 140 | if(pw != "WhyDoesDotEnvEvadeMe" && room.toLowerCase()== "classroom"){ 141 | return true; 142 | console.log("youre a student, in student mode") 143 | } 144 | 145 | return false; 146 | } 147 | 148 | function addCodeMirrorPresentModifier() { 149 | const codeMirrorDiv = document.querySelector(".CodeMirror"); 150 | if (codeMirrorDiv) { 151 | codeMirrorDiv.classList.add("CodeMirror-present"); 152 | } 153 | } 154 | 155 | function addCodeMirrorEditorModifier() { 156 | const codeMirrorDiv = document.querySelector(".CodeMirror"); 157 | if (codeMirrorDiv) codeMirrorDiv.classList.add("CodeMirror-editor"); 158 | } 159 | 160 | function initYdoc() { 161 | const ydoc = new Y.Doc(); 162 | 163 | const searchParams = new URLSearchParams(window.location.search); 164 | var room = ""; 165 | if (searchParams.has("room")){ 166 | room = searchParams.get("room"); 167 | } 168 | 169 | const provider = new WebsocketProvider( 170 | `ws${location.protocol.slice(4)}//${location.host}/ws`, 171 | room, 172 | ydoc 173 | ); 174 | // const provider = new WebsocketProvider('wss://localhost:8080', room, ydoc, { WebSocketPolyfill: require('ws') }) 175 | 176 | var editorContainer = document.getElementById("editor"); 177 | editor = CodeMirror(editorContainer, { 178 | value: _fragmentShader, 179 | lineNumbers: true, 180 | mode: "x-shader/x-vertex", 181 | gutters: ["CodeMirror-lint-markers"], 182 | lint: true, 183 | lineWrapping: !isInPresentationMode(), 184 | readOnly: isLockedPresent() 185 | //editable: false //!isLockedPresent() 186 | }); 187 | 188 | const ytext = ydoc.getText("codemirror"); 189 | // const undoManager = new Y.UndoManager(ytext, { trackedOrigins: new Set([ydoc.clientID]) }) 190 | const binding = new CodemirrorBinding(ytext, editor, provider.awareness); 191 | const setDefaultVal = () => { 192 | if (ytext.toString() === "") { 193 | ytext.insert(0, _fragmentShader); 194 | } 195 | }; 196 | 197 | 198 | if (provider.sync) { 199 | setDefaultVal(); 200 | } else { 201 | provider.on("sync", setDefaultVal); 202 | } 203 | 204 | setDefaultVal(); 205 | // editor.getDoc().markText( 206 | // { 207 | // line: 5, 208 | // ch: 1 209 | // }, 210 | // { 211 | // line: 50, 212 | // ch: 3 213 | // }, 214 | // { 215 | // css: "color : red" 216 | // } 217 | // ); 218 | 219 | if (isInPresentationMode()) { 220 | addCodeMirrorPresentModifier(); 221 | } else { 222 | addCodeMirrorEditorModifier(); 223 | } 224 | 225 | // @ts-ignore 226 | window.example = { provider, ydoc, ytext, binding, Y }; 227 | } 228 | 229 | 230 | // this function will trigger a change to the editor 231 | function onEdit() { 232 | const fragmentCode = editor.getValue(); 233 | updateShader(fragmentCode); 234 | } 235 | 236 | function updateShader(fragmentCode) { 237 | if (!checkFragmentShader(fragmentCode)) { 238 | return; 239 | } 240 | 241 | _fragmentShader = fragmentCode; 242 | 243 | isDirty = true; 244 | } 245 | 246 | window.onload = (event) => { 247 | webgl_startup(); 248 | initYdoc(); 249 | } 250 | 251 | function animateScene() { 252 | gl.viewport(0, 0, glCanvas.width, glCanvas.height); 253 | // This sets background color 254 | gl.clearColor(1, 1, 1, 1); 255 | gl.clear(gl.COLOR_BUFFER_BIT); 256 | 257 | gl.useProgram(shaderProgram); 258 | 259 | uResolution = 260 | gl.getUniformLocation(shaderProgram, "u_resolution"); 261 | uTime = 262 | gl.getUniformLocation(shaderProgram, "u_time"); 263 | uVol = 264 | gl.getUniformLocation(shaderProgram, "u_vol"); 265 | 266 | gl.uniform2fv(uResolution, resolution); 267 | gl.uniform1f(uTime, previousTime); 268 | if (camera && camera.analyser) { 269 | var bufferLength = camera.analyser.frequencyBinCount; 270 | var dataArray = new Uint8Array(bufferLength); 271 | 272 | camera.analyser.getByteTimeDomainData(dataArray); 273 | gl.uniform1f(uVol, getRMS(dataArray)); 274 | } 275 | else{ 276 | gl.uniform1f(uVol, 0.0); 277 | } 278 | 279 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); 280 | 281 | aVertexPosition = 282 | gl.getAttribLocation(shaderProgram, "aVertexPosition"); 283 | 284 | gl.enableVertexAttribArray(aVertexPosition); 285 | gl.vertexAttribPointer(aVertexPosition, vertexNumComponents, 286 | gl.FLOAT, false, 0, 0); 287 | 288 | gl.drawArrays(gl.TRIANGLES, 0, vertexCount); 289 | 290 | window.requestAnimationFrame(function(currentTime) { 291 | previousTime = previousTime + .05; 292 | // TODO here check dirty bit and recompile? 293 | if (isDirty) { 294 | // recompile and clear dirty bit 295 | shaderProgram = buildShaderProgram(); 296 | isDirty = false; 297 | } 298 | animateScene(); 299 | }); 300 | } 301 | 302 | function compileShader(type, code) { 303 | let shader = gl.createShader(type); 304 | 305 | gl.shaderSource(shader, code); 306 | gl.compileShader(shader); 307 | 308 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 309 | console.log(`Error compiling ${type === gl.VERTEX_SHADER ? "vertex" : "fragment"} shader:`); 310 | console.log(gl.getShaderInfoLog(shader)); 311 | } 312 | return shader; 313 | } 314 | 315 | 316 | function buildShaderProgram() { 317 | let program = gl.createProgram(); 318 | 319 | // Compile vertex shader 320 | let shader = compileShader(gl.VERTEX_SHADER, vertexShader()); 321 | gl.attachShader(program, shader); 322 | 323 | // Compile fragment shader 324 | shader = compileShader(gl.FRAGMENT_SHADER, fragmentShader()); 325 | gl.attachShader(program, shader); 326 | 327 | gl.linkProgram(program) 328 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 329 | console.log("Error linking shader program:"); 330 | console.log(gl.getProgramInfoLog(program)); 331 | } 332 | 333 | return program; 334 | } 335 | 336 | function webgl_startup() { 337 | glCanvas = document.getElementById("glcanvas"); 338 | if (glCanvas.width != glCanvas.clientWidth) { 339 | glCanvas.width = glCanvas.clientWidth; 340 | } 341 | if (glCanvas.height != glCanvas.clientHeight) { 342 | glCanvas.height = glCanvas.clientHeight; 343 | } 344 | gl = glCanvas.getContext("webgl"); 345 | 346 | shaderProgram = buildShaderProgram(); 347 | 348 | aspectRatio = glCanvas.width/glCanvas.height; 349 | resolution = [glCanvas.width, glCanvas.height]; 350 | 351 | vertexArray = new Float32Array([ 352 | -1, 1, 353 | 1, 1, 354 | 1, -1, 355 | -1, 1, 356 | 1, -1, 357 | -1, -1 358 | ]); 359 | 360 | vertexBuffer = gl.createBuffer(); 361 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); 362 | gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW); 363 | 364 | vertexNumComponents = 2; 365 | vertexCount = vertexArray.length/vertexNumComponents; 366 | 367 | animateScene(); 368 | } 369 | 370 | 371 | function vertexShader() { 372 | return _vertexShaderC; 373 | } 374 | 375 | function fragmentShader() { 376 | return _fragmentShader; 377 | } 378 | 379 | // this returns false if the fragment shader cannot compile 380 | // true if it can 381 | function checkFragmentShader(shaderCode, lint = false) { 382 | if (!gl) { 383 | return; 384 | } 385 | let shader = gl.createShader(gl.FRAGMENT_SHADER); 386 | gl.shaderSource(shader, shaderCode); 387 | gl.compileShader(shader); 388 | let infoLog = gl.getShaderInfoLog(shader); 389 | let result = gl.getShaderParameter(shader, gl.COMPILE_STATUS); 390 | let ret = []; 391 | if (!result) { 392 | console.log(infoLog); 393 | var errors = infoLog.split(/\r|\n/); 394 | for (let error of errors){ 395 | var splitResult = error.split(":") 396 | ret.push( { 397 | message: splitResult[3] + splitResult[4], 398 | character: splitResult[1], 399 | line: splitResult[2] 400 | }) 401 | } 402 | } 403 | 404 | if (result) { 405 | console.log("did update"); 406 | _fragmentShader = shaderCode; 407 | isDirty = true; 408 | } 409 | 410 | return ret; 411 | } 412 | 413 | 414 | (function(mod) { 415 | mod(CodeMirror); 416 | })(function(CodeMirror) { 417 | "use strict"; 418 | 419 | function validator(text, options) { 420 | var result = []; 421 | var errors = checkFragmentShader(text, true); 422 | if (errors) parseErrors(errors, result); 423 | return result; 424 | } 425 | 426 | CodeMirror.registerHelper("lint", "x-shader/x-vertex", validator); 427 | 428 | function parseErrors(errors, output) { 429 | for ( var i = 0; i < errors.length; i++) { 430 | var error = errors[i]; 431 | if (error) { 432 | if (Number(error.line) <= 0) { 433 | console.warn("Cannot display error (invalid line " + error.line + ")", error); 434 | continue; 435 | } 436 | 437 | var start = error.character - 1, end = start + 1; 438 | 439 | 440 | // Convert to format expected by validation service 441 | var hint = { 442 | message: error.message, 443 | severity: "error", 444 | from: CodeMirror.Pos(Number(error.line) - 1, start), 445 | to: CodeMirror.Pos(Number(error.line) - 1, end) 446 | }; 447 | 448 | output.push(hint); 449 | } 450 | } 451 | } 452 | }); 453 | --------------------------------------------------------------------------------