I have troubles connecting an audio element with an analyser in Safari. Safari expects the user to interact with the browser before allowing an audio context to start, which is needed to connect an analyser with the audio element. For that I wait for a click event to connect, but the click on the audio element itself is not bubbled up to the body so that it cannot be caught. Any ideas what could be missing?
Here's some code:
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
getAudioElement,
loadAudio,
getNextTrack,
} from '../../helpers/common/audio';
import { isTouch } from '../../helpers/common/screen';
import { captureKeys } from '../../helpers/common/control';
import './Player.css';
const drawAnalyser = (canvas, analyser, mode = 'frequency') => {
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
canvas.width = bufferLength;
canvas.height = 256;
const ctx = canvas.getContext('2d');
const barWidth = 1;
const barDistance = 1.375;
const paintCanvas = () => {
if (mode === 'waveform') {
analyser.getByteTimeDomainData(dataArray);
} else {
analyser.getByteFrequencyData(dataArray);
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
dataArray.forEach((item, index) => {
const barHeight = item * 3 / 4;
if (mode === 'waveform') {
ctx.fillStyle = 'rgba(255, 170, 0, 1)';
ctx.fillRect(index, canvas.height - barHeight, barWidth, 1);
} else {
ctx.fillStyle = `rgba(255, ${255 - item}, 0, ${1 - (item / canvas.height)})`;
ctx.fillRect(index * barDistance, canvas.height, barWidth, 0 - barHeight);
}
});
window.requestAnimationFrame(paintCanvas);
};
paintCanvas();
};
const connect = (audio, canvas) => {
try {
const AudioContext = window.AudioContext;
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
const source = audioContext.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioContext.destination);
drawAnalyser(canvas, analyser);
} catch (error) {
}
};
export const attachEvents = () => {
document.addEventListener('click', handleClickBody);
};
export const unattachEvents = () => {
document.removeEventListener('click', handleClickBody);
};
const handleClickBody = e => {
connect(document.querySelector('.audio'), document.querySelector('.canvas'));
unattachEvents();
};
const Player = ({ initialTrack, trackList }) => {
const [currentTrack, setCurrentTrack] = useState(initialTrack);
useEffect(() => {
loadAudio(initialTrack);
if (!isTouch()) {
attachEvents();
return unattachEvents;
}
}, [initialTrack]);
useEffect(() => {
const handleKeydown = e => {
captureKeys(e, getAudioElement());
};
document.addEventListener('keydown', handleKeydown);
return () => {
document.removeEventListener('keydown', handleKeydown);
};
});
const handleAudioEnded = () => {
const nextTrack = getNextTrack(trackList, currentTrack);
loadAudio(nextTrack, true);
setCurrentTrack(nextTrack);
};
return (
<>
<canvas className="canvas" />
<div className="controls">
<audio
className="audio"
crossOrigin="anonymous"
controls
onEnded={handleAudioEnded}
/>
</div>
</>
);
};
Player.propTypes = {
initialTrack: PropTypes.object,
trackList: PropTypes.array,
};
export default Player;
Thanks in advance.
Related
When I add scale and dpr to my drawing app's canvas to improve the look of objects on the canvas, it produces issues when resizing the window. The scale seems to be off and possibly flickering between 2 different scales as you change window size quickly? I can't seem to figure out the cause of this. As soon as I remove scale and dpr, then everything works fine. Here's my code and a codesandbox with it. https://codesandbox.io/s/holy-wave-t9y2g0?file=/src/App.js
import React, { useEffect, useRef, useState } from "react";
import getStroke from "perfect-freehand";
function getSvgPathFromStroke(stroke) {
if (!stroke.length) return "";
const d = stroke.reduce(
(acc, [x0, y0], i, arr) => {
const [x1, y1] = arr[(i + 1) % arr.length];
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
return acc;
},
["M", ...stroke[0], "Q"]
);
d.push("Z");
return d.join(" ");
}
export default function App() {
const [elements, setElements] = useState([]);
const wrapperRef = useRef(null);
const canvasRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
const [dimensions, setDimensions] = useState({
height: 0,
width: 0
});
const elementsRef = useRef(elements);
const dpr = window.devicePixelRatio || 1;
// in place of original `setElements`
const setElementsRef = (x) => {
elementsRef.current = x; // keep updated
setElements(x);
};
const setNewElements = ({ type, points, ...rest }) => {
setElementsRef([...elements, { type, points, ...rest }]);
};
useEffect(() => {
document.body.style.overscrollBehavior = "contain";
window.addEventListener("resize", () => {
resizeCanvas();
});
}, []);
useEffect(() => {
if (wrapperRef.current && canvasRef.current) {
setupCanvas(true);
}
}, [wrapperRef.current, canvasRef.current]);
useEffect(() => {
drawCanvas();
}, [elements]);
const drawCanvas = () => {
const context = canvasRef.current.getContext("2d");
context.clearRect(0, 0, dimensions.width * dpr, dimensions.height * dpr);
elementsRef.current.forEach((element) => {
const stroke = getStroke(element.points);
const pathData = getSvgPathFromStroke(stroke);
const myPath = new Path2D(pathData);
context.fill(myPath);
});
};
const setupCanvas = () => {
const newHeight = wrapperRef.current.clientHeight ?? 0;
const newWidth = wrapperRef.current.clientWidth ?? 0;
setDimensions({
height: newHeight,
width: newWidth
});
const context = canvasRef?.current?.getContext("2d");
context.scale(dpr, dpr);
return new Promise((fulfill) => fulfill(true));
};
const resizeCanvas = () => {
if (canvasRef.current) {
setupCanvas().then(() => drawCanvas());
}
};
function handleMouseDown(e) {
setIsDrawing(true);
if (canvasRef.current) {
const bounds = canvasRef?.current?.getBoundingClientRect();
const x = e.pageX - bounds.left - window.scrollX;
const y = e.pageY - bounds.top - window.scrollY;
setNewElements({ type: "pen", points: [[x, y]] });
}
}
function handleMouseMove(e) {
if (canvasRef.current && isDrawing) {
const bounds = canvasRef.current.getBoundingClientRect();
const x = e.pageX - bounds.left - window.scrollX;
const y = e.pageY - bounds.top - window.scrollY;
const arr = [...elements];
arr[arr.length - 1].points.push([x, y]);
setElementsRef(arr);
}
}
function handleMouseUp(e) {
setIsDrawing(false);
}
return (
<div
id="wrapper"
style={{ height: "100vh", width: "100vw", overflow: "hidden" }}
ref={wrapperRef}
>
<canvas
id="canvas"
ref={canvasRef}
style={{ height: "100%", width: "100%" }}
width={dimensions.width * dpr}
height={dimensions.height * dpr}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
/>
</div>
);
}
please help me! I'm using context API to pass the tracks to MucsicPlayer. but whenever I set setTracks state audio element src updates but audioRef.current doesn't update. I inspected it and saw that audioRef.current = <audio preload="auto" src(unknown)>. so ref src does not update. what should I do.
import React, { useState, useEffect, useRef, useContext } from 'react'
import { TrackContext } from '../../music/TrackContext'
const MusicPlayer = () => {
const [tracks, setTracks] = useContext(TrackContext)
console.log(tracks)
// states
const [trackIndex, setTrackIndex] = useState(0)
console.log(trackIndex)
const [trackProgress, setTrackProgress] = useState(0)
const [isPlaying, setIsPlaying] = useState(false)
// eslint-disable-next-line
const [volume, setVolume] = useState(1)
const { title, artist, image, audioSrc } = tracks[trackIndex]
//refs
const audio = new Audio(audioSrc)
const audioRef = useRef(audio)
const intervalRef = useRef()
const isReady = useRef(false)
console.log(audioRef.current)
const { duration } = audioRef.current
const toPrevTrack = () => {
if (trackIndex - 1 < 0) {
setTrackIndex(tracks.length - 1)
} else {
setTrackIndex(trackIndex - 1)
}
}
const toNextTrack = () => {
if (trackIndex < tracks.length - 1) {
setTrackIndex(trackIndex + 1)
} else {
setTrackIndex(0)
}
}
const startTimer = () => {
clearInterval(intervalRef.current)
intervalRef.current = setInterval(() => {
if (audioRef.current.ended) {
toNextTrack()
} else {
setTrackProgress(audioRef.current.currentTime);
}
}, [1000])
}
useEffect(() => {
if (isPlaying) {
audioRef.current.play()
startTimer();
} else {
clearInterval(intervalRef.current)
audioRef.current.pause()
}
// eslint-disable-next-line
}, [isPlaying])
useEffect(() => {
return () => {
audioRef.current.pause()
clearInterval(intervalRef.current)
}
}, [])
useEffect(() => {
audioRef.current.play()
audioRef.current = new Audio(audioSrc)
setTrackProgress(audioRef.current.currentTime)
if (isReady.current) {
audioRef.current.play()
setIsPlaying(true)
startTimer()
} else {
isReady.current = true
}
// eslint-disable-next-line
}, [trackIndex])
const onScrub = (value) => {
clearInterval(intervalRef.current)
audioRef.current.currentTime = value
setTrackProgress(audioRef.current.currentTime)
}
const onScrubEnd = () => {
if (!isPlaying) {
setIsPlaying(true);
}
startTimer();
}
const onScrubVolume = (value) => {
audioRef.current.volume = value
setVolume(audioRef.current.value)
}
function formatMinutes(sec) {
return new Date(sec * 1000).toUTCString().split(" ")[4].substr(3, 8)
}
const currentPercentage = duration ? `${(trackProgress / duration) * 100}%` : '0%';
const trackStyling = `-webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(${currentPercentage}, #fff), color-stop(${currentPercentage}, #777))`;
return (
<div className="player">
<div className="left-block">
<div className="art">
<img src={image} alt="" />
</div>
<div className="song-details">
<div className="song-name">{title}</div>
<div className="artist-name">
{artist}
</div>
</div>
</div>
<div className="center-block">
<div className="song-progress">
<div>{formatMinutes(audioRef.current.currentTime)}</div>
<input
value={trackProgress}
step="1"
min="1"
max={duration ? duration : `${duration}`}
onChange={(e) => onScrub(e.target.value)}
onMouseUp={onScrubEnd}
onKeyUp={onScrubEnd}
style={{ background: trackStyling }}
type="range" />
<div>{duration ? formatMinutes(duration) : "00:00"}</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default MusicPlayer
useRef will not get re-initialized on every render.
So it will stay the same as it was initialized the very first time.
So whenever you are switching your track you have to update the audioRef too.
Change your toPrevTrack and toNextTrack
const toPrevTrack = () => {
const prevIndex = trackIndex - 1 < 0 ? tracks.length - 1 : trackIndex - 1;
const { audioSrc } = tracks[prevIndex]
audioRef.current = new Audio(audioSrc);
}
const toNextTrack = () => {
const nextIndex = trackIndex < tracks.length - 1 ? trackIndex + 1 : 0;
const { audioSrc } = tracks[nextIndex]
audioRef.current = new Audio(audioSrc);
}
I am adding cursor animations to a React/Typescript project and in researching came across a CodePen (Animated Cursor
React Component) that works perfectly well.
However, when converting to a Typescript file I come across the error Property 'current' does not exist on type '[boolean, Dispatch<SetStateAction<boolean>>]'.ts(2339) on cursorVisible.current in
const onMouseEnter = () => {
cursorVisible.current = true;
toggleCursorVisibility();
};
Property cursorVisible is from const cursorVisible = useState(false);
What does Typescript need me to do so current works in Typescript? Reading the React Hooks docs, I could not see reference to current on useState and interestingly this works as a js file, only not in ts.
In the past I have used current with ref but never across useState hook.
Full file is
import React, { useEffect, useRef, useState } from 'react';
import MobileDetect from './MobileDetect';
interface CursorProps {
color: string;
outlineAlpha: number;
dotSize: number;
outlineSize: number;
outlineScale: number;
dotScale: number;
}
function AnimatedCursor({
color = '220, 90, 90',
outlineAlpha = 0.3,
dotSize = 8,
outlineSize = 8,
outlineScale = 5,
dotScale = 0.7,
}: CursorProps) {
// Bail if Mobile
if (typeof navigator !== 'undefined' && MobileDetect!.anyMobile())
return <></>;
const cursorOutline = useRef();
const cursorDot = useRef();
const requestRef = useRef();
const previousTimeRef = useRef();
const [width, setWidth] = useState(window.innerWidth);
const [height, setHeight] = useState(window.innerHeight);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const cursorVisible = useState(false);
const cursorEnlarged = useState(false);
const styles = {
cursors: {
zIndex: 999,
pointerEvents: 'none',
position: 'absolute',
top: '50%',
left: '50%',
borderRadius: '50%',
opacity: 0,
transform: 'translate(-50%, -50%)',
transition: 'opacity 0.15s ease-in-out, transform 0.15s ease-in-out',
},
cursorDot: {
width: dotSize,
height: dotSize,
backgroundColor: `rgba(${color}, 1)`,
},
cursorOutline: {
width: outlineSize,
height: outlineSize,
backgroundColor: `rgba(${color}, ${outlineAlpha})`,
},
};
// Hide default cursor
document.body.style.cursor = 'none';
// Mouse Events
const onMouseMove = (event: { pageX: number; pageY: number }) => {
const { pageX: x, pageY: y } = event;
setMousePosition({ x, y });
positionDot(event);
};
const onMouseEnter = () => {
cursorVisible.current = true;
toggleCursorVisibility();
};
const onMouseLeave = () => {
cursorVisible.current = false;
toggleCursorVisibility();
};
const onMouseDown = () => {
cursorEnlarged.current = true;
toggleCursorSize();
};
const onMouseUp = () => {
cursorEnlarged.current = false;
toggleCursorSize();
};
// Set window hxw
const onResize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
};
/**
* Hooks
*/
useEffect(() => {
// Bail if mobile
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseenter', onMouseEnter);
document.addEventListener('mouseleave', onMouseLeave);
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mouseup', onMouseUp);
window.addEventListener('resize', onResize);
requestRef.current = requestAnimationFrame(animateDotOutline);
handleLinkEvents();
return () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseenter', onMouseEnter);
document.removeEventListener('mouseleave', onMouseLeave);
document.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('resize', onResize);
cancelAnimationFrame(requestRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let { x, y } = mousePosition;
const winDimensions = { width, height };
let endX = winDimensions.width / 2;
let endY = winDimensions.height / 2;
/**
* Toggle Cursor Visiblity
*/
function toggleCursorVisibility() {
if (cursorVisible.current) {
cursorDot.current.style.opacity = 1;
cursorOutline.current.style.opacity = 1;
} else {
cursorDot.current.style.opacity = 0;
cursorOutline.current.style.opacity = 0;
}
}
/**
* Position Dot (cursor)
* #param {event}
*/
function positionDot(e: { pageX: number; pageY: number }) {
cursorVisible.current = true;
toggleCursorVisibility();
// Position the dot
endX = e.pageX;
endY = e.pageY;
cursorDot.current.style.top = `${endY}px`;
cursorDot.current.style.left = `${endX}px`;
}
/**
* Toggle Cursors Size/Scale
*/
function toggleCursorSize() {
if (cursorEnlarged.current) {
cursorDot.current.style.transform = `translate(-50%, -50%) scale(${dotScale})`;
cursorOutline.current.style.transform = `translate(-50%, -50%) scale(${outlineScale})`;
} else {
cursorDot.current.style.transform = 'translate(-50%, -50%) scale(1)';
cursorOutline.current.style.transform = 'translate(-50%, -50%) scale(1)';
}
}
/**
* Handle Links Events
* Applies mouseover/out hooks on all links
* to trigger cursor animation
*/
function handleLinkEvents() {
document.querySelectorAll('a').forEach((el) => {
el.addEventListener('mouseover', () => {
cursorEnlarged.current = true;
toggleCursorSize();
});
el.addEventListener('mouseout', () => {
cursorEnlarged.current = false;
toggleCursorSize();
});
});
}
/**
* Animate Dot Outline
* Aniamtes cursor outline with trailing effect.
* #param {number} time
*/
const animateDotOutline = (time: undefined) => {
if (previousTimeRef.current !== undefined) {
x += (endX - x) / 8;
y += (endY - y) / 8;
cursorOutline.current.style.top = `${y}px`;
cursorOutline.current.style.left = `${x}px`;
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animateDotOutline);
};
return (
<>
<div
ref={cursorOutline}
id="cursor-outline"
style={{ ...styles.cursors, ...styles.cursorOutline }}
/>
<div
ref={cursorDot}
id="cursor-inner"
style={{ ...styles.cursors, ...styles.cursorDot }}
/>
</>
);
}
export default AnimatedCursor;
Posting here in case someone else ran into this issue:
My problem was that i was using curly braces instead brackets when destructuring. Brain fart moment, but basically I was attempting to use the Object destructure notation on the array that's returned from useState.
const { someState, setSomeState } = useState(false);
should be
const [ someState, setSomeState ] = useState(false);
You'll have to ask the author of the code you're using. useState returns an array with the current value and a setter function to change the value. Normally you would use it like this:
let [cursorVisible, setCursorVisible] = useState(false);
// instead of cursorVisible.current = true
setCursorVisible(true);
There's no 'current' property on the array, unless maybe it is set by other code which would be bad form I think.
I'm trying to receive fog effect in React. The main idea is that I have two components: first component handles with updating of coordinates of clouds and their velocity, the second component is responsible for one cloud. I have problem with moving of clouds, if I don't clear canvas I can see track of every cloud, if I apply canvas.clear I can't see anything. Do you have any tip, where I should place clear canvas.clear or do you have other ideas?
The first component:
import React from 'react';
import styled from 'styled-components';
import FogPiece from './Fog-Piece.jsx';
const CanvasContext = React.createContext();
const FogDiv = styled.div`
position: absolute;
width:100vw;
height:100vh;
`
class Fog extends React.Component{
constructor(props){
super(props);
this.canvas = React.createRef();
this.state = {
ctx: null,
parameters:[],
screenWidth : 0,
screenHeight: 0,
}
}
componentDidMount = () => {
Promise.all(this.newCoordinates()).then((paramArray) =>{
this.setState({
ctx: this.canvas.current.getContext('2d'),
screenWidth: this.canvas.current.parentNode.getBoundingClientRect().width,
screenHeight: this.canvas.current.parentNode.getBoundingClientRect().height,
parameters: paramArray
});
window.requestAnimationFrame(this.update)
})
}
newCoordinates = () => {
return(Array.from(Array(this.props.density).keys()).map(elem =>{
return new Promise (resolve => {
const params = {
x: this.random(0,this.state.screenWidth),
y: this.random(0,this.state.screenHeight),
velocityX: this.random(-this.props.maxVelocity, this.props.maxVelocity),
velocityY: this.random(-this.props.maxVelocity, this.props.maxVelocity)
}
resolve(params)
})
}))
}
updateCoordinates = () => {
return(this.state.parameters.map(elem =>{
return new Promise (resolve => {
elem = this.ifCross(elem.x, elem.y, elem.velocityX, elem.velocityY);
const params = {
x: elem.x + elem.velocityX,
y: elem.y + elem.velocityY,
velocityX: elem.velocityX,
velocityY: elem.velocityY
}
resolve(params)
})
}))
}
random = (min,max) => {
return Math.random()*(max - min) + min
}
ifCross = (x,y, velocityX, velocityY) => {
if (x > this.state.screenWidth){
x = this.state.screenWidth
velocityX = - velocityX
}
if (x < 0){
x = 0
velocityX = - velocityX
}
if (y > this.state.screenHeight){
y = this.state.screenHeight
velocityY = - velocityY
}
if (y < 0){
y = 0
velocityY = - velocityY
}
return {x:x, y:y, velocityX:velocityX, velocityY:velocityY }
}
update = () => {
Promise.all(this.updateCoordinates()).then((paramArray) =>{
//here is the problem
// this.state.ctx.clearRect(0,0,this.state.screenWidth, this.state.screenHeight)
this.setState({
parameters: paramArray,
});
window.requestAnimationFrame(this.update)
})
}
render(){
return(
<FogDiv>
<canvas width={this.state.screenWidth} height={this.state.screenHeight} ref = {this.canvas} >
{this.state.ctx && (
<CanvasContext.Provider value = {this.state.ctx}>
{this.state.parameters.map(param =>(
<FogPiece
x = {param.x}
y = {param.y}
/>
))}
</CanvasContext.Provider>
)}
</canvas>
</FogDiv>
)
}
}
export default Fog;
export {
CanvasContext
}
the second one:
import React from 'react';
import styled from 'styled-components';
import {CanvasContext} from './Fog.jsx';
class FogPiece extends React.Component{
constructor(props){
super(props);
this.state = {
image:'https://media.istockphoto.com/vectors/sample-red-square-grunge-textured-isolated-stamp-vector-id471401412',
}
}
random(min,max){
return Math.random()*(max - min) + min
}
render(){
return(
<CanvasContext.Consumer>
{ctx => {
console.log("x", "y", this.props)
const img = new Image();
img.src = this.state.image;
img.onload = () => {
ctx.drawImage(img,
this.props.x,
this.props.y,
40,
40)
}
}}
</CanvasContext.Consumer>
)
}
}
export default FogPiece;
I would like to create a canvas where the user can draw an Arrow by using his mouse.
What I'm trying to accomplish is exactly this: https://jsfiddle.net/w33e9fpa/
But I don't understand how to convert that to React code, and my implementation currently doesn't work. When I run this code it seems that an arrow is drawn on the top left of the canvas, but nothing happens if I click on it.
Here's my code:
class DrawArrow extends Component {
state = {
isDrawing: false,
mode: "brush"
};
componentDidMount() {
const canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 300;
const context = canvas.getContext("2d");
this.setState({ canvas, context });
}
handleMouseDown = () => {
this.setState({ isDrawing: true });
// TODO: improve
const stage = this.arrow.parent.parent;
this.lastPointerPosition = stage.getPointerPosition();
this.setState({
posX: this.lastPointerPosition.x,
poxY: this.lastPointerPosition.y
})
}
handleMouseUp = () => {
this.setState({ isDrawing: false });
};
handleMouseMove = () => {
if (this.state.drawing === true) {
const stage = this.arrow.parent.parent;
this.lastPointerPosition = stage.getPointerPosition();
var pos = stage.getPointerPosition();
var oldPoints = this.arrow.points();
this.arrow.points([oldPoints[0], oldPoints[1], pos.x, pos.y])
this.arrow.getLayer().draw();
}
}
render() {
return (
<Arrow
points= {[this.state.posX,this.state.posY, this.state.posX, this.state.posY]}
pointerLength= {20}
pointerWidth= {20}
fill= 'black'
stroke= 'black'
strokeWidth= {4}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
onMouseMove={this.handleMouseMove}
/>
);
}
}
class NewWhite extends Component {
render() {
return (
<Stage width={900} height={700}>
<Layer>
<DrawArrow />
</Layer>
</Stage>
);
}
}
Thanks for you help !
Here you go:
import React, { Component } from "react";
import { Stage, Layer, Arrow, Circle, Line } from "react-konva";
import ReactDOM from "react-dom";
import "./styles.css";
class Drawable {
constructor(startx, starty) {
this.startx = startx;
this.starty = starty;
}
}
class ArrowDrawable extends Drawable {
constructor(startx, starty) {
super(startx, starty);
this.x = startx;
this.y = starty;
}
registerMovement(x, y) {
this.x = x;
this.y = y;
}
render() {
const points = [this.startx, this.starty, this.x, this.y];
return <Arrow points={points} fill="black" stroke="black" />;
}
}
class CircleDrawable extends ArrowDrawable {
constructor(startx, starty) {
super(startx, starty);
this.x = startx;
this.y = starty;
}
render() {
const dx = this.startx - this.x;
const dy = this.starty - this.y;
const radius = Math.sqrt(dx * dx + dy * dy);
return (
<Circle radius={radius} x={this.startx} y={this.starty} stroke="black" />
);
}
}
class FreePathDrawable extends Drawable {
constructor(startx, starty) {
super(startx, starty);
this.points = [startx, starty];
}
registerMovement(x, y) {
this.points = [...this.points, x, y];
}
render() {
return <Line points={this.points} fill="black" stroke="black" />;
}
}
class SceneWithDrawables extends Component {
constructor(props) {
super(props);
this.state = {
drawables: [],
newDrawable: [],
newDrawableType: "FreePathDrawable"
};
}
getNewDrawableBasedOnType = (x, y, type) => {
const drawableClasses = {
FreePathDrawable,
ArrowDrawable,
CircleDrawable
};
return new drawableClasses[type](x, y);
};
handleMouseDown = e => {
const { newDrawable } = this.state;
if (newDrawable.length === 0) {
const { x, y } = e.target.getStage().getPointerPosition();
const newDrawable = this.getNewDrawableBasedOnType(
x,
y,
this.state.newDrawableType
);
this.setState({
newDrawable: [newDrawable]
});
}
};
handleMouseUp = e => {
const { newDrawable, drawables } = this.state;
if (newDrawable.length === 1) {
const { x, y } = e.target.getStage().getPointerPosition();
const drawableToAdd = newDrawable[0];
drawableToAdd.registerMovement(x, y);
drawables.push(drawableToAdd);
this.setState({
newDrawable: [],
drawables
});
}
};
handleMouseMove = e => {
const { newDrawable } = this.state;
if (newDrawable.length === 1) {
const { x, y } = e.target.getStage().getPointerPosition();
const updatedNewDrawable = newDrawable[0];
updatedNewDrawable.registerMovement(x, y);
this.setState({
newDrawable: [updatedNewDrawable]
});
}
};
render() {
const drawables = [...this.state.drawables, ...this.state.newDrawable];
return (
<div>
<button
onClick={e => {
this.setState({ newDrawableType: "ArrowDrawable" });
}}
>
Draw Arrows
</button>
<button
onClick={e => {
this.setState({ newDrawableType: "CircleDrawable" });
}}
>
Draw Circles
</button>
<button
onClick={e => {
this.setState({ newDrawableType: "FreePathDrawable" });
}}
>
Draw FreeHand!
</button>
<Stage
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
onMouseMove={this.handleMouseMove}
width={900}
height={700}
>
<Layer>
{drawables.map(drawable => {
return drawable.render();
})}
</Layer>
</Stage>
</div>
);
}
}
function App() {
return <SceneWithDrawables />;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Working example to play with:
https://codesandbox.io/s/w12qznzx5