I have this map code for MapBox here: How to get click handler on animated canvas dot on MapBox map?
import { useEffect } from 'react'
import * as mapboxgl from 'mapbox-gl'
import locations from 'seeds/sample-location-list.json'
import styles from './Map.module.css'
export default function Map({ scrollable, id, style, onClick }) {
useEffect(() => {
const map = new mapboxgl.Map({
accessToken: '<token>',
container: id ?? 'map',
center: [16.572149246748594, 19.06111670348622],
zoom: 1.5,
style: style ?? 'mapbox://styles/<co>/<style>',
attributionControl: false,
})
// .addControl(new mapboxgl.AttributionControl({
// compact: true
// }), 'top-left')
var size = 120;
// This implements `StyleImageInterface`
// to draw a pulsing dot icon on the map.
var pulsingDot = {
width: size,
height: size,
data: new Uint8ClampedArray(size * size * 4),
context: null,
// When the layer is added to the map,
// get the rendering context for the map canvas.
onAdd: function () {
var canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
this.context = canvas.getContext('2d');
},
// Call once before every frame where the icon will be used.
render: function () {
var duration = 1000;
var t = (performance.now() % duration) / duration;
var radius = (size / 2) * 0.3;
var outerRadius = (size / 2) * 0.7 * t + radius;
var context = this.context;
// Draw the outer circle.
context.clearRect(0, 0, this.width, this.height);
context.beginPath();
context.arc(
this.width / 2,
this.height / 2,
outerRadius,
0,
Math.PI * 2
);
context.fillStyle = 'rgba(253, 253, 150,' + (1 - t) + ')';
context.fill();
// Draw the inner circle.
context.beginPath();
context.arc(
this.width / 2,
this.height / 2,
radius,
0,
Math.PI * 2
);
context.fillStyle = 'rgba(253, 253, 150, 1)';
context.strokeStyle = 'rgb(119, 221, 119)';
context.lineWidth = 2 + 4 * (1 - t);
context.fill();
context.stroke();
// Update this image's data with data from the canvas.
this.data = context.getImageData(
0,
0,
this.width,
this.height
).data;
// Continuously repaint the map, resulting
// in the smooth animation of the dot.
map.triggerRepaint();
// Return `true` to let the map know that the image was updated.
return true;
}
};
if (!scrollable) map.scrollZoom.disable()
map.on('load', function () {
map.addImage('pulsing-dot', pulsingDot, { pixelRatio: 2 });
locations.forEach(location => {
map.addSource(location.id.seed, {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [location.lng, location.lat] // icon position [lng, lat]
},
properties: {},
}
]
}
})
map.addLayer({
'id': location.id.seed,
'type': 'symbol',
'source': location.id.seed,
'layout': {
'icon-image': 'pulsing-dot'
}
});
})
map.on('mousemove', 'earthquakes-viz', (e) => {
// console.log
})
});
if (onClick) map.on('click', onClick)
return () => map.remove()
}, [])
return (
<div id={id ?? "map"} className={styles.map}></div>
)
}
It animates the pulsing of around 20 dots on a map canvas layer. I checked it after some time and it was at 80MB memory usage (my whole React app, of which the map is a small part part), yet my CPU was screaming, fans going off like crazy. How do I fix it so it's not so processor intensive?
Chrome says:
Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently
on .getImageData
// update this image's data with data from the canvas
this.data = context.getImageData(
The code is still in the Mapbox example but on first sight looks outdated and not very performance optimised. Did you found a solution to this?
Heres the issue:
https://github.com/mapbox/mapbox-gl-js/pull/12397
Related
I am using Phaser.js 3.55 to build a tile-based game. I have a tilemap, a layer, and a player sprite. I want to be able to click on a tile and have the player sprite move to that tile.
However, I'm having trouble getting the correct tile position when the camera is zoomed in or out. I've tried using layer.worldToTileX and layer.worldToTileY to calculate the tile position based on the pointer position, but it gives me different results depending on the zoom level.
Here's my code for the pointerdown event in the update-function:
function update (time, delta) {
controls.update(delta);
zoomLevel = this.cameras.main.zoom;
this.input.on("pointerdown", function (pointer) {
var x = layer.worldToTileX(pointer.x / zoomLevel );
var y = layer.worldToTileY(pointer.y / zoomLevel );
if (map.getTileAt(x, y)) {
var goalX = map.getTileAt(x, y).x * 32 + 16;
var goalY = map.getTileAt(x, y).y * 32 + 16;
this.tweens.add({
targets: player,
x: goalX,
y: goalY,
duration: 1000,
ease: 'Power4',
});
}
}, this);
}
What is the correct way to get the tile position based on the pointer position and the zoom level in Phaser.js?
At first I wrote the pointerdown-event in the create() function, which didn't update the zoom-scale I got by zoomLevel = this.cameras.main.zoom;. I took it to the update-function where zoomLevel would be updated, then trying to divide the current zoomLevel with the pointer.x and pointer.y. I expected the pointer so give me the currently clicked tile. Outcome: It shifted, depending on the zoomLevel.
My other code besides the update()-function
var game = new Phaser.Game(config);
var map;
var layer;
var player;
function preload () {
this.load.tilemapTiledJSON('tilemap', '/img/phaser/map.json');
this.load.image('base_tiles', '/img/phaser/outdoors.png');
this.load.spritesheet('player', '/img/phaser/dude.jpg', { frameWidth: 32, frameHeight: 48 });
}
function create () {
map = this.make.tilemap({ key: 'tilemap' });
const tileset = map.addTilesetImage('outdoors', 'base_tiles');
layer = map.createLayer(0, tileset, 0, 0);
player = this.add.sprite(500, 100, 'player', 4);
var cursors = this.input.keyboard.createCursorKeys();
var controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
acceleration: 0.06,
drag: 0.0005,
maxSpeed: 1.0
};
controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
}
You can get the position with the function getWorldPoint of the camera (link to the documentation), to get the exact position. No calculation needed
Here a demo showcasing this:
(use the Arrow UP/DOWN to zoom in or out, and click to select tile)
document.body.style = 'margin:0;';
var config = {
type: Phaser.AUTO,
width: 5 * 16,
height: 3 * 16,
zoom: 4,
pixelart: true,
scene: {
preload,
create
},
banner: false
};
function preload (){
this.load.image('tiles', 'https://labs.phaser.io/assets/tilemaps/tiles/super-mario.png');
}
function create () {
let infoText = this.add.text(2, 2, )
.setOrigin(0)
.setScale(.5)
.setDepth(100)
.setStyle({fontStyle: 'bold', fontFamily: 'Arial'});
// Load a map from a 2D array of tile indices
let level = [ [ 0, 0, 0, 0, 0],[ 0, 14, 13, 14, 0],[ 0, 0, 0, 0, 0]];
let map = this.make.tilemap({ data: level, tileWidth: 16, tileHeight: 16 });
let tiles = map.addTilesetImage('tiles');
let layer = map.createLayer(0, tiles, 0, 0);
const cam = this.cameras.main;
let tileSelector = this.add.rectangle(0, 0, 16, 16, 0xffffff, .5).setOrigin(0);
this.input.keyboard.on('keyup-UP', function (event) {
cam.zoom+=.25;
});
this.input.keyboard.on('keyup-DOWN', function (event) {
if(cam.zoom > 1){
cam.zoom-=.25;
}
});
this.input.on('pointerdown', ({x, y}) => {
let {x:worldX, y:worldY} = cam.getWorldPoint(x, y)
let tile = layer.getTileAtWorldXY(worldX, worldY);
tileSelector.setPosition(tile.pixelX, tile.pixelY);
infoText.setText(`selected TileID:${tile.index}`);
});
}
new Phaser.Game(config);
<script src="//cdn.jsdelivr.net/npm/phaser/dist/phaser.min.js"></script>
Using the sample code from Konvajs.org as a base (https://konvajs.org/docs/sandbox/Multi-touch_Scale_Stage.html), I have added a large SVG to a layer (4096 x 3444) to experiment with zoom / pan of a vector-based map, base64 encoded SVG in this instance. Initial impressions are good however during testing I experience an odd bug where during a pinch the view of the map would snap to a different location on the map not the area that I centred on.
Here is the code (map base64 code removed due to length):
// by default Konva prevent some events when node is dragging
// it improve the performance and work well for 95% of cases
// we need to enable all events on Konva, even when we are dragging a node
// so it triggers touchmove correctly
Konva.hitOnDragEnabled = true;
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
draggable: true,
});
var layer = new Konva.Layer();
var triangle = new Konva.RegularPolygon({
x: 190,
y: stage.height() / 2,
sides: 3,
radius: 80,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
});
var circle = new Konva.Circle({
x: 380,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
let bg = new Konva.Image({
width: 4096,
height: 3444
});
layer.add(bg);
var image = new Image();
image.onload = function() {
bg.image(image);
layer.draw();
};
image.src = 'data:image/svg+xml;base64,...';
function getDistance(p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
function getCenter(p1, p2) {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
};
}
var lastCenter = null;
var lastDist = 0;
stage.on('touchmove', function (e) {
e.evt.preventDefault();
var touch1 = e.evt.touches[0];
var touch2 = e.evt.touches[1];
if (touch1 && touch2) {
// if the stage was under Konva's drag&drop
// we need to stop it, and implement our own pan logic with two pointers
if (stage.isDragging()) {
stage.stopDrag();
}
var p1 = {
x: touch1.clientX,
y: touch1.clientY,
};
var p2 = {
x: touch2.clientX,
y: touch2.clientY,
};
if (!lastCenter) {
lastCenter = getCenter(p1, p2);
return;
}
var newCenter = getCenter(p1, p2);
var dist = getDistance(p1, p2);
if (!lastDist) {
lastDist = dist;
}
// local coordinates of center point
var pointTo = {
x: (newCenter.x - stage.x()) / stage.scaleX(),
y: (newCenter.y - stage.y()) / stage.scaleX(),
};
var scale = stage.scaleX() * (dist / lastDist);
stage.scaleX(scale);
stage.scaleY(scale);
// calculate new position of the stage
var dx = newCenter.x - lastCenter.x;
var dy = newCenter.y - lastCenter.y;
var newPos = {
x: newCenter.x - pointTo.x * scale + dx,
y: newCenter.y - pointTo.y * scale + dy,
};
stage.position(newPos);
lastDist = dist;
lastCenter = newCenter;
}
});
stage.on('touchend', function () {
lastDist = 0;
lastCenter = null;
});
layer.add(triangle);
layer.add(circle);
stage.add(layer);
I am unsure if this is due to the large size of the image and / or canvas or an inherent flaw in the example code from Konvas.js. This has been tested, with the same results, on 2 models of iPad Pro, iPhone X & 11, Android Pixel 3, 5 and 6 Pro.
Here is the code on codepen as an example: https://codepen.io/mr-jose/pen/WNXgbdG
Any help would be appreciated, thanks!
I faced the same issue and discovered that it was caused by the dragging functionality of the stage. Everytime if (stage.isDragging()) evaluated to true, the jump happened.
For me what worked was setting draggable to false while pinch zooming and back to true on touch end.
stage.on('touchmove', function (e) {
...
if (touch1 && touch2) {
stage.draggable(false);
....
}
});
stage.on('touchend', function (e) {
...
stage.draggable(true);
});
I know in traditional HTML5 canvas, we can use drawImage method (the longest one with 9 properties) and change frameX and frameY to make sprite sheet animation. But I am new to matter.js. I've checked matter.js document but still don't have any idea about how to animate my sprite. Here is my object:
const ball = Bodies.circle(340, 340, 10, {
density: 0.0005,
frictionAir: 0.06,
restitution: 0,
friction: 0,
render: {
sprite: {
texture: "images/blueMonster.png",
yScale: 0.2,
xScale: 0.2,
isStatic: true,
},
},
inertia: Infinity,
label: "ball",
});
World.add(world, ball);
If I need to provide more info to solve this problem, please let me know. Thank you very much for your time!
There may be a fundamental misconception here. Matter.js is a physics engine that can plug into any rendering front-end. You don't have to use the built-in MJS rendering engine which is primarily there for prototyping. You can use your existing HTML5 code or something like Phaser which has robust support for sprite sheets.
Here's a simple proof-of-concept using vanilla JS to render a sprite animation with MJS as the physics engine. The approach is to call Matter.Engine.update(engine); to run the engine each frame and use coin.position to draw the sprite. More complex animations might use vertices and angle as shown here and here in addition to the sprite sheet, but this is use-case dependent.
(async () => {
const image = await new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = reject;
image.src = "https://art.pixilart.com/c7f297523ce57fc.png";
});
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 250;
const engine = Matter.Engine.create();
const coin = Matter.Bodies.circle(100, 0, 100, {
density: 0.0005,
frictionAir: 0.06,
restitution: 0,
friction: 0,
});
const ground = Matter.Bodies.rectangle(
0, 350, 1500, 170, {isStatic: true}
);
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: canvas}
);
Matter.Composite.add(
engine.world, [coin, ground, mouseConstraint]
);
const w = 200;
const h = 170;
let frameNumber = 0;
(function rerender() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const offset = (~~frameNumber * w) % image.width;
const {x, y} = coin.position;
ctx.drawImage(
image, // image
offset, // sx
40, // sy
w, // sWidth
h, // sHeight
x - w / 2, // dx
y - h / 2, // dy
w, // dWidth
h // dHeight
);
frameNumber += 0.1;
Matter.Engine.update(engine);
requestAnimationFrame(rerender);
})();
})();
canvas {
border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<canvas></canvas>
Another approach is to plop in a gif or video--easily done since you're just making a webpage in the DOM that happens to have physics attached. See how to round an image in matter.js for an example.
There is a new Physics library http://wellcaffeinated.net/PhysicsJS/
I would like to play with it and use bitmap instead of "canvas path".
There is this function in https://github.com/wellcaffeinated/PhysicsJS/blob/master/examples/physicsjs-full.js
drawCircle: function(x, y, r, styles, ctx){
ctx = ctx || this.ctx;
ctx.beginPath();
this.setStyle( styles, ctx );
ctx.arc(x, y, r, 0, Pi2, false);
ctx.closePath();
ctx.stroke();
ctx.fill();
/*
var i = new Image();
i.src = "http://www.pngfactory.net/_png/_thumb/19626-bubka-R2D2.png";
i.onload = function() {
console.log("load");
ctx.drawImage(i, 0, 0);
};
*/
}
But when I add an img in canvas, img.src and load ; nothing happens…
Here is an "official" jsfiddle
http://jsfiddle.net/wellcaffeinated/KkDb6/
and a wiki https://github.com/wellcaffeinated/PhysicsJS/wiki/Fundamentals
Here is a solution made by the author of PhysicsJS :
http://jsfiddle.net/wellcaffeinated/mpbNF/
/**
* PhysicsJS by Jasper Palfree <wellcaffeinated.net>
* http://wellcaffeinated.net/PhysicsJS
*
* Simple Spin example for PhysicsJS
*/
Physics(function(world){
var viewWidth = 500;
var viewHeight = 300;
var renderer = Physics.renderer('canvas', {
el: 'viewport',
width: viewWidth,
height: viewHeight,
meta: false, // don't display meta data
styles: {
// set colors for the circle bodies
'circle' : {
strokeStyle: 'hsla(60, 37%, 17%, 1)',
lineWidth: 1,
fillStyle: 'hsla(60, 37%, 57%, 0.8)',
angleIndicator: 'hsla(60, 37%, 17%, 0.4)'
}
}
});
// add the renderer
world.add( renderer );
// render on each step
world.subscribe('step', function(){
world.render();
});
// bounds of the window
var viewportBounds = Physics.aabb(0, 0, viewWidth, viewHeight);
// constrain objects to these bounds
world.add(Physics.behavior('edge-collision-detection', {
aabb: viewportBounds,
restitution: 0.99,
cof: 0.99
}));
Physics.body('wheel', 'circle', function( parent ){
return {
// no need for an init
// spin the wheel at desired speed
spin: function( speed ){
// the wheels are spinning...
this.state.angular.vel = speed;
}
};
});
var myWheel = Physics.body('wheel', {
x: 40,
y: 30,
radius: 45
});
// Not a hack
myWheel.view = new Image();
myWheel.view.src = 'http://www.large-icons.com/stock-icons/free-large-twitter/round_button-icon.gif';
world.add( myWheel );
// for example, use jquery to listen for a button click, and spin the wheel on the next step
$('button').on('click', function(){
// wait for the next step before spinning the wheel
world.subscribe('step', function( data ){
myWheel.spin( 0.03 );
// only execute callback once
world.unsubscribe( 'step', data.handler );
});
});
// ensure objects bounce when edge collision is detected
world.add( Physics.behavior('body-impulse-response') );
// add some gravity
world.add( Physics.behavior('constant-acceleration') );
// subscribe to ticker to advance the simulation
Physics.util.ticker.subscribe(function( time, dt ){
world.step( time );
});
// start the ticker
Physics.util.ticker.start();
});
I have t his effect on this website
http://www.immersive-garden.com/
there's this point of light sparking, on hover you get the background, I want something similar without using flash
this is the script I'm using right now
/*
Particle Emitter JavaScript Library
Version 0.3
by Erik Friend
Creates a circular particle emitter of specified radius centered and offset at specified screen location. Particles appear outside of emitter and travel outward at specified velocity while fading until disappearing in specified decay time. Particle size is specified in pixels. Particles reduce in size toward 1px as they decay. A custom image(s) may be used to represent particles. Multiple images will be cycled randomly to create a mix of particle types.
example:
var emitter = new particle_emitter({
image: ['resources/particle.white.gif', 'resources/particle.black.gif'],
center: ['50%', '50%'], offset: [0, 0], radius: 0,
size: 6, velocity: 40, decay: 1000, rate: 10
}).start();
*/
particle_emitter = function (opts) {
// DEFAULT VALUES
var defaults = {
center: ['50%', '50%'], // center of emitter (x / y coordinates)
offset: [0, 0], // offset emitter relative to center
radius: 0, // radius of emitter circle
image: 'particle.gif', // image or array of images to use as particles
size: 1, // particle diameter in pixels
velocity: 10, // particle speed in pixels per second
decay: 500, // evaporation rate in milliseconds
rate: 10 // emission rate in particles per second
};
// PASSED PARAMETER VALUES
var _options = $.extend({}, defaults, opts);
// CONSTRUCTOR
var _timer, _margin, _distance, _interval, _is_chrome = false;
(function () {
// Detect Google Chrome to avoid alpha transparency clipping bug when adjusting opacity
if (navigator.userAgent.indexOf('Chrome') >= 0) _is_chrome = true;
// Convert particle size into emitter surface margin (particles appear outside of emitter)
_margin = _options.size / 2;
// Convert emission velocity into distance traveled
_distance = _options.velocity * (_options.decay / 1000);
// Convert emission rate into callback interval
_interval = 1000 / _options.rate;
})();
// PRIVATE METHODS
var _sparkle = function () {
// Pick a random angle and convert to radians
var rads = (Math.random() * 360) * (Math.PI / 180);
// Starting coordinates
var sx = parseInt((Math.cos(rads) * (_options.radius + _margin)) + _options.offset[0] - _margin);
var sy = parseInt((Math.sin(rads) * (_options.radius + _margin)) + _options.offset[1] - _margin);
// Ending Coordinates
var ex = parseInt((Math.cos(rads) * (_options.radius + _distance + _margin + 0.5)) + _options.offset[0] - 0.5);
var ey = parseInt((Math.sin(rads) * (_options.radius + _distance + _margin + 0.5)) + _options.offset[1] - 0.5);
// Pick from available particle images
var image;
if (typeof(_options.image) == 'object') image = _options.image[Math.floor(Math.random() * _options.image.length)];
else image = _options.image;
// Attach sparkle to page, then animate movement and evaporation
var s = $('<img>')
.attr('src', image)
.css({
zIndex: 10,
position: 'absolute',
width: _options.size + 'px',
height: _options.size + 'px',
left: _options.center[0],
top: _options.center[1],
marginLeft: sx + 'px',
marginTop: sy + 'px'
})
.appendTo('body')
.animate({
width: '1px',
height: '1px',
marginLeft: ex + 'px',
marginTop: ey + 'px',
opacity: _is_chrome ? 1 : 0
}, _options.decay, 'linear', function () { $(this).remove(); });
// Spawn another sparkle
_timer = setTimeout(function () { _sparkle(); }, _interval);
};
// PUBLIC INTERFACE
// This is what gets returned by "new particle_emitter();"
// Everything above this point behaves as private thanks to closure
return {
start:function () {
clearTimeout(_timer);
_timer = setTimeout(function () { _sparkle(); }, 0);
return(this);
},
stop:function () {
clearTimeout(_timer);
return(this);
},
centerTo:function (x, y) {
_options.center[0] = x;
_options.center[1] = y;
},
offsetTo:function (x, y) {
if ((typeof(x) == 'number') && (typeof(y) == 'number')) {
_options.center[0] = x;
_options.center[1] = y;
}
}
}
};
you probably need something like this: http://www.realcombiz.com/2012/09/customize-blackquote-with-light-bulb.html