@johnfmorton/some-shade 0.1.0-beta.9 → 0.1.1-beta

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1-beta
4
+
5
+ - Fix halftone-duotone and halftone-cmyk shaders to sample at grid cell center — dots are now clean, uniform circles instead of splotchy per-pixel noise
6
+
7
+ ## 0.1.0-beta.11
8
+
9
+ - Add grid angle option to dot grid effect (`angle` attribute)
10
+ - Add loading blur option (`loading-blur` attribute) — source image displays blurred until the WebGL effect resolves
11
+ - Add `replayTransition()` public method for programmatically replaying the loading transition
12
+
13
+ ## 0.1.0-beta.10
14
+
15
+ - **BREAKING:** Remove pixel sort effect (`pixel-sort`, `threshold`, `sort-direction`, `sort-span` attributes removed)
16
+
3
17
  ## 0.1.0-beta.9
4
18
 
5
19
  - Fade transition when processed snapshot replaces the source image on scroll
package/README.md CHANGED
@@ -59,20 +59,6 @@ Halftone effect using a single custom color.
59
59
  ></some-shade-image>
60
60
  ```
61
61
 
62
- ### Pixel Sort
63
-
64
- Sorts pixels by luminance along a configurable direction.
65
-
66
- ```html
67
- <some-shade-image
68
- src="photo.jpg"
69
- effect="pixel-sort"
70
- threshold="0.5"
71
- sort-direction="0"
72
- sort-span="64"
73
- ></some-shade-image>
74
- ```
75
-
76
62
  ### Dot Grid
77
63
 
78
64
  Renders the image as a grid of dots with a customizable background.
@@ -103,9 +89,6 @@ Renders the image as a grid of dots with a customizable background.
103
89
  | `angle-k` | number | `45` | halftone-cmyk |
104
90
  | `duotone-color` | string (hex) | `"#0099cc"` | halftone-duotone |
105
91
  | `angle` | number | `0` | halftone-duotone |
106
- | `threshold` | number | `0.5` | pixel-sort |
107
- | `sort-direction` | number | `0` | pixel-sort |
108
- | `sort-span` | number | `64` | pixel-sort |
109
92
  | `dot-offset-x` | number | `0.5` | dot-grid |
110
93
  | `dot-offset-y` | number | `0.5` | dot-grid |
111
94
  | `bg-color` | string (hex) | `"#ffffff"` | dot-grid |
package/dist/index.d.ts CHANGED
@@ -26,21 +26,34 @@ export declare class SomeShadeImage extends LitElement {
26
26
  angleM: number;
27
27
  angleY: number;
28
28
  angleK: number;
29
+ showC: number;
30
+ showM: number;
31
+ showY: number;
32
+ showK: number;
29
33
  duotoneColor: string;
30
34
  angle: number;
31
- threshold: number;
32
- sortDirection: number;
33
- sortSpan: number;
34
35
  dotOffsetX: number;
35
36
  dotOffsetY: number;
36
37
  bgColor: string;
38
+ angleWarm: number;
39
+ angleCool: number;
40
+ showWarm: number;
41
+ showCool: number;
42
+ warmColor: string;
43
+ coolColor: string;
44
+ gateWeave: number;
45
+ loadingBlur: number;
37
46
  private _webglAvailable;
38
47
  private _snapshotUrl;
39
48
  private _snapshotLoaded;
40
49
  private _image;
41
50
  private _observer;
51
+ private _resizeObserver;
52
+ private _lastClientWidth;
42
53
  private _visible;
43
54
  private _needsRender;
55
+ private _gateWeaveRaf;
56
+ private _gateWeaveTime;
44
57
  render(): TemplateResult<1>;
45
58
  connectedCallback(): void;
46
59
  updated(changed: PropertyValues): void;
@@ -49,8 +62,13 @@ export declare class SomeShadeImage extends LitElement {
49
62
  private _scheduleRender;
50
63
  private _renderEffect;
51
64
  private _onSnapshotLoad;
65
+ /** Hide the rendered snapshot momentarily, then fade it back in.
66
+ * Useful for previewing the loading-blur transition. */
67
+ replayTransition(delay?: number): void;
52
68
  private _revokeSnapshot;
53
69
  private _getUniformValues;
70
+ private _updateGateWeave;
71
+ private _stopGateWeave;
54
72
  private _parseHexColor;
55
73
  }
56
74
 
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=require("lit"),u=require("lit/decorators.js");function k(t){return t.getContext("webgl",{alpha:!0,premultipliedAlpha:!1,preserveDrawingBuffer:!0})}function S(t,o,a){const e=t.createShader(o);if(!e)throw new Error("Failed to create shader");if(t.shaderSource(e,a),t.compileShader(e),!t.getShaderParameter(e,t.COMPILE_STATUS)){const i=t.getShaderInfoLog(e);throw t.deleteShader(e),new Error(`Shader compile error: ${i}`)}return e}function w(t,o,a){const e=S(t,t.VERTEX_SHADER,o),i=S(t,t.FRAGMENT_SHADER,a),n=t.createProgram();if(!n)throw new Error("Failed to create program");if(t.attachShader(n,e),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(e),t.deleteShader(i);const r=new Map,g=t.getProgramParameter(n,t.ACTIVE_ATTRIBUTES);for(let d=0;d<g;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 p=t.getUniformLocation(n,c.name);p&&f.set(c.name,p)}}return{program:n,attribLocations:r,uniformLocations:f}}function L(t,o,a){for(const[e,i]of Object.entries(a)){const n=o.uniformLocations.get(e);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 D(t,o){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,o),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:o.naturalWidth,height:o.naturalHeight}}function P(t,o){const a=new Float32Array([-1,-1,0,1,1,-1,1,1,-1,1,0,0,1,1,1,0]),e=t.createBuffer();if(!e)throw new Error("Failed to create buffer");t.bindBuffer(t.ARRAY_BUFFER,e),t.bufferData(t.ARRAY_BUFFER,a,t.STATIC_DRAW);const i=4*Float32Array.BYTES_PER_ELEMENT,n=o.attribLocations.get("a_position");n!==void 0&&n!==-1&&(t.enableVertexAttribArray(n),t.vertexAttribPointer(n,2,t.FLOAT,!1,i,0));const r=o.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)),e}function V(t){t.drawArrays(t.TRIANGLE_STRIP,0,4)}const O=`precision mediump float;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const _=require("lit"),l=require("lit/decorators.js");function L(e){return e.getContext("webgl",{alpha:!0,premultipliedAlpha:!1,preserveDrawingBuffer:!0})}function E(e,o,r){const a=e.createShader(o);if(!a)throw new Error("Failed to create shader");if(e.shaderSource(a,r),e.compileShader(a),!e.getShaderParameter(a,e.COMPILE_STATUS)){const t=e.getShaderInfoLog(a);throw e.deleteShader(a),new Error(`Shader compile error: ${t}`)}return a}function k(e,o,r){const a=E(e,e.VERTEX_SHADER,o),t=E(e,e.FRAGMENT_SHADER,r),n=e.createProgram();if(!n)throw new Error("Failed to create program");if(e.attachShader(n,a),e.attachShader(n,t),e.linkProgram(n),!e.getProgramParameter(n,e.LINK_STATUS)){const c=e.getProgramInfoLog(n);throw e.deleteProgram(n),new Error(`Program link error: ${c}`)}e.deleteShader(a),e.deleteShader(t);const d=new Map,h=e.getProgramParameter(n,e.ACTIVE_ATTRIBUTES);for(let c=0;c<h;c++){const f=e.getActiveAttrib(n,c);f&&d.set(f.name,e.getAttribLocation(n,f.name))}const s=new Map,g=e.getProgramParameter(n,e.ACTIVE_UNIFORMS);for(let c=0;c<g;c++){const f=e.getActiveUniform(n,c);if(f){const m=e.getUniformLocation(n,f.name);m&&s.set(f.name,m)}}return{program:n,attribLocations:d,uniformLocations:s}}function D(e,o,r){for(const[a,t]of Object.entries(r)){const n=o.uniformLocations.get(a);if(n){if(typeof t=="number")e.uniform1f(n,t);else if(Array.isArray(t))switch(t.length){case 2:e.uniform2fv(n,t);break;case 3:e.uniform3fv(n,t);break;case 4:e.uniform4fv(n,t);break}}}}function z(e,o){const r=e.createTexture();if(!r)throw new Error("Failed to create texture");return e.bindTexture(e.TEXTURE_2D,r),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,o),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:r,width:o.naturalWidth,height:o.naturalHeight}}function K(e,o){const r=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,r,e.STATIC_DRAW);const t=4*Float32Array.BYTES_PER_ELEMENT,n=o.attribLocations.get("a_position");n!==void 0&&n!==-1&&(e.enableVertexAttribArray(n),e.vertexAttribPointer(n,2,e.FLOAT,!1,t,0));const d=o.attribLocations.get("a_texCoord");return d!==void 0&&d!==-1&&(e.enableVertexAttribArray(d),e.vertexAttribPointer(d,2,e.FLOAT,!1,t,2*Float32Array.BYTES_PER_ELEMENT)),a}function P(e){e.drawArrays(e.TRIANGLE_STRIP,0,4)}const M=`precision mediump float;
2
2
 
3
3
  varying vec2 v_texCoord;
4
4
 
@@ -10,6 +10,25 @@ uniform float u_angleC;
10
10
  uniform float u_angleM;
11
11
  uniform float u_angleY;
12
12
  uniform float u_angleK;
13
+ uniform float u_showC;
14
+ uniform float u_showM;
15
+ uniform float u_showY;
16
+ uniform float u_showK;
17
+
18
+ // Returns the texture-space UV of the cell center for a given rotation angle
19
+ vec2 cellCenterUV(vec2 uv, float angle) {
20
+ float rad = radians(angle);
21
+ float s = sin(rad);
22
+ float c = cos(rad);
23
+ mat2 rot = mat2(c, -s, s, c);
24
+ mat2 invRot = mat2(c, s, -s, c);
25
+
26
+ vec2 rotUV = rot * uv;
27
+ vec2 cell = floor(rotUV / u_gridSize);
28
+ vec2 cellCenter = (cell + 0.5) * u_gridSize;
29
+
30
+ return (invRot * cellCenter) / u_resolution;
31
+ }
13
32
 
14
33
  float halftone(vec2 uv, float angle, float channelValue, float gridSize, float dotRadius) {
15
34
  float rad = radians(angle);
@@ -28,31 +47,40 @@ float halftone(vec2 uv, float angle, float channelValue, float gridSize, float d
28
47
 
29
48
  void main() {
30
49
  vec2 uv = v_texCoord * u_resolution;
31
- vec4 color = texture2D(u_image, v_texCoord);
32
50
 
33
- float r = color.r;
34
- float g = color.g;
35
- float b = color.b;
51
+ // Sample at each channel's cell center (each channel has a different grid rotation)
52
+ vec3 cRgb = texture2D(u_image, cellCenterUV(uv, u_angleC)).rgb;
53
+ vec3 mRgb = texture2D(u_image, cellCenterUV(uv, u_angleM)).rgb;
54
+ vec3 yRgb = texture2D(u_image, cellCenterUV(uv, u_angleY)).rgb;
55
+ vec3 kRgb = texture2D(u_image, cellCenterUV(uv, u_angleK)).rgb;
56
+
57
+ // RGB to CMYK for each channel's sample
58
+ float cK = 1.0 - max(max(cRgb.r, cRgb.g), cRgb.b);
59
+ float cInvK = 1.0 - cK;
60
+ float cy = cInvK > 0.001 ? (cInvK - cRgb.r) / cInvK : 0.0;
36
61
 
37
- // RGB to CMYK
38
- float k = 1.0 - max(max(r, g), b);
39
- float invK = 1.0 - k;
40
- float cy = invK > 0.001 ? (invK - r) / invK : 0.0;
41
- float ma = invK > 0.001 ? (invK - g) / invK : 0.0;
42
- float ye = invK > 0.001 ? (invK - b) / invK : 0.0;
62
+ float mK = 1.0 - max(max(mRgb.r, mRgb.g), mRgb.b);
63
+ float mInvK = 1.0 - mK;
64
+ float ma = mInvK > 0.001 ? (mInvK - mRgb.g) / mInvK : 0.0;
65
+
66
+ float yK = 1.0 - max(max(yRgb.r, yRgb.g), yRgb.b);
67
+ float yInvK = 1.0 - yK;
68
+ float ye = yInvK > 0.001 ? (yInvK - yRgb.b) / yInvK : 0.0;
69
+
70
+ float k = 1.0 - max(max(kRgb.r, kRgb.g), kRgb.b);
43
71
 
44
72
  // Compute halftone dots for each channel
45
- float cDot = halftone(uv, u_angleC, cy, u_gridSize, u_dotRadius);
46
- float mDot = halftone(uv, u_angleM, ma, u_gridSize, u_dotRadius);
47
- float yDot = halftone(uv, u_angleY, ye, u_gridSize, u_dotRadius);
48
- float kDot = halftone(uv, u_angleK, k, u_gridSize, u_dotRadius);
73
+ float cDot = halftone(uv, u_angleC, cy, u_gridSize, u_dotRadius) * u_showC;
74
+ float mDot = halftone(uv, u_angleM, ma, u_gridSize, u_dotRadius) * u_showM;
75
+ float yDot = halftone(uv, u_angleY, ye, u_gridSize, u_dotRadius) * u_showY;
76
+ float kDot = halftone(uv, u_angleK, k, u_gridSize, u_dotRadius) * u_showK;
49
77
 
50
78
  // Subtractive color mixing on white paper
51
79
  float outR = (1.0 - cDot) * (1.0 - kDot);
52
80
  float outG = (1.0 - mDot) * (1.0 - kDot);
53
81
  float outB = (1.0 - yDot) * (1.0 - kDot);
54
82
 
55
- gl_FragColor = vec4(outR, outG, outB, color.a);
83
+ gl_FragColor = vec4(outR, outG, outB, texture2D(u_image, v_texCoord).a);
56
84
  }
57
85
  `,y=`attribute vec2 a_position;
58
86
  attribute vec2 a_texCoord;
@@ -62,7 +90,7 @@ void main() {
62
90
  gl_Position = vec4(a_position, 0.0, 1.0);
63
91
  v_texCoord = a_texCoord;
64
92
  }
65
- `,F={name:"halftone-cmyk",fragmentShader:O,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"}]},z=`precision mediump float;
93
+ `,I={name:"halftone-cmyk",fragmentShader:M,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"},{name:"u_showC",type:"float",default:1,attribute:"show-c"},{name:"u_showM",type:"float",default:1,attribute:"show-m"},{name:"u_showY",type:"float",default:1,attribute:"show-y"},{name:"u_showK",type:"float",default:1,attribute:"show-k"}]},O=`precision mediump float;
66
94
 
67
95
  varying vec2 v_texCoord;
68
96
 
@@ -75,17 +103,24 @@ uniform float u_angle;
75
103
 
76
104
  void main() {
77
105
  vec2 uv = v_texCoord * u_resolution;
78
- vec4 color = texture2D(u_image, v_texCoord);
79
-
80
- // Convert to luminance
81
- float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));
82
- float darkness = 1.0 - luma;
83
106
 
