Chord frequency continuous change by Tone.js - web-audio-api

How to make chord base frequency continuous interactive change using tone.js for guitar?
Apparently using Tone.Sampler with API possibly named like "pitchBend" or "pitchShift" is a way to go, but I am not sure about actual choice. In other words, how to make "capo slider"?
I solved this problem for native window.AudioContext for set of oscillators (each oscillator for one guitar string ) updated with frequency from slider like:
oscillator.frequency.setValueAtTime( frequency, context.currentTime );
which changes the tone of all strings nicely while oscillators are still alive and playing.
but 1) the timbre is not exactly a guitar for default sine sound wave shape and 2) string sometimes interfere or reverberate making sound too non-natural.
Perhaps other than tone.js platform will do the trick?
Thank you.

I did more research and it looks now the native WebAudio API solves the problem, but not yet clear how reliably and precise. I did load guitar string samples and made slider changing their speed ration to retune. Below is the code:
<!DOCTYPE html>
<html>
<head>
<title>Continuous chord. Capo effect.</title>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<script>
window.onload = function () {
//==================================================
// //\\ configures sound parameters
//==================================================
// //\\ scale constants
/*
var logStep = Math.log( 2 ) / 12;
var semitone = Math.exp( logStep );
console.log( semitone );
var semi2 = semitone * semitone;
var semi4 = semi2 * semi2;
var semi7 = semi2 * semitone;
*/
// \\// scale constants
//E3, A3, C3#,
//165, 220, 277
var initSpeed = 1;
var clipStart = 0; //0.65; //0.7;
var clipLenOriginal = 3.2; //0.43;
var doLoop = false; //true;
//speed raise
//if it is expected only one url for all tones in chord,
//adjust here speeds to generate different tones according to
//frequency ratios,
var clipLenSpeeds = [
//minor
/*
var smR0 = 1;
var smR1 = 262/220;
var smR2 = 330/220;
*/
//major
1,
1, // 277/220, //220/165
1, // 330/220, //277/165
];
var clipLengs = clipLenSpeeds.map( sp => clipLenOriginal*sp );
var clipLenSpeeds = clipLenSpeeds.map( sp => initSpeed*sp );
var dataPath = 'data';
//data are from this path:
//var dataPath = 'https://github.com/nbrosowsky/tonejs-instruments/tree/master/samples/guitar-acoustic';
var urls = [
dataPath + '/' + 'A3.wav',
dataPath + '/' + 'E3.wav',
dataPath + '/' + 'Cs3.wav',
];
//var fname = 'second-line-captured.mkv.mp3';
//var urls = clipLenSpeeds.map( el => fname );
//==================================================
// \\// configures sound parameters
//==================================================
//==================================================
// //\\ loads and prepares sourceNodes
//==================================================
let audioContext = new AudioContext();
var sourceNodes = [];
var audioBuffers = [];
Promise.all(
clipLenSpeeds.map( ( el, ix ) =>
getSample( ix )
)
).then(
( values ) => {
//console.log( 'values from last promise-then:', values );
button.removeAttribute( 'disabled' );
},
).catch( (err) => {
console.log( err );
});
//==================================================
// \\// loads and prepares sourceNodes
//==================================================
//==================================================
// //\\ GUI
//==================================================
const button = document.querySelector( '.run' );
const playbackControl = document.querySelector( '.playback-rate-control' );
const playbackValue = document.querySelector( '.playback-rate-value' );
button.setAttribute( 'disabled', 'disabled' );
button.addEventListener( 'click', () => {
audioBuffers.forEach( (val,vix) =>
startLoopByIx( vix )
);
});
playbackControl.oninput = function() {
playbackValue.innerHTML = playbackControl.value;
sourceNodes.forEach( (sn,i) => {
sn.playbackRate.value = clipLenSpeeds[i] * playbackControl.value;
});
}
//==================================================
// \\// GUI
//==================================================
return;
///part of the load and initation of audio buffer,
///returns promise from the last ".then"
function getSample( ix )
{
return fetch( urls[ ix ] )
.then( response => response.arrayBuffer() )
.then( arrayBuffer => audioContext.decodeAudioData( arrayBuffer ) )
.then( audioBuffer => {
audioBuffers[ ix ] = audioBuffer;
//if uncommented, this returned value populates
//values in Promise.all.then first param,
//return audioBuffer;
})
;
}
///fires up loop belonging to sourceNode[ ix ],
///options: either doLoop or one-time play,
function startLoopByIx( ix )
{
var audioBuffer = audioBuffers[ ix ];
var clipLen = clipLengs[ ix ];
var rate = clipLenSpeeds[ ix ];
var pan = 0; //stereo channels balance
let sourceNode = audioContext.createBufferSource();
let pannerNode = audioContext.createStereoPanner();
sourceNode.buffer = audioBuffer;
sourceNode.loop = doLoop;
sourceNode.loopStart = clipStart;
sourceNode.loopEnd = clipStart + clipLen;
sourceNode.playbackRate.value = rate; //does retuning
pannerNode.pan.value = pan;
sourceNode.connect( pannerNode );
pannerNode.connect( audioContext.destination );
if( doLoop ) {
sourceNode.start( 0, clipStart, );
} else {
sourceNode.start( 0, clipStart, clipLen );
}
sourceNodes[ ix ] = sourceNode;
}
};
</script>
</head>
<body>
<button class="run">Run samples</button><br>
<h2>Set playback rate</h2>
<input class="playback-rate-control" type="range" min="0.25" max="3" step="0.05" value="1">
<span class="playback-rate-value">1.0</span>
</body>
</html>

Related

How to display pie slice data and tooltip together using chart.js

