@johnfmorton/some-shade 0.1.0-beta.1 → 0.1.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0-beta.11
4
+
5
+ - Add grid angle option to dot grid effect (`angle` attribute)
6
+ - Add loading blur option (`loading-blur` attribute) — source image displays blurred until the WebGL effect resolves
7
+ - Add `replayTransition()` public method for programmatically replaying the loading transition
8
+
9
+ ## 0.1.0-beta.10
10
+
11
+ - **BREAKING:** Remove pixel sort effect (`pixel-sort`, `threshold`, `sort-direction`, `sort-span` attributes removed)
12
+
13
+ ## 0.1.0-beta.9
14
+
15
+ - Fade transition when processed snapshot replaces the source image on scroll
16
+
17
+ ## 0.1.0-beta.8
18
+
19
+ - Fix mobile crashes when using multiple instances on one page
20
+ - Render-then-snapshot architecture: WebGL context is created, used, and torn down per render instead of held persistently
21
+ - Cap devicePixelRatio at 2 to reduce canvas memory on 3×+ mobile screens
22
+ - Global render queue serialises WebGL across all instances (at most one context at a time)
23
+ - IntersectionObserver defers rendering for off-screen instances until they scroll into view
24
+
25
+ ## 0.1.0-beta.7
26
+
27
+ - Guard custom element registration against double-define
28
+
29
+ ## 0.1.0-beta.6
30
+
31
+ - Publish as `latest` dist-tag to fix missing sidebar links (Repository, Homepage, Issues) on npmjs.com
32
+
33
+ ## 0.1.0-beta.5
34
+
35
+ - Add license, repository, homepage, and bugs fields to package.json
36
+ - Add CHANGELOG.md and LICENSE to published package
37
+
38
+ ## 0.1.0-beta.4
39
+
40
+ - Add publish scripts to monorepo root
41
+ - Add license field to package.json (pending)
42
+
43
+ ## 0.1.0-beta.3
44
+
45
+ - Include README in published npm package
46
+ - Update all docs to use scoped package name `@johnfmorton/some-shade`
47
+
48
+ ## 0.1.0-beta.1
49
+
50
+ - Rename package to `@johnfmorton/some-shade`
51
+ - Display package name and version in playground header
52
+ - Initial publish to npm
53
+
54
+ ## 0.1.0 (unpublished)
55
+
56
+ - CMYK halftone shader effect with per-channel angle control
57
+ - Duotone halftone shader effect with custom color
58
+ - Pixel sort shader effect with configurable direction and threshold
59
+ - Dot grid shader effect with customizable background color
60
+ - Custom effect registration API (`register`, `get`, `list`)
61
+ - WebGL fallback to plain `<img>` when unavailable
62
+ - React playground with live controls and code export
63
+ - GitHub Pages deployment for playground
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # Some Shade
2
+
3
+ WebGL image effects as a web component. Drop `<some-shade-image>` into any page to apply shader-based visual effects to images.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @johnfmorton/some-shade
9
+ ```
10
+
11
+ `lit` is a dependency and will be installed automatically.
12
+
13
+ ## Quick Start
14
+
15
+ ```html
16
+ <script type="module">
17
+ import '@johnfmorton/some-shade';
18
+ </script>
19
+
20
+ <some-shade-image
21
+ src="photo.jpg"
22
+ effect="halftone-cmyk"
23
+ dot-radius="4"
24
+ grid-size="8"
25
+ ></some-shade-image>
26
+ ```
27
+
28
+ ## Effects
29
+
30
+ ### CMYK Halftone
31
+
32
+ Simulates a CMYK halftone print screen with per-channel angle control.
33
+
34
+ ```html
35
+ <some-shade-image
36
+ src="photo.jpg"
37
+ effect="halftone-cmyk"
38
+ dot-radius="4"
39
+ grid-size="8"
40
+ angle-c="15"
41
+ angle-m="75"
42
+ angle-y="0"
43
+ angle-k="45"
44
+ ></some-shade-image>
45
+ ```
46
+
47
+ ### Duotone Halftone
48
+
49
+ Halftone effect using a single custom color.
50
+
51
+ ```html
52
+ <some-shade-image
53
+ src="photo.jpg"
54
+ effect="halftone-duotone"
55
+ dot-radius="4"
56
+ grid-size="8"
57
+ duotone-color="#0099cc"
58
+ angle="0"
59
+ ></some-shade-image>
60
+ ```
61
+
62
+ ### Dot Grid
63
+
64
+ Renders the image as a grid of dots with a customizable background.
65
+
66
+ ```html
67
+ <some-shade-image
68
+ src="photo.jpg"
69
+ effect="dot-grid"
70
+ dot-radius="4"
71
+ grid-size="8"
72
+ dot-offset-x="0.5"
73
+ dot-offset-y="0.5"
74
+ bg-color="#ffffff"
75
+ ></some-shade-image>
76
+ ```
77
+
78
+ ## Attributes Reference
79
+
80
+ | Attribute | Type | Default | Effects |
81
+ |-----------|------|---------|---------|
82
+ | `src` | string | `""` | all |
83
+ | `effect` | string | `"halftone-cmyk"` | all |
84
+ | `dot-radius` | number | `4` | halftone-cmyk, halftone-duotone, dot-grid |
85
+ | `grid-size` | number | `8` | halftone-cmyk, halftone-duotone, dot-grid |
86
+ | `angle-c` | number | `15` | halftone-cmyk |
87
+ | `angle-m` | number | `75` | halftone-cmyk |
88
+ | `angle-y` | number | `0` | halftone-cmyk |
89
+ | `angle-k` | number | `45` | halftone-cmyk |
90
+ | `duotone-color` | string (hex) | `"#0099cc"` | halftone-duotone |
91
+ | `angle` | number | `0` | halftone-duotone |
92
+ | `dot-offset-x` | number | `0.5` | dot-grid |
93
+ | `dot-offset-y` | number | `0.5` | dot-grid |
94
+ | `bg-color` | string (hex) | `"#ffffff"` | dot-grid |
95
+
96
+ ## Framework Usage
97
+
98
+ The component works in any framework. Import `@johnfmorton/some-shade` once to register the custom element, then use `<some-shade-image>` in your templates.
99
+
100
+ ### React
101
+
102
+ ```tsx
103
+ import '@johnfmorton/some-shade';
104
+
105
+ declare global {
106
+ namespace JSX {
107
+ interface IntrinsicElements {
108
+ 'some-shade-image': React.DetailedHTMLProps<
109
+ React.HTMLAttributes<HTMLElement> & {
110
+ src?: string;
111
+ effect?: string;
112
+ 'dot-radius'?: number;
113
+ 'grid-size'?: number;
114
+ },
115
+ HTMLElement
116
+ >;
117
+ }
118
+ }
119
+ }
120
+
121
+ function App() {
122
+ return (
123
+ <some-shade-image
124
+ src="photo.jpg"
125
+ effect="halftone-cmyk"
126
+ dot-radius={4}
127
+ grid-size={8}
128
+ />
129
+ );
130
+ }
131
+ ```
132
+
133
+ ## Custom Effects
134
+
135
+ Register your own shader effects using the `register()` API:
136
+
137
+ ```ts
138
+ import { register } from '@johnfmorton/some-shade';
139
+
140
+ register({
141
+ name: 'my-effect',
142
+ vertexShader: `
143
+ attribute vec2 a_position;
144
+ attribute vec2 a_texCoord;
145
+ varying vec2 v_texCoord;
146
+ void main() {
147
+ gl_Position = vec4(a_position, 0.0, 1.0);
148
+ v_texCoord = a_texCoord;
149
+ }
150
+ `,
151
+ fragmentShader: `
152
+ precision mediump float;
153
+ uniform sampler2D u_image;
154
+ varying vec2 v_texCoord;
155
+ uniform float u_intensity;
156
+ void main() {
157
+ gl_FragColor = texture2D(u_image, v_texCoord) * u_intensity;
158
+ }
159
+ `,
160
+ uniforms: [
161
+ { name: 'u_intensity', type: 'float', default: 1.0, attribute: 'intensity' },
162
+ ],
163
+ });
164
+ ```
165
+
166
+ Then use it:
167
+
168
+ ```html
169
+ <some-shade-image src="photo.jpg" effect="my-effect" intensity="0.8"></some-shade-image>
170
+ ```
171
+
172
+ ### `EffectDefinition`
173
+
174
+ ```ts
175
+ interface EffectDefinition {
176
+ name: string;
177
+ fragmentShader: string;
178
+ vertexShader: string;
179
+ uniforms: UniformDefinition[];
180
+ }
181
+
182
+ interface UniformDefinition {
183
+ name: string;
184
+ type: 'float' | 'vec2' | 'vec3' | 'vec4';
185
+ default: number | number[];
186
+ attribute?: string;
187
+ }
188
+ ```
189
+
190
+ ## Programmatic API
191
+
192
+ ```ts
193
+ import { register, get, list, SomeShadeImage } from '@johnfmorton/some-shade';
194
+
195
+ register(effectDef); // Register a custom effect
196
+ get('halftone-cmyk'); // Get an effect definition by name
197
+ list(); // List all registered effect names
198
+ ```
199
+
200
+ `SomeShadeImage` is the Lit component class, exported for subclassing or direct use.
201
+
202
+ ## Browser Support
203
+
204
+ Requires WebGL. If WebGL is unavailable, the component falls back to a plain `<img>` element.
205
+
206
+ ## Development
207
+
208
+ ```bash
209
+ pnpm install
210
+ pnpm dev # Watch mode (component + playground)
211
+ pnpm build # Build everything
212
+ pnpm build:component # Build web component only
213
+ ```
214
+
215
+ ## License
216
+
217
+ ISC
package/dist/index.d.ts CHANGED
@@ -28,34 +28,31 @@ export declare class SomeShadeImage extends LitElement {
28
28
  angleK: number;
29
29
  duotoneColor: string;
30
30
  angle: number;
31
- threshold: number;
32
- sortDirection: number;
33
- sortSpan: number;
34
31
  dotOffsetX: number;
35
32
  dotOffsetY: number;
36
33
  bgColor: string;
34
+ loadingBlur: number;
37
35
  private _webglAvailable;
38
- private _canvas;
39
- private _gl;
40
- private _programInfo;
41
- private _textureInfo;
42
- private _quadBuffer;
43
- private _currentEffect;
36
+ private _snapshotUrl;
37
+ private _snapshotLoaded;
44
38
  private _image;
45
- private _resizeObserver;
39
+ private _observer;
40
+ private _visible;
41
+ private _needsRender;
46
42
  render(): TemplateResult<1>;
47
- firstUpdated(): void;
43
+ connectedCallback(): void;
48
44
  updated(changed: PropertyValues): void;
49
45
  disconnectedCallback(): void;
50
46
  private _loadImage;
51
- private _uploadTexture;
52
- private _sizeCanvas;
53
- private _handleResize;
54
- private _setupProgram;
47
+ private _scheduleRender;
48
+ private _renderEffect;
49
+ private _onSnapshotLoad;
50
+ /** Hide the rendered snapshot momentarily, then fade it back in.
51
+ * Useful for previewing the loading-blur transition. */
52
+ replayTransition(delay?: number): void;
53
+ private _revokeSnapshot;
55
54
  private _getUniformValues;
56
55
  private _parseHexColor;
57
- private _renderFrame;
58
- private _cleanup;
59
56
  }
60
57
 
61
58
  export declare interface UniformDefinition {
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("lit"),i=require("lit/decorators.js");function y(e){return e.getContext("webgl",{alpha:!0,premultipliedAlpha:!1,preserveDrawingBuffer:!0})}function p(e,t,o){const a=e.createShader(t);if(!a)throw new Error("Failed to create shader");if(e.shaderSource(a,o),e.compileShader(a),!e.getShaderParameter(a,e.COMPILE_STATUS)){const n=e.getShaderInfoLog(a);throw e.deleteShader(a),new Error(`Shader compile error: ${n}`)}return a}function x(e,t,o){const a=p(e,e.VERTEX_SHADER,t),n=p(e,e.FRAGMENT_SHADER,o),r=e.createProgram();if(!r)throw new Error("Failed to create program");if(e.attachShader(r,a),e.attachShader(r,n),e.linkProgram(r),!e.getProgramParameter(r,e.LINK_STATUS)){const f=e.getProgramInfoLog(r);throw e.deleteProgram(r),new Error(`Program link error: ${f}`)}e.deleteShader(a),e.deleteShader(n);const u=new Map,b=e.getProgramParameter(r,e.ACTIVE_ATTRIBUTES);for(let f=0;f<b;f++){const l=e.getActiveAttrib(r,f);l&&u.set(l.name,e.getAttribLocation(r,l.name))}const _=new Map,S=e.getProgramParameter(r,e.ACTIVE_UNIFORMS);for(let f=0;f<S;f++){const l=e.getActiveUniform(r,f);if(l){const g=e.getUniformLocation(r,l.name);g&&_.set(l.name,g)}}return{program:r,attribLocations:u,uniformLocations:_}}function E(e,t,o){for(const[a,n]of Object.entries(o)){const r=t.uniformLocations.get(a);if(r){if(typeof n=="number")e.uniform1f(r,n);else if(Array.isArray(n))switch(n.length){case 2:e.uniform2fv(r,n);break;case 3:e.uniform3fv(r,n);break;case 4:e.uniform4fv(r,n);break}}}}function C(e,t){const o=e.createTexture();if(!o)throw new Error("Failed to create texture");return e.bindTexture(e.TEXTURE_2D,o),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),{texture:o,width:t.naturalWidth,height:t.naturalHeight}}function I(e,t){const o=new Float32Array([-1,-1,0,1,1,-1,1,1,-1,1,0,0,1,1,1,0]),a=e.createBuffer();if(!a)throw new Error("Failed to create buffer");e.bindBuffer(e.ARRAY_BUFFER,a),e.bufferData(e.ARRAY_BUFFER,o,e.STATIC_DRAW);const n=4*Float32Array.BYTES_PER_ELEMENT,r=t.attribLocations.get("a_position");r!==void 0&&r!==-1&&(e.enableVertexAttribArray(r),e.vertexAttribPointer(r,2,e.FLOAT,!1,n,0));const u=t.attribLocations.get("a_texCoord");return u!==void 0&&u!==-1&&(e.enableVertexAttribArray(u),e.vertexAttribPointer(u,2,e.FLOAT,!1,n,2*Float32Array.BYTES_PER_ELEMENT)),a}function R(e){e.drawArrays(e.TRIANGLE_STRIP,0,4)}const T=`precision mediump float;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=require("lit"),u=require("lit/decorators.js");function w(t){return t.getContext("webgl",{alpha:!0,premultipliedAlpha:!1,preserveDrawingBuffer:!0})}function x(t,e,a){const o=t.createShader(e);if(!o)throw new Error("Failed to create shader");if(t.shaderSource(o,a),t.compileShader(o),!t.getShaderParameter(o,t.COMPILE_STATUS)){const i=t.getShaderInfoLog(o);throw t.deleteShader(o),new Error(`Shader compile error: ${i}`)}return o}function U(t,e,a){const o=x(t,t.VERTEX_SHADER,e),i=x(t,t.FRAGMENT_SHADER,a),n=t.createProgram();if(!n)throw new Error("Failed to create program");if(t.attachShader(n,o),t.attachShader(n,i),t.linkProgram(n),!t.getProgramParameter(n,t.LINK_STATUS)){const d=t.getProgramInfoLog(n);throw t.deleteProgram(n),new Error(`Program link error: ${d}`)}t.deleteShader(o),t.deleteShader(i);const r=new Map,p=t.getProgramParameter(n,t.ACTIVE_ATTRIBUTES);for(let d=0;d<p;d++){const c=t.getActiveAttrib(n,d);c&&r.set(c.name,t.getAttribLocation(n,c.name))}const f=new Map,h=t.getProgramParameter(n,t.ACTIVE_UNIFORMS);for(let d=0;d<h;d++){const c=t.getActiveUniform(n,d);if(c){const _=t.getUniformLocation(n,c.name);_&&f.set(c.name,_)}}return{program:n,attribLocations:r,uniformLocations:f}}function P(t,e,a){for(const[o,i]of Object.entries(a)){const n=e.uniformLocations.get(o);if(n){if(typeof i=="number")t.uniform1f(n,i);else if(Array.isArray(i))switch(i.length){case 2:t.uniform2fv(n,i);break;case 3:t.uniform3fv(n,i);break;case 4:t.uniform4fv(n,i);break}}}}function k(t,e){const a=t.createTexture();if(!a)throw new Error("Failed to create texture");return t.bindTexture(t.TEXTURE_2D,a),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,t.RGBA,t.UNSIGNED_BYTE,e),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.LINEAR),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.LINEAR),{texture:a,width:e.naturalWidth,height:e.naturalHeight}}function D(t,e){const a=new Float32Array([-1,-1,0,1,1,-1,1,1,-1,1,0,0,1,1,1,0]),o=t.createBuffer();if(!o)throw new Error("Failed to create buffer");t.bindBuffer(t.ARRAY_BUFFER,o),t.bufferData(t.ARRAY_BUFFER,a,t.STATIC_DRAW);const i=4*Float32Array.BYTES_PER_ELEMENT,n=e.attribLocations.get("a_position");n!==void 0&&n!==-1&&(t.enableVertexAttribArray(n),t.vertexAttribPointer(n,2,t.FLOAT,!1,i,0));const r=e.attribLocations.get("a_texCoord");return r!==void 0&&r!==-1&&(t.enableVertexAttribArray(r),t.vertexAttribPointer(r,2,t.FLOAT,!1,i,2*Float32Array.BYTES_PER_ELEMENT)),o}function O(t){t.drawArrays(t.TRIANGLE_STRIP,0,4)}const z=`precision mediump float;
2
2
 
3
3
  varying vec2 v_texCoord;
4
4
 
@@ -54,7 +54,7 @@ void main() {
54
54
 
55
55
  gl_FragColor = vec4(outR, outG, outB, color.a);
56
56
  }
57
- `,h=`attribute vec2 a_position;
57
+ `,y=`attribute vec2 a_position;
58
58
  attribute vec2 a_texCoord;
59
59
  varying vec2 v_texCoord;
60
60
 
@@ -62,7 +62,7 @@ void main() {
62
62
  gl_Position = vec4(a_position, 0.0, 1.0);
63
63
  v_texCoord = a_texCoord;
64
64
  }
65
- `,A={name:"halftone-cmyk",fragmentShader:T,vertexShader:h,uniforms:[{name:"u_dotRadius",type:"float",default:4,attribute:"dot-radius"},{name:"u_gridSize",type:"float",default:8,attribute:"grid-size"},{name:"u_angleC",type:"float",default:15,attribute:"angle-c"},{name:"u_angleM",type:"float",default:75,attribute:"angle-m"},{name:"u_angleY",type:"float",default:0,attribute:"angle-y"},{name:"u_angleK",type:"float",default:45,attribute:"angle-k"}]},w=`precision mediump float;
65
+ `,F={name:"halftone-cmyk",fragmentShader:z,vertexShader:y,uniforms:[{name:"u_dotRadius",type:"float",default:4,attribute:"dot-radius"},{name:"u_gridSize",type:"float",default:8,attribute:"grid-size"},{name:"u_angleC",type:"float",default:15,attribute:"angle-c"},{name:"u_angleM",type:"float",default:75,attribute:"angle-m"},{name:"u_angleY",type:"float",default:0,attribute:"angle-y"},{name:"u_angleK",type:"float",default:45,attribute:"angle-k"}]},B=`precision mediump float;
66
66
 
67
67
  varying vec2 v_texCoord;
68
68
 
@@ -97,76 +97,7 @@ void main() {
97
97
 
98
98
  gl_FragColor = vec4(result, color.a);
99
99
  }
100
- `,U={name:"halftone-duotone",fragmentShader:w,vertexShader:h,uniforms:[{name:"u_dotRadius",type:"float",default:4,attribute:"dot-radius"},{name:"u_gridSize",type:"float",default:8,attribute:"grid-size"},{name:"u_duotoneColor",type:"vec3",default:[0,.6,.8],attribute:"duotone-color"},{name:"u_angle",type:"float",default:0,attribute:"angle"}]},P=`precision mediump float;
101
-
102
- varying vec2 v_texCoord;
103
-
104
- uniform sampler2D u_image;
105
- uniform vec2 u_resolution;
106
- uniform float u_threshold;
107
- uniform float u_direction;
108
- uniform float u_span;
109
-
110
- float brightness(vec3 c) {
111
- return dot(c, vec3(0.299, 0.587, 0.114));
112
- }
113
-
114
- void main() {
115
- vec2 uv = v_texCoord;
116
- vec4 color = texture2D(u_image, uv);
117
- float bri = brightness(color.rgb);
118
-
119
- // Boundary pixel — pass through
120
- if (bri < u_threshold) {
121
- gl_FragColor = color;
122
- return;
123
- }
124
-
125
- float rad = radians(u_direction);
126
- vec2 dir = vec2(cos(rad), sin(rad));
127
- vec2 step = dir / u_resolution;
128
-
129
- int spanLen = int(u_span);
130
-
131
- // Walk backward to find span start
132
- int backCount = 0;
133
- for (int i = 1; i < 256; i++) {
134
- if (i >= spanLen) break;
135
- vec2 sampleUV = uv - step * float(i);
136
- if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) break;
137
- float b = brightness(texture2D(u_image, sampleUV).rgb);
138
- if (b < u_threshold) break;
139
- backCount++;
140
- }
141
-
142
- // Walk forward to find span end
143
- int fwdCount = 0;
144
- for (int i = 1; i < 256; i++) {
145
- if (i >= spanLen) break;
146
- vec2 sampleUV = uv + step * float(i);
147
- if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) break;
148
- float b = brightness(texture2D(u_image, sampleUV).rgb);
149
- if (b < u_threshold) break;
150
- fwdCount++;
151
- }
152
-
153
- int totalSpan = backCount + 1 + fwdCount;
154
- vec2 spanStartUV = uv - step * float(backCount);
155
-
156
- // Count pixels in span that are darker than current (= rank)
157
- int rank = 0;
158
- for (int i = 0; i < 256; i++) {
159
- if (i >= totalSpan) break;
160
- vec2 sampleUV = spanStartUV + step * float(i);
161
- float b = brightness(texture2D(u_image, sampleUV).rgb);
162
- if (b < bri) rank++;
163
- }
164
-
165
- // Resample at sorted position
166
- vec2 sortedUV = spanStartUV + step * float(rank);
167
- gl_FragColor = texture2D(u_image, sortedUV);
168
- }
169
- `,D={name:"pixel-sort",fragmentShader:P,vertexShader:h,uniforms:[{name:"u_threshold",type:"float",default:.5,attribute:"threshold"},{name:"u_direction",type:"float",default:0,attribute:"sort-direction"},{name:"u_span",type:"float",default:64,attribute:"sort-span"}]},k=`precision mediump float;
100
+ `,M={name:"halftone-duotone",fragmentShader:B,vertexShader:y,uniforms:[{name:"u_dotRadius",type:"float",default:4,attribute:"dot-radius"},{name:"u_gridSize",type:"float",default:8,attribute:"grid-size"},{name:"u_duotoneColor",type:"vec3",default:[0,.6,.8],attribute:"duotone-color"},{name:"u_angle",type:"float",default:0,attribute:"angle"}]},I=`precision mediump float;
170
101
 
171
102
  varying vec2 v_texCoord;
172
103
 
@@ -176,31 +107,42 @@ uniform float u_dotRadius;
176
107
  uniform float u_gridSize;
177
108
  uniform vec2 u_dotOffset;
178
109
  uniform vec3 u_bgColor;
110
+ uniform float u_angle;
179
111
 
180
112
  void main() {
181
113
  vec2 uv = v_texCoord * u_resolution;
182
114
 
183
- // Which grid cell this fragment belongs to
184
- vec2 cell = floor(uv / u_gridSize);
115
+ // Rotate grid
116
+ float rad = radians(u_angle);
117
+ float s = sin(rad);
118
+ float c = cos(rad);
119
+ mat2 rot = mat2(c, -s, s, c);
120
+ mat2 invRot = mat2(c, s, -s, c);
121
+ vec2 rotUV = rot * uv;
122
+
123
+ // Which grid cell this fragment belongs to (in rotated space)
124
+ vec2 cell = floor(rotUV / u_gridSize);
185
125
 
186
- // Cell origin in pixel space
126
+ // Cell origin in rotated space
187
127
  vec2 cellOrigin = cell * u_gridSize;
188
128
 
189
129
  // Dot center within the cell, shifted by u_dotOffset (0–1 range)
190
130
  vec2 dotCenter = cellOrigin + u_dotOffset * u_gridSize;
191
131
 
192
132
  // 4×4 multi-sample average color across the cell
133
+ // Sample points are computed in rotated space then un-rotated for texture lookup
193
134
  vec3 avg = vec3(0.0);
194
135
  for (int y = 0; y < 4; y++) {
195
136
  for (int x = 0; x < 4; x++) {
196
- vec2 sampleUV = (cellOrigin + (vec2(float(x), float(y)) + 0.5) * (u_gridSize / 4.0)) / u_resolution;
137
+ vec2 rotSample = cellOrigin + (vec2(float(x), float(y)) + 0.5) * (u_gridSize / 4.0);
138
+ vec2 sampleUV = (invRot * rotSample) / u_resolution;
197
139
  avg += texture2D(u_image, sampleUV).rgb;
198
140
  }
199
141
  }
200
142
  avg /= 16.0;
201
143
 
202
- // Wrapped (toroidal) distance from fragment to dot center within the cell
203
- vec2 d = (uv - dotCenter) / u_gridSize;
144
+ // Wrapped (toroidal) distance from fragment to dot center (in rotated space)
145
+ vec2 d = (rotUV - dotCenter) / u_gridSize;
204
146
  d = fract(d + 0.5) - 0.5; // wrap to [-0.5, 0.5]
205
147
  float dist = length(d) * u_gridSize;
206
148
 
@@ -211,15 +153,33 @@ void main() {
211
153
 
212
154
  gl_FragColor = vec4(result, 1.0);
213
155
  }
214
- `,L={name:"dot-grid",fragmentShader:k,vertexShader:h,uniforms:[{name:"u_dotRadius",type:"float",default:4,attribute:"dot-radius"},{name:"u_gridSize",type:"float",default:8,attribute:"grid-size"},{name:"u_dotOffset",type:"vec2",default:[.5,.5],attribute:"dot-offset"},{name:"u_bgColor",type:"vec3",default:[1,1,1],attribute:"bg-color"}]},m=new Map;function d(e){m.set(e.name,e)}function v(e){return m.get(e)}function z(){return Array.from(m.keys())}d(A);d(U);d(D);d(L);var F=Object.defineProperty,O=Object.getOwnPropertyDescriptor,s=(e,t,o,a)=>{for(var n=a>1?void 0:a?O(t,o):t,r=e.length-1,u;r>=0;r--)(u=e[r])&&(n=(a?u(t,o,n):u(n))||n);return a&&n&&F(t,o,n),n};exports.SomeShadeImage=class extends c.LitElement{constructor(){super(...arguments),this.src="",this.effect="halftone-cmyk",this.dotRadius=4,this.gridSize=8,this.angleC=15,this.angleM=75,this.angleY=0,this.angleK=45,this.duotoneColor="#0099cc",this.angle=0,this.threshold=.5,this.sortDirection=0,this.sortSpan=64,this.dotOffsetX=.5,this.dotOffsetY=.5,this.bgColor="#ffffff",this._webglAvailable=!0,this._canvas=null,this._gl=null,this._programInfo=null,this._textureInfo=null,this._quadBuffer=null,this._currentEffect=null,this._image=null,this._resizeObserver=null}render(){return this._webglAvailable?c.html`<canvas></canvas>`:c.html`<img .src=${this.src} alt="" />`}firstUpdated(){if(this._webglAvailable&&(this._canvas=this.shadowRoot.querySelector("canvas"),!!this._canvas)){if(this._gl=y(this._canvas),!this._gl){this._webglAvailable=!1,this.classList.add("webgl-unavailable");return}this._resizeObserver=new ResizeObserver(()=>this._handleResize()),this._resizeObserver.observe(this),this.src&&this._loadImage(this.src)}}updated(t){if(this._gl){if(t.has("src")&&this.src){this._loadImage(this.src);return}if(t.has("effect")){this._setupProgram(),this._renderFrame();return}this._renderFrame()}}disconnectedCallback(){var t;super.disconnectedCallback(),(t=this._resizeObserver)==null||t.disconnect(),this._cleanup()}_loadImage(t){const o=new Image;o.crossOrigin="anonymous",o.onload=()=>{this._image=o,this._uploadTexture(),this._sizeCanvas(),this._setupProgram(),this._renderFrame()},o.onerror=()=>{console.warn(`[some-shade] Failed to load image: ${t}`)},o.src=t}_uploadTexture(){!this._gl||!this._image||(this._textureInfo&&this._gl.deleteTexture(this._textureInfo.texture),this._textureInfo=C(this._gl,this._image))}_sizeCanvas(){if(!this._canvas||!this._textureInfo)return;const t=window.devicePixelRatio||1,o=this._textureInfo.width,a=this._textureInfo.height;this._canvas.width=o*t,this._canvas.height=a*t,this._canvas.style.aspectRatio=`${o} / ${a}`}_handleResize(){this._renderFrame()}_setupProgram(){if(!this._gl)return;const t=v(this.effect);if(!t){console.warn(`[some-shade] Unknown effect: ${this.effect}`);return}this._programInfo&&this._gl.deleteProgram(this._programInfo.program),this._currentEffect=t,this._programInfo=x(this._gl,t.vertexShader,t.fragmentShader),this._quadBuffer&&this._gl.deleteBuffer(this._quadBuffer),this._gl.useProgram(this._programInfo.program),this._quadBuffer=I(this._gl,this._programInfo)}_getUniformValues(){const t={};return this._textureInfo&&(t.u_resolution=[this._textureInfo.width*(window.devicePixelRatio||1),this._textureInfo.height*(window.devicePixelRatio||1)],t.u_dotRadius=this.dotRadius,t.u_gridSize=this.gridSize,this.effect==="halftone-cmyk"?(t.u_angleC=this.angleC,t.u_angleM=this.angleM,t.u_angleY=this.angleY,t.u_angleK=this.angleK):this.effect==="halftone-duotone"?(t.u_duotoneColor=this._parseHexColor(this.duotoneColor),t.u_angle=this.angle):this.effect==="pixel-sort"?(t.u_threshold=this.threshold,t.u_direction=this.sortDirection,t.u_span=this.sortSpan):this.effect==="dot-grid"&&(t.u_dotOffset=[this.dotOffsetX,this.dotOffsetY],t.u_bgColor=this._parseHexColor(this.bgColor))),t}_parseHexColor(t){const o=t.replace("#",""),a=parseInt(o.substring(0,2),16)/255,n=parseInt(o.substring(2,4),16)/255,r=parseInt(o.substring(4,6),16)/255;return[a,n,r]}_renderFrame(){const t=this._gl;if(!t||!this._programInfo||!this._textureInfo||!this._canvas)return;t.viewport(0,0,this._canvas.width,this._canvas.height),t.clearColor(0,0,0,0),t.clear(t.COLOR_BUFFER_BIT),t.useProgram(this._programInfo.program),t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,this._textureInfo.texture);const o=this._programInfo.uniformLocations.get("u_image");o&&t.uniform1i(o,0),E(t,this._programInfo,this._getUniformValues()),t.bindBuffer(t.ARRAY_BUFFER,this._quadBuffer);const a=4*Float32Array.BYTES_PER_ELEMENT,n=this._programInfo.attribLocations.get("a_position");n!==void 0&&n!==-1&&(t.enableVertexAttribArray(n),t.vertexAttribPointer(n,2,t.FLOAT,!1,a,0));const r=this._programInfo.attribLocations.get("a_texCoord");r!==void 0&&r!==-1&&(t.enableVertexAttribArray(r),t.vertexAttribPointer(r,2,t.FLOAT,!1,a,2*Float32Array.BYTES_PER_ELEMENT)),R(t)}_cleanup(){this._gl&&(this._textureInfo&&this._gl.deleteTexture(this._textureInfo.texture),this._programInfo&&this._gl.deleteProgram(this._programInfo.program),this._quadBuffer&&this._gl.deleteBuffer(this._quadBuffer),this._gl=null,this._programInfo=null,this._textureInfo=null,this._quadBuffer=null)}};exports.SomeShadeImage.styles=c.css`
156
+ `,N={name:"dot-grid",fragmentShader:I,vertexShader:y,uniforms:[{name:"u_dotRadius",type:"float",default:4,attribute:"dot-radius"},{name:"u_gridSize",type:"float",default:8,attribute:"grid-size"},{name:"u_dotOffset",type:"vec2",default:[.5,.5],attribute:"dot-offset"},{name:"u_bgColor",type:"vec3",default:[1,1,1],attribute:"bg-color"},{name:"u_angle",type:"float",default:0,attribute:"angle"}]},R=new Map;function b(t){R.set(t.name,t)}function A(t){return R.get(t)}function V(){return Array.from(R.keys())}b(F);b(M);b(N);var Y=Object.defineProperty,l=(t,e,a,o)=>{for(var i=void 0,n=t.length-1,r;n>=0;n--)(r=t[n])&&(i=r(e,a,i)||i);return i&&Y(e,a,i),i};const K=2;let C=Promise.resolve();function T(t){const e=C.then(t,t);return C=e,e}const X=new Set(["effect","dotRadius","gridSize","angleC","angleM","angleY","angleK","duotoneColor","angle","dotOffsetX","dotOffsetY","bgColor"]),E=class E extends m.LitElement{constructor(){super(...arguments),this.src="",this.effect="halftone-cmyk",this.dotRadius=4,this.gridSize=8,this.angleC=15,this.angleM=75,this.angleY=0,this.angleK=45,this.duotoneColor="#0099cc",this.angle=0,this.dotOffsetX=.5,this.dotOffsetY=.5,this.bgColor="#ffffff",this.loadingBlur=0,this._webglAvailable=!0,this._snapshotUrl="",this._snapshotLoaded=!1,this._image=null,this._observer=null,this._visible=!1,this._needsRender=!1}render(){if(!this._webglAvailable)return m.html`<img src=${this.src} alt="" />`;const e=this.loadingBlur>0?`filter: blur(${this.loadingBlur}px)`:"";return m.html`
157
+ <img src=${this.src} alt="" style=${e} />
158
+ ${this._snapshotUrl?m.html`<img
159
+ class="snapshot${this._snapshotLoaded?" loaded":""}"
160
+ src=${this._snapshotUrl}
161
+ @load=${this._onSnapshotLoad}
162
+ alt="" />`:""}
163
+ `}connectedCallback(){super.connectedCallback(),this._observer=new IntersectionObserver(e=>{var o;const a=this._visible;this._visible=((o=e[0])==null?void 0:o.isIntersecting)??!1,this._visible&&!a&&this._needsRender&&(this._needsRender=!1,T(()=>this._renderEffect()))},{rootMargin:"200px"}),this._observer.observe(this)}updated(e){if(e.has("src")&&this.src){this._loadImage(this.src);return}if(!this._image)return;[...e.keys()].some(o=>X.has(o))&&this._scheduleRender()}disconnectedCallback(){var e;super.disconnectedCallback(),(e=this._observer)==null||e.disconnect(),this._revokeSnapshot()}_loadImage(e){const a=new Image;a.crossOrigin="anonymous",a.onload=()=>{this._image=a,this._scheduleRender()},a.onerror=()=>{console.warn(`[some-shade] Failed to load image: ${e}`)},a.src=e}_scheduleRender(){this._visible?T(()=>this._renderEffect()):this._needsRender=!0}async _renderEffect(){var p;if(!this._image)return;const e=A(this.effect);if(!e){console.warn(`[some-shade] Unknown effect: ${this.effect}`);return}const a=Math.min(window.devicePixelRatio||1,K),o=this._image.naturalWidth,i=this._image.naturalHeight,n=document.createElement("canvas");n.width=o*a,n.height=i*a;const r=w(n);if(!r){this._webglAvailable=!1;return}try{const f=U(r,e.vertexShader,e.fragmentShader);r.useProgram(f.program);const h=k(r,this._image),d=D(r,f);r.viewport(0,0,n.width,n.height),r.clearColor(0,0,0,0),r.clear(r.COLOR_BUFFER_BIT),r.activeTexture(r.TEXTURE0),r.bindTexture(r.TEXTURE_2D,h.texture);const c=f.uniformLocations.get("u_image");c&&r.uniform1i(c,0),P(r,f,this._getUniformValues(h,a)),r.bindBuffer(r.ARRAY_BUFFER,d);const _=4*Float32Array.BYTES_PER_ELEMENT,g=f.attribLocations.get("a_position");g!==void 0&&g!==-1&&(r.enableVertexAttribArray(g),r.vertexAttribPointer(g,2,r.FLOAT,!1,_,0));const v=f.attribLocations.get("a_texCoord");v!==void 0&&v!==-1&&(r.enableVertexAttribArray(v),r.vertexAttribPointer(v,2,r.FLOAT,!1,_,2*Float32Array.BYTES_PER_ELEMENT)),O(r);const S=await new Promise(L=>n.toBlob(L));r.deleteTexture(h.texture),r.deleteProgram(f.program),r.deleteBuffer(d),S&&(this._snapshotLoaded=!1,this._revokeSnapshot(),this._snapshotUrl=URL.createObjectURL(S))}finally{(p=r.getExtension("WEBGL_lose_context"))==null||p.loseContext()}}_onSnapshotLoad(){this._snapshotLoaded=!0}replayTransition(e=500){this._snapshotUrl&&(this._snapshotLoaded=!1,this.updateComplete.then(()=>{setTimeout(()=>{this._snapshotLoaded=!0},e)}))}_revokeSnapshot(){this._snapshotUrl&&(URL.revokeObjectURL(this._snapshotUrl),this._snapshotUrl="")}_getUniformValues(e,a){const o={};return o.u_resolution=[e.width*a,e.height*a],o.u_dotRadius=this.dotRadius,o.u_gridSize=this.gridSize,this.effect==="halftone-cmyk"?(o.u_angleC=this.angleC,o.u_angleM=this.angleM,o.u_angleY=this.angleY,o.u_angleK=this.angleK):this.effect==="halftone-duotone"?(o.u_duotoneColor=this._parseHexColor(this.duotoneColor),o.u_angle=this.angle):this.effect==="dot-grid"&&(o.u_dotOffset=[this.dotOffsetX,this.dotOffsetY],o.u_bgColor=this._parseHexColor(this.bgColor),o.u_angle=this.angle),o}_parseHexColor(e){const a=e.replace("#",""),o=parseInt(a.substring(0,2),16)/255,i=parseInt(a.substring(2,4),16)/255,n=parseInt(a.substring(4,6),16)/255;return[o,i,n]}};E.styles=m.css`
215
164
  :host {
216
165
  display: block;
217
166
  position: relative;
218
167
  overflow: hidden;
219
168
  }
220
- canvas, img {
169
+ img {
221
170
  display: block;
222
171
  width: 100%;
223
172
  height: auto;
224
173
  }
225
- `;s([i.property()],exports.SomeShadeImage.prototype,"src",2);s([i.property()],exports.SomeShadeImage.prototype,"effect",2);s([i.property({type:Number,attribute:"dot-radius"})],exports.SomeShadeImage.prototype,"dotRadius",2);s([i.property({type:Number,attribute:"grid-size"})],exports.SomeShadeImage.prototype,"gridSize",2);s([i.property({type:Number,attribute:"angle-c"})],exports.SomeShadeImage.prototype,"angleC",2);s([i.property({type:Number,attribute:"angle-m"})],exports.SomeShadeImage.prototype,"angleM",2);s([i.property({type:Number,attribute:"angle-y"})],exports.SomeShadeImage.prototype,"angleY",2);s([i.property({type:Number,attribute:"angle-k"})],exports.SomeShadeImage.prototype,"angleK",2);s([i.property({attribute:"duotone-color"})],exports.SomeShadeImage.prototype,"duotoneColor",2);s([i.property({type:Number})],exports.SomeShadeImage.prototype,"angle",2);s([i.property({type:Number})],exports.SomeShadeImage.prototype,"threshold",2);s([i.property({type:Number,attribute:"sort-direction"})],exports.SomeShadeImage.prototype,"sortDirection",2);s([i.property({type:Number,attribute:"sort-span"})],exports.SomeShadeImage.prototype,"sortSpan",2);s([i.property({type:Number,attribute:"dot-offset-x"})],exports.SomeShadeImage.prototype,"dotOffsetX",2);s([i.property({type:Number,attribute:"dot-offset-y"})],exports.SomeShadeImage.prototype,"dotOffsetY",2);s([i.property({attribute:"bg-color"})],exports.SomeShadeImage.prototype,"bgColor",2);s([i.state()],exports.SomeShadeImage.prototype,"_webglAvailable",2);exports.SomeShadeImage=s([i.customElement("some-shade-image")],exports.SomeShadeImage);exports.get=v;exports.list=z;exports.register=d;
174
+ img.snapshot {
175
+ position: absolute;
176
+ inset: 0;
177
+ width: 100%;
178
+ height: 100%;
179
+ opacity: 0;
180
+ transition: opacity 0.3s ease;
181
+ }
182
+ img.snapshot.loaded {
183
+ opacity: 1;
184
+ }
185
+ `;let s=E;l([u.property()],s.prototype,"src");l([u.property()],s.prototype,"effect");l([u.property({type:Number,attribute:"dot-radius"})],s.prototype,"dotRadius");l([u.property({type:Number,attribute:"grid-size"})],s.prototype,"gridSize");l([u.property({type:Number,attribute:"angle-c"})],s.prototype,"angleC");l([u.property({type:Number,attribute:"angle-m"})],s.prototype,"angleM");l([u.property({type:Number,attribute:"angle-y"})],s.prototype,"angleY");l([u.property({type:Number,attribute:"angle-k"})],s.prototype,"angleK");l([u.property({attribute:"duotone-color"})],s.prototype,"duotoneColor");l([u.property({type:Number})],s.prototype,"angle");l([u.property({type:Number,attribute:"dot-offset-x"})],s.prototype,"dotOffsetX");l([u.property({type:Number,attribute:"dot-offset-y"})],s.prototype,"dotOffsetY");l([u.property({attribute:"bg-color"})],s.prototype,"bgColor");l([u.property({type:Number,attribute:"loading-blur"})],s.prototype,"loadingBlur");l([u.state()],s.prototype,"_webglAvailable");l([u.state()],s.prototype,"_snapshotUrl");l([u.state()],s.prototype,"_snapshotLoaded");customElements.get("some-shade-image")||customElements.define("some-shade-image",s);exports.SomeShadeImage=s;exports.get=A;exports.list=V;exports.register=b;