I have a canvas element that should be resizing as the window resizes, but for some reason it doesn't change. The element should remain a square, taking up 80% of the view height.
The relevant css looks like this, and the square class works for another element that isn't being updated in css and javascript.
static get styles() {
return [
tachyons,
css`
.square {
width: min(50vw, 75vh);
height: min(50vw, 75vh);
}`
]
}
The canvas doesn't update despite the class being recognized by the browser. I'm using Safari for testing (don't ask why).
const karelView = ({ displayAltCanvas, index, indexes, className }) => {
return html`<div class=${className || '' + ' mt5'}>
<canvas
id="canvasAlt"
class="square ${displayAltCanvas ? 'bg-light-yellow' : 'absolute o-0'}"
></canvas>
<canvas id="canvas" class="square${displayAltCanvas ? ' dn' : ''}"></canvas>
<div class="db w-100">
<input
class="w-100"
type="range"
min="0"
value=${index ? index() : 0}
max=${indexes || 0}
step="1"
#input=${e => index?.(parseInt(e.target.value))}
/>
</div>
</div>`;
};
This is where I think the error is coming from. The callback is being fired correctly, but for some reason the resolutions still match even though the window has resized and the css should have updated the clientWidth.
async handleResize(canvas) {
canvas ??= this.canvas;
if (canvas === null) {
console.warn('null canvas');
return;
}
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const resolutionMatch = canvas.width === width && canvas.height === height;
if (!resolutionMatch) {
canvas.width = width;
canvas.height = height;
}
const world = this.state || (await this.worlds).currentWorld;
// this.index !== undefined && this.index(this.index());
draw(this.canvas, world);
}
connectedCallback() {
super.connectedCallback();
this._resizers = (() => {
this.handleResize(this.canvas);
this.handleResize(this.canvasAlt);
}).bind(this);
window.addEventListener('resize', this._resizers);
}
render() {
return html` ${karelView({
displayAltCanvas: this.displayAltCanvas,
index: this.index,
indexes: this.indexes,
})}`;
}
<canvas>.clientWidth properly resizes in native code.
So if Lit is flawless, it must be a bug in your code.
Notes:
The connectedCallback will fire multiple times when you move the element in the DOM
no need for oldskool bind() (unless you pass a funtion reference)
No need to declare async handleResize (and it is not the cause of your error)
<my-element></my-element>
<script>
customElements.define('my-element', class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:'open'})
.innerHTML = `<b/><style>canvas{width:50vw}</style><canvas></canvas>`;
}
query(selector){
return this.shadowRoot.querySelector(selector);
}
connectedCallback() {
window.addEventListener("resize", evt => this.handleResize(evt) );
this.handleResize();
}
handleResize(evt) {
let canvas = this.query("canvas");
this.query("b").innerHTML = canvas.clientWidth + "px";
}
})
</script>
Or:
<my-element></my-element>
<script>
customElements.define('my-element', class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:'open'})
.innerHTML = `<b/><style>canvas{width:50vw}</style><canvas></canvas>`;
window.addEventListener("resize", evt => {
this.shadowRoot.querySelector("b").innerHTML =
this.shadowRoot.querySelector("canvas").clientWidth + "px";
});
window.dispatchEvent(new Event("resize"));
}
})
</script>
Related
When I try to set the width and height of the canvas element in the draw function, it seems to be setting them on some canvas, but not the one I'm looking at. Upon inspection of the canvas with the border I defined in the webcomponent, you will see that it still has the standard dimensions instead of being resized.
I'd like to expose the ctx of the component for use outside of it. What am I doing wrong?
class Component extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open' });
const canvasContainer = document.createElement('div');
canvasContainer.classList.add('canvas-container')
canvasContainer.innerHTML = `
<style>
.canvas-container {
width: 100%;
height: 100%;
}
canvas {
border: 1px solid black;
}
</style>
<canvas></canvas>
`;
this.shadow.appendChild(canvasContainer);
}
get ctx () {
return this.shadow.querySelector('canvas').getContext('2d');
}
}
customElements.define('canvas-component', Component);
const component = new Component();
const ctx = component.ctx;
const canvas = component.ctx.canvas;
const draw = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
console.log(canvas);
window.requestAnimationFrame(draw);
}
draw();
html, body {
width: 100%;
height: 100%;
margin: 0;
}
<canvas-component></canvas-component>
repeating comment:
Why const component = new Component(); and not work with the
in the DOM? Those are 2 different objects and only
the latter is visible on your screen (which you do NOT change the size
of)
And you can shorten that code to:
<script>
customElements.define('canvas-component', class Component extends HTMLElement {
constructor() {
super().attachShadow({mode:'open'}).innerHTML = `
<style>canvas { width:100%;height:100%; border:1px solid black }</style>
<canvas></canvas>`;
}
get ctx(){
return this.shadowRoot.querySelector('canvas').getContext('2d');
}
size(w, h) {// also clears the whole canvas
this.ctx.canvas.width = w; this.ctx.canvas.height = h;
}
rect({x=10,y=x,w=20,h=w,l=2,c='red'}){
this.ctx.lineWidth = l;
this.ctx.strokeStyle = c;
this.ctx.strokeRect( x, y, w, h );
}
});
</script>
<canvas-component id="C1"></canvas-component>
<script>
onload = () => {
C1.size(innerWidth, innerHeight);
C1.rect({});
C1.rect({x:80,w:50});
C1.rect({x:50,w:150,c:'green',l:3});
}
</script>
First time working with canvas in react. I am wondering what the best way to provide methods with the canvas context is (like paint below). Should I use Refs? State? Or is there another way. Thanks
Here is what I have for using refs so far:
class Canvas extends React.Component{
canvasRef = React.createRef();
contextRef = React.createRef();
componentDidMount() {
const canvas = this.canvasRef.current;
const context = canvas.getContext("2d");
this.contextRef.current = context;
}
paint = (e) => {
this.contextRef.current.lineTo(e.clientX, e.clientY);
this.contextRef.current.stroke();
};
render() {
return ( <
canvas id = "canvas"
ref = {
this.canvasRef
}
onMouseMove = {
this.paint
}
/>
);
}
}
Refs are correct for getting the canvas, but I'd suggest getting the 2D context each time you need it (getContext just returns the same context as the previous time).
You have a few other minor issues with that code, here's an example putting the context in an instance property, and fixing various issues called out inline.
class Canvas extends React.Component { // *** Note: You need to extend `React.Component`
canvasRef = React.createRef(); // *** Note: You shouldn't have `this.` here
paint = (e) => {
// ** Get the canvas
const canvas = this.canvasRef.current;
if (!canvas) {
return; // This probably won't happen
}
// Get the context
const context = canvas.getContext("2d");
// Get the point to draw at, allowing for the possibility
// that the canvas isn't at the top of the doc
const { left, top } = canvas.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
context.lineTo(x, y);
context.stroke();
}; // *** Added missing ; here
render() {
return <canvas ref={this.canvasRef} onMouseMove={this.paint} />;
}
}
(I removed the id on the canvas, since your component might get used in more than one place.)
In a comment you mentioned issues with state changes causing trouble (I saw that as well when trying to get and keep the context rather than getting it each time), so I've added state both to a parent container element and also to the Canvas element in the following example to check that it continues to work, which it does:
const { useState } = React;
class Canvas extends React.Component { // *** Note: You need to extend `React.Component`
canvasRef = React.createRef(); // *** Note: You shouldn't have `this.` here
state = {
ticker: 0,
};
paint = (e) => {
// ** Get the canvas
const canvas = this.canvasRef.current;
if (!canvas) {
return; // This probably won't happen
}
// Get the context
const context = canvas.getContext("2d");
// Get the point to draw at, allowing for the possibility
// that the canvas isn't at the top of the doc
const { left, top } = canvas.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
context.lineTo(x, y);
context.stroke();
}; // *** Added missing ; here
increment = () => {
this.setState(( { ticker }) => ({ ticker: ticker + 1 }));
};
render() {
const { ticker } = this.state;
return <div>
<canvas ref={this.canvasRef} onMouseMove={this.paint} />
<div onClick={this.increment}>Canvas ticker: {ticker}</div>
</div>;
}
}
class Example extends React.Component {
state = {
ticker: 0,
};
increment = () => {
this.setState(({ ticker }) => ({ ticker: ticker + 1 }));
};
render() {
const { ticker } = this.state;
return <div>
<div onClick={this.increment}>Parent ticker: {ticker}</div>
<Canvas />
</div>;
}
}
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
I am using react-image-marker which overlays marker on image inside a div.
<ImageMarker
src={props.asset.url}
markers={markers}
onAddMarker={(marker) => setMarkers([...markers, marker])}
className="object-fit-contain image-marker__image"
/>
The DOM elements are as follows:
<div class=“image-marker”>
<img src=“src” class=“image-marker__image” />
</div>
To make vertically long images fit the screen i have added css to contain the image within div
.image-marker {
width: inherit;
height: inherit;
}
.image-marker__image {
object-fit:contain !important;
width: inherit;
height: inherit;
}
But now the image is only a subpart of the entire marker area. Due to which marker can be added beyond image bounds, which i do not want.
How do you think i can tackle this. After the image has been loaded, how can i change the width of parent div to make sure they have same size and markers remain in the image bounds. Please specify with code if possible
Solved it by adding event listeners in useLayoutEffect(). Calculating image size info after it is rendered and adjusting the parent div dimensions accordingly. Initially and also when resize occurs.
If you think you have a better solution. Do specify.
useLayoutEffect(() => {
const getRenderedSize = (contains, cWidth, cHeight, width, height, pos) => {
var oRatio = width / height,
cRatio = cWidth / cHeight;
return function () {
if (contains ? (oRatio > cRatio) : (oRatio < cRatio)) {
this.width = cWidth;
this.height = cWidth / oRatio;
} else {
this.width = cHeight * oRatio;
this.height = cHeight;
}
this.left = (cWidth - this.width) * (pos / 100);
this.right = this.width + this.left;
return this;
}.call({});
}
const getImgSizeInfo = (img) => {
var pos = window.getComputedStyle(img).getPropertyValue('object-position').split(' ');
return getRenderedSize(true,
img.width,
img.height,
img.naturalWidth,
img.naturalHeight,
parseInt(pos[0]));
}
const imgDivs = reactDom.findDOMNode(imageCompRef.current).getElementsByClassName("image-marker__image")
if (imgDivs) {
if (imgDivs.length) {
const thisImg = imgDivs[0]
thisImg.addEventListener("load", (evt) => {
if (evt) {
if (evt.target) {
if (evt.target.naturalWidth && evt.target.naturalHeight) {
let renderedImgSizeInfo = getImgSizeInfo(evt.target)
if (renderedImgSizeInfo) {
setOverrideWidth(Math.round(renderedImgSizeInfo.width))
}
}
}
}
})
}
}
function updateSize() {
const thisImg = imgDivs[0]
if (thisImg){
let renderedImgSizeInfo = getImgSizeInfo(thisImg)
if (renderedImgSizeInfo) {
setOverrideWidth((prev) => {
return null
})
setTimeout(() => {
setOverrideWidth((prev) => {
return setOverrideWidth(Math.round(renderedImgSizeInfo.width))
})
}, 390);
}
}
}
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, [])
I build a SPA that should always take 100% of screen height with the target browser group - iOS(mobile) Safari. height:100vh doesn't work properly on iOS Safari -
CSS3 100vh not constant in mobile browser
https://css-tricks.com/css-fix-for-100vh-in-mobile-webkit/
A suggested solution on some resources to use -webkit-fill-available didn't help.
Therefore, I decided to control the height of the app container with JS:
const [height, setHeight] = useState(window.innerHeight);
const handleWindowSizeChange = () => {
setHeight(window.innerHeight);
};
useEffect(() => {
window.addEventListener("resize", handleWindowSizeChange);
return () => window.removeEventListener("resize", handleWindowSizeChange);
}, []);
return (
<div className="App" style={{ height: height }}>
<header className="top"></header>
<div className="main"></div>
<footer className="bottom"><button>Click</button></footer>
</div>
My solution works until we rotate the screen from portrait orientation to landscape, and back to portrait. After these rotations, we have a gap at the bottom of the screen view, right below the app footer. The same behaviour can be noticed if we open
a demo - https://react-div-100vh.vercel.app/ for a package which is supposed to solve the issue, and make the same screen rotations.
Browser: Safari 14.6 iOS/iPhone 7
Repository
Live app
CodeSandbox
Viewport height is tricky on iOS Safari.
Depending on the use case, -webkit-fill-available could work (see article).
I mostly have to use this JavaScript/CSS var fix:
.my-element {
height: 25vh;
height: calc(25 * var(--vh, 1vh));
}
<script type="application/javascript">
function handleResize () { window.document.documentElement.style.setProperty('--vh', `${window.innerHeight * 0.01}px`); }
window.onresize = handleResize
handleResize()
</script>
Inspiration: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
import React from 'react'
const getHeight = () => {
const html = document?.documentElement
return Math.max(window?.innerHeight, html?.clientHeight, html?.offsetHeight)
}
const getWidth = () => {
const html = document?.documentElement
return Math.max(window?.innerWidth, html?.clientWidth, html?.offsetWidth)
}
const isIPad = (platform, maxTouchPoints) =>
maxTouchPoints && maxTouchPoints > 2 && /MacIntel/.test(platform)
const isIPhoneOrIPad = (platform, touchpoints) =>
platform === 'iPhone' ||
/iPad/.test(platform) ||
isIPad(platform, touchpoints)
const isIDevice = isIPhoneOrIPad(
window?.navigator?.platform,
window?.navigator?.maxTouchPoints,
)
const isIOSPWA = window.navigator['standalone'] === true
const MyApp = (props) => {
const rafTimer = React.useRef(null)
const timer = React.useRef(null)
React.useEffect(() => {
if (!isIDevice) return
const iosHack = () => {
const height = getHeight()
const width = getWidth()
document.body.style.minHeight = `${height}px`
document.body.style.maxHeight = `${height}px`
if (!isIOSPWA && height > width) {
clearTimeout(timer.current)
timer.current = setTimeout(() => {
const height = getHeight()
document.body.style.minHeight = `${height}px`
document.body.style.maxHeight = `${height}px`
clearTimeout(timer.current)
timer.current = setTimeout(() => {
const height = getHeight()
document.body.style.minHeight = `${height}px`
document.body.style.maxHeight = `${height}px`
}, 300)
}, 300)
}
}
cancelAnimationFrame(rafTimer.current)
rafTimer.current = requestAnimationFrame(iosHack)
return () => {
cancelAnimationFrame(rafTimer.current)
clearTimeout(timer.current)
}
})
return <div>{props.children}</div>
}
export default MyApp
The solution was simply to use % rather than vh for HTML, Body and #root(in case or react-apps created with create-react-app) elements:
html,
body,
#root {
height: 100%;
}
After this, the content inside the root element takes the entire height of a screen and will adapt when browser tabs appear/disappear. (Tested on iOS Safari 15.3.1 and Chrome iOS v99.0.4844.59)
Credit: The article about CSS Reset from and the course of the same author
I have a relatively simply component which looks as follows (simplified)
class MouseListener extends Component {
static propTypes = {
children: PropTypes.element,
onMouseMove: PropTypes.func.isRequired
};
container = null;
onMouseMove = debounce(e => {
e.persist();
const { clientX, clientY } = e;
const { top, left, height, width } = this.container.getBoundingClientRect();
const x = (clientX - left) / width;
const y = (clientY - top) / height;
this.props.onMouseMove(x, y);
}, 50);
render() {
return (<div
ref={e => this.container = e}
onMouseMove={this.onMouseMove}
>
{this.props.children}
</div>);
}
}
However, whenever the onMouseMove is triggered, it says that the event has already passed. I expected to be able to use e.persist() to keep it in the pool for this event handler to run asynchronously.
What is the appropriate way to bind a debounced event handler in React?