I am currently building some sort of meme-editor in react and for that I am using konvajs to function similar to a Canvas.
My problem is that I am unable to center the items inside the canva stage, as there seems to be some property that just overrides my styling.
This is (part of) the return statement in my react-component:
<div className="mycanvas">
<Stage width={500} height={500} className="stage">
<Layer>
<Image image={image} className="meme" />
{textFields.map((text) => (
<Text
text={text}
draggable
fontFamily="Impact"
fontSize="30"
stroke="white"
/>
))}
</Layer>
</Stage>
</div>
And this is how the output gets rendered.
I have coloured the background of the wrapper blue, to show in which box the image should be centered.
I have already tried CSS on the classes "mycanvas", "stage" and "meme" and also on "konvajs-content" (as that showed up in my inspector for some reason). I have used align-items: center, margin: auto and a couple others, but I think normal CSS does not really apply here.
I think it is an issue regarding the generaly styling of konvajs components, but unfortunately I could not find any solution on stackoverflow or the konva documentation.
This is an instance where CSS can't help. When the image is applied to the canvas using its height and width at the x and y coordinates you supply, the pixels of the image become part of the rasterized canvas. In other words, the image doesn't exist independent of the canvas.
Therefore, if you want to center the image inside of your canvas, you need to do a little math to calculate the x and y coordinates that will place the image centered inside the canvas.
Demo
For example, if your canvas size is 500px tall and your image has a height of 350px, then you need to set the y position to 75px (i.e., (500 - 350) / 2).
The demo code below shows how to replicate the behavior of CSS object-fit: contain. This will adjust the image to fill the canvas in one direction, and then center the image in the other direction.
import { useState, useEffect } from "react";
import { Stage, Layer, Image, Text } from "react-konva";
function Example() {
const w = window.innerWidth;
const h = window.innerHeight;
const src = "https://konvajs.org/assets/yoda.jpg";
const [image, setImage] = useState(null);
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const image = new window.Image();
image.src = src;
image.addEventListener("load", handleLoad);
function handleLoad(event) {
const image = event.currentTarget;
/* after the image is loaded, you can get it's dimensions */
const imgNaturalWidth = image.width;
const imgNaturalHeight = image.height;
/*
calculate the horizontal and vertical ratio of the
image dimensions versus the canvas dimensions
*/
const hRatio = w / imgNaturalWidth;
const vRatio = h / imgNaturalHeight;
/*
to replicate the CSS Object-Fit "contain" behavior,
choose the smaller of the horizontal and vertical
ratios
if you want a "cover" behavior, use Math.max to
choose the larger of the two ratios instead
*/
const ratio = Math.min(hRatio, vRatio);
/*
scale the image to fit the canvas
*/
image.width = imgNaturalWidth * ratio;
image.height = imgNaturalHeight * ratio;
/*
calculate the offsets so the image is centered inside
the canvas
*/
const xOffset = (w - image.width) / 2;
const yOffset = (h - image.height) / 2;
setPos({
x: xOffset,
y: yOffset
});
setImage(image);
}
return () => {
image.removeEventListener("load", handleLoad);
};
}, [src, h, w]);
return (
<Stage width={w} height={h} style={{ background: "black" }}>
<Layer>
<Image x={pos.x} y={pos.y} image={image} />
<Text
text="I am centered"
fontFamily="Impact"
fontSize={50}
stroke="white"
strokeWidth={1}
x={pos.x}
y={pos.y}
/>
</Layer>
</Stage>
);
}
Related
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 10 months ago.
Improve this question
I'm loading in several images. They are various lengths and widths, but I would like for them all to feel as though they are the same "size". So, if one image is 200x100 and another image is 200x400, I would like for them both to scale in such a way that they take up the same amount of space on the screen. If I fix the width to be 200, then the second element is 4 times the size of the first.
For example:
.my_img {
width: 200px;
}
produces this behavior
How can I use css to fix the area of an image or other element? Where by area, I mean literally length times width. I would like to load in an image of arbitrary dimensions, and scale it (preserving aspect ratio) so that its area is fixed to be a given value.
I don't believe you can do this with CSS. While you can calculate square root with CSS in various ways, getting natural dimensions may be problematic. You'd need that in order to find the smallest image.
For a JavaScript solution, you'd have to first establish the smallest image area, then resize each down according to initial proportion, maintaining aspect ratio.
const images = document.querySelectorAll('img');
let smallestArea = 999999999;
const getSmallestImageByArea = () => {
images.forEach(image => {
const width = image.naturalWidth;
const height = image.naturalHeight;
if (width * height < smallestArea) {
smallestArea = width * height;
}
});
};
const sizeImagesToSmallestArea = () => {
images.forEach(image => {
let width = image.naturalWidth;
let height = image.naturalHeight;
const area = width * height;
if (area > smallestArea) {
const areaRoot = Math.sqrt(area);
const proportion = areaRoot / Math.sqrt(smallestArea);
const aspectRoot = Math.sqrt(width / height);
width = areaRoot / proportion * aspectRoot;
height = areaRoot / proportion / aspectRoot;
image.style.width = width + 'px';
image.style.height = height + 'px';
}
// show hidden images
image.style.display = 'inline';
console.log('Initial area:', area, '| Final area:', width * height);
});
};
// wait for images: https://stackoverflow.com/a/60949881/1264804
Promise.all(Array.from(document.images)
.filter(img => !img.complete)
.map(img => new Promise(resolve => {
img.onload = img.onerror = resolve;
}))).then(() => {
getSmallestImageByArea();
sizeImagesToSmallestArea();
});
/* hide images to prevent jumping effect */
img {
display: none;
}
<img src="https://via.placeholder.com/165x250" />
<img src="https://via.placeholder.com/100x50" />
<img src="https://via.placeholder.com/200x200" />
<img src="https://via.placeholder.com/1900x300" />
Maybe you can try something like this. if you want them to fit on a specific container you have to wrap them and set their height and width and let them fit on it using obejct-fit:cover. This sample have 2 different size images but they fit exactly the way I would do it no matter how big they are. Let me know if this is what you are looking at??
.my_img {
justify-content:space-around;
display:flex;
}
.my_img > img {
object-fit: fill;
height: 300px;
width: 300px;
border: 1px solid gray;
margin: 10px;
overflow:visible;
}
<div class="my_img">
<img src="https://www.kindpng.com/picc/m/227-2275045_daisy-yellow-bloom-frame-flower-border-flowers-transparent.png">
<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.clir.org%2Fwp-content%2Fuploads%2Fsites%2F6%2F2016%2F09%2FWelcome-banner.png&f=1&nofb=1">
</div>
I'm trying to create a ReactJS (using react bootstrap) mobile app that can resize (expand or contract) itself based on the screensize. The dimensions of one part of the app need to be calculated based on how much space is left on the screen after all other parts have rendered.
For example, consider the below markup -
var calcWidth = (100 / tableSize).toString() + '%';
return(
<Container>
<Row id='1'>Header and other static stuff here</Row>
<Row id='2'>
//A db driven table with square shaped cells goes here. It has the below structure -
<Container style={{width:'100%'}}>
<Row><Col style={{width:calcWidth, paddingBottom:calcWidth}}></Col>...</Row>
...
</Container>
</Row>
<Row id='3'>Footer and other static stuff here</Row>
</Container>
);
In the above markup, Row id 1 and 3 contain static stuff like headers, footers, buttons, titles etc. The Row id 2 contains a table that can contain "n" number of cells and each cell needs to be square shaped with content horizontally and vertically centerd.
The above code correctly calculates the width of each cell from the width of the container (which is 100%) and creates square shaped cells and fits perfectly horizontally. But since the height is same as the width, it gets bigger vertically and drives the footer beyond the screen. We want to avoid scrollbars. The solution seems to be to calculate calcWidth based on the remaining height available to the table, something like the below -
var remainingHeight = <total height of the container> - <height taken up by Row 1> - <height taken up by Row 3>
var width = <width of the screen>
var calcWidth = ((remainingHeight < width ? remainingHeight : width) / tableSize).toString() + '%';
My questions are -
How to calculate the remainingHeight variable above? How to let Row1 and Row3 render before Row2 and then calculate the remaining height?
How to find the total height and width of the container?
Any other better way of doing this? I'm just a newbie, probably there are some css tools to do it more efficiently?
Here you can find an example on how to calculate the height of react components after rendering:
export default function App() {
const [height1, setHeigt1] = useState(0);
const [height2, setHeight2] = useState(0);
const [height3, setHeight3] = useState(0);
const [remainingHeight, setRemainingHeight] = useState(0);
useEffect(() => {
const remainingHeight = 100 - height1 - height2 - height3;
console.log(remainingHeight);
setRemainingHeight(remainingHeight);
}, [setRemainingHeight, height1, height2, height3]);
return (
<div
id="container"
style={{
height: "100px",
backgroundColor: "firebrick",
padding: "15px"
}}
>
<ResizableComponent
id="component-1"
content={`Initial component 1 height = ${height1}`}
onHeightUpdated={setHeigt1}
/>
<ResizableComponent
id="component-2"
content={`Initial component 2 height = ${height2}`}
onHeightUpdated={setHeight2}
/>
<ResizableComponent
id="component-3"
content={`Initial component 3 height = ${height3}`}
onHeightUpdated={setHeight3}
remainingHeight={remainingHeight}
/>
</div>
);
}
export function ResizableComponent({
id,
content,
onHeightUpdated,
remainingHeight
}) {
const [height, setHeight] = useState(0);
const [isFirstRender, setIsFirstRender] = useState(true);
useEffect(() => {
const newHeight = document.getElementById(id).clientHeight;
if (height !== newHeight && isFirstRender) {
setHeight(newHeight);
setIsFirstRender(false);
}
}, [isFirstRender, id, height, onHeightUpdated, remainingHeight]);
useEffect(() => {
onHeightUpdated(height);
}, [height, onHeightUpdated]);
return (
<div
id={id}
style={
remainingHeight
? {
backgroundColor: "pink",
height: `calc(${height}px + ${remainingHeight}px)`
}
: { backgroundColor: "pink" }
}
>
{content}
</div>
);
}
I have been trying to do something that seemed easy, but I have been trying for a few hours and I can't find the solution.
I have an SVG that needs to be on top of a screen. It came from the designer with these dimensions:
<Svg width="354px" height="190px" viewBox="0 0 354 190">...</Svg>
In React Native, that would go inside of a container, and the SVG needs to take the full width of the screen, which I am taking from:
Dimensions.get("window").width
My problem is, I haven't found a way to scale the SVG to take 100% of the screen width, finding out the correct height (or a way for it to be set automatically), and preserve the aspect ratio. I've tried like a million things, including playing around with the container's aspectRatio style and its height (or not setting the height at all). Whenever I've found some "proportions" that worked, I tried in a different device with different screen width and it didn't look good at all (cropped, smaller than the screen's width, etc).
I feel like the preserveAspectRatio property in the SVG (react-native-svg) is somehow conflicting with the aspectRatio style. And I am totally lost with the preserveAspectRatio, I haven't found a way to make it scale without being cropped.
Does anyone have any idea how to achieve this?
This is my final code, which returns a HeatMap component showing an SVG, but although it has the correct height, part of the SVG is out of the screen from the right (looks cropped because it's too wide):
const windowWidth = Dimensions.get("window").width;
const getSVGRootProps = ({ width, height }) => ({
width: "100%",
height: "100%",
viewBox: `0 0 ${width} ${height}`,
preserveAspectRatio: "xMinYMin meet",
});
const FieldShape = () => {
const width = 354; // Original width
const height = 190; // Original height
const aspectRatio = width / height;
// adjusted height = <screen width> * original height / original width
const calculatedHeight = (windowWidth * height) / width;
const fieldStyles = {
width: windowWidth,
height: calculatedHeight,
aspectRatio,
};
return (
<View style={fieldStyles}>
<Svg {...getSVGRootProps({ windowWidth, calculatedHeight })}>
...
</Svg>
</View>
);
};
const HeatMap = () => {
return <FieldShape />;
};
This is the result:
I've found the solution, and I am posting it here in case anyone runs into the same problem with react native and SVG. Basically, if you're trying to get an SVG file and turn it into a component with "dynamic" parts (like programmatically set colors to path based on data, or display SVG text), you'll probably run into this issue.
What I did was to use SVGR to convert the original SVG into a react native component (with react-native-svg). Then I just replaced hardcoded data with variables (from props) as needed. It looked good, but I had a problem with the component's size. I couldn't find a consistent way to display it across different device sizes and resolutions. It seemed easy, but I tried for hours and the results were different on each screen size. After asking here and opening an issue on react-native-svg repo, I got no answers and no clues (not blaming anyone, just saying it was maybe not something a lot of people runs into). So I digged and digged and I finally found this post by Lea Verou, where she talked about absolute and relative SVG paths. That made me think that maybe I was having so many issues trying to find the perfect resizing formula because my paths weren't relative, but absolute. So I tried this jsfiddle by heyzeuss, pasting my (original) SVG code, and then copying the results. I pasted the results into this handy SVGR playground (SVG to JS) tool, and then I changed some bits to achieve my goal:
I want my SVG to take the full screen's width, width its height scaled accordingly.
So this is what I changed:
// SVG's original size is 519 width, 260 height
// <Svg width="519" height="260" viewBox="0 0 519 260">...</Svg>
// I also added a container, which enforces the aspect ratio
const originalWidth = 519;
const originalHeight = 260;
const aspectRatio = originalWidth / originalHeight;
const windowWidth = Dimensions.get("window").width;
return (
<View style={{ width: windowWidth, aspectRatio }}>
<Svg
width="100%"
height="100%"
viewBox={`0 0 ${originalWidth} ${originalHeight}`}>
...
</Svg>
</View>
)
I learned some things while doing this, for example, that there's a #svgr/cli included in create-react-app and also available in my react-native project without installing anything extra, so it must be bundled with the original dependencies too. You can run this command and it'll turn a single file or all files in a folder from .svg to React components:
npx #svgr/cli [-d out-dir] [--ignore-existing] [src-dir]
The script used to transform absolute paths to relatives is part of this library called Snap.svg, but you'll only need like a 1% of it (Snap.path.toRelative). I am thinking of having a small tool that would take all the paths in an svg file, and apply this transformation. To be totally honest, I have been dealing with SVG files for years but I never really had a proper look at how it works internally, the format of the paths, coordinates, and so on, so this has been highly instructive :)
I hope this helps you if you find yourself in the same situation!
Expanding on Luis Serrano answer, you can leave out windowWidth and use 100%.
Example:
import * as React from 'react';
import Svg, { Circle, Path } from 'react-native-svg';
import { View } from 'react-native';
export default function SvgExample() {
const originalWidth = 500;
const originalHeight = 500;
const aspectRatio = originalWidth / originalHeight;
return (
<View style={{ width: "100%", aspectRatio, backgroundColor: "salmon" }}>
<Svg width="100%" height="100%" viewBox={`0 0 ${originalWidth} ${originalHeight}`}>
<Circle cx="250" cy="250" r="40" fill="yellow" />
<Circle cx="240" cy="240" r="4" fill="black" />
<Circle cx="260" cy="240" r="4" fill="black" />
<Path d="M 240 265 A 20 20 0 0 0 260 260" strokeWidth={2} stroke="black" />
</Svg>
</View>
)
}
This has the advantage that it respects the padding of the enclosing View.
import { StyleSheet, View } from 'react-native';
import SvgExample from './SvgExample';
export default function TabOneScreen() {
return (
<View style={styles.container}>
<SvgExample/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
padding: 20,
},
});
Thanks to you both guys Luis Serrano & Phierru!
I put this whole thing into an example Snack.
SNACK:
https://snack.expo.dev/#changnoi69/fbf937
When you change the marginLeft and marginRight of that view that is wrapped around the SVG-Component the SVG resizes according to it.
<View style={{marginLeft:"20%", marginRight:"20%", backgroundColor: "pink"}}>
<NoInternetConnectionSVG />
</View>
Here is a simple codepen that draws a blue square and then a red square on top of it with opacity .5. The overlapped region in the center is purple. How do I measure that color using SVG properties or a JavaScript call? Note: the composite could be arbitrarily complex, involving raster images too, so I'm looking for a colorpicker function, not a computation based on the fill attributes.
<svg
width=200 height=200 viewbox="0 0 1000 1000"
style="float: left; border: 1px solid black">
<g>
<rect width=750 height=750 fill=blue transform="" />
<rect width=750 height=750 fill=red transform="translate(250,250)" opacity=0.5 />
</g>
</svg>
One solution, as per the comment above, is to copy into a canvas:
function colorPicker(x, y, fn) {
const canvas = document.querySelector('canvas');
const svg = document.querySelector('svg');
const ctx = canvas.getContext('2d');
const img = new Image();
const xml = new XMLSerializer().serializeToString(svg);
const svg64 = btoa(xml);
const b64Start = 'data:image/svg+xml;base64,';
const image64 = b64Start + svg64;
img.src = image64;
img.onload = () => {
ctx.drawImage(img, 0, 0);
const d = ctx.getImageData(x, y, 1, 1).data;
const color = `rgba(${d[0]}, ${d[1]}, ${d[2]}, d[3]})`;
fn(color);
}
}
const out = document.getElementById('out');
colorPicker(100, 100, color => out.innerHTML = color );
I don't like this solution at all; the browser itself has access to the color data, and it's silly we have to jump through all of these hoops, serializing, re-drawing, etc to get at it.
Note that the above code isn't portable because it relies on hardcoded id's for the svg and canvas elements. See the codepen to see it working.
UPDATE: I have posted and accepted a fully working solution in the answers section. Any code in this section is to be used as reference for comparison to your own NON-WORKING code, but is not to be used as the solution.
I'm building a dashboard and using d3.js to add a world map that will plot tweets in real time based on geo location.
The world.json file referenced in the d3.json() line is downloadable HERE (it's called world-countries.json).
The map is on the page as an SVG container and is rendered using d3.
Below are the relevant code slices.
<div id="mapContainer">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="500"></svg>
</div>
#mapContainer svg {
display:block;
margin:0 auto;
}
#mapContainer path {
fill:#DDD;
stroke:#FFF;
}
// generate US plot
function draw() {
var map = d3.select("svg");
var width = $("svg").parent().width();
var height = $("svg").parent().height();
var projection = d3.geo.equirectangular().scale(185).translate([width/2, height/2]);
var path = d3.geo.path().projection(projection);
d3.json('plugins/maps/world.json', function(collection) {
map.selectAll('path').data(collection.features).enter()
.append('path')
.attr('d', path)
.attr("width", width)
.attr("height", height);
});
}
draw();
latestLoop();
$(window).resize(function() {
draw();
});
UPDATE: I have scaled the map to an acceptable size (for my particular browser size), but it still will not scale and center when I change the size of the window. IF, however, I resize the window, then hit refresh, then the map will be centered once the page is reloaded. However, since the scale is static, it is not scaled properly.
COMPLETE SOLUTION:
Here's the solution which will resize the map AFTER the user has released the edge of the window to resize it, and center it in the parent container.
<div id="mapContainer"></div>
function draw(ht) {
$("#mapContainer").html("<svg id='map' xmlns='http://www.w3.org/2000/svg' width='100%' height='" + ht + "'></svg>");
map = d3.select("svg");
var width = $("svg").parent().width();
var height = ht;
// I discovered that the unscaled equirectangular map is 640x360. Thus, we
// should scale our map accordingly. Depending on the width ratio of the new
// container, the scale will be this ratio * 100. You could also use the height
// instead. The aspect ratio of an equirectangular map is 2:1, so that's why
// our height is half of our width.
projection = d3.geo.equirectangular().scale((width/640)*100).translate([width/2, height/2]);
var path = d3.geo.path().projection(projection);
d3.json('plugins/maps/world.json', function(collection) {
map.selectAll('path').data(collection.features).enter()
.append('path')
.attr('d', path)
.attr("width", width)
.attr("height", width/2);
});
}
draw($("#mapContainer").width()/2);
$(window).resize(function() {
if(this.resizeTO) clearTimeout(this.resizeTO);
this.resizeTO = setTimeout(function() {
$(this).trigger('resizeEnd');
}, 500);
});
$(window).bind('resizeEnd', function() {
var height = $("#mapContainer").width()/2;
$("#mapContainer svg").css("height", height);
draw(height);
});
The selection object is an multidimensional array, although in most cases it will probably have only one object in it. That object has a "clientWidth" field that tells you how wide its parent is.
So you can do this:
var selection = d3.select("#chart");
width = selection[0][0].clientWidth;
This should work:
<svg
xmlns="http://www.w3.org/2000/svg"
width="860"
height="500"
viewBox="0 0 860 500"
preserveAspectRatio="xMinYMin meet">
The best choice is to have a combined use of aspect ratio on normal definition of d3 graph's width and height. This has helped me in lot of my graph works.
Step 1 : Dynamically get the height of the div to which the graph has to be appended.
Step 2 : Declare width as aspect ratio with respect to the dynamically caught height.
var graph_div = document.getElementById(graph.divId);
graph.height = graph_div.clientHeight;
graph.width = (960/1200)*graph.height;
In d3 v4, we could do this
const projection = d3.geo.equirectangular().fitSize([width, height], geojson);
const path = d3.geo.path().projection(projection);
fitSize is equivalent to
fitExtent([[0, 0], [width, height]], geojson)
fill free to add padding