From jurgen at ivoryemr.co.za Tue Sep 2 10:45:51 2025 From: jurgen at ivoryemr.co.za (Jurgen Doll) Date: Tue, 2 Sep 2025 12:45:51 +0200 Subject: RichTextArea alternative FastCache suggestions Message-ID: <0FE25242-C7E7-4B5E-968A-CC2A4FB87E00@getmailspring.com> Hi Andy I noticed in VFlow.onContentChange (line 1051) that the whole cellCache is cleared, instead of just the relevant cells that were changed being reset. On reviewing the FastCache implementation I can see the difficulty in why this is the case, so after giving it some thought I have two simple variants of FastCache, that would enable resetting of cells in the cache, for you to consider using instead. First some background. The current FastCache is great for random data content, however if I'm not mistaken the RichTextArea data that it holds isn't random but rather consists of a range (window) of consecutive cells (paragraphs) from some start index to an end index. These ranges usually move linearly forwards or backwards a single cell at a time (for scrolling), or can jump to either the immediately preceeding or proceeding range (for paging), and sometimes jump to another range completely. It's with this type of data content and behavior that the following two variants are based on. In the first variant the HashMap is simply dropped and the cache is based solely on the Entry array using the modulo of the index and cache size. This then becomes a circular or ring cache: public class FastCircularCache { private static record Entry(int index, T cell) { } private final Entry[] data; private final int capacity; public FastCircularCache(int capacity) { data = new Entry[capacity]; this.capacity = capacity; } public T get(int row) { Entry entry = data[row % capacity]; if (entry != null && entry.index() == row) { return entry.cell(); } return null; } /** * Adds a new cell to the cache. NOTE: this method does not check whether * another cell for the given row is present, so this call must be preceded by a * {@link #get(int)}. */ public void add(int index, T cell) { data[index % capacity] = new Entry<>(index, cell); } public void reset(int start, int end) { for (int i = start; i < end; i++) { data[i % capacity] = null; } } public void clear() { Arrays.fill(data, null); } } In the second variant the array is dropped instead and the cache is based solely on the HashMap together with min and max int variables, which are the minium and maximum indices present at a particular moment in the HashMap. These are used as eviction indices, where min is evicted from the HashMap if the new index is increasing otherwise max is evicted if the new index is decreasing. This is then a window or sliding cache: public class FastSlidingCache { private int min = Integer.MAX_VALUE, max = 0; private final HashMap data; private final int capacity; public FastSlidingCache(int capacity) { data = new HashMap<>(capacity); this.capacity = capacity; } public T get(int row) { if (row < min || row > max) return null; return data.get(row); } /** * Adds a new cell to the cache. When the cache is full, this method evicts a * cell from the cache first. NOTE: this method does not check whether * another cell for the given row is present, so this call must be preceded by a * {@link #get(int)}. */ public void add(int index, T cell) { if (data.size() >= capacity) { if (index < min) while (!evict(max--)); if (index > max) while (!evict(min++)); } if (index < min) min = index; if (index > max) max = index; data.put(index, cell); } private boolean evict(int index) { return data.remove(index) != null; } public void reset(int start, int end) { for (int i = start; i < end; i++) { data.remove(i); } } public void clear() { min = Integer.MAX_VALUE; data.clear(); max = 0; } } Each variant has it's strengths and weaknesses. I hope you'll find one of them useful or at least inspiring in improving the current FastCache implementation. Kind regards Jurgen -------------- next part -------------- An HTML attachment was scrubbed... URL: From andy.goryachev at oracle.com Tue Sep 2 19:16:39 2025 From: andy.goryachev at oracle.com (Andy Goryachev) Date: Tue, 2 Sep 2025 19:16:39 +0000 Subject: [External] : RichTextArea alternative FastCache suggestions In-Reply-To: <0FE25242-C7E7-4B5E-968A-CC2A4FB87E00@getmailspring.com> References: <0FE25242-C7E7-4B5E-968A-CC2A4FB87E00@getmailspring.com> Message-ID: Dear Jurgen: Thank you for this suggestion! You are absolutely right noticing that the cache contains ranges as opposed to random indexes. I admit that the current implementation is less than optimal, but I wanted to avoid iterating over the whole cache. There is also https://bugs.openjdk.org/browse/JDK-8355986 which might have been triggered by me picking a 100 as the default value for the sliding window. Personally, I haven't noticed performance issues on my mac or my windows laptop, have you? One possibility might be an optimized hash table whose keys encode the starting index of the range. This way we could iterate over the keys, removing the whole buckets. Another alternative is a BTree+ like structure, but that gets complicated rather quickly. Another thing that did not make it into the incubator was configuration parameters in the constructor. I initially thought there might be a need to fine-tune various internal parameters (such as sliding window size or, in this case, the cache implementation), but it was shot down as unwanted exposure of the implementation details. I am going to add your comment to the ticket, thanks! -andy From: Jurgen Doll Date: Tuesday, September 2, 2025 at 03:46 To: Andy Goryachev , openjfx-discuss at openjdk.org Subject: [External] : RichTextArea alternative FastCache suggestions Hi Andy I noticed in VFlow.onContentChange (line 1051) that the whole cellCache is cleared, instead of just the relevant cells that were changed being reset. On reviewing the FastCache implementation I can see the difficulty in why this is the case, so after giving it some thought I have two simple variants of FastCache, that would enable resetting of cells in the cache, for you to consider using instead. First some background. The current FastCache is great for random data content, however if I'm not mistaken the RichTextArea data that it holds isn't random but rather consists of a range (window) of consecutive cells (paragraphs) from some start index to an end index. These ranges usually move linearly forwards or backwards a single cell at a time (for scrolling), or can jump to either the immediately preceeding or proceeding range (for paging), and sometimes jump to another range completely. It's with this type of data content and behavior that the following two variants are based on. In the first variant the HashMap is simply dropped and the cache is based solely on the Entry array using the modulo of the index and cache size. This then becomes a circular or ring cache: public class FastCircularCache { private static record Entry(int index, T cell) { } private final Entry[] data; private final int capacity; public FastCircularCache(int capacity) { data = new Entry[capacity]; this.capacity = capacity; } public T get(int row) { Entry entry = data[row % capacity]; if (entry != null && entry.index() == row) { return entry.cell(); } return null; } /** * Adds a new cell to the cache. NOTE: this method does not check whether * another cell for the given row is present, so this call must be preceded by a * {@link #get(int)}. */ public void add(int index, T cell) { data[index % capacity] = new Entry<>(index, cell); } public void reset(int start, int end) { for (int i = start; i < end; i++) { data[i % capacity] = null; } } public void clear() { Arrays.fill(data, null); } } In the second variant the array is dropped instead and the cache is based solely on the HashMap together with min and max int variables, which are the minium and maximum indices present at a particular moment in the HashMap. These are used as eviction indices, where min is evicted from the HashMap if the new index is increasing otherwise max is evicted if the new index is decreasing. This is then a window or sliding cache: public class FastSlidingCache { private int min = Integer.MAX_VALUE, max = 0; private final HashMap data; private final int capacity; public FastSlidingCache(int capacity) { data = new HashMap<>(capacity); this.capacity = capacity; } public T get(int row) { if (row < min || row > max) return null; return data.get(row); } /** * Adds a new cell to the cache. When the cache is full, this method evicts a * cell from the cache first. NOTE: this method does not check whether * another cell for the given row is present, so this call must be preceded by a * {@link #get(int)}. */ public void add(int index, T cell) { if (data.size() >= capacity) { if (index < min) while (!evict(max--)); if (index > max) while (!evict(min++)); } if (index < min) min = index; if (index > max) max = index; data.put(index, cell); } private boolean evict(int index) { return data.remove(index) != null; } public void reset(int start, int end) { for (int i = start; i < end; i++) { data.remove(i); } } public void clear() { min = Integer.MAX_VALUE; data.clear(); max = 0; } } Each variant has it's strengths and weaknesses. I hope you'll find one of them useful or at least inspiring in improving the current FastCache implementation. Kind regards Jurgen -------------- next part -------------- An HTML attachment was scrubbed... URL: From jurgen at ivoryemr.co.za Wed Sep 3 12:33:45 2025 From: jurgen at ivoryemr.co.za (Jurgen Doll) Date: Wed, 3 Sep 2025 14:33:45 +0200 Subject: [External] : RichTextArea alternative FastCache suggestions In-Reply-To: References: Message-ID: <6E29A74B-0DD4-4C12-9B25-0999A5289828@getmailspring.com> I was thinking about a cache size parameter setting maybe, although the current default of 512 is generous, I was wondering about a really large screen scenario, but then wouldn't the text be larger as well ? Maybe just have a system property in case ? On Sep 2 2025, at 9:16 pm, Andy Goryachev wrote: > Dear Jurgen: > > Thank you for this suggestion! You are absolutely right noticing that the cache contains ranges as opposed to random indexes. > > I admit that the current implementation is less than optimal, but I wanted to avoid iterating over the whole cache. There is also https://bugs.openjdk.org/browse/JDK-8355986 which might have been triggered by me picking a 100 as the default value for the sliding window. Personally, I haven't noticed performance issues on my mac or my windows laptop, have you? > > One possibility might be an optimized hash table whose keys encode the starting index of the range. This way we could iterate over the keys, removing the whole buckets. Another alternative is a BTree+ like structure, but that gets complicated rather quickly. > > Another thing that did not make it into the incubator was configuration parameters in the constructor. I initially thought there might be a need to fine-tune various internal parameters (such as sliding window size or, in this case, the cache implementation), but it was shot down as unwanted exposure of the implementation details. > > I am going to add your comment to the ticket, thanks! > > -andy > > > > > From: Jurgen Doll > Date: Tuesday, September 2, 2025 at 03:46 > To: Andy Goryachev , openjfx-discuss at openjdk.org > Subject: [External] : RichTextArea alternative FastCache suggestions > > Hi Andy > > I noticed in VFlow.onContentChange (line 1051) that the whole cellCache is cleared, instead of just the relevant cells that were changed being reset. On reviewing the FastCache implementation I can see the difficulty in why this is the case, so after giving it some thought I have two simple variants of FastCache, that would enable resetting of cells in the cache, for you to consider using instead. > > First some background. The current FastCache is great for random data content, however if I'm not mistaken the RichTextArea data that it holds isn't random but rather consists of a range (window) of consecutive cells (paragraphs) from some start index to an end index. These ranges usually move linearly forwards or backwards a single cell at a time (for scrolling), or can jump to either the immediately preceeding or proceeding range (for paging), and sometimes jump to another range completely. It's with this type of data content and behavior that the following two variants are based on. > > In the first variant the HashMap is simply dropped and the cache is based solely on the Entry array using the modulo of the index and cache size. This then becomes a circular or ring cache: > > public class FastCircularCache { > > private static record Entry(int index, T cell) { } > > > > private final Entry[] data; > > private final int capacity; > > > > public FastCircularCache(int capacity) { > > data = new Entry[capacity]; > > this.capacity = capacity; > > } > > > > public T get(int row) { > > Entry entry = data[row % capacity]; > > if (entry != null && entry.index() == row) { > > return entry.cell(); > > } > > return null; > > } > > > > /** > > * Adds a new cell to the cache. NOTE: this method does not check whether > > * another cell for the given row is present, so this call must be preceded by a > > * {@link #get(int)}. > > */ > > public void add(int index, T cell) { > > data[index % capacity] = new Entry<>(index, cell); > > } > > > > public void reset(int start, int end) { > > for (int i = start; i < end; i++) { > > data[i % capacity] = null; > > } > > } > > > > public void clear() { > > Arrays.fill(data, null); > > } > > } > > In the second variant the array is dropped instead and the cache is based solely on the HashMap together with min and max int variables, which are the minium and maximum indices present at a particular moment in the HashMap. These are used as eviction indices, where min is evicted from the HashMap if the new index is increasing otherwise max is evicted if the new index is decreasing. This is then a window or sliding cache: > > public class FastSlidingCache { > > > > private int min = Integer.MAX_VALUE, max = 0; > > private final HashMap data; > > private final int capacity; > > > > public FastSlidingCache(int capacity) { > > data = new HashMap<>(capacity); > > this.capacity = capacity; > > } > > > > public T get(int row) { > > if (row < min || row > max) return null; > > return data.get(row); > > } > > > > /** > > * Adds a new cell to the cache. When the cache is full, this method evicts a > > * cell from the cache first. NOTE: this method does not check whether > > * another cell for the given row is present, so this call must be preceded by a > > * {@link #get(int)}. > > */ > > public void add(int index, T cell) { > > if (data.size() >= capacity) { > > if (index < min) while (!evict(max--)); > > if (index > max) while (!evict(min++)); > > } > > > > if (index < min) min = index; > > if (index > max) max = index; > > > > data.put(index, cell); > > } > > > > private boolean evict(int index) { > > return data.remove(index) != null; > > } > > > > public void reset(int start, int end) { > > for (int i = start; i < end; i++) { > > data.remove(i); > > } > > } > > > > public void clear() { > > min = Integer.MAX_VALUE; > > data.clear(); > > max = 0; > > } > > } > Each variant has it's strengths and weaknesses. I hope you'll find one of them useful or at least inspiring in improving the current FastCache implementation. > > Kind regards > Jurgen > -------------- next part -------------- An HTML attachment was scrubbed... URL: From andy.goryachev at oracle.com Wed Sep 3 15:22:47 2025 From: andy.goryachev at oracle.com (Andy Goryachev) Date: Wed, 3 Sep 2025 15:22:47 +0000 Subject: [External] : RichTextArea alternative FastCache suggestions In-Reply-To: <6E29A74B-0DD4-4C12-9B25-0999A5289828@getmailspring.com> References: <6E29A74B-0DD4-4C12-9B25-0999A5289828@getmailspring.com> Message-ID: Yes, a system property might be a good compromise since it would allow the developers to customize the behavior without making it a public API. The drawback is that it can only be applied to all the instances at the same time. -andy From: Jurgen Doll Date: Wednesday, September 3, 2025 at 05:48 To: Andy Goryachev Cc: openjfx-discuss at openjdk.org Subject: Re: [External] : RichTextArea alternative FastCache suggestions I was thinking about a cache size parameter setting maybe, although the current default of 512 is generous, I was wondering about a really large screen scenario, but then wouldn't the text be larger as well ? Maybe just have a system property in case ? On Sep 2 2025, at 9:16 pm, Andy Goryachev wrote: Dear Jurgen: Thank you for this suggestion! You are absolutely right noticing that the cache contains ranges as opposed to random indexes. I admit that the current implementation is less than optimal, but I wanted to avoid iterating over the whole cache. There is also https://bugs.openjdk.org/browse/JDK-8355986 which might have been triggered by me picking a 100 as the default value for the sliding window. Personally, I haven't noticed performance issues on my mac or my windows laptop, have you? One possibility might be an optimized hash table whose keys encode the starting index of the range. This way we could iterate over the keys, removing the whole buckets. Another alternative is a BTree+ like structure, but that gets complicated rather quickly. Another thing that did not make it into the incubator was configuration parameters in the constructor. I initially thought there might be a need to fine-tune various internal parameters (such as sliding window size or, in this case, the cache implementation), but it was shot down as unwanted exposure of the implementation details. I am going to add your comment to the ticket, thanks! -andy From: Jurgen Doll Date: Tuesday, September 2, 2025 at 03:46 To: Andy Goryachev , openjfx-discuss at openjdk.org Subject: [External] : RichTextArea alternative FastCache suggestions Hi Andy I noticed in VFlow.onContentChange (line 1051) that the whole cellCache is cleared, instead of just the relevant cells that were changed being reset. On reviewing the FastCache implementation I can see the difficulty in why this is the case, so after giving it some thought I have two simple variants of FastCache, that would enable resetting of cells in the cache, for you to consider using instead. First some background. The current FastCache is great for random data content, however if I'm not mistaken the RichTextArea data that it holds isn't random but rather consists of a range (window) of consecutive cells (paragraphs) from some start index to an end index. These ranges usually move linearly forwards or backwards a single cell at a time (for scrolling), or can jump to either the immediately preceeding or proceeding range (for paging), and sometimes jump to another range completely. It's with this type of data content and behavior that the following two variants are based on. In the first variant the HashMap is simply dropped and the cache is based solely on the Entry array using the modulo of the index and cache size. This then becomes a circular or ring cache: public class FastCircularCache { private static record Entry(int index, T cell) { } private final Entry[] data; private final int capacity; public FastCircularCache(int capacity) { data = new Entry[capacity]; this.capacity = capacity; } public T get(int row) { Entry entry = data[row % capacity]; if (entry != null && entry.index() == row) { return entry.cell(); } return null; } /** * Adds a new cell to the cache. NOTE: this method does not check whether * another cell for the given row is present, so this call must be preceded by a * {@link #get(int)}. */ public void add(int index, T cell) { data[index % capacity] = new Entry<>(index, cell); } public void reset(int start, int end) { for (int i = start; i < end; i++) { data[i % capacity] = null; } } public void clear() { Arrays.fill(data, null); } } In the second variant the array is dropped instead and the cache is based solely on the HashMap together with min and max int variables, which are the minium and maximum indices present at a particular moment in the HashMap. These are used as eviction indices, where min is evicted from the HashMap if the new index is increasing otherwise max is evicted if the new index is decreasing. This is then a window or sliding cache: public class FastSlidingCache { private int min = Integer.MAX_VALUE, max = 0; private final HashMap data; private final int capacity; public FastSlidingCache(int capacity) { data = new HashMap<>(capacity); this.capacity = capacity; } public T get(int row) { if (row < min || row > max) return null; return data.get(row); } /** * Adds a new cell to the cache. When the cache is full, this method evicts a * cell from the cache first. NOTE: this method does not check whether * another cell for the given row is present, so this call must be preceded by a * {@link #get(int)}. */ public void add(int index, T cell) { if (data.size() >= capacity) { if (index < min) while (!evict(max--)); if (index > max) while (!evict(min++)); } if (index < min) min = index; if (index > max) max = index; data.put(index, cell); } private boolean evict(int index) { return data.remove(index) != null; } public void reset(int start, int end) { for (int i = start; i < end; i++) { data.remove(i); } } public void clear() { min = Integer.MAX_VALUE; data.clear(); max = 0; } } Each variant has it's strengths and weaknesses. I hope you'll find one of them useful or at least inspiring in improving the current FastCache implementation. Kind regards Jurgen -------------- next part -------------- An HTML attachment was scrubbed... URL: From notzed at gmail.com Sun Sep 21 03:02:49 2025 From: notzed at gmail.com (Michael Zucchi) Date: Sun, 21 Sep 2025 12:32:49 +0930 Subject: JDK-8210547[linux] Uncontrolled framerate, general frame sync/over-calculating animations Message-ID: Morning list! I hit the above bug[1] a little while ago while using a Transition for timing on a couple of GNU/Linux systems [2,3].? Basically it runs flat-chat because each interpolation call makes changes which triggers a new render pass which then re-runs the animators if they're active, which retriggers another render pass and so on. Trivial example that demonstrated is below.? If you set a specific frame-rate in the constructor then it doesn't occur. None of the related system properties seem to have any effect.? Tracing through the code I couldn't see how this doesn't always happen with X11/OpenGL, plus there seems to be a bunch of stale/unfinished frame throttling code that never gets run.? I also noticed that gdk_timer was used for frame timing, but this is documented as not being suitable for this task - it lacks suitable resolution and must be reset each call guaranteeing drift.? And in any event timing unlinked from the display will never be accurate. I developed a small patch to use GLX_SGI_video_sync to synchronise to the actual video frame-rate.? It's probably hooked into the wrong place but i'm not very familiar with the code and couldn't find much documentation on the internals of prism and quantum toolkit; I've attached it as a matter of interest.? With this patch enabled multiple windows will render smoothly matching the video frame rate exactly and the animation calculations are only invoked once per frame apart from wrap-around of cyclic animations (I've only tested with simple scenes). The main drawback is added latency for low-frame rate monitors (I've tested with a system that can do anything from 25 to 150), and the 'animation now timestamp' is calculated ad-hoc rather than syncing to the expected presentation time.? Having to call glXMakeContext extra times isn't very cheap either, but it's already being called a lot. With some guidance/pointers I can look further if it would be of use to the project. Regards, ?Michael Z [1] https://bugs.openjdk.org/browse/JDK-8210547 [2] gentoo, liunux 6.12.36, AMD Ryzen 4700U APU. [3] slackare64-current linux 6.12.29, Ryzen 3900X, Radeon HD7970. -- ? ? ? ? ? ? Group g = new Group(new Text("Hello")); ? ? ? ? ? ? g.setTranslateX(100); ? ? ? ? ? ? g.setTranslateY(100); ? ? ? ? ? ? root.getChildren().setAll(g); ? ? ? ? ? ? Transition anim = new Transition() { ? ? ? ? ? ? ? ? double arg = 0; ? ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? ? setCycleCount(INDEFINITE); ? ? ? ? ? ? ? ? ? ? setCycleDuration(Duration.seconds(1)); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? @Override ? ? ? ? ? ? ? ? protected void interpolate(double frac) { ? ? ? ? ? ? ? ? ? ? g.setRotate(arg++); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? }; ? ? ? ? ? ? anim.play(); -------------- next part -------------- An HTML attachment was scrubbed... URL: -------------- next part -------------- A non-text attachment was scrubbed... Name: jfx-vsync.diff Type: text/x-patch Size: 10695 bytes Desc: not available URL: From kevin.rushforth at oracle.com Mon Sep 22 15:26:08 2025 From: kevin.rushforth at oracle.com (Kevin Rushforth) Date: Mon, 22 Sep 2025 08:26:08 -0700 Subject: JDK-8210547[linux] Uncontrolled framerate, general frame sync/over-calculating animations In-Reply-To: References: Message-ID: I didn't Cc this list on my reply. I have redirected this to openjfx-dev, which is the right list for all technical discussions. Let's continue this on that mailing list. -- Kevin On 9/20/2025 8:02 PM, Michael Zucchi wrote: > > Morning list! > > I hit the above bug[1] a little while ago while using a Transition for > timing on a couple of GNU/Linux systems [2,3]. Basically it runs > flat-chat because each interpolation call makes changes which triggers > a new render pass which then re-runs the animators if they're active, > which retriggers another render pass and so on.? Trivial example that > demonstrated is below.? If you set a specific frame-rate in the > constructor then it doesn't occur. None of the related system > properties seem to have any effect.? Tracing through the code I > couldn't see how this doesn't always happen with X11/OpenGL, plus > there seems to be a bunch of stale/unfinished frame throttling code > that never gets run.? I also noticed that gdk_timer was used for frame > timing, but this is documented as not being suitable for this task - > it lacks suitable resolution and must be reset each call guaranteeing > drift.? And in any event timing unlinked from the display will never > be accurate. > > I developed a small patch to use GLX_SGI_video_sync to synchronise to > the actual video frame-rate.? It's probably hooked into the wrong > place but i'm not very familiar with the code and couldn't find much > documentation on the internals of prism and quantum toolkit; I've > attached it as a matter of interest.? With this patch enabled multiple > windows will render smoothly matching the video frame rate exactly and > the animation calculations are only invoked once per frame apart from > wrap-around of cyclic animations (I've only tested with simple scenes). > > The main drawback is added latency for low-frame rate monitors (I've > tested with a system that can do anything from 25 to 150), and the > 'animation now timestamp' is calculated ad-hoc rather than syncing to > the expected presentation time.? Having to call glXMakeContext extra > times isn't very cheap either, but it's already being called a lot. > > With some guidance/pointers I can look further if it would be of use > to the project. > > Regards, > ?Michael Z > > [1] https://bugs.openjdk.org/browse/JDK-8210547 > [2] gentoo, liunux 6.12.36, AMD Ryzen 4700U APU. > [3] slackare64-current linux 6.12.29, Ryzen 3900X, Radeon HD7970. > > -- > ? ? ? ? ? ? Group g = new Group(new Text("Hello")); > ? ? ? ? ? ? g.setTranslateX(100); > ? ? ? ? ? ? g.setTranslateY(100); > ? ? ? ? ? ? root.getChildren().setAll(g); > ? ? ? ? ? ? Transition anim = new Transition() { > ? ? ? ? ? ? ? ? double arg = 0; > > ? ? ? ? ? ? ? ? { > ? ? ? ? ? ? ? ? ? ? setCycleCount(INDEFINITE); > ? ? ? ? ? ? ? ? ? ? setCycleDuration(Duration.seconds(1)); > ? ? ? ? ? ? ? ? } > > ? ? ? ? ? ? ? ? @Override > ? ? ? ? ? ? ? ? protected void interpolate(double frac) { > ? ? ? ? ? ? ? ? ? ? g.setRotate(arg++); > ? ? ? ? ? ? ? ? } > ? ? ? ? ? ? }; > ? ? ? ? ? ? anim.play(); -------------- next part -------------- An HTML attachment was scrubbed... URL: