具有曲线检测功能的物理模拟器
说明:用鼠标光标抛掷小球。
import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; /* * 向量数学函数 */ 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, }; } /* * 该叉积函数已简化, * 由于向量 a 和 b 位于画布平面, * 因此 x 和 y 设为零 */ 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(); /* * 检查中心点周围的多个点, * 以确定法向量 */ 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; // 5% 能量损失 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; // 20% 能量损失 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 }); /* * 如果球与曲线接触,则沿 * 曲线表面法线方向反弹 */ var collision = handleCurveCollision(ball, curve); } } // 创建舞台 const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); // 创建曲线和球的独立图层 const curveLayer = new Konva.Layer(); const ballLayer = new Konva.Layer(); // 使用原始贝塞尔曲线创建曲线 const 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); // 创建原样式球 const ball = new Konva.Circle({ x: 190, y: 20, radius: 20, 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); // 添加原样式 tween 动画 const tween = new Konva.Tween({ node: ball, fill: 'red', duration: 0.3, easing: Konva.Easings.EaseOut, }); // 添加动画 const anim = new Konva.Animation(function (frame) { updateBall(frame); }, ballLayer); anim.start();