潘昌伟 commited on
Commit
56a3de9
·
1 Parent(s): fa94a11

Use pulsed city map loading sound

Browse files
Files changed (2) hide show
  1. index.html +3 -3
  2. src/app.js +53 -73
index.html CHANGED
@@ -5,8 +5,8 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>World Citybook | 全球「城市路书」地图</title>
7
  <link rel="icon" href="data:," />
8
- <link rel="stylesheet" href="src/styles.css?v=20260712-predeploy-190" />
9
- <link rel="stylesheet" href="src/ticket-clip.css?v=20260712-predeploy-190" />
10
  </head>
11
  <body>
12
  <main class="atlas-shell">
@@ -271,6 +271,6 @@
271
  retinaTiles: true,
272
  };
273
  </script>
274
- <script type="module" src="src/app.js?v=20260712-predeploy-190"></script>
275
  </body>
276
  </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>World Citybook | 全球「城市路书」地图</title>
7
  <link rel="icon" href="data:," />
8
+ <link rel="stylesheet" href="src/styles.css?v=20260712-predeploy-191" />
9
+ <link rel="stylesheet" href="src/ticket-clip.css?v=20260712-predeploy-191" />
10
  </head>
11
  <body>
12
  <main class="atlas-shell">
 
271
  retinaTiles: true,
272
  };
273
  </script>
274
+ <script type="module" src="src/app.js?v=20260712-predeploy-191"></script>
275
  </body>
276
  </html>
