-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rotate #322
Rotate #322
Conversation
@@ -135,7 +135,7 @@ export default class ResizerOptions extends Component<Props, State> { | |||
onChange={this.onChange} | |||
> | |||
<option value="stretch">Stretch</option> | |||
<option value="cover">Cover</option> | |||
<option value="contain">Contain</option> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no idea why this was called "cover" before. It's clearly "contain", and other bits of code referred to it as "contain".
src/codecs/rotate-flip/options.tsx
Outdated
onInput={this.onChange} | ||
> | ||
Rotate: | ||
</Range> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this is the best input to use. Alternatively we could have a number input with buttons either side that increment/decrement by 90.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is okay for now imo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to also mention the direction of the rotation (e.g "clockwise" or "CW").
src/codecs/rotate-flip/processor.ts
Outdated
yield ((d1 * d1Multiplier) + (d2 * d2Multiplier)) * bpp; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was happy when I wrote this, but now it looks like an unmaintainable mess. I'm going to give offscreen canvas a try, and for other browsers… main thread canvas? It'd avoid forking the logic.
92a6ac1
to
7d96c80
Compare
Hmm, stand by, there are bugs that I didn't notice in dev. |
Oh great. The bugs don't appear in dev. |
@developit one for you GoogleChromeLabs/critters#17 |
I've hacked around the issue for now, but we shouldn't merge with the hack. |
Yeah, …no wait, lol, the offscreen canvas causes Chrome to switch OSX to the discrete GPU. That means serious jank when the worker loads the offscreen canvas code, and another jank when the worker is terminated and it switches back to the integrated GPU. Ok, I'll create a test case and file. |
Given that |
Maybe it's time to bring in https://github.com/ImageMagick/ImageMagick. Once we have that it'll be easy to add loads of different transforms. |
We could implement 90° image rotation in JavaScript using ImageMagick seems quite heavy for this but I’m not sure what else you had in mind. More scaling algorithms? |
That's what this PR is currently doing.
Yeah, the resize algorithms I'd like to add are all in ImageMagick. But maybe I'm overcomplicating things, and we could just land my JS solution for now. |
Hah, I've just brought my implementation down from 900ms to 300ms, same as the offscreen canvas. It could probably be faster with multithreading too. I was using a generator in the previous implementation. Removed it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad! I though you did OffscreenCanvas only, lol.
This is just a suggestion, but we could make use of DOMMatrix
to tighten up the code, but it’s a bit more... math-y. I just whipped this up in a scratchpad, so this might not be 100% correct, but you see where I’m going.
const width = 200, height = 300;
const angle = 90;
const m = new DOMMatrix()
// Rotate points 90 degrees around origin
m.rotateSelf(0, 0, angle);
// Calculate where the extreme point (width, height) is mapped to
// so we can figure out what translation to apply to make sure
// all result coordinates remain positive
const extreme = m.transformPoint(new DOMPoint(width, height));
// We have to apply that translation *before* the rotation. So we
// undo the rotation, apply the translation, then re-apply the rotation.
m.rotateSelf(0, 0, -angle);
m.translateSelf(extreme.x < 0 ? -extreme.x : 0, extreme.y < 0 ? -extreme.y : 0);
m.rotateSelf(0, 0, angle);
for(let py = 0; py < height; py++) {
for(let px = 0; px < width; px++) {
const p = new DOMPoint(px, py);
const {x, y} = m.transformPoint(dp);
for(let i = 0; i < 4; i++) {
// etc bla bla bla
}
});
src/codecs/rotate-flip/options.tsx
Outdated
onInput={this.onChange} | ||
> | ||
Rotate: | ||
</Range> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is okay for now imo
@surma I started looking at bringing in gl-matrix for this. WDYT? |
Of course it isn’t. Eh, |
Alternatively: Conditionally load a polyfill? |
I'll give the polyfill a go. It would make the code a whole lot more readable. |
This PR breaks the pinch zoom's boundary checking, since the element is centered using transforms. I should go back to the previous method. Also, the Chrome graphics glitch is back, probably because I introduced a wrapping element but didn't move the fix into that element. |
I think yes. Otherwise the split view is just plain weird and not that useful anymore. One idea: Would it be simpler to apply the real image data rotation only when saving/exporting, and rotate the whole split view area (including both images) via a CSS transform only (just to get the preview showing in correct orientation, and then the original doesn't need to be rotated in a different way)? Or would it make sense to have an option to preserve the Exif Orientation data in the output image (even if stripping all other Exif data) instead of doing a rotation on the image data? |
You can always apply the same transform to both sides.
Then the visual and file size preview would be wrong, which doesn't seem great. |
I gave export function rotateFlip(data: ImageData, opts: RotateFlipOptions): ImageData {
const flipDimensions = opts.rotate % 180 !== 0;
const width = flipDimensions ? data.height : data.width;
const height = flipDimensions ? data.width : data.height;
const out = new ImageData(width, height);
const { width: inputWidth, height: inputHeight } = data;
const matrix = new DOMMatrix();
matrix.translateSelf(width / 2, height / 2);
matrix.scaleSelf(
opts.flipHorizontal ? -1 : 1,
opts.flipVertical ? -1 : 1,
1,
);
matrix.rotateSelf(opts.rotate);
matrix.translateSelf(-inputWidth / 2, -inputHeight / 2);
const point = new DOMPoint();
for (let y = 0; y < inputHeight; y += 1) {
for (let x = 0; x < inputWidth; x += 1) {
point.x = x;
point.y = y;
const destPoint = matrix.transformPoint(point);
const fromStart = (y * inputWidth + x) * bpp;
const destStart = (Math.round(destPoint.y) * width + Math.round(destPoint.x)) * bpp;
for (let i = 0; i < bpp; i += 1) {
out.data[destStart + i] = data.data[fromStart + i];
}
}
}
return out;
} Much simpler code, but rotating the red panda image now takes 37.5 seconds, compared to 280ms. I'm guessing the creation of point objects and the necessary rounding makes the difference. Using gl-matrix will remove the object creation, but we'd still have the rounding to deal with. Going to stick with the currently pushed solution. |
3b80b5c
to
cca7368
Compare
I've added a rotate button next to the zoom controls. I've created the idea of an "input processor", which is a set of things that happens to the input before it's split across the two sides. I'm going to add flipping, and do some of my own reviews & testing, but I'd like feedback on the UX. |
data: ImageData; | ||
vectorImage?: HTMLImageElement; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no idea what this was doing here. Probably left over from when we split the compressor out into its own module.
src/components/Options/index.tsx
Outdated
let newState = this.props.preprocessorState; | ||
|
||
newState = cleanSet(newState, `${preprocessor}.enabled`, el.checked); | ||
this.props.onPreprocessorOptionsChange(newState); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll revert this change.
src/components/Output/index.tsx
Outdated
objectFit: leftImgContain ? 'contain' : '', | ||
}} | ||
/> | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to try and remove the need for this boxing.
src/components/Output/style.scss
Outdated
align-items: center; | ||
// This is a hack to work around https://github.com/GoogleChromeLabs/critters/issues/17. | ||
// DO NOT LET THIS MERGE. I'm just putting it here so the UI can be reviewed in the PR. | ||
contain: layout !important; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might not need this hack anymore. Will check.
@@ -15,7 +14,7 @@ interface CacheResult { | |||
interface CacheEntry extends CacheResult { | |||
preprocessorState: PreprocessorState; | |||
encoderState: EncoderState; | |||
source: SourceImage; | |||
sourceData: ImageData; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The source image now has data that doesn't matter to the result cache (eg, rotation settings).
UI choice looks really good for me! |
src/lib/icons.tsx
Outdated
return ( | ||
<Icon {...otherProps}> | ||
<path | ||
transform={`rotate(${ direction === 'horizontal' ? 90 : 0 })`} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This transform is almost certainly wrong, but I'll catch it when I add the buttons properly.
@surma @kosamari @developit how important do we think 'flip' is? It's easy to add, but I'm worried about usefulness vs the UI clutter. |
Fairly low, imo. IIUC, the main demand is coming from importing pictures from your phone, where native pixel order is almost always landscape, even if the picture was taken portrait. |
I've removed flip. We can always bring it back later. |
I think this is ready. @surma mind taking another look? |
src/codecs/processor-worker/index.ts
Outdated
const { process } = await import( | ||
/* webpackChunkName: "process-imagequant" */ | ||
'../imagequant/processor', | ||
); | ||
return process(data, opts); | ||
} | ||
|
||
async function rotateFlip( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Remove flip
from the name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hah well spotted. I thought I caught all of these. Should have done a search.
src/codecs/rotate/processor-meta.ts
Outdated
@@ -0,0 +1,3 @@ | |||
export type OptionType = 0 | 90 | 180 | 270; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should do an interface here like with the other encoder options. The naming is weird right now and interfaces are easier to extend.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As in, an interface with a single property? { rotate }
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So calls would be:
rotate(data, { rotate: 90 });
const width = flipDimensions ? data.height : data.width; | ||
const height = flipDimensions ? data.width : data.height; | ||
const out = new ImageData(width, height); | ||
const { width: inputWidth, height: inputHeight } = data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super small nit: Move this up and use inputWidth
and inputHeight
when calculating width
and height
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good shout
* Basic rotate & flip * Flipping resize when orientation changes * Hack around critters issue. * Removing generator. Huge perf boost. * Stable positioning * Creating input processors * Allowing rotation to be changed * Reverting old change * Adding tooltips * No more flip * Removing need for wrapper element boxing * Adding comment * Addressing nits * Bleh
Ready!