createGUI();
const start = async () => {
// Assuming BabylonJS is already included and scene is initialized
const scene = bitbybit.babylon.scene.getScene();
const cam = bitbybit.babylon.scene.getActiveCamera() as BABYLON.ArcRotateCamera;
cam.position = new BABYLON.Vector3(10, 70, 10);
// ### AudioContext Setup
const audioCtx = new AudioContext();
const oscillatorPool = [];
for (let i = 0; i < 5; i++) {
const osc = audioCtx.createOscillator();
osc.type = 'sine';
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0, audioCtx.currentTime);
osc.connect(gain);
gain.connect(audioCtx.destination);
const vibratoOsc = audioCtx.createOscillator();
vibratoOsc.type = 'sine';
vibratoOsc.frequency.setValueAtTime(5, audioCtx.currentTime);
const vibratoGain = audioCtx.createGain();
vibratoGain.gain.setValueAtTime(10, audioCtx.currentTime);
vibratoOsc.connect(vibratoGain);
vibratoGain.connect(osc.frequency);
osc.start();
vibratoOsc.start();
oscillatorPool.push({ osc, gain, inUse: false });
}
function getAvailableOscillator() {
return oscillatorPool.find(oscData => !oscData.inUse) || null;
}
function modulateSound(osc, gain, frequency, isOpening) {
osc.frequency.setValueAtTime(frequency, audioCtx.currentTime);
if (isOpening) {
gain.gain.cancelScheduledValues(audioCtx.currentTime);
gain.gain.setValueAtTime(0, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.7, audioCtx.currentTime + 0.1);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.8);
} else {
gain.gain.cancelScheduledValues(audioCtx.currentTime);
gain.gain.setValueAtTime(0, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.5, audioCtx.currentTime + 0.2);
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.6);
}
}
// ### Shader Definitions for Petal Gradients
BABYLON.Effect.ShadersStore["petalVertexShader"] = `
precision highp float;
attribute vec3 position;
attribute vec2 uv;
uniform mat4 worldViewProjection;
varying vec2 vUV;
void main(void) {
gl_Position = worldViewProjection * vec4(position, 1.0);
vUV = uv;
}
`;
BABYLON.Effect.ShadersStore["petalFragmentShader"] = `
precision highp float;
varying vec2 vUV;
uniform float invertGradient;
void main(void) {
float t = invertGradient > 0.5 ? 1.0 - vUV.y : vUV.y;
vec3 color;
if (t < 0.5) {
color = mix(vec3(1, 0.0, 1), vec3(0.0, 0.0, 1.0), t * 2.0);
} else {
color = mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 1.0, 1.0), (t - 0.5) * 2.0);
}
gl_FragColor = vec4(color, 1.0);
}
`;
// ### Create Shader Materials
const petalMaterialNormal = new BABYLON.ShaderMaterial("petalShaderNormal", scene, {
vertex: "petal",
fragment: "petal"
}, {
attributes: ["position", "uv"],
uniforms: ["worldViewProjection", "invertGradient"]
});
petalMaterialNormal.setFloat("invertGradient", 0.0);
const petalMaterialInverted = new BABYLON.ShaderMaterial("petalShaderInverted", scene, {
vertex: "petal",
fragment: "petal"
}, {
attributes: ["position", "uv"],
uniforms: ["worldViewProjection", "invertGradient"]
});
petalMaterialInverted.setFloat("invertGradient", 1.0);
// ### Petal Creation Function
function createPetal(scene) {
const points = [
new BABYLON.Vector3(0, 0, 0),
new BABYLON.Vector3(0.15, 0.3, 0),
new BABYLON.Vector3(0.3, 0.6, 0),
new BABYLON.Vector3(0.4, 0.9, 0),
new BABYLON.Vector3(0.5, 1.2, 0),
new BABYLON.Vector3(0.6, 0.9, 0),
new BABYLON.Vector3(0.7, 0.6, 0)
];
const tube = BABYLON.MeshBuilder.CreateTube("petalTube", {
path: points,
radius: 0.03,
tessellation: 3,
cap: BABYLON.Mesh.CAP_ALL,
sideOrientation: BABYLON.Mesh.DOUBLESIDE,
updatable: false
}, scene);
return tube;
}
// ### Flower Creation Function
function createFlower(scene) {
const flower = new BABYLON.TransformNode("flower", scene);
const scale = 0.5 + Math.random() * 5.5; // Scale between 0.5 and 6.0
flower.scaling = new BABYLON.Vector3(scale, scale, scale);
let position;
let attempts = 0;
do {
position = new BABYLON.Vector3(Math.random() * 100 - 50, 0, Math.random() * 100 - 50); // Smaller 100x100 field
attempts++;
if (attempts > 10000) {
console.warn("Could not find non-overlapping position after 10000 attempts");
break;
}
} while (flowerDataArray.some(fd => BABYLON.Vector3.Distance(position, fd.position) < (scale + fd.scale) * 1.2)); // Adjusted overlap check
const amp_x = 0.05 + Math.random() * 0.2;
const amp_z = 0.05 + Math.random() * 0.2;
const amp_y = 0.05 + Math.random() * 0.2;
const phase_x = Math.random() * Math.PI * 2;
const phase_z = Math.random() * Math.PI * 2;
const phase_y = Math.random() * Math.PI * 2;
const basePosition = position.clone();
basePosition.y = amp_y;
flower.position = basePosition.clone();
const petalNodes = [];
const petalCount = 24;
const invertGradient = Math.random() < 0.5; // Per-flower gradient decision
const petalMaterial = invertGradient ? petalMaterialInverted : petalMaterialNormal;
for (let i = 0; i < petalCount; i++) {
const petalTube = createPetal(scene);
petalTube.material = petalMaterial; // Consistent material for all petals
petalTube.metadata = { flower: flower };
const petalNode = new BABYLON.TransformNode("petalNode", scene);
petalNode.rotation.y = (i / petalCount) * Math.PI * 2;
petalTube.parent = petalNode;
petalNode.parent = flower;
petalNodes.push(petalTube);
}
const flowerData = {
flower,
basePosition,
amp_x,
amp_z,
amp_y,
phase_x,
phase_z,
phase_y,
freq: 0.5 + Math.random(),
scale,
petalNodes,
position, // For overlap check
isAnimating: false,
animateFullCycle: function () {
if (this.isAnimating) return;
const oscData = getAvailableOscillator();
if (!oscData) return;
oscData.inUse = true;
modulateSound(oscData.osc, oscData.gain, 440 / this.scale, true);
this.isAnimating = true;
const openAnim = createBloomAnimation(true);
const closeAnim = createBloomAnimation(false);
scene.beginDirectAnimation(this.petalNodes, [openAnim], 0, 60, false, 1, () => {
modulateSound(oscData.osc, oscData.gain, 330 / this.scale, false);
scene.beginDirectAnimation(this.petalNodes, [closeAnim], 0, 60, false, 1, () => {
this.isAnimating = false; // Reset flag for re-triggering
oscData.inUse = false;
oscData.gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.2);
});
});
}
};
return flowerData;
}
// ### Bloom Animation Function
function createBloomAnimation(isOpening) {
const animation = new BABYLON.Animation(
"bloom",
"rotation.x",
30,
BABYLON.Animation.ANIMATIONTYPE_FLOAT,
BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
);
const keys = isOpening
? [{ frame: 0, value: 0 }, { frame: 60, value: -Math.PI / 4 }]
: [{ frame: 0, value: -Math.PI / 4 }, { frame: 60, value: 0 }];
animation.setKeys(keys);
const easing = new BABYLON.SineEase();
easing.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
animation.setEasingFunction(easing);
return animation;
}
// ### Create Flowers and Wind Animation
const flowerDataArray = [];
for (let i = 0; i < 250; i++) {
const flowerData = createFlower(scene);
flowerDataArray.push(flowerData);
}
scene.registerBeforeRender(() => {
const time = performance.now() / 1000;
flowerDataArray.forEach(flowerData => {
if (flowerData && flowerData.flower && flowerData.basePosition) {
const { basePosition, amp_x, amp_z, amp_y, phase_x, phase_z, phase_y, freq } = flowerData;
const x = basePosition.x + amp_x * Math.sin(freq * time + phase_x);
const y = basePosition.y + amp_y * Math.sin(freq * time + phase_y);
const z = basePosition.z + amp_z * Math.sin(freq * time + phase_z);
flowerData.flower.position.set(x, y, z);
} else {
console.warn("Skipping invalid flower data:", flowerData);
}
});
});
// ### Event Listeners for Interaction
const hoveredFlowers = new Set();
scene.onPointerMove = () => {
const pickResult = scene.pick(scene.pointerX, scene.pointerY);
if (pickResult.hit && pickResult.pickedMesh.metadata && pickResult.pickedMesh.metadata.flower) {
const flower = pickResult.pickedMesh.metadata.flower;
const flowerData = flowerDataArray.find(fd => fd.flower === flower);
if (flowerData && !flowerData.isAnimating && !hoveredFlowers.has(flower)) {
hoveredFlowers.add(flower);
flowerData.animateFullCycle();
}
} else {
hoveredFlowers.clear(); // Clear if not hovering over a flower
}
};
scene.onPointerUp = () => {
if (hoveredFlowers.size === 0) {
if (oscillatorPool) {
oscillatorPool.forEach(oscData => {
oscData.gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.2);
});
}
}
};
}
function createGUI() {
const gui = bitbybit.babylon.gui;
const textOpt = new Bit.Inputs.BabylonGui.CreateFullScreenUIDto();
const fullScreenUI = gui.advancedDynamicTexture.createFullScreenUI(textOpt);
const stackPanelOpt = new Bit.Inputs.BabylonGui.CreateStackPanelDto();
const stackPanel = gui.stackPanel.createStackPanel(stackPanelOpt);
const stackPanelOpt2 = new Bit.Inputs.BabylonGui.CreateStackPanelDto();
const stackPanel2 = gui.stackPanel.createStackPanel(stackPanelOpt2);
stackPanel2.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
fullScreenUI.addControl(stackPanel2);
stackPanel.paddingTopInPixels = 10;
stackPanel2.addControl(stackPanel);
const buttonOptions = new Bit.Inputs.BabylonGui.CreateButtonDto();
buttonOptions.width = "400px";
buttonOptions.height = "150px";
buttonOptions.label = "START EXPERIENCE";
buttonOptions.color = "#ffffff";
buttonOptions.background = "#00000000";
const button = gui.button.createSimpleButton(buttonOptions);
button.paddingTopInPixels = 20;
button.paddingBottomInPixels = 20;
button.onPointerClickObservable.add(() => {
start();
})
const imageOpt = new Bit.Inputs.BabylonGui.CreateImageDto();
imageOpt.url = "assets/logo-gold-small.png";
const image = gui.image.createImage(imageOpt);
const padding = 30;
image.paddingTopInPixels = padding;
image.paddingBottomInPixels = padding;
image.paddingLeftInPixels = padding;
image.paddingRightInPixels = padding;
image.onPointerClickObservable.add(() => {
window.open("https://bitbybit.dev", '_blank').focus();
});
const txtBlockOptions1 = new Bit.Inputs.BabylonGui.CreateTextBlockDto();
txtBlockOptions1.text = "Powered By";
const txtBlock1 = gui.textBlock.createTextBlock(txtBlockOptions1);
txtBlock1.height = "40px";
txtBlock1.paddingBottomInPixels = 10;
const txtBlockOptions = new Bit.Inputs.BabylonGui.CreateTextBlockDto();
txtBlockOptions.text = "BITBYBIT.DEV";
const txtBlock = gui.textBlock.createTextBlock(txtBlockOptions);
txtBlock.height = "60px";
txtBlock.paddingBottomInPixels = 30;
stackPanel2.addControl(button);
stackPanel2.addControl(image);
stackPanel2.addControl(txtBlock1);
stackPanel2.addControl(txtBlock);
}