Reputation: 364
I have an element bordered with an Svg:
.element-bordered-with-svg {
border-image-source: url('images/border.svg');
[....]
}
Inside the border.svg
there is a CSS animation (defined in the <style>
tag), like so:
<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>
I would like to start the animation only when I hover the .element-bordered-with-svg
element.
Of course, because it is not inlined, the <svg>
knows nothing about the elements of the main DOM and vice versa.
Is there a possible solution, perhaps in JavaScript, to this problem?
EDITED: following the advice of @Danny '365CSI' Engelman in the comments I've tried with this solution (see the snippet):
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>
put the SVG on page with the animations set to "paused"
in JavaScript, I've set a mouseenter
event in which the animations of the SVG in page are set to "running", the Svg is transformed in data uri and assigned to the CSS border-image
property as URL value
Similarly, in Js I've set a mouseleave
event in which the animations of the Svg in page are set to "paused", the Svg is transformed in data uri and assigned to the CSS border-image
property as URL value
The result is that when you "mouseenter" the blockquote, the animation starts but when you "mouseleave" the blockquote the animation visually reset to 0 and when you "mouseenter" again you can see that in the meantime the animation is over.
Why?
EDITED 2: the previous snippet had 2 problems:
mouseleave
to "freeze" itborder-src
before you have to change the animation-duration
according to the remaining time to play (so I've inserted a timer function) and you have to change @keyframes
injecting the new "start values" using CSSom: I've made this last thing but unfortunately the update CSSom is not "read" by the Svg as data uri and on mouseenter
again the animation restarts...Upvotes: 0
Views: 446
Reputation: 21163
Last example: animated SVG border with play/pause states
<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>
Upvotes: 1
Reputation: 21163
Create your own Custom Element <bordered-svg>
(supported in all modern Browsers) that dynamically creates the SVG with all unique values, no need for shadowDOM then.
If you add shadowDOM the CSS gets simpler, but controlling it from main DOM becomes more difficult.
<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>
Upvotes: 1