ImageCrop.java revision c5590eb1a20b112e67e4c43684790587f844fc6b
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.gallery3d.filtershow.imageshow; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.graphics.Bitmap; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.Matrix; 25import android.graphics.Paint; 26import android.graphics.RectF; 27import android.graphics.drawable.Drawable; 28import android.util.AttributeSet; 29import android.util.Log; 30 31import com.android.gallery3d.R; 32 33public class ImageCrop extends ImageGeometry { 34 private static final boolean LOGV = false; 35 private static final int MOVE_LEFT = 1; 36 private static final int MOVE_TOP = 2; 37 private static final int MOVE_RIGHT = 4; 38 private static final int MOVE_BOTTOM = 8; 39 private static final int MOVE_BLOCK = 16; 40 41 //Corners 42 private static final int TOP_LEFT = MOVE_TOP | MOVE_LEFT; 43 private static final int TOP_RIGHT = MOVE_TOP | MOVE_RIGHT; 44 private static final int BOTTOM_RIGHT = MOVE_BOTTOM | MOVE_RIGHT; 45 private static final int BOTTOM_LEFT = MOVE_BOTTOM | MOVE_LEFT; 46 47 private static final float MIN_CROP_WIDTH_HEIGHT = 0.1f; 48 private static final int TOUCH_TOLERANCE = 30; 49 50 private boolean mFirstDraw = true; 51 private float mAspectWidth = 1; 52 private float mAspectHeight = 1; 53 private boolean mFixAspectRatio = false; 54 55 private final Paint borderPaint; 56 57 private int movingEdges; 58 private final Drawable cropIndicator; 59 private final int indicatorSize; 60 61 private static final String LOGTAG = "ImageCrop"; 62 63 private static final Paint gPaint = new Paint(); 64 65 public ImageCrop(Context context) { 66 super(context); 67 Resources resources = context.getResources(); 68 cropIndicator = resources.getDrawable(R.drawable.camera_crop); 69 indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size); 70 int borderColor = Color.argb(128, 255, 255, 255); 71 borderPaint = new Paint(); 72 borderPaint.setStyle(Paint.Style.STROKE); 73 borderPaint.setColor(borderColor); 74 borderPaint.setStrokeWidth(2f); 75 } 76 77 public ImageCrop(Context context, AttributeSet attrs) { 78 super(context, attrs); 79 Resources resources = context.getResources(); 80 cropIndicator = resources.getDrawable(R.drawable.camera_crop); 81 indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size); 82 int borderColor = Color.argb(128, 255, 255, 255); 83 borderPaint = new Paint(); 84 borderPaint.setStyle(Paint.Style.STROKE); 85 borderPaint.setColor(borderColor); 86 borderPaint.setStrokeWidth(2f); 87 } 88 89 @Override 90 public String getName() { 91 return "Crop"; 92 } 93 94 private boolean switchCropBounds(int moving_corner, RectF dst) { 95 RectF crop = getCropBoundsDisplayed(); 96 float dx1 = 0; 97 float dy1 = 0; 98 float dx2 = 0; 99 float dy2 = 0; 100 if ((moving_corner & MOVE_RIGHT) != 0) { 101 dx1 = mCurrentX - crop.right; 102 } else if ((moving_corner & MOVE_LEFT) != 0) { 103 dx1 = mCurrentX - crop.left; 104 } 105 if ((moving_corner & MOVE_BOTTOM) != 0) { 106 dy1 = mCurrentY - crop.bottom; 107 } else if ((moving_corner & MOVE_TOP) != 0) { 108 dy1 = mCurrentY - crop.top; 109 } 110 RectF newCrop = null; 111 //Fix opposite corner in place and move sides 112 if (moving_corner == BOTTOM_RIGHT) { 113 newCrop = new RectF(crop.left, crop.top, crop.left + crop.height(), crop.top 114 + crop.width()); 115 } else if (moving_corner == BOTTOM_LEFT) { 116 newCrop = new RectF(crop.right - crop.height(), crop.top, crop.right, crop.top 117 + crop.width()); 118 } else if (moving_corner == TOP_LEFT) { 119 newCrop = new RectF(crop.right - crop.height(), crop.bottom - crop.width(), 120 crop.right, crop.bottom); 121 } else if (moving_corner == TOP_RIGHT) { 122 newCrop = new RectF(crop.left, crop.bottom - crop.width(), crop.left 123 + crop.height(), crop.bottom); 124 } 125 if ((moving_corner & MOVE_RIGHT) != 0) { 126 dx2 = mCurrentX - newCrop.right; 127 } else if ((moving_corner & MOVE_LEFT) != 0) { 128 dx2 = mCurrentX - newCrop.left; 129 } 130 if ((moving_corner & MOVE_BOTTOM) != 0) { 131 dy2 = mCurrentY - newCrop.bottom; 132 } else if ((moving_corner & MOVE_TOP) != 0) { 133 dy2 = mCurrentY - newCrop.top; 134 } 135 if (Math.sqrt(dx1*dx1 + dy1*dy1) > Math.sqrt(dx2*dx2 + dy2*dy2)){ 136 Matrix m = getCropBoundDisplayMatrix(); 137 Matrix m0 = new Matrix(); 138 if (!m.invert(m0)){ 139 if (LOGV) 140 Log.v(LOGTAG, "FAILED TO INVERT CROP MATRIX"); 141 return false; 142 } 143 if (!m0.mapRect(newCrop)){ 144 if (LOGV) 145 Log.v(LOGTAG, "FAILED TO MAP RECTANGLE TO RECTANGLE"); 146 return false; 147 } 148 float temp = mAspectWidth; 149 mAspectWidth = mAspectHeight; 150 mAspectHeight = temp; 151 dst.set(newCrop); 152 return true; 153 } 154 return false; 155 } 156 157 public void apply(float w, float h){ 158 mFixAspectRatio = true; 159 mAspectWidth = w; 160 mAspectHeight = h; 161 setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(), 162 getLocalStraighten())); 163 cropSetup(); 164 saveAndSetPreset(); 165 invalidate(); 166 } 167 168 public void applyOriginal() { 169 mFixAspectRatio = true; 170 RectF photobounds = getLocalPhotoBounds(); 171 float w = photobounds.width(); 172 float h = photobounds.height(); 173 float scale = Math.min(w, h); 174 mAspectWidth = w / scale; 175 mAspectHeight = h / scale; 176 setLocalCropBounds(getUntranslatedStraightenCropBounds(photobounds, 177 getLocalStraighten())); 178 cropSetup(); 179 saveAndSetPreset(); 180 invalidate(); 181 } 182 183 public void applyClear() { 184 mFixAspectRatio = false; 185 setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(), 186 getLocalStraighten())); 187 cropSetup(); 188 saveAndSetPreset(); 189 invalidate(); 190 } 191 192 private float getScaledMinWidthHeight() { 193 RectF disp = new RectF(0, 0, getWidth(), getHeight()); 194 float scaled = Math.min(disp.width(), disp.height()) * MIN_CROP_WIDTH_HEIGHT 195 / computeScale(getWidth(), getHeight()); 196 return scaled; 197 } 198 199 protected Matrix getCropRotationMatrix(float rotation, RectF localImage) { 200 Matrix m = getLocalGeoFlipMatrix(localImage.width(), localImage.height()); 201 m.postRotate(rotation, localImage.centerX(), localImage.centerY()); 202 if (!m.rectStaysRect()) { 203 return null; 204 } 205 return m; 206 } 207 208 protected Matrix getCropBoundDisplayMatrix(){ 209 Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds()); 210 if (m == null) { 211 if (LOGV) 212 Log.v(LOGTAG, "FAILED TO MAP CROP BOUNDS TO RECTANGLE"); 213 m = new Matrix(); 214 } 215 float zoom = computeScale(getWidth(), getHeight()); 216 m.postTranslate(mXOffset, mYOffset); 217 m.postScale(zoom, zoom, mCenterX, mCenterY); 218 return m; 219 } 220 221 protected RectF getCropBoundsDisplayed() { 222 RectF bounds = getLocalCropBounds(); 223 RectF crop = new RectF(bounds); 224 Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds()); 225 226 if (m == null) { 227 if (LOGV) 228 Log.v(LOGTAG, "FAILED TO MAP CROP BOUNDS TO RECTANGLE"); 229 m = new Matrix(); 230 } else { 231 m.mapRect(crop); 232 } 233 m = new Matrix(); 234 float zoom = computeScale(getWidth(), getHeight()); 235 m.setScale(zoom, zoom, mCenterX, mCenterY); 236 m.preTranslate(mXOffset, mYOffset); 237 m.mapRect(crop); 238 return crop; 239 } 240 241 private RectF getRotatedCropBounds() { 242 RectF bounds = getLocalCropBounds(); 243 RectF crop = new RectF(bounds); 244 Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds()); 245 246 if (m == null) { 247 if (LOGV) 248 Log.v(LOGTAG, "FAILED TO MAP CROP BOUNDS TO RECTANGLE"); 249 return null; 250 } else { 251 m.mapRect(crop); 252 } 253 return crop; 254 } 255 256 private RectF getUnrotatedCropBounds(RectF cropBounds) { 257 Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds()); 258 259 if (m == null) { 260 if (LOGV) 261 Log.v(LOGTAG, "FAILED TO GET ROTATION MATRIX"); 262 return null; 263 } 264 Matrix m0 = new Matrix(); 265 if (!m.invert(m0)) { 266 if (LOGV) 267 Log.v(LOGTAG, "FAILED TO INVERT ROTATION MATRIX"); 268 return null; 269 } 270 RectF crop = new RectF(cropBounds); 271 if (!m0.mapRect(crop)) { 272 if (LOGV) 273 Log.v(LOGTAG, "FAILED TO UNROTATE CROPPING BOUNDS"); 274 return null; 275 } 276 return crop; 277 } 278 279 private RectF getRotatedStraightenBounds() { 280 RectF straightenBounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(), 281 getLocalStraighten()); 282 Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds()); 283 284 if (m == null) { 285 if (LOGV) 286 Log.v(LOGTAG, "FAILED TO MAP STRAIGHTEN BOUNDS TO RECTANGLE"); 287 return null; 288 } else { 289 m.mapRect(straightenBounds); 290 } 291 return straightenBounds; 292 } 293 294 /** 295 * Sets cropped bounds; modifies the bounds if it's smaller than the allowed 296 * dimensions. 297 */ 298 public void setCropBounds(RectF bounds) { 299 // Avoid cropping smaller than minimum width or height. 300 RectF cbounds = new RectF(bounds); 301 float minWidthHeight = getScaledMinWidthHeight(); 302 float aw = mAspectWidth; 303 float ah = mAspectHeight; 304 if (mFixAspectRatio) { 305 minWidthHeight /= aw * ah; 306 int r = (int) (getLocalRotation() / 90); 307 if (r % 2 != 0) { 308 float temp = aw; 309 aw = ah; 310 ah = temp; 311 } 312 } 313 314 float newWidth = cbounds.width(); 315 float newHeight = cbounds.height(); 316 if (mFixAspectRatio) { 317 if (newWidth < (minWidthHeight * aw) || newHeight < (minWidthHeight * ah)) { 318 newWidth = minWidthHeight * aw; 319 newHeight = minWidthHeight * ah; 320 } 321 } else { 322 if (newWidth < minWidthHeight) { 323 newWidth = minWidthHeight; 324 } 325 if (newHeight < minWidthHeight) { 326 newHeight = minWidthHeight; 327 } 328 } 329 RectF pbounds = getLocalPhotoBounds(); 330 if (pbounds.width() < minWidthHeight) { 331 newWidth = pbounds.width(); 332 } 333 if (pbounds.height() < minWidthHeight) { 334 newHeight = pbounds.height(); 335 } 336 337 cbounds.set(cbounds.left, cbounds.top, cbounds.left + newWidth, cbounds.top + newHeight); 338 RectF straightenBounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(), 339 getLocalStraighten()); 340 cbounds.intersect(straightenBounds); 341 342 if (mFixAspectRatio) { 343 fixAspectRatio(cbounds, aw, ah); 344 } 345 setLocalCropBounds(cbounds); 346 invalidate(); 347 } 348 349 private void detectMovingEdges(float x, float y) { 350 RectF cropped = getCropBoundsDisplayed(); 351 movingEdges = 0; 352 353 // Check left or right. 354 float left = Math.abs(x - cropped.left); 355 float right = Math.abs(x - cropped.right); 356 if ((left <= TOUCH_TOLERANCE) && (left < right)) { 357 movingEdges |= MOVE_LEFT; 358 } 359 else if (right <= TOUCH_TOLERANCE) { 360 movingEdges |= MOVE_RIGHT; 361 } 362 363 // Check top or bottom. 364 float top = Math.abs(y - cropped.top); 365 float bottom = Math.abs(y - cropped.bottom); 366 if ((top <= TOUCH_TOLERANCE) & (top < bottom)) { 367 movingEdges |= MOVE_TOP; 368 } 369 else if (bottom <= TOUCH_TOLERANCE) { 370 movingEdges |= MOVE_BOTTOM; 371 } 372 // Check inside block. 373 if (cropped.contains(x, y) && (movingEdges == 0)) { 374 movingEdges = MOVE_BLOCK; 375 } 376 if (mFixAspectRatio && (movingEdges != MOVE_BLOCK)) { 377 movingEdges = fixEdgeToCorner(movingEdges); 378 } 379 invalidate(); 380 } 381 382 private int fixEdgeToCorner(int moving_edges){ 383 if (moving_edges == MOVE_LEFT) { 384 moving_edges |= MOVE_TOP; 385 } 386 if (moving_edges == MOVE_TOP) { 387 moving_edges |= MOVE_LEFT; 388 } 389 if (moving_edges == MOVE_RIGHT) { 390 moving_edges |= MOVE_BOTTOM; 391 } 392 if (moving_edges == MOVE_BOTTOM) { 393 moving_edges |= MOVE_RIGHT; 394 } 395 return moving_edges; 396 } 397 398 private RectF fixedCornerResize(RectF r, int moving_corner, float dx, float dy){ 399 RectF newCrop = null; 400 //Fix opposite corner in place and move sides 401 if (moving_corner == BOTTOM_RIGHT) { 402 newCrop = new RectF(r.left, r.top, r.left + r.width() + dx, r.top + r.height() 403 + dy); 404 } else if (moving_corner == BOTTOM_LEFT) { 405 newCrop = new RectF(r.right - r.width() + dx, r.top, r.right, r.top + r.height() 406 + dy); 407 } else if (moving_corner == TOP_LEFT) { 408 newCrop = new RectF(r.right - r.width() + dx, r.bottom - r.height() + dy, 409 r.right, r.bottom); 410 } else if (moving_corner == TOP_RIGHT) { 411 newCrop = new RectF(r.left, r.bottom - r.height() + dy, r.left 412 + r.width() + dx, r.bottom); 413 } 414 return newCrop; 415 } 416 417 private void moveEdges(float dX, float dY) { 418 RectF cropped = getRotatedCropBounds(); 419 float minWidthHeight = getScaledMinWidthHeight(); 420 float scale = computeScale(getWidth(), getHeight()); 421 float deltaX = dX / scale; 422 float deltaY = dY / scale; 423 int select = movingEdges; 424 if (mFixAspectRatio && (select != MOVE_BLOCK)) { 425 if (select == MOVE_LEFT) { 426 select |= MOVE_TOP; 427 } 428 if (select == MOVE_TOP) { 429 select |= MOVE_LEFT; 430 } 431 if (select == MOVE_RIGHT) { 432 select |= MOVE_BOTTOM; 433 } 434 if (select == MOVE_BOTTOM) { 435 select |= MOVE_RIGHT; 436 } 437 RectF blank = new RectF(); 438 if(switchCropBounds(select, blank)){ 439 setCropBounds(blank); 440 return; 441 } 442 } 443 444 if (select == MOVE_BLOCK) { 445 RectF straight = getRotatedStraightenBounds(); 446 // Move the whole cropped bounds within the photo display bounds. 447 deltaX = (deltaX > 0) ? Math.min(straight.right - cropped.right, deltaX) 448 : Math.max(straight.left - cropped.left, deltaX); 449 deltaY = (deltaY > 0) ? Math.min(straight.bottom - cropped.bottom, deltaY) 450 : Math.max(straight.top - cropped.top, deltaY); 451 cropped.offset(deltaX, deltaY); 452 } else { 453 float dx = 0; 454 float dy = 0; 455 456 if ((select & MOVE_LEFT) != 0) { 457 dx = Math.min(cropped.left + deltaX, cropped.right - minWidthHeight) - cropped.left; 458 } 459 if ((select & MOVE_TOP) != 0) { 460 dy = Math.min(cropped.top + deltaY, cropped.bottom - minWidthHeight) - cropped.top; 461 } 462 if ((select & MOVE_RIGHT) != 0) { 463 dx = Math.max(cropped.right + deltaX, cropped.left + minWidthHeight) 464 - cropped.right; 465 } 466 if ((select & MOVE_BOTTOM) != 0) { 467 dy = Math.max(cropped.bottom + deltaY, cropped.top + minWidthHeight) 468 - cropped.bottom; 469 } 470 471 if (mFixAspectRatio) { 472 RectF crop = getCropBoundsDisplayed(); 473 float [] l1 = {crop.left, crop.bottom}; 474 float [] l2 = {crop.right, crop.top}; 475 if(movingEdges == TOP_LEFT || movingEdges == BOTTOM_RIGHT){ 476 l1[1] = crop.top; 477 l2[1] = crop.bottom; 478 } 479 float[] b = { l1[0] - l2[0], l1[1] - l2[1] }; 480 float[] disp = {dx, dy}; 481 float[] bUnit = GeometryMath.normalize(b); 482 float sp = GeometryMath.scalarProjection(disp, bUnit); 483 dx = sp * bUnit[0]; 484 dy = sp * bUnit[1]; 485 RectF newCrop = fixedCornerResize(crop, select, dx * scale, dy * scale); 486 Matrix m = getCropBoundDisplayMatrix(); 487 Matrix m0 = new Matrix(); 488 if (!m.invert(m0)){ 489 if (LOGV) 490 Log.v(LOGTAG, "FAILED TO INVERT CROP MATRIX"); 491 return; 492 } 493 if (!m0.mapRect(newCrop)){ 494 if (LOGV) 495 Log.v(LOGTAG, "FAILED TO MAP RECTANGLE TO RECTANGLE"); 496 return; 497 } 498 setCropBounds(newCrop); 499 return; 500 } else { 501 if ((select & MOVE_LEFT) != 0) { 502 cropped.left += dx; 503 } 504 if ((select & MOVE_TOP) != 0) { 505 cropped.top += dy; 506 } 507 if ((select & MOVE_RIGHT) != 0) { 508 cropped.right += dx; 509 } 510 if ((select & MOVE_BOTTOM) != 0) { 511 cropped.bottom += dy; 512 } 513 } 514 } 515 movingEdges = select; 516 Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds()); 517 Matrix m0 = new Matrix(); 518 if (!m.invert(m0)) { 519 if (LOGV) 520 Log.v(LOGTAG, "FAILED TO INVERT ROTATION MATRIX"); 521 } 522 if (!m0.mapRect(cropped)) { 523 if (LOGV) 524 Log.v(LOGTAG, "FAILED TO UNROTATE CROPPING BOUNDS"); 525 } 526 setCropBounds(cropped); 527 } 528 529 private void drawIndicator(Canvas canvas, Drawable indicator, float centerX, float centerY) { 530 int left = (int) centerX - indicatorSize / 2; 531 int top = (int) centerY - indicatorSize / 2; 532 indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize); 533 indicator.draw(canvas); 534 } 535 536 @Override 537 protected void setActionDown(float x, float y) { 538 super.setActionDown(x, y); 539 detectMovingEdges(x, y); 540 } 541 542 @Override 543 protected void setActionUp() { 544 super.setActionUp(); 545 movingEdges = 0; 546 } 547 548 @Override 549 protected void setActionMove(float x, float y) { 550 if (movingEdges != 0){ 551 moveEdges(x - mCurrentX, y - mCurrentY); 552 } 553 super.setActionMove(x, y); 554 } 555 556 private void cropSetup() { 557 if (mFixAspectRatio) { 558 RectF cb = getRotatedCropBounds(); 559 fixAspectRatio(cb, mAspectWidth, mAspectHeight); 560 RectF cb0 = getUnrotatedCropBounds(cb); 561 setCropBounds(cb0); 562 } else { 563 setCropBounds(getLocalCropBounds()); 564 } 565 } 566 567 @Override 568 protected void gainedVisibility() { 569 cropSetup(); 570 mFirstDraw = true; 571 } 572 573 @Override 574 public void resetParameter() { 575 super.resetParameter(); 576 cropSetup(); 577 } 578 579 @Override 580 protected void lostVisibility() { 581 } 582 583 @Override 584 protected void drawShape(Canvas canvas, Bitmap image) { 585 // TODO: move style to xml 586 gPaint.setAntiAlias(true); 587 gPaint.setFilterBitmap(true); 588 gPaint.setDither(true); 589 gPaint.setARGB(255, 255, 255, 255); 590 591 if (mFirstDraw) { 592 cropSetup(); 593 mFirstDraw = false; 594 } 595 float rotation = getLocalRotation(); 596 drawTransformedBitmap(canvas, image, gPaint, true); 597 598 gPaint.setARGB(255, 125, 255, 128); 599 gPaint.setStrokeWidth(3); 600 gPaint.setStyle(Paint.Style.STROKE); 601 drawStraighten(canvas, gPaint); 602 RectF scaledCrop = unrotatedCropBounds(); 603 int decoded_moving = decoder(movingEdges, rotation); 604 canvas.save(); 605 canvas.rotate(rotation, mCenterX, mCenterY); 606 boolean notMoving = decoded_moving == 0; 607 if (((decoded_moving & MOVE_TOP) != 0) || notMoving) { 608 drawIndicator(canvas, cropIndicator, scaledCrop.centerX(), scaledCrop.top); 609 } 610 if (((decoded_moving & MOVE_BOTTOM) != 0) || notMoving) { 611 drawIndicator(canvas, cropIndicator, scaledCrop.centerX(), scaledCrop.bottom); 612 } 613 if (((decoded_moving & MOVE_LEFT) != 0) || notMoving) { 614 drawIndicator(canvas, cropIndicator, scaledCrop.left, scaledCrop.centerY()); 615 } 616 if (((decoded_moving & MOVE_RIGHT) != 0) || notMoving) { 617 drawIndicator(canvas, cropIndicator, scaledCrop.right, scaledCrop.centerY()); 618 } 619 canvas.restore(); 620 } 621 622 private int bitCycleLeft(int x, int times, int d) { 623 int mask = (1 << d) - 1; 624 int mout = x & mask; 625 times %= d; 626 int hi = mout >> (d - times); 627 int low = (mout << times) & mask; 628 int ret = x & ~mask; 629 ret |= low; 630 ret |= hi; 631 return ret; 632 } 633 634 protected int decoder(int movingEdges, float rotation) { 635 int rot = constrainedRotation(rotation); 636 switch (rot) { 637 case 90: 638 return bitCycleLeft(movingEdges, 3, 4); 639 case 180: 640 return bitCycleLeft(movingEdges, 2, 4); 641 case 270: 642 return bitCycleLeft(movingEdges, 1, 4); 643 default: 644 return movingEdges; 645 } 646 } 647} 648