summaryrefslogtreecommitdiff
path: root/public/index.html
blob: a5495c69cbbafd1b693cd325337a1d2a4ba7b656 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Yesod</title>
  <meta name="description" content="Shared rooms in VR.">
  <style>
    /* Browser-default monospace, centered. */
    #lobby, #hud, #banner { font-family: monospace; }
    input, button, select { font: inherit; }

    #lobby { position: fixed; inset: 0; z-index: 10; overflow-y: auto; padding: 1em; box-sizing: border-box; background: Canvas; color: CanvasText; text-align: center; }
    #lobby pre { margin: 1.5em 0; }
    #lobby form, #rooms-view { display: flex; flex-direction: column; align-items: center; gap: 1.6em; margin: 1.6em 0; }
    #lobby fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
    #lobby label.field, #lobby legend { display: block; margin: 0 auto 0.6em; padding: 0; }
    #lobby input[type="text"], #lobby input[type="password"] { width: 22ch; padding: 0.35em 0.5em; }
    #lobby p { margin: 0; }
    #lobby .heading { font-weight: bold; }
    #lobby button { padding: 0.45em 1.4em; }
    #lobby button.primary { padding: 0.45em 2em; }
    .buttons button + button { margin-left: 0.6em; }
    #password-hint { color: GrayText; }
    .error { color: #b00020; }
    #setup-error { margin: -0.6em 0 0; }

    .choice { position: relative; cursor: pointer; }
    .choice input { position: absolute; opacity: 0; width: 1px; height: 1px; }
    .choice:has(input:focus-visible) { outline: 1px dotted CanvasText; outline-offset: 3px; }

    /* Avatar, world and mode tiles: a thin frame on hover, a dark one when chosen. */
    .avatars .choice, .worlds .choice, .modes .choice { display: flex; flex-direction: column; align-items: center; border: 1px solid transparent; }
    .avatars .choice:hover, .worlds .choice:hover, .modes .choice:hover { border-color: #ccc; }
    .avatars .choice:has(input:checked), .worlds .choice:has(input:checked), .modes .choice:has(input:checked) { border-color: CanvasText; }

    .avatars { display: grid; grid-template-columns: repeat(4, 9em); gap: 0.6em; justify-content: center; }
    .avatars .choice { gap: 0.4em; padding: 0.5em 0.2em; }
    .avatars img { width: 7em; height: 7em; background: #eee; }

    .worlds { display: flex; flex-wrap: wrap; gap: 0.6em; justify-content: center; }
    .worlds .choice { width: 16em; gap: 0.3em; padding: 0.5em; }
    .worlds img { width: 16em; height: 9em; object-fit: cover; background: #eee; margin-bottom: 0.2em; }
    .worlds span, .modes span { color: GrayText; font-size: 0.9em; }

    .modes { display: flex; flex-wrap: wrap; gap: 0.6em; justify-content: center; }
    .modes .choice { width: 16em; gap: 0.4em; padding: 0.8em 0.6em; }
    .modes svg { width: 3em; height: 2em; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }

    .toggle { display: flex; flex-direction: column; align-items: center; gap: 0.4em; max-width: 30em; }
    .toggle label { cursor: pointer; }
    .toggle input { margin: 0 0.4em 0 0; vertical-align: -0.1em; }
    .toggle span { color: GrayText; font-size: 0.9em; }

    .colors { display: flex; gap: 0.8em; justify-content: center; }
    .colors .choice span { display: block; width: 1.8em; height: 1.8em; border-radius: 50%; border: 1px solid rgba(0, 0, 0, 0.25); }
    .colors .choice:has(input:checked) span { outline: 2px solid CanvasText; outline-offset: 2px; }

    /* Open rooms: picture, what and who, and a Join button. */
    .rooms { list-style: none; margin: 0; padding: 0; width: min(36em, 100%); }
    .rooms li { display: flex; align-items: center; gap: 1em; padding: 0.7em 0; border-top: 1px solid #ddd; text-align: left; }
    .rooms li:last-child { border-bottom: 1px solid #ddd; }
    .rooms img { width: 7em; height: 3.9em; object-fit: cover; background: #eee; flex: none; }
    .rooms .about { flex: 1; min-width: 0; line-height: 1.5; }
    .rooms .about span { display: block; color: GrayText; overflow-wrap: anywhere; }
    .rooms svg { width: 0.85em; height: 0.85em; margin-left: 0.4em; vertical-align: -0.05em; fill: none; stroke: currentColor; stroke-width: 2.5; }
    .rooms button { flex: none; }

    /* "My screen": what you're sharing, its sound, and who's here. */
    #share-view { display: flex; flex-direction: column; align-items: center; gap: 1.2em; margin: 1.6em 0; }
    #share-preview { width: min(36em, 100%); aspect-ratio: 16 / 9; background: #000; }
    #share-status, #share-people { color: GrayText; }
    #sound-field { display: flex; flex-direction: column; align-items: center; gap: 0.5em; max-width: 34em; }
    #share-sound { max-width: min(30em, 90vw); padding: 0.3em; }
    #sound-hint { color: GrayText; font-size: 0.9em; }

    #hud { position: fixed; left: 1em; bottom: 1em; z-index: 10; }
    #hud p, #banner { display: table; margin: 0.4em 0; padding: 0.2em 0.5em; background: Canvas; color: CanvasText; }
    #hud button { margin-right: 0.4em; padding: 0.3em 0.9em; }
    #banner { position: fixed; z-index: 20; top: 1em; left: 50%; transform: translateX(-50%); }
    .vr-only, body.xr-capable .desktop-only { display: none !important; }
    body.xr-capable .vr-only { display: table !important; }
    /* A white ring and dot, outlined in black so it shows on light and dark alike. */
    #crosshair { position: fixed; z-index: 10; top: 50%; left: 50%; box-sizing: border-box; width: 18px; height: 18px; margin: -9px 0 0 -9px; border: 2px solid #fff; border-radius: 50%; box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.8), inset 0 0 0 1.5px rgba(0, 0, 0, 0.8); pointer-events: none; }
    #crosshair::after { content: ''; position: absolute; top: 50%; left: 50%; width: 4px; height: 4px; margin: -2px 0 0 -2px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.8); }
    [hidden] { display: none !important; }

    @media (max-width: 40em) { .avatars { grid-template-columns: repeat(3, 9em); } }
    @media (max-width: 30em) {
      .avatars { grid-template-columns: repeat(2, 9em); }
      .rooms img { width: 4.5em; height: 4.5em; }
    }
  </style>

  <!-- Everything is served from this server; no third-party CDNs. -->
  <script>window.AFRAME_CDN_ROOT = new URL('assets/aframe/', location.href).href;</script>
  <script src="vendor/aframe-1.7.1.min.js"></script>
  <script src="socket.io/socket.io.min.js"></script>
  <script src="easyrtc/easyrtc.js"></script>
  <script src="vendor/networked-aframe-0.14.3.min.js"></script>

  <script src="js/world.js"></script>
  <script src="js/components/player-info.js"></script>
  <script src="js/components/voice-level.js"></script>
  <script src="js/components/nametag.js"></script>
  <script src="js/components/vrm-avatar.js"></script>
  <script src="js/components/locomotion.js"></script>
  <script src="js/components/wield.js"></script>
  <script src="js/components/hand-tilt.js"></script>
  <script src="js/components/campsite.js"></script>
  <script src="js/components/temple.js"></script>
  <script src="js/components/testroom.js"></script>
  <script src="js/components/cabin.js"></script>
  <script src="js/components/balcony.js"></script>
  <script src="js/components/chess.js"></script>
  <script src="js/components/screens.js"></script>
  <script src="js/share.js"></script>
  <script src="js/astro.js"></script>
  <script src="js/components/sky.js"></script>
  <script src="js/components/ambience.js"></script>
  <script src="js/schemas.js"></script>
</head>
<body>
  <main id="lobby">
<pre>
---+---+---+---+---+---+---+---

<b id="site-name">Yesod</b>
</pre>

    <section id="rooms-view">
      <p class="heading">Open rooms</p>
      <ul id="room-list" class="rooms"></ul>
      <p id="no-rooms" hidden>No one's in a room right now.</p>
      <p id="rooms-error" class="error" role="alert" hidden></p>
      <button id="start-btn" class="primary" type="button">Start a room</button>
    </section>

    <form id="setup-form" autocomplete="off" hidden>
      <p id="setup-title" class="heading"></p>

      <fieldset id="world-field">
        <legend>World</legend>
        <div id="world-grid" class="worlds"></div>
      </fieldset>

      <fieldset id="mode-field">
        <legend>Who can come</legend>
        <div class="modes">
          <label class="choice"><input type="radio" name="mode" value="open" aria-label="Open room">
            <svg viewBox="0 0 48 32" aria-hidden="true"><circle cx="16" cy="9" r="5"/><path d="M6 29c0-6 4.5-10 10-10s10 4 10 10"/><circle cx="33" cy="10" r="4.5"/><path d="M28 19.5c1.5-.7 3.2-1 5-1 5.5 0 9 3.8 9 10.5"/></svg>
            <b>Open room</b><span>Listed here so anyone can join. Add a password to keep it to people you tell.</span></label>
          <label class="choice"><input type="radio" name="mode" value="solo" aria-label="Just me">
            <svg viewBox="0 0 48 32" aria-hidden="true"><circle cx="24" cy="9" r="5"/><path d="M14 29c0-6 4.5-10 10-10s10 4 10 10"/></svg>
            <b>Just me</b><span>Runs on this device only. Nothing is listed or shared.</span></label>
        </div>
      </fieldset>

      <fieldset id="as-field" hidden>
        <legend>Join as</legend>
        <div class="modes">
          <label class="choice"><input type="radio" name="as" value="me" aria-label="Me">
            <svg viewBox="0 0 48 32" aria-hidden="true"><circle cx="24" cy="9" r="5"/><path d="M14 29c0-6 4.5-10 10-10s10 4 10 10"/></svg>
            <b>Me</b><span>With an avatar, in a headset or on this computer.</span></label>
          <label class="choice"><input type="radio" name="as" value="screen" aria-label="My screen">
            <svg viewBox="0 0 48 32" aria-hidden="true"><rect x="9" y="3" width="30" height="19" rx="1.5"/><path d="M24 22v5M17 29h14"/></svg>
            <b>My screen</b><span>Show a screen, window or tab from this computer on a big screen in the room.</span></label>
        </div>
      </fieldset>

      <div id="password-field">
        <label class="field" for="password">Password <span id="password-hint">(optional)</span></label>
        <input id="password" name="password" type="password" maxlength="64" autocapitalize="none" spellcheck="false">
      </div>

      <div id="name-field">
        <label class="field" for="name">Name</label>
        <input id="name" name="name" type="text" maxlength="20">
      </div>

      <fieldset id="avatar-field">
        <legend>Avatar</legend>
        <div id="avatar-grid" class="avatars"></div>
      </fieldset>

      <fieldset id="color-field">
        <legend>Color</legend>
        <div id="color-row" class="colors"></div>
      </fieldset>

      <div id="hide-ip-field" class="toggle" hidden>
        <label><input id="hide-ip" name="hideIp" type="checkbox"> Hide my IP address</label>
        <span>Your voice and movement go through this server, so others in the room only see its address. Turn off for a little less delay.</span>
      </div>

      <p id="setup-error" class="error" role="alert" hidden></p>
      <p class="buttons"><button id="back-btn" type="button">Back</button> <button id="go-btn" class="primary" type="submit"></button></p>
    </form>

    <section id="share-view" hidden>
      <p id="share-title" class="heading"></p>
      <video id="share-preview" muted playsinline autoplay hidden></video>
      <p id="share-status" role="status"></p>
      <p class="buttons"><button id="share-btn" class="primary" type="button">Share screen</button> <button id="stop-share-btn" type="button" hidden>Stop sharing</button></p>
      <div id="sound-field" hidden>
        <label for="share-sound">Sound</label>
        <select id="share-sound"></select>
        <button id="sound-btn" type="button">Choose a sound input…</button>
        <span id="sound-hint"></span>
      </div>
      <p id="share-people">No one else here</p>
      <p class="buttons"><button id="share-leave-btn" type="button">Leave</button></p>
    </section>

<pre>
---+---+---+---+---+---+---+---
</pre>
  </main>

  <div id="hud" hidden>
    <p><button id="vr-btn" type="button" hidden>Enter VR</button> <button id="mic-btn" type="button" aria-pressed="false">Mic on</button> <button id="link-btn" type="button" hidden>Copy link</button> <button id="leave-btn" type="button">Leave</button></p>
    <p id="people" hidden>No one else here</p>
    <p class="desktop-only">Click to look · Esc to free mouse · WASD walk · Space jump · C sit/stand · M mute<span class="world-action" hidden> · V <span></span></span><span class="world-tools" hidden> · F pick up · hold click to draw · Backspace erase your last line</span><span class="world-chess" hidden> · F pick up a chess piece, F again to put it where you look</span><span class="screen-hint" hidden> · G move a screen, scroll to resize it</span></p>
    <p class="vr-only">Left stick walk · right stick turn · A jump · X sit/stand · Y/B mute<span class="world-action" hidden> · reach for the <span></span>, then trigger</span><span class="world-tools" hidden> · grip to hold · trigger to draw · empty hand + trigger to erase</span><span class="world-chess" hidden> · grip to pick up a chess piece</span><span class="screen-hint" hidden> · point at a screen and grip to move it, grip with your other hand too to resize</span></p>
  </div>

  <p id="banner" role="status" hidden></p>
  <div id="crosshair" hidden></div>

  <a-scene
    networked-scene="app: yesod; room: lobby; adapter: easyrtc; audio: true; connectOnLoad: false; debug: false"
    renderer="antialias: true; colorManagement: true; foveationLevel: 1"
    background="color: #000000"
    loading-screen="enabled: false"
    xr-mode-ui="enabled: false"
  >
    <a-assets timeout="30000">
      <!-- Remote people: the rig carries who they are and their avatar body. -->
      <template id="rig-template">
        <a-entity vrm-avatar></a-entity>
      </template>

      <!-- Remote head: voice comes from here, name floats above it. -->
      <template id="head-template">
        <a-entity class="remote-head" voice-level networked-audio-source="refDistance: 2; rolloffFactor: 0.7; distanceModel: inverse">
          <a-entity nametag position="0 0.42 0"></a-entity>
        </a-entity>
      </template>
    </a-assets>

    <!-- The world you pick is added from its template (see worlds.json), so the
         lobby doesn't download worlds nobody chose. -->

    <!-- You. Your own body is hidden from yourself; others see your avatar. -->
    <a-entity id="rig" player-info locomotion wield screens
              networked="template: #rig-template; attachTemplateToLocal: false">
      <a-entity id="player" camera="near: 0.05" position="0 1.6 0"
                look-controls="pointerLockEnabled: true; magicWindowTrackingEnabled: false"
                networked="template: #head-template; attachTemplateToLocal: false"></a-entity>
      <a-entity id="left-hand" hand-tilt
                networked-hand-controls="hand: left; handModelStyle: lowPoly"
                networked="template: #left-hand-default-template">
        <a-text id="wrist-text" value="" align="center" width="0.5" color="#ffe2c4"
                position="0 0.06 0.06" rotation="-30 0 0"></a-text>
      </a-entity>
      <a-entity id="right-hand" hand-tilt
                networked-hand-controls="hand: right; handModelStyle: lowPoly"
                networked="template: #right-hand-default-template"></a-entity>
    </a-entity>
  </a-scene>

  <template id="world-campfire">
    <!-- Away from the fire it's nearly dark; the sky brightens these when the Moon is up. -->
    <a-entity id="ambient-light" light="type: hemisphere; color: #6a79a8; groundColor: #1c2416; intensity: 0.025"></a-entity>
    <a-entity id="moon-light" light="type: directional; color: #b9c6ff; intensity: 0.02" position="-20 30 -12"></a-entity>
    <a-entity id="sky" sky></a-entity>
    <a-entity id="ambience" ambience></a-entity>
    <a-entity id="campsite" campsite data-world="campsite"></a-entity>
  </template>

  <template id="world-temple">
    <a-entity id="ambient-light" light="type: hemisphere; color: #8a7a68; groundColor: #1a1410; intensity: 0.12"></a-entity>
    <a-entity id="moon-light" light="type: directional; color: #b9c6ff; intensity: 0.02" position="-20 30 -12"></a-entity>
    <!-- The temple comes before the sky so it can point the sky toward Boleskine. -->
    <a-entity id="temple" temple="fireSound: assets/sounds/fire.ogg" data-world="temple"></a-entity>
    <a-entity id="sky" sky></a-entity>
  </template>

  <template id="world-cabin">
    <!-- The fire does most of the lighting; the sky keeps the real Moon in the window. -->
    <a-entity id="ambient-light" light="type: hemisphere; color: #8a6e58; groundColor: #2a1c12; intensity: 0.22"></a-entity>
    <a-entity id="moon-light" light="type: directional; color: #b9c6ff; intensity: 0.02" position="0 10 -30"></a-entity>
    <a-entity id="sky" sky="pinMoon: true; moonBearing: 175; moonAltitude: 7; moonSize: 2.5"></a-entity>
    <a-entity id="cabin" cabin="fireSound: assets/sounds/fire.ogg" data-world="cabin">
      <!-- The cabin builds the chess table and its chairs around wherever the board is. -->
      <a-entity chess="table: 0.4; height: 0.62" position="0.6 0.62 0"></a-entity>
    </a-entity>
  </template>

  <template id="world-balcony">
    <!-- An overcast day: soft light from everywhere, a little more from above. -->
    <a-entity light="type: hemisphere; color: #e2e7ec; groundColor: #6d7276; intensity: 1.6"></a-entity>
    <a-entity light="type: directional; color: #f4f2ee; intensity: 0.5" position="-3 10 -4"></a-entity>
    <a-entity id="balcony" balcony="windSound: assets/sounds/wind.ogg" data-world="balcony"></a-entity>
  </template>

  <template id="world-testroom">
    <a-entity light="type: hemisphere; color: #ffffff; groundColor: #ececec; intensity: 2.2"></a-entity>
    <a-entity light="type: directional; color: #ffffff; intensity: 1.2" position="1.5 3 2"></a-entity>
    <a-entity id="testroom" testroom data-world="testroom">
      <a-entity chess position="0 0.75 0"></a-entity>
    </a-entity>
  </template>

  <script src="js/app.js"></script>
</body>
</html>