Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ 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

Save Automatically?

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

              
                
    <div class="controls">
      <ul>
        <li><input type="checkbox" name="profiling"><label for="profiling">Profiling</label></li>
        <li><input type="checkbox" name="frosted-effect" checked><label for="frosted-effect">Frosted</label></li>
      </ul>
      <img src="https://source.unsplash.com/daily" />
      <img src="https://source.unsplash.com/daily?cat" />
      <img src="https://source.unsplash.com/daily?architecture" />
      <img src="https://source.unsplash.com/daily?dancing" />
      <img src="https://source.unsplash.com/daily?music" />
      <img src="https://source.unsplash.com/daily?nature" />
      <img src="https://source.unsplash.com/daily?fashion" />
      <img src="https://source.unsplash.com/daily?business" />
      <img src="https://source.unsplash.com/daily?portrait" />
      <img src="https://source.unsplash.com/daily?model" />
    </div>

    <div class="story" data-is-root="1">
 
         <div class="text draggable" data-nofrost="1" style="font-family: Oswald; bottom: 22%; left: 5%; font-size: 2em;">
          <span>Text Magic.</span>
        </div>     
      
        <div class="text draggable" style="bottom: 5%; left: 5%; width: 320px; font-size: 1.3em;">
          <span>Because we can. Try dragging me around. I'll adapt smartly to my environment.</span>
        </div>

    </div>
              
            
!

CSS

              
                html, body {
    margin: 0;
    padding: 0;
    background: rgb(32, 32, 32);
    height: 100%;
    touch-action: none;
    overflow: hidden;
    font-family: Roboto, sans-serif;
}

body {
    display: flex;
    align-items: center;
    justify-content: center;
}

