d3.js geo projection renders a black box - svg

I am trying to make a US map using d3.js.
I use the following code to draw my map:
const EDUCATION =
'https://raw.githubusercontent.com/no-stack-dub-sack/testable-projects-fcc/master/src/data/choropleth_map/for_user_education.json';
const COUNTY =
'https://raw.githubusercontent.com/no-stack-dub-sack/testable-projects-fcc/master/src/data/choropleth_map/counties.json';
const W = 1000
const H = 500
const M = { top: 30, right: 30, bottom: 30, left: 30 }
const P = { top: 30, right: 30, bottom: 30, left: 30 }
function draw(W, H, M, P, COUNTY, EDUCATION) {
const geoJson = topojson.feature(COUNTY, COUNTY.objects.counties)
const projection = d3.geoMercator().fitSize([W, H], geoJson)
const path = d3.geoPath(projection)
const svg = d3
.select("#container")
.append("svg")
.attr("class", "svg-area")
.attr("width", W)
.attr("height", H)
const map = svg
.append('g')
.selectAll("path")
.data(geoJson.features)
.enter()
.append("path")
.attr('class', 'counties')
.attr('d', path)
}
Promise
.all([d3.json(COUNTY), d3.json(EDUCATION)])
.then(data => draw(W, H, M, P, data[0], data[1]))
.catch(err => console.log(err));
The map is too big and therefore, I decided to scale it down using the projection.fitSize() method. I followed this article to learn about it.
Without the const projection = d3.geoMercator().fitSize([W, H], geoJson) projection the map is successfully drawn. But with it, something weird happens: I see a black box and nothing else, the entire shape of the map just becomes a black box.
You can see the live example here:
https://codepen.io/mush-a/pen/RwQaYJx?editors=0010
The topojson data is given here

Related

Fixed SVG Layer Position on Open Layers Map

I have a map with multiple Layers. One of them shall project an SVG on the map, indicating certain regions (if not obvious, it's the red area in the images). I got this working on the default zoom level.
But as soon as I change the zoom in any direction, the SVG layer just moves too much in a direction depending on where I initiate the zoom change.
This is the relevant code I have:
//Map init
var mapcenter = ol.proj.fromLonLat([10.615219, 51.799502]);
var layers = [];
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: mapcenter,
zoom: 9.5
})
});
//SVG init
const svgContainer = document.createElement('div');
const xhr = new XMLHttpRequest();
xhr.open('GET', '/shared/harzregionen.svg');
xhr.addEventListener('load', function () {
const svg = xhr.responseXML.documentElement;
svgContainer.ownerDocument.importNode(svg);
svgContainer.appendChild(svg);
});
xhr.send();
const width = 350;
const height = 0;
const svgResolution = 360 / width;
svgContainer.style.width = width + 'px';
svgContainer.style.height = height + 'px';
svgContainer.className = 'svg-layer';
//SVG Layer
map.addLayer(
new ol.layer.Vector({
render: function (frameState) {
const scale = svgResolution / frameState.viewState.resolution;
const center = frameState.viewState.center;
const size = frameState.size;
const cssTransform = ol.transform.composeCssTransform(
300,
120,
480 / frameState.viewState.resolution,
480 / frameState.viewState.resolution,
frameState.viewState.rotation,
-(center[0] - mapcenter[0]) / 420, //These values were kinda
(center[1] - mapcenter[1]) / 250 //trial and error for default zoom
);
svgContainer.style.transform = cssTransform;
svgContainer.style.opacity = this.getOpacity();
return svgContainer;
}
})
);
I'm pretty sure I have to use a certain formula to calculate the values in composeCssTransform but it feels like there should be an easy way to do it instead of trial and error until it somewhat works.
So, my question is: Is there an easy way to lock an SVG layer to a set position on the world map like the layers with markers?

Text in canvas being wiped out with a video canvas underneath

Note: Minimal working example here. Click on change text button, and you can see the text will appear for one frame and disappear.
I wanted to add an overlay to my video, so I stacked two canvases and filled text to the transparent top canvas.
However, the text does not stick. It disappears.
To test if I was filling the text correctly, I tried using a black canvas (no video) underneath.
I just needed to fillText once and the text stayed.
However, with the video canvas underneath, the text won't stick unless I draw text in requestAnimationFrame, which I believe keeps drawing the same text in every frame, which is unnecessary.
The text canvas is supposed to be separate from the video canvas. Why is this getting wiped out without requestAnimationFrame?
How can I fix it?
Minimal working example here, but the code is also shown below.
import "./styles.css";
import { useEffect, useRef, useState } from "react";
export default function App() {
const inputStreamRef = useRef();
const videoRef = useRef();
const canvasRef = useRef();
const overlayCanvasRef = useRef();
const [text, setText] = useState("Hello");
function drawTextWithBackground() {
const ctx = overlayCanvasRef.current.getContext("2d");
ctx.clearRect(
0,
0,
overlayCanvasRef.current.width,
overlayCanvasRef.current.height
);
/// lets save current state as we make a lot of changes
ctx.save();
/// set font
const font = "50px monospace";
ctx.font = font;
/// draw text from top - makes life easier at the moment
ctx.textBaseline = "top";
/// color for background
ctx.fillStyle = "#FFFFFF";
/// get width of text
const textWidth = ctx.measureText(text).width;
// Just note that this way of "measuring" height is not accurate.
// You can measure height of a font by using a temporary div / span element and
// get the calculated style from that when font and text is set for it.
const textHeight = parseInt(font, 10);
const textXPosition = canvasRef.current.width / 2 - textWidth / 2;
const textYPosition = canvasRef.current.height - textHeight * 3;
ctx.save();
ctx.globalAlpha = 0.4;
/// draw background rect assuming height of font
ctx.fillRect(textXPosition, textYPosition, textWidth, textHeight);
ctx.restore(); // this applies globalAlpha to just this?
/// text color
ctx.fillStyle = "#000";
ctx.fillText(text, textXPosition, textYPosition);
/// restore original state
ctx.restore();
}
function updateCanvas() {
const ctx = canvasRef.current.getContext("2d");
ctx.drawImage(
videoRef.current,
0,
0,
videoRef.current.videoWidth,
videoRef.current.videoHeight
);
// QUESTION: Now the text won't stick unless I uncomment this below,
// which runs drawTextWithBackground in requestAnimationFrame.
// Previosuly, with just a black canvas underneath, I just needed to fillText once
// and the text stayed.
// The text canvas is supposed to be separate. Why is this getting wiped out without requestAnimationFrame?
// drawTextWithBackground();
requestAnimationFrame(updateCanvas);
}
const CAMERA_CONSTRAINTS = {
audio: true,
video: {
// the best (4k) resolution from camera
width: 4096,
height: 2160
}
};
const enableCamera = async () => {
inputStreamRef.current = await navigator.mediaDevices.getUserMedia(
CAMERA_CONSTRAINTS
);
videoRef.current.srcObject = inputStreamRef.current;
await videoRef.current.play();
// We need to set the canvas height/width to match the video element.
canvasRef.current.height = videoRef.current.videoHeight;
canvasRef.current.width = videoRef.current.videoWidth;
overlayCanvasRef.current.height = videoRef.current.videoHeight;
overlayCanvasRef.current.width = videoRef.current.videoWidth;
requestAnimationFrame(updateCanvas);
};
useEffect(() => {
enableCamera();
});
return (
<div className="App">
<video
ref={videoRef}
style={{ visibility: "hidden", position: "absolute" }}
/>
<div style={{ position: "relative", width: "90vw" }}>
<canvas
style={{ width: "100%", top: 0, left: 0, position: "static" }}
ref={canvasRef}
width="500"
height="400"
/>
<canvas
style={{
width: "100%",
top: 0,
left: 0,
position: "absolute"
}}
ref={overlayCanvasRef}
width="500"
height="400"
/>
</div>
<button
onClick={() => {
setText(text + "c");
drawTextWithBackground();
}}
>
change text
</button>
</div>
);
}
Very simple fix
The canvas is being cleared due to how React updates state.
The fix
The simplest fix is to use just the one canvas and draw the text onto the canvas used to display the video.
From the working example change the two functions updateCanvas and drawTextWithBackground as follows.
Note I have removed all the state saving in drawTextWithBackground as the canvas state is lost due to react anyway so why use the very expensive state stack functions.
function drawTextWithBackground(ctx) {
const can = ctx.canvas;
const textH = 50;
ctx.font = textH + "px monospace";
ctx.textBaseline = "top";
ctx.textAlign = "center";
ctx.fillStyle = "#FFFFFF66"; // alpha 0.4 as part of fillstyle
ctx.fillStyle = "#000";
const [W, X, Y] = [ctx.measureText(text).width, can.width, can.height - textH * 3];
ctx.fillRect(X - W / 2, Y, W, textH);
ctx.fillText(text, X, Y);
}
function updateCanvas() {
const vid = videoRef.current;
const ctx = canvasRef.current.getContext("2d");
ctx.drawImage(vid, 0, 0, vid.videoWidth, vid.videoHeight);
drawTextWithBackground(ctx);
requestAnimationFrame(updateCanvas);
}
when you update the canvas from the video, all of the pixels in the canvas are updated with pixels from the video. Think of it like a wall:
you painted "here's my text" on the wall.
Then you painted the whole wall blue. (your text is gone, and the wall is just blue).
If you paint the whole wall blue, and then repaint "here's my text", you'd still see your text on the blue wall.
When you redraw the canvas, you have to re-draw everything you want to appear on the canvas. If the video "covers" your text, you've got to redraw the text.
working example at https://github.com/dougsillars/chyronavideo which draws a chyron (the bar at the bottom of a news story) on each frame of the canvas.

