@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
363 lines (292 loc) • 11.6 kB
JavaScript
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;
}