react-matter-js
Version:
React adapter for the Matter.js physics engine
929 lines (776 loc) • 24.1 kB
JavaScript
import React, { memo, useCallback, useState, useEffect, useContext, createContext, useRef, createRef, cloneElement, Fragment } from 'react';
import Matter from 'matter-js';
import { ValueObject, shallow } from 'tuplerone';
import { useWindowSize as useWindowSize$1 } from '@react-hook/window-size';
import { css } from 'emotion';
import 'pathseg';
var valueMemo = function valueMemo(component) {
return memo(component, valueCompare);
};
var keyFilter = function keyFilter(_ref) {
var key = _ref[0];
return !key.startsWith('_');
};
var valueCompare = function valueCompare(prev, next) {
return ValueObject(prev, keyFilter) === ValueObject(next, keyFilter);
};
var relX = function relX(size) {
return function (engine) {
return engine.render.options.width * size;
};
};
var relY = function relY(size) {
return function (engine) {
return engine.render.options.height * size;
};
};
var getSize = function getSize(size, engine) {
return typeof size === 'function' ? size(engine) : size;
};
var mapEntries = function mapEntries(object, fn) {
return Object.fromEntries(Object.entries(object).map(fn));
};
var useMapSizes = function useMapSizes(sizes) {
var engine = useEngine();
return mapEntries(sizes, function (_ref) {
var key = _ref[0],
value = _ref[1];
return [key, getSize(value, engine)];
});
};
var useRerender = function useRerender() {
var _useState = useState(),
updateState = _useState[1];
return useCallback(function () {
return updateState(Symbol());
}, []);
};
var useWindowSize = function useWindowSize() {
var _windowSize$useWindow = useWindowSize$1(),
width = _windowSize$useWindow[0],
height = _windowSize$useWindow[1];
return {
width: width,
height: height
};
};
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Date.prototype.toString.call(Reflect.construct(Date, [], function () {}));
return true;
} catch (e) {
return false;
}
}
function _construct(Parent, args, Class) {
if (isNativeReflectConstruct()) {
_construct = Reflect.construct;
} else {
_construct = function _construct(Parent, args, Class) {
var a = [null];
a.push.apply(a, args);
var Constructor = Function.bind.apply(Parent, a);
var instance = new Constructor();
if (Class) _setPrototypeOf(instance, Class.prototype);
return instance;
};
}
return _construct.apply(null, arguments);
}
function _isNativeFunction(fn) {
return Function.toString.call(fn).indexOf("[native code]") !== -1;
}
function _wrapNativeSuper(Class) {
var _cache = typeof Map === "function" ? new Map() : undefined;
_wrapNativeSuper = function _wrapNativeSuper(Class) {
if (Class === null || !_isNativeFunction(Class)) return Class;
if (typeof Class !== "function") {
throw new TypeError("Super expression must either be null or a function");
}
if (typeof _cache !== "undefined") {
if (_cache.has(Class)) return _cache.get(Class);
_cache.set(Class, Wrapper);
}
function Wrapper() {
return _construct(Class, arguments, _getPrototypeOf(this).constructor);
}
Wrapper.prototype = Object.create(Class.prototype, {
constructor: {
value: Wrapper,
enumerable: false,
writable: true,
configurable: true
}
});
return _setPrototypeOf(Wrapper, Class);
};
return _wrapNativeSuper(Class);
}
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _taggedTemplateLiteralLoose(strings, raw) {
if (!raw) {
raw = strings.slice(0);
}
strings.raw = raw;
return strings;
}
var DefaultMap =
/*#__PURE__*/
function (_Map) {
_inheritsLoose(DefaultMap, _Map);
function DefaultMap(init) {
var _this;
_this = _Map.call(this) || this;
_this.init = init;
return _this;
}
var _proto = DefaultMap.prototype;
_proto.get = function get(key) {
if (!this.has(key)) {
var value = this.init(key);
this.set(key, value);
return value;
}
return _Map.prototype.get.call(this, key);
};
return DefaultMap;
}(
/*#__PURE__*/
_wrapNativeSuper(Map));
var TrackSet =
/*#__PURE__*/
function (_Set) {
_inheritsLoose(TrackSet, _Set);
function TrackSet(subs) {
var _this;
if (subs === void 0) {
subs = new Set();
}
_this = _Set.call(this) || this;
_this.subs = subs;
return _this;
}
var _proto = TrackSet.prototype;
_proto.add = function add(a) {
var _this2 = this;
_Set.prototype.add.call(this, a);
this.subs.forEach(function (sub) {
return sub(_this2);
});
return this;
};
_proto.delete = function _delete(a) {
var _this3 = this;
var result = _Set.prototype.delete.call(this, a);
if (result) {
this.subs.forEach(function (sub) {
return sub(_this3);
});
}
return result;
};
_proto.track = function track(sub) {
this.subs.add(sub);
};
_proto.untrack = function untrack(sub) {
this.subs.delete(sub);
};
_proto.clear = function clear() {
var _this4 = this;
_Set.prototype.clear.call(this);
this.subs.forEach(function (sub) {
return sub(_this4);
});
this.subs.clear();
};
return TrackSet;
}(
/*#__PURE__*/
_wrapNativeSuper(Set));
var trackCats = function trackCats(engine) {
var cats = new DefaultMap(function () {
return new TrackSet();
});
engine[catsKey] = cats;
var afterAdd = function afterAdd(_ref) {
var _body$catsKey;
var body = _ref.object;
if (!((_body$catsKey = body[catsKey]) === null || _body$catsKey === void 0 ? void 0 : _body$catsKey.length)) {
return;
}
body[catsKey].forEach(function (key) {
return void cats.get(key).add(body);
});
};
var afterRemove = function afterRemove(_ref2) {
var body = _ref2.object;
if (!body[catsKey]) {
return;
}
body[catsKey].forEach(function (key) {
return void cats.get(key).delete(body);
});
};
Matter.Events.on(engine.world, 'afterAdd', afterAdd);
Matter.Events.on(engine.world, 'afterRemove', afterRemove);
return function () {
Matter.Events.off(engine.world, 'afterAdd', afterAdd);
Matter.Events.off(engine.world, 'afterRemove', afterRemove);
cats.forEach(function (cat) {
return void cat.clear();
});
cats.clear();
};
};
var catsKey =
/*#__PURE__*/
Symbol('categories');
var useCat = function useCat(key) {
var engine = useEngine();
var rerender = useRerender();
var cat = engine[catsKey].get(key);
useEffect(function () {
cat.track(rerender);
return function () {
cat.untrack(rerender);
};
}, [cat, rerender]);
return cat;
};
var Engine = function Engine(_ref) {
var options = _ref.options,
children = _ref.children;
var _useState = useState(null),
engine = _useState[0],
setEngine = _useState[1];
useEffect(function () {
var engine = shallow(Matter.Engine.create(options));
setEngine(engine);
var clearCats = trackCats(engine);
return function () {
Matter.World.clear(engine.world, false);
Matter.Engine.clear(engine);
engine.enabled = false;
clearCats();
setEngine(null);
};
}, [options]);
return engine ? React.createElement(Provider, {
value: engine
}, children) : null;
};
var Engine$1 = /*#__PURE__*/
valueMemo(Engine);
var EngineContext =
/*#__PURE__*/
createContext(null);
var Provider = EngineContext.Provider;
var useEngine = function useEngine() {
return useContext(EngineContext);
};
function _templateObject() {
var data = _taggedTemplateLiteralLoose(["\n display: block;\n width: ", "px;\n position: relative;\n canvas {\n position: relative;\n display: block;\n z-index: 1;\n }\n "]);
_templateObject = function _templateObject() {
return data;
};
return data;
}
var Render = function Render(_ref) {
var options = _ref.options,
_ref$enableMouse = _ref.enableMouse,
enableMouse = _ref$enableMouse === void 0 ? false : _ref$enableMouse,
mouseConstraintOptions = _ref.mouseConstraintOptions,
children = _ref.children,
props = _objectWithoutPropertiesLoose(_ref, ["options", "enableMouse", "mouseConstraintOptions", "children"]);
var elementRef = useRef(null);
var engine = useEngine();
var _useState = useState(null),
render = _useState[0],
setRender = _useState[1];
var width = options.width;
useEffect(function () {
var render = shallow(Matter.Render.create({
element: elementRef.current,
engine: engine,
options: options
}));
engine.render = render;
setRender(render);
Matter.Render.run(render);
var runner = shallow(Matter.Runner.create());
Matter.Runner.run(runner, engine);
if (enableMouse || mouseConstraintOptions) {
var mouse = shallow(Matter.Mouse.create(render.canvas));
var mouseConstraint = shallow(Matter.MouseConstraint.create(engine, _extends({}, mouseConstraintOptions, {
mouse: mouse
})));
Matter.World.add(engine.world, mouseConstraint);
}
return function () {
Matter.Render.stop(render);
render.canvas.remove(); // @ts-ignore
render.canvas = null; // @ts-ignore
render.context = null;
Matter.Runner.stop(runner);
render.textures = {};
};
}, [engine, options, mouseConstraintOptions, enableMouse, setRender]);
return React.createElement("div", Object.assign({}, props, {
ref: elementRef,
className: css(_templateObject(), width)
}), render ? renderChildren(children, engine) : null);
};
var Render$1 = /*#__PURE__*/
valueMemo(Render);
var renderChildren = function renderChildren(children, engine) {
if (Array.isArray(children)) {
return children.map(function (child) {
return renderChildren(child, engine);
});
}
return typeof children === 'function' ? children(engine) : children;
};
var useClones = function useClones() {
var engine = useEngine();
var bodies = useCat(cloneKey);
var dom = pickEls(useCat(domKey));
var svg = pickEls(useCat(svgKey));
useEffect(function () {
var afterUpdate = function afterUpdate() {
bodies.forEach(function (body) {
if (body.isSleeping) {
return;
}
var _body$position = body.position,
x = _body$position.x,
y = _body$position.y;
var ref = body[cloneKey].ref; // TODO: ?
if (!ref.current) {
return;
}
ref.current.style.transform = "translate(" + x + "px, " + y + "px) rotate(" + body.angle + "rad)";
});
};
Matter.Events.on(engine, 'afterUpdate', afterUpdate);
return function () {
Matter.Events.off(engine, 'afterUpdate', afterUpdate);
};
}, [bodies, engine]);
return {
dom: dom,
svg: svg
};
};
var pickEls = function pickEls(cat) {
return Array.from(cat, function (body) {
return body[cloneKey].el;
});
};
var cloneKey =
/*#__PURE__*/
Symbol('clone');
var svgKey =
/*#__PURE__*/
Symbol('SVG clone');
var domKey =
/*#__PURE__*/
Symbol('DOM clone');
function _templateObject$1() {
var data = _taggedTemplateLiteralLoose(["\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n fill: #f60;\n"]);
_templateObject$1 = function _templateObject() {
return data;
};
return data;
}
var RenderClones = function RenderClones(_ref) {
var children = _ref.children,
_ref$options = _ref.options,
options = _ref$options === void 0 ? {} : _ref$options,
props = _objectWithoutPropertiesLoose(_ref, ["children", "options", "margin"]);
var width = options.width,
height = options.height;
var _useClones = useClones(),
dom = _useClones.dom,
svg = _useClones.svg;
return React.createElement(Render$1, Object.assign({}, props, {
options: options
}), React.createElement("div", {
className: cloneContainerStyle
}, dom), React.createElement("svg", {
viewBox: "0 0 " + width + " " + height,
className: cloneContainerStyle
}, svg), children);
};
var RenderClones$1 = /*#__PURE__*/
valueMemo(RenderClones);
var cloneContainerStyle =
/*#__PURE__*/
css(
/*#__PURE__*/
_templateObject$1());
var Body = function Body(_ref) {
var createBody = _ref.children,
_ref$cats = _ref.cats,
cats = _ref$cats === void 0 ? [] : _ref$cats,
bodyRef = _ref.bodyRef,
_ref$sizes = _ref.sizes,
sizes = _ref$sizes === void 0 ? {} : _ref$sizes;
var engine = useEngine();
useEffect(function () {
var body = shallow(createBody());
body[sizesKey] = sizes;
if (bodyRef) {
bodyRef.current = body;
}
body[catsKey] = cats;
if (body[cloneKey]) {
body[catsKey].push(body[cloneKey].key, cloneKey);
}
Matter.World.add(engine.world, body);
return function () {
Matter.World.remove(engine.world, body);
if (bodyRef) {
bodyRef.current = null;
}
};
}, [engine, createBody, cats, bodyRef, sizes]);
return null;
};
var Body$1 = /*#__PURE__*/
valueMemo(Body);
var sizesKey =
/*#__PURE__*/
Symbol('sizes');
var Rectangle = function Rectangle(_ref) {
var x = _ref.x,
y = _ref.y,
width = _ref.width,
height = _ref.height,
_ref$clone = _ref.clone,
clone = _ref$clone === void 0 ? false : _ref$clone,
options = _ref.options,
props = _objectWithoutPropertiesLoose(_ref, ["x", "y", "width", "height", "clone", "options"]);
var createBody = function createBody() {
var body = Matter.Bodies.rectangle(x, y, width, height, options);
if (clone) {
var ref = createRef();
var el = React.createElement("rect", {
x: -width / 2,
y: -height / 2,
width: width,
height: height,
ref: ref,
key: body.id
});
body[cloneKey] = {
key: svgKey,
ref: ref,
el: el
};
}
return body;
};
return React.createElement(Body$1, Object.assign({}, props), createBody);
};
var Rectangle$1 = /*#__PURE__*/
valueMemo(Rectangle);
var Circle = function Circle(_ref) {
var x = _ref.x,
y = _ref.y,
radius = _ref.radius,
_ref$clone = _ref.clone,
clone = _ref$clone === void 0 ? false : _ref$clone,
options = _ref.options,
cloneProps = _ref.cloneProps,
props = _objectWithoutPropertiesLoose(_ref, ["x", "y", "radius", "clone", "options", "cloneProps"]);
var sizes = useMapSizes({
x: x,
y: y,
radius: radius
});
var createBody = function createBody() {
var body = Matter.Bodies.circle(sizes.x, sizes.y, sizes.radius, options);
if (clone) {
var ref = createRef();
var el = React.createElement("circle", Object.assign({
cx: 0,
cy: 0,
r: sizes.radius,
ref: ref,
key: body.id
}, cloneProps));
body[cloneKey] = {
key: svgKey,
ref: ref,
el: el
};
}
return body;
};
return React.createElement(Body$1, Object.assign({}, props), createBody);
};
var Circle$1 = /*#__PURE__*/
valueMemo(Circle);
var px = function px(n) {
return n + "px";
};
var Vertices = function Vertices(_ref) {
var x = _ref.x,
y = _ref.y,
width = _ref.width,
height = _ref.height,
vertexSets = _ref.vertexSets,
_ref$options = _ref.options,
options = _ref$options === void 0 ? {} : _ref$options,
_ref$flagInternal = _ref.flagInternal,
flagInternal = _ref$flagInternal === void 0 ? false : _ref$flagInternal,
cloneID = _ref.cloneID,
_ref$cloneProps = _ref.cloneProps,
cloneProps = _ref$cloneProps === void 0 ? {} : _ref$cloneProps,
props = _objectWithoutPropertiesLoose(_ref, ["x", "y", "width", "height", "vertexSets", "options", "flagInternal", "cloneID", "cloneProps"]);
var createBody = function createBody() {
var body = Matter.Bodies.fromVertices(x, y, vertexSets, options, flagInternal);
var ref = createRef();
var _body$bounds = body.bounds,
min = _body$bounds.min,
max = _body$bounds.max;
var _width = max.x - min.x;
var _height = max.y - min.y;
var scale = Math.min(width / _width, height / _height);
Matter.Body.scale(body, scale, scale); // const viewBox = parseViewbox(
// document.querySelector(`#${cloneID}`).getAttribute("viewBox")
// );
// XXX: why?
var ratio = 1.4;
var scaledWidth = _width * scale * ratio;
var scaledHeight = _height * scale * ratio;
var el = React.createElement("g", {
ref: ref,
key: body.id
}, React.createElement("use", Object.assign({
xlinkHref: "#" + cloneID,
width: px(scaledWidth),
height: px(scaledHeight),
x: px(-scaledWidth / 2),
y: px(-scaledHeight / 2)
}, cloneProps)));
body[cloneKey] = {
key: svgKey,
ref: ref,
el: el
};
return body;
};
return React.createElement(Body$1, Object.assign({}, props), createBody);
};
var Vertices$1 = /*#__PURE__*/
valueMemo(Vertices);
var Constraint = function Constraint(_ref) {
var children = _ref.children,
length = _ref.length,
options = _objectWithoutPropertiesLoose(_ref, ["children", "length"]);
var engine = useEngine();
var bodyARef = useRef();
var bodyBRef = useRef();
useEffect(function () {
var bodyA = bodyARef.current;
var bodyB = bodyBRef.current;
var constraint = shallow(Matter.Constraint.create(_extends({
bodyA: bodyA,
bodyB: bodyB,
length: length
}, options)));
Matter.World.add(engine.world, constraint);
return function () {
Matter.World.remove(engine.world, constraint);
};
}, [options, engine, length]);
return [bodyARef, bodyBRef].map(function (bodyRef, key) {
return cloneElement(children[key], {
bodyRef: bodyRef,
key: key
});
});
};
var Constraint$1 = /*#__PURE__*/
memo(Constraint, valueCompare);
var Walls = function Walls(_ref) {
var options = _ref.options,
props = _objectWithoutPropertiesLoose(_ref, ["options"]);
var defaultProps = {
options: _extends({}, options, {
isStatic: true
})
};
var _useMapSizes = useMapSizes(pickSizes(props)),
x = _useMapSizes.x,
y = _useMapSizes.y,
width = _useMapSizes.width,
height = _useMapSizes.height,
wallWidth = _useMapSizes.wallWidth;
var top = _extends({}, defaultProps, {
x: x + width / 2,
y: y + wallWidth / 2,
width: width,
height: wallWidth
});
var bottom = _extends({}, defaultProps, {}, top, {
y: height - wallWidth / 2
});
var left = _extends({}, defaultProps, {
x: x + wallWidth / 2,
y: y + height / 2,
height: height,
width: wallWidth
});
var right = _extends({}, defaultProps, {}, left, {
x: width - wallWidth / 2
});
return React.createElement(Fragment, null, React.createElement(Rectangle$1, Object.assign({}, top)), React.createElement(Rectangle$1, Object.assign({}, bottom)), React.createElement(Rectangle$1, Object.assign({}, left)), React.createElement(Rectangle$1, Object.assign({}, right)));
};
var pickSizes = function pickSizes(_ref2) {
var x = _ref2.x,
y = _ref2.y,
width = _ref2.width,
height = _ref2.height,
_ref2$wallWidth = _ref2.wallWidth,
wallWidth = _ref2$wallWidth === void 0 ? 100 : _ref2$wallWidth;
return {
x: x,
y: y,
width: width,
height: height,
wallWidth: wallWidth
};
};
var Scene = function Scene(_ref) {
var _ref$width = _ref.width,
width = _ref$width === void 0 ? 720 : _ref$width,
_ref$height = _ref.height,
height = _ref$height === void 0 ? 540 : _ref$height,
_ref$pixelRatio = _ref.pixelRatio,
pixelRatio = _ref$pixelRatio === void 0 ? 'auto' : _ref$pixelRatio,
_ref$engineOptions = _ref.engineOptions,
engineOptions = _ref$engineOptions === void 0 ? {} : _ref$engineOptions,
_ref$rendererProps = _ref.rendererProps,
rendererProps = _ref$rendererProps === void 0 ? {} : _ref$rendererProps,
_ref$mouse = _ref.mouse,
mouse = _ref$mouse === void 0 ? true : _ref$mouse,
gravity = _ref.gravity,
_ref$walled = _ref.walled,
walled = _ref$walled === void 0 ? false : _ref$walled,
_ref$wallWidth = _ref.wallWidth,
wallWidth = _ref$wallWidth === void 0 ? 50 : _ref$wallWidth,
children = _ref.children;
var rendererOptions = _extends({
width: width,
height: height,
background: 'transparent',
wireframeBackground: 'transparent',
pixelRatio: pixelRatio
}, rendererProps.options);
var key = rendererOptions.width + "-" + rendererOptions.height;
var wall = walled ? React.createElement(Walls, {
x: -wallWidth,
y: -wallWidth,
width: function width(engine) {
return relX(1)(engine) + wallWidth;
},
height: function height(engine) {
return relY(1)(engine) + wallWidth;
},
wallWidth: wallWidth,
options: {
render: {
visible: false
}
}
}) : null;
return React.createElement(Engine$1, {
options: engineOptions,
key: key
}, React.createElement(RenderClones$1, Object.assign({}, rendererProps, {
options: rendererOptions,
enableMouse: mouse
}), wall, function (engine) {
Object.assign(engine.world.gravity, gravity);
return children;
}));
};
var Scene$1 = /*#__PURE__*/
valueMemo(Scene);
var WindowScene = function WindowScene(props) {
var _useWindowSize = useWindowSize(),
width = _useWindowSize.width,
height = _useWindowSize.height;
return React.createElement(Scene$1, Object.assign({}, props, {
width: width,
height: height
}));
};
var Shape = function Shape(_ref) {
var paths = _ref.paths,
_ref$sampleLength = _ref.sampleLength,
sampleLength = _ref$sampleLength === void 0 ? 30 : _ref$sampleLength,
props = _objectWithoutPropertiesLoose(_ref, ["paths", "sampleLength"]);
if (!paths) {
return null;
}
var vertexSets = paths.map(function (path) {
return Matter.Svg.pathToVertices(path, sampleLength);
});
return React.createElement(Vertices$1, Object.assign({
vertexSets: vertexSets
}, props));
};
window.decomp =
/*#__PURE__*/
require('poly-decomp');
export { Circle$1 as Circle, Constraint$1 as Constraint, Engine$1 as Engine, Rectangle$1 as Rectangle, Render$1 as Render, RenderClones$1 as RenderClones, Scene$1 as Scene, Shape, Vertices$1 as Vertices, Walls, WindowScene, relX, relY, useEngine };
//# sourceMappingURL=react-matter-js.esm.js.map