fragment-shader
Advanced tools
Comparing version 0.1.7 to 0.2.0
@@ -1,283 +0,64 @@ | ||
import Shader from './Shader'; | ||
import { EditorView, basicSetup } from 'codemirror'; | ||
import { EditorViewConfig, ViewUpdate, keymap } from '@codemirror/view'; | ||
import { EditorState } from '@codemirror/state'; | ||
import { clike } from '@codemirror/legacy-modes/mode/clike'; | ||
import { defaultKeymap, indentWithTab } from '@codemirror/commands'; | ||
import { StreamLanguage } from '@codemirror/language'; | ||
import { createStyleSheet } from '../util/dom'; | ||
import { formatShadertoySource } from '../util/shadertoy'; | ||
import { oneDark } from '@codemirror/theme-one-dark'; | ||
import { BLOCKS, KEYWORDS, MATH, TYPES, RAW_UTILS } from '../constants/glsl'; | ||
import { DEFAULT_FRAGMENT_SHADER, DEFAULT_UNIFORMS } from '../constants/shader'; | ||
import { type UniformValue } from '../types/shader'; | ||
import { createShaderEditor } from '../util/editor'; | ||
import { type EditorConfig } from '../types/editor'; | ||
import Shader, { DEFAULT_SHADER_CONFIG } from './Shader'; | ||
import CodeEditor from './CodeEditor'; | ||
import '../css/editor.css'; | ||
import { UniformValue } from '../types/shader'; | ||
const buildAtoms = (uniforms: UniformValue[]) => ({ | ||
...uniforms.reduce((acc: any, uniform: any) => { | ||
acc[uniform[0]] = true; | ||
return acc; | ||
}, {}), | ||
...Object.keys(RAW_UTILS).reduce((acc: any, key: string) => { | ||
acc[key] = true; | ||
return acc; | ||
}, {}), | ||
}); | ||
const defineLanguageDetails = (uniforms: UniformValue[]) => | ||
clike({ | ||
name: 'FragmentShader', | ||
types: TYPES, | ||
atoms: buildAtoms(uniforms as any), | ||
builtin: MATH, | ||
keywords: KEYWORDS, | ||
blockKeywords: BLOCKS, | ||
}); | ||
const DEFAULT_CONFIG = { | ||
target: document.body, | ||
shader: DEFAULT_FRAGMENT_SHADER, | ||
uniforms: DEFAULT_UNIFORMS, | ||
showErrors: true, | ||
onError: () => {}, | ||
onSuccess: () => {}, | ||
const DEFAULT_EDITOR_CONFIG: EditorConfig = { | ||
...DEFAULT_SHADER_CONFIG, | ||
debug: true, | ||
onUpdate: () => {}, | ||
width: window.innerWidth, | ||
height: window.innerHeight, | ||
dpr: window.devicePixelRatio, | ||
fillViewport: true, | ||
showLineNumbers: true, | ||
}; | ||
const errorStylesheet = createStyleSheet(); | ||
let errorStyleTimeout = 0; | ||
function setErrorStyle({ | ||
message, | ||
line, | ||
}: { | ||
message: string; | ||
line: string; | ||
}): void { | ||
clearTimeout(errorStyleTimeout); | ||
errorStyleTimeout = setTimeout(() => { | ||
errorStylesheet.textContent = /*css*/ ` | ||
.cm-line:nth-child(${line}) { | ||
position: relative; | ||
outline: 1px solid var(--red); | ||
} | ||
.cm-gutterElement:nth-child(${line + 1}) { | ||
background: var(--red); | ||
transition: all var(--hover-duration) var(--easing); | ||
outline: 1px solid var(--red); | ||
} | ||
.cm-line:nth-child(${line}):after { | ||
transition: all var(--hover-duration) var(--easing); | ||
content: "${message}"; | ||
position: absolute; | ||
top: 0; | ||
left: 100%; | ||
background: var(--red); | ||
color: var(--white); | ||
z-index: 100; | ||
padding: 0 .5rem; | ||
outline: 1px solid var(--red); | ||
} | ||
`; | ||
}, 500); | ||
} | ||
function hideError() { | ||
clearTimeout(errorStyleTimeout); | ||
errorStylesheet.textContent = ''; | ||
} | ||
export default class Editor { | ||
private editorView: EditorView | undefined; | ||
private config: EditorConfig; | ||
private editor: CodeEditor; | ||
private shader: Shader; | ||
private uniforms: UniformValue[]; | ||
private config: EditorConfig; | ||
private _onUpdate: Function | undefined; | ||
private container: HTMLElement; | ||
constructor( | ||
configOrShader: EditorConfig | string = DEFAULT_CONFIG, | ||
config: EditorConfig = DEFAULT_CONFIG | ||
) { | ||
if (typeof configOrShader === 'string') { | ||
this.config = { | ||
...DEFAULT_CONFIG, | ||
...config, | ||
shader: configOrShader, | ||
}; | ||
} else { | ||
this.config = { | ||
...DEFAULT_CONFIG, | ||
...configOrShader, | ||
}; | ||
} | ||
constructor(config: EditorConfig = DEFAULT_EDITOR_CONFIG) { | ||
this.config = { | ||
...DEFAULT_EDITOR_CONFIG, | ||
...config, | ||
}; | ||
if ( | ||
this.config.width !== window.innerWidth || | ||
this.config.height !== window.innerHeight | ||
) { | ||
this.config.fillViewport = false; | ||
} | ||
const { target, shader, uniforms }: any = this.config; | ||
if (!target || !shader || !uniforms) | ||
throw new Error('Initialization error.'); | ||
this.container = document.createElement('section'); | ||
this.container.classList.add('editor'); | ||
if (!this.config.showLineNumbers) | ||
this.container.classList.add('no-line-numbers'); | ||
this.sizeContainer(); | ||
target?.appendChild(this.container); | ||
this.shader = new Shader({ | ||
target: this.container, | ||
shader, | ||
uniforms, | ||
debug: this.config.showErrors, | ||
onError: this.onError.bind(this), | ||
onSuccess: this.onSuccess.bind(this), | ||
width: this.config.width, | ||
height: this.config.height, | ||
dpr: this.config.dpr, | ||
fillViewport: false, | ||
this.onUpdate = this.onUpdate.bind(this); | ||
this.onError = this.onError.bind(this); | ||
this.editor = createShaderEditor({ | ||
...this.config, | ||
onUpdate: this.onUpdate, | ||
}); | ||
this._onUpdate = this.config.onUpdate; | ||
this.uniforms = [...uniforms] as UniformValue[]; | ||
this.createEditorView(); | ||
this.onPaste = this.onPaste.bind(this); | ||
this.sizeContainer = this.sizeContainer.bind(this); | ||
window.addEventListener('paste', this.onPaste); | ||
window.addEventListener('resize', this.sizeContainer); | ||
this.shader = new Shader({ ...this.config, onError: this.onError }); | ||
} | ||
sizeContainer() { | ||
const width = this.config.fillViewport | ||
? window.innerWidth | ||
: this.config.width || window.innerWidth; | ||
const height = this.config.fillViewport | ||
? window.innerHeight | ||
: this.config.height || window.innerHeight; | ||
this.container.style.position = 'relative'; | ||
this.container.style.width = width + 'px'; | ||
this.container.style.height = height + 'px'; | ||
this.container.style.overflow = 'hidden'; | ||
if (!this.shader) return; | ||
this.shader.size = { | ||
width, | ||
height, | ||
dpr: window.devicePixelRatio, | ||
}; | ||
onUpdate(val: string) { | ||
this.editor.hideError(); | ||
this.config?.onUpdate?.(val); | ||
this.shader.rebuild({ shader: val, uniforms: this.config.uniforms }); | ||
} | ||
onError({ line, message }: any) { | ||
this.config?.onError?.({ line, message }); | ||
setErrorStyle({ line, message }); | ||
onError({ line, message }: { line: number; message: string }) { | ||
this.editor.showError({ line, message }); | ||
} | ||
onSuccess() { | ||
this.config?.onSuccess?.(); | ||
hideError(); | ||
} | ||
onPaste({ clipboardData }: ClipboardEvent) { | ||
const string = clipboardData?.getData?.('text') || ''; | ||
const isShaderToy = string.indexOf('mainImage') !== -1; | ||
if (clipboardData === null || !isShaderToy) return; | ||
this.config.shader = formatShadertoySource(string); | ||
this.shader.rebuild({ | ||
shader: this.config.shader, | ||
uniforms: this.uniforms, | ||
}); | ||
this.editorView?.setState( | ||
this.createState(this.config.shader, this.uniforms) | ||
); | ||
} | ||
rebuild(sketch: { shader: string; uniforms?: UniformValue[] }) { | ||
const { shader = '', uniforms = [] } = sketch; | ||
this.config.shader = shader; | ||
this.config.uniforms = uniforms; | ||
this.shader.rebuild({ | ||
rebuild(config: { shader?: string; uniforms?: UniformValue[] } = {}) { | ||
const { | ||
shader, | ||
uniforms, | ||
}); | ||
}: { | ||
shader?: string; | ||
uniforms?: UniformValue[]; | ||
} = { | ||
shader: '', | ||
uniforms: [], | ||
...config, | ||
}; | ||
this.editorView?.setState(this.createState(shader, uniforms)); | ||
this.shader.rebuild({ shader, uniforms }); | ||
} | ||
setUniform(key: string, value: any) { | ||
this.shader.setUniform(key, value); | ||
} | ||
start() { | ||
this.shader.start(); | ||
} | ||
stop() { | ||
this.shader.stop(); | ||
} | ||
createState( | ||
shader: string = DEFAULT_FRAGMENT_SHADER, | ||
uniforms: UniformValue[] = DEFAULT_UNIFORMS | ||
): EditorState { | ||
return EditorState.create({ | ||
doc: shader.trim(), | ||
extensions: [ | ||
basicSetup, | ||
keymap.of([defaultKeymap as any, indentWithTab]), | ||
oneDark, | ||
StreamLanguage.define(defineLanguageDetails(uniforms)), | ||
EditorView.updateListener.of(this.update.bind(this)), | ||
], | ||
}); | ||
} | ||
createEditorView(): void { | ||
this.editorView?.destroy?.(); | ||
this.editorView = new EditorView({ | ||
parent: this.container, | ||
state: this.createState(this.config.shader, this.config.uniforms), | ||
} as EditorViewConfig); | ||
} | ||
update(e: ViewUpdate) { | ||
const { state, docChanged }: any = e; | ||
if (!docChanged) return; | ||
const shader = state.doc.toString(); | ||
this._onUpdate?.(shader); | ||
hideError(); | ||
this.shader.rebuild({ shader, uniforms: this.uniforms }); | ||
} | ||
destroy() { | ||
window.removeEventListener('paste', this.onPaste); | ||
this.editorView?.destroy(); | ||
this.shader?.destroy(); | ||
this.container.remove(); | ||
this.editor.destroy(); | ||
this.shader.destroy(); | ||
} | ||
} |
@@ -23,4 +23,4 @@ import Uniform from './Uniform'; | ||
const DEFAULT_CONFIG: ShaderConfig = { | ||
target: document.body, | ||
export const DEFAULT_SHADER_CONFIG: ShaderConfig = { | ||
parent: document.body, | ||
shader: DEFAULT_FRAGMENT_SHADER, | ||
@@ -53,8 +53,8 @@ uniforms: DEFAULT_UNIFORMS, | ||
constructor( | ||
configOrShader: ShaderConfig | string = DEFAULT_CONFIG, | ||
config: ShaderConfig = DEFAULT_CONFIG | ||
configOrShader: ShaderConfig | string = DEFAULT_SHADER_CONFIG, | ||
config: ShaderConfig = DEFAULT_SHADER_CONFIG | ||
) { | ||
if (typeof configOrShader === 'string') { | ||
this.config = { | ||
...DEFAULT_CONFIG, | ||
...DEFAULT_SHADER_CONFIG, | ||
...config, | ||
@@ -65,3 +65,3 @@ shader: configOrShader, | ||
this.config = { | ||
...DEFAULT_CONFIG, | ||
...DEFAULT_SHADER_CONFIG, | ||
...configOrShader, | ||
@@ -75,8 +75,9 @@ }; | ||
this.canvas = createCanvas(this.config.target); | ||
this.canvas = createCanvas(this.config.parent); | ||
if (this.config.fillViewport) { | ||
this.canvas.style.position = 'fixed'; | ||
this.canvas.style.position = 'absolute'; | ||
this.canvas.style.top = '0'; | ||
this.canvas.style.left = '0'; | ||
this.canvas.style.zIndex = '0'; | ||
} | ||
@@ -111,4 +112,5 @@ | ||
this.tick = this.tick.bind(this); | ||
this.setUniform = this.setUniform.bind(this); | ||
this.raf = raf(this.tick.bind(this)); | ||
this.raf = raf(this.tick); | ||
@@ -193,3 +195,3 @@ if (this.config.animate) { | ||
return uniforms.reduce((acc, uniform: any, i) => { | ||
return uniforms.reduce((acc, uniform: any) => { | ||
acc[uniform[0]] = new Uniform( | ||
@@ -304,2 +306,4 @@ this.ctx, | ||
this.start(); | ||
} else { | ||
requestAnimationFrame(this.tick); | ||
} | ||
@@ -306,0 +310,0 @@ } catch (e) { |
@@ -75,2 +75,4 @@ export const k_hue = /*glsl*/ ` | ||
export const RAW_UTIL_KEYS = Object.keys(RAW_UTILS); | ||
export const GLSL_UTILS = Object.keys(RAW_UTILS).reduce( | ||
@@ -84,14 +86,5 @@ (acc: string, key: string) => { | ||
const toKeyObject = (arr: any) => | ||
arr.reduce((acc: any, key: any) => ({ ...acc, [key]: true }), {}); | ||
export const BLOCKS = ['if', 'for', 'else', 'switch', 'while'] as any; | ||
export const BLOCKS = toKeyObject([ | ||
'if', | ||
'for', | ||
'else', | ||
'switch', | ||
'while', | ||
]) as any; | ||
export const MATH = toKeyObject([ | ||
export const MATH = [ | ||
'sin', | ||
@@ -105,5 +98,5 @@ 'cos', | ||
'exp', | ||
]) as any; | ||
] as any; | ||
export const KEYWORDS = toKeyObject([ | ||
export const KEYWORDS = [ | ||
'main', | ||
@@ -116,6 +109,5 @@ 'stream', | ||
'gl_FragColor', | ||
...Object.keys(GLSL_UTILS), | ||
]) as any; | ||
] as any; | ||
export const TYPES = toKeyObject([ | ||
export const TYPES = [ | ||
'attribute', | ||
@@ -224,2 +216,2 @@ 'bool', | ||
'void', | ||
]) as any; | ||
] as any; |
{ | ||
"name": "fragment-shader", | ||
"version": "0.1.7", | ||
"version": "0.2.0", | ||
"description": "A lightweight, performant WebGL fragment shader renderer + editor. ", | ||
@@ -27,2 +27,3 @@ "main": "index.js", | ||
"dependencies": { | ||
"@codemirror/autocomplete": "^6.7.1", | ||
"@codemirror/commands": "^6.2.4", | ||
@@ -29,0 +30,0 @@ "@codemirror/language": "^6.7.0", |
@@ -1,16 +0,8 @@ | ||
import type { UniformValue } from './shader'; | ||
import { StreamParser } from '@codemirror/language'; | ||
import type { ShaderConfig } from './shader'; | ||
export interface EditorConfig { | ||
target?: HTMLElement; | ||
shader?: string; | ||
uniforms?: UniformValue[]; | ||
showErrors?: boolean; | ||
onError?: Function; | ||
onSuccess?: Function; | ||
export interface EditorConfig extends ShaderConfig { | ||
onUpdate?: Function; | ||
width?: number; | ||
height?: number; | ||
dpr?: number; | ||
fillViewport?: boolean; | ||
showLineNumbers?: boolean; | ||
document?: string; | ||
streamParser?: StreamParser<any>; | ||
} |
export interface ShaderConfig { | ||
target?: HTMLElement; | ||
parent?: HTMLElement; | ||
shader?: string; | ||
@@ -4,0 +4,0 @@ uniforms?: any[]; |
Sorry, the diff of this file is not supported yet
22
1285
44281
8
+ Added@codemirror/autocomplete@6.18.6(transitive)