Skip to content

Commit 32ad28b

Browse files
committed
Image Filter Demo
1 parent 906c831 commit 32ad28b

File tree

6 files changed

+312
-0
lines changed

6 files changed

+312
-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?e8c51b8b87e5dbdc7f5ab9aa4feb7554"></script>
44+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
45+
</body>
46+
</html>
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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 filterColor(vec4 color) {
54+
float r = floor(color.r * 255.0);
55+
float g = floor(color.g * 255.0);
56+
float b = floor(color.b * 255.0);
57+
58+
float x = r + mod(b, 16.0) * 256.0 + 0.5;
59+
float y = g + floor(b / 16.0) * 256.0 + 0.5;
60+
61+
vec2 coord = vec2(x / 4096.0, 1.0 - (y / 4096.0));
62+
63+
return texture2D(uFilter, coord);
64+
}
65+
66+
void main() {
67+
vec4 color = texture2D(uImage, vCoord);
68+
if (vCoord.x > 0.0) {
69+
color = filterColor(color);
70+
}
71+
gl_FragColor = color;
72+
}
73+
`;
74+
75+
function createTexture(imageData) {
76+
const texture = new THREE.Texture(imageData);
77+
texture.minFilter = THREE.NearestFilter;
78+
texture.magFilter = THREE.NearestFilter;
79+
texture.generateMipmaps = false;
80+
texture.needsUpdate = true;
81+
return texture;
82+
}
83+
84+
function drawImageData({ imageData, filterData, canvas }) {
85+
const { width, height } = imageData;
86+
const halfWidth = width / 2;
87+
const halfHeight = height / 2;
88+
89+
const renderer = new THREE.WebGLRenderer({
90+
canvas,
91+
antialias: false,
92+
alpha: false,
93+
});
94+
95+
const scene = new THREE.Scene();
96+
const camera = new THREE.OrthographicCamera(
97+
-halfWidth, halfWidth, halfHeight, -halfHeight, 1, 1000
98+
);
99+
camera.position.z = 10;
100+
scene.add(camera);
101+
102+
const geometry = new THREE.PlaneGeometry(width, height);
103+
const material = new THREE.ShaderMaterial({
104+
uniforms: {
105+
uImage: { value: createTexture(imageData) },
106+
uFilter: { value: createTexture(filterData) },
107+
},
108+
vertexShader,
109+
fragmentShader,
110+
});
111+
112+
const mesh = new THREE.Mesh(geometry, material);
113+
scene.add(mesh);
114+
115+
renderer.render(scene, camera);
116+
}
117+
118+
function calculateIndex(r, g, b) {
119+
const x = r + (b % 16) * 256;
120+
const y = g + Math.floor(b / 16) * 256;
121+
return y * 256 * 16 + x;
122+
}
123+
124+
class Application {
125+
filterData = null;
126+
127+
async selectFilter(event) {
128+
const file = event.target.files[0];
129+
if (!file) {
130+
return;
131+
}
132+
133+
const dataURL = await readFileAsDataURL(file);
134+
const image = await createImageFromAsDataURL(dataURL);
135+
136+
const filterCanvas = this.$refs.filterCanvas;
137+
filterCanvas.width = image.width;
138+
filterCanvas.height = image.height;
139+
140+
drawImage({ image, canvas: filterCanvas });
141+
142+
this.filterData = getImageData(filterCanvas);
143+
}
144+
145+
async selectImage(event) {
146+
if (!this.filterData) {
147+
alert('Please select a filter first.');
148+
return;
149+
}
150+
151+
const file = event.target.files[0];
152+
if (!file) {
153+
return;
154+
}
155+
156+
const dataURL = await readFileAsDataURL(file);
157+
const image = await createImageFromAsDataURL(dataURL);
158+
159+
const sourceCanvas = this.$refs.sourceCanvas;
160+
sourceCanvas.width = image.width;
161+
sourceCanvas.height = image.height;
162+
163+
const targetCanvas = this.$refs.targetCanvas;
164+
targetCanvas.width = image.width;
165+
targetCanvas.height = image.height;
166+
167+
drawImage({ image, canvas: sourceCanvas });
168+
169+
const imageData = getImageData(sourceCanvas);
170+
171+
drawImageData({
172+
imageData,
173+
filterData: this.filterData,
174+
canvas: targetCanvas,
175+
});
176+
}
177+
}

0 commit comments

Comments
 (0)