A GUI to help you to visualize trigonometric values. Built using ReactJS, SVG and Fraction.js (https://github.com/infusion/Fraction.js).
A Pen by Anthony Dugois on CodePen.
<div | |
class="ad-App" | |
id="app"> | |
</div> |
const Component = React.Component | |
class Angle extends Component { | |
propTypes = { | |
index: React.PropTypes.number.isRequired, | |
isDragging: React.PropTypes.number.isRequired, | |
angle: React.PropTypes.shape({ | |
numerator: React.PropTypes.number.isRequired, | |
denominator: React.PropTypes.number.isRequired, | |
}).isRequired, | |
dragStart: React.PropTypes.func.isRequired, | |
circleRadius: React.PropTypes.number.isRequired, | |
sketchSize: React.PropTypes.number.isRequired, | |
} | |
handleMouseDown = (e) => { | |
this.props.dragStart(e, this.props.index) | |
} | |
render() { | |
const { | |
angle, | |
index, | |
isDragging, | |
...props, | |
} = this.props | |
const sketchHalfSize = props.sketchSize / 2 | |
const numerator = angle.numerator !== "" ? angle.numerator : 1 | |
const denominator = angle.denominator !== "" ? angle.denominator : 1 | |
const radians = (numerator / denominator) * Math.PI | |
const cosRadius = sketchHalfSize + Math.cos(radians) * props.circleRadius | |
const sinRadius = sketchHalfSize - Math.sin(radians) * props.circleRadius | |
const angleRadius = 40 | |
const cosAngleRadius = sketchHalfSize + Math.cos(radians) * angleRadius | |
const sinAngleRadius = sketchHalfSize - Math.sin(radians) * angleRadius | |
return ( | |
<g className="ad-SketchAngle"> | |
<path | |
className="ad-SketchAngle-angle" | |
d={ | |
"M " + (sketchHalfSize + angleRadius) + " " + sketchHalfSize + | |
" A " + angleRadius + " " + angleRadius + ", " + | |
(Math.sin(radians) < 0 ? "0, 1, 0" : "0, 0, 0") + ", " + | |
cosAngleRadius + " " + sinAngleRadius + | |
" L " + sketchHalfSize + " " + sketchHalfSize + | |
" Z" | |
} /> | |
<g className="ad-SketchAngle-trigo"> | |
<line | |
className="ad-SketchAngle-cos" | |
x1={ sketchHalfSize } | |
y1={ sinRadius } | |
x2={ cosRadius } | |
y2={ sinRadius } /> | |
<line | |
className="ad-SketchAngle-sin" | |
x1={ cosRadius } | |
y1={ sketchHalfSize } | |
x2={ cosRadius } | |
y2={ sinRadius } /> | |
</g> | |
<line | |
className="ad-SketchAngle-line" | |
x1={ sketchHalfSize } | |
y1={ sketchHalfSize } | |
x2={ sketchHalfSize + props.circleRadius } | |
y2={ sketchHalfSize } /> | |
<line | |
className="ad-SketchAngle-line" | |
x1={ sketchHalfSize } | |
y1={ sketchHalfSize } | |
x2={ cosRadius } | |
y2={ sinRadius } /> | |
<circle | |
className={ | |
"ad-SketchAngle-dot" + | |
(isDragging === index ? " is-dragging" : "") | |
} | |
onMouseDown={ this.handleMouseDown } | |
cx={ cosRadius } | |
cy={ sinRadius } | |
r={ 6 } /> | |
</g> | |
) | |
} | |
} | |
class Sketch extends Component { | |
propTypes = { | |
angles: React.PropTypes.array.isRequired, | |
circleRadius: React.PropTypes.number.isRequired, | |
sketchSize: React.PropTypes.number.isRequired, | |
} | |
render() { | |
const { | |
angles, | |
...props, | |
} = this.props | |
const sketchHalfSize = props.sketchSize / 2 | |
const svgAngles = angles.map((angle, index) => { | |
return (<Angle | |
angle={ angle } | |
index={ index } | |
{ ...props } />) | |
}) | |
return ( | |
<svg | |
className="ad-Sketch" | |
viewBox={ "0 0 " + props.sketchSize + " " + props.sketchSize }> | |
<g className="ad-Sketch-base"> | |
<line | |
className="ad-Sketch-ortho" | |
x1={ sketchHalfSize } | |
y1={ 0 } | |
x2={ sketchHalfSize } | |
y2={ props.sketchSize } /> | |
<line | |
className="ad-Sketch-ortho" | |
x1={ 0 } | |
y1={ sketchHalfSize } | |
x2={ props.sketchSize } | |
y2={ sketchHalfSize } /> | |
<text | |
className="ad-Sketch-hint" | |
x={ sketchHalfSize + 10 } | |
y={ 10 }> | |
sin | |
</text> | |
<text | |
className="ad-Sketch-hint ad-Sketch-hint--r" | |
x={ props.sketchSize - 5 } | |
y={ sketchHalfSize - 10 }> | |
cos | |
</text> | |
<text | |
className="ad-Sketch-value" | |
x={ sketchHalfSize + props.circleRadius + 15 } | |
y={ sketchHalfSize - 10 }> | |
0 | |
</text> | |
<text | |
className="ad-Sketch-value ad-Sketch-value--r" | |
x={ sketchHalfSize - (props.circleRadius + 15) } | |
y={ sketchHalfSize - 10 }> | |
π | |
</text> | |
<text | |
className="ad-Sketch-value ad-Sketch-value--c ad-Sketch-value--t" | |
x={ sketchHalfSize } | |
y={ sketchHalfSize - (props.circleRadius + 10) }> | |
π / 2 | |
</text> | |
<text | |
className="ad-Sketch-value ad-Sketch-value--c ad-Sketch-value--b" | |
x={ sketchHalfSize } | |
y={ sketchHalfSize + props.circleRadius + 10 }> | |
3π / 2 | |
</text> | |
<circle | |
className="ad-Sketch-circle" | |
cx={ sketchHalfSize } | |
cy={ sketchHalfSize } | |
r={ props.circleRadius } /> | |
</g> | |
<g className="ad-Sketch-angles"> | |
{ svgAngles } | |
</g> | |
</svg> | |
) | |
} | |
} | |
class Icon extends Component { | |
propTypes = { | |
name: React.PropTypes.string.isRequired, | |
} | |
render() { | |
let path | |
switch (this.props.name) { | |
case "clear": | |
path = "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z" | |
break; | |
case "add": | |
path = "M810 554h-256v256h-84v-256h-256v-84h256v-256h84v256h256v84z" | |
break; | |
} | |
return ( | |
<svg | |
className="ad-Icon" | |
viewBox="0 0 1024 1024"> | |
<path d={ path } /> | |
</svg> | |
) | |
} | |
} | |
class Button extends Component { | |
propTypes = { | |
type: React.PropTypes.string, | |
size: React.PropTypes.string, | |
icon: React.PropTypes.string, | |
} | |
render() { | |
const { | |
type, | |
size, | |
icon, | |
children, | |
...props, | |
} = this.props | |
return ( | |
<button | |
className={ | |
"ad-Button" + | |
(type ? " ad-Button--" + type : "") + | |
(size ? " ad-Button--" + size : "") | |
} | |
{ ...props } | |
type="button"> | |
{ icon && (<Icon name={ icon } />) } | |
{ | |
children && ( | |
<span className="ad-Button-text"> | |
{ children } | |
</span> | |
) | |
} | |
</button> | |
) | |
} | |
} | |
class FormGroup extends Component { | |
propTypes = { | |
index: React.PropTypes.number.isRequired, | |
angle: React.PropTypes.shape({ | |
numerator: React.PropTypes.number.isRequired, | |
denominator: React.PropTypes.number.isRequired, | |
}).isRequired, | |
updateNumerator: React.PropTypes.func.isRequired, | |
updateDenominator: React.PropTypes.func.isRequired, | |
deleteFormGroup: React.PropTypes.func.isRequired, | |
} | |
handleNumerator = (e) => { | |
this.props.updateNumerator(this.props.index, e.target.value) | |
} | |
handleDenominator = (e) => { | |
this.props.updateDenominator(this.props.index, e.target.value) | |
} | |
handleClick = (e) => { | |
e.preventDefault() | |
this.props.deleteFormGroup(this.props.index) | |
} | |
render() { | |
return ( | |
<div className="ad-FormGroup"> | |
<div className="ad-FormGroup-color"></div> | |
<div className="ad-FormMath"> | |
<div className="ad-FormMath-frac"> | |
<div className="ad-FormMath-n"> | |
<input | |
className="ad-FormInput" | |
ref="numerator" | |
value={ this.props.angle.numerator } | |
onChange={ this.handleNumerator } | |
type="text" /> | |
</div> | |
<div className="ad-FormMath-n"> | |
<input | |
className="ad-FormInput" | |
ref="denominator" | |
value={ this.props.angle.denominator } | |
onChange={ this.handleDenominator } | |
type="text" /> | |
</div> | |
</div> | |
<div className="ad-FormMath-formula"> | |
π | |
</div> | |
</div> | |
<div className="ad-FormGroup-action"> | |
<Button | |
onClick={ this.handleClick } | |
type="cancel" | |
size="mini" | |
icon="clear" /> | |
</div> | |
</div> | |
) | |
} | |
} | |
class Form extends Component { | |
propTypes = { | |
angles: React.PropTypes.array.isRequired, | |
shouldScroll: React.PropTypes.bool.isRequired, | |
addFormGroup: React.PropTypes.func.isRequired, | |
blurAddButton: React.PropTypes.func.isRequired, | |
} | |
componentDidUpdate() { | |
const n = React.findDOMNode(this.refs.groups) | |
if (this.props.shouldScroll) { | |
n.scrollTop = n.scrollHeight | |
} | |
} | |
handleClick = (e) => { | |
e.preventDefault() | |
this.props.addFormGroup() | |
} | |
handleBlur = (e) => { | |
this.props.blurAddButton() | |
} | |
render() { | |
const { | |
angles, | |
addFormGroup, | |
...props, | |
} = this.props | |
let groups = angles.map((angle, index) => { | |
return ( | |
<FormGroup | |
index={ index } | |
angle={ angle } | |
{ ...props } /> | |
) | |
}) | |
return ( | |
<form className="ad-Form"> | |
<div | |
className="ad-Form-groups" | |
ref="groups"> | |
{ groups } | |
</div> | |
<div className="ad-Form-actions"> | |
<Button | |
onClick={ this.handleClick } | |
onBlur={ this.handleBlur } | |
type="primary" | |
size="full" | |
icon="add"> | |
Add angle | |
</Button> | |
</div> | |
</form> | |
) | |
} | |
} | |
class Trigonometry extends Component { | |
state = { | |
isDragging: false, | |
shouldScroll: false, | |
angles: [ | |
{ | |
numerator: 7, | |
denominator: 10, | |
}, | |
{ | |
numerator: 3, | |
denominator: 2, | |
}, | |
{ | |
numerator: 1, | |
denominator: 5, | |
}, | |
], | |
} | |
updateNumerator = (index, numerator) => { | |
if (numerator !== "") { | |
numerator = parseFloat(numerator) | |
} | |
const angles = this.state.angles.map((angle, angleIndex) => { | |
if (angleIndex === index) { | |
numerator = (numerator !== "" && isNaN(numerator)) ? angle.numerator : numerator | |
return { | |
numerator: numerator, | |
denominator: angle.denominator, | |
} | |
} | |
return angle | |
}) | |
this.setState({ angles }) | |
} | |
updateDenominator = (index, denominator) => { | |
if (denominator !== "") { | |
denominator = parseFloat(denominator) | |
if (denominator === 0) { | |
denominator = 1 | |
} | |
} | |
const angles = this.state.angles.map((angle, angleIndex) => { | |
if (angleIndex === index) { | |
denominator = (denominator !== "" && isNaN(denominator)) ? angle.denominator : denominator | |
return { | |
numerator: angle.numerator, | |
denominator: denominator, | |
} | |
} | |
return angle | |
}) | |
this.setState({ angles }) | |
} | |
blurAddButton = () => { | |
this.setState({ | |
shouldScroll: false, | |
}) | |
} | |
addFormGroup = () => { | |
const numerator = 0, | |
denominator = 1, | |
angles = this.state.angles | |
angles.push({ numerator, denominator }) | |
this.setState({ | |
angles, | |
shouldScroll: true, | |
}) | |
} | |
deleteFormGroup = (index) => { | |
let angles = this.state.angles | |
delete angles[index] | |
this.setState({ angles }) | |
} | |
drag = (e) => { | |
let i = this.state.isDragging | |
let sketch = React.findDOMNode(this.refs.sketch).getBoundingClientRect() | |
if (i !== false) { | |
const sketchHalfSize = this.props.sketchSize / 2 | |
let angles = this.state.angles, | |
x = (e.pageX - sketch.left) - sketchHalfSize, | |
y = sketchHalfSize - (e.pageY - sketch.top), | |
rad = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)), | |
sine = y / rad, | |
cosine = x / rad, | |
theta | |
theta = Math.acos(cosine) | |
if (sine < 0) { | |
theta = 2 * Math.PI - theta | |
} | |
const f = new Fraction((theta / Math.PI).toFixed(1)) | |
angles[i] = { | |
numerator: f.n, | |
denominator: f.d, | |
} | |
this.setState({ angles }) | |
} | |
} | |
dragStart = (e, index) => { | |
e.preventDefault() | |
this.setState({ | |
isDragging: index, | |
}) | |
} | |
dragEnd = (e) => { | |
this.setState({ | |
isDragging: false, | |
}) | |
} | |
render() { | |
return ( | |
<div> | |
<div className="ad-App-head"> | |
<h1 className="ad-App-title"> | |
Trigonometry Helper | |
</h1> | |
<div className="ad-App-hint"> | |
Type values to move an angle or drag it directly on the scheme. | |
</div> | |
</div> | |
<div | |
className="ad-Trigonometry" | |
onMouseUp={ this.dragEnd } | |
onMouseMove={ this.drag }> | |
<div className="ad-Trigonometry-svg"> | |
<Sketch | |
ref="sketch" | |
angles={ this.state.angles } | |
drag={ this.drag } | |
dragStart={ this.dragStart } | |
dragEnd={ this.dragEnd } | |
isDragging={ this.state.isDragging } | |
{ ...this.props } /> | |
</div> | |
<div className="ad-Trigonometry-form"> | |
<Form | |
angles={ this.state.angles } | |
shouldScroll={ this.state.shouldScroll } | |
updateNumerator={ this.updateNumerator } | |
updateDenominator={ this.updateDenominator } | |
blurAddButton={ this.blurAddButton } | |
addFormGroup={ this.addFormGroup } | |
deleteFormGroup={ this.deleteFormGroup } /> | |
</div> | |
</div> | |
<div className="ad-App-foot"> | |
<a href="https://twitter.com/a_dugois"> | |
Follow me on Twitter | |
</a> | |
</div> | |
</div> | |
) | |
} | |
} | |
React.render( | |
<Trigonometry | |
circleRadius={ 130 } | |
sketchSize={ 26 * 16 } />, | |
document.querySelector("#app") | |
) |
<script src="//cdnjs.cloudflare.com/ajax/libs/react/0.13.0/react.min.js"></script> | |
<script src="//s3-us-west-2.amazonaws.com/s.cdpn.io/80862/fraction.js"></script> |
@use cssnext; | |
@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,400italic,600,600italic); | |
:root { | |
--colorPalette-1: #37474F; | |
--colorPalette-2: #263238; | |
--colorPalette-3: #00BCD4; | |
} | |
::-webkit-scrollbar { | |
width: .5rem; | |
} | |
::-webkit-scrollbar-thumb { | |
background: var(--colorPalette-1); | |
border-radius: 10px; | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background: color(var(--colorPalette-1) l(+5%)); | |
} | |
html { | |
font-size: 16px; | |
} | |
html, body { | |
height: 100%; | |
} | |
*, | |
*::before, | |
*::after { | |
box-sizing: border-box; | |
} | |
.ad-Icon { | |
width: 1.5em; | |
height: 1.5em; | |
color: currentColor; | |
} | |
.ad-Icon path { | |
fill: currentColor; | |
} | |
.ad-Button { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
padding: .7rem 1rem; | |
border: 2px solid var(--colorPalette-1); | |
border-radius: 4px; | |
background: none; | |
cursor: pointer; | |
transition: border .1s, | |
background .1s, | |
color .1s; | |
font-size: .7rem; | |
color: var(--colorPalette-1); | |
} | |
.ad-Button:focus { | |
outline: 0; | |
} | |
.ad-Button--full { | |
width: 100%; | |
} | |
.ad-Button--mini { | |
padding: .25rem; | |
font-size: .5rem; | |
} | |
.ad-Button--primary { | |
border-color: var(--colorPalette-3); | |
color: var(--colorPalette-3); | |
} | |
.ad-Button--primary:focus, | |
.ad-Button--primary:hover { | |
background: var(--colorPalette-3); | |
color: #fff; | |
} | |
.ad-Button--cancel { | |
border-color: #fff; | |
color: #fff; | |
} | |
.ad-Button--cancel:focus, | |
.ad-Button--cancel:hover { | |
background: #fff; | |
color: var(--colorPalette-2); | |
} | |
.ad-Button-text { | |
text-transform: uppercase; | |
font-family: "Open Sans", sans-serif; | |
font-weight: bold; | |
color: currentColor; | |
} | |
.ad-Icon + .ad-Button-text { | |
margin-left: .25rem; | |
} | |
.ad-App { | |
height: 100%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
background: var(--colorPalette-3); | |
} | |
.ad-App-head { | |
margin-bottom: 2rem; | |
} | |
.ad-App-title { | |
font-family: "Open Sans", sans-serif; | |
font-weight: bold; | |
font-size: 1.05rem; | |
color: #fff; | |
} | |
.ad-App-hint { | |
margin-top: .4rem; | |
font-family: "Open Sans", sans-serif; | |
font-size: .9rem; | |
color: #fff; | |
} | |
.ad-App-foot { | |
margin-top: 1rem; | |
text-transform: uppercase; | |
text-align: right; | |
font-family: "Open Sans", sans-serif; | |
font-weight: bold; | |
font-size: .65rem; | |
} | |
.ad-App-foot a { | |
color: #fff; | |
text-decoration: underline; | |
} | |
.ad-Trigonometry { | |
overflow: hidden; | |
display: flex; | |
height: 30rem; | |
background: var(--colorPalette-1); | |
border-radius: 4px; | |
box-shadow: 0 2px 6px rgba(0, 0, 0, .4); | |
} | |
.ad-Trigonometry-svg { | |
width: 30rem; | |
height: 100%; | |
padding: 2rem; | |
} | |
.ad-Trigonometry-form { | |
width: 14rem; | |
height: 100%; | |
background: var(--colorPalette-2); | |
} | |
.ad-Sketch { | |
width: 100%; | |
height: 100%; | |
} | |
.ad-Sketch-circle { | |
stroke: #fff; | |
stroke-width: 2px; | |
fill: none; | |
} | |
.ad-Sketch-ortho { | |
stroke: color(var(--colorPalette-1) l(+5%)); | |
stroke-width: 2px; | |
} | |
.ad-Sketch-hint { | |
fill: #fff; | |
font-family: "Open Sans", sans-serif; | |
font-style: italic; | |
font-size: .75rem; | |
} | |
.ad-Sketch-value { | |
fill: #fff; | |
font-family: "Open Sans", sans-serif; | |
font-weight: bold; | |
font-size: .8rem; | |
} | |
.ad-Sketch-value--c { | |
text-anchor: middle; | |
} | |
.ad-Sketch-value--t { | |
alignment-baseline: text-after-edge; | |
} | |
.ad-Sketch-value--b { | |
alignment-baseline: text-before-edge; | |
} | |
.ad-Sketch-hint--r, | |
.ad-Sketch-value--r { | |
text-anchor: end; | |
} | |
.ad-SketchAngle:nth-child(5n+1), | |
.ad-FormGroup:nth-child(5n+1) { | |
color: #2196F3; | |
} | |
.ad-SketchAngle:nth-child(5n+2), | |
.ad-FormGroup:nth-child(5n+2) { | |
color: #66BB6A; | |
} | |
.ad-SketchAngle:nth-child(5n+3), | |
.ad-FormGroup:nth-child(5n+3) { | |
color: #F44336; | |
} | |
.ad-SketchAngle:nth-child(5n+4), | |
.ad-FormGroup:nth-child(5n+4) { | |
color: #EC407A; | |
} | |
.ad-SketchAngle:nth-child(5n+5), | |
.ad-FormGroup:nth-child(5n+5) { | |
color: #FFEB3B; | |
} | |
.ad-SketchAngle-line { | |
stroke: currentColor; | |
stroke-width: 2px; | |
stroke-linecap: round; | |
} | |
.ad-SketchAngle-angle { | |
opacity: .2; | |
fill: currentColor; | |
} | |
.ad-SketchAngle-trigo { | |
stroke: color(var(--colorPalette-1) l(+5%)); | |
stroke-width: 2px; | |
stroke-dasharray: 6, 8; | |
} | |
.ad-SketchAngle-dot { | |
fill: currentColor; | |
stroke: #fff; | |
stroke-width: 2px; | |
transition: stroke .2s, | |
stroke-width .2s; | |
} | |
.ad-SketchAngle-dot.is-dragging { | |
stroke: #fff; | |
stroke-width: 4px; | |
} | |
.ad-Form { | |
height: 100%; | |
display: flex; | |
flex-direction: column; | |
} | |
.ad-Form-groups { | |
flex: 1; | |
overflow: auto; | |
padding: 1rem 2rem 0; | |
} | |
.ad-Form-actions { | |
padding: 2rem; | |
} | |
.ad-FormGroup { | |
width: 100%; | |
padding: 1rem 0; | |
display: flex; | |
align-items: center; | |
} | |
.ad-FormGroup + .ad-FormGroup { | |
border-top: 1px solid var(--colorPalette-1); | |
} | |
.ad-FormGroup-color { | |
width: 12px; | |
height: 12px; | |
border: 2px solid #fff; | |
border-radius: 50%; | |
background: currentColor; | |
} | |
.ad-FormMath { | |
margin-left: .8rem; | |
flex: 1; | |
display: flex; | |
align-items: center; | |
} | |
.ad-FormMath-frac { | |
width: 2.5rem; | |
display: flex; | |
flex-direction: column; | |
} | |
.ad-FormMath-n + .ad-FormMath-n { | |
margin-top: .25rem; | |
padding-top: .25rem; | |
border-top: 2px solid #fff; | |
} | |
.ad-FormMath-formula { | |
flex: 1; | |
margin-left: .4rem; | |
cursor: default; | |
font-family: "Open Sans", sans-serif; | |
font-size: 1.2rem; | |
color: #fff; | |
} | |
.ad-FormInput { | |
width: 100%; | |
padding: .25rem; | |
border: none; | |
border-radius: 4px; | |
background: var(--colorPalette-1); | |
transition: background .1s; | |
text-align: center; | |
font-family: "Open Sans", sans-serif; | |
font-size: .85rem; | |
color: #fff; | |
} | |
.ad-FormInput:focus { | |
outline: 0; | |
background: color(var(--colorPalette-1) l(+10%)); | |
} |
A GUI to help you to visualize trigonometric values. Built using ReactJS, SVG and Fraction.js (https://github.com/infusion/Fraction.js).
A Pen by Anthony Dugois on CodePen.