Fixed Background in a Matter.js world - matter.js

I am looking for a way to set a background fixed to the world. In my case, Render.lookAt targets a moving body and I need to display a fiexd grid behind it to enhance the body’s movements. Is that possible?

The best way to do this is to create a body inside the world with a background sprite.
You can do this like so:
var gridBackground = Bodies.rectangle(0, 0, 1, 1, {
isStatic: true,
isSensor: true,
render: {
sprite: {
texture: "Path/To/Image.png",
xScale: neededScale,
yScale: neededScale
}
}
});
World.add(world, gridBackground);
If your world is infinite, you can also use the grid as a background-image on the body/canvas in css and move it in the opposite direction the player is moving:
Matter.Events.on(engine, "beforeupdate", function() {
var x = player.position.x * -1;
var y = player.position.y * -1;
Matter.Render.lookAt(render, {
min: {y:y-FoV/2, x:x-FoV/2},
max: {y:y+FoV/2, x:x+FoV/2},
});
});

Related

Using Matter.js, how to position SVG paths in a compound body?

I'm trying to create a capital letter "A" as an SVG shape for use with Matter.js but the letter shape displays incorrectly.
CodePen here and duplicated here:
function percentX(percent) {
return Math.round((percent / 100) * window.innerWidth);
}
function percentY(percent) {
return Math.round((percent / 100) * window.innerHeight);
}
const Engine = Matter.Engine,
Bodies = Matter.Bodies,
Body = Matter.Body,
Svg = Matter.Svg,
Vertices = Matter.Vertices,
Composite = Matter.Composite,
Render = Matter.Render,
Runner = Matter.Runner;
// create an engine
const engine = Engine.create(),
world = engine.world;
// create a renderer
const render = Render.create({
element: document.body,
engine: engine,
options: {
wireframes: false,
showInternalEdges: false,
width: percentX(100),
height: percentY(100),
background: "transparent"
}
});
let bodies = [],
bgColor = "#0A0618";
// SVGs
let vertexSets = [],
svgLetter,
svgLetterLegOne,
svgLetterLegTwo,
svgLetterCounter;
let letterX = percentX(60);
let letterY = percentY(20);
let letterXLegOne = percentX(60) - 40;
let letterYLegOne = percentY(20) + 40;
let letterXLegTwo = percentX(60) + 40;
let letterYLegTwo = percentY(20) + 40;
let letterSize = (window.innerWidth / 1000);
// A
// silhouette test (incorrectly displaying Batman ears)
$('#svg-test').find('path').each(function(i, path) {
svgTest = Bodies.fromVertices(
percentX(30),
letterY,
Vertices.scale(Svg.pathToVertices(path, 10),
letterSize,
letterSize), {
render: {
fillStyle: "white",
strokeStyle: "white",
lineWidth: 2
}
}, true);
vertexSets.push(svgTest);
});
// letter base shape
$('#svg-3').find('path').each(function(i, path) {
svgLetter = Bodies.fromVertices(
letterX,
letterY,
Vertices.scale(Svg.pathToVertices(path, 10),
letterSize,
letterSize), {
render: {
fillStyle: "yellow",
strokeStyle: "yellow",
lineWidth: 2
}
}, true);
vertexSets.push(svgLetter);
});
// left leg
$('#svg-3-leg-1').find('path').each(function(i, path) {
svgLetterLegOne = Bodies.fromVertices(
letterXLegOne,
letterYLegOne,
Vertices.scale(Svg.pathToVertices(path, 10),
letterSize,
letterSize), {
render: {
fillStyle: "green",
strokeStyle: "green",
lineWidth: 2,
isStatic: true
}
}, true);
vertexSets.push(svgLetterLegOne);
});
// right leg
$('#svg-3-leg-2').find('path').each(function(i, path) {
svgLetterLegTwo = Bodies.fromVertices(
letterXLegTwo,
letterYLegTwo,
Vertices.scale(Svg.pathToVertices(path, 10),
letterSize,
letterSize), {
render: {
fillStyle: "blue",
strokeStyle: "blue",
lineWidth: 2,
isStatic: true
}
}, true);
vertexSets.push(svgLetterLegTwo);
});
// counter (hole in the center), no need for offset repositioning
$('#svg-3-counter').find('path').each(function(i, path) {
svgLetterCounter = Bodies.fromVertices(
letterX,
letterY,
Vertices.scale(Svg.pathToVertices(path, 10),
letterSize,
letterSize), {
render: {
fillStyle: bgColor,
strokeStyle: bgColor,
lineWidth: 2
}
}, true);
vertexSets.push(svgLetterCounter);
});
// create compound body for letter "A"
var compoundBodyA = Body.create({
parts: [svgLetter, svgLetterLegOne, svgLetterLegTwo, svgLetterCounter]
});
// add A and O compound bodies to the world
Composite.add(world, [
compoundBodyA
]);
// add all SVGs to the world
Composite.add(world, vertexSets);
// run the renderer
Render.run(render);
// create runner
const runner = Runner.create();
// run the engine
Runner.run(runner, engine);
// hold in place for testing
world.gravity.y = 0;
world.gravity.x = 0;
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
}
*:focus, *::before:focus, *::after:focus {
outline: none;
}
* {
font-family: monaco, courier;
}
body {
margin: 0;
padding: 0;
background: #0A0618;
}
svg {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pathseg#1.2.1/pathseg.js"></script>
<script src="https://cdn.jsdelivr.net/npm/poly-decomp#0.3.0/build/decomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<svg class="svg-letter" id="svg-test">
<path class="st0" d="M59.3,0h46.4l59,141h-50.8l-7.4-18.8h-49L50.3,141h-50L59.3,0z"/>
</svg>
<svg class="svg-letter" id="svg-3">
<path d="M57.2,122.2H7.9L59,0h46.4l51.1,122.2h-50.3H57.2z"/>
</svg>
<svg class="svg-letter" id="svg-3-leg-1">
<path d="M0,141l7.9-18.8h49.3L50,141H0z"/>
</svg>
<svg class="svg-letter" id="svg-3-leg-2">
<path d="M106.2,122.2h50.3l7.9,18.8h-50.8L106.2,122.2z"/>
</svg>
<svg class="svg-letter" id="svg-3-counter">
<path d="M94.6,89L81.8,55L69,89H94.6z"/>
</svg>
I know Matter.js can't always handle SVGs with compound paths (an internal path creating a knockout) so my plan was to have two separate paths, the silhouette of the shape and the knockout, and group them as a compound body. Only the silhouette isn't even displaying correctly (the white version on the left). For some reason, the shape always has those Batman ears which I can't get rid of.
So my new plan is to break the silhouette shape into three parts, the main body (in yellow) and two legs (green and blue). That allows all three parts to have only four sides which seems to prevent the bug.
My problem is positioning those two legs so that they are always precisely butting up against the main body shape. I can adjust the position offset to accomplish this but since I've set the width and height of the render object to be proportional to the width and height of the browser, the letter breaks apart if the page is loaded in any other sized window.
Using variables (lines 45–54), I've tried setting the width and height of the legs to reference the main body shape with offsets:
svgThree.position.x - 40,
svgThree.position.y + 40,
And I've tried keeping all units and offsets proportional:
percentX(60) - percentX(2),
percentY(20) + percentX(2),
But nothing works. Without setting the letter to exact pixel dimensions, is there any way to keep these three paths touching and in precise relation to each other across different browser sizes?
Alternatively, if there's any way to build that SVG shape to avoid that bug, I'd greatly appreciate such a solution.
(Using Chrome Version 102.0.5005.115)
It's not a perfect solution but the one I eventually went with was this:
Setting the letter size to a static number (0.8), instead of a derivation of the window width, allowed me to set the width and height of the two legs of the letter to a static offset distance from the main body shape, so the change in the original CodePen above would be to lines 48–54:
let letterXLegOne = letterX - 43;
let letterYLegOne = letterY + 49;
let letterXLegTwo = letterX + 43;
let letterYLegTwo = letterY + 49;
let letterSize = 0.8;
The fix is incorporated into this CodePen as a final product.

How To Prevent CSS Applied to One Three.JS Scene From Being Applied To All Scenes

This may be more of a "you're going to have to do it the long way" type of deal, but here it goes...
When I apply CSS to control the opacity of only one HTML element that is the container of a Three.JS scene in where there are multiple elements that each are a container for their own scene, the CSS applied (even at an inline level) to only one of those elements containing a scene is being applied to all elements that contain a scene and not specifically the one targeted. This happens with any post applied CSS attribute, not just opacity.
The reason why I am attempting this approach at controlling opacity this way is that per my research there is no direct way to set an opacity on a Three.JS group object that contains 1 to N number of meshes. I am (in theory) trying not to have to define all materials with transparency set to "true" and then having to do recursive updates to all meshes in a Three.JS Group object where an animation would fade in/out.
Some of the group objects I'm working with will have many meshes defined in them. Thus, rather than update the opacity of each individual mesh itself contained within a Three.JS group object, my goal was/is to have individual scenes for each type of animation that is capable of having any amount of transparency applied to it run as is then just adjust the HTML element containing that animation's opacity property.
I've tried using one camera and multiple cameras to no avail. I've also tried nesting the containers in one additional element and setting CSS on the parent element but the same issue occurs. I have not tried using multiple renderers as from what I gather in research is that doing so is frowned upon and can lead to performance issues and context limits. The render loop also has "autoClear" set to false so that all scenes render together.
Here is the HTML syntax. You will notice that the first element has a inline style for opacity set to 0.5 and the second element has no inline styling applied:
<div class="three-js-container" id="scene-container-1" style="opacity:0.5;"></div>
<div class="three-js-container" id="scene-container-2"></div>
Here is the Javascript code:
/* Only one renderer instance is created */
var universalRenderer = new THREE.WebGLRenderer({antialias: true, alpha:true});
/* references to all containers are made */
var containerForScene1 = document.getElementById("scene-container-1");
var containerForScene2 = document.getElementById("scene-container-2");
/* two different cameras are created */
var cameraForScene1 = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.001, 1000);
var cameraForScene2 = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.001, 1000);
/* two different scenes are created, one for each element container */
var scene1 = new THREE.Scene();
scene1.userData.element = containerForScene1;
var scene2 = new THREE.Scene();
scene2.userData.element = containerForScene2;
/* the renderer is applied to both scene containers */
containerForScene1.appendChild(universalRenderer.domElement);
containerForScene2.appendChild(universalRenderer.domElement);
When both animations are played, both scenes are rendered at 1/2 opacity rather than just the first scene itself.
Is there a reason why CSS styling applied to one HTML scene containing element is applied to all of the other scene containing elements? Will I just have to suck it up and go the long way around in controlling mesh opacity?
Thanks.
Setting transparency for a THREE.Group:
A Group is just a container. As such, it has children, which are potentially other Groups. But you can only apply transparency to a Material, which is assigned at the Mesh level, not Groups. However, not all is lost, because you can monkey patch Group to allow you to perform the operation seamlessly.
// MONKEY PATCH
Object.defineProperty(THREE.Group.prototype, "transparent", {
set: function(newXP) {
this.traverse(node => {
if (node.material) {
node.material.transparent = newXP
node.material.opacity = (newXP) ? 0.5 : 1
}
})
}
})
// Set up the renderer
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true
})
document.body.appendChild(renderer.domElement)
renderer.setSize(window.innerWidth, window.innerHeight)
const scene = new THREE.Scene()
const size = new THREE.Vector2()
renderer.getSize(size)
const camera = new THREE.PerspectiveCamera(28, size.x / size.y, 1, 1000)
camera.position.set(0, 20, 100)
camera.lookAt(scene.position)
scene.add(camera)
camera.add(new THREE.PointLight(0xffffff, 1))
function render() {
renderer.render(scene, camera)
}
const axis = new THREE.Vector3(0, 1, 0)
function animate() {
requestAnimationFrame(animate)
camera.position.applyAxisAngle(axis, 0.005)
camera.lookAt(scene.position)
render()
}
animate()
// Populate the scene
const cubeGeo = new THREE.BoxBufferGeometry(5, 5, 5)
let opaqueCubes = []
let transparentCubes = []
const randomInRange = () => Math.random() * ((Math.random() <= 0.5) ? -10 : 10)
const opaqueGroup = new THREE.Group()
scene.add(opaqueGroup)
for (let i = 0; i < 10; ++i) {
opaqueGroup.add(new THREE.Mesh(cubeGeo, new THREE.MeshPhongMaterial({
color: "red"
})))
opaqueGroup.children[i].position.set(randomInRange(), randomInRange(), randomInRange())
}
const transparentGroup = new THREE.Group()
scene.add(transparentGroup)
for (let i = 0; i < 10; ++i) {
transparentGroup.add(new THREE.Mesh(cubeGeo, new THREE.MeshPhongMaterial({
color: "green"
})))
transparentGroup.children[i].position.set(randomInRange(-10, 10), randomInRange(-10, 10), randomInRange(-10, 10))
}
// Control the transparency from the input
const xparent = document.getElementById("xparent")
xparent.addEventListener("change", (e) => {
transparentGroup.transparent = xparent.checked
})
html,
body {
padding: 0;
margin: 0;
overflow: hidden;
}
#control {
position: absolute;
top: 0;
left: 0;
padding: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.js"></script>
<div id="control">
<label>Make the green ones transparent:<input id="xparent" type="checkbox" /></label>
<div>

