UNPKG

@pmndrs/uikit

Version:

Build performant 3D user interfaces with Three.js and yoga.

398 lines (364 loc) 16.9 kB
import { Color, FrontSide, MeshDepthMaterial, MeshDistanceMaterial, RGBADepthPacking, } from 'three'; import { setBorderRadius } from './utils.js'; import { computed } from '@preact/signals-core'; import { toAbsoluteNumber } from '../text/utils.js'; const defaultDefaults = { backgroundColor: 'transparent', borderColor: 'transparent', borderBottomLeftRadius: 0, borderTopLeftRadius: 0, borderBottomRightRadius: 0, borderTopRightRadius: 0, borderBend: 0, }; const defaultOpacity = 1; let defaultPanelMaterialConfig; export function getDefaultPanelMaterialConfig() { if (defaultPanelMaterialConfig == null) { const defaultPanelMaterialKeys = {}; for (const key in defaultDefaults) { defaultPanelMaterialKeys[key] = key; } defaultPanelMaterialConfig = createPanelMaterialConfig(defaultPanelMaterialKeys); } return defaultPanelMaterialConfig; } const colorArrayHelper = [0, 0, 0, 0]; export function createPanelMaterialConfig(keys, providedDefaults) { const defaults = { ...defaultDefaults, ...providedDefaults }; const setters = {}; for (const key in keys) { const fn = materialSetters[key]; const defaultValue = defaults[key]; setters[keys[key]] = (data, offset, value, size, opacity, onUpdate) => fn(data, offset, (value ?? defaultValue), size, opacity, onUpdate); } const defaultData = new Float32Array(16); //filled with 0s by default writeColor(defaultData, 4, defaults.backgroundColor, defaultOpacity, undefined); writeColor(defaultData, 9, defaults.borderColor, defaultOpacity, undefined); defaultData[13] = defaults.borderBend; return { hasProperty: (key) => key in setters, defaultData, setters, computedIsVisibile: (properties, borderInset, size, isVisible) => { return computed(() => { if (borderInset.value == null || size.value == null) { return false; } const backgroundColor = keys.backgroundColor == null ? defaults.backgroundColor : (properties.value[keys.backgroundColor] ?? defaults.backgroundColor); const borderColor = keys.borderColor == null ? defaults.borderColor : (properties.value[keys.borderColor] ?? defaults.borderColor); const opacity = toAbsoluteNumber(properties.value.opacity ?? defaultOpacity, () => 1); writeColor(colorArrayHelper, 0, backgroundColor ?? defaults.backgroundColor, opacity); const [width, height] = size.value; const backgroundVisible = width > 0 && height > 0 && colorArrayHelper[3] > 0; writeColor(colorArrayHelper, 0, borderColor ?? defaults.borderColor, opacity); const borderVisible = borderInset.value.some((s) => s > 0) && colorArrayHelper[3] > 0; if (!backgroundVisible && !borderVisible) { return false; } return isVisible.value; }); }, }; } const materialSetters = { //0-3 = borderSizes //4-7 = background color backgroundColor: (d, o, p, _, op, u) => writeColor(d, o + 4, p, toAbsoluteNumber(op.value, () => 1), u), //8 = border radiuses borderBottomLeftRadius: (d, o, p, { value: s }, _, u) => { s != null && writeBorderRadius(d, o + 8, 0, p, s[1], u); }, borderBottomRightRadius: (d, o, p, { value: s }, _, u) => s != null && writeBorderRadius(d, o + 8, 1, p, s[1], u), borderTopRightRadius: (d, o, p, { value: s }, _, u) => s != null && writeBorderRadius(d, o + 8, 2, p, s[1], u), borderTopLeftRadius: (d, o, p, { value: s }, _, u) => s != null && writeBorderRadius(d, o + 8, 3, p, s[1], u), //9 - 12 = border color borderColor: (d, o, p, _, op, u) => writeColor(d, o + 9, p, toAbsoluteNumber(op.value, () => 1), u), //13 borderBend: (d, o, p, _, op, u) => writeComponent(d, o + 13, toAbsoluteNumber(p, () => 1), u), //14 = width //15 = height }; function writeBorderRadius(data, offset, indexInFloat, value, height, onUpdate) { setBorderRadius(data, offset, indexInFloat, Number(value), height); onUpdate?.(offset, 1); } function writeComponent(data, offset, value, onUpdate) { data[offset] = value; onUpdate?.(offset, 1); } const colorHelper = new Color(); const rgbaRegex = /rgba\((\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\)/; export function writeColor(target, offset, color, opacity, onUpdate) { let match; if (Array.isArray(color)) { for (let i = 0; i < color.length; i++) { target[i + offset] = color[i]; } target[offset + 3] = (color.length === 3 ? 1 : target[offset + 3]) * opacity; } else if (color === 'transparent') { target.fill(0, offset, offset + 4); } else if (typeof color === 'string' && (match = color.match(rgbaRegex)) != null) { for (let i = 0; i < 3; i++) { target[i + offset] = parseFloat(match[i + 1]) / 255; } target[3 + offset] = parseFloat(match[4]) * opacity; } else { colorHelper.set(color).toArray(target, offset); target[offset + 3] = opacity; } onUpdate?.(offset, 4); } export function createPanelMaterial(MaterialClass, info) { const material = new MaterialClass(); if (material.defines == null) { material.defines = {}; } material.side = FrontSide; material.clipShadows = true; material.transparent = true; material.toneMapped = false; material.shadowSide = FrontSide; material.defines.USE_UV = ''; material.defines.USE_TANGENT = ''; const superOnBeforeCompile = material.onBeforeCompile; material.onBeforeCompile = (parameters, renderer) => { superOnBeforeCompile.call(material, parameters, renderer); if (info.type === 'normal') { parameters.uniforms.data = { value: info.data }; } compilePanelMaterial(parameters, info.type === 'instanced'); }; return material; } export class PanelDistanceMaterial extends MeshDistanceMaterial { info; constructor(info) { super(); this.info = info; if (this.defines == null) { this.defines = {}; } this.defines.USE_UV = ''; this.clipShadows = true; } onBeforeCompile(parameters, renderer) { super.onBeforeCompile(parameters, renderer); if (this.info.type === 'normal') { parameters.uniforms.data = { value: this.info.data }; } compilePanelDepthMaterial(parameters, this.info.type === 'instanced'); } } export class PanelDepthMaterial extends MeshDepthMaterial { info; constructor(info) { super({ depthPacking: RGBADepthPacking }); this.info = info; if (this.defines == null) { this.defines = {}; } this.defines.USE_UV = ''; this.clipShadows = true; } onBeforeCompile(parameters, renderer) { super.onBeforeCompile(parameters, renderer); if (this.info.type === 'normal') { parameters.uniforms.data = { value: this.info.data }; } compilePanelDepthMaterial(parameters, this.info.type === 'instanced'); } } export const instancedPanelDepthMaterial = new PanelDepthMaterial({ type: 'instanced' }); export const instancedPanelDistanceMaterial = new PanelDistanceMaterial({ type: 'instanced' }); function compilePanelDepthMaterial(parameters, instanced) { compilePanelClippingMaterial(parameters, instanced); parameters.fragmentShader = parameters.fragmentShader.replace('#include <clipping_planes_fragment>', `#include <clipping_planes_fragment> ${getFragmentOpacityCode(instanced, undefined)} `); } function compilePanelClippingMaterial(parameters, instanced) { parameters.vertexShader = parameters.vertexShader.replace('#include <common>', ` #include <common> out vec4 borderRadius; ${instanced ? '' : 'uniform highp mat4 data;'}`); parameters.vertexShader = parameters.vertexShader.replace('#include <uv_vertex>', ` #include <uv_vertex> highp int packedBorderRadius = int(data[2].x); borderRadius = vec4( float(packedBorderRadius / 125000 % 50), float(packedBorderRadius / 2500 % 50), float(packedBorderRadius / 50 % 50), float(packedBorderRadius % 50) ) * 0.01;`); if (instanced) { parameters.vertexShader = parameters.vertexShader.replace('#include <common>', ` #include <common> attribute highp mat4 aData; attribute mat4 aClipping; out mat4 data; out mat4 clipping; out vec3 localPosition;`); parameters.vertexShader = parameters.vertexShader.replace('#include <uv_vertex>', ` #include <uv_vertex> data = aData; clipping = aClipping; localPosition = (instanceMatrix * vec4(position, 1.0)).xyz;`); } parameters.fragmentShader = getFragmentShaderPrefix(instanced) + parameters.fragmentShader; parameters.fragmentShader = parameters.fragmentShader.replace('#include <clipping_planes_fragment>', getClippingPlanesFragment(instanced)); } function getFragmentShaderPrefix(instanced) { return `${instanced ? 'in' : 'uniform'} highp mat4 data; in vec4 borderRadius; ${instanced ? ` in vec3 localPosition; in mat4 clipping;` : ''} float min4(vec4 v) { vec2 tmp = min(v.xy, v.zw); return min(tmp.x, tmp.y); } float max4(vec4 v) { vec2 tmp = max(v.xy, v.zw); return max(tmp.x, tmp.y); } vec2 radiusDistance(float radius, vec2 outside, vec2 border, vec2 borderSize) { vec2 outerRadius = vec2(radius); vec2 innerRadius = outerRadius - borderSize; vec2 radiusWeightUnnorm = abs(innerRadius - border); float sum = radiusWeightUnnorm.x + radiusWeightUnnorm.y; vec2 radiusWeight = sum > 0.0 ? radiusWeightUnnorm / sum : vec2(0.5); return vec2( radius - distance(outside, outerRadius), dot(radiusWeight, innerRadius) - distance(border, innerRadius) ); } vec2 calculateCornerIntersection(float cornerRadius, vec2 borderSizes, float aspectRatio) { float tmp1 = cornerRadius - borderSizes.y; vec2 xIntersection = vec2(tmp1, tmp1 / aspectRatio); float tmp2 = cornerRadius - borderSizes.x; vec2 yIntersection = vec2(tmp2 * aspectRatio, tmp2); return min(xIntersection, yIntersection); } `; } function getClippingPlanesFragment(instanced) { const instancedClipping = instanced ? ` vec4 plane; float distanceToPlane, planeDistanceGradient; float clipOpacity = 1.0; for(int i = 0; i < 4; i++) { plane = clipping[i]; distanceToPlane = dot(localPosition, plane.xyz) + plane.w; planeDistanceGradient = fwidth(distanceToPlane) * 0.5; clipOpacity *= smoothstep(-planeDistanceGradient, planeDistanceGradient, distanceToPlane); if (clipOpacity < 0.01) discard; }` : ''; return ` ${instancedClipping} vec4 absoluteBorderSize = data[0]; vec3 backgroundColor = data[1].xyz; float backgroundOpacity = data[1].w; vec3 borderColor = data[2].yzw; float borderOpacity = data[3].x; float borderBend = data[3].y; vec2 dimensions = data[3].zw; float aspectRatio = dimensions.x / dimensions.y; vec4 borderSize = absoluteBorderSize / dimensions.yyyy; vec2 uvFlipped = vec2(vUv.x, 1.0 - vUv.y); vec4 v_outsideDistance = vec4( uvFlipped.y, (1.0 - uvFlipped.x) * aspectRatio, 1.0 - uvFlipped.y, uvFlipped.x * aspectRatio ); vec4 v_borderDistance = v_outsideDistance - borderSize; vec2 distance = vec2(min4(v_outsideDistance), min4(v_borderDistance)); vec4 negateBorderDistance = vec4(1.0) - v_borderDistance; float maxWeight = max4(negateBorderDistance); vec4 borderWeight = step(maxWeight, negateBorderDistance); vec4 insideBorder = vec4(0.0); vec2 cornerPos; float cornerRadius; vec2 cornerBorderSizes; if (all(lessThan(v_outsideDistance.wx, borderRadius.xx))) { cornerPos = v_outsideDistance.wx; cornerRadius = borderRadius.x; cornerBorderSizes = borderSize.wx; distance = radiusDistance(cornerRadius, cornerPos, v_borderDistance.wx, cornerBorderSizes); vec2 lineIntersection = calculateCornerIntersection(cornerRadius, cornerBorderSizes, aspectRatio); insideBorder.wx = max(vec2(0.0), lineIntersection - v_borderDistance.wx); } else if (all(lessThan(v_outsideDistance.yx, borderRadius.yy))) { cornerPos = v_outsideDistance.yx; cornerRadius = borderRadius.y; cornerBorderSizes = borderSize.yx; distance = radiusDistance(cornerRadius, cornerPos, v_borderDistance.yx, cornerBorderSizes); vec2 lineIntersection = calculateCornerIntersection(cornerRadius, cornerBorderSizes, aspectRatio); insideBorder.yx = max(vec2(0.0), lineIntersection - v_borderDistance.yx); } else if (all(lessThan(v_outsideDistance.yz, borderRadius.zz))) { cornerPos = v_outsideDistance.yz; cornerRadius = borderRadius.z; cornerBorderSizes = borderSize.yz; distance = radiusDistance(cornerRadius, cornerPos, v_borderDistance.yz, cornerBorderSizes); vec2 lineIntersection = calculateCornerIntersection(cornerRadius, cornerBorderSizes, aspectRatio); insideBorder.yz = max(vec2(0.0), lineIntersection - v_borderDistance.yz); } else if (all(lessThan(v_outsideDistance.zw, borderRadius.ww))) { cornerPos = v_outsideDistance.zw; cornerRadius = borderRadius.w; cornerBorderSizes = borderSize.zw; distance = radiusDistance(cornerRadius, cornerPos, v_borderDistance.zw, cornerBorderSizes); vec2 lineIntersection = calculateCornerIntersection(cornerRadius, cornerBorderSizes, aspectRatio); insideBorder.zw = max(vec2(0.0), lineIntersection - v_borderDistance.zw); } float insideBorderSum = dot(insideBorder, vec4(1.0)); if (insideBorderSum > 0.0) { borderWeight = insideBorder / insideBorderSum; } #include <clipping_planes_fragment>`; } function getFragmentOpacityCode(instanced, existingOpacity) { return `vec2 distanceGradient = fwidth(distance); float outer = smoothstep(-distanceGradient.x, distanceGradient.x, distance.x); float inner = smoothstep(-distanceGradient.y, distanceGradient.y, distance.y); float transition = 1.0 - step(0.1, outer - inner) * (1.0 - inner); float fullBackgroundOpacity = ${existingOpacity == null ? '' : `${existingOpacity} * `}backgroundOpacity; float fullBorderOpacity = min(1.0, borderOpacity + fullBackgroundOpacity); float outOpacity = ${instanced ? 'clipOpacity * ' : ''}outer * mix(fullBorderOpacity, fullBackgroundOpacity, transition); if (outOpacity < 0.01) { discard; }`; } export function compilePanelMaterial(parameters, instanced) { compilePanelClippingMaterial(parameters, instanced); parameters.fragmentShader = parameters.fragmentShader.replace('#include <color_fragment>', ` #include <color_fragment> ${getFragmentOpacityCode(instanced, 'diffuseColor.a')} vec3 mainColor = diffuseColor.rgb * backgroundColor; float borderMix = borderOpacity / max(fullBorderOpacity, 0.001); diffuseColor.rgb = mix(mix(mainColor, borderColor, borderMix), mainColor, transition); diffuseColor.a = outOpacity; `); parameters.fragmentShader = parameters.fragmentShader.replace('#include <normal_fragment_maps>', ` #include <normal_fragment_maps> vec3 bitangent = normalize(vBitangent); vec3 tangent = normalize(vTangent); mat4 directions = mat4( vec4(bitangent, 1.0), vec4(tangent, 1.0), vec4(-bitangent, 1.0), vec4(-tangent, 1.0) ); float currentBorderSize = distance.x - distance.y; float outsideNormalWeight = currentBorderSize < 1e-5 ? 0.0 : max(0.0, -distance.y / currentBorderSize) * -borderBend; vec3 outsideNormal = (borderWeight * transpose(directions)).xyz; normal = normalize(mix(normal, outsideNormal, outsideNormalWeight)); `); }