Created
March 5, 2019 03:03
-
-
Save morningtoast/bf1347b049280b655c939c8c0a52d261 to your computer and use it in GitHub Desktop.
CRT export template for PICO-8 games
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<title>PICO-8 Cartridge</title> | |
<meta name="description" content=""> | |
<STYLE TYPE="text/css"> | |
<!-- | |
canvas#canvas { width: 512px; height: 512px; } | |
.pico8_el { | |
float:left; | |
width:92px; | |
display:inline-block; | |
margin: 1px; | |
padding: 4px; | |
text-align: center; | |
color:#fff; | |
background-color:#777; | |
font-family : verdana; | |
font-size: 9pt; | |
cursor: pointer; | |
cursor: hand; | |
} | |
.pico8_el a{ | |
text-decoration: none; | |
color:#fff; | |
} | |
.pico8_el:hover{ | |
background-color:#aaa; | |
} | |
.pico8_el:link{ | |
background-color:#aaa; | |
} | |
canvas{ | |
image-rendering: optimizeSpeed; | |
image-rendering: -moz-crisp-edges; | |
image-rendering: -webkit-optimize-contrast; | |
image-rendering: optimize-contrast; | |
image-rendering: pixelated; | |
-ms-interpolation-mode: nearest-neighbor; | |
border: 0px | |
} | |
--> | |
</STYLE> | |
</head> | |
<body bgcolor=#303030> | |
<br><br><br> | |
<center><div style="width:512px;"> | |
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()"></canvas> | |
<script type="text/javascript"> | |
var canvas = document.getElementById("canvas"); | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
// show Emscripten environment where the canvas is | |
// arguments are passed to PICO-8 | |
var Module = {}; | |
Module.canvas = canvas; | |
/* | |
// When pico8_buttons is defined, PICO-8 takes each int to be a live bitfield | |
// representing the state of each player's buttons | |
var pico8_buttons = [0, 0, 0, 0, 0, 0, 0, 0]; // max 8 players | |
pico8_buttons[0] = 2 | 16; // example: player 0, RIGHT and Z held down | |
// when pico8_gpio is defined, reading and writing to gpio pins will | |
// read and write to these values | |
var pico8_gpio = new Array(128); | |
*/ | |
</script> | |
<script async type="text/javascript" src="##js_file##"></script> | |
<script> | |
// key blocker. prevent cursor keys from scrolling page while playing cart. | |
function onKeyDown_blocker(event) { | |
event = event || window.event; | |
var o = document.activeElement; | |
if (!o || o == document.body || o.tagName == "canvas") | |
{ | |
if ([32, 37, 38, 39, 40].indexOf(event.keyCode) > -1) | |
{ | |
if (event.preventDefault) event.preventDefault(); | |
} | |
} | |
} | |
document.addEventListener('keydown', onKeyDown_blocker, false); | |
</script> | |
<br> | |
<div class=pico8_el onclick="Module.pico8Reset();"> | |
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAaklEQVR4Ae2dOwoAMQhE15A+rfc/3bZ7AlMnQfywCkKsfcgMM9ZP+QHtIn0vLeBAFduiFdQ/0DmvtR5LXJ6CPSXe2ZXcFNlTxFbemKrbZPs35XogeS9xeQr+anT6LzoOwEDwZJ7jwhXUnwkTTiDQ2Ja34AAAABB0RVh0TG9kZVBORwAyMDExMDIyMeNZtsEAAAAASUVORK5CYII=" alt="Reset" width=12 height= 12/> | |
Reset</div> | |
<div class=pico8_el onclick="Module.pico8TogglePaused();"> | |
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAPUlEQVR4Ae3doQ0AIAxEUWABLPtPh2WCq26DwFSU/JPNT166QSu/Hg86W9dwLte+diP7AwAAAAAAgD+A+jM2ZAgo84I0PgAAABB0RVh0TG9kZVBORwAyMDExMDIyMeNZtsEAAAAASUVORK5CYII=" alt="Pause" width=12 height=12/> | |
Pause</div> | |
<div class=pico8_el onclick="Module.requestFullScreen(true, false);"> | |
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAaklEQVR4Ae2dsQ1AIQhExfze1v2ns3UCrfgFhmgUUAoGgHscp21wX9BqaZoDojbB96OkDJKNcTN2BHTyYNYmoT2BlPL7BKgcPfHjAVXKKadkHOn9K1r16N0czN6a95N8mnA7Aq2fTZ3Af3UKmCSMazL8HwAAABB0RVh0TG9kZVBORwAyMDExMDIyMeNZtsEAAAAASUVORK5CYII=" alt="Fullscreen" width=12 height=12/> | |
Fullscreen</div> | |
<div class=pico8_el onclick="Module.pico8ToggleSound();"> | |
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAXklEQVR4Ae2doQ4AIQxD4YLH8v9fh+ULhjpxxSwLg2uyapr1JRu1iV5Z+1BGl4+xNpX38SYo2uRvYiT5LwEmt+ocgXVLrhPEgBiw8Q5w7/kueSkK+D2tJO4E/I3GrwkqQCBabEj/4QAAABB0RVh0TG9kZVBORwAyMDExMDIyMeNZtsEAAAAASUVORK5CYII=" alt="Toggle Sound" width=12 height=12/> | |
Sound</div> | |
<div class=pico8_el ><a target="_new" href="http://www.lexaloffle.com/bbs/?cat=7&sub=2"> | |
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAlElEQVR4Ae2dMQ5FQBCGh6jcwAkkateg3DiAa+iQUGqVKi95FQfAJRQOoHeBUf8JyQqKjZ1uMzuz2e/LTE3KhyF7kSlgLOykas23f6D+A9Yp84aAOYU15pcJnfji0Il2ID8HzC4y38ZrnfIBGxeRoR3c3EWrACdsV5BOsx7OSRnrOXh4F5HzA6bevwUn8wlz7eCDsQM99B3ks0s/4QAAABB0RVh0TG9kZVBORwAyMDExMDIyMeNZtsEAAAAASUVORK5CYII=" alt="More Carts" width=12 height=12/> | |
Carts</a></div> | |
<br> | |
</div></center> | |
<br><br> | |
</body></html> | |
<!-- 8< -- ## FULLSCREEN FIX STARTS HERE ## -- --> | |
<script type="text/javascript"> | |
{ | |
var breaks = canvas.parentNode.getElementsByTagName('BR'); | |
for (var i = 0; i < breaks.length; i++) | |
canvas.parentNode.removeChild(breaks[i]); | |
} | |
canvas.parentNode.style.width="512"; | |
screen = document.createElement("div"); | |
canvas.parentNode.replaceChild(screen,canvas); | |
screen.appendChild(canvas); | |
screen.style.display = "flex"; | |
screen.style.alignItems = "center"; | |
canvas.style.margin = "auto"; | |
function resizeCanvas() | |
{ | |
var mult=128; | |
var csize=Math.floor(512/mult)*mult; | |
if (document.fullscreenElement || document.mozFullScreenElement || document.webkitIsFullScreen || document.msFullscreenElement) | |
{ | |
csize=Math.max(mult,Math.min(Math.floor(window.innerWidth/mult)*mult,Math.floor(window.innerHeight/mult)*mult)); | |
} | |
canvas.style.width = csize; | |
canvas.style.height = csize; | |
screen.parentNode.style.width=csize; | |
window.focus(); | |
} | |
window.addEventListener('load', resizeCanvas, false); | |
window.addEventListener('resize', resizeCanvas, false); | |
window.addEventListener('orientationchange', resizeCanvas, false); | |
window.addEventListener('fullscreenchange', resizeCanvas, false); | |
window.addEventListener('webkitfullscreenchange', resizeCanvas, false);//for itch.app | |
function toggleFullscreen() | |
{ | |
if (document.fullscreenElement || document.mozFullScreenElement || document.webkitIsFullScreen || document.msFullscreenElement) | |
{//exit fs | |
screen.cancelFullscreen = screen.cancelFullscreen || screen.mozCancelFullScreen || screen.webkitCancelFullScreen; | |
screen.cancelFullscreen(); | |
} | |
else | |
{//enter fs | |
screen.requestFullscreen = screen.requestFullscreen || screen.mozRequestFullScreen || screen.webkitRequestFullScreen; | |
screen.requestFullscreen(); | |
} | |
} | |
function setrfds() | |
{ | |
if (Module.requestFullScreen) | |
Module.requestFullScreen = toggleFullscreen; | |
else | |
requestAnimationFrame(setrfds); | |
} | |
requestAnimationFrame(setrfds); | |
</script> | |
<!-- 8< -- ## FULLSCREEN FIX ENDS HERE ## -- --> | |
<!-- 8< -- ## GL SHADER STUFF STARTS HERE ## -- --> | |
<!-- vertex shader --> | |
<script id="some-vertex-shader" type="x-shader/x-vertex"> | |
attribute vec2 a_position; | |
attribute vec2 a_texCoord; | |
varying vec2 v_texCoord; | |
void main() | |
{ | |
gl_Position = vec4(a_position.x, a_position.y, 0, 1); | |
v_texCoord = a_texCoord; | |
} | |
</script> | |
<!-- fragment shader --> | |
<script id="some-fragment-shader" type="x-shader/x-fragment"> | |
precision mediump float; | |
varying vec2 v_texCoord; | |
uniform vec2 u_canvasSize; | |
uniform sampler2D u_texture; | |
// PUBLIC DOMAIN CRT STYLED SCAN-LINE SHADER | |
// by Timothy Lottes | |
// https://www.shadertoy.com/view/XsjSzR | |
// modified (borked) by ultrabrite | |
// Emulated input resolution. | |
const vec2 texSize=vec2(256.0,128.0); | |
// Hardness of scanline. | |
// -8.0 = soft | |
// -16.0 = medium | |
float hardScan=-8.0; | |
// Hardness of pixels in scanline. | |
// -2.0 = soft | |
// -4.0 = hard | |
const float hardPix=-2.0; | |
// Hardness of shadow mask in scanline. | |
// 0.5 = hard | |
// 3.0 = soft | |
const float hardMask=2.0; | |
const vec3 compos = vec3(1.0/6.0,1.0/2.0,5.0/6.0); | |
// Display warp. | |
// 0.0 = none | |
// 1.0/8.0 = extreme | |
const vec2 warp=vec2(1.0/24.0,1.0/24.0); | |
//------------------------------------------------------------------------ | |
// Nearest emulated sample given floating point position and texel offset. | |
// Also zero's off screen. | |
vec3 Fetch(vec2 pos,vec2 off) | |
{ | |
pos=floor(pos * texSize + off) / texSize; | |
if (pos.x<0.0 || pos.x>=1.0 || pos.y<0.0 || pos.y>=1.0) | |
return vec3(0.0,0.0,0.0); | |
return texture2D(u_texture,pos.xy).rgb; | |
} | |
// Distance in emulated pixels to nearest texel. | |
vec2 Dist(vec2 pos) | |
{ | |
pos=pos * texSize; | |
return -((pos-floor(pos))-vec2(0.5)); | |
} | |
// 1D Gaussian. | |
float Gaus(float pos,float scale) | |
{ | |
return exp2(scale*pos*pos); | |
} | |
// 3-tap Gaussian filter along horz line. | |
vec3 Horz3(vec2 pos,float off) | |
{ | |
mat3 m=mat3(Fetch(pos,vec2(-1.0,off)), | |
Fetch(pos,vec2( 0.0,off)), | |
Fetch(pos,vec2( 1.0,off))); | |
float dst=Dist(pos).x; | |
// Convert distance to weight. | |
vec3 v=vec3(Gaus(dst-1.0,hardPix), | |
Gaus(dst+0.0,hardPix), | |
Gaus(dst+1.0,hardPix)); | |
// Return filtered sample. | |
return (m*v)/(v.x+v.y+v.z); | |
} | |
// 5-tap Gaussian filter along horz line. | |
vec3 Horz5(vec2 pos,float off) | |
{ | |
vec3 a=Fetch(pos,vec2(-2.0,off)); | |
vec3 b=Fetch(pos,vec2(-1.0,off)); | |
vec3 c=Fetch(pos,vec2( 0.0,off)); | |
vec3 d=Fetch(pos,vec2( 1.0,off)); | |
vec3 e=Fetch(pos,vec2( 2.0,off)); | |
float dstx=Dist(pos).x; | |
// Convert distance to weight. | |
float wa=Gaus(dstx-2.0,hardPix); | |
float wb=Gaus(dstx-1.0,hardPix); | |
float wc=Gaus(dstx+0.0,hardPix); | |
float wd=Gaus(dstx+1.0,hardPix); | |
float we=Gaus(dstx+2.0,hardPix); | |
// Return filtered sample. | |
return (a*wa+b*wb+c*wc+d*wd+e*we)/(wa+wb+wc+wd+we); | |
} | |
// Allow nearest three lines to effect pixel. | |
vec3 Tri(vec2 pos) | |
{ | |
mat3 m=mat3(Horz3(pos,-1.0), | |
Horz5(pos, 0.0), | |
Horz3(pos, 1.0)); | |
float dsty=Dist(pos).y; | |
vec3 v=vec3(Gaus(dsty-1.0,hardScan), | |
Gaus(dsty+0.0,hardScan), | |
Gaus(dsty+1.0,hardScan)); | |
return m*v; | |
} | |
// Distortion of scanlines, and end of screen alpha. | |
vec2 Warp(vec2 pos) | |
{ | |
pos=pos*2.0-1.0; | |
pos*=1.0+vec2(pos.y*pos.y,pos.x*pos.x)*warp; | |
return pos*0.5+0.5; | |
} | |
vec3 Mask(float x) | |
{ | |
vec3 v = clamp((fract(x)-compos)*hardMask,-1.0/3.0,1.0/3.0); | |
return 2.0/3.0+abs(v); | |
} | |
void main() | |
{ | |
gl_FragColor.rgb = Tri(Warp(v_texCoord.xy))*Mask(v_texCoord.x*texSize.x); | |
//gl_FragColor.rgb = texture2D(u_texture,v_texCoord).rgb; // original | |
gl_FragColor.a = 1.0; | |
} | |
</script> | |
<script type="text/javascript"> | |
p8Canvas = document.getElementById("canvas"); | |
canvas = p8Canvas.cloneNode(true); | |
p8Canvas.name = "p8Canvas"; | |
canvas.name = "canvas"; | |
canvas.class = ""; | |
canvas.width = p8Canvas.clientWidth; | |
canvas.height = p8Canvas.clientHeight; | |
gl = canvas.getContext('webgl'); | |
if (gl) | |
{ | |
p8Canvas.parentNode.insertBefore(canvas,p8Canvas); | |
canvas.parentNode.removeChild(p8Canvas); | |
//canvas.parentNode.style.boxShadow = "0px 0px 20px #000"; | |
function glresize() | |
{ | |
canvas.width = canvas.clientWidth; | |
canvas.height = canvas.clientHeight; | |
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); | |
gl.uniform2f(gl.getUniformLocation(program, "u_canvasSize"), gl.canvas.width, gl.canvas.height); | |
} | |
window.addEventListener('load', glresize, false); | |
window.addEventListener('resize', glresize, false); | |
window.addEventListener('orientationchange', glresize, false); | |
window.addEventListener('fullscreenchange', glresize, false); | |
window.addEventListener('webkitfullscreenchange', glresize, false);//for itch.app | |
function compileShader(gl, source, type) | |
{ | |
var shader = gl.createShader(type); | |
gl.shaderSource(shader, source); | |
gl.compileShader(shader); | |
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) | |
{ | |
var info = gl.getShaderInfoLog(shader); | |
throw ("could not compile shader:" + info); | |
} | |
return shader; | |
}; | |
var vs_script = document.getElementById("some-vertex-shader"); | |
var vs = compileShader(gl, vs_script.text, gl.VERTEX_SHADER); | |
var fs_script = document.getElementById("some-fragment-shader"); | |
var fs = compileShader(gl, fs_script.text, gl.FRAGMENT_SHADER); | |
program = gl.createProgram(); | |
gl.attachShader(program, vs); | |
gl.attachShader(program, fs); | |
gl.linkProgram(program); | |
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) | |
{ | |
var info = gl.getProgramInfoLog(program); | |
throw ("shader program failed to link:" + info); | |
} | |
gl.useProgram(program) | |
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); | |
gl.uniform3f(gl.getUniformLocation(program, "u_canvasSize"), gl.canvas.width, gl.canvas.height, 0.0 ); | |
var texCoordLocation = gl.getAttribLocation(program, "a_texCoord"); | |
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); | |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1,0, 0,0, 0,1, 0,1, 1,1, 1,0]), gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(texCoordLocation); | |
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); | |
var positionLocation = gl.getAttribLocation(program, "a_position"); | |
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); | |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1,1, -1,1, -1,-1, -1,-1, 1,-1, 1,1]), gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(positionLocation); | |
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); | |
gltex = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, gltex); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT ); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT ); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
gl.activeTexture(gl.TEXTURE0); | |
gl.uniform1i(gl.getUniformLocation(program, "u_texture0"), 0); | |
function gldraw() | |
{ | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, p8Canvas); | |
gl.drawArrays(gl.TRIANGLES, 0, 6); | |
requestAnimationFrame(gldraw); | |
} | |
gldraw(); | |
} | |
// ========================================================================================== | |
// krajzeg's gamepad support:https://github.com/krajzeg/pico8gamepad/ | |
// ========================================================================================== | |
// ====== [CONFIGURATION] - tailor to your specific needs | |
// How many PICO-8 players to support? | |
// - if set to 1, all connected controllers will control PICO-8 player 1 | |
// - if set to 2, controller #0 will control player 1, controller #2 - player 2, controller #3 - player 1, and so on | |
// - higher numbers will distribute the controls among the players in the same way | |
var supportedPlayers = 2; | |
// These flags control whether or not different types of buttons should | |
// be mapped to PICO-8 O and X buttons. | |
var mapFaceButtons = true; | |
var mapShoulderButtons = true; | |
var mapTriggerButtons = false; | |
var mapStickButtons = false; | |
// How far you have to pull an analog stick before it register as a PICO-8 d-pad direction | |
var stickDeadzone = 0.4; | |
// ====== [IMPLEMENTATION] | |
// Array through which we'll communicate with PICO-8. | |
var pico8_buttons = [0,0,0,0,0,0,0,0]; | |
// Start polling gamepads (if supported by browser) | |
if (navigator.getGamepads) | |
requestAnimationFrame(updateGamepads); | |
// Workhorse function, updates pico8_buttons once per frame. | |
function updateGamepads() { | |
var gamepads = navigator.getGamepads ? navigator.getGamepads() :[]; | |
// Reset the array. | |
for (var p = 0; p < supportedPlayers; p++) | |
pico8_buttons[p] = 0; | |
// Gather input from all known gamepads. | |
for (var i = 0; i < gamepads.length; i++) { | |
var gp = gamepads[i]; | |
if (!gp || !gp.connected) continue; | |
// which player is this assigned to? | |
var player = i % supportedPlayers; | |
var bitmask = 0; | |
// directions (from axes or d-pad "buttons") | |
bitmask |= (axis(gp,0) < -stickDeadzone || axis(gp,2) < -stickDeadzone || btn(gp,14)) ? 1 :0; // left | |
bitmask |= (axis(gp,0) > +stickDeadzone || axis(gp,2) > +stickDeadzone || btn(gp,15)) ? 2 :0; // right | |
bitmask |= (axis(gp,1) < -stickDeadzone || axis(gp,3) < -stickDeadzone || btn(gp,12)) ? 4 :0; // up | |
bitmask |= (axis(gp,1) > +stickDeadzone || axis(gp,3) > +stickDeadzone || btn(gp,13)) ? 8 :0; // down | |
// O and X buttons | |
var pressedO = | |
(mapFaceButtons && (btn(gp,0) || btn(gp,2))) || | |
(mapShoulderButtons && btn(gp,5)) || | |
(mapTriggerButtons && btn(gp,7)) || | |
(mapStickButtons && btn(gp,11)); | |
var pressedX = | |
(mapFaceButtons && (btn(gp,1) || btn(gp,3))) || | |
(mapShoulderButtons && btn(gp,4)) || | |
(mapTriggerButtons && btn(gp,6)) || | |
(mapStickButtons && btn(gp,10)); | |
bitmask |= pressedO ? 16 :0; | |
bitmask |= pressedX ? 32 :0; | |
// update array for the player (keeping any info from previous controllers) | |
pico8_buttons[player] |= bitmask; | |
// pause button is a bit different - PICO-8 only respects the 6th bit on the first player's input | |
// we allow all controllers to influence it, regardless of number of players | |
pico8_buttons[0] |= (btn(gp,8) || btn(gp,9)) ? 64 :0; | |
} | |
requestAnimationFrame(updateGamepads); | |
} | |
// Helpers for accessing gamepad | |
function axis(gp,n) { return gp.axes[n] || 0.0; } | |
function btn(gp,b) { return gp.buttons[b] ? gp.buttons[b].pressed :false; } | |
// ========================================================================================== | |
// chrome autoplay policy may2018 | |
// ========================================================================================== | |
var cartLoaded=false; | |
function loadCart() | |
{ | |
if (cartLoaded) return; | |
document.getElementById("start").style.visibility="hidden"; | |
document.getElementById("frame").style.visibility="visible"; | |
var script = document.createElement('script'); | |
script.type='text/javascript'; | |
script.async=true; | |
script.src="##js_file##"; | |
var loadFunction = function () | |
{ | |
cartLoaded=true; | |
document.getElementById("menubar").style.visibility="visible"; | |
resizeCanvas(); | |
} | |
script.onload = loadFunction; | |
script.onreadystatechange = loadFunction; | |
var s = document.getElementsByTagName('script')[0]; | |
s.parentNode.insertBefore(script,s); | |
}; | |
if (autoplay) | |
{ | |
var context = new AudioContext(); | |
context.onstatechange = function () | |
{ | |
if (context.state=='running') | |
{ | |
loadCart(); | |
context.close(); | |
} | |
}; | |
} | |
</script> | |
<!-- 8< -- ## GL SHADER STUFF ENDS HERE ## -- --> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment