【问题标题】:Start CSS animation on hover in a SVG used as border-source在用作边框源的 SVG 中悬停时启动 CSS 动画
【发布时间】:2021-01-28 19:05:08
【问题描述】:

我有一个以 Svg 为边框的元素:

.element-bordered-with-svg {

  border-image-source: url('images/border.svg');
  [....]
}

border.svg 内部有一个 CSS 动画(在 <style> 标签中定义),如下所示:

<svg class="svg-frame-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 337 198">
 
   <style>
     .svg-frame-1:hover > path {
       animation-play-state: running;
     }

     path {
       stroke:#BEA757;
       fill-opacity:0;
       stroke-width:1;
       stroke-dasharray: 1948;
       stroke-dashoffset:1948;
       animation-name: dash1;
       animation-duration: 2s;
       animation-fill-mode: forwards;
       animation-delay: 2s;  
       animation-play-state: paused; 
      }

   @keyframes dash1 {
      0%  { stroke-dashoffset:1948;}
     100%{stroke-dashoffset:0;}
    } 

  </style>

  [...]

</svg>
  

我只想在悬停.element-bordered-with-svg 元素时开始动画。

当然,因为它不是内联的,&lt;svg&gt; 对主 DOM 的元素一无所知,反之亦然。

对于这个问题有没有可能的解决方案,也许是在 JavaScript 中?

已编辑:在我尝试使用此解决方案的 cmets 中遵循 @Danny '365CSI' Engelman 的建议(请参阅 sn-p):

function docReady(fn) {
    // see if DOM is already available
    if (
      document.readyState === 'complete' ||
      document.readyState === 'interactive'
    ) {
      // call on next available tick
      setTimeout(fn, 1);
    } else {
      document.addEventListener('DOMContentLoaded', fn);
    }
  }
  function escapeRegExp(str) {
    return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
  }

  function replaceAll(str, find, replace) {
    return str.replace(new RegExp(escapeRegExp(find), 'g'), replace);
  }

  function transformInDataUri(id) {
    var svgText = new XMLSerializer().serializeToString(
      document.getElementById(id),
    );
    var raw = svgText;
    var encoded = raw.replace(/\s+/g, ' ');

    // According to Taylor Hunt, lowercase gzips better ... my tiny test confirms this
    encoded = replaceAll(encoded, '%', '%25');
    encoded = replaceAll(encoded, '> <', '><'); // normalize spaces elements
    encoded = replaceAll(encoded, '; }', ';}'); // normalize spaces CSS
    encoded = replaceAll(encoded, '<', '%3c');
    encoded = replaceAll(encoded, '>', '%3e');
    encoded = replaceAll(encoded, '"', "'");
    encoded = replaceAll(encoded, '#', '%23'); // needed for IE and Firefox
    encoded = replaceAll(encoded, '{', '%7b');
    encoded = replaceAll(encoded, '}', '%7d');
    encoded = replaceAll(encoded, '|', '%7c');
    encoded = replaceAll(encoded, '^', '%5e');
    encoded = replaceAll(encoded, '`', '%60');
    encoded = replaceAll(encoded, '@', '%40');
    var uri = 'url("data:image/svg+xml;charset=UTF-8,' + encoded + '")';
    return uri;
  }

  function getCurrentAnimatePropertyValue(el, propertyName) {
    let value = getComputedStyle(el, null).getPropertyValue(propertyName);

    return value;
  }

  function getTimer() {
    console.log('ok')
  
    let elapsedTime = 0;
    let timer;
  
    function start() {
          timer = window.setInterval(function() {
            elapsedTime += 100
            console.log(elapsedTime)
          }, 100)
     }
    
    function pause() {
      window.clearInterval(timer)
      console.log(elapsedTime)
      return elapsedTime
    }
  
    function reset() {
      window.clearInterval(timer)
      elapsedTime = 0;
      console.log(elapsedTime)
      return elapsedTime
    }
  
    
  
  
    
    return {
      start: start,
      pause: pause,
      reset: reset
    }
  
  }
  
  
  function getUpdatedDurations(currentDurations, elapsedTime) {
    let animationDurations = currentDurations; // '2s, 1s';
    let values = animationDurations.replace(/\s/g, "").split(',').map(el => { 
      let finalEl = parseFloat(el.replace("s", "")) * 1000;
      return finalEl;
    })
    let updatedValues = values.map(duration => duration - elapsedTime )
    
    let updatedValuesAsString = updatedValues.map(duration => {
      
      return (duration/1000).toString(10) + 's';
    })
    return updatedValuesAsString.toString()
  }
  

  // svg, [{ 'dash1': {'dash-offset': '1000px' }}, { 'fill': {'fill-opacity': 1 }}]
  function changeAnimationStartKeyFrame(animations) {
    
    for (let index = 0; index <document.styleSheets.length; index++) {
      let stylesheet = document.styleSheets[index];
      if(stylesheet['title'] === 'svg') {
        animations.map(animation => {
      
          let animationName = Object.keys(animation)[0];
  
          let cssRules = stylesheet['cssRules']
  
  
          let objectWithRules = animation[animationName];
          
          let propertyName = Object.keys(objectWithRules)[0]
          let updatedValue = objectWithRules[propertyName];
  
          for (let index = 0; index < cssRules.length; index++) {
            let cssRule  = cssRules[index];
           
            if(cssRule.type === 7 && cssRule.name === animationName) {
              console.log(propertyName)
              let CSSKeyframesRule = cssRule; 
    
              if(animationName === 'dash1') {
                CSSKeyframesRule.deleteRule("0%");
                CSSKeyframesRule.appendRule(`0% { ${propertyName}: ${updatedValue}; }`);
              }
              else {
                CSSKeyframesRule.deleteRule("80%");
                CSSKeyframesRule.appendRule(`80% { ${propertyName}: ${updatedValue}; }`);
              }
              
            }
            
          }
      
      })
      }
      console.log(stylesheet)
    }

   
   
    
  }

  docReady(function () {
    // charset reportedly not needed ... I need to test before implementing

    console.log(Array.from(document.styleSheets))

    var svgAsBorderSelector = 'svg-as-border';
    var svg = document.getElementById(svgAsBorderSelector);
    var svgAnimatedSelectors = '.path-1';
    var svgElementsToAnimate = svg.querySelectorAll(svgAnimatedSelectors);

    let divToBorderWithAnimatedSvg = document.querySelector('.frame-1');

    divToBorderWithAnimatedSvg.style.borderImageSource = transformInDataUri(
      svgAsBorderSelector,
    );

    let currentFillOpacity;
    let currentStrokeDashOffset;
    let currentStrokeDashArray;
    let timer = getTimer(), elapsedTime;

    divToBorderWithAnimatedSvg.addEventListener('mouseenter', (e) => {
      for (var i = 0, max = svgElementsToAnimate.length; i < max; i++) {
        let element = svgElementsToAnimate[i];

        currentStrokeDashOffset = getCurrentAnimatePropertyValue(
          element,
          'stroke-dashoffset',
        );

        currentStrokeDashArray = getCurrentAnimatePropertyValue(
          element,
          'stroke-dasharray',
        );

        currentFillOpacity = getCurrentAnimatePropertyValue(
          element,
          'fill-opacity',
        );
        document.body.clientHeight
        element.style.webkitAnimationName = 'dash1';
        timer.start();
        element.style.strokeDashoffset = currentStrokeDashOffset;
        element.style.strokeDasharray = currentStrokeDashArray;
        element.style.fillOpacity = currentFillOpacity;
        element.style.animationPlayState = 'running, running';
      }

      divToBorderWithAnimatedSvg.style.borderImageSource = transformInDataUri(
        svgAsBorderSelector,
      );
    });

    divToBorderWithAnimatedSvg.addEventListener('mouseleave', (e) => {
      for (var i = 0, max = svgElementsToAnimate.length; i < max; i++) {
        let element = svgElementsToAnimate[i];
        

        currentStrokeDashOffset = getCurrentAnimatePropertyValue(
          element,
          'stroke-dashoffset',
        );

        currentStrokeDashArray = getCurrentAnimatePropertyValue(
          element,
          'stroke-dasharray',
        );

        currentFillOpacity = getCurrentAnimatePropertyValue(
          element,
          'fill-opacity',
        );
     
        let currentAnimationTime = getCurrentAnimatePropertyValue(element, 'animation-duration');
        elapsedTime = timer.pause();
        let updatedAnimationTime = getUpdatedDurations(currentAnimationTime, elapsedTime)
        element.style.animationPlayState = 'paused, paused';
        element.style.webkitAnimationName = 'none';
        element.style.strokeDashoffset = currentStrokeDashOffset;
        element.style.strokeDasharray = currentStrokeDashArray;
        element.style.fillOpacity = currentFillOpacity;
        element.style.animationDuration = updatedAnimationTime;
        changeAnimationStartKeyFrame([{ 'dash1': {'stroke-dashoffset': currentStrokeDashOffset }}, { 'fill': {'fill-opacity': currentFillOpacity }}])     

      }
      divToBorderWithAnimatedSvg.style.borderImageSource = transformInDataUri(
        svgAsBorderSelector,
      );
    });
  });
.frame-1 {
    border: 22px solid;
    border-image-slice: 41;
    border-image-width: 32px;
    border-image-outset: 0;
    border-image-repeat: stretch;
    }

.blockquote-container blockquote, .blockquote-container blockquote p {
    color: #a17c4a;
}

.blockquote-container blockquote {
    padding: .5em;
}
<div class="blockquote-container">
        <blockquote class="frame-1">
            <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec placerat ex enim, nec tempus nisl commodo a. Nullam eu odio ut neque interdum mollis quis ac velit. Etiam pulvinar aliquam auctor.</p>

        </blockquote>
    </div>
 <svg  id='svg-as-border' class='svg-frame-1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 337 198'>
        <g fill='none' fill-rule='evenodd'>
            <style title="svg">
                .path-1 {
                    stroke: #BEA757;
                    fill-opacity: 0;
                    stroke-width: 1;
                    stroke-dasharray: 1948px;
                    stroke-dashoffset: 1948px;
                    animation-name: dash1, fill;
                    animation-duration: 2s, 1s;
                    animation-fill-mode: forwards;
                    animation-delay: 0s, 0s;
                    animation-play-state: paused, paused;

                }




                @keyframes dash1 {
                    0% {
                        stroke-dashoffset: 1948px;
                    }

                    100% {
                        stroke-dashoffset: 0;
                    }
                }



                @keyframes fill {
                    80% {
                        fill-opacity: 0;
                    }

                    100% {
                        fill-opacity: 1;
                    }
                }
            </style>
            <path class='path-1' fill='#BEA757'
                d='M22.68,176.6 L315.68,176.6 L315.68,22.6 L22.68,22.6 L22.68,176.6 Z M331.596839,180.6 L332.68,180.6 L332.68,193.6 L319.68,193.6 L319.68,180.6 L331.596839,180.6 Z M18.68,191.363552 L18.68,193.6 L5.68,193.6 L5.68,180.6 L18.68,180.6 L18.68,191.363552 Z M9.65296139,18.6 L5.68,18.6 L5.68,5.6 L18.68,5.6 L18.68,18.6 L9.65296139,18.6 Z M319.68,12.191199 L319.68,5.6 L332.68,5.6 L332.68,18.6 L319.68,18.6 L319.68,12.191199 Z M331.333807,22.158844 L336.68,22.158844 L336.68,0.6 L315.049474,0.6 L315.049474,17.9061919 L22.3105259,17.9061919 L22.3105259,0.6 L0.68,0.6 L0.68,22.158844 L18.0579387,22.158844 L18.0579387,176.041156 L0.68,176.041156 L0.68,197.6 L22.3105259,197.6 L22.3105259,180.293808 L315.049474,180.293808 L315.049474,197.6 L336.68,197.6 L336.68,176.041156 L319.304352,176.041156 L319.304352,22.158844 L331.333807,22.158844 L331.333807,22.158844 Z' />

        </g>
    </svg>
  1. 将 SVG 放在页面上,动画设置为“暂停”

  2. 在 JavaScript 中,我设置了一个 mouseenter 事件,其中页面中 SVG 的动画设置为“运行”,Svg 转换为数据 uri 并分配给 CSS border-image 属性作为 URL 值

  3. 同样,在 Js 中我设置了一个 mouseleave 事件,其中页面中的 Svg 的动画设置为“暂停”,Svg 转换为数据 uri 并作为 URL 分配给 CSS border-image 属性价值

