initial commit
This commit is contained in:
commit
e8c3f9862a
193 files changed
+29795
No files matched your search
@@ -0,0 +1,231 @@
|
||||
const MAX_RINGS = 40;
|
||||
const MAX_GLOWS = 60;
|
||||
const MAX_STREAKS = 90;
|
||||
const MAX_ARCS = 28;
|
||||
|
||||
const easeOut = (t) => 1 - (1 - t) * (1 - t);
|
||||
|
||||
export class Effects {
|
||||
constructor() {
|
||||
this.rings = [];
|
||||
this.glows = [];
|
||||
this.streaks = [];
|
||||
this.arcs = [];
|
||||
}
|
||||
|
||||
ring(x, y, {
|
||||
endRadius = 140,
|
||||
width = 5,
|
||||
life = 0.5,
|
||||
color = [1, 1, 1, 1],
|
||||
additive = true,
|
||||
} = {}) {
|
||||
if (this.rings.length >= MAX_RINGS) this.rings.shift();
|
||||
this.rings.push({
|
||||
x, y,
|
||||
radius: endRadius * 0.12,
|
||||
endRadius,
|
||||
width,
|
||||
life,
|
||||
maxLife: life,
|
||||
color,
|
||||
additive,
|
||||
});
|
||||
}
|
||||
|
||||
glow(x, y, {
|
||||
radius = 90,
|
||||
life = 0.35,
|
||||
color = [1, 1, 1, 1],
|
||||
additive = true,
|
||||
} = {}) {
|
||||
if (this.glows.length >= MAX_GLOWS) this.glows.shift();
|
||||
this.glows.push({
|
||||
x, y,
|
||||
radius: radius * 0.2,
|
||||
endRadius: radius,
|
||||
life,
|
||||
maxLife: life,
|
||||
color,
|
||||
additive,
|
||||
});
|
||||
}
|
||||
|
||||
streak(x, y, angle, length, {
|
||||
width = 3,
|
||||
life = 0.25,
|
||||
fade = true,
|
||||
color = [1, 1, 1, 1],
|
||||
additive = true,
|
||||
tail = 0.7,
|
||||
} = {}) {
|
||||
if (this.streaks.length >= MAX_STREAKS) this.streaks.shift();
|
||||
this.streaks.push({
|
||||
x: x - Math.cos(angle) * length * tail,
|
||||
y: y - Math.sin(angle) * length * tail,
|
||||
angle,
|
||||
length,
|
||||
width,
|
||||
life,
|
||||
maxLife: life,
|
||||
fade,
|
||||
color,
|
||||
additive,
|
||||
tail,
|
||||
});
|
||||
}
|
||||
|
||||
arc(x1, y1, x2, y2, {
|
||||
segments = 8,
|
||||
width = 3,
|
||||
life = 0.3,
|
||||
color = [1, 1, 1, 1],
|
||||
jitter = 26,
|
||||
additive = true,
|
||||
} = {}) {
|
||||
if (this.arcs.length >= MAX_ARCS) this.arcs.shift();
|
||||
this.arcs.push({
|
||||
x1, y1, x2, y2,
|
||||
segments,
|
||||
width,
|
||||
life,
|
||||
maxLife: life,
|
||||
color,
|
||||
jitter,
|
||||
additive,
|
||||
seed: Math.random() * 100,
|
||||
});
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
const tick = (list) => {
|
||||
for (let i = list.length - 1; i >= 0; i -= 1) {
|
||||
list[i].life -= dt;
|
||||
if (list[i].life <= 0) list.splice(i, 1);
|
||||
}
|
||||
};
|
||||
tick(this.rings);
|
||||
tick(this.glows);
|
||||
tick(this.streaks);
|
||||
tick(this.arcs);
|
||||
}
|
||||
|
||||
draw(renderer) {
|
||||
for (const list of [this.glows, this.rings, this.streaks, this.arcs]) {
|
||||
const additive = list.filter((effect) => effect.additive);
|
||||
const normal = list.filter((effect) => !effect.additive);
|
||||
|
||||
if (additive.length > 0) renderer.setBlendMode('additive');
|
||||
for (const effect of additive) this._drawEffect(renderer, effect);
|
||||
if (additive.length > 0) renderer.setBlendMode('normal');
|
||||
|
||||
for (const effect of normal) this._drawEffect(renderer, effect);
|
||||
}
|
||||
}
|
||||
|
||||
_drawEffect(renderer, effect) {
|
||||
const t = 1 - effect.life / effect.maxLife;
|
||||
const alpha = effect.life / effect.maxLife;
|
||||
|
||||
if (effect.x1 !== undefined) {
|
||||
this._drawArc(renderer, effect, t, alpha);
|
||||
} else if (effect.angle !== undefined) {
|
||||
this._drawStreak(renderer, effect, t, alpha);
|
||||
} else if (effect.width !== undefined) {
|
||||
this._drawRing(renderer, effect, t, alpha);
|
||||
} else {
|
||||
this._drawGlow(renderer, effect, t, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
_drawGlow(renderer, effect, t, alpha) {
|
||||
const radius = effect.radius + (effect.endRadius - effect.radius) * easeOut(t);
|
||||
renderer.drawTexture(
|
||||
renderer.glowTexture,
|
||||
effect.x - radius,
|
||||
effect.y - radius,
|
||||
radius * 2,
|
||||
radius * 2,
|
||||
[
|
||||
effect.color[0],
|
||||
effect.color[1],
|
||||
effect.color[2],
|
||||
effect.color[3] * alpha,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_drawRing(renderer, effect, t, alpha) {
|
||||
const radius = effect.radius + (effect.endRadius - effect.radius) * easeOut(t);
|
||||
const fade = Math.min(1, alpha * 1.3);
|
||||
renderer.drawTexture(
|
||||
renderer.ringTexture,
|
||||
effect.x - radius,
|
||||
effect.y - radius,
|
||||
radius * 2,
|
||||
radius * 2,
|
||||
[
|
||||
effect.color[0],
|
||||
effect.color[1],
|
||||
effect.color[2],
|
||||
Math.max(0, effect.color[3] * fade),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_drawStreak(renderer, effect, t, alpha) {
|
||||
const progress = easeOut(t);
|
||||
const len = effect.length * (1 - progress * (1 - effect.tail));
|
||||
const fade = effect.fade ? alpha : 1;
|
||||
renderer.drawTexture(
|
||||
renderer.glowTexture,
|
||||
effect.x,
|
||||
effect.y,
|
||||
len,
|
||||
effect.width,
|
||||
[
|
||||
effect.color[0],
|
||||
effect.color[1],
|
||||
effect.color[2],
|
||||
effect.color[3] * fade,
|
||||
],
|
||||
effect.angle,
|
||||
);
|
||||
}
|
||||
|
||||
_drawArc(renderer, effect, t, alpha) {
|
||||
const { x1, y1, x2, y2, segments, width, jitter, seed, color } = effect;
|
||||
const wobble = 0.45 + 0.55 * Math.abs(Math.sin(seed + t * 34));
|
||||
const amp = jitter * alpha * wobble;
|
||||
|
||||
let prevX = x1;
|
||||
let prevY = y1;
|
||||
for (let i = 1; i <= segments; i += 1) {
|
||||
const u = i / segments;
|
||||
const edge = (i === segments) ? 0 : 1;
|
||||
const px = x1 + (x2 - x1) * u + (Math.random() - 0.5) * 2 * amp * edge;
|
||||
const py = y1 + (y2 - y1) * u + (Math.random() - 0.5) * 2 * amp * edge;
|
||||
const len = Math.hypot(px - prevX, py - prevY);
|
||||
if (len > 0) {
|
||||
renderer.drawTexture(
|
||||
renderer.whiteTexture,
|
||||
prevX,
|
||||
prevY,
|
||||
len,
|
||||
width,
|
||||
[color[0], color[1], color[2], color[3] * alpha],
|
||||
Math.atan2(py - prevY, px - prevX),
|
||||
);
|
||||
}
|
||||
prevX = px;
|
||||
prevY = py;
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.rings.length = 0;
|
||||
this.glows.length = 0;
|
||||
this.streaks.length = 0;
|
||||
this.arcs.length = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Vector2D } from './Vector2D.js';
|
||||
|
||||
export class GameObject {
|
||||
constructor(x = 0, y = 0) {
|
||||
this.pos = new Vector2D(x, y);
|
||||
this.velocity = new Vector2D(0, 0);
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
}
|
||||
|
||||
update(dt = 1 / 60) {
|
||||
this.pos = this.velocity.mulS(dt).addV(this.pos);
|
||||
}
|
||||
|
||||
intersect(other) {
|
||||
return (
|
||||
Math.abs(this.pos.x - other.pos.x) * 2 < this.width + other.width &&
|
||||
Math.abs(this.pos.y - other.pos.y) * 2 < this.height + other.height
|
||||
);
|
||||
}
|
||||
|
||||
draw() {}
|
||||
|
||||
remove(container) {
|
||||
if (!container) return;
|
||||
const index = container.indexOf(this);
|
||||
if (index !== -1) container.splice(index, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Vector2D } from './Vector2D.js';
|
||||
|
||||
export class Input {
|
||||
constructor(canvas, viewport) {
|
||||
this.keys = new Set();
|
||||
this.pressed = new Set();
|
||||
this.mouse = new Vector2D(viewport.width / 2, viewport.height / 2);
|
||||
this.mouseDown = false;
|
||||
this.mouseClicked = false;
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
this.keys.add(event.key.toLowerCase());
|
||||
if (!event.repeat) {
|
||||
this.pressed.add(event.key.toLowerCase());
|
||||
}
|
||||
if (event.key === ' ') {
|
||||
event.preventDefault();
|
||||
if (
|
||||
document.activeElement &&
|
||||
document.activeElement.tagName === 'BUTTON' &&
|
||||
typeof document.activeElement.blur === 'function'
|
||||
) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
});
|
||||
window.addEventListener('keyup', (event) => {
|
||||
this.keys.delete(event.key.toLowerCase());
|
||||
});
|
||||
window.addEventListener('blur', () => {
|
||||
this.keys.clear();
|
||||
this.pressed.clear();
|
||||
this.mouseDown = false;
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
this.mouse.x = ((event.clientX - rect.left) / rect.width) * viewport.width;
|
||||
this.mouse.y = ((event.clientY - rect.top) / rect.height) * viewport.height;
|
||||
});
|
||||
canvas.addEventListener('mousedown', (event) => {
|
||||
if (event.button === 0) {
|
||||
this.mouseDown = true;
|
||||
this.mouseClicked = true;
|
||||
}
|
||||
});
|
||||
window.addEventListener('mouseup', () => {
|
||||
this.mouseDown = false;
|
||||
});
|
||||
}
|
||||
|
||||
clearPressed() {
|
||||
this.pressed.clear();
|
||||
this.mouseClicked = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Vector2D } from './Vector2D.js';
|
||||
|
||||
const MAX_PARTICLES = 450;
|
||||
|
||||
export class ParticleSystem {
|
||||
constructor() {
|
||||
this.particles = [];
|
||||
}
|
||||
|
||||
burst(
|
||||
x,
|
||||
y,
|
||||
{
|
||||
count = 12,
|
||||
speed = 150,
|
||||
direction = 0,
|
||||
spread = Math.PI * 2,
|
||||
life = 0.4,
|
||||
size = 4,
|
||||
color = [1, 1, 1, 1],
|
||||
gravity = 0,
|
||||
drag = 0,
|
||||
shape = 'square',
|
||||
additive = false,
|
||||
endSize = null,
|
||||
spin = 0,
|
||||
} = {},
|
||||
) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (this.particles.length >= MAX_PARTICLES) return;
|
||||
|
||||
const angle = direction + (Math.random() - 0.5) * spread;
|
||||
const magnitude = speed * (0.4 + Math.random() * 0.8);
|
||||
const velocity = new Vector2D(Math.cos(angle), Math.sin(angle)).mulS(magnitude);
|
||||
const lifetime = life * (0.6 + Math.random() * 0.6);
|
||||
const startSize = size * (0.6 + Math.random() * 0.8);
|
||||
|
||||
this.particles.push({
|
||||
pos: new Vector2D(x, y),
|
||||
velocity,
|
||||
gravity,
|
||||
drag,
|
||||
life: lifetime,
|
||||
maxLife: lifetime,
|
||||
size: startSize,
|
||||
endSize: endSize === null ? startSize * 0.15 : endSize * (0.6 + Math.random() * 0.8),
|
||||
color: [...color],
|
||||
shape,
|
||||
additive,
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
spin: (Math.random() - 0.5) * (spin * 2),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
const { particles } = this;
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const particle = particles[i];
|
||||
particle.pos = particle.velocity.mulS(dt).addV(particle.pos);
|
||||
particle.velocity.y += particle.gravity * dt;
|
||||
if (particle.drag > 0) {
|
||||
particle.velocity = particle.velocity.mulS(Math.max(0, 1 - particle.drag * dt));
|
||||
}
|
||||
particle.angle += particle.spin * dt;
|
||||
particle.life -= dt;
|
||||
if (particle.life <= 0) {
|
||||
particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw(renderer) {
|
||||
const additive = this.particles.filter((particle) => particle.additive);
|
||||
const normal = this.particles.filter((particle) => !particle.additive);
|
||||
|
||||
if (additive.length > 0) renderer.setBlendMode('additive');
|
||||
for (const particle of additive) this._drawParticle(renderer, particle);
|
||||
if (additive.length > 0) renderer.setBlendMode('normal');
|
||||
|
||||
for (const particle of normal) this._drawParticle(renderer, particle);
|
||||
}
|
||||
|
||||
_drawParticle(renderer, particle) {
|
||||
const alpha = Math.max(0, particle.life / particle.maxLife);
|
||||
const progress = 1 - alpha;
|
||||
const size = particle.size + (particle.endSize - particle.size) * progress;
|
||||
const color = [
|
||||
particle.color[0],
|
||||
particle.color[1],
|
||||
particle.color[2],
|
||||
particle.color[3] * alpha,
|
||||
];
|
||||
const texture = particle.shape === 'glow' ? renderer.glowTexture : renderer.whiteTexture;
|
||||
renderer.drawTexture(
|
||||
texture,
|
||||
particle.pos.x - size / 2,
|
||||
particle.pos.y - size / 2,
|
||||
size,
|
||||
size,
|
||||
color,
|
||||
particle.angle,
|
||||
);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.particles.length = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import { Texture } from './Texture.js';
|
||||
|
||||
const VERTEX_SOURCE = `#version 300 es
|
||||
layout(location = 0) in vec2 aPosition;
|
||||
|
||||
uniform mat4 uProjection;
|
||||
uniform mat4 uModel;
|
||||
|
||||
out vec2 vTexCoord;
|
||||
|
||||
void main() {
|
||||
vTexCoord = aPosition;
|
||||
gl_Position = uProjection * uModel * vec4(aPosition, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const FRAGMENT_SOURCE = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 vTexCoord;
|
||||
out vec4 outColor;
|
||||
|
||||
uniform sampler2D uSampler;
|
||||
uniform vec4 uColor;
|
||||
|
||||
void main() {
|
||||
outColor = texture(uSampler, vTexCoord) * uColor;
|
||||
}
|
||||
`;
|
||||
|
||||
const QUAD_POSITIONS = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
|
||||
const QUAD_INDICES = new Uint16Array([0, 1, 2, 2, 1, 3]);
|
||||
|
||||
function compileShader(gl, type, source) {
|
||||
const shader = gl.createShader(type);
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
const message = gl.getShaderInfoLog(shader);
|
||||
gl.deleteShader(shader);
|
||||
throw new Error(`Shader compile error: ${message}`);
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
export class Renderer {
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
|
||||
const gl = canvas.getContext('webgl2', {
|
||||
alpha: true,
|
||||
premultipliedAlpha: false,
|
||||
antialias: true,
|
||||
});
|
||||
if (!gl) {
|
||||
throw new Error('WebGL2 is not supported by this browser.');
|
||||
}
|
||||
this.gl = gl;
|
||||
|
||||
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SOURCE);
|
||||
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SOURCE);
|
||||
|
||||
this.program = gl.createProgram();
|
||||
gl.attachShader(this.program, vertexShader);
|
||||
gl.attachShader(this.program, fragmentShader);
|
||||
gl.linkProgram(this.program);
|
||||
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
|
||||
const message = gl.getProgramInfoLog(this.program);
|
||||
throw new Error(`Program link error: ${message}`);
|
||||
}
|
||||
gl.deleteShader(vertexShader);
|
||||
gl.deleteShader(fragmentShader);
|
||||
|
||||
this.projectionLocation = gl.getUniformLocation(this.program, 'uProjection');
|
||||
this.modelLocation = gl.getUniformLocation(this.program, 'uModel');
|
||||
this.colorLocation = gl.getUniformLocation(this.program, 'uColor');
|
||||
this.samplerLocation = gl.getUniformLocation(this.program, 'uSampler');
|
||||
|
||||
this._centeredProjection = new Float32Array(16);
|
||||
this._modelMatrix = new Float32Array(16);
|
||||
|
||||
this._setupQuad();
|
||||
this.resize(canvas.width, canvas.height);
|
||||
|
||||
gl.enable(gl.BLEND);
|
||||
gl.disable(gl.DEPTH_TEST);
|
||||
gl.disable(gl.CULL_FACE);
|
||||
|
||||
this.setBlendMode('normal');
|
||||
|
||||
gl.useProgram(this.program);
|
||||
gl.uniform1i(this.samplerLocation, 0);
|
||||
}
|
||||
|
||||
setBlendMode(mode) {
|
||||
this._blendMode = mode;
|
||||
const { gl } = this;
|
||||
if (mode === 'additive') {
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
|
||||
} else {
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
}
|
||||
|
||||
clear(r = 0, g = 0, b = 0, a = 0) {
|
||||
const { gl } = this;
|
||||
gl.clearColor(r, g, b, a);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
createTextureFromImage(image) {
|
||||
const texture = new Texture(this.gl, image.width, image.height);
|
||||
texture.uploadImage(image);
|
||||
return texture;
|
||||
}
|
||||
|
||||
get whiteTexture() {
|
||||
if (!this._whiteTexture) {
|
||||
this._whiteTexture = new Texture(this.gl, 1, 1);
|
||||
}
|
||||
return this._whiteTexture;
|
||||
}
|
||||
|
||||
get glowTexture() {
|
||||
if (!this._glowTexture) {
|
||||
this._glowTexture = this._buildRadialTexture(64, (ctx, size) => {
|
||||
const gradient = ctx.createRadialGradient(
|
||||
size / 2, size / 2, 0,
|
||||
size / 2, size / 2, size / 2,
|
||||
);
|
||||
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
|
||||
gradient.addColorStop(0.35, 'rgba(255, 255, 255, 0.6)');
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
});
|
||||
}
|
||||
return this._glowTexture;
|
||||
}
|
||||
|
||||
get ringTexture() {
|
||||
if (!this._ringTexture) {
|
||||
this._ringTexture = this._buildRadialTexture(128, (ctx, size) => {
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.lineWidth = size * 0.06;
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2, size * 0.42, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
const gradient = ctx.createRadialGradient(
|
||||
size / 2, size / 2, size * 0.28,
|
||||
size / 2, size / 2, size * 0.52,
|
||||
);
|
||||
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.5)');
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2, size * 0.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
return this._ringTexture;
|
||||
}
|
||||
|
||||
_buildRadialTexture(size, draw) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d');
|
||||
draw(ctx, size);
|
||||
const texture = new Texture(this.gl, size, size);
|
||||
texture.uploadCanvas(canvas);
|
||||
return texture;
|
||||
}
|
||||
|
||||
drawTexture(texture, x, y, width, height, color = [1, 1, 1, 1], angle = 0) {
|
||||
const { gl } = this;
|
||||
|
||||
this._setModelMatrix(x, y, width, height, angle);
|
||||
|
||||
gl.useProgram(this.program);
|
||||
gl.uniformMatrix4fv(this.projectionLocation, false, this._centeredProjection);
|
||||
gl.uniformMatrix4fv(this.modelLocation, false, this._modelMatrix);
|
||||
gl.uniform4f(this.colorLocation, ...color);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture.handle);
|
||||
gl.bindVertexArray(this._quadVao);
|
||||
gl.drawElements(gl.TRIANGLES, QUAD_INDICES.length, gl.UNSIGNED_SHORT, 0);
|
||||
gl.bindVertexArray(null);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
}
|
||||
|
||||
resize(deviceWidth, deviceHeight, worldWidth = deviceWidth, worldHeight = deviceHeight) {
|
||||
this.canvas.width = deviceWidth;
|
||||
this.canvas.height = deviceHeight;
|
||||
this.gl.viewport(0, 0, deviceWidth, deviceHeight);
|
||||
this._worldWidth = worldWidth;
|
||||
this._worldHeight = worldHeight;
|
||||
this._syncProjection(0, 0, worldWidth, worldHeight);
|
||||
}
|
||||
|
||||
setViewOffset(dx = 0, dy = 0) {
|
||||
this._syncProjection(dx, dy, this._worldWidth, this._worldHeight);
|
||||
}
|
||||
|
||||
_setupQuad() {
|
||||
const { gl } = this;
|
||||
|
||||
this._quadVao = gl.createVertexArray();
|
||||
gl.bindVertexArray(this._quadVao);
|
||||
|
||||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, QUAD_POSITIONS, gl.STATIC_DRAW);
|
||||
gl.enableVertexAttribArray(0);
|
||||
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
const indexBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, QUAD_INDICES, gl.STATIC_DRAW);
|
||||
|
||||
gl.bindVertexArray(null);
|
||||
}
|
||||
|
||||
_setModelMatrix(x, y, width, height, angle = 0) {
|
||||
const m = this._modelMatrix;
|
||||
if (angle === 0) {
|
||||
m[0] = width; m[1] = 0; m[2] = 0; m[3] = 0;
|
||||
m[4] = 0; m[5] = height; m[6] = 0; m[7] = 0;
|
||||
m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0;
|
||||
m[12] = x; m[13] = y; m[14] = 0; m[15] = 1;
|
||||
return;
|
||||
}
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
const cx = x + width / 2;
|
||||
const cy = y + height / 2;
|
||||
m[0] = width * cos; m[1] = width * sin; m[2] = 0; m[3] = 0;
|
||||
m[4] = -height * sin; m[5] = height * cos; m[6] = 0; m[7] = 0;
|
||||
m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0;
|
||||
m[12] = cx; m[13] = cy; m[14] = 0; m[15] = 1;
|
||||
}
|
||||
|
||||
_syncProjection(left, top, width, height) {
|
||||
const m = this._centeredProjection;
|
||||
m[0] = 2 / width; m[1] = 0; m[2] = 0; m[3] = 0;
|
||||
m[4] = 0; m[5] = -2 / height; m[6] = 0; m[7] = 0;
|
||||
m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0;
|
||||
m[12] = -1 - (2 * left) / width; m[13] = 1 + (2 * top) / height; m[14] = 0; m[15] = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
export class SoundSystem {
|
||||
constructor({ muted = false } = {}) {
|
||||
this.muted = muted;
|
||||
this.context = null;
|
||||
this.master = null;
|
||||
this.delaySend = null;
|
||||
this.musicOn = false;
|
||||
this.musicTimer = null;
|
||||
this._musicStep = 0;
|
||||
this._nextNoteTime = 0;
|
||||
this._baseStepDur = 60 / 132 / 4;
|
||||
this._stepDur = this._baseStepDur;
|
||||
this._intensity = 0;
|
||||
this._bossMode = false;
|
||||
this._stepsPerBar = 16;
|
||||
this._progression = [
|
||||
{ root: 73.42, chord: [146.83, 174.61, 220] },
|
||||
{ root: 58.27, chord: [116.54, 146.83, 174.61] },
|
||||
{ root: 87.31, chord: [174.61, 220, 261.63] },
|
||||
{ root: 65.41, chord: [130.81, 164.81, 196] },
|
||||
];
|
||||
this._bossProgression = [
|
||||
{ root: 41.2, chord: [82.41, 98, 123.47] },
|
||||
{ root: 65.41, chord: [130.81, 164.81, 196] },
|
||||
{ root: 55, chord: [110, 130.81, 164.81] },
|
||||
{ root: 73.42, chord: [146.83, 174.61, 220] },
|
||||
];
|
||||
this._arpPattern = [0, 1, 2, 1, 0, 2, 1, 2, 0, 1, 2, 1, 0, 2, 1, 2];
|
||||
this._leadPattern = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[587.33, 0, 698.46, 0, 880, 0, 783.99, 0, 698.46, 0, 659.26, 0, 587.33, 0, 523.25, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 440, 0, 523.25, 0, 587.33, 0, 0, 0],
|
||||
];
|
||||
this._leadPatternB = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[659.26, 0, 880, 0, 987.77, 0, 880, 0, 783.99, 0, 698.46, 0, 659.26, 0, 0, 0],
|
||||
[0, 0, 0, 0, 587.33, 0, 659.26, 0, 698.46, 0, 659.26, 0, 587.33, 0, 0, 0],
|
||||
];
|
||||
this._bassPattern = [1, 0, 1, 0, 1, 0, 1, 0];
|
||||
this._barPlan = { fill: false, breakdown: false, lift: false, leadVariant: 0, bar: -1 };
|
||||
|
||||
const unlock = () => {
|
||||
if (!this.context) {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
if (!AudioContext) return;
|
||||
this.context = new AudioContext();
|
||||
this.master = this.context.createGain();
|
||||
this.master.gain.value = this.muted ? 0 : 0.5;
|
||||
this.master.connect(this.context.destination);
|
||||
|
||||
const delay = this.context.createDelay(0.6);
|
||||
delay.delayTime.value = 0.31;
|
||||
const feedback = this.context.createGain();
|
||||
feedback.gain.value = 0.35;
|
||||
const wet = this.context.createGain();
|
||||
wet.gain.value = 0.22;
|
||||
delay.connect(feedback);
|
||||
feedback.connect(delay);
|
||||
delay.connect(wet);
|
||||
wet.connect(this.master);
|
||||
this.delaySend = delay;
|
||||
}
|
||||
if (this.context.state === 'suspended') {
|
||||
this.context.resume().catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', unlock, { once: false });
|
||||
window.addEventListener('pointerdown', unlock, { once: false });
|
||||
}
|
||||
|
||||
startMusic() {
|
||||
if (this.musicOn) return;
|
||||
this.musicOn = true;
|
||||
this._musicStep = 0;
|
||||
this._nextNoteTime = this.context ? this.context.currentTime + 0.1 : 0;
|
||||
if (this.musicTimer) clearInterval(this.musicTimer);
|
||||
this.musicTimer = setInterval(() => this._scheduleMusic(), 100);
|
||||
}
|
||||
|
||||
stopMusic() {
|
||||
this.musicOn = false;
|
||||
if (this.musicTimer) {
|
||||
clearInterval(this.musicTimer);
|
||||
this.musicTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
setIntensity(intensity) {
|
||||
const clamped = Math.max(0, Math.min(1, intensity));
|
||||
this._intensity = clamped;
|
||||
this._stepDur = this._baseStepDur * (1 - 0.35 * clamped);
|
||||
}
|
||||
|
||||
setBossMode(on) {
|
||||
this._bossMode = on;
|
||||
}
|
||||
|
||||
_scheduleMusic() {
|
||||
if (!this.context || !this.musicOn) return;
|
||||
const aheadTime = this.context.currentTime + 0.5;
|
||||
const totalSteps = this._stepsPerBar * this._progression.length;
|
||||
while (this._nextNoteTime < aheadTime) {
|
||||
try {
|
||||
this._scheduleStep(this._musicStep, this._nextNoteTime);
|
||||
} catch (error) {
|
||||
console.error('Music schedule error', error);
|
||||
}
|
||||
this._nextNoteTime += this._stepDur;
|
||||
this._musicStep = (this._musicStep + 1) % totalSteps;
|
||||
}
|
||||
}
|
||||
|
||||
_rollBarPlan(bar) {
|
||||
const plan = { fill: false, breakdown: false, lift: false, leadVariant: 0, bar };
|
||||
if (bar === 1) {
|
||||
const roll = Math.random();
|
||||
if (roll < 0.2) {
|
||||
plan.fill = true;
|
||||
} else if (roll < 0.32) {
|
||||
plan.breakdown = true;
|
||||
}
|
||||
} else if (bar === 2 || bar === 3) {
|
||||
plan.leadVariant = Math.random() < 0.35 ? 1 : 0;
|
||||
plan.lift = Math.random() < 0.25;
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
_scheduleStep(step, time) {
|
||||
const delay = time - this.context.currentTime;
|
||||
const bar = Math.floor(step / this._stepsPerBar) % this._progression.length;
|
||||
const beat = step % this._stepsPerBar;
|
||||
const barDur = this._stepDur * this._stepsPerBar;
|
||||
const intensity = this._intensity;
|
||||
const drumGain = 1 + intensity * 0.6;
|
||||
|
||||
if (beat === 0 || this._barPlan.bar !== bar) {
|
||||
this._barPlan = this._rollBarPlan(bar);
|
||||
}
|
||||
const plan = this._barPlan;
|
||||
const fill = plan.fill;
|
||||
const breakdown = plan.breakdown;
|
||||
const boss = this._bossMode;
|
||||
|
||||
let { root, chord } = boss
|
||||
? this._bossProgression[bar]
|
||||
: this._progression[bar];
|
||||
if (plan.lift && bar === 2 && !boss) {
|
||||
chord = [174.61, 207.65, 261.63];
|
||||
}
|
||||
|
||||
if (boss && beat === 0) {
|
||||
this.tone(27.5, 'sine', barDur, 0.06, 27.5, delay);
|
||||
}
|
||||
|
||||
if (beat === 0) {
|
||||
for (const note of chord) {
|
||||
this.tone(note, 'sawtooth', barDur, 0.02 + intensity * 0.01, note, delay);
|
||||
}
|
||||
}
|
||||
|
||||
const kickFreq = boss ? 44 : 55;
|
||||
const snareFreq = boss ? 700 : 1800;
|
||||
|
||||
if (!fill && !breakdown) {
|
||||
if (beat % 4 === 2 && this._bassPattern[Math.floor(beat / 4)]) {
|
||||
this.tone(root, 'square', this._stepDur * 1.5, 0.055, root, delay);
|
||||
}
|
||||
if (intensity > 0.4 && beat % 2 === 0) {
|
||||
this.tone(root * 2, 'square', this._stepDur * 0.9, 0.02, root * 2, delay);
|
||||
}
|
||||
if (beat % 4 === 0) {
|
||||
this.tone(kickFreq, 'sine', 0.14, 0.1 * drumGain, Math.max(28, kickFreq - 18), delay);
|
||||
}
|
||||
if (beat === 4 || beat === 12) {
|
||||
this.noise(0.1, 0.07 * drumGain, snareFreq, delay);
|
||||
}
|
||||
if (beat % 4 === 2) {
|
||||
this.noise(0.03, 0.02 * drumGain, 9000, delay);
|
||||
}
|
||||
if (beat % 4 === 3 || beat % 4 === 1) {
|
||||
this.noise(0.02, 0.012 * drumGain, 10000, delay);
|
||||
}
|
||||
|
||||
if (beat % 4 === 1 || beat % 4 === 3) {
|
||||
const arp = chord[this._arpPattern[beat]];
|
||||
this.tone(arp, 'triangle', 0.1, 0.018, arp, delay, this.delaySend);
|
||||
this.tone(arp * 2, 'triangle', 0.12, 0.012, arp * 2, delay);
|
||||
}
|
||||
} else if (fill) {
|
||||
if (beat % 2 === 0) {
|
||||
this.tone(55, 'sine', 0.1, 0.06, 34, delay);
|
||||
}
|
||||
if (beat >= 12) {
|
||||
this.noise(0.06, 0.045, 1500, delay);
|
||||
}
|
||||
if (beat % 2 === 1) {
|
||||
this.noise(0.03, 0.018, 8500, delay);
|
||||
}
|
||||
if (beat % 3 === 0) {
|
||||
this.noise(0.05, 0.012, 400 + beat * 160, delay);
|
||||
}
|
||||
const index = beat >= 12 ? (beat - 12) % 3 : Math.floor(beat / 2) % 3;
|
||||
const note = chord[index] * (beat >= 12 ? 4 : 2);
|
||||
this.tone(note, 'triangle', 0.09, 0.02, note, delay);
|
||||
} else if (breakdown) {
|
||||
if (beat === 0 || beat === 8) {
|
||||
this.tone(55, 'sine', 0.18, 0.08, 32, delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fill && !breakdown) {
|
||||
const lead = (plan.leadVariant === 1 ? this._leadPatternB : this._leadPattern)[bar][beat];
|
||||
if (lead > 0) {
|
||||
this.tone(lead, 'sawtooth', 0.22, 0.03, lead, delay, this.delaySend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setMuted(muted) {
|
||||
this.muted = muted;
|
||||
if (this.master && this.context) {
|
||||
this.master.gain.setTargetAtTime(muted ? 0 : 0.5, this.context.currentTime, 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
toggleMuted() {
|
||||
this.setMuted(!this.muted);
|
||||
return this.muted;
|
||||
}
|
||||
|
||||
tone(freq, type, duration, gain, glideTo = freq, delay = 0, bus = this.master) {
|
||||
if (!this.context || this.muted) return;
|
||||
if (!Number.isFinite(freq) || !Number.isFinite(glideTo)) return;
|
||||
const { context } = this;
|
||||
const start = context.currentTime + delay;
|
||||
const oscillator = context.createOscillator();
|
||||
const shape = context.createGain();
|
||||
|
||||
oscillator.type = type;
|
||||
oscillator.frequency.setValueAtTime(Math.max(1, freq), start);
|
||||
oscillator.frequency.exponentialRampToValueAtTime(Math.max(1, glideTo), start + duration);
|
||||
|
||||
shape.gain.setValueAtTime(0.0001, start);
|
||||
shape.gain.exponentialRampToValueAtTime(gain, start + 0.008);
|
||||
shape.gain.exponentialRampToValueAtTime(0.0001, start + duration);
|
||||
|
||||
oscillator.connect(shape);
|
||||
shape.connect(bus || this.master);
|
||||
oscillator.start(start);
|
||||
oscillator.stop(start + duration + 0.02);
|
||||
}
|
||||
|
||||
noise(duration, gain, filterFreq, delay = 0, bus = this.master) {
|
||||
if (!this.context || this.muted) return;
|
||||
const { context } = this;
|
||||
const start = context.currentTime + delay;
|
||||
const bufferSize = Math.max(1, Math.ceil(context.sampleRate * duration));
|
||||
const buffer = context.createBuffer(1, bufferSize, context.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i += 1) {
|
||||
data[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
const source = context.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
const filter = context.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.value = filterFreq;
|
||||
filter.Q.value = 1.2;
|
||||
|
||||
const shape = context.createGain();
|
||||
shape.gain.setValueAtTime(0.0001, start);
|
||||
shape.gain.exponentialRampToValueAtTime(gain, start + 0.005);
|
||||
shape.gain.exponentialRampToValueAtTime(0.0001, start + duration);
|
||||
|
||||
source.connect(filter);
|
||||
filter.connect(shape);
|
||||
shape.connect(bus || this.master);
|
||||
source.start(start);
|
||||
source.stop(start + duration + 0.02);
|
||||
}
|
||||
|
||||
shoot() {
|
||||
this.tone(240, 'square', 0.1, 0.075, 85);
|
||||
this.noise(0.05, 0.028, 3200);
|
||||
}
|
||||
|
||||
hit() {
|
||||
this.noise(0.12, 0.16, 1500);
|
||||
this.tone(180, 'triangle', 0.1, 0.12, 120);
|
||||
}
|
||||
|
||||
death(big = false) {
|
||||
if (big) {
|
||||
this.noise(0.35, 0.22, 700);
|
||||
this.tone(120, 'sawtooth', 0.3, 0.2, 45);
|
||||
this.tone(90, 'sine', 0.4, 0.24, 30);
|
||||
return;
|
||||
}
|
||||
this.noise(0.2, 0.14, 500);
|
||||
this.tone(140, 'triangle', 0.22, 0.16, 55);
|
||||
}
|
||||
|
||||
damage() {
|
||||
this.tone(160, 'sawtooth', 0.2, 0.18, 80);
|
||||
this.noise(0.15, 0.1, 900);
|
||||
}
|
||||
|
||||
explosion() {
|
||||
this.noise(0.7, 0.3, 400);
|
||||
this.tone(200, 'sawtooth', 0.6, 0.22, 30);
|
||||
this.tone(60, 'sine', 0.8, 0.3, 20);
|
||||
}
|
||||
|
||||
pickup() {
|
||||
this.tone(660, 'sine', 0.12, 0.14, 660);
|
||||
this.tone(990, 'sine', 0.18, 0.14, 990, 0.09);
|
||||
}
|
||||
|
||||
stingerStart() {
|
||||
this.tone(392, 'triangle', 0.12, 0.1);
|
||||
this.tone(523, 'triangle', 0.12, 0.1, undefined, 0.1);
|
||||
this.tone(659, 'triangle', 0.3, 0.12, undefined, 0.2);
|
||||
}
|
||||
|
||||
bossEntrance() {
|
||||
this.tone(27.5, 'sine', 1.6, 0.22, 27.5);
|
||||
this.tone(110, 'sawtooth', 0.5, 0.1, 55);
|
||||
this.noise(0.5, 0.12, 250);
|
||||
this.tone(82.41, 'square', 0.25, 0.08, 82.41, 0.7);
|
||||
this.tone(61.74, 'square', 0.4, 0.08, 61.74, 0.95);
|
||||
}
|
||||
|
||||
playGameOverMusic() {
|
||||
this.stopMusic();
|
||||
this._playGameOverSeq();
|
||||
}
|
||||
|
||||
_playGameOverSeq() {
|
||||
if (!this.context) return;
|
||||
const now = this.context.currentTime;
|
||||
const t = (offset) => Math.max(0, offset - now);
|
||||
|
||||
this.tone(55, 'sine', 8, 0.05, 51, t(0));
|
||||
this.tone(55, 'sine', 8, 0.03, 55, t(4));
|
||||
|
||||
const chords = [
|
||||
[110, 164.81, 220],
|
||||
[98, 146.83, 196],
|
||||
[87.31, 130.81, 174.61],
|
||||
[82.41, 123.47, 164.81],
|
||||
];
|
||||
chords.forEach((chord, i) => {
|
||||
const start = 0.2 + i * 1.5;
|
||||
for (const note of chord) {
|
||||
this.tone(note, 'sawtooth', 1.5, 0.018, note, t(start));
|
||||
}
|
||||
if (i === 0 || i === 2) {
|
||||
this.tone(55, 'sine', 0.14, 0.09, 32, t(start));
|
||||
}
|
||||
});
|
||||
|
||||
const melody = [880, 783.99, 698.46, 659.26, 587.33, 523.25, 493.88, 440];
|
||||
melody.forEach((note, i) => {
|
||||
this.tone(note, 'triangle', 0.7, 0.032, note, t(0.5 + i * 0.55), this.delaySend);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Texture } from './Texture.js';
|
||||
|
||||
export class TextRenderer {
|
||||
constructor(gl) {
|
||||
this.gl = gl;
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this._cachedKey = null;
|
||||
this._cachedEntry = null;
|
||||
}
|
||||
|
||||
draw(renderer, text, x, y, options = {}) {
|
||||
const entry = this._resolve(text, options);
|
||||
renderer.drawTexture(entry.texture, x, y, entry.width, entry.height);
|
||||
return entry;
|
||||
}
|
||||
|
||||
measure(text, options = {}) {
|
||||
return this._resolve(text, options);
|
||||
}
|
||||
|
||||
_resolve(text, options = {}) {
|
||||
const key = this._makeKey(text, options);
|
||||
if (this._cachedKey !== key) {
|
||||
this._flushCache();
|
||||
this._cachedKey = key;
|
||||
this._cachedEntry = this._rasterize(text, options);
|
||||
}
|
||||
return this._cachedEntry;
|
||||
}
|
||||
|
||||
_makeKey(text, { size = 20, font = 'arial', fillStyle = 'black', bold = true }) {
|
||||
return `${text}|${size}|${font}|${fillStyle}|${bold}`;
|
||||
}
|
||||
|
||||
_rasterize(text, { size = 20, font = 'arial', fillStyle = 'black', bold = true }) {
|
||||
const { ctx, canvas } = this;
|
||||
const pad = 4;
|
||||
const fontString = `${bold ? 'bold ' : ''}${size}px ${font}`;
|
||||
|
||||
ctx.font = fontString;
|
||||
const textWidth = Math.ceil(ctx.measureText(text).width);
|
||||
const width = textWidth + pad * 2;
|
||||
const height = Math.ceil(size * 1.3) + pad * 2;
|
||||
const scale = Math.min(window.devicePixelRatio || 1, 2);
|
||||
|
||||
canvas.width = Math.max(1, Math.ceil(width * scale));
|
||||
canvas.height = Math.max(1, Math.ceil(height * scale));
|
||||
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
ctx.font = fontString;
|
||||
ctx.fillStyle = fillStyle;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
ctx.fillText(text, pad, pad + size);
|
||||
|
||||
const texture = new Texture(this.gl, canvas.width, canvas.height);
|
||||
texture.uploadCanvas(canvas);
|
||||
|
||||
return { texture, width, height };
|
||||
}
|
||||
|
||||
_flushCache() {
|
||||
if (this._cachedEntry) {
|
||||
this._cachedEntry.texture.dispose();
|
||||
this._cachedEntry = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
export class Texture {
|
||||
constructor(gl, width = 1, height = 1) {
|
||||
this.gl = gl;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.handle = gl.createTexture();
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.handle);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RGBA,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
new Uint8Array([255, 255, 255, 255]),
|
||||
);
|
||||
this._configureSampling();
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
}
|
||||
|
||||
uploadImage(image) {
|
||||
const { gl } = this;
|
||||
this.width = image.width;
|
||||
this.height = image.height;
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.handle);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
}
|
||||
|
||||
uploadCanvas(canvas) {
|
||||
const { gl } = this;
|
||||
this.width = canvas.width;
|
||||
this.height = canvas.height;
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.handle);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
}
|
||||
|
||||
_configureSampling() {
|
||||
const { gl } = this;
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.gl.deleteTexture(this.handle);
|
||||
this.handle = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
export class Vector2D {
|
||||
constructor(x = 0, y = 0) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
mulS(scalar) {
|
||||
return new Vector2D(this.x * scalar, this.y * scalar);
|
||||
}
|
||||
|
||||
mulV(vector) {
|
||||
return new Vector2D(this.x * vector.x, this.y * vector.y);
|
||||
}
|
||||
|
||||
divS(scalar) {
|
||||
return new Vector2D(this.x / scalar, this.y / scalar);
|
||||
}
|
||||
|
||||
divV(vector) {
|
||||
return new Vector2D(this.x / vector.x, this.y / vector.y);
|
||||
}
|
||||
|
||||
addS(scalar) {
|
||||
return new Vector2D(this.x + scalar, this.y + scalar);
|
||||
}
|
||||
|
||||
addV(vector) {
|
||||
return new Vector2D(this.x + vector.x, this.y + vector.y);
|
||||
}
|
||||
|
||||
subS(scalar) {
|
||||
return new Vector2D(this.x - scalar, this.y - scalar);
|
||||
}
|
||||
|
||||
subV(vector) {
|
||||
return new Vector2D(this.x - vector.x, this.y - vector.y);
|
||||
}
|
||||
|
||||
dot(vector) {
|
||||
return this.x * vector.x + this.y * vector.y;
|
||||
}
|
||||
|
||||
abs() {
|
||||
return new Vector2D(Math.abs(this.x), Math.abs(this.y));
|
||||
}
|
||||
|
||||
length() {
|
||||
return Math.sqrt(this.dot(this));
|
||||
}
|
||||
|
||||
lengthSqrt() {
|
||||
return this.dot(this);
|
||||
}
|
||||
|
||||
normalize() {
|
||||
const length = this.length();
|
||||
this.x /= length;
|
||||
this.y /= length;
|
||||
return this;
|
||||
}
|
||||
|
||||
rotate(angle) {
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
return new Vector2D(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user