Skip to content

Instantly share code, notes, and snippets.

@davay42
Forked from taylor8294/Matter.RenderSVG.js
Created May 26, 2024 05:08
Show Gist options
  • Save davay42/710e87b9e8e675c2a21f68ad86b42123 to your computer and use it in GitHub Desktop.
Save davay42/710e87b9e8e675c2a21f68ad86b42123 to your computer and use it in GitHub Desktop.
The `RenderSVG` module is an SVG alternative to the Matter.js built-in canvas-based renderer.
/**
* Overwrite `Common.isElement` to allow for SVG elements also
*/
Matter.Common.isElement = function(obj) {
try {
return obj instanceof HTMLElement || obj instanceof SVGElement;
} catch (e) {
return (typeof obj === "object") &&
(obj.nodeType === 1) && (typeof obj.style === "object") &&
(typeof obj.ownerDocument === "object");
}
};
/**
* The `Matter.RenderSVG` module is an SVG alternative to Matter's built-in
* canvas-based renderer.
*
* @class RenderSVG
* @requires gsap/TweenMax Uses the 'static.set' function to move SVG elements each render cycle,
* this dependency could be removed by implementing the transform setting
* but TweenMax has some nice features like setting transformOrigin relative
* to the SVG element itself, and all transforms are translated into a single
* matrix transform for consistency.
*/
var RenderSVG = {};
//module.exports = RenderSVG;
(function() {
var { Body, Bounds, Composite, Common, Events, Vector, Vertices } = Matter;
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;
}
/**
* Creates a new SVG renderer
* @method create
* @param {object} options
* @return {RenderSVG} A new renderer
*/
RenderSVG.create = function(options) {
var defaults = {
controller: RenderSVG,
engine: null,
element: null,
svg: null,
container: null,
spriteContainer: null,
frameRequestId: null,
options: {
width: 800,
height: 600,
background: '#18181d',
wireframeBackground: '#222',
hasBounds: true,
enabled: true,
wireframes: true,
showContextMenu: false,
closeConstraintPath: false,
mouseConstraint: true,
//@TODO Options below here are currently not supported
showSleeping: false,
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)
if (render.options.background === 'transparent') render.options.background = 'none';
if (!options.engine)
Common.warn('No "render.engine" passed, no world to render');
render.engine = options.engine;
render.mouse = options.mouse;
render.element = options.element;
render.svg = options.svg;
render.container = options.container;
render.spriteContainer = options.spriteContainer;
// SVG and SVG parent node
if (!Common.isElement(render.svg) || render.svg.tagName.toLowerCase() !== 'svg') {
if (typeof render.svg === 'string')
render.svg = document.querySelector(render.svg)
if (!Common.isElement(render.svg) || render.svg.tagName.toLowerCase() !== 'svg') {
render.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
render.svg.setAttribute("width", render.options.width + "px")
render.svg.setAttribute("height", render.options.height + "px")
}
}
render.svg = _checkSvgXmlns(render.svg)
if (!document.body.contains(render.svg)) {
if (!Common.isElement(render.element)) {
if (typeof render.element === 'string')
render.element = document.querySelector(render.element)
if (!Common.isElement(render.element))
render.element = document.body
}
render.element.appendChild(render.svg)
if (!document.body.contains(render.element)) {
render.body.appendChild(render.element)
}
} else {
if (!Common.isElement(render.element)) {
if (typeof render.element === 'string')
render.element = document.querySelector(render.element)
if (!Common.isElement(render.element))
render.element = render.svg.parentNode
else
render.element.appendChild(render.svg)
}
}
// SVG stage container element
if (!Common.isElement(render.container)) {
if (typeof render.container === 'string')
render.container = document.querySelector(render.container)
if (!Common.isElement(render.container))
render.container = render.svg
}
if (!render.svg.contains(render.container))
render.svg.appendChild(render.container)
// SVG sprite container element
if (!Common.isElement(render.spriteContainer)) {
if (typeof render.spriteContainer === 'string')
render.spriteContainer = document.querySelector(render.spriteContainer)
if (!Common.isElement(render.spriteContainer))
render.spriteContainer = render.container
}
if (!render.svg.contains(render.spriteContainer))
render.svg.appendChild(render.spriteContainer)
// Bounds (stops DOM updates for elements outside this range)
render.bounds = render.bounds || {
min: {
x: -10,
y: -10
},
max: {
x: render.options.width + 10,
y: render.options.height + 10
}
};
// caches
render.sprites = {};
render.domNodes = {};
// prevent menus on canvas
if (!render.options.showContextMenu) {
render.svg.oncontextmenu = function(e) {
e.preventDefault();
return false;
};
render.svg.onselectstart = function(e) {
e.preventDefault();
return false;
};
}
if (render.options.mouseConstraint) {
// =============================================
// Add mouse control
// =============================================
/* Built in doesn't work for SVG renderer
* const mouse = Mouse.create(render.svg)
* var mConstraint = MouseConstraint.create({mouse:mouse})
* World.add(world,mConstraint)
* So using own implementation.
*/
/*Events.on(render.engine, 'beforeUpdate', function() {
});*/
render.mouseConstraint = {
lastLoc: null,
draggedBody: null,
wasStatic: null,
dx: 0,
dy: 0
};
render.svg.addEventListener('mousedown', function(evt) {
let loc = RenderSVG.screenToSVGCoords(render, {
x: evt.clientX,
y: evt.clientY
}),
mouseConstraint = render.mouseConstraint,
bodies = Composite.allBodies(render.engine.world);
//Sort bodies so we drag the smallest body our mouse is over
bodies = bodies.sort((a, b) => {
let ab = (a.bounds.max.x - a.bounds.min.x) * (a.bounds.max.y - a.bounds.min.y)
let bb = (b.bounds.max.x - b.bounds.min.x) * (b.bounds.max.y - b.bounds.min.y)
return ab < bb ? -1 : ab > bb ? 1 : 0
})
for (let i = 0; i < bodies.length; i++) {
let body = bodies[i];
if(Vertices.contains(body.vertices, loc)){
mouseConstraint.draggedBody = body;
mouseConstraint.dx = body.position.x - loc.x
mouseConstraint.dy = body.position.y - loc.y
if (!body.isStatic)
Body.setStatic(body, true);
else
mouseConstraint.wasStatic = true
break;
}
}
mouseConstraint.lastLoc = loc;
});
render.svg.addEventListener('mousemove', function(evt) {
let mouseConstraint = render.mouseConstraint
if (!mouseConstraint.draggedBody) return false;
let loc = RenderSVG.screenToSVGCoords(render, {
x: evt.clientX,
y: evt.clientY
})
Body.setPosition(mouseConstraint.draggedBody, {
x: loc.x + mouseConstraint.dx,
y: loc.y + mouseConstraint.dy
});
if (mouseConstraint.lastLoc && !mouseConstraint.wasStatic)
Body.setVelocity(mouseConstraint.draggedBody, {
x: Math.max(Math.min(Math.sign(loc.x - mouseConstraint.lastLoc.x) * Math.pow(Math.abs(loc.x - mouseConstraint.lastLoc.x), 1), 15), -15),
y: Math.max(Math.min(Math.sign(loc.y - mouseConstraint.lastLoc.y) * Math.pow(Math.abs(loc.y - mouseConstraint.lastLoc.y), 1), 15), -15)
});
mouseConstraint.lastLoc = loc;
});
let endDrag = function(evt) {
let mouseConstraint = render.mouseConstraint
if (mouseConstraint.draggedBody && !mouseConstraint.wasStatic)
Body.setStatic(mouseConstraint.draggedBody, false);
render.mouseConstraint = {
lastLoc: null,
draggedBody: null,
wasStatic: null,
dx: 0,
dy: 0
};
}
render.svg.addEventListener('mouseleave', endDrag);
render.svg.addEventListener('mouseup', endDrag);
}
return render;
};
function _checkSvgXmlns(svg) {
if (svg.namespaceURI !== 'http://www.w3.org/2000/svg') {
let newSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
newSvg.setAttributeNS("http://www.w3.org/2000/xmlns", "xmlns:xlink", "http://www.w3.org/1999/xlink");
while (svg.children.length) {
newSvg.appendChild(svg.children[0])
}
newSvg.id = svg.id
svg.classList.forEach((cls, i, arr) => {
newSvg.classList.add(cls)
})
newSvg.setAttribute("width", svg.width.baseVal.value + "px")
newSvg.setAttribute("height", svg.height.baseVal.value + "px")
svg.parentNode.appendChild(newSvg)
svg.parentNode.removeChild(svg)
return newSvg
}
if (svg.lookupNamespaceURI('xlink') !== 'http://www.w3.org/1999/xlink') {
svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink');
}
return svg
}
/**
* Continuously updates the SVG elements on the `requestAnimationFrame` event.
* @method run
* @param {render} render
*/
RenderSVG.run = function(render) {
(function loop(time) {
render.frameRequestId = _requestAnimationFrame(loop);
if (render.options.enabled)
RenderSVG.world(render);
})();
};
/**
* Ends execution of `Render.run` on the given `render`, by canceling the animation frame request event loop.
* @method stop
* @param {render} render
*/
RenderSVG.stop = function(render) {
_cancelAnimationFrame(render.frameRequestId);
};
/**
* Clears the scene
* @method clear
* @param {RenderSVG} render
*/
RenderSVG.clear = function(render) {
// clear stage container
while (render.container.children.length) {
render.container.removeChild(render.container.children[0]);
}
// clear sprites
while (render.spriteContainer.children.length) {
render.spriteContainer.removeChild(render.spriteContainer.children[0]);
}
// clear caches
render.sprites = {};
render.domNodes = {};
// reset background state
render.currentBackground = null;
// reset any transforms
};
/**
* Sets the background of the SVG
* @method setBackground
* @param {RenderSVG} render
* @param {string} background
*/
RenderSVG.setBackground = function(render, background) {
if (render.currentBackground !== background) {
render.svg.style.backgroundColor = background;
render.currentBackground = background;
}
};
/**
* Render the world
* @method world
* @param {engine} engine
*/
RenderSVG.world = function(render) {
var engine = render.engine,
world = engine.world,
container = render.container,
options = render.options,
bodies = Composite.allBodies(world),
allConstraints = Composite.allConstraints(world),
constraints = [],
i;
if (options.wireframes)
RenderSVG.setBackground(render, options.wireframeBackground);
else
RenderSVG.setBackground(render, options.background);
// handle bounds
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;
if (options.hasBounds) {
// Set bodies that are currently in view
for (i = 0; i < bodies.length; i++) {
let body = bodies[i];
body.render.visible = Bounds.overlaps(body.bounds, render.bounds)
}
// filter out constraints that are not in view
for (i = 0; i < allConstraints.length; i++) {
let 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
} else {
constraints = allConstraints;
}
for (i = 0; i < bodies.length; i++)
RenderSVG.body(render, bodies[i]);
for (i = 0; i < constraints.length; i++)
RenderSVG.constraint(render, constraints[i]);
};
/**
* Render the given constraint - currently a simple line
* @method constraint
* @param {engine} engine
* @param {constraint} constraint
* @todo Render a visual clue on the "stiffess" of the constraint via zig-zag lines
* @todo Allow sprites for constraints (assume unit length, render with transform="rotation(..) scale(...)")
*/
RenderSVG.constraint = function(render, constraint) {
var engine = render.engine,
bodyA = constraint.bodyA,
bodyB = constraint.bodyB,
pointA = constraint.pointA,
pointB = constraint.pointB,
container = render.container,
constraintRender = constraint.render,
domId = 'c-' + constraint.id,
domNode = render.domNodes[domId];
// don't render if constraint does not have two end points
if (!constraintRender.visible || !constraint.pointA || !constraint.pointB) {
if (domNode && domNode.parentNode)
domNode.parentNode.removeChild(domNode);
return;
}
// initialise constraint node if not existing
if (!domNode)
render.domNodes[domId] = domNode = document.createElementNS(render.svg.namespaceURI, 'path');
// add to stage container if not already there
if (Common.indexOf(container.children, domNode) === -1)
container.appendChild(domNode);
// render the constraint on every update, since they can change dynamically
var ax = pointA.x + (bodyA ? bodyA.position.x : 0),
ay = pointA.y + (bodyA ? bodyA.position.y : 0),
bx = pointB.x + (bodyB ? bodyB.position.x : 0),
by = pointB.y + (bodyB ? bodyB.position.y : 0),
cx = (ax + bx) / 2,
cy = (ay + by) / 2
ax -= cx, ay -= cy, bx -= cx, by -= cy
var d = 'M ' + [ax.toFixed(1), ay.toFixed(1), bx.toFixed(1), by.toFixed(1)].join(' ') + (constraintRender.closePath === false ? '' : (constraintRender.closePath || render.options.closeConstraintPath ? ' Z' : ''))
domNode.setAttributeNS(null, 'd', d)
TweenMax.set(domNode, {
x: cx.toFixed(1),
y: cy.toFixed(1),
fill: render.options.wireframes ? 'none' : (constraintRender.fillStyle || 'none'),
strokeWidth: render.options.wireframes ? 2 : (constraintRender.lineWidth || 0),
stroke: render.options.wireframes ? '#bbb' : (constraintRender.strokeStyle || '#bbb'),
opacity: render.options.wireframes ? 1 : (constraintRender.opacity || 1)
})
};
/**
* Render the given body / sprite
* @method body
* @param {engine} engine
* @param {body} body
* @todo Add Sleeping/Debug/Broadphase/Bounds/Velocity/Collisions/Axes/Positions/AngleIndicator/Ids/Shadows indicators on wireframes
*/
RenderSVG.body = function(render, body) {
var engine = render.engine,
bodyRender = body.render,
sprite,
spriteContainer = render.spriteContainer;
if (bodyRender.sprite && bodyRender.sprite.texture) {
let spriteId = 'b-' + body.id;
sprite = render.sprites[spriteId];
if (!sprite) {
if (Common.isElement(bodyRender.sprite.texture))
sprite = bodyRender.sprite.texture
else if (typeof bodyRender.sprite.texture === 'string')
sprite = document.querySelector(bodyRender.sprite.texture)
if (sprite) {
if (sprite.parentNode && sprite.parentNode.tagName === "defs") {
let spriteCopy = document.createElementNS(render.svg.namespaceURI, 'use')
spriteCopy.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#' + sprite.id)
spriteCopy.setAttributeNS(null, 'x', 0)
spriteCopy.setAttributeNS(null, 'y', 0)
spriteContainer.appendChild(spriteCopy)
sprite = spriteCopy
}
render.sprites[spriteId] = sprite
_saveCOMRef(render, body, sprite)
_setTransformOrigin(body, sprite)
}
}
}
if (sprite) {
if (!bodyRender.visible || bodyRender.sprite.visible === false) {
if (sprite.parentNode)
sprite.parentNode.removeChild(sprite)
return
} else if (!render.svg.contains(sprite))
spriteContainer.appendChild(sprite)
// update body sprite
let deg = 180 / Math.PI
TweenMax.set(sprite, {
x: body.position.x.toFixed(1) + (bodyRender.sprite.xOffset || 0),
y: body.position.y.toFixed(1) + (bodyRender.sprite.yOffset || 0),
rotation: (body.angle * deg).toFixed(1),
scaleX: bodyRender.sprite.xScale || 1,
scaleY: bodyRender.sprite.yScale || 1
})
if (render.options.wireframes) {
if (!bodyRender.sprite.origStyle) {
bodyRender.sprite.origStyle = {
fillStyle: sprite.style.fill || bodyRender.fillStyle,
lineWidth: sprite.style.strokeWidth || bodyRender.lineWidth,
strokeStyle: sprite.style.stroke || bodyRender.strokeStyle,
opacity: sprite.style.opacity || bodyRender.opacity
}
TweenMax.set(sprite, {
fill: 'none',
strokeWidth: 2,
stroke: '#bbb',
opacity: 1
})
}
} else if (bodyRender.sprite.origStyle) {
TweenMax.set(sprite, {
fill: bodyRender.sprite.origStyle.fillStyle,
strokeWidth: bodyRender.sprite.origStyle.lineWidth,
stroke: bodyRender.sprite.origStyle.strokeStyle,
opacity: bodyRender.sprite.origStyle.opacity
})
delete bodyRender.sprite.origStyle
}
} else {
let domId = 'b-' + body.id,
domNode = render.domNodes[domId],
container = render.container;
if (!bodyRender.visible) {
if (domNode && domNode.parentNode)
domNode.parentNode.removeChild(domNode);
return;
}
// initialise body node if not existing
if (!domNode)
domNode = render.domNodes[domId] = _createBodyDomNode(render, body);
// add to staging container if not already there
if (Common.indexOf(container.children, domNode) === -1)
container.appendChild(domNode);
// update body node
let deg = 180 / Math.PI
TweenMax.set(domNode, {
x: body.position.x.toFixed(1),
y: body.position.y.toFixed(1),
rotation: (body.angle * deg).toFixed(1),
fill: render.options.wireframes ? 'none' : (bodyRender.fillStyle || 'none'),
strokeWidth: render.options.wireframes ? 2 : (bodyRender.lineWidth || 0),
stroke: render.options.wireframes ? '#bbb' : (bodyRender.strokeStyle || '#bbb'),
opacity: render.options.wireframes ? 1 : (bodyRender.opacity || 1)
})
}
};
/**
* Turns an array of vertices into an SVG d attribute string
* @param {array} vertices
* @param {boolean} leaveOpen If true the path will not be closed
*/
function _dAttr(vertices, leaveOpen) {
return 'M ' + vertices.map(v => v.x.toFixed(1) + ' ' + v.y.toFixed(1)).join(' ') + (leaveOpen ? '' : ' Z')
}
/**
* Makes a clone of the given body but with the centre of mass at (0,0)
* @param {body} body
* @param {boolean} recurs (Whether the current call is a recursive call or not, only for internal use)
*/
function _cloneAtOrigin(body, recurs) {
var angleBefore, posBefore, result
if (!recurs) {
angleBefore = body.angle, posBefore = Object.assign({}, body.position)
Body.setAngle(body, 0)
Body.setPosition(body, {
x: 0,
y: 0
})
}
if (body.parts.length > 1) {
let clonedParts = []
body.parts.forEach((part, i, parts) => {
if (i == 0) return; //ignore self-referential first part
clonedParts.push(_cloneAtOrigin(part, true));
})
result = Body.create({
parts: clonedParts
});
} else {
result = Bodies.fromVertices(body.position.x, body.position.y, [body.vertices], {
isStatic: body.isStatic,
render: body.render
}, undefined, 0, 0)
}
if (!recurs) {
Body.setAngle(body, angleBefore)
Body.setPosition(body, posBefore)
}
return result
}
/**
* Saves where the bodies center of mass is relative to the visual centre of the element
* @param {body} bodyAtOrigin
* @param {node} domNodeAtOrigin
*/
function _saveCOMRef(render, bodyAtOrigin, domNodeAtOrigin) {
var clientBox = domNodeAtOrigin.getBoundingClientRect()
var COMOffset = RenderSVG.screenToSVGCoords(render, {
x: clientBox.x + clientBox.width / 2,
y: clientBox.y + clientBox.height / 2
})
COMOffset.x *= -1
COMOffset.y *= -1
bodyAtOrigin._COM = COMOffset
return COMOffset
}
/**
* Sets the origin for all transforms at the bodies centre of mass
* @param {body} body
* @param {node} domNode
*/
function _setTransformOrigin(body, domNode) {
var w = body.bounds.max.x - body.bounds.min.x,
h = body.bounds.max.y - body.bounds.min.y,
tx = 100 * (w / 2 + body._COM.x) / w,
ty = 100 * (h / 2 + body._COM.y) / h
TweenMax.set(domNode, {
transformOrigin: tx + '% ' + ty + '%'
})
}
/**
* Creates a body DOM node
* @method _createBodyDomNode
* @private
* @param {RenderSVG} render
* @param {body} body
* @return {node} domNode
* @deprecated
*/
var _createBodyDomNode = function(render, body, recurs) {
var domNode
body = recurs ? body : _cloneAtOrigin(body)
if (body.parts.length > 1) {
domNode = document.createElementNS(render.svg.namespaceURI, "g");
body.parts.forEach((part, i, parts) => {
if (i == 0) return; //ignore self-referential first part
var partNode = _createBodyDomNode(render, part, true);
domNode.appendChild(partNode)
})
} else {
domNode = document.createElementNS(render.svg.namespaceURI, "path");
domNode.setAttributeNS(null, 'd', _dAttr(body.vertices))
}
if (!recurs) {
_saveCOMRef(render, body, domNode)
_setTransformOrigin(body, domNode)
}
return domNode;
};
//Coordinate transformer
RenderSVG.screenToSVGCoords = function(render, pos) {
let ptTransformer = render.svg.createSVGPoint()
ptTransformer.x = pos.x;
ptTransformer.y = pos.y;
return ptTransformer.matrixTransform(render.svg.getScreenCTM().inverse());
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment