mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-09 06:12:34 -05:00
494 lines
13 KiB
JavaScript
494 lines
13 KiB
JavaScript
|
// Install some useful jQuery extensions that we use a lot
|
||
|
|
||
|
$.support.touch = 'ontouchstart' in window;
|
||
|
|
||
|
$.extend($.fn, {
|
||
|
orNull: function() {
|
||
|
return this.length > 0 ? this : null;
|
||
|
},
|
||
|
|
||
|
findAndSelf: function(selector) {
|
||
|
return this.find(selector).add(this.filter(selector));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Little Helpers
|
||
|
|
||
|
function hyphenate(str) {
|
||
|
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||
|
}
|
||
|
|
||
|
function isVisible(el) {
|
||
|
if (el.is(':hidden'))
|
||
|
return false;
|
||
|
var viewTop = $(window).scrollTop();
|
||
|
var viewBottom = viewTop + $(window).height();
|
||
|
var top = el.offset().top;
|
||
|
var bottom = top + el.height();
|
||
|
return top >= viewTop && bottom <= viewBottom
|
||
|
|| top <= viewTop && bottom >= viewTop
|
||
|
|| top <= viewBottom && bottom >= viewBottom;
|
||
|
}
|
||
|
|
||
|
function smoothScrollTo(el, callback) {
|
||
|
$('html, body').animate({
|
||
|
scrollTop: el.offset().top
|
||
|
}, 250, callback);
|
||
|
}
|
||
|
|
||
|
var behaviors = {};
|
||
|
|
||
|
behaviors.hiDPI = function() {
|
||
|
// Turn off hiDPI for all touch devices for now, until the site is built
|
||
|
// true to scale.
|
||
|
if ($.support.touch)
|
||
|
$('canvas').attr('hidpi', 'off');
|
||
|
};
|
||
|
|
||
|
behaviors.sections = function() {
|
||
|
var toc = $('.toc');
|
||
|
var checks = [];
|
||
|
var active;
|
||
|
|
||
|
function update() {
|
||
|
$.each(checks, function() {
|
||
|
if (this())
|
||
|
return false;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
$(document).scroll(update);
|
||
|
$(window).resize(update);
|
||
|
setTimeout(update, 0);
|
||
|
|
||
|
$('article section').each(function() {
|
||
|
var section = $(this);
|
||
|
var anchor = $('a', section);
|
||
|
// Move content until next section inside section
|
||
|
section.append(section.nextUntil('section'));
|
||
|
var title = anchor.attr('title') || $('h1,h2', section).first().text();
|
||
|
var id = section.attr('id');
|
||
|
if (!id) {
|
||
|
id = hyphenate(title)
|
||
|
.replace(/\s+/g, '-')
|
||
|
.replace(/^#/, '')
|
||
|
.replace(/[!"#$%&'\()*+,.\/:;<=>?@\[\\\]\^_`{|}~]+/g, '-')
|
||
|
.replace(/-+/g, '-');
|
||
|
section.attr('id', id);
|
||
|
anchor.attr('name', id);
|
||
|
}
|
||
|
|
||
|
function activate() {
|
||
|
if (active)
|
||
|
active.removeClass('active');
|
||
|
selector.addClass('active');
|
||
|
active = selector;
|
||
|
}
|
||
|
|
||
|
// Create table of contents on the fly
|
||
|
if (toc) {
|
||
|
var selector = $('<li class="entry selector"><a href="#' + id + '">'
|
||
|
+ title + '</a></li>').appendTo(toc);
|
||
|
if (section.is('.spacer'))
|
||
|
selector.addClass('spacer');
|
||
|
$('a', selector).click(function() {
|
||
|
smoothScrollTo(section, function() {
|
||
|
window.location.hash = id;
|
||
|
});
|
||
|
return false;
|
||
|
});
|
||
|
|
||
|
checks.push(function() {
|
||
|
var visible = isVisible(section);
|
||
|
if (visible)
|
||
|
activate();
|
||
|
return visible;
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Adjust height of last section so that the last anchor aligns perfectly
|
||
|
// with the top of the browser window.
|
||
|
var lastSection = $('article section:last');
|
||
|
var lastAnchor = $('a[name]', lastSection);
|
||
|
|
||
|
function resize() {
|
||
|
lastSection.height('auto');
|
||
|
var bottom = $(document).height() - lastAnchor.offset().top - $(window).height();
|
||
|
if (bottom < 0)
|
||
|
lastSection.height(lastSection.height() - bottom);
|
||
|
}
|
||
|
|
||
|
if (lastSection.length && lastAnchor.length) {
|
||
|
$(window).on({
|
||
|
load: resize,
|
||
|
resize: resize
|
||
|
});
|
||
|
resize();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
behaviors.sticky = function() {
|
||
|
$('.sticky').each(function() {
|
||
|
me = $(this);
|
||
|
container = $('<div/>').append(me.contents()).appendTo(me);
|
||
|
// Insert a div wrapper of which the fixed class is modified depending on position
|
||
|
$(window).scroll(function() {
|
||
|
if (container.is(':visible'))
|
||
|
container.toggleClass('fixed', me.offset().top - $(this).scrollTop() <= 0);
|
||
|
});
|
||
|
});
|
||
|
};
|
||
|
|
||
|
behaviors.hash = function() {
|
||
|
var hash = unescape(window.location.hash);
|
||
|
if (hash) {
|
||
|
// First see if there's a class member to open
|
||
|
var target = $(hash);
|
||
|
if (target.length) {
|
||
|
if (target.hasClass('member'))
|
||
|
toggleMember(target);
|
||
|
smoothScrollTo(target);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
behaviors.thumbnails = function() {
|
||
|
var thumbnails = $('.thumbnail');
|
||
|
var height = 0;
|
||
|
thumbnails.each(function() {
|
||
|
height = Math.max(height, $(this).height());
|
||
|
});
|
||
|
$('.thumbnail').height(height);
|
||
|
};
|
||
|
|
||
|
behaviors.expandableLists = function() {
|
||
|
$('.expandable-list').each(function() {
|
||
|
var list = $(this);
|
||
|
$('<a href="#" class="arrow"/>')
|
||
|
.prependTo(list)
|
||
|
.click(function() {
|
||
|
list.toggleClass('expanded');
|
||
|
});
|
||
|
});
|
||
|
};
|
||
|
|
||
|
behaviors.referenceClass = function() {
|
||
|
var classes = $('.reference-classes');
|
||
|
if (classes.length) {
|
||
|
// Mark currently selected class as active. Do it client-sided
|
||
|
// since the menu is generated by jsdocs.
|
||
|
var path = window.location.pathname.toLowerCase();
|
||
|
$('a[href="' + path + '"]', classes).addClass('active');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
behaviors.hover = function() {
|
||
|
$('.hover').hover(function() {
|
||
|
$('.normal', this).toggleClass('hidden');
|
||
|
$('.over', this).toggleClass('hidden');
|
||
|
});
|
||
|
};
|
||
|
|
||
|
behaviors.code = function() {
|
||
|
$('.code:visible, pre:visible code').each(function() {
|
||
|
createCode($(this));
|
||
|
});
|
||
|
};
|
||
|
|
||
|
behaviors.paperscript = function() {
|
||
|
// Ignore all paperscripts in the automatic load event, and load them
|
||
|
// separately in createPaperScript() when needed.
|
||
|
$('script[type="text/paperscript"]').attr('ignore', 'true');
|
||
|
$('.paperscript:visible').each(function() {
|
||
|
createPaperScript($(this));
|
||
|
});
|
||
|
};
|
||
|
|
||
|
function createCodeMirror(place, options, source) {
|
||
|
return new CodeMirror(place, $.extend({}, {
|
||
|
mode: 'javascript',
|
||
|
lineNumbers: true,
|
||
|
matchBrackets: true,
|
||
|
tabSize: 4,
|
||
|
indentUnit: 4,
|
||
|
indentWithTabs: true,
|
||
|
tabMode: 'shift',
|
||
|
value: source.text().match(
|
||
|
// Remove first & last empty line
|
||
|
/^\s*?[\n\r]?([\u0000-\uffff]*?)[\n\r]?\s*?$/)[1]
|
||
|
}, options));
|
||
|
}
|
||
|
|
||
|
function createCode(element) {
|
||
|
if (element.data('initialized'))
|
||
|
return;
|
||
|
var start = element.attr('start');
|
||
|
var highlight = element.attr('highlight');
|
||
|
var editor = createCodeMirror(function(el) {
|
||
|
element.replaceWith(el);
|
||
|
}, {
|
||
|
lineNumbers: !element.parent('.resource-text').length,
|
||
|
firstLineNumber: parseInt(start || 1, 10),
|
||
|
mode: element.attr('mode') || 'javascript',
|
||
|
readOnly: true
|
||
|
}, element);
|
||
|
if (highlight) {
|
||
|
var highlights = highlight.split(',');
|
||
|
for (var i = 0, l = highlights.length; i < l; i++) {
|
||
|
var highlight = highlights[i].split('-');
|
||
|
var hlStart = parseInt(highlight[0], 10) - 1;
|
||
|
var hlEnd = highlight.length == 2
|
||
|
? parseInt(highlight[1], 10) - 1 : hlStart;
|
||
|
if (start) {
|
||
|
hlStart -= start - 1;
|
||
|
hlEnd -= start - 1;
|
||
|
}
|
||
|
for (var j = hlStart; j <= hlEnd; j++) {
|
||
|
editor.setLineClass(j, 'highlight');
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
element.data('initialized', true);
|
||
|
}
|
||
|
|
||
|
function createPaperScript(element) {
|
||
|
if (element.data('initialized'))
|
||
|
return;
|
||
|
|
||
|
var script = $('script', element).orNull(),
|
||
|
runButton = $('.button.run', element).orNull();
|
||
|
if (!script)
|
||
|
return;
|
||
|
// Now load / parse / execute the script
|
||
|
script.removeAttr('ignore');
|
||
|
var scope = paper.PaperScript.load(script[0]);
|
||
|
if (!runButton)
|
||
|
return;
|
||
|
|
||
|
var canvas = $('canvas', element),
|
||
|
hasResize = canvas.attr('resize'),
|
||
|
showSplit = element.hasClass('split'),
|
||
|
sourceFirst = element.hasClass('source'),
|
||
|
editor = null,
|
||
|
hasBorders = true,
|
||
|
edited = false,
|
||
|
animateExplain,
|
||
|
explain = $('.explain', element).orNull(),
|
||
|
source = $('<div class="source hidden"/>').insertBefore(script);
|
||
|
|
||
|
if (explain) {
|
||
|
explain.addClass('hidden');
|
||
|
var text = explain.html().replace(/http:\/\/([\w.]+)/g, function(url, domain) {
|
||
|
return '<a href="' + url + '">' + domain + '</a>';
|
||
|
}).trim();
|
||
|
// Add explanation bubbles to tickle the visitor's fancy
|
||
|
var explanations = [{
|
||
|
index: 0,
|
||
|
list: [
|
||
|
[text ? 4 : 3, text || ''],
|
||
|
[1, ''],
|
||
|
[4, '<b>Note:</b> You can view and even edit<br>the source right here in the browser'],
|
||
|
[1, ''],
|
||
|
[3, 'To do so, simply press the <b>Source</b> button →']
|
||
|
]
|
||
|
}, {
|
||
|
index: 0,
|
||
|
indexIfEdited: 3, // Skip first sentence if user has already edited code
|
||
|
list: [
|
||
|
[4, ''],
|
||
|
[3, 'Why don\'t you try editing the code?'],
|
||
|
[1, ''],
|
||
|
[4, 'To run it again, simply press press <b>Run</b> →']
|
||
|
]
|
||
|
}];
|
||
|
var timer,
|
||
|
mode;
|
||
|
animateExplain = function(clearPrevious) {
|
||
|
if (timer)
|
||
|
timer = clearTimeout(timer);
|
||
|
// Set previous mode's index to the end?
|
||
|
if (mode && clearPrevious)
|
||
|
mode.index = mode.list.length;
|
||
|
mode = explanations[source.hasClass('hidden') ? 0 : 1];
|
||
|
if (edited && mode.index < mode.indexIfEdited)
|
||
|
mode.index = mode.indexIfEdited;
|
||
|
var entry = mode.list[mode.index];
|
||
|
if (entry) {
|
||
|
explain.removeClass('hidden');
|
||
|
explain.html(entry[1]);
|
||
|
timer = setTimeout(function() {
|
||
|
// Only increase once we're stepping, not in animate()
|
||
|
// itself, as entering & leaving would continuosly step
|
||
|
mode.index++;
|
||
|
animateExplain();
|
||
|
}, entry[0] * 1000);
|
||
|
}
|
||
|
if (!entry || !entry[1])
|
||
|
explain.addClass('hidden');
|
||
|
};
|
||
|
element
|
||
|
.mouseover(function() {
|
||
|
if (!timer)
|
||
|
animateExplain();
|
||
|
})
|
||
|
.mouseout(function() {
|
||
|
// Check the effect of :hover on button to see if we need
|
||
|
// to turn off...
|
||
|
// TODO: make mouseenter / mouseleave events work again
|
||
|
if (timer && runButton.css('display') == 'none') {
|
||
|
timer = clearTimeout(timer);
|
||
|
explain.addClass('hidden');
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function showSource(show) {
|
||
|
source.toggleClass('hidden', !show);
|
||
|
runButton.text(show ? 'Run' : 'Source');
|
||
|
if (explain)
|
||
|
animateExplain(true);
|
||
|
if (show && !editor) {
|
||
|
editor = createCodeMirror(source[0], {
|
||
|
onKeyEvent: function(editor, event) {
|
||
|
edited = true;
|
||
|
}
|
||
|
}, script);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function runScript() {
|
||
|
// Update script to edited version
|
||
|
var code = editor.getValue();
|
||
|
script.text(code);
|
||
|
// Keep a reference to the used canvas, since we're going to
|
||
|
// fully clear the scope and initialize again with this canvas.
|
||
|
// Support both old and new versions of paper.js for now:
|
||
|
var element = scope.view.element;
|
||
|
// Clear scope first, then evaluate a new script.
|
||
|
scope.clear();
|
||
|
scope.initialize(script[0]);
|
||
|
scope.setup(element);
|
||
|
scope.execute(code);
|
||
|
}
|
||
|
|
||
|
function resize() {
|
||
|
source
|
||
|
.width(element.width() - (hasBorders ? 2 : 1))
|
||
|
.height(element.height() - (hasBorders ? 2 : 0));
|
||
|
if (editor)
|
||
|
editor.refresh();
|
||
|
}
|
||
|
|
||
|
function toggleView() {
|
||
|
var show = source.hasClass('hidden');
|
||
|
resize();
|
||
|
canvas.toggleClass('hidden', show);
|
||
|
showSource(show);
|
||
|
if (!show)
|
||
|
runScript();
|
||
|
// Add extra margin if there is scrolling
|
||
|
var scrollHeight = $('.CodeMirror .CodeMirror-scroll', source).height();
|
||
|
runButton.css('margin-right', scrollHeight > element.height() ? 25 : 8);
|
||
|
}
|
||
|
|
||
|
if (hasResize) {
|
||
|
paper.view.on('resize', resize);
|
||
|
hasBorders = false;
|
||
|
source.css('border-width', '0 0 0 1px');
|
||
|
}
|
||
|
|
||
|
if (showSplit) {
|
||
|
showSource(true);
|
||
|
} else if (sourceFirst) {
|
||
|
toggleView();
|
||
|
}
|
||
|
|
||
|
runButton
|
||
|
.click(function() {
|
||
|
if (showSplit) {
|
||
|
runScript();
|
||
|
} else {
|
||
|
toggleView();
|
||
|
}
|
||
|
return false;
|
||
|
})
|
||
|
.mousedown(function() {
|
||
|
return false;
|
||
|
});
|
||
|
|
||
|
element.data('initialized', true);
|
||
|
}
|
||
|
|
||
|
// Reference (before behaviors)
|
||
|
|
||
|
var lastMember = null;
|
||
|
function toggleMember(member, dontScroll, offsetElement) {
|
||
|
var link = $('.member-link:first', member);
|
||
|
if (!link.length)
|
||
|
return true;
|
||
|
var desc = $('.member-description', member);
|
||
|
var visible = !link.hasClass('hidden');
|
||
|
// Retrieve y-offset before any changes, so we can correct scrolling after
|
||
|
var offset = (offsetElement || member).offset().top;
|
||
|
if (lastMember && !lastMember.is(member)) {
|
||
|
var prev = lastMember;
|
||
|
lastMember = null;
|
||
|
toggleMember(prev, true);
|
||
|
}
|
||
|
lastMember = visible && member;
|
||
|
link.toggleClass('hidden', visible);
|
||
|
desc.toggleClass('hidden', !visible);
|
||
|
if (!dontScroll) {
|
||
|
// Correct scrolling relatively to where we are, by checking the amount
|
||
|
// the element has shifted due to the above toggleMember call, and
|
||
|
// correcting by 11px offset, caused by 1px border and 10px padding.
|
||
|
var scroll = $(document).scrollTop();
|
||
|
// Only change hash if we're not in frames, since there are redrawing
|
||
|
// issues with that on Chrome.
|
||
|
if (parent === self)
|
||
|
window.location.hash = visible ? member.attr('id') : '';
|
||
|
$(document).scrollTop(scroll
|
||
|
+ (visible ? desc : link).offset().top - offset
|
||
|
+ 11 * (visible ? 1 : -1));
|
||
|
}
|
||
|
if (!member.data('initialized') && visible) {
|
||
|
behaviors.code();
|
||
|
behaviors.paperscript();
|
||
|
member.data('initialized', true);
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
$(function() {
|
||
|
$('.reference .member').each(function() {
|
||
|
var member = $(this);
|
||
|
var link = $('.member-link', member);
|
||
|
// Add header to description, with link and closing button
|
||
|
var header = $('<div class="member-header"/>').prependTo($('.member-description', member));
|
||
|
// Clone link, but remove name, id and change href
|
||
|
link.clone().removeAttr('name').removeAttr('id').attr('href', '#').appendTo(header);
|
||
|
// Add closing button.
|
||
|
header.append('<div class="member-close"><input type="button" value="Close"></div>');
|
||
|
});
|
||
|
|
||
|
// Give open / close buttons behavior
|
||
|
$('.reference')
|
||
|
.on('click', '.member-link, .member-close', function() {
|
||
|
return toggleMember($(this).parents('.member'));
|
||
|
})
|
||
|
.on('click', '.member-text a', function() {
|
||
|
if (this.href.match(/^(.*?)\/?#|$/)[1] === document.location.href.match(/^(.*?)\/?(?:#|$)/)[1]) {
|
||
|
toggleMember($(this.href.match(/(#.*)$/)[1]), false, $(this));
|
||
|
return false;
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// DOM-Ready
|
||
|
|
||
|
$(function() {
|
||
|
for (var i in behaviors)
|
||
|
behaviors[i]();
|
||
|
});
|