UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

363 lines (292 loc) • 11.6 kB
import Signal from "../../../../core/events/signal/Signal.js"; import AABB2 from "../../../../core/geom/2d/aabb/AABB2.js"; import Vector2 from "../../../../core/geom/Vector2.js"; import ObservedValue from "../../../../core/model/ObservedValue.js"; import { number_pretty_print } from "../../../../core/primitives/numbers/number_pretty_print.js"; import LabelView from "../../../../view/common/LabelView.js"; import { CSS_ABSOLUTE_POSITIONING } from "../../../../view/CSS_ABSOLUTE_POSITIONING.js"; import { CanvasView } from "../../../../view/elements/CanvasView.js"; import EmptyView from "../../../../view/elements/EmptyView.js"; import { canvas2d_plot_data_line } from "../../../graphics/canvas/canvas2d_plot_data_line.js"; import { MouseEvents } from "../../../input/devices/events/MouseEvents.js"; import { readPositionFromMouseEvent } from "../../../input/devices/PointerDevice.js"; import { DraggableAspect } from "../../../ui/DraggableAspect.js"; import { animation_curve_compute_aabb } from "../animation_curve_compute_aabb.js"; import { sample_animation_curve_to_float_array } from "../compression/sample_animation_curve_to_float_array.js"; import { Keyframe } from "../Keyframe.js"; import { build_tangent_editor } from "./build_tangent_editor.js"; /** * * @param {Vector2} size * @param {AABB2} frame * @param {Vector2} margin * @param {number} x * @param {number} y * @return {Vector2} */ function position_curve_to_canvas(size, frame, margin, x, y) { const width = size.x; const height = size.y; const curve_width = frame.getWidth(); const curve_width_multiplier = curve_width > 0 ? 1 / curve_width : 0; const u = (x - frame.x0) * curve_width_multiplier; const curve_height = frame.getHeight(); const curve_height_multiplier = curve_height > 0 ? 1 / curve_height : 0; const v = 1 - (y - frame.y0) * curve_height_multiplier; return new Vector2(margin.x + (u * (width - margin.x * 2)), margin.y + v * (height - margin.y * 2)); } /** * * @param {Vector2} size * @param {AABB2} frame * @param {Vector2} margin * @param {number} x * @param {number} y * @return {Vector2} */ function position_canvas_to_curve(size, frame, margin, x, y) { const width = size.x; const height = size.y; const u = (x - margin.x) / (width - margin.x * 2); const v = 1 - (y - margin.y) / (height - margin.y * 2); return new Vector2( u * frame.getWidth() + frame.x0, v * frame.getHeight() + frame.y0 ); } /** * * @param {AnimationCurve} curve * @param {Vector2} [size] * @param {Vector2} [margin] How much space to leave empty around the plotted bounds * @returns {View} */ export function build_curve_editor({ curve, size = new Vector2(300, 300), margin = new Vector2(36, 36) }) { const vContainer = new EmptyView({ css: { pointerEvents: "auto" } }); const graph = new CanvasView(); graph.size.copy(size); const frame = new AABB2(); const frame_updated = new Signal(); /** * * @type {ObservedValue<Keyframe>} */ const active_keyframe = new ObservedValue(null); const vCoordinate = new LabelView("", { css: { ...CSS_ABSOLUTE_POSITIONING, zIndex: 1000, background: `rgba(0, 0, 0, 0.4)`, padding: '4px 6px', color: 'white', font: '12px Tahoma, monospaced' } }); function handle_curve_update() { animation_curve_compute_aabb(frame, curve); frame_updated.send0(); update_graph(); } function update_graph() { const width = graph.size.x; const data = new Float32Array(width); sample_animation_curve_to_float_array(data, 0, curve, data.length); canvas2d_plot_data_line({ data, ctx: graph.context2d, width: width, height: graph.size.y, margin: margin, range_y: [frame.y0, frame.y1], range_x: [frame.x0, frame.x1], }); } const keyframe_views = new Map(); /** * * @param {Keyframe} keyframe */ function add_keyframe(keyframe) { const vContainerMarker = new EmptyView({ css: { position: "absolute", top: 0, left: 0, pointerEvents: "none" } }); const marker_size = 8; const marker = new EmptyView({ css: { width: `${marker_size}px`, height: `${marker_size}px`, background: "#00ff00", border: "none", position: "absolute", top: `-${marker_size / 2}px`, left: `-${marker_size / 2}px`, pointerEvents: "auto", borderRadius: `${marker_size}px` } }); // add extra element for mouse event capture to make it easier to click on the marker const marker_hit_pad_size = 24; marker.addChild(new EmptyView({ css: { opacity: 0, width: `${marker_hit_pad_size}px`, height: `${marker_hit_pad_size}px`, borderRadius: `${marker_hit_pad_size}px`, // background: 'rgba(255,0,0,0.2)', position: "absolute", top: `-${(marker_hit_pad_size - marker_size) / 2}px`, left: `-${(marker_hit_pad_size - marker_size) / 2}px` } })) function updatePosition() { const canvas_coordinates = position_curve_to_canvas( graph.size, frame, margin, keyframe.time, keyframe.value ); vContainerMarker.position.copy(canvas_coordinates); } function updateActiveState() { if (active_keyframe.get() === keyframe) { marker.css({ background: '#FFFFFF' }); } else { marker.css({ background: '#00ff00' }); } } updatePosition(); const marker_el = marker.el; const previous = new Vector2(); const draggable = new DraggableAspect({ el: marker_el, drag(position) { const delta = new Vector2(); delta.subVectors(position, previous); const keyframe_coord = position_canvas_to_curve( graph.size, frame, margin, position.x, position.y ); // console.log(keyframe_coord); keyframe.time = keyframe_coord.x; keyframe.value = keyframe_coord.y; vCoordinate.updateText(`${number_pretty_print(keyframe_coord.x)}, ${number_pretty_print(keyframe_coord.y)}`); vCoordinate.position.set( position.x, position.y - 24 ); let keyframe_index = curve.keys.indexOf(keyframe); if (keyframe_index < (curve.keys.length - 1) && keyframe.time > curve.keys[keyframe_index + 1].time) { const next_index = keyframe_index + 1; const next = curve.keys[next_index]; curve.keys[next_index] = keyframe; curve.keys[keyframe_index] = next; keyframe_index = next_index; } else if (keyframe_index > 0 && keyframe.time < curve.keys[keyframe_index - 1].time) { const prev_index = keyframe_index - 1; const prev = curve.keys[prev_index]; curve.keys[prev_index] = keyframe; curve.keys[keyframe_index] = prev; keyframe_index = prev_index; } curve.alignTangents(keyframe_index); curve.smoothTangents(keyframe_index, 1); if (keyframe_index > 0) { curve.alignTangents(keyframe_index - 1); curve.smoothTangents(keyframe_index - 1, 1); } if (keyframe_index < curve.keys.length - 1) { curve.alignTangents(keyframe_index + 1); curve.smoothTangents(keyframe_index + 1, 1); } updatePosition(); update_graph(); previous.copy(position); }, dragStart(position) { previous.copy(position); active_keyframe.set(keyframe); vContainer.addChild(vCoordinate); }, dragEnd() { handle_curve_update(); vContainer.removeChild(vCoordinate); } }); draggable.getPointer().on.tap.add( /** * * @param position * @param {MouseEvent} event */ (position, event) => { // make active active_keyframe.set(keyframe); if (event.ctrlKey) { // remove curve.remove(keyframe); remove_keyframe(keyframe); update_graph(); } }); marker.on.linked.add(draggable.start, draggable); marker.on.unlinked.add(draggable.stop, draggable); marker.bindSignal(frame_updated, updatePosition); marker.bindSignal(active_keyframe.onChanged, updateActiveState); const vTangentEditor = build_tangent_editor({ keyframe: keyframe, size: graph.size, ctx: graph.context2d, frame, margin }); vContainerMarker.addChild(vTangentEditor); vContainerMarker.addChild(marker); keyframe_views.set(keyframe, vContainerMarker); vContainer.addChild(vContainerMarker); } function remove_keyframe(keyframe) { const marker_view = keyframe_views.get(keyframe); vContainer.removeChild(marker_view); } graph.on.linked.add(() => { curve.keys.forEach(add_keyframe); }); graph.on.unlinked.add(() => { curve.keys.forEach(remove_keyframe); }); vContainer.el.addEventListener(MouseEvents.DoubleClick, (e) => { const mouse_position = new Vector2(); readPositionFromMouseEvent(mouse_position, e, vContainer.el); const curve_position = position_canvas_to_curve(graph.size, frame, margin, mouse_position.x, mouse_position.y); const key = Keyframe.from(curve_position.x, curve_position.y); const key_index = curve.add(key); curve.alignTangents(key_index); curve.smoothTangents(key_index, 1); add_keyframe(key); update_graph(); }); graph.on.linked.add(handle_curve_update); vContainer.addChild(graph); return vContainer; }