Skip to content

Commit c607935

Browse files
committed
Initial snapshot
0 parents  commit c607935

12 files changed

+9275
-0
lines changed

.editorconfig

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[*]
2+
end_of_line = lf
3+
insert_final_newline = true
4+
indent_style = space
5+
indent_size = 2
6+
trim_trailing_whitespace = true

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/node_modules

.prettierrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"printWidth": 80,
3+
"singleQuote": true
4+
}

package.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "threejs-crt-shader",
3+
"version": "0.0.1",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"storybook": "start-storybook -p 6006 -s public --no-dll",
8+
"build-storybook": "build-storybook -s public --no-dll"
9+
},
10+
"author": "Nick Matantsev <[email protected]>",
11+
"license": "MIT",
12+
"dependencies": {
13+
"@react-spring/three": "^9.0.0-rc.3",
14+
"@react-three/drei": "^2.2.13",
15+
"@types/node": "^14.14.11",
16+
"@types/react": "^17.0.0",
17+
"@types/react-dom": "^17.0.0",
18+
"konva": "^4.0.4",
19+
"onecolor": "^3.1.0",
20+
"react": "^16.14.0",
21+
"react-dom": "^16.14.0",
22+
"react-konva": "^16.9.0-0",
23+
"react-three-fiber": "^5.3.1",
24+
"react-use-gesture": "^9.0.0-beta.11",
25+
"three": "^0.121.0",
26+
"typescript": "^3.9.7"
27+
},
28+
"devDependencies": {
29+
"@babel/core": "^7.12.3",
30+
"@storybook/addon-actions": "^6.0.28",
31+
"@storybook/react": "^6.0.28",
32+
"babel-loader": "^8.2.1",
33+
"prettier": "^2.0.5",
34+
"react-is": "^16.8.0"
35+
}
36+
}

