0
0
Fork 0
mirror of https://github.com/liabru/matter-js.git synced 2024-12-28 14:09:01 -05:00
liabru-matter-js/src/render/Render.js

718 lines
No EOL
22 KiB
JavaScript

/**
* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js)
* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples.
*
* @class Render
*/
// TODO: viewports
// TODO: two.js, pixi.js
var Render = {};
(function() {
/**
* Description
* @method create
* @param {object} options
* @return {render} A new renderer
*/
Render.create = function(options) {
var defaults = {
controller: Render,
element: null,
canvas: null,
options: {
width: 800,
height: 600,
background: '#fafafa',
wireframeBackground: '#222',
enabled: true,
wireframes: true,
showSleeping: true,
showDebug: false,
showBroadphase: false,
showBounds: false,
showVelocity: false,
showCollisions: false,
showAxes: false,
showPositions: false,
showAngleIndicator: false,
showIds: false,
showShadows: false
}
};
var render = Common.extend(defaults, options);
render.canvas = render.canvas || _createCanvas(render.options.width, render.options.height);
render.context = render.canvas.getContext('2d');
render.textures = {};
Render.setBackground(render, render.options.background);
if (Common.isElement(render.element)) {
render.element.appendChild(render.canvas);
} else {
Common.log('No "render.element" passed, "render.canvas" was not inserted into document.', 'warn');
}
return render;
};
/**
* Sets the background CSS property of the canvas
* @method setBackground
* @param {render} render
* @param {string} background
*/
Render.setBackground = function(render, background) {
if (render.currentBackground !== background) {
var cssBackground = background;
if (/(jpg|gif|png)$/.test(background))
cssBackground = 'url(' + background + ')';
render.canvas.style.background = cssBackground;
render.canvas.style.backgroundSize = "contain";
render.currentBackground = background;
}
};
/**
* Description
* @method world
* @param {engine} engine
*/
Render.world = function(engine) {
var render = engine.render,
world = engine.world,
canvas = render.canvas,
context = render.context,
options = render.options,
bodies = Composite.allBodies(world),
constraints = Composite.allConstraints(world),
i;
if (options.wireframes) {
Render.setBackground(render, options.wireframeBackground);
} else {
Render.setBackground(render, options.background);
}
// clear the canvas with a transparent fill, to allow the canvas background to show
context.globalCompositeOperation = 'source-in';
context.fillStyle = "transparent";
context.fillRect(0, 0, canvas.width, canvas.height);
context.globalCompositeOperation = 'source-over';
/*if (options.showShadows && !options.wireframes)
Render.bodyShadows(engine, bodies, context);*/
if (!options.wireframes || (engine.enableSleeping && options.showSleeping)) {
// fully featured rendering of bodies
Render.bodies(engine, bodies, context);
} else {
// optimised method for wireframes only
Render.bodyWireframes(engine, bodies, context);
}
if (options.showBounds)
Render.bodyBounds(engine, bodies, context);
if (options.showAxes || options.showAngleIndicator)
Render.bodyAxes(engine, bodies, context);
if (options.showPositions)
Render.bodyPositions(engine, bodies, context);
if (options.showVelocity)
Render.bodyVelocity(engine, bodies, context);
if (options.showIds)
Render.bodyIds(engine, bodies, context);
if (options.showCollisions)
Render.collisions(engine, engine.pairs.list, context);
Render.constraints(constraints, context);
if (options.showBroadphase && engine.broadphase.current === 'grid')
Render.grid(engine, engine.broadphase[engine.broadphase.current].instance, context);
if (options.showDebug)
Render.debug(engine, context);
};
/**
* Description
* @method debug
* @param {engine} engine
* @param {RenderingContext} context
*/
Render.debug = function(engine, context) {
var c = context,
world = engine.world,
render = engine.render,
options = render.options,
bodies = Composite.allBodies(world),
space = " ";
if (engine.timing.timestamp - (render.debugTimestamp || 0) >= 500) {
var text = "";
text += "fps: " + Math.round(engine.timing.fps) + space;
if (engine.metrics.extended) {
text += "delta: " + engine.timing.delta.toFixed(3) + space;
text += "correction: " + engine.timing.correction.toFixed(3) + space;
text += "bodies: " + bodies.length + space;
if (engine.broadphase.controller === Grid)
text += "buckets: " + engine.metrics.buckets + space;
text += "\n";
text += "collisions: " + engine.metrics.collisions + space;
text += "pairs: " + engine.pairs.list.length + space;
text += "broad: " + engine.metrics.broadEff + space;
text += "mid: " + engine.metrics.midEff + space;
text += "narrow: " + engine.metrics.narrowEff + space;
}
render.debugString = text;
render.debugTimestamp = engine.timing.timestamp;
}
if (render.debugString) {
c.font = "12px Arial";
if (options.wireframes) {
c.fillStyle = 'rgba(255,255,255,0.5)';
} else {
c.fillStyle = 'rgba(0,0,0,0.5)';
}
var split = render.debugString.split('\n');
for (var i = 0; i < split.length; i++) {
c.fillText(split[i], 50, 50 + i * 18);
}
}
};
/**
* Description
* @method constraints
* @param {constraint[]} constraints
* @param {RenderingContext} context
*/
Render.constraints = function(constraints, context) {
var c = context;
for (var i = 0; i < constraints.length; i++) {
var constraint = constraints[i];
if (!constraint.render.visible || !constraint.pointA || !constraint.pointB)
continue;
var bodyA = constraint.bodyA,
bodyB = constraint.bodyB;
if (bodyA) {
c.beginPath();
c.moveTo(bodyA.position.x + constraint.pointA.x, bodyA.position.y + constraint.pointA.y);
} else {
c.beginPath();
c.moveTo(constraint.pointA.x, constraint.pointA.y);
}
if (bodyB) {
c.lineTo(bodyB.position.x + constraint.pointB.x, bodyB.position.y + constraint.pointB.y);
} else {
c.lineTo(constraint.pointB.x, constraint.pointB.y);
}
c.lineWidth = constraint.render.lineWidth;
c.strokeStyle = constraint.render.strokeStyle;
c.stroke();
}
};
/**
* Description
* @method bodyShadows
* @param {engine} engine
* @param {body[]} bodies
* @param {RenderingContext} context
*/
Render.bodyShadows = function(engine, bodies, context) {
var c = context,
render = engine.render,
options = render.options;
for (var i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (!body.render.visible)
continue;
if (body.circleRadius) {
c.beginPath();
c.arc(body.position.x, body.position.y, body.circleRadius, 0, 2 * Math.PI);
c.closePath();
} else {
c.beginPath();
c.moveTo(body.vertices[0].x, body.vertices[0].y);
for (var j = 1; j < body.vertices.length; j++) {
c.lineTo(body.vertices[j].x, body.vertices[j].y);
}
c.closePath();
}
var distanceX = body.position.x - render.options.width * 0.5,
distanceY = body.position.y - render.options.height * 0.2,
distance = Math.abs(distanceX) + Math.abs(distanceY);
c.shadowColor = 'rgba(0,0,0,0.15)';
c.shadowOffsetX = 0.05 * distanceX;
c.shadowOffsetY = 0.05 * distanceY;
c.shadowBlur = 1 + 12 * Math.min(1, distance / 1000);
c.fill();
c.shadowColor = null;
c.shadowOffsetX = null;
c.shadowOffsetY = null;
c.shadowBlur = null;
}
};
/**
* Description
* @method bodies
* @param {engine} engine
* @param {body[]} bodies
* @param {RenderingContext} context
*/
Render.bodies = function(engine, bodies, context) {
var c = context,
render = engine.render,
options = render.options,
i;
for (i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (!body.render.visible)
continue;
if (body.render.sprite && !options.wireframes) {
// body sprite
var sprite = body.render.sprite,
texture = _getTexture(render, sprite.texture);
if (options.showSleeping && body.isSleeping)
c.globalAlpha = 0.5;
c.translate(body.position.x, body.position.y);
c.rotate(body.angle);
c.drawImage(texture, texture.width * -0.5 * sprite.xScale, texture.height * -0.5 * sprite.yScale,
texture.width * sprite.xScale, texture.height * sprite.yScale);
// revert translation, hopefully faster than save / restore
c.rotate(-body.angle);
c.translate(-body.position.x, -body.position.y);
if (options.showSleeping && body.isSleeping)
c.globalAlpha = 1;
} else {
// body polygon
if (body.circleRadius) {
c.beginPath();
c.arc(body.position.x, body.position.y, body.circleRadius, 0, 2 * Math.PI);
} else {
c.beginPath();
c.moveTo(body.vertices[0].x, body.vertices[0].y);
for (var j = 1; j < body.vertices.length; j++) {
c.lineTo(body.vertices[j].x, body.vertices[j].y);
}
c.closePath();
}
if (!options.wireframes) {
if (options.showSleeping && body.isSleeping) {
c.fillStyle = Common.shadeColor(body.render.fillStyle, 50);
} else {
c.fillStyle = body.render.fillStyle;
}
c.lineWidth = body.render.lineWidth;
c.strokeStyle = body.render.strokeStyle;
c.fill();
c.stroke();
} else {
c.lineWidth = 1;
c.strokeStyle = '#bbb';
if (options.showSleeping && body.isSleeping)
c.strokeStyle = 'rgba(255,255,255,0.2)';
c.stroke();
}
}
}
};
/**
* Optimised method for drawing body wireframes in one pass
* @method bodyWireframes
* @param {engine} engine
* @param {body[]} bodies
* @param {RenderingContext} context
*/
Render.bodyWireframes = function(engine, bodies, context) {
var c = context,
i,
j;
c.beginPath();
for (i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (!body.render.visible)
continue;
c.moveTo(body.vertices[0].x, body.vertices[0].y);
for (j = 1; j < body.vertices.length; j++) {
c.lineTo(body.vertices[j].x, body.vertices[j].y);
}
c.lineTo(body.vertices[0].x, body.vertices[0].y);
}
c.lineWidth = 1;
c.strokeStyle = '#bbb';
c.stroke();
};
/**
* Draws body bounds
* @method bodyBounds
* @param {engine} engine
* @param {body[]} bodies
* @param {RenderingContext} context
*/
Render.bodyBounds = function(engine, bodies, context) {
var c = context,
render = engine.render,
options = render.options;
c.beginPath();
for (var i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (body.render.visible)
c.rect(body.bounds.min.x, body.bounds.min.y, body.bounds.max.x - body.bounds.min.x, body.bounds.max.y - body.bounds.min.y);
}
if (options.wireframes) {
c.strokeStyle = 'rgba(255,255,255,0.08)';
} else {
c.strokeStyle = 'rgba(0,0,0,0.1)';
}
c.lineWidth = 1;
c.stroke();
};
/**
* Draws body angle indicators and axes
* @method bodyAxes
* @param {engine} engine
* @param {body[]} bodies
* @param {RenderingContext} context
*/
Render.bodyAxes = function(engine, bodies, context) {
var c = context,
render = engine.render,
options = render.options,
i,
j;
c.beginPath();
for (i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (!body.render.visible)
continue;
if (options.showAxes) {
// render all axes
for (j = 0; j < body.axes.length; j++) {
var axis = body.axes[j];
c.moveTo(body.position.x, body.position.y);
c.lineTo(body.position.x + axis.x * 20, body.position.y + axis.y * 20);
}
} else {
// render a single axis indicator
c.moveTo(body.position.x, body.position.y);
c.lineTo((body.vertices[0].x + body.vertices[body.vertices.length-1].x) / 2,
(body.vertices[0].y + body.vertices[body.vertices.length-1].y) / 2);
}
}
if (options.wireframes) {
c.strokeStyle = 'indianred';
} else {
c.strokeStyle = 'rgba(0,0,0,0.3)';
}
c.lineWidth = 1;
c.stroke();
};
/**
* Draws body positions
* @method bodyPositions
* @param {engine} engine
* @param {body[]} bodies
* @param {RenderingContext} context
*/
Render.bodyPositions = function(engine, bodies, context) {
var c = context,
render = engine.render,
options = render.options,
body,
i;
c.beginPath();
// render current positions
for (i = 0; i < bodies.length; i++) {
body = bodies[i];
if (body.render.visible) {
c.arc(body.position.x, body.position.y, 3, 0, 2 * Math.PI, false);
c.closePath();
}
}
if (options.wireframes) {
c.fillStyle = 'indianred';
} else {
c.fillStyle = 'rgba(0,0,0,0.5)';
}
c.fill();
c.beginPath();
// render previous positions
for (i = 0; i < bodies.length; i++) {
body = bodies[i];
if (body.render.visible) {
c.arc(body.positionPrev.x, body.positionPrev.y, 2, 0, 2 * Math.PI, false);
c.closePath();
}
}
c.fillStyle = 'rgba(255,165,0,0.8)';
c.fill();
};
/**
* Draws body velocity
* @method bodyVelocity
* @param {engine} engine
* @param {body[]} bodies
* @param {RenderingContext} context
*/
Render.bodyVelocity = function(engine, bodies, context) {
var c = context,
render = engine.render,
options = render.options;
c.beginPath();
for (var i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (!body.render.visible)
continue;
c.moveTo(body.position.x, body.position.y);
c.lineTo(body.position.x + (body.position.x - body.positionPrev.x) * 2, body.position.y + (body.position.y - body.positionPrev.y) * 2);
}
c.lineWidth = 3;
c.strokeStyle = 'cornflowerblue';
c.stroke();
};
/**
* Draws body ids
* @method bodyIds
* @param {engine} engine
* @param {body[]} bodies
* @param {RenderingContext} context
*/
Render.bodyIds = function(engine, bodies, context) {
var c = context;
for (var i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (!body.render.visible)
continue;
c.font = "12px Arial";
c.fillStyle = 'rgba(255,255,255,0.5)';
c.fillText(body.id, body.position.x + 10, body.position.y - 10);
}
};
/**
* Description
* @method collisions
* @param {engine} engine
* @param {pair[]} pairs
* @param {RenderingContext} context
*/
Render.collisions = function(engine, pairs, context) {
var c = context,
options = engine.render.options,
pair,
collision,
i,
j;
c.beginPath();
// render collision positions
for (i = 0; i < pairs.length; i++) {
pair = pairs[i];
collision = pair.collision;
for (j = 0; j < pair.activeContacts.length; j++) {
var contact = pair.activeContacts[j],
vertex = contact.vertex;
c.rect(vertex.x - 1.5, vertex.y - 1.5, 3.5, 3.5);
}
}
if (options.wireframes) {
c.fillStyle = 'rgba(255,255,255,0.7)';
} else {
c.fillStyle = 'orange';
}
c.fill();
c.beginPath();
// render collision normals
for (i = 0; i < pairs.length; i++) {
pair = pairs[i];
collision = pair.collision;
if (pair.activeContacts.length > 0) {
var normalPosX = pair.activeContacts[0].vertex.x,
normalPosY = pair.activeContacts[0].vertex.y;
if (pair.activeContacts.length === 2) {
normalPosX = (pair.activeContacts[0].vertex.x + pair.activeContacts[1].vertex.x) / 2;
normalPosY = (pair.activeContacts[0].vertex.y + pair.activeContacts[1].vertex.y) / 2;
}
c.moveTo(normalPosX - collision.normal.x * 8, normalPosY - collision.normal.y * 8);
c.lineTo(normalPosX, normalPosY);
}
}
if (options.wireframes) {
c.strokeStyle = 'rgba(255,165,0,0.7)';
} else {
c.strokeStyle = 'orange';
}
c.lineWidth = 1;
c.stroke();
};
/**
* Description
* @method grid
* @param {engine} engine
* @param {grid} grid
* @param {RenderingContext} context
*/
Render.grid = function(engine, grid, context) {
var c = context,
options = engine.render.options;
if (options.wireframes) {
c.strokeStyle = 'rgba(255,180,0,0.1)';
} else {
c.strokeStyle = 'rgba(255,180,0,0.5)';
}
c.beginPath();
var bucketKeys = Common.keys(grid.buckets);
for (var i = 0; i < bucketKeys.length; i++) {
var bucketId = bucketKeys[i];
if (grid.buckets[bucketId].length < 2)
continue;
var region = bucketId.split(',');
c.rect(0.5 + parseInt(region[0], 10) * grid.bucketWidth,
0.5 + parseInt(region[1], 10) * grid.bucketHeight,
grid.bucketWidth,
grid.bucketHeight);
}
c.lineWidth = 1;
c.stroke();
};
/**
* Description
* @method _createCanvas
* @private
* @param {} width
* @param {} height
* @return canvas
*/
var _createCanvas = function(width, height) {
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.oncontextmenu = function() { return false; };
canvas.onselectstart = function() { return false; };
return canvas;
};
/**
* Gets the requested texture (an Image) via its path
* @method _getTexture
* @private
* @param {render} render
* @param {string} imagePath
* @return {Image} texture
*/
var _getTexture = function(render, imagePath) {
var image = render.textures[imagePath];
if (image)
return image;
image = render.textures[imagePath] = new Image();
image.src = imagePath;
return image;
};
})();