HTML animate "ghost-img" when dragging element

It's there any way to animate the "ghost image" when dragging the element?
Just like Google Drive, drag the files will trigger cool effect. I tried to add the CSS animation to the ghost-img, like this but it's not work.
go this way https://jsfiddle.net/mdvrkaer/4/ :
You miss the "drag" event. to move the ghost, make him follow the mouse cursor...
$(document).ready(function(){
dragAndDrop();
$('#ghost-img').hide();
});
function dragAndDrop(){
var dragElement = document.getElementById('row-1');
dragElement.addEventListener("dragstart",function(evt) {
$('#ghost-img').show();
//ghost image
var crt = document.getElementById('ghost-img').cloneNode(true);
crt.style.position = "absolute";
crt.style.top = "0px";
crt.style.right = "0px";
crt.style.zIndex = -1;
document.body.appendChild(crt);
evt.dataTransfer.setDragImage (crt, 0, 0);
},false);
dragElement.addEventListener("drag",function(evt) {
$('.ghost').css('top', evt.pageY);
$('.ghost').css('left', evt.pagex);
},false);
dragElement.addEventListener("dragend",function(evt) {
$('.ghost').hide();
},false);
}

Restrict panning and zoom on Here Maps 3.0

I generated a map with Here maps JS Api 3.0. I want to restrict the zoom to a min/max value and the panning to a given rectangle. Is there a way to do that?
for example:
map.setMinZoom(4);
map.setMaxZoom(14);
map.setPanRestriction(rectangle);
I am guessing you're trying to restrict the viewport to where you now have you image overlay...
The easiest way is to listen to view model and and viewport updates similar the other example:
Custom map overlay heremaps js api v3
Now you can look at the map's center coordinate and see if it is within the boundaries you wish to display. If not, set it to within the boundaries. Something along those lines may work for you:
var bounds = new H.geo.Rect(45, -45, -45, 45);
map.getViewModel().addEventListener('sync', function() {
var center = map.getCenter(),
adjustLat,
adjustLng;
if (!bounds.containsPoint(center)) {
if (center.lat > bounds.getTop()) {
adjustLat = bounds.getTop();
} else if (center.lat < bounds.getBottom()) {
adjustLat = bounds.getBottom();
} else {
adjustLat = center.lat;
}
if (center.lng < bounds.getLeft()) {
adjustLng = bounds.getLeft();
} else if (center.lng > bounds.getRight()) {
adjustLng = bounds.getRight();
} else {
adjustLng = center.lng;
}
map.setCenter({
lat: adjustLat,
lng: adjustLng
});
}
});
//Debug code to visualize where your restriction is
map.addObject(new H.map.Rect(bounds));

