- const COLORS = ['hsl(180, 100%, 50%)', 'hsl(100, 100%, 50%)', 'hsl(305, 100%, 75%)', 'hsl(250, 100%, 65%)', 'hsl(60, 100%, 50%)']
.container
svg(xmlns='http://www.w3.org/2000/svg' viewbox='0 -2 112.748 246.22')
lineargradient#popsicle-gradient(gradientunits='userSpaceOnUse' x1='0%' y1='0%' x2='100%' y2='0%' gradientTransform="rotate(45) scale(2)")
stop(offset='0%' stop-color=COLORS[0] stop-opacity='1')
stop(offset='19%' stop-color=COLORS[0] stop-opacity='1')
stop(offset='20%' stop-color=COLORS[1] stop-opacity='1')
stop(offset='39%' stop-color=COLORS[1] stop-opacity='1')
stop(offset='40%' stop-color=COLORS[2] stop-opacity='1')
stop(offset='59%' stop-color=COLORS[2] stop-opacity='1')
stop(offset='60%' stop-color=COLORS[3] stop-opacity='1')
stop(offset='79%' stop-color=COLORS[3] stop-opacity='1')
stop(offset='80%' stop-color=COLORS[4] stop-opacity='1')
stop(offset='100%' stop-color=COLORS[4] stop-opacity='1')
path(d='M56.374 0C25.143 0 0 23.65 0 53.028v126.43c0 4.793 4.102 8.652 9.198 8.652h35.604v44.654c0 4.684 5.16 8.456 11.571 8.456 6.41 0 11.572-3.772 11.572-8.456V188.11h35.605c5.095 0 9.198-3.86 9.198-8.652V53.028C112.748 23.651 87.605 0 56.374 0z')
.text(data-splitting='') Stay cool...
View Compiled
@import url('https://fonts.googleapis.com/css?family=Lato&display=swap')
*
box-sizing border-box
body
min-height 100vh
display flex
align-items center
justify-content center
font-family 'Lato', sans-serif
.container
height 40vmin
position relative
width 40vmin
width calc((112.748 / 241.22) * 40vmin)
.char
--delay calc(((var(--char-total) - var(--char-index))) - var(--word-index))
offset-path path(var(--path))
animation travel 6s calc((var(--delay) * (0.15)) * -1s) infinite linear both
offset-rotate auto 180deg
position absolute !important
font-size 4vmin
font-weight bold
top 0%
left 0%
transform translate(0, -2.5vmin)
svg
height 100%
width 100%
path
stroke #111111
stroke-width 2.5px
fill hsla(225, 100%, 50%, 0.25)
fill url(#popsicle-gradient)
.gradient
height 0
width 0
@keyframes travel
from
offset-distance 0%
to
offset-distance 100%
View Compiled
const { d3, Splitting } = window
/**
* Meanderer class. Accepts a path, container, height, width, and change handler.
* Although it doesn't need a handler. We can just call get path and let it do that.
* The checks can be handled outside. We don't need to do it inside.
*/
class Meanderer {
container
height
path
threshold
width
constructor({ height, path, threshold = 0.2, width }) {
this.height = height
this.path = path
this.threshold = threshold
this.width = width
// With what we are given create internal references
this.aspect_ratio = width / height
// Convert the path into a data set
this.path_data = this.convertPathToData(path)
this.maximums = this.getMaximums(this.path_data)
this.range_ratios = this.getRatios(this.maximums, width, height)
}
// This is relevant for when we want to interpolate points to
// the container scale. We need the minimum and maximum for both X and Y
getMaximums = data => {
const X_POINTS = data.map(point => point[0])
const Y_POINTS = data.map(point => point[1])
return [
Math.max(...X_POINTS), // x2
Math.max(...Y_POINTS), // y2
]
}
// Generate some ratios based on the data points and the path width and height
getRatios = (maxs, width, height) => [maxs[0] / width, maxs[1] / height]
/**
* Initially convert the path to data. Should only be required
* once as we are simply scaling it up and down. Only issue could be upscaling??
* Create high quality paths initially
*/
convertPathToData = path => {
// To convert the path data to points, we need an SVG path element.
const svgContainer = document.createElement('div')
// To create one though, a quick way is to use innerHTML
svgContainer.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg">
<path d="${path}"/>
</svg>`
const pathElement = svgContainer.querySelector('path')
// Now to gather up the path points using the SVGGeometryElement API 👍
const DATA = []
// Iterate over the total length of the path pushing the x and y into
// a data set for d3 to handle 👍
for (let p = 0; p < pathElement.getTotalLength(); p++) {
const { x, y } = pathElement.getPointAtLength(p)
DATA.push([x, y])
}
return DATA
}
/**
* This is where the magic happens.
* Use ratios etc. to interpolate our data set against our container bounds.
*/
generatePath = (containerWidth, containerHeight) => {
const {
height,
width,
aspect_ratio: aspectRatio,
path_data: data,
maximums: [maxWidth, maxHeight],
range_ratios: [widthRatio, heightRatio],
threshold,
} = this
const OFFSETS = [0, 0]
// Get the aspect ratio defined by the container
const newAspectRatio = containerWidth / containerHeight
// We only need to start applying offsets if the aspect ratio of the container is off 👍
// In here we need to work out which side needs the offset. It's whichever one is smallest in order to centralize.
// What if the container matches the aspect ratio...
if (Math.abs(newAspectRatio - aspectRatio) > threshold) {
// We know the tolerance is off so we need to work out a ratio
// This works flawlessly. Now we need to check for when the height is less than the width
if (width < height) {
const ratio = (height - width) / height
OFFSETS[0] = (ratio * containerWidth) / 2
} else {
const ratio = (width - height) / width
OFFSETS[1] = (ratio * containerHeight) / 2
}
}
// Create two d3 scales for X and Y
const xScale = d3
.scaleLinear()
.domain([0, maxWidth])
.range([OFFSETS[0], containerWidth * widthRatio - OFFSETS[0]])
const yScale = d3
.scaleLinear()
.domain([0, maxHeight])
.range([OFFSETS[1], containerHeight * heightRatio - OFFSETS[1]])
// Map our data points using the scales
const SCALED_POINTS = data.map(POINT => [
xScale(POINT[0]),
yScale(POINT[1]),
])
return d3.line()(SCALED_POINTS)
}
}
const CONTAINER = document.querySelector('.container')
const PATH =
'M56.374 0C25.143 0 0 23.65 0 53.028v126.43c0 4.793 4.102 8.652 9.198 8.652h35.604v44.654c0 4.684 5.16 8.456 11.571 8.456 6.41 0 11.572-3.772 11.572-8.456V188.11h35.605c5.095 0 9.198-3.86 9.198-8.652V53.028C112.748 23.651 87.605 0 56.374 0z'
const WIDTH = 112.748
const HEIGHT = 241.22
const responsivePath = new Meanderer({
path: PATH,
width: WIDTH,
height: HEIGHT,
})
const setPath = () => {
const scaledPath = responsivePath.generatePath(
CONTAINER.offsetWidth,
CONTAINER.offsetHeight
)
CONTAINER.style.setProperty('--path', `"${scaledPath}"`)
}
Splitting()
const SizeObserver = new ResizeObserver(setPath)
SizeObserver.observe(CONTAINER)
View Compiled