Creative Effects Skill — Kanvas Site

SkillWeb & browsing

Expert knowledge for creative visual effects: CSS clip-path reveals, GSAP logo assembly, shimmer overlays, magnetic hover, glitch text, and the scroll-cube breakup/reassembly sequence. All implemented with SCSS + vanilla JS — no React, no Framer Motion.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Creative Effects Skill — Kanvas Site skill

About this capability

Website for Kanvas extension

What this skill tells your AI

The instructions your AI receives, as published by meshery-extensions/kanvas-site in .agents/skills/kanvas-animations/skills/creative-effects/SKILL.md and read by ahel’s review.

Expert knowledge for creative visual effects: CSS clip-path reveals, GSAP logo assembly, shimmer overlays, magnetic hover, glitch text, and the scroll-cube breakup/reassembly sequence. All implemented with SCSS + vanilla JS — no React, no Framer Motion.

Scroll Cube: Logo Assembly Effect

The most complex creative effect on the site. The Kanvas cube SVG breaks apart on load and reassembles as the user scrolls. Implemented entirely in main.js.

Architecture

// main.js — initScrollCube()
const initScrollCube = () => {
    const wrapper = document.querySelector('.scroll-cube-wrapper');
    if (!wrapper) return;

    // 1. Inject SVG pieces (each piece is a <svg> with a <path> clip)
    // 2. GSAP.set() scatters pieces with random rotateY + translate
    // 3. ScrollTrigger scrub timeline reassembles them to origin position
    // 4. On complete: add .reunited-float class for CSS float animation
};

SCSS

.scroll-cube-wrapper {
    position: relative;
    width: 200px;
    height: 200px;
}

.scroll-cube-piece {
    position: absolute;
    inset: 0;
    will-change: transform, opacity;
}

.reunited-float {
    animation: reunitedFloat 6s ease-in-out infinite;
}

@keyframes reunitedFloat {
    0%, 100% { transform: translateY(0) rotateY(0deg); }
    25%       { transform: translateY(-12px) rotateY(5deg); }
    75%       { transform: translateY(6px)   rotateY(-3deg); }
}

@keyframes glowPulse {
    0%, 100% { filter: drop-shadow(0 0 8px  rgba($primary, 0.3)); }
    50%       { filter: drop-shadow(0 0 20px rgba($primary, 0.6)); }
}

.reunited-glow {
    animation: glowPulse 3s ease-in-out infinite;
}

CSS clip-path Reveals

Wipe reveal (entrance)

// Clip from bottom — element appears to rise into view
.clip-reveal {
    clip-path: inset(100% 0 0 0);
    transition: clip-path 0.7s cubic-bezier(0.16, 1, 0.3, 1);

    &.is-visible {
        clip-path: inset(0% 0 0 0);
    }
}

GSAP clip-path scrub

gsap.fromTo('.reveal-card',
    { clipPath: 'inset(0 100% 0 0)' },
    { clipPath: 'inset(0 0% 0 0)',
      scrollTrigger: { trigger: '.reveal-card', start: 'top 85%', end: 'top 50%', scrub: 1 }
    }
);

Split curtain (top + bottom)

// Two panels that retract in opposite directions on scroll
gsap.to('.curtain-top', {
    scaleY: 0, transformOrigin: 'top',
    scrollTrigger: { trigger: '.curtain-wrapper', start: 'top 80%', end: 'top 30%', scrub: 1 }
});
gsap.to('.curtain-bottom', {
    scaleY: 0, transformOrigin: 'bottom',
    scrollTrigger: { trigger: '.curtain-wrapper', start: 'top 80%', end: 'top 30%', scrub: 1 }
});

Shimmer Overlay (CSS only)

@keyframes shimmer {
    0%   { background-position: -200% center; }
    100% { background-position:  200% center; }
}

.shimmer-card {
    position: relative;
    overflow: hidden;

    &::after {
        content: '';
        position: absolute;
        inset: 0;
        background: linear-gradient(
            105deg,
            transparent 40%,
            rgba(255, 255, 255, 0.06) 50%,
            transparent 60%
        );
        background-size: 200% 100%;
        animation: shimmer 2.5s linear infinite;
        pointer-events: none;
    }
}

Glitch Text Effect (CSS only)

