Skip to content

Commit

Permalink
Merge pull request #553 from rtp-cgs/task/image-collage-organism
Browse files Browse the repository at this point in the history
TASK: Image collage with randomly positioned images
  • Loading branch information
jonnitto authored Feb 22, 2024
2 parents 12232e8 + 9f2b1c8 commit ee09081
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ prototype(Neos.Presentation:SquareIcon) < prototype(Neos.Fusion:Component) {
text = PropTypes:String
iconName = PropTypes:String
color = PropTypes:String
class = PropTypes:String
}

@private.backgroundColorClass = Neos.Fusion:Match {
Expand All @@ -27,7 +28,7 @@ prototype(Neos.Presentation:SquareIcon) < prototype(Neos.Fusion:Component) {
color = null

renderer = afx`
<div @if={props.iconName} class={['w-44 lg:w-56 lg:p-4 p-3 flex flex-col square-icon aspect-square text-white', private.backgroundColorClass]}>
<div @if={props.iconName} class={['w-44 lg:w-56 lg:p-4 p-3 flex flex-col square-icon aspect-square text-white', private.backgroundColorClass, props.class]}>
<Neos.Presentation:Icon name={props.iconName} class="w-8 lg:w-12 h-auto" />
<div @if={props.text} class="text mt-auto break-words hyphens-auto text-lg lg:text-2xl font-medium">{props.text}</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
prototype(Neos.Presentation:Module.ImageCollage) < prototype(Neos.Fusion:Component) {

@styleguide {
title = "Image Collage"
props {
headline.text = 'Projects created with Neos CMS'
description.text = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.'
squareIcons = Neos.Fusion:Map {
items = ${[
['Lorem ipsum', 'heart', 'purple'],
['dolor sit', 'circle', 'yellow'],
['amet, consetetur', 'clock', 'lightblue']
]}
itemName = 'item'
itemRenderer = Neos.Fusion:DataStructure {
text = ${item[0]}
iconName = ${item[1]}
color = ${item[2]}
}
}
images = Neos.Fusion:Map {
items = ${[
[ "Case 1", 800, 600, '#000000' ],
[ "Case 2", 1920, 1080, '#222222' ],
[ "Case 3", 600, 1024, '#444444' ],
[ "Case 4", 1080, 1920, '#666666' ],
[ "Case 5", 600, 600, '#888888' ],
[ "Case 6", 1440, 1024, '#AAAAAA' ],
[ "Case 7", 500, 400, '#CCCCCC' ]
]}
itemName = 'item'
itemRenderer = Neos.Fusion:DataStructure {
imageSource = Sitegeist.Kaleidoscope:DummyImageSource {
title = ${item[0]}
baseWidth = ${item[1]}
baseHeight = ${item[2]}
backgroundColor = ${item[3]}
}
alternativeText = ${'Case ' + props.item[0]}
}
}
}
}

@propTypes {
headline = PropTypes:DataStructure
description = PropTypes:DataStructure
squareIcons = PropTypes:Array {
type = PropTypes:DataStructure {
imageSource = PropTypes:InstanceOf {
type = '\\Sitegeist\\Kaleidoscope\\Domain\\ImageSourceInterface'
}
alternativeText = PropTypes:String
}
}
images = PropTypes:Array {
type = PropTypes:DataStructure {
text = PropTypes:String
iconName = PropTypes:String
color = PropTypes:String
}
}
}

renderer = afx`
<Neos.Presentation:Background variant="gradient" class="overflow-clip h-screen flex flex-col">
<div class="flex-initial">
<Neos.Presentation:Spacing size y>
<div class="grid md:grid-cols-2 items-center">
<Neos.Presentation:Headline {...props.headline} display="headline-lg" />
<Neos.Presentation:Paragraph {...props.description} display="lead" />
</div>
</Neos.Presentation:Spacing>
</div>
<div class="grow" x-data="collage"
"x-on:resize.window.debounce"="processElements">
<figure class="relative h-full n-spacing--size">
<Neos.Fusion:Loop items={props.squareIcons}>
<div class="atropos atropos-image-collage-item absolute z-10 opacity-0 transition-all overflow-visible">
<div class="atropos-scale">
<div class="atropos-rotate">
<div class="atropos-inner">
<Neos.Presentation:SquareIcon class="image-collage-item" {...item} />
</div>
</div>
</div>
</div>
</Neos.Fusion:Loop>
<Neos.Fusion:Loop items={props.images}>
<div class="atropos atropos-image-collage-item opacity-0 transition-all absolute">
<div class="atropos-scale">
<div class="atropos-rotate">
<div class="atropos-inner">
<Sitegeist.Kaleidoscope:Image
class="image-collage-item h-auto w-auto"
imageSource={item.imageSource}
alt={item.alternativeText}
srcset="160w, 320w, 480w,1x ,2x"
sizes="(min-width: 1440px) 480px, 33vw"
lazy={true}
/>
</div>
</div>
</div>
</div>
</Neos.Fusion:Loop>
</figure>
</div>

</Neos.Presentation:Background>
`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import url("atropos/atropos.css");

.atropos-active .atropos-shadow {
opacity: 0.5 !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import Alpine from 'alpinejs';
import Atropos from 'atropos';

// function that returns a random number
function getRandomNumber(min, max, substract = 0) {
return Math.round(min + Math.random() * (max - min) - substract);
}

// get the size of an element
function getSize(element) {
return { x: element.clientWidth, y: element.clientHeight };
}

Alpine.data('collage', () => ({
atropos: null,
figure: null,
positions: [],
rendered: [],
elements: [],
maxX: 0,
maxY: 0,
padding: 30,
objectMargin: 10,
maxAttempts: 50,
placeElement(element, size, type, attempts = 0) {
if (attempts >= this.maxAttempts) {
// console.error('Max attempts reached');
return;
}

const x = getRandomNumber(this.padding, this.maxX - this.padding, size.x / 2);
const y = getRandomNumber(this.padding, this.maxY - this.padding - size.y);

if (this.isOverlap(x, y, size, type)) {
attempts++;
this.placeElement(element, size, type, attempts);
return;
}

element.style.setProperty('left', x + 'px');
element.style.setProperty('top', y + 'px');
this.positions.push({ x, y, size, type });

// Push another element-box to prevent objects from different types to overlap entirely
this.positions.push({
x: x + size.x * 0.25,
y: y + size.y * 0.25,
size: { x: size.x / 2, y: size.y / 2 },
type: '*',
});

this.rendered.push(element);
element.classList.remove('opacity-0');
},
isOverlap(x, y, size, type) {
// return true if overlapping another element of the same type
for (const p of this.positions.filter((p) => p.type === '*' || p.type === type)) {
if (x - this.objectMargin > p.x + p.size.x || p.x > x + this.objectMargin + size.x) continue;
if (y - this.objectMargin > p.y + p.size.y || p.y > y + this.objectMargin + size.y) continue;
return true;
}

return false;
},
processElements() {
this.maxX = this.figure.clientWidth;
this.maxY = this.figure.clientHeight;
this.positions = [];
this.rendered = [];
this.elements.forEach((element) => {
element.classList.add('opacity-0');

// Get the inner image if it exists and set max height and width
let image = element.querySelector('img.image-collage-item');
image?.style.setProperty('max-width', this.maxX / 3 + 'px');
image?.style.setProperty('max-height', this.maxY / 3 + 'px');

if (!image || image.complete) {
// Element is not an image, or is already loaded; we can place
// it right away
this.placeElement(element, getSize(element), image ? 'img' : 'div');
} else {
// We need to wait for this image to load until we can place it
image.addEventListener('load', () => {
this.placeElement(element, getSize(element), 'img');
});
}
});
},
init() {
this.figure = this.$el.querySelector('figure');
this.elements = [...(this.figure?.querySelectorAll('.atropos-image-collage-item') ?? [])];

// Init atropos
this.$el.querySelectorAll('.atropos-image-collage-item').forEach((item) => {
Atropos({
el: item,
eventsEl: this.figure,
commonOrigin: false,

// SquareItems should elevate higher than image items
activeOffset: item.querySelector('img.image-collage-item')
? Math.random() * 20
: 50 + Math.random() * 10,
});
});

this.processElements();
},
}));
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import collapse from '@alpinejs/collapse';
import clipboard from '@ryangjchandler/alpine-clipboard';
import typewriter from '@marcreichel/alpine-typewriter';
import '../Molecule/LogoBar/LogoBar';
import '../Organism/ImageCollage';

// @ts-ignore
Alpine.plugin([anchor, clipboard, collapse, focus, intersect, typewriter]);
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@floating-ui/dom": "^1.6.3",
"@marcreichel/alpine-typewriter": "^1.2.0",
"@ryangjchandler/alpine-clipboard": "^2.3.0",
"alpinejs": "^3.13.5"
"alpinejs": "^3.13.5",
"atropos": "^2.0.2"
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,11 @@ async@^2.5.0:
dependencies:
lodash "^4.17.14"

atropos@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/atropos/-/atropos-2.0.2.tgz#8024e845487a69662b70fdb83f5e81039c934def"
integrity sha512-8f0u0hEOlBTWTSvzY17TcHuQjxUIpkTBq70/I4+UF5B43ORtOoRjm8TPBYEgLM8Ba9AWf6PDtkagbYoybdjaKg==

autoprefixer@^10.4.17:
version "10.4.17"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.17.tgz#35cd5695cbbe82f536a50fa025d561b01fdec8be"
Expand Down

0 comments on commit ee09081

Please sign in to comment.