Newer
Older
import { objectTypeForOriginAndContentType } from "../object-types";
Brian Peiris
committed
import mediaHighlightFrag from "./media-highlight-frag.glsl";
const mediaAPIEndpoint = getReticulumFetchUrl("/api/v1/media");
const commonKnownContentTypes = {
gltf: "model/gltf",
glb: "model/gltf-binary",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
pdf: "application/pdf",
mp4: "video/mp4",
mp3: "audio/mpeg"
};
// thanks to https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
function b64EncodeUnicode(str) {
// first we use encodeURIComponent to get percent-encoded UTF-8, then we convert the percent-encodings
// into raw bytes which can be fed into btoa.
const CHAR_RE = /%([0-9A-F]{2})/g;
return btoa(encodeURIComponent(str).replace(CHAR_RE, (_, p1) => String.fromCharCode("0x" + p1)));
}
export const proxiedUrlFor = (url, index) => {
// farspark doesn't know how to read '=' base64 padding characters
const base64Url = b64EncodeUnicode(url).replace(/=+$/g, "");
// translate base64 + to - and / to _ for URL safety
const encodedUrl = base64Url.replace(/\+/g, "-").replace(/\//g, "_");
const method = index != null ? "extract" : "raw";
return `https://${process.env.FARSPARK_SERVER}/0/${method}/0/0/0/${index || 0}/${encodedUrl}`;
};
const resolveUrlCache = new Map();
export const resolveUrl = async (url, index) => {
const cacheKey = `${url}|${index}`;
if (resolveUrlCache.has(cacheKey)) return resolveUrlCache.get(cacheKey);
const resolved = await fetch(mediaAPIEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ media: { url, index } })
}).then(r => r.json());
export const guessContentType = url => {
const extension = new URL(url).pathname.split(".").pop();
return commonKnownContentTypes[extension];
};
export const upload = file => {
const formData = new FormData();
formData.append("media", file);
return fetch(mediaAPIEndpoint, {
method: "POST",
body: formData
}).then(r => r.json());
};
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side/32490603#32490603
function getOrientation(file, callback) {
const reader = new FileReader();
reader.onload = function(e) {
const view = new DataView(e.target.result);
if (view.getUint16(0, false) != 0xffd8) {
return callback(-2);
}
const length = view.byteLength;
let offset = 2;
while (offset < length) {
if (view.getUint16(offset + 2, false) <= 8) return callback(-1);
const marker = view.getUint16(offset, false);
offset += 2;
if (marker == 0xffe1) {
if (view.getUint32((offset += 2), false) != 0x45786966) {
return callback(-1);
}
const little = view.getUint16((offset += 6), false) == 0x4949;
offset += view.getUint32(offset + 4, little);
const tags = view.getUint16(offset, little);
offset += 2;
for (let i = 0; i < tags; i++) {
if (view.getUint16(offset + i * 12, little) == 0x0112) {
return callback(view.getUint16(offset + i * 12 + 8, little));
}
}
} else if ((marker & 0xff00) != 0xff00) {
break;
} else {
offset += view.getUint16(offset, false);
}
}
return callback(-1);
};
reader.readAsArrayBuffer(file);
}
let interactableId = 0;
export const addMedia = (src, template, contentOrigin, resolve = false, resize = false) => {
const scene = AFRAME.scenes[0];
entity.id = "interactable-media-" + interactableId++;
entity.setAttribute("media-loader", { resize, resolve, src: typeof src === "string" ? src : "" });
const fireLoadingTimeout = setTimeout(() => {
scene.emit("media-loading", { src: src });
}, 100);
["model-loaded", "video-loaded", "image-loaded"].forEach(eventName => {
entity.addEventListener(eventName, () => {
clearTimeout(fireLoadingTimeout);
entity.object3D.scale.setScalar(0.5);
entity.setAttribute("animation__spawn-start", {
property: "scale",
dur: 300,
from: { x: 0.5, y: 0.5, z: 0.5 },
to: { x: 1.0, y: 1.0, z: 1.0 },
easing: "easeOutElastic"
});
}
scene.emit("media-loaded", { src: src });
});
});
const orientation = new Promise(function(resolve) {
if (src instanceof File) {
getOrientation(src, x => {
resolve(x);
});
} else {
resolve(1);
}
});
if (src instanceof File) {
upload(src)
.then(response => {
const srcUrl = new URL(response.raw);
srcUrl.searchParams.set("token", response.meta.access_token);
entity.setAttribute("media-loader", { resolve: false, src: srcUrl.href });
})
.catch(() => {
entity.setAttribute("media-loader", { src: "error" });
});
if (contentOrigin) {
entity.addEventListener("media_resolved", ({ detail }) => {
const objectType = objectTypeForOriginAndContentType(contentOrigin, detail.contentType);
scene.emit("object_spawned", { objectType });
});
}
return { entity, orientation };
Brian Peiris
committed
export function injectCustomShaderChunks(obj) {
const vertexRegex = /\bskinning_vertex\b/;
Brian Peiris
committed
const fragRegex = /\bgl_FragColor\b/;
const validMaterials = ["MeshStandardMaterial", "MeshBasicMaterial", "MobileStandardMaterial"];
Brian Peiris
committed
Brian Peiris
committed
obj.traverse(object => {
if (!object.material || !validMaterials.includes(object.material.type)) {
Brian Peiris
committed
return;
}
// HACK, this routine inadvertently leaves the A-Frame shaders wired to the old, dark
// material, so maps cannot be updated at runtime. This breaks UI elements who have
// hover/toggle state, so for now just skip these while we figure out a more correct
// solution.
if (object.el.classList.contains("ui")) return;
Greg Fodor
committed
if (object.el.getAttribute("text-button")) return;
Brian Peiris
committed
object.material = object.material.clone();
object.material.onBeforeCompile = shader => {
if (!vertexRegex.test(shader.vertexShader)) return;
shader.uniforms.hubs_EnableSweepingEffect = { value: false };
shader.uniforms.hubs_SweepParams = { value: [0, 0] };
shader.uniforms.hubs_InteractorOnePos = { value: [0, 0, 0] };
shader.uniforms.hubs_InteractorTwoPos = { value: [0, 0, 0] };
Brian Peiris
committed
shader.uniforms.hubs_HighlightInteractorOne = { value: false };
shader.uniforms.hubs_HighlightInteractorTwo = { value: false };
shader.uniforms.hubs_Time = { value: 0 };
const vchunk = `
if (hubs_HighlightInteractorOne || hubs_HighlightInteractorTwo) {
vec4 wt = modelMatrix * vec4(transformed, 1);
// Used in the fragment shader below.
hubs_WorldPosition = wt.xyz;
}
`;
const vlines = shader.vertexShader.split("\n");
const vindex = vlines.findIndex(line => vertexRegex.test(line));
vlines.splice(vindex + 1, 0, vchunk);
vlines.unshift("varying vec3 hubs_WorldPosition;");
vlines.unshift("uniform bool hubs_HighlightInteractorOne;");
vlines.unshift("uniform bool hubs_HighlightInteractorTwo;");
shader.vertexShader = vlines.join("\n");
const flines = shader.fragmentShader.split("\n");
const findex = flines.findIndex(line => fragRegex.test(line));
flines.splice(findex + 1, 0, mediaHighlightFrag);
flines.unshift("varying vec3 hubs_WorldPosition;");
flines.unshift("uniform bool hubs_EnableSweepingEffect;");
flines.unshift("uniform vec2 hubs_SweepParams;");
Brian Peiris
committed
flines.unshift("uniform bool hubs_HighlightInteractorOne;");
flines.unshift("uniform vec3 hubs_InteractorOnePos;");
Brian Peiris
committed
flines.unshift("uniform bool hubs_HighlightInteractorTwo;");
flines.unshift("uniform vec3 hubs_InteractorTwoPos;");
Brian Peiris
committed
flines.unshift("uniform float hubs_Time;");
shader.fragmentShader = flines.join("\n");
shaderUniforms.set(object.material.uuid, shader.uniforms);
Brian Peiris
committed
};
object.material.needsUpdate = true;
});
return shaderUniforms;
}