The prompt that made it
as published# THERMAL SHADER — COMPONENT INTEGRATION
YOU ARE GIVEN A TASK TO INTEGRATE AN EXISTING REACT COMPONENT IN THE CODEBASE
THE CODEBASE SHOULD SUPPORT:
- SHADCN PROJECT STRUCTURE
- TAILWIND CSS
- TYPESCRIPT
IF IT DOESN'T, PROVIDE INSTRUCTIONS ON HOW TO SETUP PROJECT VIA SHADCN CLI, INSTALL TAILWIND OR TYPESCRIPT.
DETERMINE THE DEFAULT PATH FOR COMPONENTS AND STYLES.
IF DEFAULT PATH FOR COMPONENTS IS NOT /COMPONENTS/UI, PROVIDE INSTRUCTIONS ON WHY IT'S IMPORTANT TO CREATE THIS FOLDER
COPY-PASTE THIS COMPONENT TO /COMPONENTS/UI FOLDER:
```tsx
THERMAL-SHADER.TSX
/* ESLINT-DISABLE @TYPESCRIPT-ESLINT/NO-EXPLICIT-ANY */
"USE CLIENT"
IMPORT * AS THREE FROM 'THREE'
IMPORT REACT, { USEEFFECT, USEREF } FROM "REACT"
INTERFACE THERMALEFFECTPROPS {
WIDTH?: NUMBER
HEIGHT?: NUMBER
CLASSNAME?: STRING
LOGOURL?: STRING // ADD LOGOURL PROP
}
CONST THERMALEFFECT: REACT.FC<THERMALEFFECTPROPS> = ({
WIDTH = 400,
HEIGHT = 400,
CLASSNAME = "",
LOGOURL = "HTTPS://RAW.GITHUBUSERCONTENT.COM/DALIM-IN/DALIM/REFS/HEADS/MAIN/APPS/UI/PUBLIC/APPLE.PNG", // DEFAULT LOGO URL
}) => {
CONST CONTAINERREF = USEREF<HTMLDIVELEMENT>(NULL)
CONST ENGINEREF = USEREF<THERMALEFFECTENGINE | NULL>(NULL)
USEEFFECT(() => {
IF (!CONTAINERREF.CURRENT) RETURN
CONST INITENGINE = ASYNC () => {
TRY {
CONST ENGINE = NEW THERMALEFFECTENGINE(CONTAINERREF.CURRENT!, LOGOURL)
AWAIT ENGINE.INIT()
ENGINEREF.CURRENT = ENGINE
} CATCH (ERROR) {
CONSOLE.ERROR("FAILED TO INITIALIZE THERMAL EFFECT:", ERROR)
}
}
INITENGINE()
RETURN () => {
IF (ENGINEREF.CURRENT) {
ENGINEREF.CURRENT.DISPOSE()
ENGINEREF.CURRENT = NULL
}
}
}, [LOGOURL])
RETURN (
<DIV CLASSNAME={`FLEX W-FULL H-FULL JUSTIFY-CENTER ${CLASSNAME}`}>
<DIV REF={CONTAINERREF} STYLE={{ WIDTH, HEIGHT }} />
</DIV>
)
}
EXPORT { THERMALEFFECT }
CLASS THERMALEFFECTENGINE IMPLEMENTS DISPOSABLE {
PRIVATE RENDERER: THREE.WEBGLRENDERER
PRIVATE LOGOURL: STRING // ADD LOGOURL PROPERTY
PRIVATE SCENE: THREE.SCENE
PRIVATE CAMERA: THREE.ORTHOGRAPHICCAMERA
PRIVATE DRAWRENDERER!: DRAWRENDERER
PRIVATE INTERACTIONMANAGER!: INTERACTIONMANAGER
PRIVATE THERMALMATERIAL!: THERMALMATERIAL
PRIVATE HEATMESH!: THREE.MESH
PRIVATE MASKTEXTURE!: THREE.TEXTURE
PRIVATE LOGOTEXTURE!: THREE.TEXTURE
PRIVATE CONTAINER: HTMLELEMENT
PRIVATE HEATUP = 0
PRIVATE PARAMETERS: EFFECTPARAMETERS = { ...DEFAULT_PARAMETERS }
PRIVATE ANIMATIONID: NUMBER | NULL = NULL
PRIVATE LASTTIME = 0
PRIVATE ANIMATIONVALUES: ANIMATIONVALUES = {
BLENDVIDEO: { VALUE: 1, TARGET: 1 },
AMOUNT: { VALUE: 0, TARGET: 1 },
MOUSE: {
POSITION: NEW THREE.VECTOR3(0, 0, 0),
TARGET: NEW THREE.VECTOR3(0, 0, 0),
},
MOVE: { VALUE: 1, TARGET: 1 },
SCROLLANIMATION: {
OPACITY: { VALUE: 1, TARGET: 1 },
SCALE: { VALUE: 1, TARGET: 1 },
POWER: { VALUE: 0.8, TARGET: 0.8 },
},
}
// RESIZE HANDLER REFERENCE SO WE CAN REMOVE IT LATER
PRIVATE RESIZEHANDLER = () => {
CONST RECT = THIS.CONTAINER.GETBOUNDINGCLIENTRECT()
THIS.RENDERER.SETSIZE(RECT.WIDTH, RECT.HEIGHT)
THIS.ONRESIZE(RECT.WIDTH, RECT.HEIGHT)
}
CONSTRUCTOR(CONTAINER: HTMLELEMENT, LOGOURL: STRING) {
THIS.CONTAINER = CONTAINER
THIS.LOGOURL = LOGOURL // STORE LOGOURL
// RENDERER
THIS.RENDERER = NEW THREE.WEBGLRENDERER({
ALPHA: TRUE,
ANTIALIAS: FALSE,
LOGARITHMICDEPTHBUFFER: FALSE,
})
THIS.RENDERER.OUTPUTCOLORSPACE = (THREE AS ANY).SRGBCOLORSPACE ?? THIS.RENDERER.OUTPUTCOLORSPACE
THIS.RENDERER.SETCLEARCOLOR(0X000000, 0)
// SCENE & CAMERA
THIS.SCENE = NEW THREE.SCENE()
THIS.CAMERA = NEW THREE.ORTHOGRAPHICCAMERA(
CAMERA_CONFIG.LEFT,
CAMERA_CONFIG.RIGHT,
CAMERA_CONFIG.TOP,
CAMERA_CONFIG.BOTTOM,
CAMERA_CONFIG.NEAR,
CAMERA_CONFIG.FAR
)
THIS.CAMERA.POSITION.Z = CAMERA_CONFIG.POSITION_Z
// ATTACH CANVAS
THIS.SETUPRENDERER()
THIS.SETUPRESIZEHANDLER()
}
PRIVATE SETUPRENDERER(): VOID {
CONST RECT = THIS.CONTAINER.GETBOUNDINGCLIENTRECT()
THIS.RENDERER.SETSIZE(RECT.WIDTH, RECT.HEIGHT)
THIS.RENDERER.SETPIXELRATIO(WINDOW.DEVICEPIXELRATIO || 1)
THIS.RENDERER.DOMELEMENT.STYLE.POINTEREVENTS = "NONE"
THIS.CONTAINER.APPENDCHILD(THIS.RENDERER.DOMELEMENT)
}
PRIVATE SETUPRESIZEHANDLER(): VOID {
WINDOW.ADDEVENTLISTENER("RESIZE", THIS.RESIZEHANDLER)
}
ASYNC INIT(): PROMISE<VOID> {
CONST ISMOBILE = ISTOUCHDEVICE()
THIS.DRAWRENDERER = NEW DRAWRENDERER(256, { RADIUSRATIO: 1000, ISMOBILE })
CONST [MASKTEXTURE, LOGOTEXTURE] = AWAIT PROMISE.ALL([
LOADTEXTURE(THIS.LOGOURL), // USE THIS.LOGOURL INSTEAD OF ASSETS.LOGO_URL
LOADTEXTURE(THIS.LOGOURL),
])
THIS.MASKTEXTURE = MASKTEXTURE
THIS.LOGOTEXTURE = LOGOTEXTURE
THIS.CREATETHERMALEFFECT()
THIS.SETUPMATERIALTEXTUREBASEDUNIFORMS()
THIS.SETUPINTERACTIONMANAGER()
CONST RECT = THIS.CONTAINER.GETBOUNDINGCLIENTRECT()
THIS.ONRESIZE(RECT.WIDTH, RECT.HEIGHT)
THIS.STARTANIMATIONLOOP()
}
PRIVATE SETUPMATERIALTEXTUREBASEDUNIFORMS(): VOID {
IF (!THIS.THERMALMATERIAL || !THIS.MASKTEXTURE) RETURN
CONST UNIFORMS = THIS.THERMALMATERIAL.GETUNIFORMS?.()
CONST TEXWIDTH =
(THIS.MASKTEXTURE.IMAGE && (THIS.MASKTEXTURE.IMAGE AS ANY).WIDTH) ||
(THIS.MASKTEXTURE.USERDATA && THIS.MASKTEXTURE.USERDATA.WIDTH) ||
512
CONST GLOWRADIUSUV = 10 / MATH.MAX(1, TEXWIDTH)
IF (UNIFORMS && UNIFORMS.GLOWRADIUS) {
UNIFORMS.GLOWRADIUS.VALUE = GLOWRADIUSUV
}
IF (UNIFORMS && UNIFORMS.GLOWINTENSITY) {
UNIFORMS.GLOWINTENSITY.VALUE = 0.7
}
}
PRIVATE CREATETHERMALEFFECT(): VOID {
THIS.THERMALMATERIAL = NEW THERMALMATERIAL({
DRAWTEXTURE: THIS.DRAWRENDERER.GETTEXTURE(),
MASKTEXTURE: THIS.MASKTEXTURE,
})
THIS.HEATMESH = NEW THREE.MESH(
NEW THREE.PLANEGEOMETRY(1, 1),
THIS.THERMALMATERIAL.GETMATERIAL()
)
THIS.HEATMESH.POSITION.SET(0, 0, 0)
THIS.SCENE.ADD(THIS.HEATMESH)
}
PRIVATE SETUPINTERACTIONMANAGER(): VOID {
CONST HITCONTAINER = THIS.CONTAINER.QUERYSELECTOR(
".THERMAL-INTERACTION-AREA"
) AS HTMLELEMENT
THIS.INTERACTIONMANAGER = NEW INTERACTIONMANAGER({
CONTAINER: THIS.CONTAINER,
HITCONTAINER: HITCONTAINER || THIS.CONTAINER,
ONPOSITIONUPDATE: (POSITION, DIRECTION) => {
THIS.ANIMATIONVALUES.MOUSE.TARGET.COPY(POSITION)
THIS.DRAWRENDERER.UPDATEDIRECTION(DIRECTION)
},
ONINTERACTIONCHANGE: (ISINTERACTING) => {
THIS.ANIMATIONVALUES.MOVE.TARGET = ISINTERACTING
? INTERACTION.HOLD_MOVE_TARGET
: INTERACTION.RELEASE_MOVE_TARGET
THIS.ANIMATIONVALUES.SCROLLANIMATION.POWER.TARGET = ISINTERACTING
? INTERACTION.HOLD_POWER_TARGET
: INTERACTION.RELEASE_POWER_TARGET
},
})
}
PRIVATE STARTANIMATIONLOOP(): VOID {
CONST ANIMATE = (CURRENTTIME: NUMBER) => {
CONST DELTATIME = (CURRENTTIME - THIS.LASTTIME) / 1000
THIS.LASTTIME = CURRENTTIME
THIS.UPDATE(DELTATIME)
THIS.RENDER()
THIS.ANIMATIONID = REQUESTANIMATIONFRAME(ANIMATE)
}
THIS.ANIMATIONID = REQUESTANIMATIONFRAME(ANIMATE)
}
PRIVATE UPDATE(DELTATIME: NUMBER): VOID {
THIS.UPDATEANIMATIONVALUES(DELTATIME)
THIS.UPDATEHEATINTERACTION(DELTATIME)
THIS.UPDATETHERMALMATERIAL()
THIS.UPDATEMESHTRANSFORM()
THIS.UPDATEDRAWRENDERER()
// NEW: UPDATE TIME UNIFORM FOR LOOPING ANIMATION IN THE SHADER
THIS.THERMALMATERIAL.UPDATETIME(THIS.LASTTIME / 1000)
}
PRIVATE UPDATEANIMATIONVALUES(DELTATIME: NUMBER): VOID {
THIS.ANIMATIONVALUES.MOUSE.POSITION.LERP(
THIS.ANIMATIONVALUES.MOUSE.TARGET,
LERPSPEED(ANIMATION.MOUSE_INTERPOLATION_SPEED, DELTATIME)
)
THIS.ANIMATIONVALUES.MOVE.VALUE = LERP(
THIS.ANIMATIONVALUES.MOVE.VALUE,
THIS.ANIMATIONVALUES.MOVE.TARGET,
LERPSPEED(ANIMATION.MOVEMENT_INTERPOLATION_SPEED, DELTATIME)
)
THIS.ANIMATIONVALUES.SCROLLANIMATION.POWER.VALUE = CLAMP(
LERP(
THIS.ANIMATIONVALUES.SCROLLANIMATION.POWER.VALUE,
THIS.ANIMATIONVALUES.SCROLLANIMATION.POWER.TARGET,
LERPSPEED(ANIMATION.POWER_INTERPOLATION_SPEED, DELTATIME)
),
ANIMATION.POWER_MIN,
ANIMATION.POWER_MAX
)
THIS.ANIMATIONVALUES.SCROLLANIMATION.OPACITY.VALUE = LERP(
THIS.ANIMATIONVALUES.SCROLLANIMATION.OPACITY.VALUE,
THIS.ANIMATIONVALUES.SCROLLANIMATION.OPACITY.TARGET *
THIS.ANIMATIONVALUES.MOVE.VALUE,
LERPSPEED(ANIMATION.SCROLL_INTERPOLATION_SPEED, DELTATIME)
)
THIS.ANIMATIONVALUES.SCROLLANIMATION.SCALE.VALUE = LERP(
THIS.ANIMATIONVALUES.SCROLLANIMATION.SCALE.VALUE,
THIS.ANIMATIONVALUES.SCROLLANIMATION.SCALE.TARGET,
LERPSPEED(ANIMATION.SCROLL_INTERPOLATION_SPEED, DELTATIME)
)
IF (THIS.ANIMATIONVALUES.AMOUNT.VALUE < 0.99999) {
THIS.ANIMATIONVALUES.AMOUNT.VALUE = LERP(
THIS.ANIMATIONVALUES.AMOUNT.VALUE,
THIS.ANIMATIONVALUES.AMOUNT.TARGET,
ANIMATION.FADE_IN_SPEED * DELTATIME * ANIMATION.TARGET_FPS
)
}
}
PRIVATE UPDATEHEATINTERACTION(DELTATIME: NUMBER): VOID {
CONST INTERACTIONSTATE = THIS.INTERACTIONMANAGER.GETINTERACTIONSTATE()
CONST MOUSESTATE = THIS.INTERACTIONMANAGER.GETMOUSESTATE()
THIS.DRAWRENDERER.UPDATEPOSITION(MOUSESTATE.POSITION, TRUE)
IF (INTERACTIONSTATE.HOLD) {
THIS.HEATUP +=
THIS.PARAMETERS.HEATSENSITIVITY * DELTATIME * ANIMATION.TARGET_FPS
THIS.HEATUP = MATH.MIN(THIS.HEATUP, ANIMATION.HEAT_MAX_VALUE)
}
THIS.DRAWRENDERER.UPDATEDRAW(THIS.HEATUP)
THIS.HEATUP *= THIS.PARAMETERS.HEATDECAY
IF (THIS.HEATUP < INTERACTION.HEAT_CLEANUP_THRESHOLD) {
THIS.HEATUP = 0
}
THIS.INTERACTIONMANAGER.UPDATEMOUSEPOSITION(
LERPSPEED(ANIMATION.MOUSE_INTERPOLATION_SPEED, DELTATIME)
)
}
PRIVATE UPDATETHERMALMATERIAL(): VOID {
IF (!THIS.THERMALMATERIAL) RETURN
THIS.THERMALMATERIAL.UPDATEUNIFORMS({
OPACITY: THIS.ANIMATIONVALUES.SCROLLANIMATION.OPACITY.VALUE,
AMOUNT: THIS.ANIMATIONVALUES.AMOUNT.VALUE,
POWER: THIS.PARAMETERS.CONTRASTPOWER,
BLENDVIDEO: THIS.PARAMETERS.VIDEOBLENDAMOUNT,
RANDOMVALUE: MATH.RANDOM(),
})
THIS.THERMALMATERIAL.UPDATEFROMPARAMETERS(THIS.PARAMETERS)
}
PRIVATE UPDATEMESHTRANSFORM(): VOID {
CONST SCALE = THIS.ANIMATIONVALUES.SCROLLANIMATION.SCALE.VALUE
IF (THIS.HEATMESH) THIS.HEATMESH.SCALE.SET(SCALE, SCALE, SCALE)
}
PRIVATE UPDATEDRAWRENDERER(): VOID {
THIS.DRAWRENDERER.UPDATEDIRECTION({ X: 0, Y: 0 })
}
PRIVATE RENDER(): VOID {
IF (THIS.DRAWRENDERER) {
CONST RECT = THIS.CONTAINER.GETBOUNDINGCLIENTRECT()
THIS.DRAWRENDERER.RESIZE(RECT.WIDTH, RECT.HEIGHT)
THIS.DRAWRENDERER.RENDER(THIS.RENDERER)
}
THIS.RENDERER.AUTOCLEAR = TRUE
THIS.RENDERER.RENDER(THIS.SCENE, THIS.CAMERA)
}
PRIVATE ONRESIZE(WIDTH: NUMBER, HEIGHT: NUMBER): VOID {
CONST ASPECTRATIO = WIDTH / HEIGHT
LET CAMERAWIDTH: NUMBER, CAMERAHEIGHT: NUMBER
IF (ASPECTRATIO >= 1) {
CAMERAHEIGHT = 1
CAMERAWIDTH = ASPECTRATIO
} ELSE {
CAMERAWIDTH = 1
CAMERAHEIGHT = 1 / ASPECTRATIO
}
THIS.CAMERA.LEFT = -CAMERAWIDTH / 2
THIS.CAMERA.RIGHT = CAMERAWIDTH / 2
THIS.CAMERA.TOP = CAMERAHEIGHT / 2
THIS.CAMERA.BOTTOM = -CAMERAHEIGHT / 2
THIS.CAMERA.UPDATEPROJECTIONMATRIX()
}
GETPARAMETERS(): READONLY<EFFECTPARAMETERS> {
RETURN { ...THIS.PARAMETERS }
}
SETPARAMETER(NAME: KEYOF EFFECTPARAMETERS, VALUE: NUMBER): VOID {
THIS.PARAMETERS[NAME] = VALUE
}
RESETPARAMETERS(): VOID {
THIS.PARAMETERS = { ...DEFAULT_PARAMETERS }
}
DISPOSE(): VOID {
IF (THIS.ANIMATIONID !== NULL) {
CANCELANIMATIONFRAME(THIS.ANIMATIONID)
THIS.ANIMATIONID = NULL
}
WINDOW.REMOVEEVENTLISTENER("RESIZE", THIS.RESIZEHANDLER)
THIS.DRAWRENDERER?.DISPOSE()
THIS.INTERACTIONMANAGER?.DISPOSE()
THIS.THERMALMATERIAL?.DISPOSE()
IF (THIS.HEATMESH) {
IF (THIS.HEATMESH.GEOMETRY) THIS.HEATMESH.GEOMETRY.DISPOSE()
}
THIS.SCENE.REMOVE(THIS.HEATMESH)
IF (THIS.MASKTEXTURE) THIS.MASKTEXTURE.DISPOSE()
IF (THIS.LOGOTEXTURE) THIS.LOGOTEXTURE.DISPOSE()
THIS.RENDERER.DISPOSE()
IF (THIS.CONTAINER.CONTAINS(THIS.RENDERER.DOMELEMENT)) {
THIS.CONTAINER.REMOVECHILD(THIS.RENDERER.DOMELEMENT)
}
}
}
INTERFACE THERMALMATERIALCONFIG {
DRAWTEXTURE: THREE.TEXTURE
MASKTEXTURE: THREE.TEXTURE
}
CLASS THERMALMATERIAL IMPLEMENTS DISPOSABLE {
PRIVATE MATERIAL: THREE.SHADERMATERIAL
PRIVATE UNIFORMS: THERMALSHADERUNIFORMS
CONSTRUCTOR(CONFIG: THERMALMATERIALCONFIG) {
THIS.UNIFORMS = THIS.CREATEUNIFORMS(CONFIG)
THIS.MATERIAL = THIS.CREATEMATERIAL()
}
PRIVATE CREATEUNIFORMS(CONFIG: THERMALMATERIALCONFIG): THERMALSHADERUNIFORMS {
CONST COLORS = THERMAL_PALETTE.MAP(HEXTORGB) AS [
[NUMBER, NUMBER, NUMBER],
[NUMBER, NUMBER, NUMBER],
[NUMBER, NUMBER, NUMBER],
[NUMBER, NUMBER, NUMBER],
[NUMBER, NUMBER, NUMBER],
[NUMBER, NUMBER, NUMBER],
[NUMBER, NUMBER, NUMBER],
]
RETURN {
// TEXTURES
BLENDVIDEO: { VALUE: 0 },
DRAWMAP: { VALUE: CONFIG.DRAWTEXTURE },
TEXTUREMAP: { VALUE: CONFIG.DRAWTEXTURE },
MASKMAP: { VALUE: CONFIG.MASKTEXTURE },
// TRANSFORM
SCALE: { VALUE: [1, 1] },
OFFSET: { VALUE: [0, 0] },
OPACITY: { VALUE: 1 },
AMOUNT: { VALUE: 0 },
// PALETTE
COLOR1: { VALUE: COLORS[0] },
COLOR2: { VALUE: COLORS[1] },
COLOR3: { VALUE: COLORS[2] },
COLOR4: { VALUE: COLORS[3] },
COLOR5: { VALUE: COLORS[4] },
COLOR6: { VALUE: COLORS[5] },
COLOR7: { VALUE: COLORS[6] },
BLEND: { VALUE: [...GRADIENT_CONFIG.BLEND_POINTS] AS [NUMBER, NUMBER, NUMBER, NUMBER] },
FADE: { VALUE: [...GRADIENT_CONFIG.FADE_RANGES] AS [NUMBER, NUMBER, NUMBER, NUMBER] },
MAXBLEND: { VALUE: [...GRADIENT_CONFIG.MAX_BLEND] AS [NUMBER, NUMBER, NUMBER, NUMBER] },
// EFFECT PARAMS
POWER: { VALUE: 0.8 },
RND: { VALUE: 0 },
HEAT: { VALUE: [0, 0, 0, 1.02] },
STRETCH: { VALUE: [1, 1, 0, 0] },
EFFECTINTENSITY: { VALUE: 1.0 },
COLORSATURATION: { VALUE: 1.3 },
GRADIENTSHIFT: { VALUE: 0.0 },
INTERACTIONSIZE: { VALUE: 1.0 },
// TIME + GLOW + BLUR
TIME: { VALUE: 0.0 },
GLOWRADIUS: { VALUE: 0.02 },
GLOWINTENSITY: { VALUE: 0.7 },
BLURAMOUNT: { VALUE: 0.005 },
// ANIMATION
ANIMATIONSPEED: { VALUE: 1.0 },
ANIMATIONINTENSITY: { VALUE: 0.5 },
WAVEFREQUENCY: { VALUE: 8.0 },
PULSESPEED: { VALUE: 2.0 },
BASEANIMATIONLEVEL: { VALUE: 0.3 },
ANIMATIONBLENDMODE: { VALUE: 1.0 },
}
}
PRIVATE CREATEMATERIAL(): THREE.SHADERMATERIAL {
RETURN NEW THREE.SHADERMATERIAL({
UNIFORMS: THIS.UNIFORMS,
VERTEXSHADER: THERMALVERTEXSHADER,
FRAGMENTSHADER: THERMALFRAGMENTSHADER,
DEPTHTEST: FALSE,
TRANSPARENT: TRUE,
})
}
UPDATEUNIFORMS(UPDATES: {
OPACITY?: NUMBER
AMOUNT?: NUMBER
POWER?: NUMBER
BLENDVIDEO?: NUMBER
EFFECTINTENSITY?: NUMBER
COLORSATURATION?: NUMBER
GRADIENTSHIFT?: NUMBER
INTERACTIONSIZE?: NUMBER
RANDOMVALUE?: NUMBER
}): VOID {
IF (UPDATES.OPACITY !== UNDEFINED) THIS.UNIFORMS.OPACITY.VALUE = UPDATES.OPACITY
IF (UPDATES.AMOUNT !== UNDEFINED) THIS.UNIFORMS.AMOUNT.VALUE = UPDATES.AMOUNT
IF (UPDATES.POWER !== UNDEFINED) THIS.UNIFORMS.POWER.VALUE = UPDATES.POWER
IF (UPDATES.BLENDVIDEO !== UNDEFINED) THIS.UNIFORMS.BLENDVIDEO.VALUE = UPDATES.BLENDVIDEO
IF (UPDATES.EFFECTINTENSITY !== UNDEFINED) THIS.UNIFORMS.EFFECTINTENSITY.VALUE = UPDATES.EFFECTINTENSITY
IF (UPDATES.COLORSATURATION !== UNDEFINED) THIS.UNIFORMS.COLORSATURATION.VALUE = UPDATES.COLORSATURATION
IF (UPDATES.GRADIENTSHIFT !== UNDEFINED) THIS.UNIFORMS.GRADIENTSHIFT.VALUE = UPDATES.GRADIENTSHIFT
IF (UPDATES.INTERACTIONSIZE !== UNDEFINED) THIS.UNIFORMS.INTERACTIONSIZE.VALUE = UPDATES.INTERACTIONSIZE
IF (UPDATES.RANDOMVALUE !== UNDEFINED) THIS.UNIFORMS.RND.VALUE = UPDATES.RANDOMVALUE
}
UPDATETEXTURES(TEXTURES: {
VIDEOTEXTURE?: THREE.VIDEOTEXTURE
DRAWTEXTURE?: THREE.TEXTURE
MASKTEXTURE?: THREE.TEXTURE
}): VOID {
IF (TEXTURES.VIDEOTEXTURE) THIS.UNIFORMS.TEXTUREMAP.VALUE = TEXTURES.VIDEOTEXTURE
IF (TEXTURES.DRAWTEXTURE) THIS.UNIFORMS.DRAWMAP.VALUE = TEXTURES.DRAWTEXTURE
IF (TEXTURES.MASKTEXTURE) THIS.UNIFORMS.MASKMAP.VALUE = TEXTURES.MASKTEXTURE
}
UPDATETRANSFORM(TRANSFORM: { SCALE?: [NUMBER, NUMBER]; OFFSET?: [NUMBER, NUMBER] }): VOID {
IF (TRANSFORM.SCALE) THIS.UNIFORMS.SCALE.VALUE = TRANSFORM.SCALE
IF (TRANSFORM.OFFSET) THIS.UNIFORMS.OFFSET.VALUE = TRANSFORM.OFFSET
}
UPDATETIME(TIME: NUMBER): VOID {
THIS.UNIFORMS.TIME.VALUE = TIME
}
UPDATEFROMPARAMETERS(PARAMETERS: EFFECTPARAMETERS): VOID {
THIS.UPDATEUNIFORMS({
EFFECTINTENSITY: PARAMETERS.EFFECTINTENSITY,
COLORSATURATION: PARAMETERS.COLORSATURATION,
GRADIENTSHIFT: PARAMETERS.GRADIENTSHIFT,
INTERACTIONSIZE: PARAMETERS.INTERACTIONRADIUS,
POWER: PARAMETERS.CONTRASTPOWER,
BLENDVIDEO: PARAMETERS.VIDEOBLENDAMOUNT,
})
}
GETMATERIAL(): THREE.SHADERMATERIAL {
RETURN THIS.MATERIAL
}
GETUNIFORMS(): THERMALSHADERUNIFORMS {
RETURN THIS.UNIFORMS
}
DISPOSE(): VOID {
THIS.MATERIAL.DISPOSE()
}
}
// INTERACTION MANAGER
INTERFACE INTERACTIONMANAGERCONFIG {
CONTAINER: HTMLELEMENT
HITCONTAINER?: HTMLELEMENT
ONPOSITIONUPDATE: (POSITION: THREE.VECTOR3, DIRECTION: { X: NUMBER; Y: NUMBER }) => VOID
ONINTERACTIONCHANGE: (ISINTERACTING: BOOLEAN) => VOID
}
CLASS INTERACTIONMANAGER IMPLEMENTS DISPOSABLE {
PRIVATE CONTAINER: HTMLELEMENT
PRIVATE HITCONTAINER: HTMLELEMENT
PRIVATE ONPOSITIONUPDATE: INTERACTIONMANAGERCONFIG['ONPOSITIONUPDATE']
PRIVATE ONINTERACTIONCHANGE: INTERACTIONMANAGERCONFIG['ONINTERACTIONCHANGE']
PRIVATE MOUSESTATE: MOUSESTATE
PRIVATE INTERACTIONSTATE: INTERACTIONSTATE
PRIVATE CLEANUPFUNCTIONS: ARRAY<() => VOID> = []
CONSTRUCTOR(CONFIG: INTERACTIONMANAGERCONFIG) {
THIS.CONTAINER = CONFIG.CONTAINER
THIS.HITCONTAINER = CONFIG.HITCONTAINER || CONFIG.CONTAINER
THIS.ONPOSITIONUPDATE = CONFIG.ONPOSITIONUPDATE
THIS.ONINTERACTIONCHANGE = CONFIG.ONINTERACTIONCHANGE
THIS.MOUSESTATE = {
POSITION: NEW THREE.VECTOR3(0, 0, 0),
TARGET: NEW THREE.VECTOR3(0, 0, 0)
}
THIS.INTERACTIONSTATE = {
HOLD: FALSE,
HEATUP: 0,
LASTNX: 0.5,
LASTNY: 0.5
}
THIS.SETUPEVENTLISTENERS()
}
PRIVATE SETUPEVENTLISTENERS(): VOID {
CONST ELEMENTS = [THIS.HITCONTAINER]
IF (THIS.CONTAINER !== THIS.HITCONTAINER) {
ELEMENTS.PUSH(THIS.CONTAINER)
}
ELEMENTS.FOREACH(ELEMENT => {
THIS.CLEANUPFUNCTIONS.PUSH(
ADDEVENTLISTENERWITHCLEANUP(ELEMENT, 'POINTERMOVE', THIS.HANDLEPOINTERMOVE),
ADDEVENTLISTENERWITHCLEANUP(ELEMENT, 'POINTERDOWN', THIS.HANDLEPOINTERDOWN),
ADDEVENTLISTENERWITHCLEANUP(ELEMENT, 'POINTERENTER', THIS.HANDLEPOINTERENTER),
ADDEVENTLISTENERWITHCLEANUP(ELEMENT, 'POINTERUP', THIS.HANDLEPOINTERUP),
ADDEVENTLISTENERWITHCLEANUP(ELEMENT, 'POINTERLEAVE', THIS.HANDLEPOINTERLEAVE)
)
})
THIS.CLEANUPFUNCTIONS.PUSH(
ADDEVENTLISTENERWITHCLEANUP(
WINDOW,
'POINTERMOVE',
THIS.HANDLEGLOBALPOINTERMOVE,
{ PASSIVE: TRUE }
)
)
}
PRIVATE HANDLEPOINTERMOVE = (EVENT: EVENT) => {
CONST POINTEREVENT = EVENT AS POINTEREVENT
THIS.UPDATEPOSITION(POINTEREVENT.CLIENTX, POINTEREVENT.CLIENTY)
THIS.SETINTERACTING(TRUE)
}
PRIVATE HANDLEPOINTERDOWN = (EVENT: EVENT) => {
CONST POINTEREVENT = EVENT AS POINTEREVENT
THIS.UPDATEPOSITION(POINTEREVENT.CLIENTX, POINTEREVENT.CLIENTY)
THIS.SETINTERACTING(TRUE)
}
PRIVATE HANDLEPOINTERENTER = (EVENT: EVENT) => {
CONST POINTEREVENT = EVENT AS POINTEREVENT
THIS.UPDATEPOSITION(POINTEREVENT.CLIENTX, POINTEREVENT.CLIENTY)
}
PRIVATE HANDLEPOINTERUP = () => {
THIS.SETINTERACTING(FALSE)
}
PRIVATE HANDLEPOINTERLEAVE = () => {
THIS.SETINTERACTING(FALSE)
}
PRIVATE HANDLEGLOBALPOINTERMOVE = (EVENT: EVENT) => {
CONST POINTEREVENT = EVENT AS POINTEREVENT
THIS.UPDATEGLOBALPOSITION(POINTEREVENT.CLIENTX, POINTEREVENT.CLIENTY)
THIS.SETINTERACTING(TRUE)
}
PRIVATE UPDATEPOSITION(CLIENTX: NUMBER, CLIENTY: NUMBER): VOID {
CONST BOUNDS = THIS.HITCONTAINER.GETBOUNDINGCLIENTRECT()
CONST { X, Y } = SCREENTONDC(CLIENTX, CLIENTY, BOUNDS)
CONST { X: DELTAX, Y: DELTAY } = CALCULATEMOVEMENTDELTA(
CLIENTX,
CLIENTY,
THIS.INTERACTIONSTATE.LASTNX,
THIS.INTERACTIONSTATE.LASTNY,
BOUNDS
)
THIS.MOUSESTATE.TARGET.SET(X, Y, 0)
THIS.ONPOSITIONUPDATE(THIS.MOUSESTATE.TARGET, { X: DELTAX, Y: DELTAY })
THIS.INTERACTIONSTATE.LASTNX = BOUNDS.WIDTH > 0 ? (CLIENTX - BOUNDS.LEFT) / BOUNDS.WIDTH : 0.5
THIS.INTERACTIONSTATE.LASTNY = BOUNDS.HEIGHT > 0 ? (CLIENTY - BOUNDS.TOP) / BOUNDS.HEIGHT : 0.5
}
PRIVATE UPDATEGLOBALPOSITION(CLIENTX: NUMBER, CLIENTY: NUMBER): VOID {
CONST BOUNDS = THIS.CONTAINER.GETBOUNDINGCLIENTRECT()
CONST { X, Y } = SCREENTONDC(CLIENTX, CLIENTY, BOUNDS)
CONST { X: DELTAX, Y: DELTAY } = CALCULATEMOVEMENTDELTA(
CLIENTX,
CLIENTY,
THIS.INTERACTIONSTATE.LASTNX,
THIS.INTERACTIONSTATE.LASTNY,
BOUNDS
)
THIS.MOUSESTATE.TARGET.SET(X, Y, 0)
THIS.ONPOSITIONUPDATE(THIS.MOUSESTATE.TARGET, { X: DELTAX, Y: DELTAY })
THIS.INTERACTIONSTATE.LASTNX = BOUNDS.WIDTH > 0 ? (CLIENTX - BOUNDS.LEFT) / BOUNDS.WIDTH : 0.5
THIS.INTERACTIONSTATE.LASTNY = BOUNDS.HEIGHT > 0 ? (CLIENTY - BOUNDS.TOP) / BOUNDS.HEIGHT : 0.5
}
PRIVATE SETINTERACTING(ISINTERACTING: BOOLEAN): VOID {
IF (THIS.INTERACTIONSTATE.HOLD !== ISINTERACTING) {
THIS.INTERACTIONSTATE.HOLD = ISINTERACTING
THIS.ONINTERACTIONCHANGE(ISINTERACTING)
}
}
GETMOUSESTATE(): READONLY<MOUSESTATE> {
RETURN THIS.MOUSESTATE
}
GETINTERACTIONSTATE(): READONLY<INTERACTIONSTATE> {
RETURN THIS.INTERACTIONSTATE
}
UPDATEMOUSEPOSITION(LERPFACTOR: NUMBER): VOID {
THIS.MOUSESTATE.POSITION.LERP(THIS.MOUSESTATE.TARGET, LERPFACTOR)
}
DISPOSE(): VOID {
THIS.CLEANUPFUNCTIONS.FOREACH(CLEANUP => CLEANUP())
THIS.CLEANUPFUNCTIONS = []
}
}
FUNCTION LOADTEXTURE(URL: STRING): PROMISE<THREE.TEXTURE> {
RETURN NEW PROMISE((RESOLVE, REJECT) => {
NEW THREE.TEXTURELOADER().LOAD(
URL,
(TEXTURE) => {
TEXTURE.WRAPS = TEXTURE.WRAPT = THREE.REPEATWRAPPING
RESOLVE(TEXTURE)
},
UNDEFINED,
(ERROR) => REJECT(ERROR)
)
})
}
FUNCTION ADDEVENTLISTENERWITHCLEANUP(
ELEMENT: EVENTTARGET,
EVENT: STRING,
HANDLER: EVENTLISTENER,
OPTIONS?: ADDEVENTLISTENEROPTIONS
): () => VOID {
ELEMENT.ADDEVENTLISTENER(EVENT, HANDLER, OPTIONS)
RETURN () => ELEMENT.REMOVEEVENTLISTENER(EVENT, HANDLER)
}
/**
* MATH UTILITIES FOR ANIMATION AND INTERPOLATION
*/
/**
* LINEAR INTERPOLATION BETWEEN TWO VALUES
*/
FUNCTION LERP(A: NUMBER, B: NUMBER, T: NUMBER): NUMBER {
RETURN A + (B - A) * T
}
EXPORT FUNCTION ISTOUCHDEVICE(): BOOLEAN {
RETURN 'ONTOUCHSTART' IN WINDOW || NAVIGATOR.MAXTOUCHPOINTS > 0
}
/**
* CONVERT FRAME-RATE INDEPENDENT LERP SPEED TO DELTA-TIME BASED FACTOR
*/
FUNCTION LERPSPEED(BASE: NUMBER, DELTATIME: NUMBER): NUMBER {
CONST N = BASE * DELTATIME * 60
RETURN N > 1 ? 1 : N < 0 ? 0 : N
}
/**
* CLAMP A VALUE BETWEEN MIN AND MAX
*/
FUNCTION CLAMP(VALUE: NUMBER, MIN: NUMBER, MAX: NUMBER): NUMBER {
RETURN MATH.MIN(MATH.MAX(VALUE, MIN), MAX)
}
/**
* CONVERT HEX COLOR STRING TO RGB VALUES NORMALIZED TO 0-1 RANGE
*/
FUNCTION HEXTORGB(HEX: STRING): [NUMBER, NUMBER, NUMBER] {
HEX = HEX.REPLACE(/^#/, '')
IF (HEX.LENGTH === 3) {
HEX = HEX.SPLIT('').MAP(C => C + C).JOIN('')
}
CONST R = PARSEINT(HEX.SLICE(0, 2), 16) / 255
CONST G = PARSEINT(HEX.SLICE(2, 4), 16) / 255
CONST B = PARSEINT(HEX.SLICE(4, 6), 16) / 255
RETURN [R, G, B]
}
/**
* CONVERT SCREEN COORDINATES TO NORMALIZED DEVICE COORDINATES (-1 TO 1)
*/
FUNCTION SCREENTONDC(
SCREENX: NUMBER,
SCREENY: NUMBER,
BOUNDS: DOMRECT
): { X: NUMBER; Y: NUMBER } {
LET NX = 0.5
LET NY = 0.5
IF (BOUNDS.WIDTH > 0) {
NX = (SCREENX - BOUNDS.LEFT) / BOUNDS.WIDTH
}
IF (BOUNDS.HEIGHT > 0) {
NY = (SCREENY - BOUNDS.TOP) / BOUNDS.HEIGHT
}
CONST X = 2 * (NX - 0.5)
CONST Y = 2 * -(NY - 0.5) // FLIP Y AXIS FOR WEBGL COORDINATES
RETURN { X, Y }
}
/**
* CALCULATE MOVEMENT DELTA FOR DIRECTION TRACKING
*/
FUNCTION CALCULATEMOVEMENTDELTA(
CURRENTX: NUMBER,
CURRENTY: NUMBER,
LASTX: NUMBER,
LASTY: NUMBER,
BOUNDS: DOMRECT
): { X: NUMBER; Y: NUMBER } {
CONST NX = BOUNDS.WIDTH > 0 ? (CURRENTX - BOUNDS.LEFT) / BOUNDS.WIDTH : 0.5
CONST NY = BOUNDS.HEIGHT > 0 ? (CURRENTY - BOUNDS.TOP) / BOUNDS.HEIGHT : 0.5
CONST NDX = NX - LASTX
CONST NDY = NY - LASTY
RETURN { X: NDX, Y: NDY }
}
CLASS DRAWRENDERER IMPLEMENTS DISPOSABLE {
PRIVATE CAMERA: THREE.ORTHOGRAPHICCAMERA
PRIVATE RENDERTARGETA: THREE.WEBGLRENDERTARGET
PRIVATE RENDERTARGETB: THREE.WEBGLRENDERTARGET
PRIVATE MATERIAL: THREE.SHADERMATERIAL
PRIVATE SCENE: THREE.SCENE
PRIVATE MESH: THREE.MESH
PRIVATE UNIFORMS: DRAWRENDERERUNIFORMS
PRIVATE OPTIONS: DRAWRENDEREROPTIONS
CONSTRUCTOR(SIZE = DRAW_RENDERER.TEXTURE_SIZE, OPTIONS: DRAWRENDEREROPTIONS = {}) {
THIS.OPTIONS = OPTIONS
THIS.CAMERA = NEW THREE.ORTHOGRAPHICCAMERA(
CAMERA_CONFIG.LEFT,
CAMERA_CONFIG.RIGHT,
CAMERA_CONFIG.TOP,
CAMERA_CONFIG.BOTTOM,
CAMERA_CONFIG.NEAR,
CAMERA_CONFIG.FAR
)
THIS.CAMERA.POSITION.Z = CAMERA_CONFIG.POSITION_Z
CONST RTOPTS: THREE.RENDERTARGETOPTIONS = {
TYPE: THREE.HALFFLOATTYPE,
FORMAT: THREE.RGBAFORMAT,
COLORSPACE: THREE.LINEARSRGBCOLORSPACE,
DEPTHBUFFER: FALSE,
STENCILBUFFER: FALSE,
MAGFILTER: THREE.LINEARFILTER,
MINFILTER: THREE.LINEARMIPMAPLINEARFILTER,
GENERATEMIPMAPS: TRUE
}
THIS.RENDERTARGETA = NEW THREE.WEBGLRENDERTARGET(SIZE, SIZE, RTOPTS)
THIS.RENDERTARGETB = NEW THREE.WEBGLRENDERTARGET(SIZE, SIZE, RTOPTS)
THIS.UNIFORMS = THIS.CREATEUNIFORMS()
THIS.MATERIAL = NEW THREE.SHADERMATERIAL({
UNIFORMS: THIS.UNIFORMS,
VERTEXSHADER: DRAWVERTEXSHADER,
FRAGMENTSHADER: DRAWFRAGMENTSHADER,
DEPTHTEST: FALSE,
TRANSPARENT: TRUE
})
THIS.SCENE = NEW THREE.SCENE()
THIS.MESH = NEW THREE.MESH(NEW THREE.PLANEGEOMETRY(1, 1), THIS.MATERIAL)
THIS.SCENE.ADD(THIS.MESH)
}
PRIVATE CREATEUNIFORMS(): DRAWRENDERERUNIFORMS {
CONST [RADIUSX, RADIUSY, RADIUSZ] = DRAW_RENDERER.UNIFORMS.RADIUS_VECTOR
RETURN {
URADIUS: { VALUE: NEW THREE.VECTOR3(RADIUSX, RADIUSY, RADIUSZ) },
UPOSITION: { VALUE: NEW THREE.VECTOR2(0, 0) },
UDIRECTION: { VALUE: NEW THREE.VECTOR4(0, 0, 0, 0) },
URESOLUTION: { VALUE: NEW THREE.VECTOR3(0, 0, 0) },
UTEXTURE: { VALUE: NULL },
USIZEDAMPING: { VALUE: DRAW_RENDERER.UNIFORMS.SIZE_DAMPING },
UFADEDAMPING: { VALUE: DRAW_RENDERER.UNIFORMS.FADE_DAMPING },
UDRAW: { VALUE: 0 }
}
}
UPDATERADIUS(PX = 0): VOID {
THIS.UNIFORMS.URADIUS.VALUE.Z = PX
}
UPDATEDRAW(VALUE = 0): VOID {
THIS.UNIFORMS.UDRAW.VALUE = VALUE
}
UPDATEPOSITION(POSITION: { X: NUMBER; Y: NUMBER }, NORMALIZED = FALSE): VOID {
LET X = POSITION.X
LET Y = POSITION.Y
IF (NORMALIZED) {
X = 0.5 * POSITION.X + 0.5
Y = 0.5 * POSITION.Y + 0.5
}
THIS.UNIFORMS.UPOSITION.VALUE.SET(X, Y)
}
UPDATEDIRECTION(DIRECTION: { X: NUMBER; Y: NUMBER }): VOID {
THIS.UNIFORMS.UDIRECTION.VALUE.SET(
DIRECTION.X,
DIRECTION.Y,
0,
DRAW_RENDERER.UNIFORMS.DIRECTION_MULTIPLIER
)
}
RESIZE(WIDTH: NUMBER, HEIGHT: NUMBER): VOID {
CONST RATIO = HEIGHT / (THIS.OPTIONS.RADIUSRATIO ?? DRAW_RENDERER.RADIUS_RATIO)
CONST BASERADIUS = THIS.OPTIONS.ISMOBILE
? DRAW_RENDERER.MOBILE_RADIUS
: DRAW_RENDERER.DESKTOP_RADIUS
CONST RADIUS = BASERADIUS * RATIO
THIS.UPDATERADIUS(RADIUS)
THIS.UNIFORMS.URESOLUTION.VALUE.SET(WIDTH, HEIGHT, 1)
}
GETTEXTURE(): THREE.TEXTURE {
RETURN THIS.RENDERTARGETB.TEXTURE
}
RENDER(RENDERER: THREE.WEBGLRENDERER): VOID {
THIS.UNIFORMS.UTEXTURE.VALUE = THIS.RENDERTARGETB.TEXTURE
CONST PREVIOUSTARGET = RENDERER.GETRENDERTARGET()
RENDERER.SETRENDERTARGET(THIS.RENDERTARGETA)
IF (RENDERER.AUTOCLEAR) RENDERER.CLEAR()
RENDERER.RENDER(THIS.SCENE, THIS.CAMERA)
RENDERER.SETRENDERTARGET(PREVIOUSTARGET)
// PING-PONG BETWEEN RENDER TARGETS
CONST TEMP = THIS.RENDERTARGETA
THIS.RENDERTARGETA = THIS.RENDERTARGETB
THIS.RENDERTARGETB = TEMP
}
/**
* DISPOSE OF ALL RESOURCES
*/
DISPOSE(): VOID {
THIS.MATERIAL.DISPOSE()
THIS.RENDERTARGETA.DISPOSE()
THIS.RENDERTARGETB.DISPOSE()
THIS.MESH.GEOMETRY.DISPOSE()
}
}
CONST THERMALVERTEXSHADER = `
/**
* THERMAL EFFECT VERTEX SHADER
* SIMPLE PASS-THROUGH THAT PREPARES UV COORDINATES FOR THE FRAGMENT SHADER
*/
VARYING VEC2 VUV;
VARYING VEC4 VCLIPPOSITION;
VOID MAIN() {
VUV = UV;
VCLIPPOSITION = PROJECTIONMATRIX * MODELVIEWMATRIX * VEC4(POSITION, 1.0);
GL_POSITION = VCLIPPOSITION;
}
`
CONST THERMALFRAGMENTSHADER = `
/**
* THERMAL EFFECT FRAGMENT SHADER
* THERMAL EFFECT VISIBLE ONLY INSIDE THE LOGO SHAPE
* LOOPS PROCEDURALLY WITH BOTTOM-TO-TOP ANIMATION
*/
PRECISION HIGHP FLOAT;
// INPUT TEXTURES
UNIFORM SAMPLER2D DRAWMAP; // MOUSE INTERACTION HEAT MAP
UNIFORM SAMPLER2D MASKMAP; // LOGO MASK (ALPHA CHANNEL)
UNIFORM FLOAT TIME; // TIME FOR ANIMATION
// ANIMATION / EFFECT PARAMETERS
UNIFORM FLOAT OPACITY;
UNIFORM FLOAT AMOUNT;
UNIFORM VEC2 SCALE;
UNIFORM VEC2 OFFSET;
UNIFORM FLOAT POWER;
UNIFORM FLOAT EFFECTINTENSITY;
UNIFORM FLOAT COLORSATURATION;
UNIFORM FLOAT GRADIENTSHIFT;
UNIFORM FLOAT INTERACTIONSIZE;
// COLOR PALETTE (7-COLOR THERMAL GRADIENT)
UNIFORM VEC3 COLOR1, COLOR2, COLOR3, COLOR4, COLOR5, COLOR6, COLOR7;
UNIFORM VEC4 BLEND, FADE, MAXBLEND;
VARYING VEC2 VUV;
VARYING VEC4 VCLIPPOSITION;
// CONVERT RGB TO LUMINANCE FOR SATURATION ADJUSTMENT
VEC3 LINEARRGBTOLUMINANCE(VEC3 C) {
FLOAT F = DOT(C, VEC3(0.2126729, 0.7151522, 0.0721750));
RETURN VEC3(F);
}
// ADJUST COLOR SATURATION
VEC3 SATURATION(VEC3 C, FLOAT S) {
RETURN MIX(LINEARRGBTOLUMINANCE(C), C, S);
}
// SIMPLE PROCEDURAL NOISE
FLOAT NOISE(VEC2 P) {
RETURN FRACT(SIN(DOT(P, VEC2(12.9898,78.233)))*43758.5453);
}
// SMOOTH NOISE
FLOAT SMOOTHNOISE(VEC2 P) {
VEC2 I = FLOOR(P);
VEC2 F = FRACT(P);
F = F*F*(3.0-2.0*F);
FLOAT A = NOISE(I);
FLOAT B = NOISE(I+VEC2(1.0,0.0));
FLOAT C = NOISE(I+VEC2(0.0,1.0));
FLOAT D = NOISE(I+VEC2(1.0,1.0));
RETURN MIX(MIX(A,B,F.X), MIX(C,D,F.X), F.Y);
}
// THERMAL GRADIENT FUNCTION
VEC3 GRADIENT(FLOAT T) {
T = CLAMP(T + GRADIENTSHIFT, 0.0, 1.0);
FLOAT P1 = BLEND.X, P2 = BLEND.Y, P3 = BLEND.Z, P4 = BLEND.W;
FLOAT P5 = MAXBLEND.X, P6 = MAXBLEND.Y;
FLOAT F1 = FADE.X, F2 = FADE.Y, F3 = FADE.Z, F4 = FADE.W;
FLOAT F5 = MAXBLEND.Z, F6 = MAXBLEND.W;
FLOAT B1 = SMOOTHSTEP(P1-F1*0.5, P1+F1*0.5, T);
FLOAT B2 = SMOOTHSTEP(P2-F2*0.5, P2+F2*0.5, T);
FLOAT B3 = SMOOTHSTEP(P3-F3*0.5, P3+F3*0.5, T);
FLOAT B4 = SMOOTHSTEP(P4-F4*0.5, P4+F4*0.5, T);
FLOAT B5 = SMOOTHSTEP(P5-F5*0.5, P5+F5*0.5, T);
FLOAT B6 = SMOOTHSTEP(P6-F6*0.5, P6+F6*0.5, T);
VEC3 COL = COLOR1;
COL = MIX(COL, COLOR2, B1);
COL = MIX(COL, COLOR3, B2);
COL = MIX(COL, COLOR4, B3);
COL = MIX(COL, COLOR5, B4);
COL = MIX(COL, COLOR6, B5);
COL = MIX(COL, COLOR7, B6);
RETURN COL;
}
VOID MAIN() {
// CLIP SPACE UV FOR DRAWMAP
VEC2 DUV = VCLIPPOSITION.XY / VCLIPPOSITION.W;
DUV = 0.5 + DUV*0.5;
// TRANSFORM UV
VEC2 UV = VUV;
UV -= 0.5;
UV /= SCALE;
UV += 0.5;
UV += OFFSET;
FLOAT O = CLAMP(OPACITY, 0.0, 1.0);
FLOAT A = CLAMP(AMOUNT, 0.0, 1.0);
FLOAT V = O * A;
// SAMPLE LOGO MASK (ALPHA)
VEC4 TEX = TEXTURE(MASKMAP, UV);
FLOAT MASK = TEX.A;
// SAMPLE MOUSE INTERACTION HEAT MAP
VEC3 DRAW = TEXTURE(DRAWMAP, DUV).RGB;
FLOAT HEATDRAW = DRAW.B * MASK * INTERACTIONSIZE;
// ADD PROCEDURAL ANIMATION FOR CONTINUOUS BOTTOM-TO-TOP LOOPING GLOW
FLOAT NOISEANIM = SMOOTHNOISE(UV * 5.0 + VEC2(TIME*1.0, TIME*1.2)); // NOISE FOR ORGANIC TEXTURE
FLOAT WAVEANIM = 0.5 + 0.5 * SIN(TIME * 0.5 + UV.Y * 8.0); // BOTTOM-TO-TOP MOTION (TIME + UV.Y MAKES WAVES MOVE UPWARD)
FLOAT TIMEANIM = MIX(NOISEANIM, WAVEANIM, 1.0); // BLEND WITH MORE WEIGHT ON WAVE FOR CLEARER DIRECTION
HEATDRAW += 0.8 * TIMEANIM; // APPLY ANIMATION WITH STRONG VISIBILITY
// BASE INTENSITY
FLOAT MAP = POW(HEATDRAW, POWER);
// GENERATE FINAL COLOR
VEC3 FINAL = GRADIENT(MAP);
FINAL = SATURATION(FINAL, COLORSATURATION);
// INTENSIFY GLOW EFFECT
FINAL *= MASK * (1.0 + MAP*1.5); // STRONGER GLOW INSIDE LOGO
// BLEND WITH BASE ZERO
FINAL = MIX(VEC3(0.0), FINAL, V * EFFECTINTENSITY);
// APPLY LOGO MASK
FINAL *= MASK;
FLOAT ALPHA = MASK * (O * A * EFFECTINTENSITY);
GL_FRAGCOLOR = VEC4(FINAL, ALPHA);
}
`;
CONST DRAWVERTEXSHADER = `
/**
* DRAW RENDERER VERTEX SHADER
* SIMPLE PASS-THROUGH FOR MOUSE INTERACTION TEXTURE RENDERING
*/
PRECISION HIGHP FLOAT;
VARYING VEC2 VUV;
VOID MAIN() {
VUV = UV;
GL_POSITION = PROJECTIONMATRIX * MODELVIEWMATRIX * VEC4(POSITION, 1.0);
}
`
CONST DRAWFRAGMENTSHADER = `
/**
* DRAW RENDERER FRAGMENT SHADER
*
* CREATES A MOUSE INTERACTION HEAT MAP BY TRACKING CURSOR POSITION AND MOVEMENT.
* USES PING-PONG RENDERING BETWEEN TWO RENDER TARGETS FOR TEMPORAL EFFECTS.
*
* OUTPUTS:
* - R/G CHANNELS: MOVEMENT DIRECTION VECTORS FOR DISTORTION EFFECTS
* - B CHANNEL: HEAT INTENSITY BASED ON INTERACTION
*/
PRECISION HIGHP FLOAT;
UNIFORM FLOAT UDRAW;
UNIFORM VEC3 URADIUS;
UNIFORM VEC3 URESOLUTION;
UNIFORM VEC2 UPOSITION;
UNIFORM VEC4 UDIRECTION;
UNIFORM FLOAT USIZEDAMPING;
UNIFORM FLOAT UFADEDAMPING;
UNIFORM SAMPLER2D UTEXTURE;
VARYING VEC2 VUV;
VOID MAIN() {
FLOAT ASPECT = URESOLUTION.X / URESOLUTION.Y;
VEC2 POS = UPOSITION;
POS.Y /= ASPECT;
VEC2 UV = VUV;
UV.Y /= ASPECT;
FLOAT DIST = DISTANCE(POS, UV) / ((URADIUS.Z * 1.5) / URESOLUTION.X);
DIST = SMOOTHSTEP(URADIUS.X, URADIUS.Y, DIST);
VEC3 DIR = UDIRECTION.XYZ * UDIRECTION.W;
VEC2 OFFSET = VEC2((-DIR.X) * (1.0 - DIST), (DIR.Y) * (1.0 - DIST));
VEC4 COLOR = TEXTURE(UTEXTURE, VUV + (OFFSET * 0.01));
COLOR *= UFADEDAMPING;
COLOR.R += OFFSET.X;
COLOR.G += OFFSET.Y;
COLOR.RG = CLAMP(COLOR.RG, -1.0, 1.0);
COLOR.B += UDRAW * (1.0 - DIST);
GL_FRAGCOLOR = VEC4(COLOR.RGB, 1.0);
}
`
// DEFAULT EFFECT PARAMETERS
CONST DEFAULT_PARAMETERS: EFFECTPARAMETERS = {
// VISUAL PARAMETERS
EFFECTINTENSITY: 1.3,
CONTRASTPOWER: 0.8,
COLORSATURATION: 1.5,
HEATSENSITIVITY: 0.5,
VIDEOBLENDAMOUNT: 1.0,
GRADIENTSHIFT: 0.0,
// BEHAVIORAL PARAMETERS
HEATDECAY: 0.9,
INTERACTIONRADIUS: 1.0,
REACTIVITY: 3.0
} AS CONST
// APPLE'S THERMAL GRADIENT COLORS (BLACK TO RED/ORANGE SPECTRUM)
CONST THERMAL_PALETTE = [
'000000', // BLACK
'073DFF', // DEEP BLUE
'53D5FD', // CYAN
'FEFCDD', // LIGHT YELLOW
'FFEC6A', // YELLOW
'F9D400', // ORANGE
'A61904' // RED
] AS CONST
// ANIMATION TIMING CONSTANTS
CONST ANIMATION = {
FADE_IN_SPEED: 0.1,
MOUSE_INTERPOLATION_SPEED: 0.8,
SCROLL_INTERPOLATION_SPEED: 0.2,
MOVEMENT_INTERPOLATION_SPEED: 0.01,
POWER_INTERPOLATION_SPEED: 0.01,
VIDEO_BLEND_SPEED: 0.1,
HEAT_MAX_VALUE: 1.3,
TARGET_FPS: 60,
// POWER VALUE CONSTRAINTS
POWER_MIN: 0.8,
POWER_MAX: 1.0
} AS CONST
// DRAW RENDERER CONFIGURATION
CONST DRAW_RENDERER = {
TEXTURE_SIZE: 256,
RADIUS_RATIO: 1000,
MOBILE_RADIUS: 350,
DESKTOP_RADIUS: 220,
// SHADER UNIFORM DEFAULTS
UNIFORMS: {
RADIUS_VECTOR: [-8, 0.9, 150] AS CONST,
SIZE_DAMPING: 0.8,
FADE_DAMPING: 0.98,
DIRECTION_MULTIPLIER: 100
}
} AS CONST
// SHADER GRADIENT BLEND PARAMETERS
CONST GRADIENT_CONFIG = {
// BLEND POINTS FOR COLOR TRANSITIONS
BLEND_POINTS: [0.4, 0.7, 0.81, 0.91] AS CONST,
// FADE RANGES FOR SMOOTH BLENDING
FADE_RANGES: [1, 1, 0.72, 0.52] AS CONST,
// MAXIMUM BLEND VALUES
MAX_BLEND: [0.8, 0.87, 0.5, 0.27] AS CONST,
// EFFECT MODIFIERS
VERTICAL_GRADIENT_START: 0.2,
VERTICAL_GRADIENT_END: 0.5,
VERTICAL_GRADIENT_MIX: 0.91,
CIRCULAR_FADE_CENTER: [0.5, 0.52] AS CONST,
CIRCULAR_FADE_INNER: 0.5,
CIRCULAR_FADE_OUTER: 0.62
} AS CONST
// CAMERA CONFIGURATION
CONST CAMERA_CONFIG = {
// ORTHOGRAPHIC CAMERA BOUNDS
LEFT: -0.5,
RIGHT: 0.5,
TOP: 0.5,
BOTTOM: -0.5,
NEAR: -1,
FAR: 1,
POSITION_Z: 1
} AS CONST
// INTERACTION THRESHOLDS AND LIMITS
CONST INTERACTION = {
HEAT_DECAY_MIN: 0.8,
HEAT_DECAY_MAX: 0.99,
HEAT_SENSITIVITY_MIN: 0.1,
HEAT_SENSITIVITY_MAX: 2.0,
INTERACTION_RADIUS_MIN: 0.1,
INTERACTION_RADIUS_MAX: 3.0,
REACTIVITY_MIN: 0.1,
REACTIVITY_MAX: 3.0,
// MOVEMENT-BASED OPACITY MODIFIERS
HOLD_MOVE_TARGET: 0.95,
RELEASE_MOVE_TARGET: 1.0,
HOLD_POWER_TARGET: 1.0,
RELEASE_POWER_TARGET: 0.8,
// HEAT CLEANUP THRESHOLD
HEAT_CLEANUP_THRESHOLD: 0.001
} AS CONST
// EFFECT PARAMETERS THAT CAN BE CONTROLLED VIA UI
INTERFACE EFFECTPARAMETERS {
// VISUAL PARAMETERS
EFFECTINTENSITY: NUMBER
CONTRASTPOWER: NUMBER
COLORSATURATION: NUMBER
HEATSENSITIVITY: NUMBER
VIDEOBLENDAMOUNT: NUMBER
GRADIENTSHIFT: NUMBER
// BEHAVIORAL PARAMETERS
HEATDECAY: NUMBER
INTERACTIONRADIUS: NUMBER
REACTIVITY: NUMBER
}
// SHADER UNIFORM TYPES
// ESLINT-DISABLE-NEXT-LINE @TYPESCRIPT-ESLINT/NO-EXPLICIT-ANY
INTERFACE THERMALSHADERUNIFORMS EXTENDS RECORD<STRING, { VALUE: ANY }> {
// TEXTURE UNIFORMS
BLENDVIDEO: { VALUE: NUMBER }
DRAWMAP: { VALUE: THREE.TEXTURE }
TEXTUREMAP: { VALUE: THREE.TEXTURE }
MASKMAP: { VALUE: THREE.TEXTURE }
// TRANSFORM UNIFORMS
SCALE: { VALUE: [NUMBER, NUMBER] }
OFFSET: { VALUE: [NUMBER, NUMBER] }
OPACITY: { VALUE: NUMBER }
AMOUNT: { VALUE: NUMBER }
// COLOR UNIFORMS (7-COLOR THERMAL GRADIENT)
COLOR1: { VALUE: [NUMBER, NUMBER, NUMBER] }
COLOR2: { VALUE: [NUMBER, NUMBER, NUMBER] }
COLOR3: { VALUE: [NUMBER, NUMBER, NUMBER] }
COLOR4: { VALUE: [NUMBER, NUMBER, NUMBER] }
COLOR5: { VALUE: [NUMBER, NUMBER, NUMBER] }
COLOR6: { VALUE: [NUMBER, NUMBER, NUMBER] }
COLOR7: { VALUE: [NUMBER, NUMBER, NUMBER] }
// GRADIENT BLEND PARAMETERS
BLEND: { VALUE: [NUMBER, NUMBER, NUMBER, NUMBER] }
FADE: { VALUE: [NUMBER, NUMBER, NUMBER, NUMBER] }
MAXBLEND: { VALUE: [NUMBER, NUMBER, NUMBER, NUMBER] }
// EFFECT PARAMETERS
POWER: { VALUE: NUMBER }
RND: { VALUE: NUMBER }
HEAT: { VALUE: [NUMBER, NUMBER, NUMBER, NUMBER] }
STRETCH: { VALUE: [NUMBER, NUMBER, NUMBER, NUMBER] }
// HUD CONTROLLABLE PARAMETERS
EFFECTINTENSITY: { VALUE: NUMBER }
COLORSATURATION: { VALUE: NUMBER }
GRADIENTSHIFT: { VALUE: NUMBER }
INTERACTIONSIZE: { VALUE: NUMBER }
}
// ESLINT-DISABLE-NEXT-LINE @TYPESCRIPT-ESLINT/NO-EXPLICIT-ANY
INTERFACE DRAWRENDERERUNIFORMS EXTENDS RECORD<STRING, { VALUE: ANY }> {
URADIUS: { VALUE: THREE.VECTOR3 }
UPOSITION: { VALUE: THREE.VECTOR2 }
UDIRECTION: { VALUE: THREE.VECTOR4 }
URESOLUTION: { VALUE: THREE.VECTOR3 }
UTEXTURE: { VALUE: THREE.TEXTURE | NULL }
USIZEDAMPING: { VALUE: NUMBER }
UFADEDAMPING: { VALUE: NUMBER }
UDRAW: { VALUE: NUMBER }
}
// ANIMATION STATE
INTERFACE ANIMATIONVALUES {
BLENDVIDEO: { VALUE: NUMBER; TARGET: NUMBER }
AMOUNT: { VALUE: NUMBER; TARGET: NUMBER }
MOUSE: {
POSITION: THREE.VECTOR3
TARGET: THREE.VECTOR3
}
MOVE: { VALUE: NUMBER; TARGET: NUMBER }
SCROLLANIMATION: {
OPACITY: { VALUE: NUMBER; TARGET: NUMBER }
SCALE: { VALUE: NUMBER; TARGET: NUMBER }
POWER: { VALUE: NUMBER; TARGET: NUMBER }
}
}
// INTERACTION STATE
INTERFACE MOUSESTATE {
POSITION: THREE.VECTOR3
TARGET: THREE.VECTOR3
}
INTERFACE INTERACTIONSTATE {
HOLD: BOOLEAN
HEATUP: NUMBER
LASTNX: NUMBER
LASTNY: NUMBER
}
// COMPONENT INTERFACES
INTERFACE DRAWRENDEREROPTIONS {
RADIUSRATIO?: NUMBER
ISMOBILE?: BOOLEAN
}
// CLEANUP INTERFACE
INTERFACE DISPOSABLE {
DISPOSE(): VOID
}
DEMO.TSX
"USE CLIENT"
IMPORT { THERMALEFFECT} FROM "@/COMPONENTS/UI/THERMAL-SHADER"
EXPORT DEFAULT FUNCTION HOME() {
RETURN (
<MAIN CLASSNAME="FLEX PX-40 GAP-10 ITEMS-CENTER W-FULL H-FULL JUSTIFY-CENTER">
<THERMALEFFECT LOGOURL="HTTPS://RAW.GITHUBUSERCONTENT.COM/DALIM-IN/DALIM/REFS/HEADS/MAIN/APPS/UI/PUBLIC/APPLE.PNG "
/>
<P CLASSNAME="TEXT-7XL FONT-THIN">X</P>
<THERMALEFFECT LOGOURL="HTTPS://RAW.GITHUBUSERCONTENT.COM/DALIM-IN/DALIM/REFS/HEADS/MAIN/APPS/UI/PUBLIC/VERCEL.PNG "
/>
</MAIN>
)
}
```
INSTALL NPM DEPENDENCIES:
```bash
THREE
```
IMPLEMENTATION GUIDELINES
1. ANALYZE THE COMPONENT STRUCTURE AND IDENTIFY ALL REQUIRED DEPENDENCIES
2. REVIEW THE COMPONENT'S ARGUMENS AND STATE
3. IDENTIFY ANY REQUIRED CONTEXT PROVIDERS OR HOOKS AND INSTALL THEM
4. QUESTIONS TO ASK
- WHAT DATA/PROPS WILL BE PASSED TO THIS COMPONENT?
- ARE THERE ANY SPECIFIC STATE MANAGEMENT REQUIREMENTS?
- ARE THERE ANY REQUIRED ASSETS (IMAGES, ICONS, ETC.)?
- WHAT IS THE EXPECTED RESPONSIVE BEHAVIOR?
- WHAT IS THE BEST PLACE TO USE THIS COMPONENT IN THE APP?
STEPS TO INTEGRATE
0. COPY PASTE ALL THE CODE ABOVE IN THE CORRECT DIRECTORIES
1. INSTALL EXTERNAL DEPENDENCIES
2. FILL IMAGE ASSETS WITH UNSPLASH STOCK IMAGES YOU KNOW EXIST
3. USE LUCIDE-REACT ICONS FOR SVGS OR LOGOS IF COMPONENT REQUIRES THEMAdds a render step. Works best with the HyperFrames or Remotion skills installed. Free, no sign-up.
Prompt by @pulkitxm. Prompts belong to their creators.