结果是,当您“鼠标输入”块引用时,动画开始,但当您“鼠标离开”块引用时,动画视觉上重置为 0,当您再次“鼠标输入”时,您可以看到动画同时结束.

为什么?

EDITED 2:之前的 sn-p 有 2 个问题:

  1. 您必须将mouseleave 上的“动画名称”设置为“无”才能“冻结”它
  2. 当你在border-src之前再次注入Svg作为数据uri时,你必须根据剩余的播放时间更改animation-duration(所以我插入了一个计时器功能)和您必须更改 @keyframes 使用 CSSom 注入新的“起始值”:我已经做了最后一件事,但不幸的是,更新 CSSom 没有被 Svg 作为数据 uri“读取”,并且在 mouseenter 上,动画再次重新启动。 ..

【问题讨论】:

  • 动画从何而来?如果你有动画,贴出代码,不要告诉我们它在哪里,不要给我们看
  • 抱歉,问题已更新
  • 图像不是交互式的,因此您需要重写它以使 SVG 内联或通过 iframe 或对象标签加载。
  • 快完成了,阿明塔!! SO Snippet 看起来很酷!您现在需要做的就是正确重新启动动画,请参阅:css-tricks.com/restart-css-animation
  • 感谢@Danny'365CSI'Engelman!不幸的是,重新启动动画是最小的问题。这里的问题是,当您“暂停”时,它什么也不暂停,而是重置为 0。我认为这是技术的本质限制,我没有看到一个 API 可以准确地获取和控制动画的进度并准确地从你停止的地方恢复。不过感谢您的帮助!

标签: javascript css animation svg border


【解决方案1】:

创建您自己的自定义元素&lt;bordered-svg&gt;(所有现代浏览器都支持),动态创建具有所有唯一值的 SVG,然后不需要 shadowDOM。

如果添加 shadowDOM,CSS 会变得更简单,但从主 DOM 控制它会变得更加困难。

<style>
  div { width: 200px;display: grid;grid-template-columns: 1fr 1fr }
  svg { width: 100%;vertical-align:top;cursor:pointer }
</style>
<div>
  <bordered-svg color="red"></bordered-svg>
  <bordered-svg color="gold"></bordered-svg>
  <bordered-svg color="green"></bordered-svg>
  <bordered-svg color="rebeccapurple"></bordered-svg>
</div>
<script>
  customElements.define("bordered-svg", class extends HTMLElement {
    connectedCallback() {
      let uniqueID = "bordered" + Math.random().toString().substr(2, 8);
      this.innerHTML = `
        <svg id="${uniqueID}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 6 6">
        <style>
        #${uniqueID} path {
          stroke:${this.getAttribute("color")||"grey"};
          stroke-width:3; stroke-dasharray: 12;
          animation-name: animation${uniqueID};
          animation-duration: 5s;
          animation-play-state: paused; 
        }
        @keyframes animation${uniqueID} {
          0%  { stroke-dashoffset:0 }
          100%{ stroke-dashoffset:1000 }
        } 
        </style>
        <path fill='none' d='M0 0h6v6h-6z'></path></svg>`;
      let style = this.querySelector(`#${uniqueID} path`).style;
      this.onmouseenter = () => style.animationPlayState = "running";
      this.onmouseleave = () => style.animationPlayState = "paused";
    }
  });
