Compare commits

...

10 Commits

13 changed files with 359 additions and 5 deletions

4
.gitignore vendored
View File

@ -22,3 +22,7 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Specific to this project
scripts/in/*
scripts/out/*

View File

@ -11,5 +11,6 @@
"devDependencies": { "devDependencies": {
"typescript": "^5.5.3", "typescript": "^5.5.3",
"vite": "^5.4.1" "vite": "^5.4.1"
} },
"packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1"
} }

27
scripts/objParser.py Normal file
View File

@ -0,0 +1,27 @@
import sys
def conv_obj(obj):
vertex_positions = []
triangle_vertices = []
lines = obj.splitlines()
for line in lines:
line = line.strip()
if line.startswith("v "):
parts = line.split()
# or do parts[2] too if 3D, adjust with your axis
vertex_positions.append([float(parts[1]), float(parts[3])])
elif line.startswith("f "):
parts = line.split()
v1 = vertex_positions[int(parts[1].split("/")[0]) - 1]
v2 = vertex_positions[int(parts[2].split("/")[0]) - 1]
v3 = vertex_positions[int(parts[3].split("/")[0]) - 1]
triangle_vertices.extend(v1 + v2 + v3)
return triangle_vertices
if __name__ == "__main__":
obj_data = sys.stdin.read()
result = conv_obj(obj_data)
print(result)

View File

@ -0,0 +1,11 @@
{
"k": {
"vertices": [-1.0, -1.0, -1.0, 1.0, -1.0, 0.0, 0.05, 1.0, -1.0, 0.0, 0.05, -1.0]
},
"e": {
"vertices": [-1.0, -1.0, -1.0, 1.0, 0.0, -1.0, -1.0, -1.0, 0.0, 0.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0]
},
"a": {
"vertices": [-0.8, -1.0, 0.0, 1.0, 0.0, 1.0, 0.8, -1.0, -0.4, 0.0, 0.4, 0.0]
}
}

File diff suppressed because one or more lines are too long

12
src/geometry/Shape2D.ts Normal file
View File

@ -0,0 +1,12 @@
import Vector2D from "./Vector2D";
export default class Shape2D {
constructor(
public position: Vector2D,
public scale: number,
public color: [number, number, number],
public verticesCount: number,
public vao: WebGLVertexArrayObject,
public renderType: GLenum,
) {}
}

11
src/geometry/Vector2D.ts Normal file
View File

@ -0,0 +1,11 @@
export default class Vector2D {
constructor(
public x: number,
public y: number,
) {}
add(other: Vector2D) {
this.x += other.x;
this.y += other.y;
}
}

View File

@ -1,18 +1,168 @@
import { showError } from "./debug"; import { showError } from "./debug";
import "./style.css"; import "./style.css";
import vsSource from "./shaders/basic.vert?raw";
import fsSource from "./shaders/basic.frag?raw";
import { loadBuffer, loadProgram, loadShader } from "./utils/shader";
import Vector2D from "./geometry/Vector2D";
import { createBasicVao } from "./utils/vao";
import Shape2D from "./geometry/Shape2D";
import outline from "./data/letter_outline.json";
import solid from "./data/letter_triangle.json";
import { Timeline, Track } from "./utils/sequence";
function render() { function render() {
const canvas = document.querySelector<HTMLCanvasElement>("#canvas"); const canvas = document.querySelector<HTMLCanvasElement>("#canvas");
if (!canvas) { if (!canvas) {
throw new Error("Canvas does not exist"); throw new Error("Canvas does not exist");
} }
const gl = canvas.getContext("webgl"); const gl = canvas.getContext("webgl2");
if (!gl) { if (!gl) {
throw new Error("This browser does not support WebGL"); throw new Error("This browser does not support WebGL 2");
} }
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const program = loadProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);
const vertexPositionAttribLocation = gl.getAttribLocation(program, "vertexPosition");
if (vertexPositionAttribLocation < 0) {
throw new Error("Failed to get attrib location for vertexPosition");
} }
const kVerts = new Float32Array(outline.k.vertices);
const kBuffer = loadBuffer(gl, kVerts);
const kVao = createBasicVao(gl, kBuffer, vertexPositionAttribLocation);
const eVerts = new Float32Array(outline.e.vertices);
const eBuffer = loadBuffer(gl, eVerts);
const eVao = createBasicVao(gl, eBuffer, vertexPositionAttribLocation);
const aVerts = new Float32Array(outline.a.vertices);
const aBuffer = loadBuffer(gl, aVerts);
const aVao = createBasicVao(gl, aBuffer, vertexPositionAttribLocation);
const KVerts = new Float32Array(solid.k.vertices);
const KBuffer = loadBuffer(gl, KVerts);
const KVao = createBasicVao(gl, KBuffer, vertexPositionAttribLocation);
const EVerts = new Float32Array(solid.e.vertices);
const EBuffer = loadBuffer(gl, EVerts);
const EVao = createBasicVao(gl, EBuffer, vertexPositionAttribLocation);
const AVerts = new Float32Array(solid.a.vertices);
const ABuffer = loadBuffer(gl, AVerts);
const AVao = createBasicVao(gl, ABuffer, vertexPositionAttribLocation);
const shapes: Shape2D[] = [
new Shape2D(new Vector2D(0, 0), 200, [0.22745, 0.35294, 0.25098], kVerts.length / 2, kVao, gl.LINES),
new Shape2D(new Vector2D(0, 0), 200, [0.22745, 0.35294, 0.25098], eVerts.length / 2, eVao, gl.LINES),
new Shape2D(new Vector2D(0, 0), 200, [0.22745, 0.35294, 0.25098], aVerts.length / 2, aVao, gl.LINES),
new Shape2D(new Vector2D(0, 0), 800, [0.98823, 0.30980, 0.21960], KVerts.length / 2, KVao, gl.TRIANGLES),
new Shape2D(new Vector2D(0, 0), 800, [0.98823, 0.30980, 0.21960], EVerts.length / 2, EVao, gl.TRIANGLES),
new Shape2D(new Vector2D(0, 0), 800, [0.98823, 0.30980, 0.21960], AVerts.length / 2, AVao, gl.TRIANGLES),
]
const pTrackK = new Track([
{ time: 0.0, value: [300, -200] },
{ time: 0.5, value: [300, 300] },
]);
const pTrackE = new Track([
{ time: 0.5, value: [625, 900] },
{ time: 1.0, value: [625, 300] },
]);
const pTrackA = new Track([
{ time: 1.0, value: [900, -200] },
{ time: 1.5, value: [900, 300] },
]);
const qTrackK = new Track([
{ time: 4.5, value: [100, 610] },
{ time: 5.0, value: [100, 75] },
]);
const qTrackE = new Track([
{ time: 5.0, value: [450, -500] },
{ time: 5.5, value: [450, 75] },
]);
const qTrackA = new Track([
{ time: 5.5, value: [800, 610] },
{ time: 6.0, value: [800, 75] },
]);
const bgTrack = new Track([
{ time: 0, value: [0.0, 0.0, 0.0] },
{ time: 3, value: [0.0, 0.0, 0.0] },
{ time: 4, value: [0.22745, 0.35294, 0.25098] },
]);
const timeline = new Timeline({
"position0": pTrackK,
"position1": pTrackE,
"position2": pTrackA,
"position3": qTrackK,
"position4": qTrackE,
"position5": qTrackA,
"backgroundColor": bgTrack
}, 8, true);
const fragmentColorUniform = gl.getUniformLocation(program, "uColor");
const canvasSizeUniform = gl.getUniformLocation(program, "uCanvasSize");
const shapeLocationUniform = gl.getUniformLocation(program, "uLocation");
const shapeScaleUniform = gl.getUniformLocation(program, "uScale");
if (!(fragmentColorUniform && canvasSizeUniform && shapeLocationUniform && shapeScaleUniform)) {
throw new Error(`Some uniform not found:
${fragmentColorUniform ? "" : "uColor"};
${canvasSizeUniform ? "" : "uCanvasSize"};
${shapeLocationUniform ? "" : "uLocation"};
${shapeScaleUniform ? "" : "uScale"};
`);
}
let startTime = performance.now();
const frame = () => {
const currentTime = (performance.now() - startTime) / 1000;
const interpolatedValues = timeline.update(currentTime);
const [r, g, b, a] = interpolatedValues.backgroundColor as number[];
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
gl.clearColor(r, g, b, a);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.uniform2f(canvasSizeUniform, canvas.width, canvas.height);
shapes.forEach((shape, index) => {
const positionKey = `position${index}`;
const [x, y] = interpolatedValues[positionKey] as number[];
gl.uniform3f(fragmentColorUniform, shape.color[0], shape.color[1], shape.color[2]);
gl.uniform2f(shapeLocationUniform, x, y);
gl.uniform1f(shapeScaleUniform, shape.scale);
gl.bindVertexArray(shape.vao);
gl.drawArrays(shape.renderType, 0, shape.verticesCount);
});
requestAnimationFrame(frame)
}
requestAnimationFrame(frame);
}
try { try {
render(); render();
} catch (err) { } catch (err) {

10
src/shaders/basic.frag Normal file
View File

@ -0,0 +1,10 @@
#version 300 es
precision mediump float;
uniform vec3 uColor;
out vec4 fragColor;
void main() {
fragColor = vec4(uColor, 1.0);
}

15
src/shaders/basic.vert Normal file
View File

@ -0,0 +1,15 @@
#version 300 es
precision mediump float;
uniform vec2 uCanvasSize;
uniform vec2 uLocation;
uniform float uScale;
in vec2 vertexPosition;
void main() {
vec2 finalVertexPosition = vertexPosition * uScale + uLocation;
vec2 clipPosition = (finalVertexPosition / uCanvasSize) * 2.0 - 1.0;
gl_Position = vec4(clipPosition, 0.0, 1.0);
}

70
src/utils/sequence.ts Normal file
View File

@ -0,0 +1,70 @@
export type InterpolatableValue = number | number[];
interface Keyframe {
time: number;
value: InterpolatableValue;
}
export class Track {
private readonly keyframes: Keyframe[];
constructor(keyframes: Keyframe[]) {
this.keyframes = keyframes.sort((a, b) => a.time - b.time);
}
interpolate(time: number): InterpolatableValue {
const { keyframes } = this;
const lastIndex = keyframes.length - 1;
if (time <= keyframes[0].time) return keyframes[0].value;
if (time >= keyframes[lastIndex].time) return keyframes[lastIndex].value;
const nextIndex = keyframes.findIndex(kf => kf.time > time);
const prevIndex = nextIndex - 1;
const [prevKf, nextKf] = [keyframes[prevIndex], keyframes[nextIndex]];
const t = (time - prevKf.time) / (nextKf.time - prevKf.time);
return this.interpolateValues(prevKf.value, nextKf.value, t);
}
private interpolateValues(start: InterpolatableValue, end: InterpolatableValue, t: number): InterpolatableValue {
if (Array.isArray(start) && Array.isArray(end)) {
return start.map((v, i) => v + t * (end[i] - v));
}
if (typeof start === 'number' && typeof end === 'number') {
return start + t * (end - start);
}
throw new Error('Mismatched value types in keyframes');
}
}
export class Timeline {
private readonly tracks: Record<string, Track>;
private readonly duration: number;
private loop: boolean;
constructor(tracks: Record<string, Track>, duration: number, loop: boolean = false) {
this.tracks = tracks;
this.duration = duration;
this.loop = loop;
}
setLoop(loop: boolean): void {
this.loop = loop;
}
update(time: number): Record<string, InterpolatableValue> {
let adjustedTime = time;
if (this.loop) {
adjustedTime = time % this.duration;
} else {
adjustedTime = Math.min(time, this.duration);
}
return Object.fromEntries(
Object.entries(this.tracks).map(([key, track]) => [key, track.interpolate(adjustedTime)])
);
}
}

View File

@ -1,4 +1,4 @@
export function loadShader(gl: WebGLRenderingContext, type: GLenum, source: string) { export function loadShader(gl: WebGL2RenderingContext, type: GLenum, source: string) {
const shader = gl.createShader(type); const shader = gl.createShader(type);
if (!shader) { if (!shader) {
throw new Error("WebGL fails to create shader for given type"); throw new Error("WebGL fails to create shader for given type");
@ -11,10 +11,12 @@ export function loadShader(gl: WebGLRenderingContext, type: GLenum, source: stri
const compileError = gl.getShaderInfoLog(shader); const compileError = gl.getShaderInfoLog(shader);
throw new Error("Error when compiling shader: " + compileError); throw new Error("Error when compiling shader: " + compileError);
} }
return shader;
} }
export function loadProgram(gl: WebGLRenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader) { export function loadProgram(gl: WebGL2RenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader) {
const program = gl.createProgram(); const program = gl.createProgram();
if (!program) { if (!program) {
throw new Error("WebGL fails to create program"); throw new Error("WebGL fails to create program");
@ -28,4 +30,19 @@ export function loadProgram(gl: WebGLRenderingContext, vertexShader: WebGLShader
const linkError = gl.getProgramInfoLog(program); const linkError = gl.getProgramInfoLog(program);
throw new Error("Error when linking program: " + linkError); throw new Error("Error when linking program: " + linkError);
} }
return program;
}
export function loadBuffer(gl: WebGL2RenderingContext, vertices: Float32Array) {
const buffer = gl.createBuffer();
if (!buffer) {
throw new Error("WebGL fails to create buffer");
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
return buffer;
} }

15
src/utils/vao.ts Normal file
View File

@ -0,0 +1,15 @@
export function createBasicVao(gl: WebGL2RenderingContext, buffer: WebGLBuffer, attrib: number) {
const vao = gl.createVertexArray();
if (!vao) {
throw new Error("Failed to create VAO");
}
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(attrib);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(attrib, 2, gl.FLOAT, false, 0, 0);
gl.bindVertexArray(null);
return vao;
}