Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217,601 changes: 217,601 additions & 0 deletions build/embedded_html_templates.h

Large diffs are not rendered by default.

67 changes: 64 additions & 3 deletions build/html/export.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,89 @@

</div>

<canvas style="width: 100%; height: 100%; margin: 0 auto; display: block; image-rendering: pixelated;" id="canvas" oncontextmenu="event.preventDefault()" onmousedown="window.focus()"></canvas>
<canvas style="width: 100%; height: 100%; margin: 0 auto; display: block; image-rendering: pixelated;" id="canvas" oncontextmenu="event.preventDefault()" onmousedown="window.focus()" tabindex="0"></canvas>
</div>

<script type="text/javascript">
var Module = {canvas: document.getElementById('canvas'), arguments:['cart.tic']}
var Module = {canvas: document.getElementById('canvas'), arguments:['cart.tic']};

const gameFrame = document.getElementById('game-frame')
// Safari/WebKit-only: remap Arrow keys with location 3 (numpad) to location 0.
(function(){
const ua = navigator.userAgent || '';
const isWebKit = /AppleWebKit/i.test(ua);
if (!isWebKit) return;

const synthesizeArrowWithLocation0 = (orig) => {
try {
const ev = new KeyboardEvent(orig.type, {
key: orig.key,
code: orig.code,
location: 0,
repeat: orig.repeat,
ctrlKey: orig.ctrlKey,
shiftKey: orig.shiftKey,
altKey: orig.altKey,
metaKey: orig.metaKey,
bubbles: true,
cancelable: true,
});
Object.defineProperty(ev, 'which', { get: () => orig.which });
Object.defineProperty(ev, 'keyCode', { get: () => orig.keyCode });
return ev;
} catch { return null; }
};

const remapArrowLocationCapture = (e) => {
const isArrow = (e.key && e.key.startsWith('Arrow')) || (e.code && e.code.startsWith('Arrow'));
if (!isArrow) return;
if (e.location === 3) {
const syn = synthesizeArrowWithLocation0(e);
if (syn) {
e.stopImmediatePropagation();
e.preventDefault();
(e.target || document.getElementById('canvas')).dispatchEvent(syn);
}
}
};

['keydown','keyup'].forEach(t => document.addEventListener(t, remapArrowLocationCapture, true));
})();

// Prevent page scrolling while the game canvas is focused
(function(){
const canvasEl = document.getElementById('canvas');
const preventArrowDefault = (e) => {
const key = e.key || e.code;
const kc = e.keyCode || e.which;
const isArrow = (key && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(key)) || (kc >= 37 && kc <= 40);
if (isArrow && document.activeElement === canvasEl) e.preventDefault();
};
document.addEventListener('keydown', preventArrowDefault, { passive: false });
canvasEl.addEventListener('keydown', preventArrowDefault, { passive: false });
})();

const gameFrame = document.getElementById('game-frame');
const displayStyle = window.getComputedStyle(gameFrame).display;

if (displayStyle === 'none')
{
let scriptTag = document.createElement('script'), // create a script tag
firstScriptTag = document.getElementsByTagName('script')[0]; // find the first script tag in the document
scriptTag.src = 'tic80.js'; // set the source of the script to your script
scriptTag.type = 'text/javascript'; // explicitly set as regular script, not module
firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); // append the script to the DOM
// Focus canvas to ensure keyboard events are routed to the game
document.getElementById('canvas').focus()
} else {
gameFrame.addEventListener('click', function() {
let scriptTag = document.createElement('script'), // create a script tag
firstScriptTag = document.getElementsByTagName('script')[0]; // find the first script tag in the document
scriptTag.src = 'tic80.js'; // set the source of the script to your script
scriptTag.type = 'text/javascript'; // explicitly set as regular script, not module
firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); // append the script to the DOM
this.remove()
// Focus canvas to ensure keyboard events are routed to the game
document.getElementById('canvas').focus()
});
}
</script>
Expand Down
61 changes: 59 additions & 2 deletions build/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

</div>

<canvas style="width: 100%; height: 100%; margin: 0 auto; display: block; image-rendering: pixelated;" id="canvas" oncontextmenu="event.preventDefault()" onmousedown="window.focus()"></canvas>
<canvas style="width: 100%; height: 100%; margin: 0 auto; display: block; image-rendering: pixelated;" id="canvas" oncontextmenu="event.preventDefault()" onmousedown="window.focus()" tabindex="0"></canvas>
</div>

<div id="add-modal" class="modal">
Expand All @@ -43,7 +43,62 @@
</div>

<script type="text/javascript">
var Module = {canvas: document.getElementById('canvas')}
var Module = {canvas: document.getElementById('canvas')};

