This repository has been archived on 2024-07-22. You can view files and clone it, but cannot push or open issues or pull requests.
Doric/doric-web/src/shader/DoricViewNode.ts

745 lines
21 KiB
TypeScript
Raw Normal View History

2019-12-19 20:44:14 +08:00
import { DoricContext } from "../DoricContext";
import { acquireViewNode } from "../DoricRegistry";
2019-12-20 16:46:25 +08:00
export enum LayoutSpec {
EXACTLY = 0,
WRAP_CONTENT = 1,
AT_MOST = 2,
}
2019-12-20 16:46:25 +08:00
const SPECIFIED = 1
const START = 1 << 1
const END = 1 << 2
const SHIFT_X = 0
const SHIFT_Y = 4
export const LEFT = (START | SPECIFIED) << SHIFT_X
export const RIGHT = (END | SPECIFIED) << SHIFT_X
export const TOP = (START | SPECIFIED) << SHIFT_Y
export const BOTTOM = (END | SPECIFIED) << SHIFT_Y
export const CENTER_X = SPECIFIED << SHIFT_X
export const CENTER_Y = SPECIFIED << SHIFT_Y
export const CENTER = CENTER_X | CENTER_Y
export type FrameSize = {
width: number,
height: number,
}
2019-12-20 17:56:01 +08:00
export function toPixelString(v: number) {
2019-12-20 15:37:56 +08:00
return `${v}px`
}
2019-12-20 15:37:56 +08:00
2021-04-15 18:34:06 +08:00
export function pixelString2Number(v: string) {
return parseFloat(v.substring(0, v.indexOf("px")))
}
2019-12-20 17:56:01 +08:00
export function toRGBAString(color: number) {
2019-12-20 15:37:56 +08:00
let strs = []
for (let i = 0; i < 32; i += 8) {
strs.push(((color >> i) & 0xff).toString(16))
}
2019-12-20 15:37:56 +08:00
strs = strs.map(e => {
if (e.length === 1) {
return '0' + e
}
return e
}).reverse()
/// RGBA
return `#${strs[1]}${strs[2]}${strs[3]}${strs[0]}`
}
2019-12-20 15:37:56 +08:00
2019-12-19 20:44:14 +08:00
export type DoricViewNodeClass = { new(...args: any[]): {} }
export interface DVModel {
id: string,
type: string,
props: {
[index: string]: any
},
}
export abstract class DoricViewNode {
viewId = ""
viewType = "View"
context: DoricContext
2019-12-26 19:15:54 +08:00
superNode?: DoricSuperNode
layoutConfig = {
2019-12-19 20:44:14 +08:00
widthSpec: LayoutSpec.EXACTLY,
heightSpec: LayoutSpec.EXACTLY,
alignment: 0,
2019-12-19 20:44:14 +08:00
weight: 0,
margin: {
left: 0,
right: 0,
top: 0,
bottom: 0
}
}
2019-12-20 13:14:48 +08:00
padding = {
left: 0,
right: 0,
top: 0,
bottom: 0,
}
2019-12-20 15:37:56 +08:00
border?: {
width: number,
color: number,
}
2019-12-20 13:14:48 +08:00
frameWidth = 0
2019-12-20 15:37:56 +08:00
2019-12-20 13:14:48 +08:00
frameHeight = 0
2019-12-20 15:37:56 +08:00
offsetX = 0
offsetY = 0
2019-12-19 20:44:14 +08:00
view!: HTMLElement
2019-12-20 15:37:56 +08:00
2021-04-16 13:20:03 +08:00
transform: {
translateX?: number,
translateY?: number,
scaleX?: number,
scaleY?: number,
rotation?: number,
rotationX?: number,
rotationY?: number
} = {}
transformOrigin: { x: number, y: number } | undefined
2019-12-19 20:44:14 +08:00
constructor(context: DoricContext) {
this.context = context
}
2019-12-26 19:15:54 +08:00
init(superNode?: DoricSuperNode) {
2019-12-26 19:01:28 +08:00
if (superNode) {
this.superNode = superNode
2019-12-26 19:15:54 +08:00
if (this instanceof DoricSuperNode) {
2019-12-26 19:01:28 +08:00
this.reusable = superNode.reusable
}
2019-12-19 20:44:14 +08:00
}
this.view = this.build()
}
abstract build(): HTMLElement
2019-12-20 15:37:56 +08:00
get paddingLeft() {
return this.padding.left || 0
}
get paddingRight() {
return this.padding.right || 0
}
get paddingTop() {
return this.padding.top || 0
}
get paddingBottom() {
return this.padding.bottom || 0
}
get borderWidth() {
return this.border?.width || 0
}
2019-12-19 20:44:14 +08:00
blend(props: { [index: string]: any }) {
2019-12-26 17:09:03 +08:00
this.view.id = `${this.viewId}`
2019-12-19 20:44:14 +08:00
for (let key in props) {
this.blendProps(this.view, key, props[key])
}
2021-04-14 17:46:58 +08:00
this.onBlending()
this.layout()
}
2021-04-14 17:46:58 +08:00
onBlending() {
2021-04-16 13:20:03 +08:00
this.updateTransform()
2021-04-14 17:46:58 +08:00
}
onBlended() {
}
2021-04-14 17:46:58 +08:00
configBorder() {
2019-12-20 15:37:56 +08:00
if (this.border) {
this.view.style.borderStyle = "solid"
this.view.style.borderWidth = toPixelString(this.border.width)
this.view.style.borderColor = toRGBAString(this.border.color)
}
}
configWidth() {
switch (this.layoutConfig.widthSpec) {
case LayoutSpec.WRAP_CONTENT:
this.view.style.width = "max-content"
break
case LayoutSpec.AT_MOST:
this.view.style.width = "100%"
break
case LayoutSpec.EXACTLY:
default:
this.view.style.width = toPixelString(this.frameWidth
- this.paddingLeft - this.paddingRight
- this.borderWidth * 2)
break
}
}
configHeight() {
switch (this.layoutConfig.heightSpec) {
case LayoutSpec.WRAP_CONTENT:
this.view.style.height = "max-content"
break
case LayoutSpec.AT_MOST:
this.view.style.height = "100%"
break
case LayoutSpec.EXACTLY:
default:
this.view.style.height = toPixelString(this.frameHeight
- this.paddingTop - this.paddingBottom
- this.borderWidth * 2)
break
}
}
configMargin() {
if (this.layoutConfig.margin) {
this.view.style.marginLeft = toPixelString(this.layoutConfig.margin.left || 0)
this.view.style.marginRight = toPixelString(this.layoutConfig.margin.right || 0)
this.view.style.marginTop = toPixelString(this.layoutConfig.margin.top || 0)
this.view.style.marginBottom = toPixelString(this.layoutConfig.margin.bottom || 0)
}
}
configPadding() {
2019-12-20 15:37:56 +08:00
if (this.padding) {
this.view.style.paddingLeft = toPixelString(this.paddingLeft)
this.view.style.paddingRight = toPixelString(this.paddingRight)
this.view.style.paddingTop = toPixelString(this.paddingTop)
this.view.style.paddingBottom = toPixelString(this.paddingBottom)
}
2019-12-19 20:44:14 +08:00
}
2019-12-20 16:46:25 +08:00
layout() {
this.configMargin()
this.configBorder()
this.configPadding()
this.configWidth()
this.configHeight()
2019-12-20 16:46:25 +08:00
}
2019-12-19 20:44:14 +08:00
blendProps(v: HTMLElement, propName: string, prop: any) {
switch (propName) {
2019-12-20 15:37:56 +08:00
case "border":
this.border = prop
break
case "padding":
this.padding = prop
break
case 'width':
this.frameWidth = prop as number
break
case 'height':
this.frameHeight = prop as number
break
2019-12-19 20:44:14 +08:00
case 'backgroundColor':
this.backgroundColor = prop as number
break
case 'layoutConfig':
const layoutConfig = prop
2019-12-19 20:44:14 +08:00
for (let key in layoutConfig) {
Reflect.set(this.layoutConfig, key, Reflect.get(layoutConfig, key, layoutConfig))
}
break
2019-12-20 13:14:48 +08:00
case 'x':
2019-12-20 15:37:56 +08:00
this.offsetX = prop as number
2019-12-20 13:14:48 +08:00
break
case 'y':
2019-12-20 15:37:56 +08:00
this.offsetY = prop as number
2019-12-20 13:14:48 +08:00
break
2019-12-20 21:26:10 +08:00
case 'onClick':
2019-12-26 19:01:28 +08:00
this.view.onclick = (event: Event) => {
2019-12-20 21:26:10 +08:00
this.callJSResponse(prop as string)
2019-12-26 19:01:28 +08:00
event.stopPropagation()
2019-12-20 21:26:10 +08:00
}
break
2019-12-21 17:49:34 +08:00
case 'corners':
if (typeof prop === 'object') {
this.view.style.borderTopLeftRadius = toPixelString(prop.leftTop)
this.view.style.borderTopRightRadius = toPixelString(prop.rightTop)
this.view.style.borderBottomRightRadius = toPixelString(prop.rightBottom)
this.view.style.borderBottomLeftRadius = toPixelString(prop.leftBottom)
} else {
this.view.style.borderRadius = toPixelString(prop)
}
break
2019-12-21 18:04:12 +08:00
case 'shadow':
const opacity = prop.opacity || 0
if (opacity > 0) {
const offsetX = prop.offsetX || 0
const offsetY = prop.offsetY || 0
const shadowColor = prop.color || 0xff000000
const shadowRadius = prop.radius
const alpha = opacity * 255
this.view.style.boxShadow = `${toPixelString(offsetX)} ${toPixelString(offsetY)} ${toPixelString(shadowRadius)} ${toRGBAString((shadowColor & 0xffffff) | ((alpha & 0xff) << 24))} `
} else {
this.view.style.boxShadow = ""
}
break
2021-04-15 15:51:24 +08:00
case 'alpha':
this.view.style.opacity = `${prop}`
break
2021-04-16 13:20:03 +08:00
case 'rotation':
this.transform.rotation = prop
break
case 'pivotX':
if (this.transformOrigin) {
this.transformOrigin.x = prop
} else {
this.transformOrigin = {
x: prop,
y: 0.5,
}
}
break
case 'pivotY':
if (this.transformOrigin) {
this.transformOrigin.y = prop
} else {
this.transformOrigin = {
x: 0.5,
y: prop,
}
}
break
default:
console.error(`Cannot blend prop for ${propName}`)
break
2019-12-19 20:44:14 +08:00
}
}
set backgroundColor(v: number) {
2019-12-20 15:37:56 +08:00
this.view.style.backgroundColor = toRGBAString(v)
2019-12-19 20:44:14 +08:00
}
static create(context: DoricContext, type: string) {
const viewNodeClass = acquireViewNode(type)
if (viewNodeClass === undefined) {
console.error(`Cannot find ViewNode for ${type}`)
return undefined
}
const ret = new viewNodeClass(context) as DoricViewNode
ret.viewType = type
return ret
}
2019-12-20 16:46:25 +08:00
2019-12-20 21:26:10 +08:00
getIdList() {
const ids: string[] = []
let viewNode: DoricViewNode | undefined = this
do {
ids.push(viewNode.viewId)
viewNode = viewNode.superNode
} while (viewNode)
return ids.reverse()
}
callJSResponse(funcId: string, ...args: any) {
const argumentsList: any = ['__response__', this.getIdList(), funcId]
for (let i = 1; i < arguments.length; i++) {
argumentsList.push(arguments[i])
}
2019-12-28 14:25:03 +08:00
return Reflect.apply(this.context.invokeEntityMethod, this.context, argumentsList)
2019-12-20 21:26:10 +08:00
}
pureCallJSResponse(funcId: string, ...args: any) {
const argumentsList: any = ['__response__', this.getIdList(), funcId]
for (let i = 1; i < arguments.length; i++) {
argumentsList.push(arguments[i])
}
return Reflect.apply(this.context.pureInvokeEntityMethod, this.context, argumentsList)
}
2021-04-15 15:51:24 +08:00
2021-04-16 13:20:03 +08:00
updateTransform() {
this.view.style.transform = Object.entries(this.transform).filter((e: [string, number?]) => !!e[1]).map((e: [string, number?]) => {
const v = e[1] || 0
switch (e[0]) {
case "translateX":
return `translateX(${v}px)`
case "scaleX":
return `scaleX(${v})`
case "scaleY":
return `scaleY(${v})`
case "rotation":
return `rotate(${v / 2}turn)`
case "rotateX":
return `rotateX(${v / 2}turn)`
case "rotateY":
return `rotateY(${v / 2}turn)`
default:
console.error(`Do not support transform ${e[0]}`)
return ""
}
}).join(" ")
}
updateTransformOrigin() {
if (this.transformOrigin) {
this.view.style.transformOrigin = `${Math.round(this.transformOrigin.x * 100)}% ${Math.round(this.transformOrigin.y * 100)}%`
}
}
2021-04-15 15:51:24 +08:00
/** ++++++++++call from doric ++++++++++*/
getWidth() {
return this.view.offsetWidth
}
getHeight() {
return this.view.offsetHeight
}
setWidth(v: number) {
this.view.style.width = toPixelString(v)
}
setHeight(v: number) {
this.view.style.height = toPixelString(v)
}
getX() {
return this.view.offsetLeft
}
getY() {
return this.view.offsetTop
}
setX(v: number) {
this.view.style.left = toPixelString(v)
}
setY(v: number) {
this.view.style.top = toPixelString(v)
}
getBackgroundColor() {
return this.view.style.backgroundColor
}
setBackgroundColor(v: number) {
this.backgroundColor = v
}
getAlpha() {
2021-04-15 18:34:06 +08:00
return parseFloat(this.view.style.opacity)
2021-04-15 15:51:24 +08:00
}
setAlpha(v: number) {
this.view.style.opacity = `${v}`
}
getCorners() {
2021-04-15 18:34:06 +08:00
return parseFloat(this.view.style.borderRadius)
2021-04-15 15:51:24 +08:00
}
setCorners(v: number) {
this.view.style.borderRadius = toPixelString(v)
}
getLocationOnScreen() {
const rect = this.view.getClientRects()[0]
return {
x: rect.left,
y: rect.top,
}
}
2021-04-16 13:20:03 +08:00
getRotation() {
return this.transform.rotation
}
setRotation(v: number) {
this.transform.rotation = v
this.updateTransform()
}
getRotationX() {
return this.transform.rotationX
}
setRotationX(v: number) {
this.transform.rotationX = v
this.updateTransform()
}
getRotationY() {
return this.transform.rotationY
}
setRotationY(v: number) {
this.transform.rotationY = v
this.updateTransform()
}
getTranslationX() {
return this.transform.translateX
}
setTranslationX(v: number) {
this.transform.translateX = v
this.updateTransform()
}
getTranslationY() {
return this.transform.translateY
}
setTranslationY(v: number) {
this.transform.translateY = v
this.updateTransform()
}
getScaleX() {
return this.transform.scaleX
}
setScaleX(v: number) {
this.transform.scaleX = v
this.updateTransform()
}
getScaleY() {
return this.transform.scaleY
}
setScaleY(v: number) {
this.transform.scaleY = v
this.updateTransform()
}
getPivotX() {
return this.transformOrigin?.x || 0.5
}
setPivotX(v: number) {
if (this.transformOrigin) {
this.transformOrigin.x = v
} else {
this.transformOrigin = {
x: v,
y: 0.5,
}
}
this.updateTransform()
}
getPivotY() {
return this.transformOrigin?.y || 0.5
}
setPivotY(v: number) {
if (this.transformOrigin) {
this.transformOrigin.y = v
} else {
this.transformOrigin = {
x: 0.5,
y: v,
}
}
this.updateTransform()
}
2021-04-15 15:51:24 +08:00
/** ----------call from doric ----------*/
2019-12-19 20:44:14 +08:00
}
2019-12-26 19:15:54 +08:00
export abstract class DoricSuperNode extends DoricViewNode {
2019-12-19 20:44:14 +08:00
reusable = false
subModels: Map<String, DVModel> = new Map
blendProps(v: HTMLElement, propName: string, prop: any) {
if (propName === 'subviews') {
if (prop instanceof Array) {
prop.forEach((e: DVModel) => {
this.mixinSubModel(e)
this.blendSubNode(e)
})
}
} else {
super.blendProps(v, propName, prop)
}
}
mixinSubModel(subNode: DVModel) {
const oldValue = this.getSubModel(subNode.id)
if (oldValue) {
this.mixin(subNode, oldValue)
} else {
this.subModels.set(subNode.id, subNode)
}
}
getSubModel(id: string) {
return this.subModels.get(id)
}
mixin(src: DVModel, target: DVModel) {
for (let key in src.props) {
if (key === "subviews") {
continue
}
Reflect.set(target.props, key, Reflect.get(src.props, key))
}
}
clearSubModels() {
this.subModels.clear()
}
removeSubModel(id: string) {
this.subModels.delete(id)
}
abstract blendSubNode(model: DVModel): void
2019-12-21 16:57:07 +08:00
abstract getSubNodeById(viewId: string): DoricViewNode | undefined
2019-12-19 20:44:14 +08:00
}
2019-12-26 19:15:54 +08:00
export abstract class DoricGroupViewNode extends DoricSuperNode {
2019-12-19 20:44:14 +08:00
childNodes: DoricViewNode[] = []
childViewIds: string[] = []
2019-12-26 19:15:54 +08:00
init(superNode?: DoricSuperNode) {
super.init(superNode)
this.view.style.overflow = "hidden"
}
2019-12-19 20:44:14 +08:00
blendProps(v: HTMLElement, propName: string, prop: any) {
if (propName === 'children') {
if (prop instanceof Array) {
this.childViewIds = prop
}
} else {
super.blendProps(v, propName, prop)
}
}
blend(props: { [index: string]: any }) {
super.blend(props)
}
2021-04-15 11:35:58 +08:00
2021-04-14 17:46:58 +08:00
onBlending() {
super.onBlending()
2019-12-19 20:44:14 +08:00
this.configChildNode()
}
2021-04-15 11:35:58 +08:00
onBlended() {
super.onBlended()
this.childNodes.forEach(e => e.onBlended())
}
2019-12-19 20:44:14 +08:00
configChildNode() {
this.childViewIds.forEach((childViewId, index) => {
const model = this.getSubModel(childViewId)
if (model === undefined) {
return
}
if (index < this.childNodes.length) {
const oldNode = this.childNodes[index]
if (oldNode.viewId === childViewId) {
//The same,skip
} else {
if (this.reusable) {
if (oldNode.viewType === model.type) {
//Same type,can be reused
oldNode.viewId = childViewId
oldNode.blend(model.props)
} else {
//Replace this view
this.view.removeChild(oldNode.view)
const newNode = DoricViewNode.create(this.context, model.type)
if (newNode === undefined) {
return
}
newNode.viewId = childViewId
newNode.init(this)
newNode.blend(model.props)
this.childNodes[index] = newNode
this.view.replaceChild(newNode.view, oldNode.view)
}
} else {
//Find in remain nodes
let position = -1
for (let start = index + 1; start < this.childNodes.length; start++) {
if (childViewId === this.childNodes[start].viewId) {
//Found
position = start
break
}
}
if (position >= 0) {
//Found swap idx,position
const reused = this.childNodes[position]
const abandoned = this.childNodes[index]
this.childNodes[index] = reused
this.childNodes[position] = abandoned
this.view.removeChild(reused.view)
this.view.insertBefore(reused.view, abandoned.view)
this.view.removeChild(abandoned.view)
if (position === this.view.childElementCount - 1) {
this.view.appendChild(abandoned.view)
} else {
this.view.insertBefore(abandoned.view, this.view.children[position])
}
} else {
//Not found,insert
const newNode = DoricViewNode.create(this.context, model.type)
if (newNode === undefined) {
return
}
newNode.viewId = childViewId
newNode.init(this)
newNode.blend(model.props)
this.childNodes[index] = newNode
this.view.insertBefore(newNode.view, this.view.children[index])
}
}
}
} else {
//Insert
const newNode = DoricViewNode.create(this.context, model.type)
if (newNode === undefined) {
return
}
newNode.viewId = childViewId
newNode.init(this)
newNode.blend(model.props)
this.childNodes.push(newNode)
this.view.appendChild(newNode.view)
}
})
let size = this.childNodes.length
for (let idx = this.childViewIds.length; idx < size; idx++) {
this.view.removeChild(this.childNodes[idx].view)
}
this.childNodes = this.childNodes.slice(0, this.childViewIds.length)
}
blendSubNode(model: DVModel) {
2019-12-20 13:14:48 +08:00
this.getSubNodeById(model.id)?.blend(model.props)
2019-12-19 20:44:14 +08:00
}
getSubNodeById(viewId: string) {
return this.childNodes.filter(e => e.viewId === viewId)[0]
}
}