.glitch {
    position: relative;

    &::before,
    &::after {
        content: attr(data-text);
        position: absolute;
        inset: 0;
    }

    &::before {
        color: rgba($primary, 0.8);
        clip-path: inset(40% 0 61% 0);
        animation: glitchTop 2s infinite linear;
    }

    &::after {
        color: rgba(255, 255, 255, 0.8);
        clip-path: inset(85% 0 7%  0);
        animation: glitchBottom 2s infinite linear;
    }
}

@keyframes glitchTop {
    0%,  90%  { transform: translateX(0); }
    91%        { transform: translateX(-2px); clip-path: inset(40% 0 61% 0); }
    93%        { transform: translateX(2px);  clip-path: inset(15% 0 80% 0); }
    95%        { transform: translateX(-1px); clip-path: inset(70% 0 25% 0); }
    100%       { transform: translateX(0);   clip-path: inset(40% 0 61% 0); }
}

@keyframes glitchBottom {
    0%,  92%  { transform: translateX(0); }
    93%        { transform: translateX(2px);  clip-path: inset(85% 0 7% 0); }
    95%        { transform: translateX(-2px); clip-path: inset(50% 0 45% 0); }
    97%        { transform: translateX(1px);  clip-path: inset(20% 0 75% 0); }
    100%       { transform: translateX(0);   clip-path: inset(85% 0 7% 0); }
}

Hugo template usage:

<span class="glitch" data-text="{{ .Title }}">{{ .Title }}</span>

Magnetic Hover (vanilla JS)

// static/scripts/main.js or a small inline script
document.querySelectorAll('[data-magnetic]').forEach(el => {
    el.addEventListener('mousemove', e => {
        const rect = el.getBoundingClientRect();
        const relX = (e.clientX - rect.left) / rect.width  - 0.5;
        const relY = (e.clientY - rect.top)  / rect.height - 0.5;
        gsap.to(el, { x: relX * 12, y: relY * 8, duration: 0.3, ease: 'power2.out' });
    });
    el.addEventListener('mouseleave', () => {
        gsap.to(el, { x: 0, y: 0, duration: 0.5, ease: 'elastic.out(1, 0.5)' });
    });
});
<button class="btn-primary" data-magnetic>Get Started</button>

Counter Scramble (fire-once)

Number counters are the only place where once: true is appropriate:

// main.js — animateCounters()
const animateCounters = () => {
    document.querySelectorAll('[data-count]').forEach(el => {
        const target = parseInt(el.dataset.count, 10);
        const suffix = el.dataset.suffix || '';
        gsap.fromTo(el,
            { innerText: 0 },
            {
                innerText: target,
                duration: 2,
                ease: 'power2.out',
                snap: { innerText: 1 },
                onUpdate() { el.textContent = Math.round(this.targets()[0]._gsap.innerText) + suffix; }
            }
        );
    });
};

ScrollTrigger.create({
    trigger: '.hero-stats',
    start: 'top 70%',
    once: true,
    onEnter: animateCounters,
});

GSAP Creative Sequence (Timeline)

For complex multi-step entrance sequences (not scroll-scrub):

// One-shot entrance (acceptable for above-the-fold, page load only)
const tl = gsap.timeline({ defaults: { ease: 'power4.out' } });
tl.from('.hero-badge',    { opacity: 0, y: 20, scale: 0.9, duration: 0.5 })
  .from('.hero-title',    { opacity: 0, y: 30, duration: 0.7 }, '-=0.2')
  .from('.hero-subtitle', { opacity: 0, y: 20, duration: 0.6 }, '-=0.4')
  .from('.hero-cta',      { opacity: 0, y: 20, scale: 0.96, duration: 0.5 }, '-=0.3');

This pattern is used in main.js for the initial header entrance only. All below-fold content uses scrub-based animations.

Anti-Patterns

  • rotation: 360 for continuous spin — too dramatic, use slow directional drift
  • bounce.out or elastic.out in scroll-scrub contexts — these eases need duration, not scrub
  • filter: blur() animated on every frame — use CSS opacity instead where possible
  • Inline style="..." — all creative effects belong in SCSS
  • Hard-coded #00b39f — always use $primary

Signals

GitHub stars
39
Forks
49
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
creative-effects
Source
github.com/meshery-extensions/kanvas-site