Can we display data in pie chart slice and also tolltip like the above image using chart.js?
Updated:
Here is my code in php page.
printf( '<table>' );
echo '<tr><td style="text-align: right;"><canvas id="pie-canvas-'
. $canvasId
. '" width=256px height=257px ></canvas></td><td style="text-align: left;width:360px;" id="legend" class="chart-legend"></td></tr>';
echo '<script type="text/javascript">drawPie('
. $canvasId
. ', '
. $data
.', '
. $legend
. ');</script>';
printf( '</table>' );
printf( '<script type="text/javascript" src="extlib/Chart.min.js"></script>' );
printf( '<script type="text/javascript" src="extlib/jquery-min.js"></script>' );
printf( '<script type="text/javascript">' );
?>
function drawPie( canvasId, data, legend )
{
//pie chart for machine status
var canvas = document.getElementById( "pie-canvas-" + canvasId );
var ctx = canvas.getContext( "2d" );
var midX = canvas.width/2;
var midY = canvas.height/2;
var piedata = [];
$.each(data,function(i,val){
piedata.push({value:val.hostStatusCount,color:val.color,label:val.status});
});
Chart.types.Pie.extend({
name: "PieAlt",
draw: function(){
Chart.types.Pie.prototype.draw.apply(this, arguments);
drawSegmentValues(this)
}
});
var myPieChart = new Chart(ctx).PieAlt(piedata, {
showTooltips: true,
tooltipTemplate: "<%= Math.round(circumference / 6.283 * 100) %>%"
});
var radius = myPieChart.outerRadius;
function drawSegmentValues(myPieChart)
{
//displays segements(number of machines) for each slice of pie in percentage
var length = myPieChart.segments.length;
var totalValue = 0;
for ( var i=0; i < length; i++ )
{
totalValue +=myPieChart.segments[i].value;
}
for( var i=0; i < length; i++ )
{
ctx.fillStyle="black";
var textSize = canvas.width/15;
ctx.font= textSize+"px Verdana";
// Get needed variables
var value = Math.round( ( ( myPieChart.segments[i].value ) / totalValue ) * 100 );
var startAngle = myPieChart.segments[i].startAngle;
var endAngle = myPieChart.segments[i].endAngle;
var middleAngle = startAngle + ( ( endAngle - startAngle ) / 2 );
// Compute text location
var posX = ( radius /1.5 ) * Math.cos( middleAngle ) + midX;
var posY = ( radius/1.5 ) * Math.sin( middleAngle ) + midY;
// Text offside by middle
var w_offset = ctx.measureText( value ).width / 2;
var h_offset = textSize / 4;
ctx.fillText( value+"%", posX - w_offset, posY + h_offset );
}
}
//legend for status
if( legend )
document.getElementById("legend").innerHTML = myPieChart.generateLegend();
}
<?
When mouse over the data in pie slice moved from its position.
How to solve this?
Extend the chart and move your drawSegmentValues to inside the draw override, like so
Chart.types.Pie.extend({
name: "PieAlt",
draw: function(){
Chart.types.Pie.prototype.draw.apply(this, arguments);
drawSegmentValues(this)
}
});
then use PieAlt
var myPieChart = new Chart(ctx).PieAlt(data, {
showTooltips: true,
tooltipTemplate: "<%= Math.round(circumference / 6.283 * 100) %>%"
});
and modify the drawSegmentValues function slightly
function drawSegmentValues(myPieChart)
{
var radius = myPieChart.outerRadius
...
Update
If you have a problem with the labels moving set the context textAlign and textBaseline properties, like so
...
ctx.font = textSize + "px Verdana";
ctx.textAlign = "start";
ctx.textBaseline = "bottom";
...

jquery.jeditable.ajaxupload.js how to send file?

maybe somebody can to explain me why I can't send file, maybe needed additional script or something els?
/*
* Ajaxupload for Jeditable
*
* Copyright (c) 2008-2009 Mika Tuupola
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/mit-license.php
*
* Depends on Ajax fileupload jQuery plugin by PHPLetter guys:
* http://www.phpletter.com/Our-Projects/AjaxFileUpload/
*
* Project home:
* http://www.appelsiini.net/projects/jeditable
*
* Revision: $Id$
*
*/
$.editable.addInputType('ajaxupload', {
/* create input element */
element : function(settings) {
settings.onblur = 'ignore';
var input = $('<input type="file" id="upload" name="upload" />');
$(this).append(input);
return(input);
},
content : function(string, settings, original) {
/* do nothing */
},
plugin : function(settings, original) {
var form = this;
form.attr("enctype", "multipart/form-data");
$("button:submit", form).bind('click', function() {
//$(".message").show();
// Modification to include original id and submitdata in target's querystring
var queryString;
if ($.isFunction(settings.submitdata)) {
queryString = jQuery.param(settings.submitdata.apply(self, [self.revert, settings]));
} else {
queryString = jQuery.param(settings.submitdata);
}
if (settings.target.indexOf('?') < 0) {
queryString = '?' + settings.id + '=' + $(original).attr('id') + '&' + queryString;
} else {
queryString = '&' + settings.id + '=' + $(original).attr('id') + '&' + queryString;
}
settings.target += queryString;
// End modification
$.ajaxFileUpload({
url: settings.target,
secureuri:false,
// Add the following line
data : settings.submitdata,
fileElementId: 'upload',
dataType: 'html',
success: function (data, status) {
$(original).html(data);
original.editing = false;
},
error: function (data, status, e) {
alert(e);
}
});
return(false);
});
}
});
$(\".ajaxupload\").editable('/?action=upload_profile_photo', {
indicator : '<i class=\"fa fa-spinner fa-pulse\"></i>',
type : 'ajaxupload',
submit : 'Upload',
cancel : 'Cancel',
tooltip : \"Click to upload...\"
});
<p class=\"ajaxupload\" id=\"profile_photo\"><img class=\"img-thumbnail img-responsive\" src=\"http://www.in-elite.com/?action=no_image_text\" alt=\"\"></p>
how result its passed only
print_r($._GET)Array ( [action] => upload_profile_photo [id] =>
profile_photo ) print_r($._POST)Array ( [value] => 20150501_153530.jpg
[id] => profile_photo )
And question so, How to send file?
Ux I found the problem ;-)
I'm forgot to add one js file called jquery.ajaxfileupload.js
<script type=\"text/javascript\" src=\"/includes/JS/jquery.ajaxfileupload.js\" charset=\"utf-8\"></script>
And content of this jquery.ajaxfileupload.js file is
jQuery.extend({
createUploadIframe: function(id, uri)
{
//create frame
var frameId = 'jUploadFrame' + id;
if(window.ActiveXObject) {
var io = document.createElement('<iframe id="' + frameId + '" name="' + frameId + '" />');
if(typeof uri== 'boolean'){
io.src = 'javascript:false';
}
else if(typeof uri== 'string'){
io.src = uri;
}
}
else {
var io = document.createElement('iframe');
io.id = frameId;
io.name = frameId;
}
io.style.position = 'absolute';
io.style.top = '-1000px';
io.style.left = '-1000px';
document.body.appendChild(io);
return io
},
createUploadForm: function(id, fileElementId)
{
//create form
var formId = 'jUploadForm' + id;
var fileId = 'jUploadFile' + id;
var form = $('<form action="" method="POST" name="' + formId + '" id="' + formId + '" enctype="multipart/form-data"></form>');
var oldElement = $('#' + fileElementId);
var newElement = $(oldElement).clone();
$(oldElement).attr('id', fileId);
$(oldElement).before(newElement);
$(oldElement).appendTo(form);
//set attributes
$(form).css('position', 'absolute');
$(form).css('top', '-1200px');
$(form).css('left', '-1200px');
$(form).appendTo('body');
return form;
},
ajaxFileUpload: function(s) {
// TODO introduce global settings, allowing the client to modify them for all requests, not only timeout
s = jQuery.extend({}, jQuery.ajaxSettings, s);
var id = new Date().getTime()
var form = jQuery.createUploadForm(id, s.fileElementId);
var io = jQuery.createUploadIframe(id, s.secureuri);
var frameId = 'jUploadFrame' + id;
var formId = 'jUploadForm' + id;
// Watch for a new set of requests
if ( s.global && ! jQuery.active++ )
{
jQuery.event.trigger( "ajaxStart" );
}
var requestDone = false;
// Create the request object
var xml = {}
if ( s.global )
jQuery.event.trigger("ajaxSend", [xml, s]);
// Wait for a response to come back
var uploadCallback = function(isTimeout)
{
var io = document.getElementById(frameId);
try
{
if(io.contentWindow)
{
xml.responseText = io.contentWindow.document.body?io.contentWindow.document.body.innerHTML:null;
xml.responseXML = io.contentWindow.document.XMLDocument?io.contentWindow.document.XMLDocument:io.contentWindow.document;
}else if(io.contentDocument)
{
xml.responseText = io.contentDocument.document.body?io.contentDocument.document.body.innerHTML:null;
xml.responseXML = io.contentDocument.document.XMLDocument?io.contentDocument.document.XMLDocument:io.contentDocument.document;
}
}catch(e)
{
jQuery.handleError(s, xml, null, e);
}
if ( xml || isTimeout == "timeout")
{
requestDone = true;
var status;
try {
status = isTimeout != "timeout" ? "success" : "error";
// Make sure that the request was successful or notmodified
if ( status != "error" )
{
// process the data (runs the xml through httpData regardless of callback)
var data = jQuery.uploadHttpData( xml, s.dataType );
// If a local callback was specified, fire it and pass it the data
if ( s.success )
s.success( data, status );
// Fire the global callback
if( s.global )
jQuery.event.trigger( "ajaxSuccess", [xml, s] );
} else
jQuery.handleError(s, xml, status);
} catch(e)
{
status = "error";
jQuery.handleError(s, xml, status, e);
}
// The request was completed
if( s.global )
jQuery.event.trigger( "ajaxComplete", [xml, s] );
// Handle the global AJAX counter
if ( s.global && ! --jQuery.active )
jQuery.event.trigger( "ajaxStop" );
// Process result
if ( s.complete )
s.complete(xml, status);
jQuery(io).unbind()
setTimeout(function()
{ try
{
$(io).remove();
$(form).remove();
} catch(e)
{
jQuery.handleError(s, xml, null, e);
}
}, 100)
xml = null
}
}
// Timeout checker
if ( s.timeout > 0 )
{
setTimeout(function(){
// Check to see if the request is still happening
if( !requestDone ) uploadCallback( "timeout" );
}, s.timeout);
}
try
{
// var io = $('#' + frameId);
var form = $('#' + formId);
$(form).attr('action', s.url);
$(form).attr('method', 'POST');
$(form).attr('target', frameId);
if(form.encoding)
{
form.encoding = 'multipart/form-data';
}
else
{
form.enctype = 'multipart/form-data';
}
$(form).submit();
} catch(e)
{
jQuery.handleError(s, xml, null, e);
}
if(window.attachEvent){
document.getElementById(frameId).attachEvent('onload', uploadCallback);
}
else{
document.getElementById(frameId).addEventListener('load', uploadCallback, false);
}
return {abort: function () {}};
},
uploadHttpData: function( r, type ) {
var data = !type;
data = type == "xml" || data ? r.responseXML : r.responseText;
// If the type is "script", eval it in global context
if ( type == "script" )
jQuery.globalEval( data );
// Get the JavaScript object, if JSON is used.
if ( type == "json" )
eval( "data = " + data );
// evaluate scripts within html
if ( type == "html" )
jQuery("<div>").html(data);
//jQuery("<div>").html(data).evalScripts();
//alert($('param', data).each(function(){alert($(this).attr('value'));}));
return data;
}
})
Now everything ok and file are uploaded successfully.... Just needed to modified php script for which type of the data you need to upload

