HW04 - james-bern/CS136 GitHub Wiki
A flipbook is a series of drawings that are shown one after the other to give the illusion of movement.
- Optional Reading: Animator's Survival Kit
In this homework, we'll make a simple flipbook animation program.
- Fun, historical animated gifs: https://reedart.wordpress.com/2013/08/19/animated-gifs-illustrating-traditional-animation-techniques/
Take half an hour or so to experience some simple animation software:
- Make a simple animation in Brush Ninja
- NOTE: You can press
O
to toggle onion skinning
- NOTE: You can press
- Watch a video about DigiCel's FlipBook
We know we need to store a lot of data, but we don't know how much. How many drawings? How many strokes per drawing? How many points per stroke?
I guess we could just make some REALLY BIG arrays, but that seems...potentially sus.
Maybe instead we can try using arrays that grow (and shrink).
Enter, the array list!
- Mirroring the point
$(x, y)$ about the line$x = a$ gives$(-(x - a) + a, y) = (2a - x, y)$ - Mirroring the point
$(x, y)$ about the line$y = b$ gives$(x, -(y - b) + b) = (x, 2b - y)$
- Java's
ArrayList<ElementType>
is generic, which means you get to choose what typeElementType
is.-
ArrayList<Point> stroke = new ArrayList<>();
is a list of points, storing a single stroke. -
ArrayList<ArrayList<Point>> page = new ArrayList<>();
is a list of lists of points, aka a list of strokes, aka a page (of the flipbook) -
ArrayList<ArrayList<ArrayList<Point>>> flipbook = new ArrayList<>();
is a list of lists of lists of points, aka a list of lists of strokes, aka a list of pages, aka a flipbook - ...
-
- My
Point
class is super simple-
Point point = new Point(x, y);
makes a new point with coordinates$(x, y)$ -
point.x
,point.y
are the point's$x$ and$y$ coordinates
-
-
flipbook.size()
is1
-
currentFrame
is0
- the flipbook consists of...
- a single blank page
| V 0 [ ]
- the user draws the letter 'A'
| V 0 [A]
- the user presses
S
| V 0 1 [A] [ ]
-
flipbook.size()
is4
-
currentFrame
is2
- there flipbook consists of...
- page with drawing of the letter
A
on page 0 - page with drawing of the letter
B
on page 1 - page with drawing of the letter
C
on page 2 - page with drawing of the letter
E
on page 3
| V [A] [B] [C] [E]
- page with drawing of the letter
- the user presses
S
("save"), which inserts a blank page and takes us to it-
flipbook.size()
is5
-
currentFrame
is3
| V [A] [B] [C] [ ] [E]
-
- the user draws a
D
| V [A] [B] [C] [D] [E]
- the user presses
.
("next page")| V [A] [B] [C] [D] [E]
- the user presses
.
- NOTE: the flipbook is treated as circular (it "wraps around")
| V [A] [B] [C] [D] [E]
- the user presses
,
("previous page")| V [A] [B] [C] [D] [E]
YouTube Link: https://youtu.be/s7KWD2pG-Qk
-
A-
- Update Cow
- Draw the drawing on the current page in BLACK
- Click and drag to draw strokes
-
HINT: You should create a new stroke when
mousePressed
is true -
HINT: You should add points to the stroke when
mouseHeld
is true
-
HINT: You should create a new stroke when
- Press
S
to insert a new blank page of immediately after the current page, and go to it- NOTE (ABCE correct behavior): https://youtu.be/ArXLMKMIPIk
- Press
.
(period) to go to the next page-
NOTE: Treat the animation as circular
-
HINT: You can use
MODULO(x, y)
, which is likex % y
but works for negativex
-
HINT: You can use
-
NOTE: Treat the animation as circular
- Press
,
(comma) to go to the previous page- NOTE: Again, treat the animation as circular
- Press
P
to play (or pause, if the animation is currently playing)-
HINT: You will want something like a
boolean paused;
variable -
NOTE: Like the Reference, your animation should play slowly enough that you can actually see what's going on!
- HINT: Use a frame or time variable so that you only "flip the page" about once every three Cow frames
-
NOTE: You may NOT use
Thread.sleep(...);
or similar; we want the app to remain responsive
-
HINT: You will want something like a
- Call
HW04_drawTimeline(int numPagesTotal, int currentPageIndex);
to draw the timeline (you will have to fill in the arguments yourself) - Copy over your code that draws the pause / play icons (⏯️) from HW01
-
A
- Press
X
to mirror the drawing on the current page (NOT all pages) horizontally about the line$x = 128.0$ - Press
Y
to mirror the drawing on the current page (NOT all pages) vertically about the line$y = 128.0$ - Press
O
to toggle drawing basic, two-frame onion skinning.- Draw the page two pages before the current page in YELLOW
-
HINT:
MODULO(..., flipbook.size())
-
HINT:
- Draw the page one page before the current page in ORANGE
- Draw the page one page after the current page in BLUE
- Draw the page two pages after the current page in CYAN
- NOTE: For all of the above, make sure your code does something reasonable when there are fewer than 5 pages
- NOTE: Treat the animation as circular
- NOTE: You may NOT repeat a bunch of code
- Draw some little symbol in the lower left to indicate if onion skinning is toggled on
- Draw the page two pages before the current page in YELLOW
- Press
-
A+
- Implement an eraser tool (that deletes whatever stroke you click closest to)
- "Mini map" timeline with tiny versions of each page so you can see and overview the whole animation at once
- Inspo: https://github.com/user-attachments/assets/529a629d-83c8-4ec7-8b61-e3eec689957f
-
A++ (105)
- Upgrade the mini map timeline; inspo:
- Scroll (two finger drag on laptop) to scroll through the timeline
-
NOTE: Hot new Cow.java feature --
double mouseScrollAmount;
-
NOTE: Hot new Cow.java feature --
- Click to select current page
- Click and drag to rearrange pages
- Scroll (two finger drag on laptop) to scroll through the timeline
- Upgrade the mini map timeline; inspo:
import java.util.ArrayList;
class HW04 extends Cow {
static class Point {
double x;
double y;
Point(double x, double y) {
this.x = x;
this.y = y;
}
};
public static void main(String[] arguments) {
// TODO
canvasConfig(0.0, 0.0, 256.0, 256.0);
while (beginFrame()) {
// TODO
// HW04_drawTimeline(numPagesTotal, currentPageIndex);
}
}
}
Where do I start?
You'll definitely want to build up this app step by step. I would personally start by making it so the user can draw a single stroke (a list of points).
Here is some code that shows you how to work with Point
, drawLine(...)
, and ArrayList
.
You will need to change it a lot (there will be a for
loop for sure.)
public static void main(String[] arguments) {
ArrayList<Point> foo = new ArrayList<Point>();
foo.add(new Point(0.0, 0.0));
foo.add(new Point(64.0, 128.0));
while (beginFrame()) {
drawLine(foo.get(0).x, foo.get(0).y, foo.get(1).x, foo.get(1).y);
drawLine(foo.get(1).x, foo.get(1).y, mouseX, mouseY);
}
}
How should I structure my code? What functions should I add?
I did all of the A until onion skinning in main()
; then I added function something like this:
static void drawPage(ArrayList<ArrayList<ArrayList<Point>>> flipbook, int pageIndex, Color color);
How do I access the last element in a list? (NOTE: It's possible to solve the homework without doing this, but you may possibly find it useful.)
list.get(list.size() - 1)
👀
import java.util.ArrayList;
class HW04A extends Cow {
static class Point {
double x;
double y;
Point(double x, double y) {
this.x = x;
this.y = y;
}
};
static void drawPage(ArrayList<ArrayList<ArrayList<Point>>> flipbook, int pageIndex, Color color) {
for (int strokeIndex = 0; strokeIndex < flipbook.get(pageIndex).size(); ++strokeIndex) {
ArrayList<Point> stroke = flipbook.get(pageIndex).get(strokeIndex);
for (int i = 0; i < stroke.size() - 1; ++i) {
drawLine(stroke.get(i).x, stroke.get(i).y, stroke.get(i + 1).x, stroke.get(i + 1).y, color);
}
}
}
public static void main(String[] arguments) {
ArrayList<ArrayList<ArrayList<Point>>> flipbook = new ArrayList<>();
flipbook.add(new ArrayList<>());
int currentPageIndex = 0;
double timeSinceLastAutomaticFlip = 0.0;
canvasConfig(0.0, 0.0, 256.0, 256.0);
while (beginFrame()) {
boolean requestAutomaticFlip = false;
if (keyToggled('P')) {
timeSinceLastAutomaticFlip += 0.0167;
if (timeSinceLastAutomaticFlip > 1.0 / 16.0) {
timeSinceLastAutomaticFlip = 0.0;
requestAutomaticFlip = true;
}
} else {
timeSinceLastAutomaticFlip = 0.0;
}
if (!mouseHeld) {
if (keyPressed('.') || requestAutomaticFlip) {
currentPageIndex = (currentPageIndex + 1) % flipbook.size();
}
if (keyPressed(',')) {
currentPageIndex = Math.floorMod(currentPageIndex - 1, flipbook.size());
}
if (keyPressed('S')) {
flipbook.add(++currentPageIndex, new ArrayList<>());
}
}
// x and y flip
for (int strokeIndex = 0; strokeIndex < flipbook.get(currentPageIndex).size(); ++strokeIndex) {
ArrayList<Point> stroke = flipbook.get(currentPageIndex).get(strokeIndex);
for (int i = 0; i < stroke.size(); ++i) {
Point point = stroke.get(i);
if (keyPressed('X')) point.x = 256.0 - point.x;
if (keyPressed('Y')) point.y = -(point.y - 128.0) + 128.0;
point.y -= mouseScrollAmount;
}
}
// create new stroke
if (mousePressed) {
flipbook.get(currentPageIndex).add(new ArrayList<>());
}
// add point to stroke
if (mouseHeld) {
ArrayList<Point> activeStroke = flipbook.get(currentPageIndex).get(flipbook.get(currentPageIndex).size() - 1);
activeStroke.add(new Point(mouseX, mouseY));
}
// // draw
// play / pause symbols
if (keyToggled('P')) {
drawTriangle(10.0, 10.0, 10.0, 20.0, 20.0, 15.0, GREEN);
} else {
drawRectangle(10.0, 10.0, 14.0, 20.0, GREEN);
drawRectangle(16.0, 10.0, 20.0, 20.0, GREEN);
}
// onion symbol
if (keyToggled('O')) {
drawCircle(32.0, 15.0, 5.0, YELLOW);
drawCircle(36.0, 15.0, 5.0, ORANGE);
drawCircle(40.0, 15.0, 5.0, BLACK);
drawCircle(44.0, 15.0, 5.0, BLUE);
drawCircle(48.0, 15.0, 5.0, CYAN);
}
// onion skin
if (keyToggled('O')) {
if (flipbook.size() >= 4) { drawPage(flipbook, MODULO(currentPageIndex - 2, flipbook.size()), YELLOW); }
if (flipbook.size() >= 2) { drawPage(flipbook, MODULO(currentPageIndex - 1, flipbook.size()), ORANGE); }
if (flipbook.size() >= 3) { drawPage(flipbook, MODULO(currentPageIndex + 1, flipbook.size()), BLUE); }
if (flipbook.size() >= 5) { drawPage(flipbook, MODULO(currentPageIndex + 2, flipbook.size()), CYAN); }
}
// current page
drawPage(flipbook, currentPageIndex, BLACK);
HW04_drawTimeline(flipbook.size(), currentPageIndex);
}
}
}