make canvas content stretch to full parent width - css

I have an array of 510 elements (number[]). I need to create a chart with this data from an array. The problem is that when I try to draw a chart with 510 points, the chart's width is stretched to a width that is larger than the width of the div. How to set canvas width equal to parent width?
<template>
<canvas class="canvas"
ref="canvasRef"
>
</canvas>
</template>
<script lang="ts">
import { Options, Prop, Watch } from 'vue-property-decorator'
import BaseComponent from '#/views/components/BaseComponent'
import { IPipe } from '#/views/apps/wave/historian/components/ChartContainer.vue'
#Options({
name: 'TestGraph',
})
export default class TestGraph extends BaseComponent {
#Prop() pipeList!: IPipe[]
mounted () {
this.$nextTick(() => {
this.drawGraph()
})
}
#Watch('pipeList', { deep: true })
drawGraph () {
const canvasRef = this.$refs.canvasRef as HTMLCanvasElement
canvasRef.width = canvasRef.offsetParent!.clientWidth
const ctx: CanvasRenderingContext2D = canvasRef.getContext('2d')!
ctx.scale(1.3, 1.4)
this.pipeList.forEach(it => {
let prevCoords = 0
if (it.selected) {
it.data.forEach((item, idx) => {
ctx.moveTo(prevCoords, item)
ctx.lineTo(prevCoords + 5, it.data[idx + 1])
prevCoords += 5
})
}
})
ctx.strokeStyle = 'rgb(255,255,255)'
ctx.stroke()
}
}
</script>
<style lang="scss" scoped>
.canvas {
display: flex;
width: 100%;
height: 100%;
position: absolute;
}
</style>

Related

Applying CSS classes to a dynamic collection of React nodes on a consistent delay

I have a dynamically sized collection of objects being passed into a Nav component that are being mapped and rendered as buttons. I want to apply a CSS animation to each button so that they slide in from off screen one at a time when the Nav component mounts. I figured that I would set up a loop through each one that updates a boolean value inside of a corresponding state object which applies the CSS class to the button to animate it, but each time that state object is updated, all of the buttons rerender which in turn starts all of the animations over. How can I prevent these rerenders?
// Nav.jsx
import React, { useState, useEffect } from 'react';
import { Button } from '../../../components';
import './Nav.scss';
const Nav = ({ actions }) => {
const [renderStates, setRenderStates] = useState(actions.reduce((accum, val) => {
return {...accum, [val.id]: false};
}, {}));
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(75);
}
setRenderStates((prev) => ({
...prev,
[i]: true,
}));
};
};
updateStates();
}, [actions.length]);
return (
<div className='Nav'>
{actions.map((act) => (
<div className={`Nav__Button ${renderStates[act.id] ? 'Animate' : ''}`} key={act.id}>
<Button icon={act.icon} onClick={act.onClick} />
</div>
))}
</div>
);
};
export default Nav;
/* Nav.scss */
.Nav {
height: 100%;
width: fit-content;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
align-self: center;
padding: 1rem;
}
.Nav > * {
margin: 20% 0,
}
.Nav__Button {
margin-left: -5rem;
}
.Animate {
animation: slideInFromLeft .4s ease;
}
#keyframes slideInFromLeft {
0% {
margin-left: -5rem;
}
75% {
margin-left: .5rem;
}
100% {
margin-left: 0;
}
}
Here's a codesandbox that illustrates the problem (refresh the embedded browser to see the issue):
https://codesandbox.io/s/react-css-animations-on-timer-8mxnsz
Any help would be appreciated. Thanks.
You will need to create a from the elements inside actions.map and render a memoized version of it so that if the props do not change it will not re-render.
import { useState, useEffect, memo } from "react";
import "./styles.css";
const Test = ({ animate, label }) => {
return (
<div className={`Nav__Button ${animate ? "Animate" : ""}`}>
<button>{label}</button>
</div>
);
};
const TestMemo = memo(Test);
export default function App() {
const actions = [
{
id: 0,
label: "button 0"
},
{
id: 1,
label: "button 1"
},
{
id: 2,
label: "button 2"
},
{
id: 3,
label: "button 3"
}
];
const [renderStates, setRenderStates] = useState(
actions.reduce((accum, val) => {
return { ...accum, [val.id]: false };
}, {})
);
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(2000);
}
setRenderStates((prev) => ({
...prev,
[i]: true
}));
}
};
updateStates();
}, [actions.length]);
return (
<div className="App">
{actions.map((act) => (
<TestMemo animate={renderStates[act.id]} label={act.label} />
))}
</div>
);
}

crop image with react - customize react-easy-crop styling

I'm trying to make a very simple react component that would crop images with react-easy-crop. Apparently it is possible to customize the style of react-easy-crop module with style prop that takes 3 objects: containerStyle, mediaStyle and cropAreaStyle.
This is the default layout:
I want to expand cropArea to full width of its container and to fit media in it by height (so that we don't see the part of the original image outside of cropArea) but can't figure out how to do it. The cropAreaStyle object doesn't seem to affect width or height since it is calculated and injected in the module file (even after setting disableAutomaticStylesInjection to true).
import React from 'react'
import ReactDOM from 'react-dom'
import Cropper from 'react-easy-crop'
import './styles.css'
class App extends React.Component {
state = {
imageSrc:
'https://img.huffingtonpost.com/asset/5ab4d4ac2000007d06eb2c56.jpeg?cache=sih0jwle4e&ops=1910_1000',
crop: { x: 0, y: 0 },
zoom: 1,
aspect: 1 / 1,
style: { containerStyle: { position: "absolute", top: "0", width: "calc(100% - 2px)", height: window.innerWidth, overflow: "hidden", border: "1px solid black" },
mediaStyle: { height: "100%", display: "block" },
cropAreaStyle: {position: "absolute", top: "0", border: "1px solid black", width: "100%", height: "100%" }}
}
onCropChange = (crop) => {
this.setState({ crop })
}
onCropComplete = (croppedArea, croppedAreaPixels) => {
console.log(croppedArea, croppedAreaPixels)
}
onZoomChange = (zoom) => {
this.setState({ zoom })
}
render() {
return (
<div className="App">
<div className="crop-container">
<Cropper
image={this.state.imageSrc}
crop={this.state.crop}
zoom={this.state.zoom}
aspect={this.state.aspect}
onCropChange={this.onCropChange}
onCropComplete={this.onCropComplete}
onZoomChange={this.onZoomChange}
style={this.state.style}
disableAutomaticStylesInjection={true}
/>
</div>
</div>
)
}
}
const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)
This is what I'm trying to achieve:
The black square is cropArea that I can't resize...
I want cropArea to remain square.
Is there an easy way to do this, without changing the module file?
The solution with another module is acceptable also
Thanks in advance
I tried to use the object cropAreaStyle but it's not working, instead use the prop cropSize and don't pass the prop aspect.
In order to get the height of the media pass the prop onMediaLoaded:
onMediaLoad = (mediaSize) => {
this.setState({
cropHeight: mediaSize.height,
});
};
App.js
import React from 'react';
import ReactDOM from 'react-dom';
import Cropper from 'react-easy-crop';
import './style.css';
class App extends React.Component {
state = {
imageSrc:
'https://img.huffingtonpost.com/asset/5ab4d4ac2000007d06eb2c56.jpeg?cache=sih0jwle4e&ops=1910_1000',
crop: { x: 0, y: 0 },
zoom: 1,
cropHeight: 0,
};
onCropChange = (crop) => {
this.setState({ crop });
};
onCropComplete = (croppedArea, croppedAreaPixels) => {
console.log(croppedArea, croppedAreaPixels);
};
onZoomChange = (zoom) => {
this.setState({ zoom });
};
onMediaLoad = (mediaSize) => {
this.setState({
cropHeight: mediaSize.height,
});
};
render() {
const cropSize = {
height: `${this.state.cropHeight}px`,
width: '100%',
};
return (
<div className="App">
<div className="crop-container">
<Cropper
image={this.state.imageSrc}
crop={this.state.crop}
zoom={this.state.zoom}
onCropChange={this.onCropChange}
onCropComplete={this.onCropComplete}
onZoomChange={this.onZoomChange}
onMediaLoaded={this.onMediaLoad}
cropSize={cropSize}
/>
</div>
</div>
);
}
}
export default App;
Demo: https://stackblitz.com/edit/react-4zmgud
It seems that what you need is the objectFit property set to vertical-cover.
See this demo: https://codesandbox.io/s/crazy-liskov-04u7m0

