Skip to content

Commit b7f5335

Browse files
committed
Image Filter Demo
1 parent 906c831 commit b7f5335

File tree

6 files changed

+314
-0
lines changed

6 files changed

+314
-0
lines changed

20241019/javascript/.node-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v20.11.1

20241019/javascript/package-lock.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

20241019/javascript/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "javascript",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "servor public index.html 8080 --reload"
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "ISC",
12+
"devDependencies": {
13+
"servor": "^4.0.2"
14+
}
15+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
*,
2+
*::before,
3+
*::after {
4+
box-sizing: inherit;
5+
}
6+
7+
html {
8+
font-size: 62.5%;
9+
box-sizing: border-box;
10+
}
11+
12+
body {
13+
font-size: 1.6rem;
14+
line-height: 1.5;
15+
color: #000;
16+
background: #FFF;
17+
}
18+
19+
#root {
20+
margin: 1rem auto;
21+
width: 90%;
22+
}
23+
24+
.container {
25+
margin-block: 1rem;
26+
}
27+
28+
.filter-canvas {
29+
zoom: .04;
30+
}
31+
32+
.canvas-wrapper {
33+
display: flex;
34+
justify-content: space-between;
35+
width: 100%;
36+
37+
.canvas {
38+
width: calc(50% - .5em);
39+
/* aspect-ratio: 16 / 9; */
40+
border: 5px solid #DDD;
41+
background: #CCC;
42+
overflow: auto;
43+
}
44+
45+
canvas {
46+
width: 100%;
47+
}
48+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!DOCTYPE html>
2+
<html lang="ko">
3+
<head>
4+
<title>Demo</title>
5+
<meta charset="utf-8">
6+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
7+
<meta name="viewport" content="width=device-width, initial-scale=1">
8+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/reset.min.css">
9+
<link rel="stylesheet" href="./css/style.css?ecb458f9c80cceaaed64513d53c03f30">
10+
</head>
11+
<body>
12+
<div id="root" x-data="new Application()">
13+
<div class="container">
14+
<div>
15+
Filter:
16+
<input type="file" accept="image/*" @change="selectFilter" />
17+
</div>
18+
</div>
19+
<div class="container">
20+
<div class="filter-canvas">
21+
<canvas x-ref="filterCanvas"></canvas>
22+
</div>
23+
</div>
24+
<div class="container">
25+
<div>
26+
Image:
27+
<input type="file" accept="image/*" @change="selectImage" />
28+
</div>
29+
</div>
30+
<div class="container canvas-wrapper">
31+
<div class="canvas">
32+
<canvas x-ref="sourceCanvas"></canvas>
33+
</div>
34+
<div class="canvas">
35+
<canvas x-ref="targetCanvas"></canvas>
36+
</div>
37+
</div>
38+
</div>
39+
<script type="module">
40+
import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/0.169.0/three.module.min.js';
41+
window.THREE = THREE;
42+
</script>
43+
<script src="./js/application.js?5b6521bb1966aebdcfe5adad5420ff87"></script>
44+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
45+
</body>
46+
</html>
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
function readFileAsDataURL(file) {
2+
return new Promise((resolve, reject) => {
3+
const reader = new FileReader();
4+
5+
reader.onload = () => {
6+
resolve(reader.result);
7+
};
8+
9+
reader.onerror = reject;
10+
11+
reader.readAsDataURL(file);
12+
});
13+
}
14+
15+
function createImageFromAsDataURL(dataURL) {
16+
return new Promise((resolve, reject) => {
17+
const image = new Image();
18+
19+
image.onload = () => {
20+
resolve(image);
21+
};
22+
23+
image.onerror = reject;
24+
25+
image.src = dataURL;
26+
});
27+
}
28+
29+
function drawImage({ image, canvas }) {
30+
const context = canvas.getContext('2d');
31+
context.drawImage(image, 0, 0);
32+
}
33+
34+
function getImageData(canvas) {
35+
const context = canvas.getContext('2d');
36+
return context.getImageData(0, 0, canvas.width, canvas.height);
37+
}
38+
39+
const vertexShader = `
40+
varying vec2 vCoord;
41+
42+
void main() {
43+
vCoord = uv;
44+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
45+
}
46+
`;
47+
48+
const fragmentShader = `
49+
uniform sampler2D uImage;
50+
uniform sampler2D uFilter;
51+
varying vec2 vCoord;
52+
53+
vec4 toSRGB(vec4 color) {
54+
return vec4(pow(color.rgb, vec3(1.0 / 2.2)), color.a);
55+
}
56+
57+
void main() {
58+
vec4 color = texture2D(uImage, vCoord);
59+
60+
float r = floor(color.r * 255.0);
61+
float g = floor(color.g * 255.0);
62+
float b = floor(color.b * 255.0);
63+
64+
float x = r + floor(r / 16.0) * 256.0;
65+
float y = g + floor(b / 16.0) * 256.0;
66+
67+
vec2 coord = vec2(x / 4096.0, 1.0 - (y / 4096.0));
68+
69+
if (vCoord.x < 0.0) {
70+
gl_FragColor = color;
71+
} else {
72+
gl_FragColor = texture2D(uFilter, coord);
73+
}
74+
}
75+
`;
76+
77+
function createTexture(imageData) {
78+
const texture = new THREE.Texture(imageData);
79+
// texture.colorSpace = THREE.SRGBColorSpace;
80+
texture.minFilter = THREE.LinearFilter;
81+
texture.magFilter = THREE.LinearFilter;
82+
texture.needsUpdate = true;
83+
return texture;
84+
}
85+
86+
function drawImageData({ imageData, filterData, canvas }) {
87+
const { width, height } = imageData;
88+
const halfWidth = width / 2;
89+
const halfHeight = height / 2;
90+
91+
const renderer = new THREE.WebGLRenderer({
92+
canvas,
93+
antialias: false,
94+
alpha: false,
95+
});
96+
97+
const scene = new THREE.Scene();
98+
const camera = new THREE.OrthographicCamera(
99+
-halfWidth, halfWidth, halfHeight, -halfHeight, 1, 1000
100+
);
101+
camera.position.z = 10;
102+
scene.add(camera);
103+
104+
const geometry = new THREE.PlaneGeometry(width, height);
105+
const material = new THREE.ShaderMaterial({
106+
uniforms: {
107+
uImage: { value: createTexture(imageData) },
108+
uFilter: { value: createTexture(filterData) },
109+
},
110+
vertexShader,
111+
fragmentShader,
112+
});
113+
114+
const mesh = new THREE.Mesh(geometry, material);
115+
scene.add(mesh);
116+
117+
renderer.render(scene, camera);
118+
}
119+
120+
function calculateIndex(r, g, b) {
121+
const x = r + (b % 16) * 256;
122+
const y = g + Math.floor(b / 16) * 256;
123+
return y * 256 * 16 + x;
124+
}
125+
126+
class Application {
127+
filterData = null;
128+
129+
async selectFilter(event) {
130+
const file = event.target.files[0];
131+
if (!file) {
132+
return;
133+
}
134+
135+
const dataURL = await readFileAsDataURL(file);
136+
const image = await createImageFromAsDataURL(dataURL);
137+
138+
const filterCanvas = this.$refs.filterCanvas;
139+
filterCanvas.width = image.width;
140+
filterCanvas.height = image.height;
141+
142+
drawImage({ image, canvas: filterCanvas });
143+
144+
this.filterData = getImageData(filterCanvas);
145+
}
146+
147+
async selectImage(event) {
148+
if (!this.filterData) {
149+
alert('Please select a filter first.');
150+
return;
151+
}
152+
153+
const file = event.target.files[0];
154+
if (!file) {
155+
return;
156+
}
157+
158+
const dataURL = await readFileAsDataURL(file);
159+
const image = await createImageFromAsDataURL(dataURL);
160+
161+
const sourceCanvas = this.$refs.sourceCanvas;
162+
sourceCanvas.width = image.width;
163+
sourceCanvas.height = image.height;
164+
165+
const targetCanvas = this.$refs.targetCanvas;
166+
targetCanvas.width = image.width;
167+
targetCanvas.height = image.height;
168+
169+
drawImage({ image, canvas: sourceCanvas });
170+
171+
const imageData = getImageData(sourceCanvas);
172+
173+
drawImageData({
174+
imageData,
175+
filterData: this.filterData,
176+
canvas: targetCanvas,
177+
});
178+
}
179+
}

0 commit comments

Comments
 (0)