352 lines
12 KiB
JavaScript
352 lines
12 KiB
JavaScript
// Constants ///////////////////////////////////////////////////////////////////
|
|
const kDimensions = {
|
|
wheelSize: 96,
|
|
leverWidth: 98,
|
|
sausageHeight: 48,
|
|
};
|
|
const kState = {
|
|
waiting: 'waiting',
|
|
readyToGrate: 'readyToGrate',
|
|
readyToLoad: 'readyToLoad'
|
|
};
|
|
const kAction = {
|
|
completeLoading: Symbol('completeLoading'),
|
|
startPullingLever: Symbol('startPullingLever'),
|
|
stopPullingLever: Symbol('stopPullingLever'),
|
|
pullLever: Symbol('pullLever'),
|
|
startTurningWheel: Symbol('startTurningWheel'),
|
|
stopTurningWheel: Symbol('stopTurningWheel'),
|
|
turnWheel: Symbol('turnWheel'),
|
|
setIsWaitingForFrame: Symbol('setIsWaitingForFrame'),
|
|
createSausage: Symbol('createSausage'),
|
|
completeGrating: Symbol('completeGrating'),
|
|
};
|
|
const kRotationDirection = {
|
|
clockwise: Symbol('clockwise'),
|
|
counterClockwise: Symbol('counterClockwise'),
|
|
};
|
|
const kPullDirection = {
|
|
right: Symbol('right'),
|
|
left: Symbol('left'),
|
|
};
|
|
|
|
// Configuration ///////////////////////////////////////////////////////////////
|
|
const configuration = {
|
|
wheelStrength: 1,
|
|
maxLoad: 100,
|
|
leverTravel: 80.72,
|
|
graterTravel: 69,
|
|
chainSpeed: 0.5,
|
|
grateStrength: 0.01,
|
|
defaultUrl: '/example.html'
|
|
};
|
|
|
|
// App State ///////////////////////////////////////////////////////////////////
|
|
/**
|
|
* The main app state
|
|
*/
|
|
let appState = null;
|
|
|
|
/**
|
|
* Functions to handle side effects
|
|
*/
|
|
const middleware = [
|
|
function completeLoading(state, action) {
|
|
if (action === kAction.turnWheel && state.currentState === kState.readyToLoad && parseFloat(state.load) >= configuration.maxLoad) {
|
|
send(kAction.completeLoading);
|
|
}
|
|
},
|
|
function updateIframe(state, action) {
|
|
if (action === kAction.turnWheel && state.currentState === kState.readyToLoad && parseFloat(state.load) <= configuration.maxLoad) {
|
|
state.currentIframe?.style.setProperty('--loadProgress', `${100 - state.load}vh`);
|
|
}
|
|
},
|
|
function completeGrating(state, action) {
|
|
if (action === kAction.pullLever && state.currentState === kState.readyToGrate && parseFloat(state.grateLevel) >= kDimensions.sausageHeight) {
|
|
removeSausages();
|
|
const $iframe = loadIframe(state.currentUrl, 0);
|
|
send(kAction.completeGrating, { iframe: $iframe });
|
|
}
|
|
},
|
|
function createSausageIfWaiting(state, action, data) {
|
|
if (action === kAction.createSausage && state.currentState === kState.waiting) {
|
|
createSausage(data.href, data.label);
|
|
}
|
|
},
|
|
function log(state, action, data) {
|
|
//console.log(state.load);
|
|
}
|
|
];
|
|
|
|
const $stateContainer = document.querySelector('.machine');
|
|
const updateState = function updateState(state) {
|
|
for (const [key, value] of Object.entries(state)) {
|
|
$stateContainer.style.setProperty(`--${key}`, value);
|
|
$stateContainer.dataset[key] = value;
|
|
}
|
|
appState = state;
|
|
};
|
|
|
|
const reduce = function reduce(stateReference, action, data) {
|
|
let state = Object.assign({}, stateReference);
|
|
|
|
switch (action) {
|
|
case kAction.turnWheel:
|
|
state.wheelRotation = `${data.angle}rad`;
|
|
if (state.currentState === kState.readyToLoad) {
|
|
const sign = data.direction === kRotationDirection.clockwise ? 1 : -1;
|
|
const load = Math.max(Math.min(state.load - sign * data.magnitude * configuration.wheelStrength, configuration.maxLoad), 0);
|
|
state.load = load;
|
|
if (load < configuration.maxLoad && load > 0) {
|
|
const chainOffset = parseFloat(state.chainOffset) + sign * data.magnitude * configuration.chainSpeed;
|
|
state.chainOffset = `${chainOffset}%`;
|
|
}
|
|
}
|
|
break;
|
|
case kAction.pullLever:
|
|
const diff = Math.abs(state.leverTravel - data.magnitude);
|
|
state.leverTravel = data.magnitude;
|
|
const rotation = data.magnitude * configuration.leverTravel / 100;
|
|
state.leverRotation = `${rotation}deg`
|
|
const gratingTravel = data.magnitude * configuration.graterTravel / 100;
|
|
state.graterTravel = `${gratingTravel}px`
|
|
if (state.currentState === kState.readyToGrate) {
|
|
const grateLevel = Math.min(parseFloat(state.grateLevel) + diff * configuration.grateStrength, kDimensions.sausageHeight);
|
|
state.grateLevel = `${grateLevel}px`;
|
|
}
|
|
break;
|
|
case kAction.createSausage:
|
|
state.currentUrl = data.href;
|
|
state.grateLevel = '0%';
|
|
state.currentState = kState.readyToGrate;
|
|
break;
|
|
case kAction.startTurningWheel:
|
|
state.isTurningWheel = true;
|
|
break;
|
|
case kAction.stopTurningWheel:
|
|
state.isTurningWheel = false;
|
|
break;
|
|
case kAction.startPullingLever:
|
|
state.isPullingLever = true;
|
|
break;
|
|
case kAction.stopPullingLever:
|
|
state.isPullingLever = false;
|
|
break;
|
|
case kAction.completeLoading:
|
|
state.currentState = kState.waiting;
|
|
state.load = 0;
|
|
break;
|
|
case kAction.completeGrating:
|
|
state.currentState = kState.readyToLoad;
|
|
state.currentIframe = data.iframe;
|
|
state.grateLevel = '0%';
|
|
state.load = 0;
|
|
break;
|
|
}
|
|
|
|
return state;
|
|
};
|
|
|
|
/**
|
|
* The main mechanism for view changes to trigger updates.
|
|
* It receives an action and its payload.
|
|
* It queues the middleware to run after this.
|
|
*/
|
|
const send = function send(action, data) {
|
|
const state = Object.assign({}, appState);
|
|
for (const m of middleware) {
|
|
setTimeout(() => m(state, action, data), 0);
|
|
}
|
|
const newState = reduce(state, action, data);
|
|
updateState(newState);
|
|
};
|
|
|
|
// Initializzation /////////////////////////////////////////////////////////////
|
|
|
|
const initialize = function initialize() {
|
|
|
|
const state = {
|
|
currentState: kState.waiting,
|
|
isPullingLever: false,
|
|
isTurningWheel: false,
|
|
leverTravel: 0,
|
|
grateLevel: 0,
|
|
leverRotation: '0deg',
|
|
graterTravel: '0rem',
|
|
wheelRotation: '0deg',
|
|
chainOffset: '0%',
|
|
load: 0,
|
|
};
|
|
updateState(state);
|
|
|
|
// Reset interaction with widgets if we lift a mouse button.
|
|
document.addEventListener('mouseup', () => {
|
|
send(kAction.stopTurningWheel)
|
|
send(kAction.stopPullingLever)
|
|
});
|
|
|
|
// The lever handle initiates interactions with the lever.
|
|
document.querySelector('.lever .handle')?.addEventListener('mousedown', () => {
|
|
send(kAction.startPullingLever)
|
|
});
|
|
|
|
// The wheel handle initiates interactions with the wheel.
|
|
document.querySelector('.wheel .handle')?.addEventListener('mousedown', () => {
|
|
send(kAction.startTurningWheel)
|
|
});
|
|
|
|
// Rotation is calculated relatively to the whole wheel.
|
|
// And travel based no the full arc of the lever
|
|
const $wheel = document.querySelector('.wheel');
|
|
const $lever = document.querySelector('.lever');
|
|
document.addEventListener('mousemove', (event) => {
|
|
if (appState.isTurningWheel && !appState.waitingForFrame) {
|
|
requestAnimationFrame(() => {
|
|
const elementPosition = $wheel.getBoundingClientRect();
|
|
const x = event.clientX - elementPosition.left;
|
|
const y = event.clientY - elementPosition.top;
|
|
const angle = angleFromCenter(x, y, kDimensions.wheelSize, kDimensions.wheelSize)
|
|
const oldAngle = parseFloat(appState.wheelRotation);
|
|
const { direction, magnitude } = rotationDirection(oldAngle, angle);
|
|
send(kAction.turnWheel, { angle, direction, magnitude });
|
|
send(kAction.setIsWaitingForFrame, { state: false })
|
|
});
|
|
send(kAction.setIsWaitingForFrame, { state: true })
|
|
}
|
|
if (appState.isPullingLever && !appState.waitingForFrame) {
|
|
requestAnimationFrame(() => {
|
|
const elementPosition = $lever.getBoundingClientRect();
|
|
const x = event.clientX - elementPosition.left;
|
|
const magnitude = Math.max(Math.min(x * 100 / kDimensions.leverWidth, 100), 0);
|
|
const direction = magnitude >= appState.leverTravel ? kPullDirection.right : kPullDirection.left;
|
|
send(kAction.pullLever, { magnitude, direction });
|
|
send(kAction.setIsWaitingForFrame, { state: false })
|
|
});
|
|
send(kAction.setIsWaitingForFrame, { state: true })
|
|
}
|
|
});
|
|
|
|
// The URL Grater is the drop area.
|
|
const $grater = document.querySelector('.grater-bed');
|
|
$grater.addEventListener('dragenter', (event) => {
|
|
event.preventDefault();
|
|
if (appState.currentState === kState.waiting) {
|
|
$grater.classList.add('drag-over');
|
|
}
|
|
});
|
|
$grater.addEventListener('dragover', (event) => event.preventDefault());
|
|
$grater.addEventListener('dragleave', (event) => {
|
|
if (!$grater.contains(event.relatedTarget)) {
|
|
$grater.classList.remove('drag-over');
|
|
}
|
|
});
|
|
$grater.addEventListener('drop', (event) => {
|
|
event.preventDefault();
|
|
if (appState.currentState !== kState.waiting) {
|
|
return;
|
|
}
|
|
const href = event.dataTransfer.getData('text/uri-list');
|
|
const label = event.dataTransfer.getData('text/plain');
|
|
$grater.classList.remove('drag-over');
|
|
send(kAction.createSausage, { href, label });
|
|
});
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const urlFromQuery = params.get("url");
|
|
const url = urlFromQuery ?? configuration.defaultUrl;
|
|
loadIframe(url, 100);
|
|
};
|
|
|
|
// Utility Functions ///////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Calculates the angle of a point (x,y) relative to the center of a square
|
|
* with dimensions (w, h). The result is in radians.
|
|
*/
|
|
const angleFromCenter = function angleFromCenter(x, y, w, h) {
|
|
const centerX = w / 2;
|
|
const centerY = h / 2;
|
|
const dx = x - centerX;
|
|
const dy = y - centerY;
|
|
return Math.atan2(dy, dx);
|
|
};
|
|
|
|
/**
|
|
* Given two angles, calculates the direction of the rotation.
|
|
*/
|
|
const rotationDirection = function rotationDirection(angle1, angle2) {
|
|
let diff = angle2 - angle1;
|
|
|
|
if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
if (diff < -Math.PI) diff += 2 * Math.PI;
|
|
|
|
const magnitude = Math.abs(diff);
|
|
const direction = diff > 0 ? kRotationDirection.counterClockwise : kRotationDirection.clockwise;
|
|
|
|
return {
|
|
direction,
|
|
magnitude
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Loads an iframe and appends it into the body.
|
|
*/
|
|
const $iframeContainer = document.querySelector('.web-content');
|
|
const loadIframe = function loadIframe(url, loadProgress) {
|
|
const $iframe = document.createElement('iframe');
|
|
$iframe.src = url;
|
|
$iframe.style.setProperty('--loadProgress', `${100 - loadProgress}vh`);
|
|
$iframeContainer.appendChild($iframe);
|
|
|
|
$iframe.addEventListener('load', () => {
|
|
const iframeDocument = $iframe.contentDocument;
|
|
|
|
iframeDocument.querySelectorAll('a').forEach(($link) => {
|
|
$link.addEventListener('click', (event) => event.preventDefault());
|
|
$link.draggable = true;
|
|
$link.addEventListener('dragstart', (event) => {
|
|
event.dataTransfer.setData('text/uri-list', $link.href);
|
|
event.dataTransfer.setData('text/plain', $link.textContent);
|
|
const ghost = $link.cloneNode(true);
|
|
ghost.style.background = 'url(\'/images/Sausage.svg\')';
|
|
ghost.style.backgroundRepeat = 'no-repeat';
|
|
ghost.style.cursor = 'grabbing';
|
|
ghost.style.display = 'block';
|
|
ghost.style.height = '3rem';
|
|
ghost.style.width = '8.125rem';
|
|
ghost.style.color = '#fff';
|
|
ghost.style.textAlign = 'center';
|
|
ghost.style.fontSize = '0.625rem';
|
|
ghost.style.fontFamily = 'sans-serif';
|
|
ghost.style.textTransform = 'uppercase';
|
|
ghost.style.paddingTop = '1rem';
|
|
|
|
document.body.appendChild(ghost);
|
|
event.dataTransfer.setDragImage(ghost, 10, 10);
|
|
});
|
|
});
|
|
});
|
|
|
|
return $iframe;
|
|
};
|
|
|
|
const createSausage = function createSausage(href, label) {
|
|
const $sausage = document.createElement('article');
|
|
$sausage.classList.add('sausage');
|
|
$sausage.dataset.href = href;
|
|
$sausage.innerText = label;
|
|
const $graterBed = document.querySelector('.grater-bed');
|
|
$graterBed?.appendChild($sausage);
|
|
};
|
|
|
|
const removeSausages = function removeSausages() {
|
|
const $graterBed = document.querySelector('.grater-bed');
|
|
const $existingSausages = $graterBed.querySelectorAll('.sausage');
|
|
for (const $existingSausage of $existingSausages) {
|
|
$graterBed?.removeChild($existingSausage);
|
|
}
|
|
}
|
|
|
|
initialize();
|