src/app.js CHANGED
@@ -6,21 +6,21 @@ import {
6
  CITY_MAP_BOOK_TOP_FORBIDDEN_BOTTOM,
7
  cityMapBookTopForbiddenBottom,
8
  hasCompleteCityMapSpecialBookLayout,
9
- } from "./cityMapBookLayout.js?v=20260712-predeploy-190";
10
  import {
11
  applySnapToGuides,
12
- } from "./layoutWorkbenchCore.js?v=20260712-predeploy-190";
13
  import {
14
  buildCityTileUrl,
15
  normalizeCityTileStyles,
16
  resolveCityTileProvider,
17
  runtimeCityTileConfig,
18
- } from "./tileProviders.js?v=20260712-predeploy-190";
19
  import {
20
  SPECIAL_BOOK_OFFSETS,
21
  SPECIAL_CATEGORY_STACK_OFFSETS,
22
  SPECIAL_DEFINITIONS,
23
- } from "./specialConfig.js?v=20260712-predeploy-190";
24
  import {
25
  SHARE_HALFTONE_DOT_RULE,
26
  shareHalftoneCropRect,
@@ -31,7 +31,7 @@ import {
31
  shareHalftoneNoisyTone,
32
  shareHalftonePlateColors,
33
  shareHalftoneSamplePoint,
34
- } from "./shareHalftone.js?v=20260712-predeploy-190";
35
  import {
36
  SAVED_BOOKS_STORAGE_KEY,
37
  buildFixedShelfRows,
@@ -39,23 +39,23 @@ import {
39
  paginateShelfItems,
40
  removeSavedBook,
41
  upsertSavedBook,
42
- } from "./savedBookshelf.js?v=20260712-predeploy-190";
43
  import {
44
  bookshelfLocationLabel,
45
  combinedLocationHeading,
46
  locationHeading,
47
- } from "./locationLabels.js?v=20260712-predeploy-190";
48
  import {
49
  SEARCH_PINYIN_ALIASES,
50
  SEARCH_TOPIC_GROUPS,
51
  SEARCH_TOPIC_ALIASES,
52
  deriveSearchTopicAliasMatchesFromText,
53
  deriveSearchTopicsFromText,
54
- } from "./searchTopics.js?v=20260712-predeploy-190";
55
  import {
56
  initAnalytics,
57
  trackEvent,
58
- } from "./analytics.js?v=20260712-predeploy-190";
59
 
60
  const VIEWBOX = { width: 1440, height: 920 };
61
  const BASE_RADIUS = 430;
@@ -502,7 +502,7 @@ const mapData = {
502
  let specialDataLoadPromise = null;
503
  const specialDataLoadPromises = new Map();
504
 
505
- const DATA_VERSION = "20260712-predeploy-190";
506
  const SHARE_HALFTONE_REVEAL_WAIT_MS = 1100;
507
  const SHARE_HALFTONE_CACHE_LIMIT = 10;
508
  const SHARE_HALFTONE_PREWARM_TIMEOUT_MS = 350;
@@ -1006,7 +1006,7 @@ function playCoinCollectSound() {
1006
  function playCityMapLoadingSound() {
1007
  withAudioContext((context) => {
1008
  const now = context.currentTime;
1009
- stopCityMapLoadingSound({ fadeDuration: 0.08 });
1010
  playCityMapLoadingIntroSound(context, now);
1011
  startCityMapLoadingLoop(context, now + 0.18);
1012
  });
@@ -1022,52 +1022,50 @@ function playCityMapLoadingIntroSound(context, now = context.currentTime) {
1022
  function startCityMapLoadingLoop(context, startAt = context.currentTime) {
1023
  const masterGain = context.createGain();
1024
  masterGain.gain.setValueAtTime(0.0001, startAt);
1025
- masterGain.gain.exponentialRampToValueAtTime(1, startAt + 0.42);
1026
  masterGain.connect(context.destination);
1027
 
1028
- const lowOscillator = context.createOscillator();
1029
- const lowGain = context.createGain();
1030
- lowOscillator.type = "sine";
1031
- lowOscillator.frequency.setValueAtTime(164.81, startAt);
1032
- lowGain.gain.setValueAtTime(0.0048, startAt);
1033
- lowOscillator.connect(lowGain).connect(masterGain);
1034
-
1035
- const shimmerOscillator = context.createOscillator();
1036
- const shimmerGain = context.createGain();
1037
- shimmerOscillator.type = "triangle";
1038
- shimmerOscillator.frequency.setValueAtTime(329.63, startAt);
1039
- shimmerGain.gain.setValueAtTime(0.0028, startAt);
1040
- shimmerOscillator.connect(shimmerGain).connect(masterGain);
1041
-
1042
- const pulseOscillator = context.createOscillator();
1043
- const pulseDepth = context.createGain();
1044
- pulseOscillator.type = "sine";
1045
- pulseOscillator.frequency.setValueAtTime(0.72, startAt);
1046
- pulseDepth.gain.setValueAtTime(0.0015, startAt);
1047
- pulseOscillator.connect(pulseDepth).connect(shimmerGain.gain);
1048
-
1049
- const noiseSource = createLoopingNoiseSource(context, 1.2);
1050
- const noiseFilter = context.createBiquadFilter();
1051
- const noiseGain = context.createGain();
1052
- noiseFilter.type = "bandpass";
1053
- noiseFilter.frequency.setValueAtTime(680, startAt);
1054
- noiseFilter.Q.setValueAtTime(0.56, startAt);
1055
- noiseGain.gain.setValueAtTime(0.0024, startAt);
1056
- noiseSource.connect(noiseFilter).connect(noiseGain).connect(masterGain);
1057
-
1058
- [lowOscillator, shimmerOscillator, pulseOscillator, noiseSource].forEach((source) => source.start(startAt));
1059
- state.audio.cityMapLoading = {
1060
  context,
1061
  masterGain,
1062
- sources: [lowOscillator, shimmerOscillator, pulseOscillator, noiseSource],
 
 
 
 
1063
  stopTimer: null,
1064
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1065
  }
1066
 
1067
  function stopCityMapLoadingSound(options = {}) {
1068
  const loading = state.audio.cityMapLoading;
1069
  if (!loading) return;
1070
- state.audio.cityMapLoading = null;
 
 
1071
  if (loading.stopTimer) window.clearTimeout(loading.stopTimer);
1072
 
1073
  const fadeDuration = Math.max(0.04, options.fadeDuration ?? 0.9);
@@ -1076,35 +1074,15 @@ function stopCityMapLoadingSound(options = {}) {
1076
  loading.masterGain.gain.cancelScheduledValues(now);
1077
  loading.masterGain.gain.setValueAtTime(Math.max(loading.masterGain.gain.value, 0.0001), now);
1078
  loading.masterGain.gain.exponentialRampToValueAtTime(0.0001, now + fadeDuration);
 
1079
  loading.stopTimer = window.setTimeout(() => {
1080
- loading.sources.forEach((source) => {
1081
- try {
1082
- source.stop();
1083
- } catch (error) {
1084
- // Source may already have been stopped by the browser after a rapid view switch.
1085
- }
1086
- });
1087
  loading.masterGain.disconnect();
 
1088
  }, Math.ceil((fadeDuration + 0.04) * 1000));
1089
  }
1090
 
1091
- function createLoopingNoiseSource(context, duration) {
1092
- const sampleRate = context.sampleRate;
1093
- const frameCount = Math.max(1, Math.floor(sampleRate * duration));
1094
- const buffer = context.createBuffer(1, frameCount, sampleRate);
1095
- const samples = buffer.getChannelData(0);
1096
- let previous = 0;
1097
- for (let index = 0; index < frameCount; index += 1) {
1098
- const white = Math.random() * 2 - 1;
1099
- previous = previous * 0.86 + white * 0.14;
1100
- samples[index] = previous;
1101
- }
1102
- const source = context.createBufferSource();
1103
- source.buffer = buffer;
1104
- source.loop = true;
1105
- return source;
1106
- }
1107
-
1108
  function playRadioControlSound() {
1109
  withAudioContext((context) => {
1110
  const now = context.currentTime;
@@ -1113,7 +1091,7 @@ function playRadioControlSound() {
1113
  });
1114
  }
1115
 
1116
- function playToneLayer(context, startAt, type, startFrequency, endFrequency, duration, peakGain) {
1117
  const oscillator = context.createOscillator();
1118
  const gain = context.createGain();
1119
  oscillator.type = type;
@@ -1122,7 +1100,7 @@ function playToneLayer(context, startAt, type, startFrequency, endFrequency, dur
1122
  gain.gain.setValueAtTime(0.0001, startAt);
1123
  gain.gain.exponentialRampToValueAtTime(peakGain, startAt + 0.012);
1124
  gain.gain.exponentialRampToValueAtTime(0.0001, startAt + duration);
1125
- oscillator.connect(gain).connect(context.destination);
1126
  oscillator.start(startAt);
1127
  oscillator.stop(startAt + duration + 0.02);
1128
  }
@@ -8110,7 +8088,9 @@ async function snapshotShareStyles() {
8110
  background: transparent !important;
8111
  }
8112
  [data-share-snapshot] .share-ticket-shell {
8113
- filter: none !important;
 
 
8114
  }
8115
  [data-share-snapshot] .share-preview__canvas .share-ticket {
8116
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.58) !important;
 
6
  CITY_MAP_BOOK_TOP_FORBIDDEN_BOTTOM,
7
  cityMapBookTopForbiddenBottom,
8
  hasCompleteCityMapSpecialBookLayout,
9
+ } from "./cityMapBookLayout.js?v=20260712-predeploy-191";
10
  import {
11
  applySnapToGuides,
12
+ } from "./layoutWorkbenchCore.js?v=20260712-predeploy-191";
13
  import {
14
  buildCityTileUrl,
15
  normalizeCityTileStyles,
16
  resolveCityTileProvider,
17
  runtimeCityTileConfig,
18
+ } from "./tileProviders.js?v=20260712-predeploy-191";
19
  import {
20
  SPECIAL_BOOK_OFFSETS,
21
  SPECIAL_CATEGORY_STACK_OFFSETS,
22
  SPECIAL_DEFINITIONS,
23
+ } from "./specialConfig.js?v=20260712-predeploy-191";
24
  import {
25
  SHARE_HALFTONE_DOT_RULE,
26
  shareHalftoneCropRect,
 
31
  shareHalftoneNoisyTone,
32
  shareHalftonePlateColors,
33
  shareHalftoneSamplePoint,
34
+ } from "./shareHalftone.js?v=20260712-predeploy-191";
35
  import {
36
  SAVED_BOOKS_STORAGE_KEY,
37
  buildFixedShelfRows,
 
39
  paginateShelfItems,
40
  removeSavedBook,
41
  upsertSavedBook,
42
+ } from "./savedBookshelf.js?v=20260712-predeploy-191";
43
  import {
44
  bookshelfLocationLabel,
45
  combinedLocationHeading,
46
  locationHeading,
47
+ } from "./locationLabels.js?v=20260712-predeploy-191";
48
  import {
49
  SEARCH_PINYIN_ALIASES,
50
  SEARCH_TOPIC_GROUPS,
51
  SEARCH_TOPIC_ALIASES,
52
  deriveSearchTopicAliasMatchesFromText,
53
  deriveSearchTopicsFromText,
54
+ } from "./searchTopics.js?v=20260712-predeploy-191";
55
  import {
56
  initAnalytics,
57
  trackEvent,
58
+ } from "./analytics.js?v=20260712-predeploy-191";
59
 
60
  const VIEWBOX = { width: 1440, height: 920 };
61
  const BASE_RADIUS = 430;
 
502
  let specialDataLoadPromise = null;
503
  const specialDataLoadPromises = new Map();
504
 
505
+ const DATA_VERSION = "20260712-predeploy-191";
506
  const SHARE_HALFTONE_REVEAL_WAIT_MS = 1100;
507
  const SHARE_HALFTONE_CACHE_LIMIT = 10;
508
  const SHARE_HALFTONE_PREWARM_TIMEOUT_MS = 350;
 
1006
  function playCityMapLoadingSound() {
1007
  withAudioContext((context) => {
1008
  const now = context.currentTime;
1009
+ stopCityMapLoadingSound({ fadeDuration: 0.08, finalPulses: 0 });
1010
  playCityMapLoadingIntroSound(context, now);
1011
  startCityMapLoadingLoop(context, now + 0.18);
1012
  });
 
1022
  function startCityMapLoadingLoop(context, startAt = context.currentTime) {
1023
  const masterGain = context.createGain();
1024
  masterGain.gain.setValueAtTime(0.0001, startAt);
1025
+ masterGain.gain.exponentialRampToValueAtTime(1, startAt + 0.18);
1026
  masterGain.connect(context.destination);
1027
 
1028
+ const loading = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1029
  context,
1030
  masterGain,
1031
+ pulseIndex: 0,
1032
+ remainingFadePulses: 0,
1033
+ stopping: false,
1034
+ stopped: false,
1035
+ pulseTimer: null,
1036
  stopTimer: null,
1037
  };
1038
+ state.audio.cityMapLoading = loading;
1039
+ scheduleCityMapLoadingPulse(loading, Math.max(0, (startAt - context.currentTime) * 1000));
1040
+ }
1041
+
1042
+ function scheduleCityMapLoadingPulse(loading, delayMs = 0) {
1043
+ if (!loading || loading.stopped) return;
1044
+ loading.pulseTimer = window.setTimeout(() => {
1045
+ if (loading.stopped) return;
1046
+ if (loading.stopping && loading.remainingFadePulses <= 0) return;
1047
+ playCityMapLoadingPulse(loading.context, loading.masterGain, loading.context.currentTime, loading.pulseIndex);
1048
+ loading.pulseIndex += 1;
1049
+ if (loading.stopping) loading.remainingFadePulses -= 1;
1050
+ if (!loading.stopping || loading.remainingFadePulses > 0) {
1051
+ scheduleCityMapLoadingPulse(loading, loading.stopping ? 285 : 365);
1052
+ }
1053
+ }, delayMs);
1054
+ }
1055
+
1056
+ function playCityMapLoadingPulse(context, destination, startAt, index = 0) {
1057
+ const notes = [329.63, 392, 369.99, 440];
1058
+ const base = notes[index % notes.length];
1059
+ playToneLayer(context, startAt, "sine", base, base * 1.12, 0.105, 0.0075, destination);
1060
+ playToneLayer(context, startAt + 0.018, "triangle", base * 1.98, base * 2.22, 0.085, 0.0038, destination);
1061
  }
1062
 
1063
  function stopCityMapLoadingSound(options = {}) {
1064
  const loading = state.audio.cityMapLoading;
1065
  if (!loading) return;
1066
+ loading.stopping = true;
1067
+ loading.remainingFadePulses = Math.max(0, options.finalPulses ?? 2);
1068
+ if (loading.pulseTimer) window.clearTimeout(loading.pulseTimer);
1069
  if (loading.stopTimer) window.clearTimeout(loading.stopTimer);
1070
 
1071
  const fadeDuration = Math.max(0.04, options.fadeDuration ?? 0.9);
 
1074
  loading.masterGain.gain.cancelScheduledValues(now);
1075
  loading.masterGain.gain.setValueAtTime(Math.max(loading.masterGain.gain.value, 0.0001), now);
1076
  loading.masterGain.gain.exponentialRampToValueAtTime(0.0001, now + fadeDuration);
1077
+ if (loading.remainingFadePulses > 0) scheduleCityMapLoadingPulse(loading, 0);
1078
  loading.stopTimer = window.setTimeout(() => {
1079
+ loading.stopped = true;
1080
+ if (loading.pulseTimer) window.clearTimeout(loading.pulseTimer);
 
 
 
 
 
1081
  loading.masterGain.disconnect();
1082
+ if (state.audio.cityMapLoading === loading) state.audio.cityMapLoading = null;
1083
  }, Math.ceil((fadeDuration + 0.04) * 1000));
1084
  }
1085
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  function playRadioControlSound() {
1087
  withAudioContext((context) => {
1088
  const now = context.currentTime;
 
1091
  });
1092
  }
1093
 
1094
+ function playToneLayer(context, startAt, type, startFrequency, endFrequency, duration, peakGain, destination = context.destination) {
1095
  const oscillator = context.createOscillator();
1096
  const gain = context.createGain();
1097
  oscillator.type = type;
 
1100
  gain.gain.setValueAtTime(0.0001, startAt);
1101
  gain.gain.exponentialRampToValueAtTime(peakGain, startAt + 0.012);
1102
  gain.gain.exponentialRampToValueAtTime(0.0001, startAt + duration);
1103
+ oscillator.connect(gain).connect(destination);
1104
  oscillator.start(startAt);
1105
  oscillator.stop(startAt + duration + 0.02);
1106
  }
 
8088
  background: transparent !important;
8089
  }
8090
  [data-share-snapshot] .share-ticket-shell {
8091
+ filter:
8092
+ drop-shadow(0 0 18px rgba(45, 48, 43, 0.15))
8093
+ drop-shadow(0 12px 32px rgba(45, 48, 43, 0.1)) !important;
8094
  }
8095
  [data-share-snapshot] .share-preview__canvas .share-ticket {
8096
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.58) !important;