diff --git a/mobile/vision/index.html b/mobile/vision/index.html index f08b7dd12..e45c79f63 100644 --- a/mobile/vision/index.html +++ b/mobile/vision/index.html @@ -307,7 +307,7 @@ input, textarea { font: inherit; color: inherit; } .compose .in input { flex: 1; background: none; border: 0; outline: none; color: var(--text); font-size: 15px; min-width: 0; } /* ---------- capture ---------- */ -#v-capture { background: #000; z-index: 25; } +#v-capture { background: transparent; z-index: 25; } #v-capture.on { display: block; } .cap-ui { position: absolute; inset: 0; pointer-events: none; } .cap-ui > * { pointer-events: auto; } @@ -320,7 +320,7 @@ input, textarea { font: inherit; color: inherit; } .callout span { color: var(--text2); font-size: 11px; } .callout .ci { width: 28px; height: 28px; border-radius: 9px; display: grid; place-items: center; background: color-mix(in oklab, var(--c) 22%, transparent); color: var(--c); } .reticle { position: absolute; width: 34px; height: 34px; margin: -17px 0 0 -17px; border: 1.5px solid var(--c, #fff); border-radius: 10px; pointer-events: none; box-shadow: 0 0 12px var(--c); } -.cap-bottom { position: absolute; left: 0; right: 0; bottom: 0; padding: 10px 16px calc(var(--sab) + 18px); display: flex; flex-direction: column; align-items: center; gap: 14px; background: linear-gradient(to top, rgba(0,0,0,.55), transparent); } +.cap-bottom { position: absolute; left: 0; right: 0; bottom: 0; padding: 10px 16px calc(var(--sab) + var(--tabH) + 26px); display: flex; flex-direction: column; align-items: center; gap: 14px; background: linear-gradient(to top, rgba(0,0,0,.55), transparent); } .zooms { display: flex; gap: 6px; padding: 4px; border-radius: 22px; } .zooms button { width: 36px; height: 36px; border-radius: 50%; font-size: 12px; font-weight: 700; color: var(--text2); } .zooms button.on { background: rgba(255,255,255,.14); color: var(--warn); } @@ -334,7 +334,7 @@ input, textarea { font: inherit; color: inherit; } .shutter:active i { transform: scale(.88); } .lastcap { width: 48px; height: 48px; border-radius: 12px; background: #111 center/cover; box-shadow: inset 0 0 0 1px rgba(255,255,255,.2); position: relative; } .lastcap .seal { position: absolute; right: -6px; top: -6px; width: 20px; height: 20px; } -.sensors { position: absolute; left: 12px; bottom: calc(var(--sab) + 210px); border-radius: 16px; padding: 8px 10px; font-size: 11px; display: none; gap: 3px; flex-direction: column; min-width: 158px; color: var(--text2); } +.sensors { position: absolute; left: 12px; bottom: calc(var(--sab) + var(--tabH) + 236px); border-radius: 16px; padding: 8px 10px; font-size: 11px; display: none; gap: 3px; flex-direction: column; min-width: 158px; color: var(--text2); } .sensors.on { display: flex; } .sensors b { color: var(--text); font-variant-numeric: tabular-nums; font-weight: 600; } .sensors svg { width: 140px; height: 26px; } @@ -351,7 +351,7 @@ input, textarea { font: inherit; color: inherit; } .step .ck { width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center; box-shadow: inset 0 0 0 1.5px var(--text3); color: transparent; transition: all .3s; } .step.done .ck { background: var(--gold); box-shadow: none; color: #1A1408; } .step b { font-weight: 600; } .step .h { font-family: var(--mono); font-size: 11px; color: var(--text2); word-break: break-all; line-height: 1.35; margin-top: 2px; } -.step svg { width: 100%; height: 30px; margin-top: 4px; } +.step svg.trace { width: 100%; height: 30px; margin-top: 4px; display: block; } .btnrow { display: flex; gap: 8px; } .btn { flex: 1; height: 50px; border-radius: 25px; font-weight: 700; font-size: 15px; display: inline-flex; align-items: center; justify-content: center; gap: 7px; background: rgba(255,255,255,.1); box-shadow: inset 0 0 0 .5px rgba(255,255,255,.12); } .btn.gold { background: linear-gradient(180deg, #F3D68A, #D9AE4F); color: #1C1405; box-shadow: inset 0 .5px 0 rgba(255,255,255,.6); } @@ -377,7 +377,7 @@ input, textarea { font: inherit; color: inherit; } .ws .wsh > * { pointer-events: auto; } .ws .wsh .tt { flex: 1; min-width: 0; height: 44px; border-radius: 22px; display: flex; align-items: center; gap: 10px; padding: 0 8px 0 14px; } .ws .wsh .tt b { font-size: 14.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; } -.ws .wsh .tt span { font-size: 11.5px; color: var(--text2); } +.ws .wsh .tt span { font-size: 11.5px; color: var(--text2); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ws .body { position: absolute; left: 0; right: 0; bottom: 0; top: 40%; border-radius: 28px 28px 0 0; display: flex; flex-direction: column; overflow: hidden; } .ws .seg { display: flex; margin: 12px 14px 8px; padding: 3px; border-radius: 12px; background: rgba(255,255,255,.07); flex: none; } .ws .seg button { flex: 1; height: 34px; border-radius: 9px; font-size: 13px; font-weight: 600; color: var(--text2); transition: all .25s; } @@ -391,7 +391,7 @@ input, textarea { font: inherit; color: inherit; } .obs .ot { font-size: 14px; font-weight: 600; line-height: 1.3; } .obs .om { font-size: 11.5px; color: var(--text3); margin-top: 3px; display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; } .obs .om .mono { color: var(--text2); } -.obs .tagid { font-family: var(--mono); font-size: 11px; color: var(--gold2); background: rgba(233,196,106,.12); padding: 1px 6px; border-radius: 6px; } +.tagid { display: inline-block; font-family: var(--font); font-weight: 700; font-size: 11px; letter-spacing: .02em; color: var(--gold2); background: rgba(233,196,106,.13); box-shadow: inset 0 0 0 .5px rgba(233,196,106,.35); padding: 2px 7px; border-radius: 7px; line-height: 1.3; } .hyp { border-radius: 20px; padding: 13px 14px; margin: 0 0 10px; background: var(--card2); box-shadow: inset 0 0 0 .5px rgba(255,255,255,.08); } .hyp.ai { box-shadow: inset 0 0 0 1px rgba(183,156,255,.45); background: linear-gradient(180deg, rgba(183,156,255,.10), rgba(183,156,255,.02)), var(--card2); } .hyp .hh { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text2); } @@ -500,7 +500,9 @@ input, textarea { font: inherit; color: inherit; } .cold .eye { width: 60px; height: 60px; margin-bottom: 18px; opacity: 0; transition: opacity 1.2s; color: #fff; } .cold.s2 .eye { opacity: .95; } .concept-tag { position: absolute; z-index: 90; left: 14px; top: calc(var(--sat) + 58px); pointer-events: none; font-size: 9.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: rgba(246,223,160,.72); padding: 3px 7px; border-radius: 6px; background: rgba(10,12,18,.35); box-shadow: inset 0 0 0 .5px rgba(233,196,106,.3); } -.app[data-tab="now"] .concept-tag, .app[data-tab="threads"] .concept-tag, .app[data-tab="you"] .concept-tag { top: calc(var(--sat) + 14px); left: auto; right: 14px; } +.app[data-tab="now"] .concept-tag { top: calc(var(--sat) + 14px); left: auto; right: 14px; } +.app[data-tab="threads"]:not([data-mode="workspace"]) .concept-tag, .app[data-tab="you"] .concept-tag { display: none; } +.app[data-mode="workspace"] .concept-tag { top: calc(var(--sat) + 62px); left: auto; right: 14px; } .app[data-tab="capture"] .concept-tag { top: calc(var(--sat) + 100px); left: 14px; } .app[data-mode="situation"] .concept-tag { left: auto; right: 14px; top: calc(var(--sat) + 62px); } @@ -1407,7 +1409,7 @@ vec3 aurora(vec3 n, float nightF){ float curt = 0.45 + 0.55*fbm(vec2(lonm*9. + uT*0.0006, uT*0.00025)); float g = exp(-pow((ml-band)/(2.2+uAurora*0.6), 2.)) * curt; float r = exp(-pow((ml-band-3.5)/3.5, 2.)) * 0.25; - return (vec3(0.12, 1.0, 0.5)*g + vec3(0.75, 0.15, 0.45)*r*0.45) * nightF * uAurora * 1.15; + return (vec3(0.12, 1.0, 0.5)*g + vec3(0.75, 0.15, 0.45)*r*0.45) * nightF * uAurora * 0.75; } vec3 cityDetail(vec3 base, vec2 q, float urban, float night, float fw, out vec3 emis){ emis = vec3(0.); @@ -1437,7 +1439,7 @@ vec3 cityDetail(vec3 base, vec2 q, float urban, float night, float fw, out vec3 if (uTraffic > 0.5) { vec2 rp = p/B; float along = abs(f.x-0.5) < abs(f.y-0.5) ? rp.y : rp.x; float cars = smoothstep(0.75, 1., sin(along*38. - uT*2.5 + h*20.)) * road * urban * (1.-aa); - emis += mix(vec3(1.,0.15,0.1), vec3(1.,0.95,0.8), step(0.5, fract(along*3.)))*cars*(0.25 + night*1.4); + emis += mix(vec3(1.,0.15,0.1), vec3(1.,0.95,0.8), step(0.5, fract(along*3.)))*cars*(0.03 + night*1.2); } float lit = step(0.72, hash12(id*3.1)) * (1.-road) * urban * night; emis += vec3(0.9,0.8,0.6)*lit*0.035; @@ -1565,7 +1567,7 @@ void main(){ float tc = -b; vec3 cp = ro + rd*max(tc,0.); float hr = length(cp); if (uAurora > 0. && hr > 1.004 && hr < 1.05) { vec3 cn = normalize(cp); float nf = 1.-smoothstep(-0.2,0.0,dot(cn,uSun)); - col += aurora(cn, nf) * smoothstep(1.05,1.012,hr) * smoothstep(1.004,1.012,hr) * 0.3; + col += aurora(cn, nf) * smoothstep(1.05,1.012,hr) * smoothstep(1.004,1.012,hr) * 0.12 * smoothstep(1.5, 0.4, uRange); } heat = 0.; } @@ -1849,8 +1851,8 @@ void main(){ vec3 base; vec3 emis = vec3(0.); float heat = 0.35; float spec = 0.; if (type == 0) { // building float h = vAttr.y; - base = mix(vec3(0.34,0.35,0.37), vec3(0.62,0.57,0.5), h); - base = mix(base, vec3(0.2,0.25,0.3), step(0.7, h)); // glass towers + base = mix(vec3(0.15,0.155,0.165), vec3(0.32,0.29,0.25), fract(h*7.13)); + base = mix(base, vec3(0.07,0.09,0.11), step(0.68, h)); // glass towers if (abs(n.z) < 0.5) { vec2 f = vec2(vUV.x/3.2, vUV.y/3.6); vec2 id = floor(f); vec2 fr = fract(f); @@ -1860,7 +1862,7 @@ void main(){ vec3 wc = mix(vec3(1.0,0.72,0.42), vec3(0.75,0.85,1.0), step(0.6, hash12(id*1.3+h))); emis += wc * lit * (0.9 + 0.6*r) * uNight * 2.2; base = mix(base, base*0.45 + uSkyCol*0.08, win*0.8); - spec = win*(1.-uNight)*0.6; + spec = win*(1.-uNight)*0.9 + step(0.68, h)*0.5; } else base *= 0.82; float ao = smoothstep(0., 14., vUV.y + (1.-abs(n.z))*0.) * 0.35 + 0.65; if (abs(n.z) < 0.5) base *= ao; @@ -2164,10 +2166,10 @@ W.layers = [ L('holo', 'eyes', 'Holograms', true, 'cube'), L('streams', 'eyes', 'Live streams', false, 'live'), L('transit', 'ground', 'Transit vehicles', false, 'tram'), L('traffic', 'ground', 'Traffic flow', true, 'car'), L('bikes', 'ground', 'Bike share', false, 'bike'), L('rail', 'ground', 'Rail', false, 'rail'), L('venues', 'ground', 'Events & venues', true, 'ticket'), L('alpr', 'ground', 'ALPR cameras', false, 'plate'), - L('radar', 'wx', 'Radar', true, 'radar'), L('clouds', 'wx', 'Clouds', true, 'cloud'), L('lightning', 'wx', 'Lightning', true, 'bolt'), + L('radar', 'wx', 'Radar', false, 'radar'), L('clouds', 'wx', 'Clouds', true, 'cloud'), L('lightning', 'wx', 'Lightning', true, 'bolt'), L('wind', 'wx', 'Wind', false, 'wind'), L('models', 'wx', 'Forecast models', false, 'spaghetti'), L('dc', 'infra', 'Datacenters', false, 'server'), L('dams', 'infra', 'Dams', false, 'dam'), L('power', 'infra', 'Power plants', false, 'bolt'), - L('milbase', 'infra', 'Military installations', false, 'shield'), L('gnss', 'infra', 'GNSS interference', true, 'jam'), L('imagery', 'infra', 'Recent satellite imagery', false, 'frame'), + L('milbase', 'infra', 'Military installations', false, 'shield'), L('gnss', 'infra', 'GNSS interference', false, 'jam'), L('imagery', 'infra', 'Recent satellite imagery', false, 'frame'), ]; W.layerById = Object.fromEntries(W.layers.map((l) => [l.id, l])); W.on = (id) => !!W.layerById[id]?.on; @@ -2440,7 +2442,7 @@ W.updateDynamic = function () { GE.SHIPS.forEach((s, i) => { const p = W._shipCache[i]; const tank = s.kind === 'tanker' || s.kind === 'LNG'; if (!showAis && !tank) return; - const ak = clamp(lerp(0.32, 0.95, smooth(1.2, 0.08, W.lastRange ?? 1)), 0.3, 1); + const ak = clamp(lerp(0.16, 0.95, smooth(1.4, 0.08, W.lastRange ?? 1)), 0.16, 1); const col = showTk && tank ? [1, 0.7, 0.35, ak] : s.queue ? [0.5, 1, 0.9, ak] : [0.22, 0.88, 0.78, ak * 0.95]; pushSpr(a, geo.vec(p.lat, p.lon, 0.03), col, (showTk && tank ? 10 : 8.5), 4, p.hdg, -1e9, 1e9, 0, i, 0.35); }); @@ -2536,6 +2538,7 @@ W.project = function (view, p) { // world unit coords → css px {x,y,z,vis} /* ---------- render ---------- */ W.quality = { scale: 1, steps: 12, bloom: true, grain: true }; +const LOD_MIN = { radar: 0.12 }; const LOD = { cctv: 0.07, street: 0.03, alpr: 0.035, bikes: 0.04, streams: 0.4, signed: 0.6, venues: 0.9, power: 0.7, dc: 1.6, dams: 1.6, milbase: 1.6, airports: 1.3, ports: 1.4, imagery: 1.6, fishing: 2.2, radar: 1.6 }; W.render = function (view, chain, outFb, opts = {}) { const gl = GL.gl, PR = GL.progs; @@ -2634,7 +2637,7 @@ W.render = function (view, chain, outFb, opts = {}) { // alpha pass (smoke, AQ, radar, GNSS, cone) gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE); common(SP); gl.uniform1f(SP.u.uPass, 1); - for (const id in W.static) if (W.on(id) && range < (LOD[id] || 99)) for (const s of W.static[id]) if (s.kind === 'sprite' && s.pass === 'alpha') { gl.bindVertexArray(s.vao); gl.drawArrays(gl.POINTS, 0, s.count); } + for (const id in W.static) if (W.on(id) && range < (LOD[id] || 99) && range > (LOD_MIN[id] || 0)) for (const s of W.static[id]) if (s.kind === 'sprite' && s.pass === 'alpha') { gl.bindVertexArray(s.vao); gl.drawArrays(gl.POINTS, 0, s.count); } if (W.alphaVao && W.alphaCount) { gl.bindVertexArray(W.alphaVao); gl.drawArrays(gl.POINTS, 0, W.alphaCount); } gl.disable(gl.BLEND); gl.disable(gl.DEPTH_TEST); gl.depthMask(true); gl.bindVertexArray(null); @@ -2934,7 +2937,7 @@ SC.draw = function (sc, view, { sun, t, eye, opts }) { const clear = (opts.lens || GE.world.lens) === 'clear'; gl.uniform3fv(P.u.uSunCol, clear ? [2.2, 2.2, 2.2] : tr.map((x) => x * k * (opts.nightForce ? 0.03 : 1))); const amb = smooth(-0.25, 0.5, sunL[2]); - gl.uniform3fv(P.u.uSkyCol, clear ? [1.2, 1.25, 1.35] : [0.03 * 20 / Math.PI * 1.6 * amb + 0.014, 0.045 * 20 / Math.PI * 1.6 * amb + 0.02, 0.085 * 20 / Math.PI * 1.6 * amb + 0.04]); + gl.uniform3fv(P.u.uSkyCol, clear ? [1.2, 1.25, 1.35] : [0.03 * 20 / Math.PI * 0.9 * amb + 0.014, 0.045 * 20 / Math.PI * 0.9 * amb + 0.02, 0.085 * 20 / Math.PI * 0.9 * amb + 0.04]); // fog colour: inscatter along the horizon in the camera's look direction const fwd = v3.scale(view.back, -1); const fh = v3.norm(v3.madd(fwd, e.up, -v3.dot(fwd, e.up))); const ro = v3.madd(o, e.up, 0.0003); let fog = atmoJS(ro, fh, 0.012, sd, 8); fog = fog.map((x) => x * 0.8 + 0.0015); @@ -3195,7 +3198,7 @@ add({ id: 'cyclone', short: 'Typhoon Halcyon', fam: 'haz', kind: 'cyclone', section: 'now', live: true, aspect: 1.25, ageS: 120, watchers: 48200, title: 'Typhoon Halcyon tracks toward Luzon Strait', place: 'Philippine Sea', lat: 16.2, lon: 128.8, camera: { lat: 15.2, lon: 127.6, range: 0.42, heading: -0.25, tilt: 0.42 }, thumbCam: { lat: 16.6, lon: 128.4, range: 0.3, heading: -0.2, tilt: 0.36 }, - layers: ['cyclones', 'radar', 'lightning', 'clouds', 'models', 'ais', 'flights'], source: 'Advisory feed (illustrative)', + layers: ['cyclones', 'lightning', 'clouds', 'models', 'ais', 'flights'], source: 'Advisory feed (illustrative)', metric: () => { const c = GE.CYCLONE.at(GE.world.offsetH); return [`${Math.round(c.wind)}km/h`, `${Math.round(c.p)} hPa · moving WNW 19 km/h`]; }, metrics: () => { const c = GE.CYCLONE.at(GE.world.offsetH); return [{ v: Math.round(c.wind), u: 'km/h', l: 'Max sustained', s: 'Advisory · 2 min' }, { v: Math.round(c.p), u: 'hPa', l: 'Central pressure', s: 'Advisory · 2 min' }, { v: 610, u: 'km', l: 'To Luzon Strait', s: 'Computed · now' }]; }, accessory: () => { const c = GE.CYCLONE.at(GE.world.offsetH); return `${Math.round(c.wind)} km/h · ${Math.round(c.p)} hPa · updated 2 min ago`; }, @@ -3263,7 +3266,7 @@ add({ add({ id: 'novosel', short: 'Fort Novosel circuits', fam: 'sky', kind: 'heli', section: 'always', live: true, aspect: 1.5, ageS: 15, watchers: 3100, ride: true, title: 'Fort Novosel helicopter school — jump into the chopper', place: 'Cairns Army Airfield, Alabama', lat: 31.276, lon: -85.713, - camera: { lat: 31.268, lon: -85.705, range: 0.0019, heading: -0.6, tilt: 1.12 }, layers: ['helis', 'flights', 'ga', 'traffic'], source: 'ADS-B / MLAT', time: dayReplay(31.276, -85.713, 15.2), + camera: { lat: 31.2705, lon: -85.7075, range: 0.0011, heading: -0.65, tilt: 1.15 }, layers: ['helis', 'flights', 'ga', 'traffic'], source: 'ADS-B / MLAT', time: dayReplay(31.276, -85.713, 15.2), metric: () => [`${GE.HELI_CIRCUITS.filter((h) => h.field).length}in circuits`, 'Cairns + 3 stagefields · 500 ft AGL'], metrics: () => [{ v: GE.HELI_CIRCUITS.filter((h) => h.field).length, u: '', l: 'Helicopters in circuits', s: 'ADS-B · 15 s' }, { v: 500, u: 'ft', l: 'Pattern altitude', s: 'Observed' }, { v: 5.5, u: 'min', l: 'Circuit time', s: 'Derived' }], brief: { lede: 'Student pilots are flying rectangular traffic patterns at Cairns and three stagefields: upwind, crosswind, downwind, base, final — over and over. Tap Ride to sit in the left seat of one of them.', series: [{ label: 'Helicopters aloft', unit: '', data: [2, 4, 9, 14, 17, 19, 19, 18, 19, 17, 19, 19], src: 'ADS-B / MLAT · hourly · 15 s', fmt: (v) => v }, { label: 'Groundspeed, HAWK 21', unit: 'kt', data: Array.from({ length: 40 }, (_, i) => 70 + 12 * Math.sin(i / 3.2) + (i % 10 > 7 ? -30 : 0)), src: 'ADS-B · 1 s · 15 s', fmt: (v) => Math.round(v) }], sources: [['ADS-B / MLAT', '15 s', 'live']] }, @@ -3431,8 +3434,8 @@ APP.minimize = function (on) { }; APP.showAcc = () => !!APP.following && (APP.tab === 'now' || APP.tab === 'threads' || APP.tab === 'you' || (APP.isWide() && APP.tab === 'globe')) && !APP.mode; APP.updateChrome = function () { - const ride = APP.mode === 'ride', sit = APP.mode === 'situation'; - $('#tabwrap').classList.toggle('hidden', ride || (sit && !APP.isWide()) || APP.intro); + const ride = APP.mode === 'ride', sit = APP.mode === 'situation', wsm = APP.mode === 'workspace'; + $('#tabwrap').classList.toggle('hidden', ride || ((sit || wsm) && !APP.isWide()) || APP.intro); $('#accHost').classList.toggle('hidden', !APP.showAcc() || APP._mini || APP.intro); $('#back').classList.toggle('on', sit); $('#sitbtns').classList.toggle('on', sit); @@ -3734,12 +3737,12 @@ SH.open = function (id, o = {}) { if (o.ride && s.ride) setTimeout(() => GE.ride.start({ kind: 'heli', id: 0 }), 650); APP.updateChrome(); }; -SH.close = function (silent) { +SH.close = function (silent, keepTime) { if (!SH.id) return; const id = SH.id; const W = GE.world; const el = sheetEl(); el.classList.remove('drag', 'wopen'); el.style.transform = APP.isWide() ? '' : 'translateY(100%)'; SH.id = null; W.focus = null; GE.setExtras && GE.setExtras(null); if (SH._savedLayers) { W.layers.forEach((l, i) => (l.on = SH._savedLayers[i])); SH._savedLayers = null; bus.emit('layers', W.layers); } - W.setWindow(null); W.setTime(null); W.auroraK = 0.85; + if (!keepTime) { W.setWindow(null); W.setTime(null); } W.auroraK = 0.85; APP.setMode(''); if (silent) return; const fromNow = !APP.isWide() && GE.now && $(`#v-now .card[data-id="${id}"]`); @@ -4129,7 +4132,7 @@ const CHAT_JET = [ ]; RIDE.start = function (p) { const W = GE.world; - if (APP.mode === 'situation') { GE.sheet.close(true); } + if (APP.mode === 'situation') { GE.sheet.close(true, true); } RIDE.p = p; RIDE.on = true; RIDE.yaw = 0; RIDE.pitch = 0; RIDE.zoom = 1; if (APP.tab !== 'globe') { APP.tab = 'globe'; $('#app').dataset.tab = 'globe'; $$('.pane .view').forEach((v) => v.classList.remove('on')); } APP.setMode('ride'); @@ -4185,7 +4188,7 @@ RIDE.frame = function (view) { g += ``; g += ``; // heading tape - const ty = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--sat')) + 70; + const ty = (parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--sat')) || 0) + 100; g += ``; for (let d = Math.floor(hdg / 5) * 5 - 40; d <= hdg + 40; d += 5) { const x = cx + (d - hdg) * 4; const dd = ((d % 360) + 360) % 360; g += ``; if (dd % 30 === 0) g += `${{ 0: 'N', 90: 'E', 180: 'S', 270: 'W' }[dd] ?? String(dd / 10).padStart(2, '0')}`; } g += `${String(Math.round(hdg)).padStart(3, '0')}`; @@ -4198,7 +4201,7 @@ RIDE.exit = function (silent) { $('#ride').classList.remove('on'); $('#ride').innerHTML = ''; const v = Cam.view(400, 800); const [la, lo] = geo.ll(v.eye); Cam.setRig(null); Cam.set({ lat: la, lon: lo, range: Math.max(0.002, v3.len(v.eye) - 1 + 0.002), tilt: 0.9, heading: RIDE.s ? RIDE.s.hdg : 0, shiftY: 0.06 }); - APP.setMode(''); + APP.setMode(''); GE.world.setWindow(null); GE.world.setTime(null); if (!silent) Cam.flyTo({ range: 0.012, tilt: 0.8 }); }; @@ -4315,7 +4318,7 @@ CAP.frame = function (view) { if (!el) { el = h('div', { 'data-k': it.k }); el.innerHTML = `
${ic(it.i, 'sm')}${esc(it.t)}
`; host.append(el); } el.querySelector('[data-s]').textContent = it.s; el.querySelector('.reticle').style.transform = `translate(${s.x}px, ${s.y}px)`; - el.querySelector('.callout').style.transform = `translate(${s.x + 24}px, ${s.y}px) translate(0, -50%)`; + const flip = s.x > view.w - 210; el.querySelector('.callout').style.transform = flip ? `translate(${s.x - 24}px, ${s.y}px) translate(-100%, -50%)` : `translate(${s.x + 24}px, ${s.y}px) translate(0, -50%)`; } host.querySelectorAll('[data-k]').forEach((e) => { if (!seen.has(e.dataset.k)) e.remove(); }); // witness sensors @@ -4357,7 +4360,7 @@ CAP.ceremony = function (b, cv) { ov.innerHTML = `
${ic('check', 'xs')}
Frame hashed · SHA-256 of ${fmt.n(b.bytes.length)} JPEG bytes
…
-
${ic('check', 'xs')}
Session sealed · IMU 150 samples @ 50 Hz · GPS ±4 m · heading ${b.session.heading}°${GE.sparkPath(b.session.imu.map((x) => x.g + x.b * 0.02), 300, 30, '#5CF2A0', 1.2)}
+
${ic('check', 'xs')}
Session sealed · IMU 150 samples @ 50 Hz · GPS ±4 m · heading ${b.session.heading}°${GE.sparkPath(b.session.imu.map((x) => x.g + x.b * 0.02), 300, 30, '#5CF2A0', 1.2)}
${ic('check', 'xs')}
Chained to this device’s previous bundle
prev ${b.prev.slice(0, 24)}…
${ic('check', 'xs')}
Bundle hash
…
${ic('check', 'xs')}
Signed · ECDSA P-256, key generated on device, non-extractable
…
@@ -4536,12 +4539,11 @@ THR.init = async function () { /* ---------- list ---------- */ THR.render = function () { const v = $('#v-threads'); - v.innerHTML = `
${ic('seal', 'xs')} Signed evidence · open ledgers

Threads

+ v.innerHTML = `
${ic('seal', 'xs')} Signed evidence · open ledgers

Threads

${ic('sparkles', 'xs')} Concept · illustrative data
${['kora', 'hydro', 'tamang', 'geo', 'sky'].map((k) => `${P[k].h[0].toUpperCase()}`).join('')}37 people reconstructing now
${Object.values(THREADS).map((T) => { const s = GE.SIT.byId[T.sit]; return `

${esc(T.title)}

${esc(T.place)} · ${esc(T.date)}

4D
${T.obs.length}observations
${T.hyps.length}hypotheses
${THR.ledgers[T.id]?.length || '–'}ledger
${T.people.slice(0, 4).map((k) => `${P[k].h[0].toUpperCase()}`).join('')}
`; }).join('')}

Concept · illustrative data. Witness posts, handles and claims are fictional.

`; - $('#thMe').addEventListener('click', () => APP.setTab('you')); $('#newThread').addEventListener('click', () => APP.toast(`${ic('threads', 'sm')} New thread · add your first observation`)); $$('.tcard').forEach((c) => { c.addEventListener('click', () => THR.openWs(c.dataset.t)); @@ -4775,7 +4777,7 @@ function globeVisible() { let lastT = performance.now(), lastDyn = 0, lastSec = 0, running = true; function frame(now) { if (!running) return; - const dt = Math.min(0.1, Math.max(0.001, (now - lastT) / 1000)); lastT = now; + const dt = Math.min(0.25, Math.max(0.001, (now - lastT) / 1000)); lastT = now; W.simMs = Date.now() + W.offsetH * 3600000; W.t = (W.simMs - NOW) / 1000; W.sun = astro.sun(W.simMs); Cam.update(dt); if (APP.mode === 'situation' && !Cam.anim && GE.sheet.targetShift != null && !APP.isWide()) Cam.shiftY += (GE.sheet.targetShift - Cam.shiftY) * Math.min(1, dt * 7); @@ -4811,7 +4813,7 @@ function coldOpen() { APP.intro = true; app.classList.add('intro'); APP.updateChrome(); if (skipAll) { Cam.set(finalCam); end(); return; } Cam.set({ lat: 8, lon: finalCam.lon + 75, range: 5.8, heading: 0, tilt: 0, shiftY: 0.0, shiftX: 0 }); - const T = red ? 1.2 : 3.4; const t0 = performance.now(); + const T = (red ? 1.2 : 3.4) * (HASH.slow ? +HASH.slow : 1); const t0 = performance.now(); GE.introFade = 0; if (!red) Cam.flyTo(finalCam, { duration: T }); else Cam.set(finalCam); diff --git a/mobile/vision/src/app.css b/mobile/vision/src/app.css index f33a7c2a0..e5493184b 100644 --- a/mobile/vision/src/app.css +++ b/mobile/vision/src/app.css @@ -302,7 +302,7 @@ input, textarea { font: inherit; color: inherit; } .compose .in input { flex: 1; background: none; border: 0; outline: none; color: var(--text); font-size: 15px; min-width: 0; } /* ---------- capture ---------- */ -#v-capture { background: #000; z-index: 25; } +#v-capture { background: transparent; z-index: 25; } #v-capture.on { display: block; } .cap-ui { position: absolute; inset: 0; pointer-events: none; } .cap-ui > * { pointer-events: auto; } @@ -315,7 +315,7 @@ input, textarea { font: inherit; color: inherit; } .callout span { color: var(--text2); font-size: 11px; } .callout .ci { width: 28px; height: 28px; border-radius: 9px; display: grid; place-items: center; background: color-mix(in oklab, var(--c) 22%, transparent); color: var(--c); } .reticle { position: absolute; width: 34px; height: 34px; margin: -17px 0 0 -17px; border: 1.5px solid var(--c, #fff); border-radius: 10px; pointer-events: none; box-shadow: 0 0 12px var(--c); } -.cap-bottom { position: absolute; left: 0; right: 0; bottom: 0; padding: 10px 16px calc(var(--sab) + 18px); display: flex; flex-direction: column; align-items: center; gap: 14px; background: linear-gradient(to top, rgba(0,0,0,.55), transparent); } +.cap-bottom { position: absolute; left: 0; right: 0; bottom: 0; padding: 10px 16px calc(var(--sab) + var(--tabH) + 26px); display: flex; flex-direction: column; align-items: center; gap: 14px; background: linear-gradient(to top, rgba(0,0,0,.55), transparent); } .zooms { display: flex; gap: 6px; padding: 4px; border-radius: 22px; } .zooms button { width: 36px; height: 36px; border-radius: 50%; font-size: 12px; font-weight: 700; color: var(--text2); } .zooms button.on { background: rgba(255,255,255,.14); color: var(--warn); } @@ -329,7 +329,7 @@ input, textarea { font: inherit; color: inherit; } .shutter:active i { transform: scale(.88); } .lastcap { width: 48px; height: 48px; border-radius: 12px; background: #111 center/cover; box-shadow: inset 0 0 0 1px rgba(255,255,255,.2); position: relative; } .lastcap .seal { position: absolute; right: -6px; top: -6px; width: 20px; height: 20px; } -.sensors { position: absolute; left: 12px; bottom: calc(var(--sab) + 210px); border-radius: 16px; padding: 8px 10px; font-size: 11px; display: none; gap: 3px; flex-direction: column; min-width: 158px; color: var(--text2); } +.sensors { position: absolute; left: 12px; bottom: calc(var(--sab) + var(--tabH) + 236px); border-radius: 16px; padding: 8px 10px; font-size: 11px; display: none; gap: 3px; flex-direction: column; min-width: 158px; color: var(--text2); } .sensors.on { display: flex; } .sensors b { color: var(--text); font-variant-numeric: tabular-nums; font-weight: 600; } .sensors svg { width: 140px; height: 26px; } @@ -346,7 +346,7 @@ input, textarea { font: inherit; color: inherit; } .step .ck { width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center; box-shadow: inset 0 0 0 1.5px var(--text3); color: transparent; transition: all .3s; } .step.done .ck { background: var(--gold); box-shadow: none; color: #1A1408; } .step b { font-weight: 600; } .step .h { font-family: var(--mono); font-size: 11px; color: var(--text2); word-break: break-all; line-height: 1.35; margin-top: 2px; } -.step svg { width: 100%; height: 30px; margin-top: 4px; } +.step svg.trace { width: 100%; height: 30px; margin-top: 4px; display: block; } .btnrow { display: flex; gap: 8px; } .btn { flex: 1; height: 50px; border-radius: 25px; font-weight: 700; font-size: 15px; display: inline-flex; align-items: center; justify-content: center; gap: 7px; background: rgba(255,255,255,.1); box-shadow: inset 0 0 0 .5px rgba(255,255,255,.12); } .btn.gold { background: linear-gradient(180deg, #F3D68A, #D9AE4F); color: #1C1405; box-shadow: inset 0 .5px 0 rgba(255,255,255,.6); } @@ -372,7 +372,7 @@ input, textarea { font: inherit; color: inherit; } .ws .wsh > * { pointer-events: auto; } .ws .wsh .tt { flex: 1; min-width: 0; height: 44px; border-radius: 22px; display: flex; align-items: center; gap: 10px; padding: 0 8px 0 14px; } .ws .wsh .tt b { font-size: 14.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; } -.ws .wsh .tt span { font-size: 11.5px; color: var(--text2); } +.ws .wsh .tt span { font-size: 11.5px; color: var(--text2); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ws .body { position: absolute; left: 0; right: 0; bottom: 0; top: 40%; border-radius: 28px 28px 0 0; display: flex; flex-direction: column; overflow: hidden; } .ws .seg { display: flex; margin: 12px 14px 8px; padding: 3px; border-radius: 12px; background: rgba(255,255,255,.07); flex: none; } .ws .seg button { flex: 1; height: 34px; border-radius: 9px; font-size: 13px; font-weight: 600; color: var(--text2); transition: all .25s; } @@ -386,7 +386,7 @@ input, textarea { font: inherit; color: inherit; } .obs .ot { font-size: 14px; font-weight: 600; line-height: 1.3; } .obs .om { font-size: 11.5px; color: var(--text3); margin-top: 3px; display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; } .obs .om .mono { color: var(--text2); } -.obs .tagid { font-family: var(--mono); font-size: 11px; color: var(--gold2); background: rgba(233,196,106,.12); padding: 1px 6px; border-radius: 6px; } +.tagid { display: inline-block; font-family: var(--font); font-weight: 700; font-size: 11px; letter-spacing: .02em; color: var(--gold2); background: rgba(233,196,106,.13); box-shadow: inset 0 0 0 .5px rgba(233,196,106,.35); padding: 2px 7px; border-radius: 7px; line-height: 1.3; } .hyp { border-radius: 20px; padding: 13px 14px; margin: 0 0 10px; background: var(--card2); box-shadow: inset 0 0 0 .5px rgba(255,255,255,.08); } .hyp.ai { box-shadow: inset 0 0 0 1px rgba(183,156,255,.45); background: linear-gradient(180deg, rgba(183,156,255,.10), rgba(183,156,255,.02)), var(--card2); } .hyp .hh { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text2); } @@ -495,7 +495,9 @@ input, textarea { font: inherit; color: inherit; } .cold .eye { width: 60px; height: 60px; margin-bottom: 18px; opacity: 0; transition: opacity 1.2s; color: #fff; } .cold.s2 .eye { opacity: .95; } .concept-tag { position: absolute; z-index: 90; left: 14px; top: calc(var(--sat) + 58px); pointer-events: none; font-size: 9.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: rgba(246,223,160,.72); padding: 3px 7px; border-radius: 6px; background: rgba(10,12,18,.35); box-shadow: inset 0 0 0 .5px rgba(233,196,106,.3); } -.app[data-tab="now"] .concept-tag, .app[data-tab="threads"] .concept-tag, .app[data-tab="you"] .concept-tag { top: calc(var(--sat) + 14px); left: auto; right: 14px; } +.app[data-tab="now"] .concept-tag { top: calc(var(--sat) + 14px); left: auto; right: 14px; } +.app[data-tab="threads"]:not([data-mode="workspace"]) .concept-tag, .app[data-tab="you"] .concept-tag { display: none; } +.app[data-mode="workspace"] .concept-tag { top: calc(var(--sat) + 62px); left: auto; right: 14px; } .app[data-tab="capture"] .concept-tag { top: calc(var(--sat) + 100px); left: 14px; } .app[data-mode="situation"] .concept-tag { left: auto; right: 14px; top: calc(var(--sat) + 62px); } diff --git a/mobile/vision/src/js/20-gl.js b/mobile/vision/src/js/20-gl.js index c4007b35a..5683866b9 100644 --- a/mobile/vision/src/js/20-gl.js +++ b/mobile/vision/src/js/20-gl.js @@ -145,7 +145,7 @@ vec3 aurora(vec3 n, float nightF){ float curt = 0.45 + 0.55*fbm(vec2(lonm*9. + uT*0.0006, uT*0.00025)); float g = exp(-pow((ml-band)/(2.2+uAurora*0.6), 2.)) * curt; float r = exp(-pow((ml-band-3.5)/3.5, 2.)) * 0.25; - return (vec3(0.12, 1.0, 0.5)*g + vec3(0.75, 0.15, 0.45)*r*0.45) * nightF * uAurora * 1.15; + return (vec3(0.12, 1.0, 0.5)*g + vec3(0.75, 0.15, 0.45)*r*0.45) * nightF * uAurora * 0.75; } vec3 cityDetail(vec3 base, vec2 q, float urban, float night, float fw, out vec3 emis){ emis = vec3(0.); @@ -175,7 +175,7 @@ vec3 cityDetail(vec3 base, vec2 q, float urban, float night, float fw, out vec3 if (uTraffic > 0.5) { vec2 rp = p/B; float along = abs(f.x-0.5) < abs(f.y-0.5) ? rp.y : rp.x; float cars = smoothstep(0.75, 1., sin(along*38. - uT*2.5 + h*20.)) * road * urban * (1.-aa); - emis += mix(vec3(1.,0.15,0.1), vec3(1.,0.95,0.8), step(0.5, fract(along*3.)))*cars*(0.25 + night*1.4); + emis += mix(vec3(1.,0.15,0.1), vec3(1.,0.95,0.8), step(0.5, fract(along*3.)))*cars*(0.03 + night*1.2); } float lit = step(0.72, hash12(id*3.1)) * (1.-road) * urban * night; emis += vec3(0.9,0.8,0.6)*lit*0.035; @@ -303,7 +303,7 @@ void main(){ float tc = -b; vec3 cp = ro + rd*max(tc,0.); float hr = length(cp); if (uAurora > 0. && hr > 1.004 && hr < 1.05) { vec3 cn = normalize(cp); float nf = 1.-smoothstep(-0.2,0.0,dot(cn,uSun)); - col += aurora(cn, nf) * smoothstep(1.05,1.012,hr) * smoothstep(1.004,1.012,hr) * 0.3; + col += aurora(cn, nf) * smoothstep(1.05,1.012,hr) * smoothstep(1.004,1.012,hr) * 0.12 * smoothstep(1.5, 0.4, uRange); } heat = 0.; } @@ -587,8 +587,8 @@ void main(){ vec3 base; vec3 emis = vec3(0.); float heat = 0.35; float spec = 0.; if (type == 0) { // building float h = vAttr.y; - base = mix(vec3(0.34,0.35,0.37), vec3(0.62,0.57,0.5), h); - base = mix(base, vec3(0.2,0.25,0.3), step(0.7, h)); // glass towers + base = mix(vec3(0.15,0.155,0.165), vec3(0.32,0.29,0.25), fract(h*7.13)); + base = mix(base, vec3(0.07,0.09,0.11), step(0.68, h)); // glass towers if (abs(n.z) < 0.5) { vec2 f = vec2(vUV.x/3.2, vUV.y/3.6); vec2 id = floor(f); vec2 fr = fract(f); @@ -598,7 +598,7 @@ void main(){ vec3 wc = mix(vec3(1.0,0.72,0.42), vec3(0.75,0.85,1.0), step(0.6, hash12(id*1.3+h))); emis += wc * lit * (0.9 + 0.6*r) * uNight * 2.2; base = mix(base, base*0.45 + uSkyCol*0.08, win*0.8); - spec = win*(1.-uNight)*0.6; + spec = win*(1.-uNight)*0.9 + step(0.68, h)*0.5; } else base *= 0.82; float ao = smoothstep(0., 14., vUV.y + (1.-abs(n.z))*0.) * 0.35 + 0.65; if (abs(n.z) < 0.5) base *= ao; diff --git a/mobile/vision/src/js/30-world.js b/mobile/vision/src/js/30-world.js index 786b8b450..172244c4d 100644 --- a/mobile/vision/src/js/30-world.js +++ b/mobile/vision/src/js/30-world.js @@ -30,10 +30,10 @@ W.layers = [ L('holo', 'eyes', 'Holograms', true, 'cube'), L('streams', 'eyes', 'Live streams', false, 'live'), L('transit', 'ground', 'Transit vehicles', false, 'tram'), L('traffic', 'ground', 'Traffic flow', true, 'car'), L('bikes', 'ground', 'Bike share', false, 'bike'), L('rail', 'ground', 'Rail', false, 'rail'), L('venues', 'ground', 'Events & venues', true, 'ticket'), L('alpr', 'ground', 'ALPR cameras', false, 'plate'), - L('radar', 'wx', 'Radar', true, 'radar'), L('clouds', 'wx', 'Clouds', true, 'cloud'), L('lightning', 'wx', 'Lightning', true, 'bolt'), + L('radar', 'wx', 'Radar', false, 'radar'), L('clouds', 'wx', 'Clouds', true, 'cloud'), L('lightning', 'wx', 'Lightning', true, 'bolt'), L('wind', 'wx', 'Wind', false, 'wind'), L('models', 'wx', 'Forecast models', false, 'spaghetti'), L('dc', 'infra', 'Datacenters', false, 'server'), L('dams', 'infra', 'Dams', false, 'dam'), L('power', 'infra', 'Power plants', false, 'bolt'), - L('milbase', 'infra', 'Military installations', false, 'shield'), L('gnss', 'infra', 'GNSS interference', true, 'jam'), L('imagery', 'infra', 'Recent satellite imagery', false, 'frame'), + L('milbase', 'infra', 'Military installations', false, 'shield'), L('gnss', 'infra', 'GNSS interference', false, 'jam'), L('imagery', 'infra', 'Recent satellite imagery', false, 'frame'), ]; W.layerById = Object.fromEntries(W.layers.map((l) => [l.id, l])); W.on = (id) => !!W.layerById[id]?.on; @@ -306,7 +306,7 @@ W.updateDynamic = function () { GE.SHIPS.forEach((s, i) => { const p = W._shipCache[i]; const tank = s.kind === 'tanker' || s.kind === 'LNG'; if (!showAis && !tank) return; - const ak = clamp(lerp(0.32, 0.95, smooth(1.2, 0.08, W.lastRange ?? 1)), 0.3, 1); + const ak = clamp(lerp(0.16, 0.95, smooth(1.4, 0.08, W.lastRange ?? 1)), 0.16, 1); const col = showTk && tank ? [1, 0.7, 0.35, ak] : s.queue ? [0.5, 1, 0.9, ak] : [0.22, 0.88, 0.78, ak * 0.95]; pushSpr(a, geo.vec(p.lat, p.lon, 0.03), col, (showTk && tank ? 10 : 8.5), 4, p.hdg, -1e9, 1e9, 0, i, 0.35); }); @@ -402,6 +402,7 @@ W.project = function (view, p) { // world unit coords → css px {x,y,z,vis} /* ---------- render ---------- */ W.quality = { scale: 1, steps: 12, bloom: true, grain: true }; +const LOD_MIN = { radar: 0.12 }; const LOD = { cctv: 0.07, street: 0.03, alpr: 0.035, bikes: 0.04, streams: 0.4, signed: 0.6, venues: 0.9, power: 0.7, dc: 1.6, dams: 1.6, milbase: 1.6, airports: 1.3, ports: 1.4, imagery: 1.6, fishing: 2.2, radar: 1.6 }; W.render = function (view, chain, outFb, opts = {}) { const gl = GL.gl, PR = GL.progs; @@ -500,7 +501,7 @@ W.render = function (view, chain, outFb, opts = {}) { // alpha pass (smoke, AQ, radar, GNSS, cone) gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE); common(SP); gl.uniform1f(SP.u.uPass, 1); - for (const id in W.static) if (W.on(id) && range < (LOD[id] || 99)) for (const s of W.static[id]) if (s.kind === 'sprite' && s.pass === 'alpha') { gl.bindVertexArray(s.vao); gl.drawArrays(gl.POINTS, 0, s.count); } + for (const id in W.static) if (W.on(id) && range < (LOD[id] || 99) && range > (LOD_MIN[id] || 0)) for (const s of W.static[id]) if (s.kind === 'sprite' && s.pass === 'alpha') { gl.bindVertexArray(s.vao); gl.drawArrays(gl.POINTS, 0, s.count); } if (W.alphaVao && W.alphaCount) { gl.bindVertexArray(W.alphaVao); gl.drawArrays(gl.POINTS, 0, W.alphaCount); } gl.disable(gl.BLEND); gl.disable(gl.DEPTH_TEST); gl.depthMask(true); gl.bindVertexArray(null); diff --git a/mobile/vision/src/js/35-scenes.js b/mobile/vision/src/js/35-scenes.js index e74a232b3..e2502b10e 100644 --- a/mobile/vision/src/js/35-scenes.js +++ b/mobile/vision/src/js/35-scenes.js @@ -220,7 +220,7 @@ SC.draw = function (sc, view, { sun, t, eye, opts }) { const clear = (opts.lens || GE.world.lens) === 'clear'; gl.uniform3fv(P.u.uSunCol, clear ? [2.2, 2.2, 2.2] : tr.map((x) => x * k * (opts.nightForce ? 0.03 : 1))); const amb = smooth(-0.25, 0.5, sunL[2]); - gl.uniform3fv(P.u.uSkyCol, clear ? [1.2, 1.25, 1.35] : [0.03 * 20 / Math.PI * 1.6 * amb + 0.014, 0.045 * 20 / Math.PI * 1.6 * amb + 0.02, 0.085 * 20 / Math.PI * 1.6 * amb + 0.04]); + gl.uniform3fv(P.u.uSkyCol, clear ? [1.2, 1.25, 1.35] : [0.03 * 20 / Math.PI * 0.9 * amb + 0.014, 0.045 * 20 / Math.PI * 0.9 * amb + 0.02, 0.085 * 20 / Math.PI * 0.9 * amb + 0.04]); // fog colour: inscatter along the horizon in the camera's look direction const fwd = v3.scale(view.back, -1); const fh = v3.norm(v3.madd(fwd, e.up, -v3.dot(fwd, e.up))); const ro = v3.madd(o, e.up, 0.0003); let fog = atmoJS(ro, fh, 0.012, sd, 8); fog = fog.map((x) => x * 0.8 + 0.0015); diff --git a/mobile/vision/src/js/50-situations.js b/mobile/vision/src/js/50-situations.js index 955dfda8e..5fcde58ae 100644 --- a/mobile/vision/src/js/50-situations.js +++ b/mobile/vision/src/js/50-situations.js @@ -48,7 +48,7 @@ add({ id: 'cyclone', short: 'Typhoon Halcyon', fam: 'haz', kind: 'cyclone', section: 'now', live: true, aspect: 1.25, ageS: 120, watchers: 48200, title: 'Typhoon Halcyon tracks toward Luzon Strait', place: 'Philippine Sea', lat: 16.2, lon: 128.8, camera: { lat: 15.2, lon: 127.6, range: 0.42, heading: -0.25, tilt: 0.42 }, thumbCam: { lat: 16.6, lon: 128.4, range: 0.3, heading: -0.2, tilt: 0.36 }, - layers: ['cyclones', 'radar', 'lightning', 'clouds', 'models', 'ais', 'flights'], source: 'Advisory feed (illustrative)', + layers: ['cyclones', 'lightning', 'clouds', 'models', 'ais', 'flights'], source: 'Advisory feed (illustrative)', metric: () => { const c = GE.CYCLONE.at(GE.world.offsetH); return [`${Math.round(c.wind)}km/h`, `${Math.round(c.p)} hPa · moving WNW 19 km/h`]; }, metrics: () => { const c = GE.CYCLONE.at(GE.world.offsetH); return [{ v: Math.round(c.wind), u: 'km/h', l: 'Max sustained', s: 'Advisory · 2 min' }, { v: Math.round(c.p), u: 'hPa', l: 'Central pressure', s: 'Advisory · 2 min' }, { v: 610, u: 'km', l: 'To Luzon Strait', s: 'Computed · now' }]; }, accessory: () => { const c = GE.CYCLONE.at(GE.world.offsetH); return `${Math.round(c.wind)} km/h · ${Math.round(c.p)} hPa · updated 2 min ago`; }, @@ -116,7 +116,7 @@ add({ add({ id: 'novosel', short: 'Fort Novosel circuits', fam: 'sky', kind: 'heli', section: 'always', live: true, aspect: 1.5, ageS: 15, watchers: 3100, ride: true, title: 'Fort Novosel helicopter school — jump into the chopper', place: 'Cairns Army Airfield, Alabama', lat: 31.276, lon: -85.713, - camera: { lat: 31.268, lon: -85.705, range: 0.0019, heading: -0.6, tilt: 1.12 }, layers: ['helis', 'flights', 'ga', 'traffic'], source: 'ADS-B / MLAT', time: dayReplay(31.276, -85.713, 15.2), + camera: { lat: 31.2705, lon: -85.7075, range: 0.0011, heading: -0.65, tilt: 1.15 }, layers: ['helis', 'flights', 'ga', 'traffic'], source: 'ADS-B / MLAT', time: dayReplay(31.276, -85.713, 15.2), metric: () => [`${GE.HELI_CIRCUITS.filter((h) => h.field).length}in circuits`, 'Cairns + 3 stagefields · 500 ft AGL'], metrics: () => [{ v: GE.HELI_CIRCUITS.filter((h) => h.field).length, u: '', l: 'Helicopters in circuits', s: 'ADS-B · 15 s' }, { v: 500, u: 'ft', l: 'Pattern altitude', s: 'Observed' }, { v: 5.5, u: 'min', l: 'Circuit time', s: 'Derived' }], brief: { lede: 'Student pilots are flying rectangular traffic patterns at Cairns and three stagefields: upwind, crosswind, downwind, base, final — over and over. Tap Ride to sit in the left seat of one of them.', series: [{ label: 'Helicopters aloft', unit: '', data: [2, 4, 9, 14, 17, 19, 19, 18, 19, 17, 19, 19], src: 'ADS-B / MLAT · hourly · 15 s', fmt: (v) => v }, { label: 'Groundspeed, HAWK 21', unit: 'kt', data: Array.from({ length: 40 }, (_, i) => 70 + 12 * Math.sin(i / 3.2) + (i % 10 > 7 ? -30 : 0)), src: 'ADS-B · 1 s · 15 s', fmt: (v) => Math.round(v) }], sources: [['ADS-B / MLAT', '15 s', 'live']] }, diff --git a/mobile/vision/src/js/60-shell.js b/mobile/vision/src/js/60-shell.js index 872dccfcd..4d0d4ffae 100644 --- a/mobile/vision/src/js/60-shell.js +++ b/mobile/vision/src/js/60-shell.js @@ -55,8 +55,8 @@ APP.minimize = function (on) { }; APP.showAcc = () => !!APP.following && (APP.tab === 'now' || APP.tab === 'threads' || APP.tab === 'you' || (APP.isWide() && APP.tab === 'globe')) && !APP.mode; APP.updateChrome = function () { - const ride = APP.mode === 'ride', sit = APP.mode === 'situation'; - $('#tabwrap').classList.toggle('hidden', ride || (sit && !APP.isWide()) || APP.intro); + const ride = APP.mode === 'ride', sit = APP.mode === 'situation', wsm = APP.mode === 'workspace'; + $('#tabwrap').classList.toggle('hidden', ride || ((sit || wsm) && !APP.isWide()) || APP.intro); $('#accHost').classList.toggle('hidden', !APP.showAcc() || APP._mini || APP.intro); $('#back').classList.toggle('on', sit); $('#sitbtns').classList.toggle('on', sit); diff --git a/mobile/vision/src/js/62-sheet.js b/mobile/vision/src/js/62-sheet.js index a7763627f..fef7fd154 100644 --- a/mobile/vision/src/js/62-sheet.js +++ b/mobile/vision/src/js/62-sheet.js @@ -45,12 +45,12 @@ SH.open = function (id, o = {}) { if (o.ride && s.ride) setTimeout(() => GE.ride.start({ kind: 'heli', id: 0 }), 650); APP.updateChrome(); }; -SH.close = function (silent) { +SH.close = function (silent, keepTime) { if (!SH.id) return; const id = SH.id; const W = GE.world; const el = sheetEl(); el.classList.remove('drag', 'wopen'); el.style.transform = APP.isWide() ? '' : 'translateY(100%)'; SH.id = null; W.focus = null; GE.setExtras && GE.setExtras(null); if (SH._savedLayers) { W.layers.forEach((l, i) => (l.on = SH._savedLayers[i])); SH._savedLayers = null; bus.emit('layers', W.layers); } - W.setWindow(null); W.setTime(null); W.auroraK = 0.85; + if (!keepTime) { W.setWindow(null); W.setTime(null); } W.auroraK = 0.85; APP.setMode(''); if (silent) return; const fromNow = !APP.isWide() && GE.now && $(`#v-now .card[data-id="${id}"]`); diff --git a/mobile/vision/src/js/64-ride.js b/mobile/vision/src/js/64-ride.js index 9d3dd0d9b..03ff5e395 100644 --- a/mobile/vision/src/js/64-ride.js +++ b/mobile/vision/src/js/64-ride.js @@ -19,7 +19,7 @@ const CHAT_JET = [ ]; RIDE.start = function (p) { const W = GE.world; - if (APP.mode === 'situation') { GE.sheet.close(true); } + if (APP.mode === 'situation') { GE.sheet.close(true, true); } RIDE.p = p; RIDE.on = true; RIDE.yaw = 0; RIDE.pitch = 0; RIDE.zoom = 1; if (APP.tab !== 'globe') { APP.tab = 'globe'; $('#app').dataset.tab = 'globe'; $$('.pane .view').forEach((v) => v.classList.remove('on')); } APP.setMode('ride'); @@ -75,7 +75,7 @@ RIDE.frame = function (view) { g += ``; g += ``; // heading tape - const ty = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--sat')) + 70; + const ty = (parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--sat')) || 0) + 100; g += ``; for (let d = Math.floor(hdg / 5) * 5 - 40; d <= hdg + 40; d += 5) { const x = cx + (d - hdg) * 4; const dd = ((d % 360) + 360) % 360; g += ``; if (dd % 30 === 0) g += `${{ 0: 'N', 90: 'E', 180: 'S', 270: 'W' }[dd] ?? String(dd / 10).padStart(2, '0')}`; } g += `${String(Math.round(hdg)).padStart(3, '0')}`; @@ -88,6 +88,6 @@ RIDE.exit = function (silent) { $('#ride').classList.remove('on'); $('#ride').innerHTML = ''; const v = Cam.view(400, 800); const [la, lo] = geo.ll(v.eye); Cam.setRig(null); Cam.set({ lat: la, lon: lo, range: Math.max(0.002, v3.len(v.eye) - 1 + 0.002), tilt: 0.9, heading: RIDE.s ? RIDE.s.hdg : 0, shiftY: 0.06 }); - APP.setMode(''); + APP.setMode(''); GE.world.setWindow(null); GE.world.setTime(null); if (!silent) Cam.flyTo({ range: 0.012, tilt: 0.8 }); }; diff --git a/mobile/vision/src/js/65-capture.js b/mobile/vision/src/js/65-capture.js index bd989daf7..8b760e79a 100644 --- a/mobile/vision/src/js/65-capture.js +++ b/mobile/vision/src/js/65-capture.js @@ -110,7 +110,7 @@ CAP.frame = function (view) { if (!el) { el = h('div', { 'data-k': it.k }); el.innerHTML = `
${ic(it.i, 'sm')}${esc(it.t)}
`; host.append(el); } el.querySelector('[data-s]').textContent = it.s; el.querySelector('.reticle').style.transform = `translate(${s.x}px, ${s.y}px)`; - el.querySelector('.callout').style.transform = `translate(${s.x + 24}px, ${s.y}px) translate(0, -50%)`; + const flip = s.x > view.w - 210; el.querySelector('.callout').style.transform = flip ? `translate(${s.x - 24}px, ${s.y}px) translate(-100%, -50%)` : `translate(${s.x + 24}px, ${s.y}px) translate(0, -50%)`; } host.querySelectorAll('[data-k]').forEach((e) => { if (!seen.has(e.dataset.k)) e.remove(); }); // witness sensors @@ -152,7 +152,7 @@ CAP.ceremony = function (b, cv) { ov.innerHTML = `
${ic('check', 'xs')}
Frame hashed · SHA-256 of ${fmt.n(b.bytes.length)} JPEG bytes
…
-
${ic('check', 'xs')}
Session sealed · IMU 150 samples @ 50 Hz · GPS ±4 m · heading ${b.session.heading}°${GE.sparkPath(b.session.imu.map((x) => x.g + x.b * 0.02), 300, 30, '#5CF2A0', 1.2)}
+
${ic('check', 'xs')}
Session sealed · IMU 150 samples @ 50 Hz · GPS ±4 m · heading ${b.session.heading}°${GE.sparkPath(b.session.imu.map((x) => x.g + x.b * 0.02), 300, 30, '#5CF2A0', 1.2)}
${ic('check', 'xs')}
Chained to this device’s previous bundle
prev ${b.prev.slice(0, 24)}…
${ic('check', 'xs')}
Bundle hash
…
${ic('check', 'xs')}
Signed · ECDSA P-256, key generated on device, non-extractable
…
diff --git a/mobile/vision/src/js/66-threads.js b/mobile/vision/src/js/66-threads.js index b234bafc6..34dfae09c 100644 --- a/mobile/vision/src/js/66-threads.js +++ b/mobile/vision/src/js/66-threads.js @@ -151,12 +151,11 @@ THR.init = async function () { /* ---------- list ---------- */ THR.render = function () { const v = $('#v-threads'); - v.innerHTML = `
${ic('seal', 'xs')} Signed evidence · open ledgers

Threads

+ v.innerHTML = `
${ic('seal', 'xs')} Signed evidence · open ledgers

Threads

${ic('sparkles', 'xs')} Concept · illustrative data
${['kora', 'hydro', 'tamang', 'geo', 'sky'].map((k) => `${P[k].h[0].toUpperCase()}`).join('')}37 people reconstructing now
${Object.values(THREADS).map((T) => { const s = GE.SIT.byId[T.sit]; return `

${esc(T.title)}

${esc(T.place)} · ${esc(T.date)}

4D
${T.obs.length}observations
${T.hyps.length}hypotheses
${THR.ledgers[T.id]?.length || '–'}ledger
${T.people.slice(0, 4).map((k) => `${P[k].h[0].toUpperCase()}`).join('')}
`; }).join('')}

Concept · illustrative data. Witness posts, handles and claims are fictional.

`; - $('#thMe').addEventListener('click', () => APP.setTab('you')); $('#newThread').addEventListener('click', () => APP.toast(`${ic('threads', 'sm')} New thread · add your first observation`)); $$('.tcard').forEach((c) => { c.addEventListener('click', () => THR.openWs(c.dataset.t)); diff --git a/mobile/vision/src/js/90-main.js b/mobile/vision/src/js/90-main.js index 7387c310f..fbb91ac71 100644 --- a/mobile/vision/src/js/90-main.js +++ b/mobile/vision/src/js/90-main.js @@ -30,7 +30,7 @@ function globeVisible() { let lastT = performance.now(), lastDyn = 0, lastSec = 0, running = true; function frame(now) { if (!running) return; - const dt = Math.min(0.1, Math.max(0.001, (now - lastT) / 1000)); lastT = now; + const dt = Math.min(0.25, Math.max(0.001, (now - lastT) / 1000)); lastT = now; W.simMs = Date.now() + W.offsetH * 3600000; W.t = (W.simMs - NOW) / 1000; W.sun = astro.sun(W.simMs); Cam.update(dt); if (APP.mode === 'situation' && !Cam.anim && GE.sheet.targetShift != null && !APP.isWide()) Cam.shiftY += (GE.sheet.targetShift - Cam.shiftY) * Math.min(1, dt * 7); @@ -66,7 +66,7 @@ function coldOpen() { APP.intro = true; app.classList.add('intro'); APP.updateChrome(); if (skipAll) { Cam.set(finalCam); end(); return; } Cam.set({ lat: 8, lon: finalCam.lon + 75, range: 5.8, heading: 0, tilt: 0, shiftY: 0.0, shiftX: 0 }); - const T = red ? 1.2 : 3.4; const t0 = performance.now(); + const T = (red ? 1.2 : 3.4) * (HASH.slow ? +HASH.slow : 1); const t0 = performance.now(); GE.introFade = 0; if (!red) Cam.flyTo(finalCam, { duration: T }); else Cam.set(finalCam);