Standardize GlitchComponent: glitch.json config, build/deploy setup, ignore node_modules

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 09:40:21 +02:00
co-authored by Claude Opus 4.8
parent 6ad4f51838
commit c75e156b8f
11 changed files with 602 additions and 221 deletions
+1
View File
@@ -1,2 +1,3 @@
node_modules/ node_modules/
.DS_Store .DS_Store
node_modules
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+95
View File
@@ -0,0 +1,95 @@
{
"componentId": "diophantine-sphere",
"displayName": "Diophantine Sphere",
"description": "Diophantine Sphere glitch component for Glitch University",
"version": "1.0.0",
"folderName": "glitch_diophantine_sphere",
"packageName": "@glitch-components/diophantine-sphere",
"entry": "dist/diophantine-sphere.js",
"source": "src/index.tsx",
"tags": [
"glitch-component",
"diophantine",
"sphere",
"integer",
"visualization"
],
"paramSchema": {
"allowNegative": {
"type": "boolean",
"label": "Allow Negative Values",
"default": true
},
"normalizeDistance": {
"type": "boolean",
"label": "Normalize Distance",
"default": true
},
"showComplexityOverlay": {
"type": "boolean",
"label": "Show Complexity Range",
"default": true
},
"complexityMode": {
"type": "select",
"label": "Complexity Mode",
"default": "surface",
"options": [
{
"value": "surface",
"label": "Surface"
},
{
"value": "single",
"label": "Single C"
},
{
"value": "range",
"label": "Range 1..C"
}
]
},
"radius": {
"type": "range",
"label": "D",
"default": 4,
"min": 1,
"max": 8,
"step": 0.25
},
"complexity": {
"type": "range",
"label": "C",
"default": 17,
"min": 1,
"max": 96,
"step": 1
},
"dotRadius": {
"type": "range",
"label": "R",
"default": 0.5,
"min": 0.05,
"max": 1.2,
"step": 0.01
},
"axisSize": {
"type": "range",
"label": "A",
"default": 1,
"min": 0.25,
"max": 2.5,
"step": 0.05
}
},
"defaultParams": {
"allowNegative": true,
"normalizeDistance": true,
"showComplexityOverlay": true,
"complexityMode": "surface",
"radius": 4,
"complexity": 17,
"dotRadius": 0.5,
"axisSize": 1
}
}
+2 -1
View File
@@ -15,7 +15,8 @@
"files": [ "files": [
"dist", "dist",
"src/types.ts", "src/types.ts",
"glitch.manifest.json" "glitch.manifest.json",
"glitch.json"
], ],
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+274 -43
View File
@@ -7,9 +7,11 @@ import type { GlitchComponentProps } from './types'
interface ComponentParams { interface ComponentParams {
allowNegative?: boolean allowNegative?: boolean
normalizeDistance?: boolean normalizeDistance?: boolean
showComplexityOverlay?: boolean
radius?: number radius?: number
complexity?: number complexity?: number
dotRadius?: number dotRadius?: number
axisSize?: number
complexityMode?: string complexityMode?: string
} }
@@ -47,6 +49,8 @@ const DEFAULT_CAMERA_DISTANCE = 13
const IDLE_ROTATION_SPEED = 0.0005 const IDLE_ROTATION_SPEED = 0.0005
const MAX_COMPLEXITY = 96 const MAX_COMPLEXITY = 96
const MAX_DOT_RADIUS = 1.2 const MAX_DOT_RADIUS = 1.2
const MAX_AXIS_SIZE = 2.5
const INITIAL_ROTATION = { x: -0.25, y: -0.85 }
const gcd2 = (a: number, b: number): number => { const gcd2 = (a: number, b: number): number => {
let left = Math.abs(a) let left = Math.abs(a)
@@ -82,11 +86,40 @@ const colorRamp = (weight: number) => {
return warm.lerp(hot, (weight - 0.5) / 0.5) return warm.lerp(hot, (weight - 0.5) / 0.5)
} }
const createSphereShellGeometry = (allowNegative: boolean) =>
allowNegative
? new THREE.SphereGeometry(1, 96, 48)
: new THREE.SphereGeometry(1, 48, 24, Math.PI / 2, Math.PI / 2, 0, Math.PI / 2)
const createL1ShellGeometry = (allowNegative: boolean) => {
if (allowNegative) return new THREE.OctahedronGeometry(1, 0)
const geometry = new THREE.BufferGeometry()
geometry.setAttribute(
'position',
new THREE.Float32BufferAttribute([
1, 0, 0,
0, 1, 0,
0, 0, 1
], 3)
)
geometry.setIndex([0, 1, 2])
geometry.computeVertexNormals()
return geometry
}
const getComplexityMode = (value: unknown): ComplexityMode => { const getComplexityMode = (value: unknown): ComplexityMode => {
if (value === 'surface' || value === 'single' || value === 'range') return value if (value === 'surface' || value === 'single' || value === 'range') return value
return 'surface' return 'surface'
} }
const getComplexityOverlayText = (mode: ComplexityMode, complexity: number) => {
if (mode === 'surface') return 'Complexity: continuous surface'
if (mode === 'range') return `Complexity range: 1..${complexity}`
return `Complexity: ${complexity}`
}
const getVectorRays = ( const getVectorRays = (
complexity: number, complexity: number,
radius: number, radius: number,
@@ -172,16 +205,20 @@ export default function Component({
const params = config.params as ComponentParams const params = config.params as ComponentParams
const initialAllowNegative = typeof params.allowNegative === 'boolean' ? params.allowNegative : true const initialAllowNegative = typeof params.allowNegative === 'boolean' ? params.allowNegative : true
const initialNormalizeDistance = typeof params.normalizeDistance === 'boolean' ? params.normalizeDistance : true const initialNormalizeDistance = typeof params.normalizeDistance === 'boolean' ? params.normalizeDistance : true
const initialShowComplexityOverlay = typeof params.showComplexityOverlay === 'boolean' ? params.showComplexityOverlay : true
const initialRadius = clamp(numberParam(params.radius, 4), 1, 8) const initialRadius = clamp(numberParam(params.radius, 4), 1, 8)
const initialComplexity = Math.round(clamp(numberParam(params.complexity, 8), 1, MAX_COMPLEXITY)) const initialComplexity = Math.round(clamp(numberParam(params.complexity, 17), 1, MAX_COMPLEXITY))
const initialDotRadius = clamp(numberParam(params.dotRadius, 0.17), 0.05, MAX_DOT_RADIUS) const initialDotRadius = clamp(numberParam(params.dotRadius, 0.5), 0.05, MAX_DOT_RADIUS)
const initialAxisSize = clamp(numberParam(params.axisSize, 1), 0.25, MAX_AXIS_SIZE)
const initialComplexityMode = getComplexityMode(params.complexityMode) const initialComplexityMode = getComplexityMode(params.complexityMode)
const [allowNegative, setAllowNegative] = useState(initialAllowNegative) const [allowNegative, setAllowNegative] = useState(initialAllowNegative)
const [normalizeDistance, setNormalizeDistance] = useState(initialNormalizeDistance) const [normalizeDistance, setNormalizeDistance] = useState(initialNormalizeDistance)
const [showComplexityOverlay, setShowComplexityOverlay] = useState(initialShowComplexityOverlay)
const [radius, setRadius] = useState(initialRadius) const [radius, setRadius] = useState(initialRadius)
const [complexity, setComplexity] = useState(initialComplexity) const [complexity, setComplexity] = useState(initialComplexity)
const [dotRadius, setDotRadius] = useState(initialDotRadius) const [dotRadius, setDotRadius] = useState(initialDotRadius)
const [axisSize, setAxisSize] = useState(initialAxisSize)
const [complexityMode, setComplexityMode] = useState<ComplexityMode>(initialComplexityMode) const [complexityMode, setComplexityMode] = useState<ComplexityMode>(initialComplexityMode)
const [completed, setCompleted] = useState(false) const [completed, setCompleted] = useState(false)
const mountRef = useRef<HTMLDivElement | null>(null) const mountRef = useRef<HTMLDivElement | null>(null)
@@ -191,13 +228,18 @@ export default function Component({
const pointMaterialRef = useRef<THREE.PointsMaterial | null>(null) const pointMaterialRef = useRef<THREE.PointsMaterial | null>(null)
const shellRef = useRef<THREE.Mesh | null>(null) const shellRef = useRef<THREE.Mesh | null>(null)
const l1ShellRef = useRef<THREE.Mesh | null>(null) const l1ShellRef = useRef<THREE.Mesh | null>(null)
const shellRimRef = useRef<THREE.Mesh | null>(null)
const l1ShellRimRef = useRef<THREE.Mesh | null>(null)
const axesRef = useRef<THREE.Group | null>(null)
const originRef = useRef<THREE.Mesh | null>(null)
const dragRef = useRef<DragState | null>(null) const dragRef = useRef<DragState | null>(null)
const pointersRef = useRef<Map<number, PointerPoint>>(new Map()) const pointersRef = useRef<Map<number, PointerPoint>>(new Map())
const pinchRef = useRef<PinchState | null>(null) const pinchRef = useRef<PinchState | null>(null)
const cameraDistanceRef = useRef(DEFAULT_CAMERA_DISTANCE) const cameraDistanceRef = useRef(DEFAULT_CAMERA_DISTANCE)
const currentCameraDistanceRef = useRef(DEFAULT_CAMERA_DISTANCE) const currentCameraDistanceRef = useRef(DEFAULT_CAMERA_DISTANCE)
const idleRotationFactorRef = useRef(1) const idleRotationFactorRef = useRef(1)
const rotationRef = useRef({ x: -0.45, y: 0.58 }) const rotationRef = useRef({ ...INITIAL_ROTATION })
const currentRotationRef = useRef({ ...INITIAL_ROTATION })
useEffect(() => { useEffect(() => {
setAllowNegative(initialAllowNegative) setAllowNegative(initialAllowNegative)
@@ -207,6 +249,10 @@ export default function Component({
setNormalizeDistance(initialNormalizeDistance) setNormalizeDistance(initialNormalizeDistance)
}, [initialNormalizeDistance]) }, [initialNormalizeDistance])
useEffect(() => {
setShowComplexityOverlay(initialShowComplexityOverlay)
}, [initialShowComplexityOverlay])
useEffect(() => { useEffect(() => {
setRadius(initialRadius) setRadius(initialRadius)
}, [initialRadius]) }, [initialRadius])
@@ -219,6 +265,10 @@ export default function Component({
setDotRadius(initialDotRadius) setDotRadius(initialDotRadius)
}, [initialDotRadius]) }, [initialDotRadius])
useEffect(() => {
setAxisSize(initialAxisSize)
}, [initialAxisSize])
useEffect(() => { useEffect(() => {
setComplexityMode(initialComplexityMode) setComplexityMode(initialComplexityMode)
}, [initialComplexityMode]) }, [initialComplexityMode])
@@ -237,6 +287,7 @@ export default function Component({
const statVectors = isSurfaceMode ? '∞' : String(totalVectors) const statVectors = isSurfaceMode ? '∞' : String(totalVectors)
const statOverlap = isSurfaceMode ? 'surface' : String(duplicateCount) const statOverlap = isSurfaceMode ? 'surface' : String(duplicateCount)
const statMaxStack = isSurfaceMode ? '--' : String(maxMultiplicity) const statMaxStack = isSurfaceMode ? '--' : String(maxMultiplicity)
const complexityOverlayText = getComplexityOverlayText(complexityMode, complexity)
const themeStyle = useMemo(() => ({ const themeStyle = useMemo(() => ({
'--gc-primary': theme?.primary, '--gc-primary': theme?.primary,
@@ -264,6 +315,9 @@ export default function Component({
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }) const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)) renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
renderer.setClearColor(0x000000, 0) renderer.setClearColor(0x000000, 0)
renderer.outputColorSpace = THREE.SRGBColorSpace
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 1.2
mount.appendChild(renderer.domElement) mount.appendChild(renderer.domElement)
rendererRef.current = renderer rendererRef.current = renderer
@@ -271,23 +325,25 @@ export default function Component({
scene.add(group) scene.add(group)
groupRef.current = group groupRef.current = group
const ambient = new THREE.AmbientLight(0xf1ffc4, 0.45) const ambient = new THREE.AmbientLight(0xf1ffc4, 0.1)
scene.add(ambient) scene.add(ambient)
const keyLight = new THREE.DirectionalLight(0xf2ff9b, 2.25) const keyLight = new THREE.DirectionalLight(0xf2ff9b, 3.35)
keyLight.position.set(3.5, 4.5, 6) keyLight.position.set(6, 7, 4.5)
scene.add(keyLight) scene.add(keyLight)
const fillLight = new THREE.DirectionalLight(0x79ff8c, 0.85) const fillLight = new THREE.DirectionalLight(0x79ff8c, 0.95)
fillLight.position.set(-5, -2, 3) fillLight.position.set(-6, 1.5, 3)
scene.add(fillLight) scene.add(fillLight)
const rimLight = new THREE.DirectionalLight(0xffffb2, 1.05) const rimLight = new THREE.DirectionalLight(0x9eeaff, 2.35)
rimLight.position.set(-2, 3, -5) rimLight.position.set(-4.5, 5, -6.5)
scene.add(rimLight) scene.add(rimLight)
const shellGeometry = new THREE.SphereGeometry(1, 96, 48) const shellGeometry = createSphereShellGeometry(initialAllowNegative)
const shellMaterial = new THREE.MeshLambertMaterial({ const shellMaterial = new THREE.MeshStandardMaterial({
color: 0xcfff55, color: 0xcfff55,
emissive: 0x16260a, emissive: 0x16260a,
emissiveIntensity: 0.18, emissiveIntensity: 0.045,
roughness: 0.22,
metalness: 0.04,
transparent: true, transparent: true,
opacity: 0.055, opacity: 0.055,
wireframe: true, wireframe: true,
@@ -296,12 +352,26 @@ export default function Component({
const shell = new THREE.Mesh(shellGeometry, shellMaterial) const shell = new THREE.Mesh(shellGeometry, shellMaterial)
group.add(shell) group.add(shell)
shellRef.current = shell shellRef.current = shell
const shellRimMaterial = new THREE.MeshBasicMaterial({
color: 0xeaff83,
transparent: true,
opacity: 0.18,
side: THREE.BackSide,
depthWrite: false,
blending: THREE.AdditiveBlending
})
const shellRim = new THREE.Mesh(shellGeometry.clone(), shellRimMaterial)
shellRim.scale.setScalar(1.018)
group.add(shellRim)
shellRimRef.current = shellRim
const l1ShellGeometry = new THREE.OctahedronGeometry(1, 0) const l1ShellGeometry = createL1ShellGeometry(initialAllowNegative)
const l1ShellMaterial = new THREE.MeshLambertMaterial({ const l1ShellMaterial = new THREE.MeshStandardMaterial({
color: 0xcfff55, color: 0xcfff55,
emissive: 0x16260a, emissive: 0x16260a,
emissiveIntensity: 0.16, emissiveIntensity: 0.035,
roughness: 0.48,
metalness: 0.03,
transparent: true, transparent: true,
opacity: 0.055, opacity: 0.055,
wireframe: true, wireframe: true,
@@ -312,18 +382,58 @@ export default function Component({
l1Shell.visible = false l1Shell.visible = false
group.add(l1Shell) group.add(l1Shell)
l1ShellRef.current = l1Shell l1ShellRef.current = l1Shell
const l1ShellRimMaterial = new THREE.MeshBasicMaterial({
color: 0xeaff83,
transparent: true,
opacity: 0.16,
side: THREE.BackSide,
depthWrite: false,
blending: THREE.AdditiveBlending
})
const l1ShellRim = new THREE.Mesh(l1ShellGeometry.clone(), l1ShellRimMaterial)
l1ShellRim.scale.setScalar(1.018)
l1ShellRim.visible = false
group.add(l1ShellRim)
l1ShellRimRef.current = l1ShellRim
const axisMaterial = new THREE.LineBasicMaterial({ color: 0xd7ff57, transparent: true, opacity: 0.12 }) const axisGeometry = new THREE.CylinderGeometry(0.024, 0.024, 2, 12)
const axisGeometry = new THREE.BufferGeometry().setFromPoints([ const createAxisMaterial = (color: number) => new THREE.MeshBasicMaterial({
new THREE.Vector3(-1, 0, 0), color,
new THREE.Vector3(1, 0, 0), transparent: true,
new THREE.Vector3(0, -1, 0), opacity: 0.68,
new THREE.Vector3(0, 1, 0), depthTest: false,
new THREE.Vector3(0, 0, -1), depthWrite: false
new THREE.Vector3(0, 0, 1) })
]) const axisXMaterial = createAxisMaterial(0xffdb47)
const axes = new THREE.LineSegments(axisGeometry, axisMaterial) const axisYMaterial = createAxisMaterial(0x7dff9a)
const axisZMaterial = createAxisMaterial(0x8ce6ff)
const axes = new THREE.Group()
const xAxis = new THREE.Mesh(axisGeometry, axisXMaterial)
xAxis.rotation.z = Math.PI / 2
const yAxis = new THREE.Mesh(axisGeometry, axisYMaterial)
const zAxis = new THREE.Mesh(axisGeometry, axisZMaterial)
zAxis.rotation.x = Math.PI / 2
for (const axis of [xAxis, yAxis, zAxis]) {
axis.renderOrder = 4
axes.add(axis)
}
axes.renderOrder = 4
group.add(axes) group.add(axes)
axesRef.current = axes
const originGeometry = new THREE.SphereGeometry(0.095, 24, 16)
const originMaterial = new THREE.MeshBasicMaterial({
color: 0xf6ff9a,
transparent: true,
opacity: 0.9,
depthTest: false,
depthWrite: false,
blending: THREE.AdditiveBlending
})
const origin = new THREE.Mesh(originGeometry, originMaterial)
origin.renderOrder = 5
group.add(origin)
originRef.current = origin
const pointMaterial = new THREE.PointsMaterial({ const pointMaterial = new THREE.PointsMaterial({
color: 0xffffff, color: 0xffffff,
@@ -357,19 +467,28 @@ export default function Component({
let animationFrame = 0 let animationFrame = 0
const render = () => { const render = () => {
const rotation = rotationRef.current const targetRotation = rotationRef.current
const currentRotation = currentRotationRef.current
currentCameraDistanceRef.current += (cameraDistanceRef.current - currentCameraDistanceRef.current) * 0.12 currentCameraDistanceRef.current += (cameraDistanceRef.current - currentCameraDistanceRef.current) * 0.12
camera.position.z = currentCameraDistanceRef.current
camera.lookAt(0, 0, 0)
group.rotation.x += (rotation.x - group.rotation.x) * 0.08
group.rotation.y += (rotation.y - group.rotation.y) * 0.08
if (dragRef.current || pinchRef.current) { if (dragRef.current || pinchRef.current) {
idleRotationFactorRef.current = 0 idleRotationFactorRef.current = 0
} else { } else {
idleRotationFactorRef.current += (1 - idleRotationFactorRef.current) * 0.006 idleRotationFactorRef.current += (1 - idleRotationFactorRef.current) * 0.006
group.rotation.y += IDLE_ROTATION_SPEED * idleRotationFactorRef.current targetRotation.y += IDLE_ROTATION_SPEED * idleRotationFactorRef.current
} }
rotation.y = group.rotation.y currentRotation.x += (targetRotation.x - currentRotation.x) * 0.08
currentRotation.y += (targetRotation.y - currentRotation.y) * 0.08
const cameraDistance = currentCameraDistanceRef.current
const vertical = clamp(currentRotation.x, -1.45, 1.45)
const horizontal = currentRotation.y
const orbitRadius = Math.cos(vertical) * cameraDistance
camera.position.set(
Math.sin(horizontal) * orbitRadius,
Math.sin(vertical) * cameraDistance,
Math.cos(horizontal) * orbitRadius
)
camera.lookAt(0, 0, 0)
renderer.render(scene, camera) renderer.render(scene, camera)
animationFrame = window.requestAnimationFrame(render) animationFrame = window.requestAnimationFrame(render)
} }
@@ -382,11 +501,19 @@ export default function Component({
pointGeometry.dispose() pointGeometry.dispose()
pointMaterial.map?.dispose() pointMaterial.map?.dispose()
pointMaterial.dispose() pointMaterial.dispose()
originGeometry.dispose()
originMaterial.dispose()
axisGeometry.dispose() axisGeometry.dispose()
axisMaterial.dispose() axisXMaterial.dispose()
shellGeometry.dispose() axisYMaterial.dispose()
axisZMaterial.dispose()
shell.geometry.dispose()
shellRim.geometry.dispose()
shellRimMaterial.dispose()
shellMaterial.dispose() shellMaterial.dispose()
l1ShellGeometry.dispose() l1Shell.geometry.dispose()
l1ShellRim.geometry.dispose()
l1ShellRimMaterial.dispose()
l1ShellMaterial.dispose() l1ShellMaterial.dispose()
renderer.dispose() renderer.dispose()
rendererRef.current = null rendererRef.current = null
@@ -395,6 +522,10 @@ export default function Component({
pointMaterialRef.current = null pointMaterialRef.current = null
shellRef.current = null shellRef.current = null
l1ShellRef.current = null l1ShellRef.current = null
shellRimRef.current = null
l1ShellRimRef.current = null
axesRef.current = null
originRef.current = null
} }
}, []) }, [])
@@ -422,12 +553,46 @@ export default function Component({
pointCloud.geometry = nextGeometry pointCloud.geometry = nextGeometry
}, [maxMultiplicity, points]) }, [maxMultiplicity, points])
useEffect(() => {
const shell = shellRef.current
const shellRim = shellRimRef.current
const l1Shell = l1ShellRef.current
const l1ShellRim = l1ShellRimRef.current
if (!shell || !shellRim || !l1Shell || !l1ShellRim) return
const nextShellGeometry = createSphereShellGeometry(allowNegative)
const nextL1ShellGeometry = createL1ShellGeometry(allowNegative)
const previousShellGeometry = shell.geometry
const previousShellRimGeometry = shellRim.geometry
const previousL1ShellGeometry = l1Shell.geometry
const previousL1ShellRimGeometry = l1ShellRim.geometry
shell.geometry = nextShellGeometry
shellRim.geometry = nextShellGeometry.clone()
l1Shell.geometry = nextL1ShellGeometry
l1ShellRim.geometry = nextL1ShellGeometry.clone()
for (const material of [shell.material, l1Shell.material]) {
if (material instanceof THREE.MeshStandardMaterial) {
material.side = allowNegative ? THREE.FrontSide : THREE.DoubleSide
material.needsUpdate = true
}
}
previousShellGeometry.dispose()
previousShellRimGeometry.dispose()
previousL1ShellGeometry.dispose()
previousL1ShellRimGeometry.dispose()
}, [allowNegative])
useEffect(() => { useEffect(() => {
shellRef.current?.scale.setScalar(radius) shellRef.current?.scale.setScalar(radius)
l1ShellRef.current?.scale.setScalar(radius) l1ShellRef.current?.scale.setScalar(radius)
const axes = groupRef.current?.children.find((child) => child.type === 'LineSegments') shellRimRef.current?.scale.setScalar(radius * 1.018)
axes?.scale.setScalar(radius * 1.18) l1ShellRimRef.current?.scale.setScalar(radius * 1.018)
}, [radius]) axesRef.current?.children.forEach((axis) => axis.scale.set(axisSize, radius * 1.18, axisSize))
originRef.current?.scale.setScalar(clamp(radius / 4, 0.75, 1.45))
}, [axisSize, radius])
useEffect(() => { useEffect(() => {
if (pointsRef.current) { if (pointsRef.current) {
@@ -436,32 +601,47 @@ export default function Component({
const euclideanShell = shellRef.current const euclideanShell = shellRef.current
const l1Shell = l1ShellRef.current const l1Shell = l1ShellRef.current
const euclideanRim = shellRimRef.current
const l1Rim = l1ShellRimRef.current
if (euclideanShell) { if (euclideanShell) {
euclideanShell.visible = normalizeDistance euclideanShell.visible = normalizeDistance
} }
if (l1Shell) { if (l1Shell) {
l1Shell.visible = !normalizeDistance l1Shell.visible = !normalizeDistance
} }
if (euclideanRim) {
euclideanRim.visible = isSurfaceMode && normalizeDistance && allowNegative
}
if (l1Rim) {
l1Rim.visible = isSurfaceMode && !normalizeDistance && allowNegative
}
for (const shell of [euclideanShell, l1Shell]) { for (const shell of [euclideanShell, l1Shell]) {
const shellMaterial = shell?.material const shellMaterial = shell?.material
if (shellMaterial instanceof THREE.MeshLambertMaterial) { if (shellMaterial instanceof THREE.MeshStandardMaterial) {
shellMaterial.wireframe = !isSurfaceMode shellMaterial.wireframe = !isSurfaceMode
shellMaterial.opacity = isSurfaceMode ? 0.34 : 0.055 shellMaterial.opacity = isSurfaceMode ? 0.5 : 0.055
shellMaterial.depthWrite = false shellMaterial.depthWrite = false
} }
} }
}, [isSurfaceMode, normalizeDistance]) }, [allowNegative, isSurfaceMode, normalizeDistance])
useEffect(() => { useEffect(() => {
const accent = new THREE.Color(theme?.accent ?? '#4de1c1') const accent = new THREE.Color(theme?.accent ?? '#4de1c1')
const rimAccent = accent.clone().lerp(new THREE.Color('#ffffaa'), 0.42)
for (const shell of [shellRef.current, l1ShellRef.current]) { for (const shell of [shellRef.current, l1ShellRef.current]) {
const shellMaterial = shell?.material const shellMaterial = shell?.material
if (shellMaterial instanceof THREE.MeshLambertMaterial) { if (shellMaterial instanceof THREE.MeshStandardMaterial) {
shellMaterial.color.copy(accent) shellMaterial.color.copy(accent)
shellMaterial.emissive.set('#16260a') shellMaterial.emissive.set('#16260a')
} }
} }
for (const shell of [shellRimRef.current, l1ShellRimRef.current]) {
const shellMaterial = shell?.material
if (shellMaterial instanceof THREE.MeshBasicMaterial) {
shellMaterial.color.copy(rimAccent)
}
}
}, [theme?.accent]) }, [theme?.accent])
useEffect(() => { useEffect(() => {
@@ -485,9 +665,11 @@ export default function Component({
data: { data: {
allowNegative, allowNegative,
normalizeDistance, normalizeDistance,
showComplexityOverlay,
complexityMode, complexityMode,
radius, radius,
dotRadius, dotRadius,
axisSize,
complexity, complexity,
rays: points.length, rays: points.length,
vectors: totalVectors, vectors: totalVectors,
@@ -506,6 +688,8 @@ export default function Component({
}) })
}, [ }, [
allowNegative, allowNegative,
showComplexityOverlay,
axisSize,
completed, completed,
complexity, complexity,
config.id, config.id,
@@ -533,6 +717,11 @@ export default function Component({
setNormalizeDistance((value) => !value) setNormalizeDistance((value) => !value)
}, [playClick]) }, [playClick])
const updateShowComplexityOverlay = useCallback(() => {
playClick('complexity-overlay')
setShowComplexityOverlay((value) => !value)
}, [playClick])
const updateRadius = useCallback((value: number) => { const updateRadius = useCallback((value: number) => {
playClick('radius') playClick('radius')
setRadius(clamp(value, 1, 8)) setRadius(clamp(value, 1, 8))
@@ -548,6 +737,11 @@ export default function Component({
setDotRadius(clamp(value, 0.05, MAX_DOT_RADIUS)) setDotRadius(clamp(value, 0.05, MAX_DOT_RADIUS))
}, [playClick]) }, [playClick])
const updateAxisSize = useCallback((value: number) => {
playClick('axis-size')
setAxisSize(clamp(value, 0.25, MAX_AXIS_SIZE))
}, [playClick])
const updateComplexityMode = useCallback((nextMode: ComplexityMode) => { const updateComplexityMode = useCallback((nextMode: ComplexityMode) => {
playClick(`${nextMode}-mode`) playClick(`${nextMode}-mode`)
setComplexityMode(nextMode) setComplexityMode(nextMode)
@@ -693,6 +887,11 @@ export default function Component({
<span>y</span> <span>y</span>
<span>z</span> <span>z</span>
</div> </div>
{showComplexityOverlay && (
<div className={styles.complexityOverlay} aria-live="polite">
{complexityOverlayText}
</div>
)}
</section> </section>
<aside className={styles.controlPanel}> <aside className={styles.controlPanel}>
@@ -738,6 +937,14 @@ export default function Component({
<input type="checkbox" checked={normalizeDistance} onChange={updateNormalizeDistance} /> <input type="checkbox" checked={normalizeDistance} onChange={updateNormalizeDistance} />
</label> </label>
<label className={styles.toggleRow}>
<span>
<strong>Show complexity range</strong>
<small>{showComplexityOverlay ? 'Visible' : 'Hidden'}</small>
</span>
<input type="checkbox" checked={showComplexityOverlay} onChange={updateShowComplexityOverlay} />
</label>
<label className={styles.sliderRow}> <label className={styles.sliderRow}>
<span> <span>
<strong>D</strong> <strong>D</strong>
@@ -810,6 +1017,30 @@ export default function Component({
</div> </div>
</label> </label>
<label className={styles.sliderRow}>
<span>
<strong>A</strong>
<small>{axisSize.toFixed(2)}</small>
</span>
<div className={styles.sliderControl}>
<button type="button" onClick={() => updateAxisSize(axisSize - 0.05)} aria-label="Decrease axis size">
-
</button>
<input
type="range"
min={0.25}
max={MAX_AXIS_SIZE}
step={0.05}
value={axisSize}
onInput={(event) => updateAxisSize(Number(event.currentTarget.value))}
onChange={(event) => updateAxisSize(Number(event.currentTarget.value))}
/>
<button type="button" onClick={() => updateAxisSize(axisSize + 0.05)} aria-label="Increase axis size">
+
</button>
</div>
</label>
<div className={styles.rayNote}> <div className={styles.rayNote}>
<span className={styles.kicker}>colour rule</span> <span className={styles.kicker}>colour rule</span>
<p> <p>
+4 -2
View File
@@ -9,6 +9,7 @@ function DevHarness() {
const params = useControls({ const params = useControls({
allowNegative: { value: true }, allowNegative: { value: true },
normalizeDistance: { value: true }, normalizeDistance: { value: true },
showComplexityOverlay: { value: true },
complexityMode: { complexityMode: {
value: 'surface', value: 'surface',
options: { options: {
@@ -18,8 +19,9 @@ function DevHarness() {
} }
}, },
radius: { value: 4, min: 1, max: 8, step: 0.25 }, radius: { value: 4, min: 1, max: 8, step: 0.25 },
complexity: { value: 8, min: 1, max: 96, step: 1 }, complexity: { value: 17, min: 1, max: 96, step: 1 },
dotRadius: { value: 0.17, min: 0.05, max: 1.2, step: 0.01 } dotRadius: { value: 0.5, min: 0.05, max: 1.2, step: 0.01 },
axisSize: { value: 1, min: 0.25, max: 2.5, step: 0.05 }
}) })
return ( return (
+19 -4
View File
@@ -18,6 +18,11 @@ export const metadata: GlitchComponentMetadata = {
label: 'Normalize Distance', label: 'Normalize Distance',
default: true default: true
}, },
showComplexityOverlay: {
type: 'boolean',
label: 'Show Complexity Range',
default: true
},
complexityMode: { complexityMode: {
type: 'select', type: 'select',
label: 'Complexity Mode', label: 'Complexity Mode',
@@ -39,7 +44,7 @@ export const metadata: GlitchComponentMetadata = {
complexity: { complexity: {
type: 'range', type: 'range',
label: 'C', label: 'C',
default: 8, default: 17,
min: 1, min: 1,
max: 96, max: 96,
step: 1 step: 1
@@ -47,19 +52,29 @@ export const metadata: GlitchComponentMetadata = {
dotRadius: { dotRadius: {
type: 'range', type: 'range',
label: 'R', label: 'R',
default: 0.17, default: 0.5,
min: 0.05, min: 0.05,
max: 1.2, max: 1.2,
step: 0.01 step: 0.01
},
axisSize: {
type: 'range',
label: 'A',
default: 1,
min: 0.25,
max: 2.5,
step: 0.05
} }
}, },
defaultParams: { defaultParams: {
allowNegative: true, allowNegative: true,
normalizeDistance: true, normalizeDistance: true,
showComplexityOverlay: true,
complexityMode: 'surface', complexityMode: 'surface',
radius: 4, radius: 4,
complexity: 8, complexity: 17,
dotRadius: 0.17 dotRadius: 0.5,
axisSize: 1
} }
} }
+51 -16
View File
@@ -10,6 +10,8 @@
--gc-font-display: var(--font-display, var(--gc-font-main)); --gc-font-display: var(--font-display, var(--gc-font-main));
--gc-font-mono: var(--font-mono, monospace); --gc-font-mono: var(--font-mono, monospace);
--gc-shell-padding: clamp(8px, 2vw, 18px); --gc-shell-padding: clamp(8px, 2vw, 18px);
--gc-viewport-size: 1000px;
--gc-control-width: 14em;
width: 100%; width: 100%;
min-height: 100dvh; min-height: 100dvh;
display: grid; display: grid;
@@ -26,9 +28,8 @@
.frame { .frame {
position: relative; position: relative;
width: min(100%, calc((100dvh - (var(--gc-shell-padding) * 2)) * 1.9)); width: calc(var(--gc-viewport-size) + var(--gc-control-width) + 2.7em);
max-height: calc(100dvh - (var(--gc-shell-padding) * 2)); height: calc(var(--gc-viewport-size) + 2em);
aspect-ratio: 1.9 / 1;
display: grid; display: grid;
grid-template-rows: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr);
overflow: hidden; overflow: hidden;
@@ -65,7 +66,7 @@
z-index: 5; z-index: 5;
left: 1em; left: 1em;
top: 0.9em; top: 0.9em;
width: min(25em, 34%); width: var(--gc-control-width);
pointer-events: none; pointer-events: none;
} }
@@ -92,7 +93,7 @@
z-index: 5; z-index: 5;
left: 1em; left: 1em;
bottom: 1em; bottom: 1em;
width: min(25em, 34%); width: var(--gc-control-width);
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.4em; gap: 0.4em;
@@ -129,7 +130,7 @@
min-height: 0; min-height: 0;
grid-row: 1; grid-row: 1;
display: grid; display: grid;
grid-template-columns: minmax(16em, 0.48fr) minmax(0, 1fr); grid-template-columns: var(--gc-control-width) var(--gc-viewport-size);
align-items: center; align-items: center;
gap: 0.7em; gap: 0.7em;
} }
@@ -146,10 +147,13 @@
grid-column: 2; grid-column: 2;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
width: min(100%, calc(100dvh - 5.2em)); width: var(--gc-viewport-size);
height: var(--gc-viewport-size);
aspect-ratio: 1; aspect-ratio: 1;
justify-self: end; justify-self: end;
overflow: hidden; overflow: hidden;
border: 3px solid #ffbd16;
box-shadow: 0 0 0 1px rgb(255 189 22 / 18%);
cursor: grab; cursor: grab;
touch-action: none; touch-action: none;
} }
@@ -186,7 +190,8 @@
.axisLabels { .axisLabels {
position: absolute; position: absolute;
inset: 0.8em; left: 1.9em;
bottom: 2.25em;
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
justify-content: flex-start; justify-content: flex-start;
@@ -208,6 +213,25 @@
text-transform: uppercase; text-transform: uppercase;
} }
.complexityOverlay {
position: absolute;
z-index: 3;
right: 1.9em;
bottom: 2.25em;
max-width: min(24em, calc(100% - 15em));
padding: 0.52em 0.72em;
border: 1px solid color-mix(in srgb, var(--gc-accent) 34%, transparent);
background: rgb(5 10 14 / 72%);
color: color-mix(in srgb, var(--gc-accent) 84%, white);
font-family: var(--gc-font-mono);
font-size: 0.72em;
font-weight: 800;
letter-spacing: 0.04em;
line-height: 1.15;
text-transform: uppercase;
pointer-events: none;
}
.controlPanel { .controlPanel {
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1;
@@ -215,8 +239,8 @@
min-height: 0; min-height: 0;
display: grid; display: grid;
align-content: start; align-content: start;
gap: 0.55em; gap: 0.3em;
padding: 0.7em; padding: 0.46em;
margin-top: 4.2em; margin-top: 4.2em;
margin-bottom: 6.8em; margin-bottom: 6.8em;
} }
@@ -250,8 +274,8 @@
.toggleRow, .toggleRow,
.sliderRow { .sliderRow {
display: grid; display: grid;
gap: 0.4em; gap: 0.28em;
padding: 0.58em; padding: 0.4em;
border: 1px solid color-mix(in srgb, var(--gc-border) 72%, transparent); border: 1px solid color-mix(in srgb, var(--gc-border) 72%, transparent);
background: rgb(5 10 14 / 54%); background: rgb(5 10 14 / 54%);
} }
@@ -337,12 +361,12 @@
display: grid; display: grid;
grid-template-columns: auto minmax(0, 1fr) auto; grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center; align-items: center;
gap: 0.45em; gap: 0.28em;
} }
.sliderControl button { .sliderControl button {
width: 1.9em; width: 1.55em;
height: 1.9em; height: 1.55em;
display: grid; display: grid;
place-items: center; place-items: center;
border: 1px solid color-mix(in srgb, var(--gc-border) 78%, transparent); border: 1px solid color-mix(in srgb, var(--gc-border) 78%, transparent);
@@ -350,7 +374,7 @@
color: var(--gc-text); color: var(--gc-text);
font: inherit; font: inherit;
font-family: var(--gc-font-mono); font-family: var(--gc-font-mono);
font-size: 0.72em; font-size: 0.68em;
font-weight: 900; font-weight: 900;
cursor: pointer; cursor: pointer;
} }
@@ -426,6 +450,17 @@
justify-self: center; justify-self: center;
} }
.axisLabels {
left: 1.45em;
bottom: 1.8em;
}
.complexityOverlay {
right: 1.45em;
bottom: 4.4em;
max-width: calc(100% - 2.9em);
}
.controlPanel { .controlPanel {
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1;
+1
View File
@@ -19,6 +19,7 @@ export default defineConfig(({ mode }) => ({
react: 'React', react: 'React',
'react-dom': 'ReactDOM' 'react-dom': 'ReactDOM'
}, },
entryFileNames: 'diophantine-sphere.js',
assetFileNames: 'assets/[name][extname]' assetFileNames: 'assets/[name][extname]'
} }
}, },