.generator
  .line
    .label 出力形式(file_type)
    .value
      %label
        %input.file_type{type: 'radio', name: 'file_type', value: 'svg'}
        SVG
      %label
        %input.file_type{type: 'radio', name: 'file_type', value: 'png', checked: true}
        png
  .line
    .label 色(color)
    .value
      %input.color{type: 'text', value: '#A5DEE4'}
  .line
    .label サイズ(size)
    .value
      %input.size{type: 'range', min: 0.1, max: 2, step: 0.1, value: 1}
  .line
    .label 「オ」のハネの大きさ(hane_size)
    .value
      %input.hane_size{type: 'range', min: 0, max: 0.4, step: 0.01, value: 0.1}
  .line
    .label 「ッ」の大きさ(xtsu_rate)
    .value
      %input.xtsu_rate{type: 'range', min: 0.1, max: 1, step: 0.1, value: 0.5}
  .line
    .label 線の幅(stroke_width)
    .value
      %input.stroke_width{type: 'range', min: 0.1, max: 2, step: 0.1, value: 1}
  .line
    .label ぼかしの強さ(blur)
    .value
      %input.blur{type: 'range', min: 0, max: 10, step: 0.1, value: 0}
  /.line
  /  .label blur2
  /  %input.blur2{type: 'range', min: 0, max: 10, step: 1, value: 0}
  .line
    .label 文字の影(shadow)
    .value
      %input.shadow_size{type: 'range', min: 0, max: 10, step: 1, value: 2}
  .line
    .label 影の色(shadow_color)
    .value
      %input.shadow_color{type: 'text', value: '#38ACB8'}
  .line
    .label 点線にする(dash)
    .value
      %input.dash{type: 'checkbox'}
  .line
    .label 点線の間隔(dasharray)
    .value
      %input.dasharray{type: 'text', value: '5 5'}
  .line
    .label アニメーション(animation)
    .value
      %input.animation{type: 'checkbox'}
  .line
    .label アニメーション時間(duration)
    .value
      %input.duration{type: 'number', value: 5}
  .line
    .label 逆向きに動かす(reverse)
    .value
      %input.reverse{type: 'checkbox'}
  .line
    .label SVGを表示する(show_svg)
    .value
      %input.show_svg{type: 'checkbox'}
.result
  .image
    .label 結果(result_image)
  .svg
    .label 結果(SVG)
View Compiled
html, body
  padding: 0
  margin: 0
  height: 100%
  width: 100%
body
  display: flex
.generator
  margin-top: 0.5em
  display: table
  > *
    display: table-row
    border-bottom: 1px solid whitesmoke
    color: #333333
    > *
      display: table-cell
      border-bottom: 1px whitesmoke solid
      padding: 0.2em
      &.label
        padding-left: 0.5em
      input
        border: none
        background: whitesmoke
.result
  margin-top: 0.5em
  padding-left: 1em
.svg
  display: none
  background: whitesmoke
svg = height = width = null
stroke_w = 10
color = ''
defaults =
  stroke_w: 10
  
$ ->
  width = $('body').outerWidth()
  height = $('body').outerHeight()  
  generate_o()
  
  $('.generator').on 'change', 'input.animation', ->
    if $(@).is(':checked')
      $('input.dash').prop('checked', true)
      $('input.file_type[value="svg"]').prop('checked', true)
      alert("アニメーションをONにしたため、点線はON、ファイル形式はSVGが選択されます。")
  .on 'change', 'input.show_svg', ->
    $('.svg').toggle($(@).is(':checked'))
  .on 'change', 'input', ->
    generate_o()
  
# path drawer
delay = 0
draw_path = (target, path) ->
  path = _.map path, (p, i) ->
    if p.length == 2 && i != 0
      [].concat(p).concat(p)
    else p
  path = target.append('path')
    .attr("d", "M" + path.join('Q'))
    .attr("stroke-width", stroke_w)
    .attr('stroke', color)
    .attr('fill', 'none')
  if $('input.dash').is(':checked')
    path.attr('stroke-dasharray', $('input.dasharray').val())
  if $('input.animation').is(':checked')
    animate = path.append('animate')
      .attr('attributeName', "stroke-dashoffset")
      .attr('dur', $('input.duration').val() + "s")
      .attr('repeatCount', "indefinite")
    if $('input.reverse').is(':checked')
      animate.attr('from', "0").attr('to', "100")
    else
      animate.attr('from', "100").attr('to', "0")
  return
  # animateMotionを使ったりとか・・・
  g = target.append('g').attr('class', 'animate')
  g.append('animateMotion')
    .attr('path', "M" + path.join('Q'))
    .attr('begin', delay + 's')
    .attr('dur', '3s')
    .attr('calcMode', 'linear')
    .attr('repeatCount', 'indefinite')
  g.append('circle')
    .attr('fill', 'white')
    .attr('r', 15)
    .attr('filter', 'url(#blur2)')

