Skip to content

Instantly share code, notes, and snippets.

@qzchenwl
Created November 25, 2015 02:56
Show Gist options
  • Save qzchenwl/9999b6e6cbe3c09947f8 to your computer and use it in GitHub Desktop.
Save qzchenwl/9999b6e6cbe3c09947f8 to your computer and use it in GitHub Desktop.
Trigonometry Helper
<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%));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment