Skip to content

Commit

Permalink
- Add custom metadata (devicePixelRatio, lastModified, dimensions) to…
Browse files Browse the repository at this point in the history
… downloaded file.

- Check custom metadata on uploaded files and skip the 8-bit transform.
  • Loading branch information
Danziger committed Jan 13, 2024
1 parent d594440 commit c98e891
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 32 deletions.
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,16 @@ TODOs & Bug

- [x] Fix accessibility sound.

- [ ] Add custom metadata (`devicePixelRatio`, `date`) to downloaded file.
- [ ] Check metadata to upload (`Use PNG metadata to know if an image was generated...`).
- [x] Add custom metadata (`devicePixelRatio`, `lastModified`, dimensions) to downloaded file.
- [x] Check custom metadata on uploaded files and skip the 8-bit transform.

- [ ] Hide "Hiring?" label in focus mode.
- [ ] Focus mode should ONLY show a hamburger icon in the footer.
- [ ] Hide "Hiring", as I'm not looking anymore.
- [ ] Change wand icon.
- [ ] Add an anchor to quickly launch / access the focus mode.
### Bugs

- [ ] `TODO: Consider updating the cursor position continuously if in interactive mode:`
- [ ] `TODO: Replace the `this.lastDrawingIndex !== randomIndex` with AbortController and signals:`
- Update the cursor position continuously if in interactive mode.

### Bugs
- Focus mode should only show the colors and a hamburger icon in the `Footer`. Hide "Hiring?" label and the whole `Nav`.

- Change wand icon.

- Adjust side paddings to be included in the header links and button.

Expand All @@ -80,6 +77,8 @@ TODOs & Bug

- Make the canvas size multiple of `unit`, so that "partial pixels" are downloaded without cropping.

- When the wand is clicked multiple times, the drawing algorithm should only run for the last click.

<br />

### JS Paint Features
Expand Down Expand Up @@ -109,6 +108,8 @@ TODOs & Bug

- Rebuild nav so that actions are just icons at the bottom of the screen and move settings to their own page.

- Add an anchor to quickly launch / access the focus mode.

- Add gimmicky sounds to some clicks: paint splash sound (color), recycle bin sound to clear...

