StackExchange™ SuperCollider Freehand Circle™ Editor
// ==UserScript== | |
// @name StackExchange™ SuperCollider Freehand Circle™ Editor | |
// @author Nathan Osman | |
// @namespace http://quickmediasolutions.com | |
// @description Allows Freehand Circles™ to be added to the images on the page | |
// @include http://stackoverflow.com/* | |
// @include http://superuser.com/* | |
// @include http://serverfault.com/* | |
// @include http://meta.stackoverflow.com/* | |
// @include http://meta.superuser.com/* | |
// @include http://meta.serverfault.com/* | |
// @include http://stackapps.com/* | |
// @include http://askubuntu.com/* | |
// @include http://meta.askubuntu.com/* | |
// @include http://*.stackexchange.com/* | |
// ==/UserScript== | |
// Here I borrow a couple functions I wrote for another | |
// UserScript that makes it easy to provide functions | |
// with complete access to the page. | |
function EmbedFunctionOnPageAndExecute(function_contents) | |
{ | |
var exec_script = document.createElement('script'); | |
exec_script.type = 'text/javascript'; | |
exec_script.textContent = "(" + function_contents.toString() + ")()"; | |
document.getElementsByTagName('head')[0].appendChild(exec_script); | |
} | |
// ...the other one | |
function EmbedFunctionOnPage(function_name, function_contents) | |
{ | |
var exec_script = document.createElement('script'); | |
exec_script.type = 'text/javascript'; | |
exec_script.textContent = function_contents.toString().replace(/function ?/, 'function ' + function_name); | |
document.getElementsByTagName('head')[0].appendChild(exec_script); | |
} | |
// This function initializes the images | |
EmbedFunctionOnPage('ProcessImages', function(selector, save_function) { | |
// Loop through all of the images on the page | |
$(selector).each(function() { | |
var img_element = this; | |
// Grab the source of the image | |
var img_src = this.src; | |
// Set it to nothing so that we can later get the script | |
// to tell us when the image has loaded. | |
this.src = ''; | |
// When the image is loaded, we proceed. | |
this.onload = function() { | |
// Now we obtain the image's width and height | |
var img_width = $(img_element).width(); | |
var img_height = $(img_element).height(); | |
// If any of the dimensions of the image are less | |
// than 100, we ignore it. | |
if(img_width <= 100 && img_height <= 100) | |
return; | |
// Now we create the canvas element. | |
var canvas_id = 'canvas_' + index; | |
$('body').append('<canvas id="' + canvas_id + '" width="' + img_width + | |
'" height="' + img_height + '" style="position: absolute; top: 0px; left: 0px; z-index: 99;"></canvas>'); | |
// Get the drawing context of the canvas | |
var canvas_element = document.getElementById(canvas_id); | |
var canvas_context = canvas_element.getContext('2d'); | |
// Set the properties for lines | |
canvas_context.strokeStyle = "rgb(255,0,0)"; | |
canvas_context.lineWidth = 2; | |
// We create a DIV which will contain the image and | |
// the associated canvas on top of it | |
var container = $('<div style="position: relative;"></div>'); | |
$(img_element).replaceWith(container); | |
$(container).append(img_element); | |
$(container).append(canvas_element); | |
// We also add some custom data properties | |
// to the canvas which we will use later on. | |
$(canvas_element).data('drawing', false); // Whether we are currently drawing | |
// on the canvas or not. | |
$(canvas_element).data('context', canvas_context); // The canvas context | |
// Determine the ID of this post | |
if($(img_element).parents('.answer').length) | |
{ | |
// Get the answer parent | |
var answer_id = $(img_element).parents('.answer').attr('id').match(/(\d+)/)[1]; | |
$(canvas_element).data('post_id', answer_id); | |
} | |
else | |
{ | |
var match_r = location.href.match(/\/(\d+)\//); | |
if(match_r != null) | |
$(canvas_element).data('post_id', match_r[1]); | |
} | |
// When the mouse enters the drawing, we | |
// display our messsage | |
$(canvas_element).mouseenter(function() { | |
// Create a div that indicates to the user that | |
// they can activate a DIV to edit the image. | |
var note_div = $('<div id="edit_notice" style="position: absolute; opacity: 0;' + | |
'background-color: #000; color: #fff; font-size: 14pt; font-weight: bold; padding: 12px;">' + | |
'Click the image to edit it.</div>'); | |
// Append that notification to the page. | |
$('body').append(note_div); | |
// Center the note on the image | |
var canvas_pos = $(container).position(); | |
var note_width = note_div.width(); | |
var note_height = note_div.height(); | |
var x_pos = (img_width - note_width) / 2; | |
var y_pos = (img_height - note_height) / 2; | |
note_div.css('left', (canvas_pos.left + x_pos) + 'px'); | |
note_div.css('top', (canvas_pos.top + y_pos) + 'px'); | |
// Now fade it in | |
note_div.stop(true, true).animate({opacity: '+=0.6'}); | |
}); | |
$(canvas_element).mouseleave(function() { | |
$('#edit_notice').stop(true, true).animate({opacity: '-=0.6'}, function() { $('#edit_notice').remove(); }); | |
}); | |
$(canvas_element).click(function() { | |
$('#wmd-input').blur(); | |
// When the mouse is clicked, we then bring | |
// in the other event handlers | |
$(canvas_element).unbind('click'); | |
$(canvas_element).unbind('mouseenter'); | |
$(canvas_element).unbind('mouseleave'); | |
// Add a new click handler that does nothing. | |
$(canvas_element).click(function() { return false; }); | |
// Get rid of any notices | |
$('#edit_notice').animate({opacity: '-=0.6'}, function() { $('#edit_notice').remove(); }); | |
// Display the toolbox | |
// HTML for the save button | |
// The SaveImage() function takes two parameters | |
// - the id of the canvas | |
// - the original src of the image | |
var save_button_html = '<div style="display: none; padding: 0px 6px 12px 6px; margin-bottom: 6px; background-color: #eee;">' + | |
'<br /><a href="javascript:void(0)" ' + | |
'style="font-size: 12pt; border: 0px; background-color: #444; padding: 6px; color: #fff;" ' + | |
'onclick="' + save_function + '(\'' + canvas_id + '\',\'' + img_src + '\')">save</a> '; | |
// Also a reset button | |
save_button_html += '<a href="javascript:void(0)" ' + | |
'style="font-size: 12pt; border: 0px; background-color: #444; padding: 6px; color: #fff;" ' + | |
'onclick="ResetImage(\'' + canvas_id + '\')">reset</a> '; | |
// And a drop-down box to select the color of the canvas | |
save_button_html += 'Color: <select onchange="ChangeColor(\'' + canvas_id + '\', this)"><option value="rgb(255,0,0)">Red</option><option value="rgb(0,0,255)">Blue</option><option value="rgb(0,255,0)">Green</option><option value="rgb(0,0,0)">Black</option><option value="rgb(255,255,255)">White</option></select> '; | |
// ...and for the pixel size | |
save_button_html += 'Brush Size: <select onchange="ChangeBrushSize(\'' + canvas_id + '\', this)"><option value="1">1</option><option value="2" selected>2</option><option value="3">3</option><option value="4">4</option></select></div>'; | |
// Add the save button when the mouse is released | |
var save_element = $(save_button_html); | |
$(canvas_element).after(save_element); | |
save_element.slideDown(); | |
// Set the hover style on the button | |
save_element.find('a').hover(function() { $(this).css('textDecoration', 'underline') }, | |
function() { $(this).css('textDecoration', 'none') }); | |
// When the mouse is clicked... | |
$(canvas_element).mousedown(function(event) { | |
$(canvas_element).data('drawing', true); | |
// Grab the current coords | |
var pos = $(container).position(); | |
$(canvas_element).data('prev_x', event.pageX - pos.left); | |
$(canvas_element).data('prev_y', event.pageY - pos.top); | |
return false; | |
}); | |
// When the mouse is moved... | |
$(canvas_element).mousemove(function(event) { | |
// Only do anything if we're drawing on the | |
// canvas at the moment. | |
if($(canvas_element).data('drawing')) | |
{ | |
// Get the previous position | |
var prev_x = $(canvas_element).data('prev_x'); | |
var prev_y = $(canvas_element).data('prev_y'); | |
// Grab the current position | |
var pos = $(container).position(); | |
var x = event.pageX - pos.left; | |
var y = event.pageY - pos.top; | |
// Now draw the line from our previous position | |
// to our current position. | |
canvas_context.beginPath(); | |
canvas_context.moveTo(prev_x, prev_y); | |
canvas_context.lineTo(x, y); | |
canvas_context.stroke(); | |
$(canvas_element).data('prev_x', x); | |
$(canvas_element).data('prev_y', y); | |
} | |
}); | |
// The completion function | |
var done = function(event) { | |
// We are no longer drawing | |
$(canvas_element).data('drawing', false); | |
return false; | |
}; | |
// When the mouse is released... | |
$(canvas_element).mouseup(done); | |
// Repeat for the mouseleave | |
$(canvas_element).mouseleave(done); | |
return false; | |
}); | |
// We increment the index | |
index++; | |
}; | |
this.src = img_src; | |
}); | |
}); | |
// This function clears the canvas | |
EmbedFunctionOnPage('ResetImage', function(canvas_id) { | |
// Get the canvas element | |
var canvas_element = document.getElementById(canvas_id); | |
// Now clear it | |
var canvas_context = $(canvas_element).data('context'); | |
canvas_context.clearRect(0, 0, $(canvas_element).width(), $(canvas_element).height()); | |
return false; | |
}); | |
// This function clears the canvas | |
EmbedFunctionOnPage('ChangeColor', function(canvas_id, select) { | |
// Get the canvas element | |
var canvas_element = document.getElementById(canvas_id); | |
// Now clear it | |
var canvas_context = $(canvas_element).data('context'); | |
// Set the new color | |
canvas_context.strokeStyle = select.value; | |
}); | |
// This function clears the canvas | |
EmbedFunctionOnPage('ChangeBrushSize', function(canvas_id, select) { | |
// Get the canvas element | |
var canvas_element = document.getElementById(canvas_id); | |
// Now clear it | |
var canvas_context = $(canvas_element).data('context'); | |
// Set the new color | |
canvas_context.lineWidth = select.value; | |
}); | |
// This function simply returns an imgur URL via a | |
// callback in response to a request for one | |
EmbedFunctionOnPage('GetNewURL', function(canvas_id, original_img_src, callback) { | |
// Firstly, grab a copy of the canvas element | |
var canvas_element = document.getElementById(canvas_id); | |
// Before we may proceed, we need to get the GUID of the first | |
// revision to a question. In order to do that, we need to | |
// get the ID of that question. | |
$.ajax({ url: 'http://api.' + page_domain + '/1.1/questions?pagesize=1', | |
dataType: 'jsonp', jsonp: 'jsonp', | |
success: function(data) { | |
// Now get the ID of that first question | |
var question_id = data['questions'][0]['question_id']; | |
$.ajax({ url: 'http://api.' + page_domain + '/1.1/revisions/' + question_id, | |
dataType: 'jsonp', jsonp: 'jsonp', | |
success: function(data) { | |
var this_question_first_guid = data['revisions'][0]['revision_guid']; | |
// Next, grab the image data (in PNG format) | |
var img_data = canvas_element.toDataURL('image/png'); | |
var server_url = new Array('http://fhc.quickmediasolutions.com/process.php'); | |
// Now we create an iframe that will be used | |
// to upload the image data to the server side script | |
var iframe_content = | |
'<form action="' + server_url + '" method="post" id="imgur_form">' + | |
'<input type="hidden" name="image_data" value="' + img_data + '" />' + | |
'<input type="hidden" name="site_domain" value="' + page_domain + '" />' + | |
'<input type="hidden" name="site_guid" value="' + this_question_first_guid + '" />' + | |
'<input type="hidden" name="image_url" value="' + original_img_src + '" /></form>'; | |
// Create and append the iframe to the document | |
var iframe = $('<iframe style="display: none;"></iframe>'); | |
$('body').append(iframe); | |
// Set the iframe's contents | |
iframe.contents().find('body').html(iframe_content); | |
// Also, later on we will need to have access to the original | |
// URL of the image, so store it in the iframe as a custom property. | |
iframe.data('src', original_img_src); | |
// ...lastly, submit the form | |
iframe.contents().find('#imgur_form').trigger('submit'); | |
// The next step is a little tricky (and quite a nice hack). | |
// We poll the iframe every so often to see if we can access | |
// it's contents - this will only be possible once the source | |
// of the iframe is back to a page within the local domain. | |
// The server side script is smart enough to redirect us to | |
// a special page on the domain that... | |
// - is within the domain | |
// - embeds the new imgur image ID in the URL | |
// Poll | |
window.setTimeout(Poll, 400, canvas_element, iframe, new Date().getTime(), this_question_first_guid, callback); | |
}, error: function() { | |
$(canvas_element).next().html('<br><span style="color: red;">There was an error retrieving the GUID of question #' + question_id + ' on ' + page_domain + '.</span>'); | |
}}); | |
}, error: function() { | |
$(canvas_element).next().html('<br><span style="color: red;">There was an error retrieving a question ID from the API.</span>'); | |
}}); | |
// Set the waiting image | |
$(canvas_element).next().html('<br>saving <img src="http://' + page_domain + '/content/img/progress-dots.gif" />'); | |
}); | |
// This function sends the image to a server | |
// side script which will upload it to fhc | |
// and return (eventually) a URL for the new image | |
EmbedFunctionOnPage('SavePostImage', function(canvas_id, original_img_src) { | |
// Simply call GetNewURL | |
GetNewURL(canvas_id, original_img_src, function(new_url) { | |
// Get the canvas | |
var canvas_element = document.getElementById(canvas_id); | |
// Now get the ID of this post | |
var post_id = $(canvas_element).data('post_id'); | |
// The next step is to get the last revision for this post | |
$.ajax({ url: 'http://api.' + page_domain + '/1.1/revisions/' + post_id, | |
dataType: 'jsonp', jsonp: 'jsonp', | |
success: function(data) { | |
// Grab the latest revision GUID... keep | |
// in mind that we may need to skip over some | |
// due to their type. (single_user is our desired type) | |
var revision_guid; | |
/* | |
for(var i=0;i<data['revisions'].length;++i) | |
{ | |
if(data['revisions'][i]['revision_type'] == 'single_user') | |
revision_guid = data['revisions'][i]['revision_guid']; | |
}*/ | |
revision_guid = data['revisions'][0]['revision_guid']; | |
// Now our final step is to fetch the markdown | |
// for that revision... | |
$.ajax({ url: 'http://' + page_domain + '/revisions/' + revision_guid + '/view-source', | |
success: function(data) { | |
// In order to avoid parsing HTML with RegEx, we'll | |
// stick the HTML in an iframe and extract what we want | |
var iframe = $('<iframe style="display: none;"></iframe>'); | |
$('body').append(iframe); | |
iframe.contents().find('body').html(data); | |
// Now we grab the contents of the PRE | |
var old_contents = iframe.contents().find('pre').text(); | |
// Replace the old image url with the new one | |
var new_contents = old_contents.replace(original_img_src, new_url); | |
// Collect some of the information to submit... we won't need | |
// some of it for answers | |
var question_title = $('#question-header .question-hyperlink').text(); | |
var tag_list = new Array(); | |
$('.post-taglist a').each(function() { tag_list.push($(this).text()); } ); | |
var question_tags = tag_list.join(' '); | |
// Remove HTML entities from the question | |
// title and the new post body. | |
question_title = question_title.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); | |
new_contents = new_contents.replace(/&/g, '&'.replace(/</g, '<').replace(/>/g, '>')); | |
// Now we need to make a POST request to put the content | |
// on the site | |
var post_form = '<form id="injectedform" style="display: none;" action="/posts/' + post_id + '/edit-submit/' + revision_guid + '" method="post"><textarea name="title">' + question_title + '</textarea><textarea name="post-text">' + new_contents + '</textarea><input type="hidden" name="fkey" value="' + StackExchange.options.user.fkey + '" /><input type="hidden" name="author" value="" /><input type="hidden" name="tagnames" value="' + question_tags + '" /><input type="hidden" name="edit-comment" value="Enhanced with freehand circles!" /></form>'; | |
// Now it's simple! Just stick this on the page and submit. | |
$('body').append(post_form); | |
$('#injectedform').trigger('submit'); | |
}, error: function() { | |
$(canvas_element).next().html('<br><span style="color: red;">There was an error retrieving revision content.</span>'); | |
}}); | |
}, error: function() { | |
$(canvas_element).next().html('<br><span style="color: red;">There was an error retrieving revision ID from the API.</span>'); | |
}}); | |
}); | |
}); | |
// This function sends the image to a server | |
// side script which will upload it to imgur.com | |
// and return (eventually) a URL for the new image | |
EmbedFunctionOnPage('SaveDraftImage', function(canvas_id, original_img_src) { | |
// We simply begin by getting the new fhc | |
// URL for the given image | |
GetNewURL(canvas_id, original_img_src, function(new_url) { | |
var box_contents = $('#wmd-input').val(); | |
// Replace the old image with the new one | |
box_contents = box_contents.replace(original_img_src, new_url); | |
$('#wmd-input').val(box_contents); | |
// Trigger a markdown update | |
Attacklab.PreviewManager.refresh(); // weird, I know :P | |
}); | |
}); | |
// This function is called continuously until we can access the iframe | |
// content. Once we can, we call the callback function with the new URL | |
// that we have obtained. | |
EmbedFunctionOnPage('Poll', function(canvas_element, iframe, start_time, question_guid, callback) { | |
// Check to see if we can get the information yet. | |
var content = iframe.contents().find('body').html(); | |
if(content == null || (content.indexOf('imgur_form') != -1)) | |
{ | |
// If we have waited 20 seconds or more, | |
// assume that something has gone wrong. | |
var time_diff = new Date().getTime() - start_time; | |
if(time_diff > 20000) | |
$(canvas_element).next().html('<br><span style="color: red;">There was an error retrieving the new image URL.</span>'); | |
else | |
window.setTimeout(Poll, 200, canvas_element, iframe, start_time, question_guid, callback); | |
} | |
else | |
{ | |
// Get the iframe content document | |
var iframe_document = iframe[0].contentDocument; | |
var parameter = iframe_document.URL.replace('http://' + page_domain + '/revisions/' + question_guid + '/view-source?parameterpass=',''); | |
// Check for an error | |
if(parameter == 'ERROR') | |
{ | |
$(canvas_element).next().html('<br><span style="color: red;">There was an error retrieving the new image URL.</span>'); | |
return; | |
} | |
// Put together the final imgur URL | |
var image_url = 'http://fhc.quickmediasolutions.com/image/' + parameter + '.png'; | |
callback(image_url); | |
} | |
}); | |
// This code will get executed immediately after | |
// being embedded on the page. | |
EmbedFunctionOnPageAndExecute(function() { | |
// Grab the domain name so we can use it later on | |
// for a few things... | |
page_domain = location.href.match(/http:\/\/([\w\d\.]+)/)[1]; | |
// Set the index base | |
index = 0; | |
// Now process the images on the page | |
ProcessImages('#question img, .answer img', 'SavePostImage'); | |
// Set a hook to process images when the entry box is edited | |
// Inject this function into the answer processing code | |
fhc_timeout_id = null; | |
Attacklab.postPreviewHtmlHook = function(a) { | |
// Clear current timeout | |
if(fhc_timeout_id != null) | |
window.clearTimeout(fhc_timeout_id); | |
// We set a timeout for the processor to execute. | |
fhc_timeout_id = window.setTimeout(function() { ProcessImages('#wmd-preview img', 'SaveDraftImage'); }, 500); | |
return a; | |
}; | |
// In case there _already_ was a draft, | |
ProcessImages('#wmd-preview img', 'SaveDraftImage'); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment