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; try { if(el.computedStyleMap().get('pointer-events').toString() === 'none') continue; } catch(e) { console.log('Firefox?', el); } 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]; window.localStorage.setItem('tomato-hint', 'true'); if(ctrl.classList.contains('tucked')) { 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); function setData(key, value) { const input = document.getElementById(`field-${key}`); input.value = value; const ev = new Event('input', { bubbles: true, cancelable: true, }); input.dispatchEvent(ev); } function lazerSettings() { setData('variance', 0); setData('gravity', 0); setData('init-vy', 0); } function defaultSettings() { setData('variance', 100); setData('gravity', 400); setData('init-vy', -100); } 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 defaultBtn = document.getElementById('field-ctrl-default'); defaultBtn.addEventListener('click', function() { defaultSettings(); }); const lazer = document.getElementById('field-ctrl-lazer'); lazer.addEventListener('click', function() { lazerSettings(); }); 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}`; window.localStorage.setItem('tomato-variance', VARIANCE); }); const gravity = document.getElementById('field-gravity'); gravity.addEventListener('input', function() { console.log('Update gravity: ', gravity.value); GRAVITY = parseInt(gravity.value); const gravityLabel = document.querySelector('label[for="field-gravity"]'); gravityLabel.innerText = `Gravity: ${GRAVITY}`; window.localStorage.setItem('tomato-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}`; window.localStorage.setItem('tomato-init-vy', INIT_VY); }); // Recover stored settings setData('variance', parseInt(window.localStorage.getItem('tomato-variance') ?? 100)); setData('gravity', parseInt(window.localStorage.getItem('tomato-gravity') ?? 400)); setData('init-vy', parseInt(window.localStorage.getItem('tomato-init-vy') ?? -100)); if(window.localStorage.getItem('tomato-hint') === 'true') { const ctrl = document.getElementsByClassName('field-ctrl')[0]; ctrl.classList.remove('tucked'); ctrl.classList.add('hidden'); } });