Pixi JS fill behaviour vs SVG fill behaviour

I'm wondering if there's a way to get the behaviour of Pixi.JS's Graphics API to fill the same way SVG paths are filled. I am guessing there may not be a quick-fix way to do this.
Basically, in SVGs when a path with a fill crosses over itself, the positive space will automatically get filled, whereas in the Pixi.JS Graphics API, if the path crosses itself it seems to try and fill the largest outside space.
This is quite difficult to explain in text so here is a codepen to show the differences between the two and how the same data will cause them to render in different ways.
And so you can see the code, here is my SVG:
<svg width="800" height="600" viewBox="0 0 800 600" xmlns="http://www.w3.org/2000/svg" style="background-color:#5BBA6F">
<path style="fill: #E74C3C; stroke:black; stroke-width:3" d="M 200 200 L 700 200 L 600 400 L 500 100 z" />
</svg>
And here is the Pixi.js implementation:
import * as PIXI from "https://cdn.skypack.dev/pixi.js";
const app = new PIXI.Application({
width: 800,
height: 600,
backgroundColor: 0x5bba6f
});
app.start();
document.getElementById("root").appendChild(app.view)
const lineData = {
color: 0xe74c3c,
start: [200, 200],
lines: [
[700, 200],
[600, 400],
[500, 100],
]
};
const { start, lines, color } = lineData;
const g = new PIXI.Graphics();
if (g) {
g.clear();
g.lineStyle({width:3})
g.beginFill(color);
g.moveTo(start[0], start[1]);
lines.map((l) => g.lineTo(l[0], l[1]));
g.closePath();
g.endFill();
app.stage.addChild(g);
}
Thanks for taking a look.
Here's our answer (it was right there in the Pixi.js issues).
To get this behaviour you need to change the "PIXI.graphicsUtils.buildPoly.triangulate" function and pull in another library called "Tess2".
import Tess2 from 'https://cdn.skypack.dev/tess2';
function triangulate(graphicsData, graphicsGeometry)
{
let points = graphicsData.points;
const holes = graphicsData.holes;
const verts = graphicsGeometry.points;
const indices = graphicsGeometry.indices;
if (points.length >= 6)
{
const holeArray = [];
// Comming soon
for (let i = 0; i < holes.length; i++)
{
const hole = holes[i];
holeArray.push(points.length / 2);
points = points.concat(hole.points);
}
console.log(points)
// Tesselate
const res = Tess2.tesselate({
contours: [points],
windingRule: Tess2.WINDING_ODD ,
elementType: Tess2.POLYGONS,
polySize: 3,
vertexSize: 2
});
if (!res.elements.length)
{
return;
}
const vrt = res.vertices;
const elm = res.elements;
const vertPos = verts.length / 2;
for (var i = 0; i < res.elements.length; i++ )
{
indices.push(res.elements[i] + vertPos);
}
for(let i = 0; i < vrt.length; i++) {
verts.push(vrt[i]);
}
}
}
Then extend the Pixi.js graphics class like this:
class TessGraphics extends PIXI.Graphics {
render(r) {
PIXI.graphicsUtils.buildPoly.triangulate = triangulate;
super.render(r);
}
}
I've updated the codepen to contain the broken version and the fixed version.
Not exactly a super easy fix. But hey I'm excited it's going!