jsPDF multi page PDF with HTML renderer

I am using jsPDF in my site to generate PDFs. But now I have multiple DIVs to print in a single PDF. which may take 2 to 3 pages.
For example:
<div id="part1">
content
</div>
<div id="part2">
content
</div>
<div id="part2">
content
</div>
my JS code
This works but not as I expected, It add a part of the content(which cannot be included in more than one page).
It removes html tags like br, h1 etc.
function formtoPDF() {
jsPDF.API.mymethod = function() {
// 'this' will be ref to internal API object. see jsPDF source
// , so you can refer to built-in methods like so:
// this.line(....)
// this.text(....)
};
var doc = new jsPDF();
doc.mymethod();
var pdfPart1 = jQuery('#genPDFpart1');
var pdfPart2 = jQuery(".ltinerary");
var pdfPart3 = jQuery("#domElementHTML");
var specialElementHandlers = {
'#loadVar': function(element, renderer) {
return true;
}
};
doc.fromHTML(pdfPart1.html() + pdfPart3.html() + pdfPart3.html(), 15, 15, {
'width': 170,
'elementHandlers': specialElementHandlers
});
doc.output('save', 'Download.pdf');
}
What's the solution for this?
I have the same working issue. Searching in MrRio github I found this: https://github.com/MrRio/jsPDF/issues/101
Basically, you have to check the actual page size always before adding new content
doc = new jsPdf();
...
pageHeight= doc.internal.pageSize.height;
// Before adding new content
y = 500 // Height position of new content
if (y >= pageHeight)
{
doc.addPage();
y = 0 // Restart height position
}
doc.text(x, y, "value");
here's an example using html2canvas & jspdf, although how you generate the canvas doesn't matter--we're just going to use the height of that as the breakpoint on a for loop, in which a new page is created and content added to it.
after the for loop, the pdf is saved.
function makePDF() {
var quotes = document.getElementById('container-fluid');
html2canvas(quotes).then((canvas) => {
//! MAKE YOUR PDF
var pdf = new jsPDF('p', 'pt', 'letter');
for (var i = 0; i <= quotes.clientHeight/980; i++) {
//! This is all just html2canvas stuff
var srcImg = canvas;
var sX = 0;
var sY = 980*i; // start 980 pixels down for every new page
var sWidth = 900;
var sHeight = 980;
var dX = 0;
var dY = 0;
var dWidth = 900;
var dHeight = 980;
window.onePageCanvas = document.createElement("canvas");
onePageCanvas.setAttribute('width', 900);
onePageCanvas.setAttribute('height', 980);
var ctx = onePageCanvas.getContext('2d');
// details on this usage of this function:
// https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Using_images#Slicing
ctx.drawImage(srcImg,sX,sY,sWidth,sHeight,dX,dY,dWidth,dHeight);
// document.body.appendChild(canvas);
var canvasDataURL = onePageCanvas.toDataURL("image/png", 1.0);
var width = onePageCanvas.width;
var height = onePageCanvas.clientHeight;
//! If we're on anything other than the first page,
// add another page
if (i > 0) {
pdf.addPage(612, 791); //8.5" x 11" in pts (in*72)
}
//! now we declare that we're working on that page
pdf.setPage(i+1);
//! now we add content to that page!
pdf.addImage(canvasDataURL, 'PNG', 20, 40, (width*.62), (height*.62));
}
//! after the for loop is finished running, we save the pdf.
pdf.save('Test.pdf');
});
}
I found the solution on this page: https://github.com/MrRio/jsPDF/issues/434
From the user: wangzhixuan
I copy the solution here:
// suppose your picture is already in a canvas
var imgData = canvas.toDataURL('image/png');
/*
Here are the numbers (paper width and height) that I found to work.
It still creates a little overlap part between the pages, but good enough for me.
if you can find an official number from jsPDF, use them.
*/
var imgWidth = 210;
var pageHeight = 295;
var imgHeight = canvas.height * imgWidth / canvas.width;
var heightLeft = imgHeight;
var doc = new jsPDF('p', 'mm');
var position = 0;
doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
while (heightLeft >= 0) {
position = heightLeft - imgHeight;
doc.addPage();
doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
doc.save( 'file.pdf');
var doc = new jsPDF('p', 'mm');
var imgData = canvas.toDataURL('image/png');
var pageHeight= doc.internal.pageSize.getHeight();
var pageWidth= doc.internal.pageSize.getWidth();
var imgheight = $('divName').height() * 25.4 / 96; //px to mm
var pagecount = Math.ceil(imgheight / pageHeight);
/* add initial page */
doc.addPage('l','mm','a4');
doc.addImage(imgData, 'PNG', 2, 0, pageWidth-4, 0);
/* add extra pages if the div size is larger than a a4 size */
if (pagecount > 0) {
var j = 1;
while (j != pagecount) {
doc.addPage('l','mm','a4');
doc.addImage(imgData, 'PNG', 2, -(j * pageHeight), pageWidth-4, 0);
j++;
}
}
You can use html2canvas plugin and jsPDF both. Process order:
html to png & png to pdf
Example code:
jQuery('#part1').html2canvas({
onrendered: function( canvas ) {
var img1 = canvas.toDataURL('image/png');
}
});
jQuery('#part2').html2canvas({
onrendered: function( canvas ) {
var img2 = canvas.toDataURL('image/png');
}
});
jQuery('#part3').html2canvas({
onrendered: function( canvas ) {
var img3 = canvas.toDataURL('image/png');
}
});
var doc = new jsPDF('p', 'mm');
doc.addImage( img1, 'PNG', 0, 0, 210, 297); // A4 sizes
doc.addImage( img2, 'PNG', 0, 90, 210, 297); // img1 and img2 on first page
doc.addPage();
doc.addImage( img3, 'PNG', 0, 0, 210, 297); // img3 on second page
doc.save("file.pdf");
$( document ).ready(function() {
$('#cmd').click(function() {
var options = {
pagesplit: true //include this in your code
};
var pdf = new jsPDF('p', 'pt', 'a4');
pdf.addHTML($("#pdfContent"), 15, 15, options, function() {
pdf.save('Menu.pdf');
});
});
});
This is my first post which support only a single page http://www.techumber.com/html-to-pdf-conversion-using-javascript/
Now, the second one will support the multiple pages.
http://www.techumber.com/how-to-convert-html-to-pdf-using-javascript-multipage/
Below is my code but the problem is that the document doesn't split to display the other part of the document in a new page.
Please improve this code.
<script type='text/javascript'>
$(document).on("click", "#btnExportToPDF", function () {
var table1 =
tableToJson($('#table1')[0]),
cellWidth =42,
rowCount = 0,
cellContents,
leftMargin = 2,
topMargin = 12,
topMarginTable =5,
headerRowHeight = 13,
rowHeight = 12,
l = {
orientation: 'p',
unit: 'mm',
format: 'a3',
compress: true,
fontSize: 11,
lineHeight: 1,
autoSize: false,
printHeaders: true
};
var doc = new jsPDF(l,'pt', 'letter');
doc.setProperties({
title: 'Test PDF Document',
subject: 'This is the subject',
author: 'author',
keywords: 'generated, javascript, web 2.0, ajax',
creator: 'author'
});
doc.cellInitialize();
$.each(table1, function (i, row)
{
rowCount++;
$.each(row, function (j, cellContent) {
if (rowCount == 1) {
doc.margins = 1;
doc.setFont("Times New Roman");
doc.setFontType("bold");
doc.setFontSize(11);
doc.cell(leftMargin, topMargin, cellWidth, headerRowHeight, cellContent, i)
}
else if (rowCount == 2) {
doc.margins = 1;
doc.setFont("Times ");
doc.setFontType("normal");
// or for normal font type use ------ doc.setFontType("normal");
doc.setFontSize(11);
doc.cell(leftMargin, topMargin, cellWidth, rowHeight, cellContent, i);
}
else {
doc.margins = 1;
doc.setFont("Times ");
doc.setFontType("normal ");
doc.setFontSize(11);
doc.cell(leftMargin, topMargin, cellWidth, rowHeight, cellContent, i);
// 1st=left margin 2nd parameter=top margin, 3rd=row cell width 4th=Row height
}
})
})
doc.save('sample Report.pdf');
});
function tableToJson(table) {
var data = [];
// first row needs to be headers
var headers = [];
for (var i=0; i<table.rows[0].cells.length; i++) {
headers[i] = table.rows[0].cells[i].innerHTML.toLowerCase().replace(/ /gi,'');
}
// go through cells
for (var i=1; i<table.rows.length; i++) {
var tableRow = table.rows[i];
var rowData = {};
for (var j=0; j<tableRow.cells.length; j++) {
rowData[ headers[j] ] = tableRow.cells[j].innerHTML;
}
data.push(rowData);
}
return data;
}
</script>
Automatically not split data to multi pages. You may split manually.
If your ( rowCount * rowHeight ) > 420mm ( A3 Height in mm ) add new page function. ( Sorry I can't edit your code without run )
After add new page leftMargin, topMargin = 0; ( start over )
I added sample code with yours. I hope it's right.
else {
doc.margins = 1;
doc.setFont("Times ");
doc.setFontType("normal ");
doc.setFontSize(11);
if ( rowCount * rowHeight > 420 ) {
doc.addPage();
rowCount = 3; // skip 1 and 2 above
} else {
// now rowcount = 3 ( top of new page for 3 )
// j is your x axis cell index ( j start from 0 on $.each function ) or you can add cellCount like rowCount and replace with
// rowcount is your y axis cell index
left = ( ( j ) * ( cellWidth + leftMargin );
top = ( ( rowcount - 3 ) * ( rowHeight + topMargin );
doc.cell( leftMargin, top, cellWidth, rowHeight, cellContent, i);
// 1st=left margin 2nd parameter=top margin, 3rd=row cell width 4th=Row height
}
}
You can convert html directly to pdf lossless. Youtube video for html => pdf example
html2canvas(element[0], {
onrendered: function (canvas) {
pages = Math.ceil(element[0].clientHeight / 1450);
for (i = 0; i <= pages; i += 1) {
if (i > 0) {
pdf.addPage();
}
srcImg = canvas;
sX = 0;
sY = 1450 * i;
sWidth = 1100;
sHeight = 1450;
dX = 0;
dY = 0;
dWidth = 1100;
dHeight = 1450;
window.onePageCanvas = document.createElement("canvas");
onePageCanvas.setAttribute('width', 1100);
onePageCanvas.setAttribute('height', 1450);
ctx = onePageCanvas.getContext('2d');
ctx.drawImage(srcImg, sX, sY, sWidth, sHeight, dX, dY, dWidth, dHeight);
canvasDataURL = onePageCanvas.toDataURL("image/png");
width = onePageCanvas.width;
height = onePageCanvas.clientHeight;
pdf.setPage(i + 1);
pdf.addImage(canvasDataURL, 'PNG', 35, 30, (width * 0.5), (height * 0.5));
}
pdf.save('testfilename.pdf');
}
});
var a = 0;
var d;
var increment;
for(n in array){
d = a++;
if(n % 6 === 0 && n != 0){
doc.addPage();
a = 1;
d = 0;
}
increment = d == 0 ? 10 : 50;
size = (d * increment) <= 0 ? 10 : d * increment;
doc.text(array[n], 10, size);
}

Mouse hover effect is not working in Three.js

I need help with this code about Three.js. I do not know why it does not work, since everything is correct and copied from other codes that do work. The problem is that no Mouse Hovering effect works.
<html>
<head>
<title>NUEVO</title>
<style>canvas { width: 100%; height: 100% }</style>
</head>
<body>
<script src="js/libs/Tween.js"></script>
<script src="js/libs/stats.min.js"></script>
<script src="https://raw.github.com/mrdoob/three.js/master/build/three.js"></script>
<script>
var scene,camera,rendered,projector;
var mouseX, mouseY, stats, container;
var objects=[];
var INTERSECTED;
var theta = 0;
init();
animate();
function init(){
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 1, 20000);
camera.position.set( 0, 150, 400 );
camera.lookAt(scene.position);
renderer = new THREE.CanvasRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
container = document.createElement( 'div' );
document.body.appendChild( container );
container.appendChild( renderer.domElement );
projector = new THREE.Projector();
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.bottom = '0px';
stats.domElement.style.zIndex = 100;
container.appendChild( stats.domElement );
for (var i=0; i<3; i++)
{
var geometry = new THREE.CubeGeometry(40,40,40);
var material = new THREE.MeshBasicMaterial({color: 0x00ff00});
var cube = new THREE.Mesh(geometry, material);
cube.position.x = (i*200) -200
objects.push(cube);
scene.add(cube);
}
window.addEventListener( 'resize', onWindowResize, false );
}
function onDocumentMouseDown( event )
{
event.preventDefault();
mouseX = ( event.clientX / window.innerWidth ) * 2 - 1;
mouseY = - ( event.clientY / window.innerHeight ) * 2 + 1;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate()
{
requestAnimationFrame(animate);
render();
update();
}
function update()
{
var vector = new THREE.Vector3( mouseX, mouseY, 1 );
projector.unprojectVector( vector, camera );
var ray = new THREE.Ray( camera.position, vector.subSelf( camera.position ).normalize() );
var intersects = ray.intersectObjects( scene.children );
if ( intersects.length > 0 ) {
if ( intersects[ 0 ].object != INTERSECTED )
{
if ( INTERSECTED )
INTERSECTED.material.color.setHex( INTERSECTED.currentHex );
// store reference to closest object as current intersection object
INTERSECTED = intersects[ 0 ].object;
// store color of closest object (for later restoration)
INTERSECTED.currentHex = INTERSECTED.material.color.getHex();
// set a new color for closest object
INTERSECTED.material.color.setHex( 0xffff00 );
} else {// there are no intersections
// restore previous intersection object (if it exists) to its original color
if ( INTERSECTED )
INTERSECTED.material.color.setHex( INTERSECTED.currentHex );
// remove previous intersection object reference
// by setting current intersection object to "nothing"
INTERSECTED = null;
}
/* for(var i=0; i<3; i++)
{
objects[i].rotation.y += 0.05;
}*/
}
}
function render()
{
//theta += 0.4;
camera.position.x = 300 * Math.sin( theta * Math.PI / 360 );
camera.position.y = 300 * Math.sin( theta * Math.PI / 360 );
camera.position.z = 300 * Math.cos( theta * Math.PI / 360 );
camera.lookAt( scene.position );
renderer.render(scene, camera);
}
</script>
</body>
</html>
This is what You want:
http://jsfiddle.net/Lx6nE/4/
What was wrong:
The link to Three.js should be towards the minified source.
The braces should be indented right becuase of flickering
The mousedown event should be captured.
The code:
var scene,camera,rendered,projector;
var mouseX, mouseY, stats, container;
var objects=[];
var INTERSECTED;
var theta = 0;
init();
animate();
function init(){
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 1, 20000);
camera.position.set( 0, 150, 400 );
camera.lookAt(scene.position);
renderer = new THREE.CanvasRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
container = document.createElement( 'div' );
document.body.appendChild( container );
container.appendChild( renderer.domElement );
projector = new THREE.Projector();
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.bottom = '0px';
stats.domElement.style.zIndex = 100;
container.appendChild( stats.domElement );
for (var i=0; i<3; i++)
{
var geometry = new THREE.CubeGeometry(40,40,40);
var material = new THREE.MeshBasicMaterial({color: 0x00ff00});
var cube = new THREE.Mesh(geometry, material);
cube.position.x = (i*50) -60
objects.push(cube);
scene.add(cube);
}
window.addEventListener( 'resize', onWindowResize, false );
window.addEventListener( 'mousedown', onDocumentMouseDown, false );
}
function onDocumentMouseDown( event )
{
event.preventDefault();
mouseX = ( event.clientX / window.innerWidth ) * 2 - 1;
mouseY = - ( event.clientY / window.innerHeight ) * 2 + 1;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate()
{
requestAnimationFrame(animate);
render();
update();
}
function update()
{
var vector = new THREE.Vector3( mouseX, mouseY, 1 );
projector.unprojectVector( vector, camera );
var ray = new THREE.Ray( camera.position, vector.subSelf( camera.position ).normalize() );
var intersects = ray.intersectObjects( scene.children );
if ( intersects.length > 0 ) {
if ( intersects[ 0 ].object != INTERSECTED )
{
if ( INTERSECTED )
INTERSECTED.material.color.setHex( INTERSECTED.currentHex );
// store reference to closest object as current intersection object
INTERSECTED = intersects[ 0 ].object;
// store color of closest object (for later restoration)
INTERSECTED.currentHex = INTERSECTED.material.color.getHex();
// set a new color for closest object
INTERSECTED.material.color.setHex( 0xffff00 );
}
} else {// there are no intersections
// restore previous intersection object (if it exists) to its original color
if ( INTERSECTED )
INTERSECTED.material.color.setHex( INTERSECTED.currentHex );
// remove previous intersection object reference
// by setting current intersection object to "nothing"
INTERSECTED = null;
/* for(var i=0; i<3; i++)
{
objects[i].rotation.y += 0.05;
}*/
}
}
function render()
{
//theta += 0.4;
camera.position.x = 300 * Math.sin( theta * Math.PI / 360 );
camera.position.y = 300 * Math.sin( theta * Math.PI / 360 );
camera.position.z = 300 * Math.cos( theta * Math.PI / 360 );
camera.lookAt( scene.position );
renderer.render(scene, camera);
}
​

flickr photobar - get description

I implemented this jquery http://tympanus.net/Tutorials/FlickrPhotobarGallery/ and I can capture setname and title but I need to get description too, so I would like to know if this is possible.
Hope someone can help me, thanks in advance!
$(function() {
var api_key = '######';
var user_id = '####';
/*
use:
Square,Thumbnail,Small,Medium or Original for the large image size you want to load!
*/
var large_image_size = 'Medium';
/*
the current Set id / the current Photo id
*/
var photo_set_id,photo_id;
var current = -1;
var continueNavigation = false;
/*
flickr API Call to get List of Sets
*/
var sets_service = 'http://api.flickr.com/services/rest/?&method=flickr.photosets.getList' + '&api_key=' + api_key;
var sets_url = sets_service + '&user_id=' + user_id + '&format=json&jsoncallback=?';
/*
flickr API Call to get List of Photos from a Set
*/
var photos_service = 'http://api.flickr.com/services/rest/?&method=flickr.photosets.getPhotos' + '&api_key=' + api_key;
/*
flickr API Call to get List of Sizes of a Photo
*/
var large_photo_service = 'http://api.flickr.com/services/rest/?&method=flickr.photos.getSizes' + '&api_key=' + api_key;
/*
elements caching...
*/
var $setsContainer = $('#sets').find('ul');
var $photosContainer = $('#images').find('ul');
var $photopreview = $('#flickr_photopreview');
var $flickrOverlay = $('#flickr_overlay');
var $loadingStatus = $('#flickr_toggle').find('.loading_small');
var ul_width,spacefit,fit;
add: LoadSets();
/* start: open Flickr Photostream */
$('#flickr_toggle').toggle(function(){
$('#photobar').stop().animate({'bottom':'0px'},200,function(){
if($setsContainer.is(':empty')){
/*
if sets not loaded, load them
*/
LoadSets();
}
});
},function(){
/*
minimize the main bar, and minimize the photos bar.
next time we maximize, the view will be on the sets
*/
$('#photobar').stop().animate({'bottom':'-96px'},200,function(){
$('#images').css('bottom','-125px');
});
});
/*
Loads the User Photo Sets
*/
function LoadSets(){
$loadingStatus.css('visibility','visible');
$.getJSON(sets_url,function(data){
if(data.stat != 'fail') {
var sets_count = data.photosets.photoset.length;
/*
adapt ul width based on number of results
*/
ul_width = sets_count * 105 + 105;
$setsContainer.css('width',ul_width + 'px');
for(var i = 0; i < sets_count; ++i){
var photoset = data.photosets.photoset[i];
var primary = photoset.primary;
var secret = photoset.secret;
var server = photoset.server;
var farm = photoset.farm;
/*
source for the small thumbnail
*/
var photoUrl = 'http://farm'+farm+'.static.flickr.com/'+server+'/'+primary+'_'+secret+'_s.jpg';
var $elem = $('<li />');
var $link = $('<a class="toLoad" href="#" />');
/*
save the info of the set in the li element,
we will use it later
*/
$link.data({
'primary' :primary,
'secret' :secret,
'server' :server,
'farm' :farm,
'photoUrl' :photoUrl,
'setName' :photoset.title._content,
'id' :photoset.id
});
$setsContainer.append($elem.append($link));
$link.bind('click',function(e){
var $this = $(this);
/*
save the current Set id in the photo_set_id variable
and load the photos of that Set
*/
$('#images').stop().animate({'bottom':'0px'},200);
if(photo_set_id!=$this.data('id')){
photo_set_id = $this.data('id');
$('#setName').html($this.data('setName'));
LoadPhotos();
}
e.preventDefault();
});
}
/*
now we load the images
(the ones in the viewport)
*/
LoadSetsImages();
}
});
}
/*
loads the images of the sets that are in the viewport
*/
function LoadSetsImages(){
var toLoad = $('.toLoad:in-viewport').size();
if(toLoad > 0)
$loadingStatus.css('visibility','visible');
var images_loaded = 0;
$('.toLoad:in-viewport').each(function(i){
var $space = $setsContainer.find('.toLoad:first');
var $img = $('<img style="display:none;" />').load(function(){
++images_loaded;
if(images_loaded == toLoad){
$loadingStatus.css('visibility','hidden');
$setsContainer.find('img').fadeIn();
}
}).error(function(){
//TODO
++images_loaded;
if(images_loaded == toLoad){
$loadingStatus.css('visibility','hidden');
$setsContainer.find('img').fadeIn();
}
}).attr('src',$space.data('photoUrl')).attr('alt',$space.data('id'));
var $set_name = $('<span />',{'html':$space.data('setName')});
$space.append($set_name).append($img).removeClass('toLoad');
});
}
/*
Loads the Set's Photos
*/
function LoadPhotos(){
$photosContainer.empty();
$loadingStatus.css('visibility','visible');
var photos_url = photos_service + '&photoset_id=' + photo_set_id + '&media=photos&format=json&jsoncallback=?';
$.getJSON(photos_url,function(data){
if(data.stat != 'fail') {
var photo_count = data.photoset.photo.length;
/*
adapt ul width based on number of results
*/
var photo_count_total = photo_count + $photosContainer.children('li').length;
ul_width = photo_count_total * 105 + 105;
$photosContainer.css('width',ul_width + 'px');
for(var i = 0; i < photo_count; ++i){
var photo = data.photoset.photo[i];
var photoid = photo.id;
var secret = photo.secret;
var server = photo.server;
var farm = photo.farm;
var photoUrl = 'http://farm'+farm+'.static.flickr.com/'+server+'/'+photoid+'_'+secret+'_s.jpg';
var $elem = $('<li />');
var $link = $('<a class="toLoad" href="#" />');
$link.data({
'photoid' :photoid,
'secret' :secret,
'server' :server,
'farm' :farm,
'photoUrl' :photoUrl,
'photo_title' :photo.title
});
$photosContainer.append($elem.append($link));
$link.bind('click',function(e){
var $this = $(this);
current = $this.parent().index();
photo_id = $this.data('photoid');
LoadLargePhoto();
e.preventDefault();
});
}
LoadPhotosImages();
}
});
}
/*
loads the images of the set's
photos that are in the viewport
*/
function LoadPhotosImages(){
var toLoad = $('.toLoad:in-viewport').size();
if(toLoad > 0)
$loadingStatus.css('visibility','visible');
var images_loaded = 0;
$('.toLoad:in-viewport').each(function(i){
var $space = $photosContainer.find('.toLoad:first');
var $img = $('<img style="display:none;" />').load(function(){
++images_loaded;
if(images_loaded == toLoad){
$loadingStatus.css('visibility','hidden');
$photosContainer.find('img').fadeIn();
/*
if we were navigating through the large images set:
*/
if(continueNavigation){
continueNavigation = false;
var $thumb = $photosContainer.find('li:nth-child(' + parseInt(current + 1) + ')').find('img');
photo_id = $thumb.attr('alt');
LoadLargePhoto();
}
}
}).error(function(){
//TODO
++images_loaded;
if(images_loaded == toLoad){
$loadingStatus.css('visibility','hidden');
$photosContainer.find('img').fadeIn();
/*
if we were navigating through the large images set:
*/
if(continueNavigation){
continueNavigation = false;
var $thumb = $photosContainer.find('li:nth-child(' + parseInt(current + 1) + ')').find('img');
photo_id = $thumb.attr('alt');
LoadLargePhoto();
}
}
}).attr('src',$space.data('photoUrl'))
.attr('alt',$space.data('photoid'));
var $photo_title = $('<span/>',{'html':$space.data('photo_title')});
$space.append($photo_title).append($img).removeClass('toLoad');
});
}
/*
loads the main image
*/
function LoadLargePhoto(){
removeLargeImage();
var $theThumb = $photosContainer.find('li:nth-child(' + parseInt(current + 1) + ')').find('img');
var photo_title = $theThumb.parent().data('photo_title');
var $loading = $photopreview.find('.loading');
$loading.show();
$photopreview.show();
$flickrOverlay.show();
$('#preview_close').show();
var large_photo_url = large_photo_service + '&photo_id=' + photo_id + '&format=json&jsoncallback=?';
$.getJSON(large_photo_url,function(data){
if(data.stat != 'fail') {
var count_sizes = data.sizes.size.length;
var largest_photo_src;
for(var i = 0; i < count_sizes; ++i){
if(data.sizes.size[i].label == large_image_size)
largest_photo_src = data.sizes.size[i].source;
}
$('<img />').load(function(){
var $this = $(this);
/*
resize the image to fit in the browser's window
*/
resize($this);
$loading.hide();
/*just to make sure theres no image:*/
removeLargeImage();
$photopreview.find('.preview').append($this);
$('#large_phototitle').empty().html(photo_title);
}).attr('src',largest_photo_src);
}
});
}
/*
close the Large Image View
*/
$('#preview_close').bind('click',function(){
$photopreview.hide();
$flickrOverlay.hide();
$('#preview_close').hide();
$('#large_phototitle').empty()
removeLargeImage();
});
/*
removes the large image from the DOM
*/
function removeLargeImage(){
$photopreview.find('img').remove();
}
/*
events to navigate through the Large Images
*/
$('#preview_img_next').bind('click',function(e){
/*
get the next one
*/
++current;
var $link = $photosContainer.find('li:nth-child(' + parseInt(current + 1) + ')');
var $thumb = $link.find('img');
/*
if the next image is not loaded,
we need to scroll the photos container
and just then continue with the navigation
*/
if(!$thumb.length && $link.length){
continueNavigation = true;
removeLargeImage();
$photopreview.find('.loading').show();
$('#images').find('.next').trigger('click');
}
else if(!$thumb.length && !$link.length){
--current;
return;
}
else{
photo_id = $thumb.attr('alt');
LoadLargePhoto();
}
e.preventDefault();
});
$('#preview_img_prev').bind('click',function(e){
/*
get the previous one
*/
var $link = $photosContainer.find('li:nth-child(' + parseInt(current) + ')');
--current;
var $thumb = $link.find('img');
/*
if the previous image is not in the viewport,
we need to scroll the photos container
and just then continue with the navigation
*/
if(!$thumb.length && !$link.length){
++current;
return;
}
if(!$thumb.is(':in-viewport') && $link.length){
$('#images').find('.prev').trigger('click');
}
photo_id = $thumb.attr('alt');
LoadLargePhoto();
e.preventDefault();
});
/*
events to navigate through the sets / photos containers
*/
var scrollAllow = true;
$('#sets,#images').find('.next').bind('click',function(e) {
var target_id = $(e.target).parent().attr('id');
var $theContainer;
if(target_id == 'sets')
$theContainer = $setsContainer;
else if(target_id == 'images')
$theContainer = $photosContainer;
if(scrollAllow){
scrollAllow = false;
spacefit = $(window).width() -44;
fit = Math.floor(spacefit / 105);
var left = parseFloat($theContainer.css('left'),10);
var moveleft = left - (fit*105);
if(ul_width - Math.abs(left) < $(window).width()){
scrollAllow = true;
e.preventDefault();
return;
}
$theContainer.animate({'left':moveleft+'px'},1000,function(){
scrollAllow = true;
if(target_id == 'sets')
LoadSetsImages();
else if(target_id == 'images')
LoadPhotosImages();
});
e.preventDefault();
}
});
$('#sets,#images').find('.prev').bind('click',function(e) {
var target_id = $(e.target).parent().attr('id');
var $theContainer;
if(target_id == 'sets')
$theContainer = $setsContainer;
else if(target_id == 'images')
$theContainer = $photosContainer;
if(scrollAllow){
scrollAllow = false;
spacefit = $(window).width() -44;
fit = Math.floor(spacefit / 105);
var left = parseFloat($theContainer.css('left'),10);
var moveleft = left + (fit*105);
if(left >= 0){
scrollAllow = true;
e.preventDefault();
return;
}
$theContainer.animate({'left':moveleft+'px'},1000,function(){
scrollAllow = true;
});
e.preventDefault();
}
});
/*
when cliking "Back to Sets"
minimize photos bar
*/
$('#images_toggle').bind('click',function(){
$('#images').stop().animate({'bottom':'-125px'},200);
});
/*
when resizing the window, resize the main image
*/
$(window).bind('resize', function() {
var $theLargeImage = $photopreview.find('img');
if($theLargeImage.length)
resize($theLargeImage);
});
/*
resizes the main image based on the windows sizes
*/
function resize($image){
var widthMargin = 10
var heightMargin = 60;
var windowH = $(window).height()-heightMargin;
var windowW = $(window).width()-widthMargin;
$photopreview.find('.preview').css({'width':$(window).width()+'px','height':($(window).height()-25)+'px'});
var theImage = new Image();
theImage.src = $image.attr("src");
var imgwidth = theImage.width;
var imgheight = theImage.height;
if((imgwidth > windowW)||(imgheight > windowH)){
if(imgwidth > imgheight){
var newwidth = windowW;
var ratio = imgwidth / windowW;
var newheight = imgheight / ratio;
theImage.height = newheight;
theImage.width = newwidth;
if(newheight>windowH){
var newnewheight= windowH;
var newratio = newheight/windowH;
var newnewwidth = newwidth/newratio;
theImage.width = newnewwidth;
theImage.height = newnewheight;
}
}
else{
var newheight = windowH;
var ratio = imgheight / windowH;
var newwidth = imgwidth / ratio;
theImage.height = newheight;
theImage.width= newwidth;
if(newwidth>windowW){
var newnewwidth = windowW;
var newratio = newwidth/windowW;
var newnewheight =newheight/newratio;
theImage.height = newnewheight;
theImage.width= newnewwidth;
}
}
}
$image.css({'width':theImage.width+'px','height':theImage.height+'px'});
}
});
If you want the description of a set you can call flickr.photosets.getInfo.
On an unrelated note, you can save an API call to flickr.photos.getSizes for each photo in the set by passing in an extras parameter specifying all of the urls you'd like to get back when calling flickr.photosets.getPhotos. For any size identifier (sq, t, m, l, o, etc.), just pass in an extra of url_(identifier) (so, url_o would have the response contain the original url if it was available). This is documented here: http://www.flickr.com/services/api/flickr.photosets.getPhotos.html