初版成品
This commit is contained in:
+121
-27
@@ -1,7 +1,7 @@
|
|||||||
export const APP_CONFIG = {
|
export const APP_CONFIG = {
|
||||||
// Rendering and canvas constraints.
|
// Rendering and canvas constraints.
|
||||||
scene: {
|
scene: {
|
||||||
background: '#030712',
|
background: '#000000',
|
||||||
maxPixelRatio: 1.75,
|
maxPixelRatio: 1.75,
|
||||||
},
|
},
|
||||||
// Default perspective camera setup.
|
// Default perspective camera setup.
|
||||||
@@ -13,13 +13,13 @@ export const APP_CONFIG = {
|
|||||||
},
|
},
|
||||||
// Shared motion timing and presets for the travel sequence.
|
// Shared motion timing and presets for the travel sequence.
|
||||||
animation: {
|
animation: {
|
||||||
speedMin: 0.1,
|
speedMin: 0.05,
|
||||||
speedMax: 3,
|
speedMax: 3,
|
||||||
defaultSpeed: 1,
|
defaultSpeed: 0.55,
|
||||||
speedPresets: {
|
speedPresets: {
|
||||||
slow: 0.55,
|
slow: 0.2,
|
||||||
steady: 1,
|
steady: 0.55,
|
||||||
deep: 1.65,
|
deep: 1.0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Starfield behavior tuned for center-area spawning and parallax depth.
|
// Starfield behavior tuned for center-area spawning and parallax depth.
|
||||||
@@ -35,20 +35,20 @@ export const APP_CONFIG = {
|
|||||||
seedProgressExponent: 0.72,
|
seedProgressExponent: 0.72,
|
||||||
baseSizeMin: 1.3,
|
baseSizeMin: 1.3,
|
||||||
baseSizeMax: 3.6,
|
baseSizeMax: 3.6,
|
||||||
baseAlphaMin: 0.62,
|
baseAlphaMin: 2.0,
|
||||||
baseAlphaMax: 1,
|
baseAlphaMax: 2.5,
|
||||||
pointScale: 52,
|
pointScale: 52,
|
||||||
pointMinSize: 2.8,
|
pointMinSize: 2.8,
|
||||||
pointMaxSize: 76,
|
pointMaxSize: 76,
|
||||||
accelerationXY: 0.22,
|
accelerationXY: 0.22,
|
||||||
accelerationZ: 0.1,
|
accelerationZ: 0.1,
|
||||||
nearFieldBoost: 1.8,
|
nearFieldBoost: 2.25,
|
||||||
edgeBoost: 7.2,
|
edgeBoost: 8.8,
|
||||||
edgeBurst: 3.4,
|
edgeBurst: 4.4,
|
||||||
trailStartProgress: 0.58,
|
trailStartProgress: 0.38,
|
||||||
trailStrength: 1,
|
trailStrength: 1.8,
|
||||||
trailLength: 0.38,
|
trailLength: 0.85,
|
||||||
trailWidth: 0.16,
|
trailWidth: 0.12,
|
||||||
},
|
},
|
||||||
// Smoke layers create volumetric light fog passing through the camera.
|
// Smoke layers create volumetric light fog passing through the camera.
|
||||||
smoke: {
|
smoke: {
|
||||||
@@ -64,19 +64,113 @@ export const APP_CONFIG = {
|
|||||||
pointerSmoothing: 0.08,
|
pointerSmoothing: 0.08,
|
||||||
maxOffset: 0.4,
|
maxOffset: 0.4,
|
||||||
},
|
},
|
||||||
// Galaxy parameters for background spiral galaxy layer.
|
// Background galaxy rendered as layered shader fog instead of dense point clouds.
|
||||||
galaxy: {
|
galaxy: {
|
||||||
count: 50000,
|
zPosition: -40,
|
||||||
radius: 15,
|
width: 72,
|
||||||
branches: 20,
|
height: 48,
|
||||||
spin: 0.3,
|
tiltX: Math.PI * 0.35,
|
||||||
randomness: 0.15,
|
tiltY: Math.PI * 0.15,
|
||||||
randomnessPower: 6,
|
palette: {
|
||||||
size: 0.008,
|
coreWhite: '#ffffff',
|
||||||
rotationSpeed: 0.001,
|
coreGold: '#ffd36b',
|
||||||
zPosition: -35,
|
warmOrange: '#ff9850',
|
||||||
insideColor: '#ff6030',
|
warmMagenta: '#7b4a86',
|
||||||
outsideColor: '#3861cbff',
|
outerBlue: '#38d9ff',
|
||||||
|
deepBlue: '#103f97',
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
scale: 1.05,
|
||||||
|
opacity: 0.42,
|
||||||
|
blendMode: 'normal',
|
||||||
|
rotationSpeed: 0.0024,
|
||||||
|
armCount: 2,
|
||||||
|
twist: 12,
|
||||||
|
armSharpness: 0.6,
|
||||||
|
noiseScale: 2.7,
|
||||||
|
noiseStrength: 0.54,
|
||||||
|
coreRadius: 0.2,
|
||||||
|
coreGlow: 3.8,
|
||||||
|
glowStrength: 1.06,
|
||||||
|
zOffset: 0,
|
||||||
|
innerColor: '#fff5de',
|
||||||
|
outerColor: '#63d8ff',
|
||||||
|
dustColor: '#1e3e78',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 1.25,
|
||||||
|
opacity: 0.38,
|
||||||
|
blendMode: 'normal',
|
||||||
|
rotationSpeed: 0.0016,
|
||||||
|
armCount: 3,
|
||||||
|
twist: 10.4,
|
||||||
|
armSharpness: 0.52,
|
||||||
|
noiseScale: 2.1,
|
||||||
|
noiseStrength: 0.68,
|
||||||
|
coreRadius: 0.24,
|
||||||
|
coreGlow: 3.5,
|
||||||
|
glowStrength: 0.82,
|
||||||
|
zOffset: -0.4,
|
||||||
|
innerColor: '#ffd8c2',
|
||||||
|
outerColor: '#6c89ff',
|
||||||
|
dustColor: '#45206b',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 1.45,
|
||||||
|
opacity: 0.52,
|
||||||
|
blendMode: 'additive',
|
||||||
|
rotationSpeed: 0.0009,
|
||||||
|
armCount: 4,
|
||||||
|
twist: 9.2,
|
||||||
|
armSharpness: 0.58,
|
||||||
|
noiseScale: 1.55,
|
||||||
|
noiseStrength: 0.76,
|
||||||
|
coreRadius: 0.28,
|
||||||
|
coreGlow: 2.8,
|
||||||
|
glowStrength: 1.08,
|
||||||
|
zOffset: -0.8,
|
||||||
|
innerColor: '#d8f4ff',
|
||||||
|
outerColor: '#25c7ef',
|
||||||
|
dustColor: '#0c2558',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 1.75,
|
||||||
|
opacity: 0.25,
|
||||||
|
blendMode: 'normal',
|
||||||
|
rotationSpeed: 0.00036,
|
||||||
|
armCount: 2,
|
||||||
|
twist: 6.8,
|
||||||
|
armSharpness: 0.28,
|
||||||
|
noiseScale: 1.05,
|
||||||
|
noiseStrength: 0.62,
|
||||||
|
coreRadius: 0.32,
|
||||||
|
coreGlow: 3.8,
|
||||||
|
glowStrength: 0.34,
|
||||||
|
zOffset: -1.3,
|
||||||
|
innerColor: '#b7e9ff',
|
||||||
|
outerColor: '#4ed5ef',
|
||||||
|
dustColor: '#10356b',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 2.1,
|
||||||
|
opacity: 0.22,
|
||||||
|
blendMode: 'normal',
|
||||||
|
rotationSpeed: 0.00018,
|
||||||
|
armCount: 2,
|
||||||
|
twist: 4.2,
|
||||||
|
armSharpness: 0.16,
|
||||||
|
noiseScale: 0.78,
|
||||||
|
noiseStrength: 0.56,
|
||||||
|
coreRadius: 0.42,
|
||||||
|
coreGlow: 3.9,
|
||||||
|
glowStrength: 0.2,
|
||||||
|
zOffset: -1.9,
|
||||||
|
innerColor: '#8fddff',
|
||||||
|
outerColor: '#53bfd8',
|
||||||
|
dustColor: '#143f73',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
// Shared UI timings used by overlays and toolbars.
|
// Shared UI timings used by overlays and toolbars.
|
||||||
ui: {
|
ui: {
|
||||||
|
|||||||
+58
-50
@@ -297,62 +297,70 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<section class="hud-layer" :class="{ 'hud-layer--active': started }">
|
<section class="hud-layer" :class="{ 'hud-layer--active': started }">
|
||||||
<transition name="controls-fade">
|
<transition name="controls-fade">
|
||||||
<div
|
<div v-if="started && controlsVisible" class="hud-bottom-container">
|
||||||
v-if="started && controlsVisible"
|
<div
|
||||||
class="glass-toolbar"
|
class="glass-toolbar"
|
||||||
@pointerdown.stop="markActivity"
|
@pointerdown.stop="markActivity"
|
||||||
@pointermove.stop="markActivity"
|
@pointermove.stop="markActivity"
|
||||||
@touchstart.stop.passive="markActivity"
|
@touchstart.stop.passive="markActivity"
|
||||||
>
|
>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<span class="toolbar-label">速度</span>
|
<span class="toolbar-label">速度</span>
|
||||||
<div class="segment-control" role="group" aria-label="旅行速度">
|
<div class="segment-control" role="group" aria-label="旅行速度">
|
||||||
|
<button
|
||||||
|
v-for="option in speedOptions"
|
||||||
|
:key="option.value"
|
||||||
|
class="segment-button"
|
||||||
|
:class="{ 'segment-button--active': option.value === speedPreset }"
|
||||||
|
type="button"
|
||||||
|
@click="handleSpeedChange(option.value)"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="toolbar-group toolbar-group--stretch">
|
||||||
|
<span class="toolbar-label">背景音乐</span>
|
||||||
|
<select class="glass-select" :value="currentTrack" @change="handleTrackChange">
|
||||||
|
<option v-for="trackName in trackOptions" :key="trackName" :value="trackName">
|
||||||
|
{{ trackName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="toolbar-group toolbar-group--stretch">
|
||||||
|
<span class="toolbar-label">音量</span>
|
||||||
|
<div class="range-wrapper" :style="{ '--volume-percent': `${volume * 100}%` }">
|
||||||
|
<input
|
||||||
|
class="glass-range"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
:value="volume"
|
||||||
|
@input="handleVolumeChange"
|
||||||
|
/>
|
||||||
|
<span class="range-value">{{ volumePercent }}</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="toolbar-group toolbar-group--compact">
|
||||||
|
<span class="toolbar-label">显示</span>
|
||||||
<button
|
<button
|
||||||
v-for="option in speedOptions"
|
class="toolbar-action"
|
||||||
:key="option.value"
|
:class="{ 'toolbar-action--active': isFullscreen }"
|
||||||
class="segment-button"
|
|
||||||
:class="{ 'segment-button--active': option.value === speedPreset }"
|
|
||||||
type="button"
|
type="button"
|
||||||
@click="handleSpeedChange(option.value)"
|
:disabled="!fullscreenAvailable"
|
||||||
|
@click="handleFullscreenToggle"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ fullscreenButtonLabel }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="toolbar-group toolbar-group--stretch">
|
<div class="footer-credit">
|
||||||
<span class="toolbar-label">BGM</span>
|
Created by <a href="https://airnan.cn/" target="_blank" rel="noopener noreferrer">@码农阿楠</a> with AI.
|
||||||
<select class="glass-select" :value="currentTrack" @change="handleTrackChange">
|
|
||||||
<option v-for="trackName in trackOptions" :key="trackName" :value="trackName">
|
|
||||||
{{ trackName }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="toolbar-group toolbar-group--stretch">
|
|
||||||
<span class="toolbar-label">音量 {{ volumePercent }}</span>
|
|
||||||
<input
|
|
||||||
class="glass-range"
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.01"
|
|
||||||
:value="volume"
|
|
||||||
@input="handleVolumeChange"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="toolbar-group toolbar-group--compact">
|
|
||||||
<span class="toolbar-label">显示</span>
|
|
||||||
<button
|
|
||||||
class="toolbar-action"
|
|
||||||
:class="{ 'toolbar-action--active': isFullscreen }"
|
|
||||||
type="button"
|
|
||||||
:disabled="!fullscreenAvailable"
|
|
||||||
@click="handleFullscreenToggle"
|
|
||||||
>
|
|
||||||
{{ fullscreenButtonLabel }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|||||||
+147
-18
@@ -13,7 +13,7 @@
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
--bg-primary: #030712;
|
--bg-primary: #000000;
|
||||||
--bg-secondary: rgba(7, 15, 30, 0.72);
|
--bg-secondary: rgba(7, 15, 30, 0.72);
|
||||||
--border-soft: rgba(148, 163, 184, 0.24);
|
--border-soft: rgba(148, 163, 184, 0.24);
|
||||||
--text-primary: #e2e8f0;
|
--text-primary: #e2e8f0;
|
||||||
@@ -37,9 +37,7 @@ body,
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background:
|
background: #000000;
|
||||||
radial-gradient(circle at top, rgba(14, 165, 233, 0.18), transparent 32%),
|
|
||||||
linear-gradient(180deg, #020617 0%, #030712 45%, #02030a 100%);
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +61,7 @@ canvas {
|
|||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: radial-gradient(circle at top, rgba(56, 189, 248, 0.08), transparent 34%);
|
background: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-layer {
|
.scene-layer {
|
||||||
@@ -80,8 +78,7 @@ canvas {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at center, transparent 0%, rgba(2, 6, 23, 0.08) 48%, rgba(2, 6, 23, 0.54) 100%),
|
radial-gradient(circle at center, transparent 30%, rgba(0, 0, 0, 0.4) 70%, rgba(0, 0, 0, 0.9) 100%);
|
||||||
linear-gradient(180deg, rgba(2, 6, 23, 0.16) 0%, rgba(2, 6, 23, 0.48) 100%);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
@@ -182,14 +179,23 @@ canvas {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-toolbar {
|
.hud-bottom-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: 28px;
|
bottom: 28px;
|
||||||
|
width: min(860px, calc(100% - 24px));
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
width: min(860px, calc(100% - 24px));
|
width: 100%;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
border-radius: 28px;
|
border-radius: 28px;
|
||||||
@@ -198,7 +204,23 @@ canvas {
|
|||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
box-shadow: 0 18px 56px rgba(2, 6, 23, 0.48);
|
box-shadow: 0 18px 56px rgba(2, 6, 23, 0.48);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
transform: translateX(-50%);
|
}
|
||||||
|
|
||||||
|
.footer-credit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(226, 232, 240, 0.5);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-credit a {
|
||||||
|
color: rgba(226, 232, 240, 0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: text-decoration 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-credit a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-group {
|
.toolbar-group {
|
||||||
@@ -226,10 +248,20 @@ canvas {
|
|||||||
|
|
||||||
.segment-control {
|
.segment-control {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 46px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(2, 6, 23, 0.34);
|
background: rgba(2, 6, 23, 0.34);
|
||||||
|
transition:
|
||||||
|
border-color 160ms ease,
|
||||||
|
background 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment-control:hover {
|
||||||
|
border-color: rgba(125, 211, 252, 0.32);
|
||||||
|
background: rgba(15, 23, 42, 0.56);
|
||||||
}
|
}
|
||||||
|
|
||||||
.segment-button {
|
.segment-button {
|
||||||
@@ -238,7 +270,11 @@ canvas {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: rgba(226, 232, 240, 0.78);
|
color: rgba(226, 232, 240, 0.78);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 10px 16px;
|
height: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 16px;
|
||||||
transition:
|
transition:
|
||||||
background 160ms ease,
|
background 160ms ease,
|
||||||
color 160ms ease;
|
color 160ms ease;
|
||||||
@@ -250,13 +286,17 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-action {
|
.toolbar-action {
|
||||||
|
height: 46px;
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
border-radius: 16px;
|
border-radius: 999px;
|
||||||
background: rgba(2, 6, 23, 0.36);
|
background: rgba(2, 6, 23, 0.36);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
transition:
|
transition:
|
||||||
border-color 160ms ease,
|
border-color 160ms ease,
|
||||||
background 160ms ease,
|
background 160ms ease,
|
||||||
@@ -288,17 +328,102 @@ canvas {
|
|||||||
|
|
||||||
.glass-select {
|
.glass-select {
|
||||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
border-radius: 16px;
|
border-radius: 999px;
|
||||||
background: rgba(2, 6, 23, 0.36);
|
background-color: rgba(2, 6, 23, 0.36);
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(226, 232, 240, 0.82)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 14px center;
|
||||||
|
background-size: 16px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
height: 46px;
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 0 14px;
|
padding: 0 36px 0 16px;
|
||||||
|
transition:
|
||||||
|
border-color 160ms ease,
|
||||||
|
background-color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-select:hover {
|
||||||
|
border-color: rgba(125, 211, 252, 0.32);
|
||||||
|
background-color: rgba(15, 23, 42, 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 46px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(to right, rgba(56, 189, 248, 0.16) var(--volume-percent, 0%), rgba(2, 6, 23, 0.36) var(--volume-percent, 0%));
|
||||||
|
background-clip: padding-box;
|
||||||
|
transition: border-color 160ms ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-wrapper:hover {
|
||||||
|
border-color: rgba(125, 211, 252, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-value {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(226, 232, 240, 0.9);
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-range {
|
.glass-range {
|
||||||
accent-color: var(--accent);
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-range::-webkit-slider-runnable-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-range::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-range::-moz-range-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-range::-moz-range-thumb {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-fade-enter-active,
|
.overlay-fade-enter-active,
|
||||||
@@ -340,12 +465,16 @@ canvas {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-toolbar {
|
.hud-bottom-container {
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
|
width: min(520px, calc(100% - 16px));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-toolbar {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
width: min(520px, calc(100% - 16px));
|
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
+306
-76
@@ -61,7 +61,13 @@ const SMOKE_RESET_Z = APP_CONFIG.smoke.resetZ
|
|||||||
const SMOKE_BOUNDS_X = APP_CONFIG.smoke.boundsX
|
const SMOKE_BOUNDS_X = APP_CONFIG.smoke.boundsX
|
||||||
const SMOKE_BOUNDS_Y = APP_CONFIG.smoke.boundsY
|
const SMOKE_BOUNDS_Y = APP_CONFIG.smoke.boundsY
|
||||||
|
|
||||||
const STAR_COLORS = ['#ffffff', '#f3f8ff', '#e8f4ff', '#d7eeff', '#fff7ea'] as const
|
const STAR_COLORS = [
|
||||||
|
'#00e5ff', // 亮青
|
||||||
|
'#ff00ea', // 亮粉
|
||||||
|
'#ffcc00', // 亮金
|
||||||
|
'#00ff66', // 亮绿
|
||||||
|
'#ff3333', // 亮红
|
||||||
|
] as const
|
||||||
const SMOKE_COLORS = ['#8dd8ff', '#adc4ff', '#d6b5ff', '#ffe7ff'] as const
|
const SMOKE_COLORS = ['#8dd8ff', '#adc4ff', '#d6b5ff', '#ffe7ff'] as const
|
||||||
|
|
||||||
const STAR_VERTEX_SHADER = `
|
const STAR_VERTEX_SHADER = `
|
||||||
@@ -78,11 +84,13 @@ varying vec2 vDir;
|
|||||||
void main() {
|
void main() {
|
||||||
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||||||
float scaledProgress = max(aProgress, 0.0);
|
float scaledProgress = max(aProgress, 0.0);
|
||||||
float pointSize = aSizeBase * (0.95 + scaledProgress * 2.85) * ${APP_CONFIG.starfield.pointScale.toFixed(1)} / max(1.0, -mvPosition.z);
|
// 放大精灵尺寸以容纳更长的拖尾
|
||||||
|
float pointSize = aSizeBase * (0.95 + scaledProgress * 2.85) * ${APP_CONFIG.starfield.pointScale.toFixed(1)} / max(1.0, -mvPosition.z) * 2.5;
|
||||||
gl_Position = projectionMatrix * mvPosition;
|
gl_Position = projectionMatrix * mvPosition;
|
||||||
gl_PointSize = clamp(pointSize, ${APP_CONFIG.starfield.pointMinSize.toFixed(1)}, ${APP_CONFIG.starfield.pointMaxSize.toFixed(1)});
|
gl_PointSize = clamp(pointSize, ${APP_CONFIG.starfield.pointMinSize.toFixed(1)} * 2.5, ${APP_CONFIG.starfield.pointMaxSize.toFixed(1)} * 2.5);
|
||||||
vColor = color;
|
vColor = color;
|
||||||
vAlpha = aAlphaBase * (0.32 + scaledProgress * 0.68);
|
// 取消原本透明度的距离衰减,直接用最高的 baseAlpha 甚至更高,保持全局最亮
|
||||||
|
vAlpha = aAlphaBase * 1.5;
|
||||||
vTail = aTail;
|
vTail = aTail;
|
||||||
vDir = aDir;
|
vDir = aDir;
|
||||||
}
|
}
|
||||||
@@ -93,27 +101,246 @@ varying vec3 vColor;
|
|||||||
varying float vAlpha;
|
varying float vAlpha;
|
||||||
varying float vTail;
|
varying float vTail;
|
||||||
varying vec2 vDir;
|
varying vec2 vDir;
|
||||||
|
uniform float uSpeed;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec2 centered = gl_PointCoord - vec2(0.5);
|
// 放大坐标系,这样中心点画得相对较小,留出极大空间画长尾巴
|
||||||
|
vec2 centered = (gl_PointCoord - vec2(0.5)) * 2.5;
|
||||||
float distanceToCenter = length(centered);
|
float distanceToCenter = length(centered);
|
||||||
float core = 1.0 - smoothstep(0.0, 0.22, distanceToCenter);
|
float hotspot = 1.0 - smoothstep(0.0, 0.06, distanceToCenter);
|
||||||
float halo = 1.0 - smoothstep(0.12, 0.5, distanceToCenter);
|
hotspot = pow(hotspot, 0.32);
|
||||||
vec2 dir = length(vDir) > 0.0001 ? normalize(vDir) : vec2(0.0, 1.0);
|
float core = 1.0 - smoothstep(0.0, 0.13, distanceToCenter);
|
||||||
|
core = pow(core, 0.44);
|
||||||
|
float halo = 1.0 - smoothstep(0.015, 0.22, distanceToCenter);
|
||||||
|
halo = pow(halo, 1.95);
|
||||||
|
|
||||||
|
// 翻转 Y 轴以匹配 WebGL 纹理坐标系
|
||||||
|
vec2 dir = length(vDir) > 0.0001 ? normalize(vec2(vDir.x, -vDir.y)) : vec2(0.0, -1.0);
|
||||||
vec2 side = vec2(-dir.y, dir.x);
|
vec2 side = vec2(-dir.y, dir.x);
|
||||||
float along = dot(centered, dir);
|
float along = dot(centered, dir);
|
||||||
float across = dot(centered, side);
|
float across = dot(centered, side);
|
||||||
float tailLong = 1.0 - smoothstep(-${(0.18 + APP_CONFIG.starfield.trailLength).toFixed(2)}, 0.05, along);
|
|
||||||
float tailThin = 1.0 - smoothstep(${(APP_CONFIG.starfield.trailWidth * 0.35).toFixed(2)}, ${(APP_CONFIG.starfield.trailWidth).toFixed(2)}, abs(across));
|
float back = -along;
|
||||||
float tailFade = smoothstep(0.01, 0.52, -along);
|
// 拖尾长度根据当前速度动态变化
|
||||||
float tail = tailLong * tailThin * tailFade * vTail;
|
float speedFactor = max(uSpeed, 0.1);
|
||||||
float alpha = (core * 0.95 + halo * 0.45 + tail * 0.85) * vAlpha;
|
float tailLength = clamp(${(APP_CONFIG.starfield.trailLength).toFixed(2)} * 2.5 * speedFactor, 0.05, 2.5);
|
||||||
|
float tailFade = 1.0 - smoothstep(0.0, tailLength, back);
|
||||||
|
float tailMask = smoothstep(-0.02, 0.05, back);
|
||||||
|
|
||||||
|
float maxWidth = ${(APP_CONFIG.starfield.trailWidth).toFixed(2)};
|
||||||
|
float width = maxWidth * mix(0.1, 1.0, pow(tailFade, 0.8));
|
||||||
|
|
||||||
|
float tailProfile = 1.0 - smoothstep(0.0, width, abs(across));
|
||||||
|
float tailHotspot = 1.0 - smoothstep(0.0, width * 0.3, abs(across));
|
||||||
|
// 速度越快,尾巴亮度也适当增强
|
||||||
|
float tail = (tailProfile * 0.8 + tailHotspot * 1.5) * tailFade * tailMask * vTail * (0.4 + 0.6 * speedFactor);
|
||||||
|
|
||||||
|
// 极高的能量累加,不丢弃任何 alpha,强制发光
|
||||||
|
float alpha = (hotspot * 3.5 + core * 2.5 + halo * 0.5 + tail * 2.8) * vAlpha;
|
||||||
|
|
||||||
if (alpha < 0.01) {
|
if (alpha < 0.01) {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
|
|
||||||
gl_FragColor = vec4(vColor, alpha);
|
vec3 color = vColor;
|
||||||
|
// 让高光点保持自身颜色的同时极度明亮,而不仅是变白
|
||||||
|
vec3 hotColor = mix(vColor, vec3(1.0), 0.5);
|
||||||
|
color = mix(color, hotColor, hotspot * 0.8 + core * 0.4);
|
||||||
|
color += hotColor * hotspot * 2.0;
|
||||||
|
color += vColor * core * 1.5;
|
||||||
|
color += vColor * tail * 1.8;
|
||||||
|
|
||||||
|
// 用极其暴力的颜色叠加
|
||||||
|
gl_FragColor = vec4(min(color, vec3(1.0)), min(alpha, 1.0));
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const GALAXY_VERTEX_SHADER = `
|
||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const GALAXY_FRAGMENT_SHADER = `
|
||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uOpacity;
|
||||||
|
uniform float uArms;
|
||||||
|
uniform float uTwist;
|
||||||
|
uniform float uArmSharpness;
|
||||||
|
uniform float uNoiseScale;
|
||||||
|
uniform float uNoiseStrength;
|
||||||
|
uniform float uCoreRadius;
|
||||||
|
uniform float uCoreGlow;
|
||||||
|
uniform float uGlowStrength;
|
||||||
|
uniform float uSeed;
|
||||||
|
uniform vec3 uCoreWhite;
|
||||||
|
uniform vec3 uCoreGold;
|
||||||
|
uniform vec3 uWarmOrange;
|
||||||
|
uniform vec3 uWarmMagenta;
|
||||||
|
uniform vec3 uOuterBlue;
|
||||||
|
uniform vec3 uDeepBlue;
|
||||||
|
uniform vec3 uInnerColor;
|
||||||
|
uniform vec3 uOuterColor;
|
||||||
|
uniform vec3 uDustColor;
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||||
|
}
|
||||||
|
|
||||||
|
float noise(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
|
||||||
|
return mix(
|
||||||
|
mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), u.x),
|
||||||
|
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x),
|
||||||
|
u.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
float fbm(vec2 p) {
|
||||||
|
float value = 0.0;
|
||||||
|
float amplitude = 0.5;
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i += 1) {
|
||||||
|
value += amplitude * noise(p);
|
||||||
|
p = p * 2.03 + vec2(17.3, 9.2);
|
||||||
|
amplitude *= 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 sampleGalaxyRamp(float t) {
|
||||||
|
float value = clamp(t, 0.0, 1.0);
|
||||||
|
vec3 color = uCoreWhite;
|
||||||
|
|
||||||
|
color = mix(color, uCoreGold, smoothstep(0.03, 0.18, value));
|
||||||
|
color = mix(color, uWarmOrange, smoothstep(0.12, 0.32, value));
|
||||||
|
color = mix(color, uWarmMagenta, smoothstep(0.24, 0.58, value));
|
||||||
|
color = mix(color, uOuterBlue, smoothstep(0.5, 0.84, value));
|
||||||
|
color = mix(color, uDeepBlue, smoothstep(0.76, 1.0, value));
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 p = vUv * 2.0 - 1.0;
|
||||||
|
p.x *= 1.08;
|
||||||
|
|
||||||
|
float radius = length(p);
|
||||||
|
float angle = atan(p.y, p.x);
|
||||||
|
float time = uTime * 0.045;
|
||||||
|
vec2 flowUv = p * uNoiseScale;
|
||||||
|
|
||||||
|
float lowNoise = fbm(flowUv + vec2(time * 0.35 + uSeed * 5.1, -time * 0.18));
|
||||||
|
float highNoise = fbm(flowUv * 2.1 - vec2(time * 0.42, time * 0.26 + uSeed * 3.7));
|
||||||
|
float veilNoise = fbm(p * (uNoiseScale * 0.55) + vec2(-time * 0.12, time * 0.08 + uSeed * 1.9));
|
||||||
|
float filamentNoise = fbm(flowUv * 3.4 + vec2(time * 0.18 - uSeed * 2.7, time * 0.24));
|
||||||
|
float clusterNoise = fbm(flowUv * 5.1 + vec2(-time * 0.32 + uSeed * 8.4, time * 0.21));
|
||||||
|
float grainNoise = noise(flowUv * 13.5 + vec2(time * 1.8 + uSeed * 14.0, -time * 1.3));
|
||||||
|
float fineNoise = fbm(flowUv * 8.2 - vec2(time * 0.65 - uSeed * 6.2, time * 0.52));
|
||||||
|
float warpedAngle = angle - radius * uTwist + (lowNoise - 0.5) * uNoiseStrength * 3.2;
|
||||||
|
vec2 swirlDir = vec2(cos(warpedAngle), sin(warpedAngle));
|
||||||
|
vec2 swirlUv = vec2(dot(p, swirlDir), dot(p, vec2(-swirlDir.y, swirlDir.x)));
|
||||||
|
float streakNoise = fbm(swirlUv * vec2(24.0, 3.2) + vec2(time * 0.12 + uSeed * 4.6, -time * 0.06));
|
||||||
|
float mottledNoise = fbm(swirlUv * vec2(10.0, 6.5) - vec2(time * 0.08 - uSeed * 5.2, time * 0.04));
|
||||||
|
float fineFiberNoise = fbm(swirlUv * vec2(46.0, 1.8) + vec2(time * 0.06 + uSeed * 11.4, -time * 0.03));
|
||||||
|
float occlusionNoise = fbm(swirlUv * vec2(5.4, 4.1) - vec2(time * 0.04 - uSeed * 2.6, time * 0.02));
|
||||||
|
float spiralWave = cos(warpedAngle * uArms);
|
||||||
|
float armBase = spiralWave * 0.5 + 0.5;
|
||||||
|
float armMask = smoothstep(1.0 - uArmSharpness, 1.0, armBase);
|
||||||
|
float softArms = smoothstep(0.2, 0.92, armBase + (lowNoise - 0.5) * 0.18);
|
||||||
|
float halo = 1.0 - smoothstep(0.12, 1.45, radius);
|
||||||
|
float outerHalo = 1.0 - smoothstep(0.4, 1.6, radius);
|
||||||
|
float core = 1.0 - smoothstep(0.0, uCoreRadius, radius);
|
||||||
|
float coreBloom = 1.0 - smoothstep(0.0, uCoreRadius * 1.6, radius);
|
||||||
|
float hotCore = 1.0 - smoothstep(0.0, uCoreRadius * 0.95, radius);
|
||||||
|
float dust = smoothstep(0.24, 0.9, highNoise);
|
||||||
|
float wisps = smoothstep(0.44, 0.9, lowNoise) * (1.0 - smoothstep(0.38, 1.08, radius));
|
||||||
|
float outerVeil = smoothstep(0.34, 0.88, veilNoise) * outerHalo;
|
||||||
|
float rim = smoothstep(0.3, 0.98, radius) * (1.0 - smoothstep(0.92, 1.3, radius));
|
||||||
|
float armRidge = pow(armMask, 1.45) * halo;
|
||||||
|
float lanePattern = 1.0 - smoothstep(0.2, 0.72, abs(sin(warpedAngle * (uArms * 0.55) + filamentNoise * 4.4)));
|
||||||
|
float dustLane = lanePattern * smoothstep(0.12, 0.88, radius) * halo * (0.35 + (1.0 - armMask) * 0.9);
|
||||||
|
float clumps = smoothstep(0.62, 0.9, filamentNoise) * softArms * rim;
|
||||||
|
float pulse = 0.5 + 0.5 * sin(uTime * 1.6 + uSeed * 9.0);
|
||||||
|
float shockRadius = uCoreRadius * (1.08 + pulse * 0.42);
|
||||||
|
float shockRing = smoothstep(shockRadius - 0.17, shockRadius - 0.04, radius) *
|
||||||
|
(1.0 - smoothstep(shockRadius + 0.03, shockRadius + 0.11, radius)) *
|
||||||
|
mix(0.55, 1.0, armMask);
|
||||||
|
float clusterMask = smoothstep(0.72, 0.9, clusterNoise) * pow(softArms, 1.45) * rim;
|
||||||
|
float clusterTwinkle = 0.55 + 0.45 * sin(uTime * 2.6 + angle * uArms * 3.2 + radius * 16.0 + uSeed * 12.0);
|
||||||
|
float armSpark = clusterMask * clusterTwinkle;
|
||||||
|
float edgeBreak = smoothstep(0.72, 0.95, veilNoise) * smoothstep(0.55, 1.08, radius) * outerHalo;
|
||||||
|
float grainMask = smoothstep(0.36, 0.84, grainNoise) * mix(0.32, 1.0, softArms) * halo;
|
||||||
|
float dustTexture = smoothstep(0.28, 0.78, fineNoise) * halo * (0.45 + (1.0 - armMask) * 0.8);
|
||||||
|
float fiberBands = smoothstep(0.48, 0.88, streakNoise) * halo * smoothstep(0.12, 0.92, radius);
|
||||||
|
float mottledDust = smoothstep(0.34, 0.82, mottledNoise) * halo;
|
||||||
|
float fineFibers = smoothstep(0.54, 0.9, fineFiberNoise) * halo * smoothstep(0.1, 0.96, radius);
|
||||||
|
float deepOcclusion = smoothstep(0.38, 0.8, occlusionNoise) * halo * smoothstep(0.18, 1.0, radius);
|
||||||
|
float dustFilaments = fiberBands * (0.4 + (1.0 - armMask) * 0.9) * (0.6 + dustLane * 0.8);
|
||||||
|
float outerSparseFibers = smoothstep(0.44, 0.88, streakNoise) *
|
||||||
|
smoothstep(0.48, 1.08, radius) *
|
||||||
|
outerHalo *
|
||||||
|
(0.45 + veilNoise * 0.8);
|
||||||
|
float microSpeckles = smoothstep(0.955, 0.992, grainNoise) * (0.18 + clusterTwinkle * 0.32) * softArms * rim;
|
||||||
|
float density = halo * mix(0.44, 1.0, armMask) * mix(0.88, 1.12, dust);
|
||||||
|
density = mix(density, halo * 0.96, 0.34);
|
||||||
|
float coreGlow = pow(core, uCoreGlow) * uGlowStrength;
|
||||||
|
float bloomGlow = pow(coreBloom, 1.8) * (uGlowStrength * 0.92);
|
||||||
|
float pulseCore = pow(coreBloom, 1.28) * (0.24 + pulse * 0.34);
|
||||||
|
float innerCloud = pow(core, 1.15) * 0.58 + pow(coreBloom, 1.3) * 0.38;
|
||||||
|
float starDust = smoothstep(0.9, 0.985, highNoise) * softArms * rim * 0.07;
|
||||||
|
|
||||||
|
density += wisps * 0.4 + outerVeil * 0.42 + innerCloud + coreGlow + bloomGlow + starDust;
|
||||||
|
density += armRidge * 0.2 + clumps * 0.24 + edgeBreak * 0.18 + shockRing * 0.28 + armSpark * 0.1 + pulseCore;
|
||||||
|
density += grainMask * 0.04 + microSpeckles * 0.018;
|
||||||
|
density *= 1.0 - dustLane * 0.34;
|
||||||
|
density *= 0.94 + grainNoise * 0.11;
|
||||||
|
density *= 1.0 - dustTexture * 0.1;
|
||||||
|
density *= 1.0 - dustFilaments * 0.26;
|
||||||
|
density *= 1.0 - fineFibers * 0.18;
|
||||||
|
density *= 1.0 - outerSparseFibers * 0.13;
|
||||||
|
density *= 1.0 - deepOcclusion * (0.12 + dustLane * 0.1);
|
||||||
|
density *= 0.9 + (1.0 - mottledDust) * 0.12;
|
||||||
|
density *= exp(-radius * 0.85);
|
||||||
|
|
||||||
|
float rampT = clamp(pow(radius, 0.72) + dustLane * 0.05 - coreBloom * 0.14 - armRidge * 0.03 + (1.0 - softArms) * 0.04, 0.0, 1.0);
|
||||||
|
vec3 rampColor = sampleGalaxyRamp(rampT);
|
||||||
|
vec3 layerTint = mix(uOuterColor, uInnerColor, clamp(core * 0.75 + armRidge * 0.24 + softArms * 0.12, 0.0, 1.0));
|
||||||
|
vec3 color = mix(rampColor, layerTint, 0.25); // 增加 layerTint 的比重,提亮
|
||||||
|
|
||||||
|
// 提亮灰尘的颜色,防止在纯黑背景下太暗
|
||||||
|
vec3 brightDust = mix(uDustColor, vec3(0.3, 0.5, 0.8), 0.3);
|
||||||
|
color = mix(color, brightDust, wisps * 0.12 + dustLane * 0.5 + (1.0 - softArms) * 0.1);
|
||||||
|
color = mix(color, brightDust, dustTexture * 0.15 + dustFilaments * 0.25 + fineFibers * 0.15 + mottledDust * 0.12 + deepOcclusion * 0.1);
|
||||||
|
color = mix(color, uWarmOrange, armRidge * 0.16 + shockRing * 0.1);
|
||||||
|
color = mix(color, uWarmMagenta, smoothstep(0.18, 0.62, radius) * (1.0 - armMask) * 0.24);
|
||||||
|
color = mix(color, uOuterBlue, outerVeil * 0.18 + edgeBreak * 0.12);
|
||||||
|
color = mix(color, mix(uOuterBlue, uDustColor, 0.4), outerSparseFibers * 0.36);
|
||||||
|
color += mix(rampColor, vec3(1.0, 0.98, 0.95), 0.4) * grainMask * 0.018;
|
||||||
|
color += mix(uCoreGold, vec3(1.0, 0.96, 0.86), 0.45) * armRidge * 0.18;
|
||||||
|
color += mix(uOuterBlue, vec3(0.9, 0.97, 1.0), 0.35) * clumps * 0.12;
|
||||||
|
color += mix(uCoreGold, uWarmOrange, 0.45) * shockRing * 0.34;
|
||||||
|
color += mix(uCoreWhite, uOuterBlue, 0.28) * armSpark * 0.12;
|
||||||
|
color += mix(uCoreWhite, uCoreGold, 0.4) * microSpeckles * 0.04;
|
||||||
|
color += uCoreWhite * pow(coreBloom, 1.8) * 0.65;
|
||||||
|
color += mix(uCoreWhite, uCoreGold, 0.22) * pow(hotCore, 1.2) * 1.0;
|
||||||
|
|
||||||
|
float alpha = density * uOpacity * (1.0 - smoothstep(1.25, 1.6, radius));
|
||||||
|
|
||||||
|
if (alpha < 0.01) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_FragColor = vec4(color, clamp(alpha, 0.0, 1.0));
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -134,6 +361,10 @@ function randomRange(min: number, max: number) {
|
|||||||
return min + Math.random() * (max - min)
|
return min + Math.random() * (max - min)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getGalaxyBlending(blendMode: 'normal' | 'additive') {
|
||||||
|
return blendMode === 'additive' ? THREE.AdditiveBlending : THREE.NormalBlending
|
||||||
|
}
|
||||||
|
|
||||||
function sampleDisk(radius: number) {
|
function sampleDisk(radius: number) {
|
||||||
const angle = Math.random() * Math.PI * 2
|
const angle = Math.random() * Math.PI * 2
|
||||||
const distance = Math.pow(Math.random(), APP_CONFIG.starfield.radialExponent) * radius
|
const distance = Math.pow(Math.random(), APP_CONFIG.starfield.radialExponent) * radius
|
||||||
@@ -236,14 +467,13 @@ export function createTravelScene(container: HTMLElement): TravelSceneController
|
|||||||
|
|
||||||
const galaxyGroup = new THREE.Group()
|
const galaxyGroup = new THREE.Group()
|
||||||
galaxyGroup.position.z = APP_CONFIG.galaxy.zPosition
|
galaxyGroup.position.z = APP_CONFIG.galaxy.zPosition
|
||||||
// Tilt the galaxy to give it a perspective view
|
galaxyGroup.rotation.x = APP_CONFIG.galaxy.tiltX
|
||||||
galaxyGroup.rotation.x = Math.PI * 0.35
|
galaxyGroup.rotation.y = APP_CONFIG.galaxy.tiltY
|
||||||
galaxyGroup.rotation.y = Math.PI * 0.15
|
|
||||||
scene.add(galaxyGroup)
|
scene.add(galaxyGroup)
|
||||||
|
|
||||||
// Create an inner group for the galaxy rotation to preserve the tilt
|
const galaxyGeometry = new THREE.PlaneGeometry(APP_CONFIG.galaxy.width, APP_CONFIG.galaxy.height, 1, 1)
|
||||||
const galaxyInnerGroup = new THREE.Group()
|
const galaxyLayers: THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial>[] = []
|
||||||
galaxyGroup.add(galaxyInnerGroup)
|
const galaxyMaterials: THREE.ShaderMaterial[] = []
|
||||||
|
|
||||||
const starsGeometry = new THREE.BufferGeometry()
|
const starsGeometry = new THREE.BufferGeometry()
|
||||||
const starPositions = new Float32Array(STAR_COUNT * 3)
|
const starPositions = new Float32Array(STAR_COUNT * 3)
|
||||||
@@ -264,7 +494,7 @@ export function createTravelScene(container: HTMLElement): TravelSceneController
|
|||||||
const pointerCurrent = { x: 0, y: 0 }
|
const pointerCurrent = { x: 0, y: 0 }
|
||||||
|
|
||||||
let started = false
|
let started = false
|
||||||
let targetSpeed = 1
|
let targetSpeed: number = APP_CONFIG.animation.defaultSpeed
|
||||||
let currentSpeed = 0
|
let currentSpeed = 0
|
||||||
let animationFrameId = 0
|
let animationFrameId = 0
|
||||||
let disposed = false
|
let disposed = false
|
||||||
@@ -276,6 +506,9 @@ export function createTravelScene(container: HTMLElement): TravelSceneController
|
|||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
vertexColors: true,
|
vertexColors: true,
|
||||||
blending: THREE.AdditiveBlending,
|
blending: THREE.AdditiveBlending,
|
||||||
|
uniforms: {
|
||||||
|
uSpeed: { value: 1.0 },
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function spawnStar(index: number, seededProgress?: number) {
|
function spawnStar(index: number, seededProgress?: number) {
|
||||||
@@ -314,7 +547,11 @@ export function createTravelScene(container: HTMLElement): TravelSceneController
|
|||||||
starDirections[index * 2] = direction.x
|
starDirections[index * 2] = direction.x
|
||||||
starDirections[index * 2 + 1] = direction.y
|
starDirections[index * 2 + 1] = direction.y
|
||||||
|
|
||||||
starColor.set(STAR_COLORS[Math.floor(Math.random() * STAR_COLORS.length)])
|
if (Math.random() < 0.9) {
|
||||||
|
starColor.set('#ffffff')
|
||||||
|
} else {
|
||||||
|
starColor.set(STAR_COLORS[Math.floor(Math.random() * STAR_COLORS.length)])
|
||||||
|
}
|
||||||
starColors[index * 3] = starColor.r
|
starColors[index * 3] = starColor.r
|
||||||
starColors[index * 3 + 1] = starColor.g
|
starColors[index * 3 + 1] = starColor.g
|
||||||
starColors[index * 3 + 2] = starColor.b
|
starColors[index * 3 + 2] = starColor.b
|
||||||
@@ -393,61 +630,47 @@ export function createTravelScene(container: HTMLElement): TravelSceneController
|
|||||||
travelRoot.add(state.mesh)
|
travelRoot.add(state.mesh)
|
||||||
}
|
}
|
||||||
|
|
||||||
const galaxyGeometry = new THREE.BufferGeometry()
|
APP_CONFIG.galaxy.layers.forEach((layerConfig, index) => {
|
||||||
const galaxyPositions = new Float32Array(APP_CONFIG.galaxy.count * 3)
|
const galaxyMaterial = new THREE.ShaderMaterial({
|
||||||
const galaxyColors = new Float32Array(APP_CONFIG.galaxy.count * 3)
|
vertexShader: GALAXY_VERTEX_SHADER,
|
||||||
const colorInside = new THREE.Color(APP_CONFIG.galaxy.insideColor)
|
fragmentShader: GALAXY_FRAGMENT_SHADER,
|
||||||
const colorOutside = new THREE.Color(APP_CONFIG.galaxy.outsideColor)
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
blending: getGalaxyBlending(layerConfig.blendMode),
|
||||||
|
uniforms: {
|
||||||
|
uTime: { value: 0 },
|
||||||
|
uOpacity: { value: layerConfig.opacity },
|
||||||
|
uArms: { value: layerConfig.armCount },
|
||||||
|
uTwist: { value: layerConfig.twist },
|
||||||
|
uArmSharpness: { value: layerConfig.armSharpness },
|
||||||
|
uNoiseScale: { value: layerConfig.noiseScale },
|
||||||
|
uNoiseStrength: { value: layerConfig.noiseStrength },
|
||||||
|
uCoreRadius: { value: layerConfig.coreRadius },
|
||||||
|
uCoreGlow: { value: layerConfig.coreGlow },
|
||||||
|
uGlowStrength: { value: layerConfig.glowStrength },
|
||||||
|
uSeed: { value: index * 0.173 + 0.31 },
|
||||||
|
uCoreWhite: { value: new THREE.Color(APP_CONFIG.galaxy.palette.coreWhite) },
|
||||||
|
uCoreGold: { value: new THREE.Color(APP_CONFIG.galaxy.palette.coreGold) },
|
||||||
|
uWarmOrange: { value: new THREE.Color(APP_CONFIG.galaxy.palette.warmOrange) },
|
||||||
|
uWarmMagenta: { value: new THREE.Color(APP_CONFIG.galaxy.palette.warmMagenta) },
|
||||||
|
uOuterBlue: { value: new THREE.Color(APP_CONFIG.galaxy.palette.outerBlue) },
|
||||||
|
uDeepBlue: { value: new THREE.Color(APP_CONFIG.galaxy.palette.deepBlue) },
|
||||||
|
uInnerColor: { value: new THREE.Color(layerConfig.innerColor) },
|
||||||
|
uOuterColor: { value: new THREE.Color(layerConfig.outerColor) },
|
||||||
|
uDustColor: { value: new THREE.Color(layerConfig.dustColor) },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
for (let i = 0; i < APP_CONFIG.galaxy.count; i += 1) {
|
const galaxyMesh = new THREE.Mesh(galaxyGeometry, galaxyMaterial)
|
||||||
const i3 = i * 3
|
galaxyMesh.scale.setScalar(layerConfig.scale)
|
||||||
const radius = Math.random() * APP_CONFIG.galaxy.radius
|
galaxyMesh.position.z = layerConfig.zOffset
|
||||||
const branchesAngle =
|
galaxyMesh.rotation.z = index * 0.8
|
||||||
((i % APP_CONFIG.galaxy.branches) / APP_CONFIG.galaxy.branches) * Math.PI * 2
|
galaxyGroup.add(galaxyMesh)
|
||||||
const spinAngle = radius * APP_CONFIG.galaxy.spin
|
galaxyLayers.push(galaxyMesh)
|
||||||
|
galaxyMaterials.push(galaxyMaterial)
|
||||||
const mixedColor = colorInside.clone()
|
|
||||||
mixedColor.lerp(colorOutside, radius / APP_CONFIG.galaxy.radius)
|
|
||||||
|
|
||||||
const randomX =
|
|
||||||
Math.random() ** APP_CONFIG.galaxy.randomnessPower *
|
|
||||||
(Math.random() < 0.5 ? 1 : -1) *
|
|
||||||
APP_CONFIG.galaxy.randomness *
|
|
||||||
radius
|
|
||||||
const randomY =
|
|
||||||
Math.random() ** APP_CONFIG.galaxy.randomnessPower *
|
|
||||||
(Math.random() < 0.5 ? 1 : -1) *
|
|
||||||
APP_CONFIG.galaxy.randomness *
|
|
||||||
radius
|
|
||||||
const randomZ =
|
|
||||||
Math.random() ** APP_CONFIG.galaxy.randomnessPower *
|
|
||||||
(Math.random() < 0.5 ? 1 : -1) *
|
|
||||||
APP_CONFIG.galaxy.randomness *
|
|
||||||
radius
|
|
||||||
|
|
||||||
galaxyPositions[i3] = Math.cos(branchesAngle + spinAngle) * radius + randomX
|
|
||||||
galaxyPositions[i3 + 1] = Math.sin(branchesAngle + spinAngle) * radius + randomY
|
|
||||||
galaxyPositions[i3 + 2] = randomZ
|
|
||||||
|
|
||||||
galaxyColors[i3] = mixedColor.r
|
|
||||||
galaxyColors[i3 + 1] = mixedColor.g
|
|
||||||
galaxyColors[i3 + 2] = mixedColor.b
|
|
||||||
}
|
|
||||||
|
|
||||||
galaxyGeometry.setAttribute('position', new THREE.BufferAttribute(galaxyPositions, 3))
|
|
||||||
galaxyGeometry.setAttribute('color', new THREE.BufferAttribute(galaxyColors, 3))
|
|
||||||
|
|
||||||
const galaxyMaterial = new THREE.PointsMaterial({
|
|
||||||
size: APP_CONFIG.galaxy.size,
|
|
||||||
sizeAttenuation: true,
|
|
||||||
depthWrite: false,
|
|
||||||
blending: THREE.AdditiveBlending,
|
|
||||||
vertexColors: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const galaxyPoints = new THREE.Points(galaxyGeometry, galaxyMaterial)
|
|
||||||
galaxyInnerGroup.add(galaxyPoints)
|
|
||||||
|
|
||||||
const clock = new THREE.Clock()
|
const clock = new THREE.Clock()
|
||||||
|
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
@@ -588,8 +811,15 @@ export function createTravelScene(container: HTMLElement): TravelSceneController
|
|||||||
const motionDelta = deltaSeconds * currentSpeed
|
const motionDelta = deltaSeconds * currentSpeed
|
||||||
updateStars(motionDelta)
|
updateStars(motionDelta)
|
||||||
updateSmoke(motionDelta * 0.62, elapsed)
|
updateSmoke(motionDelta * 0.62, elapsed)
|
||||||
|
|
||||||
|
starMaterial.uniforms.uSpeed.value = currentSpeed
|
||||||
|
|
||||||
galaxyInnerGroup.rotation.z += APP_CONFIG.galaxy.rotationSpeed * currentSpeed
|
galaxyLayers.forEach((galaxyLayer, index) => {
|
||||||
|
galaxyLayer.rotation.z -= APP_CONFIG.galaxy.layers[index].rotationSpeed * currentSpeed
|
||||||
|
})
|
||||||
|
galaxyMaterials.forEach((material, index) => {
|
||||||
|
material.uniforms.uTime.value = elapsed + index * 0.37
|
||||||
|
})
|
||||||
|
|
||||||
travelRoot.rotation.z = pointerCurrent.x * 0.03
|
travelRoot.rotation.z = pointerCurrent.x * 0.03
|
||||||
travelRoot.position.x = -pointerCurrent.x * 0.8
|
travelRoot.position.x = -pointerCurrent.x * 0.8
|
||||||
@@ -631,8 +861,8 @@ export function createTravelScene(container: HTMLElement): TravelSceneController
|
|||||||
travelRoot.remove(smokeState.mesh)
|
travelRoot.remove(smokeState.mesh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
galaxyMaterials.forEach((material) => material.dispose())
|
||||||
galaxyGeometry.dispose()
|
galaxyGeometry.dispose()
|
||||||
galaxyMaterial.dispose()
|
|
||||||
|
|
||||||
starsGeometry.dispose()
|
starsGeometry.dispose()
|
||||||
starMaterial.dispose()
|
starMaterial.dispose()
|
||||||
|
|||||||
Reference in New Issue
Block a user