hlfcoding/drawing-catalog-1

View on GitHub
sketches/Elementary/Wrap.pde

Summary

Maintainability
Test Coverage
# Wrap
# ====

class Wrap extends Node

  constructor: (params = Wrap.defaults) ->

    _.defaults params, Wrap.defaults unless params is Wrap.defaults
    super params

    @_allForces = null
    @_isReady = no
    @_needsClear = no

    @nodes = []

  # Static
  # ======

  ###
  Wrap can contain its nodes using distinct modes.
  ###

  @UNCONTAINED: 0
  @REFLECTIVE: 1
  @TOROIDAL: 2

  ###
  Wrap can render its nodes to screen in distinct states.
  ###

  @DEFAULT: 0
  @TRACE: 1

  ###
  Wrap can layout its nodes in distinct patterns.
  ###

  @UNIFORM: 0
  @RANDOM: 1

  @defaults:

    entropy: 1

    autoReplace: on # Re-add on remove node.
    contain: on # Contain nodes.
    drainAtEdge: on # Drain node inertia at edges.
    forceOptions: 0
    customForces: []

    autoMass: off
    gravity: off
    move: off
    trace: off
    varyMass: off

    nodeDensity: 1 / 10
    nodeParams: _.pick(Node.defaults,
      'vMax', 'attractDecayRate', 'evadeLifespan', 'tempRepulsionDecayRate',
      'attract', 'collide', 'varyMass')

  @setup: ->

    ###
    This needs to be called for the class to be ready for use. Some default
    attributes reference other values not ready on initial declaration.
    ###

    _.defaults @defaults, Node.defaults

    @defaults.fill = color.WHITE
    @defaults.traceFill = @traceFillColor @defaults.fill

    @defaults.containment = Wrap.REFLECTIVE
    @defaults.layoutPattern = Wrap.RANDOM
    @defaults.viewMode = Node.FORMLESS

    _.extend @defaults.nodeParams,
      _.pick(Node.defaults, 'stroke', 'viewMode')

    @defaults.frictionMag = 0.01 * sketch.state.speedFactor # constant * normal

  @traceFillColor: (fc) -> color.transparentize fc, 0.01

  # Inherited
  # =========

  draw: () ->
    tracePrev = @trace
    @trace = @nodeParams.viewMode is Node.LINE

    if tracePrev is on and @trace isnt tracePrev
      sketch.pushScreen Wrap.TRACE

    # 'Layer' the canvas if needed.
    isTraceFrame = millis() % (sketch.state.frameRate * 10) is 0
    if @trace is on and isTraceFrame
      fill @getTraceFillColor()
      @drawBoundsRect()
      noFill()

    # Clear the canvas if needed.
    if @trace is off or @_needsClear
      fill @fillColor()
      @drawBoundsRect()
      # Sometimes when switching view modes, the canvas needs to be cleared and restored.
      if @_needsClear
        @_needsClear = no
        sketch.popScreen()

    n?.draw() for n in @nodes

  # Accessors
  # ---------

  radius: -> no

  left: -> @x()
  top: -> @y()
  right: -> @w + @left()
  bottom: -> @h + @top()

  fillColor: (fc) ->
    @traceFill = Wrap.traceFillColor fc if fc?
    super fc

  # Public
  # ======

  clear: ->
    fill @fillColor()
    @drawBoundsRect()

  removeNode: (n) ->
    n.wrap = null
    @nodes.splice @nodes.indexOf(n), 1
    @updateNodeCount @nodes.length if @autoReplace

  updateNodeCount: (count, customNodeParams) ->
    count ?= @width() * @nodeDensity # Infer if needed.
    count = parseInt count
    # Contract if needed.
    if @nodes.length > count
      # Return nodes removed.
      nodes = (@nodes.pop() until @nodes.length is count)
      @nodeCount = @nodes.length
      return nodes
    # Or expand.
    hasGravity = !!(@forceOptions & PVector.GRAVITY)
    gravity = PVector.createGravity() if hasGravity
    nodes = []
    until @nodes.length is count
      nodeParams = _.extend {}, @nodeParams, customNodeParams,
        id: @nodes.length
        wrap: @
      n = new Node nodeParams
      n.p.randomize() if @layoutPattern is Wrap.RANDOM and not customNodeParams?.p?
      n.applyForce gravity if hasGravity
      n.applyForce f for f in @customForces
      n.cacheAcceleration()
      @nodes.push n
      nodes.push n
    @nodeCount = @nodes.length # Observable value for dat.GUI.
    return nodes

  updateNodeContainment: (n) ->
    shift = if @gravity is on then 1 else (1 - @entropy)

    if @containment is Wrap.REFLECTIVE
      do (v = n.v) =>
        if n.right() > @right()
          n.right @right()
          v.x *= -shift if v.x > 0
        else if n.left() < @left()
          n.left @left()
          v.x *= -shift if v.x < 0
        if n.bottom() > @bottom()
          n.bottom @bottom()
          v.y *= -shift if v.y > 0
        else if n.y() < @top()
          n.top @top()
          v.y *= -shift if v.y < 0

    else if @containment is Wrap.TOROIDAL
      contained = { x: yes, y: yes }

      if n.left() > @right() then n.right @left()
      else if n.right() < @left() then n.left @right()
      else contained.x = no
      if n.top() > @bottom() then n.bottom @top()
      else if n.bottom() < @top() then n.top @bottom()
      else contained.y = no

      if @drainAtEdge is on and (contained.x or contained.y)
        n.v.normalize()
        n.a.normalize()

  # Physics
  # -------

  applyNodeFriction: (n) ->
    friction = n.v.get()
    friction.normalize()
    friction.mult -1 * @frictionMag
    n.applyForce friction

  toggleForce: (f, toggled) ->
    return no unless toggled?
    @_allForces ?= @customForces
    isForceOption = typeof f is 'number'
    isForceName = f in @_allForces

    if isForceOption
      vec = PVector.createGravity() if f is PVector.GRAVITY
      if toggled is on then @forceOptions |= f else @forceOptions ^= f

    else if isForceName
      vec = @_allForces[f]
      if toggled is on then @customForces.push vec
      else @customForces.splice @customForces.indexOf(vec), 1

    if vec? then for n in @nodes
      n.applyForce vec, toggled
      n.cacheAcceleration()

  # Accessors
  # ---------

  getTraceFillColor: -> @traceFill

  ready: (r) -> @_ready = r if r?; @onReady() if r; @_ready

  # Callbacks
  # ---------

  mouseClicked: ->
    handled = _.any @nodes, (n) ->
      n.handleClick { mouseX, mouseY }
    , @
    return if handled
    # Add attractor node in empty space.
    @updateNodeCount @nodes.length + 1,
      attract: on
      p: [ mouseX, mouseY, 0 ]

  nodeMoved: (n) ->
    @applyNodeFriction n
    @updateNodeContainment n if @contain is on

  onNodeViewModeChange: (vm) ->
    if vm?
      @nodeParams.viewMode = vm
      n.viewMode = vm for n in @nodes
    @_needsClear = @nodeParams.viewMode is Node.LINE
    @draw()

  onReady: ->
    @onNodeViewModeChange()
    @log()