Skip to content

Commit

Permalink
allow to crop frame when exporting/sharing files or rendering animation
Browse files Browse the repository at this point in the history
  • Loading branch information
HerrZatacke committed Jul 17, 2020
1 parent dedd8ac commit a2e061c
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 40 deletions.
30 changes: 30 additions & 0 deletions src/javascript/app/components/Settings/component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SocketStateIndicator from '../SocketStateIndicator';
import cleanUrl from '../../../tools/cleanUrl';
import { getEnv } from '../../../tools/getEnv';
import supportedCanvasImageFormats from '../../../tools/supportedCanvasImageFormats/îndex';
import SVG from '../SVG';

class Settings extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -118,6 +119,33 @@ class Settings extends React.Component {
</label>
))}
</div>
<label
className={
classnames('settings__inputgroup settings__check-group', {
'settings__check-group--checked': this.props.exportCropFrame,
})
}
>
<span
className="settings__label"
title="Enable Yoyo-Effect (loop back to the beginning)"
>
Crop/remove frame when exporting or sharing images
</span>
<span
className="settings__checkbox-wrap"
>
<input
type="checkbox"
className="settings__checkbox"
checked={this.props.exportCropFrame}
onChange={({ target }) => {
this.props.setExportCropFrame(target.checked);
}}
/>
<SVG name="checkmark" />
</span>
</label>
{(this.env.env !== 'webpack-dev') ? null : (
<>
<div className="settings__inputgroup">
Expand Down Expand Up @@ -267,6 +295,8 @@ Settings.propTypes = {
pageSize: PropTypes.number.isRequired,
setPageSize: PropTypes.func.isRequired,
exportSettings: PropTypes.func.isRequired,
setExportCropFrame: PropTypes.func.isRequired,
exportCropFrame: PropTypes.bool.isRequired,
};

Settings.defaultProps = {};
Expand Down
7 changes: 7 additions & 0 deletions src/javascript/app/components/Settings/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const mapStateToProps = (state) => ({
exportFileTypes: state.exportFileTypes,
pageSize: state.pageSize,
savFrameTypes: state.savFrameTypes,
exportCropFrame: state.exportCropFrame,
});

const mapDispatchToProps = (dispatch) => ({
Expand All @@ -28,6 +29,12 @@ const mapDispatchToProps = (dispatch) => ({
payload: savFrameTypes,
});
},
setExportCropFrame(exportCropFrame) {
dispatch({
type: 'SET_EXPORT_CROP_FRAME',
payload: exportCropFrame,
});
},
setPageSize(pageSize) {
dispatch({
type: 'SET_PAGESIZE',
Expand Down
26 changes: 26 additions & 0 deletions src/javascript/app/components/Settings/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,32 @@
}
}

&__check-group {
.svg {
width: 30px;
height: 30px;
}

.tick {
display: none;
}

&--checked .tick {
display: block;
}
}

&__checkbox-wrap {
cursor: pointer;
flex: auto 1 1;
user-select: none;
margin-left: 20px;
}

&__checkbox {
display: none;
}

&__version {
margin-top: 40px;
font-size: 12px;
Expand Down
65 changes: 47 additions & 18 deletions src/javascript/app/components/VideoParamsForm/component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class VideoParamsForm extends React.Component {
yoyo: props.yoyo,
frame: props.frame,
lockFrame: props.lockFrame,
cropFrame: props.lockFrame,
};

this.callUpdate = this.callUpdate.bind(this);
Expand All @@ -34,6 +35,7 @@ class VideoParamsForm extends React.Component {
yoyo: this.state.yoyo,
frame: this.state.frame,
lockFrame: this.state.lockFrame,
cropFrame: this.state.cropFrame,
};

this.setState(cleanState);
Expand Down Expand Up @@ -120,6 +122,28 @@ class VideoParamsForm extends React.Component {
<SVG name="checkmark" />
<span className="video-params__check-label-text">Yoyo-Effect</span>
</label>
<label
className={
classnames('video-params__check-label', {
'video-params__check-label--checked': this.props.cropFrame,
})
}
>
<input
type="checkbox"
className="video-params__checkbox"
checked={this.state.cropFrame}
onChange={({ target }) => {
this.setState({
cropFrame: target.checked,
}, this.callUpdate);
}}
/>
<SVG name="checkmark" />
<span className="video-params__check-label-text">
Crop/remove frame
</span>
</label>
<div className="video-params__select-label">
Palette
</div>
Expand All @@ -139,24 +163,28 @@ class VideoParamsForm extends React.Component {
}, this.callUpdate);
}}
/>
<div className="video-params__select-label">
Frame
</div>
<FrameSelect
frame={this.state.frame}
lockFrame={this.state.lockFrame}
noFrameOption="As selected per image"
updateFrame={(frame) => {
this.setState({
frame,
}, this.callUpdate);
}}
updateFrameLock={(lockFrame) => {
this.setState({
lockFrame,
}, this.callUpdate);
}}
/>
{ this.state.cropFrame ? null : (
<>
<div className="video-params__select-label">
Frame
</div>
<FrameSelect
frame={this.state.frame}
lockFrame={this.state.lockFrame}
noFrameOption="As selected per image"
updateFrame={(frame) => {
this.setState({
frame,
}, this.callUpdate);
}}
updateFrameLock={(lockFrame) => {
this.setState({
lockFrame,
}, this.callUpdate);
}}
/>
</>
)}
</Lightbox>
);
}
Expand All @@ -170,6 +198,7 @@ VideoParamsForm.propTypes = {
invertPalette: PropTypes.bool.isRequired,
yoyo: PropTypes.bool.isRequired,
lockFrame: PropTypes.bool.isRequired,
cropFrame: PropTypes.bool.isRequired,
frame: PropTypes.string.isRequired,
cancel: PropTypes.func.isRequired,
animate: PropTypes.func.isRequired,
Expand Down
1 change: 1 addition & 0 deletions src/javascript/app/components/VideoParamsForm/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const mapStateToProps = (state) => ({
lockFrame: state.videoParams.lockFrame || false,
invertPalette: state.videoParams.invertPalette || false,
frame: state.videoParams.frame || '',
cropFrame: state.videoParams.cropFrame || false,
palette: state.videoParams.palette || '',
});

Expand Down
1 change: 1 addition & 0 deletions src/javascript/app/store/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default {
videoParams: {},
filter: {},
savFrameTypes: 'int',
exportCropFrame: false,
palettes: [
{
name: 'Black & White',
Expand Down
3 changes: 2 additions & 1 deletion src/javascript/app/store/middlewares/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const animate = (store) => (next) => (action) => {
lockFrame: videoLockFrame,
invertPalette: videoInvertPalette,
palette: videoPalette,
cropFrame,
} = state.videoParams;

const videoWriter = new WebMWriter({
Expand Down Expand Up @@ -62,7 +63,7 @@ const animate = (store) => (next) => (action) => {
});
}

return decoder.getScaledCanvas(scaleFactor);
return decoder.getScaledCanvas(scaleFactor, cropFrame);
})
)))
.then((images) => {
Expand Down
3 changes: 2 additions & 1 deletion src/javascript/app/store/middlewares/share.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ const batch = (store) => (next) => (action) => {

const shareScaleFactor = [...state.exportScaleFactors].pop() || 4;
const shareFileType = [...state.exportFileTypes].pop() || 'png';
const exportCropFrame = state.exportCropFrame;

const prepareFiles = getPrepareFiles([shareScaleFactor], [shareFileType]);
const prepareFiles = getPrepareFiles([shareScaleFactor], [shareFileType], exportCropFrame);

loadImageTiles(image, state)
.then(prepareFiles(imagePalette, image))
Expand Down
4 changes: 2 additions & 2 deletions src/javascript/app/store/middlewares/startDownload.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ const startDownload = (store) => (next) => (action) => {

if ((action.type === 'START_DOWNLOAD') || (action.type === 'DOWNLOAD_SELECTION')) {
const state = store.getState();
const { exportScaleFactors, exportFileTypes } = state;
const { exportScaleFactors, exportFileTypes, exportCropFrame } = state;

const prepareFiles = getPrepareFiles(exportScaleFactors, exportFileTypes);
const prepareFiles = getPrepareFiles(exportScaleFactors, exportFileTypes, exportCropFrame);

switch (action.type) {
case 'START_DOWNLOAD':
Expand Down
2 changes: 2 additions & 0 deletions src/javascript/app/store/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import canShare from './reducers/canShareReducer';
import confirmation from './reducers/confirmationReducer';
import dragover from './reducers/dragoverReducer';
import editImage from './reducers/editImageReducer';
import exportCropFrame from './reducers/exportCropFrameReducer';
import exportFileTypes from './reducers/exportFileTypesReducer';
import exportScaleFactors from './reducers/exportScaleFactorsReducer';
import filter from './reducers/filtersReducer';
Expand Down Expand Up @@ -33,6 +34,7 @@ export default combineReducers({
confirmation,
dragover,
editImage,
exportCropFrame,
exportFileTypes,
exportScaleFactors,
filter,
Expand Down
12 changes: 12 additions & 0 deletions src/javascript/app/store/reducers/exportCropFrameReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const exportCropFrameReducer = (value = false, action) => {
switch (action.type) {
case 'SET_EXPORT_CROP_FRAME':
return action.payload;
case 'GLOBAL_UPDATE':
return action.payload.exportCropFrame || value;
default:
return value;
}
};

export default exportCropFrameReducer;
42 changes: 26 additions & 16 deletions src/javascript/tools/Decoder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,30 @@ class Decoder {
context.putImageData(imageData, 0, 0);
}

getScaledCanvas(scaleFactor) {
const initialWidth = TILES_PER_LINE * TILE_PIXEL_WIDTH;
const initialHeight = this.getHeight();

const scaledCanvas = document.createElement('canvas');
const scaledContext = scaledCanvas.getContext('2d');
scaledCanvas.width = initialWidth * scaleFactor;
scaledCanvas.height = initialHeight * scaleFactor;

this.tiles.forEach((tile, index) => {
this.paintTileScaled(this.decodeTile(tile), index, scaledContext, scaleFactor);
});
getScaledCanvas(scaleFactor, cropFrame = false) {
// 2 tiles top/left/bottom/right -> 4 tiles to each side
const FRAME_TILES = 4;

const initialHeight = this.getHeight() - (cropFrame ? TILE_PIXEL_HEIGHT * FRAME_TILES : 0);
const initialWidth = (TILES_PER_LINE * TILE_PIXEL_WIDTH) - (cropFrame ? TILE_PIXEL_WIDTH * FRAME_TILES : 0);

const tilesPerLine = cropFrame ? TILES_PER_LINE - FRAME_TILES : TILES_PER_LINE;

const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = initialWidth * scaleFactor;
canvas.height = initialHeight * scaleFactor;

this.tiles
.map((tile, index) => (
cropFrame && this.tileIndexIsFramePart(index) ? null : tile
))
.filter(Boolean)
.forEach((tile, index) => {
this.paintTileScaled(this.decodeTile(tile), index, context, scaleFactor, tilesPerLine);
});

return scaledCanvas;
return canvas;
}

setCanvas(canvas) {
Expand Down Expand Up @@ -247,9 +257,9 @@ class Decoder {
}
}

paintTileScaled(pixels, index, canvasContext, pixelSize) {
const tileXOffset = index % TILES_PER_LINE;
const tileYOffset = Math.floor(index / TILES_PER_LINE);
paintTileScaled(pixels, index, canvasContext, pixelSize, tilesPerLine) {
const tileXOffset = index % tilesPerLine;
const tileYOffset = Math.floor(index / tilesPerLine);

const pixelXOffset = TILE_PIXEL_WIDTH * tileXOffset * pixelSize;
const pixelYOffset = TILE_PIXEL_HEIGHT * tileYOffset * pixelSize;
Expand Down
4 changes: 2 additions & 2 deletions src/javascript/tools/download/getPrepareFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Decoder from '../Decoder';
import RGBNDecoder from '../RGBNDecoder';
import generateFileName from '../generateFileName';

const getPrepareFiles = (exportScaleFactors, exportFileTypes) => (palette, image) => (tiles) => {
const getPrepareFiles = (exportScaleFactors, exportFileTypes, exportCropFrame) => (palette, image) => (tiles) => {

const isRGBN = !!image.hashes;
const decoder = isRGBN ? new RGBNDecoder() : new Decoder();
Expand Down Expand Up @@ -45,7 +45,7 @@ const getPrepareFiles = (exportScaleFactors, exportFileTypes) => (palette, image
exportScaleFactor,
});

const scaledCanvas = decoder.getScaledCanvas(exportScaleFactor);
const scaledCanvas = decoder.getScaledCanvas(exportScaleFactor, exportCropFrame);

if (scaledCanvas.msToBlob) {
window.navigator.msSaveBlob(scaledCanvas.msToBlob(), `${filename}.png`);
Expand Down

0 comments on commit a2e061c

Please sign in to comment.