Skip to content

Commit

Permalink
Rewrite clock
Browse files Browse the repository at this point in the history
  • Loading branch information
javivelasco committed Aug 23, 2015
1 parent b907fb9 commit 26cef5f
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 211 deletions.
29 changes: 29 additions & 0 deletions components/clock/face.cjsx
@@ -0,0 +1,29 @@
css = require './style'

module.exports = React.createClass

# -- States & Properties
getDefaultProps: ->
className : ''
numbers : []
radius : 0

# -- Internal methods
_numberStyle: (radius, num) ->
position : 'absolute'
left : (radius + radius * Math.sin(360 * (Math.PI/180) / 12 * (num - 1)) + @props.spacing)
top : (radius - radius * Math.cos(360 * (Math.PI/180) / 12 * (num - 1)) + @props.spacing)

_faceStyle: ->
height : @props.radius * 2
width : @props.radius * 2

# -- Render
render: ->
<div ref="root" className={"#{@props.className} #{css.face}"} style={@_faceStyle()}>
{ for i, k in @props.numbers
<span className={css.number}
key={i} style={@_numberStyle(@props.radius - @props.spacing, k + 1)}>
{i}
</span> }
</div>
87 changes: 87 additions & 0 deletions components/clock/hand.cjsx
@@ -0,0 +1,87 @@
prefixer = require "../prefixer"
css = require './style'

module.exports = React.createClass

# -- States & Properties
propTypes:
onHandChange : React.PropTypes.func

getDefaultProps: ->
angle : 0
length : 0
origin : {}

getInitialState: ->
angle : @props.angle
knobWidth : 0
radius : 0

# -- Lifecycle
componentDidMount: ->
@setState
knobWidth : @refs.knob.getDOMNode().offsetWidth

componentWillUpdate: (nextProps, nextState) ->
@props.onHandChange(nextState.angle)

# -- Event handlers
_onKnobMouseDown: ->
_addEventsToDocument(@_getMouseEventMap())

_getMouseEventMap: ->
mousemove : @onMouseMove
mouseup : @onMouseUp

onMouseMove: (event) ->
position = _getMousePosition(event)
@props.onHandMouseMove(@_getPositionRadius(position)) if @props.onHandMouseMove
newDegrees = @_trimAngleToValue(@_positionToAngle(position))
newDegrees = if newDegrees == 360 then 0 else newDegrees
@setState(angle: newDegrees) if @state.angle != newDegrees

# -- Internal methods
_getPositionRadius: (position) ->
x = @props.origin.x - position.x
y = @props.origin.y - position.y
Math.sqrt(x * x + y * y)

_trimAngleToValue: (angle) ->
@props.step * Math.round(angle/@props.step)

_positionToAngle: (position) ->
_angle360(@props.origin.x, @props.origin.y, position.x, position.y)

onMouseUp: ->
@_end(@_getMouseEventMap())

_end: (events) ->
_removeEventsFromDocument(events)

# -- Render
render: ->
style = prefixer.transform("rotate(#{@state.angle}deg)")
style.height = @props.length - @state.knobWidth/2

<div className={css.hand} style={style}>
<div ref='knob' className={css.knob} onMouseDown={@_onKnobMouseDown}></div>
</div>

# -- Static Helper functions
_addEventsToDocument = (events) ->
document.addEventListener(key, events[key], false) for key of events

_removeEventsFromDocument = (events) ->
document.removeEventListener(key, events[key], false) for key of events

_getMousePosition = (event) ->
x: event.pageX
y: event.pageY

_angle360 = (cx, cy, ex, ey) ->
theta = _angle(cx, cy, ex, ey)
if (theta < 0) then 360 + theta else theta

_angle = (cx, cy, ex, ey) ->
theta = Math.atan2(ey - cy, ex - cx) + Math.PI/2
theta * 180 / Math.PI
64 changes: 64 additions & 0 deletions components/clock/hours.cjsx
@@ -0,0 +1,64 @@
css = require './style'
Face = require './face'
Hand = require './hand'