</script>

【讨论】:

  • 这个动画太棒了...如果它在完成后没有重置,直到用户将鼠标移开。
  • 是的,对不起,我编程很烂。 OP 想要控制鼠标悬停时的动画,我可以只发布 2 条事件监听器行...
  • 使用像您这样的解决方案,您如何使用现有的 Svg 文件,如输入边框而不拉伸它(border-image 属性让您控制 Svg 的拉伸...)?跨度>
  • 将 SVG 作为 DataURI 写入背景图像:css-tricks.com/lodge/svg/09-svg-data-uris
  • 这需要我大约 40 分钟才能完成所有工作。我也必须学习一次,这将花费你一两天的时间,现在你已经有了代码和 CSS 技巧文章的链接。 StackOverflow 是为了帮助你,而不是为了完成你的工作。
【解决方案2】:

最后一个示例:带有播放/暂停状态的动画 SVG 边框

<style>
  animated-border {
    --animated-border-stroke: black;
    --animated-border-fill: darkgoldenrod;
    --animated-border-playstate: running;
    --animated-border-height: 100px;
  }

  animated-border [slot="content"] {
    text-align: center;
    height: 100%;
    padding-top: 2em;
  }

</style>

<animated-border>
  <div slot="content">
    <h2>
      Once Upon A Time...
    </h2>
    <b>there was a Web Component</b>
  </div>
</animated-border>




