@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 +63 -0
- package/README.md +217 -0
- package/dist/index.d.ts +14 -17
- package/dist/some-shade.cjs.js +42 -82
- package/dist/some-shade.es.js +303 -287
- package/dist/some-shade.umd.js +42 -82
- package/package.json +14 -2
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
|
|
39
|
-
private
|
|
40
|
-
private _programInfo;
|
|
41
|
-
private _textureInfo;
|
|
42
|
-
private _quadBuffer;
|
|
43
|
-
private _currentEffect;
|
|
36
|
+
private _snapshotUrl;
|
|
37
|
+
private _snapshotLoaded;
|
|
44
38
|
private _image;
|
|
45
|
-
private
|
|
39
|
+
private _observer;
|
|
40
|
+
private _visible;
|
|
41
|
+
private _needsRender;
|
|
46
42
|
render(): TemplateResult<1>;
|
|
47
|
-
|
|
43
|
+
connectedCallback(): void;
|
|
48
44
|
updated(changed: PropertyValues): void;
|
|
49
45
|
disconnectedCallback(): void;
|
|
50
46
|
private _loadImage;
|
|
51
|
-
private
|
|
52
|
-
private
|
|
53
|
-
private
|
|
54
|
-
|
|
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 {
|
package/dist/some-shade.cjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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
|
-
//
|
|
184
|
-
|
|
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
|
|
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
|
|
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
|
|
203
|
-
vec2 d = (
|
|
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
|
-
`,
|
|
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
|
-
|
|
169
|
+
img {
|
|
221
170
|
display: block;
|
|
222
171
|
width: 100%;
|
|
223
172
|
height: auto;
|
|
224
173
|
}
|
|
225
|
-
|
|
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;
|