module.exports = React.createClass

# -- States & Properties
propTypes:
format : React.PropTypes.oneOf(['24hr', 'ampm'])
onChange : React.PropTypes.func

getDefaultProps: ->
format : '24hr'
onChange : null

getInitialState: ->
am : false

# -- Events
_onHandMouseMove: (radius) ->
if @props.format == '24hr'
currentAm = radius < @props.radius - @props.spacing * 2
@setState am: currentAm if @state.am != currentAm

_onHandChange: (value) ->
if @props.format == '24hr'
values = if @state.am then AM_HOURS else PM_HOURS
else
values = AM_HOURS
@props.onChange(parseInt(values[value/STEP])) if @props.onChange

# -- Render
render: ->
innerRadius = @props.radius - @props.spacing * 2
handRadius = if @props.format == '24hr' && @state.am then innerRadius else @props.radius
handLength = handRadius - @props.spacing

<div>
<Face
className={css.outerSphere}
numbers={if @props.format == '24hr' then PM_HOURS else AM_HOURS}
spacing={@props.spacing}
radius={@props.radius} />
{
if @props.format == '24hr'
<Face
className={css.innerSphere}
numbers={AM_HOURS}
spacing={@props.spacing}
radius={innerRadius} />
}
<Hand
degrees={0}
length={handLength}
onHandMouseMove={@_onHandMouseMove}
onHandChange={@_onHandChange}
origin={@props.center}
step={STEP} />
</div>

# -- Private constants
AM_HOURS = [12].concat([1..11])
PM_HOURS = ['00'].concat([13..23])
STEP = 360/12
178 changes: 52 additions & 126 deletions components/clock/index.cjsx
@@ -1,149 +1,75 @@
css = require './style'
css = require './style'
Hours = require './hours'
Minutes = require './minutes'

module.exports = React.createClass

# -- States & Properties
propTypes:
className : React.PropTypes.string
className : React.PropTypes.string
mode : React.PropTypes.oneOf(['hours', 'minutes'])

getDefaultProps: ->
className : ''
className : ''
mode : 'hours'

getInitialState: ->
clockCenter : undefined
clockCenter : undefined
clockInnerMaxRadius : undefined
clockInnerMinRadius : undefined
clockMaxRadius : undefined
clockMinRadius : undefined
handInner : false
pressed : false
handAngle : 0
radius : 0

# -- Lifecycle
componentDidMount: ->
@setState
clockCenter : @_getClockCenter()
clockMaxRadius : @_getRefRadius('root')
clockMinRadius : @_getRefRadius('clockHolder')
clockInnerMaxRadius : @_getRefRadius('clockHolder')
clockInnerMinRadius : @_getRefRadius('innerClockHolder')

# -- Position Functions
_getClockCenter: ->
bounds = @refs.root.getDOMNode().getBoundingClientRect()
return {
x: bounds.left + (bounds.right - bounds.left)/2
y: bounds.top + (bounds.bottom - bounds.top) /2
}

_getRefRadius: (ref) ->
bounds = @refs[ref].getDOMNode().getBoundingClientRect()
(bounds.right - bounds.left)/2

_isInsideClockArea: (position) ->
@state.clockMinRadius < @_getPositionRadius(position) < @state.clockMaxRadius
window.addEventListener('resize', @handleResize)
@setState radius: @_getRadius()

_isInsideClockInnerArea: (position) ->
@state.clockInnerMinRadius < @_getPositionRadius(position) < @state.clockInnerMaxRadius
componentWillUpdate: ->
center = @_getCenter()
if @state.center?.x != center.x && @state.center?.y != center.y
@setState center: center

_getPositionRadius: (position) ->
x = @state.clockCenter.x - position.x
y = @state.clockCenter.y - position.y
Math.sqrt(x * x + y * y)
componentWillUnmount: ->
window.removeEventListener('resize', @handleResize)

