Skip to content

Instantly share code, notes, and snippets.

@nathan-osman
Created December 24, 2014 19:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nathan-osman/620001a7b0084d154093 to your computer and use it in GitHub Desktop.
Save nathan-osman/620001a7b0084d154093 to your computer and use it in GitHub Desktop.
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>&nbsp;&nbsp;&nbsp;&nbsp;';
// 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>&nbsp;&nbsp;&nbsp;&nbsp;';
// ...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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
new_contents = new_contents.replace(/&/g, '&amp;'.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
// 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