UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

1,459 lines (1,225 loc) 61.3 kB
/** * The `Matter.Render` module is a simple HTML5 canvas based renderer for visualising instances of `Matter.Engine`. * It is intended for development and debugging purposes, but may also be suitable for simple games. * It includes a number of drawing options including wireframe, vector with support for sprites and viewports. * * @class Render */ var Render = {}; module.exports = Render; var Body = require('../body/Body'); var Common = require('../core/Common'); var Composite = require('../body/Composite'); var Bounds = require('../geometry/Bounds'); var Events = require('../core/Events'); var Vector = require('../geometry/Vector'); // var Mouse = require('../core/Mouse'); (function() { var _requestAnimationFrame, _cancelAnimationFrame; if (typeof window !== 'undefined') { _requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || function(callback){ window.setTimeout(function() { callback(Common.now()); }, 1000 / 60); }; _cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame || window.msCancelAnimationFrame; } Render._goodFps = 30; Render._goodDelta = 1000 / 60; /** * Creates a new renderer. The options parameter is an object that specifies any properties you wish to override the defaults. * All properties have default values, and many are pre-calculated automatically based on other properties. * See the properties section below for detailed information on what you can pass via the `options` object. * @method create * @param {object} [options] * @return {render} A new renderer */ Render.create = function(options) { var defaults = { engine: null, element: null, canvas: null, mouse: null, frameRequestId: null, timing: { historySize: 60, delta: 0, deltaHistory: [], lastTime: 0, lastTimestamp: 0, lastElapsed: 0, timestampElapsed: 0, timestampElapsedHistory: [], engineDeltaHistory: [], engineElapsedHistory: [], elapsedHistory: [] }, options: { width: 800, height: 600, pixelRatio: 1, background: '#14151f', wireframeBackground: '#14151f', wireframeStrokeStyle: '#bbb', hasBounds: !!options.bounds, enabled: true, wireframes: true, showSleeping: true, showDebug: false, showStats: false, showPerformance: false, showBounds: false, showVelocity: false, showCollisions: false, showSeparations: false, showAxes: false, showPositions: false, showAngleIndicator: false, showIds: false, showVertexNumbers: false, showConvexHulls: false, showInternalEdges: false, showMousePosition: false } }; var render = Common.extend(defaults, options); if (render.canvas) { render.canvas.width = render.options.width || render.canvas.width; render.canvas.height = render.options.height || render.canvas.height; } render.mouse = options.mouse; render.engine = options.engine; render.canvas = render.canvas || _createCanvas(render.options.width, render.options.height); render.context = render.canvas.getContext('2d'); render.textures = {}; render.bounds = render.bounds || { min: { x: 0, y: 0 }, max: { x: render.canvas.width, y: render.canvas.height } }; // for temporary back compatibility only render.controller = Render; render.options.showBroadphase = false; if (render.options.pixelRatio !== 1) { Render.setPixelRatio(render, render.options.pixelRatio); } if (Common.isElement(render.element)) { render.element.appendChild(render.canvas); } return render; }; /** * Continuously updates the render canvas on the `requestAnimationFrame` event. * @method run * @param {render} render */ Render.run = function(render) { (function loop(time){ render.frameRequestId = _requestAnimationFrame(loop); _updateTiming(render, time); Render.world(render, time); if (render.options.showStats || render.options.showDebug) { Render.stats(render, render.context, time); } if (render.options.showPerformance || render.options.showDebug) { Render.performance(render, render.context, time); } })(); }; /** * Ends execution of `Render.run` on the given `render`, by canceling the animation frame request event loop. * @method stop * @param {render} render */ Render.stop = function(render) { _cancelAnimationFrame(render.frameRequestId); }; /** * Sets the pixel ratio of the renderer and updates the canvas. * To automatically detect the correct ratio, pass the string `'auto'` for `pixelRatio`. * @method setPixelRatio * @param {render} render * @param {number} pixelRatio */ Render.setPixelRatio = function(render, pixelRatio) { var options = render.options, canvas = render.canvas; if (pixelRatio === 'auto') { pixelRatio = _getPixelRatio(canvas); } options.pixelRatio = pixelRatio; canvas.setAttribute('data-pixel-ratio', pixelRatio); canvas.width = options.width * pixelRatio; canvas.height = options.height * pixelRatio; canvas.style.width = options.width + 'px'; canvas.style.height = options.height + 'px'; }; /** * Sets the render `width` and `height`. * * Updates the canvas accounting for `render.options.pixelRatio`. * * Updates the bottom right render bound `render.bounds.max` relative to the provided `width` and `height`. * The top left render bound `render.bounds.min` isn't changed. * * Follow this call with `Render.lookAt` if you need to change the render bounds. * * See also `Render.setPixelRatio`. * @method setSize * @param {render} render * @param {number} width The width (in CSS pixels) * @param {number} height The height (in CSS pixels) */ Render.setSize = function(render, width, height) { render.options.width = width; render.options.height = height; render.bounds.max.x = render.bounds.min.x + width; render.bounds.max.y = render.bounds.min.y + height; if (render.options.pixelRatio !== 1) { Render.setPixelRatio(render, render.options.pixelRatio); } else { render.canvas.width = width; render.canvas.height = height; } }; /** * Positions and sizes the viewport around the given object bounds. * Objects must have at least one of the following properties: * - `object.bounds` * - `object.position` * - `object.min` and `object.max` * - `object.x` and `object.y` * @method lookAt * @param {render} render * @param {object[]} objects * @param {vector} [padding] * @param {bool} [center=true] */ Render.lookAt = function(render, objects, padding, center) { center = typeof center !== 'undefined' ? center : true; objects = Common.isArray(objects) ? objects : [objects]; padding = padding || { x: 0, y: 0 }; // find bounds of all objects var bounds = { min: { x: Infinity, y: Infinity }, max: { x: -Infinity, y: -Infinity } }; for (var i = 0; i < objects.length; i += 1) { var object = objects[i], min = object.bounds ? object.bounds.min : (object.min || object.position || object), max = object.bounds ? object.bounds.max : (object.max || object.position || object); if (min && max) { if (min.x < bounds.min.x) bounds.min.x = min.x; if (max.x > bounds.max.x) bounds.max.x = max.x; if (min.y < bounds.min.y) bounds.min.y = min.y; if (max.y > bounds.max.y) bounds.max.y = max.y; } } // find ratios var width = (bounds.max.x - bounds.min.x) + 2 * padding.x, height = (bounds.max.y - bounds.min.y) + 2 * padding.y, viewHeight = render.canvas.height, viewWidth = render.canvas.width, outerRatio = viewWidth / viewHeight, innerRatio = width / height, scaleX = 1, scaleY = 1; // find scale factor if (innerRatio > outerRatio) { scaleY = innerRatio / outerRatio; } else { scaleX = outerRatio / innerRatio; } // enable bounds render.options.hasBounds = true; // position and size render.bounds.min.x = bounds.min.x; render.bounds.max.x = bounds.min.x + width * scaleX; render.bounds.min.y = bounds.min.y; render.bounds.max.y = bounds.min.y + height * scaleY; // center if (center) { render.bounds.min.x += width * 0.5 - (width * scaleX) * 0.5; render.bounds.max.x += width * 0.5 - (width * scaleX) * 0.5; render.bounds.min.y += height * 0.5 - (height * scaleY) * 0.5; render.bounds.max.y += height * 0.5 - (height * scaleY) * 0.5; } // padding render.bounds.min.x -= padding.x; render.bounds.max.x -= padding.x; render.bounds.min.y -= padding.y; render.bounds.max.y -= padding.y; // update mouse if (render.mouse) { // Mouse.setScale(render.mouse, { // x: (render.bounds.max.x - render.bounds.min.x) / render.canvas.width, // y: (render.bounds.max.y - render.bounds.min.y) / render.canvas.height // }); // Mouse.setOffset(render.mouse, render.bounds.min); } }; /** * Applies viewport transforms based on `render.bounds` to a render context. * @method startViewTransform * @param {render} render */ Render.startViewTransform = function(render) { var boundsWidth = render.bounds.max.x - render.bounds.min.x, boundsHeight = render.bounds.max.y - render.bounds.min.y, boundsScaleX = boundsWidth / render.options.width, boundsScaleY = boundsHeight / render.options.height; render.context.setTransform( render.options.pixelRatio / boundsScaleX, 0, 0, render.options.pixelRatio / boundsScaleY, 0, 0 ); render.context.translate(-render.bounds.min.x, -render.bounds.min.y); }; /** * Resets all transforms on the render context. * @method endViewTransform * @param {render} render */ Render.endViewTransform = function(render) { render.context.setTransform(render.options.pixelRatio, 0, 0, render.options.pixelRatio, 0, 0); }; /** * Renders the given `engine`'s `Matter.World` object. * This is the entry point for all rendering and should be called every time the scene changes. * @method world * @param {render} render */ Render.world = function(render, time) { var startTime = Common.now(), engine = render.engine, world = engine.world, canvas = render.canvas, context = render.context, options = render.options, timing = render.timing; var allBodies = Composite.allBodies(world), allConstraints = Composite.allConstraints(world), background = options.wireframes ? options.wireframeBackground : options.background, bodies = [], constraints = [], i; var event = { timestamp: engine.timing.timestamp }; Events.trigger(render, 'beforeRender', event); // apply background if it has changed if (render.currentBackground !== background) _applyBackground(render, 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'; // handle bounds if (options.hasBounds) { // filter out bodies that are not in view for (i = 0; i < allBodies.length; i++) { var body = allBodies[i]; if (Bounds.overlaps(body.bounds, render.bounds)) bodies.push(body); } // filter out constraints that are not in view for (i = 0; i < allConstraints.length; i++) { var constraint = allConstraints[i], bodyA = constraint.bodyA, bodyB = constraint.bodyB, pointAWorld = constraint.pointA, pointBWorld = constraint.pointB; if (bodyA) pointAWorld = Vector.add(bodyA.position, constraint.pointA); if (bodyB) pointBWorld = Vector.add(bodyB.position, constraint.pointB); if (!pointAWorld || !pointBWorld) continue; if (Bounds.contains(render.bounds, pointAWorld) || Bounds.contains(render.bounds, pointBWorld)) constraints.push(constraint); } // transform the view Render.startViewTransform(render); // update mouse if (render.mouse) { Mouse.setScale(render.mouse, { x: (render.bounds.max.x - render.bounds.min.x) / render.options.width, y: (render.bounds.max.y - render.bounds.min.y) / render.options.height }); Mouse.setOffset(render.mouse, render.bounds.min); } } else { constraints = allConstraints; bodies = allBodies; if (render.options.pixelRatio !== 1) { render.context.setTransform(render.options.pixelRatio, 0, 0, render.options.pixelRatio, 0, 0); } } if (!options.wireframes || (engine.enableSleeping && options.showSleeping)) { // fully featured rendering of bodies Render.bodies(render, bodies, context); } else { if (options.showConvexHulls) Render.bodyConvexHulls(render, bodies, context); // optimised method for wireframes only Render.bodyWireframes(render, bodies, context); } if (options.showBounds) Render.bodyBounds(render, bodies, context); if (options.showAxes || options.showAngleIndicator) Render.bodyAxes(render, bodies, context); if (options.showPositions) Render.bodyPositions(render, bodies, context); if (options.showVelocity) Render.bodyVelocity(render, bodies, context); if (options.showIds) Render.bodyIds(render, bodies, context); if (options.showSeparations) Render.separations(render, engine.pairs.list, context); if (options.showCollisions) Render.collisions(render, engine.pairs.list, context); if (options.showVertexNumbers) Render.vertexNumbers(render, bodies, context); if (options.showMousePosition) Render.mousePosition(render, render.mouse, context); Render.constraints(constraints, context); if (options.hasBounds) { // revert view transforms Render.endViewTransform(render); } Events.trigger(render, 'afterRender', event); // log the time elapsed computing this update timing.lastElapsed = Common.now() - startTime; }; /** * Renders statistics about the engine and world useful for debugging. * @private * @method stats * @param {render} render * @param {RenderingContext} context * @param {Number} time */ Render.stats = function(render, context, time) { var engine = render.engine, world = engine.world, bodies = Composite.allBodies(world), parts = 0, width = 55, height = 44, x = 0, y = 0; // count parts for (var i = 0; i < bodies.length; i += 1) { parts += bodies[i].parts.length; } // sections var sections = { 'Part': parts, 'Body': bodies.length, 'Cons': Composite.allConstraints(world).length, 'Comp': Composite.allComposites(world).length, 'Pair': engine.pairs.list.length }; // background context.fillStyle = '#0e0f19'; context.fillRect(x, y, width * 5.5, height); context.font = '12px Arial'; context.textBaseline = 'top'; context.textAlign = 'right'; // sections for (var key in sections) { var section = sections[key]; // label context.fillStyle = '#aaa'; context.fillText(key, x + width, y + 8); // value context.fillStyle = '#eee'; context.fillText(section, x + width, y + 26); x += width; } }; /** * Renders engine and render performance information. * @private * @method performance * @param {render} render * @param {RenderingContext} context */ Render.performance = function(render, context) { var engine = render.engine, timing = render.timing, deltaHistory = timing.deltaHistory, elapsedHistory = timing.elapsedHistory, timestampElapsedHistory = timing.timestampElapsedHistory, engineDeltaHistory = timing.engineDeltaHistory, engineUpdatesHistory = timing.engineUpdatesHistory, engineElapsedHistory = timing.engineElapsedHistory, lastEngineUpdatesPerFrame = engine.timing.lastUpdatesPerFrame, lastEngineDelta = engine.timing.lastDelta; var deltaMean = _mean(deltaHistory), elapsedMean = _mean(elapsedHistory), engineDeltaMean = _mean(engineDeltaHistory), engineUpdatesMean = _mean(engineUpdatesHistory), engineElapsedMean = _mean(engineElapsedHistory), timestampElapsedMean = _mean(timestampElapsedHistory), rateMean = (timestampElapsedMean / deltaMean) || 0, neededUpdatesPerFrame = Math.round(deltaMean / lastEngineDelta), fps = (1000 / deltaMean) || 0; var graphHeight = 4, gap = 12, width = 60, height = 34, x = 10, y = 69; // background context.fillStyle = '#0e0f19'; context.fillRect(0, 50, gap * 5 + width * 6 + 22, height); // show FPS Render.status( context, x, y, width, graphHeight, deltaHistory.length, Math.round(fps) + ' fps', fps / Render._goodFps, function(i) { return (deltaHistory[i] / deltaMean) - 1; } ); // show engine delta Render.status( context, x + gap + width, y, width, graphHeight, engineDeltaHistory.length, lastEngineDelta.toFixed(2) + ' dt', Render._goodDelta / lastEngineDelta, function(i) { return (engineDeltaHistory[i] / engineDeltaMean) - 1; } ); Render.status( context, x + (gap + width) * 2, y, width, graphHeight, engineUpdatesHistory.length, lastEngineUpdatesPerFrame + ' upf', Math.pow(Common.clamp((engineUpdatesMean / neededUpdatesPerFrame) || 1, 0, 1), 4), function(i) { return (engineUpdatesHistory[i] / engineUpdatesMean) - 1; } ); // show engine update time Render.status( context, x + (gap + width) * 3, y, width, graphHeight, engineElapsedHistory.length, engineElapsedMean.toFixed(2) + ' ut', 1 - (lastEngineUpdatesPerFrame * engineElapsedMean / Render._goodFps), function(i) { return (engineElapsedHistory[i] / engineElapsedMean) - 1; } ); // show render time Render.status( context, x + (gap + width) * 4, y, width, graphHeight, elapsedHistory.length, elapsedMean.toFixed(2) + ' rt', 1 - (elapsedMean / Render._goodFps), function(i) { return (elapsedHistory[i] / elapsedMean) - 1; } ); // show effective speed Render.status( context, x + (gap + width) * 5, y, width, graphHeight, timestampElapsedHistory.length, rateMean.toFixed(2) + ' x', rateMean * rateMean * rateMean, function(i) { return (((timestampElapsedHistory[i] / deltaHistory[i]) / rateMean) || 0) - 1; } ); }; /** * Renders a label, indicator and a chart. * @private * @method status * @param {RenderingContext} context * @param {number} x * @param {number} y * @param {number} width * @param {number} height * @param {number} count * @param {string} label * @param {string} indicator * @param {function} plotY */ Render.status = function(context, x, y, width, height, count, label, indicator, plotY) { // background context.strokeStyle = '#888'; context.fillStyle = '#444'; context.lineWidth = 1; context.fillRect(x, y + 7, width, 1); // chart context.beginPath(); context.moveTo(x, y + 7 - height * Common.clamp(0.4 * plotY(0), -2, 2)); for (var i = 0; i < width; i += 1) { context.lineTo(x + i, y + 7 - (i < count ? height * Common.clamp(0.4 * plotY(i), -2, 2) : 0)); } context.stroke(); // indicator context.fillStyle = 'hsl(' + Common.clamp(25 + 95 * indicator, 0, 120) + ',100%,60%)'; context.fillRect(x, y - 7, 4, 4); // label context.font = '12px Arial'; context.textBaseline = 'middle'; context.textAlign = 'right'; context.fillStyle = '#eee'; context.fillText(label, x + width, y - 5); }; /** * Description * @private * @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, start, end; if (bodyA) { start = Vector.add(bodyA.position, constraint.pointA); } else { start = constraint.pointA; } if (constraint.render.type === 'pin') { c.beginPath(); c.arc(start.x, start.y, 3, 0, 2 * Math.PI); c.closePath(); } else { if (bodyB) { end = Vector.add(bodyB.position, constraint.pointB); } else { end = constraint.pointB; } c.beginPath(); c.moveTo(start.x, start.y); if (constraint.render.type === 'spring') { var delta = Vector.sub(end, start), normal = Vector.perp(Vector.normalise(delta)), coils = Math.ceil(Common.clamp(constraint.length / 5, 12, 20)), offset; for (var j = 1; j < coils; j += 1) { offset = j % 2 === 0 ? 1 : -1; c.lineTo( start.x + delta.x * (j / coils) + normal.x * offset * 4, start.y + delta.y * (j / coils) + normal.y * offset * 4 ); } } c.lineTo(end.x, end.y); } if (constraint.render.lineWidth) { c.lineWidth = constraint.render.lineWidth; c.strokeStyle = constraint.render.strokeStyle; c.stroke(); } if (constraint.render.anchors) { c.fillStyle = constraint.render.strokeStyle; c.beginPath(); c.arc(start.x, start.y, 3, 0, 2 * Math.PI); c.arc(end.x, end.y, 3, 0, 2 * Math.PI); c.closePath(); c.fill(); } } }; /** * Description * @private * @method bodies * @param {render} render * @param {body[]} bodies * @param {RenderingContext} context */ Render.bodies = function(render, bodies, context) { var c = context, engine = render.engine, options = render.options, showInternalEdges = options.showInternalEdges || !options.wireframes, body, part, i, k; for (i = 0; i < bodies.length; i++) { body = bodies[i]; if (!body.render.visible) continue; // handle compound parts for (k = body.parts.length > 1 ? 1 : 0; k < body.parts.length; k++) { part = body.parts[k]; if (!part.render.visible) continue; if (options.showSleeping && body.isSleeping) { c.globalAlpha = 0.5 * part.render.opacity; } else if (part.render.opacity !== 1) { c.globalAlpha = part.render.opacity; } if (part.render.sprite && part.render.sprite.texture && !options.wireframes) { // part sprite var sprite = part.render.sprite, texture = _getTexture(render, sprite.texture); c.translate(part.position.x, part.position.y); c.rotate(part.angle); c.drawImage( texture, texture.width * -sprite.xOffset * sprite.xScale, texture.height * -sprite.yOffset * sprite.yScale, texture.width * sprite.xScale, texture.height * sprite.yScale ); // revert translation, hopefully faster than save / restore c.rotate(-part.angle); c.translate(-part.position.x, -part.position.y); } else { // part polygon if (part.circleRadius) { c.beginPath(); c.arc(part.position.x, part.position.y, part.circleRadius, 0, 2 * Math.PI); } else { c.beginPath(); c.moveTo(part.vertices[0].x, part.vertices[0].y); for (var j = 1; j < part.vertices.length; j++) { if (!part.vertices[j - 1].isInternal || showInternalEdges) { c.lineTo(part.vertices[j].x, part.vertices[j].y); } else { c.moveTo(part.vertices[j].x, part.vertices[j].y); } if (part.vertices[j].isInternal && !showInternalEdges) { c.moveTo(part.vertices[(j + 1) % part.vertices.length].x, part.vertices[(j + 1) % part.vertices.length].y); } } c.lineTo(part.vertices[0].x, part.vertices[0].y); c.closePath(); } if (!options.wireframes) { c.fillStyle = part.render.fillStyle; if (part.render.lineWidth) { c.lineWidth = part.render.lineWidth; c.strokeStyle = part.render.strokeStyle; c.stroke(); } c.fill(); } else { c.lineWidth = 1; c.strokeStyle = render.options.wireframeStrokeStyle; c.stroke(); } } c.globalAlpha = 1; } } }; /** * Optimised method for drawing body wireframes in one pass * @private * @method bodyWireframes * @param {render} render * @param {body[]} bodies * @param {RenderingContext} context */ Render.bodyWireframes = function(render, bodies, context) { var c = context, showInternalEdges = render.options.showInternalEdges, body, part, i, j, k; c.beginPath(); // render all bodies for (i = 0; i < bodies.length; i++) { body = bodies[i]; if (!body.render.visible) continue; // handle compound parts for (k = body.parts.length > 1 ? 1 : 0; k < body.parts.length; k++) { part = body.parts[k]; c.moveTo(part.vertices[0].x, part.vertices[0].y); for (j = 1; j < part.vertices.length; j++) { if (!part.vertices[j - 1].isInternal || showInternalEdges) { c.lineTo(part.vertices[j].x, part.vertices[j].y); } else { c.moveTo(part.vertices[j].x, part.vertices[j].y); } if (part.vertices[j].isInternal && !showInternalEdges) { c.moveTo(part.vertices[(j + 1) % part.vertices.length].x, part.vertices[(j + 1) % part.vertices.length].y); } } c.lineTo(part.vertices[0].x, part.vertices[0].y); } } c.lineWidth = 1; c.strokeStyle = render.options.wireframeStrokeStyle; c.stroke(); }; /** * Optimised method for drawing body convex hull wireframes in one pass * @private * @method bodyConvexHulls * @param {render} render * @param {body[]} bodies * @param {RenderingContext} context */ Render.bodyConvexHulls = function(render, bodies, context) { var c = context, body, part, i, j, k; c.beginPath(); // render convex hulls for (i = 0; i < bodies.length; i++) { body = bodies[i]; if (!body.render.visible || body.parts.length === 1) 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 = 'rgba(255,255,255,0.2)'; c.stroke(); }; /** * Renders body vertex numbers. * @private * @method vertexNumbers * @param {render} render * @param {body[]} bodies * @param {RenderingContext} context */ Render.vertexNumbers = function(render, bodies, context) { var c = context, i, j, k; for (i = 0; i < bodies.length; i++) { var parts = bodies[i].parts; for (k = parts.length > 1 ? 1 : 0; k < parts.length; k++) { var part = parts[k]; for (j = 0; j < part.vertices.length; j++) { c.fillStyle = 'rgba(255,255,255,0.2)'; c.fillText(i + '_' + j, part.position.x + (part.vertices[j].x - part.position.x) * 0.8, part.position.y + (part.vertices[j].y - part.position.y) * 0.8); } } } }; /** * Renders mouse position. * @private * @method mousePosition * @param {render} render * @param {mouse} mouse * @param {RenderingContext} context */ Render.mousePosition = function(render, mouse, context) { var c = context; c.fillStyle = 'rgba(255,255,255,0.8)'; c.fillText(mouse.position.x + ' ' + mouse.position.y, mouse.position.x + 5, mouse.position.y - 5); }; /** * Draws body bounds * @private * @method bodyBounds * @param {render} render * @param {body[]} bodies * @param {RenderingContext} context */ Render.bodyBounds = function(render, bodies, context) { var c = context, engine = render.engine, options = render.options; c.beginPath(); for (var i = 0; i < bodies.length; i++) { var body = bodies[i]; if (body.render.visible) { var parts = bodies[i].parts; for (var j = parts.length > 1 ? 1 : 0; j < parts.length; j++) { var part = parts[j]; c.rect(part.bounds.min.x, part.bounds.min.y, part.bounds.max.x - part.bounds.min.x, part.bounds.max.y - part.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 * @private * @method bodyAxes * @param {render} render * @param {body[]} bodies * @param {RenderingContext} context */ Render.bodyAxes = function(render, bodies, context) { var c = context, engine = render.engine, options = render.options, part, i, j, k; c.beginPath(); for (i = 0; i < bodies.length; i++) { var body = bodies[i], parts = body.parts; if (!body.render.visible) continue; if (options.showAxes) { // render all axes for (j = parts.length > 1 ? 1 : 0; j < parts.length; j++) { part = parts[j]; for (k = 0; k < part.axes.length; k++) { var axis = part.axes[k]; c.moveTo(part.position.x, part.position.y); c.lineTo(part.position.x + axis.x * 20, part.position.y + axis.y * 20); } } } else { for (j = parts.length > 1 ? 1 : 0; j < parts.length; j++) { part = parts[j]; for (k = 0; k < part.axes.length; k++) { // render a single axis indicator c.moveTo(part.position.x, part.position.y); c.lineTo((part.vertices[0].x + part.vertices[part.vertices.length-1].x) / 2, (part.vertices[0].y + part.vertices[part.vertices.length-1].y) / 2); } } } } if (options.wireframes) { c.strokeStyle = 'indianred'; c.lineWidth = 1; } else { c.strokeStyle = 'rgba(255, 255, 255, 0.4)'; c.globalCompositeOperation = 'overlay'; c.lineWidth = 2; } c.stroke(); c.globalCompositeOperation = 'source-over'; }; /** * Draws body positions * @private * @method bodyPositions * @param {render} render * @param {body[]} bodies * @param {RenderingContext} context */ Render.bodyPositions = function(render, bodies, context) { var c = context, engine = render.engine, options = render.options, body, part, i, k; c.beginPath(); // render current positions for (i = 0; i < bodies.length; i++) { body = bodies[i]; if (!body.render.visible) continue; // handle compound parts for (k = 0; k < body.parts.length; k++) { part = body.parts[k]; c.arc(part.position.x, part.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 * @private * @method bodyVelocity * @param {render} render * @param {body[]} bodies * @param {RenderingContext} context */ Render.bodyVelocity = function(render, bodies, context) { var c = context; c.beginPath(); for (var i = 0; i < bodies.length; i++) { var body = bodies[i]; if (!body.render.visible) continue; var velocity = Body.getVelocity(body); c.moveTo(body.position.x, body.position.y); c.lineTo(body.position.x + velocity.x, body.position.y + velocity.y); } c.lineWidth = 3; c.strokeStyle = 'cornflowerblue'; c.stroke(); }; /** * Draws body ids * @private * @method bodyIds * @param {render} render * @param {body[]} bodies * @param {RenderingContext} context */ Render.bodyIds = function(render, bodies, context) { var c = context, i, j; for (i = 0; i < bodies.length; i++) { if (!bodies[i].render.visible) continue; var parts = bodies[i].parts; for (j = parts.length > 1 ? 1 : 0; j < parts.length; j++) { var part = parts[j]; c.font = "12px Arial"; c.fillStyle = 'rgba(255,255,255,0.5)'; c.fillText(part.id, part.position.x + 10, part.position.y - 10); } } }; /** * Description * @private * @method collisions * @param {render} render * @param {pair[]} pairs * @param {RenderingContext} context */ Render.collisions = function(render, pairs, context) { var c = context, options = render.options, pair, collision, corrected, bodyA, bodyB, i, j; c.beginPath(); // render collision positions for (i = 0; i < pairs.length; i++) { pair = pairs[i]; if (!pair.isActive) continue; collision = pair.collision; for (j = 0; j < pair.contactCount; j++) { var contact = pair.contacts[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]; if (!pair.isActive) continue; collision = pair.collision; if (pair.contactCount > 0) { var normalPosX = pair.contacts[0].vertex.x, normalPosY = pair.contacts[0].vertex.y; if (pair.contactCount === 2) { normalPosX = (pair.contacts[0].vertex.x + pair.contacts[1].vertex.x) / 2; normalPosY = (pair.contacts[0].vertex.y + pair.contacts[1].vertex.y) / 2; } if (collision.bodyB === collision.supports[0].body || collision.bodyA.isStatic === true) { c.moveTo(normalPosX - collision.normal.x * 8, normalPosY - collision.normal.y * 8); } else { 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 * @private * @method separations * @param {render} render * @param {pair[]} pairs * @param {RenderingContext} context */ Render.separations = function(render, pairs, context) { var c = context, options = render.options, pair, collision, corrected, bodyA, bodyB, i, j; c.beginPath(); // render separations for (i = 0; i < pairs.length; i++) { pair = pairs[i]; if (!pair.isActive) continue; collision = pair.collision; bodyA = collision.bodyA; bodyB = collision.bodyB; var k = 1; if (!bodyB.isStatic && !bodyA.isStatic) k = 0.5; if (bodyB.isStatic) k = 0; c.moveTo(bodyB.position.x, bodyB.position.y); c.lineTo(bodyB.position.x - collision.penetration.x * k, bodyB.position.y - collision.penetration.y * k); k = 1; if (!bodyB.isStatic && !bodyA.isStatic) k = 0.5; if (bodyA.isStatic) k = 0; c.moveTo(bodyA.position.x, bodyA.position.y); c.lineTo(bodyA.position.x + collision.penetration.x * k, bodyA.position.y + collision.penetration.y * k); } if (options.wireframes) { c.strokeStyle = 'rgba(255,165,0,0.5)'; } else { c.strokeStyle = 'orange'; } c.stroke(); }; /** * Description * @private * @method inspector * @param {inspector} inspector * @param {RenderingContext} context */ Render.inspector = function(inspector, context) { var engine = inspector.engine, selected = inspector.selected, render = inspector.render, options = render.options, bounds; if (options.hasBounds) { var boundsWidth = render.bounds.max.x - render.bounds.min.x, boundsHeight = render.bounds.max.y - render.bounds.min.y, boundsScaleX = boundsWidth / render.options.width, boundsScaleY = boundsHeight / render.options.height; context.scale(1 / boundsScaleX, 1 / boundsScaleY); context.translate(-render.bounds.min.x, -render.bounds.min.y); } for (var i = 0; i < selected.length; i++) { var item = selected[i].data; context.translate(0.5, 0.5); context.lineWidth = 1; context.strokeStyle = 'rgba(255,165,0,0.9)'; context.setLineDash([1,2]); switch (item.type) { case 'body': // render body selections bounds = item.bounds; context.beginPath(); context.rect(Math.floor(bounds.min.x - 3), Math.floor(bounds.min.y - 3), Math.floor(bounds.max.x - bounds.min.x + 6), Math.floor(bounds.max.y - bounds.min.y + 6)); context.closePath(); context.stroke(); break; case 'constraint': // render constraint selections var point = item.pointA; if (item.bodyA) point = item.pointB; context.beginPath(); context.arc(point.x, point.y, 10, 0, 2 * Math.PI); context.closePath(); context.stroke(); break; } context.setLineDash([]); context.translate(-0.5, -0.5); } // render selection region if (inspector.selectStart !== null) { context.translate(0.5, 0.5); context.lineWidth = 1; context.strokeStyle = 'rgba(255,165,0,0.6)'; context.fillStyle = 'rgba(255,165,0,0.1)'; bounds = inspector.selectBounds; context.beginPath(); context.rect(Math.floor(bounds.min.x), Math.floor(bounds.min.y), Math.floor(bounds.max.x - bounds.min.x), Math.floor(bounds.max.y - bounds.min.y)); context.closePath(); context.stroke(); context.fill(); context.translate(-0.5, -0.5); } if (options.hasBounds) context.setTransform(1, 0, 0, 1, 0, 0); }; /** * Updates render timing. * @method _updateTiming * @private * @param {render} render * @param {number} time */ var _updateTiming = function(render, time) { var engine = render.engine, timing = render.timing, historySize = timing.historySize, timestamp = engine.timing.timestamp; timing.delta = time - timing.lastTime || Render._goodDelta; timing.lastTime = time; timing.timestampElapsed = timestamp - timing.lastTimestamp || 0; timing.lastTimestamp = timestamp; timing.deltaHistory.unshift(timing.delta); timing.deltaHistory.length = Math.min(timing.deltaHistory.length, historySize); timing.engineDeltaHistory.unshift(engine.timing.lastDelta); timing.engineDeltaHistory.length = Math.min(timing.engineDeltaHistory.length, historySize); timing.timestampElapsedHistory.unshift(timing.timestampElapsed); timing.timestampElapsedHistory.length = Math.min(timing.timestampElapsedHistory.length, historySize); timing.engineUpdatesHistory.unshift(engine.timing.lastUpdatesPerFrame); timing.engineUpdatesHistory.length = Math.min(timing.engineUpdatesHistory.length, historySize); timing.engineElapsedHistory.unshift(engine.timing.lastElapsed); timing.engineElapsedHistory.length = Math.min(timing.engineElapsedHistory.length, historySize); timing.elapsedHistory.unshift(timing.lastElapsed); timing.elapsedHistory.length = Math.min(timing.elapsedHistory.length, historySize); }; /** * Returns the mean value of the given numbers. * @method _mean * @private * @param {Number[]} values * @return {Number} the mean of given values */ var _mean = function(values) { va