├── LICENSE ├── README.md ├── README_OLD.md ├── package.json ├── plugin.xml ├── src ├── android │ ├── Sketch.java │ └── TouchDrawActivity.java ├── ios │ ├── BGSketchPlugin.h │ ├── BGSketchPlugin.m │ └── TouchDraw │ │ ├── BGTouchDrawView.h │ │ ├── BGTouchDrawView.m │ │ ├── BGTouchDrawViewController.h │ │ ├── BGTouchDrawViewController.m │ │ ├── UIImage+ImageWithColor.h │ │ └── UIImage+ImageWithColor.m └── windows │ └── sketchProxy.js ├── tests ├── .eslintrc ├── plugin.xml └── tests.js └── www ├── canvasHtml.js ├── css ├── cordova-plugin-sketch.css └── program.css ├── images ├── Erase.cur └── Select.png ├── program.js └── sketch.js /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cordova-plugin-sketch (Unmaintained) 2 | 3 | This plugin is part of our legacy product and is no longer supported/maintained by us. You are welcome to create/publish a fork to npm or do what you want with the code. 4 | 5 | Apologies for any inconvenience. 6 | 7 | See [here](./README_OLD.md) for documentation. 8 | -------------------------------------------------------------------------------- /README_OLD.md: -------------------------------------------------------------------------------- 1 | # cordova-plugin-sketch 2 | 3 | This plugin defines a navigator.Sketch object, which supplies an interface to launch a sketch pad, allowing the user to draw something or annotate a picture. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | cordova plugin add https://github.com/blinkmobile/cordova-plugin-sketch.git 9 | ``` 10 | 11 | ## Sketch.getSketch 12 | 13 | Displays a dialog which will allow user draw something or annotate a picture based on input options. 14 | 15 | ```javascript 16 | navigator.sketch.getSketch(onSuccess, onError, options); 17 | ``` 18 | 19 | ## Description 20 | 21 | Displays a drawable pad based upon the supplied options, then allow user to draw something or annotate the picture, change the width or color of the pen, erase the wrong drawing before finish. The available options include: 22 | 23 | - destinationType (optional) 24 | - encodingType (optional) 25 | - inputType (optional) 26 | - inputData (optional) 27 | 28 | The destinationType is the return image type, the available options are: DATA_URL, FILE_URI. If DestinationType is DATA_URL the plugin will return a string. If the DestinationType is FILE_URI, the plugin will return a file URI. The default is DATA_URL. 29 | 30 | The EncodingType is the return image encode type, the available options are: JPEG, PNG. The plugin will encode the return image based on EncodingType. If you didn't define EncodingType, the plugin will return a PNG format image. 31 | 32 | The inputType describes how to use the data provided in inputData. If you provide a Data URI or file URI as background picture, user can make annotation on the picture, if you provide nothing, user can draw a picture on a blank background, e.g. a signature. 33 | 34 | The inputData is a string or file URI of the background picture, depending on inputType. 35 | 36 | When the user presses "done", returns the image of the user sketched as an Data URI or file URI depending on input DestinationType. 37 | 38 | If the user presses "cancel", the result is `null`. 39 | 40 | ## Supported Platforms 41 | 42 | - Android 4.0 + 43 | - iOS 8.0 + 44 | - Windows 8.1 + 45 | 46 | ## Example 47 | 48 | Create a button on your page 49 | 50 | ```html 51 | 52 | 53 | ``` 54 | 55 | Then add click event 56 | 57 | ```javascript 58 | document.getElementById("cordova-plugin-sketch-open").addEventListener("click", getSketch, false); 59 | 60 | function getSketch(){ 61 | var image = document.getElementById('myImage'); 62 | navigator.sketch.getSketch(onSuccess, onFail, { 63 | destinationType: navigator.sketch.DestinationType.DATA_URL, 64 | encodingType: navigator.sketch.EncodingType.JPEG, 65 | inputType : navigator.sketch.InputType.FILE_URI, 66 | inputData : image.src 67 | }); 68 | } 69 | 70 | function onSuccess(imageData) { 71 | if(imageData == null) { return; } 72 | setTimeout(function() { 73 | // do your thing here! 74 | var image = document.getElementById('myImage'); 75 | if(imageData.indexOf("data:image") >= 0 ) { 76 | image.src = imageData; 77 | } else { 78 | image.src = "data:image/png;base64," + imageData; 79 | } 80 | }, 0); 81 | } 82 | 83 | function onFail(message) { 84 | setTimeout(function() { 85 | console.log('plugin message: ' + message); 86 | }, 0); 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-sketch", 3 | "version": "1.0.1", 4 | "description": "Cordova Sketch Plugin", 5 | "cordova": { 6 | "id": "cordova-plugin-sketch", 7 | "platforms": [ 8 | "windows" 9 | ] 10 | }, 11 | "keywords": [ 12 | "cordova", 13 | "sketch", 14 | "drawing", 15 | "signature", 16 | "ecosystem:cordova", 17 | "cordova-windows" 18 | ], 19 | "devDependencies": { 20 | "eslint": "^1.1", 21 | "eslint-config-semistandard": "^5.0", 22 | "eslint-config-standard": "^4.0" 23 | }, 24 | "author": "BlinkMobile Interactive", 25 | "license": "Apache 2.0" 26 | } 27 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Sketch 7 | Cordova Sketch Plugin 8 | Apache 2.0 9 | cordova,sketch,signature,draw 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 69 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/android/Sketch.java: -------------------------------------------------------------------------------- 1 | package au.com.blinkmobile.cordova.sketch; 2 | 3 | import android.app.Activity; 4 | import android.content.ContentResolver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.pm.ApplicationInfo; 8 | import android.content.pm.PackageManager; 9 | import android.graphics.Bitmap; 10 | import android.net.Uri; 11 | import android.os.Bundle; 12 | import android.provider.MediaStore; 13 | import android.util.Base64; 14 | 15 | import org.apache.cordova.CallbackContext; 16 | import org.apache.cordova.CordovaArgs; 17 | import org.apache.cordova.CordovaPlugin; 18 | import org.apache.cordova.LOG; 19 | import org.apache.cordova.PluginResult; 20 | import org.apache.cordova.PluginResult.Status; 21 | import org.json.JSONException; 22 | import org.json.JSONObject; 23 | 24 | import java.io.File; 25 | import java.io.FileOutputStream; 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | import java.util.UUID; 29 | 30 | /** 31 | * Created by jt on 29/03/16. 32 | */ 33 | public class Sketch extends CordovaPlugin { 34 | private static final String TAG = Sketch.class.getSimpleName(); 35 | 36 | private static final int SKETCH_REQUEST_CODE = 0x0010; 37 | private static final int ANNOTATION_REQUEST_CODE = 0x0100; 38 | 39 | private DestinationType destinationType; 40 | private EncodingType encodingType; 41 | private InputType inputType; 42 | private String inputData; 43 | private CallbackContext callbackContext; 44 | 45 | @Override 46 | public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException { 47 | if (!action.equals("getSketch")) { 48 | callbackContext.sendPluginResult( 49 | new PluginResult(Status.INVALID_ACTION, "Unsupported action: " + action)); 50 | return false; 51 | } 52 | 53 | try { 54 | JSONObject options = args.getJSONObject(0); 55 | 56 | int opt = options.getInt("destinationType"); 57 | if (opt >= 0 && opt < DestinationType.values().length) { 58 | this.destinationType = DestinationType.values()[opt]; 59 | } else { 60 | callbackContext.error("Invalid destinationType"); 61 | return false; 62 | } 63 | 64 | opt = options.getInt("encodingType"); 65 | if (opt >= 0 && opt < EncodingType.values().length) { 66 | this.encodingType = EncodingType.values()[opt]; 67 | } else { 68 | callbackContext.error("Invalid encodingType"); 69 | return false; 70 | } 71 | 72 | opt = options.getInt("inputType"); 73 | if (opt >= 0 && opt < InputType.values().length) { 74 | this.inputType = InputType.values()[opt]; 75 | } else { 76 | callbackContext.error("Invalid inputType"); 77 | return false; 78 | } 79 | 80 | if (this.inputType != InputType.NO_INPUT) { 81 | String inputData = options.getString("inputData"); 82 | 83 | if (inputData == null || inputData.isEmpty()) { 84 | callbackContext.error("input data not given"); 85 | return false; 86 | } 87 | this.inputData = inputData; 88 | } else { 89 | this.inputData = null; 90 | } 91 | 92 | if (this.cordova != null) { 93 | if (this.inputData != null && !this.inputData.isEmpty()) { 94 | doAnnotation(); 95 | } else { 96 | doSketch(); 97 | } 98 | } 99 | 100 | this.callbackContext = callbackContext; 101 | return true; 102 | } catch (JSONException e) { 103 | callbackContext.sendPluginResult(new PluginResult(Status.JSON_EXCEPTION, e.getMessage())); 104 | return false; 105 | } 106 | } 107 | 108 | private void doSketch() { 109 | this.cordova.getThreadPool().execute(new Runnable() { 110 | @Override 111 | public void run() { 112 | final Intent touchDrawIntent = new Intent(Sketch.this.cordova.getActivity(), TouchDrawActivity.class); 113 | 114 | touchDrawIntent.putExtra(TouchDrawActivity.BACKGROUND_IMAGE_TYPE, 115 | TouchDrawActivity.BackgroundImageType.COLOUR.ordinal()); 116 | touchDrawIntent.putExtra(TouchDrawActivity.BACKGROUND_COLOUR, "#FFFFFF"); 117 | 118 | if (Sketch.this.encodingType == EncodingType.PNG) { 119 | touchDrawIntent.putExtra(TouchDrawActivity.DRAWING_RESULT_ENCODING_TYPE, 120 | Bitmap.CompressFormat.PNG.ordinal()); 121 | } else if (Sketch.this.encodingType == EncodingType.JPEG) { 122 | touchDrawIntent.putExtra(TouchDrawActivity.DRAWING_RESULT_ENCODING_TYPE, 123 | Bitmap.CompressFormat.JPEG.ordinal()); 124 | } 125 | 126 | Sketch.this.cordova.getActivity().runOnUiThread(new Runnable() { 127 | @Override 128 | public void run() { 129 | Sketch.this.cordova.startActivityForResult(Sketch.this, touchDrawIntent, SKETCH_REQUEST_CODE); 130 | } 131 | }); 132 | } 133 | }); 134 | } 135 | 136 | private void doAnnotation() { 137 | this.cordova.getThreadPool().execute(new Runnable() { 138 | @Override 139 | public void run() { 140 | final Intent touchDrawIntent = new Intent(Sketch.this.cordova.getActivity(), TouchDrawActivity.class); 141 | 142 | if (Sketch.this.inputType == InputType.DATA_URL) { 143 | touchDrawIntent.putExtra(TouchDrawActivity.BACKGROUND_IMAGE_TYPE, 144 | TouchDrawActivity.BackgroundImageType.DATA_URL.ordinal()); 145 | } else if (Sketch.this.inputType == InputType.FILE_URI) { 146 | Uri inputUri = Uri.parse(inputData); 147 | String scheme = (inputUri != null && inputUri.getScheme() != null) ? inputUri.getScheme() : ""; 148 | 149 | if (scheme.equals(ContentResolver.SCHEME_CONTENT)) { 150 | // Workaround for CB-9548 (https://issues.apache.org/jira/browse/CB-9548) 151 | // The Cordova camera plugin can sometimes return a content URI instead of a file URI 152 | // when the image is selected from the photo gallery. 153 | // 154 | // However, the TouchDrawActivity can only accept a file URI or a data URI for the 155 | // background image. So, we need to read the background image data and pass it in a 156 | // format which can be handled by the TouchDrawActivity. 157 | 158 | InputStream inStream = null; 159 | try { 160 | // Write background image to a temporary file and pass it as a file URL because 161 | // there is no reliable way to get a file path from a content URI 162 | // (http://stackoverflow.com/a/19985374) 163 | ContentResolver contentResolver = Sketch.this.cordova.getActivity().getContentResolver(); 164 | inStream = contentResolver.openInputStream(inputUri); 165 | 166 | if (inStream != null) { 167 | File file = new File(Sketch.this.cordova.getActivity().getCacheDir(), UUID.randomUUID().toString()); 168 | FileOutputStream outStream = new FileOutputStream(file); 169 | byte[] data = new byte[1024]; 170 | int bytesRead; 171 | 172 | while ((bytesRead = inStream.read(data, 0, data.length)) != -1) { 173 | outStream.write(data, 0, bytesRead); 174 | } 175 | outStream.flush(); 176 | outStream.close(); 177 | 178 | Sketch.this.inputData = "file://" + file.getAbsolutePath(); 179 | touchDrawIntent.putExtra(TouchDrawActivity.BACKGROUND_IMAGE_TYPE, 180 | TouchDrawActivity.BackgroundImageType.FILE_URL.ordinal()); 181 | } 182 | } catch (IOException e) { 183 | String message = "Failed to read image data from " + inputUri; 184 | LOG.e(TAG, message); 185 | e.printStackTrace(); 186 | 187 | Sketch.this.callbackContext.error(message + ": " + e.getLocalizedMessage()); 188 | return; 189 | } finally { 190 | if (inStream != null) { 191 | try { 192 | inStream.close(); 193 | } catch (IOException e) { 194 | e.printStackTrace(); 195 | } 196 | } 197 | } 198 | 199 | } else if (scheme.equals(ContentResolver.SCHEME_FILE)) { 200 | touchDrawIntent.putExtra(TouchDrawActivity.BACKGROUND_IMAGE_TYPE, 201 | TouchDrawActivity.BackgroundImageType.FILE_URL.ordinal()); 202 | } else { 203 | String message = "invalid scheme for inputData: " + inputData ; 204 | File file = new File(inputData); 205 | 206 | LOG.d(TAG, message); 207 | if (file.exists() && !file.isDirectory()) { 208 | touchDrawIntent.putExtra(TouchDrawActivity.BACKGROUND_IMAGE_TYPE, 209 | TouchDrawActivity.BackgroundImageType.FILE_URL.ordinal()); 210 | inputData = "file://" + file.getAbsolutePath(); 211 | } else { 212 | Sketch.this.callbackContext.error(message); 213 | return; 214 | } 215 | } 216 | } 217 | 218 | if (Sketch.this.encodingType == EncodingType.PNG) { 219 | touchDrawIntent.putExtra(TouchDrawActivity.DRAWING_RESULT_ENCODING_TYPE, 220 | Bitmap.CompressFormat.PNG.ordinal()); 221 | } else if (Sketch.this.encodingType == EncodingType.JPEG) { 222 | touchDrawIntent.putExtra(TouchDrawActivity.DRAWING_RESULT_ENCODING_TYPE, 223 | Bitmap.CompressFormat.JPEG.ordinal()); 224 | } 225 | 226 | touchDrawIntent.putExtra(TouchDrawActivity.BACKGROUND_IMAGE_URL, inputData); 227 | Sketch.this.cordova.getActivity().runOnUiThread(new Runnable() { 228 | @Override 229 | public void run() { 230 | Sketch.this.cordova.startActivityForResult(Sketch.this, touchDrawIntent, ANNOTATION_REQUEST_CODE); 231 | } 232 | }); 233 | } 234 | }); 235 | } 236 | 237 | @Override 238 | public void onActivityResult(int requestCode, int resultCode, final Intent intent) { 239 | if (resultCode == Activity.RESULT_CANCELED) { 240 | this.callbackContext.success(""); 241 | return; 242 | } 243 | 244 | if (resultCode == Activity.RESULT_OK && this.cordova != null) { 245 | this.cordova.getThreadPool().execute(new Runnable() { 246 | @Override 247 | public void run() { 248 | saveDrawing(intent); 249 | } 250 | }); 251 | return; 252 | } 253 | 254 | if (resultCode == TouchDrawActivity.RESULT_TOUCHDRAW_ERROR) { 255 | Bundle extras = intent.getExtras(); 256 | String errorMessage = "Failed to generate sketch."; 257 | 258 | if (extras != null) { 259 | errorMessage += " " + extras.getString(TouchDrawActivity.DRAWING_RESULT_ERROR); 260 | } 261 | 262 | this.callbackContext.error(errorMessage); 263 | } 264 | } 265 | 266 | private void saveDrawing(Intent intent) { 267 | Bundle extras = intent.getExtras(); 268 | byte[] drawingData = null; 269 | String output = null; 270 | 271 | if (extras != null && 272 | extras.containsKey(TouchDrawActivity.DRAWING_RESULT_PARCELABLE)) { 273 | drawingData = extras.getByteArray(TouchDrawActivity.DRAWING_RESULT_PARCELABLE); 274 | } 275 | 276 | if (drawingData == null || drawingData.length == 0) { 277 | LOG.e(TAG, "Failed to read sketch result from activity"); 278 | this.callbackContext.error("Failed to read sketch result from activity"); 279 | return; 280 | } 281 | 282 | try { 283 | String ext = ""; 284 | 285 | if (encodingType == EncodingType.JPEG) { 286 | ext = "jpeg"; 287 | } else if (encodingType == EncodingType.PNG) { 288 | ext = "png"; 289 | } 290 | 291 | if (destinationType == DestinationType.DATA_URL) { 292 | output = "data:image/" + ext + ";base64," + Base64.encodeToString(drawingData, Base64.DEFAULT); 293 | } else if (destinationType == DestinationType.FILE_URI) { 294 | // Save the drawing to the app's cache dir 295 | String fileName = String.format("sketch-%s.%s", UUID.randomUUID(), ext); 296 | File filePath = new File(this.cordova.getActivity().getCacheDir(), fileName); 297 | 298 | FileOutputStream fos = new FileOutputStream(filePath); 299 | fos.write(drawingData); 300 | fos.close(); 301 | 302 | // Add the drawing to photo gallery 303 | String appName = getApplicationLabelOrPackageName(this.cordova.getActivity()); 304 | String mediaStoreUrl = MediaStore.Images.Media.insertImage(this.cordova.getActivity().getContentResolver(), 305 | filePath.getAbsolutePath(), fileName, 306 | (appName != null && !appName.isEmpty()) ? "Generated by " + appName : ""); 307 | 308 | LOG.d(TAG, (mediaStoreUrl != null) ? 309 | "Drawing saved to media store: " + mediaStoreUrl : 310 | "Failed to save drawing to media store"); 311 | 312 | // We need to return the file saved to the cache dir instead of the 313 | // file in the photo gallery because the Cordova file plugin cannot open content URIs 314 | output = "file://" + filePath.getAbsolutePath(); 315 | LOG.d(TAG, "Drawing saved to: " + output); 316 | } 317 | } catch(Exception e) { 318 | LOG.e(TAG, "Error generating output from drawing: " + e.getMessage()); 319 | 320 | this.callbackContext.error("Failed to generate output from drawing: " 321 | + e.getMessage()); 322 | return; 323 | } 324 | 325 | this.callbackContext.success(output); 326 | } 327 | 328 | // Based on http://stackoverflow.com/a/16444178 329 | private String getApplicationLabelOrPackageName(Context context) { 330 | PackageManager pm = context.getPackageManager(); 331 | ApplicationInfo appInfo = context.getApplicationInfo(); 332 | 333 | if (pm == null || appInfo == null) { 334 | return ""; 335 | } 336 | 337 | try { 338 | String label = (String) pm.getApplicationLabel(pm.getApplicationInfo(appInfo.packageName, 0)); 339 | if (label != null && !label.isEmpty()) { 340 | return label; 341 | } 342 | } catch (PackageManager.NameNotFoundException e) { 343 | LOG.w(TAG, "Failed to determine app label"); 344 | } 345 | 346 | return appInfo.packageName; 347 | } 348 | 349 | enum DestinationType { 350 | DATA_URL, 351 | FILE_URI 352 | } 353 | 354 | enum EncodingType { 355 | JPEG, 356 | PNG 357 | } 358 | 359 | enum InputType { 360 | NO_INPUT, 361 | DATA_URL, 362 | FILE_URI 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/android/TouchDrawActivity.java: -------------------------------------------------------------------------------- 1 | package au.com.blinkmobile.cordova.sketch; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.graphics.Bitmap; 7 | import android.graphics.BitmapFactory; 8 | import android.graphics.Canvas; 9 | import android.graphics.Color; 10 | import android.graphics.Paint; 11 | import android.graphics.Path; 12 | import android.graphics.Point; 13 | import android.os.Build; 14 | import android.os.Bundle; 15 | import android.view.Display; 16 | import android.view.HapticFeedbackConstants; 17 | import android.view.MotionEvent; 18 | import android.view.View; 19 | import android.view.ViewGroup; 20 | import android.view.Window; 21 | import android.widget.AdapterView; 22 | import android.widget.ArrayAdapter; 23 | import android.widget.Button; 24 | import android.widget.FrameLayout; 25 | import android.widget.LinearLayout; 26 | import android.util.Base64; 27 | import android.widget.RelativeLayout; 28 | import android.widget.Spinner; 29 | import android.widget.TextView; 30 | 31 | import java.io.ByteArrayOutputStream; 32 | import java.io.File; 33 | import java.io.FileNotFoundException; 34 | import java.io.IOException; 35 | import java.net.URI; 36 | import java.net.URISyntaxException; 37 | import java.util.Arrays; 38 | import java.util.concurrent.atomic.AtomicInteger; 39 | 40 | public class TouchDrawActivity extends Activity { 41 | public static final String DRAWING_RESULT_PARCELABLE = "drawing_result"; 42 | public static final String DRAWING_RESULT_ERROR = "drawing_error"; 43 | public static final String BACKGROUND_IMAGE_TYPE = "background_image_type"; 44 | public static final String BACKGROUND_IMAGE_URL = "background_image_url"; 45 | public static final String BACKGROUND_COLOUR = "background_colour"; 46 | public static final String STROKE_WIDTH = "stroke_width"; 47 | public static final int RESULT_TOUCHDRAW_ERROR = Activity.RESULT_FIRST_USER; 48 | public static final String DRAWING_RESULT_SCALE = "drawing_scale"; 49 | public static final String DRAWING_RESULT_ENCODING_TYPE = "drawing_encoding_type"; 50 | 51 | private Paint mPaint; 52 | private int mStrokeWidth = 4; 53 | private int mScale = 35; 54 | private Bitmap mBitmap; 55 | private TouchDrawView mTdView; 56 | private BackgroundImageType mBackgroundImageType = BackgroundImageType.COLOUR; 57 | private String mBackgroundColor = "#FFFFFF"; 58 | private String mBackgroundImageUrl = ""; 59 | private Bitmap.CompressFormat mEncodingType = Bitmap.CompressFormat.PNG; 60 | private int a, r, g, b; //Decoded ARGB color values for the background and erasing 61 | 62 | // Labels and values for stroke colour and width selection buttons 63 | private static final String[] STROKE_COLOUR_LABELS = {"RED", "BLUE", "GREEN", "BLACK"}; 64 | private static final int[] STROKE_COLOUR_VALUES = {Color.RED, Color.BLUE, Color.GREEN, Color.BLACK}; 65 | private static final String[] STROKE_WIDTH_LABELS = {"0.5x", "1x", "2x", "8x"}; 66 | private static final Integer[] STROKE_WIDTH_VALUES = {2, 4, 8, 32}; 67 | 68 | public enum BackgroundImageType { 69 | DATA_URL, 70 | FILE_URL, 71 | COLOUR 72 | } 73 | 74 | @Override 75 | protected void onCreate(Bundle savedInstanceState) { 76 | super.onCreate(savedInstanceState); 77 | Bundle intentExtras = getIntent().getExtras(); 78 | 79 | if (intentExtras != null) { 80 | mBackgroundImageType = BackgroundImageType.values()[ 81 | intentExtras.getInt(BACKGROUND_IMAGE_TYPE, BackgroundImageType.COLOUR.ordinal())]; 82 | mBackgroundImageUrl = intentExtras.getString(BACKGROUND_IMAGE_URL, mBackgroundImageUrl); 83 | mBackgroundColor = intentExtras.getString(BACKGROUND_COLOUR, mBackgroundColor); 84 | mStrokeWidth = intentExtras.getInt(STROKE_WIDTH, mStrokeWidth); 85 | mScale = intentExtras.getInt(DRAWING_RESULT_SCALE, mScale); 86 | mEncodingType = Bitmap.CompressFormat.values()[ 87 | intentExtras.getInt(DRAWING_RESULT_ENCODING_TYPE, mEncodingType.ordinal())]; 88 | } 89 | 90 | if (mBackgroundImageType != BackgroundImageType.COLOUR && mBackgroundImageUrl.isEmpty()) { 91 | Intent drawingResult = new Intent(); 92 | 93 | drawingResult.putExtra(DRAWING_RESULT_ERROR, 94 | "Background image url not given (and background image type != colour)"); 95 | setResult(RESULT_TOUCHDRAW_ERROR, drawingResult); 96 | finish(); 97 | return; 98 | } 99 | 100 | RelativeLayout tDLayout = new RelativeLayout(this); 101 | tDLayout.setHapticFeedbackEnabled(true); 102 | tDLayout.setLayoutParams(new RelativeLayout.LayoutParams( 103 | RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)); 104 | 105 | LinearLayout buttonBar = createButtonBar(); 106 | buttonBar.setId(getNextViewId()); 107 | RelativeLayout.LayoutParams buttonBarLayoutParams = new RelativeLayout.LayoutParams( 108 | RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); 109 | buttonBarLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); 110 | buttonBar.setLayoutParams(buttonBarLayoutParams); 111 | tDLayout.addView(buttonBar); 112 | 113 | LinearLayout toolBar = createToolBar(); 114 | toolBar.setId(getNextViewId()); 115 | RelativeLayout.LayoutParams toolBarLayoutParams = new RelativeLayout.LayoutParams( 116 | RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); 117 | toolBarLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); 118 | toolBar.setLayoutParams(toolBarLayoutParams); 119 | tDLayout.addView(toolBar); 120 | 121 | FrameLayout tDContainer = new FrameLayout(this); 122 | RelativeLayout.LayoutParams tDViewLayoutParams = new RelativeLayout.LayoutParams( 123 | RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); 124 | tDViewLayoutParams.addRule(RelativeLayout.BELOW, buttonBar.getId()); 125 | tDViewLayoutParams.addRule(RelativeLayout.ABOVE, toolBar.getId()); 126 | tDContainer.setLayoutParams(tDViewLayoutParams); 127 | mTdView = new TouchDrawView(this); 128 | tDContainer.addView(mTdView); 129 | tDLayout.addView(tDContainer); 130 | 131 | this.requestWindowFeature(Window.FEATURE_NO_TITLE); 132 | setContentView(tDLayout); 133 | 134 | mPaint = new Paint(); 135 | mPaint.setAntiAlias(true); 136 | mPaint.setDither(true); 137 | mPaint.setColor(Color.BLUE); 138 | mPaint.setStyle(Paint.Style.STROKE); 139 | mPaint.setStrokeJoin(Paint.Join.ROUND); 140 | mPaint.setStrokeCap(Paint.Cap.ROUND); 141 | mPaint.setStrokeWidth(mStrokeWidth); 142 | } 143 | 144 | public LinearLayout createButtonBar() { 145 | LinearLayout buttonBar = new LinearLayout(this); 146 | 147 | Button doneButton = new Button(this); 148 | doneButton.setText("Done"); 149 | doneButton.setBackgroundColor(Color.GREEN); 150 | doneButton.setLayoutParams(new LinearLayout.LayoutParams( 151 | 0, ViewGroup.LayoutParams.MATCH_PARENT, (float) 0.30)); 152 | doneButton.setOnClickListener(new View.OnClickListener() { 153 | public void onClick(View v) { 154 | v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 155 | finishDrawing(); 156 | } 157 | }); 158 | 159 | Button eraseButton = new Button(this); 160 | eraseButton.setText("Erase"); 161 | eraseButton.setBackgroundColor(Color.GRAY); 162 | eraseButton.setLayoutParams(new LinearLayout.LayoutParams( 163 | 0, ViewGroup.LayoutParams.MATCH_PARENT, (float) 0.30)); 164 | eraseButton.setOnClickListener(new View.OnClickListener() { 165 | public void onClick(View v) { 166 | v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 167 | eraseDrawing(); 168 | } 169 | }); 170 | 171 | Button cancelButton = new Button(this); 172 | cancelButton.setText("Cancel"); 173 | cancelButton.setBackgroundColor(Color.RED); 174 | cancelButton.setLayoutParams(new LinearLayout.LayoutParams( 175 | 0, ViewGroup.LayoutParams.MATCH_PARENT, (float) 0.30)); 176 | cancelButton.setOnClickListener(new View.OnClickListener() { 177 | public void onClick(View v) { 178 | v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 179 | v.setPressed(true); 180 | cancelDrawing(); 181 | } 182 | }); 183 | 184 | buttonBar.addView(doneButton); 185 | buttonBar.addView(eraseButton); 186 | buttonBar.addView(cancelButton); 187 | 188 | return buttonBar; 189 | } 190 | 191 | public LinearLayout createToolBar() { 192 | LinearLayout toolBar = new LinearLayout(this); 193 | 194 | toolBar.addView(createColourSpinner()); 195 | toolBar.addView(createWidthSpinner()); 196 | 197 | return toolBar; 198 | } 199 | 200 | public Spinner createColourSpinner() { 201 | final String strokeColourLabelPrefix = "COLOUR: "; 202 | Spinner spinner = new Spinner(this); 203 | 204 | ArrayAdapter adapter = new ArrayAdapter(this, 205 | android.R.layout.simple_spinner_dropdown_item, STROKE_COLOUR_LABELS) { 206 | @Override 207 | public View getView(int position, View convertView, ViewGroup parent) { 208 | TextView v = (TextView) super.getView(position, convertView, parent); 209 | 210 | v.setText(strokeColourLabelPrefix + "BLUE"); 211 | return v; 212 | } 213 | 214 | @Override 215 | public View getDropDownView(int position, View convertView, ViewGroup parent) { 216 | TextView v = (TextView) super.getDropDownView(position, convertView,parent); 217 | 218 | v.setText(STROKE_COLOUR_LABELS[position]); 219 | return v; 220 | } 221 | }; 222 | spinner.setAdapter(adapter); 223 | 224 | spinner.setOnItemSelectedListener(new Spinner.OnItemSelectedListener() { 225 | @Override 226 | public void onItemSelected(AdapterView adapterView, View view, int position, long id) { 227 | if (mPaint != null && position >= 0 && position < STROKE_COLOUR_VALUES.length) { 228 | mPaint.setColor(STROKE_COLOUR_VALUES[position]); 229 | 230 | adapterView.setBackgroundColor(STROKE_COLOUR_VALUES[position]); 231 | ((TextView) view).setText(strokeColourLabelPrefix + STROKE_COLOUR_LABELS[position]); 232 | } 233 | } 234 | 235 | @Override 236 | public void onNothingSelected(AdapterView adapterView) { 237 | } 238 | }); 239 | spinner.setBackgroundColor(Color.BLUE); 240 | 241 | LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 242 | 0, ViewGroup.LayoutParams.MATCH_PARENT, (float) 0.30); 243 | spinner.setLayoutParams(layoutParams); 244 | spinner.setSelection(Arrays.asList(STROKE_COLOUR_LABELS).indexOf("BLUE")); 245 | 246 | return spinner; 247 | } 248 | 249 | public Spinner createWidthSpinner() { 250 | final String strokeWidthLabelPrefix = "WIDTH: "; 251 | Spinner spinner = new Spinner(this); 252 | 253 | final ArrayAdapter adapter = new ArrayAdapter(this, 254 | android.R.layout.simple_spinner_dropdown_item, STROKE_WIDTH_LABELS) { 255 | @Override 256 | public View getView(int position, View convertView, ViewGroup parent) { 257 | TextView v = (TextView) super.getView(position, convertView, parent); 258 | 259 | v.setText(strokeWidthLabelPrefix + 260 | STROKE_WIDTH_LABELS[Arrays.asList(STROKE_WIDTH_VALUES).indexOf(mStrokeWidth)]); 261 | return v; 262 | } 263 | 264 | @Override 265 | public View getDropDownView(int position, View convertView, ViewGroup parent) { 266 | TextView v = (TextView) super.getDropDownView(position, convertView,parent); 267 | 268 | v.setText(STROKE_WIDTH_LABELS[position]); 269 | return v; 270 | } 271 | }; 272 | spinner.setAdapter(adapter); 273 | 274 | spinner.setOnItemSelectedListener(new Spinner.OnItemSelectedListener() { 275 | @Override 276 | public void onItemSelected(AdapterView adapterView, View view, int position, long id) { 277 | mStrokeWidth = STROKE_WIDTH_VALUES[position]; 278 | ((TextView) view).setText(strokeWidthLabelPrefix + 279 | STROKE_WIDTH_LABELS[Arrays.asList(STROKE_WIDTH_VALUES).indexOf(mStrokeWidth)]); 280 | 281 | if (mPaint != null) { 282 | mPaint.setStrokeWidth(mStrokeWidth); 283 | } 284 | } 285 | 286 | @Override 287 | public void onNothingSelected(AdapterView adapterView) { 288 | } 289 | }); 290 | 291 | LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 292 | 0, ViewGroup.LayoutParams.MATCH_PARENT, (float) 0.30); 293 | spinner.setLayoutParams(layoutParams); 294 | spinner.setSelection(Arrays.asList(STROKE_WIDTH_LABELS).indexOf("1x")); 295 | 296 | return spinner; 297 | } 298 | 299 | public void eraseDrawing() { 300 | try{ 301 | switch (mBackgroundImageType) { 302 | case COLOUR: 303 | mBitmap.eraseColor(Color.argb(a, r, g, b)); 304 | break; 305 | case FILE_URL: 306 | mBitmap = loadMutableBitmapFromFileURI(new URI(mBackgroundImageUrl)); 307 | break; 308 | case DATA_URL: 309 | mBitmap = loadMutableBitmapFromBase64DataUrl(mBackgroundImageUrl); 310 | break; 311 | default: 312 | return; 313 | } 314 | } catch (URISyntaxException e) { 315 | handleFileUriError(e); 316 | return; 317 | } catch (FileNotFoundException e) { 318 | handleFileIOError(e); 319 | return; 320 | } 321 | 322 | mBitmap = Bitmap.createScaledBitmap(mBitmap, mTdView.mCanvas.getWidth(), 323 | mTdView.mCanvas.getHeight(), false); 324 | mTdView.mCanvas = new Canvas(mBitmap); 325 | mTdView.invalidate(); 326 | } 327 | 328 | public void cancelDrawing() { 329 | Intent drawingResult = new Intent(); 330 | 331 | setResult(Activity.RESULT_CANCELED, drawingResult); 332 | finish(); 333 | } 334 | 335 | public Bitmap scaleBitmap(Bitmap bitmap) { 336 | int origWidth = bitmap.getWidth(); 337 | int origHeight = bitmap.getHeight(); 338 | int newWidth, newHeight; 339 | 340 | if (mScale < 100) { 341 | newWidth = (int) (origWidth * (mScale / 100.0)); 342 | newHeight = (int)(origHeight * (mScale / 100.0)); 343 | } else { 344 | return bitmap; 345 | } 346 | 347 | return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true); 348 | } 349 | 350 | @Override 351 | public void onBackPressed() { 352 | cancelDrawing(); 353 | super.onBackPressed(); 354 | } 355 | 356 | public void finishDrawing() { 357 | ByteArrayOutputStream drawing = new ByteArrayOutputStream(); 358 | scaleBitmap(mBitmap).compress(mEncodingType, 100, drawing); 359 | 360 | Intent drawingResult = new Intent(); 361 | drawingResult.putExtra(DRAWING_RESULT_PARCELABLE, drawing.toByteArray()); 362 | setResult(Activity.RESULT_OK, drawingResult); 363 | finish(); 364 | } 365 | 366 | @Override 367 | public void finish() { 368 | if (mBitmap != null) { 369 | mBitmap.recycle(); 370 | mBitmap = null; 371 | System.gc(); 372 | } 373 | 374 | super.finish(); 375 | } 376 | 377 | public class TouchDrawView extends View { 378 | public Canvas mCanvas; 379 | private Path mPath; 380 | private Paint mBitmapPaint; 381 | 382 | @SuppressWarnings("deprecation") 383 | public TouchDrawView(Context context) { 384 | super(context); 385 | Display display = getWindowManager().getDefaultDisplay(); 386 | int canvasWidth; 387 | int canvasHeight; 388 | 389 | if (mBackgroundImageType == BackgroundImageType.COLOUR) { 390 | if (Build.VERSION.SDK_INT >= 13) { 391 | Point p = new Point(); 392 | display.getSize(p); 393 | 394 | canvasWidth = p.x; 395 | canvasHeight = p.y; 396 | } else { 397 | canvasWidth = display.getWidth(); // Deprecated in SDK Versions >= 13 398 | canvasHeight = display.getHeight(); // Deprecated in SDK Versions >= 13 399 | } 400 | 401 | mBitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, 402 | Bitmap.Config.ARGB_8888); 403 | mCanvas = new Canvas(mBitmap); 404 | 405 | // Decode the hex color code for the background to ARGB 406 | a = 0xFF; // alpha value (0 -> transparent, FF -> opaque) 407 | r = Integer.valueOf("" + mBackgroundColor.charAt(1) + 408 | mBackgroundColor.charAt(2), 16); 409 | g = Integer.valueOf("" + mBackgroundColor.charAt(3) + 410 | mBackgroundColor.charAt(4), 16); 411 | b = Integer.valueOf("" + mBackgroundColor.charAt(5) + 412 | mBackgroundColor.charAt(6), 16); 413 | mCanvas.drawARGB(a, r, g, b); 414 | } else { 415 | try { 416 | if (mBackgroundImageType == BackgroundImageType.FILE_URL) { 417 | mBitmap = loadMutableBitmapFromFileURI(new URI(mBackgroundImageUrl)); 418 | 419 | if (mBitmap == null) { 420 | throw new IOException("Failed to read file: " + mBackgroundImageUrl); 421 | } 422 | } else if (mBackgroundImageType == BackgroundImageType.DATA_URL) { 423 | mBitmap = loadMutableBitmapFromBase64DataUrl(mBackgroundImageUrl); 424 | } 425 | } catch (URISyntaxException e) { 426 | handleFileUriError(e); 427 | return; 428 | } catch (IOException e) { 429 | handleFileIOError(e); 430 | return; 431 | } 432 | mCanvas = new Canvas(mBitmap); 433 | } 434 | mPath = new Path(); 435 | mBitmapPaint = new Paint(Paint.DITHER_FLAG); 436 | } 437 | 438 | @Override 439 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 440 | super.onSizeChanged(w, h, oldw, oldh); 441 | 442 | float newWidth = w; 443 | float newHeight = h; 444 | 445 | float bitmapWidth = mBitmap.getWidth(); 446 | float bitmapHeight = mBitmap.getHeight(); 447 | 448 | if (mBackgroundImageType != BackgroundImageType.COLOUR) { 449 | if (w != bitmapWidth || h != bitmapHeight) { 450 | float xRatio = w / bitmapWidth; 451 | float yRatio = h / bitmapHeight; 452 | 453 | float dominatingRatio = Math.min(xRatio, yRatio); 454 | 455 | newWidth = dominatingRatio * bitmapWidth; 456 | newHeight = dominatingRatio * bitmapHeight; 457 | 458 | } 459 | } 460 | 461 | mBitmap = Bitmap.createScaledBitmap(mBitmap, Math.round(newWidth), 462 | Math.round(newHeight), false); 463 | 464 | mCanvas.setBitmap(mBitmap); 465 | } 466 | 467 | @Override 468 | protected void onDraw(Canvas canvas) { 469 | canvas.drawColor(Color.argb(a, r, g, b)); 470 | canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint); 471 | canvas.drawPath(mPath, mPaint); 472 | } 473 | 474 | private float mX, mY; 475 | private static final float TOUCH_TOLERANCE = 4; 476 | 477 | private void touch_start(float x, float y) { 478 | mPath.reset(); 479 | mPath.moveTo(x, y); 480 | mX = x; 481 | mY = y; 482 | } 483 | private void touch_move(float x, float y) { 484 | float dx = Math.abs(x - mX); 485 | float dy = Math.abs(y - mY); 486 | if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) { 487 | mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2); 488 | mX = x; 489 | mY = y; 490 | } 491 | } 492 | private void touch_up() { 493 | mPath.lineTo(mX, mY); 494 | // commit the path to our offscreen 495 | mCanvas.drawPath(mPath, mPaint); 496 | // kill this so we don't double draw 497 | mPath.reset(); 498 | } 499 | @Override 500 | public boolean onTouchEvent(MotionEvent event) { 501 | float x = event.getX(); 502 | float y = event.getY(); 503 | 504 | switch (event.getAction()) { 505 | case MotionEvent.ACTION_DOWN: 506 | touch_start(x, y); 507 | invalidate(); 508 | break; 509 | case MotionEvent.ACTION_MOVE: 510 | touch_move(x, y); 511 | invalidate(); 512 | break; 513 | case MotionEvent.ACTION_UP: 514 | touch_up(); 515 | invalidate(); 516 | break; 517 | } 518 | return true; 519 | } 520 | } 521 | 522 | private Bitmap loadMutableBitmapFromFileURI(URI uri) throws FileNotFoundException, URISyntaxException { 523 | if (!uri.getScheme().equals("file")) { 524 | throw new URISyntaxException(mBackgroundImageUrl, "invalid scheme"); 525 | } 526 | 527 | if (uri.getQuery() != null) { 528 | // Ignore query parameters in the uri 529 | uri = new URI(uri.toString().split("\\?")[0]); 530 | } 531 | File file = new File(uri); 532 | 533 | if (!file.exists()) { 534 | throw new FileNotFoundException("File not found: " + file.getAbsolutePath()); 535 | } 536 | 537 | BitmapFactory.Options opts = new BitmapFactory.Options(); 538 | opts.inMutable = true; 539 | opts.inPreferredConfig = Bitmap.Config.ARGB_8888; 540 | return BitmapFactory.decodeFile(file.getAbsolutePath(), opts); 541 | } 542 | 543 | private Bitmap loadMutableBitmapFromBase64DataUrl(String base64DataUrl) throws URISyntaxException { 544 | if (base64DataUrl == null || base64DataUrl.isEmpty() || 545 | !base64DataUrl.matches("data:.*;base64,.*")) { 546 | throw new URISyntaxException(base64DataUrl, "invalid data url"); 547 | } 548 | 549 | String base64 = base64DataUrl.split("base64,")[1]; 550 | byte[] imgData = Base64.decode(base64, Base64.DEFAULT); 551 | 552 | BitmapFactory.Options opts = new BitmapFactory.Options(); 553 | opts.inMutable = true; 554 | opts.inPreferredConfig = Bitmap.Config.ARGB_8888; 555 | return BitmapFactory.decodeByteArray(imgData, 0, imgData.length, opts); 556 | } 557 | 558 | private void handleFileUriError(URISyntaxException e) { 559 | Intent result = new Intent(); 560 | 561 | result.putExtra(DRAWING_RESULT_ERROR, 562 | e.getReason() + ": " + e.getInput()); 563 | TouchDrawActivity.this.setResult(RESULT_TOUCHDRAW_ERROR, result); 564 | TouchDrawActivity.this.finish(); 565 | } 566 | 567 | private void handleFileIOError(IOException e) { 568 | Intent result = new Intent(); 569 | 570 | result.putExtra(DRAWING_RESULT_ERROR, e.getLocalizedMessage()); 571 | TouchDrawActivity.this.setResult(RESULT_TOUCHDRAW_ERROR, result); 572 | TouchDrawActivity.this.finish(); 573 | } 574 | 575 | private int getNextViewId() { 576 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 577 | return View.generateViewId(); // Added in API level 17 578 | } 579 | 580 | // Re-implement View.generateViewId()for API levels < 17 581 | // http://stackoverflow.com/a/15442898 582 | for (;;) { 583 | final int result = sNextGeneratedId.get(); 584 | // aapt-generated IDs have the high byte nonzero; clamp to the range under that. 585 | int newValue = result + 1; 586 | if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0. 587 | if (sNextGeneratedId.compareAndSet(result, newValue)) { 588 | return result; 589 | } 590 | } 591 | } 592 | private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); 593 | } 594 | -------------------------------------------------------------------------------- /src/ios/BGSketchPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface BGSketchPlugin : CDVPlugin 4 | 5 | - (void)getSketch:(CDVInvokedUrlCommand*)command; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /src/ios/BGSketchPlugin.m: -------------------------------------------------------------------------------- 1 | #import "BGSketchPlugin.h" 2 | #import "BGTouchDrawViewController.h" 3 | #import "UIImage+ImageWithColor.h" 4 | 5 | enum DestinationType { 6 | DestinationTypeDataUrl = 0, 7 | DestinationTypeFileUri 8 | }; 9 | typedef NSUInteger DestinationType; 10 | 11 | enum EncodingType { 12 | EncodingTypeJPEG = 0, 13 | EncodingTypePNG 14 | }; 15 | typedef NSUInteger EncodingType; 16 | 17 | enum InputType { 18 | InputTypeNoInput = 0, 19 | InputTypeDataUrl, 20 | InputTypeFileUri 21 | }; 22 | typedef NSUInteger InputType; 23 | 24 | @interface BGSketchPlugin () 25 | 26 | @property (copy) NSString* callbackId; 27 | @property (nonatomic) DestinationType destinationType; 28 | @property (nonatomic) EncodingType encodingType; 29 | @property (nonatomic) InputType inputType; 30 | @property (nonatomic, copy) NSString *inputData; 31 | @property (nonatomic, retain) UINavigationController *navigationController; 32 | @property (nonatomic, retain) BGTouchDrawViewController *touchDrawController; 33 | 34 | @end 35 | 36 | @implementation BGSketchPlugin 37 | 38 | - (void) getSketch:(CDVInvokedUrlCommand*)command 39 | { 40 | self.callbackId = command.callbackId; 41 | NSDictionary *options = command.arguments[0]; 42 | 43 | if (!options || options.count == 0) { 44 | [self sendErrorResultWithMessage:@"Insufficent number of options"]; 45 | return; 46 | } 47 | 48 | NSUInteger destinationType = [options[@"destinationType"] integerValue]; 49 | switch (destinationType) { 50 | case DestinationTypeDataUrl: 51 | case DestinationTypeFileUri: 52 | self.destinationType = destinationType; 53 | break; 54 | default: 55 | [self sendErrorResultWithMessage:@"Invalid destinationType"]; 56 | return; 57 | 58 | } 59 | 60 | NSUInteger encodingType = [options[@"encodingType"] integerValue]; 61 | switch (encodingType) { 62 | case EncodingTypeJPEG: 63 | case EncodingTypePNG: 64 | self.encodingType = encodingType; 65 | break; 66 | default: 67 | [self sendErrorResultWithMessage:@"Invalid encodingType"]; 68 | return; 69 | } 70 | 71 | NSUInteger inputType = [options[@"inputType"] integerValue]; 72 | switch (inputType) { 73 | case InputTypeNoInput: 74 | case InputTypeDataUrl: 75 | case InputTypeFileUri: 76 | self.inputType = inputType; 77 | break; 78 | default: 79 | [self sendErrorResultWithMessage:@"Invalid inputType"]; 80 | return; 81 | } 82 | 83 | if (self.inputType != InputTypeNoInput) { 84 | NSString *inputData = options[@"inputData"]; 85 | 86 | if (!inputData || [inputData isEqualToString:@""]) { 87 | [self sendErrorResultWithMessage:@"inputData not given"]; 88 | return; 89 | } 90 | 91 | self.inputData = inputData; 92 | } else { 93 | self.inputData = nil; 94 | } 95 | 96 | if (self.inputData) { 97 | [self doAnnotation]; 98 | } else { 99 | [self doSketch]; 100 | } 101 | } 102 | 103 | - (void) doAnnotation 104 | { 105 | dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ 106 | UIImage *backgroundImage = nil; 107 | NSData *imageData = nil; 108 | 109 | if (self.inputType == InputTypeDataUrl) { 110 | imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:self.inputData]]; 111 | 112 | if (!imageData) { 113 | [self sendErrorResultWithMessage:@"Failed to read image data from data url"]; 114 | return; 115 | } 116 | 117 | self.inputData = nil; // release the base64 image data 118 | } else if (self.inputType == InputTypeFileUri) { 119 | imageData = [NSData dataWithContentsOfFile:[[NSURL URLWithString:self.inputData] relativePath]]; 120 | 121 | if (!imageData) { 122 | [self sendErrorResultWithMessage:@"Failed to read image data from file"]; 123 | return; 124 | } 125 | } 126 | 127 | backgroundImage = [UIImage imageWithData:imageData]; 128 | if (!backgroundImage) { 129 | [self sendErrorResultWithMessage:@"Failed to created background image from input data"]; 130 | return; 131 | } 132 | 133 | dispatch_async(dispatch_get_main_queue(), ^{ 134 | CGSize contentSize = CGSizeMake(backgroundImage.size.width, backgroundImage.size.height); 135 | BGTouchDrawViewController *touchDrawVC = [[BGTouchDrawViewController alloc] init]; 136 | touchDrawVC.backgroundImage = backgroundImage; 137 | touchDrawVC.contentSize = contentSize; 138 | touchDrawVC.delegate = self; 139 | touchDrawVC.shouldCenterDrawing = YES; 140 | touchDrawVC.shouldResizeContentOnRotate = NO; 141 | self.touchDrawController = touchDrawVC; 142 | 143 | UIViewController *rootVC = [[UIViewController alloc] init]; // dummy root view controller 144 | UINavigationController *navVC = [[UINavigationController alloc] initWithRootViewController:rootVC]; 145 | [rootVC.navigationController setNavigationBarHidden:YES]; 146 | [navVC pushViewController:touchDrawVC animated:NO]; 147 | [self.viewController presentViewController:navVC animated:YES completion:nil]; 148 | self.navigationController = navVC; 149 | }); 150 | }); 151 | } 152 | 153 | - (void) doSketch 154 | { 155 | dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ 156 | CGSize contentSize = self.viewController.view.bounds.size; 157 | UIImage *backgroundImage = [UIImage imageWithColor:[UIColor whiteColor] 158 | andSize:CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height)]; 159 | 160 | dispatch_async(dispatch_get_main_queue(), ^{ 161 | BGTouchDrawViewController *touchDrawVC = [[BGTouchDrawViewController alloc] init]; 162 | touchDrawVC.backgroundImage = backgroundImage; 163 | touchDrawVC.contentSize = contentSize; 164 | touchDrawVC.delegate = self; 165 | touchDrawVC.shouldCenterDrawing = NO; 166 | touchDrawVC.shouldResizeContentOnRotate = YES; 167 | self.touchDrawController = touchDrawVC; 168 | 169 | UIViewController *rootVC = [[UIViewController alloc] init]; // dummy root view controller. 170 | UINavigationController *navVC = [[UINavigationController alloc] initWithRootViewController:rootVC]; 171 | [rootVC.navigationController setNavigationBarHidden:YES]; 172 | [navVC pushViewController:touchDrawVC animated:NO]; 173 | [self.viewController presentViewController:navVC animated:YES completion:nil]; 174 | self.navigationController = navVC; 175 | }); 176 | }); 177 | } 178 | 179 | - (void) sendErrorResultWithMessage:(NSString *)message 180 | { 181 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR 182 | messageAsString:message] 183 | callbackId:self.callbackId]; 184 | } 185 | 186 | - (void) dealloc 187 | { 188 | if (self.touchDrawController) { 189 | self.touchDrawController.delegate = nil; 190 | self.touchDrawController = nil; 191 | } 192 | 193 | if (self.navigationController) { 194 | self.navigationController = nil; 195 | } 196 | } 197 | 198 | #pragma mark - SaveDrawingProtocol 199 | 200 | - (void) saveDrawing:(UIImage *)drawing 201 | { 202 | dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ 203 | NSData *drawingData; 204 | NSString *encType; 205 | if (self.encodingType == EncodingTypeJPEG) { 206 | drawingData = UIImageJPEGRepresentation(drawing, 1.0f); 207 | encType = @"jpeg"; 208 | } else if (self.encodingType == EncodingTypePNG){ 209 | drawingData = UIImagePNGRepresentation(drawing); 210 | encType = @"png"; 211 | } 212 | 213 | NSString *message = nil; 214 | if (self.destinationType == DestinationTypeDataUrl) { 215 | message = [NSString stringWithFormat:@"data:image/%@;base64,%@", encType, [drawingData base64EncodedStringWithOptions:0]]; 216 | } else if (self.destinationType == DestinationTypeFileUri) { 217 | NSString *fileName = [NSString stringWithFormat:@"sketch-%@.%@", [NSUUID UUID].UUIDString, encType]; 218 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 219 | NSURL *filePath = [NSURL fileURLWithPath:[paths[0] stringByAppendingPathComponent:fileName]]; 220 | NSError *error = nil; 221 | 222 | [drawingData writeToURL:filePath options:(NSDataWritingAtomic|NSDataWritingFileProtectionComplete) 223 | error:&error]; 224 | if (error) { 225 | [self sendErrorResultWithMessage:[@"Failed to write drawing data to file: " stringByAppendingString:[error localizedDescription]]]; 226 | NSLog(@"Error: Failed to write drawing data to temp file: %@", [error localizedDescription]); 227 | } else { 228 | message = [filePath relativePath]; 229 | } 230 | } 231 | 232 | if (message) { 233 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK 234 | messageAsString:message] 235 | callbackId:self.callbackId]; 236 | NSLog(@"Drawing saved"); 237 | } 238 | 239 | dispatch_async(dispatch_get_main_queue(), ^{ 240 | [self.navigationController popToRootViewControllerAnimated:NO]; 241 | [self.navigationController dismissViewControllerAnimated:NO completion:nil]; 242 | }); 243 | }); 244 | } 245 | 246 | - (void) cancelDrawing 247 | { 248 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK 249 | messageAsString:@""] 250 | callbackId:self.callbackId]; 251 | 252 | [self.navigationController popToRootViewControllerAnimated:NO]; 253 | [self.navigationController dismissViewControllerAnimated:NO completion:nil]; 254 | NSLog(@"Drawing cancelled"); 255 | } 256 | 257 | @end 258 | -------------------------------------------------------------------------------- /src/ios/TouchDraw/BGTouchDrawView.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | 4 | @interface BGTouchDrawView : UIView 5 | { 6 | 7 | } 8 | 9 | // The layer we draw to 10 | @property (strong, nonatomic) CALayer *canvasLayer; 11 | 12 | // The layer we use to display the cached image 13 | @property (strong, nonatomic) CALayer *backgroundLayer; 14 | 15 | 16 | @property (nonatomic, strong) UIImageView *backgroundView; 17 | 18 | @property (nonatomic, strong) UIImage *backgroundImage; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /src/ios/TouchDraw/BGTouchDrawView.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "BGTouchDrawView.h" 3 | 4 | @implementation BGTouchDrawView 5 | 6 | @synthesize backgroundLayer, canvasLayer; 7 | 8 | - (id)initWithFrame:(CGRect)frame 9 | { 10 | self = [super initWithFrame:frame]; 11 | if (self) { 12 | // Only one finger/stylus for signing/drawing 13 | [self setMultipleTouchEnabled:NO]; 14 | } 15 | 16 | return self; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /src/ios/TouchDraw/BGTouchDrawViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // TouchViewController.h 3 | // TouchTracker 4 | // 5 | // Created by Shane on 22/01/12. 6 | // Copyright (c) 2012 BlinkMobile Interactive. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "BGTouchDrawView.h" 11 | 12 | #define LINE_WIDTH (3.0f) 13 | 14 | @protocol SaveDrawingProtocol 15 | 16 | - (void)saveDrawing:(UIImage *)drawing; 17 | - (void)cancelDrawing; 18 | 19 | @end 20 | 21 | @interface BGTouchDrawViewController : UIViewController {} 22 | 23 | @property (strong, nonatomic) id delegate; 24 | 25 | // The layer we draw to 26 | @property (strong, nonatomic) CALayer *canvasLayer; 27 | 28 | // The layer we use to display the cached image 29 | @property (strong, nonatomic) CALayer *backgroundLayer; 30 | 31 | // The layer we use to display the cached image 32 | @property (strong, nonatomic) UIImage *cacheImage; 33 | @property (strong, nonatomic) BGTouchDrawView *tdv; 34 | 35 | 36 | @property (assign, nonatomic) BOOL touching; 37 | @property (assign, nonatomic) BOOL moved; 38 | 39 | // The path that represents the currently drawn line 40 | @property (strong, nonatomic) UIBezierPath *path; 41 | 42 | // The current location of a touch event 43 | @property (assign, nonatomic) CGPoint pathPoint; 44 | 45 | @property (assign, nonatomic) BOOL clearCanvasLayer; 46 | @property (assign, nonatomic) BOOL clearBackgroundLayer; 47 | @property (assign, nonatomic) CGSize contentSize; 48 | @property (nonatomic, strong) UIImageView *backgroundView; 49 | 50 | @property (nonatomic, strong) UIImage *backgroundImage; 51 | 52 | @property (nonatomic, assign) BOOL clearCacheImage; 53 | 54 | @property (assign, nonatomic) BOOL saved; 55 | 56 | @property (assign, nonatomic) BOOL hasDrawing; 57 | 58 | @property(nonatomic, strong) UIButton *redButton; 59 | 60 | @property(nonatomic, strong) UIButton *blueButton; 61 | 62 | @property(nonatomic, strong) UIButton *greenButton; 63 | 64 | @property(nonatomic, strong) UIButton *blackButton; 65 | 66 | @property(nonatomic, strong) UIColor *colour; 67 | 68 | @property(nonatomic, strong) UIBarButtonItem *btColour; 69 | 70 | @property (assign, nonatomic) BOOL shouldResizeContentOnRotate; 71 | 72 | @property (assign, nonatomic) BOOL shouldCenterDrawing; 73 | 74 | - (void)saveAll; 75 | - (void)clearAll; 76 | - (void)cancelAll; 77 | 78 | 79 | @end 80 | 81 | -------------------------------------------------------------------------------- /src/ios/TouchDraw/BGTouchDrawViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // TouchViewController.m 3 | // TouchTracker 4 | // 5 | // Created by Shane on 22/01/12. 6 | // Copyright (c) 2012 BlinkMobile Interactive. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "BGTouchDrawViewController.h" 11 | #import "BGTouchDrawView.h" 12 | 13 | #define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame) 14 | #define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending) 15 | #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending) 16 | #define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending) 17 | #define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending) 18 | 19 | @implementation BGTouchDrawViewController 20 | { 21 | BOOL coloursShown; 22 | CGRect buttonsAnimationStartFrame; 23 | } 24 | 25 | - (id) init 26 | { 27 | // Call the superclass's designated initialiser 28 | self = [super initWithNibName:nil bundle:nil]; 29 | if (self) { 30 | _shouldResizeContentOnRotate = NO; 31 | _shouldCenterDrawing = NO; 32 | } 33 | return self; 34 | } 35 | 36 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 37 | { 38 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 39 | if (self) { 40 | _shouldResizeContentOnRotate = NO; 41 | _shouldCenterDrawing = NO; 42 | } 43 | return self; 44 | } 45 | 46 | - (void)didReceiveMemoryWarning 47 | { 48 | // Releases the view if it doesn't have a superview. 49 | [super didReceiveMemoryWarning]; 50 | 51 | // Release any cached data, images, etc that aren't in use. 52 | } 53 | 54 | #pragma mark - View lifecycle 55 | 56 | - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation 57 | { 58 | [self viewWillAppear:NO]; 59 | self.saved = NO; 60 | [_canvasLayer setNeedsDisplay]; 61 | [_backgroundLayer setNeedsDisplay]; 62 | } 63 | 64 | - (void)viewWillAppear:(BOOL)animated 65 | { 66 | [super viewWillAppear:animated]; 67 | 68 | _touching = NO; 69 | self.colour = [UIColor blueColor]; 70 | 71 | //disable gesture recognizer 72 | for (UIGestureRecognizer *recognizer in 73 | self.navigationController.view.gestureRecognizers) { 74 | [recognizer setEnabled:NO]; 75 | } 76 | 77 | float xRatio = 0, yRatio = 0, dominatingRatio = 0; 78 | float newHeight, newWidth; 79 | CGRect frame = self.view.bounds; 80 | if (!self.shouldResizeContentOnRotate) { 81 | if (frame.size.width != self.contentSize.width || 82 | frame.size.height != self.contentSize.height) { 83 | xRatio = frame.size.width / self.contentSize.width; 84 | yRatio = frame.size.height / self.contentSize.height; 85 | 86 | dominatingRatio = MIN(xRatio, yRatio); 87 | 88 | newWidth = dominatingRatio * self.contentSize.width; 89 | newHeight = dominatingRatio * self.contentSize.height; 90 | 91 | frame.size = CGSizeMake(newWidth, newHeight); 92 | } 93 | } else { 94 | self.contentSize = frame.size; 95 | } 96 | 97 | if (self.shouldCenterDrawing && (frame.size.width < self.view.frame.size.width || 98 | frame.size.height < self.view.frame.size.height)) { 99 | // Center the drawing along the non-dominated axis 100 | if (dominatingRatio == xRatio) { 101 | frame.origin.y = (self.view.frame.size.height - frame.size.height) / 2.0; 102 | } else { 103 | frame.origin.x = (self.view.frame.size.width - frame.size.width) / 2.0; 104 | } 105 | } 106 | 107 | self.view = self.tdv = [[BGTouchDrawView alloc]initWithFrame:frame]; 108 | self.backgroundView = [[UIImageView alloc] initWithFrame:CGRectMake(frame.origin.x, frame.origin.y, 109 | frame.size.width, frame.size.height)]; 110 | self.backgroundView.image = self.backgroundImage; 111 | 112 | [self.tdv setBackgroundColor:[UIColor grayColor]]; 113 | [self.tdv addSubview:self.backgroundView]; 114 | 115 | // Drawing layer 116 | self.canvasLayer = [CALayer layer]; 117 | [self.canvasLayer setBounds:CGRectMake(0, 0, frame.size.width, frame.size.height)]; 118 | [self.canvasLayer setPosition:CGPointMake(frame.origin.x + frame.size.width / 2.0, 119 | frame.origin.y + frame.size.height / 2.0)]; 120 | [self.canvasLayer setDelegate:self]; 121 | 122 | // Set up storage layer 123 | self.backgroundLayer = [CALayer layer]; 124 | [self.backgroundLayer setBounds:CGRectMake(0, 0, frame.size.width, frame.size.height)]; 125 | [self.backgroundLayer setPosition:CGPointMake(frame.origin.x + frame.size.width / 2.0, 126 | frame.origin.y + frame.size.height / 2.0)]; 127 | [self.backgroundLayer setDelegate:self]; 128 | 129 | [[self.tdv layer] addSublayer:_backgroundLayer]; 130 | [[self.tdv layer] addSublayer:_canvasLayer]; 131 | } 132 | 133 | - (void)setUpToolbar 134 | { 135 | self.btColour = [[UIBarButtonItem alloc] initWithTitle:@"Colour" 136 | style:UIBarButtonItemStylePlain 137 | target:self 138 | action:@selector(toggleColour:event:)]; 139 | [self setColourButtonColour]; 140 | UIBarButtonItem *btRecycle = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh 141 | target:self 142 | action:@selector(clearAll)]; 143 | UIBarButtonItem *btSave = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave 144 | target:self 145 | action:@selector(saveAll)]; 146 | UIBarButtonItem *flexItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace 147 | target:nil 148 | action:nil]; 149 | 150 | NSArray *items = [NSArray arrayWithObjects:self.btColour, flexItem, btRecycle, flexItem, btSave, nil]; 151 | [self setToolbarItems:items animated:YES]; 152 | 153 | [[self navigationController] setToolbarHidden:NO animated:NO]; 154 | self.navigationController.toolbar.barStyle = UIBarStyleDefault; 155 | } 156 | 157 | - (void)viewDidAppear:(BOOL)animated 158 | { 159 | [super viewDidAppear:animated]; 160 | } 161 | 162 | // Implement viewDidLoad to do additional setup after loading the view, typically from a nib. 163 | - (void)viewDidLoad 164 | { 165 | [super viewDidLoad]; 166 | 167 | #ifdef __IPHONE_7_0 168 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_7_0 169 | if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")) { 170 | if ([self respondsToSelector:@selector(edgesForExtendedLayout)]) { 171 | self.edgesForExtendedLayout = UIRectEdgeAll; 172 | } 173 | } 174 | #endif 175 | #else 176 | //don't have to do anything special here 177 | #endif 178 | 179 | [self setUpToolbar]; 180 | NSArray *subviews = [[[self navigationController] navigationBar] subviews]; 181 | for(UIView *view in subviews){ 182 | if([view isKindOfClass:[UITextField class]]) 183 | [view setHidden:YES]; 184 | } 185 | [[self navigationController]setNavigationBarHidden:NO]; 186 | self.navigationController.toolbar.translucent = NO; 187 | } 188 | 189 | - (void)toggleColour:(id)sender event:(UIEvent*)event 190 | { 191 | UIView *targetedView = [[event.allTouches anyObject] view]; 192 | buttonsAnimationStartFrame = [self.view convertRect:targetedView.frame 193 | fromView:targetedView]; 194 | 195 | if (coloursShown) 196 | { 197 | [self hideColourButtons]; 198 | } 199 | else 200 | { 201 | [self showColourButtons]; 202 | } 203 | } 204 | 205 | - (void)showColourButtons 206 | { 207 | if (!coloursShown) { 208 | 209 | self.redButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; 210 | self.blueButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; 211 | self.greenButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; 212 | self.blackButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; 213 | 214 | self.redButton.alpha = 0.0; 215 | self.greenButton.alpha = 0.0; 216 | self.blueButton.alpha = 0.0; 217 | self.blackButton.alpha = 0.0; 218 | 219 | self.redButton.frame = CGRectMake(buttonsAnimationStartFrame.origin.x, 220 | buttonsAnimationStartFrame.origin.y, 50.0, 50.0); 221 | self.blueButton.frame = CGRectMake(buttonsAnimationStartFrame.origin.x, 222 | buttonsAnimationStartFrame.origin.y, 50.0, 50.0); 223 | self.greenButton.frame = CGRectMake(buttonsAnimationStartFrame.origin.x, 224 | buttonsAnimationStartFrame.origin.y, 50.0, 50.0); 225 | self.blackButton.frame = CGRectMake(buttonsAnimationStartFrame.origin.x, 226 | buttonsAnimationStartFrame.origin.y, 50.0, 50.0); 227 | 228 | self.redButton.backgroundColor = [UIColor redColor]; 229 | [self.redButton setTitleColor:[UIColor whiteColor] 230 | forState:UIControlStateNormal]; 231 | [self.redButton addTarget:self 232 | action:@selector(colourChanged:) 233 | forControlEvents:UIControlEventTouchDown]; 234 | 235 | [self.redButton.layer setMasksToBounds:YES]; 236 | [self.redButton.layer setCornerRadius:25.0f]; 237 | 238 | [self.redButton setTitle:@"Red" forState:UIControlStateNormal]; 239 | [self.view addSubview:self.redButton]; 240 | 241 | self.blueButton.backgroundColor = [UIColor blueColor]; 242 | [self.blueButton setTitleColor:[UIColor whiteColor] 243 | forState:UIControlStateNormal]; 244 | [self.blueButton addTarget:self 245 | action:@selector(colourChanged:) 246 | forControlEvents:UIControlEventTouchDown]; 247 | 248 | [self.blueButton.layer setMasksToBounds:YES]; 249 | [self.blueButton.layer setCornerRadius:25.0f]; 250 | 251 | [self.blueButton setTitle:@"Blue" forState:UIControlStateNormal]; 252 | [self.view addSubview:self.blueButton]; 253 | 254 | self.greenButton.backgroundColor = [UIColor greenColor]; 255 | [self.greenButton setTitleColor:[UIColor whiteColor] 256 | forState:UIControlStateNormal]; 257 | [self.greenButton addTarget:self 258 | action:@selector(colourChanged:) 259 | forControlEvents:UIControlEventTouchDown]; 260 | 261 | [self.greenButton.layer setMasksToBounds:YES]; 262 | [self.greenButton.layer setCornerRadius:25.0f]; 263 | 264 | [self.greenButton setTitle:@"Green" forState:UIControlStateNormal]; 265 | [self.view addSubview:self.greenButton]; 266 | 267 | self.blackButton.backgroundColor = [UIColor blackColor]; 268 | [self.blackButton setTitleColor:[UIColor whiteColor] 269 | forState:UIControlStateNormal]; 270 | [self.blackButton addTarget:self 271 | action:@selector(colourChanged:) 272 | forControlEvents:UIControlEventTouchDown]; 273 | 274 | [self.blackButton.layer setMasksToBounds:YES]; 275 | [self.blackButton.layer setCornerRadius:25.0f]; 276 | 277 | [self.blackButton setTitle:@"Black" forState:UIControlStateNormal]; 278 | [self.view addSubview:self.blackButton]; 279 | 280 | coloursShown = YES; 281 | 282 | float toolBarOriginY = self.navigationController.toolbar.frame.origin.y; 283 | [UIView animateWithDuration:0.3 animations:^{ 284 | self.redButton.frame = CGRectMake(10.0, toolBarOriginY - 4*(50.0 + 10.0), 50.0, 50.0); 285 | self.blueButton.frame = CGRectMake(10.0, toolBarOriginY - 3*(50.0 + 10.0), 50.0, 50.0); 286 | self.greenButton.frame = CGRectMake(10.0, toolBarOriginY - 2*(50.0 + 10.0), 50.0, 50.0); 287 | self.blackButton.frame = CGRectMake(10.0, toolBarOriginY - (50.0 + 10.0), 50.0, 50.0); 288 | 289 | self.redButton.alpha = 1.0; 290 | self.greenButton.alpha = 1.0; 291 | self.blueButton.alpha = 1.0; 292 | self.blackButton.alpha = 1.0; 293 | }]; 294 | 295 | } 296 | } 297 | 298 | - (void)colourChanged:(id)sender 299 | { 300 | NSLog(@"Colour changed."); 301 | [self hideColourButtons]; 302 | self.colour = [(UIButton *)sender backgroundColor]; 303 | 304 | [self setColourButtonColour]; 305 | } 306 | 307 | - (void)setColourButtonColour 308 | { 309 | if ([_btColour respondsToSelector:@selector(setTintColor:)]) 310 | { 311 | // TODO: make sure the word 'White' is visible when white is selected 312 | [_btColour setTintColor:self.colour]; 313 | } 314 | } 315 | 316 | - (void)hideColourButtons 317 | { 318 | if (coloursShown) { 319 | [UIView animateWithDuration:0.3 animations:^{ 320 | self.redButton.frame = CGRectMake(buttonsAnimationStartFrame.origin.x, 321 | buttonsAnimationStartFrame.origin.y, 50.0, 40.0); 322 | self.blueButton.frame = CGRectMake(buttonsAnimationStartFrame.origin.x, 323 | buttonsAnimationStartFrame.origin.y, 50.0, 40.0); 324 | self.greenButton.frame = CGRectMake(buttonsAnimationStartFrame.origin.x, 325 | buttonsAnimationStartFrame.origin.y, 50.0, 40.0); 326 | self.blackButton.frame = CGRectMake(buttonsAnimationStartFrame.origin.x, 327 | buttonsAnimationStartFrame.origin.y, 50.0, 40.0); 328 | 329 | self.redButton.alpha = 0.0; 330 | self.greenButton.alpha = 0.0; 331 | self.blueButton.alpha = 0.0; 332 | self.blackButton.alpha = 0.0; 333 | } completion:^(BOOL finished) { 334 | [self.redButton removeFromSuperview]; 335 | [self.blueButton removeFromSuperview]; 336 | [self.greenButton removeFromSuperview]; 337 | [self.blackButton removeFromSuperview]; 338 | }]; 339 | 340 | coloursShown = NO; 341 | } 342 | } 343 | 344 | - (void)viewDidUnload 345 | { 346 | [super viewDidUnload]; 347 | // Release any retained subviews of the main view. 348 | // e.g. self.myOutlet = nil; 349 | } 350 | 351 | - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx 352 | { 353 | if (layer == _canvasLayer) { 354 | if (_clearCanvasLayer) { 355 | CGContextClearRect(ctx, [_canvasLayer bounds]); 356 | _clearCanvasLayer = NO; 357 | } 358 | 359 | if (!_touching) { 360 | return; 361 | } 362 | 363 | // Add path to context 364 | CGContextAddPath(ctx, _path.CGPath); 365 | CGContextSetLineWidth(ctx, LINE_WIDTH); 366 | CGContextSetStrokeColorWithColor(ctx, [self.colour CGColor]); 367 | CGContextStrokePath(ctx); 368 | } else if (layer == _backgroundLayer) { 369 | // Save current context state 370 | CGContextSaveGState(ctx); 371 | 372 | // Fix cached image co-ordinates 373 | CGContextTranslateCTM(ctx, 0, [_backgroundLayer bounds].size.height); 374 | CGContextScaleCTM(ctx, 1.0, -1.0); 375 | 376 | if (_clearBackgroundLayer) { 377 | CGContextClearRect(ctx, [_canvasLayer bounds]); 378 | _clearBackgroundLayer = NO; 379 | } 380 | // Draw image 381 | CGImageRef ref = [self.cacheImage CGImage]; 382 | CGContextDrawImage(ctx, [_backgroundLayer bounds], ref); 383 | 384 | // Restore context to pre CTM state 385 | CGContextRestoreGState(ctx); 386 | } else { 387 | NSLog(@"drawLayer: inContext: unhandled layer = %@", layer); 388 | } 389 | } 390 | 391 | - (void)saveAll 392 | { 393 | NSLog(@"saveAll "); 394 | 395 | //composite the cacheImage and the backgroundImage 396 | CGSize size = CGSizeMake(_contentSize.width, _contentSize.height); 397 | 398 | UIGraphicsBeginImageContext(size); 399 | [self.backgroundImage drawInRect:CGRectMake(0.0f, 0.0f, size.width, size.height)]; 400 | UIImage* annotation = self.cacheImage; 401 | 402 | //scale the cacheImage to fit the new bounds 403 | [annotation drawInRect:CGRectMake(0.0f, 0.0f, size.width, size.height)]; 404 | UIImage *output = UIGraphicsGetImageFromCurrentImageContext(); 405 | UIGraphicsEndImageContext(); 406 | 407 | self.saved = YES; 408 | [self.delegate saveDrawing:output]; 409 | 410 | self.cacheImage = nil; 411 | } 412 | 413 | - (void)clearAll 414 | { 415 | NSLog(@"clearAll "); 416 | 417 | _hasDrawing = NO; 418 | 419 | // Clear the collections 420 | _clearCanvasLayer = YES; 421 | _clearBackgroundLayer = YES; 422 | 423 | //clear the cache image 424 | UIGraphicsBeginImageContext(_contentSize); 425 | UIColor *color = [UIColor clearColor]; 426 | [color set]; 427 | UIRectFill(CGRectMake(0.0, 0.0, _contentSize.width, _contentSize.height)); 428 | 429 | _cacheImage = UIGraphicsGetImageFromCurrentImageContext(); 430 | UIGraphicsEndImageContext(); 431 | 432 | // Redraw 433 | [_canvasLayer setNeedsDisplay]; 434 | [_backgroundLayer setNeedsDisplay]; 435 | } 436 | 437 | - (void)cancelAll 438 | { 439 | NSLog(@"cancelAll "); 440 | 441 | _hasDrawing = NO; 442 | [self.delegate cancelDrawing]; 443 | } 444 | 445 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 446 | { 447 | UITouch *t = [touches anyObject]; 448 | 449 | if ([touches count] > 1) { 450 | NSLog(@"touchesBegan [touches count] > 1"); 451 | } 452 | 453 | if (_touching) { 454 | return; 455 | } 456 | 457 | // Create a line for the value 458 | CGPoint loc = [t locationInView:self.backgroundView]; 459 | 460 | // Start a new path 461 | _path = [UIBezierPath bezierPath]; 462 | _path.lineWidth = LINE_WIDTH; 463 | _path.lineJoinStyle = kCGLineJoinRound; 464 | _path.flatness = .2; 465 | 466 | [_path moveToPoint:loc]; 467 | _pathPoint = loc; 468 | _touching = YES; 469 | _moved = NO; 470 | } 471 | 472 | static CGPoint CGPointMid(CGPoint a, CGPoint b) 473 | { 474 | return (CGPoint){ (a.x + b.x) / 2.0, (a.y +b.y) / 2.0 }; 475 | } 476 | 477 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 478 | { 479 | if (_touching) { 480 | // Update linesInProcess with moved touches 481 | UITouch *t = [touches anyObject]; 482 | CGPoint currentPoint = [t locationInView:self.backgroundView]; 483 | 484 | if ([touches count] > 1) { 485 | NSLog(@"touchesMoved [touches count] > 1"); 486 | } 487 | 488 | if (!_moved) { 489 | if (!CGPointEqualToPoint(currentPoint, _pathPoint)) { 490 | [_path addLineToPoint:currentPoint]; 491 | _pathPoint = currentPoint; 492 | } 493 | } else { 494 | CGPoint midPoint = CGPointMid(currentPoint, _pathPoint); 495 | 496 | // Update current path 497 | [_path addQuadCurveToPoint:currentPoint controlPoint:midPoint]; 498 | _pathPoint = currentPoint; 499 | } 500 | // Update the line 501 | [_canvasLayer setNeedsDisplay]; 502 | } else { 503 | NSLog(@"touchesMoved: not touching..."); 504 | } 505 | _moved = YES; 506 | } 507 | 508 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 509 | { 510 | if (!_touching) { 511 | return; 512 | } 513 | 514 | if ([touches count] > 1) { 515 | NSLog(@"endTouches [touches count] > 1"); 516 | } 517 | 518 | if (!_moved) { 519 | UITouch *t = [touches anyObject]; 520 | CGPoint loc = [t locationInView:self.backgroundView]; 521 | 522 | // If this is a single touch register as a 1 x 1 dot 523 | [_path addLineToPoint:loc]; 524 | } 525 | _moved = NO; 526 | 527 | // Create a new image context 528 | CGRect bl = [_backgroundLayer bounds]; 529 | UIGraphicsBeginImageContext(CGSizeMake(bl.size.width, bl.size.height)); 530 | CGContextRef ctx = UIGraphicsGetCurrentContext(); 531 | 532 | // Image is the current drawing context 533 | UIGraphicsPushContext(ctx); 534 | 535 | // Prevent white pixels from hiding already drawn lines 536 | CGContextSetBlendMode(ctx, kCGBlendModeDarken); 537 | 538 | if (self.cacheImage != nil) { 539 | // Draw the cached image to the context 540 | [self.cacheImage drawInRect:CGRectMake(0, 0, bl.size.width, bl.size.height)]; 541 | } 542 | 543 | // Blend the drawing layer into the image context 544 | [_canvasLayer drawInContext:ctx]; 545 | UIGraphicsPopContext(); 546 | 547 | // Store image context so we can add to it later 548 | self.cacheImage = UIGraphicsGetImageFromCurrentImageContext(); 549 | UIGraphicsEndImageContext(); 550 | 551 | _touching = NO; 552 | _path = nil; 553 | _hasDrawing = YES; 554 | 555 | // Redraw 556 | [_backgroundLayer setNeedsDisplay]; 557 | } 558 | 559 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 560 | { 561 | [self touchesEnded:touches withEvent:event]; 562 | } 563 | 564 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation 565 | { 566 | // Return YES for supported orientations 567 | return (interfaceOrientation == UIInterfaceOrientationPortrait); 568 | } 569 | 570 | - (void)dealloc 571 | { 572 | if (self.cacheImage != nil) { 573 | self.cacheImage = nil; 574 | } 575 | self.backgroundImage = nil; 576 | self.backgroundLayer = nil; 577 | self.canvasLayer = nil; 578 | self.backgroundLayer.delegate = nil; 579 | self.canvasLayer.delegate = nil; 580 | [self.backgroundLayer removeFromSuperlayer]; 581 | [self.canvasLayer removeFromSuperlayer]; 582 | self.tdv = nil; 583 | } 584 | 585 | - (void)viewWillDisappear:(BOOL)animated 586 | { 587 | [super viewWillDisappear:animated]; 588 | 589 | UINavigationBar *navBar = [[self navigationController] navigationBar]; 590 | NSArray *subviews = [navBar subviews]; 591 | for(UIView *view in subviews){ 592 | if([view isKindOfClass:[UITextField class]]) 593 | [view setHidden:NO]; 594 | } 595 | if([[self navigationController] isNavigationBarHidden]){ 596 | [[self navigationController] setNavigationBarHidden:NO]; 597 | [[self navigationController] setNavigationBarHidden:YES]; 598 | }else{ 599 | [[self navigationController] setNavigationBarHidden:YES]; 600 | [[self navigationController] setNavigationBarHidden:NO]; 601 | } 602 | [navBar layoutSubviews]; 603 | 604 | if (!self.saved) { 605 | [self cancelAll]; 606 | } 607 | 608 | //re-enable gesture recognizer 609 | for (UIGestureRecognizer *recognizer in 610 | self.navigationController.view.gestureRecognizers) { 611 | [recognizer setEnabled:YES]; 612 | } 613 | } 614 | 615 | @end 616 | -------------------------------------------------------------------------------- /src/ios/TouchDraw/UIImage+ImageWithColor.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface UIImage (ImageWithColor) 4 | + (UIImage *)imageWithColor:(UIColor *)color andSize:(CGRect)size; 5 | @end 6 | -------------------------------------------------------------------------------- /src/ios/TouchDraw/UIImage+ImageWithColor.m: -------------------------------------------------------------------------------- 1 | #import "UIImage+ImageWithColor.h" 2 | 3 | @implementation UIImage (ImageWithColor) 4 | + (UIImage *)imageWithColor:(UIColor *)color andSize:(CGRect)size { 5 | UIGraphicsBeginImageContext(size.size); 6 | CGContextRef context = UIGraphicsGetCurrentContext(); 7 | 8 | CGContextSetFillColorWithColor(context, [color CGColor]); 9 | CGContextFillRect(context, size); 10 | 11 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 12 | UIGraphicsEndImageContext(); 13 | 14 | return image; 15 | } 16 | @end 17 | -------------------------------------------------------------------------------- /src/windows/sketchProxy.js: -------------------------------------------------------------------------------- 1 | (function () { 'use strict'; var confirmOnSuccess, confirmOnError; MSApp.execUnsafeLocalFunction(function () { appendCanvasHtmlJs(); }); var html = ''; var popupHtml = '
'; function appendVeapicoreJs(){ var head = document.getElementsByTagName('head').item(0); var script; script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); script.setAttribute('src', '//Microsoft.WinJS.2.0/js/ui.js'); head.appendChild(script); } function appendCanvasHtmlJs(){ var head = document.getElementsByTagName('head').item(0); var script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); script.setAttribute('src', '/www/cordova-plugin-sketch/canvasHtml.js'); head.appendChild(script); } function appendProgramJs(){ var head = document.getElementsByTagName('head').item(0); var script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); script.setAttribute('src', '/www/cordova-plugin-sketch/program.js'); head.appendChild(script); } function callbackFunction(){}; function openPopup() { appendVeapicoreJs(); MSApp.execUnsafeLocalFunction(function () { document.head.insertAdjacentHTML('beforeend', html); var body = document.getElementsByTagName('body').item(0); body.setAttribute('role', 'application'); document.body.insertAdjacentHTML('beforeend', popupHtml); }); var popup = document.getElementById('cordova-plugin-sketch-popup'); var sketchDiv = document.getElementById('cordova-plugin-sketch-sketchDiv'); MSApp.execUnsafeLocalFunction(function () { sketchDiv.insertAdjacentHTML('beforeend', Sketch.canvasHtml); appendProgramJs(); if(Sketch.inputType > 0){ var canvas = document.getElementById('InkCanvas'); var ctx = canvas.getContext("2d"); var img = document.getElementById("canvasImg"); img.setAttribute('src', Sketch.inputData); } }); popup.classList.add('cordova-plugin-sketch-nativePopUp-open'); } function closePopup(stream) { callbackFunction(stream); deletePopup(); } function cancelPopup() { callbackFunction(null); deletePopup(); } function deletePopup(){ MSApp.execUnsafeLocalFunction(function () { var popup = document.getElementById("cordova-plugin-sketch-popup"); popup.classList.remove('cordova-plugin-sketch-nativePopUp-open'); popup.parentNode.removeChild(popup); }); } var Sketch = { InputType:{ NO_INPUT: 0, // no input as background image, use as signature plugin DATA_URL: 1, // base64 encoded string stream FILE_URI: 2, // file uri (content://media/external/images/media/2 for Android) }, DestinationType:{ DATA_URL: 0, // Return base64 encoded string FILE_URI: 1 // Return file uri (content://media/external/images/media/2 for Android) }, EncodingType:{ JPEG: 0, // Return JPEG encoded image PNG: 1 // Return PNG encoded image }, DataURLType:{ 0: 'image/jpeg', 1: 'image/png' } }; Sketch.canvasHtml = ''; Sketch.destinationType = Sketch.DestinationType.DATA_URL; Sketch.encodingType = Sketch.EncodingType.PNG; Sketch.inputType = Sketch.InputType.NO_INPUT; Sketch.inputData = ''; //options will contain: // 0 destinationType:Sketch.DestinationType.FILE_URI,Sketch.DestinationType.DATA_URL // 1 encodingType:Sketch.EncodingType.PNG,Sketch.EncodingType.JPEG Sketch.getSketch = function (onSuccess, onError, options) { callbackFunction = onSuccess; confirmOnSuccess = onSuccess; confirmOnError = onError; if(options.destinationType != null) Sketch.destinationType = options.destinationType; if(options.encodingType != null) Sketch.encodingType = options.encodingType; if(options.inputType != null) Sketch.inputType = options.inputType; if(Sketch.inputType > 0 && options.inputData){ Sketch.inputData = options.inputData; } openPopup(); }; Sketch.done = function (stream) { closePopup(stream); }; Sketch.cancel = function () { cancelPopup(); }; navigator.sketch = Sketch; }()); cordova.commandProxy.add("SketchPlugin",{ getSketch:navigator.sketch.getSketch }); -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jasmine": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | Cordova Sketch Plugin Tests 8 | Apache 2.0 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | exports.defineAutoTests = function () { 2 | describe('Sketch (navigator.sketch)', function () { 3 | it('should exist', function () { 4 | expect(navigator.sketch).toBeDefined(); 5 | }); 6 | 7 | it('should contain a getSketch method', function () { 8 | expect(navigator.sketch.getSketch).toBeDefined(); 9 | expect(typeof navigator.sketch.getSketch === 'function').toBe(true); 10 | }); 11 | 12 | it('should contain a DestinationType enum', function () { 13 | expect(navigator.sketch.DestinationType).toBeDefined(); 14 | expect(typeof navigator.sketch.DestinationType === 'object').toBe(true); 15 | }); 16 | 17 | it('should contain a EncodingType enum', function () { 18 | expect(navigator.sketch.EncodingType ).toBeDefined(); 19 | expect(typeof navigator.sketch.EncodingType === 'object').toBe(true); 20 | }); 21 | 22 | it('should contain a InputType enum', function () { 23 | expect(navigator.sketch.InputType).toBeDefined(); 24 | expect(typeof navigator.sketch.InputType === 'object').toBe(true); 25 | }); 26 | }); 27 | 28 | describe('DestinationType enum', function () { 29 | it('DestinationType should contain a DATA_URL field', function () { 30 | expect(navigator.sketch.DestinationType.DATA_URL).toBeDefined(); 31 | }); 32 | 33 | it('DestinationType should contain a FILE_URI field', function () { 34 | expect(navigator.sketch.DestinationType.FILE_URI).toBeDefined(); 35 | }); 36 | }); 37 | 38 | describe('EncodingType enum', function () { 39 | it('should contain a JPEG field', function () { 40 | expect(navigator.sketch.EncodingType.JPEG).toBeDefined(); 41 | }); 42 | 43 | it('should contain a PNG field', function () { 44 | expect(navigator.sketch.EncodingType.PNG).toBeDefined(); 45 | }); 46 | }); 47 | 48 | describe('InputType enum', function () { 49 | it('should contain a NO_INPUT field', function () { 50 | expect(navigator.sketch.InputType.NO_INPUT).toBeDefined(); 51 | }); 52 | 53 | it('should contain a DATA_URL field', function () { 54 | expect(navigator.sketch.InputType.DATA_URL).toBeDefined(); 55 | }); 56 | 57 | it('should contain a FILE_URI field', function () { 58 | expect(navigator.sketch.InputType.FILE_URI).toBeDefined(); 59 | }); 60 | }); 61 | 62 | describe('getSketch method', function () { 63 | 64 | beforeEach(function(done) { 65 | setTimeout(function() { 66 | done(); 67 | }, 1); 68 | }); 69 | 70 | describe('default options.', function () { 71 | var callback = function () {}; 72 | 73 | beforeEach(function () { 74 | spyOn(cordova, 'exec'); 75 | }); 76 | 77 | it('should pass DATA_URL when the destinationType option is not given', function () { 78 | var options; 79 | 80 | navigator.sketch.getSketch(callback, callback); 81 | expect(cordova.exec).toHaveBeenCalled(); 82 | 83 | options = cordova.exec.calls.argsFor(0)[4]; 84 | if (typeof options !== 'undefined') { 85 | expect(options[0].destinationType).toEqual(navigator.sketch.DestinationType.DATA_URL); 86 | } else { 87 | expect(options).toBeDefined(); // Fail the test 88 | } 89 | }); 90 | 91 | it('should pass PNG when the encodingType option is not given', function () { 92 | var options; 93 | 94 | navigator.sketch.getSketch(callback, callback); 95 | expect(cordova.exec).toHaveBeenCalled(); 96 | 97 | options = cordova.exec.calls.argsFor(0)[4]; 98 | if (typeof options !== 'undefined') { 99 | expect(options[0].encodingType).toEqual(navigator.sketch.EncodingType.PNG); 100 | } else { 101 | expect(options).toBeDefined(); // Fail the test 102 | } 103 | }); 104 | 105 | it('should pass NO_INPUT when the inputType option is not given', function () { 106 | var options; 107 | 108 | navigator.sketch.getSketch(callback, callback); 109 | expect(cordova.exec).toHaveBeenCalled(); 110 | 111 | options = cordova.exec.calls.argsFor(0)[4]; 112 | if (typeof options !== 'undefined') { 113 | expect(options[0].inputType).toEqual(navigator.sketch.InputType.NO_INPUT); 114 | } else { 115 | expect(options).toBeDefined(); // Fail the test 116 | } 117 | }); 118 | }); 119 | 120 | describe("long asynchronous specs", function() { 121 | var successCallback = null; 122 | var originalTimeout; 123 | var errorCallback = null; 124 | var inputData; 125 | beforeEach(function() { 126 | successCallback = jasmine.createSpy('successCallback'); 127 | errorCallback = jasmine.createSpy('errorCallback'); 128 | originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; 129 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000; 130 | }); 131 | 132 | it('should be called with a JPEG file URI when destinationType is FILE_URI, encoding type is JPEG', function (done) { 133 | var filetype = '.JPEG'; 134 | navigator.sketch.getSketch(successCallback, errorCallback, { 135 | destinationType: navigator.sketch.DestinationType.FILE_URI, 136 | encodingType: navigator.sketch.EncodingType.JPEG 137 | }); 138 | setTimeout(function() { 139 | expect(successCallback).toHaveBeenCalled(); 140 | if (successCallback.calls != null && successCallback.calls.mostRecent() != undefined) { 141 | expect(successCallback.calls.mostRecent().args[0].toUpperCase().indexOf(filetype)).toBeGreaterThan(0); 142 | } 143 | done(); 144 | }, 19000); 145 | }); 146 | 147 | it('should be called with a PNG file URI when destinationType is FILE_URI, encoding type is PNG', function (done) { 148 | var filetype = '.PNG'; 149 | navigator.sketch.getSketch(successCallback, errorCallback, { 150 | destinationType: navigator.sketch.DestinationType.FILE_URI, 151 | encodingType: navigator.sketch.EncodingType.PNG 152 | }); 153 | setTimeout(function() { 154 | expect(successCallback).toHaveBeenCalled(); 155 | if (successCallback.calls != null && successCallback.calls.mostRecent() != undefined) { 156 | expect(successCallback.calls.mostRecent().args[0].toUpperCase().indexOf(filetype)).toBeGreaterThan(0); 157 | } 158 | done(); 159 | }, 19000); 160 | }); 161 | 162 | 163 | it('should be called with a JPEG encoded data stream when destinationType is DATA_URL, encoding type is JPEG', function (done) { 164 | navigator.sketch.getSketch(successCallback, errorCallback , { 165 | destinationType: navigator.sketch.DestinationType.DATA_URL, 166 | encodingType: navigator.sketch.EncodingType.JPEG 167 | }); 168 | setTimeout(function() { 169 | expect(successCallback).toHaveBeenCalled(); 170 | if (successCallback.calls != null && successCallback.calls.mostRecent() != undefined) { 171 | expect(successCallback.calls.mostRecent().args[0].indexOf('data:image/jpeg')).toBe(0); 172 | } 173 | done(); 174 | }, 19000); 175 | }); 176 | 177 | it('should be called with a PNG encoded data stream when destinationType is DATA_URL, encoding type is PNG', function (done) { 178 | navigator.sketch.getSketch(successCallback, errorCallback , { 179 | destinationType: navigator.sketch.DestinationType.DATA_URL, 180 | encodingType: navigator.sketch.EncodingType.PNG 181 | }); 182 | setTimeout(function() { 183 | expect(successCallback).toHaveBeenCalled(); 184 | if (successCallback.calls != null && successCallback.calls.mostRecent() != undefined) { 185 | expect(successCallback.calls.mostRecent().args[0].indexOf('data:image/png')).toBe(0); 186 | inputData = successCallback.calls.mostRecent().args[0]; 187 | } 188 | done(); 189 | }, 19000); 190 | }); 191 | 192 | it('should be called with a PNG encoded data stream when destinationType is DATA_URL, encoding type is PNG, inputType is DATA_URL', function (done) { 193 | navigator.sketch.getSketch(successCallback, errorCallback , { 194 | destinationType: navigator.sketch.DestinationType.DATA_URL, 195 | encodingType: navigator.sketch.EncodingType.PNG, 196 | inputType : navigator.sketch.InputType.DATA_URL, 197 | inputData : inputData 198 | }); 199 | setTimeout(function() { 200 | expect(successCallback).toHaveBeenCalled(); 201 | if (successCallback.calls != null && successCallback.calls.mostRecent() != undefined) { 202 | expect(successCallback.calls.mostRecent().args[0].indexOf('data:image/png')).toBe(0); 203 | } 204 | inputData = null; 205 | done(); 206 | }, 19000); 207 | }); 208 | 209 | afterEach(function() { 210 | jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; 211 | }); 212 | }); 213 | }); 214 | }; 215 | -------------------------------------------------------------------------------- /www/canvasHtml.js: -------------------------------------------------------------------------------- 1 | if(navigator.sketch != null && navigator.sketch != undefined){ 2 | navigator.sketch.canvasHtml = ''+ 3 | ' '+ 5 | ' '+ 12 | ' '+ 13 | '
'+ 14 | ' '+ 16 | '
'+ 17 | ' '+ 21 | '
'+ 31 | '
'+ 32 | '
'+ 38 | '
'+ 39 | '
'+ 45 | '
'+ 46 | ' '+ 49 | ' '; 50 | } 51 | -------------------------------------------------------------------------------- /www/css/cordova-plugin-sketch.css: -------------------------------------------------------------------------------- 1 | *, *::after, *::before { 2 | box-sizing: inherit; 3 | } 4 | 5 | .cordova-plugin-sketch-layout-withOrientation { 6 | flex-direction: column; 7 | } 8 | .cordova-plugin-sketch-layout-oppositeOrientation { 9 | flex-direction: row; 10 | } 11 | @media (orientation: landscape) { 12 | .cordova-plugin-sketch-layout-withOrientation { 13 | flex-direction: row; 14 | } 15 | .cordova-plugin-sketch-layout-oppositeOrientation { 16 | flex-direction: column; 17 | } 18 | } 19 | 20 | .cordova-plugin-sketch-nativePopUp { 21 | background: #000; 22 | box-sizing: border-box; 23 | display: none; 24 | height: 100%; 25 | left: 0; 26 | margin: 0; 27 | position: fixed; 28 | top: 0; 29 | width: 100%; 30 | z-index: 99999; 31 | } 32 | 33 | .cordova-plugin-sketch-nativePopUp-open { 34 | display: flex; 35 | } 36 | 37 | @media (orientation: landscape) {} 38 | 39 | .cordova-plugin-sketch-nativePopUp__button, 40 | .cordova-plugin-sketch-nativePopUp__content { 41 | margin: 0px; 42 | } 43 | 44 | .cordova-plugin-sketch-nativePopUp__content { 45 | background: #000; 46 | flex-grow: 1; 47 | flex-shrink: 1; 48 | } 49 | 50 | .cordova-plugin-sketch-nativePopUp__buttons { 51 | display: flex; 52 | flex-grow: 0; 53 | flex-shrink: 1; 54 | } 55 | 56 | .cordova-plugin-sketch-nativePopUp__button { 57 | flex-grow: 1; 58 | flex-shrink: 1; 59 | min-height: 44px; 60 | min-width: 44px; 61 | } 62 | 63 | button[data-action=cancel] { 64 | } 65 | button[data-action=done] { 66 | } 67 | -------------------------------------------------------------------------------- /www/css/program.css: -------------------------------------------------------------------------------- 1 |  2 | 3 | * 4 | { 5 | overflow: hidden; 6 | -ms-content-zooming: none; 7 | } 8 | 9 | body 10 | { 11 | touch-action: none; 12 | } 13 | 14 | #canvasGroup 15 | { 16 | display: block; 17 | position: fixed; 18 | left: 0px; 19 | right: 0px; 20 | top: 0px; 21 | bottom: 0px; 22 | width: 100%; 23 | height: 100%; 24 | overflow: scroll; 25 | z-index: 0; 26 | } 27 | 28 | div.rectangle 29 | { 30 | display: block; 31 | position: absolute; 32 | transform-origin: 0% 0%; 33 | touch-action: none; 34 | } 35 | 36 | canvas.surface 37 | { 38 | width: 100%; 39 | height: 100%; 40 | display: block; 41 | position: absolute; 42 | } 43 | 44 | #HighlightCanvas 45 | { 46 | background-color: White; 47 | z-index: 1; 48 | } 49 | 50 | #InkCanvas 51 | { 52 | background-color: White; 53 | z-index: 2; 54 | } 55 | 56 | #SelectCanvas 57 | { 58 | background-color: rgba(0, 0, 0, 0.0); 59 | z-index: 3; 60 | } 61 | 62 | #SelectionBox 63 | { 64 | background-color: rgba(0, 0, 0, 0.0); 65 | z-index: 4; 66 | } 67 | 68 | #statusMessage 69 | { 70 | position: relative; 71 | left: 125px; 72 | top: 70px; 73 | z-index: 10; 74 | color: black; 75 | font-size: 16pt; 76 | height: 32px; 77 | padding: 0px; 78 | border: 0px; 79 | } 80 | 81 | #RecoFlyout 82 | { 83 | color: black; 84 | } 85 | 86 | #Word 87 | { 88 | position: absolute; 89 | z-index: 20; 90 | visibility: hidden; 91 | } 92 | 93 | #Black {color: Black;} 94 | #Blue {color: Blue; } 95 | #Red {color: Red; } 96 | #Green {color: Green;} 97 | #Yellow {color: Yellow;} 98 | 99 | #Aqua {background-color: Aqua; } 100 | #Lime {background-color: Lime; } 101 | -------------------------------------------------------------------------------- /www/images/Erase.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneblink/cordova-plugin-sketch/b5c542d4597f2b0b572a1e986c5a38736ad0f32c/www/images/Erase.cur -------------------------------------------------------------------------------- /www/images/Select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneblink/cordova-plugin-sketch/b5c542d4597f2b0b572a1e986c5a38736ad0f32c/www/images/Select.png -------------------------------------------------------------------------------- /www/program.js: -------------------------------------------------------------------------------- 1 | // We are using Windows.UI.Input.Inking.InkManager. 2 | 3 | function showMessage(message, isError) 4 | { 5 | var statusDiv = document.getElementById("statusMessage"); 6 | if (statusDiv) 7 | { 8 | statusDiv.innerText = message; 9 | statusDiv.style.color = isError ? "blue" : "green"; 10 | } 11 | } 12 | 13 | function displayStatus(message) 14 | { 15 | showMessage(message, false); 16 | } 17 | 18 | function displayError(message) 19 | { 20 | showMessage(message, true); 21 | } 22 | 23 | window.onerror = function (msg, url, line) { displayError("Error: " + msg + " url = " + url + " line = " + line); }; 24 | 25 | // Functions to convert from and to the 32-bit int used to represent color in Windows.UI.Input.Inking.InkManager. 26 | 27 | // Convenience function used by color converters. 28 | // Assumes arg num is a number (0..255); we convert it into a 2-digit hex string. 29 | 30 | function byteHex(num) 31 | { 32 | var hex = num.toString(16); 33 | if (hex.length === 1) 34 | { 35 | hex = "0" + hex; 36 | } 37 | return hex; 38 | } 39 | 40 | // Convert from Windows.UI.Input.Inking's color code to html's color hex string. 41 | 42 | function toColorString(color) 43 | { 44 | return "#" + byteHex(color.r) + byteHex(color.g) + byteHex(color.b); 45 | } 46 | 47 | // Convert from the few color names used in this app to Windows.UI.Input.Inking's color code. 48 | // If it isn't one of those, then decode the hex string. Otherwise return gray. 49 | // The alpha component is always set to full (255). 50 | function toColorStruct(color) 51 | { 52 | switch (color) 53 | { 54 | // Ink colors 55 | case "Black": 56 | return Windows.UI.Colors.black; 57 | case "Blue": 58 | return Windows.UI.Colors.blue; 59 | case "Red": 60 | return Windows.UI.Colors.red; 61 | case "Yellow": 62 | return Windows.UI.Colors.yellow; 63 | case "Green": 64 | return Windows.UI.Colors.green; 65 | 66 | // Highlighting colors 67 | case "Aqua": 68 | return Windows.UI.Colors.aqua; 69 | case "Lime": 70 | return Windows.UI.Colors.lime; 71 | 72 | // Select colors 73 | case "Gold": 74 | return Windows.UI.Colors.gold; 75 | 76 | case "White": 77 | return Windows.UI.Colors.white; 78 | } 79 | 80 | if ((color.length === 7) && (color.charAt(0) === "#")) 81 | { 82 | var R = parseInt(color.substr(1, 2), 16); 83 | var G = parseInt(color.substr(3, 2), 16); 84 | var B = parseInt(color.substr(5, 2), 16); 85 | return Windows.UI.ColorHelper.fromArgb(255, R, G, B); 86 | } 87 | 88 | return Windows.UI.Colors.gray; 89 | } 90 | 91 | // Global variable representing the application. 92 | var app; 93 | 94 | // Global variables representing the ink interface. 95 | // The usage of a global variable for drawingAttributes is not completely necessary, 96 | // just a convenience. One could always re-fetch the current drawingAttributes 97 | // from the inkManager. 98 | var inkManager = new Windows.UI.Input.Inking.InkManager(); 99 | var drawingAttributes = new Windows.UI.Input.Inking.InkDrawingAttributes(); 100 | drawingAttributes.fitToCurve = true; 101 | inkManager.setDefaultDrawingAttributes(drawingAttributes); 102 | 103 | // These are the global canvases (and their 2D contexts) for highlighting, for drawing ink, 104 | // and for lassoing (and erasing). 105 | var hlCanvas; 106 | var hlContext; 107 | var inkCanvas; 108 | var inkContext; 109 | var selCanvas; 110 | var selContext; 111 | 112 | // The "mode" of whether we are highlighting, inking, lassoing, or erasing is controlled by this global variable, 113 | // which should be pointing to either hlContext, inkContext, or selContext. 114 | // In lassoing mode (when context points to selContext), we might also be in erasing mode; 115 | // the state of lassoing vs. erasing is kept inside the ink manager, in attribute "mode", which will 116 | // have a value from enum Windows.UI.Input.Inking.InkManipulationMode, one of either "selecting" 117 | // or "erasing" (the other value being "inking" but in that case context will be pointing to one of the other 118 | // 2 canvases). 119 | var context; 120 | 121 | // Three functions to save and restore the current mode, and to clear this state. 122 | 123 | // Note that we can get into erasing mode in one of two ways: there is a eraser button in the toolbar, 124 | // and some pens have an active back end that is meant to represent erasing. If we get into erasing 125 | // mode via the button, we stay in that mode until another button is pushed. If we get into erasing 126 | // mode via the eraser end of the stylus, we should switch out of it when the user switches to the ink 127 | // end of the stylus. And we want to return to the mode we were in before this happened. Thus we 128 | // maintain a shallow stack (depth 1) of "mode" info. 129 | 130 | var savedContext = null; 131 | var savedStyle = null; 132 | var savedCursor = null; 133 | var savedMode = null; 134 | 135 | function clearMode() 136 | { 137 | //appbar.hide(); 138 | savedContext = null; 139 | savedStyle = null; 140 | savedCursor = null; 141 | savedMode = null; 142 | } 143 | 144 | function saveMode() 145 | { 146 | if (!savedContext) 147 | { 148 | savedStyle = context.strokeStyle; 149 | savedContext = context; 150 | savedCursor = selCanvas.style.cursor; 151 | savedMode = inkManager.mode; 152 | } 153 | } 154 | 155 | function restoreMode() 156 | { 157 | if (savedContext) 158 | { 159 | context = savedContext; 160 | context.strokeStyle = savedStyle; 161 | inkManager.mode = savedMode; 162 | selCanvas.style.cursor = savedCursor; 163 | clearMode(); 164 | } 165 | } 166 | 167 | // Global variable representing the pattern used when in select mode. This is an 8*1 image with 4 bits set, 168 | // then 4 bits cleared, to give us a dashed line when drawing a lasso. 169 | var selPattern; 170 | 171 | // Global variable representing the application toolbar at the bottom of the screen. 172 | var appbar; 173 | 174 | // Global pointers to flyouts invoked by the appbar. 175 | var findFlyout; 176 | var inkColorsFlyout; 177 | var inkWidthsFlyout; 178 | var hlColorsFlyout; 179 | var hlWidthsFlyout; 180 | var moreFlyout; 181 | 182 | // Global pointer to the flyout used for displaying recognition results (top 5 alternates), 183 | // and an array of buttons (one per alternate). 184 | var recoFlyout; 185 | var clipButtons; 186 | 187 | // Global pointer to the invisible
that marks the location of the currently selected word. 188 | var wordDiv; 189 | 190 | // Global pointer to the text buffer inside the Find flyout. 191 | var findText; 192 | 193 | // Returns true if any strokes inside the ink manager are selected; false otherwise. 194 | function anySelected() 195 | { 196 | var strokes = inkManager.getStrokes(); 197 | var len = strokes.length; 198 | for (var i = 0; i < len; i++) 199 | { 200 | if (strokes[i].selected) 201 | { 202 | return true; 203 | } 204 | } 205 | return false; 206 | } 207 | 208 | //Returns true if this stroke is a highlighting stroke. 209 | function isHighlighting(stroke) 210 | { 211 | var att = stroke.drawingAttributes; 212 | return att.color.a < 200; 213 | } 214 | 215 | // Makes all strokes a part of the selection. 216 | function selectAll() 217 | { 218 | inkManager.getStrokes().forEach(function (stroke) { 219 | stroke.selected = true; 220 | }); 221 | } 222 | 223 | // Makes all non-highlight strokes a part of the selection. 224 | function selectAllNoHighlight() 225 | { 226 | inkManager.getStrokes().forEach(function (stroke) { 227 | if (!isHighlighting(stroke)) { 228 | stroke.selected = true; 229 | } 230 | }); 231 | } 232 | 233 | // Unselects any strokes which are highlighting. 234 | function unselectHighlight() 235 | { 236 | inkManager.getStrokes().forEach(function (stroke) { 237 | if (stroke.selected && isHighlighting(stroke)) { 238 | stroke.selected = false; 239 | } 240 | }); 241 | } 242 | 243 | // Returns true if the point represented by x,y is within the rect. 244 | function inRect(x, y, rect) 245 | { 246 | return ((rect.x <= x) && (x < (rect.x + rect.width)) && 247 | (rect.y <= y) && (y < (rect.y + rect.height))); 248 | } 249 | 250 | // Tests the array of results bounding boxes (from the recognition results on the ink manager). 251 | // Returns an object representing the results, with the original touch coordinates, the bounding 252 | // box, the index of the result, the array of strokes, and the array of alternates (recognition strings). 253 | // If recognition has not run or touch is outside of any word bounding box, then returns null. 254 | function hitTest(tx, ty) 255 | { 256 | var results = inkManager.getRecognitionResults(); 257 | var cWords = results.size; 258 | 259 | if (cWords === 0) 260 | { 261 | return null; 262 | } 263 | 264 | for (var i = 0; i < cWords; i++) 265 | { 266 | var rect = results[i].boundingRect; 267 | if (inRect(tx, ty, rect)) 268 | { 269 | return { 270 | index: i, 271 | handleX: tx, // Original touch point 272 | handleY: ty, 273 | strokes: results[i].getStrokes(), 274 | rect: rect, 275 | alternates: results[i].getTextCandidates() 276 | }; 277 | } 278 | } 279 | return null; 280 | } 281 | 282 | // Note that we cannot just set the width in stroke.drawingAttributes.size.width, 283 | // or the color in stroke.drawingAttributes.color. 284 | // The stroke API supports get and put operations for drawingAttributes, 285 | // but we must execute those operations separately, and change any values 286 | // inside drawingAttributes between those operations. 287 | 288 | // Change the color and width in the default (used for new strokes) to the values 289 | // currently set in the current context. 290 | function setDefaults() 291 | { 292 | var strokeSize = drawingAttributes.size; 293 | strokeSize.width = strokeSize.height = context.lineWidth; 294 | drawingAttributes.size = strokeSize; 295 | 296 | var color = toColorStruct(context.strokeStyle); 297 | color.a = (context === hlContext) ? 128 : 255; 298 | drawingAttributes.color = color; 299 | inkManager.setDefaultDrawingAttributes(drawingAttributes); 300 | } 301 | 302 | // Four functions to switch back and forth between ink mode, highlight mode, select mode, and erase mode. 303 | // There is also a temp erase mode, which uses the saveMode()/restoreMode() functions to 304 | // return us to our previous mode when done erasing. This is used for quick erasers using the back end 305 | // of the pen (for those pens that have that). 306 | // NOTE: The erase modes also attempt to set the mouse/pen cursor to the image of a chalkboard eraser 307 | // (stored in images/erase.cur), but as of this writing cursor switching is not working. 308 | 309 | function highlightMode() 310 | { 311 | clearMode(); 312 | context = hlContext; 313 | inkManager.mode = Windows.UI.Input.Inking.InkManipulationMode.inking; 314 | setDefaults(); 315 | selCanvas.style.cursor = "default"; 316 | } 317 | 318 | function inkMode() 319 | { 320 | clearMode(); 321 | context = inkContext; 322 | inkManager.mode = Windows.UI.Input.Inking.InkManipulationMode.inking; 323 | setDefaults(); 324 | selCanvas.style.cursor = "default"; 325 | } 326 | 327 | function selectMode() 328 | { 329 | clearMode(); 330 | selContext.strokeStyle = selPattern; 331 | context = selContext; 332 | inkManager.mode = Windows.UI.Input.Inking.InkManipulationMode.selecting; 333 | selCanvas.style.cursor = "default"; 334 | } 335 | 336 | function eraseMode() 337 | { 338 | clearMode(); 339 | selContext.strokeStyle = "rgba(255,255,255,0.0)"; 340 | context = selContext; 341 | inkManager.mode = Windows.UI.Input.Inking.InkManipulationMode.erasing; 342 | selCanvas.style.cursor = "url(/www/cordova-plugin-sketch/images/erase.cur), auto"; 343 | } 344 | 345 | function tempEraseMode() 346 | { 347 | saveMode(); 348 | selContext.strokeStyle = "rgba(255,255,255,0.0)"; 349 | context = selContext; 350 | inkManager.mode = inkManager.mode = Windows.UI.Input.Inking.InkManipulationMode.erasing; 351 | selCanvas.style.cursor = "url(/www/cordova-plugin-sketch/images/erase.cur), auto"; 352 | } 353 | 354 | // Set the width of a stroke. Return true if we actually changed it. 355 | // Note that we cannot just set the width in stroke.drawingAttributes.size.width. 356 | // The stroke API supports get and put operations for drawingAttributes, 357 | // but we must execute those operations separately, and change any values 358 | // inside drawingAttributes between those operations. 359 | function shapeStroke(stroke, width) 360 | { 361 | var att = stroke.drawingAttributes; 362 | var strokeSize = att.size; 363 | if (strokeSize.width !== width) 364 | { 365 | strokeSize.width = strokeSize.height = width; 366 | att.size = strokeSize; 367 | stroke.drawingAttributes = att; 368 | return true; 369 | } 370 | else 371 | { 372 | return false; 373 | } 374 | } 375 | 376 | // Set the color (and alpha) of a stroke. Return true if we actually changed it. 377 | // Note that we cannot just set the color in stroke.drawingAttributes.color. 378 | // The stroke API supports get and put operations for drawingAttributes, 379 | // but we must execute those operations separately, and change any values 380 | // inside drawingAttributes between those operations. 381 | function colorStroke(stroke, color) 382 | { 383 | var att = stroke.drawingAttributes; 384 | var clr = toColorStruct(color); 385 | if (att.color !== clr) 386 | { 387 | att.color = clr; 388 | stroke.drawingAttributes = att; 389 | return true; 390 | } 391 | else 392 | { 393 | return false; 394 | } 395 | } 396 | 397 | // Global memory of the current pointID (for pen, and, separately, for touch). 398 | // We ignore handlePointerMove() and handlePointerUp() calls that don't use the same 399 | // pointID as the most recent handlePointerDown() call. This is because the user sometimes 400 | // accidentally nudges the mouse while inking or touching. This can cause move events 401 | // for that mouse that have different x,y coordinates than the ink trace or touch path 402 | // we are currently handling. 403 | 404 | // pointer* events maintain this pointId so that one can track individual fingers, 405 | // the pen, and the mouse. 406 | 407 | // Note that when the pen fails to leave the area where it can be sensed, it does NOT 408 | // get a new ID; so it is possible for 2 or more consecutive strokes to have the same ID. 409 | 410 | var penID = -1; 411 | 412 | // This global variable holds a reference to the div that is imposed on top of selected ink. 413 | // It is used to register event handlers that allow the user to move around selected ink. 414 | var selBox; 415 | 416 | // Hides the (transparent) div that is used to capture events for moving selected ink 417 | function anchorSelection() { 418 | // Make selBox of size 0 and move it to the top-left corner 419 | selBox.style.left = "0px"; 420 | selBox.style.top = "0px"; 421 | selBox.style.width = "0px"; 422 | selBox.style.height = "0px"; 423 | } 424 | 425 | // Places the (transparent) div that is used to capture events for moving selected ink. 426 | // The assumption is that rect is the bounding box of the selected ink. 427 | function detachSelection(rect) { 428 | // Move and resize selBox so that it perfectly overlaps with rect 429 | selBox.rect = rect; 430 | selBox.style.left = selBox.rect.x + "px"; 431 | selBox.style.top = selBox.rect.y + "px"; 432 | selBox.style.width = selBox.rect.width + "px"; 433 | selBox.style.height = selBox.rect.height + "px"; 434 | } 435 | 436 | // We will accept pen down or mouse left down as the start of a stroke. 437 | // We will accept touch down or mouse right down as the start of a touch. 438 | function handlePointerDown(evt) 439 | { 440 | try 441 | { 442 | //appbar.hide(); 443 | 444 | if ((evt.pointerType === "pen") || ((evt.pointerType === "mouse") && (evt.button === 0))) 445 | { 446 | // Anchor and clear any current selection. 447 | anchorSelection(); 448 | var pt = { x: 0.0, y: 0.0 }; 449 | inkManager.selectWithLine(pt, pt); 450 | 451 | pt = evt.currentPoint; 452 | 453 | if (pt.properties.isEraser) // the back side of a pen, which we treat as an eraser 454 | { 455 | tempEraseMode(); 456 | } 457 | else 458 | { 459 | restoreMode(); 460 | } 461 | 462 | context.beginPath(); 463 | context.moveTo(pt.rawPosition.x, pt.rawPosition.y); 464 | 465 | inkManager.processPointerDown(pt); 466 | penID = evt.pointerId; 467 | } 468 | else if (evt.pointerType === "touch") 469 | { 470 | // Start the processing of events related to this pointer as part of a gesture. 471 | // In this sample we are interested in MSGestureTap event, which we use to show alternates. See handleTap event handler. 472 | selCanvas.gestureObject.addPointer(evt.pointerId); 473 | } 474 | } 475 | catch (e) 476 | { 477 | displayError("handlePointerDown " + e.toString()); 478 | } 479 | } 480 | 481 | function handlePointerMove(evt) 482 | { 483 | try 484 | { 485 | if (evt.pointerId === penID) 486 | { 487 | var pt = evt.currentPoint; 488 | context.lineTo(pt.rawPosition.x, pt.rawPosition.y); 489 | context.stroke(); 490 | // Get all the points we missed and feed them to inkManager. 491 | // The array pts has the oldest point in position length-1; the most recent point is in position 0. 492 | // Actually, the point in position 0 is the same as the point in pt above (returned by evt.currentPoint). 493 | var pts = evt.intermediatePoints; 494 | for (var i = pts.length - 1; i >= 0 ; i--) 495 | { 496 | inkManager.processPointerUpdate(pts[i]); 497 | } 498 | } 499 | 500 | // No need to process touch events - selCanvas.gestureObject takes care of them and triggers MSGesture* events. 501 | } 502 | catch (e) 503 | { 504 | displayError("handlePointerMove " + e.toString()); 505 | } 506 | } 507 | 508 | function handlePointerUp(evt) 509 | { 510 | try 511 | { 512 | if (evt.pointerId === penID) 513 | { 514 | penID = -1; 515 | var pt = evt.currentPoint; 516 | context.lineTo(pt.rawPosition.x, pt.rawPosition.y); 517 | context.stroke(); 518 | context.closePath(); 519 | 520 | var rect = inkManager.processPointerUp(pt); 521 | if (inkManager.mode === Windows.UI.Input.Inking.InkManipulationMode.selecting) 522 | { 523 | detachSelection(rect); 524 | } 525 | 526 | renderAllStrokes(); 527 | } 528 | } 529 | catch (e) 530 | { 531 | displayError("handlePointerUp " + e.toString()); 532 | } 533 | } 534 | 535 | // We treat the event of the pen leaving the canvas as the same as the pen lifting; 536 | // it completes the stroke. 537 | function handlePointerOut(evt) 538 | { 539 | try 540 | { 541 | if (evt.pointerId === penID) 542 | { 543 | var pt = evt.currentPoint; 544 | context.lineTo(pt.rawPosition.x, pt.rawPosition.y); 545 | context.stroke(); 546 | context.closePath(); 547 | inkManager.processPointerUp(pt); 548 | penID = -1; 549 | renderAllStrokes(); 550 | } 551 | } 552 | catch (e) 553 | { 554 | displayError("handlePointerOut " + e.toString()); 555 | } 556 | } 557 | 558 | function handleTap(evt) 559 | { 560 | //appbar.hide(); 561 | 562 | // Anchor and clear any current selection. 563 | if (anySelected()) 564 | { 565 | anchorSelection(); 566 | var pt = { x: 0.0, y: 0.0 }; 567 | inkManager.selectWithLine(pt, pt); 568 | renderAllStrokes(); 569 | } 570 | 571 | var touchedResults = hitTest(evt.offsetX, evt.offsetY); 572 | if (touchedResults) 573 | { 574 | touchWord(touchedResults); 575 | } 576 | } 577 | 578 | function handleSelectionBoxPointerDown(evt) 579 | { 580 | //appbar.hide(); 581 | 582 | // Start the processing of events related to this pointer as part of a gesture. 583 | // In this sample we are interested in MSGestureChange event, which we use to move selected ink. 584 | // See handleSelectionBoxGestureChange event handler. 585 | selBox.gestureObject.addPointer(evt.pointerId); 586 | } 587 | 588 | function handleSelectionBoxGestureChange(evt) 589 | { 590 | // Move selection box 591 | selBox.rect.x += evt.translationX; 592 | selBox.rect.y += evt.translationY; 593 | selBox.style.left = selBox.rect.x + "px"; 594 | selBox.style.top = selBox.rect.y + "px"; 595 | 596 | // Move selected ink 597 | inkManager.moveSelected({x: evt.translationX, y: evt.translationY}); 598 | 599 | renderAllStrokes(); 600 | } 601 | 602 | //we may need figure out a way to show different commands according to different plugin 603 | function setView() 604 | { 605 | if (window.innerWidth <= 320) { 606 | appbar.showOnlyCommands(["Clear", "Done", "Cancel"], true); 607 | // } else if (window.innerWidth <= 768) { 608 | // appbar.showOnlyCommands(["Save", "Done", "Clear", "Cancel"], true); 609 | } else { 610 | appbar.showCommands(["InkColors", "InkWidth", "ModeErase", "Done", "Clear", "Cancel"], true); 611 | } 612 | } 613 | 614 | //Draws a single stroke into a specified canvas 2D context, with a specified color and width. 615 | function renderStroke(stroke, color, width, ctx) 616 | { 617 | ctx.save(); 618 | 619 | try 620 | { 621 | ctx.beginPath(); 622 | ctx.strokeStyle = color; 623 | ctx.lineWidth = width; 624 | 625 | var first = true; 626 | stroke.getRenderingSegments().forEach(function (segment) 627 | { 628 | if (first) 629 | { 630 | ctx.moveTo(segment.position.x, segment.position.y); 631 | first = false; 632 | } 633 | else 634 | { 635 | ctx.bezierCurveTo(segment.bezierControlPoint1.x, segment.bezierControlPoint1.y, 636 | segment.bezierControlPoint2.x, segment.bezierControlPoint2.y, 637 | segment.position.x, segment.position.y); 638 | } 639 | }); 640 | 641 | ctx.stroke(); 642 | ctx.closePath(); 643 | 644 | ctx.restore(); 645 | } 646 | catch (e) 647 | { 648 | ctx.restore(); 649 | displayError("renderStroke " + e.toString()); 650 | } 651 | } 652 | 653 | // This draws a basic notepaper pattern into the highlight canvas, which is the lowest canvas. 654 | // It has a single vertical dark red line defining the left margin, and a series of horizontal blue lines. 655 | function renderPaper() 656 | { 657 | var height = hlCanvas.height; 658 | var bottom = height - 0.5; 659 | var right = hlCanvas.width - 0.5; 660 | 661 | hlContext.save(); 662 | inkContext.save(); 663 | if(navigator.sketch.inputType > 0){ 664 | var img = document.getElementById("canvasImg"); 665 | hlContext.drawImage(img, 0, 0, inkCanvas.width, inkCanvas.height); 666 | inkContext.drawImage(img, 0, 0, inkCanvas.width, inkCanvas.height); 667 | } 668 | } 669 | 670 | // Redraws (from the beginning) all strokes in the canvases. All canvases are erased, 671 | // then the paper is drawn, then all the strokes are drawn. 672 | function renderAllStrokes() 673 | { 674 | selContext.clearRect(0, 0, selCanvas.width, selCanvas.height); 675 | inkContext.clearRect(0, 0, inkCanvas.width, inkCanvas.height); 676 | hlContext.clearRect(0, 0, hlCanvas.width, hlCanvas.height); 677 | 678 | renderPaper(); 679 | 680 | inkManager.getStrokes().forEach(function (stroke) 681 | { 682 | var att = stroke.drawingAttributes; 683 | var color = toColorString(att.color); 684 | var strokeSize = att.size; 685 | var width = strokeSize.width; 686 | var hl = isHighlighting(stroke); 687 | var ctx = hl ? hlContext : inkContext; 688 | 689 | if (stroke.selected) 690 | { 691 | renderStroke(stroke, color, width * 2, ctx); 692 | var stripe = hl ? "Azure" : "White"; 693 | var w = width - (hl ? 3 : 1); 694 | renderStroke(stroke, stripe, w, ctx); 695 | } 696 | else 697 | { 698 | renderStroke(stroke, color, width, ctx); 699 | } 700 | }); 701 | } 702 | 703 | function clear() 704 | { 705 | try 706 | { 707 | //appbar.hide(); 708 | if (anySelected()) 709 | { 710 | inkManager.deleteSelected(); 711 | } 712 | else 713 | { 714 | selectAll(); 715 | inkManager.deleteSelected(); 716 | inkMode(); 717 | } 718 | 719 | renderAllStrokes(); 720 | displayStatus(""); 721 | displayError(""); 722 | } 723 | catch (e) 724 | { 725 | displayError("clear: " + e.toString()); 726 | } 727 | } 728 | 729 | // A generic function for use for any async error function (the second arg to a then() method). 730 | function asyncError(e) 731 | { 732 | displayError("Async error: " + e.toString()); 733 | } 734 | 735 | function refresh() 736 | { 737 | try 738 | { 739 | //appbar.hide(); 740 | renderAllStrokes(); 741 | } 742 | catch (e) 743 | { 744 | displayError("clear " + e.toString()); 745 | } 746 | } 747 | 748 | // A button handler which fetches the ID from the button, which should 749 | // be IW2, IW4, etc. We set the lineWidth of the inking canvas to the number part of this ID, 750 | // then set the system into ink mode (which will cause the ink manager 751 | // to change its defaults for new strokes to match the ink canvas). 752 | // If any ink strokes (not including highlight strokes) are currently selected, 753 | // we also change their width to this value. If any strokes are changed 754 | // we must re-render the entire ink display. 755 | function setInkWidth(evt) 756 | { 757 | try 758 | { 759 | //appbar.hide(); 760 | 761 | var id = evt.srcElement.id; 762 | id = id.substr(2); 763 | inkContext.lineWidth = id; 764 | inkMode(); 765 | 766 | var redraw = false; 767 | inkManager.getStrokes().forEach(function (stroke) 768 | { 769 | if (stroke.selected && !isHighlighting(stroke)) 770 | { 771 | if (shapeStroke(stroke, inkContext.lineWidth)) 772 | { 773 | redraw = true; 774 | } 775 | } 776 | }); 777 | if (redraw) 778 | { 779 | renderAllStrokes(); 780 | } 781 | } 782 | catch (e) 783 | { 784 | displayError("setInkWidth " + e.toString()); 785 | } 786 | } 787 | 788 | // A button handler which fetches the ID from the button, which should 789 | // be HW10, HW20, or HW30. We set the lineWidth of the highlighting canvas to the number part of this ID, 790 | // then set the system into highlight mode (which will cause the ink manager 791 | // to change its defaults for new strokes to match the highlight canvas). 792 | // If any highlight strokes are currently selected, we also change their width 793 | // to this value. If any strokes are changed we must re-render the dirty areas. 794 | function setHighlightWidth(evt) 795 | { 796 | try 797 | { 798 | //appbar.hide(); 799 | 800 | var id = evt.srcElement.id; 801 | id = id.substr(2); 802 | hlContext.lineWidth = id; 803 | highlightMode(); 804 | 805 | var redraw = false; 806 | inkManager.getStrokes().forEach(function (stroke) 807 | { 808 | if (stroke.selected && isHighlighting(stroke)) 809 | { 810 | if (shapeStroke(stroke, hlContext.lineWidth)) 811 | { 812 | redraw = true; 813 | } 814 | } 815 | }); 816 | if (redraw) 817 | { 818 | renderAllStrokes(); 819 | } 820 | } 821 | catch (e) 822 | { 823 | displayError("setInkWidth " + e.toString()); 824 | } 825 | } 826 | 827 | // A button handler which fetches the ID from the button, which should 828 | // be a color name. We set the strokeStyle of the inking canvas to this color, 829 | // then set the system into ink mode (which will cause the ink manager 830 | // to change its defaults for new strokes to match the ink canvas). 831 | // If any ink strokes (not including highlight strokes) are currently selected, 832 | // we also change their color to this value. If any strokes are changed 833 | // we must re-render the dirty areas. 834 | function inkColor(evt) 835 | { 836 | //appbar.hide(); 837 | inkContext.strokeStyle = evt.srcElement.id; 838 | inkMode(); 839 | 840 | var redraw = false; 841 | inkManager.getStrokes().forEach(function (stroke) 842 | { 843 | if (stroke.selected && !isHighlighting(stroke)) 844 | { 845 | if (colorStroke(stroke, inkContext.strokeStyle)) 846 | { 847 | redraw = true; 848 | } 849 | } 850 | }); 851 | if (redraw) 852 | { 853 | renderAllStrokes(); 854 | } 855 | } 856 | 857 | // A button handler which fetches the ID from the button, which should 858 | // be a color name. We set the strokeStyle of the highlight canvas to this color, 859 | // then set the system into highlight mode (which will cause the ink manager 860 | // to change its defaults for new strokes to match the highlight canvas). 861 | // If any highlight strokes are currently selected, we also change their color 862 | // to this value. If any strokes are changed we must re-render the dirty areas. 863 | function highlightColor(evt) 864 | { 865 | //appbar.hide(); 866 | hlContext.strokeStyle = evt.srcElement.id; 867 | highlightMode(); 868 | 869 | var redraw = false; 870 | inkManager.getStrokes().forEach(function (stroke) 871 | { 872 | if (stroke.selected && isHighlighting(stroke)) 873 | { 874 | if (colorStroke(stroke, hlContext.strokeStyle)) 875 | { 876 | redraw = true; 877 | } 878 | } 879 | }); 880 | if (redraw) 881 | { 882 | renderAllStrokes(); 883 | } 884 | } 885 | 886 | // Finds a specific recognizer, and sets the inkManager's default to that recognizer. 887 | // Returns true if successful. 888 | function setRecognizerByName(recname) 889 | { 890 | try 891 | { 892 | // recognizers is a normal JavaScript array 893 | var recognizers = inkManager.getRecognizers(); 894 | for (var i = 0, len = recognizers.length; i < len; i++) 895 | { 896 | if (recname === recognizers[i].name) 897 | { 898 | inkManager.setDefaultRecognizer(recognizers[i]); 899 | return true; 900 | } 901 | } 902 | } 903 | catch (e) 904 | { 905 | displayError("setRecognizerByName: " + e.toString()); 906 | } 907 | return false; 908 | } 909 | 910 | // A button handler which runs the currently-loaded handwriting recognizer over 911 | // the selected ink (not counting highlight strokes). If no ink is selected, then it 912 | // runs over all the ink (again, not counting highlight strokes). 913 | // The recognition results (a string) is displayed in the status window. 914 | // The recognition results are also stored within the ink manager itself, so that 915 | // other commands can find the bounding boxes (or ink strokes) of any specific 916 | // word of ink. 917 | function recognize(evt) 918 | { 919 | //appbar.hide(); 920 | 921 | // The recognizeAsync() method has 3 modes: selected, remaining, and all. 922 | // This particular app cannot use "all" mode because it supports highlighting. 923 | // If the user has highlighted one or more words, and we recognize in "all" mode, 924 | // we will recognize all strokes, including the highlight strokes. This usually 925 | // results in a recognition string containing many asterisks. 926 | // If we find that no strokes are selected, rather than running in "all" mode, we 927 | // select all strokes that are not highlighting strokes, then run in "selected" mode. 928 | // If some strokes were already selected, we just need to unselect any which are highlighting. 929 | 930 | // If we DID originally find that no strokes were selected, we remember that fact, so that 931 | // we can unselect them after the recognition. 932 | var bSelected = false; 933 | if (anySelected()) 934 | { 935 | unselectHighlight(); 936 | } 937 | else 938 | { 939 | selectAllNoHighlight(); 940 | bSelected = true; 941 | } 942 | 943 | // NOTE: check that we have some ink to recognize before calling RecognizerContainer::RecognizeAsync() 944 | if (anySelected()) 945 | { 946 | // recognizeAsync below will fail if another recognition task is in progress 947 | try 948 | { 949 | 950 | // Note that the third mode in recognizeAsync(), "recent", can be very useful in certain situations, 951 | // but we are not using it here. It will recognize all strokes that have been added since the last 952 | // recognition. If we were assuming that all strokes were writing, and we were trying to keep 953 | // recognition caught up with the user's writing at all times (that is, not using a Reco button), 954 | // then "recent" would be the mode we would want. 955 | 956 | // Because recognition is slower, we ask for it as an asynchronous operation. 957 | // The anonymous function (the first arg to the "then" method) will be called 958 | // as a callback when recognition has completed. If an error occurs, the second 959 | // arg will be called. 960 | inkManager.recognizeAsync(Windows.UI.Input.Inking.InkRecognitionTarget.selected).done 961 | ( 962 | function (results) 963 | { 964 | // Doing a recognition does not update the storage of results (the results that are stored inside the ink manager). 965 | // We do that ourselves by calling this method. 966 | inkManager.updateRecognitionResults(results); 967 | 968 | // The arg "results" is an array of result objects representing "words", where "words" means words of ink (not computer memory words). 969 | // IE, if you write "this is a test" that is 4 words, and results will be an array of length 4. 970 | 971 | var alternates = ""; // will accumulate the result words, with spaces between 972 | var c = results.length; 973 | for (var i = 0; i < c; i++) 974 | { 975 | // Method getTextCandidates() returns an array of recognition alternates (different interpretations of the same word of ink). 976 | // This is a standard JavaScript array of standard JavaScript strings. 977 | // For this program we only use the first (top) alternate in our display. 978 | // If we were doing search over this ink, we would want to search all alternates. 979 | var alts = results[i].getTextCandidates(); 980 | alternates = alternates + " " + alts[0]; 981 | 982 | // The specific strokes forming the current word of ink are available to us. 983 | // This feature is not used here, but we could, if we chose, display the ink, 984 | // with the recognition result for each word directly above the specific word of ink, 985 | // by fetching the bounding box of the recognitionResult (via the boundingRect property). 986 | // Or, if we needed to do something to each stroke in the recognized word, we could 987 | // call recognitionResult.getStrokes(), then iterate over the individual strokes. 988 | } 989 | displayStatus(alternates); 990 | }, 991 | function (e) 992 | { 993 | displayError("InkManager::recognizeAsync: " + e.toString()); 994 | } 995 | ); 996 | if (bSelected) 997 | { 998 | // Unselect all strokes (if we originally had no selected strokes). 999 | var pt = { x: 0.0, y: 0.0 }; 1000 | inkManager.selectWithLine(pt, pt); 1001 | } 1002 | } 1003 | catch (e) 1004 | { 1005 | displayError("recognize: " + e.toString()); 1006 | } 1007 | } 1008 | else 1009 | { 1010 | displayStatus("Must first write something."); 1011 | } 1012 | } 1013 | 1014 | // A utility function for findText() below. This takes a target string (typed in by the user) 1015 | // an an array of recognition results objects, and inspects the recognition alternates of each 1016 | // results object. If a match is found among the alternates, then all strokes in that results 1017 | // object are selected. The match is case-insensitive. 1018 | function findWord(target, results) 1019 | { 1020 | target = target.toLowerCase(); 1021 | var cWords = results.length; 1022 | 1023 | var count = 0; 1024 | for (var i = 0; i < cWords; i++) 1025 | { 1026 | var alternates = results[i].getTextCandidates(); 1027 | var cAlts = alternates.length; 1028 | for (var j = 0; j < cAlts; j++) 1029 | { 1030 | if (alternates[j].toLowerCase() === target) 1031 | { 1032 | var strokes = results[i].getStrokes(); 1033 | var cStrokes = strokes.length; 1034 | for (var k = 0; k < cStrokes; k++) 1035 | { 1036 | strokes[k].selected = true; 1037 | } 1038 | count++; 1039 | break; 1040 | } 1041 | } 1042 | } 1043 | return count; 1044 | } 1045 | 1046 | // A handler for the Find button in the Find flyout. We fetch the search string 1047 | // from the form, and the array of recognition results objects from the ink 1048 | // manager. We unselect any current selection, so that when we are done 1049 | // the selections will reflect the search results. We split the search string into 1050 | // individual words, since our recognition results objects each represent individual 1051 | // words. The actual matching is done by findWord(), defined above. 1052 | 1053 | // Note that multiple instances of a target can be found; if the target is "this" and 1054 | // the ink contains "this is this is that", 2 instances of "this" will be found and all 1055 | // strokes in both words will be selected. 1056 | 1057 | // Note that findWord() above searches all alternates. This means you might write 1058 | // "this", have it mis-recognized as "these", but the search feature MAY find it, if 1059 | // "this" appears in any of the other 4 recognition alternates for this ink. 1060 | function find(evt) 1061 | { 1062 | try 1063 | { 1064 | //appbar.hide(); 1065 | 1066 | var str = findText.value; 1067 | var results = inkManager.getRecognitionResults(); 1068 | 1069 | // This will unselect any current selection. 1070 | var pt = {x:0.0, y:0.0}; 1071 | inkManager.selectWithLine(pt, pt); 1072 | 1073 | var count = 0; 1074 | var words = str.split(" "); 1075 | for (var i = 0; i < words.length; i++) 1076 | { 1077 | count += findWord(words[i], results); 1078 | } 1079 | 1080 | if (0 < count) 1081 | { 1082 | displayStatus("Found " + /*@static_cast(String)*/count + " words"); 1083 | renderAllStrokes(); 1084 | } 1085 | else 1086 | { 1087 | displayStatus("Did not find " + str); 1088 | } 1089 | return false; 1090 | } 1091 | catch (e) 1092 | { 1093 | displayError("find: " + e.toString()); 1094 | } 1095 | return false; 1096 | } 1097 | 1098 | // A button click handler for recognition results buttons in the "reco" Flyout. 1099 | // The flyout shows the top 5 recognition results for a specific word, and 1100 | // is invoked by tapping (with finger) on a word (after recognition has been run). 1101 | // We fetch the recognition result (the innerHTML of the button, a string) and 1102 | // copy it to the clipboard. 1103 | function recoClipboard(evt) 1104 | { 1105 | try 1106 | { 1107 | recoFlyout.winControl.hide(); 1108 | var alt = evt.srcElement.innerHTML; 1109 | 1110 | var dataPackage = new Windows.ApplicationModel.DataTransfer.DataPackage(); 1111 | dataPackage.setText(alt); 1112 | Windows.ApplicationModel.DataTransfer.Clipboard.setContent(dataPackage); 1113 | displayStatus("To clipboard: " + alt); 1114 | } 1115 | catch (e) 1116 | { 1117 | displayError("recoClipboard: " + e.toString()); 1118 | } 1119 | } 1120 | 1121 | // Brings up the "reco" Flyout, after first changing the values of the 5 buttons to be 1122 | // the top 5 recognition alternates of a single word. 1123 | function touchWord(touchedResults) 1124 | { 1125 | try 1126 | { 1127 | // The Windows.UI.Input.Inking.InkManager interface normally returns 5 alternates. 1128 | // We check just to be sure we are not given more alternates than the count of buttons. 1129 | var cAlts = touchedResults.alternates.size; 1130 | if (cAlts === 0) 1131 | { 1132 | return; 1133 | } 1134 | var cButs = clipButtons.length; 1135 | if (cButs < cAlts) 1136 | { 1137 | cAlts = cButs; 1138 | } 1139 | var i; 1140 | for (i = 0; i < cAlts; i++) 1141 | { 1142 | clipButtons[i].label = touchedResults.alternates[i]; 1143 | } 1144 | for (; i < cButs; i++) 1145 | { 1146 | clipButtons[i].label = ""; 1147 | } 1148 | 1149 | // Display the reco results menu just to the left of the left-top corner of the bounding rect of the ink. 1150 | var rect = touchedResults.rect; 1151 | wordDiv.style.left = /*@static_cast(String)*/rect.x + "px"; 1152 | wordDiv.style.top = /*@static_cast(String)*/rect.y + "px"; 1153 | wordDiv.style.width = /*@static_cast(String)*/rect.width + "px"; 1154 | wordDiv.style.height = /*@static_cast(String)*/rect.height + "px"; 1155 | recoFlyout.winControl.show(wordDiv, "left"); 1156 | } 1157 | catch (e) 1158 | { 1159 | displayError("touchWord: " + e.toString()); 1160 | } 1161 | } 1162 | 1163 | // A button handler which copies the selected strokes (or all the strokes if none are selected) 1164 | // into the clipboard. The strokes can be pasted into any application that handles any of the 1165 | // ink clipboard formats, such as Windows Journal. 1166 | function copySelected(evt) 1167 | { 1168 | try 1169 | { 1170 | //appbar.hide(); 1171 | if (anySelected()) 1172 | { 1173 | displayStatus("Copying selected strokes ..."); 1174 | inkManager.copySelectedToClipboard(); 1175 | displayStatus("Copy Selected"); 1176 | } 1177 | else 1178 | { 1179 | displayStatus("Copying all strokes ..."); 1180 | selectAll(); 1181 | inkManager.copySelectedToClipboard(); 1182 | // Unselect all strokes. 1183 | var pt = {x:0.0, y:0.0}; 1184 | inkManager.selectWithLine(pt, pt); 1185 | displayStatus("Copy All"); 1186 | } 1187 | } 1188 | catch (e) 1189 | { 1190 | displayError("copySelected: " + e.toString()); 1191 | } 1192 | } 1193 | 1194 | // A button handler which copies any available strokes in the clipboard into this app. 1195 | function paste(evt) 1196 | { 1197 | //appbar.hide(); 1198 | 1199 | displayStatus("Pasting ..."); 1200 | var insertionPoint = {x: 100, y: 60}; 1201 | var canPaste = inkManager.canPasteFromClipboard(); 1202 | if (canPaste) 1203 | { 1204 | inkManager.pasteFromClipboard(insertionPoint); 1205 | displayStatus("Pasted"); 1206 | renderAllStrokes(); 1207 | } 1208 | else 1209 | { 1210 | displayStatus("Cannot paste"); 1211 | } 1212 | } 1213 | 1214 | // A keypress handler which closes the program. 1215 | // A normal program should not have this, but it is very 1216 | // convenient for testing. 1217 | function closeProgram(evt) 1218 | { 1219 | displayStatus("Closing App ..."); 1220 | window.close(); 1221 | } 1222 | 1223 | // prevent two concurrent loadAsync() operations 1224 | var asyncFlag = false; 1225 | 1226 | // Reads a gif file which contains strokes as metadata. 1227 | function readInk(storageFile) 1228 | { 1229 | if (storageFile) 1230 | { 1231 | // closure variable, visible to all promises in the following chain 1232 | var loadStream = null; 1233 | storageFile.openAsync(Windows.Storage.FileAccessMode.read).then( 1234 | function (stream) 1235 | { 1236 | // about to call loadAsync() 1237 | // prevent future calls to this API until we are done with the first call 1238 | asyncFlag = true; 1239 | loadStream = stream; 1240 | return inkManager.loadAsync(loadStream); // since we return the promise, it will be executed before the following .done 1241 | } 1242 | ).done( 1243 | function () 1244 | { 1245 | var strokes = inkManager.getStrokes(); 1246 | var c = strokes.length; 1247 | if (c === 0) 1248 | { 1249 | displayStatus("File does not contain any ink strokes."); 1250 | } 1251 | else 1252 | { 1253 | displayStatus("Loaded " + c + " strokes."); 1254 | renderAllStrokes(); 1255 | } 1256 | 1257 | // reset asyncFlag, can call loadAsync() once again 1258 | asyncFlag = false; 1259 | 1260 | // input stream is IClosable interface and requires explicit close 1261 | loadStream.close(); 1262 | }, 1263 | function (e) 1264 | { 1265 | displayError("Load failed. Make sure you tried to open a file that can be read by the InkManager."); 1266 | 1267 | // we still want to reset asyncFlag if an error occurs 1268 | asyncFlag = false; 1269 | 1270 | // if the error occurred after the stream was opened, close the stream 1271 | if (loadStream) 1272 | { 1273 | loadStream.close(); 1274 | } 1275 | } 1276 | ); 1277 | } 1278 | } 1279 | 1280 | // A button handler which fetches the file name via the file picker, then calls readInk() above. 1281 | function load(evt) 1282 | { 1283 | //appbar.hide(); 1284 | if (asyncFlag) 1285 | { 1286 | return; 1287 | } 1288 | 1289 | // Open the WinRT file picker, set the input folder, and set the input extension. 1290 | var picker = new Windows.Storage.Pickers.FileOpenPicker(); 1291 | picker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.picturesLibrary; 1292 | picker.fileTypeFilter.replaceAll([".gif"]); 1293 | picker.pickSingleFileAsync().done(readInk, asyncError); 1294 | } 1295 | 1296 | var encodeToBase64String = function (buffer) { 1297 | return Windows.Security.Cryptography.CryptographicBuffer.encodeToBase64String(buffer); 1298 | }; 1299 | 1300 | function done(){ 1301 | if (inkManager.getStrokes().size > 0) 1302 | { 1303 | if(navigator.sketch != null && navigator.sketch != undefined) 1304 | if(navigator.sketch.destinationType == navigator.sketch.DestinationType.DATA_URL) 1305 | saveToStream(); 1306 | else if(navigator.sketch.destinationType == navigator.sketch.DestinationType.FILE_URI) 1307 | saveToFile(); 1308 | } 1309 | else 1310 | { 1311 | statusMessage.innerText = "Nothing to save yet."; 1312 | } 1313 | } 1314 | 1315 | var filetype = getFileType(); 1316 | 1317 | function getFileType(){ 1318 | var encodingType = 'PNG'; 1319 | if(navigator.sketch != null && navigator.sketch != undefined) 1320 | { 1321 | if(navigator.sketch.encodingType == navigator.sketch.EncodingType.JPEG) 1322 | encodingType = 'JPEG'; 1323 | else if(navigator.sketch.encodingType == navigator.sketch.EncodingType.PNG) 1324 | encodingType = 'PNG'; 1325 | } 1326 | return encodingType; 1327 | } 1328 | 1329 | function saveToStream(){ 1330 | var dataURL = inkCanvas.toDataURL(navigator.sketch.DataURLType[navigator.sketch.encodingType], 1.0); 1331 | callbackDone(dataURL); 1332 | // var fileName = 'cordova-plugin-sketch-temporary.'+filetype; 1333 | // var repExt = Windows.Storage.CreationCollisionOption.ReplaceExisting; 1334 | // var folder = Windows.Storage.ApplicationData.current.temporaryFolder; 1335 | // folder.createFileAsync(fileName, repExt) 1336 | // .then( 1337 | // function(tempFile) { 1338 | // writeInk(tempFile) 1339 | // }) 1340 | // .done( 1341 | // function(){ 1342 | // folder.getFileAsync(fileName).done( 1343 | // function(tempFile){ 1344 | // Windows.Storage.FileIO.readBufferAsync(tempFile).done( 1345 | // function(buffer) { 1346 | // var strBase64 = encodeToBase64String(buffer); 1347 | // tempFile.deleteAsync().done( 1348 | // function() { 1349 | // callbackDone(strBase64); 1350 | // }, 1351 | // function (e) 1352 | // { 1353 | // displayError("Done " + e.toString()); 1354 | // } 1355 | // ); 1356 | // }, 1357 | // function(e) { 1358 | // displayError("Done " + e.toString()); 1359 | // } 1360 | // ); 1361 | // }, 1362 | // function(e) { 1363 | // displayError("Done " + e.toString()); 1364 | // } 1365 | // ); 1366 | // }, 1367 | // function(e) { 1368 | // displayError("Done " + e.toString()); 1369 | // } 1370 | // ); 1371 | } 1372 | 1373 | function saveToFile(){ 1374 | var picker = new Windows.Storage.Pickers.FileSavePicker(); 1375 | picker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.picturesLibrary; 1376 | picker.fileTypeChoices.insert(filetype + " file", ["." + filetype + ""]); 1377 | picker.defaultFileExtension = "." + filetype; 1378 | picker.pickSaveFileAsync().done(writeInkToFile, asyncError); 1379 | } 1380 | 1381 | function writeInkToFile(storageFile) 1382 | { 1383 | if (storageFile) 1384 | { 1385 | // closure variable, visible to all promises in the following chain 1386 | var saveStream = null; 1387 | storageFile.openAsync(Windows.Storage.FileAccessMode.readWrite).then( 1388 | function (stream) 1389 | { 1390 | saveStream = stream; 1391 | return inkManager.saveAsync(saveStream).done( 1392 | function() { 1393 | callbackDone(storageFile.path); 1394 | }, 1395 | function (e) 1396 | { 1397 | displayError("Done " + e.toString()); 1398 | } 1399 | ); // since we return the promise, it will be executed before the following .then 1400 | } 1401 | ).done( 1402 | function (result) 1403 | { 1404 | // print the size of the stream on the screen 1405 | displayStatus("File saved!"); 1406 | 1407 | // output stream is IClosable interface and requires explicit close 1408 | saveStream.close(); 1409 | }, 1410 | function (e) 1411 | { 1412 | displayError("Save " + e.toString()); 1413 | 1414 | // if the error occurred after the stream was opened, close the stream 1415 | if (saveStream) 1416 | { 1417 | saveStream.close(); 1418 | } 1419 | } 1420 | ); 1421 | } 1422 | } 1423 | 1424 | function callbackDone(stream){ 1425 | if(navigator.sketch != null && navigator.sketch != undefined) 1426 | navigator.sketch.done(stream); 1427 | } 1428 | 1429 | function cancel(){ 1430 | if(navigator.sketch != null && navigator.sketch != undefined) 1431 | navigator.sketch.cancel(); 1432 | } 1433 | 1434 | function writeInk(storageFile) 1435 | { 1436 | if (storageFile) 1437 | { 1438 | // closure variable, visible to all promises in the following chain 1439 | var saveStream = null; 1440 | storageFile.openAsync(Windows.Storage.FileAccessMode.readWrite).then( 1441 | function (stream) 1442 | { 1443 | saveStream = stream; 1444 | return inkManager.saveAsync(saveStream); // since we return the promise, it will be executed before the following .then 1445 | } 1446 | ).done( 1447 | function (result) 1448 | { 1449 | // print the size of the stream on the screen 1450 | displayStatus("File saved!"); 1451 | 1452 | // output stream is IClosable interface and requires explicit close 1453 | saveStream.close(); 1454 | }, 1455 | function (e) 1456 | { 1457 | displayError("Save " + e.toString()); 1458 | 1459 | // if the error occurred after the stream was opened, close the stream 1460 | if (saveStream) 1461 | { 1462 | saveStream.close(); 1463 | } 1464 | } 1465 | ); 1466 | } 1467 | } 1468 | 1469 | // Shows the create file dialog box. Submitting on that form will invoke saveFile() above. 1470 | function save(evt) 1471 | { 1472 | //appbar.hide(); 1473 | 1474 | // NOTE: make sure that the inkManager has some strokes to save before calling inkManager.saveAsync 1475 | if (inkManager.getStrokes().size > 0) 1476 | { 1477 | var picker = new Windows.Storage.Pickers.FileSavePicker(); 1478 | picker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.picturesLibrary; 1479 | picker.fileTypeChoices.insert("PNG file", [".PNG"]); 1480 | picker.fileTypeChoices.insert("JPEG file", [".JPEG"]); 1481 | picker.defaultFileExtension = ".PNG"; 1482 | picker.pickSaveFileAsync().done(writeInk, asyncError); 1483 | } 1484 | else 1485 | { 1486 | statusMessage.innerText = "The InkManager doesn't contain any strokes to save."; 1487 | } 1488 | } 1489 | 1490 | // A keypress handler that only handles a few keys. This is registered on the entire body. 1491 | // Escape will: 1492 | // 1. If any dialog boxes are showing, hide them and do nothing else. 1493 | // 2. Otherwise, if any strokes are selected, unselect them and do nothing else. 1494 | // 3. Otherwise, change to ink mode. 1495 | // This sequence allows us to "unpeel the onion" (it is very fast to hit escape 3 times if needed). 1496 | 1497 | // Certain control keys invoke handlers that are otherwise invoked via buttons: 1498 | // ^C Copy 1499 | // ^V Paste 1500 | // ^F Find 1501 | // ^O Load 1502 | // ^S Save 1503 | // ^R Recognize 1504 | // ^Q Quit (shuts down the sample app) 1505 | 1506 | // Note that most of these keys have standardized normal uses, and there is system code to handle that 1507 | // without our code doing anything. That code sometimes interferes with our program. All the functions 1508 | // we call from here call evt.preventDefault(), which should stop the default processing, but sometimes we still 1509 | // cannot get this code to execute. 1510 | function keypress(evt) 1511 | { 1512 | if (evt.keyCode === 27) // escape 1513 | { 1514 | evt.preventDefault(); 1515 | if (!recoFlyout.winControl.hidden) 1516 | { 1517 | recoFlyout.winControl.hide(); 1518 | renderAllStrokes(); 1519 | } 1520 | else if (anySelected()) 1521 | { 1522 | // Unselect all strokes. 1523 | var pt = {x:0.0, y:0.0}; 1524 | inkManager.selectWithLine(pt, pt); 1525 | renderAllStrokes(); 1526 | } 1527 | else 1528 | { 1529 | inkMode(); 1530 | } 1531 | } 1532 | else if (evt.keyCode === 3) // control c 1533 | { 1534 | copySelected(evt); 1535 | } 1536 | else if (evt.keyCode === 22) // control v 1537 | { 1538 | paste(evt); 1539 | } 1540 | else if (evt.keyCode === 15) // control o 1541 | { 1542 | load(evt); 1543 | } 1544 | else if (evt.keyCode === 19) // control s 1545 | { 1546 | save(evt); 1547 | } 1548 | else if (evt.keyCode === 18) // control r 1549 | { 1550 | recognize(evt); 1551 | } 1552 | else if (evt.keyCode === 17) // control q 1553 | { 1554 | closeProgram(evt); 1555 | } 1556 | } 1557 | 1558 | function inkInitialize() 1559 | { 1560 | // Utility to fetch elements by ID. 1561 | function id(elementId) 1562 | { 1563 | return document.getElementById(elementId); 1564 | } 1565 | 1566 | WinJS.UI.processAll().then( 1567 | function () 1568 | { 1569 | app = WinJS.Application; 1570 | appbar = id("bottomAppBar").winControl; 1571 | 1572 | findFlyout = id("FindFlyout"); 1573 | inkColorsFlyout = id("InkColorFlyout"); 1574 | inkWidthsFlyout = id("InkWidthFlyout"); 1575 | hlColorsFlyout = id("HighlightColorFlyout"); 1576 | hlWidthsFlyout = id("HighlightWidthFlyout"); 1577 | moreFlyout = id("MoreFlyout"); 1578 | 1579 | if(findFlyout){ 1580 | findText = id("FindString"); 1581 | findFlyout.addEventListener("aftershow", function (evt) { findText.focus(); }, false); 1582 | id("FindButton").addEventListener("click", find, false); 1583 | } 1584 | 1585 | hlCanvas = id("HighlightCanvas"); 1586 | hlCanvas.setAttribute("width", hlCanvas.offsetWidth); 1587 | hlCanvas.setAttribute("height", hlCanvas.offsetHeight); 1588 | hlContext = hlCanvas.getContext("2d"); 1589 | hlContext.lineWidth = 10; 1590 | hlContext.strokeStyle = "Yellow"; 1591 | hlContext.lineCap = "round"; 1592 | hlContext.lineJoin = "round"; 1593 | 1594 | inkCanvas = id("InkCanvas"); 1595 | inkCanvas.setAttribute("width", inkCanvas.offsetWidth); 1596 | inkCanvas.setAttribute("height", inkCanvas.offsetHeight); 1597 | inkContext = inkCanvas.getContext("2d"); 1598 | inkContext.lineWidth = 2; 1599 | inkContext.strokeStyle = "Black"; 1600 | inkContext.lineCap = "round"; 1601 | inkContext.lineJoin = "round"; 1602 | 1603 | selCanvas = id("SelectCanvas"); 1604 | selCanvas.gestureObject = new MSGesture(); 1605 | selCanvas.gestureObject.target = selCanvas; 1606 | selCanvas.setAttribute("width", selCanvas.offsetWidth); 1607 | selCanvas.setAttribute("height", selCanvas.offsetHeight); 1608 | selContext = selCanvas.getContext("2d"); 1609 | selContext.lineWidth = 1; 1610 | selContext.strokeStyle = "Gold"; 1611 | selContext.lineCap = "round"; 1612 | selContext.lineJoin = "round"; 1613 | 1614 | selBox = id("SelectionBox"); 1615 | selBox.addEventListener("pointerdown", handleSelectionBoxPointerDown, false); 1616 | selBox.addEventListener("MSGestureChange", handleSelectionBoxGestureChange, false); 1617 | selBox.gestureObject = new MSGesture(); 1618 | selBox.gestureObject.target = selBox; 1619 | selBox.style.left = "0px"; 1620 | selBox.style.top = "0px"; 1621 | selBox.style.width = "0px"; 1622 | selBox.style.height = "0px"; 1623 | 1624 | // Note that we must set the event listeners on the top-most canvas. 1625 | 1626 | selCanvas.addEventListener("pointerdown", handlePointerDown, false); 1627 | selCanvas.addEventListener("pointerup", handlePointerUp, false); 1628 | selCanvas.addEventListener("pointermove", handlePointerMove, false); 1629 | selCanvas.addEventListener("pointerout", handlePointerOut, false); 1630 | selCanvas.addEventListener("MSGestureTap", handleTap, false); 1631 | 1632 | window.addEventListener("resize", setView); 1633 | 1634 | setView(); 1635 | 1636 | var image = new Image(); 1637 | image.onload = function () { selContext.strokeStyle = selPattern = selContext.createPattern(image, "repeat"); }; 1638 | image.src = "/www/cordova-plugin-sketch/images/select.png"; 1639 | 1640 | recoFlyout = id("RecoFlyout"); 1641 | if(recoFlyout){ 1642 | clipButtons = new Array(); 1643 | for (var i = 0; i < 5; i++) 1644 | { 1645 | var ID = "Reco" + i; 1646 | clipButtons[i] = recoFlyout.winControl.getCommandById(ID); 1647 | } 1648 | } 1649 | wordDiv = id("Word"); 1650 | 1651 | //document.body.addEventListener("keypress", keypress, false); 1652 | 1653 | if (!setRecognizerByName("Microsoft English (US) Handwriting Recognizer")) 1654 | { 1655 | displayStatus("Failed to find English (US) recognizer"); 1656 | } 1657 | else 1658 | { 1659 | //displayStatus("Verba volant, Scripta manet"); 1660 | } 1661 | 1662 | inkMode(); 1663 | appbar.show(); 1664 | renderPaper(); 1665 | } 1666 | ).done( 1667 | function () 1668 | { 1669 | }, 1670 | function (e) 1671 | { 1672 | displayError("inkInitialize " + e.toString()); 1673 | } 1674 | ); 1675 | } 1676 | 1677 | // Tag the event handlers of the AppBar so that they can be used in a declarative context. 1678 | // For security reasons WinJS.UI.processAll and WinJS.Binding.processAll (and related) functions allow only 1679 | // functions that are marked as being usable declaratively to be invoked through declarative processing. 1680 | WinJS.UI.eventHandler(selectMode); 1681 | WinJS.UI.eventHandler(eraseMode); 1682 | WinJS.UI.eventHandler(clear); 1683 | WinJS.UI.eventHandler(refresh); 1684 | WinJS.UI.eventHandler(setInkWidth); 1685 | WinJS.UI.eventHandler(setHighlightWidth); 1686 | WinJS.UI.eventHandler(inkColor); 1687 | WinJS.UI.eventHandler(highlightColor); 1688 | WinJS.UI.eventHandler(recognize); 1689 | WinJS.UI.eventHandler(recoClipboard); 1690 | WinJS.UI.eventHandler(copySelected); 1691 | WinJS.UI.eventHandler(paste); 1692 | WinJS.UI.eventHandler(load); 1693 | WinJS.UI.eventHandler(save); 1694 | WinJS.UI.eventHandler(done); 1695 | WinJS.UI.eventHandler(cancel); 1696 | 1697 | 1698 | inkInitialize(); 1699 | -------------------------------------------------------------------------------- /www/sketch.js: -------------------------------------------------------------------------------- 1 | var Sketch = function () { 2 | var sketch = {}; 3 | 4 | var getSketch = function (successCallback, errorCallback, options) { 5 | var argsCheck = require('cordova/argscheck'); 6 | var opts = options || {}; 7 | 8 | argsCheck.checkArgs('fFO', 'Sketch.getSketch', arguments); 9 | if (typeof opts.destinationType === 'undefined' || opts.destinationType === null) { 10 | opts.destinationType = DestinationType.DATA_URL; 11 | } 12 | 13 | if (typeof opts.encodingType === 'undefined' || opts.encodingType === null) { 14 | opts.encodingType = EncodingType.PNG; 15 | } 16 | 17 | if (typeof opts.inputType === 'undefined' || opts.inputType === null) { 18 | opts.inputType = InputType.NO_INPUT; 19 | } 20 | 21 | cordova.exec(successCallback, errorCallback, "SketchPlugin", "getSketch", [opts]); 22 | }; 23 | 24 | var InputType = { 25 | NO_INPUT: 0, // no input as background image, use as signature plugin 26 | DATA_URL: 1, // base64 encoded string stream 27 | FILE_URI: 2 // file uri (content://media/external/images/media/2 for Android) 28 | }; 29 | 30 | var DestinationType = { 31 | DATA_URL: 0, // Return base64 encoded string 32 | FILE_URI: 1 // Return file uri (content://media/external/images/media/2 for Android) 33 | }; 34 | 35 | var EncodingType = { 36 | JPEG: 0, // Return JPEG encoded image 37 | PNG: 1 // Return PNG encoded image 38 | }; 39 | 40 | sketch.getSketch = getSketch; 41 | sketch.InputType = InputType; 42 | sketch.DestinationType = DestinationType; 43 | sketch.EncodingType = EncodingType; 44 | 45 | return sketch; 46 | }; 47 | 48 | module.exports = Sketch(); 49 | --------------------------------------------------------------------------------