【发布时间】: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 元素时开始动画。
当然,因为它不是内联的,<svg> 对主 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>
-
将 SVG 放在页面上,动画设置为“暂停”
-
在 JavaScript 中,我设置了一个
mouseenter事件,其中页面中 SVG 的动画设置为“运行”,Svg 转换为数据 uri 并分配给 CSSborder-image属性作为 URL 值 -
同样,在 Js 中我设置了一个
mouseleave事件,其中页面中的 Svg 的动画设置为“暂停”,Svg 转换为数据 uri 并作为 URL 分配给 CSSborder-image属性价值
结果是,当您“鼠标输入”块引用时,动画开始,但当您“鼠标离开”块引用时,动画视觉上重置为 0,当您再次“鼠标输入”时,您可以看到动画同时结束.
为什么?
EDITED 2:之前的 sn-p 有 2 个问题:
- 您必须将
mouseleave上的“动画名称”设置为“无”才能“冻结”它 - 当你在
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