Needle Engine

Changes between version 3.2.8-alpha and 3.2.9-alpha
Files changed (7) hide show
  1. src/engine-components/Component.ts +2 -2
  2. src/engine/engine_addressables.ts +13 -9
  3. src/engine/engine_element_loading.ts +10 -1
  4. src/engine/engine_license.ts +69 -23
  5. src/engine/engine_serialization_builtin_serializer.ts +1 -1
  6. src/engine-components/ScreenCapture.ts +7 -0
  7. src/engine-components/VideoPlayer.ts +58 -35
src/engine-components/Component.ts CHANGED
@@ -486,8 +486,8 @@
486
486
  }
487
487
  // console.trace("INTERNAL ENABLE");
488
488
  this.__didEnable = true;
489
+ this.__isEnabled = true;
489
490
  this.onEnable();
490
- this.__isEnabled = true;
491
491
  return true;
492
492
  }
493
493
 
@@ -501,8 +501,8 @@
501
501
  return;
502
502
  }
503
503
  this.__didEnable = false;
504
+ this.__isEnabled = false;
504
505
  this.onDisable();
505
- this.__isEnabled = false;
506
506
  }
507
507
 
508
508
  /** @internal */
src/engine/engine_addressables.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getParam, resolveUrl } from "../engine/engine_utils";
2
2
  import { SerializationContext, TypeSerializer } from "./engine_serialization_core";
3
3
  import { Context } from "./engine_setup";
4
- import { Group, Object3D, Texture } from "three";
4
+ import { Group, Object3D, Texture, TextureLoader } from "three";
5
5
  import { processNewScripts } from "./engine_mainloop_utils";
6
6
  import { registerPrefabProvider, syncInstantiate } from "./engine_networking_instantiate";
7
7
  import { download } from "./engine_web_api";
@@ -372,15 +372,19 @@
372
372
  return img;
373
373
  }
374
374
 
375
+ private loader: TextureLoader | null = null;
375
376
  createTexture(): Promise<Texture | null> {
376
- return this.getBitmap().then((bitmap) => {
377
- if (bitmap) {
378
- const texture = new Texture(bitmap);
379
- texture.needsUpdate = true;
380
- return texture;
381
- }
382
- return null;
383
- });
377
+ if (!this.loader) this.loader = new TextureLoader();
378
+ this.loader.setCrossOrigin("anonymous");
379
+ return this.loader.loadAsync(this.url);
380
+ // return this.getBitmap().then((bitmap) => {
381
+ // if (bitmap) {
382
+ // const texture = new Texture(bitmap);
383
+ // texture.needsUpdate = true;
384
+ // return texture;
385
+ // }
386
+ // return null;
387
+ // });
384
388
  }
385
389
 
386
390
  /** Loads the bitmap data of the image */
src/engine/engine_element_loading.ts CHANGED
@@ -253,13 +253,14 @@
253
253
  }
254
254
  loadingBarContainer.appendChild(logo);
255
255
 
256
+
256
257
  this._loadingBar = document.createElement("div");
257
258
  loadingBarContainer.appendChild(this._loadingBar);
258
259
  const getGradientPos = function (t: number): string {
259
260
  return Mathf.lerp(maxWidth * .5, 100 - maxWidth * .5, t) + "%";
260
261
  }
261
262
  this._loadingBar.style.background =
262
- `linear-gradient(90deg, #02022B ${getGradientPos(0)}, #0BA398 ${getGradientPos(.4)}, #99CC33 ${getGradientPos(.5)}, #D7DB0A ${getGradientPos(1)})`;
263
+ `linear-gradient(90deg, #02022B ${getGradientPos(0)}, #0BA398 ${getGradientPos(.4)}, #99CC33 ${getGradientPos(.5)}, #D7DB0A ${getGradientPos(1)})`;
263
264
  this._loadingBar.style.backgroundAttachment = "fixed";
264
265
  this._loadingBar.style.width = "0%";
265
266
  this._loadingBar.style.height = "100%";
@@ -301,6 +302,14 @@
301
302
  }
302
303
  }
303
304
 
305
+ if (!hasLicense) {
306
+ const nonCommercialContainer = document.createElement("div");
307
+ nonCommercialContainer.style.paddingTop = ".6em";
308
+ nonCommercialContainer.style.fontSize = ".8em";
309
+ nonCommercialContainer.innerText = "NON COMMERCIAL";
310
+ this._loadingElement.appendChild(nonCommercialContainer);
311
+ }
312
+
304
313
  return this._loadingElement;
