UNPKG

@tresjs/cientos

Version:

Collection of useful helpers and fully functional, ready-made abstractions for Tres

1,627 lines (1,556 loc) 375 kB
/** * name: @tresjs/cientos * version: v5.7.1 * (c) 2026 * description: Collection of useful helpers and fully functional, ready-made abstractions for Tres * author: Alvaro Saburido <hola@alvarosaburido.dev> (https://github.com/alvarosabu/) */ import { Fragment, Suspense, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, isReactive, isRef, mergeDefaults, mergeProps, nextTick, normalizeProps, onBeforeUnmount, onMounted, onUnmounted, openBlock, provide, reactive, ref, render, renderList, renderSlot, shallowReactive, shallowRef, toRaw, toRefs, toValue, triggerRef, unref, useAttrs, useSlots, watch, watchEffect, withAsyncContext, withCtx } from "vue"; import { buildGraph, createTimer, extend, isObject3D, logError, logWarning, normalizeColor, normalizeVectorFlexibleParam, useLoader, useLoop, useTres, useTresContext } from "@tresjs/core"; import * as THREE from "three"; import { AdditiveBlending, AlwaysStencilFunc, AnimationMixer, Audio, AudioListener, AudioLoader, BackSide, Box2, Box3, BoxGeometry, BufferAttribute, BufferGeometry, Camera, CatmullRomCurve3, ClampToEdgeWrapping, Color, CubeCamera, CubeReflectionMapping, CubeTextureLoader, CubicBezierCurve3, DataTexture, DefaultLoadingManager, DepthTexture, DirectionalLight, DoubleSide, EdgesGeometry, EqualStencilFunc, EquirectangularReflectionMapping, Euler, FloatType, FramebufferTexture, FrontSide, Group, HalfFloatType, IcosahedronGeometry, InstancedMesh, InterleavedBuffer, InterleavedBufferAttribute, KeepStencilOp, LOD, LinearFilter, MOUSE, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshLambertMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NotEqualStencilFunc, Object3D, OrthographicCamera, PerspectiveCamera, Plane, PlaneGeometry, Points, PointsMaterial, QuadraticBezierCurve3, Quaternion, REVISION, RGBAFormat, RawShaderMaterial, Raycaster, RepeatWrapping, ReplaceStencilOp, Scene, ShaderChunk, ShaderMaterial, ShapeGeometry, SkinnedMesh, Sphere, Spherical, TOUCH, TangentSpaceNormalMap, Texture, TextureLoader, UVMapping, Uniform, UniformsUtils, UnsignedByteType, Vector2, Vector3, Vector4, VideoTexture, WebGLCubeRenderTarget, WebGLRenderTarget, WebGLRenderer } from "three"; import { tryOnScopeDispose, useDebounceFn, useElementSize, useEventListener, useMagicKeys, useMouse, useScroll, useWindowScroll, useWindowSize, watchThrottled, whenever } from "@vueuse/core"; import { DRACOLoader, FBXLoader, FontLoader, GLTFExporter, GLTFLoader, HorizontalBlurShader, Line2, LineGeometry, LineMaterial, MapControls, MarchingCubes, MeshSurfaceSampler, OrbitControls, PointerLockControls, PositionalAudioHelper, RGBELoader, Reflector, RoundedBoxGeometry, SVGLoader, SimplexNoise, Sky, TextGeometry, TransformControls, VerticalBlurShader, Water, toCreasedNormals } from "three-stdlib"; import BaseCameraControls, { default as CameraControls } from "camera-controls"; import CustomShaderMaterial from "three-custom-shader-material/vanilla"; import StatsImpl from "stats.js"; import StatsGlImpl from "stats-gl"; import { BVHHelper, MeshBVH, acceleratedRaycast } from "three-mesh-bvh"; //#region src/core/abstractions/AnimatedSprite/AtlasAnimationDefinitionParser.ts /** * Expand an animation definition string into an array of numbers. * @param definitionStr - A comma-separated string of frame numbers with optional parentheses-surrounded durations. * @example - expand("0,2") === [0,2] * @example - expand("2(10)") === [2,2,2,2,2,2,2,2,2,2] * @example - expand("1-4") === [1,2,3,4] * @example - expand("10-5(2)") === [10,10,9,9,8,8,7,7,6,6,5,5] * @example - expand("1-4(3),10(2)") === [1,1,1,2,2,2,3,3,3,4,4,4,10,10] */ function expand(definitionStr) { const parsed = parse(definitionStr); const result = []; for (const { startFrame, endFrame, duration } of parsed) if (duration <= 0) continue; else if (endFrame < 0 || startFrame === endFrame) { for (let _ = 0; _ < duration; _++) result.push(startFrame); continue; } else { const sign = Math.sign(endFrame - startFrame); for (let frame = startFrame; frame !== endFrame + sign; frame += sign) for (let _ = 0; _ < duration; _++) result.push(frame); } return result; } /** * Parse an animation defintion string into an array of AnimationDefinition. * @param definitionStr - A comma-separated string of frame numbers with optional parentheses-surrounded durations. * @example - parse("0,2") === [{startFrame:0, endFrame:0, duration:1}, {startFrame:2, endFrame:2, duration:1}] * @example - parse("2(10)") === [{startFrame:2, endFrame:2, duration:10}] * @example - parse("1-4") === [{startFrame:1, endFrame:4, duration:1}] * @example - parse("10-5(2)") === [{startFrame:10, endFrame:5, duration:2}] * @example - parse("1-4(3),10(2)") === [{startFrame:1, endFrame:4, duration:3}, {startFrame:10, endFrame:10, duration:2}] */ function parse(definitionStr) { let transition = "START_FRAME_IN"; const result = []; for (const { name, value, startI } of tokenize(definitionStr)) if (transition === "START_FRAME_IN") if (name === "NUMBER") { result.push({ startFrame: value, endFrame: value, duration: 1 }); transition = "START_FRAME_OUT"; } else logDefinitionSyntaxError("number", name, definitionStr, startI); else if (transition === "START_FRAME_OUT") if (name === "COMMA") transition = "START_FRAME_IN"; else if (name === "HYPHEN") transition = "END_FRAME_IN"; else if (name === "OPEN_PAREN") transition = "DURATION_IN"; else logDefinitionSyntaxError("\",\", \"-\", \"(\"", name, definitionStr, startI); else if (transition === "END_FRAME_IN") if (name === "NUMBER") { result[result.length - 1].endFrame = value; transition = "END_FRAME_OUT"; } else logDefinitionSyntaxError("number", name, definitionStr, startI); else if (transition === "END_FRAME_OUT") if (name === "COMMA") transition = "START_FRAME_IN"; else if (name === "OPEN_PAREN") transition = "DURATION_IN"; else logDefinitionSyntaxError("',' or '('", name, definitionStr, startI); else if (transition === "DURATION_IN") if (name === "NUMBER") { result[result.length - 1].duration = value; transition = "DURATION_OUT"; } else logDefinitionSyntaxError("number", name, definitionStr, startI); else if (transition === "DURATION_OUT") if (name === "CLOSE_PAREN") transition = "NEXT_OR_DONE"; else logDefinitionSyntaxError("\"(\"", name, definitionStr, startI); else if (transition === "NEXT_OR_DONE") if (name === "COMMA") transition = "START_FRAME_IN"; else logDefinitionSyntaxError("\",\"", name, definitionStr, startI); return result; } function tokenize(definition) { const result = []; for (let ii = 0; ii < definition.length; ii++) { const c = definition[ii]; if ("0123456789".includes(c)) if (result.length && result[result.length - 1].name === "NUMBER") { result[result.length - 1].value *= 10; result[result.length - 1].value += Number.parseInt(c); } else result.push({ name: "NUMBER", value: Number.parseInt(c), startI: ii }); else if (c === " ") continue; else if (c === ",") result.push({ name: "COMMA", value: -1, startI: ii }); else if (c === "(") result.push({ name: "OPEN_PAREN", value: -1, startI: ii }); else if (c === ")") result.push({ name: "CLOSE_PAREN", value: -1, startI: ii }); else if (c === "-") result.push({ name: "HYPHEN", value: -1, startI: ii }); else logDefinitionBadCharacter("0123456789,-()", c, definition, ii); } return result; } function logDefinitionBadCharacter(expected, found, definition, index) { logError(`Cientos AnimationDefinitionParser: Unexpected character while processing animation definition: expected ${expected}, got ${found}. ${definition} ${Array.from({ length: index + 1 }).join(" ")}^`); } function logDefinitionSyntaxError(expected, found, definition, index) { logError(`Cientos AnimationDefinitionParser: Syntax error while processing animation definition: expected ${expected}, got ${found}. ${definition} ${Array.from({ length: index + 1 }).join(" ")}^`); } //#endregion //#region src/core/abstractions/AnimatedSprite/StringOps.ts const numbersAtEnd = /\d*$/; const underscoresNumbersAtEnd = /_*\d*$/; function stripUnderscoresNumbersFromEnd(str) { return str.replace(underscoresNumbersAtEnd, ""); } function getNumbersFromEnd(str) { const matches = str.match(numbersAtEnd); if (matches) return Number.parseInt(matches[matches.length - 1]); return null; } //#endregion //#region src/core/abstractions/AnimatedSprite/Atlas.ts async function getTextureAndAtlasAsync(imagePathOrImageData, atlasPathOrAtlasish) { const loader = new TextureLoader(); const texturePromise = new Promise((resolve, reject) => { loader.load(imagePathOrImageData, resolve, void 0, reject); }); const atlasishPromise = typeof atlasPathOrAtlasish !== "string" ? new Promise((resolve) => resolve(atlasPathOrAtlasish)) : fetch(atlasPathOrAtlasish).then((response) => response.json()).catch((e) => logError(`Cientos Atlas - ${e}`)); return Promise.all([texturePromise, atlasishPromise]).then(([texture$1, atlasish]) => { return [texture$1, getAtlas(atlasish, texture$1.image.width, texture$1.image.height)]; }); } function getAtlas(atlasish, textureWidth, textureHeight) { const frames = typeof atlasish === "number" || Array.isArray(atlasish) ? getAtlasFramesFromNumColsNumRows(atlasish, textureWidth, textureHeight) : getAtlasFramesFromTexturePackerData(atlasish, textureWidth, textureHeight); return { frames, animations: groupAtlasFramesByKey(frames) }; } function getAtlasFrames(atlas, animationNameOrFrameNumber, reversed) { let frames; if (typeof animationNameOrFrameNumber === "string") frames = getAtlasFramesByAnimationName(atlas, animationNameOrFrameNumber); else if (typeof animationNameOrFrameNumber === "number") frames = getAtlasFramesByIndices(atlas, animationNameOrFrameNumber, animationNameOrFrameNumber); else frames = getAtlasFramesByIndices(atlas, animationNameOrFrameNumber[0], animationNameOrFrameNumber[1]); return reversed ? frames.toReversed() : frames; } function getNullAtlasFrame() { return { name: "null", width: 0, height: 0, offsetX: 0, offsetY: 0, repeatX: 0, repeatY: 0 }; } function getAtlasFramesFromTexturePackerData(data, width, height) { return Array.isArray(data.frames) ? getAtlasFramesFromTexturePackerDataArray(data, width, height) : getAtlasFramesFromTexturePackerDataObject(data, width, height); } function getAtlasFramesFromTexturePackerDataArray(data, width, height) { const invWidth = 1 / width; const invHeight = 1 / height; return data.frames.map((d) => ({ name: d.filename, offsetX: d.frame.x * invWidth, offsetY: 1 - (d.frame.y + d.frame.h) * invHeight, repeatX: d.frame.w * invWidth, repeatY: d.frame.h * invHeight, width: d.frame.w, height: d.frame.h })); } function getAtlasFramesFromTexturePackerDataObject(data, width, height) { const invWidth = 1 / width; const invHeight = 1 / height; return Object.entries(data.frames).map(([k, v]) => ({ name: k, offsetX: v.frame.x * invWidth, offsetY: 1 - (v.frame.y + v.frame.h) * invHeight, repeatX: v.frame.w * invWidth, repeatY: v.frame.h * invHeight, width: v.frame.w, height: v.frame.h })); } function getAtlasFramesFromNumColsNumRows(numColsOrNumColsNumRows, width, height, name = "default") { const [numCols, numRows] = Array.isArray(numColsOrNumColsNumRows) ? numColsOrNumColsNumRows : [numColsOrNumColsNumRows, 1]; const frameWidth = width / numCols; const frameHeight = height / numRows; const padAmount = (numCols * numRows).toString().length; const repeatX = 1 / numCols; const repeatY = 1 / numRows; const result = []; let i = 0; for (let row = numRows - 1; row >= 0; row--) for (let col = 0; col < numCols; col++) { i++; result.push({ name: name + String(i).padStart(padAmount, "0"), offsetX: col * repeatX, offsetY: row * repeatY, repeatX, repeatY, width: frameWidth, height: frameHeight }); } return result; } function setAtlasDefinitions(atlas, definitions = {}) { const animations = groupAtlasFramesByKey(atlas.frames); for (const [animationName, definitionStr] of Object.entries(definitions)) { const frames = getAtlasFrames(atlas, animationName, false); const expandedFrameIndices = expand(definitionStr); for (const frameIndex of expandedFrameIndices) if (frameIndex < 0 || frames.length <= frameIndex) logError(`Cientos Atlas: Attempting to access frame index ${frameIndex} in animation ${animationName}, but it does not exist.`); animations[animationName] = expandedFrameIndices.map((frameIndex) => frames[frameIndex]); } atlas.animations = animations; } function getAtlasFramesByAnimationName(atlas, name) { if (!(name in atlas.animations)) { logError(`Cientos Atlas: getAtlasFramesByAnimationName The animation name "${name}" does not exist in this atlas. Available names: ${Object.keys(atlas.animations).map((n) => `* ${n}\n`).join("")}`); return [getNullAtlasFrame()]; } return atlas.animations[name]; } function getAtlasFramesByIndices(atlas, startI, endI) { if (startI < 0 || atlas.frames.length <= startI || endI < 0 || atlas.frames.length <= endI) { logError(`Cientos Atlas: getFramesByIndex – [${startI}, ${endI}] is out of bounds.`); return [getNullAtlasFrame()]; } const result = []; const sign = Math.sign(endI - startI); if (sign === 0) return [atlas.frames[startI]]; for (let i = startI; i !== endI + sign; i += sign) result.push(atlas.frames[i]); return result; } /** * @returns An object where all AtlasFrames with the same key are grouped in an ordered array by name in ascending value. * A key is defined as an alphanumeric string preceding a trailing numeric string. * E.g.: * "hero0Idle" has no key as it does not have trailing numeric string. * "heroIdle0" has the key "heroIdle". * @example ``` * groupFramesByKey([{name: hero, ...}, {name: heroJump3, ...}, {name: heroJump0, ...}, {name: heroIdle0, ...}, {name: heroIdle1, ...}]) returns * { * heroJump: [{name: heroJump0, ...}, {name: heroJump3, ...}], * heroIdle: [{name: heroIdle0, ...}, {name: heroIdle1, ...}] * } * ``` */ function groupAtlasFramesByKey(frames) { const result = {}; for (const frame of frames) if (getNumbersFromEnd(frame.name) !== null) { const key = stripUnderscoresNumbersFromEnd(frame.name); if (Object.prototype.hasOwnProperty.call(result, key)) result[key].push(frame); else result[key] = [frame]; } for (const entry of Object.values(result)) entry.sort((a, b) => a.name.localeCompare(b.name)); return result; } //#endregion //#region src/core/abstractions/AnimatedSprite/component.vue?vue&type=script&setup=true&lang.ts const _hoisted_1$57 = ["scale", "position"]; const _hoisted_2$27 = ["map", "alphaTest"]; const _hoisted_3$5 = ["scale", "position"]; const _hoisted_4$3 = [ "side", "map", "alphaTest", "depthWrite", "depthTest" ]; const TEXTURE_PX_TO_WORLD_UNITS = .01; var component_vue_vue_type_script_setup_true_lang_default$18 = /* @__PURE__ */ defineComponent({ __name: "component", props: { image: { type: String, required: true }, atlas: { type: [ String, Object, Array, Number ], required: true }, definitions: { type: Object, required: false }, fps: { type: Number, required: false, default: 30 }, loop: { type: Boolean, required: false, default: true }, animation: { type: [ String, Array, Number ], required: false, default: 0 }, paused: { type: Boolean, required: false, default: false }, reversed: { type: Boolean, required: false, default: false }, flipX: { type: Boolean, required: false, default: false }, resetOnEnd: { type: Boolean, required: false, default: false }, asSprite: { type: Boolean, required: false, default: true }, center: { type: null, required: false, default: () => [.5, .5] }, alphaTest: { type: Number, required: false, default: 0 }, depthTest: { type: Boolean, required: false, default: true }, depthWrite: { type: Boolean, required: false, default: true } }, emits: [ "frame", "end", "loop" ], async setup(__props, { expose: __expose, emit: __emit }) { let __temp, __restore; const props = __props; const emit = __emit; const { invalidate } = useTres(); watch(props, () => { invalidate(); }); const positionX = ref(0); const positionY = ref(0); const scaleX = ref(0); const scaleY = ref(0); const groupRef = shallowRef(); __expose({ instance: groupRef }); const [textureResult, atlas] = ([__temp, __restore] = withAsyncContext(() => getTextureAndAtlasAsync(props.image, props.atlas)), __temp = await __temp, __restore(), __temp); const texture$1 = Array.isArray(textureResult) ? textureResult[0] : textureResult; texture$1.matrixAutoUpdate = false; let animation = getAtlasFrames(atlas, props.animation, props.reversed); let centerX = .5; let centerY = .5; let cooldown = 1; let frame = getNullAtlasFrame(); let frameNameToEmit = null; let frameNum = 0; let frameHeldOnLoopEnd = false; let dirtyFlag = true; useLoop().onBeforeRender(({ delta }) => { if (!props.paused && !frameHeldOnLoopEnd) cooldown -= delta * props.fps; while (cooldown <= 0) { cooldown++; frameNum++; if (props.loop) { if (frameNum >= animation.length) emit("loop", animation[animation.length - 1].name); frameNum %= animation.length; } else if (frameNum >= animation.length) { frameHeldOnLoopEnd = true; frameNum = props.resetOnEnd ? 0 : animation.length - 1; emit("end", animation[animation.length - 1].name); } } if (animation[frameNum] !== frame) { frame = animation[frameNum]; frameNameToEmit = frame.name; render$1(); } if (dirtyFlag) { dirtyFlag = false; texture$1.offset.x = frame.offsetX + (props.flipX ? frame.repeatX : 0); texture$1.offset.y = frame.offsetY; texture$1.repeat.x = frame.repeatX * (props.flipX ? -1 : 1); texture$1.repeat.y = frame.repeatY; texture$1.updateMatrix(); scaleX.value = frame.width * TEXTURE_PX_TO_WORLD_UNITS; scaleY.value = frame.height * TEXTURE_PX_TO_WORLD_UNITS; positionX.value = (.5 - centerX) * frame.width * TEXTURE_PX_TO_WORLD_UNITS; positionY.value = (.5 - centerY) * frame.height * TEXTURE_PX_TO_WORLD_UNITS; } if (frameNameToEmit) { emit("frame", frameNameToEmit); frameNameToEmit = null; } }); function render$1() { dirtyFlag = true; } watch(() => props.animation, (newValue, oldValue) => { if (JSON.stringify(newValue) === JSON.stringify(oldValue)) return; animation = getAtlasFrames(atlas, props.animation, props.reversed); frameNum = 0; cooldown = 1; frameHeldOnLoopEnd = false; render$1(); }, { immediate: true }); watch(() => props.reversed, () => { frameNum = (animation.length - frameNum - 1) % animation.length; animation = getAtlasFrames(atlas, props.animation, props.reversed); if (frameHeldOnLoopEnd) frameNum = props.resetOnEnd ? 0 : animation.length - 1; render$1(); }); watch(() => props.paused, () => { frameHeldOnLoopEnd = false; }); watch(() => props.loop, () => { if (frameHeldOnLoopEnd && props.loop) frameHeldOnLoopEnd = false; }); watch(() => props.resetOnEnd, () => { if (frameHeldOnLoopEnd) { frameNum = props.resetOnEnd ? 0 : animation.length - 1; render$1(); } }); watch(() => props.flipX, render$1); watch(() => [props.center], () => { [centerX, centerY] = normalizeVectorFlexibleParam(props.center); render$1(); }, { immediate: true }); watch(() => [props.definitions], () => { setAtlasDefinitions(atlas, props.definitions); animation = getAtlasFrames(atlas, props.animation, props.reversed); cooldown = 1; frameNum = 0; render$1(); }, { immediate: true }); onUnmounted(() => { texture$1.dispose(); }); return (_ctx, _cache) => { return openBlock(), createElementBlock("TresGroup", { ref_key: "groupRef", ref: groupRef }, [props.asSprite ? (openBlock(), createElementBlock("TresSprite", { key: 0, scale: [ scaleX.value, scaleY.value, 1 ], position: [ positionX.value, positionY.value, 0 ] }, [createElementVNode("TresSpriteMaterial", { toneMapped: false, map: unref(texture$1), transparent: true, alphaTest: props.alphaTest }, null, 8, _hoisted_2$27)], 8, _hoisted_1$57)) : (openBlock(), createElementBlock("TresMesh", { key: 1, scale: [ scaleX.value, scaleY.value, 1 ], position: [ positionX.value, positionY.value, 0 ] }, [_cache[0] || (_cache[0] = createElementVNode("TresPlaneGeometry", { args: [1, 1] }, null, -1)), createElementVNode("TresMeshBasicMaterial", { toneMapped: false, side: unref(DoubleSide), map: unref(texture$1), transparent: true, alphaTest: props.alphaTest, depthWrite: props.depthWrite, depthTest: props.depthTest }, null, 8, _hoisted_4$3)], 8, _hoisted_3$5)), renderSlot(_ctx.$slots, "default")], 512); }; } }); //#endregion //#region src/core/abstractions/AnimatedSprite/component.vue var component_default$1 = component_vue_vue_type_script_setup_true_lang_default$18; //#endregion //#region src/core/abstractions/CubeCamera/useCubeCamera.ts function useCubeCamera(props) { let { resolution, renderer, scene, envMap, fog, near, far } = props; renderer = renderer ?? useTres().renderer; scene = scene ?? useTres().scene; const updateProps = () => { resolution = toValue(props.resolution) ?? 255; near = toValue(props.near) ?? .1; far = toValue(props.far) ?? 1e3; envMap = toValue(props.envMap) ?? void 0; fog = toValue(props.fog) ?? void 0; renderer = toValue(props.renderer) ?? renderer; scene = toValue(props.scene) ?? scene; }; watchEffect(updateProps); const fbo = computed(() => new WebGLCubeRenderTarget(toValue(resolution))); fbo.value.texture.type = HalfFloatType; tryOnScopeDispose(() => { fbo.value.dispose(); }); const camera = computed(() => new CubeCamera(toValue(near), toValue(far), toValue(fbo))); const update = () => { const s = toValue(scene); const originalFog = s.fog; const originalBackground = s.background; s.background = toValue(envMap) || originalBackground; s.fog = toValue(fog) || originalFog; camera.value.update(toValue(renderer), s); s.fog = originalFog; s.background = originalBackground; }; watchEffect(update); return { fbo, camera, update }; } //#endregion //#region src/core/abstractions/CubeCamera/component.vue?vue&type=script&setup=true&lang.ts const _hoisted_1$56 = ["object"]; var component_vue_vue_type_script_setup_true_lang_default$17 = /* @__PURE__ */ defineComponent({ __name: "component", props: { frames: { type: null, required: false, default: Infinity }, resolution: { type: null, required: false }, near: { type: null, required: false }, far: { type: null, required: false }, envMap: { type: null, required: false }, fog: { type: null, required: false }, renderer: { type: null, required: false }, scene: { type: null, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const groupRef = shallowRef(); const { fbo, camera, update } = useCubeCamera(props); let count = 0; useLoop().onBeforeRender(() => { if (groupRef.value && (props.frames === Infinity || count < toValue(props.frames))) { groupRef.value.visible = false; update(); groupRef.value.visible = true; if (groupRef.value) groupRef.value.traverse((obj) => { if ("material" in obj && typeof obj.material === "object" && obj.material && "envMap" in obj.material) obj.material.envMap = fbo.value.texture; }); count++; } }); __expose({ instance: groupRef, fbo, camera, update }); return (_ctx, _cache) => { return openBlock(), createElementBlock("TresGroup", { ref_key: "groupRef", ref: groupRef }, [createElementVNode("primitive", { object: unref(camera) }, null, 8, _hoisted_1$56), renderSlot(_ctx.$slots, "default")], 512); }; } }); //#endregion //#region src/core/abstractions/CubeCamera/component.vue var component_default$3 = component_vue_vue_type_script_setup_true_lang_default$17; //#endregion //#region src/core/abstractions/Billboard.vue?vue&type=script&setup=true&lang.ts var Billboard_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "Billboard", props: { autoUpdate: { type: Boolean, required: false, default: true }, lockX: { type: Boolean, required: false, default: false }, lockY: { type: Boolean, required: false, default: false }, lockZ: { type: Boolean, required: false, default: false } }, setup(__props, { expose: __expose }) { const props = __props; const outerRef = shallowRef(new Group()); const innerRef = shallowRef(new Group()); const q = new Quaternion(); const r = new Euler(); function update(camera) { if (!outerRef.value) return; if (!camera) { const { camera: ctxCamera } = useTresContext(); camera = ctxCamera.activeCamera.value; if (!camera) return; } innerRef.value.rotation.copy(r); outerRef.value.updateMatrix(); outerRef.value.updateWorldMatrix(false, false); outerRef.value.getWorldQuaternion(q); camera.getWorldQuaternion(innerRef.value.quaternion).premultiply(q.invert()); if (props.lockX) innerRef.value.rotation.x = r.x; if (props.lockY) innerRef.value.rotation.y = r.y; if (props.lockZ) innerRef.value.rotation.z = r.z; } useLoop().onBeforeRender(({ camera }) => { if (props.autoUpdate) update(camera.value); }); __expose({ instance: outerRef, update }); return (_ctx, _cache) => { return openBlock(), createElementBlock("TresGroup", { ref_key: "outerRef", ref: outerRef }, [createElementVNode("TresGroup", { ref_key: "innerRef", ref: innerRef }, [renderSlot(_ctx.$slots, "default")], 512)], 512); }; } }); //#endregion //#region src/core/abstractions/Billboard.vue var Billboard_default = Billboard_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/abstractions/GlobalAudio.ts const GlobalAudio = defineComponent({ name: "GlobalAudio", props: [ "src", "loop", "volume", "playbackRate", "playTrigger", "stopTrigger" ], async setup(props, { expose, emit }) { const { camera, renderer } = useTresContext(); const listener = new AudioListener(); camera.activeCamera.value?.add(listener); const sound = new Audio(listener); const audioLoader = new AudioLoader(); expose({ instance: sound }); onUnmounted(() => { if (sound) sound.disconnect(); }); watch(() => [props.playbackRate], () => sound.setPlaybackRate(props.playbackRate ?? 1), { immediate: true }); watch(() => [props.volume], () => sound.setVolume(props.volume ?? .5), { immediate: true }); watch(() => [props.loop], () => sound.setLoop(props.loop ?? false), { immediate: true }); watch(() => [props.src], async () => { const buffer = await audioLoader.loadAsync(props.src); sound.setBuffer(buffer); }, { immediate: true }); useEventListener(document.getElementById(props.playTrigger ?? "") || renderer.instance.domElement, "click", () => { if (sound.isPlaying) sound.pause(); else sound.play(); emit("isPlaying", sound.isPlaying); }); const btnStop = document.getElementById(props.stopTrigger ?? ""); if (btnStop) useEventListener(btnStop, "click", () => { sound.stop(); emit("isPlaying", sound.isPlaying); }); return null; } }); //#endregion //#region src/core/abstractions/GradientTexture.vue?vue&type=script&setup=true&lang.ts const _hoisted_1$55 = [ "color-space", "args", "attach" ]; var GradientTexture_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "GradientTexture", props: { stops: { type: Array, required: true }, colors: { type: Array, required: true }, attach: { type: String, required: false, default: "map" }, height: { type: Number, required: false, default: 1024 }, width: { type: Number, required: false, default: 16 }, type: { type: String, required: false, default: "linear" }, innerCircleRadius: { type: Number, required: false, default: 0 }, outerCircleRadius: { type: [String, Number], required: false, default: "auto" } }, setup(__props, { expose: __expose }) { const props = __props; const textureRef = shallowRef(); const canvas = document.createElement("canvas"); function update(canvas$1) { const context = canvas$1.getContext("2d"); canvas$1.width = props.width; canvas$1.height = props.height; let gradient; if (props.type === "linear") gradient = context.createLinearGradient(0, 0, 0, props.height); else { const canvasCenterX = canvas$1.width / 2; const canvasCenterY = canvas$1.height / 2; const radius = props.outerCircleRadius !== "auto" ? Math.abs(Number(props.outerCircleRadius)) : Math.sqrt(canvasCenterX ** 2 + canvasCenterY ** 2); gradient = context.createRadialGradient(canvasCenterX, canvasCenterY, Math.abs(props.innerCircleRadius), canvasCenterX, canvasCenterY, radius); } const tempColor = new THREE.Color(); let i = props.stops.length; while (i--) gradient.addColorStop(props.stops[i], tempColor.set(props.colors[i]).getStyle()); context.save(); context.fillStyle = gradient; context.fillRect(0, 0, props.width, props.height); context.restore(); if (textureRef.value) textureRef.value.needsUpdate = true; } const renderer = useTres().renderer; watch(() => [ props.colors, props.stops, props.height, props.width, props.type, props.innerCircleRadius, props.outerCircleRadius ], () => { update(canvas); }, { immediate: true }); if (isReactive(props.colors)) watch(props.colors, () => update(canvas)); if (isReactive(props.stops)) watch(props.stops, () => update(canvas)); __expose({ instance: textureRef }); return (_ctx, _cache) => { return openBlock(), createElementBlock("TresCanvasTexture", { ref_key: "textureRef", ref: textureRef, "color-space": unref(renderer).outputColorSpace, args: [unref(canvas)], attach: props.attach }, null, 8, _hoisted_1$55); }; } }); //#endregion //#region src/core/abstractions/GradientTexture.vue var GradientTexture_default = GradientTexture_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/utils/shaderMaterial.ts function shaderMaterial(uniforms, vertexShader, fragmentShader, onInit) { const material = class extends ShaderMaterial { key = ""; constructor(parameters = {}) { const entries = Object.entries(uniforms); super({ uniforms: entries.reduce((acc, [name, value]) => { const uniform = UniformsUtils.clone({ [name]: { value } }); return { ...acc, ...uniform }; }, {}), vertexShader, fragmentShader }); entries.forEach(([name]) => Object.defineProperty(this, name, { get: () => this.uniforms[name].value, set: (v) => this.uniforms[name].value = v })); Object.assign(this, parameters); if (onInit) onInit(this); } }; material.key = MathUtils.generateUUID(); return material; } //#endregion //#region src/core/abstractions/Image/ImageMaterialImpl.ts /** * NOTE: Source: * https://threejs.org/docs/?q=material#api/en/materials/Material.transparent */ const ImageMaterialImpl = /* @__PURE__ */ shaderMaterial({ color: /* @__PURE__ */ new Color("white"), scale: /* @__PURE__ */ new Vector2(1, 1), imageBounds: /* @__PURE__ */ new Vector2(1, 1), resolution: 1024, map: null, zoom: 1, radius: 0, grayscale: 0, opacity: 1 }, ` varying vec2 vUv; varying vec2 vPos; void main() { gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.); vUv = uv; vPos = position.xy; } `, ` // mostly from https://gist.github.com/statico/df64c5d167362ecf7b34fca0b1459a44 varying vec2 vUv; varying vec2 vPos; uniform vec2 scale; uniform vec2 imageBounds; uniform float resolution; uniform vec3 color; uniform sampler2D map; uniform float radius; uniform float zoom; uniform float grayscale; uniform float opacity; const vec3 luma = vec3(.299, 0.587, 0.114); vec4 toGrayscale(vec4 color, float intensity) { return vec4(mix(color.rgb, vec3(dot(color.rgb, luma)), intensity), color.a); } vec2 aspect(vec2 size) { return size / min(size.x, size.y); } const float PI = 3.14159265; // from https://iquilezles.org/articles/distfunctions float udRoundBox( vec2 p, vec2 b, float r ) { return length(max(abs(p)-b+r,0.0))-r; } void main() { vec2 s = aspect(scale); vec2 i = aspect(imageBounds); float rs = s.x / s.y; float ri = i.x / i.y; vec2 new = rs < ri ? vec2(i.x * s.y / i.y, s.y) : vec2(s.x, i.y * s.x / i.x); vec2 offset = (rs < ri ? vec2((new.x - s.x) / 2.0, 0.0) : vec2(0.0, (new.y - s.y) / 2.0)) / new; vec2 uv = vUv * s / new + offset; vec2 zUv = (uv - vec2(0.5, 0.5)) / zoom + vec2(0.5, 0.5); vec2 res = vec2(scale * resolution); vec2 halfRes = 0.5 * res; float b = udRoundBox(vUv.xy * res - halfRes, halfRes, resolution * radius); vec3 a = mix(vec3(1.0,0.0,0.0), vec3(0.0,0.0,0.0), smoothstep(0.0, 1.0, b)); gl_FragColor = toGrayscale(texture2D(map, zUv) * vec4(color, opacity * a), grayscale); #include <tonemapping_fragment> #include <colorspace_fragment> } `); //#endregion //#region src/core/abstractions/Image/ImageMaterial.vue?vue&type=script&setup=true&lang.ts var ImageMaterial_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "ImageMaterial", setup(__props, { expose: __expose }) { extend({ ImageMaterial: ImageMaterialImpl }); const materialRef = shallowRef(); __expose({ instance: materialRef }); return (_ctx, _cache) => { return openBlock(), createElementBlock("TresImageMaterial", { ref_key: "materialRef", ref: materialRef }, null, 512); }; } }); //#endregion //#region src/core/abstractions/Image/ImageMaterial.vue var ImageMaterial_default = ImageMaterial_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/loaders/useTexture/index.ts function useTexture(path) { return useLoader(TextureLoader, path, { initialValue: new Texture() }); } //#endregion //#region src/core/abstractions/Image/component.vue?vue&type=script&setup=true&lang.ts const _hoisted_1$54 = ["scale"]; const _hoisted_2$26 = ["args"]; var component_vue_vue_type_script_setup_true_lang_default$16 = /* @__PURE__ */ defineComponent({ __name: "component", props: { segments: { type: Number, required: false, default: 1 }, scale: { type: [Number, Array], required: false, default: 1 }, color: { type: null, required: false, default: () => new Color("white") }, zoom: { type: Number, required: false, default: 1 }, radius: { type: Number, required: false, default: 0 }, grayscale: { type: Number, required: false, default: 0 }, toneMapped: { type: Boolean, required: false, default: true }, transparent: { type: Boolean, required: false, default: false }, opacity: { type: Number, required: false, default: 1 }, side: { type: null, required: false, default: FrontSide }, texture: { type: null, required: false }, url: { type: null, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const imageRef = shallowRef(); const texture$1 = shallowRef(props.texture ?? null); const { sizes: size, renderer } = useTres(); const planeBounds = computed(() => Array.isArray(props.scale) ? [props.scale[0], props.scale[1]] : [props.scale, props.scale]); const imageBounds = computed(() => [texture$1.value?.image?.width ?? 0, texture$1.value?.image?.height ?? 0]); const resolution = computed(() => Math.max(size.width.value, size.height.value)); const { state, isLoading } = useTexture(props.url); watchEffect(() => { if (props.texture) texture$1.value = props.texture; if (!isLoading.value) { texture$1.value = state.value; texture$1.value.colorSpace = renderer.outputColorSpace; } }); const scale = computed(() => Array.isArray(props.scale) ? [...props.scale, 1] : props.scale); __expose({ instance: imageRef }); return (_ctx, _cache) => { return openBlock(), createElementBlock("TresMesh", { ref_key: "imageRef", ref: imageRef, scale: scale.value }, [renderSlot(_ctx.$slots, "default", {}, () => [createElementVNode("TresPlaneGeometry", { args: [ 1, 1, props.segments, props.segments ] }, null, 8, _hoisted_2$26)]), createVNode(ImageMaterial_default, { color: props.color, map: texture$1.value, zoom: props.zoom, grayscale: props.grayscale, opacity: props.opacity, scale: planeBounds.value, imageBounds: imageBounds.value, resolution: resolution.value, radius: __props.radius, toneMapped: __props.toneMapped, transparent: __props.transparent, side: __props.side }, null, 8, [ "color", "map", "zoom", "grayscale", "opacity", "scale", "imageBounds", "resolution", "radius", "toneMapped", "transparent", "side" ])], 8, _hoisted_1$54); }; } }); //#endregion //#region src/core/abstractions/Image/component.vue var component_default$9 = component_vue_vue_type_script_setup_true_lang_default$16; //#endregion //#region src/core/abstractions/Lensflare/LensflareImpl.ts var Lensflare = class Lensflare extends Mesh { static Geometry; isLensflare = true; type = "Lensflare"; addElement(_) {} dispose() {} constructor() { super(Lensflare.Geometry, new MeshBasicMaterial({ opacity: 0, transparent: true })); this.frustumCulled = false; this.renderOrder = Infinity; const positionScreen = new Vector3(); const positionView = new Vector3(); const tempMap = new FramebufferTexture(16, 16); const occlusionMap = new FramebufferTexture(16, 16); let currentType = UnsignedByteType; const geometry = Lensflare.Geometry; const material1a = new RawShaderMaterial({ uniforms: { scale: { value: null }, screenPosition: { value: null } }, vertexShader: ` precision highp float; uniform vec3 screenPosition; uniform vec2 scale; attribute vec3 position; void main() { gl_Position = vec4( position.xy * scale + screenPosition.xy, screenPosition.z, 1.0 ); }`, fragmentShader: ` precision highp float; void main() { gl_FragColor = vec4( 1.0, 0.0, 1.0, 1.0 ); }`, depthTest: true, depthWrite: false, transparent: false }); const material1b = new RawShaderMaterial({ uniforms: { map: { value: tempMap }, scale: { value: null }, screenPosition: { value: null } }, vertexShader: ` precision highp float; uniform vec3 screenPosition; uniform vec2 scale; attribute vec3 position; attribute vec2 uv; varying vec2 vUV; void main() { vUV = uv; gl_Position = vec4( position.xy * scale + screenPosition.xy, screenPosition.z, 1.0 ); }`, fragmentShader: ` precision highp float; uniform sampler2D map; varying vec2 vUV; void main() { gl_FragColor = texture2D( map, vUV ); }`, depthTest: false, depthWrite: false, transparent: false }); const mesh1 = new Mesh(geometry, material1a); const elements = []; const shader = LensflareElement.Shader; const material2 = new RawShaderMaterial({ name: shader.name, uniforms: { map: { value: null }, occlusionMap: { value: occlusionMap }, color: { value: new Color(16777215) }, scale: { value: new Vector2() }, screenPosition: { value: new Vector3() } }, vertexShader: shader.vertexShader, fragmentShader: shader.fragmentShader, blending: AdditiveBlending, transparent: true, depthWrite: false }); const mesh2 = new Mesh(geometry, material2); this.addElement = function(element) { elements.push(element); }; const scale = new Vector2(); const screenPositionPixels = new Vector2(); const validArea = new Box2(); const viewport = new Vector4(); this.onBeforeRender = function(renderer, _scene, camera) { renderer.getCurrentViewport(viewport); const renderTarget = renderer.getRenderTarget(); const type = renderTarget !== null ? renderTarget.texture.type : UnsignedByteType; if (currentType !== type) { tempMap.dispose(); occlusionMap.dispose(); tempMap.type = occlusionMap.type = type; currentType = type; } const invAspect = viewport.w / viewport.z; const halfViewportWidth = viewport.z / 2; const halfViewportHeight = viewport.w / 2; let size = 16 / viewport.w; scale.set(size * invAspect, size); validArea.min.set(viewport.x, viewport.y); validArea.max.set(viewport.x + (viewport.z - 16), viewport.y + (viewport.w - 16)); positionView.setFromMatrixPosition(this.matrixWorld); positionView.applyMatrix4(camera.matrixWorldInverse); if (positionView.z > 0) return; positionScreen.copy(positionView).applyMatrix4(camera.projectionMatrix); screenPositionPixels.x = viewport.x + positionScreen.x * halfViewportWidth + halfViewportWidth - 8; screenPositionPixels.y = viewport.y + positionScreen.y * halfViewportHeight + halfViewportHeight - 8; if (validArea.containsPoint(screenPositionPixels)) { renderer.copyFramebufferToTexture(tempMap, screenPositionPixels); let uniforms = material1a.uniforms; uniforms.scale.value = scale; uniforms.screenPosition.value = positionScreen; renderer.renderBufferDirect(camera, null, geometry, material1a, mesh1, null); renderer.copyFramebufferToTexture(occlusionMap, screenPositionPixels); uniforms = material1b.uniforms; uniforms.scale.value = scale; uniforms.screenPosition.value = positionScreen; renderer.renderBufferDirect(camera, null, geometry, material1b, mesh1, null); const vecX = -positionScreen.x * 2; const vecY = -positionScreen.y * 2; for (let i = 0, l = elements.length; i < l; i++) { const element = elements[i]; const uniforms$1 = material2.uniforms; uniforms$1.color.value.copy(element.color); uniforms$1.map.value = element.texture; uniforms$1.screenPosition.value.x = positionScreen.x + vecX * element.distance; uniforms$1.screenPosition.value.y = positionScreen.y + vecY * element.distance; size = element.size / viewport.w; const invAspect$1 = viewport.w / viewport.z; uniforms$1.scale.value.set(size * invAspect$1, size); material2.uniformsNeedUpdate = true; renderer.renderBufferDirect(camera, null, geometry, material2, mesh2, null); } } }; this.dispose = function() { material1a.dispose(); material1b.dispose(); material2.dispose(); tempMap.dispose(); occlusionMap.dispose(); for (let i = 0, l = elements.length; i < l; i++) elements[i].texture.dispose(); }; } }; var LensflareElement = class { texture; size; distance; color; static Shader; constructor(texture$1, size = 1, distance = 0, color = new Color(16777215)) { this.texture = texture$1; this.size = size; this.distance = distance; this.color = color; } }; LensflareElement.Shader = { name: "LensflareElementShader", uniforms: { map: { value: null }, occlusionMap: { value: null }, color: { value: null }, scale: { value: null }, screenPosition: { value: null } }, vertexShader: ` precision highp float; uniform vec3 screenPosition; uniform vec2 scale; uniform sampler2D occlusionMap; attribute vec3 position; attribute vec2 uv; varying vec2 vUV; varying float vVisibility; void main() { vUV = uv; vec2 pos = position.xy; vec4 visibility = texture2D( occlusionMap, vec2( 0.1, 0.1 ) ); visibility += texture2D( occlusionMap, vec2( 0.5, 0.1 ) ); visibility += texture2D( occlusionMap, vec2( 0.9, 0.1 ) ); visibility += texture2D( occlusionMap, vec2( 0.9, 0.5 ) ); visibility += texture2D( occlusionMap, vec2( 0.9, 0.9 ) ); visibility += texture2D( occlusionMap, vec2( 0.5, 0.9 ) ); visibility += texture2D( occlusionMap, vec2( 0.1, 0.9 ) ); visibility += texture2D( occlusionMap, vec2( 0.1, 0.5 ) ); visibility += texture2D( occlusionMap, vec2( 0.5, 0.5 ) ); vVisibility = visibility.r / 9.0; vVisibility *= 1.0 - visibility.g / 9.0; vVisibility *= visibility.b / 9.0; gl_Position = vec4( ( pos * scale + screenPosition.xy ).xy, screenPosition.z, 1.0 ); }`, fragmentShader: ` precision highp float; uniform sampler2D map; uniform vec3 color; varying vec2 vUV; varying float vVisibility; void main() { vec4 texture = texture2D( map, vUV ); texture.a *= vVisibility; gl_FragColor = texture; gl_FragColor.rgb *= color; }` }; Lensflare.Geometry = (function() { const geometry = new BufferGeometry(); const interleavedBuffer = new InterleavedBuffer(new Float32Array([ -1, -1, 0, 0, 0, 1, -1, 0, 1, 0, 1, 1, 0, 1, 1, -1, 1, 0, 0, 1 ]), 5); geometry.setIndex([ 0, 1, 2, 0, 2, 3 ]); geometry.setAttribute("position", new InterleavedBufferAttribute(interleavedBuffer, 3, 0, false)); geometry.setAttribute("uv", new InterleavedBufferAttribute(interleavedBuffer, 2, 3, false)); return geometry; })(); //#endregion //#region src/utils/easing.ts function linear(x) { return x; } function easeInCubic(x) { return x * x * x; } function easeInOutCubic(x) { return x < .5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2; } function easeInQuart(x) { return x * x * x * x; } function easeOutBounce(x) { const n1 = 7.5625; const d1 = 2.75; if (x < 1 / d1) return n1 * x * x; else if (x < 2 / d1) return n1 * (x -= 1.5 / d1) * x + .75; else if (x < 2.5 / d1) return n1 * (x -= 2.25 / d1) * x + .9375; else return n1 * (x -= 2.625 / d1) * x + .984375; } //#endregion //#region src/core/abstractions/Lensflare/constants.ts const TEXTURE_PATH = "https://raw.githubusercontent.com/Tresjs/assets/93976c7d63ac83d4a254a41a10b2362bc17e90c9/textures/lensflare/"; const circle = `${TEXTURE_PATH}circle.png`; const circleBlur = `${TEXTURE_PATH}circleBlur.png`; const circleRainbow = `${TEXTURE_PATH}circleRainbow.png`; const line = `${TEXTURE_PATH}line.png`; const poly6 = `${TEXTURE_PATH}poly6.png`; const polyStroke6 = `${TEXTURE_PATH}polyStroke6.png`; const rays = `${TEXTURE_PATH}rays.png`; const ring = `${TEXTURE_PATH}ring.png`; const starThin6 = `${TEXTURE_PATH}starThin6.png`; const oversize = { texture: [line, ring], color: ["white"], distance: [0, 0], size: [750, 1024], length: [0, 2] }; const bodyRequired0 = { texture: [circleBlur], color: ["white"], distance: [0, 0], size: [180, 512], length: [1, 1] }; const bodyRequired1 = { texture: [rays], color: ["white"], distance: [0, 0], size: [180, 512], length: [1, 1] }; const bodyOptional = { texture: [ circle, circleRainbow, ring, starThin6 ], color: ["white"], distance: [0, 0], size: [180, 512], length: [2, 3] }; const [darkPurple, darkBlue] = [3679071, 132442]; const front = { texture: [ circleBlur, circle, ring, poly6, polyStroke6 ], color: [ "dimgray", "gray", "darkgray", darkPurple, darkBlue ], distance: [.5, 2.5], size: [20, 180], length: [5, 21] }; const back = { texture: [ circleBlur, circle, ring, poly6, polyStroke6 ], color: [ "dimgray", "gray", "darkgray", darkPurple, darkBlue ], distance: [-.6, -.1], size: [180, 360], length: [0, 5] }; const defaultSeedProps = [ oversize, bodyRequired0, bodyRequired1, bodyOptional, front, back ]; const defaultLensflareElementProps = { color: "white", distance: 0, size: 512, texture: circleBlur }; //#endregion //#region src/core/abstractions/Lensflare/RandUtils.ts const clamp = MathUtils.clamp; /** * Seedable pseudorandom number tools */ var RandUtils = class { _getNext; _getGenerator; /** * Create a new seeded pseudorandom number generator. * @param [seed] - the seed for the generator * @param [getSeededRandomGenerator] - a function that returns a pseudorandom number generator * @constructor */ constructor(seed = 0, getSeededRandomGenerator) { this._getGenerator = getSeededRandomGenerator ?? this.getMulberry32; this._getNext = this._getGenerator(seed); } /** * Reseed the pseudorandom number generator */ seed(s) { this._getNext = this._getGenerator(s); } /** * Return the next pseudorandom number in the interval [0, 1] */ rand() { return this._getNext(); } /** * Random float from <low, high> interval * @param low - Low value of the interval * @param high - High value of the interval */ float(low, high) { return low + this._getNext() * (high - low); } /** * Random float from <-range/2, range/2> interval * @param range - Interval range */ floatSpread(range) { return this.float(-.5 * range, .5 * range); } /** * Random integer from <low, high> interval * @param low Low value of the interval * @param high High value of the interval */ int(low, high) { return low + Math.floor(this._getNext() * (high - low + 1)); } /** * Choose an element from an array. * @param array The array to choose from * @returns An element from the array or null if the array is empty */ choice(array) { if (!array.length) return null; return array[Math.floor(this._getNext() * array.length)]; } /** * Choose an element from an array or return defaultValue if array is empty. * @para