Save Expand icon

Ron Valstar
front-end developer

Experiment: touches

One of my first doodles coded entirely on an iPad: circles and stars are drawn in advance and cached. Then given a velocity and some kind of gravity with an inverse square.

import experiment from './base'
import color from '../math/color'
import vector from '../math/vector'
import {resize,animate,dragstart,drag,dragend} from '../signal/signals'
import {createGradient,drawCircle,drawPolygon,drawPolygram} from '../utils/canvasrenderingcontext2d'

  let inst = experiment('touches',{
    init
    ,exit
    ,handleAnimate
    ,handleResize
  })
  ,zuper = inst.zuper
  //
  // private variables
  ,target
  //
  ,w,h
  //
  ,deltaT
  //
  ,canvas
  ,context
  ,canvasTemp = document.createElement('canvas')
  //
  ,rings = {}
  ,ringsPool = []


function init(_target){
  target = _target
  canvas = zuper.init(_target)
  context = inst.context
  handleResize()
  //
  for (let i=0;i<20;i++) ringsPool.push(ring({pos:0}))
  //
  let nr = 0
    ,sizeMax = 125
    ,fnSpawn = function(){
      let fakeID = 'a'+nr++
        ,isHorVer = Math.random()<0.5
        ,fakeTouch = drag.touch(fakeID,vector(
          !isHorVer?-sizeMax+(w+2*sizeMax)*Math.random():(Math.random()<0.5?-sizeMax:w+sizeMax)
          ,isHorVer?-sizeMax+(h+2*sizeMax)*Math.random():(Math.random()<0.5?-sizeMax:h+sizeMax)
        ))
      fakeTouch.last.add(vector(9*(Math.random()-0.5),9*(Math.random()-0.5)))
      rings[fakeID] = getRing(fakeTouch)
      rings[fakeID].setSize(1).kill()
    }

  //
  setInterval(fnSpawn,1999)
  for (let i=0;i<5;i++) setTimeout(fnSpawn,i*40)
  //
  target.appendChild(canvas)
  //
  drag.stopPageScroll = false
  animate.add(handleAnimate)
  resize.add(handleResize)
  dragstart.add(function(added){//,touches){
    added.forEach((otouch,id)=>rings[id] = getRing(otouch))
  })
  dragend.add(function(del){//,touches){
    del.forEach((otouch,id)=>rings[id].kill())
  })
  //
  return canvas
}

function exit() {
  //todo:remove drag
  zuper.exit()
}

// protected methods

function handleAnimate(_deltaT,millis,frames){
  deltaT = _deltaT
  for (let ring in rings) rings[ring].update(context,deltaT,millis,frames)
}

function handleResize(){
  w = target.clientWidth
  h = target.clientHeight
  canvasTemp.width = canvas.width
  canvasTemp.height = canvas.height
  canvasTemp.getContext('2d').drawImage(canvas,0,0)
  canvas.width  = w
  canvas.height = h
  context.drawImage(canvasTemp,0,0)
}

function getRing(touch){
  return ringsPool.length?ringsPool.shift().reset(touch):ring(touch)
}

function ring(touch){
  let touchPos = touch.pos
    ,rotation = 1
    ,sizeMin = 1
    ,sizeMax = 250
    ,sizeMaxH = sizeMax/2
    ,sizeDead
    ,size = sizeMin
    ,ageMax = 333
    ,born = Date.now()
    ,died = 0
    ,diedMax = 15000
    ,dead
    ,typeMax = 8
    ,type = (typeMax*Math.random())<<0
    ,sprite = (function(){
      let  sprite = document.createElement('canvas')
        ,spriteContext = sprite.getContext('2d')
        ,sizeMaxHalf = sizeMax/2
        ,colorSprite = color.apply(color,[192+Math.random()*63<<0,Math.random()*64<<0,Math.random()*255<<0].sort(function(){return Math.random()<0.5?1:-1;}))
        ,colorShade = colorSprite.clone().multiply(0.6).rgba(0.4)
        ,fill = createGradient.call(spriteContext,type!==0,sizeMaxHalf
        // ,fill = crc.createGradient(spriteContext,type!==0,sizeMaxHalf
          ,0		,colorSprite.clone().multiply(0.9)
          ,0.6999	,colorSprite
          ,0.7	,colorShade
          ,1		,colorSprite.clone().multiply(0.7).rgba(0)
        )

      sprite.width = sprite.height = sizeMax
      spriteContext.translate(sizeMaxH,sizeMaxH)
      spriteContext.fillStyle = fill
      if (type===0)		drawCircle.call(spriteContext,0,0,sizeMaxH)
      else if (type<5)	drawPolygon.call(spriteContext,0,0,sizeMaxH,type+2)
      else				drawPolygram.call(spriteContext,0,0,sizeMaxH,1-2.2/(type-2),type-2)
      // if (type===0)		crc.drawCircle(spriteContext,0,0,sizeMaxH)
      // else if (type<5)	crc.drawPolygon(spriteContext,0,0,sizeMaxH,type+2)
      // else				crc.drawPolygram(spriteContext,0,0,sizeMaxH,1-2.2/(type-2),type-2)
      return sprite
    })()
    ,animateSignal

  function updateView(context,x,y,scale,rotation,alpha) {
    if (alpha!==1) context.globalAlpha = alpha
    context.translate(x,y)
    context.scale(scale,scale)
    context.rotate(rotation)
    context.drawImage(sprite,-sizeMaxH,-sizeMaxH)
    context.setTransform(1,0,0,1,0,0)
    if (alpha!==1) context.globalAlpha = 1
  }
  function update(context,deltaT,millis){//,frames) {
    let isAlive = died===0
      ,age = millis-(isAlive?born:died)
      ,maxAge = isAlive?ageMax:diedMax
      ,sizeFrom = isAlive?sizeMin:sizeDead
      ,sizeTo = isAlive?sizeMax:sizeMin
    if (age<maxAge) {
      let ageOne = age/maxAge
      size = sizeFrom + ageOne*(sizeTo-sizeFrom)
      if (!isAlive) {
        let centerDistance = vector(w/2,h/2).subtract(touchPos)
          ,centerDistanceSize = centerDistance.size()
        dead.add(centerDistance.multiplyNumber(1/(centerDistanceSize*centerDistanceSize)))
        touch.pos.add(dead.clone().multiply(deltaT))
        rotation = rotation+0.01*deltaT*dead.size()
        if   (touchPos.getX()<-9999
          ||touchPos.getX()>w+9999
          ||touchPos.getY()<-9999
          ||touchPos.getY()>h+9999) fnDie()
      }
    } else if (!isAlive) {
      fnDie()
    }
    let sizeOne = size/sizeMax
      ,fade = 0.1
    updateView(
      context
      ,touchPos.getX()
      ,touchPos.getY()
      ,sizeOne/2+0.5
      ,rotation
      ,sizeOne<fade?sizeOne/fade:1
    )
  }
  function moveTo(x,y){
    if (animateSignal) animatecancel()
    let  starX = touchPos.getX()
      ,starY = touchPos.getY()
      ,distX = x-starX
      ,distY = y-starY
    animateSignal = animate.during(
        800
        ,f=>updateView(starX+f*distX,starY+f*distY)
        ,kill
    )
  }
  function setSize(f){
    let ff = f>1?1:f<0?0:f
    size = sizeMax*ff
    born = Date.now()-ageMax*ff
    return returnValue
  }
  function kill(){
    if (died===0) {
      died = Date.now()-(1-size/sizeMax)*diedMax
      sizeDead = size
      dead = touch.pos.clone().subtract(touch.last).divide(deltaT)
    }
  }
  function fnDie(){
    ringsPool.push(rings[touch.id])
    delete rings[touch.id]
  }
  let returnValue = {
    update
    ,moveTo
    ,setSize
    ,kill
    ,toString: ()=>'[object ring '+touch.id+']'
  }
  returnValue.reset = function(tch){
    touch = tch
    touchPos = touch.pos
    born = Date.now()
    died = 0
    return returnValue
  }
  return returnValue
}

export const touches = inst.expose

comment send comment