.story {
    background: white;
    width: 400px;
    height: calc(400px * 16/9);
    position: relative;
    margin-left: 1em;
    border-radius: 4px;
    overflow: hidden;
    background-clip: padding-box;
    background-image: url(https://source.unsplash.com/daily?city);
    background-size: cover;
    background-position: center;
}

.story-viewport {
    width: 100%;
    height: 100%;
}

.story > * {
    position: absolute;
    cursor: default;
    box-sizing: border-box;
}

.story .text {
  padding: 0.3em 0em;
  padding-right: 0.4em;
  transform: translateX(0); /* for some reason this fixes the backdrop filter */
  --theme-color: #000;
  span {
    line-height: 1.4em;
    padding: 0.2em 0.4em;
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
    text-indent: 0;
    transition: all 0.2s ease-out;
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
  }
}

.story .text.highlight {
  padding: 0;
  padding-right: 0.4em;
  span {
    background-color: var(--theme-color);
    border-radius: 3px;
    line-height: 1.8;
  }
}

.story .text.frosted {
  backdrop-filter: blur(10px);
  border-radius: 3px;
  span {
    line-height: 1.4;
  }
}

.controls {
    height: calc(400px * 16/9);
    background: #000;
    width: 15vw;
    color: #fff;
    font-size: 10px;
    padding: 1em;
    display: flex;
    flex-direction: column;
    overflow: auto;
    box-sizing: border-box;
  
  ul {
    list-style: none;
    margin: 0;
    padding: 0;
    margin-bottom: 1em;
    display: flex;
    justify-content: space-evenly;
    li {
      margin: 0;
      padding: 0;
    }
  }
  
    img {
      width: 100%;
      height: auto;
      margin-bottom: 1em;
    }
}

canvas {
  position: absolute;
  bottom: 0;
  right: 0;
  border-top: 2px solid red;
  border-left: 2px solid red;
  height: 20%;
  image-rendering: pixelated;
}
              
            
!

JS

              
                // constants (mess with these)
const REQUIRED_CONTRAST = 3;
const OK_CONTRAST = 1.5;
const FUZZINESS = 8;
const HIGH_PRECISION = true;
const DEFAULT_IMAGE = 'https://source.unsplash.com/daily?city';
const PAGE_WIDTH = document.querySelector('.story').offsetWidth || 400;
const PAGE_HEIGHT = document.querySelector('.story').offsetHeight || 600;
const THEME_COLOR_BG = [39,45,218,255];
const THEME_COLOR_FG = [0,0,0,255];
const DEBUG = {};

// global helper functions
function calculateContrast(rgb1, rgb2) {
  
  const luminance = (r, g, b) => {
      var a = [r, g, b].map(function (v) {
          v /= 255;
          return v <= 0.03928
              ? v / 12.92
              : Math.pow( (v + 0.055) / 1.055, 2.4 );
      });
      return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
  }
  
  var lum1 = luminance(rgb1[0], rgb1[1], rgb1[2]);
  var lum2 = luminance(rgb2[0], rgb2[1], rgb2[2]);
  var brightest = Math.max(lum1, lum2);
  var darkest = Math.min(lum1, lum2);
  return (brightest + 0.05) / (darkest + 0.05);
}

/**
 * By Ken Fyrstenberg Nilsen
 * from https://stackoverflow.com/questions/21961839/simulation-background-size-cover-in-canvas
 *
 * drawCoverImage(context, image [, x, y, width, height [,offsetX, offsetY]])
 *
 * If image and context are only arguments rectangle will equal canvas
*/
function drawCoverImage(ctx, img, x, y, w, h, offsetX, offsetY) {

    if (arguments.length === 2) {
        x = y = 0;
        w = ctx.canvas.width;
        h = ctx.canvas.height;
    }

    // default offset is center
    offsetX = typeof offsetX === "number" ? offsetX : 0.5;
    offsetY = typeof offsetY === "number" ? offsetY : 0.5;

    // keep bounds [0.0, 1.0]
    if (offsetX < 0) offsetX = 0;
    if (offsetY < 0) offsetY = 0;
    if (offsetX > 1) offsetX = 1;
    if (offsetY > 1) offsetY = 1;

    var iw = img.width,
        ih = img.height,
        r = Math.min(w / iw, h / ih),
        nw = iw * r,   // new prop. width
        nh = ih * r,   // new prop. height
        cx, cy, cw, ch, ar = 1;

    // decide which gap to fill    
    if (nw < w) ar = w / nw;                             
    if (Math.abs(ar - 1) < 1e-14 && nh < h) ar = h / nh;  // updated
    nw *= ar;
    nh *= ar;

    // calc source rectangle
    cw = iw / (nw / w);
    ch = ih / (nh / h);

    cx = (iw - cw) * offsetX;
    cy = (ih - ch) * offsetY;

    // make sure source rectangle is valid
    if (cx < 0) cx = 0;
    if (cy < 0) cy = 0;
    if (cw > iw) cw = iw;
    if (ch > ih) ch = ih;

    // fill image in dest. rectangle
    ctx.drawImage(img, cx, cy, cw, ch,  x, y, w, h);
}

function loadImage(src, onload) {
  const img = new Image();
  img.crossOrigin = "Anonymous";
  img.src = src;
  img.onload = onload;
}

function createCanvasContext(debug = false) {
  const canvas = document.createElement('canvas');
  canvas.width = PAGE_WIDTH / FUZZINESS;
  canvas.height = PAGE_HEIGHT / FUZZINESS;
  
  if(debug) {
    // debug only - doesn't need to be appended
    document.body.appendChild(canvas);
  }
  
  return canvas.getContext('2d');
}

function getColorFromCoord(ctx, x, y) {
  var pixel = ctx.getImageData(
    Math.floor(x / FUZZINESS),
    Math.floor(y / FUZZINESS), 1, 1);
  var data = pixel.data;
  return data;
}

function getColorFromCoords(ctx, x, y, width, height) {
  var pixel = ctx.getImageData(
    Math.floor(x / FUZZINESS),
    Math.floor(y / FUZZINESS),
    Math.floor(width / FUZZINESS),
    Math.floor(height / FUZZINESS)
  );
  var data = pixel.data;
  return data;
}

function sampleColors(ctx, elem, x, y, w, h) {
  
  if(DEBUG.PROFILING)
    console.time("sample colors took");
  
  const width = (w || elem.offsetWidth) - 1;
  const height = (h || elem.offsetHeight) - 1;
  const white = [255,255,255,255];
  const black = THEME_COLOR_FG || [0,0,0,255]; // defaults to black, but the demo here has a blue theme color
 
  let colors;
  
  // In high precision mode, we sample all colors under the
  // text element. I imagine this could get slow, depending
  // on the resolution defined in FUZZINESS.
  if(HIGH_PRECISION) {
    const pixelData = getColorFromCoords(ctx, x, y, width, height);
    colors = [];
    for (var i = 0; i < pixelData.length; i += 4) {
      colors.push([pixelData[i], pixelData[i + 1], pixelData[i + 2], pixelData[i + 3]]);
    }    
  } else {
    // in low precision mode, we sample only a handful of
    // colors, strategic points throughout the element.
    // TODO: Factor in num of lines.
    const p1 = [x, y]; // top-left
    const p2 = [x + width/2, y]; // top-middle
    const p3 = [x + width, y]; // top-right
    const p4 = [x, y + height/2]; // middle-left
    const p5 = [x + width/2, y + height/2]; // middle-middle
    const p6 = [x + width, y + height/2]; // middle-right
    const p7 = [x, y + height]; // bottom-left
    const p8 = [x + width/2, y + height]; // bottom-middle
    const p9 = [x + width, y + height]; // bottom-right
    const points = [p1,p2,p3,p4,p5,p6,p7,p8,p9];    
    colors = points.map(p => getColorFromCoord(ctx, p[0], p[1]));
  }

  const contrasts = colors.map(c => calculateContrast(white, c));
  const contrastIsGreat = !contrasts.some(ct => ct < REQUIRED_CONTRAST);
  const contrastIsOK = !contrasts.some(ct => ct < OK_CONTRAST);

  if(contrastIsGreat) {
    //console.log('contrast is great with white font color');
    elem.classList.remove('highlight', 'frosted');
    elem.style.color = '#fff';
    
    if(DEBUG.PROFILING)
      console.timeEnd("sample colors took");
    return;
  }

  if(contrastIsOK && DEBUG.FROSTED_EFFECT && !elem.dataset.nofrost) {
    elem.classList.remove('highlight');
    elem.classList.add('frosted');
    elem.style.color = '#fff';
    
    if(DEBUG.PROFILING)
      console.timeEnd("sample colors took");
    return;
  }
  
  // try black (or alternative, dark color)..
  const altContrasts = colors.map(c => calculateContrast(black, c));
  const altContrastIsGreat = !altContrasts.some(ct => ct < REQUIRED_CONTRAST);
  const altContrastIsOK = !contrasts.some(ct => ct < OK_CONTRAST);
  
  if(altContrastIsGreat) {
    //console.log('contrast is great with dark font color');
    elem.classList.remove('highlight', 'frosted');
    elem.style.color = `rgb(${black[0]},${black[1]},${black[2]})`;
    
    if(DEBUG.PROFILING)
      console.timeEnd("sample colors took");
    return;
  }

  if(altContrastIsOK && DEBUG.FROSTED_EFFECT && !elem.dataset.nofrost) {
    elem.classList.remove('highlight');
    elem.classList.add('frosted');
    elem.style.color = `rgb(${black[0]},${black[1]},${black[2]})`;
    
    if(DEBUG.PROFILING)
      console.timeEnd("sample colors took");
    return;
  }  
  
  // if no dark or light text color worked, we have an uneven background,
  // an need to apply a text highlight
  elem.classList.add('highlight');
  elem.classList.remove('frosted');
  const darkHighlightWorksBetter = contrasts.reduce(( a, c ) => a + c, 0) < altContrasts.reduce(( a, c ) => a + c, 0);
  
  if(darkHighlightWorksBetter) {
    //console.log('contrast is great with dark highlight');
    elem.style.setProperty('--theme-color', `rgb(${THEME_COLOR_BG[0]},${THEME_COLOR_BG[1]},${THEME_COLOR_BG[2]})`);
    elem.style.color = '#fff';    
  } else {
    //console.log('contrast is great with white highlight');
    elem.style.setProperty('--theme-color', '#fff');
    elem.style.color = `rgb(${black[0]},${black[1]},${black[2]})`;    
  }
  
  if(DEBUG.PROFILING)
    console.timeEnd("sample colors took");
  
};

// from here on application code..
const ctx = createCanvasContext(true);

// handle debug checkboxes..
const debugCheckboxes = document.querySelectorAll('input');
for (const checkbox of debugCheckboxes) {
    DEBUG[checkbox.name.replace(/-/g, '_').toUpperCase()] = checkbox.checked;
    checkbox.addEventListener('change', (e) => {
        DEBUG[checkbox.name.replace(/-/g, '_').toUpperCase()] = checkbox.checked;
        resample();
    });
}

// load the image into memory
loadImage(DEFAULT_IMAGE, function() {
  
  // draw the image (this) onto our canvas to query pixel data
  drawCoverImage(ctx, this);
  
  // trigger text magic for the text elements
  const elems = document.querySelectorAll('.text');
  for (let elem of elems) {
    sampleColors(ctx, elem, elem.offsetLeft, elem.offsetTop);

    // this adds a custom listener to the custom draggable
    // I'm using here, which executes on drag, then resamples.
    elem.addEventListener('customdrag', (e) => {
      sampleColors(
        ctx,
        elem,
        Math.round(e.detail.left),
        Math.round(e.detail.top),
        Math.round(e.detail.width),
        Math.round(e.detail.height)
      );
    }, false);     
  }
 
});

function resample() {
  const elems = document.querySelectorAll('.text');
  for (let elem of elems) {
    sampleColors(ctx, elem, elem.offsetLeft, elem.offsetTop);
  }
}

// debug code from here on - to easily test with a variety of imagery
document.querySelector('.controls').onclick = (e) => {
  if(e.target.nodeName !== 'IMG') {
    return;
  }
  
  // switch out image
  document.querySelector('.story').style.backgroundImage = `url('${e.target.src}')`;
  loadImage(e.target.src, function() {
    drawCoverImage(ctx, this);
    resample();
  });
};
              
            
!
999px

Console