famo.us: Modify content in GridLayout

is it possible to modify the content of a surface (used within GridLayout) without using CSS? For example to center the text?
Basic example:
function createGrid( section, dimensions, menuData ) {
var grid = new GridLayout({
dimensions: dimensions
});
var surfaces = [];
grid.sequenceFrom(surfaces);
for(var i = 0; i < dimensions[1]; i++) {
surfaces.push(new Surface({
content: menuData[i].title,
size: [undefined, undefined],
properties: {
backgroundColor: "#ff0000",
color: "white",
textAlign: 'center',
}
}));
}
return grid;
}
I added the center property, but I also want to have the content in the middle of my surface. Do I have to use CSS or is there another way?
I tried adding another View/Surface within this Surface and added the align/origin modifier. Didn't work: I still had to adjust the origin/align values for the specific (browser) layout ...
I'm not so sure about you first question, but I can answer the second one.
text-alignhelps you horizontally center your content. So the problem is how to do it vertically.
The cleanest way to do it is to set the line-height the same as the containing div's height. In your case, there are two ways to do it:
1) calculate the height of the grid. For example, if you have a 9*9 GridLayout for the whole screen, then we will have gridHeight = window.innerHeight/9. Then you just need to add lineHeight: gridHeight to your properties object.
check http://jsfiddle.net/mrwiredancer/veLpbmmo/2/ for full exmaple
2) if you are not able to calculate the height of the grid beforehand, you can center a fixed-height(smaller than the grid's height) surface in the middle of the grid. For example, your GridLayout is contained in a dynamic view, but you're sure that your grid is no less than 20px high. Then you can do this:
var container, surface, __height = 20;
for(var i = 0; i < dimensions[1]; i++) {
container = new Container({ //View works as well
properties: {
backgroundColor: '#FF0000'
}
})
surface = new Surface({
content: menuData[i].title,
size: [undefined, __height],
properties: {
lineHeight: __height+'px', //same as surface's height
color: "white",
textAlign: 'center',
}
});
container.add(new Modifier({origin: [.5, .5]})).add(surface);
surfaces.push(container);
}
check http://jsfiddle.net/mrwiredancer/uxq30yp9/1/ for full example

Resources