- 分离轴定理相交
- 斜边旋转法碰撞
- 投影求切向碰撞
- 竖直向上抛
- 球的假想目标
- 平抛
- 自由落体
- 作用力
- box2d的简单封装
var addExample = createModuleExample('vue', { deps: ['eventemitter3'] })
function createCanvas(container, ops) {
var { width, height, background } = ops
var canvas = document.createElement('canvas')
var drawbufferWidth = width
var drawbufferHeight = height
canvas.width = drawbufferWidth;
canvas.height = drawbufferHeight;
var ctx = canvas.getContext('2d')
container.appendChild(canvas)
let globalScale=1,invertGlobalScale=1/globalScale;
let drawObjects = [], collisionHandles = []
function add(obj) {
obj.viewWidth = width;
obj.viewHeight = height
drawObjects.push(obj)
}
function remove(obj) {
let index = drawObjects.indexOf(obj)
drawObjects.splice(index, 1)
}
function clear() {
if (background) {
ctx.fillStyle = background;
ctx.fillRect(0, 0, drawbufferWidth, drawbufferHeight)
}else{
ctx.clearRect(0, 0, drawbufferWidth, drawbufferHeight)
}
}
function addCollision(bodyA, bodyB, handle) {
collisionHandles.push({
bodyA: bodyA,
bodyB: bodyA,
handle: handle
})
}
let delta = 0;
function draw() {
if (paused) {
return
}
ctx.save()
clear();
events.emit('onDraw', delta)
// 更新位置
for (let i = 0, len = drawObjects.length; i < len; i++) {
drawObjects[i].updatePosition(delta)
}
// 更新位置
for (let i = 0, len = drawObjects.length; i < len; i++) {
drawObjects[i].update(delta)
}
// 更新碰撞
for (let i = 0, len = drawObjects.length; i < len; i++) {
//drawObjects[i].update(delta)
for (let j = i + 1; j < len; j++) {
drawObjects[i].collideWith(drawObjects[j])
}
}
// 更新速度
for (let i = 0, len = drawObjects.length; i < len; i++) {
drawObjects[i].updateVelocity(delta)
}
ctx.scale(globalScale,globalScale)
ctx.lineWidth=invertGlobalScale
// 更新渲染
drawObjects.forEach(obj => {
obj.beforeDraw(ctx);
ctx.save()
ctx.beginPath();
let style = Object.entries(obj.style)
let needFill = false, needStroke = false
for (let [name, value] of style) {
if (value === null) {
continue;
}
if (name === 'fillStyle' && value !== null && value !== 'none') {
needFill = true
} else if (name === 'strokeStyle' && value !== null && value !== 'none') {
needStroke = true
}
if(name==='lineWidth'){
value=value*invertGlobalScale
}
ctx[name] = value
}
ctx.translate(obj.position.x, obj.position.y);
ctx.rotate(obj.rotation);
ctx.scale(obj.scale.x, obj.scale.y);
obj.draw(ctx)
if (needFill) {
ctx.fill()
}
if (needStroke) {
ctx.stroke()
}
ctx.restore()
obj.afterDraw(ctx)
})
events.emit('onAfterDraw', delta)
ctx.restore();
}
let drawing = false, frameAnimation
let events = new EventEmitter3()
function doDraw() {
draw();
drawing = false;
}
let paused = false
function pause() {
paused = !paused;
}
function requestDraw() {
if (drawing) {
return
}
drawing = requestAnimationFrame(doDraw)
}
let lastTime = 0, currentTime;
function onFrame() {
currentTime = performance.now()
delta = currentTime - lastTime
lastTime = currentTime
draw();
frameAnimation = requestAnimationFrame(onFrame);
}
function start() {
lastTime = performance.now();
frameAnimation = requestAnimationFrame(onFrame);
}
function stop() {
if (frameAnimation) {
cancelAnimationFrame(frameAnimation)
frameAnimation = false
}
}
function getMouseOffset(e) {
var rect = canvas.getBoundingClientRect()
return [e.clientX - rect.left, e.clientY - rect.top]
}
function eachObject(callback){
drawObjects.forEach(callback)
}
return {
setScale:(v)=>{
globalScale=v;
invertGlobalScale=1/v;
},
each:eachObject,
getMouseOffset,
pause,
ctx,
addDraw: events.on.bind(events, 'onDraw'),
draw,
add,
requestDraw,
start,
stop,
get width() {
return width
},
get height() {
return height
},
on: events.on.bind(events),
off: events.off.bind(events)
};
}
class PhysicsObject2 {
constructor() {
// 初始速度为0
this.velocity = new Vector(0, 0);
// 初始角速度为0
this.angularVelocity = 0;
// 初始加速度为0
this.acceleration = new Vector(0, 0);
// 初始角加速度为0
this.angularAcceleration = 0;
// 初始质量为1
this.mass = 1;
// 计算质量倒数
this.invMass = 1 / this.mass;
// // 初始转动惯量为1
this.momentOfInertia = 1;
this.inertia = 1; // 转动惯量
this.invInertia = 1 / this.inertia; // 转动惯量倒数
// 初始力矩为0
this.torque = 0;
// 初始作用力为0
this.force = new Vector(0, 0);
// 初始摩擦力为0
this.friction = 0;
this.staticFriction = 0;// 静态摩擦力
// 初始重力为0
this.gravity = new Vector(0, 0);
// 初始恢复力为0
this.restitution = 1;
// 初始位置为(0, 0)
this.position = new Vector(0, 0);
// 初始角度为0
this.angle = 0;
}
// 应用作用力
applyForce(force, forcePoint = Vector.create(0, 0)) {
// 计算作用力矩
const torque = force.cross(forcePoint);
// 应用作用力和力矩
this.force = this.force.add(force);
this.torque += torque;
}
// 应用冲量
applyImpulse(impulse, impulsePoint = Vector.create(0, 0)) {
// 计算冲量产生的速度变化
this.velocity = this.velocity.add(impulse.scale(this.invMass));
// 计算冲量产生的角速度变化
const torque = impulse.cross(impulsePoint);
this.angularVelocity += torque / this.momentOfInertia;
}
// 应用力矩
applyTorque(torque) {
this.torque += torque;
}
// 应用角冲量
applyAngularImpulse(angularImpulse) {
// 计算角冲量产生的角速度变化
this.angularVelocity += angularImpulse / this.momentOfInertia;
}
eulerStep2(dt) {
const frictionForce = this.velocity.scale(-1).normalize().scale(this.friction * this.mass);
// console.log('frictionForce',frictionForce)
// 计算加速度
let force = this.force.add(this.gravity).add(frictionForce);
let acceleration = force.scale(this.invMass);
// 计算角加速度
let torque = this.torque;
let angularAcceleration = torque / this.momentOfInertia;
// 计算速度和角速度
this.velocity = this.velocity.add(acceleration.scale(dt)).scale(this.friction);
this.angularVelocity += angularAcceleration * dt;
// 计算位置和角度
this.position = this.position.add(this.velocity.scale(dt));
this.angle += this.angularVelocity * dt;
}
update(dt) {
this.eulerStep(dt);
}
//欧拉求解器
eulerStep(dt) {
//计算加速度
const frictionForce = this.velocity.scale(-1).normalize().scale((1 - this.friction) * this.mass);
const totalForce = this.force.add(this.gravity).add(frictionForce);
const acceleration = totalForce.scale(this.invMass);
const angularAcceleration = this.torque * this.invInertia;
//更新位置和速度
this.position = this.position.add(this.velocity.scale(dt));
this.velocity = this.velocity.add(acceleration.scale(dt));
this.angle += this.angularVelocity * dt;
this.angularVelocity += angularAcceleration * dt;
//重置作用力和力矩
this.force.set(0, 0);
this.torque = 0;
}
}
class DrawObject {
PI2 = Math.PI * 2
static globalScale=1
static convertCoord(x,y){
return Vector.create(x/DrawObject.globalScale,y/ DrawObject.globalScale)
}
constructor(properties = {}) {
this.invertGlobalScale=1/DrawObject.globalScale
this.position = Vector.create(0,0).copy(properties.position || {x:0,y:0})
this.scale = Vector.create(1, 1).copy(properties.scale || {x:1,y:1})
this.rotation = 0;
delete properties.position;
delete properties.scale;
Object.assign(this, properties)
this.body = new PhysicsObject2();
this.body.body = this;
this.style = Object.assign({
fillStyle: '#000',
strokeStyle: 'none'
}, this.getDefaultStyle(), properties.style || {})
}
getMatrix(){
return Matrix2D.fromTranslateRotationScale(this.position,this.rotation,this.scale)
}
getLocalCoord(position){
return this.getMatrix().invert().transformVector(position)
}
setPosition(x,y){
this.position.set(x*this.invertGlobalScale,y*this.invertGlobalScale)
}
get angle(){
return this.rotation/Math.PI*180
}
set angle(value){
this.rotation=value/180*Math.PI
}
syncPosition() {
this.body.position.copy(this.position)
this.body.angle = this.rotation
}
syncPhysicsPosition() {
this.position.copy(this.body.position)
this.rotation = this.body.angle
}
getDefaultStyle() {
return {}
}
updatePosition() {
}
updateVelocity() {
}
update(dt) {
}
collideWith(other) {
}
beforeDraw() {
}
draw(ctx) {
}
afterDraw() {
}
}
class Line extends DrawObject {
constructor(p) {
super(p)
this.x = p.x*this.invertGlobalScale;
this.y = p.y*this.invertGlobalScale;
this.x2 = p.x2*this.invertGlobalScale;
this.y2 = p.y2*this.invertGlobalScale;
}
getDefaultStyle() {
return {
fillStyle: 'none',
strokeStyle: '#000',
lineWidth: 1
}
}
/**
* @param {CanvasRenderingContext2D} ctx
*/
draw(ctx) {
ctx.moveTo(this.x, this.y)
ctx.lineTo(this.x2, this.y2)
}
}
class Ball extends DrawObject {
constructor(p) {
super(p)
this.r = p.r*this.invertGlobalScale;
}
draw(ctx) {
ctx.arc(0, 0, this.r, 0, Math.PI * 2, false)
}
}
const LineObject=Line
class CircleObject extends DrawObject {
constructor(p) {
super(p)
this.r = p.r*this.invertGlobalScale;
}
draw(ctx) {
ctx.arc(0, 0, this.r, 0, Math.PI * 2, false)
}
hitTest(x,y){
let p= this.getLocalCoord(DrawObject.convertCoord(x,y))
// return Vector.create(x,y).subtract()
return p.length()<=this.r;
}
}
class PolygonObject extends DrawObject {
constructor(p) {
super(p)
this.setVertices(p.vertices)
}
getPolygonCenter(vertices) {
const center = new Vector(0, 0);
const vertexCount = vertices.length;
for (let i = 0; i < vertexCount; i++) {
center.x += vertices[i].x;
center.y += vertices[i].y;
}
center.x /= vertexCount;
center.y /= vertexCount;
return center;
}
projectCircleOntoAxis(position, radius, axis) {
const dotProduct = position.dot(axis) / axis.length();
return {
min: dotProduct - radius,
max: dotProduct + radius
};
}
/**
* 投影多边形到指定轴上的方法
* @param {Array} vertices 多边形的顶点数组,每个元素为一个Vector对象
* @param {Vector} axis 指定的轴,为一个Vector对象
* @returns {Object} 投影后的最小值和最大值,格式为 { min: 最小值, max: 最大值 }
*/
projectPolygonOntoAxis(vertices, axis) {
const numVertices = vertices.length;
let min = null;
let max = null;
for (let i = 1; i < numVertices; i++) {
const dot = vertices[i].dot(axis) / axis.length();
if (min === null || dot < min) {
min = dot;
}
if (max === null || dot > max) {
max = dot;
}
}
return { min, max };
}
getOverlap(circleProjection, polygonProjection) {
// 如果两个形状的投影区间没有重叠,则说明它们在该法向量上没有相交
if (circleProjection.max < polygonProjection.min || polygonProjection.max < circleProjection.min) {
return 0;
}
// 否则返回它们在该法向量上投影区间的重叠长度
return Math.min(circleProjection.max, polygonProjection.max) - Math.max(circleProjection.min, polygonProjection.min);
}
distanceFromPointToSegment(point, segmentStart, segmentEnd) {
// 计算线段的向量和点到线段起点的向量
const segmentVector = segmentEnd.sub(segmentStart);
const pointVector = point.sub(segmentStart);
// 计算点到线段的投影向量
const projectionLength = pointVector.dot(segmentVector) / segmentVector.dot(segmentVector);
const projectionVector = segmentVector.scale(projectionLength);
// 如果投影向量在线段上,则点到线段的距离为投影向量到点的向量的长度
if (projectionLength >= 0 && projectionLength <= 1) {
const distanceVector = pointVector.sub(projectionVector);
return distanceVector.length();
}
// 如果投影向量不在线段上,则点到线段的距离为点到线段两个端点的距离的最小值
const distanceToStart = pointVector.length();
const distanceToEnd = point.sub(segmentEnd).length();
return Math.min(distanceToStart, distanceToEnd);
}
circlePolygonCollision2(circle) {
let polygon = this;
const vertices = polygon.body.vertices;
// 检查圆形是否与多边形的边界相交
let shortestDistance = Infinity;
let collisionNormal = new Vector(0, 0);
for (let i = 0; i < vertices.length; i++) {
let currentPoint = vertices[i];
let nextPoint = vertices[(i + 1) % vertices.length];
let distanceToLine = this.distanceFromPointToSegment(circle.position, currentPoint, nextPoint);
if (distanceToLine <= circle.body.r) {
let normal = line.normal();
let distance = circle.center.distanceTo(line.midpoint());
if (distance < shortestDistance) {
shortestDistance = distance;
collisionNormal = normal;
}
}
}
// 检查圆形是否在多边形内部
let inside = false;
let j = vertices.length - 1;
for (let i = 0; i < vertices.length; i++) {
let xi = vertices[i].x;
let yi = vertices[i].y;
let xj = vertices[j].x;
let yj = vertices[j].y;
if (((yi > circle.position.y) != (yj > circle.position.y)) &&
(circle.position.x < (xj - xi) * (circle.position.y - yi) / (yj - yi) + xi)) {
inside = !inside;
}
j = i;
}
// 如果圆形在多边形内部,或者与多边形相交,则进行碰撞反应
if (inside || shortestDistance < Infinity) {
// 计算反弹向量
let velocity = circle.velocity;
let speed = velocity.length();
let normal = collisionNormal.normalize();
let reflection = velocity.reflect(normal).normalize().scale(speed);
// 移动圆形到反弹位置
let displacement = collisionNormal.scale(circle.radius - shortestDistance);
circle.center = circle.center.add(displacement);
// 更新圆形速度
circle.velocity.copy(reflection);
}
}
circlePolygonSTACollision(circle) {
const polygon = this.body;
// 计算圆心与多边形中心之间的距离
const dx = circle.position.x - polygon.position.x;
const dy = circle.position.y - polygon.position.y;
let vertices = polygon.body.vertices;
// 循环遍历多边形的每一条边
for (let i = 0; i < vertices.length; i++) {
// 获取当前边和它的法向量
const currentVertex = vertices[i];
const nextVertex = vertices[(i + 1) % vertices.length];
const edge = {
x: nextVertex.x - currentVertex.x,
y: nextVertex.y - currentVertex.y,
};
const edgeNormal = { x: edge.y, y: -edge.x };
const edgeLength = Math.sqrt(edge.x * edge.x + edge.y * edge.y);
// 将圆心投影到边的法向量上
const projection = (dx * edgeNormal.x + dy * edgeNormal.y) / edgeLength;
// 检查圆是否在这条轴上与多边形相交
if (projection > circle.body.r) {
// 圆完全在这条轴的外面,没有碰撞
return false;
}
}
// 所有的轴检查都通过了,圆与多边形相交
return true;
}
getProjectionOntoAxis(axis, vertices) {
let min = axis.dot(vertices[0]);
let max = min;
for (let i = 1; i < vertices.length; i++) {
const projection = axis.dot(vertices[i]);
if (projection < min) {
min = projection;
} else if (projection > max) {
max = projection;
}
}
return { min, max }
}
drawLine(color, v1, v2) {
let ctx = this.ctx;
if (!ctx) {
return
}
ctx.save()
ctx.beginPath();
ctx.strokeStyle = color;
ctx.moveTo(v1.x, v1.y);
ctx.lineTo(v2.x, v2.y);
ctx.stroke()
ctx.restore()
}
// 计算多边形的质心
computeCenter() {
let sumX = 0;
let sumY = 0;
for (let i = 0; i < this.vertices.length; i++) {
sumX += this.vertices[i].x;
sumY += this.vertices[i].y;
}
this.center = { x: sumX / this.vertices.length, y: sumY / this.vertices.length };
}
getNearestPoint(worldVertices, position) {
let min = Infinity, current = 0;
for (let i = 0; i < worldVertices.length; i++) {
let vertex = worldVertices[i];
let dist = position.sub(vertex);
if (dist < min) {
min = dist;
current = i;
}
}
return worldVertices[current]
}
circlePolygonCollision(circle) {
let polygon = this.body;
let worldVertices = polygon.body.transformVertices(polygon.position)
const vertices = polygon.body.vertices
const numVertices = vertices.length;
const axises = [];// 分离轴
const projectAxes = []; // 投影轴
let centerPos = circle.position.clone()
// let p1=this.getNearestPoint(worldVertices,circle.position);
// axises.push(p1.sub(circle.position));
// 求圆心到多边形每个顶点的向量,并将它们归一化作为投影轴
for (let i = 0; i < numVertices; i++) {
const vertex = vertices[i];
const axis = vertex.sub(circle.position);
axises.push(axis.normal().normalize())
// axises.push(axis.rotateTo(Math.PI / 2));
// this.drawLine('red', circle.position, axis.normal())
}
// const circleAxes = [new Vector(1, 0), new Vector(0, 1)]; // 圆形的投影轴是水平和垂直方向
// axes.push(circleAxes[0],circleAxes[1])
// // 求圆心到多边形每条边的向量,并将它们顺时针旋转90度作为投影轴
for (let i = 0; i < numVertices; i++) {
const vertexA = vertices[i];
const vertexB = vertices[(i + 1) % numVertices];
const edge = vertexB.sub(vertexA);
axises.push(edge.normal().normalize())
//const axis = edge.rotateTo(Math.PI / 2).normalize(); // 把边向量顺时针旋转90度得到法向量
//axises.push(axis);
// this.drawLine('green',circle.position,axis)
}
let smallestOverlap = Number.POSITIVE_INFINITY;
let smallestAxis = null;
// 对于每个投影轴,计算圆形和多边形在该轴上的投影区间
for (let i = 0; i < axises.length; i++) {
const axis = axises[i];
const circleProjection = this.projectCircleOntoAxis(circle.position, circle.body.r, axis);
const polygonProjection = this.projectPolygonOntoAxis(worldVertices, axis);
const overlap = this.getOverlap(circleProjection, polygonProjection);
// 如果有任意一个轴上的投影区间没有重叠,则说明两个形状没有碰撞
if (overlap === 0) {
return false;
}
// 记录重叠最小的轴和重叠量
if (overlap < smallestOverlap) {
smallestOverlap = overlap;
smallestAxis = axis;
}
}
// 计算碰撞点和碰撞法向量
let normal = smallestAxis;
if (circle.position.sub(polygon.position).dot(normal) > 0) {
normal = normal.negate();
}
// 计算相对速度
let relativeVelocity = {
x: circle.velocity.x - polygon.velocity.x,
y: circle.velocity.y - polygon.velocity.y
};
let relativeVelocityAlongNormal = normal.x * relativeVelocity.x + normal.y * relativeVelocity.y;
// 判断是否需要处理碰撞
if (relativeVelocityAlongNormal > 0) {
return;
}
// 计算碰撞响应
let e = Math.min(circle.restitution, polygon.restitution);
let f = Math.sqrt(circle.staticFriction * polygon.staticFriction);
// 计算冲量
let j = -(1 + e) * relativeVelocityAlongNormal;
j /= 1 / circle.mass + 1 / polygon.mass;
let impulse = { x: j * normal.x, y: j * normal.y };
// 更新速度和位置
circle.velocity.x += impulse.x / circle.mass;
circle.velocity.y += impulse.y / circle.mass;
// 计算切向量
let tangent = { x: -normal.y, y: normal.x };
let relativeVelocityAlongTangent = tangent.x * relativeVelocity.x + tangent.y * relativeVelocity.y;
let jt = -relativeVelocityAlongTangent;
jt /= 1 / circle.mass + 1 / polygon.mass;
// 计算摩擦力
let frictionImpulse;
if (Math.abs(jt) < j * f) {
frictionImpulse = { x: jt * tangent.x, y: jt * tangent.y };
} else {
let mu = Math.min(circle.staticFriction, polygon.staticFriction);
frictionImpulse = { x: -j * mu * tangent.x, y: -j * mu * tangent.y };
}
// 更新速度和位置
circle.velocity.x += frictionImpulse.x / circle.mass;
circle.velocity.y += frictionImpulse.y / circle.mass;
polygon.velocity.x += frictionImpulse.x / polygon.mass;
polygon.velocity.y += frictionImpulse.y / polygon.mass;
// const collisionPoint = circle.position.sub(normal.scale(circle.body.r - smallestOverlap / 2));
// // 计算碰撞后圆和多边形的速度和角速度变化
// const relativeVelocity = circle.velocity.sub(polygon.velocity.scale(polygon.invMass));
// const normalVelocity = relativeVelocity.dot(normal);
// const tangentVelocity = relativeVelocity.sub(normal.scale(normalVelocity));// 切线速度
// const friction = Math.sqrt(circle.friction * polygon.friction);
// const frictionForceMagnitude = tangentVelocity.length() * circle.mass * friction;
// const frictionForce = tangentVelocity.normalize().scale(-frictionForceMagnitude);
// const normalForceMagnitude = normalVelocity > 0 ? 0 : -normalVelocity * circle.mass * circle.restitution;
// const normalForce = normal.scale(normalForceMagnitude);
// circle.applyForce(frictionForce.add(normalForce));
// polygon.applyForce(normalForce.scale(-1));
// console.log('ffffffffffffff',collisionPoint, smallestOverlap, smallestAxis,frictionForce.add(normalForce))
// circle.applyTorque(collisionPoint.sub(circle.position).cross(normalForce));
// polygon.applyTorque(collisionPoint.sub(polygon.position).cross(normalForce.negate()));
}
transformVertices(position) {
this.t_vertices = new Array(this.vertices.length);
this.vertices.forEach((v, i) => {
this.t_vertices[i] = v.add(position)
});
return this.t_vertices;
}
setVertices(vertices = []) {
this.vertices = vertices.map(d=>{
return d.scale(this.invertGlobalScale);
});
this.edges = this.getEdges();
this.center = this.getPolygonCenter(vertices)
}
setBox(halfW, halfH) {
this.width = halfW * 2;
this.height = halfH * 2;
this.setVertices([
Vector.create(-halfW, halfH),
Vector.create(-halfW, -halfH),
Vector.create(halfW, -halfH),
Vector.create(halfW, halfH),
])
}
// 计算多边形的边向量数组
getEdges() {
const edges = [];
for (let i = 0; i < this.vertices.length; i++) {
const j = (i + 1) % this.vertices.length;
edges.push(this.vertices[j].sub(this.vertices[i]));
}
return edges;
}
// 计算多边形在给定分离轴上的投影
project(axis) {
let min = Infinity, max = -Infinity;
for (let i = 0; i < this.vertices.length; i++) {
const projection = this.vertices[i].dot(axis);
min = Math.min(min, projection);
max = Math.max(max, projection);
}
return { min, max };
}
// 获取多边形的所有边向量作为分离轴
getAxes() {
const axes = [];
for (let i = 0; i < this.edges.length; i++) {
const normal = this.edges[i].perpendicular();
axes.push(normal.normalize());
}
return axes;
}
draw(ctx) {
this.ctx = ctx;
let vertices = this.vertices;
for (let i = 0; i < vertices.length; i++) {
let v = vertices[i % vertices.length]
if (i === 0) {
ctx.moveTo(v.x, v.y)
} else {
ctx.lineTo(v.x, v.y)
}
}
ctx.closePath()
}
}
addExample("物理运动公式",function(){
return {
template:`<div><div ref="main">
</div></div>`,
data(){ return {};},
computed:{},
methods:{},
mounted(){
var container=this.$refs.main;
katex.render('$K = \frac{1}{2}mv^2$',container,{
throwOnError: false
})
}
}
})
addExample("根据给定时间,匀速", function () {
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
var { add, start, addDraw } = createCanvas(container, {
width: 500,
height: 500,
background: '#ddd'
})
var ball = new Ball({ r: 20, position: { x: 250, y: 50 } })
add(ball);
/**
* 速度与时间关系:V=V0+a*t
* 位移与时间关系: X=V0*t+0.5*a*t
* 位移与速度关系:Math.pow(V,2)-Math.pow(V0,2)=2*a*x
*/
// 重力
let gravity = 9.8;
let mass = 1;// 质量
let friction = 0;// 摩擦力
let ax = 0, ay = 0;// 加速
let forceX = 0, forceY = 0;// 作用力
let vx = 0, vy = 0;// 速度
addDraw((delta) => {
delta = delta * 0.001
// forceY=mass*gravity;
// ay=forceY/mass;
vy = vy + (gravity + forceY / mass) * delta;
ball.position.y += vy;
console.log('a', ay)
})
start()
}
}
})
addExample("物理", function () {
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
var { add, start, addDraw, pause } = createCanvas(container, {
width: 500,
height: 500,
background: '#ddd'
})
var ball = new Ball({ r: 20, position: { x: 250, y: 50 } })
ball.body.gravity.set(0, 980);
ball.syncPosition()
var ground = new PolygonObject({ position: { x: 250, y: 490 } })
ground.setBox(200, 10)
ground.syncPosition()
let i = 0
add(ball);
add(ground);
document.addEventListener('mousedown', () => {
pause()
})
addDraw((delta) => {
delta = delta * 0.001
ball.body.update(delta);
ground.circlePolygonCollision(ball.body)
ball.syncPhysicsPosition();
// if(ground.circlePolygonSTACollision(ball.body)){
// console.log('碰撞')
// }else{
// ball.body.update(delta);
// }
})
start()
}
}
})
addExample("自由落体", function () {
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
var { add, start, addDraw } = createCanvas(container, {
width: 500,
height: 500,
background: '#ddd'
})
var ball = new Ball({ r: 20, position: { x: 250, y: 50 } })
add(ball);
let gravity = 98;
let initialY = ball.position.y;
let vy = 0;
let h = 0
let time = 0;
let y = 0; // 物体的初始位置
let v = 0; // 物体的初始速度
addDraw((delta) => {
time += delta * 0.001;
let dt = delta * 0.001;
vy = 0.5 * gravity * time * time;
// ball.position.y = vy;
h = gravity * time * time / 2
// 计算物体的新位置
const deltaY = v * dt + 0.5 * gravity * dt * dt;
y += deltaY;
v += gravity * dt;
// 循环调用绘制函数
if (initialY + y <= 500 - 20) {
console.log('落下高度', y, h)
ball.position.y = initialY + y;
}
})
start()
}
}
})
addExample("竖直上抛", function () {
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
var { add, start, addDraw,ctx} = createCanvas(container, {
width: 500,
height: 500,
background: '#ddd'
})
var ball = new Ball({ r: 20, position: { x: 250, y: 450 } })
add(ball);
let initialHeight = ball.position.y;
let y = 0; // 当前高度
let gravity = 9.8 * 100;
let maxHeight = 200;// 上升高度
let v0 = Math.sqrt(2 * gravity * maxHeight); // 球的初始速度
let t = 0; // 时间
const fps = 60; // 动画帧率
const intervalTime = 1000 / fps; // 每帧间隔时间
let time = 0;
let v2 = v0;// 当前速度
addDraw((delta) => {
// 转换成秒为单位
time += delta * 0.001
let dt = delta * 0.001 // 使用时间间隔
let v = v0 - gravity * time;// 速度
// 计算当前高度
//const h = y - (0.5 * gravity * time * time);
const h = (v0 * time) - (0.5 * gravity * time * time);
//-----
// v2 = v2 - gravity * dt;// 速度
const h2 = (v2 * dt) - (0.5 * gravity * dt * dt);
y += h2;
v2 = v2 - gravity * dt
if (h >= 0) {
ball.position.y = initialHeight - h;
console.log('h', h, 'h2', y)
}
})
ctx.canvas.addEventListener('mousedown',()=>{
time=0
})
start()
}
}
});
addExample("斜抛运动", function () {
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
var { add, start, addDraw, getMouseOffset, draw } = createCanvas(container, {
width: 500,
height: 500,
background: '#ddd'
})
var ball = new Ball({ style: { fillStyle: 'blue' }, r: 20, position: { x: 100, y: 400 } })
add(ball);
var line = new Line({ x: 0, y: 0, x2: 100, y2: 0, position: { x: 100, y: 400 } })
add(line)
let initialPosition = ball.position.clone()
let speed = 200;// 初始速度
let vx = 0;
let vy = 0;
let ax = 0;
let ay = 0;
let gravity = 98;
let theta = 0;
let x = 0;
let y = 0;
let isReady = false
document.addEventListener('pointerdown', (e) => {
let [x2, y2] = getMouseOffset(e);
if (e.button == 0) {
isReady = true;
speed = Vector.create(x2, y2).subtract(Vector.create(100, 400)).length()
// 分解速度
vx = speed * Math.cos(theta)
vy = -speed * Math.sin(theta);// 转vy为正数.
x = 0;
y = 0;
console.log('speed', speed, vx, vy)
}
if (e.button == 2) {
isReady = false
ball.position.set(100, 400);
line.position.set(100, 400);
console.log('ff');
e.preventDefault()
draw()
}
})
document.addEventListener('contextmenu', (e) => {
e.preventDefault()
})
document.addEventListener('pointermove', (e) => {
let [x, y] = getMouseOffset(e);
let mouse = Vector.create(x, y)
let angle = mouse.sub(line.position).angle();
let m = Vector.create(line.x2, line.y2);
m = m.rotate(angle)
line.x2 = m.x;
line.y2 = m.y;
if (!isReady) {
theta = angle;
}
})
addDraw((delta) => {
if (!isReady) {
return;
}
let dt = delta * 0.001;
x = x + vx * dt;
let h = vy * dt - 0.5 * gravity * dt * dt;
vy = vy - gravity * dt;
y += h;
if (y >= 0) {
ball.position.x = initialPosition.x + x;
ball.position.y = initialPosition.y - y;
console.log('y', y, initialPosition.y - y)
}
})
start()
}
}
});
addExample("作用力", function () {
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
var { add, start, addDraw, getMouseOffset, draw } = createCanvas(container, {
width: 500,
height: 500,
background: '#ddd'
})
var ball = new Ball({ style: { fillStyle: 'blue' }, r: 20, position: { x: 100, y: 400 } })
add(ball);
let vx = 0;
let vy = 0;
let mass = 1;
let inv_mass = 1 / mass;
let gravity = { x: 0, y: 980 };
let forceX = 0;
let forceY = 0;
let damping = 1;
function applyImpulse(x, y) {
vx += x * inv_mass
vy += y * inv_mass
}
function applyForce(x, y) {
forceX += x;
forceY += y;
// vx=forceX*inv_mass
// vy=forceY*inv_mass
}
let isReady = false
document.addEventListener('pointerdown', (e) => {
let [x2, y2] = getMouseOffset(e);
let speed = Vector.create(x2, y2).subtract(Vector.create(0, 500))
if (e.button == 0) {
applyForce(speed.x * 10, speed.y * 10)
} else if (e.button === 2) {
applyImpulse(speed.x * 2, speed.y * 2)
}
ball.active = true;
})
document.addEventListener('contextmenu', (e) => {
e.preventDefault()
})
ball.active = true;
addDraw((delta) => {
let dt = delta * 0.001;
if (ball.active) {
vx = vx * damping + (gravity.x + forceX * inv_mass) * dt;
vy = vy * damping + (gravity.y + forceY * inv_mass) * dt;
ball.position.x += vx * dt;
ball.position.y += vy * dt;
if (ball.position.y > 470) {
ball.active = false;
vx = 0;
vy = 0;
forceX = 0
forceY = 0
}
}
})
start()
}
}
});
addExample("示例", function () {
return {
template: `<div><canvas ref="main" style="background:#ddd" width="500" height="500"></canvas></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var canvas = this.$refs.main;
// 初始化变量
const ctx = canvas.getContext('2d');
const ball = { x: 50, y: 50, vx: 0, vy: 0, radius: 20, mass: 1 };
const gravity = 9.8;
const friction = 0.95;
const restitution = 0.8;
const damping = 0.9;
let mouseX = 0, mouseY = 0, mouseDown = false;
// 鼠标事件
canvas.addEventListener('mousemove', e => {
mouseX = e.clientX;
mouseY = e.clientY;
});
canvas.addEventListener('mousedown', e => {
mouseDown = true;
});
canvas.addEventListener('mouseup', e => {
mouseDown = false;
});
// 动画循环
function loop() {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 计算作用力
let fx = 0, fy = 0;
if (mouseDown) {
fx = mouseX - ball.x;
fy = mouseY - ball.y;
}
// 计算加速度
const ax = fx / ball.mass;
const ay = fy / ball.mass + gravity;
// 计算速度和位置
ball.vx = ball.vx * friction + ax;
ball.vy = ball.vy * friction + ay;
ball.x += ball.vx;
ball.y += ball.vy;
// 检测碰撞
if (ball.x < ball.radius) {
ball.vx *= -restitution;
ball.x = ball.radius;
} else if (ball.x > canvas.width - ball.radius) {
ball.vx *= -restitution;
ball.x = canvas.width - ball.radius;
}
if (ball.y < ball.radius) {
ball.vy *= -restitution;
ball.y = ball.radius;
} else if (ball.y > canvas.height - ball.radius) {
ball.vy *= -restitution;
ball.y = canvas.height - ball.radius;
}
// 应用阻尼力
ball.vx *= damping;
ball.vy *= damping;
// 绘制小球
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = '#FF0000';
ctx.fill();
// 循环动画
requestAnimationFrame(loop);
}
// 启动动画
loop();
}
}
})
addExample("物理基本", function () {
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
}
}
})
addExample("物理基本引擎", function () {
class PhysicsWorld {
constructor(gravity) {
this.gravity = gravity;
this.objects = [];
}
addObject(object) {
this.objects.push(object);
}
removeObject(object) {
const index = this.objects.indexOf(object);
if (index !== -1) {
this.objects.splice(index, 1);
}
}
update(timeStep) {
let dt = timeStep * 0.001;
for (const object of this.objects) {
// TODO: update object position and velocity based on forces and collisions
object.update(dt);
}
}
}
class PhysicsObject {
constructor(x, y, vx = 0, vy = 0, ax = 0, ay = 0, mass = 1, radius = 10) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.ax = ax;
this.ay = ay;
this.mass = mass;
this.radius = radius;
}
// 更新物体的位置和速度
updatePositionAndVelocity(dt) {
this.vx += this.ax * dt;
this.vy += this.ay * dt;
this.x += this.vx * dt;
this.y += this.vy * dt;
}
// 计算物体的动能
getKineticEnergy() {
return 0.5 * this.mass * (this.vx * this.vx + this.vy * this.vy);
}
// 撞击另一个物体
collideWith(otherObject) {
const dx = otherObject.x - this.x;
const dy = otherObject.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const overlap = (this.radius + otherObject.radius) - distance;
if (overlap > 0) {
const totalMass = this.mass + otherObject.mass;
const ratio1 = otherObject.mass / totalMass;
const ratio2 = this.mass / totalMass;
this.x -= overlap * (dx / distance) * ratio2;
this.y -= overlap * (dy / distance) * ratio2;
otherObject.x += overlap * (dx / distance) * ratio1;
otherObject.y += overlap * (dy / distance) * ratio1;
const dvx = otherObject.vx - this.vx;
const dvy = otherObject.vy - this.vy;
const dotProduct = dx * dvx + dy * dvy;
if (dotProduct > 0) {
const impulse = (2 * dotProduct) / (distance * totalMass);
this.vx += impulse * ratio2 * dx;
this.vy += impulse * ratio2 * dy;
otherObject.vx -= impulse * ratio1 * dx;
otherObject.vy -= impulse * ratio1 * dy;
}
}
}
/**
* 分离轴定理
* 检测两个物体是否碰撞
* @param {Object} obj1 物体1,包含 x,y,width,height,points 等属性
* @param {Object} obj2 物体2,包含 x,y,width,height,points 等属性
* @return {boolean} 返回 true 表示两个物体发生了碰撞,返回 false 表示两个物体没有碰撞
*/
checkCollision2(obj1, obj2) {
// 计算两个物体的中心点坐标
const cx1 = obj1.x + obj1.width / 2;
const cy1 = obj1.y + obj1.height / 2;
const cx2 = obj2.x + obj2.width / 2;
const cy2 = obj2.y + obj2.height / 2;
// 计算两个物体中心点的距离
const dx = Math.abs(cx2 - cx1);
const dy = Math.abs(cy2 - cy1);
// 计算两个物体的宽度和高度的一半之和
const hw1 = obj1.width / 2;
const hh1 = obj1.height / 2;
const hw2 = obj2.width / 2;
const hh2 = obj2.height / 2;
// 判断是否发生碰撞
if (dx <= hw1 + hw2 && dy <= hh1 + hh2) {
// 物体发生了碰撞,进行进一步检测
const axes = [...obj1.axes, ...obj2.axes];
for (const axis of axes) {
// 计算两个物体在当前轴上的投影
const p1 = obj1.project(axis);
const p2 = obj2.project(axis);
// 判断是否重叠
if (!p1.overlap(p2)) {
// 物体没有重叠,直接返回 false
return false;
}
}
// 所有轴上都重叠,返回 true
return true;
}
// 物体没有发生碰撞,直接返回 false
return false;
}
}
class Polygon {
constructor(points) {
this.points = points;// 一个数组,表示多边形的所有顶点
this.edges = this.getEdges();// 一个数组,表示多边形的所有边
}
getEdges() {
const edges = [];
for (let i = 0; i < this.points.length; i++) {
const p1 = this.points[i];
const p2 = i === this.points.length - 1 ? this.points[0] : this.points[i + 1];
const edge = new Vector(p2.x - p1.x, p2.y - p1.y);
edges.push(edge);
}
return edges;
}
/**
* 将多边形投影到指定轴上,并返回一个包含最小值和最大值的对象
*/
project(axis) {
let min = this.points[0].dot(axis);
let max = min;
for (let i = 1; i < this.points.length; i++) {
const projection = this.points[i].dot(axis);
if (projection < min) {
min = projection;
} else if (projection > max) {
max = projection;
}
}
return { min, max };
}
/**
* 对于每条边,计算它的垂直向量,并将其加入到一个数组中
返回数组中所有向量的单位向量
*/
getAxes() {
const axes = [];
for (const edge of this.edges) {
const axis = new Vector(-edge.y, edge.x).normalize();
axes.push(axis);
}
return axes;
}
/**
* 对于多边形 A 的所有分离轴,分别在 A 和多边形 B 上计算它们在该轴上的投影,并检查它们是否重叠
如果有任意一条分离轴上的投影没有重叠,说明两个多边形不相交,返回 false
如果所有分离轴上的投影都重叠,说明两个多边形相交,返回 true
*/
overlap(other) {
let axes = this.getAxes().concat(other.getAxes());
for (let axis of axes) {
let projection1 = this.project(axis);
let projection2 = other.project(axis);
if (projection1.max < projection2.min || projection2.max < projection1.min) {
return false;
}
}
return true;
}
}
// 定义一个凹多边形的点集,每个点是一个数组,分别表示x和y坐标
// const concavePolygon = [[100, 100], [200, 150], [150, 250], [50, 200], [70, 150]];
// 定义一个函数,将凹多边形转为凸多边形
function concaveToConvex(points) {
// 首先,我们需要找到一个凹角,这里选择第一个点作为起点
const start = points[0];
let minAngle = Infinity; // 初始化最小角度为无穷大
let splitIndex = 0; // 定义切分点的索引
for (let i = 1; i < points.length - 1; i++) {
// 计算当前点、前一个点和后一个点组成的角度
const angle = getAngle(points[i - 1], points[i], points[i + 1]);
if (angle < minAngle) {
// 如果角度比当前最小角度还要小,就将其设为新的最小角度
minAngle = angle;
splitIndex = i;
}
}
// 切分凹多边形,将其分为两个凸多边形
const polygon1 = [start];
const polygon2 = [start];
let currentPolygon = polygon1;
let nextPolygon = polygon2;
for (let i = 1; i < points.length; i++) {
if (i === splitIndex || i === splitIndex + 1) {
// 将切分点添加到两个多边形中
polygon1.push(points[i]);
polygon2.push(points[i]);
if (i === splitIndex + 1) {
// 如果已经到达第二个切分点,就将下一个点添加到另一个多边形中
currentPolygon = polygon2;
nextPolygon = polygon1;
}
} else {
// 如果不是切分点,就根据当前多边形的情况添加到相应的多边形中
if (isInside(points[i], currentPolygon)) {
currentPolygon.push(points[i]);
} else {
nextPolygon.push(points[i]);
}
}
}
// 对两个凸多边形分别进行逆时针排序
polygon1.sort(sortPoints);
polygon2.sort(sortPoints);
// 返回切分后的两个凸多边形
return [polygon1, polygon2];
}
// 定义一个函数,用来计算三个点组成的夹角
function getAngle(p1, p2, p3) {
const v1 = [p1[0] - p2[0], p1[1] - p2[1]];
const v2 = [p3[0] - p2[0], p3[1] - p2[1]];
const dotProduct = v1[0] * v2[0] + v1[1] * v2[1];
const cosValue = dotProduct / Math.sqrt(Math.pow(v1))
}
class Body {
constructor() {
this.position = Vector.create(); // 位置
this.velocity = Vector.create(); // 速度
this.force = Vector.create(); // 作用力
this.mass = 1; // 质量
this.radius = 0; // 半径
this.friction = 0.8; // 摩擦力系数
this.torque = 0; // 扭矩
this.momentOfInertia = (1 / 2) * mass * radius * radius; // 惯量
this.angularVelocity = 0; // 角速度
this.angularAcceleration = 0; // 角加速度
this.rotation = 0; // 旋转角度
}
// 应用力
applyForce(force) {
this.force.add(force);
}
// 应用冲量
applyImpulse(impulse, contact) {
this.velocity = this.velocity.add(impulse.scale(1 / this.mass));
this.angularVelocity += this.momentOfInertia * Vector.cross(contact.subtract(this.position), impulse);
}
// 更新状态
update(dt) {
// 计算速度和位置
new VelocitySolver(this).solve(dt);
new PositionSolver(this).solve(dt);
}
}
class VelocitySolver {
constructor(body) {
this.body = body;
}
solve(dt) {
// 计算加速度
let force = this.body.force;
let acceleration = force.scale(1 / this.body.mass);
// 计算速度
this.body.velocity.add(acceleration.scale(dt));
}
}
class PositionSolver {
constructor(body) {
this.body = body;
}
solve(dt) {
// 计算位置
this.body.position.add(this.body.velocity.scale(dt));
}
}
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
}
}
});
addExample("分离轴定法", function () {
return {
template: `<div><canvas ref="canvas"></canvas></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var canvas = this.$refs.canvas;
var ctx = canvas.getContext('2d');
canvas.width = 800
canvas.height = 500
let halfW = 100;
let halfH = 100;
class Circle {
constructor(r) {
this.radius = r;
this.controlRadius = Math.max(5, r * 0.1)
this.position = Vector.create()
}
project(axis) {
var pro = this.position.dot(axis) / axis.length();
return { min: pro - this.radius, max: pro + this.radius };
}
testPoint(p) {
return p.sub(this.position).lengthSquared() <= this.radius * this.radius
}
testControlPoint(p) {
return p.sub(this.position).lengthSquared() <= this.controlRadius * this.controlRadius
}
draw(ctx, color) {
ctx.save()
ctx.translate(this.position.x, this.position.y)
ctx.beginPath()
ctx.strokeStyle = color
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.stroke()
// 控制点
ctx.beginPath()
ctx.fillStyle = 'blue'
ctx.arc(0, 0, this.controlRadius, 0, Math.PI * 2);
ctx.fill()
ctx.restore()
}
}
class Polygon {
constructor(radius, sideCount, offset) {
this.vertices = []
this.vertices = [];
this.position = Vector.create()
this.invMass = 1 / 10; // 质量的倒数
this.friction = 0.2; // 摩擦系数
this.restitution = 0.5; // 弹性系数
this.other = null
this.radius = radius;
this.controlRadius = Math.max(5, radius * 0.1)
this.createShape(radius, sideCount, offset)
}
testPoint(p) {
return p.sub(this.position).lengthSquared() < this.radius * this.radius
}
testControlPoint(p) {
return p.sub(this.position).lengthSquared() < this.controlRadius * this.controlRadius
}
createShape(radius = 50, edges = 3, offset = Vector.create()) {
let pi2 = Math.PI * 2;
let p = Math.PI * 2 / edges;
let p2 = 0;
for (let i = 0; i < edges; i++) {
this.vertices.push(new Vector(
radius * Math.cos(p2) + offset.x,
radius * Math.sin(p2) + offset.y,
))
p2 += p
// p2+=p+p 五角星
}
}
// 计算多边形的所有分离轴
getSeparatingAxes() {
const axes = [];
const other = this.other;
// 计算多边形的边向量
const edges = this.vertices.map((vertex, i) => {
const nextVertex = this.vertices[(i + 1) % this.vertices.length];
return nextVertex.sub(vertex);
});
// 添加所有边向量的法向量(分离轴)
edges.forEach(edge => {
const normal = edge.normal().normalize();
if (!axes.some(axis => axis.equals(normal) || axis.equals(normal.negate()))) {
axes.push(normal);
}
});
if (other) {
// 添加与其他多边形的边向量的法向量(分离轴)
other.vertices.forEach((vertex, i) => {
const nextVertex = other.vertices[(i + 1) % other.vertices.length];
const edge = nextVertex.sub(vertex)
const normal = edge.normal();
if (!axes.some(axis => axis.equals(normal) || axis.equals(normal.negate()))) {
axes.push(normal);
}
});
}
return axes;
}
transformVertices() {
return this.vertices.map(v => v.add(this.position))
}
// 计算多边形在某个向量上的投影区间
project2(axis) {
const scalars = this.transformVertices().map(vertex => vertex.dot(axis));
return {
min: Math.min(...scalars),
max: Math.max(...scalars)
};
}
// 计算多边形在某个向量上的投影区间
project(axis) {
let min = null, max = null;
let vertices = this.transformVertices();
for (let i = 0; i < vertices.length; i++) {
let vertex = vertices[i];
let dot = vertex.dot(axis) / axis.length();
if (min === null || dot < min) {
min = dot;
}
if (max === null || dot > max) {
max = dot;
}
}
return {
min: min,
max: max
};
}
getEdges() {
const edges = this.vertices.map((vertex, i) => {
const nextVertex = this.vertices[(i + 1) % this.vertices.length];
return nextVertex.sub(vertex);
});
return edges;
}
getAxes() {
// 也可以归一化 normalize
return this.getEdges().map(edge => {
return edge.normalLeft()
}).filter((normal, i, axes) => {
// if (!axes.some(axis => axis.equals(normal) || axis.equals(normal.negate()))) {
return true;
// }
//return false
})
}
overlap(p, p2) {
if (p.max < p2.min || p2.max < p.min) {
return 0
}
return Math.min(p.max, p2.max) - Math.max(p2.min, p.min)
}
collideWith(other) {
let axes = this.getAxes().concat(other.getAxes());
function isOverlay(proA, proB) {
let min, max;
if (proA.min < proB.min) {
min = proA.min;
} else {
min = proB.min;
}
if (proA.max > proB.max) {
max = proA.max;
} else {
max = proB.max;
}
return (proA.max - proA.min) + (proB.max - proB.min) < max - min;
}
// 归一化也可以 normalize
for (let axis of axes) {
let proj = this.project(axis);
let proj2 = other.project(axis);
// 如果投影区间不相交,则两个多边形不相交
// if (proj.max < proj2.min || proj2.max < proj.min) {
// return false;
// }
if (isOverlay(proj, proj2)) {
return false
}
// if ((proj.min < proj2.max && proj.min > proj2.min)
// ||
// (proj2.min < proj.max && proj2.min > proj.min)) {
// continue;
// }
// else {
// return false;
// }
// if (this.overlap(proj, proj2) === 0) {
// return false
// }
}
return true
}
getNestedPoint(center) {
let points = this.transformVertices()
let min = Infinity, p = 0;
for (let i = 0; i < points.length; i++) {
let p = points[i];
let len = p.sub(center).lengthSquared();
if (len < min) {
min = len;
p = i;
}
}
return points[p]
}
isOverlay(proA, proB) {
let min, max;
if (proA.min < proB.min) {
min = proA.min;
} else {
min = proB.min;
}
if (proA.max > proB.max) {
max = proA.max;
} else {
max = proB.max;
}
return (proA.max - proA.min) + (proB.max - proB.min) < max - min;
}
collideCircleWith(circle) {
let axises = this.getAxes();
// let vertices=this.vertices
let p = this.getNestedPoint(circle.position)
axises.push(p.sub(circle.position).normal())
// for(let i=0;i<vertices.length;i++){
// let v=vertices[i]
// axises.push(v.sub(circle.position).normal())
// }
for (let axis of axises) {
let proj = this.project(axis);
let proj2 = circle.project(axis);
if (this.isOverlay(proj, proj2)) {
return false
}
// // 如果投影区间不相交,则两个多边形不相交
// if (proj.max < proj2.min || proj2.max < proj.min) {
// return false;
// }
}
return true
}
drawShape(ctx, color) {
ctx.save()
ctx.translate(this.position.x, this.position.y)
// 绘制制控制点
ctx.beginPath();
ctx.fillStyle = 'blue'
ctx.arc(0, 0, this.controlRadius, 0, Math.PI * 2, false);
ctx.fill()
ctx.beginPath()
ctx.strokeStyle = color || "#000";
let vertices = this.vertices;
for (let i = 0; i < vertices.length; i++) {
let v = vertices[i % vertices.length]
if (i === 0) {
ctx.moveTo(v.x, v.y)
} else {
ctx.lineTo(v.x, v.y)
}
}
ctx.closePath()
ctx.stroke()
ctx.restore()
}
// 绘制多边形的分离轴和投影轴
draw(ctx) {
ctx.save();
ctx.strokeStyle = "#00ff00";
// 绘制分离轴
const separatingAxes = this.getSeparatingAxes().slice(0, 1);
[...separatingAxes].forEach((axis, i) => {
const start = this.vertices[0].add(axis.scale(1000)).add(this.position)
const end = this.vertices[0].sub(axis.scale(1000)).add(this.position)
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.stroke();
let a = this.vertices[0].add(axis.scale(80));
ctx.fillText((i) + '', a.x, a.y)
});
ctx.strokeStyle = "#ff0000";
// 绘制投影轴
const circleAxes = [new Vector(1, 0), new Vector(0, 1)]; // 圆形的投影轴是水平和垂直方向
function getProjectionOnAxis(axis, vertex) {
let min = 10000;
let max = -10000;
const dotProduct = vertex.dot(axis);
min = Math.min(min, dotProduct);
max = Math.max(max, dotProduct);
return {
min,
max,
};
}
[...separatingAxes].forEach((axis, i) => {
// // 绘制投影
const projection = this.project(axis);
const start = this.vertices[0].add(axis.scale(500)).add(this.position)
const end = this.vertices[0].add(axis.scale(500 * (projection.max - projection.min))).add(this.position)
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.stroke();
});
ctx.restore();
}
}
let p = new Polygon(80, 5, Vector.create(0, 0))
let p2 = new Polygon(80, 5, Vector.create(0, 0))
p.position.set(150, 250)
p2.position.set(275, 336)
let p3 = new Circle(80)
p3.position.set(540, 250)
let objects = [p, p2, p3], isCollection = false, isCollection2 = false;
//p.transformVertices()
// p2.transformVertices()
//p.other = p2;
function animate() {
// ctx.clearRect(0, 0, canvas.width, canvas.height);
// 指定颜色清除
ctx.fillStyle = '#ddd'
ctx.fillRect(0, 0, canvas.width, canvas.height)
p.drawShape(ctx, isCollection2 || isCollection ? 'red' : '#000')
p.draw(ctx)
p2.drawShape(ctx, isCollection ? 'red' : '#000')
p2.draw(ctx)
p3.draw(ctx, isCollection2 ? 'red' : '#000')
requestAnimationFrame(animate)
if (p.collideCircleWith(p3)) {
isCollection2 = true
} else {
isCollection2 = false
}
if (p.collideWith(p2)) {
isCollection = true
} else {
isCollection = false
}
}
animate();
let start = Vector.create(), dragObj = null
document.addEventListener('pointerdown', (e) => {
let a = canvas.getBoundingClientRect();
let mouse = Vector.create(e.clientX, e.clientY).sub(Vector.create(a.left, a.top));
start.copy(mouse)
//e.preventDefault();
dragObj = objects.find(d => d.testControlPoint(mouse));
console.log('dragObj', dragObj)
if (dragObj) {
start = start.sub(dragObj.position)
}
});
document.addEventListener('pointermove', (e) => {
let a = canvas.getBoundingClientRect();
let mouse = Vector.create(e.clientX, e.clientY).sub(Vector.create(a.left, a.top));
if (dragObj) {
dragObj.position.copy(mouse.sub(start));
}
});
document.addEventListener('pointerup', (e) => {
dragObj = null
isDrag = false;
});
// p2.draw(ctx)
}
}
});
addExample("球的撞反射", function () {
/**
碰撞后的速度计算需要考虑到动量守恒和动能守恒两个原则。
动量守恒指的是,在碰撞前和碰撞后,物体的总动量是不变的。也就是说,在碰撞瞬间,两个物体的动量之和不变。
动能守恒指的是,在碰撞前和碰撞后,物体的总动能是不变的。也就是说,在碰撞瞬间,两个物体的动能之和不变。
因此,我们可以利用这两个原则,来计算碰撞后的速度。
设物体1的质量为 m1,速度为 v1;物体2的质量为 m2,速度为 v2;碰撞后,物体1的速度为 v1',物体2的速度为 v2'。
根据动量守恒,有:
m1v1 + m2v2 = m1v1' + m2v2'
根据动能守恒,有:
1/2m1v1^2 + 1/2m2v2^2 = 1/2m1v1'^2 + 1/2m2v2'^2
将动量守恒式子中的 v1' 和 v2' 表示出来,代入动能守恒式子,可得:
v1' = (m1-m2)/(m1+m2)v1 + 2m2/(m1+m2)*v2
v2' = 2*m1/(m1+m2)*v1 + (m2-m1)/(m1+m2)*v2
这样就可以得到碰撞后的两个物体的速度了。需要注意的是,当两个物体质量相等时,碰撞后它们的速度会交换。
*
*/
class Ball extends DrawObject {
constructor(p) {
super(p);
this.radius = p.radius;
this.gravity = Vector.create(0, 980);
this.velocity = Vector.create(0, 0)
this.force = Vector.create(0, 0) // // 物体的作用力
this.friction = 0.9; // 动摩擦力
this.staticFriction = 0;// 静摩擦力
this.restitution = 0.7; // // 物体的恢复力
this.damping = 1;// 空气阻尼
this.mass = 100; // // 物体的质量
this.density = 1;//密度
// 物体的角速度
this.angularVelocity = 0;
this.angularDamping = 0; // 角度阻尼
// 物体的力矩
this.torque = 0;
// 物体的转动惯量
this.inertia = this.mass * this.radius ** 2 / 2;
this.vertices = []; // 多边形物体的顶点,默认为空数组
this.edges = []; // 多边形物体的边,默认为空数组
this.normal = new Vector(); // 碰撞点的法向量,默认为 0 向量
this.collisions = []
}
computeMass(area) {
this.mass = area * this.density;
}
update(dt) {
dt = dt * 0.001;
this.eureResolve(dt)
}
eureResolve(dt) {
let vx = this.velocity.x * this.damping + (this.gravity.x + this.force.x / this.mass) * dt;
let vy = this.velocity.y * this.damping + (this.gravity.y + this.force.y / this.mass) * dt;
this.position.x += vx * dt;
this.position.y += vy * dt;
this.velocity.x = vx;
this.velocity.y = vy;
// console.log('v', this.velocity.y)
this.force.x = 0;
this.force.y = 0;
this.torque = 0
}
// 更新物体的位置和角度
update2(dt) {
// 更新物体的速度
this.velocity.x += (this.force.x / this.mass + this.gravity.x) * dt;
this.velocity.y += (this.force.y / this.mass + this.gravity.y) * dt;
// 更新物体的位置
this.position.x += this.velocity.x * dt;
this.position.y += this.velocity.y * dt;
// 更新物体的角速度
this.angularVelocity += (this.torque / this.inertia) * dt;
// 更新物体的角度
this.angle += this.angularVelocity * dt;
// 应用阻尼
this.velocity.x *= (1 - this.damping);
this.velocity.y *= (1 - this.damping);
this.angularVelocity *= (1 - this.damping);
// 重置作用力和力矩
this.force = { x: 0, y: 0 };
this.torque = 0;
}
update3(deltaTime) {
// 计算重力
const gravityForce = { x: this.gravity.x * this.mass, y: this.gravity.y * this.mass };
this.applyForce(gravityForce);
// 计算空气阻力
const airResistanceForce = {
x: -this.velocity.x * this.damping,
y: -this.velocity.y * this.damping,
};
this.applyForce(airResistanceForce);
// 计算摩擦力
const frictionForce = {
x: -Math.sign(this.velocity.x) * Math.min(Math.abs(this.velocity.x), this.friction),
y: -Math.sign(this.velocity.y) * Math.min(Math.abs(this.velocity.y), this.friction),
};
this.applyForce(frictionForce);
// 计算总的作用力
const force = this.forceFunction(this.position, this.velocity, this.angle, this.angularVelocity);
// 计算加速度
const acceleration = { x: force.x / this.mass, y: force.y / this.mass };
this.velocity.x += acceleration.x * deltaTime;
this.velocity.y += acceleration.y * deltaTime;
// 计算角加速度
const torque = force.torque || 0;
this.angularVelocity += torque / this.momentOfInertia * deltaTime;
// 计算位置
this.position.x += this.velocity.x * deltaTime;
this.position.y += this.velocity.y * deltaTime;
// 计算角度
this.angle += this.angularVelocity * deltaTime;
}
/**
* 则合力$F_{total}$可以表示为:
$F_{total} = F - mg - kv^2$
其中,参数m表示物体质量,v表示物体速度,F表示物体受到的外力,k表示空气阻力常数。
*/
getTotalForce() {
//合力
// this.force.x=this.force.x-this.mass*this.gravity.x-
}
// 计算作用力
applyForce(force) {
this.force.x += force.x;
this.force.y += force.y;
}
// 应用力矩来更新角速度和转动惯量
applyTorque(torque) {
this.torque += torque;
this.angularVelocity += this.torque / this.inertia;
}
// 应用空气阻力来更新速度
applyDamping() {
const dampingForce = this.velocity.multiply(-1).multiply(this.damping);
this.acceleration.add(dampingForce.divide(this.mass));
}
// 应用冲量
applyImpulse(impulse, contactPoint = Vector.create()) {
this.velocity.x += impulse.x / this.mass;
this.velocity.y += impulse.y / this.mass;
this.angularVelocity +=
(contactPoint.x - this.position.x) * impulse.y / this.inertia -
(contactPoint.y - this.position.y) * impulse.x / this.inertia;
}
// 通过物体的形状计算转动惯量
calculateInertia() {
// 如果是圆形物体
if (this.radius > 0) {
this.inertia = (this.mass * this.radius ** 2) / 2;
}
// 如果是多边形物体
else if (this.vertices.length > 2) {
let numerator = 0;
let denominator = 0;
for (let i = 0; i < this.vertices.length; i++) {
const vertex1 = this.vertices[i];
const vertex2 = this.vertices[(i + 1) % this.vertices.length];
const crossProduct = vertex1.cross(vertex2);
numerator += crossProduct * (vertex1.dot(vertex1) + vertex1.dot(vertex2) + vertex2.dot(vertex2));
denominator += crossProduct;
}
this.inertia = (this.mass * numerator) / (6 * denominator);
}
}
collideWithLine(line) {
let a = Vector.create(line.x, line.y);
let b = Vector.create(line.x2, line.y2);
let c = this.position.clone();
let ac = c.sub(a)
let ab = b.sub(a);
let acProj = ac.project(ab);
let distance = acProj.distanceTo(ac); // 计算投影点与圆的距离
if (distance <= this.radius) {
if (line.static) {
return true;
}
// 计算碰撞法线
let normal = ac.sub(acProj).normalize()
// 计算相对速度
const relativeVelocity = this.velocity //.sub(collision.other.velocity);
// 计算垂直于法线的相对速度
const vn = relativeVelocity.dot(normal);
// 计算弹性碰撞的速度改变
const j = (-(1 + this.restitution) * vn) / (1 / this.mass + 1 / line.mass);
const impulse = normal.scale(j);
this.applyImpulse(impulse);
// // 计算切向相对速度
// let tangent = relativeVelocity.sub(normal.scale(vn));
// tangent= tangent.normalize();
// let jt = -relativeVelocity.dot(tangent);
// jt /= (1 / this.mass + 1 / line.mass);
// // 如果没有摩擦力,则不需要计算
// if (jt === 0) {
// return;
// }
// // 计算摩擦力的最大值
// let mu = this.friction //Math.min(this.friction, otherObject.friction);
// let frictionImpulse;
// // 如果摩擦力大于恢复力,则为动态摩擦力
// if (Math.abs(jt) < j * mu) {
// frictionImpulse = tangent.scale(jt);
// } else {
// frictionImpulse = tangent.scale(-j * mu);
// }
// console.log('frictionImpulse',frictionImpulse)
// this.applyForce(frictionImpulse)
}
return false
}
// 碰撞反应
collisionResponse(otherObject, collisionNormal) {
// 计算相对速度
let relativeVelocity = otherObject.velocity.subtract(this.velocity);
// 计算相对速度在法向量上的分量
let velAlongNormal = relativeVelocity.dot(collisionNormal);
// 如果小球朝向墙壁运动,则不需要碰撞反应
if (velAlongNormal > 0) {
return;
}
// 计算恢复力
let e = Math.min(this.restitution, otherObject.restitution);
// 计算冲量大小
let j = -(1 + e) * velAlongNormal;
j /= (1 / this.mass + 1 / otherObject.mass);
// 计算作用力
let impulse = collisionNormal.multiply(j);
// 应用作用力
this.applyForce(impulse.negate());
otherObject.applyForce(impulse);
// 计算摩擦力
let tangent = relativeVelocity.subtract(collisionNormal.multiply(relativeVelocity.dot(collisionNormal)));
tangent = tangent.normalize();
let jt = -relativeVelocity.dot(tangent);
jt /= (1 / this.mass + 1 / otherObject.mass);
// 如果没有摩擦力,则不需要计算
if (jt === 0) {
return;
}
// 计算摩擦力的最大值
let mu = Math.min(this.friction, otherObject.friction);
let frictionImpulse;
// 如果摩擦力大于恢复力,则为动态摩擦力
if (Math.abs(jt) < j * mu) {
frictionImpulse = tangent.multiply(jt);
} else {
frictionImpulse = tangent.multiply(-j * mu);
}
// 应用摩擦力
this.applyForce(frictionImpulse.negate());
otherObject.applyForce(frictionImpulse);
}
// 处理碰撞
handleCollision(collisions) {
if (collisions.length === 0) {
return;
}
// 重复处理每个碰撞点
for (let i = 0; i < collisions.length; i++) {
const collision = collisions[i];
// 计算碰撞法线
const normal = collision.normal.copy();
// 计算相对速度
const relativeVelocity = this.velocity.copy().sub(collision.other.velocity);
// 计算垂直于法线的相对速度
const vn = relativeVelocity.dot(normal);
// 计算切向相对速度
const tangent = p5.Vector.sub(relativeVelocity, p5.Vector.mult(normal, vn));
const vt = tangent.mag();
// 计算弹性碰撞的速度改变
const j = (-(1 + this.restitution) * vn) / (1 / this.mass + 1 / collision.other.mass);
const impulse = p5.Vector.mult(normal, j);
this.applyForce(impulse);
}
}
draw(ctx) {
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
}
}
return {
template: `<div><div ref="canvas"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.canvas;
var { add, start, addDraw, width, height } = createCanvas(container, {
width: 500,
height: 500,
background: '#ddd'
});
let line = new Line({
mass: 10000000000,
static: false,
x: 50,
y: height - 100,
x2: 200,
y2: height - 70
})
let ground = new Line({
mass: 10000,
static: true,
x: 0,
y: height - 20,
x2: width,
y2: height - 20
})
let ball = new Ball({ radius: 20 })
ball.position.set(100, 100)
add(ball);
add(line);
add(ground);
addDraw((dt) => {
if (ball.collideWithLine(line)) {
// return
}
if (ball.collideWithLine(ground)) {
return
}
ball.update(dt);
});
start()
}
}
});
addExample("简单的碰撞", function () {
class Ball extends DrawObject {
constructor(p) {
super(p);
this.radius = p.radius;
this.velocity = Vector.create();
this.gravity = Vector.create(0, 9.8 * 1000);
this.restitution = 0.5;
this.mass = 1;
this.force = Vector.create(0, 0)
this.friction = 0.5;
this.torque = 0;
this.angularVelocity = 0;
}
updatePosition(dt) {
dt *= 0.001
this.position.x += this.velocity.x * dt;
this.position.y += this.velocity.y * dt;
}
// 应用力
applyForce(force) {
this.force = this.force.add(force);
}
// 应用冲量
applyImpulse(impulse) {
const velocityChange = impulse.scale(1 / this.mass);
this.velocity = this.velocity.add(velocityChange);
}
updateVelocity2(dt) {
dt *= 0.001
this.velocity.x = this.velocity.x + this.gravity.x * dt;
this.velocity.y = this.velocity.y + (this.gravity.y * this.mass) * dt;
}
updateVelocity(dt) {
dt *= 0.001
// 应用重力
const gravityForce = this.gravity.scale(dt);
this.applyForce(gravityForce);
// 计算加速度
const acceleration = this.force.scale(1 / this.mass);
// 更新速度和位置
this.velocity = this.velocity.add(acceleration.scale(dt));
this.position = this.position.add(this.velocity.scale(dt));
// 计算角加速度
const angularAcceleration = this.torque / this.mass;
// 更新角速度和角度
this.angularVelocity += angularAcceleration * dt;
this.angle += this.angularVelocity * dt;
// 应用空气阻尼,减少速度和角速度
const damping = 0; // 空气阻尼系数
this.velocity = this.velocity.scale(1 - damping * dt);
this.angularVelocity *= 1 - damping * dt;
// 重置力和力矩
this.force.set(0, 0)
this.torque = 0;
}
update() {
if (this.position.y - this.radius < 0) {
this.position.y = this.radius;
this.velocity.y *= -1 * this.restitution;
}
if (this.position.y + this.radius > this.viewHeight) {
this.position.y = this.viewHeight - this.radius;
this.velocity.y *= -1 * this.restitution;
}
if (this.position.x - this.radius < 0) {
this.position.x = this.radius
this.velocity.x *= -1 * this.restitution;
}
if (this.position.x + this.radius > this.viewWidth) {
this.position.x = this.viewWidth - this.radius
this.velocity.x *= -1 * this.restitution;
}
}
// 旋转法
collideWith2(other) {
if (other instanceof Line) {
let a = Vector.create(other.points[0], other.points[1])
let b = Vector.create(other.points[2], other.points[3])
let c = this.position.clone()
let ab = b.sub(a);// ab线段
let ac = c.sub(a);
if (c.x + this.radius < a.x || c.x - this.radius > b.x) {
return
}
// 求出线段的角度
let angle = ab.angle();
// 以线段起点,为坐标原点,y小于起点y,都是负数,当圆的y 等于0时,就证明与线段起始y同水平
// 因为线段是斜的,所以保证圆的位置是相对斜线水平的,需要旋转一下
let cp = ac.rotateTo(-angle)
let r = this.radius;
// 速度的方向也要旋转
let velocity = this.velocity.rotateTo(-angle)
// 圆的没有到达线段水平位置时,都是负数
if (cp.y + r > 0 && cp.y < velocity.y) {
let velocity2 = this.velocity.rotateTo(-angle)
// 速度迭代步太高,导致没有刚好落下边界处,
// 所以要把位置重新调一下
// 这个段代码。是让圆在刚好水平线上,然后再旋转,得到落点位置
let newPos = cp.set(cp.x, -r).rotateTo(angle)
this.position.x = a.x + newPos.x;
this.position.y = a.y + newPos.y;
// this.gravity.set(0,0);
// this.velocity.set(0,0);
this.velocity = velocity.set(velocity.x, velocity.y * -1 * this.restitution).rotateTo(angle);
if (this.restitution > 0) {
// this.velocity.x*=this.restitution
}
if (this.restitution > 0) {
// this.velocity.y*=this.restitution
}
}
}
}
// 投影
collideWith(other) {
if (other instanceof Line) {
let a = Vector.create(other.points[0], other.points[1])
let b = Vector.create(other.points[2], other.points[3])
let c = this.position.clone()
let ab = b.sub(a);// 线段ab向量
let ac = c.sub(a);// 圆中心位置到线段a位置,ac向量
let abLen = ab.length() //
let proj = ac.project(ab);// ac 在ab上的投影向量
let closestPoint = proj.add(a) // ac的投影位置,就是圆受重力,要下坠的位置
let dist = c.sub(closestPoint).length(); // 圆心到投影位置的距离
if (dist <= this.radius && closestPoint.x > a.x && closestPoint.x < b.x) {
var normal = c.subtract(closestPoint).normalize();
let relativeVelocity = this.velocity.clone()
// 计算碰撞切线的方向向量
const tangent = Vector.create(-normal.y, normal.x);
// 计算法向速度和切向速度
const normalVelocity = relativeVelocity.project(normal);
const tangentVelocity = relativeVelocity.project(tangent);
// 计算法向力和切向力
// const normalForce = normal.multiply(this.gravity.scale(this.mass)); // 假设有重力
// const tangentForce = tangent.multiply(this.gravity.scale(this.mass).scale(this.friction));
// // 根据弹性系数、质量和摩擦力计算碰撞后的速度和力
// const newNormalVelocity = normalVelocity.scale(-this.restitution).subtract(normalForce);
// const newTangentVelocity = tangentVelocity.subtract(tangentForce);
// const newNormalForce = normalForce.scale(this.restitution);
// 根据弹性系数和质量计算碰撞后的速度
const newNormalVelocity = normalVelocity.scale(-this.restitution);
// // 相对y轴角度
// let angle=Math.atan2(ab.x,ab.y)-Math.PI/2;
// // 相对x轴角度
// let angle2=Math.atan2(ab.y,ab.x)
// let h=Math.sqrt(ab.cross(ac))/abLen;
// let newVelocity=this.velocity.clone();
console.log('碰撞到', newNormalVelocity.add(tangentVelocity))
this.position.y = closestPoint.y - this.radius;
// 更新速度
this.velocity = newNormalVelocity.add(tangentVelocity);
}
}
}
draw(ctx) {
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
}
}
class Line extends DrawObject {
constructor(p) {
super(p);
this.points = p.points;
this.mass = 1;
}
getDefaultStyle() {
return {
fillStyle: 'none',
strokeStyle: '#000',
lineWidth: 1
}
}
beforeDraw() {
}
draw(ctx) {
let p = this.points;
ctx.moveTo(p[0], p[1])
ctx.lineTo(p[2], p[3])
}
}
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
var { add, start, addDraw, width, height } = createCanvas(container, {
width: 500,
height: 500,
background: '#ddd'
});
let line = new Line({
points: [50, height - 130, 300, height - 60]
})
let ball = new Ball({
radius: 20, style: {
fillStyle: "blue"
}
})
let ball2 = new Ball({
radius: 20, style: {
fillStyle: "blue"
}
})
let ball3 = new Ball({
radius: 20, style: {
fillStyle: "blue"
}
})
ball.position.set(140, 100)
ball2.position.set(40, 100)
ball3.position.set(390, 100)
add(ball)
// add(ball2)
// add(ball3)
add(line)
addDraw(() => {
})
start();
}
}
})
addExample("box2d物理世界", function () {
// 调用 update
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
const {ctx,start,draw,add,addDraw,setScale}=createCanvas(container,{
width:500,
height:500,
background:"#ddd"
})
DrawObject.globalScale=50;
setScale( DrawObject.globalScale)
const world=new b2.World(new b2.Vec2(0,9.8))
const updatePosition=(obj)=>{
let b2Body=obj.b2Body;
let pos=b2Body.GetPosition()
obj.position.set(pos.x,pos.y)
obj.rotation=b2Body.GetAngle()
}
const bindBox2dObject=(obj,config={})=>{
let {static=false,type,density=1,friction=0.2,restitution=0}=config
let bodyType=type?type:static?b2.BodyType.b2_staticBody:b2.BodyType.b2_dynamicBody
let shape
let fd={
}
if(obj instanceof CircleObject){
shape=new b2.CircleShape(obj.r)
}else if(obj instanceof PolygonObject){
shape=new b2.PolygonShape()
shape.Set(obj.vertices)
}else if(obj instanceof LineObject){
shape=new b2.EdgeShape()
let v0=new b2.Vec2(obj.x,obj.y)
let v1=new b2.Vec2(obj.x2,obj.y2)
let v2=new b2.Vec2(obj.x2+1,obj.y2)
let v3=new b2.Vec2(obj.x+1,obj.y)
// shape.SetOneSided(v0,v1,v2,v3)
// console.log(v0,v1)
shape.SetTwoSided(v0,v1)
// fd.restitution=0.8
//shape.SetTwoSided(new b2.Vec2(-40.0, 0.0), new b2.Vec2(40.0, 0.0));
}
fd.shape=shape
fd.density=density
fd.friction=friction
fd.restitution=restitution
let awake=true;
if(bodyType===b2.BodyType.b2_staticBody){
fd.density=0;
awake=false
}
const body=world.CreateBody({
type:bodyType,
position:obj.position,
angle:obj.rotation
// allowSleep:false,
// awake:awake
})
// if(!static){
// body.SetPosition(obj.position)
// body.SetAngle(obj.rotation)
// }
const fixture=body.CreateFixture(fd)
obj.b2Body=body;
body.obj=obj;
obj.updatePosition=function(){
updatePosition(this)
}
}
const createBounds=(left,right,top,bottom)=>{
const leftObj=new Line({
x:left,
x2:left,
y:top,
y2:bottom,
style:{
strokeStyle:'blue'
}
});
const rightObj=new Line({
x:right,
x2:right,
y:top,
y2:bottom,
style:{
strokeStyle:'blue'
}
})
add(leftObj)
add(rightObj)
bindBox2dObject(leftObj,{static:true,restitution:0.8})
bindBox2dObject(rightObj,{static:true,restitution:0.8})
}
createBounds(10,490,0,500)
const c=new CircleObject({r:30,style:{
fillStyle:'red'
}})
const c2=new CircleObject({r:30,style:{
fillStyle:'yellow'
}})
const p=new PolygonObject({
style:{
fillStyle:"blue"
}
})
c.setPosition(250,100)
c2.setPosition(150,100)
p.setBox(50,10)
p.setPosition(250,300)
p.angle=30
const ground=new PolygonObject({
style:{
fillStyle:"blue"
}
})
ground.setBox(250,10)
ground.setPosition(250,490)
add(c)
add(c2)
add(p)
add(ground)
bindBox2dObject(c,{restitution:0.6});
bindBox2dObject(c2,{restitution:0.6});
bindBox2dObject(p,{static:true});
bindBox2dObject(ground,{static:true});
start();
addDraw((delta)=>{
// ctx.scale(25,25)
world.SetAllowSleeping(false)
world.SetWarmStarting(false);
world.SetContinuousPhysics(true);
world.SetSubStepping(false);
world.Step(delta/1000,10,10)
})
var h = 10; // 抛的高度
var g = world.GetGravity().y; // 重力加速度
var t = Math.sqrt((2 * h) / g); // 上升时间
var v0 = g * t; // 初始速度大小
var velocity = new b2.Vec2(0, v0); // 初始速度向上
// body.SetLinearVelocity(velocity);
console.log('v0',v0,velocity)
ctx.canvas.addEventListener('mousedown',e=>{
if(c.hitTest(e.offsetX,e.offsetY)){
console.log('命中c')
c.b2Body.SetLinearVelocity(velocity)
// c.b2Body.ApplyForce(new b2.Vec2(250-e.offsetX,0-e.offsetY),c.b2Body.GetPosition());
}
if(c2.hitTest(e.offsetX,e.offsetY)){
console.log('命中c2')
c2.b2Body.ApplyForce(new b2.Vec2(250-e.offsetX,0-e.offsetY),c2.b2Body.GetPosition());
}
// c2.b2Body.ApplyForce(new b2.Vec2(250-e.offsetX,0-e.offsetY),c2.b2Body.GetPosition());
//console.log('fff')
})
}
}
})
addExample("台球", function () {
return {
template: `<div><div ref="main"></div></div>`,
data() { return {}; },
computed: {},
methods: {},
mounted() {
var container = this.$refs.main;
var stageWidth = 500, stageHeight = 500;
var mapWidth = stageWidth * 1, mapHeight = stageHeight
let map = new Hilo.Container()
let game = new Game({
container: container,
width: stageWidth,
height: stageHeight,
background: '#ddd',
enableDOMEvent: true,
scenes: [],
init() {
// 初始
// this.initPhysicsWorld({
// gravity:{
// y:0,
// x:0
// }
// });
let that = this, balRadius = 30;
let normalBalls = []
function createPoolTable() {
let g = new Hilo.Graphics();
g.beginPath()
g.beginFill('#006600')
g.drawRect(0, 0, stageWidth, stageHeight)
g.endFill()
g.beginPath()
g.beginFill(null)
g.lineStyle(2, '#fff')
g.drawRect(20, 20, stageWidth - 40, stageHeight - 40)
g.endFill()
map.addChild(g)
}
createPoolTable()
function createBall(x, y, radius, fillStyle, lineStyle) {
radius = radius === null ? balRadius : radius
let ball = new Hilo.Graphics({
x: x,
y: y,
pivotX: radius,
pivotY: radius,
width: radius * 2,
height: radius * 2
})
ball.beginPath()
fillStyle && ball.beginFill(fillStyle);
ball.lineStyle(1, lineStyle)
ball.drawCircle(0, 0, radius)
ball.endFill();
return ball
}
function createNormalBall(x = 200, y = 100) {
let ball = createBall(x, y, balRadius, 'blue', '#000')
map.addChild(ball)
Hilo.util.copy(ball, Hilo.drag);
ball.startDrag();
normalBalls.push(ball)
return ball;
}
function createLine(type = 1) {
let line = new Hilo.Graphics({
});
map.addChild(line)
return {
obj: line,
update(x, y, x2, y2) {
x2=-1;
// y2*=-1;
line.clear();
ball.beginPath()
line.lineStyle(1, type === 1 ? '#000' : '#fff');
if (type === 2) {
line.setLineDash([5, 8])
}
line.moveTo(x, y)
line.lineTo(x2, y2);
line.endFill();
}
}
}
function createMailBall(_x = 300, _y = 300) {
let container = new Hilo.Container({
x: _x,
y: _y,
width: 60,
height: 60
});
let ball = createBall(0, 0, balRadius, 'red', '#000');
container.addChild(ball)
container.addTo(map);
// 射线
let { update: updateLine } = createLine();
// 射线2
let { update: updateLine2, obj: rayLine2 } = createLine(2);
// 假想球,球的撞击位置
let moveBall = createBall(0, 0, 5, 'red')
// moveBall.setLineDash([5,8])
moveBall.visible = false;
let collisionBall = createBall(0, 0, balRadius, null, 'green')
collisionBall.setLineDash([5, 8])
collisionBall.visible = false;
map.addChild(collisionBall)
map.addChild(moveBall);
Hilo.util.copy(container, Hilo.drag);
container.startDrag()
game.container.addEventListener('pointermove', e => {
let mouse = that.getMouseEvent(e);
let dx = mouse.x - container.x;
let dy = mouse.y - container.y;
let angle = Math.atan2(dy, dx) - Math.PI;
let len = Math.sqrt(dx * dx + dy * dy);
let scaler = 1000;
let nx = scaler * Math.cos(angle);
let ny = scaler * Math.sin(angle);
// console.log(x,cos,y,sin)
nx = -scaler * dx / len;
ny = -scaler * dy / len;
let x = container.x;
let y = container.y;
let x2 = x + nx;
let y2 = y + ny;
let lineStart = Vector2D.create(x, y);
let lineEnd = Vector2D.create(x2, y2);
updateLine(x, y, x2, y2);// 更新射线
// 检测射线方向,是否可以撞击到球
// 查找球心离线射线最近的点,判断点与线的距离
let minDistance = Infinity, currentBall = null;
normalBalls.forEach(ball => {
let ballPos = Vector2D.create(ball.x, ball.y)
let c = findLineCircleNearestPoint(lineStart, lineEnd, ballPos, balRadius);
let dist = c.distanceTo(ballPos);
let cueBallDist = lineStart.distanceTo(ballPos)
if (dist <= balRadius * 2 + 5 && cueBallDist < minDistance) {
collisionBall.x = c.x;
collisionBall.y = c.y;
moveBall.x = c.x;
moveBall.y = c.y;
minDistance = cueBallDist
currentBall = ball;
}
});
if (currentBall) {
moveBall.visible = true;
collisionBall.visible = true
} else {
moveBall.visible = false
collisionBall.visible = false
}
// 如果可以撞到球
if (currentBall) {
rayLine2.visible = true;
// 从碰撞球发出射线
let va = Vector2D.create(collisionBall.x, collisionBall.y);
let vb = Vector2D.create(currentBall.x, currentBall.y)
let lineEnd = vb.sub(va).normalize().scale(1000).add(va);
updateLine2(va.x, va.y, lineEnd.x, lineEnd.y)
} else {
rayLine2.visible = false
}
})
return container;
}
let ball = createNormalBall(200, 100)
// let ball2 = createNormalBall(100, 200)
let mainBall = createMailBall()
// world.createBounds(mapWidth,mapHeight)
}
});
game.stage.addChild(map)
game.on('tick', (dt) => {
// camera.tick(dt)
// map.x = -camera.scroll.x;
// console.log('map', map.x)
})
}
}
})
addExample("奇偶和绕边", function () {
/**
* nonzero|evenodd
*
* 默认值 nonzero
* 非零(nonzero)
* 该值nonzero确定形状中某个点的“内部性”,方法是从该点向任意方向绘制一条射线,
* 然后检查形状的一部分与射线相交的位置。从零计数开始,每次路径段从左到右穿过射线时加一,
* 每次路径段从右到左穿过射线时减一。在计算交叉点后,如果结果为零,则该点位于路径之外。否则,它在里面。
*
* 偶数(evenodd)
该值evenodd通过从该点向任意方向绘制一条射线并计算射线穿过给定形状的路径段数来确定形状中该点的“内部性”。
如果这个数是奇数,则点在里面;如果偶数,则点在外面。
*/
return {
extends: BaseCanvas,
data() { return {}; },
computed: {},
methods: {
/**
* @param {CanvasRenderingContext2D} ctx
* @param {HTMLCanvasElement} ctx
*/
init(ctx) {
const makePolygonVertices = (cx, cy, radius, side) => {
const points = [], p = Math.PI * 2 / side
let start = -Math.PI;
for (let i = 0; i < side; i++) {
points.push([cx + radius * Math.cos(start), cy + radius * Math.sin(start)])
start += p;
}
points.push(points[0].slice())
return points
}
const drawPolygon = (vertices, reverse = false, strokeStyle = '#000') => {
if (reverse) {
vertices = vertices.toReversed()
// vertices.slice().reverse()
}
for (let i = 0; i < vertices.length; i++) {
let i2 = i % vertices.length
ctx[i == 0 ? 'moveTo' : 'lineTo'](vertices[i2][0], vertices[i2][1])
}
// ctx.closePath()
// ctx.stroke()
}
const drawCircle = (x, y, radius, color = 'red') => {
ctx.beginPath()
ctx.fillStyle = color
ctx.arc(x, y, radius, 0, Math.PI * 2)
ctx.fill()
}
const drawLine = (x, y, x2, y2) => {
ctx.beginPath()
ctx.lineWidth = 1
ctx.strokeStyle = 'green'
ctx.moveTo(x, y)
ctx.lineTo(x2, y2)
ctx.stroke()
}
const polygon1 = makePolygonVertices(300, 300, 50, 5);
const polygon2 = makePolygonVertices(300, 300, 150, 5);
const circle = { x: 300, y: 300, radius: 5, fill: "red" }
let selectedObj
this.$on('drag', e => {
if (selectedObj) {
selectedObj.x += e.deltaX
selectedObj.y += e.deltaY
// 判断圆点是否在
isInside = checkPointFromOuter(circle.x, circle.y, d.fillRule, [polygon1, polygon2])
}
})
this.$on('mouseup', e => {
selectedObj = null
})
this.$on('mousedown', e => {
let dx = e.offsetX - circle.x;
let dy = e.offsetY - circle.y;
let dist = dx * dx + dy * dy;
if (dist <= circle.radius ** 2) {
selectedObj = circle
}
})
const d = addGuiScheme(this.$gui, {
source: {
fillRule: 'evenodd'
},
schemes: {
fillRule: {
type: "list",
params: ['evenodd', 'nonzero']
}
},
onChange: () => {
isInside = checkPointFromOuter(circle.x, circle.y, d.fillRule, [polygon1, polygon2])
}
})
const intersectionPoints = []
// 检查点是否在外部
const checkPointFromOuter = (x, y, fillRule, paths) => {
// 创建一条射线
const lineA = new Line(0, 0, x, y)
intersectionPoints.length = 0
let inside = false
let intersectCount = 0
if (fillRule === 'evenodd') {
// 计算射线,穿过边,如果是偶数,点就在外部
for (let k = 0; k < paths.length; k++) {
let path = paths[k]
for (let i = 0; i < path.length; i++) {
let start = path[i]
let end = path[(i + 1) % path.length]
let lineB = new Line(start[0], start[1], end[0], end[1])
let point = lineA.getIntersectionPoint(lineB)
if (point) {
intersectionPoints.push(point)
}
}
}
if (intersectionPoints.length > 0) {
inside = intersectionPoints.length % 2 !== 0
}
} else if (fillRule === 'nonzero') {
let windingNumber = 0;
paths[1] = paths[1].toReversed() // 把这个路径逆时针一下
// 计算射线,从路径起点出,穿过右方向的加+,左方高的-1
let pathDirection = Array.from({ length: 2 }, () => {
return {
direction: 0,
intersectionPoints: []
}
})
for (let k = 0; k < paths.length; k++) {
let path = paths[k]
let sum = 0
for (let i = 0; i < path.length; i++) {
let start = path[i]
let end = path[(i + 1) % path.length]
let lineB = new Line(start[0], start[1], end[0], end[1])
let point = lineA.getIntersectionPoint(lineB)
if (point) {
pathDirection[k].intersectionPoints.push(point)
intersectionPoints.push(point)
}
sum += lineB.cross()
}
pathDirection[k].direction = sum
console.log('sum' + k, sum)
}
if (intersectionPoints.length > 0) {
// 计算射线,从路径起点出,穿过右方向的加+,左方高的-1
pathDirection.forEach(path=>{
let direction=path.direction
let windingNumber=0
path.intersectionPoints.forEach(()=>{
if(direction>0){
windingNumber++
}else if(direction<0){
windingNumber--
}
})
intersectCount+=windingNumber%2 // 穿过同一个路径,偶数就归0
})
console.log('intersectionPoints', intersectionPoints.length, 'intersectCount', intersectCount)
inside = intersectCount !== 0
}
}
return inside
}
// 判断圆点是否在
let isInside = checkPointFromOuter(circle.x, circle.y, d.fillRule, [polygon1, polygon2])
this.requestAnimationFrame(() => {
ctx.beginPath()
drawPolygon(polygon1)
drawPolygon(polygon2, d.fillRule === "nonzero" ? true : false)
ctx.fill(d.fillRule)
drawLine(circle.x, circle.y, 0, 0)
intersectionPoints.forEach((p) => {
drawCircle(p.x, p.y, 3, 'red');
})
drawCircle(circle.x, circle.y, circle.radius, isInside ? 'red' : 'blue');
})
}
},
mounted() {
var container = this.$refs.main;
}
}
})
addExample("曲线交点", function () {
return {
extends: BaseCanvas,
data() { return {}; },
computed: {},
methods: {
/**
* @param {CanvasRenderingContext2D} ctx
* @param {HTMLCanvasElement} ctx
*/
init(ctx) {
let selectedObj;
let intersections=[]
const getMatrix=(p1,p2,p3,q1,q2,q3)=>{
let min={
x:Infinity,
y:Infinity
},max={
x:-Infinity,
y:-Infinity
}
for(let t=0;t<=1;t+=0.01){
let p=getQuadraticBezierPoint(p1,p2,p3,t)
let q=getQuadraticBezierPoint(q1,q2,q3,t)
p.x=Math.floor(p.x)
p.y=Math.floor(p.y)
q.x=Math.floor(q.x)
q.y=Math.floor(q.y)
min.x=Math.min(p.x,min.x)
min.x=Math.min(q.x,min.x)
min.y=Math.min(p.y,min.y)
min.y=Math.min(q.y,min.y)
max.x=Math.max(p.x,max.x)
max.x=Math.max(q.x,max.x)
max.y=Math.max(p.y,max.y)
max.y=Math.max(q.y,max.y)
}
let width=max.x-min.x;
let height=max.y-min.y;
let matrix=new Array(width*height);
for(let t=0;t<=1;t+=0.001){
let p=getQuadraticBezierPoint(p1,p2,p3,t)
let q=getQuadraticBezierPoint(q1,q2,q3,t)
p.x=Math.floor(p.x)
p.y=Math.floor(p.y)
q.x=Math.floor(q.x)
q.y=Math.floor(q.y)
let index=p.x+p.y*width;
let index2=q.x+q.y*width;
if(!matrix[index]){
matrix[index]={x:0,y:0,value:0,id:'',points:[]}
}
if(!matrix[index2]){
matrix[index2]={x:0,y:0,value:0,id:'',points:[]}
}
if(matrix[index].id!=='p'){
matrix[index].value++
matrix[index].x=p.x;
matrix[index].y=p.y
matrix[index].id='p'
matrix[index].points.push('p:'+p.x+','+p.y)
}
if(matrix[index2].id!=='q'){
matrix[index2].id='q'
matrix[index2].value++
matrix[index2].x=q.x;
matrix[index2].y=q.y
matrix[index2].points.push('q:'+q.x+','+q.y)
}
}
let item=matrix.filter(d=>d&&d.value==2)
return item?item:[]
}
this.$on('drag', e => {
if (selectedObj) {
selectedObj.x += e.deltaX
selectedObj.y += e.deltaY
// 创建二次贝塞尔曲线对象
// 计算出最小与最大点的两维数组
const intersections2=getMatrix.apply(null,a.points.concat(b.points))
// const intersections2=calculateIntersection2.apply(null,a.points.concat(b.points).map(d=>[d.x,d.y]))
intersections.length=0
console.log('p',intersections2)
if(intersections2&&intersections2.length){
intersections=intersections2
}
}
})
this.$on('mouseup', e => {
selectedObj = null
})
this.$on('mousedown', e => {
[a,b].some(d=>{
selectedObj=d.hitTest(e.offsetX,e.offsetY)
if(selectedObj){
return true
}
})
})
const drawCircle = (x, y, radius, color = 'red') => {
ctx.beginPath()
ctx.fillStyle = color
ctx.arc(x, y, radius, 0, Math.PI * 2)
ctx.fill()
}
const drawQuadraticCurve = (mx,my,cpx,cpy,x, y, radius, color = 'red') => {
drawCircle(mx,my,radius)
drawCircle(cpx,cpy,radius,'blue')
drawCircle(x,y,radius)
ctx.beginPath()
ctx.strokeStyle = color
ctx.moveTo(mx,my)
ctx.quadraticCurveTo(cpx,cpy,x,y)
ctx.stroke()
}
const makeQuadraticCurve=(mx,my,cpx,cpy,x, y, color = '#000')=>{
let c={x:mx,y:my}
let c1={x:cpx,y:cpy}
let c2={x:x,y:y}
let radius=5;
let objs=[c,c1,c2]
return {
points:objs,
draw(){
drawQuadraticCurve(c.x,c.y,c1.x,c1.y,c2.x,c2.y,radius,color)
},
hitTest(x,y){
let item=objs.find(circle=>{
let dx = x - circle.x;
let dy = y - circle.y;
let dist = dx * dx + dy * dy;
if (dist <= radius** 2) {
return true
}
return false
})
return item
}
}
}
let a=makeQuadraticCurve(300,300,300,350,500,100)
let b=makeQuadraticCurve(200,200,300,150,500,300)
const intersections2=getMatrix.apply(null,a.points.concat(b.points))
// const intersections2=calculateIntersection2.apply(null,a.points.concat(b.points).map(d=>[d.x,d.y]))
intersections=intersections2
console.log('p',intersections2)
this.requestAnimationFrame(() => {
a.draw()
b.draw()
for(let t=0;t<=1;t+=0.01){
let p=getQuadraticBezierPoint(a.points[0],a.points[1],a.points[2],t)
drawCircle(p.x,p.y,2,'blue')
}
intersections.forEach(d=>{
drawCircle(d.x,d.y,5,'green')
})
})
}
},
mounted() {
var container = this.$refs.main;
}
}
})