src/TestKonvaContent.jsx

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { Rect, Line, Text, Group } from 'react-konva';
3+
import onecolor from 'onecolor';
4+
5+
const GRID_CELL_COUNT = 5;
6+
const GRID_LINES = [...new Array(GRID_CELL_COUNT + 1).keys()].map(
7+
(i) => (i - GRID_CELL_COUNT * 0.5) / (GRID_CELL_COUNT * 0.5)
8+
);
9+
10+
const TestScroll = React.memo(({ width, height }) => {
11+
const [rows, setRows] = useState([0]);
12+
13+
useEffect(() => {
14+
const intervalId = window.setInterval(() => {
15+
setRows((prevRows) => {
16+
const newRows =
17+
Math.random() < 0.3
18+
? [...prevRows, Math.random() * 0.3].slice(-25)
19+
: [...prevRows];
20+
21+
newRows[newRows.length - 1] = Math.min(
22+
1,
23+
newRows[newRows.length - 1] + Math.random() * 0.15
24+
);
25+
26+
return newRows;
27+
});
28+
}, 50);
29+
30+
return () => {
31+
window.clearInterval(intervalId);
32+
};
33+
}, []);
34+
35+
return (
36+
<Group x={15} y={height - 15 - rows.length * 3}>
37+
{rows.map((amount, index) => {
38+
return (
39+
<Rect
40+
key={index}
41+
x={0}
42+
y={index * 3}
43+
width={Math.round(amount * 100)}
44+
height={2}
45+
fill="#80ff80"
46+
/>
47+
);
48+
})}
49+
</Group>
50+
);
51+
});
52+
53+
export const TestKonvaContent = ({ width, height }) => {
54+
const [blink, setBlink] = useState(false);
55+
56+
useEffect(() => {
57+
const intervalId = window.setInterval(() => {
58+
setBlink((prev) => !prev);
59+
}, 800);
60+
61+
return () => {
62+
window.clearInterval(intervalId);
63+
};
64+
}, []);
65+
66+
const [angleDeg, setAngleDeg] = useState(0);
67+
68+
useEffect(() => {
69+
const intervalId = window.setInterval(() => {
70+
setAngleDeg((prevAngleDeg) => (prevAngleDeg + 1.5) % 360);
71+
}, 50);
72+
73+
return () => {
74+
window.clearInterval(intervalId);
75+
};
76+
}, []);
77+
78+
const cx = width / 2,
79+
cy = height / 2;
80+
const currentColor = onecolor(['HSL', angleDeg / 360, 1, 0.5]).hex();
81+
const angle = (Math.PI * angleDeg) / 180;
82+
const acos = Math.cos(angle);
83+
const asin = Math.sin(angle);
84+
85+
const outA = [0, 0];
86+
const outB = [0, 0];
87+
function computeXYZ(gx, gy, out) {
88+
const wx = gx * acos - gy * asin;
89+
const wy = gx * asin + gy * acos;
90+
91+
const dz = wy + 6;
92+
const w = 1 / dz;
93+
out[0] = wx * (width / 2) * w;
94+
out[1] = -(-2.5 * (width / 2) * w) - 30;
95+
}
96+
97+
return (
98+
<>
99+
{GRID_LINES.map((gx) => {
100+
computeXYZ(2 * gx, -2, outA);
101+
computeXYZ(2 * gx, 2, outB);
102+
103+
return (
104+
<Line
105+
key={gx}
106+
points={[outA[0], outA[1], outB[0], outB[1]]}
107+
strokeWidth={1}
108+
x={width * 0.65}
109+
y={height * 0.6}
110+
stroke={currentColor}
111+
/>
112+
);
113+
})}
114+
115+
{GRID_LINES.map((gy) => {
116+
computeXYZ(-2, 2 * gy, outA);
117+
computeXYZ(2, 2 * gy, outB);
118+
119+
return (
120+
<Line
121+
key={gy}
122+
points={[outA[0], outA[1], outB[0], outB[1]]}
123+
strokeWidth={1}
124+
x={width * 0.65}
125+
y={height * 0.6}
126+
stroke={currentColor}
127+
/>
128+
);
129+
})}
130+
131+
<Rect
132+
x={2.5}
133+
y={22.5}
134+
width={width - 5}
135+
height={height - 25}
136+
stroke="#00ffff"
137+
strokeWidth={1}
138+
/>
139+
<Rect
140+
x={4.5}
141+
y={24.5}
142+
width={width - 9}
143+
height={height - 29}
144+
stroke="#00ffff"
145+
strokeWidth={1}
146+
/>
147+
148+
{blink && (
149+
<Text
150+
x={2}
151+
y={2}
152+
text="SYNTHESIS LEVEL 1"
153+
fontSize={16}
154+
fontFamily="Inconsolata"
155+
fill="#ffff00"
156+
/>
157+
)}
158+
159+
<Text
160+
x={15}
161+
y={36}
162+
text={`${Math.round(acos * 1000) / 1000}`}
163+
fontSize={10}
164+
fontFamily="Inconsolata"
165+
fill="#ff0000"
166+
/>
167+
168+
<Text
169+
x={15}
170+
y={44}
171+
text={`${Math.round(asin * 1000) / 1000}`}
172+
fontSize={10}
173+
fontFamily="Inconsolata"
174+
fill="#ff0000"
175+
/>
176+
177+
<TestScroll width={width} height={height} />
178+
</>
179+
);
180+
};

