diff --git a/_includes/nav.html b/_includes/nav.html index b9d0c45..0d4532b 100644 --- a/_includes/nav.html +++ b/_includes/nav.html @@ -51,3 +51,25 @@ + +
+
+
+ Got stuck? +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
diff --git a/_src/entrypoints/app.ts b/_src/entrypoints/app.ts index c57ad01..9a53ce3 100644 --- a/_src/entrypoints/app.ts +++ b/_src/entrypoints/app.ts @@ -1,3 +1,4 @@ +import "../lib/shoot.js"; import Empty from "../components/Empty.vue"; import IsoModal from "../components/IsoModal.vue"; import MainMirrorList from "../components/MainMirrorList.vue"; diff --git a/_src/entrypoints/default.ts b/_src/entrypoints/default.ts index 1a8b0c5..ce41177 100644 --- a/_src/entrypoints/default.ts +++ b/_src/entrypoints/default.ts @@ -1,3 +1,4 @@ +import "../lib/shoot.js"; import "../styles/global.scss"; import { suffix as siteSuffix } from "virtual:jekyll-config"; import { load as loadWebFont } from "webfontloader"; diff --git a/_src/entrypoints/fancyIndex.ts b/_src/entrypoints/fancyIndex.ts index 6797241..27ad5fd 100644 --- a/_src/entrypoints/fancyIndex.ts +++ b/_src/entrypoints/fancyIndex.ts @@ -1,3 +1,4 @@ +import "../lib/shoot.js"; import "./default"; import "../styles/fancyIndex.scss"; diff --git a/_src/lib/shoot.js b/_src/lib/shoot.js new file mode 100644 index 0000000..414cc33 --- /dev/null +++ b/_src/lib/shoot.js @@ -0,0 +1,150 @@ +const LIFE = 1; +let INIT_VY = -100; +let GRAVITY = 400; +let VARIANCE = 100; +let TOMATO = '🍅'; +const POV = 0.5; + +const tomatos = new Set()// { div, x, y, spawn, lastUpdate, vy, vx } +let cnt = 0; + +function renderTomato(now, tomato) { + const zoom = POV / (POV + (now - tomato.spawn) / 1000); + tomato.div.style.transform = `translate(${tomato.x}px, ${tomato.y}px) scale(${zoom})`; +} + +function updateTomato(now, tomato) { + let dt = (now - tomato.lastUpdate) / 1000; + let dying = (now - tomato.spawn) / 1000 >= LIFE; + if(dying) dt = LIFE - (tomato.lastUpdate - tomato.spawn) / 1000; + + tomato.lastUpdate = now; + + tomato.x += tomato.vx * dt; + tomato.y += tomato.vy * dt + 1/2 * GRAVITY * dt * dt; + tomato.vy += GRAVITY * dt; + + return dying; +} + +function dropTomato(tomato) { + tomatos.delete(tomato); + + tomato.div.innerHTML = `` + + tomato.div.classList.add('splash'); + const vpx = tomato.x - window.visualViewport.pageLeft; + const vpy = tomato.y - window.visualViewport.pageTop; + const stack = document.elementsFromPoint(vpx, vpy); + const field = document.getElementById('field'); + const ctrl = document.getElementsByClassName('field-ctrl')[0]; + for(const el of stack) { + // Skip tomato + if(field && field.contains(el)) continue; + if(ctrl && ctrl.contains(el)) continue; + if(el.computedStyleMap().get('pointer-events').toString() === 'none') continue; + console.log(el); + return el; + } + + return null; +} + +console.log('🍅 registered'); + +document.addEventListener('click', function(event) { + const ctrl = document.getElementsByClassName('field-ctrl')[0]; + if(event.pointerType === 'synthetic') return; + if(ctrl && ctrl.contains(event.target)) return; + console.log('clicked at ', event.pageX, event.pageY); + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + + const div = document.createElement('div'); + const inner = document.createElement('div'); + inner.innerText = TOMATO; + inner.className = 'tomato-inner'; + div.appendChild(inner); + div.className = 'tomato'; + + const now = document.timeline.currentTime; + const tomato = { + div, + x: event.pageX, + y: event.pageY, + spawn: now, + lastUpdate: now, + vy: INIT_VY + (Math.random() - 1/2) * VARIANCE, + vx: (Math.random() - 1/2) * VARIANCE, + }; + renderTomato(now, tomato); + + const field = document.getElementById('field'); + field.appendChild(div); + tomatos.add(tomato); + cnt += 1; + if(cnt === 10) { + const ctrl = document.getElementsByClassName('field-ctrl')[0]; + ctrl.classList.remove('tucked'); + ctrl.classList.add('hidden'); + } + +}, { + capture: true, +}); + +function loop(ts) { + const clickTargets = []; + for (const tomato of tomatos) { + const dying = updateTomato(ts, tomato); + renderTomato(ts, tomato); + if(dying) { + const target = dropTomato(tomato); + if(target) clickTargets.push(target); + } + } + + if(clickTargets.length > 0) { + for (const target of clickTargets) { + const event = new PointerEvent('click', { + bubbles: true, + pointerType: 'synthetic', + }); + target.dispatchEvent(event); + } + } + + requestAnimationFrame(loop); +} + +requestAnimationFrame(loop); + +document.addEventListener('DOMContentLoaded', function() { + const toggle = document.getElementsByClassName('field-ctrl-toggle')[0]; + toggle.addEventListener('click', function() { + const ctrl = document.getElementsByClassName('field-ctrl')[0]; + ctrl.classList.toggle('hidden'); + }); + + const variance = document.getElementById('field-variance'); + variance.addEventListener('input', function() { + VARIANCE = parseInt(variance.value); + const varianceLabel = document.querySelector('label[for="field-variance"]'); + varianceLabel.innerText = `Variance: ${VARIANCE}`; + }); + + const gravity = document.getElementById('field-gravity'); + gravity.addEventListener('input', function() { + GRAVITY = parseInt(gravity.value); + const gravityLabel = document.querySelector('label[for="field-gravity"]'); + gravityLabel.innerText = `Gravity: ${GRAVITY}`; + }); + + const init_vy = document.getElementById('field-init-vy'); + init_vy.addEventListener('input', function() { + INIT_VY = parseInt(init_vy.value); + const initVyLabel = document.querySelector('label[for="field-init-vy"]'); + initVyLabel.innerText = `Initial Velocity: ${INIT_VY}`; + }); +}); diff --git a/_src/styles/global.scss b/_src/styles/global.scss index dbc6f06..56bfd86 100644 --- a/_src/styles/global.scss +++ b/_src/styles/global.scss @@ -205,3 +205,100 @@ a { margin-right: 5px; } } + +#field { + position: absolute; + z-index: 100000; + pointer-events: none; +} + +body { + overflow-x: hidden; +} + +.tomato { + width: 40px; + height: 40px; + position: absolute; + top: -20px; + left: -20px; + transform-origin: 50% 50%; +} + +.tomato-inner { + width: 40px; + height: 40px; + + animation: tomato-rotate .5s linear infinite; + + display: flex; + align-items: center; + justify-content: center; + font-size: 40px; +} + +@keyframes tomato-rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.tomato.splash svg { + transform-origin: 50% 50%; + transform: scale(3); +} + +.field-ctrl { + position: fixed; + z-index: 100005; + bottom: 0; + left: 0; + right: 0; + padding: 0 20px 20px 20px; + transition: transform .2s ease; +} + +.field-ctrl-toggle { + margin-bottom: 20px; + padding: 0 20px; + height: 40px; + line-height: 40px; + max-width: 200px; + background: var(--bs-body-bg); + box-shadow: rgba(0,0,0, .3) 0 -2px 3px; + font-weight: bold; + cursor: pointer; + border-radius: 4px; +} + +.field-ctrl-main { + padding: 10px 20px; + background: var(--bs-body-bg); + box-shadow: rgba(0,0,0, .3) 0 -2px 3px; + border-radius: 4px; + + > div { + display: flex; + align-items: center; + + > input { + flex: 1; + } + + > label { + margin-left: 10px; + } + } +} + + +.field-ctrl.tucked { + transform: translateY(calc(100% + 20px)); +} + +.field-ctrl.hidden { + transform: translateY(calc(100% - 50px)); +}