add burst
This commit is contained in:
32
src/App.jsx
32
src/App.jsx
@@ -21,7 +21,10 @@ export default function App() {
|
|||||||
const [camSpeed, setCamSpeed] = useState(0.05);
|
const [camSpeed, setCamSpeed] = useState(0.05);
|
||||||
const [vrmModel, setVrmModel] = useState('ai.vrm');
|
const [vrmModel, setVrmModel] = useState('ai.vrm');
|
||||||
const [lang, setLang] = useState('en');
|
const [lang, setLang] = useState('en');
|
||||||
|
const [volume, setVolume] = useState(0);
|
||||||
|
const [burstSky, setBurstSky] = useState(null);
|
||||||
const langRef = useRef('en');
|
const langRef = useRef('en');
|
||||||
|
const volumeRef = useRef(0);
|
||||||
const voiceIndexRef = useRef(0);
|
const voiceIndexRef = useRef(0);
|
||||||
const voicePattern = ['normal','normal','normal','normal','normal','normal','normal','normal','skill','skill'];
|
const voicePattern = ['normal','normal','normal','normal','normal','normal','normal','normal','skill','skill'];
|
||||||
const actionIndexRef = useRef(0);
|
const actionIndexRef = useRef(0);
|
||||||
@@ -33,6 +36,7 @@ export default function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLangChange = useCallback((v) => { setLang(v); langRef.current = v; }, []);
|
const handleLangChange = useCallback((v) => { setLang(v); langRef.current = v; }, []);
|
||||||
|
const handleVolumeChange = useCallback((v) => { setVolume(v); volumeRef.current = v; }, []);
|
||||||
|
|
||||||
const playSkillVoice = useCallback(() => {
|
const playSkillVoice = useCallback(() => {
|
||||||
const type = voicePattern[voiceIndexRef.current % voicePattern.length];
|
const type = voicePattern[voiceIndexRef.current % voicePattern.length];
|
||||||
@@ -40,7 +44,7 @@ export default function App() {
|
|||||||
const suffix = langRef.current === 'en' ? '_en' : '';
|
const suffix = langRef.current === 'en' ? '_en' : '';
|
||||||
const file = `${import.meta.env.BASE_URL}voice/ai/${type}_1${suffix}.mp3`;
|
const file = `${import.meta.env.BASE_URL}voice/ai/${type}_1${suffix}.mp3`;
|
||||||
const audio = new Audio(file);
|
const audio = new Audio(file);
|
||||||
audio.volume = 0.7;
|
audio.volume = volumeRef.current;
|
||||||
audio.play().catch(() => {});
|
audio.play().catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -76,6 +80,9 @@ export default function App() {
|
|||||||
} else if (e.code === 'KeyS') {
|
} else if (e.code === 'KeyS') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
doSkillRef.current?.();
|
doSkillRef.current?.();
|
||||||
|
} else if (e.code === 'KeyB') {
|
||||||
|
e.preventDefault();
|
||||||
|
doBurstRef.current?.();
|
||||||
}
|
}
|
||||||
}, [playAnim, view]);
|
}, [playAnim, view]);
|
||||||
|
|
||||||
@@ -114,6 +121,24 @@ export default function App() {
|
|||||||
teleportIndexRef.current = idx + 1;
|
teleportIndexRef.current = idx + 1;
|
||||||
}, [playAnim]);
|
}, [playAnim]);
|
||||||
|
|
||||||
|
const burstCoolRef = useRef(0);
|
||||||
|
const doBurstRef = useRef(null);
|
||||||
|
const doBurst = useCallback(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - burstCoolRef.current < 10000) return;
|
||||||
|
burstCoolRef.current = now;
|
||||||
|
playAnim('burst');
|
||||||
|
// Sky: sunset → night during burst
|
||||||
|
setBurstSky(new Date('2024-06-21T17:30:00'));
|
||||||
|
setTimeout(() => setBurstSky(null), 4500);
|
||||||
|
const suffix = langRef.current === 'en' ? '_en' : '';
|
||||||
|
const file = `${import.meta.env.BASE_URL}voice/ai/burst_1${suffix}.mp3`;
|
||||||
|
const audio = new Audio(file);
|
||||||
|
audio.volume = volumeRef.current;
|
||||||
|
audio.play().catch(() => {});
|
||||||
|
}, [playAnim]);
|
||||||
|
doBurstRef.current = doBurst;
|
||||||
|
|
||||||
const skillCoolRef = useRef(0);
|
const skillCoolRef = useRef(0);
|
||||||
const doSkill = useCallback(() => {
|
const doSkill = useCallback(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -187,7 +212,7 @@ export default function App() {
|
|||||||
{/* Atmosphere background */}
|
{/* Atmosphere background */}
|
||||||
<div style={{ ...layerStyle, zIndex: 0, opacity: isNasa ? 0 : 1, transition: 'opacity 1.5s ease' }}>
|
<div style={{ ...layerStyle, zIndex: 0, opacity: isNasa ? 0 : 1, transition: 'opacity 1.5s ease' }}>
|
||||||
<Canvas gl={{ alpha: true, antialias: true }}>
|
<Canvas gl={{ alpha: true, antialias: true }}>
|
||||||
<AtmosphereScene timeScale={timeScale} />
|
<AtmosphereScene timeScale={timeScale} overrideDate={burstSky} overrideTimeScale={burstSky ? 8000 : undefined} />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -206,8 +231,11 @@ export default function App() {
|
|||||||
camSpeed={camSpeed}
|
camSpeed={camSpeed}
|
||||||
onCamSpeedChange={setCamSpeed}
|
onCamSpeedChange={setCamSpeed}
|
||||||
onSkill={doSkill}
|
onSkill={doSkill}
|
||||||
|
onBurst={doBurst}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
onLangChange={handleLangChange}
|
onLangChange={handleLangChange}
|
||||||
|
volume={volume}
|
||||||
|
onVolumeChange={handleVolumeChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -69,13 +69,28 @@ function GoogleMaps3DTiles() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AtmosphereScene({ timeScale: timeScaleProp }) {
|
export default function AtmosphereScene({ timeScale: timeScaleProp, overrideDate, overrideTimeScale }) {
|
||||||
const { gl } = useThree();
|
const { gl } = useThree();
|
||||||
const sunRef = useRef();
|
const sunRef = useRef();
|
||||||
const atmosphereRef = useRef();
|
const atmosphereRef = useRef();
|
||||||
const dateRef = useRef(new Date(INITIAL_DATE));
|
const dateRef = useRef(new Date(INITIAL_DATE));
|
||||||
|
const savedDateRef = useRef(null);
|
||||||
const timeScaleRef = useRef(timeScaleProp ?? TIME_SCALE);
|
const timeScaleRef = useRef(timeScaleProp ?? TIME_SCALE);
|
||||||
timeScaleRef.current = timeScaleProp ?? TIME_SCALE;
|
|
||||||
|
// Override date for burst effect
|
||||||
|
const hasBurstRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (overrideDate) {
|
||||||
|
hasBurstRef.current = true;
|
||||||
|
dateRef.current = new Date(overrideDate);
|
||||||
|
} else if (hasBurstRef.current) {
|
||||||
|
hasBurstRef.current = false;
|
||||||
|
// Burst ended: set to sunrise
|
||||||
|
dateRef.current = new Date('2024-06-22T10:00:00');
|
||||||
|
}
|
||||||
|
}, [overrideDate]);
|
||||||
|
|
||||||
|
timeScaleRef.current = overrideTimeScale ?? timeScaleProp ?? TIME_SCALE;
|
||||||
const [weather, setWeather] = useState(WEATHER_PRESETS[1]);
|
const [weather, setWeather] = useState(WEATHER_PRESETS[1]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { worldState } from './worldState';
|
|||||||
|
|
||||||
const STAGE_ROTATE_SPEED = 0.1; // キャラ周回速度
|
const STAGE_ROTATE_SPEED = 0.1; // キャラ周回速度
|
||||||
|
|
||||||
const BREATH_NEAR = 2.5; // カメラ最近距離
|
const BREATH_NEAR = 1.8; // カメラ最近距離
|
||||||
const BREATH_FAR = 5.0; // カメラ最遠距離
|
const BREATH_FAR = 4.0; // カメラ最遠距離
|
||||||
const BREATH_SPEED = 0.35; // 寄り引き速度
|
const BREATH_SPEED = 0.35; // 寄り引き速度
|
||||||
const BREATH_HEIGHT_RATIO = 0.4; // 距離に対する上下移動の割合
|
const BREATH_HEIGHT_RATIO = 0.4; // 距離に対する上下移動の割合
|
||||||
const BREATH_BASE_Y = 1.5; // カメラ基準Y位置
|
const BREATH_BASE_Y = 1.5; // カメラ基準Y位置
|
||||||
@@ -128,6 +128,10 @@ export default function AvatarScene({ selectedAnimation: animState, onZoomOut, c
|
|||||||
setEffect({ type: 'earth' });
|
setEffect({ type: 'earth' });
|
||||||
const timer = setTimeout(() => setEffect(null), SKILL_DURATION);
|
const timer = setTimeout(() => setEffect(null), SKILL_DURATION);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
|
} else if (animName === 'burst') {
|
||||||
|
setEffect({ type: 'burst' });
|
||||||
|
const timer = setTimeout(() => setEffect(null), 4500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [animState]);
|
}, [animState]);
|
||||||
|
|
||||||
|
|||||||
@@ -101,8 +101,6 @@ const SPHERE_PRESET = {
|
|||||||
{ radius: 0.2, segments: 48, size: 0.01, opacity: 0.4 },
|
{ radius: 0.2, segments: 48, size: 0.01, opacity: 0.4 },
|
||||||
{ radius: 0.26, segments: 32, size: 0.006, opacity: 0.15 },
|
{ radius: 0.26, segments: 32, size: 0.006, opacity: 0.15 },
|
||||||
],
|
],
|
||||||
lightColor: '#aaccff',
|
|
||||||
lightIntensity: 0.3,
|
|
||||||
rotateSpeed: 0.8,
|
rotateSpeed: 0.8,
|
||||||
pulseSpeed: 1.0,
|
pulseSpeed: 1.0,
|
||||||
pulseRange: 0.12,
|
pulseRange: 0.12,
|
||||||
@@ -171,12 +169,228 @@ function SphereEffect({ position = [0, 1.0, 0], scale = 1 }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- BlackHole Effect (burst) - fullscreen raymarching ---
|
||||||
|
|
||||||
|
const bhVertexShader = `
|
||||||
|
attribute vec3 position;
|
||||||
|
varying vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = position.xy;
|
||||||
|
gl_Position = vec4(position.xy, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const bhFragmentShader = `
|
||||||
|
precision highp float;
|
||||||
|
varying vec2 vUv;
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uPhase; // 0-1: blackhole, 1-2: explode, 2-3: dissipate
|
||||||
|
|
||||||
|
#define MAX_STEPS 48
|
||||||
|
#define STEP_SIZE 0.1
|
||||||
|
#define PI 3.14159265
|
||||||
|
|
||||||
|
float hash(float n){ return fract(sin(n)*43758.5453123); }
|
||||||
|
float hash2(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }
|
||||||
|
|
||||||
|
float noise(vec3 p){
|
||||||
|
vec3 i=floor(p); vec3 f=fract(p); f=f*f*(3.0-2.0*f);
|
||||||
|
float n=i.x+i.y*57.0+113.0*i.z;
|
||||||
|
return mix(mix(mix(hash(n),hash(n+1.0),f.x),mix(hash(n+57.0),hash(n+58.0),f.x),f.y),
|
||||||
|
mix(mix(hash(n+113.0),hash(n+114.0),f.x),mix(hash(n+170.0),hash(n+171.0),f.x),f.y),f.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 starField(vec3 dir){
|
||||||
|
vec3 col=vec3(0.0);
|
||||||
|
col+=smoothstep(0.4,0.9,noise(dir*3.0+vec3(0.5,1.0,0.2))+0.5*noise(dir*7.0))*vec3(0.05,0.06,0.15);
|
||||||
|
for(int i=0;i<3;i++){
|
||||||
|
float sc=pow(8.0,float(i+1));
|
||||||
|
vec3 g=fract(dir*sc)-0.5;
|
||||||
|
float id=floor(dir.x*sc)*1.3+floor(dir.y*sc)*4.7+floor(dir.z*sc)*9.1;
|
||||||
|
col+=step(0.995-float(i)*0.003,hash(id))*smoothstep(0.12,0.0,length(g))*vec3(0.9,0.95,1.0)*(1.0-float(i)*0.3);
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Phase 1: Black Hole ---
|
||||||
|
vec3 traceBlackHole(vec3 ro, vec3 rd, float mass){
|
||||||
|
float rs=1.5*mass, rs2=rs*rs;
|
||||||
|
vec3 pos=ro, vel=normalize(rd);
|
||||||
|
for(int i=0;i<MAX_STEPS;i++){
|
||||||
|
float r=length(pos);
|
||||||
|
if(r<rs*0.98) return vec3(0.0);
|
||||||
|
vel+=-(1.5*rs2/(r*r*r))*(pos/r)*STEP_SIZE;
|
||||||
|
vel=normalize(vel);
|
||||||
|
pos+=vel*STEP_SIZE;
|
||||||
|
if(r>20.0) break;
|
||||||
|
}
|
||||||
|
return starField(normalize(vel));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Phase 2: Supernova Explosion ---
|
||||||
|
vec3 supernova(vec2 uv, float t, float sr){
|
||||||
|
vec3 col=vec3(0.0);
|
||||||
|
float angle=atan(uv.y,uv.x);
|
||||||
|
|
||||||
|
// sharp shockwave rings
|
||||||
|
for(int i=0;i<3;i++){
|
||||||
|
float delay=float(i)*0.15;
|
||||||
|
float rt=max(0.0, t-delay);
|
||||||
|
float ringR=rt*(2.0+float(i)*0.6);
|
||||||
|
// very thin ring with sharp falloff
|
||||||
|
float ringDist=abs(sr-ringR);
|
||||||
|
float ringW=0.008+float(i)*0.004;
|
||||||
|
float ring=exp(-ringDist*ringDist/(ringW*ringW));
|
||||||
|
// bright edge glow just outside ring
|
||||||
|
float edgeGlow=exp(-ringDist*ringDist/(ringW*ringW*8.0))*0.3;
|
||||||
|
vec3 ringCol=mix(vec3(1.0,0.95,0.85),vec3(1.0,0.75,0.3),float(i)/3.0);
|
||||||
|
float fade=exp(-rt*1.5)*(3.0-float(i)*0.5);
|
||||||
|
col+=ringCol*(ring+edgeGlow)*fade;
|
||||||
|
}
|
||||||
|
|
||||||
|
// intense central flash with slow decay
|
||||||
|
float flash=1.0/(sr*6.0+0.03)*max(0.0,1.0-t*1.2);
|
||||||
|
col+=vec3(1.0,0.97,0.92)*flash*0.6;
|
||||||
|
|
||||||
|
// hot debris particles with trails
|
||||||
|
for(int i=0;i<50;i++){
|
||||||
|
float fi=float(i);
|
||||||
|
float a=hash(fi*1.17)*PI*2.0;
|
||||||
|
float speed=0.8+hash(fi*2.31)*2.5;
|
||||||
|
float r=t*speed;
|
||||||
|
float life=1.0-smoothstep(0.0,0.6+hash(fi*3.7)*0.6, t);
|
||||||
|
vec2 dir=vec2(cos(a),sin(a));
|
||||||
|
vec2 ppos=dir*r;
|
||||||
|
// spiral motion
|
||||||
|
float spin=hash(fi*7.3)*2.0-1.0;
|
||||||
|
ppos+=vec2(-dir.y,dir.x)*sin(t*3.0+fi)*0.1*spin;
|
||||||
|
float d=length(uv-ppos);
|
||||||
|
// sharp tiny particle with subtle trail
|
||||||
|
float trail=exp(-d*d*8000.0)+exp(-pow(dot(uv-ppos,dir),2.0)*20000.0-pow(length(uv-ppos-dir*0.01),2.0)*5000.0)*0.3;
|
||||||
|
float bright=life*trail;
|
||||||
|
// temperature gradient: white-hot core -> gold -> amber
|
||||||
|
float temp=hash(fi*9.1);
|
||||||
|
vec3 pcol=mix(vec3(1.0,0.95,0.85),mix(vec3(1.0,0.75,0.3),vec3(0.9,0.5,0.1),temp),t*0.8);
|
||||||
|
col+=pcol*bright*1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Phase 3: Supernova afterglow (reuse explosion, fade out) ---
|
||||||
|
vec3 stardust(vec2 uv, float t, float sr){
|
||||||
|
// Continue supernova at t=1.0 state, then fade everything out
|
||||||
|
float fade=pow(1.0-t, 2.0);
|
||||||
|
// Run supernova frozen near end, slowly expanding
|
||||||
|
vec2 expandedUv=uv/(1.0+t*0.3);
|
||||||
|
float expandedSr=length(expandedUv);
|
||||||
|
vec3 col=supernova(expandedUv, 0.85+t*0.15, expandedSr)*fade;
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main(){
|
||||||
|
float aspect=uResolution.x/uResolution.y;
|
||||||
|
vec2 uv=vec2(vUv.x*aspect, vUv.y);
|
||||||
|
float sr=length(uv);
|
||||||
|
|
||||||
|
vec3 col=vec3(0.0);
|
||||||
|
float alpha=0.0;
|
||||||
|
|
||||||
|
if(uPhase<1.0){
|
||||||
|
// Phase 1: Black hole forming (0 -> 1)
|
||||||
|
float grow=uPhase*uPhase;
|
||||||
|
vec2 bhUv=uv*16.0;
|
||||||
|
float mass=3.0*grow;
|
||||||
|
vec3 cp=vec3(0.0,0.0,3.0);
|
||||||
|
vec3 rd=normalize(vec3(bhUv*(1.4-mass*0.1),-1.0));
|
||||||
|
col=traceBlackHole(cp,rd,mass);
|
||||||
|
|
||||||
|
float bhSr=length(bhUv);
|
||||||
|
float rs=1.5*mass;
|
||||||
|
float g=max(grow,0.001);
|
||||||
|
col+=vec3(0.3,0.5,1.0)*0.5*0.01*g/(bhSr*5.0/g+0.008)*rs;
|
||||||
|
col+=vec3(0.6,0.75,1.0)*exp(-abs(bhSr-0.055*g)*50.0/g)*g*0.6;
|
||||||
|
|
||||||
|
col=pow(col/(1.0+col),vec3(0.45));
|
||||||
|
alpha=clamp(grow*2.0,0.0,1.0)*clamp(length(col)*3.0,0.0,1.0);
|
||||||
|
|
||||||
|
} else if(uPhase<1.1){
|
||||||
|
// White flash transition
|
||||||
|
float t=(uPhase-1.0)/0.1;
|
||||||
|
float flash=1.0-t;
|
||||||
|
col=vec3(1.0,0.98,0.95)*flash*flash;
|
||||||
|
alpha=flash;
|
||||||
|
|
||||||
|
} else if(uPhase<2.0){
|
||||||
|
// Phase 2: Supernova explosion
|
||||||
|
float t=(uPhase-1.1)/0.9;
|
||||||
|
col=supernova(uv, t, sr);
|
||||||
|
col=min(col, vec3(1.0));
|
||||||
|
alpha=clamp(length(col)*5.0,0.0,1.0);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Phase 3: Stardust fade out
|
||||||
|
float t=(uPhase-2.0)/1.0;
|
||||||
|
col=stardust(uv, t, sr);
|
||||||
|
col=min(col, vec3(1.0));
|
||||||
|
alpha=clamp(length(col)*5.0,0.0,1.0)*(1.0-t*t);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_FragColor=vec4(col,alpha);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BURST_TOTAL_DURATION = 4.5; // seconds for full animation
|
||||||
|
|
||||||
|
function BlackHoleEffect() {
|
||||||
|
const matRef = useRef();
|
||||||
|
const startRef = useRef(null);
|
||||||
|
const uniforms = useMemo(() => ({
|
||||||
|
uTime: { value: 0 },
|
||||||
|
uPhase: { value: 0 },
|
||||||
|
uResolution: { value: new THREE.Vector2(1, 1) },
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
useFrame(({ clock, gl }) => {
|
||||||
|
if (!matRef.current) return;
|
||||||
|
const t = clock.getElapsedTime();
|
||||||
|
if (startRef.current === null) startRef.current = t;
|
||||||
|
const elapsed = t - startRef.current;
|
||||||
|
// Map elapsed time to 0-3 phase range over BURST_TOTAL_DURATION
|
||||||
|
const phase = Math.min(3.0, (elapsed / BURST_TOTAL_DURATION) * 3.0);
|
||||||
|
matRef.current.uniforms.uTime.value = t;
|
||||||
|
matRef.current.uniforms.uPhase.value = phase;
|
||||||
|
const size = gl.getSize(new THREE.Vector2());
|
||||||
|
matRef.current.uniforms.uResolution.value.copy(size);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh renderOrder={999} frustumCulled={false}>
|
||||||
|
<planeGeometry args={[2, 2]} />
|
||||||
|
<rawShaderMaterial
|
||||||
|
ref={matRef}
|
||||||
|
vertexShader={bhVertexShader}
|
||||||
|
fragmentShader={bhFragmentShader}
|
||||||
|
uniforms={uniforms}
|
||||||
|
transparent
|
||||||
|
depthTest={false}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Unified export ---
|
// --- Unified export ---
|
||||||
|
|
||||||
function EnergySphere({ type = 'sun', position = [0, 1.0, 0], scale = 1 }) {
|
function EnergySphere({ type = 'sun', position = [0, 1.0, 0], scale = 1 }) {
|
||||||
if (type === 'earth' || type === 'moon') {
|
if (type === 'earth' || type === 'moon') {
|
||||||
return <SphereEffect position={position} scale={scale} />;
|
return <SphereEffect position={position} scale={scale} />;
|
||||||
}
|
}
|
||||||
|
if (type === 'burst') {
|
||||||
|
return <BlackHoleEffect />;
|
||||||
|
}
|
||||||
return <GalaxyEffect position={position} scale={scale} />;
|
return <GalaxyEffect position={position} scale={scale} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const VRMA_FILES = [
|
|||||||
{ name: 'skill', label: 'スキル', file: 'skill_end.vrma', loop: false },
|
{ name: 'skill', label: 'スキル', file: 'skill_end.vrma', loop: false },
|
||||||
{ name: 'skill_loop', label: 'スキル継続', file: 'skill_loop.vrma', loop: true },
|
{ name: 'skill_loop', label: 'スキル継続', file: 'skill_loop.vrma', loop: true },
|
||||||
{ name: 'skill_end', label: 'スキル終了', file: 'skill_end.vrma', loop: false },
|
{ name: 'skill_end', label: 'スキル終了', file: 'skill_end.vrma', loop: false },
|
||||||
|
{ name: 'burst', label: 'バースト', file: 'burst.vrma', loop: false },
|
||||||
];
|
];
|
||||||
const VRMA_URLS = VRMA_FILES.map(v => `${BASE_URL}animation/${v.file}`);
|
const VRMA_URLS = VRMA_FILES.map(v => `${BASE_URL}animation/${v.file}`);
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const btnStyle = {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ControlPanel({ timeScale, onTimeScaleChange, camSpeed, onCamSpeedChange, onSkill, lang, onLangChange }) {
|
export default function ControlPanel({ timeScale, onTimeScaleChange, camSpeed, onCamSpeedChange, onSkill, onBurst, lang, onLangChange, volume, onVolumeChange }) {
|
||||||
return (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
<div>
|
<div>
|
||||||
@@ -77,13 +77,35 @@ export default function ControlPanel({ timeScale, onTimeScaleChange, camSpeed, o
|
|||||||
style={sliderStyle}
|
style={sliderStyle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={labelStyle}>
|
||||||
|
<span>Volume</span>
|
||||||
|
<span>{Math.round(volume * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={volume * 100}
|
||||||
|
onChange={(e) => onVolumeChange(Number(e.target.value) / 100)}
|
||||||
|
style={sliderStyle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
style={{ ...btnStyle, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
|
style={{ ...btnStyle, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
|
||||||
onClick={() => onSkill?.()}
|
onClick={() => onSkill?.()}
|
||||||
>
|
>
|
||||||
<img src={`${import.meta.env.BASE_URL}icon/dot.svg`} alt="" style={{ width: 14, height: 14, filter: 'brightness(0) invert(1)', opacity: 0.7 }} />
|
<img src={`${import.meta.env.BASE_URL}icon/skill.svg`} alt="" style={{ width: 14, height: 14, filter: 'brightness(0) invert(1)', opacity: 0.7 }} />
|
||||||
<span>Skill</span>
|
<span>Skill</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
style={{ ...btnStyle, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
|
||||||
|
onClick={() => onBurst?.()}
|
||||||
|
>
|
||||||
|
<img src={`${import.meta.env.BASE_URL}icon/burst.svg`} alt="" style={{ width: 14, height: 14, filter: 'brightness(0) invert(1)', opacity: 0.7 }} />
|
||||||
|
<span>Burst</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
style={{ ...btnStyle, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
|
style={{ ...btnStyle, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
|
||||||
onClick={() => onLangChange?.(lang === 'ja' ? 'en' : 'ja')}
|
onClick={() => onLangChange?.(lang === 'ja' ? 'en' : 'ja')}
|
||||||
|
|||||||
Reference in New Issue
Block a user