Skip to content

Commit

Permalink
Add transform to control.ts and unit tests (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
neverbiasu authored Jun 20, 2024
1 parent 7920441 commit 02172a0
Show file tree
Hide file tree
Showing 9 changed files with 585 additions and 15 deletions.
158 changes: 158 additions & 0 deletions fixtures/css/css-transform.xsml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<xsml>
<head>
<title>Spatial CSS Example (Basic)</title>
<style type="text/scss">
plane {
display: relative;
flex-direction: row;
row-gap: 50px;
padding: 50px;
}
plane#p1 {
background-color: red;
height: 1024px;
width: 1024px;
position: 0 0 0;
}
plane#p2 {
background-color: blue;
height: 1024px;
width: 1024px;
position: -2 0 0;
}
plane#p3 {
background-color: pink;
height: 1024px;
width: 1024px;
position: 2 0 0;
}
</style>
</head>
<space>
<plane id="p1">
<style type="text/css">
div {
background-color: #D3CDEB;
height: 1024px;
width: 1024px;
row-gap: 50px;
flex-direction: row;
padding: 50px;
}
img {
width: 25%;
height: 25%;
object-fit: cover;
position: relative;
}
p {
position: absolute;
text-align: center;
font-size: 60px;
height: 100%;
width: 100%;
color: #887797;
}
</style>
<div>
<img src="../textures/splatting.jpg" />
<p>interactive transform</p>
</div>
</plane>
<plane id="p2">
<style type="text/css">
div {
background-color: #D9DFF3;
height: 1024px;
width: 1024px;
row-gap: 50px;
flex-direction: row;
padding: 50px;
}
img {
width: 25%;
height: 25%;
object-fit: cover;
position: relative;
transform: rotate(45deg) translateX(10px);
}
p {
position: absolute;
text-align: center;
font-size: 60px;
height: 100%;
width: 100%;
color: #7085B1;
}
</style>
<div>
<img src="../textures/splatting.jpg" />
<p>nested transform</p>
</div>
</plane>
<plane id="p3">
<style type="text/css">
div {
background-color: #FFF8ED;
height: 1024px;
width: 1024px;
row-gap: 50px;
flex-direction: row;
padding: 50px;
}
div#d2 {
background-color: #F0CC89;
transform: rotate(20deg) translateX(200px);
}
img {
width: 25%;
height: 25%;
object-fit: cover;
position: relative;
transform: translateX(100px) rotate(25deg);
}
p {
position: absolute;
text-align: center;
font-size: 60px;
height: 100%;
width: 100%;
color: #D0A0A8;
}
</style>
<div id="d1">
<div id="d2">
<img src="../textures/splatting.jpg" id="img"/>
</div>
<p>parent transform</p>
</div>
</plane>
<script>
const plane = document.getElementById('p1');
const panel = plane.shadowRoot;
const div = panel.querySelector('div');
const img = div.querySelector('img');
div.addEventListener('mousemove', (event) => {
const mouseX = event.x;
const x = Math.round(mouseX);
img.style.transform = `translateX(${x - 512}px)`;
});

const plane3 = document.getElementById('p3');
const panel3 = plane3.shadowRoot;
const div1 = panel3.getElementById('d1');
const div2 = div1.querySelector('div');
const img2 = div2.querySelector('img');
const originMouseX = 0;
const originMouseY = 0;
div2.addEventListener('mousemove', (event) => {
const mouseX = event.x;
const mouseY = event.y
const x = Math.round(mouseX);
const y = Math.round(mouseY);
const angle = Math.round(Math.atan2(y - originMouseY, x - originMouseX ) * (180 / Math.PI));
img2.style.transform = `rotate(${angle}deg)`;
});
</script>
</space>
</xsml>
Binary file added fixtures/textures/splatting.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions src/living/cssom/parsers/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,37 @@ describe('implicitSetter', () => {
expect(decl.position).toBe('1 1 1');
});
});

