Created
December 22, 2019 13:48
-
-
Save kettanaito/d4c43109d69a244b91c4d7d223778c32 to your computer and use it in GitHub Desktop.
Question code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const VendorMachine = ({ | |
ballRadius, | |
gravity, | |
density, | |
drag, | |
onButtonClick, | |
}) => { | |
const canvasRef = React.useRef() | |
const { intersection, setIntersectionRef } = useIntersection({ | |
threshold: 0.5, | |
}) | |
let ctx = null | |
const fps = 1 / 60 | |
const dt = fps * 1000 // ms | |
let timer = false | |
const mouse = { | |
x: 0, | |
y: 0, | |
isDown: false, | |
} | |
const ag = 9.81 // m/s^2 acceleration due to gravity on earth = 9.81 m/s^2. | |
let width = 0 | |
let height = 0 | |
const balls = [] | |
const setupCanvas = () => { | |
const canvas = canvasRef.current | |
ctx = canvas.getContext('2d') | |
height = canvas.height | |
width = canvas.width | |
} | |
const initDraw = () => { | |
timer = setIntersectionRef(drawFrame, dt) | |
} | |
const stopDraw = () => { | |
clearInterval(timer) | |
} | |
const getMousePosition = (event) => { | |
const canvas = canvasRef.current | |
mouse.x = event.pageX - canvas.offsetLeft | |
mouse.y = event.pageY - canvas.offsetTop | |
} | |
const handleMouseDown = (event) => { | |
if (event.nativeEvent.which === 1) { | |
getMousePosition(event) | |
mouse.isDown = true | |
const hue = getRandomNumber(0, 50) | |
const saturation = getRandomNumber(85, 95) | |
const lightness = getRandomNumber(50, 70) | |
balls.push( | |
new Ball( | |
mouse.x, | |
mouse.y, | |
ballRadius, | |
0.7, | |
10, | |
`hsl(${hue}, ${saturation}%, ${lightness}%)` | |
) | |
) | |
} | |
} | |
const handleMouseUp = (event) => { | |
if (event.nativeEvent.which === 1) { | |
mouse.isDown = false | |
balls[balls.length - 1].velocity.x = | |
(balls[balls.length - 1].position.x - mouse.x) / 10 | |
balls[balls.length - 1].velocity.y = | |
(balls[balls.length - 1].position.y - mouse.y) / 10 | |
} | |
} | |
const handleWallCollision = (ball) => { | |
if (ball.position.x > width - ball.radius) { | |
ball.velocity.x *= ball.e | |
ball.position.x = width - ball.radius | |
} | |
if (ball.position.y > height - ball.radius) { | |
ball.velocity.y *= ball.e | |
ball.position.y = height - ball.radius | |
} | |
if (ball.position.x < ball.radius) { | |
ball.velocity.x *= ball.e | |
ball.position.x = ball.radius | |
} | |
if (ball.position.y < ball.radius) { | |
ball.velocity.y *= ball.e | |
ball.position.y = ball.radius | |
} | |
} | |
const handleBallCollision = (b1) => { | |
for (var i = 0; i < balls.length; i++) { | |
var b2 = balls[i] | |
if (b1.position.x !== b2.position.x && b1.position.y !== b2.position.y) { | |
// quick check for potential collisions using AABBs | |
if ( | |
b1.position.x + b1.radius + b2.radius > b2.position.x && | |
b1.position.x < b2.position.x + b1.radius + b2.radius && | |
b1.position.y + b1.radius + b2.radius > b2.position.y && | |
b1.position.y < b2.position.y + b1.radius + b2.radius | |
) { | |
// pythagoras | |
var distX = b1.position.x - b2.position.x | |
var distY = b1.position.y - b2.position.y | |
var d = Math.sqrt(distX * distX + distY * distY) | |
// checking circle vs circle collision | |
if (d < b1.radius + b2.radius) { | |
var nx = (b2.position.x - b1.position.x) / d | |
var ny = (b2.position.y - b1.position.y) / d | |
var p = | |
(2 * | |
(b1.velocity.x * nx + | |
b1.velocity.y * ny - | |
b2.velocity.x * nx - | |
b2.velocity.y * ny)) / | |
(b1.mass + b2.mass) | |
// calulating the point of collision | |
var colPointX = | |
(b1.position.x * b2.radius + b2.position.x * b1.radius) / | |
(b1.radius + b2.radius) | |
var colPointY = | |
(b1.position.y * b2.radius + b2.position.y * b1.radius) / | |
(b1.radius + b2.radius) | |
// stoping overlap | |
b1.position.x = | |
colPointX + (b1.radius * (b1.position.x - b2.position.x)) / d | |
b1.position.y = | |
colPointY + (b1.radius * (b1.position.y - b2.position.y)) / d | |
b2.position.x = | |
colPointX + (b2.radius * (b2.position.x - b1.position.x)) / d | |
b2.position.y = | |
colPointY + (b2.radius * (b2.position.y - b1.position.y)) / d | |
// updating velocity to reflect collision | |
b1.velocity.x -= p * b1.mass * nx | |
b1.velocity.y -= p * b1.mass * ny | |
b2.velocity.x += p * b2.mass * nx | |
b2.velocity.y += p * b2.mass * ny | |
} | |
} | |
} | |
} | |
} | |
const drawFrame = () => { | |
// Clear window at the begining of every frame | |
ctx.clearRect(0, 0, width, height) | |
const ballsCount = balls.length | |
for (var i = 0; i < ballsCount; i++) { | |
if (!mouse.isDown || i !== ballsCount - 1) { | |
// physics - calculating the aerodynamic forces to drag | |
// -0.5 * Cd * A * v^2 * rho | |
var fx = | |
-drag.value * | |
density * | |
balls[i].area * | |
balls[i].velocity.x * | |
balls[i].velocity.x * | |
(balls[i].velocity.x / Math.abs(balls[i].velocity.x)) | |
var fy = | |
-drag.value * | |
density * | |
balls[i].area * | |
balls[i].velocity.y * | |
balls[i].velocity.y * | |
(balls[i].velocity.y / Math.abs(balls[i].velocity.y)) | |
fx = isNaN(fx) ? 0 : fx | |
fy = isNaN(fy) ? 0 : fy | |
// Calculating the accleration of the ball | |
// F = ma or a = F/m | |
var ax = fx / balls[i].mass | |
var ay = ag * gravity + fy / balls[i].mass | |
// Calculating the ball velocity | |
balls[i].velocity.x += ax * fps | |
balls[i].velocity.y += ay * fps | |
// Calculating the position of the ball | |
balls[i].position.x += balls[i].velocity.x * fps * 100 | |
balls[i].position.y += balls[i].velocity.y * fps * 100 | |
} | |
// Rendering the ball | |
ctx.beginPath() | |
ctx.fillStyle = balls[i].color | |
ctx.arc( | |
balls[i].position.x, | |
balls[i].position.y, | |
balls[i].radius, | |
0, | |
2 * Math.PI, | |
true | |
) | |
ctx.fill() | |
ctx.closePath() | |
// Handling the ball collisions | |
handleBallCollision(balls[i]) | |
handleWallCollision(balls[i]) | |
} | |
} | |
// Public methods | |
const throwBall = React.useCallback(() => { | |
const canvas = canvasRef.current | |
const startMousePos = { | |
// x: getRandomNumber(0, canvas.width) + canvas.offsetLeft, | |
x: canvas.width / 2 + canvas.offsetLeft, | |
y: 10 + canvas.offsetTop, | |
} | |
const endMousePos = { | |
x: startMousePos.x + getRandomNumber(-20, 20), | |
y: startMousePos.y + getRandomNumber(-20, 20), | |
} | |
const mouseDownEvent = new MouseEvent('mousedown', { | |
bubbles: true, | |
clientX: startMousePos.x, | |
clientY: startMousePos.y, | |
relatedTarget: canvas, | |
}) | |
const mouseUpEvent = new MouseEvent('mouseup', { | |
bubbles: true, | |
clientX: endMousePos.x, | |
clientY: endMousePos.y, | |
relatedTarget: canvas, | |
}) | |
mouseDownEvent.nativeEvent = mouseDownEvent | |
mouseUpEvent.nativeEvent = mouseUpEvent | |
handleMouseDown(mouseDownEvent) | |
getMousePosition(mouseUpEvent) | |
handleMouseUp(mouseUpEvent) | |
}, [canvasRef]) | |
React.useEffect(() => { | |
setupCanvas() | |
return () => stopDraw() | |
}, []) | |
React.useLayoutEffect(() => { | |
// Draw on the canvas only when the component is visible | |
if (intersection.isIntersecting) { | |
initDraw() | |
console.log('drawing...') | |
} else { | |
console.warn('stopped drawing!') | |
stopDraw() | |
} | |
}, [intersection.isIntersecting]) | |
return ( | |
<VendingMachineContainer> | |
<ButtonContainer> | |
<RedButton onClick={() => onButtonClick(throwBall)} /> | |
</ButtonContainer> | |
<img | |
ref={setIntersectionRef} | |
src={vendingMachineImage} | |
alt="Ball vending maching" | |
/> | |
<StyledCanvas ref={canvasRef} width={154} height={185} /> | |
</VendingMachineContainer> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment