Skip to content

Commit

Permalink
Merge pull request #25748 from code-dot-org/dance-thumbnails
Browse files Browse the repository at this point in the history
Dance captures thumbnails
  • Loading branch information
Madelyn Kasula committed Nov 11, 2018
2 parents 533c3e6 + 436fa40 commit 77b5e96
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 8 deletions.
17 changes: 17 additions & 0 deletions apps/src/code-studio/initApp/project.js
Expand Up @@ -88,6 +88,7 @@ var isEditing = false;
let initialSaveComplete = false;
let initialCaptureComplete = false;
let thumbnailChanged = false;
let thumbnailPngBlob = null;

/**
* Current state of our sources API data
Expand Down Expand Up @@ -793,6 +794,11 @@ var projects = module.exports = {

if (preparingRemix) {
return this.sourceHandler.prepareForRemix().then(completeAsyncSave);
} else if (thumbnailPngBlob) {
const blob = thumbnailPngBlob;
thumbnailPngBlob = null;
// Call completeAsyncSave even if thumbnail save fails.
return this.saveThumbnail(blob).then(completeAsyncSave, completeAsyncSave);
} else {
return completeAsyncSave();
}
Expand Down Expand Up @@ -1283,6 +1289,17 @@ var projects = module.exports = {
return current && current.thumbnailUrl;
},

/**
* Sets the thumbnailPngBlob variable. Caveat: This does not save the thumbnail to the current project.
* Use the saveThumbnail method to do that.
* @param {Blob} pngBlob A Blob in PNG format containing the thumbnail image.
*/
setThumbnailPngBlob(pngBlob) {
if (pngBlob) {
thumbnailPngBlob = pngBlob;
}
},

/**
* Uploads a thumbnail image to the thumbnail path in the files API. If
* successful, stores a URL to access the thumbnail in current.thumbnailUrl.
Expand Down
13 changes: 11 additions & 2 deletions apps/src/dance/Dance.js
Expand Up @@ -16,6 +16,7 @@ import trackEvent from '../util/trackEvent';
import {SignInState} from '../code-studio/progressRedux';
import logToCloud from '../logToCloud';
import {saveReplayLog} from '../code-studio/components/shareDialogRedux';
import {setThumbnailBlobFromCanvas} from '../util/thumbnail';
import SignInOrAgeDialog from "../templates/SignInOrAgeDialog";
import project from "../code-studio/initApp/project";
import {
Expand Down Expand Up @@ -94,9 +95,7 @@ Dance.prototype.init = function (config) {
this.danceReadyPromise = new Promise(resolve => {
this.danceReadyPromiseResolve = resolve;
});

this.studioApp_.labUserId = config.labUserId;

this.level.softButtons = this.level.softButtons || {};

config.afterClearPuzzle = function () {
Expand Down Expand Up @@ -615,6 +614,7 @@ Dance.prototype.updateSongMetadata = function (id) {
*/
Dance.prototype.onHandleEvents = function (currentFrameEvents) {
this.hooks.find(v => v.name === 'runUserEvents').func(currentFrameEvents);
this.captureThumbnailImage();
};

/**
Expand All @@ -640,3 +640,12 @@ Dance.prototype.displayFeedback_ = function () {
Dance.prototype.getAppReducers = function () {
return reducers;
};

/**
* Capture a thumbnail image of the play space. This will capture a PNG blob
* of the thumbnail in memory, then will save that blob to S3 when the project
* is saved.
*/
Dance.prototype.captureThumbnailImage = function () {
setThumbnailBlobFromCanvas(document.getElementById('defaultCanvas0'));
};
48 changes: 42 additions & 6 deletions apps/src/util/thumbnail.js
Expand Up @@ -27,15 +27,15 @@ let lastCaptureTimeMs = 0;
* since the last capture.
* @returns {boolean}
*/
function shouldCapture() {
function shouldCapture(captureIntervalMs = MIN_CAPTURE_INTERVAL_MS) {
const {isShareView, isEmbedView} = getStore().getState().pageConstants;
if (!project.getCurrentId() || !project.isOwner() || isShareView || isEmbedView) {
return false;
}

// Skip capturing a screenshot if we just captured one recently.
const intervalMs = Date.now() - lastCaptureTimeMs;
if (intervalMs < MIN_CAPTURE_INTERVAL_MS) {
if (intervalMs < captureIntervalMs) {
return;
}

Expand Down Expand Up @@ -72,21 +72,57 @@ export function captureThumbnailFromSvg(svg) {

/**
* Copies the image from the canvas, shrinks it to a width equal to
* THUMBNAIL_WIDTH preserving aspect ratio, and saves it to the server.
* THUMBNAIL_WIDTH preserving aspect ratio, and returns the thumbnail blob
* to a callback method.
* @param {HTMLCanvasElement} canvas
* @param {func} onComplete
*/
export function captureThumbnailFromCanvas(canvas) {
export function getThumbnailFromCanvas(canvas, captureIntervalMs, onComplete) {
if (!canvas) {
console.warn(`Thumbnail capture failed: canvas element not found.`);
onComplete(null);
return;
}
if (!shouldCapture()) {
if (!shouldCapture(captureIntervalMs)) {
onComplete(null);
return;
}
lastCaptureTimeMs = Date.now();

const thumbnailCanvas = createThumbnail(canvas);
canvasToBlob(thumbnailCanvas).then(project.saveThumbnail);
canvasToBlob(thumbnailCanvas).then(onComplete);
}

/**
* Copies the image from the canvas, shrinks it to a width equal to
* THUMBNAIL_WIDTH preserving aspect ratio, and saves it to the server.
* @param {HTMLCanvasElement} canvas
*/
export function captureThumbnailFromCanvas(canvas) {
// Only save thumbnail in callback if a PNG blob is received.
const onComplete = (pngBlob) => {
if (pngBlob) {
project.saveThumbnail(pngBlob);
}
};
getThumbnailFromCanvas(canvas, MIN_CAPTURE_INTERVAL_MS, onComplete);
}

/**
* Copies the image from the canvas, shrinks it to a width equal to
* THUMBNAIL_WIDTH preserving aspect ratio, and saves it in memory.
* When the project is saved, the thumbnail will be saved as well.
* @param {HTMLCanvasElement} canvas
*/
export function setThumbnailBlobFromCanvas(canvas) {
/**
* Since we are storing the PNG blob in memory rather than writing it
* to S3 in our onComplete callback, we are decreasing our capture interval
* to 5 seconds. The thumbnail (captured every 5+ seconds) will then be
* saved to S3 when the project is saved.
*/
const OVERRIDE_MIN_CAPTURE_INTERVAL_MS = 5000;
getThumbnailFromCanvas(canvas, OVERRIDE_MIN_CAPTURE_INTERVAL_MS, project.setThumbnailPngBlob);
}

/**
Expand Down

0 comments on commit 77b5e96

Please sign in to comment.