├── verification.jpeg ├── helper.js ├── README.md ├── Landmark.js ├── Functional.js ├── List.js ├── Grid.js ├── index.html ├── VectorField.js ├── Vector2D.js └── index.js /verification.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilvanCodes/honey_bee/master/verification.jpeg -------------------------------------------------------------------------------- /helper.js: -------------------------------------------------------------------------------- 1 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); 2 | 3 | const okay = void 0; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # honey bee 2 | 3 | ## Starting the program 4 | 5 | - open the file `index.html` in the Browser 6 | 7 | ## Code 8 | 9 | All application logic is written in `index.js`. Mathimatical formulas are abstracted in `Vector2D.js`. -------------------------------------------------------------------------------- /Landmark.js: -------------------------------------------------------------------------------- 1 | class Landmark { 2 | constructor(position, radius) { 3 | this.position = position; 4 | this.radius = radius; 5 | } 6 | 7 | static draw(lm) { 8 | const { x, y } = Grid.getCoordiantesOf(lm.position); 9 | 10 | C2D.beginPath(); 11 | C2D.arc(x, y, lm.radius * UNIT, 0, 2 * PI); 12 | C2D.fillStyle = 'aqua'; 13 | C2D.fill(); 14 | C2D.stroke(); 15 | } 16 | } -------------------------------------------------------------------------------- /Functional.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Selection of functional helpers and enables piping on extended classes. 3 | */ 4 | class Functional { 5 | 6 | static tap(f) { 7 | return x => { f(x); return x; }; 8 | } 9 | 10 | static repeat(f, n) { 11 | return x => n ? Functional.repeat(f, n-1)(f(x)) : x; 12 | } 13 | 14 | static compose(...fns){ 15 | return x => fns.reduce((v, f) => f(v), x); 16 | } 17 | 18 | static map(f) { 19 | return x => x.length ? x.map(f) : f(x); 20 | } 21 | 22 | pipe(...fns) { 23 | return fns.reduce((v, f) => f(v), this); 24 | } 25 | } 26 | 27 | // alias for less verbose access 28 | const Fnl = Functional; -------------------------------------------------------------------------------- /List.js: -------------------------------------------------------------------------------- 1 | class List extends Functional { 2 | constructor(...items) { 3 | super(); 4 | this._list = items; 5 | } 6 | 7 | static map(f) { 8 | return l => new List(...l._list.map(f)); 9 | } 10 | 11 | static apply(v) { 12 | return l => new List(...l._list.map(f => f(v))); 13 | } 14 | 15 | static reduceWith(f) { 16 | return l => new List(...l._list.reduce((acc, val, idx) => idx ? f(acc)(val) : val)) 17 | } 18 | 19 | static mapWith(f) { 20 | return l => new List(...l._list.map((val, idx, arr) => idx ? f(arr[idx-1])(val) : val).slice(-l._list.length+1)) 21 | } 22 | 23 | static flatten(l) { 24 | return new List(...l._list.reduce((acc, val) => acc.concat(val._list), [])); 25 | } 26 | 27 | static toArray(l) { 28 | return Array.from(l._list); 29 | } 30 | } -------------------------------------------------------------------------------- /Grid.js: -------------------------------------------------------------------------------- 1 | class Grid { 2 | static getCoordiantesOf(point) { 3 | Grid._checkBounds(point); 4 | const canvasPoint = Grid._translateToCanvas(point); 5 | return canvasPoint.pipe( 6 | V2D.scale(UNIT), 7 | V2D.add(new Vector2D(UNIT / 2, UNIT / 2)) 8 | ); 9 | } 10 | 11 | static _translateToCanvas(point) { 12 | return point.pipe( 13 | V2D.mult(new Vector2D(1, -1)), 14 | V2D.add(new Vector2D((SPAN - 1) / 2, (SPAN - 1) / 2)) 15 | ); 16 | } 17 | 18 | static _checkBounds(point) { 19 | Grid._checkBound(point.x); 20 | Grid._checkBound(point.y); 21 | } 22 | 23 | static _checkBound(value) { 24 | if (value > (SPAN - 1) / 2) { 25 | throw `ValueError: ${value} too large.`; 26 | } 27 | if (value < -(SPAN - 1) / 2) { 28 | throw `ValueError: ${value} too small.`; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /VectorField.js: -------------------------------------------------------------------------------- 1 | class VectorField { 2 | constructor(span, resolution = 1, value = new Vector2D(0, 0)) { 3 | this.span = span; 4 | this.bound = (span - 1) / 2; 5 | this.resolution = resolution; 6 | this._field = []; 7 | 8 | for (let i = 0; i < this.span; i++) { 9 | this._field[i] = []; 10 | for (let ii = 0; ii < this.span; ii++) { 11 | this._field[i][ii] = value; 12 | } 13 | } 14 | 15 | } 16 | 17 | getValueAt(point) { 18 | this._checkForIndex(point); 19 | const { x, y } = this._translateToIndex(point); 20 | return this._field[x][y]; 21 | } 22 | 23 | setValueAt(point, value) { 24 | this._checkForIndex(point); 25 | const { x, y } = this._translateToIndex(point); 26 | const [ x1, y1 ] = [ x, y ].map(v => Math.floor(v + 0.1)); 27 | this._field[x1][y1] = value; 28 | } 29 | 30 | _translateToIndex(point) { 31 | return point.pipe( 32 | V2D.mult(new Vector2D(1, -1)), 33 | V2D.add(new Vector2D(LEN, LEN)), 34 | V2D.scale(1 / this.resolution) 35 | ); 36 | } 37 | 38 | round(number) { 39 | return Number((Math.round(number * 1 / this.resolution) * this.resolution).toFixed(2)); 40 | } 41 | 42 | _translateFromIndex(point) { 43 | return point.pipe( 44 | V2D.scale(this.resolution), 45 | V2D.sub(new Vector2D(LEN, LEN)), 46 | V2D.mult(new Vector2D(1, -1)), 47 | v => new Vector2D(this.round(v.x), this.round(v.y)) 48 | ); 49 | } 50 | 51 | _checkForIndex(point) { 52 | try { 53 | this._checkValue(point.x); 54 | this._checkValue(point.y); 55 | } catch(e) { 56 | throw `GridError: ${point} out of bound.\n${e}`; 57 | } 58 | } 59 | 60 | _checkBounds(point) { 61 | this._checkBound(point.x); 62 | this._checkBound(point.y); 63 | } 64 | 65 | _checkValue(value) { 66 | // this._checkIsIndex(value); 67 | this._checkBound(value); 68 | } 69 | 70 | _checkIsIndex(value) { 71 | if (value !== Math.floor(value)) { 72 | throw `ValueError: ${value} must be integer.`; 73 | } 74 | } 75 | 76 | _checkBound(value) { 77 | if (value > this.bound) { 78 | throw `ValueError: ${value} too large.`; 79 | } 80 | if (value < -this.bound) { 81 | throw `ValueError: ${value} too small.`; 82 | } 83 | } 84 | 85 | draw() { 86 | this._field.forEach((column, x) => { 87 | column.forEach((row, y) => { 88 | 89 | const center = Grid.getCoordiantesOf( 90 | this._translateFromIndex(new Vector2D(x, y)), 91 | ); 92 | 93 | row.pipe( 94 | V2D.mult(new Vector2D(1, -1)), 95 | V2D.add(center), 96 | v => { v.color = row.color; return v; }, 97 | V2D.draw(center) 98 | ); 99 | }) 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Vector2D.js: -------------------------------------------------------------------------------- 1 | class Vector2D extends Functional { 2 | constructor(x = 0, y = 0) { 3 | super(); 4 | this.x = x; 5 | this.y = y; 6 | this.color = '#f0f0f0'; 7 | } 8 | 9 | // computed data props 10 | get norm() { 11 | return Math.sqrt(this.x**2 + this.y**2); 12 | } 13 | 14 | get inverse() { 15 | return new Vector2D(-this.x, -this.y); 16 | } 17 | 18 | // display properly 19 | toString() { 20 | return `Vector2D { x: ${this.x}, y: ${this.y} } Rad: ${V2D.angleTo(E1)(this)}`; 21 | } 22 | 23 | // namespaced transforamtions 24 | static from(v) { 25 | return new Vector2D(v.x, v.y); 26 | } 27 | 28 | static fromRad(a) { 29 | return new Vector2D(Math.cos(a), Math.sin(a)); 30 | } 31 | 32 | static rotate(alpha) { 33 | return v => new Vector2D( 34 | Math.cos(alpha) * v.x - Math.sin(alpha) * v.y, 35 | Math.sin(alpha) * v.x + Math.cos(alpha) * v.y 36 | ); 37 | } 38 | 39 | static rotate90(clockwise = true) { 40 | return v => clockwise ? new Vector2D(v.y, -v.x) : new Vector2D(-v.y, v.x); 41 | } 42 | 43 | static add(v2) { 44 | return v1 => new Vector2D( 45 | v1.x + v2.x, 46 | v1.y + v2.y 47 | ); 48 | } 49 | 50 | static sum(vectors) { 51 | const x = vectors.reduce((a, b) => a + b.x, 0); 52 | const y = vectors.reduce((a, b) => a + b.y, 0); 53 | return new Vector2D(x, y); 54 | } 55 | 56 | static mult(v2) { 57 | return v1 => new Vector2D( 58 | v1.x * v2.x, 59 | v1.y * v2.y 60 | ); 61 | } 62 | 63 | static sub(v2) { 64 | return v1 => Vector2D.add(v2.inverse)(v1); 65 | } 66 | 67 | static dot(v2) { 68 | return v1 => v1.x * v2.x + v1.y * v2.y; 69 | } 70 | 71 | static det(v2) { 72 | return v1 => v1.x * v2.y - v1.y * v2.x; 73 | } 74 | 75 | // gives signed angle to rotate from v1 to v2 76 | static angleTo(v2) { 77 | return v1 => Math.atan2(v1.y, v1.x) - Math.atan2(v2.y, v2.x); 78 | } 79 | 80 | // gives absolute angle between v1 and v2 81 | static angleBetween(v2) { 82 | return v1 => Math.acos(Vector2D.dot(v1)(v2) / (v1.norm * v2.norm)); 83 | } 84 | 85 | static angleBetweenDir(v2) { 86 | return v1 => Math.atan2(V2D.det(v1)(v2), V2D.dot(v1)(v2)); 87 | } 88 | 89 | static scale(s) { 90 | return v => new Vector2D( 91 | v.x * s, 92 | v.y * s 93 | ); 94 | } 95 | 96 | static proximity(epsilon) { 97 | return v2 => v => v.x > v2.x - epsilon && v.x < v2.x + epsilon && v.y > v2.y - epsilon && v.y < v2.y + epsilon; 98 | } 99 | 100 | static resize(l) { 101 | return v => Vector2D.scale(l / v.norm)(v); 102 | } 103 | 104 | // draw vector from specified origin 105 | static draw({ x, y } = new Vector2D(), tipSize = 2) { 106 | return v => { 107 | 108 | const tip1 = v.pipe( 109 | V2D.sub(new Vector2D(x, y)), 110 | V2D.rotate(3 / 4 * PI), 111 | V2D.resize(tipSize), 112 | V2D.add(v), 113 | ); 114 | 115 | const tip2 = v.pipe( 116 | V2D.sub(new Vector2D(x, y)), 117 | V2D.rotate(-3 / 4 * PI), 118 | V2D.resize(tipSize), 119 | V2D.add(v), 120 | ); 121 | 122 | C2D.strokeStyle = v.color; 123 | C2D.beginPath(); 124 | C2D.moveTo(x, y); 125 | C2D.lineTo(v.x, v.y); 126 | C2D.lineTo(tip1.x, tip1.y); 127 | C2D.lineTo(tip2.x, tip2.y); 128 | C2D.lineTo(v.x, v.y); 129 | C2D.stroke(); 130 | return v; 131 | } 132 | } 133 | } 134 | 135 | // alias for less verbose access 136 | const V2D = Vector2D; 137 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const CONTEXT_2D = canvas.getContext('2d'); 2 | const INFO = info; 3 | const CANVAS_WIDTH = 800; 4 | const CANVAS_HEIGHT = 800; 5 | const PI = Math.PI; 6 | 7 | const ANIMATE = false; 8 | const FPS = 10; 9 | const EPSILON = .05; 10 | const STEP = .01; 11 | const RESOLUTION = .2; 12 | const HOME_VECTOR_DISPLAY_BASE_LENGTH = 50; 13 | 14 | const C2D = CONTEXT_2D; 15 | 16 | let LEN = 7; 17 | let SPAN = LEN * 2 + 1; 18 | let UNIT = CANVAS_WIDTH / SPAN; 19 | 20 | const E1 = new Vector2D(1, 0); 21 | 22 | function setup() { 23 | canvas.width = CANVAS_WIDTH; 24 | canvas.height = CANVAS_HEIGHT; 25 | 26 | run(); 27 | } 28 | 29 | function generate(bee, wanted, lms, draw = false) { 30 | const beeSectors = generateRetina(bee, lms, draw); 31 | const wantedSectors = generateRetina(wanted, lms, draw); 32 | 33 | if (draw) { 34 | console.log('beeSectors', beeSectors) 35 | console.log('wantedSectors', wantedSectors) 36 | } 37 | 38 | const [bds, bls] = makeBisectorRepresentation(beeSectors); 39 | const [wds, wls] = makeBisectorRepresentation(wantedSectors); 40 | 41 | if (draw) { 42 | console.log('beedarksectors', bds) 43 | console.log('beelightsectors', bls) 44 | 45 | console.log('wanteddarksectors', wds) 46 | console.log('wantedlightsectors', wls) 47 | } 48 | 49 | const darkMatched = matchSnapshot(bds, wds); 50 | const lightMatched = matchSnapshot(bls, wls); 51 | 52 | if (draw) { 53 | console.log('darkMatched', darkMatched) 54 | console.log('lightMatched', lightMatched) 55 | } 56 | 57 | const angles = [...darkMatched, ...lightMatched]; 58 | 59 | const positionVectors = calculatePositionVectors(angles); 60 | if (draw) console.log('positionVectors', positionVectors.map(x => V2D.angleTo(E1)(x))) 61 | 62 | const posSum = V2D.sum(positionVectors); 63 | // weight the position vector by 3 64 | const stretchSum = V2D.scale(3)(posSum); 65 | 66 | if (draw) console.log('angles', angles) 67 | const turningVectors = calculateTurningVectors(angles); 68 | if (draw) console.log('turningVectors', turningVectors.map(x => V2D.angleTo(E1)(x))) 69 | 70 | const posTurn = V2D.sum(turningVectors); 71 | 72 | let homing = V2D.sum([stretchSum, posTurn]); 73 | 74 | if (draw) console.log('homing vector', homing.toString()) 75 | 76 | //homing = flyAround(homing, darkMatched) 77 | 78 | return homing; 79 | } 80 | 81 | function flyAround(homing, darkMatched) { 82 | const vectors = []; 83 | 84 | for (const sec of darkMatched) { 85 | const vecBee = V2D.fromRad(sec.bee.rad); 86 | 87 | // rechts negativ / links positiv 88 | const rad = V2D.angleTo(vecBee)(homing); 89 | 90 | // determine the turn direction 91 | const clockwise = rad > 0 ? false : true; 92 | let temp = V2D.rotate90(clockwise)(homing); 93 | 94 | // inverse the direction if the angle is larger than 180 degrees 95 | let bla = 1; 96 | if (Math.abs(rad) < 1.2) bla = 2; 97 | temp = V2D.scale(sec.bee.size * bla)(temp); 98 | 99 | vectors.push(temp); 100 | } 101 | 102 | return V2D.sum([...vectors, homing]); 103 | } 104 | 105 | async function startAnimation(start, dest, lms) { 106 | let done = false; 107 | while (!done) { 108 | C2D.clearRect(0, 0, canvas.width, canvas.height); 109 | 110 | // animate here 111 | drawCanvasOutline(); 112 | List.map(Landmark.draw)(lms); 113 | 114 | const homing = generate(start, dest, lms, true) 115 | 116 | newStart = homing.pipe( 117 | V2D.scale(STEP), 118 | V2D.add(start), 119 | ); 120 | 121 | newStart.pipe( 122 | Grid.getCoordiantesOf, 123 | V2D.draw(start.pipe(Grid.getCoordiantesOf), 6), 124 | ); 125 | 126 | start = newStart; 127 | 128 | if (V2D.proximity(EPSILON)(dest)(start)) { 129 | done = true; 130 | console.log('@HOME'); 131 | } 132 | 133 | await sleep(1 / FPS * 1000); 134 | } 135 | } 136 | 137 | function drawVectorfield(wanted, lms, ignored) { 138 | const resolution = RESOLUTION; 139 | const len = LEN * Math.round(1 / resolution); 140 | const span = len * 2 + 1; 141 | 142 | const vectorField = new VectorField(span, resolution); 143 | 144 | const conversion = 255 / 20; 145 | 146 | // generate vector field 147 | const diffs = []; 148 | 149 | for (let i = -len; i <= len; i++) { 150 | for (let ii = -len; ii <= len; ii++) { 151 | const curr = new Vector2D(vectorField.round(i * resolution), vectorField.round(ii * resolution)); 152 | 153 | const found = ignored.find(v => v.x == curr.x && v.y == curr.y); 154 | if (found) continue; 155 | 156 | const home = generate(curr, wanted, lms); 157 | 158 | const colorAmount = home.norm * conversion; 159 | 160 | // resize vector for displaying 161 | const homeUnit = V2D.resize(HOME_VECTOR_DISPLAY_BASE_LENGTH / LEN)(home); 162 | 163 | // calculate the difference between a direct vector 164 | const diff = V2D.angleBetween(home)(new Vector2D(-i, -ii)) 165 | 166 | diffs.push(diff); 167 | 168 | homeUnit.color = `rgb(${colorAmount}, 0, ${255-colorAmount}, .7)`; 169 | vectorField.setValueAt(curr, homeUnit); 170 | } 171 | } 172 | 173 | const averageDiffRad = diffs.reduce((a, b) => a + b, 0) / diffs.length; 174 | const averageDiffDeg = averageDiffRad * 180 / PI; 175 | 176 | INFO.innerHTML = `Homing precision in degree: ${averageDiffDeg.toFixed(2)}`; 177 | console.log('homing precision in degree', averageDiffDeg); 178 | 179 | 180 | vectorField.draw(); 181 | } 182 | 183 | async function run() { 184 | 185 | drawCanvasOutline(); 186 | 187 | const l1 = new Vector2D(3.5, 2); 188 | const l2 = new Vector2D(3.5, -2); 189 | const l3 = new Vector2D(0, -4); 190 | const landmarks = [l1, l2, l3]; 191 | 192 | const lms = new List(...landmarks.map(l => new Landmark(l, 0.5))); 193 | List.map(Landmark.draw)(lms); 194 | 195 | bee = new Vector2D(7, 7); 196 | wanted = new Vector2D(0, 0); 197 | 198 | //generate(bee, wanted, lms, true) 199 | 200 | const done = false 201 | if (ANIMATE) startAnimation(bee, wanted, lms); 202 | else { 203 | const ignored = [...landmarks, wanted] 204 | drawVectorfield(wanted, lms, ignored); 205 | } 206 | } 207 | 208 | function generateRetina(position, landmarks, draw = false) { 209 | const getEdge = landmark => clockwise => Fnl.compose( 210 | V2D.sub(position), 211 | V2D.rotate90(clockwise), 212 | V2D.resize(landmark.radius), 213 | V2D.add(landmark.position), 214 | ); 215 | 216 | // helper for visual debugging 217 | const drawFromPos = Fnl.compose( 218 | Grid.getCoordiantesOf, 219 | V2D.draw(position.pipe( 220 | Grid.getCoordiantesOf 221 | )) 222 | ); 223 | 224 | const getEdges = landmark => new List(false, true).pipe( 225 | List.map(getEdge(landmark)), 226 | List.apply(landmark.position) 227 | ); 228 | 229 | const rv = landmarks.pipe( 230 | List.map(getEdges) 231 | ); 232 | 233 | const allE1Spots = rv.pipe( 234 | List.map( 235 | List.map( 236 | Fnl.compose( 237 | V2D.sub(position), 238 | V2D.angleTo(E1) 239 | ) 240 | ) 241 | ), 242 | ); 243 | 244 | const plain = allE1Spots.pipe( 245 | List.map( 246 | List.toArray 247 | ), 248 | List.toArray, 249 | ); 250 | 251 | const spots = plain.map(([stop, start]) => ({ start, stop })); 252 | 253 | const actualSpots = spots; 254 | /* 255 | // remove overlapping spots 256 | const actualSpots = []; 257 | 258 | for (const spot of spots) { 259 | if (spot.marked) continue; 260 | spot.marked = true; 261 | for (const otherSpot of spots) { 262 | if (otherSpot.marked) continue; 263 | if (otherSpot.start > spot.start && otherSpot.start <= spot.stop) { 264 | spot.stop = otherSpot.stop; 265 | otherSpot.marked = true; 266 | } 267 | if (otherSpot.stop < spot.stop && otherSpot.stop >= spot.start) { 268 | spot.start = otherSpot.start; 269 | otherSpot.marked = true; 270 | } 271 | } 272 | 273 | actualSpots.push(spot); 274 | } 275 | */ 276 | 277 | 278 | if (draw) { 279 | rv.pipe( 280 | List.map( 281 | List.map(drawFromPos) 282 | ) 283 | ); 284 | } 285 | 286 | // sort in anti clockwise direction 287 | return actualSpots.sort((a, b) => { 288 | if (a.start < 0 && b.start < 0) { 289 | return a.start - b.start; 290 | } 291 | 292 | return b.start - a.start; 293 | }); 294 | } 295 | 296 | function makeBisectorRepresentation(snapshot) { 297 | freeBisectors = []; 298 | objectBisectors = []; 299 | for (let idx = 0; idx < snapshot.length; idx++) { 300 | objectBisectors.push(getMidValue(snapshot[idx].start, snapshot[idx].stop)); 301 | freeBisectors.push(getMidValue(snapshot[idx].stop, snapshot[(idx + 1) % snapshot.length].start)); 302 | } 303 | 304 | return [objectBisectors, freeBisectors]; 305 | } 306 | 307 | function getMidValue(start, stop) { 308 | let size = V2D.angleBetweenDir(V2D.fromRad(start))(V2D.fromRad(stop)); 309 | // determine when to use the larger angle 310 | if (size < 0) size = PI * 2 + size; 311 | 312 | // calculating the 'mid' angle 313 | if (start > stop) stop += PI * 2; 314 | let rad = (start + stop) / 2; 315 | if (rad > PI) rad -= PI * 2; 316 | 317 | return { rad, size }; 318 | } 319 | 320 | 321 | function matchSnapshot(bee, wanted) { 322 | const matched = []; 323 | for (const angle of bee) { 324 | let closest = null; 325 | const vec = V2D.fromRad(angle.rad); 326 | 327 | for (const wAngle of wanted) { 328 | if (closest == null) { 329 | closest = wAngle; 330 | continue; 331 | } 332 | 333 | const otherVec = V2D.fromRad(wAngle.rad); 334 | const oldVec = V2D.fromRad(closest.rad); 335 | const between = V2D.angleBetween(vec)(otherVec) ; 336 | const oldBetween = V2D.angleBetween(vec)(oldVec); 337 | 338 | if (between < oldBetween) { 339 | closest = wAngle; 340 | } 341 | } 342 | 343 | matched.push({ bee: angle, matched: closest }); 344 | } 345 | 346 | return matched; 347 | } 348 | 349 | function calculatePositionVectors(sectors) { 350 | const vectors = []; 351 | 352 | for (const sec of sectors) { 353 | let vec = V2D.fromRad(sec.bee.rad); 354 | // inverse direction of vector if bee size is bigger than matched size 355 | if (sec.bee.size > sec.matched.size) vec = vec.inverse; 356 | 357 | vectors.push(vec); 358 | } 359 | 360 | return vectors; 361 | } 362 | 363 | function calculateTurningVectors(sectors) { 364 | const vectors = []; 365 | 366 | for (const sec of sectors) { 367 | const vecBee = V2D.fromRad(sec.bee.rad); 368 | const vecWanted = V2D.fromRad(sec.matched.rad); 369 | 370 | // rechts negativ / links positiv 371 | const rad = V2D.angleTo(vecBee)(vecWanted); 372 | 373 | // determine the turn direction 374 | const clockwise = rad > 0 ? false : true; 375 | let temp = V2D.rotate90(clockwise)(vecBee); 376 | 377 | // inverse the direction if the angle is larger than 180 degrees 378 | if (sec.bee.size > PI) temp = temp.inverse; 379 | vectors.push(temp); 380 | } 381 | 382 | return vectors; 383 | } 384 | 385 | function drawCanvasOutline() { 386 | // draw outline and crosshair 387 | C2D.beginPath(); 388 | C2D.rect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 389 | C2D.moveTo(0, CANVAS_WIDTH / 2); 390 | C2D.lineTo(CANVAS_HEIGHT, CANVAS_WIDTH / 2); 391 | C2D.moveTo(CANVAS_HEIGHT / 2, 0); 392 | C2D.lineTo(CANVAS_HEIGHT / 2, CANVAS_WIDTH); 393 | C2D.stroke(); 394 | 395 | // draw center dot 396 | C2D.beginPath(); 397 | C2D.arc(CANVAS_HEIGHT / 2, CANVAS_WIDTH / 2, 4, 0, 2 * PI); 398 | C2D.fillStyle = 'black'; 399 | C2D.fill(); 400 | C2D.stroke(); 401 | } 402 | --------------------------------------------------------------------------------