Smooth loading of SVGs using SNAP.SVG - javascript

I made a point-and-click-adventure-like website using Snap.SVG. It sort of works! but i'd like to improve it.
www.esad.se
(click on the arrow on the right to go to the next image!)
the biggest problem we encountered (along with the teacher helping me at that time) was to iterate through my collection of SVGs - clicking an SVG causes a new image and a new svg to be loaded into the main page. the solution we used was to point to an array containing the SVG paths and to kill the old SVG by manipulating the DOM with
event.target.parentNode.parentNode.remove()
which we though was probably not the best solution, especially because it doesn't allow for a smooth transition between svgs.
would there be a way of using a loading method to implement smooth transitions between my SVGs (for instance, a cross-fade)?
thanks for your insights.
var s = Snap("#svg");
var first = "A1.JPG"
var data = {
"A1.JPG" : {viens : "A2.JPG", svg : "1.svg"},
"A2.JPG" : {viens : "A3.JPG", svg : "2.svg", scroll : 600}
// [... etc]
}
var currentPath = data[first]
document.images.main.src = first
var mySvg = Snap.load(currentPath.svg, function(loadedFragment){
s.append(loadedFragment)
s.click(getEventElement)
window.scroll(0,0)
});
function getEventElement( event ) {
if( event.target.localName == 'svg' ) { return }
target = event.target.parentNode.id
// if (target == "noclick") {return}
if(currentPath[target] == undefined) {
return
}
document.images.main.src = currentPath[target]
currentPath = data[currentPath[target]]
//this.clear()
event.target.parentNode.parentNode.remove()
if(currentPath.hasOwnProperty("scroll")){
window.scroll(currentPath.scroll,0)
}else{
window.scroll(0,0)
}
mySvg = Snap.load(currentPath.svg, function(loadedFragment){
s.append(loadedFragment)
//s.click(getEventElement)
});
}

I'd just do it with a CSS class. Change the line where you remove the element from the DOM to:
event.target.classList.add('fade-out');
setTimeout (function () {
event.target.parentNode.removeChild(event.target);
}, 2000);
Then in your CSS for the element add:
opacity: 1;
transition: opacity 1.5s;
And add a new style for the fade-out:
.yourSvgClass.fade-out {
opacity: 0;
}

Related

SVG element broke when updating to SVG 2, why?

I had an SVG element that was used to render lines connecting nodes in a flowchart. At the beginning of October 2018, it suddenly stopped working in Chrome - the SVG element has 0 width and height in the DOM even though it has width and height attributes defined.
After doing some searching I found that Chrome recently updated its standards to SVG 2, however this SVG is fairly simple and I can't figure out exactly what changes caused this to happen.
Details:
The SVG is inside a regular DIV with position:relative. The DIV appears properly and has height and width set.
The SVG has a class and used to have position:absolute. It no longer seems to have any style and I can't edit its style through DevTools. I'm not certain if it needed to have a style to begin with.
The SVG has a bunch of line elements in it, and nothing else. The lines have classes and their styles don't work either.
The parent DIV does have other DIV elements in it (the nodes in the flowchart). These elements all have position:absolute.
Neither the parent DIV nor the SVG exist when the page is opened. They are created using Javascript.
There are no other SVG elements on the page and no use of the "use" keyword anywhere.
What part of this breaks with SVG 2 compliance?
Here is the code:
function appendElement(type,className,to,inner){
if (type === 'svg' || type === 'line'){
var el = document.createElementNS("https://www.w3.org/2000/svg", type);
if (className !== undefined) el.setAttribute("class",className);
} else {
var el = document.createElement(type);
if (className !== undefined) el.className = className;
}
to.appendChild(el);
if (inner !== undefined) el.innerHTML = inner;
return el;
}
The function in the flowchart class
setInner(){
this.flowchart.innerHTML = '';
this.svg = appendElement('svg','bw-flowchart-svg',this.flowchart);
this.svg.setAttribute("width", 800);
this.svg.setAttribute("height", 500);
this.currentSize = [800,500];
this.listitems = [];
this.links = [];
for (var i in this.obj.nodes){
this.listitems.push(new BWBFlowchartNode(this,this.obj.nodes[i]));
}
for (var i in this.listitems){
this.listitems[i].createLinks();
}
this.checkSize();
}
The createLinks function adds all of the lines and sets their X and Y values. The lines are being added to the DOM properly.
And the style that should be applied (but neither the svg nor the lines have any styling at all)
.bw-flowchart-svg{
position:absolute;
}
.bw-flowchart-line{
stroke: rgb(0,0,0);
stroke-width: 1;
}
The following line is incorrect
document.createElementNS("https://www.w3.org/2000/svg", type);
The SVG namespace is actually
document.createElementNS("http://www.w3.org/2000/svg", type);
This is true in SVG 1.1 and is unchanged in SVG 2. A namespace is not actually a URL despite looking like one.