CSS/Vue.js synchronize animation reset with ref value change

I want to create an animation that fades between 3 states. To store these states (that contain an image and some text), I've created an array of objects (uploadSteps). I increment the index of this array at a specific interval to create an animation.
My problem comes with CSS, I would like to create a fade-out/fade-in effect between each step, so that the transition is more smooth. It works most of the time but sometimes, after a refresh and for the first transition, I think the CSS animation is restarted before the state is changed. I wasn't able to reproduce it constantly so far.
What it does => Image fade-in -> Image 1 -> Image 1 fade-Out -> Image 1 -> Image 2 fade-in
What I want => Image fade-in -> Image 1 -> Image 1 fade-Out -> Image 2 fade-in
If you pay attention you can see that there's kind of a blink effect at the transition between image 1 and 2. What could I do to solve this ? Help appreciated !
PS: I've included some videos to illustrate my issue
https://streamable.com/4aigjq - Animation KO
https://streamable.com/umu3nj - Animation OK
<template>
<div class="screenshot-upload-processing-container">
<div class="image-container">
<img
ref="imageRef"
:src="
currentStep ? $requireImage(currentStep.imagePath).toString() : ''
"
:alt="currentStep.text"
class="image"
/>
</div>
<div class="centered-row">
<span ref="textRef" class="text">{{
currentStep ? currentStep.text : ''
}}</span>
</div>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
onBeforeUnmount,
ref,
} from '#nuxtjs/composition-api';
export default defineComponent({
name: 'ScreenshotUploadProcessing',
setup() {
const stepNumber = ref(0);
const timer = ref<NodeJS.Timeout | null>(null);
const imageRef = ref<HTMLImageElement | null>(null);
const textRef = ref<HTMLSpanElement | null>(null);
const uploadSteps = computed(() => {
return [
{
imagePath: 'image1.svg',
text: 'Text 1',
},
{
imagePath: 'image2.svg',
text: 'Text 2',
},
{
imagePath: 'image3.svg',
text: 'Text 3',
},
];
});
const resetAnimations = () => {
const image = imageRef.value;
const text = textRef.value;
if (image) {
image.style.animation = 'none';
void image.offsetHeight;
image.style.animation = '';
}
if (text) {
text.style.animation = 'none';
void text.offsetHeight;
text.style.animation = '';
}
};
const updateStepNumber = () => {
timer.value = setInterval(() => {
if (stepNumber.value === uploadSteps.value.length - 1) {
stepNumber.value = 0;
} else {
stepNumber.value++;
}
resetAnimations();
}, 3000);
};
onBeforeUnmount(() => {
clearInterval(timer.value as NodeJS.Timeout);
});
const currentStep = computed(() => uploadSteps.value[stepNumber.value]);
updateStepNumber();
return {
stepNumber,
currentStep,
imageRef,
textRef,
uploadSteps,
};
},
});
</script>
<style lang="scss" scoped>
#keyframes fade {
0%,
100% {
opacity: 0;
}
40%,
60% {
opacity: 1;
}
}
.screenshot-upload-processing-container {
display: flex;
flex-direction: column;
height: 100%;
}
.image {
animation: fade 3s linear;
}
.image-container {
display: flex;
align-items: center;
justify-content: center;
height: 27rem;
margin-top: 10.3rem;
}
.centered-row {
display: flex;
align-items: center;
justify-content: center;
margin-top: $spacing-xl;
}
.text {
#extend %text-gradient-1;
animation: fade 3s linear;
font-weight: $font-weight-medium;
}
</style>

Masonry layout adaptative to child's height

