Jump to content
Search Community

Kineticjs plugin

atomictag test
Moderator Tag

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts

Hi there,

 

I am using GSAP with Kineticjs and I am very happy with the combo.

One of the things I like most about GSAP - alongside its features and super performance - is the way it helps me organizing my code and overall workflow. I think Kinetic and GSAP are a very good match for all sort of canvas-based applications.

 

I have a couple of questions/doubts about the Kineticjs GSAP plugin - which I use a lot (the autoRotate support provided by the kinetic+bezier plugins saved me literally hours of pain). The questions are quite Kinetic-specific but this forum feels like the right place to discuss this topic.

 

The GSAP Kinetic plugin handles drawings automatically by default (unless "autoDraw" is set to false - that took me quite a while to find!). On the other hand Kinetic has recently introduced batchDraw as a way to minimize layer redrawing by "batching up" updates in a special animation. I am not 100% sure what the best drawing strategy is when the two libs are combined:

 

- Should the GSAP plugin be changed to use layer.batchDraw instead of layer.draw by default with autoDraw=true? Or should that be configurable (and when would one pick one vs the other)?

- One should set autoDraw=false and handle updates manually by calling layer.batchDraw onUpdate?

 

I currently use the latter almost always as it feels like I am having more control on what gets drawn (for example when animating many groups/shapes belonging to the same layer simultaneously in different tweens/timelines). In my understanding this means each GSAP "tick" queues up drawing requests into a Kinetic Animation which then performs the actual drawing when its own RAF fires - and that does not seem very efficient (as there are 2 RAF at the same time). Please correct me if I got it all wrong.

 

Ideally I would like the GSAP plugin to do "the right thing" with batching (it seems it sort of does already?) - but would that work reliably when shapes belonging to the same layer are animated by different tweens/timelines simultaneously (that's my most frequent use-case)?

 

Many thanks to the group for any advice on this.

Keep up with the great work.

Link to comment
Share on other sites

Excellent questions. Obviously you're very knowledgeable about both KineticJS and GSAP. That's fantastic.

 

Okay, so I must admit I'm not super familiar with the guts of KineticJS, but I totally understand how performance sensitive redrawing the canvas can be and how important it is to avoid duplicate calls to the draw() method on a layer. That's why GSAP's KineticPlugin manages that for you and ensures that it only calls draw() once on a layer each tick of the engine. Here's basically how it works...

 

Each time a tween updates a KineticJS object's property, it marks its layer internally as needing a redraw and then when ALL tweens are finished (when the core ticker dispatches a "tick" event), it rips through that list that it recorded internally and calls draw() on the appropriate layers. So it doesn't matter if you have 100 timelines with 1000 tweens of various objects - the engine will only call draw() once on each layer (and only the ones that need it). 

 

I'm pretty sure this is basically what Eric's new batchDraw functionality does. I don't think there's a need to tap into that because GSAP's KineticPlugin is already doing that work nicely. I also know that Eric recently introduced a new "Tween" class that's native to KineticJS and it is tied into the batchDraw functionality under the hood. He still recommends GSAP to his users, but he needed to have a basic tweening engine in KineticJS to silence potential licensing concerns (plus I'm sure he wanted to serve his audience well by at least giving them some built-in tweening controls). 

 

I think it's best in these cases to run some benchmarks and see how things compare because if the Kinetic.Tween class performs significantly better than GSAP+KineticPlugin, it would indicate we need to do some work on our plugin. So I whipped together a relatively simple set of benchmark files that just animate 800 objects across the screen randomly over the course of 12 seconds. I have attached those files. 

 

See for yourself - GSAP with KineticPlugin is significantly smoother (at least on my computer). Feel free to crank up the "creatureCount" variable to put more stress on things. The difference was very noticeable for me. This tells me we're doing something right in terms of optimization and managing the draw() calls. 

 

One thing to note is that since the Kinetic.Tween class doesn't have any "delay" feature, I had to resort to using setTimeout() to schedule the tweens. If you know of a better way, please tell me. 

 

Also worth noting: the latest version of KineticPlugin for GSAP also integrates with DirectionalRotationPlugin, so you can do things like this:

TweenLite.to(obj, 2, {kinetic:{rotationDeg:"270_ccw"}});

In other words, you can control the direction of the rotation by adding "_ccw" for counter-clockwise, "_cw" for clockwise, and "_short" for whichever is shortest. This new version of the plugin hasn't been released yet, but it's in the zip that's attached to this thread if you want to give it a shot. 

 

So the benefits of using the KineticPlugin are:

  1. It automatically manages the draw() calls for the appropriate layers, optimizing performance
  2. You can use regular property names like "x", "y", "rotation", etc. instead of "setX", "setY", "setRotation", etc.
  3. It automatically figures out how to tween color-related values which you can define in pretty much any format, like #F00, #FF0000, rgb(255,0,0), "red", or "hsl(20, 50%, 70%)", or even rgba() and hsla(). 
  4. It integrates with BezierPlugin, including the autoRotate feature
  5. It integrates with DirectionalRotationPlugin
  6. It integrates with RoundPropsPlugin

Does that clear things up for you?

Kinetic_Benchmark.zip

Link to comment
Share on other sites

Thank you so much for your answer, Jack!

I was under the impression autoDraw was set to true by default for a reason :)

 

I ran the benchmarks on iOS 6 and the GSAP test runs well (understandably slowish given the nature of benchmark - but certainly acceptable). On the other hand the Kinetic.Tween benchmark (which is certainly slower than the GSAP one on my desktop) does not work at all on the phone with the default 800 shapes - as nothing gets animated.

 

I managed to get the Tween benchmark to run decently with ~100 shapes, but even then the animation is not smooth and stutters quite often (as if the GC was kicking in many times). Above 100 shapes Kinetic.Tween's performance deteriorates visibly - and it stops working (= animation does not even start) at 200 shapes on my iPhone 5.

 

No doubt GS is a winner vs. Kinetic.Tween (which, in fairness, Eric did in record time and it's therefore very early stage - and most likely never really tested on mobile). As a user of both GS and Kinetic I am super happy these 2 great libs can work in tandem as I don't think I would ever drop GSAP (albeit good basic Tween support is IMHO a must for any canvas framework for the simpler use-cases).

 

Back to the BatchDraw topic, I tweaked the GSAP benchmark to use Kinetic's batchDraw instead of autoDraw:

TweenLite.to(creature, 4, {
    kinetic:{x:900, y:Math.random()* 320, rotation:Math.PI*2, autoDraw:false },
    ease:Strong.easeOut,
    delay:Math.random() * 8,
    onUpdate: layer.batchDraw,
    onUpdateScope: layer});
The jury is still out on this. With a low number of shapes (~100) no difference is really noticeable (at least to me). Using batchDraw seems to stutter a little bit from time to time (same stop-the-world GC effect described above). With the default 800 shapes my perception is that batchDraw produces a smoother animation that autoDraw, although it's probably still subjective.

 

Raising the number of shapes up to 3000 (again, on iOS) reduces the animation to a crawl but it shows an interesting empiric fact: with autoDraw the canvas is redrawn 8 times within the allotted time, while with batchDraw the canvas is redrawn 13 times. With 5000 shapes, the autoDraw benchmark draws 5 times and the batchDraw one 8 times. In these cases explicit batchDraw performed inside the onUpdate callback is a winner vs. autoDraw - by ~60%.

 

It seems that getting GS to post into the Kinetic layer animation queue has some advantages - although I am not entirely sure why (as they technically seem to do the same thing - just Kinetic has its own animation loop kicked by a RAF callback). You are probably the best one to understand/comment on this.

 

Again, thanks a lot for your help on this. I much appreciate it!

  • Like 1
Link to comment
Share on other sites

I'm traveling right now, so I don't have much time but I had a few questions for you:

  1. If you set autoDraw:true in your version that's also doing the onUpdate to tie into the batchDraw, does it make any difference? In other words, use BOTH the plugins autoDraw AND KineticJS's batchDraw(). I realize this seems wasteful, but I'm curious about something.
  2. Do you get the same results if you refresh the page on your iPhone? There's an iOS bug that can cause the initial load of a page to somehow trigger requestAnimationFrame not to work consistently, so it falls back to using setTimeout(). I'm wondering if this is just a synchronization issue where Kinetic's using a requestAnimationFrame and GSAP is using setTimeout() or visa-versa or they're both using unsynchronized setTimeouts, etc. 
  3. Is there a way to disable Kinetic's batchDraw()? I wonder if Kinetic is forcing some auto-updates internally even though GSAP's KineticPlugin is trying to handle the draw() calls itself, and Kinetic is stepping on the toes of GSAP's stuff. Probably not - just wondering.
Link to comment
Share on other sites

Okay, I did a little more testing. I cranked up the creatureCount to 10,000 and tested in Chrome on the desktop with both the regular autoDraw and the Kinetic batchDraw (in an onUpdate) and they performed identically in terms of frame rate and the number of steps in the animation. Keep in mind that what matters here is NOT how many times draw() or batchDraw() gets called - it's how many times it renders NEW values during the animation. It may draw() the same values twice, and that certainly wouldn't help. 

 

I had a hunch that the difference you noticed was actually due to GSAP and KineticJS being out-of-sync in terms of updates/paints/renders, particularly because you said you noticed it on iOS and I know that the bug in iOS can force GSAP and Kinetic to fall back to using setTimeout() instead of requestAnimationFrame (only in certain scenarios). So I tried setting TweenLite.ticker.useRAF(false) so that it's forced to use setTimeout(), even in Chrome, my test bed. This would simulate what's likely happening sometimes in iOS. 

 

The result was a noticeable degradation in performance framerate-wise (roughly 25-40%), although I think that Kinetic's batchDraw() was actually getting called MORE often. Sounds weird, right? If drawing is happening more often, that should look better, right? Not really...

 

Let's say GSAP is under huge load (10,000 tweens) and it takes a while to rip through all those calculations, and halfway through the calculations, Kinetic's update cycle kicks in and does its batch drawing - it'll draw() that layer, and then the browser gets back to GSAP's calculations which also has tweens that trigger batchDraw(), and then maybe when GSAP finishes its update, Kinetic's kicks in again and says "oh, I need to draw this layer again" so it's being drawn twice for the same update cycle of GSAP. Or maybe the setTimeout() from Kinetic's update cycle fires FIRST, draws the layer, then GSAP's setTimout() updates and calculates all the new values, and then by the time that's done, Kinetic's setTimeout fires again and draws the layer. Same issue - two Kinetic updates for one GSAP update. 

 

That's why it's so critical that repaints be synchronized to animation updates. This works great when the requestAnimationFrame is supported properly in the browser, but it breaks down when setTimeout() must be used. This also seems like an argument in favor of using GSAP's KineticPlugin instead of relying on Kinetic's batchDraw() functionality. The plugin's update cycle will ALWAYS be perfectly synchronized with the animations whereas Kinetic's batchDraw may not. 

 

Does this data support what you're seeing as well? Got any other theories or questions? 

Link to comment
Share on other sites

No worries - there's no rush. So far:

 

1) Using autoDraw=true combined with onUpdate->batchDraw is the slowest of all (6 screen updates vs. 8 with autoDraw=true alone vs. 13-14 with onUpdate->batchDraw alone. (3000 shapes, iPhone 5, iOS 6).

 

2) No difference on page reload. I have used an instrumented version of GSAP with a log where the setTimeout fallback is activated and it does not seem to get hit. Same for Kineticjs: RAF is used there as well (for the batchDraw case).

 

3) When autoDraw=true and no onUpdate callback is used (the "worst" case in this test), batchDraw does not get called at all. Conversely, when autoDraw=true and batchDraw is invoked from the onUpdate callback, draw() is never called by the Kinetic GSAP plugin (as expected) so it seems there is no overlap or conflict between the two.

 

