3D Grass Greeting Card

3D Grass Greeting Card script details
Type
Typescript logo image
typescript
App Version
0.19.6
Visibility
public
Date Created
Mar 5, 2025, 6:53:46 AM
Last Edit Date
Mar 18, 2025, 7:20:04 AM

Script Details

The Code
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();