Impact Stories

Custodians Of Our Planet

We spoke with Nicole McDonald from Custodians of Our Planet (C.O.O.P), who creates sustainable, luxe bags from recycled materials. With support from the Digital Solutions Program, she refined her digital strategy and connecting with eco-conscious consumers effectively.

Digital Solutions Program

Custodians Of Our Planet

We spoke with Nicole McDonald from Custodians of Our Planet (C.O.O.P), who creates sustainable, luxe bags from recycled materials. With support from the Digital Solutions Program, she refined her digital strategy and connecting with eco-conscious consumers effectively.

Statistics

Statistics

Nicole McDonald is on a mission to make conscious consumers feel good about their choices - and look it, too.

Through her business - Custodians of our Planet (C.O.O.P) - Nicole makes beautiful, quality, luxe bags that are better for the planet.


“I want to provide an option that allows the user to feel good, not guilty, about their investment,” Nicole explains.


“I do this through the use of recycled, reclaimed or recyclable materials, offer repair for life of the product, and consider end-of-life stewardship for my products. All of this wrapped up as a beautiful, desirable bag!”


Creating a positive impact

Nicole released her first C.O.O.P bag in December 2020, and has been consciously developing the business since then. In the spirit of sustainability, Nicole - who also works as a Diagnostic Radiographer in breast cancer diagnosis - says she is considering the business as a marathon, ‘rather than a fast-fashion sprint’.

“I love that I get to be creative, and make a positive impact on our planet.”

C.O.O.P has always been an online-first platform - but Nicole needed some extra assistance in optimising her digital strategy.

Growing online with the Digital Solutions Program

“As a startup in a crowded market with a niche story, [online] was the most logical place to start and deliver the story of my brand. People will need to love the story of how I want to save the planet with my bag before they become customers. And at the moment, online is the best way to deliver this. The Digital Solutions Program is really helping me identify and refine my messaging to achieve this.”

Before joining the program, Nicole had no digital strategy in place - but knew it would be important to develop.

“I’ve never had a business before, and am aware of how much I need to learn to succeed. As my main form of gaining brand awareness and driving sales is through digital means, I hoped to gain knowledge on how to do this from someone in the business. I searched through the list of mentors to find someone who best matched what I was hoping to achieve - how to gain digital brand awareness. The bio of my mentor I chose resonated strongly; his business experience aligned perfectly to what I was after.”


Since joining the program, Nicole has found the quality and range of expertise available to be extremely valuable.

“I’ve really thought through and crafted my mission statement. I’ve also implemented digital advertising strategies based on the information I’ve learned. I’ve recommended this program to lots of other small businesses in Melbourne. I feel very lucky to have come across this amazing program and have had the chance to participate in it.”

The Digital Solutions Program is funded by the Australian Government and delivered by Melbourne Innovation Centre across Victoria. Join today to unleash the full potential of your small business in the digital realm.

More Stories

