/* * Copyright 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.nurik.roman.formwatchface.common; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; import android.graphics.RectF; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import static net.nurik.roman.formwatchface.common.MathUtil.accelerate5; import static net.nurik.roman.formwatchface.common.MathUtil.decelerate5; import static net.nurik.roman.formwatchface.common.MathUtil.interpolate; import static net.nurik.roman.formwatchface.common.MathUtil.progress; public class FormClockRenderer { private static final long DEBUG_TIME_MILLIS = 0; //new Date(2015, 2, 22, 12, 59, 52).getTime(); private static final long BOOT_TIME_MILLIS = System.currentTimeMillis(); private static final String DEBUG_TIME = null;//"0123456789"; private static final String DEBUG_GLYPH = null;//"2_3"; private static final boolean DEBUG_SHOW_RECTS = false; private int[] mAnimatedGlyphIndices = new int[20]; private int[] mTempAnimatedGlyphIndices = new int[20]; private int mAnimatedGlyphIndexCount = 0; private Glyph[] mGlyphs = new Glyph[20]; private int mGlyphCount = 0; private Options mOptions; private ClockPaints mPaints; private Font mFont = new Font(); // for offscreen glyphs private Bitmap mOffsGlyphBitmap; private Canvas mOffsGlyphCanvas; private Paint mOffsGlyphPaint; private int mOffsGlyphBitmapUnpaddedSize; private Paint mDebugShowRectPaint; private long mAnimDuration; private long mAnimTime; private Calendar mTempCalendar; private long mTimeMillis; private long mMillisToNext; private PointF mMeasuredSize = new PointF(); public FormClockRenderer(Options options, ClockPaints paints) { this.mOptions = options; this.mPaints = paints; this.mTempCalendar = Calendar.getInstance(); updateTime(); initOffsGlyphBitmap(); if (DEBUG_SHOW_RECTS) { mDebugShowRectPaint = new Paint(); mDebugShowRectPaint.setStrokeWidth(4); mDebugShowRectPaint.setColor(Color.RED); mDebugShowRectPaint.setStyle(Paint.Style.STROKE); } } private void initOffsGlyphBitmap() { mOffsGlyphBitmapUnpaddedSize = Font.DRAWHEIGHT * 5; while (mOptions.textSize < mOffsGlyphBitmapUnpaddedSize) { int newUnpaddedSize = mOffsGlyphBitmapUnpaddedSize; if (newUnpaddedSize > Font.DRAWHEIGHT) { newUnpaddedSize -= Font.DRAWHEIGHT; } else { newUnpaddedSize /= 2; } if (mOptions.textSize > newUnpaddedSize) { break; } mOffsGlyphBitmapUnpaddedSize = newUnpaddedSize; } mOffsGlyphBitmap = Bitmap.createBitmap( mOffsGlyphBitmapUnpaddedSize * 2, mOffsGlyphBitmapUnpaddedSize * 2, Bitmap.Config.ARGB_8888); mOffsGlyphCanvas = new Canvas(mOffsGlyphBitmap); mOffsGlyphPaint = new Paint(); mOffsGlyphPaint.setFilterBitmap(true); } public void setPaints(ClockPaints paints) { mPaints = paints; } public void updateTime() { mTimeMillis = System.currentTimeMillis(); if (DEBUG_TIME_MILLIS > 0) { long v = DEBUG_TIME_MILLIS + (System.currentTimeMillis() - BOOT_TIME_MILLIS); mTimeMillis = v; } mTempCalendar.setTimeInMillis(mTimeMillis); String currentTimeStr, nextTimeStr; mTempCalendar.set(Calendar.MILLISECOND, 0); if (mOptions.onlySeconds) { currentTimeStr = secondsString(mTempCalendar); mTempCalendar.add(Calendar.SECOND, 1); nextTimeStr = secondsString(mTempCalendar); } else { mTempCalendar.set(Calendar.SECOND, 0); currentTimeStr = hourMinString(mTempCalendar); mTempCalendar.add(Calendar.MINUTE, 1); nextTimeStr = hourMinString(mTempCalendar); } mMillisToNext = mTempCalendar.getTimeInMillis() - mTimeMillis; updateGlyphsAndAnimDuration(currentTimeStr, nextTimeStr); if (mMillisToNext < mAnimDuration) { // currently animating mAnimTime = mAnimDuration - mMillisToNext; } else { mAnimTime = 0; } } public void updateGlyphsAndAnimDuration(String currentTimeStr, String nextTimeStr) { mAnimatedGlyphIndexCount = 0; mGlyphCount = 0; if (DEBUG_GLYPH != null) { mGlyphs[mGlyphCount++] = mFont.getGlyph(DEBUG_GLYPH); mTempAnimatedGlyphIndices[mAnimatedGlyphIndexCount++] = 0; } else if (DEBUG_TIME != null) { for (int i = 0; i < DEBUG_TIME.length(); i++) { mGlyphs[mGlyphCount++] = mFont.getGlyph(Character.toString(DEBUG_TIME.charAt(i))); } } else { int len = currentTimeStr.length(); for (int i = 0; i < len; i++) { char c1 = currentTimeStr.charAt(i); char c2 = nextTimeStr.charAt(i); if (c1 == ':') { mGlyphs[mGlyphCount++] = mFont.getGlyph(":"); continue; } if (c1 == c2) { mGlyphs[mGlyphCount++] = mFont.getGlyph(String.valueOf(c1)); } else { mTempAnimatedGlyphIndices[mAnimatedGlyphIndexCount++] = i; mGlyphs[mGlyphCount++] = mFont.getGlyph(c1 + "_" + c2); } } } // reverse animted glyph indices for (int i = 0; i < mAnimatedGlyphIndexCount; i++) { mAnimatedGlyphIndices[i] = mTempAnimatedGlyphIndices[mAnimatedGlyphIndexCount - i - 1]; } mAnimDuration = mAnimatedGlyphIndexCount * mOptions.glyphAnimAverageDelay + mOptions.glyphAnimDuration; mAnimTime = 0; } public long timeToNextAnimation() { return mMillisToNext - mAnimDuration; } public PointF measure(boolean allowAnimate) { mMeasuredSize.set(0, 0); layoutPass(new LayoutPassCallback() { @Override public void visitGlyph(Glyph glyph, float glyphAnimProgress, RectF rect) { mMeasuredSize.x = Math.max(mMeasuredSize.x, rect.right); mMeasuredSize.y = Math.max(mMeasuredSize.y, rect.bottom); } }, allowAnimate, new RectF()); return mMeasuredSize; } private void layoutPass(LayoutPassCallback cb, boolean allowAnimate, RectF rectF) { float x = 0; for (int i = 0; i < mGlyphCount; i++) { Glyph glyph = mGlyphs[i]; float t = getGlyphAnimProgress(i); if (!allowAnimate && t > 0f) { t = 1; } float glyphWidth = glyph.getWidthAtProgress(t) * mOptions.textSize / Font.DRAWHEIGHT; rectF.set(x, 0, x + glyphWidth, mOptions.textSize); cb.visitGlyph(glyph, t, rectF); x += Math.floor(glyphWidth + (i >= 0 ? mOptions.charSpacing : 0)); } } public void draw(final Canvas canvas, float left, float top, final boolean allowAnimate, final boolean offscreenGlyphs) { mFont.canvas = offscreenGlyphs ? mOffsGlyphCanvas : canvas; int sc = canvas.save(); canvas.translate(left, top); layoutPass(new LayoutPassCallback() { @Override public void visitGlyph(Glyph glyph, float glyphAnimProgress, RectF rect) { int sc; if (glyphAnimProgress == 0) { glyph = mFont.mGlyphMap.get(glyph.getCanonicalStartGlyph()); } else if (!allowAnimate || glyphAnimProgress == 1) { glyph = mFont.mGlyphMap.get(glyph.getCanonicalEndGlyph()); glyphAnimProgress = 0; } if (offscreenGlyphs) { mOffsGlyphBitmap.eraseColor(Color.TRANSPARENT); sc = mOffsGlyphCanvas.save(); mOffsGlyphCanvas.translate( mOffsGlyphBitmapUnpaddedSize / 2, mOffsGlyphBitmapUnpaddedSize / 2); mOffsGlyphCanvas.scale( mOffsGlyphBitmapUnpaddedSize * 1f / Font.DRAWHEIGHT, mOffsGlyphBitmapUnpaddedSize * 1f / Font.DRAWHEIGHT); glyph.draw(glyphAnimProgress); mOffsGlyphCanvas.restoreToCount(sc); } if (DEBUG_SHOW_RECTS) { canvas.drawRect(rect, mDebugShowRectPaint); } sc = canvas.save(); canvas.translate(rect.left, rect.top); float scale = mOptions.textSize / (offscreenGlyphs ? mOffsGlyphBitmapUnpaddedSize : Font.DRAWHEIGHT); canvas.scale(scale, scale); if (offscreenGlyphs) { canvas.translate(-mOffsGlyphBitmapUnpaddedSize / 2, -mOffsGlyphBitmapUnpaddedSize / 2); canvas.drawBitmap(mOffsGlyphBitmap, 0, 0, mOffsGlyphPaint); } else { glyph.draw(glyphAnimProgress); // draws into mOffsGlyphCanvas } canvas.restoreToCount(sc); } }, allowAnimate, new RectF()); canvas.restoreToCount(sc); mFont.canvas = null; } private float getGlyphAnimProgress(int glyphIndex) { int indexIntoAnimatedGlyphs = -1; for (int i = 0; i < mAnimatedGlyphIndexCount; i++) { if (mAnimatedGlyphIndices[i] == glyphIndex) { indexIntoAnimatedGlyphs = i; break; } } if (indexIntoAnimatedGlyphs < 0) { return 0; // glyphs not currently animating are rendered at t=0 } // this glyph is animating float glyphStartAnimTime = 0; if (mAnimatedGlyphIndexCount > 1) { glyphStartAnimTime = interpolate(accelerate5( indexIntoAnimatedGlyphs * 1f / (mAnimatedGlyphIndexCount - 1)), 0, mAnimDuration - mOptions.glyphAnimDuration); } return progress(mAnimTime, glyphStartAnimTime, glyphStartAnimTime + mOptions.glyphAnimDuration); } String secondsString(Calendar c) { int s = c.get(Calendar.SECOND); return ":" + (s < 10 ? "0" : "") + s; } String hourMinString(Calendar c) { int h = c.get(mOptions.is24hour ? Calendar.HOUR_OF_DAY : Calendar.HOUR); if (!mOptions.is24hour && h == 0) { h = 12; } int m = c.get(Calendar.MINUTE); return (h < 10 ? " " : "") + h + ":" + (m < 10 ? "0" : "") + m; } private interface LayoutPassCallback { void visitGlyph(Glyph glyph, float glyphAnimProgress, RectF rect); } public static class Options { public float textSize; public boolean onlySeconds; public float charSpacing; public boolean is24hour; public int glyphAnimAverageDelay; public int glyphAnimDuration; public Options() { } public Options(Options copy) { this.textSize = copy.textSize; this.onlySeconds = copy.onlySeconds; this.charSpacing = copy.charSpacing; this.is24hour = copy.is24hour; this.glyphAnimAverageDelay = copy.glyphAnimAverageDelay; this.glyphAnimDuration = copy.glyphAnimDuration; } } public static class ClockPaints { public Paint fills[] = new Paint[3]; public Paint strokes[] = new Paint[3]; // optional public Paint date; public boolean hasStroke = false; } public interface Glyph { void draw(float t); float getWidthAtProgress(float t); String getCanonicalStartGlyph(); String getCanonicalEndGlyph(); } /** * Font data and common drawing operations. */ private class Font { private static final int DRAWHEIGHT = 144; private static final int COLOR_1 = 0; private static final int COLOR_2 = 1; private static final int COLOR_3 = 2; private Map<String, Glyph> mGlyphMap = new HashMap<>(); public Canvas canvas; private Path path = new Path(); private RectF tempRectF = new RectF(); public Font() { initGlyphs(); } public Glyph getGlyph(String key) { Glyph glyph = mGlyphMap.get(key); if (glyph == null) { return mGlyphMap.get("0_1"); } return glyph; } private void scaleUniform(float s, float px, float py) { canvas.scale(s, s, px, py); } /* API 21 compat methods */ private void arcTo(float l, float t, float r, float b, float startAngle, float sweepAngle, boolean forceMoveTo) { tempRectF.set(l, t, r, b); path.arcTo(tempRectF, startAngle, sweepAngle, forceMoveTo); } private void drawArc(float l, float t, float r, float b, float startAngle, float sweepAngle, boolean useCenter, Paint paint) { tempRectF.set(l, t, r, b); canvas.drawArc(tempRectF, startAngle, sweepAngle, useCenter, paint); } private void drawRoundRect(float l, float t, float r, float b, float rx, float ry, Paint paint) { tempRectF.set(l, t, r, b); canvas.drawRoundRect(tempRectF, rx, ry, paint); } private void drawOval(float l, float t, float r, float b, Paint paint) { tempRectF.set(l, t, r, b); canvas.drawOval(tempRectF, paint); } /* Stroke + fill drawing wrappers */ private void drawArc(float l, float t, float r, float b, float startAngle, float sweepAngle, boolean useCenter, int color) { drawArc(l, t, r, b, startAngle, sweepAngle, useCenter, mPaints.fills[color]); if (mPaints.hasStroke) { drawArc(l, t, r, b, startAngle, sweepAngle, useCenter, mPaints.strokes[color]); } } private void drawRoundRect(float l, float t, float r, float b, float rx, float ry, int color) { drawRoundRect(l, t, r, b, rx, ry, mPaints.fills[color]); if (mPaints.hasStroke) { drawRoundRect(l, t, r, b, rx, ry, mPaints.strokes[color]); } } private void drawOval(float l, float t, float r, float b, int color) { drawOval(l, t, r, b, mPaints.fills[color]); if (mPaints.hasStroke) { drawOval(l, t, r, b, mPaints.strokes[color]); } } private void drawRect(float l, float t, float r, float b, int color) { canvas.drawRect(l, t, r, b, mPaints.fills[color]); if (mPaints.hasStroke) { canvas.drawRect(l, t, r, b, mPaints.strokes[color]); } } private void drawPath(Path path, int color) { canvas.drawPath(path, mPaints.fills[color]); if (mPaints.hasStroke) { canvas.drawPath(path, mPaints.strokes[color]); } } private void initGlyphs() { mGlyphMap.put("0_1", new Glyph() { @Override public String getCanonicalStartGlyph() { return "0"; } @Override public String getCanonicalEndGlyph() { return "1"; } @Override public void draw(float t) { float d1 = decelerate5(progress(t, 0, 0.5f)); float d2 = decelerate5(progress(t, 0.5f, 1)); // 0 canvas.save(); // temporarily make space for the squashed zero canvas.translate(interpolate(d1, 0, interpolate(d2, 24, 0)), 0); scaleUniform(interpolate(d1, 1, 2f / 3), 72, 144); scaleUniform(interpolate(d2, 1, 0.7f), 72, 96); canvas.rotate(interpolate(d1, 45, 0), 72, 72); float stretchX = interpolate(d1, 0, interpolate(d2, 72, -36)); path.reset(); path.moveTo(72 - stretchX, 144); arcTo(-stretchX, 0, 144 - stretchX, 144, 90, 180, true); path.lineTo(72 + stretchX, 0); path.lineTo(72 + stretchX, 144); path.lineTo(72 - stretchX, 144); path.close(); drawPath(path, COLOR_2); path.reset(); arcTo(stretchX, 0, 144 + stretchX, 144, -90, 180, true); path.close(); drawPath(path, COLOR_3); canvas.restore(); // 1 if (d2 > 0) { drawRect( interpolate(d2, 28, 0), interpolate(d2, 72, 0), 100, interpolate(d2, 144, 48), COLOR_2); drawRect(28, interpolate(d2, 144, 48), 100, 144, COLOR_3); } } @Override public float getWidthAtProgress(float t) { return interpolate( decelerate5(progress(t, 0.5f, 1)), interpolate(decelerate5(progress(t, 0, 0.5f)), 144, 192), 100); } }); mGlyphMap.put("1_2", new Glyph() { @Override public String getCanonicalStartGlyph() { return "1"; } @Override public String getCanonicalEndGlyph() { return "2"; } @Override public void draw(float t) { float d = 1 - decelerate5(progress(t, 0, 0.5f)); float d1 = decelerate5(progress(t, 0.3f, 0.8f)); float d2 = decelerate5(progress(t, 0.5f, 1.0f)); // 2 if (d1 > 0) { canvas.save(); canvas.translate(interpolate(d2, 72, 0), 0); path.reset(); path.moveTo(0, 144); path.lineTo(72, 72); path.lineTo(72, 144); path.lineTo(0, 144); drawPath(path, COLOR_3); canvas.restore(); canvas.save(); // TODO: interpolate colors //ctx.fillStyle = interpolateColors(d2, o.color2, o.color1); canvas.translate(108, interpolate(d1, 72, 0)); //drawHorzHalfCircle(0, 0, 36, 72, true); drawArc(-36, 0, 36, 72, -90, 180, true, COLOR_1); canvas.restore(); canvas.save(); canvas.translate(0, interpolate(d1, 72, 0)); drawRect(interpolate(d2, 72, 8), 0, interpolate(d2, 144, 108), 72, COLOR_1); canvas.restore(); drawRect(72, 72, 144, 144, COLOR_2); } // 1 if (d > 0) { canvas.save(); canvas.translate(interpolate(d, 44, 0), 0); drawRect(interpolate(d, 28, 0), interpolate(d, 72, 0), 100, interpolate(d, 144, 48), COLOR_2); drawRect(28, interpolate(d, 144, 48), 100, 144, COLOR_3); canvas.restore(); } } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0f, 0.5f)), 100, 144); } }); mGlyphMap.put("2_3", new Glyph() { @Override public String getCanonicalStartGlyph() { return "2"; } @Override public String getCanonicalEndGlyph() { return "3"; } @Override public void draw(float t) { float d = decelerate5(progress(t, 0, 0.5f)); float d1 = decelerate5(progress(t, 0.5f, 1.0f)); // 2 if (d < 1) { canvas.save(); canvas.translate(interpolate(d, 0, -16), 0); canvas.save(); canvas.translate(interpolate(d, 0, 72), 0); path.reset(); path.moveTo(0, 144); path.lineTo(72, 72); path.lineTo(72, 144); path.lineTo(0, 144); drawPath(path, COLOR_3); canvas.restore(); // TODO: interpolateColors //.fillStyle = interpolateColors(d, o.color1, o.color2); if (d == 0) { path.reset(); path.moveTo(8, 0); path.lineTo(108, 0); arcTo(108 - 36, 0, 108 + 36, 72, -90, 180, true); path.lineTo(108, 72); path.lineTo(8, 72); path.lineTo(8, 0); path.close(); drawPath(path, COLOR_1); } else { drawArc(108 - 36, interpolate(d, 0, 72), 108 + 36, 72 + interpolate(d, 0, 72), -90, 180, true, COLOR_1); drawRect(interpolate(d, 8, 72), interpolate(d, 0, 72), interpolate(d, 108, 144), interpolate(d, 72, 144), COLOR_1); } drawRect(72, 72, 144, 144, COLOR_2); canvas.restore(); } else { // 3 // half-circle canvas.save(); scaleUniform(interpolate(d1, 0.7f, 1), 128, 144); drawArc(32, 48, 128, 144, -90, 180, true, COLOR_3); canvas.restore(); // bottom rectangle drawRect( interpolate(d1, 56, 0), interpolate(d1, 72, 96), interpolate(d1, 128, 80), interpolate(d1, 144, 144), COLOR_1); // top part with triangle canvas.save(); canvas.translate(0, interpolate(d1, 72, 0)); path.reset(); path.moveTo(128, 0); path.lineTo(80, 48); path.lineTo(80, 0); path.close(); drawPath(path, COLOR_3); drawRect( interpolate(d1, 56, 0), 0, interpolate(d1, 128, 80), interpolate(d1, 72, 48), COLOR_3); canvas.restore(); // middle rectangle canvas.save(); drawRect( interpolate(d1, 56, 32), interpolate(d1, 72, 48), interpolate(d1, 128, 80), interpolate(d1, 144, 96), COLOR_2); canvas.restore(); } } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0f, 0.5f)), 144, 128); } }); mGlyphMap.put("3_4", new Glyph() { @Override public String getCanonicalStartGlyph() { return "3"; } @Override public String getCanonicalEndGlyph() { return "4"; } @Override public void draw(float t) { float d1 = 1 - decelerate5(progress(t, 0, 0.5f)); float d2 = decelerate5(progress(t, 0.5f, 1)); // 3 if (d1 > 0) { canvas.save(); canvas.translate(interpolate(d1, 16, 0), 0); // middle rectangle canvas.save(); drawRect( interpolate(d1, 56, 32), interpolate(d1, 72, 48), interpolate(d1, 128, 80), interpolate(d1, 144, 96), COLOR_2); canvas.restore(); // half-circle canvas.save(); scaleUniform(interpolate(d1, 0.7f, 1), 128, 144); drawArc(32, 48, 128, 144, -90, 180, true, COLOR_3); canvas.restore(); // bottom rectangle drawRect( interpolate(d1, 56, 0), interpolate(d1, 72, 96), interpolate(d1, 128, 80), interpolate(d1, 144, 144), COLOR_1); // top part with triangle canvas.save(); canvas.translate(0, interpolate(d1, 72, 0)); path.reset(); path.moveTo(80, 0); path.lineTo(128, 0); path.lineTo(80, 48); if (d1 == 1) { path.lineTo(0, 48); path.lineTo(0, 0); path.lineTo(80, 0); path.close(); drawPath(path, COLOR_3); } else { path.close(); drawPath(path, COLOR_3); drawRect( interpolate(d1, 56, 0), 0, interpolate(d1, 128, 80), interpolate(d1, 72, 48), COLOR_3); } canvas.restore(); canvas.restore(); } else { // 4 // bottom rectangle drawRect(72, interpolate(d2, 144, 108), 144, 144, COLOR_2); // middle rectangle drawRect(interpolate(d2, 72, 0), interpolate(d2, 144, 72), 144, interpolate(d2, 144, 108), COLOR_1); // triangle canvas.save(); scaleUniform(d2, 144, 144); path.reset(); path.moveTo(72, 72); path.lineTo(72, 0); path.lineTo(0, 72); path.lineTo(72, 72); drawPath(path, COLOR_2); canvas.restore(); // top rectangle drawRect(72, interpolate(d2, 72, 0), 144, interpolate(d2, 144, 72), COLOR_3); } } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0.5f, 1f)), 128, 144); } }); mGlyphMap.put("4_5", new Glyph() { @Override public String getCanonicalStartGlyph() { return "4"; } @Override public String getCanonicalEndGlyph() { return "5"; } @Override public void draw(float t) { float d = decelerate5(progress(t, 0, 0.5f)); float d1 = decelerate5(progress(t, 0.5f, 1)); // 4 if (d < 1) { // bottom rectangle drawRect(interpolate(d, 72, 0), 108, interpolate(d, 144, 72), 144, COLOR_2); // top rectangle drawRect(interpolate(d, 72, 0), interpolate(d, 0, 72), interpolate(d, 144, 72), interpolate(d, 72, 144), COLOR_3); // triangle canvas.save(); scaleUniform(1 - d, 0, 144); path.reset(); path.moveTo(72, 72); path.lineTo(72, 0); path.lineTo(0, 72); path.lineTo(72, 72); drawPath(path, COLOR_2); canvas.restore(); // middle rectangle drawRect(0, 72, interpolate(d, 144, 72), interpolate(d, 108, 144), COLOR_1); } else { // 5 // wing rectangle canvas.save(); drawRect( 80, interpolate(d1, 72, 0), interpolate(d1, 80, 128), interpolate(d1, 144, 48), COLOR_2); canvas.restore(); // half-circle canvas.save(); scaleUniform(interpolate(d1, 0.75f, 1), 0, 144); canvas.translate(interpolate(d1, -48, 0), 0); drawArc(32, 48, 128, 144, -90, 180, true, COLOR_3); canvas.restore(); // bottom rectangle drawRect(0, 96, 80, 144, COLOR_2); // middle rectangle drawRect( 0, interpolate(d1, 72, 0), 80, interpolate(d1, 144, 96), COLOR_1); } } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0f, 0.5f)), 144, 128); } }); mGlyphMap.put("5_6", new Glyph() { @Override public String getCanonicalStartGlyph() { return "5"; } @Override public String getCanonicalEndGlyph() { return "6"; } @Override public void draw(float t) { float d = decelerate5(progress(t, 0, 0.7f)); float d1 = decelerate5(progress(t, 0.1f, 1)); // 5 (except half-circle) if (d < 1) { canvas.save(); scaleUniform(interpolate(d, 1, 0.25f), 108, 96); // wing rectangle drawRect(80, 0, 128, 48, COLOR_2); // bottom rectangle drawRect(0, 96, 80, 144, COLOR_2); // middle rectangle drawRect(0, 0, 80, 96, COLOR_1); canvas.restore(); } // half-circle canvas.save(); canvas.rotate(interpolate(d1, 0, 90), 72, 72); if (d1 == 0) { drawArc( 32, 48, 128, 144, -90, 180, true, COLOR_3); } else { scaleUniform(interpolate(d1, 2f / 3, 1), 80, 144); canvas.translate(interpolate(d1, 8, 0), 0); drawArc( 0, 0, 144, 144, -90, 180, true, COLOR_3); } // 6 (just the parallelogram) if (d1 > 0) { canvas.save(); canvas.rotate(-90, 72, 72); path.reset(); path.moveTo(0, 72); path.lineTo(interpolate(d1, 0, 36), interpolate(d1, 72, 0)); path.lineTo(interpolate(d1, 72, 108), interpolate(d1, 72, 0)); path.lineTo(72, 72); path.lineTo(0, 72); drawPath(path, COLOR_2); canvas.restore(); } canvas.restore(); } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0.1f, 1f)), 128, 144); } }); mGlyphMap.put("6_7", new Glyph() { @Override public String getCanonicalStartGlyph() { return "6"; } @Override public String getCanonicalEndGlyph() { return "7"; } @Override public void draw(float t) { float d = decelerate5(t); // 7 rectangle drawRect(interpolate(d, 72, 0), 0, 72, 72, COLOR_3); // 6 circle canvas.save(); canvas.translate(interpolate(d, 0, 36), 0); if (d < 1) { drawArc(0, 0, 144, 144, interpolate(d, 180, -64f), -180, true, COLOR_3); } // parallelogram path.reset(); path.moveTo(36, 0); path.lineTo(108, 0); path.lineTo(interpolate(d, 72, 36), interpolate(d, 72, 144)); path.lineTo(interpolate(d, 0, -36), interpolate(d, 72, 144)); path.close(); drawPath(path, COLOR_2); canvas.restore(); } @Override public float getWidthAtProgress(float t) { return 144; } }); mGlyphMap.put("7_8", new Glyph() { @Override public String getCanonicalStartGlyph() { return "7"; } @Override public String getCanonicalEndGlyph() { return "8"; } @Override public void draw(float t) { float d = decelerate5(progress(t, 0, 0.5f)); float d1 = decelerate5(progress(t, 0.2f, 0.5f)); float d2 = decelerate5(progress(t, 0.5f, 1)); // 8 if (d1 > 0) { if (d2 > 0) { // top canvas.save(); canvas.translate(0, interpolate(d2, 96, 0)); drawRoundRect(24, 0, 120, 48, 24, 24, COLOR_3); canvas.restore(); } // left bottom canvas.save(); canvas.translate(interpolate(d1, 24, 0), 0); scaleUniform(interpolate(d2, 0.5f, 1), 48, 144); drawArc(0, 48, 96, 144, 90, 180, true, COLOR_1); canvas.restore(); // right bottom canvas.save(); canvas.translate(interpolate(d1, -24, 0), 0); scaleUniform(interpolate(d2, 0.5f, 1), 96, 144); drawArc(48, 48, 144, 144, -90, 180, true, COLOR_2); canvas.restore(); // bottom middle canvas.save(); canvas.scale(interpolate(d1, 0, 1), 1, 72, 0); drawRect(48, interpolate(d2, 96, 48), 96, 144, COLOR_1); drawRect(interpolate(d2, 48, 96), interpolate(d2, 96, 48), 96, 144, COLOR_2); canvas.restore(); } if (d < 1) { // 7 rectangle drawRect( interpolate(d, 0, 48), interpolate(d, 0, 96), interpolate(d, 72, 96), interpolate(d, 72, 144), COLOR_3); // 7 parallelogram path.reset(); path.moveTo(interpolate(d, 72, 48), interpolate(d, 0, 96)); path.lineTo(interpolate(d, 144, 96), interpolate(d, 0, 96)); path.lineTo(interpolate(d, 72, 96), 144); path.lineTo(interpolate(d, 0, 48), 144); path.close(); drawPath(path, COLOR_2); } } @Override public float getWidthAtProgress(float t) { return 144; } }); mGlyphMap.put("8_9", new Glyph() { @Override public String getCanonicalStartGlyph() { return "8"; } @Override public String getCanonicalEndGlyph() { return "9"; } @Override public void draw(float t) { float d = decelerate5(progress(t, 0, 0.5f)); float d1 = decelerate5(progress(t, 0.5f, 1)); // 8 if (d < 1) { // top canvas.save(); canvas.translate(0, interpolate(d, 0, 48)); drawRoundRect(24, 0, 120, 48, 24, 24, COLOR_3); canvas.restore(); if (d == 0) { // left + middle bottom canvas.save(); path.reset(); path.moveTo(48, 48); path.lineTo(96, 48); path.lineTo(96, 144); path.lineTo(48, 144); arcTo(0, 48, 96, 144, 90, 180, true); drawPath(path, COLOR_1); canvas.restore(); // right bottom drawArc(48, 48, 144, 144, -90, 180, true, COLOR_2); } else { // bottom middle drawRect(interpolate(d, 48, 72) - 2, interpolate(d, 48, 0), interpolate(d, 96, 72) + 2, 144, COLOR_1); // left bottom canvas.save(); scaleUniform(interpolate(d, 2f/3, 1), 0, 144); drawArc(0, 0, 144, 144, 90, 180, true, COLOR_1); canvas.restore(); // right bottom canvas.save(); scaleUniform(interpolate(d, 2f/3, 1), 144, 144); drawArc(0, 0, 144, 144, -90, 180, true, COLOR_2); canvas.restore(); } } else { // 9 canvas.save(); canvas.rotate(interpolate(d1, -90, -180), 72, 72); // parallelogram path.reset(); path.moveTo(0, 72); path.lineTo(interpolate(d1, 0, 36), interpolate(d1, 72, 0)); path.lineTo(interpolate(d1, 72, 108), interpolate(d1, 72, 0)); path.lineTo(72, 72); path.lineTo(0, 72); drawPath(path, COLOR_3); // vanishing arc drawArc(0, 0, 144, 144, -180, interpolate(d1, 180, 0), true, COLOR_1); // primary arc drawArc(0, 0, 144, 144, 0, 180, true, COLOR_2); canvas.restore(); } } @Override public float getWidthAtProgress(float t) { return 144; } }); mGlyphMap.put("9_0", new Glyph() { @Override public String getCanonicalStartGlyph() { return "9"; } @Override public String getCanonicalEndGlyph() { return "0"; } @Override public void draw(float t) { float d = decelerate5(t); // 9 canvas.save(); canvas.rotate(interpolate(d, -180, -225), 72, 72); // parallelogram canvas.save(); path.reset(); path.moveTo(0, 72); path.lineTo(interpolate(d, 36, 0), interpolate(d, 0, 72)); path.lineTo(interpolate(d, 108, 72), interpolate(d, 0, 72)); path.lineTo(72, 72); path.lineTo(0, 72); drawPath(path, COLOR_3); canvas.restore(); // TODO: interpolate colors //ctx.fillStyle = interpolateColors(d, COLOR_1, COLOR_3); drawArc(0, 0, 144, 144, 0, interpolate(d, 0, -180), true, COLOR_3); drawArc(0, 0, 144, 144, 0, 180, true, COLOR_2); canvas.restore(); } @Override public float getWidthAtProgress(float t) { return 144; } }); mGlyphMap.put(" _1", new Glyph() { @Override public String getCanonicalStartGlyph() { return " "; } @Override public String getCanonicalEndGlyph() { return "1"; } @Override public void draw(float t) { float d1 = decelerate5(progress(t, 0, 0.5f)); float d2 = decelerate5(progress(t, 0.5f, 1)); // 1 scaleUniform(interpolate(d1, 0, 1), 0, 144); drawRect( interpolate(d2, 28, 0), interpolate(d2, 72, 0), 100, interpolate(d2, 144, 48), COLOR_2); if (d2 > 0) { drawRect(28, interpolate(d2, 144, 48), 100, 144, COLOR_3); } } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0, 0.5f)), 0, 100); } }); mGlyphMap.put("1_ ", new Glyph() { @Override public String getCanonicalStartGlyph() { return "1"; } @Override public String getCanonicalEndGlyph() { return " "; } @Override public void draw(float t) { float d1 = decelerate5(progress(t, 0, 0.5f)); float d2 = decelerate5(progress(t, 0.5f, 1)); scaleUniform(interpolate(d2, 1, 0), 0, 144); drawRect( interpolate(d1, 0, 28), interpolate(d1, 0, 72), 100, interpolate(d1, 48, 144), COLOR_2); if (d1 < 1) { drawRect(28, interpolate(d1, 48, 144), 100, 144, COLOR_3); } } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0.5f, 1)), 100, 0); } }); // 24 hour only mGlyphMap.put("2_ ", new Glyph() { @Override public String getCanonicalStartGlyph() { return "2"; } @Override public String getCanonicalEndGlyph() { return " "; } @Override public void draw(float t) { float d = decelerate5(progress(t, 0, 0.5f)); float d1 = decelerate5(progress(t, 0.5f, 1.0f)); // 2 canvas.save(); canvas.translate(interpolate(d, 0, -72), 0); if (d < 1) { canvas.save(); canvas.translate(interpolate(d, 0, 72), 0); path.reset(); path.moveTo(0, 144); path.lineTo(72, 72); path.lineTo(72, 144); path.lineTo(0, 144); drawPath(path, COLOR_3); canvas.restore(); canvas.save(); canvas.translate(0, interpolate(d, 0, 72)); canvas.translate(108, 0); drawArc(-36, 0, 36, 72, -90, 180, true, COLOR_1); canvas.restore(); canvas.save(); drawRect(interpolate(d, 8, 72), interpolate(d, 0, 72), interpolate(d, 108, 144), interpolate(d, 72, 144), COLOR_1); canvas.restore(); } canvas.save(); scaleUniform(interpolate(d1, 1, 0), 72, 144); drawRect(72, 72, 144, 144, COLOR_2); canvas.restore(); canvas.restore(); } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0, 0.5f)), 144, interpolate(decelerate5(progress(t, 0.5f, 1)), 72, 0)); } }); // 24 hour only mGlyphMap.put("3_0", new Glyph() { @Override public String getCanonicalStartGlyph() { return "3"; } @Override public String getCanonicalEndGlyph() { return "0"; } @Override public void draw(float t) { float d1 = 1 - decelerate5(progress(t, 0, 0.5f)); float d2 = decelerate5(progress(t, 0.5f, 1)); canvas.save(); canvas.rotate(interpolate(d2, 0, 45), 72, 72); canvas.translate(interpolate(d1, interpolate(d2, 16, -8), 0), 0); if (d1 > 0) { // top part of 3 with triangle canvas.save(); canvas.translate(0, interpolate(d1, 48, 0)); float x = interpolate(d1, 48, 0); path.reset(); path.moveTo(128 - x, 0); path.lineTo(80 - x, 48); path.lineTo(80 - x, 0); drawPath(path, COLOR_3); drawRect(interpolate(d1, 32, 0), 0, 80, 48, COLOR_3); canvas.restore(); } // bottom rectangle in 3 drawRect( interpolate(d1, interpolate(d2, 32, 80), 0), 96, 80, 144, COLOR_1); // middle rectangle drawRect( interpolate(d2, 32, 80), 48, 80, 96, COLOR_2); // 0 scaleUniform(interpolate(d2, 2f/3, 1), 80, 144); // half-circles canvas.translate(8, 0); if (d2 > 0) { canvas.save(); canvas.rotate(interpolate(d2, -180, 0), 72, 72); drawArc( 0, 0, 144, 144, 90, 180, true, COLOR_2); canvas.restore(); } drawArc( 0, 0, 144, 144, -90, 180, true, COLOR_3); canvas.restore(); } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0, 0.5f)), 128, 144); } }); mGlyphMap.put("5_0", new Glyph() { @Override public String getCanonicalStartGlyph() { return "5"; } @Override public String getCanonicalEndGlyph() { return "0"; } @Override public void draw(float t) { float d = decelerate5(progress(t, 0, 0.5f)); float d1 = decelerate5(progress(t, 0.5f, 1)); canvas.save(); canvas.rotate(interpolate(d1, 0, 45), 72, 72); // 5 (except half-circle) if (d < 1) { // wing rectangle canvas.save(); drawRect( 80, interpolate(d, 0, 48), interpolate(d, 128, 80), interpolate(d, 48, 144), COLOR_2); canvas.restore(); // bottom rectangle drawRect(0, 96, 80, 144, COLOR_2); } // middle rectangle drawRect( interpolate(d1, 0, 80), interpolate(d, 0, interpolate(d1, 48, 0)), 80, interpolate(d, 96, 144), COLOR_1); scaleUniform(interpolate(d1, 2f/3, 1), 80, 144); // half-circles if (d1 > 0) { canvas.save(); canvas.rotate(interpolate(d1, -180, 0), 72, 72); drawArc( 0, 0, 144, 144, 90, 180, true, COLOR_2); canvas.restore(); } canvas.translate(interpolate(d1, 8, 0), 0); drawArc( 0, 0, 144, 144, -90, 180, true, COLOR_3); canvas.restore(); } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0, 0.5f)), 128, 144); } }); mGlyphMap.put("2_1", new Glyph() { @Override public String getCanonicalStartGlyph() { return "2"; } @Override public String getCanonicalEndGlyph() { return "1"; } @Override public void draw(float t) { float d = decelerate5(progress(t, 0, 0.5f)); float d1 = decelerate5(progress(t, 0.2f, 0.5f)); float d2 = decelerate5(progress(t, 0.5f, 1)); // 2 if (d1 < 1) { canvas.save(); canvas.translate(interpolate(d, 0, 28), 0); path.reset(); path.moveTo(0, 144); path.lineTo(72, 72); path.lineTo(72, 144); path.lineTo(0, 144); drawPath(path, COLOR_3); canvas.restore(); canvas.save(); // TODO: interpolate colors //ctx.fillStyle = interpolateColors(d1, COLOR_1, COLOR_2); canvas.translate(interpolate(d, 108, 64), interpolate(d1, 0, 72)); drawArc(-36, 0, 36, 72, -90, 180, true, COLOR_1); canvas.restore(); canvas.save(); canvas.translate(0, interpolate(d1, 0, 72)); drawRect(interpolate(d, 8, 28), 0, interpolate(d, 108, 100), 72, COLOR_1); canvas.restore(); canvas.save(); canvas.translate(interpolate(d, 0, -44), 0); drawRect(72, 72, 144, 144, COLOR_2); canvas.restore(); } else { // 1 canvas.save(); drawRect(interpolate(d2, 28, 0), interpolate(d2, 72, 0), 100, interpolate(d2, 144, 48), COLOR_2); drawRect(28, interpolate(d2, 144, 48), 100, 144, COLOR_3); canvas.restore(); } } @Override public float getWidthAtProgress(float t) { return interpolate(decelerate5(progress(t, 0, 0.5f)), 144, 100); } }); mGlyphMap.put(":", new Glyph() { @Override public String getCanonicalStartGlyph() { return ":"; } @Override public String getCanonicalEndGlyph() { return ":"; } @Override public void draw(float t) { drawOval(0, 0, 48, 48, COLOR_2); drawOval(0, 96, 48, 144, COLOR_3); } @Override public float getWidthAtProgress(float t) { return 48; } }); mGlyphMap.put(" ", new Glyph() { @Override public String getCanonicalStartGlyph() { return " "; } @Override public String getCanonicalEndGlyph() { return " "; } @Override public void draw(float t) { } @Override public float getWidthAtProgress(float t) { return 0; } }); mGlyphMap.put("0", mGlyphMap.get("0_1")); mGlyphMap.put("1", mGlyphMap.get("1_2")); mGlyphMap.put("2", mGlyphMap.get("2_3")); mGlyphMap.put("3", mGlyphMap.get("3_4")); mGlyphMap.put("4", mGlyphMap.get("4_5")); mGlyphMap.put("5", mGlyphMap.get("5_6")); mGlyphMap.put("6", mGlyphMap.get("6_7")); mGlyphMap.put("7", mGlyphMap.get("7_8")); mGlyphMap.put("8", mGlyphMap.get("8_9")); mGlyphMap.put("9", mGlyphMap.get("9_0")); } } }