タイミングチャート清書サービス の履歴(No.3)

更新


公開メモ

概要

こちらで紹介したタイミングチャート清書スクリプト と同等の物を javascript で実装し、オンラインで使えるようにしました。

  • コードを書けばほぼリアルタイムで清書結果を確認できます
  • 清書したタイミングチャートは SVG または PNG 形式でダウンロードできます

設置アドレス

こちらです: http://output.jsbin.com/zacubomibi

ソースコード

LANG:html(linenumber)
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script src="https://code.jquery.com/jquery-2.1.4.js"></script>
  <script src="https://rawgit.com/eligrey/FileSaver.js/master/FileSaver.js"></script>
  <script src="https://rawgit.com/eligrey/canvas-toBlob.js/master/canvas-toBlob.js"></script>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <h1>Timing Chart Formatter</h1>
  <h2>Source</h2>
  <textarea id="source" cols="60" rows="10">clock	_~_~_~_~_~_~_~_~_~_~_
data	=?====X=DATA========X=?====
valid	_____~~~~~~~~~~______
ready	_____________[~~]______</textarea>
  <h2>Result</h2>
  <input type="button" value="Download as SVG" id="as_svg" />
  <input type="button" value="Download as PNG" id="as_png" /> Background: <input id="background" value="white" style="width:60px" />
  <br />
  <br />
  <div id="result"></div>
</g>
</svg>
</div>
</body>
</html>
LANG:coffeescript(linenumber)
class SvgPath
  constructor: (style)->
    @segments = []
    @style = style

  draw: (x1, y1, x2, y2)->
    @segments.push([x1,y1,x2,y2])

  svg: ->
    path = ("M#{s[0]},#{s[1]}L#{s[2]},#{s[3]}" for s in @segments).join("")
    """<path #{@style} d="#{path}" />\n"""

class TimeLine
  # : - ~ _ = 
  @transitions = [
    '          ', # :
    '  - 1 4 14', # -
    '  2 ~ / ~/', # ~
    '  3 ` _ _`', # _
    '  23`~/_= ', # =
    '  23`~/_=/', # /
    '  23`~/_=`', # \
    '  23`~/_X ', # X
    '  23`~/_=X', # *
  ]

  @transitionLines =
    ' ' : []
    '~' : [[1,1]]
    '_' : [[0,0]]
    '=' : [[1,1],[0,0]]
    'X' : [[1,0],[0,1]]
    '`' : [[1,0]]
    '/' : [[0,1]]
    '1' : [[1,0.5]]
    '2' : [[0.5,1]]
    '3' : [[0.5,0]]
    '4' : [[0,0.5]]
    '-' : [[0.5,0.5]]

  #                :  -     ~   _   =
  @stateLines = [[],[0.5],[1],[0],[0,1]]

  #
  @codes = ':-~_=/\\X*'
  
  constructor: (config, y)->
    @config = config
    @x = config.w_caption
    @y = y
    @path = new SvgPath(config.signal_style)
    @current = 0
    @crosses = []
    @strings = []
    @grids   = []
    @highlights = []
    
  ys: (s)->
    @y + (1-s) * @config.h_line
  y0: ->
    @y + @config.h_line
  y1: ->
    @y
  yz: ->
    @y + @config.h_line/2.0
  xh: ->
    @x + @config.w_transient/2.0
  xt: ->
    @x + @config.w_transient
  xr: ->
    @x + @config.w_transient + @config.w_hold
  
  parse: (line)->
    while line != ''
      if maches = /^\s+/.exec(line)
        ;
      else if maches = /^\|/.exec(line)
        @grids.push [@xh(), @config.grid_style]
      else if maches = /^\[/.exec(line)
        if @highlights.length==0 or Array.isArray(@highlights[@highlights.length-1])
          @highlights.push @xh()
      else if maches = /^\]/.exec(line)
        if @highlights.length>0 and not Array.isArray(@highlights[@highlights.length-1])
          @highlights[@highlights.length-1] = 
            [@highlights[@highlights.length-1], @xh(), @config.highlight_style]
      else if matches = /^([:\-~_=\/\\X*])/.exec(line)
        @addState(matches[1])
      else if matches = /"(([^"]|"")+)"/.exec(line)
        @addString(matches[1])
      else if matches = /([^:\-~_=\/\\X*\|\]\[]+)/.exec(line)
        @addString(matches[1])
      line = line.substr(matches[0].length, line.length-matches[0].length)
    
    @processStrings() + @path.svg()

  addState: (c)->
    s = TimeLine.codes.indexOf(c)
    crosses = @drawTransition(s)
    s = 4 if s > 4
    @drawState(s)
    @crosses.push([@x, crosses]) if crosses!=''
    if (@current == 0 and s != 0) or (@current != 0 and s == 0)
      @crosses.push([@x, '|'])
    @current = s
    @x = @xr()

  addString: (s)->
    @strings.push [@crosses.length, s.replace(/^\s+|\s+$/g, '')]

  drawTransition: (s)->
    crosses = ''
    transitions = TimeLine.transitions[s].substr(2*@current,2)
    for i in [0..transitions.length-1]
      crosses += @drawTransitionSub(transitions[i])
    crosses
  
  drawTransitionSub: (c)->
    crosses = ''
    for line in TimeLine.transitionLines[c]
      @path.draw(@x,  @ys(line[0]), @xt(), @ys(line[1]))
      crosses += c if line[0] != line[1]
    crosses

  drawState: (s)->
    for line in TimeLine.stateLines[s]
      @path.draw(@xt(), @ys(line), @xr(), @ys(line))
  
  processStrings: ->
    svg = []
    @crosses.push [@x,'|']
    for string in @strings
      y0 = @ys(0)
      y1 = @ys(1)
      yz = @ys(0.5)
      x1 = @crosses[string[0]-1][0]
      x1t = x1 + @config.w_transient
      x1h = x1 + @config.w_transient/2.0
      x1r = x1t + @config.w_hold
      x2 = @crosses[string[0]  ][0]
      x2t = x2 + @config.w_transient
      x2h = x2 + @config.w_transient/2.0
      x2r = x2t + @config.w_hold

      if string[1]=='?'
        path= ["M#{x1t},#{y1}H#{x2}"]
        path.push switch @crosses[string[0]][1]
          when '|'  then ""
          when 'XX' then "L#{x2h},#{yz}"
          when '/'  then "H#{x2t}"
          when '`'  then "L#{x2t},#{y0}"
          when '23' then "H#{x2t}L#{x2},#{yz}L#{x2t},#{y0}"
          when '14' then "L#{x2t},#{yz}"
          when '1'  then "L#{x2t},#{yz}V#{y0}"
          when '2'  then "H#{x2t}L#{x2},#{yz}"
          when '3'  then "V#{yz}L#{x2t},#{y0}"
          when '4'  then "H#{x2t}V#{yz}"
        path.push "L#{x2},#{y0}H#{x1t}"
        path.push switch @crosses[string[0]-1][1]
          when '|'  then ""
          when 'XX' then "L#{x1h},#{yz}"
          when '/'  then "H#{x1}"
          when '`'  then "L#{x1},#{y1}"
          when '23' then "L#{x1},#{yz}"
          when '14' then "H#{x1}L#{x1t},#{yz}L#{x1},#{y1}"
          when '1'  then "V#{yz}L#{x1},#{y1}"
          when '2'  then "H#{x1}V#{yz}"
          when '3'  then "L#{x1},#{yz}V{y1}"
          when '4'  then "H#{x1}L#{xt},#{yz}"
        path.push "Z"
        svg.push """\n<path stroke="none" d="#{path.join('')}" #{@config.notcare_style}/>"""

      sanitized = string[1].replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
      svg.push """<text x="#{(x1h + x2h)/2.0}" y="#{@ys(0)-1.5}" text-anchor="middle" """ +
               """font-size="#{@config.h_line}" #{@config.signal_font}>#{sanitized}</text>\n"""

    svg.join("")
    
