I am using D3 to manipulate an SVG, that contains several "widgets" that have behavior and can be controlled eg via events. For instance I have a spinning fan. It should be possible to turn on and off the fan. I have been able to build that in D3, but not in an elegant way, and with all the code being global. What I want to end up with is something as below:
Creation of "widget", as known from jQuery:
d3.select('svg').append('g').attr('id', 'myFan').fan();
And then I would like to turn on and off with smth like the following, also as known from jQuery:
d3.select('#myFan').fan('start')
d3.select('#myFan').fan('stop')
Is it possible to achieve this in D3, ie to create such a "widget"?
Is there generally a different approach to such a problem, when using D3?
My current unelegant solution with global code:
Robert asked for this. Code at Codepen: link.
Javascript
var fan = d3.select('body')
.append('svg')
.attr('width', 200)
.attr('height', 200)
.append('g')
.attr('class', 'fan')
.attr('id', 'myFan')
.attr('transform', 'translate(75,75)')
// Static, background disc
fan.append('circle')
.attr('cx',0)
.attr('cy',0)
.attr('r',60)
// Dynamic, rotating path
var blade = fan.append('path')
.attr('d', 'M 0 0 L -30 15 L -60 0 Z L 30 -15 L 60 0 Z ')
.attr('transform', 'translate(50,50)')
.attr('transform', 'rotate(90)');
// Static, hub
fan.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', 10)
var rotation = 0; // degrees
var speed = 10; // degree per call
function rotate() {
rotation = (rotation + speed) % 360;
blade.attr('transform', 'rotate('+rotation+')')
}
setInterval(rotate, 100);
function stop() { speed = 0; }
function start() { speed = 10; }
var ctrlPanel = d3.select('body').append('div');
ctrlPanel.append('button').text('Start').on('click', start);
ctrlPanel.append('button').text('Stop').on('click', stop);
CSS
path {
fill: red;
strok: blue;
stroke-width: 3px;
}
.fan circle {
fill: lightgray;
stroke: black;
stroke-width: 3px;
}
.fan path {
fill: darkgray;
stroke: black;
stroke-width: 3px;
}
Related
How do I make an inline background image show up for a d3 svg rect?
svg.selectAll('rect')
.data([1])
.enter().append('rect')
.attr('x', function() { return 0; })
.attr('y', function() { return 0; })
.attr('stroke', "#000000")
.attr('stroke-width', (d) => 5)
.attr('width', function() { return 200; })
.attr('height', function() { return 200; })
.attr('background-image', function() {
return `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'><path class='scatter' d='M0 -2 C7 -7 7 2 0 4.5 -7 2 -7 -7 0 -2Z' transform='translate(10,10)' style='stroke: rgb(0, 198, 228); fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 2px; stroke-opacity: 1;'></path> </svg>")`;
});
It's just black.
Here's a jsfiddle that demonstrates. Once with normal css, once with d3.js. It's the same data string; the blue heart rectangle is supposed to show up twice. I want to know how to make it show up the second time, using d3.
The context is that I will have many of these rects, and each one will have a different design, so I need to call the function to get the data for each rect. So I want to call that function for each rect that is added in the join.
Referencing https://jqueryui.com/draggable/ i am able to implement a drag drop feature within a parent element (e.g. div). However my need is to have this draggable feature to work within a polygonal element (Like a SVG polygon).
I have been searching the net, however there are examples of how to make a svg polygon draggable but not 'how to contain drag drop feature within a polygonal parent (div).
Any ideas / pointers will be helpful.
Thanks.
The short story is you need a function to check if a point is within a polygon, and then check if the four corners of your draggable object are within that shape.
Here's a rough example of doing that, using the draggable sample from jQuery, along with a point in polygon function from this answer. This example is far from perfect, but I hope it points you in the right direction.
// These are the points from the polygon
var polyPoints = [
[200, 27],
[364, 146],
[301, 339],
[98, 339],
[35, 146]
];
$("#draggable").draggable({
drag: function(e, ui) {
var element = $("#draggable")[0];
var rect = element.getBoundingClientRect();
var rectPoints = rect2points(rect);
let inside = true;
rectPoints.forEach(p => {
if(!pointInside(p, polyPoints)){
inside = false;
}
});
$("#draggable")[inside ? 'addClass' : 'removeClass']('inside').text(inside ? 'Yay!' : 'Boo!');
}
});
function rect2points(rect) {
return ([
[rect.left, rect.top],
[rect.right, rect.top],
[rect.right, rect.bottom],
[rect.left, rect.bottom]
]);
};
function pointInside(point, vs) {
var x = point[0],
y = point[1];
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i][0],
yi = vs[i][1];
var xj = vs[j][0],
yj = vs[j][1];
var intersect = ((yi > y) != (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
};
#draggable {
width: 100px;
height: 80px;
background: red;
position: absolute;
text-align: center;
padding-top: 20px;
color:#fff;
}
#draggable.inside{
background: green;
}
html, body{
margin: 0;
}
<script src="//code.jquery.com/jquery-1.12.4.js"></script>
<script src="//code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<div id="draggable">Drag me</div>
<svg width="400px" height="400px" viewBox="0 0 400 400">
<rect width="600" height="600" fill="#efefef"></rect>
<polygon points="200,27 364,146 301,339 98,339 35,146" fill="rgba(255,200,0, 1)" stroke="rgba(255,0,0,0.2" stroke-width="2"></polygon>
</svg>
Previously I was working on Phaser 2 but now I need to switch to Phaser 3.
I tried to make the canvas responsive with ScaleManager but it is not working.
I think some of the methods changed but I didn't find any help to rescale the stage full screen.
var bSize = {
bWidth: window.innerWidth ||
root.clientWidth ||
body.clientWidth,
bHeight: window.innerHeight ||
root.clientHeight ||
body.clientHeight,
};
var game;
var canvas = document.getElementById("canvas");
function create() {
// Scaling options
game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
// Have the game centered horizontally
game.scale.pageAlignHorizontally = true;
// And vertically
game.scale.pageAlignVertically = true;
// Screen size will be set automatically
game.scale.setScreenSize(true);
}
window.onload = function() {
// Create game canvas and run some blocks
game = new Phaser.Game(
bSize.bWidth, //is the width of your canvas i guess
bSize.bHeight, //is the height of your canvas i guess
Phaser.AUTO,
'frame', { create: create });
canvas.style.position = "fixed";
canvas.style.left = 0;
canvas.style.top = 0;
}
Since v3.16.0, use the Scale Manager. For short:
var config = {
type: Phaser.AUTO,
scale: {
mode: Phaser.Scale.FIT,
parent: 'phaser-example',
autoCenter: Phaser.Scale.CENTER_BOTH,
width: 800,
height: 600
},
//... other settings
scene: GameScene
};
var game = new Phaser.Game(config);
Here is the full code and here are some useful examples using the Scale Manager.
There isn't a scale manager for Phaser 3 yet but it's in development. For now I suggest following this tutorial. It basically centres the canvas with some CSS, then calls a resize function that handles maintaining the game ratio when the resize event is emitted by the window.
Here is the code used in the tutorial linked above:
The css:
canvas {
display: block;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
The resize function:
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";
}
}
Then:
window.onload = function() {
//Game config here
var config = {...};
var game = new Phaser.Game(config);
resize();
window.addEventListener("resize", resize, false);
}
Try adding max-width to canvas css and then max-height based on aspect ratio. E.g:
canvas {
display: block;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 100%;
max-height: 50vw;
}
Scale manager will handle assets(Images,Sprites,..,...) positions and size sometime?
If I want to add an extra line to an existing polyline, should I remove this existing polyline from the canvas first, modify the points matrix, and add the new polyline? Or is it possible to change the existing polyline, like changing the text of a text object?
You may remove whole polyline and add a new one or else you need to calculate the dimensions(left,top and pathoffset) and set it to polyline.
DEMO
var canvas = new fabric.Canvas('c');
var points = [];
var random = fabric.util.getRandomInt;
points.push(new fabric.Point(random(100,200),random(200,300)));
points.push(new fabric.Point(random(200,300),random(100,200)));
points.push(new fabric.Point(random(200,250),random(150,200)));
var polyLine = new fabric.Polyline(points, {
stroke: 'black',
fill: ''
});
canvas.add(polyLine);
setPolyCoords();
function addPoint(){
polyLine.points.push(new fabric.Point(random(100,400),random(100,400)));
setPolyCoords();
}
function setPolyCoords(){
polyLine._calcDimensions();
polyLine.set({
top : polyLine.minY,
left : polyLine.minX,
pathOffset : {
x: polyLine.minX + polyLine.width / 2,
y: polyLine.minY + polyLine.height / 2
}
});
polyLine.dirty = true;
polyLine.setCoords();
canvas.renderAll();
}
canvas {
border: 1px solid #f00;
margin: 0px;
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.22/fabric.js"></script>
<button onclick='addPoint()'>Add Point</button>
<canvas id="c" width="400" height="400"></canvas>
With Fabric version 2.7.0 this is become easier then in #Durga his answer.
See the new code in the demo below.
You can also skip setting the dirty flag manually by passing objectCaching: false to your polyline during construction:
var polyLine = new fabric.Polyline(points, {
stroke: 'black',
fill: '',
objectCaching: false
});
DEMO
var canvas = new fabric.Canvas('c');
var points = [];
var random = fabric.util.getRandomInt;
points.push(new fabric.Point(random(100,200),random(200,300)));
points.push(new fabric.Point(random(200,300),random(100,200)));
points.push(new fabric.Point(random(200,250),random(150,200)));
var polyLine = new fabric.Polyline(points, {
stroke: 'black',
fill: ''
});
canvas.add(polyLine);
function addPoint(){
polyLine.points.push(new fabric.Point(random(100,400),random(100,400)));
polyLine.dirty = true;
canvas.renderAll();
}
canvas {
border: 1px solid #f00;
margin: 0px;
display: block;
}
<script src="https://rawgit.com/fabricjs/fabric.js/master/dist/fabric.min.js"></script>
<button onclick='addPoint()'>Add Point</button>
<canvas id="c" width="400" height="400"></canvas>
I'm building a svg map of germany based on data from Natural Earth. Most thing work fine, including provinces, places names and lakes. However, when using rivers on a low scaled map, only parts of the river get displayed. When zooming in, the whole river is visible.
Relevant portions of the code:
<style type="text/css">
.river { stroke: cadetblue;
fill: none;
stroke-linejoin: round;
stroke-linecap: round;
}
</style>
var projection = d3.geo.mollweide()
.center([10.4, 51.1])
.scale(6300)
.translate([width/2, height/2]);
var path = d3.geo.path().projection(projection);
// [...]
var svg = d3.select("#wrapper").append("svg")
.attr("width", width)
.attr("height", height)
var map = svg.append("g")
.attr("id", "map");
// [...]
d3.json("data/final.json", function(error, deu) {
// [...]
var rivers = topojson.feature(deu, deu.objects.rivers);
// [...]
map.append("path")
.datum(rivers)
.attr("d", path)
.attr("stroke-width", "2")
.attr("class" , "river");
// [...]
}