84
107
  // Rotate grid
85
108
  float rad = radians(u_angle);
86
109
  float s = sin(rad);
87
110
  float c = cos(rad);
88
- vec2 rotUV = mat2(c, -s, s, c) * uv;
111
+ mat2 rot = mat2(c, -s, s, c);
112
+ mat2 invRot = mat2(c, s, -s, c);
113
+ vec2 rotUV = rot * uv;
114
+
115
+ // Find cell center in rotated space, un-rotate to texture space for sampling
116
+ vec2 cell = floor(rotUV / u_gridSize);
117
+ vec2 cellCenter = (cell + 0.5) * u_gridSize;
118
+ vec2 sampleUV = (invRot * cellCenter) / u_resolution;
119
+ vec4 color = texture2D(u_image, sampleUV);
120
+
121
+ // Convert to luminance
122
+ float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));
123
+ float darkness = 1.0 - luma;
89
124
 
90
125
  vec2 grid = fract(rotUV / u_gridSize) - 0.5;
91
126
  float dist = length(grid) * u_gridSize;
@@ -97,76 +132,7 @@ void main() {
97
132
 
98
133
  gl_FragColor = vec4(result, color.a);
99
134
  }
100
- `,M={name:"halftone-duotone",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_duotoneColor",type:"vec3",default:[0,.6,.8],attribute:"duotone-color"},{name:"u_angle",type:"float",default:0,attribute:"angle"}]},I=`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
- `,N={name:"pixel-sort",fragmentShader:I,vertexShader:y,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"}]},B=`precision mediump float;
135
+ `,V={name:"halftone-duotone",fragmentShader:O,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"}]},F=`precision mediump float;
170
136
 
