JointJs: Scale custom shape html along with element (using paperScroller.zoom) - svg

Using something like paperScroller.zoom(0.2, { max: 5 }); only causes svg elements to be zoomed, whereas in my custom shape, I've used html as well, which doesn't scale in tandem.
Since there's no model.change event firing on scaling, the updateBox method in ElementView doesn't get called and so the html elements don't sync their dimensions and positioning accordingly. Is there a way to work around this?

Extending on Marc_Alx's answer.
Calling paper.translate() and paper.scale() fires 'translate' and 'scale' events on the paper.
Custom Elements can listen to these events on their custom ElementView.
For example, if you scale on mousewheel event:
paper.on('blank:mousewheel', (event, x, y, delta) => {
const scale = paper.scale();
paper.scale(scale.sx + (delta * 0.01), scale.sy + (delta * 0.01),);
});
Override render methods of your custom ElementView to listen to 'scale' event on the paper.
render(...args) {
joint.dia.ElementView.prototype.render.apply(this, args);
this.listenTo(this.paper, 'scale', this.updateBox);
this.listenTo(this.paper, 'translate', this.updateBox);
this.paper.$el.prepend(this.$box);
this.updateBox();
return this;
},
And the custom ElementView's updateBox should retrieve the scale value from the paper.
updateBox() {
if (!this.paper) return;
const bbox = this.getBBox({ useModelGeometry: true });
const scale = joint.V(this.paper.viewport).scale();
// custom html updates
this.$box.find('label').text(this.model.get('label'));
this.$box.find('p').text(this.model.get('response'));
// end of custom html updates
this.$box.css({
transform: `scale(${scale.sx},${scale.sy})`,
transformOrigin: '0 0',
width: bbox.width / scale.sx,
height: bbox.height / scale.sy,
left: bbox.x,
top: bbox.y,
});
}

