Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <canvas id="main-canvas"></canvas>
<div id="instructions">Press <kbd>C</kbd> or <kbd>F</kbd></div>
<div id="custom-text">
  <textarea id="text-input" placeholder="Enter Text"></textarea>
  <button id="apply-button">Apply</button>
</div>
              
            
!

CSS

              
                @import url('https://fonts.googleapis.com/css?family=Amatic+SC:400');

html, body {
  touch-action: manipulation;
}
body {
  font-family: sans-serif;
  background: linear-gradient(135deg, #ecaeb5, #ffecb1);
}
*:focus {
  outline: none;
  border-radius: 2px;
  box-shadow: 0 0 0 3px fade(#fee12a, 50%);
}

#main-canvas {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
}

#instructions {
  font-size: 24px;
  opacity: 0.5;
  
  kbd {
    font-family: sans-serif;
    border: 1px solid currentColor;
    padding: 0 0.2em;
    border-radius: 2px;
  }
}

#custom-text {
  position: fixed;
  background: rgba(0, 0, 0, 0.1);
  font-family: sans-serif;
  bottom: 0;
  color: #fff;
  border-top-left-radius: 2px;
  border-top-right-radius: 2px;
  transition: transform 0.3s;
  padding: 0 1em;
  
  &:not(:hover) {
    transform: ~"translateY(calc(100% - 1.4em))";
  }
  
  &::before {
    content: 'Custom Text';
    display: block;
    text-align: center;
    padding: 0.2em;
  }
  
  #text-input {
    font: inherit;
    background: none;
    border: none;
    color: inherit;
  }
  #apply-button {
    font: inherit;
    display: block;
    background: none;
    border: none;
    width: 100%;
    color: #fee12a;
    padding: 0.4em;
    
    &:active {
      color: darken(#fee12a, 10%);
    }
  }
}
              
            
!

JS

              
                canvas = document.querySelector '#main-canvas'
ctx = canvas.getContext '2d'

width = 0
height = 0
scale = 1

updateSize = ->
  width = innerWidth
  height = innerHeight
  scale = devicePixelRatio
  canvas.width = width * scale
  canvas.height = height * scale

updateSize()
window.addEventListener 'resize', updateSize

# # # # # # # # # # # #

font = '24px Amatic SC, sans-serif'
padding = -1

mctx = document.createElement('canvas').getContext '2d'
measureText = (font, text) ->
  mctx.font = font
  mctx.measureText(text).width

textBubbles = []
class TextBubble extends Trace.Object
  constructor: ->
    super()
    
    textBubbles.push this
    
    @text = new Trace.AnimatedString ''
    @chars = new Trace.AnimatedNumber 0
    @visibility = new Trace.AnimatedNumber 1
    @width = new Trace.AnimatedNumber 0
    @width.interpolatorSettings = spring: [900, 70]
    @width.interpolator = Trace.AnimatedNumber.springInterpolator
    @height = new Trace.AnimatedNumber 0
    @height.interpolatorSettings = spring: [900, 70]
    @height.interpolator = Trace.AnimatedNumber.springInterpolator
    @positions = []
    @widths = []
    @offsets = []
    @springs = []
    @timer = 0
    
    @typeset 0
  
  typeset: (t) ->
    @positions = []
    @widths = []
    @offsets = []
    text = @text.getValue t, 0
    @springs = [] if text.length isnt @springs.length
    width = 0
    posX = 0
    posY = 0
    for char, i in text
      charWidth = measureText font, char
      @positions.push [posX, posY]
      posX += padding + charWidth
      if char is ' '
        posX += 3
        if posX > 180 || posX + measureText(text.substr(i).split(/\s/)[0]) > 180
          posX = 0
          posY += 22
      @widths.push charWidth
      @offsets.push Math.random()
      if not @springs[i]
        spring = new Trace.AnimatedNumber 0
        spring.interpolatorSettings =
          spring: [900, 35]
        spring.interpolator = Trace.AnimatedNumber.springInterpolator
        @springs.push spring
      width = posX if posX > width
    @width.defaultValue = width
    @height.defaultValue = posY
  
  drawSelf: (ctx, transform, t, dt) ->
    Trace.Utils.setTransformMatrix ctx, transform
    
    @timer += dt
    text = @text.getValue t, dt
    chars = @chars.getValue t, dt
    visibility = @visibility.getValue t, dt
    
    if text isnt @lastText
      @typeset t
      @lastText = text

    width = @width.getValue t, dt
    height = @height.getValue t, dt
    
    ctx.globalAlpha = @opacity.getValue t, dt
      
    ctx.fillStyle = '#000'
    ctx.beginPath()
    top = -height / 2 - 14
    left = -width / 2 - 10
    right = width / 2 + 10
    bottom = height / 2 + 14
    
    state = Math.floor 3 * (@timer % 1)
    da = 10 + 5 * state
    db = 8 + 6 * Math.sin state
    dc = 1 + 0.015 * Math.cos state
    dd = 14 - 2 * state
    de = 10 + 2 * state
    
    ctx.moveTo left + dc, top
    ctx.bezierCurveTo left + da, dc * top * 2, right - dd, top * 2, right, top
    ctx.bezierCurveTo right + 5, top + 10, right + 5, bottom - 10, right, bottom
    ctx.bezierCurveTo right - db, bottom * 2, left + 10, bottom * 2, left, bottom
    ctx.bezierCurveTo left - 5, bottom - de, left - 5, top + 10, left + dc, top
    ctx.fill()
    
    ctx.fillStyle = '#fff'
    ctx.font = font
    ctx.textBaseline = 'middle'
    ctx.textAlign = 'center'
    ctx.translate -width / 2, -height / 2

    for char, i in text
      cscale = 1
      if i + 1 > Math.floor chars
        cscale = 0
      cvis = 1 - ((1 - visibility) * (1 + @offsets[i]))
      cvis = Math.max 0, cvis
      @springs[i].defaultValue = cscale
      value = @springs[i].getValue t, dt
      value = Math.max 0, value
      value *= cvis
      ctx.save()
      ctx.globalAlpha *= value
      ctx.translate @positions[i]...
      ctx.translate @widths[i] / 2, 0
      ctx.scale 1 - 0.5 * (1 - value), 1 - 0.25 * (1 - value)
      ctx.rotate (Math.floor(3 * ((@timer + @offsets[i]) % 1)) - 1.5) / 70
      ctx.fillText char, 0, 0
      ctx.restore()

timeline = new Trace.Timeline ctx
timeline.run()
window.t = timeline

viewport = new Trace.Viewport
timeline.addChild viewport
viewport.canvasWidth = width
viewport.canvasHeight = height
viewport.canvasScale = scale
window.addEventListener 'resize', ->
  viewport.canvasWidth = innerWidth
  viewport.canvasHeight = innerHeight

viewport.width = 150
viewport.height = 100

bubbleForLines = (lines, time = 0) ->
  bubble = new TextBubble
  markers = []
  bubble.transform.scaleX.addKey time, 0
  bubble.transform.scaleX.addKey time + 0.25, 1, Trace.Easing.easeOutExpo
  bubble.transform.scaleY.addKey time, 0
  bubble.transform.scaleY.addKey time + 0.25, 1, Trace.Easing.easeOutExpo
  t = time
  for line in lines
    bubble.text.addKey t, line
    bubble.visibility.addKey t, 1, Trace.Easing.step
    bubble.chars.addKey t, 0, Trace.Easing.step
    for char, i in line
      if char is '.'
        t += 0.1
      else
        t += 0.025
      bubble.chars.addKey t, i + 1
    markers.push t
    bubble.visibility.addKey t, 1
    t += 0.25
    bubble.visibility.addKey t, 0, Trace.Easing.easeInExpo
    bubble.chars.addKey t, line.length
    t += 0.05
  t -= 0.25
  bubble.transform.scaleX.addKey t, 1
  bubble.transform.scaleY.addKey t, 1
  t += 0.25
  bubble.transform.scaleX.addKey t, 0, Trace.Easing.easeInExpo
  bubble.transform.scaleY.addKey t, 0, Trace.Easing.easeInExpo
  
  [bubble, markers, t]

container = new Trace.Object
[bubble, markers, length] =  bubbleForLines [
  'life goes on'
  'it feels so long'
  'but i report'
  'life is too short'
]
container.addChild bubble
timeline.markers.set t, 0 for t in markers
viewport.addChild container
container.addKeys
  transform:
    translateX: 75
    translateY: 50

timeline.duration = length + 0.5
timeline.loop = yes
try
  timeline.play()
catch err
  timeline.paused = false
  console.log err
  console.log timeline.play

keyDown = false
window.addEventListener 'keydown', (e) ->
  return if e.target instanceof HTMLTextAreaElement
  if e.key is 'f' or e.key is 'c'
    return if keyDown
    keyDown = true
    timeline.play()
    timeline.playbackRate = 1.5

window.addEventListener 'keyup', (e) ->
  return if e.target instanceof HTMLTextAreaElement
  if e.key is 'f' or e.key is 'c'
    keyDown = false
    timeline.playbackRate = 1

canvas.addEventListener 'touchstart', (e) ->
  return if keyDown
  e.preventDefault()
  keyDown = true
  timeline.play()
  timeline.playbackRate = 1.5

touchEndHandler = (e) ->
  e.preventDefault()
  keyDown = false
  timeline.playbackRate = 1

canvas.addEventListener 'touchend', touchEndHandler
canvas.addEventListener 'touchcancel', touchEndHandler

new FontFaceObserver('Amatic SC').load().then ->
  console.log 'loaded'
  bubble.typeset timeline.currentTime for bubble in textBubbles

document.querySelector('#apply-button').addEventListener 'click', (e) ->
  content = document.querySelector('#text-input').value
  container.removeChild bubble
  [bubble, markers, length] = bubbleForLines content.split '\n'
  container.addChild bubble
  timeline.currentTime = 0
  timeline.duration = length + 0.5
  timeline.markers = new Map() # probably shouldn't do that
  timeline.markers.set t, 0 for t in markers
  timeline.play()
              
            
!
999px

Console