|
33 | 33 | position: absolute; |
34 | 34 | left: 8px; |
35 | 35 | bottom: 8px; |
| 36 | + z-index: 10; |
| 37 | + } |
| 38 | + |
| 39 | + .circle { |
| 40 | + position: absolute; |
| 41 | + width: 80px; |
| 42 | + height: 80px; |
| 43 | + border-radius: 50%; |
| 44 | + border: 4px solid rgba(255, 255, 255, 0.5); |
| 45 | + display: flex; |
| 46 | + align-items: center; |
| 47 | + justify-content: center; |
| 48 | + font-size: 24px; |
| 49 | + font-weight: bold; |
| 50 | + color: white; |
| 51 | + transform: translate(-50%, -50%); |
| 52 | + pointer-events: none; |
| 53 | + transition: opacity 0.5s; |
| 54 | + text-shadow: 0 1px 2px rgba(0,0,0,0.5); |
36 | 55 | } |
37 | 56 | </style> |
38 | 57 | </head> |
|
47 | 66 | class App extends webfx.View { |
48 | 67 | constructor() { |
49 | 68 | super(); |
| 69 | + this.state = 'INIT'; |
| 70 | + this.rounds = 0; |
| 71 | + this.clicksInThisRound = 0; |
| 72 | + this.firstClickMs = 0; |
50 | 73 | this.onclick = (e) => { |
51 | | - if (e.target === this.history.value) return; |
52 | | - e.preventDefault(); |
53 | | - this.clickEvent.set(); |
| 74 | + if (this.history.value && e.target === this.history.value) return; |
| 75 | + if (e.preventDefault) e.preventDefault(); |
| 76 | + |
| 77 | + if (this.state === 'CLICK') { |
| 78 | + let x = e.clientX ?? e.touches?.[0]?.clientX; |
| 79 | + let y = e.clientY ?? e.touches?.[0]?.clientY; |
| 80 | + if (x === undefined) { |
| 81 | + x = window.innerWidth / 2; |
| 82 | + y = window.innerHeight / 2; |
| 83 | + } |
| 84 | + const ms = Math.round(performance.now() - this.startTime); |
| 85 | + this.addCircle(x, y, ms); |
| 86 | + this.history.value.textContent += `${ms} `; |
| 87 | + this.clicksInThisRound++; |
| 88 | + if (this.clicksInThisRound === 1) { |
| 89 | + this.firstClickMs = ms; |
| 90 | + } |
| 91 | + this.clickEvent.set(); |
| 92 | + } else { |
| 93 | + this.clickEvent.set(); |
| 94 | + } |
54 | 95 | }; |
55 | 96 | this.clickEvent = new webfx.AutoResetEvent(); |
56 | 97 | this.text = new webfx.Ref(); |
57 | 98 | this.history = new webfx.Ref(); |
| 99 | + this.circleContainer = new webfx.Ref(); |
58 | 100 | this.routine(); |
59 | 101 | } |
60 | 102 | createDom() { |
|
71 | 113 | ref: this.history, |
72 | 114 | tag: 'div.history' |
73 | 115 | }, |
| 116 | + { |
| 117 | + ref: this.circleContainer, |
| 118 | + tag: 'div', |
| 119 | + style: 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;' |
| 120 | + } |
74 | 121 | ], |
75 | 122 | } |
76 | 123 | } |
77 | 124 | set(color, str) { |
78 | 125 | this.dom.style.backgroundColor = color; |
79 | 126 | this.text.value.textContent = str || ''; |
80 | 127 | } |
| 128 | + addCircle(x, y, ms) { |
| 129 | + const el = webfx.buildDOM({ |
| 130 | + tag: 'div.circle', |
| 131 | + style: { |
| 132 | + left: x + 'px', |
| 133 | + top: y + 'px', |
| 134 | + }, |
| 135 | + text: ms.toString(), |
| 136 | + }); |
| 137 | + el.dataset.round = this.rounds; |
| 138 | + this.circleContainer.value.appendChild(el); |
| 139 | + } |
| 140 | + updateCircles() { |
| 141 | + if (!this.circleContainer.value) return; |
| 142 | + const circles = Array.from(this.circleContainer.value.children); |
| 143 | + for (const c of circles) { |
| 144 | + const age = this.rounds - parseInt(c.dataset.round); |
| 145 | + if (age >= 3) { |
| 146 | + c.remove(); |
| 147 | + } else { |
| 148 | + c.style.opacity = (1 / age).toFixed(2); |
| 149 | + } |
| 150 | + } |
| 151 | + } |
81 | 152 | async routine() { |
82 | 153 | this.set(READY, "Click to start"); |
| 154 | + this.state = 'READY'; |
83 | 155 | while (true) { |
84 | 156 | await this.clickEvent.wait(); |
| 157 | + |
85 | 158 | this.set(WAIT, "Wait for green..."); |
| 159 | + this.state = 'WAIT'; |
| 160 | + |
86 | 161 | const action = await Promise.race([ |
87 | | - new Promise(r => setTimeout(r, 1000 + Math.random() * 3000, "timer")), |
| 162 | + new Promise(r => setTimeout(r, 1000 + Math.random() * 5000, "timer")), |
88 | 163 | this.clickEvent.wait(), |
89 | 164 | ]); |
| 165 | + |
90 | 166 | if (action == "timer") { |
91 | 167 | this.set(CLICK, "CLICK NOW!"); |
92 | | - const startTime = Date.now(); |
| 168 | + this.state = 'CLICK'; |
| 169 | + this.startTime = performance.now(); |
| 170 | + this.clicksInThisRound = 0; |
| 171 | + this.firstClickMs = 0; |
| 172 | + |
| 173 | + // Wait for at least one click |
93 | 174 | await this.clickEvent.wait(); |
94 | | - const ms = Date.now() - startTime; |
95 | | - this.set(READY, `${ms} ms`); |
96 | | - this.history.value.textContent += `${ms} `; |
| 175 | + this.set(READY, `${this.firstClickMs} ms`); |
| 176 | + |
| 177 | + // Wait for multiplayer clicks |
| 178 | + await new Promise(r => setTimeout(r, 1000)); |
| 179 | + |
| 180 | + this.rounds++; |
| 181 | + this.updateCircles(); |
| 182 | + this.state = 'READY'; |
97 | 183 | } else { |
98 | 184 | this.set(READY, `Too early!`); |
| 185 | + this.state = 'READY'; |
99 | 186 | this.history.value.textContent += `(early!) `; |
100 | 187 | } |
101 | 188 | } |
|
0 commit comments