How to get top left of an object inside group fabricjs

How can I get canvas-relative position (top, left) of triangle inside an group as bellow image?
I followed this topic: How to get the canvas-relative position of an object that is in a group? but it only right when group is not rotated.
Working example you may find here: http://jsfiddle.net/mmalex/2rsevdLa/
Fabricjs provides a comprehensive explanation of how transformations are applied to the objects: http://fabricjs.com/using-transformations
Quick answer: the coordinates of an object inside the group is a point [0,0] transformed exactly how the object in the group was transformed.
Follow my comments in code to get the idea.
// 1. arrange canvas layout with group of two rectangles
var canvas = new fabric.Canvas(document.getElementById('c'));
var rect = new fabric.Rect({
width: 100,
height: 100,
left: 50,
top: 50,
fill: 'rgba(255,0,0,0.25)'
});
var smallRect = new fabric.Rect({
width: 12,
height: 12,
left: 150 - 12,
top: 50 + 50 - 12 / 2 - 10,
fill: 'rgba(250,250,0,0.5)'
});
// 2. add a position marker (red dot) for visibility and debug reasons
var refRect = new fabric.Rect({
width: 3,
height: 3,
left: 100,
top: 100,
fill: 'rgba(255,0,0,0.75)'
});
var group = new fabric.Group([rect, smallRect], {
originX: 'center',
originY: 'center'
});
canvas.add(group);
canvas.add(refRect);
canvas.renderAll();
// 3. calculate coordinates of child object in canvas space coords
function getCoords() {
// get transformation matrixes for object and group individually
var mGroup = group.calcTransformMatrix(true);
// flag true means that we need local transformation for the object,
// i.e. how object is positioned INSIDE the group
var mObject = smallRect.calcTransformMatrix(true);
console.log("group: ", fabric.util.qrDecompose(mGroup));
console.log("rect: ", fabric.util.qrDecompose(mObject));
// get total transformattions that were applied to the child object,
// the child is transformed in following order:
// canvas zoom and pan => group transformation => nested object => nested object => etc...
// for simplicity, ignore canvas zoom and pan
var mTotal = fabric.util.multiplyTransformMatrices(mGroup, mObject);
console.log("total: ", fabric.util.qrDecompose(mTotal));
// just apply transforms to origin to get what we want
var c = new fabric.Point(0, 0);
var p = fabric.util.transformPoint(c, mTotal);
console.log("coords: ", p);
document.getElementById("output").innerHTML = "Coords: " + JSON.stringify(p);
// do some chores, place red point
refRect.left = p.x - 3 / 2;
refRect.top = p.y - 3 / 2;
canvas.bringToFront(refRect);
canvas.renderAll();
}
a very simple way to get topleft is
var cords = object._getLeftTopCoords();
cords.x and cord.y will give you the result