<script>
  customElements.define("animated-border", class extends HTMLElement {
    // Animate a border picture frame. paths 1-3 are animated, path 4 is the always visible path'
    constructor() {
      let animate = (nr, duration, delay, dash = 2000) => `
      #path${nr}{
        stroke-dasharray:${dash};
        stroke-dashoffset:${dash};
        animation-name:dash${nr};
        animation-duration:${duration}s;
        animation-delay:${delay}s
      }
      @keyframes dash${nr}{
          0%{stroke-dashoffset:${dash}}
         80%{fill-opacity:0}
        100%{fill-opacity:1;stroke-dashoffset:0}}`;
      //console.warn(animate(1, "2s,1s", "0s,0s"));
      let style = `<style>:host{
        display:inline-block;
        --height:var(--animated-border-height,150px);
        height: var(--height);
        width:calc(2 * var(--height));
      }
      svg{
        background:var(--animated-border-background,transparent);
        width:100%;
        height:100%;
      }
      [id*="path"]{
        stroke:var(--animated-border-stroke);
        fill:var(--animated-border-fill);
        fill-opacity:0;stroke-width:2;
        animation-fill-mode:forwards;
        animation-play-state:var(--animated-border-playstate,paused)}
      ${animate(1,1,0)}
      ${animate(2,2,0)}
      ${animate(3,2,1)}
      </style>`;
      super().attachShadow({
        mode: "open"
      }).innerHTML = style + `<div></div>`;
      this.border = this.shadowRoot.querySelector('div');
    }
    render() {
      let random = Math.floor(1000 + Math.random() * 9000);
      let path = (nr, d) => `<path id='path${nr}' random='${random}' d='${d}'/>`;
      let svg =
        path(1, 'm23 177l293 0l0-154l-293 0l0 154zm309 4l1 0l0 13l-13 0l0-13l12 0zm-313 10l0 3l-13 0l0-13l13 0l0 10zm-9-172l-4 0l0-13l13 0l0 13l-9 0zm310-7l0-6l13 0l0 13l-13 0l0-7zm11 10l6 0l0-21l-22 0l0 17l-293 0l0-17l-21 0l0 21l17 0l0 154l-17 0l0 22l21 0l0-18l293 0l0 18l22 0l0-22l-18 0l0-154l12 0l0 0z') +
        path(2, 'm31 185l275 0c3-9 9-15 18-18l0-136c-9-3-15-9-18-17l-275 0c-3 8-9 14-17 17l0 136c8 3 14 9 17 18zm278 4l-280 0l-1-2c-2-8-9-14-17-17l-1 0l0-142l1 0c8-3 15-9 17-17l1-1l280 0l0 1c3 8 9 14 17 17l2 0l0 142l-2 0c-8 3-14 9-17 17l0 2l0 0z') +
        path(3, 'm49 194l241 0c0-6 1-11 3-16c2-5 6-10 10-14c4-4 9-8 14-10c5-2 10-3 16-3l0-102c-6-1-11-2-16-4c-5-2-10-5-14-10c-4-4-8-9-10-14c-2-5-3-10-3-15l-241 0c-1 5-2 10-4 15c-2 5-5 10-9 14c-5 5-9 8-15 10c-5 2-10 3-15 4l0 102c5 0 10 1 15 3c6 2 10 6 15 10c4 4 7 9 9 14c2 5 3 10 4 16zm245 4l-250 0l0-3c0-22-19-40-41-40l-2 0l0-111l2 0c22 0 41-19 41-41l0-2l250 0l0 2c0 22 18 41 41 41l2 0l0 111l-2 0c-23 0-41 18-41 40l0 3l0 0z') +
        path(4, 'm294 185l0 4l-251 0l0-4l251 0zm-280-142l0 111l-4 0l0-111l4 0zm314 0l0 111l-4 0l0-111l4 0zm-34-33l0 4l-251 0l0-4l251 0zm-263 175l275 0c3-9 9-15 18-18l0-136c-9-3-15-9-18-17l-275 0c-3 8-9 14-17 17l0 136c8 3 14 9 17 18zm-2 4l-1-2c-2-8-9-14-17-17l-1 0l0-142l1 0c8-3 15-9 17-17l1-1l280 0l0 1c3 8 9 14 17 17l2 0l0 142l-2 0c-8 3-14 9-17 17l0 2l-280 0z');
      svg = svg.replace(/\s+/g, ' ');
      svg = svg.replace(/#/g, "%23");
      svg = svg.replace(/"/g, "'");
      svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 340 200' fill-rule='evenodd'>${svg}<foreignObject x="0" y="0" width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%;position:absolute"><slot name="content"></slot></div>
  </foreignObject></svg>`;
      this.shadowRoot.querySelector("div").innerHTML = svg;
    }
    connectedCallback() {
      this.onmouseenter = () => this.play();
      this.onmouseleave = () => this.pause();
      this.border.addEventListener("animationend", (evt) => {
        this.animatedpathcount--;
        if (!this.animatedpathcount) this.end();
      })
      this.playstate = "paused";
      this.restart();
    }
    log(...args) {
      console.log(`%cBorderMeister`, "background:teal;color:white", ...args);
    }
    set playstate(state = "running") {
      this.style.setProperty("--animated-border-playstate", state);
    }
    get playstate() {
      return this.style.getPropertyValue("--animated-border-playstate");
    }
    restart() {
      this.log("restart");
      this.animatedpathcount = 3;
      this.render();
    }
    play() {
      this.log("play")
      if (!this.animatedpathcount) this.restart();
      this.playstate = "running";
    }
    pause() {
      this.log("pause")
      this.playstate = "paused";
    }
    end() {
      this.log("end")
      this.playstate = "paused";
    }
  });

</script>

【讨论】:

  • 谢谢,这是一个非常礼貌的解决方案,但不幸的是,我认为它无法正常工作 - 从原始问题开始 - 使用 border-source 属性和 data-uri 注入,请参阅我在 main 上的最后一次尝试问题...
  • 生活就是做出牺牲,在边界(或 IMG src)中播放的动画不会发出状态事件。
猜你喜欢
  • 1970-01-01
  • 2015-02-19
  • 2020-09-26
  • 2021-02-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-02
相关资源
最近更新 更多