From 4aa5037f5a21d95b5e9c2ddfc0c8d9f901b78f3b Mon Sep 17 00:00:00 2001 From: fjenett Date: Fri, 3 Jun 2011 19:50:41 +0000 Subject: [PATCH] javascript mode: added server as runner, custom templates, directives support, latest pjs v1.2.1 --- app/src/processing/app/Base.java | 7 +- app/src/processing/app/Editor.java | 2 +- .../mode/javascript/DirectivesEditor.java | 538 ++ .../mode/javascript/JavaScriptBuild.java | 380 +- .../mode/javascript/JavaScriptEditor.java | 259 +- .../mode/javascript/JavaScriptMode.java | 16 +- .../mode/javascript/JavaScriptServer.java | 692 ++ .../mode/javascript/JavaScriptToolbar.java | 47 +- build/build.xml | 2 - javascript/applet_js/processing.js | 6474 +++++++++-------- javascript/theme/buttons.gif | Bin 1231 -> 2671 bytes 11 files changed, 5447 insertions(+), 2970 deletions(-) create mode 100644 app/src/processing/mode/javascript/DirectivesEditor.java create mode 100644 app/src/processing/mode/javascript/JavaScriptServer.java diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index 2990e1af7..f574ff826 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -34,6 +34,7 @@ import javax.swing.tree.*; import processing.core.*; import processing.mode.android.AndroidMode; +import processing.mode.javascript.JavaScriptMode; import processing.mode.java.*; @@ -215,9 +216,9 @@ public class Base { // TODO this will be dynamically loading modes in no time defaultMode = new JavaMode(this, getContentFile("modes/java")); Mode androidMode = new AndroidMode(this, getContentFile("modes/android")); -// Mode javaScriptMode = new JavaScriptMode(this, getContentFile("modes/javascript")); -// modeList = new Mode[] { defaultMode, androidMode, javaScriptMode }; - modeList = new Mode[] { defaultMode, androidMode }; + Mode javaScriptMode = new JavaScriptMode(this, getContentFile("modes/javascript")); + modeList = new Mode[] { defaultMode, androidMode, javaScriptMode }; +// modeList = new Mode[] { defaultMode, androidMode }; // Get the sketchbook path, and make sure it's set properly determineSketchbookFolder(); diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index 7ec43dcc4..92c801309 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -58,7 +58,7 @@ public abstract class Editor extends JFrame implements RunnerListener { /** * true if this file has not yet been given a name by the user */ - boolean untitled; + protected boolean untitled; private PageFormat pageFormat; private PrinterJob printerJob; diff --git a/app/src/processing/mode/javascript/DirectivesEditor.java b/app/src/processing/mode/javascript/DirectivesEditor.java new file mode 100644 index 000000000..dce55f386 --- /dev/null +++ b/app/src/processing/mode/javascript/DirectivesEditor.java @@ -0,0 +1,538 @@ +package processing.mode.javascript; + +import processing.mode.java.JavaEditor; +import processing.app.Base; +import processing.app.Sketch; +import processing.app.SketchCode; + +import processing.core.PApplet; + +import java.awt.*; +import java.awt.event.*; + +import javax.swing.*; +import javax.swing.event.*; +import javax.swing.border.*; + +import java.io.*; + +import java.util.Map; +import java.util.HashMap; +import java.util.Vector; +import java.util.ArrayList; +import java.util.Collections; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +/* http://processingjs.org/reference/pjs%20directive */ + +public class DirectivesEditor +{ + JavaScriptEditor editor; + + static JFrame frame; + JCheckBox crispBox; + JTextField fontField; + JCheckBox globalKeyEventsBox; + JCheckBox pauseOnBlurBox; + JTextField preloadField; + JCheckBox transparentBox; + + private final static ArrayList validKeys = new ArrayList(); + static { + validKeys.add("crisp"); + validKeys.add("font"); + validKeys.add("globalKeyEvents"); + validKeys.add("pauseOnBlur"); + validKeys.add("preload"); + validKeys.add("transparent"); + } + private final static int CRISP = 0; + private final static int FONT = 1; + private final static int GLOBAL_KEY_EVENTS = 2; + private final static int PAUSE_ON_BLUR = 3; + private final static int PRELOAD = 4; + private final static int TRANSPARENT = 5; + + private Pattern pjsPattern; + + public DirectivesEditor ( JavaScriptEditor e ) + { + editor = e; + + if ( frame == null ) createFrame(); + + // see processing-1.2.0.js + pjsPattern = Pattern.compile( + "\\/\\*\\s*@pjs\\s+((?:[^\\*]|\\*+[^\\*\\/])*)\\*\\/\\s*", + Pattern.DOTALL ); + } + + public void show () + { + if ( editor.getSketch().isModified()) + { + Base.showWarning( "Directives Editor", "Please save your sketch before changing the directives.", null); + //editor.statusError("Please save your sketch before changing the directives."); + return; + } + + resetInterface(); + findRemoveDirectives(false); + + frame.setVisible(true); + } + + private void resetInterface () + { + for ( JCheckBox b : new JCheckBox[]{ crispBox, globalKeyEventsBox, pauseOnBlurBox, transparentBox } ) + { + b.setSelected(false); + } + for ( JTextField f : new JTextField[]{ fontField, preloadField } ) + { + f.setText(""); + } + } + + public void hide () + { + frame.setVisible(false); + } + + void applyDirectives () + { + findRemoveDirectives(true); + + StringBuffer buffer = new StringBuffer(); + String head = "", toe = "; \n"; + + if ( crispBox.isSelected() ) + buffer.append( head + "crisp=true" + toe ); + if ( !fontField.getText().trim().equals("") ) + buffer.append( head + "font=\""+fontField.getText().trim()+"\"" + toe ); + if ( globalKeyEventsBox.isSelected() ) + buffer.append( head + "globalKeyEvents=true" + toe ); + if ( pauseOnBlurBox.isSelected() ) + buffer.append( head + "pauseOnBlur=true" + toe ); + if ( !preloadField.getText().trim().equals("") ) + buffer.append( head + "preload=\""+preloadField.getText().trim()+"\"" + toe ); + if ( transparentBox.isSelected() ) + buffer.append( head + "transparent=true" + toe ); + + Sketch sketch = editor.getSketch(); + SketchCode code = sketch.getCode(0); // first tab + if ( buffer.length() > 0 ) + { + code.setProgram( "/* @pjs " + buffer.toString() + " */\n\n" + code.getProgram() ); + if ( sketch.getCurrentCode() == code ) // update textarea if on first tab + { + editor.setText(sketch.getCurrentCode().getProgram()); + editor.setSelection(0,0); + } + + sketch.setModified( false ); + sketch.setModified( true ); + } + } + + void findRemoveDirectives ( boolean clean ) + { + //if ( clean ) editor.startCompoundEdit(); + + Sketch sketch = editor.getSketch(); + for (int i = 0; i < sketch.getCodeCount(); i++) + { + SketchCode code = sketch.getCode(i); + String program = code.getProgram(); + StringBuffer buffer = new StringBuffer(); + + Matcher m = pjsPattern.matcher( program ); + while (m.find()) + { + String mm = m.group(); + + /* remove framing */ + mm = mm.replaceAll("^\\/\\*\\s*@pjs","").replaceAll("\\s*\\*\\/\\s*$",""); + /* fix multiline version without semicolons */ + mm = mm.replaceAll("([^;\\s\\n\\r]+)[\\s]*[\\n\\r]+","$1;"); + mm = mm.replaceAll("\n","").replaceAll("\r",""); + + if ( clean ) + { + m.appendReplacement(buffer, ""); + } + else + { + String[] directives = mm.split(";"); + for ( String d : directives ) + { + //System.out.println(d); + parseDirective(d); + } + } + } + + if ( clean ) + { + m.appendTail(buffer); + + // TODO: not working! + code.setProgram( buffer.toString() ); + code.setModified( true ); + } + } + + if ( clean ) + { + //editor.stopCompoundEdit(); + editor.setText(sketch.getCurrentCode().getProgram()); + sketch.setModified( false ); + sketch.setModified( true ); + } + } + + private void parseDirective ( String directive ) + { + if ( directive == null ) + { + System.err.println( "Directive is null." ); + return; + } + + String[] pair = directive.split("="); + if ( pair == null || pair.length != 2 ) + { + System.err.println("Unable to parse directive: \"" + directive + "\" Ignored."); + return; + } + + String key = pair[0].trim(), value = pair[1].trim(); + + if ( validKeys.indexOf(key) == -1 ) + { + System.err.println("Directive key not recognized: \"" + key + "\" Ignored." ); + return; + } + if ( value.equals("") ) + { + System.err.println("Directive value empty. Ignored."); + return; + } + + value = value.replaceAll("^\"|\"$","").replaceAll("^'|'$",""); + + //System.out.println( key + " = " + value ); + + boolean v; + switch ( validKeys.indexOf(key) ) + { + case CRISP: + v = value.toLowerCase().equals("true"); + crispBox.setSelected(v); + break; + case FONT: + fontField.setText(value); + break; + case GLOBAL_KEY_EVENTS: + v = value.toLowerCase().equals("true"); + globalKeyEventsBox.setSelected(v); + break; + case PAUSE_ON_BLUR: + v = value.toLowerCase().equals("true"); + pauseOnBlurBox.setSelected(v); + break; + case PRELOAD: + preloadField.setText(value); + break; + case TRANSPARENT: + v = value.toLowerCase().equals("true"); + transparentBox.setSelected(v); + break; + } + } + + void createFrame () + { + /* see Preferences.java */ + int GUI_BIG = 13; + int GUI_BETWEEN = 10; + int GUI_SMALL = 6; + int FIELD_SIZE = 30; + + int left = GUI_BIG; + int top = GUI_BIG; + int right = 0; + + Dimension d; + + frame = new JFrame("Directives Editor"); + Container pane = frame.getContentPane(); + pane.setLayout(null); + + JLabel label = new JLabel("Click here to read about directives."); + label.addMouseListener(new MouseListener(){ + public void mouseClicked(MouseEvent e) { + Base.openURL("http://processingjs.org/reference/pjs%20directive"); + } + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + public void mousePressed(MouseEvent e) {} + public void mouseReleased(MouseEvent e) {} + }); + pane.add(label); + d = label.getPreferredSize(); + label.setBounds(left, top, d.width, d.height); + top += d.height + GUI_BETWEEN + GUI_BETWEEN; + + // CRISP + + crispBox = + new JCheckBox("\"crisp\": disable antialiasing for line(), triangle() and rect()"); + pane.add(crispBox); + d = crispBox.getPreferredSize(); + crispBox.setBounds(left, top, d.width + 10, d.height); + right = Math.max(right, left + d.width); + top += d.height + GUI_BETWEEN; + + // FONTS + + label = new JLabel("\"font\": to load: (comma separated)"); + pane.add(label); + d = label.getPreferredSize(); + label.setBounds(left, top, d.width, d.height); + top += d.height + GUI_SMALL; + + fontField = new JTextField(FIELD_SIZE); + pane.add(fontField); + d = fontField.getPreferredSize(); + fontField.setBounds(left, top, d.width, d.height); + + JButton button = new JButton("scan"); + button.addActionListener(new ActionListener(){ + public void actionPerformed (ActionEvent e) { + handleScanFonts(); + } + }); + pane.add(button); + Dimension d2 = button.getPreferredSize(); + button.setBounds(left + d.width + GUI_SMALL, top, d2.width, d2.height); + right = Math.max(right, left + d.width + GUI_SMALL + d2.width); + top += d.height + GUI_BETWEEN; + + // GLOBAL_KEY_EVENTS + + globalKeyEventsBox = + new JCheckBox("\"globalKeyEvents\": receive global key events"); + pane.add(globalKeyEventsBox); + d = globalKeyEventsBox.getPreferredSize(); + globalKeyEventsBox.setBounds(left, top, d.width + 10, d.height); + right = Math.max(right, left + d.width); + top += d.height + GUI_BETWEEN; + + // PAUSE_ON_BLUR + + pauseOnBlurBox = + new JCheckBox("\"pauseOnBlur\": pause if applet loses focus"); + pane.add(pauseOnBlurBox); + d = pauseOnBlurBox.getPreferredSize(); + pauseOnBlurBox.setBounds(left, top, d.width + 10, d.height); + right = Math.max(right, left + d.width); + top += d.height + GUI_BETWEEN; + + // PRELOAD images + + label = new JLabel("\"preload\": images (comma separated)"); + pane.add(label); + d = label.getPreferredSize(); + label.setBounds(left, top, d.width, d.height); + top += d.height + GUI_SMALL; + + preloadField = new JTextField(FIELD_SIZE); + pane.add(preloadField); + d = preloadField.getPreferredSize(); + preloadField.setBounds(left, top, d.width, d.height); + + button = new JButton("scan"); + button.addActionListener(new ActionListener(){ + public void actionPerformed (ActionEvent e) { + handleScanImages(); + } + }); + pane.add(button); + d2 = button.getPreferredSize(); + button.setBounds(left + d.width + GUI_SMALL, top, d2.width, d2.height); + right = Math.max(right, left + d.width + GUI_SMALL + d2.width); + top += d.height + GUI_BETWEEN; + + // TRANSPARENT + + transparentBox = + new JCheckBox("\"transparent\": set applet background to be transparent"); + pane.add(transparentBox); + d = transparentBox.getPreferredSize(); + transparentBox.setBounds(left, top, d.width + 10, d.height); + right = Math.max(right, left + d.width); + top += d.height + GUI_BETWEEN; + + // APPLY / OK + + button = new JButton("OK"); + button.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + applyDirectives(); + hide(); + } + }); + pane.add(button); + d2 = button.getPreferredSize(); + int BUTTON_HEIGHT = d2.height; + int BUTTON_WIDTH = 80; + + int h = right - (BUTTON_WIDTH + GUI_SMALL + BUTTON_WIDTH); + button.setBounds(h, top, BUTTON_WIDTH, BUTTON_HEIGHT); + h += BUTTON_WIDTH + GUI_SMALL; + + button = new JButton("Cancel"); + button.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + hide(); + } + }); + pane.add(button); + button.setBounds(h, top, BUTTON_WIDTH, BUTTON_HEIGHT); + + top += BUTTON_HEIGHT + GUI_BETWEEN; + + //frame.getContentPane().add(box); + frame.pack(); + Insets insets = frame.getInsets(); + frame.setSize(right + GUI_BIG + insets.left + insets.right, + top + GUI_SMALL + insets.top + insets.bottom); + + //frame.setResizable(false); + + frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + frame.setVisible(false); + } + }); + Base.registerWindowCloseKeys(frame.getRootPane(), new ActionListener() { + public void actionPerformed(ActionEvent actionEvent) { + frame.setVisible(false); + } + }); + Base.setIcon(frame); + } + + void handleScanFonts() + { + handleScanFiles( fontField, new String[]{ + "woff", "svg", "eot", "ttf", "otf" + }); + } + + void handleScanImages() + { + handleScanFiles( preloadField, new String[]{ + "gif", "jpg", "jpeg", "png", "tga" + }); + } + + void handleScanFiles ( JTextField field, String[] extensions ) + { + String[] dataFiles = scanDataFolderForFilesByType(extensions); + if ( dataFiles == null || dataFiles.length == 0 ) + return; + + String[] oldFileList = field.getText().trim().split(","); + ArrayList newFileList = new ArrayList(); + for ( String c : oldFileList ) + { + c = c.trim(); + if ( !c.equals("") && newFileList.indexOf(c) == -1 ) // TODO check exists() here? + { + newFileList.add( c ); + } + } + for ( String c : dataFiles ) + { + c = c.trim(); + if ( !c.equals("") && newFileList.indexOf(c) == -1 ) + { + newFileList.add( c ); + } + } + Collections.sort(newFileList); + String finalFileList = ""; int i = 0; + for ( String s : newFileList ) + { + finalFileList += (i > 0 ? ", " : "") + s; + i++; + } + field.setText(finalFileList); + } + + String[] scanDataFolderForFilesByType ( String[] extensions ) + { + ArrayList files = new ArrayList(); + File dataFolder = editor.getSketch().getDataFolder(); + + if ( !dataFolder.exists() ) return null; // TODO no folder present .. warn? + + for ( String ext : extensions ) + { + String[] found = listFiles(dataFolder, true, ext); + if ( found == null || found.length == 0 ) continue; + + for ( String f : found ) + { + if ( files.indexOf(f) == -1 ) + files.add(f); + } + } + + return (String[])files.toArray(new String[0]); + } + + // #718 + // http://code.google.com/p/processing/issues/detail?id=718 + private String[] listFiles(File folder, boolean relative, String extension) { + String path = folder.getAbsolutePath(); + Vector vector = new Vector(); + if (extension != null) { + if (!extension.startsWith(".")) { + extension = "." + extension; + } + } + listFiles(relative ? (path + File.separator) : "", path, extension, vector); + String outgoing[] = new String[vector.size()]; + vector.copyInto(outgoing); + return outgoing; + } + + + private void listFiles(String basePath, + String path, String extension, + Vector vector) { + File folder = new File(path); + String[] list = folder.list(); + if (list != null) { + for (String item : list) { + if (item.charAt(0) == '.') continue; + File file = new File(path, item); + String newPath = file.getAbsolutePath(); + if (newPath.startsWith(basePath)) { + newPath = newPath.substring(basePath.length()); + } + if (extension == null || item.toLowerCase().endsWith(extension)) { + vector.add(newPath); + } + if (file.isDirectory()) { + listFiles(basePath, file.getAbsolutePath(), extension, vector); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/processing/mode/javascript/JavaScriptBuild.java b/app/src/processing/mode/javascript/JavaScriptBuild.java index ea3796550..76f2f97e9 100644 --- a/app/src/processing/mode/javascript/JavaScriptBuild.java +++ b/app/src/processing/mode/javascript/JavaScriptBuild.java @@ -4,33 +4,41 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.PrintWriter; +import java.io.FileNotFoundException; +import java.io.FileWriter; + import java.util.HashMap; import java.util.Map; +import java.util.ArrayList; import processing.app.Base; import processing.app.Mode; import processing.app.Sketch; import processing.app.SketchCode; +import processing.app.SketchException; + import processing.core.PApplet; + import processing.mode.java.JavaBuild; +import processing.mode.java.preproc.PdePreprocessor; +import processing.mode.java.preproc.PreprocessorResult; -/** - * A collection of static methods to aid in building a - * web page with Processing.js from a Sketch object. - */ -public class JavaScriptBuild { - - // public static final String SIZE_REGEX - - // public static final String PACKAGE_REGEX +public class JavaScriptBuild +{ + public final static String TEMPLATE_FOLDER_NAME = "template_js"; + public final static String EXPORTED_FOLDER_NAME = "applet_js"; + public final static String TEMPLATE_FILE_NAME = "template.html"; /** * Answers with the first java doc style comment in the string, * or an empty string if no such comment can be found. */ - public static String getDocString(String s) { + public static String getDocString ( String s ) + { String[] javadoc = PApplet.match(s, "/\\*{2,}(.*?)\\*+/"); - if (javadoc != null) { + + if (javadoc != null) + { StringBuffer dbuffer = new StringBuffer(); String[] pieces = PApplet.split(javadoc[1], '\n'); for (String line : pieces) { @@ -58,9 +66,11 @@ public class JavaScriptBuild { * @param args template keys, data values to replace them * @throws IOException when there are problems writing to or from the files */ - public static void writeTemplate(File template, File output, Map fields) throws IOException { + public static void writeTemplate ( File template, File output, Map fields ) + throws IOException + { BufferedReader reader = PApplet.createReader(template); - PrintWriter theOutWriter = PApplet.createWriter(output); + PrintWriter writer = PApplet.createWriter(output); String line = null; while ((line = reader.readLine()) != null) { @@ -80,9 +90,9 @@ public class JavaScriptBuild { } line = sb.toString(); } - theOutWriter.println(line); + writer.println(line); } - theOutWriter.close(); + writer.close(); } // ----------------------------------------------------- @@ -100,8 +110,8 @@ public class JavaScriptBuild { protected File binFolder; - - public JavaScriptBuild(Sketch sketch) { + public JavaScriptBuild ( Sketch sketch ) + { this.sketch = sketch; this.mode = sketch.getMode(); } @@ -116,32 +126,45 @@ public class JavaScriptBuild { * 3. cp -r sketch/data/* bin/ (p.js doesn't recognize the data folder) * 4. series of greps to find height, width, name, desc * 5. cat template.html | sed 's/@@sketch@@/[name]/g' ... [many sed filters] > bin/index.html + *

* * @param bin the output folder for the built sketch * @return boolean whether the build was successful */ - public boolean build(File bin) { + public boolean build ( File bin ) + { // make sure the user isn't playing "hide-the-sketch-folder" again sketch.ensureExistence(); this.binFolder = bin; - if (bin.exists()) { + if ( bin.exists() ) + { Base.removeDescendants(bin); } //else will be created during preprocesss - try { + // pass through preprocessor to catch syntax errors + try + { preprocess(bin); - } catch (IOException e) { - final String msg = "A problem occured while writing to the output folder."; - Base.showWarning("Could not build the sketch", msg, e); - return false; - } - // move the data files - if (sketch.hasDataFolder()) { + } catch ( IOException ioe ) { + final String msg = "A problem occured while writing to the output folder."; + Base.showWarning("Could not build the sketch", msg, ioe); + return false; + + } catch ( SketchException se ) { + final String msg = "The preprocessor found a problem in your code."; + Base.showWarning("Could not build the sketch", msg, se); + return false; + } + + // move the data files, copies contents of sketch/data/ to applet_js/ + if (sketch.hasDataFolder()) + { try { Base.copyDir(sketch.getDataFolder(), bin); + } catch (IOException e) { final String msg = "An exception occured while trying to copy the data folder. " + "You may have to manually move the contents of sketch/data to " + @@ -151,20 +174,52 @@ public class JavaScriptBuild { } } - // TODO Code folder contents ending in .js could be moved and added as script tags? + // as .js files are allowed now include these into the mix, + // first find 'em .. + String[] sketchFolderFilesRaw = sketch.getFolder().list(); + String[] sketchFolderFiles = new String[0]; + ArrayList sffList = new ArrayList(); + if ( sketchFolderFilesRaw != null ) + { + for ( String s : sketchFolderFilesRaw ) + { + if ( s.toLowerCase().startsWith(".") ) continue; + if ( !s.toLowerCase().endsWith(".js") ) continue; + sffList.add(s); + } + if ( sffList.size() > 0 ) + sketchFolderFiles = (String[])sffList.toArray(new String[0]); + } + for ( String s : sketchFolderFiles ) + { + try { + Base.copyFile( new File(sketch.getFolder(), s), new File(bin, s) ); + } catch ( IOException ioe ) { + String msg = "Unable to copy file: "+s; + Base.showWarning("Problem building the sketch", msg, ioe); + return false; + } + } // get width and height int wide = PApplet.DEFAULT_WIDTH; int high = PApplet.DEFAULT_HEIGHT; + // TODO + // Really scrub comments from code? + // Con: larger files, PJS needs to do it later + // Pro: being literate as we are in a script language. String scrubbed = JavaBuild.scrubComments(sketch.getCode(0).getProgram()); String[] matches = PApplet.match(scrubbed, JavaBuild.SIZE_REGEX); - if (matches != null) { - try { + if (matches != null) + { + try + { wide = Integer.parseInt(matches[1]); high = Integer.parseInt(matches[2]); // renderer + } catch (NumberFormatException e) { // found a reference to size, but it didn't seem to contain numbers final String message = @@ -177,21 +232,47 @@ public class JavaScriptBuild { } } // else no size() command found, defaults will be used - // final prep and write to template - File templateFile = sketch.getMode().getContentFile("applet_js/template.html"); + // final prep and write to template. + // getTemplateFile() is very important as it looks and preps + // any custom templates present in the sketch folder. + File templateFile = getTemplateFile(); File htmlOutputFile = new File(bin, "index.html"); - + Map templateFields = new HashMap(); - templateFields.put("width", String.valueOf(wide)); - templateFields.put("height", String.valueOf(high)); - templateFields.put("sketch", sketch.getName()); - templateFields.put("description", getSketchDescription()); - templateFields.put("source", - "" + - sketch.getName() + ""); + templateFields.put( "width", String.valueOf(wide) ); + templateFields.put( "height", String.valueOf(high) ); + templateFields.put( "sketch", sketch.getName() ); + templateFields.put( "description", getSketchDescription() ); - try{ + // generate an ID for the sketch to use with + String sketchID = sketch.getName().replaceAll("[^a-zA-Z0-9]+", "").replaceAll("^[^a-zA-Z]+",""); + // add a handy method to read the generated sketchID + String scriptFiles = "\n"; + + // main .pde file first + String sourceFiles = "" + + sketch.getName() + " "; + + // add all other files (both types: .pde and .js) + if ( sketchFolderFiles != null ) + { + for ( String s : sketchFolderFiles ) + { + sourceFiles += "" + s + " "; + scriptFiles += "\n"; + } + } + templateFields.put( "source", sourceFiles ); + templateFields.put( "scripts", scriptFiles ); + templateFields.put( "id", sketchID ); + + // process template replace tokens with content + try + { writeTemplate(templateFile, htmlOutputFile, templateFields); + } catch (IOException ioe) { final String msg = "There was a problem writing the html template " + "to the build folder."; @@ -200,9 +281,14 @@ public class JavaScriptBuild { } // finally, add Processing.js - try { - Base.copyFile(sketch.getMode().getContentFile("applet_js/processing.js"), - new File(bin, "processing.js")); + try + { + Base.copyFile( sketch.getMode().getContentFile( + EXPORTED_FOLDER_NAME+"/processing.js" + ), + new File( bin, "processing.js") + ); + } catch (IOException ioe) { final String msg = "There was a problem copying processing.js to the " + "build folder. You will have to manually add " + @@ -214,14 +300,59 @@ public class JavaScriptBuild { return true; } + + /** + * Find and return the template HTML file to use. This also checks for custom + * templates that might be living in the sketch folder. If such a "template_js" + * folder exists then it's contents will be copied over to "applet_js" and + * it's template.html will be used as template. + */ + private File getTemplateFile () + { + File sketchFolder = sketch.getFolder(); + File customTemplateFolder = new File( sketchFolder, TEMPLATE_FOLDER_NAME ); + if ( customTemplateFolder.exists() && + customTemplateFolder.isDirectory() && + customTemplateFolder.canRead() ) + { + File appletJsFolder = new File( sketchFolder, EXPORTED_FOLDER_NAME ); + + try { + //TODO: this is potentially dangerous as it might override files in applet_js + Base.copyDir( customTemplateFolder, appletJsFolder ); + if ( !(new File( appletJsFolder, TEMPLATE_FILE_NAME )).delete() ) + { + // ignore? + } + return new File( customTemplateFolder, TEMPLATE_FILE_NAME ); + } catch ( Exception e ) { + String msg = ""; + Base.showWarning("There was a problem copying your custom template folder", msg, e); + return sketch.getMode().getContentFile( + EXPORTED_FOLDER_NAME + File.separator + TEMPLATE_FILE_NAME + ); + } + } + else + return sketch.getMode().getContentFile( + EXPORTED_FOLDER_NAME + File.separator + TEMPLATE_FILE_NAME + ); + } /** - * Prepares the sketch code objects for use with Processing.js + * Collects the sketch code and runs it by the Java-mode preprocessor + * to fish for errors. + * * @param bin the output folder + * + * @see processing.mode.java.JavaBuild#preprocess(java.io.File) */ - public void preprocess(File bin) throws IOException { + public void preprocess ( File bin ) throws IOException, SketchException + { + // COLLECT .pde FILES INTO ONE, // essentially... cat sketchFolder/*.pde > bin/sketchname.pde + StringBuffer bigCode = new StringBuffer(); for (SketchCode sc : sketch.getCode()){ if (sc.isExtension("pde")) { @@ -234,9 +365,150 @@ public class JavaScriptBuild { bin.mkdirs(); } File bigFile = new File(bin, sketch.getName() + ".pde"); - Base.saveFile(bigCode.toString(), bigFile); + String bigCodeContents = bigCode.toString(); + Base.saveFile( bigCodeContents, bigFile ); + + // RUN THROUGH JAVA-MODE PREPROCESSOR, + // some minor changes made since we are not running the result + // but are only interested in any possible errors that may + // surface + + PdePreprocessor preprocessor = new PdePreprocessor( sketch.getName() ); + PreprocessorResult result; + + try + { + File outputFolder = sketch.makeTempFolder(); + final File java = new File( outputFolder, sketch.getName() + ".java" ); + final PrintWriter stream = new PrintWriter( new FileWriter(java) ); + try { + result = preprocessor.write( stream, bigCodeContents, null ); + } finally { + stream.close(); + } + + } catch (FileNotFoundException fnfe) { + fnfe.printStackTrace(); + String msg = "Build folder disappeared or could not be written"; + throw new SketchException(msg); + + } catch (antlr.RecognitionException re) { + // re also returns a column that we're not bothering with for now + + // first assume that it's the main file + int errorLine = re.getLine() - 1; + + // then search through for anyone else whose preprocName is null, + // since they've also been combined into the main pde. + int errorFile = findErrorFile(errorLine); + errorLine -= sketch.getCode(errorFile).getPreprocOffset(); + + String msg = re.getMessage(); + + if (msg.equals("expecting RCURLY, found 'null'")) { + // This can be a problem since the error is sometimes listed as a line + // that's actually past the number of lines. For instance, it might + // report "line 15" of a 14 line program. Added code to highlightLine() + // inside Editor to deal with this situation (since that code is also + // useful for other similar situations). + throw new SketchException("Found one too many { characters " + + "without a } to match it.", + errorFile, errorLine, re.getColumn()); + } + + if (msg.indexOf("expecting RBRACK") != -1) { + System.err.println(msg); + throw new SketchException("Syntax error, " + + "maybe a missing ] character?", + errorFile, errorLine, re.getColumn()); + } + + if (msg.indexOf("expecting SEMI") != -1) { + System.err.println(msg); + throw new SketchException("Syntax error, " + + "maybe a missing semicolon?", + errorFile, errorLine, re.getColumn()); + } + + if (msg.indexOf("expecting RPAREN") != -1) { + System.err.println(msg); + throw new SketchException("Syntax error, " + + "maybe a missing right parenthesis?", + errorFile, errorLine, re.getColumn()); + } + + if (msg.indexOf("preproc.web_colors") != -1) { + throw new SketchException("A web color (such as #ffcc00) " + + "must be six digits.", + errorFile, errorLine, re.getColumn(), false); + } + + //System.out.println("msg is " + msg); + throw new SketchException(msg, errorFile, + errorLine, re.getColumn()); + + } catch (antlr.TokenStreamRecognitionException tsre) { + // while this seems to store line and column internally, + // there doesn't seem to be a method to grab it.. + // so instead it's done using a regexp + + // TODO not tested since removing ORO matcher.. ^ could be a problem + String mess = "^line (\\d+):(\\d+):\\s"; + + String[] matches = PApplet.match(tsre.toString(), mess); + if (matches != null) { + int errorLine = Integer.parseInt(matches[1]) - 1; + int errorColumn = Integer.parseInt(matches[2]); + + int errorFile = 0; + for (int i = 1; i < sketch.getCodeCount(); i++) { + SketchCode sc = sketch.getCode(i); + if (sc.isExtension("pde") && + (sc.getPreprocOffset() < errorLine)) { + errorFile = i; + } + } + errorLine -= sketch.getCode(errorFile).getPreprocOffset(); + + throw new SketchException(tsre.getMessage(), + errorFile, errorLine, errorColumn); + + } else { + // this is bad, defaults to the main class.. hrm. + String msg = tsre.toString(); + throw new SketchException(msg, 0, -1, -1); + } + + } catch (SketchException pe) { + // RunnerExceptions are caught here and re-thrown, so that they don't + // get lost in the more general "Exception" handler below. + throw pe; + + } catch (Exception ex) { + // TODO better method for handling this? + System.err.println("Uncaught exception type:" + ex.getClass()); + ex.printStackTrace(); + throw new SketchException(ex.toString()); + } + } + + /** + * Copied from JavaBuild as it is protected there. + * @see processing.mode.java.JavaBuild#findErrorFile(int) + */ + protected int findErrorFile ( int errorLine ) + { + for (int i = 1; i < sketch.getCodeCount(); i++) + { + SketchCode sc = sketch.getCode(i); + if (sc.isExtension("pde") && (sc.getPreprocOffset() < errorLine)) + { + // keep looping until the errorLine is past the offset + return i; + } + } + return 0; // i give up } - /** * Parse the sketch to retrieve it's description. Answers with the first @@ -256,9 +528,10 @@ public class JavaScriptBuild { * Export the sketch to the default applet_js folder. * @return success of the operation */ - public boolean export() throws IOException { - File applet_js = new File(sketch.getFolder(), "applet_js"); - return exportApplet_js(applet_js); + public boolean export() throws IOException + { + File applet_js = new File(sketch.getFolder(), EXPORTED_FOLDER_NAME); + return exportToFolder( applet_js ); } @@ -266,7 +539,8 @@ public class JavaScriptBuild { * Export the sketch to the provided folder * @return success of the operation */ - public boolean exportApplet_js(File appletfolder) throws IOException { - return build(appletfolder); + public boolean exportToFolder( File exportFolder ) throws IOException + { + return build( exportFolder ); } } diff --git a/app/src/processing/mode/javascript/JavaScriptEditor.java b/app/src/processing/mode/javascript/JavaScriptEditor.java index d99d2c16f..444cadae8 100644 --- a/app/src/processing/mode/javascript/JavaScriptEditor.java +++ b/app/src/processing/mode/javascript/JavaScriptEditor.java @@ -11,26 +11,49 @@ import javax.swing.JOptionPane; import processing.app.Base; import processing.app.Editor; import processing.app.EditorToolbar; +import processing.app.Sketch; import processing.app.Formatter; import processing.app.Mode; import processing.mode.java.AutoFormat; +import processing.mode.java.JavaEditor; -public class JavaScriptEditor extends Editor { + +import javax.swing.*; + +public class JavaScriptEditor extends Editor +{ + private JavaScriptMode jsMode; + private JavaScriptServer jsServer; - - protected JavaScriptEditor(Base base, String path, int[] location, Mode mode) { + private DirectivesEditor directivesEditor; + + // TODO how to handle multiple servers + // TODO read settings from sketch.properties + // NOTE 0.0.0.0 does not work on XP + private static final String localDomain = "http://127.0.0.1"; + + // tapping into Java mode might not be wanted? + processing.mode.java.PdeKeyListener listener; + + protected JavaScriptEditor ( Base base, String path, int[] location, Mode mode ) + { super(base, path, location, mode); + + listener = new processing.mode.java.PdeKeyListener(this,textarea); + jsMode = (JavaScriptMode) mode; } - public EditorToolbar createToolbar() { + public EditorToolbar createToolbar () + { return new JavaScriptToolbar(this, base); } - public Formatter createFormatter() { + public Formatter createFormatter () + { return new AutoFormat(); } @@ -39,26 +62,79 @@ public class JavaScriptEditor extends Editor { // Menu methods - public JMenu buildFileMenu() { + public JMenu buildFileMenu () + { JMenuItem exportItem = Base.newJMenuItem("export title", 'E'); exportItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - handleExport(); + handleExport( true ); } }); return buildFileMenu(new JMenuItem[] { exportItem }); } - public JMenu buildSketchMenu() { - return buildSketchMenu(new JMenuItem[] {}); + public JMenu buildSketchMenu () + { + JMenuItem startServerItem = Base.newJMenuItem("Start server", 'R'); + startServerItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + handleStartServer(); + } + }); + + JMenuItem stopServerItem = new JMenuItem("Stop server"); + stopServerItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + handleStopServer(); + } + }); + + JMenuItem showDirectivesWindow = new JMenuItem("Directives"); + showDirectivesWindow.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + handleShowDirectivesEditor(); + } + }); + + JMenuItem copyTemplate = new JMenuItem("Copy template to sketch"); + copyTemplate.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Sketch sketch = getSketch(); + File ajs = sketch.getMode().getContentFile(JavaScriptBuild.EXPORTED_FOLDER_NAME); + File tjs = new File( sketch.getFolder(), JavaScriptBuild.TEMPLATE_FOLDER_NAME ); + if ( !tjs.exists() ) + { + try { + Base.copyDir( ajs, tjs ); + statusNotice( "Default template copied." ); + Base.openFolder( tjs ); + } catch ( java.io.IOException ioe ) { + Base.showWarning("Copy default template folder", + "Something went wrong when copying the template folder.", ioe); + } + } + else + statusError( "You need to remove the current "+ + "\""+JavaScriptBuild.TEMPLATE_FOLDER_NAME+"\" "+ + "folder from the sketch." ); + } + }); + + return buildSketchMenu(new JMenuItem[] { + startServerItem, stopServerItem, + showDirectivesWindow, copyTemplate + }); } - public JMenu buildHelpMenu() { + public JMenu buildHelpMenu () + { JMenu menu = new JMenu("Help "); JMenuItem item; + // TODO switch to "http://js.processing.org/"? + item = new JMenuItem("QuickStart for JS Devs"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { @@ -108,13 +184,7 @@ public class JavaScriptEditor extends Editor { item = Base.newJMenuItemShift("Find in Reference", 'F'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - if (textarea.isSelectionActive()) { - Base.openURL( - "http://www.google.com/search?q=" + - textarea.getSelectedText() + - "+site%3Ahttp%3A%2F%2Fprocessingjs.org%2Freference" - ); - } + handleFindReferenceHACK(); } }); menu.add(item); @@ -156,25 +226,129 @@ public class JavaScriptEditor extends Editor { // - - - - - - - - - - - - - - - - - - - public String getCommentPrefix() { + public String getCommentPrefix () + { return "//"; } // - - - - - - - - - - - - - - - - - - + + private void handleShowDirectivesEditor () + { + if ( directivesEditor == null ) + { + directivesEditor = new DirectivesEditor(this); + } + + directivesEditor.show(); + } + + // this catches the textarea right-click events + public void showReference( String filename ) + { + // TODO: catch handleFindReference directly + handleFindReferenceHACK(); + } + + private void handleFindReferenceHACK () + { + if (textarea.isSelectionActive()) { + Base.openURL( + "http://www.google.com/search?q=" + + textarea.getSelectedText().trim() + + "+site%3Ahttp%3A%2F%2Fprocessingjs.org%2Freference" + ); + } + } + + public void handleStartStopServer () + { + if ( jsServer != null && jsServer.isRunning()) + { + handleStopServer(); + } + else + { + handleStartServer(); + } + } + /** + * Replacement for RUN: + * export to folder, start server, open in default browser. + */ + public void handleStartServer () + { + handleExport( false ); + + File serverRoot = new File(sketch.getFolder(), JavaScriptBuild.EXPORTED_FOLDER_NAME); + + // if server hung or something else went wrong .. stop it. + if ( jsServer != null && (!jsServer.isRunning() || !jsServer.getRoot().equals(serverRoot)) ) + { + jsServer.shutDown(); + jsServer = null; + } + + if ( jsServer == null ) + { + jsServer = new JavaScriptServer( serverRoot ); + jsServer.start(); + System.out.println( "Server started." ); + + while ( !jsServer.isRunning() ) {} + + String location = localDomain + ":" + jsServer.getPort() + "/"; + System.out.println( location ); + + if ( !System.getProperty("os.name").startsWith("Mac OS") ) + Base.openURL( location ); + else + { + try { + String scpt = "osascript -e "+ + "\"tell application \\\"Finder\\\" to open location \\\"" + location + "\\\"\""; + String[] cmd = { "/bin/bash", "-c", scpt }; + Process process = new ProcessBuilder( cmd ).start(); + } catch ( Exception e ) { + Base.openURL( location ); + } + } + } + else if ( jsServer.isRunning() ) + { + System.out.println( "Server running, reload your browser window." ); + System.out.println( localDomain + ":" + jsServer.getPort() + "/" ); + } + toolbar.activate(JavaScriptToolbar.RUN); + } + + /** + * Replacement for STOP: stop server. + */ + public void handleStopServer () + { + if ( jsServer != null && jsServer.isRunning() ) + jsServer.shutDown(); + + System.out.println("Server stopped."); + toolbar.deactivate(JavaScriptToolbar.RUN); + } /** * Call the export method of the sketch and handle the gui stuff */ - public void handleExport() { + public void handleExport ( boolean openFolder ) + { if (handleExportCheckModified()) { toolbar.activate(JavaScriptToolbar.EXPORT); try { boolean success = jsMode.handleExport(sketch); - if (success) { - File appletJSFolder = new File(sketch.getFolder(), "applet_js"); + if ( success && openFolder ) { + File appletJSFolder = new File(sketch.getFolder(), JavaScriptBuild.EXPORTED_FOLDER_NAME ); Base.openFolder(appletJSFolder); + statusNotice("Finished exporting."); } else { // error message already displayed by handleExport @@ -186,8 +360,34 @@ public class JavaScriptEditor extends Editor { } } + /** + * Changed from Editor.java to automaticaly export and + * handle the server when it's running. Normal save ops otherwise. + */ + public boolean handleSaveRequest(boolean immediately) + { + if (untitled) { + return handleSaveAs(); + // need to get the name, user might also cancel here + + } else if (immediately) { + handleSave(); + if ( jsServer != null && jsServer.isRunning() ) + handleStartServer(); + } else { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + handleSave(); + if ( jsServer != null && jsServer.isRunning() ) + handleStartServer(); + } + }); + } + return true; + } - public boolean handleExportCheckModified() { + public boolean handleExportCheckModified () + { if (sketch.isModified()) { Object[] options = { "OK", "Cancel" }; int result = JOptionPane.showOptionDialog(this, @@ -215,14 +415,16 @@ public class JavaScriptEditor extends Editor { } - public void handleSave() { + public void handleSave () + { toolbar.activate(JavaScriptToolbar.SAVE); super.handleSave(); toolbar.deactivate(JavaScriptToolbar.SAVE); } - public boolean handleSaveAs() { + public boolean handleSaveAs () + { toolbar.activate(JavaScriptToolbar.SAVE); boolean result = super.handleSaveAs(); toolbar.deactivate(JavaScriptToolbar.SAVE); @@ -230,18 +432,17 @@ public class JavaScriptEditor extends Editor { } - public void handleImportLibrary(String item) { + public void handleImportLibrary (String item) + { Base.showWarning("Processing.js doesn't support libraries", "Libraries are not supported. Import statements are " + "ignored, and code relying on them will break.", null); } - /** JavaScript mode has no runner. This method is empty. */ - public void internalCloseRunner() { } - + public void internalCloseRunner () { } /** JavaScript mode does not run anything. This method is empty. */ - public void deactivateRun() { } + public void deactivateRun () { } } diff --git a/app/src/processing/mode/javascript/JavaScriptMode.java b/app/src/processing/mode/javascript/JavaScriptMode.java index 2f5dc29b5..8a869c277 100644 --- a/app/src/processing/mode/javascript/JavaScriptMode.java +++ b/app/src/processing/mode/javascript/JavaScriptMode.java @@ -13,19 +13,16 @@ import processing.app.syntax.PdeKeywords; import processing.core.PApplet; /** - * JS Mode is very simple. Since P.js is dependent on a browser there is - * no runner, just an export so that users can debug in the browser of their - * choice. + * JS Mode for Processing based on Processing.js. Comes with a server as + * replacement for the normal runner. */ public class JavaScriptMode extends Mode { - // create a new editor with the mode public Editor createEditor(Base base, String path, int[] location) { return new JavaScriptEditor(base, path, location, this); } - public JavaScriptMode(Base base, File folder) { super(base, folder); @@ -37,7 +34,6 @@ public class JavaScriptMode extends Mode { } } - protected void loadKeywords() throws IOException { File file = new File(folder, "keywords.txt"); BufferedReader reader = PApplet.createReader(file); @@ -102,16 +98,16 @@ public class JavaScriptMode extends Mode { return "pde"; } - // all file extensions it supports public String[] getExtensions() { - return new String[] {"pde", "pjs"}; + return new String[] {"pde", "js"}; } - public String[] getIgnorable() { return new String[] { - "applet_js" // not sure what color to paint this bike shed + "applet", + "applet_js", + "template_js" }; } diff --git a/app/src/processing/mode/javascript/JavaScriptServer.java b/app/src/processing/mode/javascript/JavaScriptServer.java new file mode 100644 index 000000000..164d8f96b --- /dev/null +++ b/app/src/processing/mode/javascript/JavaScriptServer.java @@ -0,0 +1,692 @@ +package processing.mode.javascript; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * Based on Sun tutorial at: http://bit.ly/fpoHAF + * + * Changed to accept a document root. + */ +class JavaScriptServer implements HttpConstants, Runnable +{ + Thread thread = null; + ServerSocket server = null; + + private ArrayList threads = new ArrayList(); + private int workers = 5; + + private File virtualRoot; + private int timeout = 5000; + private int port = 0; /* using whatever port is available */ + + private boolean running = false, inited = false; + private boolean stopping = false; + + JavaScriptServer ( File root ) + { + if ( virtualRoot == null && root.exists() && root.canRead() ) + { + virtualRoot = root; + } + } + + public File getRoot () + { + return virtualRoot; + } + + public void setRoot ( File root ) + { + virtualRoot = root; + } + + public int getTimeout () + { + return timeout; + } + + public int getPort () + { + return port; + } + + public void start () + { + thread = null; + thread = new Thread(this, "ProcessingJSServer"); + thread.start(); + } + + public void restart () + { + if ( running ) + shutDown(); + + start(); + } + + // http://bit.ly/eA8iGj + public void shutDown () + { + if ( threads != null ) + { + for ( Worker w : threads ) + { + w.stop(); + } + } + + thread = null; + try { + if ( server != null ) + server.close(); + } catch ( Exception e ) {;} + } + + public boolean isRunning () + { + return running && inited; + } + + public void run () + { + if ( virtualRoot == null ) + { + System.err.println( "ProcessingJSServer: virtual root is null." ); + return; + } + + try + { + running = true; + server = new ServerSocket( 0 ); + + port = server.getLocalPort(); + + inited = true; + + while ( thread != null ) + { + Socket s = server.accept(); + + Worker ws = new Worker( virtualRoot ); + ws.setSocket(s); + (new Thread(ws, "ProcessingJSServer Worker")).start(); + } + } catch ( IOException ioe ) { + //ioe.printStackTrace(); + } + running = false; + } +} + + +class Worker extends JavaScriptServer +implements HttpConstants, Runnable +{ + final static int BUF_SIZE = 2048; + + static final byte[] EOL = {(byte)'\r', (byte)'\n' }; + + /* buffer to use for requests */ + byte[] buf; + /* Socket to client we're handling */ + private Socket s; + + private boolean stopping = false; + + Worker ( File root ) + { + super( root ); + buf = new byte[BUF_SIZE]; + s = null; + } + + synchronized void setSocket( Socket s ) + { + this.s = s; + notify(); + } + + void stop () + { + stopping = true; + notify(); + } + + public synchronized void run () + { + while ( true && !stopping ) + { + if (s == null) + { + /* nothing to do */ + try { + wait(); + } catch (InterruptedException e) { + /* should not happen */ + continue; + } + } + if ( s != null && !stopping ) + { + try { + handleClient(); + } catch (Exception e) { + e.printStackTrace(); + } + } + s = null; + return; + } + } + + void handleClient() throws IOException { + InputStream is = new BufferedInputStream(s.getInputStream()); + PrintStream ps = new PrintStream(s.getOutputStream()); + /* we will only block in read for this many milliseconds + * before we fail with java.io.InterruptedIOException, + * at which point we will abandon the connection. + */ + s.setSoTimeout( getTimeout() ); + s.setTcpNoDelay(true); + /* zero out the buffer from last time */ + for (int i = 0; i < BUF_SIZE; i++) { + buf[i] = 0; + } + try { + /* We only support HTTP GET/HEAD, and don't + * support any fancy HTTP options, + * so we're only interested really in + * the first line. + */ + int nread = 0, r = 0; + +outerloop: + while (nread < BUF_SIZE) { + r = is.read(buf, nread, BUF_SIZE - nread); + if (r == -1) { + /* EOF */ + return; + } + int i = nread; + nread += r; + for (; i < nread; i++) { + if (buf[i] == (byte)'\n' || buf[i] == (byte)'\r') { + /* read one line */ + break outerloop; + } + } + } + + /* are we doing a GET or just a HEAD */ + boolean doingGet; + /* beginning of file name */ + int index; + if (buf[0] == (byte)'G' && + buf[1] == (byte)'E' && + buf[2] == (byte)'T' && + buf[3] == (byte)' ') { + doingGet = true; + index = 4; + } else if (buf[0] == (byte)'H' && + buf[1] == (byte)'E' && + buf[2] == (byte)'A' && + buf[3] == (byte)'D' && + buf[4] == (byte)' ') { + doingGet = false; + index = 5; + } else { + /* we don't support this method */ + ps.print("HTTP/1.0 " + HTTP_BAD_METHOD + + " unsupported method type: "); + ps.write(buf, 0, 5); + ps.write(EOL); + ps.flush(); + s.close(); + return; + } + + int i = 0; + /* find the file name, from: + * GET /foo/bar.html HTTP/1.0 + * extract "/foo/bar.html" + */ + for (i = index; i < nread; i++) { + if (buf[i] == (byte)' ') { + break; + } + } + String fname = (new String(buf, 0, index, i-index)).replace('/', File.separatorChar); + if (fname.startsWith(File.separator)) + { + fname = fname.substring(1); + } + fname = java.net.URLDecoder.decode(fname); + File targ = new File( getRoot(), fname ); + if (targ.isDirectory()) + { + File ind = new File(targ, "index.html"); + if (ind.exists()) { + targ = ind; + } + } + boolean OK = printHeaders(targ, ps); + if (doingGet) + { + if (OK) { + sendFile(targ, ps); + } else { + send404(targ, ps); + } + } + } finally { + s.close(); + } + } + + boolean printHeaders(File targ, PrintStream ps) throws IOException { + boolean ret = false; + int rCode = 0; + if (!targ.exists()) { + rCode = HTTP_NOT_FOUND; + ps.print("HTTP/1.0 " + HTTP_NOT_FOUND + " not found"); + ps.write(EOL); + ret = false; + } else { + rCode = HTTP_OK; + ps.print("HTTP/1.0 " + HTTP_OK+" OK"); + ps.write(EOL); + ret = true; + } + //System.out.println( "From " +s.getInetAddress().getHostAddress()+": GET " + + // targ.getAbsolutePath()+" --> "+rCode); + + ps.print("Server: Simple java"); + ps.write(EOL); + ps.print("Date: " + (new Date())); + ps.write(EOL); + if (ret) { + if (!targ.isDirectory()) { + ps.print("Content-length: "+targ.length()); + ps.write(EOL); + ps.print("Last Modified: " + (new + Date(targ.lastModified()))); + ps.write(EOL); + String name = targ.getName(); + int ind = name.lastIndexOf('.'); + String ct = null; + if (ind > 0) { + ct = (String) map.get(name.substring(ind)); + } + if (ct == null) { + ct = "unknown/unknown"; + } + ps.print("Content-type: " + ct); + ps.write(EOL); + } else { + ps.print("Content-type: text/html"); + ps.write(EOL); + } + } + return ret; + } + + void send404(File targ, PrintStream ps) throws IOException { + ps.write(EOL); + ps.write(EOL); + ps.println("Not Found\n\n"+ + "The requested resource was not found.\n"); + } + + void sendFile(File targ, PrintStream ps) throws IOException { + InputStream is = null; + ps.write(EOL); + if (targ.isDirectory()) { + listDirectory(targ, ps); + return; + } else { + is = new FileInputStream(targ.getAbsolutePath()); + } + + try { + int n; + while ((n = is.read(buf)) > 0) { + ps.write(buf, 0, n); + } + } finally { + is.close(); + } + } + + /* mapping of file extensions to content-types */ + static java.util.Hashtable map = new java.util.Hashtable(); + + static { + fillMap(); + } + + static void setSuffix(String k, String v) { + map.put(k, v); + } + + static void fillMap() + { + // this probably can be shortened a lot since this is not a normal server .. + setSuffix("", "content/unknown"); + setSuffix(".3dm", "x-world/x-3dmf"); + setSuffix(".3dmf", "x-world/x-3dmf"); + setSuffix(".ai", "application/pdf"); + setSuffix(".aif", "audio/x-aiff"); + setSuffix(".aifc", "audio/x-aiff"); + setSuffix(".aiff", "audio/x-aiff"); + setSuffix(".asc", "text/plain"); + setSuffix(".asd", "application/astound"); + setSuffix(".asn", "application/astound"); + setSuffix(".atom", "application/atom+xml"); + setSuffix(".au", "audio/basic"); + setSuffix(".avi", "video/x-msvideo"); + setSuffix(".avi", "video/x-msvideo"); + setSuffix(".bcpio", "application/x-bcpio"); + setSuffix(".bin", "application/octet-stream"); + setSuffix(".bmp", "image/bmp"); + setSuffix(".c", "text/plain"); + setSuffix(".c++", "text/plain"); + setSuffix(".cab", "application/x-shockwave-flash"); + setSuffix(".cc", "text/plain"); + setSuffix(".cdf", "application/x-netcdf"); + setSuffix(".cgm", "image/cgm"); + setSuffix(".chm", "application/mshelp"); + setSuffix(".cht", "audio/x-dspeeh"); + setSuffix(".class", "application/octet-stream"); + setSuffix(".cod", "image/cis-cod"); + setSuffix(".com", "application/octet-stream"); + setSuffix(".cpio", "application/x-cpio"); + setSuffix(".cpt", "application/mac-compactpro"); + setSuffix(".csh", "application/x-csh"); + setSuffix(".css", "text/css"); + setSuffix(".csv", "text/comma-separated-values"); + setSuffix(".dcr", "application/x-director"); + setSuffix(".dif", "video/x-dv"); + setSuffix(".dir", "application/x-director"); + setSuffix(".djv", "image/vnd.djvu"); + setSuffix(".djvu", "image/vnd.djvu"); + setSuffix(".dll", "application/octet-stream"); + setSuffix(".dmg", "application/octet-stream"); + setSuffix(".dms", "application/octet-stream"); + setSuffix(".doc", "application/msword"); + setSuffix(".dot", "application/msword"); + setSuffix(".dtd", "application/xml-dtd"); + setSuffix(".dus", "audio/x-dspeeh"); + setSuffix(".dv", "video/x-dv"); + setSuffix(".dvi", "application/x-dvi"); + setSuffix(".dwf", "drawing/x-dwf"); + setSuffix(".dwg", "application/acad"); + setSuffix(".dxf", "application/dxf"); + setSuffix(".dxr", "application/x-director"); + setSuffix(".eps", "application/pdf"); + setSuffix(".es", "audio/echospeech"); + setSuffix(".etx", "text/x-setext"); + setSuffix(".etx", "text/x-setext"); + setSuffix(".evy", "application/x-envoy"); + setSuffix(".exe", "application/octet-stream"); + setSuffix(".ez", "application/andrew-inset"); + setSuffix(".fh4", "image/x-freehand"); + setSuffix(".fh5", "image/x-freehand"); + setSuffix(".fhc", "image/x-freehand"); + setSuffix(".fif", "image/fif"); + setSuffix(".gif", "image/gif"); + setSuffix(".gram", "application/srgs"); + setSuffix(".grxml", "application/srgs+xml"); + setSuffix(".gtar", "application/x-gtar"); + setSuffix(".gtar", "application/x-gtar"); + setSuffix(".gz", "application/gzip"); + setSuffix(".h", "text/plain"); + setSuffix(".hdf", "application/x-hdf"); + setSuffix(".hlp", "application/mshelp"); + setSuffix(".hqx", "application/mac-binhex40"); + setSuffix(".htm", "text/html"); + setSuffix(".html", "text/html"); + setSuffix(".ice", "x-conference/x-cooltalk"); + setSuffix(".ico", "image/x-icon"); + setSuffix(".ics", "text/calendar"); + setSuffix(".ief", "image/ief"); + setSuffix(".ifb", "text/calendar"); + setSuffix(".iges", "model/iges"); + setSuffix(".igs", "model/iges"); + setSuffix(".java", "text/plain"); + setSuffix(".jnlp", "application/x-java-jnlp-file"); + setSuffix(".jp2", "image/jp2"); + setSuffix(".jpe", "image/jpeg"); + setSuffix(".jpeg", "image/jpeg"); + setSuffix(".jpg", "image/jpeg"); + setSuffix(".js", "text/javascript"); + setSuffix(".kar", "audio/midi"); + setSuffix(".latex", "application/x-latex"); + setSuffix(".latex", "application/x-latex"); + setSuffix(".lha", "application/octet-stream"); + setSuffix(".lzh", "application/octet-stream"); + setSuffix(".m3u", "audio/x-mpegurl"); + setSuffix(".m4a", "audio/mp4a-latm"); + setSuffix(".m4b", "audio/mp4a-latm"); + setSuffix(".m4p", "audio/mp4a-latm"); + setSuffix(".m4u", "video/vnd.mpegurl"); + setSuffix(".m4v", "video/x-m4v"); + setSuffix(".mac", "image/x-macpaint"); + setSuffix(".man", "application/x-troff-man"); + setSuffix(".mathml", "application/mathml+xml"); + setSuffix(".mbd", "application/mbedlet"); + setSuffix(".mcf", "image/vasa"); + setSuffix(".me", "application/x-troff-me"); + setSuffix(".mesh", "model/mesh"); + setSuffix(".mid", "audio/midi"); + setSuffix(".midi", "audio/midi"); + setSuffix(".mif", "application/mif"); + setSuffix(".mov", "video/quicktime"); + setSuffix(".movie", "video/x-sgi-movie"); + setSuffix(".mp2", "audio/mpeg"); + setSuffix(".mp3", "audio/mpeg"); + setSuffix(".mp4", "video/mp4"); + setSuffix(".mpe", "video/mpeg"); + setSuffix(".mpeg", "video/mpeg"); + setSuffix(".mpg", "video/mpeg"); + setSuffix(".mpga", "audio/mpeg"); + setSuffix(".ms", "application/x-troff-ms"); + setSuffix(".msh", "model/mesh"); + setSuffix(".mxu", "video/vnd.mpegurl"); + setSuffix(".nc", "application/x-netcdf"); + setSuffix(".nsc", "application/x-nschat"); + setSuffix(".oda", "application/oda"); + setSuffix(".oga", "audio/ogg"); + setSuffix(".ogg", "application/ogg"); + setSuffix(".ogv", "video/ogg"); + setSuffix(".pbm", "image/x-portable-bitmap"); + setSuffix(".pct", "image/pict"); + setSuffix(".pdb", "chemical/x-pdb"); + setSuffix(".pde", "text/plain"); + setSuffix(".pdf", "application/pdf"); + setSuffix(".pgm", "image/x-portable-graymap"); + setSuffix(".pgn", "application/x-chess-pgn"); + setSuffix(".php", "application/x-httpd-php"); + setSuffix(".phtml", "application/x-httpd-php"); + setSuffix(".pic", "image/pict"); + setSuffix(".pict", "image/pict"); + setSuffix(".pl", "text/plain"); + setSuffix(".png", "image/png"); + setSuffix(".pnm", "image/x-portable-anymap"); + setSuffix(".pnt", "image/x-macpaint"); + setSuffix(".pntg", "image/x-macpaint"); + setSuffix(".pot", "application/mspowerpoint"); + setSuffix(".ppm", "image/x-portable-pixmap"); + setSuffix(".pps", "application/mspowerpoint"); + setSuffix(".ppt", "application/mspowerpoint"); + setSuffix(".ppz", "application/mspowerpoint"); + setSuffix(".ps", "application/postscript"); + setSuffix(".ps", "application/postscript"); + setSuffix(".ptlk", "application/listenup"); + setSuffix(".qd3", "x-world/x-3dmf"); + setSuffix(".qd3d", "x-world/x-3dmf"); + setSuffix(".qt", "video/quicktime"); + setSuffix(".qti", "image/x-quicktime"); + setSuffix(".qtif", "image/x-quicktime"); + setSuffix(".ra", "audio/x-pn-realaudio"); + setSuffix(".ra", "audio/x-pn-realaudio"); + setSuffix(".ram", "audio/x-mpeg"); + setSuffix(".ras", "image/cmu-raster"); + setSuffix(".rdf", "application/rdf+xml"); + setSuffix(".rgb", "image/x-rgb"); + setSuffix(".rm", "application/vnd.rn-realmedia"); + setSuffix(".roff", "application/x-troff"); + setSuffix(".rpm", "audio/x-pn-realaudio-plugin"); + setSuffix(".rtc", "application/rtc"); + setSuffix(".rtf", "text/rtf"); + setSuffix(".rtx", "text/richtext"); + setSuffix(".sca", "application/x-supercard"); + setSuffix(".sgm", "text/sgml"); + setSuffix(".sgml", "text/sgml"); + setSuffix(".sh", "application/x-sh"); + setSuffix(".shar", "application/x-shar"); + setSuffix(".shtml", "text/html"); + setSuffix(".silo", "model/mesh"); + setSuffix(".sit", "application/x-stuffit"); + setSuffix(".skd", "application/x-koan"); + setSuffix(".skm", "application/x-koan"); + setSuffix(".skp", "application/x-koan"); + setSuffix(".skt", "application/x-koan"); + setSuffix(".smi", "application/smil"); + setSuffix(".smil", "application/smil"); + setSuffix(".smp", "application/studiom"); + setSuffix(".snd", "audio/basic"); + setSuffix(".so", "application/octet-stream"); + setSuffix(".spc", "text/x-speech"); + setSuffix(".spl", "application/futuresplash"); + setSuffix(".spr", "application/x-sprite"); + setSuffix(".sprite", "application/x-sprite"); + setSuffix(".src", "application/x-wais-source"); + setSuffix(".stream", "audio/x-qt-stream"); + setSuffix(".sv4cpio", "application/x-sv4cpio"); + setSuffix(".sv4crc", "application/x-sv4crc"); + setSuffix(".svg", "image/svg+xml"); + setSuffix(".swf", "application/x-shockwave-flash"); + setSuffix(".t", "application/x-troff"); + setSuffix(".talk", "text/x-speech"); + setSuffix(".tar", "application/x-tar"); + setSuffix(".tbk", "application/toolbook"); + setSuffix(".tcl", "application/x-tcl"); + setSuffix(".tex", "application/x-tex"); + setSuffix(".texi", "application/x-texinfo"); + setSuffix(".texinfo", "text/plain"); + setSuffix(".text", "text/plain"); + setSuffix(".tif", "image/tiff"); + setSuffix(".tiff", "image/tiff"); + setSuffix(".tr", "application/x-troff"); + setSuffix(".troff", "application/x-troff-man"); + setSuffix(".tsi", "audio/tsplayer"); + setSuffix(".tsp", "application/dsptype"); + setSuffix(".tsv", "text/tab-separated-values"); + setSuffix(".tsv", "text/tab-separated-values"); + setSuffix(".txt", "text/plain"); + setSuffix(".ustar", "application/x-ustar"); + setSuffix(".uu", "application/octet-stream"); + setSuffix(".vcd", "application/x-cdlink"); + setSuffix(".viv", "video/vnd.vivo"); + setSuffix(".vivo", "video/vnd.vivo"); + setSuffix(".vmd", "application/vocaltec-media-desc"); + setSuffix(".vmf", "application/vocaltec-media-file"); + setSuffix(".vox", "audio/voxware"); + setSuffix(".vrml", "model/vrml"); + setSuffix(".vts", "workbook/formulaone"); + setSuffix(".vtts", "workbook/formulaone"); + setSuffix(".vxml", "application/voicexml+xml"); + setSuffix(".wav", "audio/x-wav"); + setSuffix(".wav", "audio/x-wav"); + setSuffix(".wbmp", "image/vnd.wap.wbmp"); + setSuffix(".wbmxl", "application/vnd.wap.wbxml"); + setSuffix(".wml", "text/vnd.wap.wml"); + setSuffix(".wmlc", "application/vnd.wap.wmlc"); + setSuffix(".wmls", "text/vnd.wap.wmlscript"); + setSuffix(".wmlsc", "application/vnd.wap.wmlscriptc"); + setSuffix(".wrl", "model/vrml"); + setSuffix(".xbm", "image/x-xbitmap"); + setSuffix(".xht", "application/xhtml+xml"); + setSuffix(".xhtml", "application/xhtml+xml"); + setSuffix(".xla", "application/msexcel"); + setSuffix(".xls", "application/msexcel"); + setSuffix(".xml", "text/xml"); + setSuffix(".xpm", "image/x-xpixmap"); + setSuffix(".xsl", "application/xml"); + setSuffix(".xslt", "application/xslt+xml"); + setSuffix(".xul", "application/vnd.mozilla.xul+xml"); + setSuffix(".xwd", "image/x-windowdump"); + setSuffix(".xyz", "chemical/x-xyz"); + setSuffix(".z", "application/x-compress"); + setSuffix(".zip", "application/zip"); + } + + void listDirectory(File dir, PrintStream ps) throws IOException + { + ps.println("Directory listing

\n"); + ps.println("Parent Directory
\n"); + String[] list = dir.list(); + for (int i = 0; list != null && i < list.length; i++) { + File f = new File(dir, list[i]); + if (f.isDirectory()) { + ps.println(""+list[i]+"/
"); + } else { + ps.println(""+list[i]+"



" + (new Date()) + ""); + } + +} + +interface HttpConstants { + /** 2XX: generally "OK" */ + public static final int HTTP_OK = 200; + public static final int HTTP_CREATED = 201; + public static final int HTTP_ACCEPTED = 202; + public static final int HTTP_NOT_AUTHORITATIVE = 203; + public static final int HTTP_NO_CONTENT = 204; + public static final int HTTP_RESET = 205; + public static final int HTTP_PARTIAL = 206; + + /** 3XX: relocation/redirect */ + public static final int HTTP_MULT_CHOICE = 300; + public static final int HTTP_MOVED_PERM = 301; + public static final int HTTP_MOVED_TEMP = 302; + public static final int HTTP_SEE_OTHER = 303; + public static final int HTTP_NOT_MODIFIED = 304; + public static final int HTTP_USE_PROXY = 305; + + /** 4XX: client error */ + public static final int HTTP_BAD_REQUEST = 400; + public static final int HTTP_UNAUTHORIZED = 401; + public static final int HTTP_PAYMENT_REQUIRED = 402; + public static final int HTTP_FORBIDDEN = 403; + public static final int HTTP_NOT_FOUND = 404; + public static final int HTTP_BAD_METHOD = 405; + public static final int HTTP_NOT_ACCEPTABLE = 406; + public static final int HTTP_PROXY_AUTH = 407; + public static final int HTTP_CLIENT_TIMEOUT = 408; + public static final int HTTP_CONFLICT = 409; + public static final int HTTP_GONE = 410; + public static final int HTTP_LENGTH_REQUIRED = 411; + public static final int HTTP_PRECON_FAILED = 412; + public static final int HTTP_ENTITY_TOO_LARGE = 413; + public static final int HTTP_REQ_TOO_LONG = 414; + public static final int HTTP_UNSUPPORTED_TYPE = 415; + + /** 5XX: server error */ + public static final int HTTP_SERVER_ERROR = 500; + public static final int HTTP_INTERNAL_ERROR = 501; + public static final int HTTP_BAD_GATEWAY = 502; + public static final int HTTP_UNAVAILABLE = 503; + public static final int HTTP_GATEWAY_TIMEOUT = 504; + public static final int HTTP_VERSION = 505; +} + + + diff --git a/app/src/processing/mode/javascript/JavaScriptToolbar.java b/app/src/processing/mode/javascript/JavaScriptToolbar.java index 872bf2f9a..c997fc6f5 100644 --- a/app/src/processing/mode/javascript/JavaScriptToolbar.java +++ b/app/src/processing/mode/javascript/JavaScriptToolbar.java @@ -11,18 +11,22 @@ import processing.app.EditorToolbar; public class JavaScriptToolbar extends EditorToolbar { - // static protected final int RUN = 0; - // static protected final int STOP = 1; + static protected final int RUN = 0; + static protected final int STOP = 1; - static protected final int NEW = 0; - static protected final int OPEN = 1; - static protected final int SAVE = 2; - static protected final int EXPORT = 3; + static protected final int NEW = 2; + static protected final int OPEN = 3; + static protected final int SAVE = 4; + static protected final int EXPORT = 5; - static public String getTitle(int index, boolean shift) { - switch (index) { - case NEW: return !shift ? "New" : "New Editor Window"; + static public String getTitle ( int index, boolean shift ) + { + switch (index) + { + case RUN: return "Start server"; + case STOP: return "Stop server"; + case NEW: return !shift ? "New" : "New Editor Window"; case OPEN: return !shift ? "Open" : "Open in Another Window"; case SAVE: return "Save"; case EXPORT: return "Export for Web"; @@ -31,24 +35,35 @@ public class JavaScriptToolbar extends EditorToolbar { } - public JavaScriptToolbar(Editor editor, Base base) { + public JavaScriptToolbar ( Editor editor, Base base ) + { super(editor, base); } - public void init() { + public void init () + { Image[][] images = loadImages(); - for (int i = 0; i < 4; i++) { - addButton(getTitle(i, false), getTitle(i, true), images[i], i == NEW); + for (int i = 0; i < 6; i++) + { + addButton( getTitle(i, false), getTitle(i, true), images[i], i == NEW ); } } - - public void handlePressed(MouseEvent e, int index) { + public void handlePressed ( MouseEvent e, int index ) + { boolean shift = e.isShiftDown(); JavaScriptEditor jsEditor = (JavaScriptEditor) editor; switch (index) { + + case RUN: + jsEditor.handleStartServer(); + break; + + case STOP: + jsEditor.handleStopServer(); + break; case OPEN: JPopupMenu popup = editor.getMode().getToolbarMenu().getPopupMenu(); @@ -68,7 +83,7 @@ public class JavaScriptToolbar extends EditorToolbar { break; case EXPORT: - jsEditor.handleExport(); + jsEditor.handleExport( true ); break; } } diff --git a/build/build.xml b/build/build.xml index a4e571ffe..69b2c0de4 100644 --- a/build/build.xml +++ b/build/build.xml @@ -134,13 +134,11 @@ --> - diff --git a/javascript/applet_js/processing.js b/javascript/applet_js/processing.js index bc35ab660..e36e9e220 100644 --- a/javascript/applet_js/processing.js +++ b/javascript/applet_js/processing.js @@ -1,26 +1,28 @@ -/* +/*** - P R O C E S S I N G . J S - 1.1.0 + P R O C E S S I N G . J S - 1.2.1 a port of the Processing visualization language - License : MIT - Developer : John Resig: http://ejohn.org - Web Site : http://processingjs.org - Java Version : http://processing.org - Github Repo. : http://github.com/jeresig/processing-js - Bug Tracking : http://processing-js.lighthouseapp.com - Mozilla POW! : http://wiki.Mozilla.org/Education/Projects/ProcessingForTheWeb - Maintained by : Seneca: http://zenit.senecac.on.ca/wiki/index.php/Processing.js - Hyper-Metrix: http://hyper-metrix.com/#Processing - BuildingSky: http://weare.buildingsky.net/pages/processing-js + Processing.js is licensed under the MIT License, see LICENSE. + For a list of copyright holders, please refer to AUTHORS. - */ + http://processingjs.org -(function() { +***/ - var undef; // intentionally left undefined +(function(window, document, Math, nop, undef) { - var ajax = function ajax(url) { + var debug = (function() { + if ("console" in window) { + return function(msg) { + window.console.log('Processing.js: ' + msg); + }; + } else { + return nop(); + } + }()); + + var ajax = function(url) { var xhr = new XMLHttpRequest(); xhr.open("GET", url, false); if (xhr.overrideMimeType) { @@ -36,38 +38,6 @@ var isDOMPresent = ("document" in this) && !("fake" in this.document); /* Browsers fixes start */ - function fixReplaceByRegExp() { - var re = /t/g; - if ("t".replace(re,"") !== null && re.exec("t")) { - return; // it is not necessary - } - var _ie_replace = String.prototype.replace; - String.prototype.replace = function(searchValue, repaceValue) { - var result = _ie_replace.apply(this, arguments); - if (searchValue instanceof RegExp && searchValue.global) { - searchValue.lastIndex = 0; - } - return result; - }; - } - - function fixMatchByRegExp() { - var re = /t/g; - if ("t".match(re) !== null && re.exec("t")) { - return; // it is not necessary - } - var _ie_match = String.prototype.match; - String.prototype.match = function(searchValue) { - var result = _ie_match.apply(this, arguments); - if(searchValue instanceof RegExp && searchValue.global) { - searchValue.lastIndex = 0; - } - return result; - }; - } - fixReplaceByRegExp(); - fixMatchByRegExp(); - (function fixOperaCreateImageData() { try { if (!("createImageData" in CanvasRenderingContext2D.prototype)) { @@ -77,6 +47,36 @@ } } catch(e) {} }()); + + // Typed Arrays: fallback to WebGL arrays or Native JS arrays if unavailable + function setupTypedArray(name, fallback) { + // Check if TypedArray exists, and use if so. + if (name in window) { + return window[name]; + } + + // Check if WebGLArray exists + if (typeof window[fallback] === "function") { + return window[fallback]; + } else { + // Use Native JS array + return function(obj) { + if (obj instanceof Array) { + return obj; + } else if (typeof obj === "number") { + var arr = []; + arr.length = obj; + return arr; + } + }; + } + } + + var Float32Array = setupTypedArray("Float32Array", "WebGLFloatArray"), + Int32Array = setupTypedArray("Int32Array", "WebGLIntArray"), + Uint16Array = setupTypedArray("Uint16Array", "WebGLUnsignedShortArray"), + Uint8Array = setupTypedArray("Uint8Array", "WebGLUnsignedByteArray"); + /* Browsers fixes end */ var PConstants = { @@ -320,8 +320,6 @@ UP: 38, RIGHT: 39, DOWN: 40, - INS: 45, - DEL: 46, F1: 112, F2: 113, F3: 114, @@ -335,6 +333,8 @@ F11: 122, F12: 123, NUMLK: 144, + META: 157, + INSERT: 155, // Cursor types ARROW: 'default', @@ -373,34 +373,6 @@ MAX_LIGHTS: 8 }; - // Typed Arrays: fallback to WebGL arrays or Native JS arrays if unavailable - function setupTypedArray(name, fallback) { - // check if TypedArray exists - // typeof on Minefield and Chrome return function, typeof on Webkit returns object. - if (typeof this[name] !== "function" && typeof this[name] !== "object") { - // nope.. check if WebGLArray exists - if (typeof this[fallback] === "function") { - this[name] = this[fallback]; - } else { - // nope.. set as Native JS array - this[name] = function(obj) { - if (obj instanceof Array) { - return obj; - } else if (typeof obj === "number") { - var arr = []; - arr.length = obj; - return arr; - } - }; - } - } - } - - setupTypedArray("Float32Array", "WebGLFloatArray"); - setupTypedArray("Int32Array", "WebGLIntArray"); - setupTypedArray("Uint16Array", "WebGLUnsignedShortArray"); - setupTypedArray("Uint8Array", "WebGLUnsignedByteArray"); - /** * Returns Java hashCode() result for the object. If the object has the "hashCode" function, * it preforms the call of this function. Otherwise it uses/creates the "$id" property, @@ -452,6 +424,31 @@ } } + /** + * A ObjectIterator is an iterator wrapper for objects. If passed object contains + * the iterator method, the object instance will be replaced by the result returned by + * this method call. If passed object is an array, the ObjectIterator instance iterates + * through its items. + * + * @param {Object} obj The object to be iterated. + */ + var ObjectIterator = function(obj) { + if (obj.iterator instanceof Function) { + return obj.iterator(); + } else if (obj instanceof Array) { + // iterate through array items + var index = -1; + this.hasNext = function() { + return ++index < obj.length; + }; + this.next = function() { + return obj[index]; + }; + } else { + throw "Unable to iterate: " + obj; + } + }; + /** * An ArrayList stores a variable number of objects. * @@ -506,14 +503,25 @@ * @returns {boolean} true if the specified element is present; false otherwise. */ this.contains = function(item) { + return this.indexOf(item)>-1; + }; + /** + * @member ArrayList + * ArrayList.indexOf() Returns the position this element takes in the list, or -1 if the element is not found. + * + * @param {Object} item element whose position in this List is to be tested. + * + * @returns {int} the list position that the first match for this element holds in the list, or -1 if it is not in the list. + */ + this.indexOf = function(item) { for (var i = 0, len = array.length; i < len; ++i) { if (virtEquals(item, array[i])) { - return true; + return i; } } - return false; + return -1; }; - /** + /** * @member ArrayList * ArrayList.add() Adds the specified element to this list. * @@ -538,7 +546,39 @@ throw("Please use the proper number of parameters."); } }; - + /** + * @member ArrayList + * ArrayList.addAll(collection) appends all of the elements in the specified + * Collection to the end of this list, in the order that they are returned by + * the specified Collection's Iterator. + * + * When called as addAll(index, collection) the elements are inserted into + * this list at the position indicated by index. + * + * @param {index} Optional; specifies the position the colletion should be inserted at + * @param {collection} Any iterable object (ArrayList, HashMap.keySet(), etc.) + * @throws out of bounds error for negative index, or index greater than list size. + */ + this.addAll = function(arg1, arg2) { + // addAll(int, Collection) + var it; + if (typeof arg1 === "number") { + if (arg1 < 0 || arg1 > array.length) { + throw("Index out of bounds for addAll: " + arg1 + " greater or equal than " + array.length); + } + it = new ObjectIterator(arg2); + while (it.hasNext()) { + array.splice(arg1++, 0, it.next()); + } + } + // addAll(Collection) + else { + it = new ObjectIterator(arg1); + while (it.hasNext()) { + array.push(it.next()); + } + } + }; /** * @member ArrayList * ArrayList.set() Replaces the element at the specified position in this list with the specified element. @@ -583,15 +623,24 @@ /** * @member ArrayList - * ArrayList.remove() Removes the element at the specified position in this list. - * Shifts any subsequent elements to the left (subtracts one from their indices). + * ArrayList.remove() Removes an element either based on index, if the argument is a number, or + * by equality check, if the argument is an object. * - * @param {int} index the index of the element to removed. + * @param {int|Object} item either the index of the element to be removed, or the element itself. * - * @returns {Object} the element that was removed from the list + * @returns {Object|boolean} If removal is by index, the element that was removed, or null if nothing was removed. If removal is by object, true if removal occurred, otherwise false. */ - this.remove = function(i) { - return array.splice(i, 1)[0]; + this.remove = function(item) { + if (typeof item === 'number') { + return array.splice(item, 1)[0]; + } else { + item = this.indexOf(item); + if (item > -1) { + array.splice(item, 1); + return true; + } + return false; + } }; /** @@ -664,6 +713,10 @@ var count = 0; var hashMap = this; + function getBucketIndex(key) { + var index = virtHashCode(key) % buckets.length; + return index < 0 ? buckets.length + index : index; + } function ensureLoad() { if (count <= loadFactor * buckets.length) { return; @@ -674,10 +727,11 @@ allEntries = allEntries.concat(buckets[i]); } } + var newBucketsLength = buckets.length * 2; buckets = []; - buckets.length = buckets.length * 2; + buckets.length = newBucketsLength; for (var j = 0; j < allEntries.length; ++j) { - var index = virtHashCode(allEntries[j].key) % buckets.length; + var index = getBucketIndex(allEntries[j].key); var bucket = buckets[index]; if (bucket === undef) { buckets[index] = bucket = []; @@ -853,7 +907,7 @@ }; this.containsKey = function(key) { - var index = virtHashCode(key) % buckets.length; + var index = getBucketIndex(key); var bucket = buckets[index]; if (bucket === undef) { return false; @@ -898,7 +952,7 @@ }; this.get = function(key) { - var index = virtHashCode(key) % buckets.length; + var index = getBucketIndex(key); var bucket = buckets[index]; if (bucket === undef) { return null; @@ -932,7 +986,7 @@ }; this.put = function(key, value) { - var index = virtHashCode(key) % buckets.length; + var index = getBucketIndex(key); var bucket = buckets[index]; if (bucket === undef) { ++count; @@ -968,7 +1022,7 @@ }; this.remove = function(key) { - var index = virtHashCode(key) % buckets.length; + var index = getBucketIndex(key); var bucket = buckets[index]; if (bucket === undef) { return null; @@ -1150,31 +1204,6 @@ return PVector; }()); - /** - * A ObjectIterator is an iterator wrapper for objects. If passed object contains - * the iterator method, the object instance will be replaced by the result returned by - * this method call. If passed object is an array, the ObjectIterator instance iterates - * through its items. - * - * @param {Object} obj The object to be iterated. - */ - var ObjectIterator = function(obj) { - if (obj.iterator instanceof Function) { - return obj.iterator(); - } else if (obj instanceof Array) { - // iterate through array items - var index = -1; - this.hasNext = function() { - return ++index < obj.length; - }; - this.next = function() { - return obj[index]; - }; - } else { - throw "Unable to iterate: " + obj; - } - }; - // Building defaultScope. Changing of the prototype protects // internal Processing code from the changes in defaultScope function DefaultScope() {} @@ -1189,16 +1218,188 @@ //defaultScope.PShape = PShape; // TODO //defaultScope.PShapeSVG = PShapeSVG; // TODO - var Processing = this.Processing = function Processing(curElement, aCode) { + + var colors = { + aliceblue: "#f0f8ff", + antiquewhite: "#faebd7", + aqua: "#00ffff", + aquamarine: "#7fffd4", + azure: "#f0ffff", + beige: "#f5f5dc", + bisque: "#ffe4c4", + black: "#000000", + blanchedalmond: "#ffebcd", + blue: "#0000ff", + blueviolet: "#8a2be2", + brown: "#a52a2a", + burlywood: "#deb887", + cadetblue: "#5f9ea0", + chartreuse: "#7fff00", + chocolate: "#d2691e", + coral: "#ff7f50", + cornflowerblue: "#6495ed", + cornsilk: "#fff8dc", + crimson: "#dc143c", + cyan: "#00ffff", + darkblue: "#00008b", + darkcyan: "#008b8b", + darkgoldenrod: "#b8860b", + darkgray: "#a9a9a9", + darkgreen: "#006400", + darkkhaki: "#bdb76b", + darkmagenta: "#8b008b", + darkolivegreen: "#556b2f", + darkorange: "#ff8c00", + darkorchid: "#9932cc", + darkred: "#8b0000", + darksalmon: "#e9967a", + darkseagreen: "#8fbc8f", + darkslateblue: "#483d8b", + darkslategray: "#2f4f4f", + darkturquoise: "#00ced1", + darkviolet: "#9400d3", + deeppink: "#ff1493", + deepskyblue: "#00bfff", + dimgray: "#696969", + dodgerblue: "#1e90ff", + firebrick: "#b22222", + floralwhite: "#fffaf0", + forestgreen: "#228b22", + fuchsia: "#ff00ff", + gainsboro: "#dcdcdc", + ghostwhite: "#f8f8ff", + gold: "#ffd700", + goldenrod: "#daa520", + gray: "#808080", + green: "#008000", + greenyellow: "#adff2f", + honeydew: "#f0fff0", + hotpink: "#ff69b4", + indianred: "#cd5c5c", + indigo: "#4b0082", + ivory: "#fffff0", + khaki: "#f0e68c", + lavender: "#e6e6fa", + lavenderblush: "#fff0f5", + lawngreen: "#7cfc00", + lemonchiffon: "#fffacd", + lightblue: "#add8e6", + lightcoral: "#f08080", + lightcyan: "#e0ffff", + lightgoldenrodyellow: "#fafad2", + lightgrey: "#d3d3d3", + lightgreen: "#90ee90", + lightpink: "#ffb6c1", + lightsalmon: "#ffa07a", + lightseagreen: "#20b2aa", + lightskyblue: "#87cefa", + lightslategray: "#778899", + lightsteelblue: "#b0c4de", + lightyellow: "#ffffe0", + lime: "#00ff00", + limegreen: "#32cd32", + linen: "#faf0e6", + magenta: "#ff00ff", + maroon: "#800000", + mediumaquamarine: "#66cdaa", + mediumblue: "#0000cd", + mediumorchid: "#ba55d3", + mediumpurple: "#9370d8", + mediumseagreen: "#3cb371", + mediumslateblue: "#7b68ee", + mediumspringgreen: "#00fa9a", + mediumturquoise: "#48d1cc", + mediumvioletred: "#c71585", + midnightblue: "#191970", + mintcream: "#f5fffa", + mistyrose: "#ffe4e1", + moccasin: "#ffe4b5", + navajowhite: "#ffdead", + navy: "#000080", + oldlace: "#fdf5e6", + olive: "#808000", + olivedrab: "#6b8e23", + orange: "#ffa500", + orangered: "#ff4500", + orchid: "#da70d6", + palegoldenrod: "#eee8aa", + palegreen: "#98fb98", + paleturquoise: "#afeeee", + palevioletred: "#d87093", + papayawhip: "#ffefd5", + peachpuff: "#ffdab9", + peru: "#cd853f", + pink: "#ffc0cb", + plum: "#dda0dd", + powderblue: "#b0e0e6", + purple: "#800080", + red: "#ff0000", + rosybrown: "#bc8f8f", + royalblue: "#4169e1", + saddlebrown: "#8b4513", + salmon: "#fa8072", + sandybrown: "#f4a460", + seagreen: "#2e8b57", + seashell: "#fff5ee", + sienna: "#a0522d", + silver: "#c0c0c0", + skyblue: "#87ceeb", + slateblue: "#6a5acd", + slategray: "#708090", + snow: "#fffafa", + springgreen: "#00ff7f", + steelblue: "#4682b4", + tan: "#d2b48c", + teal: "#008080", + thistle: "#d8bfd8", + tomato: "#ff6347", + turquoise: "#40e0d0", + violet: "#ee82ee", + wheat: "#f5deb3", + white: "#ffffff", + whitesmoke: "#f5f5f5", + yellow: "#ffff00", + yellowgreen: "#9acd32" + }; + + // Manage multiple Processing instances + var processingInstances = []; + var processingInstanceIds = {}; + + var removeInstance = function(id) { + processingInstances.splice(processingInstanceIds[id], 1); + delete processingInstanceIds[id]; + }; + + var addInstance = function(processing) { + if (processing.externals.canvas.id === undef || !processing.externals.canvas.id.length) { + processing.externals.canvas.id = "__processing" + processingInstances.length; + } + processingInstanceIds[processing.externals.canvas.id] = processingInstances.length; + processingInstances.push(processing); + }; + + + var Processing = this.Processing = function(curElement, aCode) { // Previously we allowed calling Processing as a func instead of ctor, but no longer. if (!(this instanceof Processing)) { throw("called Processing constructor as if it were a function: missing 'new'."); } + function unimplemented(s) { + Processing.debug('Unimplemented - ' + s); + } + // When something new is added to "p." it must also be added to the "names" array. // The names array contains the names of everything that is inside "p." var p = this; + var pgraphicsMode = (arguments.length === 0); + if (pgraphicsMode) { + curElement = document.createElement("canvas"); + p.canvas = curElement; + } + // PJS specific (non-p5) methods and properties to externalize p.externals = { canvas: curElement, @@ -1254,14 +1455,14 @@ // Remapped vars p.__mousePressed = false; p.__keyPressed = false; - p.__frameRate = 0; + p.__frameRate = 60; // The current animation frame p.frameCount = 0; // The height/width of the canvas - p.width = curElement.width - 0; - p.height = curElement.height - 0; + p.width = 100; + p.height = 100; p.defineProperty = function(obj, name, desc) { if("defineProperty" in Object) { @@ -1279,6 +1480,7 @@ // "Private" variables used to maintain state var curContext, curSketch, + drawing, // hold a Drawing2D or Drawing3D object online = true, doFill = true, fillStyle = [1.0, 1.0, 1.0, 1.0], @@ -1300,15 +1502,16 @@ normalMode = PConstants.NORMAL_MODE_AUTO, inDraw = false, curFrameRate = 60, + curMsPerFrame = 1000/curFrameRate, curCursor = PConstants.ARROW, oldCursor = curElement.style.cursor, - curMsPerFrame = 1, curShape = PConstants.POLYGON, curShapeCount = 0, curvePoints = [], curTightness = 0, curveDet = 20, curveInited = false, + backgroundObj = -3355444, // rgb(204, 204, 204) is the default gray background colour bezDetail = 20, colorModeA = 255, colorModeX = 255, @@ -1375,10 +1578,12 @@ isContextReplaced = false, setPixelsCached, maxPixelsCached = 1000, + pressedKeysMap = [], + lastPressedKeyCode = null, codedKeys = [ PConstants.SHIFT, PConstants.CONTROL, PConstants.ALT, PConstants.CAPSLK, PConstants.PGUP, PConstants.PGDN, PConstants.END, PConstants.HOME, PConstants.LEFT, PConstants.UP, PConstants.RIGHT, PConstants.DOWN, PConstants.NUMLK, - PConstants.INS, PConstants.F1, PConstants.F2, PConstants.F3, PConstants.F4, PConstants.F5, PConstants.F6, PConstants.F7, - PConstants.F8, PConstants.F9, PConstants.F10, PConstants.F11, PConstants.F12 ]; + PConstants.INSERT, PConstants.F1, PConstants.F2, PConstants.F3, PConstants.F4, PConstants.F5, PConstants.F6, PConstants.F7, + PConstants.F8, PConstants.F9, PConstants.F10, PConstants.F11, PConstants.F12, PConstants.META ]; // Get padding and border style widths for mouse offsets var stylePaddingLeft, stylePaddingTop, styleBorderLeft, styleBorderTop; @@ -1412,17 +1617,18 @@ modelView, modelViewInv, userMatrixStack, + userReverseMatrixStack, inverseCopy, projection, manipulatingCamera = false, frustumMode = false, cameraFOV = 60 * (Math.PI / 180), - cameraX = curElement.width / 2, - cameraY = curElement.height / 2, + cameraX = p.width / 2, + cameraY = p.height / 2, cameraZ = cameraY / Math.tan(cameraFOV / 2), cameraNear = cameraZ / 10, cameraFar = cameraZ * 10, - cameraAspect = curElement.width / curElement.height; + cameraAspect = p.width / p.height; var vertArray = [], curveVertArray = [], @@ -1434,149 +1640,6 @@ //PShape stuff var curShapeMode = PConstants.CORNER; - var colors = { - aliceblue: "#f0f8ff", - antiquewhite: "#faebd7", - aqua: "#00ffff", - aquamarine: "#7fffd4", - azure: "#f0ffff", - beige: "#f5f5dc", - bisque: "#ffe4c4", - black: "#000000", - blanchedalmond: "#ffebcd", - blue: "#0000ff", - blueviolet: "#8a2be2", - brown: "#a52a2a", - burlywood: "#deb887", - cadetblue: "#5f9ea0", - chartreuse: "#7fff00", - chocolate: "#d2691e", - coral: "#ff7f50", - cornflowerblue: "#6495ed", - cornsilk: "#fff8dc", - crimson: "#dc143c", - cyan: "#00ffff", - darkblue: "#00008b", - darkcyan: "#008b8b", - darkgoldenrod: "#b8860b", - darkgray: "#a9a9a9", - darkgreen: "#006400", - darkkhaki: "#bdb76b", - darkmagenta: "#8b008b", - darkolivegreen: "#556b2f", - darkorange: "#ff8c00", - darkorchid: "#9932cc", - darkred: "#8b0000", - darksalmon: "#e9967a", - darkseagreen: "#8fbc8f", - darkslateblue: "#483d8b", - darkslategray: "#2f4f4f", - darkturquoise: "#00ced1", - darkviolet: "#9400d3", - deeppink: "#ff1493", - deepskyblue: "#00bfff", - dimgray: "#696969", - dodgerblue: "#1e90ff", - firebrick: "#b22222", - floralwhite: "#fffaf0", - forestgreen: "#228b22", - fuchsia: "#ff00ff", - gainsboro: "#dcdcdc", - ghostwhite: "#f8f8ff", - gold: "#ffd700", - goldenrod: "#daa520", - gray: "#808080", - green: "#008000", - greenyellow: "#adff2f", - honeydew: "#f0fff0", - hotpink: "#ff69b4", - indianred: "#cd5c5c", - indigo: "#4b0082", - ivory: "#fffff0", - khaki: "#f0e68c", - lavender: "#e6e6fa", - lavenderblush: "#fff0f5", - lawngreen: "#7cfc00", - lemonchiffon: "#fffacd", - lightblue: "#add8e6", - lightcoral: "#f08080", - lightcyan: "#e0ffff", - lightgoldenrodyellow: "#fafad2", - lightgrey: "#d3d3d3", - lightgreen: "#90ee90", - lightpink: "#ffb6c1", - lightsalmon: "#ffa07a", - lightseagreen: "#20b2aa", - lightskyblue: "#87cefa", - lightslategray: "#778899", - lightsteelblue: "#b0c4de", - lightyellow: "#ffffe0", - lime: "#00ff00", - limegreen: "#32cd32", - linen: "#faf0e6", - magenta: "#ff00ff", - maroon: "#800000", - mediumaquamarine: "#66cdaa", - mediumblue: "#0000cd", - mediumorchid: "#ba55d3", - mediumpurple: "#9370d8", - mediumseagreen: "#3cb371", - mediumslateblue: "#7b68ee", - mediumspringgreen: "#00fa9a", - mediumturquoise: "#48d1cc", - mediumvioletred: "#c71585", - midnightblue: "#191970", - mintcream: "#f5fffa", - mistyrose: "#ffe4e1", - moccasin: "#ffe4b5", - navajowhite: "#ffdead", - navy: "#000080", - oldlace: "#fdf5e6", - olive: "#808000", - olivedrab: "#6b8e23", - orange: "#ffa500", - orangered: "#ff4500", - orchid: "#da70d6", - palegoldenrod: "#eee8aa", - palegreen: "#98fb98", - paleturquoise: "#afeeee", - palevioletred: "#d87093", - papayawhip: "#ffefd5", - peachpuff: "#ffdab9", - peru: "#cd853f", - pink: "#ffc0cb", - plum: "#dda0dd", - powderblue: "#b0e0e6", - purple: "#800080", - red: "#ff0000", - rosybrown: "#bc8f8f", - royalblue: "#4169e1", - saddlebrown: "#8b4513", - salmon: "#fa8072", - sandybrown: "#f4a460", - seagreen: "#2e8b57", - seashell: "#fff5ee", - sienna: "#a0522d", - silver: "#c0c0c0", - skyblue: "#87ceeb", - slateblue: "#6a5acd", - slategray: "#708090", - snow: "#fffafa", - springgreen: "#00ff7f", - steelblue: "#4682b4", - tan: "#d2b48c", - teal: "#008080", - thistle: "#d8bfd8", - tomato: "#ff6347", - turquoise: "#40e0d0", - violet: "#ee82ee", - wheat: "#f5deb3", - white: "#ffffff", - whitesmoke: "#f5f5f5", - yellow: "#ffff00", - yellowgreen: "#9acd32" - }; - // Stores states for pushStyle() and popStyle(). var styleArray = []; @@ -1609,7 +1672,7 @@ // These verts are used for the fill and stroke using TRIANGLE_FAN and LINE_LOOP var rectVerts = new Float32Array([0,0,0, 0,1,0, 1,1,0, 1,0,0]); - var rectNorms = new Float32Array([0,0,-1, 0,0,-1, 0,0,-1, 0,0,-1]); + var rectNorms = new Float32Array([0,0,1, 0,0,1, 0,0,1, 0,0,1]); // Vertex shader for points and lines var vShaderSrcUnlitShape = @@ -1679,6 +1742,8 @@ " }"+ "}"; + var webglMaxTempsWorkaround = /Windows/.test(navigator.userAgent); + // Vertex shader for boxes and spheres var vertexShaderSource3D = "varying vec4 frontColor;" + @@ -1754,7 +1819,7 @@ "void DirectionalLight( inout vec3 col, inout vec3 spec, in vec3 vertNormal, in vec3 ecPos, in Light light ) {" + " float powerfactor = 0.0;" + " float nDotVP = max(0.0, dot( vertNormal, normalize(-light.position) ));" + - " float nDotVH = max(0.0, dot( vertNormal, normalize(-light.position-ecPos )));" + + " float nDotVH = max(0.0, dot( vertNormal, normalize(-light.position-normalize(ecPos) )));" + " if( nDotVP != 0.0 ){" + " powerfactor = pow( nDotVH, shininess );" + @@ -1764,7 +1829,7 @@ " spec += specular * powerfactor;" + "}" + - "void PointLight( inout vec3 col, inout vec3 spec, in vec3 vertNormal, in vec3 ecPos, in vec3 eye, in Light light ) {" + + "void PointLight( inout vec3 col, inout vec3 spec, in vec3 vertNormal, in vec3 ecPos, in Light light ) {" + " float powerfactor;" + // Get the vector from the light to the vertex @@ -1779,7 +1844,7 @@ " float attenuation = 1.0 / ( falloff[0] + ( falloff[1] * d ) + ( falloff[2] * d * d ));" + " float nDotVP = max( 0.0, dot( vertNormal, VP ));" + - " vec3 halfVector = normalize( VP + eye );" + + " vec3 halfVector = normalize( VP - normalize(ecPos) );" + " float nDotHV = max( 0.0, dot( vertNormal, halfVector ));" + " if( nDotVP == 0.0) {" + @@ -1795,7 +1860,7 @@ /* */ - "void SpotLight( inout vec3 col, inout vec3 spec, in vec3 vertNormal, in vec3 ecPos, in vec3 eye, in Light light ) {" + + "void SpotLight( inout vec3 col, inout vec3 spec, in vec3 vertNormal, in vec3 ecPos, in Light light ) {" + " float spotAttenuation;" + " float powerfactor;" + @@ -1813,18 +1878,19 @@ " float spotDot = dot( VP, ldir );" + // if the vertex falls inside the cone - // The following is failing on Windows systems - // removed until we find a workaround - //" if( spotDot < cos( light.angle ) ) {" + - //" spotAttenuation = pow( spotDot, light.concentration );" + - //" }" + - //" else{" + - " spotAttenuation = 1.0;" + - //" }" + + (webglMaxTempsWorkaround ? // Windows reports max temps error if light.angle is used + " spotAttenuation = 1.0; " : + " if( spotDot > cos( light.angle ) ) {" + + " spotAttenuation = pow( spotDot, light.concentration );" + + " }" + + " else{" + + " spotAttenuation = 0.0;" + + " }" + " attenuation *= spotAttenuation;" + + "") + " float nDotVP = max( 0.0, dot( vertNormal, VP ));" + - " vec3 halfVector = normalize( VP + eye );" + + " vec3 halfVector = normalize( VP - normalize(ecPos) );" + " float nDotHV = max( 0.0, dot( vertNormal, halfVector ));" + " if( nDotVP == 0.0 ) {" + @@ -1849,11 +1915,14 @@ " col = aColor;" + " }" + - " vec3 norm = vec3( normalTransform * vec4( Normal, 0.0 ) );" + + // We use the sphere vertices as the normals when we create the sphere buffer. + // But this only works if the sphere vertices are unit length, so we + // have to normalize the normals here. Since this is only required for spheres + // we could consider placing this in a conditional later on. + " vec3 norm = normalize(vec3( normalTransform * vec4( Normal, 0.0 ) ));" + " vec4 ecPos4 = view * model * vec4(Vertex,1.0);" + " vec3 ecPos = (vec3(ecPos4))/ecPos4.w;" + - " vec3 eye = vec3( 0.0, 0.0, 1.0 );" + // If there were no lights this draw call, just use the // assigned fill color of the shape and the specular value @@ -1879,10 +1948,10 @@ " DirectionalLight( finalDiffuse, finalSpecular, norm, ecPos, l );" + " }" + " else if( l.type == 2 ) {" + - " PointLight( finalDiffuse, finalSpecular, norm, ecPos, eye, l );" + + " PointLight( finalDiffuse, finalSpecular, norm, ecPos, l );" + " }" + " else {" + - " SpotLight( finalDiffuse, finalSpecular, norm, ecPos, eye, l );" + + " SpotLight( finalDiffuse, finalSpecular, norm, ecPos, l );" + " }" + " }" + @@ -2102,7 +2171,7 @@ } } - var imageModeCorner = function imageModeCorner(x, y, w, h, whAreSizes) { + var imageModeCorner = function(x, y, w, h, whAreSizes) { return { x: x, y: y, @@ -2112,7 +2181,7 @@ }; var imageModeConvert = imageModeCorner; - var imageModeCorners = function imageModeCorners(x, y, w, h, whAreSizes) { + var imageModeCorners = function(x, y, w, h, whAreSizes) { return { x: x, y: y, @@ -2121,7 +2190,7 @@ }; }; - var imageModeCenter = function imageModeCenter(x, y, w, h, whAreSizes) { + var imageModeCenter = function(x, y, w, h, whAreSizes) { return { x: x - w / 2, y: y - h / 2, @@ -2164,14 +2233,39 @@ return programObject; }; + //////////////////////////////////////////////////////////////////////////// + // 2D/3D drawing handling + //////////////////////////////////////////////////////////////////////////// + // Objects for shared, 2D and 3D contexts + var DrawingShared = function() {}; + var Drawing2D = function() {}; + var Drawing3D = function() {}; + var DrawingPre = function() {}; + + // Setup the prototype chain + Drawing2D.prototype = new DrawingShared(); + Drawing2D.prototype.constructor = Drawing2D; + Drawing3D.prototype = new DrawingShared(); + Drawing3D.prototype.constructor = Drawing3D; + DrawingPre.prototype = new DrawingShared(); + DrawingPre.prototype.constructor = DrawingPre; + + // A no-op function for when the user calls 3D functions from a 2D sketch + // We can change this to a throw or console.error() later if we want + DrawingShared.prototype.a3DOnlyFunction = function(){}; + //////////////////////////////////////////////////////////////////////////// // Char handling //////////////////////////////////////////////////////////////////////////// var charMap = {}; - var Char = p.Character = function Char(chr) { + var Char = p.Character = function(chr) { if (typeof chr === 'string' && chr.length === 1) { this.code = chr.charCodeAt(0); + } else if (typeof chr === 'number') { + this.code = chr; + } else if (chr instanceof Char) { + this.code = chr; } else { this.code = NaN; } @@ -2996,53 +3090,60 @@ * * @return {PMatrix2D} a PMatrix2D */ - PShapeSVG.prototype.parseMatrix = function(str) { - this.checkMatrix(2); - var pieces = []; - str.replace(/\s*(\w+)\((.*?)\)/g, function(all) { - // get a list of transform definitions - pieces.push(p.trim(all)); - }); - if (pieces.length === 0) { - //p.println("Transformation:" + str + " is empty"); - return null; - } - for (var i = 0, j = pieces.length; i < j; i++) { + PShapeSVG.prototype.parseMatrix = (function() { + function getCoords(s) { var m = []; - pieces[i].replace(/\((.*?)\)/, (function() { + s.replace(/\((.*?)\)/, (function() { return function(all, params) { // get the coordinates that can be separated by spaces or a comma m = params.replace(/,+/g, " ").split(/\s+/); }; }())); - - if (pieces[i].indexOf("matrix") !== -1) { - this.matrix.set(m[0], m[2], m[4], m[1], m[3], m[5]); - } else if (pieces[i].indexOf("translate") !== -1) { - var tx = m[0]; - var ty = (m.length === 2) ? m[1] : 0; - this.matrix.translate(tx,ty); - } else if (pieces[i].indexOf("scale") !== -1) { - var sx = m[0]; - var sy = (m.length === 2) ? m[1] : m[0]; - this.matrix.scale(sx,sy); - } else if (pieces[i].indexOf("rotate") !== -1) { - var angle = m[0]; - if (m.length === 1) { - this.matrix.rotate(p.radians(angle)); - } else if (m.length === 3) { - this.matrix.translate(m[1], m[2]); - this.matrix.rotate(p.radians(m[0])); - this.matrix.translate(-m[1], -m[2]); - } - } else if (pieces[i].indexOf("skewX") !== -1) { - this.matrix.skewX(parseFloat(m[0])); - } else if (pieces[i].indexOf("skewY") !== -1) { - this.matrix.skewY(m[0]); - } } - return this.matrix; - }; + + return function(str) { + this.checkMatrix(2); + var pieces = []; + str.replace(/\s*(\w+)\((.*?)\)/g, function(all) { + // get a list of transform definitions + pieces.push(p.trim(all)); + }); + if (pieces.length === 0) { + return null; + } + + for (var i = 0, j = pieces.length; i < j; i++) { + var m = getCoords(pieces[i]); + + if (pieces[i].indexOf("matrix") !== -1) { + this.matrix.set(m[0], m[2], m[4], m[1], m[3], m[5]); + } else if (pieces[i].indexOf("translate") !== -1) { + var tx = m[0]; + var ty = (m.length === 2) ? m[1] : 0; + this.matrix.translate(tx,ty); + } else if (pieces[i].indexOf("scale") !== -1) { + var sx = m[0]; + var sy = (m.length === 2) ? m[1] : m[0]; + this.matrix.scale(sx,sy); + } else if (pieces[i].indexOf("rotate") !== -1) { + var angle = m[0]; + if (m.length === 1) { + this.matrix.rotate(p.radians(angle)); + } else if (m.length === 3) { + this.matrix.translate(m[1], m[2]); + this.matrix.rotate(p.radians(m[0])); + this.matrix.translate(-m[1], -m[2]); + } + } else if (pieces[i].indexOf("skewX") !== -1) { + this.matrix.skewX(parseFloat(m[0])); + } else if (pieces[i].indexOf("skewY") !== -1) { + this.matrix.skewY(m[0]); + } + } + return this.matrix; + }; + }()); + /** * @member PShapeSVG * The parseChildren() function parses the specified XMLElement @@ -3109,17 +3210,19 @@ shape.parsePath(); } else if (name === "radialGradient") { //return new RadialGradient(this, elem); + unimplemented('PShapeSVG.prototype.parseChild, name = radialGradient'); } else if (name === "linearGradient") { //return new LinearGradient(this, elem); + unimplemented('PShapeSVG.prototype.parseChild, name = linearGradient'); } else if (name === "text") { - //p.println("Text in SVG files is not currently supported " + - // "convert text to outlines instead."); + unimplemented('PShapeSVG.prototype.parseChild, name = text'); } else if (name === "filter") { - //p.println("Filters are not supported."); + unimplemented('PShapeSVG.prototype.parseChild, name = filter'); } else if (name === "mask") { - //p.println("Masks are not supported."); + unimplemented('PShapeSVG.prototype.parseChild, name = mask'); } else { - //p.println("Ignoring <" + name + "> tag."); + // ignoring + nop(); } return shape; }; @@ -3470,6 +3573,7 @@ } } else if (valOf === 90) { //Z + nop(); } else if (valOf === 122) { //z this.close = true; } @@ -3581,8 +3685,7 @@ this.vertices.push(verts); } } else { - //p.println("Error parsing polygon points: " + - // "odd number of coordinates provided"); + throw("Error parsing polygon points: odd number of coordinates provided"); } } }; @@ -4107,9 +4210,18 @@ * @param {Integer }lineNr the line in the XML data where the element starts */ var XMLElement = p.XMLElement = function() { + this.attributes = []; + this.children = []; + this.fullName = null; + this.name = null; + this.namespace = ""; + this.content = null; + this.parent = null; + this.lineNr = ""; + this.systemID = ""; + this.type = "ELEMENT"; + if (arguments.length === 4) { - this.attributes = []; - this.children = []; this.fullName = arguments[0] || ""; if (arguments[1]) { this.name = arguments[1]; @@ -4122,46 +4234,15 @@ } } this.namespace = arguments[1]; - this.content = ""; this.lineNr = arguments[3]; this.systemID = arguments[2]; - this.parent = null; } - else if ((arguments.length === 2 && arguments[1].indexOf(".") > -1) ) { // filename or svg xml element - this.attributes = []; - this.children = []; - this.fullName = ""; - this.name = ""; - this.namespace = ""; - this.content = ""; - this.systemID = ""; - this.lineNr = ""; - this.parent = null; + else if ((arguments.length === 2 && arguments[1].indexOf(".") > -1) ) { + // filename or svg xml element this.parse(arguments[arguments.length -1]); } else if (arguments.length === 1 && typeof arguments[0] === "string"){ - //xml string - this.attributes = []; - this.children = []; - this.fullName = ""; - this.name = ""; - this.namespace = ""; - this.content = ""; - this.systemID = ""; - this.lineNr = ""; - this.parent = null; this.parse(arguments[0]); } - else { //empty ctor - this.attributes = []; - this.children = []; - this.fullName = ""; - this.name = ""; - this.namespace = ""; - this.content = ""; - this.systemID = ""; - this.lineNr = ""; - this.parent = null; - } }; /** * XMLElement methods @@ -4171,7 +4252,8 @@ XMLElement.prototype = { /** * @member XMLElement - * The parse() function retrieves the file via ajax() and uses DOMParser() parseFromString method to make an XML document + * The parse() function retrieves the file via ajax() and uses DOMParser() + * parseFromString method to make an XML document * @addon * * @param {String} filename name of the XML/SVG file to load @@ -4198,6 +4280,61 @@ throw(e); } }, + /** + * @member XMLElement + * Internal helper function for parse(). + * Loops through the + * @addon + * + * @param {XMLElement} parent the parent node + * @param {XML document childNodes} elementpath the remaining nodes that need parsing + * + * @return {XMLElement} the new element and its children elements + */ + parseChildrenRecursive: function (parent , elementpath){ + var xmlelement, + xmlattribute, + tmpattrib, + l, m, + child; + if (!parent) { // this element is the root element + this.fullName = elementpath.localName; + this.name = elementpath.nodeName; + xmlelement = this; + } else { // this element has a parent + xmlelement = new XMLElement(elementpath.localName, elementpath.nodeName, "", ""); + xmlelement.parent = parent; + } + + // if this is a text node, return a PCData element, instead of an XML element. + if(elementpath.nodeType === 3 && elementpath.textContent !== "") { + return this.createPCDataElement(elementpath.textContent); + } + + // bind all attributes + for (l = 0, m = elementpath.attributes.length; l < m; l++) { + tmpattrib = elementpath.attributes[l]; + xmlattribute = new XMLAttribute(tmpattrib.getname, + tmpattrib.nodeName, + tmpattrib.namespaceURI, + tmpattrib.nodeValue, + tmpattrib.nodeType); + xmlelement.attributes.push(xmlattribute); + } + + // bind all children + for (l = 0, m = elementpath.childNodes.length; l < m; l++) { + var node = elementpath.childNodes[l]; + if (node.nodeType === 1 || node.nodeType === 3) { // ELEMENT_NODE or TEXT_NODE + child = xmlelement.parseChildrenRecursive(xmlelement, node); + if (child !== null) { + xmlelement.children.push(child); + } + } + } + + return xmlelement; + }, /** * @member XMLElement * The createElement() function Creates an empty element @@ -4214,6 +4351,23 @@ return new XMLElement(arguments[0], arguments[1], arguments[2], arguments[3]); } }, + /** + * @member XMLElement + * The createPCDataElement() function creates an element to be used for #PCDATA content. + * Because Processing discards whitespace TEXT nodes, this method will not build an element + * if the passed content is empty after trimming for whitespace. + * + * @return {XMLElement} new "test" XMLElement, or null if content consists only of whitespace + */ + createPCDataElement: function (content) { + if(content.replace(/^\s+$/g,"") === "") { + return null; + } + var pcdata = new XMLElement(); + pcdata.content = content; + pcdata.type = "TEXT"; + return pcdata; + }, /** * @member XMLElement * The hasAttribute() function returns whether an attribute exists @@ -4232,52 +4386,42 @@ }, /** * @member XMLElement - * The createPCDataElement() function creates an element to be used for #PCDATA content - * - * @return {XMLElement} new XMLElement element - */ - createPCDataElement: function () { - return new XMLElement(); - }, - /** - * @member XMLElement - * The equals() function checks to see if the element being passed in equals another element - * - * @param {Object} rawElement the element to compare to - * - * @return {boolean} true if the element equals another element - */ - equals: function(object){ - if (typeof object === "Object") { - return this.equalsXMLElement(object); - } - }, - /** - * @member XMLElement - * The equalsXMLElement() function checks to see if the XMLElement being passed in equals another XMLElement + * The equals() function checks to see if the XMLElement being passed in equals another XMLElement * * @param {XMLElement} rawElement the element to compare to * * @return {boolean} true if the element equals another element */ - equalsXMLElement: function (object) { - if (object instanceof XMLElement) { - var i, j; - if (this.name !== object.getLocalName()) { return false; } - if (this.attributes.length !== object.getAttributeCount()) { return false; } - for (i = 0, j = this.attributes.length; i < j; i++){ - if (! object.hasAttribute(this.attributes[i].getName(), this.attributes[i].getNamespace())) { return false; } - if (this.attributes[i].getValue() !== object.attributes[i].getValue()) { return false; } - if (this.attributes[i].getType() !== object.attributes[i].getType()) { return false; } - } - if (this.children.length !== object.getChildCount()) { return false; } + equals: function(other) { + if (!(other instanceof XMLElement)) { + return false; + } + var i, j; + if (this.name !== other.getLocalName()) { return false; } + if (this.attributes.length !== other.getAttributeCount()) { return false; } + // attributes may be ordered differently + if (this.attributes.length !== other.attributes.length) { return false; } + var attr_name, attr_ns, attr_value, attr_type, attr_other; + for (i = 0, j = this.attributes.length; i < j; i++) { + attr_name = this.attributes[i].getName(); + attr_ns = this.attributes[i].getNamespace(); + attr_other = other.findAttribute(attr_name, attr_ns); + if (attr_other === null) { return false; } + if (this.attributes[i].getValue() !== attr_other.getValue()) { return false; } + if (this.attributes[i].getType() !== attr_other.getType()) { return false; } + } + // children must be ordered identically + if (this.children.length !== other.getChildCount()) { return false; } + if (this.children.length>0) { var child1, child2; for (i = 0, j = this.children.length; i < j; i++) { - child1 = this.getChildAtIndex(i); - child2 = object.getChildAtIndex(i); - if (! child1.equalsXMLElement(child2)) { return false; } + child1 = this.getChild(i); + child2 = other.getChild(i); + if (!child1.equals(child2)) { return false; } } return true; + } else { + return (this.content === other.content); } }, /** @@ -4287,7 +4431,12 @@ * @return {String} the (possibly null) content */ getContent: function(){ - return this.content; + if (this.type === "TEXT") { + return this.content; } + else if (this.children.length === 1 && this.children[0].type === "TEXT") { + return this.children[0].content; + } + return null; }, /** * @member XMLElement @@ -4344,6 +4493,13 @@ return this.getAttribute(arguments[0], arguments[1],arguments[2]); } }, + /** + * Processing 1.5 XML API wrapper for the generic String + * attribute getter. This may only take one argument. + */ + getString: function(attributeName) { + return this.getStringAttribute(attributeName); + }, /** * @member XMLElement * The getFloatAttribute() function returns the float attribute of the element. @@ -4364,6 +4520,13 @@ return this.getAttribute(arguments[0], arguments[1],arguments[2]); } }, + /** + * Processing 1.5 XML API wrapper for the generic float + * attribute getter. This may only take one argument. + */ + getFloat: function(attributeName) { + return this.getFloatAttribute(attributeName); + }, /** * @member XMLElement * The getIntAttribute() function returns the integer attribute of the element. @@ -4384,6 +4547,13 @@ return this.getAttribute(arguments[0], arguments[1],arguments[2]); } }, + /** + * Processing 1.5 XML API wrapper for the generic int + * attribute getter. This may only take one argument. + */ + getInt: function(attributeName) { + return this.getIntAttribute(attributeName); + }, /** * @member XMLElement * The hasChildren() function returns whether the element has children. @@ -4546,51 +4716,6 @@ } return kidMatches; }, - /** - * @member XMLElement - * Internal helper function for parse(). - * Loops through the - * @addon - * - * @param {XMLElement} parent the parent node - * @param {XML document childNodes} elementpath the remaining nodes that need parsing - * - * @return {XMLElement} the new element and its children elements - */ - parseChildrenRecursive: function (parent , elementpath){ - var xmlelement, - xmlattribute, - tmpattrib, - l, m; - if (!parent) { - this.fullName = elementpath.localName; - this.name = elementpath.nodeName; - this.content = elementpath.textContent || ""; - xmlelement = this; - } else { // a parent - xmlelement = new XMLElement(elementpath.localName, elementpath.nodeName, "", ""); - xmlelement.content = elementpath.textContent || ""; - xmlelement.parent = parent; - } - - for (l = 0, m = elementpath.attributes.length; l < m; l++) { - tmpattrib = elementpath.attributes[l]; - xmlattribute = new XMLAttribute(tmpattrib.getname, - tmpattrib.nodeName, - tmpattrib.namespaceURI, - tmpattrib.nodeValue, - tmpattrib.nodeType); - xmlelement.attributes.push(xmlattribute); - } - - for (l = 0, m = elementpath.childNodes.length; l < m; l++) { - var node = elementpath.childNodes[l]; - if (node.nodeType === 1) { // ELEMENT_NODE type - xmlelement.children.push(xmlelement.parseChildrenRecursive(xmlelement, node)); - } - } - return xmlelement; - }, /** * @member XMLElement * The isLeaf() function returns whether the element is a leaf element. @@ -4639,7 +4764,7 @@ removeChild: function(child) { if (child) { for (var i = 0, j = this.children.length; i < j; i++) { - if (this.children[i].equalsXMLElement(child)) { + if (this.children[i].equals(child)) { this.children.splice(i, 1); break; } @@ -4655,7 +4780,6 @@ removeChildAtIndex: function(index) { if (this.children.length > index) { //make sure its not outofbounds this.children.splice(index, 1); - return; } }, /** @@ -4674,6 +4798,7 @@ return this.attributes[i]; } } + return null; }, /** * @member XMLElement @@ -4687,7 +4812,7 @@ if (arguments.length === 3) { var index = arguments[0].indexOf(':'); var name = arguments[0].substring(index + 1); - attr = this.findAttribute( name, arguments[1] ); + attr = this.findAttribute(name, arguments[1]); if (attr) { attr.setValue(arguments[2]); } else { @@ -4704,6 +4829,27 @@ } } }, + /** + * Processing 1.5 XML API wrapper for the generic String + * attribute setter. This must take two arguments. + */ + setString: function(attribute, value) { + this.setAttribute(attribute, value); + }, + /** + * Processing 1.5 XML API wrapper for the generic int + * attribute setter. This must take two arguments. + */ + setInt: function(attribute, value) { + this.setAttribute(attribute, value); + }, + /** + * Processing 1.5 XML API wrapper for the generic float + * attribute setter. This must take two arguments. + */ + setFloat: function(attribute, value) { + this.setAttribute(attribute, value); + }, /** * @member XMLElement * The setContent() function sets the #PCDATA content. It is an error to call this method with a @@ -4712,6 +4858,8 @@ * @param {String} content the (possibly null) content */ setContent: function(content) { + if (this.children.length>0) { + Processing.debug("Tried to set content for XMLElement with children"); } this.content = content; }, /** @@ -4748,14 +4896,74 @@ getName: function() { return this.fullName; }, + /** + * @member XMLElement + * The getLocalName() function returns the local name (i.e. the name excluding an eventual namespace + * prefix) of the element. + * + * @return {String} the name, or null if the element only contains #PCDATA. + */ getLocalName: function() { return this.name; }, + /** + * @member XMLElement + * The getAttributeCount() function returns the number of attributes for the node + * that this XMLElement represents. + * + * @return {int} the number of attributes in this XMLelement + */ getAttributeCount: function() { return this.attributes.length; - } + }, + /** + * @member XMLElement + * The toString() function returns the XML definition of an XMLElement. + * + * @return {String} the XML definition of this XMLElement + */ + toString: function() { + // shortcut for text nodes + if(this.type==="TEXT") { return this.content; } + + // real XMLElements + var tagstring = (this.namespace !== "" && this.namespace !== this.name ? this.namespace + ":" : "") + this.name; + var xmlstring = "<" + tagstring; + var a,c; + + // serialize the attributes to XML string + for (a = 0; a"; + } + } else { + xmlstring += ">"; + for (c = 0; c"; + } + return xmlstring; + } }; + /** + * static Processing 1.5 XML API wrapper for the + * parse method. This may only take one argument. + */ + XMLElement.parse = function(xmlstring) { + var element = new XMLElement(); + element.parse(xmlstring); + return element; + }; //////////////////////////////////////////////////////////////////////////// // 2D Matrix @@ -4765,7 +4973,7 @@ * in the matrix, then number of digits left of the decimal. * Call from PMatrix2D and PMatrix3D's print() function. */ - var printMatrixHelper = function printMatrixHelper(elements) { + var printMatrixHelper = function(elements) { var big = 0; for (var i = 0; i < elements.length; i++) { if (i !== 0) { @@ -4870,6 +5078,16 @@ translate: function(tx, ty) { this.elements[2] = tx * this.elements[0] + ty * this.elements[1] + this.elements[2]; this.elements[5] = tx * this.elements[3] + ty * this.elements[4] + this.elements[5]; + }, + /** + * @member PMatrix2D + * The invTranslate() function translates this matrix by moving the current coordinates to the negative location specified by tx and ty. + * + * @param {float} tx the x-axis coordinate to move to + * @param {float} ty the y-axis coordinate to move to + */ + invTranslate: function(tx, ty) { + this.translate(-tx, -ty); }, /** * @member PMatrix2D @@ -5012,6 +5230,20 @@ this.elements[3] *= sx; this.elements[4] *= sy; } + }, + /** + * @member PMatrix2D + * The invScale() function decreases or increases the size of a shape by contracting and expanding vertices. When only one parameter is specified scale will occur in all dimensions. + * This is equivalent to a two parameter call. + * + * @param {float} sx the amount to scale on the x-axis + * @param {float} sy the amount to scale on the y-axis + */ + invScale: function(sx, sy) { + if (sx && !sy) { + sy = sx; + } + this.scale(1 / sx, 1 / sy); }, /** * @member PMatrix2D @@ -5102,6 +5334,15 @@ rotateZ: function(angle) { this.rotate(angle); }, + /** + * @member PMatrix2D + * The invRotateZ() function rotates the matrix in opposite direction. + * + * @param {float} angle the angle of rotation in radiants + */ + invRotateZ: function(angle) { + this.rotateZ(angle - Math.PI); + }, /** * @member PMatrix2D * The print() function prints out the elements of this matrix @@ -5122,7 +5363,7 @@ * PMatrix3D is a 4x4 matrix implementation. The constructor accepts another PMatrix3D or a list of six or sixteen float elements. * If no parameters are provided the matrix is set to the identity matrix. */ - var PMatrix3D = p.PMatrix3D = function PMatrix3D() { + var PMatrix3D = p.PMatrix3D = function() { // When a matrix is created, it is set to an identity matrix this.reset(); }; @@ -5209,7 +5450,7 @@ this.elements[15] += tx * this.elements[12] + ty * this.elements[13] + tz * this.elements[14]; }, /** - * @member PMatrix2D + * @member PMatrix3D * The transpose() function transpose this matrix. */ transpose: function() { @@ -5406,7 +5647,7 @@ } }, /** - * @member PMatrix2D + * @member PMatrix3D * The invApply() function applies the inverted matrix to this matrix. * * @param {float} m00 the first element of the matrix @@ -5681,7 +5922,7 @@ * @private * The matrix stack stores the transformations and translations that occur within the space. */ - var PMatrixStack = p.PMatrixStack = function PMatrixStack() { + var PMatrixStack = p.PMatrixStack = function() { this.matrixStack = []; }; @@ -5691,13 +5932,8 @@ * * @param {Object | Array} matrix the matrix to be pushed into the stack */ - PMatrixStack.prototype.load = function load() { - var tmpMatrix; - if (p.use3DContext) { - tmpMatrix = new PMatrix3D(); - } else { - tmpMatrix = new PMatrix2D(); - } + PMatrixStack.prototype.load = function() { + var tmpMatrix = drawing.$newPMatrix(); if (arguments.length === 1) { tmpMatrix.set(arguments[0]); @@ -5707,11 +5943,19 @@ this.matrixStack.push(tmpMatrix); }; + Drawing2D.prototype.$newPMatrix = function() { + return new PMatrix2D(); + }; + + Drawing3D.prototype.$newPMatrix = function() { + return new PMatrix3D(); + }; + /** * @member PMatrixStack * push adds a duplicate of the top of the stack onto the stack - uses the peek function */ - PMatrixStack.prototype.push = function push() { + PMatrixStack.prototype.push = function() { this.matrixStack.push(this.peek()); }; @@ -5721,7 +5965,7 @@ * * @returns {Object} the matrix at the top of the stack */ - PMatrixStack.prototype.pop = function pop() { + PMatrixStack.prototype.pop = function() { return this.matrixStack.pop(); }; @@ -5731,13 +5975,8 @@ * * @returns {Object} the matrix at the top of the stack */ - PMatrixStack.prototype.peek = function peek() { - var tmpMatrix; - if (p.use3DContext) { - tmpMatrix = new PMatrix3D(); - } else { - tmpMatrix = new PMatrix2D(); - } + PMatrixStack.prototype.peek = function() { + var tmpMatrix = drawing.$newPMatrix(); tmpMatrix.set(this.matrixStack[this.matrixStack.length - 1]); return tmpMatrix; @@ -5749,7 +5988,7 @@ * * @param {Object | Array} matrix the matrix to be multiplied into the stack */ - PMatrixStack.prototype.mult = function mult(matrix) { + PMatrixStack.prototype.mult = function(matrix) { this.matrixStack[this.matrixStack.length - 1].apply(matrix); }; @@ -5981,7 +6220,7 @@ */ p.subset = function(array, offset, length) { if (arguments.length === 2) { - return array.slice(offset, array.length - offset); + return array.slice(offset, array.length); } else if (arguments.length === 3) { return array.slice(offset, offset + length); } @@ -6427,7 +6666,7 @@ * * @see colorMode */ - p.color = function color(aValue1, aValue2, aValue3, aValue4) { + p.color = function(aValue1, aValue2, aValue3, aValue4) { // 4 arguments: (R, G, B, A) or (H, S, B, A) if (aValue1 !== undef && aValue2 !== undef && aValue3 !== undef && aValue4 !== undef) { @@ -6600,7 +6839,7 @@ return p.color.toHSB(colInt)[0]; }; - var verifyChannel = function verifyChannel(aColor) { + var verifyChannel = function(aColor) { if (aColor.constructor === Array) { return aColor; } else { @@ -6702,7 +6941,7 @@ * @see blendColor * @see color */ - p.lerpColor = function lerpColor(c1, c2, amt) { + p.lerpColor = function(c1, c2, amt) { // Get RGBA values for Color 1 to floats var colorBits1 = p.color(c1); var r1 = (colorBits1 & PConstants.RED_MASK) >>> 16; @@ -6766,7 +7005,7 @@ * @see fill * @see stroke */ - p.colorMode = function colorMode() { // mode, range1, range2, range3, range4 + p.colorMode = function() { // mode, range1, range2, range3, range4 curColorMode = arguments[0]; if (arguments.length > 1) { colorModeX = arguments[1]; @@ -6866,7 +7105,7 @@ * @see resetMatrix * @see applyMatrix */ - p.printMatrix = function printMatrix() { + p.printMatrix = function() { modelView.print(); }; @@ -6893,13 +7132,15 @@ * @see rotateY * @see rotateZ */ - p.translate = function translate(x, y, z) { - if (p.use3DContext) { - forwardTransform.translate(x, y, z); - reverseTransform.invTranslate(x, y, z); - } else { - curContext.translate(x, y); - } + Drawing2D.prototype.translate = function(x, y) { + forwardTransform.translate(x, y); + reverseTransform.invTranslate(x, y); + curContext.translate(x, y); + }; + + Drawing3D.prototype.translate = function(x, y, z) { + forwardTransform.translate(x, y, z); + reverseTransform.invTranslate(x, y, z); }; /** @@ -6926,13 +7167,15 @@ * @see rotateY * @see rotateZ */ - p.scale = function scale(x, y, z) { - if (p.use3DContext) { - forwardTransform.scale(x, y, z); - reverseTransform.invScale(x, y, z); - } else { - curContext.scale(x, y || x); - } + Drawing2D.prototype.scale = function(x, y) { + forwardTransform.scale(x, y); + reverseTransform.invScale(x, y); + curContext.scale(x, y || x); + }; + + Drawing3D.prototype.scale = function(x, y, z) { + forwardTransform.scale(x, y, z); + reverseTransform.invScale(x, y, z); }; /** @@ -6951,12 +7194,15 @@ * @see rotateY * @see rotateZ */ - p.pushMatrix = function pushMatrix() { - if (p.use3DContext) { - userMatrixStack.load(modelView); - } else { - saveContext(); - } + Drawing2D.prototype.pushMatrix = function() { + userMatrixStack.load(modelView); + userReverseMatrixStack.load(modelViewInv); + saveContext(); + }; + + Drawing3D.prototype.pushMatrix = function() { + userMatrixStack.load(modelView); + userReverseMatrixStack.load(modelViewInv); }; /** @@ -6970,12 +7216,15 @@ * @see popMatrix * @see pushMatrix */ - p.popMatrix = function popMatrix() { - if (p.use3DContext) { - modelView.set(userMatrixStack.pop()); - } else { - restoreContext(); - } + Drawing2D.prototype.popMatrix = function() { + modelView.set(userMatrixStack.pop()); + modelViewInv.set(userReverseMatrixStack.pop()); + restoreContext(); + }; + + Drawing3D.prototype.popMatrix = function() { + modelView.set(userMatrixStack.pop()); + modelViewInv.set(userReverseMatrixStack.pop()); }; /** @@ -6988,13 +7237,15 @@ * @see applyMatrix * @see printMatrix */ - p.resetMatrix = function resetMatrix() { - if (p.use3DContext) { - forwardTransform.reset(); - reverseTransform.reset(); - } else { - curContext.setTransform(1,0,0,1,0,0); - } + Drawing2D.prototype.resetMatrix = function() { + forwardTransform.reset(); + reverseTransform.reset(); + curContext.setTransform(1,0,0,1,0,0); + }; + + Drawing3D.prototype.resetMatrix = function() { + forwardTransform.reset(); + reverseTransform.reset(); }; /** @@ -7011,19 +7262,21 @@ * @see resetMatrix * @see printMatrix */ - p.applyMatrix = function applyMatrix() { + DrawingShared.prototype.applyMatrix = function() { var a = arguments; - if (!p.use3DContext) { - for (var cnt = a.length; cnt < 16; cnt++) { - a[cnt] = 0; - } - a[10] = a[15] = 1; - } - forwardTransform.apply(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], a[14], a[15]); reverseTransform.invApply(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], a[14], a[15]); }; + Drawing2D.prototype.applyMatrix = function() { + var a = arguments; + for (var cnt = a.length; cnt < 16; cnt++) { + a[cnt] = 0; + } + a[10] = a[15] = 1; + DrawingShared.prototype.applyMatrix.apply(this, a); + }; + /** * Rotates a shape around the x-axis the amount specified by the angle parameter. Angles should be * specified in radians (values from 0 to PI*2) or converted to radians with the radians() function. @@ -7076,6 +7329,10 @@ p.rotateZ = function(angleInRadians) { forwardTransform.rotateZ(angleInRadians); reverseTransform.invRotateZ(angleInRadians); + if (p.use3DContext) { + return; + } + curContext.rotate(angleInRadians); }; /** @@ -7128,13 +7385,12 @@ * @see popMatrix * @see pushMatrix */ - p.rotate = function rotate(angleInRadians) { - if (p.use3DContext) { - forwardTransform.rotateZ(angleInRadians); - reverseTransform.invRotateZ(angleInRadians); - } else { - curContext.rotate(angleInRadians); - } + Drawing2D.prototype.rotate = function(angleInRadians) { + p.rotateZ(angleInRadians); + }; + + Drawing3D.prototype.rotate = function(angleInRadians) { + p.rotateZ(angleInRadians); }; /** @@ -7151,7 +7407,7 @@ * * @see popStyle */ - p.pushStyle = function pushStyle() { + p.pushStyle = function() { // Save the canvas state. saveContext(); @@ -7186,7 +7442,7 @@ * * @see pushStyle */ - p.popStyle = function popStyle() { + p.popStyle = function() { var oldState = styleArray.pop(); if (oldState) { @@ -7229,7 +7485,7 @@ * @see day * @see month */ - p.year = function year() { + p.year = function() { return new Date().getFullYear(); }; /** @@ -7245,7 +7501,7 @@ * @see day * @see year */ - p.month = function month() { + p.month = function() { return new Date().getMonth() + 1; }; /** @@ -7261,7 +7517,7 @@ * @see month * @see year */ - p.day = function day() { + p.day = function() { return new Date().getDate(); }; /** @@ -7277,7 +7533,7 @@ * @see day * @see year */ - p.hour = function hour() { + p.hour = function() { return new Date().getHours(); }; /** @@ -7293,7 +7549,7 @@ * @see day * @see year */ - p.minute = function minute() { + p.minute = function() { return new Date().getMinutes(); }; /** @@ -7309,7 +7565,7 @@ * @see day * @see year */ - p.second = function second() { + p.second = function() { return new Date().getSeconds(); }; /** @@ -7325,7 +7581,7 @@ * @see day * @see year */ - p.millis = function millis() { + p.millis = function() { return new Date().getTime() - start; }; @@ -7343,7 +7599,7 @@ * @see noLoop * @see loop */ - p.redraw = function redraw() { + DrawingShared.prototype.redraw = function() { var sec = (new Date().getTime() - timeSinceLastFPS) / 1000; framesSinceLastFPS++; var fps = framesSinceLastFPS / sec; @@ -7356,28 +7612,40 @@ } p.frameCount++; + }; + + Drawing2D.prototype.redraw = function() { + DrawingShared.prototype.redraw.apply(this, arguments); + + curContext.lineWidth = lineWidth; + inDraw = true; + + saveContext(); + p.draw(); + restoreContext(); + + inDraw = false; + }; + + Drawing3D.prototype.redraw = function() { + DrawingShared.prototype.redraw.apply(this, arguments); inDraw = true; - if (p.use3DContext) { - // even if the color buffer isn't cleared with background(), - // the depth buffer needs to be cleared regardless. - curContext.clear(curContext.DEPTH_BUFFER_BIT); - curContextCache = { attributes: {}, locations: {} }; - // Delete all the lighting states and the materials the - // user set in the last draw() call. - p.noLights(); - p.lightFalloff(1, 0, 0); - p.shininess(1); - p.ambient(255, 255, 255); - p.specular(0, 0, 0); - p.camera(); - p.draw(); - } else { - saveContext(); - p.draw(); - restoreContext(); - } + // even if the color buffer isn't cleared with background(), + // the depth buffer needs to be cleared regardless. + curContext.clear(curContext.DEPTH_BUFFER_BIT); + curContextCache = { attributes: {}, locations: {} }; + // Delete all the lighting states and the materials the + // user set in the last draw() call. + p.noLights(); + p.lightFalloff(1, 0, 0); + p.shininess(1); + p.ambient(255, 255, 255); + p.specular(0, 0, 0); + p.emissive(0, 0, 0); + p.camera(); + p.draw(); inDraw = false; }; @@ -7401,7 +7669,7 @@ * @see draw * @see loop */ - p.noLoop = function noLoop() { + p.noLoop = function() { doLoop = false; loopStarted = false; clearInterval(looping); @@ -7415,7 +7683,7 @@ * * @see noLoop */ - p.loop = function loop() { + p.loop = function() { if (loopStarted) { return; } @@ -7447,7 +7715,7 @@ * * @see delay */ - p.frameRate = function frameRate(aRate) { + p.frameRate = function(aRate) { curFrameRate = aRate; curMsPerFrame = 1000 / curFrameRate; @@ -7469,7 +7737,7 @@ * * @returns none */ - p.exit = function exit() { + p.exit = function() { window.clearInterval(looping); removeInstance(p.externals.canvas.id); @@ -7516,7 +7784,7 @@ * * @see noCursor */ - p.cursor = function cursor() { + p.cursor = function() { if (arguments.length > 1 || (arguments.length === 1 && arguments[0] instanceof p.PImage)) { var image = arguments[0], x, y; @@ -7550,7 +7818,7 @@ * * @see cursor */ - p.noCursor = function noCursor() { + p.noCursor = function() { curCursor = curElement.style.cursor = PConstants.NOCURSOR; }; @@ -7574,11 +7842,11 @@ // PGraphics methods // TODO: These functions are suppose to be called before any operations are called on the // PGraphics object. They currently do nothing. - p.beginDraw = function beginDraw() {}; - p.endDraw = function endDraw() {}; + p.beginDraw = function() {}; + p.endDraw = function() {}; // Imports an external Processing.js library - p.Import = function Import(lib) { + p.Import = function(lib) { // Replace evil-eval method with a DOM