Barbarian Brewing Co.
When Pete Raine and his wife Zoe opened Barbarian Brewing Co. in Kingsville, they weren’t just launching a business. They were building a community. Over 100 local volunteers helped them bring the venue to life. But as the brewpub grew, Pete realised that passion and community alone wouldn’t be enough to build something that lasted. He needed to run it like a business — and that’s where Melbourne Innovation Centre stepped in.
[Learn More]
Digital Solutions Program
Good Huis Co.
[Learn More]
Digital Solutions Program
Bronzium
[Learn More]
Digital Solutions Program
Shirley Wu Jewellery
Shirley Wu Jewellery is known not only for its distinctive handmade pieces, but also for its sought-after jewellery-making workshops that bring people together to create, connect and share stories. What began as an experimental passion project in 2020 has grown into a thriving creative business - one that is now reaching new audiences across Adelaide and beyond.
[Learn More]
Digital Solutions Program
Good Days Studio
[Learn More]
Digital Solutions Program
Tasmanian Occasional Hampers
Tasmanian Occasional Hampers, founded by Gillian Southwell in Rose Bay, creates eco-friendly, locally sourced gift hampers that celebrate Tasmania’s makers and artisanal products. Each hamper showcases the best of the state, from gourmet treats to handcrafted goods, thoughtfully curated for weddings, corporate gifting, birthdays and special occasions.
[Learn More]
Digital Solutions Program
Aura Digital
We spoke with Valeria Chan, founder of Aura Digital, a boutique marketing agency empowering small businesses to grow online. With support from the Digital Solutions Program at Melbourne Innovation Centre, she’s refining her expertise to expand her impact and deliver even better results.
[Learn More]
Digital Solutions Program
Allied Collective
We spoke with Ruhee Meghani, founder of Allied Collective, Australia’s first inclusive facilitation agency. Ruhee empowers workplaces to prioritise equity and wellbeing. With support from the Digital Solutions Program at Melbourne Innovation Centre, she’s enhancing her digital strategy to scale her impact.
[Learn More]
Digital Solutions Program
Built for Advanture
We spoke with Samantha Jeffrey from Built for Advanture, a company that crafts high-end campervans. With support from the Digital Solutions Program, she’s refining their digital strategy and learning to enhance their online presence, vital for their evolving business.
[Learn More]
Digital Solutions Program
AxipApp
We chatted with Hayden Cassar from AxipApp, a platform that makes app development affordable for small businesses. With support from the Digital Solutions Program, he refined his pricing and marketing strategies, gaining valuable insights and connections to enhance his growing business.
[Learn More]
Digital Solutions Program
Farmhouse Seeds
We spoke with Jay Hill and David Ryan of Farmhouse Seeds, who shifted their business online during COVID-19. With support from the Digital Solutions Program, they enhanced their digital marketing, built customer relationships, and gained confidence in social media strategies.
[Learn More]
Digital Solutions Program
Table Threads
We sat down with Alina Adams, founder of Table Threads, an online business specialising in beautiful, affordable table linens. Launched during the COVID-19 pandemic, Alina sought guidance from the Digital Solutions Program at the Melbourne Innovation Centre to boost her digital marketing skills.
[Learn More]
Digital Solutions Program
Francie
We sat down with Anna from Francie, a Melbourne-based luxury knitwear brand. Anna pivoted to focus on her online store with guidance from the Digital Solutions Program. With targeted social media marketing and tailored strategies, Francie has thrived, expanding its reach while staying true to ethical production.
[Learn More]
Digital Solutions Program
Fiona Ariva
We sat down with Fiona from Wax Seals and Modern Calligraphy, an online business specialising in custom wax seal stamps and calligraphy services. Thanks to the Digital Solutions Program, she expanded her customer reach by implementing effective online marketing strategies.
[Learn More]
Digital Solutions Program
House of Handmade Collective
We sat down with Stefanie, owner of House of Handmade, a charming store in Richmond offering unique handmade gifts. After joining the Digital Solutions Program, she gained essential skills in SEO and social media strategy, empowering her to enhance her online presence and grow her business.
[Learn More]
Digital Solutions Program
SoundCheck Studios
We sat down with Zara, founder of SoundCheck Studios, which began in her Mum's barn in 2016. The Digital Solutions Program helped her develop a strategic marketing plan and enhance her online presence. Now, with a team of over 20, SoundCheck inspires students globally while fostering a supportive community.
[Learn More]
Digital Solutions Program
Moss & Pear
We sat down with Fiona, founder of moss & pear, an online Zero Waste Products Store. The Digital Solutions Program empowered her with essential business skills, transforming her fear into confidence. Now, she successfully champions sustainable living, creating eco-friendly products while supporting local Australian businesses.
[Learn More]
Digital Solutions Program
Eastern Kung Fu Academy
We sat down with Jo from The Eastern Kung Fu Academy, a family-run school in Boronia that focuses on student development. After joining the Digital Solutions Program, Jo and Peter enhanced their digital marketing skills, optimised their Google presence, and significantly improved their online engagement.
[Learn More]
const getCookie = name => { const nameExpression = `${name}=`; const cookies = document.cookie.split(';'); const cookie = cookies.find(currentCookie => currentCookie.includes(nameExpression)); return cookie ? cookie.trim().substring(nameExpression.length, cookie.length) : null; } const setCookie = (name, value, expire = 365, path = '/') => { const date = new Date(); date.setTime(date.getTime() + (expire * 24 * 3600 * 1000)); const expires = date.toUTCString(); document.cookie = `${name}=${value}; expires=${expires}; path=${path}`; }; const openCtaPopup = () => { const popup = document.querySelector('.event-popup'); gsap.set(popup, { autoAlpha: 0 }); document.querySelector('.popup-wrapper').style.display = 'block'; const tl = gsap.timeline() .fromTo(popup, { yPercent: 100, autoAlpha: 0 }, { yPercent: 0, duration: 1, autoAlpha: 1, ease: 'back.out(1.1)' }); } const closeCtaPopup = () => { const popup = document.querySelector('.popup-wrapper'); const type = popup.dataset.popupType setCookie(`${type}-popup`, "show", 2); gsap.to(popup, { autoAlpha: 0, duration: 0.5 }); } addEventListener("DOMContentLoaded", (e) => { gsap.to(window, {duration: 1, scrollTo: 0}) const header = document.querySelector('section.header-wrapper'); const positionHeader = () => { const height = calcHeight(header.querySelector('.navbarcontent'), header); header.classList.add('sticky'); document.getElementById('smooth-wrapper').style.marginTop = `${height}px`; } positionHeader(); const hasPopup = document.querySelector('.popup-wrapper'); if(hasPopup) { const type = hasPopup.dataset.popupType; window.popupCookie = getCookie(`${type}-popup`); if(window.popupCookie !== 'show') { setTimeout(() => { openCtaPopup(); }, 7000); } } document.querySelectorAll('[data-close-popup]').forEach(i => { i.addEventListener('click', (e) => { e.preventDefault(); closeCtaPopup(); }); }); const gradientSection = document.querySelector('[data-gradient-scroll]'); if(gradientSection) { const percentage = gradientSection.dataset.gradientScroll ?? 61; gsap.set(gradientSection, { background: `linear-gradient(0deg, #beced8 ${percentage}%, #341716)` }); gsap.timeline({ scrollTrigger: { trigger: gradientSection, start: 'top bottom', end: 'bottom bottom' } }) .to(gradientSection, { duration: 1, background: `linear-gradient(0deg, #beced8 ${percentage}%, #beced8)` }); } //gsap.timeline({ //scrollTrigger: { //trigger: 'footer', //start: 'top bottom', //end: 'bottom bottom' //} //}) //.to('.animated-footer-logo-wrapper', { //width: 'auto', //duration: 2, //ease: 'power1.out' //}); const openNavbarDropdown = (link) => { const element = document.getElementById(link.dataset.dropdownTrigger); const svg = link.querySelector('svg'); if(svg) { gsap.to(svg, { rotate: 180, duration: 0.4 }); } gsap.to(element, { autoAlpha: 1, y: 0, yPercent: 0, duration: 1, ease: 'power1.out', onStart: () => { element.setAttribute('data-open', "loading"); }, onComplete: () => { element.setAttribute('data-open', "true"); window.setTimeout(() => { element.focus(); }, 100); } }); } const closeNavbarDropdown = (dropdown) => { dropdown.setAttribute('data-open', "loading"); const link = document.querySelector(`[data-dropdown-trigger="${dropdown.getAttribute('id')}"]`); const svg = link?.querySelector('svg'); if(svg) { gsap.to(svg, { rotate: 0, duration: 0.4 }); } gsap.to(dropdown, { autoAlpha: 0, yPercent: -200, duration: 1, ease: 'power1.out', onComplete: () => { dropdown.removeAttribute('data-open'); } }); } document.querySelectorAll('[data-dropdown-trigger]').forEach(i => { const element = document.getElementById(i.dataset.dropdownTrigger); if(element) { element.addEventListener('blur', (e) => { if(e && (element.contains(e.relatedTarget) || e.relatedTarget === i || i.contains(e.relatedTarget))) { return false; } closeNavbarDropdown(element); }) } i.addEventListener('click', (e) => { e.preventDefault(); const element = document.getElementById(i.dataset.dropdownTrigger); if(!element) { return; } if(element.dataset.open === 'loading') { return; } if(element.hasAttribute('data-open')) { closeNavbarDropdown(element); return; } openNavbarDropdown(i); }); }); setTimeout(() => { // const firstAnchor = document.querySelector('.smooth-content [data-cta-anchor="true"]'); //if(firstAnchor && firstAnchor.checkVisibility()) { //gsap.timeline({ //scrollTrigger: { //trigger: firstAnchor, //start: 'top +200px', //scrub: 1 //} //}) //.to('.header-wrapper.sticky', { // yPercent: -100, //duration: 2 //}, 0) //.to('#smooth-wrapper', { //y: 0, //marginTop: 0, //duration: 1 //}, 0); gsap.timeline({ scrollTrigger: { trigger: header, start: '+=50', scrub: 1 } }) .to('.header-wrapper.sticky .header', { paddingTop: 12, paddingBottom: 12, duration: 0.6 }, 0) .to('#smooth-wrapper', { y: -48, duration: 0.4 }, 0); //} document.querySelectorAll('[trigger-aside-open]').forEach(i => { i.addEventListener('click', (e) => { const section = i.getAttribute('trigger-aside-open'); e.preventDefault(); openAside(); if(section === 'services') { openServicesPanel(); } else if(section === 'contact') { openContactPanel() } }); }); // document.querySelectorAll('[trigger-services-open]').forEach(i => { // i.addEventListener('click', (e) => { // openServicesPanel(); // }); // }); // document.querySelectorAll('[trigger-contact-open]').forEach(i => { // i.addEventListener('click', (e) => { // openContactPanel(); // }); // }); // document.querySelectorAll('[trigger-main-open]').forEach(i => { // i.addEventListener('click', (e) => { // openAside(); // }); // }); document.querySelectorAll('[trigger-aside-close]').forEach(i => { i.addEventListener('click', (e) => { e.preventDefault(); closeAside(); }); }); }, 1200) ScrollSmoother.create({ smooth: 2, smoothTouch: 0.1, effects: true, normalizeScroll: false, ignoreMobileResize: true, wholePixels: true }) const aside = document.getElementById('menu-aside'); aside.querySelector('.menu-panel').addEventListener('click', e => { if(e.target.classList.contains('dropdown')) { return; } if(e.target.classList.contains('navlink')) { closeAside(); return; } if(e.target.hasAttribute('trigger-aside-close')) { return; } if(e.currentTarget.classList.contains('active')) { return; } e.preventDefault(); openAside(); }); document.querySelectorAll('[data-horizontal-scroll]').forEach(i => { const items = i.querySelectorAll('[data-horizontal-items]'); const wrapper = i.querySelector('[data-horizontal-wrapper]'); const styles = window.getComputedStyle(i); const type = i.dataset.horizontalScroll ?? 'standard'; if(type === 'standard') { const tl = gsap.to(wrapper, { scrollTrigger: { trigger: i, scrub: 1.2, pin: wrapper, anticipatePin: 1, start: "center center", end: () => `+=${wrapper.scrollWidth}`, invalidateOnRefresh: true }, x: () => -(wrapper.scrollWidth - (wrapper.offsetWidth)), ease: 'none' }) } else if(type === 'sticky') { const items = i.querySelectorAll('[data-horizontal-items]:not(:first-of-type)'); items.forEach((image, k) => { gsap.set(image, {position: 'absolute'}); const tl = gsap.fromTo(image, {xPercent: 100}, { scrollTrigger: { trigger: i, start: () => "+=" + (wrapper.offsetWidth * k), end: () => "+=" + wrapper.offsetWidth, scrub: 1.2, markers: false, invalidateOnRefresh: true, toggleActions: "play none reverse none", }, x: () => `-${wrapper.offsetWidth}`, ease: 'none' }); }) ScrollTrigger.create({ trigger: i, scrub: true, markers: false, pin: true, anticipatePin: 1, start: () => `center center`, end: () => "+=" + (wrapper.offsetWidth * (items.length + 0.5)), invalidateOnRefresh: true }); } }); document.querySelectorAll('[data-vertical-pin]').forEach(i => { gsap.set(i.querySelectorAll('[data-pinned-item]'), { zIndex: (i, target, targets) => targets.length - i }); const items = i.querySelectorAll('[data-pinned-item]:not(:last-of-type)'); items.forEach((image, k) => { const tl = gsap.timeline({ scrollTrigger: { trigger: i, start: () => "top -" + (window.innerHeight < 800 ? window.innerHeight : 800) * (k + 0.5), end: () => "+=" + (window.innerHeight < 800 ? window.innerHeight : 800), scrub: 0.5, markers: false, invalidateOnRefresh: true, toggleActions: "play none reverse none", } }); tl.to(image, { height: 0 }); }); const start = window.innerHeight < 800 ? "0" : (window.innerHeight - 800) / 2 ScrollTrigger.create({ trigger: i, scrub: true, markers: false, pin: true, anticipatePin: 1, start: () => `center center`, end: () => "+=" + (items.length + 1) * (window.innerHeight < 800 ? window.innerHeight : 800), invalidateOnRefresh: true }) }); document.querySelectorAll('[data-horizontal-loop]').forEach(i => { const speed = i.dataset.horizontalLoop ?? 1; const padding = i.dataset.horizontalPadding ?? 0; if(i.querySelectorAll('[data-loop-item]').length < 2) { return false; } i.querySelectorAll('[data-loop-item]').forEach(item => { const clone = item.cloneNode(true); i.append(clone); }); const items = i.querySelectorAll('[data-loop-item]'); const reversed = i.dataset.reverseLoop === 'true'; const loop = horizontalLoop(items, {paused: true, repeat: -1, speed: speed, paddingRight: padding, reversed: reversed}); reversed ? loop.reverse() : loop.play(); const pauseListener = debounce(() => { if(!i.matches(':hover')) { return; } i.removeEventListener('mouseover', this.pauseListener); loop.pause(); const pauseInterval = setInterval(() => { if (!i.matches(':hover')) { reversed ? loop.reverse() : loop.play(); i.addEventListener('mouseover', pauseListener) } }, 1000) }, 500) i.addEventListener('mouseover', pauseListener) }); }); class AnimatedStat extends HTMLElement { connectedCallback() { this.start = parseInt(this.dataset.start); this.steps = parseInt(this.dataset.steps); this.value = parseInt(this.dataset.value); this.shouldFormat = this.dataset.formatValue === 'true'; this.unit = this.dataset.unit ?? ''; this.element = this.querySelector('[data-animated-value]'); this.element.innerText = this.shouldFormat ? `${numberWithCommas(this.start)}` : `${this.start}`; this.setupAnimation(); } setupAnimation() { gsap.registerPlugin(ScrollTrigger) gsap.registerEffect({ name:"counter", extendTimeline:true, defaults:{ end:0, duration:0.5, ease:"power1", increment: 1, }, effect: (targets, config) => { let tl = gsap.timeline() let num = targets[0].innerText.replace(/\,/g,'') targets[0].innerText = num tl.to(targets, {duration:config.duration, innerText: config.end, //snap:{innerText:config.increment}, modifiers:{ innerText:function(innerText){ return config.format ? gsap.utils.snap(config.increment, `${innerText}${config.unit ?? ''}`).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : gsap.utils.snap(config.increment, `${innerText}${config.unit ?? ''}`).toString(); } }, ease:config.ease}, 0) return tl } }) let tl = gsap.timeline({ scrollTrigger: { trigger: this.element, start: "top bottom" } }) tl.counter(this.element, {end: this.value, increment: this.steps, duration: 4, unit: this.unit, format: this.shouldFormat}) } } customElements.define('animated-stat', AnimatedStat); class CollapsibleTab extends HTMLElement { connectedCallback() { this.contentHeight = 0; this.content = this.querySelector('[data-collapsible-content]'); if(!this.content) { throw new Error('Collapsible tab requires a content element.'); } this.trigger = this.querySelector('[data-collapsible-trigger]'); if(!this.trigger) { throw new Error('Collapsible tab requires a trigger element.'); } const styles = window.getComputedStyle(this.content); const startHeight = parseInt(styles.height) > 0 ? parseFloat(styles.height) : 0; if(startHeight > 0) { this.setAttribute('data-state', 'active'); } else { this.setAttribute('data-state', 'closed'); } this.content.style.setProperty('--content-height', `${startHeight}px`); this.content.style.height = 'var(--content-height)'; this.calcHeight(); this.trigger.addEventListener('click', this.toggle.bind(this)); window.addEventListener('resize', this.calcHeight.bind(this)); } toggle() { this.dataset.state === 'active' ? this.close() : this.open(); } calcHeight() { this.contentHeight = calcHeight(this.content.querySelector('.div-block-85'), this.content) + 12; } open() { this.calcHeight(); this.content.style.setProperty('--content-height', `${this.contentHeight}px`); this.setAttribute('data-state', 'active'); setTimeout(() => { ScrollTrigger.refresh(); }, 300); } close() { this.content.style.setProperty('--content-height', `0px`); this.setAttribute('data-state', 'closed'); setTimeout(() => { ScrollTrigger.refresh(); }, 300); } } customElements.define('collapsible-tab', CollapsibleTab); class FaqTabs extends HTMLElement { connectedCallback() { this.tabs = this.querySelectorAll('.faq-tab'); this.tabs[0].classList.add('active'); this.containers = this.querySelectorAll('.w-layout-blockcontainer'); this.containers[0].classList.add('active'); this.activeIndex = 0; this.tabs.forEach((i,k) => { i.addEventListener('click', e => { e.preventDefault(); if(k === this.activeIndex) { return } this.tabs[this.activeIndex]?.classList?.remove('active'); this.containers[this.activeIndex]?.classList?.remove('active'); this.tabs[k]?.classList?.add('active'); this.containers[k]?.classList?.add('active'); this.activeIndex = k; }); }); } } customElements.define('faq-tabs', FaqTabs); function numberWithCommas(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } function debounce(fn, wait) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), wait); }; } function calcHeight(element, container) { const clone = element.cloneNode(true); Object.assign(clone.style, { overflow: 'visible', height: 'auto', maxHeight: 'none', opacity: '0', visibility: 'hidden', display: 'block' }); container.after(clone); const height = clone.offsetHeight; clone.remove(); return height; } function horizontalLoop(items, config) { let timeline; items = gsap.utils.toArray(items); config = config || {}; gsap.context(() => { // use a context so that if this is called from within another context or a gsap.matchMedia(), we can perform proper cleanup like the "resize" event handler on the window let onChange = config.onChange, lastIndex = 0, tl = gsap.timeline({repeat: config.repeat, onUpdate: onChange && function() { let i = tl.closestIndex(); if (lastIndex !== i) { lastIndex = i; onChange(items[i], i); } }, paused: config.paused, defaults: {ease: "none"}, onReverseComplete: () => tl.totalTime(tl.rawTime() + tl.duration() * 100)}), length = items.length, startX = items[0].offsetLeft, times = [], widths = [], spaceBefore = [], xPercents = [], curIndex = 0, indexIsDirty = false, center = config.center, pixelsPerSecond = (config.speed || 1) * 100, snap = config.snap === false ? v => v : gsap.utils.snap(config.snap || 1), // some browsers shift by a pixel to accommodate flex layouts, so for example if width is 20% the first element's width might be 242px, and the next 243px, alternating back and forth. So we snap to 5 percentage points to make things look more natural timeOffset = 0, container = center === true ? items[0].parentNode : gsap.utils.toArray(center)[0] || items[0].parentNode, totalWidth, getTotalWidth = () => items[length-1].offsetLeft + xPercents[length-1] / 100 * widths[length-1] - startX + spaceBefore[0] + items[length-1].offsetWidth * gsap.getProperty(items[length-1], "scaleX") + (parseFloat(config.paddingRight) || 0), populateWidths = () => { let b1 = container.getBoundingClientRect(), b2; items.forEach((el, i) => { widths[i] = parseFloat(gsap.getProperty(el, "width", "px")); xPercents[i] = snap(parseFloat(gsap.getProperty(el, "x", "px")) / widths[i] * 100 + gsap.getProperty(el, "xPercent")); b2 = el.getBoundingClientRect(); spaceBefore[i] = b2.left - (i ? b1.right : b1.left); b1 = b2; }); gsap.set(items, { // convert "x" to "xPercent" to make things responsive, and populate the widths/xPercents Arrays to make lookups faster. xPercent: i => xPercents[i] }); totalWidth = getTotalWidth(); }, timeWrap, populateOffsets = () => { timeOffset = center ? tl.duration() * (container.offsetWidth / 2) / totalWidth : 0; center && times.forEach((t, i) => { times[i] = timeWrap(tl.labels["label" + i] + tl.duration() * widths[i] / 2 / totalWidth - timeOffset); }); }, getClosest = (values, value, wrap) => { let i = values.length, closest = 1e10, index = 0, d; while (i--) { d = Math.abs(values[i] - value); if (d > wrap / 2) { d = wrap - d; } if (d < closest) { closest = d; index = i; } } return index; }, populateTimeline = () => { let i, item, curX, distanceToStart, distanceToLoop; tl.clear(); for (i = 0; i < length; i++) { item = items[i]; curX = xPercents[i] / 100 * widths[i]; distanceToStart = item.offsetLeft + curX - startX + spaceBefore[0]; distanceToLoop = distanceToStart + widths[i] * gsap.getProperty(item, "scaleX"); tl.to(item, {xPercent: snap((curX - distanceToLoop) / widths[i] * 100), duration: distanceToLoop / pixelsPerSecond}, 0) .fromTo(item, {xPercent: snap((curX - distanceToLoop + totalWidth) / widths[i] * 100)}, {xPercent: xPercents[i], duration: (curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond, immediateRender: false}, distanceToLoop / pixelsPerSecond) .add("label" + i, distanceToStart / pixelsPerSecond); times[i] = distanceToStart / pixelsPerSecond; } timeWrap = gsap.utils.wrap(0, tl.duration()); }, refresh = (deep) => { let progress = tl.progress(); tl.progress(0, true); populateWidths(); deep && populateTimeline(); populateOffsets(); deep && tl.draggable ? tl.time(times[curIndex], true) : tl.progress(progress, true); }, onResize = () => refresh(true), proxy; gsap.set(items, {x: 0}); populateWidths(); populateTimeline(); populateOffsets(); window.addEventListener("resize", onResize); function toIndex(index, vars) { vars = vars || {}; (Math.abs(index - curIndex) > length / 2) && (index += index > curIndex ? -length : length); // always go in the shortest direction let newIndex = gsap.utils.wrap(0, length, index), time = times[newIndex]; if (time > tl.time() !== index > curIndex && index !== curIndex) { // if we're wrapping the timeline's playhead, make the proper adjustments time += tl.duration() * (index > curIndex ? 1 : -1); } if (time < 0 || time > tl.duration()) { vars.modifiers = {time: timeWrap}; } curIndex = newIndex; vars.overwrite = true; gsap.killTweensOf(proxy); return vars.duration === 0 ? tl.time(timeWrap(time)) : tl.tweenTo(time, vars); } tl.toIndex = (index, vars) => toIndex(index, vars); tl.closestIndex = setCurrent => { let index = getClosest(times, tl.time(), tl.duration()); if (setCurrent) { curIndex = index; indexIsDirty = false; } return index; }; tl.current = () => indexIsDirty ? tl.closestIndex(true) : curIndex; tl.next = vars => toIndex(tl.current()+1, vars); tl.previous = vars => toIndex(tl.current()-1, vars); tl.times = times; tl.progress(1, true).progress(0, true); // pre-render for performance if (config.reversed) { tl.vars.onReverseComplete(); tl.reverse(); } if (config.draggable && typeof(Draggable) === "function") { proxy = document.createElement("div") let wrap = gsap.utils.wrap(0, 1), ratio, startProgress, draggable, dragSnap, lastSnap, initChangeX, wasPlaying, align = () => tl.progress(wrap(startProgress + (draggable.startX - draggable.x) * ratio)), syncIndex = () => tl.closestIndex(true); typeof(InertiaPlugin) === "undefined" && console.warn("InertiaPlugin required for momentum-based scrolling and snapping. https://greensock.com/club"); draggable = Draggable.create(proxy, { trigger: items[0].parentNode, type: "x", onPressInit() { let x = this.x; gsap.killTweensOf(tl); wasPlaying = !tl.paused(); tl.pause(); startProgress = tl.progress(); refresh(); ratio = 1 / totalWidth; initChangeX = (startProgress / -ratio) - x; gsap.set(proxy, {x: startProgress / -ratio}); }, onDrag: align, onThrowUpdate: align, overshootTolerance: 0, inertia: true, snap(value) { //note: if the user presses and releases in the middle of a throw, due to the sudden correction of proxy.x in the onPressInit(), the velocity could be very large, throwing off the snap. So sense that condition and adjust for it. We also need to set overshootTolerance to 0 to prevent the inertia from causing it to shoot past and come back if (Math.abs(startProgress / -ratio - this.x) < 10) { return lastSnap + initChangeX } let time = -(value * ratio) * tl.duration(), wrappedTime = timeWrap(time), snapTime = times[getClosest(times, wrappedTime, tl.duration())], dif = snapTime - wrappedTime; Math.abs(dif) > tl.duration() / 2 && (dif += dif < 0 ? tl.duration() : -tl.duration()); lastSnap = (time + dif) / tl.duration() / -ratio; return lastSnap; }, onRelease() { syncIndex(); draggable.isThrowing && (indexIsDirty = true); }, onThrowComplete: () => { syncIndex(); wasPlaying && tl.play(); } })[0]; tl.draggable = draggable; } tl.closestIndex(true); lastIndex = curIndex; onChange && onChange(items[curIndex], curIndex); timeline = tl; return () => window.removeEventListener("resize", onResize); // cleanup }); return timeline; } function openServicesPanel() { const aside = document.getElementById('menu-aside'); gsap.context(() => { const tl = gsap.timeline({ onComplete:() => { aside.querySelector('.services-panel').classList.add('active'); aside.querySelector('.contact-panel').classList.remove('active'); aside.querySelector('.menu-panel').classList.remove('active'); } }) .to('.menu-aside-content', { autoAlpha: 0, duration: 0.5 }) .to('.menu-panel', { x: 650, xPercent: 0, duration: 1, ease: 'back.out(1.1)' }) .to('.services-panel', { xPercent: 0, x: 0, right: 0, duration: 1, ease: 'back.out(1.1)' }, '<0.1') .to('.services-panel .menu-aside-content', { autoAlpha: 1, duration: 0.5 }); }, aside); } function openContactPanel() { const aside = document.getElementById('menu-aside'); gsap.context(() => { const tl = gsap.timeline({ onComplete:() => { aside.querySelector('.services-panel').classList.remove('active'); aside.querySelector('.contact-panel').classList.add('active'); aside.querySelector('.menu-panel').classList.remove('active'); } }) .to('.menu-aside-content', { autoAlpha: 0, duration: 0.5 }) .to('.menu-panel', { x: 650, xPercent: 0, duration: 1, ease: 'back.out(1.1)' }) .to('.services-panel', { x: 650, xPercent: 0, duration: 1, ease: 'back.out(1.1)' }, '<0.1') .to('.contact-panel .menu-aside-content', { autoAlpha: 1, duration: 1 }); }, aside); } function openAside() { const aside = document.getElementById('menu-aside'); window.asideCtx = gsap.context(() => { const tl = gsap.timeline({ onComplete:() => { // aside.querySelector('.services-panel').classList.remove('active'); // aside.querySelector('.contact-panel').classList.remove('active'); aside.querySelector('.menu-panel').classList.add('active'); } }); tl.set(aside, {display: 'block', pointerEvents: 'auto'}) .to(aside, { autoAlpha: 1, duration: 0.5 }) .to('.menu-panel', { xPercent: 0, x: 0, duration: 1, ease: 'back.out(1.1)' }) // .to('.services-panel', { // xPercent: 0, // x: 0, // right: 0, // duration: 1, // ease: 'back.out(1.1)' // }, '<0.1') // .to('.contact-panel', { // xPercent: 0, // x: 0, // duration: 1, // ease: 'back.out(1.1)', // right: 50 // }, '<0.1') window.asideTl = tl; }, aside); } function closeAside() { const aside = document.getElementById('menu-aside'); gsap.context(() => { // aside.querySelector('.services-panel').classList.remove('active'); // aside.querySelector('.contact-panel').classList.remove('active'); aside.querySelector('.menu-panel').classList.remove('active'); gsap.timeline() // .to('.contact-panel', { // xPercent: 100, // x: 0, // right: -50, // duration: 0.8 // }, 0.5) // .to('.services-panel', { // xPercent: 100, // x: 0, // right: -50, // duration: 0.9 // }, 0.5) .to('.menu-panel', { xPercent: 100, x: 0, right: -50, duration: 1 }, 0.5) .set('.menu-panel, .services-panel, .contact-panel', { 'transform': 'translateX(100%)' }) .set(aside, {display: 'block', pointerEvents: 'auto'}) .to(aside, { autoAlpha: 0, duration: 0.5 }, 1.5); }, aside); }