Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save volkancakil/579f868101e1e886daf8254ac8d56d83 to your computer and use it in GitHub Desktop.
Save volkancakil/579f868101e1e886daf8254ac8d56d83 to your computer and use it in GitHub Desktop.
Dynamic SVG image placeholders

Dynamic SVG image placeholders

How it works

  1. Loads the Potrace Lib (6KB min+gzip)
  2. Starts loading the full res source image
  3. Meanwhile...
  4. Grabs a low res thumbnail from Cloudinary (~5KB)
  5. Uses Potrace to generate an svg from the thumbnail
  6. Colours and fades the SVG in as a crisp, full-size placeholder
  7. Once full res loaded, overlays the svg with a nice fadein transition

Note: This demo artificially delays image loading by 1 second

FAQs:

  • 🚫 No build step required!
  • 🦉 The SVG is generated on the fly from the source image
  • 🎒 It requires an extra network round trip (to fetch the thumb from @Cloudinary)
  • 🌈 You can pick any colour for the svg
  • 💅 The SVG can be animated / styled however you want

Created by @jesstelford

A Pen by Jess Telford on CodePen.

License.

<h1>Cloudinary SVG Placeholder</h1>
<div class="column">
<p>Dynamic SVG image placeholders, powered by <a href="https://cloudinary.com/">Cloudinary</a> & <a href="https://github.com/kilobtye/potrace">Potrace</a></p>
</div>
<cloudinary-svg-placeholder
src="https://images.unsplash.com/photo-1529933037705-0d537317ae7b"
alt="Hello world"
color="lightgray"
width=500
cloudinary="demo"
animatedStroke=true
></cloudinary-svg-placeholder>
<div class="column">
<p><small><center>(Image from <a href="https://unsplash.com/">unsplash.com</a>)</small></center></p>
<p>Try these images:</p>
<p><ul>
<li><a class="alternateImg" href="https://images.unsplash.com/photo-1529932702015-d79f7e363168">Camera</a></li>
<li><a class="alternateImg" href="https://images.unsplash.com/photo-1529933037705-0d537317ae7b">Surprised Cat</a></li>
<li><a class="alternateImg" href="https://images.unsplash.com/photo-1529840180348-efd52969a4ce">Mountains</a></li>
</ul></p>
<h2>How it works</h2>
<p><ol>
<li>Loads the <a href="https://github.com/kilobtye/potrace">Potrace Lib</a> (6KB min+gzip)</li>
<li>Starts loading the full res source image</li>
<li>Meanwhile...</li>
<li><ol>
<li>Grabs a <a href="https://res.cloudinary.com/demo/image/fetch/q_10,f_jpg,w_150/https://images.unsplash.com/photo-1529933037705-0d537317ae7b">low res thumbnail from Cloudinary</a> (~5KB)</li>
<li>Uses Potrace to generate an svg from the thumbnail</li>
<li>Colours and fades the SVG in as a crisp, full-size placeholder</li>
</ol></li>
<li>Once full res loaded, overlays the svg with a nice fadein transition</li>
</ol></p>
<h2>FAQs</h2>
<p><ul>
<li>🚫 No build step required!</li>
<li>🦉 The SVG is generated on the fly from the source image</li>
<li>🎒 It requires an extra network round trip (to fetch the thumb from Cloudinary)</li>
<li>🌈 You can pick any colour for the svg</li>
<li>💅 The SVG can be animated / styled however you want</li>
</ul></p>
<p><i>Note: This demo artificially delays image loading by 1 second</i></p>
<hr />
<p>Created by <a href="https://mobile.twitter.com/jesstelford">@jesstelford</a></p>
</div>
const ARTIFICIAL_DELAY = 2000;
function createElementFromHTML(htmlString) {
const div = document.createElement('div');
div.innerHTML = htmlString;
return div.firstChild;
}
class CloudinaryImg extends HTMLElement {
static get observedAttributes() {
return ['src', 'animatedStroke'];
}
constructor() {
super();
this.lowResWidth = 150;
this.cloudName = this.getAttribute('cloudinary');
this.attachShadow({mode: 'open'});
this.initialise();
this.render();
}
initialise() {
this.imageLoaded = false;
this.svg = undefined;
this.svgElement = undefined;
this.width = this.getAttribute('width') || this.lowResWidth;
this.height = this.getAttribute('height') || this.lowResWidth;
this.shadowRoot.innerHTML = `
<style>
:host {
width: ${this.width}px;
}
.background {
position: absolute;
z-index: -1;
}
.fade-in {
opacity: 1;
animation-name: fadeInOpacity;
animation-iteration-count: 1;
animation-timing-function: ease-in;
animation-duration: 0.2s;
}
.animate-dash {
stroke-dasharray: var(--path-length);
stroke-dashoffset: var(--path-length);
animation: dash 5s linear forwards;
}
@keyframes fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes dash {
to {
stroke-dashoffset: 0;
}
}
</style>
`;
}
render() {
this.traceImage(
`https://res.cloudinary.com/${this.cloudName}/image/fetch/q_10,f_jpg,w_${this.lowResWidth}/${this.getAttribute('src')}`,
(svg) => {
this.svg = svg;
this.showSVGWhenConnected();
}
);
this.loadFullImage(() => {
this.imageLoaded = true;
this.showFullImageWhenLoaded();
});
}
// Only called for the disabled and open attributes due to observedAttributes
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'src' && oldValue !== newValue) {
this.initialise();
this.render();
}
}
traceImage(url, callback) {
// Enable CORS support
window.Potrace.img.crossOrigin = 'Anonymous';
window.Potrace.loadImageFromUrl(url);
window.Potrace.process(() => {
callback(window.Potrace.getSVG(this.width / this.lowResWidth, "curve"));
});
}
loadFullImage(done) {
this.fullImage = createElementFromHTML(`<img alt="${this.getAttribute('alt') || ''}" width=${this.width} />`)
this.fullImage.onload = () => {
done();
};
// artificial delay for fast networks
window.setTimeout(() => {
this.fullImage.src = this.getAttribute('src');
}, ARTIFICIAL_DELAY);
}
showSVGWhenConnected() {
// Only proceed if we're mounted in the DOM, the svg is loaded, and we haven't already shown the image
if (!this.connected || !this.svg || this.imageLoaded) {
return;
}
this.svgElement = createElementFromHTML(this.svg);
this.svgElement.classList.add('fade-in');
if (this.getAttribute('animatedStroke')) {
const pathEl = this.svgElement.querySelector('path');
pathEl.setAttribute('stroke-width', this.getAttribute('stroke-width') || '3');
pathEl.setAttribute('stroke', this.getAttribute('color') || 'gray');
pathEl.classList.add('animate-dash');
const pathLength = Math.ceil(pathEl.getTotalLength());
pathEl.style.setProperty('--path-length', pathLength);
} else {
pathEl.setAttribute('fill', this.getAttribute('color') || 'gray');
}
this.shadowRoot.appendChild(this.svgElement);
}
showFullImageWhenLoaded() {
// Only proceed if we're mounted in the DOM, and the image is loaded
if (!this.connected || !this.imageLoaded) {
return;
}
if (this.svgElement) {
this.svgElement.classList.add('background');
}
this.fullImage.classList.add('fade-in');
this.shadowRoot.appendChild(this.fullImage);
}
connectedCallback() {
this.connected = true;
this.showSVGWhenConnected();
this.showFullImageWhenLoaded();
}
}
customElements.define('cloudinary-svg-placeholder', CloudinaryImg);
const imgEL = document.querySelector('cloudinary-svg-placeholder');
document.querySelectorAll('a.alternateImg').forEach((node) => {
node.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
imgEL.setAttribute('src', event.target.getAttribute('href'));
})
})
<script src="https://kilobtye.github.io/potrace/potrace.js"></script>
:root {
font-family: Menlo, monospace;
font-size: calc( 0.8em + 1vmin );
}
cloudinary-svg-placeholder {
min-height: 504px;
}
img, svg, cloudinary-svg-placeholder {
display: block;
margin: 0 auto;
}
h1 {
text-align: center;
}
.column {
max-width: 20em;
margin: 0 auto;
}
.column a {
cursor: pointer;
color: inherit;
}
.column a:hover {
text-decoration: underline;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment