Using Matter.js, how to position SVG paths in a compound body? - matter.js
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.
Related
How to trace a click on the stroke in Konva-react
Can you tell me how to track the click only on the stroke and not inside the body of the figure. The shapes in which you want to trace this can be anything: Star, Rect, etc. Perhaps you have encountered this. I can't even imagine how it could be done.
You need to draw a shape with fillEnabled: false so only its stroke is visible. And only its stroke will listen to events. If you want to keep fill, then you have to use two shapes. One for fill, another for stroke and events detection. const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight }); const layer = new Konva.Layer(); stage.add(layer); const shape = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 50, stroke: 'green', strokeWidth: 10, fillEnabled: false }); layer.add(shape); shape.on('click', () => { console.log('shape click'); }) <script src="https://unpkg.com/konva#^8/konva.min.js"></script> <div id="container"></div>
Variable font weight on HTML canvas capped at 999?
I'm working on an variable type test for a project with a variable font i created. The 'wght' axis contains a range of between 0-6600. I had no problems animating the full range of values above as a DOM element with HTML and CSS. But while trying out the same thing on HTML canvas, i realised the font weight resets itself to 0 once its passes 1000. Is there a way to get around this? I tried looking into P5.js but the examples i found still manipulates DOM elements instead of using a canvas. An excerpt of my code below, but i cant quite share the working demo with the font as the font is meant to be a proprietary font eventually. #font-face { font-family: 'TestFont'; src: url('/assets/TestFont_v2.ttf') format('truetype-variations'); font-weight: 0 6600; } <canvas id="canvas-text" width="1000" height="500"> </canvas> /*** js***/ var c = document.getElementById('canvas-text'); var ctx = c.getContext('2d'); function drawCanvas() { ctx.fillStyle = 'black'; ctx.fillRect(0, 0, c.width, c.height); ctx.fillStyle = 'white'; ctx.font = (scroll_percentage * 6600) + ' ' + '20px TestFont'; const string_ = 'Lorem ipsum'; ctx.fillText(string_, 20, 20); } let scroll_percentage = 0 function updateScrollProgress() { // get percentage of total scroll var winScroll = document.body.scrollTop || document.documentElement.scrollTop; var height = document.documentElement.scrollHeight - document.documentElement.clientHeight; scroll_percentage = winScroll / height; // update canvas on scroll drawCanvas() }
Fixed Background in a Matter.js world
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}, }); });
change the legend.y property on browser resize
we use the highchart control with Angular and bootstrap. To adjust the vertical space between the chart and the legend (both are rendered by highchart as svg elements), we set the y property of the legend on page load (in the controller) like this: $scope.chartContracts = { options: { chart: { type: 'pie', marginBottom: 50 }, legend : { layout: 'horizontal', align: 'center', verticalAlign: 'bottom', x: 0, y: 4, Now in one of the Bootstraps layouts, the spacing (y) should be -10 in stead of 4 because for some reason an extra spacing is added. So it should be based on media query or something similar I guess? The question is how to do this... I tried with css but I can't seem to style SVG elements (they are called <g>)?
You can check window width and then return correct value. var wWidth = $(window).width(), value; if(wWidth < 400) { value = 5; } else { value = 10; } //scope y: value
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