I have a page on my website in which I display some cards in a masonry layout.
It's implemented in Vue 3 and I want the layout do adapt to the container's child heights.
The layout flows from top to bottom, left to right. Just like the images shows:
To achieve this, I divided the cards in different columns using the order CSS property together with a flexbox.
However, for this to work, the parent container needs to have a fixed height.
I want this to be the minimum height possible to make sure all cards fit, that is, the exact height of the longest column of the layout.
I tried to set the container's height to 0 initially and then update it based on the card's height, however, this doesn't work very well and is really janky.
<template>
<section class="container" :style="{ height: containerHeight }">
<project-card v-for="i in projects.length" :key="i" :project="projects[i - 1]"
:style="{ order: (i - 1) % numberColumns + 1, width: (100 / numberColumns) - 1.5 + '%' }"
:ref="setProjectCardRef">
</project-card>
<span v-for="i in numberColumns - 1" :key="i" :style="{ order: i }" class="item break"></span>
</section>
</template>
<script>
import Projects from "#/api/Projects";
import ProjectCard from "#/components/ProjectCard";
export default {
name: "Projects",
components: {
"project-card": ProjectCard,
},
data() {
return {
projects: [],
projectCardsRefs: [],
windowWidth: window.innerWidth,
containerHeight: "100%"
}
},
mounted() {
this.getData();
window.addEventListener('resize', () => {
this.windowWidth = window.innerWidth
})
},
methods: {
getData() {
Projects.list().then((response) => {
for (let project of response.data)
project.image_url = process.env.VUE_APP_API_ENDPOINT + project.image_url;
this.projects = response.data;
});
},
setProjectCardRef(el) {
if (!this.projectCardsRefs.includes(el))
this.projectCardsRefs.push(el)
}
},
computed: {
numberColumns() {
return Math.round(this.windowWidth / 400)
},
},
async updated() {
await new Promise(r => setTimeout(r, 200));
let heights = Array(this.numberColumns).fill(0)
for (let i = 0; i < this.projectCardsRefs.length; i++) {
const style = this.projectCardsRefs[i].$el.currentStyle || window.getComputedStyle(this.projectCardsRefs[i].$el);
const marginTop = parseInt(style.marginTop.match(/\d+/g)[0]);
const height = parseFloat(style.height.match(/\d+(.\d+)?/g)[0]);
heights[i % this.numberColumns] += height + marginTop
}
this.containerHeight = 40 + Math.max(...heights) + "px";
}
}
</script>
<style scoped>
.container {
#apply flex flex-col flex-wrap space-y-6;
}
.break {
#apply mx-3 w-0;
flex-basis: 100%;
}
</style>
How can I set the container's height based on its children in a more responsive way?
Following Paulie_D's suggestion, I implemented it using Masonry.JS
<template>
<section id="container" v-masonry transition-duration="0.2s" item-selector=".item"
percent-position="true" ref="container" :gutter="spaceBetween">
<project-card class="item" v-masonry-tile v-for="i in projects.length" :key="i" :project="projects[i - 1]"
:style="`width: ${itemWidth}px; margin-bottom: ${spaceBetween}px`"/>
</section>
</template>
<script>
import Projects from "#/api/Projects";
import ProjectCard from "#/components/ProjectCard";
export default {
name: "Projects",
components: {
"project-card": ProjectCard,
},
data() {
return {
projects: [],
containerWidth: 0,
spaceBetween: 20
}
},
mounted() {
this.getData();
new ResizeObserver(this.onResize).observe(document.getElementById("container"))
this.onResize()
},
methods: {
getData() {
Projects.list().then((response) => {
for (let project of response.data)
project.image_url = process.env.VUE_APP_API_ENDPOINT + project.image_url;
this.projects = response.data;
});
},
onResize() {
this.containerWidth = document.querySelector("main").offsetWidth;
}
},
computed: {
numberColumns() {
return Math.round(this.containerWidth / 400);
},
itemWidth() {
return (this.containerWidth - this.numberColumns * this.spaceBetween) / this.numberColumns;
}
}
}
</script>

Having trouble getting rid of the blue highlight

