Created
August 16, 2024 20:42
-
-
Save joshistoast/13a8aad9efa90c0ba514cedeb4dc80e8 to your computer and use it in GitHub Desktop.
Marquee Shenannigans
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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')); | |
| } | |
| } | |
| ) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {% 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