UNPKG

marching

Version:

Marching.js is a JavaScript library that compiles GLSL ray marchers.

876 lines (725 loc) 27 kB
const CodeMirror = require( 'codemirror' ), Toastr = require( 'toastr' ) require( '../node_modules/codemirror/mode/javascript/javascript.js' ) require( '../node_modules/codemirror/addon/edit/matchbrackets.js' ) require( '../node_modules/codemirror/addon/edit/closebrackets.js' ) require( '../node_modules/codemirror/addon/hint/show-hint.js' ) require( '../node_modules/codemirror/addon/hint/javascript-hint.js' ) require( '../node_modules/codemirror/addon/display/fullscreen.js' ) require( '../node_modules/codemirror/addon/selection/active-line.js' ) require( '../node_modules/codemirror/addon/display/panel.js' ) require( '../node_modules/mousetrap/mousetrap.min.js' ) const Tweakpane = require('tweakpane') const demos = { introduction: require( './demos/intro.js' ), ['textured transformations']: require('./demos/texture_transforms.js'), //['the superformula']: require('./demos/superformula.js' ), ['mandelbulb fractal']: require( './demos/mandelbulb.js' ), ['julia fractal']: require( './demos/julia.js' ), ['voxels and edges']: require( './demos/voxels_and_edges.js' ), ['alien portal']: require( './demos/alien_portal.js' ), ["i've got a light inside of me"]: require( './demos/let_it_shine.js' ), ['snare and sticks']: require( './demos/snare.js' ), //['kaleidoscopic fractals']: require( './demos/kifs.js' ), //['alien portal #2']: require( './demos/portal2.js' ), //['twist deformation']: require( './demos/twist.js' ), ['geometry catalog']: require( './demos/geometries.js' ), ['textures catalog']: require( './demos/textures.js' ), } const tutorials = { ['start here']: require( './demos/tutorial_1.js' ), ['constructive solid geometry']: require( './demos/csg.js' ), ['lighting and materials']: require( './demos/lighting.js' ), ['texturing']: require( './demos/textures_tutorial.js' ), ['texturing with hydra']: require( './demos/hydra.js' ), ['texturing with p5.js']: require( './demos/p5.js' ), ['post-processing effects']: require( './demos/postprocessing.js' ), ['exporting gifs and links']: require( './demos/gifs_and_links.js' ), ['audio input / fft']: require( './demos/audio.js' ), ['live coding']: require( './demos/livecoding.js' ), ['defining your own GLSL shapes']: require( './demos/constructors.js' ), ['defining procedural textures']: require( './demos/procedural_textures.js' ) } Math.export = ()=> { const arr = Object.getOwnPropertyNames( Math ) arr.forEach( el => window[el] = Math[el] ) } window.onload = function() { const ta = document.querySelector( '#cm' ) const SDF = window.Marching SDF.init( document.querySelector('canvas') ) SDF.export( window ) SDF.keys = { w:0, a:0, s:0, d:0, alt:0 } Math.export() SDF.useProxies = false let hidden = false let fontSize = .95 SDF.cameraEnabled = false //document.querySelector('#cameratoggle').onclick = e => { // SDF.cameraEnabled = e.target.checked //} CodeMirror.keyMap.playground = { fallthrough:'default', 'Ctrl-Enter'( cm ) { try { const selectedCode = getSelectionCodeColumn( cm, false ) flash( cm, selectedCode.selection ) const code = selectedCode.code const func = new Function( code ) if( SDF.useProxies === true ) { const preWindowMembers = Object.keys( window ) func() const postWindowMembers = Object.keys( window ) if( preWindowMembers.length !== postWindowMembers.length ) { createProxies( preWindowMembers, postWindowMembers, window ) } }else{ func() } } catch (e) { console.log( e ) } }, 'Shift-Ctrl-H'() { toggleGUI() }, 'Shift-Ctrl-G'() { toggleGUI() }, 'Shift-Ctrl-C'() { toggleCamera() }, 'Alt-W'( cm ) { SDF.keys.w = 1 }, 'Alt-A'( cm ) { SDF.keys.a = 1 }, 'Alt-S'( cm ) { SDF.keys.s = 1 }, 'Alt-D'( cm ) { SDF.keys.d = 1 }, 'Alt-Enter'( cm ) { try { var selectedCode = getSelectionCodeColumn( cm, true ) flash( cm, selectedCode.selection ) var code = selectedCode.code var func = new Function( code ) if( SDF.useProxies === true ) { const preWindowMembers = Object.keys( window ) func() const postWindowMembers = Object.keys( window ) if( preWindowMembers.length !== postWindowMembers.length ) { createProxies( preWindowMembers, postWindowMembers, window ) } }else{ func() } } catch (e) { console.log( e ) } }, 'Ctrl-.'( cm ) { SDF.clear() guis.forEach( g => { if( g.containerElem_ !== null ) { g.dispose() } } ) guis.length = 0 proxies.length = 0 }, "Shift-Ctrl-=": function(cm) { fontSize += .1 document.querySelector('.CodeMirror-lines').style.fontSize= fontSize + 'em' cm.refresh() }, "Shift-Ctrl--": function(cm) { fontSize -= .1 document.querySelector('.CodeMirror-lines').style.fontSize = fontSize + 'em' cm.refresh() } } const toggleToolbar = function() { if( hidden === false ) { document.querySelector('select').style.display = 'none' document.querySelector('#help').style.display = 'none' document.querySelector('#source').style.display = 'none' }else{ document.querySelector('select').style.display = 'inline-block' document.querySelector('#help').style.display = 'inline-block' document.querySelector('#source').style.display = 'inline-block' } } const toggleGUI = function() { if( hidden === false ) { cm.getWrapperElement().style.display = 'none' toggleToolbar() }else{ cm.getWrapperElement().style.display = 'block' toggleToolbar() } hidden = !hidden } window.toggleCamera = function( shouldToggleGUI=true) { Marching.cameraEnabled = !Marching.cameraEnabled //document.querySelector('#cameratoggle').checked = Marching.cameraEnabled if( shouldToggleGUI ) toggleGUI() Marching.camera.on() } // have to bind to window for when editor is hidden Mousetrap.bind('ctrl+shift+g', toggleGUI ) Mousetrap.bind('ctrl+shift+c', e => { toggleCamera() }) delete CodeMirror.keyMap.default[ 'Ctrl-H' ] const cm = CodeMirror( document.body, { value:demos.introduction, mode:'javascript', fullScreen:true, keyMap:'playground', styleActiveLine:true, autofocus:true, matchBrackets:true, autoCloseBrackets:true, }) cm.setOption('fullScreen', true ) cm.on('keyup', (cm, event) => { if( SDF.cameraEnabled ) { const code = event.key//.code.slice(3).toLowerCase() SDF.keys[ code ] = 0 }else if( event.key === 'Alt' ) { for( let key in SDF.keys ) { SDF.keys[ key ] = 0 } } }) cm.on('keydown', (cm,event) => { if( SDF.cameraEnabled ) { SDF.keys[ event.key ] = 1 event.codemirrorIgnore = 1 event.preventDefault() } }) delete CodeMirror.keyMap.default[ 'Ctrl-H' ] window.addEventListener( 'keydown', e => { if( e.key === 'h' && e.ctrlKey === true ) { toggleGUI() }else if( e.key === '.' && e.ctrlKey === true && e.shiftKey === true ) { SDF.pause() }else if( SDF.cameraEnabled ) { SDF.keys[ e.key ] = 1 } }) window.addEventListener( 'keyup', e => { if( SDF.cameraEnabled ) { SDF.keys[ e.key ] = 0 } }) const sel = document.querySelector('select') const demoGroup = document.createElement('optgroup') demoGroup.setAttribute( 'label', '----- demos -----' ) const tutorialGroup = document.createElement('optgroup') tutorialGroup.setAttribute( 'label', '----- tutorials -----' ) for( let key in demos ) { const opt = document.createElement( 'option' ) opt.innerText = key demoGroup.appendChild( opt ) } sel.appendChild( demoGroup ) for( let key in tutorials ) { const opt = document.createElement( 'option' ) opt.innerText = key tutorialGroup.appendChild( opt ) } sel.appendChild( tutorialGroup ) sel.onchange = e => { let isDemo = true code = demos[ e.target.selectedOptions[0].innerText ] if( code === undefined ) { isDemo = false code = tutorials[ e.target.selectedOptions[0].innerText ] } SDF.clear() window.onframe = null if( isDemo === true ) { eval( code ) } cm.setValue( code ) } var getSelectionCodeColumn = function( cm, findBlock ) { var pos = cm.getCursor(), text = null if( !findBlock ) { text = cm.getDoc().getSelection() if ( text === "") { text = cm.getLine( pos.line ) }else{ pos = { start: cm.getCursor('start'), end: cm.getCursor('end') } //pos = null } }else{ var startline = pos.line, endline = pos.line, pos1, pos2, sel while ( startline > 0 && cm.getLine( startline ) !== "" ) { startline-- } while ( endline < cm.lineCount() && cm.getLine( endline ) !== "" ) { endline++ } pos1 = { line: startline, ch: 0 } pos2 = { line: endline, ch: 0 } text = cm.getRange( pos1, pos2 ) pos = { start: pos1, end: pos2 } } if( pos.start === undefined ) { var lineNumber = pos.line, start = 0, end = text.length pos = { start:{ line:lineNumber, ch:start }, end:{ line:lineNumber, ch: end } } } return { selection: pos, code: text } } const flash = function(cm, pos) { let sel const cb = function() { sel.clear() } if (pos !== null) { if( pos.start ) { // if called from a findBlock keymap sel = cm.markText( pos.start, pos.end, { className:"CodeMirror-highlight" } ); }else{ // called with single line sel = cm.markText( { line: pos.line, ch:0 }, { line: pos.line, ch:null }, { className: "CodeMirror-highlight" } ) } }else{ // called with selected block sel = cm.markText( cm.getCursor(true), cm.getCursor(false), { className: "CodeMirror-highlight" } ); } window.setTimeout( cb, 250 ) } let __mute = false window.mute = function() { __mute = !__mute } const msg = function( msg, title ) { if( __mute === false ) { Toastr.options = { "closeButton": false, "debug": false, "newestOnTop": false, "progressBar": false, "positionClass": "toast-bottom-center", "preventDuplicates": false, "onclick": null, "showDuration": "300", "hideDuration": "1000", "timeOut": "4000", "extendedTimeOut": "1000", "showEasing": "swing", "hideEasing": "linear", "showMethod": "fadeIn", "hideMethod": "fadeOut" } Toastr.info( msg, title ) } } //https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js const libs = {} window.use = function( ...libs ) { if( libs.length === 1 ) { return window.__use( libs[0] ) }else{ return Promise.all( libs.map( l => window.__use( l ) ) ) } } window.__use = function( lib ) { const p = new Promise( (res,rej) => { if( lib === 'dwitter' ) { window.Dwitter = function( draw, __props ) { const props = Object.assign( {}, { width:1920, height:1080, scale:1, mirror:Texture.repeat }, __props ) const tex = Texture( 'canvas', props ) window.c = tex.canvas window.x = tex.ctx window.C = Math.cos window.S = Math.sin window.T = Math.tan window.R = (r, g, b, a = 1) => `rgba(${r | 0},${g | 0},${b | 0},${a})`; Marching.postrendercallbacks.push( t => { draw( t ); tex.update() }) return tex } res() } else if( lib.indexOf('http') > -1 ) { const p5script = document.createElement( 'script' ) p5script.src = lib document.querySelector( 'head' ).appendChild( p5script ) p5script.onload = function() { msg( `${lib} has been loaded.`, 'new module loaded' ) res() } }else if( lib === 'p5' ) { if( libs.P5 !== undefined ) { res( libs.P5 ); return } const p5script = document.createElement( 'script' ) p5script.src = 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js' document.querySelector( 'head' ).appendChild( p5script ) p5script.onload = function() { msg( 'p5 is ready to texture', 'new module loaded' ) const __p5 = p5 window.P5 = function( w=500,h=500 ) { const canvas = document.createElement('div') canvas.setAttribute('id','container') canvas.width = w canvas.height = h const sketch = function(p) { p.setup = function(){ p.createCanvas( w,h ) p.background(0) } } const p = new p5( sketch, 'container' ) p.texture = ()=> { const t = Texture('canvas', { canvas:p.canvas }) Marching.postrendercallbacks.push( ()=> t.update() ) p.texture = t return t } return p } libs.P5 = p5 res( p5 ) } } else if( lib === 'hydra' ) { if( libs.Hydra !== undefined ) { res( libs.Hydra ); return } const hydrascript = document.createElement( 'script' ) hydrascript.src = 'https://cdn.jsdelivr.net/npm/hydra-synth@1.3.0/dist/hydra-synth.js' document.querySelector( 'head' ).appendChild( hydrascript ) hydrascript.onload = function() { msg( 'hydra is ready to texture', 'new module loaded' ) const Hydrasynth = Hydra let __hydra = null window.Hydra = function( w=500,h=500 ) { const canvas = document.createElement('canvas') canvas.width = w canvas.height = h const hydra = __hydra === null ? new Hydrasynth({ canvas, global:false, detectAudio:false }) : __hydra //hydra.setResolution(w,h) if( __hydra === null ) { hydra.synth.canvas = canvas } hydra.synth.texture = ()=> { const t = Texture('canvas', { canvas:hydra.synth.canvas }) Marching.postrendercallbacks.push( ()=> t.update() ) hydra.synth.texture = t return t } __hydra = hydra return hydra.synth } libs.Hydra = Hydra res( Hydra ) } } else if( lib === 'gif' ){ if( libs.GIF !== undefined ) { res( libs.GIF ); return } // first load actual script const script = document.createElement( 'script' ) script.src = 'https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js' document.querySelector( 'head' ).appendChild( script ) const p1 = new Promise( (_res, rej) => { script.onload = _res }) // then load worker as text to create blob // since you can't pass link to non-origin for worker const p2 = fetch( 'https://cdn.jsdelivr.net/gh/jnordberg/gif.js/dist/gif.worker.js' ) .then( t => t.text() ) Promise.all([ p1, p2 ]).then( values => { msg( 'gifs can now be exported', 'new module loaded' ) const workerBlob = new Blob([ values[1] ]) const workerURL = window.URL.createObjectURL( workerBlob ) Marching.Scene.prototype.gif = function( width=600, height=335, length=60, quality=5, delay=17, filename='marching.gif' ) { const gif = new GIF({ quality, width, height, workers:2, workerScript: workerURL }) this.setdim( width,height ) let framecount = 0 Marching.postrendercallbacks.push( ()=> { gif.addFrame( Marching.canvas, { copy:true, delay }) if( framecount++ === length-1 ) { gif.render() } }) let finished = false gif.on( 'finished', blob => { // prevent chrome double finish event if( finished === false ) { const link = window.document.createElement('a') link.href = URL.createObjectURL( blob ) link.download = filename var click = new MouseEvent('click', { 'view': window, 'bubbles': true, 'cancelable': true }); link.dispatchEvent(click) finished = true msg( '', 'download complete' ) } }) return this } libs.GIF = GIF res( GIF ) }) } }) return p } const ease = t => t < .5 ? 2*t*t : -1+(4-2*t)*t window.fade = ( objname, propname, target, seconds ) => { const objsplit = objname.indexOf('.') === -1 ? null : objname.split('.') const split = propname.indexOf('.') === -1 ? null : propname.split('.') const startValue = [], diff = [] const inc = 1 / ( seconds * 60 ) let obj if( objsplit === null ) { obj = window[ objname ] }else{ obj = window objsplit.forEach( v => obj = obj[ v ] ) } const isVec = split === null && obj[ propname ].type.indexOf( 'vec' ) !== -1 let vecCount = isVec === true ? parseInt( obj[ propname ].type.slice(3) ) : null let t = 0 if( isVec ) { startValue[0] = obj[ propname ].x startValue[1] = obj[ propname ].y if( vecCount > 2 ) startValue[2] = obj[ propname ].z diff[0] = target - startValue[0] diff[1] = target - startValue[1] if( vecCount > 2 ) diff[2] = target - startValue[2] }else{ if( split === null ) { startValue[0] = obj[ propname ].value }else{ for( let i = 0; i < split.length; i++ ) { //split.forEach( (v,i) => obj = isVec ? : obj[ v ] ) obj = obj[ split[ i ] ] } startValue[0] = obj } diff[ 0 ] = target - startValue[ 0 ] } const fnc = () => { const easeValue = ease( t ) if( split === null ) { if( isVec === false ) { obj[ propname ] = startValue[0] + easeValue * diff[0] }else{ obj[ propname ].x = startValue[0] + easeValue * diff[0] obj[ propname ].y = startValue[1] + easeValue * diff[1] if( vecCount > 2 ) { obj[ propname ].z = startValue[2] + easeValue * diff[2] } } }else{ for( let i = 0; i < split.length - 1; i++ ) { //split.forEach( (v,i) => obj = isVec ? : obj[ v ] ) obj = obj[ split[ i ] ] } obj[ split[ split.length - 1 ] ]= startValue[0] + easeValue * diff[0] } t += inc if( t >= 1 ) { if( split !== null ) { obj[ split[ split.length - 1 ] ] = target }else{ obj[ propname ] = target } fnc.cancel() } } callbacks.push( fnc ) fnc.cancel = ()=> { const idx = callbacks.indexOf( fnc ) callbacks.splice( idx, 1 ) } return fnc } const proxies = [] const createProxies = function( pre, post, proxiedObj ) { const newProps = post.filter( prop => pre.indexOf( prop ) === -1 ) for( let prop of newProps ) { let obj = proxiedObj[ prop ] if( obj.params !== undefined ) { Object.defineProperty( proxiedObj, prop, { get() { return obj }, set(value) { if( obj !== undefined && value !== undefined) { for( let param of obj.params ) { if( param.name !== 'material' ) { value[ param.name ] = obj[ param.name ].value } } } obj = value } }) proxies.push( prop ) } } } let guielement = null const guis = [] const processParams = function( obj, pane, params ) { params.forEach( param => { if( param.type === 'float' ) { const guiparam = { get [param.name]() { return obj[ param.name ].value.x }, set [param.name](v){ obj[ param.name ].value.x = v; obj[ param.name ].dirty = true } } const min = param.min || 0, max = param.max || 3, step = param.step pane.addInput( guiparam, param.name, { min, max, step }) }else if( param.type === 'vec3' ) { if( param.name.search( 'color' ) === -1 ) { const guiparam = { [param.name]:{ get x() { return obj[ param.name ].value.x }, set x(v){ obj[ param.name ].value.x = v; obj[ param.name ].dirty = true }, get y() { return obj[ param.name ].value.y }, set y(v){ obj[ param.name ].value.y = v; obj[ param.name ].dirty = true }, get z() { return obj[ param.name ].value.z }, set z(v){ obj[ param.name ].value.z = v; obj[ param.name ].dirty = true } } } const min = param.min || 0, max = param.max || 3, step = param.step pane.addInput( guiparam[ param.name ], 'x', { min, max, label:param.name + ' X' }) pane.addInput( guiparam[ param.name ], 'y', { min, max, label:param.name + ' Y' }) pane.addInput( guiparam[ param.name ], 'z', { min, max, label:param.name + ' Z' }) }else{ const guiparam = { [param.name]:{r:0,g:0,b:0} } pane.addInput( guiparam, param.name, { input:'color' }) .on( 'change', v => { const rgb = v.toRgbObject() obj[ param.name ].value.x = rgb.r / 255 obj[ param.name ].value.y = rgb.g / 255 obj[ param.name ].value.z = rgb.b / 255 obj[ param.name ].dirty = true }) } } }) } const guiForObject = function( ) { const obj = this const pane = new Tweakpane({ container: document.querySelector('#menu'), title:this.name }) guis.push( pane ) const params = obj.params || obj.parameters processParams( obj, pane, params ) const transform = { get tx() { return obj.transform.translation.x }, set tx(v){ obj.transform.translation.x = v }, get ty() { return obj.transform.translation.y }, set ty(v){ obj.transform.translation.y = v }, get tz() { return obj.transform.translation.z }, set tz(v){ obj.transform.translation.z = v }, get rx() { return obj.transform.rotation.axis.x }, set rx(v){ obj.transform.rotation.axis.x = v }, get ry() { return obj.transform.rotation.axis.y }, set ry(v){ obj.transform.rotation.axis.y = v }, get rz() { return obj.transform.rotation.axis.z }, set rz(v){ obj.transform.rotation.axis.z = v }, get ra() { return obj.transform.rotation.angle }, set ra(v){ obj.transform.rotation.angle = v }, get scale() { return obj.transform.scale }, set scale(v){ obj.transform.scale = v }, } const transformFolder = pane.addFolder({ title:'Transform' }) transformFolder.addInput( transform, 'tx', { label:'Translate X', min:-5, max:5 }) transformFolder.addInput( transform, 'ty', { label:'Translate y', min:-5, max:5 }) transformFolder.addInput( transform, 'tz', { label:'Translate z', min:-5, max:5 }) transformFolder.addInput( transform, 'rx', { label:'Rotation Axis X', min:0, max:1 }) transformFolder.addInput( transform, 'ry', { label:'Rotation Axis Y', min:0, max:1 }) transformFolder.addInput( transform, 'rz', { label:'Rotation Axis Z', min:0, max:1 }) transformFolder.addInput( transform, 'ra', { label:'Rotation Angle', min:-360, max:360 }) transformFolder.addInput( transform, 'scale', { label:'Scale', min:0.001, max:5, presetKey:'transformScale' }) if( this.__textureObj !== undefined ) { const textureFolder = pane.addFolder({ title:'Texture' }) processParams( this.__textureObj.__target, textureFolder, this.__textureObj.parameters ) } this.__gui = pane this.__gui.__target = obj this.gui.applyPreset = p => applyPreset.call( this, p ) this.gui.export = pane.exportPreset.bind( pane ) return this } const applyPreset = function( presetObj ){ const keys = Object.keys( presetObj ), transformStartIdx = keys.indexOf('tx'), textureStartIdx = transformStartIdx + 8 for( let i = 0; i < transformStartIdx; i++ ) { const key = keys[ i ] this[ key ].value.x = presetObj[ key ] } this.transform.translation.x = presetObj.tx this.transform.translation.y = presetObj.ty this.transform.translation.z = presetObj.tz this.transform.rotation.x = presetObj.rx this.transform.rotation.y = presetObj.ry this.transform.rotation.z = presetObj.rz this.transform.rotation.angle = presetObj.ra this.transform.scale = presetObj.transformScale if( this.__textureObj !== undefined ) { for( let i = textureStartIdx; i < keys.length; i++ ) { const key = keys[ i ] this.__textureObj.__target[ key ].value.x = presetObj[ key ] this.__textureObj.__target[ key ].dirty = true } } this.__gui.dispose() guiForObject.call( this ) } window.gui = function() { if( guielement !== null ) { guielement.dispose() } const scene = Marching.scene guielement = pane } const _____s = SDF.Sphere() _____s.__proto__.__proto__.gui = guiForObject window.cm = cm window.getlink = function( name='link' ) { const lines = cm.getValue().split('\n') if( lines[ lines.length - 1].indexOf('getlink') > -1 ) { lines.pop() } const code = btoa( lines.join('\n' ) ) const link = `https://charlieroberts.github.io/marching/playground/index.htm?${code}` Toastr.options = { "closeButton": true, "debug": false, "newestOnTop": false, "progressBar": false, "positionClass": "toast-bottom-center", "preventDuplicates": false, "onclick": null, "showDuration": "300", "hideDuration": "1000", "timeOut": 0, "extendedTimeOut": 0, "showEasing": "swing", "hideEasing": "linear", "showMethod": "fadeIn", "hideMethod": "fadeOut", "tapToDismiss": false } Toastr["info"](`<a href="${link}">${name}</a>`, "Your sketch link:") return link } if( window.location.search !== '' ) { // use slice to get rid of ? const val = atob( window.location.search.slice(1) ) cm.setValue(val) eval( val ) }else{ eval( demos.introduction ) } }