UNPKG

@bitsy/hecks

Version:

a collection of re-usable scripts for bitsy game maker

273 lines (228 loc) • 19.5 kB
/** 😓 @file canvas replacement @summary WebGLazy bitsy integration (this one's mostly just for me) @license MIT @version 3.0.1 @author Sean S. LeBlanc @description Replaces bitsy canvas with a responsive WebGL canvas (this one's mostly just for me) HOW TO USE: 1. Copy-paste this script into a script tag after the bitsy source 2. For finer scaling, edit `var text_scale = 2` and `var scale = 4` in the bitsy source to `var text_scale = 1` and `var scale = 2` 3. Edit the hackOptions object passed to the `new WebGLazy` call as needed The shader used to render the canvas can be overriden via hack options: e.g. var hackOptions = { glazyOptions = { fragment: ` // uv-wave fragment shader precision mediump float; uniform sampler2D tex0; uniform sampler2D tex1; uniform float time; uniform vec2 resolution; void main(){ vec2 coord = gl_FragCoord.xy; vec2 uv = coord.xy / resolution.xy; uv.x += sin(uv.y * 10.0 + time / 200.0) / 60.0; uv.y += cos(uv.x * 10.0 + time / 200.0) / 60.0; vec3 col = texture2D(tex0,uv).rgb; gl_FragColor = vec4(col, 1.0); } `, }, }; */ this.hacks = this.hacks || {}; this.hacks.canvas_replacement = (function (exports,bitsy) { 'use strict'; var hackOptions = { glazyOptions: { background: "black", scaleMode: "MULTIPLES", // use "FIT" if you prefer size to pixel accuracy allowDownscaling: true, disableFeedbackTexture: true, // set this to false if you want to use the feedback texture }, init: function(glazy) { // you can set up any custom uniforms you have here if needed // e.g. glazy.glLocations.myUniform = glazy.gl.getUniformLocation(glazy.shader.program, 'myUniform'); }, update: function(glazy) { // you can update any custom uniforms you have here if needed // e.g. glazy.gl.uniform1f(glazy.glLocations.myUniform, 0); }, }; bitsy = bitsy && bitsy.hasOwnProperty('default') ? bitsy['default'] : bitsy; function t(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function e(t,e){for(var i=0;i<e.length;i++){var s=e[i];s.enumerable=s.enumerable||!1, s.configurable=!0, "value"in s&&(s.writable=!0), Object.defineProperty(t,s.key,s);}}function i(t,i,s){return i&&e(t.prototype,i), s&&e(t,s), t}function s(t){if(!s.context&&(s.context=t.getContext("webgl")||t.getContext("experimental-webgl"), !s.context))throw"No WebGL support";return s.context}var n=function(){function e(i,n){t(this,e), this.gl=new s, this.vertSource=i, this.fragSource=n, this.program=this.gl.createProgram();try{this.vertShader=this.compileShader(this.vertSource,this.gl.VERTEX_SHADER), this.fragShader=this.compileShader(this.fragSource,this.gl.FRAGMENT_SHADER);}catch(t){throw this.gl.deleteProgram(this.program), delete this.program, console.error("Couldn't create shader: ",t), t}this.gl.attachShader(this.program,this.vertShader), this.gl.deleteShader(this.vertShader), delete this.vertShader, this.gl.attachShader(this.program,this.fragShader), this.gl.deleteShader(this.fragShader), delete this.fragShader, this.gl.linkProgram(this.program);}return i(e,[{key:"compileShader",value:function(t,e){try{var i=this.gl.createShader(e);if(this.gl.shaderSource(i,t), this.gl.compileShader(i), !this.gl.getShaderParameter(i,this.gl.COMPILE_STATUS))throw this.gl.getShaderInfoLog(i);return i}catch(i){throw console.error("Couldn't compile shader ("+e+"): ",t,i), i}}},{key:"useProgram",value:function(){this.gl.useProgram(this.program);}}]), e}(),o=function(){function e(i,n,o){t(this,e), this.gl=new s, this.source=i, this.texture=this.gl.createTexture(), this.bind(n), this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGBA,this.gl.RGBA,this.gl.UNSIGNED_BYTE,this.source), this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,!0), this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MAG_FILTER,o?this.gl.NEAREST:this.gl.LINEAR), this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,o?this.gl.NEAREST:this.gl.LINEAR), this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_S,this.gl.CLAMP_TO_EDGE), this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_T,this.gl.CLAMP_TO_EDGE), this.gl.bindTexture(this.gl.TEXTURE_2D,null);}return i(e,[{key:"update",value:function(){this.bind(), this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,!0), this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGBA,this.gl.RGBA,this.gl.UNSIGNED_BYTE,this.source), this.gl.bindTexture(this.gl.TEXTURE_2D,null);}},{key:"bind",value:function(t){var e=t||this.lastBoundId||0;this.gl.activeTexture(this.gl.TEXTURE0+e), this.gl.bindTexture(this.gl.TEXTURE_2D,this.texture), this.lastBoundId=e;}}]), e}(),r="// default vertex shader\nattribute vec4 position;\nvoid main() {\n\tgl_Position = position;\n}",a="// default fragment shader\nprecision mediump float;\nuniform sampler2D tex0;\nuniform sampler2D tex1;\nuniform vec2 resolution;\n\nvoid main() {\n\tvec2 coord = gl_FragCoord.xy;\n\tvec2 uv = coord.xy / resolution.xy;\n\tgl_FragColor = vec4(texture2D(tex0, uv).rgb, 1.0);\n}",h=function(){function e(i){var s=this,n=i.source,o=i.sources,h=void 0===o?["canvas","video","img"]:o,c=i.hideSource,l=void 0===c||c,u=i.background,d=void 0===u?"black":u,g=i.scaleMultiplier,v=void 0===g?1:g,m=i.scaleMode,f=void 0===m?e.SCALE_MODES.FIT:m,p=i.allowDownscaling,E=void 0!==p&&p,T=i.timestep,b=void 0===T?1/60*1e3:T,x=i.disableFeedbackTexture,y=void 0!==x&&x,w=i.disableMouseEvents,S=void 0!==w&&w,L=i.pixelate,_=void 0===L||L,A=i.autoInit,R=void 0===A||A,M=i.vertex,C=void 0===M?r:M,F=i.fragment,z=void 0===F?a:F;t(this,e), this.main=function(t){s.lastTime=s.curTime, s.curTime=(t||s.now())-s.startTime, s.deltaTime=s.curTime-s.lastTime, s.accumulator+=s.deltaTime, s.accumulator>s.timestep&&(s.render(), s.accumulator-=s.timestep), s.requestAnimationFrame(s.main);}, this.sources=h, this.source=n||this.getSource(), this.hideSource=l, this.background=d, this.scaleMultiplier=v, this._scale=v, this.scaleMode=f, this.allowDownscaling=E, this.timestep=b, this.disableFeedbackTexture=!!y, this.disableMouseEvents=!!S, this.pixelate=_, this.vertex=C, this.fragment=z, R&&this.init();}return i(e,[{key:"getSource",value:function(){var t,e=[];for(t=0;t<this.sources.length;++t)e.push(Array.prototype.slice.call(document.getElementsByTagName(this.sources[t])));if(0===(e=Array.prototype.concat.apply([],e)).length)throw"Couldn't find an element from "+this.sources+" to use as a source";return e[0]}},{key:"insertStylesheet",value:function(){this.style=document.createElement("style"), document.head.appendChild(this.style), this.style.innerHTML="\nhtml,body,div#canvasContainer{\n\tpadding:0;\n\tmargin:0;\n\n\twidth:100%;\n\theight:100%;\n\n\ttop:0;\n\tleft:0;\n\tright:0;\n\tbottom:0;\n\n\tbackground: ".concat(this.background,";\n\tcolor:#FFFFFF;\n\n\toverflow:hidden;\n\n\t").concat(this.hideSource?"visibility: hidden!important;":"","\n}\n\ncanvas#outputCanvas{\n").concat(this.pixelate?"\n\timage-rendering: optimizeSpeed;\n\timage-rendering: -webkit-crisp-edges;\n\timage-rendering: -moz-crisp-edges;\n\timage-rendering: -o-crisp-edges; \n\timage-rendering: crisp-edges;\n\timage-rendering: -webkit-optimize-contrast;\n\timage-rendering: optimize-contrast;\n\timage-rendering: pixelated;\n\t-ms-interpolation-mode: nearest-neighbor;\n":"","\n\n\tposition:absolute;\n\tmargin:auto;\n\ttop:0;\n\tleft:-1000%;\n\tright:-1000%;\n\tbottom:0;\n\n\t\t\t").concat(this.hideSource?" visibility: visible!important;":"","\n\t\t\t").concat(this.scaleMode===this.constructor.SCALE_MODES.MULTIPLES?"\n\ttransition:\n\t\twidth 0.2s cubic-bezier(0.22, 1.84, 0.88, 0.77),\n\t\theight 0.2s cubic-bezier(0.22, 1.84, 0.88, 0.77);":"","\n};");}},{key:"init",value:function(){this.size={x:this.source.width||this.source.style.width,y:this.source.height||this.source.style.height}, this.size.x*=this.scaleMultiplier||1, this.size.y*=this.scaleMultiplier||1, this.ratio=this.size.x/this.size.y, this.insertStylesheet(), this.canvasContainer=document.createElement("div"), this.canvasContainer.id="canvasContainer", this.allowDownscaling||(this.canvasContainer.style.minWidth=this.size.x+"px", this.canvasContainer.style.minHeight=this.size.y+"px"), this.canvas=document.createElement("canvas"), this.canvas.id="outputCanvas", this.canvas.width=this.size.x, this.canvas.height=this.size.y, this.canvas.style.width=this.canvas.style.height=0, this.canvasContainer.appendChild(this.canvas), document.body.appendChild(this.canvasContainer);try{this.gl=new s(this.canvas), this.render=this.renderGL;}catch(t){console.warn("Falling back to canvas rendering: ",t), this.render=this.renderCanvas, this.canvas2d=this.canvas.getContext("2d");}this.gl&&(this.shader=new n(this.vertex,this.fragment), this.vertices=new Float32Array([-1,-1,1,-1,-1,1,1,-1,1,1,-1,1]), this.vertexBuffer=this.gl.createBuffer(), this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.vertexBuffer), this.gl.bufferData(this.gl.ARRAY_BUFFER,this.vertices,this.gl.STATIC_DRAW), this.textureSource=new o(this.source,0,this.pixelate), this.disableFeedbackTexture||(this.textureFeedback=new o(this.canvas,1,this.pixelate)), this.glLocations={position:this.gl.getAttribLocation(this.shader.program,"position"),tex0:this.gl.getUniformLocation(this.shader.program,"tex0"),tex1:this.gl.getUniformLocation(this.shader.program,"tex1"),time:this.gl.getUniformLocation(this.shader.program,"time"),resolution:this.gl.getUniformLocation(this.shader.program,"resolution")}, this.gl.enableVertexAttribArray(this.glLocations.position), this.gl.viewport(0,0,this.size.x,this.size.y), this.shader.useProgram(), this.gl.vertexAttribPointer(this.glLocations.position,2,this.gl.FLOAT,!1,0,0), this.gl.clearColor(0,0,0,1), this.gl.uniform1i(this.glLocations.tex0,0), this.gl.uniform1i(this.glLocations.tex1,1), this.gl.uniform2f(this.glLocations.resolution,this.size.x,this.size.y)), window.onresize=this.onResize.bind(this), window.onresize(), this.disableMouseEvents||(this.canvas.onmousedown=this.onMouseEvent.bind(this), this.canvas.onmouseup=this.onMouseEvent.bind(this), this.canvas.onmousemove=this.onMouseEvent.bind(this), this.canvas.onmouseenter=this.onMouseEvent.bind(this), this.canvas.onmouseexit=this.onMouseEvent.bind(this), this.canvas.onmouseover=this.onMouseEvent.bind(this), this.canvas.onmouseout=this.onMouseEvent.bind(this), this.canvas.onmouseleave=this.onMouseEvent.bind(this), this.canvas.onclick=this.onMouseEvent.bind(this), this.canvas.ondblclick=this.onMouseEvent.bind(this), this.canvas.oncontextmenu=this.onMouseEvent.bind(this), this.canvas.ontouchstart=this.onTouchEvent.bind(this), this.canvas.ontouchend=this.onTouchEvent.bind(this), this.canvas.ontouchmove=this.onTouchEvent.bind(this), this.canvas.touchcancel=this.onTouchEvent.bind(this)), this.accumulator=0, "performance"in window?this.now=function(){return window.performance.now()}:this.now=function(){return window.Date.now()}, "requestAnimationFrame"in window?this.requestAnimationFrame=function(t){window.requestAnimationFrame(t);}:this.requestAnimationFrame=function(t){setTimeout(t,-1);}, this.startTime=this.now(), this.curTime=this.lastTime=0, this.main(this.curTime);}},{key:"renderCanvas",value:function(){this.canvas2d.clearRect(0,0,this.size.x,this.size.y), this.canvas2d.drawImage(this.source,0,0);}},{key:"renderGL",value:function(){this.textureSource.update(), this.gl.uniform1f(this.glLocations.time,this.curTime), this.gl.clear(this.gl.COLOR_BUFFER_BIT|this.gl.DEPTH_BUFFER_BIT), this.shader.useProgram(), this.textureSource.bind(), this.disableFeedbackTexture||this.textureFeedback.bind(), this.gl.drawArrays(this.gl.TRIANGLES,0,this.vertices.length/2), this.disableFeedbackTexture||this.textureFeedback.update();}},{key:"onResize",value:function(){var t,e,i=this.canvasContainer.offsetWidth,s=this.canvasContainer.offsetHeight,n=i/s,o=this.constructor.SCALE_MODES,r=1;switch(n<this.ratio?s=Math.round(i/this.ratio):i=Math.round(s*this.ratio), this.scaleMode){case o.MULTIPLES:for(r=1, t=this.size.x, e=this.size.y;t+this.size.x<=i||e+this.size.y<=s;)t+=this.size.x, e+=this.size.y, r+=1;break;case o.FIT:t=i, e=s, r=i/this.size.x;break;case o.COVER:i=this.canvasContainer.offsetWidth, s=this.canvasContainer.offsetHeight, n<this.ratio?i=Math.round(s*this.ratio):s=Math.round(i/this.ratio), t=i, e=s, r=i/this.size.x;break;case o.NONE:r=1, t=this.size.x, e=this.size.y;}this._scale=this.scaleMultiplier*r, this.canvas.style.width=t+"px", this.canvas.style.height=e+"px";}},{key:"onMouseEvent",value:function(t){var e=this.canvas,i=this.source,s=e.offsetLeft+e.scrollLeft,n=e.offsetTop+e.scrollTop,o=i.offsetLeft+i.scrollLeft,r=i.offsetTop+i.scrollTop,a=1/this._scale,h=new MouseEvent(t.type,{screenX:(t.screenX-s)*a+o,screenY:(t.screenY-n)*a+r,clientX:(t.clientX-s)*a+o,clientY:(t.clientY-n)*a+r,altKey:t.altKey,shiftKey:t.shiftKey,metaKey:t.metaKey,button:t.button,buttons:t.buttons,relatedTarget:t.relatedTarget,region:t.region});i.dispatchEvent(h);}},{key:"onTouchEvent",value:function(t){var e=this.canvas,i=this.source,s=e.offsetLeft+e.scrollLeft,n=e.offsetTop+e.scrollTop,o=i.offsetLeft+i.scrollLeft,r=i.offsetTop+i.scrollTop,a=1/this._scale,h=function(t){return new Touch({identifier:t.identifier,force:t.force,rotationAngle:t.rotationAngle,target:t.target,radiusX:t.radiusX,radiusY:t.radiusY,pageX:(t.pageX-s)*a+o,pageY:(t.pageY-s)*a+o,screenX:(t.screenX-s)*a+o,screenY:(t.screenY-n)*a+r,clientX:(t.clientX-s)*a+o,clientY:(t.clientY-n)*a+r})},c=Array.from(event.touches).map(h),l=Array.from(event.targetTouches).map(h),u=Array.from(event.changedTouches).map(h),d=new event.constructor(event.type,{touches:c,targetTouches:l,changedTouches:u,ctrlKey:event.ctrlKey,shiftKey:event.shiftKey,altKey:event.altKey,metaKey:event.metaKey});i.dispatchEvent(d);}}]), e}();h.SCALE_MODES=Object.freeze({FIT:"FIT",COVER:"COVER",MULTIPLES:"MULTIPLES",NONE:"NONE"}); /** @file utils @summary miscellaneous bitsy utilities @author Sean S. LeBlanc */ /* Helper used to replace code in a script tag based on a search regex To inject code without erasing original string, using capturing groups; e.g. inject(/(some string)/,'injected before $1 injected after') */ function inject(searchRegex, replaceString) { // find the relevant script tag var scriptTags = document.getElementsByTagName('script'); var scriptTag; var code; for (var i = 0; i < scriptTags.length; ++i) { scriptTag = scriptTags[i]; var matchesSearch = scriptTag.textContent.search(searchRegex) !== -1; var isCurrentScript = scriptTag === document.currentScript; if (matchesSearch && !isCurrentScript) { code = scriptTag.textContent; break; } } // error-handling if (!code) { throw 'Couldn\'t find "' + searchRegex + '" in script tags'; } // modify the content code = code.replace(searchRegex, replaceString); // replace the old script tag with a new one using our modified code var newScriptTag = document.createElement('script'); newScriptTag.textContent = code; scriptTag.insertAdjacentElement('afterend', newScriptTag); scriptTag.remove(); } /** * Helper for getting an array with unique elements * @param {Array} array Original array * @return {Array} Copy of array, excluding duplicates */ function unique(array) { return array.filter(function (item, idx) { return array.indexOf(item) === idx; }); } /** @file kitsy-script-toolkit @summary makes it easier and cleaner to run code before and after Bitsy functions or to inject new code into Bitsy script tags @license WTFPL (do WTF you want) @version 4.0.0 @requires Bitsy Version: 4.5, 4.6 @author @mildmojo @description HOW TO USE: import {before, after, inject, addDialogTag, addDeferredDialogTag} from "./helpers/kitsy-script-toolkit"; before(targetFuncName, beforeFn); after(targetFuncName, afterFn); inject(searchRegex, replaceString); addDialogTag(tagName, dialogFn); addDeferredDialogTag(tagName, dialogFn); For more info, see the documentation at: https://github.com/seleb/bitsy-hacks/wiki/Coding-with-kitsy */ // Ex: after('load_game', function run() { alert('Loaded!'); }); function after(targetFuncName, afterFn) { var kitsy = kitsyInit(); kitsy.queuedAfterScripts[targetFuncName] = kitsy.queuedAfterScripts[targetFuncName] || []; kitsy.queuedAfterScripts[targetFuncName].push(afterFn); } function kitsyInit() { // return already-initialized kitsy if (bitsy.kitsy) { return bitsy.kitsy; } // Initialize kitsy bitsy.kitsy = { queuedInjectScripts: [], queuedBeforeScripts: {}, queuedAfterScripts: {} }; var oldStartFunc = bitsy.startExportedGame; bitsy.startExportedGame = function doAllInjections() { // Only do this once. bitsy.startExportedGame = oldStartFunc; // Rewrite scripts and hook everything up. doInjects(); applyAllHooks(); // Start the game bitsy.startExportedGame.apply(this, arguments); }; return bitsy.kitsy; } function doInjects() { bitsy.kitsy.queuedInjectScripts.forEach(function (injectScript) { inject(injectScript.searchRegex, injectScript.replaceString); }); _reinitEngine(); } function applyAllHooks() { var allHooks = unique(Object.keys(bitsy.kitsy.queuedBeforeScripts).concat(Object.keys(bitsy.kitsy.queuedAfterScripts))); allHooks.forEach(applyHook); } function applyHook(functionName) { var functionNameSegments = functionName.split('.'); var obj = bitsy; while (functionNameSegments.length > 1) { obj = obj[functionNameSegments.shift()]; } var lastSegment = functionNameSegments[0]; var superFn = obj[lastSegment]; var superFnLength = superFn ? superFn.length : 0; var functions = []; // start with befores functions = functions.concat(bitsy.kitsy.queuedBeforeScripts[functionName] || []); // then original if (superFn) { functions.push(superFn); } // then afters functions = functions.concat(bitsy.kitsy.queuedAfterScripts[functionName] || []); // overwrite original with one which will call each in order obj[lastSegment] = function () { var returnVal; var args; var i = 0; function runBefore() { // All outta functions? Finish if (i === functions.length) { return returnVal; } // Update args if provided. if (arguments.length > 0) { args = [].slice.call(arguments); } if (functions[i].length > superFnLength) { // Assume funcs that accept more args than the original are // async and accept a callback as an additional argument. return functions[i++].apply(this, args.concat(runBefore.bind(this))); } else { // run synchronously returnVal = functions[i++].apply(this, args); if (returnVal && returnVal.length) { args = returnVal; } return runBefore.apply(this, args); } } return runBefore.apply(this, arguments); }; } function _reinitEngine() { // recreate the script and dialog objects so that they'll be // referencing the code with injections instead of the original bitsy.scriptModule = new bitsy.Script(); bitsy.scriptInterpreter = bitsy.scriptModule.CreateInterpreter(); bitsy.dialogModule = new bitsy.Dialog(); bitsy.dialogRenderer = bitsy.dialogModule.CreateRenderer(); bitsy.dialogBuffer = bitsy.dialogModule.CreateBuffer(); } var glazy; after('startExportedGame', function () { glazy = new h(hackOptions.glazyOptions); hackOptions.init(glazy); }); after('update', function () { hackOptions.update(glazy); }); exports.hackOptions = hackOptions; return exports; }({},window));