class TimingChart
  @config:
    scale:          1.0
    margin:         10
    w_caption:      40
    w_hold:         10
    w_transient:    2
    h_line:         10
    h_space:        10
    signal_style:   'stroke-linecap="round" stroke-width="0.6" stroke="black" fill="none"'
    grid_style:     'stroke-linecap="round" stroke-width="0.6" stroke="red" fill="none"'
    highlight_style:'stroke="none" fill="#ff8"'
    notcare_style:  'fill="#ccc"'
    rotate:         0
    caption_font:   'fill="black" font-family="Helvetica"'
    signal_font:    'fill="black" font-family="Helvetica"'

  constructor: (config= {})->
    @config= {}
    @setConfig(TimingChart.config)
    @setConfig(config)

  setConfig: (config)->
    @config[k]= v for own k,v of config

  parse: (source)->
    @svg= []
    @grids= []
    @highlights= []
    @y= -1
    @x_max= @config.w_caption
    source= source.replace(/^\n+/, '')
    source= source.replace(/\n+$/, '')
    for line in source.split("\n")
      @parseLine(line)
    @processGrids()
    @processHighlights()
    @formatSVG(source)

  formatSVG: (source)->
    m= @config.margin
    w= (@x_max + 2 * m) * @config.scale * 1.3
    h= (@y     + 2 * m) * @config.scale * 1.3
    @width = w
    @height = h
    """
    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
      width="#{w}px" height="#{h}px" viewBox="#{-m} #{-m} #{@x_max+2*m} #{@y+2*m}" version="1.1">
    <![CDATA[
    #{source.replace(/\]\]\>/g, ']]&gt;')}
    ]]>
    <g>
    #{@svg.join("\n")}
    </g>
    </svg>
    """

  parseLine: (line)->
    return if line[0] == '#'  #### comment

    if line[0]=='@'           #### configuration
      if !(matches = /^@([^\s]+)[\s]+([^\s].*)$/.exec(line))
        throw new SyntaxError("Illegal Line: #{line}")
      if $.isNumeric(@config[matches[1]])
        @config[matches[1]] = Number(matches[2])
      else
        @config[matches[1]] = matches[2]
      return

    if line[0] == '%'         #### free string
      if !(matches = /^%(-?[\d\.]+)\s+(-?[\d\.]+)\s+?(.*)$/.exec(line))
        throw new SyntaxError("Illegal Line: #{line}")
      @svg.push("""<text x="#{matches[1]}" y="#{matches[2]}" text-anchor="middle" """ +
                """font-size="#{@config.h_line}" #{@config.signal_font}>#{matches[3]}</text>""")
      return
    
    if @y<0
      @y= 0
    else
      @y+= @config.h_space

    line= line.replace(/\s*$/, '')
    return if line==''        #### empty line

    if !(matches = /^([^\s]+)[\s]+([^\s].*)$/.exec(line))
      throw new SyntaxError("Illegal Line: #{line}")
    
    @formatCaption(matches[1])
    @formatTimeline(matches[2])
    @y+= @config.h_line

  formatCaption: (caption)->
    sanitized = caption.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
    @svg.push(
      """<text x="#{@config.w_caption-5}" y="#{@y+@config.h_line-1.5}" text-anchor="end" """+
      """font-size="#{@config.h_line}" #{@config.caption_font}>#{sanitized}</text>"""
    )

  formatTimeline: (line)->
    tline= new TimeLine(@config, @y)
    @svg.push tline.parse(line)
    @x_max= tline.x if tline.x > @x_max
    for g in tline.grids
      @grids.push g
    for h in tline.highlights
      @highlights.push h 
  
  processGrids: ->
    t = -@config.margin/2.0
    b = @y+@config.margin/2.0
    for g in @grids
      @svg.push """<path d="M#{g[0]},#{t}V#{b}" #{g[1]} />"""
    
  processHighlights: ->
    t = -@config.margin/2.0
    b = @y+@config.margin/2.0
    for h in @highlights
      if Array.isArray(h)
        @svg.unshift """<path d="M#{h[0]},#{t}V#{b}H#{h[1]}V#{t}Z" #{h[2]} />"""

download= (dataUrl, filename)->
  form = $('<form>')
  form.action = dataUrl
  form.target = '_blank'
  form.submit()
                  
lastSource = ''
update = ->
  source = $('#source').val()
  return if lastSource==source
  lastSource = source
  
  tchart= new TimingChart()
  svg= tchart.parse(source)
  $('#result').html(svg)

$ ->
  $('#as_svg').on 'click', ->
    update()
    blob = new Blob([$('#result').html()], {type: "image/svg+xml"});
    saveAs(blob, 'timing-chart.svg')

  $('#as_png').on 'click', ->
    source = $('#source').val()
    tchart= new TimingChart()
    svg= tchart.parse(source)

    canvas = document.createElement('canvas')
    canvas.width = tchart.width
    canvas.height = tchart.height

    # http://nmi.jp/archives/223
    img = new Image()
    img.onload = ->
      ctx = canvas.getContext("2d")
      ctx.fillStyle = $('#background').val()
      ctx.fillRect(0, 0, canvas.width, canvas.height)
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
      url = canvas.toDataURL('image/png')
      link = document.createElement('a')
      link.href = url
      link.download = 'timing-chart.png'
      link.click()

    img.src = "data:image/svg+xml;base64," + btoa(svg)

  setInterval(update, 100)

コメント・質問





Counter: 70620 (from 2010/06/03), today: 24, yesterday: 19