@needle-tools/engine 4.9.0-alpha.3 → 4.9.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components.needle.json +1 -1
- package/dist/{needle-engine.bundle-rZjkMCRy.js → needle-engine.bundle-BYCLCf3u.js} +5290 -5097
- package/dist/{needle-engine.bundle-xx2z8elo.min.js → needle-engine.bundle-D5_A77oU.min.js} +131 -131
- package/dist/{needle-engine.bundle-D3t6rco7.umd.cjs → needle-engine.bundle-YbxLeXEH.umd.cjs} +132 -132
- package/dist/needle-engine.js +556 -552
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/codegen/register_types.js +8 -0
- package/lib/engine/codegen/register_types.js.map +1 -1
- package/lib/engine/engine_physics_rapier.js +8 -8
- package/lib/engine/engine_physics_rapier.js.map +1 -1
- package/lib/engine/engine_serialization_builtin_serializer.d.ts +2 -2
- package/lib/engine-components/Animation.d.ts +1 -0
- package/lib/engine-components/Animation.js +9 -0
- package/lib/engine-components/Animation.js.map +1 -1
- package/lib/engine-components/Component.d.ts +1 -0
- package/lib/engine-components/Component.js.map +1 -1
- package/lib/engine-components/EventTrigger.js.map +1 -1
- package/lib/engine-components/api.d.ts +1 -0
- package/lib/engine-components/api.js +1 -0
- package/lib/engine-components/api.js.map +1 -1
- package/lib/engine-components/codegen/components.d.ts +4 -0
- package/lib/engine-components/codegen/components.js +4 -0
- package/lib/engine-components/codegen/components.js.map +1 -1
- package/lib/engine-components/physics/Attractor.d.ts +8 -0
- package/lib/engine-components/physics/Attractor.js +42 -0
- package/lib/engine-components/physics/Attractor.js.map +1 -0
- package/lib/engine-components/web/Clickthrough.d.ts +20 -0
- package/lib/engine-components/web/Clickthrough.js +59 -0
- package/lib/engine-components/web/Clickthrough.js.map +1 -0
- package/lib/engine-components/web/CursorFollow.d.ts +12 -0
- package/lib/engine-components/web/CursorFollow.js +46 -0
- package/lib/engine-components/web/CursorFollow.js.map +1 -0
- package/lib/engine-components/web/ScrollFollow.d.ts +52 -0
- package/lib/engine-components/web/ScrollFollow.js +157 -0
- package/lib/engine-components/web/ScrollFollow.js.map +1 -0
- package/lib/engine-components/web/index.d.ts +3 -0
- package/lib/engine-components/web/index.js +4 -0
- package/lib/engine-components/web/index.js.map +1 -0
- package/package.json +2 -2
- package/src/engine/codegen/register_types.ts +8 -0
- package/src/engine/engine_physics_rapier.ts +11 -11
- package/src/engine-components/Animation.ts +9 -0
- package/src/engine-components/Component.ts +1 -0
- package/src/engine-components/EventTrigger.ts +0 -1
- package/src/engine-components/api.ts +1 -0
- package/src/engine-components/codegen/components.ts +4 -0
- package/src/engine-components/physics/Attractor.ts +35 -0
- package/src/engine-components/web/Clickthrough.ts +64 -0
- package/src/engine-components/web/CursorFollow.ts +49 -0
- package/src/engine-components/web/ScrollFollow.ts +173 -0
- package/src/engine-components/web/index.ts +3 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Behaviour } from "../Component.js";
|
|
2
|
+
import { Rigidbody } from "../RigidBody.js";
|
|
3
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export class Attractor extends Behaviour {
|
|
8
|
+
|
|
9
|
+
@serializable()
|
|
10
|
+
strength: number = 1;
|
|
11
|
+
|
|
12
|
+
@serializable()
|
|
13
|
+
radius: number = 2;
|
|
14
|
+
|
|
15
|
+
@serializable(Rigidbody)
|
|
16
|
+
targets: Rigidbody[] = [];
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
update() {
|
|
20
|
+
const wp = this.gameObject.worldPosition;
|
|
21
|
+
const factor = -this.strength * this.context.time.deltaTime;
|
|
22
|
+
this.targets.forEach(t => {
|
|
23
|
+
if(!t) return;
|
|
24
|
+
const dir = t.gameObject.worldPosition.sub(wp);
|
|
25
|
+
const length = dir.length();
|
|
26
|
+
if (length > this.radius) return;
|
|
27
|
+
let _factor = factor;
|
|
28
|
+
if (length > 1) _factor /= length * length;
|
|
29
|
+
else _factor /= Math.max(.05, length);
|
|
30
|
+
t.applyImpulse(dir.multiplyScalar(_factor));
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { NEPointerEvent } from "../../engine/engine_input.js";
|
|
2
|
+
import { Behaviour } from "../Component.js";
|
|
3
|
+
import { onStart } from "../../engine/engine_lifecycle_api.js";
|
|
4
|
+
|
|
5
|
+
// Automatically add ClickThrough component if "clickthrough" attribute is present on the needle-engine element
|
|
6
|
+
onStart(ctx => {
|
|
7
|
+
const attribute = ctx.domElement.getAttribute("clickthrough");
|
|
8
|
+
if (attribute !== null && attribute !== "0" && attribute !== "false") {
|
|
9
|
+
ctx.scene.addComponent(ClickThrough);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ClickThrough component allows pointer events to "click through" the 3D canvas to HTML elements behind it.
|
|
16
|
+
*
|
|
17
|
+
* This is useful if you have a transparent canvas overlaying HTML content and want to interact with the HTML content through the transparent areas of the canvas.
|
|
18
|
+
*
|
|
19
|
+
* Usage Options:
|
|
20
|
+
* - Add the ClickThrough component to any GameObject in your scene.
|
|
21
|
+
* - Alternatively, add the `clickthrough` attribute to the `<needle-engine>` HTML element (e.g. `<needle-engine clickthrough></needle-engine>`).
|
|
22
|
+
*
|
|
23
|
+
* @link Example https://stackblitz.com/~/github.com/needle-engine/sample-3d-over-html
|
|
24
|
+
*/
|
|
25
|
+
export class ClickThrough extends Behaviour {
|
|
26
|
+
|
|
27
|
+
private _previousPointerEvents: string = 'all';
|
|
28
|
+
|
|
29
|
+
onEnable() {
|
|
30
|
+
// Register for pointer down and pointer move event
|
|
31
|
+
this.context.input.addEventListener('pointerdown', this.onPointerEvent);
|
|
32
|
+
this.context.input.addEventListener('pointermove', this.onPointerEvent, {
|
|
33
|
+
queue: 100,
|
|
34
|
+
});
|
|
35
|
+
window.addEventListener("touchend", this.onTouchEnd, { passive: true });
|
|
36
|
+
this._previousPointerEvents = this.context.domElement.style.pointerEvents;
|
|
37
|
+
}
|
|
38
|
+
onDisable() {
|
|
39
|
+
this.context.input.removeEventListener('pointerdown', this.onPointerEvent);
|
|
40
|
+
this.context.input.removeEventListener('pointermove', this.onPointerEvent);
|
|
41
|
+
window.removeEventListener("touchend", this.onTouchEnd);
|
|
42
|
+
this.context.domElement.style.pointerEvents = this._previousPointerEvents;
|
|
43
|
+
}
|
|
44
|
+
onPointerEnter() {
|
|
45
|
+
/** do nothing, necessary to raycast children */
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private onPointerEvent = (evt: NEPointerEvent) => {
|
|
49
|
+
if (evt.pointerId > 0) return;
|
|
50
|
+
const intersections = evt.intersections;
|
|
51
|
+
// If we don't had any intersections during the 3D raycasting then we disable pointer events for the needle-engine element so that content BEHIND the 3D element can receive pointer events
|
|
52
|
+
if (intersections?.length <= 0) {
|
|
53
|
+
this.context.domElement.style.pointerEvents = 'none';
|
|
54
|
+
} else {
|
|
55
|
+
this.context.domElement.style.pointerEvents = 'all';
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
private onTouchEnd = (_evt: TouchEvent) => {
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
this.context.domElement.style.pointerEvents = 'all';
|
|
62
|
+
}, 100);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
|
2
|
+
import { getTempVector } from "../../engine/engine_three_utils.js";
|
|
3
|
+
import { Behaviour } from "../Component.js";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export class CursorFollow extends Behaviour {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Damping for the movement, set to 0 for instant movement
|
|
10
|
+
* @default 0
|
|
11
|
+
*/
|
|
12
|
+
@serializable()
|
|
13
|
+
damping: number = 0;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
private _distance: number = -1;
|
|
17
|
+
updateDistance() {
|
|
18
|
+
this._distance = this.gameObject.worldPosition.distanceTo(this.context.mainCamera.worldPosition);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/** @internal */
|
|
23
|
+
update() {
|
|
24
|
+
// continuously update distance in case camera or object moves
|
|
25
|
+
this.updateDistance();
|
|
26
|
+
|
|
27
|
+
// follow cursor in screenspace but maintain initial distance from camera
|
|
28
|
+
const cursor = this.context.input.mousePositionRC;
|
|
29
|
+
const camera = this.context.mainCamera;
|
|
30
|
+
const cameraPosition = camera.worldPosition;
|
|
31
|
+
|
|
32
|
+
// create ray from camera through cursor position
|
|
33
|
+
const rayDirection = getTempVector(cursor.x, cursor.y, 1).unproject(camera);
|
|
34
|
+
rayDirection.sub(cameraPosition).normalize();
|
|
35
|
+
|
|
36
|
+
// position object at initial distance along the ray
|
|
37
|
+
const newPosition = rayDirection.multiplyScalar(this._distance).add(cameraPosition);
|
|
38
|
+
if (this.damping > 0) {
|
|
39
|
+
const pos = this.gameObject.worldPosition;
|
|
40
|
+
pos.lerp(newPosition, this.context.time.deltaTime / this.damping);
|
|
41
|
+
this.gameObject.worldPosition = pos;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.gameObject.worldPosition = newPosition;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Object3D } from "three";
|
|
2
|
+
import { serializable } from "../../engine/engine_serialization.js";
|
|
3
|
+
import { Mathf } from "../../engine/engine_math.js";
|
|
4
|
+
import { Behaviour } from "../Component.js";
|
|
5
|
+
import { PlayableDirector } from "../timeline/PlayableDirector.js";
|
|
6
|
+
import { EventList } from "../EventList.js";
|
|
7
|
+
import { Animation } from "../Animation.js";
|
|
8
|
+
import { AudioSource } from "../AudioSource.js";
|
|
9
|
+
|
|
10
|
+
type ScrollFollowEvent = {
|
|
11
|
+
/** Event type */
|
|
12
|
+
type: "change",
|
|
13
|
+
/** Current scroll value */
|
|
14
|
+
value: number,
|
|
15
|
+
/** ScrollFollow component that raised the event */
|
|
16
|
+
component: ScrollFollow,
|
|
17
|
+
/** Call to prevent invocation of default (e.g. updating targets) */
|
|
18
|
+
preventDefault: () => void,
|
|
19
|
+
defaultPrevented: boolean,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ScrollFollow extends Behaviour {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Target object(s) to follow the scroll position of the page. If null, the main camera is used.
|
|
26
|
+
*/
|
|
27
|
+
@serializable([Behaviour, Object3D])
|
|
28
|
+
target: object[] | object | null = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Damping for the movement, set to 0 for instant movement
|
|
32
|
+
* @default 0
|
|
33
|
+
*/
|
|
34
|
+
@serializable()
|
|
35
|
+
damping: number = 0;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* If set, the scroll position will be read from the specified element instead of the window.
|
|
39
|
+
* Use a CSS selector to specify the element, e.g. `#my-scrollable-div` or `.scroll-container`.
|
|
40
|
+
*/
|
|
41
|
+
@serializable()
|
|
42
|
+
htmlSelector: string | null = null;
|
|
43
|
+
|
|
44
|
+
@serializable()
|
|
45
|
+
private mode: "window" = "window";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Event fired when the scroll position changes
|
|
49
|
+
*/
|
|
50
|
+
@serializable(EventList)
|
|
51
|
+
changed: EventList<ScrollFollowEvent> = new EventList<ScrollFollowEvent>();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Current scroll value in "pages" (0 = top of page, 1 = bottom of page)
|
|
55
|
+
*/
|
|
56
|
+
get currentValue() {
|
|
57
|
+
return this.current_value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private current_value: number = 0;
|
|
61
|
+
private target_value: number = 0;
|
|
62
|
+
private applied_value: number = -1;
|
|
63
|
+
|
|
64
|
+
/** @internal */
|
|
65
|
+
onEnable() {
|
|
66
|
+
window.addEventListener("wheel", this.updateCurrentScrollValue, { passive: true });
|
|
67
|
+
this.applied_value = -1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @internal */
|
|
71
|
+
onDisable() {
|
|
72
|
+
window.removeEventListener("wheel", this.updateCurrentScrollValue);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @internal */
|
|
76
|
+
lateUpdate() {
|
|
77
|
+
|
|
78
|
+
this.updateCurrentScrollValue();
|
|
79
|
+
|
|
80
|
+
// apply damping if any
|
|
81
|
+
if (this.damping > 0) {
|
|
82
|
+
this.current_value = Mathf.lerp(this.current_value, this.target_value, this.context.time.deltaTime / this.damping);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.current_value = this.target_value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.current_value !== this.applied_value) {
|
|
89
|
+
this.applied_value = this.current_value;
|
|
90
|
+
|
|
91
|
+
let defaultPrevented = false;
|
|
92
|
+
if (this.changed.listenerCount > 0) {
|
|
93
|
+
// fire change event
|
|
94
|
+
const event: ScrollFollowEvent = {
|
|
95
|
+
type: "change",
|
|
96
|
+
value: this.current_value,
|
|
97
|
+
component: this,
|
|
98
|
+
preventDefault: () => { event.defaultPrevented = true; },
|
|
99
|
+
defaultPrevented: false,
|
|
100
|
+
};
|
|
101
|
+
this.changed.invoke(event);
|
|
102
|
+
defaultPrevented = event.defaultPrevented;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// if not prevented apply scroll
|
|
106
|
+
if (!defaultPrevented) {
|
|
107
|
+
|
|
108
|
+
// apply scroll to target(s)
|
|
109
|
+
if (Array.isArray(this.target)) {
|
|
110
|
+
this.target.forEach(t => t && this.applyScroll(t));
|
|
111
|
+
}
|
|
112
|
+
else if (this.target) {
|
|
113
|
+
this.applyScroll(this.target);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private _lastSelectorValue: string | null = null;
|
|
120
|
+
private _lastSelectorElement: Element | null = null;
|
|
121
|
+
|
|
122
|
+
private updateCurrentScrollValue = () => {
|
|
123
|
+
|
|
124
|
+
switch (this.mode) {
|
|
125
|
+
case "window":
|
|
126
|
+
if (this.htmlSelector?.length) {
|
|
127
|
+
if (this.htmlSelector !== this._lastSelectorValue) {
|
|
128
|
+
this._lastSelectorElement = document.querySelector(this.htmlSelector);
|
|
129
|
+
this._lastSelectorValue = this.htmlSelector;
|
|
130
|
+
}
|
|
131
|
+
if (this._lastSelectorElement) {
|
|
132
|
+
const rect = this._lastSelectorElement.getBoundingClientRect();
|
|
133
|
+
this.target_value = -rect.top / (rect.height - window.innerHeight);
|
|
134
|
+
if (isNaN(this.target_value) || !isFinite(this.target_value)) this.target_value = 0;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.target_value = window.scrollY / (document.body.scrollHeight - window.innerHeight);
|
|
140
|
+
}
|
|
141
|
+
if (isNaN(this.target_value) || !isFinite(this.target_value)) this.target_value = 0;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
private applyScroll(target: object) {
|
|
149
|
+
|
|
150
|
+
if (!target) return;
|
|
151
|
+
|
|
152
|
+
if (target instanceof PlayableDirector) {
|
|
153
|
+
target.time = this.current_value * target.duration;
|
|
154
|
+
if (!target.isPlaying) target.evaluate();
|
|
155
|
+
}
|
|
156
|
+
else if (target instanceof Animation) {
|
|
157
|
+
target.time = this.current_value * target.duration;
|
|
158
|
+
}
|
|
159
|
+
else if (target instanceof AudioSource) {
|
|
160
|
+
if (!target.duration) return;
|
|
161
|
+
target.time = this.current_value * target.duration;
|
|
162
|
+
}
|
|
163
|
+
else if ("scroll" in target) {
|
|
164
|
+
if (typeof target.scroll === "number") {
|
|
165
|
+
target.scroll = this.current_value;
|
|
166
|
+
}
|
|
167
|
+
else if (typeof target.scroll === "function") {
|
|
168
|
+
target.scroll(this.current_value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
}
|