<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/[email protected]/konva.min.js"></script> <meta charset="utf-8" /> <title>Konva Physics Simulator Demo</title> <style> body { margin: 0; padding: 0; overflow: hidden; background-color: #f0f0f0; } </style> </head> <body> <div id="container"></div> <script> var width = window.innerWidth; var height = window.innerHeight;
/* * Vector math functions */ function dot(a, b) { return a.x * b.x + a.y * b.y; } function magnitude(a) { return Math.sqrt(a.x * a.x + a.y * a.y); } function normalize(a) { var mag = magnitude(a);
if (mag === 0) { return { x: 0, y: 0, }; } else { return { x: a.x / mag, y: a.y / mag, }; } } function add(a, b) { return { x: a.x + b.x, y: a.y + b.y, }; } function angleBetween(a, b) { return Math.acos(dot(a, b) / (magnitude(a) * magnitude(b))); } function rotate(a, angle) { var ca = Math.cos(angle); var sa = Math.sin(angle); var rx = a.x * ca - a.y * sa; var ry = a.x * sa + a.y * ca; return { x: rx * -1, y: ry * -1, }; } function invert(a) { return { x: a.x * -1, y: a.y * -1, }; } /* * this cross product function has been simplified by * setting x and y to zero because vectors a and b * lie in the canvas plane */ function cross(a, b) { return { x: 0, y: 0, z: a.x * b.y - b.x * a.y, }; } function getNormal(curve, ball) { var curveLayer = curve.getLayer(); var context = curveLayer.getContext(); var testRadius = 20; var totalX = 0; var totalY = 0; var x = ball.x(); var y = ball.y(); /* * check various points around the center point * to determine the normal vector */ for (var n = 0; n < 20; n++) { var angle = (n * 2 * Math.PI) / 20; var offsetX = testRadius * Math.cos(angle); var offsetY = testRadius * Math.sin(angle); var testX = x + offsetX; var testY = y + offsetY; if (!context._context.isPointInPath(testX, testY)) { totalX += offsetX; totalY += offsetY; } }
var normal;
if (totalX === 0 && totalY === 0) { normal = { x: 0, y: -1, }; } else { normal = { x: totalX, y: totalY, }; }
return normalize(normal); } function handleCurveCollision(ball, curve) { var curveLayer = curve.getLayer(); var x = ball.x(); var y = ball.y();
var curveDamper = 0.05; if (curveLayer.getIntersection({ x: x, y: y })) { var normal = getNormal(curve, ball); if (normal !== null) { var angleToNormal = angleBetween(normal, invert(ball.velocity)); var crossProduct = cross(normal, ball.velocity); var polarity = crossProduct.z > 0 ? 1 : -1; var collisonAngle = polarity * angleToNormal * 2; var collisionVector = rotate(ball.velocity, collisonAngle);
ball.velocity.x = collisionVector.x; ball.velocity.y = collisionVector.y; ball.velocity.x *= 1 - curveDamper; ball.velocity.y *= 1 - curveDamper;
x += normal.x; if (ball.velocity.y > 0.1) { y += normal.y; } else { y += normal.y / 10; } ball.x(x).y(y); }
tween.finish(); } } function updateBall(frame) { var timeDiff = frame.timeDiff; var stage = ball.getStage(); var height = stage.height(); var width = stage.width(); var x = ball.x(); var y = ball.y(); var radius = ball.radius();
tween.reverse();
var gravity = 10; var speedIncrementFromGravityEachFrame = (gravity * timeDiff) / 1000; var collisionDamper = 0.2; var floorFriction = 5; var floorFrictionSpeedReduction = (floorFriction * timeDiff) / 1000;
if (ball.isDragging()) { var mousePos = stage.getPointerPosition();
if (mousePos) { var mouseX = mousePos.x; var mouseY = mousePos.y;
var c = 0.06 * timeDiff; ball.velocity = { x: c * (mouseX - ball.lastMouseX), y: c * (mouseY - ball.lastMouseY), };
ball.lastMouseX = mouseX; ball.lastMouseY = mouseY; } } else { ball.velocity.y += speedIncrementFromGravityEachFrame; x += ball.velocity.x; y += ball.velocity.y;
if (y < radius) { y = radius; ball.velocity.y *= -1; ball.velocity.y *= 1 - collisionDamper; }
if (y > height - radius) { y = height - radius; ball.velocity.y *= -1; ball.velocity.y *= 1 - collisionDamper; }
if (y == height - radius) { if (ball.velocity.x > 0.1) { ball.velocity.y -= floorFrictionSpeedReduction; } else if (ball.velocity.x < -0.1) { ball.velocity.x += floorFrictionSpeedReduction; } else { ball.velocity.x = 0; } }
if (x > width - radius) { x = width - radius; ball.velocity.x *= -1; ball.velocity.x *= 1 - collisionDamper; }
if (x < radius) { x = radius; ball.velocity.x *= -1; ball.velocity.x *= 1 - collisionDamper; }
ball.position({ x: x, y: y });
/* * if the ball comes into contact with the * curve, then bounce it in the direction of the * curve's surface normal */ collision = handleCurveCollision(ball, curve); } }
var stage = new Konva.Stage({ container: 'container', width: width, height: height, });
var curveLayer = new Konva.Layer(); var ballLayer = new Konva.Layer(); var radius = 20; var anim;
var curve = new Konva.Shape({ sceneFunc: function (context) { context.beginPath(); context.moveTo(40, height); context.bezierCurveTo( width * 0.2, -1 * height * 0.5, width * 0.7, height * 1.3, width, height * 0.5 ); context.lineTo(width, height); context.lineTo(40, height); context.closePath(); context.fillShape(this); }, fill: '#8dbdff', });
curveLayer.add(curve);
var ball = new Konva.Circle({ x: 190, y: 20, radius: radius, fill: 'blue', draggable: true, opacity: 0.8, });
ball.velocity = { x: 0, y: 0, };
ball.on('dragstart', function () { ball.velocity = { x: 0, y: 0, }; anim.start(); });
ball.on('mousedown', function () { anim.stop(); });
ball.on('mouseover', function () { document.body.style.cursor = 'pointer'; });
ball.on('mouseout', function () { document.body.style.cursor = 'default'; });
ballLayer.add(ball); stage.add(curveLayer); stage.add(ballLayer);
var tween = new Konva.Tween({ node: ball, fill: 'red', duration: 0.3, easing: Konva.Easings.EaseOut, });
anim = new Konva.Animation(function (frame) { updateBall(frame); }, ballLayer);
anim.start(); </script> </body> </html>
|