Related
I'm currently using Phaser 3, although my question isn't technically restricted to that framework, as it's more of a general JS/canvas/maths question, but:
I have a line drawn with graphics(). It’s anchored at one end, and the other end is draggable. I made a quick demo and so far, so good - you can see what I have already on CodePen.
Dragging the marker around and redrawing the line is no problem, but what I’d like is for that line to have a maximum length of 100, so even if you’re still dragging beyond that point, the line would still follow the mouse, but not get any longer than 100. Dragging inside that maximum radius, the line would shrink as normal.
I’ve put together a visual that hopefully explains it:
The issue is that I suspect this is VERY MATHS and I am very, very weak with maths. Could anyone explain like I’m five what I need to do to my code to achieve this?
Edit: Adding code in a snippet here, as requested:
var config = {
type: Phaser.AUTO,
width: 800,
height: 400,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
scene: {
preload: preload,
create: create,
update: update
}
};
var path;
var curve;
var graphics;
var game = new Phaser.Game(config);
function preload() {
this.load.spritesheet('dragcircle', 'https://labs.phaser.io/assets/sprites/dragcircle.png', { frameWidth: 16 });
}
function create() {
graphics = this.add.graphics();
path = { t: 0, vec: new Phaser.Math.Vector2() };
curve = new Phaser.Curves.Line([ 400, 390, 300, 230 ]);
var point0 = this.add.image(curve.p0.x, curve.p0.y, 'dragcircle', 0);
var point1 = this.add.image(curve.p1.x, curve.p1.y, 'dragcircle', 0).setInteractive();
point1.setData('vector', curve.p1);
this.input.setDraggable(point1);
this.input.on('drag', function (pointer, gameObject, dragX, dragY) {
gameObject.x = dragX;
gameObject.y = dragY;
gameObject.data.get('vector').set(dragX, dragY);
});
this.input.on('dragend', function (pointer, gameObject) {
let distance = Phaser.Math.Distance.Between(curve.p0.x, curve.p0.y, curve.p1.x, curve.p1.y);
console.log(distance);
});
}
function update() {
graphics.clear();
graphics.lineStyle(2, 0xffffff, 1);
curve.draw(graphics);
curve.getPoint(path.t, path.vec);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/3.55.2/phaser.min.js"></script>
You are right, you would need some math, but phaser has many helper functions, that will do the heavy lifting.
The main idea is, of this solution is
define a maxLength
get the the new point on drag, and create a real Phaser Vector2
here is some math is needed, to create the vector, just calculate destination point minus origin point
new Phaser.Math.Vector2(pointer.x - point0.x, pointer.y - point0.y) (origin point being the starting point of the desired vector, and destination point being the mouse pointer)
calculate the length of the created vector and compare it with the maxLength
if too long adjust the vector, with the handy function setLength (link to the documentation, this is where you would have needed math, but thankfully Phaser does it for us)
set the new coordinates for point1 and the curve endpoint
Here a quick demo (based on your code):
var config = {
type: Phaser.AUTO,
width: 500,
height: 170,
scene: {
preload: preload,
create: create,
update: update
}
};
var curve;
var graphics;
var game = new Phaser.Game(config);
function preload() {
this.load.spritesheet('dragcircle', 'https://labs.phaser.io/assets/sprites/dragcircle.png', { frameWidth: 16 });
}
function create() {
graphics = this.add.graphics();
curve = new Phaser.Curves.Line([ config.width/2, config.height - 20, config.width/2, 10 ]);
// define a length, could be a global constant
let maxLength = curve.p0.y - curve.p1.y;
var point0 = this.add.image(curve.p0.x, curve.p0.y, 'dragcircle', 0);
var point1 = this.add.image(curve.p1.x, curve.p1.y, 'dragcircle', 0).setInteractive();
this.input.setDraggable(point1);
// Just add for Debug Info
this.add.circle(curve.p0.x, curve.p0.y, maxLength)
.setStrokeStyle(1, 0xffffff, .5)
this.input.on('drag', function (pointer) {
let vector = new Phaser.Math.Vector2(pointer.x - point0.x, pointer.y - point0.y);
let distance = Phaser.Math.Distance.Between( point0.x, point0.y, pointer.x, pointer.y);
if(distance > maxLength){
vector.setLength(maxLength);
}
point1.x = point0.x + vector.x;
point1.y = point0.y + vector.y;
curve.p1.x = point1.x;
curve.p1.y = point1.y;
});
// NOT REALLY NEEDED
/*this.input.on('dragend', function (pointer, gameObject) {
let distance = Phaser.Math.Distance.Between(curve.p0.x, curve.p0.y, curve.p1.x, curve.p1.y);
console.log(distance);
});*/
}
function update() {
graphics.clear();
graphics.lineStyle(2, 0xffffff, 1);
curve.draw(graphics);
}
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>
Optional - Code Version using Phaser.GameObjects.Line:
This uses less code, and thanks to the Line GameObject (link to Documentation), you can directly use the vector to update the line, and also don't need the update function, graphics and so.
const config = {
type: Phaser.CANVAS,
width: 500,
height: 160,
scene: {
create
}
};
const game = new Phaser.Game(config);
const MAX_LINE_LENGTH = 100;
function create() {
let points = [ {x: config.width/2, y: config.height - 20}, {x: config.width/2, y: config.height - 120} ];
let point0 = this.add.circle(points[0].x, points[0].y, 6)
.setStrokeStyle(4, 0xff0000);
let point1 = this.add.circle(points[1].x, points[1].y, 6)
.setStrokeStyle(4, 0xff0000)
.setInteractive();
this.input.setDraggable(point1);
// Just add for Debug Info
this.add.circle(point0.x, point0.y, MAX_LINE_LENGTH)
.setStrokeStyle(1, 0xffffff, .5);
let line = this.add.line(points[0].x, points[0].y, 0, 0, 0, -100, 0x00ff00)
.setOrigin(0);
this.input.on('drag', function (pointer) {
let vector = new Phaser.Math.Vector2(pointer.x - point0.x, pointer.y - point0.y);
let distance = Phaser.Math.Distance.Between( point0.x, point0.y, pointer.x, pointer.y);
if(distance > MAX_LINE_LENGTH){
vector.setLength(MAX_LINE_LENGTH);
}
point1.x = point0.x + vector.x;
point1.y = point0.y + vector.y;
line.setTo(0, 0, vector.x, vector.y);
});
}
<script src="//cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>
Please help me out as I am not able to run the example code from matter. When I use this code with <html>, it shows a blank screen. There is no tutorial for making a cloth simulation in google or youtube. It would be appreciated if you would help. I use sublime text for editing.
var Example = Example || {};
Example.cloth = function() {
var Engine = Matter.Engine,
Render = Matter.Render,
Runner = Matter.Runner,
Body = Matter.Body,
Composites = Matter.Composites,
MouseConstraint = Matter.MouseConstraint,
Mouse = Matter.Mouse,
Composite = Matter.Composite,
Bodies = Matter.Bodies;
// create engine
var engine = Engine.create(),
world = engine.world;
// create renderer
var render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600
}
});
Render.run(render);
// create runner
var runner = Runner.create();
Runner.run(runner, engine);
// see cloth function defined later in this file
var cloth = Example.cloth.cloth(200, 200, 20, 12, 5, 5, false, 8);
for (var i = 0; i < 20; i++) {
cloth.bodies[i].isStatic = true;
}
Composite.add(world, [
cloth,
Bodies.circle(300, 500, 80, { isStatic: true, render: { fillStyle: '#060a19' }}),
Bodies.rectangle(500, 480, 80, 80, { isStatic: true, render: { fillStyle: '#060a19' }}),
Bodies.rectangle(400, 609, 800, 50, { isStatic: true })
]);
// add mouse control
var mouse = Mouse.create(render.canvas),
mouseConstraint = MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.98,
render: {
visible: false
}
}
});
Composite.add(world, mouseConstraint);
// keep the mouse in sync with rendering
render.mouse = mouse;
// fit the render viewport to the scene
Render.lookAt(render, {
min: { x: 0, y: 0 },
max: { x: 800, y: 600 }
});
// context for MatterTools.Demo
return {
engine: engine,
runner: runner,
render: render,
canvas: render.canvas,
stop: function() {
Matter.Render.stop(render);
Matter.Runner.stop(runner);
}
};
};
Example.cloth.title = 'Cloth';
Example.cloth.for = '>=0.14.2';
/**
* Creates a simple cloth like object.
* #method cloth
* #param {number} xx
* #param {number} yy
* #param {number} columns
* #param {number} rows
* #param {number} columnGap
* #param {number} rowGap
* #param {boolean} crossBrace
* #param {number} particleRadius
* #param {} particleOptions
* #param {} constraintOptions
* #return {composite} A new composite cloth
*/
Example.cloth.cloth = function(xx, yy, columns, rows, columnGap, rowGap, crossBrace, particleRadius, particleOptions, constraintOptions) {
var Body = Matter.Body,
Bodies = Matter.Bodies,
Common = Matter.Common,
Composites = Matter.Composites;
var group = Body.nextGroup(true);
particleOptions = Common.extend({ inertia: Infinity, friction: 0.00001, collisionFilter: { group: group }, render: { visible: false }}, particleOptions);
constraintOptions = Common.extend({ stiffness: 0.06, render: { type: 'line', anchors: false } }, constraintOptions);
var cloth = Composites.stack(xx, yy, columns, rows, columnGap, rowGap, function(x, y) {
return Bodies.circle(x, y, particleRadius, particleOptions);
});
Composites.mesh(cloth, columns, rows, crossBrace, constraintOptions);
cloth.label = 'Cloth Body';
return cloth;
};
if (typeof module !== 'undefined') {
module.exports = Example.cloth;
}
If you are looking to just get something on the screen, the fastest way is to use matter.js from a CDN.
You are seeing a blank screen because the example is meant to be bundled using a tool like Webpack or Parcel with other javascript files which would call the cloth() function. So all you need to do is to add Example.cloth(); at the bottom of the example code and it will execute upon loading the page.
The snippet below goes in the body tag of your HTML page which you can then open in your browser.
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js" integrity="sha512-5T245ZTH0m0RfONiFm2NF0zcYcmAuNzcGyPSQ18j8Bs5Pbfhp5HP1hosrR8XRt5M3kSRqzjNMYpm2+it/AUX/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
var Example = Example || {};
Example.cloth = function () {
var Engine = Matter.Engine,
Render = Matter.Render,
Runner = Matter.Runner,
Body = Matter.Body,
Composites = Matter.Composites,
MouseConstraint = Matter.MouseConstraint,
Mouse = Matter.Mouse,
Composite = Matter.Composite,
Bodies = Matter.Bodies;
// create engine
var engine = Engine.create(),
world = engine.world;
// create renderer
var render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600
}
});
Render.run(render);
// create runner
var runner = Runner.create();
Runner.run(runner, engine);
// see cloth function defined later in this file
var cloth = Example.cloth.cloth(200, 200, 20, 12, 5, 5, false, 8);
for (var i = 0; i < 20; i++) {
cloth.bodies[i].isStatic = true;
}
Composite.add(world, [
cloth,
Bodies.circle(300, 500, 80, { isStatic: true, render: { fillStyle: '#060a19' } }),
Bodies.rectangle(500, 480, 80, 80, { isStatic: true, render: { fillStyle: '#060a19' } }),
Bodies.rectangle(400, 609, 800, 50, { isStatic: true })
]);
// add mouse control
var mouse = Mouse.create(render.canvas),
mouseConstraint = MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.98,
render: {
visible: false
}
}
});
Composite.add(world, mouseConstraint);
// keep the mouse in sync with rendering
render.mouse = mouse;
// fit the render viewport to the scene
Render.lookAt(render, {
min: { x: 0, y: 0 },
max: { x: 800, y: 600 }
});
// context for MatterTools.Demo
return {
engine: engine,
runner: runner,
render: render,
canvas: render.canvas,
stop: function () {
Matter.Render.stop(render);
Matter.Runner.stop(runner);
}
};
};
Example.cloth.title = 'Cloth';
Example.cloth.for = '>=0.14.2';
/**
* Creates a simple cloth like object.
* #method cloth
* #param {number} xx
* #param {number} yy
* #param {number} columns
* #param {number} rows
* #param {number} columnGap
* #param {number} rowGap
* #param {boolean} crossBrace
* #param {number} particleRadius
* #param {} particleOptions
* #param {} constraintOptions
* #return {composite} A new composite cloth
*/
Example.cloth.cloth = function (xx, yy, columns, rows, columnGap, rowGap, crossBrace, particleRadius, particleOptions, constraintOptions) {
var Body = Matter.Body,
Bodies = Matter.Bodies,
Common = Matter.Common,
Composites = Matter.Composites;
var group = Body.nextGroup(true);
particleOptions = Common.extend({ inertia: Infinity, friction: 0.00001, collisionFilter: { group: group }, render: { visible: false } }, particleOptions);
constraintOptions = Common.extend({ stiffness: 0.06, render: { type: 'line', anchors: false } }, constraintOptions);
var cloth = Composites.stack(xx, yy, columns, rows, columnGap, rowGap, function (x, y) {
return Bodies.circle(x, y, particleRadius, particleOptions);
});
Composites.mesh(cloth, columns, rows, crossBrace, constraintOptions);
cloth.label = 'Cloth Body';
return cloth;
};
Example.cloth();
</script>
I'm trying to build a screen with circular bodies (bubbles). The bubbles should be able to change the scale with some interval, e.g. every 3 seconds and random value (from 1 to 3). The scale should change the size of a bubble and its mass.
Calling
Matter.Body.scale
doesn't make any effect. Bubbles keep staying the same. Maybe because I'm not using sprites with textures, but the Game-Engine entities with renderer as React.PureComponent instead. Not sure
Bubble node:
export interface BubbleProps {
body: Matter.Body;
item: Item;
}
class BubbleNode extends React.Component<BubbleProps> {
render() {
const {body, item} = this.props;
const x = body.position.x - body.circleRadius;
const y = body.position.y - body.circleRadius;
const style: ViewStyle = {
position: 'absolute',
left: x,
top: y,
width: body.circleRadius * 2,
height: body.circleRadius * 2,
backgroundColor: item.color,
borderRadius: body.circleRadius,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
padding: 3,
};
return <View style={style}>{item.content}</View>;
}
}
Bubble Entity:
const body = Matter.Bodies.circle(
randomX,
randomY,
RADIUS, //default radius 25
{
mass: 30,
}
);
return {
body,
item,
renderer: BubbleNode,
};
Update method:
// find out if the entity was already added to the world
if (Object.prototype.hasOwnProperty.call(entities, id)) {
//update scale
const scale = bubble.item.scale
Matter.Body.scale(bubble.body, scale, scale)
} else {
//add new entity
entities[id] = bubble;
Matter.World.add(physics.world, [bubble.body]);
}
Also, I need to make this scaling smooth
Edit:
Interesting thing. I was able to scale the bubbles using Matter.Body.scale but right before adding to the world. I wonder if there is a method to update the bodies after adding to the world.
Final edit
this is the method I'm using to set up the world and specify entities for the GameEngine framework
private setupWorld = (layout: LayoutRectangle) => {
const engine = Matter.Engine.create({enableSleeping: false});
const world = engine.world;
world.gravity.x = 0;
world.gravity.y = 0;
//center gravity body
Matter.use(MatterAttractors);
var attractiveBody = Matter.Bodies.circle(
layout.width / 2,
layout.height / 2,
1,
{
isSensor: true,
plugin: {
attractors: [centerGravity],
},
},
);
Matter.World.add(world, attractiveBody);
//walls
const wallThickness = 5;
let floor = Matter.Bodies.rectangle(
layout.width / 2,
layout.height - wallThickness / 2,
layout.width,
wallThickness,
{isStatic: true},
);
let ceiling = Matter.Bodies.rectangle(
layout.width / 2,
wallThickness / 2,
layout.width,
wallThickness,
{isStatic: true},
);
let left = Matter.Bodies.rectangle(
wallThickness / 2,
layout.height / 2,
wallThickness,
layout.height,
{isStatic: true},
);
let right = Matter.Bodies.rectangle(
layout.width - wallThickness / 2,
layout.height / 2,
wallThickness,
layout.height,
{isStatic: true},
);
Matter.World.add(world, [floor, ceiling, left, right]);
//basic entitites
this.entities = {
physics: {engine, world},
floor: {body: floor, color: 'green', renderer: Wall},
ceiling: {body: ceiling, color: 'green', renderer: Wall},
left: {body: left, color: 'green', renderer: Wall},
right: {body: right, color: 'green', renderer: Wall},
};
};
And here is the method that will be triggered by a parent Component with some interval
public updateNodes = (items: Item[]) => {
if (!this.state.mounted || !this.entities || !this.entities.physics || !this.layout) {
console.log('Missiing required data');
return;
}
const layout = this.layout
const entities = this.entities
const bubbles: BubbleEntity[] = items.map((item) => {
const randomX = randomPositionValue(layout.width);
const randomY = randomPositionValue(layout.height);
const body = Matter.Bodies.circle(
randomX,
randomY,
RADIUS, {
mass: 30,
}
);
body.label = item.id
return {
body,
item,
renderer: BubbleNode,
};
});
const physics = this.entities.physics as PhysicsEntity;
const allBodies = Matter.Composite.allBodies(physics.world)
bubbles.forEach((bubble) => {
//update existing or add new
const id = `bubble#${bubble.item.id}`;
if (Object.prototype.hasOwnProperty.call(entities, id)) {
//do physical node update here
//update scale and mass
const scale = bubble.item.scale
console.log('Updating item', id, scale);
//right her there used to be an issue because I used **bubble.body** which was not a correct reference to the world's body.
//so when I started to use allBodies array to find a proper reference of the body, everything started to work
let body = allBodies.find(item => item.label === bubble.item.id)
if (!!body) {
const scaledRadius = RADIUS*scale
const current = body.circleRadius || RADIUS
const scaleValue = scaledRadius/current
Matter.Body.scale(body, scaleValue, scaleValue)
}else{
console.warn('Physycal body not found, while the entity does exist');
}
} else {
console.log('Adding entity to the world');
entities[id] = bubble;
Matter.World.add(physics.world, [bubble.body]);
}
});
this.entities = entities
};
In the future, I'm going to improve that code, I will use some variables for the body and will create a matter.js plugin that will allow me to scale the body smoothly and not instant as it works right now. Also, the method above requires some clean, short implementation instead of that garbage I made attempting to make it work
Your example isn't exactly complete; it's not clear how (or if) the MJS engine is running. The first step is to make sure you have an actual rendering loop using calls to Matter.Engine.update(engine); in a requestAnimationFrame loop.
React doesn't seem critical here. It shouldn't affect the result since x/y coordinates and radii for each body in the MJS engine are extracted and handed to the View component, so the data flows in one direction. I'll leave it out for the rest of this example but it should be easy to reintroduce once you have the MJS side working to your satisfaction.
The way to scale a body in MJS is to call Matter.body.scale(body, scaleX, scaleY). This function recomputes other physical properties such as mass for the body.
There's an annoying caveat with this function: instead of setting an absolute scale as a JS canvas context or CSS transformation might, it sets a relative scale. This means each call to this function changes the baseline scaling for future calls. The problem with this is that rounding errors can accumulate and drift can occur. It also makes applying custom tweens difficult.
Workarounds are likely to be dependent on what the actual animation you hope to achieve is (and may not even be necessary), so I'll avoid prescribing anything too specific other than suggesting writing logic relative to the radius as the point of reference to ensure it stays within bounds. Other workarounds can include re-creating and scaling circles per frame.
Another gotcha when working with circles is realizing that MJS circles are n-sided polygons, so small circles lose resolution when scaled up. Again, this is use-case dependent, but you may wish to create a Bodies.polygon with more sides than would be created by Bodies.circle if you experience unusual behavior.
That said, here's a minimal, complete example of naive scaling that shows you how you can run an animation loop and call scale to adjust it dynamically over time. Consider it a proof of concept and will require adaptation to work for your use case (whatever that may be).
const engine = Matter.Engine.create();
const circles = [...Array(15)].map((_, i) => {
const elem = document.createElement("div");
elem.classList.add("circle");
document.body.append(elem);
const body = Matter.Bodies.circle(
// x, y, radius
10 * i + 60, 0, Math.random() * 5 + 20
);
return {
elem,
body,
offset: i,
scaleAmt: 0.05,
speed: 0.25,
};
});
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: document.body}
);
const walls = [
Matter.Bodies.rectangle(
// x, y, width, height
innerWidth / 2, 0, innerWidth, 40, {isStatic: true}
),
Matter.Bodies.rectangle(
innerWidth / 2, 180, innerWidth, 40, {isStatic: true}
),
Matter.Bodies.rectangle(
0, innerHeight / 2, 40, innerHeight, {isStatic: true}
),
Matter.Bodies.rectangle(
300, innerHeight / 2, 40, innerHeight, {isStatic: true}
),
];
Matter.Composite.add(
engine.world,
[mouseConstraint, ...walls, ...circles.map(e => e.body)]
);
/*
naive method to mitigate scaling drift over time.
a better approach would be to scale proportional to radius.
*/
const baseScale = 1.00063;
(function update() {
requestAnimationFrame(update);
circles.forEach(e => {
const {body, elem} = e;
const {x, y} = body.position;
const {circleRadius: radius} = body;
const scale = baseScale + e.scaleAmt * Math.sin(e.offset);
Matter.Body.scale(body, scale, scale);
e.offset += e.speed;
elem.style.top = `${y - radius / 2}px`;
elem.style.left = `${x - radius / 2}px`;
elem.style.width = `${2 * radius}px`;
elem.style.height = `${2 * radius}px`;
elem.style.transform = `rotate(${body.angle}rad)`;
});
Matter.Engine.update(engine);
})();
* {
margin: 0;
padding: 0;
}
body, html {
height: 100%;
}
.circle {
border-radius: 50%;
position: absolute;
cursor: move;
background: rgb(23, 0, 36, 1);
background: linear-gradient(
90deg,
rgba(23, 0, 36, 1) 0%,
rgba(0, 212, 255, 1) 100%
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
I'm trying to do what the image has shown, to have different canvas within a single page. Each of them has different properties and options. So far it works but
Is this a proper way to do it?
Will it be slow loading on a live server? I am experimenting it on a localhost atm.
// -------- FIRST Canvas -----
<script>
window.addEventListener('load', function() {
//Fetch our canvas
var canvas = document.getElementById('world');
// module aliases
var Engine = Matter.Engine,
Render = Matter.Render,
World = Matter.World,
Bodies = Matter.Bodies;
// create an engine
var engine = Engine.create();
// create a renderer
var render = Render.create({
canvas: canvas,
engine: engine,
options: {
width: 500,
height: 500,
background: '#000',
wireframes: false,
showAngleIndicator: false
});
// create two boxes and a ground
var boxA = Bodies.rectangle(400, 200, 80, 80);
var boxB = Bodies.rectangle(450, 50, 80, 80);
var ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true });
// add all of the bodies to the world
World.add(engine.world, [boxA, boxB, ground]);
// run the engine
Engine.run(engine);
// run the renderer
Render.run(render);
});
</script>
<canvas id="world"></canvas>
// -------- SECOND canvas -----
<script>
window.addEventListener('load', function() {
//Fetch our canvas
var canvas = document.getElementById('world2');
// module aliases
var Engine = Matter.Engine,
Render = Matter.Render,
World = Matter.World,
Bodies = Matter.Bodies;
// create an engine
var engine = Engine.create();
// create a renderer
var render = Render.create({
canvas: canvas,
engine: engine,
options: {
width: 500,
height: 500,
background: '#000',
wireframes: false,
showAngleIndicator: false
});
// create two boxes and a ground
var boxA = Bodies.rectangle(400, 200, 80, 80);
var boxB = Bodies.rectangle(450, 50, 80, 80);
var ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true });
// add all of the bodies to the world
World.add(engine.world, [boxA, boxB, ground]);
// run the engine
Engine.run(engine);
// run the renderer
Render.run(render);
});
</script>
<canvas id="world2"></canvas>
I'm trying to implement a rotation and zoom feature with a slider. I need an image to always rotate from the center of the viewport. The main idea is changing the position and offset of the stage after drag event. Here's what I've tried
const width = window.innerWidth
const height = window.innerHeight
// scale is temp
let stage = new Konva.Stage({
container: 'canvas',
width,
height,
draggable: true,
offset: {
x: width / 2,
y: height / 2
},
x: width / 2,
y: height / 2
})
let mapLayer = new Konva.Layer()
let imageObj = new Image()
imageObj.onload = function () {
let map = new Konva.Image({
image: imageObj,
prevX: 0,
prevY: 0
})
layer.add(map)
stage.add(mapLayer)
stage.on('dragstart', () => {
map.prevX = map.getAbsolutePosition().x
map.prevY = map.getAbsolutePosition().y
})
stage.on('dragend', () => {
let curX = map.getAbsolutePosition().x
let curY = map.getAbsolutePosition().y
let deltaX = Math.abs(map.prevX - curX)
let deltaY = Math.abs(map.prevY - curY)
if (curX > map.prevX) {
stage.offsetX(stage.offsetX() - deltaX)
stage.x(stage.x() - deltaX)
} else {
stage.offsetX(stage.offsetX() + deltaX)
stage.x(stage.x() + deltaX)
}
if (curY > map.prevY) {
stage.offsetY(stage.offsetY() - deltaY)
stage.y(stage.y() - deltaY)
} else {
stage.offsetY(stage.offsetY() + deltaY)
stage.y(stage.y() + deltaY)
}
stage.draw()
})
}
(if you want a full source code, clone it from here and run yarn run dev from terminal, the app lives on localhost:3000
It works fine when the image is in the normal position (not zoomed and rotated yet) but after ANY kind of rotation or zooming, dragging the stage will cause the stage to be re-positioned in a weird fashion (the offset is still correct though). How can I set position and offset correctly?
The solution to your initial issue below.
Note that I put a circle and a tooltip as well to a single layer to be able to drag them as a single one. Also, it should be added after map to have binding work.
import Konva from 'konva'
import map from './../resources/unpar.svg'
const width = window.innerWidth
const height = window.innerHeight
// scale is temp
let stage = new Konva.Stage({
container: 'canvas',
width,
height,
offset: {
x: width / 2,
y: height / 2
},
x: width / 2,
y: height / 2
})
let layer = new Konva.Layer({draggable: true})
let testCircle = new Konva.Circle({
x: 633,
y: 590,
radius: 10,
fill: 'white',
stroke: 'black'
})
testCircle.on('mousemove', function () {
tooltip.position({
x: testCircle.x() - 90,
y: testCircle.y() - 50
})
tooltip.text('Pintu Depan Gedung 10')
tooltip.show()
layer.batchDraw()
})
testCircle.on('mouseout', function () {
tooltip.hide()
layer.draw()
})
var tooltip = new Konva.Text({
text: '',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
textFill: 'white',
fill: 'black',
alpha: 0.75,
visible: false
})
let imageObj = new Image()
imageObj.onload = function () {
const map = new Konva.Image({
image: imageObj,
})
layer.add(map)
layer.add(testCircle)
layer.add(tooltip)
stage.add(layer)
}
imageObj.src = map
export default stage