The same test run on Chrome/desktop shows that autoDraw=true calls draw() on average the same or more times than the animation that backs batchDraw() does - at around 100 redraws with 3000 shapes - so there autoDraw and batchDraw are roughly on par (autoDraw slightly better - but these values may not be that reliable). On Safari/desktop I see a lot less redraws (the number of redraws is not always a good indicator - but here it shows how long it takes for the browser to do a full cycle). BatchDraw consistently redraws 17 times. GSAP with the Kinetic plugin and autoDraw=true sets to 20/21 consistently. Still I have the impression the animation with batchDraw is more fluid - but it's just that, an impression.

 

BTW, with 10000 shapes on Desktop Safari, TweenMax aften fallbacks to setTimeout (probably bc the browser is so busy when the animation is started that it doesn't get a chance to trigger a RAF within the 1s interval used by GSAP to detect possible RAF failures).

 

It has to be said that what we are trying to do here is a pretty extreme case - there is no noticeable difference (to me) between the two options with hundreds of shapes. My test is also far from being scientific - so perhaps I should setup a more rigorous test.

 

My conclusion so far is that this has nothing to do with GSAP and the GSAP Kinetic plugin redraw policy - which is very sensible and very fast (and GSAP itself is WAY faster than Kinetic.Tween).

Most likely it's a combination of timing factors more than redraw policies.

Link to comment
Share on other sites

Thanks for sharing the details. You make an excellent point about the massive quantity of tweens causing the setTimeout fallback due to requestAnimationFrame not being able to fire frequently enough. Can you try calling TweenLite.ticker.useRAF(true) AFTER creating all the tweens (or a short time after that) and see if that makes a difference in the overall performance?

Link to comment
Share on other sites

Thanks for sharing the details. You make an excellent point about the massive quantity of tweens causing the setTimeout fallback due to requestAnimationFrame not being able to fire frequently enough. Can you try calling TweenLite.ticker.useRAF(true) AFTER creating all the tweens (or a short time after that) and see if that makes a difference in the overall performance?

 

It does not seem to make a difference (I tried tuning a few delays as well). As I mentioned above I don't see the setTimeout kicking in on iOS with 3000 shapes (it does however with more shapes or if the phone is connected to a debugger - but most likely because the browser is exhausted : ).

Link to comment
Share on other sites

Hi guys!  This is Eric from KineticJS.  Thought I'd chime in :)

 

@atomic, if GS is meeting your needs in terms of the awesome additional functionality that it provides, stick with it!  The Kinetic.Tween class is a super simple, light weight tweening library that  provides the bare essentials.  If you just need basic tweening, the Kinetic.Tween class would probably meet your needs, but if you need more advanced controls, I definitely recommend GS over any other tweening library out there.

 

Also, can you elaborate a bit more on the Kinetic.Tween performance issue?  my tests showed that the performance is on par with GS (it should be, especially since it's very simple)

Link to comment
Share on other sites

Hey Eric! Welcome. Great having you chime in here. And thanks again for heartily endorsing GSAP's usage with KineticJS. 

 

As for the performance, there are some files attached to a post above that should make it pretty simple for you to compare (it says "Kinetic_Benchmark.zip"). There does seem to be a pretty big difference in performance once there are more than 100 tweens going. 

 

Nice work on all the recent updates. I know that's no small feat. 

Link to comment
Share on other sites

Ok, I think I got it. I modified the benchmark to use a "RAF pump", which is basically a dumb function that calls draw() and then RAF with itself as callback endlessly. This simulates what really happens in Kinetic's batchDraw():

  // autoDraw is set to false in the tweens
  // NO onUpdate callback is used as we will use the "pump" below to draw

  var RAF = window.requestAnimationFrame || ....

  function RAFPump() {
    RAF(function() {
      layer.draw();
      RAFPump()
    });
  }
 

Now, I start the pump as soon as all tweens are created. This essentially schedules a draw upon every "tick". AFAICT this is precisely what happens when Kinetic's batch animation is used (in this simple one-layer case): the animation runs its own RAF pump (well, a bit more clever than the one above) and draws whatever layer has been scheduled for batch-drawing in between ticks. So with the "pump" we are now completely decoupled from Kinetic's drawing routines.

 

The result? As expected, the behaviour is exactly the same shown by Kinetic's batchDraw - i.e. more frames drawn than we get with the GS Kinetic plugin in autoDraw mode.

 

So it seems that, when autoDraw=true is used, in between _onTick callbacks in the GS Kinetic Plugin (which trigger the actual layer.draw()) there is room for one or more callbacks of the pump to kick in (most likely in-between tweens, I suppose, when the state of the shapes may be slightly inconsistent?). I am not too aware of the internals of GS, but this is the only reasonable explanation I can come out with. The result in this simple - and purposely artificial - benchmark is "more frames being drawn on screen" when a pump is used vs. drawing only after all tweens have been honoured for that time-slice. I am not sure this would be a desirable effect in real-life applications when interactivity is required - but again, we are looking at something rather artificial here.

 

What do you think

Link to comment
Share on other sites

@eric first off, thanks for bringing such an awesome library to life.

 

Regarding Kinetic.Tween I am definitely an advocate of having Kinetic to provide its tweening functionality out of the box. It just makes so much sense - and it keeps Kinetic dependency-free and ready-to-use by most people who want to learn and use the library. So thanks for going through the effort of adding it to the core library. It is not about Kinetic.Tween vs. GS - they both make sense for different reasons and it's great developers can have a choice. I am going with GS because it helps me to make complex things simple and keep my application workflow tidy. I am sure many other users will just love the way Kinetic.Tween works - and I am sure it will get better and better while still being very accessible. Performance is of course important - but that's never the only reason.

 

I started this topic because I wanted to understand better what sort of drawing policy (KineticPlugin+autoDraw vs. Kinetic.Layer.batchDraw()) was going to bring the best results - as I couldn't make up my mind with the real-life applications I am building. Jack kindly cooked up an interesting - albeit purposely artificial - benchmark to test the options, which then we tweaked a bit. That benchmark incidentally shows a significant performance difference between Kinetic.Tween and GS - esp. on iOS (btw, I have also tried the edge version straight from GitHub - which is a little bit faster than stock 4.5.1). Again, this was no GS vs. Kinetic.Tween, but it is probably something you may want to have a look at.

Link to comment
Share on other sites

Jack,

 

I investigated a bit the case and run a few tests on desktop (Mac) Safari - as it shows a much worse performance than Chrome and the same symptoms of iOS Safari. I am a bit puzzled by the result - so you may want to have a look at it. 

 

It just did not make sense that the RAF pump above was going to draw more than the plugin's _onTick callback - as they both, by definition, run their callbacks at most once per RAF tick. So the problem was either than _onTick was getting called less than it should have (unlikely, as it is directly registered to the ticker) or it was getting called too much. It turns out it might be the latter.

 

As strange as it may sound (at least to me), the _onTick callback of the Kinetic Plugin is called twice per RAF tick - but only on Safari (both OSX and iOS version). Therefore the apparent 

slowness vs. the simplest possible drawing routine (= one and only one per RAF tick).

And yes, I have checked and double checked - it's a RAF and not a setTimeout. 

 

So I have setup a simple RAF callback much like the "pump" above. The assumption is that its callback is run sequentially with GS's ticker event dispatcher - as it cannot be otherwise (there is only one animation frame regardless from how many callbacks request it). In that callback I simply increase a counter on each run to mark the current animation frame - so I can track it in the Kinetic Plugin _onTick. From _onTick I log the counter, the number of times it's been called, the ticker's frame and the ticker's time. The benchmark is setup with 3000 shapes (but really any number would produce the same result) and our reference is Chrome on OSX.

 

Results on Chrome OSX:

RAF:1 TICK:1 FRAME:18 TIME:0.523
RAF:2 TICK:2 FRAME:19 TIME:0.657
RAF:3 TICK:3 FRAME:20 TIME:0.784
RAF:4 TICK:4 FRAME:21 TIME:0.904
RAF:5 TICK:5 FRAME:22 TIME:1.011
RAF:6 TICK:6 FRAME:23 TIME:1.111
RAF:7 TICK:7 FRAME:24 TIME:1.229
RAF:8 TICK:8 FRAME:25 TIME:1.386
RAF:9 TICK:9 FRAME:26 TIME:1.565
RAF:10 TICK:10 FRAME:27 TIME:1.712
...

The above shows that _onClick (the "TICK" counter) is called once per "RAF" iteration as expected. Now let's look at Safari (OSX or iOS - different values, but same result):

RAF:0 TICK:1 FRAME:33 TIME:0.742
RAF:1 TICK:2 FRAME:34 TIME:1.33
RAF:1 TICK:3 FRAME:35 TIME:1.843
RAF:2 TICK:4 FRAME:36 TIME:2.382
RAF:2 TICK:5 FRAME:37 TIME:2.99
RAF:3 TICK:6 FRAME:38 TIME:3.652
RAF:3 TICK:7 FRAME:39 TIME:4.334
RAF:4 TICK:8 FRAME:40 TIME:5.077
RAF:4 TICK:9 FRAME:41 TIME:5.858
RAF:5 TICK:10 FRAME:42 TIME:6.63
RAF:5 TICK:11 FRAME:43 TIME:7.406
RAF:6 TICK:12 FRAME:44 TIME:8.186
RAF:6 TICK:13 FRAME:45 TIME:8.949
RAF:7 TICK:14 FRAME:46 TIME:9.706
RAF:7 TICK:15 FRAME:47 TIME:10.416
RAF:8 TICK:16 FRAME:48 TIME:11.095
RAF:8 TICK:17 FRAME:49 TIME:11.718
RAF:9 TICK:18 FRAME:50 TIME:12.315
RAF:9 TICK:19 FRAME:51 TIME:12.879

From the result above you see that _onTick is called TWICE per animation frame instead of once. For this reason autoDraw=true shows a worse performance than a simple callback that draws every time a RAF kicks in: it draws the Kinetic layer twice per tick.

 

Note that the above can be reproduced with just about any number of shapes, so it seems to affect every draw performed by the Kinetic plugin with autoDraw enabled.

 

I have added frame and time to the logs above as they may give some hints on what the culprit could be. It seems to me that there might be some deeper issue here than we thought.

 

HTH

Link to comment
Share on other sites

Perhaps this is related to the above:

<div>FPS: <span id="fps"></span></div>
<script>
document.body.onload = function() {
  var fpsEl = document.getElementById('fps');
  function updateFps(evt) {
    var fps = Math.floor(evt.target.frame / evt.target.time);
    fpsEl.textContent = fps;
  }
  TweenMax.ticker.addEventListener('tick', updateFps, null, true);
}
</script>

The above outputs a reasonable value on Chrome and Firefox (~60 FPS) but it's completely wrong on Safari on OSX and iOS (it is ~120 - pretty much twice as much as it should be).

Link to comment
Share on other sites

That was incredibly helpful, atomictag. Thank you so much for putting that together. It was a very tricky bug to track down, but it boiled down to the fact that the ID that gets returned by the requestAnimationFrame request starts at 1 in Chrome, but 0 in Safari. Since 0 is a "falsey" value, it passed a conditional "if (id) {...}" check. It should be resolved in the attached preview of 1.9.7. Would you mind double-checking to see if performance improves in Safari for you? 

GSAP_1.9.7_preview.zip

  • Like 2
Link to comment
Share on other sites

Yup. All fixed now. With the latest 1.9.7 the Safari issues are gone and performance is up to the expected levels (actually higher than that - it's nearly a 100% increase on the 3000-shapes benchmark on iOS, no jokes). Guess this fix will make a lot of iOS developers happy in the future.

 

We can now answer the original question in this long thread ;) :

 

- yes, you can safely let the GS Kinetic Plugin (configured with the default autoDraw=true) handle all drawings for you as redraws are correctly batched

- yes, it's also OK to use autoDraw=false in the GS Kinetic Plugin and invoke layer.batchDraw() (NOT draw!!) inside your tweens/timelines onUpdate callbacks (if you need to do something fancier than simply pushing a layer through)

- the choice between the 2 options is pretty much up to convenience and use cases (for example, sometimes it's nice to know exactly where a draw() request is pushed in order to add debugging sprites etc... so you'd go for the second option - while in other cases you'll appreciate the convenience of not having to add tons of identical onUpdate callbacks to your tweens... and you'd go for the first one)

- there is no particular performance impact in using one strategy or the other as they both queue up the layers to draw in an array and serialize draw() calls on each of them once per tick. If you really into the bits-counting business, use autoDraw=true instead of onUpdate

- if you know your target platform has problems with RAF and defaults/fallbacks to setTimeout for tick handling, then you probably want to use autoDraw=true in the Kinetic plugin as this serializes computations and drawings within a single callback, which is likely far more reliable than having both GS and Kinetic to juggle with their own setTimeout (things can get out of sync and stutter).

 

All for today.

 

P.S. BTW, I stared at the result of the FPS test I made for a good 1/2 hour as I could not believe it would report 2x on Safari vs. Chrome. I started challenging math, the laws of physics and the universe as a whole :)

  • Like 3
Link to comment
Share on other sites

  • 8 months later...
Guest
This topic is now closed to further replies.
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...