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="canvas"></canvas>
<button id="clear">clear</button>
<div class="message">Change the constants at the top of the JS file to create different effects!</div>
<a class="link" href="https://twitter.com/kylestetz" target="_blank">by @kylestetz</a>
              
            
!

CSS

              
                @accent: #257C67;
@hover: #D66632;

body, html {
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: 'Andale Mono', monospace;
  font-size: 12px;
  overflow: hidden;
}

#canvas {
  width: 100%;
  height: 100%;
  cursor: default;
}

#clear {
  position: fixed;
  top: 10px;
  left: 10px;
  padding: 4px;
  
  font-family: 'Andale Mono', monospace;
  color: @accent;
  
  outline: none;
  border: 1px solid @accent;
  background-color: white;

  cursor: pointer;
  user-select: none;

  &:hover {
    border-color: @hover;
    color: @hover;
  }
}

.message {
  position: fixed;
  left: 10px;
  bottom: 10px;
  max-width: 50%;
  color: fade(black, 30%);
  user-select: none;
  cursor: default;
}

.link {
  display: block;
  position: fixed;
  bottom: 10px;
  right: 10px;

  color: @accent;
  text-decoration: none;
  user-select: none;

  &:hover {
    color: @hover;
  }
}
              
            
!

JS

              
                // CONSTANTS: CHANGE THESE
// Number of reflected sides
const sides = 7;
// Radius of the dot we're drawing.
const radius = 5;
// (100 - ?) The timing, in ms, of the feedback delay.
// If you bring this number way down you might
// want to turn `fadeout` up to 0.4 or 0.5.
const delay = 1500;
// (0 - 1) how much opacity to apply on each
// frame to clear the screen. Lower = slower,
// higher = faster.
const fadeout = 0.1;
// (0 - 360) Hue, in degrees, of the dot you're
// currently drawing.
const colorStart = 0;
// (0 - ?) How far should the color rotate across
// the reflected sides? 360 = one full rainbow.
const colorSpread = 360;

// SETUP
// We'll use this ID to make the feedback loop erasable.
let lastId = 0;
// Maybe scale the canvas for a retina display.
const scale = window.devicePixelRatio;
// Grab the canvas and set the width and height.
const canvasElement = document.getElementById('canvas');
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
const context = canvas.getContext('2d');

// An erase function we'll use when the window gets resized.
const erase = () => context.clearRect(0, 0, window.innerWidth * scale, window.innerHeight * scale);

// On resize set the canvas width & height so there's no stretching.
window.onresize = () => {
  canvasElement.width = window.innerWidth * scale;
  canvasElement.height = window.innerHeight * scale;
};

// STREAMS
// A stream of mouse movement.
const mouseMovement = Rx.Observable.fromEvent(document.querySelector('body'), 'mousemove');
// For some reason `fromEvent` isn't working for touchmove so we'll make our own stream.
const touchMovement = new Rx.Subject();
document.addEventListener('touchmove', e => {
  e.stopPropagation();
  touchMovement.next(e);
});
// A special stream that will create a feedback loop,
// continuously processing the same information on
// a delay.
const feedback = new Rx.Subject();
// A stream of clear button clicks.
const clearDrawing = Rx.Observable.fromEvent(document.querySelector('#clear'), 'click');
// A stream of requestAnimationFrame callbacks for doing work at 60fps.
const frames = Rx.Observable.create(observable => {
  const frame = () => {
    window.requestAnimationFrame(frame);
    observable.next();
  }
  frame();
});

// LOGIC
// Using the mouse movement as input we're going to set up
// a feedback loop that endlessly replays our event data.
mouseMovement
  .merge(touchMovement)
  // Only send movement events if the mouse button is pressed or if it's a touch event.
  .filter(e => {
    if (e.targetTouches) return true;
    return e.button === 1 || e.which === 1;
  })
  // We don't want or need to pass the full event object around
  // so we map the stream to only the values we need. We also
  // assign an ID before we enter the feedback loop, which allows
  // us to clear the current drawings by filtering out old IDs.
  .map(e => {
      if (e.targetTouches) return { x: e.targetTouches[0].clientX, y: e.targetTouches[0].clientY, id: lastId };
      return { x: e.clientX, y: e.clientY, id: lastId };
  })
  // Merge the feedback stream into this one, allowing us to
  // replay previously observed events as if they were new.
  .merge(feedback)
  // Now we're in feedback territory! Filter out any events with
  // an old ID. The ID will be incremented using the clear button.
  .filter(e => e.id === lastId)
  // On each event draw the coordinates to the canvas. We're mapping
  // here rather than subscribing because we still have to pipe
  // the data into the feedback loop. If we subscribed we wouldn't
  // be able to further modify the stream.
  .map(e => {
    // We're going to draw the data once for every symmetrical side.
    for(let i = 0; i < sides; i++) {
      // rotate the original coordinates around the center of the canvas.
      const { rx, ry } = rotateAroundCenter(e.x, e.y, (Math.PI * 2) / sides * i);
      // Set the hue based on the current rotation.
      context.fillStyle = `hsl(${colorStart + (colorSpread / sides * i)}, 100%, 50%)`;
      // Draw a circle •
      context.beginPath();
      context.arc(rx * scale, ry * scale, radius, 0, Math.PI * 2);
      context.fill();
    }
    // We're using map so we must return the event.
    return e;
  })
  // Delay the event for the specified number of ms.
  .delay(Math.max(100, delay))
  // Take the delayed event and push it into the feedback stream.
  .subscribe(e => feedback.next(e));

// when the clear button is pressed we increment
// the lastId, filtering out any old events.
clearDrawing.subscribe(e => {
  lastId = lastId + 1;
  erase();
});

// Each frame, fade the canvas out a bit by putting a
// semi-transparent white box over everything. The
// `fadeout` const determines how much opacity to apply.
frames.subscribe(() => {
  context.fillStyle = `rgba(255, 255, 255, ${fadeout})`;
  context.fillRect(0, 0, window.innerWidth * scale, window.innerHeight * scale);
})

// HELPERS
// A function for rotating a point around the center
// of the canvas by `angle` radians. We're working with
// screen coordinates here — not canvas coordinates —
// so we don't apply the `scale`.
function rotateAroundCenter(xpos, ypos, angle) {
  const cx = window.innerWidth / 2;
  const cy = window.innerHeight / 2;
  const rx = Math.cos(angle) * (xpos - cx) - Math.sin(angle) * (ypos - cy) + cx;
  const ry = Math.sin(angle) * (xpos - cx) + Math.cos(angle) * (ypos - cy) + cy;
  return { rx, ry };
}
              
            
!
999px

Console