├── .gitignore ├── src ├── override.css ├── restructure.js └── transform.py ├── README.md └── transform.py /.gitignore: -------------------------------------------------------------------------------- 1 | /*.js 2 | *.html 3 | -------------------------------------------------------------------------------- /src/override.css: -------------------------------------------------------------------------------- 1 | .pico8_container { 2 | display: -webkit-box; 3 | display: -webkit-flex; 4 | display: -ms-flexbox; 5 | display: flex; 6 | -webkit-box-orient: vertical; 7 | -webkit-box-direction: normal; 8 | -webkit-flex-direction: column; 9 | -ms-flex-direction: column; 10 | flex-direction: column; 11 | -webkit-box-align: center; 12 | -webkit-align-items: center; 13 | -ms-flex-align: center; 14 | align-items: center; 15 | } 16 | 17 | canvas#canvas { 18 | margin-top: 10px; 19 | width: 290px; 20 | height: 270px; 21 | } 22 | 23 | @media screen and (min-width: 580px) and (min-height: 600px) { 24 | canvas#canvas { 25 | margin-top: 20px; 26 | width: 580px; 27 | height: 540px; 28 | } 29 | } 30 | 31 | @media screen and (min-width: 1160px) and (min-height: 1150px) { 32 | canvas#canvas { 33 | margin-top: 30px; 34 | width: 1160px; 35 | height: 1080px; 36 | } 37 | } 38 | 39 | .pico8_options { 40 | display: -webkit-box; 41 | display: -webkit-flex; 42 | display: -ms-flexbox; 43 | display: flex; 44 | -webkit-box-pack: center; 45 | -webkit-justify-content: center; 46 | -ms-flex-pack: center; 47 | justify-content: center; 48 | -webkit-flex-wrap:wrap; 49 | -ms-flex-wrap:wrap; 50 | flex-wrap:wrap 51 | } 52 | 53 | .pico8_el { 54 | float: initial; 55 | display: initial; 56 | } 57 | -------------------------------------------------------------------------------- /src/restructure.js: -------------------------------------------------------------------------------- 1 | // purge all br elements 2 | var brs = document.querySelectorAll('br'); 3 | Array.prototype.forEach.call(brs, function (br) { 4 | br.parentNode.removeChild(br); 5 | }); 6 | 7 | // unwrap center elements 8 | var centerElems = document.querySelectorAll('center'); 9 | Array.prototype.forEach.call(centerElems, function (elem) { 10 | var parent = elem.parentNode; 11 | var children = elem.childNodes; 12 | for (var i = children.length - 1; i >= 0; i--) { 13 | parent.insertBefore(children[i], elem); 14 | } 15 | parent.removeChild(elem); 16 | }); 17 | 18 | // add pico8_container class to container element 19 | var container = document.getElementById('canvas').parentNode; 20 | var containerClassName = 'pico8_container'; 21 | if (container.classList) { 22 | container.classList.add(containerClassName); 23 | } else { 24 | container.className = 25 | container.className.replace(containerClassName, '') + 26 | ' ' + containerClassName; 27 | } 28 | 29 | // remove element-specific width from container element 30 | container.style.width = ''; 31 | 32 | // move pico 8 options to new options container element 33 | var options = document.getElementsByClassName('pico8_el'); 34 | if (options.length) { 35 | var optionsContainer = document.createElement('div'); 36 | optionsContainer.className = 'pico8_options'; 37 | container.insertBefore(optionsContainer, options[0]); 38 | Array.prototype.forEach.call(options, function (option) { 39 | optionsContainer.appendChild(option); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pico8 responsive webplayer transform 2 | 3 | [See it live here.](http://benwiley4000.github.io/pico8-responsive-webplayer-transform/pico8_responsive.html) 4 | 5 | [Download the latest release here.](https://github.com/benwiley4000/pico8-responsive-webplayer-transform/releases/latest) 6 | 7 | This is a python script you can run on an HTML file exported from the [PICO-8 Fantasy Console](http://www.lexaloffle.com/pico-8.php). It will create a new HTML file that is better suited for viewing at various window sizes, by subtly restructuring the page and including new CSS styles. Because it only changes what it needs to, you can run it on an existing page that you've already re-styled. 8 | 9 | Usage: 10 | 11 | ``` 12 | python transform.py [--lazy] 13 | ``` 14 | e.g. 15 | ``` 16 | python tramsform.py jelpi.html 17 | ``` 18 | 19 | The command above will output a new file called `jelpi-responsive.html`. Use it with the same `cartridge.js` file generated by PICO-8. 20 | 21 | ## dependencies 22 | 23 | * [Python](https://www.python.org/) 2.7+ (if you're on OSX or Linux, you likely already have Python installed.) 24 | 25 | It's also recommended (but not required) that you have the `beautifulsoup4`, `lxml`, and `cssutils` Python packages. 26 | 27 | With pip (Python's package installer): 28 | ``` 29 | pip install beautifulsoup4 lxml cssutils 30 | ``` 31 | Or via the Ubuntu repositories, if that's what you're running: 32 | ``` 33 | apt-get install python-bs4 python-lxml python-cssutils 34 | ``` 35 | 36 | If any of these is missing, the script will fall back to a JavaScript method for restructuring the page's HTML elements (which won't run until after the page's content has loaded in the browser). This works fine, but the visible delay before layout is re-shuffled might be undesirable. 37 | 38 | ## command arguments 39 | 40 | * `` (**required**) - the name of the PICO-8 web player HTML page to transform 41 | * `--lazy`, `-l` - (**optional**) - a flag that, if included, will instruct Python ***not*** to try restructuring the HTML page before saving, and instead include JavaScript that will restructure the page after the user has loaded the page in their browser. 42 | 43 | ## what this script does 44 | 45 | CSS [flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Using_CSS_flexible_boxes) styling is provided, and the game canvas is told to take up to 2X the standard PICO-8 pixel dimensions (the canvas will be 290px, 580px, or 1160px wide dependending on available horizontal and vertical real estate). 46 | 47 | It's best to stick to these three resolutions (multiples of the base resolution) in order for rendering to look nice. 48 | 49 | ## build 50 | 51 | To build `transform.py` from source, run: 52 | 53 | ``` 54 | # works in a UNIX shell environment 55 | chmod +x build 56 | ./build 57 | ``` 58 | 59 | or: 60 | 61 | ``` 62 | python build.py 63 | ``` 64 | -------------------------------------------------------------------------------- /src/transform.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # PICO-8 Responsive Web Player Transform 4 | 5 | 6 | import argparse 7 | 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument( 10 | '-l', 11 | '--lazy', 12 | action='store_true', 13 | help='If this flag is provided, we won\'t try using Python to ' + 14 | 'restructure HTML document and will always insert JavaScript ' + 15 | 'to be run on page load instead.', 16 | default=False 17 | ) 18 | parser.add_argument( 19 | 'filename', 20 | help='The name of the HTML file to be transformed.' 21 | ) 22 | args = vars(parser.parse_args()) 23 | 24 | 25 | def include_in_head (tag, html): 26 | html_parts = html.split('') 27 | return ''.join([html_parts[0] + tag] + html_parts[1:]) 28 | 29 | def override_styles (html): 30 | styles = """[CSS]""" 31 | style_tag = '\n' 32 | return include_in_head(style_tag, html) 33 | 34 | def javascript_restructure (html): 35 | script = """[JS]""" 36 | onload_script = 'window.onload = function () {\n\n' + script + '\n};\n' 37 | script_tag = '\n' 38 | return include_in_head(script_tag, html) 39 | 40 | def soup_restructure (html): 41 | soup = BeautifulSoup(html, 'lxml') 42 | 43 | # purge all br elements 44 | [br.extract() for br in soup('br')] 45 | 46 | # get rid of center elements and keep contents 47 | [center.unwrap() for center in soup('center')] 48 | 49 | # add pico8_container class to container element 50 | container = soup.find(id='canvas').parent 51 | container_class = 'pico8_container' 52 | existing_class = str(container.get('class') or '') 53 | container['class'] = ( 54 | existing_class.replace(container_class, '') + 55 | ' ' + container_class 56 | ).strip() 57 | 58 | # remove element-specific width from container element 59 | container_style = cssutils.parseStyle(container.get('style')) 60 | if container_style: 61 | container_style.setProperty('width', None) 62 | new_style = container_style.cssText.replace('\n', '') 63 | if len(new_style): 64 | container['style'] = new_style 65 | else: 66 | del container['style'] 67 | 68 | # move pico 8 options to new options container element 69 | options = soup(class_='pico8_el') 70 | if len(options): 71 | options_container = soup.new_tag('div') 72 | options_container['class'] = 'pico8_options' 73 | options[0].insert_before(options_container) 74 | for option in options: 75 | options_container.append(option) 76 | 77 | return soup.prettify() 78 | 79 | 80 | restructure = javascript_restructure 81 | if not args['lazy']: 82 | soup_available = True 83 | lxml_available = True 84 | cssutils_available = True 85 | try: 86 | from bs4 import BeautifulSoup 87 | except: 88 | soup_available = False 89 | try: 90 | import lxml 91 | except: 92 | lxml_available = False 93 | try: 94 | import cssutils 95 | except: 96 | cssutils_available = False 97 | 98 | if not (soup_available and lxml_available and cssutils_available): 99 | if not soup_available: 100 | print('[WARNING]: "beautifulsoup4" package unavailable.') 101 | if not lxml_available: 102 | print('[WARNING]: "lxml" package unavailable.') 103 | if not cssutils_available: 104 | print('[WARNING]: "cssutils" package unavailable.') 105 | print('Using JavaScript (to be run in browser) as restructuring fallback.') 106 | else: 107 | restructure = soup_restructure 108 | 109 | 110 | filename = args['filename'] 111 | f = open(filename) 112 | 113 | html = f.read().replace('', '') 114 | 115 | transformed_html = restructure(override_styles(html)) 116 | 117 | new_filename = filename.replace('.html', '-responsive.html') 118 | t = open(new_filename, 'w') 119 | t.write(transformed_html) 120 | 121 | f.close() 122 | t.close() 123 | -------------------------------------------------------------------------------- /transform.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # PICO-8 Responsive Web Player Transform 4 | 5 | 6 | import argparse 7 | 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument( 10 | '-l', 11 | '--lazy', 12 | action='store_true', 13 | help='If this flag is provided, we won\'t try using Python to ' + 14 | 'restructure HTML document and will always insert JavaScript ' + 15 | 'to be run on page load instead.', 16 | default=False 17 | ) 18 | parser.add_argument( 19 | 'filename', 20 | help='The name of the HTML file to be transformed.' 21 | ) 22 | args = vars(parser.parse_args()) 23 | 24 | 25 | def include_in_head (tag, html): 26 | html_parts = html.split('') 27 | return ''.join([html_parts[0] + tag] + html_parts[1:]) 28 | 29 | def override_styles (html): 30 | styles = """.pico8_container { 31 | display: -webkit-box; 32 | display: -webkit-flex; 33 | display: -ms-flexbox; 34 | display: flex; 35 | -webkit-box-orient: vertical; 36 | -webkit-box-direction: normal; 37 | -webkit-flex-direction: column; 38 | -ms-flex-direction: column; 39 | flex-direction: column; 40 | -webkit-box-align: center; 41 | -webkit-align-items: center; 42 | -ms-flex-align: center; 43 | align-items: center; 44 | } 45 | 46 | canvas#canvas { 47 | margin-top: 10px; 48 | width: 290px; 49 | height: 270px; 50 | } 51 | 52 | @media screen and (min-width: 580px) and (min-height: 600px) { 53 | canvas#canvas { 54 | margin-top: 20px; 55 | width: 580px; 56 | height: 540px; 57 | } 58 | } 59 | 60 | @media screen and (min-width: 1160px) and (min-height: 1150px) { 61 | canvas#canvas { 62 | margin-top: 30px; 63 | width: 1160px; 64 | height: 1080px; 65 | } 66 | } 67 | 68 | .pico8_options { 69 | display: -webkit-box; 70 | display: -webkit-flex; 71 | display: -ms-flexbox; 72 | display: flex; 73 | -webkit-box-pack: center; 74 | -webkit-justify-content: center; 75 | -ms-flex-pack: center; 76 | justify-content: center; 77 | -webkit-flex-wrap:wrap; 78 | -ms-flex-wrap:wrap; 79 | flex-wrap:wrap 80 | } 81 | 82 | .pico8_el { 83 | float: initial; 84 | display: initial; 85 | } 86 | """ 87 | style_tag = '\n' 88 | return include_in_head(style_tag, html) 89 | 90 | def javascript_restructure (html): 91 | script = """// purge all br elements 92 | var brs = document.querySelectorAll('br'); 93 | Array.prototype.forEach.call(brs, function (br) { 94 | br.parentNode.removeChild(br); 95 | }); 96 | 97 | // unwrap center elements 98 | var centerElems = document.querySelectorAll('center'); 99 | Array.prototype.forEach.call(centerElems, function (elem) { 100 | var parent = elem.parentNode; 101 | var children = elem.childNodes; 102 | for (var i = children.length - 1; i >= 0; i--) { 103 | parent.insertBefore(children[i], elem); 104 | } 105 | parent.removeChild(elem); 106 | }); 107 | 108 | // add pico8_container class to container element 109 | var container = document.getElementById('canvas').parentNode; 110 | var containerClassName = 'pico8_container'; 111 | if (container.classList) { 112 | container.classList.add(containerClassName); 113 | } else { 114 | container.className = 115 | container.className.replace(containerClassName, '') + 116 | ' ' + containerClassName; 117 | } 118 | 119 | // remove element-specific width from container element 120 | container.style.width = ''; 121 | 122 | // move pico 8 options to new options container element 123 | var options = document.getElementsByClassName('pico8_el'); 124 | if (options.length) { 125 | var optionsContainer = document.createElement('div'); 126 | optionsContainer.className = 'pico8_options'; 127 | container.insertBefore(optionsContainer, options[0]); 128 | Array.prototype.forEach.call(options, function (option) { 129 | optionsContainer.appendChild(option); 130 | }); 131 | } 132 | """ 133 | onload_script = 'window.onload = function () {\n\n' + script + '\n};\n' 134 | script_tag = '\n' 135 | return include_in_head(script_tag, html) 136 | 137 | def soup_restructure (html): 138 | soup = BeautifulSoup(html, 'lxml') 139 | 140 | # purge all br elements 141 | [br.extract() for br in soup('br')] 142 | 143 | # get rid of center elements and keep contents 144 | [center.unwrap() for center in soup('center')] 145 | 146 | # add pico8_container class to container element 147 | container = soup.find(id='canvas').parent 148 | container_class = 'pico8_container' 149 | existing_class = str(container.get('class') or '') 150 | container['class'] = ( 151 | existing_class.replace(container_class, '') + 152 | ' ' + container_class 153 | ).strip() 154 | 155 | # remove element-specific width from container element 156 | container_style = cssutils.parseStyle(container.get('style')) 157 | if container_style: 158 | container_style.setProperty('width', None) 159 | new_style = container_style.cssText.replace('\n', '') 160 | if len(new_style): 161 | container['style'] = new_style 162 | else: 163 | del container['style'] 164 | 165 | # move pico 8 options to new options container element 166 | options = soup(class_='pico8_el') 167 | if len(options): 168 | options_container = soup.new_tag('div') 169 | options_container['class'] = 'pico8_options' 170 | options[0].insert_before(options_container) 171 | for option in options: 172 | options_container.append(option) 173 | 174 | return soup.prettify() 175 | 176 | 177 | restructure = javascript_restructure 178 | if not args['lazy']: 179 | soup_available = True 180 | lxml_available = True 181 | cssutils_available = True 182 | try: 183 | from bs4 import BeautifulSoup 184 | except: 185 | soup_available = False 186 | try: 187 | import lxml 188 | except: 189 | lxml_available = False 190 | try: 191 | import cssutils 192 | except: 193 | cssutils_available = False 194 | 195 | if not (soup_available and lxml_available and cssutils_available): 196 | if not soup_available: 197 | print('[WARNING]: "beautifulsoup4" package unavailable.') 198 | if not lxml_available: 199 | print('[WARNING]: "lxml" package unavailable.') 200 | if not cssutils_available: 201 | print('[WARNING]: "cssutils" package unavailable.') 202 | print('Using JavaScript (to be run in browser) as restructuring fallback.') 203 | else: 204 | restructure = soup_restructure 205 | 206 | 207 | filename = args['filename'] 208 | f = open(filename) 209 | 210 | html = f.read().replace('', '') 211 | 212 | transformed_html = restructure(override_styles(html)) 213 | 214 | new_filename = filename.replace('.html', '-responsive.html') 215 | t = open(new_filename, 'w') 216 | t.write(transformed_html) 217 | 218 | f.close() 219 | t.close() 220 | --------------------------------------------------------------------------------