@needle-tools/engine 4.11.3 → 4.11.4-next.a568de7
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/CHANGELOG.md +4 -0
- package/dist/generateMeshBVH.worker-B9bjdr6J.js +25 -0
- package/dist/{needle-engine.bundle-B_aGaFmh.js → needle-engine.bundle-DpWrB4yf.js} +5306 -5268
- package/dist/{needle-engine.bundle-TscA1IdQ.min.js → needle-engine.bundle-lEVjhicZ.min.js} +133 -133
- package/dist/{needle-engine.bundle-YQ3I1QD8.umd.cjs → needle-engine.bundle-nX51yB5a.umd.cjs} +132 -132
- package/dist/needle-engine.js +2 -2
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/{vendor-H-9KkM5B.umd.cjs → vendor-6kAXU6fm.umd.cjs} +39 -39
- package/dist/{vendor-BahM12Xj.min.js → vendor-CsyK1CFs.min.js} +46 -46
- package/dist/{vendor-Becub4o1.js → vendor-petGQl0N.js} +3130 -3069
- package/lib/engine/api.d.ts +1 -0
- package/lib/engine/api.js +1 -0
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_addressables.d.ts +74 -11
- package/lib/engine/engine_addressables.js +74 -11
- package/lib/engine/engine_addressables.js.map +1 -1
- package/lib/engine/engine_camera.fit.d.ts +48 -3
- package/lib/engine/engine_camera.fit.js +29 -0
- package/lib/engine/engine_camera.fit.js.map +1 -1
- package/lib/engine/engine_context.d.ts +18 -3
- package/lib/engine/engine_context.js +18 -3
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/engine_utils.d.ts +0 -23
- package/lib/engine/engine_utils.js +0 -202
- package/lib/engine/engine_utils.js.map +1 -1
- package/lib/engine/engine_utils_attributes.d.ts +48 -0
- package/lib/engine/engine_utils_attributes.js +70 -0
- package/lib/engine/engine_utils_attributes.js.map +1 -0
- package/lib/engine/engine_utils_qrcode.d.ts +23 -0
- package/lib/engine/engine_utils_qrcode.js +234 -0
- package/lib/engine/engine_utils_qrcode.js.map +1 -0
- package/lib/engine/extensions/NEEDLE_components.d.ts +4 -4
- package/lib/engine/extensions/NEEDLE_components.js +36 -17
- package/lib/engine/extensions/NEEDLE_components.js.map +1 -1
- package/lib/engine/webcomponents/buttons.js +1 -1
- package/lib/engine/webcomponents/buttons.js.map +1 -1
- package/lib/engine-components/Animation.d.ts +1 -1
- package/lib/engine-components/Animation.js +1 -1
- package/lib/engine-components/AnimationCurve.d.ts +3 -0
- package/lib/engine-components/AnimationCurve.js +3 -0
- package/lib/engine-components/AnimationCurve.js.map +1 -1
- package/lib/engine-components/Animator.d.ts +2 -1
- package/lib/engine-components/Animator.js +2 -1
- package/lib/engine-components/Animator.js.map +1 -1
- package/lib/engine-components/AnimatorController.d.ts +3 -0
- package/lib/engine-components/AnimatorController.js +3 -0
- package/lib/engine-components/AnimatorController.js.map +1 -1
- package/lib/engine-components/LookAtConstraint.d.ts +4 -0
- package/lib/engine-components/LookAtConstraint.js +4 -0
- package/lib/engine-components/LookAtConstraint.js.map +1 -1
- package/lib/engine-components/NeedleMenu.d.ts +1 -0
- package/lib/engine-components/NeedleMenu.js +1 -0
- package/lib/engine-components/NeedleMenu.js.map +1 -1
- package/lib/engine-components/NestedGltf.d.ts +3 -0
- package/lib/engine-components/NestedGltf.js +3 -0
- package/lib/engine-components/NestedGltf.js.map +1 -1
- package/lib/engine-components/ReflectionProbe.d.ts +4 -0
- package/lib/engine-components/ReflectionProbe.js +4 -0
- package/lib/engine-components/ReflectionProbe.js.map +1 -1
- package/lib/engine-components/Renderer.js +14 -40
- package/lib/engine-components/Renderer.js.map +1 -1
- package/lib/engine-components/RendererLightmap.d.ts +7 -0
- package/lib/engine-components/RendererLightmap.js +30 -21
- package/lib/engine-components/RendererLightmap.js.map +1 -1
- package/lib/engine-components/SeeThrough.d.ts +3 -0
- package/lib/engine-components/SeeThrough.js +3 -0
- package/lib/engine-components/SeeThrough.js.map +1 -1
- package/lib/engine-components/Skybox.d.ts +3 -0
- package/lib/engine-components/Skybox.js +3 -0
- package/lib/engine-components/Skybox.js.map +1 -1
- package/lib/engine-components/SpriteRenderer.d.ts +14 -1
- package/lib/engine-components/SpriteRenderer.js +17 -1
- package/lib/engine-components/SpriteRenderer.js.map +1 -1
- package/lib/engine-components/splines/Spline.d.ts +3 -0
- package/lib/engine-components/splines/Spline.js +3 -0
- package/lib/engine-components/splines/Spline.js.map +1 -1
- package/lib/engine-components/splines/SplineUtils.d.ts +3 -0
- package/lib/engine-components/splines/SplineUtils.js +3 -0
- package/lib/engine-components/splines/SplineUtils.js.map +1 -1
- package/lib/engine-components/splines/SplineWalker.d.ts +3 -0
- package/lib/engine-components/splines/SplineWalker.js +3 -0
- package/lib/engine-components/splines/SplineWalker.js.map +1 -1
- package/lib/engine-components/timeline/PlayableDirector.d.ts +2 -1
- package/lib/engine-components/timeline/PlayableDirector.js +16 -9
- package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
- package/lib/engine-components/timeline/SignalAsset.d.ts +11 -1
- package/lib/engine-components/timeline/SignalAsset.js +11 -1
- package/lib/engine-components/timeline/SignalAsset.js.map +1 -1
- package/lib/engine-components/ui/Raycaster.d.ts +18 -0
- package/lib/engine-components/ui/Raycaster.js +18 -0
- package/lib/engine-components/ui/Raycaster.js.map +1 -1
- package/package.json +2 -2
- package/src/engine/api.ts +1 -0
- package/src/engine/engine_addressables.ts +76 -11
- package/src/engine/engine_camera.fit.ts +50 -4
- package/src/engine/engine_context.ts +18 -3
- package/src/engine/engine_utils.ts +0 -229
- package/src/engine/engine_utils_attributes.ts +72 -0
- package/src/engine/engine_utils_qrcode.ts +266 -0
- package/src/engine/extensions/NEEDLE_components.ts +47 -26
- package/src/engine/webcomponents/buttons.ts +1 -1
- package/src/engine-components/Animation.ts +1 -1
- package/src/engine-components/AnimationCurve.ts +4 -1
- package/src/engine-components/Animator.ts +3 -2
- package/src/engine-components/AnimatorController.ts +3 -0
- package/src/engine-components/LookAtConstraint.ts +6 -1
- package/src/engine-components/NeedleMenu.ts +1 -0
- package/src/engine-components/NestedGltf.ts +3 -0
- package/src/engine-components/ReflectionProbe.ts +4 -0
- package/src/engine-components/Renderer.ts +14 -42
- package/src/engine-components/RendererLightmap.ts +34 -20
- package/src/engine-components/SeeThrough.ts +3 -0
- package/src/engine-components/Skybox.ts +3 -0
- package/src/engine-components/SpriteRenderer.ts +18 -2
- package/src/engine-components/splines/Spline.ts +3 -0
- package/src/engine-components/splines/SplineUtils.ts +3 -1
- package/src/engine-components/splines/SplineWalker.ts +3 -0
- package/src/engine-components/timeline/PlayableDirector.ts +16 -10
- package/src/engine-components/timeline/SignalAsset.ts +13 -2
- package/src/engine-components/ui/Raycaster.ts +19 -0
- package/dist/generateMeshBVH.worker-2qGLkQjg.js +0 -25
|
@@ -129,9 +129,12 @@ export function registerComponent(script: IComponent, context?: Context) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* The context is the main
|
|
133
|
-
* It can be used to access the scene, renderer,
|
|
134
|
-
*
|
|
132
|
+
* The Needle Engine context is the main access point that holds all the data and state of a Needle Engine application.
|
|
133
|
+
* It can be used to access the {@link Context.scene}, {@link Context.renderer}, {@link Context.mainCamera}, {@link Context.input}, {@link Context.physics}, {@link Context.time}, {@link Context.connection} (networking), and more.
|
|
134
|
+
*
|
|
135
|
+
* The context is automatically created when using the `<needle-engine>` web component.
|
|
136
|
+
*
|
|
137
|
+
* @example Accessing the context from a [component](https://engine.needle.tools/docs/api/Behaviour):
|
|
135
138
|
* ```typescript
|
|
136
139
|
* import { Behaviour } from "@needle-tools/engine";
|
|
137
140
|
* import { Mesh, BoxGeometry, MeshBasicMaterial } from "three";
|
|
@@ -142,6 +145,18 @@ export function registerComponent(script: IComponent, context?: Context) {
|
|
|
142
145
|
* }
|
|
143
146
|
* }
|
|
144
147
|
* ```
|
|
148
|
+
*
|
|
149
|
+
* @example Accessing the context from a [hook](https://engine.needle.tools/docs/scripting.html#hooks) without a component e.g. from a javascript module or svelte or react component.
|
|
150
|
+
*
|
|
151
|
+
* ```typescript
|
|
152
|
+
* import { onStart } from "@needle-tools/engine";
|
|
153
|
+
*
|
|
154
|
+
* onStart((context) => {
|
|
155
|
+
* console.log("Hello from onStart hook");
|
|
156
|
+
* context.scene.add(new Mesh(new BoxGeometry(), new MeshBasicMaterial()));
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*
|
|
145
160
|
*/
|
|
146
161
|
export class Context implements IContext {
|
|
147
162
|
|
|
@@ -3,11 +3,9 @@ import { Quaternion, Vector2, Vector3, Vector4 } from "three";
|
|
|
3
3
|
|
|
4
4
|
declare type Vector = Vector2 | Vector3 | Vector4 | Quaternion;
|
|
5
5
|
|
|
6
|
-
import { needleLogoOnlySVG } from "./assets/index.js";
|
|
7
6
|
import type { Context } from "./engine_context.js";
|
|
8
7
|
import { ContextRegistry } from "./engine_context_registry.js";
|
|
9
8
|
import { type SourceIdentifier } from "./engine_types.js";
|
|
10
|
-
import type { NeedleEngineWebComponent } from "./webcomponents/needle-engine.js";
|
|
11
9
|
|
|
12
10
|
// https://schneidenbach.gitbooks.io/typescript-cookbook/content/nameof-operator.html
|
|
13
11
|
/** @internal */
|
|
@@ -899,230 +897,3 @@ export async function PromiseAllWithErrors<T>(promise: Promise<T>[]): Promise<{
|
|
|
899
897
|
|
|
900
898
|
|
|
901
899
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
/** Generates a QR code HTML image using https://github.com/davidshimjs/qrcodejs
|
|
905
|
-
* @param args.text The text to encode
|
|
906
|
-
* @param args.width The width of the QR code
|
|
907
|
-
* @param args.height The height of the QR code
|
|
908
|
-
* @param args.colorDark The color of the dark squares
|
|
909
|
-
* @param args.colorLight The color of the light squares
|
|
910
|
-
* @param args.correctLevel The error correction level to use
|
|
911
|
-
* @param args.showLogo If true, the same logo as for the Needle loading screen will be drawn in the center of the QR code
|
|
912
|
-
* @param args.showUrl If true, the URL will be shown below the QR code
|
|
913
|
-
* @param args.domElement The dom element to append the QR code to. If not provided a new div will be created and returned
|
|
914
|
-
* @returns The dom element containing the QR code
|
|
915
|
-
*/
|
|
916
|
-
export async function generateQRCode(args: { domElement?: HTMLElement, text: string, width?: number, height?: number, colorDark?: string, colorLight?: string, correctLevel?: any, showLogo?: boolean, showUrl?: boolean }): Promise<HTMLElement> {
|
|
917
|
-
|
|
918
|
-
// Ensure that the QRCode library is loaded
|
|
919
|
-
if (!globalThis["QRCode"]) {
|
|
920
|
-
const url = "https://cdn.jsdelivr.net/gh/davidshimjs/qrcodejs@gh-pages/qrcode.min.js";
|
|
921
|
-
let script = document.head.querySelector(`script[src="${url}"]`) as HTMLScriptElement;
|
|
922
|
-
if (!script) {
|
|
923
|
-
script = document.createElement("script");
|
|
924
|
-
script.src = url;
|
|
925
|
-
document.head.appendChild(script);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
await new Promise((resolve, _reject) => {
|
|
929
|
-
script.addEventListener("load", () => {
|
|
930
|
-
resolve(true);
|
|
931
|
-
});
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
const QRCODE = globalThis["QRCode"];
|
|
936
|
-
const target = args.domElement ?? document.createElement("div");
|
|
937
|
-
const qrCode = new QRCODE(target, {
|
|
938
|
-
width: args.width ?? 256,
|
|
939
|
-
height: args.height ?? 256,
|
|
940
|
-
colorDark: "#000000",
|
|
941
|
-
colorLight: "#ffffff",
|
|
942
|
-
correctLevel: args.showLogo ? QRCODE.CorrectionLevel.H : QRCODE.CorrectLevel.M,
|
|
943
|
-
...args,
|
|
944
|
-
});
|
|
945
|
-
|
|
946
|
-
// Number of rows/columns of the generated QR code
|
|
947
|
-
const moduleCount = qrCode?._oQRCode.moduleCount || 0;
|
|
948
|
-
const canvas = qrCode?._oDrawing?._elCanvas as HTMLCanvasElement;
|
|
949
|
-
|
|
950
|
-
let sizePercentage = 0.25;
|
|
951
|
-
if (moduleCount < 40)
|
|
952
|
-
sizePercentage = Math.floor(moduleCount / 4) / moduleCount;
|
|
953
|
-
else
|
|
954
|
-
sizePercentage = Math.floor(moduleCount / 6) / moduleCount;
|
|
955
|
-
|
|
956
|
-
const paddingPercentage = Math.floor(moduleCount / 20) / moduleCount;
|
|
957
|
-
try {
|
|
958
|
-
const img = await addOverlays(canvas, { showLogo: args.showLogo, logoSize: sizePercentage, logoPadding: paddingPercentage }).catch(_e => { /** ignore */ });
|
|
959
|
-
if (img) {
|
|
960
|
-
target.innerHTML = "";
|
|
961
|
-
target.append(img);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
catch { } // Ignore
|
|
965
|
-
|
|
966
|
-
if (args.showUrl !== false && args.text) {
|
|
967
|
-
// Add link label below the QR code
|
|
968
|
-
// Clean up the text. If it's a URL: remove the protocol, www. part, trailing slashes or trailing question marks
|
|
969
|
-
const existingLabel = target.querySelector(".qr-code-link-label");
|
|
970
|
-
let displayText = args.text.replace(/^(https?:\/\/)?(www\.)?/, "").replace(/\/+$/, "").replace(/\?+$/, "");
|
|
971
|
-
displayText = "Scan to visit " + displayText;
|
|
972
|
-
if (existingLabel) {
|
|
973
|
-
existingLabel.textContent = displayText;
|
|
974
|
-
} else {
|
|
975
|
-
// Create a new label
|
|
976
|
-
const linkLabel = document.createElement("div");
|
|
977
|
-
linkLabel.classList.add("qr-code-link-label");
|
|
978
|
-
args.text = displayText;
|
|
979
|
-
linkLabel.textContent = args.text;
|
|
980
|
-
linkLabel.addEventListener("click", (ev) => {
|
|
981
|
-
// Prevent the QR panel from closing
|
|
982
|
-
ev.stopImmediatePropagation();
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
linkLabel.style.textAlign = "center";
|
|
986
|
-
linkLabel.style.fontSize = "0.8em";
|
|
987
|
-
linkLabel.style.marginTop = "0.1em";
|
|
988
|
-
linkLabel.style.color = "#000000";
|
|
989
|
-
linkLabel.style.fontFamily = "'Roboto Flex', sans-serif";
|
|
990
|
-
linkLabel.style.opacity = "0.5";
|
|
991
|
-
linkLabel.style.wordBreak = "break-all";
|
|
992
|
-
linkLabel.style.wordWrap = "break-word";
|
|
993
|
-
linkLabel.style.marginBottom = "0.3em";
|
|
994
|
-
|
|
995
|
-
// Ensure max. width
|
|
996
|
-
target.style.width = "calc(210px + 20px)";
|
|
997
|
-
|
|
998
|
-
target.appendChild(linkLabel);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
return target;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
async function addOverlays(canvas: HTMLCanvasElement, args: { showLogo?: boolean, logoSize?: number, logoPadding?: number }): Promise<HTMLImageElement | void> {
|
|
1006
|
-
if (!canvas) return;
|
|
1007
|
-
|
|
1008
|
-
// Internal settings
|
|
1009
|
-
const canvasPadding = 8;
|
|
1010
|
-
const shadowBlur = 20;
|
|
1011
|
-
const rectanglePadding = args.logoPadding || 1. / 32;
|
|
1012
|
-
// With dropshadow under the logo
|
|
1013
|
-
/*
|
|
1014
|
-
const shadowColor = "#00000099";
|
|
1015
|
-
const rectangleRadius = 0.4 * 16;
|
|
1016
|
-
*/
|
|
1017
|
-
// Without dropshadow under the logo
|
|
1018
|
-
const shadowColor = "transparent";
|
|
1019
|
-
const rectangleRadius = 0;
|
|
1020
|
-
|
|
1021
|
-
// Draw the website's icon in the center of the QR code
|
|
1022
|
-
const faviconImage = new Image();
|
|
1023
|
-
const element = document.querySelector("needle-engine") as NeedleEngineWebComponent;
|
|
1024
|
-
const logoSrc = element?.getAttribute("loading-logo-src") || needleLogoOnlySVG;
|
|
1025
|
-
if (!logoSrc) return;
|
|
1026
|
-
|
|
1027
|
-
let haveLogo = false;
|
|
1028
|
-
if (args.showLogo !== false) {
|
|
1029
|
-
faviconImage.src = logoSrc;
|
|
1030
|
-
haveLogo = await new Promise((resolve, _reject) => {
|
|
1031
|
-
faviconImage.onload = () => resolve(true);
|
|
1032
|
-
faviconImage.onerror = (err) => {
|
|
1033
|
-
console.error("Error loading favicon image for QR code", err);
|
|
1034
|
-
resolve(false);
|
|
1035
|
-
};
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
// Add some padding around the canvas – we need to copy the QR code image to a larger canvas
|
|
1040
|
-
const paddedCanvas = document.createElement("canvas");
|
|
1041
|
-
paddedCanvas.width = canvas.width + canvasPadding;
|
|
1042
|
-
paddedCanvas.height = canvas.height + canvasPadding;
|
|
1043
|
-
const paddedContext = paddedCanvas.getContext("2d");
|
|
1044
|
-
if (!paddedContext) {
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
// Clear with white
|
|
1049
|
-
paddedContext.fillStyle = "#ffffff";
|
|
1050
|
-
paddedContext.fillRect(0, 0, paddedCanvas.width, paddedCanvas.height);
|
|
1051
|
-
paddedContext.drawImage(canvas, canvasPadding / 2, canvasPadding / 2);
|
|
1052
|
-
|
|
1053
|
-
// Enable anti-aliasing
|
|
1054
|
-
paddedContext.imageSmoothingEnabled = true;
|
|
1055
|
-
paddedContext.imageSmoothingQuality = "high";
|
|
1056
|
-
// @ts-ignore
|
|
1057
|
-
paddedContext.mozImageSmoothingEnabled = true;
|
|
1058
|
-
// @ts-ignore
|
|
1059
|
-
paddedContext.webkitImageSmoothingEnabled = true;
|
|
1060
|
-
|
|
1061
|
-
// Draw a slight gradient background with 10% opacity and "lighten" composite operation
|
|
1062
|
-
paddedContext.globalCompositeOperation = "lighten";
|
|
1063
|
-
const gradient = paddedContext.createLinearGradient(0, 0, 0, paddedCanvas.height);
|
|
1064
|
-
gradient.addColorStop(0, "rgb(45, 45, 45)");
|
|
1065
|
-
gradient.addColorStop(1, "rgb(45, 45, 45)");
|
|
1066
|
-
paddedContext.fillStyle = gradient;
|
|
1067
|
-
paddedContext.fillRect(0, 0, paddedCanvas.width, paddedCanvas.height);
|
|
1068
|
-
paddedContext.globalCompositeOperation = "source-over";
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
let sizeX = Math.min(canvas.width, canvas.height) * (args.logoSize || 0.25);
|
|
1072
|
-
let sizeY = sizeX;
|
|
1073
|
-
|
|
1074
|
-
if (haveLogo) {
|
|
1075
|
-
// Get aspect of image
|
|
1076
|
-
const aspect = faviconImage.width / faviconImage.height;
|
|
1077
|
-
if (aspect > 1) sizeY = sizeX / aspect;
|
|
1078
|
-
else sizeX = sizeY * aspect;
|
|
1079
|
-
|
|
1080
|
-
const rectanglePaddingPx = rectanglePadding * canvas.width;
|
|
1081
|
-
|
|
1082
|
-
// Apply padding
|
|
1083
|
-
const sizeForBackground = Math.max(sizeX, sizeY);
|
|
1084
|
-
const sizeXPadded = Math.round(sizeForBackground + rectanglePaddingPx);
|
|
1085
|
-
const sizeYPadded = Math.round(sizeForBackground + rectanglePaddingPx);
|
|
1086
|
-
const x = (paddedCanvas.width - sizeForBackground) / 2;
|
|
1087
|
-
const y = (paddedCanvas.height - sizeForBackground) / 2;
|
|
1088
|
-
|
|
1089
|
-
// Draw shape with blurred shadow
|
|
1090
|
-
paddedContext.shadowColor = shadowColor;
|
|
1091
|
-
paddedContext.shadowBlur = shadowBlur;
|
|
1092
|
-
|
|
1093
|
-
// Draw rounded rectangle with radius
|
|
1094
|
-
// Convert 0.4rem to pixels, taking DPI into account
|
|
1095
|
-
const radius = rectangleRadius;
|
|
1096
|
-
const xPadded = Math.round(x - rectanglePaddingPx / 2);
|
|
1097
|
-
const yPadded = Math.round(y - rectanglePaddingPx / 2);
|
|
1098
|
-
paddedContext.beginPath();
|
|
1099
|
-
paddedContext.moveTo(xPadded + radius, yPadded);
|
|
1100
|
-
paddedContext.lineTo(xPadded + sizeXPadded - radius, yPadded);
|
|
1101
|
-
paddedContext.quadraticCurveTo(xPadded + sizeXPadded, yPadded, xPadded + sizeXPadded, yPadded + radius);
|
|
1102
|
-
paddedContext.lineTo(xPadded + sizeXPadded, yPadded + sizeYPadded - radius);
|
|
1103
|
-
paddedContext.quadraticCurveTo(xPadded + sizeXPadded, yPadded + sizeYPadded, xPadded + sizeXPadded - radius, yPadded + sizeYPadded);
|
|
1104
|
-
paddedContext.lineTo(xPadded + radius, yPadded + sizeYPadded);
|
|
1105
|
-
paddedContext.quadraticCurveTo(xPadded, yPadded + sizeYPadded, xPadded, yPadded + sizeYPadded - radius);
|
|
1106
|
-
paddedContext.lineTo(xPadded, yPadded + radius);
|
|
1107
|
-
paddedContext.quadraticCurveTo(xPadded, yPadded, xPadded + radius, yPadded);
|
|
1108
|
-
paddedContext.fillStyle = "#ffffff";
|
|
1109
|
-
paddedContext.closePath();
|
|
1110
|
-
paddedContext.fill();
|
|
1111
|
-
paddedContext.clip();
|
|
1112
|
-
|
|
1113
|
-
// Reset shadow and draw favicon
|
|
1114
|
-
paddedContext.shadowColor = "transparent";
|
|
1115
|
-
const logoX = (paddedCanvas.width - sizeX) / 2;
|
|
1116
|
-
const logoY = (paddedCanvas.height - sizeY) / 2;
|
|
1117
|
-
paddedContext.drawImage(faviconImage, logoX, logoY, sizeX, sizeY);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
// Replace the canvas with the padded one
|
|
1121
|
-
const paddedImage = paddedCanvas.toDataURL("image/png");
|
|
1122
|
-
const img = document.createElement("img");
|
|
1123
|
-
img.src = paddedImage;
|
|
1124
|
-
img.style.width = "100%";
|
|
1125
|
-
img.style.height = "auto";
|
|
1126
|
-
|
|
1127
|
-
return img;
|
|
1128
|
-
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export namespace InternalAttributeUtils {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checks if the given value is considered "falsey" in the context of HTML attributes.
|
|
10
|
+
* A value is considered falsey if it is "0" or "false" (case-insensitive).
|
|
11
|
+
*
|
|
12
|
+
* @param value - The attribute value to check.
|
|
13
|
+
* @returns True if the value is falsey, otherwise false.
|
|
14
|
+
*/
|
|
15
|
+
export function isFalsey(value: string | null): boolean {
|
|
16
|
+
return value === "0" || value?.toLowerCase() === "false";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves the value of the specified attribute from the given element.
|
|
21
|
+
* If the attribute value is considered falsey, it returns null.
|
|
22
|
+
* @returns The attribute value or null if falsey.
|
|
23
|
+
*/
|
|
24
|
+
export function getAttributeValueIfNotFalsey(element: Element, attributeName: string, opts?: { onAttribute: (value: string) => void }): string | null {
|
|
25
|
+
const attrValue = element.getAttribute(attributeName);
|
|
26
|
+
if (isFalsey(attrValue)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
opts?.onAttribute?.call(null, attrValue!);
|
|
30
|
+
return attrValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Retrieves the value of the specified attribute from the given element.
|
|
35
|
+
* If the attribute value is considered falsey, it returns false.
|
|
36
|
+
* If the attribute is not set at all, it returns null.
|
|
37
|
+
* @returns The attribute value, false if falsey, or null if not set.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const result = HTMLAttributeUtils.getAttributeAndCheckFalsey(element, 'data-example', {
|
|
42
|
+
* onAttribute: (value, falsey) => {
|
|
43
|
+
* console.log(`Attribute value: ${value}
|
|
44
|
+
* , Is falsey: ${falsey}`);
|
|
45
|
+
* }
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* if (result === false) {
|
|
49
|
+
* console.log('The attribute is set to a falsey value.');
|
|
50
|
+
* } else if (result === null) {
|
|
51
|
+
* console.log('The attribute is not set.');
|
|
52
|
+
* } else {
|
|
53
|
+
* console.log(`The attribute value is: ${result}`);
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function getAttributeAndCheckFalsey(element: Element, attributeName: string, opts?: { onAttribute: (value: string, falsey:boolean) => void }): false | string | null {
|
|
58
|
+
const attrValue = element.getAttribute(attributeName);
|
|
59
|
+
// If the attribute is not set at all we just return
|
|
60
|
+
if(attrValue === null) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (isFalsey(attrValue)) {
|
|
64
|
+
opts?.onAttribute?.call(null, attrValue!, true);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
opts?.onAttribute?.call(null, attrValue!, false);
|
|
68
|
+
return attrValue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
|
|
2
|
+
// use for typesafe interface method calls
|
|
3
|
+
import { Quaternion, Vector2, Vector3, Vector4 } from "three";
|
|
4
|
+
|
|
5
|
+
import { needleLogoOnlySVG } from "./assets/index.js";
|
|
6
|
+
import { isDevEnvironment } from "./debug/debug.js";
|
|
7
|
+
import { hasCommercialLicense } from "./engine_license.js";
|
|
8
|
+
import { InternalAttributeUtils } from "./engine_utils_attributes.js";
|
|
9
|
+
import type { NeedleEngineWebComponent } from "./webcomponents/needle-engine.js";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/** Generates a QR code HTML image using https://github.com/davidshimjs/qrcodejs
|
|
14
|
+
* @param args.text The text to encode
|
|
15
|
+
* @param args.width The width of the QR code
|
|
16
|
+
* @param args.height The height of the QR code
|
|
17
|
+
* @param args.colorDark The color of the dark squares
|
|
18
|
+
* @param args.colorLight The color of the light squares
|
|
19
|
+
* @param args.correctLevel The error correction level to use
|
|
20
|
+
* @param args.showLogo If true, the logo will be shown in the center of the QR code. By default the Needle Logo will be used. You can override which logo is being used by setting the `needle-engine` web component's `qr-logo-src` attribute. The logo can also be disabled by setting that attribute to a falsey value (e.g. "0" or "false")
|
|
21
|
+
* @param args.showUrl If true, the URL will be shown below the QR code
|
|
22
|
+
* @param args.domElement The dom element to append the QR code to. If not provided a new div will be created and returned
|
|
23
|
+
* @returns The dom element containing the QR code
|
|
24
|
+
*/
|
|
25
|
+
export async function generateQRCode(args: { domElement?: HTMLElement, text: string, width?: number, height?: number, colorDark?: string, colorLight?: string, correctLevel?: any, showLogo?: boolean, showUrl?: boolean }): Promise<HTMLElement> {
|
|
26
|
+
|
|
27
|
+
// Ensure that the QRCode library is loaded
|
|
28
|
+
if (!globalThis["QRCode"]) {
|
|
29
|
+
const url = "https://cdn.jsdelivr.net/gh/davidshimjs/qrcodejs@gh-pages/qrcode.min.js";
|
|
30
|
+
let script = document.head.querySelector(`script[src="${url}"]`) as HTMLScriptElement;
|
|
31
|
+
if (!script) {
|
|
32
|
+
script = document.createElement("script");
|
|
33
|
+
script.src = url;
|
|
34
|
+
document.head.appendChild(script);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await new Promise((resolve, _reject) => {
|
|
38
|
+
script.addEventListener("load", () => {
|
|
39
|
+
resolve(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const QRCODE = globalThis["QRCode"];
|
|
45
|
+
const target = args.domElement ?? document.createElement("div");
|
|
46
|
+
const qrCode = new QRCODE(target, {
|
|
47
|
+
width: args.width ?? 256,
|
|
48
|
+
height: args.height ?? 256,
|
|
49
|
+
colorDark: "#000000",
|
|
50
|
+
colorLight: "#ffffff",
|
|
51
|
+
correctLevel: args.showLogo ? QRCODE.CorrectionLevel.H : QRCODE.CorrectLevel.M,
|
|
52
|
+
...args,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Number of rows/columns of the generated QR code
|
|
56
|
+
const moduleCount = qrCode?._oQRCode.moduleCount || 0;
|
|
57
|
+
const canvas = qrCode?._oDrawing?._elCanvas as HTMLCanvasElement;
|
|
58
|
+
|
|
59
|
+
let sizePercentage = 0.25;
|
|
60
|
+
if (moduleCount < 40)
|
|
61
|
+
sizePercentage = Math.floor(moduleCount / 4) / moduleCount;
|
|
62
|
+
else
|
|
63
|
+
sizePercentage = Math.floor(moduleCount / 6) / moduleCount;
|
|
64
|
+
|
|
65
|
+
const paddingPercentage = Math.floor(moduleCount / 20) / moduleCount;
|
|
66
|
+
try {
|
|
67
|
+
const img = await internalRenderQRCodeOverlays(canvas, { showLogo: args.showLogo, logoSize: sizePercentage, logoPadding: paddingPercentage }).catch(_e => { /** ignore */ });
|
|
68
|
+
if (img) {
|
|
69
|
+
target.innerHTML = "";
|
|
70
|
+
target.append(img);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch { } // Ignore
|
|
74
|
+
|
|
75
|
+
if (args.showUrl !== false && args.text) {
|
|
76
|
+
// Add link label below the QR code
|
|
77
|
+
// Clean up the text. If it's a URL: remove the protocol, www. part, trailing slashes or trailing question marks
|
|
78
|
+
const existingLabel = target.querySelector(".qr-code-link-label");
|
|
79
|
+
let displayText = args.text.replace(/^(https?:\/\/)?(www\.)?/, "").replace(/\/+$/, "").replace(/\?+$/, "");
|
|
80
|
+
displayText = "Scan to visit " + displayText;
|
|
81
|
+
if (existingLabel) {
|
|
82
|
+
existingLabel.textContent = displayText;
|
|
83
|
+
} else {
|
|
84
|
+
// Create a new label
|
|
85
|
+
const linkLabel = document.createElement("div");
|
|
86
|
+
linkLabel.classList.add("qr-code-link-label");
|
|
87
|
+
args.text = displayText;
|
|
88
|
+
linkLabel.textContent = args.text;
|
|
89
|
+
linkLabel.addEventListener("click", (ev) => {
|
|
90
|
+
// Prevent the QR panel from closing
|
|
91
|
+
ev.stopImmediatePropagation();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
linkLabel.style.textAlign = "center";
|
|
95
|
+
linkLabel.style.fontSize = "0.8em";
|
|
96
|
+
linkLabel.style.marginTop = "0.1em";
|
|
97
|
+
linkLabel.style.color = "#000000";
|
|
98
|
+
linkLabel.style.fontFamily = "'Roboto Flex', sans-serif";
|
|
99
|
+
linkLabel.style.opacity = "0.5";
|
|
100
|
+
linkLabel.style.wordBreak = "break-all";
|
|
101
|
+
linkLabel.style.wordWrap = "break-word";
|
|
102
|
+
linkLabel.style.marginBottom = "0.3em";
|
|
103
|
+
|
|
104
|
+
// Ensure max. width
|
|
105
|
+
target.style.width = "calc(210px + 20px)";
|
|
106
|
+
|
|
107
|
+
target.appendChild(linkLabel);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return target;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async function internalRenderQRCodeOverlays(canvas: HTMLCanvasElement, args: { showLogo?: boolean, logoSize?: number, logoPadding?: number }): Promise<HTMLImageElement | void> {
|
|
116
|
+
if (!canvas) return;
|
|
117
|
+
|
|
118
|
+
// Internal settings
|
|
119
|
+
const canvasPadding = 8;
|
|
120
|
+
const shadowBlur = 20;
|
|
121
|
+
const rectanglePadding = args.logoPadding || 1. / 32;
|
|
122
|
+
// With dropshadow under the logo
|
|
123
|
+
/*
|
|
124
|
+
const shadowColor = "#00000099";
|
|
125
|
+
const rectangleRadius = 0.4 * 16;
|
|
126
|
+
*/
|
|
127
|
+
// Without dropshadow under the logo
|
|
128
|
+
const shadowColor = "transparent";
|
|
129
|
+
const rectangleRadius = 0;
|
|
130
|
+
|
|
131
|
+
// Draw the website's icon in the center of the QR code
|
|
132
|
+
const image = new Image();
|
|
133
|
+
const element = document.querySelector("needle-engine") as NeedleEngineWebComponent;
|
|
134
|
+
if (!element) {
|
|
135
|
+
console.debug("[QR Code] No web component found")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const canUseCustomLogo = hasCommercialLicense();
|
|
139
|
+
|
|
140
|
+
// Query logo src from needle-engine attribute.
|
|
141
|
+
// For any supported attribute it's possible to use "falsey" values (e.g. "0" or "false" to disable the logo in the QR code)
|
|
142
|
+
let logoSrc: false | string | null = null;
|
|
143
|
+
logoSrc = InternalAttributeUtils.getAttributeAndCheckFalsey(element, "qrcode-logo-src");
|
|
144
|
+
if (canUseCustomLogo && args.showLogo !== true && logoSrc === false) return; // Explictly disabled
|
|
145
|
+
logoSrc ||= InternalAttributeUtils.getAttributeAndCheckFalsey(element, "logo-src");
|
|
146
|
+
if (canUseCustomLogo && args.showLogo !== true && logoSrc === false) return; // Explicitly disabled
|
|
147
|
+
logoSrc ||= InternalAttributeUtils.getAttributeAndCheckFalsey(element, "loading-logo-src", {
|
|
148
|
+
onAttribute: () => {
|
|
149
|
+
if (isDevEnvironment()) console.warn("[QR Code] 'loading-logo-src' is deprecated, please use 'logo-src' or 'qrcode-logo-src' instead.");
|
|
150
|
+
else console.debug("[QR Code] 'loading-logo-src' is deprecated.");
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
if (canUseCustomLogo && args.showLogo !== true && logoSrc === false) return; // Explicitly disabled
|
|
154
|
+
|
|
155
|
+
if (logoSrc && !canUseCustomLogo) {
|
|
156
|
+
console.warn("[QR Code] Custom logo is only available with a commercial license. Using default Needle logo. Please get a commercial license at https://needle.tools/pricing.");
|
|
157
|
+
logoSrc = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
logoSrc ||= needleLogoOnlySVG;
|
|
161
|
+
if (!logoSrc) return;
|
|
162
|
+
|
|
163
|
+
let haveLogo = false;
|
|
164
|
+
if (args.showLogo !== false) {
|
|
165
|
+
image.src = logoSrc;
|
|
166
|
+
haveLogo = await new Promise((resolve, _reject) => {
|
|
167
|
+
image.onload = () => resolve(true);
|
|
168
|
+
image.onerror = (err) => {
|
|
169
|
+
const errorUrl = logoSrc !== needleLogoOnlySVG ? "'" + logoSrc + "'" : null;
|
|
170
|
+
console.error("[QR Code] Error loading logo image for QR code", errorUrl, isDevEnvironment() ? err : "");
|
|
171
|
+
resolve(false);
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Add some padding around the canvas – we need to copy the QR code image to a larger canvas
|
|
177
|
+
const paddedCanvas = document.createElement("canvas");
|
|
178
|
+
paddedCanvas.width = canvas.width + canvasPadding;
|
|
179
|
+
paddedCanvas.height = canvas.height + canvasPadding;
|
|
180
|
+
const paddedContext = paddedCanvas.getContext("2d");
|
|
181
|
+
if (!paddedContext) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Clear with white
|
|
186
|
+
paddedContext.fillStyle = "#ffffff";
|
|
187
|
+
paddedContext.fillRect(0, 0, paddedCanvas.width, paddedCanvas.height);
|
|
188
|
+
paddedContext.drawImage(canvas, canvasPadding / 2, canvasPadding / 2);
|
|
189
|
+
|
|
190
|
+
// Enable anti-aliasing
|
|
191
|
+
paddedContext.imageSmoothingEnabled = true;
|
|
192
|
+
paddedContext.imageSmoothingQuality = "high";
|
|
193
|
+
// @ts-ignore
|
|
194
|
+
paddedContext.mozImageSmoothingEnabled = true;
|
|
195
|
+
// @ts-ignore
|
|
196
|
+
paddedContext.webkitImageSmoothingEnabled = true;
|
|
197
|
+
|
|
198
|
+
// Draw a slight gradient background with 10% opacity and "lighten" composite operation
|
|
199
|
+
paddedContext.globalCompositeOperation = "lighten";
|
|
200
|
+
const gradient = paddedContext.createLinearGradient(0, 0, 0, paddedCanvas.height);
|
|
201
|
+
gradient.addColorStop(0, "rgb(45, 45, 45)");
|
|
202
|
+
gradient.addColorStop(1, "rgb(45, 45, 45)");
|
|
203
|
+
paddedContext.fillStyle = gradient;
|
|
204
|
+
paddedContext.fillRect(0, 0, paddedCanvas.width, paddedCanvas.height);
|
|
205
|
+
paddedContext.globalCompositeOperation = "source-over";
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
let sizeX = Math.min(canvas.width, canvas.height) * (args.logoSize || 0.25);
|
|
209
|
+
let sizeY = sizeX;
|
|
210
|
+
|
|
211
|
+
if (haveLogo) {
|
|
212
|
+
// Get aspect of image
|
|
213
|
+
const aspect = image.width / image.height;
|
|
214
|
+
if (aspect > 1) sizeY = sizeX / aspect;
|
|
215
|
+
else sizeX = sizeY * aspect;
|
|
216
|
+
|
|
217
|
+
const rectanglePaddingPx = rectanglePadding * canvas.width;
|
|
218
|
+
|
|
219
|
+
// Apply padding
|
|
220
|
+
const sizeForBackground = Math.max(sizeX, sizeY);
|
|
221
|
+
const sizeXPadded = Math.round(sizeForBackground + rectanglePaddingPx);
|
|
222
|
+
const sizeYPadded = Math.round(sizeForBackground + rectanglePaddingPx);
|
|
223
|
+
const x = (paddedCanvas.width - sizeForBackground) / 2;
|
|
224
|
+
const y = (paddedCanvas.height - sizeForBackground) / 2;
|
|
225
|
+
|
|
226
|
+
// Draw shape with blurred shadow
|
|
227
|
+
paddedContext.shadowColor = shadowColor;
|
|
228
|
+
paddedContext.shadowBlur = shadowBlur;
|
|
229
|
+
|
|
230
|
+
// Draw rounded rectangle with radius
|
|
231
|
+
// Convert 0.4rem to pixels, taking DPI into account
|
|
232
|
+
const radius = rectangleRadius;
|
|
233
|
+
const xPadded = Math.round(x - rectanglePaddingPx / 2);
|
|
234
|
+
const yPadded = Math.round(y - rectanglePaddingPx / 2);
|
|
235
|
+
paddedContext.beginPath();
|
|
236
|
+
paddedContext.moveTo(xPadded + radius, yPadded);
|
|
237
|
+
paddedContext.lineTo(xPadded + sizeXPadded - radius, yPadded);
|
|
238
|
+
paddedContext.quadraticCurveTo(xPadded + sizeXPadded, yPadded, xPadded + sizeXPadded, yPadded + radius);
|
|
239
|
+
paddedContext.lineTo(xPadded + sizeXPadded, yPadded + sizeYPadded - radius);
|
|
240
|
+
paddedContext.quadraticCurveTo(xPadded + sizeXPadded, yPadded + sizeYPadded, xPadded + sizeXPadded - radius, yPadded + sizeYPadded);
|
|
241
|
+
paddedContext.lineTo(xPadded + radius, yPadded + sizeYPadded);
|
|
242
|
+
paddedContext.quadraticCurveTo(xPadded, yPadded + sizeYPadded, xPadded, yPadded + sizeYPadded - radius);
|
|
243
|
+
paddedContext.lineTo(xPadded, yPadded + radius);
|
|
244
|
+
paddedContext.quadraticCurveTo(xPadded, yPadded, xPadded + radius, yPadded);
|
|
245
|
+
paddedContext.fillStyle = "#ffffff";
|
|
246
|
+
paddedContext.closePath();
|
|
247
|
+
paddedContext.fill();
|
|
248
|
+
paddedContext.clip();
|
|
249
|
+
|
|
250
|
+
// Reset shadow and draw favicon
|
|
251
|
+
paddedContext.shadowColor = "transparent";
|
|
252
|
+
const logoX = (paddedCanvas.width - sizeX) / 2;
|
|
253
|
+
const logoY = (paddedCanvas.height - sizeY) / 2;
|
|
254
|
+
paddedContext.drawImage(image, logoX, logoY, sizeX, sizeY);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Replace the canvas with the padded one
|
|
258
|
+
const paddedImage = paddedCanvas.toDataURL("image/png");
|
|
259
|
+
const img = document.createElement("img");
|
|
260
|
+
img.src = paddedImage;
|
|
261
|
+
img.style.width = "100%";
|
|
262
|
+
img.style.height = "auto";
|
|
263
|
+
|
|
264
|
+
return img;
|
|
265
|
+
}
|
|
266
|
+
|