UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

343 lines (249 loc) • 8.74 kB
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(); } }