171
137
  varying vec2 v_texCoord;
172
138
 
@@ -176,31 +142,42 @@ uniform float u_dotRadius;
176
142
  uniform float u_gridSize;
177
143
  uniform vec2 u_dotOffset;
178
144
  uniform vec3 u_bgColor;
145
+ uniform float u_angle;
179
146
 
180
147
  void main() {
181
148
  vec2 uv = v_texCoord * u_resolution;
182
149
 
183
- // Which grid cell this fragment belongs to
184
- vec2 cell = floor(uv / u_gridSize);
150
+ // Rotate grid
151
+ float rad = radians(u_angle);
152
+ float s = sin(rad);
153
+ float c = cos(rad);
154
+ mat2 rot = mat2(c, -s, s, c);
155
+ mat2 invRot = mat2(c, s, -s, c);
156
+ vec2 rotUV = rot * uv;
157
+
158
+ // Which grid cell this fragment belongs to (in rotated space)
159
+ vec2 cell = floor(rotUV / u_gridSize);
185
160
 
186
- // Cell origin in pixel space
161
+ // Cell origin in rotated space
187
162
  vec2 cellOrigin = cell * u_gridSize;
188
163
 
189
164
  // Dot center within the cell, shifted by u_dotOffset (0–1 range)
190
165
  vec2 dotCenter = cellOrigin + u_dotOffset * u_gridSize;
191
166
 
192
167
  // 4×4 multi-sample average color across the cell
168
+ // Sample points are computed in rotated space then un-rotated for texture lookup
193
169
  vec3 avg = vec3(0.0);
194
170
  for (int y = 0; y < 4; y++) {
195
171
  for (int x = 0; x < 4; x++) {
196
- vec2 sampleUV = (cellOrigin + (vec2(float(x), float(y)) + 0.5) * (u_gridSize / 4.0)) / u_resolution;
172
+ vec2 rotSample = cellOrigin + (vec2(float(x), float(y)) + 0.5) * (u_gridSize / 4.0);
173
+ vec2 sampleUV = (invRot * rotSample) / u_resolution;
197
174
  avg += texture2D(u_image, sampleUV).rgb;
198
175
  }
199
176
  }
200
177
  avg /= 16.0;
201
178
 
202
- // Wrapped (toroidal) distance from fragment to dot center within the cell
203
- vec2 d = (uv - dotCenter) / u_gridSize;
179
+ // Wrapped (toroidal) distance from fragment to dot center (in rotated space)
180
+ vec2 d = (rotUV - dotCenter) / u_gridSize;
204
181
  d = fract(d + 0.5) - 0.5; // wrap to [-0.5, 0.5]
205
182
  float dist = length(d) * u_gridSize;
206
183
 
@@ -211,14 +188,70 @@ void main() {
211
188
 
212
189
  gl_FragColor = vec4(result, 1.0);
213
190
  }
214
- `,Y={name:"dot-grid",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_dotOffset",type:"vec2",default:[.5,.5],attribute:"dot-offset"},{name:"u_bgColor",type:"vec3",default:[1,1,1],attribute:"bg-color"}]},E=new Map;function _(t){E.set(t.name,t)}function U(t){return E.get(t)}function K(){return Array.from(E.keys())}_(F);_(M);_(N);_(Y);var X=Object.defineProperty,l=(t,o,a,e)=>{for(var i=void 0,n=t.length-1,r;n>=0;n--)(r=t[n])&&(i=r(o,a,i)||i);return i&&X(o,a,i),i};const G=2;let C=Promise.resolve();function T(t){const o=C.then(t,t);return C=o,o}const $=new Set(["effect","dotRadius","gridSize","angleC","angleM","angleY","angleK","duotoneColor","angle","threshold","sortDirection","sortSpan","dotOffsetX","dotOffsetY","bgColor"]),R=class R 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.threshold=.5,this.sortDirection=0,this.sortSpan=64,this.dotOffsetX=.5,this.dotOffsetY=.5,this.bgColor="#ffffff",this._webglAvailable=!0,this._snapshotUrl="",this._snapshotLoaded=!1,this._image=null,this._observer=null,this._visible=!1,this._needsRender=!1}render(){return this._webglAvailable?m.html`
215
- <img src=${this.src} alt="" />
216
- ${this._snapshotUrl?m.html`<img
191
+ `,N={name:"dot-grid",fragmentShader:F,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"}]},Y=`precision mediump float;
192
+
193
+ varying vec2 v_texCoord;
194
+
195
+ uniform sampler2D u_image;
196
+ uniform vec2 u_resolution;
197
+ uniform float u_dotRadius;
198
+ uniform float u_gridSize;
199
+ uniform float u_angleWarm;
200
+ uniform float u_angleCool;
201
+ uniform float u_angleK;
202
+ uniform float u_showWarm;
203
+ uniform float u_showCool;
204
+ uniform float u_showK;
205
+ uniform vec3 u_warmColor;
206
+ uniform vec3 u_coolColor;
207
+
208
+ float halftone(vec2 uv, float angle, float channelValue, float gridSize, float dotRadius) {
209
+ float rad = radians(angle);
210
+ float s = sin(rad);
211
+ float c = cos(rad);
212
+ mat2 rot = mat2(c, -s, s, c);
213
+
214
+ vec2 rotUV = rot * uv;
215
+ vec2 grid = fract(rotUV / gridSize) - 0.5;
216
+
217
+ float dist = length(grid) * gridSize;
218
+ float radius = dotRadius * sqrt(channelValue);
219
+
220
+ return smoothstep(radius + 0.5, radius - 0.5, dist);
221
+ }
222
+
223
+ void main() {
224
+ vec2 uv = v_texCoord * u_resolution;
225
+ vec4 color = texture2D(u_image, v_texCoord);
226
+
227
+ // Two-strip film capture: red-orange and blue-green filter responses
228
+ float warmSep = dot(color.rgb, vec3(0.7, 0.3, 0.0));
229
+ float coolSep = dot(color.rgb, vec3(0.0, 0.5, 0.5));
230
+
231
+ // Black channel: derived from overall darkness
232
+ float k = 1.0 - max(warmSep, coolSep);
233
+
234
+ // Halftone dots for each channel
235
+ float warmDot = halftone(uv, u_angleWarm, warmSep, u_gridSize, u_dotRadius) * u_showWarm;
236
+ float coolDot = halftone(uv, u_angleCool, coolSep, u_gridSize, u_dotRadius) * u_showCool;
237
+ float kDot = halftone(uv, u_angleK, k, u_gridSize, u_dotRadius) * u_showK;
238
+
239
+ // Subtractive mixing: start with white paper, subtract dye layers
240
+ vec3 paper = vec3(1.0);
241
+ paper -= warmDot * (vec3(1.0) - u_warmColor);
242
+ paper -= coolDot * (vec3(1.0) - u_coolColor);
243
+ paper *= (1.0 - kDot);
244
+
245
+ gl_FragColor = vec4(clamp(paper, 0.0, 1.0), color.a);
246
+ }
247
+ `,B={name:"technicolor-2strip",fragmentShader:Y,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_angleWarm",type:"float",default:15,attribute:"angle-warm"},{name:"u_angleCool",type:"float",default:75,attribute:"angle-cool"},{name:"u_angleK",type:"float",default:45,attribute:"angle-k"},{name:"u_showWarm",type:"float",default:1,attribute:"show-warm"},{name:"u_showCool",type:"float",default:1,attribute:"show-cool"},{name:"u_showK",type:"float",default:1,attribute:"show-k"},{name:"u_warmColor",type:"vec3",default:[.85,.25,.06],attribute:"warm-color"},{name:"u_coolColor",type:"vec3",default:[.05,.65,.6],attribute:"cool-color"}]},w=new Map;function p(e){w.set(e.name,e)}function U(e){return w.get(e)}function G(){return Array.from(w.keys())}p(I);p(V);p(N);p(B);var X=Object.defineProperty,u=(e,o,r,a)=>{for(var t=void 0,n=e.length-1,d;n>=0;n--)(d=e[n])&&(t=d(o,r,t)||t);return t&&X(o,r,t),t};const $=2;let T=Promise.resolve();function A(e){const o=T.then(e,e);return T=o,o}const q=new Set(["effect","dotRadius","gridSize","angleC","angleM","angleY","angleK","showC","showM","showY","showK","duotoneColor","angle","dotOffsetX","dotOffsetY","bgColor","angleWarm","angleCool","showWarm","showCool","warmColor","coolColor"]),R=class R extends _.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.showC=1,this.showM=1,this.showY=1,this.showK=1,this.duotoneColor="#0099cc",this.angle=0,this.dotOffsetX=.5,this.dotOffsetY=.5,this.bgColor="#ffffff",this.angleWarm=15,this.angleCool=75,this.showWarm=1,this.showCool=1,this.warmColor="#d94010",this.coolColor="#0da699",this.gateWeave=0,this.loadingBlur=0,this._webglAvailable=!0,this._snapshotUrl="",this._snapshotLoaded=!1,this._image=null,this._observer=null,this._resizeObserver=null,this._lastClientWidth=0,this._visible=!1,this._needsRender=!1,this._gateWeaveRaf=0,this._gateWeaveTime=0}render(){if(!this._webglAvailable)return _.html`<img src=${this.src} alt="" />`;const o=this.loadingBlur>0?`filter: blur(${this.loadingBlur}px)`:"";return _.html`
248
+ <img src=${this.src} alt="" style=${o} />
249
+ ${this._snapshotUrl?_.html`<img
217
250
  class="snapshot${this._snapshotLoaded?" loaded":""}"
218
251
  src=${this._snapshotUrl}
219
252
  @load=${this._onSnapshotLoad}
220
253
  alt="" />`:""}
221
- `:m.html`<img src=${this.src} alt="" />`}connectedCallback(){super.connectedCallback(),this._observer=new IntersectionObserver(o=>{var e;const a=this._visible;this._visible=((e=o[0])==null?void 0:e.isIntersecting)??!1,this._visible&&!a&&this._needsRender&&(this._needsRender=!1,T(()=>this._renderEffect()))},{rootMargin:"200px"}),this._observer.observe(this)}updated(o){if(o.has("src")&&this.src){this._loadImage(this.src);return}if(!this._image)return;[...o.keys()].some(e=>$.has(e))&&this._scheduleRender()}disconnectedCallback(){var o;super.disconnectedCallback(),(o=this._observer)==null||o.disconnect(),this._revokeSnapshot()}_loadImage(o){const a=new Image;a.crossOrigin="anonymous",a.onload=()=>{this._image=a,this._scheduleRender()},a.onerror=()=>{console.warn(`[some-shade] Failed to load image: ${o}`)},a.src=o}_scheduleRender(){this._visible?T(()=>this._renderEffect()):this._needsRender=!0}async _renderEffect(){var g;if(!this._image)return;const o=U(this.effect);if(!o){console.warn(`[some-shade] Unknown effect: ${this.effect}`);return}const a=Math.min(window.devicePixelRatio||1,G),e=this._image.naturalWidth,i=this._image.naturalHeight,n=document.createElement("canvas");n.width=e*a,n.height=i*a;const r=k(n);if(!r){this._webglAvailable=!1;return}try{const f=w(r,o.vertexShader,o.fragmentShader);r.useProgram(f.program);const h=D(r,this._image),d=P(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),L(r,f,this._getUniformValues(h,a)),r.bindBuffer(r.ARRAY_BUFFER,d);const p=4*Float32Array.BYTES_PER_ELEMENT,b=f.attribLocations.get("a_position");b!==void 0&&b!==-1&&(r.enableVertexAttribArray(b),r.vertexAttribPointer(b,2,r.FLOAT,!1,p,0));const v=f.attribLocations.get("a_texCoord");v!==void 0&&v!==-1&&(r.enableVertexAttribArray(v),r.vertexAttribPointer(v,2,r.FLOAT,!1,p,2*Float32Array.BYTES_PER_ELEMENT)),V(r);const x=await new Promise(A=>n.toBlob(A));r.deleteTexture(h.texture),r.deleteProgram(f.program),r.deleteBuffer(d),x&&(this._snapshotLoaded=!1,this._revokeSnapshot(),this._snapshotUrl=URL.createObjectURL(x))}finally{(g=r.getExtension("WEBGL_lose_context"))==null||g.loseContext()}}_onSnapshotLoad(){this._snapshotLoaded=!0}_revokeSnapshot(){this._snapshotUrl&&(URL.revokeObjectURL(this._snapshotUrl),this._snapshotUrl="")}_getUniformValues(o,a){const e={};return e.u_resolution=[o.width*a,o.height*a],e.u_dotRadius=this.dotRadius,e.u_gridSize=this.gridSize,this.effect==="halftone-cmyk"?(e.u_angleC=this.angleC,e.u_angleM=this.angleM,e.u_angleY=this.angleY,e.u_angleK=this.angleK):this.effect==="halftone-duotone"?(e.u_duotoneColor=this._parseHexColor(this.duotoneColor),e.u_angle=this.angle):this.effect==="pixel-sort"?(e.u_threshold=this.threshold,e.u_direction=this.sortDirection,e.u_span=this.sortSpan):this.effect==="dot-grid"&&(e.u_dotOffset=[this.dotOffsetX,this.dotOffsetY],e.u_bgColor=this._parseHexColor(this.bgColor)),e}_parseHexColor(o){const a=o.replace("#",""),e=parseInt(a.substring(0,2),16)/255,i=parseInt(a.substring(2,4),16)/255,n=parseInt(a.substring(4,6),16)/255;return[e,i,n]}};R.styles=m.css`
254
+ `}connectedCallback(){super.connectedCallback(),this._observer=new IntersectionObserver(o=>{var a;const r=this._visible;this._visible=((a=o[0])==null?void 0:a.isIntersecting)??!1,this._visible&&!r&&this._needsRender&&(this._needsRender=!1,A(()=>this._renderEffect()))},{rootMargin:"200px"}),this._observer.observe(this),this._resizeObserver=new ResizeObserver(()=>{if(!this._image)return;const o=this.clientWidth;o>0&&o!==this._lastClientWidth&&(this._lastClientWidth=o,this._scheduleRender())}),this._resizeObserver.observe(this)}updated(o){if(o.has("src")&&this.src){this._loadImage(this.src);return}if(!this._image)return;[...o.keys()].some(a=>q.has(a))&&this._scheduleRender(),o.has("gateWeave")&&this._updateGateWeave()}disconnectedCallback(){var o,r;super.disconnectedCallback(),(o=this._observer)==null||o.disconnect(),(r=this._resizeObserver)==null||r.disconnect(),this._stopGateWeave(),this._revokeSnapshot()}_loadImage(o){const r=new Image;r.crossOrigin="anonymous",r.onload=()=>{this._image=r,this._scheduleRender()},r.onerror=()=>{console.warn(`[some-shade] Failed to load image: ${o}`)},r.src=o}_scheduleRender(){this._visible?A(()=>this._renderEffect()):this._needsRender=!0}async _renderEffect(){var g;if(!this._image)return;const o=U(this.effect);if(!o){console.warn(`[some-shade] Unknown effect: ${this.effect}`);return}const r=Math.min(window.devicePixelRatio||1,$),a=this._image.naturalWidth,t=this._image.naturalHeight,n=this.clientWidth||a,d=Math.max(1,a/n);this._lastClientWidth=this.clientWidth;const h=document.createElement("canvas");h.width=a*r,h.height=t*r;const s=L(h);if(!s){this._webglAvailable=!1;return}try{const c=k(s,o.vertexShader,o.fragmentShader);s.useProgram(c.program);const f=z(s,this._image),m=K(s,c);s.viewport(0,0,h.width,h.height),s.clearColor(0,0,0,0),s.clear(s.COLOR_BUFFER_BIT),s.activeTexture(s.TEXTURE0),s.bindTexture(s.TEXTURE_2D,f.texture);const C=c.uniformLocations.get("u_image");C&&s.uniform1i(C,0),D(s,c,this._getUniformValues(f,r,d)),s.bindBuffer(s.ARRAY_BUFFER,m);const S=4*Float32Array.BYTES_PER_ELEMENT,v=c.attribLocations.get("a_position");v!==void 0&&v!==-1&&(s.enableVertexAttribArray(v),s.vertexAttribPointer(v,2,s.FLOAT,!1,S,0));const b=c.attribLocations.get("a_texCoord");b!==void 0&&b!==-1&&(s.enableVertexAttribArray(b),s.vertexAttribPointer(b,2,s.FLOAT,!1,S,2*Float32Array.BYTES_PER_ELEMENT)),P(s);const x=await new Promise(W=>h.toBlob(W));s.deleteTexture(f.texture),s.deleteProgram(c.program),s.deleteBuffer(m),x&&(this._snapshotLoaded=!1,this._revokeSnapshot(),this._snapshotUrl=URL.createObjectURL(x))}finally{(g=s.getExtension("WEBGL_lose_context"))==null||g.loseContext()}}_onSnapshotLoad(){this._snapshotLoaded=!0}replayTransition(o=500){this._snapshotUrl&&(this._snapshotLoaded=!1,this.updateComplete.then(()=>{setTimeout(()=>{this._snapshotLoaded=!0},o)}))}_revokeSnapshot(){this._snapshotUrl&&(URL.revokeObjectURL(this._snapshotUrl),this._snapshotUrl="")}_getUniformValues(o,r,a){const t={};return t.u_resolution=[o.width*r,o.height*r],t.u_dotRadius=this.dotRadius*a,t.u_gridSize=this.gridSize*a,this.effect==="halftone-cmyk"?(t.u_angleC=this.angleC,t.u_angleM=this.angleM,t.u_angleY=this.angleY,t.u_angleK=this.angleK,t.u_showC=this.showC,t.u_showM=this.showM,t.u_showY=this.showY,t.u_showK=this.showK):this.effect==="halftone-duotone"?(t.u_duotoneColor=this._parseHexColor(this.duotoneColor),t.u_angle=this.angle):this.effect==="dot-grid"?(t.u_dotOffset=[this.dotOffsetX,this.dotOffsetY],t.u_bgColor=this._parseHexColor(this.bgColor),t.u_angle=this.angle):this.effect==="technicolor-2strip"&&(t.u_angleWarm=this.angleWarm,t.u_angleCool=this.angleCool,t.u_angleK=this.angleK,t.u_showWarm=this.showWarm,t.u_showCool=this.showCool,t.u_showK=this.showK,t.u_warmColor=this._parseHexColor(this.warmColor),t.u_coolColor=this._parseHexColor(this.coolColor)),t}_updateGateWeave(){if(this._stopGateWeave(),this.gateWeave<=0){const a=this.renderRoot.querySelector("img.snapshot");a&&(a.style.transform="");return}const o=1e3/12,r=a=>{if(a-this._gateWeaveTime>=o){this._gateWeaveTime=a;const t=this.renderRoot.querySelector("img.snapshot");if(t){const n=(Math.random()-.5)*2*this.gateWeave,d=(Math.random()-.5)*2*this.gateWeave,h=1+this.gateWeave*.003;t.style.transform=`translate(${n}px, ${d}px) scale(${h})`}}this._gateWeaveRaf=requestAnimationFrame(r)};this._gateWeaveRaf=requestAnimationFrame(r)}_stopGateWeave(){this._gateWeaveRaf&&(cancelAnimationFrame(this._gateWeaveRaf),this._gateWeaveRaf=0)}_parseHexColor(o){const r=o.replace("#",""),a=parseInt(r.substring(0,2),16)/255,t=parseInt(r.substring(2,4),16)/255,n=parseInt(r.substring(4,6),16)/255;return[a,t,n]}};R.styles=_.css`
222
255
  :host {
223
256
  display: block;
224
257
  position: relative;
@@ -240,4 +273,4 @@ void main() {
240
273
  img.snapshot.loaded {
241
274
  opacity: 1;
242
275
  }
243
- `;let s=R;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})],s.prototype,"threshold");l([u.property({type:Number,attribute:"sort-direction"})],s.prototype,"sortDirection");l([u.property({type:Number,attribute:"sort-span"})],s.prototype,"sortSpan");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.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=U;exports.list=K;exports.register=_;
276
+ `;let i=R;u([l.property()],i.prototype,"src");u([l.property()],i.prototype,"effect");u([l.property({type:Number,attribute:"dot-radius"})],i.prototype,"dotRadius");u([l.property({type:Number,attribute:"grid-size"})],i.prototype,"gridSize");u([l.property({type:Number,attribute:"angle-c"})],i.prototype,"angleC");u([l.property({type:Number,attribute:"angle-m"})],i.prototype,"angleM");u([l.property({type:Number,attribute:"angle-y"})],i.prototype,"angleY");u([l.property({type:Number,attribute:"angle-k"})],i.prototype,"angleK");u([l.property({type:Number,attribute:"show-c"})],i.prototype,"showC");u([l.property({type:Number,attribute:"show-m"})],i.prototype,"showM");u([l.property({type:Number,attribute:"show-y"})],i.prototype,"showY");u([l.property({type:Number,attribute:"show-k"})],i.prototype,"showK");u([l.property({attribute:"duotone-color"})],i.prototype,"duotoneColor");u([l.property({type:Number})],i.prototype,"angle");u([l.property({type:Number,attribute:"dot-offset-x"})],i.prototype,"dotOffsetX");u([l.property({type:Number,attribute:"dot-offset-y"})],i.prototype,"dotOffsetY");u([l.property({attribute:"bg-color"})],i.prototype,"bgColor");u([l.property({type:Number,attribute:"angle-warm"})],i.prototype,"angleWarm");u([l.property({type:Number,attribute:"angle-cool"})],i.prototype,"angleCool");u([l.property({type:Number,attribute:"show-warm"})],i.prototype,"showWarm");u([l.property({type:Number,attribute:"show-cool"})],i.prototype,"showCool");u([l.property({attribute:"warm-color"})],i.prototype,"warmColor");u([l.property({attribute:"cool-color"})],i.prototype,"coolColor");u([l.property({type:Number,attribute:"gate-weave"})],i.prototype,"gateWeave");u([l.property({type:Number,attribute:"loading-blur"})],i.prototype,"loadingBlur");u([l.state()],i.prototype,"_webglAvailable");u([l.state()],i.prototype,"_snapshotUrl");u([l.state()],i.prototype,"_snapshotLoaded");customElements.get("some-shade-image")||customElements.define("some-shade-image",i);exports.SomeShadeImage=i;exports.get=U;exports.list=G;exports.register=p;