Skip to content

Commit 5e3d114

Browse files
committed
Record the stage, not the window, when the window is bigger
Windows lets students maximize a stage's window although the library asks for a fixed size. The stage is letterboxed into the bigger window, but the recorders still took the whole window: a GIF started before maximizing kept only the window's top-left corner afterwards (the encoder crops every frame to the first one's size), and a maximized window was encoded at full size on the render thread, which slowed a program down to about one frame per second under software rendering. GifRecorder, FrameRecorder and FFmpegRecorder now save the stage at its own size, cut out of the window the way the stage designer's capture already did. That cut-out moves from Preview to Applet.getStageImage(), so all of them share it. With the window at the stage's size the pictures are the same as before.
1 parent 265d46a commit 5e3d114

6 files changed

Lines changed: 53 additions & 33 deletions

File tree

‎.changeset/record-the-stage.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
type: patch
3+
---
4+
5+
Recordings show the stage when the window is bigger than it, for example after
6+
maximizing it: `GifRecorder`, `FrameRecorder` and `FFmpegRecorder` save the stage
7+
at its own size, without the bars around it. A GIF that was started before the
8+
window was maximized no longer shows only the window's top-left corner after it,
9+
and recording a maximized window slows the program down far less, because the
10+
frames are encoded at the stage's size.

‎src/main/java/org/openpatch/scratch/extensions/recorder/FFmpegRecorder.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ public FFmpegRecorder(String path) {
4949
/** Saves the current frame using the specified pattern. */
5050
@Override
5151
public void saveFrame() {
52-
Applet.getInstance().saveFrame(pattern);
52+
// the stage at its own size: every frame of a video has to be the same size
53+
Applet applet = Applet.getInstance();
54+
applet.getStageImage().save(applet.savePath(applet.insertFrame(pattern)));
5355
}
5456

5557
/**

‎src/main/java/org/openpatch/scratch/extensions/recorder/FrameRecorder.java‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ public FrameRecorder(String outputFolder) {
3131
}
3232

3333
/**
34-
* Saves the current frame of the applet to the specified output folder. The frame is saved as a
34+
* Saves the current frame of the stage to the specified output folder. The frame is saved as a
3535
* PNG file with a filename pattern "#######.png". The exact filename will be generated based on
36-
* the current frame number.
36+
* the current frame number. The picture is the stage at its own size, also when the window is
37+
* bigger.
3738
*/
3839
@Override
3940
public void saveFrame() {
40-
Applet.getInstance().saveFrame(Paths.get(this.outputFolder, "#######.png").toString());
41+
Applet applet = Applet.getInstance();
42+
String file = applet.insertFrame(Paths.get(this.outputFolder, "#######.png").toString());
43+
applet.getStageImage().save(applet.savePath(file));
4144
}
4245

4346
/**
@@ -46,6 +49,7 @@ public void saveFrame() {
4649
* @param filename the name of the file to save the snapshot to
4750
*/
4851
public void snapshot(String filename) {
49-
Applet.getInstance().save(Paths.get(this.outputFolder, filename).toString());
52+
Applet applet = Applet.getInstance();
53+
applet.getStageImage().save(applet.savePath(Paths.get(this.outputFolder, filename).toString()));
5054
}
5155
}

‎src/main/java/org/openpatch/scratch/extensions/recorder/GifRecorder.java‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.openpatch.scratch.Color;
44
import org.openpatch.scratch.internal.AnimatedGifEncoder;
55
import org.openpatch.scratch.internal.Applet;
6+
import processing.core.PImage;
67

78
/**
89
* The GifRecorder class is responsible for recording and saving frames as GIF images. It extends
@@ -44,23 +45,25 @@ public GifRecorder(String filePath) {
4445
}
4546

4647
/**
47-
* Saves the current frame of the applet as a GIF image.
48+
* Saves the current frame of the stage as a GIF image.
4849
*
49-
* <p>This method captures the current frame from the applet, sets the frame rate, and adds the
50-
* frame to the GIF encoder. If the applet's pixels are null, the frame is not saved.
50+
* <p>This method captures the stage at its own size, sets the frame rate, and adds the frame to
51+
* the GIF encoder. A window that is bigger than the stage (maximized, say) records the stage
52+
* without the bars around it, and the frames keep their size when the window changes during the
53+
* recording.
5154
*
5255
* <p>Note: This method catches and ignores any NullPointerException that may occur.
5356
*/
5457
@Override
5558
public void saveFrame() {
5659
try {
5760
Applet applet = Applet.getInstance();
58-
applet.loadPixels();
59-
int[] pixels = applet.pixels;
60-
if (pixels != null) {
61+
PImage stage = applet.getStageImage();
62+
stage.loadPixels();
63+
if (stage.pixels != null) {
6164
this.gifEncoder.setFrameRate(applet.frameRate);
6265
this.gifEncoder.setRepeat(0);
63-
this.gifEncoder.addFrame(pixels, applet.pixelWidth, applet.pixelHeight);
66+
this.gifEncoder.addFrame(stage.pixels, stage.pixelWidth, stage.pixelHeight);
6467
}
6568
} catch (NullPointerException e) {
6669

‎src/main/java/org/openpatch/scratch/internal/Applet.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,27 @@ public int getRenderHeight() {
206206
return this.RENDER_HEIGHT;
207207
}
208208

209+
/**
210+
* The stage as it is in the window, without the bars around it and at the
211+
* stage's own size. The window can be bigger than the stage (maximized, a
212+
* high-resolution screen, a desktop that sizes windows itself), and then the
213+
* stage is letterboxed into it. Call it on the render thread, after drawing.
214+
*
215+
* @return the stage's picture, {@link #getRenderWidth()} by
216+
* {@link #getRenderHeight()}
217+
*/
218+
public PImage getStageImage() {
219+
float ratio = Math.min((float) this.width / this.RENDER_WIDTH,
220+
(float) this.height / this.RENDER_HEIGHT);
221+
int width = Math.round(this.RENDER_WIDTH * ratio);
222+
int height = Math.round(this.RENDER_HEIGHT * ratio);
223+
var area = this.get((this.width - width) / 2, (this.height - height) / 2, width, height);
224+
if (width != this.RENDER_WIDTH || height != this.RENDER_HEIGHT) {
225+
area.resize(this.RENDER_WIDTH, this.RENDER_HEIGHT);
226+
}
227+
return area;
228+
}
229+
209230
/**
210231
* Returns the height of the current window.
211232
*

‎src/main/java/org/openpatch/scratch/internal/Preview.java‎

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ private static void snapshot(Applet applet, Stage stage, StageHooks hooks, PGrap
179179
hooks.draw(graphics);
180180
Files.createDirectories(folder);
181181
// the picture first: an editor waiting for stage.json then finds both
182-
stageArea(applet).save(folder.resolve("frame.png").toAbsolutePath().toString());
182+
applet.getStageImage().save(folder.resolve("frame.png").toAbsolutePath().toString());
183183
write(folder, describe(stage, sprites, visible, fields, applet, omit));
184184
} finally {
185185
for (int i = 0; i < sprites.size(); i++) {
@@ -188,26 +188,6 @@ private static void snapshot(Applet applet, Stage stage, StageHooks hooks, PGrap
188188
}
189189
}
190190

191-
/**
192-
* The stage as it is in the window, without the bars around it and at the
193-
* stage's own size: the window may be scaled (a high-resolution screen, a
194-
* desktop that sizes windows itself), the stage is letterboxed into it.
195-
*/
196-
private static processing.core.PImage stageArea(Applet applet) {
197-
int targetWidth = applet.getRenderWidth();
198-
int targetHeight = applet.getRenderHeight();
199-
float ratio = Math.min((float) applet.getWidth() / targetWidth,
200-
(float) applet.getHeight() / targetHeight);
201-
int width = Math.round(targetWidth * ratio);
202-
int height = Math.round(targetHeight * ratio);
203-
var area = applet.get((applet.getWidth() - width) / 2, (applet.getHeight() - height) / 2,
204-
width, height);
205-
if (width != targetWidth || height != targetHeight) {
206-
area.resize(targetWidth, targetHeight);
207-
}
208-
return area;
209-
}
210-
211191
/** The requested stage, made the way {@code new MyStage()} would make it. */
212192
private static Stage make(ClassLoader loader, String packageName) {
213193
List<String> names = new ArrayList<>();

0 commit comments

Comments
 (0)