describe('parseTransform', () => {
it('should parse translateX correctly', () => {
const result = parsers.parseTransform('translateX(10px)');
expect(result).toEqual([
new parsers.TranslationTransformFunction('translateX', ['10px'])
]);
});

it('should parse rotate correctly', () => {
const result = parsers.parseTransform('rotate(45deg)');
expect(result).toEqual([
new parsers.RotationTransformFunction('rotate', ['45deg'])
]);
});

it('should parse multiple transforms correctly', () => {
const result = parsers.parseTransform('translateX(10px) rotate(45deg)');
expect(result).toEqual([
new parsers.TranslationTransformFunction('translateX', ['10px']),
new parsers.RotationTransformFunction('rotate', ['45deg'])
]);
});

it('should process input function name error correctly', () => {
const result = parsers.parseTransform('translateM(10px)');
expect(result).toEqual([]);
});

it('should process input values error correctly', () => {
const result = parsers.parseTransform('translateX(10py)');
expect(result).toEqual([]);
});
});
130 changes: 130 additions & 0 deletions src/living/cssom/parsers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -950,3 +950,133 @@ export function shorthandSetter(
}
};
}

export class TransformFunction<Tv> {
name: string;
values: Array<Tv>;

constructor(name: string, values: Tv[] = []) {
this.name = name;
this.values = values;
}

static ProcessArgs<Tv>(
args: string[],
convert: (arg: string) => Tv
): Tv[] {
let values: Tv[] = [];
for (let arg of args) {
const value = convert(arg);
if (value === undefined) {
values.length = 0;
break;
} else {
values.push(value);
}
}
return values;
}

get valid(): boolean {
throw new Error('Method `valid` must be implemented in derived classes');
}

isRotation(): this is RotationTransformFunction {
return css3TransformFunctions['rotate'].names.includes(this.name);
}

isTranslation(): this is TranslationTransformFunction {
return css3TransformFunctions['translate'].names.includes(this.name);
}
}

export class TranslationTransformFunction extends TransformFunction<PropertyLengthValue> {
x: number;
y: number;
z: number;

constructor(name: string, args: string[]) {
const values = TransformFunction.ProcessArgs(args, toLengthStr);
super(name, values);
this.x = values[0]?.value?.['number'] ?? 0;
this.y = values[1]?.value?.['number'] ?? 0;
this.z = values[2]?.value?.['number'] ?? 0;
}

get valid(): boolean {
return this.values.length > 0 && this.values.length < 4;
}
}

export class RotationTransformFunction extends TransformFunction<PropertyAngleValue> {
angle: number;

constructor(name: string, args: string[]) {
const values = TransformFunction.ProcessArgs(args, toAngleStr);
super(name, values);
this.angle = values[0].value;
}

get valid(): boolean {
return this.values.length === 1;
}
}

const css3TransformFunctions = {
matrix: {
constructor: null,
names: ['matrix', 'matrix3d']
},
perspective: {
constructor: null,
names: ['perspective']
},
rotate: {
constructor: RotationTransformFunction,
names: ['rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ']
},
translate: {
constructor: TranslationTransformFunction,
names: ['translate', 'translate3d', 'translateX', 'translateY', 'translateZ']
},
scale: {
constructor: null,
names: [ 'scale', 'scale3d', 'scaleX', 'scaleY', 'scaleZ']
},
skew: {
constructor: null,
names: [ 'skew', 'skewX', 'skewY']
}
};
const allTransformFunctionNames = Object.values(css3TransformFunctions).flatMap(obj => obj.names);
const transformFunctionRegEx = new RegExp(`(${allTransformFunctionNames.join('|')})\\((\\w+)\\)`, 'g');

export type UnionTransformFunction = TranslationTransformFunction | RotationTransformFunction;

function addTransformFunctionToList(
list: UnionTransformFunction[],
transformFunctionName: string,
transformFunctionArgs: string[]
): UnionTransformFunction[] {
const transformFunctionData = Object.values(css3TransformFunctions)
.find(({ names }) => names.includes(transformFunctionName));
const constructor = transformFunctionData?.constructor;
if (constructor) {
const transformFunction = new constructor(transformFunctionName, transformFunctionArgs);
if (transformFunction.valid) {
list.push(transformFunction);
}
}
return list;
}

export function parseTransform(transformStr: string): UnionTransformFunction[] {
const list: UnionTransformFunction[] = [];
let parsedResult: string[] = [];
while ((parsedResult = transformFunctionRegEx.exec(transformStr)) !== null) {
const transformFunctionName: string = parsedResult[1];
const transformFunctionArgs: string[] = parsedResult[2].split(',').map(arg => arg.trim());
addTransformFunctionToList(list, transformFunctionName, transformFunctionArgs);
}
return list;
}
Loading

0 comments on commit 02172a0

Please sign in to comment.