// Safari/WebKit-only: remap Arrow keys with location 3 (numpad) to location 0.
(function(){
const ua = navigator.userAgent || '';
const isWebKit = /AppleWebKit/i.test(ua);
if (!isWebKit) return;

const synthesizeArrowWithLocation0 = (orig) => {
try {
const ev = new KeyboardEvent(orig.type, {
key: orig.key,
code: orig.code,
location: 0,
repeat: orig.repeat,
ctrlKey: orig.ctrlKey,
shiftKey: orig.shiftKey,
altKey: orig.altKey,
metaKey: orig.metaKey,
bubbles: true,
cancelable: true,
});
Object.defineProperty(ev, 'which', { get: () => orig.which });
Object.defineProperty(ev, 'keyCode', { get: () => orig.keyCode });
return ev;
} catch { return null; }
};

const remapArrowLocationCapture = (e) => {
const isArrow = (e.key && e.key.startsWith('Arrow')) || (e.code && e.code.startsWith('Arrow'));
if (!isArrow) return;
if (e.location === 3) {
const syn = synthesizeArrowWithLocation0(e);
if (syn) {
e.stopImmediatePropagation();
e.preventDefault();
(e.target || document.getElementById('canvas')).dispatchEvent(syn);
}
}
};

['keydown','keyup'].forEach(t => document.addEventListener(t, remapArrowLocationCapture, true));
})();

// Prevent page scrolling while the game canvas is focused
(function(){
const canvasEl = document.getElementById('canvas');
const preventArrowDefault = (e) => {
const key = e.key || e.code;
const kc = e.keyCode || e.which;
const isArrow = (key && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(key)) || (kc >= 37 && kc <= 40);
if (isArrow && document.activeElement === canvasEl) e.preventDefault();
};
document.addEventListener('keydown', preventArrowDefault, { passive: false });
canvasEl.addEventListener('keydown', preventArrowDefault, { passive: false });
})();

const gameFrame = document.getElementById('game-frame')

Expand All @@ -53,6 +108,8 @@
scriptTag.src = 'tic80.js'; // set the source of the script to your script
firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); // append the script to the DOM
this.remove()
// Focus canvas to ensure keyboard events are routed to the game
document.getElementById('canvas').focus()
});
</script>
</body>
Expand Down
57 changes: 56 additions & 1 deletion build/webapp/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,62 @@
<script type="module">
import startTic80 from './tic80.js'

navigator.serviceWorker.register('serviceworker.js')
navigator.serviceWorker.register('serviceworker.js');

// Safari/WebKit-only: remap Arrow keys with location 3 (numpad) to location 0.
(function(){
const ua = navigator.userAgent || '';
const isWebKit = /AppleWebKit/i.test(ua);
if (!isWebKit) return;

const synthesizeArrowWithLocation0 = (orig) => {
try {
const ev = new KeyboardEvent(orig.type, {
key: orig.key,
code: orig.code,
location: 0,
repeat: orig.repeat,
ctrlKey: orig.ctrlKey,
shiftKey: orig.shiftKey,
altKey: orig.altKey,
metaKey: orig.metaKey,
bubbles: true,
cancelable: true,
});
Object.defineProperty(ev, 'which', { get: () => orig.which });
Object.defineProperty(ev, 'keyCode', { get: () => orig.keyCode });
return ev;
} catch { return null; }
};

const remapArrowLocationCapture = (e) => {
const isArrow = (e.key && e.key.startsWith('Arrow')) || (e.code && e.code.startsWith('Arrow'));
if (!isArrow) return;
if (e.location === 3) {
const syn = synthesizeArrowWithLocation0(e);
if (syn) {
e.stopImmediatePropagation();
e.preventDefault();
(e.target || document.getElementById('canvas')).dispatchEvent(syn);
}
}
};

['keydown','keyup'].forEach(t => document.addEventListener(t, remapArrowLocationCapture, true));
})();

// Prevent page scrolling while the canvas is focused
(function(){
const canvasEl = document.getElementById('canvas');
const preventArrowDefault = (e) => {
const key = e.key || e.code;
const kc = e.keyCode || e.which;
const isArrow = (key && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(key)) || (kc >= 37 && kc <= 40);
if (isArrow && document.activeElement === canvasEl) e.preventDefault();
};
document.addEventListener('keydown', preventArrowDefault, { passive: false });
canvasEl.addEventListener('keydown', preventArrowDefault, { passive: false });
})();

