Skip to content

Commit

Permalink
Version 0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
smurf667 authored Jan 2, 2021
1 parent 6319df6 commit 27eb102
Show file tree
Hide file tree
Showing 16 changed files with 710 additions and 128 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ Now you can run the output as a normal Java application, e.g. `java [-Duser.lang

## Demonstration

Here is a video generated with the tool:
Here are two videos generated with the tool:

[![Zooming in with fixed colors](https://img.youtube.com/vi/BMJ7DHeYodc/0.jpg)](https://www.youtube.com/watch?v=BMJ7DHeYodc)
[![v0.2.0](https://img.youtube.com/vi/BOyfSGexU08/0.jpg)](https://youtu.be/BOyfSGexU08)

[![v0.1.0](https://img.youtube.com/vi/BMJ7DHeYodc/0.jpg)](https://www.youtube.com/watch?v=BMJ7DHeYodc)

And here is a random collection of frames generated by the tool. You can load those frames for further investigation too:

Expand All @@ -35,3 +37,13 @@ And here is a random collection of frames generated by the tool. You can load th
|[![demo01](/src/test/resources/demo01.gif?raw=true "demo01")](/src/test/resources/demo01.png?raw=true)|[![demo02](/src/test/resources/demo02.gif?raw=true "demo02")](/src/test/resources/demo02.png?raw=true)|
|[![demo03](/src/test/resources/demo03.gif?raw=true "demo03")](/src/test/resources/demo03.png?raw=true)|[![demo04](/src/test/resources/demo04.gif?raw=true "demo04")](/src/test/resources/demo04.png?raw=true)|
|[![demo05](/src/test/resources/demo05.gif?raw=true "demo05")](/src/test/resources/demo05.png?raw=true)|[![demo06](/src/test/resources/demo06.gif?raw=true "demo06")](/src/test/resources/demo06.png?raw=true)|

## Version history

### v0.2.0

Better use of MP4 encoding, dropped color rotation and improve zoom flow.

### v0.1.0

Initial release.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<groupId>de.engehausen</groupId>
<artifactId>mandelbrot-movie-maker</artifactId>
<packaging>jar</packaging>
<version>0.1.0</version>
<version>0.2.0</version>
<name>MandelbrotMovieMaker</name>
<description>Tool to create a zooming video of a Mandelbrot set</description>
<properties>
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/de/engehausen/mb/FrameData.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,15 @@ public FrameData(final Dimension dimension, final Number topLeft, final double s
this.frameOffset = frameOffset;
}

/**
* Creates frame data with the values of the given source.
* @param data the source data
*/
public FrameData(final FrameData data) {
this.dimension = data.dimension;
this.topLeft = new Number(data.topLeft);
this.scale = data.scale;
this.frameOffset = data.frameOffset;
}

}
141 changes: 141 additions & 0 deletions src/main/java/de/engehausen/mb/FrameStreams.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package de.engehausen.mb;

import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import de.engehausen.mb.math.Point3D;

public class FrameStreams {

public static Stream<FrameData> buildLinear(final List<FrameData> frames, final int frameCount) {
return StreamSupport.stream(new LinearIterator(frames, frameCount), false);
}

public static Stream<FrameData> buildLogarithmic(final List<FrameData> frames, final int desiredFrameCount) {
return StreamSupport.stream(new LogarithmicIterator(frames, desiredFrameCount), false);
}

private static abstract class AbstractIterator implements Spliterator<FrameData> {

protected final Iterator<FrameData> iterator;
protected final int max;
protected final FrameData inter;
protected final double scaleStart;
protected FrameData current;
protected FrameData next;
protected int pos;

public AbstractIterator(final List<FrameData> frames, final int max) {
final int size = frames.size();
if (size < 2) {
throw new IllegalArgumentException("must have at least two frames");
}
iterator = frames.iterator();
this.max = max;
current = iterator.next();
next = iterator.next();
scaleStart = current.scale;
inter = new FrameData(current);
}

@Override
public boolean tryAdvance(final Consumer<? super FrameData> consumer) {
if (pos > max) {
return false;
}
final double actual = getScale();
if (actual < next.scale) {
current = next;
next = iterator.next();
}
final Point3D interpolated = interpolate(actual);
inter.topLeft.setReal(interpolated.x);
inter.topLeft.setImaginary(interpolated.y);
inter.scale = interpolated.z;
inter.frameOffset = current.frameOffset;
consumer.accept(inter);
pos++;
return true;
}

@Override
public Spliterator<FrameData> trySplit() {
return this;
}

@Override
public long estimateSize() {
return max;
}

@Override
public int characteristics() {
return Spliterator.ORDERED;
}

protected abstract double getScale();

protected Point3D interpolate(final double scale) {
final double t = (scale - current.scale) / (next.scale - current.scale);
return Point3D.linear(
t,
new Point3D(current.topLeft, current.scale),
new Point3D(next.topLeft, next.scale)
);
}
}

private static class LinearIterator extends AbstractIterator {

private final double step;

public LinearIterator(final List<FrameData> frames, final int max) {
super(frames, max);
step = (frames.get(frames.size() - 1).scale - current.scale) / (double) max;
}

@Override
protected double getScale() {
return scaleStart + step * pos;
}

}

private static class LogarithmicIterator extends AbstractIterator {

private static final double BASE = 0.95d;
private double factor;
private double initial;

public LogarithmicIterator(final List<FrameData> frames, final int max) {
super(frames, max);
initial = current.scale;
factor = (Math.log(frames.get(frames.size() - 1).scale / (BASE * initial)) / Math.log(BASE)) / max;
}

@Override
protected double getScale() {
return initial * Math.pow(BASE, factor * pos);
}

@Override
protected Point3D interpolate(final double scale) {
final double t = (scale - current.scale) / (next.scale - current.scale);
final Point3D from = new Point3D(current.topLeft, current.scale);
final Point3D to = new Point3D(next.topLeft, next.scale);
return Point3D.bezier(
t,
from,
from,
to,
to
);
}

}

}
101 changes: 60 additions & 41 deletions src/main/java/de/engehausen/mb/MovieRenderer.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package de.engehausen.mb;

import java.awt.Dimension;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import javax.swing.ProgressMonitor;
import javax.swing.SwingWorker;

import com.xuggle.ferry.AtomicInteger;
import com.xuggle.mediatool.IMediaWriter;
import com.xuggle.mediatool.ToolFactory;
import com.xuggle.xuggler.Configuration;
import com.xuggle.xuggler.ICodec;
import com.xuggle.xuggler.IRational;
import com.xuggle.xuggler.IStreamCoder;

import de.engehausen.mb.math.MandelbrotSet;
import de.engehausen.mb.math.Number;
import de.engehausen.mb.ui.Designer;

/**
Expand All @@ -26,29 +28,37 @@ public class MovieRenderer extends SwingWorker<Void, Void> {

private final Designer designer;
private final int framesPerSecond;
private final int frameCount;
private final int qScale;
private final int bitRate;
private final ProgressMonitor progress;
private final String fileName;

/**
* Creates the render.
* @param designer the designer supplying the zoom step information
* @param fps the frames per second
* @param quality the quality (1..31, from best to worst)
* @param seconds duration of video in seconds
* @param quality the quality (1..51, from best to worst)
* @param bitrate bitrate for the video
* @param fileName the file name of the result video
* @param monitor a progress monitor
*/
public MovieRenderer(
final Designer designer,
final int fps,
final int seconds,
final int quality,
final int bitrate,
final String fileName,
final ProgressMonitor monitor
) {
this.designer = designer;
this.fileName = fileName;
framesPerSecond = fps;
frameCount = fps * seconds;
qScale = quality;
bitRate = bitrate;
progress = monitor;
}

Expand All @@ -57,51 +67,60 @@ public MovieRenderer(
*/
@Override
protected Void doInBackground() throws Exception {
final Dimension dimension = designer.getFramePreview().getPreferredSize();
final MandelbrotSet mandelbrot = designer.getMandelbrotSet();
final int[] colors = designer.getColors();
final List<FrameData> list = designer.getFrameDataList();
final int max = list.size() - 1;
FrameData current = list.get(0);
FrameData last;
final int fpz = designer.getFramesPerZoom();
final Dimension dimension = designer.getFramePreview().getPreferredSize();
final double fpzd = fpz;

final Properties configProps = new Properties();
configProps.load(MovieRenderer.class.getResourceAsStream("/h264.properties"));
configProps.setProperty("qmin", Integer.toString(qScale));
configProps.setProperty("qmax", Integer.toString(qScale));
configProps.setProperty("b", Integer.toString(bitRate));
configProps.setProperty("ab", Integer.toString(bitRate));

final IMediaWriter writer = ToolFactory.makeWriter(fileName);
final int streamIndex = writer.addVideoStream(0, 0, ICodec.ID.CODEC_ID_MPEG4, IRational.make(1000, framesPerSecond), dimension.width, dimension.height);
final IStreamCoder coder = writer.getContainer().getStream(streamIndex).getStreamCoder();
coder.setFlag(IStreamCoder.Flags.FLAG_QSCALE, true);
coder.setGlobalQuality(qScale);
coder.setProperty("qscale", qScale);
final long msGoal = (long) 1000d/framesPerSecond;
long timestamp = 0;
int zoomStep = 0;
final int streamIndex = writer
.addVideoStream(
0,
0,
ICodec.ID.CODEC_ID_H264,
IRational.make(1000, framesPerSecond),
dimension.width,
dimension.height
);
final IStreamCoder coder = writer
.getContainer()
.getStream(streamIndex)
.getStreamCoder();
Configuration.configure(configProps, coder);
final long msGoal = (long) 1000d / framesPerSecond;

final AtomicInteger count = new AtomicInteger();
final AtomicLong timestamp = new AtomicLong(-msGoal);
try {
int count = 0;
do {
last = current;
current = list.get(++zoomStep);
final Number delta = new Number(current.topLeft)
.subtract(last.topLeft)
.divide(fpzd);
final double scaleStep = (current.scale - last.scale) / fpzd;
final Number frame = new Number(last.topLeft);
double frameScale = last.scale;
int off = last.frameOffset;
for (int j = 0; j < fpz && !progress.isCanceled(); j++, count++) {
writer.encodeVideo(0, mandelbrot.render(frame, frameScale, dimension.width, dimension.height, off, colors), timestamp, TimeUnit.MILLISECONDS);
timestamp += msGoal;
progress.setProgress(count);
frame.add(delta);
frameScale += scaleStep;
if (designer.isRotateColors()) {
off++;
FrameStreams
.buildLogarithmic(
designer.getFrameDataList(),
frameCount
).forEach(frameData -> {
if (progress.isCanceled()) {
return;
}
}
} while (zoomStep < max);
} catch (Throwable jan) {
jan.printStackTrace(System.err);
writer
.encodeVideo(
0,
mandelbrot.render(
frameData.topLeft,
frameData.scale,
dimension.width,
dimension.height,
frameData.frameOffset,
colors
),
timestamp.addAndGet(msGoal),
TimeUnit.MILLISECONDS);
progress.setProgress(count.incrementAndGet());
});
} finally {
writer.close();
progress.close();
Expand Down
10 changes: 0 additions & 10 deletions src/main/java/de/engehausen/mb/PngSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ public class PngSupport {

private static final String KEY_COLORS = "colors"; //$NON-NLS-1$
private static final String KEY_FRAME_DATA = "frameData"; //$NON-NLS-1$
private static final String KEY_SHIFT_COLORS = "shift"; //$NON-NLS-1$
private static final String KEY_ZOOM_FRAMES = "fpz"; //$NON-NLS-1$

/**
* Writes the image in PNG format and attached the given meta data.
Expand Down Expand Up @@ -73,10 +71,6 @@ public MandelbrotMetaData readMetaData(final File file) throws IOException {
result.frameData = FrameData.parseFrameData(node.getAttribute(META_VALUE));
} else if (KEY_COLORS.equals(key)) {
result.colors = parseColors(node.getAttribute(META_VALUE));
} else if (KEY_SHIFT_COLORS.equals(key)) {
result.shiftColors = Boolean.parseBoolean(node.getAttribute(META_VALUE));
} else if (KEY_ZOOM_FRAMES.equals(key)) {
result.framesPerZoom = Integer.parseInt(node.getAttribute(META_VALUE), 16);
}
}
} else {
Expand All @@ -103,8 +97,6 @@ protected void writeMetaData(final File file, final MandelbrotMetaData metaData)

final IIOMetadata data = image.getMetadata();
addMetaData(data, KEY_FRAME_DATA, FrameData.toString(metaData.frameData));
addMetaData(data, KEY_ZOOM_FRAMES, Integer.toString(metaData.framesPerZoom, 16));
addMetaData(data, KEY_SHIFT_COLORS, Boolean.toString(metaData.shiftColors));
addMetaData(data, KEY_COLORS, toString(metaData.colors));

final ImageWriter writer = ImageIO.getImageWriter(reader);
Expand Down Expand Up @@ -166,8 +158,6 @@ public static class MandelbrotMetaData {

public FrameData frameData;
public int[] colors;
public int framesPerZoom;
public boolean shiftColors;

/**
* Returns the color array. This method
Expand Down
Loading

0 comments on commit 27eb102

Please sign in to comment.