Spaces:
Running
Running
潘昌伟 commited on
Commit ·
56a3de9
1
Parent(s): fa94a11
Use pulsed city map loading sound
Browse files- index.html +3 -3
- 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-
|
| 9 |
-
<link rel="stylesheet" href="src/ticket-clip.css?v=20260712-predeploy-
|
| 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-
|
| 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-
|
| 10 |
import {
|
| 11 |
applySnapToGuides,
|
| 12 |
-
} from "./layoutWorkbenchCore.js?v=20260712-predeploy-
|
| 13 |
import {
|
| 14 |
buildCityTileUrl,
|
| 15 |
normalizeCityTileStyles,
|
| 16 |
resolveCityTileProvider,
|
| 17 |
runtimeCityTileConfig,
|
| 18 |
-
} from "./tileProviders.js?v=20260712-predeploy-
|
| 19 |
import {
|
| 20 |
SPECIAL_BOOK_OFFSETS,
|
| 21 |
SPECIAL_CATEGORY_STACK_OFFSETS,
|
| 22 |
SPECIAL_DEFINITIONS,
|
| 23 |
-
} from "./specialConfig.js?v=20260712-predeploy-
|
| 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-
|
| 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-
|
| 43 |
import {
|
| 44 |
bookshelfLocationLabel,
|
| 45 |
combinedLocationHeading,
|
| 46 |
locationHeading,
|
| 47 |
-
} from "./locationLabels.js?v=20260712-predeploy-
|
| 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-
|
| 55 |
import {
|
| 56 |
initAnalytics,
|
| 57 |
trackEvent,
|
| 58 |
-
} from "./analytics.js?v=20260712-predeploy-
|
| 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-
|
| 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.
|
| 1026 |
masterGain.connect(context.destination);
|
| 1027 |
|
| 1028 |
-
const
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
stopTimer: null,
|
| 1064 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1065 |
}
|
| 1066 |
|
| 1067 |
function stopCityMapLoadingSound(options = {}) {
|
| 1068 |
const loading = state.audio.cityMapLoading;
|
| 1069 |
if (!loading) return;
|
| 1070 |
-
|
|
|
|
|
|
|
| 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.
|
| 1081 |
-
|
| 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(
|
| 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:
|
|
|
|
|
|
|
| 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;
|