I've been working on a section with expandable/collapsible sections. When I click on a section to expand or collapse it, a blue focus area shows up but it is placed on a weird angle. I don't know what is causing it and would like a solution to either get rid of it or place it back at the normal horizontal angle. Does anybody have any suggestions as to how to fix this?
I am using a Macbook and Chrome browser.
The entire grey block that this component appears in is placed at an angle as you can see from the top of the image attached below but in the reverse direction from the highlighted focus area.
My css:
#import '../../theme/variables.css';
.rotatedSection {
padding-bottom: 2rem;
}
.container {
max-width: 64rem;
margin: 0 auto;
display: flex;
padding: 2rem 0;
#media screen and (max-width: 68rem) {
margin: 0 3rem;
}
}
.accordianContainer {
flex: 1;
margin-right: 2rem;
min-width: 500px;
#media screen and (max-width: $tablet-lg-max-width) {
margin-right: 0;
}
#media screen and (max-width: 900px) {
min-width: 0;
}
}
.imageContainer {
flex: 1;
margin-left: 2rem;
max-height: 300px;
display: flex;
justify-content: center;
img {
flex: 1;
}
#media screen and (max-width: $tablet-lg-max-width) {
margin-left: 0;
}
}
.heading {
composes: h2 from 'theme/text';
margin-left: auto;
margin-right: auto;
}
My react code:
import React, {Component, PropTypes} from 'react';
import RotatedSection from 'components/RotatedSection';
import AccordionItem from './AccordionItem';
import css from './styles.css';
class AccordionSectionWithImage extends Component {
constructor (props) {
super(props);
this.state = {
activeIndex: null,
};
this.onOpen = this.onOpen.bind(this);
this.onClose = this.onClose.bind(this);
this.setActive = this.setActive.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
onOpen = (index) => {
this.setActive(index);
};
onClose = (callback = () => null) => {
this.setActive(null);
callback();
};
setActive = (activeIndex) => this.setState({activeIndex});
handleClickOutside = () => this.props.collapseOnBlur && this.onClose();
render () {
const {
entry: {
items,
heading,
image,
},
showIndex,
classNames,
meta = {},
} = this.props;
const {routeParams, toggleHamburger} = meta;
const {activeIndex} = this.state;
return (
<RotatedSection color='whisper' className={css.rotatedSection}>
<div className={css.container}>
<div className={css.accordianContainer}>
<h2 className={css.heading}>{heading}</h2>
{items && items.map((item, index) => (
<AccordionItem
key={index}
showIndex={showIndex}
entry={item}
meta={{
position: index,
isOpen: (index === activeIndex),
onOpen: () => this.onOpen(index),
onClose: () => this.onClose(),
onChildClick: () => this.onClose(toggleHamburger),
routeParams,
}}
classNames={classNames}
/>
))}
</div>
<div className={css.imageContainer}>
<img src={image && image.fields && image.fields.file.url} alt='Educational assessment' />
</div>
</div>
</RotatedSection>
);
}
}
AccordionSectionWithImage.propTypes = {
meta: PropTypes.object,
entry: PropTypes.object,
collapseOnBlur: PropTypes.bool,
showIndex: PropTypes.bool,
classNames: PropTypes.object,
};
export default AccordionSectionWithImage;
React component for individual section:
function AccordionItem (props) {
const {
meta: {
isOpen,
onOpen,
onClose,
},
entry: {
heading,
text,
},
} = props;
const handleClick = () => (isOpen ? onClose() : onOpen());
return (
<div className={css.itemContainer}>
<div className={css.innerContainer}>
<h3 className={css.heading} onClick={handleClick}>
<span className={css.titleText}>{heading}</span>
<i className={`zmdi zmdi-plus ${css.titleToggle}`} />
</h3>
{isOpen && (
<div className={css.contents}>
{text}
</div>
)}
</div>
</div>
);
}
For anybody else experiencing a similar problem:
Problem only appeared on mobile phones and the device mode of chrome inspector. It was due to the tap-highlight property.
Setting -webkit-tap-highlight-color to rgba(0,0,0,0) hid the problem but it's a non standard css property so the solution may not work for all devices/browsers/users.

Resources