305
314
  }
306
315
  }
src/engine/engine_license.ts CHANGED
@@ -30,22 +30,18 @@
30
30
  }
31
31
 
32
32
 
33
- const _licenseText = "🌵 <span class=\"text\">Made with <a href=\"https://needle.tools\" target=\"_blank\">Needle</a></span>";
34
33
  const licenseElementIdentifier = "needle-license-element";
35
34
  const licenseDuration = 30000;
36
35
  const licenseDelay = 600;
37
36
 
38
37
  function onNonCommercialVersionDetected(ctx: IContext) {
39
- insertNonCommercialUseHint(ctx);
40
- sendNonCommercialUsageMessageToAnalyticsBackend();
38
+ setTimeout(() => insertNonCommercialUseHint(ctx), 2000);
39
+ sendUsageMessageToAnalyticsBackend();
41
40
  }
42
41
 
43
42
  function insertNonCommercialUseHint(ctx: IContext) {
44
43
 
45
- let licenseText = _licenseText;
46
44
  const licenseElement = createLicenseElement();
47
- licenseElement.innerHTML = licenseText;
48
-
49
45
  const style = createLicenseStyle();
50
46
 
51
47
  const interval = setInterval(() => {
@@ -59,10 +55,22 @@
59
55
  logNonCommercialUse();
60
56
 
61
57
  let svg = `<img class="logo" src="${logoSVG}" style="width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;"/>`;
62
- svg = "<a href=\"https://needle.tools\" target=\"_blank\">" + svg + "</a>";
63
- licenseText = svg; //licenseText.replace("🌵", svg);
64
- licenseElement.innerHTML = licenseText;
58
+ const logoElement = document.createElement("div");
59
+ logoElement.innerHTML = svg;
60
+ logoElement.classList.add("logo");
61
+ licenseElement.appendChild(logoElement);
65
62
 
63
+ const textElement = document.createElement("div");
64
+ textElement.classList.add("text");
65
+ textElement.innerHTML = "Needle Engine<br/><span class=\"non-commercial\">Non Commercial</span>";
66
+ licenseElement.appendChild(textElement);
67
+
68
+ licenseElement.title = "Needle Engine — non commercial version";
69
+ licenseElement.addEventListener("click", () => {
70
+ console.log("CLICK")
71
+ globalThis.open("https://needle.tools", "_blank");
72
+ });
73
+
66
74
  const removeDelay = licenseDuration + licenseDelay;
67
75
  setTimeout(() => {
68
76
  clearInterval(interval);
@@ -110,30 +118,66 @@
110
118
  ${selector} {
111
119
  font-family: 'Roboto', sans-serif !important;
112
120
  font-weight: 300;
121
+ transition: all 0.1s ease-in-out !important;
122
+ pointer-events: all;
113
123
  }
114
124
 
125
+ ${selector}:hover {
126
+ cursor: pointer;
127
+ transition: all 0.1s ease-in-out !important;
128
+ }
129
+
115
130
  ${selector}, ${selector} > * {
116
131
  display: inline-block !important;
117
132
  visibility: visible !important;
118
133
  background: none !important;
119
134
  border: none !important;
120
135
  text-decoration: none !important;
136
+ vertical-align: middle !important;
121
137
  }
138
+
139
+ @keyframes license-animation {
140
+ 1% {
141
+ opacity: 0;
142
+ }
143
+ 2.5% {
144
+ opacity: 1;
145
+ }
146
+ 98% {
147
+ opacity: 1;
148
+ }
149
+ 99% {
150
+ opacity: 0;
151
+ }
152
+ }
153
+ ${selector} .text {
154
+ opacity: 0;
155
+ animation: license-animation;
156
+ animation-iteration-count: 1;
157
+ animation-duration: ${(licenseDuration / 1000)}s;
158
+ animation-delay: ${licenseDelay / 1000}s;
159
+ animation-easing: ease-in-out;
160
+ mix-blend-mode: difference;
161
+ color: rgb(40, 40, 40);
162
+ mix-blend-mode: difference;
163
+ line-height: 1em;
164
+ margin-left: -3px;
165
+ }
122
166
 
123
- ${selector} a {
124
- color: black !important;
125
- font-weight: 500 !important;
167
+ ${selector} .text .non-commercial {
168
+ font-size: 0.8em;
169
+ font-weight: 600;
170
+ text-transform: uppercase;
126
171
  }
127
172
 
128
- @keyframes append-animate {
173
+ @keyframes logo-animation {
129
174
  0% {
130
175
  transform: translate(0px, 10px);
131
- opacity: 0;
132
176
  pointer-events: none;
133
177
  }
134
178
  1% {
135
179
  transform: translate(0, -5px);
136
- opacity: .9;
180
+ opacity: 1;
137
181
  }
138
182
  2% {
139
183
  transform: translate(0, 2.5px);
@@ -141,7 +185,6 @@
141
185
  3% {
142
186
  transform: translate(0, 0px);
143
187
  pointer-events: all;
144
- opacity: 1;
145
188
  }
146
189
  4% {
147
190
  transform: scale(1)
@@ -161,14 +204,15 @@
161
204
  transform: scale(1)
162
205
  }
163
206
  100% {
207
+ pointer-events: none;
164
208
  opacity: 0;
165
- pointer-events: none;
166
209
  }
167
210
  }
168
- ${selector} {
211
+
212
+ ${selector} .logo {
169
213
  opacity: 0;
170
214
  pointer-events: none;
171
- animation: append-animate;
215
+ animation: logo-animation;
172
216
  animation-iteration-count: 1;
173
217
  animation-duration: ${(licenseDuration / 1000)}s;
174
218
  animation-delay: ${licenseDelay / 1000}s;
@@ -184,8 +228,9 @@
184
228
  transition: all 0.1s ease-in-out !important;
185
229
  }
186
230
 
187
- ${selector} .logo:hover {
188
- transform: scale(1.3) !important;
231
+ ${selector}:hover .logo {
232
+ transition: all 0.1s ease-in-out !important;
233
+ transform: scale(1.1) !important;
189
234
  cursor: pointer !important;
190
235
  }
191
236
  `
@@ -193,7 +238,7 @@
193
238
  }
194
239
 
195
240
 
196
- async function sendNonCommercialUsageMessageToAnalyticsBackend() {
241
+ async function sendUsageMessageToAnalyticsBackend() {
197
242
  try {
198
243
  const analyticsBackendUrlForward = "https://urls.needle.tools/analytics-endpoint";
199
244
  const res = await fetch(analyticsBackendUrlForward);
@@ -207,7 +252,8 @@
207
252
 
208
253
  let endpoint = "api/v1/register/web-request";
209
254
  if (!analyticsUrl.endsWith("/")) endpoint = "/" + endpoint;
210
- const finalUrl = `${analyticsUrl}${endpoint}?type=non-commercial&url=${encodeURIComponent(currentUrl)}&hostname=${encodeURIComponent(window.location.hostname)}&pathname=${encodeURIComponent(window.location.pathname)}&search=${encodeURIComponent(window.location.search)}&hash=${encodeURIComponent(window.location.hash)}`;
255
+ const type = hasProLicense() ? "commercial" : "non-commercial";
256
+ const finalUrl = `${analyticsUrl}${endpoint}?type=${type}&url=${encodeURIComponent(currentUrl)}&hostname=${encodeURIComponent(window.location.hostname)}&pathname=${encodeURIComponent(window.location.pathname)}&search=${encodeURIComponent(window.location.search)}&hash=${encodeURIComponent(window.location.hash)}`;
211
257
  if (debug) console.log("Sending non-commercial usage message to analytics backend", finalUrl);
212
258
 
213
259
 
src/engine/engine_serialization_builtin_serializer.ts CHANGED
@@ -362,7 +362,7 @@
362
362
  }
363
363
 
364
364
  onDeserialize(data: string, _context: SerializationContext) {
365
- if (typeof data === "string") {
365
+ if (typeof data === "string" && data.length > 0) {
366
366
  return resolveUrl(_context.gltfId, data);
367
367
  }
368
368
  return undefined;
src/engine-components/ScreenCapture.ts CHANGED
@@ -40,6 +40,13 @@
40
40
 
41
41
  export class ScreenCapture extends Behaviour implements IPointerClickHandler {
42
42
 
43
+ onPointerEnter() {
44
+ this.context.input.setCursorPointer();
45
+ }
46
+ onPointerExit() {
47
+ this.context.input.setCursorNormal();
48
+ }
49
+
43
50
  onPointerClick(evt : PointerEventData) {
44
51
  if(evt && evt.pointerId !== 0) return;
45
52
  if(this.context.connection.isInRoom === false) return;
src/engine-components/VideoPlayer.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { Behaviour, GameObject } from "./Component";
2
- import * as THREE from "three";
3
2
  import { serializable } from "../engine/engine_serialization_decorator";
4
- import { LinearFilter, Material, Mesh, Object3D, RawShaderMaterial, ShaderMaterial, Texture, TextureLoader, Vector2, Vector4, VideoTexture } from "three";
3
+ import { Material, Mesh, Object3D, ShaderMaterial, sRGBEncoding, Texture, Vector2, Vector4, VideoTexture } from "three";
5
4
  import { awaitInput } from "../engine/engine_input_utils";
6
- import { getParam, resolveUrl } from "../engine/engine_utils";
5
+ import { getParam } from "../engine/engine_utils";
7
6
  import { Renderer } from "./Renderer";
8
7
  import { getWorldScale } from "../engine/engine_three_utils";
9
8
  import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects";
10
9
  import { Context } from "../engine/engine_setup";
10
+ import { isDevEnvironment } from "../engine/debug";
11
11
 
12
12
  const debug = getParam("debugvideo");
13
13
 
@@ -46,7 +46,7 @@
46
46
  export class VideoPlayer extends Behaviour {
47
47
 
48
48
  @serializable(Object3D)
49
- renderer: THREE.Object3D | null = null;
49
+ renderer: Object3D | null = null;
50
50
  @serializable()
51
51
  playOnAwake: boolean = true;
52
52
 
@@ -149,18 +149,29 @@
149
149
  }
150
150
  private _muted: boolean = false;
151
151
 
152
+ @serializable()
153
+ private set audioOutputMode(mode: VideoAudioOutputMode) {
154
+ if (mode !== this._audioOutputMode) {
155
+ if (mode === VideoAudioOutputMode.AudioSource && isDevEnvironment()) console.warn("VideoAudioOutputMode.AudioSource is not yet implemented");
156
+ this._audioOutputMode = mode;
157
+ this.updateVideoElementSettings();
158
+ }
159
+ }
160
+ private get audioOutputMode() { return this._audioOutputMode; }
161
+
162
+ private _audioOutputMode: VideoAudioOutputMode = VideoAudioOutputMode.Direct;
163
+
152
164
  /** Set this to false to pause video playback while the tab is not active */
153
165
  playInBackground: boolean = true;
154
166
 
155
167
  private _crossOrigin: string | null = "anonymous";
156
168
 
157
- private audioOutputMode: VideoAudioOutputMode = VideoAudioOutputMode.AudioSource;
158
169
 
159
170
  private source!: VideoSource;
160
171
  private url?: string | null = null;
161
172
 
162
173
  private _videoElement: HTMLVideoElement | null = null;
163
- private _videoTexture: THREE.VideoTexture | null = null;
174
+ private _videoTexture: VideoTexture | null = null;
164
175
  private _videoMaterial: Material | null = null;
165
176
 
166
177
  private _isPlaying: boolean = false;
@@ -195,27 +206,11 @@
195
206
  }
196
207
  }
197
208
 
198
- awake(): void {
199
- this.create(this.playOnAwake);
209
+ onEnable(): void {
210
+ window.addEventListener('visibilitychange', this.visibilityChanged);
200
211
 
201
- window.addEventListener('visibilitychange', _evt => {
202
- switch (document.visibilityState) {
203
- case "hidden":
204
- if(!this.playInBackground){
205
- this.wasPlaying = this._isPlaying;
206
- this.pause();
207
- }
208
- break;
209
- case "visible":
210
- if (this.wasPlaying && !this._isPlaying) this.play();
211
- break;
212
- }
213
- });
214
- }
215
-
216
- onEnable(): void {
217
212
  if (this.playOnAwake === true) {
218
- this.handleBeginPlaying(true);
213
+ this.create(true);
219
214
  }
220
215
  if (this.screenspace) {
221
216
  this._overlay?.start();
@@ -224,9 +219,24 @@
224
219
  }
225
220
 
226
221
  onDisable(): void {
222
+ window.removeEventListener('visibilitychange', this.visibilityChanged);
227
223
  this.pause();
228
224
  }
229
225
 
226
+ private visibilityChanged = (_: Event) => {
227
+ switch (document.visibilityState) {
228
+ case "hidden":
229
+ if (!this.playInBackground) {
230
+ this.wasPlaying = this._isPlaying;
231
+ this.pause();
232
+ }
233
+ break;
234
+ case "visible":
235
+ if (this.wasPlaying && !this._isPlaying) this.play();
236
+ break;
237
+ }
238
+ }
239
+
230
240
  onDestroy(): void {
231
241
  if (this._videoElement) {
232
242
  this._videoElement.parentElement?.removeChild(this._videoElement);
@@ -258,12 +268,15 @@
258
268
  }
259
269
 
260
270
  play() {
271
+ if (!this._videoElement) this.create(false);
261
272
  if (!this._videoElement) return;
262
273
  if (this._isPlaying && !this._videoElement?.ended && !this._videoElement?.paused) return;
263
274
  this._isPlaying = true;
264
275
  if (!this._receivedInput) this._videoElement.muted = true;
265
276
  this.updateVideoElementSettings();
266
- this._videoElement?.play().catch(err => {
277
+ this._videoElement.currentTime = this.time;
278
+ this._videoElement.play().catch(err => {
279
+ console.log(err);
267
280
  // https://developer.chrome.com/blog/play-request-was-interrupted/
268
281
  if (debug)
269
282
  console.error("Error playing video", err, "CODE=" + err.code, this.videoElement?.src, this);
@@ -272,19 +285,23 @@
272
285
  this.play();
273
286
  }, 1000);
274
287
  });
275
- if (debug) console.log("play", this._videoElement);
288
+ if (debug) console.log("play", this._videoElement, this.time);
276
289
  }
277
290
 
278
291
  stop() {
279
292
  this._isPlaying = false;
293
+ this.time = 0;
280
294
  if (!this._videoElement) return;
281
295
  this._videoElement.currentTime = 0;
282
296
  this._videoElement.pause();
297
+ if (debug) console.log("STOP", this);
283
298
  }
284
299
 
285
300
  pause(): void {
301
+ this.time = this._videoElement?.currentTime ?? 0;
286
302
  this._isPlaying = false;
287
303
  this._videoElement?.pause();
304
+ if (debug) console.log("PAUSE", this, this.currentTime);
288
305
  }
289
306
 
290
307
 
@@ -301,30 +318,36 @@
301
318
 
302
319
  if (!src) return;
303
320
 
304
- // console.log(src, this);
305
321
 
306
322
  if (!this._videoElement) {
323
+ if (debug)
324
+ console.warn("Create VideoElement", this);
307
325
  this._videoElement = this.createVideoElement();
308
326
  this.context.domElement?.prepend(this._videoElement);
309
327
  // hide it because otherwise it would overlay the website with default css
310
328
  this.updateVideoElementStyles();
311
329
  }
330
+
312
331
  if (typeof src === "string") {
332
+ if (debug) console.log("Set Video src", src);
313
333
  this._videoElement.src = src;
314
- const str = this._videoElement["captureStream"]?.call(this._videoElement);
315
- this.clip = str;
334
+ // Nor sure why we did this here, but with this code the video does not restart when being paused / enable toggled
335
+ // const str = this._videoElement["captureStream"]?.call(this._videoElement);
336
+ // this.clip = str;
316
337
  }
317
- else
338
+ else {
339
+ if (debug) console.log("Set Video srcObject", src);
318
340
  this._videoElement.srcObject = src;
341
+ }
319
342
 
320
343
 
321
344
  if (!this._videoTexture)
322
- this._videoTexture = new THREE.VideoTexture(this._videoElement);
345
+ this._videoTexture = new VideoTexture(this._videoElement);
323
346
  this._videoTexture.flipY = false;
324
- this._videoTexture.encoding = THREE.sRGBEncoding;
347
+ this._videoTexture.encoding = sRGBEncoding;
325
348
  this.handleBeginPlaying(playAutomatically);
326
349
  if (debug)
327
- console.log(this);
350
+ console.log(this, playAutomatically);
328
351
  }
329
352
 
330
353
  updateAspect() {
@@ -354,7 +377,7 @@
354
377
  const video = document.createElement("video") as HTMLVideoElement;
355
378
  if (this._crossOrigin)
356
379
  video.setAttribute("crossorigin", this._crossOrigin);
357
- if (debug) console.log("create video elment", video);
380
+ if (debug) console.log("created video element", video);
358
381
  return video;
359
382
  }