I come to you because I have a problem with this library react-native-color-wheel, I would like to change the wheel so that it becomes a wheel of colors in kelvin. I've tried to tinker with several things but without any effect, I'm really struggling with polar calculations and all that goes with it if someone could point me to it.
Thanks to you guys
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Animated, Image, Dimensions, PanResponder, View } from 'react-native';
import colorsys from 'colorsys';
import styles from './styles';
export default class ColorWheelWhite extends Component {
static propTypes = {
toggleMeToforceUpdateInitialColor: PropTypes.number,
initialColor: PropTypes.string,
onColorChangeComplete: PropTypes.func,
thumbSize: PropTypes.number,
onColorChange: PropTypes.func,
thumbStyle: PropTypes.object,
prevState: PropTypes.string,
style: PropTypes.object
};
static defaultProps = {
thumbSize: 50,
initialColor: '#ffffff',
onColorChange: () => {}
};
static getDerivedStateFromProps(props, state) {
const { toggleMeToforceUpdateInitialColor } = props;
if (toggleMeToforceUpdateInitialColor !== state.toggleMeToforceUpdateInitialColor) {
return {
toggleMeToforceUpdateInitialColor
};
}
return null;
}
constructor(props) {
super(props);
const { initialColor, toggleMeToforceUpdateInitialColor = 0 } = props;
this.state = {
offset: { x: 100, y: 100 },
currentColor: initialColor,
pan: new Animated.ValueXY(),
toggleMeToforceUpdateInitialColor,
radius: 200,
panHandlerReady: true,
didUpdateThumb: false
};
}
componentDidMount = () => {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponderCapture: () => true,
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: ({ nativeEvent }) => {
if (this.outBounds(nativeEvent)) return;
if (!this.state.didUpdateThumb) {
this.updateColorAndThumbPosition(nativeEvent);
}
},
onPanResponderMove: (event, gestureState) => {
if (this.outBounds(gestureState)) return;
if (!this.state.didUpdateThumb) {
const { nativeEvent } = event;
this.updateColorAndThumbPosition(nativeEvent);
}
this.resetPanHandler();
return Animated.event(
[
null,
{
dx: this.state.pan.x,
dy: this.state.pan.y
}
],
{ listener: this.updateColor }
)(event, gestureState);
},
onMoveShouldSetPanResponder: () => true,
onPanResponderRelease: ({ nativeEvent }) => {
this.setState({
panHandlerReady: true,
didUpdateThumb: false
});
this.state.pan.flattenOffset();
const { radius } = this.calcPolar(nativeEvent);
if (radius < 0.1) {
this.forceUpdate('#ffffff');
}
if (this.props.onColorChangeComplete) {
this.props.onColorChangeComplete(this.state.hsv);
}
}
});
};
componentDidUpdate(prevProps, prevState) {
const { initialColor } = this.props;
if (initialColor && this.state.toggleMeToforceUpdateInitialColor !== prevState.toggleMeToforceUpdateInitialColor) {
this.forceUpdate(initialColor);
}
}
updateColorAndThumbPosition(nativeEvent) {
this.updateColor({ nativeEvent });
this.state.pan.setValue({
x: -this.state.left + nativeEvent.pageX - this.props.thumbSize / 2,
y: -this.state.top + nativeEvent.pageY - this.props.thumbSize / 2
});
this.setState({
didUpdateThumb: true
});
}
onLayout = () => {
this.measureOffset();
};
measureOffset() {
/*
* const {x, y, width, height} = nativeEvent.layout
* onlayout values are different than measureInWindow
* x and y are the distances to its previous element
* but in measureInWindow they are relative to the window
*/
this.self.measureInWindow((x, y, width, height) => {
const window = Dimensions.get('window');
const absX = x % width;
const radius = Math.min(width, height) / 2;
const offset = {
x: absX + width / 2,
y: (y % window.height) + height / 2
};
this.setState({
offset,
radius,
height,
width,
top: y % window.height,
left: absX
});
this.forceUpdate(this.state.currentColor);
});
}
calcPolar(gestureState) {
const { pageX, pageY, moveX, moveY } = gestureState;
const [x, y] = [pageX || moveX, pageY || moveY];
const [dx, dy] = [x - this.state.offset.x, y - this.state.offset.y];
return {
deg: Math.atan2(dy, dx) * (-180 / Math.PI),
// pitagoras r^2 = x^2 + y^2 normalized
radius: Math.sqrt(dy * dy + dx * dx) / this.state.radius
};
}
outBounds(gestureState) {
const { radius } = this.calcPolar(gestureState);
return radius > 1;
}
resetPanHandler() {
if (!this.state.panHandlerReady) {
return;
}
this.setState({ panHandlerReady: false });
this.state.pan.setOffset({
x: this.state.pan.x._value,
y: this.state.pan.y._value
});
this.state.pan.setValue({ x: 0, y: 0 });
}
calcCartesian(deg, radius) {
const r = radius * this.state.radius; // was normalized
const rad = (Math.PI * deg) / 180;
const x = r * Math.cos(rad);
const y = r * Math.sin(rad);
return {
left: this.state.width / 2 + x,
top: this.state.height / 2 - y
};
}
updateColor = ({ nativeEvent }) => {
const { deg, radius } = this.calcPolar(nativeEvent);
console.log("deg");
console.log(deg);
console.log("radius");
console.log(radius);
const hsv = { h: deg, s: 10 * radius, v: 100 };
const currentColor = colorsys.hsv2Hex(hsv);
this.setState({ hsv, currentColor });
console.log("LE HSV");
console.log(this.state.hsv);
this.props.onColorChange(hsv);
};
forceUpdate = color => {
const { h, s, v } = colorsys.hex2Hsv(color);
const { left, top } = this.calcCartesian(h, s / 100);
this.setState({ currentColor: color });
this.props.onColorChange({ h, s, v });
this.state.pan.setValue({
x: left - this.props.thumbSize / 2,
y: top - this.props.thumbSize / 2
});
};
animatedUpdate = color => {
const { h, s, v } = colorsys.hex2Hsv(color);
const { left, top } = this.calcCartesian(h, s / 100);
this.setState({ currentColor: color });
this.props.onColorChange({ h, s, v });
Animated.spring(this.state.pan, {
toValue: {
x: left - this.props.thumbSize / 2,
y: top - this.props.thumbSize / 2
}
}).start();
};
setRef = ref => {
this.self = ref;
};
render() {
const { radius } = this.state;
const thumbStyle = [
styles().circle,
this.props.thumbStyle,
{
width: this.props.thumbSize,
height: this.props.thumbSize,
borderRadius: this.props.thumbSize / 2,
backgroundColor: this.state.currentColor,
opacity: this.state.offset.x === 0 ? 0 : 1
}
];
const panHandlers = (this._panResponder && this._panResponder.panHandlers) || {};
return (
<View ref={this.setRef} onLayout={this.onLayout} style={[styles().coverResponder, this.props.style]} pointerEvents={'box-none'}>
<Image
style={[
styles().img,
{
height: radius * 2,
width: radius * 2,
borderRadius: radius - this.props.thumbSize
}
]}
source={require('../../assets/color-wheel-white.png')}
{...panHandlers}
/>
<Animated.View style={[this.state.pan.getLayout(), thumbStyle]} pointerEvents={'box-none'} />
</View>
);
}
}
Related
I have been trying to create a little animation on canvas using React.js.
I expect the red block inside the canvas to go up and drop back down when I hit my spacebar by decreasing the velocity so that it will execute the if statement I have got and pull it back down, but so far it's not working. I can see the velocity value did change and it did execute the if statement when I console.log, it doesn't seem like showing on the animation.
const { useRef, useEffect, useState } = React;
const Canvas = () => {
const { innerWidth: innerwidth, innerHeight: innerheight } = window;
const contextRef = useRef()
const [position, setPosition] = useState({ x: 100, y: 100 })
const [size, setSize] = useState({ width: 30, height: 30 })
const [velocity, setVelocity] = useState({ x: 0, y: 0 })
const gravity = 4.5
const draw = (context) => {
context.fillStyle = 'red'
context.fillRect(position.x, position.y, size.width, size.height);
};
const update = (context, canvas) => {
draw(context)
setPosition({ x:position.x, y:position.y += velocity.y })
if (position.y + size.height +velocity.y <= canvas.height) {
setVelocity({ x:velocity.x, y: velocity.y += gravity })
} else {
setVelocity({ x:velocity.x, y: velocity.y = 0 })
}
}
const animate = (context, width, height, canvas) => {
requestAnimationFrame(() => {
animate(context, width, height, canvas )
})
context.clearRect(0, 0, width, height)
update(context, canvas)
}
useEffect(() => {
const canvas = contextRef.current;
const context = canvas.getContext("2d");
canvas.width = innerwidth - 2
canvas.height = innerheight - 2
animate(context, canvas.width, canvas.height, canvas)
}, []);
const handleKeydown = (e) => {
switch(e.keyCode) {
case 37:
console.log(velocity)
return "left";
case 39:
return "right";
case 32:
setVelocity({ x:velocity.x, y:velocity.y -= 50 });
console.log(velocity)
break
default:
console.log("keyCode is " + e.keyCode)
return 'default';
}
}
return (
<canvas ref={contextRef} tabIndex={-1} onKeyDown={(e) => {
handleKeydown(e)
}} />
);
};
const App = () => <Canvas />;
ReactDOM
.createRoot(document.getElementById("root"))
.render(<App />);
html, body { width: 100%; height: 100%; margin: 0; }
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
What can I try next?
You should wrap some of your function in a useCallback so that they are not redefined on each render.
Also, I would use key instead of keyCode.
const { useCallback, useEffect, useState, useRef } = React;
const gravity = 4.5;
const Canvas = () => {
const { innerWidth: innerwidth, innerHeight: innerheight } = window;
const canvasRef = useRef();
const [position, setPosition] = useState({ x: 100, y: 100 });
const [size, setSize] = useState({ width: 30, height: 30 });
const [velocity, setVelocity] = useState({ x: 0, y: 0 });
const draw = useCallback((context) => {
context.fillStyle = 'red';
context.fillRect(position.x, position.y, size.width, size.height);
}, [position, size]);
const update = useCallback((context, canvas) => {
draw(context)
setPosition({ x: position.x, y: position.y += velocity.y })
if (position.y + size.height + velocity.y <= canvas.height) {
setVelocity({ x: velocity.x, y: velocity.y += gravity })
} else {
setVelocity({ x:velocity.x, y: velocity.y = 0 })
}
}, [position, size, velocity]);
const animate = (context, width, height, canvas) => {
requestAnimationFrame(() => {
animate(context, width, height, canvas);
});
context.clearRect(0, 0, width, height);
update(context, canvas);
};
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
canvas.width= innerwidth - 10;
canvas.height= innerheight - 10;
animate(context, canvas.width, canvas.height, canvas)
}, []);
const handleKeyDown = useCallback(({ key }) => {
switch (key) {
case 'ArrowLeft':
break;
case 'ArrowRight':
break;
case ' ': // Spacebar
setVelocity({ x: velocity.x, y: velocity.y -= 50 });
console.log(velocity)
break
default:
console.log(`Unknown key: ${key}`);
}
}, []);
return (
<canvas
ref={canvasRef}
tabIndex={-1}
onKeyDown={handleKeyDown}
/>
);
};
const App = () => <Canvas />;
ReactDOM
.createRoot(document.getElementById("root"))
.render(<App />);
html, body, #root { width: 100%; height: 100%; margin: 0; }
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Hello everyone we are building an app for a company. My task is to create an image crop feature for profile pictures. I am using the react-easy-crop library. I have implemented nearly all the features the only missing thing is zooming in to a certain pixel. Our company doesn't want users to be able to zoom in less than 600 X 600 pixels. What I meany by that is when users zoom in the image size can't be lower than 600 x 600 pixels. here is my code structure (Be aware this not the full code I have reduced it for complexity issues)
import React, { useEffect, useRef, useState, FC, useCallback, SetStateAction } from 'react';
import Cropper from 'react-easy-crop';
import Resizer from 'react-image-file-resizer';
import { Text, Button, DragAndDrop } from '#shared/components';
import { Modal } from '#shared/components/Modal/lazy';
import { getOrientation } from 'get-orientation';
import { COLOR_NAMES } from '#src/settings/colors.constants';
import { Icon } from '#src/shared/components/icon';
import { Centered } from '#src/shared/components/layout/centered';
import { useDispatch } from 'react-redux';
import { Flex } from 'rebass';
import { Dispatch } from 'redux';
import { ZoomAndApplyContainer, CropContainer } from '#app/shared/components/crop-image/styled';
import { FileValue } from '#shared/components/upload-single-document/interfaces';
import { UploadSingleImageProps } from '#app/shared/components/upload-single-image/interfaces';
import { CoverPicEditComponent, ImageUploadContainer, PicEditComponent } from '#app/shared/components/upload-single-image/styled';
import { MODAL_NAMES, MODAL_TYPES } from '#app/shared/store/constants/modal.constants';
import { ShowModalAC, HideModalAC } from '#app/shared/store/actions/modal.actions';
import { NumberOfBytesInOneMB, TOASTER_APPEARANCES } from '#app/shared/constants';
import { SetToastsActionCreator } from '#app/shared/store/actions/toast.actions';
import { validateFileType } from '#app/utils/validations';
import { PRIMARY } from '../button/button.constants';
import { FormikFieldErrorMessage } from '../formik-field/styled';
const readFile: any = (file: any) =>
new Promise((resolve: any) => {
const reader: any = new FileReader();
reader.addEventListener('load', () => resolve(reader.result), false);
reader.readAsDataURL(file);
});
const createImage: any = (url: any) =>
new Promise((resolve: any, reject: any) => {
const image: any = new Image();
image.addEventListener('load', () => resolve(image));
image.addEventListener('error', (error: any) => reject(error));
image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox
image.src = url;
});
const getRadianAngle: any = (degreeValue: any) => (degreeValue * Math.PI) / 180;
const ORIENTATION_TO_ANGLE: any = {
3: 180,
6: 90,
8: -90,
};
/**
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
* #param {File} image - Image File url
* #param {Object} pixelCrop - pixelCrop Object provided by react-easy-crop
* #param {number} rotation - optional rotation parameter
*/
const getCroppedImg: any = async (imageSrc: any, pixelCrop: any, rotation: any = 0) => {
const image: any = await createImage(imageSrc);
const canvas: any = document.createElement('canvas');
const ctx: any = canvas.getContext('2d');
const maxSize: any = Math.max(image.width, image.height);
const safeArea: any = 2 * ((maxSize / 2) * Math.sqrt(2));
// set each dimensions to double largest dimension to allow for a safe area for the
// image to rotate in without being clipped by canvas context
canvas.width = safeArea;
canvas.height = safeArea;
// translate canvas context to a central location on image to allow rotating around the center.
ctx.translate(safeArea / 2, safeArea / 2);
ctx.rotate(getRadianAngle(rotation));
ctx.translate(-safeArea / 2, -safeArea / 2);
// draw rotated image and store data.
ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5);
const data: any = ctx.getImageData(0, 0, safeArea, safeArea);
// set canvas width to final desired crop size - this will clear existing context
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
// paste generated rotate image with correct offsets for x,y crop values.
ctx.putImageData(data, Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y));
// As Base64 string
// return canvas.toDataURL('image/jpeg');
// As a blob
return new Promise((resolve: any) => {
canvas.toBlob((file: any) => {
resolve(file);
}, 'image/jpeg');
});
};
const getRotatedImage: any = async (imageSrc: any, rotation: number = 0) => {
const image: any = await createImage(imageSrc);
const canvas: any = document.createElement('canvas');
const ctx: any = canvas.getContext('2d');
const orientationChanged: boolean = rotation === 90 || rotation === -90 || rotation === 270 || rotation === -270;
if (orientationChanged) {
canvas.width = image.height;
canvas.height = image.width;
} else {
canvas.width = image.width;
canvas.height = image.height;
}
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate((rotation * Math.PI) / 180);
ctx.drawImage(image, -image.width / 2, -image.height / 2);
return new Promise((resolve: any) => {
canvas.toBlob((file: any) => {
resolve(URL.createObjectURL(file));
}, 'image/jpeg');
});
};
export const UploadSingleImage: FC<UploadSingleImageProps> = ({
setFieldValue,
setFieldTouched,
name,
extensionName,
width = '600',
height = '600',
errorMessage,
isDisabled,
fileId,
extension,
title,
validationRules,
isCoverPhoto,
customContainerCss,
onChange,
editIconName = 'edit',
onUploaded,
}: UploadSingleImageProps) => {
const [value, setValue]: [FileValue, React.Dispatch<SetStateAction<FileValue>>] = useState(null);
const [imgSrc, setImgSrc]: any = useState(null);
const [maxZoom, setMaxZoom]: any = useState(1);
const [rotation, setRotation]: any = useState(0);
const [crop, setCrop]: any = useState({ x: 0, y: 0 });
const [imageSendingFail, setImageSendingFail]: [boolean, React.Dispatch<SetStateAction<boolean>>] = useState(true);
const [zoom, setZoom]: any = useState(1);
const [croppedAreaPixels, setCroppedAreaPixels]: any = useState(null);
const showCroppedImage: any = useCallback(async () => {
try {
const cropedImage: any = await getCroppedImg(imgSrc, croppedAreaPixels, rotation);
Resizer.imageFileResizer(
cropedImage,
600,
600,
'JPEG',
100,
0,
(file: any) => {
onChange(file, setValue);
dispatch(HideModalAC(MODAL_NAMES.IMAGE_CROP_MODAL));
setImgSrc(null);
},
'blob'
);
} catch (e) {
console.error(e);
}
}, [imgSrc, croppedAreaPixels, rotation]);
const imageInput: React.MutableRefObject<HTMLInputElement> = useRef();
const dispatch: Dispatch = useDispatch();
const toast: any = useCallback((toasts: any) => dispatch(SetToastsActionCreator(toasts)), [dispatch]);
const onCropComplete: any = useCallback((croppedArea: any, croppedAreaPixel: any) => {
setCroppedAreaPixels(croppedAreaPixel);
}, []);
const handleFileDrop: any = (e: any) => {
const files: any = e.dataTransfer.files;
if (files && files.length === 1) {
validateImage(files[0]);
}
};
const onFileChange: any = async (e: any) => {
if (e.target.files && e.target.files.length === 1) {
const file: any = e.target.files[0];
validateImage(file);
}
};
const onClick: any = (e: any) => {
setZoom(1);
e.target.value = '';
};
const validateImage: (file: File) => void = async (file: any) => {
setImageSendingFail(false);
// const imageDataUrl: any = await readFile(file);
// setImgSrc(imageDataUrl);
if (setFieldTouched) {
setFieldTouched(name);
}
if (validateFileType(toast, validationRules?.fileTypes, file)) {
let imageDataUrl: any = await readFile(file);
const img: any = createImage(imageDataUrl);
if (!validationRules || validateImg(toast, validationRules, img, file)) {
const orientation: any = await getOrientation(file);
const rotationPortion: any = ORIENTATION_TO_ANGLE[orientation];
if (rotation) {
imageDataUrl = await getRotatedImage(imageDataUrl, rotationPortion);
}
setImgSrc(imageDataUrl);
dispatch(ShowModalAC(MODAL_NAMES.IMAGE_CROP_MODAL));
} else {
imageInput.current.value = '';
setImageSendingFail(true);
}
}
};
useEffect(() => {
if (fileId && extension) {
setValue({ fileId, extension });
}
}, [fileId, extension]);
useEffect(() => {
if (setFieldValue) {
setFieldValue(name, value?.fileId);
setFieldValue(extensionName, value?.extension);
}
if (onUploaded && value?.fileId) {
onUploaded(name, value);
}
}, [value?.fileId, value?.extension]);
return (
<Flex justifyContent={'center'} alignItems={'center'} flexDirection={'column'} css={{ height: '100%' }} name={name}>
{imgSrc && (
<Modal bodyCss={{ padding: 0 }} bodyHeight='90%' width={'70%'} height={'85%'} borderRadius={'4px'} modalId={MODAL_NAMES.IMAGE_CROP_MODAL} headingText={'Resmi Düzenle'} type={MODAL_TYPES.NORMAL}>
<CropContainer>
<div style={{ width: '80%', height: '70%', position: 'relative' }}>
<Cropper
image={imgSrc}
crop={crop}
zoom={zoom}
rotation={rotation}
aspect={1 / 1}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
restrictPosition={false}
onRotationChange={setRotation}
minZoom={0.5}
maxZoom={maxZoom}
onMediaLoaded={(imageSize: any) => {
if (imageSize.naturalWidth > 600) {
setMaxZoom(600 / Math.max(imageSize.height, imageSize.width));
} else {
setMaxZoom(1);
}
console.log(imageSize);
}}
/>
</div>
<ZoomAndApplyContainer>
<input type='range' value={zoom} min={0.5} max={maxZoom} step={0.05} onChange={(e: any) => setZoom(e.target.value)} />
<input type='range' value={rotation} min={0} max={360} step={10} onChange={(e: any) => setRotation(e.target.value)} />
<Button button={PRIMARY} onClick={showCroppedImage}>
Upload
</Button>
</ZoomAndApplyContainer>
</CropContainer>
</Modal>
)}
<DragAndDrop handleDrop={handleFileDrop}>
<ImageUploadContainer customContainerCss={customContainerCss} url={fileId && extension && `${fileId}${extension}`} width={width} height={height} errorMessage={errorMessage}>
{value?.fileId && value?.extension && isCoverPhoto && !isDisabled && (
<CoverPicEditComponent>
<label htmlFor={name}>
<Icon margin={'auto'} name={'upload-white'} />
<Text color={COLOR_NAMES.REMAX_WHITE} customWeight={1}>
Yeni kapak fotoğrafı yükle
</Text>
</label>
</CoverPicEditComponent>
)}
{value?.fileId && value?.extension && !isCoverPhoto && !isDisabled && (
<PicEditComponent className='edit-icon-container' htmlFor={name}>
<Icon name={editIconName} />
</PicEditComponent>
)}
{!(value?.fileId && value?.extension) && (
<Centered css={{ flexDirection: 'column' }}>
<Text customSize={[2, 3, 3]} lineHeight={1} customWeight={1} color={COLOR_NAMES.REMAX_TEXT_GREY_7f7f7f} css={{ textAlign: 'center', width: '145px', paddingBottom: '12px' }}>
{title}
</Text>
<label htmlFor={name}>
<Text customSize={[2, 3, 3]} customWeight={1} color={COLOR_NAMES.REMAX_BLUE_SELECTED_1451EF} textDecoration={'underline'}>
Dosya Seç
</Text>
</label>
</Centered>
)}
<input id={name} ref={imageInput} name={name} type='file' onChange={onFileChange} onClick={onClick} />
</ImageUploadContainer>
</DragAndDrop>
{/* Eğer resim yok ve upload işlemi fail olduysa hata mesajı basılsın */}
{!value?.fileId && imageSendingFail && errorMessage && <FormikFieldErrorMessage elipsis={true}>{errorMessage}</FormikFieldErrorMessage>}
</Flex>
);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
here is my solution for the problem: this callback inside the cropper
onMediaLoaded={(imageSize: any) => {
if (imageSize.naturalWidth > 600) {
setMaxZoom(600 / Math.max(imageSize.height, imageSize.width));
} else {
setMaxZoom(1);
}
}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
but this code doesn't ensure that I have a max zoom restriction that produces 600 x 600 in the end. (It works for some images but some others not. I would really appreciate any suggestion thanks in advance.
const onCropComplete: any = useCallback((croppedArea: any, croppedAreaPixel: any) => {
if(croppedAreaPixel.width < requiredWidth || croppedAreaPixel.height< requiredHeight){
/** do something here to disallow zoom
* this is a workaround
*/
}
setCroppedAreaPixels(croppedAreaPixel);
}, []);
(1) In deck.gl, realize the visual effect after overlaying the two layers. The effect now achieved is that TripsLayer can be automatically rendered because of the addition of animate, but ScatterplotLayer can only achieve the animation effect by dragging the timeline progress bar below, as shown below. That is, after superposition, they now appear to be two unrelated layers. In ScatterplotLayer (with DataFilterExtension added), the radius of the circle is still controlled by the progress bar at the bottom of the screen. Drag to change the size instead of auto rendering similar to TripsLayer.
I want them to achieve simultaneous rendering based on the common attribute timestamp. In other words, when the trajectory of TripsLayer moves, the circle radius of ScatterplotLayer will also change accordingly. I also added animate in ScatterplotLayer before, but it has no effect, it may be that Setstate is not adjusted correctly. Thank you very much for your help ~~
Screenshot of the effect now achieved and the complete code is as follows:
import React, {Component, Fragment} from 'react';
import {render} from 'react-dom';
import {StaticMap} from 'react-map-gl';
import {AmbientLight, PointLight, LightingEffect} from '#deck.gl/core';
import DeckGL from '#deck.gl/react';
import {PolygonLayer} from '#deck.gl/layers';
import {TripsLayer} from '#deck.gl/geo-layers';
import {ScatterplotLayer} from '#deck.gl/layers';
import {DataFilterExtension} from '#deck.gl/extensions';
import {MapView} from '#deck.gl/core';
import RangeInput from './range-input';
// Set your mapbox token here
const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line
// Source data CSV
const DATA_URL1 = {
TRIPS:
'./package1.json' // eslint-disable-line
};
const DATA_URL =
'./data2.csv'; // eslint-disable-line
const MAP_VIEW = new MapView({
// 1 is the distance between the camera and the ground
farZMultiplier: 100
});
const ambientLight = new AmbientLight({
color: [122, 122, 122],
intensity: 1.0
});
const pointLight = new PointLight({
color: [255, 255, 255],
intensity: 2.0,
position: [127.05, 37.5, 8000]
});
const lightingEffect = new LightingEffect({ambientLight, pointLight});
const material = {
ambient: 0.1,
diffuse: 0.6,
shininess: 32,
specularColor: [60, 64, 70]
};
const DEFAULT_THEME = {
buildingColor: [74, 80, 87],
trailColor0: [253, 128, 93],
trailColor1: [23, 184, 190],
material,
effects: [lightingEffect]
};
const INITIAL_VIEW_STATE = {
longitude: 126.9779692,
latitude: 37.566535,
zoom: 6,
pitch: 0,
bearing: 0
};
const landCover = [[[-74.0, 40.7], [-74.02, 40.7], [-74.02, 40.72], [-74.0, 40.72]]];
const MS_PER_DAY = 8.64e7; // milliseconds in a day
const dataFilter = new DataFilterExtension({filterSize: 1});
export default class App extends Component {
constructor(props) {
super(props);
this.state1 = {
time: 0
};
const timeRange = this._getTimeRange(props.data);
this.state = {
timeRange,
filterValue: timeRange,
hoveredObject: null,
};
this._onHover = this._onHover.bind(this);
this._renderTooltip = this._renderTooltip.bind(this);
}
componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
const timeRange = this._getTimeRange(nextProps.data);
this.setState({timeRange, filterValue: timeRange});
}
}
componentDidMount() {
this._animate();
}
componentWillUnmount() {
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
}
}
_animate() {
const {
loopLength = 1000, // unit corresponds to the timestamp in source data
animationSpeed = 20 // unit time per second
} = this.props;
const timestamp = Date.now() / 1000;
const loopTime = loopLength / animationSpeed;
this.setState({
time: ((timestamp % loopTime) / loopTime) * loopLength
});
this._animationFrame = window.requestAnimationFrame(this._animate.bind(this));
}
_getTimeRange(data) {
if (!data) {
return null;
}
return data.reduce(
(range, d) => {
const t = d.timestamp / MS_PER_DAY;
range[0] = Math.min(range[0], t);
range[1] = Math.max(range[1], t);
return range;
},
[Infinity, -Infinity]
);
}
_onHover({x, y, object}) {
this.setState({x, y, hoveredObject: object});
}
_renderLayers() {
const {
buildings = DATA_URL1.BUILDINGS,
trips = DATA_URL1.TRIPS,
trailLength = 30,
theme = DEFAULT_THEME
} = this.props;
const {data} = this.props;
const {filterValue} = this.state;
return [
data &&
new ScatterplotLayer({
id: 'earthquakes',
data,
opacity: 0.8,
radiusScale: 1,
radiusMinPixels: 1,
wrapLongitude: true,
getPosition: d => [d.longitude, d.latitude, -d.depth * 1000],
getRadius: d => d.VisitingTime * 200,
getFillColor: d => {
const r = Math.sqrt(Math.max(d.depth, 0));
return [255 - r * 15, r * 5, r * 10];
},
getFilterValue: d => d.timestamp / MS_PER_DAY, // in days
filterRange: [filterValue[0], filterValue[1]],
filterSoftRange: [
filterValue[0] * 0.9 + filterValue[1] * 0.1,
filterValue[0] * 0.1 + filterValue[1] * 0.9
],
extensions: [dataFilter],
pickable: true,
onHover: this._onHover
}),
new PolygonLayer({
id: 'ground',
data: landCover,
getPolygon: f => f,
stroked: false,
getFillColor: [0, 0, 0, 0]
}),
new TripsLayer({
id: 'trips',
data: trips,
getPath: d => d.path,
getTimestamps: d => d.timestamps,
getColor: d => (d.vendor === 0 ? theme.trailColor0 : theme.trailColor1),
opacity: 0.3,
widthMinPixels: 2,
rounded: true,
trailLength,
currentTime: this.state.time,
shadowEnabled: false
}),
new PolygonLayer({
id: 'buildings',
data: buildings,
extruded: true,
wireframe: false,
opacity: 0.5,
getPolygon: f => f.polygon,
getElevation: f => f.height,
getFillColor: theme.buildingColor,
material: theme.material
})
];
}
_renderTooltip() {
const {x, y, hoveredObject} = this.state;
return (
hoveredObject && (
<div className="tooltip" style={{top: y, left: x}}>
<div>
<b>Time: </b>
<span>{new Date(hoveredObject.timestamp).toUTCString()}</span>
</div>
<div>
<b>VisitingTime: </b>
<span>{hoveredObject.VisitingTime}</span>
</div>
<div>
<b>Depth: </b>
<span>{hoveredObject.depth} km</span>
</div>
</div>
)
);
}
_formatLabel(t) {
const date = new Date(t * MS_PER_DAY);
return `${date.getUTCFullYear()}/${date.getUTCMonth() + 1}`;
}
render() {
const {
viewState,
mapStyle = 'mapbox://styles/mapbox/light-v9',
theme = DEFAULT_THEME
} = this.props;
const {timeRange, filterValue} = this.state;
return (
<Fragment>
<DeckGL
views={MAP_VIEW}
layers={this._renderLayers()}
effects={theme.effects}
initialViewState={INITIAL_VIEW_STATE}
viewState={viewState}
controller={true}
>
<StaticMap
reuseMaps
mapStyle={mapStyle}
preventStyleDiffing={true}
mapboxApiAccessToken={MAPBOX_TOKEN}
/>
{this._renderTooltip}
</DeckGL>
{timeRange && (
<RangeInput
min={timeRange[0]}
max={timeRange[1]}
value={filterValue}
formatLabel={this._formatLabel}
onChange={({value}) => this.setState({filterValue: value})}
/>
)}
</Fragment>
);
}
}
export function renderToDOM(container) {
require('d3-request').csv(DATA_URL, (error, response) => {
if (!error) {
const data = response.map(row => ({
timestamp: new Date(`${row.DateTime} UTC`).getTime(),
latitude: Number(row.Latitude),
longitude: Number(row.Longitude),
depth: Number(row.Depth),
VisitingTime: Number(row.VisitingTime)
}));
render(<App data={data} />, container);
}
});
}
enter image description here
(2) The following code and screenshots are that I put setstate in setinterval and added a timer to implement a method called tick (). The timer component will call it every second. Did not move. You can see in the browser ’s developer tools that time is updated in real time, but filterValue is not updated (filterValue: Called to retrieve the value for each object that it will be filtered by. Returns either a number (if filterSize: 1) or an array. Here is filter by timestamp). There may be a small step that is not set correctly. Thank you very much for your advice ~
import React, {Component, Fragment} from 'react';
import {render} from 'react-dom';
import {StaticMap} from 'react-map-gl';
import {AmbientLight, PointLight, LightingEffect} from '#deck.gl/core';
import DeckGL from '#deck.gl/react';
import {PolygonLayer} from '#deck.gl/layers';
import {TripsLayer} from '#deck.gl/geo-layers';
import {ScatterplotLayer} from '#deck.gl/layers';
import {DataFilterExtension} from '#deck.gl/extensions';
import {MapView} from '#deck.gl/core';
// Set your mapbox token here
const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line
// Source data CSV
const DATA_URL1 = {
TRIPS:
'./package1.json' // eslint-disable-line
};
const DATA_URL =
'./data2.csv'; // eslint-disable-line
const MAP_VIEW = new MapView({
// 1 is the distance between the camera and the ground
farZMultiplier: 100
});
const ambientLight = new AmbientLight({
color: [122, 122, 122],
intensity: 1.0
});
const pointLight = new PointLight({
color: [255, 255, 255],
intensity: 2.0,
position: [127.05, 37.5, 8000]
});
const lightingEffect = new LightingEffect({ambientLight, pointLight});
const material = {
ambient: 0.1,
diffuse: 0.6,
shininess: 32,
specularColor: [60, 64, 70]
};
const DEFAULT_THEME = {
buildingColor: [74, 80, 87],
trailColor0: [253, 128, 93],
trailColor1: [23, 184, 190],
material,
effects: [lightingEffect]
};
const INITIAL_VIEW_STATE = {
longitude: 126.9779692,
latitude: 37.566535,
zoom: 6,
pitch: 0,
bearing: 0
};
const landCover = [[[-74.0, 40.7], [-74.02, 40.7], [-74.02, 40.72], [-74.0, 40.72]]];
const MS_PER_DAY = 8.64e7; // milliseconds in a day
const dataFilter = new DataFilterExtension({filterSize: 1});
export default class App extends Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
const timeRange = this._getTimeRange(props.data);
this.state = {
timeRange,
filterValue: timeRange,
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
const timeRange = this._getTimeRange(nextProps.data);
this.setState({timeRange, filterValue: timeRange});
}
}
componentDidMount() {
this._animate();
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
clearInterval(this.timerID);
}
}
tick() {
this.setState({
date: new Date()
});
}
_animate() {
const {
loopLength = 1000, // unit corresponds to the timestamp in source data
animationSpeed = 20 // unit time per second
} = this.props;
const timestamp = Date.now() / 1000;
const loopTime = loopLength / animationSpeed;
this.setState({
time: ((timestamp % loopTime) / loopTime) * loopLength
});
this._animationFrame = window.requestAnimationFrame(this._animate.bind(this));
}
_getTimeRange(data) {
if (!data) {
return null;
}
return data.reduce(
(range, d) => {
const t = d.timestamp / MS_PER_DAY;
range[0] = Math.min(range[0], t);
range[1] = Math.max(range[1], t);
return range;
},
[Infinity, -Infinity]
);
}
_renderLayers() {
const {
buildings = DATA_URL1.BUILDINGS,
trips = DATA_URL1.TRIPS,
trailLength = 30,
theme = DEFAULT_THEME
} = this.props;
const {data} = this.props;
const {filterValue} = this.state;
return [
data &&
new ScatterplotLayer({
id: 'ScatterplotLayer',
data,
opacity: 0.8,
radiusScale: 1,
radiusMinPixels: 1,
wrapLongitude: true,
rounded: true,
getTimestamps: d => d.timestamps,
getPosition: d => [d.longitude, d.latitude],
getRadius: d => d.VisitingTime * 200,
getFillColor: d => {
const r = Math.sqrt(Math.max(d.depth, 0));
return [255 - r * 15, r * 5, r * 10];
},
getFilterValue: d => d.timestamp / MS_PER_DAY, // in days
currentTime: this.state.time,
filterRange: [filterValue[0], filterValue[1]],
filterSoftRange: [
filterValue[0] * 0.9 + filterValue[1] * 0.1,
filterValue[0] * 0.1 + filterValue[1] * 0.9
],
extensions: [dataFilter]
}),
new PolygonLayer({
id: 'ground',
data: landCover,
getPolygon: f => f,
stroked: false,
getFillColor: [0, 0, 0, 0]
}),
new TripsLayer({
id: 'trips',
data: trips,
getPath: d => d.path,
getTimestamps: d => d.timestamps,
getColor: d => (d.vendor === 0 ? theme.trailColor0 : theme.trailColor1),
opacity: 0.3,
widthMinPixels: 2,
rounded: true,
trailLength,
currentTime: this.state.time,
shadowEnabled: false
}),
new PolygonLayer({
id: 'buildings',
data: buildings,
extruded: true,
wireframe: false,
opacity: 0.5,
getPolygon: f => f.polygon,
getElevation: f => f.height,
getFillColor: theme.buildingColor,
material: theme.material
})
];
}
render() {
const {
viewState,
mapStyle = 'mapbox://styles/mapbox/light-v9',
theme = DEFAULT_THEME
} = this.props;
return (
<Fragment>
<DeckGL
views={MAP_VIEW}
layers={this._renderLayers()}
effects={theme.effects}
initialViewState={INITIAL_VIEW_STATE}
viewState={viewState}
controller={true}
>
<StaticMap
reuseMaps
mapStyle={mapStyle}
preventStyleDiffing={true}
mapboxApiAccessToken={MAPBOX_TOKEN}
/>
</DeckGL>
</Fragment>
);
}
}
export function renderToDOM(container) {
require('d3-request').csv(DATA_URL, (error, response) => {
if (!error) {
const data = response.map(row => ({
timestamp: new Date(`${row.DateTime} UTC`).getTime(),
latitude: Number(row.Latitude),
longitude: Number(row.Longitude),
depth: Number(row.Depth),
VisitingTime: Number(row.VisitingTime)
}));
render(<App data={data} />, container);
}
});
}
enter image description here
I have the following component:
class Ball extends React.Component {
constructor(props) {
super(props);
this.state = { frame: 0, position: this.positionBall(0) };
this.nextFrame();
}
nextFrame() {
this.setState(prevState => ({
frame: prevState.frame + 1,
position: this.positionBall(prevState.frame + 1)
}));
requestAnimationFrame(() => this.nextFrame());
}
render() {
return (
<svg style={{ width: "100%", height: "100%" }}>
<circle
cx={this.state.position}
cy={this.height() / 2}
r={this.radius()}
/>
</svg>
);
}
height() {
return document.documentElement.clientHeight;
}
width() {
return document.documentElement.clientWidth;
}
radius() {
return this.height() / 10;
}
positionBall(frame) {
const maximumPosition = this.width() - 2 * this.radius();
const maximumFrame = 120;
const slope = maximumPosition / maximumFrame;
const position = Math.round(
maximumPosition +
this.radius() -
1 * Math.abs(frame % (maximumFrame * 2) - maximumFrame) * slope
);
return position;
}
}
It is a very simple animation, but it doesn't run entire smooth all the time, especially on wide screens. How can I improve this code?
See this codesandbox:
https://codesandbox.io/s/22kzjy21n
I have a card game where the user can drag cards around a screen.
How can I detect if the cards have been dragged over other View components? My draggable card code is like this:
// A draggable card
// drag drop code example used: https://github.com/brentvatne/react-native-animated-demo-tinder
// panresponder docs: https://facebook.github.io/react-native/docs/panresponder.html
class Card extends React.Component {
componentWillMount() {
this.state = {
pan: new Animated.ValueXY(),
enter: new Animated.Value(1),
}
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (e, gestureState) => {
Animated.spring(this.state.enter, {
toValue: .75,
}).start()
this.state.pan.setOffset({x: this.state.pan.x._value,
y: this.state.pan.y._value});
this.state.pan.setValue({x: 0, y: 0});
},
onPanResponderMove: Animated.event([
null, {dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: (e, {vx, vy}) => {
// do stuff when card released
}
Animated.spring(this.state.enter, {
toValue: 1,
}).start()
this.state.pan.flattenOffset();
var velocity;
if (vx >= 0) {
velocity = clamp(vx, 3, 5);
} else if (vx < 0) {
velocity = clamp(vx * -1, 3, 5) * -1;
}
Animated.spring(this.state.pan, {
toValue: {x: 0, y: toValue},
friction: 4
}).start()
}
})
}
I don't know if this would be the best way to go about it, but I would just get the onLayout of the target Views to get the x,y,width, and height...
<View onLayout={({nativeEvent: {layout: {x,y,width,height}}) => { })} />
The coordinates covered by the container would simply be (x, x+width) and (y, y+height)
onPanResponderGrant get the location of the draggable:
onPanResponderGrant: (e) => {
this.originX = e.pageX - e.locationX;
this.originY = e.pageY - e.locationY;
}
onPanResponderMove you can do something like this:
onPanResponderMove: (e, gestureState) => {
const currentX0 = this.originX + gestureState.dx
const currentY0 = this.originY + gestureState.dy
}
The draggable now covers from currentX0 to currentX0 + width and from currentY0 to currentY0 + height...
You can use these values to check to see whether it overlaps with the target view (x, x+width) and (y, y+height)