How can I animate a recently created DOM element in the same function?

I'm working to create an image gallery where the images will be composed by progressively fading in layers one on top of the other to form the final image.
I have many such layers so instead of loading them into many different <img> elements all at once (which would slow load time) I want to start off with a single <img id="base"> and then progressively add image elements with the jQuery .after() method, assign them the relevant sources and fade them in with a delay.
The problem is that I can't attach animations to the newly created elements because (I'm assuming) they don't exist yet within the same function. Here is my code:
HTML
<div id="gallery">
<img id="base" src="image-1.jpg">
</div>
CSS
#base {
opacity: 0;
}
.layers {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
JavaScript
$(document).ready(function () {
$("#base").animate({opacity: 1}, 300); //fade in base
for (var i = 1; i <= numberOfLayers; i++, gap += 300) {
// create a new element
$("#base").after("<img class='layers' src='" + imgName + ".png'>");
// fade that new element in
$("#gallery").children().eq(i).delay(gap).animate({opacity: '1'}, 300);
}
}
Please note that I've altered my actual code to illustrate this better. I'm fairly new at JavaScript but I'm a quick learner so I'd appreciate if you could tell me what I'm doing wrong and what solution I should pursue.
EDIT: I've included my code inside your JSFiddle (all you need to do is add the library-X.jpg images) : http://jsfiddle.net/pgoevx03/
I've tried to replicate the intent of the code in a cleaner/more flexible way. Please let me know if I can do anything else to help.
I'm not saying this is the best way to do it, but it should be easy enough to understand and use.
The code is untested, but should work just fine. The comments should help you out if there's any compilation error.
Note that I removed the first image in the gallery (with ID "base") from the HTML file. It will be appended the same way as the rest.
// Array storing all the images to append to the gallery
var galleryImages = [
"image-1.jpg",
"image-2.jpg",
"image-3.jpg",
"image-4.jpg",
"image-5.jpg",
"image-6.jpg",
"image-7.jpg",
"image-8.jpg",
"image-9.jpg",
"image-10.jpg"
];
// Index of the image about to be appended
var imgIndex = -1;
var baseID = "base";
$(document).ready(function() {
// Start appending images
appendAllImages();
});
// Append the images, one at a time, at the end of the gallery
function appendAllImages() {
//Move to the next image
imgIndex++;
//We've reached the last image: stop appending
if (imgIndex >= galleryImages.length) return;
//Create image object
var img = $("<img>", {
src: galleryImages[imgIndex],
});
if (imgIndex === 0) { // It's the base!
//Give the base ID to the first image
img.attr("id", baseID);
//Append the image object
$("#gallery").append(img);
} else { // It's a layer!
//Give the base ID to the first image
img.attr("class", "layers");
//Append the image object
$("#" + baseID).after(img);
}
//Fade in the image appended; append the next image once it's done fading in
img.animate({
opacity: 1,
}, 300, appendAllImages);
}

Photoswipe 4.0: Initiate 'swipe to next' programatically

SITUATION
I have been trying to trigger the 'slide to next picture' animation when the NEXT button is clicked, but i have not found a solution for this.
There is an ongoing discussion about this on GitHub, but it is only about adding the option for a slide animation, not about how to actually do it with PS as it is right now.
There was an option for it in 3.0 but as 4.0 is a complete rewrite it does not work anymore.
QUESTION
Instead of just 'jumping' to the next/prev picture when an arrow is clicked, i need the 'slide transition' that is also used when swiping/dragging the image.
There is no option to trigger that, so how can i manually trigger this effect with JS?
PhotoSwipe Slide Transitions
So, I added slide transitions to Photoswipe, and it's working nicely without disturbing native behavior.
http://codepen.io/mjau-mjau/full/XbqBbp/
http://codepen.io/mjau-mjau/pen/XbqBbp
The only limitation is that transition will not be applied between seams in loop mode (for example when looping from last slide to slide 1). In examples I have used jQuery.
Essentially, it works by simply adding a CSS transition class to the .pswp__container on demand, but we need to add some javascript events to prevent the transition from interfering with Swipe, and only if mouseUsed. We also add a patch so the transition does not get added between loop seams.
1. Add the below to your CSS
It will be applied on-demand from javascript when required.
.pswp__container_transition {
-webkit-transition: -webkit-transform 333ms cubic-bezier(0.4, 0, 0.22, 1);
transition: transform 333ms cubic-bezier(0.4, 0, 0.22, 1);
}
2. Add javascript events to assist in assigning the transition class
This can go anywhere, but must be triggered after jQuery is loaded.
var mouseUsed = false;
$('body').on('mousedown', '.pswp__scroll-wrap', function(event) {
// On mousedown, temporarily remove the transition class in preparation for swipe. $(this).children('.pswp__container_transition').removeClass('pswp__container_transition');
}).on('mousedown', '.pswp__button--arrow--left, .pswp__button--arrow--right', function(event) {
// Exlude navigation arrows from the above event.
event.stopPropagation();
}).on('mousemove.detect', function(event) {
// Detect mouseUsed before as early as possible to feed PhotoSwipe
mouseUsed = true;
$('body').off('mousemove.detect');
});
3. Add beforeChange listener to re-assign transition class on photoswipe init
The below needs to be added in your PhotoSwipe init logic.
// Create your photoswipe gallery element as usual
gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options);
// Transition Manager function (triggers only on mouseUsed)
function transitionManager() {
// Create var to store slide index
var currentSlide = options.index;
// Listen for photoswipe change event to re-apply transition class
gallery.listen('beforeChange', function() {
// Only apply transition class if difference between last and next slide is < 2
// If difference > 1, it means we are at the loop seam.
var transition = Math.abs(gallery.getCurrentIndex()-currentSlide) < 2;
// Apply transition class depending on above
$('.pswp__container').toggleClass('pswp__container_transition', transition);
// Update currentSlide
currentSlide = gallery.getCurrentIndex();
});
}
// Only apply transition manager functionality if mouse
if(mouseUsed) {
transitionManager();
} else {
gallery.listen('mouseUsed', function(){
mouseUsed = true;
transitionManager();
});
}
// init your gallery per usual
gallery.init();
You can just use a css transition:
.pswp__container{
transition:.3s ease-in-out all;
}
This might not be ideal for performance on mobile, but I just add this transition in a media query and allow users to use the swipe functionality on smaller screens.
I finally bit the bullet and spent some time making this work as nobody seemed to have a solution for that, not here neither on GitHub or anywhere else.
SOLUTION
I used the fact that a click on the arrow jumps to the next item and triggers the loading of the next image and sets the whole slide state to represent the correct situation in an instant.
So i just added custom buttons which would initiate a slide transition and then triggered a click on the original buttons (which i hid via CSS) which would update the slide state to represent the situation i created visually.
Added NEW next and prev arrows
Hid the ORIGINAL next and prev arrows via css
Animated the slide myself when the NEW next or prev arrows were clicked
Then triggered the click on the ORIGINAL next or prev arrows programmatically
So here is the code:
HTML
// THE NEW BUTTONS
<button class="NEW-button-left" title="Previous (arrow left)">PREV</button>
<button class="NEW-button-right" title="Next (arrow right)">NEXT</button>
// added right before this original lines of the example code
<button class="pswp__button pswp__button--arrow--left ...
CSS
pswp__button--arrow--left,
pswp__button--arrow--right {
display: none;
}
NEW-button-left,
NEW-button-right {
/* whatever you fancy */
}
JAVASCRIPT (helper functions)
var tx = 0; // current translation
var tdir = 0;
var slidepseactive = false;
// helper function to get current translate3d positions
// as per https://stackoverflow.com/a/7982594/826194
function getTransform(el) {
var results = $(el).css('-webkit-transform').match(/matrix(?:(3d)\(-{0,1}\d+(?:, -{0,1}\d+)*(?:, (-{0,1}\d+))(?:, (-{0,1}\d+))(?:, (-{0,1}\d+)), -{0,1}\d+\)|\(-{0,1}\d+(?:, -{0,1}\d+)*(?:, (-{0,1}\d+))(?:, (-{0,1}\d+))\))/)
if(!results) return [0, 0, 0];
if(results[1] == '3d') return results.slice(2,5);
results.push(0);
return results.slice(5, 8);
}
// set the translate x position of an element
function translate3dX($e, x) {
$e.css({
// TODO: depending on the browser we need one of those, for now just chrome
//'-webkit-transform': 'translate3d(' +String(x) + 'px, 0px, 0px)'
//, '-moz-transform': 'translate3d(' +String(x) + 'px, 0px, 0px)'
'transform': 'translate3d(' +String(x) + 'px, 0px, 0px)'
});
};
JAVASCRIPT (main)
// will slide to the left or to the right
function slidePS(direction) {
if (slidepseactive) // prevent interruptions
return;
tdir = -1;
if (direction == "left") {
tdir = 1;
}
// get the current slides transition position
var t = getTransform(".pswp__container");
tx = parseInt(t[0]);
// reset anim counter (you can use any property as anim counter)
$(".pswp__container").css("text-indent", "0px");
slidepseactive = true;
$(".pswp__container").animate(
{textIndent: 100},{
step: function (now, fx) {
// here 8.7 is the no. of pixels we move per animation step %
// so in this case we slide a total of 870px, depends on your setup
// you might want to use a percentage value, in this case it was
// a popup thats why it is a a fixed value per step
translate3dX($(this), tx + Math.round(8.7 * now * tdir));
},
duration: '300ms',
done: function () {
// now that we finished sliding trigger the original buttons so
// that the photoswipe state reflects the new situation
slidepseactive = false;
if (tdir == -1)
$(".pswp__button--arrow--right").trigger("click");
else
$(".pswp__button--arrow--left").trigger("click");
}
},
'linear');
}
// now activate our buttons
$(function(){
$(".NEW-button-left").click(function(){
slidePS("left");
});
$(".NEW-button-right").click(function(){
slidePS("right");
});
});
I used info from those SE answers:
jQuery animate a -webkit-transform
Get translate3d values of a div?
The PhotoSwipe can do this by itself when you use swipe gesture. So why not to use the internal code instead of something that doesn't work well?
With my solution everything works well, the arrow clicks, the cursor keys and even the loop back at the end and it doesn't break anything.
Simply edit the photoswipe.js file and replace the goTo function with this code:
goTo: function(index) {
var itemsDiff;
if (index == _currentItemIndex + 1) { //Next
itemsDiff = 1;
}
else { //Prev
itemsDiff = -1;
}
var itemChanged;
if(!_mainScrollAnimating) {
_currZoomedItemIndex = _currentItemIndex;
}
var nextCircle;
_currentItemIndex += itemsDiff;
if(_currentItemIndex < 0) {
_currentItemIndex = _options.loop ? _getNumItems()-1 : 0;
nextCircle = true;
} else if(_currentItemIndex >= _getNumItems()) {
_currentItemIndex = _options.loop ? 0 : _getNumItems()-1;
nextCircle = true;
}
if(!nextCircle || _options.loop) {
_indexDiff += itemsDiff;
_currPositionIndex -= itemsDiff;
itemChanged = true;
}
var animateToX = _slideSize.x * _currPositionIndex;
var animateToDist = Math.abs( animateToX - _mainScrollPos.x );
var finishAnimDuration = 333;
if(_currZoomedItemIndex === _currentItemIndex) {
itemChanged = false;
}
_mainScrollAnimating = true;
_shout('mainScrollAnimStart');
_animateProp('mainScroll', _mainScrollPos.x, animateToX, finishAnimDuration, framework.easing.cubic.out,
_moveMainScroll,
function() {
_stopAllAnimations();
_mainScrollAnimating = false;
_currZoomedItemIndex = -1;
if(itemChanged || _currZoomedItemIndex !== _currentItemIndex) {
self.updateCurrItem();
}
_shout('mainScrollAnimComplete');
}
);
if(itemChanged) {
self.updateCurrItem(true);
}
return itemChanged;
},

JQuery image slider and CSS transition works only once

I'm trying to learn jQuery plugins. I'm creating a image sliding plugin. This is how I've developed so far.
(function($){
$.imageSlider = function(selector, settings) {
//settings
var config = {
'delay':2000,
'speed':500
};
if(settings) {
$.extend(config, settings);
}
//vars
var obj = $(selector);
obj.children('img').css('transition','width 2s, height 2s, transform 2s');
var image = obj.children('img');
var count = image.length;
var i = 0;
image.eq(0).show(); //first image showing
//begin the image loop
setInterval ( function() {
image.eq(i).fadeOut(config.speed);
i = (i+1 == count) ? 0 : i+1;
image.eq(i).fadeIn(config.speed);
image.eq(i).css("transform","rotate(360deg)");
}, config.delay
);
return this;
}
})(jQuery);
But my the issue is the rotation happens only once cycle.
JSFiddle http://jsfiddle.net/va45D/1/
After all 3 images loaded as the way I wanted, then It doesn't applies the transition.
Please help me to understand whats happening here.
Hie Shan,
I can reproduce your problem, you're right. After the second time that you rotate your images they will not rotate anymore. I'm using Firefox 25.
To solve you problem I made these updates:
setInterval( function() {
image.eq(i).fadeOut(config.speed);
image.eq(i).css("transform","rotate(0deg)");
i = (i+1 == count) ? 0 : i+1;
image.eq(i).fadeIn(config.speed);
image.eq(i).css("transform","rotate(360deg)");
}, config.delay
);
When your loop over, the element keeps the same value at the end of the loop, so when your run it for the first time your have all your img elements at 0deg, at the end you transform them to 360deg position. The next time that you run your loop (this explain the problem that you have on your 2nd time), all your images starts on 360deg. If you rotate 360deg to 360deg you have the same position because there is no interval between the actual position and the new one. This can be really visible if you update your code to 45deg, as you can see on this fiddle.
Before start the process I defined a transformation that returns your element to 0 degrees. Maybe this solution solves your problem.
Here is the JSFiddle
Thanks!

Implementing a Parameter to a plugin - Shows 'X' number of elements

The current plugin, shown below, scrolls the top-most div in a series of divs with the same class upwards, then removes it from the container, and appends it to the bottom of the series (within the container). This gives the illusion of a vertical slideshow.
$.fn.rotateEach = function ( opts ) {
var $this = this,
defaults = {
delay: 5000
},
settings = $.extend(defaults, opts),
rotator = function ($elems) {
$elems.eq(0).slideUp(500, function(){
var $eq0 = $elems.eq(0).detach();
$elems.parent().append($eq0);
$eq0.fadeIn();
setTimeout(function(){ rotator( $($elems.selector) ); },
settings.delay);
});
};
setTimeout(function(){ rotator( $this ); }, settings.delay);
};
$('.dynPanelContent').rotateEach();
However, if there are a large number of elements to scroll through, this would make for a VERY long page. As such, I am attempting to re-write this script so that it accepts a parameter which will determine how many elements to display. Any elements exceeding this number will be hidden until they are in the top 'x' number of elements. Here is an example of what I have attempted to implement.
$.fn.rotateEach = function (opts) {
var $this = this,
defaults = {
delay: 5000,
//Add a parameter named elementsShown, pass in a default value of 3
elementsShown: 3
},
settings = $.extend(defaults, opts),
rotator = function ($elems) {
//Hide the elements that are past the number to be shown
for (i = settings.elementsShown; i <= $elems.eq; i++) {
$elems.eq(i).hide();
}
$elems.eq(0).slideUp(500, function () {
var $eq0 = $elems.eq(0).detach();
var $eqN = $elems.eq(settings.elementsShown) - 1;
//Check & Show the element that is now within the show range
if ($elems.eq() == $eqN) {
$elems.eq($eqN).show('slow');
}
$elems.parent().append($eq0);
$eq0.fadeIn();
setTimeout(function () { rotator($($elems.selector)); },
settings.delay);
});
};
You can use simple CSS for this, mate.
If your elements are all of the same height (which your problem has to assume: if you are rotating a whole bunch of things dynamically, you won't want your page to change height), then you don't really need to use JavaScript for this at all. Just set the height of the container to what you want and hide the overflow. Then when you remove and append, everything appears to work. This won't take care of your dynamic configuration, though.
Improved plug-in: http://jsfiddle.net/morrison/tTJaM/
Notes:
Added support for showing X elements.
Added support for rotating only certain elements.
Added support for stopping the rotations:
Stop after X milliseconds.
Stop after X rotations.
overflow-y:hidden is added to container dynamically.
Simplified your detaching/attaching.
Known Issues:
Displaying X elements doesn't check for a maximum.

Categories

Resources