# -- Helper Functions
_positionToAngle: (position) ->
_angle360(@state.clockCenter.x, @state.clockCenter.y, position.x, position.y)
# -- Events handlers
onHourChange: (hour) ->
console.log "Hour changed to #{hour}"

_trimAngleToValue: (angle) ->
step = 360/12
step * Math.round(angle/step)
onMinuteChange: (minute) ->
console.log "Minute changed to #{minute}"

_getMouseEventMap: ->
mousemove: @onMouseMove
mouseup: @onMouseUp
# -- Helper methods
_getRadius: ->
@refs.wrapper.getDOMNode().getBoundingClientRect().width/2

_moveHandToPosition: (position) ->
trimAngle = @_trimAngleToValue(@_positionToAngle(position))
if @_isInsideClockInnerArea(position)
@setState
handAngle: trimAngle
handInner: true
else if @_isInsideClockArea(position)
@setState
handAngle: trimAngle
handInner: false

_end: (events) ->
_removeEventsFromDocument(events)

# -- Event handlers
onClockMouseDown: (event) ->
position = _getMousePosition(event)
@_moveHandToPosition(position)
_addEventsToDocument(@_getMouseEventMap())
@setState pressed: true

onKnobMouseDown: (event) ->
_addEventsToDocument(@_getMouseEventMap())
@setState pressed: true

onMouseMove: (event) ->
position = _getMousePosition(event)
@_moveHandToPosition(position)
handleResize: ->
@setState
center: @_getCenter()
radius: @_getRadius()

onMouseUp: ->
@_end(@_getMouseEventMap())
@setState pressed: false
_getCenter: ->
bounds = @getDOMNode().getBoundingClientRect()
{
x: bounds.left + (bounds.right - bounds.left)/2
y: bounds.top + (bounds.bottom - bounds.top) /2
}

# -- Render
render: ->
className = @props.className
className += css.root
className += " hand-inner" if @state.handInner
className += " pressed" if @state.pressed
handStyle = transform: "rotate(#{@state.handAngle}deg)"

<div ref="root" className={className} onMouseDown={@onClockMouseDown}>
{# Main Clock }
<div ref="clock" className={css.clock}>
{ <span className={css.hour} key="hour-#{i}">{i}</span> for i in [13..23] }
<span className={css.hour} key="hour-00">00</span>
</div>

{# Inner Clock }
<div ref="innerClock" className={css.innerClock}>
{ <span className={css.innerHour} key="hour-#{i}">{i}</span> for i in [1..12] }
</div>

{# Support area holders }
<div ref="clockHolder" className={css.clockHolder}></div>
<div ref="innerClockHolder" className={css.innerClockHolder}></div>

{# Clock hand }
<div ref="hand" style={handStyle} className={css.hand}>
<div className={css.knob} onMouseDown={@onKnobMouseDown}></div>
console.log @props
<div className={css.root}>
<div ref="wrapper" className={css.wrapper} style={height: @state.radius * 2} >
{
if @props.mode == 'minutes'
<Minutes
center={@state.center}
onChange={@onMinuteChange}
radius={@state.radius}
spacing={@state.radius * 0.16} />
else if @props.mode == 'hours'
<Hours
center={@state.center}
onChange={@onHourChange}
radius={@state.radius}
spacing={@state.radius * 0.16} />
}
</div>
</div>

_getMousePosition = (event) ->
x: event.pageX
y: event.pageY

_angle = (cx, cy, ex, ey) ->
dy = ey - cy;
dx = ex - cx;
theta = Math.atan2(dy, dx) + Math.PI/2
theta = theta * 180 / Math.PI
return theta

_angle360 = (cx, cy, ex, ey) ->
theta = _angle(cx, cy, ex, ey)
theta = 360 + theta if (theta < 0)
return theta

_addEventsToDocument = (events) ->
document.addEventListener(key, events[key], false) for key of events

_removeEventsFromDocument = (events) ->
document.removeEventListener(key, events[key], false) for key of events

0 comments on commit 26cef5f

Please sign in to comment.