How would one go about adding programmatically triggered touch/mouse events in Matter.js? I have a few collision events set up for the engine, but can not trigger a mouseup event that stops the current dragging action. I've tried various combinations of targeting the canvas element, the mouse/mouseConstraint, and the non-static body.
If you, like me, came here trying to figure out how to be able to click on a Matter.js body object, let me give you one way. My goal in my project was to assign some attributes to my rectangle objects and call a function when they were clicked on.
The first thing to do was to distinguish between dragging and clicking, so I wrote(using Jquery):
$("body").on("mousedown", function(e){
mouseX1 = e.pageX;
mouseY1 = e.pageY;
});
$("body").on("mouseup", function(e){
mouseX2 = e.pageX;
mouseY2 = e.pageY;
if((mouseX1 == mouseX2) && (mouseY1 == mouseY2)){
//alert("click!\n" + mouseX2 + " " + mouseY2 +"\n");
var bodiesUnder = Matter.Query.point(books, { x: mouseX2, y: mouseY2 });
//alert("click!\n" + mouseX2 + " " + mouseY2 +"\n");
if (bodiesUnder.length > 0) {
var bodyToClick = bodiesUnder[0];
alert(bodyToClick.title2);
}
}
});
This was accomplished when listening for "mouseup" and asking if ((mouseX1 == mouseX2) && (mouseY1 == mouseY2)).
Second- the juicy part- create a var array to hold the objects, or 'bodies', we are going to dig up under the mouse. Thankfully there's this function:
var bodiesUnder = Matter.Query.point(books, { x: mouseX2, y: mouseY2 });
For the first element in here I entered "books". For you this needs to be the name of an array you've put all your objects, or 'bodies' into. If you don't have them in an array, it's not hard to throw them all in, like so:
var books = [book1, book2, book3];
Once that was all done, I was able to alert(book1.title2) to see what the title of that book (body) is. My bodies were coded as follows:
var book2 = Bodies.rectangle(390, 200, 66, 70, {
render : {
sprite : {
texture: "img/tradingIcon.jpg"
}
},
restitution : 0.3,
title1 : 'Vanessa and Terry',
title2 : 'Trading'
});
Hope that helps! This one had me hung up for a whole day.
It turns out I had incorrectly configured the Matter.Mouse module, and was re-assigning the mouse input that had already been set in MouseConstraint. The following works in regards to my original question:
Matter.mouseConstraint.mouse.mouseup(event);
Related
I have been trying to figure out how to pan/zoom using onMouseDrag, and onMouseDown in paperjs.
The only reference I have seen has been in coffescript, and does not use the paperjs tools.
This took me longer than it should have to figure out.
var toolZoomIn = new paper.Tool();
toolZoomIn.onMouseDrag = function (event) {
var a = event.downPoint.subtract(event.point);
a = a.add(paper.view.center);
paper.view.center = a;
}
you can simplify Sam P's method some more:
var toolPan = new paper.Tool();
toolPan.onMouseDrag = function (event) {
var offset = event.downPoint - event.point;
paper.view.center = paper.view.center + offset;
};
the event object already has a variable with the start point called downPoint.
i have put together a quick sketch to test this.
Unfortunately you can't rely on event.downPoint to get the previous point while you're changing the view transform. You have to save it yourself in view coordinates (as pointed out here by Jürg Lehni, developer of Paper.js).
Here's a version that works (also in this sketch):
let oldPointViewCoords;
function onMouseDown(e) {
oldPointViewCoords = view.projectToView(e.point);
}
function onMouseDrag(e) {
const delta = e.point.subtract(view.viewToProject(oldPointViewCoords));
oldPointViewCoords = view.projectToView(e.point);
view.translate(delta);
}
view.translate(view.center);
new Path.Circle({radius: 100, fillColor: 'red'});
Is it possible to create a single gravity / force point in matter.js that is at the center of x/y coordinates?
I have managed to do it with d3.js but wanted to enquire about matter.js as it has the ability to use multiple polyshapes.
http://bl.ocks.org/mbostock/1021841
The illustrious answer has arisen:
not sure if there is any interest in this. I'm a fan of what you have created. In my latest project, I used matter-js but I needed elements to gravitate to a specific point, rather than into a general direction. That was very easily accomplished. I was wondering if you are interested in that feature as well, it would not break anything.
All one has to do is setting engine.world.gravity.isPoint = true and then the gravity vector is used as point, rather than a direction. One might set:
engine.world.gravity.x = 355;
engine.world.gravity.y = 125;
engine.world.gravity.isPoint = true;
and all objects will gravitate to that point.
If this is not within the scope of this engine, I understand. Either way, thanks for the great work.
You can do this with the matter-attractors plugin. Here's their basic example:
Matter.use(
'matter-attractors' // PLUGIN_NAME
);
var Engine = Matter.Engine,
Events = Matter.Events,
Runner = Matter.Runner,
Render = Matter.Render,
World = Matter.World,
Body = Matter.Body,
Mouse = Matter.Mouse,
Common = Matter.Common,
Bodies = Matter.Bodies;
// create engine
var engine = Engine.create();
// create renderer
var render = Render.create({
element: document.body,
engine: engine,
options: {
width: Math.min(document.documentElement.clientWidth, 1024),
height: Math.min(document.documentElement.clientHeight, 1024),
wireframes: false
}
});
// create runner
var runner = Runner.create();
Runner.run(runner, engine);
Render.run(render);
// create demo scene
var world = engine.world;
world.gravity.scale = 0;
// create a body with an attractor
var attractiveBody = Bodies.circle(
render.options.width / 2,
render.options.height / 2,
50,
{
isStatic: true,
// example of an attractor function that
// returns a force vector that applies to bodyB
plugin: {
attractors: [
function(bodyA, bodyB) {
return {
x: (bodyA.position.x - bodyB.position.x) * 1e-6,
y: (bodyA.position.y - bodyB.position.y) * 1e-6,
};
}
]
}
});
World.add(world, attractiveBody);
// add some bodies that to be attracted
for (var i = 0; i < 150; i += 1) {
var body = Bodies.polygon(
Common.random(0, render.options.width),
Common.random(0, render.options.height),
Common.random(1, 5),
Common.random() > 0.9 ? Common.random(15, 25) : Common.random(5, 10)
);
World.add(world, body);
}
// add mouse control
var mouse = Mouse.create(render.canvas);
Events.on(engine, 'afterUpdate', function() {
if (!mouse.position.x) {
return;
}
// smoothly move the attractor body towards the mouse
Body.translate(attractiveBody, {
x: (mouse.position.x - attractiveBody.position.x) * 0.25,
y: (mouse.position.y - attractiveBody.position.y) * 0.25
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.12.0/matter.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/matter-attractors#0.1.6/build/matter-attractors.min.js"></script>
Historical note: the "gravity point" functionality was proposed as a feature in MJS as PR #132 but it was closed, with the author of MJS (liabru) offering the matter-attractors plugin as an alternate. At the time of writing, this answer misleadingly seems to indicate that functionality from the PR was in fact merged.
Unfortunately, the attractors library is 6 years outdated at the time of writing and raises a warning when using a newer version of MJS than 0.12.0. From discussion in issue #11, it sounds like it's OK to ignore the warning and use this plugin with, for example, 0.18.0. Here's the warning:
matter-js: Plugin.use: matter-attractors#0.1.4 is for matter-js#^0.12.0 but installed on matter-js#0.18.0.
Behavior seemed fine on cursory glance, but I'll keep 0.12.0 in the above example to silence it anyway. If you do update to a recent version, note that Matter.World is deprecated and should be replaced with Matter.Composite and engine.gravity.
I have a problem with a data string returning 'undefined' in my tooltip, while that same piece of data does affect the colors of my svg.
Objective: I want to display a map, chloropleth style, that displays a tooltip for each country with
a) country names, loaded from a .json object (works fine)
b) some values corresponding to each country, loaded from a tsv (works partially so far)
File structure :
The main .js file calls 1 topojson file (with country paths and names) as well as a .tsv file (with specific area data for each country)
Resources used:
1- MBostock's chloropleth (see note below)
2- d3.tip by Justin Palmer
Play with it
Here is a Plunker for people to play with it (due to the heaviness of the world.json I use, it may take a while to load).
---------------
Relevant code bit :
queue()
.defer(d3.json, "world.json")
.defer(d3.tsv, "winemap.tsv", function setMaptoTotal(d) { rate.set(d.name, +d.total); })
.await(ready);
var tip = d3.tip()
.attr('class', 'd3-tip')
.html(function mylabel(d) { return d.properties.name + "<table><tr><td><span style='color: #fcf772'> Total area cultivated: " + d.total +" ha</span></td></tr><tr><td> <span style='color:#bf2a2a'> Global rank: "+ d.rank + " </span></td></tr></table>";})
.direction('sw')
.offset([0, 2]);
var svg = d3.selectAll('body')
.append('svg')
.attr('width',width)
.attr('height',height)
.call(zoom)
.call(tip);
// ACTION STARTS HERE //
function ready(error, world) {
svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(world, world.objects.countries).features)
.enter().append("path")
.attr("class", function (d) { return quantize(rate.get(d.properties.name)); })
.attr("d", path)
.call(tip)
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
;
Options tried:
1- Moving .call(tip) after or inside the ready function which triggers data load via queue(). >>> This still returns undefined in my tooltip.
2- Use queue.defer(mylabel(d)). >>> Nothing appears on screen.
3- Taking into account the asynchronous nature of d3.tsv, I went through this question. If I understand well, you have to refer to the function you want to call both inside and after d3.tsv (with or without setting a global variable). I thus planned to refer to the mylabel(d) function both inside and after d3.tsv. However I noticed that function (d) { return quantize(rate.get(d.properties.name)); }) is not written in my d3.tsv yet it works fine.
Note: sorry I could not post more links at this stage, e.g. to MBostick's chloropleth or d3.tip, but it requires more reputation :)
In case anyone faces the same issue, I finally found the answer.
There were 2 problems:
1- Data should be first associated with an array.
First you need to pair, inside the ready function, the id with each data entry
function ready (error, world, data) {
var Total= {};
var Name= {};
var Rank= {};
data.forEach(function(d) {
Total[d.id] = +d.total;
Name[d.id] = d.name;
Rank[d.id] = +d.rank;
});
2- This method only works with referring explicitly to "id"
The command .data(topojson.feature(world, world.objects.countries).features) will search for the id property of each json object that you want to apply a transformation with. The bridge between your .tsv and .json MUST be id, both in your .json and .tsv files as well as the code.
The reason you can't replace id with just any name while working with topoJSON is that it holds a specific function in the topojson code itself (see below).
{var r={type:"Feature",id:t.id,properties:t.properties||{},geometry:u(n,t)};return null==t.id&&delete r.id,r}
I solved this by adding into my .tsv file a column with the id attributes of all countries objects inside my .json file. I named this column idso the pairing described in 1- can function.
Careful, you want to represent all countries that are in your .json on your .tsv, otherwise "undefined" will appear again when you hover the countries that would not be mentioned on your .tsv.
Final, working code -
function ready (error, world, data) {
var pairDataWithId= {};
var pairNameWithId= {};
var pairRankWithId= {};
// “d” refers to that data parameter, which in this case is our .csv that (while not necessary) is also named “data.”
data.forEach(function(d) {
pairDataWithId[d.id] = +d.total;
pairNameWithId[d.id] = d.name;
pairRankWithId[d.id] = +d.rank;
});
// Below my tooltip vars
var tipin = function(d) { d3.select(this).transition().duration(300).style('opacity', 1);
div.transition().duration(300).style('opacity', 1);
div.text(pairNameWithId[d.id] + " Total Area: " + pairDataWithId[d.id]+ " Rank: " + pairRankWithId[d.id])
.style('left', (d3.event.pageX) + "px")
.style('top', (d3.event.pageY -30) + "px");
};
var tipout = function() {
d3.select(this)
.transition().duration(300).style('opacity', 0.8);
div.transition().duration(300)
.style('opacity', 0);
};
// Resume
function colorme(d) { return color (pairDataWithId[d.id]);
}
svg.append('g')
.attr('class', 'region')
.selectAll('path')
.data(topojson.feature(world,world.objects.countries).features)
.enter()
.append('path')
.attr('d', path)
.style('fill', colorme)
.style('opacity', 0.8)
.on ('mouseover', tipin)
.on('mouseout', tipout);
};
Note: I stopped using d3.tip for a simpler version with full code above.
I am trying to take a string which has shape option information and create the shape on my Google Map application.
The string is made by splitting an array that was built from a local text document.
The string appears as:
Circle{center: new google.maps.LatLng(38.041872419557094, -87.6046371459961),radius:5197.017394363823,fillColor: '#000000',strokeWeight: 1,strokeColor: '#000000',map:map};
The function I have to take such string and make the shape appears as:
function loadDrawings(evt)
{
var f = evt.target.files[0];
if (!f)
{
alert("Failed to load file");
}
else if (!f.type.match('text.*'))
{
alert(f.name + " is not a valid text file.");
}
else
{
var r = new FileReader();
r.onload = function (e)
{
var contents = e.target.result;
var drawings = [];
var drawing;
var drawingType;
var shape;
var shapeOptions;
drawings = contents.split(";");
for (i = 0; i < drawings.length - 1; i++) {
drawing = drawings[i].toString();
drawingType = drawing.substr(0, drawing.indexOf('{'));
if (drawingType == "Circle")
{
shapeOptions = drawing.substr(6); //UNIQUE TO CIRCLE
shape = new google.maps.Circle(shapeOptions);
shape.setMap(map);
}
};
}
r.readAsText(f);
}
}
My issue is shapeOptions as a string does not work in the above syntax for creating the Circle. However, if I take the contents of the string, which is:
{center: new google.maps.LatLng(38.041872419557094, -87.6046371459961),radius:5197.017394363823,fillColor: '#000000',strokeWeight: 1,strokeColor: '#000000',map:map}
And directly enter it, the shape appears.
Do I need a certain variable type for my shapeOptions for this to work? I know that the new google.maps. requires (), but I have had no luck creating a variable from my string. Am I missing something here?
Much appreciation for any help!
Your shapeOptions string is a JavaScript object literal, so you can eval() it to get the object:
shapeOptions = eval( '(' + drawing.substr(6) + ')' );
Since it has map:map in it, you don't need the subsequent setMap() call.
Also, you're missing a var for the i variable. I don't really recommend the coding style where all the var statements go at the top of a function. I find it error-prone; it's too easy to omit a var without noticing it. (I know some famous JavaScript experts insist that var at the top is the only way to do it, but they fail to see the tradeoffs involved.)
You don't need the .toString() on drawings[i]. It's already a string.
You have two different brace styles. Best to pick one and stick with it. For JavaScript, putting the { on a line by itself is not recommended, because this code will not do what you expect:
return // hoping to return an object literal - but it doesn't!
{
a: 'b',
c: 'd'
}
Whereas this code does work correctly:
return {
a: 'b',
c: 'd'
}
Since you are using FileReader, I think it's safe to assume you also have .forEach() available.
You can replace the code that uses .indexOf() and the hard coded length with a regular expression.
Putting all that together, you might end up with code like this:
var r = new FileReader();
r.onload = function( e ) {
e.target.result.split(";").forEach( function( drawing ) {
var match = drawing.match( /^(\w+)({.*})$/ );
if( ! match ) return; // unrecognized
var type = match[0], options = eval( match[1] );
switch( type ) {
case "Circle":
new google.maps.Circle( options );
break;
}
});
}
r.readAsText( f );
But you may be able to take it a step further. So far we're looking at a Circle (line breaks added for readability):
Circle{
center: new google.maps.LatLng(
38.041872419557094,
-87.6046371459961
),
radius:5197.017394363823,
fillColor: '#000000',
strokeWeight: 1,
strokeColor: '#000000',
map:map
}
With only a simple change, that could be executed as JavaScript directly. You just need the 'new google.maps.' at the beginning and () around the object literal:
new google.maps.Circle({
center: new google.maps.LatLng(
38.041872419557094,
-87.6046371459961
),
radius:5197.017394363823,
fillColor: '#000000',
strokeWeight: 1,
strokeColor: '#000000',
map:map
})
I assume you will have other drawing types as well? Will they all map directly to google.maps.* objects like Circle does? If so, you could simply do:
var r = new FileReader();
r.onload = function( e ) {
e.target.result.split(";").forEach( function( drawing ) {
eval( drawing.replace(
/^(\w+)({.*})$/,
'new google.maps.$1(\$2)'
) );
});
}
r.readAsText( f );
I'm trying to implement this solution to "grey out" past events in Fullcalendar, but I'm not having any luck. I'm not too well versed in Javascript, though, so I assume I'm making some dumb mistakes.
I've been putting the suggested code into fullcalendar.js, inside the call for daySegHTML(segs) around line 4587.
I added the first two lines at the end of the function's initial var list (Why not, I figured)—so something like this:
...
var leftCol;
var rightCol;
var left;
var right;
var skinCss;
var hoy = new Date;// get today's date
hoy = parseInt((hoy.getTime()) / 1000); //get today date in unix
var html = '';
...
Then, just below, I added the other two lines inside the loop:
for (i=0; i<segCnt; i++) {
seg = segs[i];
event = seg.event;
classes = ['fc-event', 'fc-event-skin', 'fc-event-hori'];
if (isEventDraggable(event)) {
classes.push('fc-event-draggable');
}
unixevent = parseInt((event.end.getTime()) / 1000); //event date in Unix
if (unixevent < hoy) {classes.push('fc-past');} //add class if event is old
if (rtl) {
if (seg.isStart) {
classes.push('fc-corner-right');
}
...
Running this code results in a rendered calendar with no events displayed and an error message: Uncaught TypeError: Cannot call method 'getTime' of null
The "null" being referred to is, apparently, event.end.getTime(). But I'm not sure I understand what exactly is going wrong, or how things are being executed. As written, it seems like it should work. At this point in the code, from what I can tell, event.end contains a valid IETF timecode, but for some reason it's "not there" when I try to run it through getTime()?
This isn't a mission-critical tweak for me, but would still be nice—and I'd like to understand what's going on and what I'm doing wrong, as well! Any help greatly appreciated!
If you are using FullCalendar2 with Google Calendar, you will need to use the version of the code below. This uses Moment.js to do some conversions, but since FC2 requires it, you'll be using it already.
eventRender: function(event, element, view) {
var ntoday = new Date().getTime();
var eventEnd = moment( event.end ).valueOf();
var eventStart = moment( event.start ).valueOf();
if (!event.end){
if (eventStart < ntoday){
element.addClass("past-event");
element.children().addClass("past-event");
}
} else {
if (eventEnd < ntoday){
element.addClass("past-event");
element.children().addClass("past-event");
}
}
}
As per FullCalendar v1.6.4
Style past events in css:
.fc-past{background-color:red;}
Style future events in css:
.fc-future{background-color:red;}
There's no need to fiddle with fullcalendar.js. Just add a callback, like:
eventRender: function(calev, elt, view) {
if (calev.end.getTime() < sometime())
elt.addClass("greyclass");
},
you just have to define the correct CSS for .greyclass.
Every event has an ID associated with it. It is a good idea to maintain your own meta information on all events based on their ids. If you are getting the events popupated from a backend database, add a field to your table. What has worked best for me is to rely on callbacks only to get the event ids and then set/reset attributes fetched from my own data store. Just to give you some perspective, I am pasting below a section of my code snippet. The key is to target the EventDAO class for all your needs.
public class EventDAO
{
//change the connection string as per your database connection.
//private static string connectionString = "Data Source=ASHIT\\SQLEXPRESS;Initial Catalog=amit;Integrated Security=True";
//this method retrieves all events within range start-end
public static List<CalendarEvent> getEvents(DateTime start, DateTime end, long nParlorID)
{
List<CalendarEvent> events = new List<CalendarEvent>();
// your data access class instance
clsAppointments objAppts = new clsAppointments();
DataTable dt = objAppts.SelectAll( start, end);
for(int i=0; i<dt.Rows.Count; ++i)
{
CalendarEvent cevent = new CalendarEvent();
cevent.id = (int)Convert.ToInt64(dt.Rows[i]["ID"]);
.....
Int32 apptDuration = objAppts.GetDuration(); // minutes
string staffName = objAppts.GetStaffName();
string eventDesc = objAppts.GetServiceName();
cevent.title = eventDesc + ":" + staffName;
cevent.description = "Staff name: " + staffName + ", Description: " + eventDesc;
cevent.start = (DateTime)dt.Rows[i]["AppointmentDate"];
cevent.end = (DateTime) cevent.start.AddMinutes(apptDuration);
// set appropriate classNames based on whatever parameters you have.
if (cevent.start < DateTime.Now)
{
cevent.className = "pastEventsClass";
}
.....
events.Add(cevent);
}
}
}
The high level steps are as follows:
Add a property to your cevent class. Call it className or anything else you desire.
Fill it out in EventDAO class while getting all events. Use database or any other local store you maintain to get the meta information.
In your jsonresponse.ashx, retrieve the className and add it to the event returned.
Example snippet from jsonresponse.ashx:
return "{" +
"id: '" + cevent.id + "'," +
"title: '" + HttpContext.Current.Server.HtmlEncode(cevent.title) + "'," +
"start: " + ConvertToTimestamp(cevent.start).ToString() + "," +
"end: " + ConvertToTimestamp(cevent.end).ToString() + "," +
"allDay:" + allDay + "," +
"className: '" + cevent.className + "'," +
"description: '" +
HttpContext.Current.Server.HtmlEncode(cevent.description) + "'" + "},";
Adapted from #MaxD The below code is what i used for colouring past events grey.
JS for fullcalendar pulling in Json
events: '/json-feed.php',
eventRender: function(event,element,view) {
if (event.end < new Date().getTime())
element.addClass("past-event");
},
other options ....
'event.end' in my Json is a full date time '2017-10-10 10:00:00'
CSS
.past-event.fc-event, .past-event .fc-event-dot {
background: #a7a7a7;
border-color: #848484
}
eventDataTransform = (eventData) => {
let newDate = new Date();
if(new Date(newDate.setHours(0, 0, 0, 0)).getTime() > eventData.start.getTime()){
eventData.color = "grey";
}else{
eventData.color = "blue";
}
return eventData;
}
//color will change background color of event
//textColor to change the text color
Adapted from #Jeff original answer just simply check to see if an end date exists, if it does use it otherwise use the start date. There is an allDay key (true/false) but non allDay events can still be created without an end date so it will still throw an null error. Below code has worked for me.
eventRender: function(calev, elt, view) {
var ntoday = new Date().getTime();
if (!calev.end){
if (calev.start.getTime() < ntoday){
elt.addClass("past");
elt.children().addClass("past");
}
} else {
if (calev.end.getTime() < ntoday){
elt.addClass("past");
elt.children().addClass("past");
}
}
}
Ok, so here's what I've got now, that's working (kind of):
eventRender: function(calev, elt, view) {
var ntoday = new Date();
if (calev.start.getTime() < ntoday.getTime()){
elt.addClass("past");
elt.children().addClass("past");
}
}
In my stylesheet, I found I needed to restyle the outer and inner elements to change the color; thus the elt.children().addclass addition.
The only time check I could get to work, lacking an end time for all day events, was to look at the start time - but this is going to cause problems with multi-day events, obviously.
Is there another possible solution?