Created
December 24, 2014 19:11
-
-
Save nathan-osman/620001a7b0084d154093 to your computer and use it in GitHub Desktop.
StackExchange™ SuperCollider Freehand Circle™ Editor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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