UNPKG

react-matter-js

Version:

React adapter for the Matter.js physics engine

929 lines (776 loc) 24.1 kB
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