I've found a workaround for those who are not using rappid.js (paperScroller is only available for rappid).
Assuming you have an element similar to this one : http://jointjs.com/tutorial/html-elements
My answer is inspired from Murasame answer's from How to scale jonitjs graphs? and assume that you update scale of your paper on mousewheel (+0.1 -0.1).
First inside your custom ElementView store a numeric property (initialized to 1) that will store the scale, let's name it scale (accessed via this.scale).
Next in the overrided method render (of your cutom ElementView) listen to mousewheel events of the paper : this.paper.$el.on('mousewheel',…) in the handler update the scaleproperty (+=0.1 or -=0.1), then call updateBox
Finally in the updateBox method at this line :
this.$box.css({ width: bbox.width, height: bbox.height, left: bbox.x, top: bbox.y, transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)' });
Multiply width, height, x, y by this.scale to get :
this.$box.css({ width: bbox.width*this.scale, height: bbox.height*this.scale, left: bbox.x*this.scale, top: bbox.y*this.scale, transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)' });
I think it's a good start for implementing this behavior, you should also size the content of your element.

Answering this since I've spent hours trying to find a solution for this. And it turned out there's a perfectly working demo (but somehow Google always gives your the outdated tutorial. See: https://github.com/clientIO/joint/issues/1220) on JointJS. It's quite similar to Raunak Mukhia's answer though.
https://github.com/clientIO/joint/blob/master/demo/shapes/src/html.js

Related

Using Konvajs, I need to drag an object that is under another object WITHOUT bringing it to the top

Using Konvajs, I need to drag an object that is under another object WITHOUT bringing the bottom object to the top, the top object is NOT draggable.
Would appreciate any help, thank you.
Needing to detect clicks in stacked objects is a common requirement. Fortunately Konva makes this easy. Every shape will be 'listening' for events by default. So mouse over, mouse exit, click, double click, and their touch counterparts are all available to us (see docs).
In your case you need to make the shape that is higher up the stack stop listening so that one further down can hear it.
In the example below I am using the shape.listening(value) property which controls whether the shape is listening for events (true value) or not (false value).
Looking at the snippet, if we click the green spot (where the rectangles overlap), with the upper shape set listening(false) events pass through it to the next shape in the stack and we are informed that the click was on the lower shape. With the upper shape listening(true) the upper shape receives and eats the events.
I am also using an efficient approach to event detection, which is to listen for events on the layer rather than setting listeners per shape. In the case of a few shapes on the layer, using event listeners per shape is not going to be an issue, but when you have hundreds of shapes on the layer it will affect performance.
Of course delegating the listener to the layer leaves a requirement to recognise which shape was clicked. To do this I could have used logic to match on the shape names.
// when the user starts to drag shape...
layer.on('click', function(evt) {
if (evt.target.hasName('upperRect')) {
alert('Click on upperRect');
}
if (evt.target.hasName('lowerRect')) {
alert('Click on upperRect');
}
...
})
but since all I need to do here is show the name of the clicked shape, I chose to simply use
// when the user starts to drag shape...
layer.on('click', function(evt) {
showEvent('Click on ' + evt.target.name());
})
Finally it is worth noting that another approach to getting the clicked shape is to use the layer.getIntersection() method which requires a point parameter and will return the first shape (not a list of shapes!) that is on the layer at that point.
layer.on("click", function() {
var target = layer.getIntersection(stage.getPointerPosition());
if (target) {
alert('clicked on ' + target.name());
}
});
Bonus note: layer.on('click') does not detect clicks on the empty part of the layer.
// Set up a stage
stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
}),
// add a layer to draw on
layer = new Konva.Layer(),
// make the lower rect
rectLower = new Konva.Rect({
name: 'Lower rect',
x: 40,
y: 20,
width: 100,
height: 40,
stroke: 'cyan',
fill: 'cyan',
fillEnabled: true
}),
// Make the upper rect
rectUpper = rectLower.clone({
name: 'Upper rect',
stroke: 'magenta',
fill: 'magenta',
x: 0,
y: 0
}),
// add a click target for user
spot = new Konva.Circle({
x: 80,
y: 30,
fill: 'lime',
radius: 10,
listening: false
});
// Add the layer to the stage and shapes to layer
stage.add(layer);
layer.add(rectLower, rectUpper, spot)
// when the user clicks a shape...
layer.on('click', function(evt) {
showEvent('Click on ' + evt.target.name());
})
// Start / stop listening for events on to rect when checkbox changes.
$('#upperListening').on('change', function(){
switch ($(this).is(':checked')){
case true:
rectUpper.listening(true);
showEvent('Upper rect <b>is</b> listening...');
break;
case false:
rectUpper.listening(false);
showEvent('Upper rect <b>is not</b> listening...');
break;
}
// Important - if we change lsitening shapes we have to refresh hit checking list.
layer.drawHit();
})
stage.draw();
function showEvent(msg){
$('#info').html(msg)
}
body {
margin: 10;
padding: 10;
overflow: hidden;
background-color: #f0f0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#^3/konva.min.js"></script>
<p>Click the green dot. Watch the events. Click the tickbox to stop upper rect catching events.</p>
<p><input type='checkbox' id='upperListening' checked='checked' ><label for='upperListening'>Upper is listening</label>
<p id='info'>Events show here</p>
<div id="container"></div>

jquery: fancybox 3, responsive image maps and zoomable content

I want to use image-maps inside fancybox 3. Goal is to display mountain panoramas, where the user could point on a summit and get name and data. The usual recommendation is to use a SVG based image map for this like in this pen. Due to the size of the images the fancybox zoom functionality is important.
While fancybox will display SVGs as an image like in this pen it is not possible to use the <image> tag with an external source inside the SVG file. Even worse: SVG files used as source of an <img> tag would not show the map functionality (see this question).
I tried to replace the <img> tag in fancybox with an <object> tag using the SVG file as data attribute. This shows the image with the map functionality correctly but fancybox won't zoom it any more.
So eventually the question boils down to how I can make an <object> (or an inline SVG or an iframe) zoomable just like an image in fancybox 3.
I'm open to other solutions as well. I only want to use fancybox to keep the appearance and usage the same as other image galleries on the same page. I'd even use an old style <map>, where I would change the coords using jquery to have it responsive. I tried that, attaching the map manually in developer tools as well as programmatically in afterLoad event handler, but apparently this doesn't work in fancybox 3 either.
The areas are polygons, so using positioned div's as overlays is no solution.
Edit: I just discovered that I can replace <img> with a <canvas> in fancybox without loosing the zoom and drag functionality. So in theory it would be possible to use canvas paths and isPointInPath() methode. Unfortunately I need more than one path, which requires the Path2D object, which is not available in IE...
Since all options discussed in the question turned out to be not feasible and I found the pnpoly point in polygon algorithm, I did the whole thing on my own. I put the coordinates as percentages (in order to be size-independent) in an array of javascript objects like so:
var maps = {
alpen : [
{type:'poly',name:'Finsteraarhorn (4274m)',vertx:[56.48,56.08,56.06,56.46], verty:[28.5,28.75,40.25,40.25]},
{type:'rect',name:'Fiescherhörner (4049m)',coords:[58.08,29.5,59.26,43.5]},
{type:'poly',name:'Eiger (3970m)',vertx:[61.95,61.31,61.31,60.5,60.5], verty:[43,35.25,30.25,30.25,45.5]}
]
}; // maps
Since the pnpoly function requires the vertices for x and y separately I provide the coordinates this way already.
The Id of the map is stored in a data attribute in the source link:
<a href="/img/bilder/Alpen.jpg" data-type='image' data-Id='alpen' data-fancybox="img" data-caption="<h5>panorama of the alps from the black forest Belchen at sunset</h5>">
<img src="/_pano/bilder/Alpen.jpg">
</a>
CSS for the tooltip:
.my-tooltip {
color: #ccc;
background: rgba(30,30,30,.6);
position: absolute;
padding: 5px;
text-align: left;
border-radius: 5px;
font-size: 12px;
}
pnpoly and pnrect are provided as simple functions, the handling of that all is done in the afterShow event handler:
// PNPoly algorithm checkes whether point in polygon
function pnpoly(vertx, verty, testx, testy) {
var i, j, c = false;
var nvert = vertx.length;
for(i=0, j=nvert-1; i<nvert; j=i++) {
if (((verty[i] > testy) != (verty[j] > testy)) &&
(testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i])) {
c = !c;
}
}
return c;
}
// checks whether point in rectangle
function pnrect(coords,testx,testy) {
return ((testx >= coords[0]) && (testx <= coords[2]) && (testy >= coords[1]) && (testy <= coords[3]));
}
$("[data-fancybox]").fancybox({
afterShow: function( instance, slide ) {
var map = maps[$(slide.opts.\$orig).data('id')]; // Get map name from source link data-ID
if (map && map.length) { // if map present
$(".fancybox-image")
.after("<span class='my-tooltip' style='display: none'></span>") // append tooltip after image
.mousemove(function(event) { // create mousemove event handler
var offset = $(this).offset(); // get image offset, since mouse coords are global
var perX = ((event.pageX - offset.left)*100)/$(this).width(); // calculate mouse coords in image as percentages
var perY = ((event.pageY - offset.top)*100)/$(this).height();
var found = false;
var i;
for (i = 0; i < map.length; i++) { // loop over map entries
if (found = (map[i].type == 'poly') // depending on area type
?pnpoly(map[i].vertx, map[i].verty, perX, perY) // look whether coords are in polygon
:pnrect(map[i].coords, perX, perY)) // or coords are in rectangle
break; // if found stop looping
} // for (i = 0; i < map.length; i++)
if (found) {
$(".my-tooltip")
.css({bottom: 'calc(15px + '+ (100 - perY) + '%'}) // tooltip 15px above mouse coursor
.css((perX < 50) // depending on which side we are
?{right:'', left: perX + '%'} // tooltip left of mouse cursor
:{right: (100 - perX) + '%', left:''}) // or tooltip right of mouse cursor
.text(map[i].name) // set tooltip text
.show(); // show tooltip
} else {
$(".my-tooltip").hide(); // if nothing found: hide.
}
});
} else { // if (map && map.length) // if no map present
$(".fancybox-image").off('mousemove'); // remove event mousemove handler
$(".my-tooltip").remove(); // remove tooltip
} // else if (map && map.length)
} // function( instance, slide )
});
Things left to do: Find a solution for touch devices, f.e. provide a button to show all tooltips (probably rotated 90°).
As soon as the page is online I'll provide a link here to see it working...

FabricJS resizing groups and objects in them

I need to programmaticaly resize a group and it's contained elements on a fabric canvas.
By default, fabric applies scaling to objects. I can get around this easily enough by using the scalex & scaley to calculate the new height and width then I set them back to 1. This works fine for the group but I cant figure out how to set the new size for the objects contained in this group.
Eg. Before resize:
After resize:
My (typescript) code is like:
redraw() {
this.shapeGroup.set({
scaleX: 1,
scaleY: 1,
left: this.shape.origin.x,
top: this.shape.origin.y,
width: this.shape.extent.width + this.shape.line.lineWidth,
height: this.shape.extent.height + this.shape.line.lineWidth,
dirty: true
});
// const ac = this.shapeGroup.calcCoords();
this.shapeElement.set({
rx: this.shape.cornerRadius,
ry: this.shape.cornerRadius,
width: this.shape.extent.width,
height: this.shape.extent.height,
dirty: true
});
// this.shapeElement.setCoords(ac);
if (this.shape.text) {
this.textElement.set({
width: this.shapeElement.width - (this.cornerRadius * 2),
height: this.shapeElement.height - (this.cornerRadius * 2)
});
}
this.canvas.fabric.renderAll();
}
(this.shape is the underlying model of the object which is represented by 1 or more fabric objects in a group).
Has anyone done anything like this with success?
I used a similar code to resize the group items, so maybe this can lead you to a solution.
canvas.setActiveGroup(this.shapeGroup.setCoords());
var objs = canvas.getActiveGroup().getObjects();
for(i in objs){
objs[i].set({
...
});
}

Phaser 3. Change dimensions of the game during runtime

Hi please help me to find out how trully responsive game can be created with Phaser3.
Respnsiveness is critical because game (representation layer of Blockly workspace) should be able to be exapanded to larger portion on screen and shrinked back many times during the session.
The question is How I can change dimentions of the game in runtime?
--- edited ---
It turns out there is pure css solution, canvas can be ajusted with css zoom property. In browser works well (no noticeable effect on performance), in cordova app (android) works too.
Here is Richard Davey's answer if css zoom can break things:
I've never actually tried it to be honest. Give it a go and see what
happens! It might break input, or it may carry on working. That (and
possibly the Scale Manager) are the only things it would break,
though, nothing else is likely to care much.
// is size of html element size that needed to fit
let props = { w: 1195, h: 612, elementId: 'myGame' };
// need to know game fixed size
let gameW = 1000, gameH = 750;
// detect zoom ratio from width or height
let isScaleWidth = gameW / gameH > props.w / props.h ? true : false;
// find zoom ratio
let zoom = isScaleWidth ? props.w / gameW : props.h / gameH;
// get DOM element, props.elementId is parent prop from Phaser game config
let el = document.getElementById(props.elementId);
// set css zoom of canvas element
el.style.zoom = zoom;
Resize the renderer as you're doing, but you also need to update the world bounds, as well as possibly the camera's bounds.
// the x,y, width and height of the games world
// the true, true, true, true, is setting which side of the world bounding box
// to check for collisions
this.physics.world.setBounds(x, y, width, height, true, true, true, true);
// setting the camera bound will set where the camera can scroll to
// I use the 'main' camera here fro simplicity, use whichever camera you use
this.cameras.main.setBounds(0, 0, width, height);
and that's how you can set the world boundary dynamically.
window.addEventListener('resize', () => {
game.resize(window.innerWidth, window.innerHeight);
});
There is some builtin support for resizing that can be configured in the Game Config. Check out the ScaleManager options. You have a number of options you can specify in the scale property, based on your needs.
I ended up using the following:
var config = {
type: Phaser.AUTO,
parent: 'game',
width: 1280, // initial width that determines the scaled size
height: 690,
scale: {
mode: Phaser.Scale.WIDTH_CONTROLS_HEIGHT ,
autoCenter: Phaser.Scale.CENTER_BOTH
},
physics: {
default: 'arcade',
arcade: {
gravity: {y: 0, x: 0},
debug: true
}
},
scene: {
key: 'main',
preload: preload,
create: this.create,
update: this.update
},
banner: false,
};
Just in case anybody else still has this problem, I found that just resizing the game didn't work for me and physics didn't do anything in my case.
To get it to work I needed to resize the game and also the scene's viewport (you can get the scene via the scenemanager which is a property of the game):
game.resize(width,height)
scene.cameras.main.setViewport(0,0,width,height)
For Phaser 3 the resize of the game now live inside the scale like this:
window.addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight);
});
But if you need the entire game scale up and down only need this config:
const gameConfig: Phaser.Types.Core.GameConfig = {
...
scale: {
mode: Phaser.Scale.WIDTH_CONTROLS_HEIGHT,
},
...
};
export const game = new Phaser.Game(gameConfig);
Take a look at this article.
It explains how to dynamically resize the canvas while maintaining game ratio.
Note: All the code below is from the link above. I did not right any of this, it is sourced from the article linked above, but I am posting it here in case the link breaks in the future.
It uses CSS to center the canvas:
canvas{
display:block;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
It listens to the window 'resize' event and calls a function to resize the game.
Listening to the event:
window.onload = function(){
var gameConfig = {
//config here
};
var game = new Phaser.Game(gameConfig);
resize();
window.addEventListener("resize", resize, false);
}
Resizing the game:
function resize() {
var canvas = document.querySelector("canvas");
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
var windowRatio = windowWidth / windowHeight;
var gameRatio = game.config.width / game.config.height;
if(windowRatio < gameRatio){
canvas.style.width = windowWidth + "px";
canvas.style.height = (windowWidth / gameRatio) + "px";
}
else{
canvas.style.width = (windowHeight * gameRatio) + "px";
canvas.style.height = windowHeight + "px";
}
}
I have used and modified this code in my phaser 3 project and it works great.
If you want to see other ways to resize dynamically check here
Put this in your create function:
const resize = ()=>{
game.scale.resize(window.innerWidth, window.innerHeight)
}
window.addEventListener("resize", resize, false);
Important! Make sure you have phaser 3.16+
Or else this won't work!!!
Use the game.resize function:
// when the page is loaded, create our game instance
window.onload = () => {
var game = new Game(config);
game.resize(400,700);
};
Note this only changes the canvas size, nothing else.

Draw text on canvas using fabric.js

I want to draw text on canvas., how to do it any sample example?
The canvas already contains some shape drawn, i want to show text on the top of that shape on canvas
How can i do it?
Also be aware that you need to actually have loaded a cufon font. There is no default font when using Fabric.js.
<script src="fabric.js"></script>
<script src="cufon.calibri.js"></script>
There are so many fonts available from http://www.cufonfonts.com/
This being the case the author is planning on removing the need for cufon. Discussed here: Fabric.js + Google Fonts
If you're wanting to render a block, then some text inside of that block. I would do something like this.
//Render the block
var block = canvas.add(new fabric.Rect({
left: 100,
top: 100,
fill: 'blue'
}));
//Render the text after the block (so that it is in front of the block)
var text = canvas.add(new fabric.Text('I love fabricjs', {
left: block.left, //Take the block's position
top: block.top,
fill: 'white'
}));
//Render the text and block on the canvas
//This is to get the width and height of the text element
canvas.renderAll();
//Set the block to be the same width and height as the text with a bit of padding
block.set({ width: text.width + 15, height: text.height + 10 });
//Update the canvas to see the text and block positioned together,
//with the text neatly fitting inside the block
canvas.renderAll();
Take a look at How to render text tutorial.
It's as simple as:
canvas.add(new fabric.Text('foo', {
fontFamily: 'Delicious_500',
left: 100,
top: 100
}));
Also worth noting that you might need to adjust Cufon's offsetLeft to help correctly position the text. Something like:
Cufon.fonts[your-fontname-in-lowercase-here].offsetLeft = 120;
The kitchen sink demo of fabric uses this:
http://kangax.github.com/fabric.js/lib/font_definitions.js

Resources