I'm tinkering with a slowly-rotating geodesic sphere, and I was hoping you guys could help me make some tweaks:
https://codepen.io/anon/pen/QaYQBd
(The code is based on this: https://bl.ocks.org/mbostock/3057239)
var width = 1000,
height = 500;
var velocity = [.002, .002],
t0 = Date.now();
var projection = d3.geo.orthographic()
.scale(height / 2 - 10);
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
context.strokeStyle = "#fa0";
context.lineWidth = 0.5;
geodesic(3);
d3.timer(function() {
var time = Date.now() - t0;
projection.rotate([time * velocity[0], time * velocity[1]]);
redraw();
});
function redraw() {
context.clearRect(0, 0, width, height);
faces.forEach(function(d) {
d.polygon[0] = projection(d[0]);
d.polygon[1] = projection(d[1]);
d.polygon[2] = projection(d[2]);
});
context.beginPath();
faces.forEach(function(d) {
drawTriangle(d.polygon);
});
context.stroke();
}
function drawTriangle(triangle) {
context.moveTo(triangle[0][0], triangle[0][1]);
context.lineTo(triangle[1][0], triangle[1][1]);
context.lineTo(triangle[2][0], triangle[2][1]);
context.closePath();
}
function geodesic(n) {
faces = d3.geodesic.polygons(n).map(function(d) {
d = d.coordinates[0];
d.polygon = d3.geom.polygon(d.map(projection));
return d;
});
redraw();
}
I've been able to modify the code to get the design of the sphere closer to what I want. I'm not sure how to progress any further.
My goals:
Add dots to each vertex like this: https://bl.ocks.org/mbostock/3055104
Give the sphere a 3d perspective instead of being flat (Check out this design, which has a slider that lets you adjust the perspective: http://dmccooey.com/polyhedra/GeodesicIcosahedron3.html)
Have the stroke widths and dot sizes change relative to the perspective (this will make the whole thing have a more 3d solid look to it, instead of having the same stroke widths for every line segment)
Move the sphere to an arbitrary position within the canvas and allow the canvas size to change depending on the width of the browser window
Randomize the rotation and starting position of the sphere when the page reloads
If anyone can help me achieve any of these goals, I'd be ecstatic!
Related
I´ve been trying to make a desktop app (javascript, canvas) and draw 413.280 clickable circles in a certain pattern, but I can´t really figure out how to do it. I´m not convinced canvas is the best solution but I dont know how to solve this and get an app with a reasonable performance.
Here´s the layout I´m trying to get:
circle layout
I want 2 rows of circles within each line. the division in the middle is to be left empty.
Every left row has to be 588 circles.
Every right row has to be 560 circles
There are 180 lines on each side which means there's (588*2*180)= 211680 circles on the left side.
There's (560*2*180)=201600 circles on the right side.
can anyone point me in the right direction, maybe have a clue how I can solve this in the most efficient way possible? Thanks in advance.
EDIT: here's the JSFiddle I've got so far jsfiddle.net/cmxLoqej/2/
JavaScript
window.onload = draw;
function draw() {
var canvas = document.getElementById('canvas');
var c = canvas.getContext('2d');
var ycoordinate = 20;
//draw the line 180 times
for (var x = 1; x <= 180; x++) {
// draw the left side
for (var i = 1; i <= 1; i++){
c.strokeStyle = 'black';
c.moveTo(0,ycoordinate);
c.lineTo(6468,ycoordinate);
c.stroke();
ycoordinate = ycoordinate + 40;
}
}
var ycoordinate = 20;
//draw right side
for (var x = 1; x <= 180; x++) {
for (var j = 1; j <= 1; j++){
c.strokeStyle = 'black';
c.moveTo(6776,ycoordinate);
c.lineTo(canvas.width,ycoordinate);
c.stroke();
ycoordinate = ycoordinate + 40;
}
}
}
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var canvasPattern = document.createElement("canvas");
canvasPattern.width=11;
canvasPattern.height=20;
var contextPattern = canvasPattern.getContext("2d");
contextPattern.beginPath();
contextPattern.arc(5, 10, 5, 0, 2 * Math.PI, false);
contextPattern.strokeStyle = '#003300';
contextPattern.stroke();
var pattern = context.createPattern(canvasPattern,"repeat");
context.fillStyle = pattern;
context.fillRect(0, 20, 6468, 7160);
context.fill();
var canvas2 = document.getElementById('canvas');
var context2 = canvas.getContext('2d');
var canvasPattern2 = document.createElement("canvas");
canvasPattern2.width=11;
canvasPattern2.height=20;
var contextPattern2 = canvasPattern.getContext("2d");
contextPattern2.beginPath();
contextPattern2.arc(5, 10, 5, 0, 2 * Math.PI, false);
contextPattern2.strokeStyle = '#003300';
contextPattern2.stroke();
var pattern2 = context2.createPattern(canvasPattern2,"repeat");
context2.fillStyle = pattern;
context2.fillRect(6776, 20, 6160, 7160);
context2.fill();
HTML
<!DOCTYPE html>
<html>
<body>
<canvas {
id="canvas";
width= "12936" ;
height ="7400";
style= "border: 1px solid black;";
padding: 0;
margin: auto;
display: block;
}>
</canvas>
</body>
</html>
Use fill patterns of circles to create rectangular canvas images of
a single row of the left hand side
a single row of the right hand side
a combined row of each side
a single canvas of 180 rows
Use temporary CANVAS objects along the way as necessary to use the context2D.createPattern method. You should not need to add them to the DOM just to manipulate pixels.
Modify the algorithm if needed as you learn. Happy coding!
Update (edit)
Running the code added to the question shows all circles being evenly spaced horizontally and vertically.
A simpler way of drawing the canvas may be to fill two rectangles that exactly cover the left and right areas of the canvas with the circle pattern, and draw the grid lines on the canvas afterwards instead of before.
Finding the circle clicked
A click event listener on the canvas is passed a mouse event object.
The classical way to determine which circle was clicked was to first perform arithmetic on the screenX and screenY event properties for screen position, window.scrollX and window.scrollY for document scroll amounts, and the position of the canvas within the document, to find where the click occured in the canvas.
Although not yet fully standardized, offsetX and offsetY properties of the mouse event object provide the result directly. The MDN reference shows fairly good cross browser support.
Then knowledge of canvas layout can be used to determine which rectangular circle pattern was clicked, and with a bit of algebra if the click is inside the circle.
I'm trying to build an app where user can add various objects (rectangles, circles) and he can use mouse wheel to zoom-in and zoom-out.
For this zooming I set up event handler like this:
TheCanvas.on('mouse:wheel', function(options){
var p = new fabric.Point(
options.e.clientX,
options.e.clientY
);
var direction = (options.e.deltaY > 0) ? 0.9 : 1.1;
var newZoom = TheCanvas.getZoom() * direction;
// restrict too big/small zoom here:
if ((newZoom > 50) || (newZoom < 0.7)) return false;
TheCanvas.zoomToPoint( p, newZoom );
}
Everything worked fine until now. Now I want to draw a crosshair over all objects on the canvas. Something like this:
So I made my own custom object like:
CrossHairClass = fabric.util.createClass(fabric.Object, {
strokeDashArray: [1,2], // I want lines to be dashed
........
My problem is:
When user zooms with the mouse wheel, my cross-hair lines zoom their thickness too and also small dashes get bigger. But I don't want that. I want my cross-hair lines be a "hair" lines = ideally 1 pixel thick all the time regardless zoom factor of the canvas. And fine dashed line too.
Render function of my Class:
_render: function (ctx) {
// I tried it like this
var zoom = TheCanvas.getZoom();
var scale = (1/zoom) * 3.333; // with this scale it visually looked the best
// I have to scale it in X and Y while I want small dashes to stay small and also thickness of the line to stay "hair-line"
this.scaleX = this.scaleY = scale;
this.width = CROSSHAIR_SIZE / scale; // my constant from elsewhere
ctx.lineWidth = 1;
ctx.beginPath();
// this example is for horizontal line only
ctx.moveTo(-this.width / 2, 0);
ctx.lineTo(this.width / 2, 0);
this._renderStroke(ctx);
}
I tried various combinations of multiplying or dividing by scale factor or zoom factor but if I finally had lines thin, I couldn't keep their size, which must be constant (in pixels) regardless of canvas zoom. Please help.
P.S.: now I got an idea. Maybe I should create another canvas, over my current canvas and draw this crosshair on the upper canvas, which will not zoom?
EDIT 1
Based on the answer from #andreabogazzi I tried various approaches, but this finally worked out! Thanks! :)
_render: function (ctx) {
var zoom = TheCanvas.getZoom();
// ctx.save(); // this made no difference
// ctx.setTransform(1/zoom, 0, 0, 1/zoom, 0, 0); // this didn't work
this.setTransformMatrix([1/zoom, 0, 0, 1/zoom, 0, 0]);
ctx.strokStyle = 'red';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-this.widthHalf, 0); // widthHalf computed elsewhere
ctx.lineTo(this.widthHalf, 0);
this._renderStroke(ctx); // I use this instead of ctx.stroke() while this ensures my line is still nicely dashed
// ctx.restore(); // this made no difference
}
Since you created a custom class, you have to invert the zoom of your canvas before drawing.
On the _render function of your subclass, since you should be positioned in the center of your crosshair, apply a transform matrix of scale type, with scale factor of 1/zoomLevel and everything should work.
I would say the correct way is:
_render: function (ctx) {
var zoom = TheCanvas.getZoom();
ctx.save(); // this is done anyway but if you add custom ctx transform is good practice to wrap it in a save/restore couple
ctx.transform(1/zoom, 0, 0, 1/zoom, 0, 0);
ctx.strokStyle = 'red';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-this.widthHalf, 0); // widthHalf computed elsewhere
ctx.lineTo(this.widthHalf, 0);
this._renderStroke(ctx); // I use this instead of ctx.stroke() while this ensures my line is still nicely dashed
ctx.restore(); // this is done anyway but if you add custom ctx transform is good practice to wrap it in a save/restore couple
}
Now it happens that this object get cached from the fabricJS cache system that will probably create the cache depending on the canvas zoom too.
I have no understanding of the final use of this object, but you should include this calculation also in the cache canvas size calculation.
I'm trying to make a chorolpleth with a rotating globe in d3. I can get the globe to render out fine but I can't get the countries to fill with the proper color scale.
Longer explanation. I basically started with Mike Bostock's code for the spinning globe found here:
http://bl.ocks.org/mbostock/6747043
I've got some economic data for about 85 countries that I'm reading in from an external csv. And I'm trying to get the colors to map to the countries per the values in the csv. There's another Bostock example of a choropleth here (static and just the US and referenced frequently in SO d3 questions):
http://bl.ocks.org/mbostock/4060606
What I end up with are solid white (#fff) countries on the face of the globe. Which is not what I'm trying to get.
I added the ISO 3166-1 numeric codes to my csv so that I could match them to the same ids inside the topojson data. So my csv looks like:
country id curracct
Germany 276 260.9
Sweden 752 7.24
Etc.
My first thought was just to create a variable that was a function, which went through the length of the 'countries' from the topojson data and found the countries where the id equaled the id from the csv countries, then assigned the scaled color to them. Then I set 'context.fillStyle' equal to that variable/function. That didn't work.
Then I just put 'context.fillStyle' directly inside of a function (which is the code as it's currently written below). That didn't work either.
Again, I'm trying to get the 85 or so countries with data in the csv to appear color-coded on the front side spinning globe according to the scale I've set up.
My guess is that there is something I don't understand about the variable 'context' and what it's handling. If this were .style("fill", [put my function here to map the colors]) syntax I would be okay. So, anyone got any thoughts?
I'm not a coder. Actually I guess I am as I am trying to write some code. Maybe I should just say I'm a self-taught and mostly terrible coder. Although through examples, the JS console, and other questions on SO, I can usually work out where the errors are. This time I've reached a wall. Any help is appreciated. Thanks.
var width = 560,
height = 560,
speed = -1e-2,
start = Date.now();
var sphere = {type: "Sphere"};
var color = d3.scale.quantize()
.range(["#ffffd9", "#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"]);
var projection = d3.geo.orthographic()
.scale(width / 2.1)
.clipAngle(90)
.translate([width / 2, height / 2]);
var graticule = d3.geo.graticule();
var canvas = d3.select("body")
.append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
var path = d3.geo.path()
.projection(projection)
.context(context);
queue()
.defer(d3.json, "/d3/world-110m.json")
.defer(d3.csv, "trade.csv")
.await(globeTrade);
function globeTrade(error, topo, data) {
var land = topojson.feature(topo, topo.objects.land),
countries = topojson.feature(topo, topo.objects.countries),
borders = topojson.mesh(topo, topo.objects.countries, function(a, b) { return a !== b; }),
grid = graticule();
color.domain([0, d3.max(data, function(d){return d.curracct})]);
d3.timer(function() {
var λ = speed * (Date.now() - start),
φ = -15;
context.clearRect(0, 10, width, height);
context.beginPath();
path(sphere);
context.lineWidth = 2.5;
context.strokeStyle = "#000";
context.stroke();
context.fillStyle = "#fff";
context.fill();
context.save();
context.translate(width / 2, 0);
context.scale(-1, 1);
context.translate(-width / 2, 0);
projection.rotate([λ + 180, -φ]);
context.beginPath();
path(land);
context.fillStyle = "#ddd" //changed to a nuetral gray
context.fill();
context.beginPath();
path(grid);
context.lineWidth = .5;
context.strokeStyle = "rgba(119,119,119,.5)";
context.stroke();
context.beginPath();
path(borders);
context.lineWidth = .25;
context.strokeStyle="#fff";
context.stroke();
context.restore();
projection.rotate([λ, φ]);
context.beginPath();
path(grid);
context.lineWidth = .5;
context.strokeStyle = "rgba(119,119,119,.5)";
context.stroke();
// This is where I am failing
context.beginPath();
path(countries);
function render (d){
for (var j = 0; j < countries.features.length; j++) {
if (d.id == countries.features[j].id) {
context.fillStyle = color(d.curracct)
}
else {
context.fillStyle = "#737368"; //left Bostock's color for now
}
}
}
context.fill();
context.lineWidth = .1;
context.strokeStyle = "#000";
context.stroke();
});
data.forEach(function(d, i) {
d.curracct = +d.curracct;
d.id = +d.id;
});
d3.select(self.frameElement).style("height", height + "px");
</script>
</body>
I am drawing onto an HTML5 canvas with stroke() and regardless of how or when I set globalAlpha, the stroke is being drawn with some measure of transparency. I'd like for the stroke to be completely opaque (globalAlpha=1). Is there somewhere else where the alpha is being set?
In this jsfiddle, I am drawing a grid of solid black lines onto a canvas. For me, the result shows dots at the intersections, confirming that the lines are partially transparent. Here's the gist of it:
context.globalAlpha=1;
context.strokeStyle="#000";
context.beginPath();
/* draw the grid */
context.stroke();
context.closePath;
The especially weird thing (to me) is that this problem was not occurring in my code before my last computer restart, so I'm guessing there was something hanging around in the cache that was keeping the alpha at my desired level.
I'm obviously missing something here... thanks for any help you can provide.
Real answer :
Each point in a canvas has its center in its (+0.5, +0.5) coordinate.
So to avoid artifacts, start by translating the context by (0.5, 0.5) ,
then round the coordinates.
css scaling creates artifact, deal only with canvas width and height, unless
you want to deal with hidpi devices with webGL, or render at a lower resolution
with both webGL and context2D.
-> in your case, your setup code would be (with NO css width/height set ) :
( http://jsfiddle.net/gamealchemist/x9bTX/8/ )
// parameters
var canvasHorizontalRatio = 0.9;
var canvasHeight = 300;
var hCenterCanvas = true;
// setup
var canvasWidth = Math.floor(window.innerWidth * canvasHorizontalRatio);
var cnv = document.getElementById("myCanvas");
cnv.width = canvasWidth;
cnv.height = canvasHeight;
if (hCenterCanvas)
cnv.style['margin-left'] = Math.floor((window.innerWidth - canvasWidth) * 0.5) + 'px';
var ctx = cnv.getContext("2d");
ctx.translate(0.5, 0.5);
gridContext();
The rest of the code is the same as your original code, i just changed the size of you squares to get quite the same visual aspect.
ctx.beginPath();
for (var i=60; i<canvasHeight; i+=60) {
ctx.moveTo(0,i);
ctx.lineTo(canvasWidth,i);
}
for (i=60; i<canvasWidth; i+=60) {
ctx.moveTo(i,0);
ctx.lineTo(i,canvasHeight);
}
ctx.strokeStyle="#000";
ctx.stroke();
ctx.closePath();
With those changes we go from :
to :
Edit : to ensure rounding, in fact i think most convenient is to inject the context and change moveTo, lineTo :
function gridContext() {
var oldMoveTo = CanvasRenderingContext2D.prototype.moveTo;
CanvasRenderingContext2D.prototype.moveTo = function (x,y) {
x |= 0; y |= 0;
oldMoveTo.call(this, x, y);
}
var oldLineTo = CanvasRenderingContext2D.prototype.lineTo;
CanvasRenderingContext2D.prototype.lineTo = function (x,y) {
x |= 0; y |= 0;
oldLineTo.call(this, x, y);
}
}
Obviously, you must do this for all drawing functions you need.
When drawing lines on a canvas, the line itself is exactly on the pixel grid. But because the line is one pixel wide, half of it appears in each of the pixels to either side of the grid, resulting in antialising and a line that is basically 50% transparent over two pixels.
Instead, offset your line by 0.5 pixels. This will cause it to appear exactly within the pixel.
Demo
Im drawing lines from the center of a shape on mouse clicks. This works fine, until I perform a rotation on the div element holding the canvas element.
Below is the basic Javascript. rotateWrapper gets called by a button elsewhere on the page
var p;
var rot = 0;
var canvas;
var ctx;
function rotateWrapper() {
if (rot == 0) rot = 180;
else rot = 0;
$("#wCanvas").rotate({ animateTo:rot,duration:2500});
}
function draw() {
ctx.save();
ctx.moveTo(p[0], p[1]);
ctx.lineTo(p[2], p[3]);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
$(document).ready(function () {
canvas = $("#imgCanvas").get(0);
ctx = canvas.getContext("2d");
$("#imgCanvas").bind({
mouseup: function(ev) {
p[2] = ev.pageX;
p[3] = ev.pageY;
},
mousedown: function(ev) {
p = new Array(4);
p[0] = $("#wCanvas").width() / 2;
p[1] = $("#wCanvas").height() / 2;
}
});
}
Im sure Im missing something basic but this is driving me up the wall. Ive tried rotating the context in the draw method, both the amount of rot, as well as the inverse of it. Because Im rotating the container element Im thinking this has something to do with the CSS change interfering with things but not certain on that.
Any insights would be highly appreciated
"rotate" will rotate the canvas for elements drawn after the rotation. Without seeing how rotateWrapper and draw are called, it's difficult to tell how you should structure the code. But essentially, you should redraw after you want to apply the rotation. You probably want to store the rotation value, and call a central redraw routine that will apply the rotation first, then redraw.