let text1 = "BITBYBIT";
let text2 = "GREETINGS!";
const makeGrass = true;
interface Weed {
basePosition: BABYLON.Vector3,
linesMesh: BABYLON.LinesMesh,
randomOffset: number,
smoothedMouseInfluence: number,
smoothedMouseDirection: BABYLON.Vector3
};
interface ResTxt {
movedGridOfPts: Bit.Inputs.Base.Point3[];
wiresDrawnMesh: BABYLON.Mesh;
pointsDrawnMesh: BABYLON.Mesh;
}
interface ResultWeed {
weeds: Weed[],
text3D1: Bit.Advanced.Text3D.Text3DData<Bit.Inputs.OCCT.TopoDSShapePointer>;
text3D2: Bit.Advanced.Text3D.Text3DData<Bit.Inputs.OCCT.TopoDSShapePointer>;
r1: ResTxt;
r2: ResTxt;
}
let resultWeed: ResultWeed;
const numSegments = 3; // Segments per weed
const segmentLength = 0.5; // Length of each segment
const weedHeight = numSegments * segmentLength; // Total height = 2 units
const mouseEffectRadius = 3; // Radius of mouse wind influence
const maxMouseInfluence = 1; // Maximum bending strength
const recoveryAlpha = 0.1; // Recovery speed (lower = slower)
const velocityInfluenceFactor = 2; // Mouse speed effect
const scene = bitbybit.babylon.scene.getScene();
const camera = bitbybit.babylon.scene.getActiveCamera() as BABYLON.ArcRotateCamera;
camera.position = new BABYLON.Vector3(0, 30, -20);
camera.target = new BABYLON.Vector3(0, 0, -4);
createGUI();
const start = async () => {
if (!makeGrass) {
// bitbybit.draw.drawAnyAsync({
// entity: filteredPoints
// })
} else {
// **Constants**
// **Create invisible ground at the top of the weeds**
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 200, height: 100 }, scene);
ground.position.y = weedHeight; // Ground at weed tops (y = 2)
ground.visibility = 0; // Invisible but pickable
// **Weed creation**
resultWeed = await createWeeds(text1, text2, numSegments, segmentLength, scene);
// **Mouse and time tracking**
let time = 0;
let mouseWorldPos = new BABYLON.Vector3(0, weedHeight, 0); // Start at weed top height
let lastMouseWorldPos = mouseWorldPos.clone();
let mouseVelocity = new BABYLON.Vector3(0, 0, 0);
if (scene.metadata && scene.metadata.observable) {
scene.metadata.observable.remove();
}
// **Mouse movement handler**
const resObs = scene.onPointerObservable.add((pointerInfo) => {
if (pointerInfo.type === BABYLON.PointerEventTypes.POINTERMOVE && pointerInfo.event.buttons === 0) {
const pickInfo = scene.pick(pointerInfo.event.clientX, pointerInfo.event.clientY);
if (pickInfo.hit && pickInfo.pickedMesh === ground) {
lastMouseWorldPos = mouseWorldPos.clone();
mouseWorldPos = pickInfo.pickedPoint;
mouseVelocity = mouseWorldPos.subtract(lastMouseWorldPos);
}
}
});
if (scene.metadata) {
scene.metadata.observable = resObs;
} else {
scene.metadata = {
observable: resObs
}
}
// **Render loop**
bitbybit.time.registerRenderFunction(() => {
time += 0.02;
if (resultWeed.weeds.length > 0) {
resultWeed.weeds.forEach(weed => {
// **Mouse influence as radial wind**
const weedTop = weed.basePosition.clone();
weedTop.y = weedHeight;
const fromWeedTopToMouse = mouseWorldPos.subtract(weedTop); // Direction away from weed top
const distance = fromWeedTopToMouse.length();
const falloff = (distance < mouseEffectRadius) ? (1 - distance / mouseEffectRadius) : 0;
// Directional influence from mouse velocity
const velocityNorm = mouseVelocity.length() > 0 ? mouseVelocity.normalize() : new BABYLON.Vector3(0, 0, 0);
const toMouseNorm = fromWeedTopToMouse.length() > 0 ? fromWeedTopToMouse.normalize() : new BABYLON.Vector3(0, 0, 0);
const velocityDot = BABYLON.Vector3.Dot(velocityNorm, toMouseNorm);
const velocityInfluence = Math.max(0, velocityDot) * mouseVelocity.length() * velocityInfluenceFactor;
// Total target influence
const targetMouseInfluence = falloff * maxMouseInfluence + velocityInfluence;
// **Smooth recovery**
weed.smoothedMouseInfluence = BABYLON.Scalar.Lerp(
weed.smoothedMouseInfluence,
targetMouseInfluence,
recoveryAlpha
);
const targetDirection = fromWeedTopToMouse.length() > 0 ? fromWeedTopToMouse.normalize() : new BABYLON.Vector3(0, 0, 0);
weed.smoothedMouseDirection = BABYLON.Vector3.Lerp(
weed.smoothedMouseDirection,
targetDirection,
recoveryAlpha
);
// **Build weed segments**
const points = [];
let currentPoint = weed.basePosition.clone();
points.push(currentPoint.clone());
const windVariation = Math.sin(time + weed.randomOffset) * 0.15;
const windStrength = 0.05 + windVariation;
const windAxis = new BABYLON.Vector3(1, 0, 0);
let mouseAxis = null;
if (weed.smoothedMouseInfluence > 0 && weed.smoothedMouseDirection.length() > 0) {
mouseAxis = BABYLON.Vector3.Cross(BABYLON.Axis.Y, weed.smoothedMouseDirection);
}
// Accumulate angles to ensure bottom leans less
let accumulatedMouseAngle = 0;
for (let k = 1; k <= numSegments; k++) {
const windAngle = windStrength * k;
const windMatrix = BABYLON.Matrix.RotationAxis(windAxis, windAngle);
let segmentDir = new BABYLON.Vector3(0, segmentLength, 0);
if (mouseAxis && weed.smoothedMouseInfluence > 0) {
// Incremental angle increase from base to top, leaning away
const mouseInfluenceFactor = k / numSegments; // 0.2 at k=1, 1 at k=5
const mouseAngleIncrement = weed.smoothedMouseInfluence * mouseInfluenceFactor * 0.2;
accumulatedMouseAngle += mouseAngleIncrement; // Add to previous angle
const mouseMatrix = BABYLON.Matrix.RotationAxis(mouseAxis, accumulatedMouseAngle); // Positive for away
segmentDir = BABYLON.Vector3.TransformCoordinates(segmentDir, mouseMatrix);
}
// Apply wind after mouse bending
segmentDir = BABYLON.Vector3.TransformCoordinates(segmentDir, windMatrix);
currentPoint = currentPoint.add(segmentDir);
points.push(currentPoint.clone());
}
const positions = points.reduce((acc, p) => acc.concat([p.x, p.y, p.z]), []);
weed.linesMesh.updateVerticesData(BABYLON.VertexBuffer.PositionKind, positions);
});
}
})
}
}
async function createWeeds(text1: string, text2: string, numSegments: number, segmentLength: number, scene: BABYLON.Scene) {
const textOpt = new Bit.Advanced.Text3D.Text3DDto();
textOpt.text = text1.toUpperCase();
textOpt.fontType = Bit.Advanced.Text3D.fontsEnum.Roboto;
textOpt.fontVariant = Bit.Advanced.Text3D.fontVariantsEnum.BoldItalic;
textOpt.height = 0;
textOpt.fontSize = 10;
textOpt.originAlignment = Bit.Advanced.Text3D.recAlignmentEnum.centerMiddle;
const text3D1 = await bitbybit.advanced.text3d.create(textOpt);
textOpt.text = text2.toUpperCase();
textOpt.fontSize = 7;
const text3D2 = await bitbybit.advanced.text3d.create(textOpt);
const separator = 5;
const r1 = await createTextPoints(text3D1, separator);
const r2 = await createTextPoints(text3D2, -separator);
const filteredPoints = [...r1.movedGridOfPts, ...r2.movedGridOfPts];
const weeds: Weed[] = [];
const baseColor = new BABYLON.Color4(0.5, 0, 1, 1); // Purple at base
const midColor = new BABYLON.Color4(0, 0.5, 1, 1); // Blue in middle
let topColor = new BABYLON.Color4(1, 0, 1, 1); // White at top
for (let i = 0; i < filteredPoints.length; i++) {
const x = filteredPoints[i][0];
const z = filteredPoints[i][2];
const basePosition = new BABYLON.Vector3(x, 0, z);
if (i === r1.movedGridOfPts.length) {
topColor = new BABYLON.Color4(1, 1, 1, 1); // White at top
}
const points = [];
const colors = [];
for (let k = 0; k <= numSegments; k++) {
points.push(new BABYLON.Vector3(x, k * segmentLength, z));
const t = k / numSegments;
let color;
if (t < 0.5) {
color = BABYLON.Color4.Lerp(baseColor, midColor, t * 2);
} else {
color = BABYLON.Color4.Lerp(midColor, topColor, (t - 0.5) * 2);
}
colors.push(color);
}
const linesMesh = BABYLON.MeshBuilder.CreateLines("weed" + i, {
points: points,
colors: colors,
updatable: true
}, scene);
linesMesh.isPickable = false;
weeds.push({
basePosition,
linesMesh,
randomOffset: Math.random() * Math.PI * 6,
smoothedMouseInfluence: 0,
smoothedMouseDirection: new BABYLON.Vector3(0, 0, 0)
});
}
[r1.wiresDrawnMesh, r2.wiresDrawnMesh].forEach(c => {
bitbybit.babylon.mesh.setVisibility({
babylonMesh: c,
visibility: 1,
includeChildren: true,
});
});
[r1.pointsDrawnMesh, r2.pointsDrawnMesh].forEach(c => {
c.getChildMeshes().forEach(cx => cx.isVisible = true);
});
return { weeds, text3D1, text3D2, r1, r2 };
}
async function createTextPoints(txt3D: Bit.Advanced.Text3D.Text3DData<Bit.Inputs.OCCT.TopoDSShapePointer>, upDist: number) {
const hexGridOpt = new Bit.Inputs.Point.HexGridCentersDto();
const width = txt3D.boundingBox.x2 - txt3D.boundingBox.x1;
const length = txt3D.boundingBox.y2 - txt3D.boundingBox.y1;
const textWires = await bitbybit.occt.shapes.wire.getWires({ shape: txt3D.compound });
const textWiresCompound = await bitbybit.occt.shapes.compound.makeCompound({
shapes: textWires
});
const textWiresCompoundTranslated = await bitbybit.occt.transforms.translate({
shape: textWiresCompound,
translation: [0, 0, upDist]
});
const textOptions = new Bit.Inputs.Draw.DrawOcctShapeSimpleOptions();
textOptions.edgeWidth = 10;
textOptions.edgeColour = "#222222";
const wiresDrawnMesh = await bitbybit.draw.drawAnyAsync({
entity: textWiresCompoundTranslated,
options: textOptions
});
bitbybit.babylon.mesh.setVisibility({
babylonMesh: wiresDrawnMesh,
visibility: 0,
includeChildren: true,
});
hexGridOpt.pointsOnGround = true;
hexGridOpt.orientOnCenter = true;
hexGridOpt.nrHexagonsX = width * 3.5;
hexGridOpt.nrHexagonsY = length * 4;
hexGridOpt.radiusHexagon = 0.18;
const gridOfPts = bitbybit.point.hexGrid(hexGridOpt);
const faces = await bitbybit.occt.shapes.face.getFaces({ shape: txt3D.compound });
const opt = new Bit.Inputs.OCCT.FilterFacesPointsDto<Bit.Inputs.OCCT.TopoDSFacePointer>();
opt.shapes = faces;
opt.points = gridOfPts;
opt.gapTolerance = 0.1;
const filteredPoints = await bitbybit.occt.shapes.face.filterFacesPoints(opt) as Bit.Inputs.Base.Point3[];
const movedGridOfPts = bitbybit.point.translatePoints({
points: filteredPoints,
translation: [0, 0, upDist],
});
const optDraw = new Bit.Inputs.Draw.DrawBasicGeometryOptions();
optDraw.size = 0.04;
optDraw.colours = "#0000ff";
const pointsDrawnMesh = await bitbybit.draw.drawAnyAsync({
entity: movedGridOfPts,
options: optDraw,
});
pointsDrawnMesh.getChildMeshes().forEach(c => c.isVisible = false);
return { movedGridOfPts, wiresDrawnMesh, pointsDrawnMesh };
}
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 textInput1 = createInput(gui, text1, "#ff66ff", text1);
const textInput2 = createInput(gui, text2, "#5555ff", text2);
textInput1.onBlurObservable.add(async () => {
textInput1.isEnabled = false;
textInput2.isEnabled = false;
text1 = textInput1.text;
resultWeed.weeds.forEach(x => x.linesMesh.dispose());
resultWeed.r1.pointsDrawnMesh.dispose();
resultWeed.r1.wiresDrawnMesh.dispose();
resultWeed.r2.pointsDrawnMesh.dispose();
resultWeed.r2.wiresDrawnMesh.dispose();
const res = await createWeeds(text1, text2, numSegments, segmentLength, scene);
resultWeed = res;
textInput1.isEnabled = true;
textInput2.isEnabled = true;
});
textInput2.onBlurObservable.add(async () => {
textInput1.isEnabled = false;
textInput2.isEnabled = false;
text2 = textInput2.text;
resultWeed.weeds.forEach(x => x.linesMesh.dispose());
resultWeed.r1.pointsDrawnMesh.dispose();
resultWeed.r1.wiresDrawnMesh.dispose();
resultWeed.r2.pointsDrawnMesh.dispose();
resultWeed.r2.wiresDrawnMesh.dispose();
const res = await createWeeds(text1, text2, numSegments, segmentLength, scene);
resultWeed = res;
textInput1.isEnabled = true;
textInput2.isEnabled = true;
});
const buttonOptions = new Bit.Inputs.BabylonGui.CreateButtonDto();
buttonOptions.width = "400px";
buttonOptions.height = "150px";
buttonOptions.label = "COPY URL FOR SHARING!";
buttonOptions.color = "#ffffff";
buttonOptions.background = "#00000000";
const button = gui.button.createSimpleButton(buttonOptions);
button.paddingTopInPixels = 20;
button.paddingBottomInPixels = 20;
button.onPointerClickObservable.add(() => {
navigator.clipboard.writeText(`https://bitbybit.dev/app/bitbybit/z8JaJdkMv1FAtidWipDX/713kfZQNxnULXUXsz2Ox/preview?occt=true&jscad=false&manifold=false&title=${encodeURIComponent(text1)}&subtitle=${encodeURIComponent(text2)}`)
})
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(textInput1);
stackPanel2.addControl(textInput2);
stackPanel2.addControl(button);
stackPanel2.addControl(image);
stackPanel2.addControl(txtBlock1);
stackPanel2.addControl(txtBlock);
}
function createInput(gui: Bit.BabylonGui, text: string, color: string, textVariableToUpdate: string) {
const txtInpt1Opt = new Bit.Inputs.BabylonGui.CreateInputTextDto();
let originalText = text;
txtInpt1Opt.text = originalText;
txtInpt1Opt.width = "400px";
txtInpt1Opt.height = "100px";
const textInput = gui.inputText.createInputText(txtInpt1Opt);
textInput.fontSize = "40px";
textInput.paddingTopInPixels = 10;
textInput.paddingBottomInPixels = 10;
textInput.color = color;
textInput.onTextChangedObservable.add((newText) => {
if (newText.text && newText.text.length > 20) {
textInput.text = originalText.toUpperCase();
}
else {
originalText = newText.text;
textInput.text = newText.text.toUpperCase()
}
});
return textInput;
}
start();