@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
343 lines (249 loc) • 8.74 kB
JavaScript
import { assert } from "../../../../core/assert.js";
import AABB2 from "../../../../core/geom/2d/aabb/AABB2.js";
import { m3_cm_compose_transform } from "../../../../core/geom/mat3/m3_cm_compose_transform.js";
import { m3_cm_invert } from "../../../../core/geom/mat3/m3_cm_invert.js";
import { v2_length } from "../../../../core/geom/vec2/v2_length.js";
import { v2_matrix3_cm_multiply } from "../../../../core/geom/vec2/v2_matrix3_cm_multiply.js";
import { CanvasView } from "../../../../view/elements/CanvasView.js";
import EmptyView from "../../../../view/elements/EmptyView.js";
import { canvas2d_draw_grid } from "../../../graphics/canvas/canvas2d_draw_grid.js";
import { animation_curve_compute_aabb } from "../animation_curve_compute_aabb.js";
import { AnimationCurve } from "../AnimationCurve.js";
const scratch_v2_a = new Float32Array(2);
const scratch_v2_b = new Float32Array(2);
export class AnimationCurveView extends EmptyView {
#curve = AnimationCurve.constant();
#canvas = new CanvasView();
#frame = new AABB2();
#curve_bounds = new AABB2();
/**
* 3x3 transform matrix
* Transform from curve space to canvas space
* @type {Float32Array}
*/
#transform = new Float32Array(9);
/**
* 3x3 inverse of transform matrix
* @type {Float32Array}
*/
#transform_inverse = new Float32Array(9);
/**
*
* @type {number}
*/
#margin = 36
/**
*
* @param {AnimationCurve} v
*/
set curve(v) {
this.#curve = v;
this.update();
}
constructor() {
super();
this.addChild(this.#canvas);
this.size.onChanged.add((x, y) => {
this.#canvas.size.set(x, y);
this.update();
});
}
auto_set_frame() {
animation_curve_compute_aabb(this.#curve_bounds, this.#curve);
this.#frame.copy(this.#curve_bounds);
if (this.#frame.height === 0) {
this.#frame.growHeight(0.5);
}
if (this.#frame.width === 0) {
this.#frame.growWidth(0.5);
}
const margin_top = this.#margin;
const margin_bottom = this.#margin;
const margin_left = this.#margin;
const margin_right = this.#margin;
const normalized_margin_left = (margin_left / this.size.x) * this.#frame.width;
const normalized_margin_right = (margin_right / this.size.x) * this.#frame.width;
const normalized_margin_top = (margin_top / this.size.y) * this.#frame.height;
const normalized_margin_bottom = (margin_bottom / this.size.y) * this.#frame.height;
this.#frame.x0 -= normalized_margin_left;
this.#frame.x1 += normalized_margin_right;
this.#frame.y0 -= normalized_margin_top;
this.#frame.y1 += normalized_margin_bottom;
this.#update_frame();
}
#update_frame() {
const frame = this.#frame;
const size = this.size;
const scale_x = size.x / frame.width;
const scale_y = -size.y / frame.height;
m3_cm_compose_transform(
this.#transform,
-frame.x0, -frame.y1,
scale_x, scale_y,
frame.x0, frame.y1, 0
);
m3_cm_invert(this.#transform_inverse, this.#transform);
}
/**
*
* @param {Float32Array|number[]} out
* @param {number} x
* @param {number} y
*/
point_curve_to_canvas(out, x, y) {
assert.notNaN(x, 'x');
assert.notNaN(y, 'y');
v2_matrix3_cm_multiply(out, 0,
x, y, this.#transform
);
}
/**
*
* @param {Float32Array|number[]} out
* @param {number} x
* @param {number} y
*/
point_canvas_to_curve(out, x, y) {
v2_matrix3_cm_multiply(out, 0,
x, y, this.#transform_inverse
);
}
update() {
if (this.size.x <= 0 || this.size.y <= 0) {
// size too small
return;
}
this.auto_set_frame();
this.draw();
}
draw() {
const ctx = this.#canvas.context2d;
ctx.fillStyle = '#222222';
ctx.fillRect(0, 0, this.size.x, this.size.y);
this.draw_grid();
ctx.fillStyle = 'none';
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 1;
this.draw_curve();
ctx.fillStyle = 'none';
ctx.strokeStyle = '#0080ff';
ctx.lineWidth = 1;
this.draw_tangents();
this.draw_key_knots();
}
draw_grid() {
const ctx = this.#canvas.context2d;
const width = this.size.x;
const height = this.size.y;
canvas2d_draw_grid({
ctx,
width,
height,
color: '#262626',
spacing: 32,
offset_x: 0
});
canvas2d_draw_grid({
ctx,
width,
height,
color: '#303030',
spacing: 32,
offset_x: 16,
offset_y: 16
});
}
draw_key_knots() {
const keys = this.#curve.keys;
const key_count = keys.length;
for (let i = 0; i < key_count; i++) {
this.draw_key_knot(keys[i]);
}
}
draw_key_knot(key) {
const ctx = this.#canvas.context2d;
this.point_curve_to_canvas(scratch_v2_a, key.time, key.value);
const stroke_width = 1;
const radius = 2;
ctx.beginPath();
ctx.arc(scratch_v2_a[0], scratch_v2_a[1], radius + stroke_width * 0.5, 0, 2 * Math.PI, false);
ctx.fillStyle = 'green';
ctx.fill();
ctx.lineWidth = stroke_width;
ctx.strokeStyle = 'rgba(0,133,0,0.2)';
ctx.stroke();
}
/**
*
* @param {Keyframe} keyframe
*/
draw_key_tangents(keyframe) {
const handle_length = 36;
const ctx = this.#canvas.context2d;
// incoming
const in_angle = -Math.atan2(keyframe.inTangent, -1);
this.draw_tangent(ctx, keyframe, in_angle, handle_length);
// outgoing
const ut_angle = Math.atan2(keyframe.outTangent, 1);
this.draw_tangent(ctx, keyframe, ut_angle, handle_length);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {Keyframe} keyframe
* @param {number} angle
* @param {number} handle_length in pixels
*/
draw_tangent(ctx, keyframe, angle, handle_length) {
ctx.beginPath();
this.point_curve_to_canvas(scratch_v2_a, keyframe.time, keyframe.value);
ctx.moveTo(scratch_v2_a[0], scratch_v2_a[1]);
this.point_curve_to_canvas(scratch_v2_b,
keyframe.time + Math.cos(angle),
keyframe.value + Math.sin(angle)
);
// a - b
scratch_v2_b[0] -= scratch_v2_a[0];
scratch_v2_b[1] -= scratch_v2_a[1];
// normalize to desired length
const norm = handle_length / v2_length(scratch_v2_b[0], scratch_v2_b[1]);
scratch_v2_b[0] *= norm;
scratch_v2_b[1] *= norm;
// restore offset
scratch_v2_b[0] += scratch_v2_a[0];
scratch_v2_b[1] += scratch_v2_a[1];
ctx.lineTo(scratch_v2_b[0], scratch_v2_b[1]);
ctx.stroke();
}
draw_tangents() {
const curve = this.#curve;
const keys = curve.keys;
for (let i = 0; i < keys.length; i++) {
const keyframe = keys[i];
this.draw_key_tangents(keyframe);
}
}
draw_curve() {
const ctx = this.#canvas.context2d;
ctx.beginPath();
const curve = this.#curve;
const keys = curve.keys;
if (keys.length < 2) {
return;
}
const duration = curve.duration;
const first_key = curve.keys[0];
const time_start = first_key.time;
const segments = Math.ceil(this.size.x / 4);
this.point_curve_to_canvas(scratch_v2_a, first_key.time, first_key.value);
ctx.moveTo(scratch_v2_a[0], scratch_v2_a[1]);
for (let i = 1; i < segments; i++) {
const t = i / (segments - 1);
const time = time_start + duration * t;
const value = curve.evaluate(time);
this.point_curve_to_canvas(scratch_v2_a, time, value);
ctx.lineTo(scratch_v2_a[0], scratch_v2_a[1]);
}
ctx.stroke();
}
}