Commit 0abb5e6a authored by Nathan Pasko's avatar Nathan Pasko
Browse files

Initial commit

parents
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="description" content="Tunnel is a temporally-unopinionated audio/video engine for the web." />
<meta charset="utf-8">
<title></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="">
<link rel="stylesheet" href="tunnel-style.css">
</head>
<body>
<!--
Welcome to
TUNNEL
engine
===========================
Tunnel v.0.8.1
◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎
-> Load cartridge below! <-
◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎
User's Manual & more at
tunnelengine.netlify.app
===========================
-->
<!-- ARCHITECTURE -->
<div id="startup-logo">
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 396.6 396.6"><defs><style>.cls-1{fill:#ff0;}.cls-2{fill:blue;}</style></defs><g id="tunnel"><path d="M94.2,98.8H69.3v65.4h-36V98.8H6V63.4H94.2Z"/><path class="cls-1" d="M69.3,164.2V267.6h-36V164.2"/><path d="M241.3,267.6h77v65.3h72.3V297.5H354.3V232.1H304.4V210.6H276.7v-8.4h41.6v-38h-77V60.8h-36V112l-52-51.2V164.2h52v51.2l-52-51.2H116.7v65.6H105.9V164.2H69.3V217c0,17.2,0,49.6,42.1,49.6s41.9-32.4,41.9-49.6h0v50.6"/></g><g id="engine"><path class="cls-2" d="M126.7,310.7v.2c0,5.3,3.8,8.6,8.1,8.6a8.6,8.6,0,0,0,5.9-2.3l.6.8a9.5,9.5,0,0,1-6.5,2.5c-4.9,0-9-3.6-9-9.5s3.9-9.3,8.4-9.3,7.4,3.3,7.4,7.7v1.3Zm.1-1h13.8c0-5.2-3.1-7.1-6.4-7.1S127.3,304.8,126.8,309.7Z"/><path class="cls-2" d="M154.7,302.1h.9v3.5a8.9,8.9,0,0,1-.1,1.3h0a8,8,0,0,1,7.8-5.2c4.5,0,6.2,2.4,6.2,7.1v11.3h-1V309c0-3.5-.8-6.4-5.2-6.4a7.6,7.6,0,0,0-7.4,5.8,9.3,9.3,0,0,0-.3,2.4v9.3h-.9Z"/><path class="cls-2" d="M184.6,325.2a10.1,10.1,0,0,0,5.1,1.3c3.7,0,7-1.9,7-6.5v-2.7a6.1,6.1,0,0,1,.2-1.5h-.1a6.7,6.7,0,0,1-6.4,4.3c-4.7,0-8-3.7-8-9.4s3-9,7.8-9c3,0,5.8,1.4,6.6,4.5h.1a3.1,3.1,0,0,1-.2-1v-3.1h1V320c0,5.4-3.8,7.5-8,7.5A12.7,12.7,0,0,1,184,326Zm12.2-14.4c0-6.2-3-8.2-6.6-8.2s-6.8,3-6.8,8.1,2.8,8.4,7,8.4S196.8,317,196.8,310.8Z"/><path class="cls-2" d="M212,294.8h1v3.4h-1Zm0,7.3h1v18h-1Z"/><path class="cls-2" d="M227.3,302.1h1v3.5a4.6,4.6,0,0,1-.2,1.3h.1a8,8,0,0,1,7.8-5.2c4.5,0,6.1,2.4,6.1,7.1v11.3h-.9V309c0-3.5-.9-6.4-5.2-6.4a7.5,7.5,0,0,0-7.4,5.8,9.3,9.3,0,0,0-.3,2.4v9.3h-1Z"/><path class="cls-2" d="M255.9,310.7v.2c0,5.3,3.8,8.6,8.1,8.6a8.6,8.6,0,0,0,5.9-2.3l.6.8a9.5,9.5,0,0,1-6.5,2.5c-4.9,0-9-3.6-9-9.5s3.9-9.3,8.4-9.3,7.4,3.3,7.4,7.7v1.3Zm.1-1h13.8c0-5.2-3.1-7.1-6.4-7.1S256.5,304.8,256,309.7Z"/></g></svg>
</div>
<div id="no-cart" class="hiding">
<h1>No Cartridge</h1>
<h3>Load a Tunnel cartridge into this file and refresh.</h3>
<h4>User's Manual & more at</h4>
<a href="https://tunnelengine.netlify.app">tunnelengine.netlify.app</a>
</div>
<div id="corner-logo" class="hiding">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 259.2 259.2"><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M129.6,0A129.6,129.6,0,1,0,259.2,129.6,129.6,129.6,0,0,0,129.6,0Zm61.8,199.6V166h-85V140c0,8.8.1,25.5-21.5,25.5S63.2,148.8,63.2,140V112.8H44.7V79.1H30.6V60.9H76V79.1H63.2v33.7H82v33.8h5.6V112.8h18.8V59.6l26.8,26.3V59.6h18.5v53.2h39.7v19.6H170v4.3h14.2v11h25.7v33.7h18.7v18.2Z"/><polygon points="133.2 112.8 106.4 112.8 133.2 139.2 133.2 112.8"/></g></g></svg>
</div>
<header id="main-header" class="hiding">
<span id="title" class="cursor-default"></span>
<nav></nav>
</header>
<div id="ios">
<p>iOS Turn Ringer On</p>
</div>
<div id="controls">
<button id="outward-btn" aria-label="Outward"></button>
<button id="inward-btn" aria-label="Inward"></button>
</div>
<div id="frame"></div>
<!--
◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼◼︎◼︎
LOAD CATRIDGE HERE
-->
<!--
CARTRIDGE ABOVE
◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼◼︎◼︎
-->
<!-- TUNNEL ENGINE CORE -->
<script src="tunnel-main.js"></script>
<!-- PLEASE DO NOT DAMAGE ARCHITECTURE OR CORE. -->
</body>
\ No newline at end of file
# Tunnel
A temporally-unopinionated audio/video engine for the web.
**Tunnel v.0.8.1**
\ No newline at end of file
// Tunnel Engine Core
const position = {
inner: 'INNER',
focused: 'FOCUSED',
outer: 'OUTER'
}
const sides = [ 'top', 'right', 'bottom', 'left' ];
var cartridge;
var sources = [];
var currentLayer = 1;
var controlContext;
var controlOsc;
var frame;
var activeLayers = [];
// Initialization
function initNav() {
const nav = document.querySelector('header nav');
cartridge.info.nav.forEach(i => {
const elem = getNavElement(i);
nav.appendChild(elem);
});
}
function initHeader() {
if (cartridge.info.labels) {
setTimeout(() => {
document.getElementById('corner-logo').classList.add('active');
//}, 1500);
}, 1250);
document.getElementById('startup-logo').classList.add('extended');
//setTimeout(clearStartupLogo, 3500);
setTimeout(clearStartupLogo, 2500);
const title = document.getElementById('title');
title.textContent = cartridge.info.title;
if (cartridge.info.nav.length > 0)
initNav();
setTimeout(() => {
document.getElementById('main-header').classList.add('active');
//}, 2000);
}, 1500);
}
else {
setTimeout(() => {
document.getElementById('corner-logo').classList.add('active');
//}, 1500);
}, 600);
//document.getElementById('corner-logo').remove();
//setTimeout(clearStartupLogo, 2500);
setTimeout(clearStartupLogo, 1250);
document.getElementById('main-header').remove();
}
}
function initFrame() {
frame = document.getElementById('frame');
while (cartridge.layers.length < 3) {
addBlankLayer();
}
createLayer(cartridge.layers[2], position.inner, 2);
createLayer(cartridge.layers[1], position.focused, 1);
createLayer(cartridge.layers[0], position.outer, 0);
}
function initButtons() {
document.getElementById('inward-btn').addEventListener('click', () => {
moveLayer(1);
updateDisplay();
});
document.getElementById('outward-btn').addEventListener('click', () => {
moveLayer(-1);
updateDisplay();
});
}
function initIOS(val) {
var iosElem = document.getElementById('ios');
if (val) {
iosElem.classList.add('active');
window.setTimeout(() => {
iosElem.classList.remove('active');
}, 2000);
window.setTimeout(() => {
iosElem.remove();
}, 5000);
}
else {
iosElem.remove();
}
}
function loadCartridge(cart) {
// Grab the cartridge data
cartridge = cart;
// Set document title to cartridge title
document.title = cartridge.info.title;
}
function init() {
// Check for iOS & initialize if necessary
initIOS(iOS());
// Check whether a cartridge is loaded
if (detectCartridge()) {
// Fully initialize & play cartridge
initHeader();
initFrame();
initButtons();
updateDisplay();
}
else {
// No cartridge
setTimeout(clearStartupLogo, 3500);
// Show No Cartridge message
document.getElementById('no-cart').classList.add('active');
// Remove controls
document.getElementById('controls').remove();
}
}
// Main controls
function getNavElement(data) {
var elem = document.createElement('a');
elem.textContent = data.text;
elem.setAttribute('href', data.href);
elem.classList.add('prevent-select');
return elem;
}
function getControlWidgets() {
// create control widgets div with 4 children
const widgets = document.createElement('div');
widgets.classList.add('widgets');
for (var i = 0; i < 4; i++) {
const w = document.createElement('div');
widgets.appendChild(w);
}
return widgets;
}
function getControlLayerElem() {
const elem = document.createElement('div');
elem.classList.add('control')
// control layer setup
const cross1 = document.createElement('div');
cross1.classList.add('cross');
elem.appendChild(cross1);
const widgets = getControlWidgets();
elem.appendChild(widgets);
const cross2 = document.createElement('div');
cross2.classList.add('cross');
elem.appendChild(cross2);
const logo = document.createElement('img');
logo.src = 'logo-sm.svg';
elem.appendChild(logo);
return elem;
}
function createSlide(layer, index, control = false) {
var slide = document.createElement('div');
slide.classList.add('slide');
slide.classList.add(sides[index]);
/*
if (layer.background)
slide.style.backgroundColor = layer.background;*/
if (layer.images && layer.images[index].length > 0) {
slide.style.backgroundImage = `url(${layer.images[index]})`;
}
if (control) {
const elem = getControlLayerElem();
slide.appendChild(elem);
}
return slide;
}
function addAudioSource(layerId, url) {
// audio context fallback for legacy browsers
const AudioContext = window.AudioContext || window.webkitAudioContext;
// Create an audio context
const audioContext = new AudioContext();
// create buffer type audio source
var source = audioContext.createBufferSource();
// Load the buffer
// create Request
var request = new XMLHttpRequest();
// open the request -- type get, async true
request.open('GET', url, true);
//webaudio parameters
request.responseType = 'arraybuffer';
// once the request is completed...
request.onload = function() {
audioContext.decodeAudioData(request.response, function(res) {
// set up and play source after buffer has loaded
source.buffer = res;
source.loop = true;
source.start(0);
}, function() {
console.error('The request failed.');
});
};
// send request
request.send();
// store audio context & source
var s = {
id: layerId,
audioContext: audioContext,
source: source
}
sources.push(s);
}
function createLayer(data, position, index) {
// Check whether this is the control layer
const control = index == 0 ? true : false;
// Apply layer data to new HTML element
var layer = document.createElement('div');
layer.id = data.id;
layer.classList.add('layer');
layer.dataset.position = position;
// Set up audio if necessary
if (data.audio) {
addAudioSource(data.id, data.audio);
}
// Create four slides
for (var i = 0; i < 4; i++) {
var slide = createSlide(data, i, control);
layer.appendChild(slide);
}
// Report layer to array & DOM
frame.appendChild(layer);
activeLayers.push(layer);
}
function removeLayer(position) {
// try to find active layer to remove
var toRemove = activeLayers.findIndex(layer => layer.dataset.position == position);
if (toRemove >= 0) {
// remove from script and DOM
var removedElements = activeLayers.splice(toRemove, 1);
document.getElementById(removedElements[0].id).remove();
removeSourceForId(removedElements[0].id);
return removedElements;
}
}
function removeSourceForId(id) {
var i = sources.findIndex(s => s.id == id);
if (i >= 0) {
sources.splice(i, 1);
}
}
function moveLayer(val) {
if ((currentLayer + val) >= 0 && (currentLayer + val) < cartridge.layers.length) {
currentLayer = currentLayer + val;
// move inward
if (val > 0) {
var removed = removeLayer(position.outer);
// set data sets via activeLayers
// find x
var xIndex = activeLayers.findIndex(l => l.dataset.position == position.inner);
// find y
var yIndex = activeLayers.findIndex(l => l.dataset.position == position.focused);
// set x
if (xIndex >= 0) {
activeLayers[xIndex].dataset.position = position.focused;
updateBackground();
tapAudio(activeLayers[xIndex], false);
}
// set y
if (yIndex >= 0) {
activeLayers[yIndex].dataset.position = position.outer;
tapAudio(activeLayers[yIndex], true, (currentLayer == 1));
}
// create next layer
if (cartridge.layers.length > (currentLayer + 1))
createLayer(cartridge.layers[currentLayer + 1], position.inner, (currentLayer + 1));
}
// move outward
else {
var removed = removeLayer(position.inner);
// set data sets via activeLayers
// find x
var xIndex = activeLayers.findIndex(l => l.dataset.position == position.outer);
// find y
var yIndex = activeLayers.findIndex(l => l.dataset.position == position.focused);
// set x
if (xIndex >= 0) {
activeLayers[xIndex].dataset.position = position.focused;
updateBackground();
tapAudio(activeLayers[xIndex], false, (currentLayer == 0));
}
// set y
if (yIndex >= 0) {
activeLayers[yIndex].dataset.position = position.inner;
tapAudio(activeLayers[yIndex], true);
}
// create next layer
if (currentLayer > 0)
createLayer(cartridge.layers[currentLayer - 1], position.outer, (currentLayer - 1));
}
}
}
function setControlOscillator(val) {
// Control Start
if (val) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
controlContext = new AudioContext();
controlOsc = controlContext.createOscillator();
controlOsc.frequency.setValueAtTime(211.44, controlContext.currentTime);
controlOsc.connect(controlContext.destination);
controlOsc.start();
}
// Control Stop
else {
controlContext.suspend();
controlOsc.stop();
controlOsc.disconnect(controlContext.destination);
controlOsc = null;
}
}
function tapAudio(elem, stop = false, control = false) {
// try to find source with ID
var i = sources.findIndex(s => s.id == parseInt(elem.id));
if (i >= 0) {
// stop audio
if (stop) {
sources[i].source.disconnect();
sources[i].audioContext.suspend();
if (control == true) {
setControlOscillator(false);
}
}
// start audio
else {
sources[i].source.connect(sources[i].audioContext.destination);
sources[i].audioContext.resume();
if (control == true) {
setControlOscillator(true);
}
}
}
}
// Utility
function iOS() {
return [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
// iPad on iOS 13 detection
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
}
function detectCartridge() {
var val = false;
// Look for javascript cartridge
if (typeof info != 'undefined') {
val = true;
const cart = {
info: info,
layers: layers
};
loadCartridge(cart);
}
// If that didn't work, try JSON
else {
const cartridgeElem = document.getElementById('cartridge');
if (cartridgeElem) {
const j = JSON.parse(cartridgeElem.innerText);
if (typeof j.info != 'undefined') {
val = true;
const cart = {
info: j.info,
layers: j.layers
};
loadCartridge(cart);
}
}
}
return val;
}
function clearStartupLogo() {
document.getElementById('startup-logo').remove();
}
function addBlankLayer() {
console.log('add blank layer');
cartridge.layers.push(
{
id: (Math.random()*100),
name: '',
images: [ '', '', '', '' ]
}
);
}
function setLayerPosition(element, val) {
element.dataset.position = val;
}
// Update
function updateBackground() {
if (layers[currentLayer].background != undefined) {
frame.style.background = layers[currentLayer].background;
}
else {
frame.style.background = 'unset';
}
}
function updateDisplay() {
//updateLayerDisplay();
}
init();
\ No newline at end of file
:root {
--white: #fff;
--black: #111;
--yellow: #ff0;
--blue: #00f;
--greyTrans: #8a8a8a8a;
}
html {
height: -webkit-fill-available;
background: var(--white);
color: var(--black);
font-family: sans-serif;
transition: background .25s ease-in-out;
}
body {
min-height: 100vh;
min-height: -webkit-fill-available;
}
* {
position: relative;
margin: 0;
padding: 0;
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
button {
background: var(--white);
color: var(--black);
font-size: 1rem;
border: none;
border-radius: 4px;
}
button:active {
background: var(--black);
}
nav a, nav span {
margin: .25rem;
}
.hiding {
pointer-events: none;
opacity: 0;
transition: opacity 1s;
}
.hiding.active {
pointer-events: all;
opacity: 1;
}
.prevent-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.cursor-default {
cursor: default;
}
.control {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
min-height: 100vh;
min-height: -webkit-fill-available;
width: 100vw;
}
.control h1, .control h3, .control p, .control img {
background: var(--white);
}
.control h1, .control h3, .control p {
padding: .25rem;
font-family: monospace;
font-weight: 100;
text-align: center;
line-height: 2;
background: var(--white);
letter-spacing: .25em;
}
.control h1, .control h3 {
font-size: 1rem;
text-transform: uppercase;
}
.control p {
font-size: .8rem;
}
@media (min-width: 768px) {
.control h1, .control h3, .control p {
letter-spacing: 1em;
}
.control h1 {
font-size: 1.75rem
}
.control p {
font-size: 1rem;
}
.control h3 {
font-size: 1.75rem;
}
}
.control .cross {
position: relative;
height: 10vw;
width: 1px;
background: var(--black);
overflow: visible;
}
.control .cross:after {
content: "";
position: absolute;
top: calc(5vw - 1px);
left: calc(-5vw + 1px);
height: 1px;
width: 10vw;
background: var(--black);
}
.control .widgets {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
height: 15vh;
width: 15vw;
}
.control .widgets div:nth-child(1) {
background: var(--white);
}
.control .widgets div:nth-child(2) {
background: var(--yellow);
}
.control .widgets div:nth-child(3) {