- Add an option to send drawings to me (to Supabase, maybe).
Expand Down
8 changes: 7 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"node": ">=18.0.0"
},
"dependencies": {
"core-js": "^3.32.0"
"core-js": "^3.32.0",
"meta-png": "^1.0.6"
},
"devDependencies": {
"@babel/core": "^7.22.10",
Expand Down
81 changes: 67 additions & 14 deletions src/app/components/app/app.component.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { addMetadataFromBase64DataURI, addMetadata, getMetadata } from 'meta-png';

import { Ruler } from '../ruler/ruler.component';
import { JsPaint } from '../js-paint/js-paint.component';
import { IS_DESKTOP, IS_BROWSER_SUPPORTED, HAS_TOUCH, HAS_CURSOR } from '../../constants/browser.constants';
Expand Down Expand Up @@ -190,7 +193,7 @@ export class App {

// handleToggleGlobalClass

handleFileUpload(imageFile) {
async handleFileUpload(imageFile) {
if (!imageFile || !imageFile.type.startsWith('image/')) {
this.uiControls.dropZone.showError();

Expand All @@ -199,14 +202,29 @@ export class App {

const { jsPaint } = this;

function eightBit(img, pixelSize) {
// TODO: Use PNG metadata to know if an image was generated with this tool and with what devicePixelRatio.
// Those that are "originals" (we should include a hash) should be uploaded as-is, without any kind of resizing.
function eightBit(img, metadata, pixelSize) {
const {
devicePixelRatio = 1,
innerWidth,
innerHeight,
} = window;

const useDevicePixelRatio = false;
if (
metadata.devicePixelRatio === devicePixelRatio
&& img.width === innerWidth * devicePixelRatio
&& img.height === innerHeight * devicePixelRatio
&& imageFile.lastModified - metadata.lastModified < 4000
) {
console.log('DIRECT UPLOAD');

jsPaint.ctx.drawImage(img, 0, 0);
return;
}

const { devicePixelRatio = 1 } = window;
const scale = useDevicePixelRatio ? devicePixelRatio : 1;
console.log('RESIZED UPLOAD');

const useDevicePixelRatio = false;
const scale = (useDevicePixelRatio ? devicePixelRatio : metadata.devicePixelRatio) || 1;

const imageWidth = img.width;
const imageHeight = img.height;
Expand Down Expand Up @@ -249,16 +267,51 @@ export class App {
jsPaint.drawImage(canvas);
}

const img = new Image();
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();

img.onload = () => {
// Free memory:
URL.revokeObjectURL(img.src);
img.onload = () => {
resolve(img);
};

eightBit(img, jsPaint.unit);
};
img.onerror = reject;

img.src = src;
});
}

const imageSrc = URL.createObjectURL(imageFile);

const imagePromise = loadImage(imageSrc);

const metadataPromise = fetch(imageSrc).then((response) => {
if (!response.ok) {
throw new Error(`HTTP error, status = ${ response.status }`);
}

return response.arrayBuffer();
}).then((arrayBuffer) => {
const arrayBufferView = new Uint8Array(arrayBuffer);

return {
devicePixelRatio: parseFloat(getMetadata(arrayBufferView, 'devicePixelRatio'), 10) || null,
lastModified: parseInt(getMetadata(arrayBufferView, 'lastModified'), 10) || 0,
};
});

const [
metadata,
image,
] = await Promise.allSettled([
metadataPromise,
imagePromise,
]);

// Free memory:
URL.revokeObjectURL(imageSrc);

img.src = URL.createObjectURL(imageFile);
eightBit(image.value, metadata.value, jsPaint.unit);
}

handleColorChange(hexColor) {
Expand Down
45 changes: 39 additions & 6 deletions src/app/components/js-paint/js-paint.component.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { addMetadataFromBase64DataURI, addMetadata, getMetadata } from 'meta-png';

import { IS_DESKTOP, HAS_TOUCH, HAS_CURSOR } from '../../constants/browser.constants';
import { rgbToHex } from '../../utils/color/color.utils';
import { AudioService } from '../../utils/audio/audio.service';
Expand Down Expand Up @@ -514,12 +517,32 @@ export class JsPaint {
}

download() {
const link = document.createElement('A');
this.canvas.toBlob(async (blob) => {
let uintArr = new Uint8Array(await blob.arrayBuffer());

uintArr = addMetadata(uintArr, 'devicePixelRatio', this.scale);
uintArr = addMetadata(uintArr, 'lastModified', Date.now());

const blobWithMetadata = new Blob([uintArr], { type: 'image/png' });

const link = document.createElement('A');

// eslint-disable-next-line max-len
link.download = `${ FILENAMES[Math.floor(Math.random() * FILENAMES.length)] }${ FILENAME_ADJECTIVES[Math.floor(Math.random() * FILENAME_ADJECTIVES.length)] }.png`;
link.href = URL.createObjectURL(blobWithMetadata);
link.target = '_blank';
link.click();
}, 'image/png');

// TODO: Use this as fallback in case the code above throws an error:

link.download = `${ FILENAMES[Math.floor(Math.random() * FILENAMES.length)] }${ FILENAME_ADJECTIVES[Math.floor(Math.random() * FILENAME_ADJECTIVES.length)] }.png`;
link.href = this.canvas.toDataURL();
link.target = '_blank';
link.click();
// const link = document.createElement('A');

// eslint-disable-next-line max-len
// link.download = `${ FILENAMES[Math.floor(Math.random() * FILENAMES.length)] }${ FILENAME_ADJECTIVES[Math.floor(Math.random() * FILENAME_ADJECTIVES.length)] }.png`;
// link.href = this.canvas.toDataURL();
// link.target = '_blank';
// link.click();
}

// DRAW (UPLOAD) IMAGE:
Expand All @@ -540,7 +563,7 @@ export class JsPaint {
const dy = Math.round((window.innerHeight - roundedHeight) / 2 / unit) * unit;

// TODO: Consider setting `scale` globally in JsPaint:
ctx.scale(devicePixelRatio, devicePixelRatio);
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
ctx.drawImage(canvas, dx, dy);
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
Expand Down Expand Up @@ -611,6 +634,9 @@ export class JsPaint {

try {

// TODO: Use a new instance for this:
// AudioService.enable();

for (let y = 0; y < imageHeight; ++y) {
const translatedY = initialY + y;

Expand All @@ -635,8 +661,13 @@ export class JsPaint {
if (paintedPixels % 8 === 0) {
// TODO: Replace the `this.lastDrawingIndex !== randomIndex` with AbortController and signals:

// AudioService.playFreq(50);
// AudioService.resume();

// eslint-disable-next-line no-await-in-loop
await waitOneFrame(1);

// AudioService.stop();
}
}

Expand All @@ -646,6 +677,8 @@ export class JsPaint {
}
}

// AudioService.disable();

// Paint last pixel in the right color:

this.setColor(prevColor);
Expand Down

0 comments on commit c98e891

Please sign in to comment.