MaurycyLiebner / enve

Flexible, user expandable 2D animation software for Linux and Windows.
https://MaurycyLiebner.github.io/
GNU General Public License v3.0
1.1k stars 83 forks source link

Proposal: simple web player for SVGs #125

Open mestaritonttu opened 4 years ago

mestaritonttu commented 4 years ago

I thought it would be nice to have player controls for the exported SVG animations. I had recently worked with the very nice termtosvg and I realised that its player could be quite easily modified to work with enve's SMIL output.

I added the player elements and styles into the SVG. The JS code is not very elegant, I tried to clean it up as best I could. One minor issue is that after you scrub the animation, the mouseup event usually does not fire. This is also seen in the original termtosvg player. It works after clicking outside the slider.

I only tested this with a very simple animation, so I am not sure how robust the JS code is. If you like the idea of offering such a player optionally, I guess you could allow for some customisation like autoplay and looping.

Regarding accessibility:

The element has tabindex="0" so screen readers find it

The has role="group" as opposed to "img" because it is interactive

There is a element. A <desc> element is also a possibility, if wanting to describe in more detail</p> <p>The player controls have aria-hidden="true" (otherwise the screen reader will make noise about them being "clickable")</p> <p>I tested with NVDA on Windows and it read the <text> element just as I wanted. My intention is to create animations with subtitle-like text, so screen reader users have access to what is essentially being presented.</p> <p>I took some tips from Deque's article <a rel="noreferrer nofollow" target="_blank" href="https://www.deque.com/blog/creating-accessible-svgs/">Creating Accessible SVGs</a> and elsewhere.</p> <p>Note that if you want to try my example locally, you have to disable same origin policy in your browser because it loads the SVG into an object element. Instructions for <a rel="noreferrer nofollow" target="_blank" href="https://stackoverflow.com/questions/3102819/disable-same-origin-policy-in-chrome">Chrome</a>, <a rel="noreferrer nofollow" target="_blank" href="https://stackoverflow.com/questions/17088609/disable-firefox-same-origin-policy">Firefox</a>.</p> <p>Thanks a lot for creating enve!</p> <p>The SVG:</p> <pre><code><?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- Created with enve https://maurycyliebner.github.io --> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" xmlns:xlink="http://www.w3.org/1999/xlink" id="svgobject" role="group"> <title>Animation</title> <defs> <style type="text/css" id="user-style"> .foreground {fill: #c5c5c5;} .background {fill: #1c1c1c;} .color1 {fill: #ff005b;} #play-button { transform: translate(30px, calc(100% - 45px)); } #pause-button { transform: translate(30px, calc(100% - 45px)); } #wide_track { transform: translate(150px, calc(100% - 45px)); } #track { transform: translate(150px, calc(100% - 33px)); } #slider_button { transform: translate(0px, calc(100% - 30px)); } #timer { transform: translate(60px, calc(100% - 38px)); font-family: 'DejaVu Sans Mono', monospace; font-style: normal; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; } </style> <rect id="slider_wide_track" height="30px" width="65%" /> <rect id="slider_track" height="6px" width="65%" /> <circle id="slider_button" r="10px" class="color1" /> <g id="icon-play"> <rect x="-5" y="0" width="25" height="30" class="background" /> <path d="M0 4l14 11-14 11z" /> </g> <g id="icon-pause"> <rect x="-5" y="0" width="25" height="30" class="background" /> <rect x="0" y="5" width="5" height="20" /> <rect x="10" y="5" width="5" height="20" /> </g> </defs> <g transform="translate(0 0)"> <g transform="translate(0 0)"> <g> <animateTransform dur="5s" attributeName="transform" calcMode="spline" fill="freeze" keyTimes="0;0;1" keySplines="0 0 1 1;0 0 1 1" type="translate" values="66.1074 0;66.1074 0;158.054 0"/> <g> <animateTransform dur="5s" attributeName="transform" calcMode="spline" fill="freeze" keyTimes="0;0;1" keySplines="0 0 1 1;0 0 1 1" type="translate" values="0 190.604;0 190.604;0 408.725"/> <g transform="rotate(0)"> <g transform="scale(1 1)"> <g transform="scale(1 1)"> <g transform="skewX(0) skewY(0)"> <g transform="translate(0 0)" opacity="1"> <g transform="translate(0 0)"> <g font-size="20" text-anchor="start" stroke="none" stroke-width="1" fill="#000000" font-family="Arial"> <text word-spacing="0em" letter-spacing="0em">This is some cool text.</text> </g> </g> </g> </g> </g> </g> </g> </g> </g> </g> </g> <defs/> <text id="timer" class="foreground" aria-hidden="true">0:00/0:00</text> <!-- Invisible, wider track to make frame seeking easier --> <use xlink:href="#slider_wide_track" id="wide_track" class="background" aria-hidden="true" /> <!-- Visible track --> <use xlink:href="#slider_track" id="track" class="foreground" aria-hidden="true" /> <use xlink:href="#slider_button" id="slider_1" x="231px" aria-hidden="true" /> <use xlink:href="#icon-play" id="play-button" class="foreground" aria-hidden="true" /> <use xlink:href="#icon-pause" id="pause-button" class="foreground" aria-hidden="true" /> </svg></code></pre> <p>The HTML & JS:</p> <pre><code><!DOCTYPE html> <html class="no-js" lang="en"> <head> <meta charset="utf-8"> <title>Enve animation</title> <meta content="" name="description"> <meta content="width=device-width, initial-scale=1" name="viewport"> </head> <body> <object data="text-test.svg" id="svgholder" tabindex="0"></object> <script type="text/javascript"> var autoplay = false; var svgholder = document.getElementById('svgholder'); svgholder.onload = function() { var svgelement = svgholder.contentDocument; var animation = svgelement.getElementById('svgobject'); var animatedElement = svgelement.querySelector('[dur]'); var dur = animatedElement.getAttribute('dur'); var animation_duration = parseInt(dur.substring(0, (dur.length - 1))); var play_button = animation.getElementById('play-button'); var pause_button = animation.getElementById('pause-button'); var is_playing = autoplay; var sliderInterval; var timerInterval; var slider_1 = animation.getElementById('slider_1'); var timer = animation.getElementById('timer'); var limitLower = parseInt(getTranslateX(animation.getElementById('track'))); var limitUpper = limitLower + parseInt(animation.getElementById('track').getBoundingClientRect().width) - 8; pause(); update_timer(); update_slider_button(); function intervalSet() { timerInterval = setInterval(update_timer, "100ms"); sliderInterval = setInterval(update_slider_button, "100ms"); } function intervalClear() { clearInterval(sliderInterval); clearInterval(timerInterval); } function buttonToPause() { play_button.setAttribute('display', 'none'); pause_button.setAttribute('display', 'inline'); } function buttonToPlay() { play_button.setAttribute('display', 'inline'); pause_button.setAttribute('display', 'none'); } function pause() { animation.pauseAnimations(); buttonToPlay(); } function play() { animation.unpauseAnimations(); buttonToPause(); } function togglePlayPause() { if (is_playing) { pause(); is_playing = false; intervalClear(); } else if (animation.getCurrentTime() >= animation_duration) { play(); animation.setCurrentTime(0); is_playing = true; intervalSet(); } else { play(); is_playing = true; intervalSet(); } } function timer_from_seconds(t) { var minutes = Math.floor(t / 60); var seconds = Math.floor(t % 60); return minutes + ':' + ("0" + seconds).slice(-2); } function update_timer() { var current_time = (animation.getCurrentTime()) % animation_duration; timer.textContent = timer_from_seconds(current_time) + "/" + timer_from_seconds(animation_duration); } function getTranslateX(elem) { var style = window.getComputedStyle(elem); var matrix = new WebKitCSSMatrix(style.webkitTransform); return parseInt(matrix.m41); } // Return X position for an event function mx(evt) { var pt = animation.createSVGPoint(); pt.x = evt.clientX; return pt.matrixTransform(animation.getScreenCTM().inverse()); } // Set the current time of the animation to get the slider button under the cursor var move = function(evt) { var cursor_position = mx(evt); if (cursor_position.x < limitLower || cursor_position.x > limitUpper) { return; } var current_time = animation_duration * (cursor_position.x - limitLower) / (limitUpper - limitLower); animation.setCurrentTime(current_time); }; function update_slider_button() { var current_time = (animation.getCurrentTime()) % animation_duration; var current_position = limitLower + (limitUpper - limitLower) * current_time / animation_duration; slider_1.setAttribute('x', parseInt(current_position) + 'px'); } play_button.addEventListener('click', togglePlayPause, false); pause_button.addEventListener('click', togglePlayPause, false); animatedElement.addEventListener('endEvent', function() { buttonToPlay(); is_playing = false; intervalClear(); timer.textContent = timer_from_seconds(animation_duration) + "/" + timer_from_seconds(animation_duration); }, false); // Move the slider button to the cursor position when a click happens on the track of the slider animation.getElementById('wide_track').addEventListener('click', move, false); animation.getElementById('track').addEventListener('click', move, false); // Enable slider button dragging along the track slider_1.addEventListener('mousedown', function() { if (is_playing) { pause(); } else { intervalSet(); } animation.addEventListener('mousemove', move, false); // mouseup is buggy for some reason window.addEventListener('mouseup', function() { if (is_playing) { play(); } else { intervalClear(); } animation.removeEventListener('mousemove', move, false); }, false); }, false); if (autoplay) { play(); intervalSet(); } }; </script> </body> </html></code></pre> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/MaurycyLiebner"><img src="https://avatars.githubusercontent.com/u/16670651?v=4" />MaurycyLiebner</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <p>Sorry for responding with such a long delay. Great work! I am not sure how would I go about using it, because there are 2 files, so plain SVG export goes out of the window. I am not 100% sure, but I think SVG files can have <script> embedded in them, so maybe it is possible to squeeze all this into a single SVG file?</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/mestaritonttu"><img src="https://avatars.githubusercontent.com/u/829167?v=4" />mestaritonttu</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <blockquote> <p>Sorry for responding with such a long delay. Great work! I am not sure how would I go about using it, because there are 2 files, so plain SVG export goes out of the window. I am not 100% sure, but I think SVG files can have <script> embedded in them, so maybe it is possible to squeeze all this into a single SVG file?</p> </blockquote> <p>Yes, termtosvg actually does embed the <script> element originally. I guess it would be fine for enve. I was not thinking and just going by my own workflow guided by the fact that MediaWiki does not allow uploads of SVG files with scripts embedded in them (I made a termtosvg wiki widget).</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/Ratteler"><img src="https://avatars.githubusercontent.com/u/15938825?v=4" />Ratteler</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <p>I'm just starting to play with Enve. It would be great if there was a simple Export to animated SVG option.</p> </div> </div> <div class="page-bar-simple"> </div> <div class="footer"> <ul class="body"> <li>© <script> document.write(new Date().getFullYear()) </script> Githubissues.</li> <li>Githubissues is a development platform for aggregating issues.</li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <script src="/githubissues/assets/js.js"></script> <script src="/githubissues/assets/markdown.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/highlight.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/languages/go.min.js"></script> <script> hljs.highlightAll(); </script> </body> </html>