src/TestScene.jsx

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import React, { useMemo, useEffect, useRef } from 'react';
2+
import { useFrame } from 'react-three-fiber';
3+
import { useSpring, animated } from 'react-spring/three';
4+
import { useGLTF } from '@react-three/drei/useGLTF';
5+
import { Environment } from '@react-three/drei/Environment';
6+
import * as THREE from 'three';
7+
import { useGesture } from 'react-use-gesture';
8+
9+
import { CRTMaterial } from './components/CRTMaterial';
10+
11+
const MonitorCube = React.memo(({ canvasContext }) => {
12+
// simple box body with CRT surface nudged closer to viewer to avoid Z-fighting
13+
const boxWidth = 4 / 3;
14+
const boxHeight = 1;
15+
const boxDepth = 1;
16+
17+
return (
18+
<>
19+
<mesh>
20+
<boxBufferGeometry args={[boxWidth, boxHeight, boxDepth]} />
21+
<meshStandardMaterial
22+
color="#a0a8b0"
23+
metalness={0.5}
24+
roughness={0.15}
25+
/>
26+
</mesh>
27+
<mesh position={[0, 0, boxDepth * 0.5 + 0.001]}>
28+
<planeBufferGeometry args={[boxWidth - 0.01, boxHeight - 0.01]} />
29+
<CRTMaterial
30+
color="#808080"
31+
roughness={0.08}
32+
metalness={0.96}
33+
envMapIntensity={0.1}
34+
// screen contents
35+
canvasContext={canvasContext}
36+
// colour grading for the CRT emissive display
37+
crtFgColor={new THREE.Color('#f7f2e0')}
38+
crtBgColor={new THREE.Color('#100400')}
39+
// CRT screen space in object coords
40+
crtPlaneOrigin={[boxWidth * -0.5, boxHeight * -0.5, -0.08]} // bottom-left origin of CRT screen
41+
crtPlaneNormal={[0, 0, 1]} // direction where CRT screen is facing
42+
crtPlaneTangent={[boxWidth, 0, 0]} // X-axis extent of the CRT screen
43+
crtPlaneBitangent={[0, boxHeight, 0]} // Y-axis extent of the CRT screen
44+
/>
45+
</mesh>
46+
</>
47+
);
48+
});
49+
50+
export const TestScene = ({ cameraRef, canvasContext }) => {
51+
const { pos, up } = useSpring({
52+
from: {
53+
pos: [-1, -8, 1],
54+
up: [0, 1, 0],
55+
},
56+
to: {
57+
pos: [0, 0.2, 2.75],
58+
up: [0, 1, 0],
59+
},
60+
config: {
61+
tension: 5,
62+
friction: 10,
63+
},
64+
});
65+
66+
const [{ monitorRotation }, setMonitorAnim] = useSpring(() => ({
67+
from: {
68+
monitorRotation: [0, 0, 0],
69+
},
70+
to: {
71+
monitorRotation: [0, 0, 0],
72+
},
73+
config: {
74+
tension: 40,
75+
friction: 10,
76+
},
77+
}));
78+
79+
useGesture(
80+
{
81+
onMove: ({ xy: [x, y] }) => {
82+
setMonitorAnim({
83+
monitorRotation: [
84+
1.5 * (y / window.innerHeight - 0.5),
85+
1.5 * (x / window.innerWidth - 0.5),
86+
0,
87+
],
88+
});
89+
},
90+
},
91+
{ domTarget: window }
92+
);
93+
94+
const cameraTarget = useMemo(() => new THREE.Vector3(0, 0, 0), []);
95+
96+
useFrame(() => {
97+
if (!cameraRef.current) {
98+
throw new Error('no camera');
99+
}
100+
101+
cameraRef.current.lookAt(cameraTarget);
102+
});
103+
104+
return (
105+
<>
106+
<animated.perspectiveCamera ref={cameraRef} position={pos} up={up} />
107+
108+
<ambientLight color="#a08060" />
109+
<pointLight color="#f0e0ff" intensity={1.5} position={[3, 5, 5]} />
110+
111+
<React.Suspense fallback={<></>}>
112+
<animated.group rotation={monitorRotation}>
113+
<MonitorCube canvasContext={canvasContext} />
114+
</animated.group>
115+
116+
<Environment preset="warehouse" />
117+
</React.Suspense>
118+
</>
119+
);
120+
};

0 commit comments

Comments
 (0)