const initialize = () => {
const canvasSelector = '#canvas'
Expand Down
28 changes: 27 additions & 1 deletion cmake/studio.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,35 @@ endif()

set(TIC80_OUTPUT tic80)

# Generate embedded HTML templates (optional feature)
option(BUILD_HTMLLOCAL_EXPORT "Enable htmllocal export with embedded templates" OFF)

if(BUILD_HTMLLOCAL_EXPORT)
# Check if web build artifacts exist (they should be pre-built)
if(EXISTS ${CMAKE_SOURCE_DIR}/build/html/export.html AND EXISTS ${CMAKE_SOURCE_DIR}/build/bin/tic80.js AND EXISTS ${CMAKE_SOURCE_DIR}/build/bin/tic80.wasm)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/embedded_html_templates.h
COMMAND ${CMAKE_COMMAND} -E echo "Generating embedded HTML templates..."
COMMAND python3 ${CMAKE_SOURCE_DIR}/tools/embed_html.py ${CMAKE_CURRENT_BINARY_DIR}/embedded_html_templates.h
DEPENDS ${CMAKE_SOURCE_DIR}/build/html/export.html ${CMAKE_SOURCE_DIR}/build/bin/tic80.js ${CMAKE_SOURCE_DIR}/build/bin/tic80.wasm
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Embedding HTML templates for offline export"
)
set(EMBEDDED_HTML_TEMPLATES ${CMAKE_CURRENT_BINARY_DIR}/embedded_html_templates.h)
message(STATUS "htmllocal export enabled - using pre-built web artifacts")
else()
message(FATAL_ERROR "BUILD_HTMLLOCAL_EXPORT is ON but web artifacts not found in build/ directory")
endif()
else()
# Create placeholder file
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/embedded_html_templates.h "// No embedded HTML templates - BUILD_HTMLLOCAL_EXPORT disabled\n")
set(EMBEDDED_HTML_TEMPLATES ${CMAKE_CURRENT_BINARY_DIR}/embedded_html_templates.h)
endif()

add_library(tic80studio STATIC
${TIC80STUDIO_SRC}
${CMAKE_SOURCE_DIR}/build/assets/cart.png.dat)
${CMAKE_SOURCE_DIR}/build/assets/cart.png.dat
${EMBEDDED_HTML_TEMPLATES})

target_include_directories(tic80studio
PRIVATE ${THIRDPARTY_DIR}/jsmn
Expand Down
67 changes: 67 additions & 0 deletions src/studio/screens/console.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
macro(rpi) \
macro(mac) \
macro(html) \
macro(htmllocal) \
macro(binary) \
macro(tiles) \
macro(sprites) \
Expand Down Expand Up @@ -2296,6 +2297,72 @@ static void onExport_html(Console* console, const char* param, const char* filen
exportGame(console, getFilename(filename, ".zip"), param, onHtmlExportGet, params);
}

/*
* HTML Export Options:
*
* export html <name> - Downloads HTML5 templates from remote server
* export htmllocal <name> - Uses HTML5 templates embedded in TIC-80 binary
*
* Both create similar ZIP files containing index.html, cart.tic, tic80.js, and tic80.wasm
*/
static void onExport_htmllocal(Console* console, const char* param, const char* filename, ExportParams params)
{
// Include embedded HTML templates
#include "embedded_html_templates.h"

// Check if this build supports htmllocal export
#ifdef embedded_html_template
// Embedded HTML export - use templates compiled into binary (offline)
tic_mem* tic = console->tic;
const char* zipFilename = getFilename(filename, ".zip");
const char* zipPath = tic_fs_path(console->fs, zipFilename);

// Create a new ZIP file and add the template as index.html
struct zip_t *zip = zip_open(zipPath, ZIP_DEFAULT_COMPRESSION_LEVEL, 'w');
if(zip)
{
// Add the HTML template as index.html
zip_entry_open(zip, "index.html");
zip_entry_write(zip, embedded_html_template, embedded_html_template_size);
zip_entry_close(zip);

// Add the cart data
void* cart = newCart();
if(cart)
{
s32 cartSize = tic_cart_save(&tic->cart, cart);
if(cartSize)
{
zip_entry_open(zip, "cart.tic");
zip_entry_write(zip, cart, cartSize);
zip_entry_close(zip);
}
free(cart);
}

// Add embedded tic80.js
zip_entry_open(zip, "tic80.js");
zip_entry_write(zip, embedded_tic80_js, embedded_tic80_js_size);
zip_entry_close(zip);

// Add embedded tic80.wasm
zip_entry_open(zip, "tic80.wasm");
zip_entry_write(zip, embedded_tic80_wasm, embedded_tic80_wasm_size);
zip_entry_close(zip);

zip_close(zip);
onFileExported(console, zipFilename, true);
}
else
{
printError(console, "can't create zip file");
}
#else
// htmllocal export not available in this build
printError(console, "htmllocal export not available - disabled at build time");
#endif
}

static void onExport_tiles(Console* console, const char* param, const char* filename, ExportParams params)
{
exportSprites(console, getFilename(filename, PngExt), getBank(console, params.bank)->tiles.data, params);
Expand Down
Loading
Loading