# オッSVGを生成する 
generate_o = ->
  $('.result').hide()
  stroke_w = defaults.stroke_w * $('input.stroke_width').val()
  shadow_size = parseFloat($('input.shadow_size').val())
  blur_size = $('input.blur').val()
  hane_size = $('input.hane_size').val()
  color = $('input.color').val()
  rate = $('input.size').val()
  xtsu_rate = $('input.xtsu_rate').val()
  $('svg').remove()
  svg = d3.select('.svg').append('svg')
  ow = 100 * rate
  oh = 100 * rate
  tw = ow * xtsu_rate * 1.2
  th = oh * xtsu_rate
  svg
    .attr('width', ow + tw + (stroke_w * 2) + (shadow_size * 5))
    .attr('height', oh + (stroke_w * 2.5) + (shadow_size * 5))
  
  defs = svg.append('defs')
  blur = defs.append('filter').attr('id', 'blur')
  blur.append('feGaussianBlur').attr('stdDeviation', blur_size)
  
  # 使わなかった
  #blur2 = defs.append('filter').attr('id', 'blur2')
  #  .attr('width', 100)
  #  .attr('height', 100)
  #  .attr('filterUnits', "userSpaceOnUse")
  #  .attr('x', -50)
  #  .attr('y', -50)
  #blur2.append('feGaussianBlur').attr('stdDeviation', $('input.blur2').val())
      
  # 文字のshadow
  shadow = defs.append('filter')
    .attr('id', 'shadow')
    .attr('filterUnits', "userSpaceOnUse")
  shadow.append('feGaussianBlur')
    .attr('in', "SourceAlpha")
    .attr('stdDeviation', shadow_size)
    .attr('result', "blur")
  shadow.append('feOffset')
    .attr('in', "blur")
    .attr('dx', shadow_size)
    .attr('dy', shadow_size)
    .attr('result', "offsetBlur")
  shadow.append('feFlood')
    .attr('flood-color', $('input.shadow_color').val())
    .attr('flood-opacity', "1")
    .attr('result', "offsetColor")
  shadow.append('feComposite')
    .attr('in', "offsetColor")
    .attr('in2', "offsetBlur")
    .attr('operator', "in")
    .attr('result', "offsetBlur")
  shadow_fe = shadow.append('feMerge')
  shadow_fe.append('feMergeNode')
    .attr('in', "offsetBlur")
  shadow_fe.append('feMergeNode')
    .attr('in', "SourceGraphic")
  container = svg.append('g').attr('class', 'shadow_container').attr('filter', 'url(#shadow)')
  container.attr('transform', 'translate(' + [stroke_w + (shadow_size / 2), (stroke_w + shadow_size)] + ')')
  container = container.append('g').attr('class', 'blur_container').attr('filter', 'url(#blur)')
  og = container.append('g').attr('class', 'o')
  tg = container.append('g').attr('class', 't')
  
  # オの設定
  op =
    h: oh
    w: ow
    x: ow * 0.7
    y: ow * 0.2
    hane:
      x: -(ow * hane_size + stroke_w)
      y: -(oh * hane_size + stroke_w)
  tp =
    h: th
    w: tw
    x: tw * 0.7
    y: tw * 0.4
  og.attr('transform', "translate(" + [shadow_size , shadow_size] + ")")
  tg.attr('transform', "translate(" + [shadow_size + stroke_w + op.w, shadow_size + oh - tp.h] + ")")
  # オの描画
  # 横線
  draw_path(og, [
    [0, op.y]
    [op.w, op.y]
  ])
  # 縦線 + ハネ
  draw_path(og, [
    [op.x, 0]
    [op.x, op.h + op.hane.y]
    [op.x - (op.hane.x / 10), op.h - op.hane.y, op.x + op.hane.x, op.h + op.hane.y]
  ])
  # ナナメ線
  draw_path(og, [
    [op.x, op.y]
    [op.x / 2, op.y + ((op.h - op.y) * 0.75), 0, op.h * 0.9]
  ])
  
  # ッの描画
  # 点1
  draw_path(tg, [
    [0, tp.h * 0.1]
    [tp.w * 0.1, tp.y + (tp.h * 0.1)]
  ])
  # 点2
  draw_path(tg, [
    [tp.w * 0.3, 0]
    [tp.w * (0.3 + 0.1), tp.y]
  ])

  # ナナメ線
  draw_path(tg, [
    [tp.x, tp.h * 0.1]
    [tp.w * 0.8, tp.h * 0.8, tp.w * 0.1, tp.h]
  ])
  
  _.each $('svg .animate'), (g) ->
    $(g).closest('g.o, g.t').append(g)

  convert_img()
  $('.result').fadeIn()
    
convert_img = ->
  $('.image img').remove()
  svg = document.querySelector('svg')
  svgData = (new XMLSerializer).serializeToString(svg)
  canvas = document.createElement('canvas')
  canvas.width = svg.width.baseVal.value
  canvas.height = svg.height.baseVal.value
  ctx = canvas.getContext('2d')
  image = new Image

  image.onload = ->
    $('.image').append(image)
    if $('input[name="file_type"]:checked').val() == 'png'
      $(image).hide()
      ctx.drawImage(image, 0, 0 )
      $('<img>').attr('src', canvas.toDataURL("image/png")).appendTo('.image').attr(title: 'a', alt: 'b')
  image.src = 'data:image/svg+xml;charset=utf-8;base64,' + btoa(unescape(encodeURIComponent(svgData)))
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js