pool_balls.glb (957.2 KB)
pool_table.glb (1.1 MB)

Above uploaded are my two file which am using for the table and the balls of the pool.
Below is my code -
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>8 Ball Pool 3D</title>
<link rel="stylesheet" href="src/style.css" />
</head>
<body>
<form style="--min: 0; --max: 100; --val: 0">
<input type="range" min="0" max="100" value="0" list="l" />
<datalist id="l">
<option label="min" value="0"></option>
<option label="max" value="100"></option>
</datalist>
</form>
<div id="app"></div>
<script type="module" src="src/script.js"></script>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
color: #fff;
font-family: Arial, sans-serif;
overflow: hidden;
}
#app {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
}
form {
--k: calc((var(--val) - var(--min)) / (var(--max) - var(--min)));
--pos: calc(1.125em + var(--k) * (100% - 2.25em));
grid-gap: 0.25em;
place-self: center;
position: absolute;
left: 70%;
top: 50%;
transform: translate(-50%, 0%) rotate(-90deg);
z-index: 2;
min-width: 8em;
width: calc(100% - 1.5em);
max-width: 19.5em;
filter: Saturate(var(--hl, 0));
transition: filter 0.3s ease-out;
}
form:focus-within,
form:hover {
--hl: 1;
}
input[type="range"] {
height: 30px;
width: 300px;
border-radius: 2.25em;
box-shadow: 0 -1px #eaeaea, 0 1px #fff;
background: linear-gradient(#c3c3c3, #f1f1f1);
cursor: pointer;
}
input[type="range"],
input[type="range"]::-webkit-slider-runnable-track,
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
}
input[type="range"][list]::-webkit-slider-container {
min-height: 20px;
}
input[type="range"]::-webkit-slider-container {
-webkit-user-modify: read-write !important;
margin: 0.375em;
height: 1.5em;
border-radius: 0.75em;
box-shadow: inset 0 1px 4px #8c8c8c;
background: linear-gradient(#f8dd36, #d68706) 0 / var(--pos) no-repeat,
linear-gradient(#efefef, #c9c9c9);
}
input[type="range"]::-webkit-slider-runnable-track {
margin: -0.375em;
}
input[type="range"]::-moz-range-track {
margin: 0.375em;
height: 1.5em;
border-radius: 0.75em;
box-shadow: inset 0 1px 4px #8c8c8c;
background: linear-gradient(#f8dd36, #d68706) 0 / var(--pos) no-repeat,
linear-gradient(#efefef, #c9c9c9);
}
input[type="range"]::-webkit-slider-thumb {
box-sizing: border-box;
border: solid 0.375em transparent;
width: 2.25em;
height: 2.25em;
border-radius: 50%;
box-shadow: 0 2px 5px #7d7d7d;
background: linear-gradient(#c5c5c5, whitesmoke) padding-box,
linear-gradient(#fbfbfb, #c2c2c2) border-box;
cursor: ew-resize;
}
input[type="range"]::-moz-range-thumb {
box-sizing: border-box;
border: solid 0.375em transparent;
width: 2.25em;
height: 2.25em;
border-radius: 50%;
box-shadow: 0 2px 5px #7d7d7d;
background: linear-gradient(#c5c5c5, whitesmoke) padding-box,
linear-gradient(#fbfbfb, #c2c2c2) border-box;
cursor: ew-resize;
}
input[type="range"]:focus {
outline: none;
}
datalist {
grid-row: 1;
grid-template-columns: 3em 1fr 3em;
place-content: end center;
margin: 0 -0.375em;
color: #bababa;
text-align: center;
text-transform: uppercase;
}
datalist::after {
place-self: end center;
margin-bottom: 3px;
width: min(12em, 100%);
min-height: 0.5em;
grid-area: 1/2;
background: linear-gradient(90deg, transparent 2px, #f0ba22 0) -1px/1em round;
clip-path: polygon(0 calc(100% - 1px), 0 100%, 100% 100%, 100% 0);
content: "";
}
import * as THREE from "three";
import * as CANNON from "cannon-es";
import { sources } from "./config";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import CannonDebugger from "cannon-es-debugger";
import gsap from "gsap";
class Game {
constructor(sources) {
this.gameApp = document.getElementById("app");
this.scene = new THREE.Scene();
this.sizes = {
width: window.innerWidth,
height: window.innerHeight,
pixelRatio: Math.min(window.devicePixelRatio, 2),
};
this.viewSize = 4.4;
const viewSize = this.viewSize;
const aspect = this.sizes.width / this.sizes.height;
this.camera = new THREE.OrthographicCamera(
-viewSize * aspect,
viewSize * aspect,
viewSize,
-viewSize,
0.1,
1000
);
this.camera.position.set(0, 20, 0);
this.camera.rotation.set(0, Math.PI / 2, 0);
this.camera.lookAt(0, 0, 0);
this.renderer = new THREE.WebGLRenderer({
antialias: true,
});
this.renderer.setClearColor(0x000000);
this.renderer.setSize(this.sizes.width, this.sizes.height);
this.renderer.setPixelRatio(this.sizes.pixelRatio);
this.gameApp.appendChild(this.renderer.domElement);
this.time = {
current: Date.now(),
delta: 16,
};
// this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.sources = sources;
this.loadedAssets = {};
this.balls = {};
this.physicsBodies = new Map();
this.channels = new Map();
this.ballsStoppedCheckEnabled = false;
this.initPhysics();
this.loadAssets().then(() => {
const stickPos = new THREE.Vector3(0, 2.6, -2);
stickPos.applyMatrix4(new THREE.Matrix4().makeRotationY(Math.PI));
this.stick = new Stick(stickPos, this.scene, this.loadedAssets, this);
this.initLighting();
this.setupTable();
this.setupBalls();
this.initPhysicsDebugger();
});
this.resize();
this.update();
window.onresize = () => this.resize();
console.log("%cThree.js Scene is initialized", "color: green");
}
loadAssets() {
const textureLoader = new THREE.TextureLoader();
const glbLoader = new GLTFLoader();
const promises = [];
for (const source of this.sources) {
if (source.type === "glb") {
promises.push(
new Promise((resolve, reject) => {
glbLoader.load(source.path, (gltf) => {
this.loadedAssets[source.name] = gltf;
resolve(gltf);
});
})
);
} else if (source.type === "texture") {
promises.push(
new Promise((resolve, reject) => {
textureLoader.load(source.path, (texture) => {
this.loadedAssets[source.name] = texture;
resolve(texture);
});
})
);
}
}
return Promise.all(promises);
}
setupTable() {
const table = this.loadedAssets.table;
table.scene.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
const tableScene = table.scene.clone();
tableScene.rotation.y += Math.PI;
this.scene.add(tableScene);
this.createTablePhysicsBody(tableScene);
console.log("%cTable loaded successfully", "color: red");
}
createTablePhysicsBody(tableScene) {
tableScene.updateMatrixWorld(true);
const vertices = [];
const indices = [];
let vertexOffset = 0;
tableScene.traverse((child) => {
if (!child.isMesh || !child.geometry?.attributes.position) return;
const positions = child.geometry.attributes.position.array;
const index = child.geometry.index?.array;
const startVertexIndex = vertexOffset;
const vertex = new THREE.Vector3();
for (let i = 0; i < positions.length; i += 3) {
vertex.set(positions[i], positions[i + 1], positions[i + 2]);
vertex.applyMatrix4(child.matrixWorld);
vertices.push(vertex.x, vertex.y, vertex.z);
}
if (index) {
for (let i = 0; i < index.length; i++) {
indices.push(index[i] + startVertexIndex);
}
} else {
const vertexCount = positions.length / 3;
for (let i = 0; i < vertexCount; i += 3) {
if (i + 2 < vertexCount) {
indices.push(
startVertexIndex + i,
startVertexIndex + i + 1,
startVertexIndex + i + 2
);
}
}
}
vertexOffset += positions.length / 3;
});
if (vertices.length > 0 && indices.length > 0) {
const tableBody = new CANNON.Body({ mass: 0 });
tableBody.addShape(new CANNON.Trimesh(vertices, indices));
tableBody.material = this.tableMaterial;
this.world.addBody(tableBody);
this.physicsBodies.set("table", tableBody);
}
}
initPhysics() {
this.world = new CANNON.World();
this.world.gravity.set(0, -9.82, 0);
this.world.broadphase = new CANNON.NaiveBroadphase();
this.world.solver.iterations = 10;
this.fixedTimeStep = 1 / 60;
this.maxSubSteps = 3;
this.tableMaterial = new CANNON.Material("table");
this.tableMaterial.friction = 0.14;
this.tableMaterial.restitution = 0.03;
this.holeBodies = [];
}
initLighting() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.1);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(10, 10, 0);
directionalLight.castShadow = true;
this.scene.add(directionalLight);
}
initPhysicsDebugger() {
// this.cannonDebugger = new CannonDebugger(this.scene, this.world);
}
arrangeBallsInTriangle(balls) {
const eightBall = balls.find((ball) => ball.name.includes("Ball8"));
const otherBalls = balls.filter((ball) => !ball.name.includes("Ball8"));
balls[0].updateMatrixWorld(true);
const ballRadius =
Math.max(
...new THREE.Box3()
.setFromObject(balls[0])
.getSize(new THREE.Vector3())
.toArray()
) / 2;
const ballDiameter = ballRadius * 2;
const rows = [1, 2, 3, 4, 5];
const rackCenterX = -2;
const rackCenterZ = 0;
const rackY = 2.57;
const centerRowIndex = 2;
const centerRowX = rackCenterX - centerRowIndex * ballDiameter * 0.9;
eightBall.position.set(rackCenterZ, rackY, centerRowX);
let ballIndex = 0;
rows.forEach((count, rowIndex) => {
const rowX = rackCenterX - rowIndex * ballDiameter * 0.9;
const startZ = rackCenterZ - ((count - 1) * ballDiameter) / 2;
for (let i = 0; i < count; i++) {
if (rowIndex === centerRowIndex && i === Math.floor(count / 2))
continue;
if (ballIndex < otherBalls.length) {
otherBalls[ballIndex].position.set(
startZ + i * ballDiameter,
rackY,
rowX
);
ballIndex++;
}
}
});
}
setupBalls() {
const balls = this.loadedAssets.balls;
balls.scene.scale.set(0.05, 0.05, 0.05);
this.balls.striped = {};
this.balls.solid = {};
this.balls.allBalls = [];
const ballMap = [
["Ball10", (ball) => (this.balls.striped.number_10 = ball)],
["Ball11", (ball) => (this.balls.striped.number_11 = ball)],
["Ball12", (ball) => (this.balls.striped.number_12 = ball)],
["Ball13", (ball) => (this.balls.striped.number_13 = ball)],
["Ball14", (ball) => (this.balls.striped.number_14 = ball)],
["Ball15", (ball) => (this.balls.striped.number_15 = ball)],
["Ball1", (ball) => (this.balls.solid.number_1 = ball)],
["Ball2", (ball) => (this.balls.solid.number_2 = ball)],
["Ball3", (ball) => (this.balls.solid.number_3 = ball)],
["Ball4", (ball) => (this.balls.solid.number_4 = ball)],
["Ball5", (ball) => (this.balls.solid.number_5 = ball)],
["Ball6", (ball) => (this.balls.solid.number_6 = ball)],
["Ball7", (ball) => (this.balls.solid.number_7 = ball)],
["Ball8", (ball) => (this.balls.black = ball)],
["Ball9", (ball) => (this.balls.striped.number_9 = ball)],
["whiteBall", (ball) => (this.balls.white = ball)],
];
const remainingBalls = [];
const scale = 0.005;
const rotationX = 0;
const rotationZ = Math.PI;
const whiteBallY = 2.57;
balls.scene.traverse((child) => {
if (!child.isMesh) return;
child.castShadow = true;
child.receiveShadow = true;
child.scale.set(scale, scale, scale);
this.scene.add(child);
this.balls.allBalls.push(child);
// mapping
for (const [key, assign] of ballMap) {
if (child.name.includes(key)) {
child.userData.name = key;
assign(child);
}
}
// INITIAL ROTATION (only once)
this.rotateBall(child, rotationX, Math.PI, rotationZ);
if (child.name.includes("whiteBall")) {
child.position.set(0, whiteBallY, 2);
} else {
remainingBalls.push(child);
}
});
this.arrangeBallsInTriangle(remainingBalls);
this.createBallPhysicsBodies();
this.createHoleColliders();
this.initBallsStoppedChannel();
console.log("%cBalls loaded successfully", "color: red");
}
rotateBall(ballMesh, rotationX = 0, rotationY = 0, rotationZ = 0) {
if (!ballMesh) return;
// Apply rotation to mesh
ballMesh.rotation.x = rotationX;
ballMesh.rotation.y = rotationY;
ballMesh.rotation.z = rotationZ;
ballMesh.updateMatrixWorld(true);
// Apply rotation to physics body if exists
const body = this.physicsBodies.get(ballMesh);
if (body) {
const euler = new THREE.Euler(rotationX, rotationY, rotationZ);
body.quaternion.setFromEuler(euler);
}
}
initBallsStoppedChannel() {
const channel = {
listeners: [],
subscribe: (callback) => {
channel.listeners.push(callback);
return () => {
channel.listeners = channel.listeners.filter((cb) => cb !== callback);
};
},
emit: (data) => {
channel.listeners.forEach((callback) => callback(data));
},
};
this.channels.set("ballsStopped", channel);
channel.subscribe((data) => {
console.log(
"%cAll balls stopped moving!",
"color: green; font-size: 16px; font-weight: bold",
data
);
});
this.ballsStoppedCheckEnabled = true;
}
checkBallsStopped() {
if (!this.ballsStoppedCheckEnabled || this.balls.allBalls.length === 0)
return;
const velocityThreshold = 0.01;
const angularVelocityThreshold = 0.01;
let allStopped = true;
const ballStates = [];
for (const ballMesh of this.balls.allBalls) {
const body = this.physicsBodies.get(ballMesh);
if (!body) continue;
const linearVel = body.velocity.length();
const angularVel = body.angularVelocity.length();
const isStopped =
linearVel < velocityThreshold && angularVel < angularVelocityThreshold;
ballStates.push({
name: ballMesh.userData.name || ballMesh.name,
linearVelocity: linearVel,
angularVelocity: angularVel,
stopped: isStopped,
});
if (!isStopped) {
allStopped = false;
}
}
if (allStopped) {
const channel = this.channels.get("ballsStopped");
if (channel) {
channel.emit({
timestamp: Date.now(),
ballStates: ballStates,
});
this.ballsStoppedCheckEnabled = false;
}
}
}
shootWhiteBall() {
if (!this.balls.white) return;
const whiteBallBody = this.physicsBodies.get(this.balls.white);
if (whiteBallBody) {
const force = new CANNON.Vec3(0, 0, -40);
whiteBallBody.applyImpulse(force, whiteBallBody.position);
this.ballsStoppedCheckEnabled = true;
console.log("%cWhite ball shot!", "color: yellow");
}
}
createHoleColliders() {
const holeSize = 0.15;
const holeDepth = 0.3;
const tableWidth = 3;
const tableLength = 6.2;
const holePositions = [
{ x: -tableWidth / 2 + 0.2, z: -tableLength / 2, name: "corner1" },
{ x: tableWidth / 2 - 0.2, z: -tableLength / 2, name: "corner2" },
{ x: -tableWidth / 2 + 0.2, z: tableLength / 2, name: "corner3" },
{ x: tableWidth / 2 - 0.2, z: tableLength / 2, name: "corner4" },
{ x: -tableWidth / 2, z: 0, name: "side1" },
{ x: tableWidth / 2, z: 0, name: "side2" },
];
const rotationMatrix = new THREE.Matrix4().makeRotationY(Math.PI);
holePositions.forEach((pos) => {
const position = new THREE.Vector3(pos.x, 2, pos.z);
position.applyMatrix4(rotationMatrix);
const holeShape = new CANNON.Box(
new CANNON.Vec3(holeSize, holeDepth, holeSize)
);
const holeBody = new CANNON.Body({ mass: 0 });
holeBody.addShape(holeShape);
holeBody.position.set(position.x, position.y, position.z);
holeBody.userData = { type: "hole", name: pos.name };
holeBody.collisionResponse = false;
this.world.addBody(holeBody);
this.holeBodies.push(holeBody);
});
}
createBallPhysicsBodies() {
if (this.balls.allBalls.length === 0) return;
this.balls.allBalls[0].updateMatrixWorld(true);
const tempBox = new THREE.Box3().setFromObject(this.balls.allBalls[0]);
const ballSize = tempBox.getSize(new THREE.Vector3());
const ballRadius = Math.max(ballSize.x, ballSize.y, ballSize.z) / 2;
const ballMaterial = new CANNON.Material("ball", {
friction: 0.2,
restitution: 1,
});
const tableBallContact = new CANNON.ContactMaterial(
this.tableMaterial,
ballMaterial,
{
friction: 0.14,
restitution: 0.7,
contactEquationStiffness: 1e7,
contactEquationRelaxation: 3,
frictionEquationStiffness: 1e7,
frictionEquationRelaxation: 3,
}
);
const ballBallContact = new CANNON.ContactMaterial(
ballMaterial,
ballMaterial,
{
friction: 0.015,
restitution: 0.93,
contactEquationStiffness: 1e7,
contactEquationRelaxation: 3,
frictionEquationStiffness: 1e7,
frictionEquationRelaxation: 3,
}
);
this.world.addContactMaterial(tableBallContact);
this.world.addContactMaterial(ballBallContact);
this.balls.allBalls.forEach((ballMesh) => {
ballMesh.updateMatrixWorld(true);
const body = new CANNON.Body({ mass: 1 });
body.material = ballMaterial;
body.addShape(new CANNON.Sphere(ballRadius));
body.position.copy(ballMesh.position);
body.quaternion.copy(ballMesh.quaternion);
body.linearDamping = 0.7;
body.angularDamping = 0.3;
this.world.addBody(body);
this.physicsBodies.set(ballMesh, body);
});
}
resize() {
this.sizes.width = window.innerWidth;
this.sizes.height = window.innerHeight;
const viewSize = this.viewSize;
const aspect = this.sizes.width / this.sizes.height;
this.camera.left = -viewSize * aspect;
this.camera.right = viewSize * aspect;
this.camera.top = viewSize;
this.camera.bottom = -viewSize;
this.camera.updateProjectionMatrix();
this.renderer.setSize(this.sizes.width, this.sizes.height);
this.renderer.setPixelRatio(this.sizes.pixelRatio);
}
update() {
const currentTime = Date.now();
this.time.delta = currentTime - this.time.current;
this.time.current = currentTime;
this.world.step(
this.fixedTimeStep,
Math.min(this.time.delta / 1000, 0.1),
this.maxSubSteps
);
this.physicsBodies.forEach((body, mesh) => {
if (mesh instanceof THREE.Mesh) {
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
}
});
this.checkBallsStopped();
this.cannonDebugger?.update();
this.stick?.update();
this.renderer.render(this.scene, this.camera);
this.controls?.update();
window.requestAnimationFrame(() => this.update());
}
}
class Stick {
constructor(position, scene, assets, game) {
this.position = position;
this.shooting = false;
this.visible = true;
this.rotation = 0;
this.power = 0;
this.trackMouse = true;
this.scene = scene;
this.assets = assets;
this.game = game;
this.mousePosition = new THREE.Vector2();
this.isDragging = false;
this.dragStartAngle = 0;
this.dragStartMouse = new THREE.Vector2();
this.currentAngle = 0;
this.offset = 0.1;
this.isInteractingWithInput = false;
this.init();
}
init() {
this.group = new THREE.Group();
this.mesh = new THREE.Mesh(
new THREE.PlaneGeometry(3.8, 0.2),
new THREE.MeshBasicMaterial({
map: this.assets.cueStick,
alphaTest: 0.5,
side: THREE.DoubleSide,
})
);
this.mesh.rotation.set(-Math.PI / 2, 0, Math.PI / 2);
const stickLength = 3.8;
this.mesh.position.set(0, 0, stickLength / 2);
this.group.add(this.mesh);
this.group.position.copy(this.position);
this.scene.add(this.group);
this.mesh.castShadow = true;
this.mesh.receiveShadow = true;
this.currentAngle = 0;
this.group.rotation.y = this.currentAngle;
this.setupEvents();
}
setupEvents() {
const handleStart = (clientX, clientY, event) => {
// Prevent rotation if clicking on input element
if (event && event.target && event.target.tagName === "INPUT") return;
if (this.shooting || !this.trackMouse) return;
this.isDragging = true;
this.dragStartMouse.set(clientX, clientY);
this.dragStartAngle = this.currentAngle;
};
const handleMove = (clientX, clientY) => {
if (!this.isDragging || this.shooting) return;
this.mousePosition.x = (clientX / window.innerWidth) * 2 - 1;
this.mousePosition.y = -(clientY / window.innerHeight) * 2 + 1;
this.currentAngle = this.getAngle();
this.group.rotation.y = this.currentAngle;
};
const handleEnd = () => {
this.isDragging = false;
};
window.addEventListener("mousedown", (e) =>
handleStart(e.clientX, e.clientY, e)
);
window.addEventListener("mousemove", (e) => {
if (this.isDragging) {
handleMove(e.clientX, e.clientY);
}
});
window.addEventListener("mouseup", handleEnd);
window.addEventListener(
"touchstart",
(e) => {
// Prevent rotation if touching input element
if (e.target && e.target.tagName === "INPUT") {
e.preventDefault();
return;
}
e.preventDefault();
if (e.touches.length > 0) {
handleStart(e.touches[0].clientX, e.touches[0].clientY, e);
}
},
{ passive: false }
);
window.addEventListener(
"touchmove",
(e) => {
e.preventDefault();
if (this.isDragging && e.touches.length > 0) {
handleMove(e.touches[0].clientX, e.touches[0].clientY);
}
},
{ passive: false }
);
window.addEventListener(
"touchend",
(e) => {
e.preventDefault();
handleEnd();
},
{ passive: false }
);
}
addAimLine() {
this.aimLine = new THREE.Line3(this.position, new THREE.Vector3(0, 0, 2));
this.scene.add(this.aimLine);
}
getAngle() {
if (!this.group || !this.game.camera) return 0;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(this.mousePosition, this.game.camera);
const plane = new THREE.Plane(
new THREE.Vector3(0, 1, 0),
-this.group.position.y
);
const intersectionPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, intersectionPoint);
const direction = new THREE.Vector3()
.subVectors(intersectionPoint, this.group.position)
.normalize();
return Math.atan2(-direction.x, -direction.z);
}
shootBall() {
if (!this.game.balls.white || this.shooting) return;
const whiteBallBody = this.game.physicsBodies.get(this.game.balls.white);
if (!whiteBallBody) return;
const forceMagnitude = this.offset * 40;
const forceX = -Math.sin(this.currentAngle) * forceMagnitude;
const forceZ = -Math.cos(this.currentAngle) * forceMagnitude;
const force = new CANNON.Vec3(forceX, 0, forceZ);
whiteBallBody.applyForce(force, whiteBallBody.position);
this.shooting = true;
this.game.ballsStoppedCheckEnabled = true;
console.log(
"%cBall shot with force:",
"color: pink",
forceMagnitude,
"direction:",
this.currentAngle
);
}
update() {
if (this.shooting) {
this.group.visible = false;
}
console.log(this.offset);
if (this.game.balls.white) {
const whiteBallPos = this.game.balls.white.position;
const offsetX = Math.sin(this.currentAngle) * this.offset;
const offsetZ = Math.cos(this.currentAngle) * this.offset;
this.group.position.set(
whiteBallPos.x + offsetX,
this.group.position.y,
whiteBallPos.z + offsetZ
);
}
}
}
window.onload = () => {
const game = new Game(sources);
document.documentElement.classList.add("js");
const inputElement = document.querySelector('input[type="range"]');
if (inputElement) {
const handleInputEnd = () => {
if (game.stick.isInteractingWithInput) {
game.stick.shootBall();
console.log("ball shot");
inputElement.value = 0;
inputElement.parentNode.style.setProperty("--val", 0);
game.stick.offset = 0.05;
game.stick.trackMouse = true;
game.stick.isInteractingWithInput = false;
}
};
inputElement.addEventListener("mousedown", () => {
game.stick.trackMouse = false;
game.stick.isInteractingWithInput = true;
});
inputElement.addEventListener("mouseup", () => {
handleInputEnd();
});
inputElement.addEventListener("mouseleave", () => {
handleInputEnd();
});
inputElement.addEventListener("touchstart", () => {
game.stick.trackMouse = false;
game.stick.isInteractingWithInput = true;
});
inputElement.addEventListener("touchend", () => {
handleInputEnd();
});
inputElement.addEventListener("touchcancel", () => {
handleInputEnd();
});
// Handle case where user releases mouse outside the input element
window.addEventListener("mouseup", () => {
handleInputEnd();
});
window.addEventListener("touchend", () => {
handleInputEnd();
});
}
document.addEventListener(
"input",
(e) => {
let _t = e.target;
_t.parentNode.style.setProperty("--val", +_t.value);
// Map input value (0-100) to offset range (0.05 to 1.0)
const inputValue = +_t.value;
game.stick.offset = 0.05 + (inputValue / 100) * 0.95;
},
false
);
};
The physics is behaving a bit wierd - don’t know if it’s because of the trimesh for the table or i have some issue with my logic and implementation. The shooting and other things are working fine but sometimes the balls just wierdly behave.
Can anyone help and fix this out and help in this ??