Skip to content

Instantly share code, notes, and snippets.

@joshistoast
Created August 16, 2024 20:42
Show Gist options
  • Select an option

  • Save joshistoast/13a8aad9efa90c0ba514cedeb4dc80e8 to your computer and use it in GitHub Desktop.

Select an option

Save joshistoast/13a8aad9efa90c0ba514cedeb4dc80e8 to your computer and use it in GitHub Desktop.
Marquee Shenannigans
if (!customElements.get('smooth-marquee')) {
customElements.define(
'smooth-marquee',
class SmoothMarquee extends HTMLElement {
constructor() {
super();
/** @type {HTMLDivElement | null} */
this.marqueeWrap = null;
/** @type {NodeListOf<HTMLDivElement> | null} */
this.marqueeSegments = null;
/** @type {ResizeObserver | null} */
this.resizeObserver = null;
/** @type {Array<MutationObserver>} */
this.mutationObservers = [];
}
connectedCallback() {
this.initMarquee();
this.setupObservers();
}
disconnectedCallback() {
this.cleanupObservers();
}
initMarquee() {
console.log('initMarquee');
this.marqueeWrap = this.querySelector('.marquee__wrap');
this.marqueeSegments = this.querySelectorAll('.marquee__segment');
if (!this.marqueeWrap || !this.marqueeSegments.length) return;
// Initialize the marquee when content has loaded
requestAnimationFrame(() => {
this.setupMarquee();
});
}
setupObservers() {
// Observe content changes in each segment
this.marqueeSegments.forEach(segment => {
const observer = new MutationObserver(() => this.setupMarquee());
observer.observe(segment, { childList: true, subtree: true });
this.mutationObservers.push(observer);
});
// Observe resize events for the marquee wrap
this.resizeObserver = new ResizeObserver(() => this.debounce(() => this.setupMarquee(), 200));
this.resizeObserver.observe(this.marqueeWrap);
}
cleanupObservers() {
// Disconnect mutation observers
this.mutationObservers.forEach(observer => observer.disconnect());
this.mutationObservers = [];
// Disconnect resize observer
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
}
debounce(fn, wait) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), wait);
};
}
/** Set up marquee. */
setupMarquee() {
if (!this.marqueeWrap || !this.marqueeSegments.length) return
// Expand all segments to fill width of wrap and outside of viewport.
this.marqueeSegments.forEach(segment => {
this.expandSegmentToWidth(segment);
});
this.style.setProperty('--marquee-text-direction', `${this.direction === 'left' ? 'rtl' : 'ltr'}`);
this.style.setProperty('--marquee-segment-width', `${this.segmentWidth}px`);
this.style.setProperty('--marquee-segment-count', this.marqueeSegments.length);
this.style.setProperty('--marquee-wrap-width', `${this.wrapWidth}px`);
this.style.setProperty('--marquee-duration', this.calculateDuration(this.segmentWidth));
}
/**
* Clone content until it fills width of wrap.
* @param {HTMLDivElement} segment - Segment to expand.
* */
expandSegmentToWidth(segment) {
const totalWidth = this.wrapWidth;
let contentWidth = segment.offsetWidth;
while (contentWidth < totalWidth) {
const clone = segment.firstElementChild.cloneNode(true);
segment.appendChild(clone);
contentWidth += clone.offsetWidth;
}
}
/**
* Calculate duration of marquee animation based on total content width.
* @param {number} segmentWidth - Total width of content segment.
*/
calculateDuration(segmentWidth) {
const wrapWidth = this.wrapWidth;
const baseSpeed = 10;
const duration = (segmentWidth / wrapWidth) * baseSpeed;
return `${duration * this.speedMultiplier}s`;
}
get wrapWidth() {
return this.marqueeWrap.offsetWidth;
}
get segmentWidth() {
return this.marqueeSegments[0].offsetWidth;
}
get direction() {
return getComputedStyle(this).getPropertyValue('--marquee-text-direction');
}
get speedMultiplier() {
return parseFloat(getComputedStyle(this).getPropertyValue('--speed-multiplier'));
}
}
)
}
{% liquid
assign hover_animation_state = "running"
if section.settings.pause_on_hover == true
assign hover_animation_state = "paused"
endif
%}
<script src="{{ 'marquee.js' | asset_url }}" defer></script>
<smooth-marquee
style="
--marquee-direction: {{ section.settings.direction }};
--speed-multiplier: {{ section.settings.speed }};
--marquee-gap: {{ section.settings.content_gap }}px;
--hover-animation-state: {{ hover_animation_state }};
"
class="scroll-trigger animate--slide-in color-{{ section.settings.color_scheme }} gradient section-{{ section.id }}-padding no-js-hidden"
aria-live="off"
role="marquee"
{{ block.shopify_attributes }}
>
{% capture marquee_segment %}
<div class="marquee__segment">
<h2 class="title inline-richtext {{ section.settings.content_size }}">
{{ section.settings.content | default: 'Marquee Text' }}
</h2>
</div>
{% endcapture %}
<div class="marquee__wrap">
{{ marquee_segment }}
{{ marquee_segment }}
</div>
</smooth-marquee>
{% style %}
.section-{{ section.id }}-padding {
padding-top: {{ section.settings.padding_top | times: 0.75 | round: 0 }}px;
padding-bottom: {{ section.settings.padding_bottom | times: 0.75 | round: 0 }}px;
}
@media screen and (min-width: 750px) {
.section-{{ section.id }}-padding {
padding-top: {{ section.settings.padding_top }}px;
padding-bottom: {{ section.settings.padding_bottom }}px;
}
}
smooth-marquee {
display: block;
overflow: hidden;
position: relative;
white-space: nowrap;
text-align: center;
direction: var(--marquee-text-direction);
&:hover .marquee__wrap {
animation-play-state: var(--hover-animation-state);
}
}
@keyframes moveleft {
from { transform: translateX(0); }
to { transform: translateX(calc(var(--marquee-segment-width) * -1)); }
}
@keyframes moveright {
from { transform: translateX(calc(var(--marquee-segment-width) * -1)); }
to { transform: translateX(100%); }
}
.marquee__wrap {
display: flex;
white-space: nowrap;
animation: move{{ section.settings.direction }} var(--marquee-duration) linear infinite;
will-change: transform;
}
.marquee__segment {
display: inline-flex;
& > * {
margin: 0;
padding-right: var(--marquee-gap);
}
}
{% endstyle %}
{% schema %}
{
"name": "t:sections.marquee.name",
"class": "section",
"tag": "section",
"disabled_on": {
"groups": ["header", "footer"]
},
"settings": [
{
"type": "inline_richtext",
"id": "content",
"label": "t:sections.marquee.settings.content.label",
"default": "Flag Nor Fail",
},
{
"type": "select",
"id": "content_size",
"options": [
{
"value": "h2",
"label": "t:sections.all.heading_size.options__1.label"
},
{
"value": "h1",
"label": "t:sections.all.heading_size.options__2.label"
},
{
"value": "h0",
"label": "t:sections.all.heading_size.options__3.label"
},
{
"value": "hxl",
"label": "t:sections.all.heading_size.options__4.label"
},
{
"value": "hxxl",
"label": "t:sections.all.heading_size.options__5.label"
}
],
"default": "h1",
"label": "t:sections.marquee.settings.content_size.label"
},
{
"type": "range",
"id": "content_gap",
"label": "t:sections.marquee.settings.content_gap.label",
"default": 20,
"min": 0,
"max": 100
},
{
"type": "select",
"id": "direction",
"options": [
{
"value": "left",
"label": "t:sections.marquee.settings.direction.left"
},
{
"value": "right",
"label": "t:sections.marquee.settings.direction.right"
}
],
"label": "t:sections.marquee.settings.direction.label",
"default": "left"
},
{
"type": "select",
"id": "speed",
"label": "t:sections.marquee.settings.speed.label",
"options": [
{
"label": "t:sections.marquee.settings.speed.slow",
"value": "2",
},
{
"label": "t:sections.marquee.settings.speed.medium",
"value": "1",
},
{
"label": "t:sections.marquee.settings.speed.fast",
"value": "0.5",
}
],
"default": "1"
},
{
"type": "checkbox",
"id": "pause_on_hover",
"label": "t:sections.marquee.settings.pause_on_hover.label",
"default": false
},
{
"type": "color_scheme",
"id": "color_scheme",
"label": "t:sections.all.colors.label",
"default": "scheme-fnf"
},
{
"type": "header",
"content": "t:sections.all.padding.section_padding_heading"
},
{
"type": "range",
"id": "padding_top",
"label": "t:sections.all.padding.padding_top",
"default": 64,
"min": 0,
"max": 100,
"step": 4
},
{
"type": "range",
"id": "padding_bottom",
"label": "t:sections.all.padding.padding_bottom",
"default": 64,
"min": 0,
"max": 100,
"step": 4
}
],
"presets": [
{
"name": "t:sections.marquee.name",
"settings": {
"content": "Flag Nor Fail",
"content_size": "h1",
"direction": "left",
"color_scheme": "scheme-fnf"
}
}
]
}
{% endschema %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment