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:
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
.DS_Store
|
||||
node_modules
|
||||
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
+154
-154
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+95
@@ -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
@@ -15,7 +15,8 @@
|
||||
"files": [
|
||||
"dist",
|
||||
"src/types.ts",
|
||||
"glitch.manifest.json"
|
||||
"glitch.manifest.json",
|
||||
"glitch.json"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
+274
-43
@@ -7,9 +7,11 @@ import type { GlitchComponentProps } from './types'
|
||||
interface ComponentParams {
|
||||
allowNegative?: boolean
|
||||
normalizeDistance?: boolean
|
||||
showComplexityOverlay?: boolean
|
||||
radius?: number
|
||||
complexity?: number
|
||||
dotRadius?: number
|
||||
axisSize?: number
|
||||
complexityMode?: string
|
||||
}
|
||||
|
||||
@@ -47,6 +49,8 @@ const DEFAULT_CAMERA_DISTANCE = 13
|
||||
const IDLE_ROTATION_SPEED = 0.0005
|
||||
const MAX_COMPLEXITY = 96
|
||||
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 => {
|
||||
let left = Math.abs(a)
|
||||
@@ -82,11 +86,40 @@ const colorRamp = (weight: number) => {
|
||||
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 => {
|
||||
if (value === 'surface' || value === 'single' || value === 'range') return value
|
||||
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 = (
|
||||
complexity: number,
|
||||
radius: number,
|
||||
@@ -172,16 +205,20 @@ export default function Component({
|
||||
const params = config.params as ComponentParams
|
||||
const initialAllowNegative = typeof params.allowNegative === 'boolean' ? params.allowNegative : 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 initialComplexity = Math.round(clamp(numberParam(params.complexity, 8), 1, MAX_COMPLEXITY))
|
||||
const initialDotRadius = clamp(numberParam(params.dotRadius, 0.17), 0.05, MAX_DOT_RADIUS)
|
||||
const initialComplexity = Math.round(clamp(numberParam(params.complexity, 17), 1, MAX_COMPLEXITY))
|
||||
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 [allowNegative, setAllowNegative] = useState(initialAllowNegative)
|
||||
const [normalizeDistance, setNormalizeDistance] = useState(initialNormalizeDistance)
|
||||
const [showComplexityOverlay, setShowComplexityOverlay] = useState(initialShowComplexityOverlay)
|
||||
const [radius, setRadius] = useState(initialRadius)
|
||||
const [complexity, setComplexity] = useState(initialComplexity)
|
||||
const [dotRadius, setDotRadius] = useState(initialDotRadius)
|
||||
const [axisSize, setAxisSize] = useState(initialAxisSize)
|
||||
const [complexityMode, setComplexityMode] = useState<ComplexityMode>(initialComplexityMode)
|
||||
const [completed, setCompleted] = useState(false)
|
||||
const mountRef = useRef<HTMLDivElement | null>(null)
|
||||
@@ -191,13 +228,18 @@ export default function Component({
|
||||
const pointMaterialRef = useRef<THREE.PointsMaterial | null>(null)
|
||||
const shellRef = 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 pointersRef = useRef<Map<number, PointerPoint>>(new Map())
|
||||
const pinchRef = useRef<PinchState | null>(null)
|
||||
const cameraDistanceRef = useRef(DEFAULT_CAMERA_DISTANCE)
|
||||
const currentCameraDistanceRef = useRef(DEFAULT_CAMERA_DISTANCE)
|
||||
const idleRotationFactorRef = useRef(1)
|
||||
const rotationRef = useRef({ x: -0.45, y: 0.58 })
|
||||
const rotationRef = useRef({ ...INITIAL_ROTATION })
|
||||
const currentRotationRef = useRef({ ...INITIAL_ROTATION })
|
||||
|
||||
useEffect(() => {
|
||||
setAllowNegative(initialAllowNegative)
|
||||
@@ -207,6 +249,10 @@ export default function Component({
|
||||
setNormalizeDistance(initialNormalizeDistance)
|
||||
}, [initialNormalizeDistance])
|
||||
|
||||
useEffect(() => {
|
||||
setShowComplexityOverlay(initialShowComplexityOverlay)
|
||||
}, [initialShowComplexityOverlay])
|
||||
|
||||
useEffect(() => {
|
||||
setRadius(initialRadius)
|
||||
}, [initialRadius])
|
||||
@@ -219,6 +265,10 @@ export default function Component({
|
||||
setDotRadius(initialDotRadius)
|
||||
}, [initialDotRadius])
|
||||
|
||||
useEffect(() => {
|
||||
setAxisSize(initialAxisSize)
|
||||
}, [initialAxisSize])
|
||||
|
||||
useEffect(() => {
|
||||
setComplexityMode(initialComplexityMode)
|
||||
}, [initialComplexityMode])
|
||||
@@ -237,6 +287,7 @@ export default function Component({
|
||||
const statVectors = isSurfaceMode ? '∞' : String(totalVectors)
|
||||
const statOverlap = isSurfaceMode ? 'surface' : String(duplicateCount)
|
||||
const statMaxStack = isSurfaceMode ? '--' : String(maxMultiplicity)
|
||||
const complexityOverlayText = getComplexityOverlayText(complexityMode, complexity)
|
||||
|
||||
const themeStyle = useMemo(() => ({
|
||||
'--gc-primary': theme?.primary,
|
||||
@@ -264,6 +315,9 @@ export default function Component({
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
|
||||
renderer.setClearColor(0x000000, 0)
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||
renderer.toneMappingExposure = 1.2
|
||||
mount.appendChild(renderer.domElement)
|
||||
rendererRef.current = renderer
|
||||
|
||||
@@ -271,23 +325,25 @@ export default function Component({
|
||||
scene.add(group)
|
||||
groupRef.current = group
|
||||
|
||||
const ambient = new THREE.AmbientLight(0xf1ffc4, 0.45)
|
||||
const ambient = new THREE.AmbientLight(0xf1ffc4, 0.1)
|
||||
scene.add(ambient)
|
||||
const keyLight = new THREE.DirectionalLight(0xf2ff9b, 2.25)
|
||||
keyLight.position.set(3.5, 4.5, 6)
|
||||
const keyLight = new THREE.DirectionalLight(0xf2ff9b, 3.35)
|
||||
keyLight.position.set(6, 7, 4.5)
|
||||
scene.add(keyLight)
|
||||
const fillLight = new THREE.DirectionalLight(0x79ff8c, 0.85)
|
||||
fillLight.position.set(-5, -2, 3)
|
||||
const fillLight = new THREE.DirectionalLight(0x79ff8c, 0.95)
|
||||
fillLight.position.set(-6, 1.5, 3)
|
||||
scene.add(fillLight)
|
||||
const rimLight = new THREE.DirectionalLight(0xffffb2, 1.05)
|
||||
rimLight.position.set(-2, 3, -5)
|
||||
const rimLight = new THREE.DirectionalLight(0x9eeaff, 2.35)
|
||||
rimLight.position.set(-4.5, 5, -6.5)
|
||||
scene.add(rimLight)
|
||||
|
||||
const shellGeometry = new THREE.SphereGeometry(1, 96, 48)
|
||||
const shellMaterial = new THREE.MeshLambertMaterial({
|
||||
const shellGeometry = createSphereShellGeometry(initialAllowNegative)
|
||||
const shellMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0xcfff55,
|
||||
emissive: 0x16260a,
|
||||
emissiveIntensity: 0.18,
|
||||
emissiveIntensity: 0.045,
|
||||
roughness: 0.22,
|
||||
metalness: 0.04,
|
||||
transparent: true,
|
||||
opacity: 0.055,
|
||||
wireframe: true,
|
||||
@@ -296,12 +352,26 @@ export default function Component({
|
||||
const shell = new THREE.Mesh(shellGeometry, shellMaterial)
|
||||
group.add(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 l1ShellMaterial = new THREE.MeshLambertMaterial({
|
||||
const l1ShellGeometry = createL1ShellGeometry(initialAllowNegative)
|
||||
const l1ShellMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0xcfff55,
|
||||
emissive: 0x16260a,
|
||||
emissiveIntensity: 0.16,
|
||||
emissiveIntensity: 0.035,
|
||||
roughness: 0.48,
|
||||
metalness: 0.03,
|
||||
transparent: true,
|
||||
opacity: 0.055,
|
||||
wireframe: true,
|
||||
@@ -312,18 +382,58 @@ export default function Component({
|
||||
l1Shell.visible = false
|
||||
group.add(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.BufferGeometry().setFromPoints([
|
||||
new THREE.Vector3(-1, 0, 0),
|
||||
new THREE.Vector3(1, 0, 0),
|
||||
new THREE.Vector3(0, -1, 0),
|
||||
new THREE.Vector3(0, 1, 0),
|
||||
new THREE.Vector3(0, 0, -1),
|
||||
new THREE.Vector3(0, 0, 1)
|
||||
])
|
||||
const axes = new THREE.LineSegments(axisGeometry, axisMaterial)
|
||||
const axisGeometry = new THREE.CylinderGeometry(0.024, 0.024, 2, 12)
|
||||
const createAxisMaterial = (color: number) => new THREE.MeshBasicMaterial({
|
||||
color,
|
||||
transparent: true,
|
||||
opacity: 0.68,
|
||||
depthTest: false,
|
||||
depthWrite: false
|
||||
})
|
||||
const axisXMaterial = createAxisMaterial(0xffdb47)
|
||||
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)
|
||||
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({
|
||||
color: 0xffffff,
|
||||
@@ -357,19 +467,28 @@ export default function Component({
|
||||
|
||||
let animationFrame = 0
|
||||
const render = () => {
|
||||
const rotation = rotationRef.current
|
||||
const targetRotation = rotationRef.current
|
||||
const currentRotation = currentRotationRef.current
|
||||
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) {
|
||||
idleRotationFactorRef.current = 0
|
||||
} else {
|
||||
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)
|
||||
animationFrame = window.requestAnimationFrame(render)
|
||||
}
|
||||
@@ -382,11 +501,19 @@ export default function Component({
|
||||
pointGeometry.dispose()
|
||||
pointMaterial.map?.dispose()
|
||||
pointMaterial.dispose()
|
||||
originGeometry.dispose()
|
||||
originMaterial.dispose()
|
||||
axisGeometry.dispose()
|
||||
axisMaterial.dispose()
|
||||
shellGeometry.dispose()
|
||||
axisXMaterial.dispose()
|
||||
axisYMaterial.dispose()
|
||||
axisZMaterial.dispose()
|
||||
shell.geometry.dispose()
|
||||
shellRim.geometry.dispose()
|
||||
shellRimMaterial.dispose()
|
||||
shellMaterial.dispose()
|
||||
l1ShellGeometry.dispose()
|
||||
l1Shell.geometry.dispose()
|
||||
l1ShellRim.geometry.dispose()
|
||||
l1ShellRimMaterial.dispose()
|
||||
l1ShellMaterial.dispose()
|
||||
renderer.dispose()
|
||||
rendererRef.current = null
|
||||
@@ -395,6 +522,10 @@ export default function Component({
|
||||
pointMaterialRef.current = null
|
||||
shellRef.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
|
||||
}, [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(() => {
|
||||
shellRef.current?.scale.setScalar(radius)
|
||||
l1ShellRef.current?.scale.setScalar(radius)
|
||||
const axes = groupRef.current?.children.find((child) => child.type === 'LineSegments')
|
||||
axes?.scale.setScalar(radius * 1.18)
|
||||
}, [radius])
|
||||
shellRimRef.current?.scale.setScalar(radius * 1.018)
|
||||
l1ShellRimRef.current?.scale.setScalar(radius * 1.018)
|
||||
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(() => {
|
||||
if (pointsRef.current) {
|
||||
@@ -436,32 +601,47 @@ export default function Component({
|
||||
|
||||
const euclideanShell = shellRef.current
|
||||
const l1Shell = l1ShellRef.current
|
||||
const euclideanRim = shellRimRef.current
|
||||
const l1Rim = l1ShellRimRef.current
|
||||
if (euclideanShell) {
|
||||
euclideanShell.visible = normalizeDistance
|
||||
}
|
||||
if (l1Shell) {
|
||||
l1Shell.visible = !normalizeDistance
|
||||
}
|
||||
if (euclideanRim) {
|
||||
euclideanRim.visible = isSurfaceMode && normalizeDistance && allowNegative
|
||||
}
|
||||
if (l1Rim) {
|
||||
l1Rim.visible = isSurfaceMode && !normalizeDistance && allowNegative
|
||||
}
|
||||
|
||||
for (const shell of [euclideanShell, l1Shell]) {
|
||||
const shellMaterial = shell?.material
|
||||
if (shellMaterial instanceof THREE.MeshLambertMaterial) {
|
||||
if (shellMaterial instanceof THREE.MeshStandardMaterial) {
|
||||
shellMaterial.wireframe = !isSurfaceMode
|
||||
shellMaterial.opacity = isSurfaceMode ? 0.34 : 0.055
|
||||
shellMaterial.opacity = isSurfaceMode ? 0.5 : 0.055
|
||||
shellMaterial.depthWrite = false
|
||||
}
|
||||
}
|
||||
}, [isSurfaceMode, normalizeDistance])
|
||||
}, [allowNegative, isSurfaceMode, normalizeDistance])
|
||||
|
||||
useEffect(() => {
|
||||
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]) {
|
||||
const shellMaterial = shell?.material
|
||||
if (shellMaterial instanceof THREE.MeshLambertMaterial) {
|
||||
if (shellMaterial instanceof THREE.MeshStandardMaterial) {
|
||||
shellMaterial.color.copy(accent)
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -485,9 +665,11 @@ export default function Component({
|
||||
data: {
|
||||
allowNegative,
|
||||
normalizeDistance,
|
||||
showComplexityOverlay,
|
||||
complexityMode,
|
||||
radius,
|
||||
dotRadius,
|
||||
axisSize,
|
||||
complexity,
|
||||
rays: points.length,
|
||||
vectors: totalVectors,
|
||||
@@ -506,6 +688,8 @@ export default function Component({
|
||||
})
|
||||
}, [
|
||||
allowNegative,
|
||||
showComplexityOverlay,
|
||||
axisSize,
|
||||
completed,
|
||||
complexity,
|
||||
config.id,
|
||||
@@ -533,6 +717,11 @@ export default function Component({
|
||||
setNormalizeDistance((value) => !value)
|
||||
}, [playClick])
|
||||
|
||||
const updateShowComplexityOverlay = useCallback(() => {
|
||||
playClick('complexity-overlay')
|
||||
setShowComplexityOverlay((value) => !value)
|
||||
}, [playClick])
|
||||
|
||||
const updateRadius = useCallback((value: number) => {
|
||||
playClick('radius')
|
||||
setRadius(clamp(value, 1, 8))
|
||||
@@ -548,6 +737,11 @@ export default function Component({
|
||||
setDotRadius(clamp(value, 0.05, MAX_DOT_RADIUS))
|
||||
}, [playClick])
|
||||
|
||||
const updateAxisSize = useCallback((value: number) => {
|
||||
playClick('axis-size')
|
||||
setAxisSize(clamp(value, 0.25, MAX_AXIS_SIZE))
|
||||
}, [playClick])
|
||||
|
||||
const updateComplexityMode = useCallback((nextMode: ComplexityMode) => {
|
||||
playClick(`${nextMode}-mode`)
|
||||
setComplexityMode(nextMode)
|
||||
@@ -693,6 +887,11 @@ export default function Component({
|
||||
<span>y</span>
|
||||
<span>z</span>
|
||||
</div>
|
||||
{showComplexityOverlay && (
|
||||
<div className={styles.complexityOverlay} aria-live="polite">
|
||||
{complexityOverlayText}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<aside className={styles.controlPanel}>
|
||||
@@ -738,6 +937,14 @@ export default function Component({
|
||||
<input type="checkbox" checked={normalizeDistance} onChange={updateNormalizeDistance} />
|
||||
</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}>
|
||||
<span>
|
||||
<strong>D</strong>
|
||||
@@ -810,6 +1017,30 @@ export default function Component({
|
||||
</div>
|
||||
</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}>
|
||||
<span className={styles.kicker}>colour rule</span>
|
||||
<p>
|
||||
|
||||
+4
-2
@@ -9,6 +9,7 @@ function DevHarness() {
|
||||
const params = useControls({
|
||||
allowNegative: { value: true },
|
||||
normalizeDistance: { value: true },
|
||||
showComplexityOverlay: { value: true },
|
||||
complexityMode: {
|
||||
value: 'surface',
|
||||
options: {
|
||||
@@ -18,8 +19,9 @@ function DevHarness() {
|
||||
}
|
||||
},
|
||||
radius: { value: 4, min: 1, max: 8, step: 0.25 },
|
||||
complexity: { value: 8, min: 1, max: 96, step: 1 },
|
||||
dotRadius: { value: 0.17, min: 0.05, max: 1.2, step: 0.01 }
|
||||
complexity: { value: 17, min: 1, max: 96, step: 1 },
|
||||
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 (
|
||||
|
||||
+19
-4
@@ -18,6 +18,11 @@ export const metadata: GlitchComponentMetadata = {
|
||||
label: 'Normalize Distance',
|
||||
default: true
|
||||
},
|
||||
showComplexityOverlay: {
|
||||
type: 'boolean',
|
||||
label: 'Show Complexity Range',
|
||||
default: true
|
||||
},
|
||||
complexityMode: {
|
||||
type: 'select',
|
||||
label: 'Complexity Mode',
|
||||
@@ -39,7 +44,7 @@ export const metadata: GlitchComponentMetadata = {
|
||||
complexity: {
|
||||
type: 'range',
|
||||
label: 'C',
|
||||
default: 8,
|
||||
default: 17,
|
||||
min: 1,
|
||||
max: 96,
|
||||
step: 1
|
||||
@@ -47,19 +52,29 @@ export const metadata: GlitchComponentMetadata = {
|
||||
dotRadius: {
|
||||
type: 'range',
|
||||
label: 'R',
|
||||
default: 0.17,
|
||||
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: 8,
|
||||
dotRadius: 0.17
|
||||
complexity: 17,
|
||||
dotRadius: 0.5,
|
||||
axisSize: 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+51
-16
@@ -10,6 +10,8 @@
|
||||
--gc-font-display: var(--font-display, var(--gc-font-main));
|
||||
--gc-font-mono: var(--font-mono, monospace);
|
||||
--gc-shell-padding: clamp(8px, 2vw, 18px);
|
||||
--gc-viewport-size: 1000px;
|
||||
--gc-control-width: 14em;
|
||||
width: 100%;
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
@@ -26,9 +28,8 @@
|
||||
|
||||
.frame {
|
||||
position: relative;
|
||||
width: min(100%, calc((100dvh - (var(--gc-shell-padding) * 2)) * 1.9));
|
||||
max-height: calc(100dvh - (var(--gc-shell-padding) * 2));
|
||||
aspect-ratio: 1.9 / 1;
|
||||
width: calc(var(--gc-viewport-size) + var(--gc-control-width) + 2.7em);
|
||||
height: calc(var(--gc-viewport-size) + 2em);
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
@@ -65,7 +66,7 @@
|
||||
z-index: 5;
|
||||
left: 1em;
|
||||
top: 0.9em;
|
||||
width: min(25em, 34%);
|
||||
width: var(--gc-control-width);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@
|
||||
z-index: 5;
|
||||
left: 1em;
|
||||
bottom: 1em;
|
||||
width: min(25em, 34%);
|
||||
width: var(--gc-control-width);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.4em;
|
||||
@@ -129,7 +130,7 @@
|
||||
min-height: 0;
|
||||
grid-row: 1;
|
||||
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;
|
||||
gap: 0.7em;
|
||||
}
|
||||
@@ -146,10 +147,13 @@
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
width: min(100%, calc(100dvh - 5.2em));
|
||||
width: var(--gc-viewport-size);
|
||||
height: var(--gc-viewport-size);
|
||||
aspect-ratio: 1;
|
||||
justify-self: end;
|
||||
overflow: hidden;
|
||||
border: 3px solid #ffbd16;
|
||||
box-shadow: 0 0 0 1px rgb(255 189 22 / 18%);
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
@@ -186,7 +190,8 @@
|
||||
|
||||
.axisLabels {
|
||||
position: absolute;
|
||||
inset: 0.8em;
|
||||
left: 1.9em;
|
||||
bottom: 2.25em;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
@@ -208,6 +213,25 @@
|
||||
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 {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
@@ -215,8 +239,8 @@
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0.55em;
|
||||
padding: 0.7em;
|
||||
gap: 0.3em;
|
||||
padding: 0.46em;
|
||||
margin-top: 4.2em;
|
||||
margin-bottom: 6.8em;
|
||||
}
|
||||
@@ -250,8 +274,8 @@
|
||||
.toggleRow,
|
||||
.sliderRow {
|
||||
display: grid;
|
||||
gap: 0.4em;
|
||||
padding: 0.58em;
|
||||
gap: 0.28em;
|
||||
padding: 0.4em;
|
||||
border: 1px solid color-mix(in srgb, var(--gc-border) 72%, transparent);
|
||||
background: rgb(5 10 14 / 54%);
|
||||
}
|
||||
@@ -337,12 +361,12 @@
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.45em;
|
||||
gap: 0.28em;
|
||||
}
|
||||
|
||||
.sliderControl button {
|
||||
width: 1.9em;
|
||||
height: 1.9em;
|
||||
width: 1.55em;
|
||||
height: 1.55em;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid color-mix(in srgb, var(--gc-border) 78%, transparent);
|
||||
@@ -350,7 +374,7 @@
|
||||
color: var(--gc-text);
|
||||
font: inherit;
|
||||
font-family: var(--gc-font-mono);
|
||||
font-size: 0.72em;
|
||||
font-size: 0.68em;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -426,6 +450,17 @@
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.axisLabels {
|
||||
left: 1.45em;
|
||||
bottom: 1.8em;
|
||||
}
|
||||
|
||||
.complexityOverlay {
|
||||
right: 1.45em;
|
||||
bottom: 4.4em;
|
||||
max-width: calc(100% - 2.9em);
|
||||
}
|
||||
|
||||
.controlPanel {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
|
||||
@@ -19,6 +19,7 @@ export default defineConfig(({ mode }) => ({
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM'
|
||||
},
|
||||
entryFileNames: 'diophantine-sphere.js',
|
||||
assetFileNames: 'assets/[name][extname]'
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user