/** * The $1 Unistroke Recognizer (JavaScript version) * * Jacob O. Wobbrock, Ph.D. * The Information School * University of Washington * Seattle, WA 98195-2840 * wobbrock@uw.edu * * Andrew D. Wilson, Ph.D. * Microsoft Research * One Microsoft Way * Redmond, WA 98052 * awilson@microsoft.com * * Yang Li, Ph.D. * Department of Computer Science and Engineering * University of Washington * Seattle, WA 98195-2840 * yangli@cs.washington.edu * * The academic publication for the $1 recognizer, and what should be * used to cite it, is: * * Wobbrock, J.O., Wilson, A.D. and Li, Y. (2007). Gestures without * libraries, toolkits or training: A $1 recognizer for user interface * prototypes. Proceedings of the ACM Symposium on User Interface * Software and Technology (UIST '07). Newport, Rhode Island (October * 7-10, 2007). New York: ACM Press, pp. 159-168. * https://dl.acm.org/citation.cfm?id=1294238 * * The Protractor enhancement was separately published by Yang Li and programmed * here by Jacob O. Wobbrock: * * Li, Y. (2010). Protractor: A fast and accurate gesture * recognizer. Proceedings of the ACM Conference on Human * Factors in Computing Systems (CHI '10). Atlanta, Georgia * (April 10-15, 2010). New York: ACM Press, pp. 2169-2172. * https://dl.acm.org/citation.cfm?id=1753654 * * This software is distributed under the "New BSD License" agreement: * * Copyright (C) 2007-2012, Jacob O. Wobbrock, Andrew D. Wilson and Yang Li. * All rights reserved. Last updated July 14, 2018. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the names of the University of Washington nor Microsoft, * nor the names of its contributors may be used to endorse or promote * products derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Jacob O. Wobbrock OR Andrew D. Wilson * OR Yang Li BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ /* TODO for Bangle.js * Move 'points' to an array of int8 * */ #include #include #include #include #include "jsinteractive.h" #define NUMPOINTS 32 #define SQUARESIZE 176 #define PI 3.141592f #define MAX(a,b) ((a) > (b) ? (a) : (b)) #define MIN(a,b) ((a) < (b) ? (a) : (b)) // DollarRecognizer constants // const float Diagonal = sqrtf(SQUARESIZE * SQUARESIZE + SQUARESIZE * SQUARESIZE); const float AngleRange = 45.0f * PI / 180.0f; const float AnglePrecision = 2.0f * PI / 180.0f; const float Phi = 0.5f * (-1.0f + sqrtf(5.0f)); // Golden Ratio // // Point class // typedef struct { float X,Y; } Point; typedef struct { float X,Y,width,height; } Rectangle; typedef struct { Point points[NUMPOINTS]; } Unistroke; void Resample(Point *dst, int n, Point *points, int pointsLen); float IndicativeAngle(Point *points, int pointsLen); void RotateBy(Point *dst, Point *points, int pointsLen, float radians); void ScaleTo(Point *dst, Point *points, int pointsLen, float size); void TranslateTo(Point *dst, Point *points, int pointsLen, Point pt); void Vectorize(float *vector /* pointsLen*2 */, Point *points, int pointsLen); float OptimalCosineDistance(float *v1, float *v2, int vLen); float DistanceAtBestAngle(Point *points, int pointsLen, Point *T, float a, float b, float threshold); float DistanceAtAngle(Point *points, int pointsLen, Point *T, float radians); Point Centroid(Point *points, int pointsLen); Rectangle BoundingBox(Point *points, int pointsLen); float PathDistance(Point *pts1, Point *pts2, int pointsLen); float PathLength(Point *points, int pointsLen); float Distance(Point p1, Point p2); float Deg2Rad(float d); /*void dumpPts(const char *n, Point *points, int pointCount) { jsiConsolePrintf("%s\n",n); for (int i=0;i PI/4) radians -= PI/2; RotateBy(t.points, t.points, NUMPOINTS, -radians); ScaleTo(t.points, t.points, NUMPOINTS, SQUARESIZE); Point Origin = {0,0}; TranslateTo(t.points, t.points, NUMPOINTS, Origin); //t.Vector = Vectorize(this.Points); // for Protractor return t; } void uint8ToPoints(Point *points, const uint8_t *xy, int xyCount) { for (int i=0;i= I) { float qx = points[i-1].X + ((I - D) / d) * (points[i].X - points[i-1].X); float qy = points[i-1].Y + ((I - D) / d) * (points[i].Y - points[i-1].Y); Point q = {qx, qy}; dst[dstLen++] = q; // append new point 'q' points[i-1]=q;i--; // insert 'q' at position i in points s.t. 'q' will be the next i D = 0.0; } else D += d; } if (dstLen == n - 1) {// sometimes we fall a rounding-error short of adding the last point, so add it if so Point q = {points[pointsLen- 1].X, points[pointsLen - 1].Y }; dst[dstLen++] = q; } } float IndicativeAngle(Point *points, int pointsLen) { Point c = Centroid(points, pointsLen); return (float)atan2(c.Y - points[0].Y, c.X - points[0].X); } void RotateBy(Point *dst, Point *points, int pointsLen, float radians) // rotates points around centroid { Point c = Centroid(points, pointsLen); float fcos = (float)cos(radians); float fsin = (float)sin(radians); for (int i = 0; i < pointsLen; i++) { dst[i].X = (points[i].X - c.X) * fcos - (points[i].Y - c.Y) * fsin + c.X; dst[i].Y = (points[i].X - c.X) * fsin + (points[i].Y - c.Y) * fcos + c.Y; } } void ScaleTo(Point *dst, Point *points, int pointsLen, float size) // non-uniform scale; assumes 2D gestures (i.e., no lines) { Rectangle B = BoundingBox(points, pointsLen); // GW: stop us rescaling so the aspect ratio it toally wrong // eg. a line if (B.width threshold) { if (f1 < f2) { b = x2; x2 = x1; f2 = f1; x1 = Phi * a + (1.0f - Phi) * b; f1 = DistanceAtAngle(points, pointsLen, T, x1); } else { a = x1; x1 = x2; f1 = f2; x2 = (1.0f - Phi) * a + Phi * b; f2 = DistanceAtAngle(points, pointsLen, T, x2); } } return MIN(f1, f2); } float DistanceAtAngle(Point *points, int pointsLen, Point *T, float radians) { Point *newpoints = (Point*)alloca(sizeof(Point)*pointsLen); RotateBy(newpoints, points, pointsLen, radians); return PathDistance(newpoints, T, pointsLen); } Point Centroid(Point *points, int pointsLen) { Point p; p.X = 0.0; p.Y = 0.0; for (int i = 0; i < pointsLen; i++) { p.X += points[i].X; p.Y += points[i].Y; } p.X /= pointsLen; p.Y /= pointsLen; return p; } Rectangle BoundingBox(Point *points, int pointsLen) { float minX = FLT_MAX, maxX = -FLT_MAX, minY = FLT_MAX, maxY = -FLT_MAX; for (int i = 0; i < pointsLen; i++) { minX = MIN(minX, points[i].X); minY = MIN(minY, points[i].Y); maxX = MAX(maxX, points[i].X); maxY = MAX(maxY, points[i].Y); } Rectangle r = { minX, minY, maxX - minX, maxY - minY }; return r; } float PathDistance(Point *pts1, Point *pts2, int pointsLen) { float d = 0.0; for (int i = 0; i < pointsLen; i++) // assumes pts1.length == pts2.length d += Distance(pts1[i], pts2[i]); return d / pointsLen; } float PathLength(Point *points, int pointsLen) { float d = 0.0f; for (int i = 1; i < pointsLen; i++) d += Distance(points[i - 1], points[i]); return d; } float Distance(Point p1, Point p2) { float dx = p2.X - p1.X; float dy = p2.Y - p1.Y; return sqrtf((dx*dx) + (dy*dy)); } float Deg2Rad(float d) { return d * PI / 180.0f; } // ===================================================================================== Point touchHistory[NUMPOINTS]; uint8_t touchHistoryLen; // how many points in touchHistory Point touchSum; // sum of points so far bool touchLastPressed; // Is a finger on the screen? If not, next press we should start from scratch uint8_t touchCnt; // how many touches are in touchSum uint8_t touchDivisor; // how many touch points do we sum to get the next point? int touchDistance; // distance dragged /// initialise stroke detection void unistroke_init() { touchHistoryLen = 0; touchDivisor = 1; touchLastPressed = false; } #ifdef LCD_WIDTH /// Called when a touch event occurs, returns 'true' if an event should be created (by calling unistroke_getEventVar) bool unistroke_touch(int x, int y, int dx, int dy, int pts) { if (!pts) { // touch released touchLastPressed = false; return touchDistance > LCD_WIDTH/2; // fire off an event IF the distance is large amount } // now finger is definitely pressed if (!touchLastPressed) { // reset everything if this is the first time touchHistoryLen = 0; touchDivisor = 1; touchSum.X = 0; touchSum.Y = 0; touchCnt = 0; touchDistance = 0; touchLastPressed = true; } // append to touch distance touchDistance += int_sqrt32((dx*dx)+(dy*dy)); // store the sum of touch points touchSum.X += x; touchSum.Y += y; touchCnt++; // now add the point if (touchCnt >= touchDivisor) { if (touchHistoryLen==NUMPOINTS) { // if too many points, halve everything int j = 0; for (int i=0;ipoints, NUMPOINTS, uni->points, -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $1) } jsvUnLock(strokeVar); return d; } /// Given an object containing values created with unistroke_convert, compare against a unistroke JsVar *unistroke_recognise(JsVar *strokes, Unistroke *candidate) { JsVar *u = 0; float b = FLT_MAX; JsvObjectIterator it; jsvObjectIteratorNew(&it, strokes); while (jsvObjectIteratorHasValue(&it)) { // for each unistroke template JsVar *strokeVar = jsvObjectIteratorGetValue(&it); float d = unistroke_recognise_one(strokeVar, candidate); if (d < b) { b = d; // best (least) distance jsvUnLock(u); u = jsvObjectIteratorGetKey(&it); // unistroke index } jsvUnLock(strokeVar); jsvObjectIteratorNext(&it); } // CERTAINTY = useProtractor ? (1.0 - b) : (1.0 - b / (0.5 * Diagonal)) return u ? jsvAsStringAndUnLock(u) : 0; } /// Given an object containing values created with unistroke_convert, compare against an array containing XY values JsVar *unistroke_recognise_xy(JsVar *strokes, JsVar *xy) { uint8_t points8[NUMPOINTS*2]; unsigned int bytes = jsvIterateCallbackToBytes(xy, points8, sizeof(points8)); int pointCount = bytes/2; Unistroke uni = newUnistroke8(points8, pointCount); return unistroke_recognise(strokes, &uni); } JsVar *unistroke_getEventVar() { if (!touchHistoryLen) return 0; JsVar *o = jsvNewObject(); if (!o) return 0; JsVar *a = jsvNewTypedArray(ARRAYBUFFERVIEW_UINT8, touchHistoryLen*2); if (a) { JsVar *s = jsvGetArrayBufferBackingString(a,NULL); size_t n=0; for (int i=0;i