Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
**/node_modules
**/dist
**/img
**/vite.config.ts.*.mjs
*.tgz
*.log
Expand Down
Binary file added img/fruit.jpg
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you bring back the original avocado picture I removed?
That one helped me a lot during creating the component due to its simplicity, and..... it's kind of where the logo comes from : )

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/lamp.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 23 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ <h1>🥑</h1>
<div class="dot yellow"></div>
<div class="dot black"></div>
</div>
<img-halftone src="./img/friut.jpeg" shape="circle"></img-halftone>
<img-halftone src="./img/fruit.jpg" shape="circle"></img-halftone>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this... 🤣


<!--
<script type="module" src="https://cdn.skypack.dev/@9am/img-halftone"></script>
Expand All @@ -68,8 +68,28 @@ <h1>🥑</h1>
<script type="module">
import './src/index.ts';

document.querySelector('button').addEventListener('click', () => {
document.querySelector('img-halftone').src = "/img/lamp2.webp";
const halftone = document.querySelector('img-halftone');
const button = document.querySelector('button');

// Listen for all events
halftone.addEventListener('loading', (e) => {
console.log('🎨 Image loading started');
});

halftone.addEventListener('loaded', (e) => {
console.log('✅ Image loaded', e.detail);
});

halftone.addEventListener('canvasready', (e) => {
console.log('🎯 Canvas ready', e.detail);
});

halftone.addEventListener('paintcomplete', (e) => {
console.log('🎉 Painting complete');
});

button.addEventListener('click', () => {
halftone.src = "/img/lamp.jpg";
});
</script>
</body>
Expand Down
69 changes: 53 additions & 16 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,61 @@ class Channel {
this.update(options);
}

private getOrigin() {
private async getOrigin() {
const { source, deg } = this._options;
if (!source) return { origin: new Uint8ClampedArray(), vw: 0, vh: 0 };

// If the image isn't loaded yet, wait for it
if (source instanceof HTMLImageElement && !source.complete) {
await new Promise(resolve => {
source.onload = resolve;
});
}

// prepare canvas
const [w, h] = [source!.width, source!.height];
const [w, h] = [source.width, source.height];
this._angle = Channel.deg2rad(deg);
const cos = Math.cos(this.angle);
const sin = Math.sin(this.angle);
const [vw, vh] = [Math.ceil(w * cos + h * sin), Math.ceil(w * sin + h * cos)];
const cos = Math.abs(Math.cos(this.angle));
const sin = Math.abs(Math.sin(this.angle));

// Calculate dimensions and ensure they're valid positive integers
const vw = Math.max(1, Math.ceil(w * cos + h * sin));
const vh = Math.max(1, Math.ceil(w * sin + h * cos));

// Set canvas dimensions
this._canvas.width = vw;
this._canvas.height = vh;
this.viewBox = [vw, vh];

// prepare ctx
this._ctx.fillStyle = 'white';
this._ctx.fillRect(0, 0, vw, vh);
this._ctx.translate(h * sin, 0);

// Reset transform before applying new transformations
this._ctx.setTransform(1, 0, 0, 1, 0, 0);

// Apply transformations in correct order
this._ctx.translate(vw/2, vh/2);
this._ctx.rotate(this.angle);
// screenshot
this._ctx.drawImage(source!, 0, 0, w, h);
this._ctx.resetTransform();
const { data: origin } = this._ctx.getImageData(0, 0, vw, vh);
return {
origin,
vw,
vh,
};
this._ctx.translate(-w/2, -h/2);

// Draw image
this._ctx.drawImage(source, 0, 0, w, h);

// Reset transform
this._ctx.setTransform(1, 0, 0, 1, 0, 0);

try {
const imageData = this._ctx.getImageData(0, 0, vw, vh);
return {
origin: imageData.data,
vw,
vh,
};
} catch (error) {
console.error('Error getting image data:', error);
return { origin: new Uint8ClampedArray(), vw: 0, vh: 0 };
}
}

async update(options: ChannelOptions) {
Expand All @@ -79,8 +108,16 @@ class Channel {
if (!this._options.source) {
return;
}

// If the image isn't loaded yet, wait for it
if (this._options.source instanceof HTMLImageElement && !this._options.source.complete) {
await new Promise(resolve => {
this._options.source!.onload = resolve;
});
}

const { name, cellSize } = this._options;
const origin = this.getOrigin();
const origin = await this.getOrigin();
const { cells, column, row } = await pool.addTask({
...origin,
name,
Expand Down
51 changes: 47 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,37 @@ class ImgHalftone extends HTMLElement {
}
try {
this.shadowRoot!.host.classList.add('loading');
this.dispatchEvent(new CustomEvent('loading', {
bubbles: true,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm all good with events, but it's better to declare them in const somewhere, what do you think~

composed: true
}));

const img = await ImgHalftone.loadImage(this.src);
img.setAttribute('alt', this.alt);
// replace bg
this.img!.parentNode!.replaceChild(img, this.img!);
this.img = img;

// Emit loaded event
this.dispatchEvent(new CustomEvent('loaded', {
bubbles: true,
composed: true,
detail: { width: img.width, height: img.height }
}));

// limit max pixel
const source = <HTMLImageElement>this.img.cloneNode();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the root cause of the 'img not ready' problem? Not sure how will it act, maybe cloneNode on a loaded img will trigger a reloading of the URL again?
Anyway, if that's the case, will it be better to resolve the issue here by adding a waiting promise like you did below? Thanks for finding this bug, though.

const pixels = source.width * source.height;
source.crossOrigin = 'anonymous'; // Ensure we can read the pixel data
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source is cloned from img, i guess we already have the crossOrigin assigned in static loadImage : )

const pixels = this.img.width * this.img.height;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to use this.img instead of source for the pixels?

const scale = Math.sqrt(max / pixels);
source.width = Math.ceil(source.width * scale);
source.height = Math.ceil(source.height * scale);
source.width = Math.ceil(this.img.width * scale);
source.height = Math.ceil(this.img.height * scale);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above ☝️


// Wait for the cloned image to load
await new Promise((resolve) => {
source.onload = resolve;
source.src = this.img.src;
});

// update
await this.update({ source });
Expand All @@ -90,10 +109,34 @@ class ImgHalftone extends HTMLElement {
private async update({ source }: { source: HTMLImageElement }) {
const size = this.cellsize;
const cellSize: Pair = [size, size];

// Ensure image is loaded before emitting canvas ready event
await new Promise((resolve) => {
if (source.complete) {
resolve(null);
} else {
source.onload = () => resolve(null);
}
});

// Now we can safely emit canvas ready event with correct dimensions
this.dispatchEvent(new CustomEvent('canvasready', {
bubbles: true,
composed: true,
detail: { width: source.width, height: source.height }
}));

await Promise.all(
this.channels.map((channel) => channel.update({ source, cellSize }))
);
this.painter.draw(this.channels, [source!.width, source!.height]);

await this.painter.draw(this.channels, [source.width, source.height]);

// Emit paint complete event
this.dispatchEvent(new CustomEvent('paintcomplete', {
bubbles: true,
composed: true
}));
}

connectedCallback() {
Expand Down