Floorplanner Tech Blog
Canvas vs Canvas vs SVG performance on iPad
Last week I created two simple tests to get some insights on the performance of Canvas and SVG on iDevices. The test is a blue square bouncing from left to right and back again, it’s repositioned every frame. The faster a browser can render a frame, the better the performance is on the device. Well.. at least in this particular case. In the Canvas test, the scene is cleared (by using clearRect) and the square is drawn (using fillRect) at the new position. The SVG test draws the square once and only updates its x-coordinate. Thanks to Tim Knip I found another way to move the square in Canvas using CSS3 3D Transforms. Since these transforms are hardware accelerated on iDevices, it’s interesting to see how Canvas using 3D transforms performs.
| Device | OS | Browser | Canvas (redraw) | Canvas (3D transforms) | SVG |
|---|---|---|---|---|---|
| iPhone 3Gs | iOS 4.2 | Safari | 45 | 90 | 90 |
| iPhone 4 | iOS 4.2 | Safari | 30 | 92 | 80 |
| iPad 1 | iOS 4.2 | Safari | 90 | 90 | 95 |
| Nexus 1 | Android 2.2 | V8 | 75 | 99 | - |
Canvas vs SVG performance on iPad
I’m doing a bit of HTML5 performance testing, specifically on the iPhone and iPad. When doing interactive stuff, it seems that most of the resources are used for updating the scene (clearing and drawing), and not so much by doing calculations in JavaScript. I created two very simple tests to see if the performance is different using Canvas or SVG. It’s a fabulous blue cube bouncing up and down as fast as possible. The number of updates (frames) per second tells something about the performance. The higher the number the better the performance. This is definitely no scientific method of performance testing. It’s just an indication, but it’s something. Here are my two tests, Canvas and SVG. The Canvas test clears and draws the square on its new position every interval (frame) by using clearRect and fillRect. The SVG test works a little different. The square is drawn once and its x-coordinate is updated every interval. These are my findings so far:
| Device | OS | Browser | Canvas | SVG |
|---|---|---|---|---|
| MBP | OSX Leopard | Chrome 8 | 230 | 230 |
| iPhone 3G | iOS 4.2 | Safari | 19 | 73 |
| iPhone 3Gs | iOS 4.2 | Safari | 45 | 90 |
| iPhone 4 | iOS 4.2 | Safari | 30 | 80 |
| iPad 1 | iOS 4.2 | Safari | 90 | 95 |
| iPad 2 | iOS 4.3 | Safari | 42 | 96 |
| Nexus 1 | Android 2.2 | V8 | 75 | - |
| HTC Desire | Android 2.2 | V8 | 75 | - |
Merge sort in JavaScript
Whenever I have a couple of minutes of spare time, I’m trying to learn more about algorithms . The first two algorithms were selection sort and insertion sort. Next up is merge sort. Merge sort is a bit different than selection and insertion sort. Selection and insertion sort are based on a loop inside a loop. That way you compare each value of a collection (one by one) to all the other values in the collection and sort them. Both in slightly different way. As you can imagine, sorting with a loop inside a loop isn’t very fast, Θ(n^2) to be precise. Merge sort attacks the problem in a different way. You could say that it’s based on the divide and conquer principal. It divides the problem in a bunch of tiny sub-problems, solves those problems and then combines them to get a result. The divide step isn’t that difficult. It takes a collection and splits it in two (equally sized) halves. This step is repeated until the smallest collection size is reached, 1. It uses a recursive function to accomplish this, a function that calls itself.
var collection = [5, 2, 4, 7, 1, 3, 2, 6];
divide(collection, 0, collection.length-1);
// a: collection, p: start index, r: end index
function divide(a, p, r) {
if(p < r) {
var q = Math.floor((p + r)/2); // calc middle of collection
divide(a, p, q);
divide(a, q + 1, r);
merge(a, p, q, r);
}
}
The conquer step is a bit tricker. It’s based on a function that is able to merge two sorted collections into a new sorted collection.
function merge(a, p, q, r) {
var n1 = q - p + 1;
var left = [];
for(var i = 0; i < n1; i++) {
left.push(a[p + i]);
}
left.push(Infinity);
var n2 = r - q;
var right = [];
for(var j = 0; j < n2; j++) {
right.push(a[q + j + 1]);
}
right.push(Infinity);
var i = 0;
var j = 0;
for(var k = p; k < r+1; k++) {
if(left[i]
So, the collection is first divided into sub-collections with the size of one. Those sub-collections are then merged together. That's done, step by step, by merging and sorting the small collections into a bigger collection. Combine collections with a size of one into sorted collections of a size of two, then combine them into sorted collections of 4 etc. until all sub-collections are merged.
Instead of having a loop inside a loop, you have a recursive divide function and a combining loop function. This results in a faster sorting than insertion or selection sort: Θ(nlgn).Server-side PNG rendering of SWF images using Gnash
The problem
We need to display thumbnails for 2D images, which are SWF files.Option 1. Use embedded Flash
Pros:- quick drop-in solution;
- requirement for clients to have Flash installed in their browsers;
- thumbnail displaying logic depends on what we are displaying (2D, 3D, photo) = additional complexity;
- requires additional markup for embedding Flash;
- is slow to load and display;
- (the most important) embedded Flash object is not regular HTML-element and it puts some constraints on UI design (e.g. other HTML elements cannot be placed on top of rendered image).
Option 2. Manual rendering
SWF files are loaded into desktop Adobe AIR app, which produces PNG thumbnails and saves them where the client app can get them (default Paperclip paths in our case). This process can be made to work in batch mode, so there is no need to manually load, render and save each file. Pros:- thumbnails are PNG files, so they can be dealt with as any other regular images on client side;
- requires dedicated machine with the AIR app;
- when a user uploads an SWF file, it is not rendered at once, so we have to show him something else for visual feedback as we don’t have rendered image yet.
Option 3. Server-side rendering
The most logical and architecturally right option. Unfortunately, Flash player is proprietary software and there are no libraries or tools for this task. Nevertheless, it is possible, and I’ll explain how it can be done.Solution
We are going to use Gnash player. Gnash is the GNU SWF movie player, which can be run standalone on the desktop or an embedded device, as well as as a plugin for several browsers (from http://www.gnashdev.org). It it has a feature named “dump-gui”, which allows to dump player’s output as raw video file and doesn’t require X or any GUI libraries to run, what makes it a good candidate for our task.Step 1. Get Gnash
Gnash player has a package in many popular distributions, but it is compiled without the ‘dump’ option. Therefore, we need to configure and compile it from source. You can read about it here and here.
wget http://ftp.gnu.org/pub/gnu/gnash/0.8.8/gnash-0.8.8.tar.gz
./configure --enable-gui=gtk,dump
make
sudo make install
Here I’m configuring and compiling Gnash with both gtk and dump GUIs so that I could quickly check rendering results using gtk player. You won’t need it on server, of course.
Step 2. Render 2D images using SWF file as a player (supplying Flash parameters)
This step can be skipped if your swf file is displayed correctly in the player. In our case, if we run
gnash image_2d.swf
we will see only a part of an image, because image’s origin (x=0, y=0) is in the center of the image. To display it correctly, we will use another swf file named preview.swf, which, in its turn, loads and displays our 2D image. We need to supply file’s location using flash parameter named “url”. As different 2D images have different sizes, we’ll also use -j (width) and -k (height) parameters to generate large fixed size picture.
gnash preview.swf \
-P "FlashVars=url=file:///some/dir/image_2d.swf" -j 500 -k 500
Step 3. Deal with security settings
By default, preview.swf can only access files which are in or under the same directory as preview.swf. As on server side processing will take place in /tmp directory, while preview.swf will be located in the application’s directory, we need to configure Gnash to allow access to local files in /tmp. Create ~/.gnashrc file with this line:
set localSandboxPath /tmp
Step 4. Get raw video file from player
By default, player will run endlessly. There are three parameters, which can be used to stop its execution:- —once – doesn’t work in our case (throws an error);
- —timeout <sec> - should be set to some time (in seconds) after which player stops; it means, rendering will take at least 1 second (and generate 1 second of video) and we have to be sure that this time is enough to render any picture we have (or else it has to be 2, 3 seconds for all images) – not exactly what we would like;
- —max-advances N – stops after N frame advances; was tested with N=1 and worked well – we are going to use this parameter.
dump-gnash preview.swf \
-P "FlashVars=url=file:///some/dir/image_2d.swf” \
-D output.raw --max-advances 1 -j 500 -k 500
Output:
# WARNING: Gnash was told to loop the movie
# Gnash created a raw dump file with the following properties:
COLORSPACE=BGRA32
NAME=output.raw
WIDTH=500
HEIGHT=500
INTERVAL=10
FPS_DESIRED=100
TIME=0.251716
FPS_ACTUAL=99.3183
FRAMECOUNT=26
As we see from the output, Gnash produced output.raw which is 500x500 pixels raw video file with colorspace BGRA32. There are 26 frames in the video and rendering took about 0.25 sec.
Step 5. Extracting an image from raw video file
According to output, we have 8-bit BGRA raw video. What does it mean? It is just a sequence of one-byte values for each of four channels (Blue, Green, Red, Alpha) for each pixel for each frame. If we open that file with mplayer, we’ll see that our image flickers. That’s because player produces blank frames while loading image_2d.swf. What we really need from this video is the last frame. As we set output size to 500x500 pixels and each pixel is 4 bytes, we need last 1000000 bytes from output.raw.
tail -c 1MB output.raw > last_frame.raw
Step 6. Converting raw image to PNG
Knowing the structure of the raw file, we could use some PNG library and write a few lines of code to produce PNG from raw. But, as our application uses Paperclip and, in its turn, Paperclip uses Imagemagick to make thumbnails, we are going to use Imagemagick, too.
convert -size 500x500 -depth 8 rgba:last_frame.raw \
-separate -swap 0,2 -combine -trim png:render.png
Size and depth options as well as rgba prefix give convert tool information about our raw file. Imagemagick doesn’t support brga colorspace, so we are using rgba and swapping channels that is just a little bit harder than swapping letters :). Last parameter trims whitespace from the picture.
Step 7. Final script
This script is run by a subclass of Paperclip::Processor and produces source image for making thumbnails with Paperclip::Processor::Thumbnail. But this is another story :)
#!/bin/sh
raw=$(mktemp)
dump-gnash $1 -P "FlashVars=url=file://$2" \
-D $raw --max-advances 1 -j 500 -k 500
tail -c 1MB $raw | convert -size 500x500 -depth 8 rgba:- \
-separate -swap 0,2 -combine -trim png:$3
trap "rm $raw" EXIT
This script accepts three parameters:
- location of preview.swf;
- location of swf file to render;
- where to put the output
Issues
Although most of our 2D images were rendered correctly, a lot of them weren’t. Such Flash features as bitmap filters and blend modes are not yet implemented in Gnash, and are not trivial to implement. So, think about workarounds if you are using these features.Selection sort in JavaScript
In my quest to learn more about algorithms, I came across another sorting algorithm. This one is called Selection sort. The collection is sorted from beginning to end. While looping over the elements, it first finds the smallest element between the current position and the end of the collection. Then the smallest element and the current element exchange places. This way all elements are sorted when the loop has finished. In JavaScript it looks like this:
var collection = [5, 2, 4, 6, 1, 3];
var n = collection.length;
for(var j = 0; j Insertion sort in JavaScript
I started reading Introduction to Algorithms to educate myself on the subject.
There are books on algorithms that are rigorous but incomplete and others that cover masses of material but lack rigor. Introduction to Algorithms combines rigor and comprehensiveness.In order to motivate myself reading it cover to cover, I thought it’d be a good idea to blog about my progress. Beside me being “forced” to write an update every now and then, I might help somebody else in the progress. The first algorithm is solving a sorting problem. The technique is called Insertion sort. It’s suited for sorting a small number of elements and the idea is pretty simple. Insertion sort works the way many people sort a hand of playing cards. The collection is sorted from beginning to end. While looping over the elements, it compares the current element to the ones before it in the collection. All the elements that are bigger than the current element are moved one place to the right. The current element is inserted after the first element that is smaller than itself. Have a look at the Wikipedia page if you want to know more about it. In JavaScript it looks like this:
var collection = [5, 2, 4, 6, 1, 3];
for(var j = 1; j = 0 && collection[i] > key) {
collection[i+1] = collection[i];
i = i - 1;
}
collection[i+1] = key;
}
Interactive SVG on the iPad
It’s difficult to find any decent samples about interactive SVG on the iPad and iPhone. SVG is used more and more these days, but it’s still rather unknown. The combination with touch and gestures on an iDevice is even rarer.
Since I’ve been playing around with it a bit, I’d like to share what I’ve learned. This post describes the steps involved to create a blue rectangle that can be dragged, rotated and scaled.
SVG
To get things started we need to create an SVG document. The shape, in this case a blue rectangle, will be added to the document. Create a file with the .svg extension and add the following lines:
Touch
To make the rectangle interactive we need to listen to touch and gesture events. Gesture events are a special case of touch events. They are created to make it easier to handle multi touch events, like scaling and rotating. For scripting in SVG you can use the script type “text/ecmascript” inside an SVG doc. Let’s start with dragging the rectangle by adding touch listeners to the rectangle.
Then we add the script (after the rect tag) to handle the touch events and translate the rectangle.
The functions touchDown and touchMove both check if the touch is done by one finger. On touch down the start coordinates are recorded and on touch move the rectangle is moved to the new position. The function touchEnd is not used.
You can also see that the default browser action, panning the page, is prevented by calling event.preventDefault();
Gestures
Next up are handling the gestures to scale and rotate our rectangle. First add the listeners:
ongesturestart="gestureStart(event);"
ongesturechange="gestureChange(event);"
ongestureend="gestureEnd(event);"
and then the functions:
function gestureStart(event) {
event.preventDefault();
var node = event.target;
x = parseInt(node.getAttributeNS(null, 'x'));
y = parseInt(node.getAttributeNS(null, 'y'));
width = parseInt(node.getAttributeNS(null, 'width'));
height = parseInt(node.getAttributeNS(null, 'height'));
var transform = (node.getAttributeNS(null, 'transform'));
rotation = parseInt(transform.split('rotate(')[1].split(' ')[0]);
if(isNaN(rotation)) {
rotation = 0;
}
}
function gestureChange(event) {
event.preventDefault();
var node = event.target;
// scale
var newWidth = width * event.scale;
var newHeight = height * event.scale;
var newX = x - (newWidth - width)/2;
var newY = y - (newHeight - height)/2;
node.setAttributeNS(null, 'width', newWidth);
node.setAttributeNS(null, 'height', newHeight);
node.setAttributeNS(null, 'x', newX);
node.setAttributeNS(null, 'y', newY);
// rotation
var newRotation = rotation + event.rotation;
var centerX = newX + newWidth/2;
var centerY = newY + newHeight/2;
setRotation(node, newRotation, centerX, centerY);
}
function gestureEnd(event) {
rotation = rotation + event.rotation;
}
function setRotation(node, rotation, x, y) {
var centerX = x + width/2;
var centerY = y + height/2;
node.setAttributeNS(null, 'transform', 'rotate('+ rotation +' '+ x +' '+ y +')');
}
In general, these functions behave the same way as the touch functions. The values are recorded when the gesture starts and updated when the gesture changes.
Sample
That’s it. You can find a running version on the Floorplanner Labs and the whole code sample below. I added a toggle handlingGesture make sure the handling of the touch and gesture events don’t interfere. I also added a bit of a hack to make the drag work when the rectangle is rotated. The rotation is recorded and set to 0 before the movement and re-applied after the movement. You don’t need this hack when you use SVG transformations, but that’s another story.
Rails and the Google Apps Marketplace
1. Initial Setup
Get a Google Apps account, it doesn’t need to be a premier one. Create a marketplace enterprise account and setup a new application. Be sure to check “My product may be directly installed into Google Apps domains”. Fill in the manifest with the appropriate API’s permissions. An example manifest can be found below.
<?xml version="1.0" encoding="UTF-8" ?>
<ApplicationManifest xmlns="http://schemas.google.com/ApplicationManifest/2009">
<Name>[ApplicationName]</Name>
<Description>[Description]</Description>
<!-- Administrators and users will be sent to this URL for application support -->
<Support>
<!-- URL for application setup as an optional redirect during the install -->
<Link rel="setup" href="[ApplicationSetupUrl]?domain=${DOMAIN_NAME}" />
<!-- URL for application configuration, accessed from the app settings page in the control panel -->
<Link rel="manage" href="[ApplicationAdminUrl]?domain=${DOMAIN_NAME}" />
<!-- URL explaining how customers get support. -->
<Link rel="support" href="[ApplicationHelpUrl]" />
<!-- URL that is displayed to admins during the deletion process, to specify policies such as data retention, how to claim accounts, etc. -->
<Link rel="deletion-policy" href="[ApplicationPolicyUrl]" />
</Support>
<!-- Show this link in Google's universal navigation for all users -->
<Extension id="navLink" type="link">
<Name>[ApplicationName]</Name>
<Url>[ApplicationLoginUrl]?domain=${DOMAIN_NAME}</Url>
<!-- Used API's -->
<Scope ref="contactFeed"/>
<Scope ref="spreadsheetFeed"/>
<Scope ref="doclistFeed"/>
</Extension>
<!-- Declare our OpenID realm so our app is white listed -->
<Extension id="realm" type="openIdRealm">
<Url>[ApplicationRealm]</Url>
</Extension>
<Scope id="doclistFeed">
<Url>https://docs.google.com/feeds/</Url>
<Reason>[Reason]</Reason>
</Scope>
<Scope id="contactFeed">
<Url>https://www.google.com/m8/feeds/</Url>
<Reason>[Reason]</Reason>
</Scope>
</ApplicationManifest>
2. The setup page
After setting up your application and the manifest file we are going to add the application to your domain. Log into the Marketplace where you updated your manifest and click the blue “Add it now” button when you click on your application. Now the setup process begins and you have to follow the steps. On step 3 or 4 you will be redirected to your own domain to setup a Google Apps domain with your application. Below you can find a way to authenticate with Google’s OpenID server. This way you can get the e-mail and user information of the administrator of the Google Apps domain. After setting up the domain you can redirect back to Google and continue the setup process.
def login
# The domain needs to be set. For example with params[:domain]
authenticate_with_open_id("https://www.google.com/accounts/o8/site-xrds?hd=" + domain),
{ :required => ["http://axschema.org/contact/email", :email], :return_to => '/login/google'}) do |result, identity_url, registration|
if result.successful?
# Succesfully logged in
email = get_email(registration)
else
# Failed to login
end
end
end
def get_email(registration)
if !registration['email'].blank?
registration['email']
else
ax_response = OpenID::AX::FetchResponse.from_success_response(
request.env[Rack::OpenID::RESPONSE])
ax_response.data["http://axschema.org/contact/email"].first
end
end
3. Using the GDAta API’s - Retrieving contacts
After you installed the application, we can login by using the same method as described in step 2. After your logged we can try to use the GData API’s. For the GData API’s we need to use the OAuth authentication method instead of OpenID. All we need for this is the Marketplace Consumer Key, Consumer Secret and the email of the user we are trying retrieve information from. You can get these by visiting your application after you logged in with the Google Enterprise Marketplace. It’s on the listing page if you click on “View OAuth Consumer Key”. Below you can find how we get a contactlist from a user;
def get_contacts
email = 'user@email.com'
url = 'https://www.google.com/m8/feeds/contacts/default/full?xoauth_requestor_id=#{email}'
client = GData::Client::Contacts.new({:version => '3.0'})
client.headers["Authorization"] = get_authorization_header(url, :get)
response = client.get(url)
response.body
end
def get_authorization_header(url, meth)
consumer_key = 'Your-consumer-key'
consumer_secret = 'Your-consumer-secret'
consumer = OAuth::Consumer.new(consumer_key, consumer_secret, {
:site => "https://google.com",
:request_token_path => "/accounts/OAuthGetRequestToken",
:access_token_path => "/accounts/OAuthGetAccessToken",
:authorize_path => "/accounts/OAuthAuthorizeToken",
:http_method => meth
})
access_token = OAuth::AccessToken.new consumer
case meth
when :post
request = Net::HTTP::Post.new(url)
else
request = Net::HTTP::Get.new(url)
end
helper = OAuth::Client::Helper.new request, { :request_uri => url,
:consumer => consumer,
:token => access_token,
:scheme => 'header',
:signature_method => "HMAC-SHA1",
:method => meth
}
helper.header
end
Retrieving the contact list seems pretty straight forward. Our main issue was we had to change the http method when we use POST or GET. Be sure to set all the http methods correctly on the OAuth::Consumer, Net:HTTP, OAuth::Client and especially the GData::Client. Before you start creating your own code it would be wise to test your response by using Google’s OAuth Playground, which is a wonderful tool to use.
4. Using the GDAta API’s - Submitting an CSV/Excel to Google Docs
Submitting a CSV file to Google is almost as easy as retrievingyour contact list. We are going to use the same method for retrieving the authorization header. To post a file to Google Docs we also need to add two more header attributes, named “Content-Type” and “Slug”. In the body we’ll post the CSV file. The Content-Type is to specify what kind of file we’re going to upload. The Slug attribute will be the name of the file we want to show up in Google Docs.
def submit_csv_to_gdocs
email = 'user@email.com'
url = 'https://docs.google.com/feeds/default/private/full?xoauth_requestor_id=#{email}'
csv = StringIO.new
CSV::Writer.generate(csv, ',') do |line|
line << ["Name", "Value"]
end
csv.rewind
client.headers["Authorization"] = get_authorization_header(url, :post)
client.headers["Content-Type"] = "text/csv"
client.headers["Slug"] = "Your-Document-Name.csv"
feed = client.post(url, csv.read)
end
That is all. You can find more information on this in the Google Apps API Docs. If you have any question you can post them below. If I made any mistakes (which I probably have) please post them below. Have fun working with Google Apps!
Lessons Learned: Changing data storage
We’ve been in this internet startup game for a couple of years now. Every now and then we find ourselves in a situation that feels awfully familiar. Especially when we just broke something in the same way we’ve broken it before. Yes indeed… learning it the hard way. I thought it would be nice to write down a thing or two we have learned while running the Floorplanner business. The subjects of these stories are completely random and, as you might expect, tech orientated.
What?
In this case I’d like to talk a bit about changing the way we store our data. I don’t know if it’s just us, but for one reason or another, we changed it multiple times in the recent past. From storing it in a database, to file storage, to somewhere in the cloud, to a mixed setup. I can come up with two reasons why you want to switch to another storage solution. New technologies (or services like cloud services) or the notion that your initial assumptions aren’t correct anymore (because the world has changed while you weren’t paying attention). The thing that makes changing the way you store your data difficult is not the technical implementation itself, but rolling it out in a running system. The end user doesn’t care and shouldn’t notice anything. I think we came up with a simple way of releasing such an update, but first let me tell you why we changed it in the first place.Why?
In the beginning we stored everything in a relational database, users, floor plans, themes etc. We were familiar with working with a database so it was the easiest way to get the job done. Later on we opened up our service by building an API, which uses XML as it’s default format. When our API traffic grew, we noticed that a lot of recourses where used for parsing XML. Incoming requests where parsed to store the data from the XML in the database, and outgoing requests from database to XML. We didn’t actually use the data for anything else then storing it. To free up the resources used for parsing, we figured that it would be much more efficient to store (parts of) the XML as files on the server. Instead of parsing from and to XML, we’d only have to save and serve the XML from the files.How?
The implementation was a bit tricky because we had to touch every API method, but the really difficult part was rolling it out. Instead of switching everything in one big bang, we kept both storage solutions running. Whenever data was requested it was migrated from database to an XML file and served from the new file (if the XML file was already present, it would be served directly). New data was directly stored in files. This was supposed to be a temporary solution. We didn’t want to keep working with two storage solutions for the same data. However, after a while we noticed that the migration was taking too long. Instead of migrating ‘on demand’, we decided to write an script to migrate everything at once. That worked.Lesson learned
After changing our data storage for several times, we saw a pattern emerge. We never used the on demand method, because it just takes too long. Instead we did the following:- Copy the data from the old source to the new source
- Update the application so that it can work with both sources. If the new source isn’t available, it uses the old one.
- Sync the new data that was stored while copying the data and patching the app
- Update the application so that it only uses the new data source
- Sync again (just to be sure)
- Clean / remove the old source
Blender 2.5 alpha 2 - Collada export bug
Blender 2.5 is a great application to create 3D stuff. Best of all: its free! Even better: Blender 2.5 alpha 2 seems to have a much enhanced Collada importer/exporter. Blender versions before 2.5 used a python script for import and export. That script was okay, but failed on more complex scenes using rigs / animations. I believe Blender now uses Collada DOM or OpenCOLLADA. But after trying the Collada export I found a bug in Blender 2.5 alpha 2 r27226 which I filed in Blender’s bugtracker here (you need an account there). Seems export of “translation” goes sour. @collada asked me to describe the bug in public, so here is the bugreport I filed:
Blender 2.5 alpha 2 - 2.52.0 - r27226
Mac OSX 10.5.8
The .blend is a simple cube animation with 2 keys, translation and rotation only.
A typical exported translation channel looks like this (see attached collada):
<channel source="#Cube_location_Y-sampler" target="Cube/location"/>
According to the Collada 1.4.1 spec the @target atrribute here says:the locY-sampler targets *all* values of the element
with @sid="location".
In this case however we need to target only *one* value.
See "Address syntax" in Chapter 3 of the Collada 1.4.1 spec.
The targeted scene graph node looks like this (see attached collada)
<node id="Cube" type="NODE">
<translate sid="location">6.423927 -0.08766067028 1.473982</translate>
[...]
A channel targets a "sid" => see the @sid attribute on the node's translation element.
the @target property of the channel should be like:
Cube/location.X
Cube/location.Y
Cube/location.Z
or
Cube/location(0)
Cube/location(1)
Cube/location(2)
When only specifying Cube.location importers can't determine which value to target of the translation element (x, y or z).
I should note that target=”Cube/location” IS valid in theory (when the channel’s sampler specifies triplets of values). But in the case described the target is not valid afaik.
Here are the files: blender25_collada_bug
UPDATE : the bug has been fixed. Thank you Arystanbek Dyussenov! Guess you have to compile Blender 2.5 from source until the bug fix makes it into a release.