Last active
November 9, 2024 10:59
-
-
Save KrabCode/f5183a961fdae1725441a7ab1a367822 to your computer and use it in GitHub Desktop.
A calendar of 365 moons in different phases as a looping trippy visual.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.krab.lazy.*; | |
LazyGui gui; | |
String[] monthNames = new String[]{ | |
"January", | |
"February", | |
"March", | |
"April", | |
"May", | |
"June", | |
"July", | |
"August", | |
"September", | |
"October", | |
"November", | |
"December" | |
}; | |
void setup() { | |
//size(1200, 800, P2D); | |
fullScreen(P2D); | |
smooth(8); | |
frameRate(60); | |
colorMode(HSB, 1, 1, 1, 1); | |
gui = new LazyGui(this); | |
} | |
void draw() { | |
float t = gui.slider("time"); | |
float speed = gui.slider("speed", 0.5); | |
gui.sliderSet("time", t + radians(speed)); | |
float basePhase = (t * TAU) % TAU; | |
background(gui.colorPicker("bg").hex); | |
float gridPosX = gui.slider("grid x"); | |
float gridStepX = gui.slider("grid step x", 50); | |
float gh = gui.slider("grid height", 800); | |
float r = gui.slider("moon radius", 20); | |
// The time interval between a full moon and the next repetition of the same phase, a synodic month, averages about 29.53 days. -> https://en.wikipedia.org/wiki/Full_moon | |
float irlPhasePerDay = 1 / 29.53f; // 1 / 29.53 is 0.0338638672536404 | |
float phasePerDay = gui.slider("phasePerDay", irlPhasePerDay); // | |
int rows = gui.sliderInt("month count", 12); | |
push(); | |
font(); | |
gui.pushFolder("months"); | |
PVector commonMonthNameTextTranslate = gui.plotXY("month text offset", 700); | |
PVector commonDayIndexTextTranslate = gui.plotXY("day text offset", 700); | |
int moonIndex = 0; | |
for (int yi = 0; yi < rows; yi++) { | |
String monthFolderName = "for each month/" + (yi + 1); | |
gui.pushFolder(monthFolderName); | |
String monthName = gui.text("month name", monthNames[yi].substring(0, 3)); | |
int cols = gui.sliderInt("days", 30); | |
float yn = norm(yi, 0, rows-1); | |
float y = map(yn, 0, 1, -gh*0.5f, gh*0.5f); | |
PVector pos = gui.plotXY("month name pos"); | |
gui.popFolder(); | |
push(); | |
translate(0, y); | |
textAlign(LEFT, CENTER); | |
text(monthName, commonMonthNameTextTranslate.x + pos.x, commonMonthNameTextTranslate.y + pos.y); | |
for (int xi = 0; xi < cols; xi++) { | |
moonIndex++; | |
float x = gridPosX + gridStepX * xi; // left aligned columns | |
float phaseOffset = moonIndex * phasePerDay * TAU; | |
float todayPhase = (basePhase + phaseOffset) % TAU; | |
push(); | |
translate(x, 0); | |
drawMoonPhase(r, todayPhase); | |
if(yi == 0){ | |
textAlign(CENTER, CENTER); | |
text(xi + 1, commonDayIndexTextTranslate.x, commonDayIndexTextTranslate.y); | |
} | |
pop(); | |
} | |
pop(); | |
} | |
gui.popFolder(); | |
pop(); | |
Utils.record(this, gui); // Requires Utils.pde as a separate tab | |
} | |
void drawMoonPhase(float r, float phase) { | |
float lightPhase = (phase + HALF_PI); | |
boolean growing = cos(lightPhase) < 0; | |
float angle0 = phase; | |
float edge0 = cos(angle0); | |
float angle1 = phase + PI; | |
float edge1 = cos(angle1); | |
int fg = gui.colorPicker("foreground", color(0.6)).hex; | |
int bg = gui.colorPicker("background", color(0.2)).hex; | |
int outline = gui.colorPicker("outline", color(0.6)).hex; | |
push(); | |
translate(width/2, height/2); | |
translate(gui.plotXY("moon pos").x, gui.plotXY("moon pos").y); | |
fill(growing ? bg : fg); | |
circle(0, 0, r*2); | |
fill(!growing ? bg : fg); | |
noStroke(); | |
float outlineWeight = gui.slider("outline weight", 1.99); | |
float rInner = r - outlineWeight * 0.5; | |
circleArc(growing ? rInner*edge0 : rInner*edge1, rInner); | |
stroke(outline); | |
noFill(); | |
strokeWeight(outlineWeight); | |
circle(0, 0, r*2); | |
pop(); | |
} | |
void circleArc(float w, float h) { | |
int detail = gui.sliderInt("detail", 30); | |
beginShape(); | |
// outer circle | |
for (int i = 0; i < detail; i++) { | |
float n = map(i, 0, detail-1, -HALF_PI, HALF_PI); | |
float x = h * cos(n); | |
float y = h * sin(n); | |
vertex(x, y); | |
} | |
// inner arc | |
for (int i = 0; i < detail; i++) { | |
float n = - map(i, 0, detail-1, -HALF_PI, HALF_PI); | |
float x = w * cos(n); | |
float y = h * sin(n); | |
vertex(x, y); | |
} | |
endShape(); | |
} | |
PFont selectedFont; | |
void font() { | |
gui.pushFolder("font"); | |
fill(gui.colorPicker("fill", color(1)).hex); | |
int size = gui.sliderInt("size", 64, 1, 256); | |
float leading = gui.slider("leading", 64); | |
String fontName = gui.text("font name", "Arial").trim(); | |
if (gui.button("list fonts")) { | |
String[] fonts = PFont.list(); | |
for (String font : fonts) { | |
println(font + " "); // some spaces to avoid copying newlines from the console | |
} | |
} | |
if (selectedFont == null || gui.hasChanged("font name") || gui.hasChanged("size")) { | |
selectedFont = createFont(fontName, size); | |
} | |
textFont(selectedFont); | |
textLeading(leading); | |
gui.popFolder(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.*; | |
import java.nio.file.Paths; | |
import java.awt.*; | |
import java.io.File; | |
import java.io.IOException; | |
static class Utils { | |
private static int recStarted = -1; | |
private static int saveIndex = 1; | |
private static String recordingId = generateRandomShortId(); | |
public static String generateRandomShortId() { | |
return UUID.randomUUID().toString().replace("-", "").substring(0, 8); | |
} | |
public static void record(PApplet pApplet, LazyGui gui) { | |
gui.sliderInt("rec/frame"); | |
gui.sliderSet("rec/frame", saveIndex); | |
int recLengthDefault = 360; | |
int recLength = gui.sliderInt("rec/length", recLengthDefault); | |
boolean recordingInProgress = recStarted != -1 && pApplet.frameCount < recStarted + recLength; | |
boolean recordingJustEnded = recStarted != -1 && pApplet.frameCount == recStarted + recLength; | |
if (gui.button("rec/start (ctrl + k)") || | |
(Input.getCode(CONTROL).down && Input.getChar('k').pressed)) { | |
recordingId = generateRandomShortId(); | |
recStarted = pApplet.frameCount; | |
saveIndex = 1; | |
gui.sliderSet("rec/frame", saveIndex); | |
} | |
boolean stopCommand = gui.button("rec/stop (ctrl + l)"); | |
if (stopCommand || | |
(Input.getCode(CONTROL).down && Input.getChar('l').pressed)) { | |
recStarted = -1; | |
saveIndex = 1; | |
recordingJustEnded = true; | |
gui.sliderSet("rec/frame", saveIndex); | |
} | |
String sketchMainClassName = pApplet.getClass().getSimpleName(); | |
String recDir = pApplet.dataPath("video/" + sketchMainClassName + "_" + recordingId); | |
String recDirAbsolute = Paths.get(recDir).toAbsolutePath().toString(); | |
if (gui.button("rec/open folder")) { | |
Desktop desktop = Desktop.getDesktop(); | |
File dir = new File(pApplet.dataPath("video")); | |
if (!dir.exists()) { | |
//noinspection ResultOfMethodCallIgnored | |
dir.mkdirs(); | |
} | |
try { | |
desktop.open(dir); | |
} | |
catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
PVector recordRectPos = PVector.add(gui.plotXY("rec/rect pos"), new PVector(pApplet.width / 2f, pApplet.height / 2f)); | |
PVector recordRectSize = gui.plotXY("rec/rect size", pApplet.width, pApplet.height); | |
int recordRectSizeX = floor(recordRectSize.x); | |
int recordRectSizeY = floor(recordRectSize.y); | |
// prevent resolutions odd numbers because ffmpeg can't work with them | |
if (recordRectSizeX % 2 != 0) { | |
recordRectSizeX += 1; | |
} | |
if (recordRectSizeY % 2 != 0) { | |
recordRectSizeY += 1; | |
} | |
String recImageFormat = ".jpg"; | |
if (recordingInProgress) { | |
println("saved " + saveIndex + " / " + recLength); | |
PImage cutout = pApplet.get( | |
floor(recordRectPos.x) - recordRectSizeX / 2, | |
floor(recordRectPos.y) - recordRectSizeY / 2, | |
recordRectSizeX, | |
recordRectSizeY | |
); | |
cutout.save(recDir + "/" + saveIndex++ + recImageFormat); | |
} | |
if (gui.toggle("rec/show rect")) { | |
pApplet.pushStyle(); | |
pApplet.stroke(pApplet.color(0xA0FFFFFF)); | |
pApplet.noFill(); | |
pApplet.rectMode(CENTER); | |
pApplet.rect(recordRectPos.x, recordRectPos.y, recordRectSizeX, recordRectSizeY); | |
pApplet.popStyle(); | |
} | |
int ffmpegFramerate = gui.sliderInt("rec/ffmpeg fps", 60, 1, Integer.MAX_VALUE); | |
if (gui.toggle("rec/ffmpeg", true) && recordingJustEnded) { | |
String outMovieFilename = pApplet.dataPath("video/" + sketchMainClassName + "_" + recordingId); | |
String inputFormat = recDirAbsolute + "/%01d" + recImageFormat; | |
String command = String.format("ffmpeg -r %s -i %s -start_number_range 100 -an %s.mp4", | |
ffmpegFramerate, inputFormat, outMovieFilename); | |
println("running ffmpeg: " + command); | |
try { | |
Process proc = Runtime.getRuntime().exec(command); | |
new Thread(() -> { | |
Scanner sc = new Scanner(proc.getErrorStream()); | |
while (sc.hasNextLine()) { | |
println(sc.nextLine()); | |
} | |
println("finished recording " + outMovieFilename); | |
} | |
).start(); | |
} | |
catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
study code
study output
slower month grid
normal month grid
faster month grid