Applying clipTo path on image in fabric.js incorrectly repositions the image

Please take a look at this fiddle:
http://jsfiddle.net/2ktvyk4e/5/
var imgUrl, snapshotCanvas;
imgUrl = 'http://cdn-development.wecora.com/boards/backgrounds/000/001/388/cover/ocean-background.jpg';
snapshotCanvas = new fabric.StaticCanvas('snapshotCanvas', {
backgroundColor: '#e0e0e0',
width: 1000,
height: 1500
});
fabric.Image.fromURL(imgUrl, function(img) {
img.set({
width: 1000,
left: 0,
top: 0,
clipTo: function(ctx) {
return ctx.rect(0, 0, 1000, 400);
}
});
return snapshotCanvas.add(img).renderAll();
}, {
crossOrigin: 'Anonymous'
});
It's pretty simple. I'm loading an image and then trying to clip it so that the full width of the canvas but clipped so only the top 400 pixels are showing. For some reason, the clipTo causes the image to move and resize inexplicably:
As you can see, when the clipping path is applied the image is repositioned on the canvas inexplicably. If I remove the clipTo, then the image loads full canvas width no problem (of course its also full height, which we don't want).
I have no idea what is happening here or why this is occuring so any help is appreciated.
Make sure you have originX and originY left to top and left on both the canvas and your image. Also - you don't really need to clip anything to the canvas. The only real use I've found for clipTo was clipping collage images to their bounding shapes. If you want to restrict the image from being dragged above that 400px, I would recommend rendering a rectangle below it (evented = false, selectable = false) and then clipping to that rectangle.
I put this together without clipTo (and changed some numbers so I wasn't scrolling sideways). It renders the image half way down the canvas.
http://jsfiddle.net/2ktvyk4e/6/
Edit:
I dug through some source code to find the clipByName method and the two helper methods for finding stuff. I use this for keeping track of collage images and their bounding rectanlges ("images" and "clips"). I store them in an object:
imageObjects: {
'collage_0': http://some.tld/to/image.ext,
'collage_1': http://some.tld/to/image2.ext
}
Helper methods for finding either the clip or image:
findClipByClipName: function (clipName) {
var clip = _(canvas.getObjects()).where({ clipFor: clipName }).first();
return clip;
},
findImageByClipFor: function (clipFor) {
var image = _(canvas.getObjects()).where({ clipName: clipFor }).first();
return image;
},
Actual clipping method:
clipByName: function (ctx) {
this.setCoords();
var clipRect, scaleXTo1, scaleYTo1;
clipRect = collage.findClipByClipName(this.clipName);
scaleXTo1 = (1 / this.scaleX);
scaleYTo1 = (1 / this.scaleY);
ctx.save();
var ctxLeft, ctxTop;
ctxLeft = -(this.width / 2) + clipRect.strokeWidth;
ctxTop = -(this.height / 2) + clipRect.strokeWidth;
ctx.translate(ctxLeft, ctxTop);
ctx.rotate(degToRad(this.angle * -1));
ctx.scale(scaleXTo1, scaleYTo1);
ctx.beginPath();
ctx.rect(
clipRect.left - this.oCoords.tl.x,
clipRect.top - this.oCoords.tl.y,
clipRect.width,
clipRect.height
);
ctx.closePath();
ctx.restore();
function degToRad(degrees) {
return degrees * (Math.PI / 180);
}
},
And finally adding images to canvas where all of this comes together:
var clipName, clip, image;
clipName = helpers.findKeyByValue(url, this.imageObjects);
clip = this.findClipByClipName(clipName);
image = new Image();
image.onload = function () {
var collageImage = new fabric.Image(image, $.extend({}, collage.commonImageProps, {
left: clip.left,
top: clip.top,
clipName: clipName,
clipTo: function (ctx) {
return _.bind(collage.clipByName, collageImage)(ctx);
}
}));
collage.scaleImagesToClip(collageImage);
canvas.add(collageImage);
};

Resources